@vvedantb/ui 0.14.0 → 0.15.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/README.md CHANGED
@@ -7,7 +7,7 @@ Shared React 19 web UI. Source-exported: no build step.
7
7
  | `@vvedantb/ui` | The only JavaScript entry. Primitives (Radix-based), AI elements, audio (`playChime`, `useReplyChime`, `playTone`), `toast` and `SonnerToaster`, the composer (`InputArea`, deprecated 0.3 `Composer`), shell (`SidebarHeader`, `PillTabs`, `SessionsHoverCard`, and the app shell: `AppShell`, `AppSidebar`, `SidebarRail`, `RailTile`, `SidebarPanel`, `SidebarAccountMenu`, `SignOutDialog` and related helpers), app helpers (confirm skip, update watcher, settings chrome, `useStableAuth`, and from 0.8.0 `createThemeProvider`, `createPageTitleContext`, `PageMotionProvider` and `formatCompactRelativeTime`), marketing pieces from 0.9.0 (`createLandingKit` and the `Landing*` parts, `SiteFooter`, `PricingModal`), animated nav icons from 0.11.0 (`MemoryIcon`, `BoardIcon`, ... and `NavIconBase`; `SettingsIcon` `variant` from 0.12.0), artifact viewers from 0.13.0 (`ArtifactFrame`, `ArtifactList`, `ArtifactViewer`, ...), date pickers from 0.14.0 (`Calendar`, `DatePicker`, `DateTimePicker`, `MonthPicker`), `cn`, and the native tokens `tokens` and `Tokens`. See `app/README.md` in the repository for the app helpers. |
8
8
  | `@vvedantb/ui/tokens` | `tokens.css`: colour, radius, shadow, ease and font tokens for Tailwind v4. CSS. |
9
9
  | `@vvedantb/ui/tokens/tokens.css` | Same file as `@vvedantb/ui/tokens`. CSS. |
10
- | `@vvedantb/ui/tokens/css/*.css` | `index`, `base`, `dialog`, `modal`, `tabs`, `tabs-pills`, `utilities`, `scrollbar-thin`, `animations`, `nav-icons`, `compat-shadcn-rgb`. CSS. |
10
+ | `@vvedantb/ui/tokens/css/*.css` | `index`, `base`, `dialog`, `modal`, `tabs`, `tabs-pills`, `utilities`, `scrollbar-thin`, `clear-input`, `motion`, `animations`, `nav-icons`, `compat-shadcn-rgb`. CSS. `tokens.css` imports `scrollbar-thin`, `clear-input` and `motion`. |
11
11
  | `@vvedantb/ui/tokens/compat/shadcn-rgb.css` | Same file as `tokens/css/compat-shadcn-rgb.css`. CSS. |
12
12
  | `@vvedantb/ui/tokens/tailwind-preset` | Tailwind preset (`.mjs`) for `@config "@vvedantb/ui/tokens/tailwind-preset"`. Kept as a build-tool entry: Tailwind needs a resolvable package path, and it is not a React API. |
13
13
  | `@vvedantb/ui/landing.css` | Landing page classes (`landing-atmosphere`, `landing-grain`, `landing-pulse-dot`, ambient graph and preview animations, smooth `#top` scrolling). CSS. |
@@ -661,7 +661,7 @@ Rows in the panel beside the rail. Rail tiles are unchanged.
661
661
  | Focus ring | none | `focus-visible:ring-2 focus-visible:ring-foreground/35`, no outline |
662
662
  | Icon-only row | `justify-center px-0 py-1.5` | same, plus the radius, text, fill and focus classes above |
663
663
  | Icon (`sidebarNavIconClass`) | none (apps used 18px in a 20px box) | `size-[19px] shrink-0`; `text-muted-foreground` idle, `text-foreground` active |
664
- | Section label | `px-3 py-1 text-[11px]` | `px-4 py-1 text-[11px] leading-4` (16px line). Colour still comes from the app's `.sidebar-section-label` rule. |
664
+ | Section label | `px-3 py-1 text-[11px]` | `px-4 py-1 text-[11px] leading-4` (16px line). Colour comes from `.sidebar-section-label` in `tokens.css` (0.15.0; `--muted` at `--sidebar-section-label-mix` 55%, 80% on hover). |
665
665
 
666
666
  The hover fill is static: it does not slide between rows. While another row is hovered, the active row keeps its fill.
667
667
 
@@ -1059,9 +1059,9 @@ Use this when the app defines shadcn-style variables as space-separated channels
1059
1059
  @source "../node_modules/@vvedantb/ui/src/**/*.{ts,tsx}";
