@syware/jlib 0.1.0 → 0.2.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.
@@ -0,0 +1,336 @@
1
+ // src/lib/flow.ts
2
+ /** A node's box: fixed, so that every anchor and edge is computed without measuring the page. */
3
+ var FLOW_NODE = {
4
+ height: 64,
5
+ width: 240
6
+ };
7
+ var FLOW_ZOOM = {
8
+ max: 2,
9
+ min: .25,
10
+ step: 1.2
11
+ };
12
+ /** The exit of a node that names none. */
13
+ var DEFAULT_PORT = { id: "out" };
14
+ var PORT_DROP = 12;
15
+ var GRID = 8;
16
+ var STUB = 24;
17
+ var CORNER_RADIUS = 12;
18
+ var FIT_PADDING = 48;
19
+ var REVEAL_MARGIN = 48;
20
+ var ROW_GAP = 72;
21
+ var COLUMN_GAP = 32;
22
+ var portsOf = (node) => node.ports ?? [DEFAULT_PORT];
23
+ /** The exit an edge leaves by: its own, or its node's first. */
24
+ var portOfEdge = (node, edge) => edge.port ?? portsOf(node)[0]?.id ?? "";
25
+ var nodeById = (graph, id) => graph.nodes.find((node) => node.id === id);
26
+ var toGrid = (value) => Math.round(value / GRID) * GRID + 0;
27
+ var snap = (point) => ({
28
+ x: toGrid(point.x),
29
+ y: toGrid(point.y)
30
+ });
31
+ /** 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. */
32
+ var portX = (index, count) => FLOW_NODE.width * (index + .5) / count;
33
+ function outputAnchor(node, portId) {
34
+ const ports = portsOf(node);
35
+ const index = ports.findIndex((port) => port.id === portId);
36
+ if (index < 0) return;
37
+ return {
38
+ x: node.position.x + portX(index, ports.length),
39
+ y: node.position.y + FLOW_NODE.height + PORT_DROP
40
+ };
41
+ }
42
+ var inputAnchor = (node) => ({
43
+ x: node.position.x + FLOW_NODE.width / 2,
44
+ y: node.position.y
45
+ });
46
+ var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
47
+ /** The point `length` away from `from`, towards `to`. */
48
+ function towards(from, to, length) {
49
+ const span = distance(from, to);
50
+ const ratio = span === 0 ? 0 : length / span;
51
+ return {
52
+ x: from.x + (to.x - from.x) * ratio,
53
+ y: from.y + (to.y - from.y) * ratio
54
+ };
55
+ }
56
+ /**
57
+ * The corners an edge goes through, in straight runs: down from the exit, across halfway, down into the node. An
58
+ * edge that goes back up turns around the side, clear of the nodes.
59
+ */
60
+ function edgeRoute(from, to) {
61
+ const forward = to.y - from.y >= 48;
62
+ if (forward && to.x === from.x) return [from, to];
63
+ if (forward) {
64
+ const middle = (from.y + to.y) / 2;
65
+ return [
66
+ from,
67
+ {
68
+ x: from.x,
69
+ y: middle
70
+ },
71
+ {
72
+ x: to.x,
73
+ y: middle
74
+ },
75
+ to
76
+ ];
77
+ }
78
+ const below = from.y + STUB;
79
+ const above = to.y - STUB;
80
+ 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;
81
+ return [
82
+ from,
83
+ {
84
+ x: from.x,
85
+ y: below
86
+ },
87
+ {
88
+ x: side,
89
+ y: below
90
+ },
91
+ {
92
+ x: side,
93
+ y: above
94
+ },
95
+ {
96
+ x: to.x,
97
+ y: above
98
+ },
99
+ to
100
+ ];
101
+ }
102
+ /** An SVG path through the points, its corners rounded. */
103
+ function roundedPath(points, radius = CORNER_RADIUS) {
104
+ const first = points[0];
105
+ const last = points.at(-1);
106
+ if (!first || !last) return "";
107
+ const corners = points.slice(1, -1).map((corner, index) => {
108
+ const before = points[index] ?? corner;
109
+ const after = points[index + 2] ?? corner;
110
+ const round = Math.min(radius, distance(before, corner) / 2, distance(corner, after) / 2);
111
+ const enter = towards(corner, before, round);
112
+ const leave = towards(corner, after, round);
113
+ return `L ${enter.x} ${enter.y} Q ${corner.x} ${corner.y} ${leave.x} ${leave.y}`;
114
+ });
115
+ return [
116
+ `M ${first.x} ${first.y}`,
117
+ ...corners,
118
+ `L ${last.x} ${last.y}`
119
+ ].join(" ");
120
+ }
121
+ /** The point halfway along a line through the points. */
122
+ function halfway(points) {
123
+ const segments = points.slice(1).map((end, index) => ({
124
+ end,
125
+ start: points[index] ?? end
126
+ }));
127
+ const walk = { left: segments.reduce((sum, { end, start }) => sum + distance(start, end), 0) / 2 };
128
+ for (const { end, start } of segments) {
129
+ const length = distance(start, end);
130
+ if (walk.left <= length) return towards(start, end, walk.left);
131
+ walk.left -= length;
132
+ }
133
+ return points.at(-1) ?? {
134
+ x: 0,
135
+ y: 0
136
+ };
137
+ }
138
+ var edgePath = (from, to) => roundedPath(edgeRoute(from, to));
139
+ /** The point halfway along `edgePath(from, to)`. */
140
+ var edgeMidpoint = (from, to) => halfway(edgeRoute(from, to));
141
+ /** Where an edge starts and ends, or nothing when one of its ends is gone. */
142
+ function edgeEnds(graph, edge) {
143
+ const source = nodeById(graph, edge.from);
144
+ const target = nodeById(graph, edge.to);
145
+ const portId = source ? portOfEdge(source, edge) : "";
146
+ const from = source ? outputAnchor(source, portId) : void 0;
147
+ if (!source || !target || !from) return;
148
+ const tone = portsOf(source).find((port) => port.id === portId)?.tone ?? "neutral";
149
+ return {
150
+ from,
151
+ to: inputAnchor(target),
152
+ tone
153
+ };
154
+ }
155
+ 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 + FLOW_NODE.height;
156
+ /** The node under a point, other than `except`. */
157
+ var nodeAt = (graph, point, except = "") => graph.nodes.find((node) => node.id !== except && isInside(point, node));
158
+ var isPortFree = (graph, node, portId) => !graph.edges.some((edge) => edge.from === node.id && portOfEdge(node, edge) === portId);
159
+ /**
160
+ * The graph with one more edge, or nothing when the edge cannot be: a node does not connect to itself, a start takes
161
+ * no incoming edge, and an exit joins a node once.
162
+ */
163
+ function connect(graph, edge) {
164
+ const source = nodeById(graph, edge.from);
165
+ const target = nodeById(graph, edge.to);
166
+ if (!source || !target || source.id === target.id || target.start === true) return;
167
+ const port = portOfEdge(source, edge);
168
+ const known = portsOf(source).some((candidate) => candidate.id === port);
169
+ const exists = graph.edges.some((candidate) => candidate.from === source.id && candidate.to === target.id && portOfEdge(source, candidate) === port);
170
+ return known && !exists ? {
171
+ ...graph,
172
+ edges: [...graph.edges, edge]
173
+ } : void 0;
174
+ }
175
+ var moveNode = (graph, id, position) => ({
176
+ ...graph,
177
+ nodes: graph.nodes.map((node) => node.id === id ? {
178
+ ...node,
179
+ position
180
+ } : node)
181
+ });
182
+ /** The graph without the node, and without its edges. */
183
+ var removeNode = (graph, id) => ({
184
+ edges: graph.edges.filter((edge) => edge.from !== id && edge.to !== id),
185
+ nodes: graph.nodes.filter((node) => node.id !== id)
186
+ });
187
+ var removeEdge = (graph, id) => ({
188
+ ...graph,
189
+ edges: graph.edges.filter((edge) => edge.id !== id)
190
+ });
191
+ var ROW = FLOW_NODE.height + ROW_GAP;
192
+ var COLUMN = FLOW_NODE.width + COLUMN_GAP;
193
+ /** How far across a node hangs from its parent's exit: the exits share the row out, side by side, around it. */
194
+ function portShift(node, portId) {
195
+ const ports = portsOf(node);
196
+ return (Math.max(0, ports.findIndex((port) => port.id === portId)) - (ports.length - 1) / 2) * COLUMN;
197
+ }
198
+ /** Where a node added after an exit goes: a row below, under its exit, clear of the nodes after the other exits. */
199
+ function positionAfter(graph, from, portId) {
200
+ const source = nodeById(graph, from);
201
+ if (!source) return {
202
+ x: 0,
203
+ y: 0
204
+ };
205
+ return snap({
206
+ x: source.position.x + portShift(source, portId),
207
+ y: source.position.y + ROW
208
+ });
209
+ }
210
+ /** The edges that close a loop, found walking from the starts: the layout leaves them out, or a loop has no top. */
211
+ function loopEdges(graph) {
212
+ const loops = /* @__PURE__ */ new Set();
213
+ const done = /* @__PURE__ */ new Set();
214
+ const path = /* @__PURE__ */ new Set();
215
+ const visit = (id) => {
216
+ if (done.has(id)) return;
217
+ path.add(id);
218
+ for (const edge of graph.edges.filter((candidate) => candidate.from === id)) {
219
+ if (path.has(edge.to)) {
220
+ loops.add(edge.id);
221
+ continue;
222
+ }
223
+ visit(edge.to);
224
+ }
225
+ path.delete(id);
226
+ done.add(id);
227
+ };
228
+ const starts = graph.nodes.filter((node) => node.start === true);
229
+ for (const node of [...starts, ...graph.nodes]) visit(node.id);
230
+ return loops;
231
+ }
232
+ /** Each node's row: one below the lowest node leading to it, so the flow reads from top to bottom. */
233
+ function rowsOf(graph, edges) {
234
+ const rows = new Map(graph.nodes.map((node) => [node.id, 0]));
235
+ const rowOf = (id) => rows.get(id) ?? 0;
236
+ for (const _round of graph.nodes) {
237
+ const deeper = edges.filter((edge) => rowOf(edge.to) <= rowOf(edge.from));
238
+ if (deeper.length === 0) break;
239
+ for (const edge of deeper) rows.set(edge.to, Math.max(rowOf(edge.to), rowOf(edge.from) + 1));
240
+ }
241
+ return rows;
242
+ }
243
+ /** Where a node wants to be across: under its parents, each by the exit it hangs from. */
244
+ function wantedX(graph, edges, id, placed) {
245
+ const parents = edges.flatMap((edge) => {
246
+ const source = nodeById(graph, edge.from);
247
+ const x = placed.get(edge.from);
248
+ return edge.to === id && source && x !== void 0 ? [x + portShift(source, portOfEdge(source, edge))] : [];
249
+ });
250
+ return parents.length === 0 ? 0 : parents.reduce((sum, x) => sum + x, 0) / parents.length;
251
+ }
252
+ /** A row's nodes where they want to be, pushed apart where they would overlap, the row kept centred on them. */
253
+ function placeRow(wanted, placed) {
254
+ const sorted = [...wanted].sort((a, b) => a.x - b.x);
255
+ const pushed = [];
256
+ for (const item of sorted) pushed.push(Math.max(item.x, (pushed.at(-1) ?? Number.NEGATIVE_INFINITY) + COLUMN));
257
+ const drift = pushed.reduce((sum, x, index) => sum + x - (sorted[index]?.x ?? x), 0) / Math.max(1, pushed.length);
258
+ for (const [index, item] of sorted.entries()) placed.set(item.id, (pushed[index] ?? item.x) - drift);
259
+ }
260
+ /**
261
+ * The nodes laid out again, top to bottom in the order they run: the starts and the nodes nothing leads to on the
262
+ * first row, each other node a row below the lowest node leading to it, under the exits it hangs from. Edges that
263
+ * loop back up do not count. Only the positions change.
264
+ */
265
+ function layoutFlow(graph) {
266
+ const ids = new Set(graph.nodes.map((node) => node.id));
267
+ const loops = loopEdges(graph);
268
+ const edges = graph.edges.filter((edge) => !loops.has(edge.id) && ids.has(edge.from) && ids.has(edge.to));
269
+ const rows = rowsOf(graph, edges);
270
+ const placed = /* @__PURE__ */ new Map();
271
+ 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) => ({
272
+ id: node.id,
273
+ x: wantedX(graph, edges, node.id, placed)
274
+ })), placed);
275
+ return {
276
+ ...graph,
277
+ nodes: graph.nodes.map((node) => ({
278
+ ...node,
279
+ position: snap({
280
+ x: placed.get(node.id) ?? 0,
281
+ y: (rows.get(node.id) ?? 0) * ROW
282
+ })
283
+ }))
284
+ };
285
+ }
286
+ var clampZoom = (zoom) => Math.min(FLOW_ZOOM.max, Math.max(FLOW_ZOOM.min, zoom));
287
+ /** Where a point of the canvas (relative to its top left corner) is in the flow's own pixels. */
288
+ var toWorld = (viewport, screen) => ({
289
+ x: (screen.x - viewport.x) / viewport.zoom,
290
+ y: (screen.y - viewport.y) / viewport.zoom
291
+ });
292
+ /** Zooms by `factor`, keeping the point under `screen` where it is: zooming follows the pointer. */
293
+ function zoomAt(viewport, screen, factor) {
294
+ const zoom = clampZoom(viewport.zoom * factor);
295
+ const world = toWorld(viewport, screen);
296
+ return {
297
+ x: screen.x - world.x * zoom,
298
+ y: screen.y - world.y * zoom,
299
+ zoom
300
+ };
301
+ }
302
+ /** The view that shows every node, centred in a canvas of this size, never larger than life. */
303
+ function fitView(nodes, width, height) {
304
+ if (nodes.length === 0) return {
305
+ x: FIT_PADDING,
306
+ y: FIT_PADDING,
307
+ zoom: 1
308
+ };
309
+ const left = Math.min(...nodes.map((node) => node.position.x));
310
+ const top = Math.min(...nodes.map((node) => node.position.y));
311
+ const right = Math.max(...nodes.map((node) => node.position.x + FLOW_NODE.width));
312
+ const bottom = Math.max(...nodes.map((node) => node.position.y + FLOW_NODE.height + PORT_DROP));
313
+ const zoom = clampZoom(Math.min(1, (width - 96) / (right - left), (height - 96) / (bottom - top)));
314
+ return {
315
+ x: width / 2 - (left + right) / 2 * zoom,
316
+ y: height / 2 - (top + bottom) / 2 * zoom,
317
+ zoom
318
+ };
319
+ }
320
+ /** How far to move a span so that it fits between the margins of `size`: nothing when it already does. */
321
+ function shiftInto(start, end, size) {
322
+ if (start < REVEAL_MARGIN) return REVEAL_MARGIN - start;
323
+ return end > size - REVEAL_MARGIN ? size - REVEAL_MARGIN - end : 0;
324
+ }
325
+ /** The view moved just enough for a node to be seen whole, its exits included; unchanged when it already is. */
326
+ function revealIn(viewport, node, width, height) {
327
+ const { zoom } = viewport;
328
+ const left = viewport.x + node.position.x * zoom;
329
+ const top = viewport.y + node.position.y * zoom;
330
+ return {
331
+ x: viewport.x + shiftInto(left, left + FLOW_NODE.width * zoom, width),
332
+ y: viewport.y + shiftInto(top, top + (FLOW_NODE.height + PORT_DROP) * zoom, height),
333
+ zoom
334
+ };
335
+ }
336
+ export { DEFAULT_PORT, FLOW_NODE, FLOW_ZOOM, connect, edgeEnds, edgeMidpoint, edgePath, edgeRoute, fitView, inputAnchor, isPortFree, layoutFlow, moveNode, nodeAt, 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 };
@@ -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();
@@ -1,3 +1,3 @@
1
1
  // src/molecules/jl-steps.css?raw
