@devalok/shilp-sutra 0.41.0 → 0.42.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.
@@ -0,0 +1,193 @@
1
+ # Toast
2
+
3
+ Transient floating notification. Imperative API — call from event handlers, never render JSX.
4
+
5
+ ```tsx
6
+ import { toast } from '@devalok/shilp-sutra/ui/toast'
7
+ import { Toaster } from '@devalok/shilp-sutra/ui/toaster'
8
+ ```
9
+
10
+ ## When to use
11
+
12
+ - Confirmation after an action (saved, copied, sent).
13
+ - Non-blocking error feedback (failed to save — try again).
14
+ - Async progress with success/failure resolution — use `toast.promise`.
15
+ - File upload progress — use `toast.upload`.
16
+ - Inline in-flow message that must stay visible? Use `<Alert>`.
17
+ - Page-level strip (cookie banner, account warning)? Use `<Banner>`.
18
+ - Modal interruption? Use `<Dialog>`.
19
+
20
+ ## Setup
21
+
22
+ Mount `<Toaster />` **once** at the app root. Every `toast.*` call routes to this single container.
23
+
24
+ ```tsx
25
+ // app/layout.tsx (Next.js) or src/App.tsx
26
+ import { Toaster } from '@devalok/shilp-sutra/ui/toaster'
27
+
28
+ export default function RootLayout({ children }) {
29
+ return (
30
+ <html>
31
+ <body>
32
+ {children}
33
+ <Toaster />
34
+ </body>
35
+ </html>
36
+ )
37
+ }
38
+ ```
39
+
40
+ Without a mounted Toaster, every `toast.*` call is a silent no-op.
41
+
42
+ ## API
43
+
44
+ ```ts
45
+ toast('Plain message') // no icon, no accent
46
+ toast.message('Same as plain') // alias
47
+ toast.success('Saved!') // green accent + check
48
+ toast.error('Failed', { description }) // red accent + X (assertive a11y)
49
+ toast.warning('Disk low') // yellow accent + triangle
50
+ toast.info('New version available') // blue accent + info
51
+ toast.loading('Saving...') // spinner, duration: Infinity
52
+ toast.promise(asyncFn, { loading, success, error }) // one toast, three states
53
+ toast.undo('Item deleted', { onUndo, duration? }) // 8s default, Undo button
54
+ toast.upload({ files, id?, onRetry?, onRemove? }) // per-file progress
55
+ toast.custom((id) => <MyComponent />, options) // escape hatch
56
+ toast.dismiss(id?) // specific or all
57
+ ```
58
+
59
+ ## Options (every method)
60
+
61
+ | Option | Type | Notes |
62
+ |---|---|---|
63
+ | `id` | `string` | Stable id — pass the same id to update / dismiss. |
64
+ | `description` | `ReactNode` | Subline under the main message. |
65
+ | `action` | `{ label, onClick }` | Right-aligned action button. |
66
+ | `cancel` | `{ label, onClick }` | Right-aligned dismiss button. |
67
+ | `duration` | `number` (ms) | Default `5000`. `Infinity` for loading toasts. |
68
+
69
+ ## Toaster props
70
+
71
+ | Prop | Type | Default | Notes |
72
+ |---|---|---|---|
73
+ | `position` | `'top-left' \| 'top-center' \| 'top-right' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right'` | `'bottom-right'` | Global default. |
74
+ | `closeButton` | `boolean` | `false` | Show X on every toast. |
75
+ | `duration` | `number` (ms) | `5000` | Global default — overridable per toast. |
76
+ | `hotkey` | `string[]` | `['altKey', 'KeyT']` | Keyboard shortcut to focus the toast region. |
77
+ | `visibleToasts` | `number` | `3` | Max stacked toasts; older ones move to a "+N" stack. |
78
+
79
+ ## Examples
80
+
81
+ **Confirmation after save:**
82
+ ```tsx
83
+ async function handleSave() {
84
+ await api.save(data)
85
+ toast.success('Changes saved')
86
+ }
87
+ ```
88
+
89
+ **Error with description:**
90
+ ```tsx
91
+ toast.error('Upload failed', {
92
+ description: 'File is larger than 10 MB.',
93
+ })
94
+ ```
95
+
96
+ **Async with three states (`toast.promise`):**
97
+ ```tsx
98
+ toast.promise(
99
+ api.publishPost(post),
100
+ {
101
+ loading: 'Publishing post...',
102
+ success: 'Published',
103
+ error: (err) => `Failed: ${err.message}`,
104
+ }
105
+ )
106
+ ```
107
+
108
+ One toast, transitions loading → success / error. No manual `.loading()` + `.success()` choreography.
109
+
110
+ **Undo pattern (soft-delete):**
111
+ ```tsx
112
+ function deleteTask(task) {
113
+ const snapshot = task
114
+ setTasks((prev) => prev.filter((t) => t.id !== task.id))
115
+ toast.undo('Task deleted', {
116
+ onUndo: () => setTasks((prev) => [...prev, snapshot]),
117
+ })
118
+ }
119
+ ```
120
+
121
+ Default 8s duration — gives the user time to react.
122
+
123
+ **File upload progress:**
124
+ ```tsx
125
+ const id = 'upload-' + Date.now()
126
+
127
+ toast.upload({
128
+ id,
129
+ files: [
130
+ { id: 'f1', name: 'doc.pdf', size: 1_200_000, status: 'uploading', progress: 0 },
131
+ ],
132
+ })
133
+
134
+ xhr.onprogress = (e) => {
135
+ toast.upload({
136
+ id,
137
+ files: [{ id: 'f1', name: 'doc.pdf', size: 1_200_000, status: 'uploading', progress: (e.loaded / e.total) * 100 }],
138
+ })
139
+ }
140
+
141
+ xhr.onload = () => {
142
+ toast.upload({
143
+ id,
144
+ files: [{ id: 'f1', name: 'doc.pdf', size: 1_200_000, status: 'complete', progress: 100 }],
145
+ })
146
+ }
147
+ ```
148
+
149
+ Passing the same `id` updates the existing toast in place — no flicker.
150
+
151
+ **Custom action:**
152
+ ```tsx
153
+ toast('Project archived', {
154
+ action: { label: 'Open', onClick: () => router.push(`/archive/${id}`) },
155
+ })
156
+ ```
157
+
158
+ **Loading + manual resolution:**
159
+ ```tsx
160
+ const id = toast.loading('Compiling...')
161
+ try {
162
+ await compile()
163
+ toast.success('Compiled', { id }) // replaces the loading toast
164
+ } catch (err) {
165
+ toast.error('Compile failed', { id, description: err.message })
166
+ }
167
+ ```
168
+
169
+ ## Accessibility
170
+
171
+ - `toast.error` uses `aria-live="assertive"` — screen readers interrupt the current announcement.
172
+ - Other variants use `aria-live="polite"` — announced after current speech.
173
+ - `<Toaster hotkey={['altKey', 'KeyT']}>` lets keyboard users jump to the toast region.
174
+
175
+ ## Composability
176
+
177
+ - **One Toaster, many toasts:** Mount once in the root layout. Don't mount per-route — toasts will disappear on navigation.
178
+ - **Imperative only:** No JSX render path. Call `toast.*` from event handlers, async flows, error boundaries.
179
+ - **z-toast (top layer):** Sits above Dialog, Popover, everything. Don't wrap Toaster in a stacking context.
180
+ - **SSR:** Toaster is marked `'use client'` — renders nothing on the server, hydrates on mount.
181
+
182
+ See `foundations/motion.md` for the spring enter/exit, `foundations/color.md` for accent-bar colors.
183
+
184
+ ## Rules
185
+
186
+ - Mount `<Toaster />` exactly once, at the app root. Multiple Toasters double-render every toast.
187
+ - Never call `toast()` in render. Always from a handler, effect, or async function.
188
+ - Don't use the removed `useToast()` hook or `toast({ title, color })` object syntax — both were removed in 0.18.
189
+ - Use `toast.promise` for async flows — manual loading → success choreography is error-prone.
190
+ - For undo affordances, use `toast.undo` — gives the consistent 8s duration plus the styled button.
191
+ - Don't use Toast for content the user must read — they auto-dismiss. Use Alert or Banner instead.
192
+ - Pass a stable `id` when you want to update / replace a toast — without an id, repeated calls stack.
193
+ - `toast.error` is assertive — don't use it for soft warnings. Use `toast.warning`.
@@ -0,0 +1,128 @@
1
+ # Color
2
+
3
+ The kit ships an OKLCH 12-step ramp per color family (`1`=app-bg, `9`=solid/accent, `12`=hi-contrast text) plus semantic role tokens that map onto those steps. **Use the semantic role.** Reach for the numeric step only when authoring custom states.
4
+
5
+ ## Philosophy
6
+
7
+ - ~90% of surfaces should be neutral. Accent is for the primary CTA and ~1 emphasis per screen.
8
+ - Never write hex / rgb / hsl. Never use Tailwind's stock palette utilities (`text-zinc-*`, `bg-blue-*`). They don't dark-mode-flip, they aren't themable.
9
+ - Status color (error / success / warning / info) is reserved for state communication, not for decoration.
10
+
11
+ ## Decision tree — "what color do I use?"
12
+
13
+ ```
14
+ Surface (background of a region)?
15
+ → page bg → bg-surface-base
16
+ → card / widget / panel → bg-surface-raised
17
+ → sidebar / topbar → bg-surface-sunken
18
+ → dialog / popover / dropdown / input → bg-surface-overlay
19
+ → tooltip → bg-surface-inverted
20
+ → disabled control → bg-surface-disabled
21
+
22
+ Text on a surface?
23
+ → primary body / heading → text-fg
24
+ → secondary / metadata → text-fg-muted
25
+ → tertiary / hint → text-fg-subtle
26
+ → on inverted bg → text-surface-inverted-fg
27
+ → on accent-9 / solid → text-accent-fg
28
+ → on error / success / warning / info solid → text-{error|success|warning|info}-fg
29
+
30
+ Border?
31
+ → standard hairline → border-surface-border
32
+ → emphasized → border-surface-border-strong
33
+ → subtle divider → border-surface-border-subtle
34
+ → state (form invalid) → border-error-7 (or use Input state="error" instead)
35
+
36
+ Status?
37
+ → error → bg-error-3 / text-error-11 / border-error-7
38
+ → success → bg-success-3 / text-success-11 / border-success-7
39
+ → warning → bg-warning-3 / text-warning-11 / border-warning-7
40
+ → info → bg-info-3 / text-info-11 / border-info-7
41
+
42
+ Brand emphasis?
43
+ → primary CTA → use <Button> default (no className needed)
44
+ → small accent tint → bg-accent-3 / text-accent-11
45
+ → link → text-link (auto-handles hover + visited)
46
+ ```
47
+
48
+ ## Semantic surface tokens
49
+
50
+ | Token | Use |
51
+ |---|---|
52
+ | `surface-base` | Page background. The "back wall" of the app. |
53
+ | `surface-raised` | Cards, widgets, panels, anything that sits **on** the page. |
54
+ | `surface-raised-hover` | Hover state on a `surface-raised` element. |
55
+ | `surface-raised-active` | Pressed / active state. |
56
+ | `surface-sunken` | Shell chrome (sidebar, topbar), board columns, segmented-control tracks. |
57
+ | `surface-overlay` | Floating layers — dialogs, popovers, dropdowns, inputs, toasts. |
58
+ | `surface-inverted` | Tooltips, inverted badges. Pair with `surface-inverted-fg`. |
59
+ | `surface-disabled` | Disabled controls. Pair with `surface-fg-disabled`. |
60
+
61
+ Available as: `bg-surface-*`, `text-surface-*`, `border-surface-*`.
62
+
63
+ See `foundations/surfaces.md` for elevation rules and the shadow pairing matrix.
64
+
65
+ ## Foreground / text tokens
66
+
67
+ | Token | Use |
68
+ |---|---|
69
+ | `text-fg` | Primary body + headings. |
70
+ | `text-fg-muted` | Secondary content, metadata, helper text. |
71
+ | `text-fg-subtle` | Tertiary content, placeholder text. |
72
+ | `text-surface-inverted-fg` | Text on `surface-inverted`. |
73
+ | `text-accent-fg` | Text on `bg-accent-9` (solid brand). |
74
+ | `text-{error\|success\|warning\|info}-fg` | Text on the solid color background. Brand-swap safe. |
75
+ | `text-link` | Anchor color (auto-hover + visited). |
76
+
77
+ ## Border tokens
78
+
79
+ | Token | Use |
80
+ |---|---|
81
+ | `border-surface-border` | Default 1px line. |
82
+ | `border-surface-border-strong` | Emphasized line — section dividers, table headers. |
83
+ | `border-surface-border-subtle` | Hairline, lower contrast than default. |
84
+
85
+ ## Accent ramp (12 steps)
86
+
87
+ Available as `accent-1` … `accent-12`. Apply via `bg-accent-N`, `text-accent-N`, `border-accent-N`. Steps map to roles:
88
+
89
+ | Step | Role |
90
+ |---|---|
91
+ | 1 | App background tint |
92
+ | 2 | Subtle background |
93
+ | 3 | Component background (e.g. soft Button rest) |
94
+ | 4 | Hover bg |
95
+ | 5 | Active / pressed bg |
96
+ | 6 | Border subtle |
97
+ | 7 | Border default |
98
+ | 8 | Border strong |
99
+ | 9 | Solid fill (primary CTA, badge solid) |
100
+ | 10 | Solid hover |
101
+ | 11 | Lo-contrast text |
102
+ | 12 | Hi-contrast text |
103
+
104
+ The same 12-step pattern repeats for `error-*`, `success-*`, `warning-*`, `info-*` (subset: 2, 3, 4, 5, 6, 7, 9, 10, 11).
105
+
106
+ ## Theming — never hardcode
107
+
108
+ Consumers swap the accent by overriding `--color-accent-1` through `--color-accent-12` in a `:root { }` block placed **after** the kit's CSS import. Dark mode is derived algorithmically — no separate dark overrides needed.
109
+
110
+ ```css
111
+ /* In consumer's global.css, after @import "@devalok/shilp-sutra/css"; */
112
+ :root {
113
+ --color-accent-9: oklch(0.55 0.18 230); /* swap brand to blue */
114
+ /* … the kit will compute related steps via OKLCH curves */
115
+ }
116
+ ```
117
+
118
+ ## Rules
119
+
120
+ - **Never** `bg-white` / `bg-black` / `bg-zinc-*` / `bg-pink-*`. Use semantic tokens.
121
+ - **Never** put a color directly on a Button via className. Set `color="error"` (or whichever).
122
+ - **Never** combine `border-*` and `shadow-*` tokens — shadow tokens contain a ring layer.
123
+ - **Never** use `text-fg` on `bg-accent-9`. Use `text-accent-fg` (brand-swap safe).
124
+ - **Never** invent step numbers. Steps 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12 exist. Step 8 exists only on accent / neutral.
125
+
126
+ ## Forced-colors / Windows high-contrast
127
+
128
+ The kit auto-remaps every semantic color to system keywords (`Canvas`, `CanvasText`, `Highlight`, `LinkText`, `GrayText`) under `@media (forced-colors: active)`. As long as you use semantic tokens, high-contrast mode just works. Hardcoded hex breaks it.
@@ -0,0 +1,81 @@
1
+ # Dark mode
2
+
3
+ `.dark` class on `<html>` or `<body>` flips every token. No theme provider, no JS reshuffle.
4
+
5
+ ## How it works
6
+
7
+ The kit declares a custom variant: `@custom-variant dark (&:where(.dark *))`. Identical semantics to Tailwind's old `darkMode: 'class'`. Adding `.dark` higher in the DOM cascades every semantic token to its dark counterpart.
8
+
9
+ Dark tokens are **algorithmically derived** from the OKLCH primitives, not hand-tuned hex overrides. This means:
10
+
11
+ - Surfaces *lighten* with elevation in dark mode (opposite of light mode). `surface-overlay` is brighter than `surface-base` when `.dark` is active.
12
+ - Brand swaps (consumer overrides `--color-accent-9`) produce a coherent dark palette without extra work.
13
+ - Status colors stay legible — solid backgrounds darken slightly (L=0.63→0.54 in dark mode) to maintain WCAG AA.
14
+
15
+ ## Wiring the toggle
16
+
17
+ ```tsx
18
+ import { useColorMode } from '@devalok/shilp-sutra/hooks/use-color-mode'
19
+
20
+ function ThemeToggle() {
21
+ const { mode, resolvedMode, setMode } = useColorMode()
22
+ // mode: 'light' | 'dark' | 'system'
23
+ // resolvedMode: 'light' | 'dark' (after resolving 'system')
24
+
25
+ return (
26
+ <Button
27
+ variant="soft"
28
+ onClick={() => setMode(resolvedMode === 'dark' ? 'light' : 'dark')}
29
+ >
30
+ <Icon icon={resolvedMode === 'dark' ? IconSun : IconMoon} />
31
+ {resolvedMode === 'dark' ? 'Light' : 'Dark'}
32
+ </Button>
33
+ )
34
+ }
35
+ ```
36
+
37
+ `useColorMode()` handles:
38
+ - Persisting to `localStorage` (`shilp-sutra-color-mode`)
39
+ - Listening to `prefers-color-scheme` when mode is `'system'`
40
+ - Syncing across browser tabs
41
+ - Adding/removing `.dark` on `<html>`
42
+
43
+ ## What you have to do per-component
44
+
45
+ Nothing. Semantic tokens flip automatically. As long as you use `bg-surface-raised` not `bg-white`, dark mode works.
46
+
47
+ The only exception: **images, illustrations, decorative SVGs**. These don't flip. Author them as theme-aware:
48
+
49
+ ```tsx
50
+ {resolvedMode === 'dark' ? <img src="/logo-dark.svg" /> : <img src="/logo-light.svg" />}
51
+ ```
52
+
53
+ Or use a single CSS-variable-driven SVG (`fill="currentColor"` + `text-fg`).
54
+
55
+ ## Forced-colors (Windows high-contrast)
56
+
57
+ Independent of `.dark`. Activated by the OS. Every semantic color remaps to system keywords (`Canvas`, `CanvasText`, `Highlight`, `LinkText`, `GrayText`) under `@media (forced-colors: active)`. No work needed if you use semantic tokens.
58
+
59
+ ## Initial load — avoid the flash
60
+
61
+ Set the mode **before** React hydrates. Insert this script tag in `<head>` (Next.js: `app/layout.tsx`):
62
+
63
+ ```html
64
+ <script>
65
+ (function () {
66
+ var m = localStorage.getItem('shilp-sutra-color-mode') || 'system';
67
+ var dark = m === 'dark' || (m === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
68
+ if (dark) document.documentElement.classList.add('dark');
69
+ })();
70
+ </script>
71
+ ```
72
+
73
+ Without this, light mode flashes on first paint when the user prefers dark.
74
+
75
+ ## Rules
76
+
77
+ - **Never** hardcode hex / Tailwind palette colors. They don't dark-mode flip.
78
+ - **Never** write a `dark:bg-*` override on a semantic token — the token already handles dark mode.
79
+ - **For images**, use conditional rendering off `resolvedMode`.
80
+ - **Wire the head script** to prevent the FOUC on first paint.
81
+ - Default to `'system'` mode unless the product is explicitly light-only or dark-only.
@@ -0,0 +1,107 @@
1
+ # Icons
2
+
3
+ The kit uses **Tabler Icons** exclusively. Custom icon libraries are not supported.
4
+
5
+ ## Install
6
+
7
+ `@tabler/icons-react` is declared as a peer dependency:
8
+
9
+ ```bash
10
+ npm i @tabler/icons-react
11
+ ```
12
+
13
+ ## Import + use
14
+
15
+ ```tsx
16
+ import { Icon } from '@devalok/shilp-sutra/ui/icon'
17
+ import { IconHome, IconUser, IconSettings } from '@tabler/icons-react'
18
+
19
+ <Icon icon={IconHome} />
20
+ <Icon icon={IconUser} size={20} />
21
+ <Icon icon={IconSettings} className="text-fg-muted" />
22
+ ```
23
+
24
+ Or pass the component directly to a prop that accepts `IconInput`:
25
+
26
+ ```tsx
27
+ <Button startIcon={IconPlus}>Add</Button>
28
+ <Badge icon={IconCheck}>Verified</Badge>
29
+ ```
30
+
31
+ ## `IconInput` (universal icon prop type)
32
+
33
+ Every icon-accepting prop across the kit (`startIcon`, `endIcon`, `icon`, `leftIcon`, `rightIcon`) takes `IconInput`. Four shapes work interchangeably:
34
+
35
+ ```tsx
36
+ // 1. Component reference (Tabler component)
37
+ <Button startIcon={IconPlus} />
38
+
39
+ // 2. Rendered <Icon> element
40
+ <Button startIcon={<Icon icon={IconPlus} />} />
41
+
42
+ // 3. Rendered Tabler element directly
43
+ <Button startIcon={<IconPlus />} />
44
+
45
+ // 4. Any custom node (svg, span with bg)
46
+ <Button startIcon={<MyCustomIcon />} />
47
+ ```
48
+
49
+ Prefer shape 1 (component ref) — the kit applies the right size and color via context.
50
+
51
+ ## Sizing via `IconProvider`
52
+
53
+ Don't add `className="h-4 w-4"` on every Icon. Wrap a subtree:
54
+
55
+ ```tsx
56
+ import { IconProvider } from '@devalok/shilp-sutra/ui/icon-context'
57
+
58
+ <IconProvider size={16}>
59
+ <NavSection>
60
+ <Icon icon={IconHome} />
61
+ <Icon icon={IconUser} />
62
+ {/* all icons here = 16px */}
63
+ </NavSection>
64
+ </IconProvider>
65
+ ```
66
+
67
+ Override per-icon with `<Icon icon={...} size={24} />`.
68
+
69
+ Default sizes by component context:
70
+
71
+ | Context | Default size |
72
+ |---|---|
73
+ | Button (md) | 16 |
74
+ | Button (sm / xs) | 14 |
75
+ | Button (lg / xl) | 18 / 20 |
76
+ | Input (any size) | 16 |
77
+ | Badge | 12 |
78
+ | Standalone | 16 (recommended) |
79
+
80
+ ## Color
81
+
82
+ Icons inherit `currentColor` by default. To color one:
83
+
84
+ ```tsx
85
+ <Icon icon={IconAlertTriangle} className="text-warning-11" />
86
+ <Icon icon={IconCheck} className="text-success-11" />
87
+ ```
88
+
89
+ Inside a `<Button>` or `<Badge>`, color is inherited from the button/badge — don't override.
90
+
91
+ ## Multi-color / branded icons (OAuth, etc.)
92
+
93
+ OAuthButton ships brand glyphs for 13 providers. To use a brand's official multi-color SVG, pass `icon` to override:
94
+
95
+ ```tsx
96
+ <OAuthButton provider="google" icon={<GoogleColorSVG />} />
97
+ ```
98
+
99
+ For custom branded icons elsewhere, use raw `<svg>` — `<Icon>` is for Tabler outline glyphs.
100
+
101
+ ## Rules
102
+
103
+ - **Tabler only** — no lucide-react, no heroicons, no mui-icons. The kit is sized for Tabler's stroke weight.
104
+ - **Use `<Icon icon={...} />`** — don't render Tabler components directly when inside a kit component that accepts `IconInput`.
105
+ - **Don't size with className** (`h-4 w-4`) — use `size` prop or `IconProvider`.
106
+ - **Don't color icons inside Button / Badge** — they inherit from the parent. Color only on standalone icons.
107
+ - **Don't pass a string** (`"check"`) — `IconInput` excludes strings/numbers. Always a component or element.
@@ -0,0 +1,134 @@
1
+ # Motion
2
+
3
+ framer-motion is the kit's animation engine. **Don't write CSS keyframes** — use motion primitives.
4
+
5
+ ## Required setup
6
+
7
+ ```tsx
8
+ import { MotionProvider } from '@devalok/shilp-sutra/motion'
9
+
10
+ <MotionProvider reducedMotion="user">
11
+ <App />
12
+ </MotionProvider>
13
+ ```
14
+
15
+ `reducedMotion="user"` respects the OS's "reduce motion" preference. Without `MotionProvider`, the primitives still work but reduced-motion is ignored — accessibility regression.
16
+
17
+ ## Primitives
18
+
19
+ ```tsx
20
+ import {
21
+ MotionFade,
22
+ MotionCollapse,
23
+ MotionSlide,
24
+ MotionPop,
25
+ MotionScale,
26
+ MotionStagger,
27
+ } from '@devalok/shilp-sutra/motion/primitives'
28
+ ```
29
+
30
+ | Primitive | Use |
31
+ |---|---|
32
+ | `MotionFade` | Mount/unmount with opacity fade. |
33
+ | `MotionCollapse` | Height-based expand/collapse. |
34
+ | `MotionSlide` | Slide in from a direction (`from="top"|"bottom"|"left"|"right"`). |
35
+ | `MotionPop` | Scale + fade pop. Good for tooltips, badges entering. |
36
+ | `MotionScale` | Scale only. |
37
+ | `MotionStagger` | Stagger children with a configurable delay. |
38
+
39
+ ```tsx
40
+ <MotionFade>
41
+ <Card>This card fades in on mount.</Card>
42
+ </MotionFade>
43
+
44
+ <MotionStagger gap={0.05}>
45
+ {items.map((item) => (
46
+ <ListItem key={item.id}>{item.name}</ListItem>
47
+ ))}
48
+ </MotionStagger>
49
+ ```
50
+
51
+ ## Springs & tweens
52
+
53
+ ```tsx
54
+ import { springs, tweens } from '@devalok/shilp-sutra/motion'
55
+ ```
56
+
57
+ | Spring | Feel |
58
+ |---|---|
59
+ | `springs.snappy` | Decisive, quick. Default for controls. |
60
+ | `springs.smooth` | Smooth, no overshoot. Default for layout. |
61
+ | `springs.bouncy` | Playful overshoot. Use sparingly — only for delight moments. |
62
+ | `springs.gentle` | Soft, slow. For ambient motion. |
63
+
64
+ | Tween | Duration |
65
+ |---|---|
66
+ | `tweens.fade` | 0.11s — color/opacity. |
67
+ | `tweens.colorShift` | 0.07s — hover color changes. |
68
+
69
+ Apply via framer's `transition` prop:
70
+
71
+ ```tsx
72
+ import { motion } from 'framer-motion'
73
+ import { springs } from '@devalok/shilp-sutra/motion'
74
+
75
+ <motion.div animate={{ y: open ? 0 : -8 }} transition={springs.snappy}>
76
+
77
+ </motion.div>
78
+ ```
79
+
80
+ ## Duration tokens (CSS, when motion primitives aren't right)
81
+
82
+ | Token | Duration |
83
+ |---|---|
84
+ | `--duration-fast-01` | 70 ms |
85
+ | `--duration-fast-02` | 110 ms |
86
+ | `--duration-moderate-01` | 150 ms |
87
+ | `--duration-moderate-02` | 240 ms |
88
+ | `--duration-slow-01` | 400 ms |
89
+ | `--duration-slow-02` | 700 ms |
90
+
91
+ Tailwind utilities: `duration-fast-01`, `duration-moderate-01`, etc. CSS variables: `var(--duration-moderate-01)`.
92
+
93
+ The system easing is `ease-productive-standard` — apply via `transition` shorthand:
94
+
95
+ ```tsx
96
+ <div className="transition-colors duration-fast-02">…</div>
97
+ ```
98
+
99
+ ## Patterns
100
+
101
+ **Drawer / Sheet slide-in:** already built into `<Sheet>`. Don't reimplement.
102
+
103
+ **Dialog backdrop fade + content scale:** already built into `<Dialog>`. Don't reimplement.
104
+
105
+ **List item enter (e.g. new card appears):**
106
+
107
+ ```tsx
108
+ <MotionFade>
109
+ <Card>New item</Card>
110
+ </MotionFade>
111
+ ```
112
+
113
+ **Staggered table rows:**
114
+
115
+ ```tsx
116
+ <MotionStagger gap={0.03}>
117
+ {rows.map((row) => <TableRow key={row.id}>{row.cells}</TableRow>)}
118
+ </MotionStagger>
119
+ ```
120
+
121
+ **Async button feedback:** use `<Button onClickAsync>` — it auto-animates idle → loading → success/error → idle. Don't hand-roll spinner toggling.
122
+
123
+ ```tsx
124
+ <Button onClickAsync={async () => { await save() }}>Save</Button>
125
+ ```
126
+
127
+ ## Rules
128
+
129
+ - **Wrap the app in `<MotionProvider reducedMotion="user">` once.** Without it, OS reduce-motion preference is ignored.
130
+ - **Never** write CSS `@keyframes` for app animations — use framer primitives.
131
+ - **Use motion primitives** before reaching for raw `motion.*` — they bundle the right spring + reduced-motion respect.
132
+ - **Don't animate layout** (height, width, top, left) — animate `transform` and `opacity`. The primitives do this for you.
133
+ - **Don't overuse `bouncy`** — bouncy works for one delight moment per session, not for every hover.
134
+ - For async button states, use `onClickAsync` + `asyncFeedbackDuration`, not manual `loading` toggling.