@podosoft/podokit 0.4.0 → 0.5.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 (141) hide show
  1. package/README.md +2 -2
  2. package/dist/add.d.ts +2 -0
  3. package/dist/add.js +5 -1
  4. package/dist/assemble.d.ts +14 -0
  5. package/dist/assemble.js +60 -0
  6. package/dist/create.d.ts +2 -0
  7. package/dist/create.js +7 -0
  8. package/dist/eject.d.ts +11 -0
  9. package/dist/eject.js +27 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +122 -1
  12. package/dist/inspect.d.ts +43 -0
  13. package/dist/inspect.js +82 -0
  14. package/dist/lockfile.d.ts +80 -0
  15. package/dist/lockfile.js +193 -0
  16. package/dist/templates/base/dot-env.example +11 -6
  17. package/dist/templates/fullstack-nest-svelte/README.md +2 -2
  18. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -0
  19. package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +6 -3
  20. package/dist/templates/fullstack-nest-svelte/apps/api/src/common/app-exception.ts +4 -12
  21. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +1 -1
  22. package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +1 -1
  23. package/dist/templates/fullstack-nest-svelte/apps/web/package.json +2 -2
  24. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-content.svelte +1 -1
  25. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-item.svelte +1 -1
  26. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/index.ts +7 -0
  27. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
  28. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +1 -1
  29. package/dist/templates/fullstack-nest-svelte/dot-env.example +10 -5
  30. package/dist/templates/fullstack-nest-svelte/infra/docker/docker-compose.yml +37 -0
  31. package/dist/templates/fullstack-nest-svelte/infra/docker/sms-sink.mjs +40 -0
  32. package/dist/templates/fullstack-nest-svelte/tests/README.md +1 -1
  33. package/dist/templates/fullstack-nest-svelte/tests/package.json +2 -1
  34. package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +1 -1
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte +7 -1
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +149 -0
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-pagination.svelte +21 -22
  39. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +93 -0
  40. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +224 -8
  41. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +22 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +19 -3
  43. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/accept-invitation/[id]/+page.svelte +36 -0
  44. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/+page.svelte +21 -1
  45. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +360 -49
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.server.ts +2 -4
  47. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +82 -37
  48. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.server.ts +6 -0
  49. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +264 -0
  50. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.server.ts +3 -4
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +79 -45
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +26 -0
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +322 -0
  54. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.server.ts +3 -5
  55. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +254 -108
  56. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/forgot-password/+page.svelte +8 -1
  57. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +14 -0
  58. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +127 -5
  59. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +18 -2
  60. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/verify-email/+page.svelte +63 -0
  61. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +54 -0
  62. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +51 -2
  63. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +80 -0
  64. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +264 -1
  65. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +120 -0
  66. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +10 -10
  67. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +2 -2
  68. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +49 -0
  69. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +35 -0
  70. package/dist/templates/modules/admin-dashboard/files/tests/helpers/totp.ts +9 -0
  71. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +1 -1
  72. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +12 -1
  73. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +77 -6
  74. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +1 -1
  75. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -0
  76. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +60 -0
  77. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +42 -0
  78. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +1 -0
  79. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +57 -0
  80. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +52 -0
  81. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +36 -0
  82. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +9 -2
  83. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +34 -0
  84. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +32 -0
  85. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +28 -12
  86. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +35 -3
  87. package/dist/templates/modules/admin-dashboard/module.manifest.json +13 -7
  88. package/dist/templates/modules/api-key-auth/module.manifest.json +3 -3
  89. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +16 -0
  90. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +4 -0
  91. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.interceptor.ts +2 -0
  92. package/dist/templates/modules/audit-log/module.manifest.json +15 -9
  93. package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +78 -13
  94. package/dist/templates/modules/auth/files/apps/api/src/account/account.module.ts +2 -1
  95. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +73 -0
  96. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +197 -52
  97. package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +12 -0
  98. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +56 -0
  99. package/dist/templates/modules/auth/files/apps/api/src/auth/org-permissions.ts +22 -0
  100. package/dist/templates/modules/auth/files/apps/api/src/auth/permissions.ts +23 -0
  101. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.controller.ts +33 -0
  102. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.entity.ts +26 -0
  103. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.module.ts +12 -0
  104. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +118 -0
  105. package/dist/templates/modules/auth/files/apps/api/src/mail/mailer.ts +65 -0
  106. package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +38 -0
  107. package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +27 -0
  108. package/dist/templates/modules/auth/files/apps/api/src/settings/app-setting.entity.ts +16 -0
  109. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +25 -0
  110. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.module.ts +11 -0
  111. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +57 -0
  112. package/dist/templates/modules/auth/files/apps/api/src/sms/sms.ts +23 -0
  113. package/dist/templates/modules/auth/module.manifest.json +54 -20
  114. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +6 -3
  115. package/dist/templates/modules/bullmq/module.manifest.json +4 -4
  116. package/dist/templates/modules/file-upload/module.manifest.json +3 -3
  117. package/dist/templates/modules/job-progress/module.manifest.json +9 -9
  118. package/dist/templates/modules/logging/module.manifest.json +3 -3
  119. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -3
  120. package/dist/templates/modules/object-storage-s3/module.manifest.json +5 -5
  121. package/dist/templates/modules/rate-limit/module.manifest.json +2 -2
  122. package/dist/templates/modules/redis/module.manifest.json +4 -4
  123. package/dist/templates/modules/sse/module.manifest.json +4 -4
  124. package/dist/templates/todo/README.md +2 -2
  125. package/dist/templates/todo/apps/api/package.json +1 -0
  126. package/dist/templates/todo/apps/api/src/app.module.ts +6 -3
  127. package/dist/templates/todo/apps/api/src/common/app-exception.ts +4 -12
  128. package/dist/templates/todo/apps/api/src/config/env.validation.ts +1 -1
  129. package/dist/templates/todo/apps/api/src/main.ts +1 -1
  130. package/dist/templates/todo/apps/web/package.json +2 -2
  131. package/dist/templates/todo/apps/web/src/lib/components/ui/switch/index.ts +7 -0
  132. package/dist/templates/todo/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
  133. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +1 -1
  134. package/dist/templates/todo/dot-env.example +10 -5
  135. package/dist/templates/todo/infra/docker/docker-compose.yml +4 -0
  136. package/dist/templates/todo/tests/README.md +1 -1
  137. package/dist/templates/todo/tests/package.json +2 -1
  138. package/dist/templates/todo/tests/playwright.config.ts +1 -1
  139. package/dist/update.d.ts +49 -0
  140. package/dist/update.js +158 -0
  141. package/package.json +2 -2