1060
1060
  ```
1061
1061
 
1062
- Shared CSS that is safe without `tokens.css`: `compat-shadcn-rgb.css`, `dialog.css`, `utilities.css`, `scrollbar-thin.css` (reads `--scrollbar`, which the compat file defines), `modal.css` and `tabs.css` (both need the app motion variables listed in `tokens/README.md`), and `animations.css` (not with the Tailwind preset). `base.css` and `tabs-pills.css` read raw `--accent`, `--foreground`, `--muted`, `--segment` and `--default`; skip them unless the app defines those as full colour values.
1062
+ Shared CSS that is safe without `tokens.css`: `compat-shadcn-rgb.css`, `dialog.css`, `utilities.css`, `scrollbar-thin.css` (reads `--scrollbar`, which the compat file defines), `clear-input.css` and `motion.css` (import them yourself; `tokens.css` is what imports them otherwise), `modal.css` and `tabs.css` (both need the motion variables listed in `tokens/README.md` on the app `:root`, because `tokens.css` sets them), and `animations.css` (not with the Tailwind preset). `base.css` and `tabs-pills.css` read raw `--accent`, `--foreground`, `--muted`, `--segment` and `--default`; skip them unless the app defines those as full colour values.
1063
1063
 
1064
- The compat file includes a fallback `glass-panel-strong` (`rgb(var(--card) / 0.95)` with a 12px blur) for toasts and dialogs. Declare your own `@utility glass-panel-strong` after the import to override it.
1064
+ The compat file defines `.glass-panel-strong` for toasts, dialogs and hover cards as the solid popover colour (`rgb(var(--popover))`, as `bg-overlay`), the same as `tokens.css`. Before 0.15.0 it was `rgb(var(--card) / 0.95)` with a 12px blur. The rule is in `@layer components`, so an app `.glass-panel-strong` rule after the import wins.
1065
1065
 
1066
1066
  A fixture that renders the main components against sample light and dark RGB variables is in `tokens/examples/compat-shadcn-rgb/`.
1067
1067
 
package/app/README.md CHANGED
@@ -160,7 +160,7 @@ import { PageMotionProvider, useDisablePageMotion } from "@vvedantb/ui";
160
160
  </PageMotionProvider>;
161
161
  ```
162
162
 
163
- - Sets `html[data-page-motion]` to `"off"` or `"on"` in a layout effect (CSS can skip page and route transitions on `"off"`). The attribute stays after unmount.
163
+ - Sets `html[data-page-motion]` to `"off"` or `"on"` in a layout effect. The attribute stays after unmount. From 0.15.0 `tokens.css` (through `tokens/css/motion.css`) ships the `"off"` rules: `.ui-page-enter`, `.ui-enter`, `.ui-enter-up` and `.ui-stagger` fade only, `animate-in` / `animate-out`, `.t-modal` and `.t-tabs-pill` stop transitioning, and view transitions stop.
164
164
  - Renders `motionConfig` with `reducedMotion` `"always"` when disabled, else `"never"`.
165
165
  - `useDisablePageMotion()`: the `disabled` value, `false` outside the provider.
166
166
  - This module does not import `motion`. Pass `MotionConfig`, or an app component that wraps it, defined at module level, for example with `LazyMotion`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vvedantb/ui",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "license": "MIT",
5
5
  "description": "Shared React 19 UI for web apps: one JavaScript entry (primitives, shell pieces, app helpers including the Convex and Clerk provider, VITE env parsing and ensure-user, artifact viewers over the @vvedantb/server artifacts core, native tokens), design-token CSS, shared Tailwind v4 CSS and a Tailwind preset.",
6
6
  "type": "module",
@@ -27,6 +27,8 @@
27
27
  "./tokens/css/tabs-pills.css": "./tokens/src/css/tabs-pills.css",
28
28
  "./tokens/css/utilities.css": "./tokens/src/css/utilities.css",
29
29
  "./tokens/css/scrollbar-thin.css": "./tokens/src/css/scrollbar-thin.css",
30
+ "./tokens/css/clear-input.css": "./tokens/src/css/clear-input.css",
31
+ "./tokens/css/motion.css": "./tokens/src/css/motion.css",
30
32
  "./tokens/css/animations.css": "./tokens/src/css/animations.css",
31
33
  "./tokens/css/nav-icons.css": "./tokens/src/css/nav-icons.css",
32
34
  "./tokens/css/compat-shadcn-rgb.css": "./tokens/src/css/compat-shadcn-rgb.css",
@@ -21,8 +21,8 @@ export const railBadgeClass = "absolute -bottom-0.5 -right-0.5 flex h-4 min-w-4
21
21
 
22
22
  /**
23
23
  * Static section label above a group of panel nav rows. `px-4` lines the text
24
- * up with the nav row icons; the colour comes from the app's
25
- * `.sidebar-section-label` rule.
24
+ * up with the nav row icons; the colour comes from `.sidebar-section-label` in
25
+ * tokens.css.
26
26
  */
27
27
  export const sidebarSectionLabelClass =
28
28
  "sidebar-section-label px-4 py-1 text-[11px] leading-4 font-medium tracking-[-0.01em]";
@@ -130,25 +130,25 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
130
130
  )}
131
131
  {...props}
132
132
  />
133
- <div
134
- ref={mirrorRef}
135
- className={cn(
136
- "t-clear-mirror border-0 bg-transparent text-foreground shadow-none",
137
- fieldClassName,
138
- )}
139
- aria-hidden="true"
140
- />
141
- <div
142
- ref={placeholderRef}
143
- className={cn(
144
- "t-clear-placeholder border-0 bg-transparent text-field-placeholder shadow-none",
145
- fieldClassName,
146
- )}
147
- aria-hidden="true"
148
- >
149
- {placeholder}
133
+ <div className="t-clear-clip" aria-hidden="true">
134
+ <div
135
+ ref={mirrorRef}
136
+ className={cn(
137
+ "t-clear-mirror border-0 bg-transparent text-foreground shadow-none",
138
+ fieldClassName,
139
+ )}
140
+ />
141
+ <div
142
+ ref={placeholderRef}
143
+ className={cn(
144
+ "t-clear-placeholder border-0 bg-transparent text-field-placeholder shadow-none",
145
+ fieldClassName,
146
+ )}
147
+ >
148
+ {placeholder}
149
+ </div>
150
+ <div ref={glowRef} className="t-clear-glow" />
150
151
  </div>
