@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.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/vb-design-system.css +993 -0
- package/dist/vb-design-system.js +4482 -0
- package/dist/vb-design-system.min.css +1 -0
- package/dist/vb-design-system.min.js +513 -0
- package/package.json +78 -0
- package/src/index.css +21 -0
- package/src/index.js +24 -0
- package/src/lib/bundle-registry.js +156 -0
- package/src/lib/vb-element.js +96 -0
- package/src/patterns/do-dont.css +86 -0
- package/src/patterns/token-table.css +67 -0
- package/src/utils/copy-init.js +287 -0
- package/src/web-components/accessibility-specimen/api.json +51 -0
- package/src/web-components/accessibility-specimen/logic.js +232 -0
- package/src/web-components/accessibility-specimen/static.html +41 -0
- package/src/web-components/accessibility-specimen/styles.css +156 -0
- package/src/web-components/brand-specimen/api.json +37 -0
- package/src/web-components/brand-specimen/logic.js +61 -0
- package/src/web-components/brand-specimen/static.html +77 -0
- package/src/web-components/brand-specimen/styles.css +10 -0
- package/src/web-components/breakpoint-specimen/api.json +44 -0
- package/src/web-components/breakpoint-specimen/logic.js +186 -0
- package/src/web-components/breakpoint-specimen/static.html +38 -0
- package/src/web-components/breakpoint-specimen/styles.css +180 -0
- package/src/web-components/color-palette/api.json +73 -0
- package/src/web-components/color-palette/logic.js +280 -0
- package/src/web-components/color-palette/static.html +54 -0
- package/src/web-components/color-palette/styles.css +113 -0
- package/src/web-components/color-picker/_color-utils.js +210 -0
- package/src/web-components/component-sampler/api.json +15 -0
- package/src/web-components/component-sampler/logic.js +120 -0
- package/src/web-components/component-sampler/static.html +47 -0
- package/src/web-components/component-sampler/styles.css +9 -0
- package/src/web-components/font-pairer/_font-data.js +95 -0
- package/src/web-components/font-pairer/api.json +56 -0
- package/src/web-components/font-pairer/logic.js +401 -0
- package/src/web-components/font-pairer/static.html +9 -0
- package/src/web-components/font-pairer/styles.css +10 -0
- package/src/web-components/gradient-builder/_gradient-utils.js +54 -0
- package/src/web-components/gradient-builder/api.json +63 -0
- package/src/web-components/gradient-builder/logic.js +261 -0
- package/src/web-components/gradient-builder/static.html +9 -0
- package/src/web-components/gradient-builder/styles.css +10 -0
- package/src/web-components/icon-set/api.json +39 -0
- package/src/web-components/icon-set/logic.js +133 -0
- package/src/web-components/icon-set/static.html +36 -0
- package/src/web-components/icon-set/styles.css +82 -0
- package/src/web-components/icon-specimen/api.json +46 -0
- package/src/web-components/icon-specimen/logic.js +67 -0
- package/src/web-components/icon-specimen/static.html +39 -0
- package/src/web-components/icon-specimen/styles.css +38 -0
- package/src/web-components/layout-specimen/api.json +12 -0
- package/src/web-components/layout-specimen/logic.js +219 -0
- package/src/web-components/layout-specimen/static.html +22 -0
- package/src/web-components/layout-specimen/styles.css +95 -0
- package/src/web-components/motion-specimen/api.json +54 -0
- package/src/web-components/motion-specimen/logic.js +146 -0
- package/src/web-components/motion-specimen/static.html +13 -0
- package/src/web-components/motion-specimen/styles.css +29 -0
- package/src/web-components/palette-generator/_palette-utils.js +133 -0
- package/src/web-components/palette-generator/api.json +79 -0
- package/src/web-components/palette-generator/logic.js +288 -0
- package/src/web-components/palette-generator/static.html +47 -0
- package/src/web-components/palette-generator/styles.css +36 -0
- package/src/web-components/semantic-palette/_color-math.js +149 -0
- package/src/web-components/semantic-palette/api.json +41 -0
- package/src/web-components/semantic-palette/logic.js +400 -0
- package/src/web-components/semantic-palette/static.html +42 -0
- package/src/web-components/semantic-palette/styles.css +20 -0
- package/src/web-components/spacing-specimen/api.json +63 -0
- package/src/web-components/spacing-specimen/logic.js +132 -0
- package/src/web-components/spacing-specimen/static.html +71 -0
- package/src/web-components/spacing-specimen/styles.css +58 -0
- package/src/web-components/theme-catalog/api.json +14 -0
- package/src/web-components/theme-catalog/logic.js +228 -0
- package/src/web-components/theme-catalog/static.html +15 -0
- package/src/web-components/theme-export/api.json +18 -0
- package/src/web-components/theme-export/dtcg-serialize.js +417 -0
- package/src/web-components/theme-export/logic.js +201 -0
- package/src/web-components/theme-export/static.html +12 -0
- package/src/web-components/theme-export/theme-css-parse.js +93 -0
- package/src/web-components/theme-import/api.json +14 -0
- package/src/web-components/theme-import/catalog/atlassian.tokens.json +303 -0
- package/src/web-components/theme-import/catalog/bootstrap.tokens.json +304 -0
- package/src/web-components/theme-import/catalog/carbon.tokens.json +301 -0
- package/src/web-components/theme-import/catalog/catppuccin-mocha.tokens.json +301 -0
- package/src/web-components/theme-import/catalog/govuk.tokens.json +301 -0
- package/src/web-components/theme-import/catalog/manifest.json +132 -0
- package/src/web-components/theme-import/catalog/material.tokens.json +289 -0
- package/src/web-components/theme-import/catalog/salesforce.tokens.json +301 -0
- package/src/web-components/theme-import/catalog/tailwind.tokens.json +302 -0
- package/src/web-components/theme-import/dtcg-deserialize.js +322 -0
- package/src/web-components/theme-import/logic.js +263 -0
- package/src/web-components/theme-import/static.html +15 -0
- package/src/web-components/token-specimen/api.json +24 -0
- package/src/web-components/token-specimen/logic.js +256 -0
- package/src/web-components/token-specimen/static.html +47 -0
- package/src/web-components/token-specimen/styles.css +10 -0
- package/src/web-components/type-specimen/api.json +91 -0
- package/src/web-components/type-specimen/logic.js +156 -0
- package/src/web-components/type-specimen/static.html +84 -0
- package/src/web-components/type-specimen/styles.css +131 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* token-specimen: Unified design token scale display
|
|
3
|
+
*
|
|
4
|
+
* One component for multiple token types — shadows, radii, borders,
|
|
5
|
+
* colors, and sizes. The `type` attribute controls rendering while
|
|
6
|
+
* sharing the same core API (tokens, prefix, show-values, label).
|
|
7
|
+
*
|
|
8
|
+
* Reads CSS custom properties and renders a visual specimen with
|
|
9
|
+
* computed values after paint. All layout is inline so the component
|
|
10
|
+
* works without external CSS.
|
|
11
|
+
*
|
|
12
|
+
* @attr {string} type - Token type: "shadow", "radius", "border", "color", "size", "icon"
|
|
13
|
+
* @attr {string} tokens - Comma-separated token names (defaults vary by type)
|
|
14
|
+
* @attr {string} prefix - CSS variable prefix (auto-set from type if omitted)
|
|
15
|
+
* @attr {boolean} show-values - Show computed values (default: true)
|
|
16
|
+
* @attr {string} label - Optional heading label
|
|
17
|
+
* @attr {string} size - Icon size (icon type only): xs, sm, md, lg, xl, 2xl
|
|
18
|
+
* @attr {string} icon-set - Icon set (icon type only, default: "lucide")
|
|
19
|
+
* @attr {boolean} editable - Turn value cells into inputs that write the token on target scope
|
|
20
|
+
* @attr {string} target - CSS selector for the element to receive token overrides (default: ":root")
|
|
21
|
+
*
|
|
22
|
+
* @fires token-specimen:change - When a token is edited. detail: { name, value, token, target }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <token-specimen type="shadow"></token-specimen>
|
|
26
|
+
* <token-specimen type="radius" tokens="s,m,l,xl,full"></token-specimen>
|
|
27
|
+
* <token-specimen type="icon" tokens="chevron-right,check,x,search"></token-specimen>
|
|
28
|
+
* <token-specimen type="color" editable></token-specimen>
|
|
29
|
+
*/
|
|
30
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
31
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
32
|
+
|
|
33
|
+
const TYPE_DEFAULTS = {
|
|
34
|
+
shadow: {
|
|
35
|
+
prefix: '--shadow-',
|
|
36
|
+
tokens: 'xs,s,m,l,xl,2xl',
|
|
37
|
+
},
|
|
38
|
+
radius: {
|
|
39
|
+
prefix: '--radius-',
|
|
40
|
+
tokens: 'xs,s,m,l,xl,2xl,full',
|
|
41
|
+
},
|
|
42
|
+
border: {
|
|
43
|
+
prefix: '--border-width-',
|
|
44
|
+
tokens: 'thin,medium,thick',
|
|
45
|
+
},
|
|
46
|
+
color: {
|
|
47
|
+
prefix: '--color-',
|
|
48
|
+
tokens: 'primary,secondary,accent,success,warning,error,info',
|
|
49
|
+
},
|
|
50
|
+
size: {
|
|
51
|
+
prefix: '--size-',
|
|
52
|
+
tokens: '3xs,2xs,xs,s,m,l,xl,2xl,3xl',
|
|
53
|
+
},
|
|
54
|
+
icon: {
|
|
55
|
+
prefix: '',
|
|
56
|
+
tokens: 'home,search,settings,user,bell,heart,star,check,x,chevron-right,menu,trash',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const RENDERERS = {
|
|
61
|
+
shadow: renderShadow,
|
|
62
|
+
radius: renderRadius,
|
|
63
|
+
border: renderBorder,
|
|
64
|
+
color: renderColor,
|
|
65
|
+
size: renderSize,
|
|
66
|
+
icon: renderIcon,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
class TokenSpecimen extends VBElement {
|
|
70
|
+
static observedAttributes = ['type', 'tokens', 'prefix', 'show-values', 'label', 'size', 'icon-set', 'editable', 'target'];
|
|
71
|
+
|
|
72
|
+
setup() { this.#render(); }
|
|
73
|
+
|
|
74
|
+
attributeChangedCallback() {
|
|
75
|
+
if (this.isConnected) this.#render();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#render() {
|
|
79
|
+
const type = this.getAttribute('type') || 'shadow';
|
|
80
|
+
const defaults = TYPE_DEFAULTS[type] || TYPE_DEFAULTS.shadow;
|
|
81
|
+
const prefix = this.getAttribute('prefix') || defaults.prefix;
|
|
82
|
+
const tokensAttr = this.getAttribute('tokens') || defaults.tokens;
|
|
83
|
+
const showValues = this.getAttribute('show-values') !== 'false';
|
|
84
|
+
const label = this.getAttribute('label') || '';
|
|
85
|
+
const tokens = tokensAttr.split(',').map(t => t.trim());
|
|
86
|
+
|
|
87
|
+
const renderer = RENDERERS[type] || RENDERERS.shadow;
|
|
88
|
+
let html = '';
|
|
89
|
+
|
|
90
|
+
if (label) {
|
|
91
|
+
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>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
html += renderer(tokens, prefix, showValues, this);
|
|
95
|
+
this.innerHTML = html;
|
|
96
|
+
|
|
97
|
+
const editable = this.hasAttribute('editable') && type !== 'icon';
|
|
98
|
+
|
|
99
|
+
if (showValues && type !== 'icon') {
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
this.#readComputedValues(type, prefix, tokens);
|
|
102
|
+
if (editable) this.#injectEditors(type, prefix);
|
|
103
|
+
});
|
|
104
|
+
} else if (editable) {
|
|
105
|
+
requestAnimationFrame(() => this.#injectEditors(type, prefix));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#injectEditors(type, prefix) {
|
|
110
|
+
const cs = getComputedStyle(this);
|
|
111
|
+
this.querySelectorAll('[data-token-value]').forEach((node) => {
|
|
112
|
+
const el = /** @type {HTMLElement} */ (node);
|
|
113
|
+
const name = el.dataset.tokenValue;
|
|
114
|
+
const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
|
|
115
|
+
const input = document.createElement('input');
|
|
116
|
+
input.type = type === 'color' && /^#[0-9a-f]{3,8}$/i.test(raw) ? 'color' : 'text';
|
|
117
|
+
input.value = raw || '';
|
|
118
|
+
input.className = 'ts-edit';
|
|
119
|
+
input.setAttribute('data-token', name || '');
|
|
120
|
+
input.setAttribute('aria-label', `${name} value`);
|
|
121
|
+
input.style.cssText = `font-family:var(--font-mono,monospace);font-size:0.625rem;padding:0.125rem 0.25rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface,#fff);color:var(--color-text,#222);inline-size:${type === 'color' ? '3rem' : '100%'};${type === 'color' ? 'block-size:1.5rem;padding:0;' : 'max-inline-size:8rem;'}`;
|
|
122
|
+
el.replaceWith(input);
|
|
123
|
+
this.listen(input, 'change', () => this.#applyEdit(input, prefix));
|
|
124
|
+
this.listen(input, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') this.#applyEdit(input, prefix); });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#applyEdit(input, prefix) {
|
|
129
|
+
const name = input.getAttribute('data-token');
|
|
130
|
+
const value = input.value.trim();
|
|
131
|
+
if (!name || !value) return;
|
|
132
|
+
const target = /** @type {HTMLElement | null} */ (this.#resolveTarget());
|
|
133
|
+
const token = `${prefix}${name}`;
|
|
134
|
+
if (target) target.style.setProperty(token, value);
|
|
135
|
+
this.dispatchEvent(new CustomEvent('token-specimen:change', {
|
|
136
|
+
bubbles: true,
|
|
137
|
+
detail: { name, value, token, target: this.getAttribute('target') || ':root' },
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#resolveTarget() {
|
|
142
|
+
const sel = this.getAttribute('target') || ':root';
|
|
143
|
+
try {
|
|
144
|
+
return sel === ':root' ? document.documentElement : document.querySelector(sel);
|
|
145
|
+
} catch {
|
|
146
|
+
return document.documentElement;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#readComputedValues(type, prefix, tokens) {
|
|
151
|
+
const cs = getComputedStyle(this);
|
|
152
|
+
this.querySelectorAll('[data-token-value]').forEach(node => {
|
|
153
|
+
const el = /** @type {HTMLElement} */ (node);
|
|
154
|
+
const name = el.dataset.tokenValue;
|
|
155
|
+
const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
|
|
156
|
+
if (type === 'radius' || type === 'size') {
|
|
157
|
+
// Show px for length values
|
|
158
|
+
const sample = this.querySelector(`[data-token-sample="${name}"]`);
|
|
159
|
+
if (sample) {
|
|
160
|
+
const prop = type === 'radius' ? 'borderRadius' : 'width';
|
|
161
|
+
const rect = type === 'size' ? sample.getBoundingClientRect().width : null;
|
|
162
|
+
el.textContent = rect != null
|
|
163
|
+
? `${Math.round(rect * 100) / 100}px`
|
|
164
|
+
: raw || '—';
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
el.textContent = raw || '—';
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Type renderers ────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function renderShadow(tokens, prefix, showValues) {
|
|
176
|
+
let html = `<div role="list" style="display:flex;flex-wrap:wrap;gap:1rem;align-items:end">`;
|
|
177
|
+
for (const name of tokens) {
|
|
178
|
+
html += `<div role="listitem" style="text-align:center">
|
|
179
|
+
<div style="width:7rem;height:5rem;background:var(--color-surface,#fff);border-radius:var(--radius-m,0.5rem);box-shadow:var(${prefix}${name})" aria-hidden="true"></div>
|
|
180
|
+
<p style="font-family:var(--font-mono,monospace);font-size:0.75rem;color:var(--color-text-muted,#666);margin-block-start:0.5rem">${name}</p>
|
|
181
|
+
${showValues ? `<p data-token-value="${name}" style="font-family:var(--font-mono,monospace);font-size:0.625rem;color:var(--color-text-muted,#999);max-width:7rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></p>` : ''}
|
|
182
|
+
</div>`;
|
|
183
|
+
}
|
|
184
|
+
html += '</div>';
|
|
185
|
+
return html;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function renderRadius(tokens, prefix, showValues) {
|
|
189
|
+
let html = `<div role="list" style="display:flex;flex-wrap:wrap;gap:1rem;align-items:end">`;
|
|
190
|
+
for (const name of tokens) {
|
|
191
|
+
html += `<div role="listitem" style="text-align:center">
|
|
192
|
+
<div data-token-sample="${name}" style="width:4.5rem;height:4.5rem;background:var(--color-primary,oklch(55% 0.2 260));border-radius:var(${prefix}${name});display:flex;align-items:center;justify-content:center;color:#fff;font-size:0.75rem;font-family:var(--font-mono,monospace)" aria-hidden="true">${name}</div>
|
|
193
|
+
${showValues ? `<p data-token-value="${name}" style="font-family:var(--font-mono,monospace);font-size:0.625rem;color:var(--color-text-muted,#999);margin-block-start:0.25rem"></p>` : ''}
|
|
194
|
+
</div>`;
|
|
195
|
+
}
|
|
196
|
+
html += '</div>';
|
|
197
|
+
return html;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderBorder(tokens, prefix, showValues) {
|
|
201
|
+
let html = `<div role="list" style="display:flex;flex-direction:column;gap:0.75rem">`;
|
|
202
|
+
for (const name of tokens) {
|
|
203
|
+
html += `<div role="listitem" style="display:grid;grid-template-columns:4rem 1fr auto;align-items:center;gap:0.75rem">
|
|
204
|
+
<span style="font-family:var(--font-mono,monospace);font-size:0.875rem;color:var(--color-text-muted,#666);text-align:end">${name}</span>
|
|
205
|
+
<div style="border-block-start:var(${prefix}${name}) solid var(--color-text,#333);min-inline-size:4rem" aria-hidden="true"></div>
|
|
206
|
+
${showValues ? `<span data-token-value="${name}" style="font-family:var(--font-mono,monospace);font-size:0.75rem;color:var(--color-text-muted,#666);font-variant-numeric:tabular-nums"></span>` : ''}
|
|
207
|
+
</div>`;
|
|
208
|
+
}
|
|
209
|
+
html += '</div>';
|
|
210
|
+
return html;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderColor(tokens, prefix, showValues) {
|
|
214
|
+
let html = `<div role="list" style="display:flex;flex-wrap:wrap;gap:0.75rem">`;
|
|
215
|
+
for (const name of tokens) {
|
|
216
|
+
html += `<div role="listitem" style="text-align:center">
|
|
217
|
+
<div style="width:4rem;height:3rem;background:var(${prefix}${name});border-radius:var(--radius-s,0.25rem);border:1px solid var(--color-border,#ddd)" aria-hidden="true"></div>
|
|
218
|
+
<p style="font-family:var(--font-mono,monospace);font-size:0.625rem;color:var(--color-text-muted,#666);margin-block-start:0.25rem">${name}</p>
|
|
219
|
+
${showValues ? `<p data-token-value="${name}" style="font-family:var(--font-mono,monospace);font-size:0.5625rem;color:var(--color-text-muted,#999);max-width:4rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></p>` : ''}
|
|
220
|
+
</div>`;
|
|
221
|
+
}
|
|
222
|
+
html += '</div>';
|
|
223
|
+
return html;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderSize(tokens, prefix, showValues) {
|
|
227
|
+
let html = `<div role="list" style="display:flex;flex-direction:column;gap:0.25rem">`;
|
|
228
|
+
for (const name of tokens) {
|
|
229
|
+
html += `<div role="listitem" style="display:grid;grid-template-columns:3rem 1fr auto;align-items:center;gap:0.75rem;min-block-size:1.75rem">
|
|
230
|
+
<span style="font-family:var(--font-mono,monospace);font-size:0.875rem;color:var(--color-text-muted,#666);text-align:end">${name}</span>
|
|
231
|
+
<div data-token-sample="${name}" style="display:block;block-size:var(--size-m,1rem);min-inline-size:2px;inline-size:var(${prefix}${name});background:var(--color-interactive,oklch(55% 0.2 260));border-radius:var(--radius-s,0.25rem)" aria-hidden="true"></div>
|
|
232
|
+
${showValues ? `<span data-token-value="${name}" style="font-family:var(--font-mono,monospace);font-size:0.75rem;color:var(--color-text-muted,#666);font-variant-numeric:tabular-nums;min-inline-size:3.5rem;text-align:end"></span>` : ''}
|
|
233
|
+
</div>`;
|
|
234
|
+
}
|
|
235
|
+
html += '</div>';
|
|
236
|
+
return html;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderIcon(tokens, _prefix, showValues, host) {
|
|
240
|
+
const size = host?.getAttribute('size') || 'md';
|
|
241
|
+
const iconSet = host?.getAttribute('icon-set') || '';
|
|
242
|
+
const setAttr = iconSet ? ` set="${iconSet}"` : '';
|
|
243
|
+
let html = `<div role="list" style="display:flex;flex-wrap:wrap;gap:1rem">`;
|
|
244
|
+
for (const name of tokens) {
|
|
245
|
+
html += `<div role="listitem" style="display:flex;flex-direction:column;align-items:center;gap:0.375rem;min-inline-size:4.5rem">
|
|
246
|
+
<span style="display:inline-flex;align-items:center;justify-content:center;padding:var(--size-s,0.75rem);background:var(--color-surface-raised,#f5f5f5);border-radius:var(--radius-s,0.25rem);border:1px solid var(--color-border,#ddd);color:var(--color-text,#222)">
|
|
247
|
+
<icon-wc name="${name}" size="${size}"${setAttr}></icon-wc>
|
|
248
|
+
</span>
|
|
249
|
+
${showValues ? `<code style="font-family:var(--font-mono,monospace);font-size:0.625rem;color:var(--color-text-muted,#666);max-inline-size:5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${name}</code>` : ''}
|
|
250
|
+
</div>`;
|
|
251
|
+
}
|
|
252
|
+
html += '</div>';
|
|
253
|
+
return html;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
registerComponent('token-specimen', TokenSpecimen);
|
|
@@ -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>Token Specimen (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 <token-specimen> enhances.
|
|
30
|
+
|
|
31
|
+
When JavaScript loads and the <token-specimen> 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>Token Specimen</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 <token-specimen> -->
|
|
45
|
+
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "type-specimen",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Typography specimen display",
|
|
6
|
+
"htmlvalidate": {
|
|
7
|
+
"flow": true,
|
|
8
|
+
"permittedContent": [
|
|
9
|
+
"@flow"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"attributes": [
|
|
13
|
+
{
|
|
14
|
+
"name": "font-family",
|
|
15
|
+
"kind": "host-api",
|
|
16
|
+
"purpose": "config",
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "CSS font-family value"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "label",
|
|
22
|
+
"kind": "host-api",
|
|
23
|
+
"purpose": "config",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Display name for the font (optional, falls back to font-family)"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "sample",
|
|
29
|
+
"kind": "host-api",
|
|
30
|
+
"purpose": "config",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Custom sample text (default: \"The quick brown fox...\")"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "show-scale",
|
|
36
|
+
"kind": "host-api",
|
|
37
|
+
"purpose": "config",
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"description": "Show VB type scale (xs–5xl)"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "show-weights",
|
|
43
|
+
"kind": "host-api",
|
|
44
|
+
"purpose": "config",
|
|
45
|
+
"type": "boolean",
|
|
46
|
+
"description": "Show weight scale (100–900)"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "show-characters",
|
|
50
|
+
"kind": "host-api",
|
|
51
|
+
"purpose": "config",
|
|
52
|
+
"type": "boolean",
|
|
53
|
+
"description": "Show character grid"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "weights",
|
|
57
|
+
"kind": "host-api",
|
|
58
|
+
"purpose": "config",
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Comma-separated available weights (default: \"400,700\")"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "editable",
|
|
64
|
+
"kind": "host-api",
|
|
65
|
+
"purpose": "config",
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"description": "Turn the font-family label into a live input + quick-picks that write a CSS custom property."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "target",
|
|
71
|
+
"kind": "host-api",
|
|
72
|
+
"purpose": "config",
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "CSS selector for the element to receive token overrides (default: \":root\"). Only used when editable."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "token",
|
|
78
|
+
"kind": "host-api",
|
|
79
|
+
"purpose": "config",
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Token name to write when editing (default: \"font-family-base\"). Only used when editable."
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"events": [
|
|
85
|
+
{
|
|
86
|
+
"name": "type-specimen:change"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"childAttributes": [],
|
|
90
|
+
"structure": []
|
|
91
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* type-specimen: Typography specimen display
|
|
3
|
+
*
|
|
4
|
+
* Renders a font specimen with sample text, character grid, weight scale,
|
|
5
|
+
* and optional type scale display using VB's 9-step system.
|
|
6
|
+
*
|
|
7
|
+
* @attr {string} font-family - CSS font-family value
|
|
8
|
+
* @attr {string} label - Display name for the font (optional, falls back to font-family)
|
|
9
|
+
* @attr {string} sample - Custom sample text (default: "The quick brown fox...")
|
|
10
|
+
* @attr {boolean} show-scale - Show VB type scale (xs–5xl)
|
|
11
|
+
* @attr {boolean} show-weights - Show weight scale (100–900)
|
|
12
|
+
* @attr {boolean} show-characters - Show character grid
|
|
13
|
+
* @attr {string} weights - Comma-separated available weights (default: "400,700")
|
|
14
|
+
* @attr {boolean} editable - Turn the font-family label into a live input that writes a CSS custom property
|
|
15
|
+
* @attr {string} target - CSS selector for the element to receive token overrides (default: ":root")
|
|
16
|
+
* @attr {string} token - Token name to write when editing (default: "font-family-base")
|
|
17
|
+
*
|
|
18
|
+
* @fires type-specimen:change - When edited. detail: { fontFamily, token, target }
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <type-specimen font-family="Georgia" label="Georgia" show-characters show-weights></type-specimen>
|
|
22
|
+
* <type-specimen font-family="'Inter', sans-serif" editable show-scale></type-specimen>
|
|
23
|
+
*/
|
|
24
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
25
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
26
|
+
|
|
27
|
+
const QUICK_FONTS = ['system-ui', 'Georgia', 'Inter, sans-serif', '"JetBrains Mono", monospace', 'Verdana', 'Cambria', 'ui-serif'];
|
|
28
|
+
|
|
29
|
+
class TypeSpecimen extends VBElement {
|
|
30
|
+
static observedAttributes = ['font-family', 'label', 'sample', 'show-scale', 'show-weights', 'show-characters', 'weights', 'editable', 'target', 'token'];
|
|
31
|
+
|
|
32
|
+
setup() {
|
|
33
|
+
this.#render();
|
|
34
|
+
this.#wireEditing();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
attributeChangedCallback() {
|
|
38
|
+
if (this.isConnected) {
|
|
39
|
+
this.#render();
|
|
40
|
+
this.#wireEditing();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#render() {
|
|
45
|
+
const fontFamily = this.getAttribute('font-family') || 'system-ui';
|
|
46
|
+
const label = this.getAttribute('label') || fontFamily.replace(/['"]/g, '').split(',')[0];
|
|
47
|
+
const sample = this.getAttribute('sample') || 'The quick brown fox jumps over the lazy dog';
|
|
48
|
+
const showScale = this.hasAttribute('show-scale');
|
|
49
|
+
const showWeights = this.hasAttribute('show-weights');
|
|
50
|
+
const showChars = this.hasAttribute('show-characters');
|
|
51
|
+
const weightsAttr = this.getAttribute('weights') || '300,400,500,600,700';
|
|
52
|
+
const weights = weightsAttr.split(',').map(w => w.trim());
|
|
53
|
+
|
|
54
|
+
let html = '';
|
|
55
|
+
|
|
56
|
+
const editable = this.hasAttribute('editable');
|
|
57
|
+
|
|
58
|
+
// Header with font name and large sample
|
|
59
|
+
const labelHTML = editable
|
|
60
|
+
? `<input type="text" class="specimen-font-input" value="${fontFamily.replace(/"/g, '"')}" aria-label="Font family" style="font:inherit;padding:0.25rem 0.5rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);min-inline-size:18rem;max-inline-size:100%;font-family:${fontFamily};font-size:var(--font-size-sm,0.875rem)">
|
|
61
|
+
<span class="specimen-quick" style="display:inline-flex;flex-wrap:wrap;gap:0.25rem;margin-inline-start:0.5rem">
|
|
62
|
+
${QUICK_FONTS.map((f) => `<button type="button" class="specimen-quick-btn" data-font="${f.replace(/"/g, '"')}" style="font:inherit;font-size:var(--font-size-xs,0.75rem);padding:0.125rem 0.5rem;border:1px solid var(--color-border,#ccc);border-radius:999px;background:var(--color-surface,#fff);color:var(--color-text,#222);cursor:pointer">${f.split(',')[0].replace(/["']/g, '')}</button>`).join('')}
|
|
63
|
+
</span>`
|
|
64
|
+
: `<span class="specimen-label">${label}</span>`;
|
|
65
|
+
|
|
66
|
+
html += `<div class="specimen-header" style="font-family:${fontFamily}">
|
|
67
|
+
${labelHTML}
|
|
68
|
+
<p class="specimen-sample" contenteditable="plaintext-only" spellcheck="false">${sample}</p>
|
|
69
|
+
</div>`;
|
|
70
|
+
|
|
71
|
+
// Character grid
|
|
72
|
+
if (showChars) {
|
|
73
|
+
html += `<div class="specimen-chars" style="font-family:${fontFamily}">
|
|
74
|
+
<div class="char-row"><span class="char-label">Upper</span>${'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(c => `<span>${c}</span>`).join('')}</div>
|
|
75
|
+
<div class="char-row"><span class="char-label">Lower</span>${'abcdefghijklmnopqrstuvwxyz'.split('').map(c => `<span>${c}</span>`).join('')}</div>
|
|
76
|
+
<div class="char-row"><span class="char-label">Digits</span>${'0123456789'.split('').map(c => `<span>${c}</span>`).join('')}</div>
|
|
77
|
+
<div class="char-row"><span class="char-label">Punct</span>${'!@#$%^&*()_+-=[]{}|;:,.<>?'.split('').map(c => `<span>${c === '<' ? '<' : c === '>' ? '>' : c === '&' ? '&' : c}</span>`).join('')}</div>
|
|
78
|
+
</div>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Weight scale
|
|
82
|
+
if (showWeights) {
|
|
83
|
+
html += `<div class="specimen-weights">`;
|
|
84
|
+
for (const w of weights) {
|
|
85
|
+
html += `<div class="weight-sample" style="font-family:${fontFamily};font-weight:${w}">
|
|
86
|
+
<span class="weight-label">${w}</span>
|
|
87
|
+
<span class="weight-text">Aa</span>
|
|
88
|
+
</div>`;
|
|
89
|
+
}
|
|
90
|
+
html += `</div>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// VB type scale
|
|
94
|
+
if (showScale) {
|
|
95
|
+
const scale = [
|
|
96
|
+
{ name: 'xs', rem: 0.75 },
|
|
97
|
+
{ name: 'sm', rem: 0.875 },
|
|
98
|
+
{ name: 'md', rem: 1 },
|
|
99
|
+
{ name: 'lg', rem: 1.125 },
|
|
100
|
+
{ name: 'xl', rem: 1.25 },
|
|
101
|
+
{ name: '2xl', rem: 1.5 },
|
|
102
|
+
{ name: '3xl', rem: 1.875 },
|
|
103
|
+
{ name: '4xl', rem: 2.25 },
|
|
104
|
+
{ name: '5xl', rem: 3 },
|
|
105
|
+
];
|
|
106
|
+
html += `<div class="specimen-scale">`;
|
|
107
|
+
for (const step of scale) {
|
|
108
|
+
html += `<div class="scale-step" style="font-family:${fontFamily};font-size:${step.rem}rem">
|
|
109
|
+
<span class="scale-label">${step.name}</span>
|
|
110
|
+
<span class="scale-text">${sample.substring(0, 30)}</span>
|
|
111
|
+
</div>`;
|
|
112
|
+
}
|
|
113
|
+
html += `</div>`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.innerHTML = html;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#wireEditing() {
|
|
120
|
+
if (!this.hasAttribute('editable')) return;
|
|
121
|
+
const input = /** @type {HTMLInputElement | null} */ (this.querySelector('.specimen-font-input'));
|
|
122
|
+
if (input) {
|
|
123
|
+
this.listen(input, 'input', () => this.#applyFontFamily(input.value));
|
|
124
|
+
}
|
|
125
|
+
this.querySelectorAll('.specimen-quick-btn').forEach((btn) => {
|
|
126
|
+
this.listen(btn, 'click', () => {
|
|
127
|
+
const font = btn.getAttribute('data-font') || '';
|
|
128
|
+
if (input) input.value = font;
|
|
129
|
+
this.#applyFontFamily(font);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#applyFontFamily(value) {
|
|
135
|
+
const target = /** @type {HTMLElement | null} */ (this.#resolveTarget());
|
|
136
|
+
const token = this.getAttribute('token') || 'font-family-base';
|
|
137
|
+
if (target) target.style.setProperty(`--${token}`, value);
|
|
138
|
+
// Update the attribute so the specimen re-renders with the new font
|
|
139
|
+
this.setAttribute('font-family', value);
|
|
140
|
+
this.dispatchEvent(new CustomEvent('type-specimen:change', {
|
|
141
|
+
bubbles: true,
|
|
142
|
+
detail: { fontFamily: value, token, target: this.getAttribute('target') || ':root' },
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#resolveTarget() {
|
|
147
|
+
const sel = this.getAttribute('target') || ':root';
|
|
148
|
+
try {
|
|
149
|
+
return sel === ':root' ? document.documentElement : document.querySelector(sel);
|
|
150
|
+
} catch {
|
|
151
|
+
return document.documentElement;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
registerComponent('type-specimen', TypeSpecimen);
|
|
@@ -0,0 +1,84 @@
|
|
|
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>Type Specimen — Static Fallback</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<!--
|
|
10
|
+
Static fallback for <type-specimen>.
|
|
11
|
+
|
|
12
|
+
Upgrade relationship:
|
|
13
|
+
- Without JS: The font specimen is shown as static paragraphs at
|
|
14
|
+
different sizes, a character grid, and weight samples — all using
|
|
15
|
+
standard HTML and inline font-family styles. The content is readable
|
|
16
|
+
and demonstrates the typeface without any JavaScript.
|
|
17
|
+
- With JS: <type-specimen> reads `font-family`, `label`, `sample`,
|
|
18
|
+
`show-scale`, `show-weights`, and `show-characters` attributes to
|
|
19
|
+
dynamically render a specimen header with an editable sample text
|
|
20
|
+
(contenteditable), a character grid (upper, lower, digits, punctuation),
|
|
21
|
+
a weight scale (100-900), and the VB 9-step type scale (xs-5xl).
|
|
22
|
+
|
|
23
|
+
The static form preserves the core purpose — showing how a typeface
|
|
24
|
+
looks at different sizes and weights.
|
|
25
|
+
-->
|
|
26
|
+
|
|
27
|
+
<main>
|
|
28
|
+
<h1>Type Specimen</h1>
|
|
29
|
+
|
|
30
|
+
<section>
|
|
31
|
+
<h2 style="font-family: Georgia, serif">Georgia</h2>
|
|
32
|
+
|
|
33
|
+
<p style="font-family: Georgia, serif; font-size: 2.25rem">
|
|
34
|
+
The quick brown fox jumps over the lazy dog
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<h3>Character Set</h3>
|
|
38
|
+
<p style="font-family: Georgia, serif; letter-spacing: 0.1em">
|
|
39
|
+
ABCDEFGHIJKLMNOPQRSTUVWXYZ<br>
|
|
40
|
+
abcdefghijklmnopqrstuvwxyz<br>
|
|
41
|
+
0123456789<br>
|
|
42
|
+
!@#$%^&*()_+-=[]{}|;:,.<>?
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<h3>Weights</h3>
|
|
46
|
+
<p style="font-family: Georgia, serif; font-weight: 400">
|
|
47
|
+
<strong>400</strong> — The quick brown fox jumps over the lazy dog
|
|
48
|
+
</p>
|
|
49
|
+
<p style="font-family: Georgia, serif; font-weight: 700">
|
|
50
|
+
<strong>700</strong> — The quick brown fox jumps over the lazy dog
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<h3>Type Scale</h3>
|
|
54
|
+
<p style="font-family: Georgia, serif; font-size: 0.75rem">
|
|
55
|
+
<small>xs (0.75rem)</small> — The quick brown fox jumps over
|
|
56
|
+
</p>
|
|
57
|
+
<p style="font-family: Georgia, serif; font-size: 0.875rem">
|
|
58
|
+
<small>sm (0.875rem)</small> — The quick brown fox jumps over
|
|
59
|
+
</p>
|
|
60
|
+
<p style="font-family: Georgia, serif; font-size: 1rem">
|
|
61
|
+
<small>md (1rem)</small> — The quick brown fox jumps over
|
|
62
|
+
</p>
|
|
63
|
+
<p style="font-family: Georgia, serif; font-size: 1.125rem">
|
|
64
|
+
<small>lg (1.125rem)</small> — The quick brown fox jumps over
|
|
65
|
+
</p>
|
|
66
|
+
<p style="font-family: Georgia, serif; font-size: 1.25rem">
|
|
67
|
+
<small>xl (1.25rem)</small> — The quick brown fox jumps over
|
|
68
|
+
</p>
|
|
69
|
+
<p style="font-family: Georgia, serif; font-size: 1.5rem">
|
|
70
|
+
<small>2xl (1.5rem)</small> — The quick brown fox jumps over
|
|
71
|
+
</p>
|
|
72
|
+
<p style="font-family: Georgia, serif; font-size: 1.875rem">
|
|
73
|
+
<small>3xl (1.875rem)</small> — The quick brown fox jumps over
|
|
74
|
+
</p>
|
|
75
|
+
<p style="font-family: Georgia, serif; font-size: 2.25rem">
|
|
76
|
+
<small>4xl (2.25rem)</small> — The quick brown fox jumps
|
|
77
|
+
</p>
|
|
78
|
+
<p style="font-family: Georgia, serif; font-size: 3rem">
|
|
79
|
+
<small>5xl (3rem)</small> — The quick brown fox
|
|
80
|
+
</p>
|
|
81
|
+
</section>
|
|
82
|
+
</main>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|