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,963 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Storybook MANAGER chrome — a per-component commenting bar.
|
|
3
|
+
|
|
4
|
+
This is Storybook chrome (injected into the manager window via Storybook's
|
|
5
|
+
`manager-head.html`), NOT a design-system component, so a self-contained
|
|
6
|
+
widget here is correct — do not move it into design-system/.
|
|
7
|
+
|
|
8
|
+
What it does:
|
|
9
|
+
- Pins a collapsible comment panel to the RIGHT side of the Storybook UI.
|
|
10
|
+
- Derives the current component from the manager URL
|
|
11
|
+
(?path=/docs/components-drawer--docs or /story/...--default) and the
|
|
12
|
+
DS version from the pathname (/ds/<version>/storybook/), forming one
|
|
13
|
+
thread id per component: `DS-<version>-<component>` (sanitized to
|
|
14
|
+
[A-Za-z0-9_-]). One thread per component's Docs page.
|
|
15
|
+
- Loads/renders/posts/resolves comments against
|
|
16
|
+
`/__wf-comments/<ds-id>.json` SAME-ORIGIN (relative, credentials:include),
|
|
17
|
+
matching the comment object shape + behaviours of the screen panel
|
|
18
|
+
(mentions, link detection, 4-letter codes, status lifecycle).
|
|
19
|
+
- Current user via /api/auth/get-session; signed-out shows a read-only state.
|
|
20
|
+
|
|
21
|
+
All CSS is prefixed `.dsc-` and the panel lives in its own host element so it
|
|
22
|
+
can't clash with Storybook's own UI.
|
|
23
|
+
-->
|
|
24
|
+
<style>
|
|
25
|
+
#dsc-root, #dsc-root * { box-sizing: border-box; }
|
|
26
|
+
#dsc-root {
|
|
27
|
+
position: fixed; top: 0; right: 0; height: 100vh; z-index: 2147483000;
|
|
28
|
+
font-family: 'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
.dsc-toggle {
|
|
32
|
+
position: fixed; top: 50%; right: 0; transform: translateY(-50%);
|
|
33
|
+
pointer-events: auto;
|
|
34
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
35
|
+
writing-mode: vertical-rl; text-orientation: mixed;
|
|
36
|
+
background: #1f2937; color: #fff; border: 0; cursor: pointer;
|
|
37
|
+
font-size: 12px; font-weight: 600; letter-spacing: .4px;
|
|
38
|
+
padding: 12px 6px; border-radius: 8px 0 0 8px;
|
|
39
|
+
box-shadow: -2px 0 8px rgba(0,0,0,.18);
|
|
40
|
+
}
|
|
41
|
+
.dsc-toggle:hover { background: #111827; }
|
|
42
|
+
.dsc-toggle__count {
|
|
43
|
+
writing-mode: horizontal-tb; min-width: 18px; height: 18px;
|
|
44
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
45
|
+
background: #ef4444; color: #fff; border-radius: 999px;
|
|
46
|
+
font-size: 11px; font-weight: 700; padding: 0 5px;
|
|
47
|
+
}
|
|
48
|
+
.dsc-panel {
|
|
49
|
+
position: fixed; top: 0; right: 0; height: 100vh; width: 360px; max-width: 92vw;
|
|
50
|
+
background: #fff; border-left: 1px solid #e5e7eb;
|
|
51
|
+
box-shadow: -8px 0 28px rgba(0,0,0,.16);
|
|
52
|
+
transform: translateX(100%); transition: transform .22s ease;
|
|
53
|
+
display: flex; flex-direction: column; pointer-events: auto;
|
|
54
|
+
color: #111827;
|
|
55
|
+
}
|
|
56
|
+
.dsc-panel.is-open { transform: translateX(0); }
|
|
57
|
+
.dsc-header {
|
|
58
|
+
display: flex; align-items: center; gap: 8px;
|
|
59
|
+
padding: 14px 16px; border-bottom: 1px solid #e5e7eb;
|
|
60
|
+
}
|
|
61
|
+
.dsc-title { font-size: 14px; font-weight: 700; margin: 0; flex: 1; line-height: 1.3; }
|
|
62
|
+
.dsc-title small { display: block; font-weight: 500; color: #6b7280; font-size: 11px; margin-top: 2px; }
|
|
63
|
+
.dsc-close {
|
|
64
|
+
background: transparent; border: 0; cursor: pointer; color: #6b7280;
|
|
65
|
+
font-size: 18px; line-height: 1; padding: 4px;
|
|
66
|
+
}
|
|
67
|
+
.dsc-close:hover { color: #111827; }
|
|
68
|
+
.dsc-list { flex: 1; overflow-y: auto; padding: 12px 16px; }
|
|
69
|
+
.dsc-empty, .dsc-readonly { color: #6b7280; font-size: 13px; line-height: 1.5; padding: 8px 0; }
|
|
70
|
+
.dsc-readonly { border-top: 1px solid #e5e7eb; }
|
|
71
|
+
.dsc-card {
|
|
72
|
+
border: 1px solid #e5e7eb; border-radius: 10px; padding: 10px 12px; margin-bottom: 10px;
|
|
73
|
+
}
|
|
74
|
+
.dsc-card.is-resolved { opacity: .6; }
|
|
75
|
+
.dsc-card__resolved { display: block; font-size: 11px; color: #16a34a; font-weight: 600; margin-bottom: 4px; }
|
|
76
|
+
.dsc-card__text { font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
|
77
|
+
.dsc-card__meta {
|
|
78
|
+
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
|
79
|
+
margin-top: 8px; font-size: 11px; color: #6b7280;
|
|
80
|
+
}
|
|
81
|
+
.dsc-card__code {
|
|
82
|
+
font-family: ui-monospace, Menlo, monospace; font-weight: 700; letter-spacing: .5px;
|
|
83
|
+
background: #f3f4f6; border-radius: 4px; padding: 1px 5px;
|
|
84
|
+
}
|
|
85
|
+
.dsc-card__actions { margin-left: auto; display: inline-flex; gap: 4px; }
|
|
86
|
+
.dsc-icon-btn {
|
|
87
|
+
background: transparent; border: 0; cursor: pointer; color: #9ca3af;
|
|
88
|
+
padding: 3px; border-radius: 6px; display: inline-flex;
|
|
89
|
+
}
|
|
90
|
+
.dsc-icon-btn:hover { color: #111827; background: #f3f4f6; }
|
|
91
|
+
.dsc-icon-btn.is-resolved { color: #16a34a; }
|
|
92
|
+
.dsc-mention {
|
|
93
|
+
background: #ede9fe; color: #6d28d9; border-radius: 4px; padding: 0 3px; font-weight: 600;
|
|
94
|
+
}
|
|
95
|
+
.dsc-card__replies {
|
|
96
|
+
margin-top: 8px; padding-left: 10px; border-left: 2px solid #e5e7eb;
|
|
97
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
98
|
+
}
|
|
99
|
+
.dsc-reply__text { font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
|
100
|
+
.dsc-reply__meta { display: flex; align-items: center; gap: 6px; margin-top: 3px; font-size: 11px; color: #6b7280; }
|
|
101
|
+
.dsc-reply__meta .dsc-reply__del { margin-left: auto; }
|
|
102
|
+
.dsc-reply-btn {
|
|
103
|
+
align-self: flex-start; margin-top: 6px; padding: 2px 8px; border: 1px solid transparent;
|
|
104
|
+
background: none; color: #6b7280; border-radius: 4px; font: inherit; font-size: 11px; cursor: pointer;
|
|
105
|
+
}
|
|
106
|
+
.dsc-reply-btn:hover { background: #f3f4f6; color: #111827; }
|
|
107
|
+
.dsc-reply__del { background: none; border: 0; color: #9ca3af; font: inherit; font-size: 11px; cursor: pointer; padding: 0 4px; }
|
|
108
|
+
.dsc-reply__del:hover { color: #b91c1c; }
|
|
109
|
+
.dsc-reply-compose { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
|
|
110
|
+
.dsc-reply-compose textarea {
|
|
111
|
+
width: 100%; box-sizing: border-box; padding: 8px; border: 1px solid #d1d5db;
|
|
112
|
+
border-radius: 6px; font: inherit; font-size: 13px; resize: vertical; min-height: 48px;
|
|
113
|
+
}
|
|
114
|
+
.dsc-reply-compose textarea:focus { outline: 2px solid #7c3aed; outline-offset: -1px; border-color: #7c3aed; }
|
|
115
|
+
.dsc-reply-compose__row { display: flex; gap: 6px; justify-content: flex-end; }
|
|
116
|
+
.dsc-card__story {
|
|
117
|
+
display: inline-block; margin-top: 6px; font-size: 11px; font-weight: 600;
|
|
118
|
+
color: #6d28d9; background: #ede9fe; border-radius: 5px; padding: 2px 7px;
|
|
119
|
+
}
|
|
120
|
+
/* Manager comment still waiting on a designer's approval. */
|
|
121
|
+
.dsc-card__pending {
|
|
122
|
+
display: inline-block; margin-top: 6px; font-size: 11px; font-weight: 600;
|
|
123
|
+
color: #b45309; background: #fef3c7; border-radius: 5px; padding: 2px 7px;
|
|
124
|
+
}
|
|
125
|
+
/* Designer-added context on an approved manager comment. */
|
|
126
|
+
.dsc-card__designer-note {
|
|
127
|
+
margin-top: 6px; padding: 6px 8px; border-left: 3px solid #e5e7eb;
|
|
128
|
+
background: #f9fafb; border-radius: 0 6px 6px 0; font-size: 12px;
|
|
129
|
+
color: #111827; white-space: pre-wrap;
|
|
130
|
+
}
|
|
131
|
+
.dsc-card__designer-note b { font-weight: 600; }
|
|
132
|
+
/* Manager compose hint — their comments always wait on designer approval. */
|
|
133
|
+
.dsc-manager-hint { font-size: 11.5px; color: #6b7280; margin: 0 0 8px; }
|
|
134
|
+
.dsc-story-label { display: block; font-size: 12px; color: #6b7280; margin-bottom: 8px; }
|
|
135
|
+
.dsc-story-select {
|
|
136
|
+
font: inherit; font-size: 13px; color: #111827; margin-left: 4px;
|
|
137
|
+
border: 1px solid #d1d5db; border-radius: 6px; padding: 3px 6px; max-width: 200px;
|
|
138
|
+
}
|
|
139
|
+
.dsc-story-select:focus { outline: 2px solid #7c3aed; outline-offset: -1px; border-color: #7c3aed; }
|
|
140
|
+
.dsc-compose { border-top: 1px solid #e5e7eb; padding: 12px 16px; }
|
|
141
|
+
.dsc-compose textarea {
|
|
142
|
+
width: 100%; resize: vertical; min-height: 64px; font: inherit; font-size: 13px;
|
|
143
|
+
border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 10px; color: #111827;
|
|
144
|
+
}
|
|
145
|
+
.dsc-compose textarea:focus { outline: 2px solid #7c3aed; outline-offset: -1px; border-color: #7c3aed; }
|
|
146
|
+
.dsc-post {
|
|
147
|
+
margin-top: 8px; width: 100%; font: inherit; font-size: 13px; font-weight: 600;
|
|
148
|
+
padding: 9px 12px; border-radius: 8px; border: 0; background: #7c3aed; color: #fff; cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
.dsc-post:disabled { background: #c4b5fd; cursor: default; }
|
|
151
|
+
.dsc-mention-wrap { position: relative; }
|
|
152
|
+
.dsc-mention-menu {
|
|
153
|
+
position: absolute; left: 0; bottom: 100%; margin-bottom: 4px; z-index: 10;
|
|
154
|
+
min-width: 200px; max-height: 200px; overflow-y: auto;
|
|
155
|
+
background: #fff; border: 1px solid #e5e7eb; border-radius: 8px;
|
|
156
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.14);
|
|
157
|
+
}
|
|
158
|
+
.dsc-mention-menu.is-below { bottom: auto; top: 100%; margin-bottom: 0; margin-top: 4px; }
|
|
159
|
+
.dsc-mention-menu__item {
|
|
160
|
+
display: flex; gap: 8px; align-items: baseline; padding: 7px 10px; cursor: pointer; font-size: 13px;
|
|
161
|
+
}
|
|
162
|
+
.dsc-mention-menu__item.is-active, .dsc-mention-menu__item:hover { background: #f3f4f6; }
|
|
163
|
+
.dsc-mention-menu__handle { color: #6b7280; font-size: 12px; }
|
|
164
|
+
</style>
|
|
165
|
+
<script>
|
|
166
|
+
(function () {
|
|
167
|
+
// Run only in the top manager window (Storybook iframes the preview).
|
|
168
|
+
if (window.self !== window.top) return;
|
|
169
|
+
|
|
170
|
+
// Real DS buckets PLUS any storyTitle group prefix we re-home components
|
|
171
|
+
// under (e.g. `OS/Alert Dialog` → story id `os-alert-dialog`). Without the
|
|
172
|
+
// group prefix here, currentComponent() can't resolve those stories and the
|
|
173
|
+
// comment bar shows "Open a component to comment". The component slug after
|
|
174
|
+
// the prefix is the real folder name, so the thread id stays correct.
|
|
175
|
+
var BUCKETS = ['foundations', 'components', 'utilities', 'os', 'screen-patterns', 'design-patterns'];
|
|
176
|
+
|
|
177
|
+
function sanitize(s) { return String(s || '').replace(/[^A-Za-z0-9_-]/g, '-'); }
|
|
178
|
+
|
|
179
|
+
// DS version is the path segment after /ds/ → /ds/<version>/storybook/.
|
|
180
|
+
function dsVersion() {
|
|
181
|
+
var m = /\/ds\/([^/]+)\/storybook\//.exec(window.location.pathname);
|
|
182
|
+
return m ? m[1] : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The manager keeps the active story in the URL query/hash as
|
|
186
|
+
// ?path=/docs/components-drawer--docs (Docs page)
|
|
187
|
+
// ?path=/story/components-drawer--default
|
|
188
|
+
// The story id is `<bucket>-<component>--<story>`. Strip the known
|
|
189
|
+
// bucket prefix, then drop the `--<story>` suffix → the component slug.
|
|
190
|
+
function currentComponent() {
|
|
191
|
+
var hay = window.location.href;
|
|
192
|
+
var m = /[?#&]path=\/(?:docs|story)\/([^&#]+)/.exec(hay);
|
|
193
|
+
if (!m) return null;
|
|
194
|
+
var id = decodeURIComponent(m[1]); // components-drawer--docs
|
|
195
|
+
id = id.split('--')[0]; // components-bottom-sheet
|
|
196
|
+
for (var i = 0; i < BUCKETS.length; i++) {
|
|
197
|
+
var p = BUCKETS[i] + '-';
|
|
198
|
+
if (id.indexOf(p) === 0) {
|
|
199
|
+
return { bucket: BUCKETS[i], component: id.slice(p.length) };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function threadId(version, component) {
|
|
206
|
+
return 'DS-' + sanitize(version) + '-' + sanitize(component);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The full story id when viewing a single story (`?path=/story/<id>`); ''
|
|
210
|
+
// on a Docs page (no single story). Lets a comment target one story.
|
|
211
|
+
function currentStoryId() {
|
|
212
|
+
var m = /[?#&]path=\/(docs|story)\/([^&#]+)/.exec(window.location.href);
|
|
213
|
+
if (!m || m[1] !== 'story') return '';
|
|
214
|
+
return decodeURIComponent(m[2]); // e.g. components-top-bar--two-line-title
|
|
215
|
+
}
|
|
216
|
+
// Pull the component's stories from the Storybook index so the composer's
|
|
217
|
+
// "About <story>" picker can offer them (and a comment can pin to one).
|
|
218
|
+
function loadStories() {
|
|
219
|
+
state.stories = [];
|
|
220
|
+
if (!state.version || !state.comp) { buildStorySelect(); return; }
|
|
221
|
+
fetch('/ds/' + state.version + '/storybook/index.json', { cache: 'no-store' })
|
|
222
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
223
|
+
.then(function (d) {
|
|
224
|
+
if (!d) { buildStorySelect(); return; }
|
|
225
|
+
var entries = d.entries || d.stories || {};
|
|
226
|
+
var prefix = state.comp.bucket + '-' + state.comp.component + '--';
|
|
227
|
+
var list = [];
|
|
228
|
+
Object.keys(entries).forEach(function (k) {
|
|
229
|
+
var e = entries[k];
|
|
230
|
+
if (e && e.type === 'story' && k.indexOf(prefix) === 0) list.push({ id: k, name: e.name || k });
|
|
231
|
+
});
|
|
232
|
+
state.stories = list;
|
|
233
|
+
buildStorySelect();
|
|
234
|
+
})
|
|
235
|
+
.catch(function () { buildStorySelect(); });
|
|
236
|
+
}
|
|
237
|
+
function buildStorySelect() {
|
|
238
|
+
if (!storySelect) return;
|
|
239
|
+
var cur = currentStoryId();
|
|
240
|
+
storySelect.innerHTML = '';
|
|
241
|
+
var optAll = document.createElement('option');
|
|
242
|
+
optAll.value = ''; optAll.textContent = 'the whole component';
|
|
243
|
+
storySelect.appendChild(optAll);
|
|
244
|
+
(state.stories || []).forEach(function (s) {
|
|
245
|
+
var o = document.createElement('option');
|
|
246
|
+
o.value = s.id; o.textContent = s.name;
|
|
247
|
+
if (s.id === cur) o.selected = true;
|
|
248
|
+
storySelect.appendChild(o);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// --- comment helpers (mirrors prototypes/_shared/screen.njk) ----------
|
|
253
|
+
function fmtDate(iso) {
|
|
254
|
+
try {
|
|
255
|
+
var d = new Date(iso), now = new Date();
|
|
256
|
+
var sameYear = d.getFullYear() === now.getFullYear();
|
|
257
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', year: sameYear ? undefined : 'numeric' });
|
|
258
|
+
} catch (e) { return iso; }
|
|
259
|
+
}
|
|
260
|
+
// Deterministic 4-letter code from a comment id — byte-for-byte the
|
|
261
|
+
// same FNV-1a algorithm as screen.njk / worker / scripts/comment.mjs.
|
|
262
|
+
function commentCode(id) {
|
|
263
|
+
var h = 2166136261 >>> 0, s = String(id || '');
|
|
264
|
+
for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
|
|
265
|
+
var out = '';
|
|
266
|
+
for (var j = 0; j < 4; j++) { out += String.fromCharCode(65 + (h % 26)); h = Math.floor(h / 26) + 131; }
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function shortLinkLabel(url) {
|
|
270
|
+
try {
|
|
271
|
+
var u = new URL(url);
|
|
272
|
+
var host = u.hostname.replace(/^www\./, '');
|
|
273
|
+
if (host.indexOf('figma.com') >= 0) {
|
|
274
|
+
var node = u.searchParams.get('node-id');
|
|
275
|
+
return node ? ('figma.com · ' + node) : 'figma.com';
|
|
276
|
+
}
|
|
277
|
+
var tail = (u.pathname || '').replace(/\/+$/, '');
|
|
278
|
+
if (tail.length > 22) tail = tail.slice(0, 22) + '…';
|
|
279
|
+
return host + tail;
|
|
280
|
+
} catch (e) { return url.length > 42 ? url.slice(0, 42) + '…' : url; }
|
|
281
|
+
}
|
|
282
|
+
// Append text, turning http(s) URLs into links (DOM nodes → XSS-safe).
|
|
283
|
+
function appendLinkified(el, text) {
|
|
284
|
+
var re = /(https?:\/\/[^\s]+)/g, last = 0, m;
|
|
285
|
+
while ((m = re.exec(text)) !== null) {
|
|
286
|
+
if (m.index > last) el.appendChild(document.createTextNode(text.slice(last, m.index)));
|
|
287
|
+
var url = m[0], tail = '';
|
|
288
|
+
var trail = url.match(/[).,;:!?\]]+$/);
|
|
289
|
+
if (trail) { tail = trail[0]; url = url.slice(0, -tail.length); }
|
|
290
|
+
var a = document.createElement('a');
|
|
291
|
+
a.href = url; a.target = '_blank'; a.rel = 'noreferrer noopener';
|
|
292
|
+
a.textContent = shortLinkLabel(url); a.title = url; a.style.cssText = 'color:#2563eb;text-decoration:underline;word-break:break-word;';
|
|
293
|
+
el.appendChild(a);
|
|
294
|
+
if (tail) el.appendChild(document.createTextNode(tail));
|
|
295
|
+
last = m.index + m[0].length;
|
|
296
|
+
}
|
|
297
|
+
if (last < text.length) el.appendChild(document.createTextNode(text.slice(last)));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
var state = {
|
|
301
|
+
version: null, comp: null, id: null,
|
|
302
|
+
comments: [], directory: [], currentUser: null, stories: [],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
function mentionablePeople() {
|
|
306
|
+
var seen = {};
|
|
307
|
+
(state.directory || []).forEach(function (u) {
|
|
308
|
+
var email = (u.email || '').trim().toLowerCase();
|
|
309
|
+
var local = email.split('@')[0] || '';
|
|
310
|
+
var handle = local.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
311
|
+
if (!handle || seen[handle]) return;
|
|
312
|
+
seen[handle] = { handle: handle, name: (u.name || '').trim() || local, userId: u.id || null };
|
|
313
|
+
});
|
|
314
|
+
(state.comments || []).forEach(function (c) {
|
|
315
|
+
var name = (c.author || '').trim(); if (!name) return;
|
|
316
|
+
var handle = name.split(/\s+/)[0].toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
317
|
+
if (!handle || seen[handle]) return;
|
|
318
|
+
seen[handle] = { handle: handle, name: name, userId: c.userId || null };
|
|
319
|
+
});
|
|
320
|
+
return Object.keys(seen).map(function (k) { return seen[k]; });
|
|
321
|
+
}
|
|
322
|
+
function renderTextWithMentions(el, str) {
|
|
323
|
+
el.textContent = '';
|
|
324
|
+
var byHandle = {};
|
|
325
|
+
mentionablePeople().forEach(function (p) { byHandle[p.handle] = p; });
|
|
326
|
+
var re = /@([a-z0-9]+)/gi, last = 0, m;
|
|
327
|
+
while ((m = re.exec(str)) !== null) {
|
|
328
|
+
var person = byHandle[m[1].toLowerCase()];
|
|
329
|
+
if (!person) continue;
|
|
330
|
+
if (m.index > last) appendLinkified(el, str.slice(last, m.index));
|
|
331
|
+
var pill = document.createElement('span');
|
|
332
|
+
pill.className = 'dsc-mention'; pill.textContent = '@' + person.name;
|
|
333
|
+
el.appendChild(pill);
|
|
334
|
+
last = m.index + m[0].length;
|
|
335
|
+
}
|
|
336
|
+
if (last < str.length) appendLinkified(el, str.slice(last));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// --- DOM ---------------------------------------------------------------
|
|
340
|
+
var root, toggleBtn, countBadge, panel, titleEl, listEl, composeEl, inputEl, postBtn, storySelect;
|
|
341
|
+
var mMenu, mMatches = [], mActive = -1, mStart = -1;
|
|
342
|
+
// Role helpers. Designers (and admins) run the agentic loop; managers
|
|
343
|
+
// only give comments — never directly actionable (worker-enforced too).
|
|
344
|
+
function isDesignerUser(u) { return !!u && (u.role === 'designer' || u.role === 'admin'); }
|
|
345
|
+
function isManagerUser(u) { return !!u && u.role === 'manager'; }
|
|
346
|
+
|
|
347
|
+
function buildDom() {
|
|
348
|
+
root = document.createElement('div');
|
|
349
|
+
root.id = 'dsc-root';
|
|
350
|
+
root.innerHTML =
|
|
351
|
+
'<button type="button" class="dsc-toggle" id="dscToggle" title="Component comments">' +
|
|
352
|
+
'Comments <span class="dsc-toggle__count" id="dscCount" hidden>0</span>' +
|
|
353
|
+
'</button>' +
|
|
354
|
+
'<aside class="dsc-panel" id="dscPanel" aria-label="Component comments">' +
|
|
355
|
+
'<div class="dsc-header">' +
|
|
356
|
+
'<h2 class="dsc-title" id="dscTitle">Comments</h2>' +
|
|
357
|
+
'<button type="button" class="dsc-close" id="dscClose" aria-label="Close">×</button>' +
|
|
358
|
+
'</div>' +
|
|
359
|
+
'<div class="dsc-list" id="dscList"></div>' +
|
|
360
|
+
'<div class="dsc-compose" id="dscCompose" hidden>' +
|
|
361
|
+
'<label class="dsc-story-label">About <select class="dsc-story-select" id="dscStory"><option value="">the whole component</option></select></label>' +
|
|
362
|
+
'<div class="dsc-mention-wrap">' +
|
|
363
|
+
'<textarea id="dscInput" placeholder="Comment on this component… use @ to mention"></textarea>' +
|
|
364
|
+
'<div class="dsc-mention-menu" id="dscMentionMenu" hidden></div>' +
|
|
365
|
+
'</div>' +
|
|
366
|
+
'<button type="button" class="dsc-post" id="dscPost" disabled>Post</button>' +
|
|
367
|
+
'</div>' +
|
|
368
|
+
'</aside>';
|
|
369
|
+
document.body.appendChild(root);
|
|
370
|
+
toggleBtn = root.querySelector('#dscToggle');
|
|
371
|
+
countBadge = root.querySelector('#dscCount');
|
|
372
|
+
panel = root.querySelector('#dscPanel');
|
|
373
|
+
titleEl = root.querySelector('#dscTitle');
|
|
374
|
+
listEl = root.querySelector('#dscList');
|
|
375
|
+
composeEl = root.querySelector('#dscCompose');
|
|
376
|
+
inputEl = root.querySelector('#dscInput');
|
|
377
|
+
postBtn = root.querySelector('#dscPost');
|
|
378
|
+
storySelect = root.querySelector('#dscStory');
|
|
379
|
+
mMenu = root.querySelector('#dscMentionMenu');
|
|
380
|
+
|
|
381
|
+
toggleBtn.addEventListener('click', function () {
|
|
382
|
+
panel.classList.toggle('is-open');
|
|
383
|
+
});
|
|
384
|
+
root.querySelector('#dscClose').addEventListener('click', function () {
|
|
385
|
+
panel.classList.remove('is-open');
|
|
386
|
+
});
|
|
387
|
+
inputEl.addEventListener('input', function () {
|
|
388
|
+
postBtn.disabled = inputEl.value.trim().length === 0;
|
|
389
|
+
mOpen();
|
|
390
|
+
});
|
|
391
|
+
inputEl.addEventListener('blur', function () { setTimeout(mClose, 120); });
|
|
392
|
+
inputEl.addEventListener('keydown', function (e) {
|
|
393
|
+
if (mentionKeydown(e)) return;
|
|
394
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); postComment(); }
|
|
395
|
+
});
|
|
396
|
+
postBtn.addEventListener('click', postComment);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// --- mention menu (composer only) -------------------------------------
|
|
400
|
+
function mClose() { mMenu.hidden = true; mMatches = []; mActive = -1; mStart = -1; }
|
|
401
|
+
function mOpen() {
|
|
402
|
+
var val = inputEl.value, caret = inputEl.selectionStart;
|
|
403
|
+
var mm = /(^|\s)@([a-z0-9]*)$/i.exec(val.slice(0, caret));
|
|
404
|
+
if (!mm) { mClose(); return; }
|
|
405
|
+
mStart = caret - mm[2].length - 1;
|
|
406
|
+
var q = mm[2].toLowerCase();
|
|
407
|
+
mMatches = mentionablePeople()
|
|
408
|
+
.filter(function (p) { return !state.currentUser || p.userId !== state.currentUser.id; })
|
|
409
|
+
.filter(function (p) { return p.handle.indexOf(q) === 0 || p.name.toLowerCase().indexOf(q) === 0; });
|
|
410
|
+
if (!mMatches.length) { mClose(); return; }
|
|
411
|
+
mActive = 0; mMenu.innerHTML = '';
|
|
412
|
+
mMatches.forEach(function (p, i) {
|
|
413
|
+
var it = document.createElement('div');
|
|
414
|
+
it.className = 'dsc-mention-menu__item' + (i === 0 ? ' is-active' : '');
|
|
415
|
+
var nm = document.createElement('span'); nm.textContent = p.name;
|
|
416
|
+
var hd = document.createElement('span'); hd.className = 'dsc-mention-menu__handle'; hd.textContent = '@' + p.handle;
|
|
417
|
+
it.appendChild(nm); it.appendChild(hd);
|
|
418
|
+
it.addEventListener('mousedown', function (ev) { ev.preventDefault(); mPick(i); });
|
|
419
|
+
mMenu.appendChild(it);
|
|
420
|
+
});
|
|
421
|
+
mMenu.hidden = false;
|
|
422
|
+
// Auto-flip below the input when there isn't room above it.
|
|
423
|
+
mMenu.classList.remove('is-below');
|
|
424
|
+
var ir = inputEl.getBoundingClientRect();
|
|
425
|
+
if (ir.top - (mMenu.offsetHeight || 200) < 8) mMenu.classList.add('is-below');
|
|
426
|
+
}
|
|
427
|
+
function mPick(i) {
|
|
428
|
+
var p = mMatches[i]; if (!p || mStart < 0) return;
|
|
429
|
+
var val = inputEl.value, caret = inputEl.selectionStart;
|
|
430
|
+
inputEl.value = val.slice(0, mStart) + '@' + p.handle + ' ' + val.slice(caret);
|
|
431
|
+
var pos = mStart + p.handle.length + 2;
|
|
432
|
+
inputEl.setSelectionRange(pos, pos);
|
|
433
|
+
postBtn.disabled = inputEl.value.trim().length === 0;
|
|
434
|
+
mClose(); inputEl.focus();
|
|
435
|
+
}
|
|
436
|
+
function mentionKeydown(e) {
|
|
437
|
+
if (mMenu.hidden || !mMatches.length) return false;
|
|
438
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
mActive = (mActive + (e.key === 'ArrowDown' ? 1 : -1) + mMatches.length) % mMatches.length;
|
|
441
|
+
Array.prototype.forEach.call(mMenu.children, function (el, idx) { el.classList.toggle('is-active', idx === mActive); });
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); mPick(mActive); return true; }
|
|
445
|
+
if (e.key === 'Escape') { e.preventDefault(); mClose(); return true; }
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// --- networking (SAME-ORIGIN relative, credentials:include) -----------
|
|
450
|
+
function api() { return '/__wf-comments/' + state.id + '.json'; }
|
|
451
|
+
function scoped() { return state.comments.filter(function (c) { return c.screenId === state.id; }); }
|
|
452
|
+
|
|
453
|
+
function loadComments() {
|
|
454
|
+
fetch(api(), { cache: 'no-store', credentials: 'include' })
|
|
455
|
+
.then(function (r) { return r.ok ? r.json() : []; })
|
|
456
|
+
.then(function (arr) { state.comments = Array.isArray(arr) ? arr : []; render(); })
|
|
457
|
+
.catch(function () { render(); });
|
|
458
|
+
}
|
|
459
|
+
function saveComments() {
|
|
460
|
+
return fetch(api(), {
|
|
461
|
+
method: 'PUT',
|
|
462
|
+
headers: { 'Content-Type': 'application/json' },
|
|
463
|
+
body: JSON.stringify(state.comments),
|
|
464
|
+
credentials: 'include',
|
|
465
|
+
}).catch(function (e) { console.error('[dsc] save failed', e); });
|
|
466
|
+
}
|
|
467
|
+
function loadPeople() {
|
|
468
|
+
return fetch('/__wf-people.json', { cache: 'no-store', credentials: 'include' })
|
|
469
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
470
|
+
.then(function (data) { state.directory = (data && Array.isArray(data.people)) ? data.people : []; })
|
|
471
|
+
.catch(function () {});
|
|
472
|
+
}
|
|
473
|
+
function refreshSession() {
|
|
474
|
+
return fetch('/api/auth/get-session', { headers: { Accept: 'application/json' }, credentials: 'include' })
|
|
475
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
476
|
+
.then(function (data) { state.currentUser = (data && data.user) || null; })
|
|
477
|
+
.catch(function () { state.currentUser = null; });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function postComment() {
|
|
481
|
+
var text = inputEl.value.trim();
|
|
482
|
+
if (!text || !state.currentUser) return;
|
|
483
|
+
var u = state.currentUser;
|
|
484
|
+
var id = (crypto.randomUUID && crypto.randomUUID()) || (String(Date.now()) + '-' + Math.random().toString(16).slice(2));
|
|
485
|
+
// MATCH the screen-panel comment object shape so DS comments flow
|
|
486
|
+
// through the exact same pipeline (queue, worker normalize, resolve).
|
|
487
|
+
// DS comments can't be reached via /p/<id>/ (no prototype owns a DS- id),
|
|
488
|
+
// so carry the exact storybook docs URL for back-links (e.g. @mention
|
|
489
|
+
// notification emails). Mirrors the dashboard queue's derivation:
|
|
490
|
+
// /ds/<version>/storybook/?path=/docs/<bucket>-<component>--docs
|
|
491
|
+
// Optional story target — pins the comment to one story so the queue can
|
|
492
|
+
// preview ONLY that story (not the whole component docs).
|
|
493
|
+
var storyId = storySelect ? storySelect.value : '';
|
|
494
|
+
var storyName = '';
|
|
495
|
+
if (storyId) {
|
|
496
|
+
var s = (state.stories || []).find(function (x) { return x.id === storyId; });
|
|
497
|
+
storyName = s ? s.name : '';
|
|
498
|
+
}
|
|
499
|
+
var dsHref = (state.version && state.comp)
|
|
500
|
+
? (storyId
|
|
501
|
+
? '/ds/' + state.version + '/storybook/?path=/story/' + storyId
|
|
502
|
+
: '/ds/' + state.version + '/storybook/?path=/docs/' +
|
|
503
|
+
state.comp.bucket + '-' + state.comp.component + '--docs')
|
|
504
|
+
: undefined;
|
|
505
|
+
state.comments.push({
|
|
506
|
+
id: id, text: text, author: (u.name || u.email || 'Signed in user'),
|
|
507
|
+
authorEmail: (u.email || ''), userId: u.id,
|
|
508
|
+
createdAt: new Date().toISOString(), screenId: state.id,
|
|
509
|
+
// Managers only give comments: theirs start in 'pending-review' and a
|
|
510
|
+
// designer must approve (and contextualize) them before agents act.
|
|
511
|
+
// The worker enforces the same rule server-side.
|
|
512
|
+
status: isManagerUser(u) ? 'pending-review' : 'open',
|
|
513
|
+
dsHref: dsHref,
|
|
514
|
+
storyId: storyId || undefined, storyName: storyName || undefined,
|
|
515
|
+
});
|
|
516
|
+
inputEl.value = ''; postBtn.disabled = true;
|
|
517
|
+
if (storySelect) buildStorySelect();
|
|
518
|
+
render(); saveComments();
|
|
519
|
+
}
|
|
520
|
+
// Designer approval of a manager comment: inline context textarea +
|
|
521
|
+
// Approve/Cancel (reuses the reply-composer styling). Approving promotes
|
|
522
|
+
// the comment to 'open' and stores the context as designerNote.
|
|
523
|
+
function beginApprove(c, card) {
|
|
524
|
+
if (card.querySelector('.dsc-approve-row')) return;
|
|
525
|
+
var row = document.createElement('div');
|
|
526
|
+
row.className = 'dsc-reply-compose dsc-approve-row';
|
|
527
|
+
var ta = document.createElement('textarea');
|
|
528
|
+
ta.placeholder = 'Add context for the agent (optional) — what should be done with this?';
|
|
529
|
+
var btnRow = document.createElement('div');
|
|
530
|
+
btnRow.className = 'dsc-reply-compose__row';
|
|
531
|
+
var cancel = document.createElement('button');
|
|
532
|
+
cancel.type = 'button'; cancel.className = 'dsc-reply-btn'; cancel.textContent = 'Cancel';
|
|
533
|
+
var ok = document.createElement('button');
|
|
534
|
+
ok.type = 'button'; ok.className = 'dsc-post'; ok.textContent = 'Approve for agent';
|
|
535
|
+
ok.addEventListener('click', function () {
|
|
536
|
+
var idx = state.comments.findIndex(function (x) { return x.id === c.id; });
|
|
537
|
+
if (idx < 0 || !state.currentUser) return;
|
|
538
|
+
var now = new Date().toISOString();
|
|
539
|
+
var by = state.currentUser.name || state.currentUser.email || 'Designer';
|
|
540
|
+
var note = ta.value.trim();
|
|
541
|
+
var patch = { status: 'open', resolved: false, statusAt: now, statusBy: by };
|
|
542
|
+
if (note) { patch.designerNote = note; patch.designerNoteBy = by; patch.designerNoteAt = now; }
|
|
543
|
+
state.comments[idx] = Object.assign({}, state.comments[idx], patch);
|
|
544
|
+
render(); saveComments();
|
|
545
|
+
});
|
|
546
|
+
cancel.addEventListener('click', function () { row.remove(); });
|
|
547
|
+
btnRow.appendChild(cancel); btnRow.appendChild(ok);
|
|
548
|
+
row.appendChild(ta); row.appendChild(btnRow);
|
|
549
|
+
card.appendChild(row);
|
|
550
|
+
ta.focus();
|
|
551
|
+
}
|
|
552
|
+
function toggleResolved(cid) {
|
|
553
|
+
var idx = state.comments.findIndex(function (c) { return c.id === cid; });
|
|
554
|
+
if (idx < 0 || !state.currentUser) return;
|
|
555
|
+
var next = !state.comments[idx].resolved;
|
|
556
|
+
var u = state.currentUser;
|
|
557
|
+
state.comments[idx] = Object.assign({}, state.comments[idx], {
|
|
558
|
+
status: next ? 'approved' : 'open',
|
|
559
|
+
resolved: next,
|
|
560
|
+
resolvedAt: next ? new Date().toISOString() : undefined,
|
|
561
|
+
resolvedBy: next ? (u.name || u.email || 'Signed in user') : undefined,
|
|
562
|
+
resolvedById: next ? u.id : undefined,
|
|
563
|
+
});
|
|
564
|
+
render(); saveComments();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// One reply row: text (with @mentions) + author/date, plus Delete for the
|
|
568
|
+
// reply's own author. Replies aren't resolvable. (No live @ autocomplete
|
|
569
|
+
// on the reply box here — manual @mentions still render as pills.)
|
|
570
|
+
function buildReplyEl(r) {
|
|
571
|
+
var el = document.createElement('div');
|
|
572
|
+
el.className = 'dsc-reply';
|
|
573
|
+
var text = document.createElement('div');
|
|
574
|
+
text.className = 'dsc-reply__text';
|
|
575
|
+
renderTextWithMentions(text, r.text || '');
|
|
576
|
+
el.appendChild(text);
|
|
577
|
+
var meta = document.createElement('div');
|
|
578
|
+
meta.className = 'dsc-reply__meta';
|
|
579
|
+
var rcode = document.createElement('span');
|
|
580
|
+
rcode.className = 'dsc-card__code';
|
|
581
|
+
rcode.textContent = commentCode(r.id);
|
|
582
|
+
rcode.title = 'Reply code — refer to this reply by "' + rcode.textContent + '"';
|
|
583
|
+
meta.appendChild(rcode);
|
|
584
|
+
var who = document.createElement('span');
|
|
585
|
+
who.textContent = (r.author ? r.author + ' · ' : '') + (r.createdAt ? fmtDate(r.createdAt) : '');
|
|
586
|
+
meta.appendChild(who);
|
|
587
|
+
var u = state.currentUser;
|
|
588
|
+
if (u && r.userId && r.userId === u.id) {
|
|
589
|
+
var del = document.createElement('button');
|
|
590
|
+
del.type = 'button'; del.className = 'dsc-reply__del'; del.textContent = 'Delete';
|
|
591
|
+
del.addEventListener('click', function () { deleteReply(r.id); });
|
|
592
|
+
meta.appendChild(del);
|
|
593
|
+
}
|
|
594
|
+
el.appendChild(meta);
|
|
595
|
+
return el;
|
|
596
|
+
}
|
|
597
|
+
// Inline edit of a top-level comment (author or admin). Swaps the text
|
|
598
|
+
// node for a textarea + Save/Cancel, then PUTs the updated thread.
|
|
599
|
+
function editComment(c, textEl) {
|
|
600
|
+
if (!state.currentUser) return;
|
|
601
|
+
var wrap = document.createElement('div');
|
|
602
|
+
wrap.className = 'dsc-reply-compose';
|
|
603
|
+
var ta = document.createElement('textarea');
|
|
604
|
+
ta.value = c.text || '';
|
|
605
|
+
var row = document.createElement('div'); row.className = 'dsc-reply-compose__row';
|
|
606
|
+
var cancel = document.createElement('button');
|
|
607
|
+
cancel.type = 'button'; cancel.className = 'dsc-reply-btn'; cancel.textContent = 'Cancel';
|
|
608
|
+
cancel.addEventListener('click', render);
|
|
609
|
+
var save = document.createElement('button');
|
|
610
|
+
save.type = 'button'; save.className = 'dsc-post'; save.textContent = 'Save';
|
|
611
|
+
save.addEventListener('click', function () {
|
|
612
|
+
var t = ta.value.trim();
|
|
613
|
+
if (!t) return;
|
|
614
|
+
saveCommentEdit(c.id, t);
|
|
615
|
+
});
|
|
616
|
+
row.appendChild(cancel); row.appendChild(save);
|
|
617
|
+
wrap.appendChild(ta); wrap.appendChild(row);
|
|
618
|
+
textEl.replaceWith(wrap);
|
|
619
|
+
ta.focus();
|
|
620
|
+
}
|
|
621
|
+
function saveCommentEdit(cid, newText) {
|
|
622
|
+
var idx = state.comments.findIndex(function (x) { return x.id === cid; });
|
|
623
|
+
if (idx < 0 || !state.currentUser) return;
|
|
624
|
+
state.comments[idx].text = newText;
|
|
625
|
+
state.comments[idx].editedAt = new Date().toISOString();
|
|
626
|
+
saveComments().then(render);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function openReplyComposer(c, card, replyBtn) {
|
|
630
|
+
if (card.querySelector('.dsc-reply-compose')) return;
|
|
631
|
+
replyBtn.hidden = true;
|
|
632
|
+
var box = document.createElement('div');
|
|
633
|
+
box.className = 'dsc-reply-compose';
|
|
634
|
+
var ta = document.createElement('textarea');
|
|
635
|
+
ta.rows = 2; ta.placeholder = 'Reply… use @ to mention';
|
|
636
|
+
var row = document.createElement('div');
|
|
637
|
+
row.className = 'dsc-reply-compose__row';
|
|
638
|
+
var post = document.createElement('button');
|
|
639
|
+
post.type = 'button'; post.className = 'dsc-post'; post.textContent = 'Reply';
|
|
640
|
+
var cancel = document.createElement('button');
|
|
641
|
+
cancel.type = 'button'; cancel.className = 'dsc-reply__del'; cancel.textContent = 'Cancel';
|
|
642
|
+
function close() { box.remove(); replyBtn.hidden = false; }
|
|
643
|
+
post.addEventListener('click', function () {
|
|
644
|
+
var text = ta.value.trim();
|
|
645
|
+
if (!text) { close(); return; }
|
|
646
|
+
postReply(c.id, text);
|
|
647
|
+
});
|
|
648
|
+
cancel.addEventListener('click', close);
|
|
649
|
+
ta.addEventListener('keydown', function (e) {
|
|
650
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); post.click(); }
|
|
651
|
+
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
652
|
+
});
|
|
653
|
+
row.appendChild(post); row.appendChild(cancel);
|
|
654
|
+
box.appendChild(ta); box.appendChild(row);
|
|
655
|
+
card.appendChild(box);
|
|
656
|
+
ta.focus();
|
|
657
|
+
}
|
|
658
|
+
function postReply(parentId, text) {
|
|
659
|
+
if (!text || !state.currentUser) return;
|
|
660
|
+
var u = state.currentUser;
|
|
661
|
+
var id = (crypto.randomUUID && crypto.randomUUID()) || (String(Date.now()) + '-' + Math.random().toString(16).slice(2));
|
|
662
|
+
state.comments.push({
|
|
663
|
+
id: id, text: text, author: (u.name || u.email || 'Signed in user'),
|
|
664
|
+
authorEmail: (u.email || ''), userId: u.id,
|
|
665
|
+
createdAt: new Date().toISOString(), parentId: parentId,
|
|
666
|
+
});
|
|
667
|
+
render(); saveComments();
|
|
668
|
+
}
|
|
669
|
+
function deleteReply(id) {
|
|
670
|
+
state.comments = state.comments.filter(function (c) { return c.id !== id; });
|
|
671
|
+
render(); saveComments();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function render() {
|
|
675
|
+
if (!listEl) return;
|
|
676
|
+
listEl.innerHTML = '';
|
|
677
|
+
var list = scoped();
|
|
678
|
+
if (!list.length) {
|
|
679
|
+
var empty = document.createElement('div');
|
|
680
|
+
empty.className = 'dsc-empty';
|
|
681
|
+
empty.textContent = 'No comments on this component yet.';
|
|
682
|
+
listEl.appendChild(empty);
|
|
683
|
+
} else {
|
|
684
|
+
list.slice().sort(function (a, b) {
|
|
685
|
+
return (a.resolved ? 1 : 0) - (b.resolved ? 1 : 0)
|
|
686
|
+
|| String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
|
|
687
|
+
}).forEach(function (c) {
|
|
688
|
+
var card = document.createElement('div');
|
|
689
|
+
card.className = 'dsc-card' + (c.resolved ? ' is-resolved' : '');
|
|
690
|
+
if (c.resolved) {
|
|
691
|
+
var rb = document.createElement('span');
|
|
692
|
+
rb.className = 'dsc-card__resolved';
|
|
693
|
+
rb.textContent = '✓ Resolved' + (c.resolvedBy ? ' · ' + c.resolvedBy : '') + (c.resolvedAt ? ' · ' + fmtDate(c.resolvedAt) : '');
|
|
694
|
+
card.appendChild(rb);
|
|
695
|
+
}
|
|
696
|
+
var text = document.createElement('div');
|
|
697
|
+
text.className = 'dsc-card__text';
|
|
698
|
+
renderTextWithMentions(text, c.text || '');
|
|
699
|
+
card.appendChild(text);
|
|
700
|
+
if (c.storyId) {
|
|
701
|
+
var sc = document.createElement('div');
|
|
702
|
+
sc.className = 'dsc-card__story';
|
|
703
|
+
sc.textContent = '▸ ' + (c.storyName || c.storyId.split('--').pop().replace(/-/g, ' '));
|
|
704
|
+
card.appendChild(sc);
|
|
705
|
+
}
|
|
706
|
+
if (c.status === 'pending-review') {
|
|
707
|
+
var pr = document.createElement('div');
|
|
708
|
+
pr.className = 'dsc-card__pending';
|
|
709
|
+
pr.textContent = 'awaiting review';
|
|
710
|
+
pr.title = 'Manager comment — a designer must approve it before agents act on it';
|
|
711
|
+
card.appendChild(pr);
|
|
712
|
+
}
|
|
713
|
+
// Designer context added when a manager comment was approved —
|
|
714
|
+
// this is what the agent is actually asked to do with it.
|
|
715
|
+
if (c.designerNote) {
|
|
716
|
+
var dn = document.createElement('div');
|
|
717
|
+
dn.className = 'dsc-card__designer-note';
|
|
718
|
+
var dnWho = document.createElement('b');
|
|
719
|
+
dnWho.textContent = (c.designerNoteBy || 'Designer') + ': ';
|
|
720
|
+
dn.appendChild(dnWho);
|
|
721
|
+
dn.appendChild(document.createTextNode(c.designerNote));
|
|
722
|
+
card.appendChild(dn);
|
|
723
|
+
}
|
|
724
|
+
var meta = document.createElement('div');
|
|
725
|
+
meta.className = 'dsc-card__meta';
|
|
726
|
+
var code = document.createElement('span');
|
|
727
|
+
code.className = 'dsc-card__code'; code.textContent = commentCode(c.id);
|
|
728
|
+
code.title = 'Comment code — refer to this comment by "' + code.textContent + '"';
|
|
729
|
+
var stamp = document.createElement('span');
|
|
730
|
+
stamp.textContent = (c.author ? c.author + ' · ' : '') + (c.createdAt ? fmtDate(c.createdAt) : '') + (c.editedAt ? ' · edited' : '');
|
|
731
|
+
meta.appendChild(code); meta.appendChild(stamp);
|
|
732
|
+
if (state.currentUser) {
|
|
733
|
+
var actions = document.createElement('span');
|
|
734
|
+
actions.className = 'dsc-card__actions';
|
|
735
|
+
// A pending manager comment needs a designer verdict: Approve
|
|
736
|
+
// opens an inline row to add context, then promotes the comment
|
|
737
|
+
// to 'open' (the actionable status for the agentic loop).
|
|
738
|
+
if (c.status === 'pending-review' && isDesignerUser(state.currentUser)) {
|
|
739
|
+
var approve = document.createElement('button');
|
|
740
|
+
approve.type = 'button';
|
|
741
|
+
approve.className = 'dsc-icon-btn';
|
|
742
|
+
approve.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="1.5"/><path d="M8.5 12.5L11 15L15.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
743
|
+
approve.title = 'Approve for the agent queue — optionally add context first';
|
|
744
|
+
approve.setAttribute('aria-label', 'Approve for the agent queue');
|
|
745
|
+
approve.addEventListener('click', function () { beginApprove(c, card); });
|
|
746
|
+
actions.appendChild(approve);
|
|
747
|
+
}
|
|
748
|
+
// Managers only close their OWN comments (retract feedback) and
|
|
749
|
+
// never reopen — reopening re-enters the actionable flow.
|
|
750
|
+
var canResolve = !isManagerUser(state.currentUser)
|
|
751
|
+
|| ((c.userId && c.userId === state.currentUser.id) && !c.resolved);
|
|
752
|
+
if (canResolve) {
|
|
753
|
+
var resolve = document.createElement('button');
|
|
754
|
+
resolve.type = 'button';
|
|
755
|
+
resolve.className = 'dsc-icon-btn' + (c.resolved ? ' is-resolved' : '');
|
|
756
|
+
resolve.innerHTML = c.resolved
|
|
757
|
+
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M20 8L12 18L11.0773 16.9733" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 11L8 16L16 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
|
758
|
+
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M4 12L9.33333 18L20 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
759
|
+
var lbl = c.resolved ? 'Reopen comment' : 'Resolve comment';
|
|
760
|
+
resolve.title = lbl; resolve.setAttribute('aria-label', lbl);
|
|
761
|
+
resolve.addEventListener('click', function () { toggleResolved(c.id); });
|
|
762
|
+
actions.appendChild(resolve);
|
|
763
|
+
}
|
|
764
|
+
// Edit — author (or admin) can edit their own comment, matching the
|
|
765
|
+
// screen/wireflow panels. Was missing here (storybook only had reply).
|
|
766
|
+
var u0 = state.currentUser;
|
|
767
|
+
var canEdit = u0 && ((c.userId && c.userId === u0.id) || u0.role === 'admin');
|
|
768
|
+
if (canEdit) {
|
|
769
|
+
var edit = document.createElement('button');
|
|
770
|
+
edit.type = 'button'; edit.className = 'dsc-icon-btn';
|
|
771
|
+
edit.title = 'Edit comment'; edit.setAttribute('aria-label', 'Edit comment');
|
|
772
|
+
edit.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 20h9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
773
|
+
edit.addEventListener('click', function () { editComment(c, text); });
|
|
774
|
+
actions.appendChild(edit);
|
|
775
|
+
}
|
|
776
|
+
meta.appendChild(actions);
|
|
777
|
+
}
|
|
778
|
+
card.appendChild(meta);
|
|
779
|
+
// Replies (one level) under the parent + a Reply affordance.
|
|
780
|
+
var replies = state.comments
|
|
781
|
+
.filter(function (r) { return r.parentId === c.id; })
|
|
782
|
+
.sort(function (a, b) { return String(a.createdAt || '').localeCompare(String(b.createdAt || '')); });
|
|
783
|
+
if (replies.length) {
|
|
784
|
+
var rwrap = document.createElement('div');
|
|
785
|
+
rwrap.className = 'dsc-card__replies';
|
|
786
|
+
replies.forEach(function (r) { rwrap.appendChild(buildReplyEl(r)); });
|
|
787
|
+
card.appendChild(rwrap);
|
|
788
|
+
}
|
|
789
|
+
if (state.currentUser) {
|
|
790
|
+
var replyBtn = document.createElement('button');
|
|
791
|
+
replyBtn.type = 'button';
|
|
792
|
+
replyBtn.className = 'dsc-reply-btn';
|
|
793
|
+
replyBtn.textContent = 'Reply';
|
|
794
|
+
replyBtn.addEventListener('click', function () { openReplyComposer(c, card, replyBtn); });
|
|
795
|
+
card.appendChild(replyBtn);
|
|
796
|
+
}
|
|
797
|
+
listEl.appendChild(card);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
var openN = list.filter(function (c) { return !c.resolved; }).length;
|
|
801
|
+
countBadge.hidden = openN === 0;
|
|
802
|
+
countBadge.textContent = String(openN);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function applySession() {
|
|
806
|
+
// Signed out → read-only (no composer, plus a hint). Signed in →
|
|
807
|
+
// composer + the mentionable-people directory.
|
|
808
|
+
if (state.currentUser) {
|
|
809
|
+
composeEl.hidden = false;
|
|
810
|
+
// Managers' comments always wait on a designer — say so up front.
|
|
811
|
+
var hint = composeEl.querySelector('.dsc-manager-hint');
|
|
812
|
+
if (isManagerUser(state.currentUser)) {
|
|
813
|
+
if (!hint) {
|
|
814
|
+
hint = document.createElement('p');
|
|
815
|
+
hint.className = 'dsc-manager-hint';
|
|
816
|
+
hint.textContent = 'A designer reviews your comments before anything is changed.';
|
|
817
|
+
composeEl.insertBefore(hint, composeEl.firstChild);
|
|
818
|
+
}
|
|
819
|
+
} else if (hint) {
|
|
820
|
+
hint.remove();
|
|
821
|
+
}
|
|
822
|
+
loadPeople().then(render);
|
|
823
|
+
} else {
|
|
824
|
+
composeEl.hidden = true;
|
|
825
|
+
}
|
|
826
|
+
render();
|
|
827
|
+
if (!state.currentUser && state.id && listEl) {
|
|
828
|
+
var note = document.createElement('div');
|
|
829
|
+
note.className = 'dsc-readonly';
|
|
830
|
+
note.textContent = 'Sign in to the dashboard to post or resolve comments.';
|
|
831
|
+
listEl.appendChild(note);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Switch the active thread when the story/component changes.
|
|
836
|
+
function syncContext() {
|
|
837
|
+
var version = dsVersion();
|
|
838
|
+
var comp = currentComponent();
|
|
839
|
+
if (!version || !comp) {
|
|
840
|
+
// No identifiable component (e.g. the welcome page) — show a hint.
|
|
841
|
+
if (titleEl) { titleEl.innerHTML = 'Comments<small>Open a component to comment</small>'; }
|
|
842
|
+
state.id = null; state.comments = [];
|
|
843
|
+
if (listEl) { listEl.innerHTML = '<div class="dsc-empty">Select a component in the sidebar to view its comment thread.</div>'; }
|
|
844
|
+
if (composeEl) composeEl.hidden = true;
|
|
845
|
+
if (countBadge) countBadge.hidden = true;
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
var nextId = threadId(version, comp.component);
|
|
849
|
+
if (nextId === state.id) {
|
|
850
|
+
// Same component, possibly a different story — refresh the picker so it
|
|
851
|
+
// defaults to whatever story you're now viewing.
|
|
852
|
+
buildStorySelect();
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
state.version = version; state.comp = comp; state.id = nextId;
|
|
856
|
+
titleEl.innerHTML = 'Comments<small>' + comp.component + ' · ' + version + '</small>';
|
|
857
|
+
state.comments = [];
|
|
858
|
+
mClose();
|
|
859
|
+
loadComments();
|
|
860
|
+
loadStories();
|
|
861
|
+
applySession();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function start() {
|
|
865
|
+
buildDom();
|
|
866
|
+
refreshSession().then(function () {
|
|
867
|
+
applySession();
|
|
868
|
+
syncContext();
|
|
869
|
+
});
|
|
870
|
+
// The manager keeps state in the URL — react to navigation between
|
|
871
|
+
// stories/components. Storybook uses pushState (no hashchange in SB10),
|
|
872
|
+
// so poll the URL alongside the standard events.
|
|
873
|
+
var lastHref = window.location.href;
|
|
874
|
+
window.addEventListener('hashchange', syncContext);
|
|
875
|
+
window.addEventListener('popstate', syncContext);
|
|
876
|
+
setInterval(function () {
|
|
877
|
+
if (window.location.href !== lastHref) { lastHref = window.location.href; syncContext(); }
|
|
878
|
+
}, 400);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (document.readyState === 'loading') {
|
|
882
|
+
document.addEventListener('DOMContentLoaded', start);
|
|
883
|
+
} else {
|
|
884
|
+
start();
|
|
885
|
+
}
|
|
886
|
+
})();
|
|
887
|
+
</script>
|
|
888
|
+
|
|
889
|
+
<!-- Sidebar comment-count badges: annotate each component node in the
|
|
890
|
+
Storybook sidebar with its number of open comments, so you can see at a
|
|
891
|
+
glance which components have unresolved feedback. Counts come from the
|
|
892
|
+
same per-thread KV the comment bar uses, via /__wf-comment-counts.json. -->
|
|
893
|
+
<script>
|
|
894
|
+
(function () {
|
|
895
|
+
if (window.self !== window.top) return;
|
|
896
|
+
var BUCKETS = ['foundations', 'components', 'utilities', 'os', 'screen-patterns', 'design-patterns'];
|
|
897
|
+
function sanitize(s) { return String(s || '').replace(/[^A-Za-z0-9_-]/g, '-'); }
|
|
898
|
+
function dsVersion() {
|
|
899
|
+
var m = /\/ds\/([^/]+)\/storybook\//.exec(window.location.pathname);
|
|
900
|
+
return m ? m[1] : null;
|
|
901
|
+
}
|
|
902
|
+
function threadId(v, c) { return 'DS-' + sanitize(v) + '-' + sanitize(c); }
|
|
903
|
+
// A sidebar component node's data-item-id is `<bucket>-<component>` (no
|
|
904
|
+
// `--`, which marks stories/docs). Strip the bucket → the component slug.
|
|
905
|
+
function componentOf(id) {
|
|
906
|
+
if (!id || id.indexOf('--') >= 0) return null;
|
|
907
|
+
for (var i = 0; i < BUCKETS.length; i++) {
|
|
908
|
+
var p = BUCKETS[i] + '-';
|
|
909
|
+
if (id.indexOf(p) === 0) return id.slice(p.length);
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
var counts = {};
|
|
914
|
+
function styleOnce() {
|
|
915
|
+
if (document.getElementById('dsc-side-style')) return;
|
|
916
|
+
var s = document.createElement('style');
|
|
917
|
+
s.id = 'dsc-side-style';
|
|
918
|
+
s.textContent =
|
|
919
|
+
'.dsc-side-badge{margin-left:auto;flex:none;min-width:18px;height:18px;' +
|
|
920
|
+
'padding:0 5px;box-sizing:border-box;border-radius:9px;background:#f97316;' +
|
|
921
|
+
'color:#fff;font-size:11px;font-weight:600;line-height:18px;text-align:center;' +
|
|
922
|
+
'transform:translate(-3px,2px);}';
|
|
923
|
+
document.head.appendChild(s);
|
|
924
|
+
}
|
|
925
|
+
function apply() {
|
|
926
|
+
var v = dsVersion();
|
|
927
|
+
if (!v) return;
|
|
928
|
+
var nodes = document.querySelectorAll('[data-nodetype="component"]');
|
|
929
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
930
|
+
var node = nodes[i];
|
|
931
|
+
var comp = componentOf(node.getAttribute('data-item-id'));
|
|
932
|
+
var n = comp ? (counts[threadId(v, comp)] || 0) : 0;
|
|
933
|
+
var b = node.querySelector(':scope > .dsc-side-badge');
|
|
934
|
+
if (n > 0) {
|
|
935
|
+
if (!b) { b = document.createElement('span'); b.className = 'dsc-side-badge'; node.appendChild(b); }
|
|
936
|
+
var t = String(n);
|
|
937
|
+
if (b.textContent !== t) b.textContent = t; // only mutate on change → no observer loop
|
|
938
|
+
} else if (b) {
|
|
939
|
+
b.parentNode.removeChild(b);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function load() {
|
|
944
|
+
fetch('/__wf-comment-counts.json', { cache: 'no-store', credentials: 'include' })
|
|
945
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
946
|
+
.then(function (d) { if (d && d.counts) { counts = d.counts; apply(); } })
|
|
947
|
+
.catch(function () {});
|
|
948
|
+
}
|
|
949
|
+
function start() {
|
|
950
|
+
styleOnce();
|
|
951
|
+
load();
|
|
952
|
+
var pend = null;
|
|
953
|
+
new MutationObserver(function () {
|
|
954
|
+
if (pend) return;
|
|
955
|
+
pend = setTimeout(function () { pend = null; apply(); }, 150);
|
|
956
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
957
|
+
setInterval(load, 30000); // pick up new/resolved comments
|
|
958
|
+
window.addEventListener('focus', load);
|
|
959
|
+
}
|
|
960
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
|
|
961
|
+
else start();
|
|
962
|
+
})();
|
|
963
|
+
</script>
|