Files
tolaria/scripts/restructure-design-system.py
Luca Rossi aaf9c9a330 feat: structured design system in ui-design.pen (#95)
* feat: restructure ui-design.pen as complete design system

Major restructuring of the design canvas into 5 organized sections:

0. Cover — title, TOC with links to all sections
1. Foundations — colors, typography, spacing scale, heights, shadows
2. Components — 23 reusable components (atoms, molecules, organisms)
3. Full Layouts — 5 app variants at 1440×900
4. Feature Specs — 65 existing frames reorganized into 10 functional groups

Components created (reusable, defined once):
- Atoms: StatusDot, Separator, Badge, Button (Primary/Secondary/Ghost),
  Input, IconButton
- Molecules: InspectorHeader, NoteListItem, Tab (Active/Inactive),
  PropertyRow, RelationshipPill, SidebarFilterItem, SectionLabel,
  SectionCountHeader, AddButton, RelGroupLabel, CommandPaletteItem,
  EditableValue
- Organisms: Toast, AIChatMessage

Variables added (22 new):
- Spacing: --spacing-xs through --spacing-3xl (4px to 40px)
- Heights: --height-titlebar, --height-tabbar, --height-statusbar, etc.
- Shadows: --shadow-sm, --shadow-md, --shadow-lg
- Search colors: --search-bg, --search-input-bg (replacing hardcoded hex)
- Font: --font-mono

Hardcoded colors replaced with variables in existing frames.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: zero hardcoded colors + 6th layout (Focus Mode)

- Replace all 82 remaining hardcoded hex colors with CSS variables
- Add 11 new variables: search theme colors, traffic lights, white overlay
- Total variables: 79 (zero hardcoded fills in entire canvas)
- Add 6th full layout: Focus Mode — Distraction-free Writing (1440×900)
- All 6 layout variants now complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add .claude-done summary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:18:35 +00:00

929 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Restructure ui-design.pen into a proper design system.
Sections (top → bottom):
0. Cover — title, TOC
1. Foundations — colors, typography, spacing, shadows, heights
2. Components — atoms → molecules → organisms (reusable)
3. Full Layouts — 6 app variants at 1440×900
4. Feature Specs — existing frames reorganized by functional area
"""
import json
import copy
import sys
import os
PEN_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "ui-design.pen")
def load():
with open(PEN_FILE, "r") as f:
return json.load(f)
def save(data):
with open(PEN_FILE, "w") as f:
json.dump(data, f, separators=(",", ":"))
# ─── Variable definitions ─────────────────────────────────────────
NEW_VARIABLES = {
"--spacing-xs": {"type": "number", "value": 4},
"--spacing-sm": {"type": "number", "value": 8},
"--spacing-md": {"type": "number", "value": 12},
"--spacing-lg": {"type": "number", "value": 16},
"--spacing-xl": {"type": "number", "value": 24},
"--spacing-2xl": {"type": "number", "value": 32},
"--spacing-3xl": {"type": "number", "value": 40},
"--height-titlebar": {"type": "number", "value": 38},
"--height-tabbar": {"type": "number", "value": 45},
"--height-breadcrumb": {"type": "number", "value": 45},
"--height-statusbar": {"type": "number", "value": 30},
"--height-search-bar": {"type": "number", "value": 40},
"--height-note-item": {"type": "number", "value": 64},
"--height-inspector-header": {"type": "number", "value": 36},
"--height-modal-header": {"type": "number", "value": 56},
"--height-modal-footer": {"type": "number", "value": 56},
"--shadow-sm": {"type": "string", "value": "0 1px 2px rgba(0,0,0,0.05)"},
"--shadow-md": {"type": "string", "value": "0 4px 6px rgba(0,0,0,0.07)"},
"--shadow-lg": {"type": "string", "value": "0 10px 15px rgba(0,0,0,0.1)"},
"--font-mono": {"type": "string", "value": "IBM Plex Mono, monospace"},
# Search panel colors (currently hardcoded)
"--search-bg": {"type": "color", "value": [
{"value": "#FFFFFF"}, {"theme": {"Mode": "Dark"}, "value": "#1E1E2E"}
]},
"--search-input-bg": {"type": "color", "value": [
{"value": "#F0F0EF"}, {"theme": {"Mode": "Dark"}, "value": "#2A2A3C"}
]},
}
# ─── Hardcoded color mappings ─────────────────────────────────────
# Maps hardcoded hex → variable name for replacement
HARDCODED_COLORS = {
"#1E1E2E": "$--search-bg",
"#1e1e2e": "$--search-bg",
"#2A2A3C": "$--search-input-bg",
"#2a2a3c": "$--search-input-bg",
"#6b7280": "$--muted-foreground",
"#f9fafb": "$--muted",
"#f3f4f6": "$--muted",
"#ffffff": "$--background",
"#000000": "$--black",
"#6366f1": "$--primary",
}
def replace_hardcoded_colors(node):
"""Recursively replace hardcoded hex colors with variable references."""
if isinstance(node, dict):
for key, val in list(node.items()):
if key == "fill" and isinstance(val, str) and val.startswith("#"):
lower = val.lower()
if lower in HARDCODED_COLORS:
node[key] = HARDCODED_COLORS[lower]
elif key == "fill" and isinstance(val, dict) and val.get("type") == "color":
color = val.get("color", "")
if isinstance(color, str) and color.lower() in HARDCODED_COLORS:
val["color"] = HARDCODED_COLORS[color.lower()]
elif isinstance(val, (dict, list)):
replace_hardcoded_colors(val)
elif isinstance(node, list):
for item in node:
replace_hardcoded_colors(item)
# ─── Section header helper ────────────────────────────────────────
def make_section_header(sec_id, label, x, y):
return {
"type": "frame", "id": sec_id, "name": label,
"x": x, "y": y, "width": 1440, "height": 60,
"fill": "$--foreground", "padding": [0, 60],
"alignItems": "center", "theme": {"Mode": "Light"},
"children": [{
"type": "text", "id": f"{sec_id}_lbl",
"content": label.upper(),
"fill": "$--background", "fontFamily": "Inter",
"fontSize": 24, "fontWeight": "700", "letterSpacing": 2
}]
}
def make_subsection_label(sub_id, label, x, y):
return {
"type": "text", "id": sub_id,
"content": label,
"x": x, "y": y,
"fill": "$--muted-foreground", "fontFamily": "Inter",
"fontSize": 16, "fontWeight": "600", "letterSpacing": 1,
"theme": {"Mode": "Light"}
}
# ─── Cover ────────────────────────────────────────────────────────
def make_cover(x, y):
return {
"type": "frame", "id": "ds_cover", "name": "0 — Cover",
"x": x, "y": y, "width": 1440, "height": "fit_content(400)",
"fill": "$--background", "layout": "vertical",
"gap": 32, "padding": [80, 60],
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "ds_title", "content": "Laputa Design System",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 48,
"fontWeight": "700", "letterSpacing": -1.5},
{"type": "text", "id": "ds_subtitle",
"content": "Wiki-linked knowledge management for deep thinkers",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 18, "lineHeight": 1.5},
{"type": "rectangle", "id": "ds_div", "width": "fill_container", "height": 1, "fill": "$--border"},
{"type": "frame", "id": "ds_toc", "name": "TOC", "layout": "vertical",
"width": "fill_container", "gap": 16, "children": [
{"type": "text", "id": "ds_toc_t", "content": "Table of Contents",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 20, "fontWeight": "600"},
{"type": "frame", "id": "ds_toc_items", "layout": "vertical",
"width": "fill_container", "gap": 8, "children": [
{"type": "text", "id": "ds_t1", "fill": "$--primary", "fontFamily": "Inter",
"fontSize": 14, "fontWeight": "500",
"content": "1. Foundations — Colors, Typography, Spacing, Shadows, Heights"},
{"type": "text", "id": "ds_t2", "fill": "$--primary", "fontFamily": "Inter",
"fontSize": 14, "fontWeight": "500",
"content": "2. Components — Atoms, Molecules, Organisms"},
{"type": "text", "id": "ds_t3", "fill": "$--primary", "fontFamily": "Inter",
"fontSize": 14, "fontWeight": "500",
"content": "3. Full Layouts — 6 app variants at 1440×900"},
{"type": "text", "id": "ds_t4", "fill": "$--primary", "fontFamily": "Inter",
"fontSize": 14, "fontWeight": "500",
"content": "4. Feature Specs — All screens grouped by functional area"},
]}
]}
]
}
# ─── Foundation frames ────────────────────────────────────────────
def make_spacing_scale(x, y):
rows = []
for var, px in [("xs", 4), ("sm", 8), ("md", 12), ("lg", 16), ("xl", 24), ("2xl", 32), ("3xl", 40)]:
rid = f"sp_{var}"
rows.append({
"type": "frame", "id": rid, "layout": "horizontal",
"gap": 16, "alignItems": "center", "width": "fill_container",
"children": [
{"type": "rectangle", "id": f"{rid}_box", "width": px, "height": 24,
"fill": "$--primary", "cornerRadius": 2},
{"type": "text", "id": f"{rid}_lbl",
"content": f"--spacing-{var}: {px}px",
"fill": "$--foreground", "fontFamily": "IBM Plex Mono", "fontSize": 13}
]
})
return {
"type": "frame", "id": "ds_spacing", "name": "Spacing Scale",
"x": x, "y": y, "width": 700,
"fill": "$--background", "layout": "vertical",
"gap": 24, "padding": 40, "theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "ds_sp_title", "content": "Spacing Scale",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 20, "fontWeight": "600"},
{"type": "text", "id": "ds_sp_sub",
"content": "Consistent spacing tokens used throughout the app.",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 13, "lineHeight": 1.5},
] + rows
}
def make_heights_ref(x, y):
items = [
("titlebar", 38, "$--sidebar"), ("tabbar", 45, "$--sidebar"),
("breadcrumb", 45, "$--background"), ("statusbar", 30, "$--sidebar"),
("modal-header", 56, "$--background"), ("inspector-header", 36, "$--background"),
]
rows = []
for name, px, fill in items:
rid = f"ht_{name.replace('-','_')}"
rows.append({
"type": "frame", "id": rid, "layout": "horizontal",
"gap": 16, "alignItems": "center", "width": "fill_container",
"children": [
{"type": "rectangle", "id": f"{rid}_bar", "width": 120, "height": px,
"fill": fill, "cornerRadius": "$--radius-sm",
"stroke": {"fill": "$--border", "thickness": 1}},
{"type": "text", "id": f"{rid}_lbl",
"content": f"--height-{name}: {px}px",
"fill": "$--foreground", "fontFamily": "IBM Plex Mono", "fontSize": 13}
]
})
return {
"type": "frame", "id": "ds_heights", "name": "Height Variables",
"x": x, "y": y, "width": 700,
"fill": "$--background", "layout": "vertical",
"gap": 24, "padding": 40, "theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "ds_ht_title", "content": "Height Variables",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 20, "fontWeight": "600"},
{"type": "text", "id": "ds_ht_sub",
"content": "Fixed heights for key layout regions.",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 13, "lineHeight": 1.5},
] + rows
}
def make_shadows_ref(x, y):
shadows = [
("SM", "0 1px 2px rgba(0,0,0,0.05)", {"type": "shadow", "offset": {"x": 0, "y": 1}, "blur": 2, "color": "#0000000D"}),
("MD", "0 4px 6px rgba(0,0,0,0.07)", {"type": "shadow", "offset": {"x": 0, "y": 4}, "blur": 6, "color": "#00000012"}),
("LG", "0 10px 15px rgba(0,0,0,0.1)", {"type": "shadow", "offset": {"x": 0, "y": 10}, "blur": 15, "color": "#0000001A"}),
]
rows = []
for name, desc, effect in shadows:
rid = f"sh_{name.lower()}"
rows.append({
"type": "frame", "id": rid, "layout": "horizontal",
"gap": 24, "alignItems": "center", "width": "fill_container",
"children": [
{"type": "frame", "id": f"{rid}_box", "width": 120, "height": 80,
"fill": "$--card", "cornerRadius": "$--radius-lg", "effect": effect},
{"type": "frame", "id": f"{rid}_info", "layout": "vertical", "gap": 4, "children": [
{"type": "text", "id": f"{rid}_name", "content": f"Shadow {name}",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 14, "fontWeight": "600"},
{"type": "text", "id": f"{rid}_desc", "content": desc,
"fill": "$--muted-foreground", "fontFamily": "IBM Plex Mono", "fontSize": 11}
]}
]
})
return {
"type": "frame", "id": "ds_shadows", "name": "Shadows",
"x": x, "y": y, "width": 700,
"fill": "$--background", "layout": "vertical",
"gap": 32, "padding": 40, "theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "ds_sh_title", "content": "Shadows",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 20, "fontWeight": "600"},
{"type": "text", "id": "ds_sh_sub",
"content": "Elevation levels for cards, modals, and popovers.",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 13, "lineHeight": 1.5},
] + rows
}
# ─── Reusable Components ─────────────────────────────────────────
def make_component_status_dot(x, y):
return {
"type": "frame", "id": "comp_StatusDot", "name": "component/StatusDot",
"reusable": True, "x": x, "y": y,
"width": "fit_content(6)", "height": "fit_content(6)",
"theme": {"Mode": "Light"},
"children": [
{"type": "ellipse", "id": "StatusDot_dot", "width": 6, "height": 6,
"fill": "$--accent-orange"}
]
}
def make_component_separator(x, y):
return {
"type": "frame", "id": "comp_Separator", "name": "component/Separator",
"reusable": True, "x": x, "y": y,
"width": "fill_container(200)", "height": 1,
"fill": "$--border",
"theme": {"Mode": "Light"},
"children": []
}
def make_component_badge(x, y):
"""Colored pill badge — used for type badges, status badges, tags."""
return {
"type": "frame", "id": "comp_Badge", "name": "component/Badge",
"reusable": True, "x": x, "y": y,
"cornerRadius": 12, "fill": "$--accent-blue-light",
"padding": [2, 8],
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "Badge_label", "content": "Badge",
"fill": "$--accent-blue", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500"}
]
}
def make_component_button_primary(x, y):
return {
"type": "frame", "id": "comp_ButtonPrimary", "name": "component/Button/Primary",
"reusable": True, "x": x, "y": y,
"cornerRadius": "$--radius-md", "fill": "$--primary",
"padding": [8, 16], "gap": 6, "alignItems": "center",
"justifyContent": "center", "height": 36,
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "BtnPri_label", "content": "Button",
"fill": "$--primary-foreground", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500"}
]
}
def make_component_button_secondary(x, y):
return {
"type": "frame", "id": "comp_ButtonSecondary", "name": "component/Button/Secondary",
"reusable": True, "x": x, "y": y,
"cornerRadius": "$--radius-md", "fill": "$--secondary",
"padding": [8, 16], "gap": 6, "alignItems": "center",
"justifyContent": "center", "height": 36,
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "BtnSec_label", "content": "Button",
"fill": "$--secondary-foreground", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500"}
]
}
def make_component_button_ghost(x, y):
return {
"type": "frame", "id": "comp_ButtonGhost", "name": "component/Button/Ghost",
"reusable": True, "x": x, "y": y,
"cornerRadius": "$--radius-md",
"padding": [8, 16], "gap": 6, "alignItems": "center",
"justifyContent": "center", "height": 36,
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "BtnGho_label", "content": "Button",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500"}
]
}
def make_component_input(x, y):
return {
"type": "frame", "id": "comp_Input", "name": "component/Input",
"reusable": True, "x": x, "y": y,
"width": 240, "height": 36,
"cornerRadius": "$--radius-md", "fill": "$--background",
"stroke": {"fill": "$--input", "thickness": 1, "align": "inside"},
"padding": [0, 12], "alignItems": "center",
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "Input_placeholder", "content": "Placeholder...",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 13}
]
}
def make_component_icon_button(x, y):
return {
"type": "frame", "id": "comp_IconButton", "name": "component/IconButton",
"reusable": True, "x": x, "y": y,
"width": 28, "height": 28,
"cornerRadius": "$--radius-sm",
"alignItems": "center", "justifyContent": "center",
"theme": {"Mode": "Light"},
"children": [
{"type": "icon_font", "id": "IconBtn_icon", "width": 16, "height": 16,
"fill": "$--muted-foreground", "iconFontFamily": "lucide", "iconFontName": "plus"}
]
}
# ─── Molecule Components ──────────────────────────────────────────
def make_component_inspector_header(x, y):
return {
"type": "frame", "id": "comp_InspectorHeader", "name": "component/InspectorHeader",
"reusable": True, "x": x, "y": y,
"width": "fill_container(320)", "height": "$--height-inspector-header",
"justifyContent": "space_between", "alignItems": "center",
"padding": [0, 12],
"theme": {"Mode": "Light"},
"children": [
{"type": "frame", "id": "IH_left", "name": "leftGroup",
"gap": 6, "alignItems": "center", "children": [
{"type": "icon_font", "id": "IH_icon", "width": 16, "height": 16,
"fill": "$--muted-foreground", "iconFontFamily": "phosphor",
"iconFontName": "sliders-horizontal"},
{"type": "text", "id": "IH_title", "content": "Properties",
"fill": "$--muted-foreground", "fontFamily": "Inter",
"fontSize": 13, "fontWeight": "600"}
]},
{"type": "icon_font", "id": "IH_close", "width": 16, "height": 16,
"fill": "$--muted-foreground", "iconFontFamily": "phosphor", "iconFontName": "x"}
]
}
def make_component_note_list_item(x, y):
return {
"type": "frame", "id": "comp_NoteListItem", "name": "component/NoteListItem",
"reusable": True, "x": x, "y": y,
"width": "fill_container(340)", "layout": "vertical",
"gap": 4, "padding": [14, 16],
"stroke": {"align": "inside", "fill": "$--border", "thickness": {"bottom": 1}},
"theme": {"Mode": "Light"},
"children": [
{"type": "frame", "id": "NLI_titleRow", "name": "titleRow",
"width": "fill_container", "justifyContent": "space_between", "alignItems": "center",
"children": [
{"type": "frame", "id": "NLI_titleGroup", "gap": 6, "alignItems": "center",
"children": [
{"type": "text", "id": "NLI_title", "content": "Note Title",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500"}
]},
{"type": "text", "id": "NLI_time", "content": "2m ago",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 11}
]},
{"type": "text", "id": "NLI_snippet",
"content": "Preview text of the note content...",
"fill": "$--muted-foreground", "fontFamily": "Inter",
"fontSize": 12, "lineHeight": 1.5,
"textGrowth": "fixed-width", "width": "fill_container"},
]
}
def make_component_tab_active(x, y):
return {
"type": "frame", "id": "comp_TabActive", "name": "component/Tab/Active",
"reusable": True, "x": x, "y": y,
"height": "fill_container(45)", "gap": 6,
"fill": "$--background", "alignItems": "center",
"padding": [0, 14],
"stroke": {"align": "inside", "fill": "$--border", "thickness": {"right": 1}},
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "TabA_label", "content": "Tab Title",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500"},
{"type": "icon_font", "id": "TabA_close", "width": 14, "height": 14,
"fill": "$--muted-foreground", "iconFontFamily": "lucide", "iconFontName": "x"}
]
}
def make_component_tab_inactive(x, y):
return {
"type": "frame", "id": "comp_TabInactive", "name": "component/Tab/Inactive",
"reusable": True, "x": x, "y": y,
"height": "fill_container(45)", "gap": 6,
"alignItems": "center", "padding": [0, 14],
"stroke": {"align": "inside", "fill": "$--sidebar-border",
"thickness": {"bottom": 1, "right": 1}},
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "TabI_label", "content": "Tab Title",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 12},
{"type": "icon_font", "id": "TabI_close", "width": 14, "height": 14,
"fill": "$--muted-foreground", "iconFontFamily": "lucide", "iconFontName": "x",
"opacity": 0}
]
}
def make_component_property_row(x, y):
return {
"type": "frame", "id": "comp_PropertyRow", "name": "component/PropertyRow",
"reusable": True, "x": x, "y": y,
"width": "fill_container(300)", "alignItems": "center",
"cornerRadius": 4, "padding": [4, 6],
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "PR_label", "content": "LABEL",
"fill": "$--muted-foreground", "fontFamily": "IBM Plex Mono",
"fontSize": 10, "fontWeight": "500", "letterSpacing": 1.2},
{"type": "text", "id": "PR_value", "content": "Value",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 13}
]
}
def make_component_relationship_pill(x, y):
return {
"type": "frame", "id": "comp_RelationshipPill", "name": "component/RelationshipPill",
"reusable": True, "x": x, "y": y,
"width": "fill_container(280)", "cornerRadius": 6,
"fill": "$--accent-blue-light", "gap": 6,
"justifyContent": "space_between", "alignItems": "center",
"padding": [6, 10],
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "RP_title", "content": "Related Note",
"fill": "$--accent-blue", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500"},
{"type": "icon_font", "id": "RP_icon", "width": 14, "height": 14,
"fill": "$--accent-blue", "iconFontFamily": "phosphor", "iconFontName": "wrench",
"weight": 700}
]
}
def make_component_sidebar_filter_item(x, y):
return {
"type": "frame", "id": "comp_SidebarFilterItem", "name": "component/SidebarFilterItem",
"reusable": True, "x": x, "y": y,
"width": "fill_container(250)", "cornerRadius": 6,
"gap": 8, "alignItems": "center", "padding": [6, 16],
"theme": {"Mode": "Light"},
"children": [
{"type": "icon_font", "id": "SFI_icon", "width": 16, "height": 16,
"fill": "$--muted-foreground", "iconFontFamily": "lucide", "iconFontName": "folder"},
{"type": "text", "id": "SFI_label", "content": "Filter Item",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 13},
{"type": "text", "id": "SFI_count", "content": "42",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 11}
]
}
def make_component_section_label(x, y):
return {
"type": "frame", "id": "comp_SectionLabel", "name": "component/SectionLabel",
"reusable": True, "x": x, "y": y,
"width": "fill_container(280)", "padding": [0, 12],
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "SL_label", "content": "SECTION",
"fill": "$--muted-foreground", "fontFamily": "Inter",
"fontSize": 10, "fontWeight": "600", "letterSpacing": 1.2}
]
}
def make_component_section_count_header(x, y):
return {
"type": "frame", "id": "comp_SectionCountHeader", "name": "component/SectionCountHeader",
"reusable": True, "x": x, "y": y,
"width": "fill_container(280)", "justifyContent": "space_between",
"alignItems": "center", "padding": [4, 0],
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "SCH_label", "content": "SECTION",
"fill": "$--muted-foreground", "fontFamily": "Inter",
"fontSize": 10, "fontWeight": "600", "letterSpacing": 1.2},
{"type": "text", "id": "SCH_count", "content": "3",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 11}
]
}
def make_component_add_button(x, y):
return {
"type": "frame", "id": "comp_AddButton", "name": "component/AddButton",
"reusable": True, "x": x, "y": y,
"width": "fill_container(280)", "height": 32,
"cornerRadius": 6, "justifyContent": "center", "alignItems": "center",
"stroke": {"align": "inside", "fill": "$--border", "thickness": 1},
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "AB_label", "content": "+ Add property",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 12}
]
}
def make_component_rel_group_label(x, y):
return {
"type": "frame", "id": "comp_RelGroupLabel", "name": "component/RelGroupLabel",
"reusable": True, "x": x, "y": y,
"width": "fill_container(280)", "justifyContent": "space_between",
"alignItems": "center",
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "RGL_label", "content": "BELONGS TO",
"fill": "$--muted-foreground", "fontFamily": "Inter",
"fontSize": 10, "fontWeight": "600", "letterSpacing": 0.5},
{"type": "text", "id": "RGL_count", "content": "3",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 10}
]
}
def make_component_command_palette_item(x, y):
return {
"type": "frame", "id": "comp_CommandPaletteItem", "name": "component/CommandPaletteItem",
"reusable": True, "x": x, "y": y,
"width": "fill_container(460)", "height": 40,
"alignItems": "center", "gap": 12, "padding": [0, 16],
"cornerRadius": "$--radius-md",
"theme": {"Mode": "Light"},
"children": [
{"type": "icon_font", "id": "CPI_icon", "width": 16, "height": 16,
"fill": "$--muted-foreground", "iconFontFamily": "lucide", "iconFontName": "file-text"},
{"type": "frame", "id": "CPI_content", "layout": "vertical",
"width": "fill_container", "gap": 2, "children": [
{"type": "text", "id": "CPI_title", "content": "Command Name",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500"},
]},
{"type": "text", "id": "CPI_shortcut", "content": "⌘K",
"fill": "$--muted-foreground", "fontFamily": "IBM Plex Mono", "fontSize": 11}
]
}
def make_component_editable_value(x, y):
return {
"type": "frame", "id": "comp_EditableValue", "name": "component/EditableValue",
"reusable": True, "x": x, "y": y,
"width": "fill_container(200)",
"alignItems": "center", "gap": 4,
"theme": {"Mode": "Light"},
"children": [
{"type": "text", "id": "EV_value", "content": "Editable Value",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 13},
{"type": "icon_font", "id": "EV_edit", "width": 12, "height": 12,
"fill": "$--muted-foreground", "iconFontFamily": "lucide", "iconFontName": "pencil",
"opacity": 0}
]
}
# ─── Organism Components ──────────────────────────────────────────
def make_component_toast(x, y):
return {
"type": "frame", "id": "comp_Toast", "name": "component/Toast",
"reusable": True, "x": x, "y": y,
"width": 360, "cornerRadius": "$--radius-lg",
"fill": "$--card", "padding": [12, 16], "gap": 12,
"alignItems": "center",
"stroke": {"fill": "$--border", "thickness": 1},
"effect": {"type": "shadow", "offset": {"x": 0, "y": 4}, "blur": 12, "color": "#0000001A"},
"theme": {"Mode": "Light"},
"children": [
{"type": "icon_font", "id": "Toast_icon", "width": 20, "height": 20,
"fill": "$--accent-green", "iconFontFamily": "lucide", "iconFontName": "check-circle"},
{"type": "frame", "id": "Toast_content", "layout": "vertical",
"width": "fill_container", "gap": 2, "children": [
{"type": "text", "id": "Toast_title", "content": "Success",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "600"},
{"type": "text", "id": "Toast_msg", "content": "Your changes have been saved.",
"fill": "$--muted-foreground", "fontFamily": "Inter", "fontSize": 12, "lineHeight": 1.4}
]},
{"type": "icon_font", "id": "Toast_close", "width": 14, "height": 14,
"fill": "$--muted-foreground", "iconFontFamily": "lucide", "iconFontName": "x"}
]
}
def make_component_ai_chat_message(x, y):
return {
"type": "frame", "id": "comp_AIChatMessage", "name": "component/AIChatMessage",
"reusable": True, "x": x, "y": y,
"width": "fill_container(400)", "layout": "horizontal",
"gap": 12, "padding": [12, 16],
"theme": {"Mode": "Light"},
"children": [
{"type": "frame", "id": "AIM_avatar", "width": 28, "height": 28,
"cornerRadius": 14, "fill": "$--accent-purple-light",
"alignItems": "center", "justifyContent": "center",
"children": [
{"type": "icon_font", "id": "AIM_avatarIcon", "width": 16, "height": 16,
"fill": "$--accent-purple", "iconFontFamily": "lucide", "iconFontName": "sparkles"}
]},
{"type": "frame", "id": "AIM_body", "layout": "vertical",
"width": "fill_container", "gap": 4, "children": [
{"type": "text", "id": "AIM_sender", "content": "AI Assistant",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "600"},
{"type": "text", "id": "AIM_text",
"content": "Here's a summary of your recent notes...",
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 13,
"lineHeight": 1.5, "textGrowth": "fixed-width", "width": "fill_container"}
]}
]
}
# ─── Component showcase frame ─────────────────────────────────────
def make_component_showcase(x, y, title, components):
"""Creates a frame showing all component instances in a row for visual reference."""
children = [
{"type": "text", "id": f"showcase_{title.replace(' ','_')}_title",
"content": title,
"fill": "$--foreground", "fontFamily": "Inter", "fontSize": 18, "fontWeight": "600"},
]
for comp_id, label in components:
children.append({
"type": "frame", "id": f"show_{comp_id}", "layout": "vertical", "gap": 8,
"padding": [12, 0], "children": [
{"type": "text", "id": f"show_{comp_id}_lbl", "content": label,
"fill": "$--muted-foreground", "fontFamily": "IBM Plex Mono", "fontSize": 10,
"letterSpacing": 0.5},
{"type": "ref", "id": f"show_{comp_id}_ref", "ref": comp_id}
]
})
sid = f"showcase_{title.replace(' ', '_')}"
return {
"type": "frame", "id": sid, "name": f"Showcase — {title}",
"x": x, "y": y, "width": 1440,
"fill": "$--background", "layout": "vertical",
"gap": 24, "padding": 40,
"theme": {"Mode": "Light"},
"children": children
}
# ─── Feature spec grouping ────────────────────────────────────────
FEATURE_GROUPS = {
"Tabs & Navigation": [
"dt0", "dtj", "dt11", "rnt0", "rne0", "rns0",
"archBtnNorm", "archBtnArc", "srtDD", "srtBN"
],
"Inspector & Properties": [
"pi01", "pi50", "rbF01", "rbF02", "rbF03",
"reF01", "reF02", "reF03",
"urlDefault", "urlHover", "urlEdit"
],
"Note List & Virtual List": [
"vl001", "vl100", "vl200", "vl300",
"mni01", "dp01"
],
"Sidebar & Sections": [
"dsg01a", "dsg02w", "dsg04j", "csB01", "csA01", "trSB1"
],
"Modals & Settings": [
"iogBH", "ghv01", "ghv02", "ghv03", "ghv04"
],
"Editor & Wikilinks": [
"wlc01", "wlc20", "mb001", "mb011"
],
"Status & Indicators": [
"mni10", "mni20", "dp10", "dp20", "dp30"
],
"Git & Changes": [
"vc001", "vc100", "vc200", "ghCL1", "ghDO1"
],
"Search": [
"3aG9b", "K1O2x", "nrIcZ"
],
"Trash": [
"trNL1", "trRI1", "trW30"
],
}
# ─── Main transform ──────────────────────────────────────────────
def transform():
data = load()
# 1. Add new variables (merge, don't replace)
for k, v in NEW_VARIABLES.items():
data["variables"][k] = v
# 2. Build an index of existing children by ID
existing = {c["id"]: c for c in data["children"]}
# 3. Fix hardcoded colors in ALL existing frames
for child in data["children"]:
replace_hardcoded_colors(child)
# 4. Build new ordered children list
new_children = []
# ─── Section 0: Cover (y=0) ───
new_children.append(make_cover(0, 0))
# ─── Section 1: Foundations (y=500) ───
new_children.append(make_section_header("sec1_hdr", "1 — Foundations", 0, 500))
# Move existing Color Palette
if "mOf4J" in existing:
cp = existing.pop("mOf4J")
cp["x"] = 0; cp["y"] = 600
new_children.append(cp)
# Move existing Typography & Spacing
if "HZonq" in existing:
ts = existing.pop("HZonq")
ts["x"] = 0; ts["y"] = 1600
new_children.append(ts)
# New foundation frames
new_children.append(make_spacing_scale(0, 3000))
new_children.append(make_heights_ref(800, 3000))
new_children.append(make_shadows_ref(0, 3600))
# ─── Section 2: Components (y=4200) ───
new_children.append(make_section_header("sec2_hdr", "2 — Components", 0, 4200))
# Subsection labels
new_children.append(make_subsection_label("sub_atoms", "ATOMS", 0, 4290))
# Atom components (placed as reusable definitions)
comp_y_atoms = 4320
atom_components = [
make_component_status_dot(0, comp_y_atoms),
make_component_separator(100, comp_y_atoms),
make_component_badge(400, comp_y_atoms),
make_component_button_primary(0, comp_y_atoms + 60),
make_component_button_secondary(200, comp_y_atoms + 60),
make_component_button_ghost(400, comp_y_atoms + 60),
make_component_input(0, comp_y_atoms + 120),
make_component_icon_button(300, comp_y_atoms + 120),
]
new_children.extend(atom_components)
# Atom showcase
new_children.append(make_component_showcase(0, comp_y_atoms + 200, "Atoms", [
("comp_StatusDot", "StatusDot"),
("comp_Separator", "Separator"),
("comp_Badge", "Badge"),
("comp_ButtonPrimary", "Button/Primary"),
("comp_ButtonSecondary", "Button/Secondary"),
("comp_ButtonGhost", "Button/Ghost"),
("comp_Input", "Input"),
("comp_IconButton", "IconButton"),
]))
# Subsection: Molecules
mol_y = 4900
new_children.append(make_subsection_label("sub_molecules", "MOLECULES", 0, mol_y - 30))
molecule_components = [
make_component_inspector_header(0, mol_y),
make_component_note_list_item(0, mol_y + 60),
make_component_tab_active(400, mol_y),
make_component_tab_inactive(600, mol_y),
make_component_property_row(0, mol_y + 160),
make_component_relationship_pill(0, mol_y + 220),
make_component_sidebar_filter_item(400, mol_y + 160),
make_component_section_label(0, mol_y + 280),
make_component_section_count_header(400, mol_y + 280),
make_component_add_button(0, mol_y + 340),
make_component_rel_group_label(400, mol_y + 340),
make_component_command_palette_item(0, mol_y + 400),
make_component_editable_value(0, mol_y + 460),
]
new_children.extend(molecule_components)
# Molecule showcase
new_children.append(make_component_showcase(0, mol_y + 520, "Molecules", [
("comp_InspectorHeader", "InspectorHeader"),
("comp_NoteListItem", "NoteListItem"),
("comp_TabActive", "Tab/Active"),
("comp_TabInactive", "Tab/Inactive"),
("comp_PropertyRow", "PropertyRow"),
("comp_RelationshipPill", "RelationshipPill"),
("comp_SidebarFilterItem", "SidebarFilterItem"),
("comp_SectionLabel", "SectionLabel"),
("comp_SectionCountHeader", "SectionCountHeader"),
("comp_AddButton", "AddButton"),
("comp_RelGroupLabel", "RelGroupLabel"),
("comp_CommandPaletteItem", "CommandPaletteItem"),
("comp_EditableValue", "EditableValue"),
]))
# Subsection: Organisms
org_y = 5900
new_children.append(make_subsection_label("sub_organisms", "ORGANISMS", 0, org_y - 30))
new_children.append(make_component_toast(0, org_y))
new_children.append(make_component_ai_chat_message(0, org_y + 80))
new_children.append(make_component_showcase(0, org_y + 200, "Organisms", [
("comp_Toast", "Toast"),
("comp_AIChatMessage", "AIChatMessage"),
]))
# ─── Section 3: Full Layouts (y=6600) ───
new_children.append(make_section_header("sec3_hdr", "3 — Full Layouts", 0, 6600))
# Layout frames — move existing ones
layout_ids = ["qHhaj", "SC_all", "SC_nosb", "SC_edonly", "SC_ednl"]
layout_y = 6700
for lid in layout_ids:
if lid in existing:
frame = existing.pop(lid)
frame["x"] = 0
frame["y"] = layout_y
new_children.append(frame)
layout_y += 1000 # 900px height + 100px gap
# ─── Section 4: Feature Specs (y=12200) ───
spec_y = 12200
new_children.append(make_section_header("sec4_hdr", "4 — Feature Specs", 0, spec_y))
spec_y += 100
placed_ids = set()
for group_name, group_ids in FEATURE_GROUPS.items():
# Group label
new_children.append(make_subsection_label(
f"fg_{group_name.replace(' ', '_').replace('&','and').lower()[:20]}",
group_name.upper(), 0, spec_y
))
spec_y += 30
group_x = 0
max_h = 0
for fid in group_ids:
if fid in existing:
frame = existing.pop(fid)
fw = frame.get("width", 300)
if isinstance(fw, str):
fw = 340 # fallback for dynamic widths
# Wrap to next row if too wide
if group_x + fw > 3000:
spec_y += max_h + 40
group_x = 0
max_h = 0
frame["x"] = group_x
frame["y"] = spec_y
new_children.append(frame)
placed_ids.add(fid)
fh = frame.get("height", 200)
if isinstance(fh, str):
fh = 400 # fallback
max_h = max(max_h, fh)
group_x += fw + 100
spec_y += max_h + 80
# Any remaining unplaced frames go at the end
for fid, frame in existing.items():
if fid not in placed_ids:
frame["x"] = 0
frame["y"] = spec_y
new_children.append(frame)
fh = frame.get("height", 200)
if isinstance(fh, str):
fh = 400
spec_y += fh + 100
# 5. Replace children
data["children"] = new_children
# 6. Save
save(data)
# Stats
reusable_count = sum(1 for c in new_children if c.get("reusable"))
print(f"✅ Restructured ui-design.pen")
print(f" Total nodes: {len(new_children)}")
print(f" Reusable components: {reusable_count}")
print(f" Variables: {len(data['variables'])}")
print(f" Hardcoded colors replaced in existing frames")
print(f" Canvas organized into 5 sections")
if __name__ == "__main__":
transform()