@viniaraujo68/plinth 0.0.0-stage → 0.3.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 (132) 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 +2 -0
  8. package/dist/attachments/index.js +2 -0
  9. package/dist/attachments/sortable.d.ts +6 -0
  10. package/dist/attachments/sortable.js +227 -0
  11. package/dist/attachments/tooltip.d.ts +37 -0
  12. package/dist/attachments/tooltip.js +166 -0
  13. package/dist/components/AsyncButton.svelte +49 -0
  14. package/dist/components/AsyncButton.svelte.d.ts +32 -0
  15. package/dist/components/Combobox.svelte +380 -0
  16. package/dist/components/Combobox.svelte.d.ts +74 -0
  17. package/dist/components/Copyable.svelte +116 -0
  18. package/dist/components/Copyable.svelte.d.ts +37 -0
  19. package/dist/components/DateRangePicker.svelte +266 -0
  20. package/dist/components/DateRangePicker.svelte.d.ts +66 -0
  21. package/dist/components/Dialog.svelte +102 -0
  22. package/dist/components/Dialog.svelte.d.ts +38 -0
  23. package/dist/components/DragHandle.svelte +26 -0
  24. package/dist/components/DragHandle.svelte.d.ts +7 -0
  25. package/dist/components/Dropdown.svelte +204 -0
  26. package/dist/components/Dropdown.svelte.d.ts +49 -0
  27. package/dist/components/ErrorDisplay.svelte +59 -0
  28. package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
  29. package/dist/components/ErrorPage.svelte +119 -0
  30. package/dist/components/ErrorPage.svelte.d.ts +52 -0
  31. package/dist/components/JsonInput.svelte +106 -0
  32. package/dist/components/JsonInput.svelte.d.ts +40 -0
  33. package/dist/components/Listbox.svelte +257 -0
  34. package/dist/components/Listbox.svelte.d.ts +78 -0
  35. package/dist/components/LoadingButton.svelte +36 -0
  36. package/dist/components/LoadingButton.svelte.d.ts +22 -0
  37. package/dist/components/Logo.svelte +112 -0
  38. package/dist/components/Logo.svelte.d.ts +29 -0
  39. package/dist/components/Modal.svelte +187 -0
  40. package/dist/components/Modal.svelte.d.ts +62 -0
  41. package/dist/components/MultiSelect.svelte +256 -0
  42. package/dist/components/MultiSelect.svelte.d.ts +89 -0
  43. package/dist/components/OptionPanel.svelte +241 -0
  44. package/dist/components/OptionPanel.svelte.d.ts +51 -0
  45. package/dist/components/Paginator.svelte +205 -0
  46. package/dist/components/Paginator.svelte.d.ts +48 -0
  47. package/dist/components/RefreshButton.svelte +246 -0
  48. package/dist/components/RefreshButton.svelte.d.ts +46 -0
  49. package/dist/components/SegmentedControl.svelte +147 -0
  50. package/dist/components/SegmentedControl.svelte.d.ts +71 -0
  51. package/dist/components/Select.svelte +312 -0
  52. package/dist/components/Select.svelte.d.ts +68 -0
  53. package/dist/components/Skeleton.svelte +97 -0
  54. package/dist/components/Skeleton.svelte.d.ts +32 -0
  55. package/dist/components/Tooltip.svelte +60 -0
  56. package/dist/components/Tooltip.svelte.d.ts +58 -0
  57. package/dist/components/date-range.d.ts +101 -0
  58. package/dist/components/date-range.js +144 -0
  59. package/dist/components/index.d.ts +26 -0
  60. package/dist/components/index.js +38 -0
  61. package/dist/components/json-text.d.ts +22 -0
  62. package/dist/components/json-text.js +22 -0
  63. package/dist/components/pick.d.ts +18 -0
  64. package/dist/components/pick.js +18 -0
  65. package/dist/components/segmented.d.ts +7 -0
  66. package/dist/components/segmented.js +1 -0
  67. package/dist/components/select.d.ts +66 -0
  68. package/dist/components/select.js +90 -0
  69. package/dist/confirm/Confirmer.svelte +194 -0
  70. package/dist/confirm/Confirmer.svelte.d.ts +45 -0
  71. package/dist/confirm/challenge.d.ts +16 -0
  72. package/dist/confirm/challenge.js +16 -0
  73. package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
  74. package/dist/confirm/confirm-manager.svelte.js +149 -0
  75. package/dist/confirm/index.d.ts +3 -0
  76. package/dist/confirm/index.js +3 -0
  77. package/dist/error.d.ts +25 -0
  78. package/dist/error.js +39 -0
  79. package/dist/formatters.d.ts +95 -0
  80. package/dist/formatters.js +146 -0
  81. package/dist/http.d.ts +68 -0
  82. package/dist/http.js +138 -0
  83. package/dist/index.d.ts +10 -0
  84. package/dist/index.js +14 -0
  85. package/dist/routing.svelte.d.ts +169 -0
  86. package/dist/routing.svelte.js +208 -0
  87. package/dist/shell/AppShell.svelte +874 -0
  88. package/dist/shell/AppShell.svelte.d.ts +93 -0
  89. package/dist/shell/Breadcrumbs.svelte +78 -0
  90. package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
  91. package/dist/shell/crumbs.d.ts +23 -0
  92. package/dist/shell/crumbs.js +1 -0
  93. package/dist/shell/index.d.ts +3 -0
  94. package/dist/shell/index.js +6 -0
  95. package/dist/table/ColumnMenu.svelte +249 -0
  96. package/dist/table/ColumnMenu.svelte.d.ts +47 -0
  97. package/dist/table/DataTable.svelte +643 -0
  98. package/dist/table/DataTable.svelte.d.ts +153 -0
  99. package/dist/table/columns.d.ts +55 -0
  100. package/dist/table/columns.js +153 -0
  101. package/dist/table/index.d.ts +4 -0
  102. package/dist/table/index.js +8 -0
  103. package/dist/table/sort.d.ts +48 -0
  104. package/dist/table/sort.js +139 -0
  105. package/dist/table/types.d.ts +93 -0
  106. package/dist/table/types.js +1 -0
  107. package/dist/theme/ThemeController.svelte +54 -0
  108. package/dist/theme/ThemeController.svelte.d.ts +27 -0
  109. package/dist/theme/ThemeToggle.svelte +119 -0
  110. package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
  111. package/dist/theme/css-color.d.ts +23 -0
  112. package/dist/theme/css-color.js +31 -0
  113. package/dist/theme/index.d.ts +4 -0
  114. package/dist/theme/index.js +5 -0
  115. package/dist/theme/theme.css +410 -0
  116. package/dist/theme/theme.svelte.d.ts +54 -0
  117. package/dist/theme/theme.svelte.js +58 -0
  118. package/dist/toast/Toaster.svelte +257 -0
  119. package/dist/toast/Toaster.svelte.d.ts +35 -0
  120. package/dist/toast/index.d.ts +2 -0
  121. package/dist/toast/index.js +2 -0
  122. package/dist/toast/toast-manager.svelte.d.ts +99 -0
  123. package/dist/toast/toast-manager.svelte.js +176 -0
  124. package/dist/user/ScopedComponent.svelte +47 -0
  125. package/dist/user/ScopedComponent.svelte.d.ts +29 -0
  126. package/dist/user/anonymous.d.ts +21 -0
  127. package/dist/user/anonymous.js +26 -0
  128. package/dist/user/context.d.ts +55 -0
  129. package/dist/user/context.js +13 -0
  130. package/dist/user/index.d.ts +3 -0
  131. package/dist/user/index.js +6 -0
  132. package/package.json +133 -4
