runeforge 0.0.53 → 0.0.55
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.
- package/LICENSE +21 -21
- package/README.md +1342 -1222
- package/dist/components/Avatar.svelte +31 -31
- package/dist/components/IconRenderer.svelte +22 -22
- package/dist/components/Modal.svelte +75 -75
- package/dist/components/common/Header.svelte +40 -40
- package/dist/components/crud/EmbeddedField.svelte +175 -175
- package/dist/components/crud/Field.svelte +376 -376
- package/dist/components/crud/GenericCRUD.svelte +426 -435
- package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
- package/dist/components/crud/SearchInput.svelte +53 -53
- package/dist/components/crud/columns/Avatar.svelte +15 -15
- package/dist/components/crud/columns/Icon.svelte +8 -8
- package/dist/components/crud/utils/reorder.d.ts +4 -0
- package/dist/components/crud/utils/reorder.js +13 -0
- package/dist/components/crud/views/Create.svelte +232 -232
- package/dist/components/crud/views/Read.svelte +124 -124
- package/dist/components/crud/views/Update.svelte +208 -208
- package/dist/components/crud/views/list/List.svelte +291 -0
- package/dist/components/crud/views/{List.svelte.d.ts → list/List.svelte.d.ts} +4 -14
- package/dist/components/crud/views/list/Modals.svelte +56 -0
- package/dist/components/crud/views/list/Modals.svelte.d.ts +36 -0
- package/dist/components/crud/views/list/Table.svelte +176 -0
- package/dist/components/crud/views/list/Table.svelte.d.ts +59 -0
- package/dist/components/crud/views/list/Toolbar.svelte +341 -0
- package/dist/components/crud/views/list/Toolbar.svelte.d.ts +46 -0
- package/dist/components/form/Button.svelte +27 -27
- package/dist/components/form/Label.svelte +37 -37
- package/dist/components/form/MultiSelect.svelte +248 -248
- package/dist/components/form/PasswordInput.svelte +68 -68
- package/dist/components/form/Required.svelte +1 -1
- package/dist/components/form/Select.svelte +209 -209
- package/dist/components/form/Tree.svelte +62 -62
- package/dist/components/form/TreeNode.svelte +66 -66
- package/dist/components/navigation/Breadcrumbs.svelte +111 -111
- package/dist/components/table/ColumnFilter.svelte +168 -168
- package/dist/components/table/PaginatedTable.svelte +536 -215
- package/dist/components/table/PaginatedTable.svelte.d.ts +11 -2
- package/dist/components/table/Paginator.svelte +113 -113
- package/dist/components/table/SortHeader.svelte +43 -43
- package/dist/components/table/TableBody.svelte +150 -70
- package/dist/components/table/TableBody.svelte.d.ts +14 -1
- package/dist/components/table/TableHeader.svelte +88 -83
- package/dist/components/table/TableHeader.svelte.d.ts +1 -0
- package/dist/components/table/sortable.d.ts +27 -0
- package/dist/components/table/sortable.js +55 -0
- package/dist/components/table/utils.d.ts +28 -1
- package/dist/components/table/utils.js +75 -0
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/es.js +2 -0
- package/dist/i18n/types.d.ts +2 -0
- package/dist/icons/defaults/Clear.svelte +6 -6
- package/dist/icons/defaults/Create.svelte +6 -6
- package/dist/icons/defaults/Delete.svelte +6 -6
- package/dist/icons/defaults/Download.svelte +7 -7
- package/dist/icons/defaults/Edit.svelte +7 -7
- package/dist/icons/defaults/Filter.svelte +6 -6
- package/dist/icons/defaults/FilterActive.svelte +6 -6
- package/dist/icons/defaults/Folder.svelte +6 -6
- package/dist/icons/defaults/Grip.svelte +7 -0
- package/dist/icons/defaults/Grip.svelte.d.ts +7 -0
- package/dist/icons/defaults/Home.svelte +6 -6
- package/dist/icons/defaults/PasswordHide.svelte +9 -9
- package/dist/icons/defaults/PasswordShow.svelte +7 -7
- package/dist/icons/defaults/SortAsc.svelte +6 -6
- package/dist/icons/defaults/SortDesc.svelte +6 -6
- package/dist/icons/defaults/SortNone.svelte +6 -6
- package/dist/icons/defaults/View.svelte +7 -7
- package/dist/icons/sets/bootstrap.js +2 -1
- package/dist/icons/sets/default.js +2 -0
- package/dist/icons/types.d.ts +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/types/attribute.d.ts +9 -0
- package/dist/types/crud.d.ts +72 -1
- package/dist/types/table.d.ts +56 -0
- package/package.json +3 -1
- package/dist/components/crud/views/List.svelte +0 -584
|
@@ -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.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.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>
|