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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions