dekit-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { DEVICE_PRESETS, calcScale } from "./devices.js";
3
+
4
+ describe("DEVICE_PRESETS", () => {
5
+ test("has at least 10 presets", () => {
6
+ expect(DEVICE_PRESETS.length).toBeGreaterThanOrEqual(10);
7
+ });
8
+
9
+ test("every preset has valid dimensions", () => {
10
+ for (const d of DEVICE_PRESETS) {
11
+ expect(d.name).toBeTruthy();
12
+ expect(d.width).toBeGreaterThan(0);
13
+ expect(d.height).toBeGreaterThan(0);
14
+ expect(d.dpr).toBeGreaterThan(0);
15
+ }
16
+ });
17
+
18
+ test("first preset is a phone", () => {
19
+ expect(DEVICE_PRESETS[0].width).toBeLessThan(500);
20
+ });
21
+ });
22
+
23
+ describe("calcScale", () => {
24
+ test("fits a phone into a large canvas", () => {
25
+ const scale = calcScale(393, 852, 800, 600);
26
+ // 800/393 = 2.03, 600/852 = 0.70 → min is 0.70, clamped to ≤1
27
+ expect(scale).toBeCloseTo(0.704, 2);
28
+ });
29
+
30
+ test("never scales above 1", () => {
31
+ const scale = calcScale(200, 200, 800, 600);
32
+ expect(scale).toBe(1);
33
+ });
34
+
35
+ test("handles equal dimensions", () => {
36
+ const scale = calcScale(800, 600, 800, 600);
37
+ expect(scale).toBe(1);
38
+ });
39
+
40
+ test("handles device wider than canvas", () => {
41
+ const scale = calcScale(1920, 1080, 800, 600);
42
+ // 800/1920 = 0.417, 600/1080 = 0.556 → min is 0.417
43
+ expect(scale).toBeCloseTo(0.417, 2);
44
+ });
45
+
46
+ test("clamps to minimum 0.1", () => {
47
+ const scale = calcScale(10000, 10000, 100, 100);
48
+ expect(scale).toBe(0.1);
49
+ });
50
+ });
@@ -0,0 +1,33 @@
1
+ import type { DevicePreset } from "./types.js";
2
+
3
+ export const DEVICE_PRESETS: DevicePreset[] = [
4
+ { name: "iPhone SE", width: 375, height: 667, dpr: 2 },
5
+ { name: "iPhone 14", width: 390, height: 844, dpr: 3 },
6
+ { name: "iPhone 14 Pro", width: 393, height: 852, dpr: 3 },
7
+ { name: "iPhone 14 Pro Max", width: 430, height: 932, dpr: 3 },
8
+ { name: "iPhone 16", width: 393, height: 852, dpr: 3 },
9
+ { name: "iPhone 16 Pro Max", width: 440, height: 956, dpr: 3 },
10
+ { name: "iPad Mini", width: 744, height: 1133, dpr: 2 },
11
+ { name: "iPad Air", width: 820, height: 1180, dpr: 2 },
12
+ { name: 'iPad Pro 12.9"', width: 1024, height: 1366, dpr: 2 },
13
+ { name: "Samsung Galaxy S24", width: 360, height: 780, dpr: 3 },
14
+ { name: "Pixel 8", width: 412, height: 915, dpr: 2.625 },
15
+ { name: "Desktop 1280", width: 1280, height: 800, dpr: 1 },
16
+ { name: "Desktop 1440", width: 1440, height: 900, dpr: 1 },
17
+ { name: "Desktop 1920", width: 1920, height: 1080, dpr: 1 },
18
+ ];
19
+
20
+ /**
21
+ * Calculate the scale factor to fit device dimensions into available space.
22
+ * Returns a value between 0.1 and 1.
23
+ */
24
+ export function calcScale(
25
+ deviceWidth: number,
26
+ deviceHeight: number,
27
+ availableWidth: number,
28
+ availableHeight: number
29
+ ): number {
30
+ const scaleX = availableWidth / deviceWidth;
31
+ const scaleY = availableHeight / deviceHeight;
32
+ return Math.max(0.1, Math.min(scaleX, scaleY, 1));
33
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { OverlayScrollbars } from "overlayscrollbars";
4
+ import "overlayscrollbars/overlayscrollbars.css";
5
+ import { App } from "./App.js";
6
+ import "./styles/editor.css";
7
+
8
+ OverlayScrollbars.env().setDefaultOptions({
9
+ scrollbars: { theme: "os-theme-dark", autoHide: "move", autoHideDelay: 500 },
10
+ });
11
+
12
+ const root = createRoot(document.getElementById("root")!);
13
+ root.render(<App />);
@@ -0,0 +1,451 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
9
+ overflow: hidden;
10
+ height: 100vh;
11
+ background: #1e1e1e;
12
+ color: #cccccc;
13
+ }
14
+
15
+ #root {
16
+ height: 100vh;
17
+ }
18
+
19
+ .editor-layout {
20
+ display: flex;
21
+ height: 100vh;
22
+ }
23
+
24
+ .left-panel {
25
+ width: 240px;
26
+ background: #252526;
27
+ border-right: 1px solid #4a4a4a;
28
+ display: flex;
29
+ flex-direction: column;
30
+ overflow: hidden;
31
+ }
32
+
33
+ .center-panel {
34
+ flex: 1;
35
+ background: #1e1e1e;
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: stretch;
39
+ padding: 0;
40
+ min-width: 0;
41
+ }
42
+
43
+ .right-panel {
44
+ width: 280px;
45
+ background: #252526;
46
+ border-left: 1px solid #4a4a4a;
47
+ display: flex;
48
+ flex-direction: column;
49
+ overflow: hidden;
50
+ }
51
+
52
+ .right-panel-top {
53
+ flex: 0 0 35%;
54
+ display: flex;
55
+ flex-direction: column;
56
+ overflow: hidden;
57
+ border-bottom: 1px solid #4a4a4a;
58
+ }
59
+
60
+ .right-panel-top .layer-tree-scroll {
61
+ flex: 1;
62
+ }
63
+
64
+ .right-panel-bottom {
65
+ flex: 1;
66
+ display: flex;
67
+ flex-direction: column;
68
+ overflow: hidden;
69
+ }
70
+
71
+ .right-panel-bottom .properties-scroll {
72
+ flex: 1;
73
+ }
74
+
75
+ .page-list-scroll {
76
+ flex: 1;
77
+ }
78
+
79
+ .panel-header {
80
+ height: 28px;
81
+ display: flex;
82
+ align-items: center;
83
+ padding: 0 12px;
84
+ font-size: 11px;
85
+ font-weight: 600;
86
+ text-transform: uppercase;
87
+ letter-spacing: 0.5px;
88
+ color: #969696;
89
+ background: #3c3c3c;
90
+ border-bottom: 1px solid #4a4a4a;
91
+ user-select: none;
92
+ flex-shrink: 0;
93
+ }
94
+
95
+ .page-list {
96
+ list-style: none;
97
+ }
98
+
99
+ .page-item {
100
+ padding: 6px 12px;
101
+ cursor: pointer;
102
+ font-size: 13px;
103
+ color: #cccccc;
104
+ user-select: none;
105
+ }
106
+
107
+ .page-item:hover {
108
+ background: #2a2d2e;
109
+ }
110
+
111
+ .page-item.active {
112
+ background: #2a2d2e;
113
+ }
114
+
115
+ .page-list.focused .page-item.active {
116
+ background: #37373d;
117
+ color: #ffffff;
118
+ }
119
+
120
+ .page-list:focus {
121
+ outline: none;
122
+ }
123
+
124
+ .canvas-iframe {
125
+ width: 100%;
126
+ height: 100%;
127
+ border: 1px solid #3c3c3c;
128
+ border-radius: 4px;
129
+ background: #ffffff;
130
+ }
131
+
132
+
133
+ .layer-tree {
134
+ padding: 4px 0;
135
+ font-size: 13px;
136
+ }
137
+
138
+ .layer-node {
139
+ padding: 3px 0;
140
+ cursor: pointer;
141
+ white-space: nowrap;
142
+ user-select: none;
143
+ color: #cccccc;
144
+ }
145
+
146
+ .layer-node:hover {
147
+ background: #2a2d2e;
148
+ }
149
+
150
+ .layer-node.selected {
151
+ background: #2a2d2e;
152
+ }
153
+
154
+ .layer-tree.focused .layer-node.selected {
155
+ background: #37373d;
156
+ color: #ffffff;
157
+ }
158
+
159
+ .layer-tree:focus {
160
+ outline: none;
161
+ }
162
+
163
+ .layer-tag {
164
+ color: #569cd6;
165
+ }
166
+
167
+ .layer-class {
168
+ color: #9cdcfe;
169
+ margin-left: 4px;
170
+ font-size: 11px;
171
+ }
172
+
173
+ .property-section {
174
+ padding: 8px 12px;
175
+ border-bottom: 1px solid #3c3c3c;
176
+ }
177
+
178
+ .node-ref {
179
+ cursor: pointer;
180
+ color: #e2c08d;
181
+ white-space: nowrap;
182
+ }
183
+
184
+ .node-ref:hover {
185
+ color: #f5d9a0;
186
+ }
187
+
188
+ .property-value.copyable {
189
+ cursor: pointer;
190
+ }
191
+
192
+ .property-value.copyable:hover {
193
+ color: #e0a56a;
194
+ }
195
+
196
+ .property-section-title {
197
+ font-size: 11px;
198
+ font-weight: 600;
199
+ text-transform: uppercase;
200
+ color: #969696;
201
+ margin-bottom: 6px;
202
+ }
203
+
204
+ .property-row {
205
+ display: flex;
206
+ justify-content: space-between;
207
+ padding: 2px 0;
208
+ font-size: 12px;
209
+ }
210
+
211
+ .property-key {
212
+ color: #9cdcfe;
213
+ }
214
+
215
+ .property-value {
216
+ color: #ce9178;
217
+ text-align: right;
218
+ max-width: 150px;
219
+ overflow: hidden;
220
+ text-overflow: ellipsis;
221
+ }
222
+
223
+ .empty-state {
224
+ padding: 24px;
225
+ text-align: center;
226
+ color: #969696;
227
+ font-size: 13px;
228
+ }
229
+
230
+ /* Device Toolbar — matches panel headers */
231
+ .device-toolbar {
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ gap: 12px;
236
+ padding: 0 16px;
237
+ height: 28px;
238
+ background: #3c3c3c;
239
+ border-bottom: 1px solid #4a4a4a;
240
+ font-size: 12px;
241
+ line-height: 1;
242
+ color: #cccccc;
243
+ user-select: none;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .device-toolbar > * {
248
+ flex-shrink: 0;
249
+ }
250
+
251
+ .device-toolbar select {
252
+ background: transparent;
253
+ border: none;
254
+ color: #cccccc;
255
+ padding: 0 4px;
256
+ height: 20px;
257
+ font-size: 12px;
258
+ line-height: 20px;
259
+ outline: none;
260
+ cursor: pointer;
261
+ }
262
+
263
+ .device-toolbar select:hover {
264
+ color: #ffffff;
265
+ }
266
+
267
+ .device-toolbar select:focus {
268
+ color: #ffffff;
269
+ }
270
+
271
+ .device-toolbar .dim-input {
272
+ background: transparent;
273
+ border: none;
274
+ border-bottom: 1px solid transparent;
275
+ color: #cccccc;
276
+ padding: 0 2px;
277
+ height: 20px;
278
+ text-align: center;
279
+ font-size: 12px;
280
+ line-height: 20px;
281
+ outline: none;
282
+ min-width: 3ch;
283
+ }
284
+
285
+ .device-toolbar .dim-input:hover:not(:disabled) {
286
+ border-bottom-color: #666;
287
+ }
288
+
289
+ .device-toolbar .dim-input:focus:not(:disabled) {
290
+ color: #ffffff;
291
+ border-bottom-color: #2563eb;
292
+ }
293
+
294
+ .device-toolbar .dim-input:disabled {
295
+ color: #999;
296
+ cursor: default;
297
+ -moz-appearance: textfield;
298
+ }
299
+
300
+ .device-toolbar .dim-input::-webkit-inner-spin-button,
301
+ .device-toolbar .dim-input::-webkit-outer-spin-button {
302
+ -webkit-appearance: none;
303
+ margin: 0;
304
+ }
305
+
306
+ .device-toolbar .dim-separator {
307
+ color: #999;
308
+ margin: 0 -4px;
309
+ flex-shrink: 0;
310
+ }
311
+
312
+ .device-toolbar .toolbar-btn {
313
+ background: transparent;
314
+ border: none;
315
+ color: #999;
316
+ padding: 0 4px;
317
+ height: 20px;
318
+ border-radius: 3px;
319
+ cursor: pointer;
320
+ font-size: 14px;
321
+ line-height: 1;
322
+ display: inline-flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ }
326
+
327
+ .device-toolbar .toolbar-btn:hover {
328
+ color: #ffffff;
329
+ }
330
+
331
+ .device-toolbar .toolbar-btn.active {
332
+ color: #2563eb;
333
+ }
334
+
335
+ .device-toolbar .dpr-badge {
336
+ font-size: 11px;
337
+ color: #999;
338
+ height: 20px;
339
+ line-height: 20px;
340
+ }
341
+
342
+ .device-toolbar .toolbar-divider {
343
+ width: 1px;
344
+ height: 14px;
345
+ background: #555;
346
+ flex-shrink: 0;
347
+ }
348
+
349
+ .canvas-area {
350
+ flex: 1;
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ overflow: auto;
355
+ position: relative;
356
+ padding: 24px;
357
+ min-height: 0;
358
+ scrollbar-width: thin;
359
+ scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
360
+ }
361
+
362
+ .canvas-area::-webkit-scrollbar {
363
+ width: 6px;
364
+ height: 6px;
365
+ }
366
+
367
+ .canvas-area::-webkit-scrollbar-track {
368
+ background: transparent;
369
+ }
370
+
371
+ .canvas-area::-webkit-scrollbar-thumb {
372
+ background: rgba(255, 255, 255, 0.15);
373
+ border-radius: 3px;
374
+ }
375
+
376
+ .canvas-area::-webkit-scrollbar-thumb:hover {
377
+ background: rgba(255, 255, 255, 0.25);
378
+ }
379
+
380
+ .canvas-area::-webkit-scrollbar-corner {
381
+ background: transparent;
382
+ }
383
+
384
+ .canvas-area.with-grid {
385
+ background: repeating-conic-gradient(#1a1a1a 0% 25%, #1e1e1e 0% 50%) 0 0 / 16px 16px;
386
+ }
387
+
388
+ /* Iframe wrapper for transform scaling */
389
+ .iframe-wrapper {
390
+ flex-shrink: 0;
391
+ overflow: hidden;
392
+ border-radius: 4px;
393
+ }
394
+
395
+ .iframe-wrapper .canvas-iframe {
396
+ display: block;
397
+ width: 100%;
398
+ height: 100%;
399
+ border: none;
400
+ border-radius: 4px;
401
+ background: #ffffff;
402
+ box-shadow: 0 2px 16px rgba(0, 0, 0, 0.4);
403
+ }
404
+
405
+ /* OverlayScrollbars dark theme */
406
+ .os-theme-dark > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle {
407
+ background: rgba(255, 255, 255, 0.2);
408
+ }
409
+
410
+ .os-theme-dark > .os-scrollbar:hover > .os-scrollbar-track > .os-scrollbar-handle {
411
+ background: rgba(255, 255, 255, 0.35);
412
+ }
413
+
414
+ .os-theme-dark > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle.active {
415
+ background: rgba(255, 255, 255, 0.45);
416
+ }
417
+
418
+ .os-theme-dark > .os-scrollbar {
419
+ --os-size: 6px;
420
+ --os-padding-perpendicular: 1px;
421
+ --os-padding-axis: 1px;
422
+ }
423
+
424
+ .os-theme-dark > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle {
425
+ border: 1px solid rgba(255, 255, 255, 0.15);
426
+ }
427
+
428
+ /* Context menu */
429
+ .context-menu {
430
+ position: fixed;
431
+ z-index: 10000;
432
+ background: #2d2d2d;
433
+ border: 1px solid #4a4a4a;
434
+ border-radius: 4px;
435
+ padding: 4px 0;
436
+ min-width: 120px;
437
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
438
+ }
439
+
440
+ .context-menu-item {
441
+ padding: 6px 12px;
442
+ font-size: 12px;
443
+ color: #cccccc;
444
+ cursor: pointer;
445
+ user-select: none;
446
+ }
447
+
448
+ .context-menu-item:hover {
449
+ background: #094771;
450
+ color: #ffffff;
451
+ }
@@ -0,0 +1,43 @@
1
+ export interface NodeInfo {
2
+ tagName: string;
3
+ path: string;
4
+ attributes: Record<string, string>;
5
+ computedStyles: Record<string, string>;
6
+ boundingRect: { x: number; y: number; width: number; height: number };
7
+ }
8
+
9
+ export interface DOMTreeNode {
10
+ tagName: string;
11
+ path: string;
12
+ className: string;
13
+ children: DOMTreeNode[];
14
+ }
15
+
16
+ export interface EditorConfig {
17
+ pages: string[];
18
+ pagePaths: Record<string, string>;
19
+ components: string[];
20
+ designServerUrl: string;
21
+ }
22
+
23
+ // Messages from iframe → editor
24
+ export type IframeMessage =
25
+ | { type: "NODE_SELECTED"; node: NodeInfo }
26
+ | { type: "NODE_HOVERED"; node: NodeInfo }
27
+ | { type: "DOM_TREE"; tree: DOMTreeNode[] };
28
+
29
+ // Messages from editor → iframe
30
+ export type EditorMessage =
31
+ | { type: "GET_DOM_TREE" }
32
+ | { type: "HIGHLIGHT_NODE"; path: string }
33
+ | { type: "HOVER_NODE"; path: string }
34
+ | { type: "CLEAR_HIGHLIGHT" }
35
+ | { type: "SET_TOUCH_CURSOR"; enabled: boolean }
36
+ | { type: "SET_INSPECT_MODE"; enabled: boolean };
37
+
38
+ export interface DevicePreset {
39
+ name: string;
40
+ width: number;
41
+ height: number;
42
+ dpr: number;
43
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ root: __dirname,
7
+ build: {
8
+ outDir: "../dist/editor",
9
+ emptyOutDir: true,
10
+ },
11
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "dekit-cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "A code-first design toolkit — preview, inspect, and debug your HTML/CSS designs in the browser",
6
+ "keywords": ["design", "devtools", "inspector", "preview", "css", "html", "cli"],
7
+ "license": "Apache-2.0",
8
+ "repository": { "type": "git", "url": "https://github.com/myWsq/dekit" },
9
+ "homepage": "https://github.com/myWsq/dekit",
10
+ "bugs": { "url": "https://github.com/myWsq/dekit/issues" },
11
+ "files": ["bin/", "src/", "editor/", "dist/"],
12
+ "engines": { "node": ">=18" },
13
+ "bin": {
14
+ "dekit": "./bin/dekit.ts"
15
+ },
16
+ "scripts": {
17
+ "dev": "tsx bin/dekit.ts",
18
+ "build:editor": "vite build --config editor/vite.config.ts",
19
+ "test": "vitest run",
20
+ "prepublishOnly": "npm run build:editor"
21
+ },
22
+ "dependencies": {
23
+ "js-yaml": "^4.1.0",
24
+ "open": "^10.1.0",
25
+ "overlayscrollbars": "^2.14.0",
26
+ "overlayscrollbars-react": "^0.5.6",
27
+ "react-icons": "^5.6.0",
28
+ "sirv": "^3.0.0",
29
+ "vite": "^6.2.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/js-yaml": "^4.0.9",
33
+ "@types/react": "^19.0.0",
34
+ "@types/react-dom": "^19.0.0",
35
+ "react": "^19.0.0",
36
+ "react-dom": "^19.0.0",
37
+ "tsx": "^4.19.0",
38
+ "typescript": "^5.7.0",
39
+ "vitest": "^3.0.0",
40
+ "@vitejs/plugin-react": "^4.3.0"
41
+ }
42
+ }
@@ -0,0 +1,24 @@
1
+ import { createServer, type ViteDevServer } from "vite";
2
+ import type { DesignConfig } from "./types.js";
3
+ import { dekitPlugin } from "./vite-plugin-dekit.js";
4
+
5
+ export async function startDesignServer(
6
+ configRef: { current: DesignConfig },
7
+ port: number
8
+ ): Promise<ViteDevServer> {
9
+ const server = await createServer({
10
+ configFile: false,
11
+ root: configRef.current.baseDir,
12
+ server: {
13
+ port,
14
+ strictPort: true,
15
+ cors: true,
16
+ },
17
+ plugins: [dekitPlugin(configRef)],
18
+ logLevel: "silent",
19
+ });
20
+
21
+ await server.listen();
22
+ console.log(` Design server running at http://localhost:${port}`);
23
+ return server;
24
+ }