@obvi/blueprint 1.1.3 → 1.3.0
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/README.md +21 -0
- package/dist/blueprint-choices.js +329 -163
- package/dist/blueprint-contract.d.ts +15 -0
- package/dist/blueprint-contract.js +1 -0
- package/dist/blueprint-contract.json +13 -0
- package/dist/blueprint.css +503 -96
- package/dist/blueprint.js +439 -193
- package/dist/vendor/blueprint/blueprint.css +6147 -0
- package/dist/vendor/blueprint/blueprint.js +3059 -0
- package/package.json +7 -1
package/dist/blueprint.js
CHANGED
|
@@ -9,7 +9,7 @@ function extractRationale(root) {
|
|
|
9
9
|
if (!node) return null;
|
|
10
10
|
const wrap = node.ownerDocument.createElement("div");
|
|
11
11
|
wrap.className = "bp-choice-rationale";
|
|
12
|
-
wrap.append(...node.childNodes);
|
|
12
|
+
wrap.append(...[...node.childNodes].map((child) => child.cloneNode(true)));
|
|
13
13
|
return wrap;
|
|
14
14
|
}
|
|
15
15
|
function bodyWithoutRationale(root) {
|
|
@@ -120,6 +120,187 @@ function buildResolvedChoice(doc, { options, verdictKicker, resolvedValue }) {
|
|
|
120
120
|
root.append(stack);
|
|
121
121
|
return root;
|
|
122
122
|
}
|
|
123
|
+
function buildChoiceManifest(host, layout, options, resolvedValue) {
|
|
124
|
+
return {
|
|
125
|
+
id: host.id || host.getAttribute("name") || "",
|
|
126
|
+
label: host.getAttribute("data-obvious-choice-label") || host.getAttribute("aria-label") || host.getAttribute("name") || host.id || "",
|
|
127
|
+
layout,
|
|
128
|
+
resolvedValue,
|
|
129
|
+
options: options.map((option) => ({
|
|
130
|
+
value: option.value,
|
|
131
|
+
label: option.title || option.tab || option.value,
|
|
132
|
+
title: option.title
|
|
133
|
+
}))
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function dispatchChoiceEvent(host, type, detail) {
|
|
137
|
+
host.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail }));
|
|
138
|
+
}
|
|
139
|
+
function setChoiceControlsDisabled(root, disabled) {
|
|
140
|
+
for (const control of root.querySelectorAll("input, button")) {
|
|
141
|
+
if (control instanceof HTMLInputElement || control instanceof HTMLButtonElement) {
|
|
142
|
+
control.disabled = disabled;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function appendLockedHostActions(doc, root, state) {
|
|
147
|
+
const actions = el(doc, "div", "bp-choice__actions");
|
|
148
|
+
const status = el(doc, "output", "bp-choice__hint");
|
|
149
|
+
status.setAttribute("aria-live", "polite");
|
|
150
|
+
status.textContent = state.message;
|
|
151
|
+
const reset = el(doc, "button", "bp-choice__reset");
|
|
152
|
+
reset.type = "button";
|
|
153
|
+
reset.disabled = state.busy;
|
|
154
|
+
reset.textContent = state.busy ? "Saving…" : state.reconsider;
|
|
155
|
+
actions.append(status, reset);
|
|
156
|
+
root.append(actions);
|
|
157
|
+
return reset;
|
|
158
|
+
}
|
|
159
|
+
function buildInteractiveChoice(doc, source) {
|
|
160
|
+
const {
|
|
161
|
+
layout,
|
|
162
|
+
options,
|
|
163
|
+
verdictKicker,
|
|
164
|
+
adoptLabel,
|
|
165
|
+
hint,
|
|
166
|
+
reconsider,
|
|
167
|
+
compareSummary,
|
|
168
|
+
compareBody,
|
|
169
|
+
scopeId,
|
|
170
|
+
initialValue,
|
|
171
|
+
busy,
|
|
172
|
+
message
|
|
173
|
+
} = source;
|
|
174
|
+
const viewName = `${scopeId}-view`;
|
|
175
|
+
const pickName = `${scopeId}-pick`;
|
|
176
|
+
const form = el(doc, "form", `bp-choice bp-choice--${layout}`);
|
|
177
|
+
form.dataset.bpChoice = scopeId;
|
|
178
|
+
const titleOpts = options.map((option) => ({ value: option.value, title: option.title }));
|
|
179
|
+
form.append(buildVerdict(doc, verdictKicker, titleOpts));
|
|
180
|
+
if (layout === "tabs") {
|
|
181
|
+
const selectedView = options.some((option) => option.value === initialValue) ? initialValue : options[0]?.value;
|
|
182
|
+
const seg = el(doc, "div", "bp-choice__seg");
|
|
183
|
+
seg.setAttribute("role", "tablist");
|
|
184
|
+
for (const opt of options) {
|
|
185
|
+
const segOpt = el(doc, "label", "bp-choice__seg-opt");
|
|
186
|
+
const input = doc.createElement("input");
|
|
187
|
+
input.type = "radio";
|
|
188
|
+
input.name = viewName;
|
|
189
|
+
input.value = opt.value;
|
|
190
|
+
input.className = "bp-choice__view";
|
|
191
|
+
if (opt.value === selectedView) {
|
|
192
|
+
input.defaultChecked = true;
|
|
193
|
+
input.checked = true;
|
|
194
|
+
}
|
|
195
|
+
segOpt.append(input, doc.createTextNode(opt.tab));
|
|
196
|
+
seg.append(segOpt);
|
|
197
|
+
}
|
|
198
|
+
form.append(seg);
|
|
199
|
+
const panels = el(doc, "div", "bp-choice__panels");
|
|
200
|
+
for (const opt of options) {
|
|
201
|
+
const panel = el(doc, "div", "bp-choice__panel");
|
|
202
|
+
panel.dataset.value = opt.value;
|
|
203
|
+
const h3 = el(doc, "h3");
|
|
204
|
+
h3.textContent = opt.title;
|
|
205
|
+
panel.append(h3);
|
|
206
|
+
panel.append(bodyWithoutRationale(opt.el));
|
|
207
|
+
const rationale = extractRationale(opt.el);
|
|
208
|
+
if (rationale) panel.append(rationale);
|
|
209
|
+
const actions = el(doc, "div", "bp-choice__actions");
|
|
210
|
+
const adopt = el(doc, "label", "bp-choice__adopt");
|
|
211
|
+
const commit = doc.createElement("input");
|
|
212
|
+
commit.type = "radio";
|
|
213
|
+
commit.name = pickName;
|
|
214
|
+
commit.value = opt.value;
|
|
215
|
+
commit.className = "bp-choice__commit";
|
|
216
|
+
adopt.append(commit, doc.createTextNode(adoptLabel));
|
|
217
|
+
actions.append(adopt);
|
|
218
|
+
panel.append(actions);
|
|
219
|
+
panels.append(panel);
|
|
220
|
+
}
|
|
221
|
+
form.append(panels);
|
|
222
|
+
const style = el(doc, "style");
|
|
223
|
+
style.textContent = scopedTabRules(`[data-bp-choice="${scopeId}"]`, viewName, pickName, titleOpts);
|
|
224
|
+
form.prepend(style);
|
|
225
|
+
form.append(buildFooter(doc, hint || "Switch tabs to preview · adopt to commit", reconsider));
|
|
226
|
+
}
|
|
227
|
+
if (layout === "stack") {
|
|
228
|
+
const stack = el(doc, "div", "bp-choice__stack");
|
|
229
|
+
for (const opt of options) {
|
|
230
|
+
const card = el(doc, "label", "bp-choice__card");
|
|
231
|
+
const input = doc.createElement("input");
|
|
232
|
+
input.type = "radio";
|
|
233
|
+
input.name = pickName;
|
|
234
|
+
input.value = opt.value;
|
|
235
|
+
input.className = "bp-choice__pick";
|
|
236
|
+
card.append(input);
|
|
237
|
+
const head = el(doc, "div", "bp-choice__card-head");
|
|
238
|
+
if (opt.optionLabel) head.append(label(doc, opt.optionLabel));
|
|
239
|
+
const chosen = el(doc, "span", "bp-choice__tag bp-choice__tag--ink bp-choice__card-chosen");
|
|
240
|
+
chosen.textContent = "✓ Chosen";
|
|
241
|
+
const rejected = el(doc, "span", "bp-choice__tag bp-choice__tag--out bp-choice__card-rejected");
|
|
242
|
+
rejected.textContent = "Not chosen";
|
|
243
|
+
head.append(chosen, rejected);
|
|
244
|
+
card.append(head);
|
|
245
|
+
const h4 = el(doc, "h4");
|
|
246
|
+
h4.textContent = opt.title;
|
|
247
|
+
card.append(h4);
|
|
248
|
+
card.append(bodyWithoutRationale(opt.el));
|
|
249
|
+
const rationale = extractRationale(opt.el);
|
|
250
|
+
if (rationale) card.append(rationale);
|
|
251
|
+
stack.append(card);
|
|
252
|
+
}
|
|
253
|
+
form.append(stack);
|
|
254
|
+
form.append(buildFooter(doc, hint || "Select a card to commit · rejected drafts stay readable below", reconsider));
|
|
255
|
+
}
|
|
256
|
+
if (layout === "gallery") {
|
|
257
|
+
const gallery = el(doc, "div", "bp-choice__gallery");
|
|
258
|
+
for (const opt of options) {
|
|
259
|
+
const mock = el(doc, "label", "bp-choice__mock");
|
|
260
|
+
const input = doc.createElement("input");
|
|
261
|
+
input.type = "radio";
|
|
262
|
+
input.name = pickName;
|
|
263
|
+
input.value = opt.value;
|
|
264
|
+
input.className = "bp-choice__pick";
|
|
265
|
+
mock.append(input);
|
|
266
|
+
const frame = el(doc, "div", "bp-choice__mock-frame");
|
|
267
|
+
const frameInner = frameContent(opt.el);
|
|
268
|
+
if (frameInner) frame.append(frameInner);
|
|
269
|
+
mock.append(frame);
|
|
270
|
+
const cap = el(doc, "div", "bp-choice__mock-cap");
|
|
271
|
+
const h4 = el(doc, "h4");
|
|
272
|
+
h4.textContent = opt.caption;
|
|
273
|
+
cap.append(h4);
|
|
274
|
+
const chosen = el(doc, "span", "bp-choice__tag bp-choice__tag--ink bp-choice__mock-pick bp-choice__mock-chosen");
|
|
275
|
+
chosen.textContent = "✓ Chosen";
|
|
276
|
+
const rejected = el(doc, "span", "bp-choice__tag bp-choice__tag--out bp-choice__mock-pick bp-choice__mock-rejected");
|
|
277
|
+
rejected.textContent = "Not chosen";
|
|
278
|
+
cap.append(chosen, rejected);
|
|
279
|
+
mock.append(cap);
|
|
280
|
+
gallery.append(mock);
|
|
281
|
+
}
|
|
282
|
+
form.append(gallery);
|
|
283
|
+
if (compareSummary && compareBody) {
|
|
284
|
+
const details = el(doc, "details", "bp-choice__compare");
|
|
285
|
+
const summary = el(doc, "summary");
|
|
286
|
+
summary.textContent = compareSummary;
|
|
287
|
+
details.append(summary);
|
|
288
|
+
const body = el(doc, "div", "bp-choice__compare-body");
|
|
289
|
+
body.append(compareBody.cloneNode(true));
|
|
290
|
+
details.append(body);
|
|
291
|
+
form.append(details);
|
|
292
|
+
}
|
|
293
|
+
form.append(buildFooter(doc, hint || "Select a mockup to track the decision", reconsider));
|
|
294
|
+
}
|
|
295
|
+
if (message) {
|
|
296
|
+
const status = el(doc, "output", "bp-choice__hint");
|
|
297
|
+
status.setAttribute("aria-live", "polite");
|
|
298
|
+
status.textContent = message;
|
|
299
|
+
form.append(status);
|
|
300
|
+
}
|
|
301
|
+
setChoiceControlsDisabled(form, busy);
|
|
302
|
+
return form;
|
|
303
|
+
}
|
|
123
304
|
var BlueprintRationaleElement = class extends HTMLElement {
|
|
124
305
|
connectedCallback() {
|
|
125
306
|
if (this.dataset.bpRendered) return;
|
|
@@ -130,158 +311,104 @@ var BlueprintRationaleElement = class extends HTMLElement {
|
|
|
130
311
|
var BlueprintChoiceOptionElement = class extends HTMLElement {
|
|
131
312
|
};
|
|
132
313
|
var BlueprintChoiceElement = class extends HTMLElement {
|
|
314
|
+
constructor() {
|
|
315
|
+
super();
|
|
316
|
+
this.choiceSource = null;
|
|
317
|
+
this.choiceHostState = null;
|
|
318
|
+
this.choiceScopeId = null;
|
|
319
|
+
}
|
|
133
320
|
connectedCallback() {
|
|
134
|
-
if (this.
|
|
321
|
+
if (this.choiceSource) return;
|
|
135
322
|
this.dataset.bpRendered = "1";
|
|
136
|
-
const doc = this.ownerDocument;
|
|
137
323
|
const layout = (this.getAttribute("layout") || "stack").toLowerCase();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
style.textContent = scopedTabRules(`[data-bp-choice="${scopeId}"]`, viewName, pickName, titleOpts);
|
|
200
|
-
form.prepend(style);
|
|
201
|
-
form.append(
|
|
202
|
-
buildFooter(
|
|
203
|
-
doc,
|
|
204
|
-
hint || "Switch tabs to preview · adopt to commit",
|
|
205
|
-
reconsider
|
|
206
|
-
)
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
if (layout === "stack") {
|
|
210
|
-
const stack = el(doc, "div", "bp-choice__stack");
|
|
211
|
-
for (const opt of options) {
|
|
212
|
-
const card = el(doc, "label", "bp-choice__card");
|
|
213
|
-
const input = doc.createElement("input");
|
|
214
|
-
input.type = "radio";
|
|
215
|
-
input.name = pickName;
|
|
216
|
-
input.value = opt.value;
|
|
217
|
-
input.className = "bp-choice__pick";
|
|
218
|
-
card.append(input);
|
|
219
|
-
const head = el(doc, "div", "bp-choice__card-head");
|
|
220
|
-
if (opt.optionLabel) head.append(label(doc, opt.optionLabel));
|
|
221
|
-
const chosen = el(doc, "span", "bp-choice__tag bp-choice__tag--ink bp-choice__card-chosen");
|
|
222
|
-
chosen.textContent = "✓ Chosen";
|
|
223
|
-
const rejected = el(doc, "span", "bp-choice__tag bp-choice__tag--out bp-choice__card-rejected");
|
|
224
|
-
rejected.textContent = "Not chosen";
|
|
225
|
-
head.append(chosen, rejected);
|
|
226
|
-
card.append(head);
|
|
227
|
-
const h4 = el(doc, "h4");
|
|
228
|
-
h4.textContent = opt.title;
|
|
229
|
-
card.append(h4);
|
|
230
|
-
card.append(bodyWithoutRationale(opt.el));
|
|
231
|
-
const rationale = extractRationale(opt.el);
|
|
232
|
-
if (rationale) card.append(rationale);
|
|
233
|
-
stack.append(card);
|
|
234
|
-
}
|
|
235
|
-
form.append(stack);
|
|
236
|
-
form.append(
|
|
237
|
-
buildFooter(
|
|
238
|
-
doc,
|
|
239
|
-
hint || "Select a card to commit · rejected drafts stay readable below",
|
|
240
|
-
reconsider
|
|
241
|
-
)
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
if (layout === "gallery") {
|
|
245
|
-
const gallery = el(doc, "div", "bp-choice__gallery");
|
|
246
|
-
for (const opt of options) {
|
|
247
|
-
const mock = el(doc, "label", "bp-choice__mock");
|
|
248
|
-
const input = doc.createElement("input");
|
|
249
|
-
input.type = "radio";
|
|
250
|
-
input.name = pickName;
|
|
251
|
-
input.value = opt.value;
|
|
252
|
-
input.className = "bp-choice__pick";
|
|
253
|
-
mock.append(input);
|
|
254
|
-
const frame = el(doc, "div", "bp-choice__mock-frame");
|
|
255
|
-
const frameInner = frameContent(opt.el);
|
|
256
|
-
if (frameInner) frame.append(frameInner);
|
|
257
|
-
mock.append(frame);
|
|
258
|
-
const cap = el(doc, "div", "bp-choice__mock-cap");
|
|
259
|
-
const h4 = el(doc, "h4");
|
|
260
|
-
h4.textContent = opt.caption;
|
|
261
|
-
cap.append(h4);
|
|
262
|
-
const chosen = el(doc, "span", "bp-choice__tag bp-choice__tag--ink bp-choice__mock-pick bp-choice__mock-chosen");
|
|
263
|
-
chosen.textContent = "✓ Chosen";
|
|
264
|
-
const rejected = el(doc, "span", "bp-choice__tag bp-choice__tag--out bp-choice__mock-pick bp-choice__mock-rejected");
|
|
265
|
-
rejected.textContent = "Not chosen";
|
|
266
|
-
cap.append(chosen, rejected);
|
|
267
|
-
mock.append(cap);
|
|
268
|
-
gallery.append(mock);
|
|
269
|
-
}
|
|
270
|
-
form.append(gallery);
|
|
271
|
-
if (compareSummary && compareBody) {
|
|
272
|
-
const details = el(doc, "details", "bp-choice__compare");
|
|
273
|
-
const summary = el(doc, "summary");
|
|
274
|
-
summary.textContent = compareSummary;
|
|
275
|
-
details.append(summary);
|
|
276
|
-
const body = el(doc, "div", "bp-choice__compare-body");
|
|
277
|
-
body.append(compareBody.cloneNode(true));
|
|
278
|
-
details.append(body);
|
|
279
|
-
form.append(details);
|
|
324
|
+
this.choiceSource = {
|
|
325
|
+
layout,
|
|
326
|
+
options: readOptions(this),
|
|
327
|
+
verdictKicker: this.getAttribute("verdict") ?? "Chosen",
|
|
328
|
+
adoptLabel: this.getAttribute("adopt") ?? "Adopt this direction",
|
|
329
|
+
hint: this.getAttribute("hint") ?? "",
|
|
330
|
+
reconsider: this.getAttribute("reconsider") ?? "Reconsider",
|
|
331
|
+
compareSummary: this.getAttribute("compare"),
|
|
332
|
+
compareBody: this.querySelector(':scope > [slot="compare"]'),
|
|
333
|
+
resolvedValue: this.getAttribute("resolved")
|
|
334
|
+
};
|
|
335
|
+
this.choiceScopeId = nextId("bp-choice");
|
|
336
|
+
this.renderChoice();
|
|
337
|
+
dispatchChoiceEvent(this, "bp-choice-ready", this.getDecisionManifest());
|
|
338
|
+
}
|
|
339
|
+
getDecisionManifest() {
|
|
340
|
+
if (!this.choiceSource) return null;
|
|
341
|
+
return buildChoiceManifest(
|
|
342
|
+
this,
|
|
343
|
+
this.choiceSource.layout,
|
|
344
|
+
this.choiceSource.options,
|
|
345
|
+
this.choiceSource.resolvedValue
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Apply ephemeral host-owned state without mutating authored attributes.
|
|
350
|
+
* Passing null restores the authored open/resolved rendering.
|
|
351
|
+
*
|
|
352
|
+
* @param {{ status: 'open' | 'locked', value: string | null, busy?: boolean, message?: string } | null} state
|
|
353
|
+
*/
|
|
354
|
+
applyDecisionState(state) {
|
|
355
|
+
if (!this.choiceSource) return false;
|
|
356
|
+
if (state !== null) {
|
|
357
|
+
const valueExists = state.value === null || this.choiceSource.options.some((option) => option.value === state.value);
|
|
358
|
+
if (state.status !== "open" && state.status !== "locked" || !valueExists) return false;
|
|
359
|
+
if (state.status === "locked" && state.value === null) return false;
|
|
360
|
+
}
|
|
361
|
+
this.choiceHostState = state ? { status: state.status, value: state.value, busy: Boolean(state.busy), message: state.message ?? "" } : null;
|
|
362
|
+
this.renderChoice();
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
renderChoice() {
|
|
366
|
+
if (!this.choiceSource || !this.choiceScopeId) return;
|
|
367
|
+
const source = this.choiceSource;
|
|
368
|
+
const hostState = this.choiceHostState;
|
|
369
|
+
const lockedValue = hostState?.status === "locked" ? hostState.value : hostState ? null : source.resolvedValue;
|
|
370
|
+
if (lockedValue !== null) {
|
|
371
|
+
const root = buildResolvedChoice(this.ownerDocument, {
|
|
372
|
+
options: source.options,
|
|
373
|
+
verdictKicker: source.verdictKicker,
|
|
374
|
+
resolvedValue: lockedValue
|
|
375
|
+
});
|
|
376
|
+
if (hostState) {
|
|
377
|
+
const reset = appendLockedHostActions(this.ownerDocument, root, {
|
|
378
|
+
message: hostState.message,
|
|
379
|
+
reconsider: source.reconsider,
|
|
380
|
+
busy: hostState.busy
|
|
381
|
+
});
|
|
382
|
+
reset.addEventListener("click", () => {
|
|
383
|
+
dispatchChoiceEvent(this, "bp-choice-reconsider", this.getDecisionManifest());
|
|
384
|
+
});
|
|
280
385
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
);
|
|
386
|
+
this.replaceChildren(root);
|
|
387
|
+
return;
|
|
284
388
|
}
|
|
389
|
+
const form = buildInteractiveChoice(this.ownerDocument, {
|
|
390
|
+
...source,
|
|
391
|
+
scopeId: this.choiceScopeId,
|
|
392
|
+
initialValue: hostState?.value ?? null,
|
|
393
|
+
busy: hostState?.busy ?? false,
|
|
394
|
+
message: hostState?.message ?? ""
|
|
395
|
+
});
|
|
396
|
+
form.addEventListener("change", (event) => {
|
|
397
|
+
const input = event.target;
|
|
398
|
+
if (!(input instanceof HTMLInputElement) || !input.checked) return;
|
|
399
|
+
const isCommit = source.layout === "tabs" ? input.classList.contains("bp-choice__commit") : input.classList.contains("bp-choice__pick");
|
|
400
|
+
if (!isCommit) return;
|
|
401
|
+
const option = source.options.find((candidate) => candidate.value === input.value);
|
|
402
|
+
if (!option) return;
|
|
403
|
+
dispatchChoiceEvent(this, "bp-choice-commit", {
|
|
404
|
+
...this.getDecisionManifest(),
|
|
405
|
+
value: option.value,
|
|
406
|
+
option: { value: option.value, label: option.title || option.tab || option.value, title: option.title }
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
form.addEventListener("reset", () => {
|
|
410
|
+
dispatchChoiceEvent(this, "bp-choice-reconsider", this.getDecisionManifest());
|
|
411
|
+
});
|
|
285
412
|
this.replaceChildren(form);
|
|
286
413
|
}
|
|
287
414
|
};
|
|
@@ -756,11 +883,31 @@ function injectSidebar(doc, win) {
|
|
|
756
883
|
searchHint.append(searchHintCmd, searchHintKey);
|
|
757
884
|
searchField.append(searchIcon, searchLabel, searchHint);
|
|
758
885
|
search.append(searchField);
|
|
759
|
-
panel.append(search);
|
|
760
886
|
const switcher = buildDocSwitcher(doc, win);
|
|
761
|
-
|
|
887
|
+
const tocSwitcher = doc.createElement("details");
|
|
888
|
+
tocSwitcher.className = "bp-toc-switcher";
|
|
889
|
+
tocSwitcher.open = true;
|
|
890
|
+
const tocSummary = doc.createElement("summary");
|
|
891
|
+
tocSummary.className = "bp-toc-switcher__current sidebar-field";
|
|
892
|
+
tocSummary.setAttribute("aria-label", "Jump to section");
|
|
893
|
+
const tocLabel = doc.createElement("span");
|
|
894
|
+
tocLabel.className = "bp-toc-switcher__label";
|
|
895
|
+
const tocTitle = doc.createElement("span");
|
|
896
|
+
tocTitle.className = "bp-toc-switcher__title";
|
|
897
|
+
tocTitle.textContent = "Contents";
|
|
898
|
+
tocLabel.append(tocTitle);
|
|
899
|
+
const tocChevron = doc.createElement("span");
|
|
900
|
+
tocChevron.className = "bp-toc-switcher__chevron bp-transition-transform bp-duration-normal bp-ease-in-out";
|
|
901
|
+
tocChevron.setAttribute("aria-hidden", "true");
|
|
902
|
+
tocChevron.append(createChromeSvg(doc, DOC_CHEVRON_ICON));
|
|
903
|
+
tocSummary.append(tocLabel, tocChevron);
|
|
762
904
|
const list = doc.createElement("ul");
|
|
763
|
-
|
|
905
|
+
tocSwitcher.append(tocSummary, list);
|
|
906
|
+
const mobileHead = doc.createElement("div");
|
|
907
|
+
mobileHead.className = "sidebar-mobile-head";
|
|
908
|
+
mobileHead.append(tocSwitcher, search);
|
|
909
|
+
panel.append(mobileHead);
|
|
910
|
+
if (switcher) panel.append(switcher);
|
|
764
911
|
nav.append(panel, buildSidebarFooter(doc));
|
|
765
912
|
return nav;
|
|
766
913
|
}
|
|
@@ -834,6 +981,7 @@ function setThemeAnimated(doc, win, theme) {
|
|
|
834
981
|
transition = doc.startViewTransition(runUpdate);
|
|
835
982
|
}
|
|
836
983
|
if (!applied) runUpdate();
|
|
984
|
+
transition?.ready?.catch(clearSwitching);
|
|
837
985
|
if (transition?.finished) {
|
|
838
986
|
transition.finished.finally(clearSwitching).catch(clearSwitching);
|
|
839
987
|
} else {
|
|
@@ -931,14 +1079,14 @@ function wireSearchPalette(doc, win) {
|
|
|
931
1079
|
let panel = null;
|
|
932
1080
|
let input = null;
|
|
933
1081
|
let results = null;
|
|
934
|
-
let
|
|
1082
|
+
let actionDefs = [];
|
|
935
1083
|
let entries = [];
|
|
936
1084
|
let visible = [];
|
|
937
1085
|
let selected = 0;
|
|
938
1086
|
let returnFocus = null;
|
|
939
1087
|
let prevOverflow = "";
|
|
940
1088
|
const collectEntries = () => {
|
|
941
|
-
const list = doc.querySelector(".bp-sidebar .bp-sidebar__panel > ul") ?? doc.querySelector(".bp-toc > ul");
|
|
1089
|
+
const list = doc.querySelector(".bp-sidebar .bp-sidebar__panel .bp-toc-switcher > ul") ?? doc.querySelector(".bp-sidebar .bp-sidebar__panel > ul") ?? doc.querySelector(".bp-toc > ul");
|
|
942
1090
|
if (!list) return [];
|
|
943
1091
|
const out = [];
|
|
944
1092
|
let group = "";
|
|
@@ -961,16 +1109,57 @@ function wireSearchPalette(doc, win) {
|
|
|
961
1109
|
}
|
|
962
1110
|
return out;
|
|
963
1111
|
};
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1112
|
+
const modGlyph = isMacLike ? "⌘" : "⌃";
|
|
1113
|
+
const matchesShortcut = (event, key) => {
|
|
1114
|
+
const mod = isMacLike ? event.metaKey : event.ctrlKey;
|
|
1115
|
+
if (!mod || !event.shiftKey) return false;
|
|
1116
|
+
return event.key.toLowerCase() === key.toLowerCase();
|
|
1117
|
+
};
|
|
1118
|
+
const buildActionDefs = () => {
|
|
1119
|
+
const defs = [];
|
|
1120
|
+
if (doc.querySelector(".theme-toggle")) {
|
|
1121
|
+
defs.push({
|
|
1122
|
+
label: "Toggle theme",
|
|
1123
|
+
icon: THEME_MOON_ICON,
|
|
1124
|
+
chip: `${modGlyph}⇧L`,
|
|
1125
|
+
shortcut: "l",
|
|
1126
|
+
run: () => doc.querySelector(".theme-toggle")?.click()
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
if (doc.querySelector(".sidebar-toggle")) {
|
|
1130
|
+
defs.push({
|
|
1131
|
+
label: "Toggle sidebar",
|
|
1132
|
+
icon: SIDEBAR_COLLAPSE_ICON,
|
|
1133
|
+
chip: `${modGlyph}⇧B`,
|
|
1134
|
+
shortcut: "b",
|
|
1135
|
+
run: () => doc.querySelector(".sidebar-toggle")?.click()
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
return defs;
|
|
1139
|
+
};
|
|
1140
|
+
const buildAction = (def) => {
|
|
1141
|
+
const button = doc.createElement("button");
|
|
1142
|
+
button.type = "button";
|
|
1143
|
+
button.className = "docs-search__action bp-transition-colors bp-ease";
|
|
1144
|
+
const icon = createChromeSvg(doc, def.icon, 16);
|
|
1145
|
+
icon.classList.add("docs-search__action-icon");
|
|
1146
|
+
const label2 = doc.createElement("span");
|
|
1147
|
+
label2.className = "docs-search__action-label";
|
|
1148
|
+
label2.textContent = def.label;
|
|
1149
|
+
button.append(icon, label2);
|
|
1150
|
+
if (def.chip) {
|
|
1151
|
+
const chip = doc.createElement("kbd");
|
|
1152
|
+
chip.className = "docs-search__chip";
|
|
1153
|
+
for (const glyph of Array.from(def.chip)) {
|
|
1154
|
+
const key = doc.createElement("span");
|
|
1155
|
+
key.textContent = glyph;
|
|
1156
|
+
chip.append(key);
|
|
1157
|
+
}
|
|
1158
|
+
button.append(chip);
|
|
1159
|
+
}
|
|
1160
|
+
button.addEventListener("click", () => def.run(button));
|
|
1161
|
+
def.button = button;
|
|
1162
|
+
return button;
|
|
974
1163
|
};
|
|
975
1164
|
const highlight = (text, q) => {
|
|
976
1165
|
if (!q) return [doc.createTextNode(text)];
|
|
@@ -1000,6 +1189,11 @@ function wireSearchPalette(doc, win) {
|
|
|
1000
1189
|
});
|
|
1001
1190
|
const mainCol = doc.createElement("span");
|
|
1002
1191
|
mainCol.className = "docs-search__row-main";
|
|
1192
|
+
const iconBox = doc.createElement("span");
|
|
1193
|
+
iconBox.className = "docs-search__icon-box";
|
|
1194
|
+
iconBox.setAttribute("aria-hidden", "true");
|
|
1195
|
+
iconBox.textContent = "#";
|
|
1196
|
+
mainCol.append(iconBox);
|
|
1003
1197
|
if (entry.crumb) {
|
|
1004
1198
|
const crumb = doc.createElement("span");
|
|
1005
1199
|
crumb.className = "docs-search__crumb";
|
|
@@ -1008,11 +1202,7 @@ function wireSearchPalette(doc, win) {
|
|
|
1008
1202
|
}
|
|
1009
1203
|
const title = doc.createElement("span");
|
|
1010
1204
|
title.className = "docs-search__title";
|
|
1011
|
-
|
|
1012
|
-
hash.className = "docs-search__hash";
|
|
1013
|
-
hash.setAttribute("aria-hidden", "true");
|
|
1014
|
-
hash.textContent = "#";
|
|
1015
|
-
title.append(hash, ...highlight(entry.title, q));
|
|
1205
|
+
title.append(...highlight(entry.title, q));
|
|
1016
1206
|
mainCol.append(title);
|
|
1017
1207
|
const enter = doc.createElement("kbd");
|
|
1018
1208
|
enter.className = "docs-search__enter";
|
|
@@ -1020,19 +1210,22 @@ function wireSearchPalette(doc, win) {
|
|
|
1020
1210
|
row.append(mainCol, enter);
|
|
1021
1211
|
return row;
|
|
1022
1212
|
};
|
|
1023
|
-
const buildEmpty = (
|
|
1213
|
+
const buildEmpty = () => {
|
|
1024
1214
|
const wrap = doc.createElement("div");
|
|
1025
1215
|
wrap.className = "docs-search__empty";
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1216
|
+
const center = doc.createElement("div");
|
|
1217
|
+
center.className = "docs-search__empty-center";
|
|
1218
|
+
const copy = doc.createElement("div");
|
|
1219
|
+
copy.className = "docs-search__empty-copy";
|
|
1220
|
+
const title = doc.createElement("p");
|
|
1030
1221
|
title.className = "docs-search__empty-title";
|
|
1031
|
-
title.textContent =
|
|
1032
|
-
const note = doc.createElement("
|
|
1222
|
+
title.textContent = "No results found";
|
|
1223
|
+
const note = doc.createElement("p");
|
|
1033
1224
|
note.className = "docs-search__empty-note";
|
|
1034
|
-
note.textContent = "Try
|
|
1035
|
-
|
|
1225
|
+
note.textContent = "Try searching with different keywords";
|
|
1226
|
+
copy.append(title, note);
|
|
1227
|
+
center.append(copy);
|
|
1228
|
+
wrap.append(center);
|
|
1036
1229
|
return wrap;
|
|
1037
1230
|
};
|
|
1038
1231
|
const applySelection = () => {
|
|
@@ -1060,8 +1253,7 @@ function wireSearchPalette(doc, win) {
|
|
|
1060
1253
|
selected = 0;
|
|
1061
1254
|
results.replaceChildren();
|
|
1062
1255
|
if (visible.length === 0) {
|
|
1063
|
-
results.append(buildEmpty(
|
|
1064
|
-
footStatus.textContent = "0 results";
|
|
1256
|
+
results.append(buildEmpty());
|
|
1065
1257
|
input.removeAttribute("aria-activedescendant");
|
|
1066
1258
|
return;
|
|
1067
1259
|
}
|
|
@@ -1075,9 +1267,14 @@ function wireSearchPalette(doc, win) {
|
|
|
1075
1267
|
results.append(group);
|
|
1076
1268
|
visible.forEach((entry, index) => results.append(buildRow(entry, index, q)));
|
|
1077
1269
|
applySelection();
|
|
1078
|
-
footStatus.textContent = q ? `${visible.length} result${visible.length === 1 ? "" : "s"}` : "Jump to a section";
|
|
1079
1270
|
};
|
|
1080
1271
|
const onKeydown = (event) => {
|
|
1272
|
+
const action = actionDefs.find((def) => matchesShortcut(event, def.shortcut));
|
|
1273
|
+
if (action) {
|
|
1274
|
+
event.preventDefault();
|
|
1275
|
+
action.run(action.button);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1081
1278
|
if (event.key === "Escape") {
|
|
1082
1279
|
event.preventDefault();
|
|
1083
1280
|
close();
|
|
@@ -1088,6 +1285,7 @@ function wireSearchPalette(doc, win) {
|
|
|
1088
1285
|
event.preventDefault();
|
|
1089
1286
|
move(-1);
|
|
1090
1287
|
} else if (event.key === "Enter") {
|
|
1288
|
+
if (event.target?.closest?.(".docs-search__action")) return;
|
|
1091
1289
|
const row = results.querySelector(
|
|
1092
1290
|
`.docs-search__row[data-index="${selected}"]`
|
|
1093
1291
|
);
|
|
@@ -1125,12 +1323,12 @@ function wireSearchPalette(doc, win) {
|
|
|
1125
1323
|
input = doc.createElement("input");
|
|
1126
1324
|
input.className = "docs-search__input";
|
|
1127
1325
|
input.type = "search";
|
|
1128
|
-
input.placeholder = "Search the
|
|
1326
|
+
input.placeholder = "Search the blueprint…";
|
|
1129
1327
|
input.setAttribute("role", "combobox");
|
|
1130
1328
|
input.setAttribute("aria-autocomplete", "list");
|
|
1131
1329
|
input.setAttribute("aria-controls", "docs-search-listbox");
|
|
1132
1330
|
input.setAttribute("aria-expanded", "false");
|
|
1133
|
-
input.setAttribute("aria-label", "Search the
|
|
1331
|
+
input.setAttribute("aria-label", "Search the blueprint");
|
|
1134
1332
|
input.autocomplete = "off";
|
|
1135
1333
|
input.spellcheck = false;
|
|
1136
1334
|
const esc = doc.createElement("span");
|
|
@@ -1144,15 +1342,11 @@ function wireSearchPalette(doc, win) {
|
|
|
1144
1342
|
results.setAttribute("aria-label", "Search results");
|
|
1145
1343
|
const foot = doc.createElement("div");
|
|
1146
1344
|
foot.className = "docs-search__foot";
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
keyHint(["↵"], "open"),
|
|
1153
|
-
keyHint(["esc"], "close")
|
|
1154
|
-
);
|
|
1155
|
-
foot.append(footStatus, keys);
|
|
1345
|
+
const actions = doc.createElement("div");
|
|
1346
|
+
actions.className = "docs-search__actions";
|
|
1347
|
+
actionDefs = buildActionDefs();
|
|
1348
|
+
for (const def of actionDefs) actions.append(buildAction(def));
|
|
1349
|
+
foot.append(actions);
|
|
1156
1350
|
panel.append(field, results, foot);
|
|
1157
1351
|
overlay.append(panel);
|
|
1158
1352
|
(doc.body || doc.documentElement).append(overlay);
|
|
@@ -1393,7 +1587,7 @@ function initializeBlueprintRuntime(doc = document, win = window) {
|
|
|
1393
1587
|
if (!list || !listItem) return true;
|
|
1394
1588
|
const container = link.closest(".bp-sidebar, .bp-toc, .sidebar");
|
|
1395
1589
|
if (!container) return true;
|
|
1396
|
-
const topList = container.querySelector(":scope > .bp-sidebar__panel > ul") ?? container.querySelector(":scope > ul");
|
|
1590
|
+
const topList = container.querySelector(":scope > .bp-sidebar__panel .bp-toc-switcher > ul") ?? container.querySelector(":scope > .bp-sidebar__panel > ul") ?? container.querySelector(":scope > ul");
|
|
1397
1591
|
return list === topList;
|
|
1398
1592
|
};
|
|
1399
1593
|
const entries = [...doc.querySelectorAll('.bp-sidebar a[href^="#"], .bp-toc a[href^="#"], .sidebar a[href^="#"]')].filter(isPrimaryNavLink).map((link) => ({
|
|
@@ -1405,6 +1599,14 @@ function initializeBlueprintRuntime(doc = document, win = window) {
|
|
|
1405
1599
|
);
|
|
1406
1600
|
let scheduled = false;
|
|
1407
1601
|
let scrollingTo = null;
|
|
1602
|
+
const tocSwitchers = [...doc.querySelectorAll(".bp-toc-switcher")];
|
|
1603
|
+
const tocSwitcherTitles = [...doc.querySelectorAll(".bp-toc-switcher__title")];
|
|
1604
|
+
const railColumnQuery = win.matchMedia("(min-width: 861px)");
|
|
1605
|
+
const syncTocSwitcherOpen = () => {
|
|
1606
|
+
for (const sw of tocSwitchers) sw.open = railColumnQuery.matches;
|
|
1607
|
+
};
|
|
1608
|
+
syncTocSwitcherOpen();
|
|
1609
|
+
railColumnQuery.addEventListener("change", syncTocSwitcherOpen);
|
|
1408
1610
|
const placeMarker = (link) => {
|
|
1409
1611
|
const container = link.closest(".bp-sidebar, .bp-toc, .sidebar");
|
|
1410
1612
|
const list = container?.querySelector(":scope > ul") ?? container?.querySelector("ul");
|
|
@@ -1412,18 +1614,59 @@ function initializeBlueprintRuntime(doc = document, win = window) {
|
|
|
1412
1614
|
list.style.setProperty("--bp-toc-y", `${link.offsetTop}px`);
|
|
1413
1615
|
list.style.setProperty("--bp-toc-h", `${link.offsetHeight}px`);
|
|
1414
1616
|
};
|
|
1617
|
+
const revealInRail = (link) => {
|
|
1618
|
+
const panel = link.closest(".bp-sidebar__panel");
|
|
1619
|
+
if (!panel || panel.scrollHeight <= panel.clientHeight) return;
|
|
1620
|
+
const style = win.getComputedStyle(panel);
|
|
1621
|
+
const padTop = Number.parseFloat(style.scrollPaddingTop) || 0;
|
|
1622
|
+
const padBottom = Number.parseFloat(style.scrollPaddingBottom) || 0;
|
|
1623
|
+
const panelRect = panel.getBoundingClientRect();
|
|
1624
|
+
const linkRect = link.getBoundingClientRect();
|
|
1625
|
+
const safeTop = panelRect.top + padTop;
|
|
1626
|
+
const safeBottom = panelRect.bottom - padBottom;
|
|
1627
|
+
let delta = 0;
|
|
1628
|
+
if (linkRect.top < safeTop) delta = linkRect.top - safeTop;
|
|
1629
|
+
else if (linkRect.bottom > safeBottom) delta = linkRect.bottom - safeBottom;
|
|
1630
|
+
if (delta === 0) return;
|
|
1631
|
+
panel.scrollBy({
|
|
1632
|
+
top: delta,
|
|
1633
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth"
|
|
1634
|
+
});
|
|
1635
|
+
};
|
|
1636
|
+
let lastCurrent = null;
|
|
1415
1637
|
const setCurrent = (section) => {
|
|
1638
|
+
const changed = section !== lastCurrent;
|
|
1639
|
+
let activeLabel = "";
|
|
1416
1640
|
for (const item of entries) {
|
|
1417
1641
|
if (section && item.section === section) {
|
|
1418
1642
|
item.link.setAttribute("aria-current", "location");
|
|
1419
1643
|
placeMarker(item.link);
|
|
1644
|
+
if (changed) revealInRail(item.link);
|
|
1645
|
+
if (!activeLabel) activeLabel = item.link.textContent.trim();
|
|
1420
1646
|
} else {
|
|
1421
1647
|
item.link.removeAttribute("aria-current");
|
|
1422
1648
|
}
|
|
1423
1649
|
}
|
|
1650
|
+
lastCurrent = section;
|
|
1651
|
+
if (changed) {
|
|
1652
|
+
const summaryLabel = activeLabel || "Contents";
|
|
1653
|
+
for (const title of tocSwitcherTitles) title.textContent = summaryLabel;
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
const mobileHead = doc.querySelector(".sidebar-mobile-head");
|
|
1657
|
+
const contentsRail = doc.querySelector("#bp-contents-rail");
|
|
1658
|
+
const syncHeadDivider = () => {
|
|
1659
|
+
if (!mobileHead || !contentsRail) return;
|
|
1660
|
+
if (win.getComputedStyle(mobileHead).position !== "fixed") {
|
|
1661
|
+
mobileHead.removeAttribute("data-bp-head-stuck");
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const stuck = contentsRail.getBoundingClientRect().bottom <= mobileHead.getBoundingClientRect().bottom + 0.5;
|
|
1665
|
+
mobileHead.toggleAttribute("data-bp-head-stuck", stuck);
|
|
1424
1666
|
};
|
|
1425
1667
|
const update = () => {
|
|
1426
1668
|
scheduled = false;
|
|
1669
|
+
syncHeadDivider();
|
|
1427
1670
|
if (progress) {
|
|
1428
1671
|
const max = scrollHeight() - clientHeight();
|
|
1429
1672
|
progress.style.transform = `scaleX(${max > 0 ? scrollTop() / max : 0})`;
|
|
@@ -1505,6 +1748,9 @@ function initializeBlueprintRuntime(doc = document, win = window) {
|
|
|
1505
1748
|
event.preventDefault();
|
|
1506
1749
|
if (win.location.hash !== link.hash) win.history.pushState(null, "", link.hash);
|
|
1507
1750
|
if (entries.some(({ section: target }) => target === section)) setCurrent(section);
|
|
1751
|
+
if (!railColumnQuery.matches) {
|
|
1752
|
+
for (const sw of tocSwitchers) sw.open = false;
|
|
1753
|
+
}
|
|
1508
1754
|
scrollToSection(section);
|
|
1509
1755
|
});
|
|
1510
1756
|
let scrollSource = shellScroller ?? win;
|
|
@@ -1765,7 +2011,7 @@ function renderContentsTree(doc, tree) {
|
|
|
1765
2011
|
function findSidebarNavLists(doc) {
|
|
1766
2012
|
const lists = [];
|
|
1767
2013
|
for (const nav of doc.querySelectorAll(".bp-sidebar:not([data-bp-nav-manual])")) {
|
|
1768
|
-
const list = nav.querySelector(":scope > .bp-sidebar__panel > ul") ?? nav.querySelector(":scope > ul");
|
|
2014
|
+
const list = nav.querySelector(":scope > .bp-sidebar__panel .bp-toc-switcher > ul") ?? nav.querySelector(":scope > .bp-sidebar__panel > ul") ?? nav.querySelector(":scope > ul");
|
|
1769
2015
|
if (list) lists.push(list);
|
|
1770
2016
|
}
|
|
1771
2017
|
for (const nav of doc.querySelectorAll(".bp-toc:not([data-bp-nav-manual])")) {
|