@svadmin/ui 0.5.4 → 0.5.9

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": "@svadmin/ui",
3
- "version": "0.5.4",
3
+ "version": "0.5.9",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
package/src/app.css CHANGED
@@ -1,3 +1,6 @@
1
+ /* Typography — Inter font via Bunny Fonts (global CDN, works in China) */
2
+ @import url('https://fonts.bunny.net/css?family=inter:400,500,600,700&display=swap');
3
+
1
4
  /* @svadmin/ui — Design Tokens (Shadcn New York style)
2
5
  * Auto-imported by AdminApp. Can be overridden by consumer's CSS.
3
6
  *
@@ -12,8 +15,8 @@
12
15
  /* Auto-register svadmin source files for Tailwind 4 content scanning.
13
16
  * This ensures all utility classes used in @svadmin/ui and @svadmin/core
14
17
  * components are generated without manual @source in consumer projects. */
15
- @source "./**/*.{svelte,ts,js}";
16
- @source "../../core/src/**/*.{svelte,ts,js}";
18
+ @source "@svadmin/ui/src/**/*.{svelte,ts,js}";
19
+ @source "@svadmin/core/src/**/*.{svelte,ts,js}";
17
20
 
18
21
  @layer base {
19
22
  :root {
@@ -86,10 +89,25 @@
86
89
  --sidebar-border: oklch(1 0 0 / 10%);
87
90
  --sidebar-ring: oklch(0.556 0 0);
88
91
  }
89
- }
90
92
 
91
- /* Typography — Inter font via Bunny Fonts (global CDN, works in China) */
92
- @import url('https://fonts.bunny.net/css?family=inter:400,500,600,700&display=swap');
93
+ /* Emulate new-york style shadows on inputs and buttons */
94
+ [data-slot="button"],
95
+ [data-slot="input"],
96
+ [data-slot="select-trigger"],
97
+ [data-slot="select"],
98
+ [data-slot="textarea"] {
99
+ @apply shadow-sm;
100
+ }
101
+
102
+ [data-slot="card"] {
103
+ @apply shadow-sm;
104
+ }
105
+
106
+ [data-slot="dialog-content"],
107
+ [data-slot="sheet-content"] {
108
+ @apply shadow-lg;
109
+ }
110
+ }
93
111
 
94
112
  body {
95
113
  font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
2
3
  import { useSelect } from '@svadmin/core';
3
4
  import type { Filter } from '@svadmin/core';
4
5
  import { Button } from './ui/button/index.js';
@@ -57,7 +58,8 @@
57
58
  onchange?.(null);
58
59
  }
59
60
 
