f1ow 0.1.4 → 1.0.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
@@ -30,6 +30,8 @@
30
30
  - **Undo / Redo** — 100-step history snapshot system.
31
31
  - **Export** — Export canvas to PNG, SVG, or JSON.
32
32
  - **Real-Time Collaboration** — Optional CRDT via Yjs (experimental) with cursor presence.
33
+ - **Plugin / Extension System** — Register custom element types with per-type validation and default values.
34
+ - **Element Validation** — Every mutation path (add, update, import) is validated; invalid elements are rejected gracefully.
33
35
  - **Fully Themeable** — Dark mode, custom colors, all via props.
34
36
  - **Zero CSS Dependencies** — No external stylesheets required. Inline styled.
35
37
  - **TypeScript** — Full type safety with strict mode.
@@ -121,6 +123,7 @@ That's it — you get a full-featured canvas editor with a toolbar, style panel,
121
123
  | `className` | `string` | — | Root container CSS class |
122
124
  | `contextMenuItems` | `ContextMenuItem[]` or `(ctx) => ContextMenuItem[]` | — | Extra context menu items |
123
125
  | `renderContextMenu` | `(ctx) => ReactNode` | — | Replace built-in context menu |
126
+ | `customElementTypes` | `CustomElementConfig[]` | — | Register custom element types ([docs](#-custom-element-types--plugins)) |
124
127
  | `collaboration` | `CollaborationConfig` | — | Enable real-time collaboration |
125
128
  | `workerConfig` | `{ elbowWorkerUrl?: string, exportWorkerUrl?: string, disabled?: boolean }` | — | Worker URLs for Next.js ([docs](docs/NEXTJS_INTEGRATION.md)) |
126
129
 
@@ -237,7 +240,7 @@ Provides CRDT-based real-time sync with cursor presence overlay. Requires a [Yjs
237
240
 
238
241
  ## 🧩 Element Types
239
242
 
240
- `CanvasElement` is a discriminated union of 8 types:
243
+ `CanvasElement` is a discriminated union of 8 built-in types:
241
244
 
242
245
  - **Shapes** — `rectangle`, `ellipse`, `diamond`
243
246
  - **Connectors** — `line`, `arrow` (with bindings, routing, arrowheads)
@@ -245,8 +248,98 @@ Provides CRDT-based real-time sync with cursor presence overlay. Requires a [Yjs
245
248
 
246
249
  All elements share: `id`, `x`, `y`, `width`, `height`, `rotation`, `style`, `isLocked`, `isVisible`, `boundElements`, `groupIds`.
247
250
 
251
+ Custom types can be added via the plugin system — see [Custom Element Types](#-custom-element-types--plugins).
252
+
248
253
  > Full type definitions are bundled in the package `.d.ts` files.
249
254
 
255
+ ## 🔌 Custom Element Types / Plugins
256
+
257
+ f1ow supports registering custom element types. Every element passing through `addElement`, `updateElement`, `setElements`, or `importJSON` is validated — both built-in and custom types.
258
+
259
+ ### Option 1 — Global registration (before rendering)
260
+
261
+ Register once at module level so the type is available across all `<FlowCanvas>` instances:
262
+
263
+ ```ts
264
+ import { registerCustomElement } from 'f1ow';
265
+
266
+ registerCustomElement({
267
+ type: 'sticky-note',
268
+ displayName: 'Sticky Note',
269
+
270
+ // Called after base-field validation passes.
271
+ // Return true = valid, or a string = error message.
272
+ validate: (el) => typeof el.content === 'string' || 'content must be a string',
273
+
274
+ // Default field values — only fills gaps, never overwrites.
275
+ defaults: { content: '', color: '#ffeb3b' },
276
+ });
277
+ ```
278
+
279
+ ### Option 2 — Per-component registration (via prop)
280
+
281
+ Types are registered once when `<FlowCanvas>` mounts. Keep the array reference stable (module constant or `useMemo`) — changes after mount have no effect.
282
+
283
+ ```tsx
284
+ import { FlowCanvas } from 'f1ow';
285
+ import type { CustomElementConfig } from 'f1ow';
286
+
287
+ // ✅ Define outside the component (or useMemo) — stable reference
288
+ const MY_TYPES: CustomElementConfig[] = [
289
+ {
290
+ type: 'sticky-note',
291
+ displayName: 'Sticky Note',
292
+ validate: (el) => typeof el.content === 'string' || 'content must be a string',
293
+ defaults: { content: '', color: '#ffeb3b' },
294
+ },
295
+ ];
296
+
297
+ function App() {
298
+ return <FlowCanvas customElementTypes={MY_TYPES} />;
299
+ }
300
+ ```
301
+
302
+ ### `CustomElementConfig` reference
303
+
304
+ | Field | Type | Description |
305
+ | --- | --- | --- |
306
+ | `type` | `string` | **Required.** Unique type identifier (must not clash with built-ins unless `allowOverride: true`) |
307
+ | `displayName` | `string` | Human-readable name used in warnings. Defaults to `type` |
308
+ | `validate` | `(el: Record<string, unknown>) => true \| string` | Extra validation after base-field checks. Return `true` = valid, string = error message |
309
+ | `defaults` | `Partial<T>` | Default field values applied on `addElement`. Existing fields take priority |
310
+ | `allowOverride` | `boolean` | Allow replacing an existing registration. Default `false` |
311
+
312
+ ### Using the registry directly
313
+
314
+ ```ts
315
+ import { elementRegistry } from 'f1ow';
316
+
317
+ // Check if a type is registered
318
+ elementRegistry.isRegistered('sticky-note'); // true / false
319
+
320
+ // Validate any element manually
321
+ const result = elementRegistry.validateElement(myElement);
322
+ if (!result.valid) console.error(result.error);
323
+
324
+ // All registered types
325
+ elementRegistry.getRegisteredTypes();
326
+ // → ['rectangle', 'ellipse', ..., 'sticky-note']
327
+ ```
328
+
329
+ ### Built-in validation rules
330
+
331
+ Every element is validated on every write regardless of type:
332
+
333
+ | Field | Rule |
334
+ | --- | --- |
335
+ | `id` | Non-empty string |
336
+ | `type` | Must be a registered type |
337
+ | `x`, `y`, `rotation` | Finite number |
338
+ | `width`, `height` | Finite number ≥ 0 |
339
+ | `style.opacity` | Number in `[0, 1]` |
340
+ | `style.strokeWidth`, `style.fontSize` | Finite number > 0 |
341
+ | `id` / `type` in updates | Blocked — use `convertElementType` for type changes |
342
+
250
343
  ## 🛠️ Development
251
344
 
252
345
  ```bash
@@ -0,0 +1,34 @@
1
+ import { default as React } from 'react';
2
+ import { CanvasElement, ViewportState } from '../../types';
3
+ /** Screen-space bounding box passed to the annotation renderer */
4
+ export interface AnnotationScreenBounds {
5
+ /** Left edge in pixels (relative to canvas container) */
6
+ x: number;
7
+ /** Top edge in pixels (relative to canvas container) */
8
+ y: number;
9
+ /** Width in screen pixels */
10
+ width: number;
11
+ /** Height in screen pixels */
12
+ height: number;
13
+ }
14
+ /** Context provided to the `renderAnnotation` callback */
15
+ export interface AnnotationContext {
16
+ /** The canvas element being annotated */
17
+ element: CanvasElement;
18
+ /** Screen-space bounding box of the element (after zoom/pan) */
19
+ screenBounds: AnnotationScreenBounds;
20
+ /** Current viewport zoom level */
21
+ scale: number;
22
+ }
23
+ /** Signature for the `renderAnnotation` prop */
24
+ export type RenderAnnotationFn = (ctx: AnnotationContext) => React.ReactNode;
25
+ interface AnnotationsOverlayProps {
26
+ elements: CanvasElement[];
27
+ viewport: ViewportState;
28
+ /** Container dimensions for viewport-culling */
29
+ containerWidth: number;
30
+ containerHeight: number;
31
+ renderAnnotation: RenderAnnotationFn;
32
+ }
33
+ export declare const AnnotationsOverlay: React.FC<AnnotationsOverlayProps>;
34
+ export {};
@@ -0,0 +1,16 @@
1
+ import { default as React } from 'react';
2
+ import { TextElement, ArrowElement, LineElement } from '../../types';
3
+ export interface TextLabelProps {
4
+ element: TextElement;
5
+ /** The parent connector element (arrow or line) */
6
+ connector: ArrowElement | LineElement;
7
+ onChange: (id: string, updates: Partial<TextElement>) => void;
8
+ /** If true, auto-opens the textarea editor immediately after mount */
9
+ autoEdit?: boolean;
10
+ /** Called to notify parent that text editing started */
11
+ onEditStart?: (id: string) => void;
12
+ /** Called to notify parent that text editing ended */
13
+ onEditEnd?: (id: string, isEmpty: boolean) => void;
14
+ }
15
+ declare const _default: React.NamedExoticComponent<TextLabelProps>;
16
+ export default _default;