runeforge 0.0.61 → 0.0.63

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 (67) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1410 -1407
  3. package/dist/components/Avatar.svelte +31 -31
  4. package/dist/components/IconRenderer.svelte +73 -73
  5. package/dist/components/Modal.svelte +75 -75
  6. package/dist/components/common/Header.svelte +40 -40
  7. package/dist/components/crud/EmbeddedField.svelte +203 -203
  8. package/dist/components/crud/Field.svelte +122 -122
  9. package/dist/components/crud/GenericCRUD.svelte +426 -426
  10. package/dist/components/crud/SearchInput.svelte +53 -53
  11. package/dist/components/crud/columns/Avatar.svelte +15 -15
  12. package/dist/components/crud/columns/Icon.svelte +8 -8
  13. package/dist/components/crud/fields/BooleanField.svelte +38 -38
  14. package/dist/components/crud/fields/DatetimeField.svelte +250 -250
  15. package/dist/components/crud/fields/DefaultField.svelte +67 -67
  16. package/dist/components/crud/fields/FileField.svelte +70 -70
  17. package/dist/components/crud/fields/MultiSelectField.svelte +70 -70
  18. package/dist/components/crud/fields/SelectField.svelte +67 -67
  19. package/dist/components/crud/fields/TextareaField.svelte +55 -55
  20. package/dist/components/crud/fields/TreeField.svelte +1 -0
  21. package/dist/components/crud/utils/resolution.js +1 -0
  22. package/dist/components/crud/utils/tree.d.ts +5 -0
  23. package/dist/components/crud/utils/tree.js +29 -0
  24. package/dist/components/crud/views/Create.svelte +239 -239
  25. package/dist/components/crud/views/Read.svelte +131 -131
  26. package/dist/components/crud/views/Update.svelte +215 -215
  27. package/dist/components/crud/views/list/List.svelte +291 -291
  28. package/dist/components/crud/views/list/Modals.svelte +56 -56
  29. package/dist/components/crud/views/list/Table.svelte +173 -173
  30. package/dist/components/crud/views/list/Toolbar.svelte +340 -340
  31. package/dist/components/form/Button.svelte +27 -27
  32. package/dist/components/form/Label.svelte +37 -37
  33. package/dist/components/form/MultiSelect.svelte +248 -248
  34. package/dist/components/form/PasswordInput.svelte +68 -68
  35. package/dist/components/form/Required.svelte +1 -1
  36. package/dist/components/form/Select.svelte +209 -209
  37. package/dist/components/form/Tree.svelte +43 -4
  38. package/dist/components/form/Tree.svelte.d.ts +1 -0
  39. package/dist/components/form/TreeNode.svelte +31 -2
  40. package/dist/components/form/TreeNode.svelte.d.ts +2 -0
  41. package/dist/components/navigation/Breadcrumbs.svelte +111 -111
  42. package/dist/components/table/ColumnFilter.svelte +168 -168
  43. package/dist/components/table/PaginatedTable.svelte +542 -542
  44. package/dist/components/table/Paginator.svelte +113 -113
  45. package/dist/components/table/SortHeader.svelte +43 -43
  46. package/dist/components/table/TableBody.svelte +150 -150
  47. package/dist/components/table/TableHeader.svelte +88 -88
  48. package/dist/components/table/state.svelte.js +7 -2
  49. package/dist/icons/defaults/Clear.svelte +6 -6
  50. package/dist/icons/defaults/Create.svelte +6 -6
  51. package/dist/icons/defaults/Delete.svelte +6 -6
  52. package/dist/icons/defaults/Download.svelte +7 -7
  53. package/dist/icons/defaults/Edit.svelte +7 -7
  54. package/dist/icons/defaults/Filter.svelte +6 -6
  55. package/dist/icons/defaults/FilterActive.svelte +6 -6
  56. package/dist/icons/defaults/Folder.svelte +6 -6
  57. package/dist/icons/defaults/Grip.svelte +7 -7
  58. package/dist/icons/defaults/Home.svelte +6 -6
  59. package/dist/icons/defaults/PasswordHide.svelte +9 -9
  60. package/dist/icons/defaults/PasswordShow.svelte +7 -7
  61. package/dist/icons/defaults/SortAsc.svelte +6 -6
  62. package/dist/icons/defaults/SortDesc.svelte +6 -6
  63. package/dist/icons/defaults/SortNone.svelte +6 -6
  64. package/dist/icons/defaults/View.svelte +7 -7
  65. package/dist/types/attribute.d.ts +3 -0
  66. package/dist/types/crud/fields.d.ts +3 -0
  67. package/package.json +1 -1
