@syware/jlib 0.5.1 → 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 (137) hide show
  1. package/dist/all.js +11 -4
  2. package/dist/atoms/jl-app-icon.css_raw.js +3 -0
  3. package/dist/atoms/jl-app-icon.d.ts +34 -0
  4. package/dist/atoms/jl-app-icon.js +86 -0
  5. package/dist/atoms/jl-avatar.js +1 -1
  6. package/dist/atoms/jl-badge.js +9 -2
  7. package/dist/atoms/jl-button.d.ts +5 -0
  8. package/dist/atoms/jl-button.js +5 -2
  9. package/dist/atoms/jl-chip.js +3 -2
  10. package/dist/atoms/jl-input.js +5 -4
  11. package/dist/atoms/jl-link.js +2 -2
  12. package/dist/atoms/jl-rating.js +4 -5
  13. package/dist/atoms/jl-select.css_raw.js +1 -1
  14. package/dist/atoms/jl-select.d.ts +3 -2
  15. package/dist/atoms/jl-select.js +6 -5
  16. package/dist/atoms/jl-spinner.js +2 -2
  17. package/dist/base/jl-form-control.js +2 -1
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.js +10 -3
  20. package/dist/lib/app-icon.d.ts +8 -0
  21. package/dist/lib/app-icon.js +37 -0
  22. package/dist/lib/dom.d.ts +2 -0
  23. package/dist/lib/dom.js +3 -1
  24. package/dist/lib/download.d.ts +10 -0
  25. package/dist/lib/download.js +62 -0
  26. package/dist/lib/icons.d.ts +4 -0
  27. package/dist/lib/icons.js +7 -4
  28. package/dist/lib/input-list.d.ts +12 -0
  29. package/dist/lib/input-list.js +14 -0
  30. package/dist/lib/internals.d.ts +5 -0
  31. package/dist/lib/internals.js +18 -0
  32. package/dist/lib/palette.js +6 -0
  33. package/dist/lib/resize.d.ts +2 -0
  34. package/dist/lib/resize.js +10 -0
  35. package/dist/lib/table.d.ts +6 -1
  36. package/dist/lib/table.js +16 -1
  37. package/dist/lib/text.d.ts +12 -0
  38. package/dist/lib/text.js +16 -1
  39. package/dist/lib/texts.d.ts +97 -0
  40. package/dist/lib/texts.js +102 -0
  41. package/dist/lib/time.d.ts +5 -3
  42. package/dist/lib/time.js +23 -15
  43. package/dist/lib/tree.d.ts +10 -1
  44. package/dist/lib/tree.js +37 -3
  45. package/dist/lib/trigger.js +1 -1
  46. package/dist/molecules/jl-account-button.css_raw.js +1 -1
  47. package/dist/molecules/jl-account-button.d.ts +8 -1
  48. package/dist/molecules/jl-account-button.js +9 -2
  49. package/dist/molecules/jl-alert.js +3 -2
  50. package/dist/molecules/jl-breadcrumb.js +3 -3
  51. package/dist/molecules/jl-calendar.js +5 -5
  52. package/dist/molecules/jl-chip-group.js +3 -3
  53. package/dist/molecules/jl-code-block.css_raw.js +1 -1
  54. package/dist/molecules/jl-code-block.d.ts +5 -0
  55. package/dist/molecules/jl-code-block.js +5 -2
  56. package/dist/molecules/jl-combobox.js +6 -7
  57. package/dist/molecules/jl-copy-button.js +5 -4
  58. package/dist/molecules/jl-date-picker.js +7 -8
  59. package/dist/molecules/jl-empty.js +2 -2
  60. package/dist/molecules/jl-facts.css_raw.js +3 -0
  61. package/dist/molecules/jl-facts.d.ts +35 -0
  62. package/dist/molecules/jl-facts.js +64 -0
  63. package/dist/molecules/jl-file-drop.d.ts +5 -0
  64. package/dist/molecules/jl-file-drop.js +18 -15
  65. package/dist/molecules/jl-input-list.css_raw.js +3 -0
  66. package/dist/molecules/jl-input-list.d.ts +79 -0
  67. package/dist/molecules/jl-input-list.js +177 -0
  68. package/dist/molecules/jl-list-item.js +1 -1
  69. package/dist/molecules/jl-load-more.css_raw.js +3 -0
  70. package/dist/molecules/jl-load-more.d.ts +41 -0
  71. package/dist/molecules/jl-load-more.js +62 -0
  72. package/dist/molecules/jl-load-state.d.ts +4 -1
  73. package/dist/molecules/jl-load-state.js +20 -12
  74. package/dist/molecules/jl-menu.js +1 -1
  75. package/dist/molecules/jl-number-input.js +5 -5
  76. package/dist/molecules/jl-pagination.js +5 -5
  77. package/dist/molecules/jl-radio-group.js +2 -2
  78. package/dist/molecules/jl-search-button.js +5 -5
  79. package/dist/molecules/jl-search.js +5 -5
  80. package/dist/molecules/jl-segmented.js +3 -2
  81. package/dist/molecules/jl-steps.css_raw.js +1 -1
  82. package/dist/molecules/jl-steps.d.ts +2 -0
  83. package/dist/molecules/jl-steps.js +30 -14
  84. package/dist/molecules/jl-tabs.js +3 -2
  85. package/dist/molecules/jl-tag-input.js +3 -3
  86. package/dist/molecules/jl-timeline.js +1 -1
  87. package/dist/molecules/jl-toast.css_raw.js +1 -1
  88. package/dist/molecules/jl-toast.d.ts +13 -1
  89. package/dist/molecules/jl-toast.js +23 -3
  90. package/dist/molecules/jl-tooltip.d.ts +4 -1
  91. package/dist/molecules/jl-tooltip.js +27 -15
  92. package/dist/organisms/jl-command-palette.css_raw.js +1 -1
  93. package/dist/organisms/jl-command-palette.d.ts +14 -0
  94. package/dist/organisms/jl-command-palette.js +40 -10
  95. package/dist/organisms/jl-confirm.d.ts +2 -1
  96. package/dist/organisms/jl-confirm.js +18 -12
  97. package/dist/organisms/jl-dialog.js +2 -1
  98. package/dist/organisms/jl-drawer.js +2 -1
  99. package/dist/organisms/jl-flow.js +13 -13
  100. package/dist/organisms/jl-navbar.js +6 -6
  101. package/dist/organisms/jl-side-nav.css_raw.js +1 -1
  102. package/dist/organisms/jl-side-nav.d.ts +33 -1
  103. package/dist/organisms/jl-side-nav.js +71 -14
  104. package/dist/organisms/jl-tab-bar.js +3 -3
  105. package/dist/organisms/jl-table.css_raw.js +1 -1
  106. package/dist/organisms/jl-table.d.ts +9 -0
  107. package/dist/organisms/jl-table.js +36 -6
  108. package/dist/organisms/jl-toaster.css_raw.js +1 -1
  109. package/dist/organisms/jl-toaster.d.ts +9 -1
  110. package/dist/organisms/jl-toaster.js +148 -7
  111. package/dist/organisms/jl-tree.css_raw.js +1 -1
  112. package/dist/organisms/jl-tree.d.ts +71 -4
  113. package/dist/organisms/jl-tree.js +303 -40
  114. package/dist/styles/tokens.css_raw.js +1 -1
  115. package/dist/templates/jl-app-shell.css_raw.js +1 -1
  116. package/dist/templates/jl-app-shell.d.ts +16 -1
  117. package/dist/templates/jl-app-shell.js +42 -3
  118. package/dist/templates/jl-auth-layout.css_raw.js +3 -0
  119. package/dist/templates/jl-auth-layout.d.ts +35 -0
  120. package/dist/templates/jl-auth-layout.js +139 -0
  121. package/dist/templates/jl-page.css_raw.js +1 -1
  122. package/dist/templates/jl-page.js +3 -3
  123. package/dist/templates/jl-split-view.css_raw.js +1 -1
  124. package/dist/templates/jl-split-view.d.ts +21 -1
  125. package/dist/templates/jl-split-view.js +37 -4
  126. package/dist/tokens.css +2 -0
  127. package/dist/types/jl-app-icon.types.d.ts +5 -0
  128. package/dist/types/jl-command-palette.types.d.ts +7 -0
  129. package/dist/types/jl-facts.types.d.ts +17 -0
  130. package/dist/types/jl-side-nav.types.d.ts +2 -0
  131. package/dist/types/jl-split-view.types.d.ts +2 -0
  132. package/dist/types/jl-steps.types.d.ts +2 -0
  133. package/dist/types/jl-table.types.d.ts +9 -2
  134. package/dist/types/jl-toaster.types.d.ts +42 -1
  135. package/dist/types/jl-tree.types.d.ts +26 -0
  136. package/dist/types/texts.types.d.ts +5 -0
  137. package/package.json +3 -3
