@kabel-project/kabel 1.0.7

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.
Files changed (152) hide show
  1. package/(1.0.7)kabel.md +18 -0
  2. package/README.md +96 -0
  3. package/_READ_ME_MEDIA_/documentation/docs.md +293 -0
  4. package/_READ_ME_MEDIA_/workspace.png +0 -0
  5. package/comment-renderer/renderer.ts +228 -0
  6. package/controllers/base.ts +186 -0
  7. package/controllers/wasd.ts +132 -0
  8. package/docs/README.md +98 -0
  9. package/docs/_media/docs.md +289 -0
  10. package/docs/_media/index.html +168 -0
  11. package/docs/_media/workspace.png +0 -0
  12. package/docs/classes/CommentModel.md +271 -0
  13. package/docs/classes/CommentRenderer.md +457 -0
  14. package/docs/classes/ConnectableField.md +597 -0
  15. package/docs/classes/Connection.md +191 -0
  16. package/docs/classes/ContextMenuHTML.md +163 -0
  17. package/docs/classes/Coordinates.md +187 -0
  18. package/docs/classes/DropdownContainer.md +300 -0
  19. package/docs/classes/DummyField.md +393 -0
  20. package/docs/classes/Eventer.md +185 -0
  21. package/docs/classes/Field.md +461 -0
  22. package/docs/classes/InjectMsg.md +85 -0
  23. package/docs/classes/NodeSvg.md +1011 -0
  24. package/docs/classes/NumberField.md +559 -0
  25. package/docs/classes/OptConnectField.md +624 -0
  26. package/docs/classes/Renderer.md +1636 -0
  27. package/docs/classes/RendererConstants.md +343 -0
  28. package/docs/classes/Representer.md +95 -0
  29. package/docs/classes/RepresenterNode.md +175 -0
  30. package/docs/classes/TextField.md +559 -0
  31. package/docs/classes/Toolbox.md +172 -0
  32. package/docs/classes/WASDController.md +616 -0
  33. package/docs/classes/Widget.md +195 -0
  34. package/docs/classes/WorkspaceController.md +385 -0
  35. package/docs/classes/WorkspaceCoords.md +218 -0
  36. package/docs/classes/WorkspaceSvg.md +1380 -0
  37. package/docs/functions/clearMainWorkspace.md +20 -0
  38. package/docs/functions/getMainWorkspace.md +19 -0
  39. package/docs/functions/inject.md +35 -0
  40. package/docs/functions/setMainWorkspace.md +28 -0
  41. package/docs/globals.md +95 -0
  42. package/docs/interfaces/ColorStyle.md +43 -0
  43. package/docs/interfaces/ConnectorToFrom.md +57 -0
  44. package/docs/interfaces/DrawState.md +81 -0
  45. package/docs/interfaces/FieldConnectionData.md +25 -0
  46. package/docs/interfaces/FieldOptions.md +63 -0
  47. package/docs/interfaces/FieldRawBoxData.md +25 -0
  48. package/docs/interfaces/FieldVisualInfo.md +65 -0
  49. package/docs/interfaces/GridOptions.md +61 -0
  50. package/docs/interfaces/InjectOptions.md +133 -0
  51. package/docs/interfaces/InputFieldJson.md +50 -0
  52. package/docs/interfaces/KabelCommentRendering.md +31 -0
  53. package/docs/interfaces/KabelInterface.md +469 -0
  54. package/docs/interfaces/KabelNodeRendering.md +77 -0
  55. package/docs/interfaces/KabelUIX.md +105 -0
  56. package/docs/interfaces/KabelUtils.md +215 -0
  57. package/docs/interfaces/NodeEvents.md +42 -0
  58. package/docs/interfaces/NodeJson.md +104 -0
  59. package/docs/interfaces/NodePrototype.md +82 -0
  60. package/docs/interfaces/RegisteredEl.md +53 -0
  61. package/docs/interfaces/SerializedNode.md +128 -0
  62. package/docs/interfaces/TblxCategoryStruct.md +41 -0
  63. package/docs/interfaces/TblxFieldStruct.md +28 -0
  64. package/docs/interfaces/TblxNodeStruct.md +35 -0
  65. package/docs/interfaces/WidgetOptions.md +115 -0
  66. package/docs/interfaces/WidgetPrototypeList.md +15 -0
  67. package/docs/type-aliases/AnyField.md +13 -0
  68. package/docs/type-aliases/AnyFieldCls.md +13 -0
  69. package/docs/type-aliases/Color.md +13 -0
  70. package/docs/type-aliases/Connectable.md +13 -0
  71. package/docs/type-aliases/EventArgs.md +11 -0
  72. package/docs/type-aliases/EventSetupFn.md +25 -0
  73. package/docs/type-aliases/Hex.md +13 -0
  74. package/docs/type-aliases/RGBObject.md +37 -0
  75. package/docs/type-aliases/RGBString.md +13 -0
  76. package/docs/type-aliases/RGBTuple.md +13 -0
  77. package/docs/type-aliases/TblxObjStruct.md +52 -0
  78. package/docs/variables/CategoryColors.md +29 -0
  79. package/docs/variables/FieldMap.md +41 -0
  80. package/docs/variables/NodePrototypes.md +18 -0
  81. package/docs/variables/default.md +11 -0
  82. package/events/comment-drag-handle.ts +61 -0
  83. package/events/comment-input.ts +291 -0
  84. package/events/connection-line.ts +68 -0
  85. package/events/connector.ts +116 -0
  86. package/events/draggable.ts +119 -0
  87. package/events/events.ts +7 -0
  88. package/events/input-box.ts +213 -0
  89. package/events/node-x-btn.ts +25 -0
  90. package/index.d.ts +4 -0
  91. package/package.json +49 -0
  92. package/renderers/apollo/apollo.ts +21 -0
  93. package/renderers/apollo/constants.ts +40 -0
  94. package/renderers/apollo/renderer.ts +331 -0
  95. package/renderers/atlas/atlas.ts +15 -0
  96. package/renderers/constants.ts +87 -0
  97. package/renderers/renderer.ts +1288 -0
  98. package/renderers/representer-node.ts +52 -0
  99. package/renderers/representer.ts +25 -0
  100. package/src/category.ts +107 -0
  101. package/src/colors.ts +20 -0
  102. package/src/comment.ts +142 -0
  103. package/src/connection.ts +114 -0
  104. package/src/context-menu.ts +194 -0
  105. package/src/coordinates.ts +74 -0
  106. package/src/core.ts +202 -0
  107. package/src/ctx-menu-registry.ts +143 -0
  108. package/src/dropdown-menu.ts +215 -0
  109. package/src/field.ts +595 -0
  110. package/src/flyout.ts +165 -0
  111. package/src/fonts-manager.ts +38 -0
  112. package/src/grid.ts +162 -0
  113. package/src/headless-node.ts +27 -0
  114. package/src/index.ts +115 -0
  115. package/src/inject-headless.ts +18 -0
  116. package/src/inject.ts +213 -0
  117. package/src/main-workspace.ts +51 -0
  118. package/src/mutator.ts +40 -0
  119. package/src/node-types.ts +27 -0
  120. package/src/nodesvg.ts +756 -0
  121. package/src/prototypes.ts +9 -0
  122. package/src/renderer-map.ts +86 -0
  123. package/src/styles.css +224 -0
  124. package/src/toolbox.ts +125 -0
  125. package/src/types.ts +205 -0
  126. package/src/undo-redo.ts +87 -0
  127. package/src/visual-types.ts +29 -0
  128. package/src/widget-prototypes.ts +11 -0
  129. package/src/widget.ts +139 -0
  130. package/src/workspace-coords.ts +14 -0
  131. package/src/workspace-svg.ts +736 -0
  132. package/src/workspace.ts +155 -0
  133. package/test-server.js +61 -0
  134. package/themes/dark.ts +32 -0
  135. package/themes/default.ts +28 -0
  136. package/themes/themes.ts +9 -0
  137. package/tsconfig.json +25 -0
  138. package/typedoc.json +10 -0
  139. package/util/emitter.ts +33 -0
  140. package/util/env.ts +11 -0
  141. package/util/escape-html.ts +22 -0
  142. package/util/eventer.ts +108 -0
  143. package/util/has-prop.ts +4 -0
  144. package/util/parse-color.ts +42 -0
  145. package/util/path.ts +99 -0
  146. package/util/styler.ts +41 -0
  147. package/util/uid.ts +184 -0
  148. package/util/unescape-html.ts +22 -0
  149. package/util/user-state.ts +68 -0
  150. package/util/wait-anim-frames.ts +24 -0
  151. package/util/window-listeners.ts +62 -0
  152. package/webpack.config.js +80 -0
