pika-ux 1.0.0 → 1.0.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pika-ux",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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';
@@ -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,9 +19,10 @@
15
19
  interface Props {
16
20
  table: Table<TData>;
17
21
  serverSide: ServerSideConfig;
22
+ showRowsPerPage?: boolean;
18
23
  }
19
24
 
20
- let { table, serverSide }: Props = $props();
25
+ let { table, serverSide, showRowsPerPage = true }: Props = $props();
21
26
 
22
27
  // For cursor-based pagination, we can't jump to arbitrary pages
23
28
  const isCursorBased = $derived(serverSide?.paginationMode === 'cursor');
@@ -33,30 +38,32 @@
33
38
  {table.getFilteredRowModel().rows.length} row(s) selected.
34
39
  {/if}
35
40
  </div>
36
- <div class="flex items-center space-x-6 lg:space-x-8">
37
- <div class="flex items-center space-x-2">
38
- <p class="text-sm font-medium">Rows per page</p>
39
- <Select.Root
40
- allowDeselect={false}
41
- type="single"
42
- value={`${table.getState().pagination.pageSize}`}
43
- onValueChange={(value) => {
44
- table.setPageSize(Number(value));
45
- }}
46
- >
47
- <Select.Trigger class="h-8 w-[70px]">
48
- {String(table.getState().pagination.pageSize)}
49
- </Select.Trigger>
50
- <Select.Content side="top">
51
- {#each [10, 20, 50, 100, 500, 1000] as pageSize (pageSize)}
52
- <Select.Item value={`${pageSize}`}>
53
- {pageSize}
54
- </Select.Item>
55
- {/each}
56
- </Select.Content>
57
- </Select.Root>
58
- </div>
59
- <div class="flex w-[100px] items-center justify-center text-sm font-medium">
41
+ <div class="flex items-center space-x-8">
42
+ {#if showRowsPerPage}
43
+ <div class="flex items-center space-x-2">
44
+ <p class="text-sm font-medium">Rows per page</p>
45
+ <Select.Root
46
+ allowDeselect={false}
47
+ type="single"
48
+ value={`${table.getState().pagination.pageSize}`}
49
+ onValueChange={(value) => {
50
+ table.setPageSize(Number(value));
51
+ }}
52
+ >
53
+ <Select.Trigger class="h-8 w-[70px]">
54
+ {String(table.getState().pagination.pageSize)}
55
+ </Select.Trigger>
56
+ <Select.Content side="top">
57
+ {#each [10, 20, 50, 100, 500, 1000] as pageSize (pageSize)}
58
+ <Select.Item value={`${pageSize}`}>
59
+ {pageSize}
60
+ </Select.Item>
61
+ {/each}
62
+ </Select.Content>
63
+ </Select.Root>
64
+ </div>
65
+ {/if}
66
+ <div class="flex w-[100px] items-center justify-center text-sm font-medium mr-2">
60
67
  {#if table.getPageCount() > 0}
61
68
  Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
62
69
  {: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, appState)}>
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
- import type { AppState } from '$client/app/app.state.svelte';
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 { getContext, type Snippet } from 'svelte';
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(appSettings.getTableColumnVisibilityObject(tableKey));
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(appSettings.getTableNumRows(tableKey, 10));
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
- // $effect(() => {
82
- // const serverState = serverSideConfig;
83
- // if (serverState) {
84
- // const debounceMs = serverState.debounceMs ?? 300;
85
- // debouncedRequestData = debounce(async (tableState: ServerSideTableState) => {
86
- // try {
87
- // await serverState.requestData(tableState);
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);
88
99
 
89
- // // If using new API, update the tableState
90
- // if (serverSideConfig) {
91
- // serverSideConfig.tableState = { ...serverSideConfig.tableState, ...tableState };
92
- // }
93
- // } catch (error) {
94
- // serverState.onError?.(error instanceof Error ? error.message : 'Unknown error');
95
- // }
96
- // }, debounceMs);
97
- // } else {
98
- // debouncedRequestData = undefined;
99
- // }
100
- // });
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
- // function triggerServerRequest() {
104
- // const serverState = serverSideConfig;
105
- // if (!serverState || !debouncedRequestData) return;
114
+ function triggerServerRequest() {
115
+ const serverState = serverSideConfig;
116
+ if (!serverState || !debouncedRequestData) return;
106
117
 
107
- // const tableState: ServerSideTableState = {
108
- // pageIndex,
109
- // pageSize,
110
- // sorting,
111
- // columnFilters,
112
- // requestId: crypto.randomUUID(),
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
- // debouncedRequestData(tableState);
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 {
@@ -194,9 +209,9 @@
194
209
  }
195
210
 
196
211
  // Trigger server request for server-side tables
197
- // if (serverSideConfig) {
198
- // triggerServerRequest();
199
- // }
212
+ if (serverSideConfig) {
213
+ triggerServerRequest();
214
+ }
200
215
  },
201
216
  onColumnFiltersChange: (updater) => {
202
217
  if (typeof updater === 'function') {
@@ -206,13 +221,13 @@
206
221
  }
207
222
 
208
223
  // Trigger server request for server-side tables
209
- // if (serverSideConfig) {
210
- // triggerServerRequest();
211
- // }
224
+ if (serverSideConfig) {
225
+ triggerServerRequest();
226
+ }
212
227
  },
213
228
  onColumnVisibilityChange: (updater) => {
214
229
  if (typeof updater === 'function') {
215
- appSettings.setTableColumnVisibilityFromObject(tableKey, updater(columnVisibility));
230
+ tableSettings.setTableColumnVisibilityFromObject(tableKey, updater(columnVisibility));
216
231
  } else {
217
232
  throw new Error('onColumnVisibilityChange updater must be a function');
218
233
  //columnVisibility = updater;
@@ -222,12 +237,12 @@
222
237
  if (typeof updater === 'function') {
223
238
  const val = updater({ pageIndex, pageSize });
224
239
  pageIndex = val.pageIndex;
225
- appSettings.setTableNumRows(tableKey, val.pageSize);
240
+ tableSettings.setTableNumRows(tableKey, val.pageSize);
226
241
 
227
242
  // Trigger server request for server-side tables
228
- // if (serverSideConfig) {
229
- // triggerServerRequest();
230
- // }
243
+ if (serverSideConfig) {
244
+ triggerServerRequest();
245
+ }
231
246
  } else {
232
247
  throw new Error('onPaginationChange updater must be a function');
233
248
  }
@@ -237,9 +252,9 @@
237
252
  globalFilterProps.globalFilterValue = value;
238
253
 
239
254
  // Trigger server request for server-side tables
240
- // if (serverSideConfig) {
241
- // triggerServerRequest();
242
- // }
255
+ if (serverSideConfig) {
256
+ triggerServerRequest();
257
+ }
243
258
  }
244
259
  },
245
260
 
@@ -257,8 +272,15 @@
257
272
  });