@@ -11,6 +11,8 @@
11
11
  depth,
12
12
  columns,
13
13
  defaultExpanded,
14
+ visible = null,
15
+ forceExpanded = null
14
16
  }: {
15
17
  node: SelectOption;
16
18
  childrenByParent: Map<string | null, SelectOption[]>;
@@ -20,11 +22,27 @@
20
22
  depth: number;
21
23
  columns: number;
22
24
  defaultExpanded: boolean;
25
+ // Active search results: node values allowed to render, and the subset
26
+ // that must stay expanded because their match is in a descendant, not
27
+ // on the node itself. Both null outside of a search.
28
+ visible?: Set<string> | null;
29
+ forceExpanded?: Set<string> | null;
23
30
  } = $props();
24
31
 
25
32
  let expanded = $state(defaultExpanded);
26
33
 
27
- const children = $derived(childrenByParent.get(node.value) ?? []);
34
+ // A search match below this node overrides manual collapse so the result
35
+ // is reachable; re-derived only when this node's forced state actually
36
+ // flips, so a manual collapse made afterwards (still mid-search) sticks.
37
+ const isForceExpanded = $derived(forceExpanded?.has(node.value) ?? false);
38
+ $effect(() => {
39
+ if (isForceExpanded) expanded = true;
40
+ });
41
+
42
+ const allChildren = $derived(childrenByParent.get(node.value) ?? []);
43
+ const children = $derived(
44
+ visible ? allChildren.filter((child) => visible.has(child.value)) : allChildren
45
+ );
28
46
  const hasChildren = $derived(children.length > 0);
29
47
  </script>
30
48
 
@@ -60,7 +78,18 @@
60
78
 
61
79
  {#if expanded && hasChildren}
62
80
  {#each children as child (child.value)}
63
- <TreeNode node={child} {childrenByParent} {selected} {onToggle} {disabled} depth={depth + 1} {columns} {defaultExpanded} />
81
+ <TreeNode
82
+ node={child}
83
+ {childrenByParent}
84
+ {selected}
85
+ {onToggle}
86
+ {disabled}
87
+ depth={depth + 1}
88
+ {columns}
89
+ {defaultExpanded}
90
+ {visible}
91
+ {forceExpanded}
92
+ />
64
93
  {/each}
65
94
  {/if}
66
95
  </div>
@@ -9,6 +9,8 @@ type $$ComponentProps = {
9
9
  depth: number;
10
10
  columns: number;
11
11
  defaultExpanded: boolean;
12
+ visible?: Set<string> | null;
13
+ forceExpanded?: Set<string> | null;
12
14
  };
13
15
  declare const TreeNode: import("svelte").Component<$$ComponentProps, {}, "">;
14
16
  type TreeNode = ReturnType<typeof TreeNode>;
@@ -1,111 +1,111 @@
1
- <script lang="ts">
2
- import { getIconSet } from '../../icons/context.js';
3
- import { defaultIconSet } from '../../icons/sets/default.js';
4
- import { getConfig } from '../../config/context.js';
5
- import type { BreadcrumbItem } from '../../types/breadcrumb.js';
6
-
7
- let {
8
- items = [],
9
- homeHref,
10
- }: {
11
- items?: BreadcrumbItem[];
12
- homeHref?: string;
13
- } = $props();
14
-
15
- const icons = $derived(getIconSet() ?? defaultIconSet);
16
-
17
- const home: BreadcrumbItem = $derived({
18
- label: 'Inicio',
19
- icon: icons.home,
20
- link: { href: homeHref ?? getConfig().homeHref ?? '/' },
21
- });
22
-
23
- const allItems = $derived([home, ...items]);
24
- </script>
25
-
26
- <div class="breadcrumbs">
27
- <ul>
28
- {#each allItems as item, i (i)}
29
- <li>
30
- {#if item.link !== undefined}
31
- <a class="inline-flex items-center gap-2" {...item.link}>
32
- {#if item.icon}
33
- {@const ItemIcon = item.icon}
34
- <ItemIcon />
35
- {/if}
36
- <span>{item.label}</span>
37
- </a>
38
- {:else}
39
- <span class="inline-flex items-center gap-2">
40
- {#if item.icon}
41
- {@const ItemIcon = item.icon}
42
- <ItemIcon />
43
- {/if}
44
- <span>{item.label}</span>
45
- </span>
46
- {/if}
47
- </li>
48
- {/each}
49
- </ul>
50
- </div>
51
-
52
- <style>
53
- div {
54
- font-size: var(--runeforge-breadcrumb-font-size, 0.875rem);
55
- }
56
-
57
- :global(.breadcrumbs svg) {
58
- width: var(--runeforge-breadcrumb-icon-size, 1rem);
59
- height: var(--runeforge-breadcrumb-icon-size, 1rem);
60
- flex-shrink: 0;
61
- }
62
-
63
- /*
64
- * DaisyUI breadcrumbs use @layer rules that Tailwind v4 doesn't propagate
65
- * from node_modules. These unlayered rules override that gap.
66
- */
67
- :global(.breadcrumbs > ul),
68
- :global(.breadcrumbs > ol),
69
- :global(.breadcrumbs > menu) {
70
- display: flex;
71
- align-items: center;
72
- white-space: nowrap;
73
- overflow-x: auto;
74
- }
75
-
76
- :global(.breadcrumbs > ul > li),
77
- :global(.breadcrumbs > ol > li),
78
- :global(.breadcrumbs > menu > li) {
79
- display: flex;
80
- align-items: center;
81
- }
82
-
83
- :global(.breadcrumbs > ul > li + li::before),
84
- :global(.breadcrumbs > ol > li + li::before),
85
- :global(.breadcrumbs > menu > li + li::before) {
86
- content: '';
87
- display: block;
88
- opacity: 0.4;
89
- border-top: 1px solid;
90
- border-right: 1px solid;
91
- width: 0.375rem;
92
- height: 0.375rem;
93
- margin-inline: 0.5rem 0.75rem;
94
- rotate: 45deg;
95
- }
96
-
97
- /*
98
- * Tailwind preflight sets svg { display: block }, which stacks icons above
99
- * labels when the <a>/<span> inline-flex class isn't generated from node_modules.
100
- */
101
- :global(.breadcrumbs > ul > li > a),
102
- :global(.breadcrumbs > ul > li > span),
103
- :global(.breadcrumbs > ol > li > a),
104
- :global(.breadcrumbs > ol > li > span),
105
- :global(.breadcrumbs > menu > li > a),
106
- :global(.breadcrumbs > menu > li > span) {
107
- display: inline-flex;
108
- align-items: center;
109
- gap: 0.5rem;
110
- }
111
- </style>
1
+ <script lang="ts">
2
+ import { getIconSet } from '../../icons/context.js';
3
+ import { defaultIconSet } from '../../icons/sets/default.js';
4
+ import { getConfig } from '../../config/context.js';
5
+ import type { BreadcrumbItem } from '../../types/breadcrumb.js';
6
+
7
+ let {
8
+ items = [],
9
+ homeHref,
10
+ }: {
11
+ items?: BreadcrumbItem[];
12
+ homeHref?: string;
13
+ } = $props();
14
+
15
+ const icons = $derived(getIconSet() ?? defaultIconSet);
16
+
17
+ const home: BreadcrumbItem = $derived({
18
+ label: 'Inicio',
19
+ icon: icons.home,
20
+ link: { href: homeHref ?? getConfig().homeHref ?? '/' },
21
+ });
22
+
23
+ const allItems = $derived([home, ...items]);
24
+ </script>
25
+
26
+ <div class="breadcrumbs">
27
+ <ul>
28
+ {#each allItems as item, i (i)}
29
+ <li>
30
+ {#if item.link !== undefined}
31
+ <a class="inline-flex items-center gap-2" {...item.link}>
32
+ {#if item.icon}
33
+ {@const ItemIcon = item.icon}
34
+ <ItemIcon />
35
+ {/if}
36
+ <span>{item.label}</span>
37
+ </a>
38
+ {:else}
39
+ <span class="inline-flex items-center gap-2">
40
+ {#if item.icon}
41
+ {@const ItemIcon = item.icon}
42
+ <ItemIcon />
43
+ {/if}
44
+ <span>{item.label}</span>
45
+ </span>
46
+ {/if}
47
+ </li>
48
+ {/each}
49
+ </ul>
50
+ </div>
51
+
52
+ <style>
53
+ div {
54
+ font-size: var(--runeforge-breadcrumb-font-size, 0.875rem);
55
+ }
56
+
57
+ :global(.breadcrumbs svg) {
58
+ width: var(--runeforge-breadcrumb-icon-size, 1rem);
59
+ height: var(--runeforge-breadcrumb-icon-size, 1rem);
60
+ flex-shrink: 0;
61
+ }
62
+
63
+ /*
64
+ * DaisyUI breadcrumbs use @layer rules that Tailwind v4 doesn't propagate
65
+ * from node_modules. These unlayered rules override that gap.
66
+ */
67
+ :global(.breadcrumbs > ul),
68
+ :global(.breadcrumbs > ol),
69
+ :global(.breadcrumbs > menu) {
70
+ display: flex;
71
+ align-items: center;
72
+ white-space: nowrap;
73
+ overflow-x: auto;
74
+ }
75
+
76
+ :global(.breadcrumbs > ul > li),
77
+ :global(.breadcrumbs > ol > li),
78
+ :global(.breadcrumbs > menu > li) {
79
+ display: flex;
80
+ align-items: center;
81
+ }
82
+
83
+ :global(.breadcrumbs > ul > li + li::before),
84
+ :global(.breadcrumbs > ol > li + li::before),
85
+ :global(.breadcrumbs > menu > li + li::before) {
86
+ content: '';
87
+ display: block;
88
+ opacity: 0.4;
89
+ border-top: 1px solid;
90
+ border-right: 1px solid;
91
+ width: 0.375rem;
92
+ height: 0.375rem;
93
+ margin-inline: 0.5rem 0.75rem;
94
+ rotate: 45deg;
95
+ }
96
+
97
+ /*
98
+ * Tailwind preflight sets svg { display: block }, which stacks icons above
99
+ * labels when the <a>/<span> inline-flex class isn't generated from node_modules.
100
+ */
101
+ :global(.breadcrumbs > ul > li > a),
102
+ :global(.breadcrumbs > ul > li > span),
103
+ :global(.breadcrumbs > ol > li > a),
104
+ :global(.breadcrumbs > ol > li > span),
105
+ :global(.breadcrumbs > menu > li > a),
106
+ :global(.breadcrumbs > menu > li > span) {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ gap: 0.5rem;
110
+ }
111
+ </style>
@@ -1,168 +1,168 @@
1
- <script lang="ts" generics="T extends object">
2
- import { onMount } from 'svelte';
3
- import Label from '../form/Label.svelte';
4
- import Button from '../form/Button.svelte';
5
- import { getIconSet } from '../../icons/context.js';
6
- import { defaultIconSet } from '../../icons/sets/default.js';
7
- import { getStrings } from '../../i18n/context.js';
8
-
9
- const strings = getStrings();
10
-
11
- onMount(() => {
12
- import('cally');
13
- });
14
- import type { DistinctEntry } from '../../types/table.js';
15
- import type { FilterState } from './state.svelte.js';
16
- import type { ColumnDefinition } from '../../types/crud/columns.js';
17
-
18
- let {
19
- column,
20
- entries = [],
21
- filter,
22
- maxCheckboxValues = 20,
23
- onchange
24
- }: {
25
- column: ColumnDefinition<T>;
26
- entries?: DistinctEntry<T>[];
27
- filter: FilterState;
28
- maxCheckboxValues?: number;
29
- onchange?: () => void;
30
- } = $props();
31
-
32
- const icons = $derived(getIconSet() ?? defaultIconSet);
33
- const popId = $derived(`filter-pop-${column.attribute}`);
34
- const anchor = $derived(`--filter-anchor-${column.attribute}`);
35
- const isDatetime = $derived(column.type === 'datetime');
36
-
37
- const dateRange = $derived(filter.dateRangeFor(column.attribute));
38
- const calendarValue = $derived(
39
- dateRange.from || dateRange.to ? `${dateRange.from}/${dateRange.to}` : ''
40
- );
41
-
42
- let calendarEl: HTMLElement | undefined = $state();
43
-
44
- $effect(() => {
45
- if (!calendarEl) return;
46
- function handler() {
47
- const value = (calendarEl as HTMLElement & { value: string }).value ?? '';
48
- const [from = '', to = ''] = value.split('/');
49
- filter.setDateRange(column.attribute, from, to);
50
- onchange?.();
51
- }
52
- calendarEl.addEventListener('change', handler);
53
- return () => calendarEl?.removeEventListener('change', handler);
54
- });
55
-
56
- let debounceTimer: ReturnType<typeof setTimeout>;
57
- function setText(value: string) {
58
- filter.setText(column.attribute, value);
59
- clearTimeout(debounceTimer);
60
- debounceTimer = setTimeout(() => onchange?.(), 300);
61
- }
62
- function toggle(value: string) {
63
- filter.toggleValue(column.attribute, value);
64
- onchange?.();
65
- }
66
- function clear() {
67
- filter.clear(column.attribute);
68
- onchange?.();
69
- }
70
- </script>
71
-
72
- <Button
73
- variant="ghost"
74
- class={['btn-xs btn-square', filter.hasActive(column.attribute) && 'text-primary']}
75
- popovertarget={popId}
76
- style="anchor-name:{anchor}"
77
- aria-label={strings.filterColumn(column.title ?? column.attribute)}
78
- title={strings.filter}
79
- >
80
- {#if filter.hasActive(column.attribute)}
81
- {@const Icon = icons.filterActive}
82
- <Icon class="size-3.5" />
83
- {:else}
84
- {@const Icon = icons.filter}
85
- <Icon class="size-3.5" />
86
- {/if}
87
- </Button>
88
-
89
- <div
90
- popover="auto"
91
- id={popId}
92
- style="position-anchor:{anchor}; position-try-fallbacks:flip-block;"
93
- class="dropdown dropdown-end rounded-box border border-base-content/10 bg-base-100 p-3 shadow-lg"
94
- class:w-56={!isDatetime}
95
- >
96
- {#if isDatetime}
97
- <calendar-range class="cally bg-base-100" value={calendarValue} bind:this={calendarEl}>
98
- <svg
99
- aria-label={strings.previous}
100
- class="fill-current size-4"
101
- {...{ slot: 'previous' }}
102
- xmlns="http://www.w3.org/2000/svg"
103
- viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg
104
- >
105
- <svg
106
- aria-label={strings.next}
107
- class="fill-current size-4"
108
- {...{ slot: 'next' }}
109
- xmlns="http://www.w3.org/2000/svg"
110
- viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg
111
- >
112
- <calendar-month></calendar-month>
113
- </calendar-range>
114
- {#if dateRange.from || dateRange.to}
115
- <p class="mt-2 text-center text-xs text-base-content/60">
116
- {dateRange.from || '…'} → {dateRange.to || '…'}
117
- </p>
118
- {/if}
119
- {#if filter.hasActive(column.attribute)}
120
- <Button variant="ghost" class="btn-sm mt-2 w-full" onclick={clear}>
121
- {strings.clearFilter}
122
- </Button>
123
- {/if}
124
- {:else}
125
- <div class="relative">
126
- <input
127
- type="text"
128
- class={[
129
- 'input input-bordered input-sm w-full',
130
- filter.hasActive(column.attribute) && 'pr-7'
131
- ]}
132
- placeholder={strings.filterPlaceholder}
133
- value={filter.textFor(column.attribute)}
134
- oninput={(e) => setText(e.currentTarget.value)}
135
- />
136
- {#if filter.hasActive(column.attribute)}
137
- {@const Icon = icons.clear}
138
- <Button
139
- variant="ghost"
140
- class="btn-xs btn-square btn-circle absolute top-1/2 right-1 -translate-y-1/2"
141
- aria-label={strings.clearFilter}
142
- title={strings.clearFilter}
143
- onclick={clear}
144
- >
145
- <Icon class="size-3" />
146
- </Button>
147
- {/if}
148
- </div>
149
-
150
- {#if entries.length > 0 && entries.length < maxCheckboxValues}
151
- <div class="mt-2 flex max-h-60 flex-col gap-1 overflow-y-auto">
152
- {#each entries as entry (entry.key)}
153
- <Label class="flex cursor-pointer items-center gap-2 text-sm font-normal normal-case">
154
- <input
155
- type="checkbox"
156
- class="checkbox checkbox-xs shrink-0"
157
- checked={filter.isChecked(column.attribute, entry.key)}
158
- onchange={() => toggle(entry.key)}
159
- />
160
- <span class="truncate">
161
- {entry.key === '' ? strings.emptyValue : (entry.label ?? entry.key)}
162
- </span>
163
- </Label>
164
- {/each}
165
- </div>
166
- {/if}
167
- {/if}
168
- </div>
1
+ <script lang="ts" generics="T extends object">
2
+ import { onMount } from 'svelte';
3
+ import Label from '../form/Label.svelte';
4
+ import Button from '../form/Button.svelte';
5
+ import { getIconSet } from '../../icons/context.js';
6
+ import { defaultIconSet } from '../../icons/sets/default.js';
7
+ import { getStrings } from '../../i18n/context.js';
8
+
9
+ const strings = getStrings();
10
+
11
+ onMount(() => {
12
+ import('cally');
13
+ });
14
+ import type { DistinctEntry } from '../../types/table.js';
15
+ import type { FilterState } from './state.svelte.js';
16
+ import type { ColumnDefinition } from '../../types/crud/columns.js';
17
+
18
+ let {
19
+ column,
20
+ entries = [],
21
+ filter,
22
+ maxCheckboxValues = 20,
23
+ onchange
24
+ }: {
25
+ column: ColumnDefinition<T>;
26
+ entries?: DistinctEntry<T>[];
27
+ filter: FilterState;
28
+ maxCheckboxValues?: number;
29
+ onchange?: () => void;
30
+ } = $props();
31
+
32
+ const icons = $derived(getIconSet() ?? defaultIconSet);
33
+ const popId = $derived(`filter-pop-${column.attribute}`);
34
+ const anchor = $derived(`--filter-anchor-${column.attribute}`);
35
+ const isDatetime = $derived(column.type === 'datetime');
36
+
37
+ const dateRange = $derived(filter.dateRangeFor(column.attribute));
38
+ const calendarValue = $derived(
39
+ dateRange.from || dateRange.to ? `${dateRange.from}/${dateRange.to}` : ''
40
+ );
41
+
42
+ let calendarEl: HTMLElement | undefined = $state();
43
+
44
+ $effect(() => {
45
+ if (!calendarEl) return;
46
+ function handler() {
47
+ const value = (calendarEl as HTMLElement & { value: string }).value ?? '';
48
+ const [from = '', to = ''] = value.split('/');
49
+ filter.setDateRange(column.attribute, from, to);
50
+ onchange?.();
51
+ }
52
+ calendarEl.addEventListener('change', handler);
53
+ return () => calendarEl?.removeEventListener('change', handler);
54
+ });
55
+
56
+ let debounceTimer: ReturnType<typeof setTimeout>;
57
+ function setText(value: string) {
58
+ filter.setText(column.attribute, value);
59
+ clearTimeout(debounceTimer);
60
+ debounceTimer = setTimeout(() => onchange?.(), 300);
61
+ }
62
+ function toggle(value: string) {
63
+ filter.toggleValue(column.attribute, value);
64
+ onchange?.();
65
+ }
66
+ function clear() {
67
+ filter.clear(column.attribute);
68
+ onchange?.();
69
+ }
70
+ </script>
71
+
72
+ <Button
73
+ variant="ghost"
74
+ class={['btn-xs btn-square', filter.hasActive(column.attribute) && 'text-primary']}
75
+ popovertarget={popId}
76
+ style="anchor-name:{anchor}"
77
+ aria-label={strings.filterColumn(column.title ?? column.attribute)}
78
+ title={strings.filter}
79
+ >
80
+ {#if filter.hasActive(column.attribute)}
81
+ {@const Icon = icons.filterActive}
82
+ <Icon class="size-3.5" />
83
+ {:else}
84
+ {@const Icon = icons.filter}
85
+ <Icon class="size-3.5" />
86
+ {/if}
87
+ </Button>
88
+
89
+ <div
90
+ popover="auto"
91
+ id={popId}
92
+ style="position-anchor:{anchor}; position-try-fallbacks:flip-block;"
93
+ class="dropdown dropdown-end rounded-box border border-base-content/10 bg-base-100 p-3 shadow-lg"
94
+ class:w-56={!isDatetime}
95
+ >
96
+ {#if isDatetime}
97
+ <calendar-range class="cally bg-base-100" value={calendarValue} bind:this={calendarEl}>
98
+ <svg
99
+ aria-label={strings.previous}
100
+ class="fill-current size-4"
101
+ {...{ slot: 'previous' }}
102
+ xmlns="http://www.w3.org/2000/svg"
103
+ viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg
104
+ >
105
+ <svg
106
+ aria-label={strings.next}
107
+ class="fill-current size-4"
108
+ {...{ slot: 'next' }}
109
+ xmlns="http://www.w3.org/2000/svg"
110
+ viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg
111
+ >
112
+ <calendar-month></calendar-month>
113
+ </calendar-range>
114
+ {#if dateRange.from || dateRange.to}
115
+ <p class="mt-2 text-center text-xs text-base-content/60">
116
+ {dateRange.from || '…'} → {dateRange.to || '…'}
117
+ </p>
118
+ {/if}
119
+ {#if filter.hasActive(column.attribute)}
120
+ <Button variant="ghost" class="btn-sm mt-2 w-full" onclick={clear}>
121
+ {strings.clearFilter}
122
+ </Button>
123
+ {/if}
124
+ {:else}
125
+ <div class="relative">
126
+ <input
127
+ type="text"
128
+ class={[
129
+ 'input input-bordered input-sm w-full',
130
+ filter.hasActive(column.attribute) && 'pr-7'
131
+ ]}
132
+ placeholder={strings.filterPlaceholder}
133
+ value={filter.textFor(column.attribute)}
134
+ oninput={(e) => setText(e.currentTarget.value)}
135
+ />
136
+ {#if filter.hasActive(column.attribute)}
137
+ {@const Icon = icons.clear}
138
+ <Button
139
+ variant="ghost"
140
+ class="btn-xs btn-square btn-circle absolute top-1/2 right-1 -translate-y-1/2"
141
+ aria-label={strings.clearFilter}
142
+ title={strings.clearFilter}
143
+ onclick={clear}
144
+ >
145
+ <Icon class="size-3" />
146
+ </Button>
147
+ {/if}
148
+ </div>
149
+
150
+ {#if entries.length > 0 && entries.length < maxCheckboxValues}
151
+ <div class="mt-2 flex max-h-60 flex-col gap-1 overflow-y-auto">
152
+ {#each entries as entry (entry.key)}
153
+ <Label class="flex cursor-pointer items-center gap-2 text-sm font-normal normal-case">
154
+ <input
155
+ type="checkbox"
156
+ class="checkbox checkbox-xs shrink-0"
157
+ checked={filter.isChecked(column.attribute, entry.key)}
158
+ onchange={() => toggle(entry.key)}
159
+ />
160
+ <span class="truncate">
161
+ {entry.key === '' ? strings.emptyValue : (entry.label ?? entry.key)}
162
+ </span>
163
+ </Label>
164
+ {/each}
165
+ </div>
166
+ {/if}
167
+ {/if}
168
+ </div>