@profpowell/vb-design-system 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -0
  3. package/dist/vb-design-system.css +993 -0
  4. package/dist/vb-design-system.js +4482 -0
  5. package/dist/vb-design-system.min.css +1 -0
  6. package/dist/vb-design-system.min.js +513 -0
  7. package/package.json +78 -0
  8. package/src/index.css +21 -0
  9. package/src/index.js +24 -0
  10. package/src/lib/bundle-registry.js +156 -0
  11. package/src/lib/vb-element.js +96 -0
  12. package/src/patterns/do-dont.css +86 -0
  13. package/src/patterns/token-table.css +67 -0
  14. package/src/utils/copy-init.js +287 -0
  15. package/src/web-components/accessibility-specimen/api.json +51 -0
  16. package/src/web-components/accessibility-specimen/logic.js +232 -0
  17. package/src/web-components/accessibility-specimen/static.html +41 -0
  18. package/src/web-components/accessibility-specimen/styles.css +156 -0
  19. package/src/web-components/brand-specimen/api.json +37 -0
  20. package/src/web-components/brand-specimen/logic.js +61 -0
  21. package/src/web-components/brand-specimen/static.html +77 -0
  22. package/src/web-components/brand-specimen/styles.css +10 -0
  23. package/src/web-components/breakpoint-specimen/api.json +44 -0
  24. package/src/web-components/breakpoint-specimen/logic.js +186 -0
  25. package/src/web-components/breakpoint-specimen/static.html +38 -0
  26. package/src/web-components/breakpoint-specimen/styles.css +180 -0
  27. package/src/web-components/color-palette/api.json +73 -0
  28. package/src/web-components/color-palette/logic.js +280 -0
  29. package/src/web-components/color-palette/static.html +54 -0
  30. package/src/web-components/color-palette/styles.css +113 -0
  31. package/src/web-components/color-picker/_color-utils.js +210 -0
  32. package/src/web-components/component-sampler/api.json +15 -0
  33. package/src/web-components/component-sampler/logic.js +120 -0
  34. package/src/web-components/component-sampler/static.html +47 -0
  35. package/src/web-components/component-sampler/styles.css +9 -0
  36. package/src/web-components/font-pairer/_font-data.js +95 -0
  37. package/src/web-components/font-pairer/api.json +56 -0
  38. package/src/web-components/font-pairer/logic.js +401 -0
  39. package/src/web-components/font-pairer/static.html +9 -0
  40. package/src/web-components/font-pairer/styles.css +10 -0
  41. package/src/web-components/gradient-builder/_gradient-utils.js +54 -0
  42. package/src/web-components/gradient-builder/api.json +63 -0
  43. package/src/web-components/gradient-builder/logic.js +261 -0
  44. package/src/web-components/gradient-builder/static.html +9 -0
  45. package/src/web-components/gradient-builder/styles.css +10 -0
  46. package/src/web-components/icon-set/api.json +39 -0
  47. package/src/web-components/icon-set/logic.js +133 -0
  48. package/src/web-components/icon-set/static.html +36 -0
  49. package/src/web-components/icon-set/styles.css +82 -0
  50. package/src/web-components/icon-specimen/api.json +46 -0
  51. package/src/web-components/icon-specimen/logic.js +67 -0
  52. package/src/web-components/icon-specimen/static.html +39 -0
  53. package/src/web-components/icon-specimen/styles.css +38 -0
  54. package/src/web-components/layout-specimen/api.json +12 -0
  55. package/src/web-components/layout-specimen/logic.js +219 -0
  56. package/src/web-components/layout-specimen/static.html +22 -0
  57. package/src/web-components/layout-specimen/styles.css +95 -0
  58. package/src/web-components/motion-specimen/api.json +54 -0
  59. package/src/web-components/motion-specimen/logic.js +146 -0
  60. package/src/web-components/motion-specimen/static.html +13 -0
  61. package/src/web-components/motion-specimen/styles.css +29 -0
  62. package/src/web-components/palette-generator/_palette-utils.js +133 -0
  63. package/src/web-components/palette-generator/api.json +79 -0
  64. package/src/web-components/palette-generator/logic.js +288 -0
  65. package/src/web-components/palette-generator/static.html +47 -0
  66. package/src/web-components/palette-generator/styles.css +36 -0
  67. package/src/web-components/semantic-palette/_color-math.js +149 -0
  68. package/src/web-components/semantic-palette/api.json +41 -0
  69. package/src/web-components/semantic-palette/logic.js +400 -0
  70. package/src/web-components/semantic-palette/static.html +42 -0
  71. package/src/web-components/semantic-palette/styles.css +20 -0
  72. package/src/web-components/spacing-specimen/api.json +63 -0
  73. package/src/web-components/spacing-specimen/logic.js +132 -0
  74. package/src/web-components/spacing-specimen/static.html +71 -0
  75. package/src/web-components/spacing-specimen/styles.css +58 -0
  76. package/src/web-components/theme-catalog/api.json +14 -0
  77. package/src/web-components/theme-catalog/logic.js +228 -0
  78. package/src/web-components/theme-catalog/static.html +15 -0
  79. package/src/web-components/theme-export/api.json +18 -0
  80. package/src/web-components/theme-export/dtcg-serialize.js +417 -0
  81. package/src/web-components/theme-export/logic.js +201 -0
  82. package/src/web-components/theme-export/static.html +12 -0
  83. package/src/web-components/theme-export/theme-css-parse.js +93 -0
  84. package/src/web-components/theme-import/api.json +14 -0
  85. package/src/web-components/theme-import/catalog/atlassian.tokens.json +303 -0
  86. package/src/web-components/theme-import/catalog/bootstrap.tokens.json +304 -0
  87. package/src/web-components/theme-import/catalog/carbon.tokens.json +301 -0
  88. package/src/web-components/theme-import/catalog/catppuccin-mocha.tokens.json +301 -0
  89. package/src/web-components/theme-import/catalog/govuk.tokens.json +301 -0
  90. package/src/web-components/theme-import/catalog/manifest.json +132 -0
  91. package/src/web-components/theme-import/catalog/material.tokens.json +289 -0
  92. package/src/web-components/theme-import/catalog/salesforce.tokens.json +301 -0
  93. package/src/web-components/theme-import/catalog/tailwind.tokens.json +302 -0
  94. package/src/web-components/theme-import/dtcg-deserialize.js +322 -0
  95. package/src/web-components/theme-import/logic.js +263 -0
  96. package/src/web-components/theme-import/static.html +15 -0
  97. package/src/web-components/token-specimen/api.json +24 -0
  98. package/src/web-components/token-specimen/logic.js +256 -0
  99. package/src/web-components/token-specimen/static.html +47 -0
  100. package/src/web-components/token-specimen/styles.css +10 -0
  101. package/src/web-components/type-specimen/api.json +91 -0
  102. package/src/web-components/type-specimen/logic.js +156 -0
  103. package/src/web-components/type-specimen/static.html +84 -0
  104. package/src/web-components/type-specimen/styles.css +131 -0
