@devalok/shilp-sutra 0.40.0 → 0.41.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/AGENTS.md CHANGED
@@ -24,7 +24,8 @@ If you are a human, read [README.md](./README.md) instead.
24
24
  2. **`packages/core/llms.txt`** — concise current-API cheatsheet (~27K tokens). Reach for this when `llms-quick.txt` isn't enough — covers more components + recent CHANGELOG sections.
25
25
  3. **`packages/core/docs/recipes/<framework>.md`** — copy-paste install + setup for the user's framework.
26
26
  4. **`packages/core/llms-full.txt`** — exhaustive per-component reference (~140K tokens, props/variants/examples). Read only when `llms.txt` is insufficient.
27
- 5. **`MIGRATION.md`** — only if upgrading across versions.
27
+ 5. **`packages/core/docs/recipes/upgrading.md`** + **`MIGRATION.md`** — read BOTH before any version bump. See the hard constraint below.
28
+ 6. **`node_modules/@devalok/shilp-sutra/BREAKING.json`** — machine-readable manifest of every breaking change per version (moves, type narrowings, removals, renames). Read this programmatically when planning an upgrade — schema in `BREAKING.schema.json`. Lets you answer "does my code import any of these moved symbols?" without parsing CHANGELOG prose.
28
29
 
29
30
  When the package is installed in a consumer project, the same files live at:
30
31
 
