@pure-ds/core 0.6.9 → 0.6.11

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.
Files changed (90) hide show
  1. package/custom-elements.json +865 -35
  2. package/dist/types/pds.d.ts +31 -0
  3. package/dist/types/public/assets/js/pds-manager.d.ts +100 -2
  4. package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
  5. package/dist/types/public/assets/js/pds.d.ts.map +1 -1
  6. package/dist/types/public/assets/pds/components/pds-form.d.ts.map +1 -1
  7. package/dist/types/public/assets/pds/components/pds-live-converter.d.ts +8 -0
  8. package/dist/types/public/assets/pds/components/pds-live-converter.d.ts.map +1 -0
  9. package/dist/types/public/assets/pds/components/pds-live-edit.d.ts +1 -195
  10. package/dist/types/public/assets/pds/components/pds-live-edit.d.ts.map +1 -1
  11. package/dist/types/public/assets/pds/components/pds-live-importer.d.ts +2 -0
  12. package/dist/types/public/assets/pds/components/pds-live-importer.d.ts.map +1 -0
  13. package/dist/types/public/assets/pds/components/pds-live-template-canvas.d.ts +2 -0
  14. package/dist/types/public/assets/pds/components/pds-live-template-canvas.d.ts.map +1 -0
  15. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts +0 -2
  16. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts.map +1 -1
  17. package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts +20 -0
  18. package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts.map +1 -1
  19. package/dist/types/public/assets/pds/components/pds-toaster.d.ts +1 -1
  20. package/dist/types/public/assets/pds/components/pds-toaster.d.ts.map +1 -1
  21. package/dist/types/public/assets/pds/components/pds-treeview.d.ts +37 -0
  22. package/dist/types/public/assets/pds/components/pds-treeview.d.ts.map +1 -0
  23. package/dist/types/src/js/common/toast.d.ts +8 -0
  24. package/dist/types/src/js/common/toast.d.ts.map +1 -1
  25. package/dist/types/src/js/pds-core/pds-config.d.ts +1306 -13
  26. package/dist/types/src/js/pds-core/pds-config.d.ts.map +1 -1
  27. package/dist/types/src/js/pds-core/pds-enhancers-meta.d.ts.map +1 -1
  28. package/dist/types/src/js/pds-core/pds-enhancers.d.ts.map +1 -1
  29. package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
  30. package/dist/types/src/js/pds-core/pds-live.d.ts +2 -1
  31. package/dist/types/src/js/pds-core/pds-live.d.ts.map +1 -1
  32. package/dist/types/src/js/pds-core/pds-ontology.d.ts.map +1 -1
  33. package/dist/types/src/js/pds-core/pds-start-helpers.d.ts +1 -4
  34. package/dist/types/src/js/pds-core/pds-start-helpers.d.ts.map +1 -1
  35. package/dist/types/src/js/pds-live-manager/conversion-service.d.ts +66 -0
  36. package/dist/types/src/js/pds-live-manager/conversion-service.d.ts.map +1 -0
  37. package/dist/types/src/js/pds-live-manager/import-contract.d.ts +15 -0
  38. package/dist/types/src/js/pds-live-manager/import-contract.d.ts.map +1 -0
  39. package/dist/types/src/js/pds-live-manager/import-history-service.d.ts +32 -0
  40. package/dist/types/src/js/pds-live-manager/import-history-service.d.ts.map +1 -0
  41. package/dist/types/src/js/pds-live-manager/import-service.d.ts +21 -0
  42. package/dist/types/src/js/pds-live-manager/import-service.d.ts.map +1 -0
  43. package/dist/types/src/js/pds-live-manager/template-service.d.ts +17 -0
  44. package/dist/types/src/js/pds-live-manager/template-service.d.ts.map +1 -0
  45. package/dist/types/src/js/pds-manager.d.ts +4 -0
  46. package/dist/types/src/js/pds.d.ts.map +1 -1
  47. package/package.json +7 -3
  48. package/packages/pds-cli/README.md +51 -0
  49. package/packages/pds-cli/bin/pds-import.js +176 -0
  50. package/packages/pds-cli/bin/pds-static.js +31 -1
  51. package/packages/pds-cli/bin/postinstall.mjs +17 -8
  52. package/public/assets/js/app.js +23 -147
  53. package/public/assets/js/pds-manager.js +481 -248
  54. package/public/assets/js/pds.js +16 -16
  55. package/public/assets/pds/components/pds-form.js +124 -27
  56. package/public/assets/pds/components/pds-live-converter.js +47 -0
  57. package/public/assets/pds/components/pds-live-edit.js +1626 -211
  58. package/public/assets/pds/components/pds-live-importer.js +772 -0
  59. package/public/assets/pds/components/pds-live-template-canvas.js +171 -0
  60. package/public/assets/pds/components/pds-omnibox.js +146 -20
  61. package/public/assets/pds/components/pds-scrollrow.js +56 -1
  62. package/public/assets/pds/components/pds-toaster.js +50 -5
  63. package/public/assets/pds/components/pds-treeview.js +972 -0
  64. package/public/assets/pds/custom-elements.json +865 -35
  65. package/public/assets/pds/pds-css-complete.json +7 -7
  66. package/public/assets/pds/pds.css-data.json +5 -35
  67. package/public/assets/pds/templates/commerce-scroll-explorer.html +115 -0
  68. package/public/assets/pds/templates/content-brand-showcase.html +110 -0
  69. package/public/assets/pds/templates/feedback-ops-dashboard.html +91 -0
  70. package/public/assets/pds/templates/release-readiness-radar.html +69 -0
  71. package/public/assets/pds/templates/support-command-center.html +92 -0
  72. package/public/assets/pds/templates/templates.json +53 -0
  73. package/public/assets/pds/templates/workspace-settings-lab.html +131 -0
  74. package/public/assets/pds/vscode-custom-data.json +54 -4
  75. package/readme.md +34 -0
  76. package/src/js/pds-core/pds-config.js +831 -40
  77. package/src/js/pds-core/pds-enhancers-meta.js +11 -0
  78. package/src/js/pds-core/pds-enhancers.js +259 -5
  79. package/src/js/pds-core/pds-generator.js +353 -52
  80. package/src/js/pds-core/pds-live.js +630 -15
  81. package/src/js/pds-core/pds-ontology.js +6 -0
  82. package/src/js/pds-core/pds-start-helpers.js +14 -6
  83. package/src/js/pds-live-manager/conversion-service.js +3136 -0
  84. package/src/js/pds-live-manager/import-contract.js +57 -0
  85. package/src/js/pds-live-manager/import-history-service.js +145 -0
  86. package/src/js/pds-live-manager/import-service.js +255 -0
  87. package/src/js/pds-live-manager/tailwind-conversion-rules.json +383 -0
  88. package/src/js/pds-live-manager/template-service.js +170 -0
  89. package/src/js/pds.d.ts +31 -0
  90. package/src/js/pds.js +71 -60
@@ -96,9 +96,42 @@ const QUICK_STYLE_PROPERTIES = [
96
96
  "width",
97
97
  ];
98
98
 
99
+ const LIVE_EDIT_HIGHLIGHT_BLACKLIST = [
100
+ {
101
+ id: "toaster",
102
+ selector: "pds-toaster",
103
+ includeDescendants: true,
104
+ },
105
+ {
106
+ id: "live-edit-toggle",
107
+ selector: "#pds-live-edit-toggle",
108
+ includeDescendants: true,
109
+ },
110
+ {
111
+ id: "ask-dialog",
112
+ selector: "dialog",
113
+ includeDescendants: true,
114
+ },
115
+ ];
116
+
117
+ function isLiveEditHighlightBlacklisted(node) {
118
+ if (!(node instanceof Element)) return false;
119
+ return LIVE_EDIT_HIGHLIGHT_BLACKLIST.some((entry) => {
120
+ if (!entry?.selector) return false;
121
+ if (entry.includeDescendants) {
122
+ return Boolean(node.closest(entry.selector));
123
+ }
124
+ return node.matches(entry.selector);
125
+ });
126
+ }
127
+
99
128
  const INLINE_VAR_REGEX = /var\(\s*(--[^)\s,]+)\s*/g;
100
129
  const CUSTOM_PROP_REGEX = /--.+/;
101
130
  const COLOR_VALUE_REGEX = /#(?:[0-9a-f]{3,8})\b|rgba?\([^)]*\)|hsla?\([^)]*\)/gi;
131
+ const SHIKI_BUNDLE_URL = "https://esm.sh/shiki@1.29.2?bundle";
132
+ const SETTINGS_ONLY_ATTR = "data-pds-live-settings-only";
133
+
134
+ let shikiModulePromise = null;
102
135
 
