pika-ux 1.0.0 → 1.0.2
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/package.json +8 -1
- package/src/icons/lucide/index.d.ts +28 -0
- package/src/pika/copy-button/copy-button.svelte +8 -6
- package/src/pika/pika-table/pika-table-pagination.svelte +64 -31
- package/src/pika/pika-table/pika-table-row-actions.svelte +1 -1
- package/src/pika/pika-table/pika-table.svelte +113 -65
- package/src/pika/pika-table/types.ts +21 -3
- package/src/pika/pika-toggle/index.ts +13 -0
- package/src/pika/pika-toggle/toggle.svelte +52 -0
- package/src/pika/pika-toggle-group/index.ts +10 -0
- package/src/pika/pika-toggle-group/toggle-group-item.svelte +88 -0
- package/src/pika/pika-toggle-group/toggle-group.svelte +72 -0
- package/src/pika/popup-help/popup-help.svelte +9 -3
- package/src/shadcn/chart/chart-container.svelte +69 -0
- package/src/shadcn/chart/chart-style.svelte +28 -0
- package/src/shadcn/chart/chart-tooltip.svelte +126 -0
- package/src/shadcn/chart/chart-utils.ts +37 -0
- package/src/shadcn/chart/index.ts +4 -0
- package/src/shadcn/toggle/toggle.svelte +36 -45
- package/src/shadcn/toggle-group/toggle-group-item.svelte +20 -23
- package/src/shadcn/toggle-group/toggle-group.svelte +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pika-ux",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./pika/*": "./src/pika/*",
|
|
@@ -29,14 +29,21 @@
|
|
|
29
29
|
"@tailwindcss/typography": "^0.5.16",
|
|
30
30
|
"@tailwindcss/vite": "^4.1.13",
|
|
31
31
|
"@tsconfig/svelte": "^5.0.5",
|
|
32
|
+
"@types/d3-array": "^3.2.2",
|
|
33
|
+
"@types/d3-scale": "^4.0.9",
|
|
34
|
+
"@types/d3-shape": "^3.1.7",
|
|
32
35
|
"@types/fs-extra": "^11.0.4",
|
|
33
36
|
"@types/indefinite": "^2.3.4",
|
|
34
37
|
"@types/inquirer": "^9.0.8",
|
|
35
38
|
"@types/node": "^22.15.17",
|
|
36
39
|
"@types/semver": "^7.7.0",
|
|
37
40
|
"bits-ui": "^1.8.0",
|
|
41
|
+
"d3-array": "^3.2.4",
|
|
42
|
+
"d3-scale": "^4.0.2",
|
|
43
|
+
"d3-shape": "^3.2.0",
|
|
38
44
|
"embla-carousel-svelte": "^8.6.0",
|
|
39
45
|
"eslint": "^9.26.0",
|
|
46
|
+
"layerchart": "^2.0.0-next.42",
|
|
40
47
|
"tailwind-merge": "^3.3.1",
|
|
41
48
|
"tailwind-variants": "^3.1.1",
|
|
42
49
|
"tailwindcss": "^4.1.11",
|
|
@@ -1256,6 +1256,13 @@ declare module '$icons/lucide/bird' {
|
|
|
1256
1256
|
export default component;
|
|
1257
1257
|
}
|
|
1258
1258
|
|
|
1259
|
+
declare module '$icons/lucide/birdhouse' {
|
|
1260
|
+
import type { Component } from 'svelte';
|
|
1261
|
+
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
1262
|
+
const component: Component<SvelteHTMLElements['svg']>;
|
|
1263
|
+
export default component;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1259
1266
|
declare module '$icons/lucide/bitcoin' {
|
|
1260
1267
|
import type { Component } from 'svelte';
|
|
1261
1268
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
@@ -5029,6 +5036,13 @@ declare module '$icons/lucide/gamepad-2' {
|
|
|
5029
5036
|
export default component;
|
|
5030
5037
|
}
|
|
5031
5038
|
|
|
5039
|
+
declare module '$icons/lucide/gamepad-directional' {
|
|
5040
|
+
import type { Component } from 'svelte';
|
|
5041
|
+
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
5042
|
+
const component: Component<SvelteHTMLElements['svg']>;
|
|
5043
|
+
export default component;
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5032
5046
|
declare module '$icons/lucide/gauge' {
|
|
5033
5047
|
import type { Component } from 'svelte';
|
|
5034
5048
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
@@ -7038,6 +7052,13 @@ declare module '$icons/lucide/monitor-check' {
|
|
|
7038
7052
|
export default component;
|
|
7039
7053
|
}
|
|
7040
7054
|
|
|
7055
|
+
declare module '$icons/lucide/monitor-cloud' {
|
|
7056
|
+
import type { Component } from 'svelte';
|
|
7057
|
+
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
7058
|
+
const component: Component<SvelteHTMLElements['svg']>;
|
|
7059
|
+
export default component;
|
|
7060
|
+
}
|
|
7061
|
+
|
|
7041
7062
|
declare module '$icons/lucide/monitor-cog' {
|
|
7042
7063
|
import type { Component } from 'svelte';
|
|
7043
7064
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
@@ -7129,6 +7150,13 @@ declare module '$icons/lucide/moon-star' {
|
|
|
7129
7150
|
export default component;
|
|
7130
7151
|
}
|
|
7131
7152
|
|
|
7153
|
+
declare module '$icons/lucide/motorbike' {
|
|
7154
|
+
import type { Component } from 'svelte';
|
|
7155
|
+
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
7156
|
+
const component: Component<SvelteHTMLElements['svg']>;
|
|
7157
|
+
export default component;
|
|
7158
|
+
}
|
|
7159
|
+
|
|
7132
7160
|
declare module '$icons/lucide/mountain' {
|
|
7133
7161
|
import type { Component } from 'svelte';
|
|
7134
7162
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
|
|
22
22
|
/** When provided, this is the title of the button */
|
|
23
23
|
title?: string;
|
|
24
|
+
|
|
25
|
+
size?: 'small' | 'medium';
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const { children, value: propValue, embedded = false, truncateAfter = 0, showTextAsLink = false, linkCallbackFn, title }: Props = $props();
|
|
28
|
+
const { children, value: propValue, embedded = false, truncateAfter = 0, showTextAsLink = false, linkCallbackFn, title, size = 'medium' }: Props = $props();
|
|
27
29
|
|
|
28
30
|
let hiddenRef = $state<HTMLElement>() as HTMLElement;
|
|
29
31
|
let value: string | undefined = $state(undefined);
|
|
@@ -118,16 +120,16 @@
|
|
|
118
120
|
|
|
119
121
|
<span class="inline-flex w-fit items-center {embedded ? '' : 'border border-gray-200 rounded-sm'}">
|
|
120
122
|
{#if showTextAsLink}
|
|
121
|
-
<Button class="p-0" variant="link" onclick={() => linkCallbackFn?.()}>{buttonTitle}</Button>
|
|
123
|
+
<Button class="p-0 {size === 'small' ? 'text-xs' : ''}" variant="link" onclick={() => linkCallbackFn?.()}>{buttonTitle}</Button>
|
|
122
124
|
{:else}
|
|
123
|
-
<span class={embedded ? '' : 'border-r border-gray-200 px-2'}>{buttonTitle}</span>
|
|
125
|
+
<span class="{embedded ? '' : 'border-r border-gray-200 px-2'} {size === 'small' ? 'text-xs' : ''}">{buttonTitle}</span>
|
|
124
126
|
{/if}
|
|
125
|
-
<span class="w-6 h-6 flex items-center justify-center">
|
|
127
|
+
<span class="{size === 'small' ? 'w-4 h-4 ml-1' : 'w-6 h-6'} flex items-center justify-center">
|
|
126
128
|
{#if showCheckmark}
|
|
127
|
-
<Check class="w-3.5 h-3.5 text-green-500" />
|
|
129
|
+
<Check class="{size === 'small' ? 'w-2.5 h-2.5' : 'w-3.5 h-3.5'} text-green-500" />
|
|
128
130
|
{:else}
|
|
129
131
|
<Button variant="ghost" class="h-full w-full min-h-0 p-0 ml-1 rounded-none hover:border-blue-100 hover:border hover:rounded-sm" onclick={copy} disabled={!value}>
|
|
130
|
-
<Copy class="w-3.5 h-3.5 text-gray-400" />
|
|
132
|
+
<Copy class="{size === 'small' ? 'w-2.5 h-2.5' : 'w-3.5 h-3.5'} text-gray-400" />
|
|
131
133
|
</Button>
|
|
132
134
|
{/if}
|
|
133
135
|
</span>
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
type TData = unknown;
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
|
+
<!-- @component
|
|
6
|
+
PikaTablePagination - Pagination controls for PikaTable
|
|
7
|
+
-->
|
|
5
8
|
<script lang="ts" generics="TData">
|
|
9
|
+
// @ts-ignore - Props interface is private but this is a Svelte framework limitation
|
|
6
10
|
import ChevronLeft from '$icons/lucide/chevron-left';
|
|
7
11
|
import ChevronRight from '$icons/lucide/chevron-right';
|
|
8
12
|
import ChevronsLeft from '$icons/lucide/chevrons-left';
|
|
@@ -15,48 +19,77 @@
|
|
|
15
19
|
interface Props {
|
|
16
20
|
table: Table<TData>;
|
|
17
21
|
serverSide: ServerSideConfig;
|
|
22
|
+
showRowsPerPage?: boolean;
|
|
23
|
+
globalFilterActive?: boolean;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
let { table, serverSide }: Props = $props();
|
|
26
|
+
let { table, serverSide, showRowsPerPage = true, globalFilterActive = false }: Props = $props();
|
|
21
27
|
|
|
22
28
|
// For cursor-based pagination, we can't jump to arbitrary pages
|
|
23
29
|
const isCursorBased = $derived(serverSide?.paginationMode === 'cursor');
|
|
30
|
+
|
|
31
|
+
// Calculate counts for display
|
|
32
|
+
const totalLoadedRows = $derived(table.getCoreRowModel().rows.length);
|
|
33
|
+
const visibleRows = $derived(table.getRowModel().rows.length);
|
|
34
|
+
const selectedRows = $derived(table.getFilteredSelectedRowModel().rows.length);
|
|
24
35
|
</script>
|
|
25
36
|
|
|
26
37
|
<div class="flex items-center justify-between px-2">
|
|
27
|
-
<div class="
|
|
28
|
-
{#if
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
<div class="flex-1 text-sm">
|
|
39
|
+
{#if globalFilterActive && visibleRows < totalLoadedRows}
|
|
40
|
+
<!-- Client-side filtering active -->
|
|
41
|
+
<div class="flex items-center gap-2">
|
|
42
|
+
<span class="text-sm font-medium text-blue-700">
|
|
43
|
+
Showing {visibleRows} of {totalLoadedRows} loaded results
|
|
44
|
+
</span>
|
|
45
|
+
{#if selectedRows > 0}
|
|
46
|
+
<span class="text-muted-foreground text-xs">
|
|
47
|
+
({selectedRows} selected)
|
|
48
|
+
</span>
|
|
49
|
+
{/if}
|
|
50
|
+
</div>
|
|
51
|
+
{:else if selectedRows > 0}
|
|
52
|
+
<!-- No filtering, just show selection -->
|
|
53
|
+
<span class="text-muted-foreground">
|
|
54
|
+
{#if serverSide?.tableState?.totalRecords !== undefined}
|
|
55
|
+
{selectedRows} of {serverSide.tableState.totalRecords} total row(s) selected.
|
|
56
|
+
{:else}
|
|
57
|
+
{selectedRows} of {visibleRows} row(s) selected.
|
|
58
|
+
{/if}
|
|
59
|
+
</span>
|
|
31
60
|
{:else}
|
|
32
|
-
|
|
33
|
-
|
|
61
|
+
<!-- No filtering, no selection -->
|
|
62
|
+
<span class="text-muted-foreground">
|
|
63
|
+
{totalLoadedRows} row(s) loaded
|
|
64
|
+
</span>
|
|
34
65
|
{/if}
|
|
35
66
|
</div>
|
|
36
|
-
<div class="flex items-center space-x-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{pageSize}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
<div class="flex items-center space-x-8">
|
|
68
|
+
{#if showRowsPerPage}
|
|
69
|
+
<div class="flex items-center space-x-2">
|
|
70
|
+
<p class="text-sm font-medium">Rows per page</p>
|
|
71
|
+
<Select.Root
|
|
72
|
+
allowDeselect={false}
|
|
73
|
+
type="single"
|
|
74
|
+
value={`${table.getState().pagination.pageSize}`}
|
|
75
|
+
onValueChange={(value) => {
|
|
76
|
+
table.setPageSize(Number(value));
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<Select.Trigger class="h-8 w-[70px]">
|
|
80
|
+
{String(table.getState().pagination.pageSize)}
|
|
81
|
+
</Select.Trigger>
|
|
82
|
+
<Select.Content side="top">
|
|
83
|
+
{#each [10, 20, 50, 100, 500, 1000] as pageSize (pageSize)}
|
|
84
|
+
<Select.Item value={`${pageSize}`}>
|
|
85
|
+
{pageSize}
|
|
86
|
+
</Select.Item>
|
|
87
|
+
{/each}
|
|
88
|
+
</Select.Content>
|
|
89
|
+
</Select.Root>
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
<div class="flex w-[100px] items-center justify-center text-sm font-medium mr-2">
|
|
60
93
|
{#if table.getPageCount() > 0}
|
|
61
94
|
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
62
95
|
{:else}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
{:else if 'menuItems' in item}
|
|
46
46
|
{@render subMenu(item)}
|
|
47
47
|
{:else}
|
|
48
|
-
<DropdownMenu.Item onclick={() => item.onclick?.(row
|
|
48
|
+
<DropdownMenu.Item onclick={() => item.onclick?.(row)}>
|
|
49
49
|
<div class="flex items-center gap-2">
|
|
50
50
|
{#if item.icon}
|
|
51
51
|
<item.icon />
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
type TValue = unknown;
|
|
4
4
|
</script>
|
|
5
5
|
|
|
6
|
+
<!-- @component
|
|
7
|
+
PikaTable - A reusable table component with server-side pagination, sorting, and filtering support
|
|
8
|
+
-->
|
|
6
9
|
<script lang="ts" generics="TData, TValue">
|
|
7
|
-
|
|
10
|
+
// @ts-ignore - Props interface is private but this is a Svelte framework limitation
|
|
8
11
|
import { createSvelteTable } from '../../shadcn/data-table/data-table.svelte';
|
|
9
12
|
import FlexRender from '../../shadcn/data-table/flex-render.svelte';
|
|
10
13
|
import * as Table from '../../shadcn/table';
|
|
@@ -20,10 +23,10 @@
|
|
|
20
23
|
type RowSelectionState,
|
|
21
24
|
type SortingState
|
|
22
25
|
} from '@tanstack/table-core';
|
|
23
|
-
import {
|
|
26
|
+
import { type Snippet } from 'svelte';
|
|
24
27
|
import TablePagination from './pika-table-pagination.svelte';
|
|
25
28
|
import TableToolbar from './pika-table-toolbar.svelte';
|
|
26
|
-
import type { FacetedFilters, GlobalFilterProps, ServerSideConfig, ServerSideTableState } from './types';
|
|
29
|
+
import type { FacetedFilters, GlobalFilterProps, ServerSideConfig, ServerSideTableState, TableSettingsFacade } from './types';
|
|
27
30
|
|
|
28
31
|
interface Props {
|
|
29
32
|
columns: ColumnDef<TData, TValue>[];
|
|
@@ -48,6 +51,15 @@
|
|
|
48
51
|
|
|
49
52
|
// Server-side configuration
|
|
50
53
|
serverSideConfig?: ServerSideConfig;
|
|
54
|
+
|
|
55
|
+
// Facade for table settings (required)
|
|
56
|
+
tableSettings: TableSettingsFacade;
|
|
57
|
+
|
|
58
|
+
// Show rows per page selector in pagination
|
|
59
|
+
showRowsPerPage?: boolean;
|
|
60
|
+
|
|
61
|
+
// Where to show pagination controls
|
|
62
|
+
paginationPlacement?: 'top' | 'bottom' | 'both';
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
let {
|
|
@@ -59,61 +71,64 @@
|
|
|
59
71
|
toolbarContent,
|
|
60
72
|
beneathToolbarContent,
|
|
61
73
|
tableKey,
|
|
62
|
-
serverSideConfig = $bindable<ServerSideConfig>()
|
|
74
|
+
serverSideConfig = $bindable<ServerSideConfig>(),
|
|
75
|
+
tableSettings,
|
|
76
|
+
showRowsPerPage = true,
|
|
77
|
+
paginationPlacement = 'bottom'
|
|
63
78
|
}: Props = $props();
|
|
64
79
|
|
|
65
|
-
const appState = getContext<AppState>('appState');
|
|
66
|
-
const appSettings = appState.settings;
|
|
67
|
-
|
|
68
80
|
let rowSelection = $state<RowSelectionState>({});
|
|
69
|
-
let columnVisibility = $derived(
|
|
70
|
-
//$state<VisibilityState>(appSettings.getTableColumnVisibilityObject(tableKey));
|
|
81
|
+
let columnVisibility = $derived(tableSettings.getTableColumnVisibilityObject(tableKey));
|
|
71
82
|
let columnFilters = $state<ColumnFiltersState>([]);
|
|
72
83
|
let sorting = $state<SortingState>([]);
|
|
73
84
|
let pageIndex = $state(0);
|
|
74
|
-
let pageSize = $derived(
|
|
85
|
+
let pageSize = $derived(tableSettings.getTableNumRows(tableKey, 10));
|
|
75
86
|
|
|
76
87
|
// === SERVER-SIDE LOGIC ===
|
|
77
88
|
|
|
78
89
|
let debouncedRequestData: ((tableState: ServerSideTableState) => Promise<void>) | undefined;
|
|
79
90
|
|
|
80
91
|
// Create debounced function when serverSide config changes
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
$effect(() => {
|
|
93
|
+
const serverState = serverSideConfig;
|
|
94
|
+
if (serverState) {
|
|
95
|
+
const debounceMs = serverState.debounceMs ?? 300;
|
|
96
|
+
debouncedRequestData = debounce(async (tableState: ServerSideTableState) => {
|
|
97
|
+
try {
|
|
98
|
+
await serverState.requestData(tableState);
|
|
99
|
+
|
|
100
|
+
// If using new API, update the tableState
|
|
101
|
+
if (serverSideConfig) {
|
|
102
|
+
serverSideConfig.tableState = { ...serverSideConfig.tableState, ...tableState };
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
serverState.onError?.(error instanceof Error ? error.message : 'Unknown error');
|
|
106
|
+
}
|
|
107
|
+
}, debounceMs);
|
|
108
|
+
} else {
|
|
109
|
+
debouncedRequestData = undefined;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
101
112
|
|
|
102
113
|
// Trigger server request when table state changes
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
function triggerServerRequest() {
|
|
115
|
+
const serverState = serverSideConfig;
|
|
116
|
+
if (!serverState || !debouncedRequestData) return;
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
const tableState: ServerSideTableState = {
|
|
119
|
+
pageIndex,
|
|
120
|
+
pageSize,
|
|
121
|
+
sorting,
|
|
122
|
+
columnFilters,
|
|
123
|
+
totalRecords: serverState.tableState?.totalRecords,
|
|
124
|
+
scrollId: serverState.tableState?.scrollId,
|
|
125
|
+
hasNextPage: serverState.tableState?.hasNextPage,
|
|
126
|
+
isLoading: true,
|
|
127
|
+
requestId: crypto.randomUUID()
|
|
128
|
+
};
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
debouncedRequestData(tableState);
|
|
131
|
+
}
|
|
117
132
|
|
|
118
133
|
// Utility function for debouncing
|
|
119
134
|
function debounce<T extends (...args: any[]) => any>(func: T, wait: number): T {
|
|
@@ -157,7 +172,9 @@
|
|
|
157
172
|
return !!serverSideConfig;
|
|
158
173
|
},
|
|
159
174
|
get manualFiltering() {
|
|
160
|
-
|
|
175
|
+
// CRITICAL: If using client-side global filter, we must disable manualFiltering
|
|
176
|
+
// Otherwise TanStack skips ALL filtering logic before checking manualGlobalFilter
|
|
177
|
+
return serverSideConfig ? !serverSideConfig.clientSideGlobalFilter : false;
|
|
161
178
|
},
|
|
162
179
|
get manualPagination() {
|
|
163
180
|
return !!serverSideConfig;
|
|
@@ -168,13 +185,19 @@
|
|
|
168
185
|
|
|
169
186
|
const tableState = serverSideConfig?.tableState;
|
|
170
187
|
|
|
188
|
+
// For server-side pagination, use the pageSize from tableState if available
|
|
189
|
+
const effectivePageSize = tableState?.pageSize ?? pageSize;
|
|
190
|
+
|
|
171
191
|
// Calculate pageCount if totalRecords is available, regardless of pagination mode
|
|
172
|
-
if (tableState?.totalRecords !== undefined &&
|
|
173
|
-
return Math.ceil(tableState.totalRecords /
|
|
192
|
+
if (tableState?.totalRecords !== undefined && effectivePageSize > 0) {
|
|
193
|
+
return Math.ceil(tableState.totalRecords / effectivePageSize);
|
|
174
194
|
}
|
|
175
195
|
// For cursor-based pagination without totalRecords, we don't know the total
|
|
176
196
|
if (serverState.paginationMode === 'cursor') {
|
|
177
|
-
|
|
197
|
+
// For cursor pagination, determine page count based on hasNextPage
|
|
198
|
+
// If we're on page 0 and hasNextPage is false, we have 1 page
|
|
199
|
+
// If hasNextPage is true, we have at least 2 pages
|
|
200
|
+
return tableState?.hasNextPage ? pageIndex + 2 : pageIndex + 1;
|
|
178
201
|
}
|
|
179
202
|
return undefined; // Unknown page count
|
|
180
203
|
},
|
|
@@ -194,9 +217,9 @@
|
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
// Trigger server request for server-side tables
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
220
|
+
if (serverSideConfig) {
|
|
221
|
+
triggerServerRequest();
|
|
222
|
+
}
|
|
200
223
|
},
|
|
201
224
|
onColumnFiltersChange: (updater) => {
|
|
202
225
|
if (typeof updater === 'function') {
|
|
@@ -206,13 +229,13 @@
|
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
// Trigger server request for server-side tables
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
232
|
+
if (serverSideConfig) {
|
|
233
|
+
triggerServerRequest();
|
|
234
|
+
}
|
|
212
235
|
},
|
|
213
236
|
onColumnVisibilityChange: (updater) => {
|
|
214
237
|
if (typeof updater === 'function') {
|
|
215
|
-
|
|
238
|
+
tableSettings.setTableColumnVisibilityFromObject(tableKey, updater(columnVisibility));
|
|
216
239
|
} else {
|
|
217
240
|
throw new Error('onColumnVisibilityChange updater must be a function');
|
|
218
241
|
//columnVisibility = updater;
|
|
@@ -222,12 +245,12 @@
|
|
|
222
245
|
if (typeof updater === 'function') {
|
|
223
246
|
const val = updater({ pageIndex, pageSize });
|
|
224
247
|
pageIndex = val.pageIndex;
|
|
225
|
-
|
|
248
|
+
tableSettings.setTableNumRows(tableKey, val.pageSize);
|
|
226
249
|
|
|
227
250
|
// Trigger server request for server-side tables
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
251
|
+
if (serverSideConfig) {
|
|
252
|
+
triggerServerRequest();
|
|
253
|
+
}
|
|
231
254
|
} else {
|
|
232
255
|
throw new Error('onPaginationChange updater must be a function');
|
|
233
256
|
}
|
|
@@ -236,17 +259,21 @@
|
|
|
236
259
|
if (globalFilterProps?.showGlobalFilter) {
|
|
237
260
|
globalFilterProps.globalFilterValue = value;
|
|
238
261
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
262
|
+
// Only trigger server request if global filter is server-side (not client-side)
|
|
263
|
+
if (serverSideConfig && !serverSideConfig.clientSideGlobalFilter) {
|
|
264
|
+
triggerServerRequest();
|
|
265
|
+
}
|
|
266
|
+
// Otherwise, global filter will work client-side automatically via getFilteredRowModel
|
|
243
267
|
}
|
|
244
268
|
},
|
|
245
269
|
|
|
246
270
|
// === ROW MODELS ===
|
|
247
271
|
getCoreRowModel: getCoreRowModel(),
|
|
248
272
|
...(serverSideConfig
|
|
249
|
-
? {
|
|
273
|
+
? {
|
|
274
|
+
// Include filtered row model ONLY if using client-side global filter
|
|
275
|
+
...(serverSideConfig.clientSideGlobalFilter ? { getFilteredRowModel: getFilteredRowModel() } : {})
|
|
276
|
+
}
|
|
250
277
|
: {
|
|
251
278
|
getFilteredRowModel: getFilteredRowModel(),
|
|
252
279
|
getPaginationRowModel: getPaginationRowModel(),
|
|
@@ -257,8 +284,20 @@
|
|
|
257
284
|
});
|
|
258
285
|
</script>
|
|
259
286
|
|
|
260
|
-
<div class="
|
|
261
|
-
<
|
|
287
|
+
<div class="{classes ? classes : ''} ">
|
|
288
|
+
<div class="mb-4">
|
|
289
|
+
<TableToolbar {table} {globalFilterProps} {facetedFilters} {toolbarContent} {beneathToolbarContent} />
|
|
290
|
+
</div>
|
|
291
|
+
{#if paginationPlacement === 'top' || paginationPlacement === 'both'}
|
|
292
|
+
<div class="mt-2 pb-1">
|
|
293
|
+
<TablePagination
|
|
294
|
+
{table}
|
|
295
|
+
serverSide={serverSideConfig}
|
|
296
|
+
{showRowsPerPage}
|
|
297
|
+
globalFilterActive={!!(serverSideConfig?.clientSideGlobalFilter && globalFilterProps?.globalFilterValue)}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
{/if}
|
|
262
301
|
<div class="rounded-md border h-full flex flex-col overflow-y-auto">
|
|
263
302
|
<Table.Root class="h-full">
|
|
264
303
|
<Table.Header>
|
|
@@ -291,5 +330,14 @@
|
|
|
291
330
|
</Table.Body>
|
|
292
331
|
</Table.Root>
|
|
293
332
|
</div>
|
|
294
|
-
|
|
333
|
+
{#if paginationPlacement === 'bottom' || paginationPlacement === 'both'}
|
|
334
|
+
<div class="mt-2">
|
|
335
|
+
<TablePagination
|
|
336
|
+
{table}
|
|
337
|
+
serverSide={serverSideConfig}
|
|
338
|
+
{showRowsPerPage}
|
|
339
|
+
globalFilterActive={!!(serverSideConfig?.clientSideGlobalFilter && globalFilterProps?.globalFilterValue)}
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
{/if}
|
|
295
343
|
</div>
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Column, ColumnFiltersState, Row, SortingState } from '@tanstack/table-core';
|
|
1
|
+
import type { Column, ColumnFiltersState, Row, SortingState, VisibilityState } from '@tanstack/table-core';
|
|
3
2
|
import type { Component } from 'svelte';
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Facade for table settings (column visibility, page size, etc.)
|
|
6
|
+
* This allows the pika-table component to be used without depending on AppState
|
|
7
|
+
*/
|
|
8
|
+
export interface TableSettingsFacade {
|
|
9
|
+
getTableColumnVisibilityObject(tableKey: string): VisibilityState;
|
|
10
|
+
getTableNumRows(tableKey: string, defaultValue: number): number;
|
|
11
|
+
setTableNumRows(tableKey: string, value: number): void;
|
|
12
|
+
setTableColumnVisibilityFromObject(tableKey: string, visibility: VisibilityState): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
export interface FacetedFilterData {
|
|
6
16
|
label: string;
|
|
7
17
|
value: string;
|
|
@@ -44,7 +54,7 @@ export interface GlobalFilterProps {
|
|
|
44
54
|
export interface RowActionMenuItemNode<TData> {
|
|
45
55
|
label: string;
|
|
46
56
|
icon?: Component;
|
|
47
|
-
onclick?: (row: Row<TData
|
|
57
|
+
onclick?: (row: Row<TData>) => void;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
export interface RowActionMenuItemSubMenu<TData> {
|
|
@@ -101,6 +111,14 @@ export interface ServerSideConfig {
|
|
|
101
111
|
// === ERROR HANDLING ===
|
|
102
112
|
onError?: (error: string) => void;
|
|
103
113
|
|
|
114
|
+
// === CLIENT-SIDE FILTERING ===
|
|
115
|
+
/**
|
|
116
|
+
* When true, enables client-side global filter even in server-side mode.
|
|
117
|
+
* This allows users to quickly filter already-loaded results without triggering server requests.
|
|
118
|
+
* Column filters still work server-side, but the global filter searches loaded data only.
|
|
119
|
+
*/
|
|
120
|
+
clientSideGlobalFilter?: boolean;
|
|
121
|
+
|
|
104
122
|
// === DYNAMIC TABLE STATE ===
|
|
105
123
|
tableState: ServerSideTableState;
|
|
106
124
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { type VariantProps, tv } from "tailwind-variants";
|
|
3
|
+
|
|
4
|
+
export const toggleVariants = tv({
|
|
5
|
+
base: "hover:bg-gray-200 hover:text-gray-900 data-[state=on]:bg-gray-200 data-[state=on]:text-gray-900 focus-visible:ring-ring inline-flex items-center justify-center gap-2 rounded-sm text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: "bg-white",
|
|
9
|
+
outline:
|
|
10
|
+
"border-input shadow-sm hover:bg-gray-200 hover:text-gray-900 border bg-white",
|
|
11
|
+
},
|
|
12
|
+
size: {
|
|
13
|
+
default: "h-9 min-w-9 px-3",
|
|
14
|
+
sm: "h-8 min-w-8 px-2",
|
|
15
|
+
lg: "h-10 min-w-10 px-3",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: "default",
|
|
20
|
+
size: "default",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type ToggleVariant = VariantProps<typeof toggleVariants>["variant"];
|
|
25
|
+
export type ToggleSize = VariantProps<typeof toggleVariants>["size"];
|
|
26
|
+
export type ToggleVariants = VariantProps<typeof toggleVariants>;
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<script lang="ts">
|
|
30
|
+
import { Toggle as TogglePrimitive } from "bits-ui";
|
|
31
|
+
import { cn } from '../../shadcn/utils.js';
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
ref = $bindable(null),
|
|
35
|
+
pressed = $bindable(false),
|
|
36
|
+
class: className,
|
|
37
|
+
size = "default",
|
|
38
|
+
variant = "default",
|
|
39
|
+
...restProps
|
|
40
|
+
}: TogglePrimitive.RootProps & {
|
|
41
|
+
variant?: ToggleVariant;
|
|
42
|
+
size?: ToggleSize;
|
|
43
|
+
} = $props();
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<TogglePrimitive.Root
|
|
47
|
+
bind:ref
|
|
48
|
+
bind:pressed
|
|
49
|
+
data-slot="toggle"
|
|
50
|
+
class={cn(toggleVariants({ variant, size }), className)}
|
|
51
|
+
{...restProps}
|
|
52
|
+
/>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getToggleGroupCtx } from "./toggle-group.svelte";
|
|
3
|
+
import { cn } from '../../shadcn/utils.js';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
value: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
value,
|
|
15
|
+
class: className,
|
|
16
|
+
disabled = false,
|
|
17
|
+
children
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
const ctx = getToggleGroupCtx();
|
|
21
|
+
|
|
22
|
+
const isSelected = $derived(() => {
|
|
23
|
+
const currentValue = ctx.getValue();
|
|
24
|
+
if (ctx.type === 'single') {
|
|
25
|
+
return currentValue === value;
|
|
26
|
+
} else {
|
|
27
|
+
const arr = Array.isArray(currentValue) ? currentValue : [];
|
|
28
|
+
return arr.includes(value);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function handleClick() {
|
|
33
|
+
if (!disabled) {
|
|
34
|
+
ctx.toggle(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Determine if buttonWidth is a Tailwind class or CSS value
|
|
39
|
+
const widthClass = ctx.buttonWidth?.match(/^(w-|min-w-|max-w-)/) ? ctx.buttonWidth : undefined;
|
|
40
|
+
const widthStyle = ctx.buttonWidth && !widthClass ? `width: ${ctx.buttonWidth};` : '';
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onclick={handleClick}
|
|
46
|
+
disabled={disabled}
|
|
47
|
+
class={cn(
|
|
48
|
+
// Base styles
|
|
49
|
+
'inline-flex items-center justify-center',
|
|
50
|
+
'text-sm font-medium',
|
|
51
|
+
'transition-colors',
|
|
52
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
53
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
54
|
+
|
|
55
|
+
// Size
|
|
56
|
+
'h-9 px-3',
|
|
57
|
+
|
|
58
|
+
// White background by default
|
|
59
|
+
'bg-white',
|
|
60
|
+
|
|
61
|
+
// Border for outline variant
|
|
62
|
+
'border border-input',
|
|
63
|
+
|
|
64
|
+
// Selected state - gray background
|
|
65
|
+
isSelected() && 'bg-gray-100 text-gray-700',
|
|
66
|
+
|
|
67
|
+
// Hover state - use primary color from your theme
|
|
68
|
+
'hover:text-primary',
|
|
69
|
+
|
|
70
|
+
// No rounded corners, borders between buttons
|
|
71
|
+
'border-l-0 first:border-l',
|
|
72
|
+
|
|
73
|
+
// Focus
|
|
74
|
+
'focus:z-10 focus-visible:z-10',
|
|
75
|
+
|
|
76
|
+
// Width
|
|
77
|
+
widthClass,
|
|
78
|
+
|
|
79
|
+
// Flex
|
|
80
|
+
'min-w-0 flex-1 shrink-0',
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
style={widthStyle}
|
|
86
|
+
>
|
|
87
|
+
{@render children?.()}
|
|
88
|
+
</button>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { getContext, setContext } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type ToggleGroupContext = {
|
|
5
|
+
type: 'single' | 'multiple';
|
|
6
|
+
getValue: () => string | string[];
|
|
7
|
+
toggle: (itemValue: string) => void;
|
|
8
|
+
buttonWidth?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function setToggleGroupCtx(ctx: ToggleGroupContext) {
|
|
12
|
+
setContext('pika-toggle-group', ctx);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getToggleGroupCtx() {
|
|
16
|
+
return getContext<ToggleGroupContext>('pika-toggle-group');
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import { cn } from '../../shadcn/utils.js';
|
|
22
|
+
import type { Snippet } from 'svelte';
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
value?: string | string[];
|
|
26
|
+
type?: 'single' | 'multiple';
|
|
27
|
+
buttonWidth?: string;
|
|
28
|
+
class?: string;
|
|
29
|
+
variant?: 'default' | 'outline';
|
|
30
|
+
children?: Snippet;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
type = 'single',
|
|
35
|
+
value = $bindable(type === 'single' ? '' : []),
|
|
36
|
+
buttonWidth,
|
|
37
|
+
class: className,
|
|
38
|
+
variant = 'outline',
|
|
39
|
+
children
|
|
40
|
+
}: Props = $props();
|
|
41
|
+
|
|
42
|
+
function toggle(itemValue: string) {
|
|
43
|
+
if (type === 'single') {
|
|
44
|
+
value = value === itemValue ? '' : itemValue;
|
|
45
|
+
} else {
|
|
46
|
+
const arr = Array.isArray(value) ? value : [];
|
|
47
|
+
const index = arr.indexOf(itemValue);
|
|
48
|
+
if (index > -1) {
|
|
49
|
+
value = arr.filter(v => v !== itemValue);
|
|
50
|
+
} else {
|
|
51
|
+
value = [...arr, itemValue];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setToggleGroupCtx({
|
|
57
|
+
type,
|
|
58
|
+
getValue: () => value,
|
|
59
|
+
toggle,
|
|
60
|
+
buttonWidth
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<div
|
|
65
|
+
class={cn(
|
|
66
|
+
'flex w-fit items-center',
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
role="group"
|
|
70
|
+
>
|
|
71
|
+
{@render children?.()}
|
|
72
|
+
</div>
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import HelpCircleOutline from '$icons/ci/help-circle-outline';
|
|
3
|
+
import InfoIcon from '$icons/lucide/info';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
3
5
|
import * as Popover from '../../shadcn/popover';
|
|
4
6
|
import { cn } from '../../shadcn/utils';
|
|
5
|
-
import type { Snippet } from 'svelte';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
popoverClasses?: string;
|
|
9
10
|
children?: Snippet;
|
|
11
|
+
useInfoIcon?: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
let { popoverClasses, children }: Props = $props();
|
|
14
|
+
let { popoverClasses, children, useInfoIcon = false }: Props = $props();
|
|
13
15
|
|
|
14
16
|
let open = $state(false);
|
|
15
17
|
</script>
|
|
@@ -24,7 +26,11 @@
|
|
|
24
26
|
open = false;
|
|
25
27
|
}}
|
|
26
28
|
>
|
|
27
|
-
|
|
29
|
+
{#if useInfoIcon}
|
|
30
|
+
<InfoIcon class="w-4 h-4 text-gray-400 hover:text-blue-500 transition-colors" />
|
|
31
|
+
{:else}
|
|
32
|
+
<HelpCircleOutline class="w-4 h-4 text-gray-400 hover:text-blue-500 transition-colors" />
|
|
33
|
+
{/if}
|
|
28
34
|
</Popover.Trigger>
|
|
29
35
|
|
|
30
36
|
<Popover.Content class={cn('w-120', popoverClasses)}>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, type WithElementRef } from '../utils.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import ChartStyle from './chart-style.svelte';
|
|
5
|
+
import { setChartContext, type ChartConfig } from './chart-utils.js';
|
|
6
|
+
const uid = $props.id();
|
|
7
|
+
let {
|
|
8
|
+
ref = $bindable(null),
|
|
9
|
+
id = uid,
|
|
10
|
+
class: className,
|
|
11
|
+
children,
|
|
12
|
+
config,
|
|
13
|
+
...restProps
|
|
14
|
+
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
15
|
+
config: ChartConfig;
|
|
16
|
+
} = $props();
|
|
17
|
+
const chartId = `chart-${id || uid.replace(/:/g, '')}`;
|
|
18
|
+
setChartContext({
|
|
19
|
+
get config() {
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div
|
|
26
|
+
bind:this={ref}
|
|
27
|
+
data-chart={chartId}
|
|
28
|
+
data-slot="chart"
|
|
29
|
+
class={cn(
|
|
30
|
+
'flex aspect-video justify-center overflow-visible text-xs',
|
|
31
|
+
// Overrides
|
|
32
|
+
//
|
|
33
|
+
// Stroke around dots/marks when hovering
|
|
34
|
+
'[&_.stroke-white]:stroke-transparent',
|
|
35
|
+
// override the default stroke color of lines
|
|
36
|
+
'[&_.lc-line]:stroke-border/50',
|
|
37
|
+
// by default, layerchart shows a line intersecting the point when hovering, this hides that
|
|
38
|
+
'[&_.lc-highlight-line]:stroke-0',
|
|
39
|
+
// by default, when you hover a point on a stacked series chart, it will drop the opacity
|
|
40
|
+
// of the other series, this overrides that
|
|
41
|
+
'[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text-svg]:overflow-visible [&_.lc-text]:text-xs',
|
|
42
|
+
// We don't want the little tick lines between the axis labels and the chart, so we remove
|
|
43
|
+
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
|
|
44
|
+
// chart.
|
|
45
|
+
'[&_.lc-axis-tick]:stroke-0',
|
|
46
|
+
// We don't want to display the rule on the x/y axis, as there is already going to be
|
|
47
|
+
// a grid line there and rule ends up overlapping the marks because it is rendered after
|
|
48
|
+
// the marks
|
|
49
|
+
'[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
|
|
50
|
+
'[&_.lc-grid-x-radial-line]:stroke-border [&_.lc-grid-x-radial-circle]:stroke-border',
|
|
51
|
+
'[&_.lc-grid-y-radial-line]:stroke-border [&_.lc-grid-y-radial-circle]:stroke-border',
|
|
52
|
+
// Legend adjustments
|
|
53
|
+
'[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5',
|
|
54
|
+
'[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4',
|
|
55
|
+
'[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]',
|
|
56
|
+
// Labels
|
|
57
|
+
'[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent',
|
|
58
|
+
// Tick labels on th x/y axes
|
|
59
|
+
'[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal',
|
|
60
|
+
'[&_.lc-tooltip-rects-g]:fill-transparent',
|
|
61
|
+
'[&_.lc-layout-svg-g]:fill-transparent',
|
|
62
|
+
'[&_.lc-root-container]:w-full',
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
{...restProps}
|
|
66
|
+
>
|
|
67
|
+
<ChartStyle id={chartId} {config} />
|
|
68
|
+
{@render children?.()}
|
|
69
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { THEMES, type ChartConfig } from './chart-utils.js';
|
|
3
|
+
let { id, config }: { id: string; config: ChartConfig } = $props();
|
|
4
|
+
const colorConfig = $derived(config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null);
|
|
5
|
+
const themeContents = $derived.by(() => {
|
|
6
|
+
if (!colorConfig || !colorConfig.length) return;
|
|
7
|
+
const themeContents = [];
|
|
8
|
+
for (let [_theme, prefix] of Object.entries(THEMES)) {
|
|
9
|
+
let content = `${prefix} [data-chart=${id}] {\n`;
|
|
10
|
+
const color = colorConfig.map(([key, itemConfig]) => {
|
|
11
|
+
const theme = _theme as keyof typeof itemConfig.theme;
|
|
12
|
+
const color = itemConfig.theme?.[theme] || itemConfig.color;
|
|
13
|
+
return color ? `\t--color-${key}: ${color};` : null;
|
|
14
|
+
});
|
|
15
|
+
content += color.join('\n') + '\n}';
|
|
16
|
+
themeContents.push(content);
|
|
17
|
+
}
|
|
18
|
+
return themeContents.join('\n');
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#if themeContents}
|
|
23
|
+
{#key id}
|
|
24
|
+
<svelte:element this={'style'}>
|
|
25
|
+
{themeContents}
|
|
26
|
+
</svelte:element>
|
|
27
|
+
{/key}
|
|
28
|
+
{/if}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, type WithElementRef, type WithoutChildren } from '../utils.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils.js';
|
|
5
|
+
import { getTooltipContext, Tooltip as TooltipPrimitive } from 'layerchart';
|
|
6
|
+
import type { Snippet } from 'svelte';
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
function defaultFormatter(value: any, _payload: TooltipPayload[]) {
|
|
9
|
+
return `${value}`;
|
|
10
|
+
}
|
|
11
|
+
let {
|
|
12
|
+
ref = $bindable(null),
|
|
13
|
+
class: className,
|
|
14
|
+
hideLabel = false,
|
|
15
|
+
indicator = 'dot',
|
|
16
|
+
hideIndicator = false,
|
|
17
|
+
labelKey,
|
|
18
|
+
label,
|
|
19
|
+
labelFormatter = defaultFormatter,
|
|
20
|
+
labelClassName,
|
|
21
|
+
formatter,
|
|
22
|
+
nameKey,
|
|
23
|
+
color,
|
|
24
|
+
...restProps
|
|
25
|
+
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
|
|
26
|
+
hideLabel?: boolean;
|
|
27
|
+
label?: string;
|
|
28
|
+
indicator?: 'line' | 'dot' | 'dashed';
|
|
29
|
+
nameKey?: string;
|
|
30
|
+
labelKey?: string;
|
|
31
|
+
hideIndicator?: boolean;
|
|
32
|
+
labelClassName?: string;
|
|
33
|
+
labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
|
|
35
|
+
formatter?: Snippet<
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
value: unknown;
|
|
39
|
+
name: string;
|
|
40
|
+
item: TooltipPayload;
|
|
41
|
+
index: number;
|
|
42
|
+
payload: TooltipPayload[];
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
>;
|
|
46
|
+
} = $props();
|
|
47
|
+
const chart = useChart();
|
|
48
|
+
const tooltipCtx = getTooltipContext();
|
|
49
|
+
const formattedLabel = $derived.by(() => {
|
|
50
|
+
if (hideLabel || !tooltipCtx.payload?.length) return null;
|
|
51
|
+
const [item] = tooltipCtx.payload;
|
|
52
|
+
const key = labelKey ?? item?.label ?? item?.name ?? 'value';
|
|
53
|
+
const itemConfig = getPayloadConfigFromPayload(chart.config, item, key);
|
|
54
|
+
const value = !labelKey && typeof label === 'string' ? (chart.config[label as keyof typeof chart.config]?.label ?? label) : (itemConfig?.label ?? item.label);
|
|
55
|
+
if (value === undefined) return null;
|
|
56
|
+
if (!labelFormatter) return value;
|
|
57
|
+
return labelFormatter(value, tooltipCtx.payload);
|
|
58
|
+
});
|
|
59
|
+
const nestLabel = $derived(tooltipCtx.payload.length === 1 && indicator !== 'dot');
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
{#snippet TooltipLabel()}
|
|
63
|
+
{#if formattedLabel}
|
|
64
|
+
<div class={cn('font-medium', labelClassName)}>
|
|
65
|
+
{#if typeof formattedLabel === 'function'}
|
|
66
|
+
{@render formattedLabel()}
|
|
67
|
+
{:else}
|
|
68
|
+
{formattedLabel}
|
|
69
|
+
{/if}
|
|
70
|
+
</div>
|
|
71
|
+
{/if}
|
|
72
|
+
{/snippet}
|
|
73
|
+
<TooltipPrimitive.Root variant="none">
|
|
74
|
+
<div class={cn('border-border/50 bg-background grid min-w-[9rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl', className)} {...restProps}>
|
|
75
|
+
{#if !nestLabel}
|
|
76
|
+
{@render TooltipLabel()}
|
|
77
|
+
{/if}
|
|
78
|
+
<div class="grid gap-1.5">
|
|
79
|
+
{#each tooltipCtx.payload as item, i (item.key + i)}
|
|
80
|
+
{@const key = `${nameKey || item.key || item.name || 'value'}`}
|
|
81
|
+
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)}
|
|
82
|
+
{@const indicatorColor = color || item.payload?.color || item.color}
|
|
83
|
+
<div class={cn('[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5', indicator === 'dot' && 'items-center')}>
|
|
84
|
+
{#if formatter && item.value !== undefined && item.name}
|
|
85
|
+
{@render formatter({
|
|
86
|
+
value: item.value,
|
|
87
|
+
name: item.name,
|
|
88
|
+
item,
|
|
89
|
+
index: i,
|
|
90
|
+
payload: tooltipCtx.payload
|
|
91
|
+
})}
|
|
92
|
+
{:else}
|
|
93
|
+
{#if itemConfig?.icon}
|
|
94
|
+
<itemConfig.icon />
|
|
95
|
+
{:else if !hideIndicator}
|
|
96
|
+
<div
|
|
97
|
+
style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
|
|
98
|
+
class={cn('border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]', {
|
|
99
|
+
'size-2.5': indicator === 'dot',
|
|
100
|
+
'h-full w-1': indicator === 'line',
|
|
101
|
+
'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',
|
|
102
|
+
'my-0.5': nestLabel && indicator === 'dashed'
|
|
103
|
+
})}
|
|
104
|
+
></div>
|
|
105
|
+
{/if}
|
|
106
|
+
<div class={cn('flex flex-1 shrink-0 justify-between leading-none', nestLabel ? 'items-end' : 'items-center')}>
|
|
107
|
+
<div class="grid gap-1.5">
|
|
108
|
+
{#if nestLabel}
|
|
109
|
+
{@render TooltipLabel()}
|
|
110
|
+
{/if}
|
|
111
|
+
<span class="text-muted-foreground">
|
|
112
|
+
{itemConfig?.label || item.name}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
{#if item.value !== undefined}
|
|
116
|
+
<span class="text-foreground font-mono font-medium tabular-nums">
|
|
117
|
+
{item.value.toLocaleString()}
|
|
118
|
+
</span>
|
|
119
|
+
{/if}
|
|
120
|
+
</div>
|
|
121
|
+
{/if}
|
|
122
|
+
</div>
|
|
123
|
+
{/each}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</TooltipPrimitive.Root>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Tooltip } from 'layerchart';
|
|
2
|
+
import { getContext, setContext, type Component, type ComponentProps, type Snippet } from 'svelte';
|
|
3
|
+
export const THEMES = { light: '', dark: '.dark' } as const;
|
|
4
|
+
export type ChartConfig = {
|
|
5
|
+
[k in string]: {
|
|
6
|
+
label?: string;
|
|
7
|
+
icon?: Component;
|
|
8
|
+
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
|
|
9
|
+
};
|
|
10
|
+
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
|
|
11
|
+
export type TooltipPayload = ExtractSnippetParams<ComponentProps<typeof Tooltip.Root>['children']>['payload'][number];
|
|
12
|
+
// Helper to extract item config from a payload.
|
|
13
|
+
export function getPayloadConfigFromPayload(config: ChartConfig, payload: TooltipPayload, key: string) {
|
|
14
|
+
if (typeof payload !== 'object' || payload === null) return undefined;
|
|
15
|
+
const payloadPayload = 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null ? payload.payload : undefined;
|
|
16
|
+
let configLabelKey: string = key;
|
|
17
|
+
if (payload.key === key) {
|
|
18
|
+
configLabelKey = payload.key;
|
|
19
|
+
} else if (payload.name === key) {
|
|
20
|
+
configLabelKey = payload.name;
|
|
21
|
+
} else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
|
|
22
|
+
configLabelKey = payload[key as keyof typeof payload] as string;
|
|
23
|
+
} else if (payloadPayload !== undefined && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === 'string') {
|
|
24
|
+
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
|
|
25
|
+
}
|
|
26
|
+
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
|
27
|
+
}
|
|
28
|
+
type ChartContextValue = {
|
|
29
|
+
config: ChartConfig;
|
|
30
|
+
};
|
|
31
|
+
const chartContextKey = Symbol('chart-context');
|
|
32
|
+
export function setChartContext(value: ChartContextValue) {
|
|
33
|
+
return setContext(chartContextKey, value);
|
|
34
|
+
}
|
|
35
|
+
export function useChart() {
|
|
36
|
+
return getContext<ChartContextValue>(chartContextKey);
|
|
37
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import ChartContainer from './chart-container.svelte';
|
|
2
|
+
import ChartTooltip from './chart-tooltip.svelte';
|
|
3
|
+
export { getPayloadConfigFromPayload, type ChartConfig } from './chart-utils.js';
|
|
4
|
+
export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };
|
|
@@ -1,51 +1,42 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export type ToggleVariant = VariantProps<typeof toggleVariants>["variant"];
|
|
25
|
-
export type ToggleSize = VariantProps<typeof toggleVariants>["size"];
|
|
26
|
-
export type ToggleVariants = VariantProps<typeof toggleVariants>;
|
|
2
|
+
import { type VariantProps, tv } from 'tailwind-variants';
|
|
3
|
+
export const toggleVariants = tv({
|
|
4
|
+
base: "hover:bg-muted hover:text-muted-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
5
|
+
variants: {
|
|
6
|
+
variant: {
|
|
7
|
+
default: 'bg-transparent',
|
|
8
|
+
outline: 'border-input shadow-xs hover:bg-accent hover:text-accent-foreground border bg-transparent'
|
|
9
|
+
},
|
|
10
|
+
size: {
|
|
11
|
+
default: 'h-9 min-w-9 px-2',
|
|
12
|
+
sm: 'h-8 min-w-8 px-1.5',
|
|
13
|
+
lg: 'h-10 min-w-10 px-2.5'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: 'default',
|
|
18
|
+
size: 'default'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
export type ToggleVariant = VariantProps<typeof toggleVariants>['variant'];
|
|
22
|
+
export type ToggleSize = VariantProps<typeof toggleVariants>['size'];
|
|
23
|
+
export type ToggleVariants = VariantProps<typeof toggleVariants>;
|
|
27
24
|
</script>
|
|
28
25
|
|
|
29
26
|
<script lang="ts">
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} = $props();
|
|
27
|
+
import { Toggle as TogglePrimitive } from 'bits-ui';
|
|
28
|
+
import { cn } from '../utils.js';
|
|
29
|
+
let {
|
|
30
|
+
ref = $bindable(null),
|
|
31
|
+
pressed = $bindable(false),
|
|
32
|
+
class: className,
|
|
33
|
+
size = 'default',
|
|
34
|
+
variant = 'default',
|
|
35
|
+
...restProps
|
|
36
|
+
}: TogglePrimitive.RootProps & {
|
|
37
|
+
variant?: ToggleVariant;
|
|
38
|
+
size?: ToggleSize;
|
|
39
|
+
} = $props();
|
|
44
40
|
</script>
|
|
45
41
|
|
|
46
|
-
<TogglePrimitive.Root
|
|
47
|
-
bind:ref
|
|
48
|
-
bind:pressed
|
|
49
|
-
class={cn(toggleVariants({ variant, size }), className)}
|
|
50
|
-
{...restProps}
|
|
51
|
-
/>
|
|
42
|
+
<TogglePrimitive.Root bind:ref bind:pressed data-slot="toggle" class={cn(toggleVariants({ variant, size }), className)} {...restProps} />
|
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
|
|
3
|
+
import { getToggleGroupCtx } from './toggle-group.svelte';
|
|
4
|
+
import { cn } from '../utils.js';
|
|
5
|
+
import { type ToggleVariants, toggleVariants } from '../toggle/index.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
ref = $bindable(null),
|
|
9
|
-
value = $bindable(),
|
|
10
|
-
class: className,
|
|
11
|
-
size,
|
|
12
|
-
variant,
|
|
13
|
-
...restProps
|
|
14
|
-
}: ToggleGroupPrimitive.ItemProps & ToggleVariants = $props();
|
|
7
|
+
let { ref = $bindable(null), value = $bindable(), class: className, size, variant, ...restProps }: ToggleGroupPrimitive.ItemProps & ToggleVariants = $props();
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
const ctx = getToggleGroupCtx();
|
|
17
10
|
</script>
|
|
18
11
|
|
|
19
12
|
<ToggleGroupPrimitive.Item
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
bind:ref
|
|
14
|
+
data-slot="toggle-group-item"
|
|
15
|
+
data-variant={ctx.variant || variant}
|
|
16
|
+
data-size={ctx.size || size}
|
|
17
|
+
class={cn(
|
|
18
|
+
toggleVariants({
|
|
19
|
+
variant: ctx.variant || variant,
|
|
20
|
+
size: ctx.size || size
|
|
21
|
+
}),
|
|
22
|
+
'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l',
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{value}
|
|
26
|
+
{...restProps}
|
|
30
27
|
/>
|
|
@@ -33,4 +33,12 @@
|
|
|
33
33
|
Discriminated Unions + Destructing (required for bindable) do not
|
|
34
34
|
get along, so we shut typescript up by casting `value` to `never`.
|
|
35
35
|
-->
|
|
36
|
-
<ToggleGroupPrimitive.Root
|
|
36
|
+
<ToggleGroupPrimitive.Root
|
|
37
|
+
bind:value={value as never}
|
|
38
|
+
bind:ref
|
|
39
|
+
data-slot="toggle-group"
|
|
40
|
+
data-variant={variant}
|
|
41
|
+
data-size={size}
|
|
42
|
+
class={cn('group/toggle-group data-[variant=outline]:shadow-xs flex w-fit items-center rounded-md', className)}
|
|
43
|
+
{...restProps}
|
|
44
|
+
/>
|