bedrock-flows 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/auth-schema.sql +8 -0
  2. package/bin/bedrock-flows.mjs +127 -0
  3. package/lib/setup.mjs +262 -0
  4. package/package.json +11 -0
  5. package/template/.storybook/main.js +46 -0
  6. package/template/.storybook/manager-head.html +963 -0
  7. package/template/.storybook/preview-head.html +35 -0
  8. package/template/.storybook/preview.js +23 -0
  9. package/template/CHANGELOG.md +236 -0
  10. package/template/README.md +26 -0
  11. package/template/apps/dashboard/index.html +15 -0
  12. package/template/apps/dashboard/package.json +22 -0
  13. package/template/apps/dashboard/src/App.module.css +1318 -0
  14. package/template/apps/dashboard/src/App.tsx +2716 -0
  15. package/template/apps/dashboard/src/auth-client.ts +17 -0
  16. package/template/apps/dashboard/src/changelog.tsx +92 -0
  17. package/template/apps/dashboard/src/index.css +86 -0
  18. package/template/apps/dashboard/src/main.tsx +15 -0
  19. package/template/apps/dashboard/src/theme.ts +31 -0
  20. package/template/apps/dashboard/src/vite-env.d.ts +4 -0
  21. package/template/apps/dashboard/vite.config.ts +48 -0
  22. package/template/apps/worker/.dev.vars.example +50 -0
  23. package/template/apps/worker/package.json +19 -0
  24. package/template/apps/worker/src/index.ts +295 -0
  25. package/template/apps/worker/tsconfig.json +11 -0
  26. package/template/apps/worker/wrangler.jsonc +29 -0
  27. package/template/bedrock.config.ts +16 -0
  28. package/template/design-system/README.md +97 -0
  29. package/template/design-system/starter-v1/components/button/component.css +42 -0
  30. package/template/design-system/starter-v1/components/button/danger.html +21 -0
  31. package/template/design-system/starter-v1/components/button/default.html +21 -0
  32. package/template/design-system/starter-v1/components/button/disabled.html +21 -0
  33. package/template/design-system/starter-v1/components/button/ghost.html +21 -0
  34. package/template/design-system/starter-v1/components/button/macro.njk +14 -0
  35. package/template/design-system/starter-v1/components/button/primary.html +21 -0
  36. package/template/design-system/starter-v1/components/button/variants.json +30 -0
  37. package/template/design-system/starter-v1/ds.json +3 -0
  38. package/template/design-system/starter-v1/global.css +52 -0
  39. package/template/design-system/starter-v1/style.css +107 -0
  40. package/template/gitignore +8 -0
  41. package/template/package.json +41 -0
  42. package/template/prototypes/F-001-hello/1-welcome.njk +30 -0
  43. package/template/prototypes/F-001-hello/2-form.njk +46 -0
  44. package/template/prototypes/F-001-hello/3-done.njk +29 -0
  45. package/template/prototypes/F-001-hello/meta.json +6 -0
  46. package/template/prototypes/_shared/_auth-gate.njk +54 -0
  47. package/template/prototypes/_shared/delivery.njk +43 -0
  48. package/template/prototypes/_shared/layout.njk +15 -0
  49. package/template/prototypes/_shared/screen.njk +1818 -0
  50. package/template/prototypes/_shared/wireflow.njk +4731 -0
  51. package/template/public/auth-gate.css +150 -0
  52. package/template/public/bedrock/color-inspector.js +284 -0
  53. package/template/public/bedrock/component-overlay.js +219 -0
  54. package/template/public/bedrock/data/bedrock-config.js +45 -0
  55. package/template/public/bedrock/font-size-overlay.js +590 -0
  56. package/template/public/bedrock/grid-overlay.js +379 -0
  57. package/template/public/bedrock/prototype-navigation.js +974 -0
  58. package/template/public/cmdk.js +146 -0
  59. package/template/public/ds-xray.css +112 -0
  60. package/template/public/ds-xray.js +271 -0
  61. package/template/public/favicon.svg +4 -0
  62. package/template/public/icons/bolt-fill.svg +3 -0
  63. package/template/public/icons/bolt.svg +3 -0
  64. package/template/public/icons/caret-down-fill.svg +3 -0
  65. package/template/public/icons/check-double.svg +4 -0
  66. package/template/public/icons/check.svg +3 -0
  67. package/template/public/icons/chevron-left.svg +3 -0
  68. package/template/public/icons/chevron-right.svg +3 -0
  69. package/template/public/icons/circle-info.svg +6 -0
  70. package/template/public/icons/grid.svg +6 -0
  71. package/template/public/icons/message-square-1.svg +3 -0
  72. package/template/public/icons/message-square.svg +3 -0
  73. package/template/public/icons/messages.svg +4 -0
  74. package/template/public/icons/options-horizontal.svg +5 -0
  75. package/template/public/icons/swatches.svg +6 -0
  76. package/template/public/icons/workflow.svg +6 -0
  77. package/template/public/lightbox.js +87 -0
  78. package/template/public/proto-chrome.css +596 -0
  79. package/template/public/screen-comments.css +723 -0
  80. package/template/public/wireflow-client.js +26 -0
  81. package/template/scripts/build-storybooks.mjs +8 -0
  82. package/template/scripts/dev-setup.mjs +15 -0
  83. package/template/scripts/generate-stories.mjs +12 -0
  84. package/template/scripts/generate-variants.mjs +22 -0
  85. package/template/tsconfig.base.json +19 -0
