@12ui/design 0.2.51 → 0.2.52

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.
Files changed (74) hide show
  1. package/SKILL.md +1 -1
  2. package/dist/branch-client.d.ts +5 -0
  3. package/dist/branch-client.d.ts.map +1 -1
  4. package/dist/branch-client.js.map +1 -1
  5. package/dist/branch-completion.d.ts.map +1 -1
  6. package/dist/branch-completion.js +7 -0
  7. package/dist/branch-completion.js.map +1 -1
  8. package/dist/branch-identity.d.ts +25 -0
  9. package/dist/branch-identity.d.ts.map +1 -1
  10. package/dist/branch-identity.js +28 -15
  11. package/dist/branch-identity.js.map +1 -1
  12. package/dist/branch-page-conversion.d.ts.map +1 -1
  13. package/dist/branch-page-conversion.js +49 -4
  14. package/dist/branch-page-conversion.js.map +1 -1
  15. package/dist/branch-plan.d.ts +4 -1
  16. package/dist/branch-plan.d.ts.map +1 -1
  17. package/dist/branch-plan.js +7 -0
  18. package/dist/branch-plan.js.map +1 -1
  19. package/dist/branch-run-record.d.ts +4 -0
  20. package/dist/branch-run-record.d.ts.map +1 -1
  21. package/dist/branch-run-record.js.map +1 -1
  22. package/dist/package-client.d.ts +11 -0
  23. package/dist/package-client.d.ts.map +1 -1
  24. package/dist/package-client.js.map +1 -1
  25. package/dist/package-submission.d.ts +13 -2
  26. package/dist/package-submission.d.ts.map +1 -1
  27. package/dist/package-submission.js +34 -1
  28. package/dist/package-submission.js.map +1 -1
  29. package/dist/prototype-retention.d.ts +10 -0
  30. package/dist/prototype-retention.d.ts.map +1 -0
  31. package/dist/prototype-retention.js +14 -0
  32. package/dist/prototype-retention.js.map +1 -0
  33. package/dist/prototype-revision.d.ts +1 -1
  34. package/dist/prototype-revision.js +1 -1
  35. package/dist/prototype-runner.d.ts.map +1 -1
  36. package/dist/prototype-runner.js +39 -15
  37. package/dist/prototype-runner.js.map +1 -1
  38. package/dist/review-server.d.ts.map +1 -1
  39. package/dist/review-server.js +10 -1
  40. package/dist/review-server.js.map +1 -1
  41. package/open-design.json +2 -2
  42. package/package.json +1 -1
  43. package/prototype-kit/build.mjs +54 -9
  44. package/prototype-kit/lib/action-target.mjs +34 -8
  45. package/prototype-kit/lib/runtime-check.mjs +31 -12
  46. package/prototype-kit/polish/ground-probe.mjs +4 -1
  47. package/prototype-kit/polish/measure.mjs +6 -6
  48. package/prototype-kit/polish/rules/section-breathing.mjs +461 -237
  49. package/prototype-kit/polish/tests/breathing-check.mjs +75 -27
  50. package/prototype-kit/polish/tests/fixtures/section-breathing/diagram-silent.html +20 -0
  51. package/prototype-kit/polish/tests/fixtures/section-breathing/fire.html +2 -2
  52. package/prototype-kit/polish/tests/fixtures/section-breathing/floor.html +3 -3
  53. package/prototype-kit/polish/tests/fixtures/section-breathing/no-scale-silent.html +18 -0
  54. package/prototype-kit/polish/tests/fixtures/section-breathing/rhythm-nested.html +21 -0
  55. package/prototype-kit/polish/tests/fixtures/section-breathing/semantic-fire.html +10 -4
  56. package/prototype-kit/polish/tests/fixtures/section-breathing/semantic-silent.html +2 -2
  57. package/prototype-kit/polish/tests/fixtures/section-breathing/silent.html +2 -2
  58. package/prototype-kit/polish/tests/fixtures/section-breathing/tight.html +4 -4
  59. package/prototype-kit/runtime/proto.js +52 -2
  60. package/prototype-kit/tests/adversarial-runtime.mjs +179 -4
  61. package/prototype-kit/tests/fixtures/blocking-runtime/broken-interaction.html +1 -0
  62. package/prototype-kit/tests/fixtures/interaction-runtime/PROVENANCE.md +5 -0
  63. package/prototype-kit/tests/fixtures/interaction-runtime/inert-field.html +23 -0
  64. package/prototype-kit/tests/fixtures/interaction-runtime/pointer-cascade.html +19 -0
  65. package/prototype-kit/tests/fixtures/multiple-trigger-runtime/probe-all.html +14 -0
  66. package/prototype-kit/tests/fixtures/nested-inert/branch.plan.json +5 -0
  67. package/prototype-kit/tests/fixtures/nested-inert/pages/p0.html +17 -0
  68. package/prototype-kit/tests/offline-check.mjs +6 -1
  69. package/prototype-kit/tests/rules-check.mjs +28 -9
  70. package/prototype-kit/verify-runtime.mjs +10 -5
  71. package/prototype-kit/polish/tests/fixtures/section-breathing/attachment-fire.html +0 -5
  72. package/prototype-kit/polish/tests/fixtures/section-breathing/attachment-silent.html +0 -5
  73. package/prototype-kit/polish/tests/fixtures/section-breathing/shadow-fire.html +0 -5
  74. package/prototype-kit/polish/tests/fixtures/section-breathing/shadow-silent.html +0 -5
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // section-breathing: floor + background-integrity pairs.
2
+ // section-breathing: declared floor + background-integrity pairs.
3
3
  // floor three solid sections, gaps ~42 / ~102 → target ceil8(median+32) = 104: seam 1 gets +62, seam 2 (+2) is DECLINED
