1---
2/**
3 * "One line does three jobs": for each tool, the code you write to read
4 * and write one DynamoDB table from a Lambda, the config it deploys (IAM
5 * policy + env var), and what the function's code gets at runtime.
6 * Line colors show where each piece comes from: in Alchemy, one capability
7 * line produces its policy statement, the table name, and a typed client.
8 * SST's link over-grants; Pulumi and Terraform deploy the policy and env var
9 * you write line by line, and define no client.
10 */
11import { highlightTS } from "../marketing/highlightTS";
12
13// get / put: produced by that Alchemy capability line; both: the shared
14// env var; link: generated by SST's link; over: access the code never uses;
15// hand: SDK calls written by hand, unchecked; policy / env: IAM and env
16// config you define line by line.
17type Src = "get" | "put" | "both" | "link" | "over" | "hand" | "policy" | "env";
18type Lang = "ts" | "hcl" | "yaml";
19type Line = { text: string; src?: Src; write?: boolean; file?: boolean; lang?: Lang };
20
21type ToolSet = {
22 id: "alchemy" | "sst" | "pulumi" | "terraform";
23 code: Line[];
24 config: Line[];
25 runtime: Line[];
26};
27
28// Pulumi and Terraform deploy exactly the policy you write, so every config
29// line maps back to a line of code.
30const HAND_CONFIG = (arn: string, env: string): Line[] => [
31 { text: "# deployed config for api" },
32 { text: "Role:", src: "policy" },
33 { text: " Policy:", src: "policy" },
34 { text: ' Version: "2012-10-17"', src: "policy" },
35 { text: " Statement:", src: "policy" },
36 { text: " - Effect: Allow", src: "policy" },
37 { text: " Action: [dynamodb:GetItem, dynamodb:PutItem]", src: "policy" },
38 { text: ` Resource: ${arn}`, src: "policy" },
39 { text: "Environment:", src: "env" },
40 { text: " Variables:", src: "env" },
41 { text: ` ${env}`, src: "env" },
42];
43
44const NO_CLIENT: Line[] = [
45 { text: "// runtime: no client definition" },
46 { text: "// the handler builds its own SDK calls" },
47 { text: "// against process.env.TABLE_NAME" },
48];
49
50const SETS: ToolSet[] = [
51 {
52 id: "alchemy",
53 code: [
54 { text: "src/api.ts", file: true },
55 { text: "export default class Api extends Lambda.Function<Api>()(" },
56 { text: ' "Api",' },
57 { text: " { main: import.meta.url }," },
58 { text: " Effect.gen(function* () {" },
59 { text: ' const sessions = yield* DynamoDB.Table("Sessions");' },
60 { text: " const getSession = yield* DynamoDB.GetItem(sessions);", src: "get" },
61 { text: " const saveSession = yield* DynamoDB.PutItem(sessions);", src: "put", write: true },
62 { text: " return {" },
63 { text: " fetch: Effect.gen(function* () {" },
64 { text: " const { Item } = yield* getSession({ Key });" },
65 { text: " yield* saveSession({ Item: next });", write: true },
66 { text: " })," },
67 { text: " };" },
68 { text: " })," },
69 { text: ") {}" },
70 ],
71 config: [
72 { text: "# deployed config for Api" },
73 { text: "Role:" },
74 { text: " Policy:" },
75 { text: ' Version: "2012-10-17"' },
76 { text: " Statement:" },
77 { text: " - Effect: Allow", src: "get" },
78 { text: " Action: dynamodb:GetItem", src: "get" },
79 { text: " Resource: arn:…:table/api-sessions-8f2c", src: "get" },
80 { text: " - Effect: Allow", src: "put", write: true },
81 { text: " Action: dynamodb:PutItem", src: "put", write: true },
82 { text: " Resource: arn:…:table/api-sessions-8f2c", src: "put", write: true },
83 { text: "Environment:" },
84 { text: " Variables:" },
85 { text: " Sessions_tableName: api-sessions-8f2c", src: "both" },
86 ],
87 runtime: [
88 { text: "// runtime: typed clients" },
89 { text: "const getSession: (", src: "get" },
90 { text: " request: GetItemRequest,", src: "get" },
91 { text: ") => Effect<GetItemOutput, GetItemError>", src: "get" },
92 { text: "const saveSession: (", src: "put", write: true },
93 { text: " request: PutItemRequest,", src: "put", write: true },
94 { text: ") => Effect<PutItemOutput, PutItemError>", src: "put", write: true },
95 ],
96 },
97 {
98 id: "sst",
99 code: [
100 { text: "sst.config.ts", file: true },
101 { text: 'const sessions = new sst.aws.Dynamo("Sessions", { … });' },
102 { text: 'new sst.aws.Function("Api", {' },
103 { text: ' handler: "src/api.handler",' },
104 { text: " link: [sessions],", src: "link" },
105 { text: "});" },
106 { text: "src/api.ts", file: true },
107 { text: 'import { Resource } from "sst";' },
108 { text: "const client = new DynamoDBClient({});" },
109 { text: "export const handler = async () => {" },
110 { text: " const TableName = Resource.Sessions.name;", src: "link" },
111 { text: " const { Item } = await client.send(", src: "hand" },
112 { text: " new GetItemCommand({ TableName, Key }));", src: "hand" },
113 { text: " await client.send(", src: "hand" },
114 { text: " new PutItemCommand({ TableName, Item: next }));", src: "hand" },
115 { text: "};" },
116 ],
117 config: [
118 { text: "# deployed config for Api" },
119 { text: "Role:" },
120 { text: " Policy:" },
121 { text: ' Version: "2012-10-17"' },
122 { text: " Statement:" },
123 { text: " - Effect: Allow", src: "link" },
124 { text: " # every DynamoDB action, DeleteTable too", src: "over" },
125 { text: ' Action: "dynamodb:*"', src: "over" },
126 { text: " Resource:", src: "link" },
127 { text: " - arn:…:table/app-sessions", src: "link" },
128 { text: " - arn:…:table/app-sessions/*", src: "link" },
129 { text: "Environment:" },
130 { text: " Variables:" },
131 { text: ` SST_RESOURCE_Sessions: '{"name":"app-sessions",…}'`, src: "link" },
132 ],
133 runtime: [
134 { text: "// runtime" },
135 { text: "const TableName: string; // typed by link", src: "link" },
136 { text: "// no client definition: new DynamoDBClient()", src: "hand" },
137 { text: "// and send() any command, allowed or not", src: "hand" },
138 ],
139 },
140 {
141 id: "pulumi",
142 code: [
143 { text: "index.ts", file: true },
144 { text: 'const sessions = new aws.dynamodb.Table("sessions", { … });' },
145 { text: 'const role = new aws.iam.Role("api", { assumeRolePolicy: … });', src: "policy" },
146 { text: 'new aws.iam.RolePolicy("api", {', src: "policy" },
147 { text: " role: role.id,", src: "policy" },
148 { text: " policy: sessions.arn.apply((arn) => JSON.stringify({", src: "policy" },
149 { text: ' Version: "2012-10-17",', src: "policy" },
150 { text: " Statement: [{", src: "policy" },
151 { text: ' Effect: "Allow",', src: "policy" },
152 { text: ' Action: ["dynamodb:GetItem", "dynamodb:PutItem"],', src: "policy" },
153 { text: " Resource: arn,", src: "policy" },
154 { text: " }],", src: "policy" },
155 { text: " })),", src: "policy" },
156 { text: "});", src: "policy" },
157 { text: 'new aws.lambda.Function("api", {' },
158 { text: " role: role.arn," },
159 { text: " environment: {", src: "env" },
160 { text: " variables: { TABLE_NAME: sessions.name },", src: "env" },
161 { text: " },", src: "env" },
162 { text: "});" },
163 ],
164 config: HAND_CONFIG("arn:…:table/sessions-4d1a", "TABLE_NAME: sessions-4d1a"),
165 runtime: NO_CLIENT,
166 },
167 {
168 id: "terraform",
169 code: [
170 { text: "main.tf", file: true, lang: "hcl" },
171 { text: 'resource "aws_dynamodb_table" "sessions" { … }', lang: "hcl" },
172 { text: 'resource "aws_iam_role" "api" { assume_role_policy = … }', src: "policy", lang: "hcl" },
173 { text: 'resource "aws_iam_role_policy" "api" {', src: "policy", lang: "hcl" },
174 { text: " role = aws_iam_role.api.id", src: "policy", lang: "hcl" },
175 { text: " policy = jsonencode({", src: "policy", lang: "hcl" },
176 { text: ' Version = "2012-10-17"', src: "policy", lang: "hcl" },
177 { text: " Statement = [{", src: "policy", lang: "hcl" },
178 { text: ' Effect = "Allow"', src: "policy", lang: "hcl" },
179 { text: ' Action = ["dynamodb:GetItem", "dynamodb:PutItem"]', src: "policy", lang: "hcl" },
180 { text: " Resource = aws_dynamodb_table.sessions.arn", src: "policy", lang: "hcl" },
181 { text: " }]", src: "policy", lang: "hcl" },
182 { text: " })", src: "policy", lang: "hcl" },
183 { text: "}", src: "policy", lang: "hcl" },
184 { text: 'resource "aws_lambda_function" "api" {', lang: "hcl" },
185 { text: " role = aws_iam_role.api.arn", lang: "hcl" },
186 { text: " environment {", src: "env", lang: "hcl" },
187 { text: " variables = {", src: "env", lang: "hcl" },
188 { text: " TABLE_NAME = aws_dynamodb_table.sessions.name", src: "env", lang: "hcl" },
189 { text: " }", src: "env", lang: "hcl" },
190 { text: " }", src: "env", lang: "hcl" },
191 { text: "}", lang: "hcl" },
192 ],
193 config: HAND_CONFIG("arn:…:table/sessions", "TABLE_NAME: sessions"),
194 runtime: NO_CLIENT,
195 },
196];
197
198const esc = (t: string) => t.replace(/&/g, "&amp;").replace(/</g, "&lt;");
199const yaml = (t: string) => {
200 if (/^\s*#/.test(t)) return `<span class="alc-tok-c">${esc(t)}</span>`;
201 const m = t.match(/^(\s*)(- )?([A-Za-z_]\w*):(\s.*|)$/);
202 if (!m) {
203 const d = t.match(/^(\s*)- (.*)$/);
204 return d
205 ? `${d[1]}<span class="ol__dash">- </span><span class="ol__str">${esc(d[2]!)}</span>`
206 : esc(t);
207 }
208 const [, indent, dash = "", key, rest] = m;
209 const value = rest!.trim();
210 return (
211 indent +
212 (dash ? `<span class="ol__dash">${dash}</span>` : "") +
213 `<span class="ol__key">${esc(key!)}</span>:` +
214 (value ? ` <span class="ol__str">${esc(value)}</span>` : "")
215 );
216};
217const hcl = (t: string) =>
218 esc(t)
219 .replace(/("[^"]*")/g, '<span class="ol__str">$1</span>')
220 .replace(/^(\s*)(resource)\b/, '$1<span class="alc-tok-k">$2</span>')
221 .replace(/^(\s*)([A-Za-z_]\w*)(\s*=)/, '$1<span class="ol__key">$2</span>$3');
222const render = (l: Line, lang: Lang) =>
223 l.file ? esc(l.text) : (l.lang ?? lang) === "hcl" ? hcl(l.text) : lang === "yaml" ? yaml(l.text) : highlightTS(l.text);
224
225// Classes for a line, plus run edges so each colored block gets a little
226// breathing room above and below.
227const classes = (lines: Line[], i: number) => {
228 const l = lines[i]!;
229 return [
230 "ol__line",
231 l.src && `is-${l.src}`,
232 l.write && "is-write",
233 l.file && "is-file",
234 l.src && lines[i - 1]?.src !== l.src && "is-run-start",
235 l.src && lines[i + 1]?.src !== l.src && "is-run-end",
236 ];
237};
238const label = { alchemy: "Alchemy", sst: "SST", pulumi: "Pulumi", terraform: "Terraform" };
239---
240
241<figure class="ol" data-state="read" data-tool="alchemy">
242 <fieldset class="ol__modes">
243 <legend class="ol__sr">Show how each tool wires the function</legend>
244 <label>
245 <input type="radio" name="ol-state" value="alchemy-read" checked />
246 <span data-copy>Alchemy · read</span>
247 </label>
248 <label>
249 <input type="radio" name="ol-state" value="alchemy-write" />
250 <span data-copy>Alchemy · read + write</span>
251 </label>
252 <label>
253 <input type="radio" name="ol-state" value="sst" />
254 <span data-copy>SST</span>
255 </label>
256 <label>
257 <input type="radio" name="ol-state" value="pulumi" />
258 <span data-copy>Pulumi</span>
259 </label>
260 <label>
261 <input type="radio" name="ol-state" value="terraform" />
262 <span data-copy>Terraform</span>
263 </label>
264 </fieldset>
265
266 <div class="ol__sets">
267 {
268 SETS.map((set) => (
269 <div class:list={["ol__set", set.id === "alchemy" && "is-active"]} data-set={set.id}>
270 <div class="ol__stage">
271 <div class="ol__card ol__code">
272 <pre class="ol__pre" tabindex="0" aria-label={`${label[set.id]}: the code you write`}><code>{
273 set.code.map((l, i) => (
274 <span class:list={classes(set.code, i)} set:html={render(l, "ts")} />
275 ))
276 }</code></pre>
277 </div>
278 <div class="ol__outs">
279 <div class="ol__card">
280 <pre class="ol__pre" tabindex="0" aria-label={`${label[set.id]}: deployed IAM policy and environment variables`}><code>{
281 set.config.map((l, i) => (
282 <span class:list={classes(set.config, i)} set:html={render(l, "yaml")} />
283 ))
284 }</code></pre>
285 </div>
286 <div class="ol__card">
287 <pre class="ol__pre" tabindex="0" aria-label={`${label[set.id]}: what the function's code gets at runtime`}><code>{
288 set.runtime.map((l, i) => (
289 <span class:list={classes(set.runtime, i)} set:html={render(l, "ts")} />
290 ))
291 }</code></pre>
292 </div>
293 </div>
294 </div>
295
296 {set.id === "alchemy" && (
297 <>
298 <ul class="ol__legend" aria-label="Legend">
299 <li class="is-get" data-copy>
300 <code>GetItem</code> grants read and returns <code>getSession</code>
301 </li>
302 <li class="is-put is-write" data-copy>
303 <code>PutItem</code> grants write and returns <code>saveSession</code>
304 </li>
305 </ul>
306 <p class="ol__note" data-when="read" data-copy>
307 One line asks for read access. It grants exactly
308 <code>dynamodb:GetItem</code> on this table, injects the
309 table's name, and returns a typed client.
310 </p>
311 <p class="ol__note" data-when="write" data-copy>
312 Add <code>PutItem</code> and its statement, its env var, and
313 its client all follow. Remove a line and all three go with it.
314 </p>
315 </>
316 )}
317 {set.id === "sst" && (
318 <>
319 <ul class="ol__legend" aria-label="Legend">
320 <li class="is-link" data-copy>generated by <code>link</code></li>
321 <li class="is-over" data-copy>more access than the code uses</li>
322 <li class="is-hand" data-copy>written by hand, unchecked</li>
323 </ul>
324 <p class="ol__note" data-copy>
325 <code>link</code> writes the policy for you, but it grants <code>dynamodb:*</code> on the table, <code>DeleteTable</code> included, while the handler only calls <code>GetItem</code> and <code>PutItem</code>. There's no client definition, so the SDK calls are yours to keep in sync.
326 </p>
327 </>
328 )}
329 {(set.id === "pulumi" || set.id === "terraform") && (
330 <>
331 <ul class="ol__legend" aria-label="Legend">
332 <li class="is-policy" data-copy>IAM policy: every line is yours to write</li>
333 <li class="is-env" data-copy>env var: yours to wire</li>
334 </ul>
335 <p class="ol__note" data-copy>
336 {set.id === "pulumi" ? "Pulumi" : "Terraform"} deploys exactly
337 the policy you define: each statement, action, and ARN in the
338 config traces back to a line you wrote. There's no client
339 definition, so nothing checks your handler's SDK calls against
340 either.
341 </p>
342 </>
343 )}
344 </div>
345 ))
346 }
347 </div>
348 <p class="ol__sr" aria-live="polite" data-live></p>
349</figure>
350
351<script>
352 // Switches between tools (crossfade, with the height easing between
353 // them) and between Alchemy's read and read + write (lines slide in and
354 // out, handled in CSS).
355 const SWITCH_MS = 420;
356 for (const fig of document.querySelectorAll<HTMLElement>(".ol")) {
357 const wrap = fig.querySelector<HTMLElement>(".ol__sets")!;
358 const sets = [...fig.querySelectorAll<HTMLElement>(".ol__set")];
359 const live = fig.querySelector<HTMLElement>("[data-live]")!;
360 for (const s of sets) if (!s.classList.contains("is-active")) s.inert = true;
361 let timer = 0;
362
363 const announce = () => {
364 const active = sets.find((s) => s.classList.contains("is-active"))!;
365 const note = [...active.querySelectorAll<HTMLElement>(".ol__note")].find(
366 (n) => getComputedStyle(n).display !== "none",
367 );
368 live.textContent = note?.textContent?.replace(/\s+/g, " ").trim() ?? "";
369 };
370
371 fig.addEventListener("change", (e) => {
372 const value = (e.target as HTMLInputElement).value;
373 const [tool, mode] = value.split("-") as [string, string | undefined];
374 if (mode) fig.dataset.state = mode;
375 if (tool === fig.dataset.tool) {
376 announce();
377 return;
378 }
379 fig.dataset.tool = tool;
380 clearTimeout(timer);
381 wrap.style.height = `${wrap.offsetHeight}px`;
382 for (const s of sets) {
383 const on = s.dataset.set === tool;
384 s.classList.toggle("is-active", on);
385 s.inert = !on;
386 }
387 const next = sets.find((s) => s.dataset.set === tool)!;
388 requestAnimationFrame(() => {
389 wrap.classList.add("is-switching");
390 wrap.style.height = `${next.offsetHeight}px`;
391 timer = window.setTimeout(() => {
392 wrap.classList.remove("is-switching");
393 wrap.style.height = "";
394 }, SWITCH_MS);
395 });
396 announce();
397 });
398 }
399</script>
400
401<style>
402 .ol {
403 /* A quarter of a code line. */
404 --run-pad: calc(1.65em / 4);
405 --get: var(--alc-accent);
406 --put: #6da4b4;
407 --link: var(--alc-accent);
408 --hand: var(--alc-warn);
409 --over: var(--alc-danger);
410 --policy: var(--alc-warn);
411 --env: #a592d6;
412 margin: 0;
413 }
414 :global([data-theme="light"]) .ol {
415 --put: #3f7a8c;
416 --env: #6c55a8;
417 }
418 .ol__sr {
419 position: absolute;
420 width: 1px;
421 height: 1px;
422 overflow: hidden;
423 clip-path: inset(50%);
424 white-space: nowrap;
425 }
426
427 /* ---------- picker ---------- */
428 .ol__modes {
429 display: flex;
430 flex-wrap: wrap;
431 gap: 8px;
432 margin: 0 0 18px;
433 padding: 0;
434 border: 0;
435 }
436 .ol__modes label {
437 position: relative;
438 cursor: pointer;
439 }
440 .ol__modes input {
441 position: absolute;
442 inset: 0;
443 margin: 0;
444 opacity: 0;
445 cursor: pointer;
446 }
447 .ol__modes span {
448 display: inline-block;
449 padding: 6px 14px;
450 border: 1px dashed var(--alc-hairline-3);
451 border-radius: 999px;
452 font-size: 13px;
453 color: var(--alc-fg-2);
454 transition:
455 border-color 200ms ease,
456 background 200ms ease,
457 color 200ms ease;
458 }
459 .ol__modes label:hover span {
460 color: var(--alc-fg-1);
461 }
462 .ol__modes input:checked + span {
463 border-style: solid;
464 border-color: var(--alc-accent);
465 background: color-mix(in srgb, var(--alc-accent) 12%, transparent);
466 color: var(--alc-fg-1);
467 }
468 .ol__modes input:focus-visible + span {
469 outline: 2px solid var(--alc-accent);
470 outline-offset: 2px;
471 }
472
473 /* ---------- tool sets: crossfade with the height easing ---------- */
474 .ol__sets {
475 position: relative;
476 }
477 .ol__sets.is-switching {
478 transition: height 420ms cubic-bezier(0.2, 0, 0, 1);
479 }
480 .ol__set {
481 position: absolute;
482 top: 0;
483 left: 0;
484 right: 0;
485 opacity: 0;
486 visibility: hidden;
487 transition:
488 opacity 220ms ease,
489 visibility 0s linear 220ms;
490 }
491 .ol__set.is-active {
492 position: relative;
493 opacity: 1;
494 visibility: visible;
495 transition: opacity 320ms ease 120ms;
496 }
497
498 /* ---------- layout ---------- */
499 .ol__stage {
500 display: grid;
501 grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
502 column-gap: 24px;
503 align-items: center;
504 }
505 .ol__outs {
506 display: grid;
507 gap: 14px;
508 }
509
510 /* ---------- cards ---------- */
511 .ol__card {
512 min-width: 0;
513 border: 1px solid rgba(232, 220, 192, 0.1);
514 border-radius: 12px;
515 background: var(--alc-bg-code);
516 overflow: hidden;
517 }
518 .ol__pre {
519 margin: 0;
520 padding: 12px 0;
521 overflow-x: auto;
522 font-family: var(--alc-font-mono);
523 font-size: 12px;
524 line-height: 1.65;
525 color: var(--alc-code-var);
526 }
527 .ol__pre:focus-visible {
528 outline: 2px solid var(--alc-accent);
529 outline-offset: -2px;
530 }
531 .ol__line {
532 display: block;
533 max-height: 1.65em;
534 /* Clip only vertically (for the slide); long lines still scroll the
535 card sideways. */
536 overflow-x: visible;
537 overflow-y: clip;
538 padding: 0 16px;
539 white-space: pre;
540 transition:
541 max-height 380ms cubic-bezier(0.2, 0, 0, 1),
542 padding 380ms cubic-bezier(0.2, 0, 0, 1),
543 opacity 280ms ease,
544 background 300ms ease,
545 box-shadow 300ms ease;
546 }
547 /* File names inside the code card. */
548 .ol__line.is-file {
549 color: var(--alc-code-comment);
550 font-size: 11px;
551 }
552 .ol__line.is-file:not(:first-child) {
553 margin-top: 10px;
554 padding-top: 8px;
555 border-top: 1px solid rgba(232, 220, 192, 0.08);
556 max-height: none;
557 }
558 /* Colored runs in the outputs get a little room above and below. */
559 .ol__outs .ol__line.is-run-start {
560 padding-top: var(--run-pad);
561 }
562 .ol__outs .ol__line.is-run-end {
563 padding-bottom: var(--run-pad);
564 }
565 /* Lines that only exist with PutItem slide closed in the read state. */
566 .ol[data-state="read"] .ol__line.is-write {
567 max-height: 0;
568 padding-top: 0;
569 padding-bottom: 0;
570 opacity: 0;
571 }
572 .ol__line.is-get {
573 background: color-mix(in srgb, var(--get) 14%, transparent);
574 box-shadow: inset 3px 0 0 var(--get);
575 }
576 .ol__line.is-put {
577 background: color-mix(in srgb, var(--put) 16%, transparent);
578 box-shadow: inset 3px 0 0 var(--put);
579 }
580 .ol__line.is-link {
581 background: color-mix(in srgb, var(--link) 14%, transparent);
582 box-shadow: inset 3px 0 0 var(--link);
583 }
584 .ol__line.is-hand {
585 background: color-mix(in srgb, var(--hand) 12%, transparent);
586 box-shadow: inset 3px 0 0 var(--hand);
587 }
588 .ol__line.is-over {
589 background: color-mix(in srgb, var(--over) 16%, transparent);
590 box-shadow: inset 3px 0 0 var(--over);
591 }
592 .ol__line.is-policy {
593 background: color-mix(in srgb, var(--policy) 12%, transparent);
594 box-shadow: inset 3px 0 0 var(--policy);
595 }
596 .ol__line.is-env {
597 background: color-mix(in srgb, var(--env) 16%, transparent);
598 box-shadow: inset 3px 0 0 var(--env);
599 }
600 /* The env var: both capabilities inject it once PutItem is added. */
601 .ol__line.is-both {
602 background: linear-gradient(
603 90deg,
604 color-mix(in srgb, var(--get) 14%, transparent),
605 color-mix(in srgb, var(--put) 16%, transparent)
606 );
607 box-shadow:
608 inset 3px 0 0 var(--get),
609 inset 6px 0 0 var(--put);
610 }
611 .ol[data-state="read"] .ol__line.is-both {
612 background: color-mix(in srgb, var(--get) 14%, transparent);
613 box-shadow: inset 3px 0 0 var(--get);
614 }
615 .ol__key {
616 color: var(--alc-code-type);
617 }
618 .ol__str {
619 color: var(--alc-code-string);
620 }
621 .ol__dash {
622 color: var(--alc-code-comment);
623 }
624
625 /* ---------- legend + note ---------- */
626 .ol__legend {
627 display: flex;
628 flex-wrap: wrap;
629 gap: 8px 22px;
630 margin: 18px 0 0;
631 padding: 0;
632 list-style: none;
633 font-size: 13px;
634 color: var(--alc-fg-3);
635 }
636 .ol__legend li {
637 display: inline-flex;
638 align-items: center;
639 gap: 8px;
640 transition: opacity 280ms ease;
641 }
642 .ol[data-state="read"] .ol__legend li.is-write {
643 opacity: 0;
644 }
645 .ol__legend li::before {
646 content: "";
647 flex: none;
648 width: 12px;
649 height: 12px;
650 border-radius: 3px;
651 }
652 .ol__legend li.is-get::before {
653 background: var(--get);
654 }
655 .ol__legend li.is-put::before {
656 background: var(--put);
657 }
658 .ol__legend li.is-link::before {
659 background: var(--link);
660 }
661 .ol__legend li.is-hand::before {
662 background: var(--hand);
663 }
664 .ol__legend li.is-over::before {
665 background: var(--over);
666 }
667 .ol__legend li.is-policy::before {
668 background: var(--policy);
669 }
670 .ol__legend li.is-env::before {
671 background: var(--env);
672 }
673 .ol__legend code,
674 .ol__note code {
675 font-family: var(--alc-font-mono);
676 font-size: 0.92em;
677 color: var(--alc-fg-1);
678 }
679 .ol__note {
680 max-width: 680px;
681 margin: 10px 0 0;
682 font-size: 15px;
683 line-height: 1.6;
684 color: var(--alc-fg-2);
685 }
686 .ol[data-state="read"] .ol__note[data-when="write"],
687 .ol[data-state="write"] .ol__note[data-when="read"] {
688 display: none;
689 }
690
691 @media (max-width: 1000px) {
692 .ol__stage {
693 grid-template-columns: minmax(0, 1fr);
694 row-gap: 14px;
695 }
696 }
697 @media (max-width: 640px) {
698 .ol__pre {
699 font-size: 11px;
700 }
701 }
702</style>