@obvi/blueprint 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.dataset.bpRendered) return
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
- 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
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
- 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) {
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
- if (layout === 'stack') {
306
- const stack = el(doc, 'div', 'bp-choice__stack')
307
- for (const opt of options) {
308
- const card = el(doc, 'label', 'bp-choice__card')
309
- const input = doc.createElement('input')
310
- input.type = 'radio'
311
- input.name = pickName
312
- input.value = opt.value
313
- input.className = 'bp-choice__pick'
314
- card.append(input)
315
-
316
- const head = el(doc, 'div', 'bp-choice__card-head')
317
- if (opt.optionLabel) head.append(label(doc, opt.optionLabel))
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
- if (layout === 'gallery') {
344
- const gallery = el(doc, 'div', 'bp-choice__gallery')
345
- for (const opt of options) {
346
- const mock = el(doc, 'label', 'bp-choice__mock')
347
- const input = doc.createElement('input')
348
- input.type = 'radio'
349
- input.name = pickName
350
- input.value = opt.value
351
- input.className = 'bp-choice__pick'
352
- mock.append(input)
353
-
354
- const frame = el(doc, 'div', 'bp-choice__mock-frame')
355
- const frameInner = frameContent(opt.el)
356
- if (frameInner) frame.append(frameInner)
357
- mock.append(frame)
358
-
359
- const cap = el(doc, 'div', 'bp-choice__mock-cap')
360
- const h4 = el(doc, 'h4')
361
- h4.textContent = opt.caption
362
- cap.append(h4)
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
- form.append(
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.2.0",
4
- "source": "github:FlatFilers/blueprint-framework@v1.2.0",
3
+ "frameworkVersion": "1.3.0",
4
+ "source": "github:FlatFilers/blueprint-framework@v1.3.0",
5
5
  "assets": {
6
6
  "vendor/blueprint/blueprint.css": {
7
- "sha256": "34b0bf5882052a64d60bcbcf3663777d4165012cda555f6ec3f80a7711c73103"
7
+ "sha256": "7ba1864176042524b1bacdeea0d1af2445fe55860c8bf0b857bca878c45c1cbf"
8
8
  },
9
9
  "vendor/blueprint/blueprint.js": {
10
- "sha256": "9ec19cbbc24abd6da5331d7ddbace2e8b8c65e407f4734f1d0b9bcbb7a9a7f3b"
10
+ "sha256": "55f1b677d5302bddba2edb840d26bbe409252fce20345a3d4b6368fd04526954"
11
11
  }
12
12
  }
13
13
  }
@@ -4286,10 +4286,16 @@
4286
4286
  margin: 0;
4287
4287
  }
4288
4288
  /* Expanded mock floats fullscreen (position: fixed): collapse the host so
4289
- no empty dotted mat is left behind in the document flow. */
4289
+ no empty dotted mat is left behind in the document flow, and clear any
4290
+ host clipping / board paint so browsers that clip fixed descendants
4291
+ through overflow-hidden ancestors still show the overlay fullscreen. */
4290
4292
  :where(bp-mock:has(.bp-mock.is-expanded)) {
4291
4293
  margin: 0;
4292
4294
  padding: 0;
4295
+ overflow: visible;
4296
+ border-radius: 0;
4297
+ background-color: oklch(1 0 0 / 0);
4298
+ background-image: none;
4293
4299
  }
4294
4300
 
4295
4301
  /* Single-surface specimens read as ONE continuous paper card on the mat;