jsx_rosetta 0.5.1 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +128 -11
- data/CLAUDE.md +70 -0
- data/README.md +50 -0
- data/agents/jsx-rosetta-resolve-todo-file.md +90 -0
- data/lib/jsx_rosetta/ast/inflector.rb +17 -0
- data/lib/jsx_rosetta/backend/phlex.rb +1078 -77
- data/lib/jsx_rosetta/backend/rails_view.rb +1 -1
- data/lib/jsx_rosetta/backend/view_component/expression_translator.rb +73 -20
- data/lib/jsx_rosetta/backend/view_component.rb +48 -2
- data/lib/jsx_rosetta/cli.rb +175 -37
- data/lib/jsx_rosetta/icons/lucide.json +37 -0
- data/lib/jsx_rosetta/icons.rb +44 -0
- data/lib/jsx_rosetta/ir/lowering.rb +720 -31
- data/lib/jsx_rosetta/ir/radix_registry.rb +84 -0
- data/lib/jsx_rosetta/ir/types.rb +187 -3
- data/lib/jsx_rosetta/ir.rb +5 -4
- data/lib/jsx_rosetta/pages_routing.rb +640 -0
- data/lib/jsx_rosetta/version.rb +1 -1
- data/lib/jsx_rosetta.rb +8 -6
- data/plans/nextjs_pages_to_rails.md +200 -0
- data/plans/nextjs_pages_to_rails_slice_2.md +118 -0
- data/plans/nextjs_pages_to_rails_slice_3.md +121 -0
- data/plans/nextjs_pages_to_rails_slice_4.md +301 -0
- data/plans/translator_widening_and_pages_followups.md +120 -0
- data/plans/translator_widening_slice_a.md +208 -0
- data/skills/jsx-rosetta-resolve-todos/SKILL.md +206 -0
- data/skills/jsx-rosetta-resolve-todos/data/design_tokens.template.yml +71 -0
- data/skills/jsx-rosetta-resolve-todos/data/target_app_conventions.template.yml +107 -0
- data/skills/jsx-rosetta-resolve-todos/examples/design_tokens.ant_design_v5.yml +190 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/01_design_tokens.md +74 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/02_promoted_ivar.md +49 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/03_react_hooks.md +34 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/04_apollo_hooks.md +34 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/05_event_handlers.md +45 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/06_module_constants.md +29 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/07_nextjs_navigation.md +44 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/08_generic_js_bailouts.md +55 -0
- data/skills/jsx-rosetta-resolve-todos/tools/apply_promoted_ivar.rb +189 -0
- data/skills/jsx-rosetta-resolve-todos/tools/apply_substitutions.rb +292 -0
- data/skills/jsx-rosetta-resolve-todos/tools/diff_corpus.rb +161 -0
- data/skills/jsx-rosetta-resolve-todos/tools/discover_bailouts.rb +211 -0
- metadata +29 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jsx-rosetta-resolve-todos
|
|
3
|
+
description: Resolve the `# TODO:` comments left by jsx_rosetta in generated Phlex / ViewComponent files. Mechanical substitution for known patterns (design tokens, prop-passing reminders); LLM-driven recipes for hooks, data-fetching, event handlers, navigation, and module constants; sharpened-TODO emission for everything else.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# jsx-rosetta-resolve-todos
|
|
7
|
+
|
|
8
|
+
`jsx_rosetta` translates JSX/TSX into Phlex / ViewComponent / ERB and is intentionally conservative: when an expression can't be safely lowered, it preserves the original JS verbatim inside a `# TODO:` comment. This skill is the last-mile companion for converting that output into shippable Rails code.
|
|
9
|
+
|
|
10
|
+
It does **not** speculate. The hard rule, inherited from the gem itself: prefer leaving a sharper TODO over guessing wrong. Three actions per TODO:
|
|
11
|
+
|
|
12
|
+
- **resolve** — apply a known transformation, delete the TODO
|
|
13
|
+
- **sharpen** — replace a verbose TODO with a tighter one a human can clear in seconds
|
|
14
|
+
- **escalate** — leave untouched, surface in the report
|
|
15
|
+
|
|
16
|
+
## Per-file workflow
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
1. Run `ruby -c <file>` to confirm the file parses before any edits.
|
|
20
|
+
2. Walk TODOs top-to-bottom. Classify each against the routing table below.
|
|
21
|
+
3. Dispatch to the matching recipe. Take exactly one of {resolve, sharpen, escalate}.
|
|
22
|
+
4. Re-run `ruby -c`. If parsing breaks, REVERT and report `parse_failed` —
|
|
23
|
+
never ship a half-edit.
|
|
24
|
+
5. Emit a JSON line with category counts.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The mechanical pre-passes (design tokens, promoted-@ivar reminders) run as standalone scripts with no LLM involved. Run them first; they're fast, safe (always `ruby -c`-validated before write), and remove a meaningful chunk of TODOs before any agent dispatch happens.
|
|
28
|
+
|
|
29
|
+
## Routing table
|
|
30
|
+
|
|
31
|
+
The TODO comments emitted by `jsx_rosetta` follow stable shapes. Routing is regex-based:
|
|
32
|
+
|
|
33
|
+
| TODO regex | Recipe | Default action | Backing |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `# TODO: (attribute\|style declaration) "X" dropped — couldn't translate: <RHS>` (RHS matches a configured token regex) | `recipes/01_design_tokens.md` | resolve via `tools/apply_substitutions.rb` | **script** |
|
|
36
|
+
| `# TODO: render condition references binding\(s\) promoted to @ivar` | `recipes/02_promoted_ivar.md` | resolve or sharpen via `tools/apply_promoted_ivar.rb` | **script** |
|
|
37
|
+
| `# TODO: React hooks detected` | `recipes/03_react_hooks.md` | sharpen — sub-classify hook flavor | docs only |
|
|
38
|
+
| `# TODO: Apollo data-fetching hooks detected` | `recipes/04_apollo_hooks.md` | sharpen — extract query name + variables | docs only |
|
|
39
|
+
| `# TODO: translate the original JSX `<event>` handler` | `recipes/05_event_handlers.md` | sharpen — classify behavioral vs mutation | docs only |
|
|
40
|
+
| `# TODO: module-level constants` | `recipes/06_module_constants.md` | dispatch by sub-type | docs only |
|
|
41
|
+
| `# TODO: Next.js navigation hooks detected` | `recipes/07_nextjs_navigation.md` | sharpen — extract route + params | docs only |
|
|
42
|
+
| `# TODO: translate JS to Ruby — original:` | `recipes/08_generic_js_bailouts.md` | always sharpen | docs only |
|
|
43
|
+
| `# TODO: (attribute\|style declaration) "X" dropped` (RHS doesn't match any token regex) | `recipes/08_generic_js_bailouts.md` | always sharpen | docs only |
|
|
44
|
+
|
|
45
|
+
**Backing column legend:**
|
|
46
|
+
- **script** — pure-Ruby tool ships with the skill. No LLM required; safe to run unattended.
|
|
47
|
+
- **docs only** — recipe text is usable today by an LLM agent (e.g. via `jsx-rosetta-resolve-todo-file`). No mechanical tool yet; sub-classification and sharpening are agent work.
|
|
48
|
+
|
|
49
|
+
When in doubt, sharpen.
|
|
50
|
+
|
|
51
|
+
## Tools
|
|
52
|
+
|
|
53
|
+
### `tools/discover_bailouts.rb`
|
|
54
|
+
|
|
55
|
+
Pure-Ruby corpus scanner. Tallies dropped-expression RHS values, finds repeating member chains (`<root>.<member>...`), and suggests a `match:` regex per cluster. Use it to decide what's worth automating before you build any tables or recipes.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ruby tools/discover_bailouts.rb [--top N] [--json] [--all-todos] <file_or_dir>...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Output identifies the dominant "roots" in your corpus (`token`, `theme`, `vars`, etc.) — these are your candidate design-system / config namespaces. The script makes no claim about what they *mean*; that's your call.
|
|
62
|
+
|
|
63
|
+
### `tools/apply_substitutions.rb`
|
|
64
|
+
|
|
65
|
+
Pure-Ruby mechanical substitution. Reads a YAML config that declares a `match:` regex and a `tokens:` map; finds matching TODOs, splices values into the `render Foo.new(...)` immediately below.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
ruby tools/apply_substitutions.rb --config <yaml> [--dry-run] [--quiet] <file_or_dir>...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Conservative by design:
|
|
72
|
+
|
|
73
|
+
- Only single-line `render Foo.new(...)` calls
|
|
74
|
+
- String-literal `style:` attribute (or absent); hash-form `style: { ... }` is skipped without false edits
|
|
75
|
+
- Skips drops whose attribute name would produce an invalid Ruby identifier
|
|
76
|
+
- Pre-write `ruby -c` validation; on failure, the file is left untouched and the run is reported as `parse_failed`
|
|
77
|
+
|
|
78
|
+
See `examples/design_tokens.ant_design_v5.yml` for a full reference config (Ant Design v5 defaults, ~85 tokens). See `data/design_tokens.template.yml` for a blank schema you can fill in for your own design system.
|
|
79
|
+
|
|
80
|
+
### `tools/apply_promoted_ivar.rb`
|
|
81
|
+
|
|
82
|
+
Pure-Ruby resolution of the `# TODO: render condition references binding(s) promoted to @ivar` reminders. Reads each file's `def initialize` signature and either deletes the TODO (when every named prop is already in the signature) or sharpens it to a tagged single-liner naming exactly what's missing — distinguishing missing controller props from missing PascalCase imports.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
ruby tools/apply_promoted_ivar.rb [--dry-run] [--quiet] <file_or_dir>...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Idempotent: safe to re-run after wiring more controllers.
|
|
89
|
+
|
|
90
|
+
### Recipe content (`recipes/*.md`)
|
|
91
|
+
|
|
92
|
+
Each recipe describes:
|
|
93
|
+
- The TODO shape it handles
|
|
94
|
+
- The transformation rule (resolve) or the sharpened-TODO template (sharpen)
|
|
95
|
+
- What information to grep from the host Rails app
|
|
96
|
+
- When to escalate
|
|
97
|
+
|
|
98
|
+
Recipes are loaded by the `jsx-rosetta-resolve-todo-file` agent (one per file) for fan-out work, and by the human running the skill in interactive mode.
|
|
99
|
+
|
|
100
|
+
## Sharpened-TODO format
|
|
101
|
+
|
|
102
|
+
A sharpened TODO must include:
|
|
103
|
+
|
|
104
|
+
1. **What** the source was doing in one phrase — no JS dump.
|
|
105
|
+
2. **Where** the resolution belongs in Rails (controller / helper / Stimulus / view).
|
|
106
|
+
3. **What's missing** — the unanswered question that prevents resolution.
|
|
107
|
+
4. The original JS preserved in an indented `# Original:` block below.
|
|
108
|
+
|
|
109
|
+
Template:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# TODO[<category>]: <one-line what>. <where it belongs>. <what's needed>.
|
|
113
|
+
# Original:
|
|
114
|
+
# <verbatim JS, indented>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Example transformation. Before:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# TODO: React hooks detected. None translate automatically.
|
|
121
|
+
# Hotwire/Stimulus handles behavior; controllers/views handle state;
|
|
122
|
+
# turbo-frames handle async loading. Original source:
|
|
123
|
+
# const [open, setOpen] = useState(false);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
After:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
# TODO[useState]: ephemeral UI state `open`. Move to Stimulus controller value: { open: Boolean }.
|
|
130
|
+
# Original:
|
|
131
|
+
# const [open, setOpen] = useState(false);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The `[<category>]` tag is grep-friendly — humans can filter by tag to batch similar decisions.
|
|
135
|
+
|
|
136
|
+
## Modes
|
|
137
|
+
|
|
138
|
+
### Single-file (interactive)
|
|
139
|
+
|
|
140
|
+
The skill is invoked on a specific file. Workflow runs in the main conversation context. Use when reviewing the result yourself or when the file has unusual shape that the recipes won't handle cleanly.
|
|
141
|
+
|
|
142
|
+
### Batch (fan-out)
|
|
143
|
+
|
|
144
|
+
The skill walks a directory and dispatches one `jsx-rosetta-resolve-todo-file` agent per file (or per chunk). Each agent loads the recipes from this skill's directory, processes its file, and emits a JSON report. The skill aggregates reports into a corpus-level summary.
|
|
145
|
+
|
|
146
|
+
Recommended order in batch mode:
|
|
147
|
+
|
|
148
|
+
1. **Discovery pass** (`discover_bailouts.rb --json`) — surface what's worth mapping.
|
|
149
|
+
2. **Mechanical pre-passes** (`apply_substitutions.rb` for each design-system YAML you have, plus `apply_promoted_ivar.rb`) — strip out the trivially-mechanical TODOs first. No LLM, no agent.
|
|
150
|
+
3. **Agent fan-out** — for what's left, one `jsx-rosetta-resolve-todo-file` worker per file. Workers are tool-restricted (Read, Edit, Bash for `ruby -c`, Grep) and stateless. Sonnet 4.6 is the right tier; Opus is overkill for recipe application; Haiku is too light for the surrounding-code reading the recipes need.
|
|
151
|
+
4. **Re-run discovery** to confirm what was resolved and what's left to escalate to a human.
|
|
152
|
+
|
|
153
|
+
## Configuration the user owns
|
|
154
|
+
|
|
155
|
+
This skill ships with no app-specific assumptions. The files you (the consuming user) own:
|
|
156
|
+
|
|
157
|
+
- `data/design_tokens.yml` (or any other YAML you point `apply_substitutions.rb` at). Created from `data/design_tokens.template.yml` or copied from `examples/`.
|
|
158
|
+
- `data/target_app_conventions.yml` — paths, naming, CSS strategy. Recipes 03–07 read this to know where extracted helpers, controllers, and Stimulus controllers belong in your Rails app. Without it, recipes sharpen with `<TBD: see target_app_conventions.yml>` rather than guessing. Copy `data/target_app_conventions.template.yml` to seed it.
|
|
159
|
+
|
|
160
|
+
Anything in `data/` other than `*.template.*` should be `.gitignored` in the consuming repo if it contains overrides specific to that app's theme or conventions.
|
|
161
|
+
|
|
162
|
+
## Scope
|
|
163
|
+
|
|
164
|
+
This skill addresses the corpus of `# TODO:` comments that `jsx_rosetta` emits. It does not:
|
|
165
|
+
|
|
166
|
+
- Re-translate JSX (that's `jsx_rosetta` itself)
|
|
167
|
+
- Touch business logic
|
|
168
|
+
- Make architectural decisions about how a React app should map to Rails (controllers vs. service objects, Turbo Frames vs. plain links, etc.) — those are flagged for human review via sharpened TODOs
|
|
169
|
+
|
|
170
|
+
Effort expectation: a meaningful fraction of TODOs reflect genuine human-judgment decisions and will not auto-resolve under any system. The skill's value is auto-resolving the mechanical chunk and compressing the rest into single-decision items. See "Validation" below for measured impact on the gem's own stress corpus.
|
|
171
|
+
|
|
172
|
+
## Validation
|
|
173
|
+
|
|
174
|
+
Measured against the `jsx_rosetta` gem's own Phlex stress corpus (1,245 generated `.rb` files, 4,846 `# TODO:` comments) using `tools/diff_corpus.rb`. The corpus represents a *fresh translation* — controllers haven't been wired yet, so promoted-ivar TODOs sharpen rather than resolve.
|
|
175
|
+
|
|
176
|
+
| Pass | Files modified | TODOs resolved | TODOs sharpened | Parse failures |
|
|
177
|
+
|---|---|---|---|---|
|
|
178
|
+
| `apply_substitutions.rb --config examples/design_tokens.ant_design_v5.yml` | 158 / 1,245 | 336 | 0 | 0 / 1,245 |
|
|
179
|
+
| `apply_promoted_ivar.rb` | 333 / 1,245 | 0 (corpus state) | 665 | 0 / 1,245 |
|
|
180
|
+
| **Combined mechanical pre-pass** | **428 / 1,245** | **336 deleted** | **665 compressed** | **0 / 1,245** |
|
|
181
|
+
|
|
182
|
+
Net change: 4,846 → 4,510 TODO comments. Of the 1,001 TODOs the mechanical passes touched:
|
|
183
|
+
|
|
184
|
+
- 336 were deleted outright (Ant Design token references → literal values spliced into render calls)
|
|
185
|
+
- 665 were compressed from verbose multi-line reminders into tagged single-liners (`# TODO[promoted_ivar]: controller must pass <names>...`) with explicit "missing prop" vs "missing import" verdicts
|
|
186
|
+
|
|
187
|
+
The 117 unaddressed token TODOs are conservative bailouts (multi-line render shapes, hash-form `style:`, complex RHS expressions) that the substitution script intentionally skips rather than risk breaking output.
|
|
188
|
+
|
|
189
|
+
What's *not* measured here:
|
|
190
|
+
|
|
191
|
+
- The LLM-driven recipes (03–08) — they ship as documented intentions; their resolve/sharpen rate depends on a live conversion to validate against.
|
|
192
|
+
- The remaining ~3,500 TODOs are concentrated in categories (`react_hooks` 378, `event_handler` 428, `module_constants` 355, `apollo_hooks` 222, `nextjs_navigation` 105, generic JS bailouts 1,053) where the recipes default to *sharpening* — measurable as compression once an agent runs the recipes on a real corpus.
|
|
193
|
+
|
|
194
|
+
Reproduce with:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
ruby tools/diff_corpus.rb <baseline_dir> <after_pipeline_dir>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Future work
|
|
201
|
+
|
|
202
|
+
Tracked here so the skill's roadmap is visible alongside its current shape:
|
|
203
|
+
|
|
204
|
+
- **Cross-file context for fan-out workers.** Today each `jsx-rosetta-resolve-todo-file` worker is stateless — the same gql query referenced from 5 files would sharpen identically 5 times. A pre-pass that builds a project-wide "discovery digest" (recurring queries, recurring helpers, controller signatures already seen) and feeds it to each worker would let the corpus learn once. Hard to design ahead of real-conversion evidence; bolt-on later.
|
|
205
|
+
- **Drift-detection spec.** The TODO regexes in this skill's recipes need to stay in sync with the strings emitted by `lib/jsx_rosetta/backend/phlex.rb`. One round-trip test per category (fixture → translate → resolve-todos → assert N matched) would catch silent emit-format drift. Worth doing once more recipes are backed by scripts; testing stub recipes is testing a sketch.
|
|
206
|
+
- **Backing scripts for recipes 03–08.** Some sub-classifications (e.g. recipe 03's hook → Stimulus mapping for unambiguous shapes like `useState(false)`) may be mechanical enough to ship as scripts. Others (event handler classification, gql operation extraction) probably stay LLM-driven. The split will become clear after running the recipes on a real conversion.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# design_tokens.template.yml — schema for apply_substitutions.rb config.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to your consuming app, e.g.:
|
|
4
|
+
# <your-app>/.claude/skills/jsx-rosetta-resolve-todos/data/design_tokens.yml
|
|
5
|
+
# and fill in the entries that apply to your source codebase.
|
|
6
|
+
#
|
|
7
|
+
# Workflow to populate it:
|
|
8
|
+
# 1. Run discover_bailouts.rb on your jsx_rosetta-generated output to find
|
|
9
|
+
# the dominant repeating member chains. It will print a suggested
|
|
10
|
+
# `match:` regex and the top capture keys.
|
|
11
|
+
# 2. Paste the suggested regex into `match:` below.
|
|
12
|
+
# 3. For each top key, look up what it resolves to in your source codebase
|
|
13
|
+
# (the design system's published defaults, your theme overrides, your
|
|
14
|
+
# Tailwind config, etc.) and add an entry to `tokens:`.
|
|
15
|
+
# 4. Run apply_substitutions.rb --config <this file> on the corpus.
|
|
16
|
+
# 5. Re-run discover_bailouts.rb to confirm the count dropped, and to
|
|
17
|
+
# surface the next worthwhile cluster to map.
|
|
18
|
+
#
|
|
19
|
+
# Pre-built reference tables for common design systems live in the skill's
|
|
20
|
+
# `examples/` directory. If your source uses one of them with default
|
|
21
|
+
# theming, you can use that file directly instead of building this one.
|
|
22
|
+
|
|
23
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
# Schema
|
|
25
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
#
|
|
27
|
+
# match
|
|
28
|
+
# A regex string with exactly one capture group. The capture is used as the
|
|
29
|
+
# lookup key against `tokens:`. Examples:
|
|
30
|
+
# 'token\.(\w+)' → Ant Design tokens (`token.colorText`)
|
|
31
|
+
# 'theme\.palette\.(\w+)\.main' → MUI palette mains (`theme.palette.primary.main`)
|
|
32
|
+
# 'vars\.colors\.(\w+)' → vanilla-extract color vars
|
|
33
|
+
# 'tokens\.(\w+)' → Chakra UI tokens
|
|
34
|
+
#
|
|
35
|
+
# tokens
|
|
36
|
+
# Map from capture-group string → entry hash. Each entry:
|
|
37
|
+
#
|
|
38
|
+
# value The literal substitution. Type-sensitive:
|
|
39
|
+
# Integer → rendered as "<n>px" in inline style declarations,
|
|
40
|
+
# and as a bare integer in numeric component kwargs.
|
|
41
|
+
# String → used verbatim. Wrap CSS values, hex colors,
|
|
42
|
+
# rgba(), font-family lists, etc.
|
|
43
|
+
# null → explicitly skip auto-resolution. Use for entries
|
|
44
|
+
# you've intentionally chosen NOT to substitute
|
|
45
|
+
# (component-namespace tokens, ambiguous shorthand,
|
|
46
|
+
# values that depend on runtime context). The TODO
|
|
47
|
+
# stays in place for human review.
|
|
48
|
+
#
|
|
49
|
+
# tailwind (optional) Suggested utility class for a className-based
|
|
50
|
+
# refactor pass. Not used by apply_substitutions.rb directly;
|
|
51
|
+
# available as documentation for downstream recipes that want
|
|
52
|
+
# to convert inline styles to Tailwind utilities.
|
|
53
|
+
#
|
|
54
|
+
# category (optional) A grouping label for your own organization
|
|
55
|
+
# (e.g. "spacing", "color-text"). Not interpreted by tools.
|
|
56
|
+
#
|
|
57
|
+
# notes (optional) Free text for human readers — caveats, ambiguity,
|
|
58
|
+
# "verify per usage" reminders, etc.
|
|
59
|
+
|
|
60
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
# Fill in below
|
|
62
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
match: '' # e.g. 'token\.(\w+)'
|
|
65
|
+
|
|
66
|
+
tokens: {}
|
|
67
|
+
# exampleKey:
|
|
68
|
+
# value: 4
|
|
69
|
+
# tailwind: "p-1"
|
|
70
|
+
# category: spacing
|
|
71
|
+
# notes: "Ant Design v5 default 4px base."
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# target_app_conventions.template.yml
|
|
2
|
+
#
|
|
3
|
+
# Conventions of the consuming Rails application. The LLM-driven recipes
|
|
4
|
+
# (03–07) read this file to know where extracted helpers, controllers, and
|
|
5
|
+
# Stimulus controllers belong, and to know how the app is structured. Without
|
|
6
|
+
# values here, recipes sharpen with `<TBD: see target_app_conventions.yml>`
|
|
7
|
+
# rather than guessing.
|
|
8
|
+
#
|
|
9
|
+
# Copy this file to your consuming app's
|
|
10
|
+
# .claude/skills/jsx-rosetta-resolve-todos/data/target_app_conventions.yml
|
|
11
|
+
# and fill in the values that apply. Leave a key as `null` (or omit it) if
|
|
12
|
+
# your app doesn't use that surface — recipes will treat it as unavailable
|
|
13
|
+
# and escalate or sharpen accordingly.
|
|
14
|
+
|
|
15
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
# File-system layout
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
# Where Rails controllers live. Recipe 04 (Apollo) sharpens with
|
|
20
|
+
# "in <controller_path>/<name>_controller.rb#<action>" when extracting
|
|
21
|
+
# data fetches.
|
|
22
|
+
controller_path: app/controllers
|
|
23
|
+
|
|
24
|
+
# Where view-scoped helpers live. Recipe 06 (module constants) directs
|
|
25
|
+
# pure helper functions here.
|
|
26
|
+
helper_path: app/helpers
|
|
27
|
+
|
|
28
|
+
# Where app-wide service objects / POROs live. Recipe 06 directs more
|
|
29
|
+
# substantial helpers here when they're shared across controllers.
|
|
30
|
+
service_path: app/services
|
|
31
|
+
|
|
32
|
+
# Where Phlex / ViewComponent components are dropped. The components
|
|
33
|
+
# being resolved by this skill live here.
|
|
34
|
+
component_dir: app/components
|
|
35
|
+
|
|
36
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
# GraphQL — server-side
|
|
38
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
# Where GraphQL resolvers / query objects live, if your app uses
|
|
41
|
+
# graphql-ruby (or similar) server-side. Recipe 04 (Apollo) names this
|
|
42
|
+
# path when sharpening "extract this query into a resolver."
|
|
43
|
+
#
|
|
44
|
+
# Set to `null` if your app doesn't run GraphQL server-side (e.g. you're
|
|
45
|
+
# replacing client-side gql with REST endpoints or Turbo Frames).
|
|
46
|
+
graphql_resolver_path: null # e.g. app/graphql/resolvers
|
|
47
|
+
|
|
48
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
# Stimulus
|
|
50
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
# Where Stimulus controllers live. Recipes 03 (useState→value) and 05
|
|
53
|
+
# (event handlers→actions) reference this path.
|
|
54
|
+
stimulus_controller_path: app/javascript/controllers
|
|
55
|
+
|
|
56
|
+
# How Stimulus controller files are named. Determines the data-controller
|
|
57
|
+
# value emitted in HTML attributes.
|
|
58
|
+
# kebab-case → file `foo_bar_controller.js` exposes `data-controller="foo-bar"`
|
|
59
|
+
# underscore → file `foo_bar_controller.js` exposes `data-controller="foo_bar"`
|
|
60
|
+
# (Rails default with importmap is kebab-case via the Stimulus loader.)
|
|
61
|
+
stimulus_controller_naming: kebab-case
|
|
62
|
+
|
|
63
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
# Styling
|
|
65
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
# How the consuming app handles styles. Affects recipe 01's design-token
|
|
68
|
+
# substitution suggestions and recipe 03/05 advice on inline-style refactoring.
|
|
69
|
+
# tailwind → recipes prefer className utility classes (the YAML's
|
|
70
|
+
# `tailwind:` field becomes the suggested target)
|
|
71
|
+
# css-modules → recipes leave inline styles in place, suggest extraction
|
|
72
|
+
# to <component>.module.css for substantial blocks
|
|
73
|
+
# plain → inline styles stay inline; no refactor recommendation
|
|
74
|
+
css_strategy: tailwind
|
|
75
|
+
|
|
76
|
+
# Path to your Tailwind config, if css_strategy is tailwind. Recipe 01
|
|
77
|
+
# can reference this when the design-token YAML's tailwind suggestions
|
|
78
|
+
# don't match a class your config exposes.
|
|
79
|
+
tailwind_config_path: tailwind.config.js
|
|
80
|
+
|
|
81
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
# Routing
|
|
83
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
# Path to your Rails routes file. Recipe 07 (Next.js navigation) references
|
|
86
|
+
# it when sharpening `useRouter().push("/foo/[id]")` → "confirm in <routes>".
|
|
87
|
+
routes_path: config/routes.rb
|
|
88
|
+
|
|
89
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
# Naming
|
|
91
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
# Suffix appended to generated Phlex component class names. The skill
|
|
94
|
+
# uses this when generating cross-references (e.g. "controller passes
|
|
95
|
+
# @foo to FooComponent"). Match what you set with `--phlex-suffix=` on
|
|
96
|
+
# `jsx_rosetta translate`.
|
|
97
|
+
component_class_suffix: Component # e.g. ButtonComponent
|
|
98
|
+
|
|
99
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
# Optional — anything else recipes should know
|
|
101
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
# Free-text notes the recipes can read. Use for app-specific quirks the
|
|
104
|
+
# schema doesn't capture (e.g. "we use Pundit for authorization, not
|
|
105
|
+
# CanCanCan", "Turbo Frames are namespaced under app/views/frames/").
|
|
106
|
+
notes: |
|
|
107
|
+
(none)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Ant Design v5 design-token mapping — example for the jsx-rosetta-resolve-todos skill.
|
|
2
|
+
#
|
|
3
|
+
# Source: Ant Design v5 default theme (https://ant.design/docs/react/customize-theme).
|
|
4
|
+
# License: Ant Design tokens and palette values are documented public API
|
|
5
|
+
# (Ant Design is MIT-licensed). This file documents the defaults; it
|
|
6
|
+
# contains no application-specific configuration.
|
|
7
|
+
#
|
|
8
|
+
# Use this file as-is if your source app uses Ant Design v5 with default theming.
|
|
9
|
+
# If your app overrides ConfigProvider tokens, copy this file into your
|
|
10
|
+
# `.claude/skills/jsx-rosetta-resolve-todos/data/design_tokens.yml` and adjust
|
|
11
|
+
# values to match your overrides — the defaults here become the wrong answer
|
|
12
|
+
# for any token your theme has changed.
|
|
13
|
+
#
|
|
14
|
+
# Schema:
|
|
15
|
+
# match: Regex with one capture group. The capture is looked up in `tokens`.
|
|
16
|
+
# jsx_rosetta TODOs of the form
|
|
17
|
+
# # TODO: (attribute|style declaration) "X" dropped — couldn't translate: <RHS>
|
|
18
|
+
# are checked against this regex; matches drive substitution.
|
|
19
|
+
# tokens: Map of capture-group string → entry. Each entry:
|
|
20
|
+
# value Literal substitution. Integer = pixels (rendered as "<n>px" in
|
|
21
|
+
# inline style, bare integer in numeric kwargs). String = used
|
|
22
|
+
# verbatim. `null` = explicitly do not auto-resolve (e.g.
|
|
23
|
+
# component-namespace tokens that vary per app).
|
|
24
|
+
# tailwind Optional. Suggested utility class for a className refactor.
|
|
25
|
+
# category Optional grouping label.
|
|
26
|
+
# notes Optional caveat for human readers.
|
|
27
|
+
|
|
28
|
+
match: 'token\.(\w+)'
|
|
29
|
+
|
|
30
|
+
tokens:
|
|
31
|
+
sizeXXS: {value: 4, tailwind: "p-1", category: spacing}
|
|
32
|
+
paddingXXS: {value: 4, tailwind: "p-1", category: spacing}
|
|
33
|
+
marginXXS: {value: 4, tailwind: "m-1", category: spacing}
|
|
34
|
+
paddingXS: {value: 8, tailwind: "p-2", category: spacing}
|
|
35
|
+
marginXS: {value: 8, tailwind: "m-2", category: spacing}
|
|
36
|
+
paddingSM: {value: 12, tailwind: "p-3", category: spacing}
|
|
37
|
+
marginSM: {value: 12, tailwind: "m-3", category: spacing}
|
|
38
|
+
padding: {value: 16, tailwind: "p-4", category: spacing}
|
|
39
|
+
margin: {value: 16, tailwind: "m-4", category: spacing}
|
|
40
|
+
paddingMD: {value: 20, tailwind: "p-5", category: spacing}
|
|
41
|
+
marginMD: {value: 20, tailwind: "m-5", category: spacing}
|
|
42
|
+
paddingLG: {value: 24, tailwind: "p-6", category: spacing}
|
|
43
|
+
marginLG: {value: 24, tailwind: "m-6", category: spacing}
|
|
44
|
+
paddingXL: {value: 32, tailwind: "p-8", category: spacing}
|
|
45
|
+
|
|
46
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
# BORDER / RADIUS
|
|
48
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
borderRadiusSM: {value: 4, tailwind: "rounded-sm", category: radius}
|
|
51
|
+
borderRadius: {value: 6, tailwind: "rounded-md", category: radius, notes: "Ant default 6px sits between Tailwind sm(4) and md(6); rounded-md is closest."}
|
|
52
|
+
borderRadiusLG: {value: 8, tailwind: "rounded-lg", category: radius}
|
|
53
|
+
lineWidth: {value: 1, tailwind: "border", category: line}
|
|
54
|
+
border: {value: "1px solid #d9d9d9", tailwind: "border border-gray-300", category: line, notes: "Bare token.border is non-standard; assumed shorthand for the default 1px solid colorBorder."}
|
|
55
|
+
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
# TYPOGRAPHY
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
fontSizeSM: {value: 12, tailwind: "text-xs", category: typography}
|
|
61
|
+
fontSize: {value: 14, tailwind: "text-sm", category: typography}
|
|
62
|
+
fontSizeLG: {value: 16, tailwind: "text-base", category: typography}
|
|
63
|
+
fontSizeXL: {value: 20, tailwind: "text-xl", category: typography}
|
|
64
|
+
fontSizeHeading4: {value: 20, tailwind: "text-xl", category: typography}
|
|
65
|
+
fontSizeHeading3: {value: 24, tailwind: "text-2xl", category: typography}
|
|
66
|
+
fontSizeHeading2: {value: 30, tailwind: "text-3xl", category: typography}
|
|
67
|
+
fontWeightStrong: {value: 600, tailwind: "font-semibold", category: typography}
|
|
68
|
+
lineHeight: {value: 1.5714, tailwind: "leading-normal", category: typography, notes: "Ant default ≈ 22/14."}
|
|
69
|
+
fontFamily: {value: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif", tailwind: "font-sans", category: typography}
|
|
70
|
+
fontFamilyCode: {value: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace", tailwind: "font-mono", category: typography}
|
|
71
|
+
font: {value: 14, tailwind: "text-sm", category: typography, notes: "Bare `token.font` is non-standard; treated as fontSize default. Verify per usage."}
|
|
72
|
+
|
|
73
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
# COLOR — TEXT
|
|
75
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
colorTextBase: {value: "#000000", tailwind: "text-black", category: color-text}
|
|
78
|
+
colorText: {value: "rgba(0, 0, 0, 0.88)", tailwind: "text-gray-900", category: color-text}
|
|
79
|
+
colorTextSecondary: {value: "rgba(0, 0, 0, 0.65)", tailwind: "text-gray-700", category: color-text}
|
|
80
|
+
colorTextTertiary: {value: "rgba(0, 0, 0, 0.45)", tailwind: "text-gray-500", category: color-text}
|
|
81
|
+
colorTextQuaternary: {value: "rgba(0, 0, 0, 0.25)", tailwind: "text-gray-400", category: color-text}
|
|
82
|
+
colorTextDescription: {value: "rgba(0, 0, 0, 0.45)", tailwind: "text-gray-500", category: color-text}
|
|
83
|
+
colorTextDisabled: {value: "rgba(0, 0, 0, 0.25)", tailwind: "text-gray-400", category: color-text, notes: "Same alpha as quaternary; semantic intent differs."}
|
|
84
|
+
colorTextLightSolid: {value: "#ffffff", tailwind: "text-white", category: color-text, notes: "On dark/colored backgrounds."}
|
|
85
|
+
colorWhite: {value: "#ffffff", tailwind: "text-white", category: color-text}
|
|
86
|
+
colorLink: {value: "#1677ff", tailwind: "text-blue-600", category: color-text}
|
|
87
|
+
|
|
88
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
# COLOR — BACKGROUND
|
|
90
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
colorBgContainer: {value: "#ffffff", tailwind: "bg-white", category: color-bg}
|
|
93
|
+
colorBgLayout: {value: "#f5f5f5", tailwind: "bg-gray-100", category: color-bg}
|
|
94
|
+
colorBgSolid: {value: "#000000", tailwind: "bg-black", category: color-bg}
|
|
95
|
+
colorBgTextHover: {value: "rgba(0, 0, 0, 0.06)", tailwind: "hover:bg-gray-100", category: color-bg, notes: "Use as hover state, not base."}
|
|
96
|
+
|
|
97
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
# COLOR — BORDER
|
|
99
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
colorBorder: {value: "#d9d9d9", tailwind: "border-gray-300", category: color-border}
|
|
102
|
+
colorBorderSecondary: {value: "#f0f0f0", tailwind: "border-gray-200", category: color-border}
|
|
103
|
+
|
|
104
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
# COLOR — FILL (translucent overlays)
|
|
106
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
colorFillSecondary: {value: "rgba(0, 0, 0, 0.06)", tailwind: "bg-black/5", category: color-fill}
|
|
109
|
+
colorFillTertiary: {value: "rgba(0, 0, 0, 0.04)", tailwind: "bg-black/5", category: color-fill}
|
|
110
|
+
colorFillQuaternary: {value: "rgba(0, 0, 0, 0.02)", tailwind: "bg-black/[0.02]", category: color-fill}
|
|
111
|
+
colorFillAlter: {value: "rgba(0, 0, 0, 0.02)", tailwind: "bg-black/[0.02]", category: color-fill}
|
|
112
|
+
|
|
113
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
# COLOR — FEEDBACK (semantic: primary/success/warning/error/info)
|
|
115
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
colorPrimary: {value: "#1677ff", tailwind: "text-blue-600", category: color-feedback}
|
|
118
|
+
colorSuccess: {value: "#52c41a", tailwind: "text-green-600", category: color-feedback}
|
|
119
|
+
colorWarning: {value: "#faad14", tailwind: "text-amber-500", category: color-feedback}
|
|
120
|
+
colorError: {value: "#ff4d4f", tailwind: "text-red-500", category: color-feedback}
|
|
121
|
+
colorInfo: {value: "#1677ff", tailwind: "text-blue-600", category: color-feedback}
|
|
122
|
+
|
|
123
|
+
# Feedback backgrounds (lightened tints used for Alert / Tag / Badge surfaces)
|
|
124
|
+
colorPrimaryBg: {value: "#e6f4ff", tailwind: "bg-blue-50", category: color-feedback-bg}
|
|
125
|
+
colorPrimaryBgHover: {value: "#bae0ff", tailwind: "bg-blue-100", category: color-feedback-bg}
|
|
126
|
+
colorInfoBgHover: {value: "#bae0ff", tailwind: "bg-blue-100", category: color-feedback-bg}
|
|
127
|
+
colorErrorBg: {value: "#fff2f0", tailwind: "bg-red-50", category: color-feedback-bg}
|
|
128
|
+
colorErrorBgHover: {value: "#fff1f0", tailwind: "bg-red-50", category: color-feedback-bg}
|
|
129
|
+
colorWarningBg: {value: "#fffbe6", tailwind: "bg-amber-50", category: color-feedback-bg}
|
|
130
|
+
colorWarningBgHover: {value: "#fff1b8", tailwind: "bg-amber-100", category: color-feedback-bg}
|
|
131
|
+
|
|
132
|
+
# Feedback borders
|
|
133
|
+
colorErrorBorder: {value: "#ffccc7", tailwind: "border-red-200", category: color-feedback-border}
|
|
134
|
+
colorInfoBorderHover: {value: "#91caff", tailwind: "border-blue-300", category: color-feedback-border}
|
|
135
|
+
|
|
136
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
# COLOR — BRAND PALETTE (named hues; used as base color names in Tag etc.)
|
|
138
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
blue: {value: "#1677ff", tailwind: "text-blue-600", category: color-brand}
|
|
141
|
+
green: {value: "#52c41a", tailwind: "text-green-600", category: color-brand}
|
|
142
|
+
gold: {value: "#faad14", tailwind: "text-amber-500", category: color-brand}
|
|
143
|
+
orange: {value: "#fa8c16", tailwind: "text-orange-500", category: color-brand}
|
|
144
|
+
red: {value: "#f5222d", tailwind: "text-red-600", category: color-brand}
|
|
145
|
+
purple: {value: "#722ed1", tailwind: "text-purple-600", category: color-brand}
|
|
146
|
+
cyan: {value: "#13c2c2", tailwind: "text-cyan-600", category: color-brand}
|
|
147
|
+
|
|
148
|
+
# Indexed palette shades (1=lightest, 10=darkest) — shows up in custom theming.
|
|
149
|
+
blue1: {value: "#e6f4ff", tailwind: "bg-blue-50", category: color-palette-shade}
|
|
150
|
+
blue2: {value: "#bae0ff", tailwind: "bg-blue-100", category: color-palette-shade}
|
|
151
|
+
blue6: {value: "#1677ff", tailwind: "text-blue-600", category: color-palette-shade}
|
|
152
|
+
green10: {value: "#092b00", tailwind: "text-green-950", category: color-palette-shade}
|
|
153
|
+
|
|
154
|
+
# Bare `token.color` — non-standard, source-specific. Don't auto-substitute.
|
|
155
|
+
color: {value: null, tailwind: null, category: unknown, notes: "Bare token.color isn't an Ant Design v5 default. Likely a custom theme extension. Sharpen the TODO with the surrounding context; don't auto-resolve."}
|
|
156
|
+
|
|
157
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
# SHADOW
|
|
159
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
boxShadow: {value: "0 6px 16px 0 rgba(0,0,0,.08), 0 3px 6px -4px rgba(0,0,0,.12), 0 9px 28px 8px rgba(0,0,0,.05)", tailwind: "shadow-md", category: shadow}
|
|
162
|
+
boxShadowTertiary: {value: "0 1px 2px 0 rgba(0,0,0,.03), 0 1px 6px -1px rgba(0,0,0,.02), 0 2px 4px 0 rgba(0,0,0,.02)", tailwind: "shadow-sm", category: shadow}
|
|
163
|
+
|
|
164
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
# BREAKPOINT
|
|
166
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
screenLG: {value: 992, tailwind: "lg:", category: breakpoint, notes: "Ant Design lg breakpoint = 992px. Tailwind's lg: prefix is 1024px — close, not identical."}
|
|
169
|
+
|
|
170
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
# COMPONENT-NAMESPACE TOKENS — leave as TODO; resolution is per-component
|
|
172
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
#
|
|
174
|
+
# These references (e.g. `token.Tag.colorText`) reach into Ant's component-token
|
|
175
|
+
# overrides, which the consuming app may have customized via ConfigProvider.
|
|
176
|
+
# Auto-substituting the default is unsafe — it would silently lose overrides.
|
|
177
|
+
# Sharpen the TODO with a note explaining what the source was reading and
|
|
178
|
+
# defer to human review.
|
|
179
|
+
|
|
180
|
+
Tag: {value: null, tailwind: null, category: component, notes: "Ant Tag component-token namespace. Read the property in the source (e.g. token.Tag.defaultBg) and emit a sharper TODO."}
|
|
181
|
+
Layout: {value: null, tailwind: null, category: component, notes: "Ant Layout component-token namespace. Same handling as token.Tag."}
|
|
182
|
+
|
|
183
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
# AMBIGUOUS / EDGE
|
|
185
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
token: {value: null, tailwind: null, category: unknown, notes: "`token.token` — almost certainly a source bug or destructure typo. Flag for human review."}
|
|
188
|
+
|
|
189
|
+
paddingContentVerticalSM: {value: 6, tailwind: "py-1.5", category: spacing, notes: "Ant content-padding tokens; defaults vary by theme. Verify per usage."}
|
|
190
|
+
paddingContentHorizontalSM: {value: 12, tailwind: "px-3", category: spacing, notes: "Ant content-padding tokens; defaults vary by theme. Verify per usage."}
|