@viniaraujo68/plinth 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -2
  3. package/dist/anchoring.d.ts +61 -0
  4. package/dist/anchoring.js +151 -0
  5. package/dist/async-action.svelte.d.ts +37 -0
  6. package/dist/async-action.svelte.js +64 -0
  7. package/dist/attachments/index.d.ts +1 -0
  8. package/dist/attachments/index.js +1 -0
  9. package/dist/attachments/tooltip.d.ts +37 -0
  10. package/dist/attachments/tooltip.js +166 -0
  11. package/dist/components/AsyncButton.svelte +49 -0
  12. package/dist/components/AsyncButton.svelte.d.ts +32 -0
  13. package/dist/components/Combobox.svelte +380 -0
  14. package/dist/components/Combobox.svelte.d.ts +74 -0
  15. package/dist/components/Copyable.svelte +116 -0
  16. package/dist/components/Copyable.svelte.d.ts +37 -0
  17. package/dist/components/DateRangePicker.svelte +266 -0
  18. package/dist/components/DateRangePicker.svelte.d.ts +66 -0
  19. package/dist/components/Dialog.svelte +102 -0
  20. package/dist/components/Dialog.svelte.d.ts +38 -0
  21. package/dist/components/Dropdown.svelte +204 -0
  22. package/dist/components/Dropdown.svelte.d.ts +49 -0
  23. package/dist/components/ErrorDisplay.svelte +59 -0
  24. package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
  25. package/dist/components/ErrorPage.svelte +119 -0
  26. package/dist/components/ErrorPage.svelte.d.ts +52 -0
  27. package/dist/components/JsonInput.svelte +106 -0
  28. package/dist/components/JsonInput.svelte.d.ts +40 -0
  29. package/dist/components/Listbox.svelte +257 -0
  30. package/dist/components/Listbox.svelte.d.ts +78 -0
  31. package/dist/components/LoadingButton.svelte +36 -0
  32. package/dist/components/LoadingButton.svelte.d.ts +22 -0
  33. package/dist/components/Logo.svelte +112 -0
  34. package/dist/components/Logo.svelte.d.ts +29 -0
  35. package/dist/components/Modal.svelte +187 -0
  36. package/dist/components/Modal.svelte.d.ts +62 -0
  37. package/dist/components/MultiSelect.svelte +256 -0
  38. package/dist/components/MultiSelect.svelte.d.ts +89 -0
  39. package/dist/components/OptionPanel.svelte +241 -0
  40. package/dist/components/OptionPanel.svelte.d.ts +51 -0
  41. package/dist/components/Paginator.svelte +205 -0
  42. package/dist/components/Paginator.svelte.d.ts +48 -0
  43. package/dist/components/RefreshButton.svelte +246 -0
  44. package/dist/components/RefreshButton.svelte.d.ts +46 -0
  45. package/dist/components/SegmentedControl.svelte +147 -0
  46. package/dist/components/SegmentedControl.svelte.d.ts +71 -0
  47. package/dist/components/Select.svelte +312 -0
  48. package/dist/components/Select.svelte.d.ts +68 -0
  49. package/dist/components/Skeleton.svelte +97 -0
  50. package/dist/components/Skeleton.svelte.d.ts +32 -0
  51. package/dist/components/Tooltip.svelte +60 -0
  52. package/dist/components/Tooltip.svelte.d.ts +58 -0
  53. package/dist/components/date-range.d.ts +101 -0
  54. package/dist/components/date-range.js +144 -0
  55. package/dist/components/index.d.ts +25 -0
  56. package/dist/components/index.js +37 -0
  57. package/dist/components/json-text.d.ts +22 -0
  58. package/dist/components/json-text.js +22 -0
  59. package/dist/components/pick.d.ts +18 -0
  60. package/dist/components/pick.js +18 -0
  61. package/dist/components/segmented.d.ts +7 -0
  62. package/dist/components/segmented.js +1 -0
  63. package/dist/components/select.d.ts +66 -0
  64. package/dist/components/select.js +90 -0
  65. package/dist/confirm/Confirmer.svelte +194 -0
  66. package/dist/confirm/Confirmer.svelte.d.ts +45 -0
  67. package/dist/confirm/challenge.d.ts +16 -0
  68. package/dist/confirm/challenge.js +16 -0
  69. package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
  70. package/dist/confirm/confirm-manager.svelte.js +149 -0
  71. package/dist/confirm/index.d.ts +3 -0
  72. package/dist/confirm/index.js +3 -0
  73. package/dist/error.d.ts +25 -0
  74. package/dist/error.js +39 -0
  75. package/dist/formatters.d.ts +95 -0
  76. package/dist/formatters.js +146 -0
  77. package/dist/http.d.ts +68 -0
  78. package/dist/http.js +138 -0
  79. package/dist/index.d.ts +10 -0
  80. package/dist/index.js +14 -0
  81. package/dist/routing.svelte.d.ts +169 -0
  82. package/dist/routing.svelte.js +208 -0
  83. package/dist/shell/AppShell.svelte +874 -0
  84. package/dist/shell/AppShell.svelte.d.ts +93 -0
  85. package/dist/shell/Breadcrumbs.svelte +78 -0
  86. package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
  87. package/dist/shell/crumbs.d.ts +23 -0
  88. package/dist/shell/crumbs.js +1 -0
  89. package/dist/shell/index.d.ts +3 -0
  90. package/dist/shell/index.js +6 -0
  91. package/dist/table/ColumnMenu.svelte +249 -0
  92. package/dist/table/ColumnMenu.svelte.d.ts +47 -0
  93. package/dist/table/DataTable.svelte +643 -0
  94. package/dist/table/DataTable.svelte.d.ts +153 -0
  95. package/dist/table/columns.d.ts +55 -0
  96. package/dist/table/columns.js +153 -0
  97. package/dist/table/index.d.ts +4 -0
  98. package/dist/table/index.js +8 -0
  99. package/dist/table/sort.d.ts +48 -0
  100. package/dist/table/sort.js +139 -0
  101. package/dist/table/types.d.ts +93 -0
  102. package/dist/table/types.js +1 -0
  103. package/dist/theme/ThemeController.svelte +54 -0
  104. package/dist/theme/ThemeController.svelte.d.ts +27 -0
  105. package/dist/theme/ThemeToggle.svelte +119 -0
  106. package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
  107. package/dist/theme/css-color.d.ts +23 -0
  108. package/dist/theme/css-color.js +31 -0
  109. package/dist/theme/index.d.ts +4 -0
  110. package/dist/theme/index.js +5 -0
  111. package/dist/theme/theme.css +410 -0
  112. package/dist/theme/theme.svelte.d.ts +54 -0
  113. package/dist/theme/theme.svelte.js +58 -0
  114. package/dist/toast/Toaster.svelte +257 -0
  115. package/dist/toast/Toaster.svelte.d.ts +35 -0
  116. package/dist/toast/index.d.ts +2 -0
  117. package/dist/toast/index.js +2 -0
  118. package/dist/toast/toast-manager.svelte.d.ts +99 -0
  119. package/dist/toast/toast-manager.svelte.js +176 -0
  120. package/dist/user/ScopedComponent.svelte +47 -0
  121. package/dist/user/ScopedComponent.svelte.d.ts +29 -0
  122. package/dist/user/anonymous.d.ts +21 -0
  123. package/dist/user/anonymous.js +26 -0
  124. package/dist/user/context.d.ts +55 -0
  125. package/dist/user/context.js +13 -0
  126. package/dist/user/index.d.ts +3 -0
  127. package/dist/user/index.js +6 -0
  128. package/package.json +133 -4
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The pair a range picker emits, and the whole of what it emits.
3
+ *
4
+ * Both ends are ISO-8601 instants in UTC (`2026-08-29T18:04:05.123Z`), the form `toISOString()`
5
+ * produces. A string rather than a `Date` because this value's job is to travel: into a fetch
6
+ * body, a query string, a store, a load function's return — all of which serialise a `Date` to
7
+ * exactly this string anyway, while an ISO string additionally survives the round trip back and
8
+ * sorts correctly with a plain `<`. UTC rather than a local offset because two clients in
9
+ * different zones must be able to name the same window with the same characters.
10
+ */
11
+ export interface DateRange {
12
+ from: string;
13
+ to: string;
14
+ }
15
+ /**
16
+ * One relative window offered as a button — a fixed duration measured back from "now".
17
+ *
18
+ * `durationMs` is a duration and not a calendar step on purpose: "30d" means 30×24h, so the
19
+ * window keeps its length across a DST boundary instead of silently growing or shrinking an hour
20
+ * twice a year. A caller who really wants "this month" wants a custom range, not a preset.
21
+ */
22
+ export interface DateRangePreset {
23
+ /** Stable across renders — it is what the picker's selection prop holds. */
24
+ id: string;
25
+ label: string;
26
+ durationMs: number;
27
+ }
28
+ /**
29
+ * The selection value that means "the user typed the two ends themselves".
30
+ *
31
+ * It lives in the same space as the preset ids, so a preset may not claim it.
32
+ */
33
+ export declare const CUSTOM_PRESET_ID = "custom";
34
+ /** The four windows a dashboard actually gets asked for. Replace the list, not the component. */
35
+ export declare const DEFAULT_PRESETS: readonly DateRangePreset[];
36
+ /** Builds the window a preset names, ending at `now`. */
37
+ export declare const buildRange: (preset: DateRangePreset, now?: Date | number) => DateRange;
38
+ /**
39
+ * Resolves a selection into the window it stands for right now, or `null` when there is nothing
40
+ * to build — an unknown id, or {@link CUSTOM_PRESET_ID}.
41
+ *
42
+ * The initial anchor and every re-anchor tick both go through here, which is what makes "a custom
43
+ * range is never re-anchored" a property of the data rather than of the timer's wiring: even a
44
+ * tick that somehow fired while the user was in custom mode would have nothing to emit.
45
+ */
46
+ export declare const anchorSelection: (selection: string, presets: readonly DateRangePreset[], now?: Date | number) => DateRange | null;
47
+ /** Whether the pair is in order. An empty end is not a range, so it is not valid either. */
48
+ export declare const isValidRange: (from: string | null, to: string | null) => boolean;
49
+ /**
50
+ * Formats a UTC instant for the `value` of an `<input type="datetime-local">`, whose contents are
51
+ * always wall-clock time in the browser's own zone and carry no offset of their own.
52
+ *
53
+ * Two browser traps are handled here. The string may not end in `Z` — appending one, or handing
54
+ * over a sliced `toISOString()`, silently shifts the field by the local offset. And the input's
55
+ * default `step` is 60, so a value carrying seconds is a step mismatch: the field would either
56
+ * refuse it or grow a seconds segment. The instant is therefore truncated down to the minute,
57
+ * which is a real loss of up to 59 seconds — the picker re-emits after the conversion rather than
58
+ * keeping a value it is no longer showing.
59
+ *
60
+ * Returns `null` for anything `Date` cannot parse, so a bad value from an API blanks the field
61
+ * instead of writing `NaN-NaN-NaN` into it.
62
+ */
63
+ export declare const isoToLocalInput: (iso: string | null | undefined) => string | null;
64
+ /**
65
+ * Reads an `<input type="datetime-local">` value back into a UTC ISO instant, interpreting it in
66
+ * the browser's zone.
67
+ *
68
+ * The parse is explicit rather than `new Date(value)`: the platform parser reads a date-only
69
+ * string as UTC and a date-time string as local, a split that turns a half-typed field into an
70
+ * instant a whole offset away. Anything that is not a complete local date and time — which is
71
+ * what a partly filled field reports — is rejected as `null`.
72
+ *
73
+ * A local time that does not exist, the hour a spring-forward skips, is normalised by the
74
+ * platform to the instant just after the jump. That is the same answer the input's own picker
75
+ * gives, so the field and the emitted value stay in agreement.
76
+ */
77
+ export declare const localInputToIso: (local: string | null | undefined) => string | null;
78
+ /** The clock behind a relative range, driven by the picker and stopped by it. */
79
+ export interface ReAnchor {
80
+ /** Starts ticking. Idempotent, and a no-op for a non-positive interval. */
81
+ start: () => void;
82
+ /** Stops ticking and forgets that it was running. */
83
+ stop: () => void;
84
+ /**
85
+ * Reports the document's visibility. Going hidden suspends the interval; coming back fires one
86
+ * anchor immediately, because the range on screen is by then as stale as the tab is old.
87
+ */
88
+ setHidden: (hidden: boolean) => void;
89
+ }
90
+ /**
91
+ * A suspendable repeating anchor.
92
+ *
93
+ * It exists apart from the component for two reasons: the suspend-on-hidden rule is the part
94
+ * most likely to regress and the easiest to test with fake timers, and a background tab whose
95
+ * timers the browser has throttled to once a minute would otherwise fire a burst of catch-up
96
+ * anchors the moment it returns.
97
+ */
98
+ export declare const createReAnchor: (options: {
99
+ intervalMs: number;
100
+ onAnchor: () => void;
101
+ }) => ReAnchor;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The selection value that means "the user typed the two ends themselves".
3
+ *
4
+ * It lives in the same space as the preset ids, so a preset may not claim it.
5
+ */
6
+ export const CUSTOM_PRESET_ID = "custom";
7
+ const MINUTE_MS = 60_000;
8
+ const HOUR_MS = 60 * MINUTE_MS;
9
+ const DAY_MS = 24 * HOUR_MS;
10
+ /** The four windows a dashboard actually gets asked for. Replace the list, not the component. */
11
+ export const DEFAULT_PRESETS = [
12
+ { id: "1h", label: "1h", durationMs: HOUR_MS },
13
+ { id: "24h", label: "24h", durationMs: DAY_MS },
14
+ { id: "7d", label: "7d", durationMs: 7 * DAY_MS },
15
+ { id: "30d", label: "30d", durationMs: 30 * DAY_MS },
16
+ ];
17
+ /** Builds the window a preset names, ending at `now`. */
18
+ export const buildRange = (preset, now = Date.now()) => {
19
+ const end = now instanceof Date ? now.getTime() : now;
20
+ return { from: new Date(end - preset.durationMs).toISOString(), to: new Date(end).toISOString() };
21
+ };
22
+ /**
23
+ * Resolves a selection into the window it stands for right now, or `null` when there is nothing
24
+ * to build — an unknown id, or {@link CUSTOM_PRESET_ID}.
25
+ *
26
+ * The initial anchor and every re-anchor tick both go through here, which is what makes "a custom
27
+ * range is never re-anchored" a property of the data rather than of the timer's wiring: even a
28
+ * tick that somehow fired while the user was in custom mode would have nothing to emit.
29
+ */
30
+ export const anchorSelection = (selection, presets, now = Date.now()) => {
31
+ if (selection === CUSTOM_PRESET_ID)
32
+ return null;
33
+ const preset = presets.find(({ id }) => id === selection);
34
+ return preset ? buildRange(preset, now) : null;
35
+ };
36
+ /** Whether the pair is in order. An empty end is not a range, so it is not valid either. */
37
+ export const isValidRange = (from, to) => {
38
+ if (from === null || to === null)
39
+ return false;
40
+ const start = Date.parse(from);
41
+ const end = Date.parse(to);
42
+ return !Number.isNaN(start) && !Number.isNaN(end) && start <= end;
43
+ };
44
+ const pad = (value, length = 2) => String(value).padStart(length, "0");
45
+ const LOCAL_INPUT_PATTERN = /^(\d{4,6})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
46
+ /**
47
+ * Formats a UTC instant for the `value` of an `<input type="datetime-local">`, whose contents are
48
+ * always wall-clock time in the browser's own zone and carry no offset of their own.
49
+ *
50
+ * Two browser traps are handled here. The string may not end in `Z` — appending one, or handing
51
+ * over a sliced `toISOString()`, silently shifts the field by the local offset. And the input's
52
+ * default `step` is 60, so a value carrying seconds is a step mismatch: the field would either
53
+ * refuse it or grow a seconds segment. The instant is therefore truncated down to the minute,
54
+ * which is a real loss of up to 59 seconds — the picker re-emits after the conversion rather than
55
+ * keeping a value it is no longer showing.
56
+ *
57
+ * Returns `null` for anything `Date` cannot parse, so a bad value from an API blanks the field
58
+ * instead of writing `NaN-NaN-NaN` into it.
59
+ */
60
+ export const isoToLocalInput = (iso) => {
61
+ if (!iso)
62
+ return null;
63
+ const instant = new Date(iso);
64
+ if (Number.isNaN(instant.getTime()))
65
+ return null;
66
+ const date = `${pad(instant.getFullYear(), 4)}-${pad(instant.getMonth() + 1)}-${pad(instant.getDate())}`;
67
+ return `${date}T${pad(instant.getHours())}:${pad(instant.getMinutes())}`;
68
+ };
69
+ /**
70
+ * Reads an `<input type="datetime-local">` value back into a UTC ISO instant, interpreting it in
71
+ * the browser's zone.
72
+ *
73
+ * The parse is explicit rather than `new Date(value)`: the platform parser reads a date-only
74
+ * string as UTC and a date-time string as local, a split that turns a half-typed field into an
75
+ * instant a whole offset away. Anything that is not a complete local date and time — which is
76
+ * what a partly filled field reports — is rejected as `null`.
77
+ *
78
+ * A local time that does not exist, the hour a spring-forward skips, is normalised by the
79
+ * platform to the instant just after the jump. That is the same answer the input's own picker
80
+ * gives, so the field and the emitted value stay in agreement.
81
+ */
82
+ export const localInputToIso = (local) => {
83
+ if (!local)
84
+ return null;
85
+ const match = LOCAL_INPUT_PATTERN.exec(local);
86
+ if (!match)
87
+ return null;
88
+ const [, year, month, day, hours, minutes, seconds = "0"] = match;
89
+ const instant = new Date(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes), Number(seconds));
90
+ // `Date` rolls a nonsensical component over into the next unit -- month 13 becomes January --
91
+ // so the only way to reject "2026-02-31" is to ask what came back out.
92
+ if (Number.isNaN(instant.getTime()) ||
93
+ instant.getMonth() !== Number(month) - 1 ||
94
+ instant.getDate() !== Number(day))
95
+ return null;
96
+ return instant.toISOString();
97
+ };
98
+ /**
99
+ * A suspendable repeating anchor.
100
+ *
101
+ * It exists apart from the component for two reasons: the suspend-on-hidden rule is the part
102
+ * most likely to regress and the easiest to test with fake timers, and a background tab whose
103
+ * timers the browser has throttled to once a minute would otherwise fire a burst of catch-up
104
+ * anchors the moment it returns.
105
+ */
106
+ export const createReAnchor = (options) => {
107
+ let timer;
108
+ let running = false;
109
+ let hidden = false;
110
+ const schedule = () => {
111
+ if (timer !== undefined || !running || hidden || options.intervalMs <= 0)
112
+ return;
113
+ timer = setInterval(options.onAnchor, options.intervalMs);
114
+ };
115
+ const clear = () => {
116
+ if (timer === undefined)
117
+ return;
118
+ clearInterval(timer);
119
+ timer = undefined;
120
+ };
121
+ return {
122
+ start: () => {
123
+ running = true;
124
+ schedule();
125
+ },
126
+ stop: () => {
127
+ running = false;
128
+ clear();
129
+ },
130
+ setHidden: (next) => {
131
+ if (next === hidden)
132
+ return;
133
+ hidden = next;
134
+ if (hidden) {
135
+ clear();
136
+ return;
137
+ }
138
+ if (!running)
139
+ return;
140
+ options.onAnchor();
141
+ schedule();
142
+ },
143
+ };
144
+ };
@@ -0,0 +1,25 @@
1
+ export { default as AsyncButton } from "./AsyncButton.svelte";
2
+ export { default as Combobox } from "./Combobox.svelte";
3
+ export { default as Copyable } from "./Copyable.svelte";
4
+ export { default as DateRangePicker } from "./DateRangePicker.svelte";
5
+ export { default as Dialog } from "./Dialog.svelte";
6
+ export { default as Dropdown } from "./Dropdown.svelte";
7
+ export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
8
+ export { default as ErrorPage } from "./ErrorPage.svelte";
9
+ export { default as JsonInput } from "./JsonInput.svelte";
10
+ export { default as Listbox } from "./Listbox.svelte";
11
+ export { default as LoadingButton } from "./LoadingButton.svelte";
12
+ export { default as Logo } from "./Logo.svelte";
13
+ export { default as Modal } from "./Modal.svelte";
14
+ export { default as MultiSelect } from "./MultiSelect.svelte";
15
+ export { default as Paginator } from "./Paginator.svelte";
16
+ export { default as RefreshButton } from "./RefreshButton.svelte";
17
+ export { default as SegmentedControl } from "./SegmentedControl.svelte";
18
+ export { default as Select } from "./Select.svelte";
19
+ export { default as Skeleton } from "./Skeleton.svelte";
20
+ export { default as Tooltip } from "./Tooltip.svelte";
21
+ export { PICK_CLIP_PATH, PICK_PATH } from "./pick.js";
22
+ export { buildRange, CUSTOM_PRESET_ID, DEFAULT_PRESETS, isoToLocalInput, localInputToIso, type DateRange, type DateRangePreset, } from "./date-range.js";
23
+ export { matchesSelectQuery, normalizeForSearch, type SelectOption } from "./select.js";
24
+ export { formatJsonText, parseJsonText, type JsonTextOptions, type JsonTextResult, } from "./json-text.js";
25
+ export type { SegmentedOption } from "./segmented.js";
@@ -0,0 +1,37 @@
1
+ export { default as AsyncButton } from "./AsyncButton.svelte";
2
+ export { default as Combobox } from "./Combobox.svelte";
3
+ export { default as Copyable } from "./Copyable.svelte";
4
+ export { default as DateRangePicker } from "./DateRangePicker.svelte";
5
+ export { default as Dialog } from "./Dialog.svelte";
6
+ export { default as Dropdown } from "./Dropdown.svelte";
7
+ export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
8
+ export { default as ErrorPage } from "./ErrorPage.svelte";
9
+ export { default as JsonInput } from "./JsonInput.svelte";
10
+ export { default as Listbox } from "./Listbox.svelte";
11
+ export { default as LoadingButton } from "./LoadingButton.svelte";
12
+ export { default as Logo } from "./Logo.svelte";
13
+ export { default as Modal } from "./Modal.svelte";
14
+ export { default as MultiSelect } from "./MultiSelect.svelte";
15
+ export { default as Paginator } from "./Paginator.svelte";
16
+ export { default as RefreshButton } from "./RefreshButton.svelte";
17
+ export { default as SegmentedControl } from "./SegmentedControl.svelte";
18
+ export { default as Select } from "./Select.svelte";
19
+ export { default as Skeleton } from "./Skeleton.svelte";
20
+ export { default as Tooltip } from "./Tooltip.svelte";
21
+ // The mark's outline as data. `Logo` and the shell's avatar already read it from here, and an app
22
+ // that wants the same silhouette -- a placeholder cut to the pick, an avatar of its own -- has no
23
+ // other way to get it than copying the curve out of the library, which is how two versions of one
24
+ // shape start existing.
25
+ export { PICK_CLIP_PATH, PICK_PATH } from "./pick.js";
26
+ // The half of `date-range.js` a consumer of the picker has a use for: the two prop types, the
27
+ // preset list and the reserved id they are written against, and the three conversions anyone
28
+ // pairing their own field with a `DateRange` has to repeat otherwise. The rest -- the selection
29
+ // anchor, the pair validator and the re-anchor clock -- is how the picker drives itself, and the
30
+ // picker never emits a pair that would fail the validator.
31
+ export { buildRange, CUSTOM_PRESET_ID, DEFAULT_PRESETS, isoToLocalInput, localInputToIso, } from "./date-range.js";
32
+ // The half of `select.ts` a consumer has a use for: the option type every caller of either
33
+ // control has to name, and the default matcher, so widening the search -- matching the value too,
34
+ // a synonym list -- can compose with the accent folding instead of reimplementing it. The
35
+ // highlight walk stays private: it is how the two components drive themselves.
36
+ export { matchesSelectQuery, normalizeForSearch } from "./select.js";
37
+ export { formatJsonText, parseJsonText, } from "./json-text.js";
@@ -0,0 +1,22 @@
1
+ /** What reading the text of a JSON field yielded: the parsed value, or why it could not be read. */
2
+ export type JsonTextResult = {
3
+ valid: true;
4
+ data: unknown;
5
+ } | {
6
+ valid: false;
7
+ error: string;
8
+ };
9
+ export interface JsonTextOptions {
10
+ /** Rejects blank text. Off by default, so an optional field left blank reads as `undefined`. */
11
+ required?: boolean;
12
+ /** The error for blank text when `required`. */
13
+ requiredMessage?: string;
14
+ }
15
+ /**
16
+ * Parses the text of a JSON field. Blank text is valid and `undefined`, or invalid when
17
+ * `required`; anything else goes through `JSON.parse`, whose message is carried through as the
18
+ * error, since it names where the document broke.
19
+ */
20
+ export declare const parseJsonText: (text: string, { required, requiredMessage }?: JsonTextOptions) => JsonTextResult;
21
+ /** Pretty-prints the text of a JSON field, or hands it back untouched when it does not parse. */
22
+ export declare const formatJsonText: (text: string, indent?: number) => string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Parses the text of a JSON field. Blank text is valid and `undefined`, or invalid when
3
+ * `required`; anything else goes through `JSON.parse`, whose message is carried through as the
4
+ * error, since it names where the document broke.
5
+ */
6
+ export const parseJsonText = (text, { required = false, requiredMessage = "Required" } = {}) => {
7
+ if (!text.trim())
8
+ return required ? { valid: false, error: requiredMessage } : { valid: true, data: undefined };
9
+ try {
10
+ return { valid: true, data: JSON.parse(text) };
11
+ }
12
+ catch (error) {
13
+ return { valid: false, error: error instanceof Error ? error.message : String(error) };
14
+ }
15
+ };
16
+ /** Pretty-prints the text of a JSON field, or hands it back untouched when it does not parse. */
17
+ export const formatJsonText = (text, indent = 2) => {
18
+ const result = parseJsonText(text);
19
+ if (!result.valid || result.data === undefined)
20
+ return text;
21
+ return JSON.stringify(result.data, null, indent);
22
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The brand mark's outline, stated once. `Logo` strokes it; anything that wants the pick as a
3
+ * silhouette — the shell's avatar, a placeholder cut to the same shape — clips to it. Three
4
+ * copies of the same curve would drift the moment one of them is nudged.
5
+ *
6
+ * Drawn in a 32-unit square, and the curve is the mark's centre line: stroked at 3.5 it reaches
7
+ * 1.75 units past this on every side, so a fill of the same path is the smaller of the two shapes.
8
+ */
9
+ export declare const PICK_PATH = "M16 3.6 C17.8 3.6 19 5 20 7 L25.1 17.2 C26.6 20 27 22 26.3 23.7 C24.7 27 20.9 28.6 16 28.6 C11.1 28.6 7.3 27 5.7 23.7 C5 22 5.4 20 6.9 17.2 L12 7 C13 5 14.2 3.6 16 3.6 Z";
10
+ /**
11
+ * The same outline in the 0..1 space a `<clipPath clipPathUnits="objectBoundingBox">` wants, which
12
+ * is what makes one declaration fit an avatar of any size. CSS `clip-path: path()` cannot: its
13
+ * coordinates are user units and never scale with the box.
14
+ *
15
+ * Scaling every number in the string is only sound because the outline is `M`/`L`/`C` with
16
+ * absolute coordinates — an arc would carry sweep flags in the same stream and come out mangled.
17
+ */
18
+ export declare const PICK_CLIP_PATH: string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The brand mark's outline, stated once. `Logo` strokes it; anything that wants the pick as a
3
+ * silhouette — the shell's avatar, a placeholder cut to the same shape — clips to it. Three
4
+ * copies of the same curve would drift the moment one of them is nudged.
5
+ *
6
+ * Drawn in a 32-unit square, and the curve is the mark's centre line: stroked at 3.5 it reaches
7
+ * 1.75 units past this on every side, so a fill of the same path is the smaller of the two shapes.
8
+ */
9
+ export const PICK_PATH = "M16 3.6 C17.8 3.6 19 5 20 7 L25.1 17.2 C26.6 20 27 22 26.3 23.7 C24.7 27 20.9 28.6 16 28.6 C11.1 28.6 7.3 27 5.7 23.7 C5 22 5.4 20 6.9 17.2 L12 7 C13 5 14.2 3.6 16 3.6 Z";
10
+ /**
11
+ * The same outline in the 0..1 space a `<clipPath clipPathUnits="objectBoundingBox">` wants, which
12
+ * is what makes one declaration fit an avatar of any size. CSS `clip-path: path()` cannot: its
13
+ * coordinates are user units and never scale with the box.
14
+ *
15
+ * Scaling every number in the string is only sound because the outline is `M`/`L`/`C` with
16
+ * absolute coordinates — an arc would carry sweep flags in the same stream and come out mangled.
17
+ */
18
+ export const PICK_CLIP_PATH = PICK_PATH.replace(/[\d.]+/g, (value) => String(Number(value) / 32));
@@ -0,0 +1,7 @@
1
+ /** One choice of a `SegmentedControl`. `id` is the value the control reports. */
2
+ export interface SegmentedOption<Id extends string = string> {
3
+ id: Id;
4
+ label: string;
5
+ /** Shown but not pickable — a view that exists but has nothing to show right now. */
6
+ disabled?: boolean;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * One row of a `Select` or a `Combobox`.
3
+ *
4
+ * `value` is the identity: it is what the control binds, what `onchange` reports, and what the
5
+ * option list is keyed by, so two options may not share one. `label` is what a human reads, what
6
+ * the default matcher searches and what a `Combobox` puts in its text field — a code that only
7
+ * means something to the backend belongs in `value`, never in `label`.
8
+ */
9
+ export interface SelectOption {
10
+ value: string;
11
+ label: string;
12
+ disabled?: boolean;
13
+ }
14
+ /**
15
+ * Strips accents and case so that a search compares the letters a person meant rather than the
16
+ * ones they managed to type.
17
+ *
18
+ * `NFD` splits a precomposed letter into its base plus a combining mark — `á` becomes `a` + U+0301
19
+ * — and removing every mark leaves the base behind. Without it a Portuguese list is close to
20
+ * unsearchable: `Otávio`, `Itaipava` and `São Gonçalo` are all words a user types unaccented, and
21
+ * a plain `includes` finds none of them.
22
+ *
23
+ * `toLowerCase` and not `toLocaleLowerCase`: the locale-aware form maps `I` to `ı` under a Turkish
24
+ * locale, which would make the result depend on the machine the code runs on.
25
+ */
26
+ export declare const normalizeForSearch: (text: string) => string;
27
+ /**
28
+ * The matcher `Combobox` uses when no `filter` is passed: the label contains every word of the
29
+ * query, in any order, accent- and case-insensitive. Punctuation stays significant.
30
+ *
31
+ * Exported so a consumer who only wants to widen it — matching the value too, or a synonym list —
32
+ * can compose rather than reimplement the normalisation.
33
+ */
34
+ export declare const matchesSelectQuery: (option: SelectOption, query: string) => boolean;
35
+ /**
36
+ * The options whose `text` contains every word of the query, under the same folding as
37
+ * {@link matchesSelectQuery}; all of them for a blank query.
38
+ */
39
+ export declare const filterOptions: <T>(options: readonly T[], query: string, text: (option: T) => string) => readonly T[];
40
+ /**
41
+ * The first selectable index from `from`, walking in `delta`'s direction; `-1` when there is none.
42
+ *
43
+ * Both controls move a highlight over the same list under the same rule — a disabled row is
44
+ * stepped over rather than landed on — and the walk stops at the ends instead of wrapping, so
45
+ * holding an arrow key rests against the edge rather than cycling forever.
46
+ */
47
+ export declare const nextSelectableIndex: (options: readonly SelectOption[], from: number, delta: number) => number;
48
+ /** The selectable index at `index`, clamped to the list, else the nearest one after it, else before. */
49
+ export declare const nearestSelectableIndex: (options: readonly SelectOption[], index: number) => number;
50
+ /**
51
+ * The selectable option whose label starts with `typed`, folded, searching from `active` and
52
+ * wrapping around; `-1` when none does. One letter, or the same letter repeated, moves past
53
+ * `active` and so cycles through the labels starting with it; a longer prefix may stay on it.
54
+ */
55
+ export declare const typeaheadMatch: (options: readonly SelectOption[], active: number, typed: string) => number;
56
+ /** Whether a key press types a character: one character long, without Ctrl, Meta or Alt. */
57
+ export declare const isTypedKey: (event: Pick<KeyboardEvent, "key" | "ctrlKey" | "metaKey" | "altKey">) => boolean;
58
+ /** Characters typed within half a second of each other, gathered into one prefix. */
59
+ export declare class Typeahead {
60
+ private typed;
61
+ private at;
62
+ /** Whether a character came in the last half second, so a Space extends the prefix. */
63
+ get typing(): boolean;
64
+ /** Adds a character and returns the prefix so far. */
65
+ add(char: string): string;
66
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Strips accents and case so that a search compares the letters a person meant rather than the
3
+ * ones they managed to type.
4
+ *
5
+ * `NFD` splits a precomposed letter into its base plus a combining mark — `á` becomes `a` + U+0301
6
+ * — and removing every mark leaves the base behind. Without it a Portuguese list is close to
7
+ * unsearchable: `Otávio`, `Itaipava` and `São Gonçalo` are all words a user types unaccented, and
8
+ * a plain `includes` finds none of them.
9
+ *
10
+ * `toLowerCase` and not `toLocaleLowerCase`: the locale-aware form maps `I` to `ı` under a Turkish
11
+ * locale, which would make the result depend on the machine the code runs on.
12
+ */
13
+ export const normalizeForSearch = (text) => text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
14
+ const searchWords = (query) => normalizeForSearch(query).split(/\s+/).filter(Boolean);
15
+ const containsEveryWord = (text, words) => {
16
+ const haystack = normalizeForSearch(text);
17
+ return words.every((word) => haystack.includes(word));
18
+ };
19
+ /**
20
+ * The matcher `Combobox` uses when no `filter` is passed: the label contains every word of the
21
+ * query, in any order, accent- and case-insensitive. Punctuation stays significant.
22
+ *
23
+ * Exported so a consumer who only wants to widen it — matching the value too, or a synonym list —
24
+ * can compose rather than reimplement the normalisation.
25
+ */
26
+ export const matchesSelectQuery = (option, query) => containsEveryWord(option.label, searchWords(query));
27
+ /**
28
+ * The options whose `text` contains every word of the query, under the same folding as
29
+ * {@link matchesSelectQuery}; all of them for a blank query.
30
+ */
31
+ export const filterOptions = (options, query, text) => {
32
+ const words = searchWords(query);
33
+ if (words.length === 0)
34
+ return options;
35
+ return options.filter((option) => containsEveryWord(text(option), words));
36
+ };
37
+ /**
38
+ * The first selectable index from `from`, walking in `delta`'s direction; `-1` when there is none.
39
+ *
40
+ * Both controls move a highlight over the same list under the same rule — a disabled row is
41
+ * stepped over rather than landed on — and the walk stops at the ends instead of wrapping, so
42
+ * holding an arrow key rests against the edge rather than cycling forever.
43
+ */
44
+ export const nextSelectableIndex = (options, from, delta) => {
45
+ for (let index = from; index >= 0 && index < options.length; index += delta)
46
+ if (!options[index].disabled)
47
+ return index;
48
+ return -1;
49
+ };
50
+ /** The selectable index at `index`, clamped to the list, else the nearest one after it, else before. */
51
+ export const nearestSelectableIndex = (options, index) => {
52
+ const from = Math.min(Math.max(index, 0), options.length - 1);
53
+ const after = nextSelectableIndex(options, from, 1);
54
+ return after >= 0 ? after : nextSelectableIndex(options, from, -1);
55
+ };
56
+ /**
57
+ * The selectable option whose label starts with `typed`, folded, searching from `active` and
58
+ * wrapping around; `-1` when none does. One letter, or the same letter repeated, moves past
59
+ * `active` and so cycles through the labels starting with it; a longer prefix may stay on it.
60
+ */
61
+ export const typeaheadMatch = (options, active, typed) => {
62
+ if (options.length === 0 || typed === "")
63
+ return -1;
64
+ const cycling = typed === typed[0].repeat(typed.length);
65
+ const prefix = normalizeForSearch(cycling ? typed[0] : typed);
66
+ for (let offset = cycling ? 1 : 0; offset <= options.length; offset++) {
67
+ const index = (((active + offset) % options.length) + options.length) % options.length;
68
+ const candidate = options[index];
69
+ if (!candidate.disabled && normalizeForSearch(candidate.label).startsWith(prefix))
70
+ return index;
71
+ }
72
+ return -1;
73
+ };
74
+ /** Whether a key press types a character: one character long, without Ctrl, Meta or Alt. */
75
+ export const isTypedKey = (event) => event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
76
+ /** Characters typed within half a second of each other, gathered into one prefix. */
77
+ export class Typeahead {
78
+ typed = "";
79
+ at = -Infinity;
80
+ /** Whether a character came in the last half second, so a Space extends the prefix. */
81
+ get typing() {
82
+ return Date.now() - this.at < 500;
83
+ }
84
+ /** Adds a character and returns the prefix so far. */
85
+ add(char) {
86
+ this.typed = this.typing ? this.typed + char : char;
87
+ this.at = Date.now();
88
+ return this.typed;
89
+ }
90
+ }