4
4
  // probe on the fire fixture the pixel probe accepts the accepted seam (9/9 probes in the owner's colour, document
5
5
  // grew by exactly the addition); with a deliberately wrong owner colour it must fail with 'coverage'
6
+ // rhythm a semantic seam's amount is the page's own scale T = min(largest, 2 x median) structural gap, measured
7
+ // from the page's real air line; nested seams ride the enclosing painter; drawings are never opened
6
8
  import path from 'node:path';
9
+ import fs from 'node:fs';
10
+ import crypto from 'node:crypto';
7
11
  import { chromium } from '@playwright/test';
8
12
  import * as rule from '../rules/section-breathing.mjs';
9
13
  import { applyOverridesScript, overridesToCss, mergeOverrides } from '../overrides.mjs';
@@ -35,43 +39,87 @@ check('probe accepts a solid seam (9 probes, height +32)', good.ok && good.probe
35
39
  const bad = await probeSeams(page, seamMeta(fire).map((s) => ({ ...s, color: 'rgb(0, 90, 200)' })), base);
36
40
  check('probe rejects a seam whose pixels are not the declared ground', !bad.ok && bad.failures.every((f) => f.kind === 'coverage' || f.kind === 'floor-exposure') && bad.failures.length === 9, JSON.stringify({ ok: bad.ok, n: bad.failures.length, kinds: [...new Set(bad.failures.map((f) => f.kind))] }));
37
41
 
38
- // attachment pair: only a declared decoration touching the seam travels with the following section
39
- await page.goto('file://' + path.join(here, 'fixtures/section-breathing/attachment-fire.html'), { waitUntil: 'load' });
40
- const attachmentBase = await page.evaluate(() => document.documentElement.scrollHeight);
41
- const attachmentFire = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
42
- const attachmentMove = attachmentFire.find((o) => o.id === 'viewport-1-a-fruit');
43
- await page.evaluate(applyOverridesScript(), overridesToCss(mergeOverrides(attachmentFire)));
44
- const attachmentProbe = await probeSeams(page, seamMeta(attachmentFire), attachmentBase);
45
- check('boundary-hugging decoration travels and keeps its seam attachment', attachmentMove?.dy === 32 && seamMeta(attachmentFire)[0]?.attachments?.[0]?.id === 'viewport-1-a-fruit' && attachmentProbe.ok, JSON.stringify({ move: attachmentMove, ground: attachmentProbe }));
46
- await page.goto('file://' + path.join(here, 'fixtures/section-breathing/attachment-silent.html'), { waitUntil: 'load' });
47
- const attachmentSilent = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
48
- check('ordinary boundary image is not reclassified as decoration', !attachmentSilent.some((o) => o.id === 'viewport-1-a-product-image'), JSON.stringify(attachmentSilent.filter((o) => o.id === 'viewport-1-a-product-image')));
49
-
50
- // semantic pair: direct declared FAQ/footer boundaries breathe; a dense dashboard boundary stays silent
42
+ // semantic pair: model-declared FAQ/footer boundaries breathe; a model-declared
43
+ // content-bearing dashboard boundary refuses and the fixture bytes stay exact.
51
44
  await page.goto('file://' + path.join(here, 'fixtures/section-breathing/semantic-fire.html'), { waitUntil: 'load' });
52
45
  const semanticBase = await page.evaluate(() => document.documentElement.scrollHeight);
53
46
  const semantic = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
47
+ const semanticCensus = await page.evaluate(rule.browserFn(), { metrics: {}, params: { ...rule.params, census: true } });
54
48
  const semanticMerged = mergeOverrides(semantic);
55
49
  await page.evaluate(applyOverridesScript(), overridesToCss(semanticMerged));
56
50
  const semanticProbe = await probeSeams(page, seamMeta(semantic), semanticBase);
57
51
  const faq = semanticMerged.find((o) => o.id === 'viewport-1-a-faq');
58
52
  const footer = semanticMerged.find((o) => o.id === 'viewport-1-a-footer');
59
53
  const section = semanticMerged.find((o) => o.id === 'viewport-1-a');
60
- check('declared FAQ and footer boundaries each gain 32 px', faq?.dy === 32 && footer?.dy === 64 && section?.h === 1088 && faq?.meta?.seam?.beforePeers?.includes('viewport-1-a-comparison-aside') && semanticProbe.ok, JSON.stringify({ faq, footer, section, ground: semanticProbe }));
61
- await page.goto('file://' + path.join(here, 'fixtures/section-breathing/semantic-silent.html'), { waitUntil: 'load' });
54
+ // page scale: structural gaps 40 (comparison) and 24 (faq) → median 32, largest 40, T = min(40, 64) = 40.
55
+ // Both seams sit at 16 px, so each is brought up to 40 (+24) and never past it.
56
+ check('declared FAQ and footer boundaries each rise to the 40 px page scale',
57
+ faq?.dy === 24 && footer?.dy === 48 && section?.h === 1072
58
+ && semanticCensus.every((row) => row.pageScale?.target === 40 && row.gap === 16 && row.addition === 24)
59
+ && faq?.meta?.seam?.beforePeers?.includes('viewport-1-a-comparison-aside') && semanticProbe.ok,
60
+ JSON.stringify({ faq, footer, section, scale: semanticCensus.map((r) => r.pageScale), ground: semanticProbe }));
61
+ const silentFile = path.join(here, 'fixtures/section-breathing/semantic-silent.html');
62
+ const silentBefore = fs.readFileSync(silentFile);
63
+ await page.goto('file://' + silentFile, { waitUntil: 'load' });
62
64
  const semanticSilent = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
63
- check('dense dashboard footer boundary stays tight', semanticSilent.length === 0, JSON.stringify(semanticSilent));
65
+ const silentCensus = await page.evaluate(rule.browserFn(), { metrics: {}, params: { ...rule.params, census: true } });
66
+ const silentAfter = fs.readFileSync(silentFile);
67
+ const sha = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
68
+ check('declared content-bearing boundary stays byte-identical', semanticSilent.length === 0
69
+ && silentCensus[0]?.declined === 'declared-not-openable'
70
+ && sha(silentBefore) === sha(silentAfter), JSON.stringify({ census: silentCensus, sha256: sha(silentAfter) }));
71
+
72
+ // rhythm model: a nested seam, measured from the page's real air line, painted by the enclosing panel.
73
+ // Pairwise the declared intro→details pair reads 200 px of air and would propose nothing; the page
74
+ // actually shows 40 px because the floating note card outlives intro inside the seam.
75
+ await page.goto('file://' + path.join(here, 'fixtures/section-breathing/rhythm-nested.html'), { waitUntil: 'load' });
76
+ const nestedBase = await page.evaluate(() => document.documentElement.scrollHeight);
77
+ const nestedCensus = await page.evaluate(rule.browserFn(), { metrics: {}, params: { ...rule.params, census: true } });
78
+ const nestedRow = nestedCensus[0];
79
+ const nested = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
80
+ const nestedMerged = mergeOverrides(nested);
81
+ const pairwiseGap = await page.evaluate(() => (
82
+ document.getElementById('viewport-1-a-panel-details').getBoundingClientRect().top
83
+ - document.getElementById('viewport-1-a-panel-intro').getBoundingClientRect().bottom));
84
+ check('nested seam measures the real air line, not the declared pair',
85
+ nestedRow?.gap === 40 && pairwiseGap === 200 && nestedRow?.airAnchor === 'viewport-1-a-panel-note'
86
+ && nestedRow?.before === 'viewport-1-a-panel-intro' && nestedRow?.group === 'viewport-1-a-panel',
87
+ JSON.stringify({ realAir: nestedRow?.gap, pairwiseGap, anchor: nestedRow?.airAnchor }));
88
+ check('nested seam rises to the page scale and grows every container it sits in',
89
+ nestedRow?.addition === 40 && nestedRow?.pageScale?.target === 80 && nestedRow?.declined === null
90
+ && nestedRow?.ground?.execution === 'group-shift'
91
+ && nestedMerged.find((o) => o.id === 'viewport-1-a-panel-details')?.dy === 40
92
+ && nestedMerged.find((o) => o.id === 'viewport-1-a-panel')?.h === 840
93
+ && nestedMerged.find((o) => o.id === 'viewport-1-a')?.h === 1064,
94
+ JSON.stringify({ addition: nestedRow?.addition, scale: nestedRow?.pageScale, overrides: nestedMerged.map((o) => ({ id: o.id, dy: o.dy, h: o.h })) }));
95
+ await page.evaluate(applyOverridesScript(), overridesToCss(nestedMerged));
96
+ const nestedProbe = await probeSeams(page, seamMeta(nested), nestedBase);
97
+ check('the enclosing panel really paints the nested air (9 real-pixel probes)',
98
+ nestedProbe.ok && nestedProbe.probes === 9 && nestedProbe.documentHeight === nestedBase + 40,
99
+ JSON.stringify({ ok: nestedProbe.ok, probes: nestedProbe.probes, h: [nestedBase, nestedProbe.documentHeight], failures: nestedProbe.failures.slice(0, 2) }));
100
+
101
+ // do-not-touch: same shape, but the panel's children share no common edge and vary wildly in width,
102
+ // so vertical position is carrying meaning. This is the metro-diagram geometry; it is never opened.
103
+ const diagramFile = path.join(here, 'fixtures/section-breathing/diagram-silent.html');
104
+ const diagramBefore = fs.readFileSync(diagramFile);
105
+ await page.goto('file://' + diagramFile, { waitUntil: 'load' });
106
+ const diagram = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
107
+ const diagramCensus = await page.evaluate(rule.browserFn(), { metrics: {}, params: { ...rule.params, census: true } });
108
+ check('a declared seam inside a drawing is refused and the fixture stays byte-identical',
109
+ diagram.length === 0 && diagramCensus[0]?.declined === 'declared-inside-diagram-group'
110
+ && sha(diagramBefore) === sha(fs.readFileSync(diagramFile)),
111
+ JSON.stringify({ census: diagramCensus, sha256: sha(diagramBefore) }));
64
112
 
65
- // shadow-gradient pair: the measured neutral monotone fade is tolerated; a hard coloured band still fails
66
- for (const [fixture, expected] of [['shadow-fire.html', true], ['shadow-silent.html', false]]) {
67
- await page.goto('file://' + path.join(here, 'fixtures/section-breathing', fixture), { waitUntil: 'load' });
68
- const shadowBase = await page.evaluate(() => document.documentElement.scrollHeight);
69
- const shadow = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
70
- await page.evaluate(applyOverridesScript(), overridesToCss(mergeOverrides(shadow)));
71
- const verdict = await probeSeams(page, seamMeta(shadow), shadowBase);
72
- const ok = expected ? verdict.ok && verdict.toleratedShadowGradients > 0 : !verdict.ok && verdict.failures.some((f) => f.kind === 'coverage');
73
- check(expected ? 'neutral monotone shadow fade is tolerated' : 'hard coloured seam band stays rejected', ok, JSON.stringify(verdict));
74
- }
113
+ // stays silent: a page with no structural gap has no scale, so the model proposes nothing rather
114
+ // than falling back to a constant.
115
+ await page.goto('file://' + path.join(here, 'fixtures/section-breathing/no-scale-silent.html'), { waitUntil: 'load' });
116
+ const noScale = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
117
+ const noScaleCensus = await page.evaluate(rule.browserFn(), { metrics: {}, params: { ...rule.params, census: true } });
118
+ check('a page with no structural gap proposes nothing',
119
+ noScale.length === 0 && noScaleCensus.length === 2
120
+ && noScaleCensus.every((row) => row.addition === 0 && row.pageScale === null
121
+ && row.declined === 'page has no structural gap to set a scale from'),
122
+ JSON.stringify(noScaleCensus.map((r) => ({ after: r.after, addition: r.addition, declined: r.declined }))));
75
123
  await browser.close();
76
124
  if (failures) { console.log(`${failures} failure(s)`); process.exit(1); }
77
125
  console.log('section-breathing pairs green');
@@ -0,0 +1,20 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1024px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <!--
3
+ Do-not-touch: a drawing, not a stack.
4
+ Same shape as rhythm-nested.html — a declared seam between two children of a painted panel — but
5
+ the panel's children share no common left, right or centre edge and their widths vary wildly.
6
+ Vertical position carries meaning here (this is the geometry of the metro-line diagram on
7
+ r4-b13-kinetic--network-map), so the seam is refused as declared-inside-diagram-group.
8
+ -->
9
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1024px"><div id="page-flow">
10
+ <section id="viewport-1-a" style="top:0;height:1024px">
11
+ <div id="viewport-1-a-page" data-ly-section-ground-owner="diagram-ground" style="position:absolute;inset:0;background:#ffffff"></div>
12
+ <div id="viewport-1-a-map" style="position:absolute;left:120px;top:120px;width:1296px;height:800px;background:#eef2f6">
13
+ <div id="viewport-1-a-map-westgate" style="position:absolute;left:0;top:0;width:200px;height:60px"><p class="text" id="viewport-1-a-map-westgate-name" style="left:0;top:0">Westgate</p><p class="text" id="viewport-1-a-map-westgate-time" style="left:0;top:30px">4 min</p></div>
14
+ <div id="viewport-1-a-map-harbourfront" style="position:absolute;left:320px;top:120px;width:60px;height:60px"><p class="text" id="viewport-1-a-map-harbourfront-name" style="left:0;top:0">Harbour</p><p class="text" id="viewport-1-a-map-harbourfront-time" style="left:0;top:30px">7</p></div>
15
+ <div id="viewport-1-a-map-riverside" style="position:absolute;left:140px;top:240px;width:480px;height:60px"><p class="text" id="viewport-1-a-map-riverside-name" style="left:0;top:0">Riverside Interchange</p><p class="text" id="viewport-1-a-map-riverside-time" style="left:0;top:30px">11 min</p></div>
16
+ <div id="viewport-1-a-map-oakwood" style="position:absolute;left:600px;top:360px;width:110px;height:60px"><p class="text" id="viewport-1-a-map-oakwood-name" style="left:0;top:0">Oakwood</p><p class="text" id="viewport-1-a-map-oakwood-time" style="left:0;top:30px">15</p></div>
17
+ <div id="viewport-1-a-map-hillcrest" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a-map-oakwood" data-ly-section-owner="diagram-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="terminus" style="position:absolute;left:260px;top:480px;width:300px;height:60px"><p class="text" id="viewport-1-a-map-hillcrest-name" style="left:0;top:0">Hillcrest</p><p class="text" id="viewport-1-a-map-hillcrest-time" style="left:0;top:30px">22 min</p></div>
18
+ </div>
19
+ </section>
20
+ </div></div></div>
@@ -1,6 +1,6 @@
1
1
  <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
- <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
2
+ <div class="generated-screen" data-ly-section-ground-owner="page-ground" style="background:#fbf6ee"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
3
3
  <!-- solid → solid: the hero's own cream section is the full-band ground; ink gap 24 px → one seam → +32 -->
4
4
  <section id="viewport-1-a" style="top:0;height:700px;background:#fbf6ee"><h1 id="viewport-1-a-heading" class="text" style="left:80px;top:200px;font-size:48px">Give your time.</h1><p id="viewport-1-a-text" class="text" style="left:80px;top:660px;font-size:16px">Join neighbors helping pack food.</p></section>
5
- <section id="viewport-2-a" style="top:700px;height:700px;background:#fbf6ee"><p id="viewport-2-a-eyebrow" class="text" style="left:80px;top:20px;font-size:12px">UPCOMING OPPORTUNITIES</p><h2 id="viewport-2-a-heading" class="text" style="left:80px;top:60px;font-size:32px">This week</h2></section>
5
+ <section id="viewport-2-a" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a" data-ly-section-owner="page-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="page" style="top:700px;height:700px;background:#fbf6ee"><p id="viewport-2-a-eyebrow" class="text" style="left:80px;top:20px;font-size:12px">UPCOMING OPPORTUNITIES</p><h2 id="viewport-2-a-heading" class="text" style="left:80px;top:60px;font-size:32px">This week</h2></section>
6
6
  </div></div></div>
@@ -1,7 +1,7 @@
1
1
  <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
- <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:2100px"><div id="page-flow">
2
+ <div class="generated-screen" data-ly-section-ground-owner="page-ground" style="background:#fff"><div class="ly-anchor-canvas" style="position:relative;height:2100px"><div id="page-flow">
3
3
  <!-- three solid sections, ink gaps 40 / 100: median 70 → target ceil8(102)=104 → additions 64 (cap) and 4 (declined: below the floor) -->
4
4
  <section id="viewport-1-a" style="top:0;height:700px;background:#fff"><h1 id="viewport-1-a-heading" class="text" style="left:80px;top:200px;font-size:48px">One</h1><p id="viewport-1-a-text" class="text" style="left:80px;top:660px;font-size:16px">end of one</p></section>
5
- <section id="viewport-2-a" style="top:700px;height:700px;background:#fff"><p id="viewport-2-a-top" class="text" style="left:80px;top:20px;font-size:16px">start of two</p><p id="viewport-2-a-text" class="text" style="left:80px;top:600px;font-size:16px">end of two</p></section>
6
- <section id="viewport-3-a" style="top:1400px;height:700px;background:#fff"><p id="viewport-3-a-top" class="text" style="left:80px;top:20px;font-size:16px">start of three</p></section>
5
+ <section id="viewport-2-a" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a" data-ly-section-owner="page-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="page" style="top:700px;height:700px;background:#fff"><p id="viewport-2-a-top" class="text" style="left:80px;top:20px;font-size:16px">start of two</p><p id="viewport-2-a-text" class="text" style="left:80px;top:600px;font-size:16px">end of two</p></section>
6
+ <section id="viewport-3-a" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-2-a" data-ly-section-owner="page-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="page" style="top:1400px;height:700px;background:#fff"><p id="viewport-3-a-top" class="text" style="left:80px;top:20px;font-size:16px">start of three</p></section>
7
7
  </div></div></div>
@@ -0,0 +1,18 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1024px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <!--
3
+ Stays silent: a page with nothing to measure a scale from.
4
+ Both declarations are valid and openable, but every group carries a single ink leaf, so the page
5
+ has no structural gap anywhere and no page scale exists. The rhythm model can only redistribute
6
+ air a page already contains somewhere; with no scale it proposes nothing rather than inventing a
7
+ constant. (This is the pre-rhythm semantic-fire fixture, kept for exactly that property.)
8
+ -->
9
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1024px"><div id="page-flow">
10
+ <section id="viewport-1-a" style="top:0;height:1024px">
11
+ <div id="viewport-1-a-page" data-ly-section-ground-owner="semantic-ground" style="position:absolute;inset:0;background:#fefefe"></div>
12
+ <div id="viewport-1-a-comparison" style="position:absolute;left:120px;top:200px;width:1296px;height:400px;border:1px solid #ddd"><p class="text" id="viewport-1-a-comparison-text" style="left:20px;top:20px">Comparison</p></div>
13
+ <div id="viewport-1-a-comparison-aside" style="position:absolute;left:816px;top:240px;width:600px;height:320px"></div>
14
+ <div style="position:absolute;left:0;top:600px;width:1536px;height:16px;background:#eee8e2"></div>
15
+ <div id="viewport-1-a-faq" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a-comparison" data-ly-section-owner="semantic-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="questions" style="position:absolute;left:120px;top:616px;width:1296px;height:120px"><p class="text" id="viewport-1-a-faq-text" style="left:0;top:0">Common purchase questions</p></div>
16
+ <footer id="viewport-1-a-footer" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a-faq" data-ly-section-owner="semantic-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="footer" style="position:absolute;left:0;top:752px;width:1536px;height:272px;background:#f8f8f8"><p class="text" id="viewport-1-a-footer-text" style="left:120px;top:40px">Footer</p></footer>
17
+ </section>
18
+ </div></div></div>
@@ -0,0 +1,21 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1024px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <!--
3
+ Nested seam + real air line.
4
+ The declared pair is intro -> details, two children of a panel that is NOT a direct child of the
5
+ section. The panel's own solid ground paints the cut, so no new ownership proof is needed.
6
+ Pairwise, intro->details reads 200 px of air; the page actually shows 40 px, because the floating
7
+ note card outlives intro inside the seam. Page scale: panel gaps 80 / 40 / 40 -> median 40,
8
+ largest 80, T = min(80, 2*40) = 80. Addition = 80 - 40 = 40. Measured pairwise it would be
9
+ max(0, 80 - 200) = 0, so this fixture fires only when the air line is read correctly.
10
+ -->
11
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1024px"><div id="page-flow">
12
+ <section id="viewport-1-a" style="top:0;height:1024px">
13
+ <div id="viewport-1-a-page" data-ly-section-ground-owner="nested-ground" style="position:absolute;inset:0;background:#ffffff"></div>
14
+ <div id="viewport-1-a-panel" style="position:absolute;left:120px;top:120px;width:1296px;height:800px;background:#f2efe9">
15
+ <div id="viewport-1-a-panel-heading" style="position:absolute;left:20px;top:0;width:1256px;height:40px"><p class="text" id="viewport-1-a-panel-heading-title" style="left:0;top:0">Release notes</p><p class="text" id="viewport-1-a-panel-heading-date" style="left:600px;top:0">March</p></div>
16
+ <div id="viewport-1-a-panel-intro" style="position:absolute;left:20px;top:120px;width:1256px;height:120px"><p class="text" id="viewport-1-a-panel-intro-lead" style="left:0;top:0">What changed this month</p><p class="text" id="viewport-1-a-panel-intro-body" style="left:0;top:60px">Faster exports and a calmer inbox.</p></div>
17
+ <div id="viewport-1-a-panel-note" style="position:absolute;left:900px;top:280px;width:300px;height:120px;background:#e6e0d4"><p class="text" id="viewport-1-a-panel-note-label" style="left:16px;top:16px">Note</p><p class="text" id="viewport-1-a-panel-note-body" style="left:16px;top:56px">Rolling out slowly.</p></div>
18
+ <div id="viewport-1-a-panel-details" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a-panel-intro" data-ly-section-owner="nested-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="details" style="position:absolute;left:20px;top:440px;width:1256px;height:320px"><p class="text" id="viewport-1-a-panel-details-title" style="left:0;top:0">Every change</p><p class="text" id="viewport-1-a-panel-details-body" style="left:0;top:60px">Twelve fixes and two new panels.</p></div>
19
+ </div>
20
+ </section>
21
+ </div></div></div>
@@ -1,11 +1,17 @@
1
1
  <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1024px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
2
  <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1024px"><div id="page-flow">
3
3
  <section id="viewport-1-a" style="top:0;height:1024px">
4
- <div id="viewport-1-a-page" style="position:absolute;inset:0;background:#fefefe"></div>
5
- <div id="viewport-1-a-comparison" style="position:absolute;left:120px;top:200px;width:1296px;height:400px;border:1px solid #ddd"><p class="text" id="viewport-1-a-comparison-text" style="left:20px;top:20px">Comparison</p></div>
4
+ <div id="viewport-1-a-page" data-ly-section-ground-owner="semantic-ground" style="position:absolute;inset:0;background:#fefefe"></div>
5
+ <div id="viewport-1-a-comparison" style="position:absolute;left:120px;top:200px;width:1296px;height:400px;border:1px solid #ddd">
6
+ <div id="viewport-1-a-comparison-plans" style="position:absolute;left:20px;top:20px;width:1256px;height:100px"><p class="text" id="viewport-1-a-comparison-plans-title" style="left:0;top:0">Compare plans</p><p class="text" id="viewport-1-a-comparison-plans-note" style="left:0;top:40px">Billed annually</p></div>
7
+ <div id="viewport-1-a-comparison-table" style="position:absolute;left:20px;top:160px;width:1256px;height:200px"><p class="text" id="viewport-1-a-comparison-table-head" style="left:0;top:0">Included</p><p class="text" id="viewport-1-a-comparison-table-body" style="left:0;top:40px">Unlimited seats</p></div>
8
+ </div>
6
9
  <div id="viewport-1-a-comparison-aside" style="position:absolute;left:816px;top:240px;width:600px;height:320px"></div>
7
10
  <div style="position:absolute;left:0;top:600px;width:1536px;height:16px;background:#eee8e2"></div>
8
- <div id="viewport-1-a-faq" style="position:absolute;left:120px;top:616px;width:1296px;height:120px"><p class="text" id="viewport-1-a-faq-text" style="left:0;top:0">Common purchase questions</p></div>
9
- <footer id="viewport-1-a-footer" style="position:absolute;left:0;top:752px;width:1536px;height:272px;background:#f8f8f8"><p class="text" id="viewport-1-a-footer-text" style="left:120px;top:40px">Footer</p></footer>
11
+ <div id="viewport-1-a-faq" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a-comparison" data-ly-section-owner="semantic-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="questions" style="position:absolute;left:120px;top:616px;width:1296px;height:120px">
12
+ <div id="viewport-1-a-faq-lead" style="position:absolute;left:0;top:0;width:1296px;height:40px"><p class="text" id="viewport-1-a-faq-lead-title" style="left:0;top:0">Common purchase questions</p><p class="text" id="viewport-1-a-faq-lead-note" style="left:600px;top:0">Updated weekly</p></div>
13
+ <div id="viewport-1-a-faq-items" style="position:absolute;left:0;top:64px;width:1296px;height:56px"><p class="text" id="viewport-1-a-faq-items-one" style="left:0;top:0">Can I change plans?</p><p class="text" id="viewport-1-a-faq-items-two" style="left:0;top:28px">Do you offer refunds?</p></div>
14
+ </div>
15
+ <footer id="viewport-1-a-footer" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a-faq" data-ly-section-owner="semantic-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="footer" style="position:absolute;left:0;top:752px;width:1536px;height:272px;background:#f8f8f8"><p class="text" id="viewport-1-a-footer-text" style="left:120px;top:40px">Footer</p></footer>
10
16
  </section>
11
17
  </div></div></div>
@@ -1,8 +1,8 @@
1
1
  <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1024px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
2
  <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1024px"><div id="page-flow">
3
3
  <section id="viewport-1-a" style="top:0;height:1024px">
4
- <div id="viewport-1-a-page" style="position:absolute;inset:0;background:#081018"></div>
4
+ <div id="viewport-1-a-page" data-ly-section-ground-owner="content-ground" style="position:absolute;inset:0;background:#081018"></div>
5
5
  <div id="viewport-1-a-dashboard" style="position:absolute;left:24px;top:100px;width:1488px;height:700px;border:1px solid #334"><p class="text" id="viewport-1-a-dashboard-text" style="left:20px;top:20px;color:white">Dense operations dashboard</p></div>
6
- <footer id="viewport-1-a-footer" style="position:absolute;left:0;top:804px;width:1536px;height:220px;background:#0b141d"><p class="text" id="viewport-1-a-footer-text" style="left:24px;top:40px;color:white">Compact dashboard footer</p></footer>
6
+ <footer id="viewport-1-a-footer" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a-dashboard" data-ly-section-owner="content-ground" data-ly-section-openability="not-openable" data-ly-section-fact="footer" style="position:absolute;left:0;top:804px;width:1536px;height:220px;background:#0b141d"><p class="text" id="viewport-1-a-footer-text" style="left:24px;top:40px;color:white">Compact dashboard footer</p></footer>
7
7
  </section>
8
8
  </div></div></div>
@@ -1,6 +1,6 @@
1
1
  <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
- <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
2
+ <div class="generated-screen" data-ly-section-ground-owner="content-ground"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
3
3
  <!-- gradient → image: both grounds are experimental classes; the seam's ink gap is narrow but the page must abstain -->
4
4
  <section id="viewport-1-a" style="top:0;height:700px;background-image:linear-gradient(#fbf6ee,#efe6d2)"><h1 id="viewport-1-a-heading" class="text" style="left:80px;top:200px;font-size:48px">Give your time.</h1><p id="viewport-1-a-text" class="text" style="left:80px;top:660px;font-size:16px">Join neighbors.</p></section>
5
- <section id="viewport-2-a" style="top:700px;height:700px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==);background-size:cover;background-repeat:no-repeat"><p id="viewport-2-a-eyebrow" class="text" style="left:80px;top:20px;font-size:12px">UPCOMING</p></section>
5
+ <section id="viewport-2-a" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a" data-ly-section-owner="content-ground" data-ly-section-openability="not-openable" data-ly-section-fact="page" style="top:700px;height:700px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==);background-size:cover;background-repeat:no-repeat"><p id="viewport-2-a-eyebrow" class="text" style="left:80px;top:20px;font-size:12px">UPCOMING</p></section>
6
6
  </div></div></div>
