@zseven-w/pen-types 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ This package provides all TypeScript types and interfaces for the OpenPencil des
23
23
  ## Usage
24
24
 
25
25
  ```ts
26
- import type { PenDocument, PenNode, FrameNode } from '@zseven-w/pen-types'
26
+ import type { PenDocument, PenNode, FrameNode } from '@zseven-w/pen-types';
27
27
  ```
28
28
 
29
29
  ## License
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@zseven-w/pen-types",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Type definitions for OpenPencil document model",
5
+ "files": [
6
+ "src"
7
+ ],
5
8
  "type": "module",
6
9
  "exports": {
7
10
  ".": {
@@ -9,9 +12,6 @@
9
12
  "import": "./src/index.ts"
10
13
  }
11
14
  },
12
- "files": [
13
- "src"
14
- ],
15
15
  "scripts": {
16
16
  "typecheck": "tsc --noEmit"
17
17
  },
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { FRAMEWORKS } from '../codegen';
3
+ import type { NodeSnapshot, ResolvedDepContract } from '../codegen';
4
+
5
+ describe('codegen types', () => {
6
+ it('FRAMEWORKS contains all 8 frameworks', () => {
7
+ expect(FRAMEWORKS).toHaveLength(8);
8
+ expect(FRAMEWORKS).toContain('react');
9
+ expect(FRAMEWORKS).toContain('flutter');
10
+ });
11
+
12
+ it('NodeSnapshot allows truncated children', () => {
13
+ const snapshot: NodeSnapshot = {
14
+ id: 'n1',
15
+ type: 'frame',
16
+ name: 'Test',
17
+ children: '...',
18
+ } as NodeSnapshot;
19
+ expect(snapshot.children).toBe('...');
20
+ });
21
+
22
+ it('NodeSnapshot allows nested snapshots', () => {
23
+ const snapshot: NodeSnapshot = {
24
+ id: 'n1',
25
+ type: 'frame',
26
+ name: 'Parent',
27
+ children: [{ id: 'n2', type: 'rectangle', name: 'Child' } as NodeSnapshot],
28
+ } as NodeSnapshot;
29
+ expect(Array.isArray(snapshot.children)).toBe(true);
30
+ });
31
+
32
+ it('ResolvedDepContract allows null', () => {
33
+ const resolved: ResolvedDepContract = null;
34
+ expect(resolved).toBeNull();
35
+ });
36
+ });
package/src/canvas.ts CHANGED
@@ -7,25 +7,25 @@ export type ToolType =
7
7
  | 'polygon'
8
8
  | 'path'
9
9
  | 'text'
10
- | 'hand'
10
+ | 'hand';
11
11
 
12
12
  export interface ViewportState {
13
- zoom: number
14
- panX: number
15
- panY: number
13
+ zoom: number;
14
+ panX: number;
15
+ panY: number;
16
16
  }
17
17
 
18
18
  export interface SelectionState {
19
- selectedIds: string[]
20
- activeId: string | null
21
- hoveredId: string | null
22
- enteredFrameId: string | null
23
- enteredFrameStack: string[]
19
+ selectedIds: string[];
20
+ activeId: string | null;
21
+ hoveredId: string | null;
22
+ enteredFrameId: string | null;
23
+ enteredFrameStack: string[];
24
24
  }
25
25
 
26
26
  export interface CanvasInteraction {
27
- isDrawing: boolean
28
- isPanning: boolean
29
- isDragging: boolean
30
- drawStartPoint: { x: number; y: number } | null
27
+ isDrawing: boolean;
28
+ isPanning: boolean;
29
+ isDragging: boolean;
30
+ drawStartPoint: { x: number; y: number } | null;
31
31
  }
