Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Design System - VoIdea
|
||||
|
||||
**Purpose:** Unified source of truth for UI across all platforms
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [tokens.json](tokens.json) - Primary source of truth
|
||||
2. [tokens.yaml](tokens.yaml) - YAML version
|
||||
3. [generators/](generators/) - Platform-specific generators
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Design tokens are the single source of truth for:
|
||||
- Colors
|
||||
- Typography
|
||||
- Spacing
|
||||
- Border radius
|
||||
- Shadows
|
||||
- Transitions
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Web (CSS)
|
||||
`ash
|
||||
python generators/css_generator.py
|
||||
`
|
||||
|
||||
### iOS (Swift)
|
||||
`ash
|
||||
python generators/swift_generator.py
|
||||
`
|
||||
|
||||
### Android (Kotlin)
|
||||
`ash
|
||||
python generators/kotlin_generator.py
|
||||
`
|
||||
|
||||
---
|
||||
|
||||
## Themes
|
||||
|
||||
1. **system** - Auto-detect based on OS preference
|
||||
2. **dark** - Dark theme
|
||||
3. **light** - Light theme
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
Design tokens are auto-updated by system agents.
|
||||
Never edit generated files manually.
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-05-10*
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Generate CSS custom properties from design tokens."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
_FILE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(_FILE_DIR)))
|
||||
TOKENS_PATH = os.path.join(ROOT, "docs", "design-system", "tokens.json")
|
||||
OUTPUT_PATH = os.path.join(ROOT, "app", "design-tokens", "css", "theme.css")
|
||||
|
||||
|
||||
def load_tokens() -> dict:
|
||||
with open(TOKENS_PATH, encoding="utf-8-sig") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def generate(tokens: dict) -> str:
|
||||
lines = [
|
||||
"/* Auto-generated from design tokens — do not edit manually */",
|
||||
":root {",
|
||||
]
|
||||
|
||||
for color_name, shades in tokens.get("colors", {}).items():
|
||||
if isinstance(shades, dict):
|
||||
for shade, value in shades.items():
|
||||
if shade != "system":
|
||||
if shade == "default":
|
||||
lines.append(f" --color-{color_name}: {value};")
|
||||
elif shade == "hover":
|
||||
lines.append(f" --color-{color_name}-hover: {value};")
|
||||
else:
|
||||
lines.append(f" --color-{color_name}-{shade}: {value};")
|
||||
|
||||
for family_name, value in tokens.get("typography", {}).get("font_family", {}).items():
|
||||
lines.append(f" --font-{family_name}: {value};")
|
||||
|
||||
for size_name, value in tokens.get("typography", {}).get("size", {}).items():
|
||||
lines.append(f" --font-size-{size_name}: {value};")
|
||||
|
||||
for space_name, value in tokens.get("spacing", {}).items():
|
||||
lines.append(f" --spacing-{space_name}: {value};")
|
||||
|
||||
for radius_name, value in tokens.get("border_radius", {}).items():
|
||||
lines.append(f" --radius-{radius_name}: {value};")
|
||||
|
||||
lines.append("}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
tokens = load_tokens()
|
||||
css = generate(tokens)
|
||||
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
|
||||
f.write(css)
|
||||
print(f"Written: {OUTPUT_PATH}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Generate Android colors.xml from design tokens."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
from xml.dom import minidom
|
||||
|
||||
_FILE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(_FILE_DIR)))
|
||||
TOKENS_PATH = os.path.join(ROOT, "docs", "design-system", "tokens.json")
|
||||
OUTPUT_PATH = os.path.join(ROOT, "app", "design-tokens", "kotlin", "colors.xml")
|
||||
|
||||
|
||||
def load_tokens() -> dict:
|
||||
with open(TOKENS_PATH, encoding="utf-8-sig") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def hex_to_argb(hex_color: str) -> str:
|
||||
h = hex_color.lstrip("#")
|
||||
if len(h) == 6:
|
||||
return f"FF{h}"
|
||||
return h
|
||||
|
||||
|
||||
def generate(tokens: dict) -> str:
|
||||
root = ET.Element("resources")
|
||||
|
||||
for color_name, shades in tokens.get("colors", {}).items():
|
||||
if isinstance(shades, dict):
|
||||
for shade, value in shades.items():
|
||||
if shade not in ("system",) and isinstance(value, str) and value.startswith("#"):
|
||||
res_name = f"{color_name}_{shade}"
|
||||
argb = hex_to_argb(value)
|
||||
child = ET.SubElement(root, "color")
|
||||
child.set("name", res_name)
|
||||
child.text = f"#{argb}"
|
||||
|
||||
for space_name, value in tokens.get("spacing", {}).items():
|
||||
dp = value.replace("rem", "").strip()
|
||||
child = ET.SubElement(root, "dimen")
|
||||
child.set("name", f"spacing_{space_name}")
|
||||
child.text = f"{float(dp) * 16:.0f}dp"
|
||||
|
||||
rough_string = ET.tostring(root, encoding="unicode")
|
||||
reparsed = minidom.parseString(rough_string)
|
||||
return '<?xml version="1.0" encoding="utf-8"?>\n' + reparsed.toprettyxml(indent=" ")
|
||||
|
||||
|
||||
def main():
|
||||
tokens = load_tokens()
|
||||
xml = generate(tokens)
|
||||
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
|
||||
f.write(xml)
|
||||
print(f"Written: {OUTPUT_PATH}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Generate Swift Color enum from design tokens."""
|
||||
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
|
||||
_FILE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(_FILE_DIR)))
|
||||
TOKENS_PATH = os.path.join(ROOT, "docs", "design-system", "tokens.json")
|
||||
OUTPUT_PATH = os.path.join(ROOT, "app", "design-tokens", "swift", "Colors.swift")
|
||||
|
||||
|
||||
def load_tokens() -> dict:
|
||||
with open(TOKENS_PATH, encoding="utf-8-sig") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
|
||||
h = hex_color.lstrip("#")
|
||||
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
||||
|
||||
|
||||
def generate(tokens: dict) -> str:
|
||||
lines = [
|
||||
"// Auto-generated from design tokens — do not edit manually",
|
||||
"import SwiftUI",
|
||||
"",
|
||||
"extension Color {",
|
||||
]
|
||||
|
||||
for color_name, shades in tokens.get("colors", {}).items():
|
||||
if isinstance(shades, dict):
|
||||
for shade, value in shades.items():
|
||||
if shade not in ("system",) and isinstance(value, str) and value.startswith("#"):
|
||||
try:
|
||||
r, g, b = hex_to_rgb(value)
|
||||
suffix = shade.capitalize()
|
||||
swift_name = f"{color_name}{suffix}"
|
||||
lines.append(f" static let {swift_name} = Color(red: {r/255:.4f}, green: {g/255:.4f}, blue: {b/255:.4f})")
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
for space_name, value in tokens.get("spacing", {}).items():
|
||||
lines.append(f" static let spacing{space_name.capitalize()} = CGFloat({value.replace('rem', '').strip()})")
|
||||
|
||||
lines.append("}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
tokens = load_tokens()
|
||||
swift = generate(tokens)
|
||||
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
|
||||
f.write(swift)
|
||||
print(f"Written: {OUTPUT_PATH}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"project": "VoIdea",
|
||||
"updated": "2026-05-10",
|
||||
"themes": ["system", "dark", "light"],
|
||||
"colors": {
|
||||
"primary": {
|
||||
"50": "#EEF2FF",
|
||||
"500": "#6366F1",
|
||||
"600": "#4F46E5",
|
||||
"default": "#6366F1",
|
||||
"hover": "#4F46E5"
|
||||
},
|
||||
"background": {
|
||||
"system": "auto",
|
||||
"dark": "#0F172A",
|
||||
"light": "#FFFFFF"
|
||||
},
|
||||
"text": {
|
||||
"primary": {
|
||||
"system": "auto",
|
||||
"dark": "#F8FAFC",
|
||||
"light": "#0F172A"
|
||||
}
|
||||
},
|
||||
"semantic": {
|
||||
"error": "#EF4444",
|
||||
"warning": "#F59E0B",
|
||||
"success": "#22C55E",
|
||||
"info": "#3B82F6"
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
"font_family": {
|
||||
"primary": "Inter, system-ui, sans-serif"
|
||||
},
|
||||
"size": {
|
||||
"xs": "0.75rem",
|
||||
"sm": "0.875rem",
|
||||
"base": "1rem",
|
||||
"lg": "1.125rem"
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
"md": "1rem",
|
||||
"lg": "1.5rem"
|
||||
},
|
||||
"border_radius": {
|
||||
"sm": "0.25rem",
|
||||
"md": "0.5rem",
|
||||
"lg": "0.75rem"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user