2
- var jl_steps_default = ":host {\n display: block;\n container-type: inline-size;\n --marker: 28px;\n}\n\nol {\n display: flex;\n padding: 0;\n list-style: none;\n}\n\n.step {\n display: flex;\n flex: 1;\n min-width: 0;\n}\n\n.step:last-child {\n flex: none;\n}\n\n.head {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-xs);\n background: none;\n text-align: start;\n}\n\nbutton.head {\n cursor: pointer;\n}\n\n.marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--marker);\n height: var(--marker);\n border: 1px solid var(--jl-line-strong);\n border-radius: 50%;\n background: var(--jl-surface);\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n font-weight: 650;\n font-variant-numeric: tabular-nums;\n}\n\n.text {\n display: grid;\n gap: 2px;\n min-width: 0;\n padding-top: 4px;\n}\n\n.label {\n color: var(--jl-muted);\n font-size: var(--jl-font-md);\n font-weight: 600;\n line-height: 20px;\n}\n\n.description {\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n line-height: 1.45;\n}\n\n.step[data-status=\"done\"] .marker {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent);\n}\n\n.step[data-status=\"done\"] .label {\n color: var(--jl-text);\n}\n\n.step[data-status=\"current\"] .marker {\n border-color: var(--jl-accent);\n background: var(--jl-accent);\n color: var(--jl-on-accent);\n box-shadow: 0 0 0 4px var(--jl-accent-soft);\n}\n\n.step[data-status=\"current\"] .label {\n color: var(--jl-text);\n font-weight: 700;\n}\n\nbutton.head:hover .label {\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n\n.line {\n flex: 1;\n min-width: 16px;\n height: 1px;\n margin: calc(var(--marker) / 2) 12px 0;\n background: var(--jl-line-strong);\n}\n\n.step[data-status=\"done\"] .line {\n background: var(--jl-accent);\n}\n\n.step:last-child .line {\n display: none;\n}\n\n/* Vertical: the markers in a column joined by a rule, the text beside them. */\n:host([orientation=\"vertical\"]) ol {\n flex-direction: column;\n}\n\n:host([orientation=\"vertical\"]) .step {\n position: relative;\n flex: none;\n padding-bottom: 24px;\n}\n\n:host([orientation=\"vertical\"]) .step:last-child {\n padding-bottom: 0;\n}\n\n:host([orientation=\"vertical\"]) .line {\n position: absolute;\n top: calc(var(--marker) + 4px);\n bottom: 4px;\n left: calc(var(--marker) / 2);\n width: 1px;\n height: auto;\n min-width: 0;\n margin: 0;\n}\n\n/* Narrow, the horizontal steps keep their markers and only the current step's text. */\n@container (width < 560px) {\n /* Out of sight, still read: the same rules as .sr-only. */\n :host(:not([orientation=\"vertical\"])) .step:not([data-status=\"current\"]) .text {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n }\n\n :host(:not([orientation=\"vertical\"])) .description {\n display: none;\n }\n\n :host(:not([orientation=\"vertical\"])) .line {\n margin: calc(var(--marker) / 2) 8px 0;\n }\n}\n";
2
+ var jl_steps_default = ":host {\n display: block;\n container-type: inline-size;\n --marker: 28px;\n}\n\nol {\n display: flex;\n padding: 0;\n list-style: none;\n}\n\n.step {\n display: flex;\n flex: 1;\n min-width: 0;\n}\n\n.step:last-child {\n flex: none;\n}\n\n.head {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-xs);\n background: none;\n text-align: start;\n}\n\nbutton.head {\n cursor: pointer;\n}\n\n.marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--marker);\n height: var(--marker);\n border: 1px solid var(--jl-line-strong);\n border-radius: 50%;\n background: var(--jl-surface);\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n font-weight: 650;\n font-variant-numeric: tabular-nums;\n}\n\n.text {\n display: grid;\n gap: 2px;\n min-width: 0;\n padding-top: 4px;\n}\n\n.label {\n color: var(--jl-muted);\n font-size: var(--jl-font-md);\n font-weight: 600;\n line-height: 20px;\n}\n\n.description {\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n line-height: 1.45;\n}\n\n.step[data-status=\"done\"] .marker {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent);\n}\n\n.step[data-status=\"done\"] .label {\n color: var(--jl-text);\n}\n\n.step[data-status=\"current\"] .marker {\n border-color: var(--jl-accent);\n background: var(--jl-accent);\n color: var(--jl-on-accent);\n box-shadow: 0 0 0 4px var(--jl-accent-soft);\n}\n\n.step[data-status=\"current\"] .label {\n color: var(--jl-accent);\n font-weight: 700;\n}\n\nbutton.head:hover .label {\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n\n.line {\n flex: 1;\n min-width: 24px;\n height: 1px;\n /* Further from the label before it than from the next marker: a label reads as its own step's. */\n margin: calc(var(--marker) / 2) 12px 0 20px;\n background: var(--jl-line-strong);\n}\n\n.step[data-status=\"done\"] .line {\n background: var(--jl-accent);\n}\n\n.step:last-child .line {\n display: none;\n}\n\n/* Vertical: the markers in a column joined by a rule, the text beside them. */\n:host([orientation=\"vertical\"]) ol {\n flex-direction: column;\n}\n\n:host([orientation=\"vertical\"]) .step {\n position: relative;\n flex: none;\n padding-bottom: 24px;\n}\n\n:host([orientation=\"vertical\"]) .step:last-child {\n padding-bottom: 0;\n}\n\n:host([orientation=\"vertical\"]) .line {\n position: absolute;\n top: calc(var(--marker) + 4px);\n bottom: 4px;\n left: calc(var(--marker) / 2 - 0.5px);\n width: 1px;\n height: auto;\n min-width: 0;\n margin: 0;\n}\n\n/* Narrow, the horizontal steps keep their markers and only the current step's text. */\n@container (width < 560px) {\n /* Out of sight, still read: the same rules as .sr-only. */\n :host(:not([orientation=\"vertical\"])) .step:not([data-status=\"current\"]) .text {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n }\n\n :host(:not([orientation=\"vertical\"])) .description {\n display: none;\n }\n\n :host(:not([orientation=\"vertical\"])) .line {\n min-width: 8px;\n margin: calc(var(--marker) / 2) 8px 0;\n }\n\n /* The current step takes three shares of the room, the others one, never less than their marker and line: the\n label never runs into the next step, and takes an ellipsis only when there is no room at all. */\n :host(:not([orientation=\"vertical\"])) .step:not([data-status=\"current\"]) {\n min-width: min-content;\n }\n\n :host(:not([orientation=\"vertical\"])) .step[data-status=\"current\"] {\n flex: 3 1 0;\n }\n\n :host(:not([orientation=\"vertical\"])) .step[data-status=\"current\"] .label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n :host(:not([orientation=\"vertical\"])) .step[data-status=\"current\"] .line {\n min-width: 24px;\n margin-left: 16px;\n }\n}\n";
3
3
  export { jl_steps_default as default };