package/src/codegen.ts ADDED
@@ -0,0 +1,147 @@
1
+ import type { PenNode } from './pen.js';
2
+
3
+ // === Canonical framework type ===
4
+
5
+ export type Framework =
6
+ | 'react'
7
+ | 'vue'
8
+ | 'svelte'
9
+ | 'html'
10
+ | 'flutter'
11
+ | 'swiftui'
12
+ | 'compose'
13
+ | 'react-native';
14
+
15
+ export const FRAMEWORKS: Framework[] = [
16
+ 'react',
17
+ 'vue',
18
+ 'svelte',
19
+ 'html',
20
+ 'flutter',
21
+ 'swiftui',
22
+ 'compose',
23
+ 'react-native',
24
+ ];
25
+
26
+ // === Step 1 output: AI planner returns this (no node data, minimal tokens) ===
27
+
28
+ export interface PlannedChunk {
29
+ id: string;
30
+ name: string;
31
+ nodeIds: string[];
32
+ role: string;
33
+ suggestedComponentName: string;
34
+ dependencies: string[];
35
+ exposedSlots?: string[];
36
+ }
37
+
38
+ export interface CodePlanFromAI {
39
+ chunks: PlannedChunk[];
40
+ sharedStyles: { name: string; description: string }[];
41
+ rootLayout: { direction: string; gap: number; responsive: boolean };
42
+ }
43
+
44
+ // === Runtime: hydrated with node data + execution order ===
45
+
46
+ export interface ExecutableChunk extends PlannedChunk {
47
+ nodes: PenNode[];
48
+ order: number;
49
+ depContracts: ChunkContract[];
50
+ }
51
+
52
+ export interface CodeExecutionPlan {
53
+ chunks: ExecutableChunk[];
54
+ sharedStyles: { name: string; description: string }[];
55
+ rootLayout: { direction: string; gap: number; responsive: boolean };
56
+ }
57
+
58
+ // === Chunk contract: structured metadata output from each chunk ===
59
+
60
+ export interface ChunkContract {
61
+ chunkId: string;
62
+ componentName: string;
63
+ exportedProps: PropDef[];
64
+ slots: SlotDef[];
65
+ cssClasses: string[];
66
+ cssVariables: string[];
67
+ imports: ImportDef[];
68
+ }
69
+
70
+ export interface PropDef {
71
+ name: string;
72
+ type: string;
73
+ required: boolean;
74
+ }
75
+
76
+ export interface SlotDef {
77
+ name: string;
78
+ description: string;
79
+ }
80
+
81
+ export interface ImportDef {
82
+ source: string;
83
+ specifiers: string[];
84
+ }
85
+
86
+ // === Chunk generation output ===
87
+
88
+ export interface ChunkResult {
89
+ chunkId: string;
90
+ code: string;
91
+ contract: ChunkContract;
92
+ }
93
+
94
+ // === Progress events ===
95
+
96
+ export type ChunkStatus = 'pending' | 'running' | 'done' | 'degraded' | 'failed' | 'skipped';
97
+
98
+ export type CodeGenProgress =
99
+ | {
100
+ step: 'planning';
101
+ status: 'running' | 'done' | 'failed';
102
+ plan?: CodePlanFromAI;
103
+ error?: string;
104
+ }
105
+ | {
106
+ step: 'chunk';
107
+ chunkId: string;
108
+ name: string;
109
+ status: ChunkStatus;
110
+ result?: ChunkResult;
111
+ error?: string;
112
+ }
113
+ | { step: 'assembly'; status: 'running' | 'done' | 'failed'; error?: string }
114
+ | { step: 'complete'; finalCode: string; degraded: boolean }
115
+ | { step: 'error'; message: string; chunkId?: string };
116
+
117
+ // === Contract validation ===
118
+
119
+ export interface ContractValidationResult {
120
+ valid: boolean;
121
+ issues: string[];
122
+ }
123
+
124
+ // === Wire DTO types (MCP/CLI responses) ===
125
+
126
+ /**
127
+ * Depth-limited node snapshot for wire transfer.
128
+ * When depth is exhausted, `children` is the string `"..."` instead of NodeSnapshot[].
129
+ */
130
+ export type NodeSnapshot = Omit<PenNode, 'children'> & {
131
+ children?: NodeSnapshot[] | '...';
132
+ };
133
+
134
+ /**
135
+ * Hydrated chunk payload returned by codegen_plan and codegen_submit_chunk.
136
+ * Uses NodeSnapshot (depth-limited) instead of PenNode[].
137
+ * depContracts entries may be null when a dependency chunk failed/was skipped.
138
+ */
139
+ export interface ExecutableChunkPayload extends Omit<ExecutableChunk, 'nodes' | 'depContracts'> {
140
+ nodes: NodeSnapshot[];
141
+ depContracts: ResolvedDepContract[];
142
+ }
143
+
144
+ /**
145
+ * A dependency contract that may be absent if the upstream chunk failed or was skipped.
146
+ */
147
+ export type ResolvedDepContract = ChunkContract | null;
package/src/design-md.ts CHANGED
@@ -1,24 +1,24 @@
1
1
  export interface DesignMdSpec {
2
2
  /** Original markdown source (for round-trip fidelity) */
3
- raw: string
4
- projectName?: string
5
- visualTheme?: string
6
- colorPalette?: DesignMdColor[]
7
- typography?: DesignMdTypography
8
- componentStyles?: string
9
- layoutPrinciples?: string
10
- generationNotes?: string
3
+ raw: string;
4
+ projectName?: string;
5
+ visualTheme?: string;
6
+ colorPalette?: DesignMdColor[];
7
+ typography?: DesignMdTypography;
8
+ componentStyles?: string;
9
+ layoutPrinciples?: string;
10
+ generationNotes?: string;
11
11
  }
12
12
 
13
13
  export interface DesignMdColor {
14
- name: string
15
- hex: string
16
- role: string
14
+ name: string;
15
+ hex: string;
16
+ role: string;
17
17
  }