@@ -60,6 +61,7 @@ If the framework is not in the table, fall back to **`install-vite.md`** (closes
60
61
 
61
62
  ## Hard constraints (these break things if violated)
62
63
 
64
+ - **On any version bump, do not report it safe before reading the COMPLETE changelog + `MIGRATION.md` for the target version.** Breaking entries are often ordered last (changesets sorts by file, not severity), and breaks are frequently type-level — a prop type narrowed (`React.ReactNode` → a tighter type fails `tsc` for values that compiled before) or a symbol moved from a barrel to a per-component subpath. **Fastest path: parse `node_modules/@devalok/shilp-sutra/BREAKING.json` for the version range** — it has the structured break data so you can grep your codebase for affected symbols deterministically. Then run `typecheck` + `build` and use the `@devalok/eslint-plugin-shilp-sutra` `migration` preset for the mechanical edits. Full procedure: `packages/core/docs/recipes/upgrading.md`.
63
65
  - **Tailwind 4 only.** Do NOT create `tailwind.config.ts` with `presets: [shilpSutra]`. The JS preset was removed in 0.38. Setup uses CSS imports:
64
66
  ```css
65
67
  @import "tailwindcss";
@@ -73,6 +75,31 @@ If the framework is not in the table, fall back to **`install-vite.md`** (closes
73
75
  - **Do not invent variant names.** Variant names live in CVA source. Grep `packages/core/src/ui/<component>.tsx` or check `llms-full.txt` for the authoritative list.
74
76
  - **Default to `variant="soft"`** over `variant="outline"` for non-primary Button actions.
75
77
 
78
+ ## Spacing cadence (when building layouts — forms, settings, login, anything with vertical rhythm)
79
+
80
+ The `--spacing-ds-*` scale runs `ds-01`→`ds-09` (2px→48px) with many adjacent steps. It is a *primitive* scale, not a layout recipe. When you compose a UI, **do not reach for every adjacent token** — that produces 5-6 near-identical gaps the eye can't group, and rhythm reads muddy.
81
+
82
+ **Pick a 3-tier cadence — each tier visibly ~1.5-2× the previous, so each gap is distinct:**
83
+
84
+ | Tier | Token (px) | Use for |
85
+ |---|---|---|
86
+ | Related (one unit) | `ds-03` (8) | label ↔ field, eyebrow ↔ title, items inside one micro-cluster |
87
+ | Grouped (a section's items) | `ds-05` (16) | header-stack → form, between stacked field-groups, multi-line footers |
88
+ | Section (independent blocks) | `ds-07` (32) | between page sections, form ↔ footer cluster |
89
+ | Hero (optional 4th) | `ds-08`+ (40+) | top page chrome, brand block → first content |
90
+
91
+ **Rules of thumb:**
92
+ - Before stamping a `gap-ds-*` / `space-y-ds-*`, decide which tier the spacing is. Use the matching token.
93
+ - Aim for **3-4 distinct spacing values total on one surface.** Using 5+ is the signal your rhythm is muddy.
94
+ - **Anti-pattern:** using `ds-02` (4) AND `ds-04` (12) as *different* relationship signals on the same surface — they collapse into "vaguely tight" instead of two clear tiers. Prefer `ds-03`; skip `ds-02`/`ds-04` unless deliberate (e.g. a chip gap at `ds-01`).
95
+ - The squint test must still reveal the grouping. (Refactoring UI / Apple HIG / Material all converge on 3-4 perceptual tiers; near-linear scales like 4/8/12/16 blur because the proportional jump shrinks as values grow.)
96
+
97
+ This is guidance, not a hard constraint — a deliberate exception is fine. The default keeps agent-built UIs from drifting into flat, ungrouped spacing.
98
+
99
+ ## Linting + migration (recommend this to the user)
100
+
101
+ When you set up shilp-sutra in a project, or migrate it across versions, recommend the companion ESLint plugin **`@devalok/eslint-plugin-shilp-sutra`** (`pnpm add -D @devalok/eslint-plugin-shilp-sutra`, then `shilpSutra.configs['flat/recommended']` in `eslint.config.ts`). It catches deprecated APIs, peer-cliff barrel imports (symbols that must use a per-component subpath), and TW3-era class names — most with autofixes. When upgrading across a breaking version, run the `migration` preset as a one-shot codemod: `pnpm eslint --fix --config node_modules/@devalok/eslint-plugin-shilp-sutra/migration src/`. Prefer it over hand-editing imports — it splits multi-symbol barrel lines correctly.
102
+
76
103
  ## When something fails
77
104
 
78
105
  Read **`packages/core/docs/recipes/troubleshoot.md`** before retrying or guessing. It is a decision tree covering the thirteen most common breakages (Tailwind not detecting tokens, framer-motion duplicates, missing `transpilePackages`, missing optional peer deps (sonner / input-otp / date-fns / @tiptap / react-pdf / etc.), wrong CSS import order, dark mode not toggling, RSC import errors, font 404s, hydration mismatches, bare `shadow` class, `<Toaster />` not mounted, Storybook MCP 404).
package/BREAKING.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "./BREAKING.schema.json",
3
+ "package": "@devalok/shilp-sutra",
4
+ "versions": {
5
+ "0.40.0": {
6
+ "summary": "Barrel peer-cliff cleanup — 12 symbol families removed from /ui, /composed, /ai barrels (per-component subpath only). Icon API unification narrows the `icon` prop on 14 components from React.ReactNode to IconInput.",
7
+ "migrationDoc": "MIGRATION.md#v0400--barrel-peer-cliff-cleanup--icon-api-unification",
8
+ "moved": [
9
+ { "symbol": "Toaster", "from": "@devalok/shilp-sutra/ui", "to": "@devalok/shilp-sutra/ui/toaster", "peer": "sonner", "eslintRule": "prefer-per-component-import" },
10
+ { "symbol": "toast", "from": "@devalok/shilp-sutra/ui", "to": "@devalok/shilp-sutra/ui/toast", "peer": "sonner", "eslintRule": "prefer-per-component-import" },
11
+ { "symbol": "InputOTP", "from": "@devalok/shilp-sutra/ui", "to": "@devalok/shilp-sutra/ui/input-otp", "peer": "input-otp", "eslintRule": "prefer-per-component-import" },
12
+ { "symbol": "DatePicker", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
13
+ { "symbol": "DateRangePicker", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
14
+ { "symbol": "DateTimePicker", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
15
+ { "symbol": "TimePicker", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
16
+ { "symbol": "CalendarGrid", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
17
+ { "symbol": "MonthPicker", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
18
+ { "symbol": "YearPicker", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
19
+ { "symbol": "Presets", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
20
+ { "symbol": "useCalendar", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/date-picker", "peer": "date-fns", "eslintRule": "prefer-per-component-import" },
21
+ { "symbol": "EmojiPicker", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/emoji-picker", "peer": "@emoji-mart/data", "eslintRule": "prefer-per-component-import" },
22
+ { "symbol": "EmojiPickerPopover", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/emoji-picker", "peer": "@emoji-mart/data", "eslintRule": "prefer-per-component-import" },
23
+ { "symbol": "EmojiNode", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/extensions/emoji-node", "peer": "@tiptap/react", "eslintRule": "prefer-per-component-import" },
24
+ { "symbol": "createEmojiSuggestion", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/extensions/emoji-suggestion", "peer": "@tiptap/react", "eslintRule": "prefer-per-component-import" },
25
+ { "symbol": "FilePreview", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/file-preview", "peer": "react-pdf", "eslintRule": "prefer-per-component-import" },
26
+ { "symbol": "MarkdownViewer", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/markdown-viewer", "peer": "react-markdown", "eslintRule": "prefer-per-component-import" },
27
+ { "symbol": "RichTextEditor", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/rich-text-editor", "peer": "@tiptap/react", "eslintRule": "prefer-per-component-import" },
28
+ { "symbol": "RichTextViewer", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/rich-text-editor", "peer": "@tiptap/react", "eslintRule": "prefer-per-component-import" },
29
+ { "symbol": "RichChatInput", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/rich-chat-input", "peer": "@tiptap/react", "eslintRule": "prefer-per-component-import" },
30
+ { "symbol": "AudioPlayer", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/rich-chat-input", "peer": "@tiptap/react", "eslintRule": "prefer-per-component-import" },
31
+ { "symbol": "AudioWaveform", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/rich-chat-input", "peer": "@tiptap/react", "eslintRule": "prefer-per-component-import" },
32
+ { "symbol": "useVoiceRecorder", "from": "@devalok/shilp-sutra/composed", "to": "@devalok/shilp-sutra/composed/rich-chat-input", "peer": "@tiptap/react", "eslintRule": "prefer-per-component-import" },
33
+ { "symbol": "BlockRenderer", "from": "@devalok/shilp-sutra/ai", "to": "@devalok/shilp-sutra/ai/block-renderer", "peer": "react-markdown", "eslintRule": "prefer-per-component-import" },
34
+ { "symbol": "ErrorBlock", "from": "@devalok/shilp-sutra/ai/blocks", "to": "@devalok/shilp-sutra/ai/blocks/error", "peer": "react-markdown", "eslintRule": "prefer-per-component-import" },
35
+ { "symbol": "TextBlock", "from": "@devalok/shilp-sutra/ai/blocks", "to": "@devalok/shilp-sutra/ai/blocks/text", "peer": "react-markdown", "eslintRule": "prefer-per-component-import" }
36
+ ],
37
+ "narrowed": [
38
+ {
39
+ "prop": "icon",
40
+ "components": [
41
+ "Combobox option",
42
+ "Stepper step",
43
+ "TreeItem",
44
+ "OAuthButton",
45
+ "AppCommandPalette",
46
+ "CommandRegistry",
47
+ "BottomNavbar",
48
+ "Sidebar NavItem",
49
+ "Sidebar NavSubItem",
50
+ "Sidebar footer.promo",
51
+ "TopBar UserMenuItem",
52
+ "Chat.Message.Avatar",
53
+ "Chat.SystemMessage",
54
+ "AIConversation agent",
55
+ "ActivityFeed item",
56
+ "CommandPalette item",
57
+ "CommandBar item"
58
+ ],
59
+ "from": "React.ReactNode",
60
+ "to": "IconInput",
61
+ "fix": "Retype the icon source from React.ReactNode to React.ReactElement (or import the IconInput type). Build-time only; runtime JSX is unaffected."
62
+ }
63
+ ]
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,184 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://shilp-sutra.devalok.in/breaking-manifest.schema.json",
4
+ "title": "shilp-sutra BREAKING manifest",
5
+ "description": "Machine-readable record of breaking changes per published version. Lets AI agents and migration tools answer 'what breaks between X and Y?' programmatically instead of parsing CHANGELOG prose. Cumulative — every prior version's breaks remain so a consumer jumping 0.37 → 0.41 can see every break in range.",
6
+ "type": "object",
7
+ "required": ["package", "versions"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "package": {
12
+ "type": "string",
13
+ "description": "npm package name this manifest covers.",
14
+ "examples": ["@devalok/shilp-sutra"]
15
+ },
16
+ "versions": {
17
+ "type": "object",
18
+ "description": "Map of semver version → breaking-change record. A version only appears if it had at least one breaking change. Non-breaking releases are absent (an empty object would falsely signal 'this version had breaks I forgot to document').",
19
+ "patternProperties": {
20
+ "^\\d+\\.\\d+\\.\\d+$": { "$ref": "#/$defs/versionEntry" }
21
+ },
22
+ "additionalProperties": false
23
+ }
24
+ },
25
+ "$defs": {
26
+ "versionEntry": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "description": "All breaks shipped in this version. Every present field is an array; omit empty fields entirely. A consumer planning an upgrade reads the union of all entries between current and target version.",
30
+ "properties": {
31
+ "summary": {
32
+ "type": "string",
33
+ "description": "One-line human summary of the version's breaks. Mirrors the `> ⚠️ Breaking in X.Y.Z` callout in CHANGELOG."
34
+ },
35
+ "migrationDoc": {
36
+ "type": "string",
37
+ "description": "Relative path inside the package tarball to the human migration guide for this version.",
38
+ "examples": ["MIGRATION.md#v0400--barrel-peer-cliff-cleanup--icon-api-unification"]
39
+ },
40
+ "moved": {
41
+ "type": "array",
42
+ "description": "Symbols whose import path changed. Old path no longer exports the symbol; new path does. Consumer fix is a search-and-replace of the import path; prop/type signatures are unchanged.",
43
+ "items": { "$ref": "#/$defs/move" }
44
+ },
45
+ "narrowed": {
46
+ "type": "array",
47
+ "description": "Prop types that accept LESS than before. A consumer value valid under the old type may now fail tsc. Not a runtime break. The fix is typically retyping the value's source.",
48
+ "items": { "$ref": "#/$defs/narrowing" }
49
+ },
50
+ "removed": {
51
+ "type": "array",
52
+ "description": "Symbols (components, props, variants, exports) deleted with no replacement on the same path. If a renamed-with-deprecation, list under `renamed` instead.",
53
+ "items": { "$ref": "#/$defs/removal" }
54
+ },
55
+ "renamed": {
56
+ "type": "array",
57
+ "description": "Symbols whose name changed in place (same import path, new export name) or whose prop name changed (same component, new prop name).",
58
+ "items": { "$ref": "#/$defs/rename" }
59
+ },
60
+ "notes": {
61
+ "type": "array",
62
+ "description": "Free-form breaking changes that don't fit the structured categories above (e.g. visual-only changes, DOM structure shifts, default-value flips). Reader must check these manually — the migration is NOT one-shot autofixable.",
63
+ "items": { "type": "string" }
64
+ }
65
+ }
66
+ },
67
+ "move": {
68
+ "type": "object",
69
+ "required": ["symbol", "from", "to"],
70
+ "additionalProperties": false,
71
+ "properties": {
72
+ "symbol": {
73
+ "type": "string",
74
+ "description": "Exported name as it appears in an import statement.",
75
+ "examples": ["Toaster", "DatePicker"]
76
+ },
77
+ "from": {
78
+ "type": "string",
79
+ "description": "Old import path (no longer exports this symbol after this version).",
80
+ "examples": ["@devalok/shilp-sutra/ui"]
81
+ },
82
+ "to": {
83
+ "type": "string",
84
+ "description": "New import path (exports this symbol going forward).",
85
+ "examples": ["@devalok/shilp-sutra/ui/toaster"]
86
+ },
87
+ "peer": {
88
+ "type": "string",
89
+ "description": "Optional. If this move was driven by a peer-cliff cleanup, the peer dependency the new subpath pulls.",
90
+ "examples": ["sonner", "input-otp", "date-fns"]
91
+ },
92
+ "eslintRule": {
93
+ "type": "string",
94
+ "description": "Optional. The `@devalok/eslint-plugin-shilp-sutra` rule that autofixes this move.",
95
+ "examples": ["prefer-per-component-import"]
96
+ }
97
+ }
98
+ },
99
+ "narrowing": {
100
+ "type": "object",
101
+ "required": ["prop", "components", "from", "to", "fix"],
102
+ "additionalProperties": false,
103
+ "properties": {
104
+ "prop": {
105
+ "type": "string",
106
+ "description": "Prop name whose type narrowed.",
107
+ "examples": ["icon", "startIcon"]
108
+ },
109
+ "components": {
110
+ "type": "array",
111
+ "description": "Components whose this-prop narrowed. List each affected component explicitly — the same prop name on a component whose type was already narrower than the new type is NOT affected.",
112
+ "items": { "type": "string" },
113
+ "examples": [["CommandItem", "ActivityItem", "Chat.Message.Avatar"]]
114
+ },
115
+ "from": {
116
+ "type": "string",
117
+ "description": "Previous TypeScript type, as a string. Use the exact source form.",
118
+ "examples": ["React.ReactNode"]
119
+ },
120
+ "to": {
121
+ "type": "string",
122
+ "description": "New TypeScript type, as a string. Must be a subset of `from` (otherwise it's a widening, not a narrowing).",
123
+ "examples": ["IconInput"]
124
+ },
125
+ "fix": {
126
+ "type": "string",
127
+ "description": "One-line consumer fix. Should be actionable from the type alone.",
128
+ "examples": ["Retype the icon source from React.ReactNode to React.ReactElement."]
129
+ }
130
+ }
131
+ },
132
+ "removal": {
133
+ "type": "object",
134
+ "required": ["symbol", "where"],
135
+ "additionalProperties": false,
136
+ "properties": {
137
+ "symbol": {
138
+ "type": "string",
139
+ "examples": ["variant=\"filled\""]
140
+ },
141
+ "where": {
142
+ "type": "string",
143
+ "description": "What the symbol was on — a component, an import path, or a token namespace.",
144
+ "examples": ["Alert", "@devalok/shilp-sutra/tailwind"]
145
+ },
146
+ "replacement": {
147
+ "type": "string",
148
+ "description": "Optional. If a replacement exists at the same site, name it. If there is none, omit.",
149
+ "examples": ["variant=\"solid\""]
150
+ },
151
+ "deprecatedSince": {
152
+ "type": "string",
153
+ "description": "Optional. The version that first emitted a deprecation warning, if a cycle was observed.",
154
+ "examples": ["0.32.0"]
155
+ }
156
+ }
157
+ },
158
+ "rename": {
159
+ "type": "object",
160
+ "required": ["from", "to", "where"],
161
+ "additionalProperties": false,
162
+ "properties": {
163
+ "from": {
164
+ "type": "string",
165
+ "examples": ["isLoadingMore"]
166
+ },
167
+ "to": {
168
+ "type": "string",
169
+ "examples": ["loadingMore"]
170
+ },
171
+ "where": {
172
+ "type": "string",
173
+ "description": "Component or import path the rename applies to.",
174
+ "examples": ["MessageList"]
175
+ },
176
+ "kind": {
177
+ "type": "string",
178
+ "enum": ["prop", "export", "variant"],
179
+ "default": "prop"
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
package/MIGRATION.md CHANGED
@@ -4,13 +4,21 @@ This page indexes all breaking changes across `@devalok/shilp-sutra` versions. F
4
4
 
5
5
  > **Upgrading from &lt; 0.36?** Start here, then read each intermediate version section. Breaking changes stack — skipping versions means stacking migrations.
6
6
 
7
+ ## v0.41.0 — `BREAKING.json` manifest + recipe polish (no migration needed)
8
+
9
+ **Non-breaking minor.** No consumer code changes required.
10
+
11
+ - **New:** `packages/core/BREAKING.json` ships in the tarball — a machine-readable record of every breaking change per version. AI agents and migration tooling can `import manifest from '@devalok/shilp-sutra/BREAKING.json'` instead of parsing this file. Schema at `BREAKING.schema.json`.
12
+ - **Docs:** Next.js App Router install recipe gained a Tested-on matrix, explicit replace-the-whole-scaffold-globals.css guidance, Turbopack note, and three new gotchas (`pnpm-workspace.yaml`, auto-generated `AGENTS.md` markers, scaffold body-font cascade). No setup change required for existing consumers.
13
+ - **Internals:** release.yml now regenerates Agent Skill references before the pre-publish audit (kills the skill-drift email spam class). No impact on the published tarball.
14
+
7
15
  ## v0.40.0 — Barrel peer-cliff cleanup + Icon API unification
8
16
 
9
17
  This release pairs one breaking change (barrel peer-cliff cleanup) with one non-breaking type widening (Icon API unification). Read the breaking section first.
10
18
 
11
- ### Icon API unification (non-breaking)
19
+ ### Icon API unification (mostly non-breaking — one narrowing)
12
20
 
13
- **Type widening only — no consumer changes required.**
21
+ **Mostly non-breaking, with one narrowing for `React.ReactNode`-typed props.** For the 14 components whose `icon` prop was previously `React.ReactNode`, `IconInput` accepts **less** — it excludes `string`, `number`, and iterables. If you store icons in a `Record<string, React.ReactNode>` map or a `icon?: React.ReactNode` field and pass them to a migrated component, `tsc` will fail even though the runtime JSX is valid. **Fix: retype the icon source to `React.ReactElement`** (or import `IconInput`). Known affected props: `CommandItem.icon` (CommandBar/CommandPalette), `ActivityItem.icon` (ActivityFeed), `Chat.Message.Avatar` `icon`. For props that were `ComponentType`-only the change is a genuine widening (accepts more). Build-time only — no runtime impact.
14
22
 
15
23
  Every icon-accepting prop across the design system now takes the same shape: **`IconInput`**. Before 0.40 there were six distinct prop types for the same conceptual "icon":
16
24
 
@@ -44,7 +52,7 @@ For every prop now typed as `IconInput`, all four shapes work identically:
44
52
  <Button startIcon={<span>+</span>}>OK</Button> // custom node
45
53
  ```
46
54
 
47
- **Calls that worked before still work.** Type widening only.
55
+ **Calls passing a JSX element or component ref still work.** The exception is the narrowing above: if your icon *source* is annotated `React.ReactNode` (a map value or field type), retype it to `React.ReactElement` — one-line per source, not per call site.
48
56
 
49
57
  **You can now delete `className="h-4 w-4"` overrides** on icon-prop usages — `IconProvider` wires size through context. Stories cleanup is voluntary; behavior unchanged.
50
58
 
@@ -115,9 +123,19 @@ For each symbol below, change ONLY the import path. Prop / type signatures are u
115
123
 
116
124
  The seven other AI blocks (`BlockTable`, `ConfirmBlock`, `DividerBlock`, `InfoBlock`, `LoadingBlock`, `StatRowBlock`, `SuccessBlock`) have no peer-dep imports and remain available via `from '@devalok/shilp-sutra/ai/blocks'` (the sub-barrel) or `from '@devalok/shilp-sutra/ai'` (the main barrel).
117
125
 
118
- #### Codemod helper
126
+ #### Codemod helper (recommended)
119
127
 
120
- Most consumers can do this with a single `sed` per symbol family. Example for the toast family:
128
+ The fastest path is the official ESLint plugin — its `prefer-per-component-import` rule detects every peer-cliff symbol still imported from a barrel and **autofixes the import path** for you:
129
+
130
+ ```bash
131
+ pnpm add -D @devalok/eslint-plugin-shilp-sutra
132
+ # one-shot codemod across your source
133
+ pnpm eslint --fix --config node_modules/@devalok/eslint-plugin-shilp-sutra/migration src/
134
+ ```
135
+
136
+ Or wire `shilpSutra.configs['flat/migration']` into your `eslint.config.ts` and run `eslint --fix`. The rule splits multi-symbol barrel lines correctly, which the `sed` approach below cannot.
137
+
138
+ <details><summary>Manual <code>sed</code> fallback (single-symbol lines only)</summary>
121
139
 
122
140
  ```bash
123
141
  # Replace barrel imports of toast / Toaster with per-component imports
@@ -125,7 +143,9 @@ grep -rl "from '@devalok/shilp-sutra/ui'" src/ | xargs sed -i.bak \
125
143
  -e "s|import { \\(.*\\)toast\\(.*\\)} from '@devalok/shilp-sutra/ui'|import { toast } from '@devalok/shilp-sutra/ui/toast'\\nimport { \\1\\2} from '@devalok/shilp-sutra/ui'|"
126
144
  ```
127
145
 
128
- (Adjust per project — the regex assumes a single `toast` import on the line. For multi-symbol lines, splitting by hand is faster than perfecting the regex.)
146
+ (Adjust per project — the regex assumes a single `toast` import on the line. For multi-symbol lines, the ESLint autofix above is far more reliable.)
147
+
148
+ </details>
129
149
 
130
150
  #### Per-chart subpaths added (non-breaking)
131
151
 
package/README.md CHANGED
@@ -32,7 +32,7 @@ pnpm add sonner
32
32
  ```ts
33
33
  // next.config.ts
34
34
  export default {
35
- transpilePackages: ['@devalok/shilp-sutra', '@devalok/shilp-sutra-brand'],
35
+ transpilePackages: ['@devalok/shilp-sutra'],
36
36
  }
37
37
  ```
38
38
 
@@ -31,6 +31,7 @@ If the framework is not listed, start with [install-vite.md](./install-vite.md)
31
31
  |---|---|
32
32
  | [customize-brand.md](./customize-brand.md) | Token override cookbook — colors, radius role tokens, `[data-shape]` presets, fonts, spacing scale, dark-mode mapping. **Start here**, or skip ahead to the Themer ↓ |
33
33
  | [server-components.md](./server-components.md) | Per-component RSC-safety matrix; correct import patterns for Server Components |
34
+ | [upgrading.md](./upgrading.md) | How to bump versions safely — read the full breaking surface, find affected call sites, autofix with the ESLint plugin, typecheck before deploy |
34
35
 
35
36
  ### The fast path: Themer
36
37
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  > Setup recipe for adding `@devalok/shilp-sutra` to a Next.js 13+ App Router project.
4
4
 
5
+ > **Tested cold-install on:** Next 16.2.6 + React 19.2 + Turbopack + pnpm 10.30 + Node 22 + Windows 11 (2026-05-25). Earlier versions of Next 13/14/15 are still supported on this recipe; deltas called out inline.
6
+
5
7
  ## 1. Detect the framework
6
8
 
7
9
  You are in this recipe if **all** of these are true:
@@ -40,12 +42,6 @@ Add only if you will render `<Toaster />`:
40
42
  pnpm add sonner
41
43
  ```
42
44
 
43
- Add brand assets package if you need Devalok or Karm logos:
44
-
45
- ```bash
46
- pnpm add @devalok/shilp-sutra-brand
47
- ```
48
-
49
45
  ### 2a. Optional peer dependencies (install ONLY when importing the matching subpath)
50
46
 
51
47
  Some components depend on third-party libraries that ship as optional peers. **Install BEFORE first import** of the matching component, or `next build` will exit with `Module not found`. Skip entirely if you only use core components (`Button`, `Text`, `Stack`, `Dialog`, `Toast`, `Form*`, `Input`, `Card`, etc.).
@@ -65,27 +61,35 @@ Some components depend on third-party libraries that ship as optional peers. **I
65
61
 
66
62
  ## 3. Configure PostCSS
67
63
 
68
- Create or update `postcss.config.mjs` at the project root:
64
+ **Next 14+ scaffolds this for you.** Verify `postcss.config.mjs` (or `.js` / `.cjs` / `.json`) at the project root contains:
69
65
 
70
66
  ```js
71
- export default {
67
+ const config = {
72
68
  plugins: {
73
69
  "@tailwindcss/postcss": {},
74
70
  },
75
71
  };
72
+
73
+ export default config;
76
74
  ```
77
75
 
78
- If a `postcss.config.{js,cjs,json}` already exists, merge the plugin in. Do not delete the existing file.
76
+ If the file is missing, create it with the contents above. If another PostCSS file exists with different plugins, merge `@tailwindcss/postcss` in do not delete the existing file.
79
77
 
80
78
  ## 4. Wire Tailwind 4 + design tokens
81
79
 
82
- Locate the global CSS file. Common paths in priority order:
80
+ ### 4a. Locate the global CSS file
81
+
82
+ Next 13+ default location depends on the `--src-dir` choice at scaffold time:
83
+
84
+ - `src/app/globals.css` — default since Next 14 when scaffolded without `--src-dir false`. **Most common in Next 16+ defaults.**
85
+ - `app/globals.css` — when scaffolded with `--src-dir false` (no `src/`).
86
+ - `app/global.css` — older Next 13 scaffolds.
83
87
 
84
- - `app/globals.css`
85
- - `src/app/globals.css`
86
- - `app/global.css`
88
+ If none exists, create at whichever path matches the project's existing `app/` location.
87
89
 
88
- If none exists, create `app/globals.css`. Set the file contents to (or merge into):
90
+ ### 4b. Replace the scaffold's CSS with the shilp-sutra setup
91
+
92
+ A fresh `create-next-app` scaffold writes a `globals.css` with `:root` color vars, an `@theme inline` block linked to Geist font vars, a `prefers-color-scheme` dark override, and a `body { font-family: Arial }` block. **Replace the entire file** with the shilp-sutra setup unless you have a specific reason to keep scaffold styles:
89
93
 
90
94
  ```css
91
95
  @import "tailwindcss";
@@ -94,7 +98,11 @@ If none exists, create `app/globals.css`. Set the file contents to (or merge int
94
98
 
95
99
  **Order matters.** `tailwindcss` MUST come first. The shilp-sutra `/css` entry registers `@theme` blocks that the Tailwind import must process.
96
100
 
97
- If the project has its own theme overrides, place them AFTER both imports:
101
+ If you want to keep the scaffold's color/font vars alongside shilp-sutra tokens (rare — usually you want one or the other), put scaffold's `@theme inline` / `:root` / `body` blocks AFTER the shilp-sutra import. Otherwise the scaffold's `body { font-family: Arial }` will compete with shilp-sutra's font setup.
102
+
103
+ ### 4c. Optional: add your own theme overrides
104
+
105
+ Place after both imports:
98
106
 
99
107
  ```css
100
108
  @import "tailwindcss";
@@ -105,12 +113,16 @@ If the project has its own theme overrides, place them AFTER both imports:
105
113
  }
106
114
  ```
107
115
 
108
- Import the CSS file once from `app/layout.tsx` (it should already be imported in a fresh `create-next-app` project):
116
+ ### 4d. Ensure CSS is imported from the layout
117
+
118
+ The CSS import in `app/layout.tsx` (or `src/app/layout.tsx`) should already exist in a fresh scaffold:
109
119
 
110
120
  ```tsx
111
121
  import "./globals.css";
112
122
  ```
113
123
 
124
+ If you removed the Geist-font import from layout.tsx (see § 6), keep this CSS import line.
125
+
114
126
  ## 5. Configure `transpilePackages`
115
127
 
116
128
  Edit `next.config.{ts,js,mjs}`. Add the `transpilePackages` field:
@@ -119,7 +131,7 @@ Edit `next.config.{ts,js,mjs}`. Add the `transpilePackages` field:
119
131
  import type { NextConfig } from "next";
120
132
 
121
133
  const nextConfig: NextConfig = {
122
- transpilePackages: ["@devalok/shilp-sutra", "@devalok/shilp-sutra-brand"],
134
+ transpilePackages: ["@devalok/shilp-sutra"],
123
135
  };
124
136
 
125
137
  export default nextConfig;
@@ -129,6 +141,8 @@ If a `transpilePackages` array already exists, append to it. Do not replace.
129
141
 
130
142
  Without `transpilePackages`, Next will refuse to load our pre-built `dist/*.js` because it ships native ESM that does not match Next's CJS-leaning loader for `node_modules`.
131
143
 
144
+ **Turbopack:** as of Next 16, Turbopack is the default bundler (`next dev` and `next build`). `transpilePackages` is respected by both Turbopack and the Webpack backend. Tested on Turbopack 16.2 cold-install (2026-05-25); no extra config needed.
145
+
132
146
  ## 6. Scaffold the Providers wrapper
133
147
 
134
148
  Create `app/providers.tsx`:
@@ -155,13 +169,21 @@ export function Providers({ children }: { children: ReactNode }) {
155
169
  - Drop the `Toaster` import and its JSX usage
156
170
  - Skip installing `sonner`
157
171
 
158
- Mount `<Providers>` from `app/layout.tsx`:
172
+ Mount `<Providers>` from `app/layout.tsx` (or `src/app/layout.tsx`). **Replace the scaffold's layout** with the version below — the scaffold imports `next/font/google` (Geist) and applies font-variable classes to `<html>`, which you don't need when shilp-sutra ships its own fonts:
159
173
 
160
174
  ```tsx
175
+ import type { Metadata } from "next";
161
176
  import "./globals.css";
162
177
  import { Providers } from "./providers";
163
178
 
164
- export default function RootLayout({ children }: { children: React.ReactNode }) {
179
+ export const metadata: Metadata = {
180
+ title: "Your app",
181
+ description: "Built with shilp-sutra",
182
+ };
183
+
184
+ export default function RootLayout({
185
+ children,
186
+ }: Readonly<{ children: React.ReactNode }>) {
165
187
  return (
166
188
  <html lang="en" suppressHydrationWarning>
167
189
  <body>
@@ -172,11 +194,20 @@ export default function RootLayout({ children }: { children: React.ReactNode })
172
194
  }
173
195
  ```
174
196
 
197
+ Specifically, **remove these scaffold lines**:
198
+
199
+ - `import { Geist, Geist_Mono } from "next/font/google";`
200
+ - The `const geistSans = Geist({...})` and `const geistMono = Geist_Mono({...})` blocks
201
+ - The `className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}` on `<html>`
202
+ - The `className="min-h-full flex flex-col"` on `<body>`
203
+
204
+ If you want to keep Geist alongside shilp-sutra's fonts, leave the `Geist` imports — but know that shilp-sutra's `font-sans` token resolves to Inter (body) and Ranade (display), not to the scaffold's `--font-geist-sans`. Loading both is wasted bytes.
205
+
175
206
  `suppressHydrationWarning` on `<html>` is required because `next-themes` writes the `class` attribute before React hydrates. Without it, every page logs a hydration warning.
176
207
 
177
208
  ## 7. Verify the install
178
209
 
179
- Replace the contents of `app/page.tsx`:
210
+ Replace the contents of `app/page.tsx` (or `src/app/page.tsx`). The scaffold's `page.tsx` imports `next/image` and renders the Vercel marketing layout — replace the whole file:
180
211
 
181
212
  ```tsx
182
213
  import { Button } from "@devalok/shilp-sutra/ui/button";
@@ -215,6 +246,7 @@ If anything is off, see [troubleshoot.md](./troubleshoot.md).
215
246
  ## 8. Common gotchas
216
247
 
217
248
  - **CSS import order.** `tailwindcss` BEFORE `@devalok/shilp-sutra/css`. Reversing the order silently produces a build with no design-system utilities.
249
+ - **Scaffold's `body { font-family: Arial }` overrides shilp-sutra fonts.** The `create-next-app` `globals.css` template sets `font-family: Arial, Helvetica, sans-serif` on `<body>`. If you kept the scaffold's body styles, they win the cascade over shilp-sutra's `font-sans`. § 4b says to replace the whole file — follow it.
218
250
  - **Multiple `framer-motion` copies.** Run `pnpm why framer-motion`. If it shows more than one resolved version, contexts (`MotionConfig`, `LayoutGroup`, `AnimatePresence`) silently break. Fix:
219
251
  ```jsonc
220
252
  // package.json
@@ -228,7 +260,9 @@ If anything is off, see [troubleshoot.md](./troubleshoot.md).
228
260
  ```
229
261
  For npm/yarn/bun equivalents, see [troubleshoot.md](./troubleshoot.md).
230
262
  - **Per-component imports keep RSC fast AND avoid peer-dep cliffs.** Inside Server Components, prefer `@devalok/shilp-sutra/ui/text`, `…/composed/page-header`, etc. The barrel `@devalok/shilp-sutra/ui` re-exports many client components — including some with hard peer-dep imports (e.g. `input-otp`) — so it both inflates the client bundle and forces those peers to be installed even when you never render those components. See [server-components.md](./server-components.md) for the full RSC-safety matrix.
231
- - **`p-3` vs `p-ds-03` — both are valid.** DS spacing uses the `--spacing-ds-*` namespace (`p-ds-04`, `gap-ds-03`); Tailwind 4's default numeric scale (`p-4`, `gap-2`) coexists by design. Pick `p-ds-*` for values that should track DS theme changes (card padding, form gaps); pick `p-N` for one-off layout values (section breathing room). Do NOT mass-codemod `p-4` → `p-ds-04` — that is not what the package intends.
263
+ - **`p-3` vs `p-ds-03` — both are valid.** DS spacing uses the `--spacing-ds-*` namespace (`p-ds-04`, `gap-ds-03`); Tailwind 4's default numeric scale (`p-4`, `gap-2`) coexists by design. Pick `p-ds-*` for values that should track DS theme changes (card padding, form gaps); pick `p-N` for one-off layout values (section breathing room). Do NOT mass-codemod `p-4` → `p-ds-04` — that is not what the package intends. For layout rhythm, pick a 3-tier cadence (`ds-03` related / `ds-05` grouped / `ds-07` section), not every adjacent token.
264
+ - **Auto-generated `pnpm-workspace.yaml`.** `pnpm 10+` writes a `pnpm-workspace.yaml` at the project root on first install with `ignoredBuiltDependencies` entries. This is harmless for a standalone app, but if you're nesting this project inside a larger monorepo, delete this file and use the parent monorepo's workspace config instead.
265
+ - **Auto-generated `AGENTS.md`.** `create-next-app` writes an `AGENTS.md` with managed `<!-- BEGIN:nextjs-agent-rules -->` / `<!-- END:nextjs-agent-rules -->` markers. Shilp Sutra's agent rules use `<!-- BEGIN:shilp-sutra-agent-rules -->` markers — they coexist cleanly. If you install the shilp-sutra Agent Skill (see the repo root `AGENTS.md` for the one-liner), it adds its block alongside Next's, not over it.
232
266
  - **Bare `shadow` is dead.** Tailwind 4 has no `--shadow-DEFAULT`. Use `shadow-raised`, `shadow-overlay`, or `shadow-floating`.
233
267
 
234
268
  ## 9. What you should NOT do
@@ -92,11 +92,9 @@ After editing, delete the lockfile + `node_modules` and reinstall.
92
92
  Add:
93
93
 
94
94
  ```ts
95
- transpilePackages: ["@devalok/shilp-sutra", "@devalok/shilp-sutra-brand"],
95
+ transpilePackages: ["@devalok/shilp-sutra"],
96
96
  ```
97
97
 
98
- If `@devalok/shilp-sutra-brand` is not installed, list only `@devalok/shilp-sutra`.
99
-
100
98
  ## Symptom: Build error `Cannot find module 'sonner' / 'input-otp' / 'date-fns' / '@tiptap/react' / 'react-pdf' / 'react-markdown' / '@emoji-mart/react'`
101
99
 
102
100
  **Diagnosis:** an optional peer dependency is missing. Each component below has a peer it pulls only when imported. Install the matching peer (always BEFORE the first import):
@@ -117,6 +115,8 @@ If `@devalok/shilp-sutra-brand` is not installed, list only `@devalok/shilp-sutr
117
115
 
118
116
  These ship as **optional** peers so consumers who never render the matching component don't pay the install cost. Once you import the component, the peer becomes required. Each affected component's JSDoc carries the same install hint — hover the import in your editor to see it inline.
119
117
 
118
+ **Catch this at edit time, not build time:** install `@devalok/eslint-plugin-shilp-sutra` (`pnpm add -D @devalok/eslint-plugin-shilp-sutra`, then `shilpSutra.configs['flat/recommended']`). Its `prefer-per-component-import` rule flags peer-cliff symbols imported from a barrel and autofixes the path — surfacing the cliff in your editor before the bundler ever fails.
119
+
120
120
  For the full table in your framework's install recipe, see `install-<framework>.md → §2a. Optional peer dependencies`.
121
121
 
122
122
  ## Symptom: Hydration warning on every page load (Next.js)