@pure-ds/core 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CSS-INTELLISENSE-LIMITATION.md +98 -0
- package/CSS-INTELLISENSE-QUICK-REF.md +238 -0
- package/INTELLISENSE.md +384 -0
- package/LICENSE +15 -0
- package/custom-elements-manifest.config.js +30 -0
- package/custom-elements.json +2003 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/packages/pds-configurator/src/figma-export.d.ts +13 -0
- package/dist/types/packages/pds-configurator/src/figma-export.d.ts.map +1 -0
- package/dist/types/packages/pds-configurator/src/pds-config-form.d.ts +2 -0
- package/dist/types/packages/pds-configurator/src/pds-config-form.d.ts.map +1 -0
- package/dist/types/packages/pds-configurator/src/pds-configurator.d.ts +2 -0
- package/dist/types/packages/pds-configurator/src/pds-configurator.d.ts.map +1 -0
- package/dist/types/packages/pds-configurator/src/pds-demo.d.ts +2 -0
- package/dist/types/packages/pds-configurator/src/pds-demo.d.ts.map +1 -0
- package/dist/types/pds.config.d.ts +13 -0
- package/dist/types/pds.config.d.ts.map +1 -0
- package/dist/types/pds.d.ts +408 -0
- package/dist/types/public/assets/js/app.d.ts +2 -0
- package/dist/types/public/assets/js/app.d.ts.map +1 -0
- package/dist/types/public/assets/js/pds.d.ts +23 -0
- package/dist/types/public/assets/js/pds.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-calendar.d.ts +23 -0
- package/dist/types/public/assets/pds/components/pds-calendar.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-drawer.d.ts +2 -0
- package/dist/types/public/assets/pds/components/pds-drawer.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-icon.d.ts +53 -0
- package/dist/types/public/assets/pds/components/pds-icon.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-jsonform.d.ts +104 -0
- package/dist/types/public/assets/pds/components/pds-jsonform.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-richtext.d.ts +121 -0
- package/dist/types/public/assets/pds/components/pds-richtext.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts +61 -0
- package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-splitpanel.d.ts +1 -0
- package/dist/types/public/assets/pds/components/pds-splitpanel.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-tabstrip.d.ts +39 -0
- package/dist/types/public/assets/pds/components/pds-tabstrip.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-toaster.d.ts +111 -0
- package/dist/types/public/assets/pds/components/pds-toaster.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-upload.d.ts +83 -0
- package/dist/types/public/assets/pds/components/pds-upload.d.ts.map +1 -0
- package/dist/types/src/js/app.d.ts +2 -0
- package/dist/types/src/js/app.d.ts.map +1 -0
- package/dist/types/src/js/common/ask.d.ts +22 -0
- package/dist/types/src/js/common/ask.d.ts.map +1 -0
- package/dist/types/src/js/common/common.d.ts +3 -0
- package/dist/types/src/js/common/common.d.ts.map +1 -0
- package/dist/types/src/js/common/font-loader.d.ts +24 -0
- package/dist/types/src/js/common/font-loader.d.ts.map +1 -0
- package/dist/types/src/js/common/msg.d.ts +3 -0
- package/dist/types/src/js/common/msg.d.ts.map +1 -0
- package/dist/types/src/js/lit.d.ts +25 -0
- package/dist/types/src/js/lit.d.ts.map +1 -0
- package/dist/types/src/js/pds-configurator/figma-export.d.ts +13 -0
- package/dist/types/src/js/pds-configurator/figma-export.d.ts.map +1 -0
- package/dist/types/src/js/pds-configurator/pds-config-form.d.ts +2 -0
- package/dist/types/src/js/pds-configurator/pds-config-form.d.ts.map +1 -0
- package/dist/types/src/js/pds-configurator/pds-configurator.d.ts +2 -0
- package/dist/types/src/js/pds-configurator/pds-configurator.d.ts.map +1 -0
- package/dist/types/src/js/pds-configurator/pds-demo.d.ts +2 -0
- package/dist/types/src/js/pds-configurator/pds-demo.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-config.d.ts +758 -0
- package/dist/types/src/js/pds-core/pds-config.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-enhancer-metadata.d.ts +6 -0
- package/dist/types/src/js/pds-core/pds-enhancer-metadata.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-enhancers.d.ts +14 -0
- package/dist/types/src/js/pds-core/pds-enhancers.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-enums.d.ts +87 -0
- package/dist/types/src/js/pds-core/pds-enums.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-generator.d.ts +741 -0
- package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-ontology.d.ts +48 -0
- package/dist/types/src/js/pds-core/pds-ontology.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-paths.d.ts +37 -0
- package/dist/types/src/js/pds-core/pds-paths.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-query.d.ts +102 -0
- package/dist/types/src/js/pds-core/pds-query.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-registry.d.ts +40 -0
- package/dist/types/src/js/pds-core/pds-registry.d.ts.map +1 -0
- package/dist/types/src/js/pds.d.ts +109 -0
- package/dist/types/src/js/pds.d.ts.map +1 -0
- package/dist/types/src/pds-core/pds-api.d.ts +31 -0
- package/dist/types/src/pds-core/pds-api.d.ts.map +1 -0
- package/package.json +104 -0
- package/packages/pds-cli/README.md +15 -0
- package/packages/pds-cli/bin/generate-css-data.js +565 -0
- package/packages/pds-cli/bin/generate-manifest.js +352 -0
- package/packages/pds-cli/bin/pds-build-icons.js +152 -0
- package/packages/pds-cli/bin/pds-dx.js +114 -0
- package/packages/pds-cli/bin/pds-static.js +556 -0
- package/packages/pds-cli/bin/pds.js +127 -0
- package/packages/pds-cli/bin/postinstall.js +380 -0
- package/packages/pds-cli/bin/sync-assets.js +252 -0
- package/packages/pds-cli/lib/asset-roots.js +47 -0
- package/packages/pds-cli/lib/fs-writer.js +75 -0
- package/pds.css-data.json +5 -0
- package/pds.html-data.json +5 -0
- package/public/assets/js/app.js +5719 -0
- package/public/assets/js/lit.js +131 -0
- package/public/assets/js/pds.js +3423 -0
- package/public/assets/pds/components/pds-calendar.js +837 -0
- package/public/assets/pds/components/pds-drawer.js +857 -0
- package/public/assets/pds/components/pds-icon.js +338 -0
- package/public/assets/pds/components/pds-jsonform.js +1775 -0
- package/public/assets/pds/components/pds-richtext.js +1035 -0
- package/public/assets/pds/components/pds-scrollrow.js +331 -0
- package/public/assets/pds/components/pds-splitpanel.js +401 -0
- package/public/assets/pds/components/pds-tabstrip.js +251 -0
- package/public/assets/pds/components/pds-toaster.js +446 -0
- package/public/assets/pds/components/pds-upload.js +657 -0
- package/public/assets/pds/custom-elements.json +2003 -0
- package/public/assets/pds/icons/pds-icons.svg +498 -0
- package/public/assets/pds/pds-css-complete.json +1861 -0
- package/public/assets/pds/pds.css-data.json +2152 -0
- package/public/assets/pds/vscode-custom-data.json +824 -0
- package/readme.md +1870 -0
- package/src/js/pds-core/pds-config.js +1162 -0
- package/src/js/pds-core/pds-enhancer-metadata.js +75 -0
- package/src/js/pds-core/pds-enhancers.js +357 -0
- package/src/js/pds-core/pds-enums.js +86 -0
- package/src/js/pds-core/pds-generator.js +5317 -0
- package/src/js/pds-core/pds-ontology.js +256 -0
- package/src/js/pds-core/pds-paths.js +109 -0
- package/src/js/pds-core/pds-query.js +571 -0
- package/src/js/pds-core/pds-registry.js +129 -0
- package/src/js/pds-core/pds.d.ts +129 -0
- package/src/js/pds.d.ts +408 -0
- package/src/js/pds.js +1579 -0
package/src/js/pds.js
ADDED
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
/// <reference path="./pds.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public PDS runtime object exported to consumers.
|
|
5
|
+
*
|
|
6
|
+
* This object exposes the core runtime building blocks for the Pure Design System.
|
|
7
|
+
* It intentionally provides a small, stable surface area so consuming apps can:
|
|
8
|
+
* - programmatically generate design system artifacts (via `Generator`),
|
|
9
|
+
* - adopt styles into Shadow DOM (via `adoptLayers` / `adoptPrimitives`),
|
|
10
|
+
* - query runtime mode and obtain constructable stylesheets (via `registry`).
|
|
11
|
+
*
|
|
12
|
+
* Common events in the PDS ecosystem (emitted by other packages/components):
|
|
13
|
+
* - `design-updated` — emitted by the designer component when the in-memory design changes (detail: { config }).
|
|
14
|
+
* - `pds-generated` — emitted by the PDS configurator when generation completes (detail: { modules, meta }).
|
|
15
|
+
* - `pds-error` — emitted by the PDS configurator when generation fails (detail: Error).
|
|
16
|
+
*
|
|
17
|
+
* Error handling notes:
|
|
18
|
+
* - Methods that perform dynamic imports (e.g. `adoptLayers` via the registry) may log and return fallbacks
|
|
19
|
+
* rather than throwing; consumers should check return values (e.g. null) and listen for `pds-error` in UI flows.
|
|
20
|
+
* - `createStylesheet(css)` will throw synchronously if the provided CSS cannot be parsed by the browser's
|
|
21
|
+
* `CSSStyleSheet.replaceSync()` — callers should guard invalid input or wrap calls in try/catch.
|
|
22
|
+
*
|
|
23
|
+
* @typedef {Object} PDSAPI
|
|
24
|
+
* @property {typeof import("./pds-core/pds-generator.js").Generator} Generator - Generator class to produce design system assets
|
|
25
|
+
* @property {import("./pds-core/pds-registry.js").PDSRegistry} registry - Singleton runtime registry for live/static mode
|
|
26
|
+
* @property {any} ontology - Ontology helpers and metadata for components
|
|
27
|
+
* @property {(shadowRoot: ShadowRoot, layers?: string[], additionalSheets?: CSSStyleSheet[]) => Promise<void>} adoptLayers - Adopt multiple layers into a ShadowRoot. May log errors and fallback to additionalSheets when static imports fail.
|
|
28
|
+
* @property {(shadowRoot: ShadowRoot, additionalSheets?: CSSStyleSheet[]) => Promise<void>} adoptPrimitives - Adopt primitives layer into a ShadowRoot. Designed as a convenience for components.
|
|
29
|
+
* @property {(css:string) => CSSStyleSheet} createStylesheet - Create a constructable stylesheet from CSS text. @throws {DOMException} on invalid CSS in some browsers.
|
|
30
|
+
* @property {() => boolean} isLiveMode - Returns true when running in live/designer-backed mode
|
|
31
|
+
* @property {(el: Element) => import("./pds-core/pds-ontology.js").ComponentDef | null} findComponentForElement - Helper to find a component definition for a DOM element
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Workspace for the Pure Design System runtime API
|
|
36
|
+
* PDS is now an EventTarget so consumers can subscribe to a single, consistent
|
|
37
|
+
* event bus instead of listening on window/document or individual elements.
|
|
38
|
+
*/
|
|
39
|
+
class PDSBase extends EventTarget {}
|
|
40
|
+
/** @type {PDSAPI & PDSBase} */
|
|
41
|
+
const PDS = new PDSBase();
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
Generator,
|
|
45
|
+
adoptLayers,
|
|
46
|
+
adoptPrimitives,
|
|
47
|
+
createStylesheet,
|
|
48
|
+
isLiveMode,
|
|
49
|
+
} from "./pds-core/pds-generator.js";
|
|
50
|
+
import { registry } from "./pds-core/pds-registry.js";
|
|
51
|
+
import ontology from "./pds-core/pds-ontology.js";
|
|
52
|
+
import { findComponentForElement } from "./pds-core/pds-ontology.js";
|
|
53
|
+
import { presets, defaultLog } from "./pds-core/pds-config.js";
|
|
54
|
+
import { enums } from "./pds-core/pds-enums.js";
|
|
55
|
+
import { ask } from "./common/ask.js";
|
|
56
|
+
import { PDSQuery } from "./pds-core/pds-query.js";
|
|
57
|
+
import * as common from "./common/common.js";
|
|
58
|
+
import { defaultPDSEnhancers } from "./pds-core/pds-enhancers.js";
|
|
59
|
+
import { resolvePublicAssetURL } from "./pds-core/pds-paths.js";
|
|
60
|
+
|
|
61
|
+
// Font loading utilities
|
|
62
|
+
import { loadTypographyFonts } from "./common/font-loader.js";
|
|
63
|
+
|
|
64
|
+
/** Generator class — use to programmatically create design system assets from a config */
|
|
65
|
+
PDS.Generator = Generator;
|
|
66
|
+
|
|
67
|
+
/** Singleton runtime registry. Use `registry.setDesigner()` to enable live mode or `registry.setStaticMode()` for static assets */
|
|
68
|
+
PDS.registry = registry;
|
|
69
|
+
|
|
70
|
+
/** Ontology and metadata about components and tokens */
|
|
71
|
+
PDS.ontology = ontology;
|
|
72
|
+
|
|
73
|
+
/** Adopt a set of layered stylesheets into a ShadowRoot */
|
|
74
|
+
PDS.adoptLayers = adoptLayers;
|
|
75
|
+
|
|
76
|
+
/** Convenience to adopt only primitives into a ShadowRoot */
|
|
77
|
+
PDS.adoptPrimitives = adoptPrimitives;
|
|
78
|
+
|
|
79
|
+
/** Create a constructable CSSStyleSheet from a CSS string */
|
|
80
|
+
PDS.createStylesheet = createStylesheet;
|
|
81
|
+
|
|
82
|
+
/** Return true when running inside a live/designer-backed environment */
|
|
83
|
+
PDS.isLiveMode = isLiveMode;
|
|
84
|
+
|
|
85
|
+
PDS.enums = enums;
|
|
86
|
+
|
|
87
|
+
PDS.ask = ask;
|
|
88
|
+
|
|
89
|
+
// Expose common utilities (deepMerge, isObject, etc.)
|
|
90
|
+
PDS.common = common;
|
|
91
|
+
|
|
92
|
+
// Expose presets object directly
|
|
93
|
+
PDS.presets = presets;
|
|
94
|
+
|
|
95
|
+
/** Find a component definition (ontology) for a given DOM element */
|
|
96
|
+
PDS.findComponentForElement = findComponentForElement;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Smart query interface for design system questions
|
|
100
|
+
* @param {string} question - Natural language query about tokens, components, utilities, or patterns
|
|
101
|
+
* @returns {Promise<Array>} Array of results with text, value, icon, category, code examples
|
|
102
|
+
* @example
|
|
103
|
+
* const results = await PDS.query("what is the focus border color on inputs?");
|
|
104
|
+
* const results = await PDS.query("how do I create an icon-only button?");
|
|
105
|
+
*/
|
|
106
|
+
PDS.query = async function(question) {
|
|
107
|
+
const queryEngine = new PDSQuery(PDS);
|
|
108
|
+
return await queryEngine.search(question);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function __emitPDSReady(detail) {
|
|
112
|
+
const hasCustomEvent = typeof CustomEvent === "function";
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const readyEvent = hasCustomEvent
|
|
116
|
+
? new CustomEvent("pds:ready", { detail })
|
|
117
|
+
: new Event("pds:ready");
|
|
118
|
+
PDS.dispatchEvent(readyEvent);
|
|
119
|
+
} catch (e) {}
|
|
120
|
+
|
|
121
|
+
if (typeof document !== "undefined") {
|
|
122
|
+
if (hasCustomEvent) {
|
|
123
|
+
const eventOptions = { detail, bubbles: true, composed: true };
|
|
124
|
+
try {
|
|
125
|
+
document.dispatchEvent(new CustomEvent("pds:ready", eventOptions));
|
|
126
|
+
} catch (e) {}
|
|
127
|
+
try {
|
|
128
|
+
document.dispatchEvent(new CustomEvent("pds-ready", eventOptions));
|
|
129
|
+
} catch (e) {}
|
|
130
|
+
} else {
|
|
131
|
+
try {
|
|
132
|
+
document.dispatchEvent(new Event("pds:ready"));
|
|
133
|
+
} catch (e) {}
|
|
134
|
+
try {
|
|
135
|
+
document.dispatchEvent(new Event("pds-ready"));
|
|
136
|
+
} catch (e) {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Current configuration (set after PDS.start() completes) - read-only, frozen after initialization */
|
|
142
|
+
Object.defineProperty(PDS, "currentConfig", {
|
|
143
|
+
value: null,
|
|
144
|
+
writable: true,
|
|
145
|
+
enumerable: true,
|
|
146
|
+
configurable: false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compiled design system state - provides structured access to all generated tokens,
|
|
151
|
+
* layers, and metadata. Available in live mode when a generator is active.
|
|
152
|
+
* Returns the generator's compiled representation or null if not in live mode.
|
|
153
|
+
*
|
|
154
|
+
* Structure includes:
|
|
155
|
+
* - tokens: All generated token groups (colors, spacing, typography, etc.)
|
|
156
|
+
* - layers: CSS content and metadata for each layer (tokens, primitives, components, utilities)
|
|
157
|
+
* - config: Configuration snapshot used to generate the current state
|
|
158
|
+
* - capabilities: Runtime environment capabilities
|
|
159
|
+
* - references: Links to ontology and enums for introspection
|
|
160
|
+
* - meta: Computed metadata about the design system
|
|
161
|
+
* - helpers: Utility methods to query the compiled state
|
|
162
|
+
*/
|
|
163
|
+
Object.defineProperty(PDS, "compiled", {
|
|
164
|
+
get() {
|
|
165
|
+
// Only available in live mode when we have a generator
|
|
166
|
+
if (PDS.registry?.isLive && PDS.registry?._designer) {
|
|
167
|
+
return PDS.registry._designer.compiled;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
},
|
|
171
|
+
enumerable: true,
|
|
172
|
+
configurable: false,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Always expose PDS on the window in browser contexts so consumers can access it in both live and static modes
|
|
176
|
+
if (typeof window !== "undefined") {
|
|
177
|
+
// @ts-ignore
|
|
178
|
+
window.PDS = PDS;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// FOUC Prevention: Add pds-ready class when PDS is fully initialized in live mode
|
|
183
|
+
// This works in conjunction with CSS injected by the live() function
|
|
184
|
+
if (typeof document !== "undefined") {
|
|
185
|
+
PDS.addEventListener("pds:ready", (event) => {
|
|
186
|
+
const mode = event.detail?.mode;
|
|
187
|
+
if (mode) {
|
|
188
|
+
// Add mode-specific class (pds-live or pds-static)
|
|
189
|
+
document.documentElement.classList.add(`pds-${mode}`);
|
|
190
|
+
|
|
191
|
+
// Only add pds-ready class in live mode for FOUC prevention
|
|
192
|
+
if (mode === "live") {
|
|
193
|
+
document.documentElement.classList.add("pds-ready");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Theme management (centralized on PDS.theme)
|
|
201
|
+
// Consumers may read/write `PDS.theme` with values: 'system' | 'light' | 'dark'
|
|
202
|
+
// Setting the property persists to localStorage (when available), updates
|
|
203
|
+
// document.documentElement[data-theme] to an explicit 'light'|'dark' value
|
|
204
|
+
// and emits a `pds:theme:changed` event. Reading the property returns the
|
|
205
|
+
// raw stored preference (or null if none).
|
|
206
|
+
|
|
207
|
+
// Private module-level variables for theme management
|
|
208
|
+
const __themeStorageKey = "pure-ds-theme";
|
|
209
|
+
let __themeMQ = null;
|
|
210
|
+
let __themeMQListener = null;
|
|
211
|
+
|
|
212
|
+
// Private: Apply resolved theme to document
|
|
213
|
+
function __applyResolvedTheme(raw) {
|
|
214
|
+
try {
|
|
215
|
+
if (typeof document === "undefined") return;
|
|
216
|
+
let resolved = "light";
|
|
217
|
+
if (!raw) {
|
|
218
|
+
// No stored preference: use OS preference
|
|
219
|
+
const prefersDark =
|
|
220
|
+
typeof window !== "undefined" &&
|
|
221
|
+
window.matchMedia &&
|
|
222
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
223
|
+
resolved = prefersDark ? "dark" : "light";
|
|
224
|
+
} else if (raw === "system") {
|
|
225
|
+
const prefersDark =
|
|
226
|
+
typeof window !== "undefined" &&
|
|
227
|
+
window.matchMedia &&
|
|
228
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
229
|
+
resolved = prefersDark ? "dark" : "light";
|
|
230
|
+
} else {
|
|
231
|
+
resolved = raw;
|
|
232
|
+
}
|
|
233
|
+
document.documentElement.setAttribute("data-theme", resolved);
|
|
234
|
+
} catch (e) {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Private: Setup system theme change listener when needed
|
|
238
|
+
function __setupSystemListenerIfNeeded(raw) {
|
|
239
|
+
try {
|
|
240
|
+
// Remove any existing listener first
|
|
241
|
+
if (__themeMQ && __themeMQListener) {
|
|
242
|
+
try {
|
|
243
|
+
if (typeof __themeMQ.removeEventListener === "function")
|
|
244
|
+
__themeMQ.removeEventListener("change", __themeMQListener);
|
|
245
|
+
else if (typeof __themeMQ.removeListener === "function")
|
|
246
|
+
__themeMQ.removeListener(__themeMQListener);
|
|
247
|
+
} catch (e) {}
|
|
248
|
+
__themeMQ = null;
|
|
249
|
+
__themeMQListener = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
raw === "system" &&
|
|
254
|
+
typeof window !== "undefined" &&
|
|
255
|
+
window.matchMedia
|
|
256
|
+
) {
|
|
257
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
258
|
+
const listener = (e) => {
|
|
259
|
+
const isDark = e?.matches === undefined ? mq.matches : e.matches;
|
|
260
|
+
try {
|
|
261
|
+
const newTheme = isDark ? "dark" : "light";
|
|
262
|
+
document.documentElement.setAttribute("data-theme", newTheme);
|
|
263
|
+
PDS.dispatchEvent(
|
|
264
|
+
new CustomEvent("pds:theme:changed", {
|
|
265
|
+
detail: { theme: newTheme, source: "system" },
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
} catch (ex) {}
|
|
269
|
+
};
|
|
270
|
+
__themeMQ = mq;
|
|
271
|
+
__themeMQListener = listener;
|
|
272
|
+
if (typeof mq.addEventListener === "function")
|
|
273
|
+
mq.addEventListener("change", listener);
|
|
274
|
+
else if (typeof mq.addListener === "function") mq.addListener(listener);
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
Object.defineProperty(PDS, "theme", {
|
|
280
|
+
get() {
|
|
281
|
+
try {
|
|
282
|
+
if (typeof window === "undefined") return null;
|
|
283
|
+
return localStorage.getItem(__themeStorageKey) || null;
|
|
284
|
+
} catch (e) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
set(value) {
|
|
289
|
+
try {
|
|
290
|
+
if (typeof window === "undefined") return;
|
|
291
|
+
if (value === null || value === undefined) {
|
|
292
|
+
localStorage.removeItem(__themeStorageKey);
|
|
293
|
+
} else {
|
|
294
|
+
localStorage.setItem(__themeStorageKey, value);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Apply resolved (light/dark) value to document
|
|
298
|
+
__applyResolvedTheme(value);
|
|
299
|
+
// Setup system change listener only when 'system' is selected
|
|
300
|
+
__setupSystemListenerIfNeeded(value);
|
|
301
|
+
|
|
302
|
+
// Emit a notification with the raw preference (value may be 'system')
|
|
303
|
+
PDS.dispatchEvent(
|
|
304
|
+
new CustomEvent("pds:theme:changed", {
|
|
305
|
+
detail: { theme: value, source: "api" },
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
} catch (e) {}
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ----------------------------------------------------------------------------
|
|
313
|
+
// Default Enhancers — first-class citizens alongside AutoDefiner
|
|
314
|
+
// ----------------------------------------------------------------------------
|
|
315
|
+
/**
|
|
316
|
+
* Default DOM enhancers shipped with PDS. These are lightweight progressive
|
|
317
|
+
* enhancements that can be applied to vanilla markup. Consumers can override
|
|
318
|
+
* or add to these via the `enhancers` option of PDS.start({ mode }).
|
|
319
|
+
*/
|
|
320
|
+
PDS.defaultEnhancers = defaultPDSEnhancers;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Validate a design configuration for accessibility sanity checks.
|
|
324
|
+
* Currently validates color contrast for primary buttons and base surface text
|
|
325
|
+
* in both light and dark themes.
|
|
326
|
+
*
|
|
327
|
+
* @param {object} designConfig - A full or partial PDS config object
|
|
328
|
+
* @param {object} [options]
|
|
329
|
+
* @param {number} [options.minContrast=4.5] - Minimum contrast ratio for normal text
|
|
330
|
+
* @returns {{ ok: boolean, issues: Array<{path:string, message:string, ratio:number, min:number, context?:string}> }}
|
|
331
|
+
*/
|
|
332
|
+
function validateDesign(designConfig = {}, options = {}) {
|
|
333
|
+
const MIN = Number(options.minContrast || 4.5);
|
|
334
|
+
|
|
335
|
+
// Local helpers (keep public; no dependency on private Generator methods)
|
|
336
|
+
const hexToRgb = (hex) => {
|
|
337
|
+
const h = String(hex || "").replace("#", "");
|
|
338
|
+
const full =
|
|
339
|
+
h.length === 3
|
|
340
|
+
? h
|
|
341
|
+
.split("")
|
|
342
|
+
.map((c) => c + c)
|
|
343
|
+
.join("")
|
|
344
|
+
: h;
|
|
345
|
+
const num = parseInt(full || "0", 16);
|
|
346
|
+
return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 };
|
|
347
|
+
};
|
|
348
|
+
const luminance = (hex) => {
|
|
349
|
+
const { r, g, b } = hexToRgb(hex);
|
|
350
|
+
const srgb = [r / 255, g / 255, b / 255].map((v) =>
|
|
351
|
+
v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)
|
|
352
|
+
);
|
|
353
|
+
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
|
|
354
|
+
};
|
|
355
|
+
const contrast = (a, b) => {
|
|
356
|
+
if (!a || !b) return 0;
|
|
357
|
+
const L1 = luminance(a);
|
|
358
|
+
const L2 = luminance(b);
|
|
359
|
+
const lighter = Math.max(L1, L2);
|
|
360
|
+
const darker = Math.min(L1, L2);
|
|
361
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const issues = [];
|
|
365
|
+
try {
|
|
366
|
+
// Build tokens from the candidate config
|
|
367
|
+
const gen = new PDS.Generator({ design: structuredClone(designConfig) });
|
|
368
|
+
const c = gen.tokens.colors;
|
|
369
|
+
|
|
370
|
+
// Light theme checks - use computed interactive tokens
|
|
371
|
+
const light = {
|
|
372
|
+
surfaceBg: c.surface?.base,
|
|
373
|
+
surfaceText: c.gray?.[900] || "#000000",
|
|
374
|
+
primaryFill: c.interactive?.light?.fill || c.primary?.[600],
|
|
375
|
+
primaryText: c.interactive?.light?.text || c.primary?.[600],
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Primary button (light): check button fill with white text
|
|
379
|
+
const lightBtnRatio = contrast(light.primaryFill, "#ffffff");
|
|
380
|
+
if (lightBtnRatio < MIN) {
|
|
381
|
+
issues.push({
|
|
382
|
+
path: "/colors/primary",
|
|
383
|
+
message: `Primary button contrast too low in light theme (${lightBtnRatio.toFixed(
|
|
384
|
+
2
|
|
385
|
+
)} < ${MIN}). Choose a darker primary.`,
|
|
386
|
+
ratio: lightBtnRatio,
|
|
387
|
+
min: MIN,
|
|
388
|
+
context: "light/btn-primary",
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Surface text (light): text vs surface base
|
|
393
|
+
const lightTextRatio = contrast(light.surfaceBg, light.surfaceText);
|
|
394
|
+
if (lightTextRatio < MIN) {
|
|
395
|
+
issues.push({
|
|
396
|
+
path: "/colors/background",
|
|
397
|
+
message: `Base text contrast on surface (light) is too low (${lightTextRatio.toFixed(
|
|
398
|
+
2
|
|
399
|
+
)} < ${MIN}). Adjust background or secondary (gray).`,
|
|
400
|
+
ratio: lightTextRatio,
|
|
401
|
+
min: MIN,
|
|
402
|
+
context: "light/surface-text",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Primary text for outline/link: check link text on surface
|
|
407
|
+
const lightOutlineRatio = contrast(light.primaryText, light.surfaceBg);
|
|
408
|
+
if (lightOutlineRatio < MIN) {
|
|
409
|
+
issues.push({
|
|
410
|
+
path: "/colors/primary",
|
|
411
|
+
message: `Primary text on surface is too low for outline/link styles (light) (${lightOutlineRatio.toFixed(
|
|
412
|
+
2
|
|
413
|
+
)} < ${MIN}). Choose a darker primary or lighter surface.`,
|
|
414
|
+
ratio: lightOutlineRatio,
|
|
415
|
+
min: MIN,
|
|
416
|
+
context: "light/outline",
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Dark theme checks - use computed interactive tokens
|
|
421
|
+
const d = c.dark;
|
|
422
|
+
if (d) {
|
|
423
|
+
const dark = {
|
|
424
|
+
surfaceBg: d.surface?.base || c.surface?.inverse,
|
|
425
|
+
primaryFill: c.interactive?.dark?.fill || d.primary?.[600],
|
|
426
|
+
primaryText: c.interactive?.dark?.text || d.primary?.[600],
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Primary button (dark): check button fill with white text
|
|
430
|
+
const darkBtnRatio = contrast(dark.primaryFill, "#ffffff");
|
|
431
|
+
if (darkBtnRatio < MIN) {
|
|
432
|
+
issues.push({
|
|
433
|
+
path: "/colors/darkMode/primary",
|
|
434
|
+
message: `Primary button contrast too low in dark theme (${darkBtnRatio.toFixed(
|
|
435
|
+
2
|
|
436
|
+
)} < ${MIN}). Override darkMode.primary or pick a brighter hue.`,
|
|
437
|
+
ratio: darkBtnRatio,
|
|
438
|
+
min: MIN,
|
|
439
|
+
context: "dark/btn-primary",
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Outline/link style in dark: check link text on dark surface
|
|
444
|
+
const darkOutlineRatio = contrast(dark.primaryText, dark.surfaceBg);
|
|
445
|
+
if (darkOutlineRatio < MIN) {
|
|
446
|
+
issues.push({
|
|
447
|
+
path: "/colors/darkMode/primary",
|
|
448
|
+
message: `Primary text on surface is too low for outline/link styles (dark) (${darkOutlineRatio.toFixed(
|
|
449
|
+
2
|
|
450
|
+
)} < ${MIN}). Override darkMode.primary/background.`,
|
|
451
|
+
ratio: darkOutlineRatio,
|
|
452
|
+
min: MIN,
|
|
453
|
+
context: "dark/outline",
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} catch (err) {
|
|
458
|
+
issues.push({
|
|
459
|
+
path: "/",
|
|
460
|
+
message: `Validation failed: ${String(err?.message || err)}`,
|
|
461
|
+
ratio: 0,
|
|
462
|
+
min: 0,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return { ok: issues.length === 0, issues };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Expose validator on the public API */
|
|
470
|
+
PDS.validateDesign = validateDesign;
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Validate multiple design configurations at once.
|
|
474
|
+
* Useful for build-time enforcement of preset compliance.
|
|
475
|
+
*
|
|
476
|
+
* @param {Array<object>} designs - Array of design configs; items may include an optional `name` property.
|
|
477
|
+
* @param {object} [options] - Options forwarded to validateDesign (e.g., { minContrast })
|
|
478
|
+
* @returns {{ ok: boolean, results: Array<{ name?: string, ok: boolean, issues: Array<{path:string, message:string, ratio:number, min:number, context?:string}> }> }}
|
|
479
|
+
*/
|
|
480
|
+
function validateDesigns(designs = [], options = {}) {
|
|
481
|
+
const results = [];
|
|
482
|
+
|
|
483
|
+
const list = Array.isArray(designs)
|
|
484
|
+
? designs
|
|
485
|
+
: designs && typeof designs === "object"
|
|
486
|
+
? Object.values(designs)
|
|
487
|
+
: [];
|
|
488
|
+
|
|
489
|
+
for (const item of list) {
|
|
490
|
+
let name;
|
|
491
|
+
let configToValidate = null;
|
|
492
|
+
|
|
493
|
+
// Accept a few shapes:
|
|
494
|
+
// - string => treat as preset id/name
|
|
495
|
+
// - { preset, design?, name? } => resolve preset then merge overrides
|
|
496
|
+
// - full config object (legacy) => validate directly
|
|
497
|
+
if (typeof item === "string") {
|
|
498
|
+
const id = String(item).toLowerCase();
|
|
499
|
+
const found =
|
|
500
|
+
presets?.[id] ||
|
|
501
|
+
Object.values(presets || {}).find(
|
|
502
|
+
(p) =>
|
|
503
|
+
__slugify(p.name) === id ||
|
|
504
|
+
String(p.name || "").toLowerCase() === id
|
|
505
|
+
);
|
|
506
|
+
if (!found) {
|
|
507
|
+
results.push({
|
|
508
|
+
name: item,
|
|
509
|
+
ok: false,
|
|
510
|
+
issues: [
|
|
511
|
+
{
|
|
512
|
+
path: "/",
|
|
513
|
+
message: `Preset not found: ${item}`,
|
|
514
|
+
ratio: 0,
|
|
515
|
+
min: 0,
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
});
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
name = found.name || id;
|
|
522
|
+
configToValidate = structuredClone(found);
|
|
523
|
+
} else if (item && typeof item === "object") {
|
|
524
|
+
name = item.name || item.preset || undefined;
|
|
525
|
+
if ("preset" in item || "design" in item) {
|
|
526
|
+
const effectivePreset = String(item.preset || "default").toLowerCase();
|
|
527
|
+
const found =
|
|
528
|
+
presets?.[effectivePreset] ||
|
|
529
|
+
Object.values(presets || {}).find(
|
|
530
|
+
(p) =>
|
|
531
|
+
__slugify(p.name) === effectivePreset ||
|
|
532
|
+
String(p.name || "").toLowerCase() === effectivePreset
|
|
533
|
+
);
|
|
534
|
+
if (!found) {
|
|
535
|
+
results.push({
|
|
536
|
+
name,
|
|
537
|
+
ok: false,
|
|
538
|
+
issues: [
|
|
539
|
+
{
|
|
540
|
+
path: "/",
|
|
541
|
+
message: `Preset not found: ${item.preset}`,
|
|
542
|
+
ratio: 0,
|
|
543
|
+
min: 0,
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
});
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
let base = structuredClone(found);
|
|
550
|
+
if (item.design && typeof item.design === "object") {
|
|
551
|
+
base = __deepMerge(base, structuredClone(item.design));
|
|
552
|
+
}
|
|
553
|
+
configToValidate = base;
|
|
554
|
+
} else {
|
|
555
|
+
// Assume a full config object
|
|
556
|
+
configToValidate = item;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!configToValidate) {
|
|
561
|
+
results.push({
|
|
562
|
+
name,
|
|
563
|
+
ok: false,
|
|
564
|
+
issues: [
|
|
565
|
+
{ path: "/", message: "Invalid design entry", ratio: 0, min: 0 },
|
|
566
|
+
],
|
|
567
|
+
});
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const { ok, issues } = validateDesign(configToValidate, options);
|
|
572
|
+
results.push({ name, ok, issues });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return { ok: results.every((r) => r.ok), results };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Expose batch validator on the public API */
|
|
579
|
+
PDS.validateDesigns = validateDesigns;
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Initialize PDS in live mode with the given configuration (new unified shape).
|
|
583
|
+
* Typically invoked via PDS.start({ mode: 'live', ... }).
|
|
584
|
+
*
|
|
585
|
+
* Shape:
|
|
586
|
+
* PDS.start({
|
|
587
|
+
* mode: 'live',
|
|
588
|
+
* preset?: string,
|
|
589
|
+
* design?: object,
|
|
590
|
+
* autoDefine?: {
|
|
591
|
+
* baseURL?: string,
|
|
592
|
+
* predefine?: string[],
|
|
593
|
+
* mapper?: (tag:string)=>string|undefined|null, // return undefined/null/false to let PDS default mapper handle it
|
|
594
|
+
* // plus any AutoDefiner flags: scanExisting, observeShadows, patchAttachShadow, debounceMs, onError
|
|
595
|
+
* },
|
|
596
|
+
* // runtime flags (optional)
|
|
597
|
+
* applyGlobalStyles?: boolean,
|
|
598
|
+
* manageTheme?: boolean,
|
|
599
|
+
* themeStorageKey?: string,
|
|
600
|
+
* preloadStyles?: boolean,
|
|
601
|
+
* criticalLayers?: string[]
|
|
602
|
+
* })
|
|
603
|
+
*
|
|
604
|
+
* @param {object} config - The PDS configuration object (unified shape)
|
|
605
|
+
* @returns {Promise<{generator: Generator, config: object, theme: string, autoDefiner?: any}>}
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* await PDS.start({ mode: 'live',
|
|
609
|
+
* preset: 'paper-and-ink',
|
|
610
|
+
* design: { colors: { accent: '#FF4081' } },
|
|
611
|
+
* autoDefine: { predefine: ['pds-icon'] }
|
|
612
|
+
* });
|
|
613
|
+
*/
|
|
614
|
+
// Internal: resolve theme and set html[data-theme], return resolvedTheme and storedTheme
|
|
615
|
+
function __resolveThemeAndApply({ manageTheme, themeStorageKey }) {
|
|
616
|
+
let resolvedTheme = "light";
|
|
617
|
+
let storedTheme = null;
|
|
618
|
+
if (manageTheme && typeof window !== "undefined") {
|
|
619
|
+
// Read raw preference (may be null, 'system', 'light', 'dark') using provided storage key
|
|
620
|
+
try {
|
|
621
|
+
storedTheme = localStorage.getItem(themeStorageKey) || null;
|
|
622
|
+
} catch (e) {
|
|
623
|
+
storedTheme = null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Apply the resolved theme and ensure system listener exists when needed
|
|
627
|
+
try {
|
|
628
|
+
__applyResolvedTheme(storedTheme);
|
|
629
|
+
__setupSystemListenerIfNeeded(storedTheme);
|
|
630
|
+
} catch (e) {}
|
|
631
|
+
|
|
632
|
+
// Compute explicit resolvedTheme to return
|
|
633
|
+
if (storedTheme) {
|
|
634
|
+
if (storedTheme === "system") {
|
|
635
|
+
const prefersDark =
|
|
636
|
+
window.matchMedia &&
|
|
637
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
638
|
+
resolvedTheme = prefersDark ? "dark" : "light";
|
|
639
|
+
} else {
|
|
640
|
+
resolvedTheme = storedTheme;
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
const prefersDark =
|
|
644
|
+
window.matchMedia &&
|
|
645
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
646
|
+
resolvedTheme = prefersDark ? "dark" : "light";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return { resolvedTheme, storedTheme };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Internal: deep merge utility (arrays replace; objects merge)
|
|
653
|
+
function __deepMerge(target = {}, source = {}) {
|
|
654
|
+
if (!source || typeof source !== "object") return target;
|
|
655
|
+
const out = Array.isArray(target) ? [...target] : { ...target };
|
|
656
|
+
for (const [key, value] of Object.entries(source)) {
|
|
657
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
658
|
+
out[key] = __deepMerge(
|
|
659
|
+
out[key] && typeof out[key] === "object" ? out[key] : {},
|
|
660
|
+
value
|
|
661
|
+
);
|
|
662
|
+
} else {
|
|
663
|
+
out[key] = value;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Internal: create a slug for matching names like "Paper & Ink" -> "paper-and-ink"
|
|
670
|
+
function __slugify(str = "") {
|
|
671
|
+
return String(str)
|
|
672
|
+
.toLowerCase()
|
|
673
|
+
.replace(/&/g, " and ")
|
|
674
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
675
|
+
.replace(/^-+|-+$/g, "");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Internal: recursively remove functions from an object to make it cloneable
|
|
679
|
+
function __stripFunctions(obj) {
|
|
680
|
+
if (obj === null || obj === undefined) return obj;
|
|
681
|
+
if (typeof obj === "function") return undefined;
|
|
682
|
+
if (typeof obj !== "object") return obj;
|
|
683
|
+
|
|
684
|
+
if (Array.isArray(obj)) {
|
|
685
|
+
return obj.map(item => __stripFunctions(item)).filter(item => item !== undefined);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const result = {};
|
|
689
|
+
for (const key in obj) {
|
|
690
|
+
if (obj.hasOwnProperty(key)) {
|
|
691
|
+
const value = obj[key];
|
|
692
|
+
if (typeof value !== "function") {
|
|
693
|
+
const stripped = __stripFunctions(value);
|
|
694
|
+
if (stripped !== undefined) {
|
|
695
|
+
result[key] = stripped;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const __ABSOLUTE_URL_PATTERN__ = /^[a-z][a-z0-9+\-.]*:\/\//i;
|
|
704
|
+
const __MODULE_URL__ = (() => {
|
|
705
|
+
try {
|
|
706
|
+
return import.meta.url;
|
|
707
|
+
} catch (e) {
|
|
708
|
+
return undefined;
|
|
709
|
+
}
|
|
710
|
+
})();
|
|
711
|
+
|
|
712
|
+
function __ensureAbsoluteAssetURL(value, options = {}) {
|
|
713
|
+
if (!value || __ABSOLUTE_URL_PATTERN__.test(value)) {
|
|
714
|
+
return value;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const { preferModule = true } = options;
|
|
718
|
+
|
|
719
|
+
const tryModule = () => {
|
|
720
|
+
if (!__MODULE_URL__) return null;
|
|
721
|
+
try {
|
|
722
|
+
return new URL(value, __MODULE_URL__).href;
|
|
723
|
+
} catch (e) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const tryWindow = () => {
|
|
729
|
+
if (typeof window === "undefined" || !window.location?.origin) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
return new URL(value, window.location.origin).href;
|
|
734
|
+
} catch (e) {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const resolved = preferModule
|
|
740
|
+
? tryModule() || tryWindow()
|
|
741
|
+
: tryWindow() || tryModule();
|
|
742
|
+
|
|
743
|
+
return resolved || value;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const __ensureTrailingSlash = (value) =>
|
|
747
|
+
typeof value === "string" && value.length && !value.endsWith("/")
|
|
748
|
+
? `${value}/`
|
|
749
|
+
: value;
|
|
750
|
+
|
|
751
|
+
const __MODULE_DEFAULT_ASSET_ROOT__ = (() => {
|
|
752
|
+
if (!__MODULE_URL__) return undefined;
|
|
753
|
+
try {
|
|
754
|
+
const parsed = new URL(__MODULE_URL__);
|
|
755
|
+
if (/\/public\/assets\/js\//.test(parsed.pathname)) {
|
|
756
|
+
return new URL("../pds/", __MODULE_URL__).href;
|
|
757
|
+
}
|
|
758
|
+
} catch (e) {
|
|
759
|
+
return undefined;
|
|
760
|
+
}
|
|
761
|
+
return undefined;
|
|
762
|
+
})();
|
|
763
|
+
|
|
764
|
+
function __resolveRuntimeAssetRoot(config) {
|
|
765
|
+
const hasCustomRoot = Boolean(config?.public?.root || config?.static?.root);
|
|
766
|
+
let candidate = resolvePublicAssetURL(config);
|
|
767
|
+
|
|
768
|
+
if (!hasCustomRoot && __MODULE_DEFAULT_ASSET_ROOT__) {
|
|
769
|
+
candidate = __MODULE_DEFAULT_ASSET_ROOT__;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return __ensureTrailingSlash(__ensureAbsoluteAssetURL(candidate));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Internal: normalize first-arg config to a full generator config and extract enhancers if provided inline
|
|
776
|
+
function __normalizeInitConfig(inputConfig = {}, options = {}) {
|
|
777
|
+
// If caller passed a plain design config (legacy), keep as-is
|
|
778
|
+
const hasDesignKeys =
|
|
779
|
+
typeof inputConfig === "object" &&
|
|
780
|
+
("colors" in inputConfig ||
|
|
781
|
+
"typography" in inputConfig ||
|
|
782
|
+
"spatialRhythm" in inputConfig ||
|
|
783
|
+
"shape" in inputConfig ||
|
|
784
|
+
"behavior" in inputConfig ||
|
|
785
|
+
"layout" in inputConfig ||
|
|
786
|
+
"advanced" in inputConfig ||
|
|
787
|
+
"a11y" in inputConfig ||
|
|
788
|
+
"components" in inputConfig ||
|
|
789
|
+
"icons" in inputConfig);
|
|
790
|
+
|
|
791
|
+
// Extract potential inline enhancers from config; prefer inline over options
|
|
792
|
+
let inlineEnhancers = inputConfig && inputConfig.enhancers;
|
|
793
|
+
if (inlineEnhancers && !Array.isArray(inlineEnhancers)) {
|
|
794
|
+
// If an object was provided, convert to array of values
|
|
795
|
+
inlineEnhancers = Object.values(inlineEnhancers);
|
|
796
|
+
}
|
|
797
|
+
const enhancers = inlineEnhancers ?? options.enhancers ?? [];
|
|
798
|
+
|
|
799
|
+
// New API: { preset?: string, design?: object }
|
|
800
|
+
const presetId = inputConfig && inputConfig.preset;
|
|
801
|
+
const designOverrides = inputConfig && inputConfig.design;
|
|
802
|
+
|
|
803
|
+
const hasNewShape =
|
|
804
|
+
"preset" in (inputConfig || {}) ||
|
|
805
|
+
"design" in (inputConfig || {}) ||
|
|
806
|
+
"enhancers" in (inputConfig || {});
|
|
807
|
+
|
|
808
|
+
let generatorConfig;
|
|
809
|
+
let presetInfo = null;
|
|
810
|
+
|
|
811
|
+
if (hasNewShape) {
|
|
812
|
+
// Always resolve a preset; default if none provided
|
|
813
|
+
const effectivePreset = String(presetId || "default").toLowerCase();
|
|
814
|
+
const found =
|
|
815
|
+
presets?.[effectivePreset] ||
|
|
816
|
+
Object.values(presets || {}).find(
|
|
817
|
+
(p) =>
|
|
818
|
+
__slugify(p.name) === effectivePreset ||
|
|
819
|
+
String(p.name || "").toLowerCase() === effectivePreset
|
|
820
|
+
);
|
|
821
|
+
if (!found)
|
|
822
|
+
throw new Error(`PDS preset not found: "${presetId || "default"}"`);
|
|
823
|
+
|
|
824
|
+
presetInfo = {
|
|
825
|
+
id: found.id || __slugify(found.name),
|
|
826
|
+
name: found.name || found.id || String(effectivePreset),
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// Merge preset with design overrides
|
|
830
|
+
let mergedDesign = structuredClone(found);
|
|
831
|
+
if (designOverrides && typeof designOverrides === "object") {
|
|
832
|
+
// Strip functions before cloning to avoid DataCloneError
|
|
833
|
+
const cloneableDesign = __stripFunctions(designOverrides);
|
|
834
|
+
mergedDesign = __deepMerge(mergedDesign, structuredClone(cloneableDesign));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Build structured config with design nested
|
|
838
|
+
// Exclude runtime-specific properties that shouldn't be passed to Generator
|
|
839
|
+
const {
|
|
840
|
+
mode, autoDefine, applyGlobalStyles, manageTheme,
|
|
841
|
+
themeStorageKey, preloadStyles, criticalLayers,
|
|
842
|
+
preset: _preset, design: _design, enhancers: _enhancers,
|
|
843
|
+
log: userLog, // Extract log if provided at root
|
|
844
|
+
...otherProps
|
|
845
|
+
} = inputConfig;
|
|
846
|
+
|
|
847
|
+
generatorConfig = {
|
|
848
|
+
...otherProps, // Keep only generator-relevant properties
|
|
849
|
+
design: mergedDesign,
|
|
850
|
+
preset: presetInfo.name,
|
|
851
|
+
// Add log method at root level (use user's or default)
|
|
852
|
+
log: userLog || defaultLog,
|
|
853
|
+
};
|
|
854
|
+
} else if (hasDesignKeys) {
|
|
855
|
+
// Back-compat: treat the provided object as the full design, wrap it
|
|
856
|
+
// Extract log before cloning to avoid DataCloneError
|
|
857
|
+
const { log: userLog, ...designConfig } = inputConfig;
|
|
858
|
+
generatorConfig = {
|
|
859
|
+
design: structuredClone(designConfig),
|
|
860
|
+
log: userLog || defaultLog,
|
|
861
|
+
};
|
|
862
|
+
} else {
|
|
863
|
+
// Nothing recognizable: use default preset
|
|
864
|
+
const foundDefault =
|
|
865
|
+
presets?.["default"] ||
|
|
866
|
+
Object.values(presets || {}).find((p) => __slugify(p.name) === "default");
|
|
867
|
+
if (!foundDefault) throw new Error("PDS default preset not available");
|
|
868
|
+
presetInfo = {
|
|
869
|
+
id: foundDefault.id || "default",
|
|
870
|
+
name: foundDefault.name || "Default",
|
|
871
|
+
};
|
|
872
|
+
generatorConfig = {
|
|
873
|
+
design: structuredClone(foundDefault),
|
|
874
|
+
preset: presetInfo.name,
|
|
875
|
+
log: defaultLog,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return { generatorConfig, enhancers, presetInfo };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Internal: setup AutoDefiner and run enhancers
|
|
883
|
+
async function __setupAutoDefinerAndEnhancers(options) {
|
|
884
|
+
const {
|
|
885
|
+
autoDefineBaseURL = "/auto-define/",
|
|
886
|
+
autoDefinePreload = [],
|
|
887
|
+
autoDefineMapper = null,
|
|
888
|
+
enhancers = [],
|
|
889
|
+
// New: raw overrides for AutoDefiner config (scanExisting, observeShadows, etc.)
|
|
890
|
+
autoDefineOverrides = null,
|
|
891
|
+
autoDefinePreferModule = true,
|
|
892
|
+
} = options;
|
|
893
|
+
|
|
894
|
+
// // Warn if assets not present (best-effort)
|
|
895
|
+
// try {
|
|
896
|
+
// if (typeof window !== "undefined") {
|
|
897
|
+
// const response = await fetch(`${autoDefineBaseURL}pds-icon.js`, {
|
|
898
|
+
// method: "HEAD",
|
|
899
|
+
// });
|
|
900
|
+
// if (!response.ok) {
|
|
901
|
+
// // No config available in this context, using console
|
|
902
|
+
// console.warn("⚠️ PDS components not found in auto-define directory.");
|
|
903
|
+
// }
|
|
904
|
+
// }
|
|
905
|
+
// } catch {}
|
|
906
|
+
|
|
907
|
+
// Merge defaults with user enhancers (user overrides by selector)
|
|
908
|
+
const mergedEnhancers = (() => {
|
|
909
|
+
const map = new Map();
|
|
910
|
+
(PDS.defaultEnhancers || []).forEach((e) => map.set(e.selector, e));
|
|
911
|
+
(enhancers || []).forEach((e) => map.set(e.selector, e));
|
|
912
|
+
return Array.from(map.values());
|
|
913
|
+
})();
|
|
914
|
+
|
|
915
|
+
// Setup AutoDefiner in browser context (it already observes shadow DOMs)
|
|
916
|
+
let autoDefiner = null;
|
|
917
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
918
|
+
// Dynamically import AutoDefiner to avoid Node/CJS interop at build time
|
|
919
|
+
let AutoDefinerCtor = null;
|
|
920
|
+
try {
|
|
921
|
+
const mod = await import("pure-web/auto-definer");
|
|
922
|
+
AutoDefinerCtor =
|
|
923
|
+
mod?.AutoDefiner || mod?.default?.AutoDefiner || mod?.default || null;
|
|
924
|
+
} catch (e) {
|
|
925
|
+
// No config available in this context, using console
|
|
926
|
+
console.warn("AutoDefiner not available:", e?.message || e);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const defaultMapper = (tag) => {
|
|
930
|
+
switch (tag) {
|
|
931
|
+
case "pds-tabpanel":
|
|
932
|
+
return "pds-tabstrip.js";
|
|
933
|
+
default:
|
|
934
|
+
return `${tag}.js`;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
// Respect user overrides but never allow them to overwrite our mapper wrapper.
|
|
939
|
+
const { mapper: _overrideMapperIgnored, ...restAutoDefineOverrides } =
|
|
940
|
+
autoDefineOverrides && typeof autoDefineOverrides === "object"
|
|
941
|
+
? autoDefineOverrides
|
|
942
|
+
: {};
|
|
943
|
+
|
|
944
|
+
const normalizedBaseURL = autoDefineBaseURL
|
|
945
|
+
? __ensureTrailingSlash(
|
|
946
|
+
__ensureAbsoluteAssetURL(autoDefineBaseURL, {
|
|
947
|
+
preferModule: autoDefinePreferModule,
|
|
948
|
+
})
|
|
949
|
+
)
|
|
950
|
+
: autoDefineBaseURL;
|
|
951
|
+
|
|
952
|
+
const autoDefineConfig = {
|
|
953
|
+
baseURL: normalizedBaseURL,
|
|
954
|
+
predefine: autoDefinePreload,
|
|
955
|
+
scanExisting: true,
|
|
956
|
+
observeShadows: true,
|
|
957
|
+
patchAttachShadow: true,
|
|
958
|
+
debounceMs: 16,
|
|
959
|
+
enhancers: mergedEnhancers,
|
|
960
|
+
onError: (tag, err) => {
|
|
961
|
+
if (typeof tag === "string" && tag.startsWith("pds-")) {
|
|
962
|
+
// No config available in this context, using console
|
|
963
|
+
console.warn(
|
|
964
|
+
`⚠️ PDS component <${tag}> not found. Assets may not be installed.`
|
|
965
|
+
);
|
|
966
|
+
} else {
|
|
967
|
+
console.error(`❌ Auto-define error for <${tag}>:`, err);
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
// Apply all user overrides except mapper so we can still wrap it
|
|
971
|
+
...restAutoDefineOverrides,
|
|
972
|
+
mapper: (tag) => {
|
|
973
|
+
// If already defined, do nothing
|
|
974
|
+
if (customElements.get(tag)) return null;
|
|
975
|
+
|
|
976
|
+
// If a custom mapper exists, let it try first; if it returns a non-value, fallback to default
|
|
977
|
+
if (typeof autoDefineMapper === "function") {
|
|
978
|
+
try {
|
|
979
|
+
const mapped = autoDefineMapper(tag);
|
|
980
|
+
if (mapped === undefined) {
|
|
981
|
+
return defaultMapper(tag);
|
|
982
|
+
}
|
|
983
|
+
return mapped;
|
|
984
|
+
} catch (e) {
|
|
985
|
+
// Be resilient: if custom mapper throws, fall back to default
|
|
986
|
+
console.warn(
|
|
987
|
+
"Custom autoDefine.mapper error; falling back to default:",
|
|
988
|
+
e?.message || e
|
|
989
|
+
);
|
|
990
|
+
return defaultMapper(tag);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// No custom mapper provided — use default
|
|
995
|
+
return defaultMapper(tag);
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
if (AutoDefinerCtor) {
|
|
1000
|
+
autoDefiner = new AutoDefinerCtor(autoDefineConfig);
|
|
1001
|
+
if (
|
|
1002
|
+
autoDefinePreload.length > 0 &&
|
|
1003
|
+
typeof AutoDefinerCtor.define === "function"
|
|
1004
|
+
) {
|
|
1005
|
+
await AutoDefinerCtor.define(...autoDefinePreload, {
|
|
1006
|
+
baseURL: autoDefineBaseURL,
|
|
1007
|
+
mapper: autoDefineConfig.mapper,
|
|
1008
|
+
onError: autoDefineConfig.onError,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// Rely on AutoDefiner to run enhancers across light and shadow DOMs
|
|
1014
|
+
|
|
1015
|
+
return { autoDefiner };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async function live(config) {
|
|
1019
|
+
if (!config || typeof config !== "object") {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
"PDS.start({ mode: 'live', ... }) requires a valid configuration object"
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// FOUC Prevention: Use constructable stylesheet for synchronous, immediate effect
|
|
1026
|
+
if (typeof document !== "undefined" && document.adoptedStyleSheets) {
|
|
1027
|
+
const css = /*css*/`
|
|
1028
|
+
html { opacity: 0; }
|
|
1029
|
+
html.pds-ready { opacity: 1; transition: opacity 0.3s ease-in; }
|
|
1030
|
+
`
|
|
1031
|
+
try {
|
|
1032
|
+
// Check if we've already added the FOUC prevention sheet
|
|
1033
|
+
const hasFoucSheet = document.adoptedStyleSheets.some(sheet => sheet._pdsFouc);
|
|
1034
|
+
if (!hasFoucSheet) {
|
|
1035
|
+
const foucSheet = new CSSStyleSheet();
|
|
1036
|
+
foucSheet.replaceSync(css);
|
|
1037
|
+
foucSheet._pdsFouc = true;
|
|
1038
|
+
document.adoptedStyleSheets = [foucSheet, ...document.adoptedStyleSheets];
|
|
1039
|
+
}
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
// Fallback for browsers that don't support constructable stylesheets
|
|
1042
|
+
// No config available here, using console
|
|
1043
|
+
console.warn("Constructable stylesheets not supported, using <style> tag fallback:", e);
|
|
1044
|
+
const existingFoucStyle = document.head.querySelector("style[data-pds-fouc]");
|
|
1045
|
+
if (!existingFoucStyle) {
|
|
1046
|
+
const foucStyle = document.createElement("style");
|
|
1047
|
+
foucStyle.setAttribute("data-pds-fouc", "");
|
|
1048
|
+
foucStyle.textContent = css;
|
|
1049
|
+
document.head.insertBefore(foucStyle, document.head.firstChild);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// PDS is exposed on window at module init for both modes
|
|
1055
|
+
|
|
1056
|
+
// Extract runtime flags directly from unified config
|
|
1057
|
+
let applyGlobalStyles = config.applyGlobalStyles ?? true;
|
|
1058
|
+
let manageTheme = config.manageTheme ?? true;
|
|
1059
|
+
let themeStorageKey = config.themeStorageKey ?? "pure-ds-theme";
|
|
1060
|
+
let preloadStyles = config.preloadStyles ?? false;
|
|
1061
|
+
let criticalLayers = config.criticalLayers ?? ["tokens", "primitives"];
|
|
1062
|
+
|
|
1063
|
+
// New unified shape: autoDefine inside the first argument
|
|
1064
|
+
const cfgAuto = (config && config.autoDefine) || null;
|
|
1065
|
+
if (cfgAuto && typeof cfgAuto === "object") {
|
|
1066
|
+
// no-op here; resolved below for __setupAutoDefinerAndEnhancers
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
try {
|
|
1070
|
+
// 1) Handle theme preference
|
|
1071
|
+
const { resolvedTheme, storedTheme } = __resolveThemeAndApply({
|
|
1072
|
+
manageTheme,
|
|
1073
|
+
themeStorageKey,
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// 2) Normalize first-arg API: support { preset, design, enhancers }
|
|
1077
|
+
const normalized = __normalizeInitConfig(config, {});
|
|
1078
|
+
const userEnhancers = normalized.enhancers;
|
|
1079
|
+
// Extract log function before cloning to avoid DataCloneError
|
|
1080
|
+
const { log: logFn, ...configToClone } = normalized.generatorConfig;
|
|
1081
|
+
const generatorConfig = structuredClone(configToClone);
|
|
1082
|
+
// Add log back after cloning
|
|
1083
|
+
generatorConfig.log = logFn;
|
|
1084
|
+
if (manageTheme) {
|
|
1085
|
+
generatorConfig.theme = resolvedTheme;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const generator = new PDS.Generator(generatorConfig);
|
|
1089
|
+
|
|
1090
|
+
// 3) Load fonts from Google Fonts if needed (before applying styles)
|
|
1091
|
+
if (generatorConfig.design?.typography) {
|
|
1092
|
+
try {
|
|
1093
|
+
await loadTypographyFonts(generatorConfig.design.typography);
|
|
1094
|
+
} catch (ex) {
|
|
1095
|
+
generatorConfig?.log?.("warn", "Failed to load some fonts from Google Fonts:", ex);
|
|
1096
|
+
// Continue anyway - the system will fall back to default fonts
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// 4) Preload critical styles synchronously to prevent flash
|
|
1101
|
+
if (preloadStyles && typeof window !== "undefined" && document.head) {
|
|
1102
|
+
try {
|
|
1103
|
+
// Generate critical CSS layers synchronously
|
|
1104
|
+
const criticalCSS = criticalLayers
|
|
1105
|
+
.map((layer) => {
|
|
1106
|
+
try {
|
|
1107
|
+
return generator.css?.[layer] || "";
|
|
1108
|
+
} catch (e) {
|
|
1109
|
+
generatorConfig?.log?.(
|
|
1110
|
+
"warn",
|
|
1111
|
+
`Failed to generate critical CSS for layer "${layer}":`,
|
|
1112
|
+
e
|
|
1113
|
+
);
|
|
1114
|
+
return "";
|
|
1115
|
+
}
|
|
1116
|
+
})
|
|
1117
|
+
.filter((css) => css.trim())
|
|
1118
|
+
.join("\n");
|
|
1119
|
+
|
|
1120
|
+
if (criticalCSS) {
|
|
1121
|
+
// Remove any existing PDS critical styles
|
|
1122
|
+
const existingCritical = document.head.querySelector(
|
|
1123
|
+
"style[data-pds-critical]"
|
|
1124
|
+
);
|
|
1125
|
+
if (existingCritical) {
|
|
1126
|
+
existingCritical.remove();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Inject critical CSS as a <style> tag in head
|
|
1130
|
+
const styleEl = document.createElement("style");
|
|
1131
|
+
styleEl.setAttribute("data-pds-critical", "");
|
|
1132
|
+
styleEl.textContent = criticalCSS;
|
|
1133
|
+
|
|
1134
|
+
// Insert early in head, but after charset/viewport if present
|
|
1135
|
+
const insertAfter = document.head.querySelector(
|
|
1136
|
+
'meta[charset], meta[name="viewport"]'
|
|
1137
|
+
);
|
|
1138
|
+
if (insertAfter) {
|
|
1139
|
+
insertAfter.parentNode.insertBefore(
|
|
1140
|
+
styleEl,
|
|
1141
|
+
insertAfter.nextSibling
|
|
1142
|
+
);
|
|
1143
|
+
} else {
|
|
1144
|
+
document.head.insertBefore(styleEl, document.head.firstChild);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
generatorConfig?.log?.("warn", "Failed to preload critical styles:", error);
|
|
1149
|
+
// Continue without critical styles - better than crashing
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Set the registry to use this designer
|
|
1154
|
+
PDS.registry.setDesigner(generator, {
|
|
1155
|
+
presetName: normalized.presetInfo?.name,
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// Apply styles globally if requested (default behavior)
|
|
1159
|
+
if (applyGlobalStyles) {
|
|
1160
|
+
await PDS.Generator.applyStyles(generator);
|
|
1161
|
+
|
|
1162
|
+
// Clean up critical styles after adoptedStyleSheets are applied
|
|
1163
|
+
if (typeof window !== "undefined") {
|
|
1164
|
+
// Small delay to ensure adoptedStyleSheets have taken effect
|
|
1165
|
+
setTimeout(() => {
|
|
1166
|
+
// Remove any previously inlined critical/preload styles that were unlayered
|
|
1167
|
+
const criticalStyle = document.head.querySelector(
|
|
1168
|
+
"style[data-pds-critical]"
|
|
1169
|
+
);
|
|
1170
|
+
if (criticalStyle) criticalStyle.remove();
|
|
1171
|
+
|
|
1172
|
+
const preloadStyle = document.head.querySelector(
|
|
1173
|
+
"style[data-pds-preload]"
|
|
1174
|
+
);
|
|
1175
|
+
if (preloadStyle) preloadStyle.remove();
|
|
1176
|
+
|
|
1177
|
+
// Remove legacy fallback runtime style tag if present
|
|
1178
|
+
const legacyRuntime = document.getElementById(
|
|
1179
|
+
"pds-runtime-stylesheet"
|
|
1180
|
+
);
|
|
1181
|
+
if (legacyRuntime) legacyRuntime.remove();
|
|
1182
|
+
}, 100);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Note: auto-define base URL is used internally; no globals are written
|
|
1187
|
+
|
|
1188
|
+
// Derive a sensible default AutoDefiner base for LIVE mode too when not provided.
|
|
1189
|
+
// Use the normalized public asset root so live and static modes share the same directory layout.
|
|
1190
|
+
const assetRootURL = __resolveRuntimeAssetRoot(config);
|
|
1191
|
+
|
|
1192
|
+
let derivedAutoDefineBaseURL;
|
|
1193
|
+
if (cfgAuto && cfgAuto.baseURL) {
|
|
1194
|
+
derivedAutoDefineBaseURL = __ensureTrailingSlash(
|
|
1195
|
+
__ensureAbsoluteAssetURL(cfgAuto.baseURL, { preferModule: false })
|
|
1196
|
+
);
|
|
1197
|
+
} else {
|
|
1198
|
+
derivedAutoDefineBaseURL = `${assetRootURL}components/`;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// 5) Set up AutoDefiner + run enhancers (defaults merged with user)
|
|
1202
|
+
let autoDefiner = null;
|
|
1203
|
+
try {
|
|
1204
|
+
const res = await __setupAutoDefinerAndEnhancers({
|
|
1205
|
+
autoDefineBaseURL: derivedAutoDefineBaseURL,
|
|
1206
|
+
autoDefinePreload:
|
|
1207
|
+
(cfgAuto && Array.isArray(cfgAuto.predefine) && cfgAuto.predefine) ||
|
|
1208
|
+
[],
|
|
1209
|
+
autoDefineMapper:
|
|
1210
|
+
(cfgAuto && typeof cfgAuto.mapper === "function" && cfgAuto.mapper) ||
|
|
1211
|
+
null,
|
|
1212
|
+
enhancers: userEnhancers,
|
|
1213
|
+
autoDefineOverrides: cfgAuto || null,
|
|
1214
|
+
autoDefinePreferModule: !(cfgAuto && cfgAuto.baseURL),
|
|
1215
|
+
});
|
|
1216
|
+
autoDefiner = res.autoDefiner;
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
generatorConfig?.log?.("error", "❌ Failed to initialize AutoDefiner/Enhancers:", error);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Determine resolved config to expose (generator stores input as options)
|
|
1222
|
+
const resolvedConfig = generator?.options || generatorConfig;
|
|
1223
|
+
|
|
1224
|
+
// Expose current config as frozen read-only on PDS, preserving input shape
|
|
1225
|
+
// Strip functions before cloning to avoid DataCloneError
|
|
1226
|
+
const cloneableConfig = __stripFunctions(config);
|
|
1227
|
+
PDS.currentConfig = Object.freeze({
|
|
1228
|
+
mode: "live",
|
|
1229
|
+
...structuredClone(cloneableConfig),
|
|
1230
|
+
design: structuredClone(normalized.generatorConfig.design),
|
|
1231
|
+
preset: normalized.generatorConfig.preset,
|
|
1232
|
+
theme: resolvedTheme,
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// Emit event to notify that PDS is ready (unified)
|
|
1236
|
+
__emitPDSReady({
|
|
1237
|
+
mode: "live",
|
|
1238
|
+
generator,
|
|
1239
|
+
config: resolvedConfig,
|
|
1240
|
+
theme: resolvedTheme,
|
|
1241
|
+
autoDefiner,
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
return {
|
|
1245
|
+
generator,
|
|
1246
|
+
config: resolvedConfig,
|
|
1247
|
+
theme: resolvedTheme,
|
|
1248
|
+
autoDefiner,
|
|
1249
|
+
};
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
// Emit error event
|
|
1252
|
+
PDS.dispatchEvent(
|
|
1253
|
+
new CustomEvent("pds:error", {
|
|
1254
|
+
detail: { error },
|
|
1255
|
+
})
|
|
1256
|
+
);
|
|
1257
|
+
throw error;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Start PDS given a unified configuration and an explicit mode.
|
|
1263
|
+
*
|
|
1264
|
+
* @param {object} config - Unified configuration
|
|
1265
|
+
* @param {('live'|'static')} [config.mode='live'] - Runtime mode selector
|
|
1266
|
+
* @returns {Promise<any>} Live returns { generator, config, theme, autoDefiner }; Static returns { config, theme, autoDefiner }
|
|
1267
|
+
*/
|
|
1268
|
+
async function start(config) {
|
|
1269
|
+
const mode = (config && config.mode) || "live";
|
|
1270
|
+
const { mode: _omit, ...rest } = config || {};
|
|
1271
|
+
if (mode === "static") return staticInit(rest);
|
|
1272
|
+
return live(rest);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/** Primary unified entry point */
|
|
1276
|
+
PDS.start = start;
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Initialize PDS in static mode with the same unified configuration shape as live mode.
|
|
1281
|
+
*
|
|
1282
|
+
* Shape:
|
|
1283
|
+
* PDS.start({
|
|
1284
|
+
* mode: 'static',
|
|
1285
|
+
* preset?: string,
|
|
1286
|
+
* design?: object,
|
|
1287
|
+
* autoDefine?: {
|
|
1288
|
+
* baseURL?: string,
|
|
1289
|
+
* predefine?: string[],
|
|
1290
|
+
* mapper?: (tag:string)=>string,
|
|
1291
|
+
* // plus any AutoDefiner flags
|
|
1292
|
+
* },
|
|
1293
|
+
* // static/runtime flags (optional)
|
|
1294
|
+
* applyGlobalStyles?: boolean,
|
|
1295
|
+
* manageTheme?: boolean,
|
|
1296
|
+
* themeStorageKey?: string,
|
|
1297
|
+
* staticPaths?: Record<string,string>
|
|
1298
|
+
* })
|
|
1299
|
+
*/
|
|
1300
|
+
async function staticInit(config) {
|
|
1301
|
+
if (!config || typeof config !== "object") {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
"PDS.start({ mode: 'static', ... }) requires a valid configuration object"
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const applyGlobalStyles = config.applyGlobalStyles ?? true;
|
|
1308
|
+
const manageTheme = config.manageTheme ?? true;
|
|
1309
|
+
const themeStorageKey = config.themeStorageKey ?? "pure-ds-theme";
|
|
1310
|
+
let staticPaths = config.staticPaths ?? {};
|
|
1311
|
+
const assetRootURL = __resolveRuntimeAssetRoot(config);
|
|
1312
|
+
const cfgAuto = (config && config.autoDefine) || null;
|
|
1313
|
+
let autoDefineBaseURL;
|
|
1314
|
+
if (cfgAuto && cfgAuto.baseURL) {
|
|
1315
|
+
autoDefineBaseURL = __ensureTrailingSlash(
|
|
1316
|
+
__ensureAbsoluteAssetURL(cfgAuto.baseURL, { preferModule: false })
|
|
1317
|
+
);
|
|
1318
|
+
} else {
|
|
1319
|
+
autoDefineBaseURL = `${assetRootURL}components/`;
|
|
1320
|
+
}
|
|
1321
|
+
const autoDefinePreload =
|
|
1322
|
+
(cfgAuto && Array.isArray(cfgAuto.predefine) && cfgAuto.predefine) || [];
|
|
1323
|
+
const autoDefineMapper =
|
|
1324
|
+
(cfgAuto && typeof cfgAuto.mapper === "function" && cfgAuto.mapper) || null;
|
|
1325
|
+
|
|
1326
|
+
try {
|
|
1327
|
+
// 1) Theme
|
|
1328
|
+
const { resolvedTheme } = __resolveThemeAndApply({
|
|
1329
|
+
manageTheme,
|
|
1330
|
+
themeStorageKey,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
// Normalize first-arg to allow { preset, design, enhancers }
|
|
1334
|
+
const normalized = __normalizeInitConfig(config, {});
|
|
1335
|
+
const userEnhancers = normalized.enhancers;
|
|
1336
|
+
|
|
1337
|
+
// 2) Derive static asset URLs from the normalized public root
|
|
1338
|
+
const baseStaticPaths = {
|
|
1339
|
+
tokens: `${assetRootURL}styles/pds-tokens.css.js`,
|
|
1340
|
+
primitives: `${assetRootURL}styles/pds-primitives.css.js`,
|
|
1341
|
+
components: `${assetRootURL}styles/pds-components.css.js`,
|
|
1342
|
+
utilities: `${assetRootURL}styles/pds-utilities.css.js`,
|
|
1343
|
+
styles: `${assetRootURL}styles/pds-styles.css.js`,
|
|
1344
|
+
};
|
|
1345
|
+
staticPaths = { ...baseStaticPaths, ...staticPaths };
|
|
1346
|
+
|
|
1347
|
+
// 3) Static mode registry
|
|
1348
|
+
PDS.registry.setStaticMode(staticPaths);
|
|
1349
|
+
|
|
1350
|
+
// 4) Apply global static styles if requested
|
|
1351
|
+
if (applyGlobalStyles && typeof document !== "undefined") {
|
|
1352
|
+
try {
|
|
1353
|
+
// Always adopt from exported css.js modules so static mode loads from JS exports
|
|
1354
|
+
const stylesSheet = await PDS.registry.getStylesheet("styles");
|
|
1355
|
+
if (stylesSheet) {
|
|
1356
|
+
// Tag and adopt alongside existing non-PDS sheets
|
|
1357
|
+
stylesSheet._pds = true;
|
|
1358
|
+
const others = (document.adoptedStyleSheets || []).filter(
|
|
1359
|
+
(s) => s._pds !== true
|
|
1360
|
+
);
|
|
1361
|
+
document.adoptedStyleSheets = [...others, stylesSheet];
|
|
1362
|
+
}
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
// No config available in static mode, using console
|
|
1365
|
+
console.warn("Failed to apply static styles:", e);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// 5) AutoDefiner + Enhancers
|
|
1370
|
+
let autoDefiner = null;
|
|
1371
|
+
try {
|
|
1372
|
+
const res = await __setupAutoDefinerAndEnhancers({
|
|
1373
|
+
autoDefineBaseURL,
|
|
1374
|
+
autoDefinePreload,
|
|
1375
|
+
autoDefineMapper,
|
|
1376
|
+
enhancers: userEnhancers,
|
|
1377
|
+
autoDefineOverrides: cfgAuto || null,
|
|
1378
|
+
autoDefinePreferModule: !(cfgAuto && cfgAuto.baseURL),
|
|
1379
|
+
});
|
|
1380
|
+
autoDefiner = res.autoDefiner;
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
// No config available in static mode, using console
|
|
1383
|
+
console.error(
|
|
1384
|
+
"❌ Failed to initialize AutoDefiner/Enhancers (static):",
|
|
1385
|
+
error
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Expose current config as frozen read-only on PDS, preserving input shape
|
|
1390
|
+
// Strip functions before cloning to avoid DataCloneError
|
|
1391
|
+
const cloneableConfig = __stripFunctions(config);
|
|
1392
|
+
PDS.currentConfig = Object.freeze({
|
|
1393
|
+
mode: "static",
|
|
1394
|
+
...structuredClone(cloneableConfig),
|
|
1395
|
+
design: structuredClone(normalized.generatorConfig.design),
|
|
1396
|
+
preset: normalized.generatorConfig.preset,
|
|
1397
|
+
theme: resolvedTheme,
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
// 6) Emit ready event (unified)
|
|
1401
|
+
__emitPDSReady({
|
|
1402
|
+
mode: "static",
|
|
1403
|
+
config: normalized.generatorConfig,
|
|
1404
|
+
theme: resolvedTheme,
|
|
1405
|
+
autoDefiner,
|
|
1406
|
+
});
|
|
1407
|
+
return {
|
|
1408
|
+
config: normalized.generatorConfig,
|
|
1409
|
+
theme: resolvedTheme,
|
|
1410
|
+
autoDefiner,
|
|
1411
|
+
};
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
PDS.dispatchEvent(new CustomEvent("pds:error", { detail: { error } }));
|
|
1414
|
+
throw error;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Note: PDS.static is not exported. Use PDS.start({ mode: 'static', ... }).
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Change the current theme programmatically.
|
|
1422
|
+
* This updates localStorage, the data-theme attribute, and regenerates styles if in live mode.
|
|
1423
|
+
*
|
|
1424
|
+
* @param {string} theme - Theme to apply: 'light', 'dark', or 'system'
|
|
1425
|
+
* @param {object} [options] - Optional settings
|
|
1426
|
+
* @param {string} [options.storageKey='pure-ds-theme'] - localStorage key for theme preference
|
|
1427
|
+
* @param {boolean} [options.persist=true] - Whether to save to localStorage
|
|
1428
|
+
* @returns {Promise<string>} The resolved theme ('light' or 'dark')
|
|
1429
|
+
*/
|
|
1430
|
+
async function setTheme(theme, options = {}) {
|
|
1431
|
+
const { storageKey = "pure-ds-theme", persist = true } = options;
|
|
1432
|
+
|
|
1433
|
+
if (!["light", "dark", "system"].includes(theme)) {
|
|
1434
|
+
throw new Error(
|
|
1435
|
+
`Invalid theme "${theme}". Must be "light", "dark", or "system".`
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (typeof window === "undefined") {
|
|
1440
|
+
return theme === "system" ? "light" : theme;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
let resolvedTheme = theme;
|
|
1444
|
+
|
|
1445
|
+
// Resolve 'system' to actual preference
|
|
1446
|
+
if (theme === "system") {
|
|
1447
|
+
const prefersDark =
|
|
1448
|
+
window.matchMedia &&
|
|
1449
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1450
|
+
resolvedTheme = prefersDark ? "dark" : "light";
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Update data-theme attribute
|
|
1454
|
+
document.documentElement.setAttribute("data-theme", resolvedTheme);
|
|
1455
|
+
|
|
1456
|
+
// Persist to localStorage if requested
|
|
1457
|
+
if (persist) {
|
|
1458
|
+
localStorage.setItem(storageKey, theme);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// If we're in live mode, regenerate styles with new theme
|
|
1462
|
+
if (PDS.registry.isLive && PDS.registry.hasDesigner) {
|
|
1463
|
+
try {
|
|
1464
|
+
const currentDesigner = PDS.registry._designer; // Access internal designer
|
|
1465
|
+
if (currentDesigner && currentDesigner.configure) {
|
|
1466
|
+
// Update the designer's config with new theme
|
|
1467
|
+
const newConfig = { ...currentDesigner.config, theme: resolvedTheme };
|
|
1468
|
+
currentDesigner.configure(newConfig);
|
|
1469
|
+
|
|
1470
|
+
// Reapply styles
|
|
1471
|
+
await PDS.Generator.applyStyles(currentDesigner);
|
|
1472
|
+
}
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
currentDesigner?.options?.log?.("warn", "Failed to update styles for new theme:", error);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Emit theme change event (unified)
|
|
1479
|
+
PDS.dispatchEvent(
|
|
1480
|
+
new CustomEvent("pds:theme:changed", {
|
|
1481
|
+
detail: {
|
|
1482
|
+
theme: resolvedTheme,
|
|
1483
|
+
requested: theme,
|
|
1484
|
+
source: "programmatic",
|
|
1485
|
+
},
|
|
1486
|
+
})
|
|
1487
|
+
);
|
|
1488
|
+
|
|
1489
|
+
return resolvedTheme;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/** Change the current theme programmatically */
|
|
1493
|
+
PDS.setTheme = setTheme;
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Preload minimal CSS to prevent flash of unstyled content.
|
|
1497
|
+
* Call this BEFORE any DOM content is rendered for best results.
|
|
1498
|
+
* This is a lightweight alternative to full PDS.start({ mode: 'live' }) initialization.
|
|
1499
|
+
*
|
|
1500
|
+
* @param {object} config - Minimal PDS config (colors at minimum)
|
|
1501
|
+
* @param {object} [options] - Optional settings
|
|
1502
|
+
* @param {string} [options.theme] - Theme to generate for ('light', 'dark', or 'system')
|
|
1503
|
+
* @param {string[]} [options.layers=['tokens']] - Which CSS layers to preload
|
|
1504
|
+
* @returns {void}
|
|
1505
|
+
*
|
|
1506
|
+
* @example
|
|
1507
|
+
* ```html
|
|
1508
|
+
* <script type="module">
|
|
1509
|
+
* import { PDS } from 'pure-ds/pds-core';
|
|
1510
|
+
* // Call immediately to prevent flash
|
|
1511
|
+
* PDS.preloadCritical({ colors: { primary: '#007acc' } });
|
|
1512
|
+
* </script>
|
|
1513
|
+
* ```
|
|
1514
|
+
*/
|
|
1515
|
+
function preloadCritical(config, options = {}) {
|
|
1516
|
+
if (typeof window === "undefined" || !document.head || !config) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const { theme, layers = ["tokens"] } = options;
|
|
1521
|
+
|
|
1522
|
+
try {
|
|
1523
|
+
// Resolve theme quickly
|
|
1524
|
+
let resolvedTheme = theme || "light";
|
|
1525
|
+
if (theme === "system" || !theme) {
|
|
1526
|
+
const prefersDark =
|
|
1527
|
+
window.matchMedia &&
|
|
1528
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1529
|
+
resolvedTheme = prefersDark ? "dark" : "light";
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Set theme attribute immediately
|
|
1533
|
+
document.documentElement.setAttribute("data-theme", resolvedTheme);
|
|
1534
|
+
|
|
1535
|
+
// Generate minimal CSS synchronously
|
|
1536
|
+
// Normalize config to ensure design property exists
|
|
1537
|
+
const tempConfig = config.design
|
|
1538
|
+
? { ...config, theme: resolvedTheme }
|
|
1539
|
+
: { design: config, theme: resolvedTheme };
|
|
1540
|
+
const tempGenerator = new PDS.Generator(tempConfig);
|
|
1541
|
+
|
|
1542
|
+
const criticalCSS = layers
|
|
1543
|
+
.map((layer) => {
|
|
1544
|
+
try {
|
|
1545
|
+
return tempGenerator.css?.[layer] || "";
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
return "";
|
|
1548
|
+
}
|
|
1549
|
+
})
|
|
1550
|
+
.filter((css) => css.trim())
|
|
1551
|
+
.join("\n");
|
|
1552
|
+
|
|
1553
|
+
if (criticalCSS) {
|
|
1554
|
+
// Remove any existing critical styles
|
|
1555
|
+
const existing = document.head.querySelector("style[data-pds-preload]");
|
|
1556
|
+
if (existing) existing.remove();
|
|
1557
|
+
|
|
1558
|
+
// Inject immediately
|
|
1559
|
+
const styleEl = document.createElement("style");
|
|
1560
|
+
styleEl.setAttribute("data-pds-preload", "");
|
|
1561
|
+
styleEl.textContent = criticalCSS;
|
|
1562
|
+
|
|
1563
|
+
// Insert as early as possible
|
|
1564
|
+
document.head.insertBefore(styleEl, document.head.firstChild);
|
|
1565
|
+
}
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
// Fail silently - better than blocking page load
|
|
1568
|
+
// No config available in this context, using console
|
|
1569
|
+
console.warn("PDS preload failed:", error);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/** Preload minimal CSS to prevent flash of unstyled content */
|
|
1574
|
+
PDS.preloadCritical = preloadCritical;
|
|
1575
|
+
|
|
1576
|
+
// Note: PDS object is not frozen to allow runtime properties like currentConfig
|
|
1577
|
+
// to be set during initialization. The config object itself is frozen for immutability.
|
|
1578
|
+
|
|
1579
|
+
export { PDS, validateDesign };
|