@syware/jlib 0.6.0 → 0.7.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 (114) hide show
  1. package/dist/all.js +9 -4
  2. package/dist/atoms/jl-app-icon.d.ts +5 -0
  3. package/dist/atoms/jl-app-icon.js +26 -4
  4. package/dist/atoms/jl-badge.js +1 -1
  5. package/dist/atoms/jl-button.d.ts +5 -0
  6. package/dist/atoms/jl-button.js +4 -1
  7. package/dist/atoms/jl-chip.js +3 -2
  8. package/dist/atoms/jl-input.js +4 -3
  9. package/dist/atoms/jl-link.js +1 -1
  10. package/dist/atoms/jl-rating.js +4 -5
  11. package/dist/atoms/jl-select.css_raw.js +1 -1
  12. package/dist/atoms/jl-select.d.ts +3 -2
  13. package/dist/atoms/jl-select.js +5 -4
  14. package/dist/atoms/jl-spinner.js +2 -2
  15. package/dist/base/jl-form-control.js +2 -1
  16. package/dist/index.d.ts +8 -0
  17. package/dist/index.js +8 -3
  18. package/dist/lib/app-icon.d.ts +8 -0
  19. package/dist/lib/app-icon.js +37 -0
  20. package/dist/lib/dom.d.ts +2 -0
  21. package/dist/lib/dom.js +3 -1
  22. package/dist/lib/download.d.ts +10 -0
  23. package/dist/lib/download.js +62 -0
  24. package/dist/lib/icons.d.ts +4 -0
  25. package/dist/lib/icons.js +7 -4
  26. package/dist/lib/input-list.d.ts +12 -0
  27. package/dist/lib/input-list.js +14 -0
  28. package/dist/lib/internals.d.ts +5 -0
  29. package/dist/lib/internals.js +18 -0
  30. package/dist/lib/palette.js +6 -0
  31. package/dist/lib/resize.d.ts +2 -0
  32. package/dist/lib/resize.js +10 -0
  33. package/dist/lib/table.d.ts +6 -1
  34. package/dist/lib/table.js +16 -1
  35. package/dist/lib/text.d.ts +2 -0
  36. package/dist/lib/text.js +4 -1
  37. package/dist/lib/texts.d.ts +97 -0
  38. package/dist/lib/texts.js +102 -0
  39. package/dist/lib/tree.d.ts +10 -1
  40. package/dist/lib/tree.js +37 -3
  41. package/dist/molecules/jl-account-button.js +1 -1
  42. package/dist/molecules/jl-alert.js +2 -1
  43. package/dist/molecules/jl-breadcrumb.js +3 -3
  44. package/dist/molecules/jl-calendar.js +4 -4
  45. package/dist/molecules/jl-chip-group.js +2 -2
  46. package/dist/molecules/jl-code-block.css_raw.js +1 -1
  47. package/dist/molecules/jl-code-block.d.ts +5 -0
  48. package/dist/molecules/jl-code-block.js +5 -2
  49. package/dist/molecules/jl-combobox.js +5 -6
  50. package/dist/molecules/jl-copy-button.js +5 -4
  51. package/dist/molecules/jl-date-picker.js +6 -7
  52. package/dist/molecules/jl-empty.js +1 -1
  53. package/dist/molecules/jl-facts.css_raw.js +3 -0
  54. package/dist/molecules/jl-facts.d.ts +35 -0
  55. package/dist/molecules/jl-facts.js +64 -0
  56. package/dist/molecules/jl-file-drop.d.ts +5 -0
  57. package/dist/molecules/jl-file-drop.js +17 -14
  58. package/dist/molecules/jl-input-list.css_raw.js +3 -0
  59. package/dist/molecules/jl-input-list.d.ts +79 -0
  60. package/dist/molecules/jl-input-list.js +177 -0
  61. package/dist/molecules/jl-load-more.css_raw.js +3 -0
  62. package/dist/molecules/jl-load-more.d.ts +41 -0
  63. package/dist/molecules/jl-load-more.js +62 -0
  64. package/dist/molecules/jl-load-state.js +6 -7
  65. package/dist/molecules/jl-number-input.js +4 -4
  66. package/dist/molecules/jl-pagination.js +5 -5
  67. package/dist/molecules/jl-radio-group.js +2 -2
  68. package/dist/molecules/jl-search-button.js +4 -4
  69. package/dist/molecules/jl-search.js +4 -4
  70. package/dist/molecules/jl-segmented.js +2 -1
  71. package/dist/molecules/jl-steps.css_raw.js +1 -1
  72. package/dist/molecules/jl-steps.d.ts +2 -0
  73. package/dist/molecules/jl-steps.js +30 -14
  74. package/dist/molecules/jl-tabs.js +2 -1
  75. package/dist/molecules/jl-tag-input.js +2 -2
  76. package/dist/molecules/jl-toast.css_raw.js +1 -1
  77. package/dist/molecules/jl-toast.d.ts +13 -1
  78. package/dist/molecules/jl-toast.js +23 -3
  79. package/dist/organisms/jl-command-palette.css_raw.js +1 -1
  80. package/dist/organisms/jl-command-palette.d.ts +14 -0
  81. package/dist/organisms/jl-command-palette.js +39 -9
  82. package/dist/organisms/jl-confirm.js +5 -7
  83. package/dist/organisms/jl-dialog.js +2 -1
  84. package/dist/organisms/jl-drawer.js +2 -1
  85. package/dist/organisms/jl-flow.js +12 -12
  86. package/dist/organisms/jl-navbar.js +6 -6
  87. package/dist/organisms/jl-side-nav.css_raw.js +1 -1
  88. package/dist/organisms/jl-side-nav.d.ts +33 -1
  89. package/dist/organisms/jl-side-nav.js +70 -13
  90. package/dist/organisms/jl-tab-bar.js +2 -2
  91. package/dist/organisms/jl-table.css_raw.js +1 -1
  92. package/dist/organisms/jl-table.d.ts +9 -0
  93. package/dist/organisms/jl-table.js +35 -5
  94. package/dist/organisms/jl-toaster.d.ts +9 -1
  95. package/dist/organisms/jl-toaster.js +148 -7
  96. package/dist/organisms/jl-tree.css_raw.js +1 -1
  97. package/dist/organisms/jl-tree.d.ts +71 -4
  98. package/dist/organisms/jl-tree.js +303 -40
  99. package/dist/templates/jl-app-shell.css_raw.js +1 -1
  100. package/dist/templates/jl-app-shell.d.ts +10 -0
  101. package/dist/templates/jl-app-shell.js +16 -2
  102. package/dist/templates/jl-auth-layout.js +1 -1
  103. package/dist/templates/jl-page.js +3 -3
  104. package/dist/templates/jl-split-view.js +5 -4
  105. package/dist/types/jl-app-icon.types.d.ts +5 -0
  106. package/dist/types/jl-command-palette.types.d.ts +7 -0
  107. package/dist/types/jl-facts.types.d.ts +17 -0
  108. package/dist/types/jl-side-nav.types.d.ts +2 -0
  109. package/dist/types/jl-steps.types.d.ts +2 -0
  110. package/dist/types/jl-table.types.d.ts +9 -2
  111. package/dist/types/jl-toaster.types.d.ts +42 -1
  112. package/dist/types/jl-tree.types.d.ts +26 -0
  113. package/dist/types/texts.types.d.ts +5 -0
  114. package/package.json +3 -3
