@grafloria/element 0.4.1 → 0.4.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafloria/element",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -44,6 +44,7 @@
44
44
  */
45
45
  import { GroupModel, NodeModel, type GridColumnLayout } from '@grafloria/engine';
46
46
  import { type DashboardGridHandle, type DashboardGridOptions, type DashboardResponsiveOptions } from './grid-binder.js';
47
+ import type { SplitNode } from './split-layout.js';
47
48
  /** A widget, declared as data. */
48
49
  export interface DashboardWidgetSpec {
49
50
  id: string;
@@ -60,10 +61,50 @@ export interface DashboardWidgetSpec {
60
61
  y?: number;
61
62
  /** Pinned: never pushed, refuses the mover, survives every reflow. */
62
63
  pinned?: boolean;
64
+ /**
65
+ * SIZE LIMITS in cells (gridstack's minW/maxW/minH/maxH). A resize — by
66
+ * hand, by the API, or by a column change scaling widths — clamps to them.
67
+ * `maxRows` here is the WIDGET's row limit; a container's inner row count is
68
+ * its own `maxRows` field one level up, which is why these live in `limits`.
69
+ */
70
+ limits?: {
71
+ minSpan?: number;
72
+ maxSpan?: number;
73
+ minRows?: number;
74
+ maxRows?: number;
75
+ };
76
+ /** May the user drag it? Default true. The API can always move it. */
77
+ movable?: boolean;
78
+ /** May the user resize it? Default true (no handle when false). The API can always resize it. */
79
+ resizable?: boolean;
63
80
  /** Your payload — passed straight back to `renderWidget`. */
64
81
  data?: Record<string, unknown>;
65
82
  /** Optional title used by the built-in fallback renderer. */
66
83
  title?: string;
84
+ /**
85
+ * CONTAINMENT. A widget carrying `widgets` is a CONTAINER: it mounts as a
86
+ * member group (a locked slab in its parent's grid, exactly like a view's
87
+ * board one level down) with its own nested pack grid bound on it. Children
88
+ * lay out inside its frame; dragging a tile across the boundary adopts it
89
+ * live in either direction, and one undo restores the whole gesture.
90
+ * Containers may nest — tested to TWO levels; deeper is not exercised by
91
+ * the gates and rides at your own risk. A container renders no card of its
92
+ * own (`kind`/`data` are carried for your bookkeeping and serialization,
93
+ * not painted).
94
+ */
95
+ widgets?: DashboardWidgetSpec[];
96
+ /**
97
+ * Container only: column count of the INNER grid (default: the parent
98
+ * board's column count).
99
+ */
100
+ columns?: number;
101
+ /**
102
+ * Container only: the inner grid's designed row count. A child resized past
103
+ * it ESCALATES — the container's slab grows a row in the parent board (the
104
+ * ratchet), instead of the child overflowing the frame. Default: the row
105
+ * extent of the declared children.
106
+ */
107
+ maxRows?: number;
67
108
  }
68
109
  /** One board. Multiple views are the tab pattern: only one is on-camera. */
69
110
  export interface DashboardViewSpec {
@@ -74,14 +115,41 @@ export interface DashboardViewSpec {
74
115
  columns?: number;
75
116
  width?: number;
76
117
  height?: number;
118
+ /** Per-view layout (default: the dashboard-level `layout`). `toJSON()` writes it per view. */
119
+ layout?: 'grid' | 'split';
120
+ /**
121
+ * SPLIT layout only: the authored splitter tree (see `layout`). Omit it and
122
+ * the tree is derived from the widgets' cells, so a grid-authored view keeps
123
+ * its proportions when it opens as a split board. `toJSON()` writes it back.
124
+ */
125
+ tree?: SplitNode | null;
77
126
  }
78
127
  export interface DashboardOptions {
79
128
  /** Column count for every view (default 12). */
80
129
  columns?: number;
81
130
  /** Gap between widgets AND the board padding, px (default 8). */
82
131
  gap?: number;
83
- /** Sizing mode (default 'fit' — squeeze rows; 'grow' extends the board). */
132
+ /**
133
+ * Sizing mode. 'grow': rows keep `rowHeight` and the board extends
134
+ * downward — the default on a FLUID board, and what every grid library
135
+ * does: dragging one tile never resizes another. 'fit': the board keeps its
136
+ * height and rows squeeze so everything stays on one screen (bounded, see
137
+ * `overflow`) — the default on a FIXED board, and the choice for a designer
138
+ * who wants the whole dashboard visible at once.
139
+ */
84
140
  sizing?: 'fit' | 'grow';
141
+ /**
142
+ * HOW THE BOARD IS LAID OUT (the DevExpress question, decided 6 Sep 2026).
143
+ * 'grid' (the default): the cell grid — columns, spans, push, gravity, the
144
+ * gridstack model. 'split': a splitter tree — the board is always covered;
145
+ * one widget fills it, a second halves it, a third halves the larger half
146
+ * the other way; dividers drag as percentages; a drag lifts the widget out
147
+ * and an insertion line on the nearest edge says where it lands; a removed
148
+ * widget's slot goes to its siblings. Sizing is always fit under 'split'.
149
+ * Switch live with `handle.setLayout()`: grid cells become a tree by
150
+ * guillotine cuts, a tree becomes cells by snapping to the columns.
151
+ */
152
+ layout?: 'grid' | 'split';
85
153
  /** Row height in 'grow' mode, px (default 130). */
86
154
  rowHeight?: number;
87
155
  /** Board size, px (default 1180 × 660). */
@@ -89,6 +157,37 @@ export interface DashboardOptions {
89
157
  height?: number;
90
158
  /** Engine float mode (default false → gravity packs upward). */
91
159
  float?: boolean;
160
+ /**
161
+ * DIAGRAM OR LAYOUT — the one switch (decision of 2026-09-06).
162
+ *
163
+ * 'fluid' (the default): the board is 100% of its container, laid out at
164
+ * real CSS pixels; zoom is pinned at 1; a plain wheel scrolls; in 'fit' the
165
+ * height follows the container too. What every grid library does, and
166
+ * what "responsive" means to a dashboard author.
167
+ * 'fixed': the authored `width`/`height` are the world, and the camera frames
168
+ * them — today's behaviour, kept for a dashboard embedded inside a larger
169
+ * diagram. An explicit `width` implies 'fixed', so existing boards keep
170
+ * their behaviour without naming a mode.
171
+ */
172
+ mode?: 'fluid' | 'fixed';
173
+ /**
174
+ * FIT MEANS BOUNDED. In 'fit' the board never changes size; widgets do. Past
175
+ * the row floor the design height is a CAPACITY: a drop, resize or
176
+ * `addWidget()` that would need one row too many is refused (the placeholder
177
+ * stays put, the palette chip dims, `addWidget` returns undefined) — at
178
+ * design time, instead of tiles painted past the frame. A board that already
179
+ * holds more than fits (a grow→fit switch, a loaded document) squeezes its
180
+ * rows below the floor: a bounded fit board NEVER scrolls. 'scroll' is the
181
+ * opt-in for boards that want more than fits: the frame extends to hold the
182
+ * rows at the floor height and the canvas pans.
183
+ */
184
+ overflow?: 'bounded' | 'scroll';
185
+ /**
186
+ * STATIC board (gridstack's `staticGrid`): no drag, no resize, no handles —
187
+ * the viewer's mode. The API (moveTo, resize, addWidget, undo) still works,
188
+ * so a designer/viewer pair is one flag apart. Live: `handle.setStatic()`.
189
+ */
190
+ static?: boolean;
92
191
  /**
93
192
  * RIGHT-TO-LEFT boards: column x=0 renders at the RIGHT edge and columns run
94
193
  * leftwards. Cells are untouched — the same `widgets` array describes the
@@ -128,6 +227,14 @@ export interface DashboardSpec {
128
227
  finalize: (api: unknown) => void;
129
228
  /** Live handle, populated by finalize(). */
130
229
  readonly handle: DashboardHandle;
230
+ /**
231
+ * Instance options the spec asks `render()` to apply — a fluid board pins
232
+ * the zoom range to 1 so the layout can never become a scaled picture.
233
+ */
234
+ renderOptions?: {
235
+ minZoom?: number;
236
+ maxZoom?: number;
237
+ };
131
238
  }
132
239
  /**
133
240
  * A whole board as plain data: every `DashboardOptions` field except the
@@ -152,6 +259,14 @@ export interface DashboardHandle {
152
259
  widget(id: string): WidgetHandle | undefined;
153
260
  /** Every widget handle of a view (default: the active one). */
154
261
  widgetsOf(viewId?: string): WidgetHandle[];
262
+ /**
263
+ * Switch a view (default: the active one) between the cell grid and the
264
+ * split tree, live and keeping the picture: cells → tree by guillotine cuts,
265
+ * tree → cells by snapping to the columns. Persisted on the board, so a
266
+ * saved document reopens in the layout it was left in.
267
+ */
268
+ setLayout(layout: 'grid' | 'split', viewId?: string): void;
269
+ getLayout(viewId?: string): 'grid' | 'split';
155
270
  /** Live sizing/float switches — the two prototype toggles. */
156
271
  setSizing(mode: 'fit' | 'grow'): void;
157
272
  getSizing(): 'fit' | 'grow';
@@ -169,6 +284,9 @@ export interface DashboardHandle {
169
284
  /** RTL mirroring, live — pixels only, cells never change. */
170
285
  setRtl(on: boolean): void;
171
286
  getRtl(): boolean;
287
+ /** Static (read-only for the pointer) mode, live — the viewer/designer switch. */
288
+ setStatic(on: boolean): void;
289
+ getStatic(): boolean;
172
290
  /**
173
291
  * Add a widget to a view. CREATES the node (you do not pre-build one), wires
174
292
  * its metadata, and commits node + membership as ONE undoable step.
@@ -287,15 +405,38 @@ export interface DashboardApiRef {
287
405
  addGroup(g: GroupModel): void;
288
406
  getGroup(id: string): GroupModel | undefined;
289
407
  removeGroup?(id: string): unknown;
408
+ removeNode?(id: string): unknown;
409
+ /** Derived writes (a layout switch) bypass the history like the binder's own. */
410
+ runSystemWrite?(fn: () => void): void;
290
411
  };
291
412
  getEngine?: () => {
292
413
  commandManager: {
293
414
  execute(c: unknown): unknown;
294
415
  };
416
+ /** The engine's bus — the kit listens for history events on it (D3). */
417
+ eventBus?: {
418
+ on(event: string, handler: (...args: unknown[]) => void): () => void;
419
+ };
295
420
  };
296
421
  renderNow(): void;
297
422
  viewport?: {
298
423
  fitToBounds(r: unknown, pad: number, o?: unknown): void;
424
+ /** Fluid boards pin the camera instead of framing the board. */
425
+ setZoom?(z: number): unknown;
426
+ getViewport?(): {
427
+ x: number;
428
+ y: number;
429
+ width: number;
430
+ height: number;
431
+ };
432
+ setViewport?(r: {
433
+ x: number;
434
+ y: number;
435
+ width: number;
436
+ height: number;
437
+ }): void;
438
+ /** Camera changes (wheel, drag-pan, a canvas resize) — the fluid clamp listens. */
439
+ onChange?(listener: (state: unknown) => void): () => void;
299
440
  };
300
441
  }
301
442
  /**
@@ -316,7 +457,17 @@ export interface DashboardHandleContext {
316
457
  groups: Map<string, GroupModel>;
317
458
  binders: Map<string, DashboardGridHandle>;
318
459
  specById: Map<string, DashboardWidgetSpec>;
460
+ /** Widget id → the BOARD that owns it (a view id, or a container id). */
319
461
  viewOfWidget: Map<string, string>;
462
+ /** Every board group — the views PLUS every container. Views also live in
463
+ * `groups` (the parking map showView drives); containers deliberately do
464
+ * NOT — parking flings a group to OFFSCREEN_X, and a container must follow
465
+ * its parent, not travel on its own. */
466
+ boardGroups: Map<string, GroupModel>;
467
+ /** Board id → its authored widgets array (views and containers alike). */
468
+ boardWidgets: Map<string, DashboardWidgetSpec[]>;
469
+ /** Board id → the VIEW it belongs to (identity for views). */
470
+ viewOfBoard: Map<string, string>;
320
471
  hosts: Map<string, HTMLElement>;
321
472
  renderWidget: (widget: DashboardWidgetSpec, host: HTMLElement) => void;
322
473
  columns: number;
@@ -324,6 +475,13 @@ export interface DashboardHandleContext {
324
475
  rowHeight: number;
325
476
  boardW: number;
326
477
  boardH: number;
478
+ /** See DashboardOptions.mode / overflow. */
479
+ mode: 'fluid' | 'fixed';
480
+ overflow: 'bounded' | 'scroll';
481
+ /** See DashboardOptions.layout — per view. */
482
+ layoutOf: Map<string, 'grid' | 'split'>;
483
+ /** Set by finalize: re-bind a VIEW's board under the given layout (setLayout). */
484
+ rebindView?: (viewId: string, layout: 'grid' | 'split') => void;
327
485
  /**
328
486
  * Spread verbatim into `toJSON()` output — carries width/height/responsive
329
487
  * and any other authored option so a new `DashboardOptions` field round-trips
@@ -335,6 +493,28 @@ export interface DashboardHandleContext {
335
493
  active: string;
336
494
  /** MUTABLE — set by the caller's finalize once the render API exists. */
337
495
  apiRef: DashboardApiRef | null;
496
+ /** The consumer's layout hook, if any (dashboard() passes its option). */
497
+ onLayoutChange?: (viewId: string, widgets: DashboardWidgetSpec[]) => void;
498
+ /**
499
+ * Set by createDashboardHandle. `reportChanged()` fires `onLayoutChange` for
500
+ * every view whose layout differs from the last report — ONE reporter for
501
+ * pointer commits, API calls, undo/redo and column changes alike, so a
502
+ * consumer's autosave sees every change and never the same change twice.
503
+ * `attachHistory()` is what finalize calls once `apiRef` exists: it
504
+ * subscribes the boards to the command history so an undo re-syncs them
505
+ * without the consumer calling refresh().
506
+ */
507
+ reportChanged?: () => void;
508
+ attachHistory?: () => void;
509
+ /** Unsubscribers dispose() runs. */
510
+ subscriptions?: Array<() => void>;
511
+ /**
512
+ * Re-bind a CONTAINER whose group came back through the history (undo of a
513
+ * container removal restores the group as a fresh GroupModel, which the old
514
+ * binder cannot see). Set by the two finalizes; called by the history
515
+ * handler for any board group the model holds without a binder.
516
+ */
517
+ rebindContainer?: (id: string) => void;
338
518
  }
339
519
  /**
340
520
  * Build THE `DashboardHandle` — the one and only implementation, shared by