layout-style-css 1.1.2 → 2.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/CHANGELOG.md +81 -44
- package/CONTRIBUTING.md +55 -50
- package/LICENSE +21 -21
- package/README.md +236 -636
- package/SECURITY.md +50 -45
- package/demo/assets/apple-touch-icon.svg +7 -7
- package/demo/assets/favicon.svg +7 -7
- package/demo/browserconfig.xml +9 -9
- package/demo/demo.css +523 -0
- package/demo/demo.js +605 -0
- package/demo/index.html +325 -976
- package/demo/robots.txt +4 -4
- package/demo/site.webmanifest +23 -23
- package/demo/sitemap.xml +9 -9
- package/dist/core.css +6 -0
- package/{styles/layout-ui-style-kit-bridge.css → dist/integrations/ui-style-kit.css} +175 -170
- package/dist/layout-style-css.css +1473 -1903
- package/dist/layout-style-css.min.css +1 -1
- package/dist/legacy.css +787 -0
- package/dist/personalities/bauhaus.css +46 -0
- package/dist/personalities/bento.css +43 -0
- package/dist/personalities/brutalism.css +36 -0
- package/dist/personalities/cyberpunk.css +36 -0
- package/dist/personalities/f-pattern.css +48 -0
- package/dist/personalities/maximalist.css +51 -0
- package/dist/personalities/minimal-saas.css +37 -0
- package/dist/personalities/mondrian.css +44 -0
- package/dist/personalities/neumorphism.css +46 -0
- package/dist/personalities/retro-glass.css +44 -0
- package/dist/personalities/retrofuturism.css +36 -0
- package/dist/personalities/split-screen.css +41 -0
- package/dist/personalities/synthwave.css +48 -0
- package/dist/personalities/tactile.css +39 -0
- package/dist/personalities/y2k.css +36 -0
- package/dist/personalities/z-pattern.css +49 -0
- package/dist/personalities.css +18 -0
- package/dist/primitives.css +219 -0
- package/dist/recipes.css +270 -0
- package/dist/utilities.css +162 -0
- package/dist/wrappers.css +174 -0
- package/docs/wiki/Contributing.md +36 -30
- package/docs/wiki/Demo-And-GitHub-Pages.md +53 -39
- package/docs/wiki/Getting-Started.md +51 -68
- package/docs/wiki/Home.md +33 -48
- package/docs/wiki/Installation-And-CDN.md +72 -58
- package/docs/wiki/Layout-Primitives.md +48 -57
- package/docs/wiki/Layout-Recipes.md +49 -80
- package/docs/wiki/Layout-Styles.md +33 -43
- package/docs/wiki/Migrating-To-2.0.md +186 -0
- package/docs/wiki/Release-And-Publishing.md +77 -69
- package/docs/wiki/Security-And-Support.md +36 -34
- package/docs/wiki/UI-Style-Kit-Compatibility.md +49 -54
- package/docs/wiki/_Sidebar.md +15 -14
- package/package.json +98 -107
- package/styles/core.css +6 -0
- package/{dist/layout-ui-style-kit-bridge.css → styles/integrations/ui-style-kit.css} +175 -170
- package/styles/legacy.css +173 -0
- package/styles/personalities/bauhaus.css +46 -0
- package/styles/personalities/bento.css +43 -0
- package/styles/personalities/brutalism.css +36 -0
- package/styles/personalities/cyberpunk.css +36 -0
- package/styles/personalities/f-pattern.css +48 -0
- package/styles/personalities/maximalist.css +51 -0
- package/styles/personalities/minimal-saas.css +37 -0
- package/styles/personalities/mondrian.css +44 -0
- package/styles/personalities/neumorphism.css +46 -0
- package/styles/personalities/retro-glass.css +44 -0
- package/styles/personalities/retrofuturism.css +36 -0
- package/styles/personalities/split-screen.css +41 -0
- package/styles/personalities/synthwave.css +48 -0
- package/styles/personalities/tactile.css +39 -0
- package/styles/personalities/y2k.css +36 -0
- package/styles/personalities/z-pattern.css +49 -0
- package/styles/personalities.css +18 -0
- package/styles/primitives.css +219 -0
- package/styles/recipes.css +270 -0
- package/styles/utilities.css +162 -0
- package/styles/wrappers.css +174 -0
- package/dist/layout-all-with-ui-kit-and-interactive-surface.css +0 -3
- package/dist/layout-all-with-ui-kit.css +0 -2
- package/dist/layout-all.css +0 -18
- package/dist/layout-base.css +0 -539
- package/dist/layout-style-bauhaus.css +0 -70
- package/dist/layout-style-bento.css +0 -76
- package/dist/layout-style-brutalism.css +0 -70
- package/dist/layout-style-cyberpunk.css +0 -70
- package/dist/layout-style-f-pattern.css +0 -76
- package/dist/layout-style-maximalist.css +0 -93
- package/dist/layout-style-minimal-saas.css +0 -67
- package/dist/layout-style-mondrian.css +0 -77
- package/dist/layout-style-neumorphism.css +0 -72
- package/dist/layout-style-retro-glass.css +0 -82
- package/dist/layout-style-retrofuturism.css +0 -71
- package/dist/layout-style-split-screen.css +0 -62
- package/dist/layout-style-synthwave.css +0 -88
- package/dist/layout-style-tactile.css +0 -70
- package/dist/layout-style-y2k.css +0 -72
- package/dist/layout-style-z-pattern.css +0 -76
- package/styles/layout-base.css +0 -539
- package/styles/layout-style-bauhaus.css +0 -70
- package/styles/layout-style-bento.css +0 -76
- package/styles/layout-style-brutalism.css +0 -70
- package/styles/layout-style-cyberpunk.css +0 -70
- package/styles/layout-style-f-pattern.css +0 -76
- package/styles/layout-style-maximalist.css +0 -93
- package/styles/layout-style-minimal-saas.css +0 -67
- package/styles/layout-style-mondrian.css +0 -77
- package/styles/layout-style-neumorphism.css +0 -72
- package/styles/layout-style-retro-glass.css +0 -82
- package/styles/layout-style-retrofuturism.css +0 -71
- package/styles/layout-style-split-screen.css +0 -62
- package/styles/layout-style-synthwave.css +0 -88
- package/styles/layout-style-tactile.css +0 -70
- package/styles/layout-style-y2k.css +0 -72
- package/styles/layout-style-z-pattern.css +0 -76
package/demo/demo.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
const UI_STYLE_KIT_VERSION = "2.1.0";
|
|
2
|
+
const UI_STYLE_KIT_MANIFEST_URL = `https://unpkg.com/ui-style-kit-css@${UI_STYLE_KIT_VERSION}/manifest.json`;
|
|
3
|
+
const UI_STYLE_KIT_MANIFEST_FALLBACK = Object.freeze({
|
|
4
|
+
version: UI_STYLE_KIT_VERSION,
|
|
5
|
+
presets: Object.freeze([
|
|
6
|
+
Object.freeze({ id: "minimal-saas", label: "Minimal SaaS", prefix: "saas" }),
|
|
7
|
+
Object.freeze({ id: "bento", label: "Bento", prefix: "bento" }),
|
|
8
|
+
Object.freeze({ id: "maximalist", label: "Maximalist", prefix: "max" }),
|
|
9
|
+
Object.freeze({ id: "bauhaus", label: "Bauhaus", prefix: "bau" }),
|
|
10
|
+
Object.freeze({ id: "tactile", label: "Tactile", prefix: "tactile" }),
|
|
11
|
+
Object.freeze({ id: "neumorphism", label: "Neumorphism", prefix: "neo" }),
|
|
12
|
+
Object.freeze({ id: "retrofuturism", label: "Retrofuturism", prefix: "retro" }),
|
|
13
|
+
Object.freeze({ id: "brutalism", label: "Brutalism", prefix: "brutal" }),
|
|
14
|
+
Object.freeze({ id: "cyberpunk", label: "Cyberpunk", prefix: "cyber" }),
|
|
15
|
+
Object.freeze({ id: "y2k", label: "Y2K", prefix: "y2k" }),
|
|
16
|
+
Object.freeze({ id: "retro-glass", label: "Retro Glass", prefix: "rg" })
|
|
17
|
+
]),
|
|
18
|
+
themes: Object.freeze([
|
|
19
|
+
"midnight-gold",
|
|
20
|
+
"ocean-steel",
|
|
21
|
+
"forest-moss",
|
|
22
|
+
"sunset-ember",
|
|
23
|
+
"royal-plum",
|
|
24
|
+
"graphite-cyan",
|
|
25
|
+
"desert-sage",
|
|
26
|
+
"rose-quartz",
|
|
27
|
+
"cyber-lime",
|
|
28
|
+
"arctic-indigo"
|
|
29
|
+
]),
|
|
30
|
+
modes: Object.freeze(["light", "dark", "contrast"])
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function normalizeUiStyleKitManifest(manifest) {
|
|
34
|
+
const normalized = {
|
|
35
|
+
version: String(manifest?.version ?? UI_STYLE_KIT_VERSION),
|
|
36
|
+
presets: Array.isArray(manifest?.presets) ? manifest.presets : [],
|
|
37
|
+
themes: Array.isArray(manifest?.themes) ? manifest.themes : [],
|
|
38
|
+
modes: Array.isArray(manifest?.modes) ? manifest.modes : []
|
|
39
|
+
};
|
|
40
|
+
const presets = normalized.presets
|
|
41
|
+
.map((preset) => ({
|
|
42
|
+
id: String(preset?.id ?? ""),
|
|
43
|
+
label: String(preset?.label ?? preset?.id ?? ""),
|
|
44
|
+
prefix: String(preset?.prefix ?? "")
|
|
45
|
+
}))
|
|
46
|
+
.filter((preset) => preset.id && preset.prefix);
|
|
47
|
+
|
|
48
|
+
if (presets.length === 0 || normalized.themes.length === 0 || normalized.modes.length === 0) {
|
|
49
|
+
throw new Error("UI Style Kit manifest is missing presets, themes, or modes.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
version: normalized.version,
|
|
54
|
+
presets: Object.freeze(presets.map((preset) => Object.freeze(preset))),
|
|
55
|
+
themes: Object.freeze(normalized.themes.map(String)),
|
|
56
|
+
modes: Object.freeze(normalized.modes.map(String))
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function loadUiStyleKitManifest() {
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(UI_STYLE_KIT_MANIFEST_URL, { cache: "force-cache" });
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`HTTP ${response.status}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return normalizeUiStyleKitManifest(await response.json());
|
|
69
|
+
} catch (error) {
|
|
70
|
+
/*
|
|
71
|
+
The published demo can render before the companion UI release reaches every CDN edge.
|
|
72
|
+
The fallback mirrors the 2.1 manifest contract so layout behavior stays testable.
|
|
73
|
+
*/
|
|
74
|
+
console.warn("UI Style Kit 2.1 manifest unavailable; using the packaged fallback.", error);
|
|
75
|
+
return UI_STYLE_KIT_MANIFEST_FALLBACK;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const UI_STYLE_KIT_MANIFEST = await loadUiStyleKitManifest();
|
|
80
|
+
const ALLOWLISTS = Object.freeze({
|
|
81
|
+
wrapper: Object.freeze(["default", "compact", "prose", "content", "wide", "full", "breakout"]),
|
|
82
|
+
recipe: Object.freeze([
|
|
83
|
+
"app-shell",
|
|
84
|
+
"dashboard",
|
|
85
|
+
"docs",
|
|
86
|
+
"list-detail",
|
|
87
|
+
"split-hero",
|
|
88
|
+
"gallery",
|
|
89
|
+
"card-grid"
|
|
90
|
+
]),
|
|
91
|
+
personality: Object.freeze([
|
|
92
|
+
"minimal-saas",
|
|
93
|
+
"bauhaus",
|
|
94
|
+
"tactile",
|
|
95
|
+
"cyberpunk",
|
|
96
|
+
"f-pattern",
|
|
97
|
+
"brutalism",
|
|
98
|
+
"neumorphism",
|
|
99
|
+
"y2k",
|
|
100
|
+
"retro-glass",
|
|
101
|
+
"z-pattern",
|
|
102
|
+
"retrofuturism",
|
|
103
|
+
"mondrian",
|
|
104
|
+
"synthwave",
|
|
105
|
+
"bento",
|
|
106
|
+
"maximalist",
|
|
107
|
+
"split-screen"
|
|
108
|
+
]),
|
|
109
|
+
container: Object.freeze(["auto", "40rem", "47rem", "49rem", "63rem", "65rem", "80rem"]),
|
|
110
|
+
density: Object.freeze(["compact", "comfortable", "spacious"]),
|
|
111
|
+
ui: Object.freeze(UI_STYLE_KIT_MANIFEST.presets.map((preset) => preset.id)),
|
|
112
|
+
theme: Object.freeze(UI_STYLE_KIT_MANIFEST.themes),
|
|
113
|
+
mode: Object.freeze(UI_STYLE_KIT_MANIFEST.modes),
|
|
114
|
+
ecosystem: Object.freeze(["layout-only", "layout-ui", "all-three"])
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const DEFAULT_STATE = Object.freeze({
|
|
118
|
+
wrapper: "default",
|
|
119
|
+
recipe: "app-shell",
|
|
120
|
+
personality: "minimal-saas",
|
|
121
|
+
container: "auto",
|
|
122
|
+
density: "comfortable",
|
|
123
|
+
ui: "minimal-saas",
|
|
124
|
+
theme: "arctic-indigo",
|
|
125
|
+
mode: "light",
|
|
126
|
+
ecosystem: "all-three"
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const CONTAINER_WIDTHS = Object.freeze({
|
|
130
|
+
auto: "100%",
|
|
131
|
+
"40rem": "40rem",
|
|
132
|
+
"47rem": "47rem",
|
|
133
|
+
"49rem": "49rem",
|
|
134
|
+
"63rem": "63rem",
|
|
135
|
+
"65rem": "65rem",
|
|
136
|
+
"80rem": "80rem"
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const DENSITY_GAPS = Object.freeze({
|
|
140
|
+
compact: "0.5rem",
|
|
141
|
+
comfortable: "1rem",
|
|
142
|
+
spacious: "1.5rem"
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const UI_CLASS_PREFIXES = Object.freeze(
|
|
146
|
+
Object.fromEntries(UI_STYLE_KIT_MANIFEST.presets.map((preset) => [preset.id, preset.prefix]))
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const RECIPE_CLASSES = Object.freeze({
|
|
150
|
+
"app-shell": "ly-app-shell",
|
|
151
|
+
dashboard: "ly-dashboard",
|
|
152
|
+
docs: "ly-docs",
|
|
153
|
+
"list-detail": "ly-list-detail",
|
|
154
|
+
"split-hero": "ly-split-hero",
|
|
155
|
+
gallery: "ly-gallery",
|
|
156
|
+
"card-grid": "ly-card-grid"
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const RECIPE_AREAS = Object.freeze({
|
|
160
|
+
"app-shell": Object.freeze(["header", "nav", "main", "aside", "footer"]),
|
|
161
|
+
dashboard: Object.freeze(["header", "nav", "main", "aside", "footer"]),
|
|
162
|
+
docs: Object.freeze(["header", "nav", "main", "aside", "footer"]),
|
|
163
|
+
"list-detail": Object.freeze(["primary", "secondary", "actions"]),
|
|
164
|
+
"split-hero": Object.freeze(["content", "media", "actions"])
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const ECOSYSTEM_IMPORTS = Object.freeze({
|
|
168
|
+
"layout-only": Object.freeze(['import "layout-style-css";']),
|
|
169
|
+
"layout-ui": Object.freeze([
|
|
170
|
+
'import "ui-style-kit-css/visual.css";',
|
|
171
|
+
'import "layout-style-css";'
|
|
172
|
+
]),
|
|
173
|
+
"all-three": Object.freeze([
|
|
174
|
+
'import "ui-style-kit-css/visual.css";',
|
|
175
|
+
'import "ui-style-kit-css/interactive-surface-theme.css";',
|
|
176
|
+
'import "interactive-surface-css/state-core.css";',
|
|
177
|
+
'import "layout-style-css";'
|
|
178
|
+
])
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const ECOSYSTEM_LABELS = Object.freeze({
|
|
182
|
+
"layout-only": "Layout only",
|
|
183
|
+
"layout-ui": "Layout + UI",
|
|
184
|
+
"all-three": "All three libraries"
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const body = document.body;
|
|
188
|
+
const controls = Object.fromEntries(
|
|
189
|
+
Object.keys(ALLOWLISTS).map((key) => [key, document.querySelector(`[data-query-key="${key}"]`)])
|
|
190
|
+
);
|
|
191
|
+
const previewFrame = document.querySelector("#previewFrame");
|
|
192
|
+
const previewRoot = document.querySelector("#previewRoot");
|
|
193
|
+
const previewWrapper = document.querySelector("#previewWrapper");
|
|
194
|
+
const recipePreview = document.querySelector("#recipePreview");
|
|
195
|
+
const importsSnippet = document.querySelector("#importsSnippet");
|
|
196
|
+
const markupSnippet = document.querySelector("#markupSnippet");
|
|
197
|
+
const copyStatus = document.querySelector("#copyStatus");
|
|
198
|
+
const ecosystemStatus = document.querySelector("#ecosystemStatus");
|
|
199
|
+
const containerReadout = document.querySelector("#containerReadout");
|
|
200
|
+
const uiKitStylesheet = document.querySelector("#uiKitStylesheet");
|
|
201
|
+
const uiKitInteractiveThemeStylesheet = document.querySelector("#uiKitInteractiveThemeStylesheet");
|
|
202
|
+
const interactiveSurfaceStylesheet = document.querySelector("#interactiveSurfaceStylesheet");
|
|
203
|
+
const layoutIntegrationStylesheet = document.querySelector("#layoutIntegrationStylesheet");
|
|
204
|
+
const layoutCoreStylesheet = document.querySelector("#layoutCoreStylesheet");
|
|
205
|
+
const drawerToggle = document.querySelector("#demoControlsToggle");
|
|
206
|
+
const drawer = document.querySelector("#demoControlsDrawer");
|
|
207
|
+
const drawerClose = document.querySelector("#demoControlsClose");
|
|
208
|
+
const drawerBackdrop = document.querySelector("#demoControlsBackdrop");
|
|
209
|
+
const stateToggle = document.querySelector("#stateToggle");
|
|
210
|
+
const mobileControlsQuery = window.matchMedia("(max-width: 63.999rem)");
|
|
211
|
+
|
|
212
|
+
body.dataset.uiManifestVersion = UI_STYLE_KIT_MANIFEST.version;
|
|
213
|
+
syncUiManifestSelectOptions();
|
|
214
|
+
|
|
215
|
+
let state = readStateFromQuery();
|
|
216
|
+
let drawerReturnFocus = null;
|
|
217
|
+
let querySyncTimer = null;
|
|
218
|
+
let hasSynchronizedQuery = false;
|
|
219
|
+
|
|
220
|
+
function readStateFromQuery() {
|
|
221
|
+
const query = new URLSearchParams(window.location.search);
|
|
222
|
+
|
|
223
|
+
return Object.fromEntries(
|
|
224
|
+
Object.entries(ALLOWLISTS).map(([key, values]) => {
|
|
225
|
+
const candidate = query.get(key);
|
|
226
|
+
return [key, candidate !== null && values.includes(candidate) ? candidate : DEFAULT_STATE[key]];
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function syncQuery() {
|
|
232
|
+
const writeQuery = () => {
|
|
233
|
+
const url = new URL(window.location.href);
|
|
234
|
+
url.search = "";
|
|
235
|
+
|
|
236
|
+
for (const key of Object.keys(ALLOWLISTS)) {
|
|
237
|
+
url.searchParams.set(key, state[key]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
window.history.replaceState(null, "", url);
|
|
241
|
+
hasSynchronizedQuery = true;
|
|
242
|
+
querySyncTimer = null;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
if (!hasSynchronizedQuery) {
|
|
246
|
+
writeQuery();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Coalesce rapid control changes so WebKit's history safety limit is never exceeded. */
|
|
251
|
+
window.clearTimeout(querySyncTimer);
|
|
252
|
+
querySyncTimer = window.setTimeout(writeQuery, 200);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function createElement(tagName, { className, text, attributes = {}, data = {} } = {}) {
|
|
256
|
+
const element = document.createElement(tagName);
|
|
257
|
+
|
|
258
|
+
if (className) {
|
|
259
|
+
element.className = className;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (text !== undefined) {
|
|
263
|
+
element.textContent = text;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
267
|
+
element.setAttribute(name, value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const [name, value] of Object.entries(data)) {
|
|
271
|
+
element.dataset[name] = value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return element;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function createAction(sequence) {
|
|
278
|
+
return createElement("button", {
|
|
279
|
+
className: "interactive-surface",
|
|
280
|
+
text: `${formatLabel(sequence)} action`,
|
|
281
|
+
attributes: { type: "button", "data-ui-kit": "button" },
|
|
282
|
+
data: { demoButton: "", demoFocus: sequence }
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function createRegion(area) {
|
|
287
|
+
const semanticTags = {
|
|
288
|
+
header: "header",
|
|
289
|
+
nav: "nav",
|
|
290
|
+
aside: "aside",
|
|
291
|
+
footer: "footer",
|
|
292
|
+
media: "figure",
|
|
293
|
+
actions: "div"
|
|
294
|
+
};
|
|
295
|
+
const element = createElement(semanticTags[area] ?? "section", {
|
|
296
|
+
className: "demo-region",
|
|
297
|
+
attributes: { "data-ui-kit": "card" },
|
|
298
|
+
data: { lyArea: area, demoSequence: area }
|
|
299
|
+
});
|
|
300
|
+
const heading = createElement("strong", { text: formatLabel(area) });
|
|
301
|
+
|
|
302
|
+
if (area === "nav") {
|
|
303
|
+
element.setAttribute("aria-label", "Preview navigation");
|
|
304
|
+
} else {
|
|
305
|
+
element.setAttribute("aria-label", `${formatLabel(area)} region`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
element.append(heading, createAction(area));
|
|
309
|
+
|
|
310
|
+
if (area === "secondary") {
|
|
311
|
+
const scroll = createElement("div", {
|
|
312
|
+
className: "ly-scroll demo-list-scroll",
|
|
313
|
+
attributes: { tabindex: "0", "aria-label": "Scrollable detail activity" }
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
for (let index = 1; index <= 10; index += 1) {
|
|
317
|
+
scroll.append(createElement("p", { text: `Detail activity ${index}` }));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
element.append(scroll);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return element;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function createGridItem(index) {
|
|
327
|
+
const sequence = `item-${index}`;
|
|
328
|
+
const item = createElement("article", {
|
|
329
|
+
className: "demo-region demo-grid-item",
|
|
330
|
+
attributes: { "data-ui-kit": "card" },
|
|
331
|
+
data: { demoSequence: sequence }
|
|
332
|
+
});
|
|
333
|
+
item.append(createElement("strong", { text: `Item ${index}` }), createAction(sequence));
|
|
334
|
+
return item;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function renderRecipe() {
|
|
338
|
+
const recipe = state.recipe;
|
|
339
|
+
const children = RECIPE_AREAS[recipe]
|
|
340
|
+
? RECIPE_AREAS[recipe].map((area) => createRegion(area))
|
|
341
|
+
: Array.from({ length: recipe === "gallery" ? 5 : 6 }, (_, index) => createGridItem(index + 1));
|
|
342
|
+
|
|
343
|
+
recipePreview.className = `${RECIPE_CLASSES[recipe]} demo-recipe`;
|
|
344
|
+
recipePreview.dataset.lyRecipe = recipe;
|
|
345
|
+
recipePreview.replaceChildren(...children);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function formatLabel(value) {
|
|
349
|
+
return value
|
|
350
|
+
.split("-")
|
|
351
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
352
|
+
.join(" ");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function syncUiManifestSelectOptions() {
|
|
356
|
+
const optionGroups = {
|
|
357
|
+
ui: UI_STYLE_KIT_MANIFEST.presets.map((preset) => ({
|
|
358
|
+
value: preset.id,
|
|
359
|
+
label: preset.label || formatLabel(preset.id)
|
|
360
|
+
})),
|
|
361
|
+
theme: UI_STYLE_KIT_MANIFEST.themes.map((theme) => ({
|
|
362
|
+
value: theme,
|
|
363
|
+
label: formatLabel(theme)
|
|
364
|
+
})),
|
|
365
|
+
mode: UI_STYLE_KIT_MANIFEST.modes.map((mode) => ({
|
|
366
|
+
value: mode,
|
|
367
|
+
label: mode === "contrast" ? "High contrast" : formatLabel(mode)
|
|
368
|
+
}))
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
for (const [key, options] of Object.entries(optionGroups)) {
|
|
372
|
+
const select = controls[key];
|
|
373
|
+
|
|
374
|
+
if (!select) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
select.replaceChildren(
|
|
379
|
+
...options.map(({ value, label }) => {
|
|
380
|
+
const option = document.createElement("option");
|
|
381
|
+
option.value = value;
|
|
382
|
+
option.textContent = label;
|
|
383
|
+
return option;
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function syncUiKitClasses() {
|
|
390
|
+
const prefix = UI_CLASS_PREFIXES[state.ui];
|
|
391
|
+
|
|
392
|
+
document.querySelectorAll("[data-ui-kit]").forEach((element) => {
|
|
393
|
+
const previousClasses = element.dataset.uiKitApplied?.split(" ").filter(Boolean) ?? [];
|
|
394
|
+
|
|
395
|
+
if (previousClasses.length > 0) {
|
|
396
|
+
element.classList.remove(...previousClasses);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const nextClasses = element.dataset.uiKit
|
|
400
|
+
.split(/\s+/)
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.map((role) => `${prefix}-${role}`);
|
|
403
|
+
|
|
404
|
+
if (nextClasses.length > 0) {
|
|
405
|
+
element.classList.add(...nextClasses);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
element.dataset.uiKitApplied = nextClasses.join(" ");
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function syncEcosystem() {
|
|
413
|
+
const includesUi = state.ecosystem !== "layout-only";
|
|
414
|
+
const includesInteractiveSurface = state.ecosystem === "all-three";
|
|
415
|
+
|
|
416
|
+
uiKitStylesheet.disabled = !includesUi;
|
|
417
|
+
uiKitInteractiveThemeStylesheet.disabled = !includesInteractiveSurface;
|
|
418
|
+
interactiveSurfaceStylesheet.disabled = !includesInteractiveSurface;
|
|
419
|
+
layoutIntegrationStylesheet.disabled = true;
|
|
420
|
+
layoutCoreStylesheet.disabled = false;
|
|
421
|
+
ecosystemStatus.textContent = ECOSYSTEM_LABELS[state.ecosystem];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function buildMarkupSnippet() {
|
|
425
|
+
const wrapperClass = state.wrapper === "default" ? "ly-wrapper" : `ly-wrapper ly-wrapper--${state.wrapper}`;
|
|
426
|
+
const rootClass = RECIPE_CLASSES[state.recipe];
|
|
427
|
+
const areas = RECIPE_AREAS[state.recipe];
|
|
428
|
+
const childMarkup = areas
|
|
429
|
+
? areas.map((area) => ` <section data-ly-area="${area}">${formatLabel(area)}</section>`).join("\n")
|
|
430
|
+
: ` <article>Item 1</article>\n <article>Item 2</article>`;
|
|
431
|
+
|
|
432
|
+
return [
|
|
433
|
+
`<div class="ly-root" data-ly-layout="${state.personality}">`,
|
|
434
|
+
` <div class="${wrapperClass}">`,
|
|
435
|
+
` <div class="${rootClass}" data-ly-recipe="${state.recipe}">`,
|
|
436
|
+
childMarkup,
|
|
437
|
+
" </div>",
|
|
438
|
+
" </div>",
|
|
439
|
+
"</div>"
|
|
440
|
+
].join("\n");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function syncSnippets() {
|
|
444
|
+
// Snippets are rendered as inert text from allowlisted state, never parsed as HTML.
|
|
445
|
+
importsSnippet.textContent = ECOSYSTEM_IMPORTS[state.ecosystem].join("\n");
|
|
446
|
+
markupSnippet.textContent = buildMarkupSnippet();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function applyState({ updateQuery = true } = {}) {
|
|
450
|
+
for (const [key, control] of Object.entries(controls)) {
|
|
451
|
+
control.value = state[key];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
body.dataset.ui = state.ui;
|
|
455
|
+
body.dataset.theme = state.theme;
|
|
456
|
+
body.dataset.mode = state.mode;
|
|
457
|
+
body.dataset.lyLayout = state.personality;
|
|
458
|
+
body.dataset.density = state.density;
|
|
459
|
+
body.dataset.ecosystem = state.ecosystem;
|
|
460
|
+
previewRoot.dataset.lyLayout = state.personality;
|
|
461
|
+
previewRoot.style.setProperty("--ly-gap", DENSITY_GAPS[state.density]);
|
|
462
|
+
previewRoot.style.setProperty("--ly-grid-gap", DENSITY_GAPS[state.density]);
|
|
463
|
+
|
|
464
|
+
previewWrapper.className =
|
|
465
|
+
state.wrapper === "default" ? "ly-wrapper" : `ly-wrapper ly-wrapper--${state.wrapper}`;
|
|
466
|
+
previewFrame.dataset.containerWidth = state.container;
|
|
467
|
+
previewFrame.style.setProperty("--demo-container-inline-size", CONTAINER_WIDTHS[state.container]);
|
|
468
|
+
containerReadout.textContent = `Container: ${state.container}`;
|
|
469
|
+
|
|
470
|
+
renderRecipe();
|
|
471
|
+
syncUiKitClasses();
|
|
472
|
+
syncEcosystem();
|
|
473
|
+
syncSnippets();
|
|
474
|
+
|
|
475
|
+
if (updateQuery) {
|
|
476
|
+
syncQuery();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function setDrawerOpen(open, { returnFocus = true } = {}) {
|
|
481
|
+
if (!mobileControlsQuery.matches) {
|
|
482
|
+
drawerToggle.setAttribute("aria-expanded", "false");
|
|
483
|
+
drawer.hidden = false;
|
|
484
|
+
drawer.inert = false;
|
|
485
|
+
drawer.setAttribute("aria-hidden", "false");
|
|
486
|
+
drawerBackdrop.hidden = true;
|
|
487
|
+
drawerReturnFocus = null;
|
|
488
|
+
delete body.dataset.demoControlsOpen;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
drawerToggle.setAttribute("aria-expanded", String(open));
|
|
493
|
+
drawer.hidden = !open;
|
|
494
|
+
drawer.inert = !open;
|
|
495
|
+
drawer.setAttribute("aria-hidden", String(!open));
|
|
496
|
+
drawerBackdrop.hidden = !open;
|
|
497
|
+
|
|
498
|
+
if (open) {
|
|
499
|
+
const activeElement = document.activeElement;
|
|
500
|
+
const hasMeaningfulActiveElement =
|
|
501
|
+
activeElement instanceof HTMLElement &&
|
|
502
|
+
activeElement !== body &&
|
|
503
|
+
activeElement !== document.documentElement;
|
|
504
|
+
|
|
505
|
+
/* WebKit may leave pointer-activated buttons unfocused, so never return focus to the page body. */
|
|
506
|
+
drawerReturnFocus = hasMeaningfulActiveElement ? activeElement : drawerToggle;
|
|
507
|
+
body.dataset.demoControlsOpen = "true";
|
|
508
|
+
drawerClose.focus({ preventScroll: true });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
delete body.dataset.demoControlsOpen;
|
|
513
|
+
|
|
514
|
+
if (returnFocus) {
|
|
515
|
+
const focusTarget = drawerReturnFocus instanceof HTMLElement ? drawerReturnFocus : drawerToggle;
|
|
516
|
+
focusTarget.focus({ preventScroll: true });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
drawerReturnFocus = null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function trapDrawerFocus(event) {
|
|
523
|
+
if (!mobileControlsQuery.matches || drawer.hidden || event.key !== "Tab") {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const focusable = [...drawer.querySelectorAll("button:not([disabled]), select:not([disabled])")];
|
|
528
|
+
const first = focusable.at(0);
|
|
529
|
+
const last = focusable.at(-1);
|
|
530
|
+
|
|
531
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
532
|
+
event.preventDefault();
|
|
533
|
+
last.focus();
|
|
534
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
535
|
+
event.preventDefault();
|
|
536
|
+
first.focus();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function copySnippet(button) {
|
|
541
|
+
const target = document.querySelector(`#${button.dataset.copyTarget}`);
|
|
542
|
+
const label = button.dataset.copyTarget === "importsSnippet" ? "imports" : "markup";
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
await navigator.clipboard.writeText(target.textContent);
|
|
546
|
+
copyStatus.dataset.copyState = "success";
|
|
547
|
+
copyStatus.textContent = `Copied ${label}.`;
|
|
548
|
+
} catch {
|
|
549
|
+
const fallback = createElement("textarea", { text: target.textContent });
|
|
550
|
+
fallback.setAttribute("aria-hidden", "true");
|
|
551
|
+
fallback.style.position = "fixed";
|
|
552
|
+
fallback.style.opacity = "0";
|
|
553
|
+
document.body.append(fallback);
|
|
554
|
+
fallback.select();
|
|
555
|
+
const copied = document.execCommand("copy");
|
|
556
|
+
fallback.remove();
|
|
557
|
+
copyStatus.dataset.copyState = copied ? "success" : "error";
|
|
558
|
+
copyStatus.textContent = copied ? `Copied ${label}.` : `Unable to copy ${label}.`;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
for (const [key, control] of Object.entries(controls)) {
|
|
563
|
+
control.addEventListener("change", () => {
|
|
564
|
+
const candidate = control.value;
|
|
565
|
+
|
|
566
|
+
// Control values still pass through the same allowlists used for deep links.
|
|
567
|
+
if (!ALLOWLISTS[key].includes(candidate)) {
|
|
568
|
+
control.value = state[key];
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
state = { ...state, [key]: candidate };
|
|
573
|
+
applyState();
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
drawerToggle.addEventListener("click", () => {
|
|
578
|
+
setDrawerOpen(drawerToggle.getAttribute("aria-expanded") !== "true");
|
|
579
|
+
});
|
|
580
|
+
drawerClose.addEventListener("click", () => setDrawerOpen(false));
|
|
581
|
+
drawerBackdrop.addEventListener("click", () => setDrawerOpen(false));
|
|
582
|
+
mobileControlsQuery.addEventListener("change", () => setDrawerOpen(false, { returnFocus: false }));
|
|
583
|
+
|
|
584
|
+
document.addEventListener("keydown", (event) => {
|
|
585
|
+
if (event.key === "Escape" && drawerToggle.getAttribute("aria-expanded") === "true") {
|
|
586
|
+
setDrawerOpen(false);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
trapDrawerFocus(event);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
stateToggle.addEventListener("click", () => {
|
|
594
|
+
const pressed = stateToggle.getAttribute("aria-pressed") !== "true";
|
|
595
|
+
stateToggle.setAttribute("aria-pressed", String(pressed));
|
|
596
|
+
stateToggle.textContent = pressed ? "Active state on" : "Toggle active state";
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
document.querySelectorAll("[data-copy-target]").forEach((button) => {
|
|
600
|
+
button.addEventListener("click", () => copySnippet(button));
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
applyState();
|
|
604
|
+
setDrawerOpen(false, { returnFocus: false });
|
|
605
|
+
body.dataset.demoReady = "true";
|