@vvedantb/ui 0.32.0 → 0.34.0

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
@@ -212,7 +212,10 @@ import { SessionsHoverCard } from "@vvedantb/ui";
212
212
  ## Panels, search and confirm
213
213
 
214
214
  ```tsx
215
- import { ConfirmDialog, SearchInput, SlideOverPanel } from "@vvedantb/ui";
215
+ import {
216
+ ConfirmDialog, Drawer, DrawerContent, DrawerTitle, SearchInput, Sheet, SheetContent,
217
+ SheetTitle, SlideOverPanel,
218
+ } from "@vvedantb/ui";
216
219
 
217
220
  <div className="relative">
218
221
  <SlideOverPanel open={open} onClose={() => setOpen(false)} header={<h2>Details</h2>}>
@@ -220,6 +223,20 @@ import { ConfirmDialog, SearchInput, SlideOverPanel } from "@vvedantb/ui";
220
223
  </SlideOverPanel>
221
224
  </div>
222
225
 
226
+ <Sheet open={open} onOpenChange={setOpen} direction="right">
227
+ <SheetContent motionBlur surfaceClassName="p-4">
228
+ <SheetTitle>Details</SheetTitle>
229
+ Body
230
+ </SheetContent>
231
+ </Sheet>
232
+
233
+ {/* Drawer* names are aliases of Sheet* */}
234
+ <Drawer open={open} onOpenChange={setOpen}>
235
+ <DrawerContent motionBlur>
236
+ <DrawerTitle>Share</DrawerTitle>
237
+ </DrawerContent>
238
+ </Drawer>
239
+
223
240
  <SearchInput value={query} onChange={setQuery} label="Search files" shortcut="/" />
224
241
 
225
242
  <SearchInput variant="toolbar" value={query} onChange={setQuery} />
@@ -229,7 +246,8 @@ import { ConfirmDialog, SearchInput, SlideOverPanel } from "@vvedantb/ui";
229
246
  <ConfirmDialog size="md" open={open} onOpenChange={setOpen} title="Delete item?" description="This cannot be undone." confirmLabel="Delete" variant="destructive" onConfirm={remove} />
230
247
  ```
231
248
 
232
- - `SlideOverPanel`: a right-side panel, absolutely positioned inside its nearest positioned parent. It is not a portal or dialog. It slides in with a spring and is hidden below `md`. Only the close button closes it (no Escape, no outside click). `width` is `"w-96"` (default) or `"w-80"`. `closeLabel` defaults to `"Close panel"`. `className` merges last.
249
+ - `Sheet` / `Drawer` (vaul): mobile sheet and side drawer. `SheetContent` / `DrawerContent` take `side` (defaults to the root `direction`), opt-in `motionBlur` (default `false`) and `surfaceClassName`. With `motionBlur`, `@web-kits/motion-blur` blurs the panel only while it slides or is dragged (Echo, 180° shutter; off under `prefers-reduced-motion`). The moving element stays transparent; an inner `data-slot="sheet-surface"` holds the fill, shadow, corners and scroll. `className` then styles the moving host; `surfaceClassName` styles the surface. Without `motionBlur`, the DOM matches the previous single-element surface (`surfaceClassName` merges onto it after `className`). `Drawer*` exports are aliases of the `Sheet*` parts.
250
+ - `SlideOverPanel`: a right-side panel, absolutely positioned inside its nearest positioned parent. It is not a portal or dialog. It slides in with a spring and is hidden below `md`. Only the close button closes it (no Escape, no outside click). `width` is `"w-96"` (default) or `"w-80"`. `closeLabel` defaults to `"Close panel"`. `className` merges last. Does not use `motionBlur`.
233
251
  - `SearchInput`: the search field. A `ClearInput` with a search icon on the left and the clear button on the right (shown while there is text; clearing calls `onChange("")`). `label` (default `"Search"`) is the `aria-label` and, without `placeholder`, the placeholder. `clearLabel` defaults to `"Clear search"`. `shortcut` (such as `"/"` or `"⌘K"`) shows a `Kbd` hint on the right while the value is empty. `size`: `"sm"` (`h-8`, default) or `"md"` (`h-10`). `variant`: `"default"` or `"toolbar"` (transparent until hover or focus, `rounded-field`, full width on mobile and `sm:w-44 md:w-52`). Other input props pass through; `enterKeyHint="search"`. `className` goes to the `<input>`, `wrapperClassName` to the wrapper, the ref to the `<input>`.