@@ -0,0 +1,156 @@
1
+ /* --------------------------------------------------------------------------
2
+ accessibility-specimen: WCAG contrast-ratio table + a11y checklist
3
+ for design-system docs.
4
+ -------------------------------------------------------------------------- */
5
+
6
+ accessibility-specimen {
7
+ display: block;
8
+ font: inherit;
9
+ }
10
+
11
+ /* -------- Contrast table -------- */
12
+
13
+ accessibility-specimen .as-contrast-table {
14
+ inline-size: 100%;
15
+ border-collapse: collapse;
16
+ border: var(--border-width-thin, 1px) solid var(--color-border, #ddd);
17
+ border-radius: var(--radius-m, 0.5rem);
18
+ overflow: hidden;
19
+ font-size: 0.95em;
20
+ }
21
+
22
+ accessibility-specimen .as-contrast-table th,
23
+ accessibility-specimen .as-contrast-table td {
24
+ padding-block: var(--size-2xs, 0.375rem);
25
+ padding-inline: var(--size-s, 0.5rem);
26
+ text-align: start;
27
+ border-block-end: var(--border-width-thin, 1px) solid var(--color-border, #ddd);
28
+ }
29
+
30
+ accessibility-specimen .as-contrast-table tr:last-child th,
31
+ accessibility-specimen .as-contrast-table tr:last-child td {
32
+ border-block-end: none;
33
+ }
34
+
35
+ accessibility-specimen .as-contrast-table thead {
36
+ background: var(--color-surface-raised, var(--color-surface, #fff));
37
+ font-size: 0.85em;
38
+ text-transform: uppercase;
39
+ letter-spacing: 0.04em;
40
+ color: var(--color-text-muted, currentColor);
41
+ }
42
+
43
+ accessibility-specimen .as-pair-label {
44
+ font-weight: 500;
45
+ display: flex;
46
+ align-items: center;
47
+ gap: var(--size-2xs, 0.375rem);
48
+ font-variant-numeric: tabular-nums;
49
+ }
50
+
51
+ accessibility-specimen .as-swatch {
52
+ display: inline-block;
53
+ inline-size: 1.25rem;
54
+ block-size: 1.25rem;
55
+ border-radius: var(--radius-s, 0.25rem);
56
+ border: var(--border-width-thin, 1px) solid currentColor;
57
+ flex-shrink: 0;
58
+ }
59
+
60
+ accessibility-specimen .as-pair-sample {
61
+ font-weight: 500;
62
+ font-size: 1em;
63
+ white-space: nowrap;
64
+ text-align: center;
65
+ }
66
+
67
+ accessibility-specimen .as-pair-ratio {
68
+ font-variant-numeric: tabular-nums;
69
+ font-weight: 600;
70
+ }
71
+
72
+ accessibility-specimen .as-badge {
73
+ text-align: center;
74
+ font-weight: 600;
75
+ font-size: 1.1em;
76
+ inline-size: 3em;
77
+ }
78
+
79
+ accessibility-specimen .as-pass {
80
+ color: var(--color-success, #2e7d32);
81
+ }
82
+
83
+ accessibility-specimen .as-fail {
84
+ color: var(--color-error, #c62828);
85
+ }
86
+
87
+ accessibility-specimen .as-empty {
88
+ text-align: center;
89
+ font-style: italic;
90
+ color: var(--color-text-muted, currentColor);
91
+ padding-block: var(--size-l, 1.5rem) !important;
92
+ }
93
+
94
+ /* -------- Checklist -------- */
95
+
96
+ accessibility-specimen .as-checklist {
97
+ list-style: none;
98
+ margin: 0;
99
+ padding: 0;
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: var(--size-2xs, 0.375rem);
103
+ }
104
+
105
+ accessibility-specimen .as-checklist > li {
106
+ display: flex;
107
+ align-items: flex-start;
108
+ gap: var(--size-xs, 0.5rem);
109
+ padding-block: var(--size-2xs, 0.375rem);
110
+ padding-inline: var(--size-s, 0.5rem);
111
+ border: var(--border-width-thin, 1px) solid var(--color-border, #ddd);
112
+ border-radius: var(--radius-s, 0.25rem);
113
+ }
114
+
115
+ accessibility-specimen .as-checklist-icon {
116
+ flex-shrink: 0;
117
+ display: inline-flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ inline-size: 1.5rem;
121
+ block-size: 1.5rem;
122
+ border-radius: var(--radius-full, 999px);
123
+ font-weight: 700;
124
+ line-height: 1;
125
+ }
126
+
127
+ accessibility-specimen .as-status-pass { border-color: var(--color-success, #2e7d32); }
128
+ accessibility-specimen .as-status-pass .as-checklist-icon {
129
+ background: var(--color-success, #2e7d32); color: var(--color-text-on-success, white);
130
+ }
131
+
132
+ accessibility-specimen .as-status-fail { border-color: var(--color-error, #c62828); }
133
+ accessibility-specimen .as-status-fail .as-checklist-icon {
134
+ background: var(--color-error, #c62828); color: var(--color-text-on-error, white);
135
+ }
136
+
137
+ accessibility-specimen .as-status-warn { border-color: var(--color-warning, #b8860b); }
138
+ accessibility-specimen .as-status-warn .as-checklist-icon {
139
+ background: var(--color-warning, #b8860b); color: var(--color-text-on-warning, white);
140
+ }
141
+
142
+ accessibility-specimen .as-status-na .as-checklist-icon {
143
+ background: var(--color-text-muted, oklch(60% 0 0)); color: var(--color-surface, white);
144
+ }
145
+
146
+ accessibility-specimen .as-sr-only {
147
+ position: absolute;
148
+ inline-size: 1px;
149
+ block-size: 1px;
150
+ padding: 0;
151
+ margin: -1px;
152
+ overflow: hidden;
153
+ clip: rect(0 0 0 0);
154
+ white-space: nowrap;
155
+ border: 0;
156
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "../../../schemas/api.schema.json",
3
+ "element": "brand-specimen",
4
+ "type": "web-component",
5
+ "description": "Brand mark specimen: shows a <brand-mark> on light/dark surfaces, a size scale, and a clear-space box",
6
+ "htmlvalidate": {
7
+ "flow": true,
8
+ "permittedContent": [
9
+ "@flow"
10
+ ]
11
+ },
12
+ "attributes": [
13
+ {
14
+ "name": "src",
15
+ "kind": "host-api",
16
+ "purpose": "config",
17
+ "type": "string",
18
+ "description": "Logo image URL, passed through to each <brand-mark src>"
19
+ },
20
+ {
21
+ "name": "name",
22
+ "kind": "host-api",
23
+ "purpose": "config",
24
+ "type": "string",
25
+ "description": "Brand name, passed through to each <brand-mark wordmark> (default: \"Brand\")"
26
+ },
27
+ {
28
+ "name": "data-sizes",
29
+ "kind": "data",
30
+ "purpose": "config",
31
+ "type": "string",
32
+ "description": "Space-separated <brand-mark data-size> values for the scale row (default: \"s m l xl\")"
33
+ }
34
+ ],
35
+ "childAttributes": [],
36
+ "structure": []
37
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * brand-specimen: DS specimen documenting a brand mark
3
+ *
4
+ * Shows a `<brand-mark>` on light and dark surfaces, at a size scale, and
5
+ * inside a clear-space box. Pairs with the other identity/token specimens
6
+ * (color-palette, semantic-palette) for brand-guideline pages.
7
+ *
8
+ * `<brand-mark>` itself is a VB-core component (runtime peer) — this
9
+ * specimen only arranges instances of it; it does not re-implement its
10
+ * theme-aware light/dark logo swap.
11
+ *
12
+ * @attr {string} src - Logo image URL, passed through to `<brand-mark src>`
13
+ * @attr {string} name - Brand name, passed through as `<brand-mark>` child text content
14
+ * @attr {string} data-sizes - Space-separated `<brand-mark data-size>` values
15
+ * for the scale row (default: "s m l xl")
16
+ *
17
+ * @example
18
+ * <brand-specimen src="/logo.svg" name="Acme"></brand-specimen>
19
+ * <brand-specimen src="/logo.svg" name="Acme" data-sizes="xs s m l xl 2xl"></brand-specimen>
20
+ */
21
+
22
+ import { registerComponent } from '../../lib/bundle-registry.js';
23
+ import { VBElement } from '../../lib/vb-element.js';
24
+
25
+ class BrandSpecimen extends VBElement {
26
+ static observedAttributes = ['src', 'name', 'data-sizes'];
27
+
28
+ setup() {
29
+ this.#render();
30
+ }
31
+
32
+ attributeChangedCallback() {
33
+ if (this.isConnected) this.#render();
34
+ }
35
+
36
+ #render() {
37
+ const src = this.getAttribute('src') || '';
38
+ const name = this.getAttribute('name') || 'Brand';
39
+ const sizes = (this.getAttribute('data-sizes') || 's m l xl').trim().split(/\s+/);
40
+ // `<brand-mark>` treats `wordmark` as a boolean presence attribute and
41
+ // reads the displayed brand name from its light-DOM text content — the
42
+ // name must be the element's child text, not the value of `wordmark=`.
43
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
44
+ const mark = (size) =>
45
+ `<brand-mark${src ? ` src="${esc(src)}"` : ''} wordmark${size ? ` data-size="${size}"` : ''}>${esc(name)}</brand-mark>`;
46
+ this.innerHTML = `
47
+ <section class="brand-specimen">
48
+ <div class="bs-surfaces">
49
+ <figure data-surface="light">${mark('l')}<figcaption>On light</figcaption></figure>
50
+ <figure data-surface="dark">${mark('l')}<figcaption>On dark</figcaption></figure>
51
+ </div>
52
+ <div class="bs-scale" data-scale>
53
+ ${sizes.map(s => `<span>${mark(s)}<small>${s}</small></span>`).join('')}
54
+ </div>
55
+ <div class="bs-clearspace"><span class="bs-clear-box">${mark('l')}</span>
56
+ <figcaption>Clear space</figcaption></div>
57
+ </section>`;
58
+ }
59
+ }
60
+
61
+ registerComponent('brand-specimen', BrandSpecimen);
@@ -0,0 +1,77 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Brand Specimen (Static Fallback)</title>
7
+ <style>
8
+ /*
9
+ * Static fallback for <brand-specimen>.
10
+ *
11
+ * Upgrade relationship:
12
+ * When JavaScript loads and the <brand-specimen> custom element is
13
+ * defined, it renders a <brand-mark> on light and dark surface
14
+ * panels, a size scale, and a clear-space box.
15
+ *
16
+ * Without JS, this static page presents the same information as
17
+ * plain semantic HTML: a labeled logo image and a list of the
18
+ * size-scale names, providing equivalent content through markup
19
+ * alone.
20
+ */
21
+
22
+ *, *::before, *::after { box-sizing: border-box; }
23
+
24
+ body {
25
+ font-family: system-ui, sans-serif;
26
+ line-height: 1.6;
27
+ margin: 0;
28
+ padding: 2rem;
29
+ color: #1a1a1a;
30
+ background: #f8f8f8;
31
+ }
32
+
33
+ h1 { margin-block-end: 1rem; }
34
+
35
+ .surfaces {
36
+ display: grid;
37
+ grid-template-columns: 1fr 1fr;
38
+ gap: 1rem;
39
+ max-width: 30rem;
40
+ margin-block-end: 1.5rem;
41
+ }
42
+
43
+ figure {
44
+ margin: 0;
45
+ padding: 1.5rem;
46
+ border-radius: 0.5rem;
47
+ text-align: center;
48
+ }
49
+
50
+ figure img { max-height: 3rem; }
51
+
52
+ .surfaces figure:first-child { background: #fff; color: #111; }
53
+ .surfaces figure:last-child { background: #111; color: #fff; }
54
+
55
+ figcaption { font-size: 0.875rem; margin-block-start: 0.5rem; }
56
+
57
+ ul { font-family: ui-monospace, monospace; font-size: 0.875rem; color: #666; }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <h1>Brand Mark</h1>
62
+ <p>These panels show the brand mark on light and dark surfaces. With JavaScript enabled, this becomes an interactive specimen with a size scale and clear-space guide.</p>
63
+
64
+ <div class="surfaces">
65
+ <figure><img src="" alt="" width="0" height="0"><figcaption>On light</figcaption></figure>
66
+ <figure><img src="" alt="" width="0" height="0"><figcaption>On dark</figcaption></figure>
67
+ </div>
68
+
69
+ <p>Size scale:</p>
70
+ <ul>
71
+ <li>s</li>
72
+ <li>m</li>
73
+ <li>l</li>
74
+ <li>xl</li>
75
+ </ul>
76
+ </body>
77
+ </html>
@@ -0,0 +1,10 @@
1
+ .brand-specimen { display: grid; gap: var(--size-l, 1.5rem); }
2
+ .bs-surfaces { display: grid; grid-template-columns: 1fr 1fr; gap: var(--size-m, 1rem); }
3
+ .bs-surfaces figure { margin: 0; padding: var(--size-l, 1.5rem); border-radius: var(--radius-m, 0.5rem);
4
+ display: grid; place-items: center; gap: var(--size-s, 0.75rem); }
5
+ .bs-surfaces [data-surface="light"] { background: #fff; color: #111; }
6
+ .bs-surfaces [data-surface="dark"] { background: #111; color: #fff; }
7
+ .bs-scale { display: flex; align-items: end; gap: var(--size-l, 1.5rem); flex-wrap: wrap; }
8
+ .bs-scale span { display: grid; justify-items: center; gap: var(--size-2xs, 0.25rem); }
9
+ .bs-clear-box { display: inline-block; padding: 1em; outline: 1px dashed var(--color-border, #d4d4d8);
10
+ outline-offset: 0.25em; }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "../../../schemas/api.schema.json",
3
+ "element": "breakpoint-specimen",
4
+ "type": "web-component",
5
+ "description": "Responsive breakpoint visualization with live width readout",
6
+ "htmlvalidate": {
7
+ "flow": true,
8
+ "permittedContent": [
9
+ "@flow"
10
+ ]
11
+ },
12
+ "attributes": [
13
+ {
14
+ "name": "tokens",
15
+ "kind": "host-api",
16
+ "purpose": "config",
17
+ "type": "string",
18
+ "description": "Comma-separated breakpoint names (default: \"sm,md,lg,xl\")"
19
+ },
20
+ {
21
+ "name": "prefix",
22
+ "kind": "host-api",
23
+ "purpose": "config",
24
+ "type": "string",
25
+ "description": "CSS variable prefix (default: \"--bp-\")"
26
+ },
27
+ {
28
+ "name": "label",
29
+ "kind": "host-api",
30
+ "purpose": "config",
31
+ "type": "string",
32
+ "description": "Optional heading label"
33
+ },
34
+ {
35
+ "name": "data-observe",
36
+ "kind": "data",
37
+ "purpose": "config",
38
+ "type": "string",
39
+ "description": "CSS selector of a container to track instead of the viewport. Container-query mode (JS-only)."
40
+ }
41
+ ],
42
+ "childAttributes": [],
43
+ "structure": []
44
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * breakpoint-specimen: Responsive breakpoint visualization
3
+ *
4
+ * Joins the specimen family (spacing, type, motion, token). Renders a
5
+ * ruler of viewport breakpoints with a live cursor at the current
6
+ * width and the active breakpoint label called out.
7
+ *
8
+ * Reads --bp-* tokens from :root by default; can be given an explicit
9
+ * tokens list. Tracks the viewport (window) by default; data-observe
10
+ * accepts a CSS selector to track an arbitrary container instead.
11
+ *
12
+ * Progressive enhancement:
13
+ * 1. HTML — a semantic <dl> of breakpoint name → value remains
14
+ * readable without JS or CSS.
15
+ * 2. CSS — pure-CSS active-row highlighting via @media rules pulls
16
+ * the matching token into a callout, no JS required for the
17
+ * "which breakpoint is active" cue (viewport mode only).
18
+ * 3. JS — adds the live numeric width readout and the moving
19
+ * cursor on the ruler, plus container-query mode (data-observe).
20
+ *
21
+ * @attr {string} tokens - Comma-separated breakpoint names
22
+ * (default: "sm,md,lg,xl")
23
+ * @attr {string} prefix - CSS variable prefix (default: "--bp-")
24
+ * @attr {string} label - Optional heading label
25
+ * @attr {string} data-observe - CSS selector of a container to track
26
+ * instead of the viewport. JS-only.
27
+ *
28
+ * @example
29
+ * <breakpoint-specimen></breakpoint-specimen>
30
+ * <breakpoint-specimen tokens="sm,md,lg" label="Core breakpoints"></breakpoint-specimen>
31
+ * <breakpoint-specimen data-observe="#preview-frame"></breakpoint-specimen>
32
+ */
33
+
34
+ import { registerComponent } from '../../lib/bundle-registry.js';
35
+ import { VBElement } from '../../lib/vb-element.js';
36
+
37
+ class BreakpointSpecimen extends VBElement {
38
+ static observedAttributes = ['tokens', 'prefix', 'label', 'data-observe'];
39
+
40
+ setup() {
41
+ this.#render();
42
+ this.#start();
43
+ }
44
+
45
+ attributeChangedCallback() {
46
+ if (this.isConnected) {
47
+ this.#stop();
48
+ this.#render();
49
+ this.#start();
50
+ }
51
+ }
52
+
53
+ teardown() {
54
+ this.#stop();
55
+ }
56
+
57
+ #render() {
58
+ const tokensAttr = this.getAttribute('tokens') || 'sm,md,lg,xl';
59
+ const prefix = this.getAttribute('prefix') || '--bp-';
60
+ const label = this.getAttribute('label') || '';
61
+ const names = tokensAttr.split(',').map((t) => t.trim()).filter(Boolean);
62
+
63
+ const cs = getComputedStyle(document.documentElement);
64
+ const stops = names
65
+ .map((name) => {
66
+ const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
67
+ const px = this.#toPx(raw);
68
+ return { name, raw, px };
69
+ })
70
+ .filter((s) => s.px > 0)
71
+ .sort((a, b) => a.px - b.px);
72
+
73
+ this.#stops = stops;
74
+ this.#max = stops.length ? stops[stops.length - 1].px * 1.15 : 1;
75
+
76
+ const labelHTML = label ? `<header class="bps-label">${label}</header>` : '';
77
+
78
+ const readoutHTML = `
79
+ <output class="bps-readout" aria-live="polite">
80
+ <span class="bps-width">—</span>
81
+ <span class="bps-active" data-active="">—</span>
82
+ </output>
83
+ `;
84
+
85
+ const rulerHTML = `
86
+ <figure class="bps-ruler" aria-hidden="true">
87
+ <span class="bps-cursor" data-cursor></span>
88
+ ${stops.map((s) => `
89
+ <span class="bps-stop" style="--bps-pos:${(s.px / this.#max) * 100}%" data-bp="${s.name}">
90
+ <span class="bps-tick"></span>
91
+ <span class="bps-stop-label">${s.name}<br><small>${Math.round(s.px)}px</small></span>
92
+ </span>
93
+ `).join('')}
94
+ </figure>
95
+ `;
96
+
97
+ const tableHTML = `
98
+ <dl class="bps-list">
99
+ ${stops.map((s) => `
100
+ <div class="bps-row" data-bp="${s.name}">
101
+ <dt>${s.name}</dt>
102
+ <dd><code>${s.raw}</code> — ${Math.round(s.px)}px and up</dd>
103
+ </div>
104
+ `).join('')}
105
+ </dl>
106
+ `;
107
+
108
+ this.innerHTML = `${labelHTML}${readoutHTML}${rulerHTML}${tableHTML}`;
109
+ this.#widthEl = /** @type {HTMLElement | null} */ (this.querySelector('.bps-width'));
110
+ this.#activeEl = /** @type {HTMLElement | null} */ (this.querySelector('.bps-active'));
111
+ this.#cursorEl = /** @type {HTMLElement | null} */ (this.querySelector('[data-cursor]'));
112
+ }
113
+
114
+ #start() {
115
+ const sel = this.getAttribute('data-observe');
116
+ if (sel) {
117
+ const target = document.querySelector(sel);
118
+ if (!target) return;
119
+ this.#ro = new ResizeObserver(() => this.#update(target.clientWidth));
120
+ this.#ro.observe(target);
121
+ this.#update(target.clientWidth);
122
+ return;
123
+ }
124
+ const update = () => this.#update(window.innerWidth);
125
+ update();
126
+ this.listen(window, 'resize', update, { passive: true });
127
+ }
128
+
129
+ #stop() {
130
+ if (this.#ro) {
131
+ this.#ro.disconnect();
132
+ this.#ro = null;
133
+ }
134
+ }
135
+
136
+ #update(width) {
137
+ if (!this.#widthEl || !this.#activeEl) return;
138
+ this.#widthEl.textContent = `${Math.round(width)}px`;
139
+ const active = this.#activeFor(width);
140
+ this.#activeEl.textContent = active ? active.name : 'base';
141
+ this.#activeEl.dataset.active = active ? active.name : '';
142
+ if (this.#cursorEl) {
143
+ const pct = Math.min(100, (width / this.#max) * 100);
144
+ this.#cursorEl.style.setProperty('--bps-pos', `${pct}%`);
145
+ }
146
+ this.querySelectorAll('[data-bp]').forEach((el) => {
147
+ const matches = active && el.getAttribute('data-bp') === active.name;
148
+ el.toggleAttribute('data-current', !!matches);
149
+ });
150
+ }
151
+
152
+ #activeFor(width) {
153
+ let match = null;
154
+ for (const stop of this.#stops) {
155
+ if (width >= stop.px) match = stop;
156
+ }
157
+ return match;
158
+ }
159
+
160
+ #toPx(value) {
161
+ if (!value) return 0;
162
+ const n = parseFloat(value);
163
+ if (Number.isNaN(n)) return 0;
164
+ if (value.endsWith('rem') || value.endsWith('em')) {
165
+ const root = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
166
+ return n * root;
167
+ }
168
+ return n;
169
+ }
170
+
171
+ /** @type {Array<{name: string, raw: string, px: number}>} */
172
+ #stops = [];
173
+ #max = 1;
174
+ /** @type {ResizeObserver | null} */
175
+ #ro = null;
176
+ /** @type {HTMLElement | null} */
177
+ #widthEl = null;
178
+ /** @type {HTMLElement | null} */
179
+ #activeEl = null;
180
+ /** @type {HTMLElement | null} */
181
+ #cursorEl = null;
182
+ }
183
+
184
+ registerComponent('breakpoint-specimen', BreakpointSpecimen);
185
+
186
+ export { BreakpointSpecimen };
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Breakpoint Specimen — Static Fallback</title>
7
+ <style>
8
+ body { font-family: system-ui, sans-serif; padding: 2rem; }
9
+ dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; max-width: 30rem; }
10
+ dt { font-family: ui-monospace, monospace; font-weight: 600; }
11
+ dd { margin: 0; color: #666; }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <!--
16
+ Static fallback for <breakpoint-specimen>.
17
+
18
+ Upgrade relationship:
19
+ - Without JS, modern browsers: the @media-driven highlight in the
20
+ component's stylesheet calls out the active row, so even no-JS
21
+ readers see which breakpoint is in effect.
22
+ - Without JS or CSS: the <dl> below remains a readable list of
23
+ breakpoint names and their token values.
24
+ - With JS: the component adds a live numeric width readout and a
25
+ moving cursor on a visual ruler, plus container-query tracking
26
+ via data-observe.
27
+ -->
28
+
29
+ <breakpoint-specimen>
30
+ <dl>
31
+ <dt>sm</dt><dd>37.5rem (600px) and up — compact mobile</dd>
32
+ <dt>md</dt><dd>48rem (768px) and up — tablet / mobile split</dd>
33
+ <dt>lg</dt><dd>64rem (1024px) and up — desktop layouts</dd>
34
+ <dt>xl</dt><dd>87.5rem (1400px) and up — ultra-wide enhancements</dd>
35
+ </dl>
36
+ </breakpoint-specimen>
37
+ </body>
38
+ </html>