258
273
  </script>
259
274
 
260
- <div class="space-y-4 {classes ? classes : ''} ">
261
- <TableToolbar {table} {globalFilterProps} {facetedFilters} {toolbarContent} {beneathToolbarContent} />
275
+ <div class="{classes ? classes : ''} ">
276
+ <div class="mb-4">
277
+ <TableToolbar {table} {globalFilterProps} {facetedFilters} {toolbarContent} {beneathToolbarContent} />
278
+ </div>
279
+ {#if paginationPlacement === 'top' || paginationPlacement === 'both'}
280
+ <div class="mt-2 pb-1">
281
+ <TablePagination {table} serverSide={serverSideConfig} {showRowsPerPage} />
282
+ </div>
283
+ {/if}
262
284
  <div class="rounded-md border h-full flex flex-col overflow-y-auto">
263
285
  <Table.Root class="h-full">
264
286
  <Table.Header>
@@ -291,5 +313,9 @@
291
313
  </Table.Body>
292
314
  </Table.Root>
293
315
  </div>
294
- <TablePagination {table} serverSide={serverSideConfig} />
316
+ {#if paginationPlacement === 'bottom' || paginationPlacement === 'both'}
317
+ <div class="mt-2">
318
+ <TablePagination {table} serverSide={serverSideConfig} {showRowsPerPage} />
319
+ </div>
320
+ {/if}
295
321
  </div>
@@ -1,7 +1,17 @@
1
- import type { AppState } from '$client/app/app.state.svelte';
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>, appState: AppState) => void;
57
+ onclick?: (row: Row<TData>) => void;
48
58
  }
49
59
 
50
60
  export interface RowActionMenuItemSubMenu<TData> {
@@ -0,0 +1,13 @@
1
+ import Root from "./toggle.svelte";
2
+ export {
3
+ toggleVariants,
4
+ type ToggleSize,
5
+ type ToggleVariant,
6
+ type ToggleVariants,
7
+ } from "./toggle.svelte";
8
+
9
+ export {
10
+ Root,
11
+ //
12
+ Root as Toggle,
13
+ };
@@ -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,10 @@
1
+ import Root from "./toggle-group.svelte";
2
+ import Item from "./toggle-group-item.svelte";
3
+
4
+ export {
5
+ Root,
6
+ Item,
7
+ //
8
+ Root as ToggleGroup,
9
+ Item as ToggleGroupItem,
10
+ };
@@ -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
- <HelpCircleOutline class="w-4 h-4 text-gray-400 hover:text-blue-500 transition-colors" />
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
- import { type VariantProps, tv } from "tailwind-variants";
3
-
4
- export const toggleVariants = tv({
5
- base: "hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center gap-2 rounded-md 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-transparent",
9
- outline:
10
- "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-sm",
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>;
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
- import { Toggle as TogglePrimitive } from "bits-ui";
31
- import { cn } from "../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();
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
- 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";
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
- let {
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
- const ctx = getToggleGroupCtx();
9
+ const ctx = getToggleGroupCtx();
17
10
  </script>
18
11
 
19
12
  <ToggleGroupPrimitive.Item
20
- bind:ref
21
- class={cn(
22
- toggleVariants({
23
- variant: ctx.variant || variant,
24
- size: ctx.size || size,
25
- }),
26
- className
27
- )}
28
- {value}
29
- {...restProps}
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 bind:value={value as never} bind:ref class={cn('flex items-center justify-center gap-1', className)} {...restProps} />
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
+ />