@ghchinoy/litflow 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,71 @@
1
+ import { LitElement, html } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { Position } from '@xyflow/system';
4
+
5
+ @customElement('lit-handle')
6
+ export class LitHandle extends LitElement {
7
+ createRenderRoot() {
8
+ return this;
9
+ }
10
+
11
+ @property({ type: String, reflect: true })
12
+ type: 'source' | 'target' = 'source';
13
+
14
+ @property({ type: String, reflect: true, attribute: 'data-handlepos' })
15
+ position: Position = Position.Top;
16
+
17
+ @property({ type: String, reflect: true, attribute: 'data-handleid' })
18
+ handleId?: string;
19
+
20
+ @property({ type: String, reflect: true, attribute: 'data-nodeid' })
21
+ nodeId?: string;
22
+
23
+ constructor() {
24
+ super();
25
+ this.addEventListener('mousedown', (e) => this._onPointerDown(e));
26
+ this.addEventListener('touchstart', (e) => this._onPointerDown(e));
27
+ }
28
+
29
+ private _onPointerDown(e: MouseEvent | TouchEvent) {
30
+ e.stopPropagation();
31
+ const nodeId = this.getAttribute('data-nodeid');
32
+
33
+ this.dispatchEvent(new CustomEvent('handle-pointer-down', {
34
+ bubbles: true,
35
+ composed: true,
36
+ detail: {
37
+ event: e,
38
+ handleId: this.handleId,
39
+ nodeId,
40
+ type: this.type,
41
+ handleDomNode: this,
42
+ }
43
+ }));
44
+ }
45
+
46
+ connectedCallback() {
47
+ super.connectedCallback();
48
+ this.classList.add('lit-flow__handle');
49
+ this.classList.add(this.type);
50
+ this.classList.add('connectable');
51
+ this.classList.add('connectableend');
52
+ }
53
+
54
+ updated(changedProperties: Map<string, any>) {
55
+ if (changedProperties.has('nodeId') || changedProperties.has('handleId') || changedProperties.has('type')) {
56
+ this.setAttribute('data-id', `lit-flow-${this.nodeId || ''}-${this.handleId || ''}-${this.type}`);
57
+ }
58
+ }
59
+
60
+ render() {
61
+ return html`
62
+ <div style="
63
+ width: 100%;
64
+ height: 100%;
65
+ background: inherit;
66
+ border-radius: inherit;
67
+ pointer-events: none;
68
+ "></div>
69
+ `;
70
+ }
71
+ }
@@ -0,0 +1,161 @@
1
+ import { LitElement, html, css, svg } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { SignalWatcher } from '@lit-labs/signals';
4
+ import {
5
+ getInternalNodesBounds,
6
+ getBoundsOfRects,
7
+ XYMinimap,
8
+ type Rect,
9
+ type XYMinimapInstance,
10
+ type PanZoomInstance,
11
+ type InternalNodeBase,
12
+ type Transform,
13
+ } from '@xyflow/system';
14
+
15
+ type Constructor<T> = new (...args: any[]) => T;
16
+
17
+ @customElement('lit-minimap')
18
+ export class LitMinimap extends (SignalWatcher as <T extends Constructor<LitElement>>(base: T) => T)(LitElement) {
19
+ static styles = css`
20
+ :host {
21
+ display: block;
22
+ position: absolute;
23
+ right: 10px;
24
+ bottom: 10px;
25
+ z-index: 5;
26
+ background: var(--md-sys-color-surface);
27
+ border: 1px solid var(--md-sys-color-outline-variant);
28
+ border-radius: var(--md-sys-shape-corner-extra-small);
29
+ box-shadow: var(--md-sys-elevation-1);
30
+ overflow: hidden;
31
+ }
32
+
33
+ svg {
34
+ display: block;
35
+ }
36
+
37
+ .minimap-node {
38
+ fill: var(--md-sys-color-surface-variant);
39
+ stroke: var(--md-sys-color-outline-variant);
40
+ stroke-width: 1;
41
+ }
42
+
43
+ .minimap-mask {
44
+ fill: var(--lit-flow-minimap-mask);
45
+ fill-rule: evenodd;
46
+ }
47
+ `;
48
+
49
+ @property({ type: Object })
50
+ panZoom?: PanZoomInstance;
51
+
52
+ @property({ type: Object })
53
+ nodeLookup = new Map<string, InternalNodeBase>();
54
+
55
+ @property({ type: Array })
56
+ transform: Transform = [0, 0, 1];
57
+
58
+ @property({ type: Array })
59
+ translateExtent: [[number, number], [number, number]] = [[-Infinity, -Infinity], [Infinity, Infinity]];
60
+
61
+ @property({ type: Number })
62
+ width = 0;
63
+
64
+ @property({ type: Number })
65
+ height = 0;
66
+
67
+ @state()
68
+ private _minimapInstance?: XYMinimapInstance;
69
+
70
+ updated(changedProperties: Map<string, any>) {
71
+ if (!this._minimapInstance && this.panZoom) {
72
+ const svgEl = this.shadowRoot?.querySelector('svg');
73
+ if (svgEl) {
74
+ this._minimapInstance = XYMinimap({
75
+ domNode: svgEl,
76
+ panZoom: this.panZoom,
77
+ getTransform: () => this.transform,
78
+ getViewScale: () => {
79
+ const boundingRect = this._getBoundingRect();
80
+ return Math.max(boundingRect.width / 200, boundingRect.height / 150);
81
+ },
82
+ });
83
+ }
84
+ }
85
+
86
+ if (this._minimapInstance && (changedProperties.has('width') || changedProperties.has('height') || changedProperties.has('translateExtent'))) {
87
+ this._minimapInstance.update({
88
+ width: this.width,
89
+ height: this.height,
90
+ translateExtent: this.translateExtent,
91
+ });
92
+ }
93
+ }
94
+
95
+ private _getBoundingRect(): Rect {
96
+ const viewBB: Rect = {
97
+ x: -this.transform[0] / this.transform[2],
98
+ y: -this.transform[1] / this.transform[2],
99
+ width: this.width / this.transform[2],
100
+ height: this.height / this.transform[2],
101
+ };
102
+
103
+ return this.nodeLookup.size > 0
104
+ ? getBoundsOfRects(getInternalNodesBounds(this.nodeLookup), viewBB)
105
+ : viewBB;
106
+ }
107
+
108
+ render() {
109
+ const boundingRect = this._getBoundingRect();
110
+ const viewBB: Rect = {
111
+ x: -this.transform[0] / this.transform[2],
112
+ y: -this.transform[1] / this.transform[2],
113
+ width: this.width / this.transform[2],
114
+ height: this.height / this.transform[2],
115
+ };
116
+
117
+ const elementWidth = 200;
118
+ const elementHeight = 150;
119
+ const scaledWidth = boundingRect.width / elementWidth;
120
+ const scaledHeight = boundingRect.height / elementHeight;
121
+ const viewScale = Math.max(scaledWidth, scaledHeight);
122
+ const viewWidth = viewScale * elementWidth;
123
+ const viewHeight = viewScale * elementHeight;
124
+ const offset = 5 * viewScale;
125
+
126
+ const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset;
127
+ const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
128
+ const width = viewWidth + offset * 2;
129
+ const height = viewHeight + offset * 2;
130
+
131
+ return html`
132
+ <svg
133
+ width="${elementWidth}"
134
+ height="${elementHeight}"
135
+ viewBox="${x} ${y} ${width} ${height}"
136
+ >
137
+ ${Array.from(this.nodeLookup.values()).map((node) => {
138
+ const { x, y } = node.internals.positionAbsolute;
139
+ const w = node.measured.width || 0;
140
+ const h = node.measured.height || 0;
141
+ return svg`
142
+ <rect
143
+ class="minimap-node"
144
+ x="${x}"
145
+ y="${y}"
146
+ width="${w}"
147
+ height="${h}"
148
+ rx="2"
149
+ ry="2"
150
+ />
151
+ `;
152
+ })}
153
+ <path
154
+ class="minimap-mask"
155
+ d="M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
156
+ M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z"
157
+ />
158
+ </svg>
159
+ `;
160
+ }
161
+ }
@@ -0,0 +1,40 @@
1
+ import { LitElement, html } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { SignalWatcher } from '@lit-labs/signals';
4
+
5
+ type Constructor<T> = new (...args: any[]) => T;
6
+
7
+ @customElement('lit-node')
8
+ export class LitNode extends (SignalWatcher as <T extends Constructor<LitElement>>(base: T) => T)(LitElement) {
9
+ createRenderRoot() {
10
+ return this;
11
+ }
12
+
13
+ @property({ type: String })
14
+ label = '';
15
+
16
+ @property({ type: String, reflect: true })
17
+ type = 'default';
18
+
19
+ @property({ type: Object })
20
+ data: any = {};
21
+
22
+ @property({ type: Boolean, reflect: true })
23
+ selected = false;
24
+
25
+ @property({ type: String, attribute: 'data-id', reflect: true })
26
+ nodeId = '';
27
+
28
+ render() {
29
+ return html`
30
+ <div class="label" style="font-size: 12px; color: #222; pointer-events: none;">${this.label}</div>
31
+ <slot></slot>
32
+ ${this.type === 'input' || this.type === 'default'
33
+ ? html`<lit-handle type="source" data-handlepos="bottom" data-nodeid="${this.nodeId}"></lit-handle>`
34
+ : ''}
35
+ ${this.type === 'output' || this.type === 'default'
36
+ ? html`<lit-handle type="target" data-handlepos="top" data-nodeid="${this.nodeId}"></lit-handle>`
37
+ : ''}
38
+ `;
39
+ }
40
+ }
package/src/store.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { signal } from '@lit-labs/signals';
2
+ import {
3
+ type NodeBase,
4
+ type EdgeBase,
5
+ type InternalNodeBase,
6
+ type CoordinateExtent,
7
+ type NodeOrigin,
8
+ type SnapGrid,
9
+ type Transform,
10
+ type PanZoomInstance,
11
+ type ConnectionInProgress,
12
+ infiniteExtent,
13
+ } from '@xyflow/system';
14
+
15
+ export interface FlowState {
16
+ nodes: ReturnType<typeof signal<NodeBase[]>>;
17
+ edges: ReturnType<typeof signal<EdgeBase[]>>;
18
+ nodeLookup: Map<string, InternalNodeBase>;
19
+ parentLookup: Map<string, Map<string, InternalNodeBase>>;
20
+ nodeExtent: CoordinateExtent;
21
+ snapGrid: SnapGrid;
22
+ snapToGrid: boolean;
23
+ nodeOrigin: NodeOrigin;
24
+ multiSelectionActive: boolean;
25
+ transform: ReturnType<typeof signal<Transform>>;
26
+ autoPanOnNodeDrag: boolean;
27
+ nodesDraggable: boolean;
28
+ selectNodesOnDrag: boolean;
29
+ nodeDragThreshold: number;
30
+ panZoom: PanZoomInstance | null;
31
+ domNode: Element | null;
32
+ connectionInProgress: ReturnType<typeof signal<ConnectionInProgress | null>>;
33
+ }
34
+
35
+ export function createInitialState(): FlowState {
36
+ return {
37
+ nodes: signal([]),
38
+ edges: signal([]),
39
+ nodeLookup: new Map(),
40
+ parentLookup: new Map(),
41
+ nodeExtent: infiniteExtent,
42
+ snapGrid: [15, 15],
43
+ snapToGrid: false,
44
+ nodeOrigin: [0, 0],
45
+ multiSelectionActive: false,
46
+ transform: signal([0, 0, 1]),
47
+ autoPanOnNodeDrag: true,
48
+ nodesDraggable: true,
49
+ selectNodesOnDrag: true,
50
+ nodeDragThreshold: 0,
51
+ panZoom: null,
52
+ domNode: null,
53
+ connectionInProgress: signal(null),
54
+ };
55
+ }
package/src/theme.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { css } from 'lit';
2
+
3
+ export const m3Tokens = css`
4
+ :host {
5
+ /* Colors - Light Theme Defaults */
6
+ --md-sys-color-primary: #005ac1;
7
+ --md-sys-color-on-primary: #ffffff;
8
+ --md-sys-color-primary-container: #d8e2ff;
9
+ --md-sys-color-on-primary-container: #001a41;
10
+
11
+ --md-sys-color-secondary: #575e71;
12
+ --md-sys-color-on-secondary: #ffffff;
13
+ --md-sys-color-secondary-container: #dbe2f9;
14
+ --md-sys-color-on-secondary-container: #141b2c;
15
+
16
+ --md-sys-color-surface: #fefbff;
17
+ --md-sys-color-on-surface: #1b1b1f;
18
+ --md-sys-color-surface-variant: #e1e2ec;
19
+ --md-sys-color-on-surface-variant: #44474f;
20
+
21
+ --md-sys-color-outline: #74777f;
22
+ --md-sys-color-outline-variant: #c4c6d0;
23
+
24
+ --md-sys-color-background: #fefbff;
25
+ --md-sys-color-on-background: #1b1b1f;
26
+
27
+ --md-sys-color-error: #ba1a1a;
28
+ --md-sys-color-on-error: #ffffff;
29
+
30
+ /* Typography */
31
+ --md-sys-typescale-body-medium-font: Roboto, sans-serif;
32
+ --md-sys-typescale-body-medium-size: 14px;
33
+ --md-sys-typescale-label-small-size: 11px;
34
+ --md-sys-typescale-label-medium-size: 12px;
35
+
36
+ /* Elevation */
37
+ --md-sys-elevation-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
38
+ --md-sys-elevation-2: 0px 2px 6px 2px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
39
+
40
+ /* Shape */
41
+ --md-sys-shape-corner-small: 8px;
42
+ --md-sys-shape-corner-medium: 12px;
43
+ --md-sys-shape-corner-extra-small: 4px;
44
+
45
+ /* LitFlow Specific Tokens (mapped to M3) */
46
+ --lit-flow-node-bg: var(--md-sys-color-surface);
47
+ --lit-flow-node-border: var(--md-sys-color-outline-variant);
48
+ --lit-flow-node-selected-border: var(--md-sys-color-primary);
49
+ --lit-flow-node-text: var(--md-sys-color-on-surface);
50
+
51
+ --lit-flow-handle-source: var(--md-sys-color-secondary);
52
+ --lit-flow-handle-target: var(--md-sys-color-primary);
53
+ --lit-flow-handle-outline: var(--md-sys-color-surface);
54
+
55
+ --lit-flow-controls-bg: var(--md-sys-color-surface);
56
+ --lit-flow-controls-button-text: var(--md-sys-color-on-surface);
57
+
58
+ --lit-flow-minimap-bg: var(--md-sys-color-surface);
59
+ --lit-flow-minimap-mask: rgba(0, 0, 0, 0.08);
60
+ }
61
+ `;