@@ -1,7 +1,7 @@
1
1
  <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1600px}section{position:absolute;left:0;width:1536px;height:400px;background:#fff}.text{position:absolute;white-space:nowrap;font-size:16px;line-height:16px}</style>
2
- <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1600px"><div id="page-flow">
2
+ <div class="generated-screen" data-ly-section-ground-owner="page-ground" style="background:#fff"><div class="ly-anchor-canvas" style="position:relative;height:1600px"><div id="page-flow">
3
3
  <section id="viewport-1-a" style="top:0"><p id="viewport-1-a-end" class="text" style="left:80px;top:376px">One</p></section>
4
- <section id="viewport-2-a" style="top:400px"><p id="viewport-2-a-start" class="text" style="left:80px;top:0">Two start</p><p id="viewport-2-a-end" class="text" style="left:80px;top:376px">Two end</p></section>
5
- <section id="viewport-3-a" style="top:800px"><p id="viewport-3-a-start" class="text" style="left:80px;top:0">Three start</p><p id="viewport-3-a-end" class="text" style="left:80px;top:370px">Three end</p></section>
6
- <section id="viewport-4-a" style="top:1200px"><p id="viewport-4-a-start" class="text" style="left:80px;top:16px">Four</p></section>
4
+ <section id="viewport-2-a" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-1-a" data-ly-section-owner="page-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="page" style="top:400px"><p id="viewport-2-a-start" class="text" style="left:80px;top:0">Two start</p><p id="viewport-2-a-end" class="text" style="left:80px;top:376px">Two end</p></section>
5
+ <section id="viewport-3-a" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-2-a" data-ly-section-owner="page-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="page" style="top:800px"><p id="viewport-3-a-start" class="text" style="left:80px;top:0">Three start</p><p id="viewport-3-a-end" class="text" style="left:80px;top:370px">Three end</p></section>
6
+ <section id="viewport-4-a" data-ly-section-boundary="before" data-ly-section-boundary-revision="section-boundary-v1" data-ly-section-before="viewport-3-a" data-ly-section-owner="page-ground" data-ly-section-openability="openable" data-ly-section-continuation="owner-fill" data-ly-section-fact="page" style="top:1200px"><p id="viewport-4-a-start" class="text" style="left:80px;top:16px">Four</p></section>
7
7
  </div></div></div>
@@ -111,12 +111,59 @@
111
111
  setTimeout(() => { t.classList.remove('proto-toast-in'); setTimeout(() => t.remove(), 300); }, 1800);
112
112
  }
