@pure-ds/core 0.5.61 → 0.6.1

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 (43) hide show
  1. package/dist/types/packages/pds-configurator/src/pds-home-content.d.ts +375 -0
  2. package/dist/types/packages/pds-configurator/src/pds-home-content.d.ts.map +1 -0
  3. package/dist/types/packages/pds-configurator/src/pds-home.d.ts +2 -0
  4. package/dist/types/packages/pds-configurator/src/pds-home.d.ts.map +1 -0
  5. package/dist/types/pds.config.d.ts +2 -2
  6. package/dist/types/pds.config.d.ts.map +1 -1
  7. package/dist/types/pds.d.ts +3 -0
  8. package/dist/types/public/assets/js/pds-manager.d.ts +144 -429
  9. package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
  10. package/dist/types/public/assets/js/pds.d.ts +3 -4
  11. package/dist/types/public/assets/js/pds.d.ts.map +1 -1
  12. package/dist/types/public/assets/pds/components/pds-live-edit.d.ts +150 -0
  13. package/dist/types/public/assets/pds/components/pds-live-edit.d.ts.map +1 -0
  14. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts +2 -0
  15. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts.map +1 -1
  16. package/dist/types/public/assets/pds/components/pds-richtext.d.ts.map +1 -1
  17. package/dist/types/public/assets/pds/components/pds-theme.d.ts +5 -0
  18. package/dist/types/public/assets/pds/components/pds-theme.d.ts.map +1 -1
  19. package/dist/types/src/js/pds-core/pds-config.d.ts +3 -0
  20. package/dist/types/src/js/pds-core/pds-config.d.ts.map +1 -1
  21. package/dist/types/src/js/pds-core/pds-enhancers.d.ts.map +1 -1
  22. package/dist/types/src/js/pds-core/pds-live.d.ts.map +1 -1
  23. package/dist/types/src/js/pds-core/pds-ontology.d.ts.map +1 -1
  24. package/dist/types/src/js/pds-core/pds-theme-utils.d.ts +6 -0
  25. package/dist/types/src/js/pds-core/pds-theme-utils.d.ts.map +1 -0
  26. package/dist/types/src/js/pds.d.ts.map +1 -1
  27. package/package.json +1 -4
  28. package/packages/pds-cli/bin/templates/bootstrap/pds.config.js +1 -1
  29. package/public/assets/js/app.js +106 -5636
  30. package/public/assets/js/pds-manager.js +137 -137
  31. package/public/assets/js/pds.js +7 -7
  32. package/public/assets/pds/components/pds-live-edit.js +1555 -0
  33. package/public/assets/pds/components/pds-omnibox.js +558 -369
  34. package/public/assets/pds/components/pds-richtext.js +57 -7
  35. package/public/assets/pds/components/pds-theme.js +58 -0
  36. package/readme.md +2 -2
  37. package/src/js/pds-core/pds-config.js +21 -3
  38. package/src/js/pds-core/pds-enhancers.js +61 -4
  39. package/src/js/pds-core/pds-live.js +180 -1
  40. package/src/js/pds-core/pds-ontology.js +8 -0
  41. package/src/js/pds-core/pds-theme-utils.js +33 -0
  42. package/src/js/pds.d.ts +3 -0
  43. package/src/js/pds.js +22 -0
