@vvedantb/ui 0.2.0 → 0.2.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.
Files changed (78) hide show
  1. package/README.md +89 -3
  2. package/app/README.md +100 -0
  3. package/app/src/confirm/ConfirmSkipHint.tsx +1 -1
  4. package/app/src/settings/SettingsEmptyState.tsx +2 -2
  5. package/app/src/settings/SettingsField.tsx +1 -1
  6. package/app/src/settings/SettingsSection.tsx +1 -1
  7. package/app/src/settings/SettingsToggleRow.tsx +1 -1
  8. package/package.json +16 -10
  9. package/shell/src/PillTabs.tsx +1 -1
  10. package/shell/src/SessionsHoverCard.tsx +148 -0
  11. package/shell/src/index.ts +5 -0
  12. package/src/ai-elements/artifact.tsx +1 -1
  13. package/src/ai-elements/attachments.tsx +3 -3
  14. package/src/ai-elements/chain-of-thought.tsx +5 -5
  15. package/src/ai-elements/code-block.tsx +2 -2
  16. package/src/ai-elements/conversation.tsx +2 -2
  17. package/src/ai-elements/inline-citation.tsx +6 -6
  18. package/src/ai-elements/message.tsx +2 -2
  19. package/src/ai-elements/prompt-input.tsx +3 -3
  20. package/src/ai-elements/queue.tsx +2 -2
  21. package/src/ai-elements/reasoning.tsx +1 -1
  22. package/src/ai-elements/shimmer.tsx +5 -5
  23. package/src/ai-elements/sources.tsx +2 -2
  24. package/src/ai-elements/speech-input.tsx +1 -1
  25. package/src/ai-elements/suggestion.tsx +1 -1
  26. package/src/audio/index.ts +3 -0
  27. package/src/audio/playChime.ts +76 -0
  28. package/src/audio/useReplyChime.ts +62 -0
  29. package/src/index.ts +2 -0
  30. package/src/ui/_menu-classes.alt.ts +2 -2
  31. package/src/ui/_menu-classes.ts +2 -2
  32. package/src/ui/accordion.alt.tsx +2 -2
  33. package/src/ui/badge.tsx +1 -1
  34. package/src/ui/breadcrumb.tsx +2 -2
  35. package/src/ui/button.tsx +3 -3
  36. package/src/ui/checkbox.tsx +1 -1
  37. package/src/ui/clear-input.alt.tsx +1 -1
  38. package/src/ui/clear-input.tsx +1 -1
  39. package/src/ui/command.tsx +5 -5
  40. package/src/ui/dialog.tsx +1 -1
  41. package/src/ui/input-group.alt.tsx +1 -1
  42. package/src/ui/input-group.tsx +2 -2
  43. package/src/ui/kbd.tsx +1 -1
  44. package/src/ui/labeled-switch-row.tsx +2 -2
  45. package/src/ui/progress.tsx +1 -1
  46. package/src/ui/radix-select.tsx +3 -3
  47. package/src/ui/search-input.tsx +1 -1
  48. package/src/ui/select.tsx +4 -4
  49. package/src/ui/sonner-icons.tsx +94 -0
  50. package/src/ui/sonner.tsx +167 -34
  51. package/src/ui/spinner.tsx +1 -1
  52. package/src/ui/switch.tsx +2 -2
  53. package/src/ui/table.tsx +2 -2
  54. package/src/ui/time-picker.tsx +3 -3
  55. package/tokens/README.md +98 -0
  56. package/tokens/examples/compat-shadcn-rgb/Fixture.tsx +45 -0
  57. package/tokens/examples/compat-shadcn-rgb/README.md +11 -0
  58. package/tokens/examples/compat-shadcn-rgb/index.html +12 -0
  59. package/tokens/examples/compat-shadcn-rgb/main.tsx +5 -0
  60. package/tokens/examples/compat-shadcn-rgb/package.json +14 -0
  61. package/tokens/examples/compat-shadcn-rgb/styles.css +67 -0
  62. package/tokens/examples/compat-shadcn-rgb/tsconfig.json +14 -0
  63. package/tokens/examples/compat-shadcn-rgb/vite.config.ts +11 -0
  64. package/tokens/examples/toaster/Fixture.tsx +53 -0
  65. package/tokens/examples/toaster/README.md +18 -0
  66. package/tokens/examples/toaster/compat.html +12 -0
  67. package/tokens/examples/toaster/main-compat.tsx +5 -0
  68. package/tokens/examples/toaster/main-tokens.tsx +5 -0
  69. package/tokens/examples/toaster/package.json +14 -0
  70. package/tokens/examples/toaster/styles-compat.css +82 -0
  71. package/tokens/examples/toaster/styles-tokens.css +16 -0
  72. package/tokens/examples/toaster/tokens.html +12 -0
  73. package/tokens/examples/toaster/tsconfig.json +14 -0
  74. package/tokens/examples/toaster/vite.config.ts +12 -0
  75. package/tokens/src/css/compat-shadcn-rgb.css +82 -0
  76. package/tokens/src/css/utilities.css +5 -0
  77. package/tokens/src/tailwind-preset.mjs +6 -0
  78. package/tokens/src/tokens.css +15 -0