@@ -0,0 +1,87 @@
1
+ // Lightbox — shared image viewer for the prototype harness (comment
2
+ // attachments in the screen/wireflow panels and the dashboard queue).
3
+ // Self-contained: injects its own styles, exposes window.bfLightbox.open(url,
4
+ // alt, caption). Click backdrop / press Esc / click × to close; the image fits the
5
+ // viewport and stays crisp at its native size when smaller. Top window only.
6
+ (function () {
7
+ if (window.self !== window.top) return;
8
+ if (window.bfLightbox) return;
9
+
10
+ var ov = null;
11
+ var imgEl = null;
12
+ var capEl = null;
13
+ var prevOverflow = '';
14
+
15
+ function injectStyle() {
16
+ if (document.getElementById('bf-lightbox-style')) return;
17
+ var s = document.createElement('style');
18
+ s.id = 'bf-lightbox-style';
19
+ s.textContent =
20
+ '.bf-lightbox{position:fixed;inset:0;z-index:100001;display:none;align-items:center;justify-content:center;background:rgba(8,11,18,.82);backdrop-filter:blur(2px);cursor:zoom-out}' +
21
+ '.bf-lightbox.is-open{display:flex}' +
22
+ '.bf-lightbox img{max-width:94vw;max-height:80vh;display:block;border-radius:4px;box-shadow:0 24px 80px rgba(0,0,0,.5);background:#fff;cursor:default}' +
23
+ '.bf-lightbox__close{position:fixed;top:14px;right:14px;width:36px;height:36px;border:0;border-radius:999px;background:rgba(255,255,255,.12);color:#fff;font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}' +
24
+ '.bf-lightbox__close:hover{background:rgba(255,255,255,.24)}' +
25
+ '.bf-lightbox__figure{display:flex;flex-direction:column;align-items:center;gap:12px;max-width:94vw;cursor:default}' +
26
+ '.bf-lightbox__caption{max-width:70ch;max-height:22vh;overflow-y:auto;padding:8px 14px;border-radius:8px;background:rgba(255,255,255,.08);color:#e7ecf3;font:13px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;text-align:center;white-space:pre-wrap}' +
27
+ '.bf-lightbox__caption[hidden]{display:none}';
28
+ document.head.appendChild(s);
29
+ }
30
+
31
+ function build() {
32
+ injectStyle();
33
+ ov = document.createElement('div');
34
+ ov.className = 'bf-lightbox';
35
+ ov.setAttribute('role', 'dialog');
36
+ ov.setAttribute('aria-modal', 'true');
37
+ ov.setAttribute('aria-label', 'Image preview');
38
+ var figure = document.createElement('div');
39
+ figure.className = 'bf-lightbox__figure';
40
+ imgEl = document.createElement('img');
41
+ imgEl.alt = '';
42
+ capEl = document.createElement('div');
43
+ capEl.className = 'bf-lightbox__caption';
44
+ capEl.hidden = true;
45
+ var close = document.createElement('button');
46
+ close.type = 'button';
47
+ close.className = 'bf-lightbox__close';
48
+ close.setAttribute('aria-label', 'Close image preview');
49
+ close.textContent = '×';
50
+ close.addEventListener('click', closeBox);
51
+ figure.appendChild(imgEl);
52
+ figure.appendChild(capEl);
53
+ ov.appendChild(figure);
54
+ ov.appendChild(close);
55
+ // Backdrop click closes; clicking the image itself does not.
56
+ ov.addEventListener('click', function (e) {
57
+ if (e.target === ov) closeBox();
58
+ });
59
+ document.addEventListener('keydown', function (e) {
60
+ if (e.key === 'Escape' && ov.classList.contains('is-open')) {
61
+ e.preventDefault();
62
+ closeBox();
63
+ }
64
+ });
65
+ document.body.appendChild(ov);
66
+ }
67
+
68
+ function openBox(url, alt, caption) {
69
+ if (!ov) build();
70
+ imgEl.src = url;
71
+ imgEl.alt = alt || 'Attached image';
72
+ capEl.textContent = caption || '';
73
+ capEl.hidden = !caption;
74
+ prevOverflow = document.documentElement.style.overflow;
75
+ document.documentElement.style.overflow = 'hidden'; // scroll lock
76
+ ov.classList.add('is-open');
77
+ }
78
+
79
+ function closeBox() {
80
+ if (!ov) return;
81
+ ov.classList.remove('is-open');
82
+ imgEl.src = '';
83
+ document.documentElement.style.overflow = prevOverflow;
84
+ }
85
+
86
+ window.bfLightbox = { open: openBox, close: closeBox };
87
+ })();
@@ -0,0 +1,596 @@
1
+ /* Phone-frame containment for toast/snackbar — production uses
2
+ position:fixed (correct for a real device viewport), but inside the
3
+ 402×874 phone preview that lands at the BROWSER viewport bottom-right,
4
+ off the phone. Scope the override to `.is-phone-frame` (set on <html>
5
+ by screen.njk when phoneFrame=true) so production behaviour is
6
+ untouched. */
7
+ html.is-phone-frame body { position: relative; }
8
+ html.is-phone-frame .toast,
9
+ html.is-phone-frame .snackbar { position: absolute !important; }
10
+
11
+ /*
12
+ * Prototype chrome — top + bottom harness bars that wrap every prototype
13
+ * sub-page. Not part of any DS component; harness scaffolding only.
14
+ *
15
+ * Chrome is hidden by default on every sub-page (iframed or standalone) so
16
+ * the screen looks like the real product. Press `.` to toggle visibility,
17
+ * `Esc` to hide. A tiny dot in the bottom-left acts as a discoverable
18
+ * affordance and click target.
19
+ *
20
+ * The legacy `.is-embedded` class is still added when the page is iframed
21
+ * (kept for future use); the default-hidden state already covers it.
22
+ *
23
+ * Layout note: when visible, the bars sit in normal flow at the top and
24
+ * bottom of <body> and *push the page content down/up* — they no longer
25
+ * overlay it. This keeps fixed elements inside the prototype (sidebars,
26
+ * headers) from being clipped by the harness bars when the designer
27
+ * presses `.` to inspect them. <body> is a flex column with a min-height
28
+ * of 100vh so the inner content still fills the viewport when chrome is
29
+ * hidden.
30
+ */
31
+
32
+ /* === Body layout ======================================================== */
33
+
34
+ body {
35
+ min-height: 100vh;
36
+ display: flex;
37
+ flex-direction: column;
38
+ margin: 0;
39
+ }
40
+ /* Whatever sits between the two bars stretches to fill the space. The
41
+ `> :not(...)` selector is intentionally broad — any direct child that
42
+ isn't the harness chrome behaves like the page content slot. */
43
+ /*
44
+ * Default growth behavior for direct body children: fill the column. The
45
+ * base rule uses a low-specificity universal selector so per-class
46
+ * overrides below can win without resorting to !important. The harness
47
+ * chrome bars opt out via their own class rule.
48
+ */
49
+ body > * {
50
+ flex: 1 1 auto;
51
+ min-height: 0;
52
+ }
53
+ .proto-topbar,
54
+ .proto-bottombar { flex: 0 0 auto; }
55
+
56
+ /* These blocks are now inside .proto-content, so no per-class flex
57
+ overrides are needed. .proto-content itself fills the gap between the
58
+ harness bars; its children stack at natural height by default. */
59
+
60
+ /* === Stage wrapper ===================================================== *
61
+ * `.proto-stage` wraps the topbar, the page content slot, and the
62
+ * bottombar. Body is the outer flex column (above), but the same logic
63
+ * needs to apply *inside* the stage so the content slot fills the gap
64
+ * between the two harness bars. Without this, pages whose content is
65
+ * shorter than the viewport leave whitespace below the bottombar — the
66
+ * sticky `bottom: 0` doesn't help when the bar is already at its
67
+ * natural document position with room to spare beneath. */
68
+ .proto-stage {
69
+ display: flex;
70
+ flex-direction: column;
71
+ min-height: 100%;
72
+ }
73
+ .proto-content {
74
+ flex: 1 1 auto;
75
+ min-height: 0;
76
+ display: flex;
77
+ flex-direction: column;
78
+ }
79
+ .proto-stage > .proto-topbar,
80
+ .proto-stage > .proto-bottombar { flex: 0 0 auto; }
81
+
82
+ /* === Bars =============================================================== */
83
+
84
+ /* Chrome tokens — re-mapped under prefers-color-scheme: dark below. The
85
+ prototype content (everything between the top and bottom harness bars)
86
+ does NOT use these — it stays in the DS's own light palette so designs
87
+ look like the real product regardless of the user's system theme. */
88
+ :root {
89
+ --proto-chrome-bg: #fff;
90
+ --proto-chrome-pill-bg: #f8fafc;
91
+ --proto-chrome-text: #0f172a;
92
+ --proto-chrome-text-muted: #64748b;
93
+ --proto-chrome-border: #e5e7eb;
94
+ --proto-chrome-nav-bg: #fff;
95
+ /* Height the topbar / bottombar occupy at the viewport edges. The
96
+ comments panel (position:fixed) insets by these so it never tucks
97
+ under the chrome bars. Zeroed when the chrome is hidden/embedded. */
98
+ --proto-topbar-h: 45px;
99
+ --proto-bottombar-h: 45px;
100
+ }
101
+ /* Honors the shared manual theme override (data-theme on <html>, set
102
+ from the dashboard/wireflow user menu) and falls back to the OS. */
103
+ @media (prefers-color-scheme: dark) {
104
+ :root:not([data-theme='light']) {
105
+ --proto-chrome-bg: #0f172a;
106
+ --proto-chrome-pill-bg: #1e293b;
107
+ --proto-chrome-text: #f1f5f9;
108
+ --proto-chrome-text-muted: #94a3b8;
109
+ --proto-chrome-border: #334155;
110
+ --proto-chrome-nav-bg: #1e293b;
111
+ }
112
+ }
113
+ :root[data-theme='dark'] {
114
+ --proto-chrome-bg: #0f172a;
115
+ --proto-chrome-pill-bg: #1e293b;
116
+ --proto-chrome-text: #f1f5f9;
117
+ --proto-chrome-text-muted: #94a3b8;
118
+ --proto-chrome-border: #334155;
119
+ --proto-chrome-nav-bg: #1e293b;
120
+ }
121
+
122
+ .proto-topbar,
123
+ .proto-bottombar {
124
+ position: sticky;
125
+ /* Sit above any in-prototype modal/backdrop. The harness chrome is
126
+ review tooling — Back / Continue and the page-states dropdown must
127
+ stay reachable even when the product UI under test pops a dialog. */
128
+ z-index: 1000;
129
+ display: flex;
130
+ align-items: center;
131
+ background: var(--proto-chrome-bg);
132
+ padding: 8px 16px;
133
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
134
+ flex-shrink: 0;
135
+ }
136
+
137
+ .proto-topbar {
138
+ top: 0;
139
+ /* Back link + step pill anchored left so the right edge stays free for
140
+ the comments sidebar. */
141
+ justify-content: flex-start;
142
+ gap: 12px;
143
+ border-bottom: 1px solid var(--proto-chrome-border);
144
+ }
145
+
146
+ .proto-bottombar {
147
+ bottom: 0;
148
+ /* Three columns: spacer · centered nav · right-aligned comments toggle.
149
+ Centering the Back/Continue feels more intentional than left-pinned
150
+ and leaves room for a right-side action without overlap. */
151
+ display: grid;
152
+ grid-template-columns: 1fr auto 1fr;
153
+ align-items: center;
154
+ border-top: 1px solid var(--proto-chrome-border);
155
+ }
156
+ .proto-bottombar > .proto-nav { grid-column: 2; justify-self: center; }
157
+ .proto-bottombar > .proto-bottombar__right {
158
+ grid-column: 3;
159
+ justify-self: end;
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 8px;
163
+ }
164
+
165
+ /* === Viewport control + preview pane =================================== *
166
+ * The Bedrock breakpoint control, inline at the right of the topbar. Picking
167
+ * a width swaps the content slot for a width-constrained iframe (.proto-preview)
168
+ * so real CSS media queries fire — injected by the script in screen.njk. */
169
+ .proto-viewport {
170
+ margin-left: auto; /* push to the right edge of the topbar */
171
+ position: relative;
172
+ display: inline-flex;
173
+ align-items: stretch;
174
+ }
175
+ /* Main half — quick desktop/mobile toggle. */
176
+ .proto-viewport__main {
177
+ width: 28px;
178
+ height: 26px;
179
+ padding: 0;
180
+ flex: 0 0 auto;
181
+ display: inline-flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ background: var(--proto-chrome-pill-bg);
185
+ border: 1px solid var(--proto-chrome-border);
186
+ border-right: none;
187
+ border-radius: 6px 0 0 6px;
188
+ color: var(--proto-chrome-text);
189
+ cursor: pointer;
190
+ }
191
+ .proto-viewport__main svg { width: 15px; height: 15px; display: block; }
192
+ .proto-viewport__main:hover { background: var(--proto-chrome-border); }
193
+ .proto-viewport__main.is-active {
194
+ background: var(--proto-chrome-text);
195
+ border-color: var(--proto-chrome-text);
196
+ color: var(--proto-chrome-bg);
197
+ }
198
+ /* Caret half — a <summary> toggling the breakpoint menu. */
199
+ .proto-viewport__menu { position: relative; display: inline-flex; }
200
+ .proto-viewport__caret {
201
+ list-style: none;
202
+ cursor: pointer;
203
+ user-select: none;
204
+ width: 16px;
205
+ height: 26px;
206
+ padding: 0;
207
+ background: var(--proto-chrome-pill-bg);
208
+ border: 1px solid var(--proto-chrome-border);
209
+ border-radius: 0 6px 6px 0;
210
+ color: var(--proto-chrome-text-muted);
211
+ display: inline-flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ }
215
+ .proto-viewport__caret::-webkit-details-marker { display: none; }
216
+ .proto-viewport__caret::marker { content: ''; }
217
+ .proto-viewport__caret:hover,
218
+ .proto-viewport__menu[open] .proto-viewport__caret { background: var(--proto-chrome-border); color: var(--proto-chrome-text); }
219
+ /* Breakpoint dropdown. */
220
+ .proto-viewport__panel {
221
+ position: absolute;
222
+ top: calc(100% + 6px);
223
+ right: 0;
224
+ z-index: 1001;
225
+ min-width: 190px;
226
+ padding: 4px;
227
+ background: var(--proto-chrome-nav-bg);
228
+ border: 1px solid var(--proto-chrome-border);
229
+ border-radius: 8px;
230
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 1px;
234
+ }
235
+ .proto-viewport__item {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 8px;
239
+ width: 100%;
240
+ padding: 6px 8px;
241
+ border: 0;
242
+ background: none;
243
+ cursor: pointer;
244
+ border-radius: 6px;
245
+ color: var(--proto-chrome-text);
246
+ font: inherit;
247
+ font-size: 12px;
248
+ text-align: left;
249
+ }
250
+ .proto-viewport__item:hover { background: var(--proto-chrome-pill-bg); }
251
+ .proto-viewport__item.is-active { background: var(--proto-chrome-text); color: var(--proto-chrome-bg); }
252
+ .proto-viewport__item-icon { display: inline-flex; flex: 0 0 auto; }
253
+ .proto-viewport__item-icon svg { width: 16px; height: 16px; display: block; }
254
+ .proto-viewport__item-label { flex: 1; }
255
+ .proto-viewport__item-w { color: var(--proto-chrome-text-muted); font-variant-numeric: tabular-nums; }
256
+ .proto-viewport__item.is-active .proto-viewport__item-w { color: inherit; opacity: 0.75; }
257
+ /* Breakpoints outside the flow's intended viewport range — still clickable
258
+ (the range is advisory), just visibly demoted. */
259
+ .proto-viewport__item.is-out-of-range > * { opacity: 0.4; }
260
+ .proto-viewport__item.is-out-of-range:hover > * { opacity: 0.7; }
261
+ /* Intended-range footer under the breakpoint list. */
262
+ .proto-viewport__range {
263
+ margin-top: 3px;
264
+ padding: 6px 8px 4px;
265
+ border-top: 1px solid var(--proto-chrome-border);
266
+ font-size: 10.5px;
267
+ color: var(--proto-chrome-text-muted);
268
+ white-space: nowrap;
269
+ }
270
+ .proto-viewport__range span { opacity: 0.7; font-variant-numeric: tabular-nums; }
271
+ .proto-viewport__btn:hover { color: var(--proto-chrome-text); }
272
+ .proto-viewport__btn.is-active {
273
+ background: var(--proto-chrome-text);
274
+ border-color: var(--proto-chrome-text);
275
+ color: var(--proto-chrome-bg);
276
+ }
277
+
278
+ .proto-preview { display: none; }
279
+ .proto-stage.is-previewing > .proto-content { display: none; }
280
+ .proto-stage.is-previewing > .proto-preview {
281
+ display: flex;
282
+ /* A definite height so a frame set to height:100% (breakpoints with no
283
+ declared height, e.g. desktop/tablet) actually fills the pane instead of
284
+ collapsing — the .proto-stage flex chain bottoms out at an auto-height
285
+ body, so a percentage height has nothing to resolve against otherwise.
286
+ Sits between the two sticky 45px chrome bars. */
287
+ height: calc(100vh - var(--proto-topbar-h) - var(--proto-bottombar-h));
288
+ align-items: flex-start;
289
+ justify-content: center;
290
+ overflow: auto;
291
+ padding: 16px;
292
+ background: #e5e7eb;
293
+ }
294
+ @media (prefers-color-scheme: dark) {
295
+ :root:not([data-theme='light']) .proto-stage.is-previewing > .proto-preview { background: #111; }
296
+ }
297
+ :root[data-theme='dark'] .proto-stage.is-previewing > .proto-preview { background: #111; }
298
+ .proto-preview__frame {
299
+ position: relative;
300
+ background: #fff;
301
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
302
+ max-width: 100%;
303
+ flex: 0 0 auto;
304
+ }
305
+ .proto-preview__iframe { display: block; width: 100%; height: 100%; border: none; }
306
+ /* Right-edge drag handle to resize the preview container. */
307
+ .proto-preview__resize {
308
+ position: absolute;
309
+ top: 0;
310
+ right: -6px;
311
+ width: 12px;
312
+ height: 100%;
313
+ z-index: 2;
314
+ cursor: ew-resize;
315
+ touch-action: none;
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ }
320
+ .proto-preview__resize::before {
321
+ content: '';
322
+ width: 4px;
323
+ height: 44px;
324
+ border-radius: 2px;
325
+ background: rgba(15, 23, 42, 0.25);
326
+ }
327
+ .proto-preview__resize:hover::before { background: rgba(15, 23, 42, 0.55); }
328
+ /* Live dimension readout, centered at the top of the frame. Transient: shown
329
+ (.is-on, toggled by the resize/breakpoint code) only while the size is
330
+ changing, then faded out so it doesn't cover the screen under review. */
331
+ .proto-preview__size {
332
+ position: absolute;
333
+ top: 6px;
334
+ left: 50%;
335
+ transform: translateX(-50%);
336
+ z-index: 2;
337
+ padding: 2px 8px;
338
+ border-radius: 999px;
339
+ background: rgba(15, 23, 42, 0.7);
340
+ color: #fff;
341
+ font: 600 11px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
342
+ font-variant-numeric: tabular-nums;
343
+ pointer-events: none;
344
+ white-space: nowrap;
345
+ opacity: 0;
346
+ transition: opacity 0.25s ease;
347
+ }
348
+ .proto-preview__size.is-on { opacity: 1; }
349
+ /* Amber readout while the frame sits outside the intended viewport range. */
350
+ .proto-preview__size.is-out-of-range { background: rgba(180, 83, 9, 0.85); }
351
+
352
+ /* === Children of the bars =============================================== */
353
+
354
+ .proto-pill {
355
+ display: inline-block;
356
+ font-size: 11px;
357
+ font-weight: 500;
358
+ color: var(--proto-chrome-text-muted);
359
+ background: var(--proto-chrome-pill-bg);
360
+ padding: 4px 10px;
361
+ border-radius: 999px;
362
+ }
363
+
364
+ .proto-home {
365
+ display: inline-flex;
366
+ align-items: center;
367
+ gap: 5px;
368
+ font-size: 11px;
369
+ color: var(--proto-chrome-text-muted);
370
+ background: var(--proto-chrome-pill-bg);
371
+ text-decoration: none;
372
+ padding: 4px 10px 4px 8px;
373
+ border-radius: 999px;
374
+ }
375
+ .proto-home__icon { flex: none; display: block; }
376
+ .proto-home:hover { color: var(--proto-chrome-text); }
377
+
378
+ /* Page-states dropdown — sits in the topbar after the step pill. Only
379
+ rendered when the page has 2+ states (sibling `<base>--<state>.njk`
380
+ files or declared URL-param states via `pageParamStates`). Uses
381
+ <details>/<summary> so open/close needs no JS. */
382
+ .proto-states {
383
+ position: relative;
384
+ margin-left: 4px;
385
+ }
386
+ .proto-states__summary {
387
+ display: inline-flex;
388
+ align-items: center;
389
+ gap: 6px;
390
+ height: 24px;
391
+ padding: 0 10px;
392
+ font-size: 11px;
393
+ background: var(--proto-chrome-nav-bg);
394
+ border: 1px solid var(--proto-chrome-border);
395
+ border-radius: 6px;
396
+ color: var(--proto-chrome-text);
397
+ cursor: pointer;
398
+ list-style: none;
399
+ user-select: none;
400
+ }
401
+ .proto-states__summary::-webkit-details-marker { display: none; }
402
+ .proto-states__summary:hover { background: var(--proto-chrome-pill-bg); }
403
+ .proto-states__label {
404
+ color: var(--proto-chrome-text-muted);
405
+ text-transform: uppercase;
406
+ letter-spacing: 0.04em;
407
+ font-size: 10px;
408
+ }
409
+ .proto-states__value {
410
+ font-weight: 500;
411
+ }
412
+ .proto-states__caret { font-size: 9px; opacity: 0.7; }
413
+ .proto-states__panel {
414
+ position: absolute;
415
+ top: calc(100% + 4px);
416
+ left: 0;
417
+ min-width: 180px;
418
+ display: flex;
419
+ flex-direction: column;
420
+ padding: 4px;
421
+ background: var(--proto-chrome-bg);
422
+ border: 1px solid var(--proto-chrome-border);
423
+ border-radius: 6px;
424
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
425
+ /* Sit above any in-prototype dialog/modal overlay. The harness chrome
426
+ is review tooling — it must always be reachable, even when the
427
+ product UI under test has popped a modal at z-index ~100-200. */
428
+ z-index: 1000;
429
+ }
430
+ .proto-states__item {
431
+ display: block;
432
+ padding: 6px 10px;
433
+ font-size: 12px;
434
+ color: var(--proto-chrome-text);
435
+ text-decoration: none;
436
+ border-radius: 4px;
437
+ white-space: nowrap;
438
+ }
439
+ .proto-states__item:hover {
440
+ background: var(--proto-chrome-pill-bg);
441
+ color: var(--proto-chrome-text);
442
+ }
443
+ .proto-states__item.is-active {
444
+ background: var(--proto-chrome-pill-bg);
445
+ color: var(--proto-chrome-text);
446
+ font-weight: 500;
447
+ }
448
+ .proto-states__item.is-active::before {
449
+ content: '✓ ';
450
+ color: var(--proto-chrome-text-muted);
451
+ }
452
+
453
+ .proto-nav {
454
+ display: flex;
455
+ align-items: center;
456
+ gap: 8px;
457
+ }
458
+ .proto-nav a {
459
+ display: inline-flex;
460
+ align-items: center;
461
+ height: 28px;
462
+ box-sizing: border-box;
463
+ padding: 0 14px;
464
+ font-size: 12px;
465
+ line-height: 1;
466
+ background: var(--proto-chrome-nav-bg);
467
+ color: var(--proto-chrome-text);
468
+ text-decoration: none;
469
+ border-radius: 6px;
470
+ border: 1px solid var(--proto-chrome-border);
471
+ }
472
+ .proto-nav a:hover { background: var(--proto-chrome-pill-bg); }
473
+ /* Primary navigation (Continue) shares the chrome's neutral palette —
474
+ it's prototype-harness chrome, not a product CTA, so it must not look
475
+ like a brand action. The chrome stays grey-on-grey, with a slightly
476
+ darker fill so Continue still reads as the primary action. */
477
+ .proto-nav a.primary {
478
+ background: var(--proto-chrome-text);
479
+ color: var(--proto-chrome-bg);
480
+ border-color: var(--proto-chrome-text);
481
+ }
482
+ .proto-nav a.primary:hover {
483
+ background: var(--proto-chrome-text-muted);
484
+ border-color: var(--proto-chrome-text-muted);
485
+ }
486
+
487
+ /* === Phone-frame mode ===================================================
488
+ Pages that render a phone-shaped body (e.g. 402×874 in F-002) opt in by
489
+ setting `{% set phoneFrame = true %}` before extending screen.njk, which
490
+ adds `is-phone-frame` to <html>. In this mode:
491
+ - <html> becomes a centering flex container so the phone-sized body
492
+ sits in the middle of the viewport with the html background
493
+ (slate-300) showing on all sides.
494
+ - The harness bars detach from normal flow and pin to the viewport
495
+ top/bottom, full-width, so they sit *outside* the phone frame
496
+ instead of being squeezed inside its 402px column.
497
+ The standard sticky-in-flow behavior is unchanged for desktop pages. */
498
+ .is-phone-frame {
499
+ display: flex;
500
+ align-items: center;
501
+ justify-content: center;
502
+ min-height: 100vh;
503
+ }
504
+ /* Clamp the phone body to its declared size so inner scrollable regions
505
+ (overflow-y: auto children) actually scroll instead of the body growing.
506
+ height wins over per-page min-height because .is-phone-frame body has
507
+ higher specificity than the bare `body` rule in {% block styles %}.
508
+ width is clamped too so the standalone screen renders as a 402px phone
509
+ column (the wireflow node cell enforces this on its own, but a directly
510
+ opened screen has no cell — without this it stretches to the full
511
+ viewport and looks like the desktop layout). */
512
+ .is-phone-frame body {
513
+ width: 402px;
514
+ height: 874px;
515
+ overflow: hidden;
516
+ }
517
+ .is-phone-frame .proto-topbar,
518
+ .is-phone-frame .proto-bottombar {
519
+ position: fixed;
520
+ left: 0;
521
+ right: 0;
522
+ width: auto;
523
+ }
524
+ .is-phone-frame .proto-topbar { top: 0; }
525
+ .is-phone-frame .proto-bottombar { bottom: 0; }
526
+
527
+ /* === Hidden states ====================================================== */
528
+
529
+ .is-embedded .proto-topbar,
530
+ .is-embedded .proto-bottombar,
531
+ .is-chrome-hidden .proto-topbar,
532
+ .is-chrome-hidden .proto-bottombar { display: none !important; }
533
+
534
+ /* Bars gone → the comments panel can use the full viewport height. */
535
+ .is-embedded,
536
+ .is-chrome-hidden {
537
+ --proto-topbar-h: 0px;
538
+ --proto-bottombar-h: 0px;
539
+ }
540
+
541
+ /* === Phone chrome compaction ============================================ *
542
+ * On an actual touch device the chrome competes with a small screen, so it
543
+ * slims down: the viewport preview control disappears (the device IS the
544
+ * viewport), and the step pill drops its "Step" prefix — "2 of 5 · <title>"
545
+ * (the title stays; it's the useful part). */
546
+ @media (pointer: coarse) {
547
+ .proto-viewport { display: none; }
548
+ .proto-pill__prefix { display: none; }
549
+ }
550
+
551
+ /* === Copy-to-Figma button (bottombar right) ============================== *
552
+ * Visual parity with .screen-viewlocal / .screen-comments-toggle so the
553
+ * three sit as a clean group at the bottom-right of the harness chrome.
554
+ * Lives in proto-chrome.css (not screen-comments.css) because it's harness
555
+ * scaffolding, unrelated to comments.
556
+ */
557
+ .screen-figma-copy {
558
+ display: inline-flex;
559
+ align-items: center;
560
+ gap: 6px;
561
+ height: 28px;
562
+ padding: 0 12px;
563
+ background: var(--proto-chrome-nav-bg);
564
+ color: var(--proto-chrome-text);
565
+ border: 1px solid var(--proto-chrome-border);
566
+ border-radius: 6px;
567
+ font: inherit;
568
+ font-size: 12px;
569
+ cursor: pointer;
570
+ }
571
+ .screen-figma-copy:hover:not(:disabled) { background: var(--proto-chrome-pill-bg); }
572
+ .screen-figma-copy:disabled { opacity: 0.7; cursor: progress; }
573
+ /* Busy: swap the clipboard icon for a small spinner so the in-flight state
574
+ reads as deliberate progress instead of a stalled control. */
575
+ .screen-figma-copy.is-busy svg { display: none; }
576
+ .screen-figma-copy.is-busy::before {
577
+ content: '';
578
+ width: 12px;
579
+ height: 12px;
580
+ flex: 0 0 auto;
581
+ border: 2px solid var(--proto-chrome-border);
582
+ border-top-color: var(--proto-chrome-text);
583
+ border-radius: 50%;
584
+ animation: figma-copy-spin 0.7s linear infinite;
585
+ }
586
+ @keyframes figma-copy-spin { to { transform: rotate(360deg); } }
587
+ .screen-figma-copy.is-done {
588
+ background: var(--proto-chrome-text);
589
+ color: var(--proto-chrome-bg);
590
+ border-color: var(--proto-chrome-text);
591
+ }
592
+ .screen-figma-copy.is-error {
593
+ border-color: #dc2626;
594
+ color: #dc2626;
595
+ }
596
+ @media (max-width: 720px) { .screen-figma-copy { display: none !important; } }