@obvi/blueprint 1.2.0 → 1.3.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/README.md +21 -0
- package/dist/blueprint-choices.js +329 -163
- package/dist/blueprint-contract.json +4 -4
- package/dist/blueprint.css +171 -30
- package/dist/blueprint.js +275 -148
- package/dist/vendor/blueprint/blueprint.css +171 -30
- package/dist/vendor/blueprint/blueprint.js +275 -148
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -16,6 +16,27 @@ A blueprint is a technical document that should look like one: monochrome ink on
|
|
|
16
16
|
|
|
17
17
|
The result is one portable stylesheet, no build step, that turns plain semantic markup into a polished blueprint.
|
|
18
18
|
|
|
19
|
+
## Host decision integration
|
|
20
|
+
|
|
21
|
+
`<bp-choice>` keeps its author-facing HTML contract while exposing a semantic
|
|
22
|
+
runtime boundary for Obvious. A connected choice emits `bp-choice-ready` with
|
|
23
|
+
its stable manifest. Committing and reconsidering emit the bubbling, composed
|
|
24
|
+
`bp-choice-commit` and `bp-choice-reconsider` events. Tab preview emits nothing.
|
|
25
|
+
|
|
26
|
+
Hosts apply ephemeral owner state through `choice.applyDecisionState(state)`:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
choice.applyDecisionState({ status: 'locked', value: 'phased', busy: false })
|
|
30
|
+
choice.applyDecisionState({ status: 'open', value: 'phased', busy: false })
|
|
31
|
+
choice.applyDecisionState(null) // restore the authored open/resolved state
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The component retains its authored option model and owns every rerender. Hosts
|
|
35
|
+
must use the manifest, events, and method instead of querying generated
|
|
36
|
+
`.bp-choice__*` markup. Applying host state never mutates the authored
|
|
37
|
+
`resolved` attribute, so a host can present an owner-only reconsideration while
|
|
38
|
+
the stored document remains at its last committed decision.
|
|
39
|
+
|
|
19
40
|
## Learn more
|
|
20
41
|
|
|
21
42
|
Blueprints are created and rendered through Obvious. To see what they are and how to use them, go to **[obvious.ai](https://obvious.ai)**.
|
|
@@ -37,7 +37,7 @@ function extractRationale(root) {
|
|
|
37
37
|
if (!node) return null
|
|
38
38
|
const wrap = node.ownerDocument.createElement('div')
|
|
39
39
|
wrap.className = 'bp-choice-rationale'
|
|
40
|
-
wrap.append(...node.childNodes)
|
|
40
|
+
wrap.append(...[...node.childNodes].map((child) => child.cloneNode(true)))
|
|
41
41
|
return wrap
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -201,6 +201,239 @@ function buildResolvedChoice(doc, { options, verdictKicker, resolvedValue }) {
|
|
|
201
201
|
return root
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/** @param {HTMLElement} host @param {ChoiceLayout} layout @param {ReturnType<typeof readOptions>} options @param {string | null} resolvedValue */
|
|
205
|
+
function buildChoiceManifest(host, layout, options, resolvedValue) {
|
|
206
|
+
return {
|
|
207
|
+
id: host.id || host.getAttribute('name') || '',
|
|
208
|
+
label:
|
|
209
|
+
host.getAttribute('data-obvious-choice-label') ||
|
|
210
|
+
host.getAttribute('aria-label') ||
|
|
211
|
+
host.getAttribute('name') ||
|
|
212
|
+
host.id ||
|
|
213
|
+
'',
|
|
214
|
+
layout,
|
|
215
|
+
resolvedValue,
|
|
216
|
+
options: options.map((option) => ({
|
|
217
|
+
value: option.value,
|
|
218
|
+
label: option.title || option.tab || option.value,
|
|
219
|
+
title: option.title,
|
|
220
|
+
})),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {HTMLElement} host
|
|
226
|
+
* @param {'bp-choice-ready' | 'bp-choice-commit' | 'bp-choice-reconsider'} type
|
|
227
|
+
* @param {Record<string, unknown>} detail
|
|
228
|
+
*/
|
|
229
|
+
function dispatchChoiceEvent(host, type, detail) {
|
|
230
|
+
host.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail }))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** @param {HTMLElement} root @param {boolean} disabled */
|
|
234
|
+
function setChoiceControlsDisabled(root, disabled) {
|
|
235
|
+
for (const control of root.querySelectorAll('input, button')) {
|
|
236
|
+
if (control instanceof HTMLInputElement || control instanceof HTMLButtonElement) {
|
|
237
|
+
control.disabled = disabled
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {Document} doc
|
|
244
|
+
* @param {HTMLElement} root
|
|
245
|
+
* @param {{ message: string, reconsider: string, busy: boolean }} state
|
|
246
|
+
*/
|
|
247
|
+
function appendLockedHostActions(doc, root, state) {
|
|
248
|
+
const actions = el(doc, 'div', 'bp-choice__actions')
|
|
249
|
+
const status = el(doc, 'output', 'bp-choice__hint')
|
|
250
|
+
status.setAttribute('aria-live', 'polite')
|
|
251
|
+
status.textContent = state.message
|
|
252
|
+
const reset = el(doc, 'button', 'bp-choice__reset')
|
|
253
|
+
reset.type = 'button'
|
|
254
|
+
reset.disabled = state.busy
|
|
255
|
+
reset.textContent = state.busy ? 'Saving…' : state.reconsider
|
|
256
|
+
actions.append(status, reset)
|
|
257
|
+
root.append(actions)
|
|
258
|
+
return reset
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {Document} doc
|
|
263
|
+
* @param {{
|
|
264
|
+
* layout: ChoiceLayout,
|
|
265
|
+
* options: ReturnType<typeof readOptions>,
|
|
266
|
+
* verdictKicker: string,
|
|
267
|
+
* adoptLabel: string,
|
|
268
|
+
* hint: string,
|
|
269
|
+
* reconsider: string,
|
|
270
|
+
* compareSummary: string | null,
|
|
271
|
+
* compareBody: Element | null,
|
|
272
|
+
* scopeId: string,
|
|
273
|
+
* initialValue: string | null,
|
|
274
|
+
* busy: boolean,
|
|
275
|
+
* message: string,
|
|
276
|
+
* }} source
|
|
277
|
+
*/
|
|
278
|
+
function buildInteractiveChoice(doc, source) {
|
|
279
|
+
const {
|
|
280
|
+
layout,
|
|
281
|
+
options,
|
|
282
|
+
verdictKicker,
|
|
283
|
+
adoptLabel,
|
|
284
|
+
hint,
|
|
285
|
+
reconsider,
|
|
286
|
+
compareSummary,
|
|
287
|
+
compareBody,
|
|
288
|
+
scopeId,
|
|
289
|
+
initialValue,
|
|
290
|
+
busy,
|
|
291
|
+
message,
|
|
292
|
+
} = source
|
|
293
|
+
const viewName = `${scopeId}-view`
|
|
294
|
+
const pickName = `${scopeId}-pick`
|
|
295
|
+
const form = el(doc, 'form', `bp-choice bp-choice--${layout}`)
|
|
296
|
+
form.dataset.bpChoice = scopeId
|
|
297
|
+
const titleOpts = options.map((option) => ({ value: option.value, title: option.title }))
|
|
298
|
+
form.append(buildVerdict(doc, verdictKicker, titleOpts))
|
|
299
|
+
|
|
300
|
+
if (layout === 'tabs') {
|
|
301
|
+
const selectedView = options.some((option) => option.value === initialValue) ? initialValue : options[0]?.value
|
|
302
|
+
const seg = el(doc, 'div', 'bp-choice__seg')
|
|
303
|
+
seg.setAttribute('role', 'tablist')
|
|
304
|
+
for (const opt of options) {
|
|
305
|
+
const segOpt = el(doc, 'label', 'bp-choice__seg-opt')
|
|
306
|
+
const input = doc.createElement('input')
|
|
307
|
+
input.type = 'radio'
|
|
308
|
+
input.name = viewName
|
|
309
|
+
input.value = opt.value
|
|
310
|
+
input.className = 'bp-choice__view'
|
|
311
|
+
if (opt.value === selectedView) {
|
|
312
|
+
input.defaultChecked = true
|
|
313
|
+
input.checked = true
|
|
314
|
+
}
|
|
315
|
+
segOpt.append(input, doc.createTextNode(opt.tab))
|
|
316
|
+
seg.append(segOpt)
|
|
317
|
+
}
|
|
318
|
+
form.append(seg)
|
|
319
|
+
|
|
320
|
+
const panels = el(doc, 'div', 'bp-choice__panels')
|
|
321
|
+
for (const opt of options) {
|
|
322
|
+
const panel = el(doc, 'div', 'bp-choice__panel')
|
|
323
|
+
panel.dataset.value = opt.value
|
|
324
|
+
const h3 = el(doc, 'h3')
|
|
325
|
+
h3.textContent = opt.title
|
|
326
|
+
panel.append(h3)
|
|
327
|
+
panel.append(bodyWithoutRationale(opt.el))
|
|
328
|
+
const rationale = extractRationale(opt.el)
|
|
329
|
+
if (rationale) panel.append(rationale)
|
|
330
|
+
|
|
331
|
+
const actions = el(doc, 'div', 'bp-choice__actions')
|
|
332
|
+
const adopt = el(doc, 'label', 'bp-choice__adopt')
|
|
333
|
+
const commit = doc.createElement('input')
|
|
334
|
+
commit.type = 'radio'
|
|
335
|
+
commit.name = pickName
|
|
336
|
+
commit.value = opt.value
|
|
337
|
+
commit.className = 'bp-choice__commit'
|
|
338
|
+
adopt.append(commit, doc.createTextNode(adoptLabel))
|
|
339
|
+
actions.append(adopt)
|
|
340
|
+
panel.append(actions)
|
|
341
|
+
panels.append(panel)
|
|
342
|
+
}
|
|
343
|
+
form.append(panels)
|
|
344
|
+
|
|
345
|
+
const style = el(doc, 'style')
|
|
346
|
+
style.textContent = scopedTabRules(`[data-bp-choice="${scopeId}"]`, viewName, pickName, titleOpts)
|
|
347
|
+
form.prepend(style)
|
|
348
|
+
form.append(buildFooter(doc, hint || 'Switch tabs to preview · adopt to commit', reconsider))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (layout === 'stack') {
|
|
352
|
+
const stack = el(doc, 'div', 'bp-choice__stack')
|
|
353
|
+
for (const opt of options) {
|
|
354
|
+
const card = el(doc, 'label', 'bp-choice__card')
|
|
355
|
+
const input = doc.createElement('input')
|
|
356
|
+
input.type = 'radio'
|
|
357
|
+
input.name = pickName
|
|
358
|
+
input.value = opt.value
|
|
359
|
+
input.className = 'bp-choice__pick'
|
|
360
|
+
card.append(input)
|
|
361
|
+
|
|
362
|
+
const head = el(doc, 'div', 'bp-choice__card-head')
|
|
363
|
+
if (opt.optionLabel) head.append(label(doc, opt.optionLabel))
|
|
364
|
+
const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__card-chosen')
|
|
365
|
+
chosen.textContent = '✓ Chosen'
|
|
366
|
+
const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__card-rejected')
|
|
367
|
+
rejected.textContent = 'Not chosen'
|
|
368
|
+
head.append(chosen, rejected)
|
|
369
|
+
card.append(head)
|
|
370
|
+
|
|
371
|
+
const h4 = el(doc, 'h4')
|
|
372
|
+
h4.textContent = opt.title
|
|
373
|
+
card.append(h4)
|
|
374
|
+
card.append(bodyWithoutRationale(opt.el))
|
|
375
|
+
const rationale = extractRationale(opt.el)
|
|
376
|
+
if (rationale) card.append(rationale)
|
|
377
|
+
stack.append(card)
|
|
378
|
+
}
|
|
379
|
+
form.append(stack)
|
|
380
|
+
form.append(buildFooter(doc, hint || 'Select a card to commit · rejected drafts stay readable below', reconsider))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (layout === 'gallery') {
|
|
384
|
+
const gallery = el(doc, 'div', 'bp-choice__gallery')
|
|
385
|
+
for (const opt of options) {
|
|
386
|
+
const mock = el(doc, 'label', 'bp-choice__mock')
|
|
387
|
+
const input = doc.createElement('input')
|
|
388
|
+
input.type = 'radio'
|
|
389
|
+
input.name = pickName
|
|
390
|
+
input.value = opt.value
|
|
391
|
+
input.className = 'bp-choice__pick'
|
|
392
|
+
mock.append(input)
|
|
393
|
+
|
|
394
|
+
const frame = el(doc, 'div', 'bp-choice__mock-frame')
|
|
395
|
+
const frameInner = frameContent(opt.el)
|
|
396
|
+
if (frameInner) frame.append(frameInner)
|
|
397
|
+
mock.append(frame)
|
|
398
|
+
|
|
399
|
+
const cap = el(doc, 'div', 'bp-choice__mock-cap')
|
|
400
|
+
const h4 = el(doc, 'h4')
|
|
401
|
+
h4.textContent = opt.caption
|
|
402
|
+
cap.append(h4)
|
|
403
|
+
const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__mock-pick bp-choice__mock-chosen')
|
|
404
|
+
chosen.textContent = '✓ Chosen'
|
|
405
|
+
const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__mock-pick bp-choice__mock-rejected')
|
|
406
|
+
rejected.textContent = 'Not chosen'
|
|
407
|
+
cap.append(chosen, rejected)
|
|
408
|
+
mock.append(cap)
|
|
409
|
+
gallery.append(mock)
|
|
410
|
+
}
|
|
411
|
+
form.append(gallery)
|
|
412
|
+
|
|
413
|
+
if (compareSummary && compareBody) {
|
|
414
|
+
const details = el(doc, 'details', 'bp-choice__compare')
|
|
415
|
+
const summary = el(doc, 'summary')
|
|
416
|
+
summary.textContent = compareSummary
|
|
417
|
+
details.append(summary)
|
|
418
|
+
const body = el(doc, 'div', 'bp-choice__compare-body')
|
|
419
|
+
body.append(compareBody.cloneNode(true))
|
|
420
|
+
details.append(body)
|
|
421
|
+
form.append(details)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
form.append(buildFooter(doc, hint || 'Select a mockup to track the decision', reconsider))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (message) {
|
|
428
|
+
const status = el(doc, 'output', 'bp-choice__hint')
|
|
429
|
+
status.setAttribute('aria-live', 'polite')
|
|
430
|
+
status.textContent = message
|
|
431
|
+
form.append(status)
|
|
432
|
+
}
|
|
433
|
+
setChoiceControlsDisabled(form, busy)
|
|
434
|
+
return form
|
|
435
|
+
}
|
|
436
|
+
|
|
204
437
|
class BlueprintRationaleElement extends HTMLElement {
|
|
205
438
|
connectedCallback() {
|
|
206
439
|
if (this.dataset.bpRendered) return
|
|
@@ -212,180 +445,113 @@ class BlueprintRationaleElement extends HTMLElement {
|
|
|
212
445
|
class BlueprintChoiceOptionElement extends HTMLElement {}
|
|
213
446
|
|
|
214
447
|
class BlueprintChoiceElement extends HTMLElement {
|
|
448
|
+
constructor() {
|
|
449
|
+
super()
|
|
450
|
+
this.choiceSource = null
|
|
451
|
+
this.choiceHostState = null
|
|
452
|
+
this.choiceScopeId = null
|
|
453
|
+
}
|
|
454
|
+
|
|
215
455
|
connectedCallback() {
|
|
216
|
-
if (this.
|
|
456
|
+
if (this.choiceSource) return
|
|
217
457
|
this.dataset.bpRendered = '1'
|
|
218
|
-
|
|
219
|
-
const doc = this.ownerDocument
|
|
220
458
|
/** @type {ChoiceLayout} */
|
|
221
459
|
const layout = (this.getAttribute('layout') || 'stack').toLowerCase()
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const resolvedValue = this.getAttribute('resolved')
|
|
233
|
-
if (resolvedValue !== null) {
|
|
234
|
-
this.replaceChildren(buildResolvedChoice(doc, { options, verdictKicker, resolvedValue }))
|
|
235
|
-
return
|
|
460
|
+
this.choiceSource = {
|
|
461
|
+
layout,
|
|
462
|
+
options: readOptions(this),
|
|
463
|
+
verdictKicker: this.getAttribute('verdict') ?? 'Chosen',
|
|
464
|
+
adoptLabel: this.getAttribute('adopt') ?? 'Adopt this direction',
|
|
465
|
+
hint: this.getAttribute('hint') ?? '',
|
|
466
|
+
reconsider: this.getAttribute('reconsider') ?? 'Reconsider',
|
|
467
|
+
compareSummary: this.getAttribute('compare'),
|
|
468
|
+
compareBody: this.querySelector(':scope > [slot="compare"]'),
|
|
469
|
+
resolvedValue: this.getAttribute('resolved'),
|
|
236
470
|
}
|
|
471
|
+
this.choiceScopeId = nextId('bp-choice')
|
|
472
|
+
this.renderChoice()
|
|
473
|
+
dispatchChoiceEvent(this, 'bp-choice-ready', this.getDecisionManifest())
|
|
474
|
+
}
|
|
237
475
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (layout === 'tabs') {
|
|
249
|
-
const seg = el(doc, 'div', 'bp-choice__seg')
|
|
250
|
-
seg.setAttribute('role', 'tablist')
|
|
251
|
-
for (const [index, opt] of options.entries()) {
|
|
252
|
-
const segOpt = el(doc, 'label', 'bp-choice__seg-opt')
|
|
253
|
-
const input = doc.createElement('input')
|
|
254
|
-
input.type = 'radio'
|
|
255
|
-
input.name = viewName
|
|
256
|
-
input.value = opt.value
|
|
257
|
-
input.className = 'bp-choice__view'
|
|
258
|
-
if (index === 0) {
|
|
259
|
-
input.defaultChecked = true
|
|
260
|
-
input.checked = true
|
|
261
|
-
}
|
|
262
|
-
segOpt.append(input, doc.createTextNode(opt.tab))
|
|
263
|
-
seg.append(segOpt)
|
|
264
|
-
}
|
|
265
|
-
form.append(seg)
|
|
266
|
-
|
|
267
|
-
const panels = el(doc, 'div', 'bp-choice__panels')
|
|
268
|
-
for (const opt of options) {
|
|
269
|
-
const panel = el(doc, 'div', 'bp-choice__panel')
|
|
270
|
-
panel.dataset.value = opt.value
|
|
271
|
-
const h3 = el(doc, 'h3')
|
|
272
|
-
h3.textContent = opt.title
|
|
273
|
-
panel.append(h3)
|
|
274
|
-
panel.append(bodyWithoutRationale(opt.el))
|
|
275
|
-
const rationale = extractRationale(opt.el)
|
|
276
|
-
if (rationale) panel.append(rationale)
|
|
277
|
-
|
|
278
|
-
const actions = el(doc, 'div', 'bp-choice__actions')
|
|
279
|
-
const adopt = el(doc, 'label', 'bp-choice__adopt')
|
|
280
|
-
const commit = doc.createElement('input')
|
|
281
|
-
commit.type = 'radio'
|
|
282
|
-
commit.name = pickName
|
|
283
|
-
commit.value = opt.value
|
|
284
|
-
commit.className = 'bp-choice__commit'
|
|
285
|
-
adopt.append(commit, doc.createTextNode(adoptLabel))
|
|
286
|
-
actions.append(adopt)
|
|
287
|
-
panel.append(actions)
|
|
288
|
-
panels.append(panel)
|
|
289
|
-
}
|
|
290
|
-
form.append(panels)
|
|
291
|
-
|
|
292
|
-
const style = el(doc, 'style')
|
|
293
|
-
style.textContent = scopedTabRules(`[data-bp-choice="${scopeId}"]`, viewName, pickName, titleOpts)
|
|
294
|
-
form.prepend(style)
|
|
295
|
-
|
|
296
|
-
form.append(
|
|
297
|
-
buildFooter(
|
|
298
|
-
doc,
|
|
299
|
-
hint || 'Switch tabs to preview · adopt to commit',
|
|
300
|
-
reconsider
|
|
301
|
-
)
|
|
302
|
-
)
|
|
303
|
-
}
|
|
476
|
+
getDecisionManifest() {
|
|
477
|
+
if (!this.choiceSource) return null
|
|
478
|
+
return buildChoiceManifest(
|
|
479
|
+
this,
|
|
480
|
+
this.choiceSource.layout,
|
|
481
|
+
this.choiceSource.options,
|
|
482
|
+
this.choiceSource.resolvedValue
|
|
483
|
+
)
|
|
484
|
+
}
|
|
304
485
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__card-chosen')
|
|
319
|
-
chosen.textContent = '✓ Chosen'
|
|
320
|
-
const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__card-rejected')
|
|
321
|
-
rejected.textContent = 'Not chosen'
|
|
322
|
-
head.append(chosen, rejected)
|
|
323
|
-
card.append(head)
|
|
324
|
-
|
|
325
|
-
const h4 = el(doc, 'h4')
|
|
326
|
-
h4.textContent = opt.title
|
|
327
|
-
card.append(h4)
|
|
328
|
-
card.append(bodyWithoutRationale(opt.el))
|
|
329
|
-
const rationale = extractRationale(opt.el)
|
|
330
|
-
if (rationale) card.append(rationale)
|
|
331
|
-
stack.append(card)
|
|
332
|
-
}
|
|
333
|
-
form.append(stack)
|
|
334
|
-
form.append(
|
|
335
|
-
buildFooter(
|
|
336
|
-
doc,
|
|
337
|
-
hint || 'Select a card to commit · rejected drafts stay readable below',
|
|
338
|
-
reconsider
|
|
339
|
-
)
|
|
340
|
-
)
|
|
486
|
+
/**
|
|
487
|
+
* Apply ephemeral host-owned state without mutating authored attributes.
|
|
488
|
+
* Passing null restores the authored open/resolved rendering.
|
|
489
|
+
*
|
|
490
|
+
* @param {{ status: 'open' | 'locked', value: string | null, busy?: boolean, message?: string } | null} state
|
|
491
|
+
*/
|
|
492
|
+
applyDecisionState(state) {
|
|
493
|
+
if (!this.choiceSource) return false
|
|
494
|
+
if (state !== null) {
|
|
495
|
+
const valueExists =
|
|
496
|
+
state.value === null || this.choiceSource.options.some((option) => option.value === state.value)
|
|
497
|
+
if ((state.status !== 'open' && state.status !== 'locked') || !valueExists) return false
|
|
498
|
+
if (state.status === 'locked' && state.value === null) return false
|
|
341
499
|
}
|
|
500
|
+
this.choiceHostState = state
|
|
501
|
+
? { status: state.status, value: state.value, busy: Boolean(state.busy), message: state.message ?? '' }
|
|
502
|
+
: null
|
|
503
|
+
this.renderChoice()
|
|
504
|
+
return true
|
|
505
|
+
}
|
|
342
506
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__mock-pick bp-choice__mock-chosen')
|
|
364
|
-
chosen.textContent = '✓ Chosen'
|
|
365
|
-
const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__mock-pick bp-choice__mock-rejected')
|
|
366
|
-
rejected.textContent = 'Not chosen'
|
|
367
|
-
cap.append(chosen, rejected)
|
|
368
|
-
mock.append(cap)
|
|
369
|
-
gallery.append(mock)
|
|
370
|
-
}
|
|
371
|
-
form.append(gallery)
|
|
372
|
-
|
|
373
|
-
if (compareSummary && compareBody) {
|
|
374
|
-
const details = el(doc, 'details', 'bp-choice__compare')
|
|
375
|
-
const summary = el(doc, 'summary')
|
|
376
|
-
summary.textContent = compareSummary
|
|
377
|
-
details.append(summary)
|
|
378
|
-
const body = el(doc, 'div', 'bp-choice__compare-body')
|
|
379
|
-
body.append(compareBody.cloneNode(true))
|
|
380
|
-
details.append(body)
|
|
381
|
-
form.append(details)
|
|
507
|
+
renderChoice() {
|
|
508
|
+
if (!this.choiceSource || !this.choiceScopeId) return
|
|
509
|
+
const source = this.choiceSource
|
|
510
|
+
const hostState = this.choiceHostState
|
|
511
|
+
const lockedValue = hostState?.status === 'locked' ? hostState.value : hostState ? null : source.resolvedValue
|
|
512
|
+
if (lockedValue !== null) {
|
|
513
|
+
const root = buildResolvedChoice(this.ownerDocument, {
|
|
514
|
+
options: source.options,
|
|
515
|
+
verdictKicker: source.verdictKicker,
|
|
516
|
+
resolvedValue: lockedValue,
|
|
517
|
+
})
|
|
518
|
+
if (hostState) {
|
|
519
|
+
const reset = appendLockedHostActions(this.ownerDocument, root, {
|
|
520
|
+
message: hostState.message,
|
|
521
|
+
reconsider: source.reconsider,
|
|
522
|
+
busy: hostState.busy,
|
|
523
|
+
})
|
|
524
|
+
reset.addEventListener('click', () => {
|
|
525
|
+
dispatchChoiceEvent(this, 'bp-choice-reconsider', this.getDecisionManifest())
|
|
526
|
+
})
|
|
382
527
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
buildFooter(doc, hint || 'Select a mockup to track the decision', reconsider)
|
|
386
|
-
)
|
|
528
|
+
this.replaceChildren(root)
|
|
529
|
+
return
|
|
387
530
|
}
|
|
388
531
|
|
|
532
|
+
const form = buildInteractiveChoice(this.ownerDocument, {
|
|
533
|
+
...source,
|
|
534
|
+
scopeId: this.choiceScopeId,
|
|
535
|
+
initialValue: hostState?.value ?? null,
|
|
536
|
+
busy: hostState?.busy ?? false,
|
|
537
|
+
message: hostState?.message ?? '',
|
|
538
|
+
})
|
|
539
|
+
form.addEventListener('change', (event) => {
|
|
540
|
+
const input = event.target
|
|
541
|
+
if (!(input instanceof HTMLInputElement) || !input.checked) return
|
|
542
|
+
const isCommit = source.layout === 'tabs' ? input.classList.contains('bp-choice__commit') : input.classList.contains('bp-choice__pick')
|
|
543
|
+
if (!isCommit) return
|
|
544
|
+
const option = source.options.find((candidate) => candidate.value === input.value)
|
|
545
|
+
if (!option) return
|
|
546
|
+
dispatchChoiceEvent(this, 'bp-choice-commit', {
|
|
547
|
+
...this.getDecisionManifest(),
|
|
548
|
+
value: option.value,
|
|
549
|
+
option: { value: option.value, label: option.title || option.tab || option.value, title: option.title },
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
form.addEventListener('reset', () => {
|
|
553
|
+
dispatchChoiceEvent(this, 'bp-choice-reconsider', this.getDecisionManifest())
|
|
554
|
+
})
|
|
389
555
|
this.replaceChildren(form)
|
|
390
556
|
}
|
|
391
557
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"markupSchema": 2,
|
|
3
|
-
"frameworkVersion": "1.
|
|
4
|
-
"source": "github:FlatFilers/blueprint-framework@v1.
|
|
3
|
+
"frameworkVersion": "1.3.1",
|
|
4
|
+
"source": "github:FlatFilers/blueprint-framework@v1.3.1",
|
|
5
5
|
"assets": {
|
|
6
6
|
"vendor/blueprint/blueprint.css": {
|
|
7
|
-
"sha256": "
|
|
7
|
+
"sha256": "eaf4ca1936c6231709fdafea34f47c5e919e31b9a5d3d69398542af7ba30bb83"
|
|
8
8
|
},
|
|
9
9
|
"vendor/blueprint/blueprint.js": {
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "55f1b677d5302bddba2edb840d26bbe409252fce20345a3d4b6368fd04526954"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
}
|