@@ -341,7 +341,13 @@ export class RichText extends HTMLElement {
341
341
  async #adoptStyles() {
342
342
  // Component stylesheet (tokens + semantic vars)
343
343
  const componentStyles = PDS.createStylesheet(/*css*/ `@layer richtext {
344
- :host { display:block; color: var(--rt-fg, var(--color-text-primary)); font: var(--font-body-sm, 14px/1.35 system-ui,-apple-system,Segoe UI,Roboto,sans-serif); }
344
+ :host {
345
+ display: block;
346
+ color: var(--rt-fg, var(--color-text-primary));
347
+ font-family: var(--rt-font-family, var(--font-family-body));
348
+ font-size: var(--rt-font-size, var(--font-size-base));
349
+ line-height: var(--rt-line-height, var(--font-line-height-normal));
350
+ }
345
351
  :host([disabled]) { opacity: .6; pointer-events: none; }
346
352
  .box { border: 1px solid var(--rt-border, var(--color-border, currentColor)); border-radius: var(--radius-md,8px); background: var(--rt-bg, var(--color-input-bg)); }
347
353
  .box:focus-within {
@@ -353,11 +359,15 @@ export class RichText extends HTMLElement {
353
359
  }
354
360
  }
355
361
 
356
- .toolbar {background-color: var(--surface-subtle-bg); display:flex; gap: var(--spacing-2,10px); align-items:center; border-bottom: 1px solid var(--rt-border, var(--color-border-muted)); border-radius: var(--radius-md,8px) var(--radius-md,8px) 0 0; }
357
- .tbtn { transition: none; display:inline-flex; align-items:center; justify-content:center; width:22px; height:22px; border-radius: var(--radius-sm,6px); cursor:pointer; user-select:none; color: inherit; background: transparent; border:none; }
362
+ .toolbar { background-color: var(--surface-subtle-bg); display: flex; gap: var(--spacing-0, 2px); align-items: center; padding: var(--spacing-0, 2px); border-bottom: 1px solid var(--rt-border, var(--color-border-muted)); border-radius: var(--radius-md,8px) var(--radius-md,8px) 0 0; width: 100%; max-width: 100%; flex-wrap: nowrap; box-sizing: border-box; overflow: hidden; }
363
+ .tbtn { transition: none; display:inline-flex; align-items:center; justify-content:center; width:22px; height:22px; border-radius: var(--radius-sm,6px); cursor:pointer; user-select:none; color: inherit; background: transparent; border:none;
364
+
365
+ padding: var(--spacing-0) var(--spacing-6, 8px);
366
+
367
+ }
358
368
  .tbtn:hover { background: var(--color-surface-hover, color-mix(in oklab, CanvasText 12%, transparent)); }
359
369
  .edwrap { position:relative; }
360
- .ed { display: block; min-height:90px; max-height: 400px; overflow:auto; padding: var(--spacing-1, 0) var(--spacing-2, 0); outline:none; word-break:break-word; border-radius: 0 0 var(--radius-md,8px) var(--radius-md,8px); background: var(--rt-editor-bg, var(--color-input-bg)); }
370
+ .ed { display: block; font-weight: normal; min-height:90px; max-height: 400px; overflow:auto; padding: var(--spacing-1, 0) var(--spacing-2, 0); outline:none; word-break:break-word; border-radius: 0 0 var(--radius-md,8px) var(--radius-md,8px); background: var(--rt-editor-bg, var(--color-input-bg)); }
361
371
  .ed[contenteditable="true"]:empty::before { content: attr(data-ph); color: var(--rt-muted, var(--color-text-muted)); pointer-events:none; }
362
372
  .send { margin-left:auto; display:inline-flex; gap: var(--spacing-2,8px); align-items:center; }
363
373
  button.icon { background:transparent; border:0; color:inherit; cursor:pointer; padding:6px; border-radius: var(--radius-sm,6px); }
@@ -589,9 +599,28 @@ export class RichText extends HTMLElement {
589
599
 
590
600
  #toggleCode() {
591
601
  // Wrap selection with <code> or unwrap if already in code
592
- const sel = window.getSelection();
602
+ const root = this.shadowRoot;
603
+ const getSel = () =>
604
+ root && typeof root.getSelection === "function"
605
+ ? root.getSelection()
606
+ : window.getSelection();
607
+ let sel = getSel();
593
608
  if (!sel || sel.rangeCount === 0) return;
594
- const range = sel.getRangeAt(0);
609
+ let range = sel.getRangeAt(0);
610
+ const inEditor = (r) => {
611
+ const node =
612
+ r.commonAncestorContainer.nodeType === 1
613
+ ? r.commonAncestorContainer
614
+ : r.commonAncestorContainer.parentNode;
615
+ return !!(node && this.#editorDiv && this.#editorDiv.contains(node));
616
+ };
617
+ if (!inEditor(range)) {
618
+ this.#focus();
619
+ sel = getSel();
620
+ if (!sel || sel.rangeCount === 0) return;
621
+ range = sel.getRangeAt(0);
622
+ if (!inEditor(range)) return;
623
+ }
595
624
  // Simple toggle: if ancestor <code>, unwrap; else wrap
596
625
  const codeAncestor = this.#closestAncestor(
597
626
  range.commonAncestorContainer,
@@ -604,7 +633,28 @@ export class RichText extends HTMLElement {
604
633
  parent.removeChild(codeAncestor);
605
634
  } else {
606
635
  const wrapper = document.createElement("code");
607
- range.surroundContents(wrapper);
636
+ if (range.collapsed) {
637
+ const caret = document.createTextNode("\u200B");
638
+ wrapper.appendChild(caret);
639
+ range.insertNode(wrapper);
640
+ const next = document.createRange();
641
+ next.setStart(caret, 1);
642
+ next.collapse(true);
643
+ sel.removeAllRanges();
644
+ sel.addRange(next);
645
+ } else {
646
+ try {
647
+ range.surroundContents(wrapper);
648
+ } catch {
649
+ const contents = range.extractContents();
650
+ wrapper.appendChild(contents);
651
+ range.insertNode(wrapper);
652
+ }
653
+ const next = document.createRange();
654
+ next.selectNodeContents(wrapper);
655
+ sel.removeAllRanges();
656
+ sel.addRange(next);
657
+ }
608
658
  }
609
659
  }
610
660
 
@@ -13,6 +13,39 @@ const THEME_OPTIONS = [
13
13
 
14
14
  const DEFAULT_LABEL = "Theme";
15
15
  const LAYERS = ["tokens", "primitives", "components", "utilities"];
16
+ const DEFAULT_THEMES = ["light", "dark"];
17
+ const VALID_THEMES = new Set(DEFAULT_THEMES);
18
+
19
+ const normalizePresetThemes = (preset) => {
20
+ const themes = Array.isArray(preset?.themes)
21
+ ? preset.themes.map((theme) => String(theme).toLowerCase())
22
+ : DEFAULT_THEMES;
23
+ const normalized = themes.filter((theme) => VALID_THEMES.has(theme));
24
+ return normalized.length ? normalized : DEFAULT_THEMES;
25
+ };
26
+
27
+ const resolveThemePreference = (preference) => {
28
+ const normalized = String(preference || "").toLowerCase();
29
+ if (VALID_THEMES.has(normalized)) return normalized;
30
+
31
+ if (typeof document !== "undefined") {
32
+ const applied = document.documentElement?.getAttribute("data-theme");
33
+ if (VALID_THEMES.has(applied)) return applied;
34
+ }
35
+
36
+ if (typeof window !== "undefined" && window.matchMedia) {
37
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
38
+ return prefersDark ? "dark" : "light";
39
+ }
40
+
41
+ return "light";
42
+ };
43
+
44
+ const isPresetThemeCompatible = (preset, themePreference) => {
45
+ const resolvedTheme = resolveThemePreference(themePreference);
46
+ const themes = normalizePresetThemes(preset);
47
+ return themes.includes(resolvedTheme);
48
+ };
16
49
 
17
50
  class PdsTheme extends HTMLElement {
18
51
  static get observedAttributes() {
@@ -123,6 +156,21 @@ class PdsTheme extends HTMLElement {
123
156
  return;
124
157
  }
125
158
 
159
+ const currentPreset = PDS.currentConfig?.design || null;
160
+ if (currentPreset && !isPresetThemeCompatible(currentPreset, value)) {
161
+ const resolvedTheme = resolveThemePreference(value);
162
+ const presetName =
163
+ currentPreset?.name ||
164
+ PDS.currentPreset?.name ||
165
+ PDS.currentConfig?.preset ||
166
+ "current preset";
167
+ console.warn(
168
+ `PDS theme "${resolvedTheme}" not supported by preset "${presetName}".`
169
+ );
170
+ this.#syncCheckedState();
171
+ return;
172
+ }
173
+
126
174
  if (PDS.theme !== value) {
127
175
  PDS.theme = value;
128
176
  }
@@ -157,10 +205,20 @@ class PdsTheme extends HTMLElement {
157
205
 
158
206
  #syncCheckedState() {
159
207
  const currentTheme = PDS.theme || "system";
208
+ const currentPreset = PDS.currentConfig?.design || null;
209
+ const supportedThemes = normalizePresetThemes(currentPreset);
160
210
  this.shadowRoot
161
211
  .querySelectorAll('input[type="radio"]')
162
212
  .forEach((radio) => {
163
213
  radio.checked = radio.value === currentTheme;
214
+ if (radio.value === "system") {
215
+ radio.disabled = false;
216
+ } else if (currentPreset) {
217
+ const resolved = resolveThemePreference(radio.value);
218
+ radio.disabled = !supportedThemes.includes(resolved);
219
+ } else {
220
+ radio.disabled = false;
221
+ }
164
222
  });
165
223
  }
166
224
  }
package/readme.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](#license)
7
7
  [![npm version](https://img.shields.io/npm/v/@pure-ds/core.svg)](https://www.npmjs.com/package/@pure-ds/core)
8
8
 
9
- ![Pure Design System logo](public/assets/img/logo.png)
9
+ ![Pure Design System logo](public/assets/img/pds-logo.svg)
10
10
 
11
11
  ## With Great Standards Comes Great Power
12
12
 
@@ -571,7 +571,7 @@ PDS.dispatchEvent(new CustomEvent('pds:toast', {
571
571
  <script type="importmap">
572
572
  {
573
573
  "imports": {
574
- "#showdown": "https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.mjs"
574
+ "#showdown": "https://cdn.jsdelivr.net/npm/showdown@2.1.0/+esm"
575
575
  }
576
576
  }
577
577
  </script>
@@ -187,6 +187,7 @@ import { enums } from "./pds-enums.js";
187
187
  * @property {PDSAutoDefineConfig} [autoDefine]
188
188
  * @property {string} [managerURL]
189
189
  * @property {any} [manager]
190
+ * @property {boolean} [liveEdit]
190
191
  * @property {any} [log]
191
192
  */
192
193
 
@@ -565,16 +566,31 @@ const __INIT_CONFIG_SPEC__ = {
565
566
  properties: {
566
567
  mode: { type: "string" },
567
568
  preset: { type: "string" },
568
- design: { type: "object" },
569
+ design: __DESIGN_CONFIG_SPEC__,
569
570
  enhancers: { type: ["object", "array"] },
570
571
  applyGlobalStyles: { type: "boolean" },
571
572
  manageTheme: { type: "boolean" },
572
573
  themeStorageKey: { type: "string" },
573
574
  preloadStyles: { type: "boolean" },
574
575
  criticalLayers: { type: "array", items: { type: "string" } },
575
- autoDefine: { type: "object" },
576
+ autoDefine: {
577
+ type: "object",
578
+ allowUnknown: false,
579
+ properties: {
580
+ predefine: { type: "array", items: { type: "string" } },
581
+ mapper: { type: __ANY_TYPE__ },
582
+ enhancers: { type: ["object", "array"] },
583
+ scanExisting: { type: "boolean" },
584
+ observeShadows: { type: "boolean" },
585
+ patchAttachShadow: { type: "boolean" },
586
+ debounceMs: { type: "number" },
587
+ onError: { type: __ANY_TYPE__ },
588
+ baseURL: { type: "string" },
589
+ },
590
+ },
576
591
  managerURL: { type: "string" },
577
592
  manager: { type: __ANY_TYPE__ },
593
+ liveEdit: { type: "boolean" },
578
594
  log: { type: __ANY_TYPE__ },
579
595
  },
580
596
  };
@@ -799,6 +815,7 @@ export const presets = {
799
815
  id: "paper-and-ink",
800
816
  name: "Paper & Ink",
801
817
  tags: ["app", "featured"],
818
+ themes: ["light"], // Not optimized for dark mode
802
819
  description: "Ultra-minimal design with focus on typography and whitespace",
803
820
  colors: {
804
821
  primary: "#171717",
@@ -1037,6 +1054,7 @@ export const presets = {
1037
1054
  "pastel-play": {
1038
1055
  id: "pastel-play",
1039
1056
  name: "Pastel Play",
1057
+ themes: ["light"], // Not optimized for dark mode due to pastel contrast challenges
1040
1058
  description:
1041
1059
  "Playful pastels with soft surfaces and friendly rounded shapes",
1042
1060
  colors: {
@@ -1081,7 +1099,7 @@ export const presets = {
1081
1099
  accent: "#06b6d4", // cyan signal
1082
1100
  background: "#f8fafc",
1083
1101
  darkMode: {
1084
- background: "#0b0b0b",
1102
+ background: "#0c0c0c",
1085
1103
  secondary: "#9ca3af",
1086
1104
  // Set a chromatic primary in dark mode to ensure both:
1087
1105
  // - outline/link contrast on dark surface, and
@@ -77,16 +77,55 @@ function enhanceDropdown(elem) {
77
77
  }
78
78
 
79
79
  const resolveDirection = () => {
80
- const mode = (elem.getAttribute("data-mode") || "auto").toLowerCase();
80
+ const mode = (
81
+ elem.getAttribute("data-direction") ||
82
+ elem.getAttribute("data-dropdown-direction") ||
83
+ elem.getAttribute("data-mode") ||
84
+ "auto"
85
+ ).toLowerCase();
81
86
  if (mode === "up" || mode === "down") return mode;
82
87
  const rect = elem.getBoundingClientRect();
88
+ const menuRect = menu?.getBoundingClientRect?.() || { height: 0 };
89
+ const menuHeight = Math.max(
90
+ menu?.offsetHeight || 0,
91
+ menu?.scrollHeight || 0,
92
+ menuRect.height || 0,
93
+ 200
94
+ );
83
95
  const spaceBelow = Math.max(0, window.innerHeight - rect.bottom);
84
96
  const spaceAbove = Math.max(0, rect.top);
97
+ if (spaceBelow >= menuHeight) return "down";
98
+ if (spaceAbove >= menuHeight) return "up";
85
99
  return spaceAbove > spaceBelow ? "up" : "down";
86
100
  };
87
101
 
102
+ const resolveAlign = () => {
103
+ const align = (
104
+ elem.getAttribute("data-align") ||
105
+ elem.getAttribute("data-dropdown-align") ||
106
+ "auto"
107
+ ).toLowerCase();
108
+ if (align === "left" || align === "right" || align === "start" || align === "end") {
109
+ return align === "start" ? "left" : align === "end" ? "right" : align;
110
+ }
111
+ const rect = elem.getBoundingClientRect();
112
+ const menuRect = menu?.getBoundingClientRect?.() || { width: 0 };
113
+ const menuWidth = Math.max(
114
+ menu?.offsetWidth || 0,
115
+ menu?.scrollWidth || 0,
116
+ menuRect.width || 0,
117
+ 240
118
+ );
119
+ const spaceRight = Math.max(0, window.innerWidth - rect.left);
120
+ const spaceLeft = Math.max(0, rect.right);
121
+ if (spaceRight >= menuWidth) return "left";
122
+ if (spaceLeft >= menuWidth) return "right";
123
+ return spaceLeft > spaceRight ? "right" : "left";
124
+ };
125
+
88
126
  const openMenu = () => {
89
127
  elem.dataset.dropdownDirection = resolveDirection();
128
+ elem.dataset.dropdownAlign = resolveAlign();
90
129
  menu.setAttribute("aria-hidden", "false");
91
130
  trigger?.setAttribute("aria-expanded", "true");
92
131
  };
@@ -264,16 +303,34 @@ function enhanceRequired(elem) {
264
303
  elem.dataset.enhancedRequired = "true";
265
304
 
266
305
  const enhanceRequiredField = (input) => {
267
-
268
- const label = input.closest("label");
306
+ let label;
307
+ if(input.closest("[role$=group]")) { // Handles both radiogroup and group
308
+ label = input.closest("[role$=group]").querySelector("legend");
309
+ }
310
+ else{
311
+ label = input.closest("label");
312
+ }
269
313
  if (!label) return;
314
+
315
+
270
316
  if (label.querySelector(".required-asterisk")) return;
271
317
 
272
318
  const asterisk = document.createElement("span");
273
319
  asterisk.classList.add("required-asterisk");
274
320
  asterisk.textContent = "*";
275
321
  asterisk.style.marginLeft = "4px";
276
- label.querySelector("span").appendChild(asterisk);
322
+
323
+ const labelText = label.querySelector("span, [data-label]");
324
+ if (labelText) {
325
+ labelText.appendChild(asterisk);
326
+ } else {
327
+ const field = label.querySelector("input, select, textarea");
328
+ if (field) {
329
+ label.insertBefore(asterisk, field);
330
+ } else {
331
+ label.appendChild(asterisk);
332
+ }
333
+ }
277
334
 
278
335
  const form = input.closest("form");
279
336
  if (form && !form.querySelector(".required-legend")) {
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { Generator } from "./pds-generator.js";
6
6
  import { applyStyles, adoptLayers, adoptPrimitives } from "./pds-runtime.js";
7
- import { presets, defaultLog } from "./pds-config.js";
7
+ import { presets, defaultLog, PDS_CONFIG_RELATIONS } from "./pds-config.js";
8
8
  import { defaultPDSEnhancers } from "./pds-enhancers.js";
9
9
  import { defaultPDSEnhancerMetadata } from "./pds-enhancers-meta.js";
10
10
  import { resolvePublicAssetURL } from "./pds-paths.js";
@@ -19,10 +19,77 @@ import {
19
19
  setupAutoDefinerAndEnhancers,
20
20
  stripFunctions,
21
21
  } from "./pds-start-helpers.js";
22
+ import {
23
+ isPresetThemeCompatible,
24
+ resolveThemePreference,
25
+ } from "./pds-theme-utils.js";
22
26
 
23
27
  let __liveApiReady = false;
24
28
  let __queryClass = null;
25
29
 
30
+ function getStoredLiveConfig() {
31
+ if (typeof window === "undefined" || !window.localStorage) return null;
32
+ try {
33
+ const raw = window.localStorage.getItem("pure-ds-config");
34
+ if (!raw) return null;
35
+ const parsed = JSON.parse(raw);
36
+ if (parsed && ("preset" in parsed || "design" in parsed)) return parsed;
37
+ } catch (e) {
38
+ return null;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function deepMergeConfig(target = {}, source = {}) {
44
+ if (!source || typeof source !== "object") return target;
45
+ const out = Array.isArray(target) ? [...target] : { ...target };
46
+ for (const [key, value] of Object.entries(source)) {
47
+ if (value && typeof value === "object" && !Array.isArray(value)) {
48
+ out[key] = deepMergeConfig(
49
+ out[key] && typeof out[key] === "object" ? out[key] : {},
50
+ value
51
+ );
52
+ } else {
53
+ out[key] = value;
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function applyStoredConfigOverrides(baseConfig) {
60
+ const stored = getStoredLiveConfig();
61
+ if (!stored || !baseConfig || typeof baseConfig !== "object") return baseConfig;
62
+
63
+ const storedPreset = stored.preset;
64
+ const storedDesign =
65
+ stored.design && typeof stored.design === "object" ? stored.design : null;
66
+
67
+ if (!storedPreset && !storedDesign) return baseConfig;
68
+
69
+ const hasNewShape =
70
+ "preset" in baseConfig || "design" in baseConfig || "enhancers" in baseConfig;
71
+
72
+ let nextConfig = { ...baseConfig };
73
+
74
+ if (storedPreset) {
75
+ nextConfig.preset = storedPreset;
76
+ }
77
+
78
+ if (storedDesign) {
79
+ if (hasNewShape) {
80
+ const baseDesign =
81
+ baseConfig.design && typeof baseConfig.design === "object"
82
+ ? baseConfig.design
83
+ : {};
84
+ nextConfig.design = deepMergeConfig(baseDesign, storedDesign);
85
+ } else {
86
+ nextConfig = deepMergeConfig(baseConfig, storedDesign);
87
+ }
88
+ }
89
+
90
+ return nextConfig;
91
+ }
92
+
26
93
  async function __attachLiveAPIs(PDS, { applyResolvedTheme, setupSystemListenerIfNeeded }) {
27
94
  if (__liveApiReady) return;
28
95
 
@@ -45,6 +112,7 @@ async function __attachLiveAPIs(PDS, { applyResolvedTheme, setupSystemListenerIf
45
112
  PDS.enums = enums;
46
113
  PDS.common = commonModule || {};
47
114
  PDS.presets = presets;
115
+ PDS.configRelations = PDS_CONFIG_RELATIONS;
48
116
  PDS.enhancerMetadata = defaultPDSEnhancerMetadata;
49
117
  PDS.applyStyles = function(generator) {
50
118
  return applyStyles(generator || Generator.instance);
@@ -73,6 +141,91 @@ async function __attachLiveAPIs(PDS, { applyResolvedTheme, setupSystemListenerIf
73
141
  return await queryEngine.search(question);
74
142
  };
75
143
 
144
+ PDS.applyLivePreset = async function(presetId, options = {}) {
145
+ if (!presetId) return false;
146
+ if (!PDS.registry?.isLive) {
147
+ console.warn("PDS.applyLivePreset is only available in live mode.");
148
+ return false;
149
+ }
150
+
151
+ const baseConfig = PDS.currentConfig || {};
152
+ const { design: _design, preset: _preset, ...rest } = baseConfig;
153
+ const inputConfig = {
154
+ ...structuredClone(stripFunctions(rest)),
155
+ preset: presetId,
156
+ };
157
+
158
+ const normalized = normalizeInitConfig(inputConfig, {}, {
159
+ presets,
160
+ defaultLog,
161
+ });
162
+
163
+ const resolvedTheme = resolveThemePreference(PDS.theme);
164
+ if (!isPresetThemeCompatible(normalized.generatorConfig.design, resolvedTheme)) {
165
+ const presetName =
166
+ normalized.presetInfo?.name ||
167
+ normalized.generatorConfig?.design?.name ||
168
+ presetId;
169
+ console.warn(
170
+ `PDS theme "${resolvedTheme}" not supported by preset "${presetName}".`
171
+ );
172
+ }
173
+
174
+ if (baseConfig.theme && !normalized.generatorConfig.theme) {
175
+ normalized.generatorConfig.theme = baseConfig.theme;
176
+ }
177
+
178
+ const generator = new Generator(normalized.generatorConfig);
179
+
180
+ if (normalized.generatorConfig.design?.typography) {
181
+ try {
182
+ await loadTypographyFonts(normalized.generatorConfig.design.typography);
183
+ } catch (error) {
184
+ normalized.generatorConfig?.log?.(
185
+ "warn",
186
+ "Failed to load some fonts from Google Fonts:",
187
+ error,
188
+ );
189
+ }
190
+ }
191
+
192
+ await applyStyles(generator);
193
+
194
+ const presetInfo = normalized.presetInfo || { id: presetId, name: presetId };
195
+ PDS.currentPreset = presetInfo;
196
+ PDS.currentConfig = Object.freeze({
197
+ ...baseConfig,
198
+ preset: normalized.generatorConfig.preset,
199
+ design: structuredClone(normalized.generatorConfig.design),
200
+ theme: normalized.generatorConfig.theme || baseConfig.theme,
201
+ });
202
+
203
+ const persist = options?.persist !== false;
204
+ if (persist && typeof window !== "undefined") {
205
+ const storageKey = "pure-ds-config";
206
+ try {
207
+ const storedRaw = localStorage.getItem(storageKey);
208
+ const storedParsed = storedRaw ? JSON.parse(storedRaw) : null;
209
+ const nextStored = {
210
+ ...(storedParsed && typeof storedParsed === "object"
211
+ ? storedParsed
212
+ : {}),
213
+ preset: presetInfo.id || presetId,
214
+ design: structuredClone(normalized.generatorConfig.design || {}),
215
+ };
216
+ localStorage.setItem(storageKey, JSON.stringify(nextStored));
217
+ } catch (error) {
218
+ normalized.generatorConfig?.log?.(
219
+ "warn",
220
+ "Failed to store preset:",
221
+ error,
222
+ );
223
+ }
224
+ }
225
+
226
+ return true;
227
+ };
228
+
76
229
  // Live-only compiled getter
77
230
  if (!Object.getOwnPropertyDescriptor(PDS, "compiled")) {
78
231
  Object.defineProperty(PDS, "compiled", {
@@ -147,6 +300,8 @@ export async function startLive(PDS, config, { emitReady, applyResolvedTheme, se
147
300
  );
148
301
  }
149
302
 
303
+ config = applyStoredConfigOverrides(config);
304
+
150
305
  // Attach live-only API surface (ontology, presets, query, etc.)
151
306
  await __attachLiveAPIs(PDS, { applyResolvedTheme, setupSystemListenerIfNeeded });
152
307
  attachFoucListener(PDS);
@@ -197,6 +352,16 @@ export async function startLive(PDS, config, { emitReady, applyResolvedTheme, se
197
352
 
198
353
  // 2) Normalize first-arg API: support { preset, design, enhancers }
199
354
  const normalized = normalizeInitConfig(config, {}, { presets, defaultLog });
355
+ if (manageTheme && !isPresetThemeCompatible(normalized.generatorConfig.design, resolvedTheme)) {
356
+ const presetName =
357
+ normalized.presetInfo?.name ||
358
+ normalized.generatorConfig?.design?.name ||
359
+ normalized.generatorConfig?.preset ||
360
+ "current preset";
361
+ console.warn(
362
+ `PDS theme "${resolvedTheme}" not supported by preset "${presetName}".`
363
+ );
364
+ }
200
365
  const userEnhancers = normalized.enhancers;
201
366
  const { log: logFn, ...configToClone } = normalized.generatorConfig;
202
367
  const generatorConfig = structuredClone(configToClone);
@@ -345,6 +510,20 @@ export async function startLive(PDS, config, { emitReady, applyResolvedTheme, se
345
510
  enhancers: mergedEnhancers,
346
511
  });
347
512
 
513
+
514
+ if (config?.liveEdit && typeof document !== "undefined") {
515
+ try {
516
+ if (!document.querySelector("pds-live-edit")) {
517
+ setTimeout(() => {
518
+ const liveEditor = document.createElement("pds-live-edit");
519
+ document.body.appendChild(liveEditor);
520
+ }, 1000);
521
+ }
522
+ } catch (error) {
523
+ generatorConfig?.log?.("warn", "Live editor failed to start:", error);
524
+ }
525
+ }
526
+
348
527
  emitReady({
349
528
  mode: "live",
350
529
  generator,
@@ -246,6 +246,14 @@ export const ontology = {
246
246
  tags: ["form", "schema", "auto-generate"],
247
247
  category: "form"
248
248
  },
249
+ {
250
+ id: "pds-live-edit",
251
+ name: "Live Edit",
252
+ description: "Contextual live editing for PDS design settings",
253
+ selectors: ["pds-live-edit"],
254
+ tags: ["editor", "live", "config", "tooling"],
255
+ category: "tooling"
256
+ },
249
257
  {
250
258
  id: "pds-splitpanel",
251
259
  name: "Split Panel",
@@ -0,0 +1,33 @@
1
+ const DEFAULT_THEMES = ["light", "dark"];
2
+ const VALID_THEMES = new Set(DEFAULT_THEMES);
3
+
4
+ export function normalizePresetThemes(preset) {
5
+ const themes = Array.isArray(preset?.themes)
6
+ ? preset.themes.map((theme) => String(theme).toLowerCase())
7
+ : DEFAULT_THEMES;
8
+ const normalized = themes.filter((theme) => VALID_THEMES.has(theme));
9
+ return normalized.length ? normalized : DEFAULT_THEMES;
10
+ }
11
+
12
+ export function resolveThemePreference(preference, { preferDocument = true } = {}) {
13
+ const normalized = String(preference || "").toLowerCase();
14
+ if (VALID_THEMES.has(normalized)) return normalized;
15
+
16
+ if (preferDocument && typeof document !== "undefined") {
17
+ const applied = document.documentElement?.getAttribute("data-theme");
18
+ if (VALID_THEMES.has(applied)) return applied;
19
+ }
20
+
21
+ if (typeof window !== "undefined" && window.matchMedia) {
22
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
23
+ return prefersDark ? "dark" : "light";
24
+ }
25
+
26
+ return "light";
27
+ }
28
+
29
+ export function isPresetThemeCompatible(preset, themePreference) {
30
+ const resolvedTheme = resolveThemePreference(themePreference);
31
+ const themes = normalizePresetThemes(preset);
32
+ return themes.includes(resolvedTheme);
33
+ }
package/src/js/pds.d.ts CHANGED
@@ -104,6 +104,9 @@ export interface CompiledState {
104
104
  };
105
105
  }
106
106
 
107
+ /** Public config types for editor IntelliSense */
108
+ export type PDSInitConfig = import("./pds-core/pds-config.js").PDSInitConfig;
109
+
107
110
  /**
108
111
  * Generator - programmatic API to produce tokens, layered CSS and helper modules from a config.
109
112
  * Typical usage:
package/src/js/pds.js CHANGED
@@ -68,6 +68,10 @@ import {
68
68
  setupAutoDefinerAndEnhancers,
69
69
  stripFunctions,
70
70
  } from "./pds-core/pds-start-helpers.js";
71
+ import {
72
+ isPresetThemeCompatible,
73
+ resolveThemePreference,
74
+ } from "./pds-core/pds-theme-utils.js";
71
75
 
72
76
  const __slugifyPreset = (str = "") =>
73
77
  String(str)
@@ -261,6 +265,24 @@ Object.defineProperty(PDS, "theme", {
261
265
  set(value) {
262
266
  try {
263
267
  if (typeof window === "undefined") return;
268
+ const currentPreset = PDS.currentConfig?.design || null;
269
+ const resolvedTheme = resolveThemePreference(value);
270
+ if (currentPreset && !isPresetThemeCompatible(currentPreset, resolvedTheme)) {
271
+ const presetName =
272
+ currentPreset?.name ||
273
+ PDS.currentPreset?.name ||
274
+ PDS.currentConfig?.preset ||
275
+ "current preset";
276
+ console.warn(
277
+ `PDS theme "${resolvedTheme}" not supported by preset "${presetName}".`
278
+ );
279
+ PDS.dispatchEvent(
280
+ new CustomEvent("pds:theme:blocked", {
281
+ detail: { theme: value, resolvedTheme, preset: presetName },
282
+ })
283
+ );
284
+ return;
285
+ }
264
286
  if (value === null || value === undefined) {
265
287
  localStorage.removeItem(__themeStorageKey);
266
288
  } else {