@rimelight/ui 0.0.7 → 0.0.8
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/package.json +5 -3
- package/src/components/Head.astro +153 -153
- package/src/integrations/ui.ts +98 -106
- package/src/nuxt.ts +125 -0
- package/src/components/BlogPost.astro +0 -195
- package/src/components/DynamicShowcase.astro +0 -681
- package/src/components/ExtendedCarousel.astro +0 -107
- package/src/components/FormattedDate.astro +0 -26
- package/src/components/HeaderLink.astro +0 -24
- package/src/components/ProjectPost.astro +0 -197
- package/src/components/RLHead.astro +0 -3
- package/src/components/nuxt/index.ts +0 -1
|
@@ -1,681 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Dynamic Showcase with Auto-Generated Controls
|
|
4
|
-
*
|
|
5
|
-
* Uses Starwind components for all controls.
|
|
6
|
-
* Shows a single component instance that updates dynamically.
|
|
7
|
-
*/
|
|
8
|
-
import Badge from "@rimelight/ui/components/badge/Badge.astro";
|
|
9
|
-
import Button from "@rimelight/ui/components/button/Button.astro";
|
|
10
|
-
import Input from "@rimelight/ui/components/input/Input.astro";
|
|
11
|
-
import Label from "@rimelight/ui/components/label/Label.astro";
|
|
12
|
-
import Switch from "@rimelight/ui/components/switch/Switch.astro";
|
|
13
|
-
import Table from "@rimelight/ui/components/table/Table.astro";
|
|
14
|
-
import TableBody from "@rimelight/ui/components/table/TableBody.astro";
|
|
15
|
-
import TableCell from "@rimelight/ui/components/table/TableCell.astro";
|
|
16
|
-
import TableHead from "@rimelight/ui/components/table/TableHead.astro";
|
|
17
|
-
import TableHeader from "@rimelight/ui/components/table/TableHeader.astro";
|
|
18
|
-
import TableRow from "@rimelight/ui/components/table/TableRow.astro";
|
|
19
|
-
import Select from "@rimelight/ui/components/select/Select.astro";
|
|
20
|
-
import SelectContent from "@rimelight/ui/components/select/SelectContent.astro";
|
|
21
|
-
import SelectItem from "@rimelight/ui/components/select/SelectItem.astro";
|
|
22
|
-
import SelectTrigger from "@rimelight/ui/components/select/SelectTrigger.astro";
|
|
23
|
-
import SelectValue from "@rimelight/ui/components/select/SelectValue.astro";
|
|
24
|
-
import Textarea from "@rimelight/ui/components/textarea/Textarea.astro";
|
|
25
|
-
import Checkbox from "@rimelight/ui/components/checkbox/Checkbox.astro";
|
|
26
|
-
|
|
27
|
-
export interface PropDef {
|
|
28
|
-
name: string;
|
|
29
|
-
type: string;
|
|
30
|
-
default?: any;
|
|
31
|
-
description?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface DynamicShowcaseProps {
|
|
35
|
-
/** Unique identifier for this showcase instance */
|
|
36
|
-
id: string;
|
|
37
|
-
/** Display name of the component */
|
|
38
|
-
componentName: string;
|
|
39
|
-
/** Description of what the component does */
|
|
40
|
-
description?: string;
|
|
41
|
-
/** Prop definitions */
|
|
42
|
-
props: PropDef[];
|
|
43
|
-
/** Initial prop values */
|
|
44
|
-
initialProps?: Record<string, any>;
|
|
45
|
-
/** Additional classes for the preview container */
|
|
46
|
-
previewClassName?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type Props = DynamicShowcaseProps;
|
|
50
|
-
|
|
51
|
-
const {
|
|
52
|
-
id,
|
|
53
|
-
componentName,
|
|
54
|
-
description,
|
|
55
|
-
props,
|
|
56
|
-
initialProps = {},
|
|
57
|
-
previewClassName
|
|
58
|
-
} = Astro.props;
|
|
59
|
-
|
|
60
|
-
// Build initial state
|
|
61
|
-
const initialState: Record<string, { value: any; omitted: boolean }> = {};
|
|
62
|
-
props.forEach(prop => {
|
|
63
|
-
if (prop.name in initialProps) {
|
|
64
|
-
initialState[prop.name] = { value: initialProps[prop.name], omitted: false };
|
|
65
|
-
} else if (prop.default !== undefined) {
|
|
66
|
-
initialState[prop.name] = { value: prop.default, omitted: false };
|
|
67
|
-
} else {
|
|
68
|
-
initialState[prop.name] = { value: undefined, omitted: true };
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
function getPropKind(type: string): 'string' | 'number' | 'boolean' | 'enum' | 'array' | 'object' {
|
|
73
|
-
const lower = type.toLowerCase();
|
|
74
|
-
if (lower === 'boolean') return 'boolean';
|
|
75
|
-
if (lower === 'number') return 'number';
|
|
76
|
-
if (lower.includes('[]')) return 'array';
|
|
77
|
-
if (lower.startsWith('{') || lower.includes('record')) return 'object';
|
|
78
|
-
if (lower.includes('|')) return 'enum';
|
|
79
|
-
return 'string';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function extractEnumValues(type: string): string[] {
|
|
83
|
-
const matches = type.match(/'([^']+)'|"([^"]+)"/g);
|
|
84
|
-
if (matches) {
|
|
85
|
-
return matches.map(m => m.replace(/['"]/g, ''));
|
|
86
|
-
}
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
<section class="py-12 space-y-12 border-t border-border first:border-t-0" data-showcase={id}>
|
|
92
|
-
<div class="container space-y-8">
|
|
93
|
-
<!-- Header -->
|
|
94
|
-
<div class="space-y-4">
|
|
95
|
-
<div class="space-y-1">
|
|
96
|
-
<h2 class="text-3xl font-semibold tracking-tight">{componentName}</h2>
|
|
97
|
-
{description && <p class="text-muted-foreground text-lg">{description}</p>}
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
<!-- Interactive Playground -->
|
|
102
|
-
<div class="grid lg:grid-cols-2 gap-8">
|
|
103
|
-
<!-- Controls Panel -->
|
|
104
|
-
<div class="space-y-6">
|
|
105
|
-
<div class="flex items-center justify-between">
|
|
106
|
-
<h3 class="text-sm font-medium text-muted-foreground uppercase tracking-wider">Controls</h3>
|
|
107
|
-
<Button variant="outline" size="sm" data-action="reset" class="text-xs">Reset</Button>
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<div class="space-y-4" data-controls>
|
|
111
|
-
{props.map((prop) => {
|
|
112
|
-
const kind = getPropKind(prop.type);
|
|
113
|
-
const enumValues = extractEnumValues(prop.type);
|
|
114
|
-
const state = initialState[prop.name];
|
|
115
|
-
if (!state) return null;
|
|
116
|
-
const isOmitted = state.omitted;
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<div class="space-y-2 p-4 rounded-lg border border-border bg-card/50" data-prop-control={prop.name}>
|
|
120
|
-
<div class="flex items-center justify-between gap-md">
|
|
121
|
-
<Label class="text-sm font-medium flex items-center gap-md">
|
|
122
|
-
{prop.name}
|
|
123
|
-
<Badge variant="outline" class="font-mono text-[10px] uppercase">
|
|
124
|
-
{prop.type}
|
|
125
|
-
</Badge>
|
|
126
|
-
</Label>
|
|
127
|
-
<div class="flex items-center gap-md">
|
|
128
|
-
<Label for={`${id}-omit-${prop.name}`} class="text-xs text-muted-foreground">Omit</Label>
|
|
129
|
-
<Switch
|
|
130
|
-
id={`${id}-omit-${prop.name}`}
|
|
131
|
-
data-prop={prop.name}
|
|
132
|
-
checked={isOmitted}
|
|
133
|
-
data-state={isOmitted ? 'checked' : 'unchecked'}
|
|
134
|
-
size="sm"
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
{kind === 'boolean' && (
|
|
140
|
-
<div class="flex items-center gap-md">
|
|
141
|
-
<Checkbox
|
|
142
|
-
id={`${id}-value-${prop.name}`}
|
|
143
|
-
data-prop={prop.name}
|
|
144
|
-
data-type="boolean"
|
|
145
|
-
checked={!!state.value}
|
|
146
|
-
disabled={isOmitted}
|
|
147
|
-
size="md"
|
|
148
|
-
/>
|
|
149
|
-
<Label for={`${id}-value-${prop.name}`} class="text-sm">
|
|
150
|
-
{state.value ? 'True' : 'False'}
|
|
151
|
-
</Label>
|
|
152
|
-
</div>
|
|
153
|
-
)}
|
|
154
|
-
|
|
155
|
-
{kind === 'string' && (
|
|
156
|
-
<Input
|
|
157
|
-
id={`${id}-value-${prop.name}`}
|
|
158
|
-
data-prop={prop.name}
|
|
159
|
-
data-type="string"
|
|
160
|
-
value={state.value ?? ''}
|
|
161
|
-
placeholder={prop.default ?? `Enter ${prop.name}`}
|
|
162
|
-
class="w-full"
|
|
163
|
-
disabled={isOmitted}
|
|
164
|
-
size="md"
|
|
165
|
-
/>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
|
-
{kind === 'number' && (
|
|
169
|
-
<Input
|
|
170
|
-
id={`${id}-value-${prop.name}`}
|
|
171
|
-
data-prop={prop.name}
|
|
172
|
-
data-type="number"
|
|
173
|
-
type="number"
|
|
174
|
-
value={state.value ?? ''}
|
|
175
|
-
placeholder={String(prop.default ?? '')}
|
|
176
|
-
class="w-full"
|
|
177
|
-
disabled={isOmitted}
|
|
178
|
-
size="md"
|
|
179
|
-
/>
|
|
180
|
-
)}
|
|
181
|
-
|
|
182
|
-
{kind === 'enum' && enumValues.length > 0 && (
|
|
183
|
-
<div class="w-full" data-prop-control={prop.name}>
|
|
184
|
-
<Select
|
|
185
|
-
id={`${id}-select-${prop.name}`}
|
|
186
|
-
data-prop={prop.name}
|
|
187
|
-
data-type="enum"
|
|
188
|
-
data-value={state.value ?? ''}
|
|
189
|
-
class="w-full"
|
|
190
|
-
>
|
|
191
|
-
<SelectTrigger size="md">
|
|
192
|
-
<SelectValue placeholder="Select..." />
|
|
193
|
-
</SelectTrigger>
|
|
194
|
-
<SelectContent size="md">
|
|
195
|
-
<SelectItem value="">(default)</SelectItem>
|
|
196
|
-
{enumValues.map((value) => (
|
|
197
|
-
<SelectItem value={value} data-selected={state.value === value ? 'true' : undefined}>
|
|
198
|
-
{value}
|
|
199
|
-
</SelectItem>
|
|
200
|
-
))}
|
|
201
|
-
</SelectContent>
|
|
202
|
-
</Select>
|
|
203
|
-
</div>
|
|
204
|
-
)}
|
|
205
|
-
|
|
206
|
-
{kind === 'array' && (
|
|
207
|
-
<Textarea
|
|
208
|
-
id={`${id}-value-${prop.name}`}
|
|
209
|
-
data-prop={prop.name}
|
|
210
|
-
data-type="array"
|
|
211
|
-
placeholder="JSON array"
|
|
212
|
-
class="w-full font-mono text-xs"
|
|
213
|
-
disabled={isOmitted}
|
|
214
|
-
size="md"
|
|
215
|
-
>{state.value != null ? JSON.stringify(state.value, null, 2) : ''}</Textarea>
|
|
216
|
-
)}
|
|
217
|
-
|
|
218
|
-
{kind === 'object' && (
|
|
219
|
-
<Textarea
|
|
220
|
-
id={`${id}-value-${prop.name}`}
|
|
221
|
-
data-prop={prop.name}
|
|
222
|
-
data-type="object"
|
|
223
|
-
placeholder="JSON object"
|
|
224
|
-
class="w-full font-mono text-xs"
|
|
225
|
-
disabled={isOmitted}
|
|
226
|
-
size="md"
|
|
227
|
-
>{state.value != null ? JSON.stringify(state.value, null, 2) : ''}</Textarea>
|
|
228
|
-
)}
|
|
229
|
-
|
|
230
|
-
{prop.description && (
|
|
231
|
-
<p class="text-xs text-muted-foreground">{prop.description}</p>
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
);
|
|
235
|
-
})}
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
|
|
239
|
-
<!-- Preview Panel -->
|
|
240
|
-
<div class="space-y-4">
|
|
241
|
-
<h3 class="text-sm font-medium text-muted-foreground uppercase tracking-wider">Preview</h3>
|
|
242
|
-
<div class:list={[
|
|
243
|
-
"w-full min-h-[400px] rounded-xl border border-border bg-muted/5 p-6 flex items-center justify-center overflow-auto",
|
|
244
|
-
previewClassName
|
|
245
|
-
]}>
|
|
246
|
-
<div
|
|
247
|
-
id={`${id}-preview`}
|
|
248
|
-
data-preview
|
|
249
|
-
data-showcase-id={id}
|
|
250
|
-
data-component-name={componentName}
|
|
251
|
-
data-initial-props={JSON.stringify(initialState)}
|
|
252
|
-
class="w-full h-full"
|
|
253
|
-
>
|
|
254
|
-
<slot />
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
|
|
258
|
-
<!-- Active Props JSON -->
|
|
259
|
-
<div class="rounded-lg border border-border bg-card overflow-hidden">
|
|
260
|
-
<div class="px-4 py-2 bg-muted/50 border-b border-border flex items-center justify-between">
|
|
261
|
-
<h4 class="text-xs font-medium text-muted-foreground uppercase tracking-wider">Active Props</h4>
|
|
262
|
-
<Button variant="ghost" size="sm" data-action="copy" class="h-6 text-xs">Copy</Button>
|
|
263
|
-
</div>
|
|
264
|
-
<pre class="p-4 text-xs font-mono text-muted-foreground overflow-x-auto max-h-[300px]">
|
|
265
|
-
<code data-active-props>{JSON.stringify(
|
|
266
|
-
Object.fromEntries(
|
|
267
|
-
Object.entries(initialState)
|
|
268
|
-
.filter(([_, v]) => !v.omitted && v.value !== undefined)
|
|
269
|
-
.map(([k, v]) => [k, v.value])
|
|
270
|
-
), null, 2
|
|
271
|
-
)}</code>
|
|
272
|
-
</pre>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
</div>
|
|
276
|
-
|
|
277
|
-
<!-- Prop Reference Table -->
|
|
278
|
-
{props.length > 0 && (
|
|
279
|
-
<div>
|
|
280
|
-
<h3 class="text-sm font-medium text-muted-foreground uppercase tracking-wider mb-4">Prop Reference</h3>
|
|
281
|
-
<div class="rounded-xl border border-border bg-card overflow-hidden">
|
|
282
|
-
<Table>
|
|
283
|
-
<TableHeader>
|
|
284
|
-
<TableRow>
|
|
285
|
-
<TableHead class="w-[15%]">Prop</TableHead>
|
|
286
|
-
<TableHead class="w-[15%]">Type</TableHead>
|
|
287
|
-
<TableHead class="w-[15%]">Default</TableHead>
|
|
288
|
-
<TableHead>Description</TableHead>
|
|
289
|
-
</TableRow>
|
|
290
|
-
</TableHeader>
|
|
291
|
-
<TableBody>
|
|
292
|
-
{props.map((prop) => (
|
|
293
|
-
<TableRow>
|
|
294
|
-
<TableCell class="font-mono text-sm font-medium text-primary">
|
|
295
|
-
{prop.name}
|
|
296
|
-
</TableCell>
|
|
297
|
-
<TableCell>
|
|
298
|
-
<Badge variant="outline" class="font-mono text-[10px] uppercase">
|
|
299
|
-
{prop.type}
|
|
300
|
-
</Badge>
|
|
301
|
-
</TableCell>
|
|
302
|
-
<TableCell class="font-mono text-xs text-muted-foreground">
|
|
303
|
-
{prop.default !== undefined ? String(prop.default) : "—"}
|
|
304
|
-
</TableCell>
|
|
305
|
-
<TableCell class="text-sm">
|
|
306
|
-
{prop.description || "—"}
|
|
307
|
-
</TableCell>
|
|
308
|
-
</TableRow>
|
|
309
|
-
))}
|
|
310
|
-
</TableBody>
|
|
311
|
-
</Table>
|
|
312
|
-
</div>
|
|
313
|
-
</div>
|
|
314
|
-
)}
|
|
315
|
-
</div>
|
|
316
|
-
</section>
|
|
317
|
-
|
|
318
|
-
<script>
|
|
319
|
-
interface PropState {
|
|
320
|
-
value: unknown;
|
|
321
|
-
omitted: boolean;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
interface CurrentState {
|
|
325
|
-
[key: string]: PropState;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function initShowcase(showcaseEl: HTMLElement) {
|
|
329
|
-
const id = showcaseEl.dataset.showcase;
|
|
330
|
-
if (!id) return;
|
|
331
|
-
|
|
332
|
-
const controlsContainer = showcaseEl.querySelector('[data-controls]') as HTMLElement | null;
|
|
333
|
-
const previewContainer = showcaseEl.querySelector('[data-preview]') as HTMLElement | null;
|
|
334
|
-
const activePropsCode = showcaseEl.querySelector('[data-active-props]') as HTMLElement | null;
|
|
335
|
-
const resetBtn = showcaseEl.querySelector('[data-action="reset"]') as HTMLElement | null;
|
|
336
|
-
const copyBtn = showcaseEl.querySelector('[data-action="copy"]') as HTMLElement | null;
|
|
337
|
-
|
|
338
|
-
if (!controlsContainer || !previewContainer || !activePropsCode) return;
|
|
339
|
-
|
|
340
|
-
// Parse initial state
|
|
341
|
-
const initialPropsJson = previewContainer.dataset.initialProps;
|
|
342
|
-
if (!initialPropsJson) return;
|
|
343
|
-
|
|
344
|
-
let currentState: CurrentState = JSON.parse(initialPropsJson);
|
|
345
|
-
|
|
346
|
-
function getActiveProps(): Record<string, unknown> {
|
|
347
|
-
const active: Record<string, unknown> = {};
|
|
348
|
-
Object.entries(currentState).forEach(([key, state]) => {
|
|
349
|
-
if (!state.omitted && state.value !== undefined) {
|
|
350
|
-
active[key] = state.value;
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
return active;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function updateActivePropsDisplay() {
|
|
357
|
-
if (activePropsCode) {
|
|
358
|
-
activePropsCode.textContent = JSON.stringify(getActiveProps(), null, 2);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function updatePreview() {
|
|
363
|
-
updateActivePropsDisplay();
|
|
364
|
-
|
|
365
|
-
const activeProps = getActiveProps();
|
|
366
|
-
|
|
367
|
-
// Dispatch custom event with new props
|
|
368
|
-
if (previewContainer) {
|
|
369
|
-
previewContainer.dispatchEvent(new CustomEvent('showcase:props-update', {
|
|
370
|
-
detail: { props: activeProps, allProps: currentState }
|
|
371
|
-
}));
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function setPropOmitted(propName: string, omitted: boolean) {
|
|
376
|
-
if (currentState[propName]) {
|
|
377
|
-
currentState[propName].omitted = omitted;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Update control disabled state
|
|
381
|
-
const controlEl = controlsContainer?.querySelector(`[data-prop-control="${propName}"]`) as HTMLElement | null;
|
|
382
|
-
if (controlEl) {
|
|
383
|
-
const input = controlEl.querySelector('input, select, textarea, [data-type]') as HTMLElement | null;
|
|
384
|
-
if (input) {
|
|
385
|
-
if (omitted) {
|
|
386
|
-
input.setAttribute('disabled', 'true');
|
|
387
|
-
input.classList.add('opacity-50');
|
|
388
|
-
} else {
|
|
389
|
-
input.removeAttribute('disabled');
|
|
390
|
-
input.classList.remove('opacity-50');
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function updatePropValue(propName: string, value: unknown) {
|
|
397
|
-
if (currentState[propName]) {
|
|
398
|
-
currentState[propName].value = value;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Handle omit switch changes
|
|
403
|
-
controlsContainer?.querySelectorAll('[id^="-omit-"]').forEach(el => {
|
|
404
|
-
el.addEventListener('starwind-switch:change', (e: Event) => {
|
|
405
|
-
const customEvent = e as CustomEvent<{ checked: boolean }>;
|
|
406
|
-
const { checked: isOmitted } = customEvent.detail;
|
|
407
|
-
const propName = (el as HTMLElement).dataset.prop;
|
|
408
|
-
if (!propName) return;
|
|
409
|
-
|
|
410
|
-
setPropOmitted(propName, isOmitted);
|
|
411
|
-
updatePreview();
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// Handle Input changes (string, number)
|
|
416
|
-
controlsContainer?.querySelectorAll('[id*="-value-"]').forEach(el => {
|
|
417
|
-
// Handle text input changes
|
|
418
|
-
el.addEventListener('input', (e: Event) => {
|
|
419
|
-
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
|
|
420
|
-
const propName = (el as HTMLElement).dataset.prop;
|
|
421
|
-
const type = (el as HTMLElement).dataset.type;
|
|
422
|
-
if (!propName) return;
|
|
423
|
-
|
|
424
|
-
let value: unknown = target.value;
|
|
425
|
-
|
|
426
|
-
if (type === 'number') {
|
|
427
|
-
value = value === '' ? undefined : Number(value);
|
|
428
|
-
} else if (type === 'array' || type === 'object') {
|
|
429
|
-
try {
|
|
430
|
-
value = value === '' ? undefined : JSON.parse(value as string);
|
|
431
|
-
} catch {
|
|
432
|
-
// Keep as string if invalid JSON
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
updatePropValue(propName, value);
|
|
437
|
-
updatePreview();
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// Handle checkbox changes (boolean) - Starwind Checkbox
|
|
441
|
-
el.addEventListener('starwind-checkbox:change', (e: Event) => {
|
|
442
|
-
const customEvent = e as CustomEvent<{ checked: boolean }>;
|
|
443
|
-
const { checked } = customEvent.detail;
|
|
444
|
-
const propName = (el as HTMLElement).dataset.prop;
|
|
445
|
-
console.log('[Showcase Checkbox] prop:', propName, 'checked:', checked);
|
|
446
|
-
if (!propName) return;
|
|
447
|
-
|
|
448
|
-
updatePropValue(propName, checked);
|
|
449
|
-
updatePreview();
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
// Handle Starwind Select changes - listen on the wrapper
|
|
454
|
-
controlsContainer?.querySelectorAll('[data-prop-control]').forEach((controlEl: Element) => {
|
|
455
|
-
const selectWrapper = controlEl.querySelector('.starwind-select') as HTMLElement | null;
|
|
456
|
-
if (selectWrapper) {
|
|
457
|
-
selectWrapper.addEventListener('starwind-select:change', (e: Event) => {
|
|
458
|
-
const customEvent = e as CustomEvent<{ value: string }>;
|
|
459
|
-
const { value } = customEvent.detail;
|
|
460
|
-
const propName = (controlEl as HTMLElement).dataset.propControl;
|
|
461
|
-
if (!propName) return;
|
|
462
|
-
|
|
463
|
-
updatePropValue(propName, value || undefined);
|
|
464
|
-
updatePreview();
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// Reset button
|
|
470
|
-
resetBtn?.addEventListener('click', () => {
|
|
471
|
-
const freshState: CurrentState = JSON.parse(initialPropsJson);
|
|
472
|
-
currentState = freshState;
|
|
473
|
-
|
|
474
|
-
// Reset all controls
|
|
475
|
-
Object.keys(freshState).forEach((propName) => {
|
|
476
|
-
const state = freshState[propName];
|
|
477
|
-
if (!state) return;
|
|
478
|
-
const controlEl = controlsContainer?.querySelector(`[data-prop-control="${propName}"]`) as HTMLElement | null;
|
|
479
|
-
if (!controlEl) return;
|
|
480
|
-
|
|
481
|
-
const omitSwitch = controlEl.querySelector('[id^*="-omit-"]') as HTMLElement | null;
|
|
482
|
-
const valueInput = controlEl.querySelector('[id^*="-value-"]') as HTMLElement | null;
|
|
483
|
-
const selectWrapper = controlEl.querySelector('.starwind-select') as HTMLElement | null;
|
|
484
|
-
|
|
485
|
-
if (omitSwitch) {
|
|
486
|
-
// Reset Switch component
|
|
487
|
-
omitSwitch.setAttribute('data-state', state.omitted ? 'checked' : 'unchecked');
|
|
488
|
-
omitSwitch.setAttribute('aria-checked', state.omitted ? 'true' : 'false');
|
|
489
|
-
omitSwitch.dispatchEvent(new CustomEvent('starwind-switch:change', {
|
|
490
|
-
detail: { checked: state.omitted, switchId: omitSwitch.id },
|
|
491
|
-
bubbles: true
|
|
492
|
-
}));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (valueInput) {
|
|
496
|
-
if (valueInput.tagName === 'INPUT' && (valueInput as HTMLInputElement).type === 'checkbox') {
|
|
497
|
-
// Starwind checkbox
|
|
498
|
-
(valueInput as HTMLInputElement).checked = !!state.value;
|
|
499
|
-
valueInput.setAttribute('aria-checked', state.value ? 'true' : 'false');
|
|
500
|
-
valueInput.dispatchEvent(new CustomEvent('starwind-checkbox:change', {
|
|
501
|
-
detail: { checked: !!state.value },
|
|
502
|
-
bubbles: true
|
|
503
|
-
}));
|
|
504
|
-
} else if (valueInput.tagName === 'INPUT') {
|
|
505
|
-
(valueInput as HTMLInputElement).value = String(state.value ?? '');
|
|
506
|
-
} else if (valueInput.tagName === 'TEXTAREA') {
|
|
507
|
-
(valueInput as HTMLTextAreaElement).value = state.value !== undefined ? JSON.stringify(state.value, null, 2) : '';
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (selectWrapper) {
|
|
512
|
-
// Starwind Select - use programmatic select event
|
|
513
|
-
selectWrapper.dispatchEvent(new CustomEvent('starwind-select:select', {
|
|
514
|
-
detail: { value: String(state.value ?? ''), selectId: selectWrapper.id },
|
|
515
|
-
bubbles: true
|
|
516
|
-
}));
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (valueInput || selectWrapper) {
|
|
520
|
-
const element: HTMLElement = (valueInput || selectWrapper)!;
|
|
521
|
-
if (state.omitted) {
|
|
522
|
-
element.setAttribute('disabled', 'true');
|
|
523
|
-
element.classList.add('opacity-50');
|
|
524
|
-
} else {
|
|
525
|
-
element.removeAttribute('disabled');
|
|
526
|
-
element.classList.remove('opacity-50');
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
updatePreview();
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
// Copy button
|
|
535
|
-
copyBtn?.addEventListener('click', async () => {
|
|
536
|
-
const text = activePropsCode?.textContent;
|
|
537
|
-
if (text) {
|
|
538
|
-
await navigator.clipboard.writeText(text);
|
|
539
|
-
if (copyBtn) {
|
|
540
|
-
copyBtn.textContent = 'Copied!';
|
|
541
|
-
setTimeout(() => {
|
|
542
|
-
copyBtn.textContent = 'Copy';
|
|
543
|
-
}, 2000);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
// Initial setup
|
|
549
|
-
Object.entries(currentState).forEach(([propName, state]) => {
|
|
550
|
-
if (state.omitted) {
|
|
551
|
-
setPropOmitted(propName, true);
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
updateActivePropsDisplay();
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Initialize
|
|
559
|
-
if (document.readyState === 'loading') {
|
|
560
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
561
|
-
document.querySelectorAll('[data-showcase]').forEach(el => initShowcase(el as HTMLElement));
|
|
562
|
-
});
|
|
563
|
-
} else {
|
|
564
|
-
document.querySelectorAll('[data-showcase]').forEach(el => initShowcase(el as HTMLElement));
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
document.addEventListener('astro:page-load', () => {
|
|
568
|
-
document.querySelectorAll('[data-showcase]').forEach(el => initShowcase(el as HTMLElement));
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
// Client-side preview renderer - singleton pattern
|
|
572
|
-
// This script runs once globally, not per-component
|
|
573
|
-
|
|
574
|
-
interface ShowcasePreviewAPI {
|
|
575
|
-
renderers: Map<string, (props: Record<string, unknown>, container: HTMLElement) => void>;
|
|
576
|
-
registerRenderer: (name: string, renderer: (props: Record<string, unknown>, container: HTMLElement) => void) => void;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
declare global {
|
|
580
|
-
interface Window {
|
|
581
|
-
ShowcasePreview?: ShowcasePreviewAPI;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (!window.ShowcasePreview) {
|
|
586
|
-
window.ShowcasePreview = {
|
|
587
|
-
renderers: new Map<string, (props: Record<string, unknown>, container: HTMLElement) => void>(),
|
|
588
|
-
registerRenderer: function(name: string, renderer: (props: Record<string, unknown>, container: HTMLElement) => void) {
|
|
589
|
-
this.renderers.set(name, renderer);
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Initialize preview handlers
|
|
595
|
-
function initPreviewHandlers() {
|
|
596
|
-
// Initialize showcase controls for each showcase section
|
|
597
|
-
document.querySelectorAll('[data-showcase]').forEach(el => {
|
|
598
|
-
initShowcase(el as HTMLElement);
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
// Initialize preview containers
|
|
602
|
-
document.querySelectorAll('div[data-preview]').forEach(el => {
|
|
603
|
-
if (!el.hasAttribute('data-preview-initialized')) {
|
|
604
|
-
el.setAttribute('data-preview-initialized', 'true');
|
|
605
|
-
const componentName = (el as HTMLElement).dataset.componentName;
|
|
606
|
-
const initialPropsJson = (el as HTMLElement).dataset.initialProps;
|
|
607
|
-
|
|
608
|
-
if (componentName && initialPropsJson) {
|
|
609
|
-
const props: Record<string, PropState> = JSON.parse(initialPropsJson);
|
|
610
|
-
|
|
611
|
-
// Look up renderer at event time, not initialization time
|
|
612
|
-
el.addEventListener('showcase:props-update', (e: Event) => {
|
|
613
|
-
const newProps = (e as CustomEvent<{ props: Record<string, unknown> }>).detail.props;
|
|
614
|
-
const renderer = window.ShowcasePreview?.renderers.get(componentName);
|
|
615
|
-
if (renderer) {
|
|
616
|
-
renderer(newProps, el as HTMLElement);
|
|
617
|
-
} else {
|
|
618
|
-
// Update data attributes
|
|
619
|
-
Object.entries(newProps).forEach(([key, value]) => {
|
|
620
|
-
if (value === undefined || value === null) {
|
|
621
|
-
el.removeAttribute(`data-${key}`);
|
|
622
|
-
} else if (typeof value === 'boolean') {
|
|
623
|
-
if (value) {
|
|
624
|
-
el.setAttribute(`data-${key}`, 'true');
|
|
625
|
-
} else {
|
|
626
|
-
el.removeAttribute(`data-${key}`);
|
|
627
|
-
}
|
|
628
|
-
} else {
|
|
629
|
-
el.setAttribute(`data-${key}`, String(value));
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
// Initial render - look up renderer
|
|
636
|
-
const renderer = window.ShowcasePreview?.renderers.get(componentName);
|
|
637
|
-
if (renderer) {
|
|
638
|
-
renderer(Object.fromEntries(
|
|
639
|
-
Object.entries(props).filter(([_, v]) => !v.omitted && v.value !== undefined).map(([k, v]) => [k, v.value])
|
|
640
|
-
), el as HTMLElement);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Initialize on load - defer until DOM is ready
|
|
648
|
-
function initShowcasePreview() {
|
|
649
|
-
if (document.readyState === 'loading') {
|
|
650
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
651
|
-
initPreviewHandlers();
|
|
652
|
-
});
|
|
653
|
-
} else {
|
|
654
|
-
initPreviewHandlers();
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
initShowcasePreview();
|
|
659
|
-
document.addEventListener('astro:page-load', initPreviewHandlers);
|
|
660
|
-
|
|
661
|
-
// Export function for page to trigger initial render
|
|
662
|
-
window.dispatchEvent(new CustomEvent('showcase-preview:ready'));
|
|
663
|
-
</script>
|
|
664
|
-
|
|
665
|
-
<style>
|
|
666
|
-
[data-showcase] [data-controls] [disabled] {
|
|
667
|
-
cursor: not-allowed;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
[data-showcase] select {
|
|
671
|
-
cursor: pointer;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
[data-showcase] select:disabled,
|
|
675
|
-
[data-showcase] input:disabled,
|
|
676
|
-
[data-showcase] textarea:disabled {
|
|
677
|
-
opacity: 0.5;
|
|
678
|
-
cursor: not-allowed;
|
|
679
|
-
}
|
|
680
|
-
</style>
|
|
681
|
-
|