18
18
 
19
19
  export interface DesignMdTypography {
20
- fontFamily?: string
21
- headings?: string
22
- body?: string
23
- scale?: string
20
+ fontFamily?: string;
21
+ headings?: string;
22
+ body?: string;
23
+ scale?: string;
24
24
  }
package/src/engine.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { PenDocument } from './pen.js';
2
+ import type { ViewportState, ToolType } from './canvas.js';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Engine Options
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export interface DesignEngineOptions {
9
+ /** URL pattern for CanvasKit WASM files. */
10
+ canvasKitPath?: string | ((file: string) => string);
11
+ /** Base URL for bundled font files. */
12
+ fontBasePath?: string;
13
+ /** Custom Google Fonts CSS endpoint. */
14
+ googleFontsCssUrl?: string;
15
+ /** Icon lookup function for resolving icon names to SVG path data. */
16
+ iconLookup?: IconLookupFn;
17
+ /** Canvas background color. Default: '#1a1a1a' */
18
+ backgroundColor?: string;
19
+ /** Device pixel ratio override. */
20
+ devicePixelRatio?: number;
21
+ /** Maximum undo/redo history states. Default: 300 */
22
+ maxHistoryStates?: number;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Engine Events
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export interface DesignEngineEvents {
30
+ /** Fired after document mutation (batch-aware: only once per batch). Payload is immutable ref. */
31
+ 'document:change': (doc: PenDocument) => void;
32
+ 'selection:change': (ids: string[]) => void;
33
+ 'viewport:change': (state: ViewportState) => void;
34
+ 'tool:change': (tool: ToolType) => void;
35
+ 'history:change': (state: { canUndo: boolean; canRedo: boolean }) => void;
36
+ 'node:hover': (id: string | null) => void;
37
+ 'page:change': (pageId: string) => void;
38
+ /** Fired after canvas re-render (browser adapter only). */
39
+ render: () => void;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Code Generation
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export type CodePlatform =
47
+ | 'react'
48
+ | 'html'
49
+ | 'css'
50
+ | 'vue'
51
+ | 'svelte'
52
+ | 'flutter'
53
+ | 'swiftui'
54
+ | 'compose'
55
+ | 'react-native';
56
+
57
+ /** Structured code generation result. */
58
+ export interface CodeResult {
59
+ files: Array<{ filename: string; content: string; language: string }>;
60
+ /** CSS variables block if the document uses design variables. */
61
+ variables?: string;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Icon Lookup
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /** Injectable icon lookup function for resolving icon names to SVG path data. */
69
+ export interface IconLookupFn {
70
+ (name: string): { d: string; iconId: string; style: 'stroke' | 'fill' } | null;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Canvas Interaction Types
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export interface TextEditState {
78
+ nodeId: string;
79
+ x: number;
80
+ y: number;
81
+ w: number;
82
+ h: number;
83
+ content: string;
84
+ fontSize: number;
85
+ fontFamily: string;
86
+ fontWeight: number;
87
+ textAlign: string;
88
+ color: string;
89
+ lineHeight: number;
90
+ }
91
+
92
+ export interface AgentIndicatorEntry {
93
+ nodeId: string;
94
+ color: string;
95
+ name: string;
96
+ }
97
+
98
+ export interface AgentFrameEntry {
99
+ frameId: string;
100
+ color: string;
101
+ name: string;
102
+ }
103
+
104
+ export interface InsertionIndicator {
105
+ x: number;
106
+ y: number;
107
+ length: number;
108
+ orientation: 'horizontal' | 'vertical';
109
+ }
110
+
111
+ export interface ContainerHighlight {
112
+ x: number;
113
+ y: number;
114
+ w: number;
115
+ h: number;
116
+ }
package/src/index.ts CHANGED
@@ -12,22 +12,13 @@ export type {
12
12
  ShadowEffect,
13
13
  PenEffect,
14
14
  StyledTextSegment,
15
- } from './styles.js'
15
+ } from './styles.js';
16
16
 
17
17
  // Variables
18
- export type {
19
- VariableDefinition,
20
- VariableValue,
21
- ThemedValue,
22
- } from './variables.js'
18
+ export type { VariableDefinition, VariableValue, ThemedValue } from './variables.js';
23
19
 
24
20
  // Canvas
25
- export type {
26
- ToolType,
27
- ViewportState,
28
- SelectionState,
29
- CanvasInteraction,
30
- } from './canvas.js'
21
+ export type { ToolType, ViewportState, SelectionState, CanvasInteraction } from './canvas.js';
31
22
 
32
23
  // Document model
33
24
  export type {
@@ -43,6 +34,9 @@ export type {
43
34
  EllipseNode,
44
35
  LineNode,
45
36
  PolygonNode,
37
+ PenPathHandle,
38
+ PenPathAnchor,
39
+ PenPathPointType,
46
40
  PathNode,
47
41
  TextNode,
48
42
  ImageFitMode,
@@ -50,24 +44,51 @@ export type {
50
44
  IconFontNode,
51
45
  RefNode,
52
46
  PenNode,
53
- } from './pen.js'
47
+ } from './pen.js';
54
48
 
55
49
  // UIKit
56
- export type {
57
- ComponentCategory,
58
- KitComponent,
59
- UIKit,
60
- } from './uikit.js'
50
+ export type { ComponentCategory, KitComponent, UIKit } from './uikit.js';
61
51
 
62
52
  // Theme presets
63
- export type {
64
- ThemePreset,
65
- ThemePresetFile,
66
- } from './theme-preset.js'
53
+ export type { ThemePreset, ThemePresetFile } from './theme-preset.js';
67
54
 
68
55
  // Design.md
56
+ export type { DesignMdSpec, DesignMdColor, DesignMdTypography } from './design-md.js';
57
+
58
+ // Layout
59
+ export type { Padding } from './layout.js';
60
+
61
+ // Engine
62
+ export type {
63
+ DesignEngineOptions,
64
+ DesignEngineEvents,
65
+ CodePlatform,
66
+ CodeResult,
67
+ TextEditState,
68
+ AgentIndicatorEntry,
69
+ AgentFrameEntry,
70
+ InsertionIndicator,
71
+ ContainerHighlight,
72
+ IconLookupFn,
73
+ } from './engine.js';
74
+
75
+ // Codegen
69
76
  export type {
70
- DesignMdSpec,
71
- DesignMdColor,
72
- DesignMdTypography,
73
- } from './design-md.js'
77
+ Framework,
78
+ PlannedChunk,
79
+ CodePlanFromAI,
80
+ ExecutableChunk,
81
+ CodeExecutionPlan,
82
+ ChunkContract,
83
+ PropDef,
84
+ SlotDef,
85
+ ImportDef,
86
+ ChunkResult,
87
+ ChunkStatus,
88
+ CodeGenProgress,
89
+ ContractValidationResult,
90
+ NodeSnapshot,
91
+ ExecutableChunkPayload,
92
+ ResolvedDepContract,
93
+ } from './codegen.js';
94
+ export { FRAMEWORKS } from './codegen.js';
package/src/layout.ts ADDED
@@ -0,0 +1,6 @@
1
+ export interface Padding {
2
+ top: number;
3
+ right: number;
4
+ bottom: number;
5
+ left: number;
6
+ }
package/src/pen.ts CHANGED
@@ -1,28 +1,23 @@
1
- import type {
2
- PenFill,
3
- PenStroke,
4
- PenEffect,
5
- StyledTextSegment,
6
- } from './styles.js'
7
- import type { VariableDefinition } from './variables.js'
1
+ import type { PenFill, PenStroke, PenEffect, StyledTextSegment } from './styles.js';
2
+ import type { VariableDefinition } from './variables.js';
8
3
 
9
4
  // --- Page ---
10
5
 
11
6
  export interface PenPage {
12
- id: string
13
- name: string
14
- children: PenNode[]
7
+ id: string;
8
+ name: string;
9
+ children: PenNode[];
15
10
  }
16
11
 
17
12
  // --- Document Root ---
18
13
 
19
14
  export interface PenDocument {
20
- version: string
21
- name?: string
22
- themes?: Record<string, string[]>
23
- variables?: Record<string, VariableDefinition>
24
- pages?: PenPage[]
25
- children: PenNode[]
15
+ version: string;
16
+ name?: string;
17
+ themes?: Record<string, string[]>;
18
+ variables?: Record<string, VariableDefinition>;
19
+ pages?: PenPage[];
20
+ children: PenNode[];
26
21
  }
27
22
 
28
23
  // --- Node Types ---
@@ -38,171 +33,179 @@ export type PenNodeType =
38
33
  | 'text'
39
34
  | 'image'
40
35
  | 'icon_font'
41
- | 'ref'
36
+ | 'ref';
42
37
 
43
- export type SizingBehavior = number | 'fit_content' | 'fill_container' | string
38
+ export type SizingBehavior = number | 'fit_content' | 'fill_container' | string;
44
39
 
45
40
  // --- Base ---
46
41
 
47
42
  export interface PenNodeBase {
48
- id: string
49
- type: PenNodeType
50
- name?: string
51
- role?: string // semantic role for AI generation ("button", "card", "heading", etc.)
52
- x?: number
53
- y?: number
54
- rotation?: number
55
- opacity?: number | string // number or $variable
56
- enabled?: boolean | string
57
- visible?: boolean // default true
58
- locked?: boolean // default false
59
- flipX?: boolean
60
- flipY?: boolean
61
- theme?: Record<string, string>
43
+ id: string;
44
+ type: PenNodeType;
45
+ name?: string;
46
+ role?: string; // semantic role for AI generation ("button", "card", "heading", etc.)
47
+ x?: number;
48
+ y?: number;
49
+ rotation?: number;
50
+ opacity?: number | string; // number or $variable
51
+ enabled?: boolean | string;
52
+ visible?: boolean; // default true
53
+ locked?: boolean; // default false
54
+ flipX?: boolean;
55
+ flipY?: boolean;
56
+ theme?: Record<string, string>;
62
57
  }
63
58
 
64
59
  // --- Container (shared layout props) ---
65
60
 
66
61
  export interface ContainerProps {
67
- width?: SizingBehavior
68
- height?: SizingBehavior
69
- layout?: 'none' | 'vertical' | 'horizontal'
70
- gap?: number | string
71
- padding?:
72
- | number
73
- | [number, number]
74
- | [number, number, number, number]
75
- | string
76
- justifyContent?:
77
- | 'start'
78
- | 'center'
79
- | 'end'
80
- | 'space_between'
81
- | 'space_around'
82
- alignItems?: 'start' | 'center' | 'end'
83
- clipContent?: boolean
84
- children?: PenNode[]
85
- cornerRadius?: number | [number, number, number, number]
86
- fill?: PenFill[]
87
- stroke?: PenStroke
88
- effects?: PenEffect[]
62
+ width?: SizingBehavior;
63
+ height?: SizingBehavior;
64
+ layout?: 'none' | 'vertical' | 'horizontal';
65
+ gap?: number | string;
66
+ padding?: number | [number, number] | [number, number, number, number] | string;
67
+ justifyContent?: 'start' | 'center' | 'end' | 'space_between' | 'space_around';
68
+ alignItems?: 'start' | 'center' | 'end';
69
+ clipContent?: boolean;
70
+ children?: PenNode[];
71
+ cornerRadius?: number | [number, number, number, number];
72
+ fill?: PenFill[];
73
+ stroke?: PenStroke;
74
+ effects?: PenEffect[];
89
75
  }
90
76
 
91
77
  // --- Concrete Nodes ---
92
78
 
93
79
  export interface FrameNode extends PenNodeBase, ContainerProps {
94
- type: 'frame'
95
- reusable?: boolean
96
- slot?: string[]
80
+ type: 'frame';
81
+ reusable?: boolean;
82
+ slot?: string[];
97
83
  }
98
84
 
99
85
  export interface GroupNode extends PenNodeBase, ContainerProps {
100
- type: 'group'
86
+ type: 'group';
101
87
  }
102
88
 
103
89
  export interface RectangleNode extends PenNodeBase, ContainerProps {
104
- type: 'rectangle'
90
+ type: 'rectangle';
105
91
  }
106
92
 
107
93
  export interface EllipseNode extends PenNodeBase {
108
- type: 'ellipse'
109
- width?: SizingBehavior
110
- height?: SizingBehavior
111
- cornerRadius?: number
112
- innerRadius?: number
113
- startAngle?: number
114
- sweepAngle?: number
115
- fill?: PenFill[]
116
- stroke?: PenStroke
117
- effects?: PenEffect[]
94
+ type: 'ellipse';
95
+ width?: SizingBehavior;
96
+ height?: SizingBehavior;
97
+ cornerRadius?: number;
98
+ innerRadius?: number;
99
+ startAngle?: number;
100
+ sweepAngle?: number;
101
+ fill?: PenFill[];
102
+ stroke?: PenStroke;
103
+ effects?: PenEffect[];
118
104
  }
119
105
 
120
106
  export interface LineNode extends PenNodeBase {
121
- type: 'line'
122
- x2?: number
123
- y2?: number
124
- stroke?: PenStroke
125
- effects?: PenEffect[]
107
+ type: 'line';
108
+ x2?: number;
109
+ y2?: number;
110
+ stroke?: PenStroke;
111
+ effects?: PenEffect[];
126
112
  }
127
113
 
128
114
  export interface PolygonNode extends PenNodeBase {
129
- type: 'polygon'
130
- polygonCount: number
131
- width?: SizingBehavior
132
- height?: SizingBehavior
133
- cornerRadius?: number
134
- fill?: PenFill[]
135
- stroke?: PenStroke
136
- effects?: PenEffect[]
115
+ type: 'polygon';
116
+ polygonCount: number;
117
+ width?: SizingBehavior;
118
+ height?: SizingBehavior;
119
+ cornerRadius?: number;
120
+ fill?: PenFill[];
121
+ stroke?: PenStroke;
122
+ effects?: PenEffect[];
123
+ }
124
+
125
+ export interface PenPathHandle {
126
+ x: number;
127
+ y: number;
128
+ }
129
+
130
+ export type PenPathPointType = 'corner' | 'mirrored' | 'independent';
131
+
132
+ export interface PenPathAnchor {
133
+ x: number;
134
+ y: number;
135
+ handleIn: PenPathHandle | null;
136
+ handleOut: PenPathHandle | null;
137
+ pointType?: PenPathPointType;
137
138
  }
138
139
 
139
140
  export interface PathNode extends PenNodeBase {
140
- type: 'path'
141
- iconId?: string // Iconify icon ID, e.g. "lucide:home"
142
- d: string
143
- width?: SizingBehavior
144
- height?: SizingBehavior
145
- fill?: PenFill[]
146
- stroke?: PenStroke
147
- effects?: PenEffect[]
141
+ type: 'path';
142
+ iconId?: string; // Iconify icon ID, e.g. "lucide:home"
143
+ d: string;
144
+ anchors?: PenPathAnchor[];
145
+ closed?: boolean;
146
+ width?: SizingBehavior;
147
+ height?: SizingBehavior;
148
+ fill?: PenFill[];
149
+ stroke?: PenStroke;
150
+ effects?: PenEffect[];
148
151
  }
149
152
 
150
153
  export interface TextNode extends PenNodeBase {
151
- type: 'text'
152
- width?: SizingBehavior
153
- height?: SizingBehavior
154
- content: string | StyledTextSegment[]
155
- fontFamily?: string
156
- fontSize?: number
157
- fontWeight?: number | string
158
- fontStyle?: 'normal' | 'italic'
159
- letterSpacing?: number
160
- lineHeight?: number
161
- textAlign?: 'left' | 'center' | 'right' | 'justify'
162
- textAlignVertical?: 'top' | 'middle' | 'bottom'
163
- textGrowth?: 'auto' | 'fixed-width' | 'fixed-width-height'
164
- underline?: boolean
165
- strikethrough?: boolean
166
- fill?: PenFill[]
167
- effects?: PenEffect[]
168
- }
169
-
170
- export type ImageFitMode = 'fill' | 'fit' | 'crop' | 'tile'
154
+ type: 'text';
155
+ width?: SizingBehavior;
156
+ height?: SizingBehavior;
157
+ content: string | StyledTextSegment[];
158
+ fontFamily?: string;
159
+ fontSize?: number;
160
+ fontWeight?: number | string;
161
+ fontStyle?: 'normal' | 'italic';
162
+ letterSpacing?: number;
163
+ lineHeight?: number;
164
+ textAlign?: 'left' | 'center' | 'right' | 'justify';
165
+ textAlignVertical?: 'top' | 'middle' | 'bottom';
166
+ textGrowth?: 'auto' | 'fixed-width' | 'fixed-width-height';
167
+ underline?: boolean;
168
+ strikethrough?: boolean;
169
+ fill?: PenFill[];
170
+ effects?: PenEffect[];
171
+ }
172
+
173
+ export type ImageFitMode = 'fill' | 'fit' | 'crop' | 'tile';
171
174
 
172
175
  export interface ImageNode extends PenNodeBase {
173
- type: 'image'
174
- src: string
175
- objectFit?: ImageFitMode
176
- width?: SizingBehavior
177
- height?: SizingBehavior
178
- cornerRadius?: number | [number, number, number, number]
179
- effects?: PenEffect[]
180
- exposure?: number // -100 to 100
181
- contrast?: number // -100 to 100
182
- saturation?: number // -100 to 100
183
- temperature?: number // -100 to 100
184
- tint?: number // -100 to 100
185
- highlights?: number // -100 to 100
186
- shadows?: number // -100 to 100
187
- imagePrompt?: string // Descriptive prompt for AI image generation (long)
188
- imageSearchQuery?: string // Short keywords for image search (e.g. "burger fries")
176
+ type: 'image';
177
+ src: string;
178
+ objectFit?: ImageFitMode;
179
+ width?: SizingBehavior;
180
+ height?: SizingBehavior;
181
+ cornerRadius?: number | [number, number, number, number];
182
+ effects?: PenEffect[];
183
+ exposure?: number; // -100 to 100
184
+ contrast?: number; // -100 to 100
185
+ saturation?: number; // -100 to 100
186
+ temperature?: number; // -100 to 100
187
+ tint?: number; // -100 to 100
188
+ highlights?: number; // -100 to 100
189
+ shadows?: number; // -100 to 100
190
+ imagePrompt?: string; // Descriptive prompt for AI image generation (long)
191
+ imageSearchQuery?: string; // Short keywords for image search (e.g. "burger fries")
189
192
  }
190
193
 
191
194
  export interface IconFontNode extends PenNodeBase {
192
- type: 'icon_font'
193
- iconFontName: string
194
- iconFontFamily?: string
195
- width?: SizingBehavior
196
- height?: SizingBehavior
197
- fill?: PenFill[]
198
- stroke?: PenStroke
195
+ type: 'icon_font';
196
+ iconFontName: string;
197
+ iconFontFamily?: string;
198
+ width?: SizingBehavior;
199
+ height?: SizingBehavior;
200
+ fill?: PenFill[];
201
+ stroke?: PenStroke;
199
202
  }
200
203
 
201
204
  export interface RefNode extends PenNodeBase {
202
- type: 'ref'
203
- ref: string
204
- descendants?: Record<string, Partial<PenNode>>
205
- children?: PenNode[]
205
+ type: 'ref';
206
+ ref: string;
207
+ descendants?: Record<string, Partial<PenNode>>;
208
+ children?: PenNode[];
206
209
  }
207
210
 
208
211
  // --- Union ---
@@ -218,4 +221,4 @@ export type PenNode =
218
221
  | TextNode
219
222
  | ImageNode
220
223
  | IconFontNode
221
- | RefNode
224
+ | RefNode;
package/src/styles.ts CHANGED
@@ -9,100 +9,97 @@ export type BlendMode =
9
9
  | 'hue'
10
10
  | 'saturation'
11
11
  | 'color'
12
- | 'luminosity'
12
+ | 'luminosity';
13
13
 
14
14
  // --- Fill Types ---
15
15
 
16
16
  export interface SolidFill {
17
- type: 'solid'
18
- color: string // #RRGGBB or #RRGGBBAA
19
- opacity?: number
20
- blendMode?: BlendMode
17
+ type: 'solid';
18
+ color: string; // #RRGGBB or #RRGGBBAA
19
+ opacity?: number;
20
+ blendMode?: BlendMode;
21
21
  }
22
22
 
23
23
  export interface GradientStop {
24
- offset: number // 0 to 1
25
- color: string
24
+ offset: number; // 0 to 1
25
+ color: string;
26
26
  }
27
27
 
28
28
  export interface LinearGradientFill {
29
- type: 'linear_gradient'
30
- angle?: number
31
- stops: GradientStop[]
32
- opacity?: number
33
- blendMode?: BlendMode
29
+ type: 'linear_gradient';
30
+ angle?: number;
31
+ stops: GradientStop[];
32
+ opacity?: number;
33
+ blendMode?: BlendMode;
34
34
  }
35
35
 
36
36
  export interface RadialGradientFill {
37
- type: 'radial_gradient'
38
- cx?: number
39
- cy?: number
40
- radius?: number
41
- stops: GradientStop[]
42
- opacity?: number
43
- blendMode?: BlendMode
37
+ type: 'radial_gradient';
38
+ cx?: number;
39
+ cy?: number;
40
+ radius?: number;
41
+ stops: GradientStop[];
42
+ opacity?: number;
43
+ blendMode?: BlendMode;
44
44
  }
45
45
 
46
46
  export interface ImageFill {
47
- type: 'image'
48
- url: string
49
- mode?: 'fill' | 'fit' | 'crop' | 'tile' | 'stretch'
50
- opacity?: number
51
- exposure?: number // -100 to 100
52
- contrast?: number // -100 to 100
53
- saturation?: number // -100 to 100
54
- temperature?: number // -100 to 100
55
- tint?: number // -100 to 100
56
- highlights?: number // -100 to 100
57
- shadows?: number // -100 to 100
47
+ type: 'image';
48
+ url: string;
49
+ mode?: 'fill' | 'fit' | 'crop' | 'tile' | 'stretch';
50
+ opacity?: number;
51
+ exposure?: number; // -100 to 100
52
+ contrast?: number; // -100 to 100
53
+ saturation?: number; // -100 to 100
54
+ temperature?: number; // -100 to 100
55
+ tint?: number; // -100 to 100
56
+ highlights?: number; // -100 to 100
57
+ shadows?: number; // -100 to 100
58
58
  }
59
59
 
60
- export type PenFill =
61
- | SolidFill
62
- | LinearGradientFill
63
- | RadialGradientFill
64
- | ImageFill
60
+ export type PenFill = SolidFill | LinearGradientFill | RadialGradientFill | ImageFill;
65
61
 
66
62
  // --- Stroke ---
67
63
 
68
64
  export interface PenStroke {
69
- thickness: number | [number, number, number, number]
70
- align?: 'inside' | 'center' | 'outside'
71
- join?: 'miter' | 'bevel' | 'round'
72
- cap?: 'none' | 'round' | 'square'
73
- dashPattern?: number[]
74
- fill?: PenFill[]
65
+ thickness: number | [number, number, number, number];
66
+ align?: 'inside' | 'center' | 'outside';
67
+ join?: 'miter' | 'bevel' | 'round';
68
+ cap?: 'none' | 'round' | 'square';
69
+ dashPattern?: number[];
70
+ dashOffset?: number;
71
+ fill?: PenFill[];
75
72
  }
76
73
 
77
74
  // --- Effects ---
78
75
 
79
76
  export interface BlurEffect {
80
- type: 'blur' | 'background_blur'
81
- radius: number
77
+ type: 'blur' | 'background_blur';
78
+ radius: number;
82
79
  }
83
80
 
84
81
  export interface ShadowEffect {
85
- type: 'shadow'
86
- inner?: boolean
87
- offsetX: number
88
- offsetY: number
89
- blur: number
90
- spread: number
91
- color: string
82
+ type: 'shadow';
83
+ inner?: boolean;
84
+ offsetX: number;
85
+ offsetY: number;
86
+ blur: number;
87
+ spread: number;
88
+ color: string;
92
89
  }
93
90
 
94
- export type PenEffect = BlurEffect | ShadowEffect
91
+ export type PenEffect = BlurEffect | ShadowEffect;
95
92
 
96
93
  // --- Text ---
97
94
 
98
95
  export interface StyledTextSegment {
99
- text: string
100
- fontFamily?: string
101
- fontSize?: number
102
- fontWeight?: number
103
- fontStyle?: 'normal' | 'italic'
104
- fill?: string
105
- underline?: boolean
106
- strikethrough?: boolean
107
- href?: string
96
+ text: string;
97
+ fontFamily?: string;
98
+ fontSize?: number;
99
+ fontWeight?: number;
100
+ fontStyle?: 'normal' | 'italic';
101
+ fill?: string;
102
+ underline?: boolean;
103
+ strikethrough?: boolean;
104
+ href?: string;
108
105
  }
@@ -1,17 +1,17 @@
1
- import type { VariableDefinition } from './variables.js'
1
+ import type { VariableDefinition } from './variables.js';
2
2
 
3
3
  export interface ThemePreset {
4
- id: string
5
- name: string
6
- themes: Record<string, string[]>
7
- variables: Record<string, VariableDefinition>
8
- createdAt: number
4
+ id: string;
5
+ name: string;
6
+ themes: Record<string, string[]>;
7
+ variables: Record<string, VariableDefinition>;
8
+ createdAt: number;
9
9
  }
10
10
 
11
11
  export interface ThemePresetFile {
12
- type: 'openpencil-theme-preset'
13
- version: '1.0.0'
14
- name: string
15
- themes: Record<string, string[]>
16
- variables: Record<string, VariableDefinition>
12
+ type: 'openpencil-theme-preset';
13
+ version: '1.0.0';
14
+ name: string;
15
+ themes: Record<string, string[]>;
16
+ variables: Record<string, VariableDefinition>;
17
17
  }
package/src/uikit.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PenDocument } from './pen.js'
1
+ import type { PenDocument } from './pen.js';
2
2
 
3
3
  export type ComponentCategory =
4
4
  | 'buttons'
@@ -8,35 +8,35 @@ export type ComponentCategory =
8
8
  | 'layout'
9
9
  | 'feedback'
10
10
  | 'data-display'
11
- | 'other'
11
+ | 'other';
12
12
 
13
13
  export interface KitComponent {
14
14
  /** Node ID of the reusable FrameNode in the kit document */
15
- id: string
15
+ id: string;
16
16
  /** Display name */
17
- name: string
17
+ name: string;
18
18
  /** Category for organization in the browser */
19
- category: ComponentCategory
19
+ category: ComponentCategory;
20
20
  /** Tags for search */
21
- tags: string[]
21
+ tags: string[];
22
22
  /** Component dimensions for preview sizing */
23
- width: number
24
- height: number
23
+ width: number;
24
+ height: number;
25
25
  }
26
26
 
27
27
  export interface UIKit {
28
28
  /** Unique identifier */
29
- id: string
29
+ id: string;
30
30
  /** Display name */
31
- name: string
31
+ name: string;
32
32
  /** Optional description */
33
- description?: string
33
+ description?: string;
34
34
  /** Version string */
35
- version: string
35
+ version: string;
36
36
  /** Whether this is a built-in kit that ships with the app */
37
- builtIn: boolean
37
+ builtIn: boolean;
38
38
  /** Backing PenDocument containing the reusable nodes */
39
- document: PenDocument
39
+ document: PenDocument;
40
40
  /** Extracted component metadata for browsing */
41
- components: KitComponent[]
41
+ components: KitComponent[];
42
42
  }
package/src/variables.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  export interface VariableDefinition {
2
- type: 'color' | 'number' | 'boolean' | 'string'
3
- value: VariableValue
2
+ type: 'color' | 'number' | 'boolean' | 'string';
3
+ value: VariableValue;
4
4
  }
5
5
 
6
- export type VariableValue = string | number | boolean | ThemedValue[]
6
+ export type VariableValue = string | number | boolean | ThemedValue[];
7
7
 
8
8
  export interface ThemedValue {
9
- value: string | number | boolean
10
- theme?: Record<string, string>
9
+ value: string | number | boolean;
10
+ theme?: Record<string, string>;
11
11
  }