@pure-ds/core 0.6.8 → 0.6.10

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 (42) hide show
  1. package/custom-elements.json +71 -28
  2. package/dist/types/pds.d.ts +30 -0
  3. package/dist/types/public/assets/js/pds-manager.d.ts +146 -429
  4. package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
  5. package/dist/types/public/assets/js/pds.d.ts +3 -4
  6. package/dist/types/public/assets/js/pds.d.ts.map +1 -1
  7. package/dist/types/public/assets/pds/components/pds-form.d.ts.map +1 -1
  8. package/dist/types/public/assets/pds/components/pds-live-edit.d.ts +1 -169
  9. package/dist/types/public/assets/pds/components/pds-live-edit.d.ts.map +1 -1
  10. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts +0 -2
  11. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts.map +1 -1
  12. package/dist/types/src/js/pds-core/pds-config.d.ts +1306 -13
  13. package/dist/types/src/js/pds-core/pds-config.d.ts.map +1 -1
  14. package/dist/types/src/js/pds-core/pds-enhancers-meta.d.ts.map +1 -1
  15. package/dist/types/src/js/pds-core/pds-enhancers.d.ts.map +1 -1
  16. package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
  17. package/dist/types/src/js/pds-core/pds-live.d.ts.map +1 -1
  18. package/dist/types/src/js/pds-core/pds-ontology.d.ts.map +1 -1
  19. package/dist/types/src/js/pds-core/pds-start-helpers.d.ts +1 -4
  20. package/dist/types/src/js/pds-core/pds-start-helpers.d.ts.map +1 -1
  21. package/dist/types/src/js/pds-manager.d.ts +1 -0
  22. package/dist/types/src/js/pds.d.ts.map +1 -1
  23. package/package.json +2 -2
  24. package/packages/pds-cli/bin/pds-static.js +16 -1
  25. package/public/assets/js/app.js +21 -21
  26. package/public/assets/js/pds-manager.js +291 -161
  27. package/public/assets/js/pds.js +16 -16
  28. package/public/assets/pds/components/pds-form.js +124 -27
  29. package/public/assets/pds/components/pds-live-edit.js +1214 -104
  30. package/public/assets/pds/components/pds-omnibox.js +10 -18
  31. package/public/assets/pds/custom-elements.json +71 -28
  32. package/public/assets/pds/pds-css-complete.json +1 -6
  33. package/public/assets/pds/pds.css-data.json +5 -35
  34. package/src/js/pds-core/pds-config.js +822 -31
  35. package/src/js/pds-core/pds-enhancers-meta.js +11 -0
  36. package/src/js/pds-core/pds-enhancers.js +113 -5
  37. package/src/js/pds-core/pds-generator.js +183 -23
  38. package/src/js/pds-core/pds-live.js +177 -2
  39. package/src/js/pds-core/pds-ontology.js +6 -0
  40. package/src/js/pds-core/pds-start-helpers.js +14 -6
  41. package/src/js/pds.d.ts +30 -0
  42. package/src/js/pds.js +36 -60
