@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,180 @@
|
|
|
1
|
+
/* breakpoint-specimen: Responsive breakpoint visualization */
|
|
2
|
+
|
|
3
|
+
breakpoint-specimen:not(:defined) {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
breakpoint-specimen {
|
|
8
|
+
display: block;
|
|
9
|
+
font-family: var(--font-sans, system-ui);
|
|
10
|
+
color: var(--color-text, #1a1a1a);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@scope (breakpoint-specimen) {
|
|
14
|
+
.bps-label {
|
|
15
|
+
display: block;
|
|
16
|
+
font-size: var(--font-size-xs, 0.75rem);
|
|
17
|
+
font-weight: 700;
|
|
18
|
+
text-transform: uppercase;
|
|
19
|
+
letter-spacing: 0.08em;
|
|
20
|
+
color: var(--color-text-muted, #666);
|
|
21
|
+
margin-block-end: var(--size-s, 0.75rem);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.bps-readout {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: baseline;
|
|
27
|
+
gap: var(--size-s, 0.75rem);
|
|
28
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
29
|
+
font-variant-numeric: tabular-nums;
|
|
30
|
+
margin-block-end: var(--size-m, 1rem);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.bps-width {
|
|
34
|
+
font-size: var(--font-size-l, 1.25rem);
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.bps-active {
|
|
39
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
40
|
+
padding: var(--size-3xs, 0.125rem) var(--size-xs, 0.5rem);
|
|
41
|
+
border-radius: var(--radius-s, 0.25rem);
|
|
42
|
+
background: var(--color-surface-raised, oklch(96% 0 0));
|
|
43
|
+
color: var(--color-text-muted, #666);
|
|
44
|
+
border: 1px solid var(--color-border, oklch(85% 0 0));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.bps-active[data-active]:not([data-active=""]) {
|
|
48
|
+
background: var(--color-accent-soft, oklch(94% 0.04 260));
|
|
49
|
+
color: var(--color-accent, oklch(45% 0.18 260));
|
|
50
|
+
border-color: currentColor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.bps-ruler {
|
|
54
|
+
position: relative;
|
|
55
|
+
block-size: var(--size-xl, 2.5rem);
|
|
56
|
+
margin: var(--size-s, 0.75rem) 0 var(--size-l, 1.5rem);
|
|
57
|
+
border-block-start: 2px solid var(--color-border, oklch(85% 0 0));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.bps-cursor {
|
|
61
|
+
position: absolute;
|
|
62
|
+
inset-block: -0.5rem auto;
|
|
63
|
+
inline-size: 2px;
|
|
64
|
+
block-size: calc(100% + 1rem);
|
|
65
|
+
background: var(--color-accent, oklch(45% 0.18 260));
|
|
66
|
+
inset-inline-start: var(--bps-pos, 0%);
|
|
67
|
+
transform: translateX(-1px);
|
|
68
|
+
transition: inset-inline-start 0.12s linear;
|
|
69
|
+
z-index: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.bps-stop {
|
|
73
|
+
position: absolute;
|
|
74
|
+
inset-block-start: -2px;
|
|
75
|
+
inset-inline-start: var(--bps-pos, 0%);
|
|
76
|
+
display: block;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.bps-tick {
|
|
80
|
+
display: block;
|
|
81
|
+
inline-size: 1px;
|
|
82
|
+
block-size: var(--size-xs, 0.5rem);
|
|
83
|
+
background: var(--color-text-muted, #666);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.bps-stop-label {
|
|
87
|
+
position: absolute;
|
|
88
|
+
inset-block-start: var(--size-s, 0.75rem);
|
|
89
|
+
inset-inline-start: 0;
|
|
90
|
+
transform: translateX(-50%);
|
|
91
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
92
|
+
font-size: var(--font-size-xs, 0.75rem);
|
|
93
|
+
color: var(--color-text-muted, #666);
|
|
94
|
+
text-align: center;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
line-height: 1.2;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.bps-stop[data-current] .bps-stop-label {
|
|
100
|
+
color: var(--color-accent, oklch(45% 0.18 260));
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.bps-stop[data-current] .bps-tick {
|
|
105
|
+
background: var(--color-accent, oklch(45% 0.18 260));
|
|
106
|
+
inline-size: 2px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.bps-list {
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-columns: max-content 1fr;
|
|
112
|
+
gap: var(--size-2xs, 0.25rem) var(--size-m, 1rem);
|
|
113
|
+
margin: 0;
|
|
114
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.bps-row {
|
|
118
|
+
display: contents;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.bps-row dt {
|
|
122
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
color: var(--color-text-muted, #666);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.bps-row dd {
|
|
128
|
+
margin: 0;
|
|
129
|
+
color: var(--color-text-muted, #666);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.bps-row[data-current] dt,
|
|
133
|
+
.bps-row[data-current] dd {
|
|
134
|
+
color: var(--color-accent, oklch(45% 0.18 260));
|
|
135
|
+
font-weight: 600;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Pure-CSS active-row highlight (viewport mode, no JS).
|
|
140
|
+
Mirrors the canonical --bp-* values in src/tokens/breakpoints.css —
|
|
141
|
+
keep these in sync. JS layers a more precise data-current attribute
|
|
142
|
+
on top once it runs. */
|
|
143
|
+
|
|
144
|
+
breakpoint-specimen:not([data-observe]) [data-bp="sm"] { font-style: italic; }
|
|
145
|
+
|
|
146
|
+
@media (width >= 37.5rem) {
|
|
147
|
+
breakpoint-specimen:not([data-observe]) [data-bp="sm"] { font-style: normal; }
|
|
148
|
+
breakpoint-specimen:not([data-observe]) .bps-row[data-bp="sm"] dt,
|
|
149
|
+
breakpoint-specimen:not([data-observe]) .bps-row[data-bp="sm"] dd {
|
|
150
|
+
color: var(--color-text, currentColor);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@media (width >= 48rem) {
|
|
155
|
+
breakpoint-specimen:not([data-observe]) .bps-row[data-bp="md"] dt,
|
|
156
|
+
breakpoint-specimen:not([data-observe]) .bps-row[data-bp="md"] dd {
|
|
157
|
+
color: var(--color-text, currentColor);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (width >= 64rem) {
|
|
162
|
+
breakpoint-specimen:not([data-observe]) .bps-row[data-bp="lg"] dt,
|
|
163
|
+
breakpoint-specimen:not([data-observe]) .bps-row[data-bp="lg"] dd {
|
|
164
|
+
color: var(--color-text, currentColor);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@media (width >= 87.5rem) {
|
|
169
|
+
breakpoint-specimen:not([data-observe]) .bps-row[data-bp="xl"] dt,
|
|
170
|
+
breakpoint-specimen:not([data-observe]) .bps-row[data-bp="xl"] dd {
|
|
171
|
+
color: var(--color-text, currentColor);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@media print {
|
|
176
|
+
breakpoint-specimen .bps-readout,
|
|
177
|
+
breakpoint-specimen .bps-ruler {
|
|
178
|
+
display: none;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "color-palette",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Interactive color swatch display",
|
|
6
|
+
"htmlvalidate": {
|
|
7
|
+
"flow": true,
|
|
8
|
+
"permittedContent": [
|
|
9
|
+
"@flow"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"attributes": [
|
|
13
|
+
{
|
|
14
|
+
"name": "colors",
|
|
15
|
+
"kind": "host-api",
|
|
16
|
+
"purpose": "config",
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Comma-separated color values (hex, rgb, oklch, etc.)"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "names",
|
|
22
|
+
"kind": "host-api",
|
|
23
|
+
"purpose": "config",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Comma-separated swatch labels (optional)"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "layout",
|
|
29
|
+
"kind": "host-api",
|
|
30
|
+
"purpose": "config",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Display mode: \"inline\" (default), \"grid\", \"list\""
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "show-values",
|
|
36
|
+
"kind": "host-api",
|
|
37
|
+
"purpose": "config",
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"description": "Always show color values (otherwise hover-only)"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "show-names",
|
|
43
|
+
"kind": "host-api",
|
|
44
|
+
"purpose": "config",
|
|
45
|
+
"type": "boolean",
|
|
46
|
+
"description": "Show name labels below swatches (auto-enabled if names attr set)"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "size",
|
|
50
|
+
"kind": "host-api",
|
|
51
|
+
"purpose": "config",
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Swatch size: \"sm\", \"md\" (default), \"lg\""
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "editable",
|
|
57
|
+
"kind": "host-api",
|
|
58
|
+
"purpose": "config",
|
|
59
|
+
"type": "boolean",
|
|
60
|
+
"description": "Click a swatch to edit its color via the native OS picker. Updates the colors attribute and fires color-palette:change."
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"events": [
|
|
64
|
+
{
|
|
65
|
+
"name": "color-palette:select"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "color-palette:change"
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"childAttributes": [],
|
|
72
|
+
"structure": []
|
|
73
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* color-palette: Interactive color swatch display
|
|
3
|
+
*
|
|
4
|
+
* Renders a row/grid of color swatches from a comma-separated list.
|
|
5
|
+
* Supports hex, rgb, hsl, and oklch color formats. Click to copy.
|
|
6
|
+
* Names appear below swatches; hex values appear on hover inside.
|
|
7
|
+
* All layout styles are inline so the component works without external CSS.
|
|
8
|
+
*
|
|
9
|
+
* With `editable`, each swatch launches the native OS color picker on click
|
|
10
|
+
* and updates its color on change; `color-palette:change` fires with the
|
|
11
|
+
* updated colors array.
|
|
12
|
+
*
|
|
13
|
+
* @attr {string} colors - Comma-separated color values (hex, rgb, oklch, etc.)
|
|
14
|
+
* @attr {string} names - Comma-separated swatch labels (optional)
|
|
15
|
+
* @attr {string} layout - Display mode: "inline" (default), "grid", "list"
|
|
16
|
+
* @attr {boolean} show-values - Always show color values (otherwise hover-only)
|
|
17
|
+
* @attr {boolean} show-names - Show name labels below swatches (auto-enabled if names attr set)
|
|
18
|
+
* @attr {string} size - Swatch size: "sm", "md" (default), "lg"
|
|
19
|
+
* @attr {boolean} editable - Click a swatch to edit via native color picker
|
|
20
|
+
*
|
|
21
|
+
* @fires color-palette:select - When a swatch is clicked, detail: { color, name, index }
|
|
22
|
+
* @fires color-palette:change - When an editable swatch changes, detail: { color, name, index, colors }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <color-palette colors="#ff6b6b,#4ecdc4,#45b7d1" names="Red,Teal,Sky"></color-palette>
|
|
26
|
+
* <color-palette colors="#6366f1,#ec4899,#22c55e" editable></color-palette>
|
|
27
|
+
*/
|
|
28
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
29
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
30
|
+
import { copyText } from '../../utils/copy-init.js';
|
|
31
|
+
|
|
32
|
+
export class ColorPalette extends VBElement {
|
|
33
|
+
static observedAttributes = ['colors', 'names', 'layout', 'show-values', 'show-names', 'size', 'editable'];
|
|
34
|
+
|
|
35
|
+
/** @type {string[]} */ #colors = [];
|
|
36
|
+
|
|
37
|
+
setup() {
|
|
38
|
+
this.#render();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
attributeChangedCallback() {
|
|
42
|
+
if (this.isConnected) this.#render();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#render() {
|
|
46
|
+
const colorsRaw = this.getAttribute('colors') || '';
|
|
47
|
+
const namesRaw = this.getAttribute('names') || '';
|
|
48
|
+
const layout = this.getAttribute('layout') || 'inline';
|
|
49
|
+
const size = this.getAttribute('size') || 'md';
|
|
50
|
+
const showValues = this.hasAttribute('show-values');
|
|
51
|
+
const showNames = this.hasAttribute('show-names') || namesRaw.length > 0;
|
|
52
|
+
const editable = this.hasAttribute('editable');
|
|
53
|
+
|
|
54
|
+
const colors = this.#parseColorList(colorsRaw);
|
|
55
|
+
this.#colors = colors.slice();
|
|
56
|
+
const names = namesRaw ? namesRaw.split(',').map(n => n.trim()) : [];
|
|
57
|
+
|
|
58
|
+
const sizes = { sm: 48, md: 80, lg: 120 };
|
|
59
|
+
const px = sizes[size] || 80;
|
|
60
|
+
|
|
61
|
+
// Layout styles — inline so no external CSS needed
|
|
62
|
+
let containerStyle = `display:flex;flex-wrap:wrap;gap:var(--size-xs,0.5rem)`;
|
|
63
|
+
if (layout === 'grid') {
|
|
64
|
+
containerStyle = `display:grid;grid-template-columns:repeat(auto-fill,minmax(${px}px,1fr));gap:var(--size-xs,0.5rem)`;
|
|
65
|
+
} else if (layout === 'list') {
|
|
66
|
+
containerStyle = `display:flex;flex-direction:column;gap:var(--size-xs,0.5rem)`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const swatches = colors.map((color, i) => {
|
|
70
|
+
const name = names[i] || '';
|
|
71
|
+
const contrast = this.#contrastColor(color);
|
|
72
|
+
|
|
73
|
+
const wrapStyle = layout === 'list'
|
|
74
|
+
? `display:flex;flex-direction:row;align-items:center;gap:0.75rem`
|
|
75
|
+
: `display:flex;flex-direction:column;align-items:center;gap:0.25rem;max-inline-size:${px}px`;
|
|
76
|
+
|
|
77
|
+
const boxSize = layout === 'list' ? 36 : px;
|
|
78
|
+
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`;
|
|
79
|
+
|
|
80
|
+
// Editable mode: wrap a native color input, styled as the swatch.
|
|
81
|
+
// Clicking it opens the OS color picker directly.
|
|
82
|
+
if (editable) {
|
|
83
|
+
const hexValue = this.#ensureHex(color);
|
|
84
|
+
return `<div class="swatch-wrap" role="listitem" style="${wrapStyle}">
|
|
85
|
+
<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.">
|
|
86
|
+
<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">
|
|
87
|
+
<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>
|
|
88
|
+
</label>
|
|
89
|
+
${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>` : ''}
|
|
90
|
+
</div>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return `<div class="swatch-wrap" role="listitem" style="${wrapStyle}">
|
|
94
|
+
<button type="button" class="color-box" data-index="${i}"
|
|
95
|
+
style="${buttonStyle}"
|
|
96
|
+
title="Click to copy${name ? ': ' + name : ''}"
|
|
97
|
+
aria-label="${name || 'Color ' + (i + 1)}: ${color}">
|
|
98
|
+
<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>
|
|
99
|
+
</button>
|
|
100
|
+
${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>` : ''}
|
|
101
|
+
</div>`;
|
|
102
|
+
}).join('');
|
|
103
|
+
|
|
104
|
+
this.innerHTML = `<div class="palette ${layout}" role="list" aria-label="Color palette" style="${containerStyle}">${swatches}</div>`;
|
|
105
|
+
|
|
106
|
+
if (editable) {
|
|
107
|
+
this.#wireEditable(names);
|
|
108
|
+
} else {
|
|
109
|
+
// Resolve var() references to computed values
|
|
110
|
+
this.#resolveVarColors(colors, names);
|
|
111
|
+
this.#wireReadonly(colors, names, showValues);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#wireReadonly(colors, names, showValues) {
|
|
116
|
+
this.querySelectorAll('.color-box').forEach((el) => {
|
|
117
|
+
const btn = /** @type {HTMLElement} */ (el);
|
|
118
|
+
if (!showValues) {
|
|
119
|
+
btn.addEventListener('pointerenter', () => {
|
|
120
|
+
const val = /** @type {HTMLElement | null} */ (btn.querySelector('.color-value'));
|
|
121
|
+
if (val) val.style.opacity = '1';
|
|
122
|
+
});
|
|
123
|
+
btn.addEventListener('pointerleave', () => {
|
|
124
|
+
const val = /** @type {HTMLElement | null} */ (btn.querySelector('.color-value'));
|
|
125
|
+
if (val) val.style.opacity = '0';
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
btn.addEventListener('click', () => {
|
|
130
|
+
const idx = Number(btn.dataset.index);
|
|
131
|
+
const color = colors[idx];
|
|
132
|
+
const name = names[idx] || '';
|
|
133
|
+
|
|
134
|
+
copyText(color, { button: btn, announceMessage: 'Color copied' });
|
|
135
|
+
this.dispatchEvent(new CustomEvent('color-palette:select', {
|
|
136
|
+
bubbles: true, detail: { color, name, index: idx }
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
btn.style.outline = '3px solid currentColor';
|
|
140
|
+
btn.style.outlineOffset = '2px';
|
|
141
|
+
setTimeout(() => { btn.style.outline = ''; btn.style.outlineOffset = ''; }, 600);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#wireEditable(names) {
|
|
147
|
+
this.querySelectorAll('.color-input').forEach((/** @type {HTMLInputElement} */ input) => {
|
|
148
|
+
input.addEventListener('input', () => {
|
|
149
|
+
const idx = Number(input.dataset.index);
|
|
150
|
+
const hex = input.value;
|
|
151
|
+
this.#colors[idx] = hex;
|
|
152
|
+
|
|
153
|
+
// Live update the swatch without a full re-render
|
|
154
|
+
const label = /** @type {HTMLElement | null} */ (input.parentElement);
|
|
155
|
+
if (label) {
|
|
156
|
+
label.style.background = hex;
|
|
157
|
+
label.style.color = this.#contrastColor(hex);
|
|
158
|
+
const valSpan = label.querySelector('.color-value');
|
|
159
|
+
if (valSpan) valSpan.textContent = this.#formatValue(hex);
|
|
160
|
+
label.title = `Click to edit${names[idx] ? ': ' + names[idx] : ''} (${hex})`;
|
|
161
|
+
label.setAttribute('aria-label', `${names[idx] || 'Color ' + (idx + 1)}: ${hex}. Click to edit.`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Sync the colors attribute so listeners on reconnect see the current state
|
|
165
|
+
this.setAttribute('colors', this.#colors.join(','));
|
|
166
|
+
|
|
167
|
+
this.dispatchEvent(new CustomEvent('color-palette:change', {
|
|
168
|
+
bubbles: true,
|
|
169
|
+
detail: {
|
|
170
|
+
color: hex,
|
|
171
|
+
name: names[idx] || '',
|
|
172
|
+
index: idx,
|
|
173
|
+
colors: this.#colors.slice(),
|
|
174
|
+
},
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Public accessor — used by sibling components that need the current palette. */
|
|
181
|
+
get colors() {
|
|
182
|
+
return this.#colors.slice();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Resolve var() references to computed hex after swatches are in the DOM */
|
|
186
|
+
#resolveVarColors(colors, names) {
|
|
187
|
+
this.querySelectorAll('.color-box').forEach((/** @type {HTMLElement} */ btn) => {
|
|
188
|
+
const idx = Number(btn.dataset.index);
|
|
189
|
+
const raw = colors[idx];
|
|
190
|
+
if (!raw || !raw.includes('var(')) return;
|
|
191
|
+
|
|
192
|
+
const computed = getComputedStyle(btn).backgroundColor;
|
|
193
|
+
const hex = this.#rgbToHex(computed) || computed;
|
|
194
|
+
colors[idx] = hex;
|
|
195
|
+
this.#colors[idx] = hex;
|
|
196
|
+
|
|
197
|
+
const val = btn.querySelector('.color-value');
|
|
198
|
+
if (val) val.textContent = hex;
|
|
199
|
+
|
|
200
|
+
btn.style.color = this.#contrastColor(hex);
|
|
201
|
+
const name = names[idx] || '';
|
|
202
|
+
btn.title = `Click to copy${name ? ': ' + name : ''} (${hex})`;
|
|
203
|
+
btn.setAttribute('aria-label', `${name || 'Color ' + (idx + 1)}: ${hex}`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Convert rgb(r, g, b) to hex string */
|
|
208
|
+
#rgbToHex(rgb) {
|
|
209
|
+
const m = rgb.match(/rgba?\(\s*([\d.]+),?\s*([\d.]+),?\s*([\d.]+)/);
|
|
210
|
+
if (!m) return null;
|
|
211
|
+
const [, r, g, b] = m;
|
|
212
|
+
const toHex = (n) => Math.round(Number(n)).toString(16).padStart(2, '0');
|
|
213
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Ensure a color string is a #rrggbb hex suitable for <input type="color">. */
|
|
217
|
+
#ensureHex(color) {
|
|
218
|
+
if (/^#[0-9a-f]{6}$/i.test(color)) return color.toLowerCase();
|
|
219
|
+
if (/^#[0-9a-f]{3}$/i.test(color)) {
|
|
220
|
+
return ('#' + color.slice(1).split('').map((c) => c + c).join('')).toLowerCase();
|
|
221
|
+
}
|
|
222
|
+
if (typeof document === 'undefined') return '#000000';
|
|
223
|
+
const probe = document.createElement('span');
|
|
224
|
+
probe.style.color = color;
|
|
225
|
+
probe.style.display = 'none';
|
|
226
|
+
document.body.appendChild(probe);
|
|
227
|
+
const computed = getComputedStyle(probe).color;
|
|
228
|
+
probe.remove();
|
|
229
|
+
return this.#rgbToHex(computed) || '#000000';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Shorten oklch values for display */
|
|
233
|
+
#formatValue(color) {
|
|
234
|
+
if (color.startsWith('#')) return color;
|
|
235
|
+
const oklch = color.match(/oklch\(\s*([\d.]+)%?\s+([\d.]+)\s+([\d.]+)/);
|
|
236
|
+
if (oklch) return `${oklch[1]}% .${oklch[2].replace('0.', '')}`;
|
|
237
|
+
return color.length > 12 ? color.slice(0, 12) + '…' : color;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Parse comma-separated color list, handling oklch() which contains commas */
|
|
241
|
+
#parseColorList(raw) {
|
|
242
|
+
if (!raw) return [];
|
|
243
|
+
const colors = [];
|
|
244
|
+
let depth = 0;
|
|
245
|
+
let current = '';
|
|
246
|
+
for (const ch of raw) {
|
|
247
|
+
if (ch === '(') depth++;
|
|
248
|
+
else if (ch === ')') depth--;
|
|
249
|
+
if (ch === ',' && depth === 0) {
|
|
250
|
+
colors.push(current.trim());
|
|
251
|
+
current = '';
|
|
252
|
+
} else {
|
|
253
|
+
current += ch;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (current.trim()) colors.push(current.trim());
|
|
257
|
+
return colors;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Return black or white depending on perceived lightness */
|
|
261
|
+
#contrastColor(color) {
|
|
262
|
+
const oklchMatch = color.match(/oklch\(\s*([\d.]+)%?\s/);
|
|
263
|
+
if (oklchMatch) {
|
|
264
|
+
const L = parseFloat(oklchMatch[1]);
|
|
265
|
+
const lightness = L > 1 ? L / 100 : L;
|
|
266
|
+
return lightness > 0.6 ? '#000' : '#fff';
|
|
267
|
+
}
|
|
268
|
+
if (color.startsWith('#')) {
|
|
269
|
+
const hex = color.replace('#', '');
|
|
270
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
271
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
272
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
273
|
+
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
274
|
+
return lum > 0.4 ? '#000' : '#fff';
|
|
275
|
+
}
|
|
276
|
+
return '#000';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
registerComponent('color-palette', ColorPalette);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Color Palette — Static Fallback</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<!--
|
|
10
|
+
Static fallback for <color-palette>.
|
|
11
|
+
|
|
12
|
+
Upgrade relationship:
|
|
13
|
+
- Without JS: Color values are presented as a simple list of text items.
|
|
14
|
+
Users can read and manually copy color codes. Names and values are
|
|
15
|
+
visible as plain text alongside visual swatch indicators using inline
|
|
16
|
+
background colors on small spans.
|
|
17
|
+
- With JS: <color-palette> parses the `colors` and `names` attributes,
|
|
18
|
+
renders interactive swatch buttons with proper contrast text, supports
|
|
19
|
+
click-to-copy via the Clipboard API, fires `color-palette:select`
|
|
20
|
+
events, and provides visual flash feedback on selection. Supports
|
|
21
|
+
inline, grid, and list layouts.
|
|
22
|
+
|
|
23
|
+
The static form preserves the essential information — the color values
|
|
24
|
+
themselves — which is the primary content of the component.
|
|
25
|
+
-->
|
|
26
|
+
|
|
27
|
+
<main>
|
|
28
|
+
<h1>Color Palette</h1>
|
|
29
|
+
|
|
30
|
+
<ul aria-label="Color palette">
|
|
31
|
+
<li>
|
|
32
|
+
<span style="display:inline-block;width:1em;height:1em;background:#ff6b6b;vertical-align:middle;border:1px solid #ccc" aria-hidden="true"></span>
|
|
33
|
+
Red: <code>#ff6b6b</code>
|
|
34
|
+
</li>
|
|
35
|
+
<li>
|
|
36
|
+
<span style="display:inline-block;width:1em;height:1em;background:#4ecdc4;vertical-align:middle;border:1px solid #ccc" aria-hidden="true"></span>
|
|
37
|
+
Teal: <code>#4ecdc4</code>
|
|
38
|
+
</li>
|
|
39
|
+
<li>
|
|
40
|
+
<span style="display:inline-block;width:1em;height:1em;background:#45b7d1;vertical-align:middle;border:1px solid #ccc" aria-hidden="true"></span>
|
|
41
|
+
Sky: <code>#45b7d1</code>
|
|
42
|
+
</li>
|
|
43
|
+
<li>
|
|
44
|
+
<span style="display:inline-block;width:1em;height:1em;background:#96ceb4;vertical-align:middle;border:1px solid #ccc" aria-hidden="true"></span>
|
|
45
|
+
Sage: <code>#96ceb4</code>
|
|
46
|
+
</li>
|
|
47
|
+
<li>
|
|
48
|
+
<span style="display:inline-block;width:1em;height:1em;background:#ffeaa7;vertical-align:middle;border:1px solid #ccc" aria-hidden="true"></span>
|
|
49
|
+
Butter: <code>#ffeaa7</code>
|
|
50
|
+
</li>
|
|
51
|
+
</ul>
|
|
52
|
+
</main>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|