@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.
- package/MIGRATION.md +8 -0
- package/llms-full.txt +1 -1
- package/make-kit/Guidelines.md +71 -0
- package/make-kit/components/badge.md +162 -0
- package/make-kit/components/button.md +125 -0
- package/make-kit/components/card.md +147 -0
- package/make-kit/components/dialog.md +167 -0
- package/make-kit/components/dropdown-menu.md +205 -0
- package/make-kit/components/form.md +189 -0
- package/make-kit/components/icon.md +152 -0
- package/make-kit/components/input.md +154 -0
- package/make-kit/components/overview.md +308 -0
- package/make-kit/components/popover.md +201 -0
- package/make-kit/components/select.md +148 -0
- package/make-kit/components/stack.md +165 -0
- package/make-kit/components/table.md +215 -0
- package/make-kit/components/tabs.md +162 -0
- package/make-kit/components/text.md +139 -0
- package/make-kit/components/toast.md +193 -0
- package/make-kit/foundations/color.md +128 -0
- package/make-kit/foundations/dark-mode.md +81 -0
- package/make-kit/foundations/icons.md +107 -0
- package/make-kit/foundations/motion.md +134 -0
- package/make-kit/foundations/radius.md +78 -0
- package/make-kit/foundations/spacing.md +110 -0
- package/make-kit/foundations/surfaces.md +121 -0
- package/make-kit/foundations/typography.md +120 -0
- package/make-kit/setup.md +130 -0
- package/package.json +5 -2
- package/skill/SKILL.md +1 -1
- package/skill/references/components-full.md +1 -1
|
@@ -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.
|