From 54cea1cac5c4a251207fe3499e4f17a2ab323b67 Mon Sep 17 00:00:00 2001 From: Ahmad Hakim Date: Sat, 27 Jun 2026 13:57:02 +0300 Subject: [PATCH 1/2] chore(update): buridan cli --- cli/main.py | 648 ++++++++++++++++++++---------------- cli/scrollbar_css.py | 444 ++++++++++++++++++++++++ cli/shimmer_css.py | 104 ++++++ cli/tailwind_config.py | 58 ++++ docs/getting_started/cli.md | 59 +++- pyproject.toml | 2 +- 6 files changed, 1020 insertions(+), 295 deletions(-) create mode 100644 cli/scrollbar_css.py create mode 100644 cli/shimmer_css.py create mode 100644 cli/tailwind_config.py diff --git a/cli/main.py b/cli/main.py index 6c799af..c30120e 100644 --- a/cli/main.py +++ b/cli/main.py @@ -1,29 +1,49 @@ import argparse import ast -import os import shutil import sys from pathlib import Path -# Import registries +# ── registries ──────────────────────────────────────────────────────────────── from app.registry.colors import COLOR_THEMES from app.registry.components import COMPONENT_REGISTRY from app.registry.fonts import FONT_REGISTRY from app.registry.radii import RADIUS_OPTIONS from app.registry.styles import STYLE_REGISTRY from app.registry.themes import BASE_THEMES +from cli.scrollbar_css import SCROLLBAR_CSS +from cli.shimmer_css import SHIMMER_CSS + +# ── string constants ────────────────────────────────────────────────────────── +from cli.tailwind_config import TAILWIND_CONFIG_SNIPPET + +# ── utilities bundle injected by `buridan init` ─────────────────────────────── +# Order matters — each block is appended in sequence. +UTILITIES_BUNDLE = [ + ("shimmer", SHIMMER_CSS), + ("scrollbar", SCROLLBAR_CSS), +] + +# Sentinel comments used to detect whether a utility is already present. +UTILITY_SENTINELS = { + "shimmer": "/* ── Shimmer utility", + "scrollbar": "/* ── Scrollbar utilities", +} + + +# ── helpers ─────────────────────────────────────────────────────────────────── CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -def from_base62(s): +def from_base62(s: str) -> int: n = 0 for i in range(len(s) - 1, -1, -1): n = n * 62 + CHARS.find(s[i]) return n -def flatten_vars(obj): +def flatten_vars(obj: dict) -> dict: out = {} for k, v in obj.items(): key = "--radius" if k == "radius" else f"--{k}" @@ -31,35 +51,32 @@ def flatten_vars(obj): return out -def rebuild_theme(config): +def rebuild_theme(config: dict) -> dict: base_id = config.get("baseId") color_id = config.get("colorId") chart_id = config.get("chartId") style_id = config.get("styleId") font_id = config.get("fontId") radius = config.get("radius") - dark_mode = config.get("darkMode", False) + dark = config.get("darkMode", False) base = next((b for b in BASE_THEMES if b["id"] == base_id), None) if not base: return {} - base_vars = flatten_vars(base["dark"] if dark_mode else base["light"]) - theme = {**base_vars} + theme = {**flatten_vars(base["dark"] if dark else base["light"])} if color_id: color = next((c for c in COLOR_THEMES if c["id"] == color_id), None) if color: - cvars = flatten_vars(color["dark"] if dark_mode else color["light"]) - for k, v in cvars.items(): + for k, v in flatten_vars(color["dark"] if dark else color["light"]).items(): if not k.startswith("--chart-"): theme[k] = v if chart_id: chart = next((c for c in COLOR_THEMES if c["id"] == chart_id), None) if chart: - cvars = flatten_vars(chart["dark"] if dark_mode else chart["light"]) - for k, v in cvars.items(): + for k, v in flatten_vars(chart["dark"] if dark else chart["light"]).items(): if k.startswith("--chart-"): theme[k] = v @@ -79,7 +96,7 @@ def rebuild_theme(config): return theme -def decode_seed(seed): +def decode_seed(seed: str) -> dict | None: if not seed: return None if seed == "b0": @@ -122,351 +139,418 @@ def decode_seed(seed): return None -def generate_from_seed(seed, dark_mode): - config = decode_seed(seed) - if not config: - # For simplicity in CLI, we only support valid encoded seeds - print(f"Error: Invalid preset ID '{seed}'") - sys.exit(1) - - config["darkMode"] = dark_mode - return rebuild_theme(config) +def format_css(config: dict) -> str: + return "\n".join(f" {k}: {v};" for k, v in config.items() if k.startswith("--")) -def format_css(config): - return "\n".join( - [f" {k}: {v};" for k, v in config.items() if k.startswith("--")] - ) +def generate_theme_css(seed: str) -> str: + def build(dark): + return format_css(rebuild_theme({**decode_seed(seed), "darkMode": dark})) - -def generate_full_css(seed): - light_config = generate_from_seed(seed, False) - dark_config = generate_from_seed(seed, True) - return f":root {{\n{format_css(light_config)}\n}}\n\n.dark {{\n{format_css(dark_config)}\n}}" - - -TAILWIND_CONFIG_SNIPPET = """ rx.plugins.TailwindV4Plugin( - TailwindConfig( - darkMode="class", - plugins=["@tailwindcss/typography", "tailwind-scrollbar"], - theme={ - "extend": { - "colors": { - "background": "var(--background)", - "foreground": "var(--foreground)", - "card": "var(--card)", - "card-foreground": "var(--card-foreground)", - "popover": "var(--popover)", - "popover-foreground": "var(--popover-foreground)", - "primary": "var(--primary)", - "primary-foreground": "var(--primary-foreground)", - "secondary": "var(--secondary)", - "secondary-foreground": "var(--secondary-foreground)", - "muted": "var(--muted)", - "muted-foreground": "var(--muted-foreground)", - "accent": "var(--accent)", - "accent-foreground": "var(--accent-foreground)", - "destructive": "var(--destructive)", - "border": "var(--border)", - "input": "var(--input)", - "ring": "var(--ring)", - "chart-1": "var(--chart-1)", - "chart-2": "var(--chart-2)", - "chart-3": "var(--chart-3)", - "chart-4": "var(--chart-4)", - "chart-5": "var(--chart-5)", - "sidebar": "var(--sidebar)", - "sidebar-foreground": "var(--sidebar-foreground)", - "sidebar-primary": "var(--sidebar-primary)", - "sidebar-primary-foreground": "var(--sidebar-primary-foreground)", - "sidebar-accent": "var(--sidebar-accent)", - "sidebar-accent-foreground": "var(--sidebar-accent-foreground)", - "sidebar-border": "var(--sidebar-border)", - "sidebar-ring": "var(--sidebar-ring)", - }, - "fontFamily": { - "theme": "var(--font-family)", - }, - "borderRadius": { - "radius": "var(--radius)", - }, - "padding": { - "card": "var(--card-padding)", - }, - "gap": { - "card": "var(--card-gap)", - }, - "boxShadow": { - "default": "var(--shadow)", - }, - } - }, - ) - ),""" + return f":root {{\n{build(False)}\n}}\n\n.dark {{\n{build(True)}\n}}" -def get_source_root(): - """Locate the Buridan UI source components.""" - # 1. Check relative to this file (development mode) - # cli/main.py -> ROOT +def get_source_root() -> Path | None: source_root = Path(__file__).parent.parent if (source_root / "components").exists(): return source_root - - # 2. Try to find the 'components' package try: import components + return Path(components.__file__).parent.parent except (ImportError, AttributeError): return None -def resolve_dependencies(component_names, registry): - """Recursively resolve dependencies for a list of components.""" - required_files = set() - visited_components = set() +def resolve_dependencies(component_names: list[str], registry: dict) -> list[str]: + required_files: set[str] = set() + visited: set[str] = set() - def add_component(name): - name_lower = name.lower() - if name_lower in visited_components: + def add(name: str): + key = name.lower() + if key in visited: return - visited_components.add(name_lower) - - entry = registry.get(name_lower) + visited.add(key) + entry = registry.get(key) if not entry: print(f"Warning: Component '{name}' not found in registry.") return - - for file_path in entry["files"]: - required_files.add(file_path) - + for f in entry["files"]: + required_files.add(f) for dep in entry.get("dependencies", []): - add_component(dep) + add(dep) for name in component_names: - add_component(name) + add(name) - return sorted(list(required_files)) + return sorted(required_files) -def add_components_to_project(component_names, target_root=None): - """Add components and their dependencies to the target project.""" +BLOCK_SOURCE_PREFIX = "app/www/library/blocks/" + + +def remap_dest(rel: str) -> str: + """Remap block source paths to blocks/ in the user's project root.""" + if rel.startswith(BLOCK_SOURCE_PREFIX): + return "blocks/" + Path(rel).name + return rel + + +def add_components_to_project( + component_names: list[str], target_root: Path = None +) -> bool: if target_root is None: target_root = Path.cwd() if not (target_root / "rxconfig.py").exists(): - print("Error: rxconfig.py not found. Please run this command in a Reflex project root.") + print( + "Error: rxconfig.py not found. Please run this command in a Reflex project root." + ) return False source_root = get_source_root() if not source_root: - print("Error: Could not locate Buridan source components. Are you in the repo or is it installed?") + print("Error: Could not locate Buridan source components.") return False - files_to_copy = resolve_dependencies(component_names, COMPONENT_REGISTRY) - if not files_to_copy: + files = resolve_dependencies(component_names, COMPONENT_REGISTRY) + if not files: print("No files to copy.") return False - for rel_path_str in files_to_copy: - src_path = source_root / rel_path_str - dest_path = target_root / rel_path_str + for rel in files: + src = source_root / rel + dest = target_root / remap_dest(rel) - if not src_path.exists(): - print(f"Warning: Source file {src_path} does not exist.") + if not src.exists(): + print(f"Warning: Source file {src} does not exist.") continue - # Create target directory - dest_path.parent.mkdir(parents=True, exist_ok=True) + dest.parent.mkdir(parents=True, exist_ok=True) - # Ensure __init__.py files exist up to the components root - current_dir = dest_path.parent - while current_dir != target_root and current_dir.name != "": - init_file = current_dir / "__init__.py" - if not init_file.exists(): - init_file.touch() - if current_dir.name == "components": + # ensure __init__.py chain exists up to components/ or blocks/ + current = dest.parent + while current != target_root: + init = current / "__init__.py" + if not init.exists(): + init.touch() + if current.name in ("components", "blocks"): break - current_dir = current_dir.parent + current = current.parent - # Copy the file - shutil.copy2(src_path, dest_path) - print(f"✓ Added {rel_path_str}") + shutil.copy2(src, dest) + print(f"✓ Added {remap_dest(rel)}") return True -def init(preset, include): +def patch_rxconfig(config_path: Path) -> None: + """Inject TailwindV4 plugin into rxconfig.py if not already present.""" + content = config_path.read_text() + + if "var(--background)" in content: + print("• rxconfig.py already contains Buridan UI theme tokens.") + return + + try: + tree = ast.parse(content) + snippet = TAILWIND_CONFIG_SNIPPET.strip().rstrip(",") + lines = content.splitlines() + modified = False + + for node in ast.walk(tree): + if not isinstance(node, ast.Assign): + continue + call = node.value + if not ( + isinstance(call, ast.Call) + and isinstance(call.func, ast.Attribute) + and call.func.attr.lower() == "config" + and getattr(call.func.value, "id", "") == "rx" + ): + continue + + plugins_kw = next((kw for kw in call.keywords if kw.arg == "plugins"), None) + + if plugins_kw: + for i in range(call.lineno - 1, call.end_lineno): + if "plugins=" in lines[i]: + lines[i] = lines[i].replace( + "plugins=[", f"plugins=[\n{snippet}," + ) + modified = True + break + else: + for i in range(call.lineno - 1, call.end_lineno): + if "Config(" in lines[i] or "config(" in lines[i]: + indent = " " * (len(lines[i]) - len(lines[i].lstrip())) + lines[i] = lines[i].replace( + "(", f"(\n{indent} plugins=[\n{snippet}\n{indent} ]," + ) + modified = True + break + + if modified: + content = "\n".join(lines) + content = content.replace("rx.plugins.TailwindV4Plugin(),", "") + content = content.replace("rx.plugins.TailwindV4Plugin()", "") + content = content.replace(",,", ",") + break + + if not modified: + if "rx.Config(" in content: + content = content.replace( + "rx.Config(", f"rx.Config(\n plugins=[\n{snippet}\n ]," + ) + modified = True + elif "rx.config(" in content: + content = content.replace( + "rx.config(", f"rx.config(\n plugins=[\n{snippet}\n ]," + ) + modified = True + + if modified: + import_stmt = "from reflex.plugins.shared_tailwind import TailwindConfig" + if import_stmt not in content: + content = f"{import_stmt}\n{content}" + config_path.write_text(content) + print("✓ Updated rxconfig.py with Buridan UI Tailwind config.") + else: + print( + "Warning: Could not automatically update rxconfig.py. Please configure manually." + ) + + except Exception as e: + print(f"Warning: Could not parse rxconfig.py ({e}). Please configure manually.") + + +# ── commands ────────────────────────────────────────────────────────────────── + + +def cmd_init(): + """ + Initialize Buridan UI utilities in an existing Reflex project. + + - Locates or creates assets/globals.css + - Appends CSS utilities (shimmer, scrollbar) if not already present + - Updates rxconfig.py with TailwindV4 plugin config + """ root = Path.cwd() - config_path = root / "rxconfig.py" - if not config_path.exists(): + if not (root / "rxconfig.py").exists(): print( "Error: rxconfig.py not found. Please run this command in a Reflex project root." ) - return + sys.exit(1) - # 1. Generate globals.css + # ── ensure assets/globals.css exists ───────────────────────────────────── assets_dir = root / "assets" assets_dir.mkdir(exist_ok=True) css_path = assets_dir / "globals.css" - css_content = generate_full_css(preset) - css_path.write_text(css_content) - print(f"✓ Created {css_path.relative_to(root)}") - - # 2. Update rxconfig.py Safely - config_content = config_path.read_text() - if "var(--background)" in config_content: - print(f"• rxconfig.py already contains Buridan UI theme tokens.") + if not css_path.exists(): + css_path.write_text("") + print(f"✓ Created {css_path.relative_to(root)}") else: - try: - tree = ast.parse(config_content) - modified = False - clean_snippet = TAILWIND_CONFIG_SNIPPET.strip().rstrip(",") - - # Look for ANY assignment that looks like a Reflex config - for node in ast.walk(tree): - if isinstance(node, ast.Assign): - # Check if the right side is rx.Config(...) or rx.config(...) - call_node = node.value - if ( - isinstance(call_node, ast.Call) - and isinstance(call_node.func, ast.Attribute) - and call_node.func.attr.lower() == "config" - and getattr(call_node.func.value, "id", "") == "rx" - ): - # Found it! - lines = config_content.splitlines() - plugins_kw = next( - (kw for kw in call_node.keywords if kw.arg == "plugins"), - None, - ) + print(f"• Found {css_path.relative_to(root)}") + + # ── append utilities that aren't already present ────────────────────────── + current_css = css_path.read_text() + added = [] + + for name, css in UTILITIES_BUNDLE: + sentinel = UTILITY_SENTINELS[name] + if sentinel in current_css: + print(f"• {name} utility already present in globals.css — skipping.") + else: + current_css += f"\n{css}" + added.append(name) + + if added: + css_path.write_text(current_css) + for name in added: + print(f"✓ Added {name} utility to globals.css") + + # ── patch rxconfig.py ───────────────────────────────────────────────────── + patch_rxconfig(root / "rxconfig.py") + + # ── next steps ──────────────────────────────────────────────────────────── + print("\n✓ Buridan UI initialized successfully.") + print("\nNext steps:") + print(" 1. Add globals.css to your app stylesheets:") + print(' app = rx.App(stylesheets=["globals.css"])') + print(" 2. Apply a theme:") + print(" buridan apply --preset b0") + print(" 3. Add components:") + print(" buridan add button") + + +def cmd_apply(preset: str): + """ + Apply a theme preset to the project. + + Decodes the preset seed and writes :root / .dark CSS variables + to assets/globals.css, preserving any existing utility blocks. + """ + root = Path.cwd() - if plugins_kw: - # Scenario 1: plugins=[] exists - target_line_idx = -1 - # Search only within the call node's range - for i in range(call_node.lineno - 1, call_node.end_lineno): - if "plugins=" in lines[i]: - target_line_idx = i - break - - if target_line_idx != -1: - lines[target_line_idx] = lines[target_line_idx].replace( - "plugins=[", f"plugins=[\n{clean_snippet}," - ) - config_content = "\n".join(lines) - modified = True - else: - # Scenario 2: plugins keyword is missing - target_line_idx = -1 - for i in range(call_node.lineno - 1, call_node.end_lineno): - if "Config(" in lines[i] or "config(" in lines[i]: - target_line_idx = i - break - - if target_line_idx != -1: - indent = " " * (len(lines[target_line_idx]) - len(lines[target_line_idx].lstrip())) - lines[target_line_idx] = lines[target_line_idx].replace( - "(", f"(\n{indent} plugins=[\n{clean_snippet}\n{indent} ]," - ) - config_content = "\n".join(lines) - modified = True - - if modified: - # Clean up potential duplicates or empty calls - config_content = config_content.replace("rx.plugins.TailwindV4Plugin(),", "") - config_content = config_content.replace("rx.plugins.TailwindV4Plugin()", "") - config_content = config_content.replace(",,", ",") - print(f"✓ Updated rxconfig.py with Buridan UI tokens.") - break - - if not modified: - # Last resort fallback: Simple string replacement if AST failed to identify nodes correctly - if "rx.Config(" in config_content or "rx.config(" in config_content: - print("• Using string replacement fallback for rxconfig.py...") - config_content = config_content.replace( - "rx.Config(", f"rx.Config(\n plugins=[\n{clean_snippet}\n ]," - ).replace( - "rx.config(", f"rx.config(\n plugins=[\n{clean_snippet}\n ]," - ) - modified = True + if not (root / "rxconfig.py").exists(): + print("Error: rxconfig.py not found. Please run this in a Reflex project root.") + sys.exit(1) - if modified: - import_stmt = "from reflex.plugins.shared_tailwind import TailwindConfig" - if import_stmt not in config_content: - config_content = f"{import_stmt}\n{config_content}" - config_path.write_text(config_content) + config = decode_seed(preset) + if not config: + print( + f"Error: Invalid preset ID '{preset}'. Get a valid preset from https://buridan-ui.com/create" + ) + sys.exit(1) + + assets_dir = root / "assets" + assets_dir.mkdir(exist_ok=True) + css_path = assets_dir / "globals.css" + + theme_css = generate_theme_css(preset) + theme_marker = ":root {" + + if css_path.exists(): + current = css_path.read_text() + if theme_marker in current: + # replace existing :root / .dark block, preserve everything after + # find where :root starts and .dark block ends + lines = current.splitlines() + start_line = next( + (i for i, l in enumerate(lines) if l.strip().startswith(":root {")), + None, + ) + if start_line is not None: + # find end of .dark block + brace_depth = 0 + end_line = start_line + in_block = False + for i, line in enumerate(lines[start_line:], start_line): + brace_depth += line.count("{") - line.count("}") + if brace_depth > 0: + in_block = True + if in_block and brace_depth == 0: + end_line = i + # keep going to also consume .dark {} + # find next block + rest = lines[end_line + 1 :] + for j, l in enumerate(rest): + if l.strip().startswith(".dark {"): + bd = 0 + for k, dl in enumerate( + lines[end_line + 1 + j :], end_line + 1 + j + ): + bd += dl.count("{") - dl.count("}") + if bd > 0: + pass + if bd == 0 and k > end_line + 1 + j: + end_line = k + break + break + break + + after = "\n".join(lines[end_line + 1 :]).lstrip("\n") + new_css = theme_css + ("\n\n" + after if after else "") + css_path.write_text(new_css) else: - print("Warning: Could not automatically update rxconfig.py. Please add the Buridan UI Tailwind snippet manually.") - - except Exception as e: - print(f"Warning: Could not parse rxconfig.py ({e}). Please configure manually.") - - # 3. Include components if full - if include == "full": - # For 'full', we'll add ALL components from the registry - all_components = [ - name - for name in COMPONENT_REGISTRY.keys() - if name - not in ["twmerge", "component", "base_ui", "hugeicon", "others_icons"] - ] - print(f"Adding all {len(all_components)} components to project...") - add_components_to_project(all_components, target_root=root) - - print("\nSuccess! Buridan UI has been initialized.") - print("Next steps:") - print("1. Update your rx.App initialization in your main file:") - print(' app = rx.App(stylesheets=["globals.css"])') - print("2. You can now add more components using 'buridan add '") + css_path.write_text(theme_css + "\n\n" + current) + else: + # no existing theme — prepend + css_path.write_text(theme_css + "\n\n" + current) + else: + css_path.write_text(theme_css) + print(f"✓ Applied preset '{preset}' to globals.css") + print("\nNext steps:") + print(" Add globals.css to your app stylesheets if you haven't already:") + print(' app = rx.App(stylesheets=["globals.css"])') -def main(): - parser = argparse.ArgumentParser(description="Buridan UI CLI") - subparsers = parser.add_subparsers(dest="command") - # init command - init_parser = subparsers.add_parser( - "init", help="Initialize Buridan UI in a project" - ) - init_parser.add_argument( - "--preset", required=True, help="Theme preset ID (e.g. b0, b2D0wqNxT)" - ) - init_parser.add_argument( - "--include", - choices=["full", "theme-only"], - default="full", - help="What to include (default: full)", +def cmd_create(): + """Open the Buridan UI theme builder in the browser.""" + import webbrowser + + url = "https://buridan.reflex.run/create" + print(f"Opening theme builder: {url}") + webbrowser.open(url) + + +def cmd_add(component_names: list[str]): + """Add one or more components and their dependencies to the project.""" + css_path = Path.cwd() / "assets" / "globals.css" + if not css_path.exists() or ":root {" not in css_path.read_text(): + print("⚠ Warning: No theme detected in assets/globals.css.") + print( + " Components use CSS variables that require a theme to render correctly." + ) + print(" Run 'buridan apply --preset b0' to apply a default theme.\n") + + if not add_components_to_project(component_names): + sys.exit(1) + """Add one or more components and their dependencies to the project.""" + css_path = Path.cwd() / "assets" / "globals.css" + if not css_path.exists() or ":root {" not in css_path.read_text(): + print("⚠ Warning: No theme detected in assets/globals.css.") + print( + " Components use CSS variables that require a theme to render correctly." + ) + print(" Run 'buridan apply --preset b0' to apply a default theme.\n") + + if not add_components_to_project(component_names): + sys.exit(1) + + +def cmd_list(): + """List all available components.""" + skip = {"twmerge", "component", "base_ui", "hugeicon", "others_icons"} + names = sorted(n for n in COMPONENT_REGISTRY if n not in skip) + print(f"Available components ({len(names)}):\n") + for name in names: + print(f" {name}") + + +# ── entry point ─────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser( + prog="buridan", + description="Buridan UI — CLI for Reflex component library", ) + sub = parser.add_subparsers(dest="command") + + # create + sub.add_parser("create", help="Open the Buridan UI theme builder in your browser") - # add command - add_parser = subparsers.add_parser( - "add", help="Add components to your project" + # apply + apply_p = sub.add_parser( + "apply", help="Apply a theme preset (from buridan-ui.com/create)" ) - add_parser.add_argument( - "components", nargs="+", help="Names of components to add" + apply_p.add_argument( + "--preset", required=True, help="Preset ID e.g. b0 or b2D0wqNxT" ) - # list command - subparsers.add_parser( - "list", help="List all available components" - ) + # add + add_p = sub.add_parser("add", help="Add components to your project") + add_p.add_argument("components", nargs="+", help="Component name(s) to add") + + # list + sub.add_parser("list", help="List all available components") args = parser.parse_args() - if args.command == "init": - init(args.preset, args.include) + if args.command == "create": + cmd_create() + elif args.command == "init": + cmd_init() + elif args.command == "apply": + cmd_apply(args.preset) elif args.command == "add": - add_components_to_project(args.components) + cmd_add(args.components) elif args.command == "list": - print("Available components:") - # Filter out utilities and icons for a cleaner list, or show all? - # Let's show all that aren't purely internal utilities - for name in sorted(COMPONENT_REGISTRY.keys()): - if name not in ["twmerge", "component", "base_ui", "hugeicon", "others_icons"]: - print(f" - {name}") + cmd_list() else: parser.print_help() diff --git a/cli/scrollbar_css.py b/cli/scrollbar_css.py new file mode 100644 index 0000000..101ee2d --- /dev/null +++ b/cli/scrollbar_css.py @@ -0,0 +1,444 @@ +SCROLLBAR_CSS = """ +/* ── Scrollbar utilities ──────────────────────────────────────────────────── + + * ────────────────────────────────────────────────────────────────────────── */ + +@property --scroll-fade-t { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --scroll-fade-b { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --scroll-fade-s { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --scroll-fade-e { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --scroll-fade-mask { + syntax: "*"; + inherits: false; +} + +@theme inline { + @keyframes scroll-fade-reveal-t { + from { + --scroll-fade-t: 0px; + } + to { + --scroll-fade-t: var( + --_scroll-fade-size-t, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + } + } + @keyframes scroll-fade-reveal-b { + from { + --scroll-fade-b: var( + --_scroll-fade-size-b, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + } + to { + --scroll-fade-b: 0px; + } + } + @keyframes scroll-fade-reveal-s { + from { + --scroll-fade-s: 0px; + } + to { + --scroll-fade-s: var( + --_scroll-fade-size-s, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + } + } + @keyframes scroll-fade-reveal-e { + from { + --scroll-fade-e: var( + --_scroll-fade-size-e, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + } + to { + --scroll-fade-e: 0px; + } + } +} + +@utility scroll-fade { + --_scroll-fade-size-t: var( + --scroll-fade-t-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --_scroll-fade-size-b: var( + --scroll-fade-b-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-block: linear-gradient( + to bottom, + transparent 0, + #000 var(--scroll-fade-t, 0px), + #000 calc(100% - var(--scroll-fade-b, 0px)), + transparent 100% + ); + -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-block)); + mask-image: var(--scroll-fade-mask, var(--scroll-fade-block)); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: + scroll-fade-reveal-t 1ms ease-in-out, + scroll-fade-reveal-b 1ms ease-in-out; + animation-timeline: scroll(self y), scroll(self y); + animation-range: + 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)), + calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))) + 100%; + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-t: var(--_scroll-fade-size-t); + --scroll-fade-b: var(--_scroll-fade-size-b); + } +} + +@utility scroll-fade-y { + --_scroll-fade-size-t: var( + --scroll-fade-t-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --_scroll-fade-size-b: var( + --scroll-fade-b-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-block: linear-gradient( + to bottom, + transparent 0, + #000 var(--scroll-fade-t, 0px), + #000 calc(100% - var(--scroll-fade-b, 0px)), + transparent 100% + ); + -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-block)); + mask-image: var(--scroll-fade-mask, var(--scroll-fade-block)); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: + scroll-fade-reveal-t 1ms ease-in-out, + scroll-fade-reveal-b 1ms ease-in-out; + animation-timeline: scroll(self y), scroll(self y); + animation-range: + 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)), + calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))) + 100%; + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-t: var(--_scroll-fade-size-t); + --scroll-fade-b: var(--_scroll-fade-size-b); + } +} + +@utility scroll-fade-x { + --_scroll-fade-size-s: var( + --scroll-fade-s-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --_scroll-fade-size-e: var( + --scroll-fade-e-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-inline: linear-gradient( + to right, + transparent 0, + #000 var(--scroll-fade-s, 0px), + #000 calc(100% - var(--scroll-fade-e, 0px)), + transparent 100% + ); + &:where([dir="rtl"], [dir="rtl"] *) { + --scroll-fade-inline: linear-gradient( + to left, + transparent 0, + #000 var(--scroll-fade-s, 0px), + #000 calc(100% - var(--scroll-fade-e, 0px)), + transparent 100% + ); + } + -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline)); + mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline)); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: + scroll-fade-reveal-s 1ms ease-in-out, + scroll-fade-reveal-e 1ms ease-in-out; + animation-timeline: scroll(self inline), scroll(self inline); + animation-range: + 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)), + calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))) + 100%; + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-s: var(--_scroll-fade-size-s); + --scroll-fade-e: var(--_scroll-fade-size-e); + } +} + +@utility scroll-fade-t { + --_scroll-fade-size-t: var( + --scroll-fade-t-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-mask: linear-gradient( + to bottom, + transparent 0, + #000 var(--scroll-fade-t, 0px), + #000 100% + ); + -webkit-mask-image: var(--scroll-fade-mask); + mask-image: var(--scroll-fade-mask); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: scroll-fade-reveal-t 1ms ease-in-out; + animation-timeline: scroll(self y); + animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)); + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-t: var(--_scroll-fade-size-t); + } +} + +@utility scroll-fade-b { + --_scroll-fade-size-b: var( + --scroll-fade-b-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-mask: linear-gradient( + to bottom, + #000 0, + #000 calc(100% - var(--scroll-fade-b, 0px)), + transparent 100% + ); + -webkit-mask-image: var(--scroll-fade-mask); + mask-image: var(--scroll-fade-mask); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: scroll-fade-reveal-b 1ms ease-in-out; + animation-timeline: scroll(self y); + animation-range: calc( + 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24)) + ) + 100%; + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-b: var(--_scroll-fade-size-b); + } +} + +@utility scroll-fade-l { + --_scroll-fade-size-s: var( + --scroll-fade-s-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-mask: linear-gradient( + to right, + transparent 0, + #000 var(--scroll-fade-s, 0px), + #000 100% + ); + -webkit-mask-image: var(--scroll-fade-mask); + mask-image: var(--scroll-fade-mask); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: scroll-fade-reveal-s 1ms ease-in-out; + animation-timeline: scroll(self x); + animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)); + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-s: var(--_scroll-fade-size-s); + } +} + +@utility scroll-fade-r { + --_scroll-fade-size-e: var( + --scroll-fade-e-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-mask: linear-gradient( + to right, + #000 0, + #000 calc(100% - var(--scroll-fade-e, 0px)), + transparent 100% + ); + -webkit-mask-image: var(--scroll-fade-mask); + mask-image: var(--scroll-fade-mask); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: scroll-fade-reveal-e 1ms ease-in-out; + animation-timeline: scroll(self x); + animation-range: calc( + 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24)) + ) + 100%; + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-e: var(--_scroll-fade-size-e); + } +} + +@utility scroll-fade-s { + --_scroll-fade-size-s: var( + --scroll-fade-s-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-mask: linear-gradient( + to right, + transparent 0, + #000 var(--scroll-fade-s, 0px), + #000 100% + ); + &:where([dir="rtl"], [dir="rtl"] *) { + --scroll-fade-mask: linear-gradient( + to left, + transparent 0, + #000 var(--scroll-fade-s, 0px), + #000 100% + ); + } + -webkit-mask-image: var(--scroll-fade-mask); + mask-image: var(--scroll-fade-mask); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: scroll-fade-reveal-s 1ms ease-in-out; + animation-timeline: scroll(self inline); + animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)); + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-s: var(--_scroll-fade-size-s); + } +} + +@utility scroll-fade-e { + --_scroll-fade-size-e: var( + --scroll-fade-e-size, + var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))) + ); + --scroll-fade-mask: linear-gradient( + to right, + #000 0, + #000 calc(100% - var(--scroll-fade-e, 0px)), + transparent 100% + ); + &:where([dir="rtl"], [dir="rtl"] *) { + --scroll-fade-mask: linear-gradient( + to left, + #000 0, + #000 calc(100% - var(--scroll-fade-e, 0px)), + transparent 100% + ); + } + -webkit-mask-image: var(--scroll-fade-mask); + mask-image: var(--scroll-fade-mask); + -webkit-mask-composite: source-in; + mask-composite: intersect; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + + @supports (animation-timeline: scroll()) { + animation: scroll-fade-reveal-e 1ms ease-in-out; + animation-timeline: scroll(self inline); + animation-range: calc( + 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24)) + ) + 100%; + animation-fill-mode: both; + } + + @supports not (animation-timeline: scroll()) { + --scroll-fade-e: var(--_scroll-fade-size-e); + } +} + +@utility scroll-fade-* { + --scroll-fade-size: calc(var(--spacing) * --value(integer)); + --scroll-fade-size: --value([length], [percentage]); +} + +@utility scroll-fade-t-* { + --scroll-fade-t-size: calc(var(--spacing) * --value(integer)); + --scroll-fade-t-size: --value([length], [percentage]); +} + +@utility scroll-fade-b-* { + --scroll-fade-b-size: calc(var(--spacing) * --value(integer)); + --scroll-fade-b-size: --value([length], [percentage]); +} + +@utility scroll-fade-s-* { + --scroll-fade-s-size: calc(var(--spacing) * --value(integer)); + --scroll-fade-s-size: --value([length], [percentage]); +} + +@utility scroll-fade-e-* { + --scroll-fade-e-size: calc(var(--spacing) * --value(integer)); + --scroll-fade-e-size: --value([length], [percentage]); +} + +@utility scroll-fade-none { + --scroll-fade-mask: none; +} + +""" diff --git a/cli/shimmer_css.py b/cli/shimmer_css.py new file mode 100644 index 0000000..7adf58d --- /dev/null +++ b/cli/shimmer_css.py @@ -0,0 +1,104 @@ +SHIMMER_CSS = """ +/* ── Shimmer utility ──────────────────────────────────────────────────────── + * Usage: class_name="shimmer w-fit text-sm text-muted-foreground" + * Modifiers: shimmer-once, shimmer-reverse, shimmer-none, + * shimmer-color-, shimmer-duration-, + * shimmer-spread-, shimmer-angle- + * ────────────────────────────────────────────────────────────────────────── */ + +@property --shimmer-angle { + syntax: ""; + inherits: true; + initial-value: 20deg; +} +@property --shimmer-image { + syntax: "*"; + inherits: false; +} +@property --shimmer-text-fill { + syntax: "*"; + inherits: false; +} + +@theme inline { + @keyframes tw-shimmer { + from { background-position: 100% 0; } + to { background-position: 0 0; } + } +} + +@utility shimmer { + --_spread: var(--shimmer-spread, calc(3ch + 40px)); + --_base: currentColor; + --_highlight: var( + --shimmer-color, + oklch(from currentColor l c h / calc(alpha * 0.2)) + ); + + background-image: var( + --shimmer-image, + linear-gradient( + calc(90deg + var(--shimmer-angle)), + var(--_base) calc(50% - var(--_spread)), + color-mix(in oklch, var(--_highlight), var(--_base) 50%) + calc(50% - var(--_spread) * 0.5), + var(--_highlight) 50%, + color-mix(in oklch, var(--_highlight), var(--_base) 50%) + calc(50% + var(--_spread) * 0.5), + var(--_base) calc(50% + var(--_spread)) + ) + ); + background-repeat: no-repeat; + background-size: calc(200% + var(--_spread) * 2) 100%; + background-position: 0 0; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: var(--shimmer-text-fill, transparent); + animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite; + + @variant dark { + --_highlight: var( + --shimmer-color, + oklch(from currentColor max(0.8, calc(l + 0.4)) c h / calc(alpha + 0.4)) + ); + } + + &:where([dir="rtl"], [dir="rtl"] *) { + animation-direction: reverse; + } +} + +@utility shimmer-once { animation-iteration-count: 1; } +@utility shimmer-reverse { animation-direction: reverse; } +@utility shimmer-none { --shimmer-image: none; --shimmer-text-fill: currentColor; } + +@utility shimmer-color-* { + --shimmer-color: --value(--color, [color]); + --shimmer-color: color-mix( + in oklch, + --value(--color, [color]) calc(--modifier(integer) * 1%), + transparent + ); +} + +@utility shimmer-duration-* { + --shimmer-duration: calc(--value(integer) * 1ms); +} + +@utility shimmer-spread-* { + --shimmer-spread: calc(var(--spacing) * --value(integer)); + --shimmer-spread: --value([length], [percentage]); +} + +@utility shimmer-angle-* { + --shimmer-angle: calc(--value(integer) * 1deg); +} + +@media (prefers-reduced-motion: reduce) { + .shimmer { + animation: none; + background-image: none; + -webkit-text-fill-color: currentColor; + } +} +""" diff --git a/cli/tailwind_config.py b/cli/tailwind_config.py new file mode 100644 index 0000000..ecbcad0 --- /dev/null +++ b/cli/tailwind_config.py @@ -0,0 +1,58 @@ +TAILWIND_CONFIG_SNIPPET = """ rx.plugins.TailwindV4Plugin( + TailwindConfig( + darkMode="class", + plugins=["@tailwindcss/typography", "tailwind-scrollbar", "tailwindcss-animate"], + theme={ + "extend": { + "colors": { + "background": "var(--background)", + "foreground": "var(--foreground)", + "card": "var(--card)", + "card-foreground": "var(--card-foreground)", + "popover": "var(--popover)", + "popover-foreground": "var(--popover-foreground)", + "primary": "var(--primary)", + "primary-foreground": "var(--primary-foreground)", + "secondary": "var(--secondary)", + "secondary-foreground": "var(--secondary-foreground)", + "muted": "var(--muted)", + "muted-foreground": "var(--muted-foreground)", + "accent": "var(--accent)", + "accent-foreground": "var(--accent-foreground)", + "destructive": "var(--destructive)", + "border": "var(--border)", + "input": "var(--input)", + "ring": "var(--ring)", + "chart-1": "var(--chart-1)", + "chart-2": "var(--chart-2)", + "chart-3": "var(--chart-3)", + "chart-4": "var(--chart-4)", + "chart-5": "var(--chart-5)", + "sidebar": "var(--sidebar)", + "sidebar-foreground": "var(--sidebar-foreground)", + "sidebar-primary": "var(--sidebar-primary)", + "sidebar-primary-foreground": "var(--sidebar-primary-foreground)", + "sidebar-accent": "var(--sidebar-accent)", + "sidebar-accent-foreground": "var(--sidebar-accent-foreground)", + "sidebar-border": "var(--sidebar-border)", + "sidebar-ring": "var(--sidebar-ring)", + }, + "fontFamily": { + "theme": "var(--font-family)", + }, + "borderRadius": { + "radius": "var(--radius)", + }, + "padding": { + "card": "var(--card-padding)", + }, + "gap": { + "card": "var(--card-gap)", + }, + "boxShadow": { + "default": "var(--shadow)", + }, + } + }, + ) + ),""" diff --git a/docs/getting_started/cli.md b/docs/getting_started/cli.md index a211537..c3d3eb1 100644 --- a/docs/getting_started/cli.md +++ b/docs/getting_started/cli.md @@ -6,35 +6,53 @@ order: 3 # buridan -Use the buridan CLI to add components and themes to your project. +Use the buridan CLI to add components, apply themes, and manage your Buridan UI project. ## Installation -Install the CLI using pip: - ```bash pip install buridan-create ``` -## Usage +All commands must be run from your Reflex project root, where `rxconfig.py` is located. + +## create + +Open the Buridan UI theme builder in your browser. Use it to customize your design system and generate a unique preset ID. -All `buridan` commands must be executed from your Reflex project root, where the `rxconfig.py` file is located. +```bash +buridan create +``` ## init -Initialize Buridan UI in your project. This command generates the `assets/globals.css` file and updates your `rxconfig.py` with the required Tailwind configuration and theme tokens. +Initialize Buridan UI in your project. This command sets up CSS utilities (shimmer, scrollbar) in `assets/globals.css` and updates `rxconfig.py` with the required Tailwind configuration. + +```bash +buridan init +``` + +## apply + +Apply a theme preset to your project. Generates `:root` and `.dark` CSS variable blocks in `assets/globals.css` based on the preset ID from the theme builder. ```bash -buridan init --preset +buridan apply --preset ``` Arguments: -- `--preset`: The theme preset ID from the Buridan UI creator. -- `--include`: Specify `full` to include all available components or `theme-only` for just the CSS and configuration. Defaults to `full`. +- `--preset`: The theme preset ID from the Buridan UI theme builder. Use `b0` for the default theme. + +Example: + +```bash +buridan apply --preset b0 +buridan apply --preset b2D0wqNxT +``` ## add -Add specific components and their required dependencies to your project. +Add components and their dependencies to your project. ```bash buridan add @@ -46,12 +64,29 @@ You can add multiple components at once: buridan add button input select ``` -This command automatically resolves and adds any required utilities, icons, or base components while maintaining the correct directory structure. +Blocks (charts, dashboards, etc.) can be added the same way: + +```bash +buridan add line_chart_01 +``` + +Components are placed in `components/`, blocks in `blocks/`. Dependencies are resolved and added automatically. + +> **Note:** Components require a theme to render correctly. Run `buridan apply` before using components. ## list -Display all available components that can be added to your project. +Display all available components and blocks. ```bash buridan list ``` + +## Recommended workflow + +```bash +buridan create # build your theme, copy the preset ID +buridan init # set up utilities and Tailwind config +buridan apply --preset # apply your theme +buridan add button input select # add the components you need +``` diff --git a/pyproject.toml b/pyproject.toml index 7956d49..9086faa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "buridan-create" -version = "0.1.9" +version = "0.1.12" description = "CLI to initialize Buridan UI design system in Reflex apps" requires-python = ">=3.11" dependencies = [ From 0d833bb1273c07920c6d8f981b0d21085c36eb0c Mon Sep 17 00:00:00 2001 From: Ahmad Hakim Date: Sat, 27 Jun 2026 13:58:17 +0300 Subject: [PATCH 2/2] chore(update): buridan cli --- assets/docs/getting-started/cli.md | 59 ++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/assets/docs/getting-started/cli.md b/assets/docs/getting-started/cli.md index c3825ad..9a9544b 100644 --- a/assets/docs/getting-started/cli.md +++ b/assets/docs/getting-started/cli.md @@ -2,35 +2,53 @@ # buridan -Use the buridan CLI to add components and themes to your project. +Use the buridan CLI to add components, apply themes, and manage your Buridan UI project. ## Installation -Install the CLI using pip: - ```bash pip install buridan-create ``` -## Usage +All commands must be run from your Reflex project root, where `rxconfig.py` is located. + +## create + +Open the Buridan UI theme builder in your browser. Use it to customize your design system and generate a unique preset ID. -All `buridan` commands must be executed from your Reflex project root, where the `rxconfig.py` file is located. +```bash +buridan create +``` ## init -Initialize Buridan UI in your project. This command generates the `assets/globals.css` file and updates your `rxconfig.py` with the required Tailwind configuration and theme tokens. +Initialize Buridan UI in your project. This command sets up CSS utilities (shimmer, scrollbar) in `assets/globals.css` and updates `rxconfig.py` with the required Tailwind configuration. + +```bash +buridan init +``` + +## apply + +Apply a theme preset to your project. Generates `:root` and `.dark` CSS variable blocks in `assets/globals.css` based on the preset ID from the theme builder. ```bash -buridan init --preset +buridan apply --preset ``` Arguments: -- `--preset`: The theme preset ID from the Buridan UI creator. -- `--include`: Specify `full` to include all available components or `theme-only` for just the CSS and configuration. Defaults to `full`. +- `--preset`: The theme preset ID from the Buridan UI theme builder. Use `b0` for the default theme. + +Example: + +```bash +buridan apply --preset b0 +buridan apply --preset b2D0wqNxT +``` ## add -Add specific components and their required dependencies to your project. +Add components and their dependencies to your project. ```bash buridan add @@ -42,12 +60,29 @@ You can add multiple components at once: buridan add button input select ``` -This command automatically resolves and adds any required utilities, icons, or base components while maintaining the correct directory structure. +Blocks (charts, dashboards, etc.) can be added the same way: + +```bash +buridan add line_chart_01 +``` + +Components are placed in `components/`, blocks in `blocks/`. Dependencies are resolved and added automatically. + +> **Note:** Components require a theme to render correctly. Run `buridan apply` before using components. ## list -Display all available components that can be added to your project. +Display all available components and blocks. ```bash buridan list ``` + +## Recommended workflow + +```bash +buridan create # build your theme, copy the preset ID +buridan init # set up utilities and Tailwind config +buridan apply --preset # apply your theme +buridan add button input select # add the components you need +```