@@ -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,8 +3,9 @@ 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 { setOrRemove, uniqueId } from "../lib/dom.js";
6
+ import { jlText } from "../lib/texts.js";
7
7
  import { JlIcon } from "../atoms/jl-icon.js";
8
+ import { setOrRemove, uniqueId } from "../lib/dom.js";
8
9
  import { pageLocale } from "../lib/locale.js";
9
10
  import { checkFiles, describeAccept, formatFileSize, isSameFile } from "../lib/files.js";
10
11
  import jl_file_drop_default from "./jl-file-drop.css_raw.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 };
@@ -1,9 +1,9 @@
1
1
  import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { registerIcons } from "../lib/icons.js";
4
+ import { JlIcon } from "../atoms/jl-icon.js";
4
5
  import { setOrRemove } from "../lib/dom.js";
5
6
  import { hasSlotted } from "../lib/slots.js";
6
- import { JlIcon } from "../atoms/jl-icon.js";
7
7
  import { isActivationKey, startedOnControl } from "../lib/surface.js";
8
8
  import jl_list_item_default from "./jl-list-item.css_raw.js";
9
9
  import { css } from "@jadis/core";
@@ -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 };
@@ -0,0 +1,41 @@
1
+ import { JlElement } from '../base/jl-element';
2
+ /**
3
+ * The end of a long list read a page at a time: how much of it shows ("30 of 120") and a button that asks for the
4
+ * next page (`more`). It shows the button's spinner while `loading`, and takes no room once the whole list shows.
5
+ */
6
+ export declare class JlLoadMore extends JlElement {
7
+ static readonly selector = "jl-load-more";
8
+ readonly events: Readonly<import("@jadis/core").UseEventsHandler<{
9
+ more: undefined;
10
+ }>>;
11
+ /** How many items the list shows. */
12
+ readonly shown: Readonly<Readonly<{
13
+ get(): number;
14
+ set(setter: number | ((prevState: number) => number)): void;
15
+ }>>;
16
+ /** How many it has in all. */
17
+ readonly total: Readonly<Readonly<{
18
+ get(): number;
19
+ set(setter: number | ((prevState: number) => number)): void;
20
+ }>>;
21
+ /** The next page is on its way: the button shows a spinner and ignores presses. */
22
+ readonly loading: Readonly<Readonly<{
23
+ get(): boolean;
24
+ set(setter: boolean | ((prevState: boolean) => boolean)): void;
25
+ }>>;
26
+ /** The button's text. */
27
+ readonly label: Readonly<Readonly<{
28
+ get(): string;
29
+ set(setter: string | ((prevState: string) => string)): void;
30
+ }>>;
31
+ /** What shows beside it; `{shown}` and `{total}` are filled. */
32
+ readonly countText: Readonly<Readonly<{
33
+ get(): string;
34
+ set(setter: string | ((prevState: string) => string)): void;
35
+ }>>;
36
+ private readonly _refs;
37
+ templateHtml(): Node;
38
+ templateCss(): string;
39
+ onConnect(): void;
40
+ private render;
41
+ }
@@ -0,0 +1,62 @@
1
+ import { BASE_STYLES } from "../styles/shared.js";
2
+ import { JlElement } from "../base/jl-element.js";
3
+ import { fillText } from "../lib/text.js";
4
+ import { jlText } from "../lib/texts.js";
5
+ import { JlButton } from "../atoms/jl-button.js";
6
+ import { pageLocale } from "../lib/locale.js";
7
+ import jl_load_more_default from "./jl-load-more.css_raw.js";
8
+ import { css } from "@jadis/core";
9
+ import { jsx, jsxs } from "@jadis/core/jsx-runtime";
10
+ // src/molecules/jl-load-more.tsx
11
+ /**
12
+ * The end of a long list read a page at a time: how much of it shows ("30 of 120") and a button that asks for the
13
+ * next page (`more`). It shows the button's spinner while `loading`, and takes no room once the whole list shows.
14
+ */
15
+ var JlLoadMore = class extends JlElement {
16
+ static selector = "jl-load-more";
17
+ events = this.useEvents();
18
+ /** How many items the list shows. */
19
+ shown = this.useChange(0, () => this.render(), { attribute: "shown" });
20
+ /** How many it has in all. */
21
+ total = this.useChange(0, () => this.render(), { attribute: "total" });
22
+ /** The next page is on its way: the button shows a spinner and ignores presses. */
23
+ loading = this.useChange(false, (value) => this._refs.button.loading.set(value), { attribute: "loading" });
24
+ /** The button's text. */
25
+ label = this.useChange(jlText("showMore"), (value) => this._refs.button.label.set(value), { attribute: "label" });
26
+ /** What shows beside it; `{shown}` and `{total}` are filled. */
27
+ countText = this.useChange(jlText("shownOfTotal"), () => this.render(), { attribute: "count-text" });
28
+ _refs = this.useRefs((ref) => ({
29
+ button: ref(JlButton),
30
+ count: ref(".count")
31
+ }));
32
+ templateHtml() {
33
+ return /* @__PURE__ */ jsxs("div", {
34
+ class: "more",
35
+ attrs: { part: "base" },
36
+ children: [/* @__PURE__ */ jsx("span", {
37
+ class: "count",
38
+ "aria-live": "polite"
39
+ }), /* @__PURE__ */ jsx(JlButton, { size: "sm" })]
40
+ });
41
+ }
42
+ templateCss() {
43
+ return css`${BASE_STYLES}${jl_load_more_default}`;
44
+ }
45
+ onConnect() {
46
+ this._refs.button.events.register("press", () => {
47
+ if (!this.loading.get()) this.events.emit("more");
48
+ });
49
+ }
50
+ render() {
51
+ const locale = pageLocale();
52
+ const shown = this.shown.get();
53
+ const total = this.total.get();
54
+ this._refs.count.textContent = fillText(this.countText.get(), {
55
+ shown: shown.toLocaleString(locale),
56
+ total: total.toLocaleString(locale)
57
+ });
58
+ this.reflect("complete", shown >= total);
59
+ }
60
+ };
61
+ JlLoadMore.register();
62
+ export { JlLoadMore };
@@ -49,9 +49,12 @@ export declare class JlLoadState extends JlElement {
49
49
  /**
50
50
  * Fetches the data and draws it. The first time, and after a failure, the skeleton shows while it comes; later
51
51
  * (a refresh), what is shown stays until the new data is there, and stays too if it fails. Resolves to whether it
52
- * worked: a refresh that fails says so only here, for the page to tell people.
52
+ * worked: a refresh that fails says so only here, for the page to tell people. A load overtaken by a later one
53
+ * (a search typed on, a poll during a reload) draws nothing and resolves true: the later one decides.
53
54
  */
54
55
  load<Value>(task: LoadTask<Value>): Promise<boolean>;
56
+ /** Whether data is shown (the content or the empty state), which a refresh keeps. */
57
+ private isShowing;
55
58
  templateHtml(): Node;
56
59
  templateCss(): string;
57
60
  onConnect(): void;
@@ -3,6 +3,7 @@ import { JlElement } from "../base/jl-element.js";
3
3
  import { clampedNumber, oneOf } from "../lib/attributes.js";
4
4
  import { define } from "../lib/define.js";
5
5
  import { registerIcons } from "../lib/icons.js";
6
+ import { jlText } from "../lib/texts.js";
6
7
  import { JlButton } from "../atoms/jl-button.js";
7
8
  import { JlSkeleton } from "../atoms/jl-skeleton.js";
8
9
  import { JlEmpty } from "./jl-empty.js";
@@ -28,8 +29,6 @@ var LOAD_SKELETONS = [
28
29
  "lines"
29
30
  ];
30
31
  var DEFAULT_ERROR_HEADING = "This could not be loaded";
31
- var DEFAULT_ERROR_TEXT = "Check the connection, then try again. Nothing was changed.";
32
- var DEFAULT_RETRY_LABEL = "Try again";
33
32
  var DEFAULT_DELAY_MS = 300;
34
33
  var MAX_DELAY_MS = 5e3;
35
34
  var DEFAULT_COUNT = 3;
@@ -95,36 +94,45 @@ var JlLoadState = class extends JlElement {
95
94
  parse: clampedNumber(DEFAULT_DELAY_MS, 0, MAX_DELAY_MS)
96
95
  });
