Skip to content

Tweakcn import: support shadcn-registry JSON format from /r/themes/<name>.json #92

Description

@BunsDev

Context

The new tweakcn import flow (app/src/themes/tweakcn_import.rs::parse_blocks) parses the CSS-block format users get from tweakcn's "Copy CSS" button:

```css
:root { --background: oklch(...); ... }
.dark { --background: oklch(...); ... }
```

But tweakcn also publishes themes as a shadcn registry JSON at stable URLs like https://tweakcn.com/r/themes/<name>.json (e.g. /r/themes/vercel.json):

```json
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "vercel",
"type": "registry:style",
"cssVars": {
"theme": { "font-sans": "Geist, sans-serif", ... },
"light": { "background": "oklch(0.99 0 0)", "foreground": "oklch(0 0 0)", ... },
"dark": { "background": "oklch(0 0 0)", "foreground": "oklch(1 0 0)", ... }
}
}
```

The editor-URL pattern users see in their browser (https://tweakcn.com/editor/theme?theme=vercel) maps deterministically to https://tweakcn.com/r/themes/vercel.json.

Why this matters

Users who hand a Cast-Codes contributor a tweakcn share URL today have to:

  1. Open tweakcn.com
  2. Click "Code" / "Copy CSS"
  3. Paste into the import modal

Each manual step is a place to drop content. Worse, the modal currently chokes on full pastes (issue #91). If we supported the JSON path:

  • Drag a .json file onto the modal directly, OR
  • Paste a tweakcn URL into the modal and we fetch + extract (still local-only — https://tweakcn.com is the user's choice of source, not a Cast-Codes hosted dependency).

Suggested implementation

Add a sibling parser at parse_registry_json(input: &str) -> Result<ParsedBlocks, ImportError> in app/src/themes/tweakcn_import.rs:

```rust
pub fn parse_blocks_or_json(input: &str) -> Result<ParsedBlocks, ImportError> {
let trimmed = input.trim_start();
if trimmed.starts_with('{') {
parse_registry_json(input)
} else {
parse_blocks(input)
}
}

fn parse_registry_json(input: &str) -> Result<ParsedBlocks, ImportError> {
let v: serde_json::Value = serde_json::from_str(input)
.map_err(|e| ImportError::Io(format!("invalid JSON: {e}")))?;
let css_vars = v.get("cssVars").ok_or(ImportError::NoColorBlocksFound)?;
let name = v.get("name").and_then(|n| n.as_str()).map(String::from);

let mut blocks = ParsedBlocks { name_comment: name, ..Default::default() };

for (mode_key, target) in [("light", &mut blocks.light), ("dark", &mut blocks.dark)] {
    if let Some(map) = css_vars.get(mode_key).and_then(|m| m.as_object()) {
        for (k, v) in map {
            let s = match v.as_str() { Some(s) => s, None => continue };
            // Same oklch(L C H) parsing as parse_blocks's parse_decls closure.
            let Some(args) = s.strip_prefix("oklch(").and_then(|t| t.strip_suffix(')')) else { continue };
            // … reuse the triple-parsing logic …
        }
    }
}

if blocks.light.is_empty() && blocks.dark.is_empty() {
    return Err(ImportError::NoColorBlocksFound);
}
Ok(blocks)

}
```

Then wire the modal's on_css_changed to call parse_blocks_or_json. Drag-drop already handles .css; extend the extension check to allow .json too.

URL-fetch is more involved (network call from the OSS build — needs to clear the [castcodes-fork-local-boundary](skill: castcodes-fork-local-boundary) check). A safer middle step: if the user pastes https://tweakcn.com/..., surface a hint "Paste the JSON from the registry URL instead of the share URL" with a click-to-copy curl command.

Discovery context

Validated during smoke test of branch castcodes/theme-tweakcn-impl. The user pasted a Vercel-theme tweakcn URL into the modal expecting it to work; the modal showed CSS but with so much overhead the user gave up and asked us to extract via URL externally. I curl'd /r/themes/vercel.json and converted to CSS via a one-off Python script, then ran through parse_blocks + to_warp_theme — output landed cleanly in ~/.cast-codes/themes/vercel.yaml. Adding native JSON support would have eliminated the conversion step.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions