@loidolt/theme-svelte 0.4.0 → 0.6.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 (127) hide show
  1. package/README.md +11 -0
  2. package/dist/announcer.svelte.d.ts +36 -0
  3. package/dist/announcer.svelte.js +73 -0
  4. package/dist/attachments.d.ts +83 -0
  5. package/dist/attachments.js +211 -0
  6. package/dist/components/ActiveFilterChips.svelte +80 -0
  7. package/dist/components/ActiveFilterChips.svelte.d.ts +41 -0
  8. package/dist/components/AlertDialog.svelte +4 -4
  9. package/dist/components/AppShell.svelte +9 -2
  10. package/dist/components/AppShell.svelte.d.ts +3 -1
  11. package/dist/components/AspectRatio.svelte +32 -0
  12. package/dist/components/AspectRatio.svelte.d.ts +13 -0
  13. package/dist/components/AudioPlayer.svelte +94 -0
  14. package/dist/components/AudioPlayer.svelte.d.ts +24 -0
  15. package/dist/components/Avatar.svelte +85 -0
  16. package/dist/components/Avatar.svelte.d.ts +31 -0
  17. package/dist/components/Badge.svelte +28 -2
  18. package/dist/components/Badge.svelte.d.ts +13 -1
  19. package/dist/components/Combobox.svelte +180 -0
  20. package/dist/components/Combobox.svelte.d.ts +77 -0
  21. package/dist/components/DataTable.svelte +228 -0
  22. package/dist/components/DataTable.svelte.d.ts +62 -0
  23. package/dist/components/DataTableColumnVisibility.svelte +46 -0
  24. package/dist/components/DataTableColumnVisibility.svelte.d.ts +31 -0
  25. package/dist/components/DataTableFacetedFilter.svelte +87 -0
  26. package/dist/components/DataTableFacetedFilter.svelte.d.ts +40 -0
  27. package/dist/components/DataTablePagination.svelte +68 -0
  28. package/dist/components/DataTablePagination.svelte.d.ts +42 -0
  29. package/dist/components/DataTableSearch.svelte +67 -0
  30. package/dist/components/DataTableSearch.svelte.d.ts +37 -0
  31. package/dist/components/Dialog.svelte +35 -14
  32. package/dist/components/Dialog.svelte.d.ts +7 -0
  33. package/dist/components/Drawer.svelte +3 -3
  34. package/dist/components/DropdownMenu.svelte +95 -28
  35. package/dist/components/DropdownMenu.svelte.d.ts +12 -3
  36. package/dist/components/Field.svelte +2 -3
  37. package/dist/components/Fieldset.svelte +2 -3
  38. package/dist/components/Filmstrip.svelte +10 -3
  39. package/dist/components/FilterPanel.svelte +62 -0
  40. package/dist/components/FilterPanel.svelte.d.ts +22 -0
  41. package/dist/components/Lightbox.svelte +239 -0
  42. package/dist/components/Lightbox.svelte.d.ts +40 -0
  43. package/dist/components/LiveRegion.svelte +48 -0
  44. package/dist/components/LiveRegion.svelte.d.ts +20 -0
  45. package/dist/components/MediaCarousel.svelte +177 -0
  46. package/dist/components/MediaCarousel.svelte.d.ts +32 -0
  47. package/dist/components/MediaEmbed.svelte +116 -0
  48. package/dist/components/MediaEmbed.svelte.d.ts +34 -0
  49. package/dist/components/MediaGrid.svelte +91 -0
  50. package/dist/components/MediaGrid.svelte.d.ts +24 -0
  51. package/dist/components/MultiSelect.svelte +232 -0
  52. package/dist/components/MultiSelect.svelte.d.ts +67 -0
  53. package/dist/components/NumberField.svelte +53 -25
  54. package/dist/components/NumberField.svelte.d.ts +47 -18
  55. package/dist/components/OTPInput.svelte +94 -0
  56. package/dist/components/OTPInput.svelte.d.ts +26 -0
  57. package/dist/components/PasswordInput.svelte +93 -0
  58. package/dist/components/PasswordInput.svelte.d.ts +24 -0
  59. package/dist/components/Progress.svelte +57 -14
  60. package/dist/components/Progress.svelte.d.ts +13 -0
  61. package/dist/components/RadioGroup.svelte +19 -5
  62. package/dist/components/RadioGroup.svelte.d.ts +3 -2
  63. package/dist/components/Select.svelte +15 -5
  64. package/dist/components/Select.svelte.d.ts +3 -2
  65. package/dist/components/SignaturePad.svelte +317 -0
  66. package/dist/components/SignaturePad.svelte.d.ts +48 -0
  67. package/dist/components/Skeleton.svelte +40 -9
  68. package/dist/components/Skeleton.svelte.d.ts +8 -0
  69. package/dist/components/SkipLink.svelte +48 -0
  70. package/dist/components/SkipLink.svelte.d.ts +14 -0
  71. package/dist/components/Slider.svelte +148 -0
  72. package/dist/components/Slider.svelte.d.ts +55 -0
  73. package/dist/components/Spinner.svelte +11 -2
  74. package/dist/components/Spinner.svelte.d.ts +3 -0
  75. package/dist/components/Tabs.svelte.d.ts +1 -1
  76. package/dist/components/Thumbnail.svelte +52 -6
  77. package/dist/components/Thumbnail.svelte.d.ts +14 -2
  78. package/dist/components/Toast.svelte +24 -0
  79. package/dist/components/Toast.svelte.d.ts +10 -0
  80. package/dist/components/ToastViewport.svelte +10 -1
  81. package/dist/components/ToastViewport.svelte.d.ts +2 -0
  82. package/dist/components/Toolbar.svelte +49 -0
  83. package/dist/components/Toolbar.svelte.d.ts +23 -0
  84. package/dist/components/Tooltip.svelte +9 -1
  85. package/dist/components/Tooltip.svelte.d.ts +2 -0
  86. package/dist/components/VideoPlayer.svelte +136 -0
  87. package/dist/components/VideoPlayer.svelte.d.ts +38 -0
  88. package/dist/data-table-utils.d.ts +62 -0
  89. package/dist/data-table-utils.js +91 -0
  90. package/dist/data-table.svelte.d.ts +132 -0
  91. package/dist/data-table.svelte.js +292 -0
  92. package/dist/hls.d.ts +35 -0
  93. package/dist/hls.js +37 -0
  94. package/dist/hls.svelte.d.ts +22 -0
  95. package/dist/hls.svelte.js +66 -0
  96. package/dist/index.d.ts +47 -4
  97. package/dist/index.js +35 -2
  98. package/dist/internal/EditableCell.svelte +58 -0
  99. package/dist/internal/EditableCell.svelte.d.ts +8 -0
  100. package/dist/internal/ListboxEntries.svelte +49 -0
  101. package/dist/internal/ListboxEntries.svelte.d.ts +35 -0
  102. package/dist/internal/media/MediaControls.svelte +163 -0
  103. package/dist/internal/media/MediaControls.svelte.d.ts +12 -0
  104. package/dist/internal/media/MediaStage.svelte +49 -0
  105. package/dist/internal/media/MediaStage.svelte.d.ts +11 -0
  106. package/dist/internal/media/MediaTile.svelte +36 -0
  107. package/dist/internal/media/MediaTile.svelte.d.ts +11 -0
  108. package/dist/internal/media/keys.d.ts +7 -0
  109. package/dist/internal/media/keys.js +51 -0
  110. package/dist/internal/media/labels.d.ts +19 -0
  111. package/dist/internal/media/labels.js +15 -0
  112. package/dist/internal/options.d.ts +9 -0
  113. package/dist/internal/options.js +20 -0
  114. package/dist/media-player.svelte.d.ts +61 -0
  115. package/dist/media-player.svelte.js +246 -0
  116. package/dist/media-utils.d.ts +112 -0
  117. package/dist/media-utils.js +205 -0
  118. package/dist/media-zoom.svelte.d.ts +33 -0
  119. package/dist/media-zoom.svelte.js +126 -0
  120. package/dist/signature.d.ts +55 -0
  121. package/dist/signature.js +52 -0
  122. package/dist/toaster.svelte.d.ts +31 -2
  123. package/dist/toaster.svelte.js +30 -2
  124. package/dist/types.d.ts +62 -3
  125. package/dist/utils.d.ts +9 -0
  126. package/dist/utils.js +11 -0
  127. package/package.json +10 -4