@@ -0,0 +1,155 @@
1
+ import NodePrototypes from "./prototypes";
2
+ import newHeadlessNode from "./headless-node";
3
+ import Widget from "./widget";
4
+ import WidgetPrototypes from "./widget-prototypes";
5
+ import CommentModel from "./comment";
6
+ import UndoRedoHistory from "./undo-redo";
7
+ import { ConnectableField } from "./field";
8
+
9
+ /**
10
+ * Headless workspace that holds nodes, widgets, and comments without any rendering or camera.
11
+ */
12
+ class Workspace {
13
+ _nodeDB: Map<string, any>; // Node storage
14
+ _widgetDB: Map<string, Widget>;
15
+ _commentDB: Set<CommentModel>;
16
+ isHeadless: boolean = true;
17
+ constructor() {
18
+ this._nodeDB = new Map();
19
+ this._widgetDB = new Map();
20
+ this._commentDB = new Set();
21
+ }
22
+
23
+ /** Node management */
24
+ addNode(node: any, nodeId?: string) {
25
+ const id = nodeId || node.id;
26
+ if (this._nodeDB.has(id)) {
27
+ console.warn(`Node with id ${id} already exists, overwriting.`);
28
+ }
29
+ if (node.workspace !== this) node.workspace = this;
30
+ this._nodeDB.set(id, node);
31
+ }
32
+
33
+ newNode(type: keyof typeof NodePrototypes, add: boolean = true) {
34
+ if (!NodePrototypes[type]) return;
35
+ const node = newHeadlessNode(type as string);
36
+ if (!node) return;
37
+ if (add) this.addNode(node);
38
+ return node;
39
+ }
40
+
41
+ spawnAt(type: keyof typeof NodePrototypes, x: number, y: number) {
42
+ const node = this.newNode(type, false);
43
+ if (!node) return;
44
+ node.relativeCoords.set(x, y);
45
+ this.addNode(node);
46
+ return node;
47
+ }
48
+
49
+ getNode(id: string | any) {
50
+ if (id instanceof Object && id.id) return id;
51
+ return this._nodeDB.get(id);
52
+ }
53
+
54
+ removeNodeById(id: string) {
55
+ const node = this._nodeDB.get(id);
56
+ if (!node) return;
57
+ this.derefNode(node);
58
+ this._nodeDB.delete(id);
59
+ }
60
+
61
+ removeNode(node: any) {
62
+ if (!node) return;
63
+ this.removeNodeById(node.id);
64
+ }
65
+
66
+ derefNode(node: any) {
67
+ const prev = node.previousConnection?.getFrom?.();
68
+ if (prev?.nextConnection) prev.nextConnection.disconnectTo?.();
69
+ const next = node.nextConnection?.getTo?.();
70
+ if (next?.previousConnection) next.previousConnection.disconnectFrom?.();
71
+
72
+ for (let field of node.allFields()) {
73
+ if ((field as ConnectableField).hasConnectable?.()) {
74
+ (field as ConnectableField).disconnect();
75
+ }
76
+ }
77
+ }
78
+
79
+ /** Widget management */
80
+ newWidget(type: string): Widget | void {
81
+ const opts = WidgetPrototypes[type];
82
+ if (!opts) return;
83
+ const wdgt = opts.cls ? new opts.cls(this, opts) : new Widget(this, opts);
84
+ this._widgetDB.set(wdgt.id, wdgt);
85
+ return wdgt;
86
+ }
87
+
88
+ getWidget(id: string): Widget | undefined {
89
+ return this._widgetDB.get(id);
90
+ }
91
+
92
+ /** Comment management */
93
+ addComment() {
94
+ const model = new CommentModel(this);
95
+ this._commentDB.add(model);
96
+ return model;
97
+ }
98
+
99
+ getComment(id: string) {
100
+ return Array.from(this._commentDB).find(e => e.id === id);
101
+ }
102
+
103
+ removeComment(commentOrId: CommentModel | string) {
104
+ let comment: CommentModel | undefined;
105
+ if (typeof commentOrId === "string") comment = this.getComment(commentOrId);
106
+ else comment = commentOrId;
107
+ if (!comment) return false;
108
+ this._commentDB.delete(comment);
109
+ return true;
110
+ }
111
+
112
+ getComments() {
113
+ return Array.from(this._commentDB);
114
+ }
115
+
116
+ /**
117
+ * Internal: Add widget to DB
118
+ * @param wdgt - The widget
119
+ */
120
+ _addWidgetToDB(wdgt: Widget) {
121
+ this._widgetDB.set(wdgt.id, wdgt);
122
+ }
123
+ /**
124
+ * Internal: Delete a widget from DB.
125
+ * @param wdgt - Widget to delete
126
+ */
127
+ _delWidgetFromDB(wdgt: Widget) {
128
+ this._widgetDB.delete(wdgt.id);
129
+ }
130
+
131
+ /** Serialization */
132
+ fromJson(json: { nodes: any[]; circular: boolean }, recordBigEvent: boolean = false) {
133
+ for (let [, node] of this._nodeDB.entries()) this.removeNode(node);
134
+
135
+ if (json.circular) {
136
+ for (let node of json.nodes) {
137
+ (node.constructor as any).deserialize(node, this);
138
+ }
139
+ } else {
140
+ for (let node of json.nodes) {
141
+ (node.constructor as any).fromJson(node, this);
142
+ }
143
+ }
144
+ }
145
+
146
+ toJson(circular: boolean) {
147
+ const nodes = [];
148
+ for (let [, node] of this._nodeDB) {
149
+ if (node.topLevel) nodes.push(circular ? node.serialize() : node.toJson());
150
+ }
151
+ return { circular, nodes };
152
+ }
153
+ }
154
+
155
+ export default Workspace;
package/test-server.js ADDED
@@ -0,0 +1,61 @@
1
+ const express = require('express');
2
+ const path = require('path');
3
+ const chokidar = require('chokidar');
4
+ const livereload = require('livereload');
5
+ const connectLivereload = require('connect-livereload');
6
+ const { exec } = require('child_process');
7
+
8
+ const app = express();
9
+ const PORT = 3000;
10
+
11
+ // Path to your dist folder
12
+ const DIST_DIR = path.resolve(__dirname, 'dist');
13
+
14
+ // ---- LiveReload server ----
15
+ const liveReloadServer = livereload.createServer();
16
+ liveReloadServer.watch(DIST_DIR);
17
+
18
+ // Notify browser when files change
19
+ liveReloadServer.server.once('connection', () => {
20
+ setTimeout(() => {
21
+ liveReloadServer.refresh('/');
22
+ }, 100);
23
+ });
24
+
25
+ // Middleware to inject livereload script
26
+ app.use(connectLivereload());
27
+
28
+ // Serve all files relative to project root
29
+ app.use(express.static(DIST_DIR, { index: 'index.html' }));
30
+
31
+ // Watch source files for changes and rebuild
32
+ // Watch multiple source directories for changes
33
+ const watcher = chokidar.watch([
34
+ path.resolve(__dirname, 'src'),
35
+ path.resolve(__dirname, 'themes'),
36
+ path.resolve(__dirname, 'renderers'),
37
+ path.resolve(__dirname, 'util'),
38
+ path.resolve(__dirname, 'events'),
39
+ path.resolve(__dirname, 'controllers'),
40
+ path.resolve(__dirname, 'media'),
41
+ path.resolve(__dirname, 'comment-renderer')
42
+ ], {
43
+ ignored: /(^|[\/\\])\../, // ignore dotfiles
44
+ persistent: true
45
+ });
46
+
47
+
48
+ watcher.on('change', (filePath) => {
49
+ console.log(`[watcher] File changed: ${filePath}`);
50
+ console.log('[watcher] Running build...');
51
+ exec('npm run build', (err, stdout, stderr) => {
52
+ if (err) console.error(err);
53
+ console.log(stdout);
54
+ console.error(stderr);
55
+ liveReloadServer.refresh('/');
56
+ });
57
+ });
58
+
59
+ app.listen(PORT, () => {
60
+ console.log(`Test server running: http://localhost:${PORT}`);
61
+ });
package/themes/dark.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { WSTheme } from '../src/workspace-svg';
2
+
3
+ const KabelDarkTheme: WSTheme = {
4
+ UIStyles: {
5
+ workspaceBGColor: '#121412ff',
6
+ toolboxCategoriesBG: {
7
+ position: 'absolute',
8
+ left: '0',
9
+ top: '0',
10
+ width: '20%',
11
+ height: '100%',
12
+ background: '#1b1b1b',
13
+ color: '#e0e0e0',
14
+ overflowY: 'auto',
15
+ border: 'none',
16
+ outline: 'none',
17
+ },
18
+ toolboxFlyoutBG: {
19
+ display: 'block',
20
+ background: '#1e1e1e',
21
+ color: '#e0e0e0',
22
+ overflowY: 'auto',
23
+ padding: '4px',
24
+ borderRadius: '4px',
25
+ boxShadow: '0 2px 6px rgba(0,0,0,0.5)',
26
+ border: 'none',
27
+ outline: 'none',
28
+ },
29
+ },
30
+ };
31
+
32
+ export default KabelDarkTheme;
@@ -0,0 +1,28 @@
1
+ import type {WSTheme} from '../src/workspace-svg'
2
+
3
+
4
+
5
+ const KabelWSTheme: WSTheme = {
6
+ UIStyles: {
7
+ workspaceBGColor: '#ffffff',
8
+ toolboxCategoriesBG: {
9
+ position: 'absolute',
10
+ left: '0',
11
+ top: '0',
12
+ width: '20%',
13
+ height: '100%',
14
+ background: 'rgba(240,240,240,0.9)',
15
+ overflowY: 'auto',
16
+ },
17
+ toolboxFlyoutBG: {
18
+ display: 'none',
19
+ background: 'rgba(240,240,240,0.9)', // reuse same neutral bg
20
+ overflowY: 'auto',
21
+ padding: '4px', // like nodes padding
22
+ borderRadius: '4px',
23
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
24
+ },
25
+ },
26
+ };
27
+
28
+ export default KabelWSTheme;
@@ -0,0 +1,9 @@
1
+ import KabelWSTheme from './default';
2
+ import KabelDarkTheme from './dark';
3
+
4
+ const Themes = {
5
+ Classic: KabelWSTheme,
6
+ Dark: KabelDarkTheme
7
+ }
8
+
9
+ export default Themes;
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ // File Layout
4
+ "rootDir": "./",
5
+ "outDir": "./dist",
6
+
7
+ // Environment Settings
8
+ "module": "node20",
9
+ "target": "ES2022",
10
+ "types": ["node"],
11
+
12
+ // Outputs
13
+ "sourceMap": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+
17
+ // Type Safety
18
+ "strict": true,
19
+ "noUncheckedIndexedAccess": true,
20
+ "exactOptionalPropertyTypes": true,
21
+ "skipLibCheck": true
22
+ },
23
+ "include": ["src/**/*", "util", "controllers", "events", "comment-renderer", "renderers", "*.d.ts"],
24
+ "exclude": ["node_modules", "dist"]
25
+ }
package/typedoc.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "entryPoints": ["src/index.ts", "src"],
3
+ "out": "_READ_ME_MEDIA_/internal-docs",
4
+ "exclude": ["**/node_modules/**", "**/dist/**"],
5
+ "excludePrivate": false,
6
+ "includeVersion": true,
7
+ "tsconfig": "tsconfig.json", // your TS config
8
+ "plugin": ["typedoc-plugin-markdown"], // optional: markdown output
9
+ "name": "Kabel Project Docs"
10
+ }
@@ -0,0 +1,33 @@
1
+ type EventHandler<T = any> = (payload: T) => void;
2
+
3
+ class EventEmitter<Events extends Record<string, any>> {
4
+ private listeners: { [K in keyof Events]?: EventHandler<Events[K]>[] } = {};
5
+
6
+ on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>) {
7
+ if (!this.listeners[event]) this.listeners[event] = [];
8
+ this.listeners[event]!.push(handler);
9
+ return this;
10
+ }
11
+
12
+ off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>) {
13
+ if (!this.listeners[event]) return this;
14
+ this.listeners[event] = this.listeners[event]!.filter(h => h !== handler);
15
+ return this;
16
+ }
17
+
18
+ emit<K extends keyof Events>(event: K, payload: Events[K]) {
19
+ if (!this.listeners[event]) return false;
20
+ this.listeners[event]!.forEach(handler => handler(payload));
21
+ return true;
22
+ }
23
+
24
+ once<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>) {
25
+ const wrapper = (payload: Events[K]) => {
26
+ handler(payload);
27
+ this.off(event, wrapper);
28
+ };
29
+ this.on(event, wrapper);
30
+ return this;
31
+ }
32
+ }
33
+ export default EventEmitter;
package/util/env.ts ADDED
@@ -0,0 +1,11 @@
1
+ const isNode = typeof process !== 'undefined' && !!process.versions?.node;
2
+ const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
3
+ const isWebWorker = typeof self !== 'undefined' && typeof self.importScripts === 'function';
4
+
5
+ const env: {
6
+ isBrowser: boolean;
7
+ isNode: boolean;
8
+ isWebWorker: boolean;
9
+ } = { isNode, isBrowser, isWebWorker };
10
+
11
+ export default env;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Escapes special characters in a string to their corresponding HTML entities.
3
+ *
4
+ * Specifically, it replaces:
5
+ * - `&` with `&amp;`
6
+ * - `'` with `&apos;`
7
+ * - `"` with `&quot;`
8
+ * - `<` with `&lt;`
9
+ * - `>` with `&gt;`
10
+ *
11
+ * @param {string} s - The string to escape.
12
+ * @returns {string} The escaped string with special characters replaced by HTML entities.
13
+ */
14
+ function escapeAttr(s: string): string {
15
+ return s.replace(/&/g, "&amp;")
16
+ .replace(/'/g, "&apos;")
17
+ .replace(/"/g, "&quot;")
18
+ .replace(/</g, "&lt;")
19
+ .replace(/>/g, "&gt;");
20
+ }
21
+
22
+ export default escapeAttr;
@@ -0,0 +1,108 @@
1
+ import { Svg, Rect, Circle, G, Path, Element } from '@svgdotjs/svg.js';
2
+
3
+ export type EventType = string;
4
+ export type EventArgs = Record<string, any>;
5
+ export type EventSetupFn = (el: Element, args?: EventArgs) => (() => void) | void;
6
+
7
+ export interface RegisteredEl {
8
+ tags: string[],
9
+ el: Element;
10
+ type: EventType;
11
+ args?: EventArgs | undefined; // allow undefined explicitly
12
+ destroyFn?: (() => void) | undefined; // allow undefined explicitly
13
+ }
14
+
15
+ /**
16
+ * Used by the Kabel renderer to tag svg.js elements as interactable with the kabel system.
17
+ */
18
+ class Eventer {
19
+ private elements: RegisteredEl[] = [];
20
+ private eventRegistry: Map<EventType, EventSetupFn> = new Map();
21
+
22
+ // Register an event type with a setup function
23
+ registerEvent(type: EventType, setupFn: EventSetupFn) {
24
+ this.eventRegistry.set(type, setupFn);
25
+ return this; // allow chaining
26
+ }
27
+ tagElement(el: Element, tags?: string[] | string) {
28
+ if (!tags) return this;
29
+ const tagList = Array.isArray(tags) ? tags : [tags];
30
+
31
+ // Find the registered elements for this el
32
+ for (const reg of this.elements) {
33
+ if (reg.el === el) {
34
+ for (const t of tagList) {
35
+ if (!reg.tags.includes(t)) reg.tags.push(t);
36
+ }
37
+ }
38
+ }
39
+ return this;
40
+ }
41
+
42
+ destroyByTag(tag: string) {
43
+ let destroyed = false;
44
+ this.elements = this.elements.filter(reg => {
45
+ if (reg.tags.includes(tag)) {
46
+ if (reg.destroyFn) {
47
+ reg.destroyFn();
48
+ destroyed = true;
49
+ }
50
+ return false; // remove this element
51
+ }
52
+ return true; // keep element
53
+ });
54
+ return destroyed ? 1 : 0;
55
+ }
56
+
57
+ // addElement
58
+ addElement(el: Element, types: EventType | EventType[], args?: EventArgs) {
59
+ const typeList = Array.isArray(types) ? types : [types];
60
+ for (const type of typeList) {
61
+ const destroyFn = this.setupElement(el, type, args) as (() => void) | undefined;
62
+ this.elements.push({
63
+ tags: [],
64
+ el,
65
+ type,
66
+ args,
67
+ destroyFn
68
+ });
69
+ }
70
+ return this;
71
+ }
72
+
73
+ // refresh
74
+ refresh() {
75
+ for (const reg of this.elements) {
76
+ if (reg.destroyFn) reg.destroyFn();
77
+ reg.destroyFn = this.setupElement(reg.el, reg.type, reg.args) as (() => void) | undefined;
78
+ }
79
+ }
80
+
81
+
82
+ // Destroy event(s) for an element
83
+ destroyElement(el: Element, type?: EventType) {
84
+ let destroyed = false;
85
+ for (const reg of this.elements) {
86
+ if (reg.el === el && (!type || reg.type === type)) {
87
+ if (reg.destroyFn) {
88
+ reg.destroyFn();
89
+ destroyed = true;
90
+ }
91
+ // Remove from elements array
92
+ this.elements = this.elements.filter(r => r !== reg);
93
+ }
94
+ }
95
+ return destroyed ? 1 : 0;
96
+ }
97
+
98
+ private setupElement(el: Element, type: EventType, args?: EventArgs): (() => void) | undefined {
99
+ const setupFn = this.eventRegistry.get(type);
100
+ if (!setupFn) return;
101
+ const destroyFn = setupFn(el, args);
102
+ return destroyFn instanceof Function ? destroyFn : undefined;
103
+ }
104
+ }
105
+
106
+ const eventer = new Eventer();
107
+ export default eventer;
108
+ export { Eventer };
@@ -0,0 +1,4 @@
1
+
2
+ export default function hasProp(obj: object, name: string) {
3
+ return Object.prototype.hasOwnProperty.call(obj, name)
4
+ }
@@ -0,0 +1,42 @@
1
+ // color-utils.ts
2
+ import type { Color, Hex } from '../src/visual-types';
3
+
4
+ /**
5
+ * Parse any Color type into a hex string "#RRGGBB"
6
+ */
7
+ export function parseColor(color: Color): Hex {
8
+ if (typeof color === 'string') {
9
+ if (color.startsWith('#')) {
10
+ // Already a hex string, normalize to full #RRGGBB
11
+ let hex = color.slice(1);
12
+ if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
13
+ return `#${hex}`;
14
+ } else {
15
+ // RGB string "r, g, b"
16
+ const parts = color.split(',').map(s => parseInt(s.trim(), 10));
17
+ if (parts.length !== 3) throw new Error(`Invalid RGB string: ${color}`);
18
+ const [r, g, b] = parts;
19
+ if ((!r && r!== 0) || (!g && g!== 0) || (!b && b !== 0)) {
20
+ console.warn(
21
+ "Invalid RGB tuple"
22
+ );
23
+ return "#000";
24
+ }
25
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b
26
+ .toString(16)
27
+ .padStart(2, '0')}`;
28
+ }
29
+ } else if (Array.isArray(color)) {
30
+ // RGBTuple [r,g,b]
31
+ const [r, g, b] = color;
32
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b
33
+ .toString(16)
34
+ .padStart(2, '0')}`;
35
+ } else {
36
+ // RGBObject {r,g,b}
37
+ const { r, g, b } = color;
38
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b
39
+ .toString(16)
40
+ .padStart(2, '0')}`;
41
+ }
42
+ }
package/util/path.ts ADDED
@@ -0,0 +1,99 @@
1
+ // path.ts
2
+ /**
3
+ * Utility functions to generate SVG path strings or translate them.
4
+ */
5
+
6
+ /** Rounded rectangle */
7
+ export function roundedRect(width: number, height: number, radius: number): string {
8
+ radius = Math.min(radius, width / 2, height / 2);
9
+ return `
10
+ M${radius},0
11
+ H${width - radius}
12
+ A${radius},${radius} 0 0 1 ${width},${radius}
13
+ V${height - radius}
14
+ A${radius},${radius} 0 0 1 ${width - radius},${height}
15
+ H${radius}
16
+ A${radius},${radius} 0 0 1 0,${height - radius}
17
+ V${radius}
18
+ A${radius},${radius} 0 0 1 ${radius},0
19
+ Z
20
+ `.replace(/\s+/g, ' ').trim();
21
+ }
22
+
23
+ /** Rounded triangle pointing up */
24
+ export function roundedTri(width: number, height: number, radius: number): string {
25
+ const halfW = width / 2;
26
+ radius = Math.min(radius, halfW, height / 2);
27
+ return `
28
+ M${halfW},0
29
+ L${width - radius},${height - radius}
30
+ A${radius},${radius} 0 0 1 ${width - radius*2},${height}
31
+ L${radius*2},${height}
32
+ A${radius},${radius} 0 0 1 ${radius},${height - radius}
33
+ Z
34
+ `.replace(/\s+/g, ' ').trim();
35
+ }
36
+
37
+ /** Circle */
38
+ export function circle(radius: number): string {
39
+ return `
40
+ M${radius},0
41
+ A${radius},${radius} 0 1,0 ${-radius},0
42
+ A${radius},${radius} 0 1,0 ${radius},0
43
+ Z
44
+ `.replace(/\s+/g, ' ').trim();
45
+ }
46
+
47
+ /** Ellipse */
48
+ export function ellipse(rx: number, ry: number): string {
49
+ return `
50
+ M${rx},0
51
+ A${rx},${ry} 0 1,0 ${-rx},0
52
+ A${rx},${ry} 0 1,0 ${rx},0
53
+ Z
54
+ `.replace(/\s+/g, ' ').trim();
55
+ }
56
+
57
+ /** Star with n points */
58
+ export function star(radius: number, points: number = 5): string {
59
+ if (points < 2) throw new Error('Star must have at least 2 points');
60
+ let path = '';
61
+ const step = (Math.PI * 2) / (points * 2);
62
+ for (let i = 0; i < points * 2; i++) {
63
+ const r = i % 2 === 0 ? radius : radius / 2;
64
+ const x = r * Math.sin(i * step);
65
+ const y = -r * Math.cos(i * step);
66
+ path += i === 0 ? `M${x},${y}` : ` L${x},${y}`;
67
+ }
68
+ path += ' Z';
69
+ return path;
70
+ }
71
+
72
+ /** Regular polygon (triangle, pentagon, hexagon, etc) */
73
+ export function polygon(radius: number, sides: number = 3): string {
74
+ if (sides < 3) throw new Error('Polygon must have at least 3 sides');
75
+ let path = '';
76
+ const step = (Math.PI * 2) / sides;
77
+ for (let i = 0; i < sides; i++) {
78
+ const x = radius * Math.cos(i * step - Math.PI / 2);
79
+ const y = radius * Math.sin(i * step - Math.PI / 2);
80
+ path += i === 0 ? `M${x},${y}` : ` L${x},${y}`;
81
+ }
82
+ path += ' Z';
83
+ return path;
84
+ }
85
+ import SvgPath from 'svgpath';
86
+
87
+ /**
88
+ * Rotate an SVG path string around a given point
89
+ * @param path - SVG path string
90
+ * @param angle - rotation angle in degrees
91
+ * @param cx - x-coordinate of rotation center (default 0)
92
+ * @param cy - y-coordinate of rotation center (default 0)
93
+ * @returns new rotated SVG path string
94
+ */
95
+ export function rotatePath(path: string, angle: number, cx = 0, cy = 0): string {
96
+ return new SvgPath(path)
97
+ .rotate(angle, cx, cy)
98
+ .toString();
99
+ }
package/util/styler.ts ADDED
@@ -0,0 +1,41 @@
1
+ class Styler {
2
+ private styles: Map<string, HTMLStyleElement>;
3
+
4
+ constructor() {
5
+ this.styles = new Map();
6
+ }
7
+
8
+ appendStyles(id: string, css: string): void {
9
+ if (this.styles.has(id)) return; // Do not append if id exists
10
+
11
+ const styleEl = document.createElement('style');
12
+ styleEl.id = id;
13
+ styleEl.textContent = css;
14
+ document.head.appendChild(styleEl);
15
+ this.styles.set(id, styleEl);
16
+ }
17
+
18
+ removeStyles(id: string): void {
19
+ const styleEl = this.styles.get(id);
20
+ if (!styleEl) return;
21
+
22
+ styleEl.remove();
23
+ this.styles.delete(id);
24
+ }
25
+
26
+ updateStyles(id: string, css: string): void {
27
+ const styleEl = this.styles.get(id);
28
+ if (!styleEl) return;
29
+
30
+ styleEl.textContent = css;
31
+ }
32
+
33
+ hasStyles(id: string): boolean {
34
+ return this.styles.has(id);
35
+ }
36
+ }
37
+ export {
38
+ Styler
39
+ }
40
+ const styler = new Styler();
41
+ export default styler;