@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,4482 @@
|
|
|
1
|
+
// src/lib/bundle-registry.js
|
|
2
|
+
var reducedMotionMq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
3
|
+
var components = /* @__PURE__ */ new Map();
|
|
4
|
+
function registerComponent(tag, impl, opts = {}) {
|
|
5
|
+
const priority = opts.priority ?? 10;
|
|
6
|
+
const meta = { impl, bundle: opts.bundle, contract: opts.contract, priority };
|
|
7
|
+
const existing = components.get(tag);
|
|
8
|
+
if (customElements.get(tag)) {
|
|
9
|
+
if (!existing || existing.priority >= priority) {
|
|
10
|
+
if (existing && existing.priority === priority && existing.impl !== impl) {
|
|
11
|
+
console.warn(
|
|
12
|
+
`[VB Bundle] Tag <${tag}> already registered by "${existing.bundle}" (priority ${existing.priority}). Skipping "${opts.bundle}".`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.warn(
|
|
18
|
+
`[VB Bundle] Tag <${tag}> defined by "${existing.bundle}" cannot be replaced (customElements.define is permanent). "${opts.bundle}" has higher priority but arrived late.`
|
|
19
|
+
);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (existing && existing.priority >= priority) {
|
|
23
|
+
if (existing.priority === priority) {
|
|
24
|
+
console.warn(
|
|
25
|
+
`[VB Bundle] Tag <${tag}> already registered by "${existing.bundle}". Skipping "${opts.bundle}" (first wins at equal priority).`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
components.set(tag, meta);
|
|
31
|
+
customElements.define(tag, impl);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/lib/vb-element.js
|
|
35
|
+
var VBElement = class extends HTMLElement {
|
|
36
|
+
#cleanups = [];
|
|
37
|
+
/** @type {ElementInternals | undefined} */
|
|
38
|
+
#internals;
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
if (this.hasAttribute("data-upgraded")) return;
|
|
41
|
+
if (this.setup() === false) return;
|
|
42
|
+
this.setAttribute("data-upgraded", "");
|
|
43
|
+
queueMicrotask(() => {
|
|
44
|
+
this.dispatchEvent(new CustomEvent(`${this.localName}:upgraded`, { bubbles: true }));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
disconnectedCallback() {
|
|
48
|
+
for (const fn of this.#cleanups) fn();
|
|
49
|
+
this.#cleanups = [];
|
|
50
|
+
this.removeAttribute("data-upgraded");
|
|
51
|
+
this.teardown();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Track an event listener for automatic cleanup on disconnect.
|
|
55
|
+
* @param {EventTarget} target
|
|
56
|
+
* @param {string} event
|
|
57
|
+
* @param {EventListenerOrEventListenerObject} handler
|
|
58
|
+
* @param {AddEventListenerOptions} [opts]
|
|
59
|
+
*/
|
|
60
|
+
listen(target, event, handler, opts) {
|
|
61
|
+
target.addEventListener(event, handler, opts);
|
|
62
|
+
this.#cleanups.push(() => target.removeEventListener(event, handler, opts));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Override in subclass. Return false to abort upgrade.
|
|
66
|
+
* @returns {boolean | void}
|
|
67
|
+
*/
|
|
68
|
+
setup() {
|
|
69
|
+
}
|
|
70
|
+
/** Override in subclass for cleanup beyond event listeners. */
|
|
71
|
+
teardown() {
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Toggle a CustomStateSet entry targetable via the `:state(name)` CSS selector.
|
|
75
|
+
* Use for component-private flags. For author-facing state, keep using
|
|
76
|
+
* data-* / aria-* attributes — see admin/specs/custom-state-set-research.md.
|
|
77
|
+
*
|
|
78
|
+
* Lazily attaches ElementInternals on first call; subclasses that already
|
|
79
|
+
* attached internals (form-associated components) must hand them over via
|
|
80
|
+
* `_adoptInternals(this.attachInternals())` in their constructor to avoid
|
|
81
|
+
* the double-attach throw.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} name
|
|
84
|
+
* @param {boolean} on
|
|
85
|
+
*/
|
|
86
|
+
setState(name, on) {
|
|
87
|
+
if (!this.#internals) this.#internals = this.attachInternals();
|
|
88
|
+
const states = this.#internals.states;
|
|
89
|
+
try {
|
|
90
|
+
if (on) states.add(name);
|
|
91
|
+
else states.delete(name);
|
|
92
|
+
} catch {
|
|
93
|
+
const legacy = `--${name}`;
|
|
94
|
+
if (on) states.add(legacy);
|
|
95
|
+
else states.delete(legacy);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Hand pre-attached ElementInternals to the base class. Form-associated
|
|
100
|
+
* subclasses call this in their constructor right after attachInternals().
|
|
101
|
+
* @param {ElementInternals} internals
|
|
102
|
+
*/
|
|
103
|
+
_adoptInternals(internals) {
|
|
104
|
+
if (!this.#internals) this.#internals = internals;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/utils/copy-init.js
|
|
109
|
+
var COPIED_DURATION = 1500;
|
|
110
|
+
var ANNOUNCE_DURATION = 1e3;
|
|
111
|
+
var SELECTOR = "[data-copy], [data-copy-target], [data-paste-target]";
|
|
112
|
+
var DEFAULT_ANNOUNCE = "Copied to clipboard";
|
|
113
|
+
var DEFAULT_PASTE_ANNOUNCE = "Pasted from clipboard";
|
|
114
|
+
var resetTimers = /* @__PURE__ */ new WeakMap();
|
|
115
|
+
async function copyText(text, options = {}) {
|
|
116
|
+
if (text == null || text === "") return false;
|
|
117
|
+
const { button, announceMessage = DEFAULT_ANNOUNCE, duration = COPIED_DURATION } = options;
|
|
118
|
+
try {
|
|
119
|
+
await navigator.clipboard.writeText(text);
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
applyFeedback({ button, duration, announceMessage, eventDetail: { text } });
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
function applyFeedback({ button, duration, announceMessage, eventDetail, state = "copied", eventName = "copy" }) {
|
|
127
|
+
if (button) {
|
|
128
|
+
button.dataset.state = state;
|
|
129
|
+
const prior = resetTimers.get(button);
|
|
130
|
+
if (prior) clearTimeout(prior);
|
|
131
|
+
resetTimers.set(button, setTimeout(() => {
|
|
132
|
+
delete button.dataset.state;
|
|
133
|
+
resetTimers.delete(button);
|
|
134
|
+
}, duration));
|
|
135
|
+
button.dispatchEvent(new CustomEvent(eventName, {
|
|
136
|
+
bubbles: true,
|
|
137
|
+
detail: eventDetail
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
announce(announceMessage, button ?? document.body);
|
|
141
|
+
}
|
|
142
|
+
async function pasteFromClipboard(target, options = {}) {
|
|
143
|
+
const { button, announceMessage = DEFAULT_PASTE_ANNOUNCE, duration = COPIED_DURATION } = options;
|
|
144
|
+
let text;
|
|
145
|
+
try {
|
|
146
|
+
text = await navigator.clipboard.readText();
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (target) {
|
|
151
|
+
if ("value" in target && (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) {
|
|
152
|
+
target.value = text;
|
|
153
|
+
} else {
|
|
154
|
+
target.textContent = text;
|
|
155
|
+
}
|
|
156
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
157
|
+
}
|
|
158
|
+
applyFeedback({
|
|
159
|
+
button,
|
|
160
|
+
duration,
|
|
161
|
+
announceMessage,
|
|
162
|
+
eventDetail: { text },
|
|
163
|
+
state: "pasted",
|
|
164
|
+
eventName: "paste"
|
|
165
|
+
});
|
|
166
|
+
return text;
|
|
167
|
+
}
|
|
168
|
+
function initCopyButtons(root = document) {
|
|
169
|
+
root.querySelectorAll(SELECTOR).forEach(enhanceButton);
|
|
170
|
+
}
|
|
171
|
+
function enhanceButton(button) {
|
|
172
|
+
if (button.hasAttribute("data-copy-init")) return;
|
|
173
|
+
button.setAttribute("data-copy-init", "");
|
|
174
|
+
button.addEventListener("click", () => {
|
|
175
|
+
if (button.dataset.pasteTarget) {
|
|
176
|
+
const target = document.querySelector(button.dataset.pasteTarget);
|
|
177
|
+
if (!target) return;
|
|
178
|
+
pasteFromClipboard(target, { button });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const text = getText(button);
|
|
182
|
+
if (!text) return;
|
|
183
|
+
copyText(text, { button });
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function getText(button) {
|
|
187
|
+
if (button.dataset.copy) return button.dataset.copy;
|
|
188
|
+
if (button.dataset.copyTarget) {
|
|
189
|
+
const target = document.querySelector(button.dataset.copyTarget);
|
|
190
|
+
if (!target) return "";
|
|
191
|
+
const attr = button.dataset.copyAttr;
|
|
192
|
+
if (attr) return target.getAttribute(attr) ?? "";
|
|
193
|
+
return target.textContent ?? "";
|
|
194
|
+
}
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
function announce(message, context) {
|
|
198
|
+
const el2 = document.createElement("div");
|
|
199
|
+
el2.setAttribute("role", "status");
|
|
200
|
+
el2.setAttribute("aria-live", "polite");
|
|
201
|
+
el2.className = "visually-hidden";
|
|
202
|
+
el2.textContent = message;
|
|
203
|
+
context.appendChild(el2);
|
|
204
|
+
setTimeout(() => el2.remove(), ANNOUNCE_DURATION);
|
|
205
|
+
}
|
|
206
|
+
if (document.readyState === "loading") {
|
|
207
|
+
document.addEventListener("DOMContentLoaded", () => initCopyButtons());
|
|
208
|
+
} else {
|
|
209
|
+
initCopyButtons();
|
|
210
|
+
}
|
|
211
|
+
var observer = new MutationObserver((mutations) => {
|
|
212
|
+
for (const mutation of mutations) {
|
|
213
|
+
for (const node of mutation.addedNodes) {
|
|
214
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
215
|
+
const el2 = (
|
|
216
|
+
/** @type {Element} */
|
|
217
|
+
node
|
|
218
|
+
);
|
|
219
|
+
if (el2.matches(SELECTOR)) {
|
|
220
|
+
enhanceButton(
|
|
221
|
+
/** @type {HTMLElement} */
|
|
222
|
+
el2
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
el2.querySelectorAll(SELECTOR).forEach((child) => enhanceButton(
|
|
226
|
+
/** @type {HTMLElement} */
|
|
227
|
+
child
|
|
228
|
+
));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
233
|
+
|
|
234
|
+
// src/web-components/color-palette/logic.js
|
|
235
|
+
var ColorPalette = class extends VBElement {
|
|
236
|
+
static observedAttributes = ["colors", "names", "layout", "show-values", "show-names", "size", "editable"];
|
|
237
|
+
/** @type {string[]} */
|
|
238
|
+
#colors = [];
|
|
239
|
+
setup() {
|
|
240
|
+
this.#render();
|
|
241
|
+
}
|
|
242
|
+
attributeChangedCallback() {
|
|
243
|
+
if (this.isConnected) this.#render();
|
|
244
|
+
}
|
|
245
|
+
#render() {
|
|
246
|
+
const colorsRaw = this.getAttribute("colors") || "";
|
|
247
|
+
const namesRaw = this.getAttribute("names") || "";
|
|
248
|
+
const layout = this.getAttribute("layout") || "inline";
|
|
249
|
+
const size = this.getAttribute("size") || "md";
|
|
250
|
+
const showValues = this.hasAttribute("show-values");
|
|
251
|
+
const showNames = this.hasAttribute("show-names") || namesRaw.length > 0;
|
|
252
|
+
const editable = this.hasAttribute("editable");
|
|
253
|
+
const colors = this.#parseColorList(colorsRaw);
|
|
254
|
+
this.#colors = colors.slice();
|
|
255
|
+
const names = namesRaw ? namesRaw.split(",").map((n) => n.trim()) : [];
|
|
256
|
+
const sizes = { sm: 48, md: 80, lg: 120 };
|
|
257
|
+
const px = sizes[size] || 80;
|
|
258
|
+
let containerStyle = `display:flex;flex-wrap:wrap;gap:var(--size-xs,0.5rem)`;
|
|
259
|
+
if (layout === "grid") {
|
|
260
|
+
containerStyle = `display:grid;grid-template-columns:repeat(auto-fill,minmax(${px}px,1fr));gap:var(--size-xs,0.5rem)`;
|
|
261
|
+
} else if (layout === "list") {
|
|
262
|
+
containerStyle = `display:flex;flex-direction:column;gap:var(--size-xs,0.5rem)`;
|
|
263
|
+
}
|
|
264
|
+
const swatches = colors.map((color, i) => {
|
|
265
|
+
const name = names[i] || "";
|
|
266
|
+
const contrast = this.#contrastColor(color);
|
|
267
|
+
const wrapStyle = layout === "list" ? `display:flex;flex-direction:row;align-items:center;gap:0.75rem` : `display:flex;flex-direction:column;align-items:center;gap:0.25rem;max-inline-size:${px}px`;
|
|
268
|
+
const boxSize = layout === "list" ? 36 : px;
|
|
269
|
+
const buttonStyle = `background:${color};color:${contrast};width:${boxSize}px;height:${boxSize}px;border:1px solid oklch(0% 0 0/0.15);border-radius:var(--radius-s,0.25rem);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;font-family:var(--font-mono,monospace);position:relative;overflow:hidden;flex-shrink:0`;
|
|
270
|
+
if (editable) {
|
|
271
|
+
const hexValue = this.#ensureHex(color);
|
|
272
|
+
return `<div class="swatch-wrap" role="listitem" style="${wrapStyle}">
|
|
273
|
+
<label class="color-box color-box-edit" style="${buttonStyle};cursor:pointer" data-index="${i}" title="Click to edit${name ? ": " + name : ""}" aria-label="${name || "Color " + (i + 1)}: ${color}. Click to edit.">
|
|
274
|
+
<input type="color" class="color-input" value="${hexValue}" data-index="${i}" style="position:absolute;inset:0;opacity:0;cursor:pointer;inline-size:100%;block-size:100%" aria-label="${name || "Color " + (i + 1)} picker">
|
|
275
|
+
<span class="color-value" style="font-size:0.625rem;line-height:1.2;opacity:${showValues ? "1" : "0"};text-align:center;padding:2px 4px;word-break:break-all;transition:opacity 0.15s ease;pointer-events:none">${this.#formatValue(color)}</span>
|
|
276
|
+
</label>
|
|
277
|
+
${showNames && name ? `<span style="font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666);text-align:center;max-inline-size:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${name}</span>` : ""}
|
|
278
|
+
</div>`;
|
|
279
|
+
}
|
|
280
|
+
return `<div class="swatch-wrap" role="listitem" style="${wrapStyle}">
|
|
281
|
+
<button type="button" class="color-box" data-index="${i}"
|
|
282
|
+
style="${buttonStyle}"
|
|
283
|
+
title="Click to copy${name ? ": " + name : ""}"
|
|
284
|
+
aria-label="${name || "Color " + (i + 1)}: ${color}">
|
|
285
|
+
<span class="color-value" style="font-size:0.625rem;line-height:1.2;opacity:${showValues ? "1" : "0"};text-align:center;padding:2px 4px;word-break:break-all;transition:opacity 0.15s ease">${this.#formatValue(color)}</span>
|
|
286
|
+
</button>
|
|
287
|
+
${showNames && name ? `<span style="font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666);text-align:center;max-inline-size:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${name}</span>` : ""}
|
|
288
|
+
</div>`;
|
|
289
|
+
}).join("");
|
|
290
|
+
this.innerHTML = `<div class="palette ${layout}" role="list" aria-label="Color palette" style="${containerStyle}">${swatches}</div>`;
|
|
291
|
+
if (editable) {
|
|
292
|
+
this.#wireEditable(names);
|
|
293
|
+
} else {
|
|
294
|
+
this.#resolveVarColors(colors, names);
|
|
295
|
+
this.#wireReadonly(colors, names, showValues);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
#wireReadonly(colors, names, showValues) {
|
|
299
|
+
this.querySelectorAll(".color-box").forEach((el2) => {
|
|
300
|
+
const btn = (
|
|
301
|
+
/** @type {HTMLElement} */
|
|
302
|
+
el2
|
|
303
|
+
);
|
|
304
|
+
if (!showValues) {
|
|
305
|
+
btn.addEventListener("pointerenter", () => {
|
|
306
|
+
const val = (
|
|
307
|
+
/** @type {HTMLElement | null} */
|
|
308
|
+
btn.querySelector(".color-value")
|
|
309
|
+
);
|
|
310
|
+
if (val) val.style.opacity = "1";
|
|
311
|
+
});
|
|
312
|
+
btn.addEventListener("pointerleave", () => {
|
|
313
|
+
const val = (
|
|
314
|
+
/** @type {HTMLElement | null} */
|
|
315
|
+
btn.querySelector(".color-value")
|
|
316
|
+
);
|
|
317
|
+
if (val) val.style.opacity = "0";
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
btn.addEventListener("click", () => {
|
|
321
|
+
const idx = Number(btn.dataset.index);
|
|
322
|
+
const color = colors[idx];
|
|
323
|
+
const name = names[idx] || "";
|
|
324
|
+
copyText(color, { button: btn, announceMessage: "Color copied" });
|
|
325
|
+
this.dispatchEvent(new CustomEvent("color-palette:select", {
|
|
326
|
+
bubbles: true,
|
|
327
|
+
detail: { color, name, index: idx }
|
|
328
|
+
}));
|
|
329
|
+
btn.style.outline = "3px solid currentColor";
|
|
330
|
+
btn.style.outlineOffset = "2px";
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
btn.style.outline = "";
|
|
333
|
+
btn.style.outlineOffset = "";
|
|
334
|
+
}, 600);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
#wireEditable(names) {
|
|
339
|
+
this.querySelectorAll(".color-input").forEach((input) => {
|
|
340
|
+
input.addEventListener("input", () => {
|
|
341
|
+
const idx = Number(input.dataset.index);
|
|
342
|
+
const hex = input.value;
|
|
343
|
+
this.#colors[idx] = hex;
|
|
344
|
+
const label = (
|
|
345
|
+
/** @type {HTMLElement | null} */
|
|
346
|
+
input.parentElement
|
|
347
|
+
);
|
|
348
|
+
if (label) {
|
|
349
|
+
label.style.background = hex;
|
|
350
|
+
label.style.color = this.#contrastColor(hex);
|
|
351
|
+
const valSpan = label.querySelector(".color-value");
|
|
352
|
+
if (valSpan) valSpan.textContent = this.#formatValue(hex);
|
|
353
|
+
label.title = `Click to edit${names[idx] ? ": " + names[idx] : ""} (${hex})`;
|
|
354
|
+
label.setAttribute("aria-label", `${names[idx] || "Color " + (idx + 1)}: ${hex}. Click to edit.`);
|
|
355
|
+
}
|
|
356
|
+
this.setAttribute("colors", this.#colors.join(","));
|
|
357
|
+
this.dispatchEvent(new CustomEvent("color-palette:change", {
|
|
358
|
+
bubbles: true,
|
|
359
|
+
detail: {
|
|
360
|
+
color: hex,
|
|
361
|
+
name: names[idx] || "",
|
|
362
|
+
index: idx,
|
|
363
|
+
colors: this.#colors.slice()
|
|
364
|
+
}
|
|
365
|
+
}));
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/** Public accessor — used by sibling components that need the current palette. */
|
|
370
|
+
get colors() {
|
|
371
|
+
return this.#colors.slice();
|
|
372
|
+
}
|
|
373
|
+
/** Resolve var() references to computed hex after swatches are in the DOM */
|
|
374
|
+
#resolveVarColors(colors, names) {
|
|
375
|
+
this.querySelectorAll(".color-box").forEach((btn) => {
|
|
376
|
+
const idx = Number(btn.dataset.index);
|
|
377
|
+
const raw = colors[idx];
|
|
378
|
+
if (!raw || !raw.includes("var(")) return;
|
|
379
|
+
const computed = getComputedStyle(btn).backgroundColor;
|
|
380
|
+
const hex = this.#rgbToHex(computed) || computed;
|
|
381
|
+
colors[idx] = hex;
|
|
382
|
+
this.#colors[idx] = hex;
|
|
383
|
+
const val = btn.querySelector(".color-value");
|
|
384
|
+
if (val) val.textContent = hex;
|
|
385
|
+
btn.style.color = this.#contrastColor(hex);
|
|
386
|
+
const name = names[idx] || "";
|
|
387
|
+
btn.title = `Click to copy${name ? ": " + name : ""} (${hex})`;
|
|
388
|
+
btn.setAttribute("aria-label", `${name || "Color " + (idx + 1)}: ${hex}`);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/** Convert rgb(r, g, b) to hex string */
|
|
392
|
+
#rgbToHex(rgb) {
|
|
393
|
+
const m = rgb.match(/rgba?\(\s*([\d.]+),?\s*([\d.]+),?\s*([\d.]+)/);
|
|
394
|
+
if (!m) return null;
|
|
395
|
+
const [, r, g, b] = m;
|
|
396
|
+
const toHex = (n) => Math.round(Number(n)).toString(16).padStart(2, "0");
|
|
397
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
398
|
+
}
|
|
399
|
+
/** Ensure a color string is a #rrggbb hex suitable for <input type="color">. */
|
|
400
|
+
#ensureHex(color) {
|
|
401
|
+
if (/^#[0-9a-f]{6}$/i.test(color)) return color.toLowerCase();
|
|
402
|
+
if (/^#[0-9a-f]{3}$/i.test(color)) {
|
|
403
|
+
return ("#" + color.slice(1).split("").map((c) => c + c).join("")).toLowerCase();
|
|
404
|
+
}
|
|
405
|
+
if (typeof document === "undefined") return "#000000";
|
|
406
|
+
const probe = document.createElement("span");
|
|
407
|
+
probe.style.color = color;
|
|
408
|
+
probe.style.display = "none";
|
|
409
|
+
document.body.appendChild(probe);
|
|
410
|
+
const computed = getComputedStyle(probe).color;
|
|
411
|
+
probe.remove();
|
|
412
|
+
return this.#rgbToHex(computed) || "#000000";
|
|
413
|
+
}
|
|
414
|
+
/** Shorten oklch values for display */
|
|
415
|
+
#formatValue(color) {
|
|
416
|
+
if (color.startsWith("#")) return color;
|
|
417
|
+
const oklch = color.match(/oklch\(\s*([\d.]+)%?\s+([\d.]+)\s+([\d.]+)/);
|
|
418
|
+
if (oklch) return `${oklch[1]}% .${oklch[2].replace("0.", "")}`;
|
|
419
|
+
return color.length > 12 ? color.slice(0, 12) + "\u2026" : color;
|
|
420
|
+
}
|
|
421
|
+
/** Parse comma-separated color list, handling oklch() which contains commas */
|
|
422
|
+
#parseColorList(raw) {
|
|
423
|
+
if (!raw) return [];
|
|
424
|
+
const colors = [];
|
|
425
|
+
let depth = 0;
|
|
426
|
+
let current = "";
|
|
427
|
+
for (const ch of raw) {
|
|
428
|
+
if (ch === "(") depth++;
|
|
429
|
+
else if (ch === ")") depth--;
|
|
430
|
+
if (ch === "," && depth === 0) {
|
|
431
|
+
colors.push(current.trim());
|
|
432
|
+
current = "";
|
|
433
|
+
} else {
|
|
434
|
+
current += ch;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (current.trim()) colors.push(current.trim());
|
|
438
|
+
return colors;
|
|
439
|
+
}
|
|
440
|
+
/** Return black or white depending on perceived lightness */
|
|
441
|
+
#contrastColor(color) {
|
|
442
|
+
const oklchMatch = color.match(/oklch\(\s*([\d.]+)%?\s/);
|
|
443
|
+
if (oklchMatch) {
|
|
444
|
+
const L = parseFloat(oklchMatch[1]);
|
|
445
|
+
const lightness = L > 1 ? L / 100 : L;
|
|
446
|
+
return lightness > 0.6 ? "#000" : "#fff";
|
|
447
|
+
}
|
|
448
|
+
if (color.startsWith("#")) {
|
|
449
|
+
const hex = color.replace("#", "");
|
|
450
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
451
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
452
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
453
|
+
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
454
|
+
return lum > 0.4 ? "#000" : "#fff";
|
|
455
|
+
}
|
|
456
|
+
return "#000";
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
registerComponent("color-palette", ColorPalette);
|
|
460
|
+
|
|
461
|
+
// src/web-components/color-picker/_color-utils.js
|
|
462
|
+
function hslToRgb(h, s, l) {
|
|
463
|
+
s /= 100;
|
|
464
|
+
l /= 100;
|
|
465
|
+
const k = (n) => (n + h / 30) % 12;
|
|
466
|
+
const a = s * Math.min(l, 1 - l);
|
|
467
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
|
|
468
|
+
return {
|
|
469
|
+
r: Math.round(f(0) * 255),
|
|
470
|
+
g: Math.round(f(8) * 255),
|
|
471
|
+
b: Math.round(f(4) * 255)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function rgbToHsl(r, g, b) {
|
|
475
|
+
r /= 255;
|
|
476
|
+
g /= 255;
|
|
477
|
+
b /= 255;
|
|
478
|
+
const max = Math.max(r, g, b);
|
|
479
|
+
const min = Math.min(r, g, b);
|
|
480
|
+
const l = (max + min) / 2;
|
|
481
|
+
let h = 0;
|
|
482
|
+
let s = 0;
|
|
483
|
+
if (max !== min) {
|
|
484
|
+
const d = max - min;
|
|
485
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
486
|
+
switch (max) {
|
|
487
|
+
case r:
|
|
488
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
489
|
+
break;
|
|
490
|
+
case g:
|
|
491
|
+
h = ((b - r) / d + 2) / 6;
|
|
492
|
+
break;
|
|
493
|
+
case b:
|
|
494
|
+
h = ((r - g) / d + 4) / 6;
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
h: Math.round(h * 360),
|
|
500
|
+
s: Math.round(s * 100),
|
|
501
|
+
l: Math.round(l * 100)
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function hexToRgb(hex) {
|
|
505
|
+
hex = hex.replace(/^#/, "");
|
|
506
|
+
if (hex.length === 3) {
|
|
507
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
508
|
+
}
|
|
509
|
+
const n = parseInt(hex, 16);
|
|
510
|
+
return {
|
|
511
|
+
r: n >> 16 & 255,
|
|
512
|
+
g: n >> 8 & 255,
|
|
513
|
+
b: n & 255
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function rgbToHex(r, g, b) {
|
|
517
|
+
return "#" + [r, g, b].map(
|
|
518
|
+
(v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")
|
|
519
|
+
).join("");
|
|
520
|
+
}
|
|
521
|
+
function hexToHsl(hex) {
|
|
522
|
+
const { r, g, b } = hexToRgb(hex);
|
|
523
|
+
return rgbToHsl(r, g, b);
|
|
524
|
+
}
|
|
525
|
+
function hslToHex(h, s, l) {
|
|
526
|
+
const { r, g, b } = hslToRgb(h, s, l);
|
|
527
|
+
return rgbToHex(r, g, b);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/web-components/palette-generator/_palette-utils.js
|
|
531
|
+
var wrap = (h) => (h % 360 + 360) % 360;
|
|
532
|
+
function complementary(h, s, l) {
|
|
533
|
+
return {
|
|
534
|
+
colors: [hslToHex(h, s, l), hslToHex(wrap(h + 180), s, l)],
|
|
535
|
+
names: ["Base", "Complement"]
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function analogous(h, s, l) {
|
|
539
|
+
return {
|
|
540
|
+
colors: [
|
|
541
|
+
hslToHex(wrap(h - 30), s, l),
|
|
542
|
+
hslToHex(h, s, l),
|
|
543
|
+
hslToHex(wrap(h + 30), s, l)
|
|
544
|
+
],
|
|
545
|
+
names: ["Analog A", "Base", "Analog B"]
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function triadic(h, s, l) {
|
|
549
|
+
return {
|
|
550
|
+
colors: [
|
|
551
|
+
hslToHex(h, s, l),
|
|
552
|
+
hslToHex(wrap(h + 120), s, l),
|
|
553
|
+
hslToHex(wrap(h + 240), s, l)
|
|
554
|
+
],
|
|
555
|
+
names: ["Base", "Triad A", "Triad B"]
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function splitComplementary(h, s, l) {
|
|
559
|
+
return {
|
|
560
|
+
colors: [
|
|
561
|
+
hslToHex(h, s, l),
|
|
562
|
+
hslToHex(wrap(h + 150), s, l),
|
|
563
|
+
hslToHex(wrap(h + 210), s, l)
|
|
564
|
+
],
|
|
565
|
+
names: ["Base", "Split A", "Split B"]
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function tetradic(h, s, l) {
|
|
569
|
+
return {
|
|
570
|
+
colors: [
|
|
571
|
+
hslToHex(h, s, l),
|
|
572
|
+
hslToHex(wrap(h + 90), s, l),
|
|
573
|
+
hslToHex(wrap(h + 180), s, l),
|
|
574
|
+
hslToHex(wrap(h + 270), s, l)
|
|
575
|
+
],
|
|
576
|
+
names: ["Base", "Tetrad A", "Tetrad B", "Tetrad C"]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
var MONO_STEPS = [
|
|
580
|
+
{ label: "50", l: 96 },
|
|
581
|
+
{ label: "100", l: 90 },
|
|
582
|
+
{ label: "200", l: 80 },
|
|
583
|
+
{ label: "300", l: 70 },
|
|
584
|
+
{ label: "400", l: 60 },
|
|
585
|
+
{ label: "500", l: 50 },
|
|
586
|
+
{ label: "600", l: 40 },
|
|
587
|
+
{ label: "700", l: 30 },
|
|
588
|
+
{ label: "800", l: 22 },
|
|
589
|
+
{ label: "900", l: 15 },
|
|
590
|
+
{ label: "950", l: 10 }
|
|
591
|
+
];
|
|
592
|
+
function monochromatic(h, s, _l) {
|
|
593
|
+
const colors = [];
|
|
594
|
+
const names = [];
|
|
595
|
+
for (const step of MONO_STEPS) {
|
|
596
|
+
const satAdj = step.l > 85 || step.l < 20 ? Math.max(s * 0.6, 5) : s;
|
|
597
|
+
colors.push(hslToHex(h, Math.round(satAdj), step.l));
|
|
598
|
+
names.push(step.label);
|
|
599
|
+
}
|
|
600
|
+
return { colors, names };
|
|
601
|
+
}
|
|
602
|
+
var algorithms = {
|
|
603
|
+
complementary,
|
|
604
|
+
analogous,
|
|
605
|
+
triadic,
|
|
606
|
+
"split-complementary": splitComplementary,
|
|
607
|
+
tetradic,
|
|
608
|
+
monochromatic
|
|
609
|
+
};
|
|
610
|
+
function generatePalette(hex, harmony) {
|
|
611
|
+
const { h, s, l } = hexToHsl(hex);
|
|
612
|
+
const fn = algorithms[harmony] || algorithms.complementary;
|
|
613
|
+
return fn(h, s, l);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/web-components/palette-generator/logic.js
|
|
617
|
+
var PaletteGenerator = class extends VBElement {
|
|
618
|
+
static observedAttributes = [
|
|
619
|
+
"seed",
|
|
620
|
+
"harmony",
|
|
621
|
+
"include-seed",
|
|
622
|
+
"show-export",
|
|
623
|
+
"layout",
|
|
624
|
+
"size",
|
|
625
|
+
"show-values",
|
|
626
|
+
"show-names"
|
|
627
|
+
];
|
|
628
|
+
/** @type {HTMLElement|null} */
|
|
629
|
+
#pickerEl = null;
|
|
630
|
+
/** @type {string} */
|
|
631
|
+
#currentSeed = "";
|
|
632
|
+
/** @type {string[]} */
|
|
633
|
+
#colors = [];
|
|
634
|
+
/** @type {string[]} */
|
|
635
|
+
#names = [];
|
|
636
|
+
setup() {
|
|
637
|
+
this.#pickerEl = this.querySelector("color-picker") || this.querySelector('input[type="color"]');
|
|
638
|
+
this.#currentSeed = this.#resolveSeed();
|
|
639
|
+
if (!this.#currentSeed) return;
|
|
640
|
+
this.#render();
|
|
641
|
+
this.#wireInteractive();
|
|
642
|
+
this.#emitGenerate();
|
|
643
|
+
}
|
|
644
|
+
attributeChangedCallback() {
|
|
645
|
+
if (this.isConnected && this.hasAttribute("data-upgraded")) {
|
|
646
|
+
this.#currentSeed = this.#resolveSeed();
|
|
647
|
+
if (this.#currentSeed) this.#render();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** Extract seed hex from the picker element or seed attribute */
|
|
651
|
+
#resolveSeed() {
|
|
652
|
+
if (this.#pickerEl) {
|
|
653
|
+
const cp = (
|
|
654
|
+
/** @type {any} */
|
|
655
|
+
this.#pickerEl
|
|
656
|
+
);
|
|
657
|
+
if (cp.tagName === "COLOR-PICKER") {
|
|
658
|
+
const val = cp.value;
|
|
659
|
+
if (val && val !== "#000000") return val;
|
|
660
|
+
const input = (
|
|
661
|
+
/** @type {HTMLInputElement | null} */
|
|
662
|
+
cp.querySelector('input[type="color"]')
|
|
663
|
+
);
|
|
664
|
+
return input?.value || val || "";
|
|
665
|
+
}
|
|
666
|
+
return cp.value || "";
|
|
667
|
+
}
|
|
668
|
+
return this.getAttribute("seed") || "";
|
|
669
|
+
}
|
|
670
|
+
/** Render the full component: picker area + swatches + export toolbar */
|
|
671
|
+
#render() {
|
|
672
|
+
const harmony = this.getAttribute("harmony") || "complementary";
|
|
673
|
+
const layout = this.getAttribute("layout") || "inline";
|
|
674
|
+
const size = this.getAttribute("size") || "md";
|
|
675
|
+
const showValues = this.hasAttribute("show-values");
|
|
676
|
+
const showNames = this.hasAttribute("show-names");
|
|
677
|
+
const showExport = this.hasAttribute("show-export");
|
|
678
|
+
const { colors, names } = generatePalette(this.#currentSeed, harmony);
|
|
679
|
+
this.#colors = colors;
|
|
680
|
+
this.#names = names;
|
|
681
|
+
const sizes = { sm: 48, md: 80, lg: 120 };
|
|
682
|
+
const px = sizes[size] || 80;
|
|
683
|
+
let containerStyle = `display:flex;flex-wrap:wrap;gap:var(--size-xs,0.5rem)`;
|
|
684
|
+
if (layout === "grid") {
|
|
685
|
+
containerStyle = `display:grid;grid-template-columns:repeat(auto-fill,minmax(${px}px,1fr));gap:var(--size-xs,0.5rem)`;
|
|
686
|
+
} else if (layout === "list") {
|
|
687
|
+
containerStyle = `display:flex;flex-direction:column;gap:var(--size-xs,0.5rem)`;
|
|
688
|
+
}
|
|
689
|
+
const swatches = colors.map((color, i) => {
|
|
690
|
+
const name = names[i] || "";
|
|
691
|
+
const contrast = this.#contrastColor(color);
|
|
692
|
+
const wrapStyle = layout === "list" ? `display:flex;flex-direction:row;align-items:center;gap:0.75rem` : `display:flex;flex-direction:column;align-items:center;gap:0.25rem;max-inline-size:${px}px`;
|
|
693
|
+
const boxSize = layout === "list" ? 36 : px;
|
|
694
|
+
const formatted = color.length > 12 ? color.slice(0, 12) + "\u2026" : color;
|
|
695
|
+
return `<div class="swatch-wrap" role="listitem" style="${wrapStyle}">
|
|
696
|
+
<button type="button" class="color-box" data-index="${i}"
|
|
697
|
+
style="background:${color};color:${contrast};width:${boxSize}px;height:${boxSize}px;border:1px solid oklch(0% 0 0/0.15);border-radius:var(--radius-s,0.25rem);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;font-family:var(--font-mono,monospace);position:relative;overflow:hidden;flex-shrink:0"
|
|
698
|
+
title="Click to copy${name ? ": " + name : ""}"
|
|
699
|
+
aria-label="${name || "Color " + (i + 1)}: ${color}">
|
|
700
|
+
<span class="color-value" style="font-size:0.625rem;line-height:1.2;opacity:${showValues ? "1" : "0"};text-align:center;padding:2px 4px;word-break:break-all;transition:opacity 0.15s ease">${formatted}</span>
|
|
701
|
+
</button>
|
|
702
|
+
${showNames && name ? `<span style="font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666);text-align:center;max-inline-size:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${name}</span>` : ""}
|
|
703
|
+
</div>`;
|
|
704
|
+
}).join("");
|
|
705
|
+
let exportHTML = "";
|
|
706
|
+
if (showExport) {
|
|
707
|
+
exportHTML = `<div class="pg-export" role="toolbar" aria-label="Export palette" style="display:flex;gap:0.5rem;margin-block-start:var(--size-xs,0.5rem)">
|
|
708
|
+
<button type="button" class="pg-copy-hex" style="padding:0.25rem 0.75rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface,#fff);color:var(--color-text,#333);cursor:pointer;font-size:var(--font-size-xs,0.75rem);font-family:inherit">Copy Hex</button>
|
|
709
|
+
<button type="button" class="pg-copy-css" style="padding:0.25rem 0.75rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface,#fff);color:var(--color-text,#333);cursor:pointer;font-size:var(--font-size-xs,0.75rem);font-family:inherit">Copy CSS</button>
|
|
710
|
+
</div>`;
|
|
711
|
+
}
|
|
712
|
+
const pickerEl = this.#pickerEl;
|
|
713
|
+
for (const child of [...this.children]) {
|
|
714
|
+
if (child === pickerEl) continue;
|
|
715
|
+
if (child.classList?.contains("pg-seed-label")) continue;
|
|
716
|
+
child.remove();
|
|
717
|
+
}
|
|
718
|
+
if (pickerEl && !this.querySelector(".pg-seed-label")) {
|
|
719
|
+
const label = document.createElement("span");
|
|
720
|
+
label.className = "pg-seed-label";
|
|
721
|
+
label.textContent = "Seed Color";
|
|
722
|
+
pickerEl.before(label);
|
|
723
|
+
}
|
|
724
|
+
const paletteDiv = document.createElement("div");
|
|
725
|
+
paletteDiv.className = `palette ${layout}`;
|
|
726
|
+
paletteDiv.setAttribute("role", "list");
|
|
727
|
+
paletteDiv.setAttribute("aria-label", "Color palette");
|
|
728
|
+
paletteDiv.style.cssText = containerStyle;
|
|
729
|
+
paletteDiv.innerHTML = swatches;
|
|
730
|
+
if (pickerEl) {
|
|
731
|
+
pickerEl.after(paletteDiv);
|
|
732
|
+
} else {
|
|
733
|
+
this.prepend(paletteDiv);
|
|
734
|
+
}
|
|
735
|
+
if (exportHTML) {
|
|
736
|
+
const tmp = document.createElement("div");
|
|
737
|
+
tmp.innerHTML = exportHTML;
|
|
738
|
+
if (tmp.firstElementChild) this.append(tmp.firstElementChild);
|
|
739
|
+
}
|
|
740
|
+
this.#wireSwatches(paletteDiv, colors, names, showValues);
|
|
741
|
+
if (showExport) {
|
|
742
|
+
this.#wireExport();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/** Wire click-to-copy and hover on swatches */
|
|
746
|
+
#wireSwatches(container, colors, names, showValues) {
|
|
747
|
+
container.querySelectorAll(".color-box").forEach((el2) => {
|
|
748
|
+
const btn = (
|
|
749
|
+
/** @type {HTMLElement} */
|
|
750
|
+
el2
|
|
751
|
+
);
|
|
752
|
+
if (!showValues) {
|
|
753
|
+
btn.addEventListener("pointerenter", () => {
|
|
754
|
+
const val = (
|
|
755
|
+
/** @type {HTMLElement | null} */
|
|
756
|
+
btn.querySelector(".color-value")
|
|
757
|
+
);
|
|
758
|
+
if (val) val.style.opacity = "1";
|
|
759
|
+
});
|
|
760
|
+
btn.addEventListener("pointerleave", () => {
|
|
761
|
+
const val = (
|
|
762
|
+
/** @type {HTMLElement | null} */
|
|
763
|
+
btn.querySelector(".color-value")
|
|
764
|
+
);
|
|
765
|
+
if (val) val.style.opacity = "0";
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
btn.addEventListener("click", () => {
|
|
769
|
+
const idx = Number(btn.dataset.index);
|
|
770
|
+
const color = colors[idx];
|
|
771
|
+
const name = names[idx] || "";
|
|
772
|
+
copyText(color, { button: btn, announceMessage: "Color copied" });
|
|
773
|
+
this.dispatchEvent(new CustomEvent("color-palette:select", {
|
|
774
|
+
bubbles: true,
|
|
775
|
+
detail: { color, name, index: idx }
|
|
776
|
+
}));
|
|
777
|
+
btn.style.outline = "3px solid currentColor";
|
|
778
|
+
btn.style.outlineOffset = "2px";
|
|
779
|
+
setTimeout(() => {
|
|
780
|
+
btn.style.outline = "";
|
|
781
|
+
btn.style.outlineOffset = "";
|
|
782
|
+
}, 600);
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/** Wire up interactive picker for live regeneration */
|
|
787
|
+
#wireInteractive() {
|
|
788
|
+
const cp = this.#pickerEl;
|
|
789
|
+
if (!cp) return;
|
|
790
|
+
if (cp.tagName === "COLOR-PICKER") {
|
|
791
|
+
this.listen(cp, "color-picker:change", (e) => {
|
|
792
|
+
this.#currentSeed = /** @type {CustomEvent} */
|
|
793
|
+
e.detail.hex;
|
|
794
|
+
this.#render();
|
|
795
|
+
this.#emitGenerate();
|
|
796
|
+
});
|
|
797
|
+
} else {
|
|
798
|
+
this.listen(cp, "input", () => {
|
|
799
|
+
this.#currentSeed = /** @type {any} */
|
|
800
|
+
cp.value;
|
|
801
|
+
this.#render();
|
|
802
|
+
this.#emitGenerate();
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
/** Wire export toolbar buttons */
|
|
807
|
+
#wireExport() {
|
|
808
|
+
const hexBtn = this.querySelector(".pg-copy-hex");
|
|
809
|
+
const cssBtn = this.querySelector(".pg-copy-css");
|
|
810
|
+
if (hexBtn) {
|
|
811
|
+
hexBtn.addEventListener("click", () => {
|
|
812
|
+
copyText(this.#colors.join(", "), {
|
|
813
|
+
button: (
|
|
814
|
+
/** @type {HTMLElement} */
|
|
815
|
+
hexBtn
|
|
816
|
+
),
|
|
817
|
+
announceMessage: "Hex values copied"
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
if (cssBtn) {
|
|
822
|
+
cssBtn.addEventListener("click", () => {
|
|
823
|
+
const harmony = this.getAttribute("harmony") || "complementary";
|
|
824
|
+
const css = this.#formatCSSExport(harmony);
|
|
825
|
+
copyText(css, {
|
|
826
|
+
button: (
|
|
827
|
+
/** @type {HTMLElement} */
|
|
828
|
+
cssBtn
|
|
829
|
+
),
|
|
830
|
+
announceMessage: "CSS variables copied"
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/** Return black or white depending on perceived lightness */
|
|
836
|
+
#contrastColor(color) {
|
|
837
|
+
if (color.startsWith("#")) {
|
|
838
|
+
const { r, g, b } = hexToRgb(color);
|
|
839
|
+
const lum = 0.2126 * (r / 255) + 0.7152 * (g / 255) + 0.0722 * (b / 255);
|
|
840
|
+
return lum > 0.4 ? "#000" : "#fff";
|
|
841
|
+
}
|
|
842
|
+
return "#000";
|
|
843
|
+
}
|
|
844
|
+
/** Format colors as CSS custom properties */
|
|
845
|
+
#formatCSSExport(harmony) {
|
|
846
|
+
if (harmony === "monochromatic") {
|
|
847
|
+
return this.#colors.map((c, i) => `--color-seed-${this.#names[i] || i + 1}: ${c};`).join("\n");
|
|
848
|
+
}
|
|
849
|
+
return this.#colors.map((c, i) => {
|
|
850
|
+
const name = this.#names[i] ? this.#names[i].toLowerCase().replace(/\s+/g, "-") : `${i + 1}`;
|
|
851
|
+
return `--palette-${name}: ${c};`;
|
|
852
|
+
}).join("\n");
|
|
853
|
+
}
|
|
854
|
+
#emitGenerate() {
|
|
855
|
+
const harmony = this.getAttribute("harmony") || "complementary";
|
|
856
|
+
this.dispatchEvent(new CustomEvent("palette-generator:generate", {
|
|
857
|
+
bubbles: true,
|
|
858
|
+
detail: { colors: [...this.#colors], harmony, seed: this.#currentSeed }
|
|
859
|
+
}));
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
registerComponent("palette-generator", PaletteGenerator);
|
|
863
|
+
|
|
864
|
+
// src/web-components/type-specimen/logic.js
|
|
865
|
+
var QUICK_FONTS = ["system-ui", "Georgia", "Inter, sans-serif", '"JetBrains Mono", monospace', "Verdana", "Cambria", "ui-serif"];
|
|
866
|
+
var TypeSpecimen = class extends VBElement {
|
|
867
|
+
static observedAttributes = ["font-family", "label", "sample", "show-scale", "show-weights", "show-characters", "weights", "editable", "target", "token"];
|
|
868
|
+
setup() {
|
|
869
|
+
this.#render();
|
|
870
|
+
this.#wireEditing();
|
|
871
|
+
}
|
|
872
|
+
attributeChangedCallback() {
|
|
873
|
+
if (this.isConnected) {
|
|
874
|
+
this.#render();
|
|
875
|
+
this.#wireEditing();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
#render() {
|
|
879
|
+
const fontFamily = this.getAttribute("font-family") || "system-ui";
|
|
880
|
+
const label = this.getAttribute("label") || fontFamily.replace(/['"]/g, "").split(",")[0];
|
|
881
|
+
const sample = this.getAttribute("sample") || "The quick brown fox jumps over the lazy dog";
|
|
882
|
+
const showScale = this.hasAttribute("show-scale");
|
|
883
|
+
const showWeights = this.hasAttribute("show-weights");
|
|
884
|
+
const showChars = this.hasAttribute("show-characters");
|
|
885
|
+
const weightsAttr = this.getAttribute("weights") || "300,400,500,600,700";
|
|
886
|
+
const weights = weightsAttr.split(",").map((w) => w.trim());
|
|
887
|
+
let html = "";
|
|
888
|
+
const editable = this.hasAttribute("editable");
|
|
889
|
+
const labelHTML = editable ? `<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)">
|
|
890
|
+
<span class="specimen-quick" style="display:inline-flex;flex-wrap:wrap;gap:0.25rem;margin-inline-start:0.5rem">
|
|
891
|
+
${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("")}
|
|
892
|
+
</span>` : `<span class="specimen-label">${label}</span>`;
|
|
893
|
+
html += `<div class="specimen-header" style="font-family:${fontFamily}">
|
|
894
|
+
${labelHTML}
|
|
895
|
+
<p class="specimen-sample" contenteditable="plaintext-only" spellcheck="false">${sample}</p>
|
|
896
|
+
</div>`;
|
|
897
|
+
if (showChars) {
|
|
898
|
+
html += `<div class="specimen-chars" style="font-family:${fontFamily}">
|
|
899
|
+
<div class="char-row"><span class="char-label">Upper</span>${"ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((c) => `<span>${c}</span>`).join("")}</div>
|
|
900
|
+
<div class="char-row"><span class="char-label">Lower</span>${"abcdefghijklmnopqrstuvwxyz".split("").map((c) => `<span>${c}</span>`).join("")}</div>
|
|
901
|
+
<div class="char-row"><span class="char-label">Digits</span>${"0123456789".split("").map((c) => `<span>${c}</span>`).join("")}</div>
|
|
902
|
+
<div class="char-row"><span class="char-label">Punct</span>${"!@#$%^&*()_+-=[]{}|;:,.<>?".split("").map((c) => `<span>${c === "<" ? "<" : c === ">" ? ">" : c === "&" ? "&" : c}</span>`).join("")}</div>
|
|
903
|
+
</div>`;
|
|
904
|
+
}
|
|
905
|
+
if (showWeights) {
|
|
906
|
+
html += `<div class="specimen-weights">`;
|
|
907
|
+
for (const w of weights) {
|
|
908
|
+
html += `<div class="weight-sample" style="font-family:${fontFamily};font-weight:${w}">
|
|
909
|
+
<span class="weight-label">${w}</span>
|
|
910
|
+
<span class="weight-text">Aa</span>
|
|
911
|
+
</div>`;
|
|
912
|
+
}
|
|
913
|
+
html += `</div>`;
|
|
914
|
+
}
|
|
915
|
+
if (showScale) {
|
|
916
|
+
const scale = [
|
|
917
|
+
{ name: "xs", rem: 0.75 },
|
|
918
|
+
{ name: "sm", rem: 0.875 },
|
|
919
|
+
{ name: "md", rem: 1 },
|
|
920
|
+
{ name: "lg", rem: 1.125 },
|
|
921
|
+
{ name: "xl", rem: 1.25 },
|
|
922
|
+
{ name: "2xl", rem: 1.5 },
|
|
923
|
+
{ name: "3xl", rem: 1.875 },
|
|
924
|
+
{ name: "4xl", rem: 2.25 },
|
|
925
|
+
{ name: "5xl", rem: 3 }
|
|
926
|
+
];
|
|
927
|
+
html += `<div class="specimen-scale">`;
|
|
928
|
+
for (const step of scale) {
|
|
929
|
+
html += `<div class="scale-step" style="font-family:${fontFamily};font-size:${step.rem}rem">
|
|
930
|
+
<span class="scale-label">${step.name}</span>
|
|
931
|
+
<span class="scale-text">${sample.substring(0, 30)}</span>
|
|
932
|
+
</div>`;
|
|
933
|
+
}
|
|
934
|
+
html += `</div>`;
|
|
935
|
+
}
|
|
936
|
+
this.innerHTML = html;
|
|
937
|
+
}
|
|
938
|
+
#wireEditing() {
|
|
939
|
+
if (!this.hasAttribute("editable")) return;
|
|
940
|
+
const input = (
|
|
941
|
+
/** @type {HTMLInputElement | null} */
|
|
942
|
+
this.querySelector(".specimen-font-input")
|
|
943
|
+
);
|
|
944
|
+
if (input) {
|
|
945
|
+
this.listen(input, "input", () => this.#applyFontFamily(input.value));
|
|
946
|
+
}
|
|
947
|
+
this.querySelectorAll(".specimen-quick-btn").forEach((btn) => {
|
|
948
|
+
this.listen(btn, "click", () => {
|
|
949
|
+
const font = btn.getAttribute("data-font") || "";
|
|
950
|
+
if (input) input.value = font;
|
|
951
|
+
this.#applyFontFamily(font);
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
#applyFontFamily(value) {
|
|
956
|
+
const target = (
|
|
957
|
+
/** @type {HTMLElement | null} */
|
|
958
|
+
this.#resolveTarget()
|
|
959
|
+
);
|
|
960
|
+
const token = this.getAttribute("token") || "font-family-base";
|
|
961
|
+
if (target) target.style.setProperty(`--${token}`, value);
|
|
962
|
+
this.setAttribute("font-family", value);
|
|
963
|
+
this.dispatchEvent(new CustomEvent("type-specimen:change", {
|
|
964
|
+
bubbles: true,
|
|
965
|
+
detail: { fontFamily: value, token, target: this.getAttribute("target") || ":root" }
|
|
966
|
+
}));
|
|
967
|
+
}
|
|
968
|
+
#resolveTarget() {
|
|
969
|
+
const sel = this.getAttribute("target") || ":root";
|
|
970
|
+
try {
|
|
971
|
+
return sel === ":root" ? document.documentElement : document.querySelector(sel);
|
|
972
|
+
} catch {
|
|
973
|
+
return document.documentElement;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
registerComponent("type-specimen", TypeSpecimen);
|
|
978
|
+
|
|
979
|
+
// src/web-components/spacing-specimen/logic.js
|
|
980
|
+
var SpacingSpecimen = class extends VBElement {
|
|
981
|
+
static observedAttributes = ["tokens", "prefix", "show-values", "label", "editable", "target"];
|
|
982
|
+
setup() {
|
|
983
|
+
this.#render();
|
|
984
|
+
this.#wireEditing();
|
|
985
|
+
}
|
|
986
|
+
attributeChangedCallback() {
|
|
987
|
+
if (this.isConnected) {
|
|
988
|
+
this.#render();
|
|
989
|
+
this.#wireEditing();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
#render() {
|
|
993
|
+
const tokensAttr = this.getAttribute("tokens") || "3xs,2xs,xs,s,m,l,xl,2xl,3xl";
|
|
994
|
+
const prefix = this.getAttribute("prefix") || "--size-";
|
|
995
|
+
const showValues = this.getAttribute("show-values") !== "false";
|
|
996
|
+
const label = this.getAttribute("label") || "";
|
|
997
|
+
const editable = this.hasAttribute("editable");
|
|
998
|
+
const tokens = tokensAttr.split(",").map((t) => t.trim());
|
|
999
|
+
let html = "";
|
|
1000
|
+
if (label) {
|
|
1001
|
+
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>`;
|
|
1002
|
+
}
|
|
1003
|
+
html += `<div role="list" aria-label="${label || "Spacing scale"}" style="display:flex;flex-direction:column;gap:0.25rem">`;
|
|
1004
|
+
for (const name of tokens) {
|
|
1005
|
+
const varName = `${prefix}${name}`;
|
|
1006
|
+
const valueControl = editable ? `<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">` : 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>` : "";
|
|
1007
|
+
html += `<div role="listitem" style="display:grid;grid-template-columns:3rem 1fr auto;align-items:center;gap:0.75rem;min-block-size:1.75rem">
|
|
1008
|
+
<span style="font-family:var(--font-mono,monospace);font-size:0.875rem;color:var(--color-text-muted,#666);text-align:end">${name}</span>
|
|
1009
|
+
<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>
|
|
1010
|
+
${valueControl}
|
|
1011
|
+
</div>`;
|
|
1012
|
+
}
|
|
1013
|
+
html += "</div>";
|
|
1014
|
+
this.innerHTML = html;
|
|
1015
|
+
requestAnimationFrame(() => {
|
|
1016
|
+
if (editable) {
|
|
1017
|
+
const cs = getComputedStyle(this);
|
|
1018
|
+
this.querySelectorAll(".scale-edit").forEach((node) => {
|
|
1019
|
+
const input = (
|
|
1020
|
+
/** @type {HTMLInputElement} */
|
|
1021
|
+
node
|
|
1022
|
+
);
|
|
1023
|
+
const name = input.getAttribute("data-token");
|
|
1024
|
+
const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
|
|
1025
|
+
input.value = raw || "";
|
|
1026
|
+
});
|
|
1027
|
+
} else if (showValues) {
|
|
1028
|
+
this.querySelectorAll(".scale-bar").forEach((bar) => {
|
|
1029
|
+
const px = bar.getBoundingClientRect().width;
|
|
1030
|
+
const valueEl = bar.nextElementSibling;
|
|
1031
|
+
if (valueEl) valueEl.textContent = `${Math.round(px * 100) / 100}px`;
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
#wireEditing() {
|
|
1037
|
+
if (!this.hasAttribute("editable")) return;
|
|
1038
|
+
const prefix = this.getAttribute("prefix") || "--size-";
|
|
1039
|
+
this.querySelectorAll(".scale-edit").forEach((input) => {
|
|
1040
|
+
this.listen(input, "change", () => this.#applyTokenEdit(input, prefix));
|
|
1041
|
+
this.listen(input, "keydown", (e) => {
|
|
1042
|
+
if (
|
|
1043
|
+
/** @type {KeyboardEvent} */
|
|
1044
|
+
e.key === "Enter"
|
|
1045
|
+
) this.#applyTokenEdit(input, prefix);
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
#applyTokenEdit(input, prefix) {
|
|
1050
|
+
const name = input.getAttribute("data-token");
|
|
1051
|
+
const value = input.value.trim();
|
|
1052
|
+
if (!name || !value) return;
|
|
1053
|
+
const target = (
|
|
1054
|
+
/** @type {HTMLElement | null} */
|
|
1055
|
+
this.#resolveTarget()
|
|
1056
|
+
);
|
|
1057
|
+
const token = `${prefix}${name}`;
|
|
1058
|
+
if (target) target.style.setProperty(token, value);
|
|
1059
|
+
this.dispatchEvent(new CustomEvent("spacing-specimen:change", {
|
|
1060
|
+
bubbles: true,
|
|
1061
|
+
detail: { name, value, token, target: this.getAttribute("target") || ":root" }
|
|
1062
|
+
}));
|
|
1063
|
+
}
|
|
1064
|
+
#resolveTarget() {
|
|
1065
|
+
const sel = this.getAttribute("target") || ":root";
|
|
1066
|
+
try {
|
|
1067
|
+
return sel === ":root" ? document.documentElement : document.querySelector(sel);
|
|
1068
|
+
} catch {
|
|
1069
|
+
return document.documentElement;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
registerComponent("spacing-specimen", SpacingSpecimen);
|
|
1074
|
+
|
|
1075
|
+
// src/web-components/token-specimen/logic.js
|
|
1076
|
+
var TYPE_DEFAULTS = {
|
|
1077
|
+
shadow: {
|
|
1078
|
+
prefix: "--shadow-",
|
|
1079
|
+
tokens: "xs,s,m,l,xl,2xl"
|
|
1080
|
+
},
|
|
1081
|
+
radius: {
|
|
1082
|
+
prefix: "--radius-",
|
|
1083
|
+
tokens: "xs,s,m,l,xl,2xl,full"
|
|
1084
|
+
},
|
|
1085
|
+
border: {
|
|
1086
|
+
prefix: "--border-width-",
|
|
1087
|
+
tokens: "thin,medium,thick"
|
|
1088
|
+
},
|
|
1089
|
+
color: {
|
|
1090
|
+
prefix: "--color-",
|
|
1091
|
+
tokens: "primary,secondary,accent,success,warning,error,info"
|
|
1092
|
+
},
|
|
1093
|
+
size: {
|
|
1094
|
+
prefix: "--size-",
|
|
1095
|
+
tokens: "3xs,2xs,xs,s,m,l,xl,2xl,3xl"
|
|
1096
|
+
},
|
|
1097
|
+
icon: {
|
|
1098
|
+
prefix: "",
|
|
1099
|
+
tokens: "home,search,settings,user,bell,heart,star,check,x,chevron-right,menu,trash"
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
var RENDERERS = {
|
|
1103
|
+
shadow: renderShadow,
|
|
1104
|
+
radius: renderRadius,
|
|
1105
|
+
border: renderBorder,
|
|
1106
|
+
color: renderColor,
|
|
1107
|
+
size: renderSize,
|
|
1108
|
+
icon: renderIcon
|
|
1109
|
+
};
|
|
1110
|
+
var TokenSpecimen = class extends VBElement {
|
|
1111
|
+
static observedAttributes = ["type", "tokens", "prefix", "show-values", "label", "size", "icon-set", "editable", "target"];
|
|
1112
|
+
setup() {
|
|
1113
|
+
this.#render();
|
|
1114
|
+
}
|
|
1115
|
+
attributeChangedCallback() {
|
|
1116
|
+
if (this.isConnected) this.#render();
|
|
1117
|
+
}
|
|
1118
|
+
#render() {
|
|
1119
|
+
const type = this.getAttribute("type") || "shadow";
|
|
1120
|
+
const defaults = TYPE_DEFAULTS[type] || TYPE_DEFAULTS.shadow;
|
|
1121
|
+
const prefix = this.getAttribute("prefix") || defaults.prefix;
|
|
1122
|
+
const tokensAttr = this.getAttribute("tokens") || defaults.tokens;
|
|
1123
|
+
const showValues = this.getAttribute("show-values") !== "false";
|
|
1124
|
+
const label = this.getAttribute("label") || "";
|
|
1125
|
+
const tokens = tokensAttr.split(",").map((t) => t.trim());
|
|
1126
|
+
const renderer = RENDERERS[type] || RENDERERS.shadow;
|
|
1127
|
+
let html = "";
|
|
1128
|
+
if (label) {
|
|
1129
|
+
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>`;
|
|
1130
|
+
}
|
|
1131
|
+
html += renderer(tokens, prefix, showValues, this);
|
|
1132
|
+
this.innerHTML = html;
|
|
1133
|
+
const editable = this.hasAttribute("editable") && type !== "icon";
|
|
1134
|
+
if (showValues && type !== "icon") {
|
|
1135
|
+
requestAnimationFrame(() => {
|
|
1136
|
+
this.#readComputedValues(type, prefix, tokens);
|
|
1137
|
+
if (editable) this.#injectEditors(type, prefix);
|
|
1138
|
+
});
|
|
1139
|
+
} else if (editable) {
|
|
1140
|
+
requestAnimationFrame(() => this.#injectEditors(type, prefix));
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
#injectEditors(type, prefix) {
|
|
1144
|
+
const cs = getComputedStyle(this);
|
|
1145
|
+
this.querySelectorAll("[data-token-value]").forEach((node) => {
|
|
1146
|
+
const el2 = (
|
|
1147
|
+
/** @type {HTMLElement} */
|
|
1148
|
+
node
|
|
1149
|
+
);
|
|
1150
|
+
const name = el2.dataset.tokenValue;
|
|
1151
|
+
const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
|
|
1152
|
+
const input = document.createElement("input");
|
|
1153
|
+
input.type = type === "color" && /^#[0-9a-f]{3,8}$/i.test(raw) ? "color" : "text";
|
|
1154
|
+
input.value = raw || "";
|
|
1155
|
+
input.className = "ts-edit";
|
|
1156
|
+
input.setAttribute("data-token", name || "");
|
|
1157
|
+
input.setAttribute("aria-label", `${name} value`);
|
|
1158
|
+
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;"}`;
|
|
1159
|
+
el2.replaceWith(input);
|
|
1160
|
+
this.listen(input, "change", () => this.#applyEdit(input, prefix));
|
|
1161
|
+
this.listen(input, "keydown", (e) => {
|
|
1162
|
+
if (
|
|
1163
|
+
/** @type {KeyboardEvent} */
|
|
1164
|
+
e.key === "Enter"
|
|
1165
|
+
) this.#applyEdit(input, prefix);
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
#applyEdit(input, prefix) {
|
|
1170
|
+
const name = input.getAttribute("data-token");
|
|
1171
|
+
const value = input.value.trim();
|
|
1172
|
+
if (!name || !value) return;
|
|
1173
|
+
const target = (
|
|
1174
|
+
/** @type {HTMLElement | null} */
|
|
1175
|
+
this.#resolveTarget()
|
|
1176
|
+
);
|
|
1177
|
+
const token = `${prefix}${name}`;
|
|
1178
|
+
if (target) target.style.setProperty(token, value);
|
|
1179
|
+
this.dispatchEvent(new CustomEvent("token-specimen:change", {
|
|
1180
|
+
bubbles: true,
|
|
1181
|
+
detail: { name, value, token, target: this.getAttribute("target") || ":root" }
|
|
1182
|
+
}));
|
|
1183
|
+
}
|
|
1184
|
+
#resolveTarget() {
|
|
1185
|
+
const sel = this.getAttribute("target") || ":root";
|
|
1186
|
+
try {
|
|
1187
|
+
return sel === ":root" ? document.documentElement : document.querySelector(sel);
|
|
1188
|
+
} catch {
|
|
1189
|
+
return document.documentElement;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
#readComputedValues(type, prefix, tokens) {
|
|
1193
|
+
const cs = getComputedStyle(this);
|
|
1194
|
+
this.querySelectorAll("[data-token-value]").forEach((node) => {
|
|
1195
|
+
const el2 = (
|
|
1196
|
+
/** @type {HTMLElement} */
|
|
1197
|
+
node
|
|
1198
|
+
);
|
|
1199
|
+
const name = el2.dataset.tokenValue;
|
|
1200
|
+
const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
|
|
1201
|
+
if (type === "radius" || type === "size") {
|
|
1202
|
+
const sample = this.querySelector(`[data-token-sample="${name}"]`);
|
|
1203
|
+
if (sample) {
|
|
1204
|
+
const prop = type === "radius" ? "borderRadius" : "width";
|
|
1205
|
+
const rect = type === "size" ? sample.getBoundingClientRect().width : null;
|
|
1206
|
+
el2.textContent = rect != null ? `${Math.round(rect * 100) / 100}px` : raw || "\u2014";
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
el2.textContent = raw || "\u2014";
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
function renderShadow(tokens, prefix, showValues) {
|
|
1215
|
+
let html = `<div role="list" style="display:flex;flex-wrap:wrap;gap:1rem;align-items:end">`;
|
|
1216
|
+
for (const name of tokens) {
|
|
1217
|
+
html += `<div role="listitem" style="text-align:center">
|
|
1218
|
+
<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>
|
|
1219
|
+
<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>
|
|
1220
|
+
${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>` : ""}
|
|
1221
|
+
</div>`;
|
|
1222
|
+
}
|
|
1223
|
+
html += "</div>";
|
|
1224
|
+
return html;
|
|
1225
|
+
}
|
|
1226
|
+
function renderRadius(tokens, prefix, showValues) {
|
|
1227
|
+
let html = `<div role="list" style="display:flex;flex-wrap:wrap;gap:1rem;align-items:end">`;
|
|
1228
|
+
for (const name of tokens) {
|
|
1229
|
+
html += `<div role="listitem" style="text-align:center">
|
|
1230
|
+
<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>
|
|
1231
|
+
${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>` : ""}
|
|
1232
|
+
</div>`;
|
|
1233
|
+
}
|
|
1234
|
+
html += "</div>";
|
|
1235
|
+
return html;
|
|
1236
|
+
}
|
|
1237
|
+
function renderBorder(tokens, prefix, showValues) {
|
|
1238
|
+
let html = `<div role="list" style="display:flex;flex-direction:column;gap:0.75rem">`;
|
|
1239
|
+
for (const name of tokens) {
|
|
1240
|
+
html += `<div role="listitem" style="display:grid;grid-template-columns:4rem 1fr auto;align-items:center;gap:0.75rem">
|
|
1241
|
+
<span style="font-family:var(--font-mono,monospace);font-size:0.875rem;color:var(--color-text-muted,#666);text-align:end">${name}</span>
|
|
1242
|
+
<div style="border-block-start:var(${prefix}${name}) solid var(--color-text,#333);min-inline-size:4rem" aria-hidden="true"></div>
|
|
1243
|
+
${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>` : ""}
|
|
1244
|
+
</div>`;
|
|
1245
|
+
}
|
|
1246
|
+
html += "</div>";
|
|
1247
|
+
return html;
|
|
1248
|
+
}
|
|
1249
|
+
function renderColor(tokens, prefix, showValues) {
|
|
1250
|
+
let html = `<div role="list" style="display:flex;flex-wrap:wrap;gap:0.75rem">`;
|
|
1251
|
+
for (const name of tokens) {
|
|
1252
|
+
html += `<div role="listitem" style="text-align:center">
|
|
1253
|
+
<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>
|
|
1254
|
+
<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>
|
|
1255
|
+
${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>` : ""}
|
|
1256
|
+
</div>`;
|
|
1257
|
+
}
|
|
1258
|
+
html += "</div>";
|
|
1259
|
+
return html;
|
|
1260
|
+
}
|
|
1261
|
+
function renderSize(tokens, prefix, showValues) {
|
|
1262
|
+
let html = `<div role="list" style="display:flex;flex-direction:column;gap:0.25rem">`;
|
|
1263
|
+
for (const name of tokens) {
|
|
1264
|
+
html += `<div role="listitem" style="display:grid;grid-template-columns:3rem 1fr auto;align-items:center;gap:0.75rem;min-block-size:1.75rem">
|
|
1265
|
+
<span style="font-family:var(--font-mono,monospace);font-size:0.875rem;color:var(--color-text-muted,#666);text-align:end">${name}</span>
|
|
1266
|
+
<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>
|
|
1267
|
+
${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>` : ""}
|
|
1268
|
+
</div>`;
|
|
1269
|
+
}
|
|
1270
|
+
html += "</div>";
|
|
1271
|
+
return html;
|
|
1272
|
+
}
|
|
1273
|
+
function renderIcon(tokens, _prefix, showValues, host) {
|
|
1274
|
+
const size = host?.getAttribute("size") || "md";
|
|
1275
|
+
const iconSet = host?.getAttribute("icon-set") || "";
|
|
1276
|
+
const setAttr = iconSet ? ` set="${iconSet}"` : "";
|
|
1277
|
+
let html = `<div role="list" style="display:flex;flex-wrap:wrap;gap:1rem">`;
|
|
1278
|
+
for (const name of tokens) {
|
|
1279
|
+
html += `<div role="listitem" style="display:flex;flex-direction:column;align-items:center;gap:0.375rem;min-inline-size:4.5rem">
|
|
1280
|
+
<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)">
|
|
1281
|
+
<icon-wc name="${name}" size="${size}"${setAttr}></icon-wc>
|
|
1282
|
+
</span>
|
|
1283
|
+
${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>` : ""}
|
|
1284
|
+
</div>`;
|
|
1285
|
+
}
|
|
1286
|
+
html += "</div>";
|
|
1287
|
+
return html;
|
|
1288
|
+
}
|
|
1289
|
+
registerComponent("token-specimen", TokenSpecimen);
|
|
1290
|
+
|
|
1291
|
+
// src/web-components/component-sampler/logic.js
|
|
1292
|
+
var COMPONENT_RENDERERS = {
|
|
1293
|
+
button: () => `
|
|
1294
|
+
<layout-cluster data-layout-gap="s">
|
|
1295
|
+
<button>Primary</button>
|
|
1296
|
+
<button class="secondary">Secondary</button>
|
|
1297
|
+
<button disabled>Disabled</button>
|
|
1298
|
+
</layout-cluster>`,
|
|
1299
|
+
input: () => `
|
|
1300
|
+
<layout-stack data-layout-gap="xs">
|
|
1301
|
+
<input type="text" placeholder="Text input" aria-label="Sample text input"/>
|
|
1302
|
+
<input type="email" placeholder="email@example.com" aria-label="Sample email"/>
|
|
1303
|
+
<input type="text" value="Read only" readonly aria-label="Read-only input"/>
|
|
1304
|
+
</layout-stack>`,
|
|
1305
|
+
select: () => `
|
|
1306
|
+
<select aria-label="Sample select">
|
|
1307
|
+
<option>Choose an option</option>
|
|
1308
|
+
<option>Option A</option>
|
|
1309
|
+
<option>Option B</option>
|
|
1310
|
+
<option>Option C</option>
|
|
1311
|
+
</select>`,
|
|
1312
|
+
checkbox: () => `
|
|
1313
|
+
<layout-stack data-layout-gap="xs">
|
|
1314
|
+
<label><input type="checkbox" checked="checked"/> Checked option</label>
|
|
1315
|
+
<label><input type="checkbox"/> Unchecked option</label>
|
|
1316
|
+
<label><input type="checkbox" disabled/> Disabled option</label>
|
|
1317
|
+
</layout-stack>`,
|
|
1318
|
+
radio: () => `
|
|
1319
|
+
<layout-stack data-layout-gap="xs">
|
|
1320
|
+
<label><input type="radio" name="sampler-radio" checked="checked"/> Selected</label>
|
|
1321
|
+
<label><input type="radio" name="sampler-radio"/> Unselected</label>
|
|
1322
|
+
<label><input type="radio" name="sampler-radio" disabled/> Disabled</label>
|
|
1323
|
+
</layout-stack>`,
|
|
1324
|
+
badge: () => `
|
|
1325
|
+
<layout-cluster data-layout-gap="xs">
|
|
1326
|
+
<layout-badge>Default</layout-badge>
|
|
1327
|
+
<layout-badge data-color="success">Success</layout-badge>
|
|
1328
|
+
<layout-badge data-color="warning">Warning</layout-badge>
|
|
1329
|
+
<layout-badge data-color="danger">Danger</layout-badge>
|
|
1330
|
+
<layout-badge data-color="info">Info</layout-badge>
|
|
1331
|
+
</layout-cluster>`,
|
|
1332
|
+
progress: () => `
|
|
1333
|
+
<layout-stack data-layout-gap="xs">
|
|
1334
|
+
<progress value="33" max="100">33%</progress>
|
|
1335
|
+
<progress value="66" max="100">66%</progress>
|
|
1336
|
+
<progress value="100" max="100">100%</progress>
|
|
1337
|
+
</layout-stack>`,
|
|
1338
|
+
range: () => `
|
|
1339
|
+
<input type="range" min="0" max="100" value="50" aria-label="Sample range"/>`,
|
|
1340
|
+
textarea: () => `
|
|
1341
|
+
<textarea rows="3" placeholder="Textarea sample" aria-label="Sample textarea" style="width:100%"></textarea>`
|
|
1342
|
+
};
|
|
1343
|
+
var ComponentSampler = class extends VBElement {
|
|
1344
|
+
static observedAttributes = ["components", "label", "compact"];
|
|
1345
|
+
setup() {
|
|
1346
|
+
this.#render();
|
|
1347
|
+
}
|
|
1348
|
+
attributeChangedCallback() {
|
|
1349
|
+
if (this.isConnected) this.#render();
|
|
1350
|
+
}
|
|
1351
|
+
#render() {
|
|
1352
|
+
const componentsAttr = this.getAttribute("components") || "button,input,select,checkbox,radio,badge,progress";
|
|
1353
|
+
const label = this.getAttribute("label") || "";
|
|
1354
|
+
const compact = this.hasAttribute("compact");
|
|
1355
|
+
const components2 = componentsAttr.split(",").map((c) => c.trim());
|
|
1356
|
+
const gap = compact ? "0.75rem" : "1.25rem";
|
|
1357
|
+
let html = "";
|
|
1358
|
+
if (label) {
|
|
1359
|
+
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>`;
|
|
1360
|
+
}
|
|
1361
|
+
html += `<section style="display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:${gap}">`;
|
|
1362
|
+
for (const name of components2) {
|
|
1363
|
+
const renderer = COMPONENT_RENDERERS[name];
|
|
1364
|
+
if (!renderer) continue;
|
|
1365
|
+
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)">
|
|
1366
|
+
<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>
|
|
1367
|
+
${renderer()}
|
|
1368
|
+
</article>`;
|
|
1369
|
+
}
|
|
1370
|
+
html += "</section>";
|
|
1371
|
+
this.innerHTML = html;
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
registerComponent("component-sampler", ComponentSampler);
|
|
1375
|
+
|
|
1376
|
+
// src/web-components/semantic-palette/_color-math.js
|
|
1377
|
+
var srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
1378
|
+
var linearToSrgb = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
1379
|
+
var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
1380
|
+
function hexToOklch(hex) {
|
|
1381
|
+
const { r, g, b } = hexToRgb(hex);
|
|
1382
|
+
const lr = srgbToLinear(r / 255);
|
|
1383
|
+
const lg = srgbToLinear(g / 255);
|
|
1384
|
+
const lb = srgbToLinear(b / 255);
|
|
1385
|
+
const lcone = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
|
|
1386
|
+
const mcone = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
|
|
1387
|
+
const scone = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
|
|
1388
|
+
const lp = Math.cbrt(lcone);
|
|
1389
|
+
const mp = Math.cbrt(mcone);
|
|
1390
|
+
const sp = Math.cbrt(scone);
|
|
1391
|
+
const L = 0.2104542553 * lp + 0.793617785 * mp - 0.0040720468 * sp;
|
|
1392
|
+
const A = 1.9779984951 * lp - 2.428592205 * mp + 0.4505937099 * sp;
|
|
1393
|
+
const B = 0.0259040371 * lp + 0.7827717662 * mp - 0.808675766 * sp;
|
|
1394
|
+
const c = Math.sqrt(A * A + B * B);
|
|
1395
|
+
let h = Math.atan2(B, A) * 180 / Math.PI;
|
|
1396
|
+
if (h < 0) h += 360;
|
|
1397
|
+
return { l: L, c, h };
|
|
1398
|
+
}
|
|
1399
|
+
function oklchToHex(l, c, h) {
|
|
1400
|
+
const hr = h * Math.PI / 180;
|
|
1401
|
+
const A = c * Math.cos(hr);
|
|
1402
|
+
const B = c * Math.sin(hr);
|
|
1403
|
+
const lp = l + 0.3963377774 * A + 0.2158037573 * B;
|
|
1404
|
+
const mp = l - 0.1055613458 * A - 0.0638541728 * B;
|
|
1405
|
+
const sp = l - 0.0894841775 * A - 1.291485548 * B;
|
|
1406
|
+
const lcone = lp * lp * lp;
|
|
1407
|
+
const mcone = mp * mp * mp;
|
|
1408
|
+
const scone = sp * sp * sp;
|
|
1409
|
+
const lr = 4.0767416621 * lcone - 3.3077115913 * mcone + 0.2309699292 * scone;
|
|
1410
|
+
const lg = -1.2684380046 * lcone + 2.6097574011 * mcone - 0.3413193965 * scone;
|
|
1411
|
+
const lb = -0.0041960863 * lcone - 0.7034186147 * mcone + 1.707614701 * scone;
|
|
1412
|
+
const r = clamp01(linearToSrgb(lr));
|
|
1413
|
+
const g = clamp01(linearToSrgb(lg));
|
|
1414
|
+
const b = clamp01(linearToSrgb(lb));
|
|
1415
|
+
return rgbToHex(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255));
|
|
1416
|
+
}
|
|
1417
|
+
function resolveToHex(input, probeEl) {
|
|
1418
|
+
if (!input) return null;
|
|
1419
|
+
const s = String(input).trim();
|
|
1420
|
+
if (/^#[0-9a-f]{6}$/i.test(s)) return s.toLowerCase();
|
|
1421
|
+
if (/^#[0-9a-f]{3}$/i.test(s)) {
|
|
1422
|
+
return ("#" + s.slice(1).split("").map((c) => c + c).join("")).toLowerCase();
|
|
1423
|
+
}
|
|
1424
|
+
if (typeof document === "undefined") return null;
|
|
1425
|
+
const probe = probeEl || document.createElement("span");
|
|
1426
|
+
if (!probeEl) {
|
|
1427
|
+
probe.style.display = "none";
|
|
1428
|
+
document.body.appendChild(probe);
|
|
1429
|
+
}
|
|
1430
|
+
probe.style.color = "";
|
|
1431
|
+
probe.style.color = s;
|
|
1432
|
+
const computed = getComputedStyle(probe).color;
|
|
1433
|
+
if (!probeEl) probe.remove();
|
|
1434
|
+
const m = computed.match(/rgba?\(\s*([0-9.]+)[\s,]+([0-9.]+)[\s,]+([0-9.]+)/);
|
|
1435
|
+
if (!m) return null;
|
|
1436
|
+
return rgbToHex(Math.round(+m[1]), Math.round(+m[2]), Math.round(+m[3]));
|
|
1437
|
+
}
|
|
1438
|
+
function formatOklch({ l, c, h }) {
|
|
1439
|
+
return `oklch(${(l * 100).toFixed(1)}% ${c.toFixed(3)} ${h.toFixed(1)})`;
|
|
1440
|
+
}
|
|
1441
|
+
function relativeLuminance(hex) {
|
|
1442
|
+
const { r, g, b } = hexToRgb(hex);
|
|
1443
|
+
const channel = (v) => {
|
|
1444
|
+
const s = v / 255;
|
|
1445
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
1446
|
+
};
|
|
1447
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
1448
|
+
}
|
|
1449
|
+
function contrastRatio(hex1, hex2) {
|
|
1450
|
+
const l1 = relativeLuminance(hex1);
|
|
1451
|
+
const l2 = relativeLuminance(hex2);
|
|
1452
|
+
const lighter = Math.max(l1, l2);
|
|
1453
|
+
const darker = Math.min(l1, l2);
|
|
1454
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
1455
|
+
}
|
|
1456
|
+
function wcagLevel(ratio) {
|
|
1457
|
+
if (ratio >= 7) return "AAA";
|
|
1458
|
+
if (ratio >= 4.5) return "AA";
|
|
1459
|
+
return "Fail";
|
|
1460
|
+
}
|
|
1461
|
+
function pickContrastingHex(hex) {
|
|
1462
|
+
return relativeLuminance(hex) > 0.4 ? "#000000" : "#ffffff";
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/web-components/semantic-palette/logic.js
|
|
1466
|
+
var BRAND_ROLES = ["primary", "secondary", "accent"];
|
|
1467
|
+
var STATUS_ROLES = ["success", "warning", "error", "info"];
|
|
1468
|
+
var ALL_ROLES = [...BRAND_ROLES, ...STATUS_ROLES];
|
|
1469
|
+
var ROLE_LABELS = {
|
|
1470
|
+
primary: "Primary",
|
|
1471
|
+
secondary: "Secondary",
|
|
1472
|
+
accent: "Accent",
|
|
1473
|
+
success: "Success",
|
|
1474
|
+
warning: "Warning",
|
|
1475
|
+
error: "Error",
|
|
1476
|
+
info: "Info"
|
|
1477
|
+
};
|
|
1478
|
+
var LEVEL_COLORS = {
|
|
1479
|
+
aaa: { bg: "oklch(92% 0.08 145)", fg: "oklch(30% 0.12 145)" },
|
|
1480
|
+
aa: { bg: "oklch(94% 0.08 90)", fg: "oklch(35% 0.12 70)" },
|
|
1481
|
+
fail: { bg: "oklch(92% 0.08 25)", fg: "oklch(35% 0.15 25)" }
|
|
1482
|
+
};
|
|
1483
|
+
var MONO = "var(--font-mono, monospace)";
|
|
1484
|
+
var MUTED = "var(--color-text-muted, #666)";
|
|
1485
|
+
var BORDER = "var(--color-border, #ddd)";
|
|
1486
|
+
var SURFACE = "var(--color-surface, #fff)";
|
|
1487
|
+
var TEXT = "var(--color-text, #222)";
|
|
1488
|
+
var RADIUS_S = "var(--radius-s, 0.25rem)";
|
|
1489
|
+
var RADIUS_M = "var(--radius-m, 0.5rem)";
|
|
1490
|
+
var SemanticPalette = class extends VBElement {
|
|
1491
|
+
static observedAttributes = ["colors", "roles", "show-export", "label"];
|
|
1492
|
+
/** @type {string[]} */
|
|
1493
|
+
#colors = [];
|
|
1494
|
+
/** @type {string[]} */
|
|
1495
|
+
#roles = [];
|
|
1496
|
+
/** @type {HTMLElement|null} */
|
|
1497
|
+
#source = null;
|
|
1498
|
+
/** @type {HTMLElement|null} */
|
|
1499
|
+
#previews = null;
|
|
1500
|
+
/** @type {HTMLElement|null} */
|
|
1501
|
+
#probe = null;
|
|
1502
|
+
setup() {
|
|
1503
|
+
this.#probe = document.createElement("span");
|
|
1504
|
+
this.#probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none";
|
|
1505
|
+
this.append(this.#probe);
|
|
1506
|
+
this.#source = this.querySelector("color-palette, palette-generator");
|
|
1507
|
+
this.#resolveRoles();
|
|
1508
|
+
this.#readColors();
|
|
1509
|
+
this.#renderShell();
|
|
1510
|
+
this.#renderPreviews("init");
|
|
1511
|
+
this.#wireSource();
|
|
1512
|
+
this.#wireExport();
|
|
1513
|
+
this.#emitChange("init");
|
|
1514
|
+
}
|
|
1515
|
+
attributeChangedCallback(name) {
|
|
1516
|
+
if (!this.isConnected || !this.hasAttribute("data-upgraded")) return;
|
|
1517
|
+
if (name === "roles") this.#resolveRoles();
|
|
1518
|
+
if (name === "colors") this.#readColors();
|
|
1519
|
+
this.#renderShell();
|
|
1520
|
+
this.#renderPreviews("attribute");
|
|
1521
|
+
this.#wireExport();
|
|
1522
|
+
this.#emitChange("attribute");
|
|
1523
|
+
}
|
|
1524
|
+
teardown() {
|
|
1525
|
+
this.#probe?.remove();
|
|
1526
|
+
this.#probe = null;
|
|
1527
|
+
}
|
|
1528
|
+
// ── Source + state ─────────────────────────────────────────────────
|
|
1529
|
+
#readColors() {
|
|
1530
|
+
const cp = this.querySelector("color-palette");
|
|
1531
|
+
if (cp) {
|
|
1532
|
+
if (typeof /** @type {any} */
|
|
1533
|
+
cp.colors !== "undefined" && /** @type {any} */
|
|
1534
|
+
cp.colors.length) {
|
|
1535
|
+
this.#colors = /** @type {any} */
|
|
1536
|
+
cp.colors.slice();
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const raw2 = cp.getAttribute("colors") || "";
|
|
1540
|
+
const parsed = raw2.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1541
|
+
if (parsed.length) {
|
|
1542
|
+
this.#colors = parsed;
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
const pg = this.querySelector("palette-generator");
|
|
1547
|
+
if (pg) {
|
|
1548
|
+
const seed = pg.getAttribute("seed") || /** @type {HTMLInputElement | null} */
|
|
1549
|
+
pg.querySelector('input[type="color"]')?.value || "";
|
|
1550
|
+
const harmony = pg.getAttribute("harmony") || "complementary";
|
|
1551
|
+
if (seed) {
|
|
1552
|
+
try {
|
|
1553
|
+
const { colors } = generatePalette(seed, harmony);
|
|
1554
|
+
this.#colors = colors.slice();
|
|
1555
|
+
return;
|
|
1556
|
+
} catch {
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const raw = this.getAttribute("colors") || "";
|
|
1561
|
+
this.#colors = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1562
|
+
}
|
|
1563
|
+
#resolveRoles() {
|
|
1564
|
+
const raw = this.getAttribute("roles") || BRAND_ROLES.join(",");
|
|
1565
|
+
const roles = raw.split(",").map((s) => s.trim().toLowerCase()).filter((r) => ALL_ROLES.includes(r));
|
|
1566
|
+
this.#roles = roles.length ? roles : [...BRAND_ROLES];
|
|
1567
|
+
}
|
|
1568
|
+
#mapping() {
|
|
1569
|
+
const map = {};
|
|
1570
|
+
for (let i = 0; i < this.#roles.length; i++) {
|
|
1571
|
+
map[this.#roles[i]] = this.#colors[i] || this.#colors[this.#colors.length - 1] || "#888888";
|
|
1572
|
+
}
|
|
1573
|
+
return map;
|
|
1574
|
+
}
|
|
1575
|
+
// ── Render ─────────────────────────────────────────────────────────
|
|
1576
|
+
#renderShell() {
|
|
1577
|
+
const existing = this.querySelector(":scope > .sp-shell");
|
|
1578
|
+
if (existing) existing.remove();
|
|
1579
|
+
const label = this.getAttribute("label");
|
|
1580
|
+
const showExport = this.hasAttribute("show-export");
|
|
1581
|
+
const shell = document.createElement("div");
|
|
1582
|
+
shell.className = "sp-shell";
|
|
1583
|
+
shell.style.cssText = "display:flex;flex-direction:column;gap:var(--size-m,1rem);margin-block-start:var(--size-s,0.75rem)";
|
|
1584
|
+
if (label) {
|
|
1585
|
+
const p = document.createElement("p");
|
|
1586
|
+
p.style.cssText = "font-weight:600;margin:0";
|
|
1587
|
+
p.textContent = label;
|
|
1588
|
+
shell.append(p);
|
|
1589
|
+
}
|
|
1590
|
+
const previews = document.createElement("section");
|
|
1591
|
+
previews.className = "sp-previews";
|
|
1592
|
+
previews.setAttribute("aria-label", "Role previews");
|
|
1593
|
+
previews.style.cssText = `display:grid;grid-template-columns:repeat(auto-fill,minmax(min(22rem,100%),1fr));gap:var(--size-m,1rem)`;
|
|
1594
|
+
shell.append(previews);
|
|
1595
|
+
this.#previews = previews;
|
|
1596
|
+
if (showExport) {
|
|
1597
|
+
shell.insertAdjacentHTML("beforeend", this.#renderExport());
|
|
1598
|
+
}
|
|
1599
|
+
this.append(shell);
|
|
1600
|
+
}
|
|
1601
|
+
#renderPreviews(_source) {
|
|
1602
|
+
if (!this.#previews) return;
|
|
1603
|
+
const map = this.#mapping();
|
|
1604
|
+
this.#previews.innerHTML = this.#roles.map((role) => this.#renderCard(role, map[role])).join("");
|
|
1605
|
+
}
|
|
1606
|
+
#renderCard(role, hex) {
|
|
1607
|
+
const roleLabel = ROLE_LABELS[role] || role;
|
|
1608
|
+
const isStatus = STATUS_ROLES.includes(role);
|
|
1609
|
+
const samples = isStatus ? this.#renderStatusSamples(role, hex) : this.#renderBrandSamples(role, hex);
|
|
1610
|
+
const cardStyle = `display:flex;flex-direction:column;gap:var(--size-s,0.75rem);padding:var(--size-m,1rem);border:1px solid ${BORDER};border-radius:${RADIUS_M};background:${SURFACE};min-inline-size:0`;
|
|
1611
|
+
return `<article class="sp-card" data-role="${role}" aria-label="${roleLabel} role preview" style="${cardStyle}">
|
|
1612
|
+
<header style="display:flex;align-items:center;justify-content:space-between;gap:var(--size-s,0.75rem)">
|
|
1613
|
+
<div>
|
|
1614
|
+
<code style="font-family:${MONO};font-size:var(--font-size-sm,0.875rem);font-weight:600">${role}</code>
|
|
1615
|
+
<span style="display:block;font-size:var(--font-size-xs,0.75rem);color:${MUTED}">${roleLabel}</span>
|
|
1616
|
+
</div>
|
|
1617
|
+
<div style="display:flex;align-items:center;gap:0.5rem">
|
|
1618
|
+
<span class="sp-swatch" style="display:inline-block;inline-size:1.75rem;block-size:1.75rem;border-radius:${RADIUS_S};border:1px solid oklch(0% 0 0 / 0.15);background:${hex}"></span>
|
|
1619
|
+
<code style="font-family:${MONO};font-size:var(--font-size-xs,0.75rem);color:${MUTED}">${hex}</code>
|
|
1620
|
+
</div>
|
|
1621
|
+
</header>
|
|
1622
|
+
<div class="sp-samples" style="display:flex;flex-wrap:wrap;gap:var(--size-s,0.75rem);align-items:flex-start">${samples}</div>
|
|
1623
|
+
</article>`;
|
|
1624
|
+
}
|
|
1625
|
+
#renderBrandSamples(role, hex) {
|
|
1626
|
+
const textOnRole = this.#resolveTextOnRole(role, hex);
|
|
1627
|
+
const surface = this.#resolveSurface();
|
|
1628
|
+
const subtleBg = this.#resolveSubtle(role, hex);
|
|
1629
|
+
const subtleFg = this.#darken(hex);
|
|
1630
|
+
return [
|
|
1631
|
+
this.#sample({
|
|
1632
|
+
render: `<button type="button" tabindex="-1" style="all:unset;display:inline-block;padding:0.375rem 0.875rem;border-radius:${RADIUS_S};background:${hex};color:${textOnRole};font-family:inherit;font-size:var(--font-size-sm,0.875rem);font-weight:600;cursor:default">Button</button>`,
|
|
1633
|
+
fg: textOnRole,
|
|
1634
|
+
bg: hex,
|
|
1635
|
+
caption: "On role"
|
|
1636
|
+
}),
|
|
1637
|
+
this.#sample({
|
|
1638
|
+
render: `<span style="color:${hex};background:${surface};padding:0.25rem 0.5rem;border-radius:${RADIUS_S};font-size:var(--font-size-sm,0.875rem);font-weight:500;text-decoration:underline">Linked text</span>`,
|
|
1639
|
+
fg: hex,
|
|
1640
|
+
bg: surface || "#ffffff",
|
|
1641
|
+
caption: "On surface"
|
|
1642
|
+
}),
|
|
1643
|
+
this.#sample({
|
|
1644
|
+
render: `<span style="display:inline-block;padding:0.125rem 0.5rem;border-radius:999px;background:${subtleBg};color:${subtleFg};font-size:var(--font-size-xs,0.75rem);font-weight:600;letter-spacing:0.04em;text-transform:uppercase">Badge</span>`,
|
|
1645
|
+
fg: subtleFg,
|
|
1646
|
+
bg: subtleBg,
|
|
1647
|
+
caption: "Subtle"
|
|
1648
|
+
})
|
|
1649
|
+
].join("");
|
|
1650
|
+
}
|
|
1651
|
+
#renderStatusSamples(role, hex) {
|
|
1652
|
+
const surface = this.#resolveSurface();
|
|
1653
|
+
const subtleBg = this.#resolveStatusSubtle(role, hex);
|
|
1654
|
+
const statusText = this.#resolveStatusText(role, hex);
|
|
1655
|
+
const onRole = pickContrastingHex(hex);
|
|
1656
|
+
return [
|
|
1657
|
+
this.#sample({
|
|
1658
|
+
render: `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-radius:${RADIUS_S};background:${subtleBg};color:${statusText};border:1px solid ${hex};font-size:var(--font-size-sm,0.875rem);max-inline-size:100%">
|
|
1659
|
+
<span style="display:inline-block;inline-size:0.5rem;block-size:0.5rem;border-radius:50%;background:${hex};flex-shrink:0"></span>
|
|
1660
|
+
<span><strong>${ROLE_LABELS[role] || role}</strong> \u2014 message text</span>
|
|
1661
|
+
</div>`,
|
|
1662
|
+
fg: statusText,
|
|
1663
|
+
bg: subtleBg,
|
|
1664
|
+
caption: "Alert"
|
|
1665
|
+
}),
|
|
1666
|
+
this.#sample({
|
|
1667
|
+
render: `<span style="display:inline-block;padding:0.125rem 0.5rem;border-radius:999px;background:${hex};color:${onRole};font-size:var(--font-size-xs,0.75rem);font-weight:700;letter-spacing:0.04em;text-transform:uppercase">${role}</span>`,
|
|
1668
|
+
fg: onRole,
|
|
1669
|
+
bg: hex,
|
|
1670
|
+
caption: "Badge"
|
|
1671
|
+
}),
|
|
1672
|
+
this.#sample({
|
|
1673
|
+
render: `<span style="color:${hex};background:${surface};padding:0.25rem 0.5rem;border-radius:${RADIUS_S};font-size:var(--font-size-sm,0.875rem);font-weight:600">Inline status</span>`,
|
|
1674
|
+
fg: hex,
|
|
1675
|
+
bg: surface || "#ffffff",
|
|
1676
|
+
caption: "On surface"
|
|
1677
|
+
})
|
|
1678
|
+
].join("");
|
|
1679
|
+
}
|
|
1680
|
+
#sample({ render, fg, bg, caption }) {
|
|
1681
|
+
const fgHex = resolveToHex(fg, this.#probe || void 0) || "#000000";
|
|
1682
|
+
const bgHex = resolveToHex(bg, this.#probe || void 0) || "#ffffff";
|
|
1683
|
+
const ratio = contrastRatio(fgHex, bgHex);
|
|
1684
|
+
const level = wcagLevel(ratio).toLowerCase();
|
|
1685
|
+
const lc = LEVEL_COLORS[level];
|
|
1686
|
+
const chipStyle = `display:inline-flex;align-items:center;gap:0.25rem;padding:0.125rem 0.375rem;border-radius:${RADIUS_S};font-family:${MONO};font-size:var(--font-size-xs,0.75rem);background:${lc.bg};color:${lc.fg}`;
|
|
1687
|
+
return `<div style="display:flex;flex-direction:column;gap:0.25rem;align-items:flex-start;min-inline-size:0;max-inline-size:100%">
|
|
1688
|
+
<div style="max-inline-size:100%">${render}</div>
|
|
1689
|
+
<div style="display:flex;align-items:center;gap:0.375rem;font-size:var(--font-size-xs,0.75rem);color:${MUTED}">
|
|
1690
|
+
<span style="${chipStyle}" title="${caption}: ${ratio.toFixed(2)}:1">${ratio.toFixed(2)} <small>${level.toUpperCase()}</small></span>
|
|
1691
|
+
<span>${caption}</span>
|
|
1692
|
+
</div>
|
|
1693
|
+
</div>`;
|
|
1694
|
+
}
|
|
1695
|
+
#renderExport() {
|
|
1696
|
+
const toolbar = `display:flex;gap:var(--size-2xs,0.375rem);padding-block-start:var(--size-xs,0.5rem);border-block-start:1px solid ${BORDER}`;
|
|
1697
|
+
const btn = `padding:0.375rem 0.875rem;border:1px solid ${BORDER};border-radius:${RADIUS_S};background:${SURFACE};color:${TEXT};cursor:pointer;font:inherit;font-size:var(--font-size-sm,0.875rem)`;
|
|
1698
|
+
return `<div class="sp-export" role="toolbar" aria-label="Export semantic palette" style="${toolbar}">
|
|
1699
|
+
<button type="button" class="sp-copy-css" style="${btn}">Copy Theme CSS</button>
|
|
1700
|
+
<button type="button" class="sp-copy-json" style="${btn}">Copy JSON</button>
|
|
1701
|
+
</div>`;
|
|
1702
|
+
}
|
|
1703
|
+
// ── Wiring ─────────────────────────────────────────────────────────
|
|
1704
|
+
#wireSource() {
|
|
1705
|
+
const cp = this.querySelector("color-palette");
|
|
1706
|
+
if (cp) {
|
|
1707
|
+
this.listen(cp, "color-palette:change", () => {
|
|
1708
|
+
this.#readColors();
|
|
1709
|
+
this.#renderPreviews("palette");
|
|
1710
|
+
this.#emitChange("palette");
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
const pg = this.querySelector("palette-generator");
|
|
1714
|
+
if (pg) {
|
|
1715
|
+
this.listen(pg, "palette-generator:generate", () => {
|
|
1716
|
+
this.#readColors();
|
|
1717
|
+
this.#renderPreviews("palette");
|
|
1718
|
+
this.#emitChange("palette");
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
#wireExport() {
|
|
1723
|
+
const cssBtn = this.querySelector(".sp-copy-css");
|
|
1724
|
+
const jsonBtn = this.querySelector(".sp-copy-json");
|
|
1725
|
+
if (cssBtn) {
|
|
1726
|
+
this.listen(cssBtn, "click", () => {
|
|
1727
|
+
copyText(this.#buildCSS(), {
|
|
1728
|
+
button: (
|
|
1729
|
+
/** @type {HTMLElement} */
|
|
1730
|
+
cssBtn
|
|
1731
|
+
),
|
|
1732
|
+
announceMessage: "Theme CSS copied"
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
if (jsonBtn) {
|
|
1737
|
+
this.listen(jsonBtn, "click", () => {
|
|
1738
|
+
copyText(JSON.stringify(this.#mapping(), null, 2), {
|
|
1739
|
+
button: (
|
|
1740
|
+
/** @type {HTMLElement} */
|
|
1741
|
+
jsonBtn
|
|
1742
|
+
),
|
|
1743
|
+
announceMessage: "JSON copied"
|
|
1744
|
+
});
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
// ── Token resolution ───────────────────────────────────────────────
|
|
1749
|
+
#cs() {
|
|
1750
|
+
return getComputedStyle(this);
|
|
1751
|
+
}
|
|
1752
|
+
#resolveSurface() {
|
|
1753
|
+
return this.#cs().getPropertyValue("--color-surface").trim() || "#ffffff";
|
|
1754
|
+
}
|
|
1755
|
+
#resolveTextOnRole(role, fallbackHex) {
|
|
1756
|
+
const explicit = this.#cs().getPropertyValue(`--color-text-on-${role}`).trim();
|
|
1757
|
+
return explicit || pickContrastingHex(fallbackHex);
|
|
1758
|
+
}
|
|
1759
|
+
#resolveSubtle(role, fallbackHex) {
|
|
1760
|
+
const explicit = this.#cs().getPropertyValue(`--color-${role}-subtle`).trim();
|
|
1761
|
+
if (explicit) return explicit;
|
|
1762
|
+
const { h } = hexToOklch(fallbackHex);
|
|
1763
|
+
return oklchToHex(0.95, 0.03, h);
|
|
1764
|
+
}
|
|
1765
|
+
#resolveStatusSubtle(role, fallbackHex) {
|
|
1766
|
+
const explicit = this.#cs().getPropertyValue(`--color-${role}-subtle`).trim();
|
|
1767
|
+
if (explicit) return explicit;
|
|
1768
|
+
const { h } = hexToOklch(fallbackHex);
|
|
1769
|
+
return oklchToHex(0.94, 0.05, h);
|
|
1770
|
+
}
|
|
1771
|
+
#resolveStatusText(role, fallbackHex) {
|
|
1772
|
+
const explicit = this.#cs().getPropertyValue(`--color-${role}-text`).trim();
|
|
1773
|
+
if (explicit) return explicit;
|
|
1774
|
+
const { h } = hexToOklch(fallbackHex);
|
|
1775
|
+
return oklchToHex(0.3, 0.14, h);
|
|
1776
|
+
}
|
|
1777
|
+
#darken(hex) {
|
|
1778
|
+
const { l, c, h } = hexToOklch(hex);
|
|
1779
|
+
return oklchToHex(Math.max(0.2, l - 0.25), c, h);
|
|
1780
|
+
}
|
|
1781
|
+
// ── Export + emit ──────────────────────────────────────────────────
|
|
1782
|
+
#buildCSS() {
|
|
1783
|
+
const map = this.#mapping();
|
|
1784
|
+
const brand = [];
|
|
1785
|
+
const status = [];
|
|
1786
|
+
for (const role of this.#roles) {
|
|
1787
|
+
const hex = map[role];
|
|
1788
|
+
if (!hex) continue;
|
|
1789
|
+
const { l, c, h } = hexToOklch(hex);
|
|
1790
|
+
if (BRAND_ROLES.includes(role)) {
|
|
1791
|
+
brand.push(
|
|
1792
|
+
` --hue-${role}: ${h.toFixed(1)};`,
|
|
1793
|
+
` --lightness-${role}: ${(l * 100).toFixed(1)}%;`,
|
|
1794
|
+
` --chroma-${role}: ${c.toFixed(3)};`
|
|
1795
|
+
);
|
|
1796
|
+
} else {
|
|
1797
|
+
status.push(` --color-${role}: ${formatOklch({ l, c, h })};`);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const parts = [];
|
|
1801
|
+
if (brand.length) parts.push(" /* Brand seeds */", ...brand);
|
|
1802
|
+
if (status.length) {
|
|
1803
|
+
if (brand.length) parts.push("");
|
|
1804
|
+
parts.push(" /* Status colors */", ...status);
|
|
1805
|
+
}
|
|
1806
|
+
return `:root {
|
|
1807
|
+
${parts.join("\n")}
|
|
1808
|
+
}
|
|
1809
|
+
`;
|
|
1810
|
+
}
|
|
1811
|
+
#emitChange(source) {
|
|
1812
|
+
this.dispatchEvent(new CustomEvent("semantic-palette:change", {
|
|
1813
|
+
bubbles: true,
|
|
1814
|
+
detail: { mapping: this.#mapping(), tokens: this.#buildCSS(), source }
|
|
1815
|
+
}));
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
registerComponent("semantic-palette", SemanticPalette);
|
|
1819
|
+
|
|
1820
|
+
// src/web-components/motion-specimen/logic.js
|
|
1821
|
+
var DEFAULT_EASINGS = "1,2,3,4,5,in-1,in-2,in-3,out-1,out-2,out-3,out-4,out-5,elastic-1,elastic-2,squish-1,squish-2";
|
|
1822
|
+
var DEFAULT_DURATIONS = "instant,fast,normal,slow,slower";
|
|
1823
|
+
var KEYFRAMES_ID = "ms-keyframes";
|
|
1824
|
+
var KEYFRAMES_CSS = `
|
|
1825
|
+
@keyframes ms-slide { from { inset-inline-start: 0 } to { inset-inline-start: calc(100% - 1rem) } }
|
|
1826
|
+
@keyframes ms-fill { 0% { transform: scaleX(0) } 60% { transform: scaleX(1) } 100% { transform: scaleX(1); opacity: 0.3 } }
|
|
1827
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1828
|
+
motion-specimen .ms-dot { animation: none !important; inset-inline-start: calc(50% - 0.5rem) !important }
|
|
1829
|
+
motion-specimen .ms-bar-fill { animation: none !important; transform: scaleX(0.6) !important; opacity: 0.6 !important }
|
|
1830
|
+
}
|
|
1831
|
+
`;
|
|
1832
|
+
function ensureKeyframes() {
|
|
1833
|
+
if (typeof document === "undefined" || document.getElementById(KEYFRAMES_ID)) return;
|
|
1834
|
+
const style = document.createElement("style");
|
|
1835
|
+
style.id = KEYFRAMES_ID;
|
|
1836
|
+
style.textContent = KEYFRAMES_CSS;
|
|
1837
|
+
document.head.append(style);
|
|
1838
|
+
}
|
|
1839
|
+
var MONO2 = "var(--font-mono, monospace)";
|
|
1840
|
+
var MUTED2 = "var(--color-text-muted, #666)";
|
|
1841
|
+
var INTERACTIVE = "var(--color-interactive, oklch(55% 0.2 260))";
|
|
1842
|
+
var SUNKEN = "var(--color-surface-sunken, #f1f1f1)";
|
|
1843
|
+
var MotionSpecimen = class extends VBElement {
|
|
1844
|
+
static observedAttributes = ["type", "tokens", "prefix", "duration", "show-values", "label"];
|
|
1845
|
+
setup() {
|
|
1846
|
+
ensureKeyframes();
|
|
1847
|
+
this.#render();
|
|
1848
|
+
}
|
|
1849
|
+
attributeChangedCallback() {
|
|
1850
|
+
if (this.isConnected && this.hasAttribute("data-upgraded")) this.#render();
|
|
1851
|
+
}
|
|
1852
|
+
#render() {
|
|
1853
|
+
const type = this.getAttribute("type") || "easing";
|
|
1854
|
+
const previewDuration = this.getAttribute("duration") || "1.2s";
|
|
1855
|
+
const showValues = this.getAttribute("show-values") !== "false";
|
|
1856
|
+
const label = this.getAttribute("label") || "";
|
|
1857
|
+
let html = "";
|
|
1858
|
+
if (label) html += `<p style="font-weight:600;margin:0 0 var(--size-s,0.75rem)">${label}</p>`;
|
|
1859
|
+
if (type === "easing" || type === "both") {
|
|
1860
|
+
html += this.#renderEasingSection(previewDuration, showValues);
|
|
1861
|
+
}
|
|
1862
|
+
if (type === "duration" || type === "both") {
|
|
1863
|
+
if (type === "both") html += `<div aria-hidden="true" style="block-size:var(--size-m,1rem)"></div>`;
|
|
1864
|
+
html += this.#renderDurationSection(showValues);
|
|
1865
|
+
}
|
|
1866
|
+
this.innerHTML = html;
|
|
1867
|
+
if (showValues) {
|
|
1868
|
+
requestAnimationFrame(() => this.#fillValues());
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
#rowStyle() {
|
|
1872
|
+
return `display:grid;grid-template-columns:5rem 1fr auto;align-items:center;gap:var(--size-s,0.75rem);min-block-size:1.75rem`;
|
|
1873
|
+
}
|
|
1874
|
+
#nameStyle() {
|
|
1875
|
+
return `font-family:${MONO2};font-size:var(--font-size-sm,0.875rem);color:${MUTED2};text-align:end`;
|
|
1876
|
+
}
|
|
1877
|
+
#valueStyle() {
|
|
1878
|
+
return `font-family:${MONO2};font-size:var(--font-size-xs,0.75rem);color:${MUTED2};font-variant-numeric:tabular-nums;white-space:nowrap`;
|
|
1879
|
+
}
|
|
1880
|
+
#listStyle() {
|
|
1881
|
+
return `list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--size-2xs,0.375rem)`;
|
|
1882
|
+
}
|
|
1883
|
+
#renderEasingSection(previewDuration, showValues) {
|
|
1884
|
+
const names = (this.getAttribute("tokens") || DEFAULT_EASINGS).split(",").map((s) => s.trim());
|
|
1885
|
+
const prefix = this.getAttribute("prefix") || "--ease-";
|
|
1886
|
+
const trackStyle = `position:relative;display:block;block-size:0.5rem;background:${SUNKEN};border-radius:999px`;
|
|
1887
|
+
const dotStyle = `position:absolute;inset-block-start:50%;inset-inline-start:0;inline-size:1rem;block-size:1rem;background:${INTERACTIVE};border-radius:50%;transform:translateY(-50%)`;
|
|
1888
|
+
const rows = names.map((name) => {
|
|
1889
|
+
const varName = `${prefix}${name}`;
|
|
1890
|
+
return `<li class="ms-row ms-row-ease" role="listitem" style="${this.#rowStyle()}">
|
|
1891
|
+
<span style="${this.#nameStyle()}">${name}</span>
|
|
1892
|
+
<span style="${trackStyle}" aria-hidden="true">
|
|
1893
|
+
<span class="ms-dot" style="${dotStyle};animation: ms-slide ${previewDuration} var(${varName}) infinite alternate"></span>
|
|
1894
|
+
</span>
|
|
1895
|
+
${showValues ? `<span class="ms-value" data-var="${varName}" style="${this.#valueStyle()}"></span>` : ""}
|
|
1896
|
+
</li>`;
|
|
1897
|
+
}).join("");
|
|
1898
|
+
return `<ul class="ms-list" role="list" aria-label="Easing scale" style="${this.#listStyle()}">${rows}</ul>`;
|
|
1899
|
+
}
|
|
1900
|
+
#renderDurationSection(showValues) {
|
|
1901
|
+
const names = (this.getAttribute("tokens") || DEFAULT_DURATIONS).split(",").map((s) => s.trim());
|
|
1902
|
+
const prefix = this.getAttribute("prefix") || "--duration-";
|
|
1903
|
+
const barStyle = `position:relative;display:block;block-size:0.5rem;background:${SUNKEN};border-radius:999px;overflow:hidden`;
|
|
1904
|
+
const fillStyle = `position:absolute;inset-block:0;inset-inline-start:0;inline-size:100%;background:${INTERACTIVE};transform-origin:left center;animation-name:ms-fill;animation-iteration-count:infinite;animation-timing-function:linear`;
|
|
1905
|
+
const rows = names.map((name) => {
|
|
1906
|
+
const varName = `${prefix}${name}`;
|
|
1907
|
+
return `<li class="ms-row ms-row-dur" role="listitem" style="${this.#rowStyle()}">
|
|
1908
|
+
<span style="${this.#nameStyle()}">${name}</span>
|
|
1909
|
+
<span style="${barStyle}" aria-hidden="true">
|
|
1910
|
+
<span class="ms-bar-fill" style="${fillStyle};animation-duration: var(${varName})"></span>
|
|
1911
|
+
</span>
|
|
1912
|
+
${showValues ? `<span class="ms-value" data-var="${varName}" style="${this.#valueStyle()}"></span>` : ""}
|
|
1913
|
+
</li>`;
|
|
1914
|
+
}).join("");
|
|
1915
|
+
return `<ul class="ms-list" role="list" aria-label="Duration scale" style="${this.#listStyle()}">${rows}</ul>`;
|
|
1916
|
+
}
|
|
1917
|
+
#fillValues() {
|
|
1918
|
+
this.querySelectorAll(".ms-value").forEach((el2) => {
|
|
1919
|
+
const v = el2.getAttribute("data-var");
|
|
1920
|
+
if (!v) return;
|
|
1921
|
+
const computed = getComputedStyle(this).getPropertyValue(v).trim();
|
|
1922
|
+
el2.textContent = computed || "\u2014";
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
registerComponent("motion-specimen", MotionSpecimen);
|
|
1927
|
+
|
|
1928
|
+
// src/web-components/theme-export/dtcg-serialize.js
|
|
1929
|
+
var VB_NS = "com.vanilla-breeze";
|
|
1930
|
+
var SPEC = "2025.10";
|
|
1931
|
+
function serializeDTCG(entries, options = {}) {
|
|
1932
|
+
const root = {};
|
|
1933
|
+
const unmapped = [];
|
|
1934
|
+
let seedDerivation = false;
|
|
1935
|
+
for (const [name, rawValue] of entries) {
|
|
1936
|
+
const value = String(rawValue).trim();
|
|
1937
|
+
const target = mapPrefix(name);
|
|
1938
|
+
if (!target) {
|
|
1939
|
+
unmapped.push(name);
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
if (target.path[0] === "color" && target.path[1] === "seeds") {
|
|
1943
|
+
seedDerivation = true;
|
|
1944
|
+
}
|
|
1945
|
+
const token = buildToken(name, value, target);
|
|
1946
|
+
insertAt(root, target.path, target.name, token);
|
|
1947
|
+
}
|
|
1948
|
+
const meta = { spec: SPEC };
|
|
1949
|
+
if (options.vbVersion) meta.vbVersion = options.vbVersion;
|
|
1950
|
+
if (seedDerivation) meta.seedDerivation = true;
|
|
1951
|
+
if (unmapped.length) meta.unmapped = unmapped;
|
|
1952
|
+
root.$extensions = { [VB_NS]: meta };
|
|
1953
|
+
return root;
|
|
1954
|
+
}
|
|
1955
|
+
function mapPrefix(name) {
|
|
1956
|
+
const prefixes = [
|
|
1957
|
+
// color seeds (must precede --color-)
|
|
1958
|
+
{ p: "--hue-", path: ["color", "seeds"], type: "number", keep: true },
|
|
1959
|
+
{ p: "--lightness-", path: ["color", "seeds"], type: "number", keep: true },
|
|
1960
|
+
{ p: "--chroma-", path: ["color", "seeds"], type: "number", keep: true },
|
|
1961
|
+
// color
|
|
1962
|
+
{ p: "--color-", path: ["color"], type: "color" },
|
|
1963
|
+
// typography
|
|
1964
|
+
{ p: "--font-size-", path: ["typography", "size"], type: "dimension" },
|
|
1965
|
+
{ p: "--font-weight-", path: ["typography", "weight"], type: "fontWeight" },
|
|
1966
|
+
{ p: "--font-", path: ["typography", "family"], type: "fontFamily" },
|
|
1967
|
+
{ p: "--line-height-", path: ["typography", "lineHeight"], type: "number" },
|
|
1968
|
+
{ p: "--letter-spacing-", path: ["typography", "letterSpacing"], type: "dimension" },
|
|
1969
|
+
// spacing
|
|
1970
|
+
{ p: "--size-", path: ["spacing"], type: "dimension" },
|
|
1971
|
+
// border
|
|
1972
|
+
{ p: "--radius-", path: ["border", "radius"], type: "dimension" },
|
|
1973
|
+
{ p: "--border-width-", path: ["border", "width"], type: "dimension" },
|
|
1974
|
+
// motion
|
|
1975
|
+
{ p: "--duration-", path: ["motion", "duration"], type: "duration" },
|
|
1976
|
+
{ p: "--ease-", path: ["motion", "easing"], type: "cubicBezier" },
|
|
1977
|
+
// effect
|
|
1978
|
+
{ p: "--shadow-", path: ["effect", "shadow"], type: "shadow" }
|
|
1979
|
+
];
|
|
1980
|
+
for (const { p, path, type, keep } of prefixes) {
|
|
1981
|
+
if (name.startsWith(p)) {
|
|
1982
|
+
const tail = keep ? name.slice(2) : name.slice(p.length);
|
|
1983
|
+
return { path, type, name: tail };
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
function buildToken(_name, value, target) {
|
|
1989
|
+
switch (target.type) {
|
|
1990
|
+
case "color":
|
|
1991
|
+
return buildColorToken(value);
|
|
1992
|
+
case "fontFamily":
|
|
1993
|
+
return { $type: "fontFamily", $value: parseFontFamily(value) };
|
|
1994
|
+
case "fontWeight":
|
|
1995
|
+
return { $type: "fontWeight", $value: parseNumber(value) };
|
|
1996
|
+
case "number":
|
|
1997
|
+
return buildNumberToken(value);
|
|
1998
|
+
case "dimension":
|
|
1999
|
+
return buildDimensionToken(value);
|
|
2000
|
+
case "duration":
|
|
2001
|
+
return { $type: "duration", $value: parseDuration(value) };
|
|
2002
|
+
case "cubicBezier":
|
|
2003
|
+
return buildCubicBezierToken(value);
|
|
2004
|
+
case "shadow":
|
|
2005
|
+
return buildShadowToken(value);
|
|
2006
|
+
default:
|
|
2007
|
+
return { $value: value };
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
function buildColorToken(value) {
|
|
2011
|
+
const ld = parseLightDark(value);
|
|
2012
|
+
if (ld) {
|
|
2013
|
+
return {
|
|
2014
|
+
$root: { $type: "color", $value: parseColorValue(ld.light) ?? opaque(ld.light) },
|
|
2015
|
+
light: { $type: "color", $value: parseColorValue(ld.light) ?? opaque(ld.light) },
|
|
2016
|
+
dark: { $type: "color", $value: parseColorValue(ld.dark) ?? opaque(ld.dark) },
|
|
2017
|
+
$extensions: { [VB_NS]: { lightDark: value } }
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
if (/\bfrom\b/.test(value) && /^oklch\(/i.test(value)) {
|
|
2021
|
+
return {
|
|
2022
|
+
$type: "color",
|
|
2023
|
+
$value: { colorSpace: "srgb", components: [0, 0, 0] },
|
|
2024
|
+
$extensions: { [VB_NS]: { expression: value } }
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
const parsed = parseColorValue(value);
|
|
2028
|
+
if (parsed) return { $type: "color", $value: parsed };
|
|
2029
|
+
return { $type: "color", $value: { colorSpace: "srgb", components: [0, 0, 0] }, $extensions: { [VB_NS]: { literal: value } } };
|
|
2030
|
+
}
|
|
2031
|
+
function opaque(value) {
|
|
2032
|
+
return { colorSpace: "srgb", components: [0, 0, 0], $vbLiteral: value };
|
|
2033
|
+
}
|
|
2034
|
+
function parseColorValue(raw) {
|
|
2035
|
+
const value = String(raw).trim();
|
|
2036
|
+
let m = value.match(/^oklch\(\s*([^)]+)\)\s*$/i);
|
|
2037
|
+
if (m) {
|
|
2038
|
+
const [comps, alpha] = splitAlpha(m[1]);
|
|
2039
|
+
const parts = comps.trim().split(/\s+/);
|
|
2040
|
+
if (parts.length < 3) return null;
|
|
2041
|
+
const L = parsePercentOrNumber(parts[0]);
|
|
2042
|
+
const C = parseNumber(parts[1]);
|
|
2043
|
+
const H = parseNumber(parts[2]);
|
|
2044
|
+
if ([L, C, H].some(Number.isNaN)) return null;
|
|
2045
|
+
const out = {
|
|
2046
|
+
colorSpace: "oklch",
|
|
2047
|
+
// OKLCH lightness is 0–1; VB writes 55%.
|
|
2048
|
+
components: [normalizeLightness(parts[0], L), C, H]
|
|
2049
|
+
};
|
|
2050
|
+
if (alpha != null) out.alpha = alpha;
|
|
2051
|
+
return out;
|
|
2052
|
+
}
|
|
2053
|
+
m = value.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
|
|
2054
|
+
if (m) {
|
|
2055
|
+
const hex = m[1];
|
|
2056
|
+
let r, g, b, a;
|
|
2057
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
2058
|
+
r = parseInt(hex[0] + hex[0], 16) / 255;
|
|
2059
|
+
g = parseInt(hex[1] + hex[1], 16) / 255;
|
|
2060
|
+
b = parseInt(hex[2] + hex[2], 16) / 255;
|
|
2061
|
+
if (hex.length === 4) a = parseInt(hex[3] + hex[3], 16) / 255;
|
|
2062
|
+
} else {
|
|
2063
|
+
r = parseInt(hex.slice(0, 2), 16) / 255;
|
|
2064
|
+
g = parseInt(hex.slice(2, 4), 16) / 255;
|
|
2065
|
+
b = parseInt(hex.slice(4, 6), 16) / 255;
|
|
2066
|
+
if (hex.length === 8) a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
2067
|
+
}
|
|
2068
|
+
const out = {
|
|
2069
|
+
colorSpace: "srgb",
|
|
2070
|
+
components: [r, g, b],
|
|
2071
|
+
hex: "#" + (hex.length <= 4 ? hex.slice(0, 3).split("").map((c) => c + c).join("") : hex.slice(0, 6)).toLowerCase()
|
|
2072
|
+
};
|
|
2073
|
+
if (a != null) out.alpha = a;
|
|
2074
|
+
return out;
|
|
2075
|
+
}
|
|
2076
|
+
m = value.match(/^rgba?\(\s*([^)]+)\)\s*$/i);
|
|
2077
|
+
if (m) {
|
|
2078
|
+
const parts = m[1].split(/\s*,\s*|\s+/).filter(Boolean);
|
|
2079
|
+
if (parts.length < 3) return null;
|
|
2080
|
+
const r = parseRGBChannel(parts[0]);
|
|
2081
|
+
const g = parseRGBChannel(parts[1]);
|
|
2082
|
+
const b = parseRGBChannel(parts[2]);
|
|
2083
|
+
const out = { colorSpace: "srgb", components: [r, g, b] };
|
|
2084
|
+
if (parts[3] != null) out.alpha = parsePercentOrNumber(parts[3]);
|
|
2085
|
+
return out;
|
|
2086
|
+
}
|
|
2087
|
+
return null;
|
|
2088
|
+
}
|
|
2089
|
+
function normalizeLightness(raw, parsed) {
|
|
2090
|
+
if (typeof raw === "string" && raw.endsWith("%")) return parsed / 100;
|
|
2091
|
+
return parsed;
|
|
2092
|
+
}
|
|
2093
|
+
function splitAlpha(inner) {
|
|
2094
|
+
const idx = inner.indexOf("/");
|
|
2095
|
+
if (idx < 0) return [inner, null];
|
|
2096
|
+
const comps = inner.slice(0, idx);
|
|
2097
|
+
const alpha = parsePercentOrNumber(inner.slice(idx + 1).trim());
|
|
2098
|
+
return [comps, alpha];
|
|
2099
|
+
}
|
|
2100
|
+
function parseRGBChannel(s) {
|
|
2101
|
+
const v = s.trim();
|
|
2102
|
+
if (v.endsWith("%")) return parseFloat(v) / 100;
|
|
2103
|
+
return parseFloat(v) / 255;
|
|
2104
|
+
}
|
|
2105
|
+
function buildNumberToken(value) {
|
|
2106
|
+
const n = parsePercentOrNumber(value);
|
|
2107
|
+
const tok = { $type: "number", $value: n };
|
|
2108
|
+
if (typeof value === "string" && value.trim().endsWith("%")) {
|
|
2109
|
+
tok.$extensions = { [VB_NS]: { unit: "%" } };
|
|
2110
|
+
}
|
|
2111
|
+
return tok;
|
|
2112
|
+
}
|
|
2113
|
+
function buildDimensionToken(value) {
|
|
2114
|
+
const v = String(value).trim();
|
|
2115
|
+
const m = v.match(/^(-?[\d.]+)(px|rem|em|%)?$/);
|
|
2116
|
+
if (!m) return { $type: "dimension", $value: { value: 0, unit: "px" }, $extensions: { [VB_NS]: { literal: v } } };
|
|
2117
|
+
const n = parseFloat(m[1]);
|
|
2118
|
+
const unit = m[2] || "px";
|
|
2119
|
+
const dtcgUnit = unit === "px" || unit === "rem" ? unit : "rem";
|
|
2120
|
+
const tok = { $type: "dimension", $value: { value: n, unit: dtcgUnit } };
|
|
2121
|
+
if (unit !== "px" && unit !== "rem") {
|
|
2122
|
+
tok.$extensions = { [VB_NS]: { unit } };
|
|
2123
|
+
}
|
|
2124
|
+
return tok;
|
|
2125
|
+
}
|
|
2126
|
+
function parseNumber(value) {
|
|
2127
|
+
return parseFloat(String(value).trim());
|
|
2128
|
+
}
|
|
2129
|
+
function parsePercentOrNumber(value) {
|
|
2130
|
+
const v = String(value).trim();
|
|
2131
|
+
if (v.endsWith("%")) return parseFloat(v);
|
|
2132
|
+
return parseFloat(v);
|
|
2133
|
+
}
|
|
2134
|
+
function parseFontFamily(value) {
|
|
2135
|
+
const out = [];
|
|
2136
|
+
let buf = "";
|
|
2137
|
+
let quote = null;
|
|
2138
|
+
for (const ch of String(value)) {
|
|
2139
|
+
if (quote) {
|
|
2140
|
+
if (ch === quote) {
|
|
2141
|
+
quote = null;
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
buf += ch;
|
|
2145
|
+
} else if (ch === '"' || ch === "'") {
|
|
2146
|
+
quote = ch;
|
|
2147
|
+
} else if (ch === ",") {
|
|
2148
|
+
const t2 = buf.trim();
|
|
2149
|
+
if (t2) out.push(t2);
|
|
2150
|
+
buf = "";
|
|
2151
|
+
} else {
|
|
2152
|
+
buf += ch;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
const t = buf.trim();
|
|
2156
|
+
if (t) out.push(t);
|
|
2157
|
+
return out;
|
|
2158
|
+
}
|
|
2159
|
+
function parseDuration(value) {
|
|
2160
|
+
const v = String(value).trim();
|
|
2161
|
+
let m = v.match(/^(-?[\d.]+)(ms|s)$/);
|
|
2162
|
+
if (m) return { value: parseFloat(m[1]), unit: m[2] };
|
|
2163
|
+
return { value: parseFloat(v), unit: "ms" };
|
|
2164
|
+
}
|
|
2165
|
+
function buildCubicBezierToken(value) {
|
|
2166
|
+
const m = String(value).match(/^cubic-bezier\(\s*([^)]+)\)\s*$/i);
|
|
2167
|
+
if (!m) return { $type: "cubicBezier", $value: [0, 0, 1, 1], $extensions: { [VB_NS]: { literal: value } } };
|
|
2168
|
+
const parts = m[1].split(",").map((s) => parseFloat(s.trim()));
|
|
2169
|
+
if (parts.length !== 4 || parts.some(Number.isNaN)) {
|
|
2170
|
+
return { $type: "cubicBezier", $value: [0, 0, 1, 1], $extensions: { [VB_NS]: { literal: value } } };
|
|
2171
|
+
}
|
|
2172
|
+
return { $type: "cubicBezier", $value: parts };
|
|
2173
|
+
}
|
|
2174
|
+
function buildShadowToken(value) {
|
|
2175
|
+
const stops = splitTopLevelCommas(String(value));
|
|
2176
|
+
const parsed = stops.map(parseShadowStop);
|
|
2177
|
+
if (parsed.some((p) => p == null)) {
|
|
2178
|
+
return { $type: "shadow", $value: { offsetX: { value: 0, unit: "px" }, offsetY: { value: 0, unit: "px" }, blur: { value: 0, unit: "px" }, color: { colorSpace: "srgb", components: [0, 0, 0] } }, $extensions: { [VB_NS]: { literal: value } } };
|
|
2179
|
+
}
|
|
2180
|
+
if (parsed.length === 1) return { $type: "shadow", $value: parsed[0] };
|
|
2181
|
+
return { $type: "shadow", $value: parsed };
|
|
2182
|
+
}
|
|
2183
|
+
function parseShadowStop(raw) {
|
|
2184
|
+
const s = raw.trim();
|
|
2185
|
+
const colorMatch = s.match(/(#[0-9a-f]{3,8}|rgba?\([^)]+\)|oklch\([^)]+\))$/i);
|
|
2186
|
+
if (!colorMatch) return null;
|
|
2187
|
+
const color = parseColorValue(colorMatch[0]);
|
|
2188
|
+
if (!color) return null;
|
|
2189
|
+
const dimsRaw = s.slice(0, colorMatch.index).trim();
|
|
2190
|
+
const dims = dimsRaw.split(/\s+/).filter(Boolean);
|
|
2191
|
+
if (dims.length < 3) return null;
|
|
2192
|
+
const dim = (i) => parseDimensionPart(dims[i]);
|
|
2193
|
+
const out = {
|
|
2194
|
+
offsetX: dim(0),
|
|
2195
|
+
offsetY: dim(1),
|
|
2196
|
+
blur: dim(2),
|
|
2197
|
+
color
|
|
2198
|
+
};
|
|
2199
|
+
if (dims[3]) out.spread = dim(3);
|
|
2200
|
+
return out;
|
|
2201
|
+
}
|
|
2202
|
+
function parseDimensionPart(part) {
|
|
2203
|
+
const m = part.match(/^(-?[\d.]+)(px|rem|em)?$/);
|
|
2204
|
+
if (!m) return { value: 0, unit: "px" };
|
|
2205
|
+
const n = parseFloat(m[1]);
|
|
2206
|
+
const unit = m[2] || "px";
|
|
2207
|
+
const dtcgUnit = unit === "px" || unit === "rem" ? unit : "rem";
|
|
2208
|
+
return { value: n, unit: dtcgUnit };
|
|
2209
|
+
}
|
|
2210
|
+
function splitTopLevelCommas(value) {
|
|
2211
|
+
const out = [];
|
|
2212
|
+
let buf = "";
|
|
2213
|
+
let depth = 0;
|
|
2214
|
+
for (const ch of value) {
|
|
2215
|
+
if (ch === "(") depth++;
|
|
2216
|
+
else if (ch === ")") depth--;
|
|
2217
|
+
if (ch === "," && depth === 0) {
|
|
2218
|
+
out.push(buf);
|
|
2219
|
+
buf = "";
|
|
2220
|
+
} else {
|
|
2221
|
+
buf += ch;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
if (buf.trim()) out.push(buf);
|
|
2225
|
+
return out;
|
|
2226
|
+
}
|
|
2227
|
+
function parseLightDark(value) {
|
|
2228
|
+
const m = String(value).match(/^light-dark\(\s*([\s\S]+)\s*\)\s*$/i);
|
|
2229
|
+
if (!m) return null;
|
|
2230
|
+
const parts = splitTopLevelCommas(m[1]);
|
|
2231
|
+
if (parts.length !== 2) return null;
|
|
2232
|
+
return { light: parts[0].trim(), dark: parts[1].trim() };
|
|
2233
|
+
}
|
|
2234
|
+
function insertAt(root, path, name, token) {
|
|
2235
|
+
let node = root;
|
|
2236
|
+
for (const key of path) {
|
|
2237
|
+
if (!node[key] || typeof node[key] !== "object") node[key] = {};
|
|
2238
|
+
node = node[key];
|
|
2239
|
+
}
|
|
2240
|
+
node[name] = token;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// src/web-components/theme-export/logic.js
|
|
2244
|
+
var DEFAULT_INCLUDE = "--color-,--hue-,--lightness-,--chroma-,--font-,--size-,--radius-,--shadow-,--border-width-,--ease-,--duration-,--line-height-,--letter-spacing-";
|
|
2245
|
+
var LIVE_EVENTS = [
|
|
2246
|
+
"type-specimen:change",
|
|
2247
|
+
"spacing-specimen:change",
|
|
2248
|
+
"token-specimen:change",
|
|
2249
|
+
"semantic-palette:change"
|
|
2250
|
+
];
|
|
2251
|
+
var ThemeExport = class extends VBElement {
|
|
2252
|
+
static observedAttributes = ["scope", "selector", "include", "format", "label", "live"];
|
|
2253
|
+
setup() {
|
|
2254
|
+
this.#render();
|
|
2255
|
+
this.#refresh();
|
|
2256
|
+
this.#wireButtons();
|
|
2257
|
+
this.#wireLive();
|
|
2258
|
+
}
|
|
2259
|
+
attributeChangedCallback() {
|
|
2260
|
+
if (this.isConnected && this.hasAttribute("data-upgraded")) {
|
|
2261
|
+
this.#render();
|
|
2262
|
+
this.#refresh();
|
|
2263
|
+
this.#wireButtons();
|
|
2264
|
+
this.#wireLive();
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
#scopeEl() {
|
|
2268
|
+
const sel = this.getAttribute("scope") || ":root";
|
|
2269
|
+
try {
|
|
2270
|
+
return sel === ":root" ? document.documentElement : document.querySelector(sel);
|
|
2271
|
+
} catch {
|
|
2272
|
+
return document.documentElement;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
#prefixes() {
|
|
2276
|
+
const raw = this.getAttribute("include") || DEFAULT_INCLUDE;
|
|
2277
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2278
|
+
}
|
|
2279
|
+
#collect() {
|
|
2280
|
+
const scope = this.#scopeEl();
|
|
2281
|
+
if (!scope) return [];
|
|
2282
|
+
const prefixes = this.#prefixes();
|
|
2283
|
+
const style = (
|
|
2284
|
+
/** @type {HTMLElement} */
|
|
2285
|
+
scope.style
|
|
2286
|
+
);
|
|
2287
|
+
const entries = [];
|
|
2288
|
+
for (let i = 0; i < style.length; i++) {
|
|
2289
|
+
const name = style.item(i);
|
|
2290
|
+
if (!name.startsWith("--")) continue;
|
|
2291
|
+
if (!prefixes.some((p) => name.startsWith(p))) continue;
|
|
2292
|
+
const value = style.getPropertyValue(name).trim();
|
|
2293
|
+
if (value) entries.push([name, value]);
|
|
2294
|
+
}
|
|
2295
|
+
return entries;
|
|
2296
|
+
}
|
|
2297
|
+
#format(entries) {
|
|
2298
|
+
const format = this.getAttribute("format") || "css";
|
|
2299
|
+
if (format === "json") {
|
|
2300
|
+
const obj = Object.fromEntries(entries);
|
|
2301
|
+
return JSON.stringify(obj, null, 2);
|
|
2302
|
+
}
|
|
2303
|
+
if (format === "dtcg") {
|
|
2304
|
+
const vbVersion = this.getAttribute("vb-version") || void 0;
|
|
2305
|
+
return JSON.stringify(serializeDTCG(entries, { vbVersion }), null, 2);
|
|
2306
|
+
}
|
|
2307
|
+
const selector = this.getAttribute("selector") || ":root";
|
|
2308
|
+
if (!entries.length) {
|
|
2309
|
+
return `${selector} {
|
|
2310
|
+
/* No theme overrides detected yet. Edit a specimen to populate. */
|
|
2311
|
+
}
|
|
2312
|
+
`;
|
|
2313
|
+
}
|
|
2314
|
+
const lines = entries.map(([name, value]) => ` ${name}: ${value};`).join("\n");
|
|
2315
|
+
return `${selector} {
|
|
2316
|
+
${lines}
|
|
2317
|
+
}
|
|
2318
|
+
`;
|
|
2319
|
+
}
|
|
2320
|
+
#render() {
|
|
2321
|
+
const label = this.getAttribute("label") || "";
|
|
2322
|
+
const labelHTML = label ? `<p style="font-weight:600;margin:0 0 var(--size-xs,0.5rem)">${label}</p>` : "";
|
|
2323
|
+
const toolbarStyle = `display:flex;gap:var(--size-2xs,0.375rem);margin-block-end:var(--size-xs,0.5rem);align-items:center;flex-wrap:wrap`;
|
|
2324
|
+
const btn = `padding:0.375rem 0.75rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface,#fff);color:var(--color-text,#222);cursor:pointer;font:inherit;font-size:var(--font-size-xs,0.75rem)`;
|
|
2325
|
+
const sel = `padding:0.375rem 0.5rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface,#fff);color:var(--color-text,#222);font:inherit;font-size:var(--font-size-xs,0.75rem)`;
|
|
2326
|
+
const ta = `inline-size:100%;block-size:16rem;padding:var(--size-s,0.75rem);border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface-sunken,#f8f8f8);color:var(--color-text,#222);font-family:var(--font-mono,monospace);font-size:var(--font-size-xs,0.75rem);white-space:pre;overflow:auto;tab-size:2;resize:vertical;box-sizing:border-box`;
|
|
2327
|
+
const current = this.getAttribute("format") || "css";
|
|
2328
|
+
this.innerHTML = `
|
|
2329
|
+
${labelHTML}
|
|
2330
|
+
<div class="te-toolbar" style="${toolbarStyle}">
|
|
2331
|
+
<label style="display:flex;gap:var(--size-2xs,0.375rem);align-items:center;font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666)">
|
|
2332
|
+
Format
|
|
2333
|
+
<select class="te-format" style="${sel}" aria-label="Output format">
|
|
2334
|
+
<option value="css"${current === "css" ? " selected" : ""}>CSS</option>
|
|
2335
|
+
<option value="json"${current === "json" ? " selected" : ""}>JSON</option>
|
|
2336
|
+
<option value="dtcg"${current === "dtcg" ? " selected" : ""}>DTCG</option>
|
|
2337
|
+
</select>
|
|
2338
|
+
</label>
|
|
2339
|
+
<button type="button" class="te-refresh" style="${btn}" aria-label="Refresh">Refresh</button>
|
|
2340
|
+
<button type="button" class="te-copy" style="${btn}">Copy</button>
|
|
2341
|
+
<button type="button" class="te-download" style="${btn}">Download</button>
|
|
2342
|
+
<span class="te-count" style="align-self:center;font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666);margin-inline-start:auto"></span>
|
|
2343
|
+
</div>
|
|
2344
|
+
<textarea class="te-output" readonly aria-label="Theme output" style="${ta}"></textarea>
|
|
2345
|
+
`;
|
|
2346
|
+
}
|
|
2347
|
+
#refresh() {
|
|
2348
|
+
const entries = this.#collect();
|
|
2349
|
+
const output = this.#format(entries);
|
|
2350
|
+
const ta = this.querySelector(".te-output");
|
|
2351
|
+
const count = this.querySelector(".te-count");
|
|
2352
|
+
if (ta) ta.value = output;
|
|
2353
|
+
if (count) count.textContent = `${entries.length} token${entries.length === 1 ? "" : "s"}`;
|
|
2354
|
+
this.dispatchEvent(new CustomEvent("theme-export:change", {
|
|
2355
|
+
bubbles: true,
|
|
2356
|
+
detail: { output, format: this.getAttribute("format") || "css", tokens: Object.fromEntries(entries) }
|
|
2357
|
+
}));
|
|
2358
|
+
}
|
|
2359
|
+
#wireButtons() {
|
|
2360
|
+
const refresh = this.querySelector(".te-refresh");
|
|
2361
|
+
const copy = this.querySelector(".te-copy");
|
|
2362
|
+
const download = this.querySelector(".te-download");
|
|
2363
|
+
const formatSel = (
|
|
2364
|
+
/** @type {HTMLSelectElement|null} */
|
|
2365
|
+
this.querySelector(".te-format")
|
|
2366
|
+
);
|
|
2367
|
+
if (formatSel) {
|
|
2368
|
+
this.listen(formatSel, "change", () => {
|
|
2369
|
+
this.setAttribute("format", formatSel.value);
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
if (refresh) this.listen(refresh, "click", () => this.#refresh());
|
|
2373
|
+
if (copy) {
|
|
2374
|
+
this.listen(copy, "click", () => {
|
|
2375
|
+
const ta = (
|
|
2376
|
+
/** @type {HTMLTextAreaElement} */
|
|
2377
|
+
this.querySelector(".te-output")
|
|
2378
|
+
);
|
|
2379
|
+
if (!ta) return;
|
|
2380
|
+
copyText(ta.value, {
|
|
2381
|
+
button: (
|
|
2382
|
+
/** @type {HTMLElement} */
|
|
2383
|
+
copy
|
|
2384
|
+
),
|
|
2385
|
+
announceMessage: "Theme copied"
|
|
2386
|
+
});
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
if (download) {
|
|
2390
|
+
this.listen(download, "click", () => this.#download());
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
#download() {
|
|
2394
|
+
const ta = (
|
|
2395
|
+
/** @type {HTMLTextAreaElement} */
|
|
2396
|
+
this.querySelector(".te-output")
|
|
2397
|
+
);
|
|
2398
|
+
if (!ta) return;
|
|
2399
|
+
const format = this.getAttribute("format") || "css";
|
|
2400
|
+
let ext = "css";
|
|
2401
|
+
let mime = "text/css";
|
|
2402
|
+
let filename = "theme.css";
|
|
2403
|
+
if (format === "json") {
|
|
2404
|
+
ext = "json";
|
|
2405
|
+
mime = "application/json";
|
|
2406
|
+
filename = "theme.json";
|
|
2407
|
+
} else if (format === "dtcg") {
|
|
2408
|
+
ext = "tokens.json";
|
|
2409
|
+
mime = "application/design-tokens+json";
|
|
2410
|
+
filename = "theme.tokens.json";
|
|
2411
|
+
}
|
|
2412
|
+
const blob = new Blob([ta.value], { type: mime });
|
|
2413
|
+
const url = URL.createObjectURL(blob);
|
|
2414
|
+
const a = document.createElement("a");
|
|
2415
|
+
a.href = url;
|
|
2416
|
+
a.download = filename;
|
|
2417
|
+
a.click();
|
|
2418
|
+
URL.revokeObjectURL(url);
|
|
2419
|
+
}
|
|
2420
|
+
#wireLive() {
|
|
2421
|
+
if (!this.hasAttribute("live")) return;
|
|
2422
|
+
for (const ev of LIVE_EVENTS) {
|
|
2423
|
+
this.listen(document, ev, () => this.#refresh());
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
registerComponent("theme-export", ThemeExport);
|
|
2428
|
+
|
|
2429
|
+
// src/web-components/accessibility-specimen/logic.js
|
|
2430
|
+
function parseColor(str) {
|
|
2431
|
+
const s = String(str || "").trim();
|
|
2432
|
+
if (!s) return null;
|
|
2433
|
+
let m = s.match(/^#([0-9a-f]{3,8})$/i);
|
|
2434
|
+
if (m) {
|
|
2435
|
+
const h = m[1];
|
|
2436
|
+
if (h.length === 3) return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)];
|
|
2437
|
+
if (h.length === 4) return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)];
|
|
2438
|
+
if (h.length === 6) return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
2439
|
+
if (h.length === 8) return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
2440
|
+
}
|
|
2441
|
+
m = s.match(/^rgba?\s*\(\s*([0-9.]+)[\s,]+([0-9.]+)[\s,]+([0-9.]+)/i);
|
|
2442
|
+
if (m) return [Math.round(+m[1]), Math.round(+m[2]), Math.round(+m[3])];
|
|
2443
|
+
if (typeof document !== "undefined") {
|
|
2444
|
+
const probe = document.createElement("span");
|
|
2445
|
+
probe.style.color = s;
|
|
2446
|
+
probe.style.display = "none";
|
|
2447
|
+
document.body.appendChild(probe);
|
|
2448
|
+
const cs = getComputedStyle(probe).color;
|
|
2449
|
+
probe.remove();
|
|
2450
|
+
const rgb = cs.match(/^rgba?\s*\(\s*([0-9.]+)[\s,]+([0-9.]+)[\s,]+([0-9.]+)/i);
|
|
2451
|
+
if (rgb) return [Math.round(+rgb[1]), Math.round(+rgb[2]), Math.round(+rgb[3])];
|
|
2452
|
+
}
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
function lin(c) {
|
|
2456
|
+
const v = c / 255;
|
|
2457
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
2458
|
+
}
|
|
2459
|
+
function luminance([r, g, b]) {
|
|
2460
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
2461
|
+
}
|
|
2462
|
+
function contrastRatio2(fg, bg) {
|
|
2463
|
+
const f = parseColor(fg);
|
|
2464
|
+
const b = parseColor(bg);
|
|
2465
|
+
if (!f || !b) return null;
|
|
2466
|
+
const l1 = luminance(f);
|
|
2467
|
+
const l2 = luminance(b);
|
|
2468
|
+
const lighter = Math.max(l1, l2);
|
|
2469
|
+
const darker = Math.min(l1, l2);
|
|
2470
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
2471
|
+
}
|
|
2472
|
+
function wcagBadges(ratio) {
|
|
2473
|
+
if (ratio == null) return { aaNormal: false, aaLarge: false, aaaNormal: false, aaaLarge: false };
|
|
2474
|
+
return {
|
|
2475
|
+
aaNormal: ratio >= 4.5,
|
|
2476
|
+
aaLarge: ratio >= 3,
|
|
2477
|
+
aaaNormal: ratio >= 7,
|
|
2478
|
+
aaaLarge: ratio >= 4.5
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
var AccessibilitySpecimen = class extends VBElement {
|
|
2482
|
+
static observedAttributes = ["type"];
|
|
2483
|
+
setup() {
|
|
2484
|
+
const type = this.getAttribute("type") || "contrast";
|
|
2485
|
+
if (type === "checklist") this.#renderChecklist();
|
|
2486
|
+
else this.#renderContrast();
|
|
2487
|
+
}
|
|
2488
|
+
attributeChangedCallback(_name, oldVal, newVal) {
|
|
2489
|
+
if (!this.isConnected || oldVal === newVal) return;
|
|
2490
|
+
this.setup();
|
|
2491
|
+
}
|
|
2492
|
+
// ── Contrast mode ─────────────────────────────────────────────────
|
|
2493
|
+
#renderContrast() {
|
|
2494
|
+
const pairs = (
|
|
2495
|
+
/** @type {HTMLElement[]} */
|
|
2496
|
+
[...this.querySelectorAll(":scope > button[data-fg][data-bg]")].map((b) => ({
|
|
2497
|
+
fg: b.dataset.fg,
|
|
2498
|
+
bg: b.dataset.bg,
|
|
2499
|
+
label: b.dataset.label || b.textContent.trim() || `${b.dataset.fg} on ${b.dataset.bg}`
|
|
2500
|
+
}))
|
|
2501
|
+
);
|
|
2502
|
+
const table = document.createElement("table");
|
|
2503
|
+
table.className = "as-contrast-table";
|
|
2504
|
+
const thead = document.createElement("thead");
|
|
2505
|
+
thead.innerHTML = `
|
|
2506
|
+
<tr>
|
|
2507
|
+
<th scope="col">Pair</th>
|
|
2508
|
+
<th scope="col">Sample</th>
|
|
2509
|
+
<th scope="col">Ratio</th>
|
|
2510
|
+
<th scope="col">AA</th>
|
|
2511
|
+
<th scope="col">AA Large</th>
|
|
2512
|
+
<th scope="col">AAA</th>
|
|
2513
|
+
<th scope="col">AAA Large</th>
|
|
2514
|
+
</tr>`;
|
|
2515
|
+
table.appendChild(thead);
|
|
2516
|
+
const tbody = document.createElement("tbody");
|
|
2517
|
+
if (pairs.length === 0) {
|
|
2518
|
+
const tr = document.createElement("tr");
|
|
2519
|
+
const td = document.createElement("td");
|
|
2520
|
+
td.colSpan = 7;
|
|
2521
|
+
td.className = "as-empty";
|
|
2522
|
+
td.textContent = 'No color pairs authored. Add <button data-fg="\u2026" data-bg="\u2026">Label</button> children.';
|
|
2523
|
+
tr.appendChild(td);
|
|
2524
|
+
tbody.appendChild(tr);
|
|
2525
|
+
} else {
|
|
2526
|
+
for (const p of pairs) tbody.appendChild(this.#renderPairRow(p));
|
|
2527
|
+
}
|
|
2528
|
+
table.appendChild(tbody);
|
|
2529
|
+
this.replaceChildren(table);
|
|
2530
|
+
}
|
|
2531
|
+
#renderPairRow({ fg, bg, label }) {
|
|
2532
|
+
const ratio = contrastRatio2(fg, bg);
|
|
2533
|
+
const badges = wcagBadges(ratio);
|
|
2534
|
+
const ratioText = ratio == null ? "?" : `${ratio.toFixed(2)}:1`;
|
|
2535
|
+
const tr = document.createElement("tr");
|
|
2536
|
+
const tdLabel = document.createElement("th");
|
|
2537
|
+
tdLabel.scope = "row";
|
|
2538
|
+
tdLabel.className = "as-pair-label";
|
|
2539
|
+
tdLabel.innerHTML = `<span class="as-swatch" style="background:${bg};border-color:${fg}"></span><span>${escapeText(label)}</span>`;
|
|
2540
|
+
tr.appendChild(tdLabel);
|
|
2541
|
+
const tdSample = document.createElement("td");
|
|
2542
|
+
tdSample.className = "as-pair-sample";
|
|
2543
|
+
tdSample.style.background = bg;
|
|
2544
|
+
tdSample.style.color = fg;
|
|
2545
|
+
tdSample.textContent = "Aa Bb 123";
|
|
2546
|
+
tr.appendChild(tdSample);
|
|
2547
|
+
const tdRatio = document.createElement("td");
|
|
2548
|
+
tdRatio.className = "as-pair-ratio";
|
|
2549
|
+
tdRatio.textContent = ratioText;
|
|
2550
|
+
tr.appendChild(tdRatio);
|
|
2551
|
+
for (const key of ["aaNormal", "aaLarge", "aaaNormal", "aaaLarge"]) {
|
|
2552
|
+
const td = document.createElement("td");
|
|
2553
|
+
const ok = badges[key];
|
|
2554
|
+
td.className = `as-badge ${ok ? "as-pass" : "as-fail"}`;
|
|
2555
|
+
td.textContent = ok ? "\u2713" : "\u2717";
|
|
2556
|
+
td.setAttribute("aria-label", ok ? "Pass" : "Fail");
|
|
2557
|
+
tr.appendChild(td);
|
|
2558
|
+
}
|
|
2559
|
+
return tr;
|
|
2560
|
+
}
|
|
2561
|
+
// ── Checklist mode ────────────────────────────────────────────────
|
|
2562
|
+
#renderChecklist() {
|
|
2563
|
+
const list = this.querySelector(":scope > ul, :scope > ol");
|
|
2564
|
+
if (!list) return;
|
|
2565
|
+
list.classList.add("as-checklist");
|
|
2566
|
+
for (const item of list.querySelectorAll(":scope > li")) {
|
|
2567
|
+
this.#decorateChecklistItem(item);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
#decorateChecklistItem(item) {
|
|
2571
|
+
if (item.dataset.decorated === "") return;
|
|
2572
|
+
item.dataset.decorated = "";
|
|
2573
|
+
const status = item.dataset.status || "na";
|
|
2574
|
+
item.classList.add(`as-status-${status}`);
|
|
2575
|
+
const icon = document.createElement("span");
|
|
2576
|
+
icon.className = "as-checklist-icon";
|
|
2577
|
+
icon.setAttribute("aria-hidden", "true");
|
|
2578
|
+
icon.textContent = { pass: "\u2713", fail: "\u2717", warn: "!", na: "\u2013" }[status] || "\u2013";
|
|
2579
|
+
const label = { pass: "Pass", fail: "Fail", warn: "Warning", na: "Not applicable" }[status] || "Not applicable";
|
|
2580
|
+
const sr = document.createElement("span");
|
|
2581
|
+
sr.className = "as-sr-only";
|
|
2582
|
+
sr.textContent = `${label}: `;
|
|
2583
|
+
item.prepend(icon, sr);
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
function escapeText(s) {
|
|
2587
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2588
|
+
}
|
|
2589
|
+
registerComponent("accessibility-specimen", AccessibilitySpecimen);
|
|
2590
|
+
|
|
2591
|
+
// src/web-components/breakpoint-specimen/logic.js
|
|
2592
|
+
var BreakpointSpecimen = class extends VBElement {
|
|
2593
|
+
static observedAttributes = ["tokens", "prefix", "label", "data-observe"];
|
|
2594
|
+
setup() {
|
|
2595
|
+
this.#render();
|
|
2596
|
+
this.#start();
|
|
2597
|
+
}
|
|
2598
|
+
attributeChangedCallback() {
|
|
2599
|
+
if (this.isConnected) {
|
|
2600
|
+
this.#stop();
|
|
2601
|
+
this.#render();
|
|
2602
|
+
this.#start();
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
teardown() {
|
|
2606
|
+
this.#stop();
|
|
2607
|
+
}
|
|
2608
|
+
#render() {
|
|
2609
|
+
const tokensAttr = this.getAttribute("tokens") || "sm,md,lg,xl";
|
|
2610
|
+
const prefix = this.getAttribute("prefix") || "--bp-";
|
|
2611
|
+
const label = this.getAttribute("label") || "";
|
|
2612
|
+
const names = tokensAttr.split(",").map((t) => t.trim()).filter(Boolean);
|
|
2613
|
+
const cs = getComputedStyle(document.documentElement);
|
|
2614
|
+
const stops = names.map((name) => {
|
|
2615
|
+
const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
|
|
2616
|
+
const px = this.#toPx(raw);
|
|
2617
|
+
return { name, raw, px };
|
|
2618
|
+
}).filter((s) => s.px > 0).sort((a, b) => a.px - b.px);
|
|
2619
|
+
this.#stops = stops;
|
|
2620
|
+
this.#max = stops.length ? stops[stops.length - 1].px * 1.15 : 1;
|
|
2621
|
+
const labelHTML = label ? `<header class="bps-label">${label}</header>` : "";
|
|
2622
|
+
const readoutHTML = `
|
|
2623
|
+
<output class="bps-readout" aria-live="polite">
|
|
2624
|
+
<span class="bps-width">\u2014</span>
|
|
2625
|
+
<span class="bps-active" data-active="">\u2014</span>
|
|
2626
|
+
</output>
|
|
2627
|
+
`;
|
|
2628
|
+
const rulerHTML = `
|
|
2629
|
+
<figure class="bps-ruler" aria-hidden="true">
|
|
2630
|
+
<span class="bps-cursor" data-cursor></span>
|
|
2631
|
+
${stops.map((s) => `
|
|
2632
|
+
<span class="bps-stop" style="--bps-pos:${s.px / this.#max * 100}%" data-bp="${s.name}">
|
|
2633
|
+
<span class="bps-tick"></span>
|
|
2634
|
+
<span class="bps-stop-label">${s.name}<br><small>${Math.round(s.px)}px</small></span>
|
|
2635
|
+
</span>
|
|
2636
|
+
`).join("")}
|
|
2637
|
+
</figure>
|
|
2638
|
+
`;
|
|
2639
|
+
const tableHTML = `
|
|
2640
|
+
<dl class="bps-list">
|
|
2641
|
+
${stops.map((s) => `
|
|
2642
|
+
<div class="bps-row" data-bp="${s.name}">
|
|
2643
|
+
<dt>${s.name}</dt>
|
|
2644
|
+
<dd><code>${s.raw}</code> \u2014 ${Math.round(s.px)}px and up</dd>
|
|
2645
|
+
</div>
|
|
2646
|
+
`).join("")}
|
|
2647
|
+
</dl>
|
|
2648
|
+
`;
|
|
2649
|
+
this.innerHTML = `${labelHTML}${readoutHTML}${rulerHTML}${tableHTML}`;
|
|
2650
|
+
this.#widthEl = /** @type {HTMLElement | null} */
|
|
2651
|
+
this.querySelector(".bps-width");
|
|
2652
|
+
this.#activeEl = /** @type {HTMLElement | null} */
|
|
2653
|
+
this.querySelector(".bps-active");
|
|
2654
|
+
this.#cursorEl = /** @type {HTMLElement | null} */
|
|
2655
|
+
this.querySelector("[data-cursor]");
|
|
2656
|
+
}
|
|
2657
|
+
#start() {
|
|
2658
|
+
const sel = this.getAttribute("data-observe");
|
|
2659
|
+
if (sel) {
|
|
2660
|
+
const target = document.querySelector(sel);
|
|
2661
|
+
if (!target) return;
|
|
2662
|
+
this.#ro = new ResizeObserver(() => this.#update(target.clientWidth));
|
|
2663
|
+
this.#ro.observe(target);
|
|
2664
|
+
this.#update(target.clientWidth);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
const update = () => this.#update(window.innerWidth);
|
|
2668
|
+
update();
|
|
2669
|
+
this.listen(window, "resize", update, { passive: true });
|
|
2670
|
+
}
|
|
2671
|
+
#stop() {
|
|
2672
|
+
if (this.#ro) {
|
|
2673
|
+
this.#ro.disconnect();
|
|
2674
|
+
this.#ro = null;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
#update(width) {
|
|
2678
|
+
if (!this.#widthEl || !this.#activeEl) return;
|
|
2679
|
+
this.#widthEl.textContent = `${Math.round(width)}px`;
|
|
2680
|
+
const active = this.#activeFor(width);
|
|
2681
|
+
this.#activeEl.textContent = active ? active.name : "base";
|
|
2682
|
+
this.#activeEl.dataset.active = active ? active.name : "";
|
|
2683
|
+
if (this.#cursorEl) {
|
|
2684
|
+
const pct = Math.min(100, width / this.#max * 100);
|
|
2685
|
+
this.#cursorEl.style.setProperty("--bps-pos", `${pct}%`);
|
|
2686
|
+
}
|
|
2687
|
+
this.querySelectorAll("[data-bp]").forEach((el2) => {
|
|
2688
|
+
const matches = active && el2.getAttribute("data-bp") === active.name;
|
|
2689
|
+
el2.toggleAttribute("data-current", !!matches);
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
#activeFor(width) {
|
|
2693
|
+
let match = null;
|
|
2694
|
+
for (const stop of this.#stops) {
|
|
2695
|
+
if (width >= stop.px) match = stop;
|
|
2696
|
+
}
|
|
2697
|
+
return match;
|
|
2698
|
+
}
|
|
2699
|
+
#toPx(value) {
|
|
2700
|
+
if (!value) return 0;
|
|
2701
|
+
const n = parseFloat(value);
|
|
2702
|
+
if (Number.isNaN(n)) return 0;
|
|
2703
|
+
if (value.endsWith("rem") || value.endsWith("em")) {
|
|
2704
|
+
const root = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
2705
|
+
return n * root;
|
|
2706
|
+
}
|
|
2707
|
+
return n;
|
|
2708
|
+
}
|
|
2709
|
+
/** @type {Array<{name: string, raw: string, px: number}>} */
|
|
2710
|
+
#stops = [];
|
|
2711
|
+
#max = 1;
|
|
2712
|
+
/** @type {ResizeObserver | null} */
|
|
2713
|
+
#ro = null;
|
|
2714
|
+
/** @type {HTMLElement | null} */
|
|
2715
|
+
#widthEl = null;
|
|
2716
|
+
/** @type {HTMLElement | null} */
|
|
2717
|
+
#activeEl = null;
|
|
2718
|
+
/** @type {HTMLElement | null} */
|
|
2719
|
+
#cursorEl = null;
|
|
2720
|
+
};
|
|
2721
|
+
registerComponent("breakpoint-specimen", BreakpointSpecimen);
|
|
2722
|
+
|
|
2723
|
+
// src/web-components/layout-specimen/logic.js
|
|
2724
|
+
var PRIMITIVES = [
|
|
2725
|
+
{
|
|
2726
|
+
name: "layout-stack",
|
|
2727
|
+
desc: "Vertical stack with consistent gap between children.",
|
|
2728
|
+
snippet: `<layout-stack data-layout-gap="m">
|
|
2729
|
+
<p>One</p>
|
|
2730
|
+
<p>Two</p>
|
|
2731
|
+
<p>Three</p>
|
|
2732
|
+
</layout-stack>`,
|
|
2733
|
+
render: () => el("layout-stack", { "data-layout-gap": "s" }, [box("One"), box("Two"), box("Three")])
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
name: "layout-cluster",
|
|
2737
|
+
desc: "Horizontal cluster that wraps; for tag rows, button groups.",
|
|
2738
|
+
snippet: `<layout-cluster data-layout-gap="s">
|
|
2739
|
+
<button>One</button> <button>Two</button> <button>Three</button>
|
|
2740
|
+
</layout-cluster>`,
|
|
2741
|
+
render: () => el("layout-cluster", { "data-layout-gap": "s" }, [chip("One"), chip("Two"), chip("Three"), chip("Four")])
|
|
2742
|
+
},
|
|
2743
|
+
{
|
|
2744
|
+
name: "layout-grid",
|
|
2745
|
+
desc: "Auto-fit responsive grid; columns flow based on min-item-size.",
|
|
2746
|
+
snippet: `<layout-grid data-layout-min="12rem">
|
|
2747
|
+
<div>1</div> <div>2</div> <div>3</div> <div>4</div>
|
|
2748
|
+
</layout-grid>`,
|
|
2749
|
+
render: () => el("layout-grid", { "data-layout-min": "6rem", "data-layout-gap": "s" }, [box("1"), box("2"), box("3"), box("4"), box("5"), box("6")])
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
name: "layout-center",
|
|
2753
|
+
desc: "Center a block horizontally with optional max-width.",
|
|
2754
|
+
snippet: `<layout-center data-layout-max="readable">
|
|
2755
|
+
<p>Centered prose.</p>
|
|
2756
|
+
</layout-center>`,
|
|
2757
|
+
render: () => el("layout-center", { "data-layout-max": "narrow", style: "border: 1px dashed var(--color-border); padding: var(--size-2xs)" }, [box("Centered")])
|
|
2758
|
+
},
|
|
2759
|
+
{
|
|
2760
|
+
name: "layout-cover",
|
|
2761
|
+
desc: "Vertical cover layout: optional header + main area + optional footer, with main filling available space.",
|
|
2762
|
+
snippet: `<layout-cover style="block-size: 12rem">
|
|
2763
|
+
<header>Top</header>
|
|
2764
|
+
<main>Center</main>
|
|
2765
|
+
<footer>Bottom</footer>
|
|
2766
|
+
</layout-cover>`,
|
|
2767
|
+
render: () => el("layout-cover", { style: "block-size: 9rem; border: 1px dashed var(--color-border); padding: var(--size-2xs)" }, [
|
|
2768
|
+
el("header", {}, [box("Top")]),
|
|
2769
|
+
el("main", {}, [box("Center", "main")]),
|
|
2770
|
+
el("footer", {}, [box("Bottom")])
|
|
2771
|
+
])
|
|
2772
|
+
},
|
|
2773
|
+
{
|
|
2774
|
+
name: "layout-imposter",
|
|
2775
|
+
desc: "Absolute centering \u2014 child sits in the dead center of the parent.",
|
|
2776
|
+
snippet: `<div style="position:relative; block-size:8rem">
|
|
2777
|
+
<layout-imposter><span>Centered</span></layout-imposter>
|
|
2778
|
+
</div>`,
|
|
2779
|
+
render: () => el("div", { style: "position:relative; block-size: 6rem; border: 1px dashed var(--color-border)" }, [
|
|
2780
|
+
el("layout-imposter", {}, [box("Imposter")])
|
|
2781
|
+
])
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
name: "layout-columns",
|
|
2785
|
+
desc: "Multi-column text layout (CSS columns); good for newspapery prose.",
|
|
2786
|
+
snippet: `<layout-columns data-layout-columns="3">
|
|
2787
|
+
<p>Long prose flows across columns\u2026</p>
|
|
2788
|
+
</layout-columns>`,
|
|
2789
|
+
render: () => el("layout-columns", { "data-layout-columns": "3" }, [
|
|
2790
|
+
el("p", { style: "margin: 0; font-size: 0.85em" }, ["One two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen."])
|
|
2791
|
+
])
|
|
2792
|
+
},
|
|
2793
|
+
{
|
|
2794
|
+
name: "layout-card",
|
|
2795
|
+
desc: "Stack of header + body + actions with card chrome (border + padding).",
|
|
2796
|
+
snippet: `<layout-card>
|
|
2797
|
+
<h3>Title</h3>
|
|
2798
|
+
<p>Body text.</p>
|
|
2799
|
+
<div data-layout-card-actions><button>OK</button></div>
|
|
2800
|
+
</layout-card>`,
|
|
2801
|
+
render: () => el("layout-card", {}, [
|
|
2802
|
+
el("h3", { style: "margin: 0; font-size: 0.9em" }, ["Card title"]),
|
|
2803
|
+
el("p", { style: "margin: 0; font-size: 0.85em" }, ["Card body content."])
|
|
2804
|
+
])
|
|
2805
|
+
},
|
|
2806
|
+
{
|
|
2807
|
+
name: "layout-canvas",
|
|
2808
|
+
desc: "Full-bleed canvas section that breaks the centered max-width.",
|
|
2809
|
+
snippet: `<layout-canvas>
|
|
2810
|
+
<p>Edge-to-edge content.</p>
|
|
2811
|
+
</layout-canvas>`,
|
|
2812
|
+
render: () => el("layout-canvas", { style: "background: var(--color-surface-sunken, oklch(94% 0 0)); padding: var(--size-2xs)" }, [box("Full-bleed")])
|
|
2813
|
+
},
|
|
2814
|
+
{
|
|
2815
|
+
name: "layout-badge",
|
|
2816
|
+
desc: "Tiny inline badge / pill for inline labels.",
|
|
2817
|
+
snippet: `<layout-badge>NEW</layout-badge>`,
|
|
2818
|
+
render: () => el("layout-cluster", { "data-layout-gap": "xs" }, [
|
|
2819
|
+
el("layout-badge", {}, ["NEW"]),
|
|
2820
|
+
el("layout-badge", {}, ["BETA"]),
|
|
2821
|
+
el("layout-badge", {}, ["v2.0"])
|
|
2822
|
+
])
|
|
2823
|
+
},
|
|
2824
|
+
{
|
|
2825
|
+
name: "layout-reel",
|
|
2826
|
+
desc: "Horizontal scrollable reel with snap points.",
|
|
2827
|
+
snippet: `<layout-reel>
|
|
2828
|
+
<article>1</article> <article>2</article> <article>3</article>
|
|
2829
|
+
</layout-reel>`,
|
|
2830
|
+
render: () => el("layout-reel", {}, [box("1", "reel"), box("2", "reel"), box("3", "reel"), box("4", "reel"), box("5", "reel"), box("6", "reel")])
|
|
2831
|
+
},
|
|
2832
|
+
{
|
|
2833
|
+
name: "layout-sidebar",
|
|
2834
|
+
desc: "Sidebar + main split that collapses to a stack on narrow viewports.",
|
|
2835
|
+
snippet: `<layout-sidebar>
|
|
2836
|
+
<aside>Side</aside>
|
|
2837
|
+
<main>Main</main>
|
|
2838
|
+
</layout-sidebar>`,
|
|
2839
|
+
render: () => el("layout-sidebar", {}, [
|
|
2840
|
+
el("aside", { style: "background: var(--color-surface-sunken, oklch(94% 0 0)); padding: var(--size-2xs)" }, ["Side"]),
|
|
2841
|
+
el("main", {}, [box("Main")])
|
|
2842
|
+
])
|
|
2843
|
+
},
|
|
2844
|
+
{
|
|
2845
|
+
name: "layout-switcher",
|
|
2846
|
+
desc: "Container that switches from a row to a stack at a configurable threshold.",
|
|
2847
|
+
snippet: `<layout-switcher data-layout-threshold="30rem">
|
|
2848
|
+
<div>One</div> <div>Two</div>
|
|
2849
|
+
</layout-switcher>`,
|
|
2850
|
+
render: () => el("layout-switcher", { "data-layout-threshold": "20rem" }, [box("One"), box("Two")])
|
|
2851
|
+
},
|
|
2852
|
+
{
|
|
2853
|
+
name: "layout-text",
|
|
2854
|
+
desc: "Constrains line length to a readable measure (~60ch).",
|
|
2855
|
+
snippet: `<layout-text>
|
|
2856
|
+
<p>Constrained-measure prose for comfortable reading.</p>
|
|
2857
|
+
</layout-text>`,
|
|
2858
|
+
render: () => el("layout-text", {}, [
|
|
2859
|
+
el("p", { style: "margin: 0; font-size: 0.85em" }, ["This paragraph is constrained to a comfortable reading measure (~60ch) regardless of the parent width."])
|
|
2860
|
+
])
|
|
2861
|
+
}
|
|
2862
|
+
];
|
|
2863
|
+
function el(tag, attrs = {}, children = []) {
|
|
2864
|
+
const node = document.createElement(tag);
|
|
2865
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
2866
|
+
if (v == null) continue;
|
|
2867
|
+
if (k === "style") node.setAttribute("style", v);
|
|
2868
|
+
else node.setAttribute(k, v === true ? "" : String(v));
|
|
2869
|
+
}
|
|
2870
|
+
for (const c of children) {
|
|
2871
|
+
if (typeof c === "string") node.appendChild(document.createTextNode(c));
|
|
2872
|
+
else if (c) node.appendChild(c);
|
|
2873
|
+
}
|
|
2874
|
+
return node;
|
|
2875
|
+
}
|
|
2876
|
+
function box(label, variant) {
|
|
2877
|
+
const b = el("div", { class: `ls-box${variant ? ` ls-box-${variant}` : ""}` }, [label]);
|
|
2878
|
+
return b;
|
|
2879
|
+
}
|
|
2880
|
+
function chip(label) {
|
|
2881
|
+
return el("span", { class: "ls-chip" }, [label]);
|
|
2882
|
+
}
|
|
2883
|
+
var LayoutSpecimen = class extends VBElement {
|
|
2884
|
+
setup() {
|
|
2885
|
+
const only = this.getAttribute("data-only");
|
|
2886
|
+
const allowed = only ? new Set(only.split(",").map((s) => s.trim())) : null;
|
|
2887
|
+
const list = PRIMITIVES.filter((p) => !allowed || allowed.has(p.name) || allowed.has(p.name.replace(/^layout-/, "")));
|
|
2888
|
+
this.replaceChildren();
|
|
2889
|
+
for (const prim of list) {
|
|
2890
|
+
this.appendChild(this.#renderEntry(prim));
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
#renderEntry(prim) {
|
|
2894
|
+
const section = el("section", { class: "ls-entry" });
|
|
2895
|
+
const head = el("header", { class: "ls-entry-head" }, [
|
|
2896
|
+
el("h3", { class: "ls-entry-name" }, [prim.name]),
|
|
2897
|
+
el("p", { class: "ls-entry-desc" }, [prim.desc])
|
|
2898
|
+
]);
|
|
2899
|
+
const live = el("div", { class: "ls-live" });
|
|
2900
|
+
live.appendChild(prim.render());
|
|
2901
|
+
const code = el("pre", { class: "ls-snippet" }, [el("code", {}, [prim.snippet])]);
|
|
2902
|
+
section.append(head, live, code);
|
|
2903
|
+
return section;
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
registerComponent("layout-specimen", LayoutSpecimen);
|
|
2907
|
+
|
|
2908
|
+
// src/web-components/font-pairer/_font-data.js
|
|
2909
|
+
var FONTS = [
|
|
2910
|
+
// Serif
|
|
2911
|
+
{ name: "Playfair Display", category: "serif", weights: "400;600;700;900", style: "elegant" },
|
|
2912
|
+
{ name: "Merriweather", category: "serif", weights: "300;400;700;900", style: "readable" },
|
|
2913
|
+
{ name: "Lora", category: "serif", weights: "400;500;600;700", style: "elegant" },
|
|
2914
|
+
{ name: "Source Serif 4", category: "serif", weights: "300;400;600;700", style: "neutral" },
|
|
2915
|
+
{ name: "Libre Baskerville", category: "serif", weights: "400;700", style: "classic" },
|
|
2916
|
+
{ name: "Crimson Text", category: "serif", weights: "400;600;700", style: "classic" },
|
|
2917
|
+
{ name: "DM Serif Display", category: "serif", weights: "400", style: "bold" },
|
|
2918
|
+
{ name: "Cormorant Garamond", category: "serif", weights: "300;400;500;600;700", style: "elegant" },
|
|
2919
|
+
// Sans-Serif
|
|
2920
|
+
{ name: "Inter", category: "sans-serif", weights: "300;400;500;600;700", style: "neutral" },
|
|
2921
|
+
{ name: "Open Sans", category: "sans-serif", weights: "300;400;600;700", style: "friendly" },
|
|
2922
|
+
{ name: "Roboto", category: "sans-serif", weights: "300;400;500;700", style: "neutral" },
|
|
2923
|
+
{ name: "Lato", category: "sans-serif", weights: "300;400;700;900", style: "warm" },
|
|
2924
|
+
{ name: "Nunito", category: "sans-serif", weights: "300;400;600;700", style: "friendly" },
|
|
2925
|
+
{ name: "Work Sans", category: "sans-serif", weights: "300;400;500;600;700", style: "geometric" },
|
|
2926
|
+
{ name: "DM Sans", category: "sans-serif", weights: "400;500;700", style: "geometric" },
|
|
2927
|
+
{ name: "Plus Jakarta Sans", category: "sans-serif", weights: "300;400;500;600;700", style: "modern" },
|
|
2928
|
+
{ name: "Space Grotesk", category: "sans-serif", weights: "300;400;500;600;700", style: "technical" },
|
|
2929
|
+
{ name: "Manrope", category: "sans-serif", weights: "300;400;500;600;700", style: "geometric" },
|
|
2930
|
+
{ name: "IBM Plex Sans", category: "sans-serif", weights: "300;400;500;600", style: "technical" },
|
|
2931
|
+
{ name: "Outfit", category: "sans-serif", weights: "300;400;500;600;700", style: "modern" },
|
|
2932
|
+
// Display
|
|
2933
|
+
{ name: "Bebas Neue", category: "display", weights: "400", style: "bold" },
|
|
2934
|
+
{ name: "Abril Fatface", category: "display", weights: "400", style: "elegant" },
|
|
2935
|
+
{ name: "Oswald", category: "display", weights: "300;400;500;600;700", style: "condensed" },
|
|
2936
|
+
{ name: "Sora", category: "display", weights: "300;400;500;600;700", style: "modern" },
|
|
2937
|
+
{ name: "Fraunces", category: "display", weights: "300;400;500;600;700;900", style: "elegant" },
|
|
2938
|
+
// Monospace
|
|
2939
|
+
{ name: "JetBrains Mono", category: "monospace", weights: "400;500;600;700", style: "technical" },
|
|
2940
|
+
{ name: "Fira Code", category: "monospace", weights: "300;400;500;600;700", style: "technical" },
|
|
2941
|
+
{ name: "IBM Plex Mono", category: "monospace", weights: "400;500;600", style: "technical" },
|
|
2942
|
+
{ name: "Source Code Pro", category: "monospace", weights: "300;400;500;600;700", style: "neutral" }
|
|
2943
|
+
];
|
|
2944
|
+
var SUGGESTED_PAIRINGS = [
|
|
2945
|
+
["Playfair Display", "Inter"],
|
|
2946
|
+
["DM Serif Display", "DM Sans"],
|
|
2947
|
+
["Fraunces", "Plus Jakarta Sans"],
|
|
2948
|
+
["Lora", "Open Sans"],
|
|
2949
|
+
["Merriweather", "Roboto"],
|
|
2950
|
+
["Oswald", "Lato"],
|
|
2951
|
+
["Cormorant Garamond", "Work Sans"],
|
|
2952
|
+
["Sora", "Inter"],
|
|
2953
|
+
["Abril Fatface", "Nunito"],
|
|
2954
|
+
["Source Serif 4", "Source Code Pro"],
|
|
2955
|
+
["Space Grotesk", "IBM Plex Sans"],
|
|
2956
|
+
["Outfit", "Manrope"]
|
|
2957
|
+
];
|
|
2958
|
+
function googleFontsUrl(family, weights = "400;700") {
|
|
2959
|
+
const encoded = family.replace(/\s+/g, "+");
|
|
2960
|
+
return `https://fonts.googleapis.com/css2?family=${encoded}:wght@${weights}&display=swap`;
|
|
2961
|
+
}
|
|
2962
|
+
async function loadFont(family, weights = "400;700") {
|
|
2963
|
+
const id = `gf-${family.replace(/\s+/g, "-").toLowerCase()}`;
|
|
2964
|
+
if (document.getElementById(id)) {
|
|
2965
|
+
await document.fonts.ready;
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
const link = document.createElement("link");
|
|
2969
|
+
link.id = id;
|
|
2970
|
+
link.rel = "stylesheet";
|
|
2971
|
+
link.href = googleFontsUrl(family, weights);
|
|
2972
|
+
document.head.appendChild(link);
|
|
2973
|
+
await document.fonts.ready;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// src/web-components/font-pairer/logic.js
|
|
2977
|
+
var E = (font, tag, role, extra = "") => `${extra} contenteditable="plaintext-only" spellcheck="false" class="fp-editable" data-role="${role}"`;
|
|
2978
|
+
var PREVIEWS = {
|
|
2979
|
+
combined: {
|
|
2980
|
+
label: "Combined",
|
|
2981
|
+
render: (f, t) => `
|
|
2982
|
+
<p style="font-family:${f.display};font-size:var(--font-size-3xl,2rem);font-weight:800;margin:0;line-height:1.15"
|
|
2983
|
+
${E(f.display, "p", "heading")}>${t.heading}</p>
|
|
2984
|
+
<h3 style="font-family:${f.heading};font-size:var(--font-size-lg,1.125rem);font-weight:600;margin:0"
|
|
2985
|
+
${E(f.heading, "h3", "subheading")}>${t.subheading}</h3>
|
|
2986
|
+
<p style="font-family:${f.body};font-size:var(--font-size-md,1rem);line-height:1.65;margin:0;color:var(--color-text-muted,#666)"
|
|
2987
|
+
${E(f.body, "p", "body")}>${t.body}</p>
|
|
2988
|
+
<pre style="font-family:${f.code};font-size:var(--font-size-sm,0.875rem);line-height:1.5;margin:0;padding:var(--size-s,0.75rem);background:var(--color-surface-sunken,#eee);border-radius:var(--radius-s,0.25rem);overflow-x:auto"><code ${E(f.code, "code", "code")}>${t.code}</code></pre>`
|
|
2989
|
+
},
|
|
2990
|
+
article: {
|
|
2991
|
+
label: "Article",
|
|
2992
|
+
render: (f, t) => `
|
|
2993
|
+
<small style="font-family:${f.body};font-size:var(--font-size-xs,0.75rem);text-transform:uppercase;letter-spacing:0.08em;color:var(--color-interactive,oklch(55% .2 260));font-weight:600"
|
|
2994
|
+
${E(f.body, "small", "eyebrow")}>${t.eyebrow}</small>
|
|
2995
|
+
<h2 style="font-family:${f.heading};font-size:var(--font-size-2xl,1.75rem);font-weight:700;margin:0;line-height:1.2"
|
|
2996
|
+
${E(f.heading, "h2", "heading")}>${t.heading}</h2>
|
|
2997
|
+
<p style="font-family:${f.body};font-size:var(--font-size-sm,0.875rem);color:var(--color-text-muted,#666);margin:0"
|
|
2998
|
+
${E(f.body, "p", "byline")}>${t.byline}</p>
|
|
2999
|
+
<p style="font-family:${f.body};line-height:1.7;margin:0" ${E(f.body, "p", "body")}>${t.body}</p>
|
|
3000
|
+
<h3 style="font-family:${f.heading};font-size:var(--font-size-lg,1.125rem);font-weight:600;margin:0"
|
|
3001
|
+
${E(f.heading, "h3", "subheading")}>${t.subheading}</h3>
|
|
3002
|
+
<p style="font-family:${f.body};line-height:1.7;margin:0" ${E(f.body, "p", "body2")}>${t.body2}</p>
|
|
3003
|
+
<blockquote style="font-family:${f.display};font-style:italic;font-size:var(--font-size-lg,1.125rem);border-inline-start:3px solid var(--color-interactive,oklch(55% .2 260));padding-inline-start:var(--size-m,1rem);margin:0;color:var(--color-text-muted,#666)"
|
|
3004
|
+
${E(f.display, "blockquote", "quote")}>${t.quote}</blockquote>
|
|
3005
|
+
<pre style="font-family:${f.code};font-size:var(--font-size-sm,0.875rem);line-height:1.5;margin:0;padding:var(--size-s,0.75rem);background:var(--color-surface-sunken,#eee);border-radius:var(--radius-s,0.25rem);overflow-x:auto"><code ${E(f.code, "code", "code")}>${t.code}</code></pre>`
|
|
3006
|
+
},
|
|
3007
|
+
"long-article": {
|
|
3008
|
+
label: "Long Article",
|
|
3009
|
+
render: (f, t) => `
|
|
3010
|
+
<small style="font-family:${f.body};font-size:var(--font-size-xs,0.75rem);text-transform:uppercase;letter-spacing:0.08em;color:var(--color-interactive,oklch(55% .2 260));font-weight:600">${t.eyebrow}</small>
|
|
3011
|
+
<h1 style="font-family:${f.display};font-size:var(--font-size-3xl,2rem);font-weight:800;margin:0;line-height:1.15">${t.heading}</h1>
|
|
3012
|
+
<p style="font-family:${f.body};font-size:var(--font-size-sm,0.875rem);color:var(--color-text-muted,#666);margin:0">${t.byline}</p>
|
|
3013
|
+
<p style="font-family:${f.body};font-size:var(--font-size-lg,1.125rem);line-height:1.6;margin:0;color:var(--color-text-muted,#666)">${t.lead}</p>
|
|
3014
|
+
<h2 style="font-family:${f.heading};font-size:var(--font-size-xl,1.375rem);font-weight:700;margin:0">${t.subheading}</h2>
|
|
3015
|
+
<p style="font-family:${f.body};line-height:1.7;margin:0">${t.body}</p>
|
|
3016
|
+
<p style="font-family:${f.body};line-height:1.7;margin:0">${t.body2}</p>
|
|
3017
|
+
<blockquote style="font-family:${f.display};font-style:italic;font-size:var(--font-size-lg,1.125rem);border-inline-start:3px solid var(--color-interactive,oklch(55% .2 260));padding-inline-start:var(--size-m,1rem);margin:0;color:var(--color-text-muted,#666)">${t.quote}</blockquote>
|
|
3018
|
+
<h2 style="font-family:${f.heading};font-size:var(--font-size-xl,1.375rem);font-weight:700;margin:0">Implementation Details</h2>
|
|
3019
|
+
<p style="font-family:${f.body};line-height:1.7;margin:0">Design tokens form the atomic layer of any visual system. Colors, spacing, typography, and motion are expressed as named variables that cascade through every component. When a token changes, every surface that references it updates automatically \u2014 no find-and-replace, no missed instances.</p>
|
|
3020
|
+
<pre style="font-family:${f.code};font-size:var(--font-size-sm,0.875rem);line-height:1.5;margin:0;padding:var(--size-s,0.75rem);background:var(--color-surface-sunken,#eee);border-radius:var(--radius-s,0.25rem);overflow-x:auto"><code>${t.code}</code></pre>
|
|
3021
|
+
<h3 style="font-family:${f.heading};font-size:var(--font-size-lg,1.125rem);font-weight:600;margin:0">Key Takeaways</h3>
|
|
3022
|
+
<ul style="font-family:${f.body};line-height:1.7;margin:0;padding-inline-start:1.5rem">
|
|
3023
|
+
<li>Typography sets the emotional tone before a single word is read</li>
|
|
3024
|
+
<li>Pair contrast with harmony \u2014 serif headings with sans-serif body</li>
|
|
3025
|
+
<li>Test at real content lengths, not just sample sentences</li>
|
|
3026
|
+
<li>Code fonts need clear distinction between similar glyphs</li>
|
|
3027
|
+
</ul>`
|
|
3028
|
+
},
|
|
3029
|
+
card: {
|
|
3030
|
+
label: "Card UI",
|
|
3031
|
+
render: (f, t) => `
|
|
3032
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--size-m,1rem)">
|
|
3033
|
+
<div style="border:1px solid var(--color-border,#ddd);border-radius:var(--radius-m,0.5rem);padding:var(--size-m,1rem);display:flex;flex-direction:column;gap:var(--size-xs,0.5rem)">
|
|
3034
|
+
<h4 style="font-family:${f.heading};font-weight:600;margin:0" ${E(f.heading, "h4", "cardTitle")}>Premium Plan</h4>
|
|
3035
|
+
<p style="font-family:${f.display};font-size:var(--font-size-2xl,1.75rem);font-weight:700;margin:0">$49<small style="font-size:0.5em;font-weight:400">/mo</small></p>
|
|
3036
|
+
<p style="font-family:${f.body};font-size:var(--font-size-sm,0.875rem);color:var(--color-text-muted,#666);margin:0" ${E(f.body, "p", "cardBody")}>Everything you need to build faster.</p>
|
|
3037
|
+
<code style="font-family:${f.code};font-size:var(--font-size-xs,0.75rem);background:var(--color-surface-sunken,#eee);padding:0.2rem 0.4rem;border-radius:0.25rem" ${E(f.code, "code", "cardCode")}>npm install acme</code>
|
|
3038
|
+
</div>
|
|
3039
|
+
<div style="border:1px solid var(--color-border,#ddd);border-radius:var(--radius-m,0.5rem);padding:var(--size-m,1rem);display:flex;flex-direction:column;gap:var(--size-xs,0.5rem)">
|
|
3040
|
+
<h4 style="font-family:${f.heading};font-weight:600;margin:0">Enterprise</h4>
|
|
3041
|
+
<p style="font-family:${f.display};font-size:var(--font-size-2xl,1.75rem);font-weight:700;margin:0">Custom</p>
|
|
3042
|
+
<p style="font-family:${f.body};font-size:var(--font-size-sm,0.875rem);color:var(--color-text-muted,#666);margin:0">Dedicated support and SLA guarantees.</p>
|
|
3043
|
+
<code style="font-family:${f.code};font-size:var(--font-size-xs,0.75rem);background:var(--color-surface-sunken,#eee);padding:0.2rem 0.4rem;border-radius:0.25rem">contact@acme.dev</code>
|
|
3044
|
+
</div>
|
|
3045
|
+
</div>`
|
|
3046
|
+
},
|
|
3047
|
+
hero: {
|
|
3048
|
+
label: "Hero",
|
|
3049
|
+
render: (f, t) => `
|
|
3050
|
+
<div style="text-align:center;padding:var(--size-xl,2rem) 0">
|
|
3051
|
+
<p style="font-family:${f.body};font-size:var(--font-size-xs,0.75rem);text-transform:uppercase;letter-spacing:0.1em;color:var(--color-interactive,oklch(55% .2 260));font-weight:600;margin:0 0 var(--size-xs,0.5rem)"
|
|
3052
|
+
${E(f.body, "p", "eyebrow")}>${t.eyebrow}</p>
|
|
3053
|
+
<h1 style="font-family:${f.display};font-size:clamp(2rem,5vw,3rem);font-weight:800;margin:0;line-height:1.1"
|
|
3054
|
+
${E(f.display, "h1", "heading")}>${t.heading}</h1>
|
|
3055
|
+
<p style="font-family:${f.body};font-size:var(--font-size-lg,1.125rem);color:var(--color-text-muted,#666);margin:var(--size-s,0.75rem) auto 0;max-width:40ch"
|
|
3056
|
+
${E(f.body, "p", "body")}>${t.body}</p>
|
|
3057
|
+
</div>`
|
|
3058
|
+
}
|
|
3059
|
+
};
|
|
3060
|
+
var FontPairer = class extends VBElement {
|
|
3061
|
+
static observedAttributes = ["heading-font", "body-font", "code-font", "display-font", "sample", "show-export", "show-suggestions", "preview"];
|
|
3062
|
+
#fonts = { heading: "", body: "", code: "", display: "" };
|
|
3063
|
+
#preview = "combined";
|
|
3064
|
+
#sampleText = {
|
|
3065
|
+
heading: "The Art of Typography",
|
|
3066
|
+
subheading: "Building visual harmony through font pairing",
|
|
3067
|
+
body: "Good typography is invisible \u2014 it lets the content speak without distraction. The best font pairings create harmony between heading impact and body readability. Every typeface carries personality: serif fonts convey trust and tradition, while sans-serif fonts feel modern and clean.",
|
|
3068
|
+
body2: "Choosing the right pairing is about contrast and complement. A bold display font for headlines needs a quiet, readable body font to balance it. Monospace fonts ground technical content in precision. The interplay between these roles defines your brand voice.",
|
|
3069
|
+
code: 'const theme = {\n heading: "Playfair Display",\n body: "Inter",\n tokens: { size: 16, scale: 1.25 }\n};',
|
|
3070
|
+
eyebrow: "Design Systems",
|
|
3071
|
+
byline: "By Jamie Chen \xB7 April 14, 2026 \xB7 8 min read",
|
|
3072
|
+
lead: "How native CSS features and design tokens are replacing the complex tooling we thought we needed.",
|
|
3073
|
+
quote: '"Good typography is invisible \u2014 it lets the content breathe."'
|
|
3074
|
+
};
|
|
3075
|
+
setup() {
|
|
3076
|
+
this.#fonts.heading = this.getAttribute("heading-font") || "Playfair Display";
|
|
3077
|
+
this.#fonts.body = this.getAttribute("body-font") || "Inter";
|
|
3078
|
+
this.#fonts.code = this.getAttribute("code-font") || "JetBrains Mono";
|
|
3079
|
+
this.#fonts.display = this.getAttribute("display-font") || this.#fonts.heading;
|
|
3080
|
+
this.#preview = this.getAttribute("preview") || "combined";
|
|
3081
|
+
this.#render();
|
|
3082
|
+
this.#loadAllFonts();
|
|
3083
|
+
}
|
|
3084
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
3085
|
+
if (oldVal === newVal || !this.isConnected) return;
|
|
3086
|
+
if (name === "heading-font") this.#fonts.heading = newVal || "Playfair Display";
|
|
3087
|
+
if (name === "body-font") this.#fonts.body = newVal || "Inter";
|
|
3088
|
+
if (name === "code-font") this.#fonts.code = newVal || "JetBrains Mono";
|
|
3089
|
+
if (name === "display-font") this.#fonts.display = newVal || this.#fonts.heading;
|
|
3090
|
+
if (name === "preview") this.#preview = newVal || "combined";
|
|
3091
|
+
this.#render();
|
|
3092
|
+
this.#loadAllFonts();
|
|
3093
|
+
}
|
|
3094
|
+
async #loadAllFonts() {
|
|
3095
|
+
const loads = Object.values(this.#fonts).filter(Boolean).map((name) => {
|
|
3096
|
+
const f = FONTS.find((x) => x.name === name);
|
|
3097
|
+
return loadFont(name, f?.weights || "400;700");
|
|
3098
|
+
});
|
|
3099
|
+
await Promise.all(loads);
|
|
3100
|
+
this.#updateFonts();
|
|
3101
|
+
}
|
|
3102
|
+
#render() {
|
|
3103
|
+
const showExport = this.hasAttribute("show-export");
|
|
3104
|
+
const showSuggestions = this.hasAttribute("show-suggestions");
|
|
3105
|
+
const gap = "var(--size-m, 1rem)";
|
|
3106
|
+
const smGap = "var(--size-s, 0.75rem)";
|
|
3107
|
+
const xsGap = "var(--size-xs, 0.5rem)";
|
|
3108
|
+
const radius = "var(--radius-m, 0.5rem)";
|
|
3109
|
+
const border = "var(--color-border, #ddd)";
|
|
3110
|
+
const surface = "var(--color-surface, #fff)";
|
|
3111
|
+
const raised = "var(--color-surface-raised, #f5f5f5)";
|
|
3112
|
+
const muted = "var(--color-text-muted, #666)";
|
|
3113
|
+
const smFont = "var(--font-size-sm, 0.875rem)";
|
|
3114
|
+
const xsFont = "var(--font-size-xs, 0.75rem)";
|
|
3115
|
+
const selectStyle = `font:inherit;font-size:${smFont};padding:0.35rem 0.5rem;border:1px solid ${border};border-radius:4px;background:${surface}`;
|
|
3116
|
+
const roles = [
|
|
3117
|
+
{ key: "heading", label: "Heading" },
|
|
3118
|
+
{ key: "body", label: "Body" },
|
|
3119
|
+
{ key: "code", label: "Code" },
|
|
3120
|
+
{ key: "display", label: "Display" }
|
|
3121
|
+
];
|
|
3122
|
+
const selects = roles.map((r) => `
|
|
3123
|
+
<label style="display:flex;flex-direction:column;gap:3px;flex:1;min-width:9rem;font-size:${xsFont};font-weight:600;color:${muted};text-transform:uppercase;letter-spacing:0.04em">
|
|
3124
|
+
${r.label}
|
|
3125
|
+
<select class="fp-select" data-role="${r.key}" style="${selectStyle}">
|
|
3126
|
+
${this.#buildOptions(this.#fonts[r.key])}
|
|
3127
|
+
</select>
|
|
3128
|
+
</label>
|
|
3129
|
+
`).join("");
|
|
3130
|
+
const previewTabs = Object.entries(PREVIEWS).map(
|
|
3131
|
+
([key, { label }]) => `<button type="button" class="fp-tab" data-mode="${key}"
|
|
3132
|
+
style="all:unset;cursor:pointer;font-size:${xsFont};padding:0.3rem 0.75rem;border-radius:999px;${key === this.#preview ? `background:var(--color-interactive,oklch(55% .2 260));color:white` : `border:1px solid ${border}`}">${label}</button>`
|
|
3133
|
+
).join("");
|
|
3134
|
+
const fontStyles = {
|
|
3135
|
+
heading: `'${this.#fonts.heading}', serif`,
|
|
3136
|
+
body: `'${this.#fonts.body}', sans-serif`,
|
|
3137
|
+
code: `'${this.#fonts.code}', monospace`,
|
|
3138
|
+
display: `'${this.#fonts.display}', serif`
|
|
3139
|
+
};
|
|
3140
|
+
const previewHTML = PREVIEWS[this.#preview]?.render(fontStyles, this.#sampleText) || "";
|
|
3141
|
+
const suggestionPills = SUGGESTED_PAIRINGS.map(
|
|
3142
|
+
([h, b]) => `<button type="button" class="fp-suggestion" data-h="${h}" data-b="${b}"
|
|
3143
|
+
style="all:unset;cursor:pointer;font-size:${xsFont};padding:0.3rem 0.5rem;border:1px solid ${border};border-radius:4px;white-space:nowrap;text-align:left;line-height:1.3"
|
|
3144
|
+
title="${h} + ${b}"><strong>${h}</strong><br><span style="color:${muted}">${b}</span></button>`
|
|
3145
|
+
).join("");
|
|
3146
|
+
const cssCode = this.#buildCSS();
|
|
3147
|
+
const codeSection = `<details class="fp-code-details" style="border:1px solid ${border};border-radius:${radius};overflow:hidden"${showExport ? " open" : ""}>
|
|
3148
|
+
<summary style="padding:${xsGap} ${smGap};cursor:pointer;font-size:${xsFont};font-weight:600;color:${muted};text-transform:uppercase;letter-spacing:0.04em;background:${raised}">Code</summary>
|
|
3149
|
+
<div style="padding:${smGap}">
|
|
3150
|
+
<pre style="font-family:var(--font-mono,monospace);font-size:${xsFont};margin:0;white-space:pre-wrap;word-break:break-all" class="fp-css-output">${cssCode}</pre>
|
|
3151
|
+
<div style="display:flex;gap:${xsGap};margin-block-start:${smGap}">
|
|
3152
|
+
<button type="button" class="fp-copy-css"
|
|
3153
|
+
style="all:unset;cursor:pointer;font-size:${xsFont};padding:0.3rem 0.65rem;border:1px solid ${border};border-radius:4px;background:${surface}">Copy CSS</button>
|
|
3154
|
+
<button type="button" class="fp-copy-import"
|
|
3155
|
+
style="all:unset;cursor:pointer;font-size:${xsFont};padding:0.3rem 0.65rem;border:1px solid ${border};border-radius:4px;background:${surface}">Copy @import</button>
|
|
3156
|
+
</div>
|
|
3157
|
+
</div>
|
|
3158
|
+
</details>`;
|
|
3159
|
+
const mainContent = `<div style="display:flex;flex-direction:column;gap:${gap};flex:1;min-width:0">
|
|
3160
|
+
<div style="display:flex;flex-wrap:wrap;gap:${xsGap}">${selects}</div>
|
|
3161
|
+
<div style="display:flex;flex-wrap:wrap;gap:0.375rem;align-items:center">
|
|
3162
|
+
<span style="font-size:${xsFont};font-weight:600;color:${muted};text-transform:uppercase;letter-spacing:0.04em;margin-inline-end:${xsGap}">Examples</span>
|
|
3163
|
+
${previewTabs}
|
|
3164
|
+
</div>
|
|
3165
|
+
<div style="display:flex;flex-direction:column;gap:${xsGap}">
|
|
3166
|
+
<span style="font-size:${xsFont};font-weight:600;color:${muted};text-transform:uppercase;letter-spacing:0.04em">Preview</span>
|
|
3167
|
+
<div class="fp-preview" style="padding:var(--size-l,1.5rem);background:${raised};border-radius:${radius};border:1px solid ${border};display:flex;flex-direction:column;gap:${smGap};max-height:24rem;overflow-y:auto">
|
|
3168
|
+
${previewHTML}
|
|
3169
|
+
</div>
|
|
3170
|
+
</div>
|
|
3171
|
+
${codeSection}
|
|
3172
|
+
</div>`;
|
|
3173
|
+
if (showSuggestions) {
|
|
3174
|
+
this.innerHTML = `<div style="display:flex;gap:${gap};flex-wrap:wrap">
|
|
3175
|
+
${mainContent}
|
|
3176
|
+
<aside style="flex:0 0 11rem;display:flex;flex-direction:column;gap:${xsGap}">
|
|
3177
|
+
<span style="font-size:${xsFont};font-weight:600;color:${muted};text-transform:uppercase;letter-spacing:0.04em">Pairings</span>
|
|
3178
|
+
<div style="display:flex;flex-direction:column;gap:0.25rem">${suggestionPills}</div>
|
|
3179
|
+
</aside>
|
|
3180
|
+
</div>`;
|
|
3181
|
+
} else {
|
|
3182
|
+
this.innerHTML = mainContent;
|
|
3183
|
+
}
|
|
3184
|
+
this.#wire();
|
|
3185
|
+
}
|
|
3186
|
+
#buildOptions(selected) {
|
|
3187
|
+
const categories = ["serif", "sans-serif", "display", "monospace"];
|
|
3188
|
+
return categories.map((cat) => {
|
|
3189
|
+
const fonts = FONTS.filter((f) => f.category === cat);
|
|
3190
|
+
const opts = fonts.map(
|
|
3191
|
+
(f) => `<option value="${f.name}"${f.name === selected ? " selected" : ""}>${f.name}</option>`
|
|
3192
|
+
).join("");
|
|
3193
|
+
return `<optgroup label="${cat}">${opts}</optgroup>`;
|
|
3194
|
+
}).join("");
|
|
3195
|
+
}
|
|
3196
|
+
#buildCSS() {
|
|
3197
|
+
const lines = [];
|
|
3198
|
+
const roles = { heading: "serif", body: "sans-serif", code: "monospace", display: "serif" };
|
|
3199
|
+
for (const [role, fallback] of Object.entries(roles)) {
|
|
3200
|
+
const name = this.#fonts[role];
|
|
3201
|
+
if (!name) continue;
|
|
3202
|
+
const f = FONTS.find((x) => x.name === name);
|
|
3203
|
+
lines.push(`--font-${role}: "${name}", ${f?.category || fallback};`);
|
|
3204
|
+
}
|
|
3205
|
+
return lines.join("\n");
|
|
3206
|
+
}
|
|
3207
|
+
#buildImports() {
|
|
3208
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3209
|
+
const lines = [];
|
|
3210
|
+
for (const name of Object.values(this.#fonts)) {
|
|
3211
|
+
if (!name || seen.has(name)) continue;
|
|
3212
|
+
seen.add(name);
|
|
3213
|
+
const f = FONTS.find((x) => x.name === name);
|
|
3214
|
+
lines.push(`@import url('${googleFontsUrl(name, f?.weights || "400;700")}');`);
|
|
3215
|
+
}
|
|
3216
|
+
return lines.join("\n");
|
|
3217
|
+
}
|
|
3218
|
+
#wire() {
|
|
3219
|
+
this.querySelectorAll(".fp-select").forEach((sel) => {
|
|
3220
|
+
sel.addEventListener("change", (e) => {
|
|
3221
|
+
const target = (
|
|
3222
|
+
/** @type {HTMLSelectElement} */
|
|
3223
|
+
e.target
|
|
3224
|
+
);
|
|
3225
|
+
this.#fonts[target.dataset.role] = target.value;
|
|
3226
|
+
this.#loadAllFonts();
|
|
3227
|
+
this.#renderPreview();
|
|
3228
|
+
this.#updateCode();
|
|
3229
|
+
this.#emit();
|
|
3230
|
+
});
|
|
3231
|
+
});
|
|
3232
|
+
this.querySelectorAll(".fp-tab").forEach((btn) => {
|
|
3233
|
+
btn.addEventListener("click", () => {
|
|
3234
|
+
const el2 = (
|
|
3235
|
+
/** @type {HTMLElement} */
|
|
3236
|
+
btn
|
|
3237
|
+
);
|
|
3238
|
+
this.#preview = el2.dataset.mode || "combined";
|
|
3239
|
+
this.#render();
|
|
3240
|
+
this.#loadAllFonts();
|
|
3241
|
+
});
|
|
3242
|
+
});
|
|
3243
|
+
this.querySelectorAll(".fp-suggestion").forEach((btn) => {
|
|
3244
|
+
btn.addEventListener("click", () => {
|
|
3245
|
+
const el2 = (
|
|
3246
|
+
/** @type {HTMLElement} */
|
|
3247
|
+
btn
|
|
3248
|
+
);
|
|
3249
|
+
this.#fonts.heading = el2.dataset.h || "";
|
|
3250
|
+
this.#fonts.body = el2.dataset.b || "";
|
|
3251
|
+
this.#fonts.display = el2.dataset.h || "";
|
|
3252
|
+
this.#render();
|
|
3253
|
+
this.#loadAllFonts();
|
|
3254
|
+
this.#emit();
|
|
3255
|
+
});
|
|
3256
|
+
});
|
|
3257
|
+
this.querySelectorAll(".fp-editable").forEach((el2) => {
|
|
3258
|
+
const node = (
|
|
3259
|
+
/** @type {HTMLElement} */
|
|
3260
|
+
el2
|
|
3261
|
+
);
|
|
3262
|
+
node.addEventListener("blur", () => {
|
|
3263
|
+
const role = node.dataset.role;
|
|
3264
|
+
if (role && node.textContent.trim()) {
|
|
3265
|
+
this.#sampleText[role] = node.textContent.trim();
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
});
|
|
3269
|
+
this.querySelector(".fp-copy-css")?.addEventListener("click", (e) => {
|
|
3270
|
+
copyText(this.#buildCSS(), {
|
|
3271
|
+
button: (
|
|
3272
|
+
/** @type {HTMLElement} */
|
|
3273
|
+
e.target
|
|
3274
|
+
),
|
|
3275
|
+
announceMessage: "CSS copied"
|
|
3276
|
+
});
|
|
3277
|
+
});
|
|
3278
|
+
this.querySelector(".fp-copy-import")?.addEventListener("click", (e) => {
|
|
3279
|
+
copyText(this.#buildImports(), {
|
|
3280
|
+
button: (
|
|
3281
|
+
/** @type {HTMLElement} */
|
|
3282
|
+
e.target
|
|
3283
|
+
),
|
|
3284
|
+
announceMessage: "@import statement copied"
|
|
3285
|
+
});
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
#renderPreview() {
|
|
3289
|
+
const container = this.querySelector(".fp-preview");
|
|
3290
|
+
if (!container) return;
|
|
3291
|
+
const fontStyles = {
|
|
3292
|
+
heading: `'${this.#fonts.heading}', serif`,
|
|
3293
|
+
body: `'${this.#fonts.body}', sans-serif`,
|
|
3294
|
+
code: `'${this.#fonts.code}', monospace`,
|
|
3295
|
+
display: `'${this.#fonts.display}', serif`
|
|
3296
|
+
};
|
|
3297
|
+
container.innerHTML = PREVIEWS[this.#preview]?.render(fontStyles, this.#sampleText) || "";
|
|
3298
|
+
container.querySelectorAll(".fp-editable").forEach((el2) => {
|
|
3299
|
+
const node = (
|
|
3300
|
+
/** @type {HTMLElement} */
|
|
3301
|
+
el2
|
|
3302
|
+
);
|
|
3303
|
+
node.addEventListener("blur", () => {
|
|
3304
|
+
const role = node.dataset.role;
|
|
3305
|
+
if (role && node.textContent.trim()) {
|
|
3306
|
+
this.#sampleText[role] = node.textContent.trim();
|
|
3307
|
+
}
|
|
3308
|
+
});
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
#updateFonts() {
|
|
3312
|
+
this.#renderPreview();
|
|
3313
|
+
this.#updateCode();
|
|
3314
|
+
}
|
|
3315
|
+
#updateCode() {
|
|
3316
|
+
const output = this.querySelector(".fp-css-output");
|
|
3317
|
+
if (output) output.textContent = this.#buildCSS();
|
|
3318
|
+
}
|
|
3319
|
+
#emit() {
|
|
3320
|
+
this.dispatchEvent(new CustomEvent("font-pairer:change", {
|
|
3321
|
+
bubbles: true,
|
|
3322
|
+
detail: { ...this.#fonts }
|
|
3323
|
+
}));
|
|
3324
|
+
}
|
|
3325
|
+
};
|
|
3326
|
+
registerComponent("font-pairer", FontPairer);
|
|
3327
|
+
|
|
3328
|
+
// src/web-components/gradient-builder/_gradient-utils.js
|
|
3329
|
+
function buildGradientCSS(stops, { type = "linear", angle = 90, interpolation = "oklab" } = {}) {
|
|
3330
|
+
if (stops.length < 2) return "transparent";
|
|
3331
|
+
const sorted = [...stops].sort((a, b) => a.position - b.position);
|
|
3332
|
+
const colorStops = sorted.map((s) => `${s.color} ${s.position}%`).join(", ");
|
|
3333
|
+
const space = interpolation !== "srgb" ? ` in ${interpolation}` : "";
|
|
3334
|
+
if (type === "radial") {
|
|
3335
|
+
return `radial-gradient(circle${space}, ${colorStops})`;
|
|
3336
|
+
}
|
|
3337
|
+
return `linear-gradient(${angle}deg${space}, ${colorStops})`;
|
|
3338
|
+
}
|
|
3339
|
+
function parseColorStops(colorsStr) {
|
|
3340
|
+
if (!colorsStr) return defaultStops();
|
|
3341
|
+
const colors = colorsStr.split(",").map((c) => c.trim()).filter(Boolean);
|
|
3342
|
+
if (colors.length < 2) return defaultStops();
|
|
3343
|
+
return colors.map((color, i) => ({
|
|
3344
|
+
color,
|
|
3345
|
+
position: Math.round(i / (colors.length - 1) * 100)
|
|
3346
|
+
}));
|
|
3347
|
+
}
|
|
3348
|
+
function defaultStops() {
|
|
3349
|
+
return [
|
|
3350
|
+
{ color: "#6366f1", position: 0 },
|
|
3351
|
+
{ color: "#ec4899", position: 100 }
|
|
3352
|
+
];
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
// src/web-components/gradient-builder/logic.js
|
|
3356
|
+
var GradientBuilder = class extends VBElement {
|
|
3357
|
+
static observedAttributes = ["colors", "type", "angle", "interpolation", "show-export", "show-controls"];
|
|
3358
|
+
/** @type {import('./_gradient-utils.js').GradientStop[]} */
|
|
3359
|
+
#stops = [];
|
|
3360
|
+
#type = "linear";
|
|
3361
|
+
#angle = 90;
|
|
3362
|
+
#interpolation = "oklab";
|
|
3363
|
+
setup() {
|
|
3364
|
+
this.#stops = parseColorStops(this.getAttribute("colors") ?? "");
|
|
3365
|
+
this.#type = this.getAttribute("type") || "linear";
|
|
3366
|
+
this.#angle = Number(this.getAttribute("angle")) || 90;
|
|
3367
|
+
this.#interpolation = this.getAttribute("interpolation") || "oklab";
|
|
3368
|
+
this.#render();
|
|
3369
|
+
return true;
|
|
3370
|
+
}
|
|
3371
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
3372
|
+
if (oldVal === newVal || !this.isConnected) return;
|
|
3373
|
+
if (name === "colors") this.#stops = parseColorStops(newVal);
|
|
3374
|
+
if (name === "type") this.#type = newVal || "linear";
|
|
3375
|
+
if (name === "angle") this.#angle = Number(newVal) || 90;
|
|
3376
|
+
if (name === "interpolation") this.#interpolation = newVal || "oklab";
|
|
3377
|
+
this.#render();
|
|
3378
|
+
}
|
|
3379
|
+
get css() {
|
|
3380
|
+
return buildGradientCSS(this.#stops, {
|
|
3381
|
+
type: this.#type,
|
|
3382
|
+
angle: this.#angle,
|
|
3383
|
+
interpolation: this.#interpolation
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
#render() {
|
|
3387
|
+
const showExport = this.hasAttribute("show-export");
|
|
3388
|
+
const showControls = this.getAttribute("show-controls") !== "false";
|
|
3389
|
+
const css = this.css;
|
|
3390
|
+
const gap = "var(--size-m, 1rem)";
|
|
3391
|
+
const smGap = "var(--size-s, 0.75rem)";
|
|
3392
|
+
const xsGap = "var(--size-xs, 0.5rem)";
|
|
3393
|
+
const radius = "var(--radius-m, 0.5rem)";
|
|
3394
|
+
const border = "var(--color-border, #ddd)";
|
|
3395
|
+
const surface = "var(--color-surface, #fff)";
|
|
3396
|
+
const muted = "var(--color-text-muted, #666)";
|
|
3397
|
+
const mono = "var(--font-mono, monospace)";
|
|
3398
|
+
const smFont = "var(--font-size-sm, 0.875rem)";
|
|
3399
|
+
const xsFont = "var(--font-size-xs, 0.75rem)";
|
|
3400
|
+
const preview = `<div class="gb-preview" style="height:4rem;border-radius:${radius};background:${css};border:1px solid ${border}"></div>`;
|
|
3401
|
+
const selectStyle = `font:inherit;font-size:${smFont};padding:0.25rem 0.5rem;border:1px solid ${border};border-radius:4px;background:${surface}`;
|
|
3402
|
+
let controls = "";
|
|
3403
|
+
if (showControls) {
|
|
3404
|
+
controls = `<div style="display:flex;flex-direction:column;gap:${smGap};font-size:${smFont}">
|
|
3405
|
+
<div style="display:flex;flex-wrap:wrap;gap:${gap};align-items:center">
|
|
3406
|
+
<label style="display:flex;align-items:center;gap:${xsGap}">
|
|
3407
|
+
Type
|
|
3408
|
+
<select class="gb-type" style="${selectStyle}">
|
|
3409
|
+
<option value="linear"${this.#type === "linear" ? " selected" : ""}>Linear</option>
|
|
3410
|
+
<option value="radial"${this.#type === "radial" ? " selected" : ""}>Radial</option>
|
|
3411
|
+
</select>
|
|
3412
|
+
</label>
|
|
3413
|
+
<label style="display:flex;align-items:center;gap:${xsGap}${this.#type === "radial" ? ";opacity:0.4;pointer-events:none" : ""}">
|
|
3414
|
+
Angle
|
|
3415
|
+
<input type="range" class="gb-angle" min="0" max="360" value="${this.#angle}" style="width:8rem;accent-color:var(--color-interactive,oklch(55% .2 260))">
|
|
3416
|
+
<span class="gb-angle-value" style="min-width:2.5em;font-family:${mono};font-size:${xsFont}">${this.#angle}\xB0</span>
|
|
3417
|
+
</label>
|
|
3418
|
+
</div>
|
|
3419
|
+
<label style="display:inline-flex;align-items:center;gap:${xsGap}">
|
|
3420
|
+
Color Space
|
|
3421
|
+
<select class="gb-space" style="${selectStyle}">
|
|
3422
|
+
<option value="oklab"${this.#interpolation === "oklab" ? " selected" : ""}>oklab</option>
|
|
3423
|
+
<option value="oklch"${this.#interpolation === "oklch" ? " selected" : ""}>oklch</option>
|
|
3424
|
+
<option value="srgb"${this.#interpolation === "srgb" ? " selected" : ""}>sRGB</option>
|
|
3425
|
+
</select>
|
|
3426
|
+
</label>
|
|
3427
|
+
</div>`;
|
|
3428
|
+
}
|
|
3429
|
+
const sorted = [...this.#stops].map((s, i) => ({ ...s, origIndex: i })).sort((a, b) => a.position - b.position);
|
|
3430
|
+
const stopRows = sorted.map((stop) => {
|
|
3431
|
+
const i = stop.origIndex;
|
|
3432
|
+
return `<div style="display:flex;align-items:center;gap:0.375rem" data-stop="${i}">
|
|
3433
|
+
<input type="color" value="${stop.color}" class="gb-stop-color" data-i="${i}"
|
|
3434
|
+
style="width:1.75rem;height:1.75rem;padding:0;border:1px solid ${border};border-radius:4px;cursor:pointer;flex-shrink:0">
|
|
3435
|
+
<input type="range" min="0" max="100" value="${stop.position}" class="gb-stop-pos-range" data-i="${i}"
|
|
3436
|
+
style="flex:1;min-width:3rem;max-width:8rem;accent-color:${stop.color}">
|
|
3437
|
+
<span style="font-family:${mono};font-size:${xsFont};min-width:2.5em;text-align:right" class="gb-stop-pos-label" data-i="${i}">${stop.position}%</span>
|
|
3438
|
+
${this.#stops.length > 2 ? `<button type="button" class="gb-remove" data-i="${i}"
|
|
3439
|
+
style="all:unset;cursor:pointer;font-size:1rem;color:${muted};padding:0 0.25rem" title="Remove stop">×</button>` : ""}
|
|
3440
|
+
</div>`;
|
|
3441
|
+
}).join("");
|
|
3442
|
+
const addBtn = `<button type="button" class="gb-add"
|
|
3443
|
+
style="all:unset;cursor:pointer;font-size:${smFont};color:var(--color-interactive,oklch(55% .2 260));font-weight:600">+ Add Stop</button>`;
|
|
3444
|
+
this.innerHTML = `
|
|
3445
|
+
<div style="display:flex;flex-direction:column;gap:${gap}">
|
|
3446
|
+
${preview}
|
|
3447
|
+
${controls}
|
|
3448
|
+
<div style="display:flex;flex-direction:column;gap:${xsGap}">
|
|
3449
|
+
<span style="font-size:${xsFont};font-weight:600;color:${muted};text-transform:uppercase;letter-spacing:0.05em">Color Stops</span>
|
|
3450
|
+
${stopRows}
|
|
3451
|
+
${addBtn}
|
|
3452
|
+
</div>
|
|
3453
|
+
<div style="display:flex;flex-direction:column;gap:${xsGap}">
|
|
3454
|
+
<span style="font-size:${xsFont};font-weight:600;color:${muted};text-transform:uppercase;letter-spacing:0.05em">Code</span>
|
|
3455
|
+
<div style="display:flex;align-items:start;gap:${xsGap}">
|
|
3456
|
+
<div style="flex:1;font-family:${mono};font-size:${xsFont};padding:${smGap};background:var(--color-surface-raised,#f5f5f5);border-radius:${radius};word-break:break-all;color:var(--color-text,#222)" class="gb-css-output">${css}</div>
|
|
3457
|
+
${showExport ? `<button type="button" class="gb-copy"
|
|
3458
|
+
style="all:unset;cursor:pointer;font-size:${xsFont};padding:0.35rem 0.75rem;border:1px solid ${border};border-radius:4px;background:${surface};white-space:nowrap;flex-shrink:0">Copy CSS</button>` : ""}
|
|
3459
|
+
</div>
|
|
3460
|
+
</div>
|
|
3461
|
+
</div>
|
|
3462
|
+
`;
|
|
3463
|
+
this.#wire();
|
|
3464
|
+
}
|
|
3465
|
+
#wire() {
|
|
3466
|
+
this.querySelector(".gb-type")?.addEventListener("change", (e) => {
|
|
3467
|
+
const t = (
|
|
3468
|
+
/** @type {HTMLSelectElement} */
|
|
3469
|
+
e.target
|
|
3470
|
+
);
|
|
3471
|
+
this.#type = t.value;
|
|
3472
|
+
this.#render();
|
|
3473
|
+
this.#emit();
|
|
3474
|
+
});
|
|
3475
|
+
const angleInput = this.querySelector(".gb-angle");
|
|
3476
|
+
const angleValue = this.querySelector(".gb-angle-value");
|
|
3477
|
+
angleInput?.addEventListener("input", (e) => {
|
|
3478
|
+
const t = (
|
|
3479
|
+
/** @type {HTMLInputElement} */
|
|
3480
|
+
e.target
|
|
3481
|
+
);
|
|
3482
|
+
this.#angle = Number(t.value);
|
|
3483
|
+
if (angleValue) angleValue.textContent = `${this.#angle}\xB0`;
|
|
3484
|
+
const preview = (
|
|
3485
|
+
/** @type {HTMLElement | null} */
|
|
3486
|
+
this.querySelector(".gb-preview")
|
|
3487
|
+
);
|
|
3488
|
+
if (preview) preview.style.background = this.css;
|
|
3489
|
+
this.#emit();
|
|
3490
|
+
});
|
|
3491
|
+
this.querySelector(".gb-space")?.addEventListener("change", (e) => {
|
|
3492
|
+
const t = (
|
|
3493
|
+
/** @type {HTMLSelectElement} */
|
|
3494
|
+
e.target
|
|
3495
|
+
);
|
|
3496
|
+
this.#interpolation = t.value;
|
|
3497
|
+
this.#render();
|
|
3498
|
+
this.#emit();
|
|
3499
|
+
});
|
|
3500
|
+
this.querySelectorAll(".gb-stop-color").forEach((input) => {
|
|
3501
|
+
input.addEventListener("input", (e) => {
|
|
3502
|
+
const t = (
|
|
3503
|
+
/** @type {HTMLInputElement} */
|
|
3504
|
+
e.target
|
|
3505
|
+
);
|
|
3506
|
+
const i = Number(t.dataset.i);
|
|
3507
|
+
this.#stops[i].color = t.value;
|
|
3508
|
+
this.#updatePreview();
|
|
3509
|
+
this.#emit();
|
|
3510
|
+
});
|
|
3511
|
+
});
|
|
3512
|
+
this.querySelectorAll(".gb-stop-pos-range").forEach((input) => {
|
|
3513
|
+
input.addEventListener("input", (e) => {
|
|
3514
|
+
const t = (
|
|
3515
|
+
/** @type {HTMLInputElement} */
|
|
3516
|
+
e.target
|
|
3517
|
+
);
|
|
3518
|
+
const i = Number(t.dataset.i);
|
|
3519
|
+
const val = Math.max(0, Math.min(100, Number(t.value) || 0));
|
|
3520
|
+
this.#stops[i].position = val;
|
|
3521
|
+
const label = this.querySelector(`.gb-stop-pos-label[data-i="${i}"]`);
|
|
3522
|
+
if (label) label.textContent = `${val}%`;
|
|
3523
|
+
this.#updatePreview();
|
|
3524
|
+
this.#emit();
|
|
3525
|
+
});
|
|
3526
|
+
});
|
|
3527
|
+
this.querySelectorAll(".gb-remove").forEach((btn) => {
|
|
3528
|
+
btn.addEventListener("click", (e) => {
|
|
3529
|
+
const t = (
|
|
3530
|
+
/** @type {HTMLElement} */
|
|
3531
|
+
e.target
|
|
3532
|
+
);
|
|
3533
|
+
const i = Number(t.dataset.i);
|
|
3534
|
+
this.#stops.splice(i, 1);
|
|
3535
|
+
this.#render();
|
|
3536
|
+
this.#emit();
|
|
3537
|
+
});
|
|
3538
|
+
});
|
|
3539
|
+
this.querySelector(".gb-add")?.addEventListener("click", () => {
|
|
3540
|
+
const last = this.#stops[this.#stops.length - 1];
|
|
3541
|
+
const prev = this.#stops[this.#stops.length - 2];
|
|
3542
|
+
const pos = Math.round((prev.position + last.position) / 2);
|
|
3543
|
+
this.#stops.splice(this.#stops.length - 1, 0, { color: "#888888", position: pos });
|
|
3544
|
+
this.#render();
|
|
3545
|
+
this.#emit();
|
|
3546
|
+
});
|
|
3547
|
+
this.querySelector(".gb-copy")?.addEventListener("click", (e) => {
|
|
3548
|
+
const btn = (
|
|
3549
|
+
/** @type {HTMLElement | null} */
|
|
3550
|
+
e.target
|
|
3551
|
+
);
|
|
3552
|
+
if (!btn) return;
|
|
3553
|
+
copyText(this.css, { button: btn, announceMessage: "Gradient CSS copied" });
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
#updatePreview() {
|
|
3557
|
+
const css = this.css;
|
|
3558
|
+
const preview = (
|
|
3559
|
+
/** @type {HTMLElement | null} */
|
|
3560
|
+
this.querySelector(".gb-preview")
|
|
3561
|
+
);
|
|
3562
|
+
if (preview) preview.style.background = css;
|
|
3563
|
+
const output = this.querySelector(".gb-css-output");
|
|
3564
|
+
if (output) output.textContent = css;
|
|
3565
|
+
}
|
|
3566
|
+
#emit() {
|
|
3567
|
+
this.dispatchEvent(new CustomEvent("gradient-builder:change", {
|
|
3568
|
+
bubbles: true,
|
|
3569
|
+
detail: {
|
|
3570
|
+
css: this.css,
|
|
3571
|
+
stops: [...this.#stops],
|
|
3572
|
+
type: this.#type,
|
|
3573
|
+
angle: this.#angle,
|
|
3574
|
+
interpolation: this.#interpolation
|
|
3575
|
+
}
|
|
3576
|
+
}));
|
|
3577
|
+
}
|
|
3578
|
+
};
|
|
3579
|
+
registerComponent("gradient-builder", GradientBuilder);
|
|
3580
|
+
|
|
3581
|
+
// src/web-components/theme-import/dtcg-deserialize.js
|
|
3582
|
+
var VB_NS2 = "com.vanilla-breeze";
|
|
3583
|
+
function deserializeDTCG(doc) {
|
|
3584
|
+
const tokens = [];
|
|
3585
|
+
const ignored = [];
|
|
3586
|
+
const tree = doc && typeof doc === "object" ? doc : {};
|
|
3587
|
+
walk(tree, [], void 0, tree, tokens, ignored);
|
|
3588
|
+
return {
|
|
3589
|
+
tokens,
|
|
3590
|
+
ignored,
|
|
3591
|
+
stats: { applied: tokens.length, ignored: ignored.length }
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
function walk(node, path, inheritedType, root, tokens, ignored) {
|
|
3595
|
+
if (!node || typeof node !== "object") return;
|
|
3596
|
+
if ("$value" in node) {
|
|
3597
|
+
handleToken(node, path, inheritedType ?? node.$type, root, tokens, ignored);
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
if (isVariantContainer(node)) {
|
|
3601
|
+
handleVariantToken(node, path, inheritedType ?? node.$type ?? "color", root, tokens, ignored);
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
const groupType = node.$type ?? inheritedType;
|
|
3605
|
+
for (const [key, child] of Object.entries(node)) {
|
|
3606
|
+
if (key.startsWith("$")) continue;
|
|
3607
|
+
walk(child, [...path, key], groupType, root, tokens, ignored);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
function isVariantContainer(node) {
|
|
3611
|
+
const variants = ["$root", "light", "dark"];
|
|
3612
|
+
return variants.some((k) => node[k] && typeof node[k] === "object" && "$value" in node[k]);
|
|
3613
|
+
}
|
|
3614
|
+
function pathToVar(path) {
|
|
3615
|
+
const [head, second, ...rest] = path;
|
|
3616
|
+
switch (head) {
|
|
3617
|
+
case "color": {
|
|
3618
|
+
if (second === "seeds") {
|
|
3619
|
+
return rest.length ? `--${rest.join("-")}` : null;
|
|
3620
|
+
}
|
|
3621
|
+
const tail = [second, ...rest].filter(Boolean).join("-");
|
|
3622
|
+
return tail ? `--color-${tail}` : null;
|
|
3623
|
+
}
|
|
3624
|
+
case "typography": {
|
|
3625
|
+
const leaf = rest.join("-") || second;
|
|
3626
|
+
const child = rest.length ? rest.join("-") : "";
|
|
3627
|
+
switch (second) {
|
|
3628
|
+
case "family":
|
|
3629
|
+
return child ? `--font-${child}` : null;
|
|
3630
|
+
case "size":
|
|
3631
|
+
return child ? `--font-size-${child}` : null;
|
|
3632
|
+
case "weight":
|
|
3633
|
+
return child ? `--font-weight-${child}` : null;
|
|
3634
|
+
case "lineHeight":
|
|
3635
|
+
return child ? `--line-height-${child}` : null;
|
|
3636
|
+
case "letterSpacing":
|
|
3637
|
+
return child ? `--letter-spacing-${child}` : null;
|
|
3638
|
+
default:
|
|
3639
|
+
return null;
|
|
3640
|
+
}
|
|
3641
|
+
const _ = leaf;
|
|
3642
|
+
}
|
|
3643
|
+
case "spacing":
|
|
3644
|
+
return [second, ...rest].filter(Boolean).length ? `--size-${[second, ...rest].filter(Boolean).join("-")}` : null;
|
|
3645
|
+
case "border": {
|
|
3646
|
+
const child = rest.join("-") || "";
|
|
3647
|
+
if (second === "radius") return child ? `--radius-${child}` : null;
|
|
3648
|
+
if (second === "width") return child ? `--border-width-${child}` : null;
|
|
3649
|
+
return null;
|
|
3650
|
+
}
|
|
3651
|
+
case "motion": {
|
|
3652
|
+
const child = rest.join("-") || "";
|
|
3653
|
+
if (second === "duration") return child ? `--duration-${child}` : null;
|
|
3654
|
+
if (second === "easing") return child ? `--ease-${child}` : null;
|
|
3655
|
+
return null;
|
|
3656
|
+
}
|
|
3657
|
+
case "effect": {
|
|
3658
|
+
const child = rest.join("-") || "";
|
|
3659
|
+
if (second === "shadow") return child ? `--shadow-${child}` : null;
|
|
3660
|
+
return null;
|
|
3661
|
+
}
|
|
3662
|
+
default:
|
|
3663
|
+
return null;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
function handleToken(token, path, type, root, tokens, ignored) {
|
|
3667
|
+
if ((type === "typography" || token.$type === "typography") && token.$value && typeof token.$value === "object" && !Array.isArray(token.$value) && path[0] === "typography") {
|
|
3668
|
+
unpackTypography(token, path, tokens);
|
|
3669
|
+
return;
|
|
3670
|
+
}
|
|
3671
|
+
const cssVar = pathToVar(path);
|
|
3672
|
+
if (!cssVar) {
|
|
3673
|
+
ignored.push(path.join("."));
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
const effectiveType = type ?? inferTypeFromPath(path);
|
|
3677
|
+
const ext = token.$extensions && token.$extensions[VB_NS2] || {};
|
|
3678
|
+
if (ext.expression) {
|
|
3679
|
+
tokens.push([cssVar, ext.expression]);
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
if (ext.lightDark) {
|
|
3683
|
+
tokens.push([cssVar, ext.lightDark]);
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
const value = renderValue(token.$value, effectiveType, ext, root);
|
|
3687
|
+
if (value == null) {
|
|
3688
|
+
ignored.push(path.join("."));
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
tokens.push([cssVar, value]);
|
|
3692
|
+
}
|
|
3693
|
+
function inferTypeFromPath(path) {
|
|
3694
|
+
const [head, second] = path;
|
|
3695
|
+
if (head === "color") return "color";
|
|
3696
|
+
if (head === "spacing") return "dimension";
|
|
3697
|
+
if (head === "border" && (second === "radius" || second === "width")) return "dimension";
|
|
3698
|
+
if (head === "motion" && second === "duration") return "duration";
|
|
3699
|
+
if (head === "motion" && second === "easing") return "cubicBezier";
|
|
3700
|
+
if (head === "effect" && second === "shadow") return "shadow";
|
|
3701
|
+
if (head === "typography") {
|
|
3702
|
+
if (second === "family") return "fontFamily";
|
|
3703
|
+
if (second === "size") return "dimension";
|
|
3704
|
+
if (second === "weight") return "fontWeight";
|
|
3705
|
+
if (second === "lineHeight") return "number";
|
|
3706
|
+
if (second === "letterSpacing") return "dimension";
|
|
3707
|
+
}
|
|
3708
|
+
return void 0;
|
|
3709
|
+
}
|
|
3710
|
+
function handleVariantToken(node, path, type, root, tokens, ignored) {
|
|
3711
|
+
const cssVar = pathToVar(path);
|
|
3712
|
+
if (!cssVar) {
|
|
3713
|
+
ignored.push(path.join("."));
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
const ext = node.$extensions && node.$extensions[VB_NS2] || {};
|
|
3717
|
+
if (ext.lightDark) {
|
|
3718
|
+
tokens.push([cssVar, ext.lightDark]);
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
const light = node.light?.$value ? renderValue(node.light.$value, type, {}, root) : null;
|
|
3722
|
+
const dark = node.dark?.$value ? renderValue(node.dark.$value, type, {}, root) : null;
|
|
3723
|
+
const root_ = node.$root?.$value ? renderValue(node.$root.$value, type, {}, root) : null;
|
|
3724
|
+
if (light && dark) {
|
|
3725
|
+
tokens.push([cssVar, `light-dark(${light}, ${dark})`]);
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
if (root_) {
|
|
3729
|
+
tokens.push([cssVar, root_]);
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
if (light) {
|
|
3733
|
+
tokens.push([cssVar, light]);
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
if (dark) {
|
|
3737
|
+
tokens.push([cssVar, dark]);
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
ignored.push(path.join("."));
|
|
3741
|
+
}
|
|
3742
|
+
function renderValue(value, type, ext, root) {
|
|
3743
|
+
if (typeof value === "string" && /^\{[^}]+\}$/.test(value)) {
|
|
3744
|
+
const targetPath = value.slice(1, -1).split(".");
|
|
3745
|
+
const targetVar = pathToVar(targetPath);
|
|
3746
|
+
if (targetVar) return `var(${targetVar})`;
|
|
3747
|
+
return value;
|
|
3748
|
+
}
|
|
3749
|
+
switch (type) {
|
|
3750
|
+
case "color":
|
|
3751
|
+
return renderColor2(value, ext);
|
|
3752
|
+
case "fontFamily":
|
|
3753
|
+
return renderFontFamily(value);
|
|
3754
|
+
case "fontWeight":
|
|
3755
|
+
return String(value);
|
|
3756
|
+
case "number":
|
|
3757
|
+
return renderNumber(value, ext);
|
|
3758
|
+
case "dimension":
|
|
3759
|
+
return renderDimension(value, ext);
|
|
3760
|
+
case "duration":
|
|
3761
|
+
return renderDuration(value);
|
|
3762
|
+
case "cubicBezier":
|
|
3763
|
+
return Array.isArray(value) ? `cubic-bezier(${value.join(", ")})` : null;
|
|
3764
|
+
case "shadow":
|
|
3765
|
+
return renderShadow2(value);
|
|
3766
|
+
default:
|
|
3767
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
3768
|
+
return null;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
function renderColor2(value, ext) {
|
|
3772
|
+
if (!value || typeof value !== "object") return null;
|
|
3773
|
+
if (value.hex) return value.hex;
|
|
3774
|
+
if (value.colorSpace === "oklch") {
|
|
3775
|
+
const [L, C, H] = value.components || [0, 0, 0];
|
|
3776
|
+
const alpha = value.alpha != null ? ` / ${value.alpha}` : "";
|
|
3777
|
+
return `oklch(${L} ${C} ${H}${alpha})`;
|
|
3778
|
+
}
|
|
3779
|
+
if (value.colorSpace === "srgb") {
|
|
3780
|
+
const [r, g, b] = value.components || [0, 0, 0];
|
|
3781
|
+
const a = value.alpha;
|
|
3782
|
+
if (a != null) {
|
|
3783
|
+
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`;
|
|
3784
|
+
}
|
|
3785
|
+
return rgbToHex2(r, g, b);
|
|
3786
|
+
}
|
|
3787
|
+
return null;
|
|
3788
|
+
}
|
|
3789
|
+
function rgbToHex2(r, g, b) {
|
|
3790
|
+
const h = (n) => Math.round(n * 255).toString(16).padStart(2, "0");
|
|
3791
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
3792
|
+
}
|
|
3793
|
+
var BARE_FONT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
3794
|
+
"serif",
|
|
3795
|
+
"sans-serif",
|
|
3796
|
+
"monospace",
|
|
3797
|
+
"cursive",
|
|
3798
|
+
"fantasy",
|
|
3799
|
+
"system-ui",
|
|
3800
|
+
"ui-serif",
|
|
3801
|
+
"ui-sans-serif",
|
|
3802
|
+
"ui-monospace",
|
|
3803
|
+
"ui-rounded",
|
|
3804
|
+
"math",
|
|
3805
|
+
"emoji",
|
|
3806
|
+
"fangsong",
|
|
3807
|
+
"-apple-system",
|
|
3808
|
+
"BlinkMacSystemFont"
|
|
3809
|
+
]);
|
|
3810
|
+
function renderFontFamily(value) {
|
|
3811
|
+
if (typeof value === "string") return value;
|
|
3812
|
+
if (!Array.isArray(value)) return null;
|
|
3813
|
+
return value.map((f) => {
|
|
3814
|
+
if (BARE_FONT_KEYWORDS.has(f)) return f;
|
|
3815
|
+
if (f.startsWith('"') || f.startsWith("'")) return f;
|
|
3816
|
+
return `"${f}"`;
|
|
3817
|
+
}).join(", ");
|
|
3818
|
+
}
|
|
3819
|
+
function renderNumber(value, ext) {
|
|
3820
|
+
if (ext.unit === "%") return `${value}%`;
|
|
3821
|
+
return String(value);
|
|
3822
|
+
}
|
|
3823
|
+
function renderDimension(value, ext) {
|
|
3824
|
+
if (!value || typeof value !== "object") return null;
|
|
3825
|
+
const unit = ext.unit || value.unit || "px";
|
|
3826
|
+
return `${value.value}${unit}`;
|
|
3827
|
+
}
|
|
3828
|
+
function renderDuration(value) {
|
|
3829
|
+
if (!value || typeof value !== "object") return null;
|
|
3830
|
+
return `${value.value}${value.unit || "ms"}`;
|
|
3831
|
+
}
|
|
3832
|
+
function renderShadow2(value) {
|
|
3833
|
+
if (Array.isArray(value)) return value.map(renderShadowStop).filter(Boolean).join(", ");
|
|
3834
|
+
return renderShadowStop(value);
|
|
3835
|
+
}
|
|
3836
|
+
function renderShadowStop(stop) {
|
|
3837
|
+
if (!stop) return null;
|
|
3838
|
+
const ox = renderDimension(stop.offsetX, {}) ?? "0px";
|
|
3839
|
+
const oy = renderDimension(stop.offsetY, {}) ?? "0px";
|
|
3840
|
+
const blur = renderDimension(stop.blur, {}) ?? "0px";
|
|
3841
|
+
const spread = stop.spread ? " " + renderDimension(stop.spread, {}) : "";
|
|
3842
|
+
const color = renderColor2(stop.color, {}) ?? "#000";
|
|
3843
|
+
return `${ox} ${oy} ${blur}${spread} ${color}`;
|
|
3844
|
+
}
|
|
3845
|
+
function unpackTypography(token, path, tokens) {
|
|
3846
|
+
const v = token.$value;
|
|
3847
|
+
const leaf = path[path.length - 1];
|
|
3848
|
+
if (v.fontFamily != null) tokens.push([`--font-${leaf}`, renderFontFamily(v.fontFamily)]);
|
|
3849
|
+
if (v.fontSize != null) tokens.push([`--font-size-${leaf}`, renderDimension(v.fontSize, {})]);
|
|
3850
|
+
if (v.fontWeight != null) tokens.push([`--font-weight-${leaf}`, String(v.fontWeight)]);
|
|
3851
|
+
if (v.lineHeight != null) tokens.push([`--line-height-${leaf}`, String(v.lineHeight)]);
|
|
3852
|
+
if (v.letterSpacing != null) tokens.push([`--letter-spacing-${leaf}`, renderDimension(v.letterSpacing, {})]);
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// src/web-components/theme-import/logic.js
|
|
3856
|
+
var ThemeImport = class extends VBElement {
|
|
3857
|
+
static observedAttributes = ["target", "placeholder"];
|
|
3858
|
+
setup() {
|
|
3859
|
+
this.#render();
|
|
3860
|
+
this.#wire();
|
|
3861
|
+
}
|
|
3862
|
+
attributeChangedCallback() {
|
|
3863
|
+
if (this.isConnected && this.hasAttribute("data-upgraded")) {
|
|
3864
|
+
this.#render();
|
|
3865
|
+
this.#wire();
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
#targetEl() {
|
|
3869
|
+
const sel = this.getAttribute("target") || "#preview";
|
|
3870
|
+
try {
|
|
3871
|
+
return (
|
|
3872
|
+
/** @type {HTMLElement|null} */
|
|
3873
|
+
document.querySelector(sel)
|
|
3874
|
+
);
|
|
3875
|
+
} catch {
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
#render() {
|
|
3880
|
+
const placeholder = this.getAttribute("placeholder") || "Paste DTCG tokens.json here\u2026";
|
|
3881
|
+
const btn = `padding:0.375rem 0.75rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface,#fff);color:var(--color-text,#222);cursor:pointer;font:inherit;font-size:var(--font-size-xs,0.75rem)`;
|
|
3882
|
+
const inp = `padding:0.375rem 0.5rem;border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface,#fff);color:var(--color-text,#222);font:inherit;font-size:var(--font-size-xs,0.75rem)`;
|
|
3883
|
+
const ta = `inline-size:100%;block-size:10rem;padding:var(--size-s,0.75rem);border:1px solid var(--color-border,#ccc);border-radius:var(--radius-s,0.25rem);background:var(--color-surface-sunken,#f8f8f8);color:var(--color-text,#222);font-family:var(--font-mono,monospace);font-size:var(--font-size-xs,0.75rem);white-space:pre;overflow:auto;tab-size:2;resize:vertical;box-sizing:border-box`;
|
|
3884
|
+
const row = `display:flex;flex-wrap:wrap;gap:var(--size-2xs,0.375rem);align-items:center;margin-block-end:var(--size-s,0.5rem)`;
|
|
3885
|
+
const muted = `font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666)`;
|
|
3886
|
+
this.innerHTML = `
|
|
3887
|
+
<div class="ti-modes" role="tablist" aria-label="Import source" style="${row}">
|
|
3888
|
+
<button type="button" class="ti-mode" data-mode="paste" style="${btn}" role="tab" aria-selected="true">Paste</button>
|
|
3889
|
+
<button type="button" class="ti-mode" data-mode="file" style="${btn}" role="tab" aria-selected="false">File</button>
|
|
3890
|
+
<button type="button" class="ti-mode" data-mode="url" style="${btn}" role="tab" aria-selected="false">URL</button>
|
|
3891
|
+
<span style="${muted};margin-inline-start:auto" class="ti-target"></span>
|
|
3892
|
+
</div>
|
|
3893
|
+
|
|
3894
|
+
<div class="ti-pane ti-pane-paste" role="tabpanel">
|
|
3895
|
+
<textarea class="ti-paste" aria-label="DTCG JSON" placeholder="${placeholder}" style="${ta}"></textarea>
|
|
3896
|
+
<div style="${row};margin-block-start:var(--size-2xs,0.375rem)">
|
|
3897
|
+
<button type="button" class="ti-apply-paste" style="${btn}">Apply</button>
|
|
3898
|
+
</div>
|
|
3899
|
+
</div>
|
|
3900
|
+
|
|
3901
|
+
<div class="ti-pane ti-pane-file" role="tabpanel" hidden>
|
|
3902
|
+
<label style="display:flex;flex-direction:column;gap:var(--size-2xs,0.375rem);${muted}">
|
|
3903
|
+
<span>DTCG file (.tokens.json or .json)</span>
|
|
3904
|
+
<input class="ti-file" type="file" accept=".json,.tokens,.tokens.json,application/json,application/design-tokens+json" style="${inp}">
|
|
3905
|
+
</label>
|
|
3906
|
+
</div>
|
|
3907
|
+
|
|
3908
|
+
<div class="ti-pane ti-pane-url" role="tabpanel" hidden>
|
|
3909
|
+
<label style="display:flex;flex-direction:column;gap:var(--size-2xs,0.375rem);${muted}">
|
|
3910
|
+
<span>DTCG URL (CORS-permitted host)</span>
|
|
3911
|
+
<input class="ti-url" type="url" placeholder="https://example.com/theme.tokens.json" style="${inp}">
|
|
3912
|
+
</label>
|
|
3913
|
+
<div style="${row};margin-block-start:var(--size-2xs,0.375rem)">
|
|
3914
|
+
<button type="button" class="ti-apply-url" style="${btn}">Fetch & apply</button>
|
|
3915
|
+
</div>
|
|
3916
|
+
</div>
|
|
3917
|
+
|
|
3918
|
+
<output class="ti-result" style="display:block;margin-block-start:var(--size-s,0.5rem);${muted}"></output>
|
|
3919
|
+
|
|
3920
|
+
<details class="ti-ignored" style="margin-block-start:var(--size-2xs,0.375rem)" hidden>
|
|
3921
|
+
<summary style="${muted};cursor:pointer">Ignored tokens</summary>
|
|
3922
|
+
<ul class="ti-ignored-list" style="margin:var(--size-2xs,0.375rem) 0 0;padding-inline-start:1.25rem;${muted}"></ul>
|
|
3923
|
+
</details>
|
|
3924
|
+
|
|
3925
|
+
<div style="${row};margin-block-start:var(--size-s,0.5rem)">
|
|
3926
|
+
<button type="button" class="ti-reset" style="${btn}">Reset preview scope</button>
|
|
3927
|
+
</div>
|
|
3928
|
+
`;
|
|
3929
|
+
this.#refreshTargetLabel();
|
|
3930
|
+
}
|
|
3931
|
+
#refreshTargetLabel() {
|
|
3932
|
+
const span = this.querySelector(".ti-target");
|
|
3933
|
+
if (!span) return;
|
|
3934
|
+
const sel = this.getAttribute("target") || "#preview";
|
|
3935
|
+
const found = this.#targetEl();
|
|
3936
|
+
span.textContent = found ? `Target: ${sel}` : `Target ${sel} not found`;
|
|
3937
|
+
}
|
|
3938
|
+
#wire() {
|
|
3939
|
+
for (const tab of this.querySelectorAll(".ti-mode")) {
|
|
3940
|
+
this.listen(tab, "click", () => this.#switchMode(
|
|
3941
|
+
/** @type {HTMLElement} */
|
|
3942
|
+
tab.dataset.mode || "paste"
|
|
3943
|
+
));
|
|
3944
|
+
}
|
|
3945
|
+
const apply = this.querySelector(".ti-apply-paste");
|
|
3946
|
+
if (apply) this.listen(apply, "click", () => this.#applyFromPaste());
|
|
3947
|
+
const file = (
|
|
3948
|
+
/** @type {HTMLInputElement|null} */
|
|
3949
|
+
this.querySelector(".ti-file")
|
|
3950
|
+
);
|
|
3951
|
+
if (file) this.listen(file, "change", () => this.#applyFromFile(file));
|
|
3952
|
+
const applyUrl = this.querySelector(".ti-apply-url");
|
|
3953
|
+
if (applyUrl) this.listen(applyUrl, "click", () => this.#applyFromURL());
|
|
3954
|
+
const reset = this.querySelector(".ti-reset");
|
|
3955
|
+
if (reset) this.listen(reset, "click", () => this.#reset());
|
|
3956
|
+
}
|
|
3957
|
+
#switchMode(mode) {
|
|
3958
|
+
for (const tab of this.querySelectorAll(".ti-mode")) {
|
|
3959
|
+
const t = (
|
|
3960
|
+
/** @type {HTMLElement} */
|
|
3961
|
+
tab
|
|
3962
|
+
);
|
|
3963
|
+
const active = t.dataset.mode === mode;
|
|
3964
|
+
t.setAttribute("aria-selected", String(active));
|
|
3965
|
+
}
|
|
3966
|
+
for (const pane of this.querySelectorAll(".ti-pane")) {
|
|
3967
|
+
const p = (
|
|
3968
|
+
/** @type {HTMLElement} */
|
|
3969
|
+
pane
|
|
3970
|
+
);
|
|
3971
|
+
p.hidden = !p.classList.contains(`ti-pane-${mode}`);
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
#applyFromPaste() {
|
|
3975
|
+
const ta = (
|
|
3976
|
+
/** @type {HTMLTextAreaElement|null} */
|
|
3977
|
+
this.querySelector(".ti-paste")
|
|
3978
|
+
);
|
|
3979
|
+
if (!ta) return;
|
|
3980
|
+
this.#applyJSON(ta.value, "paste");
|
|
3981
|
+
}
|
|
3982
|
+
/** @param {HTMLInputElement} input */
|
|
3983
|
+
async #applyFromFile(input) {
|
|
3984
|
+
const file = input.files?.[0];
|
|
3985
|
+
if (!file) return;
|
|
3986
|
+
try {
|
|
3987
|
+
const text = await file.text();
|
|
3988
|
+
this.#applyJSON(text, "file");
|
|
3989
|
+
} catch (err) {
|
|
3990
|
+
this.#fail(err, "file");
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
async #applyFromURL() {
|
|
3994
|
+
const input = (
|
|
3995
|
+
/** @type {HTMLInputElement|null} */
|
|
3996
|
+
this.querySelector(".ti-url")
|
|
3997
|
+
);
|
|
3998
|
+
const url = input?.value?.trim();
|
|
3999
|
+
if (!url) return;
|
|
4000
|
+
try {
|
|
4001
|
+
const res = await fetch(url, { mode: "cors" });
|
|
4002
|
+
if (!res.ok) throw new Error(`Fetch failed: ${res.status} ${res.statusText}`);
|
|
4003
|
+
const text = await res.text();
|
|
4004
|
+
this.#applyJSON(text, "url");
|
|
4005
|
+
} catch (err) {
|
|
4006
|
+
this.#fail(err, "url");
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* @param {string} text
|
|
4011
|
+
* @param {'paste'|'file'|'url'} source
|
|
4012
|
+
*/
|
|
4013
|
+
#applyJSON(text, source) {
|
|
4014
|
+
let doc;
|
|
4015
|
+
try {
|
|
4016
|
+
doc = JSON.parse(text);
|
|
4017
|
+
} catch (err) {
|
|
4018
|
+
this.#fail(err, "parse");
|
|
4019
|
+
return;
|
|
4020
|
+
}
|
|
4021
|
+
const result = deserializeDTCG(doc);
|
|
4022
|
+
const target = this.#targetEl();
|
|
4023
|
+
if (!target) {
|
|
4024
|
+
this.#fail(new Error(`Target ${this.getAttribute("target") || "#preview"} not found`), "apply");
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
for (const [name, value] of result.tokens) {
|
|
4028
|
+
target.style.setProperty(name, value);
|
|
4029
|
+
}
|
|
4030
|
+
this.#showResult(result, source);
|
|
4031
|
+
this.dispatchEvent(new CustomEvent("theme-import:applied", {
|
|
4032
|
+
bubbles: true,
|
|
4033
|
+
detail: {
|
|
4034
|
+
tokens: result.tokens,
|
|
4035
|
+
applied: result.stats.applied,
|
|
4036
|
+
ignored: result.ignored,
|
|
4037
|
+
source
|
|
4038
|
+
}
|
|
4039
|
+
}));
|
|
4040
|
+
}
|
|
4041
|
+
#reset() {
|
|
4042
|
+
const target = this.#targetEl();
|
|
4043
|
+
if (!target) return;
|
|
4044
|
+
const style = target.style;
|
|
4045
|
+
const remove = [];
|
|
4046
|
+
for (let i = 0; i < style.length; i++) {
|
|
4047
|
+
const name = style.item(i);
|
|
4048
|
+
if (name.startsWith("--")) remove.push(name);
|
|
4049
|
+
}
|
|
4050
|
+
for (const name of remove) style.removeProperty(name);
|
|
4051
|
+
const out = this.querySelector(".ti-result");
|
|
4052
|
+
if (out) out.textContent = `Cleared ${remove.length} custom properties from ${this.getAttribute("target") || "#preview"}.`;
|
|
4053
|
+
const ig = this.querySelector(".ti-ignored");
|
|
4054
|
+
if (ig) ig.hidden = true;
|
|
4055
|
+
}
|
|
4056
|
+
#showResult(result, source) {
|
|
4057
|
+
const out = this.querySelector(".ti-result");
|
|
4058
|
+
if (out) {
|
|
4059
|
+
out.textContent = `Applied ${result.stats.applied} token${result.stats.applied === 1 ? "" : "s"} from ${source}${result.stats.ignored ? ` \xB7 ${result.stats.ignored} ignored` : ""}.`;
|
|
4060
|
+
}
|
|
4061
|
+
const ig = this.querySelector(".ti-ignored");
|
|
4062
|
+
const list = this.querySelector(".ti-ignored-list");
|
|
4063
|
+
if (ig && list) {
|
|
4064
|
+
const det = (
|
|
4065
|
+
/** @type {HTMLDetailsElement} */
|
|
4066
|
+
ig
|
|
4067
|
+
);
|
|
4068
|
+
if (result.ignored.length === 0) {
|
|
4069
|
+
det.hidden = true;
|
|
4070
|
+
list.innerHTML = "";
|
|
4071
|
+
} else {
|
|
4072
|
+
det.hidden = false;
|
|
4073
|
+
list.innerHTML = result.ignored.map((p) => `<li>${escapeHTML(p)}</li>`).join("");
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
#fail(err, phase) {
|
|
4078
|
+
const out = this.querySelector(".ti-result");
|
|
4079
|
+
if (out) out.textContent = `Error (${phase}): ${err && err.message ? err.message : String(err)}`;
|
|
4080
|
+
this.dispatchEvent(new CustomEvent("theme-import:error", {
|
|
4081
|
+
bubbles: true,
|
|
4082
|
+
detail: { error: err, phase }
|
|
4083
|
+
}));
|
|
4084
|
+
}
|
|
4085
|
+
};
|
|
4086
|
+
function escapeHTML(s) {
|
|
4087
|
+
return String(s).replace(/[&<>"']/g, (ch) => (
|
|
4088
|
+
/** @type {Record<string,string>} */
|
|
4089
|
+
{
|
|
4090
|
+
"&": "&",
|
|
4091
|
+
"<": "<",
|
|
4092
|
+
">": ">",
|
|
4093
|
+
'"': """,
|
|
4094
|
+
"'": "'"
|
|
4095
|
+
}[ch]
|
|
4096
|
+
));
|
|
4097
|
+
}
|
|
4098
|
+
registerComponent("theme-import", ThemeImport);
|
|
4099
|
+
|
|
4100
|
+
// src/web-components/theme-catalog/logic.js
|
|
4101
|
+
var ThemeCatalog = class extends VBElement {
|
|
4102
|
+
static observedAttributes = ["target", "catalog-base"];
|
|
4103
|
+
/** @type {any[] | null} */
|
|
4104
|
+
#entries = null;
|
|
4105
|
+
#lastAppliedId = "";
|
|
4106
|
+
setup() {
|
|
4107
|
+
this.#renderShell();
|
|
4108
|
+
this.#loadAndRender();
|
|
4109
|
+
}
|
|
4110
|
+
attributeChangedCallback() {
|
|
4111
|
+
if (this.isConnected && this.hasAttribute("data-upgraded")) {
|
|
4112
|
+
this.#renderShell();
|
|
4113
|
+
this.#loadAndRender();
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
#targetEl() {
|
|
4117
|
+
const sel = this.getAttribute("target") || "#preview";
|
|
4118
|
+
try {
|
|
4119
|
+
return (
|
|
4120
|
+
/** @type {HTMLElement|null} */
|
|
4121
|
+
document.querySelector(sel)
|
|
4122
|
+
);
|
|
4123
|
+
} catch {
|
|
4124
|
+
return null;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
/**
|
|
4128
|
+
* Resolve the catalog base URL. The component is bundled into the VB
|
|
4129
|
+
* core so it ships from the same origin as `dist/cdn/components/theme-catalog.js`.
|
|
4130
|
+
* Catalog files live in src/web-components/theme-import/catalog/ — the
|
|
4131
|
+
* publish pipeline copies them under dist/cdn/themes/catalog/ (Phase 4
|
|
4132
|
+
* build step). Auto-detect from a known sibling import or accept an
|
|
4133
|
+
* explicit `catalog-base` attribute override.
|
|
4134
|
+
*/
|
|
4135
|
+
#base() {
|
|
4136
|
+
const explicit = this.getAttribute("catalog-base");
|
|
4137
|
+
if (explicit) return explicit.replace(/\/$/, "");
|
|
4138
|
+
if (typeof window !== "undefined" && /** @type {any} */
|
|
4139
|
+
window.__VB_CATALOG_BASE) {
|
|
4140
|
+
return String(
|
|
4141
|
+
/** @type {any} */
|
|
4142
|
+
window.__VB_CATALOG_BASE
|
|
4143
|
+
).replace(/\/$/, "");
|
|
4144
|
+
}
|
|
4145
|
+
return "/cdn/themes/catalog";
|
|
4146
|
+
}
|
|
4147
|
+
#renderShell() {
|
|
4148
|
+
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`;
|
|
4149
|
+
const grid = `display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:var(--size-s,0.75rem)`;
|
|
4150
|
+
const muted = `font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666)`;
|
|
4151
|
+
const title = `margin:0;font-size:var(--font-size-md,1rem);font-weight:600;color:var(--color-text,#222)`;
|
|
4152
|
+
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)`;
|
|
4153
|
+
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`;
|
|
4154
|
+
this.innerHTML = `
|
|
4155
|
+
<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">
|
|
4156
|
+
<strong style="font-size:var(--font-size-sm,0.875rem)">Start from a known system</strong>
|
|
4157
|
+
<span class="tc-target" style="${muted}"></span>
|
|
4158
|
+
</div>
|
|
4159
|
+
<div class="tc-grid" style="${grid}" role="list"></div>
|
|
4160
|
+
<output class="tc-result" style="display:block;margin-block-start:var(--size-s,0.75rem);${muted}"></output>
|
|
4161
|
+
<p class="tc-attribution" style="margin-block-start:var(--size-2xs,0.375rem);${muted}" hidden></p>
|
|
4162
|
+
<details class="tc-ignored" style="margin-block-start:var(--size-2xs,0.375rem)" hidden>
|
|
4163
|
+
<summary style="${muted};cursor:pointer">Ignored tokens</summary>
|
|
4164
|
+
<ul class="tc-ignored-list" style="margin:var(--size-2xs,0.375rem) 0 0;padding-inline-start:1.25rem;${muted}"></ul>
|
|
4165
|
+
</details>
|
|
4166
|
+
<style>
|
|
4167
|
+
:host ._card { ${card} }
|
|
4168
|
+
:host ._title { ${title} }
|
|
4169
|
+
:host ._btn { ${btn} }
|
|
4170
|
+
:host ._tag { ${tag} }
|
|
4171
|
+
</style>
|
|
4172
|
+
`;
|
|
4173
|
+
this.#refreshTargetLabel();
|
|
4174
|
+
}
|
|
4175
|
+
#refreshTargetLabel() {
|
|
4176
|
+
const span = this.querySelector(".tc-target");
|
|
4177
|
+
if (!span) return;
|
|
4178
|
+
const sel = this.getAttribute("target") || "#preview";
|
|
4179
|
+
const found = this.#targetEl();
|
|
4180
|
+
span.textContent = found ? `Target: ${sel}` : `Target ${sel} not found`;
|
|
4181
|
+
}
|
|
4182
|
+
async #loadAndRender() {
|
|
4183
|
+
const grid = this.querySelector(".tc-grid");
|
|
4184
|
+
if (!grid) return;
|
|
4185
|
+
try {
|
|
4186
|
+
const res = await fetch(`${this.#base()}/manifest.json`);
|
|
4187
|
+
if (!res.ok) throw new Error(`Catalog manifest fetch failed: ${res.status}`);
|
|
4188
|
+
const manifest = await res.json();
|
|
4189
|
+
this.#entries = manifest.entries || [];
|
|
4190
|
+
this.#renderTiles();
|
|
4191
|
+
} catch (err) {
|
|
4192
|
+
grid.innerHTML = "";
|
|
4193
|
+
const out = this.querySelector(".tc-result");
|
|
4194
|
+
if (out) out.textContent = `Couldn't load catalog manifest: ${err && err.message ? err.message : String(err)}`;
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
#renderTiles() {
|
|
4198
|
+
const grid = this.querySelector(".tc-grid");
|
|
4199
|
+
if (!grid || !this.#entries) return;
|
|
4200
|
+
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)`;
|
|
4201
|
+
const title = `margin:0;font-size:var(--font-size-md,1rem);font-weight:600;color:var(--color-text,#222)`;
|
|
4202
|
+
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)`;
|
|
4203
|
+
const muted = `font-size:var(--font-size-xs,0.75rem);color:var(--color-text-muted,#666)`;
|
|
4204
|
+
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`;
|
|
4205
|
+
grid.innerHTML = "";
|
|
4206
|
+
for (const entry of this.#entries) {
|
|
4207
|
+
const tile = document.createElement("article");
|
|
4208
|
+
tile.setAttribute("role", "listitem");
|
|
4209
|
+
tile.setAttribute("data-id", entry.id);
|
|
4210
|
+
tile.style.cssText = card;
|
|
4211
|
+
tile.innerHTML = `
|
|
4212
|
+
<h3 style="${title}">${escapeHTML2(entry.name)}</h3>
|
|
4213
|
+
<p style="${muted};margin:0">${escapeHTML2(entry.summary || "")}</p>
|
|
4214
|
+
<p style="${muted};margin:0">License: ${escapeHTML2(entry.license)}</p>
|
|
4215
|
+
<p style="margin:0">${(entry.tags || []).map((t) => `<span style="${tag}">${escapeHTML2(t)}</span>`).join("")}</p>
|
|
4216
|
+
<button type="button" class="tc-apply" style="${btn}" data-id="${escapeAttr(entry.id)}" aria-label="Apply ${escapeAttr(entry.name)} tokens">
|
|
4217
|
+
Apply
|
|
4218
|
+
</button>
|
|
4219
|
+
`;
|
|
4220
|
+
const btnEl = (
|
|
4221
|
+
/** @type {HTMLButtonElement|null} */
|
|
4222
|
+
tile.querySelector("button.tc-apply")
|
|
4223
|
+
);
|
|
4224
|
+
if (btnEl) this.listen(btnEl, "click", () => this.#applyEntry(entry));
|
|
4225
|
+
grid.appendChild(tile);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
/**
|
|
4229
|
+
* @param {{ id: string, name: string, file: string, attribution: string, source: string, license: string }} entry
|
|
4230
|
+
*/
|
|
4231
|
+
async #applyEntry(entry) {
|
|
4232
|
+
const target = this.#targetEl();
|
|
4233
|
+
if (!target) {
|
|
4234
|
+
this.#fail(entry.id, new Error(`Target ${this.getAttribute("target") || "#preview"} not found`), "apply");
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4237
|
+
try {
|
|
4238
|
+
const res = await fetch(`${this.#base()}/${entry.file}`);
|
|
4239
|
+
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
|
|
4240
|
+
const doc = await res.json();
|
|
4241
|
+
const result = deserializeDTCG(doc);
|
|
4242
|
+
for (const [name, value] of result.tokens) {
|
|
4243
|
+
target.style.setProperty(name, value);
|
|
4244
|
+
}
|
|
4245
|
+
this.#showResult(entry, result);
|
|
4246
|
+
this.#lastAppliedId = entry.id;
|
|
4247
|
+
this.dispatchEvent(new CustomEvent("theme-catalog:applied", {
|
|
4248
|
+
bubbles: true,
|
|
4249
|
+
detail: { id: entry.id, applied: result.stats.applied, ignored: result.ignored, tokens: result.tokens, source: "catalog" }
|
|
4250
|
+
}));
|
|
4251
|
+
} catch (err) {
|
|
4252
|
+
this.#fail(entry.id, err, "fetch-or-apply");
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
#showResult(entry, result) {
|
|
4256
|
+
const out = this.querySelector(".tc-result");
|
|
4257
|
+
if (out) {
|
|
4258
|
+
out.textContent = `Applied ${result.stats.applied} token${result.stats.applied === 1 ? "" : "s"} from ${entry.name}${result.stats.ignored ? ` \xB7 ${result.stats.ignored} ignored` : ""}.`;
|
|
4259
|
+
}
|
|
4260
|
+
const att = (
|
|
4261
|
+
/** @type {HTMLElement|null} */
|
|
4262
|
+
this.querySelector(".tc-attribution")
|
|
4263
|
+
);
|
|
4264
|
+
if (att) {
|
|
4265
|
+
att.hidden = false;
|
|
4266
|
+
att.textContent = entry.attribution;
|
|
4267
|
+
}
|
|
4268
|
+
const ig = (
|
|
4269
|
+
/** @type {HTMLDetailsElement|null} */
|
|
4270
|
+
this.querySelector(".tc-ignored")
|
|
4271
|
+
);
|
|
4272
|
+
const list = this.querySelector(".tc-ignored-list");
|
|
4273
|
+
if (ig && list) {
|
|
4274
|
+
if (result.ignored.length === 0) {
|
|
4275
|
+
ig.hidden = true;
|
|
4276
|
+
list.innerHTML = "";
|
|
4277
|
+
} else {
|
|
4278
|
+
ig.hidden = false;
|
|
4279
|
+
list.innerHTML = result.ignored.map((p) => `<li>${escapeHTML2(p)}</li>`).join("");
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
#fail(id, err, phase) {
|
|
4284
|
+
const out = this.querySelector(".tc-result");
|
|
4285
|
+
if (out) out.textContent = `Error (${phase}): ${err && err.message ? err.message : String(err)}`;
|
|
4286
|
+
this.dispatchEvent(new CustomEvent("theme-catalog:error", {
|
|
4287
|
+
bubbles: true,
|
|
4288
|
+
detail: { id, error: err, phase }
|
|
4289
|
+
}));
|
|
4290
|
+
}
|
|
4291
|
+
};
|
|
4292
|
+
function escapeHTML2(s) {
|
|
4293
|
+
return String(s).replace(/[&<>"']/g, (ch) => (
|
|
4294
|
+
/** @type {Record<string,string>} */
|
|
4295
|
+
{
|
|
4296
|
+
"&": "&",
|
|
4297
|
+
"<": "<",
|
|
4298
|
+
">": ">",
|
|
4299
|
+
'"': """,
|
|
4300
|
+
"'": "'"
|
|
4301
|
+
}[ch]
|
|
4302
|
+
));
|
|
4303
|
+
}
|
|
4304
|
+
function escapeAttr(s) {
|
|
4305
|
+
return escapeHTML2(s);
|
|
4306
|
+
}
|
|
4307
|
+
registerComponent("theme-catalog", ThemeCatalog);
|
|
4308
|
+
|
|
4309
|
+
// src/web-components/brand-specimen/logic.js
|
|
4310
|
+
var BrandSpecimen = class extends VBElement {
|
|
4311
|
+
static observedAttributes = ["src", "name", "data-sizes"];
|
|
4312
|
+
setup() {
|
|
4313
|
+
this.#render();
|
|
4314
|
+
}
|
|
4315
|
+
attributeChangedCallback() {
|
|
4316
|
+
if (this.isConnected) this.#render();
|
|
4317
|
+
}
|
|
4318
|
+
#render() {
|
|
4319
|
+
const src = this.getAttribute("src") || "";
|
|
4320
|
+
const name = this.getAttribute("name") || "Brand";
|
|
4321
|
+
const sizes = (this.getAttribute("data-sizes") || "s m l xl").trim().split(/\s+/);
|
|
4322
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4323
|
+
const mark = (size) => `<brand-mark${src ? ` src="${esc(src)}"` : ""} wordmark${size ? ` data-size="${size}"` : ""}>${esc(name)}</brand-mark>`;
|
|
4324
|
+
this.innerHTML = `
|
|
4325
|
+
<section class="brand-specimen">
|
|
4326
|
+
<div class="bs-surfaces">
|
|
4327
|
+
<figure data-surface="light">${mark("l")}<figcaption>On light</figcaption></figure>
|
|
4328
|
+
<figure data-surface="dark">${mark("l")}<figcaption>On dark</figcaption></figure>
|
|
4329
|
+
</div>
|
|
4330
|
+
<div class="bs-scale" data-scale>
|
|
4331
|
+
${sizes.map((s) => `<span>${mark(s)}<small>${s}</small></span>`).join("")}
|
|
4332
|
+
</div>
|
|
4333
|
+
<div class="bs-clearspace"><span class="bs-clear-box">${mark("l")}</span>
|
|
4334
|
+
<figcaption>Clear space</figcaption></div>
|
|
4335
|
+
</section>`;
|
|
4336
|
+
}
|
|
4337
|
+
};
|
|
4338
|
+
registerComponent("brand-specimen", BrandSpecimen);
|
|
4339
|
+
|
|
4340
|
+
// src/web-components/icon-set/logic.js
|
|
4341
|
+
var iconBase = () => document.documentElement.dataset.iconPath || "/cdn/icons";
|
|
4342
|
+
var IconSet = class extends VBElement {
|
|
4343
|
+
static observedAttributes = ["set", "names"];
|
|
4344
|
+
/** @type {IntersectionObserver | null} Lazy-mounts [data-icon] icons as cells scroll in. */
|
|
4345
|
+
#io = null;
|
|
4346
|
+
setup() {
|
|
4347
|
+
this.#render();
|
|
4348
|
+
this.#load();
|
|
4349
|
+
}
|
|
4350
|
+
attributeChangedCallback() {
|
|
4351
|
+
if (this.isConnected) {
|
|
4352
|
+
this.#render();
|
|
4353
|
+
this.#load();
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
disconnectedCallback() {
|
|
4357
|
+
this.#io?.disconnect();
|
|
4358
|
+
this.#io = null;
|
|
4359
|
+
super.disconnectedCallback?.();
|
|
4360
|
+
}
|
|
4361
|
+
get set() {
|
|
4362
|
+
return this.getAttribute("set") || "lucide";
|
|
4363
|
+
}
|
|
4364
|
+
async #load() {
|
|
4365
|
+
const attr = this.getAttribute("names");
|
|
4366
|
+
let names = attr ? attr.split(/[\s,]+/).filter(Boolean) : null;
|
|
4367
|
+
if (!names) {
|
|
4368
|
+
try {
|
|
4369
|
+
names = await (await fetch(`${iconBase()}/${this.set}.json`)).json();
|
|
4370
|
+
} catch {
|
|
4371
|
+
names = [];
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
this.#renderGrid(names);
|
|
4375
|
+
}
|
|
4376
|
+
#render() {
|
|
4377
|
+
this.innerHTML = `
|
|
4378
|
+
<div class="icon-set">
|
|
4379
|
+
<label class="icon-set__search">
|
|
4380
|
+
<span class="visually-hidden">Filter icons</span>
|
|
4381
|
+
<input type="search" placeholder="Filter ${this.set} icons\u2026">
|
|
4382
|
+
</label>
|
|
4383
|
+
<ul class="icon-set__grid" data-layout="grid" role="list"></ul>
|
|
4384
|
+
</div>`;
|
|
4385
|
+
const input = (
|
|
4386
|
+
/** @type {HTMLInputElement} */
|
|
4387
|
+
this.querySelector("input")
|
|
4388
|
+
);
|
|
4389
|
+
this.listen(input, "input", (e) => this.#filter(
|
|
4390
|
+
/** @type {HTMLInputElement} */
|
|
4391
|
+
e.target.value
|
|
4392
|
+
));
|
|
4393
|
+
}
|
|
4394
|
+
#renderGrid(names) {
|
|
4395
|
+
const ul = this.querySelector(".icon-set__grid");
|
|
4396
|
+
if (!ul) return;
|
|
4397
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4398
|
+
ul.innerHTML = names.map((n) => `
|
|
4399
|
+
<li data-icon-name="${esc(n)}">
|
|
4400
|
+
<button type="button" title="Copy \u201C${esc(n)}\u201D" data-copy="${esc(n)}">
|
|
4401
|
+
<span class="icon-set__icon" data-name="${esc(n)}"></span>
|
|
4402
|
+
<span>${esc(n)}</span>
|
|
4403
|
+
</button>
|
|
4404
|
+
</li>`).join("");
|
|
4405
|
+
this.listen(ul, "click", (e) => {
|
|
4406
|
+
const btn = (
|
|
4407
|
+
/** @type {HTMLElement} */
|
|
4408
|
+
e.target.closest("[data-copy]")
|
|
4409
|
+
);
|
|
4410
|
+
if (btn) navigator.clipboard?.writeText(
|
|
4411
|
+
/** @type {HTMLElement} */
|
|
4412
|
+
btn.dataset.copy ?? ""
|
|
4413
|
+
);
|
|
4414
|
+
});
|
|
4415
|
+
const set = this.set;
|
|
4416
|
+
this.#io?.disconnect();
|
|
4417
|
+
this.#io = new IntersectionObserver((entries, obs) => {
|
|
4418
|
+
for (const entry of entries) {
|
|
4419
|
+
if (!entry.isIntersecting) continue;
|
|
4420
|
+
const slot = (
|
|
4421
|
+
/** @type {HTMLElement} */
|
|
4422
|
+
entry.target
|
|
4423
|
+
);
|
|
4424
|
+
obs.unobserve(slot);
|
|
4425
|
+
if (slot.firstElementChild) continue;
|
|
4426
|
+
const icon = document.createElement("i");
|
|
4427
|
+
icon.setAttribute("data-icon", slot.dataset.name ?? "");
|
|
4428
|
+
icon.setAttribute("data-icon-set", set);
|
|
4429
|
+
icon.setAttribute("aria-hidden", "true");
|
|
4430
|
+
slot.appendChild(icon);
|
|
4431
|
+
}
|
|
4432
|
+
}, { rootMargin: "400px" });
|
|
4433
|
+
for (const slot of ul.querySelectorAll(".icon-set__icon")) this.#io.observe(slot);
|
|
4434
|
+
}
|
|
4435
|
+
#filter(q) {
|
|
4436
|
+
const term = q.trim().toLowerCase();
|
|
4437
|
+
for (const li of this.querySelectorAll("[data-icon-name]")) {
|
|
4438
|
+
li.hidden = !!term && !/** @type {HTMLElement} */
|
|
4439
|
+
(li.dataset.iconName ?? "").toLowerCase().includes(term);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
};
|
|
4443
|
+
registerComponent("icon-set", IconSet);
|
|
4444
|
+
|
|
4445
|
+
// src/web-components/icon-specimen/logic.js
|
|
4446
|
+
var IconSpecimen = class extends VBElement {
|
|
4447
|
+
static observedAttributes = ["set", "names", "data-sizes"];
|
|
4448
|
+
setup() {
|
|
4449
|
+
this.#render();
|
|
4450
|
+
}
|
|
4451
|
+
attributeChangedCallback() {
|
|
4452
|
+
if (this.isConnected) this.#render();
|
|
4453
|
+
}
|
|
4454
|
+
get set() {
|
|
4455
|
+
return this.getAttribute("set") || "lucide";
|
|
4456
|
+
}
|
|
4457
|
+
get names() {
|
|
4458
|
+
return (this.getAttribute("names") || "").split(/[\s,]+/).filter(Boolean);
|
|
4459
|
+
}
|
|
4460
|
+
get sizes() {
|
|
4461
|
+
return (this.getAttribute("data-sizes") || "1rem 1.5rem 2rem").trim().split(/\s+/);
|
|
4462
|
+
}
|
|
4463
|
+
#render() {
|
|
4464
|
+
const { set, names, sizes } = this;
|
|
4465
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4466
|
+
const cell = (n) => sizes.map((sz) => `
|
|
4467
|
+
<td><i data-icon="${esc(n)}" data-icon-set="${set}" style="font-size:${sz}" aria-hidden="true"></i></td>`).join("");
|
|
4468
|
+
this.innerHTML = `
|
|
4469
|
+
<table class="icon-specimen">
|
|
4470
|
+
<thead>
|
|
4471
|
+
<tr>
|
|
4472
|
+
<th scope="col">Icon</th>
|
|
4473
|
+
${sizes.map((sz) => `<th scope="col">${sz}</th>`).join("")}
|
|
4474
|
+
</tr>
|
|
4475
|
+
</thead>
|
|
4476
|
+
<tbody>
|
|
4477
|
+
${names.map((n) => `<tr><th scope="row"><code>${n}</code></th>${cell(n)}</tr>`).join("")}
|
|
4478
|
+
</tbody>
|
|
4479
|
+
</table>`;
|
|
4480
|
+
}
|
|
4481
|
+
};
|
|
4482
|
+
registerComponent("icon-specimen", IconSpecimen);
|