@grafloria/element 0.4.2 → 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 +1 -1
- package/src/lib/dashboard-kit/dashboard.d.ts +147 -1
- package/src/lib/dashboard-kit/dashboard.js +524 -113
- package/src/lib/dashboard-kit/grid-binder.d.ts +43 -0
- package/src/lib/dashboard-kit/grid-binder.js +719 -106
- package/src/lib/dashboard-kit/index.d.ts +2 -0
- package/src/lib/dashboard-kit/index.js +2 -0
- package/src/lib/dashboard-kit/split-binder.d.ts +69 -0
- package/src/lib/dashboard-kit/split-binder.js +1042 -0
- package/src/lib/dashboard-kit/split-layout.d.ts +128 -0
- package/src/lib/dashboard-kit/split-layout.js +425 -0
- package/src/lib/dashboard-kit/styles.js +137 -20
- package/src/lib/dashboard-kit/widgets.d.ts +16 -1
- package/src/lib/dashboard-kit/widgets.js +146 -33
- package/src/lib/diagram-kit/card.d.ts +46 -0
- package/src/lib/diagram-kit/card.js +149 -0
- package/src/lib/diagram-kit/er.js +5 -3
- package/src/lib/diagram-kit/styles.js +40 -5
- package/src/lib/diagram-kit/uml.js +3 -4
- package/src/lib/diagram-kit/update.js +7 -3
- package/src/lib/grafloria.js +4 -4
- package/src/lib/load.d.ts +5 -0
- package/src/lib/load.js +72 -8
|
@@ -81,6 +81,133 @@ export function entityAutoHeight(entity, editable = false) {
|
|
|
81
81
|
return entity.height;
|
|
82
82
|
return ER_HEAD_H + entity.columns.length * ER_ROW_H + ER_BORDER_SLACK + (editable ? ER_ADD_H : 0);
|
|
83
83
|
}
|
|
84
|
+
/* ---------------------------------------------------------------------------
|
|
85
|
+
* Card WIDTH — the half of the sizing contract that was missing.
|
|
86
|
+
*
|
|
87
|
+
* Height has always been derived from content (entityAutoHeight above), but
|
|
88
|
+
* width was a flat constant, so anything wider than the default simply fell off
|
|
89
|
+
* the card — and every one of those overflows was SILENT. Measured on the
|
|
90
|
+
* shipped demos: the ER head lost 125px of `A_VERY_LONG_TABLE_NAME…` with no
|
|
91
|
+
* ellipsis, `from_warehouse_id` needed 221px against the 190 it got, and a UML
|
|
92
|
+
* method signature needed 487px against 190 — 297px of it cut mid-glyph, on the
|
|
93
|
+
* one line of a class diagram that carries the actual information.
|
|
94
|
+
*
|
|
95
|
+
* So a card now sizes to its content in BOTH axes. The constants below mirror
|
|
96
|
+
* the stylesheet's box model exactly; if a rule there changes, change it here.
|
|
97
|
+
* ------------------------------------------------------------------------- */
|
|
98
|
+
/** Horizontal padding shared by `.axk-row`, `.axk-entity-head`, `.axk-member`
|
|
99
|
+
* and `.axk-uml-name` — all of them are `padding: Npx 10px`. */
|
|
100
|
+
export const CARD_PAD_X = 20;
|
|
101
|
+
/** `.axk-row`'s `gap: 8px`. */
|
|
102
|
+
export const ER_ROW_GAP = 8;
|
|
103
|
+
/** `.axk-key`'s fixed `width: 22px`. */
|
|
104
|
+
export const ER_KEY_W = 22;
|
|
105
|
+
/** `.axk-ty`'s `min-width: 52px` — it never measures narrower than its target. */
|
|
106
|
+
export const ER_TYPE_MIN_W = 52;
|
|
107
|
+
/** `.axk-col-del`'s `width: 14px` (present only when editable). */
|
|
108
|
+
export const CARD_DEL_W = 14;
|
|
109
|
+
/**
|
|
110
|
+
* Horizontal counterpart of ER_BORDER_SLACK: the html wrapper's padding
|
|
111
|
+
* (`html.padding ?? 4`, so 8px across) plus the card's own 1px left/right
|
|
112
|
+
* borders. The node's width is the OUTER box; the row only ever gets what is
|
|
113
|
+
* left after these, so a derived width that ignored them came out 10px short
|
|
114
|
+
* and truncated by a hair — which is exactly how it was first measured.
|
|
115
|
+
*/
|
|
116
|
+
export const CARD_SLACK_X = 10;
|
|
117
|
+
/** Defaults, and the floor every derived width is taken against. */
|
|
118
|
+
export const ER_DEFAULT_WIDTH = 190;
|
|
119
|
+
export const UML_DEFAULT_WIDTH = 200;
|
|
120
|
+
/**
|
|
121
|
+
* Ceiling for a DERIVED width. A card that sized itself to a pathological
|
|
122
|
+
* identifier would dominate the diagram, so past this the stylesheet's ellipsis
|
|
123
|
+
* takes over — visibly, which is the point — and an author who needs the whole
|
|
124
|
+
* string sets an explicit `width`. An explicit width is never clamped.
|
|
125
|
+
*/
|
|
126
|
+
export const CARD_MAX_AUTO_W = 420;
|
|
127
|
+
/* The exact CSS fonts the stylesheet paints these strings with. */
|
|
128
|
+
const FONT_COL = '12px system-ui, sans-serif';
|
|
129
|
+
const FONT_COL_PK = '600 12px system-ui, sans-serif';
|
|
130
|
+
const FONT_TYPE = '11px system-ui, sans-serif';
|
|
131
|
+
const FONT_HEAD = '600 11px system-ui, sans-serif';
|
|
132
|
+
const FONT_ADD = '600 11px system-ui, sans-serif';
|
|
133
|
+
const FONT_MEMBER = '11px ui-monospace, Menlo, monospace';
|
|
134
|
+
const FONT_UML_NAME = '700 12px system-ui, sans-serif';
|
|
135
|
+
const FONT_UML_STEREO = '500 10px system-ui, sans-serif';
|
|
136
|
+
/** `.axk-entity-head` is `letter-spacing: .3px`, which canvas does not apply. */
|
|
137
|
+
const HEAD_TRACKING = 0.3;
|
|
138
|
+
let measureCtx;
|
|
139
|
+
const measureCache = new Map();
|
|
140
|
+
/**
|
|
141
|
+
* Width of `text` in CSS pixels, as the browser will really lay it out.
|
|
142
|
+
*
|
|
143
|
+
* Canvas `measureText` was calibrated against live `getBoundingClientRect()`
|
|
144
|
+
* widths for these exact fonts and agreed to within 0.02px, which is what makes
|
|
145
|
+
* deriving a card's width from its content safe rather than a guess.
|
|
146
|
+
*
|
|
147
|
+
* Outside a browser (jsdom, SSR) there is no 2d context and this falls back to
|
|
148
|
+
* a per-character average. The fallback only has to be roughly right: it feeds
|
|
149
|
+
* a `Math.max` against the default width, and anything it underestimates is
|
|
150
|
+
* ellipsised by the stylesheet rather than lost.
|
|
151
|
+
*/
|
|
152
|
+
export function measureCardText(text, font, tracking = 0) {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
if (!text)
|
|
155
|
+
return 0;
|
|
156
|
+
const key = `${font}\u0000${tracking}\u0000${text}`;
|
|
157
|
+
const hit = measureCache.get(key);
|
|
158
|
+
if (hit !== undefined)
|
|
159
|
+
return hit;
|
|
160
|
+
if (measureCtx === undefined) {
|
|
161
|
+
// Feature-DETECT rather than probe: jsdom defines CanvasRenderingContext2D
|
|
162
|
+
// only when node-canvas is installed, and calling getContext('2d') without
|
|
163
|
+
// it logs a "not implemented" through the virtual console on every suite.
|
|
164
|
+
measureCtx = null;
|
|
165
|
+
if (typeof document !== 'undefined' && typeof CanvasRenderingContext2D !== 'undefined') {
|
|
166
|
+
try {
|
|
167
|
+
measureCtx = document.createElement('canvas').getContext('2d');
|
|
168
|
+
}
|
|
169
|
+
catch (_c) {
|
|
170
|
+
measureCtx = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let width;
|
|
175
|
+
if (measureCtx) {
|
|
176
|
+
measureCtx.font = font;
|
|
177
|
+
width = measureCtx.measureText(text).width + tracking * text.length;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const px = Number.parseFloat((_b = (_a = /(\d+(?:\.\d+)?)px/.exec(font)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '12');
|
|
181
|
+
width = text.length * (px * 0.55 + tracking);
|
|
182
|
+
}
|
|
183
|
+
measureCache.set(key, width);
|
|
184
|
+
return width;
|
|
185
|
+
}
|
|
186
|
+
/** Card width for an entity — explicit width wins, else derived from content. */
|
|
187
|
+
export function entityAutoWidth(entity, editable = false) {
|
|
188
|
+
var _a, _b;
|
|
189
|
+
if (entity.width != null)
|
|
190
|
+
return entity.width;
|
|
191
|
+
// The head is uppercased and tracked by CSS, so measure what is PAINTED
|
|
192
|
+
// rather than what the author typed.
|
|
193
|
+
const title = ((_a = entity.name) !== null && _a !== void 0 ? _a : entity.id).toUpperCase();
|
|
194
|
+
let needed = CARD_PAD_X + measureCardText(title, FONT_HEAD, HEAD_TRACKING);
|
|
195
|
+
for (const c of entity.columns) {
|
|
196
|
+
const name = measureCardText(c.name, c.pk ? FONT_COL_PK : FONT_COL);
|
|
197
|
+
const type = Math.max(ER_TYPE_MIN_W, measureCardText((_b = c.type) !== null && _b !== void 0 ? _b : '', FONT_TYPE));
|
|
198
|
+
needed = Math.max(needed, CARD_PAD_X +
|
|
199
|
+
ER_KEY_W +
|
|
200
|
+
ER_ROW_GAP +
|
|
201
|
+
name +
|
|
202
|
+
ER_ROW_GAP +
|
|
203
|
+
type +
|
|
204
|
+
(editable ? ER_ROW_GAP + CARD_DEL_W : 0));
|
|
205
|
+
}
|
|
206
|
+
if (editable) {
|
|
207
|
+
needed = Math.max(needed, CARD_PAD_X + measureCardText('\uFF0B add column', FONT_ADD));
|
|
208
|
+
}
|
|
209
|
+
return Math.min(CARD_MAX_AUTO_W, Math.max(ER_DEFAULT_WIDTH, Math.ceil(needed + CARD_SLACK_X)));
|
|
210
|
+
}
|
|
84
211
|
/**
|
|
85
212
|
* The `.axk-uml` content tree. `editable` adds a delete control per member and
|
|
86
213
|
* a trailing add affordance in each compartment.
|
|
@@ -152,6 +279,28 @@ export function classAutoHeight(cls, editable = false) {
|
|
|
152
279
|
UML_PAD * 2 +
|
|
153
280
|
12);
|
|
154
281
|
}
|
|
282
|
+
/** Card width for a class — explicit width wins, else derived from content. */
|
|
283
|
+
export function classAutoWidth(cls, editable = false) {
|
|
284
|
+
var _a, _b, _c;
|
|
285
|
+
if (cls.width != null)
|
|
286
|
+
return cls.width;
|
|
287
|
+
// `.axk-member` goes flex only when editable, and it has NO gap — the delete
|
|
288
|
+
// control sits straight against the text.
|
|
289
|
+
const del = editable ? CARD_DEL_W : 0;
|
|
290
|
+
let needed = CARD_PAD_X + measureCardText((_a = cls.name) !== null && _a !== void 0 ? _a : cls.id, FONT_UML_NAME);
|
|
291
|
+
if (cls.stereotype) {
|
|
292
|
+
needed = Math.max(needed, CARD_PAD_X + measureCardText(`\u00AB${cls.stereotype}\u00BB`, FONT_UML_STEREO));
|
|
293
|
+
}
|
|
294
|
+
for (const m of [...((_b = cls.attributes) !== null && _b !== void 0 ? _b : []), ...((_c = cls.methods) !== null && _c !== void 0 ? _c : [])]) {
|
|
295
|
+
needed = Math.max(needed, CARD_PAD_X + measureCardText(m, FONT_MEMBER) + del);
|
|
296
|
+
}
|
|
297
|
+
if (editable) {
|
|
298
|
+
for (const label of ['\uFF0B attribute', '\uFF0B method']) {
|
|
299
|
+
needed = Math.max(needed, CARD_PAD_X + measureCardText(label, FONT_ADD));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return Math.min(CARD_MAX_AUTO_W, Math.max(UML_DEFAULT_WIDTH, Math.ceil(needed + CARD_SLACK_X)));
|
|
303
|
+
}
|
|
155
304
|
/**
|
|
156
305
|
* Match old columns to new columns for port reconciliation. Returns a map from
|
|
157
306
|
* OLD index → NEW index; an old index absent from the map is a REMOVED column
|
|
@@ -22,13 +22,12 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { ensureDiagramKitStyles } from './styles.js';
|
|
24
24
|
import { bindRowInteractions } from './rows.js';
|
|
25
|
-
import { entityCardContent, entityAutoHeight, erRowCenterY, ER_ROW_H, ER_HEAD_H } from './card.js';
|
|
25
|
+
import { entityCardContent, entityAutoHeight, entityAutoWidth, erRowCenterY, ER_ROW_H, ER_HEAD_H, } from './card.js';
|
|
26
26
|
import { bindCardEditing } from './editing.js';
|
|
27
27
|
// Layout constants + the row-centre helper live in card.ts now (the ONE source
|
|
28
28
|
// of truth shared with update.ts). Re-exported here so `import … from './er.js'`
|
|
29
29
|
// keeps working.
|
|
30
30
|
export { erRowCenterY, ER_ROW_H, ER_HEAD_H };
|
|
31
|
-
const DEFAULT_WIDTH = 190;
|
|
32
31
|
const CARDINALITY = {
|
|
33
32
|
'one-to-many': { tail: 'one', head: 'crow-foot' },
|
|
34
33
|
'one-to-one': { tail: 'one', head: 'one' },
|
|
@@ -68,7 +67,10 @@ export function erDiagram(options) {
|
|
|
68
67
|
// How many edges already landed on a given entity row+side — drives the
|
|
69
68
|
// spread (dy) so shared columns (a PK referenced twice) don't stack.
|
|
70
69
|
const rowLandings = new Map();
|
|
71
|
-
|
|
70
|
+
// Width is derived from content the same way height is, so a long column name
|
|
71
|
+
// or table title widens the card instead of falling off it. Ports read this
|
|
72
|
+
// same function, so they stay glued to the edge whatever it works out to.
|
|
73
|
+
const width = (e) => entityAutoWidth(e, options.editable === true);
|
|
72
74
|
const fieldPort = (end, side) => {
|
|
73
75
|
var _a, _b;
|
|
74
76
|
const entity = end.entity;
|
|
@@ -22,19 +22,42 @@ const CSS = `
|
|
|
22
22
|
display: flex; flex-direction: column; }
|
|
23
23
|
.axk-entity-body { flex: 1; min-height: 0; overflow-y: hidden; }
|
|
24
24
|
.axk-entity-body.axk-scroll { overflow-y: auto; scrollbar-width: thin; }
|
|
25
|
+
/* The head is sized by entityAutoWidth, but a title past the auto-width ceiling
|
|
26
|
+
still has to degrade VISIBLY: this rule used to be absent entirely, so a long
|
|
27
|
+
table name ran off the card and was cut by .axk-entity's overflow:hidden with
|
|
28
|
+
nothing to show for it (measured: 305px of name in a 180px head — 125px gone,
|
|
29
|
+
no ellipsis, no hint). nowrap also keeps the head exactly ER_HEAD_H tall,
|
|
30
|
+
which entityAutoHeight's row math depends on. */
|
|
25
31
|
.axk-entity-head { background: #334155; color: #fff; font-weight: 600;
|
|
26
|
-
letter-spacing: .3px; padding: 5px 10px; text-transform: uppercase; font-size: 11px;
|
|
32
|
+
letter-spacing: .3px; padding: 5px 10px; text-transform: uppercase; font-size: 11px;
|
|
33
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
27
34
|
.axk-row { display: flex; align-items: center; gap: 8px; padding: 3px 10px;
|
|
28
35
|
border-top: 1px solid #e2e8f0; }
|
|
29
36
|
.axk-key { width: 22px; font-size: 9px; font-weight: 700; color: #b45309; }
|
|
30
37
|
.axk-key.axk-fk { color: #6d28d9; }
|
|
31
|
-
|
|
38
|
+
/* ONE LINE PER COLUMN, ALWAYS — this rule is load-bearing for the card's HEIGHT.
|
|
39
|
+
entityAutoHeight allocates exactly ER_ROW_H per column, and .axk-entity-body is
|
|
40
|
+
overflow-y:hidden, so a name that wrapped to a second line pushed the last row
|
|
41
|
+
past the card's bottom edge and it silently vanished (measured: a 40px row
|
|
42
|
+
against the 25px the height math had reserved — 14px of the final column gone,
|
|
43
|
+
with no scrollbar to hint at it). Long identifiers ellipsis instead, so the
|
|
44
|
+
ROW always survives even when the text does not fit; an author who needs the
|
|
45
|
+
whole identifier visible sets an explicit width on the entity. (No title
|
|
46
|
+
attribute: the HTML-node contract deliberately passes text through
|
|
47
|
+
textContent and no attributes, and a tooltip is not worth widening it.)
|
|
48
|
+
min-width:0 is what lets a flex child shrink below its content. */
|
|
49
|
+
.axk-col { flex: 1; min-width: 0; color: #0f172a;
|
|
50
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
32
51
|
.axk-ty {
|
|
33
52
|
color: #64748b; font-size: 11px;
|
|
34
53
|
/* A new column starts with an EMPTY type, which collapsed the cell to zero
|
|
35
54
|
width — there was nothing to double-click, so a type could never be set on
|
|
36
55
|
a field you just added. Reserve a target and hint that it is editable. */
|
|
37
56
|
min-width: 52px; text-align: right; cursor: text;
|
|
57
|
+
/* Same contract as .axk-col: a long type must not wrap the row either.
|
|
58
|
+
It keeps its reserved width and never shrinks away. */
|
|
59
|
+
flex: 0 0 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
60
|
+
max-width: 45%;
|
|
38
61
|
}
|
|
39
62
|
.axk-ty:empty::before { content: 'type'; color: #cbd5e1; font-style: italic; }
|
|
40
63
|
.axk-ty:hover { color: #0f172a; }
|
|
@@ -47,14 +70,25 @@ const CSS = `
|
|
|
47
70
|
display: flex; flex-direction: column; }
|
|
48
71
|
.axk-uml-body { flex: 1; min-height: 0; overflow-y: hidden; }
|
|
49
72
|
.axk-uml-body.axk-scroll { overflow-y: auto; scrollbar-width: thin; }
|
|
73
|
+
/* Same one-line contract as .axk-entity-head: UML_NAME_H is what
|
|
74
|
+
classAutoHeight reserves, so a wrapped class name would push the first
|
|
75
|
+
compartment past the card's bottom edge. */
|
|
50
76
|
.axk-uml-name { text-align: center; font-weight: 700; padding: 5px 10px;
|
|
51
|
-
background: #eef2ff; color: #1e1b4b;
|
|
77
|
+
background: #eef2ff; color: #1e1b4b;
|
|
78
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
52
79
|
.axk-uml-name.axk-abstract { font-style: italic; }
|
|
53
80
|
.axk-uml-stereo { display: block; font-size: 10px; font-weight: 500; opacity: .8; }
|
|
54
81
|
.axk-uml-comp { border-top: 1px solid #475569; padding: 3px 0; }
|
|
55
82
|
.axk-uml-comp.axk-empty { min-height: 8px; }
|
|
83
|
+
/* Members already refused to wrap, but with no ellipsis they were cut
|
|
84
|
+
MID-GLYPH by the card's overflow:hidden — a method signature needing 487px
|
|
85
|
+
in a 190px card lost 297px of itself silently, on the one line a class
|
|
86
|
+
diagram exists to show. classAutoWidth now widens the card to fit; past its
|
|
87
|
+
ceiling this ellipsis says so. min-width:0 lets the editable flex variant
|
|
88
|
+
shrink. */
|
|
56
89
|
.axk-member { padding: 1px 10px; font: 11px/1.5 ui-monospace, Menlo, monospace;
|
|
57
|
-
color: #0f172a; white-space: nowrap;
|
|
90
|
+
color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
91
|
+
min-width: 0; }
|
|
58
92
|
|
|
59
93
|
/* ===== Row interactivity (cards are interactive; drag stays geometric) ===== */
|
|
60
94
|
.axk-entity, .axk-uml { user-select: none; -webkit-user-select: none; }
|
|
@@ -84,7 +118,8 @@ g.node-group[data-selected="true"]:has(.axk-uml) rect.diagram-node {
|
|
|
84
118
|
/* Only editable members (which wrap their text in .axk-mtext) go flex — a
|
|
85
119
|
read-only member stays a plain text div, so its golden never shifts. */
|
|
86
120
|
.axk-member:has(.axk-mtext) { display: flex; align-items: center; }
|
|
87
|
-
.axk-member .axk-mtext { flex: 1;
|
|
121
|
+
.axk-member .axk-mtext { flex: 1; min-width: 0;
|
|
122
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
88
123
|
.axk-entity-add, .axk-uml-add { padding: 3px 10px; font-size: 11px; font-weight: 600;
|
|
89
124
|
color: #2563eb; cursor: pointer; border-top: 1px dashed #cbd5e1; user-select: none; }
|
|
90
125
|
.axk-uml-add { color: #4f46e5; border-top: 1px dashed #c7d2fe; text-align: left; }
|
|
@@ -24,9 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { ensureDiagramKitStyles } from './styles.js';
|
|
26
26
|
import { bindRowInteractions } from './rows.js';
|
|
27
|
-
import { classCardContent, classAutoHeight } from './card.js';
|
|
27
|
+
import { classCardContent, classAutoHeight, classAutoWidth } from './card.js';
|
|
28
28
|
import { bindCardEditing } from './editing.js';
|
|
29
|
-
const DEFAULT_WIDTH = 200;
|
|
30
29
|
const STROKE = '#475569';
|
|
31
30
|
const DASH = '6,4';
|
|
32
31
|
/**
|
|
@@ -70,11 +69,11 @@ export function umlDiagram(options) {
|
|
|
70
69
|
ensureDiagramKitStyles();
|
|
71
70
|
const editable = options.editable === true;
|
|
72
71
|
const nodes = options.classes.map((cls, i) => {
|
|
73
|
-
var _a
|
|
72
|
+
var _a;
|
|
74
73
|
return {
|
|
75
74
|
id: cls.id,
|
|
76
75
|
position: (_a = cls.position) !== null && _a !== void 0 ? _a : { x: 80 + (i % 3) * 320, y: 60 + Math.floor(i / 3) * 260 },
|
|
77
|
-
size: { width: (
|
|
76
|
+
size: { width: classAutoWidth(cls, editable), height: classAutoHeight(cls, editable) },
|
|
78
77
|
// interactive: members are real DOM targets (hover, row selection, inline
|
|
79
78
|
// editing); node drag/select stay geometric in the binder.
|
|
80
79
|
metadata: Object.assign({ html: { content: classCardContent(cls, editable), interactive: true }, kitClass: cls, kitEditable: editable }, (options.rowSelection === false ? { kitRowSelection: false } : {})),
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { __awaiter } from "tslib";
|
|
31
31
|
import { Command, PortModel, LinkModel } from '@grafloria/engine';
|
|
32
|
-
import { entityCardContent, entityAutoHeight, classCardContent, classAutoHeight, erRowCenterY, rowIndexFromY, matchColumns, } from './card.js';
|
|
32
|
+
import { entityCardContent, entityAutoHeight, entityAutoWidth, classCardContent, classAutoHeight, classAutoWidth, erRowCenterY, rowIndexFromY, matchColumns, } from './card.js';
|
|
33
33
|
/**
|
|
34
34
|
* The single undoable edit. Captures the card's whole before-state on first
|
|
35
35
|
* execute and restores it on undo, so an edit — however many rows and edges it
|
|
@@ -187,7 +187,11 @@ export function updateEntity(api, entityId, delta) {
|
|
|
187
187
|
newKit: newEntity,
|
|
188
188
|
content: entityCardContent(newEntity, editable),
|
|
189
189
|
height: entityAutoHeight(newEntity, editable),
|
|
190
|
-
|
|
190
|
+
// Derived, not carried over: renaming a column to something longer has to
|
|
191
|
+
// widen the card the same way the builder would have, or the edit lands
|
|
192
|
+
// inside a card still sized for the old text and is truncated on arrival.
|
|
193
|
+
// An explicit delta.width still wins — entityAutoWidth honours it.
|
|
194
|
+
width: entityAutoWidth(newEntity, editable),
|
|
191
195
|
};
|
|
192
196
|
};
|
|
193
197
|
return runUpdate(api, entityId, 'er', build);
|
|
@@ -219,7 +223,7 @@ export function updateClass(api, classId, delta) {
|
|
|
219
223
|
newKit: newClass,
|
|
220
224
|
content: classCardContent(newClass, editable),
|
|
221
225
|
height: classAutoHeight(newClass, editable),
|
|
222
|
-
width: newClass
|
|
226
|
+
width: classAutoWidth(newClass, editable),
|
|
223
227
|
};
|
|
224
228
|
};
|
|
225
229
|
return runUpdate(api, classId, 'uml', build);
|
package/src/lib/grafloria.js
CHANGED
|
@@ -12,7 +12,7 @@ import { registerNodeType, registeredNodeTypes, getNodeType } from './node-type-
|
|
|
12
12
|
* card — `render()` is the embedding surface, not a parser.
|
|
13
13
|
*/
|
|
14
14
|
export function render(spec, target, options = {}) {
|
|
15
|
-
var _a, _b, _c, _d;
|
|
15
|
+
var _a, _b, _c, _d, _e;
|
|
16
16
|
const element = typeof target === 'string'
|
|
17
17
|
? document.querySelector(target)
|
|
18
18
|
: target;
|
|
@@ -22,14 +22,14 @@ export function render(spec, target, options = {}) {
|
|
|
22
22
|
// Kit specs (dashboard()) carry looser node typing than DiagramSpec — both
|
|
23
23
|
// flow into createDiagram's NodeInput[] the same way.
|
|
24
24
|
const parsed = (typeof spec === 'string' ? parseSpec(spec) : spec);
|
|
25
|
-
const instance = createDiagram(element, Object.assign(Object.assign({}, options), { nodes: (
|
|
25
|
+
const instance = createDiagram(element, Object.assign(Object.assign(Object.assign({}, ((_a = parsed.renderOptions) !== null && _a !== void 0 ? _a : {})), options), { nodes: (_b = parsed.nodes) !== null && _b !== void 0 ? _b : [], edges: (_c = parsed.edges) !== null && _c !== void 0 ? _c : [],
|
|
26
26
|
// Wire the global registry in, so `registerNodeType` works for the tiny API
|
|
27
27
|
// exactly as it does for `<grafloria-flow>` — unless the caller supplies their
|
|
28
28
|
// own. A KIT SPEC may also carry its own painter (dashboard() does: every
|
|
29
29
|
// widget is a custom HTML node), and it must be honoured — otherwise the
|
|
30
30
|
// documented one-liner `render(dashboard({…}), host)` mounts a board whose
|
|
31
31
|
// widgets never paint. Precedence: explicit option > spec > registry.
|
|
32
|
-
renderCustomNode: (
|
|
32
|
+
renderCustomNode: (_e = (_d = options.renderCustomNode) !== null && _d !== void 0 ? _d : parsed.renderCustomNode) !== null && _e !== void 0 ? _e : ((node, host) => { var _a; return (_a = getNodeType(node.type)) === null || _a === void 0 ? void 0 : _a(node, host); }) }));
|
|
33
33
|
// Diagram-kit specs (erDiagram/umlDiagram) carry a `finalize(api)` for the
|
|
34
34
|
// post-render wiring that needs the LIVE instance — row interactions,
|
|
35
35
|
// multiplicity chips. Auto-run it so a kit diagram is fully wired from one
|
|
@@ -39,7 +39,7 @@ export function render(spec, target, options = {}) {
|
|
|
39
39
|
try {
|
|
40
40
|
maybeFinalize(instance);
|
|
41
41
|
}
|
|
42
|
-
catch (
|
|
42
|
+
catch (_f) {
|
|
43
43
|
/* a kit's finalize must never break the mount */
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/lib/load.d.ts
CHANGED
|
@@ -56,6 +56,11 @@ export interface LoadedDiagramSpec {
|
|
|
56
56
|
* app's own painter, exactly as `dashboard({ renderWidget })` did.
|
|
57
57
|
*/
|
|
58
58
|
readonly handle: DashboardHandle;
|
|
59
|
+
/** Instance options the loaded spec asks `render()` to apply (a fluid board pins zoom). */
|
|
60
|
+
renderOptions?: {
|
|
61
|
+
minZoom?: number;
|
|
62
|
+
maxZoom?: number;
|
|
63
|
+
};
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
61
66
|
* Turn a saved document back into something `render()` can mount.
|
package/src/lib/load.js
CHANGED
|
@@ -67,6 +67,8 @@ import { bindRowInteractions } from './diagram-kit/rows.js';
|
|
|
67
67
|
import { bindCardEditing } from './diagram-kit/editing.js';
|
|
68
68
|
import { ensureDiagramKitStyles } from './diagram-kit/styles.js';
|
|
69
69
|
import { bindDashboardGrid } from './dashboard-kit/grid-binder.js';
|
|
70
|
+
import { bindDashboardSplit, SPLIT_TREE_KEY } from './dashboard-kit/split-binder.js';
|
|
71
|
+
import { gridItemFromCell } from './dashboard-kit/grid-mapping.js';
|
|
70
72
|
import { cellFromGridItem } from './dashboard-kit/grid-mapping.js';
|
|
71
73
|
import { ensureDashboardKitStyles } from './dashboard-kit/styles.js';
|
|
72
74
|
import { defaultWidgetRenderer } from './dashboard-kit/widgets.js';
|
|
@@ -86,7 +88,9 @@ function widgetSpecOf(node) {
|
|
|
86
88
|
if (kind === undefined)
|
|
87
89
|
return null;
|
|
88
90
|
const title = node.getMetadata('widgetTitle');
|
|
89
|
-
return Object.assign(Object.assign({ id: node.id, kind: kind }, (typeof title === 'string' ? { title } : {})), { data: ((_a = node.getMetadata('widgetSpec')) !== null && _a !== void 0 ? _a : {}), span: node.getMetadata('columnSpan'), rows: node.getMetadata('rowSpan') })
|
|
91
|
+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ id: node.id, kind: kind }, (typeof title === 'string' ? { title } : {})), { data: ((_a = node.getMetadata('widgetSpec')) !== null && _a !== void 0 ? _a : {}), span: node.getMetadata('columnSpan'), rows: node.getMetadata('rowSpan') }), (node.getMetadata('widgetLimits') !== undefined
|
|
92
|
+
? { limits: Object.assign({}, node.getMetadata('widgetLimits')) }
|
|
93
|
+
: {})), (node.getMetadata('widgetMovable') === false ? { movable: false } : {})), (node.getMetadata('widgetResizable') === false ? { resizable: false } : {}));
|
|
90
94
|
}
|
|
91
95
|
/**
|
|
92
96
|
* Turn a saved document back into something `render()` can mount.
|
|
@@ -101,7 +105,7 @@ function widgetSpecOf(node) {
|
|
|
101
105
|
* of either.
|
|
102
106
|
*/
|
|
103
107
|
export function fromDocument(document, options = {}) {
|
|
104
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
108
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
105
109
|
const parsed = typeof document === 'string' ? parseDocument(document) : document;
|
|
106
110
|
const model = new DiagramSerializer().deserialize(parsed);
|
|
107
111
|
const nodes = model.getNodes();
|
|
@@ -198,6 +202,11 @@ export function fromDocument(document, options = {}) {
|
|
|
198
202
|
rowHeight: (_g = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.baseRowHeight) !== null && _g !== void 0 ? _g : 130,
|
|
199
203
|
boardW: (_k = (_j = (_h = viewGroups[0]) === null || _h === void 0 ? void 0 : _h.size) === null || _j === void 0 ? void 0 : _j.width) !== null && _k !== void 0 ? _k : 1180,
|
|
200
204
|
boardH: (_o = (_m = (_l = viewGroups[0]) === null || _l === void 0 ? void 0 : _l.size) === null || _m === void 0 ? void 0 : _m.height) !== null && _o !== void 0 ? _o : 660,
|
|
205
|
+
// A board saved before `mode` existed carries no flag and was authored as
|
|
206
|
+
// a fixed world — it stays one. Fluid is only what was saved fluid.
|
|
207
|
+
mode: (firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.fluid) === true ? 'fluid' : 'fixed',
|
|
208
|
+
overflow: (_p = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.overflow) !== null && _p !== void 0 ? _p : 'bounded',
|
|
209
|
+
layoutOf: new Map(viewGroups.map((g) => { var _a, _b; return [g.id, ((_b = (_a = g.getMetadata('dashboardBoard')) === null || _a === void 0 ? void 0 : _a.layout) !== null && _b !== void 0 ? _b : 'grid')]; })),
|
|
201
210
|
// responsive is NOT in the document (a runtime seam), so it is deliberately
|
|
202
211
|
// absent from the round-trip; width/height/columns/gap/sizing/float/rtl are.
|
|
203
212
|
optionsBase: firstBoard
|
|
@@ -208,11 +217,15 @@ export function fromDocument(document, options = {}) {
|
|
|
208
217
|
sizing: firstBoard.sizing,
|
|
209
218
|
float: firstBoard.float,
|
|
210
219
|
rtl: firstBoard.rtl,
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
mode: firstBoard.fluid === true ? 'fluid' : 'fixed',
|
|
221
|
+
overflow: (_q = firstBoard.overflow) !== null && _q !== void 0 ? _q : 'bounded',
|
|
222
|
+
static: (_r = firstBoard.static) !== null && _r !== void 0 ? _r : false,
|
|
223
|
+
layout: (_s = firstBoard.layout) !== null && _s !== void 0 ? _s : 'grid',
|
|
224
|
+
width: (_u = (_t = viewGroups[0]) === null || _t === void 0 ? void 0 : _t.size) === null || _u === void 0 ? void 0 : _u.width,
|
|
225
|
+
height: (_w = (_v = viewGroups[0]) === null || _v === void 0 ? void 0 : _v.size) === null || _w === void 0 ? void 0 : _w.height,
|
|
213
226
|
}
|
|
214
227
|
: {},
|
|
215
|
-
active: (
|
|
228
|
+
active: (_x = activeGroup === null || activeGroup === void 0 ? void 0 : activeGroup.id) !== null && _x !== void 0 ? _x : 'main',
|
|
216
229
|
apiRef: null,
|
|
217
230
|
};
|
|
218
231
|
const handle = createDashboardHandle(ctx);
|
|
@@ -231,7 +244,7 @@ export function fromDocument(document, options = {}) {
|
|
|
231
244
|
(_b = getNodeType(node.type)) === null || _b === void 0 ? void 0 : _b(node, host);
|
|
232
245
|
};
|
|
233
246
|
const finalize = (api) => {
|
|
234
|
-
var _a, _b, _c, _d;
|
|
247
|
+
var _a, _b, _c, _d, _e;
|
|
235
248
|
const a = api;
|
|
236
249
|
if (!a)
|
|
237
250
|
return;
|
|
@@ -276,10 +289,61 @@ export function fromDocument(document, options = {}) {
|
|
|
276
289
|
const board = group.getMetadata('dashboardBoard');
|
|
277
290
|
if (!board)
|
|
278
291
|
continue;
|
|
279
|
-
boards.set(group.id,
|
|
292
|
+
boards.set(group.id, bindBoard(group, board));
|
|
280
293
|
}
|
|
294
|
+
// LIVE LAYOUT SWITCH on a loaded document — the same contract dashboard()
|
|
295
|
+
// finalize offers: cells persisted where the grid reads them, any tree and
|
|
296
|
+
// column cache cleared, the board's `layout` flag flipped, a fresh binder.
|
|
297
|
+
ctx.rebindView = (viewId, next) => {
|
|
298
|
+
var _a;
|
|
299
|
+
const group = model.getGroup(viewId);
|
|
300
|
+
const b = boards.get(viewId);
|
|
301
|
+
const board = group === null || group === void 0 ? void 0 : group.getMetadata('dashboardBoard');
|
|
302
|
+
if (!group || !b || !board)
|
|
303
|
+
return;
|
|
304
|
+
const cells = b.saveLayout().cells;
|
|
305
|
+
b.dispose();
|
|
306
|
+
model.runSystemWrite(() => {
|
|
307
|
+
var _a;
|
|
308
|
+
for (const [id, cell] of cells) {
|
|
309
|
+
const n = model.getNode(id);
|
|
310
|
+
if (n)
|
|
311
|
+
n.setMetadata('gridItem', gridItemFromCell(cell));
|
|
312
|
+
else
|
|
313
|
+
(_a = model.getGroup(id)) === null || _a === void 0 ? void 0 : _a.setMetadata('gridItem', gridItemFromCell(cell));
|
|
314
|
+
}
|
|
315
|
+
group.setMetadata(SPLIT_TREE_KEY, undefined);
|
|
316
|
+
group.setMetadata('dashboardLayouts', undefined);
|
|
317
|
+
group.setMetadata('dashboardBoard', Object.assign(Object.assign({}, board), { layout: next }));
|
|
318
|
+
});
|
|
319
|
+
ctx.layoutOf.set(viewId, next);
|
|
320
|
+
boards.set(viewId, bindBoard(group, Object.assign(Object.assign({}, board), { layout: next })));
|
|
321
|
+
(_a = boards.get(viewId)) === null || _a === void 0 ? void 0 : _a.sync();
|
|
322
|
+
};
|
|
323
|
+
// A container removed and restored through the history comes back as a
|
|
324
|
+
// fresh group: bind it again from its own persisted geometry.
|
|
325
|
+
ctx.rebindContainer = (id) => {
|
|
326
|
+
const group = model.getGroup(id);
|
|
327
|
+
const board = group === null || group === void 0 ? void 0 : group.getMetadata('dashboardBoard');
|
|
328
|
+
if (!group || !board)
|
|
329
|
+
return;
|
|
330
|
+
ctx.boardGroups.set(id, group);
|
|
331
|
+
boards.set(id, bindBoard(group, board));
|
|
332
|
+
};
|
|
333
|
+
function bindBoard(group, board) {
|
|
334
|
+
return board.layout === 'split'
|
|
335
|
+
? bindDashboardSplit(a, group, Object.assign({}, board))
|
|
336
|
+
: bindDashboardGrid(a, group, Object.assign({}, board));
|
|
337
|
+
}
|
|
338
|
+
// A loaded board follows the history exactly as an authored one does: an
|
|
339
|
+
// undo re-syncs every binder without the consumer calling refresh().
|
|
340
|
+
(_e = ctx.attachHistory) === null || _e === void 0 ? void 0 : _e.call(ctx);
|
|
281
341
|
};
|
|
282
|
-
return { nodes, edges: model.getLinks(), renderCustomNode,
|
|
342
|
+
return Object.assign({ nodes, edges: model.getLinks(), renderCustomNode,
|
|
343
|
+
finalize,
|
|
344
|
+
model,
|
|
345
|
+
boards,
|
|
346
|
+
handle }, (ctx.mode === 'fluid' ? { renderOptions: { minZoom: 1, maxZoom: 1 } } : {}));
|
|
283
347
|
}
|
|
284
348
|
function parseDocument(json) {
|
|
285
349
|
try {
|