@@ -0,0 +1,62 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ColumnAlign, Option } from './types.js';
3
+ /** A column filter: text is matched as a substring, a list as a set of accepted values. */
4
+ export type DataTableFilterValue = string | string[];
5
+ export interface DataTableColumn<T> {
6
+ /** Unique within the table; the key for sorting, filtering and visibility. */
7
+ id: string;
8
+ /** Column heading, and the column's name in menus. */
9
+ header: string;
10
+ /** Where the value comes from. Defaults to `row[id]`. */
11
+ accessor?: keyof T | ((row: T) => unknown);
12
+ sortable?: boolean;
13
+ /** Custom order for this column. Defaults to numbers, then dates, then natural text order. */
14
+ compare?: (a: T, b: T) => number;
15
+ align?: ColumnAlign;
16
+ /** A CSS width for the column. */
17
+ width?: string;
18
+ /** Offered in the column-visibility menu. Defaults to `true`. */
19
+ hideable?: boolean;
20
+ /** Included in the global search. Defaults to `true`. */
21
+ searchable?: boolean;
22
+ /**
23
+ * Hide the column at and below a breakpoint, so a narrow screen keeps the columns that matter.
24
+ * The data stays searchable and sortable.
25
+ */
26
+ hideBelow?: 'compact' | 'expanded';
27
+ /** Display text. Defaults to the value as text, with dates as local dates. */
28
+ format?: (value: unknown, row: T) => string;
29
+ /** Custom filter test. Defaults to a substring match for text and set membership for lists. */
30
+ filter?: (row: T, value: DataTableFilterValue) => boolean;
31
+ /** Choices for a faceted filter. Defaults to the distinct values present in the data. */
32
+ filterOptions?: Option[];
33
+ /** Custom cell content. Receives the row's data. */
34
+ cell?: Snippet<[T]>;
35
+ /** Allow editing the cell in place. `DataTable` reports edits through `onCellEdit`. */
36
+ editable?: boolean;
37
+ }
38
+ /** The value a column reads from a row. */
39
+ export declare function cellValue<T>(row: T, column: DataTableColumn<T>): unknown;
40
+ /** The text a cell shows. */
41
+ export declare function formatCell<T>(row: T, column: DataTableColumn<T>): string;
42
+ /**
43
+ * Ascending order for two cell values: numbers numerically, dates by time, everything else in
44
+ * natural text order ("Sheet 2" before "Sheet 10"). Empty values always sort last, whichever
45
+ * the direction, so they never crowd the top of a list.
46
+ */
47
+ export declare function compareValues(a: unknown, b: unknown): number;
48
+ /**
49
+ * A comparator for one column. `direction` 1 is ascending, -1 descending; empty values stay at
50
+ * the end either way.
51
+ */
52
+ export declare function rowComparator<T>(column: DataTableColumn<T>, direction: 1 | -1): (a: T, b: T) => number;
53
+ /** Sorts a copy of `rows` by one column. */
54
+ export declare function sortRows<T>(rows: readonly T[], column: DataTableColumn<T>, direction: 1 | -1): T[];
55
+ /** Whether a filter is set to anything at all. */
56
+ export declare const isActiveFilter: (value: DataTableFilterValue | null | undefined) => boolean;
57
+ /** The default column filter: a case-insensitive substring for text, membership for a list. */
58
+ export declare function matchesFilter<T>(row: T, column: DataTableColumn<T>, value: DataTableFilterValue): boolean;
59
+ /** Whether any searchable column's text contains `query`. */
60
+ export declare function matchesSearch<T>(row: T, columns: readonly DataTableColumn<T>[], query: string): boolean;
61
+ /** How many rows hold each distinct value of a column — the counts beside a faceted filter. */
62
+ export declare function countValues<T>(rows: readonly T[], column: DataTableColumn<T>): Map<string, number>;
@@ -0,0 +1,91 @@
1
+ /** The value a column reads from a row. */
2
+ export function cellValue(row, column) {
3
+ const { accessor, id } = column;
4
+ if (typeof accessor === 'function')
5
+ return accessor(row);
6
+ return row[(accessor ?? id)];
7
+ }
8
+ /** The text a cell shows. */
9
+ export function formatCell(row, column) {
10
+ const value = cellValue(row, column);
11
+ if (column.format)
12
+ return column.format(value, row);
13
+ if (value == null)
14
+ return '';
15
+ if (value instanceof Date)
16
+ return Number.isNaN(value.getTime()) ? '' : value.toLocaleDateString();
17
+ if (Array.isArray(value))
18
+ return value.join(', ');
19
+ return String(value);
20
+ }
21
+ const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
22
+ /**
23
+ * Ascending order for two cell values: numbers numerically, dates by time, everything else in
24
+ * natural text order ("Sheet 2" before "Sheet 10"). Empty values always sort last, whichever
25
+ * the direction, so they never crowd the top of a list.
26
+ */
27
+ export function compareValues(a, b) {
28
+ if (typeof a === 'number' && typeof b === 'number')
29
+ return a - b;
30
+ if (a instanceof Date && b instanceof Date)
31
+ return a.getTime() - b.getTime();
32
+ if (typeof a === 'boolean' && typeof b === 'boolean')
33
+ return Number(a) - Number(b);
34
+ return collator.compare(String(a), String(b));
35
+ }
36
+ const isEmpty = (value) => value == null || value === '' || (value instanceof Date && Number.isNaN(value.getTime()));
37
+ /**
38
+ * A comparator for one column. `direction` 1 is ascending, -1 descending; empty values stay at
39
+ * the end either way.
40
+ */
41
+ export function rowComparator(column, direction) {
42
+ return (a, b) => {
43
+ if (column.compare)
44
+ return column.compare(a, b) * direction;
45
+ const [left, right] = [cellValue(a, column), cellValue(b, column)];
46
+ const [leftEmpty, rightEmpty] = [isEmpty(left), isEmpty(right)];
47
+ if (leftEmpty || rightEmpty)
48
+ return Number(leftEmpty) - Number(rightEmpty);
49
+ return compareValues(left, right) * direction;
50
+ };
51
+ }
52
+ /** Sorts a copy of `rows` by one column. */
53
+ export function sortRows(rows, column, direction) {
54
+ return [...rows].sort(rowComparator(column, direction));
55
+ }
56
+ /** Whether a filter is set to anything at all. */
57
+ export const isActiveFilter = (value) => value != null && (Array.isArray(value) ? value.length > 0 : value.trim() !== '');
58
+ /** The default column filter: a case-insensitive substring for text, membership for a list. */
59
+ export function matchesFilter(row, column, value) {
60
+ if (column.filter)
61
+ return column.filter(row, value);
62
+ const raw = cellValue(row, column);
63
+ if (Array.isArray(value)) {
64
+ const accepted = new Set(value);
65
+ return Array.isArray(raw)
66
+ ? raw.some((item) => accepted.has(String(item)))
67
+ : accepted.has(String(raw ?? ''));
68
+ }
69
+ return formatCell(row, column).toLocaleLowerCase().includes(value.trim().toLocaleLowerCase());
70
+ }
71
+ /** Whether any searchable column's text contains `query`. */
72
+ export function matchesSearch(row, columns, query) {
73
+ const needle = query.trim().toLocaleLowerCase();
74
+ if (!needle)
75
+ return true;
76
+ return columns.some((column) => column.searchable !== false && formatCell(row, column).toLocaleLowerCase().includes(needle));
77
+ }
78
+ /** How many rows hold each distinct value of a column — the counts beside a faceted filter. */
79
+ export function countValues(rows, column) {
80
+ const counts = new Map();
81
+ for (const row of rows) {
82
+ const raw = cellValue(row, column);
83
+ for (const item of Array.isArray(raw) ? raw : [raw]) {
84
+ if (isEmpty(item))
85
+ continue;
86
+ const key = String(item);
87
+ counts.set(key, (counts.get(key) ?? 0) + 1);
88
+ }
89
+ }
90
+ return counts;
91
+ }
@@ -0,0 +1,132 @@
1
+ import { type DataTableColumn, type DataTableFilterValue } from './data-table-utils.js';
2
+ import type { SortDirection } from './types.js';
3
+ export type { DataTableColumn, DataTableFilterValue } from './data-table-utils.js';
4
+ /** The column and direction a table is sorted by. */
5
+ export interface DataTableSort {
6
+ column: string;
7
+ direction: Exclude<SortDirection, 'none'>;
8
+ }
9
+ export interface DataTableRow<T> {
10
+ /** From `getRowId`; stable across sorting, filtering and paging. */
11
+ id: string;
12
+ /** Position in `data`. */
13
+ index: number;
14
+ original: T;
15
+ selected: boolean;
16
+ }
17
+ export interface DataTableOptions<T> {
18
+ /**
19
+ * The rows. Pass a getter to keep the table in step with changing data:
20
+ * `createDataTable({ get data() { return rows; }, columns })`.
21
+ */
22
+ data: T[];
23
+ columns: DataTableColumn<T>[];
24
+ /** Stable id for a row. Defaults to its index in `data`, which does not survive reordering. */
25
+ getRowId?: (row: T, index: number) => string;
26
+ initialSort?: DataTableSort;
27
+ /** Rows per page. `Infinity` turns paging off. */
28
+ pageSize?: number;
29
+ initialPage?: number;
30
+ initialSearch?: string;
31
+ initialFilters?: Record<string, DataTableFilterValue>;
32
+ initialHidden?: string[];
33
+ initialSelected?: string[];
34
+ /** Row selection. Defaults to `'none'`. */
35
+ selection?: 'none' | 'single' | 'multiple';
36
+ isRowSelectable?: (row: T) => boolean;
37
+ /**
38
+ * Hand work to a server. A manual step leaves `data` as given — you sort, filter or page it
39
+ * yourself in the matching callback and pass the result back in.
40
+ */
41
+ manual?: {
42
+ sorting?: boolean;
43
+ filtering?: boolean;
44
+ pagination?: boolean;
45
+ };
46
+ /** Total rows on the server. Required with manual pagination. */
47
+ rowCount?: number;
48
+ onSortChange?: (sort: DataTableSort | null) => void;
49
+ onSearchChange?: (search: string) => void;
50
+ onFiltersChange?: (filters: Record<string, DataTableFilterValue>) => void;
51
+ onPageChange?: (page: {
52
+ page: number;
53
+ pageSize: number;
54
+ }) => void;
55
+ onSelectionChange?: (selected: string[]) => void;
56
+ onVisibilityChange?: (hidden: string[]) => void;
57
+ }
58
+ export interface DataTableState<T> {
59
+ /** The current page of rows. */
60
+ readonly rows: DataTableRow<T>[];
61
+ /** Every row that passes search and filters, in sort order, across all pages. */
62
+ readonly filteredRows: DataTableRow<T>[];
63
+ /** Visible columns, in order. */
64
+ readonly columns: DataTableColumn<T>[];
65
+ readonly allColumns: DataTableColumn<T>[];
66
+ /** Rows across all pages after filtering — or the server's `rowCount`. */
67
+ readonly rowCount: number;
68
+ /** 1-based, so it feeds `Pagination` directly. */
69
+ readonly page: number;
70
+ readonly pageSize: number;
71
+ readonly pageCount: number;
72
+ readonly sort: DataTableSort | null;
73
+ readonly search: string;
74
+ readonly filters: Record<string, DataTableFilterValue>;
75
+ readonly selection: 'none' | 'single' | 'multiple';
76
+ readonly selected: string[];
77
+ readonly selectedCount: number;
78
+ /** Every selectable row on this page is selected. */
79
+ readonly allSelected: boolean;
80
+ /** Some, but not all, selectable rows on this page are selected. */
81
+ readonly someSelected: boolean;
82
+ readonly hidden: string[];
83
+ /** A column's sort state, ready for `TableHeader`'s `sort` prop. */
84
+ sortOf(columnId: string): SortDirection;
85
+ /** Sort by a column. Without a direction it goes ascending, then flips on repeat. */
86
+ sortBy(columnId: string, direction?: DataTableSort['direction']): void;
87
+ clearSort(): void;
88
+ setSearch(search: string): void;
89
+ /** Set a column filter; `null`, `''` or `[]` clears it. */
90
+ setFilter(columnId: string, value: DataTableFilterValue | null): void;
91
+ clearFilters(): void;
92
+ setPage(page: number): void;
93
+ setPageSize(size: number): void;
94
+ next(): void;
95
+ previous(): void;
96
+ isSelected(rowId: string): boolean;
97
+ isSelectable(row: DataTableRow<T>): boolean;
98
+ /** Select, deselect (`on: false`) or toggle a row. In `single` mode selecting clears the rest. */
99
+ select(rowId: string, on?: boolean): void;
100
+ /** Select or clear every selectable row on the current page. */
101
+ toggleAll(on?: boolean): void;
102
+ clearSelection(): void;
103
+ isVisible(columnId: string): boolean;
104
+ setColumnVisible(columnId: string, visible: boolean): void;
105
+ showAllColumns(): void;
106
+ /** Counts of each value in a column, among rows passing every *other* filter. */
107
+ facets(columnId: string): Map<string, number>;
108
+ /** The text a cell shows. */
109
+ format(row: T, column: DataTableColumn<T>): string;
110
+ /** Back to the initial sort, search, filters, page, selection and visibility. */
111
+ reset(): void;
112
+ }
113
+ /**
114
+ * Table state without the table: sorting, search, column filters, paging, row selection and
115
+ * column visibility over an array, in runes. Render it with `DataTable`, or feed its values to
116
+ * `Table`, `TableHeader` and `Pagination` yourself.
117
+ *
118
+ * ```ts
119
+ * const table = createDataTable({
120
+ * get data() { return sheets; },
121
+ * columns: [
122
+ * { id: 'name', header: 'Name', sortable: true },
123
+ * { id: 'layers', header: 'Layers', align: 'end', sortable: true },
124
+ * ],
125
+ * getRowId: (sheet) => sheet.id,
126
+ * pageSize: 25,
127
+ * });
128
+ * ```
129
+ *
130
+ * Works during SSR and outside components: state is set synchronously and nothing subscribes.
131
+ */
132
+ export declare function createDataTable<T>(options: DataTableOptions<T>): DataTableState<T>;
@@ -0,0 +1,292 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ import { countValues, formatCell, isActiveFilter, matchesFilter, matchesSearch, rowComparator, } from './data-table-utils.js';
3
+ const validPageSize = (size) => size === Infinity || (Number.isInteger(size) && size > 0);
4
+ /**
5
+ * Table state without the table: sorting, search, column filters, paging, row selection and
6
+ * column visibility over an array, in runes. Render it with `DataTable`, or feed its values to
7
+ * `Table`, `TableHeader` and `Pagination` yourself.
8
+ *
9
+ * ```ts
10
+ * const table = createDataTable({
11
+ * get data() { return sheets; },
12
+ * columns: [
13
+ * { id: 'name', header: 'Name', sortable: true },
14
+ * { id: 'layers', header: 'Layers', align: 'end', sortable: true },
15
+ * ],
16
+ * getRowId: (sheet) => sheet.id,
17
+ * pageSize: 25,
18
+ * });
19
+ * ```
20
+ *
21
+ * Works during SSR and outside components: state is set synchronously and nothing subscribes.
22
+ */
23
+ export function createDataTable(options) {
24
+ const { initialSort, pageSize: initialPageSize = 10, initialPage = 1, initialSearch = '', initialFilters = {}, initialHidden = [], initialSelected = [], manual = {}, } = options;
25
+ const ids = options.columns.map((column) => column.id);
26
+ const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
27
+ if (duplicate !== undefined) {
28
+ throw new RangeError(`createDataTable: duplicate column id "${duplicate}"`);
29
+ }
30
+ if (!validPageSize(initialPageSize)) {
31
+ throw new RangeError('createDataTable: `pageSize` must be a positive integer or Infinity');
32
+ }
33
+ if (initialSort && !ids.includes(initialSort.column)) {
34
+ throw new RangeError(`createDataTable: unknown initialSort column "${initialSort.column}"`);
35
+ }
36
+ if (manual.pagination && options.rowCount === undefined) {
37
+ throw new RangeError('createDataTable: manual pagination needs `rowCount`');
38
+ }
39
+ let sort = $state(initialSort ?? null);
40
+ let search = $state(initialSearch);
41
+ let filters = $state({ ...initialFilters });
42
+ let page = $state(initialPage);
43
+ let pageSize = $state(initialPageSize);
44
+ const selected = new SvelteSet(initialSelected);
45
+ const hidden = new SvelteSet(initialHidden);
46
+ const selection = $derived(options.selection ?? 'none');
47
+ const allColumns = $derived(options.columns);
48
+ const columns = $derived(allColumns.filter((column) => !hidden.has(column.id)));
49
+ const getRowId = $derived(options.getRowId ?? ((_row, index) => String(index)));
50
+ const indexed = $derived(options.data.map((original, index) => ({ id: getRowId(original, index), index, original })));
51
+ const column = (id) => allColumns.find((one) => one.id === id);
52
+ /** Rows passing the search and every filter except `skip` (for facet counts). */
53
+ const passing = (skip) => {
54
+ if (manual.filtering)
55
+ return indexed;
56
+ return indexed.filter(({ original }) => {
57
+ if (!matchesSearch(original, columns, search))
58
+ return false;
59
+ for (const [id, value] of Object.entries(filters)) {
60
+ if (id === skip || !isActiveFilter(value))
61
+ continue;
62
+ const target = column(id);
63
+ if (target && !matchesFilter(original, target, value))
64
+ return false;
65
+ }
66
+ return true;
67
+ });
68
+ };
69
+ const filtered = $derived(passing());
70
+ const sorted = $derived.by(() => {
71
+ if (manual.sorting || !sort)
72
+ return filtered;
73
+ const target = column(sort.column);
74
+ if (!target)
75
+ return filtered;
76
+ const compare = rowComparator(target, sort.direction === 'ascending' ? 1 : -1);
77
+ return [...filtered].sort((a, b) => compare(a.original, b.original));
78
+ });
79
+ const rowCount = $derived(manual.pagination ? (options.rowCount ?? 0) : sorted.length);
80
+ const pageCount = $derived(pageSize === Infinity ? 1 : Math.max(1, Math.ceil(rowCount / pageSize)));
81
+ // Data can shrink under the current page; show the last page that still exists.
82
+ const currentPage = $derived(Math.min(Math.max(1, page), pageCount));
83
+ const withSelection = (row) => ({
84
+ ...row,
85
+ selected: selected.has(row.id),
86
+ });
87
+ const filteredRows = $derived(sorted.map(withSelection));
88
+ const rows = $derived(manual.pagination || pageSize === Infinity
89
+ ? filteredRows
90
+ : filteredRows.slice((currentPage - 1) * pageSize, currentPage * pageSize));
91
+ const isSelectable = (row) => selection !== 'none' && (options.isRowSelectable?.(row.original) ?? true);
92
+ const selectableOnPage = $derived(rows.filter(isSelectable));
93
+ const allSelected = $derived(selectableOnPage.length > 0 && selectableOnPage.every((row) => selected.has(row.id)));
94
+ const someSelected = $derived(!allSelected && selectableOnPage.some((row) => selected.has(row.id)));
95
+ const emitPage = () => options.onPageChange?.({ page: currentPage, pageSize });
96
+ const emitSelection = () => options.onSelectionChange?.([...selected]);
97
+ const emitVisibility = () => options.onVisibilityChange?.([...hidden]);
98
+ /** A narrower result set starts over at the first page. */
99
+ const backToFirstPage = () => {
100
+ if (page === 1)
101
+ return;
102
+ page = 1;
103
+ emitPage();
104
+ };
105
+ function setPage(next) {
106
+ const clamped = Math.min(Math.max(1, Math.trunc(next) || 1), pageCount);
107
+ if (clamped === currentPage && clamped === page)
108
+ return;
109
+ page = clamped;
110
+ emitPage();
111
+ }
112
+ function select(rowId, on) {
113
+ if (selection === 'none')
114
+ return;
115
+ const next = on ?? !selected.has(rowId);
116
+ if (next === selected.has(rowId))
117
+ return;
118
+ if (next && selection === 'single')
119
+ selected.clear();
120
+ if (next)
121
+ selected.add(rowId);
122
+ else
123
+ selected.delete(rowId);
124
+ emitSelection();
125
+ }
126
+ return {
127
+ get rows() {
128
+ return rows;
129
+ },
130
+ get filteredRows() {
131
+ return filteredRows;
132
+ },
133
+ get columns() {
134
+ return columns;
135
+ },
136
+ get allColumns() {
137
+ return allColumns;
138
+ },
139
+ get rowCount() {
140
+ return rowCount;
141
+ },
142
+ get page() {
143
+ return currentPage;
144
+ },
145
+ get pageSize() {
146
+ return pageSize;
147
+ },
148
+ get pageCount() {
149
+ return pageCount;
150
+ },
151
+ get sort() {
152
+ return sort;
153
+ },
154
+ get search() {
155
+ return search;
156
+ },
157
+ get filters() {
158
+ return filters;
159
+ },
160
+ get selection() {
161
+ return selection;
162
+ },
163
+ get selected() {
164
+ return [...selected];
165
+ },
166
+ get selectedCount() {
167
+ return selected.size;
168
+ },
169
+ get allSelected() {
170
+ return allSelected;
171
+ },
172
+ get someSelected() {
173
+ return someSelected;
174
+ },
175
+ get hidden() {
176
+ return [...hidden];
177
+ },
178
+ sortOf: (columnId) => (sort?.column === columnId ? sort.direction : 'none'),
179
+ sortBy(columnId, direction) {
180
+ if (!column(columnId))
181
+ return;
182
+ const next = direction ??
183
+ (sort?.column === columnId && sort.direction === 'ascending' ? 'descending' : 'ascending');
184
+ sort = { column: columnId, direction: next };
185
+ options.onSortChange?.(sort);
186
+ },
187
+ clearSort() {
188
+ if (!sort)
189
+ return;
190
+ sort = null;
191
+ options.onSortChange?.(null);
192
+ },
193
+ setSearch(next) {
194
+ if (next === search)
195
+ return;
196
+ search = next;
197
+ options.onSearchChange?.(next);
198
+ backToFirstPage();
199
+ },
200
+ setFilter(columnId, value) {
201
+ const { [columnId]: previous, ...rest } = filters;
202
+ if (!isActiveFilter(value) && !isActiveFilter(previous))
203
+ return;
204
+ filters = isActiveFilter(value) ? { ...rest, [columnId]: value } : rest;
205
+ options.onFiltersChange?.(filters);
206
+ backToFirstPage();
207
+ },
208
+ clearFilters() {
209
+ if (Object.keys(filters).length === 0)
210
+ return;
211
+ filters = {};
212
+ options.onFiltersChange?.(filters);
213
+ backToFirstPage();
214
+ },
215
+ setPage,
216
+ setPageSize(size) {
217
+ if (!validPageSize(size)) {
218
+ throw new RangeError('createDataTable: `pageSize` must be a positive integer or Infinity');
219
+ }
220
+ if (size === pageSize)
221
+ return;
222
+ pageSize = size;
223
+ page = 1;
224
+ emitPage();
225
+ },
226
+ next: () => setPage(currentPage + 1),
227
+ previous: () => setPage(currentPage - 1),
228
+ isSelected: (rowId) => selected.has(rowId),
229
+ isSelectable,
230
+ select,
231
+ toggleAll(on) {
232
+ if (selection !== 'multiple')
233
+ return;
234
+ const next = on ?? !allSelected;
235
+ let changed = false;
236
+ for (const row of selectableOnPage) {
237
+ if (selected.has(row.id) === next)
238
+ continue;
239
+ if (next)
240
+ selected.add(row.id);
241
+ else
242
+ selected.delete(row.id);
243
+ changed = true;
244
+ }
245
+ if (changed)
246
+ emitSelection();
247
+ },
248
+ clearSelection() {
249
+ if (selected.size === 0)
250
+ return;
251
+ selected.clear();
252
+ emitSelection();
253
+ },
254
+ isVisible: (columnId) => !hidden.has(columnId),
255
+ setColumnVisible(columnId, visible) {
256
+ if (visible === !hidden.has(columnId))
257
+ return;
258
+ if (visible)
259
+ hidden.delete(columnId);
260
+ else
261
+ hidden.add(columnId);
262
+ emitVisibility();
263
+ },
264
+ showAllColumns() {
265
+ if (hidden.size === 0)
266
+ return;
267
+ hidden.clear();
268
+ emitVisibility();
269
+ },
270
+ facets(columnId) {
271
+ const target = column(columnId);
272
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- a fresh, read-only result
273
+ if (!target)
274
+ return new Map();
275
+ return countValues(passing(columnId).map((row) => row.original), target);
276
+ },
277
+ format: (row, target) => formatCell(row, target),
278
+ reset() {
279
+ sort = initialSort ?? null;
280
+ search = initialSearch;
281
+ filters = { ...initialFilters };
282
+ page = initialPage;
283
+ pageSize = initialPageSize;
284
+ selected.clear();
285
+ for (const id of initialSelected)
286
+ selected.add(id);
287
+ hidden.clear();
288
+ for (const id of initialHidden)
289
+ hidden.add(id);
290
+ },
291
+ };
292
+ }
package/dist/hls.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /** The slice of the hls.js API the players use. */
2
+ export interface HlsInstance {
3
+ loadSource(url: string): void;
4
+ attachMedia(media: HTMLMediaElement): void;
5
+ startLoad(): void;
6
+ recoverMediaError(): void;
7
+ destroy(): void;
8
+ on(event: string, callback: (event: string, data: HlsErrorData) => void): void;
9
+ }
10
+ export interface HlsErrorData {
11
+ type?: string;
12
+ details?: string;
13
+ fatal?: boolean;
14
+ }
15
+ export interface HlsConstructor {
16
+ new (config?: Record<string, unknown>): HlsInstance;
17
+ isSupported(): boolean;
18
+ readonly Events: {
19
+ ERROR: string;
20
+ } & Record<string, string>;
21
+ readonly ErrorTypes: {
22
+ NETWORK_ERROR: string;
23
+ MEDIA_ERROR: string;
24
+ } & Record<string, string>;
25
+ }
26
+ export type HlsLoader = () => Promise<HlsConstructor> | HlsConstructor;
27
+ /** Registers how to load hls.js — typically a dynamic import. `null` unregisters. */
28
+ export declare function setHlsLoader(loader: HlsLoader | null): void;
29
+ /**
30
+ * Resolves hls.js through `loader`, else the registered loader, else a global `Hls` from a
31
+ * script tag. Resolves `null` rather than rejecting when none is available.
32
+ */
33
+ export declare function loadHls(loader?: HlsLoader): Promise<HlsConstructor | null>;
34
+ /** Whether the browser plays HLS without hls.js (Safari, iOS). */
35
+ export declare function canPlayHlsNatively(media?: HTMLMediaElement | null): boolean;
package/dist/hls.js ADDED
@@ -0,0 +1,37 @@
1
+ /*
2
+ * HLS (`.m3u8`) playback. Safari plays HLS natively; everywhere else needs hls.js, which this
3
+ * package never imports itself — it is an optional peer you register once, so apps that never
4
+ * stream do not pay for it:
5
+ *
6
+ * import { setHlsLoader } from '@loidolt/theme-svelte';
7
+ * setHlsLoader(() => import('hls.js').then((module) => module.default));
8
+ */
9
+ let registered = null;
10
+ let cached = null;
11
+ /** Registers how to load hls.js — typically a dynamic import. `null` unregisters. */
12
+ export function setHlsLoader(loader) {
13
+ registered = loader;
14
+ cached = null;
15
+ }
16
+ /**
17
+ * Resolves hls.js through `loader`, else the registered loader, else a global `Hls` from a
18
+ * script tag. Resolves `null` rather than rejecting when none is available.
19
+ */
20
+ export function loadHls(loader) {
21
+ const resolve = (source) => Promise.resolve()
22
+ .then(source)
23
+ .then((constructor) => constructor ?? null)
24
+ .catch(() => null);
25
+ if (loader)
26
+ return resolve(loader);
27
+ cached ??= resolve(registered ?? (() => globalThis.Hls));
28
+ return cached;
29
+ }
30
+ /** Whether the browser plays HLS without hls.js (Safari, iOS). */
31
+ export function canPlayHlsNatively(media) {
32
+ const probe = media ?? (typeof document === 'undefined' ? null : document.createElement('video'));
33
+ if (!probe)
34
+ return false;
35
+ return (probe.canPlayType('application/vnd.apple.mpegurl') !== '' ||
36
+ probe.canPlayType('application/x-mpegURL') !== '');
37
+ }
@@ -0,0 +1,22 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ import { type HlsLoader } from './hls.js';
3
+ export type HlsStatus = 'idle' | 'loading' | 'native' | 'attached' | 'unsupported' | 'error';
4
+ export interface HlsSourceOptions {
5
+ /** Loader for this player only. Defaults to the one given to `setHlsLoader()`. */
6
+ loader?: HlsLoader;
7
+ /** Passed to the hls.js constructor. */
8
+ config?: Record<string, unknown>;
9
+ onError?: (error: Error) => void;
10
+ }
11
+ export interface HlsSource {
12
+ /** Attach a playlist to a media element: `{@attach hls.source(url)}`. */
13
+ source(src: string): Attachment<HTMLMediaElement>;
14
+ readonly status: HlsStatus;
15
+ readonly error: Error | null;
16
+ }
17
+ /**
18
+ * Plays an HLS playlist in a media element — natively where the browser can, through hls.js
19
+ * where it cannot. Fatal network errors retry and media errors attempt recovery before the
20
+ * status turns to `error`.
21
+ */
22
+ export declare function createHlsSource(options?: HlsSourceOptions): HlsSource;