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