@@ -0,0 +1,94 @@
1
+ import { Spinner } from "./spinner";
2
+
3
+ // 24px filled icons for `SonnerToaster` (`toastIcons="filled24"`). Self-contained
4
+ // SVGs, tinted with package colour tokens. Sonner wraps each in its `data-icon` slot.
5
+
6
+ function InfoIcon() {
7
+ return (
8
+ <svg
9
+ aria-hidden="true"
10
+ className="h-6 w-6 fill-current text-primary"
11
+ fill="none"
12
+ viewBox="0 0 24 24"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ >
15
+ <path d="M12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22ZM12.75 16C12.75 16.41 12.41 16.75 12 16.75C11.59 16.75 11.25 16.41 11.25 16L11.25 11C11.25 10.59 11.59 10.25 12 10.25C12.41 10.25 12.75 10.59 12.75 11L12.75 16ZM11.08 7.62C11.13 7.49 11.2 7.39 11.29 7.29C11.39 7.2 11.5 7.13 11.62 7.08C11.74 7.03 11.87 7 12 7C12.13 7 12.26 7.03 12.38 7.08C12.5 7.13 12.61 7.2 12.71 7.29C12.8 7.39 12.87 7.49 12.92 7.62C12.97 7.74 13 7.87 13 8C13 8.13 12.97 8.26 12.92 8.38C12.87 8.5 12.8 8.61 12.71 8.71C12.61 8.8 12.5 8.87 12.38 8.92C12.14 9.02 11.86 9.02 11.62 8.92C11.5 8.87 11.39 8.8 11.29 8.71C11.2 8.61 11.13 8.5 11.08 8.38C11.03 8.26 11 8.13 11 8C11 7.87 11.03 7.74 11.08 7.62Z" />
16
+ </svg>
17
+ );
18
+ }
19
+
20
+ function SuccessIcon() {
21
+ return (
22
+ <svg
23
+ aria-hidden="true"
24
+ className="h-6 w-6 fill-current text-success"
25
+ fill="none"
26
+ viewBox="0 0 24 24"
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ >
29
+ <path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16.78 9.7L11.11 15.37C10.97 15.51 10.78 15.59 10.58 15.59C10.38 15.59 10.19 15.51 10.05 15.37L7.22 12.54C6.93 12.25 6.93 11.77 7.22 11.48C7.51 11.19 7.99 11.19 8.28 11.48L10.58 13.78L15.72 8.64C16.01 8.35 16.49 8.35 16.78 8.64C17.07 8.93 17.07 9.4 16.78 9.7Z" />
30
+ </svg>
31
+ );
32
+ }
33
+
34
+ function WarningIcon() {
35
+ return (
36
+ <svg
37
+ aria-hidden="true"
38
+ className="h-6 w-6 fill-current text-warning"
39
+ fill="none"
40
+ viewBox="0 0 24 24"
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ >
43
+ <path
44
+ clipRule="evenodd"
45
+ d="M3 10.417C3 7.219 3 5.62 3.378 5.082C3.755 4.545 5.258 4.03 8.265 3.001L8.838 2.805C10.405 2.268 11.188 2 12 2C12.812 2 13.595 2.268 15.162 2.805L15.735 3.001C18.742 4.03 20.245 4.545 20.622 5.082C21 5.62 21 7.22 21 10.417V11.991C21 17.629 16.761 20.366 14.101 21.527C13.38 21.842 13.02 22 12 22C10.98 22 10.62 21.842 9.899 21.527C7.239 20.365 3 17.63 3 11.991V10.417ZM12 7.25C12.1989 7.25 12.3897 7.32902 12.5303 7.46967C12.671 7.61032 12.75 7.80109 12.75 8V12C12.75 12.1989 12.671 12.3897 12.5303 12.5303C12.3897 12.671 12.1989 12.75 12 12.75C11.8011 12.75 11.6103 12.671 11.4697 12.5303C11.329 12.3897 11.25 12.1989 11.25 12V8C11.25 7.80109 11.329 7.61032 11.4697 7.46967C11.6103 7.32902 11.8011 7.25 12 7.25ZM12 16C12.2652 16 12.5196 15.8946 12.7071 15.7071C12.8946 15.5196 13 15.2652 13 15C13 14.7348 12.8946 14.4804 12.7071 14.2929C12.5196 14.1054 12.2652 14 12 14C11.7348 14 11.4804 14.1054 11.2929 14.2929C11.1054 14.4804 11 14.7348 11 15C11 15.2652 11.1054 15.5196 11.2929 15.7071C11.4804 15.8946 11.7348 16 12 16Z"
46
+ fillRule="evenodd"
47
+ />
48
+ </svg>
49
+ );
50
+ }
51
+
52
+ function ErrorIcon() {
53
+ return (
54
+ <svg
55
+ aria-hidden="true"
56
+ className="h-6 w-6 fill-current text-destructive"
57
+ fill="none"
58
+ viewBox="0 0 20 20"
59
+ xmlns="http://www.w3.org/2000/svg"
60
+ >
61
+ <path d="M17.51 3.85L11.57 0.42C10.6 -0.14 9.4 -0.14 8.42 0.42L2.49 3.85C1.52 4.41 0.919998 5.45 0.919998 6.58V13.42C0.919998 14.54 1.52 15.58 2.49 16.15L8.43 19.58C9.4 20.14 10.6 20.14 11.58 19.58L17.52 16.15C18.49 15.59 19.09 14.55 19.09 13.42V6.58C19.08 5.45 18.48 4.42 17.51 3.85ZM9.25 5.75C9.25 5.34 9.59 5 10 5C10.41 5 10.75 5.34 10.75 5.75V11C10.75 11.41 10.41 11.75 10 11.75C9.59 11.75 9.25 11.41 9.25 11V5.75ZM10.92 14.63C10.87 14.75 10.8 14.86 10.71 14.96C10.52 15.15 10.27 15.25 10 15.25C9.87 15.25 9.74 15.22 9.62 15.17C9.49 15.12 9.39 15.05 9.29 14.96C9.2 14.86 9.13 14.75 9.07 14.63C9.02 14.51 9 14.38 9 14.25C9 13.99 9.1 13.73 9.29 13.54C9.39 13.45 9.49 13.38 9.62 13.33C9.99 13.17 10.43 13.26 10.71 13.54C10.8 13.64 10.87 13.74 10.92 13.87C10.97 13.99 11 14.12 11 14.25C11 14.38 10.97 14.51 10.92 14.63Z" />
62
+ </svg>
63
+ );
64
+ }
65
+
66
+ function CloseIcon() {
67
+ return (
68
+ <svg
69
+ aria-hidden="true"
70
+ className="h-3.5 w-3.5"
71
+ fill="none"
72
+ stroke="currentColor"
73
+ strokeLinecap="round"
74
+ strokeLinejoin="round"
75
+ strokeWidth={2}
76
+ viewBox="0 0 24 24"
77
+ xmlns="http://www.w3.org/2000/svg"
78
+ >
79
+ <path d="M18 6L6 18M6 6l12 12" />
80
+ </svg>
81
+ );
82
+ }
83
+
84
+ /** 24px success / info / warning / error icons, a matching loader and a 14px close cross. */
85
+ const filledToastIcons = {
86
+ success: <SuccessIcon />,
87
+ info: <InfoIcon />,
88
+ warning: <WarningIcon />,
89
+ error: <ErrorIcon />,
90
+ loading: <Spinner size="md" aria-hidden="true" />,
91
+ close: <CloseIcon />,
92
+ };
93
+
94
+ export { filledToastIcons };
package/src/ui/sonner.tsx CHANGED
@@ -1,47 +1,180 @@
1
+ import type { CSSProperties } from "react";
1
2
  import { Toaster, type ToasterProps } from "sonner";
