@pure-ds/storybook 0.1.3 → 0.1.5
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/default-pds.config.js +1 -0
- package/dist/pds-reference.json +1 -1
- package/package.json +3 -2
- package/public/assets/js/app.js +486 -9855
- package/public/assets/js/lit.js +3 -1048
- package/public/assets/js/pds.js +309 -6687
- package/scripts/package-build.js +7 -0
- package/src/js/pds-configurator/figma-export.js +153 -0
- package/src/js/pds-configurator/pds-config-form.js +1058 -0
- package/src/js/pds-configurator/pds-configurator.js +22 -0
- package/src/js/pds-configurator/pds-demo.js +3621 -0
- package/stories/components/PdsDrawer.stories.js +19 -602
- package/stories/components/PdsIcon.stories.js +6 -22
- package/stories/components/PdsTabstrip.stories.js +26 -434
- package/stories/foundations/Colors.stories.js +75 -240
- package/stories/foundations/Icons.stories.js +177 -287
- package/stories/foundations/Spacing.stories.js +57 -161
- package/stories/foundations/Typography.stories.js +68 -945
- package/stories/primitives/Alerts.stories.js +31 -25
- package/stories/primitives/Badges.stories.js +35 -146
- package/stories/primitives/Buttons.stories.js +85 -213
- package/stories/primitives/Cards.stories.js +53 -330
- package/stories/primitives/Forms.stories.js +161 -92
- package/public/assets/js/app.js.map +0 -7
- package/public/assets/js/lit.js.map +0 -7
- package/public/assets/js/pds.js.map +0 -7
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { LitElement, html, nothing } from "../../../src/js/lit.js";
|
|
2
|
+
//import { config } from "../config";
|
|
3
|
+
import { Generator } from "../../../src/js/pds-core/pds-generator.js";
|
|
4
|
+
import { presets } from "../../../src/js/pds-core/pds-config.js";
|
|
5
|
+
import { PDS, validateDesign } from "../../../src/js/pds.js";
|
|
6
|
+
import { deepMerge } from "../../../src/js/common/common.js";
|
|
7
|
+
import { loadTypographyFonts } from "../../../src/js/common/font-loader.js";
|
|
8
|
+
import { AutoComplete } from "pure-web/ac";
|
|
9
|
+
import { figmafyTokens } from "./figma-export.js";
|
|
10
|
+
const STORAGE_KEY = "pure-ds-config";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async function toast(message, options = {}) {
|
|
15
|
+
let toaster = document.querySelector("#global-toaster");
|
|
16
|
+
if (!toaster) {
|
|
17
|
+
toaster = document.createElement("pds-toaster");
|
|
18
|
+
toaster.id = "global-toaster";
|
|
19
|
+
document.body.appendChild(toaster);
|
|
20
|
+
await customElements.whenDefined("pds-toaster");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return toaster.toast(message, options);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
customElements.define(
|
|
27
|
+
"pds-config-form",
|
|
28
|
+
class extends LitElement {
|
|
29
|
+
#tmr;
|
|
30
|
+
#lastDesignEmit = 0;
|
|
31
|
+
#scheduledDesignEmit = null;
|
|
32
|
+
#scheduledApply = null;
|
|
33
|
+
#designEmitDelay = 500; // ms throttle
|
|
34
|
+
|
|
35
|
+
static properties = {
|
|
36
|
+
config: { type: Object, state: true },
|
|
37
|
+
schema: { type: Object, state: true },
|
|
38
|
+
mode: { type: String },
|
|
39
|
+
inspectorMode: { type: Boolean, state: true },
|
|
40
|
+
formValues: { type: Object, state: true }, // Filtered values for the form
|
|
41
|
+
validationIssues: { type: Array, state: true }, // Accessibility/design issues from PDS.validateDesign
|
|
42
|
+
formKey: { type: Number, state: true }, // Force form re-render
|
|
43
|
+
showInspector: { type: Boolean, attribute: "show-inspector" }, // Show/hide Code Inspector button
|
|
44
|
+
showPresetSelector: { type: Boolean, attribute: "show-preset-selector" }, // Show/hide Preset selector
|
|
45
|
+
showThemeSelector: { type: Boolean, attribute: "show-theme-selector" }, // Show/hide Theme selector
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
createRenderRoot() {
|
|
49
|
+
return this; // Disable shadow DOM
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
connectedCallback() {
|
|
53
|
+
super.connectedCallback();
|
|
54
|
+
this.formKey = 0;
|
|
55
|
+
this._validationToastId = null;
|
|
56
|
+
|
|
57
|
+
this.mode = "simple";
|
|
58
|
+
this.inspectorMode = false;
|
|
59
|
+
|
|
60
|
+
// Boolean attributes: when present they're true, when absent they're false
|
|
61
|
+
// No need to set defaults - Lit handles boolean attributes correctly
|
|
62
|
+
|
|
63
|
+
this.config = this.loadConfig();
|
|
64
|
+
|
|
65
|
+
// Listen for inspector deactivation requests from the unified PDS bus
|
|
66
|
+
this._inspectorDeactivateHandler = () => {
|
|
67
|
+
if (this.inspectorMode) {
|
|
68
|
+
this.inspectorMode = false;
|
|
69
|
+
this.dispatchInspectorModeChange();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
PDS.addEventListener(
|
|
73
|
+
"pds:inspector:deactivate",
|
|
74
|
+
this._inspectorDeactivateHandler
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Let PDS manage the theme: apply the resolved theme and enable system listener
|
|
78
|
+
try {
|
|
79
|
+
PDS._applyResolvedTheme(PDS.theme);
|
|
80
|
+
PDS._setupSystemListenerIfNeeded(PDS.theme);
|
|
81
|
+
} catch (ex) {
|
|
82
|
+
/* ignore if document not available or other errors */
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.updateForm();
|
|
86
|
+
|
|
87
|
+
// Apply host-level CSS variable overrides from the default design config
|
|
88
|
+
// so the designer UI doesn't visually change when user edits are applied
|
|
89
|
+
// elsewhere (these variables remain fixed for the designer element).
|
|
90
|
+
this.applyDefaultHostVariables();
|
|
91
|
+
// Defer initial validation until after the form has rendered
|
|
92
|
+
this._initialValidationScheduled = false;
|
|
93
|
+
this._loadingToastShown = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
disconnectedCallback() {
|
|
97
|
+
super.disconnectedCallback();
|
|
98
|
+
if (this._inspectorDeactivateHandler) {
|
|
99
|
+
PDS.removeEventListener(
|
|
100
|
+
"pds:inspector:deactivate",
|
|
101
|
+
this._inspectorDeactivateHandler
|
|
102
|
+
);
|
|
103
|
+
this._inspectorDeactivateHandler = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Apply CSS custom properties to the designer host derived from
|
|
109
|
+
* the default preset. This locks spacing and typography variables
|
|
110
|
+
* for the configurator UI so it remains stable while editing.
|
|
111
|
+
*/
|
|
112
|
+
applyDefaultHostVariables() {
|
|
113
|
+
try {
|
|
114
|
+
const baseConfig = structuredClone(presets.default);
|
|
115
|
+
const tmpDesigner = new Generator({ design: baseConfig });
|
|
116
|
+
|
|
117
|
+
// Spacing tokens (keys are numeric strings 1..N)
|
|
118
|
+
const spacing = tmpDesigner.generateSpacingTokens(
|
|
119
|
+
baseConfig.spatialRhythm || {}
|
|
120
|
+
);
|
|
121
|
+
Object.entries(spacing).forEach(([key, val]) => {
|
|
122
|
+
try {
|
|
123
|
+
this.style.setProperty(`--spacing-${key}`, val);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
/* ignore per-property failures */
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Also expose base unit explicitly (px) so consumers can read it directly
|
|
130
|
+
const baseUnitValue =
|
|
131
|
+
(baseConfig.spatialRhythm && baseConfig.spatialRhythm.baseUnit) || 16;
|
|
132
|
+
this.style.setProperty("--base-unit", `${baseUnitValue}px`);
|
|
133
|
+
|
|
134
|
+
// Typography tokens
|
|
135
|
+
const typography = tmpDesigner.generateTypographyTokens(
|
|
136
|
+
baseConfig.typography || {}
|
|
137
|
+
);
|
|
138
|
+
if (typography.fontSize) {
|
|
139
|
+
Object.entries(typography.fontSize).forEach(([k, v]) => {
|
|
140
|
+
this.style.setProperty(`--font-size-${k}`, v);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Also expose the numeric baseFontSize explicitly
|
|
144
|
+
const baseFontSizeValue =
|
|
145
|
+
(baseConfig.typography && baseConfig.typography.baseFontSize) || 16;
|
|
146
|
+
this.style.setProperty("--base-font-size", `${baseFontSizeValue}px`);
|
|
147
|
+
if (typography.fontFamily) {
|
|
148
|
+
Object.entries(typography.fontFamily).forEach(([k, v]) => {
|
|
149
|
+
this.style.setProperty(`--font-family-${k}`, v);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (typography.lineHeight) {
|
|
153
|
+
Object.entries(typography.lineHeight).forEach(([k, v]) => {
|
|
154
|
+
this.style.setProperty(`--font-lineHeight-${k}`, v);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.debug("pds-config-form: applied default host CSS variables");
|
|
159
|
+
} catch (ex) {
|
|
160
|
+
console.warn(
|
|
161
|
+
"pds-config-form: failed to apply default host variables",
|
|
162
|
+
ex
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
updateForm() {
|
|
168
|
+
this.schema = null; // Reset schema to show loading state
|
|
169
|
+
fetch(`/assets/data/auto-design-${this.mode}.json`)
|
|
170
|
+
.then((response) => response.json())
|
|
171
|
+
.then((data) => {
|
|
172
|
+
this.schema = data;
|
|
173
|
+
// Sync form values to current config filtered by schema
|
|
174
|
+
this.formValues = this.filterConfigForSchema(this.config);
|
|
175
|
+
// Force form re-render if needed
|
|
176
|
+
this.formKey = (this.formKey || 0) + 1;
|
|
177
|
+
})
|
|
178
|
+
.catch((ex) => {
|
|
179
|
+
console.warn("Failed to load schema:", ex);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- Storage helpers: preset + overrides ---
|
|
184
|
+
// Deep equality for primitives/arrays/objects
|
|
185
|
+
_isEqual(a, b) {
|
|
186
|
+
if (a === b) return true;
|
|
187
|
+
if (typeof a !== typeof b) return false;
|
|
188
|
+
if (a && b && typeof a === "object") {
|
|
189
|
+
if (Array.isArray(a)) {
|
|
190
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
191
|
+
for (let i = 0; i < a.length; i++)
|
|
192
|
+
if (!this._isEqual(a[i], b[i])) return false;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
const aKeys = Object.keys(a);
|
|
196
|
+
const bKeys = Object.keys(b);
|
|
197
|
+
if (aKeys.length !== bKeys.length) {
|
|
198
|
+
// lengths can differ but still be equal when we only compare a -> b; we will compare per-key below
|
|
199
|
+
}
|
|
200
|
+
for (const k of new Set([...aKeys, ...bKeys])) {
|
|
201
|
+
if (!this._isEqual(a[k], b[k])) return false;
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
_deepDiff(base, target) {
|
|
209
|
+
// Return only properties in target that differ from base
|
|
210
|
+
if (this._isEqual(base, target)) return undefined;
|
|
211
|
+
if (
|
|
212
|
+
base &&
|
|
213
|
+
target &&
|
|
214
|
+
typeof base === "object" &&
|
|
215
|
+
typeof target === "object" &&
|
|
216
|
+
!Array.isArray(base) &&
|
|
217
|
+
!Array.isArray(target)
|
|
218
|
+
) {
|
|
219
|
+
const out = {};
|
|
220
|
+
const keys = new Set([...Object.keys(target)]);
|
|
221
|
+
for (const k of keys) {
|
|
222
|
+
const d = this._deepDiff(base[k], target[k]);
|
|
223
|
+
if (d !== undefined) out[k] = d;
|
|
224
|
+
}
|
|
225
|
+
return Object.keys(out).length ? out : undefined;
|
|
226
|
+
}
|
|
227
|
+
// For arrays or primitives, return the target if not equal
|
|
228
|
+
return this._isEqual(base, target) ? undefined : target;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_resolvePresetBase(presetId) {
|
|
232
|
+
const id = String(presetId || "default").toLowerCase();
|
|
233
|
+
const preset = presets?.[id] || presets?.["default"];
|
|
234
|
+
return preset
|
|
235
|
+
? JSON.parse(JSON.stringify(preset))
|
|
236
|
+
: JSON.parse(JSON.stringify(presets.default));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
loadConfig() {
|
|
240
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
241
|
+
if (stored) {
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(stored);
|
|
244
|
+
// New shape: { preset?: string, design?: object }
|
|
245
|
+
if (parsed && ("preset" in parsed || "design" in parsed)) {
|
|
246
|
+
const presetId = parsed.preset || "default";
|
|
247
|
+
const base = this._resolvePresetBase(presetId);
|
|
248
|
+
const full = deepMerge(base, parsed.design || {});
|
|
249
|
+
this._stored = { preset: presetId, design: parsed.design || {} };
|
|
250
|
+
this._legacy = false;
|
|
251
|
+
return full;
|
|
252
|
+
}
|
|
253
|
+
// Legacy: merged full config
|
|
254
|
+
const merged = deepMerge(config.design, parsed);
|
|
255
|
+
this._stored = null;
|
|
256
|
+
this._legacy = true;
|
|
257
|
+
return merged;
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.warn("Failed to parse stored config, using defaults", e);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
this._stored = { preset: "default", design: {} };
|
|
263
|
+
this._legacy = false;
|
|
264
|
+
return JSON.parse(JSON.stringify(this._resolvePresetBase("default")));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
saveConfig() {
|
|
268
|
+
try {
|
|
269
|
+
// Determine baseline: preset base if known, else default
|
|
270
|
+
const presetId = (this._stored && this._stored.preset) || "default";
|
|
271
|
+
const base = this._resolvePresetBase(presetId);
|
|
272
|
+
const overrides = this._deepDiff(base, this.config) || {};
|
|
273
|
+
const toStore = { preset: presetId, design: overrides };
|
|
274
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
|
275
|
+
this._stored = toStore;
|
|
276
|
+
this._legacy = false;
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.warn("Failed to save config: ", e);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
updated(changedProps) {
|
|
283
|
+
if (changedProps.has("schema")) {
|
|
284
|
+
// When schema changes (mode switch), update form values
|
|
285
|
+
this.formValues = this.filterConfigForSchema(this.config);
|
|
286
|
+
|
|
287
|
+
const delay = this._initialValidationScheduled ? 50 : 150;
|
|
288
|
+
this._initialValidationScheduled = true;
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
try {
|
|
291
|
+
this.applyStyles(true);
|
|
292
|
+
} catch (ex) {
|
|
293
|
+
console.warn("Delayed applyStyles failed:", ex);
|
|
294
|
+
}
|
|
295
|
+
}, delay);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async applyStyles(useUserConfig = false) {
|
|
300
|
+
// Runtime baseline: if using user config, build from preset base + overrides; else default
|
|
301
|
+
let baseConfig = structuredClone(presets.default);
|
|
302
|
+
if (useUserConfig && this.config) {
|
|
303
|
+
const presetId = (this._stored && this._stored.preset) || "default";
|
|
304
|
+
const presetBase = this._resolvePresetBase(presetId);
|
|
305
|
+
baseConfig = deepMerge(presetBase, this.config);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Load fonts from Google Fonts if needed (before validation and style generation)
|
|
309
|
+
if (baseConfig.typography) {
|
|
310
|
+
try {
|
|
311
|
+
await loadTypographyFonts(baseConfig.typography);
|
|
312
|
+
} catch (ex) {
|
|
313
|
+
console.warn("Failed to load some fonts from Google Fonts:", ex);
|
|
314
|
+
// Continue anyway - the system will fall back to default fonts
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Validate before generating/applying styles
|
|
319
|
+
try {
|
|
320
|
+
const v = validateDesign(baseConfig);
|
|
321
|
+
if (!v.ok) {
|
|
322
|
+
this.validationIssues = v.issues;
|
|
323
|
+
if (!this._validationToastId) {
|
|
324
|
+
const summary = v.issues
|
|
325
|
+
.slice(0, 3)
|
|
326
|
+
.map((i) => `• ${i.message}`)
|
|
327
|
+
.join("\n");
|
|
328
|
+
this._validationToastId = toast(
|
|
329
|
+
`Design has accessibility issues. Fix before applying.\n${summary}`,
|
|
330
|
+
{ type: "error", persistent: true }
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return; // Do not apply invalid styles
|
|
334
|
+
}
|
|
335
|
+
// Clear any existing persistent error toast
|
|
336
|
+
this.validationIssues = [];
|
|
337
|
+
if (this._validationToastId) {
|
|
338
|
+
document
|
|
339
|
+
.querySelector("#global-toaster")
|
|
340
|
+
?.dismissToast(this._validationToastId);
|
|
341
|
+
this._validationToastId = null;
|
|
342
|
+
}
|
|
343
|
+
} catch (ex) {
|
|
344
|
+
console.warn("Validation failed unexpectedly:", ex);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Pass explicit theme option (from PDS.theme) separately so
|
|
348
|
+
// configs remain theme-agnostic while Generator can emit the correct
|
|
349
|
+
// scoping (html[data-theme] vs prefers-color-scheme).
|
|
350
|
+
const storedTheme = PDS.theme || null;
|
|
351
|
+
|
|
352
|
+
const generatorOptions = { design: structuredClone(baseConfig) };
|
|
353
|
+
if (storedTheme) generatorOptions.theme = storedTheme;
|
|
354
|
+
|
|
355
|
+
this.generator = new Generator(generatorOptions);
|
|
356
|
+
Generator.applyStyles(this.generator);
|
|
357
|
+
|
|
358
|
+
// Let PDS ensure document html[data-theme] reflects persisted preference
|
|
359
|
+
try {
|
|
360
|
+
PDS._applyResolvedTheme(PDS.theme);
|
|
361
|
+
PDS._setupSystemListenerIfNeeded(PDS.theme);
|
|
362
|
+
} catch (ex) {
|
|
363
|
+
/* ignore in non-browser environments */
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Emit design-updated in a throttled manner with the actual designer
|
|
367
|
+
this.scheduleDesignUpdatedEmit({
|
|
368
|
+
config: baseConfig,
|
|
369
|
+
designer: this.generator,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Centralized throttled emitter for the `design-updated` event.
|
|
375
|
+
* Accepts an object with { config, designer } to include in the event detail.
|
|
376
|
+
*/
|
|
377
|
+
scheduleDesignUpdatedEmit(detail) {
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
|
|
380
|
+
const emitNow = () => {
|
|
381
|
+
this.#lastDesignEmit = Date.now();
|
|
382
|
+
// Clear any scheduled timer
|
|
383
|
+
if (this.#scheduledDesignEmit) {
|
|
384
|
+
clearTimeout(this.#scheduledDesignEmit);
|
|
385
|
+
this.#scheduledDesignEmit = null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
PDS.dispatchEvent(
|
|
389
|
+
new CustomEvent("pds:design:updated", {
|
|
390
|
+
detail,
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
if (now - this.#lastDesignEmit >= this.#designEmitDelay) {
|
|
396
|
+
// Enough time has passed — emit immediately
|
|
397
|
+
emitNow();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Otherwise schedule a trailing emit (only one scheduled at a time)
|
|
402
|
+
if (!this.#scheduledDesignEmit) {
|
|
403
|
+
const delay = this.#designEmitDelay - (now - this.#lastDesignEmit);
|
|
404
|
+
this.#scheduledDesignEmit = setTimeout(() => {
|
|
405
|
+
this.#scheduledDesignEmit = null;
|
|
406
|
+
emitNow();
|
|
407
|
+
}, delay);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
toggleInspectorMode() {
|
|
412
|
+
this.inspectorMode = !this.inspectorMode;
|
|
413
|
+
this.dispatchInspectorModeChange();
|
|
414
|
+
|
|
415
|
+
toast(
|
|
416
|
+
this.inspectorMode
|
|
417
|
+
? "Code Inspector active - click any element in the showcase to view its code"
|
|
418
|
+
: "Code Inspector deactivated",
|
|
419
|
+
{ type: "info", duration: 3000 }
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
dispatchInspectorModeChange() {
|
|
424
|
+
// Dispatch event to notify showcase (unified)
|
|
425
|
+
PDS.dispatchEvent(
|
|
426
|
+
new CustomEvent("pds:inspector:mode:changed", {
|
|
427
|
+
detail: { active: this.inspectorMode },
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Flatten nested config to dot-notation for pds-jsonform
|
|
433
|
+
flattenConfig(obj, prefix = "") {
|
|
434
|
+
const flattened = {};
|
|
435
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
436
|
+
// Use JSON Pointer format with / separator
|
|
437
|
+
const newKey = prefix ? `${prefix}/${key}` : `/${key}`;
|
|
438
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
439
|
+
Object.assign(flattened, this.flattenConfig(value, newKey));
|
|
440
|
+
} else {
|
|
441
|
+
flattened[newKey] = value;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return flattened;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Get schema property paths in JSON Pointer format for pds-jsonform
|
|
448
|
+
getSchemaProperties(schema, prefix = "") {
|
|
449
|
+
const paths = new Set();
|
|
450
|
+
if (!schema || !schema.properties) return paths;
|
|
451
|
+
|
|
452
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
453
|
+
const jsonPointerPath = prefix ? `${prefix}/${key}` : `/${key}`;
|
|
454
|
+
paths.add(jsonPointerPath);
|
|
455
|
+
|
|
456
|
+
if (value.type === "object" && value.properties) {
|
|
457
|
+
const nested = this.getSchemaProperties(value, jsonPointerPath);
|
|
458
|
+
nested.forEach((p) => paths.add(p));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return paths;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Filter config values to only include those that exist in the current schema
|
|
465
|
+
filterConfigForSchema(config) {
|
|
466
|
+
if (!config) {
|
|
467
|
+
return {};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!this.schema) {
|
|
471
|
+
// If schema isn't loaded yet, return full flattened config
|
|
472
|
+
return this.flattenConfig(config);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const validPaths = this.getSchemaProperties(this.schema);
|
|
476
|
+
const flattened = this.flattenConfig(config);
|
|
477
|
+
const filtered = {};
|
|
478
|
+
|
|
479
|
+
for (const [key, value] of Object.entries(flattened)) {
|
|
480
|
+
if (validPaths.has(key) && value !== null && value !== undefined) {
|
|
481
|
+
filtered[key] = value;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return filtered;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
handleFormChange = (event) => {
|
|
489
|
+
// Get values from the pds-jsonform's serialize method or from event detail
|
|
490
|
+
let values;
|
|
491
|
+
let changedField = null;
|
|
492
|
+
|
|
493
|
+
// Capture which field changed for smart scrolling
|
|
494
|
+
if (event.type === "pw:value-change" && event.detail) {
|
|
495
|
+
changedField = event.detail.name;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (event.detail && event.detail.json) {
|
|
499
|
+
// pw:serialize event provides { json, formData, valid, issues }
|
|
500
|
+
values = event.detail.json;
|
|
501
|
+
} else {
|
|
502
|
+
// pw:value-change event - get values directly from the form element
|
|
503
|
+
const form =
|
|
504
|
+
event.currentTarget?.tagName?.toUpperCase() === "PDS-JSONFORM"
|
|
505
|
+
? event.currentTarget
|
|
506
|
+
: this.querySelector("pds-jsonform");
|
|
507
|
+
|
|
508
|
+
if (form) {
|
|
509
|
+
// Use getValuesFlat() to get JSON Pointer formatted keys
|
|
510
|
+
values = form.getValuesFlat?.() || form.values || {};
|
|
511
|
+
} else {
|
|
512
|
+
console.warn("No form element found in form change event", event);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
console.log("Form values received:", values);
|
|
518
|
+
|
|
519
|
+
// Convert flattened dot-notation or JSON-pointer keys to nested structure
|
|
520
|
+
// e.g., { "colors.primary": "#123" } => { colors: { primary: "#123" } }
|
|
521
|
+
// and { "/colors/primary": "#123" } => { colors: { primary: "#123" } }
|
|
522
|
+
const nestedValues = {};
|
|
523
|
+
const unescapePointer = (seg) =>
|
|
524
|
+
seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
525
|
+
|
|
526
|
+
for (const [key, value] of Object.entries(values)) {
|
|
527
|
+
if (!key) continue;
|
|
528
|
+
|
|
529
|
+
if (key.startsWith("/")) {
|
|
530
|
+
// JSON Pointer style
|
|
531
|
+
const raw = key.replace(/^\//, "");
|
|
532
|
+
const parts = raw.split("/").map(unescapePointer);
|
|
533
|
+
let current = nestedValues;
|
|
534
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
535
|
+
const p = parts[i];
|
|
536
|
+
if (!current[p] || typeof current[p] !== "object") current[p] = {};
|
|
537
|
+
current = current[p];
|
|
538
|
+
}
|
|
539
|
+
current[parts[parts.length - 1]] = value;
|
|
540
|
+
} else if (key.includes(".")) {
|
|
541
|
+
const parts = key.split(".");
|
|
542
|
+
let current = nestedValues;
|
|
543
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
544
|
+
if (!current[parts[i]] || typeof current[parts[i]] !== "object")
|
|
545
|
+
current[parts[i]] = {};
|
|
546
|
+
current = current[parts[i]];
|
|
547
|
+
}
|
|
548
|
+
current[parts[parts.length - 1]] = value;
|
|
549
|
+
} else {
|
|
550
|
+
nestedValues[key] = value;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
console.log("Nested values:", nestedValues);
|
|
555
|
+
|
|
556
|
+
// Validate candidate config before persisting/applying
|
|
557
|
+
const candidate = deepMerge(structuredClone(this.config), nestedValues);
|
|
558
|
+
const validation = PDS.validateDesign(candidate);
|
|
559
|
+
if (!validation.ok) {
|
|
560
|
+
this.validationIssues = validation.issues;
|
|
561
|
+
// Show persistent toaster once
|
|
562
|
+
if (!this._validationToastId) {
|
|
563
|
+
const summary = validation.issues
|
|
564
|
+
.slice(0, 3)
|
|
565
|
+
.map((i) => `• ${i.message}`)
|
|
566
|
+
.join("\n");
|
|
567
|
+
this._validationToastId = toast(
|
|
568
|
+
`Design has accessibility issues. Fix before saving.\n${summary}`,
|
|
569
|
+
{ type: "error" }
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return; // Do not persist or apply invalid config
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
this.validationIssues = [];
|
|
576
|
+
// Clear persistent toast if present
|
|
577
|
+
try {
|
|
578
|
+
if (this._validationToastId) {
|
|
579
|
+
document
|
|
580
|
+
.querySelector("#global-toaster")
|
|
581
|
+
?.dismissToast(this._validationToastId);
|
|
582
|
+
this._validationToastId = null;
|
|
583
|
+
}
|
|
584
|
+
} catch {}
|
|
585
|
+
// Accept new config and persist
|
|
586
|
+
this.config = candidate;
|
|
587
|
+
console.log("Updated (persisted) config (not applied):", this.config);
|
|
588
|
+
this.saveConfig();
|
|
589
|
+
|
|
590
|
+
// Emit event for showcase to scroll to relevant section
|
|
591
|
+
if (changedField) {
|
|
592
|
+
console.log(
|
|
593
|
+
"🔔 Emitting design-field-changed event for field:",
|
|
594
|
+
changedField
|
|
595
|
+
);
|
|
596
|
+
PDS.dispatchEvent(
|
|
597
|
+
new CustomEvent("pds:design:field:changed", {
|
|
598
|
+
detail: {
|
|
599
|
+
field: changedField,
|
|
600
|
+
config: this.config,
|
|
601
|
+
},
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
// Debounce applying styles using the user-edited config so the showcase
|
|
606
|
+
// receives the actual runtime CSS generated from user edits. We still
|
|
607
|
+
// persist immediately, but only apply (and emit) the styles at most once
|
|
608
|
+
// per #designEmitDelay to avoid excessive recalculation.
|
|
609
|
+
try {
|
|
610
|
+
if (this.#scheduledApply) {
|
|
611
|
+
clearTimeout(this.#scheduledApply);
|
|
612
|
+
this.#scheduledApply = null;
|
|
613
|
+
}
|
|
614
|
+
this.#scheduledApply = setTimeout(() => {
|
|
615
|
+
this.#scheduledApply = null;
|
|
616
|
+
this.applyStyles(true); // apply with user config and emit
|
|
617
|
+
}, this.#designEmitDelay);
|
|
618
|
+
} catch (ex) {
|
|
619
|
+
console.warn("Failed to schedule applyStyles with user config:", ex);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
handleReset = async () => {
|
|
624
|
+
const result = await PDS.ask(
|
|
625
|
+
"Reset to default configuration? This will clear your saved settings."
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
if (result) {
|
|
629
|
+
// Migrate to new storage shape: preset 'default' with no overrides
|
|
630
|
+
this._stored = { preset: "default", design: {} };
|
|
631
|
+
const base = this._resolvePresetBase("default");
|
|
632
|
+
this.config = JSON.parse(JSON.stringify(base));
|
|
633
|
+
this.formValues = this.filterConfigForSchema(this.config); // Update form values
|
|
634
|
+
this.formKey = (this.formKey || 0) + 1; // Increment to force form update
|
|
635
|
+
this.saveConfig();
|
|
636
|
+
this.applyStyles(true);
|
|
637
|
+
|
|
638
|
+
toast("Configuration reset to defaults", {
|
|
639
|
+
type: "info",
|
|
640
|
+
duration: 2000,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
applyPreset = async (preset) => {
|
|
646
|
+
const result = await PDS.ask(
|
|
647
|
+
`Load "${preset.name}" preset? This will replace your current settings.`
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
if (result) {
|
|
651
|
+
// Build from preset only (no default baseline); store as preset + overrides {}
|
|
652
|
+
const presetConfig = JSON.parse(JSON.stringify(preset));
|
|
653
|
+
const validationResult = PDS.validateDesign(presetConfig);
|
|
654
|
+
if (!validationResult.ok) {
|
|
655
|
+
this.validationIssues = validationResult.issues;
|
|
656
|
+
if (!this._validationToastId) {
|
|
657
|
+
const summary = validationResult.issues
|
|
658
|
+
.slice(0, 3)
|
|
659
|
+
.map((i) => `• ${i.message}`)
|
|
660
|
+
.join("\n");
|
|
661
|
+
this._validationToastId = toast(
|
|
662
|
+
`Preset "${preset.name}" has accessibility issues — not applied.\n${summary}`,
|
|
663
|
+
{ type: "error", persistent: true }
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
this.validationIssues = [];
|
|
670
|
+
// Clear any persistent error toast if present
|
|
671
|
+
try {
|
|
672
|
+
if (this._validationToastId) {
|
|
673
|
+
document
|
|
674
|
+
.querySelector("#global-toaster")
|
|
675
|
+
?.dismissToast(this._validationToastId);
|
|
676
|
+
this._validationToastId = null;
|
|
677
|
+
}
|
|
678
|
+
} catch {}
|
|
679
|
+
this.config = presetConfig;
|
|
680
|
+
this._stored = {
|
|
681
|
+
preset: (preset.id || preset.name || "").toLowerCase(),
|
|
682
|
+
design: {},
|
|
683
|
+
};
|
|
684
|
+
this.formValues = this.filterConfigForSchema(this.config);
|
|
685
|
+
this.saveConfig();
|
|
686
|
+
this.applyStyles(true);
|
|
687
|
+
|
|
688
|
+
toast(`"${preset.name}" preset loaded successfully!`, {
|
|
689
|
+
type: "success",
|
|
690
|
+
duration: 3000,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
handleThemeChange(e) {
|
|
696
|
+
try {
|
|
697
|
+
const value = e.target.value;
|
|
698
|
+
// Update centralized theme via PDS (this persists + applies + sets up listeners)
|
|
699
|
+
PDS.theme = value;
|
|
700
|
+
|
|
701
|
+
// Apply immediately and emit styles using the user config (keep config separate)
|
|
702
|
+
this.applyStyles(true);
|
|
703
|
+
|
|
704
|
+
toast(`Theme set to ${value}`, { type: "info", duration: 1200 });
|
|
705
|
+
} catch (ex) {
|
|
706
|
+
console.warn("Failed to change theme:", ex);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
handleDownload = (format) => {
|
|
711
|
+
let content, filename, mimeType;
|
|
712
|
+
|
|
713
|
+
switch (format) {
|
|
714
|
+
case "css":
|
|
715
|
+
content = this.generator.layeredCSS;
|
|
716
|
+
filename = "pure-ds.css";
|
|
717
|
+
mimeType = "text/css";
|
|
718
|
+
break;
|
|
719
|
+
|
|
720
|
+
case "config":
|
|
721
|
+
content = `// Pure Design System Configuration
|
|
722
|
+
// Generated: ${new Date().toISOString()}
|
|
723
|
+
|
|
724
|
+
import { PDS } from 'pure-ds';
|
|
725
|
+
|
|
726
|
+
export const pdsConfig = ${JSON.stringify(this.config, null, 2)};
|
|
727
|
+
`;
|
|
728
|
+
filename = "pds.config.js";
|
|
729
|
+
mimeType = "text/javascript";
|
|
730
|
+
break;
|
|
731
|
+
|
|
732
|
+
case "tokens":
|
|
733
|
+
const tokens = figmafyTokens(this.generator.generateTokens());
|
|
734
|
+
content = JSON.stringify(tokens, null, 2);
|
|
735
|
+
filename = "design-tokens.json";
|
|
736
|
+
mimeType = "application/json";
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const blob = new Blob([content], { type: mimeType });
|
|
741
|
+
const url = URL.createObjectURL(blob);
|
|
742
|
+
const a = document.createElement("a");
|
|
743
|
+
a.href = url;
|
|
744
|
+
a.download = filename;
|
|
745
|
+
a.click();
|
|
746
|
+
URL.revokeObjectURL(url);
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
render() {
|
|
750
|
+
if (!this.schema) {
|
|
751
|
+
if (!this._loadingToastShown) {
|
|
752
|
+
this._loadingToastShown = true;
|
|
753
|
+
setTimeout(() => {
|
|
754
|
+
try {
|
|
755
|
+
toast("Loading schema...", { duration: 1000 });
|
|
756
|
+
} catch {}
|
|
757
|
+
}, 250);
|
|
758
|
+
}
|
|
759
|
+
return nothing;
|
|
760
|
+
}
|
|
761
|
+
return html`
|
|
762
|
+
<div class="designer-container">
|
|
763
|
+
<div class="designer-toolbar">
|
|
764
|
+
<label data-toggle id="mode-toggle">
|
|
765
|
+
<input
|
|
766
|
+
type="checkbox"
|
|
767
|
+
.checked=${this.mode === "advanced"}
|
|
768
|
+
@change=${(e) => {
|
|
769
|
+
this.mode = e.target.checked ? "advanced" : "simple";
|
|
770
|
+
this.updateForm();
|
|
771
|
+
}}
|
|
772
|
+
/><span
|
|
773
|
+
>${this.mode === "advanced"
|
|
774
|
+
? "Switch to Basic Mode"
|
|
775
|
+
: "Switch to Advanced Mode"}</span
|
|
776
|
+
>
|
|
777
|
+
</label>
|
|
778
|
+
|
|
779
|
+
${this.showInspector
|
|
780
|
+
? html`
|
|
781
|
+
<button
|
|
782
|
+
class="inspector-toggle ${this.inspectorMode
|
|
783
|
+
? "active"
|
|
784
|
+
: ""}"
|
|
785
|
+
@click=${this.toggleInspectorMode}
|
|
786
|
+
title="${this.inspectorMode
|
|
787
|
+
? "Deactivate"
|
|
788
|
+
: "Activate"} Code Inspector"
|
|
789
|
+
>
|
|
790
|
+
<pds-icon
|
|
791
|
+
icon="${this.inspectorMode ? "eye-slash" : "code"}"
|
|
792
|
+
size="sm"
|
|
793
|
+
></pds-icon>
|
|
794
|
+
<span
|
|
795
|
+
>${this.inspectorMode
|
|
796
|
+
? "Inspector Active"
|
|
797
|
+
: "Code Inspector"}</span
|
|
798
|
+
>
|
|
799
|
+
</button>
|
|
800
|
+
`
|
|
801
|
+
: nothing}
|
|
802
|
+
${this.showThemeSelector
|
|
803
|
+
? html`
|
|
804
|
+
<fieldset
|
|
805
|
+
role="radiogroup"
|
|
806
|
+
aria-label="Theme"
|
|
807
|
+
class="theme-select buttons"
|
|
808
|
+
>
|
|
809
|
+
<legend>Theme</legend>
|
|
810
|
+
${(() => {
|
|
811
|
+
const stored = PDS.theme || null;
|
|
812
|
+
const selected = stored || "system";
|
|
813
|
+
return html`
|
|
814
|
+
<label>
|
|
815
|
+
<input
|
|
816
|
+
type="radio"
|
|
817
|
+
name="theme"
|
|
818
|
+
value="system"
|
|
819
|
+
@change=${this.handleThemeChange}
|
|
820
|
+
.checked=${selected === "system"}
|
|
821
|
+
/>
|
|
822
|
+
<span
|
|
823
|
+
><pds-icon icon="moon-stars"></pds-icon>
|
|
824
|
+
System</span
|
|
825
|
+
>
|
|
826
|
+
</label>
|
|
827
|
+
<label>
|
|
828
|
+
<input
|
|
829
|
+
type="radio"
|
|
830
|
+
name="theme"
|
|
831
|
+
value="light"
|
|
832
|
+
@change=${this.handleThemeChange}
|
|
833
|
+
.checked=${selected === "light"}
|
|
834
|
+
/>
|
|
835
|
+
<span><pds-icon icon="sun"></pds-icon> Light</span>
|
|
836
|
+
</label>
|
|
837
|
+
<label>
|
|
838
|
+
<input
|
|
839
|
+
type="radio"
|
|
840
|
+
name="theme"
|
|
841
|
+
value="dark"
|
|
842
|
+
@change=${this.handleThemeChange}
|
|
843
|
+
.checked=${selected === "dark"}
|
|
844
|
+
/>
|
|
845
|
+
<span><pds-icon icon="moon"></pds-icon> Dark</span>
|
|
846
|
+
</label>
|
|
847
|
+
`;
|
|
848
|
+
})()}
|
|
849
|
+
</fieldset>
|
|
850
|
+
`
|
|
851
|
+
: nothing}
|
|
852
|
+
${this.showPresetSelector
|
|
853
|
+
? html`
|
|
854
|
+
<fieldset>
|
|
855
|
+
<legend>Preset</legend>
|
|
856
|
+
<div class="input-icon">
|
|
857
|
+
<pds-icon icon="magnifying-glass"></pds-icon>
|
|
858
|
+
<input
|
|
859
|
+
@focus=${(e) =>
|
|
860
|
+
AutoComplete.connect(
|
|
861
|
+
e,
|
|
862
|
+
this.presetAutoCompleteSettings
|
|
863
|
+
)}
|
|
864
|
+
id="preset-search"
|
|
865
|
+
type="search"
|
|
866
|
+
placeholder="Start typing to search all presets..."
|
|
867
|
+
autocomplete="off"
|
|
868
|
+
/>
|
|
869
|
+
</div>
|
|
870
|
+
</fieldset>
|
|
871
|
+
`
|
|
872
|
+
: nothing}
|
|
873
|
+
</div>
|
|
874
|
+
|
|
875
|
+
<div class="designer-form-container">
|
|
876
|
+
<pds-jsonform
|
|
877
|
+
.jsonSchema=${this.schema}
|
|
878
|
+
.uiSchema=${this._designerUiSchema()}
|
|
879
|
+
.values=${this.formValues || {}}
|
|
880
|
+
hide-reset
|
|
881
|
+
hide-submit
|
|
882
|
+
@pw:value-change=${this.handleFormChange}
|
|
883
|
+
@pw:serialize=${this.handleFormChange}
|
|
884
|
+
>
|
|
885
|
+
</pds-jsonform>
|
|
886
|
+
</div>
|
|
887
|
+
|
|
888
|
+
<div class="designer-actions">
|
|
889
|
+
<nav data-dropdown>
|
|
890
|
+
<button class="btn-primary" style="width: 100%;">
|
|
891
|
+
<pds-icon icon="download" size="sm"></pds-icon>
|
|
892
|
+
<span>Download</span>
|
|
893
|
+
<pds-icon icon="caret-down" size="sm"></pds-icon>
|
|
894
|
+
</button>
|
|
895
|
+
|
|
896
|
+
<menu>
|
|
897
|
+
<li>
|
|
898
|
+
<a
|
|
899
|
+
href="#"
|
|
900
|
+
@click=${(e) => {
|
|
901
|
+
e.preventDefault();
|
|
902
|
+
this.handleDownload("css");
|
|
903
|
+
}}
|
|
904
|
+
>
|
|
905
|
+
<pds-icon icon="file-css" size="sm"></pds-icon>
|
|
906
|
+
<span>CSS File</span>
|
|
907
|
+
</a>
|
|
908
|
+
</li>
|
|
909
|
+
|
|
910
|
+
<li>
|
|
911
|
+
<a
|
|
912
|
+
href="#"
|
|
913
|
+
@click=${(e) => {
|
|
914
|
+
e.preventDefault();
|
|
915
|
+
this.handleDownload("config");
|
|
916
|
+
}}
|
|
917
|
+
>
|
|
918
|
+
<pds-icon icon="file-js" size="sm"></pds-icon>
|
|
919
|
+
<span>Config File</span>
|
|
920
|
+
</a>
|
|
921
|
+
</li>
|
|
922
|
+
|
|
923
|
+
<li>
|
|
924
|
+
<a
|
|
925
|
+
href="#"
|
|
926
|
+
@click=${(e) => {
|
|
927
|
+
e.preventDefault();
|
|
928
|
+
this.handleDownload("tokens");
|
|
929
|
+
}}
|
|
930
|
+
>
|
|
931
|
+
<pds-icon icon="brackets-curly" size="sm"></pds-icon>
|
|
932
|
+
<span>Design Tokens (JSON)</span>
|
|
933
|
+
</a>
|
|
934
|
+
</li>
|
|
935
|
+
</menu>
|
|
936
|
+
</nav>
|
|
937
|
+
</div>
|
|
938
|
+
</div>
|
|
939
|
+
`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
get presetAutoCompleteSettings() {
|
|
943
|
+
return {
|
|
944
|
+
direction: "down",
|
|
945
|
+
//debug: true,
|
|
946
|
+
iconHandler: (item) => {
|
|
947
|
+
const preset = PDS.presets[item.id];
|
|
948
|
+
return /*html*/ `<span class="preset-colors">
|
|
949
|
+
<span style="background-color: ${preset.colors.primary}"></span>
|
|
950
|
+
<span style="background-color: ${preset.colors.secondary}"></span>
|
|
951
|
+
<span style="background-color: ${preset.colors.accent}"></span>
|
|
952
|
+
</span>`;
|
|
953
|
+
},
|
|
954
|
+
categories: {
|
|
955
|
+
Presets: {
|
|
956
|
+
action: (options) => {
|
|
957
|
+
const preset = PDS.presets[options.id];
|
|
958
|
+
this.applyPreset(preset);
|
|
959
|
+
},
|
|
960
|
+
getItems: (options) => {
|
|
961
|
+
const maxPresets = 10;
|
|
962
|
+
const all = Object.values(PDS.presets);
|
|
963
|
+
let filtered = [];
|
|
964
|
+
|
|
965
|
+
if (options.search.length > 0) {
|
|
966
|
+
let n = 0;
|
|
967
|
+
filtered = all.filter((preset) => {
|
|
968
|
+
n++;
|
|
969
|
+
return (
|
|
970
|
+
(n <= maxPresets &&
|
|
971
|
+
preset.name
|
|
972
|
+
.toLowerCase()
|
|
973
|
+
.includes(options.search.toLowerCase())) ||
|
|
974
|
+
(preset.description &&
|
|
975
|
+
preset.description
|
|
976
|
+
.toLowerCase()
|
|
977
|
+
.includes(options.search.toLowerCase()))
|
|
978
|
+
);
|
|
979
|
+
});
|
|
980
|
+
} else {
|
|
981
|
+
filtered = all
|
|
982
|
+
.filter((preset) => preset.tags?.includes("featured"))
|
|
983
|
+
.slice(0, maxPresets);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return filtered.map((preset) => {
|
|
987
|
+
return {
|
|
988
|
+
id: preset.id,
|
|
989
|
+
text: preset.name,
|
|
990
|
+
description: preset.description,
|
|
991
|
+
icon: "palette",
|
|
992
|
+
};
|
|
993
|
+
});
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Provide a uiSchema to customize widgets for designer UX (datalists for fonts, ranges for numeric values)
|
|
1001
|
+
_designerUiSchema() {
|
|
1002
|
+
// Common font-family suggestions (similar to dev console suggestions)
|
|
1003
|
+
const fontSuggestions = [
|
|
1004
|
+
"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
|
1005
|
+
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial",
|
|
1006
|
+
"'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
1007
|
+
"Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
1008
|
+
"Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
|
1009
|
+
"ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
|
1010
|
+
];
|
|
1011
|
+
|
|
1012
|
+
// UI schema (paths use the pds-jsonform path notation, e.g. /typography/fontFamilyHeadings)
|
|
1013
|
+
const ui = {};
|
|
1014
|
+
|
|
1015
|
+
// Font family fields: use datalist via ui.datalist
|
|
1016
|
+
ui["/typography/fontFamilyHeadings"] = { "ui:datalist": fontSuggestions };
|
|
1017
|
+
ui["/typography/fontFamilyBody"] = { "ui:datalist": fontSuggestions };
|
|
1018
|
+
ui["/typography/fontFamilyMono"] = {
|
|
1019
|
+
"ui:datalist": [
|
|
1020
|
+
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
|
|
1021
|
+
"Consolas, 'Liberation Mono', Menlo, monospace",
|
|
1022
|
+
"'Fira Code', 'Cascadia Code', 'Source Code Pro', monospace",
|
|
1023
|
+
],
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
// Numeric fields rendered as ranges for better UX
|
|
1027
|
+
ui["/typography/baseFontSize"] = {
|
|
1028
|
+
"ui:widget": "input-range",
|
|
1029
|
+
"ui:min": 12,
|
|
1030
|
+
"ui:max": 24,
|
|
1031
|
+
};
|
|
1032
|
+
ui["/typography/fontScale"] = {
|
|
1033
|
+
"ui:widget": "input-range",
|
|
1034
|
+
"ui:min": 1.1,
|
|
1035
|
+
"ui:max": 1.618,
|
|
1036
|
+
"ui:step": 0.01,
|
|
1037
|
+
};
|
|
1038
|
+
ui["/spatialRhythm/baseUnit"] = {
|
|
1039
|
+
"ui:widget": "input-range",
|
|
1040
|
+
"ui:min": 4,
|
|
1041
|
+
"ui:max": 32,
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
// Dark mode color overrides
|
|
1045
|
+
ui["/colors/darkMode/background"] = { "ui:widget": "input-color" };
|
|
1046
|
+
ui["/colors/darkMode/secondary"] = { "ui:widget": "input-color" };
|
|
1047
|
+
|
|
1048
|
+
// Advanced: container padding
|
|
1049
|
+
ui["/spatialRhythm/containerPadding"] = {
|
|
1050
|
+
"ui:widget": "input-range",
|
|
1051
|
+
"ui:min": 0,
|
|
1052
|
+
"ui:max": 4,
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
return ui;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
);
|