@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,113 @@
1
+ /* --- Progressive enhancement: no-JS fallback --- */
2
+ color-palette:not(:defined) {
3
+ display: block;
4
+ }
5
+
6
+ color-palette {
7
+ display: block;
8
+ }
9
+
10
+ /* Palette container */
11
+ color-palette .palette {
12
+ display: flex;
13
+ flex-wrap: wrap;
14
+ gap: var(--size-xs, 0.5rem);
15
+ }
16
+
17
+ color-palette .palette.grid {
18
+ display: grid;
19
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
20
+ }
21
+
22
+ color-palette .palette.list {
23
+ flex-direction: column;
24
+ }
25
+
26
+ color-palette .palette.list .swatch-wrap {
27
+ flex-direction: row;
28
+ align-items: center;
29
+ gap: var(--size-s, 0.75rem);
30
+ }
31
+
32
+ color-palette .palette.list .color-box {
33
+ inline-size: 36px !important;
34
+ block-size: 36px !important;
35
+ }
36
+
37
+ /* Swatch wrapper — vertical stack of color box + name */
38
+ color-palette .swatch-wrap {
39
+ display: flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ gap: var(--size-2xs, 0.25rem);
43
+ }
44
+
45
+ /* Color box — the clickable swatch */
46
+ color-palette .color-box {
47
+ position: relative;
48
+ border: 1px solid oklch(0% 0 0 / 0.15);
49
+ border-radius: var(--radius-s, 0.25rem);
50
+ cursor: pointer;
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ padding: 0;
55
+ font-family: var(--font-mono, monospace);
56
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
57
+ overflow: hidden;
58
+ }
59
+
60
+ color-palette .color-box:hover {
61
+ transform: translateY(-2px);
62
+ box-shadow: 0 4px 8px oklch(0% 0 0 / 0.15);
63
+ z-index: 1;
64
+ }
65
+
66
+ color-palette .color-box:focus-visible {
67
+ outline: 2px solid var(--color-focus-ring, currentColor);
68
+ outline-offset: 2px;
69
+ }
70
+
71
+ /* Hex/value overlay — hidden by default, visible on hover */
72
+ color-palette .color-value {
73
+ font-size: 0.625rem;
74
+ line-height: 1.2;
75
+ opacity: 0;
76
+ transition: opacity 0.15s ease;
77
+ text-align: center;
78
+ padding: 2px 4px;
79
+ word-break: break-all;
80
+ }
81
+
82
+ color-palette .color-box:hover .color-value,
83
+ color-palette .color-box:focus-visible .color-value,
84
+ color-palette .color-value.visible {
85
+ opacity: 1;
86
+ }
87
+
88
+ /* "Copied!" feedback overlay */
89
+ color-palette .color-box.copied::after {
90
+ content: 'Copied!';
91
+ position: absolute;
92
+ inset: 0;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ background: oklch(0% 0 0 / 0.6);
97
+ color: white;
98
+ font-size: var(--font-size-xs, 0.75rem);
99
+ font-weight: 600;
100
+ font-family: var(--font-sans, system-ui);
101
+ border-radius: inherit;
102
+ }
103
+
104
+ /* Name label below the swatch */
105
+ color-palette .color-name {
106
+ font-size: var(--font-size-xs, 0.75rem);
107
+ color: var(--color-text-muted, #666);
108
+ text-align: center;
109
+ max-inline-size: 100%;
110
+ overflow: hidden;
111
+ text-overflow: ellipsis;
112
+ white-space: nowrap;
113
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Color conversion utilities for color-picker.
3
+ * All functions are pure — no side effects or DOM access.
4
+ */
5
+
6
+ /**
7
+ * HSL (h: 0-360, s: 0-100, l: 0-100) → RGB (r,g,b: 0-255)
8
+ */
9
+ export function hslToRgb(h, s, l) {
10
+ s /= 100;
11
+ l /= 100;
12
+ const k = (n) => (n + h / 30) % 12;
13
+ const a = s * Math.min(l, 1 - l);
14
+ const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
15
+ return {
16
+ r: Math.round(f(0) * 255),
17
+ g: Math.round(f(8) * 255),
18
+ b: Math.round(f(4) * 255),
19
+ };
20
+ }
21
+
22
+ /**
23
+ * RGB (r,g,b: 0-255) → HSL (h: 0-360, s: 0-100, l: 0-100)
24
+ */
25
+ export function rgbToHsl(r, g, b) {
26
+ r /= 255;
27
+ g /= 255;
28
+ b /= 255;
29
+ const max = Math.max(r, g, b);
30
+ const min = Math.min(r, g, b);
31
+ const l = (max + min) / 2;
32
+ let h = 0;
33
+ let s = 0;
34
+
35
+ if (max !== min) {
36
+ const d = max - min;
37
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
38
+ switch (max) {
39
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
40
+ case g: h = ((b - r) / d + 2) / 6; break;
41
+ case b: h = ((r - g) / d + 4) / 6; break;
42
+ }
43
+ }
44
+
45
+ return {
46
+ h: Math.round(h * 360),
47
+ s: Math.round(s * 100),
48
+ l: Math.round(l * 100),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Hex string → RGB
54
+ */
55
+ export function hexToRgb(hex) {
56
+ hex = hex.replace(/^#/, '');
57
+ if (hex.length === 3) {
58
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
59
+ }
60
+ const n = parseInt(hex, 16);
61
+ return {
62
+ r: (n >> 16) & 255,
63
+ g: (n >> 8) & 255,
64
+ b: n & 255,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * RGB → Hex string (with #)
70
+ */
71
+ export function rgbToHex(r, g, b) {
72
+ return '#' + [r, g, b].map(v =>
73
+ Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')
74
+ ).join('');
75
+ }
76
+
77
+ /**
78
+ * Hex → HSL
79
+ */
80
+ export function hexToHsl(hex) {
81
+ const { r, g, b } = hexToRgb(hex);
82
+ return rgbToHsl(r, g, b);
83
+ }
84
+
85
+ /**
86
+ * HSL → Hex
87
+ */
88
+ export function hslToHex(h, s, l) {
89
+ const { r, g, b } = hslToRgb(h, s, l);
90
+ return rgbToHex(r, g, b);
91
+ }
92
+
93
+ /**
94
+ * Clamp a value between min and max
95
+ */
96
+ export function clamp(val, min, max) {
97
+ return Math.max(min, Math.min(max, val));
98
+ }
99
+
100
+ // ── OkLCH conversions ─────────────────────────────────────────────────
101
+ // Algorithm: Björn Ottosson — https://bottosson.github.io/posts/oklab/
102
+ // Pipeline: sRGB ↔ linear sRGB ↔ OkLab ↔ OkLCH
103
+
104
+ function srgbToLinear(c) {
105
+ c /= 255;
106
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
107
+ }
108
+
109
+ function linearToSrgb(c) {
110
+ const v = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
111
+ return Math.round(clamp(v, 0, 1) * 255);
112
+ }
113
+
114
+ /**
115
+ * RGB (0-255) → OkLCH (L: 0-1, C: 0+, H: 0-360)
116
+ */
117
+ export function rgbToOklch(r, g, b) {
118
+ const lr = srgbToLinear(r);
119
+ const lg = srgbToLinear(g);
120
+ const lb = srgbToLinear(b);
121
+
122
+ const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
123
+ const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
124
+ const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
125
+
126
+ const l_ = Math.cbrt(l);
127
+ const m_ = Math.cbrt(m);
128
+ const s_ = Math.cbrt(s);
129
+
130
+ const L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
131
+ const a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
132
+ const bLab = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
133
+
134
+ const C = Math.sqrt(a * a + bLab * bLab);
135
+ let H = Math.atan2(bLab, a) * 180 / Math.PI;
136
+ if (H < 0) H += 360;
137
+
138
+ // Achromatic colors collapse hue (atan2 noise on tiny chroma is meaningless).
139
+ if (C < 1e-4) H = 0;
140
+
141
+ return { L, C, H };
142
+ }
143
+
144
+ /**
145
+ * OkLCH → RGB (0-255). Out-of-gamut colors are clamped to sRGB.
146
+ */
147
+ export function oklchToRgb(L, C, H) {
148
+ const rad = H * Math.PI / 180;
149
+ const a = C * Math.cos(rad);
150
+ const bLab = C * Math.sin(rad);
151
+
152
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * bLab;
153
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * bLab;
154
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * bLab;
155
+
156
+ const l = l_ ** 3;
157
+ const m = m_ ** 3;
158
+ const s = s_ ** 3;
159
+
160
+ const lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
161
+ const lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
162
+ const lb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
163
+
164
+ return { r: linearToSrgb(lr), g: linearToSrgb(lg), b: linearToSrgb(lb) };
165
+ }
166
+
167
+ export function hslToOklch(h, s, l) {
168
+ const rgb = hslToRgb(h, s, l);
169
+ return rgbToOklch(rgb.r, rgb.g, rgb.b);
170
+ }
171
+
172
+ export function oklchToHsl(L, C, H) {
173
+ const { r, g, b } = oklchToRgb(L, C, H);
174
+ return rgbToHsl(r, g, b);
175
+ }
176
+
177
+ // ── CSS color-string formatters ───────────────────────────────────────
178
+ // Modern CSS Color 4 syntax (space-separated, no commas, % for percents).
179
+
180
+ export function formatHex(h, s, l) {
181
+ return hslToHex(h, s, l);
182
+ }
183
+
184
+ export function formatRgb(h, s, l) {
185
+ const { r, g, b } = hslToRgb(h, s, l);
186
+ return `rgb(${r} ${g} ${b})`;
187
+ }
188
+
189
+ export function formatHsl(h, s, l) {
190
+ return `hsl(${h}deg ${s}% ${l}%)`;
191
+ }
192
+
193
+ export function formatOklch(h, s, l) {
194
+ const { L, C, H } = hslToOklch(h, s, l);
195
+ return `oklch(${(L * 100).toFixed(1)}% ${C.toFixed(3)} ${H.toFixed(1)}deg)`;
196
+ }
197
+
198
+ /**
199
+ * Format the current HSL state as the requested CSS color string.
200
+ * @param {'hex'|'rgb'|'hsl'|'oklch'} format
201
+ */
202
+ export function formatColor(format, h, s, l) {
203
+ switch (format) {
204
+ case 'rgb': return formatRgb(h, s, l);
205
+ case 'hsl': return formatHsl(h, s, l);
206
+ case 'oklch': return formatOklch(h, s, l);
207
+ case 'hex':
208
+ default: return formatHex(h, s, l);
209
+ }
210
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "../../../schemas/api.schema.json",
3
+ "element": "component-sampler",
4
+ "type": "web-component",
5
+ "description": "Themed UI element preview grid. Renders native HTML elements styled by the active VB theme.",
6
+ "htmlvalidate": {
7
+ "flow": true,
8
+ "permittedContent": ["@flow"]
9
+ },
10
+ "attributes": [
11
+ { "name": "components", "kind": "host-api", "purpose": "config", "type": "string", "default": "button,input,select,checkbox,radio,badge,progress", "description": "Comma-separated component types to render" },
12
+ { "name": "label", "kind": "host-api", "purpose": "config", "type": "string", "description": "Optional heading label" },
13
+ { "name": "compact", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Reduced spacing variant" }
14
+ ]
15
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * component-sampler: Themed UI element preview grid
3
+ *
4
+ * Renders a grid of native HTML elements (buttons, inputs, selects, etc.)
5
+ * styled by VB's cascade. Theme changes propagate automatically via CSS
6
+ * custom properties — no re-render needed.
7
+ *
8
+ * @attr {string} components - Comma-separated types to render
9
+ * (default: "button,input,select,checkbox,radio,badge,progress")
10
+ * @attr {string} label - Optional heading
11
+ * @attr {boolean} compact - Reduced spacing
12
+ *
13
+ * @example
14
+ * <component-sampler></component-sampler>
15
+ * <component-sampler components="button,badge,progress" label="Key Components"></component-sampler>
16
+ */
17
+ import { registerComponent } from '../../lib/bundle-registry.js';
18
+ import { VBElement } from '../../lib/vb-element.js';
19
+
20
+ const COMPONENT_RENDERERS = {
21
+ button: () => `
22
+ <layout-cluster data-layout-gap="s">
23
+ <button>Primary</button>
24
+ <button class="secondary">Secondary</button>
25
+ <button disabled>Disabled</button>
26
+ </layout-cluster>`,
27
+
28
+ input: () => `
29
+ <layout-stack data-layout-gap="xs">
30
+ <input type="text" placeholder="Text input" aria-label="Sample text input"/>
31
+ <input type="email" placeholder="email@example.com" aria-label="Sample email"/>
32
+ <input type="text" value="Read only" readonly aria-label="Read-only input"/>
33
+ </layout-stack>`,
34
+
35
+ select: () => `
36
+ <select aria-label="Sample select">
37
+ <option>Choose an option</option>
38
+ <option>Option A</option>
39
+ <option>Option B</option>
40
+ <option>Option C</option>
41
+ </select>`,
42
+
43
+ checkbox: () => `
44
+ <layout-stack data-layout-gap="xs">
45
+ <label><input type="checkbox" checked="checked"/> Checked option</label>
46
+ <label><input type="checkbox"/> Unchecked option</label>
47
+ <label><input type="checkbox" disabled/> Disabled option</label>
48
+ </layout-stack>`,
49
+
50
+ radio: () => `
51
+ <layout-stack data-layout-gap="xs">
52
+ <label><input type="radio" name="sampler-radio" checked="checked"/> Selected</label>
53
+ <label><input type="radio" name="sampler-radio"/> Unselected</label>
54
+ <label><input type="radio" name="sampler-radio" disabled/> Disabled</label>
55
+ </layout-stack>`,
56
+
57
+ badge: () => `
58
+ <layout-cluster data-layout-gap="xs">
59
+ <layout-badge>Default</layout-badge>
60
+ <layout-badge data-color="success">Success</layout-badge>
61
+ <layout-badge data-color="warning">Warning</layout-badge>
62
+ <layout-badge data-color="danger">Danger</layout-badge>
63
+ <layout-badge data-color="info">Info</layout-badge>
64
+ </layout-cluster>`,
65
+
66
+ progress: () => `
67
+ <layout-stack data-layout-gap="xs">
68
+ <progress value="33" max="100">33%</progress>
69
+ <progress value="66" max="100">66%</progress>
70
+ <progress value="100" max="100">100%</progress>
71
+ </layout-stack>`,
72
+
73
+ range: () => `
74
+ <input type="range" min="0" max="100" value="50" aria-label="Sample range"/>`,
75
+
76
+ textarea: () => `
77
+ <textarea rows="3" placeholder="Textarea sample" aria-label="Sample textarea" style="width:100%"></textarea>`,
78
+ };
79
+
80
+ class ComponentSampler extends VBElement {
81
+ static observedAttributes = ['components', 'label', 'compact'];
82
+
83
+ setup() { this.#render(); }
84
+
85
+ attributeChangedCallback() {
86
+ if (this.isConnected) this.#render();
87
+ }
88
+
89
+ #render() {
90
+ const componentsAttr = this.getAttribute('components')
91
+ || 'button,input,select,checkbox,radio,badge,progress';
92
+ const label = this.getAttribute('label') || '';
93
+ const compact = this.hasAttribute('compact');
94
+ const components = componentsAttr.split(',').map(c => c.trim());
95
+
96
+ const gap = compact ? '0.75rem' : '1.25rem';
97
+ let html = '';
98
+
99
+ if (label) {
100
+ html += `<p style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:var(--color-text-muted,#666);margin-block-end:0.75rem;font-family:var(--font-sans,system-ui)">${label}</p>`;
101
+ }
102
+
103
+ html += `<section style="display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:${gap}">`;
104
+
105
+ for (const name of components) {
106
+ const renderer = COMPONENT_RENDERERS[name];
107
+ if (!renderer) continue;
108
+
109
+ html += `<article style="border:1px solid var(--color-border,#ddd);border-radius:var(--radius-m,0.5rem);padding:var(--size-m,1rem);background:var(--color-surface,#fff)">
110
+ <p style="font-size:0.625rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:var(--color-text-muted,#999);margin-block-end:var(--size-s,0.5rem);font-family:var(--font-sans,system-ui)">${name}</p>
111
+ ${renderer()}
112
+ </article>`;
113
+ }
114
+
115
+ html += '</section>';
116
+ this.innerHTML = html;
117
+ }
118
+ }
119
+
120
+ registerComponent('component-sampler', ComponentSampler);
@@ -0,0 +1,47 @@
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>Component Sampler (Static Fallback)</title>
7
+ <style>
8
+ /*
9
+ Minimal baseline styling for the no-JS fallback.
10
+ A production page would load the project stylesheet instead;
11
+ these rules exist only so the file is self-contained.
12
+ */
13
+ *, *::before, *::after { box-sizing: border-box; }
14
+
15
+ body {
16
+ font-family: system-ui, sans-serif;
17
+ line-height: 1.5;
18
+ max-inline-size: 40rem;
19
+ margin-inline: auto;
20
+ padding: 2rem 1rem;
21
+ }
22
+ </style>
23
+ </head>
24
+ <body>
25
+
26
+ <!--
27
+ Upgrade relationship
28
+ ====================
29
+ This static HTML IS the baseline that <component-sampler> enhances.
30
+
31
+ When JavaScript loads and the <component-sampler> custom element is
32
+ defined, the markup is progressively enhanced with additional
33
+ interactivity, ARIA attributes, and keyboard navigation.
34
+
35
+ Without JS the core content remains accessible and functional.
36
+ -->
37
+
38
+ <h1>Component Sampler</h1>
39
+
40
+ <noscript>
41
+ <p>This component works best with JavaScript enabled.</p>
42
+ </noscript>
43
+
44
+ <!-- TODO: Add meaningful static fallback markup for <component-sampler> -->
45
+
46
+ </body>
47
+ </html>
@@ -0,0 +1,9 @@
1
+ /* component-sampler: themed UI element preview grid */
2
+
3
+ component-sampler:not(:defined) {
4
+ display: block;
5
+ }
6
+
7
+ component-sampler {
8
+ display: block;
9
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Curated font list and pairing suggestions for the font-pairer component.
3
+ * Each font includes metadata for categorization and pairing logic.
4
+ */
5
+
6
+ /**
7
+ * @typedef {{ name: string, category: string, weights: string, style: string }} FontEntry
8
+ */
9
+
10
+ /** @type {FontEntry[]} */
11
+ export const FONTS = [
12
+ // Serif
13
+ { name: 'Playfair Display', category: 'serif', weights: '400;600;700;900', style: 'elegant' },
14
+ { name: 'Merriweather', category: 'serif', weights: '300;400;700;900', style: 'readable' },
15
+ { name: 'Lora', category: 'serif', weights: '400;500;600;700', style: 'elegant' },
16
+ { name: 'Source Serif 4', category: 'serif', weights: '300;400;600;700', style: 'neutral' },
17
+ { name: 'Libre Baskerville',category: 'serif', weights: '400;700', style: 'classic' },
18
+ { name: 'Crimson Text', category: 'serif', weights: '400;600;700', style: 'classic' },
19
+ { name: 'DM Serif Display', category: 'serif', weights: '400', style: 'bold' },
20
+ { name: 'Cormorant Garamond', category: 'serif', weights: '300;400;500;600;700', style: 'elegant' },
21
+
22
+ // Sans-Serif
23
+ { name: 'Inter', category: 'sans-serif', weights: '300;400;500;600;700', style: 'neutral' },
24
+ { name: 'Open Sans', category: 'sans-serif', weights: '300;400;600;700', style: 'friendly' },
25
+ { name: 'Roboto', category: 'sans-serif', weights: '300;400;500;700', style: 'neutral' },
26
+ { name: 'Lato', category: 'sans-serif', weights: '300;400;700;900', style: 'warm' },
27
+ { name: 'Nunito', category: 'sans-serif', weights: '300;400;600;700', style: 'friendly' },
28
+ { name: 'Work Sans', category: 'sans-serif', weights: '300;400;500;600;700', style: 'geometric' },
29
+ { name: 'DM Sans', category: 'sans-serif', weights: '400;500;700', style: 'geometric' },
30
+ { name: 'Plus Jakarta Sans',category: 'sans-serif', weights: '300;400;500;600;700', style: 'modern' },
31
+ { name: 'Space Grotesk', category: 'sans-serif', weights: '300;400;500;600;700', style: 'technical' },
32
+ { name: 'Manrope', category: 'sans-serif', weights: '300;400;500;600;700', style: 'geometric' },
33
+ { name: 'IBM Plex Sans', category: 'sans-serif', weights: '300;400;500;600', style: 'technical' },
34
+ { name: 'Outfit', category: 'sans-serif', weights: '300;400;500;600;700', style: 'modern' },
35
+
36
+ // Display
37
+ { name: 'Bebas Neue', category: 'display', weights: '400', style: 'bold' },
38
+ { name: 'Abril Fatface', category: 'display', weights: '400', style: 'elegant' },
39
+ { name: 'Oswald', category: 'display', weights: '300;400;500;600;700', style: 'condensed' },
40
+ { name: 'Sora', category: 'display', weights: '300;400;500;600;700', style: 'modern' },
41
+ { name: 'Fraunces', category: 'display', weights: '300;400;500;600;700;900', style: 'elegant' },
42
+
43
+ // Monospace
44
+ { name: 'JetBrains Mono', category: 'monospace', weights: '400;500;600;700', style: 'technical' },
45
+ { name: 'Fira Code', category: 'monospace', weights: '300;400;500;600;700', style: 'technical' },
46
+ { name: 'IBM Plex Mono', category: 'monospace', weights: '400;500;600', style: 'technical' },
47
+ { name: 'Source Code Pro', category: 'monospace', weights: '300;400;500;600;700', style: 'neutral' },
48
+ ];
49
+
50
+ /** Curated pairings: [heading, body] */
51
+ export const SUGGESTED_PAIRINGS = [
52
+ ['Playfair Display', 'Inter'],
53
+ ['DM Serif Display', 'DM Sans'],
54
+ ['Fraunces', 'Plus Jakarta Sans'],
55
+ ['Lora', 'Open Sans'],
56
+ ['Merriweather', 'Roboto'],
57
+ ['Oswald', 'Lato'],
58
+ ['Cormorant Garamond', 'Work Sans'],
59
+ ['Sora', 'Inter'],
60
+ ['Abril Fatface', 'Nunito'],
61
+ ['Source Serif 4', 'Source Code Pro'],
62
+ ['Space Grotesk', 'IBM Plex Sans'],
63
+ ['Outfit', 'Manrope'],
64
+ ];
65
+
66
+ /**
67
+ * Build a Google Fonts URL for a font.
68
+ * @param {string} family
69
+ * @param {string} weights
70
+ * @returns {string}
71
+ */
72
+ export function googleFontsUrl(family, weights = '400;700') {
73
+ const encoded = family.replace(/\s+/g, '+');
74
+ return `https://fonts.googleapis.com/css2?family=${encoded}:wght@${weights}&display=swap`;
75
+ }
76
+
77
+ /**
78
+ * Load a Google Font by injecting a <link> tag (deduplicates).
79
+ * @param {string} family
80
+ * @param {string} weights
81
+ * @returns {Promise<void>}
82
+ */
83
+ export async function loadFont(family, weights = '400;700') {
84
+ const id = `gf-${family.replace(/\s+/g, '-').toLowerCase()}`;
85
+ if (document.getElementById(id)) {
86
+ await document.fonts.ready;
87
+ return;
88
+ }
89
+ const link = document.createElement('link');
90
+ link.id = id;
91
+ link.rel = 'stylesheet';
92
+ link.href = googleFontsUrl(family, weights);
93
+ document.head.appendChild(link);
94
+ await document.fonts.ready;
95
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "../../../schemas/api.schema.json",
3
+ "element": "font-pairer",
4
+ "type": "web-component",
5
+ "description": "Interactive font pairing tool with Google Fonts loading and CSS export",
6
+ "attributes": [
7
+ {
8
+ "name": "heading-font",
9
+ "kind": "host-api",
10
+ "purpose": "config",
11
+ "type": "string",
12
+ "description": "Google Fonts family name for headings"
13
+ },
14
+ {
15
+ "name": "body-font",
16
+ "kind": "host-api",
17
+ "purpose": "config",
18
+ "type": "string",
19
+ "description": "Google Fonts family name for body text"
20
+ },
21
+ {
22
+ "name": "sample",
23
+ "kind": "host-api",
24
+ "purpose": "config",
25
+ "type": "string",
26
+ "description": "Custom sample heading text"
27
+ },
28
+ {
29
+ "name": "show-export",
30
+ "kind": "host-api",
31
+ "purpose": "config",
32
+ "type": "boolean",
33
+ "description": "Show Copy CSS / Copy @import toolbar"
34
+ },
35
+ {
36
+ "name": "show-suggestions",
37
+ "kind": "host-api",
38
+ "purpose": "config",
39
+ "type": "boolean",
40
+ "description": "Show curated pairing suggestions"
41
+ }
42
+ ],
43
+ "events": [
44
+ {
45
+ "name": "font-pairer:change"
46
+ }
47
+ ],
48
+ "childAttributes": [],
49
+ "structure": [],
50
+ "htmlvalidate": {
51
+ "flow": true,
52
+ "permittedContent": [
53
+ "@flow"
54
+ ]
55
+ }
56
+ }