113
113
 
114
+ // WAI-ARIA 1.2: aria-disabled="true" on an ancestor disables the element
115
+ // and ALL focusable descendants — a nearer descendant's own
116
+ // aria-disabled="false" does not cancel it. closest('[aria-disabled]')
117
+ // stops at the first match regardless of its value, so it can find a
118
+ // descendant's "false" and miss a "true" further up. Walk the whole
119
+ // ancestor chain instead.
120
+ function ariaDisabledAncestor(el) {
121
+ for (let node = el; node; node = node.parentElement) {
122
+ const value = node.getAttribute && node.getAttribute('aria-disabled');
123
+ if (value != null && value.trim().toLowerCase() === 'true') return node;
124
+ }
125
+ return null;
126
+ }
127
+
128
+ function sourceInertReason(el) {
129
+ if (el.closest('[disabled]')) return 'disabled';
130
+ if (el.closest('[inert]')) return 'inert';
131
+ if (el.closest('[hidden]')) return 'hidden';
132
+ if (el.closest('[data-proto-demoted]')) return 'demoted';
133
+ // The converter's own marker for a non-native control it deliberately
134
+ // left presentational — must be honored here too, or a real click can
135
+ // reactivate a control the source explicitly declared static.
136
+ if (el.closest('[data-12ui-presentational-control]')) return 'presentational-control';
137
+ if (ariaDisabledAncestor(el)) return 'aria-disabled';
138
+ return null;
139
+ }
140
+
141
+ function markBoundAction(el, action) {
142
+ const inertReason = sourceInertReason(el);
143
+ if (inertReason) {
144
+ el.dataset.protoSuppressed = `source-${inertReason}`;
145
+ return false;
146
+ }
147
+ // This must run in the real browser DOM. The build-time Linkedom CSSOM
148
+ // drops declaration priority, while an inline important declaration here
149
+ // outranks every selector-based rule, including #id!important.
150
+ el.style.setProperty('pointer-events', 'auto', 'important');
151
+ el.dataset.protoPointerRestored = 'true';
152
+ el.dataset.protoRuntimeBound = action;
153
+ el.classList.add('proto-clickable');
154
+ return true;
155
+ }
156
+
114
157
  // ---- bind actions
