@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.
Files changed (50) hide show
  1. package/README.md +3 -17
  2. package/dist/all.js +5 -4
  3. package/dist/atoms/jl-icon.css_raw.js +1 -1
  4. package/dist/atoms/jl-icon.d.ts +7 -1
  5. package/dist/atoms/jl-icon.js +4 -1
  6. package/dist/atoms/jl-input.d.ts +1 -0
  7. package/dist/atoms/jl-input.js +2 -0
  8. package/dist/atoms/jl-progress-ring.js +1 -6
  9. package/dist/atoms/jl-select.d.ts +6 -0
  10. package/dist/atoms/jl-select.js +18 -1
  11. package/dist/atoms/jl-switch.d.ts +12 -1
  12. package/dist/atoms/jl-switch.js +11 -1
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +4 -3
  15. package/dist/lib/calendar.d.ts +15 -1
  16. package/dist/lib/calendar.js +72 -1
  17. package/dist/lib/dom.d.ts +2 -0
  18. package/dist/lib/dom.js +8 -1
  19. package/dist/lib/flow.d.ts +70 -0
  20. package/dist/lib/flow.js +353 -0
  21. package/dist/lib/options.d.ts +5 -0
  22. package/dist/lib/options.js +13 -1
  23. package/dist/molecules/jl-accordion-item.css_raw.js +1 -1
  24. package/dist/molecules/jl-accordion-item.d.ts +6 -1
  25. package/dist/molecules/jl-accordion-item.js +21 -6
  26. package/dist/molecules/jl-alert.d.ts +1 -0
  27. package/dist/molecules/jl-alert.js +3 -1
  28. package/dist/molecules/jl-date-picker.d.ts +10 -4
  29. package/dist/molecules/jl-date-picker.js +21 -8
  30. package/dist/molecules/jl-search.d.ts +3 -1
  31. package/dist/molecules/jl-search.js +8 -6
  32. package/dist/molecules/jl-segmented.css_raw.js +1 -1
  33. package/dist/molecules/jl-segmented.js +14 -7
  34. package/dist/molecules/jl-stat.css_raw.js +1 -1
  35. package/dist/molecules/jl-stat.d.ts +6 -0
  36. package/dist/molecules/jl-stat.js +6 -0
  37. package/dist/molecules/jl-steps.css_raw.js +1 -1
  38. package/dist/molecules/jl-timeline.css_raw.js +1 -1
  39. package/dist/molecules/jl-timeline.d.ts +2 -1
  40. package/dist/molecules/jl-timeline.js +18 -16
  41. package/dist/molecules/jl-toast.css_raw.js +1 -1
  42. package/dist/molecules/jl-toast.d.ts +1 -0
  43. package/dist/molecules/jl-toast.js +3 -1
  44. package/dist/organisms/jl-flow.css_raw.js +3 -0
  45. package/dist/organisms/jl-flow.d.ts +109 -0
  46. package/dist/organisms/jl-flow.js +755 -0
  47. package/dist/types/calendar.types.d.ts +2 -0
  48. package/dist/types/jl-flow.types.d.ts +94 -0
  49. package/dist/types/jl-segmented.types.d.ts +2 -0
  50. package/package.json +1 -1
@@ -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 };
@@ -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>;
@@ -21,4 +21,16 @@ var TONE_ICONS = {
21
21
  ok: "circle-check",
22
22
  warn: "triangle-alert"
23
23
  };
24
- export { CONTROL_SIZES, TONES, TONE_ICONS };
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.heading {\n flex: 1;\n min-width: 0;\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";
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(JlIcon, {
45
- class: "icon",
46
- size: ICON_SIZE,
47
- hidden: true
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",
@@ -34,5 +34,6 @@ export declare class JlAlert extends JlElement {
34
34
  templateCss(): string;
35
35
  onConnect(): void;
36
36
  private renderActions;
37
+ /** The tone's own icon is filled, as a toast's is; an icon of the page's stays as it is drawn. */
37
38
  private renderIcon;
38
39
  }
@@ -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 written for people (in the `locale`) and opens a calendar from its
6
- * button, Alt + Down or Down. People may also type the date as ISO (`2026-10-25`). `value` and the form hold the
7
- * ISO date.
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
- /** The typed text: an ISO date sets the value, an empty field clears it, anything else goes back to the date. */
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 written for people (in the `locale`) and opens a calendar from its
32
- * button, Alt + Down or Down. People may also type the date as ISO (`2026-10-25`). `value` and the form hold the
33
- * ISO date.
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
- placeholder = this.useChange("", (value) => setOrRemove(this._refs.native, "placeholder", value), { attribute: "placeholder" });
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
- /** The typed text: an ISO date sets the value, an empty field clears it, anything else goes back to the date. */
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 = parseIsoDate(text);
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;