@@ -0,0 +1,153 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ClassValue } from "svelte/elements";
3
+ import type { Column, ColumnState, SortState } from "./types.js";
4
+ declare function $$render<T>(): {
5
+ props: {
6
+ rows: readonly T[];
7
+ columns: Column<T>[];
8
+ /** Identity of a row, so re-sorting moves the existing markup instead of rebuilding it. */
9
+ rowKey: (row: T) => string | number;
10
+ /**
11
+ * The order the table is in. Bindable and defaulted, so it is uncontrolled when passed as a
12
+ * plain value — an initial sort — and controlled the moment a caller writes `bind:sort`.
13
+ */
14
+ sort?: SortState;
15
+ /** Collation locale for string columns. Defaults to the runtime's. */
16
+ locale?: string;
17
+ onRowClick?: (row: T) => void;
18
+ /** Shown in place of the rows when there are none. */
19
+ empty?: Snippet;
20
+ /**
21
+ * Replaces the label/value pairs in card form, for rows that read better as a shape than as a
22
+ * list. Never rendered in table form, so a cell snippet and this one do not compete.
23
+ *
24
+ * The second argument is the row's 0-based position in the order currently rendered, so a
25
+ * rank or a medal can follow the sort without the caller re-deriving it. A snippet declared
26
+ * with one parameter stays assignable and simply ignores it.
27
+ */
28
+ card?: Snippet<[T, number]>;
29
+ /** Accessible name of the table. A prop because the library ships no translations. */
30
+ label?: string;
31
+ /**
32
+ * Accessible name for a sortable header's control, given the column — `Sort by Elevation`
33
+ * rather than `Elevation`. Absent by default, which leaves the button named by its own label.
34
+ * A function, and not a string, because the wording is per column and the library ships no
35
+ * translations.
36
+ */
37
+ sortLabel?: (column: Column<T>) => string;
38
+ /**
39
+ * Goes on the `<table>` itself, next to daisyUI's `table`. Density and zebra striping are
40
+ * `table-sm` and `table-zebra` — classes the framework already has, so they are not re-exposed
41
+ * here as props that could only spell them differently.
42
+ */
43
+ class?: ClassValue;
44
+ /**
45
+ * Rendered in a bar above the table, at its start — the place for the controls that change
46
+ * what the table shows, so they sit with it instead of floating somewhere above it. The
47
+ * columns menu, when there is one, takes the other end of the same bar.
48
+ */
49
+ toolbar?: Snippet;
50
+ /**
51
+ * How the reader arranged the columns: order, visibility and dragged widths. Bindable and
52
+ * defaulted like `sort`: leave it out and the table keeps the arrangement to itself, bind it
53
+ * to read it back or to share it between tables. `undefined` means "as the columns declare".
54
+ */
55
+ columnState?: ColumnState;
56
+ /**
57
+ * Remembers `columnState` in `localStorage` under this key, so the reader's arrangement
58
+ * survives a reload. The stored state is read after the first render, not during it: a table
59
+ * rendered on the server has no storage to read, and changing the markup before hydration
60
+ * finished would tear it — so a stored layout lands one frame late instead. A stored state
61
+ * takes precedence over the `columnState` a parent passes in, since it is the reader's own
62
+ * later choice.
63
+ */
64
+ storageKey?: string | null;
65
+ /**
66
+ * Lets the reader drag a header's right edge to resize its column, double-click the edge to
67
+ * hand the column back to its content, or focus the edge and use the arrow keys. Columns opt
68
+ * out one by one with `resizable: false`; widths only apply to the table form, never to cards.
69
+ */
70
+ resizable?: boolean;
71
+ /**
72
+ * Turns on the columns menu, as the text of its trigger. The menu shows and hides columns and
73
+ * puts them in another order; a prop rather than a default because the library ships no
74
+ * translations, and the menu has nothing to be called without it.
75
+ */
76
+ columnsLabel?: string;
77
+ /** The menu's action that forgets the reader's arrangement, widths included. */
78
+ resetColumnsLabel?: string;
79
+ /** Names the menu's step buttons for a screen reader — "Move Year up" rather than "↑". */
80
+ moveColumnLabel?: (column: Column<T>, direction: "up" | "down") => string;
81
+ /** Names a column's resize handle for a screen reader. */
82
+ resizeLabel?: (column: Column<T>) => string;
83
+ };
84
+ exports: {};
85
+ bindings: "sort" | "columnState";
86
+ slots: {};
87
+ events: {};
88
+ };
89
+ declare class __sveltets_Render<T> {
90
+ props(): ReturnType<typeof $$render<T>>['props'];
91
+ events(): ReturnType<typeof $$render<T>>['events'];
92
+ slots(): ReturnType<typeof $$render<T>>['slots'];
93
+ bindings(): "sort" | "columnState";
94
+ exports(): {};
95
+ }
96
+ interface $$IsomorphicComponent {
97
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
98
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
99
+ } & ReturnType<__sveltets_Render<T>['exports']>;
100
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
101
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
102
+ }
103
+ /**
104
+ * A sortable table that becomes a list of cards on a narrow screen. Columns are described once, as
105
+ * data; the rows are ordered in the browser.
106
+ *
107
+ * Which form renders is decided by a **container query on the table's own wrapper**, not by
108
+ * JavaScript. There is no resize listener and no `matchMedia`, so nothing flashes the wrong layout on
109
+ * the first paint — and because the wrapper is its own container, dropping the table into a narrow
110
+ * box renders its card form on a desktop, which is how the showcase shows both at once.
111
+ *
112
+ * It is one DOM either way. In card form the header row collapses into a strip of sort pills, each
113
+ * cell prints its column's label beside its value, and no row is duplicated to be hidden.
114
+ *
115
+ * Sorting is client-side and stable, so a second sort reads as a tiebreak on the first. Missing
116
+ * values sort last in both directions. `sort` is bindable: pass a value for an initial order, bind
117
+ * for a shared or persisted one.
118
+ *
119
+ * A `cell` or `card` snippet is rendered with the row and, second, the row's 0-based position in the
120
+ * order on screen — the rank column that has to follow the sort is the reason it is there, and
121
+ * re-running `sortRows` outside the table is what it replaces. Snippets that take only the row keep
122
+ * working unchanged.
123
+ *
124
+ * `sortLabel` names the sort control for a screen reader, from the column — "Sort by Elevation"
125
+ * rather than "Elevation" — on the header buttons and on the card-form pills alike, since they are
126
+ * the same element. Left out, the button is named by its own label.
127
+ *
128
+ * The reader can arrange the columns, when the table asks for it. `columnsLabel` adds a menu that
129
+ * shows, hides and reorders them; `resizable` adds a drag handle to every header's edge; `storageKey`
130
+ * remembers both across reloads. All three are off by default, and a column says whether it takes
131
+ * part — `hideable`, `hiddenByDefault`, `resizable`, `minWidth`. The arrangement is one bindable
132
+ * `columnState`, stored as overrides of what the columns declare, so adding a column in a later
133
+ * release neither loses a stored layout nor ends up somewhere nobody put it. Sorting by a column the
134
+ * reader hid keeps working: the order is about the rows, not about what is on screen.
135
+ *
136
+ * It stays short of a data grid — no filtering, no virtualization, no saved views. A table that
137
+ * needs those needs a different component.
138
+ *
139
+ * ```svelte
140
+ * <DataTable
141
+ * {rows}
142
+ * columns={[
143
+ * { key: "name", label: "Name" },
144
+ * { key: "size", label: "Size", numeric: true },
145
+ * ]}
146
+ * rowKey={(row) => row.id}
147
+ * sort={{ key: "size", direction: "desc" }}
148
+ * />
149
+ * ```
150
+ */
151
+ declare const DataTable: $$IsomorphicComponent;
152
+ type DataTable<T> = InstanceType<typeof DataTable<T>>;
153
+ export default DataTable;
@@ -0,0 +1,55 @@
1
+ import type { Column, ColumnState } from "./types.js";
2
+ /**
3
+ * The arrangeable half of a table: which columns show, in what order and at what width. Like
4
+ * `sort.ts` it never touches the DOM, and every change returns a new state rather than editing the
5
+ * one it was given — the state is usually a bound prop, and a mutation in place would change it
6
+ * without the owner hearing about it.
7
+ */
8
+ /** The narrowest a resize can make a column when the column does not say otherwise. */
9
+ export declare const DEFAULT_MIN_WIDTH = 48;
10
+ export declare const emptyColumnState: () => ColumnState;
11
+ export declare const isColumnHideable: <T>(column: Column<T>) => boolean;
12
+ export declare const isColumnResizable: <T>(column: Column<T>) => boolean;
13
+ export declare const minWidthOf: <T>(column: Column<T>) => number;
14
+ /**
15
+ * The columns in the reader's order. Keys the state names that no longer exist are skipped, a key
16
+ * named twice counts once, and a column the state never heard of — added after the state was saved
17
+ * — goes right after the nearest column declared before it, which is where the table's author put
18
+ * it relative to its neighbours.
19
+ */
20
+ export declare const orderColumns: <T>(columns: readonly Column<T>[], state: ColumnState | undefined) => Column<T>[];
21
+ /** A column that cannot be hidden is always shown, whatever a stored state says about it. */
22
+ export declare const isColumnVisible: <T>(column: Column<T>, state: ColumnState | undefined) => boolean;
23
+ export declare const visibleColumns: <T>(columns: readonly Column<T>[], state: ColumnState | undefined) => Column<T>[];
24
+ /**
25
+ * Whether the reader may take this column away. The last visible column is refused: a table with no
26
+ * columns has nothing left to click to bring the others back but the menu, and a header row with
27
+ * no headers looks broken rather than empty.
28
+ */
29
+ export declare const canHideColumn: <T>(columns: readonly Column<T>[], state: ColumnState | undefined, key: string) => boolean;
30
+ /** The width the reader dragged, never below the column's minimum; `undefined` sizes to content. */
31
+ export declare const columnWidth: <T>(column: Column<T>, state: ColumnState | undefined) => number | undefined;
32
+ export declare const setColumnVisible: (state: ColumnState | undefined, key: string, visible: boolean) => ColumnState;
33
+ /**
34
+ * Moves a column to a position in the full order — hidden columns included, since the menu that
35
+ * calls this lists them too. The resulting order names every column, so the move survives a
36
+ * column being added later without the moved one drifting back.
37
+ */
38
+ export declare const moveColumn: <T>(columns: readonly Column<T>[], state: ColumnState | undefined, key: string, to: number) => ColumnState;
39
+ /** `null` forgets the width, which hands the column back to its content. */
40
+ export declare const setColumnWidth: (state: ColumnState | undefined, key: string, width: number | null) => ColumnState;
41
+ /** Whether the state asks for anything other than what the columns declare. */
42
+ export declare const isColumnStateEmpty: (state: ColumnState | undefined) => boolean;
43
+ /**
44
+ * Reads a state back from untrusted JSON — storage an older release wrote, or one a person edited
45
+ * by hand. Entries of the wrong type are dropped one by one instead of failing the whole state, so
46
+ * a single bad width does not cost the reader their column order.
47
+ */
48
+ export declare const parseColumnState: (value: unknown) => ColumnState | undefined;
49
+ /**
50
+ * Storage access throws outright in a sandboxed frame or with site data blocked, and a table that
51
+ * forgets its layout is not worth an exception — both helpers swallow it, as the shell's sidebar
52
+ * does.
53
+ */
54
+ export declare const readColumnState: (storageKey: string) => ColumnState | undefined;
55
+ export declare const writeColumnState: (storageKey: string, state: ColumnState | undefined) => void;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The arrangeable half of a table: which columns show, in what order and at what width. Like
3
+ * `sort.ts` it never touches the DOM, and every change returns a new state rather than editing the
4
+ * one it was given — the state is usually a bound prop, and a mutation in place would change it
5
+ * without the owner hearing about it.
6
+ */
7
+ /** The narrowest a resize can make a column when the column does not say otherwise. */
8
+ export const DEFAULT_MIN_WIDTH = 48;
9
+ /**
10
+ * Bumped whenever the stored shape changes meaning. A stored state from another version is dropped
11
+ * rather than migrated: it is a layout preference, and losing it costs one drag, where reading it
12
+ * wrong could hide a column the reader has no idea is there.
13
+ */
14
+ const STORAGE_VERSION = 1;
15
+ export const emptyColumnState = () => ({ order: [], visibility: {}, widths: {} });
16
+ export const isColumnHideable = (column) => column.hideable !== false;
17
+ export const isColumnResizable = (column) => column.resizable !== false;
18
+ export const minWidthOf = (column) => column.minWidth ?? DEFAULT_MIN_WIDTH;
19
+ /**
20
+ * The columns in the reader's order. Keys the state names that no longer exist are skipped, a key
21
+ * named twice counts once, and a column the state never heard of — added after the state was saved
22
+ * — goes right after the nearest column declared before it, which is where the table's author put
23
+ * it relative to its neighbours.
24
+ */
25
+ export const orderColumns = (columns, state) => {
26
+ const byKey = new Map(columns.map((column) => [column.key, column]));
27
+ const keys = (state?.order ?? []).filter((key, index, all) => byKey.has(key) && all.indexOf(key) === index);
28
+ columns.forEach((column, declared) => {
29
+ if (keys.includes(column.key))
30
+ return;
31
+ let at = 0;
32
+ for (let previous = declared - 1; previous >= 0; previous--) {
33
+ const position = keys.indexOf(columns[previous].key);
34
+ if (position !== -1) {
35
+ at = position + 1;
36
+ break;
37
+ }
38
+ }
39
+ keys.splice(at, 0, column.key);
40
+ });
41
+ return keys.map((key) => byKey.get(key));
42
+ };
43
+ /** A column that cannot be hidden is always shown, whatever a stored state says about it. */
44
+ export const isColumnVisible = (column, state) => {
45
+ if (!isColumnHideable(column))
46
+ return true;
47
+ return state?.visibility[column.key] ?? !column.hiddenByDefault;
48
+ };
49
+ export const visibleColumns = (columns, state) => orderColumns(columns, state).filter((column) => isColumnVisible(column, state));
50
+ /**
51
+ * Whether the reader may take this column away. The last visible column is refused: a table with no
52
+ * columns has nothing left to click to bring the others back but the menu, and a header row with
53
+ * no headers looks broken rather than empty.
54
+ */
55
+ export const canHideColumn = (columns, state, key) => {
56
+ const column = columns.find((candidate) => candidate.key === key);
57
+ if (!column || !isColumnHideable(column) || !isColumnVisible(column, state))
58
+ return false;
59
+ return visibleColumns(columns, state).length > 1;
60
+ };
61
+ /** The width the reader dragged, never below the column's minimum; `undefined` sizes to content. */
62
+ export const columnWidth = (column, state) => {
63
+ const width = state?.widths[column.key];
64
+ if (width === undefined || !isColumnResizable(column))
65
+ return undefined;
66
+ return Math.max(width, minWidthOf(column));
67
+ };
68
+ const withDefaults = (state) => state ?? emptyColumnState();
69
+ export const setColumnVisible = (state, key, visible) => {
70
+ const current = withDefaults(state);
71
+ return { ...current, visibility: { ...current.visibility, [key]: visible } };
72
+ };
73
+ /**
74
+ * Moves a column to a position in the full order — hidden columns included, since the menu that
75
+ * calls this lists them too. The resulting order names every column, so the move survives a
76
+ * column being added later without the moved one drifting back.
77
+ */
78
+ export const moveColumn = (columns, state, key, to) => {
79
+ const keys = orderColumns(columns, state).map((column) => column.key);
80
+ const from = keys.indexOf(key);
81
+ if (from === -1)
82
+ return withDefaults(state);
83
+ keys.splice(from, 1);
84
+ keys.splice(Math.max(0, Math.min(to, keys.length)), 0, key);
85
+ return { ...withDefaults(state), order: keys };
86
+ };
87
+ /** `null` forgets the width, which hands the column back to its content. */
88
+ export const setColumnWidth = (state, key, width) => {
89
+ const current = withDefaults(state);
90
+ const others = Object.fromEntries(Object.entries(current.widths).filter(([column]) => column !== key));
91
+ return {
92
+ ...current,
93
+ widths: width === null ? others : { ...others, [key]: Math.round(width) },
94
+ };
95
+ };
96
+ /** Whether the state asks for anything other than what the columns declare. */
97
+ export const isColumnStateEmpty = (state) => !state ||
98
+ (state.order.length === 0 &&
99
+ Object.keys(state.visibility).length === 0 &&
100
+ Object.keys(state.widths).length === 0);
101
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
102
+ /**
103
+ * Reads a state back from untrusted JSON — storage an older release wrote, or one a person edited
104
+ * by hand. Entries of the wrong type are dropped one by one instead of failing the whole state, so
105
+ * a single bad width does not cost the reader their column order.
106
+ */
107
+ export const parseColumnState = (value) => {
108
+ if (!isRecord(value) || value.v !== STORAGE_VERSION)
109
+ return undefined;
110
+ const order = Array.isArray(value.order)
111
+ ? value.order.filter((key) => typeof key === "string")
112
+ : [];
113
+ const visibility = {};
114
+ if (isRecord(value.visibility))
115
+ for (const [key, shown] of Object.entries(value.visibility))
116
+ if (typeof shown === "boolean")
117
+ visibility[key] = shown;
118
+ const widths = {};
119
+ if (isRecord(value.widths))
120
+ for (const [key, width] of Object.entries(value.widths))
121
+ if (typeof width === "number" && Number.isFinite(width) && width > 0)
122
+ widths[key] = width;
123
+ return { order, visibility, widths };
124
+ };
125
+ /**
126
+ * Storage access throws outright in a sandboxed frame or with site data blocked, and a table that
127
+ * forgets its layout is not worth an exception — both helpers swallow it, as the shell's sidebar
128
+ * does.
129
+ */
130
+ export const readColumnState = (storageKey) => {
131
+ if (typeof localStorage === "undefined")
132
+ return undefined;
133
+ try {
134
+ const raw = localStorage.getItem(storageKey);
135
+ return raw === null ? undefined : parseColumnState(JSON.parse(raw));
136
+ }
137
+ catch {
138
+ return undefined;
139
+ }
140
+ };
141
+ export const writeColumnState = (storageKey, state) => {
142
+ if (typeof localStorage === "undefined")
143
+ return;
144
+ try {
145
+ if (isColumnStateEmpty(state))
146
+ localStorage.removeItem(storageKey);
147
+ else
148
+ localStorage.setItem(storageKey, JSON.stringify({ v: STORAGE_VERSION, ...state }));
149
+ }
150
+ catch {
151
+ // Same sandbox as above.
152
+ }
153
+ };
@@ -0,0 +1,4 @@
1
+ export { default as DataTable } from "./DataTable.svelte";
2
+ export type { Column, ColumnState, SortDirection, SortState, SortValue } from "./types.js";
3
+ export { ariaSort, cellText, cellValue, compareSortValues, competitionRanks, nextSort, sortFromParams, sortRows, sortToParams, sortValue, type SortParamNames, } from "./sort.js";
4
+ export { canHideColumn, columnWidth, emptyColumnState, isColumnStateEmpty, isColumnVisible, moveColumn, orderColumns, parseColumnState, readColumnState, setColumnVisible, setColumnWidth, visibleColumns, writeColumnState, } from "./columns.js";
@@ -0,0 +1,8 @@
1
+ // A sortable table that turns into cards on a narrow screen, whose columns the reader can show,
2
+ // hide, reorder and resize, and the pure functions behind it. Not a data grid: no filtering, no
3
+ // virtualization, no saved views. `@viniaraujo68/plinth/table`.
4
+ export { default as DataTable } from "./DataTable.svelte";
5
+ export { ariaSort, cellText, cellValue, compareSortValues, competitionRanks, nextSort, sortFromParams, sortRows, sortToParams, sortValue, } from "./sort.js";
6
+ // The layout functions the table runs on, for a caller that arranges columns outside the menu —
7
+ // a preset layout, a "show everything" shortcut, or a state kept somewhere other than storage.
8
+ export { canHideColumn, columnWidth, emptyColumnState, isColumnStateEmpty, isColumnVisible, moveColumn, orderColumns, parseColumnState, readColumnState, setColumnVisible, setColumnWidth, visibleColumns, writeColumnState, } from "./columns.js";
@@ -0,0 +1,48 @@
1
+ import type { Column, SortDirection, SortState, SortValue } from "./types.js";
2
+ /** What the cell shows by default, and what it sorts by unless the column says otherwise. */
3
+ export declare const cellValue: <T>(column: Column<T>, row: T) => unknown;
4
+ /** How a cell reads with no snippet to render it, and what a missing value looks like. */
5
+ export declare const cellText: <T>(column: Column<T>, row: T) => string;
6
+ export declare const sortValue: <T>(column: Column<T>, row: T) => SortValue;
7
+ /**
8
+ * Ascending comparison of two present values. Mixed types fall back to a string comparison rather
9
+ * than to `NaN`, which is what a `-` on incomparable operands would produce and what would then
10
+ * make the whole sort order undefined.
11
+ */
12
+ export declare const compareSortValues: (a: SortValue, b: SortValue, locale?: string) => number;
13
+ /**
14
+ * A new array in the requested order. `Array.prototype.sort` is stable by specification, so rows
15
+ * the comparator calls equal keep the order they arrived in — which is what makes a second sort
16
+ * on a different column read as a tiebreak rather than as a shuffle.
17
+ */
18
+ export declare const sortRows: <T>(rows: readonly T[], column: Column<T>, direction: SortDirection, locale?: string) => T[];
19
+ /** What a click on `column`'s header means: flip the column it already sorts, or adopt a new one. */
20
+ export declare const nextSort: <T>(current: SortState | undefined, column: Column<T>) => SortState;
21
+ /** The `aria-sort` of a sortable header. Unsortable headers carry no such attribute at all. */
22
+ export declare const ariaSort: <T>(current: SortState | undefined, column: Column<T>) => "ascending" | "descending" | "none";
23
+ /**
24
+ * Standard competition ranks ("1224") for rows already in display order: rows whose values compare
25
+ * equal share the rank of the first of them, and the next distinct value skips the places the tie
26
+ * used up. A missing value gets no rank at all — it sorts last, and "tied for last because nobody
27
+ * knows" is not a position worth printing.
28
+ *
29
+ * The table hands a `cell` snippet the row's position, which is the right number until two rows
30
+ * tie; a leaderboard that has to say "joint second" needs this instead.
31
+ */
32
+ export declare const competitionRanks: <T>(ordered: readonly T[], valueOf: (row: T) => SortValue, locale?: string) => (number | null)[];
33
+ /** The search parameter names a sort is written under, when the URL has room for more than one. */
34
+ export interface SortParamNames {
35
+ key: string;
36
+ direction: string;
37
+ }
38
+ /**
39
+ * Reads a sort out of a URL's search parameters, so a shared link opens on the order its sender was
40
+ * looking at. Anything missing or unrecognised falls back to `fallback` piece by piece: a link with
41
+ * only `?sort=year` gets the fallback's direction.
42
+ */
43
+ export declare const sortFromParams: (params: URLSearchParams, fallback: SortState, names?: SortParamNames) => SortState;
44
+ /**
45
+ * The search parameters that write `sort` back, as updates for `withSearchParams`: a piece equal to
46
+ * the fallback is `null` — removed — so the URL of a table in its default order stays clean.
47
+ */
48
+ export declare const sortToParams: (sort: SortState, fallback: SortState, names?: SortParamNames) => Record<string, string | null>;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The table's pure half: reading a cell, ordering the rows, and deciding what a header click
3
+ * means. None of it touches the DOM, so it is checked in node rather than in a browser.
4
+ */
5
+ /**
6
+ * A value with no meaningful text of its own stringifies to `[object Object]`, which is what the
7
+ * lint rule below is warning about and what this accepts: such rows read the same, so they compare
8
+ * equal and a stable sort leaves them exactly where they were. Ordering them any other way would
9
+ * mean guessing at a meaning the column never declared — that is what `sortBy` is for.
10
+ */
11
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
12
+ const asText = (value) => String(value);
13
+ /** What the cell shows by default, and what it sorts by unless the column says otherwise. */
14
+ export const cellValue = (column, row) => column.value ? column.value(row) : row[column.key];
15
+ /** How a cell reads with no snippet to render it, and what a missing value looks like. */
16
+ export const cellText = (column, row) => {
17
+ const value = cellValue(column, row);
18
+ if (value === null || value === undefined)
19
+ return "—";
20
+ return asText(value);
21
+ };
22
+ export const sortValue = (column, row) => {
23
+ if (column.sortBy)
24
+ return column.sortBy(row);
25
+ const raw = cellValue(column, row);
26
+ if (raw === null || raw === undefined)
27
+ return raw;
28
+ if (typeof raw === "string" || typeof raw === "number" || raw instanceof Date)
29
+ return raw;
30
+ // Anything else orders the way it renders, which is the only ordering the table can defend
31
+ // without being told what the value means.
32
+ return asText(raw);
33
+ };
34
+ /** A missing value, an unparsed date and a `NaN` are all "no value", and all sort last. */
35
+ const isMissing = (value) => {
36
+ if (value === null || value === undefined)
37
+ return true;
38
+ if (value instanceof Date)
39
+ return Number.isNaN(value.getTime());
40
+ return typeof value === "number" && Number.isNaN(value);
41
+ };
42
+ /**
43
+ * Ascending comparison of two present values. Mixed types fall back to a string comparison rather
44
+ * than to `NaN`, which is what a `-` on incomparable operands would produce and what would then
45
+ * make the whole sort order undefined.
46
+ */
47
+ export const compareSortValues = (a, b, locale) => {
48
+ if (typeof a === "number" && typeof b === "number")
49
+ return a - b;
50
+ if (a instanceof Date && b instanceof Date)
51
+ return a.getTime() - b.getTime();
52
+ // `numeric` so "Row 9" precedes "Row 10"; `base` so case and accents do not decide an order the
53
+ // reader cannot see — equal strings compare 0 and keep their incoming order.
54
+ return String(a).localeCompare(String(b), locale, { numeric: true, sensitivity: "base" });
55
+ };
56
+ /**
57
+ * A new array in the requested order. `Array.prototype.sort` is stable by specification, so rows
58
+ * the comparator calls equal keep the order they arrived in — which is what makes a second sort
59
+ * on a different column read as a tiebreak rather than as a shuffle.
60
+ */
61
+ export const sortRows = (rows, column, direction, locale) => {
62
+ const factor = direction === "asc" ? 1 : -1;
63
+ return [...rows].sort((a, b) => {
64
+ const left = sortValue(column, a);
65
+ const right = sortValue(column, b);
66
+ // Missing values are pushed to the end in both directions, so this test sits outside the
67
+ // direction flip rather than inside the comparison it multiplies.
68
+ const leftMissing = isMissing(left);
69
+ const rightMissing = isMissing(right);
70
+ if (leftMissing && rightMissing)
71
+ return 0;
72
+ if (leftMissing)
73
+ return 1;
74
+ if (rightMissing)
75
+ return -1;
76
+ return factor * compareSortValues(left, right, locale);
77
+ });
78
+ };
79
+ /** What a click on `column`'s header means: flip the column it already sorts, or adopt a new one. */
80
+ export const nextSort = (current, column) => {
81
+ const flipped = current?.direction === "asc" ? "desc" : "asc";
82
+ if (current?.key === column.key)
83
+ return { key: column.key, direction: flipped };
84
+ // A fresh numeric column opens on its largest value: "top by revenue" is the question a number
85
+ // column is almost always there to answer.
86
+ const fallback = column.numeric ? "desc" : "asc";
87
+ return { key: column.key, direction: column.defaultSortDirection ?? fallback };
88
+ };
89
+ /** The `aria-sort` of a sortable header. Unsortable headers carry no such attribute at all. */
90
+ export const ariaSort = (current, column) => {
91
+ if (current?.key !== column.key)
92
+ return "none";
93
+ return current.direction === "asc" ? "ascending" : "descending";
94
+ };
95
+ /**
96
+ * Standard competition ranks ("1224") for rows already in display order: rows whose values compare
97
+ * equal share the rank of the first of them, and the next distinct value skips the places the tie
98
+ * used up. A missing value gets no rank at all — it sorts last, and "tied for last because nobody
99
+ * knows" is not a position worth printing.
100
+ *
101
+ * The table hands a `cell` snippet the row's position, which is the right number until two rows
102
+ * tie; a leaderboard that has to say "joint second" needs this instead.
103
+ */
104
+ export const competitionRanks = (ordered, valueOf, locale) => {
105
+ const ranks = [];
106
+ let previous;
107
+ ordered.forEach((row, index) => {
108
+ const value = valueOf(row);
109
+ if (isMissing(value)) {
110
+ ranks.push(null);
111
+ return;
112
+ }
113
+ const tied = index > 0 && !isMissing(previous) && compareSortValues(previous, value, locale) === 0;
114
+ ranks.push(tied ? ranks[index - 1] : index + 1);
115
+ previous = value;
116
+ });
117
+ return ranks;
118
+ };
119
+ const SORT_PARAMS = { key: "sort", direction: "dir" };
120
+ /**
121
+ * Reads a sort out of a URL's search parameters, so a shared link opens on the order its sender was
122
+ * looking at. Anything missing or unrecognised falls back to `fallback` piece by piece: a link with
123
+ * only `?sort=year` gets the fallback's direction.
124
+ */
125
+ export const sortFromParams = (params, fallback, names = SORT_PARAMS) => {
126
+ const direction = params.get(names.direction);
127
+ return {
128
+ key: params.get(names.key) ?? fallback.key,
129
+ direction: direction === "asc" || direction === "desc" ? direction : fallback.direction,
130
+ };
131
+ };
132
+ /**
133
+ * The search parameters that write `sort` back, as updates for `withSearchParams`: a piece equal to
134
+ * the fallback is `null` — removed — so the URL of a table in its default order stays clean.
135
+ */
136
+ export const sortToParams = (sort, fallback, names = SORT_PARAMS) => ({
137
+ [names.key]: sort.key === fallback.key ? null : sort.key,
138
+ [names.direction]: sort.direction === fallback.direction ? null : sort.direction,
139
+ });
@@ -0,0 +1,93 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ClassValue } from "svelte/elements";
3
+ /**
4
+ * A value the table knows how to order. `null` and `undefined` both mean "missing" and sort to
5
+ * the end in either direction — a blank cell is never the answer to "who is first".
6
+ */
7
+ export type SortValue = string | number | Date | null | undefined;
8
+ export type SortDirection = "asc" | "desc";
9
+ /**
10
+ * Which column the table is ordered by, and how. Absent means "the order the rows arrived in",
11
+ * which is a state the table can start in but never returns to: a header toggles between the two
12
+ * directions rather than cycling through a third, unsorted step.
13
+ */
14
+ export interface SortState {
15
+ key: string;
16
+ direction: SortDirection;
17
+ }
18
+ /**
19
+ * One column of a {@link Column}-driven table.
20
+ *
21
+ * `key` is a plain string rather than `keyof T` on purpose: a ratio, a total, or a pair of fields
22
+ * rendered as one cell has no field to name, and forcing those to alias a real one would be worse
23
+ * than losing the check. When `key` does name a field, everything else has a working default —
24
+ * `{ key: "name", label: "Name" }` reads it, renders it and sorts by it.
25
+ */
26
+ export interface Column<T> {
27
+ /** Identifies the column: keys the header, and is the default field accessor. */
28
+ key: string;
29
+ label: string;
30
+ /** Reads the cell's value. Defaults to `row[key]`. */
31
+ value?: (row: T) => unknown;
32
+ /** Whether the header sorts. On by default — a column that cannot be ordered opts out. */
33
+ sortable?: boolean;
34
+ /**
35
+ * What to order by, when that is not what the cell shows: a formatted date sorts by the
36
+ * `Date`, a status badge by its rank. Defaults to {@link Column.value}.
37
+ */
38
+ sortBy?: (row: T) => SortValue;
39
+ /** Direction the first click picks. Defaults to descending for `numeric`, ascending otherwise. */
40
+ defaultSortDirection?: SortDirection;
41
+ /** Defaults to `right` for `numeric`, `left` otherwise. */
42
+ align?: "left" | "center" | "right";
43
+ /** Renders the column in tabular figures, so digits line up column-wise as the rows change. */
44
+ numeric?: boolean;
45
+ /** Extra classes for this column's cells and header. */
46
+ class?: ClassValue;
47
+ /**
48
+ * Whether the reader can take the column off the table from the columns menu. On by default; a
49
+ * column that the rest of the row makes no sense without — the name a row is about, the rank a
50
+ * reader counts down — opts out, and the menu lists it without a checkbox.
51
+ */
52
+ hideable?: boolean;
53
+ /**
54
+ * Starts the column hidden, so a table can carry a detail nobody needs at a glance without
55
+ * making everyone who does not want it take it away first. Ignored when `hideable` is `false`.
56
+ */
57
+ hiddenByDefault?: boolean;
58
+ /** Whether the header grows a resize handle when the table is `resizable`. On by default. */
59
+ resizable?: boolean;
60
+ /** The narrowest a reader can drag the column, in pixels. Defaults to 48. */
61
+ minWidth?: number;
62
+ /**
63
+ * Renders the cell. Without it the cell is `String(value)`, and `null`/`undefined` is an
64
+ * em dash.
65
+ *
66
+ * The second argument is the row's 0-based position in the order currently rendered, which is
67
+ * what a rank or a medal column follows; it changes with the sort. A snippet declared with only
68
+ * the row parameter stays assignable and simply ignores it.
69
+ *
70
+ * Svelte hoists a template snippet that captures no state to module scope, so a `columns` array
71
+ * declared with `const` can usually name one — but a snippet that reads `$state` is declared
72
+ * where it appears in the template, after the instance script has run. Build the array with
73
+ * `$derived` and the distinction stops mattering.
74
+ */
75
+ cell?: Snippet<[T, number]>;
76
+ }
77
+ /**
78
+ * The part of a table the reader arranges: which columns show, in what order, and how wide the
79
+ * ones they dragged are. Everything is an override of what the columns declare, so a table whose
80
+ * columns change — one added by a later release, one that only exists for some data — reads an old
81
+ * state without losing its own defaults, and an empty state means "as declared".
82
+ */
83
+ export interface ColumnState {
84
+ /**
85
+ * Column keys in the order the reader put them. Keys that no longer name a column are ignored,
86
+ * and a column missing from the list keeps its declared place next to its declared neighbour.
87
+ */
88
+ order: string[];
89
+ /** Per-key shown or hidden, over the column's own `hiddenByDefault`. */
90
+ visibility: Record<string, boolean>;
91
+ /** Widths the reader dragged, in pixels. A column with no entry sizes to its content. */
92
+ widths: Record<string, number>;
93
+ }
@@ -0,0 +1 @@
1
+ export {};