@grafloria/element 0.4.2 → 0.4.4

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.2",
3
+ "version": "0.4.4",
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,6 +61,22 @@ 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. */
@@ -98,14 +115,41 @@ export interface DashboardViewSpec {
98
115
  columns?: number;
99
116
  width?: number;
100
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;
101
126
  }
102
127
  export interface DashboardOptions {
103
128
  /** Column count for every view (default 12). */
104
129
  columns?: number;
105
130
  /** Gap between widgets AND the board padding, px (default 8). */
106
131
  gap?: number;
107
- /** 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
+ */
108
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';
109
153
  /** Row height in 'grow' mode, px (default 130). */
110
154
  rowHeight?: number;
111
155
  /** Board size, px (default 1180 × 660). */
@@ -113,6 +157,37 @@ export interface DashboardOptions {
113
157
  height?: number;
114
158
  /** Engine float mode (default false → gravity packs upward). */
115
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;
116
191
  /**
117
192
  * RIGHT-TO-LEFT boards: column x=0 renders at the RIGHT edge and columns run
118
193
  * leftwards. Cells are untouched — the same `widgets` array describes the
@@ -152,6 +227,14 @@ export interface DashboardSpec {
152
227
  finalize: (api: unknown) => void;
153
228
  /** Live handle, populated by finalize(). */
154
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
+ };
155
238
  }
156
239
  /**
157
240
  * A whole board as plain data: every `DashboardOptions` field except the
@@ -176,6 +259,14 @@ export interface DashboardHandle {
176
259
  widget(id: string): WidgetHandle | undefined;
177
260
  /** Every widget handle of a view (default: the active one). */
178
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';
179
270
  /** Live sizing/float switches — the two prototype toggles. */
180
271
  setSizing(mode: 'fit' | 'grow'): void;
181
272
  getSizing(): 'fit' | 'grow';
@@ -193,6 +284,9 @@ export interface DashboardHandle {
193
284
  /** RTL mirroring, live — pixels only, cells never change. */
194
285
  setRtl(on: boolean): void;
195
286
  getRtl(): boolean;
287
+ /** Static (read-only for the pointer) mode, live — the viewer/designer switch. */
288
+ setStatic(on: boolean): void;
289
+ getStatic(): boolean;
196
290
  /**
197
291
  * Add a widget to a view. CREATES the node (you do not pre-build one), wires
198
292
  * its metadata, and commits node + membership as ONE undoable step.
@@ -311,15 +405,38 @@ export interface DashboardApiRef {
311
405
  addGroup(g: GroupModel): void;
312
406
  getGroup(id: string): GroupModel | undefined;
313
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;
314
411
  };
315
412
  getEngine?: () => {
316
413
  commandManager: {
317
414
  execute(c: unknown): unknown;
318
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
+ };
319
420
  };
320
421
  renderNow(): void;
321
422
  viewport?: {
322
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;
323
440
  };
324
441
  }
325
442
  /**
@@ -358,6 +475,13 @@ export interface DashboardHandleContext {
358
475
  rowHeight: number;
359
476
  boardW: number;
360
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;
361
485
  /**
362
486
  * Spread verbatim into `toJSON()` output — carries width/height/responsive
363
487
  * and any other authored option so a new `DashboardOptions` field round-trips
@@ -369,6 +493,28 @@ export interface DashboardHandleContext {
369
493
  active: string;
370
494
  /** MUTABLE — set by the caller's finalize once the render API exists. */
371
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;
372
518
  }
373
519
  /**
374
520
  * Build THE `DashboardHandle` — the one and only implementation, shared by