234
252
  - `ToolbarSearchInput` (deprecated in 0.17.0): a wrapper over `<SearchInput variant="toolbar">` with `placeholder="Search..."`. `className` goes to the wrapper, `inputClassName` to the `<input>`.
235
253
  - Field focus: every field (`Input`, `Textarea`, `Select` and `RadixSelect` triggers, `ClearInput`, `SearchInput`, `InputGroup`, date and time picker triggers) uses `fieldFocusClass`: the border turns `focus-border` and a 1px `ring-focus-ring` shows. Wrappers use `fieldFocusWithinClass` (`has-focus-visible:`). Both are exported for app fields.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vvedantb/ui",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "license": "MIT",
5
5
  "description": "Shared React 19 UI for web apps: one main 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), a client entry without @vvedantb/server, convex or Clerk for Chrome extensions, design-token CSS, shared Tailwind v4 CSS, a Tailwind preset and packaged Instrument Sans fonts.",
6
6
  "type": "module",
@@ -89,6 +89,7 @@
89
89
  "@dnd-kit/modifiers": "^9.0.0",
90
90
  "@dnd-kit/sortable": "^10.0.0",
91
91
  "@dnd-kit/utilities": "^3.2.2",
92
+ "@web-kits/motion-blur": "^0.1.0",
92
93
  "dayjs": "^1.11.13",
93
94
  "katex": "^0.16.45",
94
95
  "media-chrome": "^4.14.0",
package/src/ui/sheet.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
+ import { useMotionBlur } from "@web-kits/motion-blur/react";
4
5
  import { Drawer as DrawerPrimitive } from "vaul";
5
6
  import { cn } from "../utils/cn";
6
7
  import { assignRef } from "../utils/ref";
@@ -58,34 +59,132 @@ const SheetOverlay = React.forwardRef<
58
59
  ));
59
60
  SheetOverlay.displayName = "SheetOverlay";
60
61
 