97
96
  errorHeading = this.useChange(DEFAULT_ERROR_HEADING, (value) => this._refs.error.heading.set(value), { attribute: "error-heading" });
98
- errorText = this.useChange(DEFAULT_ERROR_TEXT, (value) => this._refs.error.description.set(value), { attribute: "error-text" });
99
- retryLabel = this.useChange(DEFAULT_RETRY_LABEL, (value) => this._refs.retry.label.set(value), { attribute: "retry-label" });
100
- _work = {};
97
+ errorText = this.useChange(jlText("loadStateError"), (value) => this._refs.error.description.set(value), { attribute: "error-text" });
98
+ retryLabel = this.useChange(jlText("retry"), (value) => this._refs.retry.label.set(value), { attribute: "retry-label" });
99
+ _work = { latest: 0 };
101
100
  _refs = this.useRefs((ref) => ({
102
101
  bones: ref(".bones"),
103
102
  content: ref(".content"),
104
103
  empty: ref(".empty"),
105
- error: ref("jl-empty"),
104
+ error: ref(JlEmpty),
106
105
  loading: ref(".loading"),
107
106
  retry: ref(".retry")
108
107
  }));
109
108
  /**
110
109
  * Fetches the data and draws it. The first time, and after a failure, the skeleton shows while it comes; later
111
110
  * (a refresh), what is shown stays until the new data is there, and stays too if it fails. Resolves to whether it
112
- * worked: a refresh that fails says so only here, for the page to tell people.
111
+ * worked: a refresh that fails says so only here, for the page to tell people. A load overtaken by a later one
112
+ * (a search typed on, a poll during a reload) draws nothing and resolves true: the later one decides.
113
113
  */
