@obvi/blueprint 1.0.9 → 1.0.10
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 +4 -1
- package/dist/blueprint-choices.js +507 -0
- package/dist/blueprint.css +519 -1
- package/dist/blueprint.js +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -74,6 +74,9 @@ Reusable primitives promoted from repeated blueprint authoring patterns:
|
|
|
74
74
|
- `.bp-lede` — narrative lede escape hatch
|
|
75
75
|
- `.bp-decision` — primary decision panel: an inverted `.bp-decision__bar` (kicker `.bp-label` + optional `.bp-decision__status` pill + optional `.bp-decision__meta` provenance with `<time>`), the hero `.bp-decision-stmt`, stacked `.bp-decision__tenets` (a `<dl>`), and a hatched `.bp-decision__revisit` change-condition footer. Add `.bp-decision--collapsible` on a `<details>` whose `<summary>` is the `.bp-decision__bar` (kicker + a compact `.bp-decision__title` left, `.bp-decision__meta` + `.bp-decision__caret` right) for a native, script-free collapsed form: collapsed it is just the bar, and the tenets + revisit footer reveal on expand
|
|
76
76
|
- `<bp-callout type="locked|invariant|ref">` — typed callout element (expands to the `.bp-callout` family below). Drafting-style: four L-shaped corner registration ticks (drawn as background gradients, no extra DOM) bracket the body instead of a box, headed by a `.bp-ctag` label (icon + compact mono caption). `--locked` uses solid ink ticks, `--invariant` adds the hatch fill with heavier ticks, `--ref` uses soft ticks. `label="…"` overrides the caption and `icon="none"` drops the glyph; the raw `.bp-callout--locked` / `--invariant` / `--ref` markup stays valid for hand-built or stored documents
|
|
77
|
+
- `<bp-choice layout="tabs|stack|gallery">` — interactive deliberation for section directions or mockup picks; expands to a CSS-only form (verdict banner, inline rationale, reconsider reset)
|
|
78
|
+
- `<bp-preflight>` — pre-draft decision panel; gates a fenced draft target until required questions are answered
|
|
79
|
+
- `<bp-choice-record>` — compact archive of resolved decisions with optional considered-options disclosure
|
|
77
80
|
- `.bp-deflist` — widened definition-list variant
|
|
78
81
|
- `.bp-option-grid`, `.bp-opt--rec`, `.bp-verdict` — parallel option comparisons
|
|
79
82
|
- `.bp-sequence` — numbered linear pipeline
|
|
@@ -109,7 +112,7 @@ an npm token with access to the organization without committing it:
|
|
|
109
112
|
Pin the exact package version so the stylesheet is immutable across installs:
|
|
110
113
|
|
|
111
114
|
```bash
|
|
112
|
-
npm install --save-exact @obvi/blueprint@1.0.
|
|
115
|
+
npm install --save-exact @obvi/blueprint@1.0.10
|
|
113
116
|
```
|
|
114
117
|
|
|
115
118
|
Import the canonical stylesheet from the supported package export:
|
|
@@ -0,0 +1,507 @@
|
|
|
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
|
+
// verdict: kicker on the committed banner (default "Chosen")
|
|
18
|
+
// adopt: tabs-only commit button label (default "Adopt this direction")
|
|
19
|
+
// hint: footer hint; reconsider: reset button label (default "Reconsider")
|
|
20
|
+
// compare: gallery-only <details> summary (omit to skip compare block)
|
|
21
|
+
|
|
22
|
+
/** @typedef {'tabs' | 'stack' | 'gallery'} ChoiceLayout */
|
|
23
|
+
|
|
24
|
+
let choiceSeq = 0
|
|
25
|
+
|
|
26
|
+
/** @param {string} prefix */
|
|
27
|
+
function nextId(prefix) {
|
|
28
|
+
choiceSeq += 1
|
|
29
|
+
return `${prefix}-${choiceSeq}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {ParentNode} root */
|
|
33
|
+
function extractRationale(root) {
|
|
34
|
+
const node = root.querySelector(':scope > bp-rationale')
|
|
35
|
+
if (!node) return null
|
|
36
|
+
const wrap = node.ownerDocument.createElement('div')
|
|
37
|
+
wrap.className = 'bp-choice-rationale'
|
|
38
|
+
wrap.append(...node.childNodes)
|
|
39
|
+
return wrap
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @param {Element} root */
|
|
43
|
+
function bodyWithoutRationale(root) {
|
|
44
|
+
const frag = root.ownerDocument.createDocumentFragment()
|
|
45
|
+
for (const child of root.childNodes) {
|
|
46
|
+
if (child instanceof Element && child.tagName === 'BP-RATIONALE') continue
|
|
47
|
+
if (child instanceof Element && child.getAttribute('slot') === 'frame') continue
|
|
48
|
+
frag.append(child.cloneNode(true))
|
|
49
|
+
}
|
|
50
|
+
return frag
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {Element} root */
|
|
54
|
+
function frameContent(root) {
|
|
55
|
+
const slotted = root.querySelector(':scope > [slot="frame"]')
|
|
56
|
+
if (slotted) {
|
|
57
|
+
const frame = slotted.cloneNode(true)
|
|
58
|
+
frame.removeAttribute('slot')
|
|
59
|
+
return frame
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {Document} doc
|
|
66
|
+
* @param {string} tag
|
|
67
|
+
* @param {string} [className]
|
|
68
|
+
*/
|
|
69
|
+
function el(doc, tag, className) {
|
|
70
|
+
const node = doc.createElement(tag)
|
|
71
|
+
if (className) node.className = className
|
|
72
|
+
return node
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Document} doc
|
|
77
|
+
* @param {string} text
|
|
78
|
+
* @param {string} [className]
|
|
79
|
+
*/
|
|
80
|
+
function label(doc, text, className) {
|
|
81
|
+
const node = el(doc, 'span', className ?? 'bp-label')
|
|
82
|
+
node.textContent = text
|
|
83
|
+
return node
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {Document} doc
|
|
88
|
+
* @param {string} kicker
|
|
89
|
+
* @param {Array<{ value: string, title: string }>} options
|
|
90
|
+
*/
|
|
91
|
+
function buildVerdict(doc, kicker, options) {
|
|
92
|
+
const verdict = el(doc, 'div', 'bp-choice__verdict')
|
|
93
|
+
verdict.append(label(doc, kicker))
|
|
94
|
+
for (const opt of options) {
|
|
95
|
+
const pick = el(doc, 'p', 'bp-choice__verdict-pick')
|
|
96
|
+
pick.dataset.for = opt.value
|
|
97
|
+
pick.textContent = opt.title
|
|
98
|
+
verdict.append(pick)
|
|
99
|
+
}
|
|
100
|
+
const meta = el(doc, 'span', 'bp-choice__verdict-meta')
|
|
101
|
+
meta.textContent = 'You · just now'
|
|
102
|
+
verdict.append(meta)
|
|
103
|
+
return verdict
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {Document} doc
|
|
108
|
+
* @param {string} hint
|
|
109
|
+
* @param {string} reconsider
|
|
110
|
+
*/
|
|
111
|
+
function buildFooter(doc, hint, reconsider) {
|
|
112
|
+
const actions = el(doc, 'div', 'bp-choice__actions')
|
|
113
|
+
if (hint) {
|
|
114
|
+
const hintEl = el(doc, 'span', 'bp-choice__hint')
|
|
115
|
+
hintEl.textContent = hint
|
|
116
|
+
actions.append(hintEl)
|
|
117
|
+
}
|
|
118
|
+
const reset = el(doc, 'button', 'bp-choice__reset')
|
|
119
|
+
reset.type = 'reset'
|
|
120
|
+
reset.textContent = reconsider
|
|
121
|
+
actions.append(reset)
|
|
122
|
+
return actions
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} scope
|
|
127
|
+
* @param {string} viewName
|
|
128
|
+
* @param {string} pickName
|
|
129
|
+
* @param {Array<{ value: string, title: string }>} options
|
|
130
|
+
*/
|
|
131
|
+
function scopedTabRules(scope, viewName, pickName, options) {
|
|
132
|
+
const lines = options.map(
|
|
133
|
+
(opt) =>
|
|
134
|
+
`${scope}:has([name="${viewName}"][value="${opt.value}"]:checked) .bp-choice__panel[data-value="${opt.value}"]{display:block}` +
|
|
135
|
+
`${scope}:has([name="${pickName}"][value="${opt.value}"]:checked) .bp-choice__verdict-pick[data-for="${opt.value}"]{display:block}`
|
|
136
|
+
)
|
|
137
|
+
return lines.join('\n')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @param {HTMLElement} host */
|
|
141
|
+
function readOptions(host) {
|
|
142
|
+
return [...host.querySelectorAll(':scope > bp-choice-option')].map((node) => ({
|
|
143
|
+
el: node,
|
|
144
|
+
value: node.getAttribute('value') ?? '',
|
|
145
|
+
tab: node.getAttribute('tab') ?? node.getAttribute('label') ?? node.getAttribute('value') ?? '',
|
|
146
|
+
title: node.getAttribute('title') ?? node.getAttribute('caption') ?? node.getAttribute('tab') ?? '',
|
|
147
|
+
optionLabel: node.getAttribute('label') ?? node.getAttribute('tab') ?? '',
|
|
148
|
+
caption: node.getAttribute('caption') ?? node.getAttribute('title') ?? '',
|
|
149
|
+
}))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class BlueprintRationaleElement extends HTMLElement {
|
|
153
|
+
connectedCallback() {
|
|
154
|
+
if (this.dataset.bpRendered) return
|
|
155
|
+
this.dataset.bpRendered = '1'
|
|
156
|
+
this.classList.add('bp-choice-rationale')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class BlueprintChoiceOptionElement extends HTMLElement {}
|
|
161
|
+
|
|
162
|
+
class BlueprintChoiceElement extends HTMLElement {
|
|
163
|
+
connectedCallback() {
|
|
164
|
+
if (this.dataset.bpRendered) return
|
|
165
|
+
this.dataset.bpRendered = '1'
|
|
166
|
+
|
|
167
|
+
const doc = this.ownerDocument
|
|
168
|
+
/** @type {ChoiceLayout} */
|
|
169
|
+
const layout = (this.getAttribute('layout') || 'stack').toLowerCase()
|
|
170
|
+
const verdictKicker = this.getAttribute('verdict') ?? 'Chosen'
|
|
171
|
+
const adoptLabel = this.getAttribute('adopt') ?? 'Adopt this direction'
|
|
172
|
+
const hint = this.getAttribute('hint') ?? ''
|
|
173
|
+
const reconsider = this.getAttribute('reconsider') ?? 'Reconsider'
|
|
174
|
+
const compareSummary = this.getAttribute('compare')
|
|
175
|
+
const compareBody = this.querySelector(':scope > [slot="compare"]')
|
|
176
|
+
|
|
177
|
+
const options = readOptions(this)
|
|
178
|
+
const scopeId = nextId('bp-choice')
|
|
179
|
+
const viewName = `${scopeId}-view`
|
|
180
|
+
const pickName = `${scopeId}-pick`
|
|
181
|
+
|
|
182
|
+
const form = el(doc, 'form', `bp-choice bp-choice--${layout}`)
|
|
183
|
+
form.dataset.bpChoice = scopeId
|
|
184
|
+
|
|
185
|
+
const titleOpts = options.map((o) => ({ value: o.value, title: o.title }))
|
|
186
|
+
form.append(buildVerdict(doc, verdictKicker, titleOpts))
|
|
187
|
+
|
|
188
|
+
if (layout === 'tabs') {
|
|
189
|
+
const seg = el(doc, 'div', 'bp-choice__seg')
|
|
190
|
+
seg.setAttribute('role', 'tablist')
|
|
191
|
+
for (const [index, opt] of options.entries()) {
|
|
192
|
+
const segOpt = el(doc, 'label', 'bp-choice__seg-opt')
|
|
193
|
+
const input = doc.createElement('input')
|
|
194
|
+
input.type = 'radio'
|
|
195
|
+
input.name = viewName
|
|
196
|
+
input.value = opt.value
|
|
197
|
+
input.className = 'bp-choice__view'
|
|
198
|
+
if (index === 0) input.checked = true
|
|
199
|
+
segOpt.append(input, doc.createTextNode(opt.tab))
|
|
200
|
+
seg.append(segOpt)
|
|
201
|
+
}
|
|
202
|
+
form.append(seg)
|
|
203
|
+
|
|
204
|
+
const panels = el(doc, 'div', 'bp-choice__panels')
|
|
205
|
+
for (const opt of options) {
|
|
206
|
+
const panel = el(doc, 'div', 'bp-choice__panel')
|
|
207
|
+
panel.dataset.value = opt.value
|
|
208
|
+
const h3 = el(doc, 'h3')
|
|
209
|
+
h3.textContent = opt.title
|
|
210
|
+
panel.append(h3)
|
|
211
|
+
panel.append(bodyWithoutRationale(opt.el))
|
|
212
|
+
const rationale = extractRationale(opt.el)
|
|
213
|
+
if (rationale) panel.append(rationale)
|
|
214
|
+
|
|
215
|
+
const actions = el(doc, 'div', 'bp-choice__actions')
|
|
216
|
+
const adopt = el(doc, 'label', 'bp-choice__adopt')
|
|
217
|
+
const commit = doc.createElement('input')
|
|
218
|
+
commit.type = 'radio'
|
|
219
|
+
commit.name = pickName
|
|
220
|
+
commit.value = opt.value
|
|
221
|
+
commit.className = 'bp-choice__commit'
|
|
222
|
+
adopt.append(commit, doc.createTextNode(adoptLabel))
|
|
223
|
+
actions.append(adopt)
|
|
224
|
+
panel.append(actions)
|
|
225
|
+
panels.append(panel)
|
|
226
|
+
}
|
|
227
|
+
form.append(panels)
|
|
228
|
+
|
|
229
|
+
const style = el(doc, 'style')
|
|
230
|
+
style.textContent = scopedTabRules(`[data-bp-choice="${scopeId}"]`, viewName, pickName, titleOpts)
|
|
231
|
+
form.prepend(style)
|
|
232
|
+
|
|
233
|
+
form.append(
|
|
234
|
+
buildFooter(
|
|
235
|
+
doc,
|
|
236
|
+
hint || 'Switch tabs to preview · adopt to commit',
|
|
237
|
+
reconsider
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (layout === 'stack') {
|
|
243
|
+
const stack = el(doc, 'div', 'bp-choice__stack')
|
|
244
|
+
for (const opt of options) {
|
|
245
|
+
const card = el(doc, 'label', 'bp-choice__card')
|
|
246
|
+
const input = doc.createElement('input')
|
|
247
|
+
input.type = 'radio'
|
|
248
|
+
input.name = pickName
|
|
249
|
+
input.value = opt.value
|
|
250
|
+
input.className = 'bp-choice__pick'
|
|
251
|
+
card.append(input)
|
|
252
|
+
|
|
253
|
+
const head = el(doc, 'div', 'bp-choice__card-head')
|
|
254
|
+
if (opt.optionLabel) head.append(label(doc, opt.optionLabel))
|
|
255
|
+
const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__card-chosen')
|
|
256
|
+
chosen.textContent = '✓ Chosen'
|
|
257
|
+
const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__card-rejected')
|
|
258
|
+
rejected.textContent = 'Not chosen'
|
|
259
|
+
head.append(chosen, rejected)
|
|
260
|
+
card.append(head)
|
|
261
|
+
|
|
262
|
+
const h4 = el(doc, 'h4')
|
|
263
|
+
h4.textContent = opt.title
|
|
264
|
+
card.append(h4)
|
|
265
|
+
card.append(bodyWithoutRationale(opt.el))
|
|
266
|
+
const rationale = extractRationale(opt.el)
|
|
267
|
+
if (rationale) card.append(rationale)
|
|
268
|
+
stack.append(card)
|
|
269
|
+
}
|
|
270
|
+
form.append(stack)
|
|
271
|
+
form.append(
|
|
272
|
+
buildFooter(
|
|
273
|
+
doc,
|
|
274
|
+
hint || 'Select a card to commit · rejected drafts stay readable below',
|
|
275
|
+
reconsider
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (layout === 'gallery') {
|
|
281
|
+
const gallery = el(doc, 'div', 'bp-choice__gallery')
|
|
282
|
+
for (const opt of options) {
|
|
283
|
+
const mock = el(doc, 'label', 'bp-choice__mock')
|
|
284
|
+
const input = doc.createElement('input')
|
|
285
|
+
input.type = 'radio'
|
|
286
|
+
input.name = pickName
|
|
287
|
+
input.value = opt.value
|
|
288
|
+
input.className = 'bp-choice__pick'
|
|
289
|
+
mock.append(input)
|
|
290
|
+
|
|
291
|
+
const frame = el(doc, 'div', 'bp-choice__mock-frame')
|
|
292
|
+
const frameInner = frameContent(opt.el)
|
|
293
|
+
if (frameInner) frame.append(frameInner)
|
|
294
|
+
mock.append(frame)
|
|
295
|
+
|
|
296
|
+
const cap = el(doc, 'div', 'bp-choice__mock-cap')
|
|
297
|
+
const h4 = el(doc, 'h4')
|
|
298
|
+
h4.textContent = opt.caption
|
|
299
|
+
cap.append(h4)
|
|
300
|
+
const chosen = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-choice__mock-pick bp-choice__mock-chosen')
|
|
301
|
+
chosen.textContent = '✓ Chosen'
|
|
302
|
+
const rejected = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out bp-choice__mock-pick bp-choice__mock-rejected')
|
|
303
|
+
rejected.textContent = 'Not chosen'
|
|
304
|
+
cap.append(chosen, rejected)
|
|
305
|
+
mock.append(cap)
|
|
306
|
+
gallery.append(mock)
|
|
307
|
+
}
|
|
308
|
+
form.append(gallery)
|
|
309
|
+
|
|
310
|
+
if (compareSummary && compareBody) {
|
|
311
|
+
const details = el(doc, 'details', 'bp-choice__compare')
|
|
312
|
+
const summary = el(doc, 'summary')
|
|
313
|
+
summary.textContent = compareSummary
|
|
314
|
+
details.append(summary)
|
|
315
|
+
const body = el(doc, 'div', 'bp-choice__compare-body')
|
|
316
|
+
body.append(compareBody.cloneNode(true))
|
|
317
|
+
details.append(body)
|
|
318
|
+
form.append(details)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
form.append(
|
|
322
|
+
buildFooter(doc, hint || 'Select a mockup to track the decision', reconsider)
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.replaceChildren(form)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
class BlueprintPreflightAElement extends HTMLElement {}
|
|
331
|
+
|
|
332
|
+
class BlueprintPreflightQElement extends HTMLElement {}
|
|
333
|
+
|
|
334
|
+
class BlueprintPreflightElement extends HTMLElement {
|
|
335
|
+
connectedCallback() {
|
|
336
|
+
if (this.dataset.bpRendered) return
|
|
337
|
+
this.dataset.bpRendered = '1'
|
|
338
|
+
|
|
339
|
+
const doc = this.ownerDocument
|
|
340
|
+
const title = this.getAttribute('title') ?? 'Before drafting'
|
|
341
|
+
const draftName = this.getAttribute('draft') ?? 'section'
|
|
342
|
+
const hint = this.getAttribute('hint') ?? 'Answer all questions to unlock · choices stay editable'
|
|
343
|
+
const reconsider = this.getAttribute('reconsider') ?? 'Reset answers'
|
|
344
|
+
const scopeId = nextId('bp-preflight')
|
|
345
|
+
|
|
346
|
+
const questions = [...this.querySelectorAll(':scope > bp-preflight-q')]
|
|
347
|
+
const form = el(doc, 'form')
|
|
348
|
+
const panel = el(doc, 'div', 'bp-preflight')
|
|
349
|
+
panel.dataset.bpPreflight = scopeId
|
|
350
|
+
|
|
351
|
+
const bar = el(doc, 'div', 'bp-preflight__bar')
|
|
352
|
+
bar.append(label(doc, title))
|
|
353
|
+
const count = el(doc, 'span', 'bp-preflight__count')
|
|
354
|
+
count.textContent = `${questions.length} required`
|
|
355
|
+
bar.append(count)
|
|
356
|
+
panel.append(bar)
|
|
357
|
+
|
|
358
|
+
const list = el(doc, 'div', 'bp-preflight__questions')
|
|
359
|
+
/** @type {string[]} */
|
|
360
|
+
const gateSelectors = []
|
|
361
|
+
|
|
362
|
+
for (const qNode of questions) {
|
|
363
|
+
const qName = qNode.getAttribute('name') ?? nextId('q')
|
|
364
|
+
const kind = (qNode.getAttribute('kind') || 'one').toLowerCase()
|
|
365
|
+
const prompt = qNode.getAttribute('prompt') ?? ''
|
|
366
|
+
const answers = [...qNode.querySelectorAll(':scope > bp-preflight-a')]
|
|
367
|
+
|
|
368
|
+
const q = el(doc, 'div', 'bp-preflight__q')
|
|
369
|
+
const promptEl = el(doc, 'p', 'bp-preflight__prompt')
|
|
370
|
+
promptEl.append(doc.createTextNode(prompt))
|
|
371
|
+
const kindEl = el(doc, 'span', 'bp-preflight__kind')
|
|
372
|
+
kindEl.textContent = kind === 'many' ? '· choose any' : '· choose one'
|
|
373
|
+
promptEl.append(kindEl)
|
|
374
|
+
const resolved = el(doc, 'span', 'bp-choice__tag bp-choice__tag--ink bp-preflight__resolved')
|
|
375
|
+
resolved.textContent = '✓ Resolved'
|
|
376
|
+
promptEl.append(resolved)
|
|
377
|
+
q.append(promptEl)
|
|
378
|
+
|
|
379
|
+
const chips = el(doc, 'div', 'bp-preflight__chips')
|
|
380
|
+
for (const aNode of answers) {
|
|
381
|
+
const chip = el(doc, 'label', 'bp-preflight__chip')
|
|
382
|
+
const input = doc.createElement('input')
|
|
383
|
+
input.type = kind === 'many' ? 'checkbox' : 'radio'
|
|
384
|
+
input.name = qName
|
|
385
|
+
input.value = aNode.getAttribute('value') ?? aNode.textContent?.trim() ?? ''
|
|
386
|
+
chip.append(input, doc.createTextNode(aNode.textContent?.trim() ?? ''))
|
|
387
|
+
chips.append(chip)
|
|
388
|
+
}
|
|
389
|
+
q.append(chips)
|
|
390
|
+
|
|
391
|
+
const rationale = extractRationale(qNode)
|
|
392
|
+
if (rationale) q.append(rationale)
|
|
393
|
+
list.append(q)
|
|
394
|
+
gateSelectors.push(`[name="${qName}"]:checked`)
|
|
395
|
+
}
|
|
396
|
+
panel.append(list)
|
|
397
|
+
|
|
398
|
+
const gateWrap = el(doc, 'div')
|
|
399
|
+
gateWrap.style.padding = 'var(--bp-space-3)'
|
|
400
|
+
const gate = el(doc, 'div', 'bp-preflight__gate')
|
|
401
|
+
const locked = el(doc, 'div', 'bp-preflight__gate-locked')
|
|
402
|
+
const lockedTag = el(doc, 'span', 'bp-choice__tag')
|
|
403
|
+
lockedTag.textContent = `⌧ Section fenced — resolve the ${questions.length} decisions above to draft`
|
|
404
|
+
locked.append(lockedTag)
|
|
405
|
+
const ready = el(doc, 'div', 'bp-preflight__gate-ready')
|
|
406
|
+
const readyP = el(doc, 'p')
|
|
407
|
+
readyP.style.margin = '0 0 var(--bp-space-1)'
|
|
408
|
+
readyP.textContent = 'All decisions resolved.'
|
|
409
|
+
const draftBtn = el(doc, 'span', 'bp-preflight__draft')
|
|
410
|
+
draftBtn.textContent = `Draft “${draftName}” →`
|
|
411
|
+
ready.append(readyP, draftBtn)
|
|
412
|
+
gate.append(locked, ready)
|
|
413
|
+
gateWrap.append(gate)
|
|
414
|
+
panel.append(gateWrap)
|
|
415
|
+
|
|
416
|
+
const style = el(doc, 'style')
|
|
417
|
+
const gateRule = gateSelectors.map((sel) => `:has(${sel})`).join('')
|
|
418
|
+
style.textContent =
|
|
419
|
+
`[data-bp-preflight="${scopeId}"]${gateRule}{--bp-preflight-ready:1}` +
|
|
420
|
+
`[data-bp-preflight="${scopeId}"]${gateRule}{border-color:var(--bp-ink-line)}` +
|
|
421
|
+
`[data-bp-preflight="${scopeId}"]${gateRule} .bp-preflight__gate{background-image:none;border-style:solid}` +
|
|
422
|
+
`[data-bp-preflight="${scopeId}"]${gateRule} .bp-preflight__gate-locked{display:none}` +
|
|
423
|
+
`[data-bp-preflight="${scopeId}"]${gateRule} .bp-preflight__gate-ready{display:block}`
|
|
424
|
+
panel.prepend(style)
|
|
425
|
+
|
|
426
|
+
const footer = el(doc, 'div', 'bp-preflight__footer')
|
|
427
|
+
const hintEl = el(doc, 'span', 'bp-choice__hint')
|
|
428
|
+
hintEl.textContent = hint
|
|
429
|
+
const reset = el(doc, 'button', 'bp-choice__reset')
|
|
430
|
+
reset.type = 'reset'
|
|
431
|
+
reset.textContent = reconsider
|
|
432
|
+
footer.append(hintEl, reset)
|
|
433
|
+
|
|
434
|
+
form.append(panel, footer)
|
|
435
|
+
this.replaceChildren(form)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
class BlueprintChoiceRecordRowElement extends HTMLElement {
|
|
440
|
+
connectedCallback() {
|
|
441
|
+
if (this.dataset.bpRendered) return
|
|
442
|
+
this.dataset.bpRendered = '1'
|
|
443
|
+
|
|
444
|
+
const doc = this.ownerDocument
|
|
445
|
+
const rowLabel = this.getAttribute('label') ?? ''
|
|
446
|
+
const value = this.getAttribute('value') ?? ''
|
|
447
|
+
const alts = this.getAttribute('alts') ?? ''
|
|
448
|
+
|
|
449
|
+
const dl = el(doc, 'dl', 'bp-choice-record__row')
|
|
450
|
+
const dt = el(doc, 'dt')
|
|
451
|
+
dt.textContent = rowLabel
|
|
452
|
+
const dd = el(doc, 'dd')
|
|
453
|
+
dd.textContent = value
|
|
454
|
+
dl.append(dt, dd)
|
|
455
|
+
if (alts) {
|
|
456
|
+
const tag = el(doc, 'span', 'bp-choice__tag bp-choice__tag--out')
|
|
457
|
+
tag.textContent = alts
|
|
458
|
+
dl.append(tag)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const considered = this.querySelector(':scope > [slot="considered"]')
|
|
462
|
+
const nodes = [dl]
|
|
463
|
+
if (considered) {
|
|
464
|
+
const details = el(doc, 'details', 'bp-choice__compare')
|
|
465
|
+
details.style.marginTop = 'var(--bp-space-2)'
|
|
466
|
+
const summary = el(doc, 'summary')
|
|
467
|
+
summary.textContent = '+ Show the options considered'
|
|
468
|
+
details.append(summary)
|
|
469
|
+
const body = el(doc, 'div', 'bp-choice__compare-body')
|
|
470
|
+
body.append(considered.cloneNode(true))
|
|
471
|
+
details.append(body)
|
|
472
|
+
nodes.push(details)
|
|
473
|
+
}
|
|
474
|
+
this.replaceChildren(...nodes)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
class BlueprintChoiceRecordElement extends HTMLElement {
|
|
479
|
+
connectedCallback() {
|
|
480
|
+
if (this.dataset.bpRendered) return
|
|
481
|
+
this.dataset.bpRendered = '1'
|
|
482
|
+
const wrap = this.ownerDocument.createElement('div')
|
|
483
|
+
wrap.className = 'bp-choice-record'
|
|
484
|
+
wrap.append(...this.childNodes)
|
|
485
|
+
this.replaceChildren(wrap)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** @param {string} name @param {typeof HTMLElement} ctor */
|
|
490
|
+
function define(name, ctor) {
|
|
491
|
+
if (typeof customElements !== 'undefined' && !customElements.get(name)) {
|
|
492
|
+
customElements.define(name, ctor)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function registerBlueprintChoiceElements() {
|
|
497
|
+
define('bp-rationale', BlueprintRationaleElement)
|
|
498
|
+
define('bp-choice-option', BlueprintChoiceOptionElement)
|
|
499
|
+
define('bp-choice', BlueprintChoiceElement)
|
|
500
|
+
define('bp-preflight-a', BlueprintPreflightAElement)
|
|
501
|
+
define('bp-preflight-q', BlueprintPreflightQElement)
|
|
502
|
+
define('bp-preflight', BlueprintPreflightElement)
|
|
503
|
+
define('bp-choice-record-row', BlueprintChoiceRecordRowElement)
|
|
504
|
+
define('bp-choice-record', BlueprintChoiceRecordElement)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
registerBlueprintChoiceElements()
|
package/dist/blueprint.css
CHANGED
|
@@ -1472,7 +1472,8 @@
|
|
|
1472
1472
|
}
|
|
1473
1473
|
:where(.bp-opt--rec) {
|
|
1474
1474
|
background: var(--bp-fill-amb);
|
|
1475
|
-
|
|
1475
|
+
outline: 2px solid var(--bp-ink);
|
|
1476
|
+
outline-offset: -2px;
|
|
1476
1477
|
}
|
|
1477
1478
|
:where(.bp-verdict) {
|
|
1478
1479
|
display: inline-block;
|
|
@@ -1548,6 +1549,523 @@
|
|
|
1548
1549
|
background-image: var(--bp-hatch);
|
|
1549
1550
|
}
|
|
1550
1551
|
|
|
1552
|
+
/* ---- Interactive choice family ----------------------------------
|
|
1553
|
+
CSS-only deliberation primitives (radio / checkbox + :has() +
|
|
1554
|
+
native form reset). Author with <bp-choice>, <bp-preflight>, and
|
|
1555
|
+
related elements; blueprint-choices.js expands them to this markup.
|
|
1556
|
+
Selected items always get a full ink border — never a left-bar only. */
|
|
1557
|
+
|
|
1558
|
+
:where(bp-choice),
|
|
1559
|
+
:where(bp-preflight),
|
|
1560
|
+
:where(bp-choice-record) {
|
|
1561
|
+
display: block;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
:where(.bp-choice) {
|
|
1565
|
+
margin: var(--bp-space-4) 0;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/* Verdict banner — hidden until a choice is committed. */
|
|
1569
|
+
:where(.bp-choice__verdict) {
|
|
1570
|
+
display: none;
|
|
1571
|
+
align-items: center;
|
|
1572
|
+
gap: var(--bp-space-2);
|
|
1573
|
+
padding: var(--bp-space-2) var(--bp-space-3);
|
|
1574
|
+
background: var(--bp-ink);
|
|
1575
|
+
color: var(--bp-paper);
|
|
1576
|
+
border-radius: var(--bp-radius-4);
|
|
1577
|
+
margin-bottom: var(--bp-space-3);
|
|
1578
|
+
}
|
|
1579
|
+
:where(.bp-choice:has(.bp-choice__commit:checked)) .bp-choice__verdict,
|
|
1580
|
+
:where(.bp-choice:has(.bp-choice__pick:checked)) .bp-choice__verdict {
|
|
1581
|
+
display: flex;
|
|
1582
|
+
}
|
|
1583
|
+
:where(.bp-choice__verdict) > :where(.bp-label) {
|
|
1584
|
+
margin: 0;
|
|
1585
|
+
color: var(--bp-paper);
|
|
1586
|
+
font-size: var(--bp-label-lg);
|
|
1587
|
+
letter-spacing: var(--bp-label-lg-ls);
|
|
1588
|
+
}
|
|
1589
|
+
:where(.bp-choice__verdict-pick) {
|
|
1590
|
+
font-family: var(--bp-sans);
|
|
1591
|
+
font-weight: var(--bp-weight-strong);
|
|
1592
|
+
font-size: var(--bp-text-h4);
|
|
1593
|
+
margin: 0;
|
|
1594
|
+
}
|
|
1595
|
+
:where(.bp-choice__verdict-pick[data-for]) {
|
|
1596
|
+
display: none;
|
|
1597
|
+
}
|
|
1598
|
+
:where(.bp-choice__verdict-meta) {
|
|
1599
|
+
margin: 0 0 0 auto;
|
|
1600
|
+
font-family: var(--bp-mono);
|
|
1601
|
+
font-size: var(--bp-label-md);
|
|
1602
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
1603
|
+
text-transform: uppercase;
|
|
1604
|
+
color: color-mix(in oklch, var(--bp-paper) 72%, transparent);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
:where(.bp-choice-rationale) {
|
|
1608
|
+
margin-top: var(--bp-space-2);
|
|
1609
|
+
font-size: var(--bp-text-small);
|
|
1610
|
+
line-height: var(--bp-lh-small);
|
|
1611
|
+
color: var(--bp-text-secondary);
|
|
1612
|
+
}
|
|
1613
|
+
:where(.bp-choice-rationale) > :where(* + *) {
|
|
1614
|
+
margin-top: var(--bp-space-1);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
:where(.bp-choice__actions) {
|
|
1618
|
+
display: flex;
|
|
1619
|
+
align-items: center;
|
|
1620
|
+
gap: var(--bp-space-3);
|
|
1621
|
+
margin-top: var(--bp-space-3);
|
|
1622
|
+
}
|
|
1623
|
+
:where(.bp-choice__hint) {
|
|
1624
|
+
font-family: var(--bp-mono);
|
|
1625
|
+
font-size: var(--bp-label-md);
|
|
1626
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
1627
|
+
text-transform: uppercase;
|
|
1628
|
+
color: var(--bp-text-secondary);
|
|
1629
|
+
}
|
|
1630
|
+
:where(.bp-choice__reset) {
|
|
1631
|
+
appearance: none;
|
|
1632
|
+
background: none;
|
|
1633
|
+
border: 0;
|
|
1634
|
+
padding: 0;
|
|
1635
|
+
cursor: pointer;
|
|
1636
|
+
font-family: var(--bp-mono);
|
|
1637
|
+
font-size: var(--bp-label-md);
|
|
1638
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
1639
|
+
text-transform: uppercase;
|
|
1640
|
+
color: var(--bp-text-secondary);
|
|
1641
|
+
text-decoration: underline dotted;
|
|
1642
|
+
text-underline-offset: 3px;
|
|
1643
|
+
}
|
|
1644
|
+
:where(.bp-choice__reset:hover) {
|
|
1645
|
+
color: var(--bp-ink);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
:where(.bp-choice__tag) {
|
|
1649
|
+
display: inline-flex;
|
|
1650
|
+
align-items: center;
|
|
1651
|
+
gap: 5px;
|
|
1652
|
+
font-family: var(--bp-mono);
|
|
1653
|
+
font-size: var(--bp-label-sm);
|
|
1654
|
+
letter-spacing: var(--bp-label-sm-ls);
|
|
1655
|
+
text-transform: uppercase;
|
|
1656
|
+
color: var(--bp-text-secondary);
|
|
1657
|
+
}
|
|
1658
|
+
:where(.bp-choice__tag--ink) {
|
|
1659
|
+
color: var(--bp-paper);
|
|
1660
|
+
background: var(--bp-ink);
|
|
1661
|
+
padding: 2px 8px;
|
|
1662
|
+
}
|
|
1663
|
+
:where(.bp-choice__tag--out) {
|
|
1664
|
+
color: var(--bp-ink);
|
|
1665
|
+
border: 1px solid var(--bp-ink-faint);
|
|
1666
|
+
padding: 1px 7px;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/* ---- layout: tabs (preview drafts, then adopt) ------------------- */
|
|
1670
|
+
:where(.bp-choice--tabs) :where(.bp-choice__seg) {
|
|
1671
|
+
display: flex;
|
|
1672
|
+
flex-wrap: wrap;
|
|
1673
|
+
gap: 0;
|
|
1674
|
+
border: 1px solid var(--bp-ink-line);
|
|
1675
|
+
border-radius: var(--bp-radius-4);
|
|
1676
|
+
overflow: hidden;
|
|
1677
|
+
margin: 0 0 var(--bp-space-3);
|
|
1678
|
+
width: fit-content;
|
|
1679
|
+
}
|
|
1680
|
+
:where(.bp-choice__seg-opt) {
|
|
1681
|
+
position: relative;
|
|
1682
|
+
cursor: pointer;
|
|
1683
|
+
font-family: var(--bp-mono);
|
|
1684
|
+
font-size: var(--bp-label-md);
|
|
1685
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
1686
|
+
text-transform: uppercase;
|
|
1687
|
+
color: var(--bp-text-secondary);
|
|
1688
|
+
padding: 6px 14px;
|
|
1689
|
+
border-right: 1px solid var(--bp-ink-line);
|
|
1690
|
+
}
|
|
1691
|
+
:where(.bp-choice__seg-opt:last-child) {
|
|
1692
|
+
border-right: 0;
|
|
1693
|
+
}
|
|
1694
|
+
:where(.bp-choice__seg-opt) :where(input) {
|
|
1695
|
+
position: absolute;
|
|
1696
|
+
opacity: 0;
|
|
1697
|
+
pointer-events: none;
|
|
1698
|
+
}
|
|
1699
|
+
:where(.bp-choice__seg-opt:has(input:checked)) {
|
|
1700
|
+
background: var(--bp-ink);
|
|
1701
|
+
color: var(--bp-paper);
|
|
1702
|
+
}
|
|
1703
|
+
:where(.bp-choice--tabs) :where(.bp-choice__panel) {
|
|
1704
|
+
display: none;
|
|
1705
|
+
}
|
|
1706
|
+
:where(.bp-choice__panel > h3:first-child) {
|
|
1707
|
+
margin-top: 0;
|
|
1708
|
+
}
|
|
1709
|
+
:where(.bp-choice__adopt) {
|
|
1710
|
+
display: inline-flex;
|
|
1711
|
+
align-items: center;
|
|
1712
|
+
gap: 8px;
|
|
1713
|
+
cursor: pointer;
|
|
1714
|
+
border: 1px solid var(--bp-ink);
|
|
1715
|
+
border-radius: var(--bp-radius-4);
|
|
1716
|
+
padding: 6px 14px;
|
|
1717
|
+
font-family: var(--bp-mono);
|
|
1718
|
+
font-size: var(--bp-label-md);
|
|
1719
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
1720
|
+
text-transform: uppercase;
|
|
1721
|
+
color: var(--bp-ink);
|
|
1722
|
+
background: var(--bp-paper);
|
|
1723
|
+
}
|
|
1724
|
+
:where(.bp-choice__adopt:hover) {
|
|
1725
|
+
background: var(--bp-fill-hi);
|
|
1726
|
+
}
|
|
1727
|
+
:where(.bp-choice__adopt) :where(input) {
|
|
1728
|
+
position: absolute;
|
|
1729
|
+
opacity: 0;
|
|
1730
|
+
pointer-events: none;
|
|
1731
|
+
}
|
|
1732
|
+
:where(.bp-choice__panel:has(.bp-choice__commit:checked)) {
|
|
1733
|
+
border: 1px solid var(--bp-ink);
|
|
1734
|
+
background: var(--bp-fill-amb);
|
|
1735
|
+
padding: var(--bp-space-3);
|
|
1736
|
+
margin-left: calc(-1 * var(--bp-space-3));
|
|
1737
|
+
margin-right: calc(-1 * var(--bp-space-3));
|
|
1738
|
+
border-radius: var(--bp-radius-4);
|
|
1739
|
+
}
|
|
1740
|
+
:where(.bp-choice__panel:has(.bp-choice__commit:checked)) :where(.bp-choice__adopt) {
|
|
1741
|
+
background: var(--bp-ink);
|
|
1742
|
+
color: var(--bp-paper);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/* ---- layout: stack (all cards visible) --------------------------- */
|
|
1746
|
+
:where(.bp-choice--stack) :where(.bp-choice__stack) {
|
|
1747
|
+
display: grid;
|
|
1748
|
+
gap: var(--bp-space-3);
|
|
1749
|
+
}
|
|
1750
|
+
:where(.bp-choice__card) {
|
|
1751
|
+
display: block;
|
|
1752
|
+
position: relative;
|
|
1753
|
+
border: 1px solid var(--bp-edge);
|
|
1754
|
+
border-radius: var(--bp-radius-4);
|
|
1755
|
+
padding: var(--bp-space-3);
|
|
1756
|
+
cursor: pointer;
|
|
1757
|
+
background: var(--bp-paper);
|
|
1758
|
+
}
|
|
1759
|
+
:where(.bp-choice__card:hover) {
|
|
1760
|
+
border-color: var(--bp-ink-line);
|
|
1761
|
+
}
|
|
1762
|
+
:where(.bp-choice__card) > :where(input.bp-choice__pick) {
|
|
1763
|
+
position: absolute;
|
|
1764
|
+
top: var(--bp-space-3);
|
|
1765
|
+
right: var(--bp-space-3);
|
|
1766
|
+
accent-color: var(--bp-ink);
|
|
1767
|
+
width: 16px;
|
|
1768
|
+
height: 16px;
|
|
1769
|
+
}
|
|
1770
|
+
:where(.bp-choice__card-head) {
|
|
1771
|
+
display: flex;
|
|
1772
|
+
align-items: baseline;
|
|
1773
|
+
gap: var(--bp-space-2);
|
|
1774
|
+
margin-bottom: var(--bp-space-1);
|
|
1775
|
+
}
|
|
1776
|
+
:where(.bp-choice__card) :where(h4) {
|
|
1777
|
+
margin: 0;
|
|
1778
|
+
padding-right: var(--bp-space-4);
|
|
1779
|
+
}
|
|
1780
|
+
:where(.bp-choice__card-chosen),
|
|
1781
|
+
:where(.bp-choice__card-rejected) {
|
|
1782
|
+
display: none;
|
|
1783
|
+
}
|
|
1784
|
+
:where(.bp-choice__card:has(.bp-choice__pick:checked)) {
|
|
1785
|
+
border: 2px solid var(--bp-ink);
|
|
1786
|
+
background: var(--bp-fill-amb);
|
|
1787
|
+
}
|
|
1788
|
+
:where(.bp-choice__card:has(.bp-choice__pick:checked)) :where(.bp-choice__card-chosen) {
|
|
1789
|
+
display: inline-flex;
|
|
1790
|
+
}
|
|
1791
|
+
:where(.bp-choice--stack:has(.bp-choice__pick:checked)) :where(.bp-choice__card:not(:has(.bp-choice__pick:checked))) {
|
|
1792
|
+
opacity: 0.62;
|
|
1793
|
+
background-image: var(--bp-hatch);
|
|
1794
|
+
}
|
|
1795
|
+
:where(.bp-choice--stack:has(.bp-choice__pick:checked)) :where(.bp-choice__card:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__card-rejected) {
|
|
1796
|
+
display: inline-flex;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/* ---- layout: gallery (mockup pick) ------------------------------- */
|
|
1800
|
+
:where(.bp-choice--gallery) :where(.bp-choice__gallery) {
|
|
1801
|
+
display: grid;
|
|
1802
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1803
|
+
gap: var(--bp-space-3);
|
|
1804
|
+
}
|
|
1805
|
+
:where(.bp-choice__mock) {
|
|
1806
|
+
display: block;
|
|
1807
|
+
position: relative;
|
|
1808
|
+
cursor: pointer;
|
|
1809
|
+
border: 1px solid var(--bp-edge);
|
|
1810
|
+
border-radius: var(--bp-radius-4);
|
|
1811
|
+
overflow: hidden;
|
|
1812
|
+
background: var(--bp-paper);
|
|
1813
|
+
}
|
|
1814
|
+
:where(.bp-choice__mock:hover) {
|
|
1815
|
+
border-color: var(--bp-ink-line);
|
|
1816
|
+
}
|
|
1817
|
+
:where(.bp-choice__mock) > :where(input.bp-choice__pick) {
|
|
1818
|
+
position: absolute;
|
|
1819
|
+
opacity: 0;
|
|
1820
|
+
pointer-events: none;
|
|
1821
|
+
}
|
|
1822
|
+
:where(.bp-choice__mock-frame) {
|
|
1823
|
+
aspect-ratio: 4 / 3;
|
|
1824
|
+
border-bottom: 1px solid var(--bp-edge);
|
|
1825
|
+
background: var(--bp-bg);
|
|
1826
|
+
padding: 10px;
|
|
1827
|
+
display: grid;
|
|
1828
|
+
gap: 6px;
|
|
1829
|
+
}
|
|
1830
|
+
:where(.bp-choice__mock-cap) {
|
|
1831
|
+
display: flex;
|
|
1832
|
+
align-items: baseline;
|
|
1833
|
+
gap: var(--bp-space-2);
|
|
1834
|
+
padding: var(--bp-space-2) var(--bp-space-3);
|
|
1835
|
+
}
|
|
1836
|
+
:where(.bp-choice__mock-cap) :where(h4) {
|
|
1837
|
+
margin: 0;
|
|
1838
|
+
font-size: var(--bp-text-body);
|
|
1839
|
+
}
|
|
1840
|
+
:where(.bp-choice__mock-pick) {
|
|
1841
|
+
margin-left: auto;
|
|
1842
|
+
}
|
|
1843
|
+
:where(.bp-choice__mock-chosen),
|
|
1844
|
+
:where(.bp-choice__mock-rejected) {
|
|
1845
|
+
display: none;
|
|
1846
|
+
}
|
|
1847
|
+
:where(.bp-choice__mock:has(.bp-choice__pick:checked)) {
|
|
1848
|
+
border: 2px solid var(--bp-ink);
|
|
1849
|
+
}
|
|
1850
|
+
:where(.bp-choice__mock:has(.bp-choice__pick:checked)) :where(.bp-choice__mock-chosen) {
|
|
1851
|
+
display: inline-flex;
|
|
1852
|
+
}
|
|
1853
|
+
:where(.bp-choice--gallery:has(.bp-choice__pick:checked)) :where(.bp-choice__mock:not(:has(.bp-choice__pick:checked))) {
|
|
1854
|
+
opacity: 0.5;
|
|
1855
|
+
}
|
|
1856
|
+
:where(.bp-choice--gallery:has(.bp-choice__pick:checked)) :where(.bp-choice__mock:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__mock-frame) {
|
|
1857
|
+
background-image: var(--bp-hatch);
|
|
1858
|
+
}
|
|
1859
|
+
:where(.bp-choice--gallery:has(.bp-choice__pick:checked)) :where(.bp-choice__mock:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__mock-rejected) {
|
|
1860
|
+
display: inline-flex;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
:where(.bp-choice__compare) {
|
|
1864
|
+
margin-top: var(--bp-space-3);
|
|
1865
|
+
border: 1px solid var(--bp-edge);
|
|
1866
|
+
border-radius: var(--bp-radius-4);
|
|
1867
|
+
}
|
|
1868
|
+
:where(.bp-choice__compare > summary) {
|
|
1869
|
+
cursor: pointer;
|
|
1870
|
+
list-style: none;
|
|
1871
|
+
padding: var(--bp-space-2) var(--bp-space-3);
|
|
1872
|
+
font-family: var(--bp-mono);
|
|
1873
|
+
font-size: var(--bp-label-md);
|
|
1874
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
1875
|
+
text-transform: uppercase;
|
|
1876
|
+
color: var(--bp-text-secondary);
|
|
1877
|
+
}
|
|
1878
|
+
:where(.bp-choice__compare > summary)::-webkit-details-marker {
|
|
1879
|
+
display: none;
|
|
1880
|
+
}
|
|
1881
|
+
:where(.bp-choice__compare-body) {
|
|
1882
|
+
padding: var(--bp-space-3);
|
|
1883
|
+
border-top: 1px solid var(--bp-edge);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/* ---- Pre-flight (decisions before drafting) ---------------------- */
|
|
1887
|
+
:where(.bp-preflight) {
|
|
1888
|
+
border: 1px solid var(--bp-ink-line);
|
|
1889
|
+
border-radius: var(--bp-radius-6);
|
|
1890
|
+
overflow: hidden;
|
|
1891
|
+
margin: var(--bp-space-4) 0;
|
|
1892
|
+
}
|
|
1893
|
+
:where(.bp-preflight__bar) {
|
|
1894
|
+
display: flex;
|
|
1895
|
+
align-items: center;
|
|
1896
|
+
gap: var(--bp-space-2);
|
|
1897
|
+
padding: var(--bp-space-2) var(--bp-space-3);
|
|
1898
|
+
background: var(--bp-ink);
|
|
1899
|
+
color: var(--bp-paper);
|
|
1900
|
+
}
|
|
1901
|
+
:where(.bp-preflight__bar) > :where(.bp-label) {
|
|
1902
|
+
margin: 0;
|
|
1903
|
+
color: var(--bp-paper);
|
|
1904
|
+
}
|
|
1905
|
+
:where(.bp-preflight__count) {
|
|
1906
|
+
margin-left: auto;
|
|
1907
|
+
font-family: var(--bp-mono);
|
|
1908
|
+
font-size: var(--bp-label-md);
|
|
1909
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
1910
|
+
text-transform: uppercase;
|
|
1911
|
+
color: color-mix(in oklch, var(--bp-paper) 78%, transparent);
|
|
1912
|
+
}
|
|
1913
|
+
:where(.bp-preflight__questions) {
|
|
1914
|
+
counter-reset: bp-preflight-q;
|
|
1915
|
+
}
|
|
1916
|
+
:where(.bp-preflight__q) {
|
|
1917
|
+
counter-increment: bp-preflight-q;
|
|
1918
|
+
padding: var(--bp-space-3) var(--bp-space-3) var(--bp-space-3) calc(var(--bp-space-6) + 4px);
|
|
1919
|
+
position: relative;
|
|
1920
|
+
border-bottom: 1px solid var(--bp-edge);
|
|
1921
|
+
}
|
|
1922
|
+
:where(.bp-preflight__q::before) {
|
|
1923
|
+
content: counter(bp-preflight-q, decimal-leading-zero);
|
|
1924
|
+
position: absolute;
|
|
1925
|
+
left: var(--bp-space-3);
|
|
1926
|
+
top: var(--bp-space-3);
|
|
1927
|
+
font-family: var(--bp-mono);
|
|
1928
|
+
font-size: var(--bp-label-lg);
|
|
1929
|
+
letter-spacing: 0.08em;
|
|
1930
|
+
color: var(--bp-ink-soft);
|
|
1931
|
+
}
|
|
1932
|
+
:where(.bp-preflight__q:has(input:checked)::before) {
|
|
1933
|
+
color: var(--bp-ink);
|
|
1934
|
+
}
|
|
1935
|
+
:where(.bp-preflight__prompt) {
|
|
1936
|
+
font-family: var(--bp-sans);
|
|
1937
|
+
font-weight: var(--bp-weight-strong);
|
|
1938
|
+
font-size: var(--bp-text-h4);
|
|
1939
|
+
margin: 0 0 var(--bp-space-1);
|
|
1940
|
+
display: flex;
|
|
1941
|
+
align-items: baseline;
|
|
1942
|
+
gap: var(--bp-space-2);
|
|
1943
|
+
}
|
|
1944
|
+
:where(.bp-preflight__kind) {
|
|
1945
|
+
font-family: var(--bp-mono);
|
|
1946
|
+
font-size: var(--bp-label-sm);
|
|
1947
|
+
letter-spacing: var(--bp-label-sm-ls);
|
|
1948
|
+
text-transform: uppercase;
|
|
1949
|
+
color: var(--bp-text-secondary);
|
|
1950
|
+
font-weight: var(--bp-weight-body);
|
|
1951
|
+
}
|
|
1952
|
+
:where(.bp-preflight__resolved) {
|
|
1953
|
+
display: none;
|
|
1954
|
+
margin-left: auto;
|
|
1955
|
+
}
|
|
1956
|
+
:where(.bp-preflight__q:has(input:checked)) :where(.bp-preflight__resolved) {
|
|
1957
|
+
display: inline-flex;
|
|
1958
|
+
}
|
|
1959
|
+
:where(.bp-preflight__chips) {
|
|
1960
|
+
display: flex;
|
|
1961
|
+
flex-wrap: wrap;
|
|
1962
|
+
gap: var(--bp-space-2);
|
|
1963
|
+
margin-top: var(--bp-space-2);
|
|
1964
|
+
}
|
|
1965
|
+
:where(.bp-preflight__chip) {
|
|
1966
|
+
display: inline-flex;
|
|
1967
|
+
align-items: center;
|
|
1968
|
+
gap: 7px;
|
|
1969
|
+
cursor: pointer;
|
|
1970
|
+
border: 1px solid var(--bp-ink-faint);
|
|
1971
|
+
border-radius: var(--bp-radius-pill);
|
|
1972
|
+
padding: 5px 12px;
|
|
1973
|
+
font-size: var(--bp-text-small);
|
|
1974
|
+
background: var(--bp-paper);
|
|
1975
|
+
}
|
|
1976
|
+
:where(.bp-preflight__chip:hover) {
|
|
1977
|
+
border-color: var(--bp-ink-line);
|
|
1978
|
+
}
|
|
1979
|
+
:where(.bp-preflight__chip) :where(input) {
|
|
1980
|
+
accent-color: var(--bp-ink);
|
|
1981
|
+
margin: 0;
|
|
1982
|
+
}
|
|
1983
|
+
:where(.bp-preflight__chip:has(input:checked)) {
|
|
1984
|
+
background: var(--bp-ink);
|
|
1985
|
+
color: var(--bp-paper);
|
|
1986
|
+
border-color: var(--bp-ink);
|
|
1987
|
+
}
|
|
1988
|
+
:where(.bp-preflight__gate) {
|
|
1989
|
+
margin: var(--bp-space-3);
|
|
1990
|
+
border: 1px dashed var(--bp-ink-line);
|
|
1991
|
+
border-radius: var(--bp-radius-6);
|
|
1992
|
+
padding: var(--bp-space-4);
|
|
1993
|
+
text-align: center;
|
|
1994
|
+
background-image: var(--bp-hatch);
|
|
1995
|
+
color: var(--bp-text-secondary);
|
|
1996
|
+
}
|
|
1997
|
+
:where(.bp-preflight__gate-ready) {
|
|
1998
|
+
display: none;
|
|
1999
|
+
}
|
|
2000
|
+
:where(.bp-preflight--ready) :where(.bp-preflight__gate) {
|
|
2001
|
+
background-image: none;
|
|
2002
|
+
border-style: solid;
|
|
2003
|
+
border-color: var(--bp-ink-line);
|
|
2004
|
+
}
|
|
2005
|
+
:where(.bp-preflight--ready) :where(.bp-preflight__gate-locked) {
|
|
2006
|
+
display: none;
|
|
2007
|
+
}
|
|
2008
|
+
:where(.bp-preflight--ready) :where(.bp-preflight__gate-ready) {
|
|
2009
|
+
display: block;
|
|
2010
|
+
}
|
|
2011
|
+
:where(.bp-preflight__draft) {
|
|
2012
|
+
display: inline-flex;
|
|
2013
|
+
align-items: center;
|
|
2014
|
+
gap: 8px;
|
|
2015
|
+
border: 1px solid var(--bp-ink);
|
|
2016
|
+
border-radius: var(--bp-radius-4);
|
|
2017
|
+
padding: 8px 16px;
|
|
2018
|
+
font-family: var(--bp-mono);
|
|
2019
|
+
font-size: var(--bp-label-md);
|
|
2020
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
2021
|
+
text-transform: uppercase;
|
|
2022
|
+
background: var(--bp-ink);
|
|
2023
|
+
color: var(--bp-paper);
|
|
2024
|
+
cursor: pointer;
|
|
2025
|
+
margin-top: var(--bp-space-2);
|
|
2026
|
+
}
|
|
2027
|
+
:where(.bp-preflight__footer) {
|
|
2028
|
+
display: flex;
|
|
2029
|
+
align-items: center;
|
|
2030
|
+
gap: var(--bp-space-3);
|
|
2031
|
+
margin-top: var(--bp-space-3);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/* ---- Choice record (static decided archive) ---------------------- */
|
|
2035
|
+
:where(.bp-choice-record) {
|
|
2036
|
+
margin: var(--bp-space-4) 0;
|
|
2037
|
+
}
|
|
2038
|
+
:where(.bp-choice-record__row) {
|
|
2039
|
+
border: 1px solid var(--bp-edge);
|
|
2040
|
+
border-radius: var(--bp-radius-4);
|
|
2041
|
+
padding: var(--bp-space-2) var(--bp-space-3);
|
|
2042
|
+
display: grid;
|
|
2043
|
+
grid-template-columns: minmax(8rem, 14rem) 1fr auto;
|
|
2044
|
+
gap: var(--bp-space-2) var(--bp-space-3);
|
|
2045
|
+
align-items: baseline;
|
|
2046
|
+
}
|
|
2047
|
+
:where(.bp-choice-record__row + .bp-choice-record__row) {
|
|
2048
|
+
margin-top: var(--bp-space-2);
|
|
2049
|
+
}
|
|
2050
|
+
:where(.bp-choice-record__row) :where(dt) {
|
|
2051
|
+
font-family: var(--bp-mono);
|
|
2052
|
+
font-size: var(--bp-label-md);
|
|
2053
|
+
letter-spacing: var(--bp-label-md-ls);
|
|
2054
|
+
text-transform: uppercase;
|
|
2055
|
+
color: var(--bp-text-secondary);
|
|
2056
|
+
margin: 0;
|
|
2057
|
+
}
|
|
2058
|
+
:where(.bp-choice-record__row) :where(dd) {
|
|
2059
|
+
margin: 0;
|
|
2060
|
+
font-weight: var(--bp-weight-medium);
|
|
2061
|
+
}
|
|
2062
|
+
@media (max-width: 700px) {
|
|
2063
|
+
:where(.bp-choice-record__row) {
|
|
2064
|
+
grid-template-columns: 1fr;
|
|
2065
|
+
gap: 2px;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1551
2069
|
/* ---- Data-table wrap: let wide tables break past the prose measure */
|
|
1552
2070
|
:where(.bp-table-wrap) {
|
|
1553
2071
|
max-width: 100%;
|
package/dist/blueprint.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// Optional behavior for TOC current state and reading progress.
|
|
2
|
+
import { registerBlueprintChoiceElements } from './blueprint-choices.js'
|
|
3
|
+
|
|
4
|
+
registerBlueprintChoiceElements()
|
|
5
|
+
|
|
2
6
|
export function initializeBlueprintRuntime(doc = document, win = window) {
|
|
3
7
|
const root = doc.documentElement
|
|
4
8
|
if (root.dataset.blueprintRuntime === 'ready') return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obvi/blueprint",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "A classless-first CSS design system for beautiful technical blueprint documents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"exports": {
|
|
24
24
|
".": "./dist/blueprint.css",
|
|
25
25
|
"./blueprint.js": "./dist/blueprint.js",
|
|
26
|
+
"./blueprint-choices.js": "./dist/blueprint-choices.js",
|
|
26
27
|
"./blueprint.css": "./dist/blueprint.css",
|
|
27
28
|
"./styles.css": "./dist/blueprint.css",
|
|
28
29
|
"./code-highlighting.css": "./dist/code-highlighting/blueprint-code.css",
|