@@ -1,3 +1,3 @@
1
1
  // src/molecules/jl-timeline.css?raw
2
- var jl_timeline_default = ":host {\n display: block;\n --marker: 22px;\n --gap: 20px;\n --title-size: var(--jl-font-md);\n}\n\n:host([compact]) {\n --marker: 18px;\n --gap: 12px;\n --title-size: var(--jl-font-sm);\n}\n\n.timeline {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.event {\n position: relative;\n display: grid;\n grid-template-columns: var(--marker) minmax(0, 1fr);\n gap: 12px;\n padding-bottom: var(--gap);\n --tone: var(--jl-muted);\n --soft: var(--jl-surface-3);\n}\n\n.event:last-child {\n padding-bottom: 0;\n}\n\n.event[data-tone=\"accent\"] {\n --tone: var(--jl-accent);\n --soft: var(--jl-accent-soft);\n}\n\n.event[data-tone=\"ok\"] {\n --tone: var(--jl-ok);\n --soft: var(--jl-ok-soft);\n}\n\n.event[data-tone=\"warn\"] {\n --tone: var(--jl-warn);\n --soft: var(--jl-warn-soft);\n}\n\n.event[data-tone=\"danger\"] {\n --tone: var(--jl-danger);\n --soft: var(--jl-danger-soft);\n}\n\n.event[data-tone=\"info\"] {\n --tone: var(--jl-info);\n --soft: var(--jl-info-soft);\n}\n\n/* The line runs from this marker's centre to the next one's, behind the markers. */\n.event:not(:last-child)::before {\n position: absolute;\n top: 10px;\n bottom: -10px;\n left: calc(var(--marker) / 2 - 0.5px);\n width: 1px;\n background: var(--jl-line-strong);\n content: \"\";\n}\n\n.rail {\n position: relative;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 20px;\n}\n\n.marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n}\n\n.dot {\n width: 10px;\n height: 10px;\n background: var(--tone);\n box-shadow: 0 0 0 3px var(--jl-surface);\n}\n\n:host([compact]) .dot {\n width: 8px;\n height: 8px;\n}\n\n.circle {\n width: var(--marker);\n height: var(--marker);\n border: 1px solid color-mix(in srgb, var(--tone) 30%, var(--soft));\n background: var(--soft);\n color: var(--tone);\n box-shadow: 0 0 0 3px var(--jl-surface);\n}\n\n.content {\n display: grid;\n gap: 2px;\n}\n\n.line {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n gap: 2px 12px;\n}\n\n.title {\n color: var(--jl-text);\n font-size: var(--title-size);\n font-weight: 600;\n line-height: 20px;\n}\n\n.time {\n flex: none;\n margin-left: auto;\n color: var(--jl-muted);\n font-family: var(--jl-font-mono);\n font-size: var(--jl-font-xs);\n}\n\n.description {\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n line-height: 1.5;\n}\n\n:host([compact]) .description {\n font-size: var(--jl-font-xs);\n}\n";
2
+ var jl_timeline_default = ":host {\n display: block;\n --marker: 22px;\n --gap: 20px;\n --title-size: var(--jl-font-md);\n}\n\n:host([compact]) {\n --marker: 18px;\n --gap: 12px;\n --title-size: var(--jl-font-sm);\n}\n\n.timeline {\n /* The markers (and the room after them), the times and the texts in three columns shared by every event: the times\n line up beside their markers, each next to its title, however wide the timeline. Without any time, the middle\n column takes no room. */\n display: grid;\n grid-template-columns: calc(var(--marker) + 12px) auto minmax(0, 1fr);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.event {\n position: relative;\n display: grid;\n grid-column: 1 / -1;\n /* Browsers without subgrid keep the columns per event. */\n grid-template-columns: calc(var(--marker) + 12px) auto minmax(0, 1fr);\n padding-bottom: var(--gap);\n --tone: var(--jl-muted);\n --soft: var(--jl-surface-3);\n}\n\n@supports (grid-template-columns: subgrid) {\n .event {\n grid-template-columns: subgrid;\n }\n}\n\n.event:last-child {\n padding-bottom: 0;\n}\n\n.event[data-tone=\"accent\"] {\n --tone: var(--jl-accent);\n --soft: var(--jl-accent-soft);\n}\n\n.event[data-tone=\"ok\"] {\n --tone: var(--jl-ok);\n --soft: var(--jl-ok-soft);\n}\n\n.event[data-tone=\"warn\"] {\n --tone: var(--jl-warn);\n --soft: var(--jl-warn-soft);\n}\n\n.event[data-tone=\"danger\"] {\n --tone: var(--jl-danger);\n --soft: var(--jl-danger-soft);\n}\n\n.event[data-tone=\"info\"] {\n --tone: var(--jl-info);\n --soft: var(--jl-info-soft);\n}\n\n/* The line runs from this marker's centre to the next one's, behind the markers. */\n.event:not(:last-child)::before {\n position: absolute;\n top: 10px;\n bottom: -10px;\n left: calc(var(--marker) / 2 - 0.5px);\n width: 1px;\n background: var(--jl-line-strong);\n content: \"\";\n}\n\n.rail {\n position: relative;\n grid-column: 1;\n width: var(--marker);\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 20px;\n}\n\n.marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n}\n\n.dot {\n width: 10px;\n height: 10px;\n background: var(--tone);\n box-shadow: 0 0 0 3px var(--jl-surface);\n}\n\n:host([compact]) .dot {\n width: 8px;\n height: 8px;\n}\n\n.circle {\n width: var(--marker);\n height: var(--marker);\n border: 1px solid color-mix(in srgb, var(--tone) 30%, var(--soft));\n background: var(--soft);\n color: var(--tone);\n box-shadow: 0 0 0 3px var(--jl-surface);\n}\n\n.content {\n display: grid;\n grid-column: 3;\n gap: 2px;\n}\n\n.title {\n color: var(--jl-text);\n font-size: var(--title-size);\n font-weight: 600;\n line-height: 20px;\n}\n\n.time {\n grid-column: 2;\n padding-right: 12px;\n color: var(--jl-muted);\n font-family: var(--jl-font-mono);\n font-size: var(--jl-font-xs);\n /* On the title's line. */\n line-height: 20px;\n white-space: nowrap;\n}\n\n.description {\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n line-height: 1.5;\n}\n\n:host([compact]) .description {\n font-size: var(--jl-font-xs);\n}\n";
3
3
  export { jl_timeline_default as default };
