@obvi/blueprint 1.0.9 → 1.1.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.
@@ -0,0 +1,567 @@
1
+ // blueprint-choices.js — interactive choice / preflight Web Components.
2
+ //
3
+ // Authoring shrinks to a small set of elements; each component expands to
4
+ // light-DOM markup styled by blueprint.css. Interaction is CSS-only (radio /
5
+ // checkbox + :has() + native form reset). Per-instance scoped rules are
6
+ // injected only where value-specific selectors are required (tab panels,
7
+ // verdict labels, preflight gate).
8
+ //
9
+ // <bp-choice layout="tabs" verdict="Adopted">
10
+ // <bp-choice-option value="a" tab="Draft A" title="Phased rollout">
11
+ // <p>…</p>
12
+ // <bp-rationale><p><strong>Trade-off.</strong> …</p></bp-rationale>
13
+ // </bp-choice-option>
14
+ // </bp-choice>
15
+ //
16
+ // layout: tabs | stack | gallery
17
+ // resolved: option value of an already-made decision — renders statically
18
+ // (no inputs, no reset); the named option carries the verdict
19
+ // verdict: kicker on the committed banner (default "Chosen")
20
+ // adopt: tabs-only commit button label (default "Adopt this direction")
21
+ // hint: footer hint; reconsider: reset button label (default "Reconsider")
22
+ // compare: gallery-only <details> summary (omit to skip compare block)
23
+
24
+ /** @typedef {'tabs' | 'stack' | 'gallery'} ChoiceLayout */
25
+
26
+ let choiceSeq = 0
27
+
28
+ /** @param {string} prefix */
29
+ function nextId(prefix) {
30
+ choiceSeq += 1
31
+ return `${prefix}-${choiceSeq}`
32
+ }
33
+
34
+ /** @param {ParentNode} root */
35
+ function extractRationale(root) {
36
+ const node = root.querySelector(':scope > bp-rationale')
37
+ if (!node) return null
38
+ const wrap = node.ownerDocument.createElement('div')
39
+ wrap.className = 'bp-choice-rationale'
40
+ wrap.append(...node.childNodes)
41
+ return wrap
42
+ }
43
+
44
+ /** @param {Element} root */
45
+ function bodyWithoutRationale(root) {
46
+ const frag = root.ownerDocument.createDocumentFragment()
47
+ for (const child of root.childNodes) {
48
+ if (child instanceof Element && child.tagName === 'BP-RATIONALE') continue
49
+ if (child instanceof Element && child.getAttribute('slot') === 'frame') continue
50
+ frag.append(child.cloneNode(true))
51
+ }
52
+ return frag
53
+ }
54
+
55
+ /** @param {Element} root */
56
+ function frameContent(root) {
57
+ const slotted = root.querySelector(':scope > [slot="frame"]')
58
+ if (slotted) {
59
+ const frame = slotted.cloneNode(true)
60
+ frame.removeAttribute('slot')
61
+ return frame
62
+ }
63
+ return null
64
+ }
65
+
66
+ /**
67
+ * @param {Document} doc
68
+ * @param {string} tag
69
+ * @param {string} [className]
70
+ */
71
+ function el(doc, tag, className) {
72
+ const node = doc.createElement(tag)
73
+ if (className) node.className = className
74
+ return node
75
+ }
76
+
77
+ /**
78
+ * @param {Document} doc
79
+ * @param {string} text
80
+ * @param {string} [className]
81
+ */
82
+ function label(doc, text, className) {
83
+ const node = el(doc, 'span', className ?? 'bp-label')
84
+ node.textContent = text
85
+ return node
86
+ }
87
+
88
+ /**
89
+ * @param {Document} doc
90
+ * @param {string} kicker
91
+ * @param {Array<{ value: string, title: string }>} options
92
+ */
93
+ function buildVerdict(doc, kicker, options, meta = 'You · just now') {
94
+ const verdict = el(doc, 'div', 'bp-choice__verdict')
95
+ verdict.append(label(doc, kicker))
96
+ for (const opt of options) {
97
+ const pick = el(doc, 'p', 'bp-choice__verdict-pick')
98
+ pick.dataset.for = opt.value
99
+ pick.textContent = opt.title
100
+ verdict.append(pick)
101
+ }
102
+ if (meta) {
103
+ const metaEl = el(doc, 'span', 'bp-choice__verdict-meta')
104
+ metaEl.textContent = meta
105
+ verdict.append(metaEl)
106
+ }
107
+ return verdict
108
+ }
109
+
110
+ /**
111
+ * @param {Document} doc
112
+ * @param {string} hint
113
+ * @param {string} reconsider
114
+ */
115
+ function buildFooter(doc, hint, reconsider) {
116
+ const actions = el(doc, 'div', 'bp-choice__actions')
117
+ if (hint) {
118
+ const hintEl = el(doc, 'span', 'bp-choice__hint')
119
+ hintEl.textContent = hint
120
+ actions.append(hintEl)
121
+ }
122
+ const reset = el(doc, 'button', 'bp-choice__reset')
123
+ reset.type = 'reset'
124
+ reset.textContent = reconsider
125
+ actions.append(reset)
126
+ return actions
127
+ }
128
+
129
+ /**
130
+ * @param {string} scope
131
+ * @param {string} viewName
132
+ * @param {string} pickName
133
+ * @param {Array<{ value: string, title: string }>} options
134
+ */
135
+ function scopedTabRules(scope, viewName, pickName, options) {
136
+ const lines = options.map(
137
+ (opt) =>
138
+ `${scope}:has([name="${viewName}"][value="${opt.value}"]:checked) .bp-choice__panel[data-value="${opt.value}"]{display:block}` +
139
+ `${scope}:has([name="${pickName}"][value="${opt.value}"]:checked) .bp-choice__verdict-pick[data-for="${opt.value}"]{display:block}`
140
+ )
141
+ return lines.join('\n')
142
+ }
143
+
144
+ /** @param {HTMLElement} host */
145
+ function readOptions(host) {
146
+ return [...host.querySelectorAll(':scope > bp-choice-option')].map((node) => ({
147
+ el: node,
148
+ value: node.getAttribute('value') ?? '',
149
+ tab: node.getAttribute('tab') ?? node.getAttribute('label') ?? node.getAttribute('value') ?? '',
150
+ title: node.getAttribute('title') ?? node.getAttribute('caption') ?? node.getAttribute('tab') ?? '',
151
+ optionLabel: node.getAttribute('label') ?? node.getAttribute('tab') ?? '',
152
+ caption: node.getAttribute('caption') ?? node.getAttribute('title') ?? '',
153
+ }))
154
+ }
155
+
156
+ /**
157
+ * Static, non-interactive rendering: the decision is already made. The option
158
+ * named by `resolved` carries the verdict; the rest read as considered-and-set-
159
+ * aside. No form, no inputs, no reset.
160
+ *
161
+ * @param {Document} doc
162
+ * @param {{ options: ReturnType<typeof readOptions>, verdictKicker: string, resolvedValue: string }} spec
163
+ */
164
+ function buildResolvedChoice(doc, { options, verdictKicker, resolvedValue }) {
165
+ const root = el(doc, 'div', 'bp-choice bp-choice--resolved')
166
+ const winner = options.find((o) => o.value === resolvedValue) ?? options[0]
167
+
168
+ const verdict = buildVerdict(
169
+ doc,
170
+ verdictKicker,
171
+ options.map((o) => ({ value: o.value, title: o.title })),
172
+ ''
173
+ )
174
+ const winnerPick = winner && verdict.querySelector(`.bp-choice__verdict-pick[data-for="${winner.value}"]`)
175
+ if (winnerPick) winnerPick.setAttribute('data-resolved', '')
176
+ root.append(verdict)
177
+
178
+ const stack = el(doc, 'div', 'bp-choice__stack')
179
+ for (const opt of options) {
180
+ const card = el(doc, 'div', 'bp-choice__card')
181
+ if (winner && opt.value === winner.value) card.setAttribute('data-resolved', '')
182
+
183
+ const head = el(doc, 'div', 'bp-choice__card-head')
184
+ if (opt.optionLabel) head.append(label(doc, opt.optionLabel))
185
+ const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__card-chosen')
186
+ chosen.textContent = `✓ ${verdictKicker}`
187
+ const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__card-rejected')
188
+ rejected.textContent = 'Not chosen'
189
+ head.append(chosen, rejected)
190
+ card.append(head)
191
+
192
+ const h4 = el(doc, 'h4')
193
+ h4.textContent = opt.title
194
+ card.append(h4)
195
+ card.append(bodyWithoutRationale(opt.el))
196
+ const rationale = extractRationale(opt.el)
197
+ if (rationale) card.append(rationale)
198
+ stack.append(card)
199
+ }
200
+ root.append(stack)
201
+ return root
202
+ }
203
+
204
+ class BlueprintRationaleElement extends HTMLElement {
205
+ connectedCallback() {
206
+ if (this.dataset.bpRendered) return
207
+ this.dataset.bpRendered = '1'
208
+ this.classList.add('bp-choice-rationale')
209
+ }
210
+ }
211
+
212
+ class BlueprintChoiceOptionElement extends HTMLElement {}
213
+
214
+ class BlueprintChoiceElement extends HTMLElement {
215
+ connectedCallback() {
216
+ if (this.dataset.bpRendered) return
217
+ this.dataset.bpRendered = '1'
218
+
219
+ const doc = this.ownerDocument
220
+ /** @type {ChoiceLayout} */
221
+ const layout = (this.getAttribute('layout') || 'stack').toLowerCase()
222
+ const verdictKicker = this.getAttribute('verdict') ?? 'Chosen'
223
+ const adoptLabel = this.getAttribute('adopt') ?? 'Adopt this direction'
224
+ const hint = this.getAttribute('hint') ?? ''
225
+ const reconsider = this.getAttribute('reconsider') ?? 'Reconsider'
226
+ const compareSummary = this.getAttribute('compare')
227
+ const compareBody = this.querySelector(':scope > [slot="compare"]')
228
+
229
+ const options = readOptions(this)
230
+
231
+ // resolved: render the decision as already made (no interaction).
232
+ const resolvedValue = this.getAttribute('resolved')
233
+ if (resolvedValue !== null) {
234
+ this.replaceChildren(buildResolvedChoice(doc, { options, verdictKicker, resolvedValue }))
235
+ return
236
+ }
237
+
238
+ const scopeId = nextId('bp-choice')
239
+ const viewName = `${scopeId}-view`
240
+ const pickName = `${scopeId}-pick`
241
+
242
+ const form = el(doc, 'form', `bp-choice bp-choice--${layout}`)
243
+ form.dataset.bpChoice = scopeId
244
+
245
+ const titleOpts = options.map((o) => ({ value: o.value, title: o.title }))
246
+ form.append(buildVerdict(doc, verdictKicker, titleOpts))
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) input.checked = true
259
+ segOpt.append(input, doc.createTextNode(opt.tab))
260
+ seg.append(segOpt)
261
+ }
262
+ form.append(seg)
263
+
264
+ const panels = el(doc, 'div', 'bp-choice__panels')
265
+ for (const opt of options) {
266
+ const panel = el(doc, 'div', 'bp-choice__panel')
267
+ panel.dataset.value = opt.value
268
+ const h3 = el(doc, 'h3')
269
+ h3.textContent = opt.title
270
+ panel.append(h3)
271
+ panel.append(bodyWithoutRationale(opt.el))
272
+ const rationale = extractRationale(opt.el)
273
+ if (rationale) panel.append(rationale)
274
+
275
+ const actions = el(doc, 'div', 'bp-choice__actions')
276
+ const adopt = el(doc, 'label', 'bp-choice__adopt')
277
+ const commit = doc.createElement('input')
278
+ commit.type = 'radio'
279
+ commit.name = pickName
280
+ commit.value = opt.value
281
+ commit.className = 'bp-choice__commit'
282
+ adopt.append(commit, doc.createTextNode(adoptLabel))
283
+ actions.append(adopt)
284
+ panel.append(actions)
285
+ panels.append(panel)
286
+ }
287
+ form.append(panels)
288
+
289
+ const style = el(doc, 'style')
290
+ style.textContent = scopedTabRules(`[data-bp-choice="${scopeId}"]`, viewName, pickName, titleOpts)
291
+ form.prepend(style)
292
+
293
+ form.append(
294
+ buildFooter(
295
+ doc,
296
+ hint || 'Switch tabs to preview · adopt to commit',
297
+ reconsider
298
+ )
299
+ )
300
+ }
301
+
302
+ if (layout === 'stack') {
303
+ const stack = el(doc, 'div', 'bp-choice__stack')
304
+ for (const opt of options) {
305
+ const card = el(doc, 'label', 'bp-choice__card')
306
+ const input = doc.createElement('input')
307
+ input.type = 'radio'
308
+ input.name = pickName
309
+ input.value = opt.value
310
+ input.className = 'bp-choice__pick'
311
+ card.append(input)
312
+
313
+ const head = el(doc, 'div', 'bp-choice__card-head')
314
+ if (opt.optionLabel) head.append(label(doc, opt.optionLabel))
315
+ const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__card-chosen')
316
+ chosen.textContent = '✓ Chosen'
317
+ const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__card-rejected')
318
+ rejected.textContent = 'Not chosen'
319
+ head.append(chosen, rejected)
320
+ card.append(head)
321
+
322
+ const h4 = el(doc, 'h4')
323
+ h4.textContent = opt.title
324
+ card.append(h4)
325
+ card.append(bodyWithoutRationale(opt.el))
326
+ const rationale = extractRationale(opt.el)
327
+ if (rationale) card.append(rationale)
328
+ stack.append(card)
329
+ }
330
+ form.append(stack)
331
+ form.append(
332
+ buildFooter(
333
+ doc,
334
+ hint || 'Select a card to commit · rejected drafts stay readable below',
335
+ reconsider
336
+ )
337
+ )
338
+ }
339
+
340
+ if (layout === 'gallery') {
341
+ const gallery = el(doc, 'div', 'bp-choice__gallery')
342
+ for (const opt of options) {
343
+ const mock = el(doc, 'label', 'bp-choice__mock')
344
+ const input = doc.createElement('input')
345
+ input.type = 'radio'
346
+ input.name = pickName
347
+ input.value = opt.value
348
+ input.className = 'bp-choice__pick'
349
+ mock.append(input)
350
+
351
+ const frame = el(doc, 'div', 'bp-choice__mock-frame')
352
+ const frameInner = frameContent(opt.el)
353
+ if (frameInner) frame.append(frameInner)
354
+ mock.append(frame)
355
+
356
+ const cap = el(doc, 'div', 'bp-choice__mock-cap')
357
+ const h4 = el(doc, 'h4')
358
+ h4.textContent = opt.caption
359
+ cap.append(h4)
360
+ const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__mock-pick bp-choice__mock-chosen')
361
+ chosen.textContent = '✓ Chosen'
362
+ const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__mock-pick bp-choice__mock-rejected')
363
+ rejected.textContent = 'Not chosen'
364
+ cap.append(chosen, rejected)
365
+ mock.append(cap)
366
+ gallery.append(mock)
367
+ }
368
+ form.append(gallery)
369
+
370
+ if (compareSummary && compareBody) {
371
+ const details = el(doc, 'details', 'bp-choice__compare')
372
+ const summary = el(doc, 'summary')
373
+ summary.textContent = compareSummary
374
+ details.append(summary)
375
+ const body = el(doc, 'div', 'bp-choice__compare-body')
376
+ body.append(compareBody.cloneNode(true))
377
+ details.append(body)
378
+ form.append(details)
379
+ }
380
+
381
+ form.append(
382
+ buildFooter(doc, hint || 'Select a mockup to track the decision', reconsider)
383
+ )
384
+ }
385
+
386
+ this.replaceChildren(form)
387
+ }
388
+ }
389
+
390
+ class BlueprintPreflightAElement extends HTMLElement {}
391
+
392
+ class BlueprintPreflightQElement extends HTMLElement {}
393
+
394
+ class BlueprintPreflightElement extends HTMLElement {
395
+ connectedCallback() {
396
+ if (this.dataset.bpRendered) return
397
+ this.dataset.bpRendered = '1'
398
+
399
+ const doc = this.ownerDocument
400
+ const title = this.getAttribute('title') ?? 'Before drafting'
401
+ const draftName = this.getAttribute('draft') ?? 'section'
402
+ const hint = this.getAttribute('hint') ?? 'Answer all questions to unlock · choices stay editable'
403
+ const reconsider = this.getAttribute('reconsider') ?? 'Reset answers'
404
+ const scopeId = nextId('bp-preflight')
405
+
406
+ const questions = [...this.querySelectorAll(':scope > bp-preflight-q')]
407
+ const form = el(doc, 'form')
408
+ const panel = el(doc, 'div', 'bp-preflight')
409
+ panel.dataset.bpPreflight = scopeId
410
+
411
+ const bar = el(doc, 'div', 'bp-preflight__bar')
412
+ bar.append(label(doc, title))
413
+ const count = el(doc, 'span', 'bp-preflight__count')
414
+ count.textContent = `${questions.length} required`
415
+ bar.append(count)
416
+ panel.append(bar)
417
+
418
+ const list = el(doc, 'div', 'bp-preflight__questions')
419
+ /** @type {string[]} */
420
+ const gateSelectors = []
421
+
422
+ for (const qNode of questions) {
423
+ const qName = qNode.getAttribute('name') ?? nextId('q')
424
+ const kind = (qNode.getAttribute('kind') || 'one').toLowerCase()
425
+ const prompt = qNode.getAttribute('prompt') ?? ''
426
+ const answers = [...qNode.querySelectorAll(':scope > bp-preflight-a')]
427
+
428
+ const q = el(doc, 'div', 'bp-preflight__q')
429
+ const promptEl = el(doc, 'p', 'bp-preflight__prompt')
430
+ promptEl.append(doc.createTextNode(prompt))
431
+ const kindEl = el(doc, 'span', 'bp-preflight__kind')
432
+ kindEl.textContent = kind === 'many' ? '· choose any' : '· choose one'
433
+ promptEl.append(kindEl)
434
+ const resolved = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-preflight__resolved')
435
+ resolved.textContent = '✓ Resolved'
436
+ promptEl.append(resolved)
437
+ q.append(promptEl)
438
+
439
+ const chips = el(doc, 'div', 'bp-preflight__chips')
440
+ for (const aNode of answers) {
441
+ const chip = el(doc, 'label', 'bp-preflight__chip')
442
+ const input = doc.createElement('input')
443
+ input.type = kind === 'many' ? 'checkbox' : 'radio'
444
+ input.name = qName
445
+ input.value = aNode.getAttribute('value') ?? aNode.textContent?.trim() ?? ''
446
+ chip.append(input, doc.createTextNode(aNode.textContent?.trim() ?? ''))
447
+ chips.append(chip)
448
+ }
449
+ q.append(chips)
450
+
451
+ const rationale = extractRationale(qNode)
452
+ if (rationale) q.append(rationale)
453
+ list.append(q)
454
+ gateSelectors.push(`[name="${qName}"]:checked`)
455
+ }
456
+ panel.append(list)
457
+
458
+ const gateWrap = el(doc, 'div')
459
+ gateWrap.style.padding = 'var(--bp-space-3)'
460
+ const gate = el(doc, 'div', 'bp-preflight__gate')
461
+ const locked = el(doc, 'div', 'bp-preflight__gate-locked')
462
+ const lockedTag = el(doc, 'span', 'bp-choice__tag')
463
+ lockedTag.textContent = `⌧ Section fenced — resolve the ${questions.length} decisions above to draft`
464
+ locked.append(lockedTag)
465
+ const ready = el(doc, 'div', 'bp-preflight__gate-ready')
466
+ const readyP = el(doc, 'p')
467
+ readyP.style.margin = '0 0 var(--bp-space-1)'
468
+ readyP.textContent = 'All decisions resolved.'
469
+ const draftBtn = el(doc, 'span', 'bp-preflight__draft')
470
+ draftBtn.textContent = `Draft “${draftName}” →`
471
+ ready.append(readyP, draftBtn)
472
+ gate.append(locked, ready)
473
+ gateWrap.append(gate)
474
+ panel.append(gateWrap)
475
+
476
+ const style = el(doc, 'style')
477
+ const gateRule = gateSelectors.map((sel) => `:has(${sel})`).join('')
478
+ style.textContent =
479
+ `[data-bp-preflight="${scopeId}"]${gateRule}{--bp-preflight-ready:1}` +
480
+ `[data-bp-preflight="${scopeId}"]${gateRule}{border-color:var(--bp-ink-line)}` +
481
+ `[data-bp-preflight="${scopeId}"]${gateRule} .bp-preflight__gate{background-image:none;border-style:solid}` +
482
+ `[data-bp-preflight="${scopeId}"]${gateRule} .bp-preflight__gate-locked{display:none}` +
483
+ `[data-bp-preflight="${scopeId}"]${gateRule} .bp-preflight__gate-ready{display:block}`
484
+ panel.prepend(style)
485
+
486
+ const footer = el(doc, 'div', 'bp-preflight__footer')
487
+ const hintEl = el(doc, 'span', 'bp-choice__hint')
488
+ hintEl.textContent = hint
489
+ const reset = el(doc, 'button', 'bp-choice__reset')
490
+ reset.type = 'reset'
491
+ reset.textContent = reconsider
492
+ footer.append(hintEl, reset)
493
+
494
+ form.append(panel, footer)
495
+ this.replaceChildren(form)
496
+ }
497
+ }
498
+
499
+ class BlueprintChoiceRecordRowElement extends HTMLElement {
500
+ connectedCallback() {
501
+ if (this.dataset.bpRendered) return
502
+ this.dataset.bpRendered = '1'
503
+
504
+ const doc = this.ownerDocument
505
+ const rowLabel = this.getAttribute('label') ?? ''
506
+ const value = this.getAttribute('value') ?? ''
507
+ const alts = this.getAttribute('alts') ?? ''
508
+
509
+ const dl = el(doc, 'dl', 'bp-choice-record__row')
510
+ const dt = el(doc, 'dt')
511
+ dt.textContent = rowLabel
512
+ const dd = el(doc, 'dd')
513
+ dd.textContent = value
514
+ dl.append(dt, dd)
515
+ if (alts) {
516
+ const tag = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out')
517
+ tag.textContent = alts
518
+ dl.append(tag)
519
+ }
520
+
521
+ const considered = this.querySelector(':scope > [slot="considered"]')
522
+ const nodes = [dl]
523
+ if (considered) {
524
+ const details = el(doc, 'details', 'bp-choice__compare')
525
+ details.style.marginTop = 'var(--bp-space-2)'
526
+ const summary = el(doc, 'summary')
527
+ summary.textContent = '+ Show the options considered'
528
+ details.append(summary)
529
+ const body = el(doc, 'div', 'bp-choice__compare-body')
530
+ body.append(considered.cloneNode(true))
531
+ details.append(body)
532
+ nodes.push(details)
533
+ }
534
+ this.replaceChildren(...nodes)
535
+ }
536
+ }
537
+
538
+ class BlueprintChoiceRecordElement extends HTMLElement {
539
+ connectedCallback() {
540
+ if (this.dataset.bpRendered) return
541
+ this.dataset.bpRendered = '1'
542
+ const wrap = this.ownerDocument.createElement('div')
543
+ wrap.className = 'bp-choice-record'
544
+ wrap.append(...this.childNodes)
545
+ this.replaceChildren(wrap)
546
+ }
547
+ }
548
+
549
+ /** @param {string} name @param {typeof HTMLElement} ctor */
550
+ function define(name, ctor) {
551
+ if (typeof customElements !== 'undefined' && !customElements.get(name)) {
552
+ customElements.define(name, ctor)
553
+ }
554
+ }
555
+
556
+ export function registerBlueprintChoiceElements() {
557
+ define('bp-rationale', BlueprintRationaleElement)
558
+ define('bp-choice-option', BlueprintChoiceOptionElement)
559
+ define('bp-choice', BlueprintChoiceElement)
560
+ define('bp-preflight-a', BlueprintPreflightAElement)
561
+ define('bp-preflight-q', BlueprintPreflightQElement)
562
+ define('bp-preflight', BlueprintPreflightElement)
563
+ define('bp-choice-record-row', BlueprintChoiceRecordRowElement)
564
+ define('bp-choice-record', BlueprintChoiceRecordElement)
565
+ }
566
+
567
+ registerBlueprintChoiceElements()