3
+ import { filledToastIcons } from "./sonner-icons";
2
4
  import "./sonner.css";
3
5
 
4
- // sonner toaster aligned with flat tonal surfaces `unstyled: true` strips sonner's default visual styling so our classes are
5
- // the single source of truth toast surfaces use `glass, panel, strong` (flat
6
- // popover fill) plus `smooth-shadow-ring-md` `!font, sans` is forced on the toaster section because sonner sets its own
7
- // `font, family` on `[data, sonner, toaster]` from a stylesheet that ships with
8
- // the package (loaded after our globals), so plain inheritance from `<body>`
9
- // loses the cascade the `!important` here guarantees instrument sans wins, toasts inherit from the section, so this single class covers everything variant icons (success / error / warning / info) are tinted via a
10
- // `data, icon` descendant selector, leaving the rest of the toast neutral callers should pass `theme` from `next, themes` so sonner's internal
11
- // `data, theme` attribute matches the active app theme (used by its focus
12
- // ring and a few internal states that aren't reachable via classnames) stacking: `expand={false}` keeps toasts collapsed (offset + scale) hovering
13
- // the stack expands them `sonner.css` hides back, toast content while collapsed
14
- // because `unstyled` disables sonner's built, in `data, styled` stack rules
15
- function SonnerToaster(props: ToasterProps) {
6
+ // Sonner toaster aligned with flat tonal surfaces. `unstyled: true` strips
7
+ // sonner's default visual styling so our classes are the single source of
8
+ // truth. Toast surfaces use `glass-panel-strong` (flat popover fill) plus
9
+ // `smooth-shadow-ring-md`. `!font-sans` is forced on the toaster section because
10
+ // sonner sets its own `font-family` on `[data-sonner-toaster]` from a stylesheet
11
+ // that loads after app globals. Variant icons (success / error / warning / info)
12
+ // are tinted via a `data-icon` descendant selector, leaving the rest of the toast
13
+ // neutral. Callers should pass `theme` so sonner's internal `data-theme`
14
+ // attribute matches the active app theme.
15
+ //
16
+ // Stacking: `expand={false}` keeps toasts collapsed (offset + scale). Hovering
17
+ // the stack expands them. `sonner.css` hides back-toast content while collapsed
18
+ // because `unstyled` disables sonner's built-in `data-styled` stack rules.
19
+ //
20
+ // Caller props merge with these defaults instead of replacing them: see
21
+ // `mergeToastClassNames`, `mergeToasterStyle` and the `SonnerToaster` props.
22
+
23
+ type ToastClassNames = NonNullable<
24
+ NonNullable<ToasterProps["toastOptions"]>["classNames"]
25
+ >;
26
+ type ToasterStyle = CSSProperties & Record<`--${string}`, string>;
27
+
28
+ type SonnerToasterOptions = {
29
+ /** `"lg"` (default) uses `rounded-lg`. `"surface"` uses `rounded-surface`. */
30
+ toastRadius?: "lg" | "surface";
31
+ /** `"default"` keeps sonner's icons. `"filled24"` uses 24px filled icons. */
32
+ toastIcons?: "default" | "filled24";
33
+ /**
34
+ * `"default"` keeps the text-style close button. `"hoverChip"` uses a round chip at the
35
+ * top-right, shown on hover (always on small screens) and enables sonner's `closeButton`
36
+ * unless the caller sets it.
37
+ */
38
+ closeButtonStyle?: "default" | "hoverChip";
39
+ /** When true, set sonner's `--normal-*` variables from `--color-*` tokens and `font-family: inherit`. */
40
+ sonnerVars?: boolean;
41
+ /** When true, descriptions use `text-muted-foreground!` so sonner's dark rule cannot win. */
42
+ descriptionMuted?: boolean;
43
+ };
44
+
45
+ const hoverChipCloseButtonClassName =
46
+ "pointer-events-none absolute -right-2 -top-2 z-10 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-card/95 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring group-hover:pointer-events-auto group-hover:opacity-100 max-sm:pointer-events-auto max-sm:opacity-100 max-sm:hit-target";
47
+
48
+ const defaultCloseButtonClassName =
49
+ "rounded-lg bg-default px-2.5 py-1 text-xs font-medium text-foreground transition-colors hover:bg-default/78";
50
+
51
+ /** Default `classNames`, with the opt-in variants applied. No options gives the 0.2.1 defaults. */
52
+ function createToasterClassNames({
53
+ toastRadius = "lg",
54
+ closeButtonStyle = "default",
55
+ descriptionMuted = false,
56
+ }: Pick<
57
+ SonnerToasterOptions,
58
+ "toastRadius" | "closeButtonStyle" | "descriptionMuted"
59
+ > = {}): ToastClassNames {
60
+ const radius = toastRadius === "surface" ? "rounded-surface" : "rounded-lg";
61
+ return {
62
+ toast: `group toast glass-panel-strong flex w-full items-center gap-3 ${radius} px-4 py-3 text-sm text-foreground smooth-shadow-ring-md`,
63
+ title: "font-medium leading-snug",
64
+ description: descriptionMuted
65
+ ? "text-[13px] leading-snug text-muted-foreground!"
66
+ : "text-[13px] leading-snug text-muted-foreground",
67
+ actionButton:
68
+ "rounded-lg bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground transition-opacity hover:opacity-90",
69
+ cancelButton:
70
+ "rounded-lg bg-surface-secondary px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-surface-tertiary",
71
+ closeButton:
72
+ closeButtonStyle === "hoverChip"
73
+ ? hoverChipCloseButtonClassName
74
+ : defaultCloseButtonClassName,
75
+ loader: "text-muted-foreground",
76
+ success: "[&_[data-icon]]:text-success",
77
+ error: "[&_[data-icon]]:text-danger",
78
+ warning: "[&_[data-icon]]:text-warning",
79
+ info: "[&_[data-icon]]:text-primary",
80
+ };
81
+ }
82
+
83
+ /** Per key, joins the default class string and the caller string (defaults first, so callers win). */
84
+ function mergeToastClassNames(
85
+ defaults: ToastClassNames,
86
+ overrides?: ToastClassNames,
87
+ ): ToastClassNames {
88
+ if (!overrides) return defaults;
89
+ const merged: Record<string, string | undefined> = { ...defaults };
90
+ for (const [key, value] of Object.entries(overrides)) {
91
+ if (!value) continue;
92
+ const base = merged[key];
93
+ merged[key] = base ? `${base} ${value}` : value;
94
+ }
95
+ return merged as ToastClassNames;
96
+ }
97
+
98
+ /** Sonner variables read from `--color-*` tokens. Work with `tokens.css` and the compat file. */
99
+ const sonnerVarsStyle: ToasterStyle = {
100
+ fontFamily: "inherit",
101
+ "--normal-bg": "var(--color-card)",
102
+ "--normal-border": "var(--color-border)",
103
+ "--normal-text": "var(--color-foreground)",
104
+ "--normal-bg-hover": "var(--color-muted)",
105
+ "--normal-border-hover": "var(--color-border)",
106
+ };
107
+
108
+ /** `{ ...defaults, ...overrides }`. The package adds no style keys unless `sonnerVars` is on. */
109
+ function mergeToasterStyle(
110
+ defaults: CSSProperties | undefined,
111
+ overrides: CSSProperties | undefined,
112
+ ): CSSProperties | undefined {
113
+ if (!defaults) return overrides;
114
+ return overrides ? { ...defaults, ...overrides } : defaults;
115
+ }
116
+
117
+ /** Package defaults first. Caller wins per field, except `classNames`, which merge. */
118
+ function mergeToastOptions(
119
+ defaults: NonNullable<ToasterProps["toastOptions"]>,
120
+ overrides?: ToasterProps["toastOptions"],
121
+ ): NonNullable<ToasterProps["toastOptions"]> {
122
+ const { classNames, ...rest } = overrides ?? {};
123
+ return {
124
+ ...defaults,
125
+ ...rest,
126
+ classNames: mergeToastClassNames(defaults.classNames ?? {}, classNames),
127
+ };
128
+ }
129
+
130
+ function SonnerToaster({
131
+ toastRadius,
132
+ toastIcons = "default",
133
+ closeButtonStyle = "default",
134
+ sonnerVars = false,
135
+ descriptionMuted,
136
+ toastOptions,
137
+ icons,
138
+ style,
139
+ className,
140
+ closeButton,
141
+ ...props
142
+ }: ToasterProps & SonnerToasterOptions) {
143
+ const mergedIcons =
144
+ toastIcons === "filled24" ? { ...filledToastIcons, ...icons } : icons;
16
145
  return (
17
146
  <Toaster
18
147
  expand={false}
19
148
  visibleToasts={4}
20
149
  gap={12}
21
- className="toaster group !font-sans"
22
- toastOptions={{
23
- unstyled: true,
24
- classNames: {
25
- toast:
26
- "group toast glass-panel-strong flex w-full items-center gap-3 rounded-lg px-4 py-3 text-sm text-foreground smooth-shadow-ring-md",
27
- title: "font-medium leading-snug",
28
- description: "text-[13px] leading-snug text-muted",
29
- actionButton:
30
- "rounded-lg bg-accent px-2.5 py-1 text-xs font-medium text-accent-foreground transition-opacity hover:opacity-90",
31
- cancelButton:
32
- "rounded-lg bg-surface-secondary px-2.5 py-1 text-xs font-medium text-muted transition-colors hover:bg-surface-tertiary",
33
- closeButton:
34
- "rounded-lg bg-default px-2.5 py-1 text-xs font-medium text-foreground transition-colors hover:bg-default/78",
35
- loader: "text-muted",
36
- success: "[&_[data-icon]]:text-success",
37
- error: "[&_[data-icon]]:text-danger",
38
- warning: "[&_[data-icon]]:text-warning",
39
- info: "[&_[data-icon]]:text-accent",
40
- },
41
- }}
42
150
  {...props}
151
+ closeButton={closeButton ?? (closeButtonStyle === "hoverChip" ? true : undefined)}
152
+ className={className ? `toaster group !font-sans ${className}` : "toaster group !font-sans"}
153
+ icons={mergedIcons}
154
+ style={mergeToasterStyle(sonnerVars ? sonnerVarsStyle : undefined, style)}
155
+ toastOptions={mergeToastOptions(
156
+ {
157
+ unstyled: true,
158
+ classNames: createToasterClassNames({
159
+ toastRadius,
160
+ closeButtonStyle,
161
+ descriptionMuted,
162
+ }),
163
+ },
164
+ toastOptions,
165
+ )}
43
166
  />
44
167
  );
45
168
  }
46
169
 
47
- export { SonnerToaster };
170
+ export {
171
+ SonnerToaster,
172
+ createToasterClassNames,
173
+ hoverChipCloseButtonClassName,
174
+ mergeToastClassNames,
175
+ mergeToastOptions,
176
+ mergeToasterStyle,
177
+ sonnerVarsStyle,
178
+ };
179
+ export { filledToastIcons } from "./sonner-icons";
180
+ export type { SonnerToasterOptions };
@@ -18,7 +18,7 @@ function Spinner({
18
18
  <IconLoader2
19
19
  role="status"
20
20
  aria-label="Loading"
21
- className={cn("animate-spin text-muted", sizeClasses[size], className)}
21
+ className={cn("animate-spin text-muted-foreground", sizeClasses[size], className)}
22
22
  {...props}
23
23
  />
24
24
  );
package/src/ui/switch.tsx CHANGED
@@ -8,7 +8,7 @@ const Switch = React.forwardRef<
8
8
  >(({ className, ...props }, ref) => (
9
9
  <SwitchPrimitive.Root
10
10
  className={cn(
11
- "peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-border bg-default transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-accent data-[state=checked]:bg-accent data-[state=unchecked]:bg-default before:absolute before:inset-[-8px] before:content-['']",
11
+ "peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-border bg-default transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=unchecked]:bg-default before:absolute before:inset-[-8px] before:content-['']",
12
12
  className,
13
13
  )}
14
14
  {...props}
@@ -16,7 +16,7 @@ const Switch = React.forwardRef<
16
16
  >
17
17
  <SwitchPrimitive.Thumb
18
18
  className={cn(
19
- "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-[transform,background-color] duration-200 ease-smooth data-[state=checked]:translate-x-4 data-[state=checked]:bg-accent-foreground data-[state=unchecked]:translate-x-0",
19
+ "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-[transform,background-color] duration-200 ease-smooth data-[state=checked]:translate-x-4 data-[state=checked]:bg-primary-foreground data-[state=unchecked]:translate-x-0",
20
20
  )}
21
21
  />
22
22
  </SwitchPrimitive.Root>
package/src/ui/table.tsx CHANGED
@@ -76,7 +76,7 @@ const TableHead = React.forwardRef<
76
76
  <th
77
77
  ref={ref}
78
78
  className={cn(
79
- "h-11 px-4 text-left align-middle text-xs font-semibold uppercase tracking-widest text-muted [&:has([role=checkbox])]:pr-0",
79
+ "h-11 px-4 text-left align-middle text-xs font-semibold uppercase tracking-widest text-muted-foreground [&:has([role=checkbox])]:pr-0",
80
80
  className,
81
81
  )}
82
82
  {...props}
@@ -105,7 +105,7 @@ const TableCaption = React.forwardRef<
105
105
  >(({ className, ...props }, ref) => (
106
106
  <caption
107
107
  ref={ref}
108
- className={cn("mt-4 text-sm text-muted", className)}
108
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
109
109
  {...props}
110
110
  />
111
111
  ));
@@ -94,7 +94,7 @@ const TimePicker = React.forwardRef<HTMLButtonElement, TimePickerProps>(
94
94
  )}
95
95
  >
96
96
  <span>{`${hour}:${minute}`}</span>
97
- <IconClock className="h-3.5 w-3.5 text-muted" />
97
+ <IconClock className="h-3.5 w-3.5 text-muted-foreground" />
98
98
  </button>
99
99
  </PopoverTrigger>
100
100
  <PopoverContent
@@ -134,7 +134,7 @@ const TimeColumn = React.forwardRef<HTMLDivElement, TimeColumnProps>(
134
134
  ({ label, values, selected, onSelect }, ref) => {
135
135
  return (
136
136
  <div className="flex w-14 flex-col">
137
- <div className="px-2 pb-1 text-[10px] uppercase tracking-wider text-muted">
137
+ <div className="px-2 pb-1 text-[10px] uppercase tracking-wider text-muted-foreground">
138
138
  {label}
139
139
  </div>
140
140
  <div
@@ -152,7 +152,7 @@ const TimeColumn = React.forwardRef<HTMLDivElement, TimeColumnProps>(
152
152
  className={cn(
153
153
  "flex h-7 w-full items-center justify-center text-xs tabular-nums transition-colors",
154
154
  isSelected
155
- ? "bg-accent text-accent-foreground"
155
+ ? "bg-primary text-primary-foreground"
156
156
  : "text-foreground/80 hover:bg-surface-tertiary/50",
157
157
  )}
158
158
  >
@@ -0,0 +1,98 @@
1
+ # `@vvedantb/ui/tokens`
2
+
3
+ Design tokens and shared Tailwind v4 CSS for the vvedantb web apps, plus JS tokens for React Native. These are subpaths of `@vvedantb/ui`, source-exported: no build step. Paths in the `File` column are relative to `packages/ui/tokens/`.
4
+
5
+ ## Exports
6
+
7
+ | Export | File | What it is |
8
+ | --- | --- | --- |
9
+ | `@vvedantb/ui/tokens`, `@vvedantb/ui/tokens/tokens.css` | `src/tokens.css` | Colour, radius, shadow, ease and font tokens (`@theme`), light/dark variables, `dark` variant, `*` border and `body` defaults. In 0.2.1 it also defines shadcn-style aliases (`primary`, `muted-foreground`, `secondary`, `destructive`, `card`, `popover`, `input`, `ring`) that point at the same tokens. |
10
+ | `@vvedantb/ui/tokens/native` | `src/native.ts` | JS colours, radii and spacing for React Native. `@vvedantb/native-ui` keeps its own copy in `src/tokens.ts`; a test keeps the two equal. |
11
+ | `@vvedantb/ui/tokens/css/base.css` | `src/css/base.css` | `html` touch/accent, `::selection`, control transitions, pointer cursor, range accent (`@layer base`). |
12
+ | `@vvedantb/ui/tokens/css/dialog.css` | `src/css/dialog.css` | Unlayered `max-width: none` for `@vvedantb/ui` compact (bottom-sheet) dialogs. |
13
+ | `@vvedantb/ui/tokens/css/modal.css` | `src/css/modal.css` | `.t-modal`, `.is-open`, `.is-closing` and `t-modal-presence-wait` (`@layer components`). |
14
+ | `@vvedantb/ui/tokens/css/tabs.css` | `src/css/tabs.css` | `.t-tab`, active colour, `.t-tabs-pill`, small-screen `.t-tabs` scrolling. |
15
+ | `@vvedantb/ui/tokens/css/tabs-pills.css` | `src/css/tabs-pills.css` | `--tabs-*` colour variables, `.t-tabs--segmented`, `.t-tabs--pills` and pill hover / active fills. |
16
+ | `@vvedantb/ui/tokens/css/utilities.css` | `src/css/utilities.css` | `@utility hit-target`, `@utility reveal-on-hover` and `@utility rounded-surface` (`clamp(0.75rem, var(--radius), 1rem)`). |
17
+ | `@vvedantb/ui/tokens/css/scrollbar-thin.css` | `src/css/scrollbar-thin.css` | `.scrollbar-thin` (`@layer utilities`, always emitted). |
18
+ | `@vvedantb/ui/tokens/css/animations.css` | `src/css/animations.css` | `animate-aurora`, `animate-fade-in-up`, `animate-indeterminate` as `@theme` (CSS-first apps). |
19
+ | `@vvedantb/ui/tokens/css/compat-shadcn-rgb.css`, `@vvedantb/ui/tokens/compat/shadcn-rgb.css` | `src/css/compat-shadcn-rgb.css` | Replaces `tokens.css` for apps with shadcn-style `R G B` channel variables. Defines only the UI-only `--color-*` names as `rgb(var(--x) / alpha)` references, `--radius-field`, `--scrollbar`, `--field-radius` and a `glass-panel-strong` fallback. Import it **or** `tokens.css`, never both. |
20
+ | `@vvedantb/ui/tokens/css/index.css` | `src/css/index.css` | base + dialog + utilities + scrollbar-thin + animations, for new CSS-first apps. |
21
+ | `@vvedantb/ui/tokens/tailwind-preset` | `src/tailwind-preset.mjs` (+ `.d.mts`) | Tailwind preset for apps that load a JS config with `@config`. |
22
+
23
+ Every file was extracted because the same rule (same at-rule context, selector and body) sits in 3 or more of the five apps. The measured duplication is in `verify/ANALYSIS.md`, and the proof that each app compiles to the same CSS after migrating is in `verify/RESULTS.md`. Blocks that only 2 apps share, or that drift in a way that would change an app's CSS, stay in the apps; `verify/ANALYSIS.md` lists them.
24
+
25
+ ## Use
26
+
27
+ CSS-first app (no `tailwind.config`):
28
+
29
+ ```css
30
+ @import "tailwindcss";
31
+ @import "@vvedantb/ui/tokens";
32
+ @import "@vvedantb/ui/tokens/css/animations.css";
33
+ @import "@vvedantb/ui/tokens/css/base.css";
34
+ /* ...app @layer base rules... */
35
+ @import "@vvedantb/ui/tokens/css/modal.css";
36
+ @import "@vvedantb/ui/tokens/css/tabs.css";
37
+ /* ...app @layer components rules... */
38
+ ```
39
+
40
+ App with a JS config (`@config "../tailwind.config.ts"`):
41
+
42
+ ```ts
43
+ import { tokensPreset } from "@vvedantb/ui/tokens/tailwind-preset";
44
+
45
+ export default {
46
+ presets: [tokensPreset()],
47
+ // or tokensPreset({ color: (name) => `color-mix(in oklch, var(--${name}) calc(<alpha-value> * 100%), transparent)` })
48
+ content: ["./src/**/*.{ts,tsx}"],
49
+ theme: { extend: { /* app-only colours, fonts, animations */ } },
50
+ plugins: [tailwindcssAnimate],
51
+ };
52
+ ```
53
+
54
+ The preset holds `darkMode: "class"`, the shared colour map (`border`, `surface.*`, `danger.*`, `field.*`, shadcn aliases `primary`/`secondary`/`destructive`/`card`/`popover`/`input` ...), `borderRadius` lg/md/sm/field, `boxShadow` soft/panel/insetSoft, `ease-smooth`, `screens` xs to 2xl in px, and the aurora / fade-in-up / indeterminate / accordion keyframes. It does not hold plugins, `content`, fonts, `accent`/`muted`/`ring`/`sidebar` (they differ per app) or accordion timings.
55
+
56
+ Do not import `css/animations.css` in an app that uses the preset: the preset already defines the same keyframes and animations.
57
+
58
+ ### Where to put the imports
59
+
60
+ Tailwind inlines an `@import` where it sits, even in the middle of a file (it must be at top level, not inside an `@layer` block). Put each import where the app's own copy of the rules was. Within one layer, later rules win at equal specificity, so moving shared rules above or below app rules can flip the result for elements matched by both. `examples/migration.md` gives the exact position for each app, and `pnpm --filter @vvedantb/ui verify` checks it.
61
+
62
+ ### Variables each file needs
63
+
64
+ | File | Needs | Provided by |
65
+ | --- | --- | --- |
66
+ | `base.css` | `--accent`, `--foreground` | tokens.css or the app theme |
67
+ | `dialog.css` | none | |
68
+ | `modal.css` | `--modal-open-dur`, `--modal-close-dur`, `--modal-scale`, `--modal-scale-close`, `--modal-ease` | app `:root` |
69
+ | `tabs.css` | `--tabs-dur`, `--tabs-ease` (app `:root`); `--tabs-text-muted`, `--tabs-text-active`, `--tabs-pill-bg` | colours: tabs-pills.css or the app theme |
70
+ | `tabs-pills.css` | `--muted`, `--segment`, `--segment-foreground`, `--default` | tokens.css or the app theme |
71
+ | `utilities.css` | `--breakpoint-sm` (Tailwind default or app theme) | Tailwind |
72
+ | `scrollbar-thin.css` | `--scrollbar` | tokens.css or the app theme |
73
+ | `animations.css` | none | |
74
+ | `tailwind-preset` | the CSS variables named in its colour map, `--radius`, `--field-radius` | tokens.css or the app theme |
75
+
76
+ ### Using an existing shadcn RGB theme
77
+
78
+ See the package `README.md`. With the compat file, `base.css`, `scrollbar-thin.css`, `utilities.css`, `dialog.css`, `modal.css` and `tabs.css` need no `tokens.css`. `base.css` reads `--accent` and `--foreground` and `tabs-pills.css` reads `--muted`, `--segment` and `--default`. Those are not channel-compatible, so skip `tabs-pills.css` and `index.css` (which includes `base.css`) unless the app defines those variables as full colours.
79
+
80
+ ### Parameters
81
+
82
+ Where apps share a rule but drifted in one value, the value is a custom property whose fallback is the most common value. Set it on `:root` to keep a different value.
83
+
84
+ | File | Property | Default | Set by |
85
+ | --- | --- | --- | --- |
86
+ | `base.css` | `--tokens-control-transition-property` | `background-color, border-color, color, box-shadow, transform, opacity` | app-c: `background-color, color, transform, opacity` |
87
+ | `base.css` | `--tokens-control-transition-duration` | `240ms` | app-c: `100ms` |
88
+
89
+ ## Develop
90
+
91
+ ```sh
92
+ pnpm --filter @vvedantb/ui build # export targets exist + tsc (primitives, shell, app)
93
+ pnpm --filter @vvedantb/ui test # vitest (app helpers) + offline node --test: exports, CSS structure, Tailwind compile, preset
94
+ pnpm --filter @vvedantb/ui analyze # rule-level duplication across the app clones -> verify/ANALYSIS.tables.md
95
+ pnpm --filter @vvedantb/ui verify # before/after compile of every app -> verify/RESULTS.md, examples/migration.md
96
+ ```
97
+
98
+ `analyze` and `verify` need the app clones (`APPS_DIR`, default `/workspace/cvx`; name or symlink the clones `app-a` to `app-e` inside it) and network access the first time: they install Tailwind 4.3.3, the plugins, postcss and jiti into a scratch directory (`VERIFY_DIR`, default `/workspace/pk-tokens-verify`, outside the repo). The migration for each app is data in `verify/migrations/<app>.json`. Each removed block must equal the shared file's block with the same context and selector, or the run fails.
@@ -0,0 +1,45 @@
1
+ import { useState } from "react";
2
+ import { toast } from "sonner";
3
+ import {
4
+ Button,
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogTitle,
9
+ DialogTrigger,
10
+ Input,
11
+ SonnerToaster,
12
+ Switch,
13
+ Textarea,
14
+ } from "../../../src";
15
+
16
+ export function Fixture() {
17
+ const [dark, setDark] = useState(false);
18
+ return (
19
+ <div className={dark ? "dark" : ""}>
20
+ <main className="min-h-screen space-y-6 bg-background p-8 text-foreground">
21
+ <section className="flex flex-wrap items-center gap-3">
22
+ <Button onClick={() => setDark((d) => !d)}>Toggle theme</Button>
23
+ <Button onClick={() => toast("Saved", { description: "Fixture toast" })}>
24
+ Show toast
25
+ </Button>
26
+ <Dialog>
27
+ <DialogTrigger asChild>
28
+ <Button>Open dialog</Button>
29
+ </DialogTrigger>
30
+ <DialogContent>
31
+ <DialogTitle>Dialog</DialogTitle>
32
+ <DialogDescription>Body text on the popover colour.</DialogDescription>
33
+ </DialogContent>
34
+ </Dialog>
35
+ </section>
36
+ <section className="grid max-w-md gap-3">
37
+ <Input placeholder="Input placeholder" />
38
+ <Textarea placeholder="Textarea placeholder" />
39
+ <Switch defaultChecked aria-label="Sample switch" />
40
+ </section>
41
+ <SonnerToaster />
42
+ </main>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,11 @@
1
+ # compat-shadcn-rgb fixture
2
+
3
+ Renders Button, toast, Dialog, Input, Textarea and Switch against sample shadcn-style `R G B` variables for light and dark. `styles.css` imports `compat-shadcn-rgb.css` and not `tokens.css`.
4
+
5
+ Run it from this folder (it is not a workspace member, and React resolves from the parent `packages/ui` install):
6
+
7
+ ```sh
8
+ pnpm install --ignore-workspace && pnpm dev
9
+ ```
10
+
11
+ Open the printed URL and use "Toggle theme" to switch between light and dark. `Fixture.tsx` is not part of the package typecheck.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>compat-shadcn-rgb fixture</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,5 @@
1
+ import { createRoot } from "react-dom/client";
2
+ import { Fixture } from "./Fixture";
3
+ import "./styles.css";
4
+
5
+ createRoot(document.getElementById("root")!).render(<Fixture />);
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "compat-shadcn-rgb-fixture",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build"
8
+ },
9
+ "devDependencies": {
10
+ "@tailwindcss/vite": "4.3.3",
11
+ "tailwindcss": "4.3.3",
12
+ "vite": "^7.0.0"
13
+ }
14
+ }
@@ -0,0 +1,67 @@
1
+ @import "tailwindcss";
2
+ @import "../../src/css/compat-shadcn-rgb.css";
3
+ @source "../../../src/**/*.{ts,tsx}";
4
+ @source "./*.tsx";
5
+
6
+ @custom-variant dark (&:where(.dark, .dark *));
7
+
8
+ /* Sample shadcn-style RGB channel variables owned by the app. */
9
+ :root {
10
+ --background: 244 245 246;
11
+ --foreground: 24 24 27;
12
+ --card: 255 255 255;
13
+ --card-foreground: 24 24 27;
14
+ --popover: 255 255 255;
15
+ --popover-foreground: 24 24 27;
16
+ --primary: 37 99 235;
17
+ --primary-foreground: 255 255 255;
18
+ --secondary: 235 236 238;
19
+ --secondary-foreground: 24 24 27;
20
+ --muted: 235 236 238;
21
+ --muted-foreground: 100 100 110;
22
+ --accent: 235 236 238;
23
+ --accent-foreground: 24 24 27;
24
+ --destructive: 220 38 38;
25
+ --destructive-foreground: 255 255 255;
26
+ --border: 224 225 228;
27
+ --input: 224 225 228;
28
+ --ring: 37 99 235;
29
+ --radius: 0.75rem;
30
+ }
31
+
32
+ .dark {
33
+ --background: 15 15 17;
34
+ --foreground: 244 244 245;
35
+ --card: 28 28 31;
36
+ --card-foreground: 244 244 245;
37
+ --popover: 28 28 31;
38
+ --popover-foreground: 244 244 245;
39
+ --primary: 96 165 250;
40
+ --primary-foreground: 15 15 17;
41
+ --secondary: 39 39 43;
42
+ --secondary-foreground: 244 244 245;
43
+ --muted: 39 39 43;
44
+ --muted-foreground: 161 161 170;
45
+ --accent: 39 39 43;
46
+ --accent-foreground: 244 244 245;
47
+ --destructive: 248 113 113;
48
+ --destructive-foreground: 15 15 17;
49
+ --border: 46 46 51;
50
+ --input: 46 46 51;
51
+ --ring: 96 165 250;
52
+ }
53
+
54
+ @theme inline {
55
+ --color-background: rgb(var(--background));
56
+ --color-foreground: rgb(var(--foreground));
57
+ --color-border: rgb(var(--border));
58
+ --color-primary: rgb(var(--primary));
59
+ --color-primary-foreground: rgb(var(--primary-foreground));
60
+ --color-muted-foreground: rgb(var(--muted-foreground));
61
+ }
62
+
63
+ body {
64
+ background: rgb(var(--background));
65
+ color: rgb(var(--foreground));
66
+ font-family: system-ui, sans-serif;
67
+ }