114
114
  async load(task) {
115
+ this._work.latest += 1;
116
+ const ticket = this._work.latest;
115
117
  this._work.retry = () => this.load(task);
116
- const showing = this.state.get() === "ready" || this.state.get() === "empty";
117
- if (!showing) this.state.set("loading");
118
+ if (!this.isShowing()) this.state.set("loading");
118
119
  try {
119
120
  const value = await task.fetch();
121
+ if (ticket !== this._work.latest) return true;
120
122
  task.draw(value);
121
123
  this.state.set(task.isEmpty?.(value) === true ? "empty" : "ready");
122
124
  return true;
123
125
  } catch {
124
- if (!showing) this.state.set("error");
126
+ if (ticket !== this._work.latest) return true;
127
+ if (!this.isShowing()) this.state.set("error");
125
128
  return false;
126
129
  }
127
130
  }
131
+ /** Whether data is shown (the content or the empty state), which a refresh keeps. */
132
+ isShowing() {
133
+ const state = this.state.get();
134
+ return state === "ready" || state === "empty";
135
+ }
128
136
  templateHtml() {
129
137
  return /* @__PURE__ */ jsxs("div", {
130
138
  class: "load-state",
@@ -143,12 +151,12 @@ var JlLoadState = class extends JlElement {
143
151
  class: "error",
144
152
  icon: "circle-alert",
145
153
  heading: DEFAULT_ERROR_HEADING,
146
- description: DEFAULT_ERROR_TEXT,
154
+ description: jlText("loadStateError"),
147
155
  hidden: true,
148
156
  children: /* @__PURE__ */ jsx(JlButton, {
149
157
  class: "retry",
150
158
  icon: "refresh",
151
- label: DEFAULT_RETRY_LABEL
159
+ label: jlText("retry")
152
160
  })
153
161
  }),
154
162
  /* @__PURE__ */ jsx("div", {
@@ -2,8 +2,8 @@ import { BASE_STYLES, FLOATING_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 { PLACEMENTS, followAnchor } from "../lib/floating.js";
5
- import { uniqueId } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { uniqueId } from "../lib/dom.js";
7
7
  import { firstIndex, lastIndex, letterIndex, stepIndex } from "../lib/roving.js";
8
8
  import { setTriggerAria } from "../lib/trigger.js";
9
9
  import jl_menu_default from "./jl-menu.css_raw.js";