@@ -0,0 +1,149 @@
1
+ <script lang="ts" module>
2
+ export type DataTableColumn<Row> = {
3
+ key: string;
4
+ label: string;
5
+ sortable?: boolean;
6
+ class?: string;
7
+ // Value used for client-side sorting (defaults to row[key]). Use for nested
8
+ // or derived fields, e.g. (s) => s.user.email.
9
+ value?: (row: Row) => string | number | boolean | Date | null | undefined;
10
+ };
11
+ export type SortState = { key: string; dir: "asc" | "desc" };
12
+
13
+ // Shared page size for all admin tables — sized so a desktop viewport shows a
14
+ // full list at a glance (rather than the old 5-row pages). Change it here to
15
+ // retune every table at once.
16
+ export const DEFAULT_PAGE_SIZE = 15;
17
+ </script>
18
+
19
+ <script lang="ts" generics="Row">
20
+ import type { Snippet } from "svelte";
21
+ import { Button } from "$lib/components/ui/button";
22
+ import * as Table from "$lib/components/ui/table";
23
+ import TablePagination from "$lib/components/table-pagination.svelte";
24
+ import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
25
+ import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
26
+ import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
27
+
28
+ // The one table used across the admin console: sortable headers + a shared
29
+ // pagination footer. Two modes:
30
+ // - client (default): pass all `rows`; sorting + paging happen here.
31
+ // - manual (server): set manualSort/manualPagination, pass `total`, and refetch
32
+ // in `onChange` using the emitted sort + page.
33
+ type Props = {
34
+ columns: DataTableColumn<Row>[];
35
+ rows: Row[];
36
+ row: Snippet<[Row]>; // renders the <Table.Cell>s for one row
37
+ getKey?: (row: Row, index: number) => string | number;
38
+ empty?: string;
39
+ sort?: SortState | null;
40
+ manualSort?: boolean;
41
+ page?: number;
42
+ perPage?: number; // 0 => no pagination footer
43
+ total?: number; // server total (defaults to rows.length)
44
+ label?: string;
45
+ manualPagination?: boolean;
46
+ onChange?: (state: { sort: SortState | null; page: number }) => void;
47
+ };
48
+ let {
49
+ columns,
50
+ rows,
51
+ row,
52
+ getKey = (_r, i) => i,
53
+ empty = "No data.",
54
+ sort = $bindable(null),
55
+ manualSort = false,
56
+ page = $bindable(1),
57
+ perPage = 0,
58
+ total,
59
+ label,
60
+ manualPagination = false,
61
+ onChange,
62
+ }: Props = $props();
63
+
64
+ function toggle(col: DataTableColumn<Row>): void {
65
+ if (!col.sortable) return;
66
+ sort =
67
+ sort?.key === col.key
68
+ ? { key: col.key, dir: sort.dir === "asc" ? "desc" : "asc" }
69
+ : { key: col.key, dir: "asc" };
70
+ if (!manualPagination) page = 1;
71
+ onChange?.({ sort, page });
72
+ }
73
+
74
+ function cmp(a: unknown, b: unknown): number {
75
+ if (a == null && b == null) return 0;
76
+ if (a == null) return -1;
77
+ if (b == null) return 1;
78
+ if (typeof a === "number" && typeof b === "number") return a - b;
79
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
80
+ return String(a).localeCompare(String(b), undefined, { numeric: true });
81
+ }
82
+
83
+ const sorted = $derived.by(() => {
84
+ if (manualSort || !sort) return rows;
85
+ const active = sort;
86
+ const col = columns.find((c) => c.key === active.key);
87
+ const read = (r: Row) => (col?.value ? col.value(r) : (r as Record<string, unknown>)[active.key]);
88
+ const factor = active.dir === "asc" ? 1 : -1;
89
+ return [...rows].sort((a, b) => cmp(read(a), read(b)) * factor);
90
+ });
91
+ const view = $derived.by(() =>
92
+ perPage && !manualPagination ? sorted.slice((page - 1) * perPage, page * perPage) : sorted,
93
+ );
94
+ const count = $derived(total ?? rows.length);
95
+ </script>
96
+
97
+ <div class="rounded-md border">
98
+ <Table.Root>
99
+ <Table.Header>
100
+ <Table.Row>
101
+ {#each columns as col (col.key)}
102
+ <Table.Head
103
+ class={col.class}
104
+ aria-sort={sort?.key === col.key ? (sort.dir === "asc" ? "ascending" : "descending") : undefined}
105
+ >
106
+ {#if col.sortable}
107
+ <Button variant="ghost" size="sm" class="-mx-2 h-auto gap-1 px-2 py-1 font-medium" onclick={() => toggle(col)}>
108
+ {col.label}
109
+ {#if sort?.key === col.key}
110
+ {#if sort.dir === "asc"}<ChevronUpIcon class="size-3.5" />{:else}<ChevronDownIcon class="size-3.5" />{/if}
111
+ {:else}
112
+ <ChevronsUpDownIcon class="size-3.5 opacity-40" />
113
+ {/if}
114
+ </Button>
115
+ {:else}
116
+ {col.label}
117
+ {/if}
118
+ </Table.Head>
119
+ {/each}
120
+ </Table.Row>
121
+ </Table.Header>
122
+ <Table.Body>
123
+ {#each view as r, i (getKey(r, i))}
124
+ <Table.Row>{@render row(r)}</Table.Row>
125
+ {:else}
126
+ <Table.Row>
127
+ <Table.Cell colspan={columns.length} class="text-muted-foreground py-8 text-center">{empty}</Table.Cell>
128
+ </Table.Row>
129
+ {/each}
130
+ </Table.Body>
131
+ <!-- Pagination lives in the table footer so every DataTable is consistent
132
+ top-to-bottom (sortable header, rows, footer pager). -->
133
+ {#if perPage}
134
+ <Table.Footer class="bg-transparent">
135
+ <Table.Row class="hover:bg-transparent">
136
+ <Table.Cell colspan={columns.length} class="py-2">
137
+ <TablePagination
138
+ {count}
139
+ {perPage}
140
+ bind:page
141
+ {label}
142
+ onPageChange={manualPagination ? (p) => onChange?.({ sort, page: p }) : undefined}
143
+ />
144
+ </Table.Cell>
145
+ </Table.Row>
146
+ </Table.Footer>
147
+ {/if}
148
+ </Table.Root>
149
+ </div>
@@ -1,9 +1,10 @@
1
1
  <script lang="ts">
2
2
  import * as Pagination from "$lib/components/ui/pagination";
3
3
 
4
- // Shared table footer: a count label plus shadcn-svelte pagination that appears
5
- // only when the list spans more than one page. Bind `page` (1-based). For
6
- // server-side lists pass `onPageChange` to refetch; client-side lists just bind.
4
+ // Shared table footer: a count label plus shadcn-svelte pagination. The pager
5
+ // is always shown (even for a single page or an empty list) so the footer stays
6
+ // consistent across tables. Bind `page` (1-based). For server-side lists pass
7
+ // `onPageChange` to refetch; client-side lists just bind.
7
8
  type Props = {
8
9
  count: number;
9
10
  perPage?: number;
@@ -18,23 +19,21 @@
18
19
  {#if label}
19
20
  <span class="text-muted-foreground shrink-0 text-sm whitespace-nowrap">{label}</span>
20
21
  {/if}
21
- {#if count > perPage}
22
- <Pagination.Root {count} {perPage} bind:page {onPageChange}>
23
- {#snippet children({ pages, currentPage })}
24
- <Pagination.Content>
25
- <Pagination.Item><Pagination.PrevButton /></Pagination.Item>
26
- {#each pages as p (p.key)}
27
- {#if p.type === "ellipsis"}
28
- <Pagination.Item><Pagination.Ellipsis /></Pagination.Item>
29
- {:else}
30
- <Pagination.Item>
31
- <Pagination.Link page={p} isActive={currentPage === p.value}>{p.value}</Pagination.Link>
32
- </Pagination.Item>
33
- {/if}
34
- {/each}
35
- <Pagination.Item><Pagination.NextButton /></Pagination.Item>
36
- </Pagination.Content>
37
- {/snippet}
38
- </Pagination.Root>
39
- {/if}
22
+ <Pagination.Root count={Math.max(count, 1)} {perPage} bind:page {onPageChange}>
23
+ {#snippet children({ pages, currentPage })}
24
+ <Pagination.Content>
25
+ <Pagination.Item><Pagination.PrevButton /></Pagination.Item>
26
+ {#each pages as p (p.key)}
27
+ {#if p.type === "ellipsis"}
28
+ <Pagination.Item><Pagination.Ellipsis /></Pagination.Item>
29
+ {:else}
30
+ <Pagination.Item>
31
+ <Pagination.Link page={p} isActive={currentPage === p.value}>{p.value}</Pagination.Link>
32
+ </Pagination.Item>
33
+ {/if}
34
+ {/each}
35
+ <Pagination.Item><Pagination.NextButton /></Pagination.Item>
36
+ </Pagination.Content>
37
+ {/snippet}
38
+ </Pagination.Root>
40
39
  </div>
@@ -0,0 +1,93 @@
1
+ <script lang="ts" module>
2
+ export type ToolbarFilter = {
3
+ key: string;
4
+ label: string;
5
+ // First option is the "all" choice (usually value "").
6
+ options: { value: string; label: string }[];
7
+ };
8
+ export type ToolbarSearchField = { value: string; label: string };
9
+ </script>
10
+
11
+ <script lang="ts">
12
+ import { Button } from "$lib/components/ui/button";
13
+ import { Input } from "$lib/components/ui/input";
14
+ import { Label } from "$lib/components/ui/label";
15
+ import * as Select from "$lib/components/ui/select";
16
+
17
+ // Shared toolbar above a DataTable. Filters (enum columns) and search (free-text
18
+ // columns that can't be a filter) both commit together on the Search button /
19
+ // Enter — never per keystroke, so many conditions apply in one pass. The search
20
+ // field itself is a select, so a page can search by Email, Name, ... whatever
21
+ // isn't covered by a filter.
22
+ type Props = {
23
+ filters?: ToolbarFilter[];
24
+ filterValues?: Record<string, string>;
25
+ searchFields?: ToolbarSearchField[]; // ≥1 to show the search row
26
+ searchField?: string; // selected field value (bound)
27
+ searchPlaceholder?: string;
28
+ search?: string; // the query text (bound)
29
+ filterHeading?: string;
30
+ searchHeading?: string;
31
+ searchButton?: string;
32
+ onSearch: () => void; // commit filters + search + field together
33
+ };
34
+ let {
35
+ filters = [],
36
+ filterValues = $bindable({}),
37
+ searchFields = [],
38
+ searchField = $bindable(""),
39
+ searchPlaceholder,
40
+ search = $bindable(""),
41
+ filterHeading = "Filter",
42
+ searchHeading = "Search",
43
+ searchButton = "Search",
44
+ onSearch,
45
+ }: Props = $props();
46
+
47
+ function filterTrigger(f: ToolbarFilter): string {
48
+ return f.options.find((o) => o.value === (filterValues[f.key] ?? ""))?.label ?? f.options[0]?.label ?? "";
49
+ }
50
+ const searchFieldLabel = $derived(
51
+ searchFields.find((f) => f.value === searchField)?.label ?? searchFields[0]?.label ?? "",
52
+ );
53
+ </script>
54
+
55
+ <div class="flex flex-col gap-3 rounded-md border p-3">
56
+ {#if filters.length}
57
+ <div class="flex flex-wrap items-center gap-x-4 gap-y-2">
58
+ <span class="text-muted-foreground w-14 shrink-0 text-sm font-medium">{filterHeading}</span>
59
+ {#each filters as f (f.key)}
60
+ <div class="flex items-center gap-2">
61
+ <Label class="text-sm">{f.label}</Label>
62
+ <Select.Root type="single" bind:value={filterValues[f.key]}>
63
+ <Select.Trigger class="w-36">{filterTrigger(f)}</Select.Trigger>
64
+ <Select.Content>
65
+ {#each f.options as o (o.value)}
66
+ <Select.Item value={o.value}>{o.label}</Select.Item>
67
+ {/each}
68
+ </Select.Content>
69
+ </Select.Root>
70
+ </div>
71
+ {/each}
72
+ </div>
73
+ {/if}
74
+ {#if searchFields.length}
75
+ <form class="flex flex-wrap items-center gap-2" onsubmit={(e) => { e.preventDefault(); onSearch(); }}>
76
+ <span class="text-muted-foreground w-14 shrink-0 text-sm font-medium">{searchHeading}</span>
77
+ {#if searchFields.length > 1}
78
+ <Select.Root type="single" bind:value={searchField}>
79
+ <Select.Trigger class="w-32">{searchFieldLabel}</Select.Trigger>
80
+ <Select.Content>
81
+ {#each searchFields as f (f.value)}
82
+ <Select.Item value={f.value}>{f.label}</Select.Item>
83
+ {/each}
84
+ </Select.Content>
85
+ </Select.Root>
86
+ {:else}
87
+ <Label class="text-sm" for="toolbar-search">{searchFields[0].label}</Label>
88
+ {/if}
89
+ <Input id="toolbar-search" bind:value={search} placeholder={searchPlaceholder} class="max-w-xs flex-1" />
90
+ <Button type="submit">{searchButton}</Button>
91
+ </form>
92
+ {/if}
93
+ </div>