skema-core 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.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Skema
2
+
3
+ A drawing-based website development tool that transforms how you annotate and communicate design changes.
4
+
5
+ ## Overview
6
+
7
+ Skema is a React component that provides a tldraw-powered drawing overlay for annotating and manipulating DOM elements visually. It sits on top of your localhost website, allowing developers to annotate, draw, and select DOM elements directly on the live page.
8
+
9
+ ## Features
10
+
11
+ - **Drawing Overlay**: Use tldraw's powerful drawing tools directly on your website
12
+ - **DOM Picker**: Select any element on the page to capture its selector, bounding box, and context
13
+ - **Annotation Export**: Export all annotations in a structured JSON format optimized for AI agents
14
+ - **Non-Invasive**: Transparent overlay that doesn't interfere with your page when not in use
15
+
16
+ ## Installation
17
+
18
+ ### 1. Install the package
19
+
20
+ ```bash
21
+ bun add skema-core
22
+ # or
23
+ npm install skema-core
24
+ ```
25
+
26
+ ### 2. Create the API route
27
+
28
+ Run the init command to create the Gemini API route in your Next.js App Router project:
29
+
30
+ ```bash
31
+ bunx skema init
32
+ # or
33
+ npx skema init
34
+ ```
35
+
36
+ This creates `app/api/gemini/route.ts` (or `src/app/api/gemini/route.ts`) which handles annotation processing.
37
+
38
+ ### 3. Set up your Gemini API key
39
+
40
+ Add your [Google AI API key](https://aistudio.google.com/apikey) to your `.env` file:
41
+
42
+ ```env
43
+ GEMINI_API_KEY=your_api_key_here
44
+ ```
45
+
46
+ ### 4. Add Skema to your app
47
+
48
+ Wrap your app with the Skema component (development only):
49
+
50
+ ```tsx
51
+ import { Skema } from 'skema-core';
52
+
53
+ export default function Page() {
54
+ return (
55
+ <>
56
+ {/* Your page content */}
57
+ <main>...</main>
58
+
59
+ {/* Skema overlay - only in development */}
60
+ {process.env.NODE_ENV === 'development' && <Skema />}
61
+ </>
62
+ );
63
+ }
64
+ ```
65
+
66
+ That's it! Press **⌘⇧E** (Cmd+Shift+E) to toggle the Skema overlay.
67
+
68
+ ## Keyboard Shortcuts
69
+
70
+ - **⌘⇧E** (Cmd+Shift+E / Ctrl+Shift+E): Toggle Skema overlay
71
+ - **P**: Activate DOM Picker tool
72
+
73
+ ## Export Format
74
+
75
+ When you export annotations, Skema generates a JSON structure like this:
76
+
77
+ ```json
78
+ {
79
+ "version": "1.0.0",
80
+ "timestamp": "2024-01-24T12:00:00Z",
81
+ "viewport": {
82
+ "width": 1920,
83
+ "height": 1080,
84
+ "scrollX": 0,
85
+ "scrollY": 150
86
+ },
87
+ "pathname": "/",
88
+ "annotations": [
89
+ {
90
+ "type": "dom_selection",
91
+ "id": "dom-1706097600000-abc123",
92
+ "selector": ".hero-section > button.cta-primary",
93
+ "tagName": "button",
94
+ "elementPath": ".hero-section > button",
95
+ "text": "Get Started",
96
+ "boundingBox": { "x": 100, "y": 200, "width": 150, "height": 40 },
97
+ "timestamp": 1706097600000,
98
+ "pathname": "/"
99
+ }
100
+ ]
101
+ }
102
+ ```
103
+
104
+ ## Props
105
+
106
+ | Prop | Type | Default | Description |
107
+ |------|------|---------|-------------|
108
+ | `enabled` | `boolean` | `true` | Whether Skema overlay is enabled |
109
+ | `onAnnotationsChange` | `(annotations: Annotation[]) => void` | - | Callback when annotations change |
110
+ | `toggleShortcut` | `string` | `'mod+shift+e'` | Keyboard shortcut to toggle Skema |
111
+ | `initialAnnotations` | `Annotation[]` | `[]` | Initial annotations to load |
112
+ | `zIndex` | `number` | `99999` | Z-index for the overlay |
113
+
114
+ ## License
115
+
116
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+
7
+ var ROUTE_CONTENT = `export { POST, DELETE } from 'skema-core/server';
8
+ `;
9
+ function init() {
10
+ const cwd = process.cwd();
11
+ const appDir = fs.existsSync(path.join(cwd, "app")) ? path.join(cwd, "app") : fs.existsSync(path.join(cwd, "src/app")) ? path.join(cwd, "src/app") : null;
12
+ if (!appDir) {
13
+ console.error("\u274C Could not find app/ or src/app/ directory.");
14
+ console.error(" Make sure you run this from a Next.js App Router project root.");
15
+ process.exit(1);
16
+ }
17
+ const apiDir = path.join(appDir, "api", "gemini");
18
+ const routePath = path.join(apiDir, "route.ts");
19
+ if (fs.existsSync(routePath)) {
20
+ console.log("\u2713 API route already exists at", routePath.replace(cwd, "."));
21
+ return;
22
+ }
23
+ if (!fs.existsSync(apiDir)) {
24
+ fs.mkdirSync(apiDir, { recursive: true });
25
+ console.log("\u2713 Created", apiDir.replace(cwd, "."));
26
+ }
27
+ fs.writeFileSync(routePath, ROUTE_CONTENT);
28
+ console.log("\u2713 Created", routePath.replace(cwd, "."));
29
+ console.log("");
30
+ console.log("\u{1F389} Skema is ready! The Gemini CLI integration is now set up.");
31
+ console.log("");
32
+ console.log("Usage:");
33
+ console.log(" 1. Add <Skema /> to your page");
34
+ console.log(" 2. Press Cmd+Shift+E to toggle the overlay");
35
+ console.log(" 3. Annotate elements and Gemini CLI will make the changes");
36
+ }
37
+ init();
@@ -0,0 +1,356 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Represents a single DOM element in a selection
5
+ */
6
+ interface DOMElement {
7
+ selector: string;
8
+ tagName: string;
9
+ elementPath: string;
10
+ text: string;
11
+ boundingBox: BoundingBox;
12
+ cssClasses?: string;
13
+ attributes?: Record<string, string>;
14
+ }
15
+ /**
16
+ * Represents a DOM element selection (can contain one or more elements)
17
+ */
18
+ interface DOMSelection {
19
+ id: string;
20
+ selector: string;
21
+ tagName: string;
22
+ elementPath: string;
23
+ text: string;
24
+ boundingBox: BoundingBox;
25
+ timestamp: number;
26
+ pathname: string;
27
+ cssClasses?: string;
28
+ attributes?: Record<string, string>;
29
+ /** User annotation comment */
30
+ comment?: string;
31
+ /** Whether this is a multi-element selection */
32
+ isMultiSelect?: boolean;
33
+ /** Individual elements when this is a grouped selection */
34
+ elements?: DOMElement[];
35
+ }
36
+ /**
37
+ * Pending annotation state for the popup
38
+ */
39
+ interface PendingAnnotation {
40
+ /** Screen X position (percentage of viewport width) */
41
+ x: number;
42
+ /** Screen Y position (pixels from top of document) */
43
+ y: number;
44
+ /** Y position relative to viewport (for popup positioning) */
45
+ clientY: number;
46
+ /** Element identifier string */
47
+ element: string;
48
+ /** CSS selector path */
49
+ elementPath: string;
50
+ /** Selected text content if any */
51
+ selectedText?: string;
52
+ /** Bounding box of selected element(s) */
53
+ boundingBox?: BoundingBox;
54
+ /** Whether this is a multi-element selection */
55
+ isMultiSelect?: boolean;
56
+ /** The DOM selection(s) being annotated */
57
+ selections?: DOMSelection[];
58
+ /** Type of annotation */
59
+ annotationType: 'dom_selection' | 'drawing';
60
+ /** Drawing shape IDs if annotating drawings */
61
+ shapeIds?: string[];
62
+ }
63
+ /**
64
+ * Bounding box for element positioning
65
+ */
66
+ interface BoundingBox {
67
+ x: number;
68
+ y: number;
69
+ width: number;
70
+ height: number;
71
+ }
72
+ /**
73
+ * Viewport information for coordinate calculations
74
+ */
75
+ interface ViewportInfo {
76
+ width: number;
77
+ height: number;
78
+ scrollX: number;
79
+ scrollY: number;
80
+ }
81
+ /**
82
+ * Computed styles for an element
83
+ */
84
+ interface ElementStyles {
85
+ fontFamily?: string;
86
+ fontSize?: string;
87
+ fontWeight?: string;
88
+ lineHeight?: string;
89
+ letterSpacing?: string;
90
+ textAlign?: string;
91
+ color?: string;
92
+ padding?: string;
93
+ margin?: string;
94
+ gap?: string;
95
+ display?: string;
96
+ flexDirection?: string;
97
+ alignItems?: string;
98
+ justifyContent?: string;
99
+ backgroundColor?: string;
100
+ borderRadius?: string;
101
+ border?: string;
102
+ boxShadow?: string;
103
+ width?: string;
104
+ height?: string;
105
+ maxWidth?: string;
106
+ }
107
+ /**
108
+ * Nearby element with style context for AI
109
+ */
110
+ interface NearbyElement {
111
+ selector: string;
112
+ tagName: string;
113
+ text?: string;
114
+ className?: string;
115
+ styles?: ElementStyles;
116
+ tailwindClasses?: string[];
117
+ }
118
+ /**
119
+ * Project-level style context
120
+ */
121
+ interface ProjectStyleContext {
122
+ cssFramework?: 'tailwind' | 'bootstrap' | 'material-ui' | 'chakra' | 'vanilla' | 'css-modules' | 'styled-components' | 'unknown';
123
+ cssVariables?: Record<string, string>;
124
+ colorPalette?: string[];
125
+ baseFontFamily?: string;
126
+ baseFontSize?: string;
127
+ }
128
+ /**
129
+ * Drawing annotation from tldraw
130
+ */
131
+ interface DrawingAnnotation {
132
+ id: string;
133
+ type: 'drawing';
134
+ tool: string;
135
+ shapes: unknown[];
136
+ boundingBox: BoundingBox;
137
+ relatedTo?: string;
138
+ timestamp: number;
139
+ /** User annotation comment describing what to build */
140
+ comment?: string;
141
+ /** SVG representation of the drawing for AI processing */
142
+ drawingSvg?: string;
143
+ /** Base64 PNG image of the drawing for vision AI */
144
+ drawingImage?: string;
145
+ /** Extracted text content from text shapes in the drawing */
146
+ extractedText?: string;
147
+ /** Grid configuration used for positioning reference */
148
+ gridConfig?: {
149
+ color: string;
150
+ size: number;
151
+ labels: boolean;
152
+ };
153
+ /** Viewport info for relative sizing context */
154
+ viewport?: ViewportInfo;
155
+ /** Project-level style context */
156
+ projectStyles?: ProjectStyleContext;
157
+ /** Nearby DOM elements that the drawing may relate to */
158
+ nearbyElements?: NearbyElement[];
159
+ }
160
+ /**
161
+ * Gesture action annotation
162
+ */
163
+ interface GestureAnnotation {
164
+ id: string;
165
+ type: 'gesture';
166
+ gesture: 'scribble_delete' | 'circle' | 'rectangle' | 'arrow';
167
+ target?: string;
168
+ boundingBox: BoundingBox;
169
+ timestamp: number;
170
+ }
171
+ /**
172
+ * Union type for all annotation types
173
+ */
174
+ type Annotation = {
175
+ type: 'dom_selection';
176
+ } & DOMSelection | DrawingAnnotation | GestureAnnotation;
177
+ /**
178
+ * Complete export format for annotations
179
+ */
180
+ interface AnnotationExport {
181
+ version: string;
182
+ timestamp: string;
183
+ viewport: ViewportInfo;
184
+ pathname: string;
185
+ annotations: Annotation[];
186
+ }
187
+ /**
188
+ * Skema component props
189
+ */
190
+ interface SkemaProps {
191
+ /** Whether Skema overlay is enabled */
192
+ enabled?: boolean;
193
+ /** Callback when annotations change */
194
+ onAnnotationsChange?: (annotations: Annotation[]) => void;
195
+ /** Callback when a single annotation is submitted - for real-time integrations like Gemini */
196
+ onAnnotationSubmit?: (annotation: Annotation, comment: string) => void;
197
+ /** Callback when an annotation is deleted - for reverting changes */
198
+ onAnnotationDelete?: (annotationId: string) => void;
199
+ /** Keyboard shortcut to toggle Skema (default: Cmd/Ctrl + Shift + E) */
200
+ toggleShortcut?: string;
201
+ /** Initial annotations to load */
202
+ initialAnnotations?: Annotation[];
203
+ /** Z-index for the overlay (default: 99999) */
204
+ zIndex?: number;
205
+ }
206
+ /**
207
+ * Skema mode - determines what tools are available
208
+ */
209
+ type SkemaMode = 'select' | 'draw';
210
+
211
+ /**
212
+ * Main Skema component - renders tldraw as a transparent overlay
213
+ */
214
+ declare const Skema: React.FC<SkemaProps>;
215
+
216
+ interface AnnotationPopupProps {
217
+ /** Element name/description to display in header */
218
+ element: string;
219
+ /** Optional selected/highlighted text */
220
+ selectedText?: string;
221
+ /** Placeholder text for the textarea */
222
+ placeholder?: string;
223
+ /** Initial value for textarea (for edit mode) */
224
+ initialValue?: string;
225
+ /** Label for submit button (default: "Add") */
226
+ submitLabel?: string;
227
+ /** Called when annotation is submitted with text */
228
+ onSubmit: (text: string) => void;
229
+ /** Called when popup is cancelled/dismissed */
230
+ onCancel: () => void;
231
+ /** Position styles (left, top) */
232
+ style?: React.CSSProperties;
233
+ /** Custom accent color (hex) */
234
+ accentColor?: string;
235
+ /** External exit state (parent controls exit animation) */
236
+ isExiting?: boolean;
237
+ /** Whether this is a multi-select annotation */
238
+ isMultiSelect?: boolean;
239
+ }
240
+ interface AnnotationPopupHandle {
241
+ /** Shake the popup (e.g., when user clicks outside) */
242
+ shake: () => void;
243
+ }
244
+ declare const AnnotationPopup: React.ForwardRefExoticComponent<AnnotationPopupProps & React.RefAttributes<AnnotationPopupHandle>>;
245
+
246
+ /**
247
+ * Generates a unique CSS selector for an element
248
+ */
249
+ declare function generateSelector(element: HTMLElement): string;
250
+ /**
251
+ * Gets a readable path for an element (e.g., "article > section > p")
252
+ */
253
+ declare function getElementPath(target: HTMLElement, maxDepth?: number): string;
254
+ /**
255
+ * Identifies an element and returns a human-readable name
256
+ */
257
+ declare function identifyElement(target: HTMLElement): string;
258
+ /**
259
+ * Gets bounding box for an element in document coordinates (includes scroll offset)
260
+ */
261
+ declare function getBoundingBox(element: HTMLElement): BoundingBox;
262
+ /**
263
+ * Gets CSS class names from an element (cleaned of module hashes)
264
+ */
265
+ declare function getElementClasses(target: HTMLElement): string;
266
+ /**
267
+ * Creates a DOMSelection from an element
268
+ */
269
+ declare function createDOMSelection(element: HTMLElement): DOMSelection;
270
+ /**
271
+ * Checks if an element should be ignored for DOM picking
272
+ */
273
+ declare function shouldIgnoreElement(element: HTMLElement): boolean;
274
+
275
+ /**
276
+ * Gets current viewport information
277
+ */
278
+ declare function getViewportInfo(): ViewportInfo;
279
+ /**
280
+ * Converts viewport-relative coordinates to document coordinates
281
+ */
282
+ declare function viewportToDocument(x: number, y: number, viewport?: ViewportInfo): {
283
+ x: number;
284
+ y: number;
285
+ };
286
+ /**
287
+ * Converts document coordinates to viewport-relative coordinates
288
+ */
289
+ declare function documentToViewport(x: number, y: number, viewport?: ViewportInfo): {
290
+ x: number;
291
+ y: number;
292
+ };
293
+ /**
294
+ * Converts a bounding box from viewport to document coordinates
295
+ */
296
+ declare function bboxViewportToDocument(bbox: BoundingBox, viewport?: ViewportInfo): BoundingBox;
297
+ /**
298
+ * Converts a bounding box from document to viewport coordinates
299
+ */
300
+ declare function bboxDocumentToViewport(bbox: BoundingBox, viewport?: ViewportInfo): BoundingBox;
301
+ /**
302
+ * Checks if two bounding boxes intersect
303
+ */
304
+ declare function bboxIntersects(a: BoundingBox, b: BoundingBox): boolean;
305
+ /**
306
+ * Checks if point is inside bounding box
307
+ */
308
+ declare function pointInBbox(x: number, y: number, bbox: BoundingBox): boolean;
309
+ /**
310
+ * Gets the center point of a bounding box
311
+ */
312
+ declare function bboxCenter(bbox: BoundingBox): {
313
+ x: number;
314
+ y: number;
315
+ };
316
+ /**
317
+ * Expands a bounding box by a padding amount
318
+ */
319
+ declare function expandBbox(bbox: BoundingBox, padding: number): BoundingBox;
320
+ /**
321
+ * Creates a bounding box from two points
322
+ */
323
+ declare function bboxFromPoints(x1: number, y1: number, x2: number, y2: number): BoundingBox;
324
+
325
+ /**
326
+ * Convert a Blob to a base64 string
327
+ */
328
+ declare function blobToBase64(blob: Blob): Promise<string>;
329
+ /**
330
+ * Add a labeled grid overlay to an SVG string
331
+ * Grid uses A/B/C column labels and 0/1/2 row numbers for positioning reference
332
+ * @param svgString - The SVG markup to add grid to
333
+ * @param opts - Grid options (color, cell size, whether to show labels)
334
+ * @returns SVG string with grid overlay added
335
+ */
336
+ declare function addGridToSvg(svgString: string, opts?: {
337
+ color?: string;
338
+ size?: number;
339
+ labels?: boolean;
340
+ }): string;
341
+ /**
342
+ * Get the grid cell reference (e.g., "B2") for a given position
343
+ * @param x - X coordinate in pixels
344
+ * @param y - Y coordinate in pixels
345
+ * @param gridSize - Size of each grid cell (default 100px)
346
+ * @returns Grid cell reference string (e.g., "B2")
347
+ */
348
+ declare function getGridCellReference(x: number, y: number, gridSize?: number): string;
349
+ /**
350
+ * Extract text content from tldraw shapes
351
+ * @param shapes - Array of tldraw shapes
352
+ * @returns Combined text content from text and note shapes
353
+ */
354
+ declare function extractTextFromShapes(shapes: unknown[]): string;
355
+
356
+ export { type Annotation, type AnnotationExport, AnnotationPopup, type AnnotationPopupHandle, type AnnotationPopupProps, type BoundingBox, type DOMElement, type DOMSelection, type DrawingAnnotation, type ElementStyles, type GestureAnnotation, type NearbyElement, type PendingAnnotation, type ProjectStyleContext, Skema, type SkemaMode, type SkemaProps, type ViewportInfo, addGridToSvg, bboxCenter, bboxDocumentToViewport, bboxFromPoints, bboxIntersects, bboxViewportToDocument, blobToBase64, createDOMSelection, Skema as default, documentToViewport, expandBbox, extractTextFromShapes, generateSelector, getBoundingBox, getElementClasses, getElementPath, getGridCellReference, getViewportInfo, identifyElement, pointInBbox, shouldIgnoreElement, viewportToDocument };