103
136
  const ENUM_FIELD_OPTIONS = {
104
137
  "shape.borderWidth": ["hairline", "thin", "medium", "thick"],
@@ -108,6 +141,47 @@ let cachedTokenIndex = null;
108
141
  let cachedTokenIndexMeta = null;
109
142
  let colorNormalizer = null;
110
143
 
144
+ function escapeHtml(value) {
145
+ return String(value || "")
146
+ .replaceAll("&", "&")
147
+ .replaceAll("<", "&lt;")
148
+ .replaceAll(">", "&gt;")
149
+ .replaceAll('"', "&quot;")
150
+ .replaceAll("'", "&#39;");
151
+ }
152
+
153
+ async function loadShikiModule() {
154
+ if (shikiModulePromise) return shikiModulePromise;
155
+ shikiModulePromise = import(SHIKI_BUNDLE_URL).catch(() => null);
156
+ return shikiModulePromise;
157
+ }
158
+
159
+ function resolveShikiTheme() {
160
+ const isDark =
161
+ PDS?.theme === "dark" ||
162
+ document.documentElement.getAttribute("data-theme") === "dark" ||
163
+ document.documentElement.classList.contains("theme-dark");
164
+ return isDark ? "github-dark-default" : "github-light-default";
165
+ }
166
+
167
+ async function renderHtmlWithShiki(code = "") {
168
+ const fallback = `<pre><code>${escapeHtml(code)}</code></pre>`;
169
+ const shiki = await loadShikiModule();
170
+
171
+ if (!shiki || typeof shiki.codeToHtml !== "function") {
172
+ return fallback;
173
+ }
174
+
175
+ try {
176
+ return shiki.codeToHtml(String(code || ""), {
177
+ lang: "html",
178
+ theme: resolveShikiTheme(),
179
+ });
180
+ } catch (error) {
181
+ return fallback;
182
+ }
183
+ }
184
+
111
185
  const GLOBAL_LAYOUT_PATHS = new Set([
112
186
  "layout.maxWidth",
113
187
  "layout.maxWidths",
@@ -138,6 +212,8 @@ const DARK_MODE_PATH_MARKER = ".darkMode.";
138
212
  const QUICK_EDIT_LIMIT = 4;
139
213
  const DROPDOWN_VIEWPORT_PADDING = 8;
140
214
  const FONT_FAMILY_PATH_REGEX = /^typography\.fontFamily/i;
215
+ const FORM_THEME_CONTEXT_FIELD = "__pdsThemeContext";
216
+ const FORM_THEME_CONTEXT_POINTER = `/${FORM_THEME_CONTEXT_FIELD}`;
141
217
 
142
218
  function isHoverCapable() {
143
219
  if (typeof window === "undefined" || !window.matchMedia) return false;
@@ -188,8 +264,13 @@ ${EDITOR_TAG} {
188
264
  .${DROPDOWN_CLASS} menu {
189
265
  min-width: max-content;
190
266
  max-width: 350px;
267
+ margin: 0;
268
+ padding: 0;
269
+ list-style: none;
191
270
  }
192
271
  .${DROPDOWN_CLASS} .pds-live-editor-menu {
272
+ display: block;
273
+ background-color: var(--color-surface-base);
193
274
  padding: var(--spacing-1);
194
275
  max-width: 350px;
195
276
  padding-bottom: 0;
@@ -230,8 +311,41 @@ ${EDITOR_TAG} {
230
311
  background: var(--color-surface-base);
231
312
  position: sticky;
232
313
  justify-content: space-between;
314
+ align-items: center;
315
+ bottom: 0;
316
+ z-index: 1;
317
+ }
318
+ .pds-live-editor-drawer-footer {
319
+ position: sticky;
233
320
  bottom: 0;
234
321
  z-index: 1;
322
+ padding-top: var(--spacing-3);
323
+ padding-bottom: env(safe-area-inset-bottom, 0);
324
+ border-top: var(--border-width-thin) solid var(--color-border);
325
+ background: var(--color-surface-base);
326
+ }
327
+ .pds-live-editor-drawer-footer > button {
328
+ width: 100%;
329
+ justify-content: center;
330
+ }
331
+ .pds-live-editor-drawer-footer .pds-live-editor-reset-btn {
332
+ color: var(--color-danger-700);
333
+ border-color: var(--color-danger-700);
334
+ }
335
+ .pds-live-editor-drawer-footer .pds-live-editor-reset-btn:hover,
336
+ .pds-live-editor-drawer-footer .pds-live-editor-reset-btn:focus-visible,
337
+ .pds-live-editor-drawer-footer .pds-live-editor-reset-btn:active {
338
+ color: var(--color-danger-700);
339
+ border-color: var(--color-danger-700);
340
+ }
341
+ .pds-live-editor-drawer-content {
342
+ min-height: 100%;
343
+ display: grid;
344
+ grid-template-rows: minmax(0, 1fr) auto;
345
+ gap: var(--spacing-4);
346
+ }
347
+ .pds-live-editor-drawer-content > .accordion {
348
+ min-height: 0;
235
349
  }
236
350
  `;
237
351
  document.head.appendChild(style);
@@ -311,6 +425,14 @@ function deepMerge(target = {}, source = {}) {
311
425
  return out;
312
426
  }
313
427
 
428
+ function markNodeTreeAsLiveEditIgnored(node) {
429
+ if (!(node instanceof Element)) return;
430
+ node.setAttribute("data-pds-live-edit-ignore", "true");
431
+ node.querySelectorAll("*").forEach((element) => {
432
+ element.setAttribute("data-pds-live-edit-ignore", "true");
433
+ });
434
+ }
435
+
314
436
  function titleize(value) {
315
437
  return String(value)
316
438
  .replace(/([a-z])([A-Z])/g, "$1 $2")
@@ -589,6 +711,62 @@ function toColorInputValue(value) {
589
711
  return hexValue || value;
590
712
  }
591
713
 
714
+ function isColorPath(path) {
715
+ return String(path || "").toLowerCase().startsWith("colors.");
716
+ }
717
+
718
+ function inferColorVariableCandidates(path) {
719
+ const normalizedPath = String(path || "").toLowerCase();
720
+ const key = normalizedPath.replace(/^colors\./, "").replace(/^darkmode\./, "");
721
+ const tail = key.split(".").pop();
722
+
723
+ const directMap = {
724
+ primary: ["--color-primary-500"],
725
+ secondary: ["--color-secondary-500", "--color-gray-500"],
726
+ accent: ["--color-accent-500"],
727
+ background: ["--color-surface-base"],
728
+ success: ["--color-success-500"],
729
+ warning: ["--color-warning-500"],
730
+ danger: ["--color-danger-500"],
731
+ info: ["--color-info-500"],
732
+ };
733
+
734
+ const candidates = new Set();
735
+ if (tail && directMap[tail]) {
736
+ directMap[tail].forEach((item) => candidates.add(item));
737
+ }
738
+ if (tail) {
739
+ candidates.add(`--color-${tail}-500`);
740
+ }
741
+
742
+ return Array.from(candidates);
743
+ }
744
+
745
+ function resolveColorValueForPath(path, value, hintValue) {
746
+ if (!isColorPath(path)) return null;
747
+
748
+ const fromValue = toColorInputValue(value);
749
+ if (normalizeHexColor(fromValue)) return fromValue;
750
+
751
+ const fromHint = toColorInputValue(hintValue);
752
+ if (normalizeHexColor(fromHint)) return fromHint;
753
+
754
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
755
+ const root = document.documentElement;
756
+ if (!root) return null;
757
+
758
+ const style = window.getComputedStyle(root);
759
+ const candidates = inferColorVariableCandidates(path);
760
+ for (const varName of candidates) {
761
+ const raw = style.getPropertyValue(varName).trim();
762
+ if (!raw) continue;
763
+ const resolved = toColorInputValue(raw);
764
+ if (normalizeHexColor(resolved)) return resolved;
765
+ }
766
+
767
+ return null;
768
+ }
769
+
592
770
  function getCustomPropertyNames(style) {
593
771
  const names = [];
594
772
  if (!style) return names;
@@ -929,7 +1107,105 @@ function splitFontFamilyStack(value) {
929
1107
  return parts;
930
1108
  }
931
1109
 
932
- function getPresetFontFamilyVariations() {
1110
+ const GENERIC_FONT_FAMILIES = new Set([
1111
+ "serif",
1112
+ "sans-serif",
1113
+ "monospace",
1114
+ "cursive",
1115
+ "fantasy",
1116
+ "system-ui",
1117
+ "ui-serif",
1118
+ "ui-sans-serif",
1119
+ "ui-monospace",
1120
+ "ui-rounded",
1121
+ "emoji",
1122
+ "math",
1123
+ "fangsong",
1124
+ ]);
1125
+
1126
+ let loadGoogleFontFnPromise = null;
1127
+ let managerModulePromise = null;
1128
+
1129
+ function normalizeFontName(fontFamily) {
1130
+ return String(fontFamily || "")
1131
+ .trim()
1132
+ .replace(/^['"]+|['"]+$/g, "")
1133
+ .trim();
1134
+ }
1135
+
1136
+ function isLikelyLoadableFont(fontFamily) {
1137
+ const normalized = normalizeFontName(fontFamily).toLowerCase();
1138
+ if (!normalized) return false;
1139
+ return !GENERIC_FONT_FAMILIES.has(normalized);
1140
+ }
1141
+
1142
+ async function getLoadGoogleFontFn() {
1143
+ if (typeof PDS?.loadGoogleFont === "function") {
1144
+ return PDS.loadGoogleFont;
1145
+ }
1146
+ if (loadGoogleFontFnPromise) return loadGoogleFontFnPromise;
1147
+ loadGoogleFontFnPromise = (async () => {
1148
+ const manager = await getPdsManagerModule();
1149
+ if (typeof manager?.loadGoogleFont === "function") {
1150
+ return manager.loadGoogleFont;
1151
+ }
1152
+ return null;
1153
+ })();
1154
+ return loadGoogleFontFnPromise;
1155
+ }
1156
+
1157
+ async function getPdsManagerModule() {
1158
+ if (managerModulePromise) return managerModulePromise;
1159
+
1160
+ managerModulePromise = (async () => {
1161
+ const candidates = [
1162
+ PDS?.currentConfig?.managerURL,
1163
+ "../core/pds-manager.js",
1164
+ "/assets/pds/core/pds-manager.js",
1165
+ ].filter(Boolean);
1166
+
1167
+ const attempted = new Set();
1168
+ for (const candidate of candidates) {
1169
+ try {
1170
+ const resolved = new URL(candidate, import.meta.url).href;
1171
+ if (attempted.has(resolved)) continue;
1172
+ attempted.add(resolved);
1173
+ const mod = await import(resolved);
1174
+ if (mod && typeof mod === "object") return mod;
1175
+ } catch (e) {}
1176
+ }
1177
+ return null;
1178
+ })();
1179
+
1180
+ return managerModulePromise;
1181
+ }
1182
+
1183
+ async function loadTypographyFontsForDesign(typography) {
1184
+ if (!typography || typeof typography !== "object") return;
1185
+
1186
+ const loadGoogleFont = await getLoadGoogleFontFn();
1187
+ if (typeof loadGoogleFont !== "function") return;
1188
+
1189
+ const families = [
1190
+ typography.fontFamilyHeadings,
1191
+ typography.fontFamilyBody,
1192
+ typography.fontFamilyMono,
1193
+ ];
1194
+
1195
+ const fontNames = new Set();
1196
+ families.forEach((stack) => {
1197
+ splitFontFamilyStack(stack).forEach((item) => {
1198
+ const fontName = normalizeFontName(item);
1199
+ if (isLikelyLoadableFont(fontName)) {
1200
+ fontNames.add(fontName);
1201
+ }
1202
+ });
1203
+ });
1204
+
1205
+ await Promise.allSettled(Array.from(fontNames).map((name) => loadGoogleFont(name)));
1206
+ }
1207
+
1208
+ function getPresetFontFamilyVariations(previewFontSize = resolveFontFamilyPreviewFontSize()) {
933
1209
  const presets = Object.values(PDS?.presets || {});
934
1210
  const seen = new Set();
935
1211
  const items = [];
@@ -938,9 +1214,11 @@ function getPresetFontFamilyVariations() {
938
1214
  if (!normalized || seen.has(normalized)) return;
939
1215
  seen.add(normalized);
940
1216
  items.push({
1217
+ //index: items.length,
941
1218
  id: normalized,
1219
+ value: normalized,
942
1220
  text: normalized,
943
- style: `font-family: ${normalized}`,
1221
+ style: `font-family: ${normalized}; font-size: ${previewFontSize};`,
944
1222
  });
945
1223
  };
946
1224
 
@@ -959,35 +1237,95 @@ function getPresetFontFamilyVariations() {
959
1237
  });
960
1238
  });
961
1239
 
962
- return items;
1240
+ return items.sort((a, b) =>
1241
+ String(b?.text || "").localeCompare(String(a?.text || ""), undefined, {
1242
+ sensitivity: "base",
1243
+ })
1244
+ );
1245
+ }
1246
+
1247
+ function resolveFontFamilyPreviewFontSize(control) {
1248
+ const controlInput = control?.querySelector?.(".ac-input");
1249
+ if (controlInput) {
1250
+ const fontSize = getComputedStyle(controlInput).fontSize;
1251
+ if (fontSize) return fontSize;
1252
+ }
1253
+
1254
+ const selectors = [
1255
+ "[name='/typography/fontFamilyBody']",
1256
+ "[name='/typography/fontFamilyHeadings']",
1257
+ "[name='/typography/fontFamilyMono']",
1258
+ ];
1259
+
1260
+ for (const selector of selectors) {
1261
+ const omnibox = document.querySelector(selector);
1262
+ const input = omnibox?.shadowRoot?.querySelector?.(".ac-input");
1263
+ if (!input) continue;
1264
+ const fontSize = getComputedStyle(input).fontSize;
1265
+ if (fontSize) return fontSize;
1266
+ }
1267
+
1268
+ return "var(--font-size-md)";
1269
+ }
1270
+
1271
+ async function loadGoogleFontsForFontFamilyItems(items) {
1272
+ if (!Array.isArray(items) || !items.length) return;
1273
+
1274
+ const loadGoogleFont = await getLoadGoogleFontFn();
1275
+ if (typeof loadGoogleFont !== "function") return;
1276
+
1277
+ const fontNames = new Set();
1278
+ items.forEach((item) => {
1279
+ const stack = item?.value || item?.text;
1280
+ splitFontFamilyStack(stack).forEach((entry) => {
1281
+ const fontName = normalizeFontName(entry);
1282
+ if (isLikelyLoadableFont(fontName)) {
1283
+ fontNames.add(fontName);
1284
+ }
1285
+ });
1286
+ });
1287
+
1288
+ if (!fontNames.size) return;
1289
+ await Promise.allSettled(Array.from(fontNames).map((name) => loadGoogleFont(name)));
963
1290
  }
964
1291
 
965
1292
  function buildFontFamilyOmniboxSettings() {
966
- const allItems = getPresetFontFamilyVariations();
967
- const filterItems = (search) => {
1293
+ const filterItems = (items, search) => {
968
1294
  const query = String(search || "").trim().toLowerCase();
969
- if (!query) return allItems;
970
- return allItems.filter((item) => item.text.toLowerCase().includes(query));
1295
+ if (!query) return items;
1296
+ return items.filter((item) => {
1297
+ const text = String(
1298
+ item?.text || item?.id || item?.element?.textContent || ""
1299
+ ).toLowerCase();
1300
+ return text.includes(query);
1301
+ });
971
1302
  };
972
1303
 
973
- return {
1304
+
1305
+ return {
1306
+ //debug: true,
1307
+ itemGrid: "0 1fr 0",
974
1308
  hideCategory: true,
975
- iconHandler: (item) => {
976
-
1309
+ iconHandler: (item) => {
977
1310
  return "";
978
1311
  },
979
1312
  categories: {
980
1313
  FontFamilies: {
981
1314
  trigger: () => true,
982
- getItems: (options) => filterItems(options?.search),
983
- action: (options, ev) => {
984
- const input = document.querySelector("[name='/typography/fontFamilyHeadings']");
1315
+ getItems: async (options) => {
1316
+ const previewFontSize = resolveFontFamilyPreviewFontSize(options?.control);
1317
+ const allItems = getPresetFontFamilyVariations(previewFontSize);
1318
+ const items = filterItems(allItems, options?.search);
985
1319
 
986
- if (input && input.tagName === "PDS-OMNIBOX") {
987
- input.value = options?.text || "";
988
-
1320
+ await loadGoogleFontsForFontFamilyItems(items);
1321
+
1322
+ return items;
1323
+ },
1324
+ action: (options) => {
1325
+ const input = document.querySelector("pds-omnibox");
1326
+ if (input) {
1327
+ input.value = options.text;
989
1328
  }
990
- return options?.text || options?.id;
991
1329
  },
992
1330
  },
993
1331
  },
@@ -1034,7 +1372,7 @@ function buildSchemaFromPaths(paths, design, hints = {}) {
1034
1372
  };
1035
1373
 
1036
1374
  const isColorValue = (value, path) => {
1037
- if (String(path || "").toLowerCase().startsWith("colors.")) return true;
1375
+ if (isColorPath(path)) return true;
1038
1376
  if (typeof value !== "string") return false;
1039
1377
  return /^#([0-9a-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
1040
1378
  };
@@ -1064,8 +1402,12 @@ function buildSchemaFromPaths(paths, design, hints = {}) {
1064
1402
  const value = getValueAtPath(design, [category, ...rest]);
1065
1403
  const hintValue = hints[path];
1066
1404
  const enumOptions = getEnumOptions(path);
1067
- const normalizedValue = normalizeEnumValue(path, value);
1068
- const normalizedHint = normalizeEnumValue(path, hintValue);
1405
+ const resolvedColorValue = resolveColorValueForPath(path, value, hintValue);
1406
+ const normalizedValue = normalizeEnumValue(path, resolvedColorValue ?? value);
1407
+ const normalizedHint = normalizeEnumValue(
1408
+ path,
1409
+ resolvedColorValue ?? hintValue,
1410
+ );
1069
1411
  const inferredType = Array.isArray(value)
1070
1412
  ? "array"
1071
1413
  : value === null
@@ -1232,6 +1574,10 @@ async function applyDesignPatch(patch) {
1232
1574
  const nextOptions = { ...currentOptions, design: nextDesign };
1233
1575
  if (resolvedPresetId) nextOptions.preset = resolvedPresetId;
1234
1576
 
1577
+ try {
1578
+ await loadTypographyFontsForDesign(nextDesign?.typography);
1579
+ } catch (e) {}
1580
+
1235
1581
  const nextGenerator = new Generator(nextOptions);
1236
1582
  if (PDS?.applyStyles) {
1237
1583
  await PDS.applyStyles(nextGenerator);
@@ -1307,6 +1653,17 @@ function getActivePresetId() {
1307
1653
  return stored?.preset || PDS?.currentConfig?.preset || PDS?.currentPreset || null;
1308
1654
  }
1309
1655
 
1656
+ function getPresetNameById(presetId) {
1657
+ if (!presetId) return "";
1658
+ const presets = PDS?.presets || {};
1659
+ const preset =
1660
+ presets?.[presetId] ||
1661
+ Object.values(presets || {}).find(
1662
+ (candidate) => String(candidate?.id || candidate?.name) === String(presetId)
1663
+ );
1664
+ return preset?.name || String(presetId);
1665
+ }
1666
+
1310
1667
  async function applyPresetSelection(presetId) {
1311
1668
  if (!presetId) return;
1312
1669
  setStoredConfig({
@@ -1466,18 +1823,58 @@ async function exportFromLiveEdit(format) {
1466
1823
  }
1467
1824
 
1468
1825
  function setFormSchemas(form, schema, uiSchema, design) {
1469
- form.jsonSchema = schema;
1470
- form.uiSchema = uiSchema;
1471
- form.values = shallowClone(design);
1826
+ const themedSchema = withThemeConditionalSchema(schema, uiSchema);
1827
+ const formValues = withThemeContextValues(design);
1828
+ form.jsonSchema = themedSchema.schema;
1829
+ form.uiSchema = themedSchema.uiSchema;
1830
+ form.values = formValues;
1472
1831
  }
1473
1832
 
1474
- async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1475
- const { schema, uiSchema } = buildSchemaFromPaths(paths, design, hints);
1833
+ async function waitForElementDefinition(tagName, timeoutMs = 4000) {
1834
+ if (customElements.get(tagName)) return true;
1835
+
1836
+ let probe = null;
1837
+ try {
1838
+ if (typeof document !== "undefined" && document.body) {
1839
+ probe = document.createElement(tagName);
1840
+ probe.setAttribute("hidden", "");
1841
+ probe.setAttribute("aria-hidden", "true");
1842
+ probe.style.display = "none";
1843
+ document.body.appendChild(probe);
1844
+ }
1845
+ } catch (e) {}
1846
+
1847
+ await Promise.race([
1848
+ customElements.whenDefined(tagName),
1849
+ new Promise((_, reject) => {
1850
+ setTimeout(() => reject(new Error(`Timed out waiting for <${tagName}> definition`)), timeoutMs);
1851
+ }),
1852
+ ]);
1853
+
1854
+ try {
1855
+ if (probe && probe.parentNode) {
1856
+ probe.parentNode.removeChild(probe);
1857
+ }
1858
+ } catch (e) {}
1476
1859
 
1477
- if (!customElements.get("pds-form")) {
1478
- await customElements.whenDefined("pds-form");
1860
+ if (!customElements.get(tagName)) {
1861
+ throw new Error(`<${tagName}> is not defined`);
1479
1862
  }
1480
1863
 
1864
+ return true;
1865
+ }
1866
+
1867
+ async function createConfiguredForm({
1868
+ schema,
1869
+ uiSchema,
1870
+ values,
1871
+ onSubmit,
1872
+ onUndo,
1873
+ normalizeFlatValues,
1874
+ formOptions,
1875
+ }) {
1876
+ await waitForElementDefinition("pds-form");
1877
+
1481
1878
  const form = document.createElement("pds-form");
1482
1879
  const fontFamilyOmniboxSettings = buildFontFamilyOmniboxSettings();
1483
1880
  form.setAttribute("hide-actions", "");
@@ -1488,16 +1885,38 @@ async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1488
1885
  enhancements: {
1489
1886
  rangeOutput: true,
1490
1887
  },
1888
+ ...(formOptions && typeof formOptions === "object" ? formOptions : {}),
1491
1889
  };
1890
+
1492
1891
  form.defineRenderer(
1493
1892
  "font-family-omnibox",
1494
1893
  ({ id, path, value, attrs, set }) => {
1495
- const resolveSelectedValue = (options, actionResult) => {
1894
+ const resolveSelectedValue = (options, actionResult, selectionEvent) => {
1496
1895
  if (typeof actionResult === "string" && actionResult.trim()) {
1497
1896
  return actionResult;
1498
1897
  }
1898
+
1899
+ const eventDetail = selectionEvent?.detail;
1900
+ const fromEventValue = String(eventDetail?.value || "").trim();
1901
+ if (fromEventValue) return fromEventValue;
1902
+
1903
+ const fromEventText = String(eventDetail?.text || "").trim();
1904
+ if (fromEventText) return fromEventText;
1905
+
1906
+ const fromEventElementText = String(
1907
+ eventDetail?.element?.textContent || ""
1908
+ ).trim();
1909
+ if (fromEventElementText) return fromEventElementText;
1910
+
1499
1911
  const fromText = String(options?.text || "").trim();
1500
1912
  if (fromText) return fromText;
1913
+
1914
+ const fromValue = String(options?.value || "").trim();
1915
+ if (fromValue) return fromValue;
1916
+
1917
+ const fromElementText = String(options?.element?.textContent || "").trim();
1918
+ if (fromElementText) return fromElementText;
1919
+
1501
1920
  return String(options?.id || "").trim();
1502
1921
  };
1503
1922
 
@@ -1509,15 +1928,36 @@ async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1509
1928
  categoryName,
1510
1929
  {
1511
1930
  ...categoryConfig,
1512
- action: (options) => {
1931
+ action: (...args) => {
1932
+ const [options, selectionEvent] = args;
1513
1933
  const actionResult =
1514
1934
  typeof originalAction === "function"
1515
- ? originalAction(options)
1935
+ ? originalAction(...args)
1516
1936
  : undefined;
1517
- const selected = resolveSelectedValue(options, actionResult);
1937
+
1938
+ if (actionResult && typeof actionResult.then === "function") {
1939
+ return actionResult.then((resolved) => {
1940
+ const selected = resolveSelectedValue(
1941
+ options,
1942
+ resolved,
1943
+ selectionEvent
1944
+ );
1945
+ if (selected) {
1946
+ set(selected);
1947
+ }
1948
+ return resolved;
1949
+ });
1950
+ }
1951
+
1952
+ const selected = resolveSelectedValue(
1953
+ options,
1954
+ actionResult,
1955
+ selectionEvent
1956
+ );
1518
1957
  if (selected) {
1519
1958
  set(selected);
1520
1959
  }
1960
+
1521
1961
  return actionResult;
1522
1962
  },
1523
1963
  },
@@ -1529,8 +1969,11 @@ async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1529
1969
  const omnibox = document.createElement("pds-omnibox");
1530
1970
  omnibox.id = id;
1531
1971
  omnibox.setAttribute("name", path);
1532
- omnibox.setAttribute("item-grid", "0 1fr");
1533
- omnibox.setAttribute("placeholder", attrs?.placeholder || "Select a font family");
1972
+ omnibox.setAttribute("item-grid", "0 1fr 0");
1973
+ omnibox.setAttribute(
1974
+ "placeholder",
1975
+ attrs?.placeholder || "Select a font family"
1976
+ );
1534
1977
  omnibox.value = value ?? "";
1535
1978
  omnibox.settings = {
1536
1979
  ...fontFamilyOmniboxSettings,
@@ -1542,161 +1985,691 @@ async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1542
1985
  omnibox.addEventListener("change", (event) => {
1543
1986
  set(event?.target?.value ?? omnibox.value ?? "");
1544
1987
  });
1988
+ omnibox.addEventListener("result-selected", (event) => {
1989
+ const selected = resolveSelectedValue(event?.detail, undefined, event);
1990
+ if (!selected) return;
1991
+ omnibox.value = selected;
1992
+ set(selected);
1993
+ });
1545
1994
  return omnibox;
1546
1995
  }
1547
1996
  );
1997
+
1548
1998
  form.addEventListener("pw:submit", onSubmit);
1549
- const values = shallowClone(design || {});
1550
- Object.keys(ENUM_FIELD_OPTIONS).forEach((path) => {
1551
- const normalized = normalizeEnumValue(path, getValueAtPath(values, path.split(".")));
1552
- if (normalized !== undefined) {
1553
- setValueAtPath(values, path.split("."), normalized);
1554
- }
1555
- });
1556
- Object.entries(hints || {}).forEach(([path, hintValue]) => {
1557
- const segments = path.split(".");
1558
- const currentValue = getValueAtPath(values, segments);
1559
- if (currentValue === undefined || currentValue === null) {
1560
- setValueAtPath(values, segments, hintValue);
1561
- }
1562
- });
1563
- setFormSchemas(form, schema, uiSchema, values);
1999
+ if (typeof normalizeFlatValues === "function") {
2000
+ form._normalizeFlatValues = normalizeFlatValues;
2001
+ }
2002
+ setFormSchemas(form, schema, uiSchema, values || {});
1564
2003
 
1565
- // Apply button (will trigger form submit programmatically)
1566
2004
  const applyBtn = document.createElement("button");
1567
2005
  applyBtn.className = "btn-primary btn-sm";
1568
2006
  applyBtn.type = "button";
1569
2007
  applyBtn.textContent = "Apply";
1570
2008
  applyBtn.addEventListener("click", async () => {
1571
- // Manually trigger pw:submit event for pds-form
1572
2009
  if (typeof form.getValuesFlat === "function") {
1573
- // Wait for form to be ready if it's still loading
1574
2010
  if (!customElements.get("pds-form")) {
1575
2011
  await customElements.whenDefined("pds-form");
1576
2012
  }
1577
-
1578
- const flatValues = form.getValuesFlat();
2013
+
2014
+ const flatValues =
2015
+ typeof form._normalizeFlatValues === "function"
2016
+ ? form._normalizeFlatValues(form.getValuesFlat())
2017
+ : form.getValuesFlat();
1579
2018
  const event = new CustomEvent("pw:submit", {
1580
2019
  detail: {
1581
2020
  json: flatValues,
1582
2021
  formData: new FormData(),
1583
2022
  valid: true,
1584
- issues: []
2023
+ issues: [],
1585
2024
  },
1586
2025
  bubbles: true,
1587
- cancelable: true
2026
+ cancelable: true,
1588
2027
  });
1589
2028
  form.dispatchEvent(event);
1590
2029
  }
1591
2030
  });
1592
2031
 
1593
- // Undo button
1594
2032
  const undoBtn = document.createElement("button");
1595
- undoBtn.className = "btn-secondary btn-sm";
2033
+ undoBtn.className = "btn-secondary btn-sm icon-only";
1596
2034
  undoBtn.type = "button";
1597
- undoBtn.textContent = "Undo";
2035
+ undoBtn.setAttribute("aria-label", "Undo");
2036
+ undoBtn.setAttribute("title", "Undo");
2037
+ const undoIcon = document.createElement("pds-icon");
2038
+ undoIcon.setAttribute("icon", "arrow-counter-clockwise");
2039
+ undoIcon.setAttribute("size", "sm");
2040
+ undoBtn.appendChild(undoIcon);
1598
2041
  undoBtn.addEventListener("click", onUndo);
1599
2042
 
1600
2043
  return { form, applyBtn, undoBtn };
1601
2044
  }
1602
2045
 
1603
- class PdsLiveEdit extends HTMLElement {
1604
- constructor() {
1605
- super();
1606
- this._boundMouseOver = this._handleMouseOver.bind(this);
1607
- this._boundMouseOut = this._handleMouseOut.bind(this);
1608
- this._boundMouseMove = this._handleMouseMove.bind(this);
1609
- this._boundReposition = this._repositionDropdown.bind(this);
1610
- this._activeTarget = null;
1611
- this._activeDropdown = null;
1612
- this._holdOpen = false;
1613
- this._closeTimer = null;
1614
- this._drawer = null;
1615
- this._selectors = null;
1616
- this._lastPointer = null;
1617
- this._boundDocKeydown = this._handleDocumentKeydown.bind(this);
1618
- this._connected = false;
1619
- this._undoStack = [];
1620
- this._dropdownMenuOpen = false;
1621
- this._dropdownObserver = null;
2046
+ async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
2047
+ const quickPayload = buildQuickConfigPayload(paths, design, hints);
2048
+ const schema = quickPayload?.schema;
2049
+ const uiSchema = quickPayload?.uiSchema;
2050
+ const values = quickPayload?.values;
2051
+
2052
+ if (!schema || !uiSchema) {
2053
+ throw new Error("Central config form metadata is unavailable for quick edit");
1622
2054
  }
1623
2055
 
1624
- connectedCallback() {
1625
- if (this._connected) return;
1626
- if (PdsLiveEdit._activeInstance && PdsLiveEdit._activeInstance !== this) {
1627
- PdsLiveEdit._activeInstance._teardown();
1628
- }
1629
- PdsLiveEdit._activeInstance = this;
1630
- this._connected = true;
1631
- if (!isHoverCapable()) return;
2056
+ return createConfiguredForm({
2057
+ schema,
2058
+ uiSchema,
2059
+ values: values || {},
2060
+ onSubmit,
2061
+ onUndo,
2062
+ formOptions: {
2063
+ layouts: {
2064
+ arrays: "compact",
2065
+ },
2066
+ enhancements: {
2067
+ rangeOutput: true,
2068
+ },
2069
+ },
2070
+ });
2071
+ }
1632
2072
 
1633
- ensureStyles();
1634
- this._selectors = collectSelectors();
1635
- document.addEventListener("mouseover", this._boundMouseOver, true);
1636
- document.addEventListener("mouseout", this._boundMouseOut, true);
1637
- document.addEventListener("mousemove", this._boundMouseMove, true);
2073
+ function getConfigFormPayloadFromMetadata(design) {
2074
+ if (typeof PDS?.buildConfigFormSchema === "function") {
2075
+ return PDS.buildConfigFormSchema(design);
1638
2076
  }
1639
2077
 
1640
- disconnectedCallback() {
1641
- this._teardown();
2078
+ const payload = PDS?.configFormSchema;
2079
+ if (payload && payload.schema && payload.uiSchema) {
2080
+ return {
2081
+ schema: payload.schema,
2082
+ uiSchema: payload.uiSchema,
2083
+ values: shallowClone(design || payload.values || {}),
2084
+ metadata: payload.metadata || {},
2085
+ };
1642
2086
  }
1643
2087
 
1644
- _teardown() {
1645
- if (this._connected) {
1646
- document.removeEventListener("mouseover", this._boundMouseOver, true);
1647
- document.removeEventListener("mouseout", this._boundMouseOut, true);
1648
- document.removeEventListener("mousemove", this._boundMouseMove, true);
1649
- }
1650
- this._removeRepositionListeners();
1651
- this._clearCloseTimer();
1652
- this._removeActiveUI();
1653
- this._connected = false;
1654
- if (PdsLiveEdit._activeInstance === this) {
1655
- PdsLiveEdit._activeInstance = null;
1656
- }
1657
- }
2088
+ return null;
2089
+ }
1658
2090
 
1659
- _handleMouseOver(event) {
1660
- if (!event?.target || !(event.target instanceof Element)) return;
1661
-
1662
- // Check if we're hovering over the dropdown (including Shadow DOM elements)
1663
- if (this._activeDropdown) {
1664
- const path = event.composedPath ? event.composedPath() : [event.target];
1665
- const isOverDropdown = path.some(node => node === this._activeDropdown);
1666
- if (isOverDropdown) {
1667
- this._clearCloseTimer();
1668
- return;
1669
- }
1670
- }
1671
-
1672
- const target = this._findEditableTarget(event.target);
1673
-
1674
- // If hovering over the same active target, just clear timer
1675
- if (target && target === this._activeTarget) {
1676
- this._clearCloseTimer();
1677
- return;
1678
- }
1679
-
1680
- // If hovering over a new target, show its editor
1681
- if (target && target !== this._activeTarget) {
1682
- this._removeActiveUI();
1683
- this._showForTarget(target);
2091
+ function deepClone(value) {
2092
+ if (typeof structuredClone === "function") {
2093
+ try {
2094
+ return structuredClone(value);
2095
+ } catch (e) {
2096
+ // Fall through to JSON clone
1684
2097
  }
1685
2098
  }
2099
+ return JSON.parse(JSON.stringify(value));
2100
+ }
1686
2101
 
1687
- _handleMouseOut(event) {
1688
- if (!this._activeTarget) return;
1689
-
1690
- // Schedule a delayed close - the safe zone logic will determine if we actually close
1691
- this._scheduleClose();
1692
- }
2102
+ function withThemeContextValues(values) {
2103
+ const nextValues = shallowClone(values || {});
2104
+ nextValues[FORM_THEME_CONTEXT_FIELD] = getActiveTheme().value;
2105
+ return nextValues;
2106
+ }
1693
2107
 
1694
- _findEditableTarget(node) {
2108
+ function mergeVisibleWhenCondition(existingCondition, conditionToAdd) {
2109
+ if (!existingCondition) return deepClone(conditionToAdd);
2110
+ if (
2111
+ existingCondition &&
2112
+ typeof existingCondition === "object" &&
2113
+ Array.isArray(existingCondition.$and)
2114
+ ) {
2115
+ return {
2116
+ ...deepClone(existingCondition),
2117
+ $and: [...existingCondition.$and.map((entry) => deepClone(entry)), deepClone(conditionToAdd)],
2118
+ };
2119
+ }
2120
+ return {
2121
+ $and: [deepClone(existingCondition), deepClone(conditionToAdd)],
2122
+ };
2123
+ }
2124
+
2125
+ function collectSchemaNodeMap(schemaNode, pointer = "", out = new Map()) {
2126
+ if (!schemaNode || typeof schemaNode !== "object") return out;
2127
+ if (pointer) {
2128
+ out.set(pointer, schemaNode);
2129
+ }
2130
+ if (schemaNode.type === "object" && schemaNode.properties && typeof schemaNode.properties === "object") {
2131
+ Object.entries(schemaNode.properties).forEach(([key, childNode]) => {
2132
+ collectSchemaNodeMap(childNode, `${pointer}/${key}`, out);
2133
+ });
2134
+ }
2135
+ return out;
2136
+ }
2137
+
2138
+ function applyThemeVisibilityConditions(schema, uiSchema) {
2139
+ if (!schema || typeof schema !== "object") {
2140
+ return uiSchema && typeof uiSchema === "object" ? deepClone(uiSchema) : {};
2141
+ }
2142
+
2143
+ const conditionedUi = uiSchema && typeof uiSchema === "object" ? deepClone(uiSchema) : {};
2144
+ const schemaNodeMap = collectSchemaNodeMap(schema);
2145
+
2146
+ const darkCondition = { [FORM_THEME_CONTEXT_POINTER]: "dark" };
2147
+ const lightCondition = { [FORM_THEME_CONTEXT_POINTER]: { $ne: "dark" } };
2148
+
2149
+ const ensureCondition = (pointer, condition) => {
2150
+ if (!pointer || pointer === FORM_THEME_CONTEXT_POINTER) return;
2151
+ const current = conditionedUi[pointer] && typeof conditionedUi[pointer] === "object"
2152
+ ? conditionedUi[pointer]
2153
+ : {};
2154
+ conditionedUi[pointer] = {
2155
+ ...current,
2156
+ "ui:visibleWhen": mergeVisibleWhenCondition(current["ui:visibleWhen"], condition),
2157
+ };
2158
+ };
2159
+
2160
+ schemaNodeMap.forEach((node, pointer) => {
2161
+ if (!pointer.includes("/darkMode") || pointer === FORM_THEME_CONTEXT_POINTER) return;
2162
+
2163
+ ensureCondition(pointer, darkCondition);
2164
+
2165
+ const isDarkLeaf = !(node?.type === "object" && node?.properties);
2166
+ if (!isDarkLeaf) return;
2167
+
2168
+ const lightPointer = pointer.replace("/darkMode/", "/");
2169
+ const lightNode = schemaNodeMap.get(lightPointer);
2170
+ const isLightLeaf = !!lightNode && !(lightNode?.type === "object" && lightNode?.properties);
2171
+ if (isLightLeaf) {
2172
+ ensureCondition(lightPointer, lightCondition);
2173
+ }
2174
+ });
2175
+
2176
+ conditionedUi[FORM_THEME_CONTEXT_POINTER] = {
2177
+ ...(conditionedUi[FORM_THEME_CONTEXT_POINTER] || {}),
2178
+ "ui:hidden": true,
2179
+ };
2180
+
2181
+ return conditionedUi;
2182
+ }
2183
+
2184
+ function withThemeConditionalSchema(schema, uiSchema) {
2185
+ const baseSchema = deepClone(schema || { type: "object", properties: {} });
2186
+ if (!baseSchema.properties || typeof baseSchema.properties !== "object") {
2187
+ baseSchema.properties = {};
2188
+ }
2189
+
2190
+ baseSchema.properties[FORM_THEME_CONTEXT_FIELD] = {
2191
+ type: "string",
2192
+ oneOf: [
2193
+ { const: "light", title: "Light" },
2194
+ { const: "dark", title: "Dark" },
2195
+ ],
2196
+ };
2197
+
2198
+ const conditionedUi = applyThemeVisibilityConditions(baseSchema, uiSchema);
2199
+ return { schema: baseSchema, uiSchema: conditionedUi };
2200
+ }
2201
+
2202
+ function shouldKeepPathForSelection(selectedPaths, path) {
2203
+ if (!path) return true;
2204
+ return selectedPaths.some((selectedPath) => {
2205
+ if (selectedPath === path) return true;
2206
+ return selectedPath.startsWith(`${path}.`);
2207
+ });
2208
+ }
2209
+
2210
+ function pruneSchemaForPaths(node, selectedPaths, path = "") {
2211
+ if (!node || typeof node !== "object") return node;
2212
+ if (!isObjectSchemaNode(node)) return deepClone(node);
2213
+
2214
+ if (path && !shouldKeepPathForSelection(selectedPaths, path)) {
2215
+ return null;
2216
+ }
2217
+
2218
+ const properties = {};
2219
+ Object.entries(node.properties || {}).forEach(([key, childNode]) => {
2220
+ const childPath = path ? `${path}.${key}` : key;
2221
+ if (!shouldKeepPathForSelection(selectedPaths, childPath)) return;
2222
+ const prunedChild = pruneSchemaForPaths(childNode, selectedPaths, childPath);
2223
+ if (prunedChild) {
2224
+ properties[key] = prunedChild;
2225
+ }
2226
+ });
2227
+
2228
+ if (!Object.keys(properties).length) return null;
2229
+
2230
+ const clonedNode = deepClone(node);
2231
+ clonedNode.properties = properties;
2232
+ if (Array.isArray(clonedNode.required)) {
2233
+ clonedNode.required = clonedNode.required.filter((key) =>
2234
+ Object.prototype.hasOwnProperty.call(properties, key)
2235
+ );
2236
+ }
2237
+ return clonedNode;
2238
+ }
2239
+
2240
+ function uiPointerToPath(pointer) {
2241
+ if (!pointer || pointer === "/") return "";
2242
+ return pointer
2243
+ .replace(/^\//, "")
2244
+ .split("/")
2245
+ .filter(Boolean)
2246
+ .join(".");
2247
+ }
2248
+
2249
+ function filterUiSchemaForPaths(uiSchema, selectedPaths) {
2250
+ if (!uiSchema || typeof uiSchema !== "object") return {};
2251
+ const filtered = {};
2252
+ Object.entries(uiSchema).forEach(([pointer, value]) => {
2253
+ const path = uiPointerToPath(pointer);
2254
+ if (!path || shouldKeepPathForSelection(selectedPaths, path)) {
2255
+ filtered[pointer] = deepClone(value);
2256
+ }
2257
+ });
2258
+ return filtered;
2259
+ }
2260
+
2261
+ function buildValuesForPaths(valuesSource, selectedPaths, hints = {}) {
2262
+ const values = {};
2263
+ selectedPaths.forEach((path) => {
2264
+ const segments = path.split(".");
2265
+ let value = getValueAtPath(valuesSource, segments);
2266
+ if ((value === undefined || value === null) && hints[path] !== undefined) {
2267
+ value = hints[path];
2268
+ }
2269
+ if (isColorPath(path)) {
2270
+ const resolvedColorValue = resolveColorValueForPath(path, value, hints[path]);
2271
+ if (resolvedColorValue) {
2272
+ value = resolvedColorValue;
2273
+ }
2274
+ }
2275
+ if (value !== undefined) {
2276
+ setValueAtPath(values, segments, deepClone(value));
2277
+ }
2278
+ });
2279
+ return values;
2280
+ }
2281
+
2282
+ function buildQuickConfigPayload(paths, design, hints = {}) {
2283
+ const payload = getConfigFormPayloadFromMetadata(design);
2284
+ if (!payload?.schema || !payload?.uiSchema) return null;
2285
+
2286
+ const selectedPaths = normalizePaths(paths);
2287
+ if (!selectedPaths.length) return null;
2288
+
2289
+ const schema = pruneSchemaForPaths(payload.schema, selectedPaths, "");
2290
+ if (!schema) return null;
2291
+
2292
+ const uiSchema = filterUiSchemaForPaths(payload.uiSchema, selectedPaths);
2293
+ const valuesSource =
2294
+ payload?.values && typeof payload.values === "object"
2295
+ ? payload.values
2296
+ : shallowClone(design || {});
2297
+ const values = buildValuesForPaths(valuesSource || {}, selectedPaths, hints);
2298
+
2299
+ return { schema, uiSchema, values };
2300
+ }
2301
+
2302
+ const FULL_CONFIG_GROUPS_KEY = "__groups";
2303
+
2304
+ function isObjectSchemaNode(node) {
2305
+ return !!(node && typeof node === "object" && node.type === "object" && node.properties);
2306
+ }
2307
+
2308
+ function buildGroupedFullConfigPayload(payload, design) {
2309
+ const values =
2310
+ payload?.values && typeof payload.values === "object"
2311
+ ? payload.values
2312
+ : shallowClone(design || {});
2313
+
2314
+ if (!payload?.schema || !payload?.uiSchema || !isObjectSchemaNode(payload.schema)) {
2315
+ return {
2316
+ schema: payload?.schema,
2317
+ uiSchema: payload?.uiSchema,
2318
+ values,
2319
+ normalizeFlatValues: null,
2320
+ };
2321
+ }
2322
+
2323
+ const rootProperties = payload.schema.properties || {};
2324
+ const groupedKeys = [];
2325
+ const scalarKeys = [];
2326
+
2327
+ Object.entries(rootProperties).forEach(([key, schemaNode]) => {
2328
+ if (isObjectSchemaNode(schemaNode)) {
2329
+ groupedKeys.push(key);
2330
+ return;
2331
+ }
2332
+ scalarKeys.push(key);
2333
+ });
2334
+
2335
+ if (!groupedKeys.length || !scalarKeys.length) {
2336
+ return {
2337
+ schema: payload.schema,
2338
+ uiSchema: payload.uiSchema,
2339
+ values,
2340
+ normalizeFlatValues: null,
2341
+ };
2342
+ }
2343
+
2344
+ const transformedSchema = {
2345
+ ...payload.schema,
2346
+ properties: {
2347
+ ...Object.fromEntries(scalarKeys.map((key) => [key, rootProperties[key]])),
2348
+ [FULL_CONFIG_GROUPS_KEY]: {
2349
+ type: "object",
2350
+ title: "Design Groups",
2351
+ properties: Object.fromEntries(
2352
+ groupedKeys.map((key) => [key, rootProperties[key]])
2353
+ ),
2354
+ },
2355
+ },
2356
+ };
2357
+
2358
+ const transformedValues = {
2359
+ ...Object.fromEntries(scalarKeys.map((key) => [key, values?.[key]])),
2360
+ [FULL_CONFIG_GROUPS_KEY]: Object.fromEntries(
2361
+ groupedKeys.map((key) => [key, values?.[key]])
2362
+ ),
2363
+ };
2364
+
2365
+ const transformedUiSchema = { ...(payload.uiSchema || {}) };
2366
+ const addGroupPrefix = (path = "") => `/${FULL_CONFIG_GROUPS_KEY}${path}`;
2367
+
2368
+ groupedKeys.forEach((key) => {
2369
+ const originalPath = `/${key}`;
2370
+
2371
+ if (Object.prototype.hasOwnProperty.call(transformedUiSchema, originalPath)) {
2372
+ transformedUiSchema[addGroupPrefix(originalPath)] = transformedUiSchema[originalPath];
2373
+ delete transformedUiSchema[originalPath];
2374
+ }
2375
+
2376
+ Object.keys(transformedUiSchema).forEach((path) => {
2377
+ if (!path.startsWith(`${originalPath}/`)) return;
2378
+ transformedUiSchema[addGroupPrefix(path)] = transformedUiSchema[path];
2379
+ delete transformedUiSchema[path];
2380
+ });
2381
+ });
2382
+
2383
+ transformedUiSchema[`/${FULL_CONFIG_GROUPS_KEY}`] = {
2384
+ "ui:layout": "accordion",
2385
+ "ui:layoutOptions": { openFirst: false },
2386
+ };
2387
+
2388
+ const normalizeFlatValues = (flatValues = {}) => {
2389
+ const normalized = {};
2390
+ const groupPointerPrefix = `/${FULL_CONFIG_GROUPS_KEY}/`;
2391
+ const groupDotPrefix = `${FULL_CONFIG_GROUPS_KEY}.`;
2392
+ Object.entries(flatValues || {}).forEach(([path, value]) => {
2393
+ const inputPath = String(path || "");
2394
+ if (!inputPath) return;
2395
+ if (inputPath === FULL_CONFIG_GROUPS_KEY || inputPath === `/${FULL_CONFIG_GROUPS_KEY}`) {
2396
+ return;
2397
+ }
2398
+
2399
+ if (inputPath.startsWith(groupPointerPrefix)) {
2400
+ normalized[`/${inputPath.slice(groupPointerPrefix.length)}`] = value;
2401
+ return;
2402
+ }
2403
+
2404
+ if (inputPath.startsWith(groupDotPrefix)) {
2405
+ normalized[inputPath.slice(groupDotPrefix.length)] = value;
2406
+ return;
2407
+ }
2408
+
2409
+ normalized[inputPath] = value;
2410
+ });
2411
+ return normalized;
2412
+ };
2413
+
2414
+ return {
2415
+ schema: transformedSchema,
2416
+ uiSchema: transformedUiSchema,
2417
+ values: transformedValues,
2418
+ normalizeFlatValues,
2419
+ };
2420
+ }
2421
+
2422
+ async function buildFullConfigForm(design, onSubmit, onUndo) {
2423
+ const payload = getConfigFormPayloadFromMetadata(design);
2424
+ if (!payload?.schema || !payload?.uiSchema) return null;
2425
+
2426
+ const groupedPayload = buildGroupedFullConfigPayload(payload, design);
2427
+
2428
+ return createConfiguredForm({
2429
+ schema: groupedPayload.schema,
2430
+ uiSchema: groupedPayload.uiSchema,
2431
+ values: groupedPayload.values,
2432
+ onSubmit,
2433
+ onUndo,
2434
+ normalizeFlatValues: groupedPayload.normalizeFlatValues,
2435
+ formOptions: {
2436
+ layouts: {
2437
+ arrays: "compact",
2438
+ },
2439
+ enhancements: {
2440
+ rangeOutput: true,
2441
+ },
2442
+ },
2443
+ });
2444
+ }
2445
+
2446
+ class PdsLiveEdit extends HTMLElement {
2447
+ constructor() {
2448
+ super();
2449
+ this._boundMouseOver = this._handleMouseOver.bind(this);
2450
+ this._boundMouseOut = this._handleMouseOut.bind(this);
2451
+ this._boundMouseMove = this._handleMouseMove.bind(this);
2452
+ this._boundReposition = this._repositionDropdown.bind(this);
2453
+ this._activeTarget = null;
2454
+ this._activeDropdown = null;
2455
+ this._holdOpen = false;
2456
+ this._closeTimer = null;
2457
+ this._drawer = null;
2458
+ this._selectors = null;
2459
+ this._lastPointer = null;
2460
+ this._boundDocKeydown = this._handleDocumentKeydown.bind(this);
2461
+ this._connected = false;
2462
+ this._undoStack = [];
2463
+ this._dropdownMenuOpen = false;
2464
+ this._dropdownObserver = null;
2465
+ this._boundThemeChanged = this._handleThemeChanged.bind(this);
2466
+ this._themeRefreshInFlight = null;
2467
+ this._drawerConfigFormContainer = null;
2468
+ this._drawerConfigFooter = null;
2469
+ this._interactiveEditingEnabled = true;
2470
+ this._interactionListenersAttached = false;
2471
+ }
2472
+
2473
+ connectedCallback() {
2474
+ if (this._connected) return;
2475
+ if (PdsLiveEdit._activeInstance && PdsLiveEdit._activeInstance !== this) {
2476
+ PdsLiveEdit._activeInstance._teardown();
2477
+ }
2478
+ PdsLiveEdit._activeInstance = this;
2479
+ this._connected = true;
2480
+ if (this.hasAttribute(SETTINGS_ONLY_ATTR)) {
2481
+ this._interactiveEditingEnabled = false;
2482
+ }
2483
+ if (PDS && typeof PDS.addEventListener === "function") {
2484
+ PDS.addEventListener("pds:theme:changed", this._boundThemeChanged);
2485
+ }
2486
+ if (this._interactiveEditingEnabled) {
2487
+ this._enableInteractiveEditing();
2488
+ }
2489
+ }
2490
+
2491
+ disconnectedCallback() {
2492
+ this._teardown();
2493
+ }
2494
+
2495
+ _teardown() {
2496
+ this._disableInteractiveEditing({ clearUI: true });
2497
+ this._connected = false;
2498
+ if (PDS && typeof PDS.removeEventListener === "function") {
2499
+ PDS.removeEventListener("pds:theme:changed", this._boundThemeChanged);
2500
+ }
2501
+ this._removeRepositionListeners();
2502
+ this._clearCloseTimer();
2503
+ this._removeActiveUI();
2504
+ this._drawerConfigFormContainer = null;
2505
+ this._drawerConfigFooter = null;
2506
+ if (PdsLiveEdit._activeInstance === this) {
2507
+ PdsLiveEdit._activeInstance = null;
2508
+ }
2509
+ }
2510
+
2511
+ _enableInteractiveEditing() {
2512
+ if (!isHoverCapable()) return;
2513
+ ensureStyles();
2514
+ this._selectors = collectSelectors();
2515
+ if (this._interactionListenersAttached) return;
2516
+ document.addEventListener("mouseover", this._boundMouseOver, true);
2517
+ document.addEventListener("mouseout", this._boundMouseOut, true);
2518
+ document.addEventListener("mousemove", this._boundMouseMove, true);
2519
+ this._interactionListenersAttached = true;
2520
+ }
2521
+
2522
+ _disableInteractiveEditing(options = {}) {
2523
+ if (this._interactionListenersAttached) {
2524
+ document.removeEventListener("mouseover", this._boundMouseOver, true);
2525
+ document.removeEventListener("mouseout", this._boundMouseOut, true);
2526
+ document.removeEventListener("mousemove", this._boundMouseMove, true);
2527
+ this._interactionListenersAttached = false;
2528
+ }
2529
+
2530
+ if (options?.clearUI !== false) {
2531
+ this._removeActiveUI();
2532
+ }
2533
+ }
2534
+
2535
+ setInteractiveEditingEnabled(enabled = true) {
2536
+ const next = Boolean(enabled);
2537
+ this._interactiveEditingEnabled = next;
2538
+
2539
+ if (next) {
2540
+ this.removeAttribute(SETTINGS_ONLY_ATTR);
2541
+ } else {
2542
+ this.setAttribute(SETTINGS_ONLY_ATTR, "true");
2543
+ }
2544
+
2545
+ if (!this._connected) {
2546
+ return next;
2547
+ }
2548
+
2549
+ if (next) {
2550
+ this._enableInteractiveEditing();
2551
+ } else {
2552
+ this._disableInteractiveEditing({ clearUI: true });
2553
+ }
2554
+
2555
+ return next;
2556
+ }
2557
+
2558
+ isInteractiveEditingEnabled() {
2559
+ return Boolean(this._interactiveEditingEnabled);
2560
+ }
2561
+
2562
+ async _handleThemeChanged() {
2563
+ if (this._themeRefreshInFlight) return this._themeRefreshInFlight;
2564
+ this._themeRefreshInFlight = (async () => {
2565
+ try {
2566
+ await this._refreshQuickFormForTheme();
2567
+ await this._refreshDrawerConfigFormForTheme();
2568
+ } finally {
2569
+ this._themeRefreshInFlight = null;
2570
+ }
2571
+ })();
2572
+ return this._themeRefreshInFlight;
2573
+ }
2574
+
2575
+ async _refreshQuickFormForTheme() {
2576
+ if (!this._activeDropdown || !this._activeTarget) return;
2577
+ if (!document.contains(this._activeTarget)) return;
2578
+
2579
+ const formContainer = this._activeDropdown.querySelector('.pds-live-editor-form-container');
2580
+ const footer = this._activeDropdown.querySelector('.pds-live-editor-footer');
2581
+ if (!formContainer || !footer) return;
2582
+
2583
+ const currentDesign = shallowClone(PDS?.currentConfig?.design || {});
2584
+ const quickContext = collectQuickContext(this._activeTarget);
2585
+ const limitedPaths = quickContext.paths.slice(0, QUICK_EDIT_LIMIT);
2586
+ const quickPaths = quickContext.paths;
2587
+
2588
+ await this._renderQuickForm(
2589
+ formContainer,
2590
+ footer,
2591
+ limitedPaths,
2592
+ currentDesign,
2593
+ quickContext.hints,
2594
+ this._activeTarget,
2595
+ quickPaths
2596
+ );
2597
+ }
2598
+
2599
+ async _refreshDrawerConfigFormForTheme() {
2600
+ if (!this._drawer) return;
2601
+ if (!this._drawer.hasAttribute("open")) return;
2602
+ if (!this._drawerConfigFormContainer || !this._drawerConfigFooter) return;
2603
+
2604
+ const fullDesign = shallowClone(PDS?.currentConfig?.design || {});
2605
+ const fullConfigFormResult = await buildFullConfigForm(
2606
+ fullDesign,
2607
+ (event) => this._handleFormSubmit(event, fullConfigFormResult?.form),
2608
+ () => this._handleUndo()
2609
+ );
2610
+
2611
+ this._drawerConfigFormContainer.replaceChildren();
2612
+ this._drawerConfigFooter.replaceChildren();
2613
+
2614
+ if (fullConfigFormResult?.form) {
2615
+ fullConfigFormResult.form._undoBtn = fullConfigFormResult.undoBtn;
2616
+ fullConfigFormResult.undoBtn.disabled = this._undoStack.length === 0;
2617
+ this._drawerConfigFormContainer.appendChild(fullConfigFormResult.form);
2618
+ this._drawerConfigFooter.appendChild(fullConfigFormResult.applyBtn);
2619
+ this._drawerConfigFooter.appendChild(fullConfigFormResult.undoBtn);
2620
+ } else {
2621
+ const unavailable = document.createElement("p");
2622
+ unavailable.className = "text-muted";
2623
+ unavailable.textContent =
2624
+ "Full config metadata is unavailable in this runtime.";
2625
+ this._drawerConfigFormContainer.appendChild(unavailable);
2626
+ }
2627
+ }
2628
+
2629
+ _handleMouseOver(event) {
2630
+ if (!this._interactiveEditingEnabled) return;
2631
+ if (!event?.target || !(event.target instanceof Element)) return;
2632
+
2633
+ // Check if we're hovering over the dropdown (including Shadow DOM elements)
2634
+ if (this._activeDropdown) {
2635
+ const path = event.composedPath ? event.composedPath() : [event.target];
2636
+ const isOverDropdown = path.some(node => node === this._activeDropdown);
2637
+ if (isOverDropdown) {
2638
+ this._clearCloseTimer();
2639
+ return;
2640
+ }
2641
+ }
2642
+
2643
+ const target = this._findEditableTarget(event.target);
2644
+
2645
+ // If hovering over the same active target, just clear timer
2646
+ if (target && target === this._activeTarget) {
2647
+ this._clearCloseTimer();
2648
+ return;
2649
+ }
2650
+
2651
+ // If hovering over a new target, show its editor
2652
+ if (target && target !== this._activeTarget) {
2653
+ this._removeActiveUI();
2654
+ this._showForTarget(target);
2655
+ }
2656
+ }
2657
+
2658
+ _handleMouseOut(event) {
2659
+ if (!this._activeTarget) return;
2660
+
2661
+ // Schedule a delayed close - the safe zone logic will determine if we actually close
2662
+ this._scheduleClose();
2663
+ }
2664
+
2665
+ _findEditableTarget(node) {
1695
2666
  const tag = node.tagName?.toLowerCase?.();
1696
2667
  if (tag && ["html", "head", "meta", "link", "style", "script", "title"].includes(tag)) {
1697
2668
  return null;
1698
2669
  }
1699
2670
  if (!this._selectors?.selector) return null;
2671
+ if (isLiveEditHighlightBlacklisted(node)) return null;
2672
+ if (node.closest("[data-pds-live-edit-ignore]")) return null;
1700
2673
  if (node.closest(EDITOR_TAG)) return null;
1701
2674
  if (node.closest(`.${DROPDOWN_CLASS}`)) return null;
1702
2675
  if (node.closest("pds-drawer")) return null;
@@ -1709,6 +2682,7 @@ class PdsLiveEdit extends HTMLElement {
1709
2682
  }
1710
2683
 
1711
2684
  _showForTarget(target) {
2685
+ if (!this._interactiveEditingEnabled) return;
1712
2686
  const quickContext = collectQuickContext(target);
1713
2687
  const quickPaths = quickContext.paths;
1714
2688
  if (!quickPaths.length) return;
@@ -1753,8 +2727,13 @@ class PdsLiveEdit extends HTMLElement {
1753
2727
  this._lastPointer = null;
1754
2728
  this._dropdownMenuOpen = false;
1755
2729
 
1756
- // Always re-enable mouseover when UI is removed
1757
- this._addMouseOverListener();
2730
+ if (
2731
+ this._connected &&
2732
+ this._interactiveEditingEnabled &&
2733
+ this._interactionListenersAttached
2734
+ ) {
2735
+ this._addMouseOverListener();
2736
+ }
1758
2737
  }
1759
2738
 
1760
2739
  _addDocumentListeners() {
@@ -1901,6 +2880,7 @@ class PdsLiveEdit extends HTMLElement {
1901
2880
 
1902
2881
  _addMouseOverListener() {
1903
2882
  if (typeof document === "undefined") return;
2883
+ if (!this._interactiveEditingEnabled) return;
1904
2884
  document.addEventListener("mouseover", this._boundMouseOver, true);
1905
2885
  }
1906
2886
 
@@ -2017,7 +2997,7 @@ class PdsLiveEdit extends HTMLElement {
2017
2997
  button.appendChild(icon);
2018
2998
 
2019
2999
  const menu = document.createElement("menu");
2020
- const quickItem = document.createElement("li");
3000
+ const quickItem = document.createElement("div");
2021
3001
  quickItem.className = "pds-live-editor-menu";
2022
3002
 
2023
3003
  const header = document.createElement("div");
@@ -2063,13 +3043,28 @@ class PdsLiveEdit extends HTMLElement {
2063
3043
  container.replaceChildren();
2064
3044
  footer.replaceChildren();
2065
3045
 
2066
- const { form, applyBtn, undoBtn } = await buildForm(
2067
- paths,
2068
- design,
2069
- (event) => this._handleFormSubmit(event, form),
2070
- () => this._handleUndo(),
2071
- hints
2072
- );
3046
+ let form;
3047
+ let applyBtn;
3048
+ let undoBtn;
3049
+ try {
3050
+ const result = await buildForm(
3051
+ paths,
3052
+ design,
3053
+ (event) => this._handleFormSubmit(event, form),
3054
+ () => this._handleUndo(),
3055
+ hints
3056
+ );
3057
+ form = result.form;
3058
+ applyBtn = result.applyBtn;
3059
+ undoBtn = result.undoBtn;
3060
+ } catch (error) {
3061
+ const fallback = document.createElement("p");
3062
+ fallback.className = "text-muted";
3063
+ fallback.textContent = "Editor form unavailable. Lazy component definition did not complete in time.";
3064
+ container.appendChild(fallback);
3065
+ console.warn("[PDS Live Edit] Failed to render quick form:", error);
3066
+ return;
3067
+ }
2073
3068
 
2074
3069
  // Store reference to undo button for enabling/disabling
2075
3070
  form._undoBtn = undoBtn;
@@ -2095,13 +3090,142 @@ class PdsLiveEdit extends HTMLElement {
2095
3090
  this._openDrawer(target, quickPaths);
2096
3091
  this._removeActiveUI();
2097
3092
  });
3093
+
3094
+ const quickModeNav = await this._buildQuickModeDropdown();
2098
3095
 
2099
3096
  // Add buttons to footer
2100
3097
  footer.appendChild(applyBtn);
2101
3098
  footer.appendChild(undoBtn);
3099
+ footer.appendChild(quickModeNav);
2102
3100
  footer.appendChild(gearBtn);
2103
3101
  }
2104
3102
 
3103
+ async _buildQuickModeDropdown() {
3104
+ const nav = document.createElement("nav");
3105
+ nav.setAttribute("data-dropdown", "");
3106
+ nav.setAttribute("data-mode", "auto");
3107
+ nav.setAttribute("data-direction", "auto");
3108
+
3109
+ const button = document.createElement("button");
3110
+ button.type = "button";
3111
+ button.className = "btn-outline btn-sm icon-only";
3112
+ button.setAttribute("aria-label", "Quick theme and preset");
3113
+
3114
+ const icon = document.createElement("pds-icon");
3115
+ icon.setAttribute("icon", "palette");
3116
+ icon.setAttribute("size", "sm");
3117
+ button.appendChild(icon);
3118
+
3119
+ const menu = document.createElement("menu");
3120
+ const content = await this._buildQuickModeContent();
3121
+ menu.appendChild(content);
3122
+ nav.append(button, menu);
3123
+ return nav;
3124
+ }
3125
+
3126
+ async _buildQuickModeContent() {
3127
+ const content = document.createElement("div");
3128
+ content.className = "pds-live-editor-menu stack-sm";
3129
+
3130
+ const themeLabel = document.createElement("label");
3131
+ themeLabel.className = "stack-xs";
3132
+ const themeText = document.createElement("span");
3133
+ themeText.textContent = "Theme";
3134
+ const themeToggle = document.createElement("pds-theme");
3135
+ themeLabel.append(themeText, themeToggle);
3136
+ content.appendChild(themeLabel);
3137
+
3138
+ const presetLabel = document.createElement("label");
3139
+ presetLabel.className = "stack-xs";
3140
+ const presetText = document.createElement("span");
3141
+ presetText.textContent = "Preset";
3142
+ presetLabel.appendChild(presetText);
3143
+
3144
+ let presetControlRendered = false;
3145
+ try {
3146
+ await waitForElementDefinition("pds-omnibox");
3147
+
3148
+ const presetOmnibox = document.createElement("pds-omnibox");
3149
+ presetOmnibox.setAttribute("item-grid", "0 1fr 0");
3150
+ presetOmnibox.setAttribute("placeholder", "Search presets...");
3151
+
3152
+ const activePresetId = getActivePresetId();
3153
+ const activePresetName = getPresetNameById(activePresetId);
3154
+ if (activePresetName) {
3155
+ presetOmnibox.value = activePresetName;
3156
+ }
3157
+
3158
+ const omniboxSettingsBuilder =
3159
+ typeof PDS?.buildPresetOmniboxSettings === "function"
3160
+ ? PDS.buildPresetOmniboxSettings.bind(PDS)
3161
+ : null;
3162
+
3163
+ if (omniboxSettingsBuilder) {
3164
+ presetOmnibox.settings = omniboxSettingsBuilder({
3165
+ onSelect: async ({ preset, selection }) => {
3166
+ if (selection?.disabled) return selection?.id;
3167
+ const presetId = preset?.id || selection?.id;
3168
+ await applyPresetSelection(presetId);
3169
+ return presetId;
3170
+ },
3171
+ });
3172
+ }
3173
+
3174
+ presetOmnibox.addEventListener("result-selected", (event) => {
3175
+ const selectedText = event?.detail?.text;
3176
+ if (typeof selectedText === "string" && selectedText.trim()) {
3177
+ presetOmnibox.value = selectedText;
3178
+ }
3179
+ });
3180
+
3181
+ presetLabel.appendChild(presetOmnibox);
3182
+ presetControlRendered = true;
3183
+ } catch (error) {
3184
+ console.warn("[PDS Live Edit] Quick preset omnibox unavailable, falling back to select.", error);
3185
+ }
3186
+
3187
+ if (!presetControlRendered) {
3188
+ const presetSelect = document.createElement("select");
3189
+ const presetOptions = getPresetOptions();
3190
+ const activePreset = getActivePresetId();
3191
+
3192
+ presetOptions.forEach((preset) => {
3193
+ const option = document.createElement("option");
3194
+ option.value = preset.id;
3195
+ option.textContent = preset.name;
3196
+ if (String(preset.id) === String(activePreset)) {
3197
+ option.selected = true;
3198
+ }
3199
+ presetSelect.appendChild(option);
3200
+ });
3201
+
3202
+ presetSelect.addEventListener("change", async (event) => {
3203
+ const nextPreset = event.target?.value;
3204
+ await applyPresetSelection(nextPreset);
3205
+ });
3206
+
3207
+ presetLabel.appendChild(presetSelect);
3208
+ }
3209
+
3210
+ content.appendChild(presetLabel);
3211
+ return content;
3212
+ }
3213
+
3214
+ async createSharedQuickModeMenuItem() {
3215
+ const item = document.createElement("li");
3216
+ item.className = "pds-live-shared-quick-mode-item";
3217
+ item.setAttribute("data-pds-live-edit-ignore", "true");
3218
+
3219
+ const quickModeContent = await this._buildQuickModeContent();
3220
+ if (!quickModeContent) {
3221
+ return item;
3222
+ }
3223
+
3224
+ markNodeTreeAsLiveEditIgnored(quickModeContent);
3225
+ item.appendChild(quickModeContent);
3226
+ return item;
3227
+ }
3228
+
2105
3229
  async _openDrawer(target, quickPaths) {
2106
3230
  if (!this._drawer) {
2107
3231
  this._drawer = document.createElement("pds-drawer");
@@ -2110,6 +3234,8 @@ class PdsLiveEdit extends HTMLElement {
2110
3234
  this.appendChild(this._drawer);
2111
3235
  }
2112
3236
 
3237
+ this._drawer.style.setProperty("--drawer-width", "min(96vw, 44rem)");
3238
+
2113
3239
  if (!customElements.get("pds-drawer")) {
2114
3240
  await customElements.whenDefined("pds-drawer");
2115
3241
  }
@@ -2121,7 +3247,7 @@ class PdsLiveEdit extends HTMLElement {
2121
3247
 
2122
3248
  const content = document.createElement("div");
2123
3249
  content.setAttribute("slot", "drawer-content");
2124
- content.className = "stack-md";
3250
+ content.className = "pds-live-editor-drawer-content";
2125
3251
 
2126
3252
  const presetCard = document.createElement("section");
2127
3253
  presetCard.className = "card surface-elevated stack-sm";
@@ -2137,26 +3263,72 @@ class PdsLiveEdit extends HTMLElement {
2137
3263
  presetText.textContent = "Choose a base style";
2138
3264
  presetLabel.appendChild(presetText);
2139
3265
 
2140
- const presetSelect = document.createElement("select");
2141
- const presetOptions = getPresetOptions();
2142
- const activePreset = getActivePresetId();
3266
+ let presetControlRendered = false;
3267
+ try {
3268
+ await waitForElementDefinition("pds-omnibox");
3269
+
3270
+ const presetOmnibox = document.createElement("pds-omnibox");
3271
+ presetOmnibox.setAttribute("item-grid", "0 1fr 0");
3272
+ presetOmnibox.setAttribute("placeholder", "Search presets...");
2143
3273
 
2144
- presetOptions.forEach((preset) => {
2145
- const option = document.createElement("option");
2146
- option.value = preset.id;
2147
- option.textContent = preset.name;
2148
- if (String(preset.id) === String(activePreset)) {
2149
- option.selected = true;
3274
+ const activePresetId = getActivePresetId();
3275
+ const activePresetName = getPresetNameById(activePresetId);
3276
+ if (activePresetName) {
3277
+ presetOmnibox.value = activePresetName;
2150
3278
  }
2151
- presetSelect.appendChild(option);
2152
- });
2153
3279
 
2154
- presetSelect.addEventListener("change", async (event) => {
2155
- const nextPreset = event.target?.value;
2156
- await applyPresetSelection(nextPreset);
2157
- });
3280
+ const omniboxSettingsBuilder =
3281
+ typeof PDS?.buildPresetOmniboxSettings === "function"
3282
+ ? PDS.buildPresetOmniboxSettings.bind(PDS)
3283
+ : null;
3284
+
3285
+ if (omniboxSettingsBuilder) {
3286
+ presetOmnibox.settings = omniboxSettingsBuilder({
3287
+ onSelect: async ({ preset, selection }) => {
3288
+ if (selection?.disabled) return selection?.id;
3289
+ const presetId = preset?.id || selection?.id;
3290
+ await applyPresetSelection(presetId);
3291
+ return presetId;
3292
+ },
3293
+ });
3294
+ }
3295
+
3296
+ presetOmnibox.addEventListener("result-selected", (event) => {
3297
+ const selectedText = event?.detail?.text;
3298
+ if (typeof selectedText === "string" && selectedText.trim()) {
3299
+ presetOmnibox.value = selectedText;
3300
+ }
3301
+ });
3302
+
3303
+ presetLabel.appendChild(presetOmnibox);
3304
+ presetControlRendered = true;
3305
+ } catch (error) {
3306
+ console.warn("[PDS Live Edit] Preset omnibox unavailable, falling back to select.", error);
3307
+ }
3308
+
3309
+ if (!presetControlRendered) {
3310
+ const presetSelect = document.createElement("select");
3311
+ const presetOptions = getPresetOptions();
3312
+ const activePreset = getActivePresetId();
3313
+
3314
+ presetOptions.forEach((preset) => {
3315
+ const option = document.createElement("option");
3316
+ option.value = preset.id;
3317
+ option.textContent = preset.name;
3318
+ if (String(preset.id) === String(activePreset)) {
3319
+ option.selected = true;
3320
+ }
3321
+ presetSelect.appendChild(option);
3322
+ });
3323
+
3324
+ presetSelect.addEventListener("change", async (event) => {
3325
+ const nextPreset = event.target?.value;
3326
+ await applyPresetSelection(nextPreset);
3327
+ });
3328
+
3329
+ presetLabel.appendChild(presetSelect);
3330
+ }
2158
3331
 
2159
- presetLabel.appendChild(presetSelect);
2160
3332
  presetCard.appendChild(presetLabel);
2161
3333
 
2162
3334
  const themeCard = document.createElement("section");
@@ -2169,6 +3341,84 @@ class PdsLiveEdit extends HTMLElement {
2169
3341
  const themeToggle = document.createElement("pds-theme");
2170
3342
  themeCard.appendChild(themeToggle);
2171
3343
 
3344
+ const configCard = document.createElement("section");
3345
+ configCard.className = "card surface-elevated stack-sm";
3346
+
3347
+ const configTitle = document.createElement("h4");
3348
+ configTitle.textContent = "Configuration";
3349
+ configCard.appendChild(configTitle);
3350
+
3351
+ const configDescription = document.createElement("p");
3352
+ configDescription.className = "text-muted";
3353
+ configDescription.textContent =
3354
+ "Edit the full design config generated from PDS metadata.";
3355
+ configCard.appendChild(configDescription);
3356
+
3357
+ const configFormContainer = document.createElement("div");
3358
+ configFormContainer.className = "stack-sm";
3359
+ configCard.appendChild(configFormContainer);
3360
+
3361
+ const configFooter = document.createElement("div");
3362
+ configFooter.className = "flex gap-sm";
3363
+ configCard.appendChild(configFooter);
3364
+ this._drawerConfigFormContainer = configFormContainer;
3365
+ this._drawerConfigFooter = configFooter;
3366
+
3367
+ const fullDesign = shallowClone(PDS?.currentConfig?.design || {});
3368
+ const fullConfigFormResult = await buildFullConfigForm(
3369
+ fullDesign,
3370
+ (event) => this._handleFormSubmit(event, fullConfigFormResult?.form),
3371
+ () => this._handleUndo()
3372
+ );
3373
+
3374
+ if (fullConfigFormResult?.form) {
3375
+ fullConfigFormResult.form._undoBtn = fullConfigFormResult.undoBtn;
3376
+ fullConfigFormResult.undoBtn.disabled = this._undoStack.length === 0;
3377
+ configFormContainer.appendChild(fullConfigFormResult.form);
3378
+ configFooter.appendChild(fullConfigFormResult.applyBtn);
3379
+ configFooter.appendChild(fullConfigFormResult.undoBtn);
3380
+ } else {
3381
+ const unavailable = document.createElement("p");
3382
+ unavailable.className = "text-muted";
3383
+ unavailable.textContent =
3384
+ "Full config metadata is unavailable in this runtime.";
3385
+ configFormContainer.appendChild(unavailable);
3386
+ }
3387
+
3388
+ const templateCard = document.createElement("section");
3389
+ templateCard.className = "card surface-elevated stack-sm";
3390
+
3391
+ const templateTitle = document.createElement("h4");
3392
+ templateTitle.textContent = "Canvas Templates";
3393
+ templateCard.appendChild(templateTitle);
3394
+
3395
+ const templateCanvas = document.createElement("pds-live-template-canvas");
3396
+ templateCanvas.addEventListener("pds:live-template:inject", async (event) => {
3397
+ await this._applyImportResult(event?.detail?.result, { injectTemplate: true });
3398
+ });
3399
+ templateCard.appendChild(templateCanvas);
3400
+
3401
+ const importCard = document.createElement("section");
3402
+ importCard.className = "card surface-elevated stack-sm";
3403
+
3404
+ const importTitle = document.createElement("h4");
3405
+ importTitle.textContent = "Import & Convert";
3406
+ importCard.appendChild(importTitle);
3407
+
3408
+ const importer = document.createElement("pds-live-importer");
3409
+
3410
+ importer.addEventListener("pds:live-import:result", async (event) => {
3411
+ const result = event?.detail?.result;
3412
+ if (!result) return;
3413
+ await this._applyImportResult(result, {
3414
+ injectTemplate: true,
3415
+ importMode: event?.detail?.importMode,
3416
+ });
3417
+ this._endLiveEditSessionAfterImport();
3418
+ });
3419
+
3420
+ importCard.append(importer);
3421
+
2172
3422
  const exportCard = document.createElement("section");
2173
3423
  exportCard.className = "card surface-elevated stack-sm";
2174
3424
 
@@ -2228,54 +3478,49 @@ class PdsLiveEdit extends HTMLElement {
2228
3478
  exportNav.append(exportButton, exportMenu);
2229
3479
  exportCard.appendChild(exportNav);
2230
3480
 
2231
- const searchCard = document.createElement("section");
2232
- searchCard.className = "card surface-elevated stack-sm";
3481
+ const resetButton = document.createElement("button");
3482
+ resetButton.type = "button";
3483
+ resetButton.className = "btn-outline pds-live-editor-reset-btn";
3484
+ resetButton.textContent = "Reset Config";
3485
+ resetButton.addEventListener("click", async () => {
3486
+ await this.resetConfig();
3487
+ });
2233
3488
 
2234
- const searchTitle = document.createElement("h4");
2235
- searchTitle.textContent = "Search PDS";
2236
- searchCard.appendChild(searchTitle);
3489
+ const drawerFooter = document.createElement("div");
3490
+ drawerFooter.className = "pds-live-editor-drawer-footer";
3491
+ drawerFooter.appendChild(resetButton);
2237
3492
 
2238
- const omnibox = document.createElement("pds-omnibox");
2239
- omnibox.setAttribute("placeholder", "Search tokens, utilities, components...");
2240
- omnibox.settings = {
2241
- iconHandler: (item) => {
2242
- return item.icon ? `<pds-icon icon="${item.icon}"></pds-icon>` : null;
2243
- },
2244
- categories: {
2245
- Query: {
2246
- trigger: (options) => options.search.length >= 2,
2247
- getItems: async (options) => {
2248
- const query = (options.search || "").trim();
2249
- if (!query) return [];
2250
- try {
2251
- const results = await PDS.query(query);
2252
- return (results || []).map((result) => ({
2253
- text: result.text,
2254
- id: result.value,
2255
- icon: result.icon || "magnifying-glass",
2256
- category: result.category,
2257
- code: result.code,
2258
- }));
2259
- } catch (error) {
2260
- console.warn("Omnibox query failed:", error);
2261
- return [];
2262
- }
2263
- },
2264
- action: async (options) => {
2265
- if (options?.code && navigator.clipboard) {
2266
- await navigator.clipboard.writeText(options.code);
2267
- await PDS.toast("Copied token to clipboard", { type: "success" });
2268
- }
2269
- },
2270
- },
2271
- },
3493
+ const accordion = document.createElement("section");
3494
+ accordion.className = "accordion";
3495
+ accordion.setAttribute("aria-label", "Design settings groups");
3496
+
3497
+ const buildAccordionGroup = (title, nodes = [], open = false, sectionId = "") => {
3498
+ const details = document.createElement("details");
3499
+ if (open) details.open = true;
3500
+ if (sectionId) details.dataset.section = sectionId;
3501
+
3502
+ const summary = document.createElement("summary");
3503
+ summary.textContent = title;
3504
+
3505
+ const body = document.createElement("div");
3506
+ body.className = "stack-md";
3507
+ nodes.forEach((node) => {
3508
+ if (node) body.appendChild(node);
3509
+ });
3510
+
3511
+ details.append(summary, body);
3512
+ return details;
2272
3513
  };
2273
- searchCard.appendChild(omnibox);
2274
3514
 
2275
- content.appendChild(presetCard);
2276
- content.appendChild(themeCard);
2277
- content.appendChild(exportCard);
2278
- content.appendChild(searchCard);
3515
+ accordion.appendChild(buildAccordionGroup("Preset & Theme", [presetCard, themeCard], true, "preset-theme"));
3516
+ accordion.appendChild(buildAccordionGroup("Configuration", [configCard], false, "configuration"));
3517
+ accordion.appendChild(buildAccordionGroup("Canvas", [templateCard], false, "canvas"));
3518
+ accordion.appendChild(
3519
+ buildAccordionGroup("Import & Export", [importCard, exportCard], false, "import-convert-export")
3520
+ );
3521
+
3522
+ content.appendChild(accordion);
3523
+ content.appendChild(drawerFooter);
2279
3524
 
2280
3525
  this._drawer.replaceChildren(header, content);
2281
3526
 
@@ -2290,6 +3535,161 @@ class PdsLiveEdit extends HTMLElement {
2290
3535
  await exportFromLiveEdit(format);
2291
3536
  }
2292
3537
 
3538
+ _ensureLiveCanvasContainer() {
3539
+ let container = document.getElementById("pds-live-edit-canvas");
3540
+ if (container) return container;
3541
+
3542
+ container = document.createElement("section");
3543
+ container.id = "pds-live-edit-canvas";
3544
+ container.className = "card stack-md";
3545
+
3546
+ const heading = document.createElement("h3");
3547
+ heading.textContent = "PDS Live Canvas";
3548
+ const description = document.createElement("p");
3549
+ description.className = "text-muted";
3550
+ description.textContent = "Injected templates render here to preview config changes.";
3551
+
3552
+ const canvas = document.createElement("div");
3553
+ canvas.id = "pds-live-edit-canvas-content";
3554
+ canvas.className = "stack-md";
3555
+
3556
+ container.append(heading, description, canvas);
3557
+ document.body.appendChild(container);
3558
+ return container;
3559
+ }
3560
+
3561
+ _injectTemplateIntoCanvas(template) {
3562
+ if (!template || typeof template !== "object") return;
3563
+ const container = this._ensureLiveCanvasContainer();
3564
+ const canvas = container.querySelector("#pds-live-edit-canvas-content");
3565
+ if (!canvas) return;
3566
+ canvas.innerHTML = String(template.html || "");
3567
+ }
3568
+
3569
+ async openDesignSettings() {
3570
+ const target = this._activeTarget || document.body;
3571
+ const quickPaths = this._activeTarget ? collectQuickContext(this._activeTarget).paths : [];
3572
+
3573
+ await this._openDrawer(target, quickPaths);
3574
+
3575
+ const drawer = this._drawer;
3576
+ if (!drawer) return;
3577
+
3578
+ const presetGroup = drawer.querySelector('details[data-section="preset-theme"]');
3579
+ if (presetGroup) {
3580
+ presetGroup.setAttribute("open", "");
3581
+ }
3582
+
3583
+ const configGroup = drawer.querySelector('details[data-section="configuration"]');
3584
+ if (configGroup) {
3585
+ configGroup.setAttribute("open", "");
3586
+ }
3587
+ }
3588
+
3589
+ async resetConfig() {
3590
+ let confirmed = true;
3591
+ if (typeof PDS?.ask === "function") {
3592
+ confirmed = await PDS.ask(
3593
+ "This clears your saved local configuration and reloads the page.",
3594
+ {
3595
+ title: "Reset Config?",
3596
+ type: "confirm",
3597
+ buttons: {
3598
+ ok: { name: "Reset", variant: "danger" },
3599
+ cancel: { name: "Cancel", cancel: true },
3600
+ },
3601
+ }
3602
+ );
3603
+ }
3604
+
3605
+ if (!confirmed) return false;
3606
+
3607
+ try {
3608
+ window.localStorage.removeItem("pure-ds-config");
3609
+ } catch (e) {}
3610
+
3611
+ window.location.reload();
3612
+ return true;
3613
+ }
3614
+
3615
+ async openImportDetailsFromToast(options = {}) {
3616
+ const requestedFileName = String(options?.fileName || "");
3617
+ const requestedSourceType = String(options?.sourceType || "");
3618
+ const requestedImportMode = String(options?.importMode || "");
3619
+ const target = this._activeTarget || document.body;
3620
+ const quickPaths = this._activeTarget ? collectQuickContext(this._activeTarget).paths : [];
3621
+
3622
+ await this._openDrawer(target, quickPaths);
3623
+
3624
+ const drawer = this._drawer;
3625
+ if (!drawer) return;
3626
+
3627
+ const importGroup = drawer.querySelector('details[data-section="import-convert-export"]');
3628
+ if (importGroup) {
3629
+ importGroup.setAttribute("open", "");
3630
+ }
3631
+
3632
+ const tryOpenImporterDetails = (attempt = 0) => {
3633
+ const importer = drawer.querySelector("pds-live-importer");
3634
+ if (!importer) {
3635
+ if (attempt < 12) setTimeout(() => tryOpenImporterDetails(attempt + 1), 100);
3636
+ return;
3637
+ }
3638
+
3639
+ if (typeof importer.openHistoryDetailsByMeta === "function") {
3640
+ importer.openHistoryDetailsByMeta({
3641
+ fileName: requestedFileName,
3642
+ sourceType: requestedSourceType,
3643
+ importMode: requestedImportMode,
3644
+ });
3645
+ }
3646
+ };
3647
+
3648
+ tryOpenImporterDetails();
3649
+ }
3650
+
3651
+ _endLiveEditSessionAfterImport() {
3652
+ this._removeActiveUI();
3653
+
3654
+ if (this._drawer) {
3655
+ if (typeof this._drawer.closeDrawer === "function") {
3656
+ this._drawer.closeDrawer();
3657
+ } else {
3658
+ this._drawer.removeAttribute("open");
3659
+ }
3660
+ }
3661
+
3662
+ this.dispatchEvent(
3663
+ new CustomEvent("pds:live-edit:disable", {
3664
+ bubbles: true,
3665
+ composed: true,
3666
+ detail: { reason: "import-complete" },
3667
+ })
3668
+ );
3669
+ }
3670
+
3671
+ async _applyImportResult(result, options = {}) {
3672
+ if (!result || typeof result !== "object") return;
3673
+
3674
+ const importMode = String(options?.importMode || result?.meta?.importMode || "convert-only");
3675
+ const injectTemplate = options.injectTemplate !== false;
3676
+ const validationBlocked = Boolean(result?.meta?.validationBlocked || result?.meta?.validation?.ok === false);
3677
+ const shouldApplyPatch =
3678
+ options.applyDesignPatch === true ||
3679
+ (options.applyDesignPatch !== false && importMode !== "convert-only" && !validationBlocked);
3680
+ const patch = result.designPatch;
3681
+ const patchKeys =
3682
+ patch && typeof patch === "object" ? Object.keys(patch).filter(Boolean) : [];
3683
+
3684
+ if (shouldApplyPatch && patchKeys.length > 0) {
3685
+ await applyDesignPatch(patch);
3686
+ }
3687
+
3688
+ if (injectTemplate && result.template?.html) {
3689
+ this._injectTemplateIntoCanvas(result.template);
3690
+ }
3691
+ }
3692
+
2293
3693
  async _handleFormSubmit(event, form) {
2294
3694
  if (!form || typeof form.getValuesFlat !== "function") return;
2295
3695
 
@@ -2308,9 +3708,24 @@ class PdsLiveEdit extends HTMLElement {
2308
3708
  }
2309
3709
 
2310
3710
  // Apply the changes
2311
- const flatValues = form.getValuesFlat();
2312
- const patch = {};
3711
+ const eventJson = event?.detail?.json;
3712
+ const hasEventPayload =
3713
+ eventJson && typeof eventJson === "object" && Object.keys(eventJson).length > 0;
3714
+ const flatValues = hasEventPayload
3715
+ ? eventJson
3716
+ : typeof form._normalizeFlatValues === "function"
3717
+ ? form._normalizeFlatValues(form.getValuesFlat())
3718
+ : form.getValuesFlat();
3719
+ const sanitizedFlatValues = {};
2313
3720
  Object.entries(flatValues || {}).forEach(([path, value]) => {
3721
+ if (path === FORM_THEME_CONTEXT_POINTER || path === FORM_THEME_CONTEXT_FIELD) {
3722
+ return;
3723
+ }
3724
+ sanitizedFlatValues[path] = value;
3725
+ });
3726
+
3727
+ const patch = {};
3728
+ Object.entries(sanitizedFlatValues).forEach(([path, value]) => {
2314
3729
  setValueAtJsonPath(patch, path, value);
2315
3730
  });
2316
3731
  await applyDesignPatch(patch);