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.
- package/auth-schema.sql +8 -0
- package/bin/bedrock-flows.mjs +127 -0
- package/lib/setup.mjs +262 -0
- package/package.json +11 -0
- package/template/.storybook/main.js +46 -0
- package/template/.storybook/manager-head.html +963 -0
- package/template/.storybook/preview-head.html +35 -0
- package/template/.storybook/preview.js +23 -0
- package/template/CHANGELOG.md +236 -0
- package/template/README.md +26 -0
- package/template/apps/dashboard/index.html +15 -0
- package/template/apps/dashboard/package.json +22 -0
- package/template/apps/dashboard/src/App.module.css +1318 -0
- package/template/apps/dashboard/src/App.tsx +2716 -0
- package/template/apps/dashboard/src/auth-client.ts +17 -0
- package/template/apps/dashboard/src/changelog.tsx +92 -0
- package/template/apps/dashboard/src/index.css +86 -0
- package/template/apps/dashboard/src/main.tsx +15 -0
- package/template/apps/dashboard/src/theme.ts +31 -0
- package/template/apps/dashboard/src/vite-env.d.ts +4 -0
- package/template/apps/dashboard/vite.config.ts +48 -0
- package/template/apps/worker/.dev.vars.example +50 -0
- package/template/apps/worker/package.json +19 -0
- package/template/apps/worker/src/index.ts +295 -0
- package/template/apps/worker/tsconfig.json +11 -0
- package/template/apps/worker/wrangler.jsonc +29 -0
- package/template/bedrock.config.ts +16 -0
- package/template/design-system/README.md +97 -0
- package/template/design-system/starter-v1/components/button/component.css +42 -0
- package/template/design-system/starter-v1/components/button/danger.html +21 -0
- package/template/design-system/starter-v1/components/button/default.html +21 -0
- package/template/design-system/starter-v1/components/button/disabled.html +21 -0
- package/template/design-system/starter-v1/components/button/ghost.html +21 -0
- package/template/design-system/starter-v1/components/button/macro.njk +14 -0
- package/template/design-system/starter-v1/components/button/primary.html +21 -0
- package/template/design-system/starter-v1/components/button/variants.json +30 -0
- package/template/design-system/starter-v1/ds.json +3 -0
- package/template/design-system/starter-v1/global.css +52 -0
- package/template/design-system/starter-v1/style.css +107 -0
- package/template/gitignore +8 -0
- package/template/package.json +41 -0
- package/template/prototypes/F-001-hello/1-welcome.njk +30 -0
- package/template/prototypes/F-001-hello/2-form.njk +46 -0
- package/template/prototypes/F-001-hello/3-done.njk +29 -0
- package/template/prototypes/F-001-hello/meta.json +6 -0
- package/template/prototypes/_shared/_auth-gate.njk +54 -0
- package/template/prototypes/_shared/delivery.njk +43 -0
- package/template/prototypes/_shared/layout.njk +15 -0
- package/template/prototypes/_shared/screen.njk +1818 -0
- package/template/prototypes/_shared/wireflow.njk +4731 -0
- package/template/public/auth-gate.css +150 -0
- package/template/public/bedrock/color-inspector.js +284 -0
- package/template/public/bedrock/component-overlay.js +219 -0
- package/template/public/bedrock/data/bedrock-config.js +45 -0
- package/template/public/bedrock/font-size-overlay.js +590 -0
- package/template/public/bedrock/grid-overlay.js +379 -0
- package/template/public/bedrock/prototype-navigation.js +974 -0
- package/template/public/cmdk.js +146 -0
- package/template/public/ds-xray.css +112 -0
- package/template/public/ds-xray.js +271 -0
- package/template/public/favicon.svg +4 -0
- package/template/public/icons/bolt-fill.svg +3 -0
- package/template/public/icons/bolt.svg +3 -0
- package/template/public/icons/caret-down-fill.svg +3 -0
- package/template/public/icons/check-double.svg +4 -0
- package/template/public/icons/check.svg +3 -0
- package/template/public/icons/chevron-left.svg +3 -0
- package/template/public/icons/chevron-right.svg +3 -0
- package/template/public/icons/circle-info.svg +6 -0
- package/template/public/icons/grid.svg +6 -0
- package/template/public/icons/message-square-1.svg +3 -0
- package/template/public/icons/message-square.svg +3 -0
- package/template/public/icons/messages.svg +4 -0
- package/template/public/icons/options-horizontal.svg +5 -0
- package/template/public/icons/swatches.svg +6 -0
- package/template/public/icons/workflow.svg +6 -0
- package/template/public/lightbox.js +87 -0
- package/template/public/proto-chrome.css +596 -0
- package/template/public/screen-comments.css +723 -0
- package/template/public/wireflow-client.js +26 -0
- package/template/scripts/build-storybooks.mjs +8 -0
- package/template/scripts/dev-setup.mjs +15 -0
- package/template/scripts/generate-stories.mjs +12 -0
- package/template/scripts/generate-variants.mjs +22 -0
- 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; } }
|