@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
@@ -4,6 +4,7 @@ import { jsonOf, oneOf } from "../lib/attributes.js";
4
4
  import { followAnchor } from "../lib/floating.js";
5
5
  import { registerIcons } from "../lib/icons.js";
6
6
  import { CONTROL_SIZES } from "../lib/options.js";
7
+ import { jlText } from "../lib/texts.js";
7
8
  import { JlIcon } from "../atoms/jl-icon.js";
8
9
  import { setOrRemove, uniqueId } from "../lib/dom.js";
9
10
  import { keepInView } from "../lib/keep-in-view.js";
@@ -23,8 +24,6 @@ var ACTION_ICON_SIZE = 14;
23
24
  var CHECK_SIZE = 14;
24
25
  var NONE = -1;
25
26
  var PLACEMENT = "bottom-start";
26
- var VALUE_MISSING = "Choose an option.";
27
- var DEFAULT_EMPTY_TEXT = "No results";
28
27
  /** A JSON attribute that should hold a list of options; anything else gives none. */
29
28
  var parseOptions = (value) => {
30
29
  const parsed = jsonOf([])(value);
@@ -57,7 +56,7 @@ var JlCombobox = class extends JlFormControl {
57
56
  /** Shows a button that empties the field while it holds text. */
58
57
  clearable = this.useChange(false, () => this.renderClear(), { attribute: "clearable" });
59
58
  /** What the list says when no option matches. */
60
- emptyText = this.useChange(DEFAULT_EMPTY_TEXT, (value) => {
59
+ emptyText = this.useChange(jlText("noResults"), (value) => {
61
60
  this._refs.empty.textContent = value;
62
61
  }, { attribute: "empty-text" });
63
62
  required = this.useChange(false, (value) => {
@@ -112,7 +111,7 @@ var JlCombobox = class extends JlFormControl {
112
111
  /* @__PURE__ */ jsx("button", {
113
112
  class: "action clear",
114
113
  type: "button",
115
- "aria-label": "Clear",
114
+ "aria-label": jlText("clear"),
116
115
  tabIndex: -1,
117
116
  hidden: true,
118
117
  children: /* @__PURE__ */ jsx(JlIcon, {
@@ -123,7 +122,7 @@ var JlCombobox = class extends JlFormControl {
123
122
  /* @__PURE__ */ jsx("button", {
124
123
  class: "action toggle",
125
124
  type: "button",
126
- "aria-label": "Show the options",
125
+ "aria-label": jlText("showOptions"),
127
126
  tabIndex: -1,
128
127
  children: /* @__PURE__ */ jsx(JlIcon, {
129
128
  name: "chevron-down",
@@ -429,7 +428,7 @@ var JlCombobox = class extends JlFormControl {
429
428
  }
430
429
  renderValidity() {
431
430
  if (this.required.get() && this.value.get() === "") {
432
- this.internals.setValidity({ valueMissing: true }, VALUE_MISSING, this._refs.native);
431
+ this.internals.setValidity({ valueMissing: true }, jlText("chooseOption"), this._refs.native);
433
432
  return;
434
433
  }
435
434
  this.internals.setValidity({});
@@ -3,6 +3,7 @@ import { JlElement } from "../base/jl-element.js";
3
3
  import { oneOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
5
  import { CONTROL_SIZES } from "../lib/options.js";
6
+ import { jlText } from "../lib/texts.js";
6
7
  import { JlButton } from "../atoms/jl-button.js";
7
8
  import jl_copy_button_default from "./jl-copy-button.css_raw.js";
8
9
  import { css } from "@jadis/core";
@@ -28,12 +29,12 @@ var JlCopyButton = class extends JlElement {
28
29
  parse: oneOf(CONTROL_SIZES, "sm")
29
30
  });
30
31
  _timer = { id: 0 };
31
- _refs = this.useRefs((ref) => ({ button: ref("jl-button") }));
32
+ _refs = this.useRefs((ref) => ({ button: ref(JlButton) }));
32
33
  templateHtml() {
33
34
  return /* @__PURE__ */ jsx(JlButton, {
34
35
  variant: "ghost",
35
36
  icon: COPY_ICON,
36
- hint: "Copy"
37
+ hint: jlText("copy")
37
38
  });
38
39
  }
39
40
  templateCss() {
@@ -55,12 +56,12 @@ var JlCopyButton = class extends JlElement {
55
56
  confirm() {
56
57
  const button = this._refs.button;
57
58
  button.icon.set(DONE_ICON);
58
- button.hint.set("Copied");
59
+ button.hint.set(jlText("copied"));
59
60
  this.reflect("copied", true);
60
61
  window.clearTimeout(this._timer.id);
61
62
  this._timer.id = window.setTimeout(() => {
62
63
  button.icon.set(COPY_ICON);
63
- button.hint.set("Copy");
64
+ button.hint.set(jlText("copy"));
64
65
  this.reflect("copied", false);
65
66
  }, CONFIRM_MS);
66
67
  }
@@ -4,6 +4,7 @@ import { clampedNumber, oneOf } from "../lib/attributes.js";
4
4
  import { followAnchor } from "../lib/floating.js";
5
5
  import { registerIcons } from "../lib/icons.js";
6
6
  import { CONTROL_SIZES } from "../lib/options.js";
7
+ import { jlText } from "../lib/texts.js";
7
8
  import { JlIcon } from "../atoms/jl-icon.js";
8
9
  import { setOrRemove, uniqueId } from "../lib/dom.js";
9
10
  import { NUMERIC_DATE, compareDates, dateOrder, datePattern, formatDate, isDateText, parseIsoDate, parseTypedDate, toIsoDate } from "../lib/calendar.js";
@@ -24,8 +25,6 @@ var PLACEMENT = "bottom-start";
24
25
  var MONDAY = 1;
25
26
  var SUNDAY = 0;
26
27
  var SATURDAY = 6;
27
- var VALUE_MISSING = "Choose a date.";
28
- var DIALOG_LABEL = "Choose a date";
29
28
  var parseWeekday = (value) => Math.round(clampedNumber(MONDAY, SUNDAY, SATURDAY)(value));
30
29
  /**
31
30
  * 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
@@ -81,7 +80,7 @@ var JlDatePicker = class extends JlFormControl {
81
80
  openOnPress: null
82
81
  };
83
82
  _refs = this.useRefs((ref) => ({
84
- calendar: ref("jl-calendar"),
83
+ calendar: ref(JlCalendar),
85
84
  clear: ref(".clear"),
86
85
  control: ref(".control"),
87
86
  native: ref("input"),
@@ -117,7 +116,7 @@ var JlDatePicker = class extends JlFormControl {
117
116
  /* @__PURE__ */ jsx("button", {
118
117
  class: "action clear",
119
118
  type: "button",
120
- "aria-label": "Clear",
119
+ "aria-label": jlText("clear"),
121
120
  tabIndex: -1,
122
121
  hidden: true,
123
122
  children: /* @__PURE__ */ jsx(JlIcon, {
@@ -128,7 +127,7 @@ var JlDatePicker = class extends JlFormControl {
128
127
  /* @__PURE__ */ jsx("button", {
129
128
  class: "action open",
130
129
  type: "button",
131
- "aria-label": DIALOG_LABEL,
130
+ "aria-label": jlText("chooseDate"),
132
131
  "aria-haspopup": "dialog",
133
132
  "aria-expanded": "false",
134
133
  "aria-controls": this._popupId,
@@ -143,7 +142,7 @@ var JlDatePicker = class extends JlFormControl {
143
142
  id: this._popupId,
144
143
  popover: "auto",
145
144
  role: "dialog",
146
- "aria-label": DIALOG_LABEL,
145
+ "aria-label": jlText("chooseDate"),
147
146
  attrs: { part: "popup" },
148
147
  children: /* @__PURE__ */ jsx(JlCalendar, { class: "calendar" })
149
148
  })] });
@@ -314,7 +313,7 @@ var JlDatePicker = class extends JlFormControl {
314
313
  }
315
314
  renderMissing(anchor) {
316
315
  if (this.required.get()) {
317
- this.internals.setValidity({ valueMissing: true }, VALUE_MISSING, anchor);
316
+ this.internals.setValidity({ valueMissing: true }, jlText("chooseDateMissing"), anchor);
318
317
  return;
319
318
  }
320
319
  this.internals.setValidity({});
@@ -43,7 +43,7 @@ var JlEmpty = class extends JlElement {
43
43
  actions: ref(".actions"),
44
44
  description: ref(".description"),
45
45
  heading: ref(".heading"),
46
- icon: ref("jl-icon"),
46
+ icon: ref(JlIcon),
47
47
  medallion: ref(".medallion"),
48
48
  slot: ref("slot")
49
49
  }));
@@ -0,0 +1,3 @@
1
+ // src/molecules/jl-facts.css?raw
2
+ var jl_facts_default = ":host {\n display: block;\n container-type: inline-size;\n}\n\n:host([empty]) {\n display: none;\n}\n\n.facts {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n gap: 6px 24px;\n margin: 0;\n font-size: var(--jl-font-md);\n}\n\n/* Each pair is a row of the grid: the labels line up in one column, the values in the other. */\n.fact {\n display: contents;\n}\n\ndt {\n color: var(--jl-muted);\n}\n\ndd {\n min-width: 0;\n margin: 0;\n color: var(--jl-text);\n overflow-wrap: anywhere;\n}\n\ncode {\n font-family: var(--jl-font-mono);\n font-size: var(--jl-font-sm);\n word-break: break-all;\n}\n\na {\n color: var(--jl-accent);\n text-decoration: none;\n}\n\na:hover {\n color: var(--jl-accent-strong);\n text-decoration: underline;\n}\n\n:host([orientation=\"vertical\"]) .facts {\n grid-template-columns: repeat(auto-fill, minmax(min(100%, 180px), 1fr));\n gap: 16px 24px;\n}\n\n:host([orientation=\"vertical\"]) .fact {\n display: grid;\n gap: 2px;\n align-content: start;\n}\n\n@container (width < 420px) {\n .facts {\n grid-template-columns: minmax(0, 1fr);\n gap: 12px;\n }\n\n .fact {\n display: grid;\n gap: 2px;\n }\n}\n";
3
+ export { jl_facts_default as default };
@@ -0,0 +1,35 @@
1
+ import { JlElement } from '../base/jl-element';
2
+ import type { Fact, FactRenderer, FactsOrientation } from '../types/jl-facts.types';
3
+ export declare const FACTS_ORIENTATIONS: readonly FactsOrientation[];
4
+ /**
5
+ * The facts about one thing, label and value: an object's size and digest, an order's customer and date. Facts
6
+ * without a value are left out, and an element with none takes no room. Values can be code (monospace, broken
7
+ * anywhere), links, or drawn by code (`renderers`, by a fact's `key`). Narrower than 420 pixels, labels go over
8
+ * their values.
9
+ */
10
+ export declare class JlFacts extends JlElement {
11
+ static readonly selector = "jl-facts";
12
+ readonly items: Readonly<Readonly<{
13
+ get(): Fact[];
14
+ set(setter: Fact[] | ((prevState: Fact[]) => Fact[])): void;
15
+ }>>;
16
+ readonly orientation: Readonly<Readonly<{
17
+ get(): FactsOrientation;
18
+ set(setter: ((prevState: FactsOrientation) => FactsOrientation) | FactsOrientation): void;
19
+ }>>;
20
+ /** Draws the values of some facts by code, by their `key`: `{ type: (fact) => <JlBadge label={…} /> }`. */
21
+ readonly renderers: Readonly<Readonly<{
22
+ get(): Record<string, FactRenderer>;
23
+ set(setter: Record<string, FactRenderer> | ((prevState: Record<string, FactRenderer>) => Record<string, FactRenderer>)): void;
24
+ }>>;
25
+ /** The list's name for screen readers. */
26
+ readonly label: Readonly<Readonly<{
27
+ get(): string;
28
+ set(setter: string | ((prevState: string) => string)): void;
29
+ }>>;
30
+ private readonly _refs;
31
+ templateHtml(): Node;
32
+ templateCss(): string;
33
+ private render;
34
+ private valueNode;
35
+ }
@@ -0,0 +1,64 @@
1
+ import { BASE_STYLES } from "../styles/shared.js";
2
+ import { JlElement } from "../base/jl-element.js";
3
+ import { jsonOf, oneOf } from "../lib/attributes.js";
4
+ import { setOrRemove } from "../lib/dom.js";
5
+ import jl_facts_default from "./jl-facts.css_raw.js";
6
+ import { css } from "@jadis/core";
7
+ import { jsx, jsxs } from "@jadis/core/jsx-runtime";
8
+ // src/molecules/jl-facts.tsx
9
+ var FACTS_ORIENTATIONS = ["horizontal", "vertical"];
10
+ var hasValue = (fact) => fact.value !== null && fact.value !== void 0 && fact.value !== "";
11
+ /**
12
+ * The facts about one thing, label and value: an object's size and digest, an order's customer and date. Facts
13
+ * without a value are left out, and an element with none takes no room. Values can be code (monospace, broken
14
+ * anywhere), links, or drawn by code (`renderers`, by a fact's `key`). Narrower than 420 pixels, labels go over
15
+ * their values.
16
+ */
17
+ var JlFacts = class extends JlElement {
18
+ static selector = "jl-facts";
19
+ items = this.useChange([], () => this.render(), {
20
+ attribute: "items",
21
+ parse: jsonOf([])
22
+ });
23
+ orientation = this.useChange("horizontal", (value) => this.reflect("orientation", value), {
24
+ attribute: "orientation",
25
+ parse: oneOf(FACTS_ORIENTATIONS, "horizontal")
26
+ });
27
+ /** Draws the values of some facts by code, by their `key`: `{ type: (fact) => <JlBadge label={…} /> }`. */
28
+ renderers = this.useChange({}, () => this.render());
29
+ /** The list's name for screen readers. */
30
+ label = this.useChange("", (value) => setOrRemove(this._refs.list, "aria-label", value), { attribute: "label" });
31
+ _refs = this.useRefs((ref) => ({ list: ref("dl") }));
32
+ templateHtml() {
33
+ return /* @__PURE__ */ jsx("dl", {
34
+ class: "facts",
35
+ attrs: { part: "base" }
36
+ });
37
+ }
38
+ templateCss() {
39
+ return css`${BASE_STYLES}${jl_facts_default}`;
40
+ }
41
+ render() {
42
+ const shown = this.items.get().filter(hasValue);
43
+ this._refs.list.replaceChildren(...shown.map((fact) => /* @__PURE__ */ jsxs("div", {
44
+ class: "fact",
45
+ children: [/* @__PURE__ */ jsx("dt", { children: fact.label }), /* @__PURE__ */ jsx("dd", {
46
+ title: fact.hint,
47
+ children: this.valueNode(fact)
48
+ })]
49
+ })));
50
+ this.reflect("empty", shown.length === 0);
51
+ }
52
+ valueNode(fact) {
53
+ const renderer = fact.key === void 0 ? void 0 : this.renderers.get()[fact.key];
54
+ if (renderer !== void 0) return renderer(fact);
55
+ const text = String(fact.value);
56
+ const content = fact.code ? /* @__PURE__ */ jsx("code", { children: text }) : document.createTextNode(text);
57
+ return fact.href === void 0 ? content : /* @__PURE__ */ jsx("a", {
58
+ href: fact.href,
59
+ children: content
60
+ });
61
+ }
62
+ };
63
+ JlFacts.register();
64
+ export { FACTS_ORIENTATIONS, JlFacts };
@@ -61,6 +61,11 @@ export declare class JlFileDrop extends JlFormControl {
61
61
  focus(options?: FocusOptions): void;
62
62
  /** Opens the file chooser. */
63
63
  browse(): void;
64
+ /**
65
+ * Takes files as a drop would, such as files dropped elsewhere on the page: the ones it accepts join the list (or
66
+ * replace the file there) and fire `change`, the others say why not.
67
+ */
68
+ add(files: readonly File[]): void;
64
69
  /** Takes every file back, and empties the list of refused ones. */
65
70
  clear(): void;
66
71
  templateHtml(): Node;
@@ -3,6 +3,7 @@ import { JlFormControl } from "../base/jl-form-control.js";
3
3
  import { clampedNumber, oneOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
5
  import { CONTROL_SIZES } from "../lib/options.js";
6
+ import { jlText } from "../lib/texts.js";
6
7
  import { JlIcon } from "../atoms/jl-icon.js";
7
8
  import { setOrRemove, uniqueId } from "../lib/dom.js";
8
9
  import { pageLocale } from "../lib/locale.js";
@@ -30,15 +31,10 @@ var ALERT_ICON_SIZE = 14;
30
31
  var NO_LIMIT = 0;
31
32
  var FILES_TYPE = "Files";
32
33
  var IMAGE_FAMILY = "image/";
33
- var VALUE_MISSING = "Choose a file.";
34
- var DEFAULT_TEXT = {
35
- multiple: "Drop files here",
36
- single: "Drop a file here"
37
- };
38
34
  var REASONS = {
39
- count: () => "is one too many: the field takes a single file",
40
- size: (rules) => `is larger than ${formatFileSize(rules.maxSize, pageLocale())}`,
41
- type: (rules) => `is not an accepted type (${describeAccept(rules.accept)})`
35
+ count: () => jlText("fileTooMany"),
36
+ size: (rules) => jlText("fileTooLarge", { size: formatFileSize(rules.maxSize, pageLocale()) }),
37
+ type: (rules) => jlText("fileWrongType", { types: describeAccept(rules.accept) })
42
38
  };
43
39
  var hasFiles = (event) => event.dataTransfer?.types.includes(FILES_TYPE) ?? false;
44
40
  /**
@@ -114,6 +110,13 @@ var JlFileDrop = class extends JlFormControl {
114
110
  if (!this.disabled.get()) this._refs.picker.click();
115
111
  });
116
112
  }
113
+ /**
114
+ * Takes files as a drop would, such as files dropped elsewhere on the page: the ones it accepts join the list (or
115
+ * replace the file there) and fire `change`, the others say why not.
116
+ */
117
+ add(files) {
118
+ this.whenRendered(() => this.take([...files]));
119
+ }
117
120
  /** Takes every file back, and empties the list of refused ones. */
118
121
  clear() {
119
122
  this.whenRendered(() => {
@@ -148,7 +151,7 @@ var JlFileDrop = class extends JlFormControl {
148
151
  class: "browse",
149
152
  type: "button",
150
153
  "aria-describedby": this._ids.hint,
151
- children: "Browse"
154
+ children: jlText("browse")
152
155
  })
153
156
  ]
154
157
  }),
@@ -165,7 +168,7 @@ var JlFileDrop = class extends JlFormControl {
165
168
  }),
166
169
  /* @__PURE__ */ jsx("ul", {
167
170
  class: "files",
168
- "aria-label": "Chosen files",
171
+ "aria-label": jlText("chosenFiles"),
169
172
  hidden: true
170
173
  })
171
174
  ] });
@@ -297,7 +300,7 @@ var JlFileDrop = class extends JlFormControl {
297
300
  editable ? /* @__PURE__ */ jsx("button", {
298
301
  class: "file-remove",
299
302
  type: "button",
300
- "aria-label": `Remove ${file.name}`,
303
+ "aria-label": jlText("removeNamed", { name: file.name }),
301
304
  "data-index": String(index),
302
305
  children: /* @__PURE__ */ jsx(JlIcon, {
303
306
  name: "x",
@@ -327,14 +330,14 @@ var JlFileDrop = class extends JlFormControl {
327
330
  }
328
331
  renderText() {
329
332
  const text = this.text.get();
330
- const fallback = this.multiple.get() ? DEFAULT_TEXT.multiple : DEFAULT_TEXT.single;
333
+ const fallback = jlText(this.multiple.get() ? "dropFiles" : "dropFile");
331
334
  this._refs.text.textContent = text === "" ? fallback : text;
332
335
  }
333
336
  /** What it takes, under the text: `PNG, JPG · up to 5 MB`. */
334
337
  renderHint() {
335
338
  const types = describeAccept(this.accept.get());
336
339
  const maxSize = this.maxSize.get();
337
- const hint = [types, maxSize === NO_LIMIT ? "" : `up to ${formatFileSize(maxSize, pageLocale())}`].filter((part) => part !== "").join(" · ");
340
+ const hint = [types, maxSize === NO_LIMIT ? "" : jlText("fileUpTo", { size: formatFileSize(maxSize, pageLocale()) })].filter((part) => part !== "").join(" · ");
338
341
  this._refs.hint.textContent = hint;
339
342
  this._refs.hint.hidden = hint === "";
340
343
  }
@@ -351,7 +354,7 @@ var JlFileDrop = class extends JlFormControl {
351
354
  }
352
355
  renderValidity() {
353
356
  if (this.required.get() && this._state.files.length === 0) {
354
- this.internals.setValidity({ valueMissing: true }, VALUE_MISSING, this._refs.browse);
357
+ this.internals.setValidity({ valueMissing: true }, jlText("chooseFile"), this._refs.browse);
355
358
  return;
356
359
  }
357
360
  this.internals.setValidity({});
@@ -0,0 +1,3 @@
1
+ // src/molecules/jl-input-list.css?raw
2
+ var jl_input_list_default = ":host {\n display: block;\n}\n\n.list,\n.rows {\n display: grid;\n gap: var(--jl-space-2);\n}\n\n.row {\n display: flex;\n align-items: start;\n gap: var(--jl-space-1);\n}\n\n.row > :first-child {\n flex: 1;\n min-width: 0;\n}\n\n[hidden] {\n display: none;\n}\n";
3
+ export { jl_input_list_default as default };
@@ -0,0 +1,79 @@
1
+ import { JlElement } from '../base/jl-element';
2
+ import type { InputType } from '../types/jl-input.types';
3
+ import type { SelectOption } from '../types/jl-select.types';
4
+ /**
5
+ * A field with several values, each in its own row: typed in an input (or a text area when `multiline`), or picked
6
+ * from `options`. People add a row and remove one; `max` caps how many. `values` holds those filled in, in their
7
+ * order, and `change` fires with them. Put it in a `jl-field` for its label, hint and error.
8
+ */
9
+ export declare class JlInputList extends JlElement {
10
+ static readonly selector = "jl-input-list";
11
+ readonly events: Readonly<import("@jadis/core").UseEventsHandler<{
12
+ change: string[];
13
+ }>>;
14
+ /** The values filled in, in their order; rows left empty are not among them. */
15
+ readonly values: Readonly<Readonly<{
16
+ get(): string[];
17
+ set(setter: string[] | ((prevState: string[]) => string[])): void;
18
+ }>>;
19
+ /** The input of each row: text, a number, a date, a link… */
20
+ readonly type: Readonly<Readonly<{
21
+ get(): InputType;
22
+ set(setter: ((prevState: InputType) => InputType) | InputType): void;
23
+ }>>;
24
+ /** Each value on several lines, in a text area. */
25
+ readonly multiline: Readonly<Readonly<{
26
+ get(): boolean;
27
+ set(setter: boolean | ((prevState: boolean) => boolean)): void;
28
+ }>>;
29
+ /** The values to pick from, each row a select; none lets people type. */
30
+ readonly options: Readonly<Readonly<{
31
+ get(): SelectOption[];
32
+ set(setter: SelectOption[] | ((prevState: SelectOption[]) => SelectOption[])): void;
33
+ }>>;
34
+ /** How many values it takes at most; 0 takes any number. */
35
+ readonly max: Readonly<Readonly<{
36
+ get(): number;
37
+ set(setter: number | ((prevState: number) => number)): void;
38
+ }>>;
39
+ readonly placeholder: Readonly<Readonly<{
40
+ get(): string;
41
+ set(setter: string | ((prevState: string) => string)): void;
42
+ }>>;
43
+ /** The name of each row for screen readers: the field's label. */
44
+ readonly label: Readonly<Readonly<{
45
+ get(): string;
46
+ set(setter: string | ((prevState: string) => string)): void;
47
+ }>>;
48
+ readonly disabled: Readonly<Readonly<{
49
+ get(): boolean;
50
+ set(setter: boolean | ((prevState: boolean) => boolean)): void;
51
+ }>>;
52
+ /** The add button's text. */
53
+ readonly addLabel: Readonly<Readonly<{
54
+ get(): string;
55
+ set(setter: string | ((prevState: string) => string)): void;
56
+ }>>;
57
+ /** The text a screen reader says for each row's remove button. */
58
+ readonly removeLabel: Readonly<Readonly<{
59
+ get(): string;
60
+ set(setter: string | ((prevState: string) => string)): void;
61
+ }>>;
62
+ private readonly _state;
63
+ private readonly _refs;
64
+ templateHtml(): Node;
65
+ templateCss(): string;
66
+ onConnect(): void;
67
+ /** Values set from outside start the rows again; those the rows themselves gave change nothing. */
68
+ private follow;
69
+ private renderRows;
70
+ private renderAdd;
71
+ private renderDisabled;
72
+ private row;
73
+ /** The control of a row: a select among the options, a text area, or an input of the type; `onInput` hears it. */
74
+ private control;
75
+ private change;
76
+ private addRow;
77
+ private removeRow;
78
+ private publish;
79
+ }
@@ -0,0 +1,177 @@
1
+ import { BASE_STYLES } from "../styles/shared.js";
2
+ import { JlElement } from "../base/jl-element.js";
3
+ import { clampedNumber, jsonOf, oneOf } from "../lib/attributes.js";
4
+ import { registerIcons } from "../lib/icons.js";
5
+ import { jlText } from "../lib/texts.js";
6
+ import { JlButton } from "../atoms/jl-button.js";
7
+ import { INPUT_TYPES, JlInput } from "../atoms/jl-input.js";
8
+ import { JlSelect } from "../atoms/jl-select.js";
9
+ import { JlTextarea } from "../atoms/jl-textarea.js";
10
+ import { canAddRow, filledValues, rowsOf, sameValues, withRowAt, withoutRowAt } from "../lib/input-list.js";
11
+ import jl_input_list_default from "./jl-input-list.css_raw.js";
12
+ import { css } from "@jadis/core";
13
+ import { Plus, X } from "lucide";
14
+ import { jsx, jsxs } from "@jadis/core/jsx-runtime";
15
+ // src/molecules/jl-input-list.tsx
16
+ registerIcons({
17
+ plus: Plus,
18
+ x: X
19
+ });
20
+ var NO_LIMIT = 0;
21
+ var MAX_LIMIT = 1e3;
22
+ /**
23
+ * A field with several values, each in its own row: typed in an input (or a text area when `multiline`), or picked
24
+ * from `options`. People add a row and remove one; `max` caps how many. `values` holds those filled in, in their
25
+ * order, and `change` fires with them. Put it in a `jl-field` for its label, hint and error.
26
+ */
27
+ var JlInputList = class extends JlElement {
28
+ static selector = "jl-input-list";
29
+ events = this.useEvents();
30
+ /** The values filled in, in their order; rows left empty are not among them. */
31
+ values = this.useChange([], (values) => this.follow(values), {
32
+ attribute: "values",
33
+ parse: jsonOf([])
34
+ });
35
+ /** The input of each row: text, a number, a date, a link… */
36
+ type = this.useChange("text", () => this.renderRows(), {
37
+ attribute: "type",
38
+ parse: oneOf(INPUT_TYPES, "text")
39
+ });
40
+ /** Each value on several lines, in a text area. */
41
+ multiline = this.useChange(false, () => this.renderRows(), { attribute: "multiline" });
42
+ /** The values to pick from, each row a select; none lets people type. */
43
+ options = this.useChange([], () => this.renderRows(), {
44
+ attribute: "options",
45
+ parse: jsonOf([])
46
+ });
47
+ /** How many values it takes at most; 0 takes any number. */
48
+ max = this.useChange(NO_LIMIT, () => this.renderAdd(), {
49
+ attribute: "max",
50
+ parse: clampedNumber(NO_LIMIT, NO_LIMIT, MAX_LIMIT)
51
+ });
52
+ placeholder = this.useChange("", () => this.renderRows(), { attribute: "placeholder" });
53
+ /** The name of each row for screen readers: the field's label. */
54
+ label = this.useChange("", () => this.renderRows(), { attribute: "label" });
55
+ disabled = this.useChange(false, (value) => this.renderDisabled(value), { attribute: "disabled" });
56
+ /** The add button's text. */
57
+ addLabel = this.useChange(jlText("addValue"), () => this.renderAdd(), { attribute: "add-label" });
58
+ /** The text a screen reader says for each row's remove button. */
59
+ removeLabel = this.useChange(jlText("removeValue"), () => this.renderRows(), { attribute: "remove-label" });
60
+ _state = { rows: [""] };
61
+ _refs = this.useRefs((ref) => ({
62
+ add: ref(".add"),
63
+ rows: ref(".rows")
64
+ }));
65
+ templateHtml() {
66
+ return /* @__PURE__ */ jsxs("div", {
67
+ class: "list",
68
+ attrs: { part: "base" },
69
+ children: [/* @__PURE__ */ jsx("div", { class: "rows" }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(JlButton, {
70
+ class: "add",
71
+ variant: "ghost",
72
+ size: "sm",
73
+ icon: "plus"
74
+ }) })]
75
+ });
76
+ }
77
+ templateCss() {
78
+ return css`${BASE_STYLES}${jl_input_list_default}`;
79
+ }
80
+ onConnect() {
81
+ this._refs.add.events.register("press", () => this.addRow());
82
+ }
83
+ /** Values set from outside start the rows again; those the rows themselves gave change nothing. */
84
+ follow(values) {
85
+ if (sameValues(values, filledValues(this._state.rows))) return;
86
+ this._state.rows = rowsOf(values);
87
+ this.renderRows();
88
+ }
89
+ renderRows() {
90
+ this._refs.rows.replaceChildren(...this._state.rows.map((value, index) => this.row(value, index)));
91
+ this.renderAdd();
92
+ }
93
+ renderAdd() {
94
+ const add = this._refs.add;
95
+ add.label.set(this.addLabel.get());
96
+ add.hidden = this.disabled.get() || !canAddRow(this._state.rows.length, this.max.get());
97
+ }
98
+ renderDisabled(disabled) {
99
+ this.reflect("disabled", disabled);
100
+ this.renderRows();
101
+ }
102
+ row(value, index) {
103
+ const control = this.control(value, (text) => this.change(index, text));
104
+ const remove = /* @__PURE__ */ jsx(JlButton, {
105
+ class: "remove",
106
+ variant: "ghost",
107
+ size: "sm",
108
+ icon: "x",
109
+ hint: this.removeLabel.get()
110
+ });
111
+ remove.events.register("press", () => this.removeRow(index));
112
+ remove.hidden = this.disabled.get() || this._state.rows.length === 1;
113
+ return /* @__PURE__ */ jsxs("div", {
114
+ class: "row",
115
+ children: [control, remove]
116
+ });
117
+ }
118
+ /** The control of a row: a select among the options, a text area, or an input of the type; `onInput` hears it. */
119
+ control(value, onInput) {
120
+ const label = this.label.get();
121
+ const disabled = this.disabled.get();
122
+ const options = this.options.get();
123
+ if (options.length > 0) {
124
+ const select = /* @__PURE__ */ jsx(JlSelect, {
125
+ label,
126
+ options,
127
+ disabled
128
+ });
129
+ select.value.set(value);
130
+ select.events.register("change", onInput);
131
+ return select;
132
+ }
133
+ if (this.multiline.get()) {
134
+ const area = /* @__PURE__ */ jsx(JlTextarea, {
135
+ label,
136
+ value,
137
+ placeholder: this.placeholder.get(),
138
+ disabled,
139
+ autosize: true
140
+ });
141
+ area.events.register("input", onInput);
142
+ return area;
143
+ }
144
+ const input = /* @__PURE__ */ jsx(JlInput, {
145
+ label,
146
+ value,
147
+ type: this.type.get(),
148
+ placeholder: this.placeholder.get(),
149
+ disabled
150
+ });
151
+ input.events.register("input", onInput);
152
+ return input;
153
+ }
154
+ change(index, text) {
155
+ this._state.rows = withRowAt(this._state.rows, index, text);
156
+ this.publish();
157
+ }
158
+ addRow() {
159
+ if (!canAddRow(this._state.rows.length, this.max.get())) return;
160
+ this._state.rows = [...this._state.rows, ""];
161
+ this.renderRows();
162
+ const added = this._refs.rows.lastElementChild?.firstElementChild;
163
+ if (added instanceof HTMLElement) added.focus();
164
+ }
165
+ removeRow(index) {
166
+ this._state.rows = withoutRowAt(this._state.rows, index);
167
+ this.renderRows();
168
+ this.publish();
169
+ }
170
+ publish() {
171
+ const values = filledValues(this._state.rows);
172
+ this.values.set(values);
173
+ this.events.emit("change", values);
174
+ }
175
+ };
176
+ JlInputList.register();
177
+ export { JlInputList };
@@ -0,0 +1,3 @@
1
+ // src/molecules/jl-load-more.css?raw
2
+ var jl_load_more_default = ":host {\n display: block;\n}\n\n:host([complete]) {\n display: none;\n}\n\n.more {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n align-items: center;\n justify-content: center;\n padding: 12px 0;\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n}\n";
3
+ export { jl_load_more_default as default };