@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,42 @@
|
|
|
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>Semantic Palette (Static Fallback)</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, sans-serif;
|
|
11
|
+
line-height: 1.5;
|
|
12
|
+
max-inline-size: 40rem;
|
|
13
|
+
margin-inline: auto;
|
|
14
|
+
padding: 2rem 1rem;
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
|
|
20
|
+
<!--
|
|
21
|
+
Upgrade relationship
|
|
22
|
+
====================
|
|
23
|
+
This static HTML is the baseline that <semantic-palette> enhances.
|
|
24
|
+
Without JS the colors list is still readable as a definition list.
|
|
25
|
+
-->
|
|
26
|
+
|
|
27
|
+
<h1>Semantic Palette</h1>
|
|
28
|
+
|
|
29
|
+
<noscript>
|
|
30
|
+
<p>This component works best with JavaScript enabled.</p>
|
|
31
|
+
</noscript>
|
|
32
|
+
|
|
33
|
+
<semantic-palette colors="#6366f1,#ec4899,#22c55e">
|
|
34
|
+
<dl>
|
|
35
|
+
<dt>primary</dt><dd>#6366f1</dd>
|
|
36
|
+
<dt>secondary</dt><dd>#ec4899</dd>
|
|
37
|
+
<dt>accent</dt><dd>#22c55e</dd>
|
|
38
|
+
</dl>
|
|
39
|
+
</semantic-palette>
|
|
40
|
+
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semantic-palette: interaction polish only.
|
|
3
|
+
* All layout is inline in logic.js; this file covers focus states and
|
|
4
|
+
* no-JS fallback hiding.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
semantic-palette[data-upgraded] > dl,
|
|
8
|
+
semantic-palette[data-upgraded] > ul,
|
|
9
|
+
semantic-palette[data-upgraded] > ol {
|
|
10
|
+
display: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
semantic-palette .sp-export button:hover {
|
|
14
|
+
background: var(--color-surface-hover, var(--color-surface-raised, #f5f5f5));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
semantic-palette .sp-export button:focus-visible {
|
|
18
|
+
outline: var(--focus-ring-width, 2px) solid var(--focus-ring-color, currentColor);
|
|
19
|
+
outline-offset: 2px;
|
|
20
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "spacing-specimen",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Design token spacing scale display",
|
|
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 token names to display"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "prefix",
|
|
22
|
+
"kind": "host-api",
|
|
23
|
+
"purpose": "config",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "CSS variable prefix (default: \"--size-\")"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "show-values",
|
|
29
|
+
"kind": "host-api",
|
|
30
|
+
"purpose": "config",
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"description": "Show computed px values (default: true)"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "label",
|
|
36
|
+
"kind": "host-api",
|
|
37
|
+
"purpose": "config",
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Optional heading label"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "editable",
|
|
43
|
+
"kind": "host-api",
|
|
44
|
+
"purpose": "config",
|
|
45
|
+
"type": "boolean",
|
|
46
|
+
"description": "Turn value cells into inputs that write the token on the target scope."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "target",
|
|
50
|
+
"kind": "host-api",
|
|
51
|
+
"purpose": "config",
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "CSS selector for the element to receive token overrides (default: \":root\"). Only used when editable."
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"events": [
|
|
57
|
+
{
|
|
58
|
+
"name": "spacing-specimen:change"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"childAttributes": [],
|
|
62
|
+
"structure": []
|
|
63
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spacing-specimen: Design token spacing scale display
|
|
3
|
+
*
|
|
4
|
+
* Reads VB spacing tokens from computed styles and renders a visual
|
|
5
|
+
* scale with labeled bars and computed values. Auto-detects the current
|
|
6
|
+
* theme's spacing system.
|
|
7
|
+
*
|
|
8
|
+
* Uses CSS variables directly for bar widths (no JS measurement needed
|
|
9
|
+
* for rendering). Reads computed px values after paint for labels.
|
|
10
|
+
* All layout styles are inline so the component works without external CSS.
|
|
11
|
+
*
|
|
12
|
+
* @attr {string} tokens - Comma-separated token names to display
|
|
13
|
+
* (default: "3xs,2xs,xs,s,m,l,xl,2xl,3xl")
|
|
14
|
+
* @attr {string} prefix - CSS variable prefix (default: "--size-")
|
|
15
|
+
* @attr {boolean} show-values - Show computed px values (default: true)
|
|
16
|
+
* @attr {string} label - Optional heading label
|
|
17
|
+
* @attr {boolean} editable - Turn value cells into inputs that write the token on target scope
|
|
18
|
+
* @attr {string} target - CSS selector for the element to receive token overrides (default: ":root")
|
|
19
|
+
*
|
|
20
|
+
* @fires spacing-specimen:change - When a token is edited. detail: { name, value, token, target }
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* <spacing-specimen></spacing-specimen>
|
|
24
|
+
* <spacing-specimen tokens="xs,s,m,l,xl" label="Core Scale"></spacing-specimen>
|
|
25
|
+
* <spacing-specimen editable target=":root"></spacing-specimen>
|
|
26
|
+
*/
|
|
27
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
28
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
29
|
+
|
|
30
|
+
class SpacingSpecimen extends VBElement {
|
|
31
|
+
static observedAttributes = ['tokens', 'prefix', 'show-values', 'label', 'editable', 'target'];
|
|
32
|
+
|
|
33
|
+
setup() {
|
|
34
|
+
this.#render();
|
|
35
|
+
this.#wireEditing();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
attributeChangedCallback() {
|
|
39
|
+
if (this.isConnected) {
|
|
40
|
+
this.#render();
|
|
41
|
+
this.#wireEditing();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#render() {
|
|
46
|
+
const tokensAttr = this.getAttribute('tokens') || '3xs,2xs,xs,s,m,l,xl,2xl,3xl';
|
|
47
|
+
const prefix = this.getAttribute('prefix') || '--size-';
|
|
48
|
+
const showValues = this.getAttribute('show-values') !== 'false';
|
|
49
|
+
const label = this.getAttribute('label') || '';
|
|
50
|
+
const editable = this.hasAttribute('editable');
|
|
51
|
+
|
|
52
|
+
const tokens = tokensAttr.split(',').map(t => t.trim());
|
|
53
|
+
|
|
54
|
+
let html = '';
|
|
55
|
+
|
|
56
|
+
if (label) {
|
|
57
|
+
html += `<div 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}</div>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
html += `<div role="list" aria-label="${label || 'Spacing scale'}" style="display:flex;flex-direction:column;gap:0.25rem">`;
|
|
61
|
+
|
|
62
|
+
for (const name of tokens) {
|
|
63
|
+
const varName = `${prefix}${name}`;
|
|
64
|
+
const valueControl = editable
|
|
65
|
+
? `<input type="text" class="scale-edit" data-token="${name}" aria-label="${name} value" style="font-family:var(--font-mono,monospace);font-size:0.75rem;padding:0.125rem 0.375rem;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:6rem;text-align:end">`
|
|
66
|
+
: (showValues ? `<span class="scale-value" 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>` : '');
|
|
67
|
+
|
|
68
|
+
html += `<div role="listitem" style="display:grid;grid-template-columns:3rem 1fr auto;align-items:center;gap:0.75rem;min-block-size:1.75rem">
|
|
69
|
+
<span style="font-family:var(--font-mono,monospace);font-size:0.875rem;color:var(--color-text-muted,#666);text-align:end">${name}</span>
|
|
70
|
+
<div class="scale-bar" style="display:block;block-size:var(--size-m,1rem);min-inline-size:2px;inline-size:var(${varName});background:var(--color-interactive,oklch(55% 0.2 260));border-radius:var(--radius-s,0.25rem)" aria-hidden="true"></div>
|
|
71
|
+
${valueControl}
|
|
72
|
+
</div>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
html += '</div>';
|
|
76
|
+
this.innerHTML = html;
|
|
77
|
+
|
|
78
|
+
// Read computed px values / fill edit inputs after paint
|
|
79
|
+
requestAnimationFrame(() => {
|
|
80
|
+
if (editable) {
|
|
81
|
+
const cs = getComputedStyle(this);
|
|
82
|
+
this.querySelectorAll('.scale-edit').forEach((node) => {
|
|
83
|
+
const input = /** @type {HTMLInputElement} */ (node);
|
|
84
|
+
const name = input.getAttribute('data-token');
|
|
85
|
+
const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
|
|
86
|
+
input.value = raw || '';
|
|
87
|
+
});
|
|
88
|
+
} else if (showValues) {
|
|
89
|
+
this.querySelectorAll('.scale-bar').forEach((bar) => {
|
|
90
|
+
const px = bar.getBoundingClientRect().width;
|
|
91
|
+
const valueEl = bar.nextElementSibling;
|
|
92
|
+
if (valueEl) valueEl.textContent = `${Math.round(px * 100) / 100}px`;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#wireEditing() {
|
|
99
|
+
if (!this.hasAttribute('editable')) return;
|
|
100
|
+
const prefix = this.getAttribute('prefix') || '--size-';
|
|
101
|
+
this.querySelectorAll('.scale-edit').forEach((input) => {
|
|
102
|
+
this.listen(input, 'change', () => this.#applyTokenEdit(input, prefix));
|
|
103
|
+
this.listen(input, 'keydown', (e) => {
|
|
104
|
+
if (/** @type {KeyboardEvent} */ (e).key === 'Enter') this.#applyTokenEdit(input, prefix);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#applyTokenEdit(input, prefix) {
|
|
110
|
+
const name = input.getAttribute('data-token');
|
|
111
|
+
const value = input.value.trim();
|
|
112
|
+
if (!name || !value) return;
|
|
113
|
+
const target = /** @type {HTMLElement | null} */ (this.#resolveTarget());
|
|
114
|
+
const token = `${prefix}${name}`;
|
|
115
|
+
if (target) target.style.setProperty(token, value);
|
|
116
|
+
this.dispatchEvent(new CustomEvent('spacing-specimen:change', {
|
|
117
|
+
bubbles: true,
|
|
118
|
+
detail: { name, value, token, target: this.getAttribute('target') || ':root' },
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#resolveTarget() {
|
|
123
|
+
const sel = this.getAttribute('target') || ':root';
|
|
124
|
+
try {
|
|
125
|
+
return sel === ':root' ? document.documentElement : document.querySelector(sel);
|
|
126
|
+
} catch {
|
|
127
|
+
return document.documentElement;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
registerComponent('spacing-specimen', SpacingSpecimen);
|
|
@@ -0,0 +1,71 @@
|
|
|
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>Spacing Specimen (Static Fallback)</title>
|
|
7
|
+
<style>
|
|
8
|
+
/*
|
|
9
|
+
* Static fallback for <spacing-specimen>.
|
|
10
|
+
*
|
|
11
|
+
* Upgrade relationship:
|
|
12
|
+
* When JavaScript loads and the <spacing-specimen> custom element is
|
|
13
|
+
* defined, it reads the VB spacing tokens from computed styles and
|
|
14
|
+
* renders a visual scale with labeled bars and computed pixel values.
|
|
15
|
+
*
|
|
16
|
+
* Without JS, this static page presents a simple list of spacing
|
|
17
|
+
* token names and their CSS custom property references, providing
|
|
18
|
+
* equivalent information through plain semantic HTML.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
font-family: system-ui, sans-serif;
|
|
25
|
+
line-height: 1.6;
|
|
26
|
+
margin: 0;
|
|
27
|
+
padding: 2rem;
|
|
28
|
+
color: #1a1a1a;
|
|
29
|
+
background: #f8f8f8;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
h1 { margin-block-end: 1rem; }
|
|
33
|
+
|
|
34
|
+
dl {
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-columns: 4rem 1fr;
|
|
37
|
+
gap: 0.5rem 1rem;
|
|
38
|
+
max-width: 30rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
dt {
|
|
42
|
+
font-family: ui-monospace, monospace;
|
|
43
|
+
font-size: 0.875rem;
|
|
44
|
+
text-align: end;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
dd {
|
|
48
|
+
margin: 0;
|
|
49
|
+
font-family: ui-monospace, monospace;
|
|
50
|
+
font-size: 0.875rem;
|
|
51
|
+
color: #666;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
54
|
+
</head>
|
|
55
|
+
<body>
|
|
56
|
+
<h1>Spacing Scale</h1>
|
|
57
|
+
<p>These tokens define the spacing system. With JavaScript enabled, this becomes a visual bar chart.</p>
|
|
58
|
+
|
|
59
|
+
<dl>
|
|
60
|
+
<dt>3xs</dt><dd>var(--size-3xs)</dd>
|
|
61
|
+
<dt>2xs</dt><dd>var(--size-2xs)</dd>
|
|
62
|
+
<dt>xs</dt><dd>var(--size-xs)</dd>
|
|
63
|
+
<dt>s</dt><dd>var(--size-s)</dd>
|
|
64
|
+
<dt>m</dt><dd>var(--size-m)</dd>
|
|
65
|
+
<dt>l</dt><dd>var(--size-l)</dd>
|
|
66
|
+
<dt>xl</dt><dd>var(--size-xl)</dd>
|
|
67
|
+
<dt>2xl</dt><dd>var(--size-2xl)</dd>
|
|
68
|
+
<dt>3xl</dt><dd>var(--size-3xl)</dd>
|
|
69
|
+
</dl>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* --- Progressive enhancement: no-JS fallback --- */
|
|
2
|
+
spacing-specimen:not(:defined) {
|
|
3
|
+
display: block;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
spacing-specimen {
|
|
7
|
+
display: block;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
spacing-specimen .specimen-label {
|
|
11
|
+
display: block;
|
|
12
|
+
font-size: var(--font-size-xs, 0.75rem);
|
|
13
|
+
font-weight: 700;
|
|
14
|
+
text-transform: uppercase;
|
|
15
|
+
letter-spacing: 0.08em;
|
|
16
|
+
color: var(--color-text-muted, #666);
|
|
17
|
+
margin-block-end: var(--size-s, 0.75rem);
|
|
18
|
+
font-family: var(--font-sans, system-ui);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
spacing-specimen .specimen-scale {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: var(--size-2xs, 0.25rem);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
spacing-specimen .scale-step {
|
|
28
|
+
display: grid;
|
|
29
|
+
grid-template-columns: 3rem 1fr auto;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: var(--size-s, 0.75rem);
|
|
32
|
+
min-block-size: 1.75rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
spacing-specimen .scale-name {
|
|
36
|
+
font-family: var(--font-mono, monospace);
|
|
37
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
38
|
+
color: var(--color-text-muted, #666);
|
|
39
|
+
text-align: end;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
spacing-specimen .scale-bar {
|
|
43
|
+
display: block;
|
|
44
|
+
block-size: var(--size-m, 1rem);
|
|
45
|
+
min-inline-size: 2px;
|
|
46
|
+
background: var(--color-interactive, oklch(55% 0.2 260));
|
|
47
|
+
border-radius: var(--radius-s, 0.25rem);
|
|
48
|
+
transition: inline-size 0.3s ease;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
spacing-specimen .scale-value {
|
|
52
|
+
font-family: var(--font-mono, monospace);
|
|
53
|
+
font-size: var(--font-size-xs, 0.75rem);
|
|
54
|
+
color: var(--color-text-muted, #666);
|
|
55
|
+
font-variant-numeric: tabular-nums;
|
|
56
|
+
min-inline-size: 3.5rem;
|
|
57
|
+
text-align: end;
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "theme-catalog",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Browse and apply curated public design-system token sets (Material, IBM Carbon, Salesforce Lightning, GOV.UK, Atlassian, Tailwind, Bootstrap, Catppuccin Mocha). Renders a tile grid; clicking a tile fetches the entry's DTCG file and applies its tokens to a target preview scope via the same deserializer <theme-import> uses.",
|
|
6
|
+
"attributes": [
|
|
7
|
+
{ "name": "target", "kind": "host-api", "purpose": "config", "type": "string", "default": "#preview", "description": "CSS selector for the scope to apply tokens to. Tokens land as inline custom properties." },
|
|
8
|
+
{ "name": "catalog-base", "kind": "host-api", "purpose": "config", "type": "string", "description": "Base URL for catalog files (manifest.json + entry .tokens.json). Defaults to /cdn/themes/catalog; override for hosted contexts." }
|
|
9
|
+
],
|
|
10
|
+
"events": [
|
|
11
|
+
{ "name": "theme-catalog:applied", "description": "Fired after an entry is applied. detail: { id, applied, ignored, tokens, source }" },
|
|
12
|
+
{ "name": "theme-catalog:error", "description": "Fired when fetch / parse / apply fails. detail: { id, error, phase }" }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* theme-catalog: Browse and apply curated public design-system token sets.
|
|
3
|
+
*
|
|
4
|
+
* Renders a tile grid of vendored DTCG entries (Material, IBM Carbon,
|
|
5
|
+
* Salesforce Lightning, GOV.UK, Atlassian, Tailwind, Bootstrap,
|
|
6
|
+
* Catppuccin Mocha). Clicking a tile fetches the entry's .tokens.json,
|
|
7
|
+
* deserializes it via the Phase 3 deserializer, and writes the resulting
|
|
8
|
+
* custom properties to a target scope (default #preview).
|
|
9
|
+
*
|
|
10
|
+
* Companion to <theme-import>. theme-import handles ad-hoc paste/file/URL
|
|
11
|
+
* imports; theme-catalog is the curated quick-start surface.
|
|
12
|
+
*
|
|
13
|
+
* @attr {string} target - CSS selector for the scope to apply tokens to (default: "#preview").
|
|
14
|
+
* @attr {string} catalog-base - Base URL for the catalog files. Default auto-detects from the
|
|
15
|
+
* component path; override for hosted contexts.
|
|
16
|
+
*
|
|
17
|
+
* @fires theme-catalog:applied - When an entry is applied. detail: { id, applied, ignored, tokens, source }
|
|
18
|
+
* @fires theme-catalog:error - On fetch / parse / apply failure. detail: { id, error, phase }
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <theme-catalog target="#preview"></theme-catalog>
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
25
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
26
|
+
import { deserializeDTCG } from '../theme-import/dtcg-deserialize.js';
|
|
27
|
+
|
|
28
|
+
class ThemeCatalog extends VBElement {
|
|
29
|
+
static observedAttributes = ['target', 'catalog-base'];
|
|
30
|
+
|
|
31
|
+
/** @type {any[] | null} */
|
|
32
|
+
#entries = null;
|
|
33
|
+
#lastAppliedId = '';
|
|
34
|
+
|
|
35
|
+
setup() {
|
|
36
|
+
this.#renderShell();
|
|
37
|
+
this.#loadAndRender();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
attributeChangedCallback() {
|
|
41
|
+
if (this.isConnected && this.hasAttribute('data-upgraded')) {
|
|
42
|
+
this.#renderShell();
|
|
43
|
+
this.#loadAndRender();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#targetEl() {
|
|
48
|
+
const sel = this.getAttribute('target') || '#preview';
|
|
49
|
+
try {
|
|
50
|
+
return /** @type {HTMLElement|null} */ (document.querySelector(sel));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the catalog base URL. The component is bundled into the VB
|
|
58
|
+
* core so it ships from the same origin as `dist/cdn/components/theme-catalog.js`.
|
|
59
|
+
* Catalog files live in src/web-components/theme-import/catalog/ — the
|
|
60
|
+
* publish pipeline copies them under dist/cdn/themes/catalog/ (Phase 4
|
|
61
|
+
* build step). Auto-detect from a known sibling import or accept an
|
|
62
|
+
* explicit `catalog-base` attribute override.
|
|
63
|
+
*/
|
|
64
|
+
#base() {
|
|
65
|
+
const explicit = this.getAttribute('catalog-base');
|
|
66
|
+
if (explicit) return explicit.replace(/\/$/, '');
|
|
67
|
+
if (typeof window !== 'undefined' && /** @type {any} */ (window).__VB_CATALOG_BASE) {
|
|
68
|
+
return String(/** @type {any} */ (window).__VB_CATALOG_BASE).replace(/\/$/, '');
|
|
69
|
+
}
|
|
70
|
+
return '/cdn/themes/catalog';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#renderShell() {
|
|
74
|
+
const card = `border:1px solid var(--color-border,#ddd);border-radius:var(--radius-m,0.5rem);background:var(--color-surface,#fff);padding:var(--size-m,1rem);display:flex;flex-direction:column;gap:var(--size-2xs,0.375rem);min-block-size:0`;
|
|
75
|
+
const grid = `display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:var(--size-s,0.75rem)`;
|
|
76
|
+
const muted = `font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666)`;
|
|
77
|
+
const title = `margin:0;font-size:var(--font-size-md,1rem);font-weight:600;color:var(--color-text,#222)`;
|
|
78
|
+
const btn = `align-self:flex-start;padding:0.375rem 0.75rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-primary,#0066cc);color:var(--color-text-on-primary,#fff);cursor:pointer;font:inherit;font-size:var(--font-size-xs,0.75rem)`;
|
|
79
|
+
const tag = `display:inline-block;padding:0.125rem 0.375rem;border-radius:0.25rem;background:var(--color-surface-raised,#f3f3f3);${muted};margin-inline-end:0.25rem`;
|
|
80
|
+
|
|
81
|
+
this.innerHTML = `
|
|
82
|
+
<div class="tc-head" style="display:flex;align-items:baseline;gap:var(--size-s,0.75rem);margin-block-end:var(--size-s,0.75rem);flex-wrap:wrap">
|
|
83
|
+
<strong style="font-size:var(--font-size-sm,0.875rem)">Start from a known system</strong>
|
|
84
|
+
<span class="tc-target" style="${muted}"></span>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="tc-grid" style="${grid}" role="list"></div>
|
|
87
|
+
<output class="tc-result" style="display:block;margin-block-start:var(--size-s,0.75rem);${muted}"></output>
|
|
88
|
+
<p class="tc-attribution" style="margin-block-start:var(--size-2xs,0.375rem);${muted}" hidden></p>
|
|
89
|
+
<details class="tc-ignored" style="margin-block-start:var(--size-2xs,0.375rem)" hidden>
|
|
90
|
+
<summary style="${muted};cursor:pointer">Ignored tokens</summary>
|
|
91
|
+
<ul class="tc-ignored-list" style="margin:var(--size-2xs,0.375rem) 0 0;padding-inline-start:1.25rem;${muted}"></ul>
|
|
92
|
+
</details>
|
|
93
|
+
<style>
|
|
94
|
+
:host ._card { ${card} }
|
|
95
|
+
:host ._title { ${title} }
|
|
96
|
+
:host ._btn { ${btn} }
|
|
97
|
+
:host ._tag { ${tag} }
|
|
98
|
+
</style>
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
this.#refreshTargetLabel();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#refreshTargetLabel() {
|
|
105
|
+
const span = this.querySelector('.tc-target');
|
|
106
|
+
if (!span) return;
|
|
107
|
+
const sel = this.getAttribute('target') || '#preview';
|
|
108
|
+
const found = this.#targetEl();
|
|
109
|
+
span.textContent = found ? `Target: ${sel}` : `Target ${sel} not found`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async #loadAndRender() {
|
|
113
|
+
const grid = this.querySelector('.tc-grid');
|
|
114
|
+
if (!grid) return;
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch(`${this.#base()}/manifest.json`);
|
|
117
|
+
if (!res.ok) throw new Error(`Catalog manifest fetch failed: ${res.status}`);
|
|
118
|
+
const manifest = await res.json();
|
|
119
|
+
this.#entries = manifest.entries || [];
|
|
120
|
+
this.#renderTiles();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
grid.innerHTML = '';
|
|
123
|
+
const out = this.querySelector('.tc-result');
|
|
124
|
+
if (out) out.textContent = `Couldn't load catalog manifest: ${err && err.message ? err.message : String(err)}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#renderTiles() {
|
|
129
|
+
const grid = this.querySelector('.tc-grid');
|
|
130
|
+
if (!grid || !this.#entries) return;
|
|
131
|
+
const card = `border:1px solid var(--color-border,#ddd);border-radius:var(--radius-m,0.5rem);background:var(--color-surface,#fff);padding:var(--size-m,1rem);display:flex;flex-direction:column;gap:var(--size-2xs,0.375rem)`;
|
|
132
|
+
const title = `margin:0;font-size:var(--font-size-md,1rem);font-weight:600;color:var(--color-text,#222)`;
|
|
133
|
+
const btn = `align-self:flex-start;margin-block-start:auto;padding:0.375rem 0.75rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-primary,#0066cc);color:var(--color-text-on-primary,#fff);cursor:pointer;font:inherit;font-size:var(--font-size-xs,0.75rem)`;
|
|
134
|
+
const muted = `font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666)`;
|
|
135
|
+
const tag = `display:inline-block;padding:0.125rem 0.375rem;border-radius:0.25rem;background:var(--color-surface-raised,#f3f3f3);${muted};margin-inline-end:0.25rem`;
|
|
136
|
+
|
|
137
|
+
grid.innerHTML = '';
|
|
138
|
+
for (const entry of this.#entries) {
|
|
139
|
+
const tile = document.createElement('article');
|
|
140
|
+
tile.setAttribute('role', 'listitem');
|
|
141
|
+
tile.setAttribute('data-id', entry.id);
|
|
142
|
+
tile.style.cssText = card;
|
|
143
|
+
tile.innerHTML = `
|
|
144
|
+
<h3 style="${title}">${escapeHTML(entry.name)}</h3>
|
|
145
|
+
<p style="${muted};margin:0">${escapeHTML(entry.summary || '')}</p>
|
|
146
|
+
<p style="${muted};margin:0">License: ${escapeHTML(entry.license)}</p>
|
|
147
|
+
<p style="margin:0">${(entry.tags || []).map((t) => `<span style="${tag}">${escapeHTML(t)}</span>`).join('')}</p>
|
|
148
|
+
<button type="button" class="tc-apply" style="${btn}" data-id="${escapeAttr(entry.id)}" aria-label="Apply ${escapeAttr(entry.name)} tokens">
|
|
149
|
+
Apply
|
|
150
|
+
</button>
|
|
151
|
+
`;
|
|
152
|
+
const btnEl = /** @type {HTMLButtonElement|null} */ (tile.querySelector('button.tc-apply'));
|
|
153
|
+
if (btnEl) this.listen(btnEl, 'click', () => this.#applyEntry(entry));
|
|
154
|
+
grid.appendChild(tile);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {{ id: string, name: string, file: string, attribution: string, source: string, license: string }} entry
|
|
160
|
+
*/
|
|
161
|
+
async #applyEntry(entry) {
|
|
162
|
+
const target = this.#targetEl();
|
|
163
|
+
if (!target) {
|
|
164
|
+
this.#fail(entry.id, new Error(`Target ${this.getAttribute('target') || '#preview'} not found`), 'apply');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(`${this.#base()}/${entry.file}`);
|
|
169
|
+
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
|
|
170
|
+
const doc = await res.json();
|
|
171
|
+
const result = deserializeDTCG(doc);
|
|
172
|
+
for (const [name, value] of result.tokens) {
|
|
173
|
+
target.style.setProperty(name, value);
|
|
174
|
+
}
|
|
175
|
+
this.#showResult(entry, result);
|
|
176
|
+
this.#lastAppliedId = entry.id;
|
|
177
|
+
this.dispatchEvent(new CustomEvent('theme-catalog:applied', {
|
|
178
|
+
bubbles: true,
|
|
179
|
+
detail: { id: entry.id, applied: result.stats.applied, ignored: result.ignored, tokens: result.tokens, source: 'catalog' },
|
|
180
|
+
}));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
this.#fail(entry.id, err, 'fetch-or-apply');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#showResult(entry, result) {
|
|
187
|
+
const out = this.querySelector('.tc-result');
|
|
188
|
+
if (out) {
|
|
189
|
+
out.textContent = `Applied ${result.stats.applied} token${result.stats.applied === 1 ? '' : 's'} from ${entry.name}${result.stats.ignored ? ` · ${result.stats.ignored} ignored` : ''}.`;
|
|
190
|
+
}
|
|
191
|
+
const att = /** @type {HTMLElement|null} */ (this.querySelector('.tc-attribution'));
|
|
192
|
+
if (att) {
|
|
193
|
+
att.hidden = false;
|
|
194
|
+
att.textContent = entry.attribution;
|
|
195
|
+
}
|
|
196
|
+
const ig = /** @type {HTMLDetailsElement|null} */ (this.querySelector('.tc-ignored'));
|
|
197
|
+
const list = this.querySelector('.tc-ignored-list');
|
|
198
|
+
if (ig && list) {
|
|
199
|
+
if (result.ignored.length === 0) {
|
|
200
|
+
ig.hidden = true;
|
|
201
|
+
list.innerHTML = '';
|
|
202
|
+
} else {
|
|
203
|
+
ig.hidden = false;
|
|
204
|
+
list.innerHTML = result.ignored.map((p) => `<li>${escapeHTML(p)}</li>`).join('');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#fail(id, err, phase) {
|
|
210
|
+
const out = this.querySelector('.tc-result');
|
|
211
|
+
if (out) out.textContent = `Error (${phase}): ${err && err.message ? err.message : String(err)}`;
|
|
212
|
+
this.dispatchEvent(new CustomEvent('theme-catalog:error', {
|
|
213
|
+
bubbles: true,
|
|
214
|
+
detail: { id, error: err, phase },
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function escapeHTML(s) {
|
|
220
|
+
return String(s).replace(/[&<>"']/g, (ch) => /** @type {Record<string,string>} */ ({
|
|
221
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
222
|
+
})[ch]);
|
|
223
|
+
}
|
|
224
|
+
function escapeAttr(s) { return escapeHTML(s); }
|
|
225
|
+
|
|
226
|
+
registerComponent('theme-catalog', ThemeCatalog);
|
|
227
|
+
|
|
228
|
+
export { ThemeCatalog };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Theme Catalog (Static Fallback)</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>Theme Catalog</h1>
|
|
9
|
+
<noscript><p>This component works best with JavaScript enabled.</p></noscript>
|
|
10
|
+
<theme-catalog target="#preview"></theme-catalog>
|
|
11
|
+
<section id="preview">
|
|
12
|
+
<p>Preview scope.</p>
|
|
13
|
+
</section>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "theme-export",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Collects VB custom property overrides from a scope and emits them as a theme.css block (or JSON). The assembly glue for the Theme Composer.",
|
|
6
|
+
"attributes": [
|
|
7
|
+
{ "name": "scope", "kind": "host-api", "purpose": "config", "type": "string", "default": ":root", "description": "CSS selector for the scope to read overrides from." },
|
|
8
|
+
{ "name": "selector", "kind": "host-api", "purpose": "config", "type": "string", "default": ":root", "description": "Selector written in the emitted CSS output." },
|
|
9
|
+
{ "name": "include", "kind": "host-api", "purpose": "config", "type": "string", "description": "Comma-separated prefixes to collect. Default covers all VB token categories." },
|
|
10
|
+
{ "name": "format", "kind": "host-api", "purpose": "config", "type": "enum", "values": ["css", "json", "dtcg"], "default": "css", "description": "Output format. 'dtcg' emits Design Tokens Community Group JSON (stable spec 2025.10)." },
|
|
11
|
+
{ "name": "vb-version", "kind": "host-api", "purpose": "config", "type": "string", "description": "Optional VB version string included in DTCG $extensions metadata." },
|
|
12
|
+
{ "name": "label", "kind": "host-api", "purpose": "config", "type": "string", "description": "Optional heading label." },
|
|
13
|
+
{ "name": "live", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Re-scan whenever an editable specimen fires a :change event." }
|
|
14
|
+
],
|
|
15
|
+
"events": [
|
|
16
|
+
{ "name": "theme-export:change" }
|
|
17
|
+
]
|
|
18
|
+
}
|