@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,156 @@
|
|
|
1
|
+
/* --------------------------------------------------------------------------
|
|
2
|
+
accessibility-specimen: WCAG contrast-ratio table + a11y checklist
|
|
3
|
+
for design-system docs.
|
|
4
|
+
-------------------------------------------------------------------------- */
|
|
5
|
+
|
|
6
|
+
accessibility-specimen {
|
|
7
|
+
display: block;
|
|
8
|
+
font: inherit;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* -------- Contrast table -------- */
|
|
12
|
+
|
|
13
|
+
accessibility-specimen .as-contrast-table {
|
|
14
|
+
inline-size: 100%;
|
|
15
|
+
border-collapse: collapse;
|
|
16
|
+
border: var(--border-width-thin, 1px) solid var(--color-border, #ddd);
|
|
17
|
+
border-radius: var(--radius-m, 0.5rem);
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
font-size: 0.95em;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
accessibility-specimen .as-contrast-table th,
|
|
23
|
+
accessibility-specimen .as-contrast-table td {
|
|
24
|
+
padding-block: var(--size-2xs, 0.375rem);
|
|
25
|
+
padding-inline: var(--size-s, 0.5rem);
|
|
26
|
+
text-align: start;
|
|
27
|
+
border-block-end: var(--border-width-thin, 1px) solid var(--color-border, #ddd);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
accessibility-specimen .as-contrast-table tr:last-child th,
|
|
31
|
+
accessibility-specimen .as-contrast-table tr:last-child td {
|
|
32
|
+
border-block-end: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
accessibility-specimen .as-contrast-table thead {
|
|
36
|
+
background: var(--color-surface-raised, var(--color-surface, #fff));
|
|
37
|
+
font-size: 0.85em;
|
|
38
|
+
text-transform: uppercase;
|
|
39
|
+
letter-spacing: 0.04em;
|
|
40
|
+
color: var(--color-text-muted, currentColor);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
accessibility-specimen .as-pair-label {
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: var(--size-2xs, 0.375rem);
|
|
48
|
+
font-variant-numeric: tabular-nums;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
accessibility-specimen .as-swatch {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
inline-size: 1.25rem;
|
|
54
|
+
block-size: 1.25rem;
|
|
55
|
+
border-radius: var(--radius-s, 0.25rem);
|
|
56
|
+
border: var(--border-width-thin, 1px) solid currentColor;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
accessibility-specimen .as-pair-sample {
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
font-size: 1em;
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
text-align: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
accessibility-specimen .as-pair-ratio {
|
|
68
|
+
font-variant-numeric: tabular-nums;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
accessibility-specimen .as-badge {
|
|
73
|
+
text-align: center;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
font-size: 1.1em;
|
|
76
|
+
inline-size: 3em;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
accessibility-specimen .as-pass {
|
|
80
|
+
color: var(--color-success, #2e7d32);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
accessibility-specimen .as-fail {
|
|
84
|
+
color: var(--color-error, #c62828);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
accessibility-specimen .as-empty {
|
|
88
|
+
text-align: center;
|
|
89
|
+
font-style: italic;
|
|
90
|
+
color: var(--color-text-muted, currentColor);
|
|
91
|
+
padding-block: var(--size-l, 1.5rem) !important;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* -------- Checklist -------- */
|
|
95
|
+
|
|
96
|
+
accessibility-specimen .as-checklist {
|
|
97
|
+
list-style: none;
|
|
98
|
+
margin: 0;
|
|
99
|
+
padding: 0;
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: var(--size-2xs, 0.375rem);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
accessibility-specimen .as-checklist > li {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: flex-start;
|
|
108
|
+
gap: var(--size-xs, 0.5rem);
|
|
109
|
+
padding-block: var(--size-2xs, 0.375rem);
|
|
110
|
+
padding-inline: var(--size-s, 0.5rem);
|
|
111
|
+
border: var(--border-width-thin, 1px) solid var(--color-border, #ddd);
|
|
112
|
+
border-radius: var(--radius-s, 0.25rem);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
accessibility-specimen .as-checklist-icon {
|
|
116
|
+
flex-shrink: 0;
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
inline-size: 1.5rem;
|
|
121
|
+
block-size: 1.5rem;
|
|
122
|
+
border-radius: var(--radius-full, 999px);
|
|
123
|
+
font-weight: 700;
|
|
124
|
+
line-height: 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
accessibility-specimen .as-status-pass { border-color: var(--color-success, #2e7d32); }
|
|
128
|
+
accessibility-specimen .as-status-pass .as-checklist-icon {
|
|
129
|
+
background: var(--color-success, #2e7d32); color: var(--color-text-on-success, white);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
accessibility-specimen .as-status-fail { border-color: var(--color-error, #c62828); }
|
|
133
|
+
accessibility-specimen .as-status-fail .as-checklist-icon {
|
|
134
|
+
background: var(--color-error, #c62828); color: var(--color-text-on-error, white);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
accessibility-specimen .as-status-warn { border-color: var(--color-warning, #b8860b); }
|
|
138
|
+
accessibility-specimen .as-status-warn .as-checklist-icon {
|
|
139
|
+
background: var(--color-warning, #b8860b); color: var(--color-text-on-warning, white);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
accessibility-specimen .as-status-na .as-checklist-icon {
|
|
143
|
+
background: var(--color-text-muted, oklch(60% 0 0)); color: var(--color-surface, white);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
accessibility-specimen .as-sr-only {
|
|
147
|
+
position: absolute;
|
|
148
|
+
inline-size: 1px;
|
|
149
|
+
block-size: 1px;
|
|
150
|
+
padding: 0;
|
|
151
|
+
margin: -1px;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
clip: rect(0 0 0 0);
|
|
154
|
+
white-space: nowrap;
|
|
155
|
+
border: 0;
|
|
156
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "brand-specimen",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Brand mark specimen: shows a <brand-mark> on light/dark surfaces, a size scale, and a clear-space box",
|
|
6
|
+
"htmlvalidate": {
|
|
7
|
+
"flow": true,
|
|
8
|
+
"permittedContent": [
|
|
9
|
+
"@flow"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"attributes": [
|
|
13
|
+
{
|
|
14
|
+
"name": "src",
|
|
15
|
+
"kind": "host-api",
|
|
16
|
+
"purpose": "config",
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Logo image URL, passed through to each <brand-mark src>"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "name",
|
|
22
|
+
"kind": "host-api",
|
|
23
|
+
"purpose": "config",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Brand name, passed through to each <brand-mark wordmark> (default: \"Brand\")"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "data-sizes",
|
|
29
|
+
"kind": "data",
|
|
30
|
+
"purpose": "config",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Space-separated <brand-mark data-size> values for the scale row (default: \"s m l xl\")"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"childAttributes": [],
|
|
36
|
+
"structure": []
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brand-specimen: DS specimen documenting a brand mark
|
|
3
|
+
*
|
|
4
|
+
* Shows a `<brand-mark>` on light and dark surfaces, at a size scale, and
|
|
5
|
+
* inside a clear-space box. Pairs with the other identity/token specimens
|
|
6
|
+
* (color-palette, semantic-palette) for brand-guideline pages.
|
|
7
|
+
*
|
|
8
|
+
* `<brand-mark>` itself is a VB-core component (runtime peer) — this
|
|
9
|
+
* specimen only arranges instances of it; it does not re-implement its
|
|
10
|
+
* theme-aware light/dark logo swap.
|
|
11
|
+
*
|
|
12
|
+
* @attr {string} src - Logo image URL, passed through to `<brand-mark src>`
|
|
13
|
+
* @attr {string} name - Brand name, passed through as `<brand-mark>` child text content
|
|
14
|
+
* @attr {string} data-sizes - Space-separated `<brand-mark data-size>` values
|
|
15
|
+
* for the scale row (default: "s m l xl")
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <brand-specimen src="/logo.svg" name="Acme"></brand-specimen>
|
|
19
|
+
* <brand-specimen src="/logo.svg" name="Acme" data-sizes="xs s m l xl 2xl"></brand-specimen>
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
23
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
24
|
+
|
|
25
|
+
class BrandSpecimen extends VBElement {
|
|
26
|
+
static observedAttributes = ['src', 'name', 'data-sizes'];
|
|
27
|
+
|
|
28
|
+
setup() {
|
|
29
|
+
this.#render();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
attributeChangedCallback() {
|
|
33
|
+
if (this.isConnected) this.#render();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#render() {
|
|
37
|
+
const src = this.getAttribute('src') || '';
|
|
38
|
+
const name = this.getAttribute('name') || 'Brand';
|
|
39
|
+
const sizes = (this.getAttribute('data-sizes') || 's m l xl').trim().split(/\s+/);
|
|
40
|
+
// `<brand-mark>` treats `wordmark` as a boolean presence attribute and
|
|
41
|
+
// reads the displayed brand name from its light-DOM text content — the
|
|
42
|
+
// name must be the element's child text, not the value of `wordmark=`.
|
|
43
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
44
|
+
const mark = (size) =>
|
|
45
|
+
`<brand-mark${src ? ` src="${esc(src)}"` : ''} wordmark${size ? ` data-size="${size}"` : ''}>${esc(name)}</brand-mark>`;
|
|
46
|
+
this.innerHTML = `
|
|
47
|
+
<section class="brand-specimen">
|
|
48
|
+
<div class="bs-surfaces">
|
|
49
|
+
<figure data-surface="light">${mark('l')}<figcaption>On light</figcaption></figure>
|
|
50
|
+
<figure data-surface="dark">${mark('l')}<figcaption>On dark</figcaption></figure>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="bs-scale" data-scale>
|
|
53
|
+
${sizes.map(s => `<span>${mark(s)}<small>${s}</small></span>`).join('')}
|
|
54
|
+
</div>
|
|
55
|
+
<div class="bs-clearspace"><span class="bs-clear-box">${mark('l')}</span>
|
|
56
|
+
<figcaption>Clear space</figcaption></div>
|
|
57
|
+
</section>`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
registerComponent('brand-specimen', BrandSpecimen);
|
|
@@ -0,0 +1,77 @@
|
|
|
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>Brand Specimen (Static Fallback)</title>
|
|
7
|
+
<style>
|
|
8
|
+
/*
|
|
9
|
+
* Static fallback for <brand-specimen>.
|
|
10
|
+
*
|
|
11
|
+
* Upgrade relationship:
|
|
12
|
+
* When JavaScript loads and the <brand-specimen> custom element is
|
|
13
|
+
* defined, it renders a <brand-mark> on light and dark surface
|
|
14
|
+
* panels, a size scale, and a clear-space box.
|
|
15
|
+
*
|
|
16
|
+
* Without JS, this static page presents the same information as
|
|
17
|
+
* plain semantic HTML: a labeled logo image and a list of the
|
|
18
|
+
* size-scale names, providing equivalent content through markup
|
|
19
|
+
* alone.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
font-family: system-ui, sans-serif;
|
|
26
|
+
line-height: 1.6;
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: 2rem;
|
|
29
|
+
color: #1a1a1a;
|
|
30
|
+
background: #f8f8f8;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
h1 { margin-block-end: 1rem; }
|
|
34
|
+
|
|
35
|
+
.surfaces {
|
|
36
|
+
display: grid;
|
|
37
|
+
grid-template-columns: 1fr 1fr;
|
|
38
|
+
gap: 1rem;
|
|
39
|
+
max-width: 30rem;
|
|
40
|
+
margin-block-end: 1.5rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
figure {
|
|
44
|
+
margin: 0;
|
|
45
|
+
padding: 1.5rem;
|
|
46
|
+
border-radius: 0.5rem;
|
|
47
|
+
text-align: center;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
figure img { max-height: 3rem; }
|
|
51
|
+
|
|
52
|
+
.surfaces figure:first-child { background: #fff; color: #111; }
|
|
53
|
+
.surfaces figure:last-child { background: #111; color: #fff; }
|
|
54
|
+
|
|
55
|
+
figcaption { font-size: 0.875rem; margin-block-start: 0.5rem; }
|
|
56
|
+
|
|
57
|
+
ul { font-family: ui-monospace, monospace; font-size: 0.875rem; color: #666; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<h1>Brand Mark</h1>
|
|
62
|
+
<p>These panels show the brand mark on light and dark surfaces. With JavaScript enabled, this becomes an interactive specimen with a size scale and clear-space guide.</p>
|
|
63
|
+
|
|
64
|
+
<div class="surfaces">
|
|
65
|
+
<figure><img src="" alt="" width="0" height="0"><figcaption>On light</figcaption></figure>
|
|
66
|
+
<figure><img src="" alt="" width="0" height="0"><figcaption>On dark</figcaption></figure>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<p>Size scale:</p>
|
|
70
|
+
<ul>
|
|
71
|
+
<li>s</li>
|
|
72
|
+
<li>m</li>
|
|
73
|
+
<li>l</li>
|
|
74
|
+
<li>xl</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
.brand-specimen { display: grid; gap: var(--size-l, 1.5rem); }
|
|
2
|
+
.bs-surfaces { display: grid; grid-template-columns: 1fr 1fr; gap: var(--size-m, 1rem); }
|
|
3
|
+
.bs-surfaces figure { margin: 0; padding: var(--size-l, 1.5rem); border-radius: var(--radius-m, 0.5rem);
|
|
4
|
+
display: grid; place-items: center; gap: var(--size-s, 0.75rem); }
|
|
5
|
+
.bs-surfaces [data-surface="light"] { background: #fff; color: #111; }
|
|
6
|
+
.bs-surfaces [data-surface="dark"] { background: #111; color: #fff; }
|
|
7
|
+
.bs-scale { display: flex; align-items: end; gap: var(--size-l, 1.5rem); flex-wrap: wrap; }
|
|
8
|
+
.bs-scale span { display: grid; justify-items: center; gap: var(--size-2xs, 0.25rem); }
|
|
9
|
+
.bs-clear-box { display: inline-block; padding: 1em; outline: 1px dashed var(--color-border, #d4d4d8);
|
|
10
|
+
outline-offset: 0.25em; }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "breakpoint-specimen",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Responsive breakpoint visualization with live width readout",
|
|
6
|
+
"htmlvalidate": {
|
|
7
|
+
"flow": true,
|
|
8
|
+
"permittedContent": [
|
|
9
|
+
"@flow"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"attributes": [
|
|
13
|
+
{
|
|
14
|
+
"name": "tokens",
|
|
15
|
+
"kind": "host-api",
|
|
16
|
+
"purpose": "config",
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Comma-separated breakpoint names (default: \"sm,md,lg,xl\")"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "prefix",
|
|
22
|
+
"kind": "host-api",
|
|
23
|
+
"purpose": "config",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "CSS variable prefix (default: \"--bp-\")"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "label",
|
|
29
|
+
"kind": "host-api",
|
|
30
|
+
"purpose": "config",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Optional heading label"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "data-observe",
|
|
36
|
+
"kind": "data",
|
|
37
|
+
"purpose": "config",
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "CSS selector of a container to track instead of the viewport. Container-query mode (JS-only)."
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"childAttributes": [],
|
|
43
|
+
"structure": []
|
|
44
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* breakpoint-specimen: Responsive breakpoint visualization
|
|
3
|
+
*
|
|
4
|
+
* Joins the specimen family (spacing, type, motion, token). Renders a
|
|
5
|
+
* ruler of viewport breakpoints with a live cursor at the current
|
|
6
|
+
* width and the active breakpoint label called out.
|
|
7
|
+
*
|
|
8
|
+
* Reads --bp-* tokens from :root by default; can be given an explicit
|
|
9
|
+
* tokens list. Tracks the viewport (window) by default; data-observe
|
|
10
|
+
* accepts a CSS selector to track an arbitrary container instead.
|
|
11
|
+
*
|
|
12
|
+
* Progressive enhancement:
|
|
13
|
+
* 1. HTML — a semantic <dl> of breakpoint name → value remains
|
|
14
|
+
* readable without JS or CSS.
|
|
15
|
+
* 2. CSS — pure-CSS active-row highlighting via @media rules pulls
|
|
16
|
+
* the matching token into a callout, no JS required for the
|
|
17
|
+
* "which breakpoint is active" cue (viewport mode only).
|
|
18
|
+
* 3. JS — adds the live numeric width readout and the moving
|
|
19
|
+
* cursor on the ruler, plus container-query mode (data-observe).
|
|
20
|
+
*
|
|
21
|
+
* @attr {string} tokens - Comma-separated breakpoint names
|
|
22
|
+
* (default: "sm,md,lg,xl")
|
|
23
|
+
* @attr {string} prefix - CSS variable prefix (default: "--bp-")
|
|
24
|
+
* @attr {string} label - Optional heading label
|
|
25
|
+
* @attr {string} data-observe - CSS selector of a container to track
|
|
26
|
+
* instead of the viewport. JS-only.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <breakpoint-specimen></breakpoint-specimen>
|
|
30
|
+
* <breakpoint-specimen tokens="sm,md,lg" label="Core breakpoints"></breakpoint-specimen>
|
|
31
|
+
* <breakpoint-specimen data-observe="#preview-frame"></breakpoint-specimen>
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
35
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
36
|
+
|
|
37
|
+
class BreakpointSpecimen extends VBElement {
|
|
38
|
+
static observedAttributes = ['tokens', 'prefix', 'label', 'data-observe'];
|
|
39
|
+
|
|
40
|
+
setup() {
|
|
41
|
+
this.#render();
|
|
42
|
+
this.#start();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
attributeChangedCallback() {
|
|
46
|
+
if (this.isConnected) {
|
|
47
|
+
this.#stop();
|
|
48
|
+
this.#render();
|
|
49
|
+
this.#start();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
teardown() {
|
|
54
|
+
this.#stop();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#render() {
|
|
58
|
+
const tokensAttr = this.getAttribute('tokens') || 'sm,md,lg,xl';
|
|
59
|
+
const prefix = this.getAttribute('prefix') || '--bp-';
|
|
60
|
+
const label = this.getAttribute('label') || '';
|
|
61
|
+
const names = tokensAttr.split(',').map((t) => t.trim()).filter(Boolean);
|
|
62
|
+
|
|
63
|
+
const cs = getComputedStyle(document.documentElement);
|
|
64
|
+
const stops = names
|
|
65
|
+
.map((name) => {
|
|
66
|
+
const raw = cs.getPropertyValue(`${prefix}${name}`).trim();
|
|
67
|
+
const px = this.#toPx(raw);
|
|
68
|
+
return { name, raw, px };
|
|
69
|
+
})
|
|
70
|
+
.filter((s) => s.px > 0)
|
|
71
|
+
.sort((a, b) => a.px - b.px);
|
|
72
|
+
|
|
73
|
+
this.#stops = stops;
|
|
74
|
+
this.#max = stops.length ? stops[stops.length - 1].px * 1.15 : 1;
|
|
75
|
+
|
|
76
|
+
const labelHTML = label ? `<header class="bps-label">${label}</header>` : '';
|
|
77
|
+
|
|
78
|
+
const readoutHTML = `
|
|
79
|
+
<output class="bps-readout" aria-live="polite">
|
|
80
|
+
<span class="bps-width">—</span>
|
|
81
|
+
<span class="bps-active" data-active="">—</span>
|
|
82
|
+
</output>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const rulerHTML = `
|
|
86
|
+
<figure class="bps-ruler" aria-hidden="true">
|
|
87
|
+
<span class="bps-cursor" data-cursor></span>
|
|
88
|
+
${stops.map((s) => `
|
|
89
|
+
<span class="bps-stop" style="--bps-pos:${(s.px / this.#max) * 100}%" data-bp="${s.name}">
|
|
90
|
+
<span class="bps-tick"></span>
|
|
91
|
+
<span class="bps-stop-label">${s.name}<br><small>${Math.round(s.px)}px</small></span>
|
|
92
|
+
</span>
|
|
93
|
+
`).join('')}
|
|
94
|
+
</figure>
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
const tableHTML = `
|
|
98
|
+
<dl class="bps-list">
|
|
99
|
+
${stops.map((s) => `
|
|
100
|
+
<div class="bps-row" data-bp="${s.name}">
|
|
101
|
+
<dt>${s.name}</dt>
|
|
102
|
+
<dd><code>${s.raw}</code> — ${Math.round(s.px)}px and up</dd>
|
|
103
|
+
</div>
|
|
104
|
+
`).join('')}
|
|
105
|
+
</dl>
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
this.innerHTML = `${labelHTML}${readoutHTML}${rulerHTML}${tableHTML}`;
|
|
109
|
+
this.#widthEl = /** @type {HTMLElement | null} */ (this.querySelector('.bps-width'));
|
|
110
|
+
this.#activeEl = /** @type {HTMLElement | null} */ (this.querySelector('.bps-active'));
|
|
111
|
+
this.#cursorEl = /** @type {HTMLElement | null} */ (this.querySelector('[data-cursor]'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#start() {
|
|
115
|
+
const sel = this.getAttribute('data-observe');
|
|
116
|
+
if (sel) {
|
|
117
|
+
const target = document.querySelector(sel);
|
|
118
|
+
if (!target) return;
|
|
119
|
+
this.#ro = new ResizeObserver(() => this.#update(target.clientWidth));
|
|
120
|
+
this.#ro.observe(target);
|
|
121
|
+
this.#update(target.clientWidth);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const update = () => this.#update(window.innerWidth);
|
|
125
|
+
update();
|
|
126
|
+
this.listen(window, 'resize', update, { passive: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#stop() {
|
|
130
|
+
if (this.#ro) {
|
|
131
|
+
this.#ro.disconnect();
|
|
132
|
+
this.#ro = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#update(width) {
|
|
137
|
+
if (!this.#widthEl || !this.#activeEl) return;
|
|
138
|
+
this.#widthEl.textContent = `${Math.round(width)}px`;
|
|
139
|
+
const active = this.#activeFor(width);
|
|
140
|
+
this.#activeEl.textContent = active ? active.name : 'base';
|
|
141
|
+
this.#activeEl.dataset.active = active ? active.name : '';
|
|
142
|
+
if (this.#cursorEl) {
|
|
143
|
+
const pct = Math.min(100, (width / this.#max) * 100);
|
|
144
|
+
this.#cursorEl.style.setProperty('--bps-pos', `${pct}%`);
|
|
145
|
+
}
|
|
146
|
+
this.querySelectorAll('[data-bp]').forEach((el) => {
|
|
147
|
+
const matches = active && el.getAttribute('data-bp') === active.name;
|
|
148
|
+
el.toggleAttribute('data-current', !!matches);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#activeFor(width) {
|
|
153
|
+
let match = null;
|
|
154
|
+
for (const stop of this.#stops) {
|
|
155
|
+
if (width >= stop.px) match = stop;
|
|
156
|
+
}
|
|
157
|
+
return match;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#toPx(value) {
|
|
161
|
+
if (!value) return 0;
|
|
162
|
+
const n = parseFloat(value);
|
|
163
|
+
if (Number.isNaN(n)) return 0;
|
|
164
|
+
if (value.endsWith('rem') || value.endsWith('em')) {
|
|
165
|
+
const root = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
166
|
+
return n * root;
|
|
167
|
+
}
|
|
168
|
+
return n;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @type {Array<{name: string, raw: string, px: number}>} */
|
|
172
|
+
#stops = [];
|
|
173
|
+
#max = 1;
|
|
174
|
+
/** @type {ResizeObserver | null} */
|
|
175
|
+
#ro = null;
|
|
176
|
+
/** @type {HTMLElement | null} */
|
|
177
|
+
#widthEl = null;
|
|
178
|
+
/** @type {HTMLElement | null} */
|
|
179
|
+
#activeEl = null;
|
|
180
|
+
/** @type {HTMLElement | null} */
|
|
181
|
+
#cursorEl = null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
registerComponent('breakpoint-specimen', BreakpointSpecimen);
|
|
185
|
+
|
|
186
|
+
export { BreakpointSpecimen };
|
|
@@ -0,0 +1,38 @@
|
|
|
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>Breakpoint Specimen — Static Fallback</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: system-ui, sans-serif; padding: 2rem; }
|
|
9
|
+
dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; max-width: 30rem; }
|
|
10
|
+
dt { font-family: ui-monospace, monospace; font-weight: 600; }
|
|
11
|
+
dd { margin: 0; color: #666; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<!--
|
|
16
|
+
Static fallback for <breakpoint-specimen>.
|
|
17
|
+
|
|
18
|
+
Upgrade relationship:
|
|
19
|
+
- Without JS, modern browsers: the @media-driven highlight in the
|
|
20
|
+
component's stylesheet calls out the active row, so even no-JS
|
|
21
|
+
readers see which breakpoint is in effect.
|
|
22
|
+
- Without JS or CSS: the <dl> below remains a readable list of
|
|
23
|
+
breakpoint names and their token values.
|
|
24
|
+
- With JS: the component adds a live numeric width readout and a
|
|
25
|
+
moving cursor on a visual ruler, plus container-query tracking
|
|
26
|
+
via data-observe.
|
|
27
|
+
-->
|
|
28
|
+
|
|
29
|
+
<breakpoint-specimen>
|
|
30
|
+
<dl>
|
|
31
|
+
<dt>sm</dt><dd>37.5rem (600px) and up — compact mobile</dd>
|
|
32
|
+
<dt>md</dt><dd>48rem (768px) and up — tablet / mobile split</dd>
|
|
33
|
+
<dt>lg</dt><dd>64rem (1024px) and up — desktop layouts</dd>
|
|
34
|
+
<dt>xl</dt><dd>87.5rem (1400px) and up — ultra-wide enhancements</dd>
|
|
35
|
+
</dl>
|
|
36
|
+
</breakpoint-specimen>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|