60
- function handleSearchInput(v: string) {
61
+ function handleSearchInput(e: Event) {
62
+ const v = (e.currentTarget as HTMLInputElement).value;
61
63
  searchInputValue = v;
62
64
  onSearchChange(v);
63
65
  }
@@ -88,8 +90,8 @@
88
90
  <div class="flex items-center border-b px-2">
89
91
  <Search class="mr-1.5 h-3.5 w-3.5 shrink-0 opacity-50" />
90
92
  <Command.Input
91
- value={searchInputValue}
92
- onValueChange={handleSearchInput}
93
+ bind:value={searchInputValue}
94
+ oninput={handleSearchInput}
93
95
  placeholder="Search..."
94
96
  class="flex h-9 w-full bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground"
95
97
  />
@@ -3,10 +3,10 @@
3
3
  import { Button } from './ui/button/index.js';
4
4
  import { t } from '@svadmin/core/i18n';
5
5
 
6
- let { open = false, title, message = '',
6
+ let { open = $bindable(false), title, message = '',
7
7
  confirmText, cancelText, variant = 'danger',
8
8
  onconfirm, oncancel } = $props<{
9
- open: boolean;
9
+ open?: boolean;
10
10
  title?: string;
11
11
  message?: string;
12
12
  confirmText?: string;
@@ -67,5 +67,6 @@
67
67
  <ConfirmDialog
68
68
  bind:open={confirmOpen}
69
69
  title={t('common.deleteConfirm')}
70
- onConfirm={handleDelete}
70
+ onconfirm={handleDelete}
71
+ oncancel={() => confirmOpen = false}
71
72
  />
@@ -220,7 +220,7 @@
220
220
  {#if multiVal.length > 0}
221
221
  <div class="flex flex-wrap gap-1 mt-1">
222
222
  {#each multiVal as v}
223
- {@const label = field.options?.find(o => o.value === v)?.label ?? String(v)}
223
+ {@const label = field.options?.find((o: any) => o.value === v)?.label ?? String(v)}
224
224
  <Badge variant="secondary" class="gap-1">
225
225
  {label}
226
226
  <button type="button" onclick={() => toggleMulti(v)} class="ml-0.5 rounded-sm hover:text-destructive hover:bg-destructive/10 transition-colors" aria-label={t('common.clear')}>×</button>
@@ -63,7 +63,7 @@
63
63
  {placeholder}
64
64
  bind:value
65
65
  class="pl-9 pr-9"
66
- {autocomplete}
66
+ autocomplete={autocomplete as any}
67
67
  />
68
68
  <TooltipButton
69
69
  tooltip={showPassword ? 'Hide password' : 'Show password'}
@@ -244,7 +244,7 @@
244
244
  </Button>
245
245
  {/snippet}
246
246
  </DropdownMenu.Trigger>
247
- <DropdownMenu.Content side="right" align="end" class="w-40">
247
+ <DropdownMenu.Content align="end" class="w-40">
248
248
  {#each colorThemes as ct}
249
249
  <DropdownMenu.Item onclick={() => setColorTheme(ct.id)} class="gap-2">
250
250
  <span
@@ -2,7 +2,7 @@
2
2
  import { fly } from 'svelte/transition';
3
3
  import { useStepsForm } from '@svadmin/core';
4
4
  import type { FieldDefinition } from '@svadmin/core';
5
- import { getResource } from '@svadmin/core';
5
+ import { getResource, useOne } from '@svadmin/core';
6
6
  import { navigate } from '@svadmin/core/router';
7
7
  import { t } from '@svadmin/core/i18n';
8
8
  import { Button } from './ui/button/index.js';
@@ -49,13 +49,88 @@
49
49
  .filter((f): f is FieldDefinition => f != null && f.key !== primaryKey) ?? []
50
50
  );
51
51
 
52
+ const flatStepsFields = steps.flatMap(s => s.fields)
53
+ .map(key => resource.fields.find(f => f.key === key))
54
+ .filter((f): f is FieldDefinition => f != null);
55
+
56
+ const existingQuery = mode === 'edit' && id != null
57
+ ? stepsForm.query as { data?: { data: Record<string, unknown> }, isLoading: boolean }
58
+ : null;
59
+
60
+ let formData = $state<Record<string, unknown>>({});
61
+ let fieldErrors = $state<Record<string, string>>({});
62
+ let initialized = $state(false);
63
+
64
+ function getDefaultForType(field: FieldDefinition): unknown {
65
+ switch (field.type) {
66
+ case 'text': case 'textarea': case 'richtext': case 'image': return '';
67
+ case 'number': return 0;
68
+ case 'boolean': return false;
69
+ case 'tags': case 'images': case 'multiselect': return [];
70
+ case 'select': return field.options?.[0]?.value ?? '';
71
+ case 'json': return {};
72
+ default: return '';
73
+ }
74
+ }
75
+
76
+ $effect.pre(() => {
77
+ if (initialized) return;
78
+ if (mode === 'create') {
79
+ const defaults: Record<string, unknown> = {};
80
+ for (const field of flatStepsFields) {
81
+ defaults[field.key] = field.defaultValue ?? getDefaultForType(field);
82
+ }
83
+ formData = defaults;
84
+ initialized = true;
85
+ } else if (existingQuery && existingQuery.data?.data) {
86
+ formData = { ...existingQuery.data.data };
87
+ initialized = true;
88
+ }
89
+ });
90
+
91
+ function handleFieldChange(key: string, val: unknown) {
92
+ formData[key] = val;
93
+ if (fieldErrors[key]) delete fieldErrors[key];
94
+ }
95
+
96
+ function validateCurrentStep(): boolean {
97
+ const errors: Record<string, string> = {};
98
+ let valid = true;
99
+ for (const field of currentFields) {
100
+ const value = formData[field.key];
101
+ if (field.required && (value === undefined || value === null || value === '')) {
102
+ errors[field.key] = t('validation.required');
103
+ valid = false;
104
+ } else if (field.validate) {
105
+ const msg = field.validate(value);
106
+ if (msg) {
107
+ errors[field.key] = msg;
108
+ valid = false;
109
+ }
110
+ }
111
+ }
112
+ fieldErrors = errors;
113
+ return valid;
114
+ }
115
+
52
116
  function handleSubmit() {
117
+ if (!validateCurrentStep()) return;
118
+
53
119
  if (!isLastStep) {
54
120
  next();
55
121
  return;
56
122
  }
57
- stepsForm.onFinish(stepsForm.formValues);
123
+
124
+ const cleanData: Record<string, unknown> = {};
125
+ for (const field of flatStepsFields) {
126
+ const value = formData[field.key];
127
+ if (value !== undefined) cleanData[field.key] = value;
128
+ }
129
+
130
+ stepsForm.onFinish(cleanData, { redirect: 'list' });
58
131
  }
132
+
133
+ const isLoadingData = $derived(mode === 'edit' && !initialized);
59
134
  </script>
60
135
 
61
136
  <div class="space-y-6">
@@ -103,12 +178,15 @@
103
178
  <Card.CardContent>
104
179
  {#key currentStep}
105
180
  <form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} in:fly={{ x: 30, duration: 250 }}>
106
- {#each currentFields as field}
181
+ {#each currentFields as field (field.key)}
107
182
  <FieldRenderer
108
183
  {field}
109
- value={stepsForm.formValues[field.key]}
110
- onchange={(v) => stepsForm.setFieldValue(field.key, v)}
184
+ value={formData[field.key]}
185
+ onchange={(v) => handleFieldChange(field.key, v)}
111
186
  />
187
+ {#if fieldErrors[field.key]}
188
+ <p class="text-destructive text-[0.8125rem] mt-1">{fieldErrors[field.key]}</p>
189
+ {/if}
112
190
  {/each}
113
191
 
114
192
  <div class="flex items-center justify-between pt-4">
@@ -5,12 +5,12 @@
5
5
  import type { Component } from 'svelte';
6
6
 
7
7
  export interface FieldComponentMap {
8
- [fieldType: string]: Component<Record<string, unknown>>;
8
+ [fieldType: string]: Component<any>;
9
9
  }
10
10
 
11
11
  // Default display component map (read-only field display)
12
12
  // Dynamically imports components only when first used
13
- const displayCache = new Map<string, Component<Record<string, unknown>>>();
13
+ const displayCache = new Map<string, Component<any>>();
14
14
 
15
15
  // Pre-import all field display components (tree-shaken by bundler)
16
16
  import BooleanField from './fields/BooleanField.svelte';
@@ -52,13 +52,13 @@ export const builtinDisplayComponents: FieldComponentMap = {
52
52
  // ─── Custom component registry ─────────────────────────────────
53
53
  // Users can register custom field display components at runtime
54
54
 
55
- const customDisplayComponents = new Map<string, Component<Record<string, unknown>>>();
55
+ const customDisplayComponents = new Map<string, Component<any>>();
56
56
 
57
57
  /**
58
58
  * Register a custom field display component for a given field type.
59
59
  * The component will be used by ShowPage and any other display contexts.
60
60
  */
61
- export function registerDisplayComponent(fieldType: string, component: Component<Record<string, unknown>>): void {
61
+ export function registerDisplayComponent(fieldType: string, component: Component<any>): void {
62
62
  customDisplayComponents.set(fieldType, component);
63
63
  }
64
64
 
@@ -67,7 +67,7 @@ export function registerDisplayComponent(fieldType: string, component: Component
67
67
  * Custom components take precedence over built-in ones.
68
68
  * Returns undefined if no component is registered for the type.
69
69
  */
70
- export function getDisplayComponent(fieldType: string): Component<Record<string, unknown>> | undefined {
70
+ export function getDisplayComponent(fieldType: string): Component<any> | undefined {
71
71
  return customDisplayComponents.get(fieldType) ?? builtinDisplayComponents[fieldType];
72
72
  }
73
73
 
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { Badge } from '../ui/badge/index.js';
3
3
 
4
- let { value } = $props<{
4
+ let { value, options } = $props<{
5
5
  value: (string | number)[] | null | undefined;
6
6
  options?: { label: string; value: string | number }[];
7
7
  }>();
@@ -11,8 +11,9 @@
11
11
 
12
12
  {#if items.length > 0}
13
13
  <div class="flex flex-wrap gap-1">
14
- {#each items as item}
15
- <Badge variant="secondary">{String(item)}</Badge>
14
+ {#each items as v}
15
+ {@const label = options?.find((o: { label: string; value: string | number }) => o.value === v)?.label ?? String(v)}
16
+ <Badge variant="secondary" class="font-normal">{label}</Badge>
16
17
  {/each}
17
18
  </div>
18
19
  {:else}
@@ -7,7 +7,7 @@
7
7
  }>();
8
8
 
9
9
  const label = $derived(
10
- options?.find(o => o.value === value)?.label ?? (value != null ? String(value) : '—')
10
+ options?.find((o: any) => o.value === value)?.label ?? (value != null ? String(value) : '—')
11
11
  );
12
12
  </script>
13
13
 
@@ -2,12 +2,14 @@
2
2
  import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
3
3
  import type { WithoutChildrenOrChild } from "bits-ui";
4
4
  import { cn } from "../../../utils.js";
5
+ import type { Snippet } from "svelte";
5
6
 
6
7
  let {
7
8
  ref = $bindable(null),
8
9
  class: className,
10
+ children,
9
11
  ...restProps
10
- }: WithoutChildrenOrChild<AlertDialogPrimitive.ContentProps> & { class?: string } = $props();
12
+ }: WithoutChildrenOrChild<AlertDialogPrimitive.ContentProps> & { children?: Snippet; class?: string } = $props();
11
13
  </script>
12
14
 
13
15
  <AlertDialogPrimitive.Portal>
@@ -21,5 +23,7 @@
21
23
  className,
22
24
  )}
23
25
  {...restProps}
24
- />
26
+ >
27
+ {@render children?.()}
28
+ </AlertDialogPrimitive.Content>
25
29
  </AlertDialogPrimitive.Portal>
@@ -15,14 +15,14 @@
15
15
  link: "text-primary underline-offset-4 hover:underline",
16
16
  },
17
17
  size: {
18
- default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
18
+ default: "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
19
19
  xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
20
- sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
21
- lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
22
- icon: "size-8",
20
+ sm: "h-8 gap-1 rounded-[min(var(--radius-md),12px)] px-3 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3.5",
21
+ lg: "h-10 gap-2 px-4 has-data-[icon=inline-end]:pr-3.5 has-data-[icon=inline-start]:pl-3.5",
22
+ icon: "size-9",
23
23
  "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
24
- "icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
25
- "icon-lg": "size-9",
24
+ "icon-sm": "size-8 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
25
+ "icon-lg": "size-10",
26
26
  },
27
27
  },
28
28
  defaultVariants: {
@@ -10,6 +10,6 @@
10
10
  }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
11
  </script>
12
12
 
13
- <div bind:this={ref} data-slot="card-title" class={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)} {...restProps}>
13
+ <div bind:this={ref} data-slot="card-title" class={cn("leading-none font-semibold tracking-tight group-data-[size=sm]/card:text-sm", className)} {...restProps}>
14
14
  {@render children?.()}
15
15
  </div>
@@ -25,7 +25,7 @@
25
25
  bind:this={ref}
26
26
  data-slot={dataSlot}
27
27
  class={cn(
28
- "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
28
+ "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-9 rounded-lg border bg-transparent px-3 py-1.5 text-base transition-colors file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
29
29
  className
30
30
  )}
31
31
  type="file"
@@ -38,7 +38,7 @@
38
38
  bind:this={ref}
39
39
  data-slot={dataSlot}
40
40
  class={cn(
41
- "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
41
+ "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-9 rounded-lg border bg-transparent px-3 py-1.5 text-base transition-colors file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
42
42
  className
43
43
  )}
44
44
  {type}
@@ -20,7 +20,7 @@
20
20
  bind:this={ref}
21
21
  data-slot="select"
22
22
  class={cn(
23
- "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 w-full appearance-none rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors focus-visible:ring-3 aria-invalid:ring-3 md:text-sm placeholder:text-muted-foreground outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 bg-[length:16px] bg-[right_8px_center] bg-no-repeat",
23
+ "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-9 w-full appearance-none rounded-lg border bg-transparent px-3 py-1.5 text-base transition-colors focus-visible:ring-3 aria-invalid:ring-3 md:text-sm placeholder:text-muted-foreground outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 bg-[length:16px] bg-[right_8px_center] bg-no-repeat",
24
24
  "bg-[url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e\")]",
25
25
  className
26
26
  )}
@@ -27,7 +27,7 @@
27
27
  {sideOffset}
28
28
  {side}
29
29
  class={cn(
30
- "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm bg-foreground text-background z-50 w-fit max-w-xs origin-(--bits-tooltip-content-transform-origin)",
30
+ "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm bg-primary text-primary-foreground z-50 w-fit max-w-xs origin-(--bits-tooltip-content-transform-origin)",
31
31
  className
32
32
  )}
33
33
  {...restProps}
@@ -37,7 +37,7 @@
37
37
  {#snippet child({ props })}
38
38
  <div
39
39
  class={cn(
40
- "size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground z-50",
40
+ "size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-primary fill-primary z-50",
41
41
  "data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%+2px)]",
42
42
  "data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%+1px)]",
43
43
  "data-[side=right]:translate-x-[calc(50%+2px)] data-[side=right]:translate-y-1/2",