@@ -2,19 +2,28 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
+ import { jlText } from "../lib/texts.js";
5
6
  import { JlIcon } from "../atoms/jl-icon.js";
6
7
  import { setOrRemove, uniqueId } from "../lib/dom.js";
8
+ import { JlButton } from "../atoms/jl-button.js";
7
9
  import { findItem, firstLeaf, pathInSections } from "../lib/nav-tree.js";
8
10
  import jl_side_nav_default from "./jl-side-nav.css_raw.js";
9
11
  import { css } from "@jadis/core";
10
- import { ChevronRight } from "lucide";
12
+ import { ChevronRight, PanelLeftClose, PanelLeftOpen } from "lucide";
11
13
  import { jsx, jsxs } from "@jadis/core/jsx-runtime";
12
14
  // src/organisms/jl-side-nav.tsx
13
- registerIcons({ "chevron-right": ChevronRight });
15
+ registerIcons({
16
+ "chevron-right": ChevronRight,
17
+ "panel-left-close": PanelLeftClose,
18
+ "panel-left-open": PanelLeftOpen
19
+ });
14
20
  var ICON_SIZE = 16;
15
21
  var CHEVRON_SIZE = 14;
16
- var DEFAULT_LABEL = "Main";
22
+ var COLLAPSE_ICON = "panel-left-close";
23
+ var EXPAND_ICON = "panel-left-open";
17
24
  var ROW_SELECTOR = ".item";
25
+ var NEW_TAB = "_blank";
26
+ var SAFE_REL = "noopener noreferrer";
18
27
  /** A JSON attribute that should hold a list of sections; anything else gives none. */