151
- <div ref={glowRef} className="t-clear-glow" aria-hidden="true" />
152
152
  {value.length > 0 && !clearing ? (
153
153
  <button
154
154
  type="button"
@@ -1,19 +1,24 @@
1
1
  /** transitions.dev input clear with dissolve — see 13-input-clear-dissolve.md */
2
2
 
3
- function readCssNumber(
3
+ // Times come back in ms. CSS minifiers rewrite `1000ms` as `1s`, so a value
4
+ // in seconds is converted rather than read as 1.
5
+ export function readCssNumber(
4
6
  root: HTMLElement,
5
7
  name: string,
6
8
  fallback: number,
7
9
  ): number {
8
- const raw = getComputedStyle(root).getPropertyValue(name);
10
+ const raw = getComputedStyle(root).getPropertyValue(name).trim();
9
11
  const parsed = parseFloat(raw);
10
- return Number.isFinite(parsed) ? parsed : fallback;
12
+ if (!Number.isFinite(parsed)) {
13
+ return fallback;
14
+ }
15
+ return /\ds$/.test(raw) ? parsed * 1000 : parsed;
11
16
  }
12
17
 
13
- function readCssEase(root: HTMLElement, name: string): (t: number) => number {
18
+ export function readCssEase(root: HTMLElement, name: string): (t: number) => number {
14
19
  const raw = getComputedStyle(root).getPropertyValue(name);
15
20
  const match = raw.match(
16
- /cubic-bezier\(([-\d.]+),([-\d.]+),([-\d.]+),([-\d.]+)\)/,
21
+ /cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/,
17
22
  );
18
23
  if (!match) {
19
24
  return (t) => t;
package/src/ui/dialog.tsx CHANGED
@@ -213,7 +213,7 @@ DialogBody.displayName = "DialogBody";
213
213
 
214
214
  // no fill of its own: the footer is not sticky, so it shows the content's
215
215
  // surface. a token here (it was `bg-overlay`) showed as an inset block when
216
- // that surface differs (`glass-panel-strong` from the app, or `bg-*` on the
216
+ // that surface differs (an app's own `glass-panel-strong`, or `bg-*` on the
217
217
  // content)
218
218
  const DialogFooter = ({
219
219
  className,
package/tokens/README.md CHANGED
@@ -6,18 +6,20 @@ Design tokens and shared Tailwind v4 CSS for the vvedantb web apps. The CSS and
6
6
 
7
7
  | Export | File | What it is |
8
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, the radius utilities `rounded-surface` (`clamp(0.75rem, var(--radius), 1rem)`), `rounded-menu-item` (`min(var(--radius), 0.5rem)`) and `rounded-control` (`min(var(--radius), 0.625rem)`) (0.7.1; `rounded-surface` was in `utilities.css` before), `*` border and `body` defaults. 0.7.2 adds `--sidebar-accent` / `sidebar-accent` (sidebar nav row fill: light `var(--surface-tertiary)`, dark `oklch(22.64% 0 0)`). 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. |
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, the radius utilities `rounded-surface` (`clamp(0.75rem, var(--radius), 1rem)`), `rounded-menu-item` (`min(var(--radius), 0.5rem)`) and `rounded-control` (`min(var(--radius), 0.625rem)`) (0.7.1; `rounded-surface` was in `utilities.css` before), `*` border and `body` defaults. 0.7.2 adds `--sidebar-accent` / `sidebar-accent` (sidebar nav row fill: light `var(--surface-tertiary)`, dark `oklch(22.64% 0 0)`). 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. From 0.15.0 it imports `clear-input.css`, `scrollbar-thin.css` and `motion.css`, sets the `--modal-*` and `--tabs-*` motion variables on `:root`, and defines `.glass-panel-strong` (`var(--overlay)`) and `.sidebar-section-label`. |
10
10
  | `tokens` and `Tokens` from `@vvedantb/ui` | `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
11
  | `@vvedantb/ui/tokens/css/base.css` | `src/css/base.css` | `html` touch/accent, `::selection`, control transitions, pointer cursor, range accent (`@layer base`). |
12
12
  | `@vvedantb/ui/tokens/css/dialog.css` | `src/css/dialog.css` | Unlayered `max-width: none` for `@vvedantb/ui` compact (bottom-sheet) dialogs. |
13
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. |
14
+ | `@vvedantb/ui/tokens/css/tabs.css` | `src/css/tabs.css` | `.t-tabs` container (0.15.0), `.t-tab` with its press scale, active colour, `.t-tabs-pill`, small-screen `.t-tabs` scrolling. |
15
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
16
  | `@vvedantb/ui/tokens/css/utilities.css` | `src/css/utilities.css` | `@utility hit-target` and `@utility reveal-on-hover`. `rounded-surface` moved to `tokens.css` in 0.7.1. |
17
- | `@vvedantb/ui/tokens/css/scrollbar-thin.css` | `src/css/scrollbar-thin.css` | `.scrollbar-thin` (`@layer utilities`, always emitted). |
17
+ | `@vvedantb/ui/tokens/css/scrollbar-thin.css` | `src/css/scrollbar-thin.css` | `.scrollbar-thin` (`@layer utilities`, always emitted). Imported by `tokens.css` from 0.15.0. |
18
+ | `@vvedantb/ui/tokens/css/clear-input.css` | `src/css/clear-input.css` | `ClearInput` layers (`.t-clear*`) and the `--clear-*` / `--glow-*` timings. Imported by `tokens.css`. From 0.15.1 the layers clip in `.t-clear-clip`, so `.t-clear` keeps the focus ring, and the placeholder layer only shows during `.is-clearing`. |
19
+ | `@vvedantb/ui/tokens/css/motion.css` | `src/css/motion.css` | `.ui-page-enter`, `.ui-enter`, `.ui-enter-up`, `.ui-stagger`, their keyframes and reduced-motion rules, and the `html[data-page-motion="off"]` overrides for `PageMotionProvider`. Imported by `tokens.css`. |
18
20
  | `@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`, the same three radius utilities as `tokens.css` 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/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`, the same three radius utilities as `tokens.css` and `.glass-panel-strong` (solid `--popover` from 0.15.0). Import it **or** `tokens.css`, never both. |
22
+ | `@vvedantb/ui/tokens/css/index.css` | `src/css/index.css` | base + dialog + modal + tabs + tabs-pills + utilities + animations + nav-icons, for new CSS-first apps (`tokens.css` brings scrollbar-thin). |
21
23
  | `@vvedantb/ui/tokens/tailwind-preset` | `src/tailwind-preset.mjs` (+ `.d.mts`) | Tailwind preset for apps that load a JS config with `@config`. |
22
24
 
23
25
  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.
@@ -35,6 +37,7 @@ CSS-first app (no `tailwind.config`):
35
37
  @import "@vvedantb/ui/tokens/css/modal.css";
36
38
  @import "@vvedantb/ui/tokens/css/tabs.css";
37
39
  /* ...app @layer components rules... */
40
+ /* no --modal-* / --tabs-* needed: tokens.css sets them */
38
41
  ```
39
42
 
40
43
  App with a JS config (`@config "../tailwind.config.ts"`):
@@ -65,17 +68,18 @@ Tailwind inlines an `@import` where it sits, even in the middle of a file (it mu
65
68
  | --- | --- | --- |
66
69
  | `base.css` | `--accent`, `--foreground` | tokens.css or the app theme |
67
70
  | `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 |
71
+ | `modal.css` | `--modal-open-dur`, `--modal-close-dur`, `--modal-scale`, `--modal-scale-close`, `--modal-ease` | tokens.css (0.15.0), or app `:root` with the compat file |
72
+ | `tabs.css` | `--tabs-dur`, `--tabs-ease` (tokens.css, or app `:root` with the compat file); `--tabs-text-muted`, `--tabs-text-active`, `--tabs-pill-bg` | colours: tabs-pills.css or the app theme |
70
73
  | `tabs-pills.css` | `--muted`, `--segment`, `--segment-foreground`, `--default` | tokens.css or the app theme |
71
74
  | `utilities.css` | `--breakpoint-sm` (Tailwind default or app theme) | Tailwind |
72
75
  | `scrollbar-thin.css` | `--scrollbar` | tokens.css or the app theme |
76
+ | `clear-input.css`, `motion.css` | none (own `:root` defaults) | |
73
77
  | `animations.css` | none | |
74
78
  | `tailwind-preset` | the CSS variables named in its colour map, `--radius`, `--field-radius` | tokens.css or the app theme |
75
79
 
76
80
  ### Using an existing shadcn RGB theme
77
81
 
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.
82
+ See the package `README.md`. With the compat file, `base.css`, `scrollbar-thin.css`, `clear-input.css`, `motion.css`, `utilities.css`, `dialog.css`, `modal.css` and `tabs.css` need no `tokens.css` (`modal.css` and `tabs.css` then need the `--modal-*` / `--tabs-*` values on the app `:root`). `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
83
 
80
84
  ### Parameters
81
85
 
@@ -86,6 +90,24 @@ Where apps share a rule but drifted in one value, the value is a custom property
86
90
  | `base.css` | `--tokens-control-transition-property` | `background-color, border-color, color, box-shadow, transform, opacity` | app-c: `background-color, color, transform, opacity` |
87
91
  | `base.css` | `--tokens-control-transition-duration` | `240ms` | app-c: `100ms` |
88
92
 
93
+ From 0.15.0 the files that moved out of the apps take `:root` variables too. Every rule is in a Tailwind layer, so an app rule for the same selector (unlayered, or later in the same layer) also wins.
94
+
95
+ | File | Property | Default |
96
+ | --- | --- | --- |
97
+ | `tokens.css` | `--modal-open-dur`, `--modal-close-dur` | `180ms`, `120ms` |
98
+ | `tokens.css` | `--modal-scale`, `--modal-scale-close` | `0.96`, `0.96` |
99
+ | `tokens.css` | `--modal-ease`, `--tabs-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` |
100
+ | `tokens.css` | `--tabs-dur` | `200ms` |
101
+ | `tokens.css` | `--sidebar-section-label-mix`, `--sidebar-section-label-hover-mix` | `55%`, `80%` (share of `--muted`) |
102
+ | `scrollbar-thin.css` | `--scrollbar-thin-rest`, `--scrollbar-thin-hover` | `40%`, `80%` (share of `--scrollbar`) |
103
+ | `clear-input.css` | `--clear-dur`, `--clear-out-dur`, `--clear-in-dur` | `1000ms`, `400ms`, `400ms` |
104
+ | `clear-input.css` | `--clear-out-fly`, `--clear-in-fly` | `12px`, `12px` |
105
+ | `clear-input.css` | `--clear-out-ease`, `--clear-in-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` |
106
+ | `clear-input.css` | `--glow-delay`, `--glow-peak-at`, `--glow-opacity`, `--glow-spread` | `50ms`, `0.15`, `0.85`, `1.5` |
107
+ | `motion.css` | `--ui-enter-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` |
108
+ | `motion.css` | `--ui-page-enter-dur`, `--ui-enter-dur`, `--ui-enter-up-dur` | `160ms`, `200ms`, `220ms` |
109
+ | `motion.css` | `--ui-stagger-step` | `40ms` (children 1 to 8) |
110
+
89
111
  ## Develop
90
112
 
91
113
  ```sh
@@ -0,0 +1,82 @@
1
+ /*
2
+ * @vvedantb/ui/tokens/css/clear-input.css: layers for @vvedantb/ui
3
+ * ClearInput (`.t-clear`, clip, mirror, placeholder and glow), the transitions.dev
4
+ * input clear with dissolve. tokens.css imports it.
5
+ *
6
+ * clearInputDissolve reads the variables below from `html`. Set any of them
7
+ * on the app `:root` to change the timing (numbers in ms or px):
8
+ * --clear-dur, --clear-out-dur, --clear-in-dur, --clear-out-fly,
9
+ * --clear-in-fly, --clear-out-ease, --clear-in-ease, --glow-delay,
10
+ * --glow-peak-at, --glow-opacity, --glow-spread
11
+ */
12
+
13
+ @layer base {
14
+ :root {
15
+ --clear-dur: 1000ms;
16
+ --clear-out-dur: 400ms;
17
+ --clear-in-dur: 400ms;
18
+ --clear-out-fly: 12px;
19
+ --clear-in-fly: 12px;
20
+ --clear-out-ease: cubic-bezier(0.22, 1, 0.36, 1);
21
+ --clear-in-ease: cubic-bezier(0.22, 1, 0.36, 1);
22
+ --glow-delay: 50ms;
23
+ --glow-peak-at: 0.15;
24
+ --glow-opacity: 0.85;
25
+ --glow-spread: 1.5;
26
+ }
27
+ }
28
+
29
+ @layer components {
30
+ /* No overflow clip on .t-clear: it would cut the input's focus ring.
31
+ .t-clear-clip clips the layers to the field instead. It never moves, so
32
+ the mirror and placeholder can fly in and out under it. Its radius
33
+ follows .t-clear; put a rounded-* class on wrapperClassName to match an
34
+ input with another radius. */
35
+ .t-clear {
36
+ position: relative;
37
+ border-radius: var(--radius-field, 0);
38
+ }
39
+ .t-clear-clip,
40
+ .t-clear-mirror,
41
+ .t-clear-placeholder,
42
+ .t-clear-glow {
43
+ position: absolute;
44
+ inset: 0;
45
+ pointer-events: none;
46
+ overflow: hidden;
47
+ }
48
+ .t-clear-clip {
49
+ border-radius: inherit;
50
+ }
51
+ .t-clear-mirror,
52
+ .t-clear-placeholder {
53
+ display: flex;
54
+ align-items: center;
55
+ white-space: nowrap;
56
+ opacity: 0;
57
+ z-index: 2;
58
+ }
59
+ /* The input's own placeholder shows at rest. The layer only shows while
60
+ clearInputDissolve drops it in, and the native one hides meanwhile. */
61
+ .t-clear.has-value .t-clear-mirror,
62
+ .t-clear.is-clearing .t-clear-mirror,
63
+ .t-clear.is-clearing .t-clear-placeholder {
64
+ opacity: 1;
65
+ }
66
+ .t-clear.is-clearing > input::placeholder {
67
+ opacity: 0;
68
+ }
69
+ .t-clear.has-value > input,
70
+ .t-clear.is-clearing > input {
71
+ -webkit-text-fill-color: transparent;
72
+ }
73
+ .t-clear-glow {
74
+ opacity: 0;
75
+ z-index: 3;
76
+ mix-blend-mode: multiply;
77
+ }
78
+ .dark .t-clear-glow,
79
+ [data-theme="dark"] .t-clear-glow {
80
+ mix-blend-mode: screen;
81
+ }
82
+ }
@@ -89,12 +89,13 @@
89
89
  }
90
90
 
91
91
  /*
92
- * Fallback fill for toasts, dialogs and hover cards (`glass-panel-strong`).
93
- * Equivalent to `bg-card/95 backdrop-blur-md`. An app may override it by
94
- * declaring its own `@utility glass-panel-strong` after this import.
92
+ * Fill for toasts, dialogs and hover cards (`glass-panel-strong`): the solid
93
+ * popover colour, as `bg-overlay`, matching tokens.css. Any app
94
+ * `.glass-panel-strong` rule (unlayered, or in @layer components after this
95
+ * import) wins.
95
96
  */
96
- @utility glass-panel-strong {
97
- background-color: rgb(var(--card) / 0.95);
98
- -webkit-backdrop-filter: blur(12px);
99
- backdrop-filter: blur(12px);
97
+ @layer components {
98
+ .glass-panel-strong {
99
+ background-color: rgb(var(--popover));
100
+ }
100
101
  }
@@ -6,15 +6,18 @@
6
6
  * @import "@vvedantb/ui/tokens";
7
7
  * @import "@vvedantb/ui/tokens/css/index.css";
8
8
  *
9
- * modal.css, tabs.css and tabs-pills.css need app motion variables
10
- * (--modal-*, --tabs-dur, --tabs-ease) and are not included. Apps that use
11
- * the tailwind preset must not import animations.css; import the other
12
- * files one by one instead. nav-icons.css styles the animated nav icons.
9
+ * tokens.css already brings scrollbar-thin.css, clear-input.css and
10
+ * motion.css, and the --modal-* / --tabs-* variables modal.css and tabs.css
11
+ * read. Apps that use the tailwind preset must not import animations.css;
12
+ * import the other files one by one instead. nav-icons.css styles the
13
+ * animated nav icons.
13
14
  */
14
15
 
15
16
  @import "./base.css";
16
17
  @import "./dialog.css";
18
+ @import "./modal.css";
19
+ @import "./tabs.css";
20
+ @import "./tabs-pills.css";
17
21
  @import "./utilities.css";
18
- @import "./scrollbar-thin.css";
19
22
  @import "./animations.css";
20
23
  @import "./nav-icons.css";
@@ -2,8 +2,9 @@
2
2
  * @vvedantb/ui/tokens/css/modal.css: .t-modal open / close transition used by
3
3
  * @vvedantb/ui Dialog (transitions.dev pattern).
4
4
  *
5
- * Needs on :root: --modal-open-dur, --modal-close-dur, --modal-scale,
6
- * --modal-scale-close, --modal-ease.
5
+ * Reads --modal-open-dur, --modal-close-dur, --modal-scale,
6
+ * --modal-scale-close and --modal-ease. tokens.css sets them on :root; set
7
+ * them on the app :root to change them (apps on compat-shadcn-rgb.css must).
7
8
  */
8
9
 
9
10
  @layer components {
@@ -0,0 +1,156 @@
1
+ /*
2
+ * @vvedantb/ui/tokens/css/motion.css: page and list enter animations
3
+ * (`.ui-page-enter`, `.ui-enter`, `.ui-enter-up`, `.ui-stagger`) and the
4
+ * overrides for PageMotionProvider's `html[data-page-motion="off"]`.
5
+ * tokens.css imports it. Keyframe names (`ui-*`) do not clash with
6
+ * animations.css or the tailwind preset.
7
+ *
8
+ * Set on :root to change (defaults shown):
9
+ * --ui-enter-ease: cubic-bezier(0.22, 1, 0.36, 1)
10
+ * --ui-page-enter-dur: 160ms
11
+ * --ui-enter-dur: 200ms
12
+ * --ui-enter-up-dur: 220ms
13
+ * --ui-stagger-step: 40ms (delay between the first 8 children)
14
+ *
15
+ * The app keeps its own view transition setup (names, timings, morphs); this
16
+ * file only turns those animations off for reduced motion and page motion off.
17
+ */
18
+
19
+ @layer base {
20
+ :root {
21
+ --ui-enter-ease: cubic-bezier(0.22, 1, 0.36, 1);
22
+ --ui-page-enter-dur: 160ms;
23
+ --ui-enter-dur: 200ms;
24
+ --ui-enter-up-dur: 220ms;
25
+ --ui-stagger-step: 40ms;
26
+ }
27
+ }
28
+
29
+ @layer components {
30
+ .ui-page-enter {
31
+ animation: ui-page-enter var(--ui-page-enter-dur) var(--ui-enter-ease)
32
+ backwards;
33
+ }
34
+
35
+ .ui-enter,
36
+ .ui-stagger > * {
37
+ animation: ui-enter var(--ui-enter-dur) var(--ui-enter-ease) backwards;
38
+ }
39
+
40
+ .ui-enter-up {
41
+ animation: ui-enter-up var(--ui-enter-up-dur) var(--ui-enter-ease)
42
+ backwards;
43
+ }
44
+
45
+ .ui-stagger > *:nth-child(2) {
46
+ animation-delay: calc(var(--ui-stagger-step) * 1);
47
+ }
48
+ .ui-stagger > *:nth-child(3) {
49
+ animation-delay: calc(var(--ui-stagger-step) * 2);
50
+ }
51
+ .ui-stagger > *:nth-child(4) {
52
+ animation-delay: calc(var(--ui-stagger-step) * 3);
53
+ }
54
+ .ui-stagger > *:nth-child(5) {
55
+ animation-delay: calc(var(--ui-stagger-step) * 4);
56
+ }
57
+ .ui-stagger > *:nth-child(6) {
58
+ animation-delay: calc(var(--ui-stagger-step) * 5);
59
+ }
60
+ .ui-stagger > *:nth-child(7) {
61
+ animation-delay: calc(var(--ui-stagger-step) * 6);
62
+ }
63
+ .ui-stagger > *:nth-child(8) {
64
+ animation-delay: calc(var(--ui-stagger-step) * 7);
65
+ }
66
+
67
+ @keyframes ui-page-enter {
68
+ from {
69
+ opacity: 0;
70
+ transform: translateY(4px);
71
+ }
72
+ to {
73
+ opacity: 1;
74
+ transform: translateY(0);
75
+ }
76
+ }
77
+
78
+ @keyframes ui-enter {
79
+ from {
80
+ opacity: 0;
81
+ transform: translateY(6px);
82
+ }
83
+ to {
84
+ opacity: 1;
85
+ transform: translateY(0);
86
+ }
87
+ }
88
+
89
+ @keyframes ui-enter-up {
90
+ from {
91
+ opacity: 0;
92
+ transform: translateY(12px);
93
+ }
94
+ to {
95
+ opacity: 1;
96
+ transform: translateY(0);
97
+ }
98
+ }
99
+
100
+ @keyframes ui-fade-only {
101
+ from {
102
+ opacity: 0;
103
+ }
104
+ to {
105
+ opacity: 1;
106
+ }
107
+ }
108
+
109
+ @media (prefers-reduced-motion: reduce) {
110
+ .ui-page-enter,
111
+ .ui-enter,
112
+ .ui-enter-up,
113
+ .ui-stagger > * {
114
+ animation-duration: 1ms;
115
+ animation-name: ui-fade-only;
116
+ }
117
+
118
+ ::view-transition-group(*),
119
+ ::view-transition-old(*),
120
+ ::view-transition-new(*) {
121
+ animation: none !important;
122
+ }
123
+ }
124
+
125
+ /*
126
+ * PageMotionProvider `disabled` sets `html[data-page-motion="off"]`. Page and
127
+ * route enters, list stagger, `animate-in` / `animate-out`, the modal and
128
+ * tab pill transitions and view transitions stop. Spinners keep running and
129
+ * the modal still reaches its open state, so dialogs still appear.
130
+ */
131
+ html[data-page-motion="off"] .ui-page-enter,
132
+ html[data-page-motion="off"] .ui-enter,
133
+ html[data-page-motion="off"] .ui-enter-up,
134
+ html[data-page-motion="off"] .ui-stagger > * {
135
+ animation-duration: 1ms;
136
+ animation-name: ui-fade-only;
137
+ }
138
+
139
+ html[data-page-motion="off"] .animate-in,
140
+ html[data-page-motion="off"] .animate-out {
141
+ animation-duration: 0s !important;
142
+ animation-delay: 0s !important;
143
+ }
144
+
145
+ html[data-page-motion="off"] .t-modal,
146
+ html[data-page-motion="off"] .t-modal.is-closing,
147
+ html[data-page-motion="off"] .t-tabs-pill {
148
+ transition-duration: 0s !important;
149
+ }
150
+
151
+ html[data-page-motion="off"]::view-transition-group(*),
152
+ html[data-page-motion="off"]::view-transition-old(*),
153
+ html[data-page-motion="off"]::view-transition-new(*) {
154
+ animation: none !important;
155
+ }
156
+ }
@@ -1,40 +1,68 @@
1
1
  /*
2
2
  * @vvedantb/ui/tokens/css/scrollbar-thin.css: `.scrollbar-thin` (always emitted,
3
- * plain @layer utilities). Needs --scrollbar.
3
+ * plain @layer utilities). Needs --scrollbar. tokens.css imports it; apps on
4
+ * compat-shadcn-rgb.css import it themselves.
4
5
  *
5
- * Standard scrollbar properties are ignored by Chrome 121+ when webkit rules
6
- * exist, so the Firefox fallback is gated with @supports. Chrome gets the
7
- * 6px thumb; Firefox gets thin themed.
6
+ * Standard properties first (Chrome 121+, Firefox). Chrome ignores webkit
7
+ * scrollbar rules once `scrollbar-color` is set, so the 6px webkit thumb is
8
+ * only a fallback for browsers without `scrollbar-color`.
9
+ *
10
+ * Thumb strength (share of --scrollbar), set on :root to change:
11
+ * --scrollbar-thin-rest: 40%
12
+ * --scrollbar-thin-hover: 80%
8
13
  */
9
14
 
10
- @layer utilities {
11
- @supports not selector(::-webkit-scrollbar) {
12
- .scrollbar-thin {
13
- scrollbar-width: thin;
14
- scrollbar-color: color-mix(in oklch, var(--scrollbar) 80%, transparent)
15
- transparent;
16
- }
17
-
18
- .scrollbar-thin:hover {
19
- scrollbar-color: color-mix(in oklch, var(--scrollbar) 100%, transparent)
20
- transparent;
21
- }
15
+ @layer base {
16
+ :root {
17
+ --scrollbar-thin-rest: 40%;
18
+ --scrollbar-thin-hover: 80%;
22
19
  }
20
+ }
23
21
 
24
- .scrollbar-thin::-webkit-scrollbar {
25
- width: 6px;
22
+ @layer utilities {
23
+ .scrollbar-thin {
24
+ scrollbar-width: thin;
25
+ scrollbar-color: color-mix(
26
+ in oklch,
27
+ var(--scrollbar) var(--scrollbar-thin-rest),
28
+ transparent
29
+ )
30
+ transparent;
26
31
  }
27
32
 
28
- .scrollbar-thin::-webkit-scrollbar-track {
29
- background: transparent;
33
+ .scrollbar-thin:hover {
34
+ scrollbar-color: color-mix(
35
+ in oklch,
36
+ var(--scrollbar) var(--scrollbar-thin-hover),
37
+ transparent
38
+ )
39
+ transparent;
30
40
  }
31
41
 
32
- .scrollbar-thin::-webkit-scrollbar-thumb {
33
- background-color: color-mix(in oklch, var(--scrollbar) 80%, transparent);
34
- border-radius: 3px;
35
- }
42
+ @supports not (scrollbar-color: auto) {
43
+ .scrollbar-thin::-webkit-scrollbar {
44
+ width: 6px;
45
+ }
46
+
47
+ .scrollbar-thin::-webkit-scrollbar-track {
48
+ background: transparent;
49
+ }
36
50
 
37
- .scrollbar-thin::-webkit-scrollbar-thumb:hover {
38
- background-color: color-mix(in oklch, var(--scrollbar) 100%, transparent);
51
+ .scrollbar-thin::-webkit-scrollbar-thumb {
52
+ background-color: color-mix(
53
+ in oklch,
54
+ var(--scrollbar) var(--scrollbar-thin-rest),
55
+ transparent
56
+ );
57
+ border-radius: 3px;
58
+ }
59
+
60
+ .scrollbar-thin:hover::-webkit-scrollbar-thumb {
61
+ background-color: color-mix(
62
+ in oklch,
63
+ var(--scrollbar) var(--scrollbar-thin-hover),
64
+ transparent
65
+ );
66
+ }
39
67
  }
40
68
  }
@@ -1,13 +1,19 @@
1
1
  /*
2
2
  * @vvedantb/ui/tokens/css/tabs.css: sliding tab bar (.t-tab, .t-tabs-pill)
3
- * used by @vvedantb/ui tabsSliding (transitions.dev pattern). The .t-tabs
4
- * container rule stays in the app; it differs per app.
3
+ * used by @vvedantb/ui tabsSliding (transitions.dev pattern), and the .t-tabs
4
+ * container. An app rule for any of them wins (@layer components).
5
5
  *
6
- * Needs: --tabs-dur, --tabs-ease, --tabs-text-muted, --tabs-text-active,
7
- * --tabs-pill-bg. tabs-pills.css sets the three colour variables.
6
+ * Needs: --tabs-dur, --tabs-ease (defaults in tokens.css), --tabs-text-muted,
7
+ * --tabs-text-active, --tabs-pill-bg. tabs-pills.css sets the three colour
8
+ * variables.
8
9
  */
9
10
 
10
11
  @layer components {
12
+ .t-tabs {
13
+ position: relative;
14
+ display: inline-flex;
15
+ align-items: center;
16
+ }
11
17
  @media (max-width: 639px) {
12
18
  .t-tabs {
13
19
  justify-content: safe center;
@@ -29,7 +35,12 @@
29
35
  cursor: pointer;
30
36
  border-radius: 48px;
31
37
  z-index: 1;
32
- transition: color var(--tabs-dur) var(--tabs-ease);
38
+ transition:
39
+ color var(--tabs-dur) var(--tabs-ease),
40
+ transform 100ms ease-out;
41
+ }
42
+ .t-tab:active {
43
+ transform: scale(0.97);
33
44
  }
34
45
  .t-tab:not([aria-selected="true"]):not([data-state="active"]):hover,
35
46
  .t-tab[aria-selected="true"],
@@ -16,8 +16,19 @@
16
16
  * @import url("https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&display=swap");
17
17
  *
18
18
  * Without it, --font-sans falls back to system-ui.
19
+ *
20
+ * From 0.15.0 this file also brings, on by default: the ClearInput layers,
21
+ * `.scrollbar-thin`, the `.ui-enter` family with the PageMotionProvider
22
+ * overrides, `.glass-panel-strong`, `.sidebar-section-label` and the
23
+ * modal / tabs motion variables. Every rule sits in a Tailwind layer and the
24
+ * timings and strengths are `:root` variables, so app rules and app `:root`
25
+ * values win.
19
26
  */
20
27
 
28
+ @import "./css/clear-input.css";
29
+ @import "./css/scrollbar-thin.css";
30
+ @import "./css/motion.css";
31
+
21
32
  @custom-variant dark (&:where(.dark, .dark *));
22
33
 
23
34
  @theme {
@@ -226,3 +237,44 @@
226
237
  -moz-osx-font-smoothing: grayscale;
227
238
  }
228
239
  }
240
+
241
+ /* Motion for css/modal.css (`.t-modal`) and css/tabs.css (`.t-tab`,
242
+ `.t-tabs-pill`). Set any of them on the app `:root` to change it. */
243
+ @layer base {
244
+ :root {
245
+ --modal-open-dur: 180ms;
246
+ --modal-close-dur: 120ms;
247
+ --modal-scale: 0.96;
248
+ --modal-scale-close: 0.96;
249
+ --modal-ease: cubic-bezier(0.22, 1, 0.36, 1);
250
+ --tabs-dur: 200ms;
251
+ --tabs-ease: cubic-bezier(0.22, 1, 0.36, 1);
252
+ --sidebar-section-label-mix: 55%;
253
+ --sidebar-section-label-hover-mix: 80%;
254
+ }
255
+ }
256
+
257
+ @layer components {
258
+ /* Solid overlay fill for Dialog, SonnerToaster, SlideOverPanel and
259
+ SessionsHoverCard `surface="glass"`. Same as `bg-overlay`. */
260
+ .glass-panel-strong {
261
+ background-color: var(--overlay);
262
+ }
263
+
264
+ /* Shell section label (`sidebarSectionLabelClass`). */
265
+ .sidebar-section-label {
266
+ color: color-mix(
267
+ in oklch,
268
+ var(--muted) var(--sidebar-section-label-mix),
269
+ transparent
270
+ );
271
+ }
272
+
273
+ .sidebar-section-label:hover {
274
+ color: color-mix(
275
+ in oklch,
276
+ var(--muted) var(--sidebar-section-label-hover-mix),
277
+ transparent
278
+ );
279
+ }
280
+ }