61
- const sheetSideClass: Record<SheetSide, string> = {
62
+ // Position and size. Stays on the element vaul moves.
63
+ const sheetSideLayoutClass: Record<SheetSide, string> = {
62
64
  bottom:
63
- "inset-x-0 bottom-[var(--overlay-vv-bottom,0px)] mt-24 flex max-h-[min(96dvh,calc(var(--overlay-vv-height,100dvh)-1.5rem))] h-auto flex-col rounded-t-2xl pb-[max(0.75rem,env(safe-area-inset-bottom,0px))]",
64
- top: "inset-x-0 top-0 mb-24 flex h-auto max-h-[var(--overlay-vv-height,100dvh)] flex-col rounded-b-2xl pt-[max(0.75rem,env(safe-area-inset-top,0px))]",
65
- left: "inset-y-2 left-2 right-auto flex h-auto w-80 max-w-[88vw] flex-col rounded-2xl",
66
- right:
67
- "inset-y-2 left-auto right-2 flex h-auto w-80 max-w-[88vw] flex-col rounded-2xl",
65
+ "inset-x-0 bottom-[var(--overlay-vv-bottom,0px)] mt-24 flex max-h-[min(96dvh,calc(var(--overlay-vv-height,100dvh)-1.5rem))] h-auto flex-col",
66
+ top: "inset-x-0 top-0 mb-24 flex h-auto max-h-[var(--overlay-vv-height,100dvh)] flex-col",
67
+ left: "inset-y-2 left-2 right-auto flex h-auto w-80 max-w-[88vw] flex-col",
68
+ right: "inset-y-2 left-auto right-2 flex h-auto w-80 max-w-[88vw] flex-col",
68
69
  };
69
70
 
71
+ // Corners and safe-area padding. Go on the visible surface.
72
+ const sheetSideSurfaceClass: Record<SheetSide, string> = {
73
+ bottom: "rounded-t-2xl pb-[max(0.75rem,env(safe-area-inset-bottom,0px))]",
74
+ top: "rounded-b-2xl pt-[max(0.75rem,env(safe-area-inset-top,0px))]",
75
+ left: "rounded-2xl",
76
+ right: "rounded-2xl",
77
+ };
78
+
79
+ // Without `motionBlur`, one element is both the moving part and the surface.
80
+ const sheetContentClass =
81
+ "fixed z-50 overflow-y-auto bg-overlay/95 text-overlay-foreground smooth-shadow-ring-xl outline-hidden backdrop-blur-xl motion-reduce:transition-none";
82
+
83
+ // With `motionBlur`, the element vaul moves has to stay transparent, because the
84
+ // blur is drawn into its first child. It also stops clipping (no overflow), so
85
+ // the streak and the surface shadow can reach past its edges. `after:hidden`
86
+ // drops vaul's `::after` fill: it inherits the now transparent background, and
87
+ // the old `overflow-y-auto` clipped it anyway.
88
+ const sheetBlurHostClass =
89
+ "fixed z-50 outline-hidden motion-reduce:transition-none after:hidden";
90
+
91
+ // The visible surface inside it. It fills the host and is the scroll container.
92
+ const sheetBlurSurfaceClass =
93
+ "flex min-h-0 flex-auto flex-col overflow-y-auto bg-overlay/95 text-overlay-foreground smooth-shadow-ring-xl backdrop-blur-xl";
94
+
95
+ /**
96
+ * `@web-kits/motion-blur` reads transforms with `DOMMatrix` and the computed
97
+ * `translate`, polls `getAnimations()` and checks `matchMedia`, and throws
98
+ * without them. jsdom, happy-dom and browsers without the individual transform
99
+ * properties lack some of these, so the blur stays off there and the sheet
100
+ * still renders.
101
+ */
102
+ function supportsMotionBlur(node: HTMLElement): boolean {
103
+ return (
104
+ typeof DOMMatrix === "function" &&
105
+ typeof window.matchMedia === "function" &&
106
+ typeof node.getAnimations === "function" &&
107
+ typeof window.getComputedStyle(node).translate === "string"
108
+ );
109
+ }
110
+
111
+ const syncedEchoCopies = new WeakSet<Element>();
112
+
113
+ /**
114
+ * Echo draws copies of the surface next to it, and a fresh copy starts at
115
+ * scroll 0. Give each copy the surface's scroll once, so a scrolled sheet does
116
+ * not flash its top while it closes.
117
+ */
118
+ function syncEchoScroll(host: HTMLElement): void {
119
+ const surface = host.firstElementChild;
120
+ if (!(surface instanceof HTMLElement)) return;
121
+ for (let copy = surface.nextElementSibling; copy; copy = copy.nextElementSibling) {
122
+ if (syncedEchoCopies.has(copy) || copy.getAttribute("aria-hidden") !== "true") continue;
123
+ syncedEchoCopies.add(copy);
124
+ copy.scrollTop = surface.scrollTop;
125
+ copy.scrollLeft = surface.scrollLeft;
126
+ }
127
+ }
128
+
70
129
  const SheetContent = React.forwardRef<
71
130
  React.ComponentRef<typeof DrawerPrimitive.Content>,
72
131
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & {
73
132
  /** Defaults to the parent Sheet's `direction`. */
74
133
  side?: SheetSide;
134
+ /**
135
+ * Blur the sheet while it slides in, out or is dragged, with
136
+ * `@web-kits/motion-blur` (Echo, 180° shutter). Off under
137
+ * `prefers-reduced-motion`. When `true`, the content element moves and
138
+ * stays transparent, and an inner `data-slot="sheet-surface"` div holds the
139
+ * fill, shadow, corners and scroll. `className` then styles the moving
140
+ * element only (size, position); use `surfaceClassName` for the surface.
141
+ * Default `false`.
142
+ */
143
+ motionBlur?: boolean;
144
+ /**
145
+ * Classes for the visible surface: the inner div when `motionBlur` is on,
146
+ * otherwise the content element itself (after `className`).
147
+ */
148
+ surfaceClassName?: string;
75
149
  }
76
- >(({ className, children, side: sideProp, ...props }, ref) => {
150
+ >(({ className, children, side: sideProp, motionBlur = false, surfaceClassName, ...props }, ref) => {
77
151
  const direction = React.useContext(SheetDirectionContext);
78
152
  const side = sideProp ?? direction;
79
153
  const overlayCleanupRef = React.useRef<(() => void) | null>(null);
154
+ const blurCleanupRef = React.useRef<(() => void) | null>(null);
155
+ const hostRef = React.useRef<HTMLDivElement | null>(null);
156
+
157
+ const onBlurFrame = React.useCallback(() => {
158
+ if (hostRef.current) syncEchoScroll(hostRef.current);
159
+ }, []);
160
+ // Called on every render (hook rules); it does nothing until its ref is set.
161
+ const { ref: attachMotionBlur } = useMotionBlur({ onFrame: onBlurFrame });
162
+
163
+ const detachMotionBlur = React.useCallback(() => {
164
+ blurCleanupRef.current?.();
165
+ blurCleanupRef.current = null;
166
+ }, []);
167
+
168
+ const connectMotionBlur = React.useCallback(
169
+ (node: HTMLDivElement) => {
170
+ detachMotionBlur();
171
+ if (!supportsMotionBlur(node)) return;
172
+ blurCleanupRef.current = attachMotionBlur(node) ?? null;
173
+ },
174
+ [attachMotionBlur, detachMotionBlur],
175
+ );
80
176
 
81
177
  const mergedRef = React.useCallback(
82
178
  (node: HTMLDivElement | null) => {
83
179
  overlayCleanupRef.current?.();
84
180
  overlayCleanupRef.current = null;
181
+ detachMotionBlur();
182
+ hostRef.current = node;
85
183
  if (node) {
86
184
  const viewport = connectOverlayViewport(node);
87
185
  const keyboard = connectKeyboardScroll(node);
88
186
  const pointerEvents = connectBodyPointerEventsFix();
187
+ if (motionBlur) connectMotionBlur(node);
89
188
  overlayCleanupRef.current = () => {
90
189
  viewport();
91
190
  keyboard();
@@ -94,34 +193,65 @@ const SheetContent = React.forwardRef<
94
193
  }
95
194
  assignRef(ref, node);
96
195
  },
97
- [ref],
196
+ [ref, motionBlur, connectMotionBlur, detachMotionBlur],
98
197
  );
99
198
 
100
199
  React.useLayoutEffect(() => {
101
200
  return () => {
102
201
  overlayCleanupRef.current?.();
103
202
  overlayCleanupRef.current = null;
203
+ detachMotionBlur();
104
204
  };
105
- }, []);
205
+ }, [detachMotionBlur]);
206
+
207
+ // vaul may keep the same content node when only `motionBlur` flips, so the
208
+ // callback ref above does not always re-run. Sync attach/detach here.
209
+ React.useLayoutEffect(() => {
210
+ const node = hostRef.current;
211
+ if (!node) return;
212
+ if (motionBlur) connectMotionBlur(node);
213
+ else detachMotionBlur();
214
+ }, [motionBlur, connectMotionBlur, detachMotionBlur]);
215
+
216
+ const handle =
217
+ side === "bottom" || side === "top" ? (
218
+ <div className="flex min-h-11 shrink-0 items-center justify-center">
219
+ <SheetHandle className="relative mx-auto h-1 w-12 shrink-0 rounded-full bg-surface-tertiary after:absolute after:inset-x-[-16px] after:inset-y-[-20px] after:content-['']" />
220
+ </div>
221
+ ) : null;
106
222
 
107
223
  return (
108
224
  <SheetPortal>
109
225
  <SheetOverlay />
110
226
  <DrawerPrimitive.Content
111
227
  ref={mergedRef}
112
- className={cn(
113
- "fixed z-50 overflow-y-auto bg-overlay/95 text-overlay-foreground smooth-shadow-ring-xl outline-hidden backdrop-blur-xl motion-reduce:transition-none",
114
- sheetSideClass[side],
115
- className,
116
- )}
228
+ className={
229
+ motionBlur
230
+ ? cn(sheetBlurHostClass, sheetSideLayoutClass[side], className)
231
+ : cn(
232
+ sheetContentClass,
233
+ sheetSideLayoutClass[side],
234
+ sheetSideSurfaceClass[side],
235
+ className,
236
+ surfaceClassName,
237
+ )
238
+ }
117
239
  {...props}
118
240
  >
119
- {side === "bottom" || side === "top" ? (
120
- <div className="flex min-h-11 shrink-0 items-center justify-center">
121
- <SheetHandle className="relative mx-auto h-1 w-12 shrink-0 rounded-full bg-surface-tertiary after:absolute after:inset-x-[-16px] after:inset-y-[-20px] after:content-['']" />
241
+ {motionBlur ? (
242
+ <div
243
+ data-slot="sheet-surface"
244
+ className={cn(sheetBlurSurfaceClass, sheetSideSurfaceClass[side], surfaceClassName)}
245
+ >
246
+ {handle}
247
+ {children}
122
248
  </div>
123
- ) : null}
124
- {children}
249
+ ) : (
250
+ <>
251
+ {handle}
252
+ {children}
253
+ </>
254
+ )}
125
255
  </DrawerPrimitive.Content>
126
256
  </SheetPortal>
127
257
  );
@@ -165,3 +295,19 @@ export {
165
295
  SheetHeader,
166
296
  SheetFooter,
167
297
  };
298
+
299
+ // The same parts under vaul's name, for apps that call the sliding panel a
300
+ // drawer. One implementation, so `DrawerContent` takes `motionBlur` too.
301
+ export {
302
+ Sheet as Drawer,
303
+ SheetPortal as DrawerPortal,
304
+ SheetOverlay as DrawerOverlay,
305
+ SheetTrigger as DrawerTrigger,
306
+ SheetClose as DrawerClose,
307
+ SheetContent as DrawerContent,
308
+ SheetHandle as DrawerHandle,
309
+ SheetTitle as DrawerTitle,
310
+ SheetDescription as DrawerDescription,
311
+ SheetHeader as DrawerHeader,
312
+ SheetFooter as DrawerFooter,
313
+ };
package/tokens/README.md CHANGED
@@ -13,7 +13,7 @@ Design tokens and shared Tailwind v4 CSS for the vvedantb web apps. The CSS and
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
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
- | `@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. |
16
+ | `@vvedantb/ui/tokens/css/utilities.css` | `src/css/utilities.css` | `@utility hit-target`, `reveal-on-hover`, `motion-press`, `motion-base`, `motion-emphasized` and `scrollbar-none`. The motion utilities read `--motion-fast`, `--motion-base`, `--motion-ease-out` and `--motion-ease-emphasized`, with defaults in the utility. `rounded-surface` moved to `tokens.css` in 0.7.1. |
17
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
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
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`. |
@@ -72,7 +72,7 @@ Tailwind inlines an `@import` where it sits, even in the middle of a file (it mu
72
72
  | `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 |
73
73
  | `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 |
74
74
  | `tabs-pills.css` | `--muted`, `--segment`, `--segment-foreground`, `--default` | tokens.css or the app theme |
75
- | `utilities.css` | `--breakpoint-sm` (Tailwind default or app theme) | Tailwind |
75
+ | `utilities.css` | `--breakpoint-sm` (Tailwind default or app theme); optional `--motion-*` | Tailwind |
76
76
  | `scrollbar-thin.css` | `--scrollbar` | tokens.css or the app theme |
77
77
  | `clear-input.css`, `motion.css` | none (own `:root` defaults) | |
78
78
  | `type-scale.css` | none (own `@theme` values) | |
@@ -81,6 +81,16 @@
81
81
  border-radius: clamp(0.75rem, var(--radius), 1rem);
82
82
  }
83
83
 
84
+ @utility rounded-t-surface {
85
+ border-radius: clamp(0.75rem, var(--radius), 1rem)
86
+ clamp(0.75rem, var(--radius), 1rem) 0 0;
87
+ }
88
+
89
+ @utility rounded-b-surface {
90
+ border-radius: 0 0 clamp(0.75rem, var(--radius), 1rem)
91
+ clamp(0.75rem, var(--radius), 1rem);
92
+ }
93
+
84
94
  @utility rounded-menu-item {
85
95
  border-radius: min(var(--radius), 0.5rem);
86
96
  }
@@ -1,6 +1,13 @@
1
1
  /*
2
2
  * @vvedantb/ui/tokens/css/utilities.css: Tailwind `@utility` helpers. Generated
3
3
  * only when used, and they take variants (e.g. `max-sm:hit-target`).
4
+ *
5
+ * The motion utilities read these variables. Set them on :root to change
6
+ * (defaults shown):
7
+ * --motion-fast: 150ms (press feedback)
8
+ * --motion-base: 220ms (colour, shadow, opacity)
9
+ * --motion-ease-out: cubic-bezier(0.22, 1, 0.36, 1)
10
+ * --motion-ease-emphasized: cubic-bezier(0.2, 0.8, 0.2, 1)
4
11
  */
5
12
 
6
13
  /* Extends sub-40px controls to a 40x40px tap target without changing layout.
@@ -32,3 +39,52 @@
32
39
  }
33
40
  }
34
41
  }
42
+
43
+ /* Each motion utility lists `scale`, `translate` and `rotate` as well as
44
+ `transform`. Tailwind v4 compiles `scale-[0.98]` to `scale: 0.98` and
45
+ `translate-x-*` to `translate: ...`, the individual transform properties.
46
+ `transition-property: transform` does not match them, so without them a
47
+ press or slide would not animate. */
48
+
49
+ /* Hover and state changes: colour, shadow, opacity and transforms. */
50
+ @utility motion-base {
51
+ transition-property:
52
+ color, background-color, border-color, box-shadow, transform, scale,
53
+ translate, rotate, opacity;
54
+ transition-duration: var(--motion-base, 220ms);
55
+ transition-timing-function: var(--motion-ease-out, cubic-bezier(0.22, 1, 0.36, 1));
56
+ }
57
+
58
+ /* Movement on screen: transforms, opacity and shadow on the emphasized curve. */
59
+ @utility motion-emphasized {
60
+ transition-property: transform, scale, translate, rotate, opacity, box-shadow;
61
+ transition-duration: var(--motion-base, 220ms);
62
+ transition-timing-function: var(--motion-ease-emphasized, cubic-bezier(0.2, 0.8, 0.2, 1));
63
+ }
64
+
65
+ /* Pressable controls: pair with `active:scale-[0.97]`. The press runs on
66
+ --motion-fast and colour trails on --motion-base. It covers colour too,
67
+ because Tailwind's `transition-colors` comes later in the sheet at the same
68
+ specificity and would replace this whole declaration. So call sites need no
69
+ separate `transition-*` class. */
70
+ @utility motion-press {
71
+ transition-property:
72
+ transform, scale, translate, rotate, color, background-color, border-color,
73
+ box-shadow, opacity, fill, stroke;
74
+ transition-duration:
75
+ var(--motion-fast, 150ms), var(--motion-fast, 150ms),
76
+ var(--motion-fast, 150ms), var(--motion-fast, 150ms),
77
+ var(--motion-base, 220ms), var(--motion-base, 220ms),
78
+ var(--motion-base, 220ms), var(--motion-base, 220ms),
79
+ var(--motion-base, 220ms), var(--motion-base, 220ms),
80
+ var(--motion-base, 220ms);
81
+ transition-timing-function: var(--motion-ease-out, cubic-bezier(0.22, 1, 0.36, 1));
82
+ }
83
+
84
+ /* Hides the scrollbar. The element still scrolls (wheel, drag, script). */
85
+ @utility scrollbar-none {
86
+ scrollbar-width: none;
87
+ &::-webkit-scrollbar {
88
+ display: none;
89
+ }
90
+ }
@@ -118,6 +118,16 @@
118
118
  border-radius: clamp(0.75rem, var(--radius), 1rem);
119
119
  }
120
120
 
121
+ @utility rounded-t-surface {
122
+ border-radius: clamp(0.75rem, var(--radius), 1rem)
123
+ clamp(0.75rem, var(--radius), 1rem) 0 0;
124
+ }
125
+
126
+ @utility rounded-b-surface {
127
+ border-radius: 0 0 clamp(0.75rem, var(--radius), 1rem)
128
+ clamp(0.75rem, var(--radius), 1rem);
129
+ }
130
+
121
131
  @utility rounded-menu-item {
122
132
  border-radius: min(var(--radius), 0.5rem);
123
133
  }