dekit-cli 0.4.1 → 0.4.3
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/README.md +10 -26
- package/dist/editor/assets/{index-D16Bm-Rw.js → index-hySfUCIY.js} +10 -10
- package/dist/editor/index.html +1 -1
- package/docs/usage.md +196 -7
- package/editor/src/App.tsx +8 -0
- package/editor/src/types.ts +8 -0
- package/package.json +1 -1
- package/src/cli/init.test.ts +22 -0
- package/src/cli/init.ts +12 -1
- package/src/cli/screenshot.ts +21 -7
- package/src/design-server.ts +0 -1
- package/src/editor-server.ts +8 -1
- package/src/parser.test.ts +164 -0
- package/src/parser.ts +74 -3
- package/src/server.ts +1 -3
- package/src/types.ts +16 -0
- package/src/vite-plugin-dekit.test.ts +85 -1
- package/src/vite-plugin-dekit.ts +42 -2
- package/templates/projects/mobile/dekit.yaml +1 -0
package/dist/editor/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Dekit Editor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-hySfUCIY.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-HLKs1tz-.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/docs/usage.md
CHANGED
|
@@ -132,6 +132,7 @@ Every page and component must be registered:
|
|
|
132
132
|
|
|
133
133
|
```yaml
|
|
134
134
|
version: 1.0
|
|
135
|
+
device: "iPhone 16"
|
|
135
136
|
global-style: "global.css"
|
|
136
137
|
|
|
137
138
|
components:
|
|
@@ -143,6 +144,13 @@ pages:
|
|
|
143
144
|
home:
|
|
144
145
|
template: "pages/home/home.html"
|
|
145
146
|
style: "pages/home/home.css"
|
|
147
|
+
properties:
|
|
148
|
+
showBanner:
|
|
149
|
+
type: boolean
|
|
150
|
+
default: true
|
|
151
|
+
itemCount:
|
|
152
|
+
type: number
|
|
153
|
+
default: 3
|
|
146
154
|
about:
|
|
147
155
|
template: "pages/about/about.html"
|
|
148
156
|
style: "pages/about/about.css"
|
|
@@ -150,6 +158,44 @@ pages:
|
|
|
150
158
|
|
|
151
159
|
All paths are relative to the `.dekit/` directory.
|
|
152
160
|
|
|
161
|
+
### device
|
|
162
|
+
|
|
163
|
+
Sets the target device for the project. Affects screenshot default viewport and editor initial device.
|
|
164
|
+
|
|
165
|
+
Accepts a preset name (e.g. `"iPhone 16"`, `"iPad Air"`, `"Desktop 1440"`) or custom dimensions:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
device:
|
|
169
|
+
width: 400
|
|
170
|
+
height: 800
|
|
171
|
+
dpr: 2
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
When omitted, screenshots default to 1280×800.
|
|
175
|
+
|
|
176
|
+
### Page Properties
|
|
177
|
+
|
|
178
|
+
Pages can declare properties — typed parameters with defaults. Properties are injected as `window.__DEKIT_PROPS__` in the page, so your HTML/JS can read them to control rendering.
|
|
179
|
+
|
|
180
|
+
**Supported types:** `boolean`, `number`, `string`.
|
|
181
|
+
|
|
182
|
+
**Using properties in page HTML:**
|
|
183
|
+
```html
|
|
184
|
+
<section class="banner">...</section>
|
|
185
|
+
<script>
|
|
186
|
+
if (!window.__DEKIT_PROPS__.showBanner) {
|
|
187
|
+
document.querySelector('.banner').remove();
|
|
188
|
+
}
|
|
189
|
+
</script>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Override properties in screenshots** with `--props`:
|
|
193
|
+
```bash
|
|
194
|
+
dekit screenshot '$${home}' --props showBanner=false --props itemCount=5
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Values are converted based on the declared type (not guessed). Unspecified props use their default.
|
|
198
|
+
|
|
153
199
|
## CLI Reference
|
|
154
200
|
|
|
155
201
|
### dekit init [path] [--template name]
|
|
@@ -162,6 +208,7 @@ Templates: `blank`, `landing`, `dashboard`, `mobile`.
|
|
|
162
208
|
dekit init # .dekit/ in current directory
|
|
163
209
|
dekit init my-project # my-project/.dekit/
|
|
164
210
|
dekit init --template landing # With template
|
|
211
|
+
dekit init --template mobile --device "iPhone 16" # With device
|
|
165
212
|
dekit init --template # List available templates
|
|
166
213
|
```
|
|
167
214
|
|
|
@@ -202,6 +249,7 @@ dekit screenshot '$${home@.hero}' # Element only
|
|
|
202
249
|
dekit screenshot '$${home}' --device iphone-14 # Device size
|
|
203
250
|
dekit screenshot '$${home}' --full-page # Include scroll area
|
|
204
251
|
dekit screenshot '$${home}' -o output.png # Custom output path
|
|
252
|
+
dekit screenshot '$${home}' --props showBanner=false --props itemCount=5 # Override properties
|
|
205
253
|
dekit screenshot --all # All pages
|
|
206
254
|
```
|
|
207
255
|
|
|
@@ -231,19 +279,160 @@ When you see a `$${...}` pattern in user messages:
|
|
|
231
279
|
|
|
232
280
|
You don't need to parse refs yourself — just pass them to the CLI.
|
|
233
281
|
|
|
282
|
+
## Best Practices
|
|
283
|
+
|
|
284
|
+
### 1. Start with DESIGN.md
|
|
285
|
+
|
|
286
|
+
Before writing any HTML/CSS, read `.dekit/DESIGN.md`. This is the design system source of truth for the project.
|
|
287
|
+
|
|
288
|
+
- **If DESIGN.md exists** — read it and follow the design specifications (colors, typography, components, spacing) when building pages.
|
|
289
|
+
- **If DESIGN.md does not exist** — generate one based on the current design task requirements before writing any code. Infer the visual style, color palette, and layout rules from the user's request, then write `.dekit/DESIGN.md`.
|
|
290
|
+
- **If the user's request conflicts with DESIGN.md** — update DESIGN.md first to reflect the new requirements, then implement the changes. DESIGN.md is always the single source of truth; code follows the spec, not the other way around.
|
|
291
|
+
|
|
292
|
+
The recommended sections:
|
|
293
|
+
|
|
294
|
+
```markdown
|
|
295
|
+
# DESIGN.md
|
|
296
|
+
|
|
297
|
+
## 1. Visual Theme & Atmosphere
|
|
298
|
+
Clean, minimal SaaS aesthetic with generous white space. Light and airy,
|
|
299
|
+
professional but approachable. Rounded geometry, subtle shadows, no hard borders.
|
|
300
|
+
|
|
301
|
+
## 2. Color Palette & Roles
|
|
302
|
+
- **Royal Blue** (`#2563eb`): Primary brand, CTA backgrounds, active states
|
|
303
|
+
- **Amber** (`#f59e0b`): Accent, highlights, badges
|
|
304
|
+
- **Slate 900** (`#0f172a`): Headings, primary text
|
|
305
|
+
- **Slate 500** (`#64748b`): Body text, descriptions
|
|
306
|
+
- **Slate 100** (`#f1f5f9`): Surface backgrounds, cards
|
|
307
|
+
- **White** (`#ffffff`): Page background
|
|
308
|
+
- **Red 500** (`#ef4444`): Error states, destructive actions
|
|
309
|
+
- **Green 500** (`#22c55e`): Success states, confirmations
|
|
310
|
+
|
|
311
|
+
## 3. Typography Rules
|
|
312
|
+
- Font: system-ui, -apple-system, sans-serif
|
|
313
|
+
- Headings: weight 700, tight line-height (1.1-1.2)
|
|
314
|
+
- Body: weight 400, relaxed line-height (1.5-1.6), 16px base
|
|
315
|
+
- Small/labels: weight 500, 12-14px, uppercase tracking for overlines
|
|
316
|
+
|
|
317
|
+
## 4. Component Stylings
|
|
318
|
+
- **Buttons**: 8px radius, 12px 24px padding. Primary: blue bg + white text.
|
|
319
|
+
Outline: transparent bg + blue border + blue text. Hover darkens 10%.
|
|
320
|
+
- **Cards**: white bg, 12px radius, 1px solid #e2e8f0 border,
|
|
321
|
+
shadow 0 1px 3px rgba(0,0,0,0.1). No header stripe.
|
|
322
|
+
- **Inputs**: 8px radius, 1px #d1d5db border, 10px 14px padding.
|
|
323
|
+
Focus: blue border + blue ring shadow.
|
|
324
|
+
|
|
325
|
+
## 5. Layout & Spacing Principles
|
|
326
|
+
- 4px base grid. Spacing scale: 4, 8, 16, 24, 32, 48, 64, 96.
|
|
327
|
+
- Page max-width: 1200px, centered.
|
|
328
|
+
- Section vertical padding: 64-96px.
|
|
329
|
+
- Card grid: 16-24px gap.
|
|
330
|
+
|
|
331
|
+
## 6. Pages
|
|
332
|
+
- **home** — hero with CTA, features grid (3 cols), pricing cards, footer
|
|
333
|
+
- **dashboard** — sidebar nav, stats bar, task list with filters
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Write DESIGN.md first, then derive `global.css` tokens from it. Update the document as the design evolves.
|
|
337
|
+
|
|
338
|
+
### 2. Derive global.css from DESIGN.md
|
|
339
|
+
|
|
340
|
+
Translate the colors, typography, and spacing from DESIGN.md into CSS custom properties in `global.css`:
|
|
341
|
+
|
|
342
|
+
```css
|
|
343
|
+
:root {
|
|
344
|
+
/* From §2 Color Palette */
|
|
345
|
+
--color-primary: #2563eb;
|
|
346
|
+
--color-accent: #f59e0b;
|
|
347
|
+
--color-text: #0f172a;
|
|
348
|
+
--color-muted: #64748b;
|
|
349
|
+
--color-bg: #ffffff;
|
|
350
|
+
--color-surface: #f1f5f9;
|
|
351
|
+
--color-border: #e2e8f0;
|
|
352
|
+
--color-error: #ef4444;
|
|
353
|
+
--color-success: #22c55e;
|
|
354
|
+
|
|
355
|
+
/* From §4 Component Stylings */
|
|
356
|
+
--radius-sm: 4px;
|
|
357
|
+
--radius-md: 8px;
|
|
358
|
+
--radius-lg: 12px;
|
|
359
|
+
|
|
360
|
+
/* From §5 Layout & Spacing */
|
|
361
|
+
--space-xs: 4px;
|
|
362
|
+
--space-sm: 8px;
|
|
363
|
+
--space-md: 16px;
|
|
364
|
+
--space-lg: 24px;
|
|
365
|
+
--space-xl: 48px;
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Pages should reference these variables (`var(--color-primary)`) instead of hardcoding values. This ensures consistency and makes global changes a single-file edit.
|
|
370
|
+
|
|
371
|
+
### 3. Build section by section, screenshot after each
|
|
372
|
+
|
|
373
|
+
Don't write an entire page before checking the result. Build incrementally:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
1. Write the hero section HTML + CSS
|
|
377
|
+
2. dekit screenshot '$${home@.hero}' # Verify just the hero
|
|
378
|
+
3. Write the features grid
|
|
379
|
+
4. dekit screenshot '$${home}' # Verify overall layout
|
|
380
|
+
5. Continue section by section
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Catching issues early avoids cascading problems. A broken layout in the hero affects everything below it.
|
|
384
|
+
|
|
385
|
+
### 4. Use properties for state variants
|
|
386
|
+
|
|
387
|
+
When a page has multiple states (empty, loading, error, different data counts), use properties instead of duplicating pages:
|
|
388
|
+
|
|
389
|
+
```yaml
|
|
390
|
+
pages:
|
|
391
|
+
dashboard:
|
|
392
|
+
template: pages/dashboard/dashboard.html
|
|
393
|
+
style: pages/dashboard/dashboard.css
|
|
394
|
+
properties:
|
|
395
|
+
taskCount:
|
|
396
|
+
type: number
|
|
397
|
+
default: 5
|
|
398
|
+
showEmptyState:
|
|
399
|
+
type: boolean
|
|
400
|
+
default: false
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Then screenshot each variant:
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
dekit screenshot '$${dashboard}' # Normal state
|
|
407
|
+
dekit screenshot '$${dashboard}' --props showEmptyState=true # Empty state
|
|
408
|
+
dekit screenshot '$${dashboard}' --props taskCount=0 # Zero items
|
|
409
|
+
dekit screenshot '$${dashboard}' --props taskCount=100 # Many items
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### 5. Extract repeated patterns into components
|
|
413
|
+
|
|
414
|
+
When you see the same HTML structure appear twice or more, extract it into a Web Component:
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
dekit add component ui-card
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Move the shared markup into the component's `<template>` with `<slot>` for variable content. This keeps pages DRY and makes global updates to the pattern a single-file change.
|
|
421
|
+
|
|
234
422
|
## Design Workflow
|
|
235
423
|
|
|
236
424
|
### Self-directed design loop
|
|
237
425
|
|
|
238
426
|
```
|
|
239
427
|
1. dekit init --template landing # Start project
|
|
240
|
-
2.
|
|
241
|
-
3.
|
|
242
|
-
4.
|
|
243
|
-
5.
|
|
244
|
-
6.
|
|
245
|
-
7.
|
|
246
|
-
8.
|
|
428
|
+
2. Read .dekit/DESIGN.md or generate one # Design system first
|
|
429
|
+
3. Derive design tokens in global.css # From DESIGN.md
|
|
430
|
+
4. Build page section by section # Incremental progress
|
|
431
|
+
5. dekit screenshot '$${home}' # Verify after each section
|
|
432
|
+
6. Read the screenshot, iterate # Fix issues early
|
|
433
|
+
7. Extract components when patterns repeat
|
|
434
|
+
8. Screenshot all property variants # Cover edge cases
|
|
435
|
+
9. dekit serve # Let human review
|
|
247
436
|
```
|
|
248
437
|
|
|
249
438
|
### Human feedback loop
|
package/editor/src/App.tsx
CHANGED
|
@@ -56,6 +56,14 @@ export function App() {
|
|
|
56
56
|
if (data.pages.length > 0) {
|
|
57
57
|
setCurrentPage(data.pages[0]);
|
|
58
58
|
}
|
|
59
|
+
if (data.device) {
|
|
60
|
+
const preset = DEVICE_PRESETS.find(
|
|
61
|
+
(d) => d.name === data.device!.name
|
|
62
|
+
);
|
|
63
|
+
setDeviceMode(preset ? preset.name : "custom");
|
|
64
|
+
setDeviceWidth(data.device.width);
|
|
65
|
+
setDeviceHeight(data.device.height);
|
|
66
|
+
}
|
|
59
67
|
});
|
|
60
68
|
}, []);
|
|
61
69
|
|
package/editor/src/types.ts
CHANGED
|
@@ -14,10 +14,18 @@ export interface DOMTreeNode {
|
|
|
14
14
|
children: DOMTreeNode[];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface DeviceConfig {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
dpr: number;
|
|
21
|
+
name?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export interface EditorConfig {
|
|
18
25
|
pages: string[];
|
|
19
26
|
pagePaths: Record<string, string>;
|
|
20
27
|
components: string[];
|
|
28
|
+
device: DeviceConfig | null;
|
|
21
29
|
designServerUrl: string;
|
|
22
30
|
}
|
|
23
31
|
|
package/package.json
CHANGED
package/src/cli/init.test.ts
CHANGED
|
@@ -48,6 +48,28 @@ describe("runInit", () => {
|
|
|
48
48
|
await expect(runInit([tempDir])).rejects.toThrow(/already exists/i);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
test("mobile template includes device by default", async () => {
|
|
52
|
+
await runInit([tempDir, "--template", "mobile"]);
|
|
53
|
+
|
|
54
|
+
const content = readFileSync(join(tempDir, ".dekit/dekit.yaml"), "utf-8");
|
|
55
|
+
expect(content).toContain("iPhone 16");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("--device flag writes device to dekit.yaml", async () => {
|
|
59
|
+
await runInit([tempDir, "--device", "iPad Air"]);
|
|
60
|
+
|
|
61
|
+
const content = readFileSync(join(tempDir, ".dekit/dekit.yaml"), "utf-8");
|
|
62
|
+
expect(content).toContain("iPad Air");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("--device flag overrides template default device", async () => {
|
|
66
|
+
await runInit([tempDir, "--template", "mobile", "--device", "Pixel 8"]);
|
|
67
|
+
|
|
68
|
+
const content = readFileSync(join(tempDir, ".dekit/dekit.yaml"), "utf-8");
|
|
69
|
+
expect(content).toContain("Pixel 8");
|
|
70
|
+
expect(content).not.toContain("iPhone 16");
|
|
71
|
+
});
|
|
72
|
+
|
|
51
73
|
test("lists templates when --template given without value", async () => {
|
|
52
74
|
const consoleSpy = vi.spyOn(console, "log");
|
|
53
75
|
await runInit(["--template"]);
|
package/src/cli/init.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, cpSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve, join } from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
3
4
|
|
|
4
5
|
const TEMPLATES_DIR = join(import.meta.dirname, "../../templates/projects");
|
|
5
6
|
|
|
@@ -8,6 +9,7 @@ const AVAILABLE_TEMPLATES = ["blank", "landing", "dashboard", "mobile"];
|
|
|
8
9
|
export async function runInit(args: string[]) {
|
|
9
10
|
let targetDir = process.cwd();
|
|
10
11
|
let template = "blank";
|
|
12
|
+
let deviceArg: string | undefined;
|
|
11
13
|
|
|
12
14
|
for (let i = 0; i < args.length; i++) {
|
|
13
15
|
const arg = args[i];
|
|
@@ -19,6 +21,8 @@ export async function runInit(args: string[]) {
|
|
|
19
21
|
}
|
|
20
22
|
template = next;
|
|
21
23
|
i++;
|
|
24
|
+
} else if (arg === "--device") {
|
|
25
|
+
deviceArg = args[++i];
|
|
22
26
|
} else if (!arg.startsWith("-")) {
|
|
23
27
|
targetDir = resolve(arg);
|
|
24
28
|
}
|
|
@@ -44,5 +48,12 @@ export async function runInit(args: string[]) {
|
|
|
44
48
|
mkdirSync(dekitDir, { recursive: true });
|
|
45
49
|
cpSync(templateDir, dekitDir, { recursive: true });
|
|
46
50
|
|
|
51
|
+
if (deviceArg) {
|
|
52
|
+
const yamlPath = join(dekitDir, "dekit.yaml");
|
|
53
|
+
const doc = yaml.load(readFileSync(yamlPath, "utf-8")) as Record<string, unknown>;
|
|
54
|
+
doc.device = deviceArg;
|
|
55
|
+
writeFileSync(yamlPath, yaml.dump(doc, { quotingType: '"', forceQuotes: false }));
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
console.log(`Created dekit project in ${dekitDir} (template: ${template})`);
|
|
48
59
|
}
|
package/src/cli/screenshot.ts
CHANGED
|
@@ -19,6 +19,7 @@ function parseArgs(args: string[]): {
|
|
|
19
19
|
width?: number;
|
|
20
20
|
height?: number;
|
|
21
21
|
fullPage: boolean;
|
|
22
|
+
props: Record<string, string>;
|
|
22
23
|
} {
|
|
23
24
|
let ref: string | undefined;
|
|
24
25
|
let outputPath: string | undefined;
|
|
@@ -26,6 +27,7 @@ function parseArgs(args: string[]): {
|
|
|
26
27
|
let width: number | undefined;
|
|
27
28
|
let height: number | undefined;
|
|
28
29
|
let fullPage = false;
|
|
30
|
+
const props: Record<string, string> = {};
|
|
29
31
|
|
|
30
32
|
for (let i = 0; i < args.length; i++) {
|
|
31
33
|
const arg = args[i];
|
|
@@ -39,6 +41,13 @@ function parseArgs(args: string[]): {
|
|
|
39
41
|
height = parseInt(args[++i], 10);
|
|
40
42
|
} else if (arg === "--full-page") {
|
|
41
43
|
fullPage = true;
|
|
44
|
+
} else if (arg === "--props") {
|
|
45
|
+
const kv = args[++i];
|
|
46
|
+
const eqIdx = kv.indexOf("=");
|
|
47
|
+
if (eqIdx === -1) {
|
|
48
|
+
throw new Error(`Invalid --props format: "${kv}". Expected key=value`);
|
|
49
|
+
}
|
|
50
|
+
props[kv.slice(0, eqIdx)] = kv.slice(eqIdx + 1);
|
|
42
51
|
} else if (!arg.startsWith("-")) {
|
|
43
52
|
ref = arg;
|
|
44
53
|
}
|
|
@@ -48,19 +57,19 @@ function parseArgs(args: string[]): {
|
|
|
48
57
|
throw new Error("Missing ref argument. Expected format: $${page} or $${page@selector}");
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
return { ref, outputPath, deviceName, width, height, fullPage };
|
|
60
|
+
return { ref, outputPath, deviceName, width, height, fullPage, props };
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
export async function runScreenshot(config: DesignConfig, args: string[]) {
|
|
55
|
-
const { ref, outputPath, deviceName, width, height, fullPage } = parseArgs(args);
|
|
64
|
+
const { ref, outputPath, deviceName, width, height, fullPage, props } = parseArgs(args);
|
|
56
65
|
|
|
57
66
|
// Parse and validate the ref (throws on invalid format)
|
|
58
67
|
const parsedRef = parseRef(ref);
|
|
59
68
|
|
|
60
|
-
// Resolve viewport from --device or --width/--height flags
|
|
61
|
-
let viewportWidth = 1280;
|
|
62
|
-
let viewportHeight = 800;
|
|
63
|
-
let devicePixelRatio = 1;
|
|
69
|
+
// Resolve viewport from --device or --width/--height flags, falling back to config.device
|
|
70
|
+
let viewportWidth = config.device?.width ?? 1280;
|
|
71
|
+
let viewportHeight = config.device?.height ?? 800;
|
|
72
|
+
let devicePixelRatio = config.device?.dpr ?? 1;
|
|
64
73
|
|
|
65
74
|
if (deviceName) {
|
|
66
75
|
const normalizedInput = deviceName.toLowerCase().replace(/\s+/g, "-");
|
|
@@ -131,7 +140,12 @@ export async function runScreenshot(config: DesignConfig, args: string[]) {
|
|
|
131
140
|
});
|
|
132
141
|
|
|
133
142
|
const pageKey = parsedRef.pageKey;
|
|
134
|
-
const
|
|
143
|
+
const urlObj = new URL(`http://localhost:${port}/page/${pageKey}`);
|
|
144
|
+
urlObj.searchParams.set("noinspector", "");
|
|
145
|
+
for (const [key, value] of Object.entries(props)) {
|
|
146
|
+
urlObj.searchParams.set(`props.${key}`, value);
|
|
147
|
+
}
|
|
148
|
+
const url = urlObj.toString();
|
|
135
149
|
await page.goto(url, { waitUntil: "networkidle0" });
|
|
136
150
|
|
|
137
151
|
if (parsedRef.type === "element") {
|
package/src/design-server.ts
CHANGED
|
@@ -20,6 +20,5 @@ export async function startDesignServer(
|
|
|
20
20
|
await server.listen();
|
|
21
21
|
const address = server.httpServer!.address();
|
|
22
22
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
23
|
-
console.log(` Design server running at http://localhost:${port}`);
|
|
24
23
|
return server;
|
|
25
24
|
}
|
package/src/editor-server.ts
CHANGED
|
@@ -20,10 +20,18 @@ export async function startEditorServer(
|
|
|
20
20
|
for (const [name, def] of Object.entries(config.pages)) {
|
|
21
21
|
pagePaths[name] = join(config.baseDir, def.template);
|
|
22
22
|
}
|
|
23
|
+
const pageProperties: Record<string, Record<string, { type: string; default: unknown }>> = {};
|
|
24
|
+
for (const [name, def] of Object.entries(config.pages)) {
|
|
25
|
+
if (def.properties) {
|
|
26
|
+
pageProperties[name] = def.properties;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
23
29
|
const payload = {
|
|
24
30
|
pages: Object.keys(config.pages),
|
|
25
31
|
pagePaths,
|
|
32
|
+
pageProperties,
|
|
26
33
|
components: Object.keys(config.components),
|
|
34
|
+
device: config.device ?? null,
|
|
27
35
|
designServerUrl: `http://localhost:${designPort}`,
|
|
28
36
|
};
|
|
29
37
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -62,7 +70,6 @@ export async function startEditorServer(
|
|
|
62
70
|
|
|
63
71
|
return new Promise((resolve) => {
|
|
64
72
|
server.listen(port, () => {
|
|
65
|
-
console.log(` Editor server running at http://localhost:${port}`);
|
|
66
73
|
resolve(server);
|
|
67
74
|
});
|
|
68
75
|
});
|
package/src/parser.test.ts
CHANGED
|
@@ -55,6 +55,170 @@ pages:
|
|
|
55
55
|
).rejects.toThrow(/not found|does not exist/i);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
test("parses device as preset name string", async () => {
|
|
59
|
+
const yaml = `
|
|
60
|
+
version: 1.0
|
|
61
|
+
device: "iPhone 16"
|
|
62
|
+
pages: {}
|
|
63
|
+
`;
|
|
64
|
+
const config = await parseDesignConfig("/fake/dekit.yaml", yaml);
|
|
65
|
+
expect(config.device).toEqual({
|
|
66
|
+
width: 393,
|
|
67
|
+
height: 852,
|
|
68
|
+
dpr: 3,
|
|
69
|
+
name: "iPhone 16",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("parses device as custom dimensions object", async () => {
|
|
74
|
+
const yaml = `
|
|
75
|
+
version: 1.0
|
|
76
|
+
device:
|
|
77
|
+
width: 400
|
|
78
|
+
height: 800
|
|
79
|
+
dpr: 2
|
|
80
|
+
pages: {}
|
|
81
|
+
`;
|
|
82
|
+
const config = await parseDesignConfig("/fake/dekit.yaml", yaml);
|
|
83
|
+
expect(config.device).toEqual({
|
|
84
|
+
width: 400,
|
|
85
|
+
height: 800,
|
|
86
|
+
dpr: 2,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("device defaults dpr to 1 when omitted", async () => {
|
|
91
|
+
const yaml = `
|
|
92
|
+
version: 1.0
|
|
93
|
+
device:
|
|
94
|
+
width: 400
|
|
95
|
+
height: 800
|
|
96
|
+
pages: {}
|
|
97
|
+
`;
|
|
98
|
+
const config = await parseDesignConfig("/fake/dekit.yaml", yaml);
|
|
99
|
+
expect(config.device!.dpr).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("device is undefined when not specified", async () => {
|
|
103
|
+
const yaml = `
|
|
104
|
+
version: 1.0
|
|
105
|
+
pages: {}
|
|
106
|
+
`;
|
|
107
|
+
const config = await parseDesignConfig("/fake/dekit.yaml", yaml);
|
|
108
|
+
expect(config.device).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("rejects unknown device preset name", async () => {
|
|
112
|
+
const yaml = `
|
|
113
|
+
version: 1.0
|
|
114
|
+
device: "Nokia 3310"
|
|
115
|
+
pages: {}
|
|
116
|
+
`;
|
|
117
|
+
await expect(
|
|
118
|
+
parseDesignConfig("/fake/dekit.yaml", yaml)
|
|
119
|
+
).rejects.toThrow(/Unknown device/i);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("rejects device object without width/height", async () => {
|
|
123
|
+
const yaml = `
|
|
124
|
+
version: 1.0
|
|
125
|
+
device:
|
|
126
|
+
dpr: 2
|
|
127
|
+
pages: {}
|
|
128
|
+
`;
|
|
129
|
+
await expect(
|
|
130
|
+
parseDesignConfig("/fake/dekit.yaml", yaml)
|
|
131
|
+
).rejects.toThrow(/width.*height/i);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("parses page properties", async () => {
|
|
135
|
+
const yaml = `
|
|
136
|
+
version: 1.0
|
|
137
|
+
pages:
|
|
138
|
+
home:
|
|
139
|
+
template: "pages/home/home.html"
|
|
140
|
+
style: "pages/home/home.css"
|
|
141
|
+
properties:
|
|
142
|
+
showBanner:
|
|
143
|
+
type: boolean
|
|
144
|
+
default: true
|
|
145
|
+
itemCount:
|
|
146
|
+
type: number
|
|
147
|
+
default: 3
|
|
148
|
+
title:
|
|
149
|
+
type: string
|
|
150
|
+
default: "Hello"
|
|
151
|
+
`;
|
|
152
|
+
const config = await parseDesignConfig("/fake/dekit.yaml", yaml);
|
|
153
|
+
expect(config.pages["home"].properties).toEqual({
|
|
154
|
+
showBanner: { type: "boolean", default: true },
|
|
155
|
+
itemCount: { type: "number", default: 3 },
|
|
156
|
+
title: { type: "string", default: "Hello" },
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("page without properties has undefined properties", async () => {
|
|
161
|
+
const yaml = `
|
|
162
|
+
version: 1.0
|
|
163
|
+
pages:
|
|
164
|
+
home:
|
|
165
|
+
template: "pages/home/home.html"
|
|
166
|
+
style: "pages/home/home.css"
|
|
167
|
+
`;
|
|
168
|
+
const config = await parseDesignConfig("/fake/dekit.yaml", yaml);
|
|
169
|
+
expect(config.pages["home"].properties).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("rejects invalid property type", async () => {
|
|
173
|
+
const yaml = `
|
|
174
|
+
version: 1.0
|
|
175
|
+
pages:
|
|
176
|
+
home:
|
|
177
|
+
template: "pages/home/home.html"
|
|
178
|
+
style: "pages/home/home.css"
|
|
179
|
+
properties:
|
|
180
|
+
color:
|
|
181
|
+
type: color
|
|
182
|
+
default: red
|
|
183
|
+
`;
|
|
184
|
+
await expect(
|
|
185
|
+
parseDesignConfig("/fake/dekit.yaml", yaml)
|
|
186
|
+
).rejects.toThrow(/Invalid type.*color/i);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("rejects property with mismatched default type", async () => {
|
|
190
|
+
const yaml = `
|
|
191
|
+
version: 1.0
|
|
192
|
+
pages:
|
|
193
|
+
home:
|
|
194
|
+
template: "pages/home/home.html"
|
|
195
|
+
style: "pages/home/home.css"
|
|
196
|
+
properties:
|
|
197
|
+
count:
|
|
198
|
+
type: number
|
|
199
|
+
default: "not a number"
|
|
200
|
+
`;
|
|
201
|
+
await expect(
|
|
202
|
+
parseDesignConfig("/fake/dekit.yaml", yaml)
|
|
203
|
+
).rejects.toThrow(/must be a number/i);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("rejects property missing default", async () => {
|
|
207
|
+
const yaml = `
|
|
208
|
+
version: 1.0
|
|
209
|
+
pages:
|
|
210
|
+
home:
|
|
211
|
+
template: "pages/home/home.html"
|
|
212
|
+
style: "pages/home/home.css"
|
|
213
|
+
properties:
|
|
214
|
+
flag:
|
|
215
|
+
type: boolean
|
|
216
|
+
`;
|
|
217
|
+
await expect(
|
|
218
|
+
parseDesignConfig("/fake/dekit.yaml", yaml)
|
|
219
|
+
).rejects.toThrow(/Missing default/i);
|
|
220
|
+
});
|
|
221
|
+
|
|
58
222
|
test("rejects missing version field", async () => {
|
|
59
223
|
const yaml = `
|
|
60
224
|
global-style: "global.css"
|