@pure-ds/core 0.6.2 → 0.6.4

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.
@@ -1,1555 +1,1940 @@
1
- const PDS = globalThis.PDS;
2
-
3
- const EDITOR_TAG = "pds-live-edit";
4
- const STYLE_ID = "pds-live-editor-styles";
5
- const TARGET_ATTR = "data-pds-live-target";
6
- const DROPDOWN_CLASS = "pds-live-editor-dropdown";
7
- const MARKER_CLASS = "pds-live-editor-marker";
8
-
9
- const CATEGORY_ICONS = {
10
- colors: "palette",
11
- typography: "text-aa",
12
- spatialRhythm: "grid-four",
13
- shape: "circle",
14
- layout: "grid-four",
15
- behavior: "sparkle",
16
- layers: "squares-four",
17
- icons: "sparkle",
18
- };
19
-
20
- const QUICK_RULES = [
21
- {
22
- selector: "button, .btn-primary, .btn-secondary, .btn-outline, .btn-sm, .btn-xs, .btn-lg",
23
- paths: [
24
- "colors.primary",
25
- "shape.radiusSize",
26
- "spatialRhythm.buttonPadding",
27
- "layout.buttonMinHeight",
28
- "behavior.transitionSpeed",
29
- ],
30
- },
31
- {
32
- selector: "input, textarea, select, label",
33
- paths: [
34
- "colors.secondary",
35
- "shape.radiusSize",
36
- "spatialRhythm.inputPadding",
37
- "layout.inputMinHeight",
38
- "typography.fontFamilyBody",
39
- ],
40
- },
41
- {
42
- selector: ".card, .surface-base, .surface-elevated, .surface-sunken, .surface-subtle",
43
- paths: [
44
- "colors.background",
45
- "shape.radiusSize",
46
- "layers.baseShadowOpacity",
47
- ],
48
- },
49
- {
50
- selector: "nav, menu",
51
- paths: ["colors.background", "typography.fontFamilyBody", "spatialRhythm.baseUnit"],
52
- },
53
- {
54
- selector: "pds-icon",
55
- paths: ["icons.defaultSize", "icons.weight"],
56
- },
57
- ];
58
-
59
- const DEFAULT_QUICK_PATHS = [
60
- "colors.primary",
61
- "typography.fontFamilyBody",
62
- "shape.radiusSize",
63
- ];
64
-
65
- const QUICK_STYLE_PROPERTIES = [
66
- "background-color",
67
- "color",
68
- "border-color",
69
- "border-top-color",
70
- "border-right-color",
71
- "border-bottom-color",
72
- "border-left-color",
73
- "outline-color",
74
- "box-shadow",
75
- "text-shadow",
76
- "fill",
77
- "stroke",
78
- "font-family",
79
- "font-size",
80
- "font-weight",
81
- "letter-spacing",
82
- "line-height",
83
- "border-radius",
84
- "padding",
85
- "padding-top",
86
- "padding-right",
87
- "padding-bottom",
88
- "padding-left",
89
- "gap",
90
- "row-gap",
91
- "column-gap",
92
- "min-height",
93
- "min-width",
94
- "height",
95
- "width",
96
- ];
97
-
98
- const INLINE_VAR_REGEX = /var\(\s*(--[^)\s,]+)\s*/g;
99
- const COLOR_VALUE_REGEX = /#(?:[0-9a-f]{3,8})\b|rgba?\([^)]*\)|hsla?\([^)]*\)/gi;
100
-
101
- let cachedTokenIndex = null;
102
- let cachedTokenIndexMeta = null;
103
- let colorNormalizer = null;
104
-
105
- const GLOBAL_LAYOUT_PATHS = new Set([
106
- "layout.maxWidth",
107
- "layout.maxWidths",
108
- "layout.breakpoints",
109
- "layout.containerMaxWidth",
110
- "layout.containerPadding",
111
- "layout.gridColumns",
112
- "layout.gridGutter",
113
- "layout.densityCompact",
114
- "layout.densityNormal",
115
- "layout.densityComfortable",
116
- ]);
117
-
118
- const FORM_CONTEXT_PATHS = new Set([
119
- "spatialRhythm.inputPadding",
120
- "layout.inputMinHeight",
121
- "behavior.focusRingWidth",
122
- "behavior.focusRingOpacity",
123
- ]);
124
-
125
- const SURFACE_CONTEXT_PATHS = new Set([
126
- "colors.background",
127
- "layers.baseShadowOpacity",
128
- "layout.baseShadowOpacity",
129
- ]);
130
-
131
- const DARK_MODE_PATH_MARKER = ".darkMode.";
132
- const QUICK_EDIT_LIMIT = 4;
133
- const DROPDOWN_VIEWPORT_PADDING = 8;
134
-
135
- function isHoverCapable() {
136
- if (typeof window === "undefined" || !window.matchMedia) return false;
137
- return window.matchMedia("(hover: hover) and (pointer: fine)").matches;
138
- }
139
-
140
- function ensureStyles() {
141
- if (typeof document === "undefined") return;
142
- if (document.getElementById(STYLE_ID)) return;
143
- const style = document.createElement("style");
144
- style.id = STYLE_ID;
145
- style.textContent = `
146
- ${EDITOR_TAG} {
147
- display: contents;
148
- }
149
- [${TARGET_ATTR}] {
150
- position: relative;
151
- }
152
- .${DROPDOWN_CLASS} {
153
- position: fixed;
154
- top: var(--pds-live-edit-top, auto);
155
- right: var(--pds-live-edit-right, auto);
156
- bottom: var(--pds-live-edit-bottom, auto);
157
- left: var(--pds-live-edit-left, auto);
158
- z-index: var(--z-popover);
159
- }
160
- .${MARKER_CLASS} {
161
- pointer-events: auto;
162
- }
163
- .context-edit {
164
- min-width: 0;
165
- min-height: 0;
166
- width: var(--spacing-6);
167
- height: var(--spacing-6);
168
- padding: 0;
169
- }
170
- .${DROPDOWN_CLASS} menu {
171
- min-width: max-content;
172
- max-width: 350px;
173
- }
174
- .${DROPDOWN_CLASS} .pds-live-editor-menu {
175
- padding: var(--spacing-1);
176
- max-width: 350px;
177
- }
178
- .${DROPDOWN_CLASS} .pds-live-editor-title {
179
- display: block;
180
- font-size: var(--font-size-sm);
181
- font-weight: var(--font-weight-semibold);
182
- margin-bottom: var(--spacing-2);
183
- }
184
- .${DROPDOWN_CLASS} .pds-live-editor-header {
185
- display: flex;
186
- align-items: center;
187
- justify-content: space-between;
188
- gap: var(--spacing-2);
189
- }
190
- .${DROPDOWN_CLASS} .pds-live-editor-debug {
191
- font-size: var(--font-size-xs);
192
- opacity: 0.7;
193
- white-space: pre-wrap;
194
- margin-top: var(--spacing-2);
195
- }
196
- .${DROPDOWN_CLASS} .pds-live-editor-menu input[type="color"] {
197
- width: var(--spacing-9);
198
- height: var(--spacing-6);
199
- max-width: var(--spacing-9);
200
- min-width: var(--spacing-9);
201
- padding: 0;
202
- border-radius: var(--radius-sm);
203
- }
204
- `;
205
- document.head.appendChild(style);
206
- }
207
-
208
- function isSelectorSupported(selector) {
209
- if (typeof selector !== "string" || !selector.trim()) return false;
210
- if (typeof CSS !== "undefined" && typeof CSS.supports === "function") {
211
- try {
212
- return CSS.supports(`selector(${selector})`);
213
- } catch (e) {
214
- return false;
215
- }
216
- }
217
- if (typeof document === "undefined") return false;
218
- try {
219
- document.querySelector(selector);
220
- return true;
221
- } catch (e) {
222
- return false;
223
- }
224
- }
225
-
226
- function collectSelectors() {
227
- const ontology = PDS?.ontology;
228
- const selectors = new Set();
229
- if (!ontology) return { selector: "", list: [] };
230
-
231
- const addSelector = (selector) => {
232
- if (typeof selector !== "string" || !selector.trim()) return;
233
- if (isSelectorSupported(selector)) selectors.add(selector);
234
- };
235
-
236
- const addSelectorList = (list) => {
237
- (list || []).forEach((selector) => addSelector(selector));
238
- };
239
-
240
- const sections = [ontology.primitives, ontology.components, ontology.layoutPatterns];
241
- sections.forEach((items) => {
242
- if (!Array.isArray(items)) return;
243
- items.forEach((item) => {
244
- addSelectorList(item?.selectors || []);
245
- });
246
- });
247
-
248
- Object.values(ontology.utilities || {}).forEach((group) => {
249
- if (!group || typeof group !== "object") return;
250
- Object.values(group).forEach((list) => addSelectorList(list));
251
- });
252
-
253
- (ontology.enhancements || []).forEach((enhancer) => {
254
- addSelector(enhancer?.selector);
255
- });
256
-
257
- addSelector("body");
258
- addSelector("[data-dropdown]");
259
- addSelector("*");
260
-
261
- return { selector: Array.from(selectors).join(", "), list: Array.from(selectors) };
262
- }
263
-
264
- function shallowClone(value) {
265
- if (!value || typeof value !== "object") return value;
266
- return Array.isArray(value) ? [...value] : { ...value };
267
- }
268
-
269
- function deepMerge(target = {}, source = {}) {
270
- if (!source || typeof source !== "object") return target;
271
- const out = Array.isArray(target) ? [...target] : { ...target };
272
- for (const [key, value] of Object.entries(source)) {
273
- if (value && typeof value === "object" && !Array.isArray(value)) {
274
- out[key] = deepMerge(out[key] && typeof out[key] === "object" ? out[key] : {}, value);
275
- } else {
276
- out[key] = value;
277
- }
278
- }
279
- return out;
280
- }
281
-
282
- function titleize(value) {
283
- return String(value)
284
- .replace(/([a-z])([A-Z])/g, "$1 $2")
285
- .replace(/[_-]+/g, " ")
286
- .replace(/\s+/g, " ")
287
- .trim()
288
- .replace(/^./, (char) => char.toUpperCase());
289
- }
290
-
291
- function getValueAtPath(obj, pathSegments) {
292
- let current = obj;
293
- for (const segment of pathSegments) {
294
- if (!current || typeof current !== "object") return undefined;
295
- current = current[segment];
296
- }
297
- return current;
298
- }
299
-
300
- function setValueAtPath(target, pathSegments, value) {
301
- let current = target;
302
- for (let i = 0; i < pathSegments.length; i += 1) {
303
- const segment = pathSegments[i];
304
- if (i === pathSegments.length - 1) {
305
- current[segment] = value;
306
- return;
307
- }
308
- if (!current[segment] || typeof current[segment] !== "object") {
309
- current[segment] = {};
310
- }
311
- current = current[segment];
312
- }
313
- }
314
-
315
- function setValueAtJsonPath(target, jsonPath, value) {
316
- if (!jsonPath || typeof jsonPath !== "string") return;
317
- const parts = jsonPath.replace(/^\//, "").split("/").filter(Boolean);
318
- if (!parts.length) return;
319
- let current = target;
320
- parts.forEach((segment, index) => {
321
- if (index === parts.length - 1) {
322
- current[segment] = value;
323
- return;
324
- }
325
- if (!current[segment] || typeof current[segment] !== "object") {
326
- current[segment] = {};
327
- }
328
- current = current[segment];
329
- });
330
- }
331
-
332
- function normalizePaths(paths) {
333
- const relations = PDS?.configRelations || {};
334
- const seen = new Set();
335
- const filtered = [];
336
- (paths || []).forEach((path) => {
337
- if (!relations[path]) return;
338
- if (seen.has(path)) return;
339
- seen.add(path);
340
- filtered.push(path);
341
- });
342
- return filtered;
343
- }
344
-
345
- function collectRelationPathsByCategory() {
346
- const relations = PDS?.configRelations || {};
347
- const result = {};
348
- Object.keys(relations).forEach((path) => {
349
- const [category] = path.split(".");
350
- if (!category) return;
351
- if (!result[category]) result[category] = [];
352
- result[category].push(path);
353
- });
354
- return result;
355
- }
356
-
357
- function collectPathsFromRelations(target) {
358
- const relations = PDS?.configRelations || {};
359
- const paths = [];
360
- Object.entries(relations).forEach(([path, relation]) => {
361
- const rules = relation?.rules || [];
362
- if (!Array.isArray(rules) || !rules.length) return;
363
- const matches = rules.some((rule) => {
364
- const selectors = rule?.selectors || [];
365
- return selectors.some((selector) => {
366
- try {
367
- return target.matches(selector) || Boolean(target.querySelector(selector));
368
- } catch (e) {
369
- return false;
370
- }
371
- });
372
- });
373
- if (matches) paths.push(path);
374
- });
375
- return paths;
376
- }
377
-
378
- function collectQuickRulePaths(target) {
379
- return QUICK_RULES.filter((rule) => {
380
- try {
381
- return target.matches(rule.selector);
382
- } catch (e) {
383
- return false;
384
- }
385
- }).flatMap((rule) => rule.paths);
386
- }
387
-
388
- function filterPathsByContext(target, paths) {
389
- if (!target || !paths.length) return paths;
390
- const isGlobal = target.matches("body, main");
391
- const isInForm = Boolean(target.closest("form, pds-form"));
392
- const isOnSurface = Boolean(
393
- target.closest(
394
- ".card, .surface-base, .surface-elevated, .surface-sunken, .surface-subtle"
395
- )
396
- );
397
- const theme = getActiveTheme();
398
-
399
- return paths.filter((path) => {
400
- if (!theme.isDark && path.includes(DARK_MODE_PATH_MARKER)) return false;
401
- if (path.startsWith("typography.") && !isGlobal) return false;
402
- if (GLOBAL_LAYOUT_PATHS.has(path) && !isGlobal) return false;
403
- if (FORM_CONTEXT_PATHS.has(path) && !isInForm) return false;
404
- if (SURFACE_CONTEXT_PATHS.has(path) && !(isOnSurface || isGlobal)) return false;
405
- return true;
406
- });
407
- }
408
-
409
- function getActiveTheme() {
410
- if (typeof document === "undefined") return { value: "light", isDark: false };
411
- const attr = document.documentElement?.getAttribute("data-theme");
412
- const value = attr === "dark" ? "dark" : "light";
413
- return { value, isDark: value === "dark" };
414
- }
415
-
416
- function getSpacingOffset() {
417
- if (typeof window === "undefined" || typeof document === "undefined") return 8;
418
- const value = window
419
- .getComputedStyle(document.documentElement)
420
- .getPropertyValue("--spacing-2")
421
- .trim();
422
- const parsed = Number.parseFloat(value);
423
- if (Number.isNaN(parsed)) return 8;
424
- return parsed;
425
- }
426
-
427
- function ensureColorNormalizer() {
428
- if (typeof document === "undefined") return null;
429
- if (colorNormalizer && document.contains(colorNormalizer)) return colorNormalizer;
430
- if (!document.body) return null;
431
- const probe = document.createElement("span");
432
- probe.setAttribute("data-pds-color-normalizer", "");
433
- probe.style.position = "fixed";
434
- probe.style.left = "-9999px";
435
- probe.style.top = "0";
436
- probe.style.opacity = "0";
437
- probe.style.pointerEvents = "none";
438
- document.body.appendChild(probe);
439
- colorNormalizer = probe;
440
- return colorNormalizer;
441
- }
442
-
443
- function normalizeCssColor(value) {
444
- if (!value || typeof window === "undefined") return null;
445
- const probe = ensureColorNormalizer();
446
- if (!probe) return null;
447
- probe.style.color = "";
448
- probe.style.color = value;
449
- return window.getComputedStyle(probe).color || null;
450
- }
451
-
452
- function rgbToHex(value) {
453
- if (!value) return null;
454
- const match = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
455
- if (!match) return null;
456
- const [r, g, b] = match.slice(1, 4).map((num) => {
457
- const parsed = Number.parseInt(num, 10);
458
- if (Number.isNaN(parsed)) return 0;
459
- return Math.max(0, Math.min(255, parsed));
460
- });
461
- const hex = (channel) => channel.toString(16).padStart(2, "0");
462
- return `#${hex(r)}${hex(g)}${hex(b)}`;
463
- }
464
-
465
- function normalizeHexColor(value) {
466
- if (!value) return null;
467
- const trimmed = value.trim();
468
- if (!trimmed.startsWith("#")) return null;
469
- if (trimmed.length === 4) {
470
- const [r, g, b] = trimmed.slice(1).split("");
471
- return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
472
- }
473
- if (trimmed.length === 7) return trimmed.toLowerCase();
474
- return null;
475
- }
476
-
477
- function toColorInputValue(value) {
478
- if (!value) return value;
479
- const normalizedHex = normalizeHexColor(value);
480
- if (normalizedHex) return normalizedHex;
481
- const normalized = normalizeCssColor(value) || value;
482
- const hexValue = normalizeHexColor(normalized) || rgbToHex(normalized);
483
- return hexValue || value;
484
- }
485
-
486
- function getCustomPropertyNames(style) {
487
- const names = [];
488
- if (!style) return names;
489
- for (let i = 0; i < style.length; i += 1) {
490
- const name = style[i];
491
- if (name && name.startsWith("--")) names.push(name);
492
- }
493
- return names;
494
- }
495
-
496
- function makeTokenMatchers(relations) {
497
- const matchers = [];
498
- Object.entries(relations || {}).forEach(([path, relation]) => {
499
- const tokens = relation?.tokens || [];
500
- if (!Array.isArray(tokens)) return;
501
- tokens.forEach((pattern) => {
502
- if (!pattern || typeof pattern !== "string") return;
503
- const escaped = pattern
504
- .replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&")
505
- .replace(/\*/g, ".*");
506
- const regex = new RegExp(`^${escaped}$`);
507
- matchers.push({ path, regex });
508
- });
509
- });
510
- return matchers;
511
- }
512
-
513
- function addToValueMap(map, key, value) {
514
- if (!key) return;
515
- const entry = map.get(key);
516
- if (entry) {
517
- entry.add(value);
518
- } else {
519
- map.set(key, new Set([value]));
520
- }
521
- }
522
-
523
- function getTokenIndex() {
524
- if (typeof window === "undefined" || typeof document === "undefined") return null;
525
- const relations = PDS?.configRelations || {};
526
- const relationCount = Object.keys(relations).length;
527
- const root = document.documentElement;
528
- if (!root) return null;
529
- const rootStyle = window.getComputedStyle(root);
530
- const bodyStyle = document.body ? window.getComputedStyle(document.body) : null;
531
- const customProps = Array.from(
532
- new Set([...
533
- getCustomPropertyNames(rootStyle),
534
- ...getCustomPropertyNames(bodyStyle),
535
- ])
536
- );
537
- const meta = { relationCount, propCount: customProps.length };
538
- if (cachedTokenIndex && cachedTokenIndexMeta) {
539
- if (
540
- cachedTokenIndexMeta.relationCount === meta.relationCount &&
541
- cachedTokenIndexMeta.propCount === meta.propCount
542
- ) {
543
- return cachedTokenIndex;
544
- }
545
- }
546
-
547
- const matchers = makeTokenMatchers(relations);
548
- const varToPaths = new Map();
549
- const valueToVars = new Map();
550
- const colorToVars = new Map();
551
-
552
- const getVarValue = (varName) => {
553
- const rootValue = rootStyle.getPropertyValue(varName).trim();
554
- if (rootValue) return rootValue;
555
- if (!bodyStyle) return "";
556
- return bodyStyle.getPropertyValue(varName).trim();
557
- };
558
-
559
- customProps.forEach((varName) => {
560
- const varValue = getVarValue(varName);
561
- if (varValue) {
562
- addToValueMap(valueToVars, varValue, varName);
563
- const normalizedColor = normalizeCssColor(varValue);
564
- if (normalizedColor) addToValueMap(colorToVars, normalizedColor, varName);
565
- }
566
- matchers.forEach(({ path, regex }) => {
567
- if (regex.test(varName)) {
568
- addToValueMap(varToPaths, varName, path);
569
- }
570
- });
571
- });
572
-
573
- cachedTokenIndex = { varToPaths, valueToVars, colorToVars, matchers, getVarValue };
574
- cachedTokenIndexMeta = meta;
575
- return cachedTokenIndex;
576
- }
577
-
578
- function collectVarRefsFromInline(element) {
579
- const vars = new Set();
580
- if (!element || typeof element.getAttribute !== "function") return vars;
581
- const styleAttr = element.getAttribute("style") || "";
582
- if (!styleAttr) return vars;
583
- INLINE_VAR_REGEX.lastIndex = 0;
584
- let match = INLINE_VAR_REGEX.exec(styleAttr);
585
- while (match) {
586
- if (match[1]) vars.add(match[1]);
587
- match = INLINE_VAR_REGEX.exec(styleAttr);
588
- }
589
- return vars;
590
- }
591
-
592
- function collectScanTargets(target, limit = 120) {
593
- const nodes = [target];
594
- if (!target || typeof target.querySelectorAll !== "function") return nodes;
595
- const descendants = Array.from(target.querySelectorAll("*"));
596
- if (descendants.length <= limit) return nodes.concat(descendants);
597
- return nodes.concat(descendants.slice(0, limit));
598
- }
599
-
600
- function collectPathsFromComputedStyles(target) {
601
- const index = getTokenIndex();
602
- if (!index || !target || typeof window === "undefined") return [];
603
- const { varToPaths, valueToVars, colorToVars, matchers, getVarValue } = index;
604
- const varsSeen = new Set();
605
- const varsOrdered = [];
606
- const scanTargets = collectScanTargets(target);
607
- const addVarName = (name) => {
608
- if (!name || varsSeen.has(name)) return;
609
- varsSeen.add(name);
610
- varsOrdered.push(name);
611
- };
612
- const addVarSet = (set) => {
613
- set.forEach((name) => addVarName(name));
614
- };
615
-
616
- scanTargets.forEach((node) => {
617
- addVarSet(collectVarRefsFromInline(node));
618
-
619
- let style = null;
620
- try {
621
- style = window.getComputedStyle(node);
622
- } catch (e) {
623
- style = null;
624
- }
625
- if (!style) return;
626
-
627
- QUICK_STYLE_PROPERTIES.forEach((prop) => {
628
- const value = style.getPropertyValue(prop);
629
- if (!value) return;
630
- const trimmed = value.trim();
631
- if (trimmed && valueToVars.has(trimmed)) {
632
- valueToVars.get(trimmed).forEach((varName) => addVarName(varName));
633
- }
634
-
635
- const colors = trimmed.match(COLOR_VALUE_REGEX) || [];
636
- colors.forEach((color) => {
637
- const normalized = normalizeCssColor(color) || color;
638
- if (colorToVars.has(normalized)) {
639
- colorToVars.get(normalized).forEach((varName) => addVarName(varName));
640
- }
641
- });
642
- });
643
- });
644
-
645
- const paths = [];
646
- const seenPaths = new Set();
647
- const hints = {};
648
- const addPath = (path) => {
649
- if (!path || seenPaths.has(path)) return;
650
- seenPaths.add(path);
651
- paths.push(path);
652
- };
653
- const addHint = (path, varName) => {
654
- if (!path || !varName) return;
655
- if (!path.startsWith("colors.")) return;
656
- if (path.includes(DARK_MODE_PATH_MARKER) && !getActiveTheme().isDark) return;
657
- if (hints[path]) return;
658
- const rawValue = getVarValue ? getVarValue(varName) : "";
659
- if (!rawValue) return;
660
- const normalized = toColorInputValue(rawValue);
661
- hints[path] = normalized;
662
- };
663
- const matchVarName = (varName) => {
664
- const direct = varToPaths.get(varName);
665
- if (direct) {
666
- direct.forEach((path) => {
667
- addPath(path);
668
- addHint(path, varName);
669
- });
670
- return;
671
- }
672
- (matchers || []).forEach(({ path, regex }) => {
673
- if (regex.test(varName)) {
674
- addPath(path);
675
- addHint(path, varName);
676
- }
677
- });
678
- };
679
-
680
- varsOrdered.forEach((varName) => matchVarName(varName));
681
-
682
- return { paths: normalizePaths(paths), hints, debug: { vars: varsOrdered, paths } };
683
- }
684
-
685
- function collectQuickContext(target) {
686
- const computed = collectPathsFromComputedStyles(target);
687
- const byComputed = computed?.paths || [];
688
- const byRelations = collectPathsFromRelations(target);
689
- const byQuickRules = collectQuickRulePaths(target);
690
- const hints = computed?.hints || {};
691
- const debug = computed?.debug || { vars: [], paths: [] };
692
-
693
- const filtered = filterPathsByContext(target, [
694
- ...byComputed,
695
- ...byRelations,
696
- ...byQuickRules,
697
- ]);
698
- if (!filtered.length) {
699
- return {
700
- paths: normalizePaths(DEFAULT_QUICK_PATHS),
701
- hints: {},
702
- debug: { vars: [], paths: [] },
703
- };
704
- }
705
- return { paths: normalizePaths(filtered), hints, debug };
706
- }
707
-
708
- function collectDrawerPaths(quickPaths) {
709
- const categories = new Set();
710
- quickPaths.forEach((path) => categories.add(path.split(".")[0]));
711
- const expanded = [];
712
- const relationMap = collectRelationPathsByCategory();
713
- categories.forEach((category) => {
714
- const fields = relationMap[category] || [];
715
- expanded.push(...fields);
716
- });
717
- return normalizePaths([...quickPaths, ...expanded]);
718
- }
719
-
720
- function buildSchemaFromPaths(paths, design, hints = {}) {
721
- const schema = { type: "object", properties: {} };
722
- const uiSchema = {};
723
-
724
- const getStepFromValue = (value) => {
725
- if (typeof value !== "number" || Number.isNaN(value)) return null;
726
- const parts = String(value).split(".");
727
- if (parts.length < 2) return 1;
728
- const decimals = parts[1].length;
729
- return Number(`0.${"1".padStart(decimals, "0")}`);
730
- };
731
-
732
- const inferRangeBounds = (path, value) => {
733
- const hint = String(path || "").toLowerCase();
734
- if (hint.includes("opacity")) return { min: 0, max: 1, step: 0.01 };
735
- if (hint.includes("scale") || hint.includes("ratio")) {
736
- return { min: 1, max: 2, step: 0.01 };
737
- }
738
- if (
739
- hint.includes("size") ||
740
- hint.includes("radius") ||
741
- hint.includes("padding") ||
742
- hint.includes("gap") ||
743
- hint.includes("spacing") ||
744
- hint.includes("width") ||
745
- hint.includes("height") ||
746
- hint.includes("shadow")
747
- ) {
748
- return { min: 0, max: 64, step: 1 };
749
- }
750
- if (typeof value === "number") {
751
- if (value >= 0 && value <= 1) return { min: 0, max: 1, step: 0.01 };
752
- const magnitude = Math.max(1, Math.abs(value));
753
- const upper = Math.max(10, Math.ceil(magnitude * 4));
754
- return { min: 0, max: upper, step: getStepFromValue(value) || 1 };
755
- }
756
- return { min: 0, max: 100, step: 1 };
757
- };
758
-
759
- const isColorValue = (value, path) => {
760
- if (String(path || "").toLowerCase().startsWith("colors.")) return true;
761
- if (typeof value !== "string") return false;
762
- return /^#([0-9a-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
763
- };
764
-
765
- paths.forEach((path) => {
766
- const segments = path.split(".");
767
- const [category, ...rest] = segments;
768
- if (!category || !rest.length) return;
769
-
770
- let parent = schema.properties[category];
771
- if (!parent) {
772
- parent = { type: "object", title: titleize(category), properties: {} };
773
- schema.properties[category] = parent;
774
- }
775
-
776
- let current = parent;
777
- for (let i = 0; i < rest.length; i += 1) {
778
- const segment = rest[i];
779
- if (i === rest.length - 1) {
780
- const value = getValueAtPath(design, [category, ...rest]);
781
- const hintValue = hints[path];
782
- const inferredType = Array.isArray(value)
783
- ? "array"
784
- : value === null
785
- ? "string"
786
- : typeof value;
787
- const schemaType = inferredType === "number" || inferredType === "boolean"
788
- ? inferredType
789
- : "string";
790
- current.properties[segment] = {
791
- type: schemaType,
792
- title: titleize(segment),
793
- examples:
794
- value !== undefined && value !== null
795
- ? [value]
796
- : hintValue !== undefined
797
- ? [hintValue]
798
- : undefined,
799
- };
800
-
801
- const pointer = `/${[category, ...rest].join("/")}`;
802
- const uiEntry = {
803
- "ui:icon": CATEGORY_ICONS[category] || "sparkle",
804
- };
805
-
806
- if (isColorValue(value, path)) {
807
- uiEntry["ui:widget"] = "input-color";
808
- } else if (schemaType === "number") {
809
- const bounds = inferRangeBounds(path, value);
810
- uiEntry["ui:widget"] = "input-range";
811
- uiEntry["ui:min"] = bounds.min;
812
- uiEntry["ui:max"] = bounds.max;
813
- uiEntry["ui:step"] = bounds.step;
814
- }
815
-
816
- uiSchema[pointer] = uiEntry;
817
- return;
818
- }
819
-
820
- if (!current.properties[segment]) {
821
- current.properties[segment] = { type: "object", properties: {} };
822
- }
823
- current = current.properties[segment];
824
- }
825
- });
826
-
827
- return { schema, uiSchema };
828
- }
829
-
830
- async function getGeneratorClass() {
831
- if (!PDS?.getGenerator) return null;
832
- try {
833
- return await PDS.getGenerator();
834
- } catch (e) {
835
- return null;
836
- }
837
- }
838
-
839
- async function applyDesignPatch(patch) {
840
- if (!patch || typeof patch !== "object") return;
841
- const Generator = await getGeneratorClass();
842
- const generator = Generator?.instance;
843
- if (!generator || !generator.options) return;
844
-
845
- const presetKeyMatches = (key, compareTo) => {
846
- if (!key || !compareTo) return false;
847
- return slugifyPreset(key) === slugifyPreset(compareTo);
848
- };
849
-
850
- const slugifyPreset = (value) =>
851
- String(value || "")
852
- .toLowerCase()
853
- .replace(/&/g, " and ")
854
- .replace(/[^a-z0-9]+/g, "-")
855
- .replace(/^-+|-+$/g, "");
856
-
857
- const resolvePresetBase = (presetId) => {
858
- const presets = PDS?.presets || {};
859
- if (!presetId) return { id: null, preset: null };
860
- if (presets[presetId]) {
861
- return { id: presetId, preset: presets[presetId] };
862
- }
863
- const presetKeys = Object.keys(presets || {});
864
- const matchedKey = presetKeys.find((key) => presetKeyMatches(key, presetId));
865
- if (matchedKey) {
866
- return { id: matchedKey, preset: presets[matchedKey] };
867
- }
868
- const found = Object.values(presets).find((preset) => {
869
- const name = preset?.name || preset?.id || "";
870
- return presetKeyMatches(name, presetId);
871
- });
872
- if (found) {
873
- const foundId = found.id || found.name || presetId;
874
- return { id: foundId, preset: found };
875
- }
876
- return { id: presetId, preset: null };
877
- };
878
-
879
- const currentOptions = generator.options;
880
- let storedConfig = null;
881
- if (typeof window !== "undefined" && window.localStorage) {
882
- try {
883
- const raw = window.localStorage.getItem("pure-ds-config");
884
- if (raw) {
885
- const parsed = JSON.parse(raw);
886
- if (parsed && ("preset" in parsed || "design" in parsed)) {
887
- storedConfig = parsed;
888
- }
889
- }
890
- } catch (e) {
891
- storedConfig = null;
892
- }
893
- }
894
-
895
- const storedPreset = storedConfig?.preset;
896
- const hasStoredPreset = Boolean(storedPreset);
897
- const storedOverrides =
898
- storedConfig && storedConfig.design && typeof storedConfig.design === "object"
899
- ? storedConfig.design
900
- : {};
901
- let presetId = storedPreset || currentOptions.preset || PDS?.currentConfig?.preset || null;
902
- const inferredPreset = currentOptions.design?.id || currentOptions.design?.name || null;
903
- if (!presetId && inferredPreset && !hasStoredPreset) {
904
- const inferredMatch = resolvePresetBase(inferredPreset);
905
- if (inferredMatch?.preset) presetId = inferredMatch.id;
906
- }
907
- if (String(presetId || "").toLowerCase() === "default" && inferredPreset && !hasStoredPreset) {
908
- const inferredMatch = resolvePresetBase(inferredPreset);
909
- if (inferredMatch?.preset) presetId = inferredMatch.id;
910
- }
911
-
912
- const resolvedPreset = resolvePresetBase(presetId);
913
- const resolvedPresetId = resolvedPreset.id || presetId || null;
914
- const presetBase = resolvedPreset.preset || null;
915
-
916
- const baseDesign = presetBase
917
- ? deepMerge(shallowClone(presetBase), storedOverrides)
918
- : shallowClone(currentOptions.design || {});
919
- const nextDesign = deepMerge(shallowClone(baseDesign), patch);
920
- const nextOptions = { ...currentOptions, design: nextDesign };
921
- if (resolvedPresetId) nextOptions.preset = resolvedPresetId;
922
-
923
- const nextGenerator = new Generator(nextOptions);
924
- if (PDS?.applyStyles) {
925
- await PDS.applyStyles(nextGenerator);
926
- }
927
-
928
- if (PDS) {
929
- try {
930
- PDS.currentConfig = Object.freeze({
931
- ...(PDS.currentConfig || {}),
932
- design: structuredClone(nextDesign),
933
- preset: resolvedPresetId || PDS.currentConfig?.preset,
934
- });
935
- } catch (e) {
936
- PDS.currentConfig = {
937
- ...(PDS.currentConfig || {}),
938
- design: nextDesign,
939
- preset: resolvedPresetId || PDS.currentConfig?.preset,
940
- };
941
- }
942
-
943
- try {
944
- const event = new CustomEvent("design-updated", {
945
- detail: { config: nextDesign },
946
- });
947
- PDS.dispatchEvent(event);
948
- } catch (e) {}
949
- }
950
-
951
- if (typeof window !== "undefined" && window.localStorage) {
952
- try {
953
- const nextStored = {
954
- preset: resolvedPresetId || null,
955
- design: shallowClone(nextDesign),
956
- };
957
- window.localStorage.setItem("pure-ds-config", JSON.stringify(nextStored));
958
- } catch (e) {}
959
- }
960
- }
961
-
962
- function getStoredConfig() {
963
- if (typeof window === "undefined" || !window.localStorage) return null;
964
- try {
965
- const raw = window.localStorage.getItem("pure-ds-config");
966
- if (!raw) return null;
967
- const parsed = JSON.parse(raw);
968
- if (parsed && ("preset" in parsed || "design" in parsed)) return parsed;
969
- } catch (e) {
970
- return null;
971
- }
972
- return null;
973
- }
974
-
975
- function setStoredConfig(nextConfig) {
976
- if (typeof window === "undefined" || !window.localStorage) return;
977
- try {
978
- window.localStorage.setItem("pure-ds-config", JSON.stringify(nextConfig));
979
- } catch (e) {}
980
- }
981
-
982
- function getPresetOptions() {
983
- const presets = PDS?.presets || {};
984
- return Object.values(presets)
985
- .map((preset) => ({
986
- id: preset?.id || preset?.name,
987
- name: preset?.name || preset?.id || "Unnamed",
988
- }))
989
- .filter((preset) => preset.id)
990
- .sort((a, b) => String(a.name).localeCompare(String(b.name)));
991
- }
992
-
993
- function getActivePresetId() {
994
- const stored = getStoredConfig();
995
- return stored?.preset || PDS?.currentConfig?.preset || PDS?.currentPreset || null;
996
- }
997
-
998
- async function applyPresetSelection(presetId) {
999
- if (!presetId) return;
1000
- setStoredConfig({
1001
- preset: presetId,
1002
- design: {},
1003
- });
1004
- await applyDesignPatch({});
1005
- }
1006
-
1007
- function setFormSchemas(form, schema, uiSchema, design) {
1008
- form.jsonSchema = schema;
1009
- form.uiSchema = uiSchema;
1010
- form.values = shallowClone(design);
1011
- }
1012
-
1013
- async function buildForm(paths, design, onChange, hints = {}) {
1014
- const { schema, uiSchema } = buildSchemaFromPaths(paths, design, hints);
1015
- const form = document.createElement("pds-form");
1016
- form.setAttribute("hide-actions", "");
1017
- form.options = {
1018
- layouts: {
1019
- arrays: "compact",
1020
- },
1021
- enhancements: {
1022
- rangeOutput: true,
1023
- },
1024
- };
1025
- form.addEventListener("pw:value-change", onChange);
1026
- const values = shallowClone(design || {});
1027
- Object.entries(hints || {}).forEach(([path, hintValue]) => {
1028
- const segments = path.split(".");
1029
- const currentValue = getValueAtPath(values, segments);
1030
- if (currentValue === undefined || currentValue === null) {
1031
- setValueAtPath(values, segments, hintValue);
1032
- }
1033
- });
1034
- setFormSchemas(form, schema, uiSchema, values);
1035
-
1036
- if (!customElements.get("pds-form")) {
1037
- customElements.whenDefined("pds-form").then(() => {
1038
- setFormSchemas(form, schema, uiSchema, values);
1039
- });
1040
- }
1041
-
1042
- return form;
1043
- }
1044
-
1045
- class PdsLiveEdit extends HTMLElement {
1046
- constructor() {
1047
- super();
1048
- this._boundMouseOver = this._handleMouseOver.bind(this);
1049
- this._boundMouseOut = this._handleMouseOut.bind(this);
1050
- this._boundMouseMove = this._handleMouseMove.bind(this);
1051
- this._boundReposition = this._repositionDropdown.bind(this);
1052
- this._activeTarget = null;
1053
- this._activeDropdown = null;
1054
- this._holdOpen = false;
1055
- this._closeTimer = null;
1056
- this._drawer = null;
1057
- this._pendingPatch = null;
1058
- this._applyTimer = null;
1059
- this._selectors = null;
1060
- this._lastPointer = null;
1061
- this._boundDocPointer = this._handleDocumentPointer.bind(this);
1062
- this._boundDocKeydown = this._handleDocumentKeydown.bind(this);
1063
- this._connected = false;
1064
- }
1065
-
1066
- connectedCallback() {
1067
- if (this._connected) return;
1068
- if (PdsLiveEdit._activeInstance && PdsLiveEdit._activeInstance !== this) {
1069
- PdsLiveEdit._activeInstance._teardown();
1070
- }
1071
- PdsLiveEdit._activeInstance = this;
1072
- this._connected = true;
1073
- if (!isHoverCapable()) return;
1074
-
1075
- ensureStyles();
1076
- this._selectors = collectSelectors();
1077
- document.addEventListener("mouseover", this._boundMouseOver, true);
1078
- document.addEventListener("mouseout", this._boundMouseOut, true);
1079
- document.addEventListener("mousemove", this._boundMouseMove, true);
1080
- }
1081
-
1082
- disconnectedCallback() {
1083
- this._teardown();
1084
- }
1085
-
1086
- _teardown() {
1087
- if (this._connected) {
1088
- document.removeEventListener("mouseover", this._boundMouseOver, true);
1089
- document.removeEventListener("mouseout", this._boundMouseOut, true);
1090
- document.removeEventListener("mousemove", this._boundMouseMove, true);
1091
- }
1092
- this._removeRepositionListeners();
1093
- this._clearCloseTimer();
1094
- this._removeActiveUI();
1095
- this._connected = false;
1096
- if (PdsLiveEdit._activeInstance === this) {
1097
- PdsLiveEdit._activeInstance = null;
1098
- }
1099
- }
1100
-
1101
- _handleMouseOver(event) {
1102
- if (!event?.target || !(event.target instanceof Element)) return;
1103
- this._clearCloseTimer();
1104
- if (this._activeDropdown && this._activeDropdown.contains(event.target)) return;
1105
- const target = this._findEditableTarget(event.target);
1106
- if (!target || target === this._activeTarget) return;
1107
-
1108
- this._removeActiveUI();
1109
- this._showForTarget(target);
1110
- }
1111
-
1112
- _handleMouseOut(event) {
1113
- if (!this._activeTarget) return;
1114
- }
1115
-
1116
- _findEditableTarget(node) {
1117
- const tag = node.tagName?.toLowerCase?.();
1118
- if (tag && ["html", "head", "meta", "link", "style", "script", "title"].includes(tag)) {
1119
- return null;
1120
- }
1121
- if (!this._selectors?.selector) return null;
1122
- if (node.closest(EDITOR_TAG)) return null;
1123
- if (node.closest(`.${DROPDOWN_CLASS}`)) return null;
1124
- if (node.closest("pds-drawer")) return null;
1125
-
1126
- try {
1127
- return node.closest(this._selectors.selector);
1128
- } catch (e) {
1129
- return null;
1130
- }
1131
- }
1132
-
1133
- _showForTarget(target) {
1134
- const quickContext = collectQuickContext(target);
1135
- const quickPaths = quickContext.paths;
1136
- if (!quickPaths.length) return;
1137
-
1138
- this._holdOpen = true;
1139
-
1140
- target.setAttribute(TARGET_ATTR, "true");
1141
- const dropdown = this._buildDropdown(
1142
- target,
1143
- quickPaths,
1144
- quickContext.hints,
1145
- quickContext.debug
1146
- );
1147
- document.body.appendChild(dropdown);
1148
- this._positionDropdown(target, dropdown);
1149
- this._addRepositionListeners();
1150
- this._addDocumentListeners();
1151
-
1152
- this._activeTarget = target;
1153
- this._activeDropdown = dropdown;
1154
- }
1155
-
1156
- _removeActiveUI() {
1157
- this._clearCloseTimer();
1158
- this._removeRepositionListeners();
1159
- this._removeDocumentListeners();
1160
- if (this._activeDropdown && this._activeDropdown.parentNode) {
1161
- this._activeDropdown.parentNode.removeChild(this._activeDropdown);
1162
- }
1163
- if (this._activeTarget) {
1164
- this._activeTarget.removeAttribute(TARGET_ATTR);
1165
- }
1166
- this._activeTarget = null;
1167
- this._activeDropdown = null;
1168
- this._holdOpen = false;
1169
- }
1170
-
1171
- _addDocumentListeners() {
1172
- if (typeof document === "undefined") return;
1173
- document.addEventListener("pointerdown", this._boundDocPointer, true);
1174
- document.addEventListener("keydown", this._boundDocKeydown, true);
1175
- }
1176
-
1177
- _removeDocumentListeners() {
1178
- if (typeof document === "undefined") return;
1179
- document.removeEventListener("pointerdown", this._boundDocPointer, true);
1180
- document.removeEventListener("keydown", this._boundDocKeydown, true);
1181
- }
1182
-
1183
- _handleDocumentPointer(event) {
1184
- if (!this._activeDropdown || !this._activeTarget) return;
1185
- const target = event?.target;
1186
- if (!(target instanceof Element)) return;
1187
- if (this._activeDropdown.contains(target)) return;
1188
- if (this._activeTarget.contains(target)) return;
1189
- this._removeActiveUI();
1190
- }
1191
-
1192
- _handleDocumentKeydown(event) {
1193
- if (!event) return;
1194
- if (event.key !== "Escape") return;
1195
- event.preventDefault();
1196
- this._removeActiveUI();
1197
- }
1198
-
1199
- _scheduleClose() {
1200
- if (typeof window === "undefined") return;
1201
- this._clearCloseTimer();
1202
- this._closeTimer = window.setTimeout(() => {
1203
- if (this._holdOpen) return;
1204
- if (this._activeDropdown && this._activeDropdown.matches(":hover")) return;
1205
- if (this._activeTarget && this._activeTarget.matches(":hover")) return;
1206
- if (this._isPointerWithinSafeZone()) {
1207
- this._scheduleClose();
1208
- return;
1209
- }
1210
- this._removeActiveUI();
1211
- }, 500);
1212
- }
1213
-
1214
- _clearCloseTimer() {
1215
- if (this._closeTimer) {
1216
- clearTimeout(this._closeTimer);
1217
- this._closeTimer = null;
1218
- }
1219
- }
1220
-
1221
- _addRepositionListeners() {
1222
- if (typeof window === "undefined") return;
1223
- window.addEventListener("scroll", this._boundReposition, true);
1224
- window.addEventListener("resize", this._boundReposition);
1225
- }
1226
-
1227
- _removeRepositionListeners() {
1228
- if (typeof window === "undefined") return;
1229
- window.removeEventListener("scroll", this._boundReposition, true);
1230
- window.removeEventListener("resize", this._boundReposition);
1231
- }
1232
-
1233
- _repositionDropdown() {
1234
- if (!this._activeTarget || !this._activeDropdown) return;
1235
- if (!document.contains(this._activeTarget)) {
1236
- this._removeActiveUI();
1237
- return;
1238
- }
1239
- this._positionDropdown(this._activeTarget, this._activeDropdown);
1240
- }
1241
-
1242
- _positionDropdown(target, dropdown) {
1243
- if (!target || !dropdown) return;
1244
- const rect = target.getBoundingClientRect();
1245
- const spacing = getSpacingOffset();
1246
- const width = Math.max(dropdown.offsetWidth || 0, 160);
1247
- const height = Math.max(dropdown.offsetHeight || 0, 120);
1248
- const spaceRight = Math.max(0, window.innerWidth - rect.right);
1249
- const spaceLeft = Math.max(0, rect.left);
1250
- const spaceBelow = Math.max(0, window.innerHeight - rect.bottom);
1251
- const spaceAbove = Math.max(0, rect.top);
1252
-
1253
- const alignRight = spaceRight >= width || spaceRight >= spaceLeft;
1254
- const alignBottom = spaceBelow >= height || spaceBelow >= spaceAbove;
1255
-
1256
- const rightOffset = Math.max(0, window.innerWidth - rect.right);
1257
- const bottomOffset = Math.max(0, window.innerHeight - rect.bottom);
1258
-
1259
- dropdown.style.setProperty(
1260
- "--pds-live-edit-left",
1261
- alignRight ? `${rect.left + spacing}px` : "auto"
1262
- );
1263
- dropdown.style.setProperty(
1264
- "--pds-live-edit-right",
1265
- alignRight ? "auto" : `${rightOffset + spacing}px`
1266
- );
1267
- dropdown.style.setProperty(
1268
- "--pds-live-edit-top",
1269
- alignBottom ? `${rect.top + spacing}px` : "auto"
1270
- );
1271
- dropdown.style.setProperty(
1272
- "--pds-live-edit-bottom",
1273
- alignBottom ? "auto" : `${bottomOffset + spacing}px`
1274
- );
1275
-
1276
- const adjusted = dropdown.getBoundingClientRect();
1277
- let shiftX = 0;
1278
- let shiftY = 0;
1279
- if (adjusted.left < DROPDOWN_VIEWPORT_PADDING) {
1280
- shiftX = DROPDOWN_VIEWPORT_PADDING - adjusted.left;
1281
- } else if (adjusted.right > window.innerWidth - DROPDOWN_VIEWPORT_PADDING) {
1282
- shiftX = window.innerWidth - DROPDOWN_VIEWPORT_PADDING - adjusted.right;
1283
- }
1284
- if (adjusted.top < DROPDOWN_VIEWPORT_PADDING) {
1285
- shiftY = DROPDOWN_VIEWPORT_PADDING - adjusted.top;
1286
- } else if (adjusted.bottom > window.innerHeight - DROPDOWN_VIEWPORT_PADDING) {
1287
- shiftY = window.innerHeight - DROPDOWN_VIEWPORT_PADDING - adjusted.bottom;
1288
- }
1289
- if (shiftX || shiftY) {
1290
- const currentLeft = parseFloat(dropdown.style.getPropertyValue("--pds-live-edit-left"));
1291
- const currentTop = parseFloat(dropdown.style.getPropertyValue("--pds-live-edit-top"));
1292
- const currentRight = parseFloat(dropdown.style.getPropertyValue("--pds-live-edit-right"));
1293
- const currentBottom = parseFloat(dropdown.style.getPropertyValue("--pds-live-edit-bottom"));
1294
- if (!Number.isNaN(currentLeft)) {
1295
- dropdown.style.setProperty("--pds-live-edit-left", `${currentLeft + shiftX}px`);
1296
- } else if (!Number.isNaN(currentRight)) {
1297
- dropdown.style.setProperty("--pds-live-edit-right", `${currentRight - shiftX}px`);
1298
- }
1299
- if (!Number.isNaN(currentTop)) {
1300
- dropdown.style.setProperty("--pds-live-edit-top", `${currentTop + shiftY}px`);
1301
- } else if (!Number.isNaN(currentBottom)) {
1302
- dropdown.style.setProperty("--pds-live-edit-bottom", `${currentBottom - shiftY}px`);
1303
- }
1304
- }
1305
- }
1306
-
1307
- _handleMouseMove(event) {
1308
- if (!event) return;
1309
- this._lastPointer = { x: event.clientX, y: event.clientY };
1310
- }
1311
-
1312
- _isPointerWithinSafeZone() {
1313
- if (!this._lastPointer || !this._activeTarget || !this._activeDropdown) return false;
1314
- const targetRect = this._activeTarget.getBoundingClientRect();
1315
- const dropdownRect = this._activeDropdown.getBoundingClientRect();
1316
- const padding = 12;
1317
- const left = Math.min(targetRect.left, dropdownRect.left) - padding;
1318
- const right = Math.max(targetRect.right, dropdownRect.right) + padding;
1319
- const top = Math.min(targetRect.top, dropdownRect.top) - padding;
1320
- const bottom = Math.max(targetRect.bottom, dropdownRect.bottom) + padding;
1321
- const { x, y } = this._lastPointer;
1322
- return x >= left && x <= right && y >= top && y <= bottom;
1323
- }
1324
-
1325
- _buildDropdown(target, quickPaths, hints, debug) {
1326
- const nav = document.createElement("nav");
1327
- nav.className = DROPDOWN_CLASS;
1328
- nav.setAttribute("data-dropdown", "");
1329
- nav.setAttribute("data-direction", "auto");
1330
- nav.setAttribute("data-mode", "auto");
1331
-
1332
- const button = document.createElement("button");
1333
- button.className = `context-edit btn-primary btn-xs icon-only ${MARKER_CLASS}`;
1334
- button.setAttribute("type", "button");
1335
- button.setAttribute("data-direction", "auto");
1336
- button.setAttribute("aria-label", "Edit design settings");
1337
-
1338
- const icon = document.createElement("pds-icon");
1339
- icon.setAttribute("icon", "pencil");
1340
- icon.setAttribute("size", "sm");
1341
- button.appendChild(icon);
1342
-
1343
- const menu = document.createElement("menu");
1344
- const quickItem = document.createElement("li");
1345
- quickItem.className = "pds-live-editor-menu";
1346
-
1347
- const header = document.createElement("div");
1348
- header.className = "pds-live-editor-header";
1349
-
1350
- const title = document.createElement("span");
1351
- title.className = "pds-live-editor-title";
1352
- title.textContent = "Quick edit";
1353
- header.appendChild(title);
1354
-
1355
- const openButton = document.createElement("button");
1356
- openButton.className = "btn-outline btn-xs icon-only";
1357
- openButton.setAttribute("type", "button");
1358
- openButton.setAttribute("aria-label", "More settings");
1359
- const openIcon = document.createElement("pds-icon");
1360
- openIcon.setAttribute("icon", "gear");
1361
- openIcon.setAttribute("size", "sm");
1362
- openButton.appendChild(openIcon);
1363
- openButton.addEventListener("click", (event) => {
1364
- event.preventDefault();
1365
- event.stopPropagation();
1366
- this._openDrawer(target, quickPaths);
1367
- });
1368
- header.appendChild(openButton);
1369
-
1370
- quickItem.appendChild(header);
1371
-
1372
- const design = shallowClone(PDS?.currentConfig?.design || {});
1373
- const formContainer = document.createElement("div");
1374
- quickItem.appendChild(formContainer);
1375
-
1376
- menu.appendChild(quickItem);
1377
-
1378
- nav.appendChild(button);
1379
- nav.appendChild(menu);
1380
-
1381
- const limitedPaths = quickPaths.slice(0, QUICK_EDIT_LIMIT);
1382
- this._renderQuickForm(formContainer, limitedPaths, design, hints);
1383
-
1384
- if (debug && (debug.vars?.length || debug.paths?.length)) {
1385
- const debugBlock = document.createElement("div");
1386
- debugBlock.className = "pds-live-editor-debug";
1387
- const debugVars = (debug.vars || []).slice(0, 8).join(", ");
1388
- const debugPaths = (debug.paths || []).slice(0, 8).join(", ");
1389
- debugBlock.textContent = `vars: ${debugVars}\npaths: ${debugPaths}`;
1390
- quickItem.appendChild(debugBlock);
1391
- }
1392
-
1393
- return nav;
1394
- }
1395
-
1396
- async _renderQuickForm(container, paths, design, hints) {
1397
- container.replaceChildren();
1398
- const form = await buildForm(paths, design, (event) =>
1399
- this._handleValueChange(event),
1400
- hints
1401
- );
1402
- container.appendChild(form);
1403
- }
1404
-
1405
- async _openDrawer(target, quickPaths) {
1406
- if (!this._drawer) {
1407
- this._drawer = document.createElement("pds-drawer");
1408
- this._drawer.setAttribute("position", "right");
1409
- this._drawer.setAttribute("show-close", "");
1410
- this.appendChild(this._drawer);
1411
- }
1412
-
1413
- if (!customElements.get("pds-drawer")) {
1414
- await customElements.whenDefined("pds-drawer");
1415
- }
1416
-
1417
- const header = document.createElement("div");
1418
- header.setAttribute("slot", "drawer-header");
1419
- header.className = "flex items-center justify-between";
1420
- header.textContent = "Design settings";
1421
-
1422
- const content = document.createElement("div");
1423
- content.setAttribute("slot", "drawer-content");
1424
- content.className = "stack-md";
1425
-
1426
- const presetCard = document.createElement("section");
1427
- presetCard.className = "card surface-elevated stack-sm";
1428
-
1429
- const presetTitle = document.createElement("h4");
1430
- presetTitle.textContent = "Preset";
1431
- presetCard.appendChild(presetTitle);
1432
-
1433
- const presetLabel = document.createElement("label");
1434
- presetLabel.className = "stack-xs";
1435
-
1436
- const presetText = document.createElement("span");
1437
- presetText.textContent = "Choose a base style";
1438
- presetLabel.appendChild(presetText);
1439
-
1440
- const presetSelect = document.createElement("select");
1441
- const presetOptions = getPresetOptions();
1442
- const activePreset = getActivePresetId();
1443
-
1444
- presetOptions.forEach((preset) => {
1445
- const option = document.createElement("option");
1446
- option.value = preset.id;
1447
- option.textContent = preset.name;
1448
- if (String(preset.id) === String(activePreset)) {
1449
- option.selected = true;
1450
- }
1451
- presetSelect.appendChild(option);
1452
- });
1453
-
1454
- presetSelect.addEventListener("change", async (event) => {
1455
- const nextPreset = event.target?.value;
1456
- await applyPresetSelection(nextPreset);
1457
- });
1458
-
1459
- presetLabel.appendChild(presetSelect);
1460
- presetCard.appendChild(presetLabel);
1461
-
1462
- const themeCard = document.createElement("section");
1463
- themeCard.className = "card surface-elevated stack-sm";
1464
-
1465
- const themeTitle = document.createElement("h4");
1466
- themeTitle.textContent = "Theme";
1467
- themeCard.appendChild(themeTitle);
1468
-
1469
- const themeToggle = document.createElement("pds-theme");
1470
- themeCard.appendChild(themeToggle);
1471
-
1472
- const searchCard = document.createElement("section");
1473
- searchCard.className = "card surface-elevated stack-sm";
1474
-
1475
- const searchTitle = document.createElement("h4");
1476
- searchTitle.textContent = "Search PDS";
1477
- searchCard.appendChild(searchTitle);
1478
-
1479
- const omnibox = document.createElement("pds-omnibox");
1480
- omnibox.setAttribute("placeholder", "Search tokens, utilities, components...");
1481
- omnibox.settings = {
1482
- iconHandler: (item) => {
1483
- return item.icon ? `<pds-icon icon="${item.icon}"></pds-icon>` : null;
1484
- },
1485
- categories: {
1486
- Query: {
1487
- trigger: (options) => options.search.length >= 2,
1488
- getItems: async (options) => {
1489
- const query = (options.search || "").trim();
1490
- if (!query) return [];
1491
- try {
1492
- const results = await PDS.query(query);
1493
- return (results || []).map((result) => ({
1494
- text: result.text,
1495
- id: result.value,
1496
- icon: result.icon || "magnifying-glass",
1497
- category: result.category,
1498
- code: result.code,
1499
- }));
1500
- } catch (error) {
1501
- console.warn("Omnibox query failed:", error);
1502
- return [];
1503
- }
1504
- },
1505
- action: async (options) => {
1506
- if (options?.code && navigator.clipboard) {
1507
- await navigator.clipboard.writeText(options.code);
1508
- await PDS.toast("Copied token to clipboard", { type: "success" });
1509
- }
1510
- },
1511
- },
1512
- },
1513
- };
1514
- searchCard.appendChild(omnibox);
1515
-
1516
- content.appendChild(presetCard);
1517
- content.appendChild(themeCard);
1518
- content.appendChild(searchCard);
1519
-
1520
- this._drawer.replaceChildren(header, content);
1521
-
1522
- if (typeof this._drawer.openDrawer === "function") {
1523
- this._drawer.openDrawer();
1524
- } else {
1525
- this._drawer.setAttribute("open", "");
1526
- }
1527
- }
1528
-
1529
- _handleValueChange(event) {
1530
- const form = event?.currentTarget;
1531
- if (!form || typeof form.getValuesFlat !== "function") return;
1532
- const flatValues = form.getValuesFlat();
1533
- const patch = {};
1534
- Object.entries(flatValues || {}).forEach(([path, value]) => {
1535
- setValueAtJsonPath(patch, path, value);
1536
- });
1537
- this._schedulePatch(patch);
1538
- }
1539
-
1540
- _schedulePatch(patch) {
1541
- this._pendingPatch = this._pendingPatch
1542
- ? deepMerge(this._pendingPatch, patch)
1543
- : patch;
1544
-
1545
- if (this._applyTimer) return;
1546
- this._applyTimer = window.setTimeout(async () => {
1547
- const nextPatch = this._pendingPatch;
1548
- this._pendingPatch = null;
1549
- this._applyTimer = null;
1550
- await applyDesignPatch(nextPatch);
1551
- }, 50);
1552
- }
1553
- }
1554
-
1
+ const PDS = globalThis.PDS;
2
+
3
+ const EDITOR_TAG = "pds-live-edit";
4
+ const STYLE_ID = "pds-live-editor-styles";
5
+ const TARGET_ATTR = "data-pds-live-target";
6
+ const DROPDOWN_CLASS = "pds-live-editor-dropdown";
7
+ const MARKER_CLASS = "pds-live-editor-marker";
8
+
9
+ const CATEGORY_ICONS = {
10
+ colors: "palette",
11
+ typography: "text-aa",
12
+ spatialRhythm: "grid-four",
13
+ shape: "circle",
14
+ layout: "grid-four",
15
+ behavior: "sparkle",
16
+ layers: "squares-four",
17
+ icons: "sparkle",
18
+ };
19
+
20
+ const QUICK_RULES = [
21
+ {
22
+ selector: "button, .btn-primary, .btn-secondary, .btn-outline, .btn-sm, .btn-xs, .btn-lg",
23
+ paths: [
24
+ "colors.primary",
25
+ "shape.radiusSize",
26
+ "spatialRhythm.buttonPadding",
27
+ "layout.buttonMinHeight",
28
+ "behavior.transitionSpeed",
29
+ ],
30
+ },
31
+ {
32
+ selector: "input, textarea, select, label",
33
+ paths: [
34
+ "colors.secondary",
35
+ "shape.radiusSize",
36
+ "shape.borderWidth",
37
+ "spatialRhythm.inputPadding",
38
+ "layout.inputMinHeight",
39
+ "typography.fontFamilyBody",
40
+ ],
41
+ },
42
+ {
43
+ selector: ".card, .surface-base, .surface-elevated, .surface-sunken, .surface-subtle",
44
+ paths: [
45
+ "colors.background",
46
+ "shape.radiusSize",
47
+ "layers.baseShadowOpacity",
48
+ ],
49
+ },
50
+ {
51
+ selector: "nav, menu",
52
+ paths: ["colors.background", "typography.fontFamilyBody", "spatialRhythm.baseUnit"],
53
+ },
54
+ {
55
+ selector: "pds-icon",
56
+ paths: ["icons.defaultSize", "icons.weight"],
57
+ },
58
+ ];
59
+
60
+ const DEFAULT_QUICK_PATHS = [
61
+ "colors.primary",
62
+ "typography.fontFamilyBody",
63
+ "shape.radiusSize",
64
+ ];
65
+
66
+ const QUICK_STYLE_PROPERTIES = [
67
+ "background-color",
68
+ "color",
69
+ "border-color",
70
+ "border-top-color",
71
+ "border-right-color",
72
+ "border-bottom-color",
73
+ "border-left-color",
74
+ "outline-color",
75
+ "box-shadow",
76
+ "text-shadow",
77
+ "fill",
78
+ "stroke",
79
+ "font-family",
80
+ "font-size",
81
+ "font-weight",
82
+ "letter-spacing",
83
+ "line-height",
84
+ "border-radius",
85
+ "padding",
86
+ "padding-top",
87
+ "padding-right",
88
+ "padding-bottom",
89
+ "padding-left",
90
+ "gap",
91
+ "row-gap",
92
+ "column-gap",
93
+ "min-height",
94
+ "min-width",
95
+ "height",
96
+ "width",
97
+ ];
98
+
99
+ const INLINE_VAR_REGEX = /var\(\s*(--[^)\s,]+)\s*/g;
100
+ const CUSTOM_PROP_REGEX = /--.+/;
101
+ const COLOR_VALUE_REGEX = /#(?:[0-9a-f]{3,8})\b|rgba?\([^)]*\)|hsla?\([^)]*\)/gi;
102
+
103
+ const ENUM_FIELD_OPTIONS = {
104
+ "shape.borderWidth": ["hairline", "thin", "medium", "thick"],
105
+ };
106
+
107
+ let cachedTokenIndex = null;
108
+ let cachedTokenIndexMeta = null;
109
+ let colorNormalizer = null;
110
+
111
+ const GLOBAL_LAYOUT_PATHS = new Set([
112
+ "layout.maxWidth",
113
+ "layout.maxWidths",
114
+ "layout.breakpoints",
115
+ "layout.containerMaxWidth",
116
+ "layout.containerPadding",
117
+ "layout.gridColumns",
118
+ "layout.gridGutter",
119
+ "layout.densityCompact",
120
+ "layout.densityNormal",
121
+ "layout.densityComfortable",
122
+ ]);
123
+
124
+ const FORM_CONTEXT_PATHS = new Set([
125
+ "spatialRhythm.inputPadding",
126
+ "layout.inputMinHeight",
127
+ "behavior.focusRingWidth",
128
+ "behavior.focusRingOpacity",
129
+ ]);
130
+
131
+ const SURFACE_CONTEXT_PATHS = new Set([
132
+ "colors.background",
133
+ "layers.baseShadowOpacity",
134
+ "layout.baseShadowOpacity",
135
+ ]);
136
+
137
+ const DARK_MODE_PATH_MARKER = ".darkMode.";
138
+ const QUICK_EDIT_LIMIT = 4;
139
+ const DROPDOWN_VIEWPORT_PADDING = 8;
140
+
141
+ function isHoverCapable() {
142
+ if (typeof window === "undefined" || !window.matchMedia) return false;
143
+ return window.matchMedia("(hover: hover) and (pointer: fine)").matches;
144
+ }
145
+
146
+ function ensureStyles() {
147
+ if (typeof document === "undefined") return;
148
+ if (document.getElementById(STYLE_ID)) return;
149
+ const style = document.createElement("style");
150
+ style.id = STYLE_ID;
151
+ style.textContent = `
152
+ ${EDITOR_TAG} {
153
+ display: contents;
154
+ }
155
+ [${TARGET_ATTR}] {
156
+ position: relative;
157
+ outline: 2px solid var(--color-primary-500);
158
+ outline-offset: -2px;
159
+ }
160
+ [${TARGET_ATTR}]::after {
161
+ content: '';
162
+ position: absolute;
163
+ inset: 0;
164
+ background-color: var(--color-primary-500);
165
+ opacity: 0.08;
166
+ pointer-events: none;
167
+ z-index: var(--z-base);
168
+ }
169
+ .${DROPDOWN_CLASS} {
170
+ position: fixed;
171
+ top: var(--pds-live-edit-top, auto);
172
+ right: var(--pds-live-edit-right, auto);
173
+ bottom: var(--pds-live-edit-bottom, auto);
174
+ left: var(--pds-live-edit-left, auto);
175
+ z-index: var(--z-popover);
176
+ }
177
+ .${MARKER_CLASS} {
178
+ pointer-events: auto;
179
+ }
180
+ .context-edit {
181
+ min-width: 0;
182
+ min-height: 0;
183
+ width: var(--spacing-6);
184
+ height: var(--spacing-6);
185
+ padding: 0;
186
+ }
187
+ .${DROPDOWN_CLASS} menu {
188
+ min-width: max-content;
189
+ max-width: 350px;
190
+ }
191
+ .${DROPDOWN_CLASS} .pds-live-editor-menu {
192
+ padding: var(--spacing-1);
193
+ max-width: 350px;
194
+ padding-bottom: 0;
195
+ }
196
+ .${DROPDOWN_CLASS} .pds-live-editor-form-container {
197
+ padding-bottom: var(--spacing-2);
198
+ }
199
+ .${DROPDOWN_CLASS} .pds-live-editor-title {
200
+ display: block;
201
+ font-size: var(--font-size-sm);
202
+ font-weight: var(--font-weight-semibold);
203
+ margin-bottom: var(--spacing-2);
204
+ }
205
+ .${DROPDOWN_CLASS} .pds-live-editor-header {
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: space-between;
209
+ gap: var(--spacing-2);
210
+ }
211
+ .${DROPDOWN_CLASS} .pds-live-editor-debug {
212
+ font-size: var(--font-size-xs);
213
+ opacity: 0.7;
214
+ white-space: pre-wrap;
215
+ margin-top: var(--spacing-2);
216
+ }
217
+ .${DROPDOWN_CLASS} .pds-live-editor-menu input[type="color"] {
218
+ height: var(--spacing-6);
219
+ min-width: var(--spacing-9);
220
+ max-width: unset;
221
+ padding: 0;
222
+ border-radius: var(--radius-sm);
223
+ }
224
+ .${DROPDOWN_CLASS} .pds-live-editor-footer {
225
+ display: flex;
226
+ gap: var(--spacing-2);
227
+ padding: var(--spacing-2);
228
+ border-top: 1px solid var(--color-border);
229
+ background: var(--color-surface-base);
230
+ position: sticky;
231
+ justify-content: space-between;
232
+ bottom: 0;
233
+ z-index: 1;
234
+ }
235
+ `;
236
+ document.head.appendChild(style);
237
+ }
238
+
239
+ function isSelectorSupported(selector) {
240
+ if (typeof selector !== "string" || !selector.trim()) return false;
241
+ if (typeof CSS !== "undefined" && typeof CSS.supports === "function") {
242
+ try {
243
+ return CSS.supports(`selector(${selector})`);
244
+ } catch (e) {
245
+ return false;
246
+ }
247
+ }
248
+ if (typeof document === "undefined") return false;
249
+ try {
250
+ document.querySelector(selector);
251
+ return true;
252
+ } catch (e) {
253
+ return false;
254
+ }
255
+ }
256
+
257
+ function collectSelectors() {
258
+ const ontology = PDS?.ontology;
259
+ const selectors = new Set();
260
+ if (!ontology) return { selector: "", list: [] };
261
+
262
+ const addSelector = (selector) => {
263
+ if (typeof selector !== "string" || !selector.trim()) return;
264
+ if (isSelectorSupported(selector)) selectors.add(selector);
265
+ };
266
+
267
+ const addSelectorList = (list) => {
268
+ (list || []).forEach((selector) => addSelector(selector));
269
+ };
270
+
271
+ const sections = [ontology.primitives, ontology.components, ontology.layoutPatterns];
272
+ sections.forEach((items) => {
273
+ if (!Array.isArray(items)) return;
274
+ items.forEach((item) => {
275
+ addSelectorList(item?.selectors || []);
276
+ });
277
+ });
278
+
279
+ Object.values(ontology.utilities || {}).forEach((group) => {
280
+ if (!group || typeof group !== "object") return;
281
+ Object.values(group).forEach((list) => addSelectorList(list));
282
+ });
283
+
284
+ (ontology.enhancements || []).forEach((enhancer) => {
285
+ addSelector(enhancer?.selector);
286
+ });
287
+
288
+ addSelector("body");
289
+ addSelector("[data-dropdown]");
290
+ addSelector("*");
291
+
292
+ return { selector: Array.from(selectors).join(", "), list: Array.from(selectors) };
293
+ }
294
+
295
+ function shallowClone(value) {
296
+ if (!value || typeof value !== "object") return value;
297
+ return Array.isArray(value) ? [...value] : { ...value };
298
+ }
299
+
300
+ function deepMerge(target = {}, source = {}) {
301
+ if (!source || typeof source !== "object") return target;
302
+ const out = Array.isArray(target) ? [...target] : { ...target };
303
+ for (const [key, value] of Object.entries(source)) {
304
+ if (value && typeof value === "object" && !Array.isArray(value)) {
305
+ out[key] = deepMerge(out[key] && typeof out[key] === "object" ? out[key] : {}, value);
306
+ } else {
307
+ out[key] = value;
308
+ }
309
+ }
310
+ return out;
311
+ }
312
+
313
+ function titleize(value) {
314
+ return String(value)
315
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
316
+ .replace(/[_-]+/g, " ")
317
+ .replace(/\s+/g, " ")
318
+ .trim()
319
+ .replace(/^./, (char) => char.toUpperCase());
320
+ }
321
+
322
+ function getValueAtPath(obj, pathSegments) {
323
+ let current = obj;
324
+ for (const segment of pathSegments) {
325
+ if (!current || typeof current !== "object") return undefined;
326
+ current = current[segment];
327
+ }
328
+ return current;
329
+ }
330
+
331
+ function setValueAtPath(target, pathSegments, value) {
332
+ let current = target;
333
+ for (let i = 0; i < pathSegments.length; i += 1) {
334
+ const segment = pathSegments[i];
335
+ if (i === pathSegments.length - 1) {
336
+ current[segment] = value;
337
+ return;
338
+ }
339
+ if (!current[segment] || typeof current[segment] !== "object") {
340
+ current[segment] = {};
341
+ }
342
+ current = current[segment];
343
+ }
344
+ }
345
+
346
+ function setValueAtJsonPath(target, jsonPath, value) {
347
+ if (!jsonPath || typeof jsonPath !== "string") return;
348
+ const parts = jsonPath.replace(/^\//, "").split("/").filter(Boolean);
349
+ if (!parts.length) return;
350
+ let current = target;
351
+ parts.forEach((segment, index) => {
352
+ if (index === parts.length - 1) {
353
+ current[segment] = value;
354
+ return;
355
+ }
356
+ if (!current[segment] || typeof current[segment] !== "object") {
357
+ current[segment] = {};
358
+ }
359
+ current = current[segment];
360
+ });
361
+ }
362
+
363
+ function getEnumOptions(path) {
364
+ if (path === "shape.borderWidth") {
365
+ const enumKeys = Object.keys(PDS?.enums?.BorderWidths || {});
366
+ if (enumKeys.length) return enumKeys;
367
+ }
368
+ return ENUM_FIELD_OPTIONS[path] || null;
369
+ }
370
+
371
+ function normalizeEnumValue(path, value) {
372
+ const options = getEnumOptions(path);
373
+ if (!options || !options.length) return value;
374
+ if (typeof value === "string" && options.includes(value)) return value;
375
+
376
+ if (path === "shape.borderWidth" && typeof value === "number") {
377
+ const source = PDS?.enums?.BorderWidths || {
378
+ hairline: 0.5,
379
+ thin: 1,
380
+ medium: 2,
381
+ thick: 3,
382
+ };
383
+ const found = Object.entries(source).find(([, num]) => Number(num) === Number(value));
384
+ if (found) return found[0];
385
+ }
386
+
387
+ return value;
388
+ }
389
+
390
+ function normalizePaths(paths) {
391
+ const relations = PDS?.configRelations || {};
392
+ const seen = new Set();
393
+ const filtered = [];
394
+ (paths || []).forEach((path) => {
395
+ if (!relations[path]) return;
396
+ if (seen.has(path)) return;
397
+ seen.add(path);
398
+ filtered.push(path);
399
+ });
400
+ return filtered;
401
+ }
402
+
403
+ function collectRelationPathsByCategory() {
404
+ const relations = PDS?.configRelations || {};
405
+ const result = {};
406
+ Object.keys(relations).forEach((path) => {
407
+ const [category] = path.split(".");
408
+ if (!category) return;
409
+ if (!result[category]) result[category] = [];
410
+ result[category].push(path);
411
+ });
412
+ return result;
413
+ }
414
+
415
+ function collectPathsFromRelations(target) {
416
+ const relations = PDS?.configRelations || {};
417
+ const paths = [];
418
+ Object.entries(relations).forEach(([path, relation]) => {
419
+ const rules = relation?.rules || [];
420
+ if (!Array.isArray(rules) || !rules.length) return;
421
+ const matches = rules.some((rule) => {
422
+ const selectors = rule?.selectors || [];
423
+ return selectors.some((selector) => {
424
+ try {
425
+ return target.matches(selector) || Boolean(target.querySelector(selector));
426
+ } catch (e) {
427
+ return false;
428
+ }
429
+ });
430
+ });
431
+ if (matches) paths.push(path);
432
+ });
433
+ return paths;
434
+ }
435
+
436
+ function collectQuickRulePaths(target) {
437
+ return QUICK_RULES.filter((rule) => {
438
+ try {
439
+ return target.matches(rule.selector);
440
+ } catch (e) {
441
+ return false;
442
+ }
443
+ }).flatMap((rule) => rule.paths);
444
+ }
445
+
446
+ function pathExistsInDesign(path, design) {
447
+ if (!path || !design) return false;
448
+ const segments = path.split(".");
449
+ let current = design;
450
+ for (const segment of segments) {
451
+ if (!current || typeof current !== "object") return false;
452
+ if (!(segment in current)) return false;
453
+ current = current[segment];
454
+ }
455
+ return true;
456
+ }
457
+
458
+ function filterPathsByContext(target, paths) {
459
+ if (!target || !paths.length) return paths;
460
+ const isGlobal = target.matches("body, main");
461
+ const isInForm = Boolean(target.closest("form, pds-form"));
462
+ const isOnSurface = Boolean(
463
+ target.closest(
464
+ ".card, .surface-base, .surface-elevated, .surface-sunken, .surface-subtle"
465
+ )
466
+ );
467
+ const theme = getActiveTheme();
468
+ const design = PDS?.currentConfig?.design || {};
469
+
470
+ return paths.filter((path) => {
471
+ if (!theme.isDark && path.includes(DARK_MODE_PATH_MARKER)) return false;
472
+ if (path.startsWith("typography.") && !isGlobal) return false;
473
+ if (GLOBAL_LAYOUT_PATHS.has(path) && !isGlobal) return false;
474
+ if (FORM_CONTEXT_PATHS.has(path) && !isInForm) return false;
475
+ if (SURFACE_CONTEXT_PATHS.has(path) && !(isOnSurface || isGlobal)) return false;
476
+ // Opacity fields should never be under colors category
477
+ if (path.startsWith("colors.") && path.toLowerCase().includes("opacity")) return false;
478
+ // Always allow borderWidth in quick edit (design may be partial/override-only)
479
+ if (path === "shape.borderWidth") return true;
480
+ // Filter out paths that don't exist in the current design config
481
+ if (!pathExistsInDesign(path, design)) return false;
482
+ return true;
483
+ });
484
+ }
485
+
486
+ function getActiveTheme() {
487
+ if (typeof document === "undefined") return { value: "light", isDark: false };
488
+ const attr = document.documentElement?.getAttribute("data-theme");
489
+ const value = attr === "dark" ? "dark" : "light";
490
+ return { value, isDark: value === "dark" };
491
+ }
492
+
493
+ function getSpacingOffset() {
494
+ if (typeof window === "undefined" || typeof document === "undefined") return 8;
495
+ const value = window
496
+ .getComputedStyle(document.documentElement)
497
+ .getPropertyValue("--spacing-2")
498
+ .trim();
499
+ const parsed = Number.parseFloat(value);
500
+ if (Number.isNaN(parsed)) return 8;
501
+ return parsed;
502
+ }
503
+
504
+ function ensureColorNormalizer() {
505
+ if (typeof document === "undefined") return null;
506
+ if (colorNormalizer && document.contains(colorNormalizer)) return colorNormalizer;
507
+ if (!document.body) return null;
508
+ const probe = document.createElement("span");
509
+ probe.setAttribute("data-pds-color-normalizer", "");
510
+ probe.style.position = "fixed";
511
+ probe.style.left = "-9999px";
512
+ probe.style.top = "0";
513
+ probe.style.opacity = "0";
514
+ probe.style.pointerEvents = "none";
515
+ document.body.appendChild(probe);
516
+ colorNormalizer = probe;
517
+ return colorNormalizer;
518
+ }
519
+
520
+ function normalizeCssColor(value) {
521
+ if (!value || typeof window === "undefined") return null;
522
+ const probe = ensureColorNormalizer();
523
+ if (!probe) return null;
524
+ probe.style.color = "";
525
+ probe.style.color = value;
526
+ return window.getComputedStyle(probe).color || null;
527
+ }
528
+
529
+ function rgbToHex(value) {
530
+ if (!value) return null;
531
+ const match = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
532
+ if (!match) return null;
533
+ const [r, g, b] = match.slice(1, 4).map((num) => {
534
+ const parsed = Number.parseInt(num, 10);
535
+ if (Number.isNaN(parsed)) return 0;
536
+ return Math.max(0, Math.min(255, parsed));
537
+ });
538
+ const hex = (channel) => channel.toString(16).padStart(2, "0");
539
+ return `#${hex(r)}${hex(g)}${hex(b)}`;
540
+ }
541
+
542
+ function normalizeHexColor(value) {
543
+ if (!value) return null;
544
+ const trimmed = value.trim();
545
+ if (!trimmed.startsWith("#")) return null;
546
+ if (trimmed.length === 4) {
547
+ const [r, g, b] = trimmed.slice(1).split("");
548
+ return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
549
+ }
550
+ if (trimmed.length === 7) return trimmed.toLowerCase();
551
+ return null;
552
+ }
553
+
554
+ function toColorInputValue(value) {
555
+ if (!value) return value;
556
+ const normalizedHex = normalizeHexColor(value);
557
+ if (normalizedHex) return normalizedHex;
558
+ const normalized = normalizeCssColor(value) || value;
559
+ const hexValue = normalizeHexColor(normalized) || rgbToHex(normalized);
560
+ return hexValue || value;
561
+ }
562
+
563
+ function getCustomPropertyNames(style) {
564
+ const names = [];
565
+ if (!style) return names;
566
+ for (let i = 0; i < style.length; i += 1) {
567
+ const name = style[i];
568
+ if (name && name.startsWith("--")) names.push(name);
569
+ }
570
+ return names;
571
+ }
572
+
573
+ function makeTokenMatchers(relations) {
574
+ const matchers = [];
575
+ Object.entries(relations || {}).forEach(([path, relation]) => {
576
+ const tokens = relation?.tokens || [];
577
+ if (!Array.isArray(tokens)) return;
578
+ tokens.forEach((pattern) => {
579
+ if (!pattern || typeof pattern !== "string") return;
580
+ const escaped = pattern
581
+ .replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&")
582
+ .replace(/\*/g, ".*");
583
+ const regex = new RegExp(`^${escaped}$`);
584
+ matchers.push({ path, regex });
585
+ });
586
+ });
587
+ return matchers;
588
+ }
589
+
590
+ function addToValueMap(map, key, value) {
591
+ if (!key) return;
592
+ const entry = map.get(key);
593
+ if (entry) {
594
+ entry.add(value);
595
+ } else {
596
+ map.set(key, new Set([value]));
597
+ }
598
+ }
599
+
600
+ function getTokenIndex() {
601
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
602
+ const relations = PDS?.configRelations || {};
603
+ const relationCount = Object.keys(relations).length;
604
+ const root = document.documentElement;
605
+ if (!root) return null;
606
+ const rootStyle = window.getComputedStyle(root);
607
+ const bodyStyle = document.body ? window.getComputedStyle(document.body) : null;
608
+ const customProps = Array.from(
609
+ new Set([...
610
+ getCustomPropertyNames(rootStyle),
611
+ ...getCustomPropertyNames(bodyStyle),
612
+ ])
613
+ );
614
+ const meta = { relationCount, propCount: customProps.length };
615
+ if (cachedTokenIndex && cachedTokenIndexMeta) {
616
+ if (
617
+ cachedTokenIndexMeta.relationCount === meta.relationCount &&
618
+ cachedTokenIndexMeta.propCount === meta.propCount
619
+ ) {
620
+ return cachedTokenIndex;
621
+ }
622
+ }
623
+
624
+ const matchers = makeTokenMatchers(relations);
625
+ const varToPaths = new Map();
626
+ const valueToVars = new Map();
627
+ const colorToVars = new Map();
628
+
629
+ const getVarValue = (varName) => {
630
+ const rootValue = rootStyle.getPropertyValue(varName).trim();
631
+ if (rootValue) return rootValue;
632
+ if (!bodyStyle) return "";
633
+ return bodyStyle.getPropertyValue(varName).trim();
634
+ };
635
+
636
+ customProps.forEach((varName) => {
637
+ const varValue = getVarValue(varName);
638
+ if (varValue) {
639
+ addToValueMap(valueToVars, varValue, varName);
640
+ const normalizedColor = normalizeCssColor(varValue);
641
+ if (normalizedColor) addToValueMap(colorToVars, normalizedColor, varName);
642
+ }
643
+ matchers.forEach(({ path, regex }) => {
644
+ if (regex.test(varName)) {
645
+ addToValueMap(varToPaths, varName, path);
646
+ }
647
+ });
648
+ });
649
+
650
+ cachedTokenIndex = { varToPaths, valueToVars, colorToVars, matchers, getVarValue };
651
+ cachedTokenIndexMeta = meta;
652
+ return cachedTokenIndex;
653
+ }
654
+
655
+ function extractAllVarRefs(text) {
656
+ const vars = new Set();
657
+ if (!text) return vars;
658
+ // Extract all custom property names (--*) from text, including nested var() fallbacks
659
+ const customPropMatches = text.matchAll(/--[a-zA-Z0-9_-]+/g);
660
+ for (const match of customPropMatches) {
661
+ vars.add(match[0]);
662
+ }
663
+ return vars;
664
+ }
665
+
666
+ function collectVarRefsFromInline(element) {
667
+ const vars = new Set();
668
+ if (!element || typeof element.getAttribute !== "function") return vars;
669
+ const styleAttr = element.getAttribute("style") || "";
670
+ if (!styleAttr) return vars;
671
+ return extractAllVarRefs(styleAttr);
672
+ }
673
+
674
+ function collectScanTargets(target, limit = 120) {
675
+ const nodes = [target];
676
+ if (!target || typeof target.querySelectorAll !== "function") return nodes;
677
+ const descendants = Array.from(target.querySelectorAll("*"));
678
+ if (descendants.length <= limit) return nodes.concat(descendants);
679
+ return nodes.concat(descendants.slice(0, limit));
680
+ }
681
+
682
+ function collectVarRefsFromMatchingRules(element) {
683
+ const vars = new Set();
684
+ if (!element || typeof window === "undefined") return vars;
685
+
686
+ try {
687
+ // Scan all stylesheets for rules that match this element
688
+ const sheets = Array.from(document.styleSheets);
689
+
690
+ for (const sheet of sheets) {
691
+ try {
692
+ // Skip external sheets due to CORS
693
+ const rules = sheet.cssRules || sheet.rules;
694
+ if (!rules) continue;
695
+
696
+ for (const rule of rules) {
697
+ // Check CSSStyleRule (ignoring @media, @keyframes, etc for now)
698
+ if (rule.type === CSSRule.STYLE_RULE) {
699
+ try {
700
+ if (element.matches(rule.selectorText)) {
701
+ // Extract var refs from all properties in this rule
702
+ const cssText = rule.style.cssText;
703
+ if (cssText) {
704
+ const extracted = extractAllVarRefs(cssText);
705
+ extracted.forEach(v => vars.add(v));
706
+ }
707
+ }
708
+ } catch (e) {
709
+ // Invalid selector or matching error
710
+ }
711
+ }
712
+ }
713
+ } catch (e) {
714
+ // CORS or other sheet access error
715
+ }
716
+ }
717
+ } catch (e) {
718
+ // Fallback silently
719
+ }
720
+
721
+ return vars;
722
+ }
723
+
724
+ function collectPathsFromComputedStyles(target) {
725
+ const index = getTokenIndex();
726
+ if (!index || !target || typeof window === "undefined") return [];
727
+ const { varToPaths, valueToVars, colorToVars, matchers, getVarValue } = index;
728
+ const varsSeen = new Set();
729
+ const varsOrdered = [];
730
+ const scanTargets = collectScanTargets(target);
731
+ const addVarName = (name) => {
732
+ if (!name || varsSeen.has(name)) return;
733
+ varsSeen.add(name);
734
+ varsOrdered.push(name);
735
+ };
736
+ const addVarSet = (set) => {
737
+ set.forEach((name) => addVarName(name));
738
+ };
739
+
740
+ scanTargets.forEach((node) => {
741
+ addVarSet(collectVarRefsFromInline(node));
742
+ addVarSet(collectVarRefsFromMatchingRules(node));
743
+
744
+ let style = null;
745
+ try {
746
+ style = window.getComputedStyle(node);
747
+ } catch (e) {
748
+ style = null;
749
+ }
750
+ if (!style) return;
751
+
752
+ // Extract var refs from all properties (custom AND standard)
753
+ // We scan custom properties to find nested var() chains
754
+ // We scan standard properties to find var() usage
755
+ // We do NOT add custom property names themselves - those are definitions, not usage
756
+ for (let i = 0; i < style.length; i += 1) {
757
+ const propName = style[i];
758
+ const propValue = style.getPropertyValue(propName);
759
+ if (propValue) {
760
+ // Extract all var() references including nested fallbacks
761
+ addVarSet(extractAllVarRefs(propValue));
762
+ }
763
+ }
764
+
765
+ QUICK_STYLE_PROPERTIES.forEach((prop) => {
766
+ const value = style.getPropertyValue(prop);
767
+ if (!value) return;
768
+ const trimmed = value.trim();
769
+
770
+ // Extract var refs from the value (handles var() with fallbacks)
771
+ addVarSet(extractAllVarRefs(trimmed));
772
+
773
+ if (trimmed && valueToVars.has(trimmed)) {
774
+ valueToVars.get(trimmed).forEach((varName) => addVarName(varName));
775
+ }
776
+
777
+ const colors = trimmed.match(COLOR_VALUE_REGEX) || [];
778
+ colors.forEach((color) => {
779
+ const normalized = normalizeCssColor(color) || color;
780
+ if (colorToVars.has(normalized)) {
781
+ colorToVars.get(normalized).forEach((varName) => addVarName(varName));
782
+ }
783
+ });
784
+ });
785
+ });
786
+
787
+ const paths = [];
788
+ const seenPaths = new Set();
789
+ const hints = {};
790
+ const addPath = (path) => {
791
+ if (!path || seenPaths.has(path)) return;
792
+ seenPaths.add(path);
793
+ paths.push(path);
794
+ };
795
+ const addHint = (path, varName) => {
796
+ if (!path || !varName) return;
797
+ if (!path.startsWith("colors.")) return;
798
+ if (path.includes(DARK_MODE_PATH_MARKER) && !getActiveTheme().isDark) return;
799
+ if (hints[path]) return;
800
+ const rawValue = getVarValue ? getVarValue(varName) : "";
801
+ if (!rawValue) return;
802
+ const normalized = toColorInputValue(rawValue);
803
+ hints[path] = normalized;
804
+ };
805
+ const matchVarName = (varName) => {
806
+ const direct = varToPaths.get(varName);
807
+ if (direct) {
808
+ direct.forEach((path) => {
809
+ addPath(path);
810
+ addHint(path, varName);
811
+ });
812
+ return;
813
+ }
814
+ (matchers || []).forEach(({ path, regex }) => {
815
+ if (regex.test(varName)) {
816
+ addPath(path);
817
+ addHint(path, varName);
818
+ }
819
+ });
820
+ };
821
+
822
+ varsOrdered.forEach((varName) => matchVarName(varName));
823
+
824
+ return { paths: normalizePaths(paths), hints, debug: { vars: varsOrdered, paths } };
825
+ }
826
+
827
+ function collectQuickContext(target) {
828
+ const computed = collectPathsFromComputedStyles(target);
829
+ const byComputed = computed?.paths || [];
830
+ const byRelations = collectPathsFromRelations(target);
831
+ const byQuickRules = collectQuickRulePaths(target);
832
+ const hints = computed?.hints || {};
833
+ const debug = computed?.debug || { vars: [], paths: [] };
834
+
835
+ // Prioritize quick rule paths first (selector-based), then computed/relations
836
+ const filtered = filterPathsByContext(target, [
837
+ ...byQuickRules,
838
+ ...byComputed,
839
+ ...byRelations,
840
+ ]);
841
+ if (!filtered.length) {
842
+ return {
843
+ paths: normalizePaths(DEFAULT_QUICK_PATHS),
844
+ hints: {},
845
+ debug: { vars: [], paths: [] },
846
+ };
847
+ }
848
+ return { paths: normalizePaths(filtered), hints, debug };
849
+ }
850
+
851
+ function collectDrawerPaths(quickPaths) {
852
+ const categories = new Set();
853
+ quickPaths.forEach((path) => categories.add(path.split(".")[0]));
854
+ const expanded = [];
855
+ const relationMap = collectRelationPathsByCategory();
856
+ categories.forEach((category) => {
857
+ const fields = relationMap[category] || [];
858
+ expanded.push(...fields);
859
+ });
860
+ return normalizePaths([...quickPaths, ...expanded]);
861
+ }
862
+
863
+ function buildSchemaFromPaths(paths, design, hints = {}) {
864
+ const schema = { type: "object", properties: {} };
865
+ const uiSchema = {};
866
+
867
+ const getStepFromValue = (value) => {
868
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
869
+ const parts = String(value).split(".");
870
+ if (parts.length < 2) return 1;
871
+ const decimals = parts[1].length;
872
+ return Number(`0.${"1".padStart(decimals, "0")}`);
873
+ };
874
+
875
+ const inferRangeBounds = (path, value) => {
876
+ const hint = String(path || "").toLowerCase();
877
+ if (hint.includes("opacity")) return { min: 0, max: 1, step: 0.01 };
878
+ if (hint.includes("scale") || hint.includes("ratio")) {
879
+ return { min: 1, max: 2, step: 0.01 };
880
+ }
881
+ if (
882
+ hint.includes("size") ||
883
+ hint.includes("radius") ||
884
+ hint.includes("padding") ||
885
+ hint.includes("gap") ||
886
+ hint.includes("spacing") ||
887
+ hint.includes("width") ||
888
+ hint.includes("height") ||
889
+ hint.includes("shadow")
890
+ ) {
891
+ return { min: 0, max: 64, step: 1 };
892
+ }
893
+ if (typeof value === "number") {
894
+ if (value >= 0 && value <= 1) return { min: 0, max: 1, step: 0.01 };
895
+ const magnitude = Math.max(1, Math.abs(value));
896
+ const upper = Math.max(10, Math.ceil(magnitude * 4));
897
+ return { min: 0, max: upper, step: getStepFromValue(value) || 1 };
898
+ }
899
+ return { min: 0, max: 100, step: 1 };
900
+ };
901
+
902
+ const isColorValue = (value, path) => {
903
+ if (String(path || "").toLowerCase().startsWith("colors.")) return true;
904
+ if (typeof value !== "string") return false;
905
+ return /^#([0-9a-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
906
+ };
907
+
908
+ paths.forEach((path) => {
909
+ const segments = path.split(".");
910
+ const [category, ...rest] = segments;
911
+ if (!category || !rest.length) return;
912
+
913
+ let parent = schema.properties[category];
914
+ if (!parent) {
915
+ parent = { type: "object", title: titleize(category), properties: {} };
916
+ schema.properties[category] = parent;
917
+ }
918
+
919
+ let current = parent;
920
+ for (let i = 0; i < rest.length; i += 1) {
921
+ const segment = rest[i];
922
+ if (i === rest.length - 1) {
923
+ const value = getValueAtPath(design, [category, ...rest]);
924
+ const hintValue = hints[path];
925
+ const enumOptions = getEnumOptions(path);
926
+ const normalizedValue = normalizeEnumValue(path, value);
927
+ const normalizedHint = normalizeEnumValue(path, hintValue);
928
+ const inferredType = Array.isArray(value)
929
+ ? "array"
930
+ : value === null
931
+ ? "string"
932
+ : typeof value;
933
+ const schemaType = enumOptions?.length
934
+ ? "string"
935
+ : inferredType === "number" || inferredType === "boolean"
936
+ ? inferredType
937
+ : "string";
938
+ current.properties[segment] = {
939
+ type: schemaType,
940
+ title: titleize(segment),
941
+ ...(enumOptions?.length
942
+ ? {
943
+ oneOf: enumOptions.map((option) => ({
944
+ const: option,
945
+ title: titleize(option),
946
+ })),
947
+ }
948
+ : {}),
949
+ examples:
950
+ normalizedValue !== undefined && normalizedValue !== null
951
+ ? [normalizedValue]
952
+ : normalizedHint !== undefined
953
+ ? [normalizedHint]
954
+ : undefined,
955
+ };
956
+
957
+ const pointer = `/${[category, ...rest].join("/")}`;
958
+ const uiEntry = {};
959
+
960
+ if (enumOptions?.length) {
961
+ uiEntry["ui:widget"] = "select";
962
+ }
963
+
964
+ // Check for opacity/numeric fields BEFORE color check
965
+ const pathLower = String(path || "").toLowerCase();
966
+ const isOpacityField = pathLower.includes("opacity");
967
+
968
+ if (isOpacityField || (schemaType === "number" && !isColorValue(value, path))) {
969
+ const bounds = inferRangeBounds(path, value);
970
+ uiEntry["ui:widget"] = "input-range";
971
+ uiEntry["ui:min"] = bounds.min;
972
+ uiEntry["ui:max"] = bounds.max;
973
+ uiEntry["ui:step"] = bounds.step;
974
+ } else if (isColorValue(value, path)) {
975
+ uiEntry["ui:widget"] = "input-color";
976
+ }
977
+
978
+ const isTextOrNumberInput =
979
+ (schemaType === "string" || schemaType === "number") &&
980
+ !uiEntry["ui:widget"];
981
+ if (isTextOrNumberInput) {
982
+ uiEntry["ui:icon"] = CATEGORY_ICONS[category] || "sparkle";
983
+ }
984
+
985
+ uiSchema[pointer] = uiEntry;
986
+ return;
987
+ }
988
+
989
+ if (!current.properties[segment]) {
990
+ current.properties[segment] = { type: "object", properties: {} };
991
+ }
992
+ current = current.properties[segment];
993
+ }
994
+ });
995
+
996
+ return { schema, uiSchema };
997
+ }
998
+
999
+ async function getGeneratorClass() {
1000
+ if (!PDS?.getGenerator) return null;
1001
+ try {
1002
+ return await PDS.getGenerator();
1003
+ } catch (e) {
1004
+ return null;
1005
+ }
1006
+ }
1007
+
1008
+ async function applyDesignPatch(patch) {
1009
+ if (!patch || typeof patch !== "object") return;
1010
+ const Generator = await getGeneratorClass();
1011
+ const generator = Generator?.instance;
1012
+ if (!generator || !generator.options) return;
1013
+
1014
+ const presetKeyMatches = (key, compareTo) => {
1015
+ if (!key || !compareTo) return false;
1016
+ return slugifyPreset(key) === slugifyPreset(compareTo);
1017
+ };
1018
+
1019
+ const slugifyPreset = (value) =>
1020
+ String(value || "")
1021
+ .toLowerCase()
1022
+ .replace(/&/g, " and ")
1023
+ .replace(/[^a-z0-9]+/g, "-")
1024
+ .replace(/^-+|-+$/g, "");
1025
+
1026
+ const resolvePresetBase = (presetId) => {
1027
+ const presets = PDS?.presets || {};
1028
+ if (!presetId) return { id: null, preset: null };
1029
+ if (presets[presetId]) {
1030
+ return { id: presetId, preset: presets[presetId] };
1031
+ }
1032
+ const presetKeys = Object.keys(presets || {});
1033
+ const matchedKey = presetKeys.find((key) => presetKeyMatches(key, presetId));
1034
+ if (matchedKey) {
1035
+ return { id: matchedKey, preset: presets[matchedKey] };
1036
+ }
1037
+ const found = Object.values(presets).find((preset) => {
1038
+ const name = preset?.name || preset?.id || "";
1039
+ return presetKeyMatches(name, presetId);
1040
+ });
1041
+ if (found) {
1042
+ const foundId = found.id || found.name || presetId;
1043
+ return { id: foundId, preset: found };
1044
+ }
1045
+ return { id: presetId, preset: null };
1046
+ };
1047
+
1048
+ const currentOptions = generator.options;
1049
+ let storedConfig = null;
1050
+ if (typeof window !== "undefined" && window.localStorage) {
1051
+ try {
1052
+ const raw = window.localStorage.getItem("pure-ds-config");
1053
+ if (raw) {
1054
+ const parsed = JSON.parse(raw);
1055
+ if (parsed && ("preset" in parsed || "design" in parsed)) {
1056
+ storedConfig = parsed;
1057
+ }
1058
+ }
1059
+ } catch (e) {
1060
+ storedConfig = null;
1061
+ }
1062
+ }
1063
+
1064
+ const storedPreset = storedConfig?.preset;
1065
+ const hasStoredPreset = Boolean(storedPreset);
1066
+ const storedOverrides =
1067
+ storedConfig && storedConfig.design && typeof storedConfig.design === "object"
1068
+ ? storedConfig.design
1069
+ : {};
1070
+ let presetId = storedPreset || currentOptions.preset || PDS?.currentConfig?.preset || null;
1071
+ const inferredPreset = currentOptions.design?.id || currentOptions.design?.name || null;
1072
+ if (!presetId && inferredPreset && !hasStoredPreset) {
1073
+ const inferredMatch = resolvePresetBase(inferredPreset);
1074
+ if (inferredMatch?.preset) presetId = inferredMatch.id;
1075
+ }
1076
+ if (String(presetId || "").toLowerCase() === "default" && inferredPreset && !hasStoredPreset) {
1077
+ const inferredMatch = resolvePresetBase(inferredPreset);
1078
+ if (inferredMatch?.preset) presetId = inferredMatch.id;
1079
+ }
1080
+
1081
+ const resolvedPreset = resolvePresetBase(presetId);
1082
+ const resolvedPresetId = resolvedPreset.id || presetId || null;
1083
+ const presetBase = resolvedPreset.preset || null;
1084
+
1085
+ const baseDesign = presetBase
1086
+ ? deepMerge(shallowClone(presetBase), storedOverrides)
1087
+ : shallowClone(currentOptions.design || {});
1088
+ const nextDesign = deepMerge(shallowClone(baseDesign), patch);
1089
+ const nextOptions = { ...currentOptions, design: nextDesign };
1090
+ if (resolvedPresetId) nextOptions.preset = resolvedPresetId;
1091
+
1092
+ const nextGenerator = new Generator(nextOptions);
1093
+ if (PDS?.applyStyles) {
1094
+ await PDS.applyStyles(nextGenerator);
1095
+ }
1096
+
1097
+ if (PDS) {
1098
+ try {
1099
+ PDS.currentConfig = Object.freeze({
1100
+ ...(PDS.currentConfig || {}),
1101
+ design: structuredClone(nextDesign),
1102
+ preset: resolvedPresetId || PDS.currentConfig?.preset,
1103
+ });
1104
+ } catch (e) {
1105
+ PDS.currentConfig = {
1106
+ ...(PDS.currentConfig || {}),
1107
+ design: nextDesign,
1108
+ preset: resolvedPresetId || PDS.currentConfig?.preset,
1109
+ };
1110
+ }
1111
+
1112
+ try {
1113
+ const event = new CustomEvent("design-updated", {
1114
+ detail: { config: nextDesign },
1115
+ });
1116
+ PDS.dispatchEvent(event);
1117
+ } catch (e) {}
1118
+ }
1119
+
1120
+ if (typeof window !== "undefined" && window.localStorage) {
1121
+ try {
1122
+ const nextStored = {
1123
+ preset: resolvedPresetId || null,
1124
+ design: shallowClone(nextDesign),
1125
+ };
1126
+ window.localStorage.setItem("pure-ds-config", JSON.stringify(nextStored));
1127
+ } catch (e) {}
1128
+ }
1129
+ }
1130
+
1131
+ function getStoredConfig() {
1132
+ if (typeof window === "undefined" || !window.localStorage) return null;
1133
+ try {
1134
+ const raw = window.localStorage.getItem("pure-ds-config");
1135
+ if (!raw) return null;
1136
+ const parsed = JSON.parse(raw);
1137
+ if (parsed && ("preset" in parsed || "design" in parsed)) return parsed;
1138
+ } catch (e) {
1139
+ return null;
1140
+ }
1141
+ return null;
1142
+ }
1143
+
1144
+ function setStoredConfig(nextConfig) {
1145
+ if (typeof window === "undefined" || !window.localStorage) return;
1146
+ try {
1147
+ window.localStorage.setItem("pure-ds-config", JSON.stringify(nextConfig));
1148
+ } catch (e) {}
1149
+ }
1150
+
1151
+ function getPresetOptions() {
1152
+ const presets = PDS?.presets || {};
1153
+ return Object.values(presets)
1154
+ .map((preset) => ({
1155
+ id: preset?.id || preset?.name,
1156
+ name: preset?.name || preset?.id || "Unnamed",
1157
+ }))
1158
+ .filter((preset) => preset.id)
1159
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)));
1160
+ }
1161
+
1162
+ function getActivePresetId() {
1163
+ const stored = getStoredConfig();
1164
+ return stored?.preset || PDS?.currentConfig?.preset || PDS?.currentPreset || null;
1165
+ }
1166
+
1167
+ async function applyPresetSelection(presetId) {
1168
+ if (!presetId) return;
1169
+ setStoredConfig({
1170
+ preset: presetId,
1171
+ design: {},
1172
+ });
1173
+ await applyDesignPatch({});
1174
+ }
1175
+
1176
+ function setFormSchemas(form, schema, uiSchema, design) {
1177
+ form.jsonSchema = schema;
1178
+ form.uiSchema = uiSchema;
1179
+ form.values = shallowClone(design);
1180
+ }
1181
+
1182
+ async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1183
+ const { schema, uiSchema } = buildSchemaFromPaths(paths, design, hints);
1184
+ const form = document.createElement("pds-form");
1185
+ form.setAttribute("hide-actions", "");
1186
+ form.options = {
1187
+ layouts: {
1188
+ arrays: "compact",
1189
+ },
1190
+ enhancements: {
1191
+ rangeOutput: true,
1192
+ },
1193
+ };
1194
+ form.addEventListener("pw:submit", onSubmit);
1195
+ const values = shallowClone(design || {});
1196
+ Object.keys(ENUM_FIELD_OPTIONS).forEach((path) => {
1197
+ const normalized = normalizeEnumValue(path, getValueAtPath(values, path.split(".")));
1198
+ if (normalized !== undefined) {
1199
+ setValueAtPath(values, path.split("."), normalized);
1200
+ }
1201
+ });
1202
+ Object.entries(hints || {}).forEach(([path, hintValue]) => {
1203
+ const segments = path.split(".");
1204
+ const currentValue = getValueAtPath(values, segments);
1205
+ if (currentValue === undefined || currentValue === null) {
1206
+ setValueAtPath(values, segments, hintValue);
1207
+ }
1208
+ });
1209
+ setFormSchemas(form, schema, uiSchema, values);
1210
+
1211
+ if (!customElements.get("pds-form")) {
1212
+ customElements.whenDefined("pds-form").then(() => {
1213
+ setFormSchemas(form, schema, uiSchema, values);
1214
+ });
1215
+ }
1216
+
1217
+ // Apply button (will trigger form submit programmatically)
1218
+ const applyBtn = document.createElement("button");
1219
+ applyBtn.className = "btn-primary btn-sm";
1220
+ applyBtn.type = "button";
1221
+ applyBtn.textContent = "Apply";
1222
+ applyBtn.addEventListener("click", async () => {
1223
+ // Manually trigger pw:submit event for pds-form
1224
+ if (typeof form.getValuesFlat === "function") {
1225
+ // Wait for form to be ready if it's still loading
1226
+ if (!customElements.get("pds-form")) {
1227
+ await customElements.whenDefined("pds-form");
1228
+ }
1229
+
1230
+ const flatValues = form.getValuesFlat();
1231
+ const event = new CustomEvent("pw:submit", {
1232
+ detail: {
1233
+ json: flatValues,
1234
+ formData: new FormData(),
1235
+ valid: true,
1236
+ issues: []
1237
+ },
1238
+ bubbles: true,
1239
+ cancelable: true
1240
+ });
1241
+ form.dispatchEvent(event);
1242
+ }
1243
+ });
1244
+
1245
+ // Undo button
1246
+ const undoBtn = document.createElement("button");
1247
+ undoBtn.className = "btn-secondary btn-sm";
1248
+ undoBtn.type = "button";
1249
+ undoBtn.textContent = "Undo";
1250
+ undoBtn.addEventListener("click", onUndo);
1251
+
1252
+ return { form, applyBtn, undoBtn };
1253
+ }
1254
+
1255
+ class PdsLiveEdit extends HTMLElement {
1256
+ constructor() {
1257
+ super();
1258
+ this._boundMouseOver = this._handleMouseOver.bind(this);
1259
+ this._boundMouseOut = this._handleMouseOut.bind(this);
1260
+ this._boundMouseMove = this._handleMouseMove.bind(this);
1261
+ this._boundReposition = this._repositionDropdown.bind(this);
1262
+ this._activeTarget = null;
1263
+ this._activeDropdown = null;
1264
+ this._holdOpen = false;
1265
+ this._closeTimer = null;
1266
+ this._drawer = null;
1267
+ this._selectors = null;
1268
+ this._lastPointer = null;
1269
+ this._boundDocKeydown = this._handleDocumentKeydown.bind(this);
1270
+ this._connected = false;
1271
+ this._undoStack = [];
1272
+ this._dropdownMenuOpen = false;
1273
+ this._dropdownObserver = null;
1274
+ }
1275
+
1276
+ connectedCallback() {
1277
+ if (this._connected) return;
1278
+ if (PdsLiveEdit._activeInstance && PdsLiveEdit._activeInstance !== this) {
1279
+ PdsLiveEdit._activeInstance._teardown();
1280
+ }
1281
+ PdsLiveEdit._activeInstance = this;
1282
+ this._connected = true;
1283
+ if (!isHoverCapable()) return;
1284
+
1285
+ ensureStyles();
1286
+ this._selectors = collectSelectors();
1287
+ document.addEventListener("mouseover", this._boundMouseOver, true);
1288
+ document.addEventListener("mouseout", this._boundMouseOut, true);
1289
+ document.addEventListener("mousemove", this._boundMouseMove, true);
1290
+ }
1291
+
1292
+ disconnectedCallback() {
1293
+ this._teardown();
1294
+ }
1295
+
1296
+ _teardown() {
1297
+ if (this._connected) {
1298
+ document.removeEventListener("mouseover", this._boundMouseOver, true);
1299
+ document.removeEventListener("mouseout", this._boundMouseOut, true);
1300
+ document.removeEventListener("mousemove", this._boundMouseMove, true);
1301
+ }
1302
+ this._removeRepositionListeners();
1303
+ this._clearCloseTimer();
1304
+ this._removeActiveUI();
1305
+ this._connected = false;
1306
+ if (PdsLiveEdit._activeInstance === this) {
1307
+ PdsLiveEdit._activeInstance = null;
1308
+ }
1309
+ }
1310
+
1311
+ _handleMouseOver(event) {
1312
+ if (!event?.target || !(event.target instanceof Element)) return;
1313
+
1314
+ // Check if we're hovering over the dropdown (including Shadow DOM elements)
1315
+ if (this._activeDropdown) {
1316
+ const path = event.composedPath ? event.composedPath() : [event.target];
1317
+ const isOverDropdown = path.some(node => node === this._activeDropdown);
1318
+ if (isOverDropdown) {
1319
+ this._clearCloseTimer();
1320
+ return;
1321
+ }
1322
+ }
1323
+
1324
+ const target = this._findEditableTarget(event.target);
1325
+
1326
+ // If hovering over the same active target, just clear timer
1327
+ if (target && target === this._activeTarget) {
1328
+ this._clearCloseTimer();
1329
+ return;
1330
+ }
1331
+
1332
+ // If hovering over a new target, show its editor
1333
+ if (target && target !== this._activeTarget) {
1334
+ this._removeActiveUI();
1335
+ this._showForTarget(target);
1336
+ }
1337
+ }
1338
+
1339
+ _handleMouseOut(event) {
1340
+ if (!this._activeTarget) return;
1341
+
1342
+ // Schedule a delayed close - the safe zone logic will determine if we actually close
1343
+ this._scheduleClose();
1344
+ }
1345
+
1346
+ _findEditableTarget(node) {
1347
+ const tag = node.tagName?.toLowerCase?.();
1348
+ if (tag && ["html", "head", "meta", "link", "style", "script", "title"].includes(tag)) {
1349
+ return null;
1350
+ }
1351
+ if (!this._selectors?.selector) return null;
1352
+ if (node.closest(EDITOR_TAG)) return null;
1353
+ if (node.closest(`.${DROPDOWN_CLASS}`)) return null;
1354
+ if (node.closest("pds-drawer")) return null;
1355
+
1356
+ try {
1357
+ return node.closest(this._selectors.selector);
1358
+ } catch (e) {
1359
+ return null;
1360
+ }
1361
+ }
1362
+
1363
+ _showForTarget(target) {
1364
+ const quickContext = collectQuickContext(target);
1365
+ const quickPaths = quickContext.paths;
1366
+ if (!quickPaths.length) return;
1367
+
1368
+ this._holdOpen = true;
1369
+
1370
+ target.setAttribute(TARGET_ATTR, "true");
1371
+ const dropdown = this._buildDropdown(
1372
+ target,
1373
+ quickPaths,
1374
+ quickContext.hints,
1375
+ quickContext.debug
1376
+ );
1377
+ document.body.appendChild(dropdown);
1378
+ this._positionDropdown(target, dropdown);
1379
+ this._addRepositionListeners();
1380
+ this._addDocumentListeners();
1381
+ this._addMouseMoveListener();
1382
+
1383
+ this._activeTarget = target;
1384
+ this._activeDropdown = dropdown;
1385
+
1386
+ // Watch for dropdown menu opening/closing
1387
+ this._watchDropdownState();
1388
+ }
1389
+
1390
+ _removeActiveUI() {
1391
+ this._clearCloseTimer();
1392
+ this._removeRepositionListeners();
1393
+ this._removeDocumentListeners();
1394
+ this._removeMouseMoveListener();
1395
+ this._unwatchDropdownState();
1396
+ if (this._activeDropdown && this._activeDropdown.parentNode) {
1397
+ this._activeDropdown.parentNode.removeChild(this._activeDropdown);
1398
+ }
1399
+ if (this._activeTarget) {
1400
+ this._activeTarget.removeAttribute(TARGET_ATTR);
1401
+ }
1402
+ this._activeTarget = null;
1403
+ this._activeDropdown = null;
1404
+ this._holdOpen = false;
1405
+ this._lastPointer = null;
1406
+ this._dropdownMenuOpen = false;
1407
+
1408
+ // Always re-enable mouseover when UI is removed
1409
+ this._addMouseOverListener();
1410
+ }
1411
+
1412
+ _addDocumentListeners() {
1413
+ if (typeof document === "undefined") return;
1414
+ document.addEventListener("keydown", this._boundDocKeydown, true);
1415
+ }
1416
+
1417
+ _removeDocumentListeners() {
1418
+ if (typeof document === "undefined") return;
1419
+ document.removeEventListener("keydown", this._boundDocKeydown, true);
1420
+ }
1421
+
1422
+ _handleDocumentKeydown(event) {
1423
+ if (!event || event.key !== "Escape") return;
1424
+ if (this._activeDropdown || this._activeTarget) {
1425
+ event.preventDefault();
1426
+ this._removeActiveUI();
1427
+ }
1428
+ }
1429
+
1430
+ _scheduleClose() {
1431
+ if (typeof window === "undefined") return;
1432
+ this._clearCloseTimer();
1433
+ this._closeTimer = window.setTimeout(() => {
1434
+ if (this._holdOpen) return;
1435
+ if (this._activeDropdown && this._activeDropdown.matches(":hover")) return;
1436
+ if (this._activeTarget && this._activeTarget.matches(":hover")) return;
1437
+ if (this._isPointerWithinSafeZone()) {
1438
+ // Still in safe zone, check again soon
1439
+ this._scheduleClose();
1440
+ return;
1441
+ }
1442
+ this._removeActiveUI();
1443
+ }, 300);
1444
+ }
1445
+
1446
+ _clearCloseTimer() {
1447
+ if (this._closeTimer) {
1448
+ clearTimeout(this._closeTimer);
1449
+ this._closeTimer = null;
1450
+ }
1451
+ }
1452
+
1453
+ _addRepositionListeners() {
1454
+ if (typeof window === "undefined") return;
1455
+ window.addEventListener("scroll", this._boundReposition, true);
1456
+ window.addEventListener("resize", this._boundReposition);
1457
+ }
1458
+
1459
+ _removeRepositionListeners() {
1460
+ if (typeof window === "undefined") return;
1461
+ window.removeEventListener("scroll", this._boundReposition, true);
1462
+ window.removeEventListener("resize", this._boundReposition);
1463
+ }
1464
+
1465
+ _repositionDropdown() {
1466
+ if (!this._activeTarget || !this._activeDropdown) return;
1467
+ if (!document.contains(this._activeTarget)) {
1468
+ this._removeActiveUI();
1469
+ return;
1470
+ }
1471
+ this._positionDropdown(this._activeTarget, this._activeDropdown);
1472
+ }
1473
+
1474
+ _positionDropdown(target, dropdown) {
1475
+ if (!target || !dropdown) return;
1476
+ const rect = target.getBoundingClientRect();
1477
+ const spacing = getSpacingOffset();
1478
+ const width = Math.max(dropdown.offsetWidth || 0, 160);
1479
+ const height = Math.max(dropdown.offsetHeight || 0, 120);
1480
+ const spaceRight = Math.max(0, window.innerWidth - rect.right);
1481
+ const spaceLeft = Math.max(0, rect.left);
1482
+ const spaceBelow = Math.max(0, window.innerHeight - rect.bottom);
1483
+ const spaceAbove = Math.max(0, rect.top);
1484
+
1485
+ const alignRight = spaceRight >= width || spaceRight >= spaceLeft;
1486
+ const alignBottom = spaceBelow >= height || spaceBelow >= spaceAbove;
1487
+
1488
+ const rightOffset = Math.max(0, window.innerWidth - rect.right);
1489
+ const bottomOffset = Math.max(0, window.innerHeight - rect.bottom);
1490
+
1491
+ dropdown.style.setProperty(
1492
+ "--pds-live-edit-left",
1493
+ alignRight ? `${rect.left + spacing}px` : "auto"
1494
+ );
1495
+ dropdown.style.setProperty(
1496
+ "--pds-live-edit-right",
1497
+ alignRight ? "auto" : `${rightOffset + spacing}px`
1498
+ );
1499
+ dropdown.style.setProperty(
1500
+ "--pds-live-edit-top",
1501
+ alignBottom ? `${rect.top + spacing}px` : "auto"
1502
+ );
1503
+ dropdown.style.setProperty(
1504
+ "--pds-live-edit-bottom",
1505
+ alignBottom ? "auto" : `${bottomOffset + spacing}px`
1506
+ );
1507
+
1508
+ const adjusted = dropdown.getBoundingClientRect();
1509
+ let shiftX = 0;
1510
+ let shiftY = 0;
1511
+ if (adjusted.left < DROPDOWN_VIEWPORT_PADDING) {
1512
+ shiftX = DROPDOWN_VIEWPORT_PADDING - adjusted.left;
1513
+ } else if (adjusted.right > window.innerWidth - DROPDOWN_VIEWPORT_PADDING) {
1514
+ shiftX = window.innerWidth - DROPDOWN_VIEWPORT_PADDING - adjusted.right;
1515
+ }
1516
+ if (adjusted.top < DROPDOWN_VIEWPORT_PADDING) {
1517
+ shiftY = DROPDOWN_VIEWPORT_PADDING - adjusted.top;
1518
+ } else if (adjusted.bottom > window.innerHeight - DROPDOWN_VIEWPORT_PADDING) {
1519
+ shiftY = window.innerHeight - DROPDOWN_VIEWPORT_PADDING - adjusted.bottom;
1520
+ }
1521
+ if (shiftX || shiftY) {
1522
+ const currentLeft = parseFloat(dropdown.style.getPropertyValue("--pds-live-edit-left"));
1523
+ const currentTop = parseFloat(dropdown.style.getPropertyValue("--pds-live-edit-top"));
1524
+ const currentRight = parseFloat(dropdown.style.getPropertyValue("--pds-live-edit-right"));
1525
+ const currentBottom = parseFloat(dropdown.style.getPropertyValue("--pds-live-edit-bottom"));
1526
+ if (!Number.isNaN(currentLeft)) {
1527
+ dropdown.style.setProperty("--pds-live-edit-left", `${currentLeft + shiftX}px`);
1528
+ } else if (!Number.isNaN(currentRight)) {
1529
+ dropdown.style.setProperty("--pds-live-edit-right", `${currentRight - shiftX}px`);
1530
+ }
1531
+ if (!Number.isNaN(currentTop)) {
1532
+ dropdown.style.setProperty("--pds-live-edit-top", `${currentTop + shiftY}px`);
1533
+ } else if (!Number.isNaN(currentBottom)) {
1534
+ dropdown.style.setProperty("--pds-live-edit-bottom", `${currentBottom - shiftY}px`);
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ _handleMouseMove(event) {
1540
+ if (!event) return;
1541
+ this._lastPointer = { x: event.clientX, y: event.clientY };
1542
+ }
1543
+
1544
+ _addMouseMoveListener() {
1545
+ if (typeof document === "undefined") return;
1546
+ document.addEventListener("mousemove", this._boundMouseMove, true);
1547
+ }
1548
+
1549
+ _removeMouseMoveListener() {
1550
+ if (typeof document === "undefined") return;
1551
+ document.removeEventListener("mousemove", this._boundMouseMove, true);
1552
+ }
1553
+
1554
+ _addMouseOverListener() {
1555
+ if (typeof document === "undefined") return;
1556
+ document.addEventListener("mouseover", this._boundMouseOver, true);
1557
+ }
1558
+
1559
+ _removeMouseOverListener() {
1560
+ if (typeof document === "undefined") return;
1561
+ document.removeEventListener("mouseover", this._boundMouseOver, true);
1562
+ }
1563
+
1564
+ _watchDropdownState() {
1565
+ if (!this._activeDropdown) return;
1566
+
1567
+ const menu = this._activeDropdown.querySelector("menu");
1568
+ if (!menu) return;
1569
+
1570
+ // Create a MutationObserver to watch for aria-hidden changes
1571
+ this._dropdownObserver = new MutationObserver((mutations) => {
1572
+ mutations.forEach((mutation) => {
1573
+ if (mutation.attributeName === "aria-hidden") {
1574
+ const isOpen = menu.getAttribute("aria-hidden") === "false";
1575
+
1576
+ if (isOpen && !this._dropdownMenuOpen) {
1577
+ // Dropdown just opened - pause mouseover
1578
+ this._dropdownMenuOpen = true;
1579
+ this._removeMouseOverListener();
1580
+ } else if (!isOpen && this._dropdownMenuOpen) {
1581
+ // Dropdown just closed - resume mouseover
1582
+ this._dropdownMenuOpen = false;
1583
+ this._addMouseOverListener();
1584
+ }
1585
+ }
1586
+ });
1587
+ });
1588
+
1589
+ this._dropdownObserver.observe(menu, {
1590
+ attributes: true,
1591
+ attributeFilter: ["aria-hidden"]
1592
+ });
1593
+ }
1594
+
1595
+ _unwatchDropdownState() {
1596
+ if (this._dropdownObserver) {
1597
+ this._dropdownObserver.disconnect();
1598
+ this._dropdownObserver = null;
1599
+ }
1600
+ }
1601
+
1602
+ _watchDropdownState() {
1603
+ if (!this._activeDropdown) return;
1604
+
1605
+ const menu = this._activeDropdown.querySelector("menu");
1606
+ if (!menu) return;
1607
+
1608
+ // Create a MutationObserver to watch for aria-hidden changes
1609
+ this._dropdownObserver = new MutationObserver((mutations) => {
1610
+ mutations.forEach((mutation) => {
1611
+ if (mutation.attributeName === "aria-hidden") {
1612
+ const isOpen = menu.getAttribute("aria-hidden") === "false";
1613
+
1614
+ if (isOpen && !this._dropdownMenuOpen) {
1615
+ // Dropdown just opened - pause mouseover
1616
+ this._dropdownMenuOpen = true;
1617
+ this._removeMouseOverListener();
1618
+ } else if (!isOpen && this._dropdownMenuOpen) {
1619
+ // Dropdown just closed - resume mouseover
1620
+ this._dropdownMenuOpen = false;
1621
+ this._addMouseOverListener();
1622
+ }
1623
+ }
1624
+ });
1625
+ });
1626
+
1627
+ this._dropdownObserver.observe(menu, {
1628
+ attributes: true,
1629
+ attributeFilter: ["aria-hidden"]
1630
+ });
1631
+ }
1632
+
1633
+ _unwatchDropdownState() {
1634
+ if (this._dropdownObserver) {
1635
+ this._dropdownObserver.disconnect();
1636
+ this._dropdownObserver = null;
1637
+ }
1638
+ }
1639
+
1640
+ _isPointerWithinSafeZone() {
1641
+ if (!this._lastPointer || !this._activeTarget || !this._activeDropdown) return false;
1642
+ const targetRect = this._activeTarget.getBoundingClientRect();
1643
+ const dropdownRect = this._activeDropdown.getBoundingClientRect();
1644
+ const padding = 12;
1645
+ const left = Math.min(targetRect.left, dropdownRect.left) - padding;
1646
+ const right = Math.max(targetRect.right, dropdownRect.right) + padding;
1647
+ const top = Math.min(targetRect.top, dropdownRect.top) - padding;
1648
+ const bottom = Math.max(targetRect.bottom, dropdownRect.bottom) + padding;
1649
+ const { x, y } = this._lastPointer;
1650
+ return x >= left && x <= right && y >= top && y <= bottom;
1651
+ }
1652
+
1653
+ _buildDropdown(target, quickPaths, hints, debug) {
1654
+ const nav = document.createElement("nav");
1655
+ nav.className = DROPDOWN_CLASS;
1656
+ nav.setAttribute("data-dropdown", "");
1657
+ nav.setAttribute("data-direction", "auto");
1658
+ nav.setAttribute("data-mode", "auto");
1659
+
1660
+ const button = document.createElement("button");
1661
+ button.className = `context-edit btn-primary btn-xs icon-only ${MARKER_CLASS}`;
1662
+ button.setAttribute("type", "button");
1663
+ button.setAttribute("data-direction", "auto");
1664
+ button.setAttribute("aria-label", "Edit design settings");
1665
+
1666
+ const icon = document.createElement("pds-icon");
1667
+ icon.setAttribute("icon", "pencil");
1668
+ icon.setAttribute("size", "sm");
1669
+ button.appendChild(icon);
1670
+
1671
+ const menu = document.createElement("menu");
1672
+ const quickItem = document.createElement("li");
1673
+ quickItem.className = "pds-live-editor-menu";
1674
+
1675
+ const header = document.createElement("div");
1676
+ header.className = "pds-live-editor-header";
1677
+
1678
+ const title = document.createElement("span");
1679
+ title.className = "pds-live-editor-title";
1680
+ title.textContent = "Quick edit";
1681
+ header.appendChild(title);
1682
+
1683
+ quickItem.appendChild(header);
1684
+
1685
+ const design = shallowClone(PDS?.currentConfig?.design || {});
1686
+ const formContainer = document.createElement("div");
1687
+ formContainer.className = "pds-live-editor-form-container";
1688
+ quickItem.appendChild(formContainer);
1689
+
1690
+ // Create footer with Apply/Undo/Gear buttons
1691
+ const footer = document.createElement("div");
1692
+ footer.className = "pds-live-editor-footer";
1693
+ quickItem.appendChild(footer);
1694
+
1695
+ menu.appendChild(quickItem);
1696
+
1697
+ nav.appendChild(button);
1698
+ nav.appendChild(menu);
1699
+
1700
+ const limitedPaths = quickPaths.slice(0, QUICK_EDIT_LIMIT);
1701
+ this._renderQuickForm(formContainer, footer, limitedPaths, design, hints, target, quickPaths);
1702
+
1703
+ // Log debug info to console instead of rendering
1704
+ if (debug && (debug.vars?.length || debug.paths?.length)) {
1705
+ const debugVars = (debug.vars || []).slice(0, 8).join(", ");
1706
+ const debugPaths = (debug.paths || []).slice(0, 8).join(", ");
1707
+ console.log(`[PDS Live Edit] vars: ${debugVars}`);
1708
+ console.log(`[PDS Live Edit] paths: ${debugPaths}`);
1709
+ }
1710
+
1711
+ return nav;
1712
+ }
1713
+
1714
+ async _renderQuickForm(container, footer, paths, design, hints, target, quickPaths) {
1715
+ container.replaceChildren();
1716
+ footer.replaceChildren();
1717
+
1718
+ const { form, applyBtn, undoBtn } = await buildForm(
1719
+ paths,
1720
+ design,
1721
+ (event) => this._handleFormSubmit(event, form),
1722
+ () => this._handleUndo(),
1723
+ hints
1724
+ );
1725
+
1726
+ // Store reference to undo button for enabling/disabling
1727
+ form._undoBtn = undoBtn;
1728
+
1729
+ // Disable undo initially if no history
1730
+ undoBtn.disabled = this._undoStack.length === 0;
1731
+
1732
+ // Add form to container
1733
+ container.appendChild(form);
1734
+
1735
+ // Create gear button for footer
1736
+ const gearBtn = document.createElement("button");
1737
+ gearBtn.className = "btn-outline btn-sm icon-only";
1738
+ gearBtn.type = "button";
1739
+ gearBtn.setAttribute("aria-label", "More settings");
1740
+ const gearIcon = document.createElement("pds-icon");
1741
+ gearIcon.setAttribute("icon", "caret-right");
1742
+ gearIcon.setAttribute("size", "sm");
1743
+ gearBtn.appendChild(gearIcon);
1744
+ gearBtn.addEventListener("click", (event) => {
1745
+ event.preventDefault();
1746
+ event.stopPropagation();
1747
+ this._openDrawer(target, quickPaths);
1748
+ this._removeActiveUI();
1749
+ });
1750
+
1751
+ // Add buttons to footer
1752
+ footer.appendChild(applyBtn);
1753
+ footer.appendChild(undoBtn);
1754
+ footer.appendChild(gearBtn);
1755
+ }
1756
+
1757
+ async _openDrawer(target, quickPaths) {
1758
+ if (!this._drawer) {
1759
+ this._drawer = document.createElement("pds-drawer");
1760
+ this._drawer.setAttribute("position", "right");
1761
+ this._drawer.setAttribute("show-close", "");
1762
+ this.appendChild(this._drawer);
1763
+ }
1764
+
1765
+ if (!customElements.get("pds-drawer")) {
1766
+ await customElements.whenDefined("pds-drawer");
1767
+ }
1768
+
1769
+ const header = document.createElement("div");
1770
+ header.setAttribute("slot", "drawer-header");
1771
+ header.className = "flex items-center justify-between";
1772
+ header.textContent = "Design settings";
1773
+
1774
+ const content = document.createElement("div");
1775
+ content.setAttribute("slot", "drawer-content");
1776
+ content.className = "stack-md";
1777
+
1778
+ const presetCard = document.createElement("section");
1779
+ presetCard.className = "card surface-elevated stack-sm";
1780
+
1781
+ const presetTitle = document.createElement("h4");
1782
+ presetTitle.textContent = "Preset";
1783
+ presetCard.appendChild(presetTitle);
1784
+
1785
+ const presetLabel = document.createElement("label");
1786
+ presetLabel.className = "stack-xs";
1787
+
1788
+ const presetText = document.createElement("span");
1789
+ presetText.textContent = "Choose a base style";
1790
+ presetLabel.appendChild(presetText);
1791
+
1792
+ const presetSelect = document.createElement("select");
1793
+ const presetOptions = getPresetOptions();
1794
+ const activePreset = getActivePresetId();
1795
+
1796
+ presetOptions.forEach((preset) => {
1797
+ const option = document.createElement("option");
1798
+ option.value = preset.id;
1799
+ option.textContent = preset.name;
1800
+ if (String(preset.id) === String(activePreset)) {
1801
+ option.selected = true;
1802
+ }
1803
+ presetSelect.appendChild(option);
1804
+ });
1805
+
1806
+ presetSelect.addEventListener("change", async (event) => {
1807
+ const nextPreset = event.target?.value;
1808
+ await applyPresetSelection(nextPreset);
1809
+ });
1810
+
1811
+ presetLabel.appendChild(presetSelect);
1812
+ presetCard.appendChild(presetLabel);
1813
+
1814
+ const themeCard = document.createElement("section");
1815
+ themeCard.className = "card surface-elevated stack-sm";
1816
+
1817
+ const themeTitle = document.createElement("h4");
1818
+ themeTitle.textContent = "Theme";
1819
+ themeCard.appendChild(themeTitle);
1820
+
1821
+ const themeToggle = document.createElement("pds-theme");
1822
+ themeCard.appendChild(themeToggle);
1823
+
1824
+ const searchCard = document.createElement("section");
1825
+ searchCard.className = "card surface-elevated stack-sm";
1826
+
1827
+ const searchTitle = document.createElement("h4");
1828
+ searchTitle.textContent = "Search PDS";
1829
+ searchCard.appendChild(searchTitle);
1830
+
1831
+ const omnibox = document.createElement("pds-omnibox");
1832
+ omnibox.setAttribute("placeholder", "Search tokens, utilities, components...");
1833
+ omnibox.settings = {
1834
+ iconHandler: (item) => {
1835
+ return item.icon ? `<pds-icon icon="${item.icon}"></pds-icon>` : null;
1836
+ },
1837
+ categories: {
1838
+ Query: {
1839
+ trigger: (options) => options.search.length >= 2,
1840
+ getItems: async (options) => {
1841
+ const query = (options.search || "").trim();
1842
+ if (!query) return [];
1843
+ try {
1844
+ const results = await PDS.query(query);
1845
+ return (results || []).map((result) => ({
1846
+ text: result.text,
1847
+ id: result.value,
1848
+ icon: result.icon || "magnifying-glass",
1849
+ category: result.category,
1850
+ code: result.code,
1851
+ }));
1852
+ } catch (error) {
1853
+ console.warn("Omnibox query failed:", error);
1854
+ return [];
1855
+ }
1856
+ },
1857
+ action: async (options) => {
1858
+ if (options?.code && navigator.clipboard) {
1859
+ await navigator.clipboard.writeText(options.code);
1860
+ await PDS.toast("Copied token to clipboard", { type: "success" });
1861
+ }
1862
+ },
1863
+ },
1864
+ },
1865
+ };
1866
+ searchCard.appendChild(omnibox);
1867
+
1868
+ content.appendChild(presetCard);
1869
+ content.appendChild(themeCard);
1870
+ content.appendChild(searchCard);
1871
+
1872
+ this._drawer.replaceChildren(header, content);
1873
+
1874
+ if (typeof this._drawer.openDrawer === "function") {
1875
+ this._drawer.openDrawer();
1876
+ } else {
1877
+ this._drawer.setAttribute("open", "");
1878
+ }
1879
+ }
1880
+
1881
+ async _handleFormSubmit(event, form) {
1882
+ if (!form || typeof form.getValuesFlat !== "function") return;
1883
+
1884
+ // Save current STORED config (preset + overrides) to undo stack before applying
1885
+ const storedConfig = getStoredConfig() || { preset: null, design: {} };
1886
+ try {
1887
+ this._undoStack.push(structuredClone(storedConfig));
1888
+ } catch (e) {
1889
+ // Fallback for environments without structuredClone
1890
+ this._undoStack.push(JSON.parse(JSON.stringify(storedConfig)));
1891
+ }
1892
+
1893
+ // Limit undo stack size
1894
+ if (this._undoStack.length > 10) {
1895
+ this._undoStack.shift();
1896
+ }
1897
+
1898
+ // Apply the changes
1899
+ const flatValues = form.getValuesFlat();
1900
+ const patch = {};
1901
+ Object.entries(flatValues || {}).forEach(([path, value]) => {
1902
+ setValueAtJsonPath(patch, path, value);
1903
+ });
1904
+ await applyDesignPatch(patch);
1905
+
1906
+ // Enable undo button
1907
+ if (form._undoBtn) {
1908
+ form._undoBtn.disabled = false;
1909
+ }
1910
+ }
1911
+
1912
+ async _handleUndo() {
1913
+ if (this._undoStack.length === 0) return;
1914
+
1915
+ // Get the previous stored config (preset + overrides)
1916
+ const previousConfig = this._undoStack.pop();
1917
+
1918
+ // Update localStorage to fully replace with previous config
1919
+ setStoredConfig(previousConfig);
1920
+
1921
+ // Apply an empty patch to trigger regeneration with the restored design
1922
+ await applyDesignPatch({});
1923
+
1924
+ // Re-render the form with the current dropdown's form container
1925
+ if (this._activeDropdown) {
1926
+ const formContainer = this._activeDropdown.querySelector('.pds-live-editor-form-container');
1927
+ const footer = this._activeDropdown.querySelector('.pds-live-editor-footer');
1928
+ if (formContainer && footer) {
1929
+ // Get the actual current design after applyDesignPatch has run
1930
+ const currentDesign = shallowClone(PDS?.currentConfig?.design || {});
1931
+ const quickContext = collectQuickContext(this._activeTarget);
1932
+ const limitedPaths = quickContext.paths.slice(0, QUICK_EDIT_LIMIT);
1933
+ const quickPaths = quickContext.paths;
1934
+ await this._renderQuickForm(formContainer, footer, limitedPaths, currentDesign, quickContext.hints, this._activeTarget, quickPaths);
1935
+ }
1936
+ }
1937
+ }
1938
+ }
1939
+
1555
1940
  customElements.define(EDITOR_TAG, PdsLiveEdit);