19
28
  var parseSections = (value) => {
20
29
  const parsed = jsonOf([])(value);
@@ -26,10 +35,12 @@ var isReachable = (row) => row.closest("[hidden]") === null && row.offsetParent
26
35
  /**
27
36
  * The navigation of an app, down its side: sections of items with icons, badges and nested items that open with
28
37
  * a chevron. `value` is the active item (its parents open to show it); `collapsed` makes it a rail of icons, with
29
- * the labels as tooltips. Items with an `href` are links; the others fire `select`.
38
+ * the labels as tooltips, and `collapsible` lets people fold and unfold it with a button at its bottom. Items with
39
+ * an `href` are links; the others fire `select`.
30
40
  */
31
41
  var JlSideNav = class extends JlElement {
32
42
  static selector = "jl-side-nav";
43
+ /** `collapse`: people folded the side nav into its rail (true) or unfolded it (false), for the app to remember. */
33
44
  events = this.useEvents();
34
45
  sections = this.useChange([], () => this.render(), {
35
46
  attribute: "sections",
@@ -40,16 +51,28 @@ var JlSideNav = class extends JlElement {
40
51
  /** Shows only the icons, in a narrow rail; the labels become tooltips. */
41
52
  collapsed = this.useChange(false, (value) => {
42
53
  this.reflect("collapsed", value);
43
- this.render();
54
+ this.renderRail();
44
55
  }, { attribute: "collapsed" });
56
+ /** Adds a button at the bottom that folds the side nav into its rail and unfolds it. */
57
+ collapsible = this.useChange(false, () => this.renderFold(), { attribute: "collapsible" });
58
+ /** The fold button's text while the side nav is open. */
59
+ collapseLabel = this.useChange(jlText("collapseMenu"), () => this.renderFold(), { attribute: "collapse-label" });
60
+ /** Its hint while the side nav is a rail. */
61
+ expandLabel = this.useChange(jlText("expandMenu"), () => this.renderFold(), { attribute: "expand-label" });
62
+ /**
63
+ * Set by `jl-app-shell` while the side nav is its drawer, on a narrow screen: the drawer shows it whole, and its
64
+ * fold button hides, `collapsed` waiting for a wide screen again.
65
+ */
66
+ inDrawer = this.useChange(false, () => this.renderRail(), { attribute: "in-drawer" });
45
67
  /** The name of the navigation for screen readers. */
46
- label = this.useChange(DEFAULT_LABEL, (value) => {
47
- this._refs.nav.setAttribute("aria-label", value === "" ? DEFAULT_LABEL : value);
68
+ label = this.useChange(jlText("mainNavigation"), (value) => {
69
+ this._refs.nav.setAttribute("aria-label", value === "" ? jlText("mainNavigation") : value);
48
70
  }, { attribute: "label" });
49
71
  _prefix = uniqueId("jl-side-nav");
50
72
  /** The parents people opened, or that hold the active item. */
51
73
  _state = { open: /* @__PURE__ */ new Set() };
52
74
  _refs = this.useRefs((ref) => ({
75
+ fold: ref(JlButton),
53
76
  nav: ref("nav"),
54
77
  sections: ref(".sections")
55
78
  }));
@@ -59,7 +82,15 @@ var JlSideNav = class extends JlElement {
59
82
  children: [
60
83
  /* @__PURE__ */ jsx("slot", { name: "header" }),
61
84
  /* @__PURE__ */ jsx("div", { class: "sections" }),
62
- /* @__PURE__ */ jsx("slot", { name: "footer" })
85
+ /* @__PURE__ */ jsxs("div", {
86
+ class: "bottom",
87
+ children: [/* @__PURE__ */ jsx("slot", { name: "footer" }), /* @__PURE__ */ jsx(JlButton, {
88
+ class: "fold",
89
+ variant: "ghost",
90
+ size: "sm",
91
+ hidden: true
92
+ })]
93
+ })
63
94
  ]
64
95
  });
65
96
  }
@@ -67,6 +98,7 @@ var JlSideNav = class extends JlElement {
67
98
  return css`${BASE_STYLES}${jl_side_nav_default}`;
68
99
  }
69
100
  onConnect() {
101
+ this._refs.fold.events.register("press", () => this.fold());
70
102
  this.on(this._refs.sections, "click", (event) => this.onClick(event));
71
103
  this.on(this._refs.sections, "keydown", (event) => this.onKey(event));
72
104
  }
@@ -86,12 +118,35 @@ var JlSideNav = class extends JlElement {
86
118
  const row = target.closest(ROW_SELECTOR);
87
119
  const item = findItem(this.sections.get(), row?.dataset.id ?? "");
88
120
  if (item === null) return;
89
- if (hasChildren(item) && item.href === void 0 && !this.collapsed.get()) {
121
+ if (hasChildren(item) && item.href === void 0 && !this.isRail()) {
90
122
  this.toggleItem(item.id);
91
123
  return;
92
124
  }
93
125
  this.choose(row?.dataset.target ?? item.id);
94
126
  }
127
+ /** Whether it shows as the rail now: folded, and not in a drawer. */
128
+ isRail() {
129
+ return this.collapsed.get() && !this.inDrawer.get();
130
+ }
131
+ fold() {
132
+ const collapsed = !this.collapsed.get();
133
+ this.collapsed.set(collapsed);
134
+ this.events.emit("collapse", collapsed);
135
+ }
136
+ /** The rail, or the whole side nav: drawn again, said on the host (`rail`) for the styles and the page. */
137
+ renderRail() {
138
+ this.reflect("rail", this.isRail());
139
+ this.render();
140
+ this.renderFold();
141
+ }
142
+ renderFold() {
143
+ const fold = this._refs.fold;
144
+ const rail = this.isRail();
145
+ fold.hidden = !this.collapsible.get() || this.inDrawer.get();
146
+ fold.icon.set(rail ? EXPAND_ICON : COLLAPSE_ICON);
147
+ fold.label.set(rail ? "" : this.collapseLabel.get());
148
+ fold.hint.set(rail ? this.expandLabel.get() : "");
149
+ }
95
150
  choose(id) {
96
151
  this.value.set(id);
97
152
  this.events.emit("select", id);
@@ -117,7 +172,7 @@ var JlSideNav = class extends JlElement {
117
172
  }
118
173
  }
119
174
  openOrGoDown(id, row) {
120
- if (row.dataset.parent === void 0 || this.collapsed.get()) return;
175
+ if (row.dataset.parent === void 0 || this.isRail()) return;
121
176
  if (!this._state.open.has(id)) {
122
177
  this.toggleItem(id, true);
123
178
  return;
@@ -161,7 +216,7 @@ var JlSideNav = class extends JlElement {
161
216
  class: "entry",
162
217
  children: [
163
218
  this.rowNode(item, depth, childrenId),
164
- parent && item.href !== void 0 && !this.collapsed.get() ? /* @__PURE__ */ jsx("button", {
219
+ parent && item.href !== void 0 && !this.isRail() ? /* @__PURE__ */ jsx("button", {
165
220
  class: "expander",
166
221
  type: "button",
167
222
  "data-id": item.id,
@@ -187,7 +242,7 @@ var JlSideNav = class extends JlElement {
187
242
  * leaf, since its children cannot show there.
188
243
  */
189
244
  rowNode(item, depth, childrenId) {
190
- const collapsed = this.collapsed.get();
245
+ const collapsed = this.isRail();
191
246
  const parent = hasChildren(item);
192
247
  const target = collapsed ? firstLeaf(item) : item;
193
248
  const toggles = parent && item.href === void 0 && !collapsed;
@@ -202,6 +257,8 @@ var JlSideNav = class extends JlElement {
202
257
  };
203
258
  if (target.href !== void 0) return /* @__PURE__ */ jsx("a", {
204
259
  href: target.href,
260
+ target: target.target,
261
+ rel: target.target === NEW_TAB ? SAFE_REL : void 0,
205
262
  ...attributes,
206
263
  children: content
207
264
  });
@@ -259,7 +316,7 @@ var JlSideNav = class extends JlElement {
259
316
  if (opened.length > 0) this.renderOpen();
260
317
  for (const row of this._refs.sections.querySelectorAll(ROW_SELECTOR)) {
261
318
  const id = row.dataset.id ?? "";
262
- const active = id === value || this.collapsed.get() && row.dataset.target === value;
319
+ const active = id === value || this.isRail() && row.dataset.target === value;
263
320
  setOrRemove(row, "aria-current", active ? "page" : "");
264
321
  row.classList.toggle("holds-active", !active && path.includes(id));
265
322
  }
@@ -2,6 +2,7 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
+ import { jlText } from "../lib/texts.js";
5
6
  import { JlIcon } from "../atoms/jl-icon.js";
6
7
  import { setOrRemove } from "../lib/dom.js";
7
8
  import jl_tab_bar_default from "./jl-tab-bar.css_raw.js";
@@ -11,7 +12,6 @@ import { Fragment, jsx, jsxs } from "@jadis/core/jsx-runtime";
11
12
  // src/organisms/jl-tab-bar.tsx
12
13
  registerIcons({ ellipsis: Ellipsis });
13
14
  var ICON_SIZE = 22;
14
- var DEFAULT_LABEL = "Shortcuts";
15
15
  var MAX_COUNT = 99;
16
16
  var ITEM_SELECTOR = ".item";
17
17
  /** A JSON attribute that should hold a list of items; anything else gives none. */
@@ -45,7 +45,7 @@ var JlTabBar = class extends JlElement {
45
45
  this._refs.more.hidden = value === "";
46
46
  }, { attribute: "more" });
47
47
  /** The name of the navigation for screen readers. */
48
- label = this.useChange(DEFAULT_LABEL, (value) => this._refs.nav.setAttribute("aria-label", value === "" ? DEFAULT_LABEL : value), { attribute: "label" });
48
+ label = this.useChange(jlText("shortcuts"), (value) => this._refs.nav.setAttribute("aria-label", value === "" ? jlText("shortcuts") : value), { attribute: "label" });
49
49
  _refs = this.useRefs((ref) => ({
50
50
  items: ref(".items"),
51
51
  more: ref(".more"),
@@ -1,3 +1,3 @@
1
1
  // src/organisms/jl-table.css?raw
2
- var jl_table_default = ":host {\n display: block;\n /* The table scrolls sideways inside its frame: its width never pushes the page, or a grid around it, wider. */\n contain: inline-size;\n --max-height: none;\n --cell-y: 10px;\n --cell-x: 14px;\n}\n\n:host([dense]) {\n --cell-y: 6px;\n --cell-x: 12px;\n}\n\n.scroll {\n max-height: var(--max-height);\n overflow: auto;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius);\n background: var(--jl-surface);\n overscroll-behavior-x: contain;\n}\n\ntable {\n width: 100%;\n border-collapse: separate;\n border-spacing: 0;\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n line-height: 20px;\n}\n\nth,\ntd {\n padding: var(--cell-y) var(--cell-x);\n white-space: nowrap;\n vertical-align: middle;\n}\n\nth {\n position: sticky;\n top: 0;\n z-index: 1;\n border-bottom: 1px solid var(--jl-line);\n background: var(--jl-surface-2);\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-weight: 600;\n letter-spacing: 0.01em;\n text-align: left;\n}\n\ntd {\n border-top: 1px solid var(--jl-line-soft);\n}\n\ntbody tr:first-child td {\n border-top: none;\n}\n\ntbody tr.empty td {\n padding: 40px var(--cell-x);\n color: var(--jl-muted);\n text-align: center;\n}\n\n.align-left {\n text-align: left;\n}\n\n.align-center {\n text-align: center;\n}\n\n.align-right {\n text-align: right;\n}\n\n.sort {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n margin: -2px -4px;\n padding: 2px 4px;\n border: none;\n border-radius: var(--jl-radius-xs);\n background: none;\n color: inherit;\n font-weight: inherit;\n letter-spacing: inherit;\n cursor: pointer;\n}\n\n.sort:hover {\n color: var(--jl-text);\n}\n\n.sort-icon {\n color: var(--jl-faint);\n}\n\nth[data-sorted] {\n color: var(--jl-text);\n}\n\nth[data-sorted] .sort-icon {\n color: var(--jl-accent);\n}\n\n/* Selected rows keep their shade: the hover and the stripes leave them alone. */\ntbody tr[data-selected] td {\n background: var(--jl-accent-soft);\n}\n\ntbody tr:not(.empty, .skeleton, [data-selected]):hover td {\n background: var(--jl-surface-2);\n}\n\n:host([striped]) tbody tr:nth-child(even):not(.empty, [data-selected]) td {\n background: var(--jl-surface-2);\n}\n\n:host([striped]) tbody tr:not(.empty, .skeleton, [data-selected]):hover td {\n background: var(--jl-surface-3);\n}\n\n.format-number {\n font-variant-numeric: tabular-nums;\n}\n\n.format-date {\n color: var(--jl-text-2);\n font-variant-numeric: tabular-nums;\n}\n\ncode {\n padding: 2px 6px;\n border-radius: var(--jl-radius-xs);\n background: var(--jl-code-bg);\n font-size: var(--jl-font-xs);\n}\n\n.select-cell {\n width: 44px;\n padding-right: 0;\n line-height: 0;\n}\n\n/* The checkbox: the native input keeps the keyboard and the states, the mark beside it draws the tick. */\n.check {\n position: relative;\n display: inline-flex;\n}\n\n.checkbox {\n width: 16px;\n height: 16px;\n margin: 0;\n border: 1.5px solid var(--jl-line-strong);\n border-radius: 4px;\n background: var(--jl-surface);\n cursor: pointer;\n appearance: none;\n transition:\n background var(--jl-fast) var(--jl-ease),\n border-color var(--jl-fast) var(--jl-ease);\n}\n\n.checkbox:hover {\n border-color: var(--jl-muted);\n}\n\n.checkbox:checked,\n.checkbox:indeterminate {\n border-color: var(--jl-accent);\n background: var(--jl-accent);\n}\n\n.checkbox:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.mark {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.checkbox:checked + .mark::after {\n position: absolute;\n top: 2.5px;\n left: 5.5px;\n width: 5px;\n height: 9px;\n border: solid var(--jl-on-accent);\n border-width: 0 2px 2px 0;\n rotate: 45deg;\n content: \"\";\n}\n\n.checkbox:indeterminate + .mark::after {\n position: absolute;\n top: 7px;\n right: 4px;\n left: 4px;\n height: 2px;\n border-radius: 1px;\n background: var(--jl-on-accent);\n content: \"\";\n}\n\n.empty-content {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n\n.empty jl-icon {\n color: var(--jl-faint);\n}\n\n.bone {\n display: inline-block;\n width: 70%;\n max-width: 160px;\n height: 10px;\n border-radius: var(--jl-radius-pill);\n background:\n linear-gradient(90deg, var(--jl-surface-3) 30%, var(--jl-surface-2) 50%, var(--jl-surface-3) 70%) 0 0 / 300%\n 100%;\n vertical-align: middle;\n animation: shimmer 1.4s linear infinite;\n}\n\n.bone.box {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n}\n\ntbody tr.skeleton:nth-child(3n + 2) .bone:not(.box) {\n width: 50%;\n}\n\ntbody tr.skeleton:nth-child(3n) .bone:not(.box) {\n width: 85%;\n}\n\n/* Shortened to a millisecond by the base styles, an endless shimmer would flicker: it stands still instead. */\n@media (prefers-reduced-motion: reduce) {\n .bone {\n animation: none;\n }\n}\n\n@keyframes shimmer {\n from {\n background-position: 100% 0;\n }\n\n to {\n background-position: 0 0;\n }\n}\n";
2
+ var jl_table_default = ":host {\n display: block;\n /* The table scrolls sideways inside its frame: its width never pushes the page, or a grid around it, wider. */\n contain: inline-size;\n --max-height: none;\n --cell-y: 10px;\n --cell-x: 14px;\n}\n\n:host([dense]) {\n --cell-y: 6px;\n --cell-x: 12px;\n}\n\n.scroll {\n max-height: var(--max-height);\n overflow: auto;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius);\n background: var(--jl-surface);\n overscroll-behavior-x: contain;\n}\n\ntable {\n width: 100%;\n border-collapse: separate;\n border-spacing: 0;\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n line-height: 20px;\n}\n\nth,\ntd {\n padding: var(--cell-y) var(--cell-x);\n white-space: nowrap;\n vertical-align: middle;\n}\n\nth {\n position: sticky;\n top: 0;\n z-index: 1;\n border-bottom: 1px solid var(--jl-line);\n background: var(--jl-surface-2);\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-weight: 600;\n letter-spacing: 0.01em;\n text-align: left;\n}\n\ntd {\n border-top: 1px solid var(--jl-line-soft);\n}\n\ntbody tr:first-child td {\n border-top: none;\n}\n\ntbody tr.empty td {\n padding: 40px var(--cell-x);\n color: var(--jl-muted);\n text-align: center;\n}\n\n.align-left {\n text-align: left;\n}\n\n.align-center {\n text-align: center;\n}\n\n.align-right {\n text-align: right;\n}\n\n.sort {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n margin: -2px -4px;\n padding: 2px 4px;\n border: none;\n border-radius: var(--jl-radius-xs);\n background: none;\n color: inherit;\n font-weight: inherit;\n letter-spacing: inherit;\n cursor: pointer;\n}\n\n.sort:hover {\n color: var(--jl-text);\n}\n\n.sort-icon {\n color: var(--jl-faint);\n}\n\nth[data-sorted] {\n color: var(--jl-text);\n}\n\nth[data-sorted] .sort-icon {\n color: var(--jl-accent);\n}\n\n/* Selected rows keep their shade: the hover and the stripes leave them alone. */\ntbody tr[data-selected] td {\n background: var(--jl-accent-soft);\n}\n\n/* Marked rows (a search's matches) carry a bar at their start and a shade, which selection and hover take over. */\ntbody tr[data-marked] td {\n background: var(--jl-accent-soft);\n}\n\ntbody tr[data-marked] td:first-child {\n box-shadow: inset 3px 0 0 var(--jl-accent);\n}\n\ntbody tr:not(.empty, .skeleton, [data-selected]):hover td {\n background: var(--jl-surface-2);\n}\n\n:host([striped]) tbody tr:nth-child(even):not(.empty, [data-selected], [data-marked]) td {\n background: var(--jl-surface-2);\n}\n\n:host([striped]) tbody tr:not(.empty, .skeleton, [data-selected]):hover td {\n background: var(--jl-surface-3);\n}\n\n.format-number {\n font-variant-numeric: tabular-nums;\n}\n\n/* Broken anywhere, so a long address wraps too; never so narrow that a word stands alone on each line. */\ntd.wrap {\n min-width: 160px;\n white-space: normal;\n overflow-wrap: anywhere;\n}\n\n.format-date {\n color: var(--jl-text-2);\n font-variant-numeric: tabular-nums;\n}\n\ncode {\n padding: 2px 6px;\n border-radius: var(--jl-radius-xs);\n background: var(--jl-code-bg);\n font-size: var(--jl-font-xs);\n}\n\n.select-cell {\n width: 44px;\n padding-right: 0;\n line-height: 0;\n}\n\n/* The checkbox: the native input keeps the keyboard and the states, the mark beside it draws the tick. */\n.check {\n position: relative;\n display: inline-flex;\n}\n\n.checkbox {\n width: 16px;\n height: 16px;\n margin: 0;\n border: 1.5px solid var(--jl-line-strong);\n border-radius: 4px;\n background: var(--jl-surface);\n cursor: pointer;\n appearance: none;\n transition:\n background var(--jl-fast) var(--jl-ease),\n border-color var(--jl-fast) var(--jl-ease);\n}\n\n.checkbox:hover {\n border-color: var(--jl-muted);\n}\n\n.checkbox:checked,\n.checkbox:indeterminate {\n border-color: var(--jl-accent);\n background: var(--jl-accent);\n}\n\n.checkbox:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.mark {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.checkbox:checked + .mark::after {\n position: absolute;\n top: 2.5px;\n left: 5.5px;\n width: 5px;\n height: 9px;\n border: solid var(--jl-on-accent);\n border-width: 0 2px 2px 0;\n rotate: 45deg;\n content: \"\";\n}\n\n.checkbox:indeterminate + .mark::after {\n position: absolute;\n top: 7px;\n right: 4px;\n left: 4px;\n height: 2px;\n border-radius: 1px;\n background: var(--jl-on-accent);\n content: \"\";\n}\n\n.empty-content {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n\n.empty jl-icon {\n color: var(--jl-faint);\n}\n\n.bone {\n display: inline-block;\n width: 70%;\n max-width: 160px;\n height: 10px;\n border-radius: var(--jl-radius-pill);\n background:\n linear-gradient(90deg, var(--jl-surface-3) 30%, var(--jl-surface-2) 50%, var(--jl-surface-3) 70%) 0 0 / 300%\n 100%;\n vertical-align: middle;\n animation: shimmer 1.4s linear infinite;\n}\n\n.bone.box {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n}\n\ntbody tr.skeleton:nth-child(3n + 2) .bone:not(.box) {\n width: 50%;\n}\n\ntbody tr.skeleton:nth-child(3n) .bone:not(.box) {\n width: 85%;\n}\n\n/* Shortened to a millisecond by the base styles, an endless shimmer would flicker: it stands still instead. */\n@media (prefers-reduced-motion: reduce) {\n .bone {\n animation: none;\n }\n}\n\n@keyframes shimmer {\n from {\n background-position: 100% 0;\n }\n\n to {\n background-position: 0 0;\n }\n}\n\n/* A column's description: a quieter line under the cell's value. */\n.cell-description {\n margin-top: 2px;\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n}\n";
3
3
  export { jl_table_default as default };
@@ -46,6 +46,11 @@ export declare class JlTable extends JlElement {
46
46
  get(): TableRowKey[];
47
47
  set(setter: TableRowKey[] | ((prevState: TableRowKey[]) => TableRowKey[])): void;
48
48
  }>>;
49
+ /** The keys of rows to draw attention to, such as a search's matches: a mark, not a selection. */
50
+ readonly marked: Readonly<Readonly<{
51
+ get(): TableRowKey[];
52
+ set(setter: TableRowKey[] | ((prevState: TableRowKey[]) => TableRowKey[])): void;
53
+ }>>;
49
54
  /** Shorter rows, for long lists. */
50
55
  readonly dense: Readonly<Readonly<{
51
56
  get(): boolean;
@@ -101,9 +106,13 @@ export declare class JlTable extends JlElement {
101
106
  private sortedRows;
102
107
  private renderBody;
103
108
  private bodyRow;
109
+ /** A cell's value, drawn by its renderer or in its format, with the row's description under it when it has one. */
104
110
  private cell;
111
+ private cellValue;
105
112
  private skeletonRows;
106
113
  private emptyRow;
114
+ /** The bar and the shade of marked rows, changed in place: one body row per key while there are rows. */
115
+ private renderMarks;
107
116
  /** The checkboxes and the shading of selected rows, changed in place. */
108
117
  private renderSelection;
109
118
  }
@@ -2,14 +2,15 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf, oneOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
+ import { columnAlign, columnWidth, formatCell, nextSortDirection, rowKeyOf, selectionState, sortRows, toggleAll, toggleKey } from "../lib/table.js";
6
+ import { jlText } from "../lib/texts.js";
5
7
  import { JlIcon } from "../atoms/jl-icon.js";
6
8
  import { setOrRemove } from "../lib/dom.js";
7
9
  import { JlBadge } from "../atoms/jl-badge.js";
8
- import { columnAlign, columnWidth, formatCell, nextSortDirection, rowKeyOf, selectionState, sortRows, toggleAll, toggleKey } from "../lib/table.js";
9
10
  import jl_table_default from "./jl-table.css_raw.js";
10
11
  import { css } from "@jadis/core";
11
12
  import { ArrowDown, ArrowUp, ChevronsUpDown, Inbox } from "lucide";
12
- import { jsx, jsxs } from "@jadis/core/jsx-runtime";
13
+ import { Fragment, jsx, jsxs } from "@jadis/core/jsx-runtime";
13
14
  // src/organisms/jl-table.tsx
14
15
  registerIcons({
15
16
  "arrow-down": ArrowDown,
@@ -22,8 +23,8 @@ var TABLE_SORT_DIRECTIONS = [
22
23
  "asc",
23
24
  "desc"
24
25
  ];
26
+ var MARKED_ATTRIBUTE = "data-marked";
25
27
  var DEFAULT_ROW_KEY = "id";
26
- var DEFAULT_EMPTY_TEXT = "No data";
27
28
  var SKELETON_ROWS = 5;
28
29
  var SORT_ICON_SIZE = 14;
29
30
  var EMPTY_ICON_SIZE = 18;
@@ -45,6 +46,7 @@ var CELL_MAKERS = {
45
46
  }),
46
47
  code: (text) => text === "" ? "" : /* @__PURE__ */ jsx("code", { children: text }),
47
48
  date: (text) => text,
49
+ datetime: (text) => text,
48
50
  number: (text) => text,
49
51
  text: (text) => text
50
52
  };
@@ -99,6 +101,11 @@ var JlTable = class extends JlElement {
99
101
  attribute: "selected",
100
102
  parse: jsonOf([])
101
103
  });
104
+ /** The keys of rows to draw attention to, such as a search's matches: a mark, not a selection. */
105
+ marked = this.useChange([], () => this.renderMarks(), {
106
+ attribute: "marked",
107
+ parse: jsonOf([])
108
+ });
102
109
  /** Shorter rows, for long lists. */
103
110
  dense = this.useChange(false, (value) => this.reflect("dense", value), { attribute: "dense" });
104
111
  /** Shades every other row. */
@@ -106,7 +113,7 @@ var JlTable = class extends JlElement {
106
113
  /** A CSS height past which the rows scroll under the header: `320px`, `50vh`; a number is pixels. */
107
114
  maxHeight = this.useChange("", (value) => this.renderMaxHeight(value), { attribute: "max-height" });
108
115
  /** What the table says when it has no rows. */
109
- emptyText = this.useChange(DEFAULT_EMPTY_TEXT, () => this.schedule("body"), { attribute: "empty-text" });
116
+ emptyText = this.useChange(jlText("noData"), () => this.schedule("body"), { attribute: "empty-text" });
110
117
  /** Shows placeholder rows while the data loads. */
111
118
  loading = this.useChange(false, (value) => {
112
119
  this.reflect("loading", value);
@@ -250,6 +257,7 @@ var JlTable = class extends JlElement {
250
257
  const sorted = this.sortedRows();
251
258
  this._view.keys = sorted.map((row, index) => keys.get(row) ?? index);
252
259
  this._refs.body.replaceChildren(...sorted.map((row, index) => this.bodyRow(row, index, columns)));
260
+ this.renderMarks();
253
261
  }
254
262
  bodyRow(row, index, columns) {
255
263
  const first = columns[0];
@@ -259,11 +267,24 @@ var JlTable = class extends JlElement {
259
267
  children: checkbox(`Select ${name}`, index)
260
268
  }) : null;
261
269
  return /* @__PURE__ */ jsxs("tr", { children: [select, columns.map((column) => /* @__PURE__ */ jsx("td", {
262
- class: `align-${columnAlign(column)} format-${column.format ?? "text"}`,
270
+ class: `align-${columnAlign(column)} format-${column.format ?? "text"}${column.wrap ? " wrap" : ""}`,
263
271
  children: this.cell(row, column)
264
272
  }))] });
265
273
  }
274
+ /** A cell's value, drawn by its renderer or in its format, with the row's description under it when it has one. */
266
275
  cell(row, column) {
276
+ const value = this.cellValue(row, column);
277
+ const description = column.description === void 0 ? "" : formatCell(row[column.description]);
278
+ if (description === "") return value;
279
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
280
+ class: "cell-value",
281
+ children: value
282
+ }), /* @__PURE__ */ jsx("div", {
283
+ class: "cell-description",
284
+ children: description
285
+ })] });
286
+ }
287
+ cellValue(row, column) {
267
288
  const renderer = this.renderers.get()[column.key];
268
289
  if (renderer !== void 0) return renderer(row);
269
290
  const format = column.format ?? "text";
@@ -297,6 +318,15 @@ var JlTable = class extends JlElement {
297
318
  })
298
319
  });
299
320
  }
321
+ /** The bar and the shade of marked rows, changed in place: one body row per key while there are rows. */
322
+ renderMarks() {
323
+ const marked = new Set(this.marked.get());
324
+ const rows = this._view.keys.length === 0 ? [] : [...this._refs.body.children];
325
+ for (const [index, row] of rows.entries()) {
326
+ const key = this._view.keys[index];
327
+ row.toggleAttribute(MARKED_ATTRIBUTE, key !== void 0 && marked.has(key));
328
+ }
329
+ }
300
330
  /** The checkboxes and the shading of selected rows, changed in place. */
301
331
  renderSelection() {
302
332
  const selected = new Set(this.selected.get());
@@ -1,5 +1,5 @@
1
1
  import { JlElement } from '../base/jl-element';
2
- import type { ToasterPlacement, ToastHandle, ToastOptions } from '../types/jl-toaster.types';
2
+ import type { DownloadOptions, DownloadResult, DownloadTexts, ToasterPlacement, ToastHandle, ToastOptions } from '../types/jl-toaster.types';
3
3
  export declare const TOASTER_PLACEMENTS: readonly ToasterPlacement[];
4
4
  /**
5
5
  * The corner of the window where notifications stack, newest nearest the edge, at most `max` at a time. It lives in
@@ -49,3 +49,11 @@ export declare class JlToaster extends JlElement {
49
49
  * Returns a handle to close it early.
50
50
  */
51
51
  export declare function toast(options: ToastOptions): ToastHandle;
52
+ /** A download's texts, in the language the page set (`setTexts`): what `download()` says unless told otherwise. */
53
+ export declare const downloadTexts: () => DownloadTexts;
54
+ /**
55
+ * Downloads a file with its progress in a toast of the page's toaster: the time the server takes to prepare it,
56
+ * the bytes received (a bar when it says the size), then the name it was saved under, or why it failed. Its
57
+ * Cancel stops it. Call it from a download link's click, leaving a modified click (a new tab) to the browser.
58
+ */
59
+ export declare function download(url: string, options?: DownloadOptions): Promise<DownloadResult>;
@@ -1,11 +1,19 @@
1
1
  import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { clampedNumber, oneOf } from "../lib/attributes.js";
4
+ import { registerIcons } from "../lib/icons.js";
5
+ import { formatDuration } from "../lib/time.js";
6
+ import { fillText } from "../lib/text.js";
7
+ import { jlText } from "../lib/texts.js";
8
+ import { formatFileSize } from "../lib/files.js";
4
9
  import { JlToast } from "../molecules/jl-toast.js";
10
+ import { answerError, dispositionFileName, readWithProgress, saveBlob, urlFileName } from "../lib/download.js";
5
11
  import jl_toaster_default from "./jl-toaster.css_raw.js";
6
12
  import { css } from "@jadis/core";
13
+ import { Download } from "lucide";
7
14
  import { jsx } from "@jadis/core/jsx-runtime";
8
15
  // src/organisms/jl-toaster.tsx
16
+ registerIcons({ download: Download });
9
17
  var TOASTER_PLACEMENTS = [
10
18
  "bottom-right",
11
19
  "bottom-center",
@@ -18,11 +26,26 @@ var MAX_LIMIT = 20;
18
26
  var DEFAULT_DURATION = 5e3;
19
27
  var LONG_DURATION = 8e3;
20
28
  var LONG_TONE = "danger";
21
- var DEFAULT_LABEL = "Notifications";
22
29
  var STACKED = "data-stacked";
23
30
  /** How long a toast stays when its options do not say: longer for an error, or to give time to press its action. */
24
31
  var defaultDuration = (options) => options.tone === LONG_TONE || (options.actionLabel ?? "") !== "" ? LONG_DURATION : DEFAULT_DURATION;
25
32
  var OPEN = ":popover-open";
33
+ /** The inputs of a toast that an update may change, each set when the update gives it. */
34
+ var TOAST_SETTERS = {
35
+ actionLabel: (toast, value) => toast.actionLabel.set(value ?? ""),
36
+ closable: (toast, value) => toast.closable.set(value ?? false),
37
+ duration: (toast, value) => toast.duration.set(value ?? 0),
38
+ heading: (toast, value) => toast.heading.set(value ?? ""),
39
+ icon: (toast, value) => toast.icon.set(value ?? ""),
40
+ onAction: () => void 0,
41
+ progress: (toast, value) => toast.progress.set(value ?? null),
42
+ text: (toast, value) => toast.text.set(value ?? ""),
43
+ tone: (toast, value) => toast.tone.set(value ?? "neutral"),
44
+ working: (toast, value) => toast.working.set(value ?? false)
45
+ };
46
+ function updateToast(toast, changes) {
47
+ for (const key of Object.keys(changes)) TOAST_SETTERS[key](toast, changes[key]);
48
+ }
26
49
  /**
27
50
  * The corner of the window where notifications stack, newest nearest the edge, at most `max` at a time. It lives in
28
51
  * the top layer, so it shows over open dialogs, and its live region reads new toasts to screen readers. Pages
@@ -40,7 +63,7 @@ var JlToaster = class extends JlElement {
40
63
  parse: clampedNumber(DEFAULT_MAX, 1, MAX_LIMIT)
41
64
  });
42
65
  /** The region's name for screen readers. */
43
- label = this.useChange(DEFAULT_LABEL, (value) => this._refs.region.setAttribute("aria-label", value), { attribute: "label" });
66
+ label = this.useChange(jlText("notifications"), (value) => this._refs.region.setAttribute("aria-label", value), { attribute: "label" });
44
67
  _watched = /* @__PURE__ */ new WeakSet();
45
68
  _refs = this.useRefs((ref) => ({
46
69
  region: ref(".region"),
@@ -58,13 +81,21 @@ var JlToaster = class extends JlElement {
58
81
  icon: options.icon,
59
82
  duration: options.duration ?? defaultDuration(options),
60
83
  closable: options.closable ?? true,
61
- actionLabel: options.actionLabel
84
+ actionLabel: options.actionLabel,
85
+ working: options.working,
86
+ progress: options.progress
62
87
  });
63
- const onAction = options.onAction;
64
- if (onAction !== void 0) toast.addEventListener("action", () => onAction());
88
+ const actions = { onAction: options.onAction };
89
+ toast.addEventListener("action", () => actions.onAction?.());
65
90
  this.append(toast);
66
91
  this.whenRendered(() => this.raise());
67
- return { dismiss: () => toast.dismiss() };
92
+ return {
93
+ dismiss: () => toast.dismiss(),
94
+ update: (changes) => {
95
+ if ("onAction" in changes) actions.onAction = changes.onAction;
96
+ updateToast(toast, changes);
97
+ }
98
+ };
68
99
  }
69
100
  templateHtml() {
70
101
  return /* @__PURE__ */ jsx("section", {
@@ -146,4 +177,114 @@ function pageToaster() {
146
177
  function toast(options) {
147
178
  return pageToaster().notify(options);
148
179
  }
149
- export { JlToaster, TOASTER_PLACEMENTS, toast };
180
+ var DOWNLOAD_ICON = "download";
181
+ var SAVED_VISIBLE_MS = 6e3;
182
+ var PREPARING_TICK_MS = 1e3;
183
+ var PROGRESS_INTERVAL_MS = 250;
184
+ var PERCENT = 100;
185
+ var FALLBACK_NAME = "download";
186
+ var CONTENT_LENGTH = "content-length";
187
+ var CONTENT_DISPOSITION = "content-disposition";
188
+ /** A download's texts, in the language the page set (`setTexts`): what `download()` says unless told otherwise. */
189
+ var downloadTexts = () => ({
190
+ cancel: jlText("downloadCancel"),
191
+ failed: jlText("downloadFailed"),
192
+ preparing: jlText("downloadPreparing"),
193
+ received: jlText("downloadReceived"),
194
+ receivedOf: jlText("downloadReceivedOf"),
195
+ saved: jlText("downloadSaved")
196
+ });
197
+ /** The toast's text and bar for the bytes received: a share of the size when the server said it. */
198
+ function receiving(received, total, texts) {
199
+ const size = formatFileSize(received);
200
+ if (total <= 0) return {
201
+ progress: null,
202
+ text: fillText(texts.received, { received: size })
203
+ };
204
+ return {
205
+ progress: Math.min(PERCENT, received / total * PERCENT),
206
+ text: fillText(texts.receivedOf, {
207
+ received: size,
208
+ total: formatFileSize(total)
209
+ })
210
+ };
211
+ }
212
+ async function failureOf(response) {
213
+ const body = await response.json().catch(() => null);
214
+ return new Error(answerError(body, response.status));
215
+ }
216
+ /**
217
+ * Downloads a file with its progress in a toast of the page's toaster: the time the server takes to prepare it,
218
+ * the bytes received (a bar when it says the size), then the name it was saved under, or why it failed. Its
219
+ * Cancel stops it. Call it from a download link's click, leaving a modified click (a new tab) to the browser.
220
+ */
221
+ async function download(url, options = {}) {
222
+ const texts = {
223
+ ...downloadTexts(),
224
+ ...options.texts
225
+ };
226
+ const controller = new AbortController();
227
+ const started = performance.now();
228
+ const preparingText = () => fillText(texts.preparing, { time: formatDuration(performance.now() - started) });
229
+ const handle = toast({
230
+ actionLabel: texts.cancel,
231
+ closable: false,
232
+ duration: 0,
233
+ heading: options.title ?? urlFileName(url, FALLBACK_NAME),
234
+ icon: DOWNLOAD_ICON,
235
+ onAction: () => controller.abort(),
236
+ progress: null,
237
+ text: preparingText(),
238
+ working: true
239
+ });
240
+ const preparing = window.setInterval(() => handle.update({ text: preparingText() }), PREPARING_TICK_MS);
241
+ try {
242
+ const response = await fetch(url, {
243
+ ...options.init,
244
+ signal: controller.signal
245
+ });
246
+ window.clearInterval(preparing);
247
+ if (!response.ok) throw await failureOf(response);
248
+ const total = Number(response.headers.get(CONTENT_LENGTH)) || 0;
249
+ const blob = await readWithProgress(response, (received) => handle.update(receiving(received, total, texts)), PROGRESS_INTERVAL_MS);
250
+ const name = dispositionFileName(response.headers.get(CONTENT_DISPOSITION)) ?? options.fileName ?? urlFileName(url, FALLBACK_NAME);
251
+ saveBlob(blob, name);
252
+ handle.update({
253
+ actionLabel: "",
254
+ closable: true,
255
+ duration: SAVED_VISIBLE_MS,
256
+ icon: "",
257
+ text: fillText(texts.saved, {
258
+ name,
259
+ size: formatFileSize(blob.size)
260
+ }),
261
+ tone: "ok",
262
+ working: false
263
+ });
264
+ return {
265
+ name,
266
+ outcome: "saved",
267
+ size: blob.size
268
+ };
269
+ } catch (error) {
270
+ window.clearInterval(preparing);
271
+ if (controller.signal.aborted) {
272
+ handle.dismiss();
273
+ return { outcome: "canceled" };
274
+ }
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ handle.update({
277
+ actionLabel: "",
278
+ closable: true,
279
+ icon: "",
280
+ text: fillText(texts.failed, { error: message }),
281
+ tone: "danger",
282
+ working: false
283
+ });
284
+ return {
285
+ error: message,
286
+ outcome: "failed"
287
+ };
288
+ }
289
+ }
290
+ export { JlToaster, TOASTER_PLACEMENTS, download, downloadTexts, toast };
@@ -1,3 +1,3 @@
1
1
  // src/organisms/jl-tree.css?raw
2
- var jl_tree_default = ":host {\n display: block;\n --indent: 16px;\n --row-height: 30px;\n}\n\n.tree {\n color: var(--jl-text-2);\n font-size: var(--jl-font-md);\n}\n\n.group {\n position: relative;\n}\n\n/* A quiet line under each open folder, from its chevron down along its children. */\n.group::before {\n position: absolute;\n top: 0;\n bottom: 4px;\n left: calc(8px + var(--level) * var(--indent) + 6.5px);\n width: 1px;\n background: var(--jl-line);\n content: \"\";\n}\n\n.item:focus-visible {\n box-shadow: none;\n}\n\n.row {\n display: flex;\n align-items: center;\n gap: 6px;\n height: var(--row-height);\n padding: 0 8px 0 calc(8px + var(--level) * var(--indent));\n border-radius: var(--jl-radius-sm);\n cursor: pointer;\n user-select: none;\n}\n\n.row:hover {\n background: var(--jl-surface-2);\n color: var(--jl-text);\n}\n\n.item:focus-visible > .row {\n box-shadow: var(--jl-focus-ring);\n}\n\n.item[aria-selected=\"true\"] > .row {\n background: var(--jl-accent-soft);\n color: var(--jl-text);\n font-weight: 600;\n}\n\n.chevron {\n display: inline-flex;\n flex: none;\n justify-content: center;\n width: 14px;\n color: var(--jl-faint);\n transition: rotate var(--jl-fast) var(--jl-ease);\n}\n\n.item[aria-expanded=\"true\"] > .row > .chevron {\n rotate: 90deg;\n}\n\n.icon {\n color: var(--jl-muted);\n}\n\n.item[aria-selected=\"true\"] > .row > .icon {\n color: var(--jl-accent);\n}\n\n.label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n";
2
+ var jl_tree_default = ":host {\n display: block;\n --indent: 16px;\n --row-height: 30px;\n}\n\n.tree {\n color: var(--jl-text-2);\n font-size: var(--jl-font-md);\n}\n\n.group {\n position: relative;\n}\n\n/* A quiet line under each open folder, from its chevron down along its children. */\n.group::before {\n position: absolute;\n top: 0;\n bottom: 4px;\n left: calc(8px + var(--level) * var(--indent) + 6.5px);\n width: 1px;\n background: var(--jl-line);\n content: \"\";\n}\n\n.item:focus-visible {\n box-shadow: none;\n}\n\n.row {\n display: flex;\n align-items: center;\n gap: 6px;\n height: var(--row-height);\n padding: 0 8px 0 calc(8px + var(--level) * var(--indent));\n border-radius: var(--jl-radius-sm);\n cursor: pointer;\n user-select: none;\n}\n\n.row:hover {\n background: var(--jl-surface-2);\n color: var(--jl-text);\n}\n\n.item:focus-visible > .row {\n box-shadow: var(--jl-focus-ring);\n}\n\n.item[aria-selected=\"true\"] > .row {\n background: var(--jl-accent-soft);\n color: var(--jl-text);\n font-weight: 600;\n}\n\n.chevron {\n display: inline-flex;\n flex: none;\n justify-content: center;\n width: 14px;\n color: var(--jl-faint);\n transition: rotate var(--jl-fast) var(--jl-ease);\n}\n\n.item[aria-expanded=\"true\"] > .row > .chevron {\n rotate: 90deg;\n}\n\n.icon {\n color: var(--jl-muted);\n}\n\n.item[aria-selected=\"true\"] > .row > .icon {\n color: var(--jl-accent);\n}\n\n.label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* A link's row looks like any row: the address shows in the status bar, and a middle click opens a tab. */\na.row {\n color: inherit;\n text-decoration: none;\n}\n\n/* What ends a level: its page on the way, the button for its next page, an error to load again. */\n.item.level-end > .row {\n color: var(--jl-muted);\n}\n\n.level-end[aria-disabled=\"true\"] > .row {\n cursor: default;\n}\n\n.level-end[aria-disabled=\"true\"] > .row:hover {\n background: none;\n}\n\n.spinner {\n flex: none;\n color: var(--jl-muted);\n}\n\n.more,\n.retry {\n flex: none;\n color: var(--jl-accent);\n font-weight: 500;\n}\n\n.level-end > .row:hover .more,\n.level-end > .row:hover .retry {\n color: var(--jl-accent-strong);\n}\n\n.icon.error {\n color: var(--jl-danger);\n}\n\n.note {\n display: flex;\n align-items: center;\n gap: 6px;\n min-height: var(--row-height);\n padding: 0 8px 0 calc(8px + var(--level) * var(--indent));\n color: var(--jl-muted);\n}\n\n/* A folder's count, or a short word, after its label. */\n.badge {\n flex: none;\n margin-left: auto;\n color: var(--jl-faint);\n font-size: var(--jl-font-xs);\n font-variant-numeric: tabular-nums;\n}\n";
3
3
  export { jl_tree_default as default };