@syware/jlib 0.1.3 → 0.3.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.
- package/README.md +3 -17
- package/dist/all.js +5 -4
- package/dist/atoms/jl-icon.css_raw.js +1 -1
- package/dist/atoms/jl-icon.d.ts +7 -1
- package/dist/atoms/jl-icon.js +4 -1
- package/dist/atoms/jl-input.d.ts +1 -0
- package/dist/atoms/jl-input.js +2 -0
- package/dist/atoms/jl-progress-ring.js +1 -6
- package/dist/atoms/jl-select.d.ts +6 -0
- package/dist/atoms/jl-select.js +18 -1
- package/dist/atoms/jl-switch.d.ts +12 -1
- package/dist/atoms/jl-switch.js +11 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -3
- package/dist/lib/calendar.d.ts +15 -1
- package/dist/lib/calendar.js +72 -1
- package/dist/lib/dom.d.ts +2 -0
- package/dist/lib/dom.js +8 -1
- package/dist/lib/flow.d.ts +70 -0
- package/dist/lib/flow.js +353 -0
- package/dist/lib/options.d.ts +5 -0
- package/dist/lib/options.js +13 -1
- package/dist/molecules/jl-accordion-item.css_raw.js +1 -1
- package/dist/molecules/jl-accordion-item.d.ts +6 -1
- package/dist/molecules/jl-accordion-item.js +21 -6
- package/dist/molecules/jl-alert.d.ts +1 -0
- package/dist/molecules/jl-alert.js +3 -1
- package/dist/molecules/jl-date-picker.d.ts +10 -4
- package/dist/molecules/jl-date-picker.js +21 -8
- package/dist/molecules/jl-search.d.ts +3 -1
- package/dist/molecules/jl-search.js +8 -6
- package/dist/molecules/jl-segmented.css_raw.js +1 -1
- package/dist/molecules/jl-segmented.js +14 -7
- package/dist/molecules/jl-stat.css_raw.js +1 -1
- package/dist/molecules/jl-stat.d.ts +6 -0
- package/dist/molecules/jl-stat.js +6 -0
- package/dist/molecules/jl-steps.css_raw.js +1 -1
- package/dist/molecules/jl-timeline.css_raw.js +1 -1
- package/dist/molecules/jl-timeline.d.ts +2 -1
- package/dist/molecules/jl-timeline.js +18 -16
- package/dist/molecules/jl-toast.css_raw.js +1 -1
- package/dist/molecules/jl-toast.d.ts +1 -0
- package/dist/molecules/jl-toast.js +3 -1
- package/dist/organisms/jl-flow.css_raw.js +3 -0
- package/dist/organisms/jl-flow.d.ts +109 -0
- package/dist/organisms/jl-flow.js +755 -0
- package/dist/types/calendar.types.d.ts +2 -0
- package/dist/types/jl-flow.types.d.ts +94 -0
- package/dist/types/jl-segmented.types.d.ts +2 -0
- package/package.json +1 -1
package/dist/lib/flow.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// src/lib/flow.ts
|
|
2
|
+
/**
|
|
3
|
+
* A node's box: fixed, so that every anchor and edge is computed without measuring the page. A node with a detail
|
|
4
|
+
* line is taller.
|
|
5
|
+
*/
|
|
6
|
+
var FLOW_NODE = {
|
|
7
|
+
height: 64,
|
|
8
|
+
heightWithDetail: 96,
|
|
9
|
+
width: 240
|
|
10
|
+
};
|
|
11
|
+
var FLOW_ZOOM = {
|
|
12
|
+
max: 2,
|
|
13
|
+
min: .25,
|
|
14
|
+
step: 1.2
|
|
15
|
+
};
|
|
16
|
+
/** The exit of a node that names none. */
|
|
17
|
+
var DEFAULT_PORT = { id: "out" };
|
|
18
|
+
var PORT_DROP = 12;
|
|
19
|
+
var GRID = 8;
|
|
20
|
+
var STUB = 24;
|
|
21
|
+
var CORNER_RADIUS = 12;
|
|
22
|
+
var FIT_PADDING = 48;
|
|
23
|
+
var REVEAL_MARGIN = 48;
|
|
24
|
+
var ROW_GAP = 72;
|
|
25
|
+
var COLUMN_GAP = 32;
|
|
26
|
+
var portsOf = (node) => node.ports ?? [DEFAULT_PORT];
|
|
27
|
+
var nodeHeight = (node) => node.detail ? FLOW_NODE.heightWithDetail : FLOW_NODE.height;
|
|
28
|
+
/** The exit an edge leaves by: its own, or its node's first. */
|
|
29
|
+
var portOfEdge = (node, edge) => edge.port ?? portsOf(node)[0]?.id ?? "";
|
|
30
|
+
var nodeById = (graph, id) => graph.nodes.find((node) => node.id === id);
|
|
31
|
+
var toGrid = (value) => Math.round(value / GRID) * GRID + 0;
|
|
32
|
+
var snap = (point) => ({
|
|
33
|
+
x: toGrid(point.x),
|
|
34
|
+
y: toGrid(point.y)
|
|
35
|
+
});
|
|
36
|
+
/** Where a node's n-th exit sits along its bottom edge: the edge cut in equal parts, each exit in the middle of one. */
|
|
37
|
+
var portX = (index, count) => FLOW_NODE.width * (index + .5) / count;
|
|
38
|
+
function outputAnchor(node, portId) {
|
|
39
|
+
const ports = portsOf(node);
|
|
40
|
+
const index = ports.findIndex((port) => port.id === portId);
|
|
41
|
+
if (index < 0) return;
|
|
42
|
+
return {
|
|
43
|
+
x: node.position.x + portX(index, ports.length),
|
|
44
|
+
y: node.position.y + nodeHeight(node) + PORT_DROP
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
var inputAnchor = (node) => ({
|
|
48
|
+
x: node.position.x + FLOW_NODE.width / 2,
|
|
49
|
+
y: node.position.y
|
|
50
|
+
});
|
|
51
|
+
var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
52
|
+
/** The point `length` away from `from`, towards `to`. */
|
|
53
|
+
function towards(from, to, length) {
|
|
54
|
+
const span = distance(from, to);
|
|
55
|
+
const ratio = span === 0 ? 0 : length / span;
|
|
56
|
+
return {
|
|
57
|
+
x: from.x + (to.x - from.x) * ratio,
|
|
58
|
+
y: from.y + (to.y - from.y) * ratio
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The corners an edge goes through, in straight runs: down from the exit, across halfway, down into the node. An
|
|
63
|
+
* edge that goes back up turns around the side, clear of the nodes.
|
|
64
|
+
*/
|
|
65
|
+
function edgeRoute(from, to) {
|
|
66
|
+
const forward = to.y - from.y >= 48;
|
|
67
|
+
if (forward && to.x === from.x) return [from, to];
|
|
68
|
+
if (forward) {
|
|
69
|
+
const middle = (from.y + to.y) / 2;
|
|
70
|
+
return [
|
|
71
|
+
from,
|
|
72
|
+
{
|
|
73
|
+
x: from.x,
|
|
74
|
+
y: middle
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
x: to.x,
|
|
78
|
+
y: middle
|
|
79
|
+
},
|
|
80
|
+
to
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
const below = from.y + STUB;
|
|
84
|
+
const above = to.y - STUB;
|
|
85
|
+
const side = Math.abs(to.x - from.x) > FLOW_NODE.width ? (from.x + to.x) / 2 : Math.max(from.x, to.x) + FLOW_NODE.width;
|
|
86
|
+
return [
|
|
87
|
+
from,
|
|
88
|
+
{
|
|
89
|
+
x: from.x,
|
|
90
|
+
y: below
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
x: side,
|
|
94
|
+
y: below
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
x: side,
|
|
98
|
+
y: above
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
x: to.x,
|
|
102
|
+
y: above
|
|
103
|
+
},
|
|
104
|
+
to
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
/** An SVG path through the points, its corners rounded. */
|
|
108
|
+
function roundedPath(points, radius = CORNER_RADIUS) {
|
|
109
|
+
const first = points[0];
|
|
110
|
+
const last = points.at(-1);
|
|
111
|
+
if (!first || !last) return "";
|
|
112
|
+
const corners = points.slice(1, -1).map((corner, index) => {
|
|
113
|
+
const before = points[index] ?? corner;
|
|
114
|
+
const after = points[index + 2] ?? corner;
|
|
115
|
+
const round = Math.min(radius, distance(before, corner) / 2, distance(corner, after) / 2);
|
|
116
|
+
const enter = towards(corner, before, round);
|
|
117
|
+
const leave = towards(corner, after, round);
|
|
118
|
+
return `L ${enter.x} ${enter.y} Q ${corner.x} ${corner.y} ${leave.x} ${leave.y}`;
|
|
119
|
+
});
|
|
120
|
+
return [
|
|
121
|
+
`M ${first.x} ${first.y}`,
|
|
122
|
+
...corners,
|
|
123
|
+
`L ${last.x} ${last.y}`
|
|
124
|
+
].join(" ");
|
|
125
|
+
}
|
|
126
|
+
/** The point halfway along a line through the points. */
|
|
127
|
+
function halfway(points) {
|
|
128
|
+
const segments = points.slice(1).map((end, index) => ({
|
|
129
|
+
end,
|
|
130
|
+
start: points[index] ?? end
|
|
131
|
+
}));
|
|
132
|
+
const walk = { left: segments.reduce((sum, { end, start }) => sum + distance(start, end), 0) / 2 };
|
|
133
|
+
for (const { end, start } of segments) {
|
|
134
|
+
const length = distance(start, end);
|
|
135
|
+
if (walk.left <= length) return towards(start, end, walk.left);
|
|
136
|
+
walk.left -= length;
|
|
137
|
+
}
|
|
138
|
+
return points.at(-1) ?? {
|
|
139
|
+
x: 0,
|
|
140
|
+
y: 0
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
var edgePath = (from, to) => roundedPath(edgeRoute(from, to));
|
|
144
|
+
/** The point halfway along `edgePath(from, to)`. */
|
|
145
|
+
var edgeMidpoint = (from, to) => halfway(edgeRoute(from, to));
|
|
146
|
+
/** Where an edge starts and ends, or nothing when one of its ends is gone. */
|
|
147
|
+
function edgeEnds(graph, edge) {
|
|
148
|
+
const source = nodeById(graph, edge.from);
|
|
149
|
+
const target = nodeById(graph, edge.to);
|
|
150
|
+
const portId = source ? portOfEdge(source, edge) : "";
|
|
151
|
+
const from = source ? outputAnchor(source, portId) : void 0;
|
|
152
|
+
if (!source || !target || !from) return;
|
|
153
|
+
const tone = portsOf(source).find((port) => port.id === portId)?.tone ?? "neutral";
|
|
154
|
+
return {
|
|
155
|
+
from,
|
|
156
|
+
to: inputAnchor(target),
|
|
157
|
+
tone
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
var isInside = (point, node) => point.x >= node.position.x && point.x <= node.position.x + FLOW_NODE.width && point.y >= node.position.y && point.y <= node.position.y + nodeHeight(node);
|
|
161
|
+
/** A run went this way: its node left by the edge's exit, and the node it leads to ran too. */
|
|
162
|
+
function isEdgeTaken(graph, edge) {
|
|
163
|
+
const source = nodeById(graph, edge.from);
|
|
164
|
+
const target = nodeById(graph, edge.to);
|
|
165
|
+
return source?.taken !== void 0 && source.taken === portOfEdge(source, edge) && target?.status !== void 0;
|
|
166
|
+
}
|
|
167
|
+
/** The position of a node centred on a point: where something dropped there lands. */
|
|
168
|
+
var centeredAt = (point) => snap({
|
|
169
|
+
x: point.x - FLOW_NODE.width / 2,
|
|
170
|
+
y: point.y - FLOW_NODE.height / 2
|
|
171
|
+
});
|
|
172
|
+
/** The node under a point, other than `except`. */
|
|
173
|
+
var nodeAt = (graph, point, except = "") => graph.nodes.find((node) => node.id !== except && isInside(point, node));
|
|
174
|
+
var isPortFree = (graph, node, portId) => !graph.edges.some((edge) => edge.from === node.id && portOfEdge(node, edge) === portId);
|
|
175
|
+
/**
|
|
176
|
+
* The graph with one more edge, or nothing when the edge cannot be: a node does not connect to itself, a start takes
|
|
177
|
+
* no incoming edge, and an exit joins a node once.
|
|
178
|
+
*/
|
|
179
|
+
function connect(graph, edge) {
|
|
180
|
+
const source = nodeById(graph, edge.from);
|
|
181
|
+
const target = nodeById(graph, edge.to);
|
|
182
|
+
if (!source || !target || source.id === target.id || target.start === true) return;
|
|
183
|
+
const port = portOfEdge(source, edge);
|
|
184
|
+
const known = portsOf(source).some((candidate) => candidate.id === port);
|
|
185
|
+
const exists = graph.edges.some((candidate) => candidate.from === source.id && candidate.to === target.id && portOfEdge(source, candidate) === port);
|
|
186
|
+
return known && !exists ? {
|
|
187
|
+
...graph,
|
|
188
|
+
edges: [...graph.edges, edge]
|
|
189
|
+
} : void 0;
|
|
190
|
+
}
|
|
191
|
+
var moveNode = (graph, id, position) => ({
|
|
192
|
+
...graph,
|
|
193
|
+
nodes: graph.nodes.map((node) => node.id === id ? {
|
|
194
|
+
...node,
|
|
195
|
+
position
|
|
196
|
+
} : node)
|
|
197
|
+
});
|
|
198
|
+
/** The graph without the node, and without its edges. */
|
|
199
|
+
var removeNode = (graph, id) => ({
|
|
200
|
+
edges: graph.edges.filter((edge) => edge.from !== id && edge.to !== id),
|
|
201
|
+
nodes: graph.nodes.filter((node) => node.id !== id)
|
|
202
|
+
});
|
|
203
|
+
var removeEdge = (graph, id) => ({
|
|
204
|
+
...graph,
|
|
205
|
+
edges: graph.edges.filter((edge) => edge.id !== id)
|
|
206
|
+
});
|
|
207
|
+
var COLUMN = FLOW_NODE.width + COLUMN_GAP;
|
|
208
|
+
var rowOfFlow = (graph) => Math.max(FLOW_NODE.height, ...graph.nodes.map(nodeHeight)) + ROW_GAP;
|
|
209
|
+
/** How far across a node hangs from its parent's exit: the exits share the row out, side by side, around it. */
|
|
210
|
+
function portShift(node, portId) {
|
|
211
|
+
const ports = portsOf(node);
|
|
212
|
+
return (Math.max(0, ports.findIndex((port) => port.id === portId)) - (ports.length - 1) / 2) * COLUMN;
|
|
213
|
+
}
|
|
214
|
+
/** Where a node added after an exit goes: a row below, under its exit, clear of the nodes after the other exits. */
|
|
215
|
+
function positionAfter(graph, from, portId) {
|
|
216
|
+
const source = nodeById(graph, from);
|
|
217
|
+
if (!source) return {
|
|
218
|
+
x: 0,
|
|
219
|
+
y: 0
|
|
220
|
+
};
|
|
221
|
+
return snap({
|
|
222
|
+
x: source.position.x + portShift(source, portId),
|
|
223
|
+
y: source.position.y + nodeHeight(source) + ROW_GAP
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/** The edges that close a loop, found walking from the starts: the layout leaves them out, or a loop has no top. */
|
|
227
|
+
function loopEdges(graph) {
|
|
228
|
+
const loops = /* @__PURE__ */ new Set();
|
|
229
|
+
const done = /* @__PURE__ */ new Set();
|
|
230
|
+
const path = /* @__PURE__ */ new Set();
|
|
231
|
+
const visit = (id) => {
|
|
232
|
+
if (done.has(id)) return;
|
|
233
|
+
path.add(id);
|
|
234
|
+
for (const edge of graph.edges.filter((candidate) => candidate.from === id)) {
|
|
235
|
+
if (path.has(edge.to)) {
|
|
236
|
+
loops.add(edge.id);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
visit(edge.to);
|
|
240
|
+
}
|
|
241
|
+
path.delete(id);
|
|
242
|
+
done.add(id);
|
|
243
|
+
};
|
|
244
|
+
const starts = graph.nodes.filter((node) => node.start === true);
|
|
245
|
+
for (const node of [...starts, ...graph.nodes]) visit(node.id);
|
|
246
|
+
return loops;
|
|
247
|
+
}
|
|
248
|
+
/** Each node's row: one below the lowest node leading to it, so the flow reads from top to bottom. */
|
|
249
|
+
function rowsOf(graph, edges) {
|
|
250
|
+
const rows = new Map(graph.nodes.map((node) => [node.id, 0]));
|
|
251
|
+
const rowOf = (id) => rows.get(id) ?? 0;
|
|
252
|
+
for (const _round of graph.nodes) {
|
|
253
|
+
const deeper = edges.filter((edge) => rowOf(edge.to) <= rowOf(edge.from));
|
|
254
|
+
if (deeper.length === 0) break;
|
|
255
|
+
for (const edge of deeper) rows.set(edge.to, Math.max(rowOf(edge.to), rowOf(edge.from) + 1));
|
|
256
|
+
}
|
|
257
|
+
return rows;
|
|
258
|
+
}
|
|
259
|
+
/** Where a node wants to be across: under its parents, each by the exit it hangs from. */
|
|
260
|
+
function wantedX(graph, edges, id, placed) {
|
|
261
|
+
const parents = edges.flatMap((edge) => {
|
|
262
|
+
const source = nodeById(graph, edge.from);
|
|
263
|
+
const x = placed.get(edge.from);
|
|
264
|
+
return edge.to === id && source && x !== void 0 ? [x + portShift(source, portOfEdge(source, edge))] : [];
|
|
265
|
+
});
|
|
266
|
+
return parents.length === 0 ? 0 : parents.reduce((sum, x) => sum + x, 0) / parents.length;
|
|
267
|
+
}
|
|
268
|
+
/** A row's nodes where they want to be, pushed apart where they would overlap, the row kept centred on them. */
|
|
269
|
+
function placeRow(wanted, placed) {
|
|
270
|
+
const sorted = [...wanted].sort((a, b) => a.x - b.x);
|
|
271
|
+
const pushed = [];
|
|
272
|
+
for (const item of sorted) pushed.push(Math.max(item.x, (pushed.at(-1) ?? Number.NEGATIVE_INFINITY) + COLUMN));
|
|
273
|
+
const drift = pushed.reduce((sum, x, index) => sum + x - (sorted[index]?.x ?? x), 0) / Math.max(1, pushed.length);
|
|
274
|
+
for (const [index, item] of sorted.entries()) placed.set(item.id, (pushed[index] ?? item.x) - drift);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The nodes laid out again, top to bottom in the order they run: the starts and the nodes nothing leads to on the
|
|
278
|
+
* first row, each other node a row below the lowest node leading to it, under the exits it hangs from. Edges that
|
|
279
|
+
* loop back up do not count. Only the positions change.
|
|
280
|
+
*/
|
|
281
|
+
function layoutFlow(graph) {
|
|
282
|
+
const ids = new Set(graph.nodes.map((node) => node.id));
|
|
283
|
+
const loops = loopEdges(graph);
|
|
284
|
+
const edges = graph.edges.filter((edge) => !loops.has(edge.id) && ids.has(edge.from) && ids.has(edge.to));
|
|
285
|
+
const rows = rowsOf(graph, edges);
|
|
286
|
+
const row = rowOfFlow(graph);
|
|
287
|
+
const placed = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const row of [...new Set(rows.values())].sort((a, b) => a - b)) placeRow(graph.nodes.filter((node) => rows.get(node.id) === row).map((node) => ({
|
|
289
|
+
id: node.id,
|
|
290
|
+
x: wantedX(graph, edges, node.id, placed)
|
|
291
|
+
})), placed);
|
|
292
|
+
return {
|
|
293
|
+
...graph,
|
|
294
|
+
nodes: graph.nodes.map((node) => ({
|
|
295
|
+
...node,
|
|
296
|
+
position: snap({
|
|
297
|
+
x: placed.get(node.id) ?? 0,
|
|
298
|
+
y: (rows.get(node.id) ?? 0) * row
|
|
299
|
+
})
|
|
300
|
+
}))
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
var clampZoom = (zoom) => Math.min(FLOW_ZOOM.max, Math.max(FLOW_ZOOM.min, zoom));
|
|
304
|
+
/** Where a point of the canvas (relative to its top left corner) is in the flow's own pixels. */
|
|
305
|
+
var toWorld = (viewport, screen) => ({
|
|
306
|
+
x: (screen.x - viewport.x) / viewport.zoom,
|
|
307
|
+
y: (screen.y - viewport.y) / viewport.zoom
|
|
308
|
+
});
|
|
309
|
+
/** Zooms by `factor`, keeping the point under `screen` where it is: zooming follows the pointer. */
|
|
310
|
+
function zoomAt(viewport, screen, factor) {
|
|
311
|
+
const zoom = clampZoom(viewport.zoom * factor);
|
|
312
|
+
const world = toWorld(viewport, screen);
|
|
313
|
+
return {
|
|
314
|
+
x: screen.x - world.x * zoom,
|
|
315
|
+
y: screen.y - world.y * zoom,
|
|
316
|
+
zoom
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/** The view that shows every node, centred in a canvas of this size, never larger than life. */
|
|
320
|
+
function fitView(nodes, width, height) {
|
|
321
|
+
if (nodes.length === 0) return {
|
|
322
|
+
x: FIT_PADDING,
|
|
323
|
+
y: FIT_PADDING,
|
|
324
|
+
zoom: 1
|
|
325
|
+
};
|
|
326
|
+
const left = Math.min(...nodes.map((node) => node.position.x));
|
|
327
|
+
const top = Math.min(...nodes.map((node) => node.position.y));
|
|
328
|
+
const right = Math.max(...nodes.map((node) => node.position.x + FLOW_NODE.width));
|
|
329
|
+
const bottom = Math.max(...nodes.map((node) => node.position.y + nodeHeight(node) + PORT_DROP));
|
|
330
|
+
const zoom = clampZoom(Math.min(1, (width - 96) / (right - left), (height - 96) / (bottom - top)));
|
|
331
|
+
return {
|
|
332
|
+
x: width / 2 - (left + right) / 2 * zoom,
|
|
333
|
+
y: height / 2 - (top + bottom) / 2 * zoom,
|
|
334
|
+
zoom
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/** How far to move a span so that it fits between the margins of `size`: nothing when it already does. */
|
|
338
|
+
function shiftInto(start, end, size) {
|
|
339
|
+
if (start < REVEAL_MARGIN) return REVEAL_MARGIN - start;
|
|
340
|
+
return end > size - REVEAL_MARGIN ? size - REVEAL_MARGIN - end : 0;
|
|
341
|
+
}
|
|
342
|
+
/** The view moved just enough for a node to be seen whole, its exits included; unchanged when it already is. */
|
|
343
|
+
function revealIn(viewport, node, width, height) {
|
|
344
|
+
const { zoom } = viewport;
|
|
345
|
+
const left = viewport.x + node.position.x * zoom;
|
|
346
|
+
const top = viewport.y + node.position.y * zoom;
|
|
347
|
+
return {
|
|
348
|
+
x: viewport.x + shiftInto(left, left + FLOW_NODE.width * zoom, width),
|
|
349
|
+
y: viewport.y + shiftInto(top, top + (nodeHeight(node) + PORT_DROP) * zoom, height),
|
|
350
|
+
zoom
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export { DEFAULT_PORT, FLOW_NODE, FLOW_ZOOM, centeredAt, connect, edgeEnds, edgeMidpoint, edgePath, edgeRoute, fitView, inputAnchor, isEdgeTaken, isPortFree, layoutFlow, moveNode, nodeAt, nodeHeight, outputAnchor, portOfEdge, portX, portsOf, positionAfter, removeEdge, removeNode, revealIn, roundedPath, snap, toWorld, zoomAt };
|
package/dist/lib/options.d.ts
CHANGED
|
@@ -3,3 +3,8 @@ export declare const TONES: readonly Tone[];
|
|
|
3
3
|
export declare const CONTROL_SIZES: readonly ControlSize[];
|
|
4
4
|
/** The icon that says each tone at a glance: alerts, toasts and callouts use it. */
|
|
5
5
|
export declare const TONE_ICONS: Record<Tone, string>;
|
|
6
|
+
/**
|
|
7
|
+
* Whether a tone's icon reads well filled (`jl-icon filled`): a sign in a circle or a triangle does, the accent's
|
|
8
|
+
* sparkles do not.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TONE_ICON_FILLED: Record<Tone, boolean>;
|
package/dist/lib/options.js
CHANGED
|
@@ -21,4 +21,16 @@ var TONE_ICONS = {
|
|
|
21
21
|
ok: "circle-check",
|
|
22
22
|
warn: "triangle-alert"
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Whether a tone's icon reads well filled (`jl-icon filled`): a sign in a circle or a triangle does, the accent's
|
|
26
|
+
* sparkles do not.
|
|
27
|
+
*/
|
|
28
|
+
var TONE_ICON_FILLED = {
|
|
29
|
+
accent: false,
|
|
30
|
+
danger: true,
|
|
31
|
+
info: true,
|
|
32
|
+
neutral: true,
|
|
33
|
+
ok: true,
|
|
34
|
+
warn: true
|
|
35
|
+
};
|
|
36
|
+
export { CONTROL_SIZES, TONES, TONE_ICONS, TONE_ICON_FILLED };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// src/molecules/jl-accordion-item.css?raw
|
|
2
|
-
var jl_accordion_item_default = ":host {\n display: block;\n border-bottom: 1px solid var(--jl-line);\n /* Lets the content's height animate from 0 to auto. */\n interpolate-size: allow-keywords;\n --pad-x: 0px;\n}\n\nsummary {\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 48px;\n padding: 12px var(--pad-x);\n border-radius: var(--jl-radius-xs);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n font-weight: 600;\n line-height: 1.4;\n list-style: none;\n cursor: pointer;\n user-select: none;\n}\n\nsummary::-webkit-details-marker {\n display: none;\n}\n\n.
|
|
2
|
+
var jl_accordion_item_default = ":host {\n display: block;\n border-bottom: 1px solid var(--jl-line);\n /* Lets the content's height animate from 0 to auto. */\n interpolate-size: allow-keywords;\n --pad-x: 0px;\n}\n\nsummary {\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 48px;\n padding: 12px var(--pad-x);\n border-radius: var(--jl-radius-xs);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n font-weight: 600;\n line-height: 1.4;\n list-style: none;\n cursor: pointer;\n user-select: none;\n}\n\nsummary::-webkit-details-marker {\n display: none;\n}\n\n.titles {\n display: grid;\n flex: 1;\n min-width: 0;\n}\n\n.description {\n overflow: hidden;\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n font-weight: 400;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.icon {\n color: var(--jl-muted);\n}\n\n.chevron {\n color: var(--jl-faint);\n transition: rotate var(--jl-slow) var(--jl-ease);\n}\n\nsummary:hover .chevron {\n color: var(--jl-text);\n}\n\ndetails[open] .chevron {\n rotate: 180deg;\n}\n\nsummary:focus-visible {\n box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--jl-brand) 60%, transparent);\n}\n\n/*\n * The content box is a pseudo-element BASE_STYLES' \"* { box-sizing: inherit }\" does not reach: without this, what\n * it holds (and the slotted fields, which inherit through it) would fall back to content-box.\n */\ndetails::details-content {\n box-sizing: inherit;\n block-size: 0;\n overflow: clip;\n transition:\n block-size var(--jl-slow) var(--jl-ease),\n content-visibility var(--jl-slow) allow-discrete;\n}\n\ndetails[open]::details-content {\n block-size: auto;\n}\n\n.content {\n padding: 0 var(--pad-x) 16px;\n color: var(--jl-text-2);\n font-size: var(--jl-font-md);\n line-height: 1.6;\n}\n\n:host([disabled]) summary {\n color: var(--jl-faint);\n cursor: not-allowed;\n}\n\n:host([disabled]) summary:hover .chevron {\n color: var(--jl-faint);\n}\n";
|
|
3
3
|
export { jl_accordion_item_default as default };
|
|
@@ -12,11 +12,16 @@ export declare class JlAccordionItem extends JlElement {
|
|
|
12
12
|
get(): string;
|
|
13
13
|
set(setter: string | ((prevState: string) => string)): void;
|
|
14
14
|
}>>;
|
|
15
|
+
/** A quieter line under the heading, shown open or closed: what the section holds ("3 fields", "2 pages"). */
|
|
16
|
+
readonly description: Readonly<Readonly<{
|
|
17
|
+
get(): string;
|
|
18
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
19
|
+
}>>;
|
|
15
20
|
readonly open: Readonly<Readonly<{
|
|
16
21
|
get(): boolean;
|
|
17
22
|
set(setter: boolean | ((prevState: boolean) => boolean)): void;
|
|
18
23
|
}>>;
|
|
19
|
-
/** An icon before the heading. */
|
|
24
|
+
/** An icon before the heading; the `icon` slot takes a richer one instead. */
|
|
20
25
|
readonly icon: Readonly<Readonly<{
|
|
21
26
|
get(): string;
|
|
22
27
|
set(setter: string | ((prevState: string) => string)): void;
|
|
@@ -19,8 +19,13 @@ var JlAccordionItem = class extends JlElement {
|
|
|
19
19
|
heading = this.useChange("", (value) => {
|
|
20
20
|
this._refs.heading.textContent = value;
|
|
21
21
|
}, { attribute: "heading" });
|
|
22
|
+
/** A quieter line under the heading, shown open or closed: what the section holds ("3 fields", "2 pages"). */
|
|
23
|
+
description = this.useChange("", (value) => {
|
|
24
|
+
this._refs.description.textContent = value;
|
|
25
|
+
this._refs.description.hidden = value === "";
|
|
26
|
+
}, { attribute: "description" });
|
|
22
27
|
open = this.useChange(false, (value) => this.renderOpen(value), { attribute: "open" });
|
|
23
|
-
/** An icon before the heading. */
|
|
28
|
+
/** An icon before the heading; the `icon` slot takes a richer one instead. */
|
|
24
29
|
icon = this.useChange("", (name) => {
|
|
25
30
|
this._refs.icon.name.set(name);
|
|
26
31
|
this._refs.icon.hidden = name === "";
|
|
@@ -32,6 +37,7 @@ var JlAccordionItem = class extends JlElement {
|
|
|
32
37
|
this._refs.summary.tabIndex = value ? -1 : 0;
|
|
33
38
|
}, { attribute: "disabled" });
|
|
34
39
|
_refs = this.useRefs((ref) => ({
|
|
40
|
+
description: ref(".description"),
|
|
35
41
|
details: ref("details"),
|
|
36
42
|
heading: ref(".heading"),
|
|
37
43
|
icon: ref(".icon"),
|
|
@@ -41,12 +47,21 @@ var JlAccordionItem = class extends JlElement {
|
|
|
41
47
|
return /* @__PURE__ */ jsxs("details", {
|
|
42
48
|
attrs: { part: "base" },
|
|
43
49
|
children: [/* @__PURE__ */ jsxs("summary", { children: [
|
|
44
|
-
/* @__PURE__ */ jsx(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
/* @__PURE__ */ jsx("slot", {
|
|
51
|
+
name: "icon",
|
|
52
|
+
children: /* @__PURE__ */ jsx(JlIcon, {
|
|
53
|
+
class: "icon",
|
|
54
|
+
size: ICON_SIZE,
|
|
55
|
+
hidden: true
|
|
56
|
+
})
|
|
57
|
+
}),
|
|
58
|
+
/* @__PURE__ */ jsxs("span", {
|
|
59
|
+
class: "titles",
|
|
60
|
+
children: [/* @__PURE__ */ jsx("span", { class: "heading" }), /* @__PURE__ */ jsx("span", {
|
|
61
|
+
class: "description",
|
|
62
|
+
hidden: true
|
|
63
|
+
})]
|
|
48
64
|
}),
|
|
49
|
-
/* @__PURE__ */ jsx("span", { class: "heading" }),
|
|
50
65
|
/* @__PURE__ */ jsx(JlIcon, {
|
|
51
66
|
class: "chevron",
|
|
52
67
|
name: "chevron-down",
|
|
@@ -2,7 +2,7 @@ import { BASE_STYLES } from "../styles/shared.js";
|
|
|
2
2
|
import { JlElement } from "../base/jl-element.js";
|
|
3
3
|
import { oneOf } from "../lib/attributes.js";
|
|
4
4
|
import { registerIcons } from "../lib/icons.js";
|
|
5
|
-
import { TONES, TONE_ICONS } from "../lib/options.js";
|
|
5
|
+
import { TONES, TONE_ICONS, TONE_ICON_FILLED } from "../lib/options.js";
|
|
6
6
|
import { hasSlotted } from "../lib/slots.js";
|
|
7
7
|
import { JlIcon } from "../atoms/jl-icon.js";
|
|
8
8
|
import { JlButton } from "../atoms/jl-button.js";
|
|
@@ -119,10 +119,12 @@ var JlAlert = class extends JlElement {
|
|
|
119
119
|
renderActions() {
|
|
120
120
|
this._refs.actions.hidden = !hasSlotted(this._refs.actionsSlot);
|
|
121
121
|
}
|
|
122
|
+
/** The tone's own icon is filled, as a toast's is; an icon of the page's stays as it is drawn. */
|
|
122
123
|
renderIcon() {
|
|
123
124
|
const own = this.icon.get();
|
|
124
125
|
const name = own === "" ? TONE_ICONS[this.tone.get()] : own;
|
|
125
126
|
this._refs.icon.name.set(name === NO_ICON ? "" : name);
|
|
127
|
+
this._refs.icon.filled.set(own === "" && TONE_ICON_FILLED[this.tone.get()]);
|
|
126
128
|
this._refs.icon.hidden = name === NO_ICON;
|
|
127
129
|
this.reflect("has-icon", name !== NO_ICON);
|
|
128
130
|
}
|
|
@@ -2,9 +2,10 @@ import { JlFormControl } from '../base/jl-form-control';
|
|
|
2
2
|
import type { Weekday } from '../types/calendar.types';
|
|
3
3
|
import type { ControlSize } from '../types/options.types';
|
|
4
4
|
/**
|
|
5
|
-
* A field for a date: it shows the date
|
|
6
|
-
* button, Alt + Down or Down. People may also type the date as
|
|
7
|
-
*
|
|
5
|
+
* A field for a date: it shows the date in digits, in the `locale`'s order (`25.10.2026`, `10/25/2026`), and opens a
|
|
6
|
+
* calendar from its button, Alt + Down or Down. People may also type the date: as it shows, with any separators or
|
|
7
|
+
* none (`25102026`), or year first. Phones show their number keys; letters are refused. `value` and the form hold
|
|
8
|
+
* the ISO date.
|
|
8
9
|
*/
|
|
9
10
|
export declare class JlDatePicker extends JlFormControl {
|
|
10
11
|
static readonly selector = "jl-date-picker";
|
|
@@ -16,6 +17,7 @@ export declare class JlDatePicker extends JlFormControl {
|
|
|
16
17
|
get(): string;
|
|
17
18
|
set(setter: string | ((prevState: string) => string)): void;
|
|
18
19
|
}>>;
|
|
20
|
+
/** Without one, the field shows how a date is typed in its locale: `dd.mm.yyyy`. */
|
|
19
21
|
readonly placeholder: Readonly<Readonly<{
|
|
20
22
|
get(): string;
|
|
21
23
|
set(setter: string | ((prevState: string) => string)): void;
|
|
@@ -91,7 +93,10 @@ export declare class JlDatePicker extends JlFormControl {
|
|
|
91
93
|
private onPick;
|
|
92
94
|
private openPopup;
|
|
93
95
|
private closePopup;
|
|
94
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* The typed text: a date in the locale's order (any separators, or none) or year first sets the value, an empty
|
|
98
|
+
* field clears it, anything else goes back to the date there was.
|
|
99
|
+
*/
|
|
95
100
|
private commitText;
|
|
96
101
|
/** Sets the value as people chose it, and says so when it changed. */
|
|
97
102
|
private commit;
|
|
@@ -99,6 +104,7 @@ export declare class JlDatePicker extends JlFormControl {
|
|
|
99
104
|
/** The value when it is a real ISO date, otherwise nothing. */
|
|
100
105
|
private validIso;
|
|
101
106
|
private display;
|
|
107
|
+
private renderPlaceholder;
|
|
102
108
|
private renderValue;
|
|
103
109
|
private renderBound;
|
|
104
110
|
/** Required, and within min and max, as a native date field checks it. */
|
|
@@ -6,7 +6,7 @@ import { registerIcons } from "../lib/icons.js";
|
|
|
6
6
|
import { CONTROL_SIZES } from "../lib/options.js";
|
|
7
7
|
import { setOrRemove, uniqueId } from "../lib/dom.js";
|
|
8
8
|
import { JlIcon } from "../atoms/jl-icon.js";
|
|
9
|
-
import { compareDates, formatDate, parseIsoDate, toIsoDate } from "../lib/calendar.js";
|
|
9
|
+
import { NUMERIC_DATE, compareDates, dateOrder, datePattern, formatDate, isDateText, parseIsoDate, parseTypedDate, toIsoDate } from "../lib/calendar.js";
|
|
10
10
|
import { pageLocale } from "../lib/locale.js";
|
|
11
11
|
import { JlCalendar } from "./jl-calendar.js";
|
|
12
12
|
import jl_date_picker_default from "./jl-date-picker.css_raw.js";
|
|
@@ -28,16 +28,18 @@ var VALUE_MISSING = "Choose a date.";
|
|
|
28
28
|
var DIALOG_LABEL = "Choose a date";
|
|
29
29
|
var parseWeekday = (value) => Math.round(clampedNumber(MONDAY, SUNDAY, SATURDAY)(value));
|
|
30
30
|
/**
|
|
31
|
-
* A field for a date: it shows the date
|
|
32
|
-
* button, Alt + Down or Down. People may also type the date as
|
|
33
|
-
*
|
|
31
|
+
* A field for a date: it shows the date in digits, in the `locale`'s order (`25.10.2026`, `10/25/2026`), and opens a
|
|
32
|
+
* calendar from its button, Alt + Down or Down. People may also type the date: as it shows, with any separators or
|
|
33
|
+
* none (`25102026`), or year first. Phones show their number keys; letters are refused. `value` and the form hold
|
|
34
|
+
* the ISO date.
|
|
34
35
|
*/
|
|
35
36
|
var JlDatePicker = class extends JlFormControl {
|
|
36
37
|
static selector = "jl-date-picker";
|
|
37
38
|
events = this.useEvents();
|
|
38
39
|
/** The date, as ISO (`2026-10-25`); empty for none. */
|
|
39
40
|
value = this.useChange("", () => this.renderValue(), { attribute: "value" });
|
|
40
|
-
|
|
41
|
+
/** Without one, the field shows how a date is typed in its locale: `dd.mm.yyyy`. */
|
|
42
|
+
placeholder = this.useChange("", () => this.renderPlaceholder(), { attribute: "placeholder" });
|
|
41
43
|
/** The field's name for screen readers, when no visible label names it. */
|
|
42
44
|
label = this.useChange("", (value) => setOrRemove(this._refs.native, "aria-label", value), { attribute: "label" });
|
|
43
45
|
/** The first day that can be chosen, as ISO. */
|
|
@@ -47,6 +49,7 @@ var JlDatePicker = class extends JlFormControl {
|
|
|
47
49
|
/** The language the date and the calendar are written in: `fr-CH`. The page's by default. */
|
|
48
50
|
locale = this.useChange("", (value) => {
|
|
49
51
|
this._refs.calendar.locale.set(value);
|
|
52
|
+
this.renderPlaceholder();
|
|
50
53
|
this.renderValue();
|
|
51
54
|
}, { attribute: "locale" });
|
|
52
55
|
/** The day the calendar's weeks start on: 0 for Sunday, 1 for Monday. */
|
|
@@ -106,6 +109,7 @@ var JlDatePicker = class extends JlFormControl {
|
|
|
106
109
|
role: "combobox",
|
|
107
110
|
autocomplete: "off",
|
|
108
111
|
spellcheck: false,
|
|
112
|
+
inputMode: "decimal",
|
|
109
113
|
"aria-haspopup": "dialog",
|
|
110
114
|
"aria-expanded": "false",
|
|
111
115
|
"aria-controls": this._popupId
|
|
@@ -154,6 +158,9 @@ var JlDatePicker = class extends JlFormControl {
|
|
|
154
158
|
event.stopPropagation();
|
|
155
159
|
this.renderClear();
|
|
156
160
|
});
|
|
161
|
+
this.on(native, "beforeinput", (event) => {
|
|
162
|
+
if (event.data !== null && !isDateText(event.data)) event.preventDefault();
|
|
163
|
+
});
|
|
157
164
|
this.on(native, "change", () => this.commitText());
|
|
158
165
|
this.on(native, "keydown", (event) => this.onKey(event));
|
|
159
166
|
this.on(open, "pointerdown", () => {
|
|
@@ -232,7 +239,10 @@ var JlDatePicker = class extends JlFormControl {
|
|
|
232
239
|
closePopup() {
|
|
233
240
|
if (this.isOpen()) this._refs.popup.hidePopover();
|
|
234
241
|
}
|
|
235
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* The typed text: a date in the locale's order (any separators, or none) or year first sets the value, an empty
|
|
244
|
+
* field clears it, anything else goes back to the date there was.
|
|
245
|
+
*/
|
|
236
246
|
commitText() {
|
|
237
247
|
const text = this._refs.native.value.trim();
|
|
238
248
|
if (text === this.display(this.value.get())) return;
|
|
@@ -240,7 +250,7 @@ var JlDatePicker = class extends JlFormControl {
|
|
|
240
250
|
this.commit("");
|
|
241
251
|
return;
|
|
242
252
|
}
|
|
243
|
-
const date =
|
|
253
|
+
const date = parseTypedDate(text, dateOrder(pageLocale(this.locale.get())));
|
|
244
254
|
if (date === null) {
|
|
245
255
|
this.renderValue();
|
|
246
256
|
return;
|
|
@@ -264,7 +274,10 @@ var JlDatePicker = class extends JlFormControl {
|
|
|
264
274
|
}
|
|
265
275
|
display(value) {
|
|
266
276
|
const date = parseIsoDate(value);
|
|
267
|
-
return date === null ? "" : formatDate(date, pageLocale(this.locale.get()));
|
|
277
|
+
return date === null ? "" : formatDate(date, pageLocale(this.locale.get()), NUMERIC_DATE);
|
|
278
|
+
}
|
|
279
|
+
renderPlaceholder() {
|
|
280
|
+
this._refs.native.placeholder = this.placeholder.get() || datePattern(pageLocale(this.locale.get()));
|
|
268
281
|
}
|
|
269
282
|
renderValue() {
|
|
270
283
|
const value = this.validIso();
|
|
@@ -9,6 +9,7 @@ export declare class JlSearch extends JlFormControl {
|
|
|
9
9
|
readonly events: Readonly<import("@jadis/core").UseEventsHandler<{
|
|
10
10
|
input: string;
|
|
11
11
|
search: string;
|
|
12
|
+
enter: string;
|
|
12
13
|
}>>;
|
|
13
14
|
readonly value: Readonly<Readonly<{
|
|
14
15
|
get(): string;
|
|
@@ -52,6 +53,8 @@ export declare class JlSearch extends JlFormControl {
|
|
|
52
53
|
focus(options?: FocusOptions): void;
|
|
53
54
|
/** Selects the whole text, ready to be replaced. */
|
|
54
55
|
select(): void;
|
|
56
|
+
/** Empties the field, as its clear button does: `input` and `search` fire with `''`, and it takes the focus. */
|
|
57
|
+
clear(): void;
|
|
55
58
|
templateHtml(): Node;
|
|
56
59
|
templateCss(): string;
|
|
57
60
|
onConnect(): void;
|
|
@@ -64,7 +67,6 @@ export declare class JlSearch extends JlFormControl {
|
|
|
64
67
|
private onPageKey;
|
|
65
68
|
private scheduleSearch;
|
|
66
69
|
private search;
|
|
67
|
-
private clear;
|
|
68
70
|
private renderValue;
|
|
69
71
|
private renderActions;
|
|
70
72
|
private renderShortcut;
|