@@ -137,6 +137,7 @@ const SURFACE_CONTEXT_PATHS = new Set([
137
137
  const DARK_MODE_PATH_MARKER = ".darkMode.";
138
138
  const QUICK_EDIT_LIMIT = 4;
139
139
  const DROPDOWN_VIEWPORT_PADDING = 8;
140
+ const FONT_FAMILY_PATH_REGEX = /^typography\.fontFamily/i;
140
141
 
141
142
  function isHoverCapable() {
142
143
  if (typeof window === "undefined" || !window.matchMedia) return false;
@@ -187,14 +188,22 @@ ${EDITOR_TAG} {
187
188
  .${DROPDOWN_CLASS} menu {
188
189
  min-width: max-content;
189
190
  max-width: 350px;
191
+ margin: 0;
192
+ padding: 0;
193
+ list-style: none;
194
+ overflow: visible;
190
195
  }
191
196
  .${DROPDOWN_CLASS} .pds-live-editor-menu {
197
+ display: block;
198
+ background-color: var(--color-surface-base);
192
199
  padding: var(--spacing-1);
193
200
  max-width: 350px;
194
201
  padding-bottom: 0;
202
+ overflow: visible;
195
203
  }
196
204
  .${DROPDOWN_CLASS} .pds-live-editor-form-container {
197
205
  padding-bottom: var(--spacing-2);
206
+ overflow: visible;
198
207
  }
199
208
  .${DROPDOWN_CLASS} .pds-live-editor-title {
200
209
  display: block;
@@ -443,6 +452,33 @@ function collectQuickRulePaths(target) {
443
452
  }).flatMap((rule) => rule.paths);
444
453
  }
445
454
 
455
+ function isHeadingElement(target) {
456
+ if (!target || !(target instanceof Element)) return false;
457
+ const tag = target.tagName?.toLowerCase?.() || "";
458
+ if (/^h[1-6]$/.test(tag)) return true;
459
+ if (target.getAttribute("role") === "heading") return true;
460
+ return false;
461
+ }
462
+
463
+ function hasMeaningfulText(target) {
464
+ if (!target || !(target instanceof Element)) return false;
465
+ const tag = target.tagName?.toLowerCase?.() || "";
466
+ if (["script", "style", "svg", "path", "defs", "symbol"].includes(tag)) return false;
467
+ const text = target.textContent || "";
468
+ return text.trim().length > 0;
469
+ }
470
+
471
+ function collectTypographyPathsForTarget(target) {
472
+ if (!target || !(target instanceof Element)) return [];
473
+ if (isHeadingElement(target)) {
474
+ return ["typography.fontFamilyHeadings"];
475
+ }
476
+ if (hasMeaningfulText(target)) {
477
+ return ["typography.fontFamilyBody"];
478
+ }
479
+ return [];
480
+ }
481
+
446
482
  function pathExistsInDesign(path, design) {
447
483
  if (!path || !design) return false;
448
484
  const segments = path.split(".");
@@ -455,7 +491,7 @@ function pathExistsInDesign(path, design) {
455
491
  return true;
456
492
  }
457
493
 
458
- function filterPathsByContext(target, paths) {
494
+ function filterPathsByContext(target, paths, alwaysAllow = new Set()) {
459
495
  if (!target || !paths.length) return paths;
460
496
  const isGlobal = target.matches("body, main");
461
497
  const isInForm = Boolean(target.closest("form, pds-form"));
@@ -468,6 +504,7 @@ function filterPathsByContext(target, paths) {
468
504
  const design = PDS?.currentConfig?.design || {};
469
505
 
470
506
  return paths.filter((path) => {
507
+ if (alwaysAllow.has(path)) return true;
471
508
  if (!theme.isDark && path.includes(DARK_MODE_PATH_MARKER)) return false;
472
509
  if (path.startsWith("typography.") && !isGlobal) return false;
473
510
  if (GLOBAL_LAYOUT_PATHS.has(path) && !isGlobal) return false;
@@ -560,6 +597,62 @@ function toColorInputValue(value) {
560
597
  return hexValue || value;
561
598
  }
562
599
 
600
+ function isColorPath(path) {
601
+ return String(path || "").toLowerCase().startsWith("colors.");
602
+ }
603
+
604
+ function inferColorVariableCandidates(path) {
605
+ const normalizedPath = String(path || "").toLowerCase();
606
+ const key = normalizedPath.replace(/^colors\./, "").replace(/^darkmode\./, "");
607
+ const tail = key.split(".").pop();
608
+
609
+ const directMap = {
610
+ primary: ["--color-primary-500"],
611
+ secondary: ["--color-secondary-500", "--color-gray-500"],
612
+ accent: ["--color-accent-500"],
613
+ background: ["--color-surface-base"],
614
+ success: ["--color-success-500"],
615
+ warning: ["--color-warning-500"],
616
+ danger: ["--color-danger-500"],
617
+ info: ["--color-info-500"],
618
+ };
619
+
620
+ const candidates = new Set();
621
+ if (tail && directMap[tail]) {
622
+ directMap[tail].forEach((item) => candidates.add(item));
623
+ }
624
+ if (tail) {
625
+ candidates.add(`--color-${tail}-500`);
626
+ }
627
+
628
+ return Array.from(candidates);
629
+ }
630
+
631
+ function resolveColorValueForPath(path, value, hintValue) {
632
+ if (!isColorPath(path)) return null;
633
+
634
+ const fromValue = toColorInputValue(value);
635
+ if (normalizeHexColor(fromValue)) return fromValue;
636
+
637
+ const fromHint = toColorInputValue(hintValue);
638
+ if (normalizeHexColor(fromHint)) return fromHint;
639
+
640
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
641
+ const root = document.documentElement;
642
+ if (!root) return null;
643
+
644
+ const style = window.getComputedStyle(root);
645
+ const candidates = inferColorVariableCandidates(path);
646
+ for (const varName of candidates) {
647
+ const raw = style.getPropertyValue(varName).trim();
648
+ if (!raw) continue;
649
+ const resolved = toColorInputValue(raw);
650
+ if (normalizeHexColor(resolved)) return resolved;
651
+ }
652
+
653
+ return null;
654
+ }
655
+
563
656
  function getCustomPropertyNames(style) {
564
657
  const names = [];
565
658
  if (!style) return names;
@@ -768,7 +861,10 @@ function collectPathsFromComputedStyles(target) {
768
861
  const trimmed = value.trim();
769
862
 
770
863
  // Extract var refs from the value (handles var() with fallbacks)
771
- addVarSet(extractAllVarRefs(trimmed));
864
+ const directVarRefs = extractAllVarRefs(trimmed);
865
+ addVarSet(directVarRefs);
866
+ const hasDirectVarRefs = directVarRefs.size > 0;
867
+ if (hasDirectVarRefs) return;
772
868
 
773
869
  if (trimmed && valueToVars.has(trimmed)) {
774
870
  valueToVars.get(trimmed).forEach((varName) => addVarName(varName));
@@ -829,15 +925,18 @@ function collectQuickContext(target) {
829
925
  const byComputed = computed?.paths || [];
830
926
  const byRelations = collectPathsFromRelations(target);
831
927
  const byQuickRules = collectQuickRulePaths(target);
928
+ const byTypographyContext = collectTypographyPathsForTarget(target);
832
929
  const hints = computed?.hints || {};
833
930
  const debug = computed?.debug || { vars: [], paths: [] };
931
+ const alwaysAllow = new Set(byTypographyContext);
834
932
 
835
933
  // Prioritize quick rule paths first (selector-based), then computed/relations
836
934
  const filtered = filterPathsByContext(target, [
935
+ ...byTypographyContext,
837
936
  ...byQuickRules,
838
937
  ...byComputed,
839
938
  ...byRelations,
840
- ]);
939
+ ], alwaysAllow);
841
940
  if (!filtered.length) {
842
941
  return {
843
942
  paths: normalizePaths(DEFAULT_QUICK_PATHS),
@@ -860,6 +959,252 @@ function collectDrawerPaths(quickPaths) {
860
959
  return normalizePaths([...quickPaths, ...expanded]);
861
960
  }
862
961
 
962
+ function splitFontFamilyStack(value) {
963
+ if (typeof value !== "string") return [];
964
+ const input = value.trim();
965
+ if (!input) return [];
966
+ const parts = [];
967
+ let buffer = "";
968
+ let quote = null;
969
+ for (let i = 0; i < input.length; i += 1) {
970
+ const char = input[i];
971
+ if (quote) {
972
+ buffer += char;
973
+ if (char === quote && input[i - 1] !== "\\") {
974
+ quote = null;
975
+ }
976
+ continue;
977
+ }
978
+ if (char === '"' || char === "'") {
979
+ quote = char;
980
+ buffer += char;
981
+ continue;
982
+ }
983
+ if (char === ",") {
984
+ const token = buffer.trim();
985
+ if (token) parts.push(token);
986
+ buffer = "";
987
+ continue;
988
+ }
989
+ buffer += char;
990
+ }
991
+ const last = buffer.trim();
992
+ if (last) parts.push(last);
993
+ return parts;
994
+ }
995
+
996
+ const GENERIC_FONT_FAMILIES = new Set([
997
+ "serif",
998
+ "sans-serif",
999
+ "monospace",
1000
+ "cursive",
1001
+ "fantasy",
1002
+ "system-ui",
1003
+ "ui-serif",
1004
+ "ui-sans-serif",
1005
+ "ui-monospace",
1006
+ "ui-rounded",
1007
+ "emoji",
1008
+ "math",
1009
+ "fangsong",
1010
+ ]);
1011
+
1012
+ let loadGoogleFontFnPromise = null;
1013
+
1014
+ function normalizeFontName(fontFamily) {
1015
+ return String(fontFamily || "")
1016
+ .trim()
1017
+ .replace(/^['"]+|['"]+$/g, "")
1018
+ .trim();
1019
+ }
1020
+
1021
+ function isLikelyLoadableFont(fontFamily) {
1022
+ const normalized = normalizeFontName(fontFamily).toLowerCase();
1023
+ if (!normalized) return false;
1024
+ return !GENERIC_FONT_FAMILIES.has(normalized);
1025
+ }
1026
+
1027
+ async function getLoadGoogleFontFn() {
1028
+ if (typeof PDS?.loadGoogleFont === "function") {
1029
+ return PDS.loadGoogleFont;
1030
+ }
1031
+ if (loadGoogleFontFnPromise) return loadGoogleFontFnPromise;
1032
+ loadGoogleFontFnPromise = (async () => {
1033
+ const candidates = [
1034
+ PDS?.currentConfig?.managerURL,
1035
+ "../core/pds-manager.js",
1036
+ "/assets/pds/core/pds-manager.js",
1037
+ ].filter(Boolean);
1038
+
1039
+ const attempted = new Set();
1040
+ for (const candidate of candidates) {
1041
+ try {
1042
+ const resolved = new URL(candidate, import.meta.url).href;
1043
+ if (attempted.has(resolved)) continue;
1044
+ attempted.add(resolved);
1045
+ const mod = await import(resolved);
1046
+ if (typeof mod?.loadGoogleFont === "function") {
1047
+ return mod.loadGoogleFont;
1048
+ }
1049
+ } catch (e) {}
1050
+ }
1051
+ return null;
1052
+ })();
1053
+ return loadGoogleFontFnPromise;
1054
+ }
1055
+
1056
+ async function loadTypographyFontsForDesign(typography) {
1057
+ if (!typography || typeof typography !== "object") return;
1058
+
1059
+ const loadGoogleFont = await getLoadGoogleFontFn();
1060
+ if (typeof loadGoogleFont !== "function") return;
1061
+
1062
+ const families = [
1063
+ typography.fontFamilyHeadings,
1064
+ typography.fontFamilyBody,
1065
+ typography.fontFamilyMono,
1066
+ ];
1067
+
1068
+ const fontNames = new Set();
1069
+ families.forEach((stack) => {
1070
+ splitFontFamilyStack(stack).forEach((item) => {
1071
+ const fontName = normalizeFontName(item);
1072
+ if (isLikelyLoadableFont(fontName)) {
1073
+ fontNames.add(fontName);
1074
+ }
1075
+ });
1076
+ });
1077
+
1078
+ await Promise.allSettled(Array.from(fontNames).map((name) => loadGoogleFont(name)));
1079
+ }
1080
+
1081
+ function getPresetFontFamilyVariations(previewFontSize = resolveFontFamilyPreviewFontSize()) {
1082
+ const presets = Object.values(PDS?.presets || {});
1083
+ const seen = new Set();
1084
+ const items = [];
1085
+ const addItem = (fontFamily) => {
1086
+ const normalized = String(fontFamily || "").trim().replace(/\s+/g, " ");
1087
+ if (!normalized || seen.has(normalized)) return;
1088
+ seen.add(normalized);
1089
+ items.push({
1090
+ //index: items.length,
1091
+ id: normalized,
1092
+ value: normalized,
1093
+ text: normalized,
1094
+ style: `font-family: ${normalized}; font-size: ${previewFontSize};`,
1095
+ });
1096
+ };
1097
+
1098
+ presets.forEach((preset) => {
1099
+ const typography = preset?.typography || {};
1100
+ ["fontFamilyHeadings", "fontFamilyBody"].forEach((key) => {
1101
+ const stack = typography[key];
1102
+ if (typeof stack !== "string" || !stack.trim()) return;
1103
+
1104
+ addItem(stack);
1105
+ const parts = splitFontFamilyStack(stack);
1106
+ parts.forEach((part) => addItem(part));
1107
+ for (let i = 1; i < parts.length; i += 1) {
1108
+ addItem(parts.slice(i).join(", "));
1109
+ }
1110
+ });
1111
+ });
1112
+
1113
+ return items.sort((a, b) =>
1114
+ String(b?.text || "").localeCompare(String(a?.text || ""), undefined, {
1115
+ sensitivity: "base",
1116
+ })
1117
+ );
1118
+ }
1119
+
1120
+ function resolveFontFamilyPreviewFontSize(control) {
1121
+ const controlInput = control?.querySelector?.(".ac-input");
1122
+ if (controlInput) {
1123
+ const fontSize = getComputedStyle(controlInput).fontSize;
1124
+ if (fontSize) return fontSize;
1125
+ }
1126
+
1127
+ const selectors = [
1128
+ "[name='/typography/fontFamilyBody']",
1129
+ "[name='/typography/fontFamilyHeadings']",
1130
+ "[name='/typography/fontFamilyMono']",
1131
+ ];
1132
+
1133
+ for (const selector of selectors) {
1134
+ const omnibox = document.querySelector(selector);
1135
+ const input = omnibox?.shadowRoot?.querySelector?.(".ac-input");
1136
+ if (!input) continue;
1137
+ const fontSize = getComputedStyle(input).fontSize;
1138
+ if (fontSize) return fontSize;
1139
+ }
1140
+
1141
+ return "var(--font-size-md)";
1142
+ }
1143
+
1144
+ async function loadGoogleFontsForFontFamilyItems(items) {
1145
+ if (!Array.isArray(items) || !items.length) return;
1146
+
1147
+ const loadGoogleFont = await getLoadGoogleFontFn();
1148
+ if (typeof loadGoogleFont !== "function") return;
1149
+
1150
+ const fontNames = new Set();
1151
+ items.forEach((item) => {
1152
+ const stack = item?.value || item?.text;
1153
+ splitFontFamilyStack(stack).forEach((entry) => {
1154
+ const fontName = normalizeFontName(entry);
1155
+ if (isLikelyLoadableFont(fontName)) {
1156
+ fontNames.add(fontName);
1157
+ }
1158
+ });
1159
+ });
1160
+
1161
+ if (!fontNames.size) return;
1162
+ await Promise.allSettled(Array.from(fontNames).map((name) => loadGoogleFont(name)));
1163
+ }
1164
+
1165
+ function buildFontFamilyOmniboxSettings() {
1166
+ const filterItems = (items, search) => {
1167
+ const query = String(search || "").trim().toLowerCase();
1168
+ if (!query) return items;
1169
+ return items.filter((item) => {
1170
+ const text = String(
1171
+ item?.text || item?.id || item?.element?.textContent || ""
1172
+ ).toLowerCase();
1173
+ return text.includes(query);
1174
+ });
1175
+ };
1176
+
1177
+
1178
+ return {
1179
+ //debug: true,
1180
+ itemGrid: "0 1fr 0",
1181
+ hideCategory: true,
1182
+ iconHandler: (item) => {
1183
+ return "";
1184
+ },
1185
+ categories: {
1186
+ FontFamilies: {
1187
+ trigger: () => true,
1188
+ getItems: async (options) => {
1189
+ const previewFontSize = resolveFontFamilyPreviewFontSize(options?.control);
1190
+ const allItems = getPresetFontFamilyVariations(previewFontSize);
1191
+ const items = filterItems(allItems, options?.search);
1192
+
1193
+ await loadGoogleFontsForFontFamilyItems(items);
1194
+
1195
+ return items;
1196
+ },
1197
+ action: (options) => {
1198
+ const input = document.querySelector("pds-omnibox");
1199
+ if (input) {
1200
+ input.value = options.text;
1201
+ }
1202
+ },
1203
+ },
1204
+ },
1205
+ };
1206
+ }
1207
+
863
1208
  function buildSchemaFromPaths(paths, design, hints = {}) {
864
1209
  const schema = { type: "object", properties: {} };
865
1210
  const uiSchema = {};
@@ -900,7 +1245,7 @@ function buildSchemaFromPaths(paths, design, hints = {}) {
900
1245
  };
901
1246
 
902
1247
  const isColorValue = (value, path) => {
903
- if (String(path || "").toLowerCase().startsWith("colors.")) return true;
1248
+ if (isColorPath(path)) return true;
904
1249
  if (typeof value !== "string") return false;
905
1250
  return /^#([0-9a-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
906
1251
  };
@@ -914,6 +1259,13 @@ function buildSchemaFromPaths(paths, design, hints = {}) {
914
1259
  if (!parent) {
915
1260
  parent = { type: "object", title: titleize(category), properties: {} };
916
1261
  schema.properties[category] = parent;
1262
+
1263
+ if (category === "colors") {
1264
+ uiSchema[`/${category}`] = {
1265
+ "ui:layout": "flex",
1266
+ "ui:layoutOptions": { wrap: true, gap: "sm" },
1267
+ };
1268
+ }
917
1269
  }
918
1270
 
919
1271
  let current = parent;
@@ -923,8 +1275,12 @@ function buildSchemaFromPaths(paths, design, hints = {}) {
923
1275
  const value = getValueAtPath(design, [category, ...rest]);
924
1276
  const hintValue = hints[path];
925
1277
  const enumOptions = getEnumOptions(path);
926
- const normalizedValue = normalizeEnumValue(path, value);
927
- const normalizedHint = normalizeEnumValue(path, hintValue);
1278
+ const resolvedColorValue = resolveColorValueForPath(path, value, hintValue);
1279
+ const normalizedValue = normalizeEnumValue(path, resolvedColorValue ?? value);
1280
+ const normalizedHint = normalizeEnumValue(
1281
+ path,
1282
+ resolvedColorValue ?? hintValue,
1283
+ );
928
1284
  const inferredType = Array.isArray(value)
929
1285
  ? "array"
930
1286
  : value === null
@@ -973,6 +1329,8 @@ function buildSchemaFromPaths(paths, design, hints = {}) {
973
1329
  uiEntry["ui:step"] = bounds.step;
974
1330
  } else if (isColorValue(value, path)) {
975
1331
  uiEntry["ui:widget"] = "input-color";
1332
+ } else if (FONT_FAMILY_PATH_REGEX.test(path) && schemaType === "string") {
1333
+ uiEntry["ui:widget"] = "font-family-omnibox";
976
1334
  }
977
1335
 
978
1336
  const isTextOrNumberInput =
@@ -1089,6 +1447,10 @@ async function applyDesignPatch(patch) {
1089
1447
  const nextOptions = { ...currentOptions, design: nextDesign };
1090
1448
  if (resolvedPresetId) nextOptions.preset = resolvedPresetId;
1091
1449
 
1450
+ try {
1451
+ await loadTypographyFontsForDesign(nextDesign?.typography);
1452
+ } catch (e) {}
1453
+
1092
1454
  const nextGenerator = new Generator(nextOptions);
1093
1455
  if (PDS?.applyStyles) {
1094
1456
  await PDS.applyStyles(nextGenerator);
@@ -1164,6 +1526,17 @@ function getActivePresetId() {
1164
1526
  return stored?.preset || PDS?.currentConfig?.preset || PDS?.currentPreset || null;
1165
1527
  }
1166
1528
 
1529
+ function getPresetNameById(presetId) {
1530
+ if (!presetId) return "";
1531
+ const presets = PDS?.presets || {};
1532
+ const preset =
1533
+ presets?.[presetId] ||
1534
+ Object.values(presets || {}).find(
1535
+ (candidate) => String(candidate?.id || candidate?.name) === String(presetId)
1536
+ );
1537
+ return preset?.name || String(presetId);
1538
+ }
1539
+
1167
1540
  async function applyPresetSelection(presetId) {
1168
1541
  if (!presetId) return;
1169
1542
  setStoredConfig({
@@ -1173,15 +1546,208 @@ async function applyPresetSelection(presetId) {
1173
1546
  await applyDesignPatch({});
1174
1547
  }
1175
1548
 
1549
+ function figmafyTokens(rawTokens) {
1550
+ const isPlainObject = (value) =>
1551
+ value !== null && typeof value === "object" && !Array.isArray(value);
1552
+
1553
+ const detectType = (path, key, value) => {
1554
+ const root = path[0];
1555
+
1556
+ if (root === "colors") {
1557
+ if (key === "scheme") return "string";
1558
+ return "color";
1559
+ }
1560
+
1561
+ if (root === "spacing" || root === "radius" || root === "borderWidths") {
1562
+ return "dimension";
1563
+ }
1564
+
1565
+ if (root === "typography") {
1566
+ const group = path[1];
1567
+ if (group === "fontFamily") return "fontFamily";
1568
+ if (group === "fontSize") return "fontSize";
1569
+ if (group === "fontWeight") return "fontWeight";
1570
+ if (group === "lineHeight") return "lineHeight";
1571
+ return "string";
1572
+ }
1573
+
1574
+ if (root === "shadows") return "shadow";
1575
+ if (root === "layout") return "dimension";
1576
+ if (root === "transitions") return "duration";
1577
+ if (root === "zIndex") return "number";
1578
+
1579
+ if (root === "icons") {
1580
+ if (key === "defaultSize" || path.includes("sizes")) {
1581
+ return "dimension";
1582
+ }
1583
+ return "string";
1584
+ }
1585
+
1586
+ if (typeof value === "number") {
1587
+ return "number";
1588
+ }
1589
+
1590
+ if (typeof value === "string") {
1591
+ if (/^\d+(\.\d+)?ms$/.test(value)) return "duration";
1592
+ if (/^\d+(\.\d+)?(px|rem|em|vh|vw|%)$/.test(value)) return "dimension";
1593
+
1594
+ if (
1595
+ /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value) ||
1596
+ /^(rgb|rgba|hsl|hsla|oklab|lab)\(/.test(value)
1597
+ ) {
1598
+ return "color";
1599
+ }
1600
+ }
1601
+
1602
+ return undefined;
1603
+ };
1604
+
1605
+ const walk = (node, path = []) => {
1606
+ if (node == null) return node;
1607
+
1608
+ if (Array.isArray(node)) {
1609
+ return node.map((item, index) => walk(item, path.concat(String(index))));
1610
+ }
1611
+
1612
+ if (isPlainObject(node)) {
1613
+ if (
1614
+ Object.prototype.hasOwnProperty.call(node, "value") &&
1615
+ (Object.prototype.hasOwnProperty.call(node, "type") ||
1616
+ Object.keys(node).length === 1)
1617
+ ) {
1618
+ return node;
1619
+ }
1620
+
1621
+ const result = {};
1622
+ for (const [key, value] of Object.entries(node)) {
1623
+ result[key] = walk(value, path.concat(key));
1624
+ }
1625
+ return result;
1626
+ }
1627
+
1628
+ const key = path[path.length - 1] ?? "";
1629
+ const type = detectType(path, key, node);
1630
+ let value = node;
1631
+
1632
+ if (type === "number" && typeof value === "string") {
1633
+ const num = Number(value);
1634
+ if (!Number.isNaN(num)) value = num;
1635
+ }
1636
+
1637
+ return type ? { value, type } : { value };
1638
+ };
1639
+
1640
+ return walk(rawTokens, []);
1641
+ }
1642
+
1643
+ function downloadTextFile(content, filename, mimeType) {
1644
+ if (typeof document === "undefined") return;
1645
+ const blob = new Blob([content], { type: mimeType });
1646
+ const url = URL.createObjectURL(blob);
1647
+ const anchor = document.createElement("a");
1648
+ anchor.href = url;
1649
+ anchor.download = filename;
1650
+ anchor.click();
1651
+ URL.revokeObjectURL(url);
1652
+ }
1653
+
1654
+ function getLiveEditExportConfig() {
1655
+ const stored = getStoredConfig();
1656
+ const design = shallowClone(PDS?.currentConfig?.design || stored?.design || {});
1657
+ const preset = stored?.preset || PDS?.currentConfig?.preset || PDS?.currentPreset || null;
1658
+ return {
1659
+ preset,
1660
+ design,
1661
+ };
1662
+ }
1663
+
1664
+ function buildConfigModuleContent(config) {
1665
+ return `export const pdsConfig = ${JSON.stringify(config, null, 2)};\n\nexport default pdsConfig;\n`;
1666
+ }
1667
+
1668
+ async function exportFromLiveEdit(format) {
1669
+ try {
1670
+ if (format === "config") {
1671
+ const config = getLiveEditExportConfig();
1672
+ const content = buildConfigModuleContent(config);
1673
+ downloadTextFile(content, "pds.config.js", "text/javascript");
1674
+ await PDS?.toast?.("Exported config file", { type: "success" });
1675
+ return;
1676
+ }
1677
+
1678
+ if (format === "figma") {
1679
+ const Generator = await getGeneratorClass();
1680
+ const generator = Generator?.instance;
1681
+ if (!generator || typeof generator.generateTokens !== "function") {
1682
+ throw new Error("Token generator unavailable");
1683
+ }
1684
+
1685
+ const rawTokens = generator.generateTokens();
1686
+ const figmaTokens = figmafyTokens(rawTokens);
1687
+ const content = JSON.stringify(figmaTokens, null, 2);
1688
+ downloadTextFile(content, "design-tokens.figma.json", "application/json");
1689
+ await PDS?.toast?.("Exported Figma tokens", { type: "success" });
1690
+ return;
1691
+ }
1692
+ } catch (error) {
1693
+ console.warn("[pds-live-edit] Export failed", error);
1694
+ await PDS?.toast?.("Export failed", { type: "error" });
1695
+ }
1696
+ }
1697
+
1176
1698
  function setFormSchemas(form, schema, uiSchema, design) {
1177
1699
  form.jsonSchema = schema;
1178
1700
  form.uiSchema = uiSchema;
1179
1701
  form.values = shallowClone(design);
1180
1702
  }
1181
1703
 
1182
- async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1183
- const { schema, uiSchema } = buildSchemaFromPaths(paths, design, hints);
1704
+ async function waitForElementDefinition(tagName, timeoutMs = 4000) {
1705
+ if (customElements.get(tagName)) return true;
1706
+
1707
+ let probe = null;
1708
+ try {
1709
+ if (typeof document !== "undefined" && document.body) {
1710
+ probe = document.createElement(tagName);
1711
+ probe.setAttribute("hidden", "");
1712
+ probe.setAttribute("aria-hidden", "true");
1713
+ probe.style.display = "none";
1714
+ document.body.appendChild(probe);
1715
+ }
1716
+ } catch (e) {}
1717
+
1718
+ await Promise.race([
1719
+ customElements.whenDefined(tagName),
1720
+ new Promise((_, reject) => {
1721
+ setTimeout(() => reject(new Error(`Timed out waiting for <${tagName}> definition`)), timeoutMs);
1722
+ }),
1723
+ ]);
1724
+
1725
+ try {
1726
+ if (probe && probe.parentNode) {
1727
+ probe.parentNode.removeChild(probe);
1728
+ }
1729
+ } catch (e) {}
1730
+
1731
+ if (!customElements.get(tagName)) {
1732
+ throw new Error(`<${tagName}> is not defined`);
1733
+ }
1734
+
1735
+ return true;
1736
+ }
1737
+
1738
+ async function createConfiguredForm({
1739
+ schema,
1740
+ uiSchema,
1741
+ values,
1742
+ onSubmit,
1743
+ onUndo,
1744
+ normalizeFlatValues,
1745
+ formOptions,
1746
+ }) {
1747
+ await waitForElementDefinition("pds-form");
1748
+
1184
1749
  const form = document.createElement("pds-form");
1750
+ const fontFamilyOmniboxSettings = buildFontFamilyOmniboxSettings();
1185
1751
  form.setAttribute("hide-actions", "");
1186
1752
  form.options = {
1187
1753
  layouts: {
@@ -1190,68 +1756,464 @@ async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1190
1756
  enhancements: {
1191
1757
  rangeOutput: true,
1192
1758
  },
1759
+ ...(formOptions && typeof formOptions === "object" ? formOptions : {}),
1193
1760
  };
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);
1761
+
1762
+ form.defineRenderer(
1763
+ "font-family-omnibox",
1764
+ ({ id, path, value, attrs, set }) => {
1765
+ const resolveSelectedValue = (options, actionResult, selectionEvent) => {
1766
+ if (typeof actionResult === "string" && actionResult.trim()) {
1767
+ return actionResult;
1768
+ }
1769
+
1770
+ const eventDetail = selectionEvent?.detail;
1771
+ const fromEventValue = String(eventDetail?.value || "").trim();
1772
+ if (fromEventValue) return fromEventValue;
1773
+
1774
+ const fromEventText = String(eventDetail?.text || "").trim();
1775
+ if (fromEventText) return fromEventText;
1776
+
1777
+ const fromEventElementText = String(
1778
+ eventDetail?.element?.textContent || ""
1779
+ ).trim();
1780
+ if (fromEventElementText) return fromEventElementText;
1781
+
1782
+ const fromText = String(options?.text || "").trim();
1783
+ if (fromText) return fromText;
1784
+
1785
+ const fromValue = String(options?.value || "").trim();
1786
+ if (fromValue) return fromValue;
1787
+
1788
+ const fromElementText = String(options?.element?.textContent || "").trim();
1789
+ if (fromElementText) return fromElementText;
1790
+
1791
+ return String(options?.id || "").trim();
1792
+ };
1793
+
1794
+ const categories = Object.fromEntries(
1795
+ Object.entries(fontFamilyOmniboxSettings.categories || {}).map(
1796
+ ([categoryName, categoryConfig]) => {
1797
+ const originalAction = categoryConfig?.action;
1798
+ return [
1799
+ categoryName,
1800
+ {
1801
+ ...categoryConfig,
1802
+ action: (...args) => {
1803
+ const [options, selectionEvent] = args;
1804
+ const actionResult =
1805
+ typeof originalAction === "function"
1806
+ ? originalAction(...args)
1807
+ : undefined;
1808
+
1809
+ if (actionResult && typeof actionResult.then === "function") {
1810
+ return actionResult.then((resolved) => {
1811
+ const selected = resolveSelectedValue(
1812
+ options,
1813
+ resolved,
1814
+ selectionEvent
1815
+ );
1816
+ if (selected) {
1817
+ set(selected);
1818
+ }
1819
+ return resolved;
1820
+ });
1821
+ }
1822
+
1823
+ const selected = resolveSelectedValue(
1824
+ options,
1825
+ actionResult,
1826
+ selectionEvent
1827
+ );
1828
+ if (selected) {
1829
+ set(selected);
1830
+ }
1831
+
1832
+ return actionResult;
1833
+ },
1834
+ },
1835
+ ];
1836
+ }
1837
+ )
1838
+ );
1839
+
1840
+ const omnibox = document.createElement("pds-omnibox");
1841
+ omnibox.id = id;
1842
+ omnibox.setAttribute("name", path);
1843
+ omnibox.setAttribute("item-grid", "0 1fr 0");
1844
+ omnibox.setAttribute(
1845
+ "placeholder",
1846
+ attrs?.placeholder || "Select a font family"
1847
+ );
1848
+ omnibox.value = value ?? "";
1849
+ omnibox.settings = {
1850
+ ...fontFamilyOmniboxSettings,
1851
+ categories,
1852
+ };
1853
+ omnibox.addEventListener("input", (event) => {
1854
+ set(event?.target?.value ?? omnibox.value ?? "");
1855
+ });
1856
+ omnibox.addEventListener("change", (event) => {
1857
+ set(event?.target?.value ?? omnibox.value ?? "");
1858
+ });
1859
+ omnibox.addEventListener("result-selected", (event) => {
1860
+ const selected = resolveSelectedValue(event?.detail, undefined, event);
1861
+ if (!selected) return;
1862
+ omnibox.value = selected;
1863
+ set(selected);
1864
+ });
1865
+ return omnibox;
1207
1866
  }
1208
- });
1209
- setFormSchemas(form, schema, uiSchema, values);
1867
+ );
1210
1868
 
1211
- if (!customElements.get("pds-form")) {
1212
- customElements.whenDefined("pds-form").then(() => {
1213
- setFormSchemas(form, schema, uiSchema, values);
1214
- });
1869
+ form.addEventListener("pw:submit", onSubmit);
1870
+ if (typeof normalizeFlatValues === "function") {
1871
+ form._normalizeFlatValues = normalizeFlatValues;
1215
1872
  }
1873
+ setFormSchemas(form, schema, uiSchema, values || {});
1216
1874
 
1217
- // Apply button (will trigger form submit programmatically)
1218
1875
  const applyBtn = document.createElement("button");
1219
1876
  applyBtn.className = "btn-primary btn-sm";
1220
1877
  applyBtn.type = "button";
1221
1878
  applyBtn.textContent = "Apply";
1222
1879
  applyBtn.addEventListener("click", async () => {
1223
- // Manually trigger pw:submit event for pds-form
1224
1880
  if (typeof form.getValuesFlat === "function") {
1225
- // Wait for form to be ready if it's still loading
1226
1881
  if (!customElements.get("pds-form")) {
1227
1882
  await customElements.whenDefined("pds-form");
1228
1883
  }
1229
-
1230
- const flatValues = form.getValuesFlat();
1884
+
1885
+ const flatValues =
1886
+ typeof form._normalizeFlatValues === "function"
1887
+ ? form._normalizeFlatValues(form.getValuesFlat())
1888
+ : form.getValuesFlat();
1231
1889
  const event = new CustomEvent("pw:submit", {
1232
1890
  detail: {
1233
1891
  json: flatValues,
1234
1892
  formData: new FormData(),
1235
1893
  valid: true,
1236
- issues: []
1894
+ issues: [],
1237
1895
  },
1238
1896
  bubbles: true,
1239
- cancelable: true
1897
+ cancelable: true,
1240
1898
  });
1241
1899
  form.dispatchEvent(event);
1242
1900
  }
1243
1901
  });
1244
1902
 
1245
- // Undo button
1246
1903
  const undoBtn = document.createElement("button");
1247
- undoBtn.className = "btn-secondary btn-sm";
1904
+ undoBtn.className = "btn-secondary btn-sm icon-only";
1248
1905
  undoBtn.type = "button";
1249
- undoBtn.textContent = "Undo";
1906
+ undoBtn.setAttribute("aria-label", "Undo");
1907
+ undoBtn.setAttribute("title", "Undo");
1908
+ const undoIcon = document.createElement("pds-icon");
1909
+ undoIcon.setAttribute("icon", "arrow-counter-clockwise");
1910
+ undoIcon.setAttribute("size", "sm");
1911
+ undoBtn.appendChild(undoIcon);
1250
1912
  undoBtn.addEventListener("click", onUndo);
1251
1913
 
1252
1914
  return { form, applyBtn, undoBtn };
1253
1915
  }
1254
1916
 
1917
+ async function buildForm(paths, design, onSubmit, onUndo, hints = {}) {
1918
+ const quickPayload = buildQuickConfigPayload(paths, design, hints);
1919
+ const schema = quickPayload?.schema;
1920
+ const uiSchema = quickPayload?.uiSchema;
1921
+ const values = quickPayload?.values;
1922
+
1923
+ if (!schema || !uiSchema) {
1924
+ throw new Error("Central config form metadata is unavailable for quick edit");
1925
+ }
1926
+
1927
+ return createConfiguredForm({
1928
+ schema,
1929
+ uiSchema,
1930
+ values: values || {},
1931
+ onSubmit,
1932
+ onUndo,
1933
+ formOptions: {
1934
+ layouts: {
1935
+ arrays: "compact",
1936
+ },
1937
+ enhancements: {
1938
+ rangeOutput: true,
1939
+ },
1940
+ },
1941
+ });
1942
+ }
1943
+
1944
+ function getConfigFormPayloadFromMetadata(design) {
1945
+ if (typeof PDS?.buildConfigFormSchema === "function") {
1946
+ return PDS.buildConfigFormSchema(design);
1947
+ }
1948
+
1949
+ const payload = PDS?.configFormSchema;
1950
+ if (payload && payload.schema && payload.uiSchema) {
1951
+ return {
1952
+ schema: payload.schema,
1953
+ uiSchema: payload.uiSchema,
1954
+ values: shallowClone(design || payload.values || {}),
1955
+ metadata: payload.metadata || {},
1956
+ };
1957
+ }
1958
+
1959
+ return null;
1960
+ }
1961
+
1962
+ function deepClone(value) {
1963
+ if (typeof structuredClone === "function") {
1964
+ try {
1965
+ return structuredClone(value);
1966
+ } catch (e) {
1967
+ // Fall through to JSON clone
1968
+ }
1969
+ }
1970
+ return JSON.parse(JSON.stringify(value));
1971
+ }
1972
+
1973
+ function shouldKeepPathForSelection(selectedPaths, path) {
1974
+ if (!path) return true;
1975
+ return selectedPaths.some((selectedPath) => {
1976
+ if (selectedPath === path) return true;
1977
+ return selectedPath.startsWith(`${path}.`);
1978
+ });
1979
+ }
1980
+
1981
+ function pruneSchemaForPaths(node, selectedPaths, path = "") {
1982
+ if (!node || typeof node !== "object") return node;
1983
+ if (!isObjectSchemaNode(node)) return deepClone(node);
1984
+
1985
+ if (path && !shouldKeepPathForSelection(selectedPaths, path)) {
1986
+ return null;
1987
+ }
1988
+
1989
+ const properties = {};
1990
+ Object.entries(node.properties || {}).forEach(([key, childNode]) => {
1991
+ const childPath = path ? `${path}.${key}` : key;
1992
+ if (!shouldKeepPathForSelection(selectedPaths, childPath)) return;
1993
+ const prunedChild = pruneSchemaForPaths(childNode, selectedPaths, childPath);
1994
+ if (prunedChild) {
1995
+ properties[key] = prunedChild;
1996
+ }
1997
+ });
1998
+
1999
+ if (!Object.keys(properties).length) return null;
2000
+
2001
+ const clonedNode = deepClone(node);
2002
+ clonedNode.properties = properties;
2003
+ if (Array.isArray(clonedNode.required)) {
2004
+ clonedNode.required = clonedNode.required.filter((key) =>
2005
+ Object.prototype.hasOwnProperty.call(properties, key)
2006
+ );
2007
+ }
2008
+ return clonedNode;
2009
+ }
2010
+
2011
+ function uiPointerToPath(pointer) {
2012
+ if (!pointer || pointer === "/") return "";
2013
+ return pointer
2014
+ .replace(/^\//, "")
2015
+ .split("/")
2016
+ .filter(Boolean)
2017
+ .join(".");
2018
+ }
2019
+
2020
+ function filterUiSchemaForPaths(uiSchema, selectedPaths) {
2021
+ if (!uiSchema || typeof uiSchema !== "object") return {};
2022
+ const filtered = {};
2023
+ Object.entries(uiSchema).forEach(([pointer, value]) => {
2024
+ const path = uiPointerToPath(pointer);
2025
+ if (!path || shouldKeepPathForSelection(selectedPaths, path)) {
2026
+ filtered[pointer] = deepClone(value);
2027
+ }
2028
+ });
2029
+ return filtered;
2030
+ }
2031
+
2032
+ function buildValuesForPaths(valuesSource, selectedPaths, hints = {}) {
2033
+ const values = {};
2034
+ selectedPaths.forEach((path) => {
2035
+ const segments = path.split(".");
2036
+ let value = getValueAtPath(valuesSource, segments);
2037
+ if ((value === undefined || value === null) && hints[path] !== undefined) {
2038
+ value = hints[path];
2039
+ }
2040
+ if (isColorPath(path)) {
2041
+ const resolvedColorValue = resolveColorValueForPath(path, value, hints[path]);
2042
+ if (resolvedColorValue) {
2043
+ value = resolvedColorValue;
2044
+ }
2045
+ }
2046
+ if (value !== undefined) {
2047
+ setValueAtPath(values, segments, deepClone(value));
2048
+ }
2049
+ });
2050
+ return values;
2051
+ }
2052
+
2053
+ function buildQuickConfigPayload(paths, design, hints = {}) {
2054
+ const payload = getConfigFormPayloadFromMetadata(design);
2055
+ if (!payload?.schema || !payload?.uiSchema) return null;
2056
+
2057
+ const selectedPaths = normalizePaths(paths);
2058
+ if (!selectedPaths.length) return null;
2059
+
2060
+ const schema = pruneSchemaForPaths(payload.schema, selectedPaths, "");
2061
+ if (!schema) return null;
2062
+
2063
+ const uiSchema = filterUiSchemaForPaths(payload.uiSchema, selectedPaths);
2064
+ const valuesSource =
2065
+ payload?.values && typeof payload.values === "object"
2066
+ ? payload.values
2067
+ : shallowClone(design || {});
2068
+ const values = buildValuesForPaths(valuesSource || {}, selectedPaths, hints);
2069
+
2070
+ return { schema, uiSchema, values };
2071
+ }
2072
+
2073
+ const FULL_CONFIG_GROUPS_KEY = "__groups";
2074
+
2075
+ function isObjectSchemaNode(node) {
2076
+ return !!(node && typeof node === "object" && node.type === "object" && node.properties);
2077
+ }
2078
+
2079
+ function buildGroupedFullConfigPayload(payload, design) {
2080
+ const values =
2081
+ payload?.values && typeof payload.values === "object"
2082
+ ? payload.values
2083
+ : shallowClone(design || {});
2084
+
2085
+ if (!payload?.schema || !payload?.uiSchema || !isObjectSchemaNode(payload.schema)) {
2086
+ return {
2087
+ schema: payload?.schema,
2088
+ uiSchema: payload?.uiSchema,
2089
+ values,
2090
+ normalizeFlatValues: null,
2091
+ };
2092
+ }
2093
+
2094
+ const rootProperties = payload.schema.properties || {};
2095
+ const groupedKeys = [];
2096
+ const scalarKeys = [];
2097
+
2098
+ Object.entries(rootProperties).forEach(([key, schemaNode]) => {
2099
+ if (isObjectSchemaNode(schemaNode)) {
2100
+ groupedKeys.push(key);
2101
+ return;
2102
+ }
2103
+ scalarKeys.push(key);
2104
+ });
2105
+
2106
+ if (!groupedKeys.length || !scalarKeys.length) {
2107
+ return {
2108
+ schema: payload.schema,
2109
+ uiSchema: payload.uiSchema,
2110
+ values,
2111
+ normalizeFlatValues: null,
2112
+ };
2113
+ }
2114
+
2115
+ const transformedSchema = {
2116
+ ...payload.schema,
2117
+ properties: {
2118
+ ...Object.fromEntries(scalarKeys.map((key) => [key, rootProperties[key]])),
2119
+ [FULL_CONFIG_GROUPS_KEY]: {
2120
+ type: "object",
2121
+ title: "Design Groups",
2122
+ properties: Object.fromEntries(
2123
+ groupedKeys.map((key) => [key, rootProperties[key]])
2124
+ ),
2125
+ },
2126
+ },
2127
+ };
2128
+
2129
+ const transformedValues = {
2130
+ ...Object.fromEntries(scalarKeys.map((key) => [key, values?.[key]])),
2131
+ [FULL_CONFIG_GROUPS_KEY]: Object.fromEntries(
2132
+ groupedKeys.map((key) => [key, values?.[key]])
2133
+ ),
2134
+ };
2135
+
2136
+ const transformedUiSchema = { ...(payload.uiSchema || {}) };
2137
+ const addGroupPrefix = (path = "") => `/${FULL_CONFIG_GROUPS_KEY}${path}`;
2138
+
2139
+ groupedKeys.forEach((key) => {
2140
+ const originalPath = `/${key}`;
2141
+
2142
+ if (Object.prototype.hasOwnProperty.call(transformedUiSchema, originalPath)) {
2143
+ transformedUiSchema[addGroupPrefix(originalPath)] = transformedUiSchema[originalPath];
2144
+ delete transformedUiSchema[originalPath];
2145
+ }
2146
+
2147
+ Object.keys(transformedUiSchema).forEach((path) => {
2148
+ if (!path.startsWith(`${originalPath}/`)) return;
2149
+ transformedUiSchema[addGroupPrefix(path)] = transformedUiSchema[path];
2150
+ delete transformedUiSchema[path];
2151
+ });
2152
+ });
2153
+
2154
+ transformedUiSchema[`/${FULL_CONFIG_GROUPS_KEY}`] = {
2155
+ "ui:layout": "accordion",
2156
+ "ui:layoutOptions": { openFirst: false },
2157
+ };
2158
+
2159
+ const normalizeFlatValues = (flatValues = {}) => {
2160
+ const normalized = {};
2161
+ const groupPointerPrefix = `/${FULL_CONFIG_GROUPS_KEY}/`;
2162
+ const groupDotPrefix = `${FULL_CONFIG_GROUPS_KEY}.`;
2163
+ Object.entries(flatValues || {}).forEach(([path, value]) => {
2164
+ const inputPath = String(path || "");
2165
+ if (!inputPath) return;
2166
+ if (inputPath === FULL_CONFIG_GROUPS_KEY || inputPath === `/${FULL_CONFIG_GROUPS_KEY}`) {
2167
+ return;
2168
+ }
2169
+
2170
+ if (inputPath.startsWith(groupPointerPrefix)) {
2171
+ normalized[`/${inputPath.slice(groupPointerPrefix.length)}`] = value;
2172
+ return;
2173
+ }
2174
+
2175
+ if (inputPath.startsWith(groupDotPrefix)) {
2176
+ normalized[inputPath.slice(groupDotPrefix.length)] = value;
2177
+ return;
2178
+ }
2179
+
2180
+ normalized[inputPath] = value;
2181
+ });
2182
+ return normalized;
2183
+ };
2184
+
2185
+ return {
2186
+ schema: transformedSchema,
2187
+ uiSchema: transformedUiSchema,
2188
+ values: transformedValues,
2189
+ normalizeFlatValues,
2190
+ };
2191
+ }
2192
+
2193
+ async function buildFullConfigForm(design, onSubmit, onUndo) {
2194
+ const payload = getConfigFormPayloadFromMetadata(design);
2195
+ if (!payload?.schema || !payload?.uiSchema) return null;
2196
+
2197
+ const groupedPayload = buildGroupedFullConfigPayload(payload, design);
2198
+
2199
+ return createConfiguredForm({
2200
+ schema: groupedPayload.schema,
2201
+ uiSchema: groupedPayload.uiSchema,
2202
+ values: groupedPayload.values,
2203
+ onSubmit,
2204
+ onUndo,
2205
+ normalizeFlatValues: groupedPayload.normalizeFlatValues,
2206
+ formOptions: {
2207
+ layouts: {
2208
+ arrays: "compact",
2209
+ },
2210
+ enhancements: {
2211
+ rangeOutput: true,
2212
+ },
2213
+ },
2214
+ });
2215
+ }
2216
+
1255
2217
  class PdsLiveEdit extends HTMLElement {
1256
2218
  constructor() {
1257
2219
  super();
@@ -1669,7 +2631,7 @@ class PdsLiveEdit extends HTMLElement {
1669
2631
  button.appendChild(icon);
1670
2632
 
1671
2633
  const menu = document.createElement("menu");
1672
- const quickItem = document.createElement("li");
2634
+ const quickItem = document.createElement("div");
1673
2635
  quickItem.className = "pds-live-editor-menu";
1674
2636
 
1675
2637
  const header = document.createElement("div");
@@ -1715,13 +2677,28 @@ class PdsLiveEdit extends HTMLElement {
1715
2677
  container.replaceChildren();
1716
2678
  footer.replaceChildren();
1717
2679
 
1718
- const { form, applyBtn, undoBtn } = await buildForm(
1719
- paths,
1720
- design,
1721
- (event) => this._handleFormSubmit(event, form),
1722
- () => this._handleUndo(),
1723
- hints
1724
- );
2680
+ let form;
2681
+ let applyBtn;
2682
+ let undoBtn;
2683
+ try {
2684
+ const result = await buildForm(
2685
+ paths,
2686
+ design,
2687
+ (event) => this._handleFormSubmit(event, form),
2688
+ () => this._handleUndo(),
2689
+ hints
2690
+ );
2691
+ form = result.form;
2692
+ applyBtn = result.applyBtn;
2693
+ undoBtn = result.undoBtn;
2694
+ } catch (error) {
2695
+ const fallback = document.createElement("p");
2696
+ fallback.className = "text-muted";
2697
+ fallback.textContent = "Editor form unavailable. Lazy component definition did not complete in time.";
2698
+ container.appendChild(fallback);
2699
+ console.warn("[PDS Live Edit] Failed to render quick form:", error);
2700
+ return;
2701
+ }
1725
2702
 
1726
2703
  // Store reference to undo button for enabling/disabling
1727
2704
  form._undoBtn = undoBtn;
@@ -1789,26 +2766,72 @@ class PdsLiveEdit extends HTMLElement {
1789
2766
  presetText.textContent = "Choose a base style";
1790
2767
  presetLabel.appendChild(presetText);
1791
2768
 
1792
- const presetSelect = document.createElement("select");
1793
- const presetOptions = getPresetOptions();
1794
- const activePreset = getActivePresetId();
2769
+ let presetControlRendered = false;
2770
+ try {
2771
+ await waitForElementDefinition("pds-omnibox");
2772
+
2773
+ const presetOmnibox = document.createElement("pds-omnibox");
2774
+ presetOmnibox.setAttribute("item-grid", "0 1fr 0");
2775
+ presetOmnibox.setAttribute("placeholder", "Search presets...");
1795
2776
 
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;
2777
+ const activePresetId = getActivePresetId();
2778
+ const activePresetName = getPresetNameById(activePresetId);
2779
+ if (activePresetName) {
2780
+ presetOmnibox.value = activePresetName;
1802
2781
  }
1803
- presetSelect.appendChild(option);
1804
- });
1805
2782
 
1806
- presetSelect.addEventListener("change", async (event) => {
1807
- const nextPreset = event.target?.value;
1808
- await applyPresetSelection(nextPreset);
1809
- });
2783
+ const omniboxSettingsBuilder =
2784
+ typeof PDS?.buildPresetOmniboxSettings === "function"
2785
+ ? PDS.buildPresetOmniboxSettings.bind(PDS)
2786
+ : null;
2787
+
2788
+ if (omniboxSettingsBuilder) {
2789
+ presetOmnibox.settings = omniboxSettingsBuilder({
2790
+ onSelect: async ({ preset, selection }) => {
2791
+ if (selection?.disabled) return selection?.id;
2792
+ const presetId = preset?.id || selection?.id;
2793
+ await applyPresetSelection(presetId);
2794
+ return presetId;
2795
+ },
2796
+ });
2797
+ }
2798
+
2799
+ presetOmnibox.addEventListener("result-selected", (event) => {
2800
+ const selectedText = event?.detail?.text;
2801
+ if (typeof selectedText === "string" && selectedText.trim()) {
2802
+ presetOmnibox.value = selectedText;
2803
+ }
2804
+ });
2805
+
2806
+ presetLabel.appendChild(presetOmnibox);
2807
+ presetControlRendered = true;
2808
+ } catch (error) {
2809
+ console.warn("[PDS Live Edit] Preset omnibox unavailable, falling back to select.", error);
2810
+ }
2811
+
2812
+ if (!presetControlRendered) {
2813
+ const presetSelect = document.createElement("select");
2814
+ const presetOptions = getPresetOptions();
2815
+ const activePreset = getActivePresetId();
2816
+
2817
+ presetOptions.forEach((preset) => {
2818
+ const option = document.createElement("option");
2819
+ option.value = preset.id;
2820
+ option.textContent = preset.name;
2821
+ if (String(preset.id) === String(activePreset)) {
2822
+ option.selected = true;
2823
+ }
2824
+ presetSelect.appendChild(option);
2825
+ });
2826
+
2827
+ presetSelect.addEventListener("change", async (event) => {
2828
+ const nextPreset = event.target?.value;
2829
+ await applyPresetSelection(nextPreset);
2830
+ });
2831
+
2832
+ presetLabel.appendChild(presetSelect);
2833
+ }
1810
2834
 
1811
- presetLabel.appendChild(presetSelect);
1812
2835
  presetCard.appendChild(presetLabel);
1813
2836
 
1814
2837
  const themeCard = document.createElement("section");
@@ -1821,53 +2844,129 @@ class PdsLiveEdit extends HTMLElement {
1821
2844
  const themeToggle = document.createElement("pds-theme");
1822
2845
  themeCard.appendChild(themeToggle);
1823
2846
 
1824
- const searchCard = document.createElement("section");
1825
- searchCard.className = "card surface-elevated stack-sm";
2847
+ const configCard = document.createElement("section");
2848
+ configCard.className = "card surface-elevated stack-sm";
1826
2849
 
1827
- const searchTitle = document.createElement("h4");
1828
- searchTitle.textContent = "Search PDS";
1829
- searchCard.appendChild(searchTitle);
2850
+ const configTitle = document.createElement("h4");
2851
+ configTitle.textContent = "Configuration";
2852
+ configCard.appendChild(configTitle);
1830
2853
 
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);
2854
+ const configDescription = document.createElement("p");
2855
+ configDescription.className = "text-muted";
2856
+ configDescription.textContent =
2857
+ "Edit the full design config generated from PDS metadata.";
2858
+ configCard.appendChild(configDescription);
2859
+
2860
+ const configFormContainer = document.createElement("div");
2861
+ configFormContainer.className = "stack-sm";
2862
+ configCard.appendChild(configFormContainer);
2863
+
2864
+ const configFooter = document.createElement("div");
2865
+ configFooter.className = "flex gap-sm";
2866
+ configCard.appendChild(configFooter);
2867
+
2868
+ const fullDesign = shallowClone(PDS?.currentConfig?.design || {});
2869
+ const fullConfigFormResult = await buildFullConfigForm(
2870
+ fullDesign,
2871
+ (event) => this._handleFormSubmit(event, fullConfigFormResult?.form),
2872
+ () => this._handleUndo()
2873
+ );
2874
+
2875
+ if (fullConfigFormResult?.form) {
2876
+ fullConfigFormResult.form._undoBtn = fullConfigFormResult.undoBtn;
2877
+ fullConfigFormResult.undoBtn.disabled = this._undoStack.length === 0;
2878
+ configFormContainer.appendChild(fullConfigFormResult.form);
2879
+ configFooter.appendChild(fullConfigFormResult.applyBtn);
2880
+ configFooter.appendChild(fullConfigFormResult.undoBtn);
2881
+ } else {
2882
+ const unavailable = document.createElement("p");
2883
+ unavailable.className = "text-muted";
2884
+ unavailable.textContent =
2885
+ "Full config metadata is unavailable in this runtime.";
2886
+ configFormContainer.appendChild(unavailable);
2887
+ }
2888
+
2889
+ const exportCard = document.createElement("section");
2890
+ exportCard.className = "card surface-elevated stack-sm";
2891
+
2892
+ const exportTitle = document.createElement("h4");
2893
+ exportTitle.textContent = "Export";
2894
+ exportCard.appendChild(exportTitle);
2895
+
2896
+ const exportNav = document.createElement("nav");
2897
+ exportNav.setAttribute("data-dropdown", "");
2898
+ exportNav.setAttribute("data-mode", "auto");
2899
+
2900
+ const exportButton = document.createElement("button");
2901
+ exportButton.className = "btn-primary";
2902
+ const exportIcon = document.createElement("pds-icon");
2903
+ exportIcon.setAttribute("icon", "download");
2904
+ exportIcon.setAttribute("size", "sm");
2905
+ const exportLabel = document.createElement("span");
2906
+ exportLabel.textContent = "Download";
2907
+ const exportCaret = document.createElement("pds-icon");
2908
+ exportCaret.setAttribute("icon", "caret-down");
2909
+ exportCaret.setAttribute("size", "sm");
2910
+ exportButton.append(exportIcon, exportLabel, exportCaret);
2911
+
2912
+ const exportMenu = document.createElement("menu");
2913
+
2914
+ const configItem = document.createElement("li");
2915
+ const configLink = document.createElement("a");
2916
+ configLink.href = "#";
2917
+ configLink.addEventListener("click", async (event) => {
2918
+ event.preventDefault();
2919
+ await this._handleExport("config");
2920
+ });
2921
+ const configIcon = document.createElement("pds-icon");
2922
+ configIcon.setAttribute("icon", "file-js");
2923
+ configIcon.setAttribute("size", "sm");
2924
+ const configLabel = document.createElement("span");
2925
+ configLabel.textContent = "Config File";
2926
+ configLink.append(configIcon, configLabel);
2927
+ configItem.appendChild(configLink);
2928
+
2929
+ const figmaItem = document.createElement("li");
2930
+ const figmaLink = document.createElement("a");
2931
+ figmaLink.href = "#";
2932
+ figmaLink.addEventListener("click", async (event) => {
2933
+ event.preventDefault();
2934
+ await this._handleExport("figma");
2935
+ });
2936
+ const figmaIcon = document.createElement("pds-icon");
2937
+ figmaIcon.setAttribute("icon", "brackets-curly");
2938
+ figmaIcon.setAttribute("size", "sm");
2939
+ const figmaLabel = document.createElement("span");
2940
+ figmaLabel.textContent = "Figma Tokens (JSON)";
2941
+ figmaLink.append(figmaIcon, figmaLabel);
2942
+ figmaItem.appendChild(figmaLink);
2943
+
2944
+ exportMenu.append(configItem, figmaItem);
2945
+ exportNav.append(exportButton, exportMenu);
2946
+ exportCard.appendChild(exportNav);
2947
+
2948
+ const resetCard = document.createElement("section");
2949
+ resetCard.className = "card surface-elevated stack-sm";
2950
+
2951
+ const resetTitle = document.createElement("h4");
2952
+ resetTitle.textContent = "Reset";
2953
+ resetCard.appendChild(resetTitle);
2954
+
2955
+ const resetButton = document.createElement("button");
2956
+ resetButton.type = "button";
2957
+ resetButton.className = "btn-outline";
2958
+ resetButton.textContent = "Reset Config";
2959
+ resetButton.addEventListener("click", () => {
2960
+ window.localStorage.removeItem("pure-ds-config");
2961
+ window.location.reload();
2962
+ });
2963
+ resetCard.appendChild(resetButton);
1867
2964
 
1868
2965
  content.appendChild(presetCard);
1869
2966
  content.appendChild(themeCard);
1870
- content.appendChild(searchCard);
2967
+ content.appendChild(configCard);
2968
+ content.appendChild(exportCard);
2969
+ content.appendChild(resetCard);
1871
2970
 
1872
2971
  this._drawer.replaceChildren(header, content);
1873
2972
 
@@ -1878,6 +2977,10 @@ class PdsLiveEdit extends HTMLElement {
1878
2977
  }
1879
2978
  }
1880
2979
 
2980
+ async _handleExport(format) {
2981
+ await exportFromLiveEdit(format);
2982
+ }
2983
+
1881
2984
  async _handleFormSubmit(event, form) {
1882
2985
  if (!form || typeof form.getValuesFlat !== "function") return;
1883
2986
 
@@ -1896,7 +2999,14 @@ class PdsLiveEdit extends HTMLElement {
1896
2999
  }
1897
3000
 
1898
3001
  // Apply the changes
1899
- const flatValues = form.getValuesFlat();
3002
+ const eventJson = event?.detail?.json;
3003
+ const hasEventPayload =
3004
+ eventJson && typeof eventJson === "object" && Object.keys(eventJson).length > 0;
3005
+ const flatValues = hasEventPayload
3006
+ ? eventJson
3007
+ : typeof form._normalizeFlatValues === "function"
3008
+ ? form._normalizeFlatValues(form.getValuesFlat())
3009
+ : form.getValuesFlat();
1900
3010
  const patch = {};
1901
3011
  Object.entries(flatValues || {}).forEach(([path, value]) => {
1902
3012
  setValueAtJsonPath(patch, path, value);