@@ -2,7 +2,8 @@ import { JlElement } from '../base/jl-element';
2
2
  import type { TimelineItem } from '../types/jl-timeline.types';
3
3
  /**
4
4
  * What happened, in order, along a vertical line: each event a dot (or an icon in a circle) in its tone's colour,
5
- * a title, an optional description, and its time on the right. `compact` tightens it for side panels.
5
+ * its time beside the marker (the times in one column), a title and an optional description. `compact` tightens it
6
+ * for side panels.
6
7
  */
7
8
  export declare class JlTimeline extends JlElement {
8
9
  static readonly selector = "jl-timeline";
@@ -11,7 +11,8 @@ var ICON_SIZE = 14;
11
11
  var COMPACT_ICON_SIZE = 12;
12
12
  /**
13
13
  * What happened, in order, along a vertical line: each event a dot (or an icon in a circle) in its tone's colour,
14
- * a title, an optional description, and its time on the right. `compact` tightens it for side panels.
14
+ * its time beside the marker (the times in one column), a title and an optional description. `compact` tightens it
15
+ * for side panels.
15
16
  */
16
17
  var JlTimeline = class extends JlElement {
17
18
  static selector = "jl-timeline";
@@ -55,25 +56,26 @@ var JlTimeline = class extends JlElement {
55
56
  return /* @__PURE__ */ jsxs("li", {
56
57
  class: "event",
57
58
  "data-tone": item.tone ?? "neutral",
58
- children: [/* @__PURE__ */ jsx("span", {
59
- class: "rail",
60
- children: marker
61
- }), /* @__PURE__ */ jsxs("div", {
62
- class: "content",
63
- children: [/* @__PURE__ */ jsxs("div", {
64
- class: "line",
59
+ children: [
60
+ /* @__PURE__ */ jsx("span", {
61
+ class: "rail",
62
+ children: marker
63
+ }),
64
+ item.time === void 0 ? null : /* @__PURE__ */ jsx("time", {
65
+ class: "time",
66
+ children: item.time
67
+ }),
68
+ /* @__PURE__ */ jsxs("div", {
69
+ class: "content",
65
70
  children: [/* @__PURE__ */ jsx("p", {
66
71
  class: "title",
67
72
  children: item.title
68
- }), item.time === void 0 ? null : /* @__PURE__ */ jsx("time", {
69
- class: "time",
70
- children: item.time
73
+ }), item.description === void 0 ? null : /* @__PURE__ */ jsx("p", {
74
+ class: "description",
75
+ children: item.description
71
76
  })]
72
- }), item.description === void 0 ? null : /* @__PURE__ */ jsx("p", {
73
- class: "description",
74
- children: item.description
75
- })]
76
- })]
77
+ })
78
+ ]
77
79
  });
78
80
  }
79
81
  };
@@ -1,3 +1,3 @@
1
1
  // src/molecules/jl-toast.css?raw
2
- var jl_toast_default = ":host {\n display: block;\n --tone: var(--jl-text-2);\n --bar: var(--jl-brand);\n}\n\n:host([tone=\"ok\"]) {\n --tone: var(--jl-ok);\n --bar: var(--jl-ok);\n}\n\n:host([tone=\"warn\"]) {\n --tone: var(--jl-warn);\n --bar: var(--jl-warn);\n}\n\n:host([tone=\"danger\"]) {\n --tone: var(--jl-danger);\n --bar: var(--jl-danger);\n}\n\n:host([tone=\"info\"]) {\n --tone: var(--jl-info);\n --bar: var(--jl-info);\n}\n\n.toast {\n position: relative;\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 14px 12px 14px 16px;\n overflow: hidden;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-lg);\n background: var(--jl-surface);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n line-height: 20px;\n box-shadow: var(--jl-shadow-lg);\n}\n\n.icon {\n margin-top: 1px;\n color: var(--tone);\n}\n\n.content {\n display: grid;\n flex: 1;\n gap: 2px;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.heading {\n font-weight: 650;\n}\n\n.text {\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n}\n\n.heading[hidden] + .text {\n color: var(--jl-text-2);\n font-size: var(--jl-font-md);\n}\n\n.buttons {\n display: flex;\n flex: none;\n align-items: center;\n gap: 2px;\n margin: -5px -4px -5px 0;\n}\n\n.buttons:not(:has(jl-button:not([hidden]))) {\n display: none;\n}\n\n.close {\n --jl-accent: var(--jl-muted);\n --jl-accent-soft: var(--jl-surface-3);\n}\n\n.progress {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n height: 2px;\n}\n\n.bar {\n height: 100%;\n background: var(--bar);\n transform-origin: left center;\n}\n\n:host([paused]) .bar {\n opacity: 0.55;\n}\n\n/* The line still counts the time, but does not move for people who asked for less motion. */\n@media (prefers-reduced-motion: reduce) {\n .progress {\n visibility: hidden;\n }\n}\n";
2
+ var jl_toast_default = ":host {\n display: block;\n /* Coloured as jl-alert is, from the same tokens: a toned toast is tinted, its border and icon in its tone. */\n --fg: var(--jl-text-2);\n --bg: var(--jl-surface);\n --border: var(--jl-line);\n --bar: var(--jl-brand);\n}\n\n:host([tone=\"ok\"]),\n:host([tone=\"warn\"]),\n:host([tone=\"danger\"]),\n:host([tone=\"info\"]) {\n --border: color-mix(in srgb, var(--fg) 24%, var(--bg));\n --bar: var(--fg);\n}\n\n:host([tone=\"ok\"]) {\n --fg: var(--jl-ok);\n --bg: var(--jl-ok-soft);\n}\n\n:host([tone=\"warn\"]) {\n --fg: var(--jl-warn);\n --bg: var(--jl-warn-soft);\n}\n\n:host([tone=\"danger\"]) {\n --fg: var(--jl-danger);\n --bg: var(--jl-danger-soft);\n}\n\n:host([tone=\"info\"]) {\n --fg: var(--jl-info);\n --bg: var(--jl-info-soft);\n}\n\n.toast {\n position: relative;\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 14px 12px 14px 16px;\n overflow: hidden;\n border: 1px solid var(--border);\n border-radius: var(--jl-radius-lg);\n background: var(--bg);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n line-height: 20px;\n box-shadow: var(--jl-shadow-lg);\n}\n\n.icon {\n margin-top: 1px;\n color: var(--fg);\n}\n\n.content {\n display: grid;\n flex: 1;\n gap: 2px;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.heading {\n font-weight: 650;\n}\n\n.text {\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n}\n\n.heading[hidden] + .text {\n color: var(--jl-text-2);\n font-size: var(--jl-font-md);\n}\n\n.buttons {\n display: flex;\n flex: none;\n align-items: center;\n gap: 2px;\n margin: -5px -4px -5px 0;\n}\n\n.buttons:not(:has(jl-button:not([hidden]))) {\n display: none;\n}\n\n.close {\n --jl-accent: var(--fg);\n --jl-accent-soft: color-mix(in srgb, var(--fg) 12%, transparent);\n}\n\n.progress {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n height: 2px;\n}\n\n.bar {\n height: 100%;\n background: var(--bar);\n transform-origin: left center;\n}\n\n:host([paused]) .bar {\n opacity: 0.55;\n}\n\n/* The line still counts the time, but does not move for people who asked for less motion. */\n@media (prefers-reduced-motion: reduce) {\n .progress {\n visibility: hidden;\n }\n}\n";
3
3
  export { jl_toast_default as default };
@@ -69,6 +69,7 @@ export declare class JlToast extends JlElement {
69
69
  private hold;
70
70
  /** Whether the focus, moving to `next`, stays in the toast (its buttons, or what the page slotted in it). */
71
71
  private holdsFocus;
72
+ /** The tone's own icon is filled, so its colour shows at a glance; an icon of the page's stays as it is drawn. */
72
73
  private renderIcon;
73
74
  /** Inside a toaster, its live region announces the toast; alone, the toast is its own. */
74
75
  private renderRole;