115
158
  const bound = { navigate: 0, dropdown: 0, modal: 0, field: 0 };
116
159
  for (const el of document.querySelectorAll('[data-proto-action]')) {
117
160
  const action = el.dataset.protoAction;
118
- el.classList.add('proto-clickable');
119
161
  if (action === 'navigate') {
162
+ if (!el.dataset.protoTarget) {
163
+ el.dataset.protoSuppressed = 'unresolved-navigation';
164
+ continue;
165
+ }
166
+ if (!markBoundAction(el, action)) continue;
120
167
  bound.navigate++;
121
168
  el.addEventListener('click', (e) => {
122
169
  e.preventDefault(); e.stopPropagation();
@@ -131,10 +178,12 @@
131
178
  el.dataset.protoSuppressed = 'invalid-dropdown';
132
179
  continue;
133
180
  }
181
+ if (!markBoundAction(el, action)) continue;
134
182
  bound.dropdown++;
135
183
  const labelEl = el.dataset.protoLabel ? document.getElementById(el.dataset.protoLabel) : null;
136
184
  el.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openDropdown(el, options, labelEl); });
137
185
  } else if (action === 'modal') {
186
+ if (!markBoundAction(el, action)) continue;
138
187
  bound.modal++;
139
188
  const modal = JSON.parse(el.dataset.protoModal || '{}');
140
189
  el.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openModal(modal); });
@@ -145,6 +194,7 @@
145
194
  for (const el of document.querySelectorAll('[data-proto-field]')) {
146
195
  const textEl = el.dataset.protoField ? document.getElementById(el.dataset.protoField) : null;
147
196
  if (!textEl) continue;
197
+ if (!markBoundAction(el, 'field')) continue;
148
198
  bound.field++;
149
199
  const placeholder = textEl.textContent;
150
200
  const submitLabel = el.dataset.protoSubmit || 'Send';
@@ -164,7 +214,7 @@
164
214
  });
165
215
  el.addEventListener('click', (e) => { if (e.target !== textEl) { textEl.focus(); } });
166
216
  for (const btn of document.querySelectorAll(`[data-proto-submit-for="${textEl.id}"]`)) {
167
- btn.classList.add('proto-clickable');
217
+ if (!markBoundAction(btn, 'field-submit')) continue;
168
218
  btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); textEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); });
169
219
  }
170
220
  }