Initial public push: docs cosmos v4 + AI module + framework groundwork
This is the snapshot the production landing site (nibiru-framework.com) is deployed from. Brings together the recent splash + docs migration to the v4 "Cosmos" design system, the new in-framework AI module, and the framework groundwork that backs the framework-reference extraction. What lands: - docs/: Astro + Starlight site with the v4 dark cosmic palette, GalaxyHero canvas constellation, Mission Control chat (wired to /api/oracle → api.neuronetz.ai via providers.mjs Ollama), 5-panel MMVC stage (Model · AI · Module · Controller · View), translated EN/DE/JA/ES/FR content, PWA + sitemap + llms.txt + Umami analytics. - docs/design-system/: canonical mockup bundle (source/index-v2.html for splash, source/docs-system.html + preview/ for docs, SPEC.md, tokens). - docs/scripts/extraction/framework-reference-v2.md: deep framework reference (~1.6k lines, file:line citations, every public factory and idiom — basis for the LoRA training corpus. - application/module/ai/: AI module with chat / embed / RAG / agent plugins, plus pdoQuery / httpGet / fileRead tools and Modelfile + smoke-test in training/. - application/module/users/: user / ACL / form-factory traits used as the reference plugin pattern for the framework docs. - application/settings/config/database/: schema + seed migrations including the AI module tables (200–203). - Form factory + autogenerator changes the framework-reference-v2 covers. Production secrets stay out: docs/.env, settings.production.ini and ai.production.ini are all gitignored (.example files are in tree). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
docs/design-system/README.md
Normal file
61
docs/design-system/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Nibiru Atelier — Design System
|
||||
|
||||
The visual language of the Nibiru framework. A lotus on cream paper, lit by morning light.
|
||||
|
||||
## What's here
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `tokens.json` | Source of truth, [W3C Design Tokens Community Group](https://design-tokens.github.io/community-group/format/) format. |
|
||||
| `tokens.css` | CSS custom properties, namespaced `--nibiru-*`. |
|
||||
| `tokens.scss` | SCSS variables and maps. |
|
||||
| `tailwind.preset.js` | Tailwind preset extending colours, fonts, shadows. |
|
||||
|
||||
## Use
|
||||
|
||||
### CSS
|
||||
|
||||
```html
|
||||
<link rel="stylesheet"
|
||||
href="https://nibiru-framework.com/design-system/tokens.css">
|
||||
<style>
|
||||
body { background: var(--nibiru-paper); color: var(--nibiru-ink);
|
||||
font-family: var(--nibiru-font-text); }
|
||||
h1 { font-variation-settings: var(--nibiru-fv-display-hero);
|
||||
letter-spacing: var(--nibiru-tracking-display); }
|
||||
.cta { background: var(--nibiru-ink); color: var(--nibiru-paper);
|
||||
padding: 0.7rem 1.2rem;
|
||||
border-radius: var(--nibiru-radius-md); }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Tailwind
|
||||
|
||||
```jsx
|
||||
<button className="bg-nibiru-ink text-nibiru-paper px-5 py-2 rounded-md font-display tracking-body">
|
||||
Read the docs
|
||||
</button>
|
||||
|
||||
<h1 className="text-hero text-nibiru-ink tracking-display">
|
||||
Create. Invent. <em className="text-nibiru-iris-deep">Impress.</em>
|
||||
</h1>
|
||||
```
|
||||
|
||||
## Full guide
|
||||
|
||||
→ <https://nibiru-framework.com/en/design/overview/>
|
||||
|
||||
## Versioning
|
||||
|
||||
`4.0.0` — **Cosmos**. Dark-first AI-framework brand. Magenta-amber-blue nebula
|
||||
palette on a deep-space body, Space Grotesk display + Inter Tight body +
|
||||
JetBrains Mono. Light "paper" surfaces still available via `.alt` sections for
|
||||
content-heavy reading.
|
||||
|
||||
`3.0.0` — **Atelier × Cosmos**. Reconciles the editorial atelier (paper-and-ink)
|
||||
with a modern AI-tool dialect: warmer butter page (`#fdf6df`), Geist type,
|
||||
rounder surfaces (8–16 px), plus a dark cosmic sub-palette and a code surface
|
||||
stack for hero / chat / code-card components.
|
||||
|
||||
`2.0.0` — Atelier release. Lotus-violet, sky-blue and warm-cream system drawn
|
||||
from the actual brand mark. Bricolage Grotesque, editorial radii.
|
||||
406
docs/design-system/SPEC.md
Normal file
406
docs/design-system/SPEC.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# Handoff: Nibiru Docs Design System
|
||||
|
||||
## Overview
|
||||
|
||||
This handoff covers the **Nibiru documentation site** redesign — a cosmic-themed, dark-primary design system for the framework's docs portal. It includes a complete component library (navigation, code blocks, callouts, tables, API references, search modal, 404, etc.) plus a working full-page preview of the "What is Nibiru?" article styled with the new system.
|
||||
|
||||
The metaphor carried throughout: **Nibiru is a star map**. The framework's MMVC architecture (Model · Module · View · Controller) is a constellation, and the docs adopt the same atmospheric vocabulary — deep cosmic plum backgrounds, magenta/cyan/amber accents echoing nebula colors, terminal-aesthetic code blocks, and subtle galaxy textures behind content.
|
||||
|
||||
A companion start page (`index-v2.html`) is included for context — it's the marketing site this docs design extends.
|
||||
|
||||
## About the Design Files
|
||||
|
||||
The files in this bundle are **design references created in HTML** — interactive prototypes showing intended look, behavior, and layout. They are **not production code to copy directly**.
|
||||
|
||||
Your task is to **recreate these designs in the Nibiru docs site's actual environment** (likely a static-site generator like Nextra, Mintlify, Docusaurus, or VitePress, given the original screenshot's nav structure), using whatever templating, theming, and component patterns that environment provides. The HTML/JSX in this bundle uses React + Babel inline as a fast prototyping medium — your real implementation should use your stack's idioms.
|
||||
|
||||
If no docs framework has been chosen yet, **Nextra (with the docs theme) or Mintlify** are good fits — both support exactly this kind of three-column layout (sidebar + content + on-this-page TOC) with custom theming hooks.
|
||||
|
||||
## Fidelity
|
||||
|
||||
**High-fidelity.** All colors, typography, spacing, border radii, shadows, and component states are final. Treat the hex values, font sizes, and CSS tokens in `tokens.css` as the source of truth.
|
||||
|
||||
The single open question is how dark/light theme switching is wired — the prototype uses a `.theme-light` class on a wrapper element, but your stack may prefer `data-theme` attributes or CSS `color-scheme`.
|
||||
|
||||
## Screens / Views
|
||||
|
||||
The design canvas in `source/docs-system.html` shows every component grouped into reviewable sections. The full-page preview is the most important reference — it shows how everything composes.
|
||||
|
||||
### 1. Full Page Preview ("What is Nibiru?")
|
||||
|
||||
**Purpose**: Recreate the documentation article from the original screenshot in the new design system.
|
||||
|
||||
**Layout**: Three-column grid, full viewport height.
|
||||
- **Top nav**: 60px tall, sticky, `backdrop-filter: blur(16px)`, semi-transparent background.
|
||||
- **Main grid**: `grid-template-columns: 280px 1fr 240px` (sidebar / content / TOC).
|
||||
- **Content column**: `max-width: 760px`, centered within its track, padding `32px 48px`.
|
||||
|
||||
**Components used**: TopNav · Sidebar · PageHeader · Prose body · DocTable · Callout · Pagination · RightTOC
|
||||
|
||||
### 2. Top Nav
|
||||
|
||||
**Purpose**: Brand mark, search, GitHub link, theme toggle, locale picker.
|
||||
|
||||
**Layout**: `grid-template-columns: 240px 1fr auto`, gap 32px, padding `0 24px`, height 60px.
|
||||
|
||||
**Components**:
|
||||
- **Brand**: Lotus PNG (36px tall) + "Nibiru" (17px / 600) + "Create, Invent, Impress" tagline (10px muted, lowercase).
|
||||
- **Search button**: Pill-shaped, 420px max-width, centered. `background: var(--space-2)`, `border: 1px solid var(--line)`, contains search icon + "Search" placeholder + `⌘ K` kbd badge. Hover: brighten background.
|
||||
- **Right cluster**: GitHub icon (36×36), theme toggle (sun/moon), locale dropdown ("EN ▾"). All 36×36 with hover background `var(--space-2)`.
|
||||
|
||||
### 3. Sidebar
|
||||
|
||||
**Purpose**: Sectioned navigation with collapsible groups and active-page indication.
|
||||
|
||||
**Layout**: 280px wide, padding `24px 12px 24px 24px`, `border-right: 1px solid var(--line)`, faint background tint `rgba(11,4,16,0.4)`.
|
||||
|
||||
**Sections**: "Get Started" / "The Framework" / "CLI" / "Advanced" — each a collapsible group with a chevron that rotates 0deg/-90deg.
|
||||
|
||||
**Section heading**: 11px JetBrains Mono, 0.12em letter-spacing, uppercase, muted color.
|
||||
|
||||
**Items**: 13.5px, padding `6px 10px`, border-radius 8px. Hover: background `var(--space-2)`. Active state has a magenta gradient background (`linear-gradient(90deg, rgba(184,107,255,0.18), rgba(184,107,255,0.04))`) and a 3px magenta indicator bar with a glow shadow on the left edge.
|
||||
|
||||
**Badges**: `core` (magenta tint) and `new` (green tint), 9px JetBrains Mono uppercase.
|
||||
|
||||
### 4. Right TOC ("On this page")
|
||||
|
||||
**Layout**: 240px wide, padding `24px 16px 24px 24px`.
|
||||
|
||||
**Heading**: 11px JetBrains Mono uppercase muted.
|
||||
|
||||
**List**: `border-left: 1px solid var(--line)`, items 13px with 12px padding-left. Active item: magenta text + magenta 2px left border (replacing the 1px line border at that position).
|
||||
|
||||
**Sub-items (level 2)**: Indented 24px, 12.5px, more muted.
|
||||
|
||||
**Footer**: "Edit this page" link with pencil icon.
|
||||
|
||||
### 5. Page Header
|
||||
|
||||
**Layout**: `margin-bottom: 32px`, `padding-bottom: 24px`, `border-bottom: 1px solid var(--line)`.
|
||||
|
||||
**Breadcrumbs**: 11px JetBrains Mono uppercase, separated by `/`. Last crumb is brighter.
|
||||
|
||||
**Title**: 36px / 600 / -0.025em letter-spacing.
|
||||
|
||||
**Summary**: 18px, color `var(--star-soft)`, max-width 60ch.
|
||||
|
||||
**Last-updated meta**: 11px JetBrains Mono with a 6px green dot (with green glow shadow).
|
||||
|
||||
### 6. Body Prose
|
||||
|
||||
**Base**: 16px / 1.7 line-height, color `var(--star-soft)`.
|
||||
|
||||
**h2**: 28px / 600, with a 24px magenta gradient line above it (`width: 24px; height: 1px; background: linear-gradient(90deg, var(--nebula-mag), transparent)`). 40px top margin, 16px bottom margin.
|
||||
|
||||
**h3**: 22px / 600. 32px top, 12px bottom.
|
||||
|
||||
**em**: Restyled — color `var(--nebula-mag-2)`, NOT italic.
|
||||
|
||||
**Inline code**: Monospace 0.86em, magenta-2 text on `var(--space-2)` with 1px line border, 4px radius, 1px 6px padding.
|
||||
|
||||
**Blockquote**: 2px magenta left border, 16px left padding, magenta gradient fade background.
|
||||
|
||||
**ul markers**: Magenta accent.
|
||||
|
||||
### 7. Code Block
|
||||
|
||||
**Aesthetic**: Mission-Control terminal — `background: #0a0210`, 12px radius, magenta gradient overlay (4% opacity, 135deg from top-left).
|
||||
|
||||
**Header**: `padding: 8px 14px`, `border-bottom: 1px solid rgba(244,238,219,0.06)`, `background: rgba(0,0,0,0.3)`.
|
||||
- Three colored dots (rose, amber, green at 50% alpha — like macOS traffic lights but recolored to nebula palette).
|
||||
- Filename in JetBrains Mono 11px.
|
||||
- Language badge (10px JetBrains Mono uppercase, magenta text on transparent background with magenta border).
|
||||
- Copy button on the right (changes to "Copied ✓" for 1.4s on click).
|
||||
|
||||
**Body**: 13px JetBrains Mono, 1.7 line-height, padding `16px 20px`.
|
||||
|
||||
**Syntax tokens** (CSS classes already defined):
|
||||
| Token | Class | Color |
|
||||
|---|---|---|
|
||||
| Keyword | `.tk-kw` | `#ff7ab8` rose |
|
||||
| Class/type | `.tk-cn` | `#ffb574` amber |
|
||||
| Function | `.tk-fn` | `#6ad9ff` cyan |
|
||||
| Variable | `.tk-var` | `#d8a8ff` magenta |
|
||||
| String | `.tk-str` | `#6ee7b0` green |
|
||||
| Number | `.tk-num` | `#ffb574` amber |
|
||||
| Namespace | `.tk-ns` | muted cream |
|
||||
| Comment | `.tk-cm` | very muted, italic |
|
||||
|
||||
In production, hook these classes up to your syntax highlighter (Shiki, Prism, etc.) via a custom token theme.
|
||||
|
||||
### 8. Callouts (Note / Tip / Warning / Danger)
|
||||
|
||||
**Layout**: Flex row with 14px gap, 14px×18px padding, 8px radius, 1px colored border, tinted background (~6% alpha of the accent), radial gradient overlay at 5% opacity from top-left.
|
||||
|
||||
**Glyph**: 24×24 rounded with `filter: drop-shadow(0 0 6px currentColor)` for the cosmic glow.
|
||||
|
||||
**Color mapping**:
|
||||
| Kind | Color | Hex |
|
||||
|---|---|---|
|
||||
| Note | Cyan | `#6ad9ff` |
|
||||
| Tip | Green | `#6ee7b0` |
|
||||
| Warning | Amber | `#ffb574` |
|
||||
| Danger | Rose | `#ff7ab8` |
|
||||
|
||||
**Title**: 13px / 600 in the accent color.
|
||||
|
||||
**Body**: 15px in `var(--star-soft)`.
|
||||
|
||||
### 9. Tables
|
||||
|
||||
**Layout**: `border: 1px solid var(--line)`, 8px radius, `overflow: hidden`.
|
||||
|
||||
**Header**: `background: var(--space-2)`, padding `10px 16px`, 11px JetBrains Mono uppercase muted.
|
||||
|
||||
**Rows**: `padding: 12px 16px`, 13.5px, dashed bottom border. Hover: `background: rgba(184,107,255,0.04)`.
|
||||
|
||||
**First column**: Brighter (`var(--star)`), 500 weight, `white-space: nowrap`.
|
||||
|
||||
**Inline `<code>`**: Magenta-2 on `var(--space-2)`.
|
||||
|
||||
### 10. Tabs
|
||||
|
||||
**List**: `background: var(--space-2)`, padding 4px, 4px gap.
|
||||
|
||||
**Trigger**: 6px×14px padding, 13px / 500. Inactive: muted text. Active: bright text on `var(--space-3)`, 1px magenta inset shadow at the top, sm shadow.
|
||||
|
||||
**Use case**: "PHP / SQL / YAML" code variants, "macOS / Linux / Windows" install instructions.
|
||||
|
||||
### 11. API Block
|
||||
|
||||
**Container**: `padding: 18px 20px`, 12px radius, gradient background (`linear-gradient(180deg, var(--space-2), transparent)`).
|
||||
|
||||
**Signature**: 14px JetBrains Mono on `rgba(0,0,0,0.3)` with line border, 8px radius. Highlights function name (cyan), arg names (magenta-2), types (amber). Punctuation muted.
|
||||
|
||||
**Section labels**: "Parameters" / "Returns" — 10px JetBrains Mono uppercase muted.
|
||||
|
||||
**Param row**:
|
||||
- Name (13px JetBrains Mono magenta-2 / 500)
|
||||
- Type pill (11px JetBrains Mono amber on amber-tinted bg)
|
||||
- Optional `required` pill (rose tint)
|
||||
- Optional `default: <code>...</code>` (muted, with brighter code value)
|
||||
- Description (13.5px star-soft)
|
||||
- Dashed bottom border between params
|
||||
|
||||
### 12. Cards / Feature Grid
|
||||
|
||||
**Layout**: `grid-template-columns: repeat(2, 1fr)`, 14px gap.
|
||||
|
||||
**Card**: `background: var(--space-2)`, 1px line border, 12px radius, 18px padding. Hover: border becomes magenta-tinted, lifts 2px, background brightens, top-right arrow icon fades in & slides into place.
|
||||
|
||||
**Icon block**: 36×36 rounded square with a custom gradient background per card (mag→rose, cyan→mag, amber→rose, green→cyan).
|
||||
|
||||
**Title**: 15px / 600. **Description**: 13px star-soft.
|
||||
|
||||
### 13. Search Modal (⌘K)
|
||||
|
||||
**Container**: 580px wide, 16px radius, `background: var(--space-2)`, glow shadow (`0 0 60px rgba(184,107,255,0.1)`).
|
||||
|
||||
**Sections**:
|
||||
- Input row (16px placeholder text, search icon, `esc` kbd badge).
|
||||
- "Recent" — pills (rounded, `var(--space-3)` background).
|
||||
- "Results" — list with kind badges (page=magenta, section=cyan, api=amber), title with breadcrumb prefix, excerpt, `→` indicator. Active result has a magenta left border and tinted bg.
|
||||
- Footer — kbd hints (`↑↓` navigate · `↵` open · `⌘K` close) + "Powered by Nibiru Search".
|
||||
|
||||
**Highlight matches**: `<mark>` element gets `background: rgba(184,107,255,0.3)`, no padding.
|
||||
|
||||
### 14. Pagination
|
||||
|
||||
**Layout**: Two-column grid, top border, 48px top margin.
|
||||
|
||||
**Link card**: `background: var(--space-2)`, 1px border, 12px radius, padding `14px 18px`. Two-line content: small "Previous"/"Next" label (10px JetBrains Mono uppercase muted) + 14px / 500 page title.
|
||||
|
||||
**Hover**: Border picks up magenta glow, bg brightens, chevron arrow translates ±3px.
|
||||
|
||||
### 15. Floating Action Button (Help)
|
||||
|
||||
**Button**: 52px circle, gradient `linear-gradient(135deg, magenta, rose)`, 12px×32px shadow + inner white highlight.
|
||||
|
||||
**Pulse ring**: `::after` pseudo-element animating from scale(1) → scale(1.4), opacity 0.7 → 0, infinite 2.4s.
|
||||
|
||||
**Tooltip**: Pops out to the left on hover (kept in DOM for review).
|
||||
|
||||
### 16. 404 / "Off-orbit"
|
||||
|
||||
**Layout**: Centered, 520px max-width.
|
||||
|
||||
**Hero illustration**: SVG with two dashed orbital ellipses around a bright central star (radial gradient white→amber→magenta-fade), and a cyan probe drifting off-orbit at the top-right with a "PROBE-404" callout label and a dashed line connecting it back to the star.
|
||||
|
||||
**Code label**: "404 / off-orbit" in 11px JetBrains Mono cyan.
|
||||
|
||||
**Title**: "This page drifted away from Nibiru." 28px / 600.
|
||||
|
||||
**Actions**: Primary button (magenta solid) + ghost button (line border).
|
||||
|
||||
### 17. Mobile Drawer
|
||||
|
||||
**Container**: 320px wide, 640px tall in the prototype (in real life: full viewport with slide-in animation).
|
||||
|
||||
**Structure**: Header (brand + close X) → search button → sidebar (full nav) → footer (Theme / EN / GitHub pills).
|
||||
|
||||
## Interactions & Behavior
|
||||
|
||||
| Interaction | Trigger | Behavior |
|
||||
|---|---|---|
|
||||
| Theme toggle | Click sun/moon icon in nav | Add/remove `.theme-light` on root. Persist to `localStorage`. |
|
||||
| Sidebar collapse | Click section heading | Toggle children visibility, rotate chevron 90°. Animate `transform: rotate()` 200ms ease. |
|
||||
| Sidebar active | Current page | Apply `.active` class — magenta gradient bg + 3px magenta glowing left bar. |
|
||||
| TOC active | Scroll position | Use `IntersectionObserver` on `.prose h2[id]` elements. Highlight matching TOC item. |
|
||||
| Search open | `⌘K` / `Ctrl+K` / click search button | Open modal, focus input. `Esc` closes. ↑↓ navigates results, ↵ opens. |
|
||||
| Code copy | Click "Copy" button | Copy to clipboard, swap to "Copied ✓" for 1400ms, then revert. |
|
||||
| Tab switch | Click trigger | Update `aria-selected`, swap panel. No transition — instant. |
|
||||
| Card hover | Pointer enter | `transform: translateY(-2px)`, border magenta-tint, arrow fades in & translates. 200ms transitions. |
|
||||
| Pagination hover | Pointer enter | Chevron translates ±3px in the direction of travel, border magenta. |
|
||||
| FAB pulse | Always | 2.4s infinite ring animation. |
|
||||
|
||||
## State Management
|
||||
|
||||
This is a docs site — most state is URL-driven (current page, anchor, search query). The minimum client state needed:
|
||||
|
||||
- `theme: "dark" | "light"` (persisted to `localStorage`)
|
||||
- `sidebarOpen: boolean` (mobile drawer)
|
||||
- `searchOpen: boolean` (⌘K modal)
|
||||
- `expandedSections: Record<string, boolean>` (sidebar collapse — defaults to "current section open")
|
||||
- `activeTocId: string` (driven by IntersectionObserver, not user input)
|
||||
- `copyStateByBlock: Record<string, boolean>` (per-block "Copied" flash — local component state is fine)
|
||||
|
||||
## Design Tokens
|
||||
|
||||
All tokens live in **`source/docs-system/tokens.css`**. Copy this file into your codebase verbatim — it's the source of truth.
|
||||
|
||||
### Colors (dark theme)
|
||||
| Name | Hex | Use |
|
||||
|---|---|---|
|
||||
| `--space` | `#0b0410` | Page background |
|
||||
| `--space-2` | `#120822` | Panel surfaces |
|
||||
| `--space-3` | `#1a0c2e` | Raised surfaces |
|
||||
| `--plum` | `#2a1545` | Deep accents |
|
||||
| `--plum-2` | `#3a1d5e` | Hover plum |
|
||||
| `--star` | `#f4eedb` | Primary text |
|
||||
| `--star-soft` | `#d8d2c0` | Secondary text |
|
||||
| `--muted` | `#8b85a3` | Tertiary / labels |
|
||||
| `--muted-2` | `#5e5878` | Very muted |
|
||||
| `--nebula-mag` | `#b86bff` | Primary accent · links |
|
||||
| `--nebula-mag-2` | `#d8a8ff` | Hover / em |
|
||||
| `--nebula-cyan` | `#6ad9ff` | Note callout |
|
||||
| `--nebula-amber` | `#ffb574` | Warning callout / numbers |
|
||||
| `--nebula-rose` | `#ff7ab8` | Danger callout / keywords |
|
||||
| `--nebula-green` | `#6ee7b0` | Tip callout / strings |
|
||||
|
||||
### Light theme overrides
|
||||
Defined under `.theme-light` in `tokens.css` — accent colors shift to higher-contrast equivalents (purple replaces magenta, teal replaces cyan) against a parchment `#faf6ec` background.
|
||||
|
||||
### Type scale
|
||||
| Token | px | Use |
|
||||
|---|---|---|
|
||||
| `--fs-xs` | 12 | Tiny meta |
|
||||
| `--fs-sm` | 13 | UI |
|
||||
| `--fs-base` | 15 | Default |
|
||||
| `--fs-md` | 16 | Body prose |
|
||||
| `--fs-lg` | 18 | Page summary |
|
||||
| `--fs-xl` | 22 | h3 |
|
||||
| `--fs-2xl` | 28 | h2 |
|
||||
| `--fs-3xl` | 36 | Page title |
|
||||
| `--fs-4xl` | 48 | Page hero |
|
||||
|
||||
### Spacing
|
||||
4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 / 48 / 64 — exposed as `--space-1` through `--space-16` (note: var name pun with the cosmic palette `--space`; see comment in tokens.css).
|
||||
|
||||
### Radii
|
||||
4 / 8 / 12 / 16 / 20 / 999px (pill).
|
||||
|
||||
### Shadows
|
||||
- `--shadow-sm`: `0 1px 2px rgba(0,0,0,0.3)`
|
||||
- `--shadow-md`: `0 8px 24px -8px rgba(0,0,0,0.5)`
|
||||
- `--shadow-lg`: `0 24px 48px -16px rgba(0,0,0,0.6)`
|
||||
- `--shadow-glow`: `0 0 24px rgba(184,107,255,0.25)`
|
||||
|
||||
### Fonts
|
||||
- **Sans**: `Inter Tight` (400/500/600/700) — fall back to Inter, system sans
|
||||
- **Mono**: `JetBrains Mono` (400/500/600)
|
||||
- No display serif — explicitly all-sans for density and clarity.
|
||||
|
||||
Load via Google Fonts:
|
||||
```html
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
```
|
||||
|
||||
Or self-host for performance.
|
||||
|
||||
## Assets
|
||||
|
||||
- **`source/docs-system/assets/lotus.png`** — The Nibiru lotus mark, cropped from the supplied logo. 861×569. Use at 36px height in the nav (auto width). For a smaller mark icon variant, ask the brand owner for a square crop.
|
||||
- **`source/docs-system/assets/nibiru-full.png`** — The full logo with "Nibiru" wordmark and "Create, Invent, Impress" tagline. Use on a marketing splash or auth page; not the docs nav.
|
||||
|
||||
The docs nav uses **lotus + typeset wordmark separately** so we get crisper text than scaling a PNG, and so the tagline can use our `--muted` token color.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
source/
|
||||
├── docs-system.html # Main design canvas — every component side-by-side
|
||||
├── design-canvas.jsx # The canvas component itself (pan/zoom shell, you can ignore this)
|
||||
├── index-v2.html # Companion: the marketing start page (for brand context)
|
||||
└── docs-system/
|
||||
├── tokens.css # ★ DESIGN TOKENS — copy into your codebase
|
||||
├── components.css # ★ COMPONENT STYLES — copy or port to your styling system
|
||||
├── assets/
|
||||
│ ├── lotus.png # Brand mark
|
||||
│ └── nibiru-full.png # Full logo
|
||||
└── components/
|
||||
├── navigation.jsx # TopNav, Sidebar, RightTOC, LotusMark
|
||||
├── typography-and-code.jsx # PageHeader, Prose, CodeBlock, Callout, DocTable
|
||||
└── extras.jsx # Tabs, ApiBlock, CardGrid, SearchModal, FAB, Pagination,
|
||||
# MobileDrawer, NotFound
|
||||
|
||||
preview/
|
||||
└── nibiru-docs-system.html # Single-file bundled preview (all assets inlined,
|
||||
# works offline, just open in a browser)
|
||||
```
|
||||
|
||||
### Where to start
|
||||
|
||||
1. Open `preview/nibiru-docs-system.html` in a browser to see the full design canvas. **This file works offline** — everything is inlined, just double-click it.
|
||||
2. Read `source/docs-system/tokens.css` end-to-end — these tokens drive everything.
|
||||
3. Read `source/docs-system/components.css` — every visual decision is here, organized by component with `/* ====== COMPONENT ====== */` headers.
|
||||
4. Check the JSX components for structure (don't port React verbatim — just use them as a reference for DOM hierarchy and class names).
|
||||
5. Pick a target stack (Nextra / Mintlify / VitePress / your own React app) and start porting. The CSS will land cleanly because it's plain CSS variables + class selectors — no framework lock-in.
|
||||
|
||||
### ⚠️ Running the source HTML locally
|
||||
|
||||
The files in `source/` use external `<script src="...jsx">` imports. **They will NOT work if you double-click `docs-system.html` directly** — browsers block cross-origin requests on `file://` URLs, so the JSX modules fail to load and you'll see `ReferenceError: TopNav is not defined` in the console.
|
||||
|
||||
You need a local HTTP server. From the project root:
|
||||
|
||||
```bash
|
||||
# Python (pre-installed on macOS / Linux)
|
||||
cd design_handoff_nibiru_docs_system/source
|
||||
python3 -m http.server 8000
|
||||
# then open http://localhost:8000/docs-system.html
|
||||
|
||||
# OR: Node
|
||||
npx serve design_handoff_nibiru_docs_system/source
|
||||
# follow the printed URL
|
||||
|
||||
# OR: PHP (since this is a PHP project)
|
||||
cd design_handoff_nibiru_docs_system/source
|
||||
php -S localhost:8000
|
||||
# then open http://localhost:8000/docs-system.html
|
||||
```
|
||||
|
||||
If you just want to **look** at the design without a server, use `preview/nibiru-docs-system.html` — it's a single self-contained file with everything bundled in.
|
||||
|
||||
### Migration tips
|
||||
|
||||
- **If using Nextra**: Override the theme via `theme.config.tsx` and inject `tokens.css` + `components.css` as global styles. Use Nextra's MDX components (`<Callout>`, `<Tabs>`, etc.) and re-skin them with our class names.
|
||||
- **If using Mintlify**: Customize via `mint.json` `colors` section + a custom CSS file. Most components in this design map 1:1 to Mintlify primitives.
|
||||
- **If using VitePress**: Override default theme components in `.vitepress/theme/`. The sidebar/TOC layout maps directly.
|
||||
- **If rolling your own**: Copy both CSS files into `app/globals.css` (or equivalent), build React/Vue/Svelte components matching the structure in the JSX files, render MDX with a custom component map.
|
||||
|
||||
## Open Questions for the Developer
|
||||
|
||||
1. **Theme persistence**: How should we remember the user's theme choice? `localStorage` is the prototype's assumption.
|
||||
2. **Search backend**: The modal is a UI shell — needs a search index. Algolia DocSearch / Mintlify Search / a local index (FlexSearch, Pagefind) all work.
|
||||
3. **Syntax highlighting**: We define the token classes; you wire them up. Shiki with a custom theme matching our `.tk-*` classes is recommended.
|
||||
4. **i18n**: The locale dropdown is decorative in the prototype. Wire it up if/when translations exist.
|
||||
5. **Mobile breakpoints**: Not heavily specified. Suggested: hide right TOC below 1100px, swap left sidebar for the mobile drawer below 768px.
|
||||
758
docs/design-system/docs-page-mockup.html
Normal file
758
docs/design-system/docs-page-mockup.html
Normal file
@@ -0,0 +1,758 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Documentation page · Nibiru — design mockup</title>
|
||||
<meta name="description" content="Design mockup for a typical Nibiru documentation page. Layout, type, components and surfaces — built on the v4 Cosmos design system.">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#0a0414">
|
||||
|
||||
<!-- Type: Inter Tight (body) + Space Grotesk (display) + JetBrains Mono -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@200..800&family=Space+Grotesk:wght@300..700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Canonical Nibiru tokens (also embedded inline below for portability) -->
|
||||
<link rel="stylesheet" href="./tokens.css">
|
||||
|
||||
<style>
|
||||
/* ============================================================================
|
||||
* Documentation-page mockup — Nibiru v4 Cosmos
|
||||
*
|
||||
* Standalone HTML the designer can open in a browser and riff on. Mirrors
|
||||
* the production stack (Astro + Starlight) at the visual layer: header on
|
||||
* top, sidebar nav on the left, article in the middle, on-page TOC on
|
||||
* the right. Pure CSS, no JS — open it with file:// and it just works.
|
||||
*
|
||||
* Hand off: the designer should treat this as the "before" baseline for
|
||||
* doc pages. The startpage (separate file) sets the brand tone; this
|
||||
* shows how that tone translates to a content-heavy reading layout.
|
||||
* ========================================================================= */
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
|
||||
background: var(--nibiru-space, #0a0414);
|
||||
background-image:
|
||||
radial-gradient(ellipse 70% 50% at 0% 0%, rgba(184, 107, 255, 0.10), transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 100% 0%, rgba(91, 141, 255, 0.07), transparent 60%);
|
||||
background-attachment: fixed;
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
font-size: 16px;
|
||||
line-height: 1.55;
|
||||
font-feature-settings: "ss01", "cv11";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
::selection { background: rgba(184, 107, 255, 0.4); color: var(--nibiru-star, #f4eedb); }
|
||||
|
||||
.mono, code, kbd, pre {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
}
|
||||
.display { font-family: 'Space Grotesk', 'Inter Tight', ui-sans-serif, sans-serif; }
|
||||
|
||||
/* ============== HEADER ============== */
|
||||
.doc-header {
|
||||
position: sticky; top: 0; z-index: 60;
|
||||
height: 64px;
|
||||
display: flex; align-items: center;
|
||||
padding: 0 24px;
|
||||
background: rgba(6, 3, 15, 0.72);
|
||||
border-bottom: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
backdrop-filter: saturate(150%) blur(14px);
|
||||
-webkit-backdrop-filter: saturate(150%) blur(14px);
|
||||
}
|
||||
.doc-header-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 24px;
|
||||
}
|
||||
.brand {
|
||||
display: flex; align-items: center; gap: 10px; flex: none;
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
}
|
||||
.brand-mark {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle at 35% 35%, #f4eedb 0%, #b86bff 40%, #5b8dff 80%, #1c0f3a 100%);
|
||||
box-shadow: 0 0 12px rgba(184, 107, 255, 0.5);
|
||||
}
|
||||
.brand-name {
|
||||
font-size: 18px; font-weight: 500; letter-spacing: -0.02em;
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
}
|
||||
.brand-name em { font-style: normal; font-weight: 300; }
|
||||
.nav-version {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.04em;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
padding: 3px 7px;
|
||||
border: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
border-radius: 999px;
|
||||
margin-left: 6px;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.nav-version .dot {
|
||||
width: 5px; height: 5px; border-radius: 50%;
|
||||
background: #7ad6a3; box-shadow: 0 0 8px rgba(122, 214, 163, 0.7);
|
||||
}
|
||||
.doc-nav {
|
||||
display: flex; gap: 24px; align-items: center;
|
||||
flex: 1; justify-content: center;
|
||||
}
|
||||
.doc-nav a {
|
||||
font-size: 14px;
|
||||
color: rgba(244, 238, 219, 0.7);
|
||||
transition: color 160ms ease;
|
||||
}
|
||||
.doc-nav a:hover, .doc-nav a[aria-current="page"] {
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
}
|
||||
.search-inline {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
min-width: 220px;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
background: rgba(244, 238, 219, 0.04);
|
||||
border: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
border-radius: 999px;
|
||||
color: rgba(244, 238, 219, 0.55);
|
||||
font-size: 13px;
|
||||
transition: border-color 160ms ease, background-color 160ms ease;
|
||||
}
|
||||
.search-inline:hover { border-color: var(--nibiru-line-strong, rgba(244, 238, 219, 0.28)); }
|
||||
.search-inline svg { width: 14px; height: 14px; flex: none; }
|
||||
.search-inline kbd {
|
||||
margin-left: auto;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
border-radius: 4px;
|
||||
background: rgba(244, 238, 219, 0.03);
|
||||
}
|
||||
.lang-switcher {
|
||||
display: flex; gap: 4px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
}
|
||||
.lang-switcher a { padding: 4px 8px; border-radius: 6px; }
|
||||
.lang-switcher a[aria-current="true"] {
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
background: rgba(184, 107, 255, 0.12);
|
||||
}
|
||||
|
||||
/* ============== LAYOUT SHELL ============== */
|
||||
.doc-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr) 240px;
|
||||
gap: 0;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.doc-shell { grid-template-columns: 240px 1fr; }
|
||||
.doc-toc { display: none; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.doc-shell { grid-template-columns: 1fr; }
|
||||
.doc-sidebar { display: none; }
|
||||
}
|
||||
|
||||
/* ============== SIDEBAR ============== */
|
||||
.doc-sidebar {
|
||||
position: sticky; top: 64px;
|
||||
height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
padding: 28px 16px 28px 24px;
|
||||
border-right: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
background: rgba(10, 4, 20, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
font-size: 14px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(184, 107, 255, 0.3) transparent;
|
||||
}
|
||||
.doc-sidebar h3 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
margin: 22px 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.doc-sidebar h3:first-child { margin-top: 0; }
|
||||
.doc-sidebar ul {
|
||||
list-style: none; padding: 0; margin: 0;
|
||||
display: flex; flex-direction: column; gap: 1px;
|
||||
}
|
||||
.doc-sidebar a {
|
||||
display: block;
|
||||
padding: 7px 12px;
|
||||
color: rgba(244, 238, 219, 0.7);
|
||||
border-left: 1.5px solid transparent;
|
||||
border-radius: 0 8px 8px 0;
|
||||
transition: color 160ms ease, border-color 160ms ease, background-color 160ms ease;
|
||||
}
|
||||
.doc-sidebar a:hover {
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
border-left-color: var(--nibiru-iris-soft, #d4b4ff);
|
||||
background: rgba(184, 107, 255, 0.06);
|
||||
}
|
||||
.doc-sidebar a[aria-current="page"] {
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
font-weight: 500;
|
||||
border-left-color: var(--nibiru-nebula-mag, #b86bff);
|
||||
background: rgba(184, 107, 255, 0.10);
|
||||
}
|
||||
|
||||
/* ============== ARTICLE ============== */
|
||||
.doc-article {
|
||||
min-width: 0;
|
||||
padding: 56px 64px 120px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.doc-article { padding: 32px 24px 80px; }
|
||||
}
|
||||
.breadcrumbs {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
margin-bottom: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.breadcrumbs a:hover { color: var(--nibiru-star, #f4eedb); }
|
||||
.breadcrumbs .sep { color: var(--nibiru-line-strong, rgba(244, 238, 219, 0.28)); }
|
||||
|
||||
.doc-article h1 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: clamp(2rem, 1.5rem + 1.6vw, 2.75rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.04em;
|
||||
font-weight: 400;
|
||||
margin: 0 0 18px;
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
}
|
||||
.doc-article h1 em {
|
||||
font-style: normal; font-weight: 500;
|
||||
background: linear-gradient(110deg, #ffb574 0%, #b86bff 70%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
.doc-lede {
|
||||
font-size: 1.18rem;
|
||||
line-height: 1.55;
|
||||
color: rgba(244, 238, 219, 0.72);
|
||||
max-width: 56ch;
|
||||
margin: 0 0 36px;
|
||||
}
|
||||
|
||||
.doc-meta {
|
||||
display: flex; gap: 28px; align-items: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
padding-bottom: 28px;
|
||||
margin-bottom: 36px;
|
||||
border-bottom: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.doc-meta strong { color: rgba(244, 238, 219, 0.85); font-weight: 500; }
|
||||
.doc-meta .pulse {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: #7ad6a3;
|
||||
box-shadow: 0 0 8px rgba(122, 214, 163, 0.7);
|
||||
}
|
||||
|
||||
.doc-article h2 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.15;
|
||||
margin: 56px 0 16px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
}
|
||||
.doc-article h3 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 1.18rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 36px 0 12px;
|
||||
color: var(--nibiru-iris-soft, #d4b4ff);
|
||||
}
|
||||
.doc-article h4 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
margin: 28px 0 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.doc-article p {
|
||||
color: rgba(244, 238, 219, 0.85);
|
||||
margin: 0 0 18px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.doc-article strong { color: var(--nibiru-star, #f4eedb); font-weight: 600; }
|
||||
.doc-article a {
|
||||
color: var(--nibiru-iris-soft, #d4b4ff);
|
||||
border-bottom: 1px solid rgba(184, 107, 255, 0.4);
|
||||
transition: color 200ms ease, border-color 200ms ease;
|
||||
}
|
||||
.doc-article a:hover {
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
border-bottom-color: var(--nibiru-nebula-mag, #b86bff);
|
||||
}
|
||||
.doc-article ul, .doc-article ol {
|
||||
margin: 0 0 18px; padding-left: 24px;
|
||||
}
|
||||
.doc-article li { margin: 6px 0; color: rgba(244, 238, 219, 0.85); }
|
||||
|
||||
/* ============== INLINE CODE ============== */
|
||||
.doc-article :not(pre) > code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.86em;
|
||||
background: rgba(184, 107, 255, 0.14);
|
||||
color: var(--nibiru-iris-soft, #d4b4ff);
|
||||
padding: 0.05em 0.4em;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(184, 107, 255, 0.20);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ============== CODE BLOCK ============== */
|
||||
.code-block {
|
||||
background: var(--nibiru-code-bg, #050208);
|
||||
border: 1px solid var(--nibiru-line-strong, rgba(244, 238, 219, 0.28));
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
margin: 22px 0;
|
||||
box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
.code-block-head {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid rgba(244, 238, 219, 0.08);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.10em; text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
}
|
||||
.code-block-head .file { color: var(--nibiru-nebula-mag, #b86bff); }
|
||||
.code-block-head .copy {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(244, 238, 219, 0.12);
|
||||
color: rgba(244, 238, 219, 0.7);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.10em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.code-block-head .copy:hover { border-color: var(--nibiru-iris-soft, #d4b4ff); color: var(--nibiru-star, #f4eedb); }
|
||||
.code-block pre {
|
||||
margin: 0; padding: 18px 20px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px; line-height: 1.7;
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tk-c { color: #6e6680; font-style: italic; }
|
||||
.tk-k { color: #c8a8ff; }
|
||||
.tk-cls { color: #f0d4ff; }
|
||||
.tk-fn { color: #b8d4ff; }
|
||||
.tk-s { color: #ffd9a3; }
|
||||
.tk-v { color: #ffb4d8; }
|
||||
.tk-attr { color: #d4baff; }
|
||||
.tk-n { color: #b8e0c2; }
|
||||
|
||||
/* ============== CALLOUT / ASIDE ============== */
|
||||
.callout {
|
||||
background: rgba(184, 107, 255, 0.06);
|
||||
border: 1px solid rgba(244, 238, 219, 0.10);
|
||||
border-left: 3px solid var(--nibiru-nebula-mag, #b86bff);
|
||||
padding: 14px 16px 14px 18px;
|
||||
border-radius: 0 10px 10px 0;
|
||||
margin: 22px 0;
|
||||
}
|
||||
.callout--tip { border-left-color: var(--nibiru-nebula-amber, #ffb574); background: rgba(255, 181, 116, 0.06); }
|
||||
.callout--danger { border-left-color: #ff8a9d; background: rgba(255, 138, 157, 0.06); }
|
||||
.callout-title {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.10em; text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.callout p { margin: 0; }
|
||||
|
||||
/* ============== TABLE ============== */
|
||||
.doc-article table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 22px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.doc-article thead {
|
||||
border-bottom: 2px solid var(--nibiru-line-strong, rgba(244, 238, 219, 0.28));
|
||||
}
|
||||
.doc-article th {
|
||||
text-align: left;
|
||||
padding: 10px 12px 10px 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
}
|
||||
.doc-article td {
|
||||
padding: 10px 12px 10px 0;
|
||||
border-bottom: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
color: rgba(244, 238, 219, 0.85);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* ============== ON-PAGE TOC (right rail) ============== */
|
||||
.doc-toc {
|
||||
position: sticky; top: 64px;
|
||||
height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
padding: 56px 24px 56px 16px;
|
||||
border-left: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
font-size: 13px;
|
||||
}
|
||||
.doc-toc h4 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
margin: 0 0 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.doc-toc ul {
|
||||
list-style: none; padding: 0; margin: 0;
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
.doc-toc a {
|
||||
color: rgba(244, 238, 219, 0.6);
|
||||
display: block;
|
||||
padding: 4px 0;
|
||||
transition: color 160ms ease;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.doc-toc a:hover { color: var(--nibiru-star, #f4eedb); }
|
||||
.doc-toc a.active {
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
border-left: 2px solid var(--nibiru-nebula-mag, #b86bff);
|
||||
margin-left: -10px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.doc-toc .l2 { padding-left: 12px; font-size: 12px; }
|
||||
|
||||
/* ============== PAGE FOOTER (prev / next) ============== */
|
||||
.page-pagination {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 80px;
|
||||
padding-top: 36px;
|
||||
border-top: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.page-pagination { grid-template-columns: 1fr; }
|
||||
}
|
||||
.page-pagination a {
|
||||
background: var(--nibiru-night, #120825);
|
||||
border: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
border-radius: 14px;
|
||||
padding: 16px 18px;
|
||||
transition: border-color 200ms, transform 200ms;
|
||||
}
|
||||
.page-pagination a:hover {
|
||||
border-color: var(--nibiru-nebula-mag, #b86bff);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.page-pagination .lbl {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--nibiru-muted, #6e6680);
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.page-pagination .ttl {
|
||||
font-size: 15px; font-weight: 500;
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
}
|
||||
.page-pagination .next { text-align: right; }
|
||||
|
||||
/* ============== HELP STRIP ============== */
|
||||
.help-strip {
|
||||
margin-top: 48px;
|
||||
padding: 18px 22px;
|
||||
background: rgba(244, 238, 219, 0.03);
|
||||
border: 1px solid var(--nibiru-line, rgba(244, 238, 219, 0.12));
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
.help-strip-text { color: rgba(244, 238, 219, 0.75); }
|
||||
.help-strip-actions { display: flex; gap: 8px; }
|
||||
.help-strip-actions a {
|
||||
padding: 7px 14px;
|
||||
border: 1px solid var(--nibiru-line-strong, rgba(244, 238, 219, 0.28));
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
color: rgba(244, 238, 219, 0.85);
|
||||
transition: color 160ms, border-color 160ms;
|
||||
border-bottom: 1px solid var(--nibiru-line-strong, rgba(244, 238, 219, 0.28));
|
||||
}
|
||||
.help-strip-actions a:hover {
|
||||
color: var(--nibiru-star, #f4eedb);
|
||||
border-color: var(--nibiru-iris-soft, #d4b4ff);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ====================== HEADER ====================== -->
|
||||
<header class="doc-header">
|
||||
<div class="doc-header-row">
|
||||
<a class="brand" href="#">
|
||||
<span class="brand-mark"></span>
|
||||
<span class="brand-name">Nibiru<em> docs</em></span>
|
||||
<span class="nav-version"><span class="dot"></span>v0.9.2</span>
|
||||
</a>
|
||||
<nav class="doc-nav" aria-label="Primary">
|
||||
<a href="#" aria-current="page">Docs</a>
|
||||
<a href="#">MMVC</a>
|
||||
<a href="#">AI module</a>
|
||||
<a href="#">CLI</a>
|
||||
<a href="#">Showcase</a>
|
||||
</nav>
|
||||
<div class="doc-header-tools" style="display: flex; align-items: center; gap: 12px;">
|
||||
<div class="search-inline" role="search">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-3-3"/></svg>
|
||||
<span>Search the docs</span>
|
||||
<kbd>⌘ K</kbd>
|
||||
</div>
|
||||
<div class="lang-switcher" aria-label="Language">
|
||||
<a href="#" aria-current="true">EN</a>
|
||||
<a href="#">DE</a>
|
||||
<a href="#">JA</a>
|
||||
<a href="#">ES</a>
|
||||
<a href="#">FR</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ====================== SHELL ====================== -->
|
||||
<div class="doc-shell">
|
||||
|
||||
<!-- ====== SIDEBAR ====== -->
|
||||
<aside class="doc-sidebar" aria-label="Section navigation">
|
||||
<h3>Get started</h3>
|
||||
<ul>
|
||||
<li><a href="#">What is Nibiru?</a></li>
|
||||
<li><a href="#">Installation</a></li>
|
||||
<li><a href="#">Quick start</a></li>
|
||||
<li><a href="#" aria-current="page">Project structure</a></li>
|
||||
<li><a href="#">Run it locally</a></li>
|
||||
<li><a href="#">Deployment</a></li>
|
||||
</ul>
|
||||
|
||||
<h3>The framework</h3>
|
||||
<ul>
|
||||
<li><a href="#">Architecture (MMVC)</a></li>
|
||||
<li><a href="#">Bootstrap & Dispatcher</a></li>
|
||||
<li><a href="#">Routing</a></li>
|
||||
<li><a href="#">Controllers</a></li>
|
||||
<li><a href="#">Views & Smarty</a></li>
|
||||
<li><a href="#">Models</a></li>
|
||||
<li><a href="#">Modules</a></li>
|
||||
<li><a href="#">Forms</a></li>
|
||||
<li><a href="#">Database & Migrations</a></li>
|
||||
<li><a href="#">Auth</a></li>
|
||||
</ul>
|
||||
|
||||
<h3>AI in Nibiru</h3>
|
||||
<ul>
|
||||
<li><a href="#">The AI module</a></li>
|
||||
<li><a href="#">Chat plugin</a></li>
|
||||
<li><a href="#">Embed plugin</a></li>
|
||||
<li><a href="#">RAG plugin</a></li>
|
||||
<li><a href="#">Agent plugin</a></li>
|
||||
<li><a href="#">Ask the Oracle</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<!-- ====== ARTICLE ====== -->
|
||||
<main class="doc-article">
|
||||
|
||||
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||
<a href="#">Docs</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="#">Get started</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Project structure</span>
|
||||
</nav>
|
||||
|
||||
<h1>Project <em>structure</em></h1>
|
||||
<p class="doc-lede">Where Nibiru puts things, and why. The shape on disk maps to the four MMVC roles — read top-down and the framework will explain itself.</p>
|
||||
|
||||
<div class="doc-meta">
|
||||
<span><span class="pulse"></span> <strong>Stable</strong> · v0.9.2</span>
|
||||
<span>Updated <strong>2 days ago</strong></span>
|
||||
<span>Reading time <strong>~ 6 min</strong></span>
|
||||
<span>Edit on <strong>GitHub</strong></span>
|
||||
</div>
|
||||
|
||||
<p>A fresh Nibiru install is just a Composer-managed PHP project with a single CLI binary, an <code>application/</code> tree for your code, and a <code>core/</code> tree for the framework itself. Everything else — Smarty's compiled templates, the public docroot, the migrations history — falls out of those four conventions.</p>
|
||||
|
||||
<h2 id="overview">Overview</h2>
|
||||
<p>Open the repo and you'll see this:</p>
|
||||
|
||||
<div class="code-block">
|
||||
<div class="code-block-head">
|
||||
<span class="file">tree.txt</span>
|
||||
<button class="copy" type="button">Copy</button>
|
||||
</div>
|
||||
<pre><span class="tk-c"># A Nibiru project, top level</span>
|
||||
my-app/
|
||||
├── application/ <span class="tk-c">// your code lives here</span>
|
||||
│ ├── controller/
|
||||
│ ├── module/
|
||||
│ ├── view/
|
||||
│ └── settings/
|
||||
├── core/ <span class="tk-c">// the framework — don't edit</span>
|
||||
├── public/ <span class="tk-c">// docroot — point your vhost here</span>
|
||||
├── nibiru <span class="tk-c">// the CLI binary</span>
|
||||
├── composer.json
|
||||
└── index.php <span class="tk-c">// front controller</span></pre>
|
||||
</div>
|
||||
|
||||
<h3 id="application">application/</h3>
|
||||
<p>Your code. Each role in <strong>MMVC</strong> is its own subdirectory: <code>controller/</code>, <code>module/</code>, <code>view/</code>. The fourth — Models — live inside their own <code>module/<name>/models/</code> subtrees, because in MMVC every module owns the data shape it touches.</p>
|
||||
|
||||
<div class="callout callout--tip">
|
||||
<div class="callout-title">Tip</div>
|
||||
<p>Run <code>./nibiru -c products</code> and the CLI will scaffold the controller, the module, the navigation entry and a Smarty template into the right places automatically.</p>
|
||||
</div>
|
||||
|
||||
<h3 id="core">core/</h3>
|
||||
<p>The framework itself. <strong>Don't edit it.</strong> If you find yourself reaching in here, the answer is almost always to extend a class in <code>application/</code> instead. The single exception: <a href="#">contributing back</a>.</p>
|
||||
|
||||
<h3 id="public">public/</h3>
|
||||
<p>Your vhost docroot. Static assets are served directly from this directory; everything else falls through to the front controller via <code>index.php</code>. The <code>.htaccess</code> in the project root takes care of rewrites for Apache; the included <a href="#"><code>vhost.conf</code></a> shows the equivalent for nginx.</p>
|
||||
|
||||
<h2 id="naming">Naming conventions</h2>
|
||||
<p>Every controller is <code><name>Controller.php</code>, every module sits at <code>application/module/<name>/</code>, every Smarty template lives at <code>application/view/templates/<name>.tpl</code>. The CLI enforces this — you don't have to remember it.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Created by</th>
|
||||
<th>Owns</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>application/controller/</code></td>
|
||||
<td><code>./nibiru -c <name></code></td>
|
||||
<td>Routing, dispatch, lifecycle</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>application/module/</code></td>
|
||||
<td><code>./nibiru -m <name></code></td>
|
||||
<td>Capabilities, plugins, models</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>application/view/templates/</code></td>
|
||||
<td>scaffolded with controller</td>
|
||||
<td>Smarty 3 templates</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>application/settings/migrations/</code></td>
|
||||
<td><code>./nibiru -mi-add <n></code></td>
|
||||
<td>Timestamped SQL migrations</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="callout">
|
||||
<div class="callout-title">Note</div>
|
||||
<p>Smarty templates compile to <code>application/view/templates_c/</code> at first request. That directory is gitignored — clear it with <code>./nibiru -cache-clear</code> if a template ever sticks.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="next">Next</h2>
|
||||
<p>You know where things go. Now wire one up — the <a href="#">Quick start</a> takes you from an empty project to a running controller in five commands.</p>
|
||||
|
||||
<!-- prev / next -->
|
||||
<nav class="page-pagination" aria-label="Page navigation">
|
||||
<a class="prev" href="#">
|
||||
<span class="lbl">← Previous</span>
|
||||
<span class="ttl">Quick start</span>
|
||||
</a>
|
||||
<a class="next" href="#">
|
||||
<span class="lbl">Next →</span>
|
||||
<span class="ttl">Run it locally</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- help strip -->
|
||||
<aside class="help-strip">
|
||||
<span class="help-strip-text">Was this page helpful?</span>
|
||||
<div class="help-strip-actions">
|
||||
<a href="#">Yes</a>
|
||||
<a href="#">Suggest an edit</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ====== ON-PAGE TOC ====== -->
|
||||
<aside class="doc-toc" aria-label="On this page">
|
||||
<h4>On this page</h4>
|
||||
<ul>
|
||||
<li><a href="#overview" class="active">Overview</a></li>
|
||||
<li><a href="#application" class="l2">application/</a></li>
|
||||
<li><a href="#core" class="l2">core/</a></li>
|
||||
<li><a href="#public" class="l2">public/</a></li>
|
||||
<li><a href="#naming">Naming conventions</a></li>
|
||||
<li><a href="#next">Next</a></li>
|
||||
</ul>
|
||||
|
||||
<h4 style="margin-top: 36px;">Notes</h4>
|
||||
<p style="font-size: 12px; color: var(--nibiru-muted, #6e6680); line-height: 1.5;">
|
||||
This is a design mockup, not a real docs page. The TOC scroll-spy, search modal,
|
||||
and language-switcher are wired-up in the production Astro/Starlight build —
|
||||
treat the visual elements here as the brief.
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
docs/design-system/nibiru-framework.com.zip
Normal file
BIN
docs/design-system/nibiru-framework.com.zip
Normal file
Binary file not shown.
201
docs/design-system/preview/nibiru-docs-system.html
Normal file
201
docs/design-system/preview/nibiru-docs-system.html
Normal file
File diff suppressed because one or more lines are too long
936
docs/design-system/source/design-canvas.jsx
Normal file
936
docs/design-system/source/design-canvas.jsx
Normal file
@@ -0,0 +1,936 @@
|
||||
|
||||
// DesignCanvas.jsx — Figma-ish design canvas wrapper
|
||||
// Warm gray grid bg + Sections + Artboards + PostIt notes.
|
||||
// Artboards are reorderable (grip-drag), deletable, labels/titles are
|
||||
// inline-editable, and any artboard can be opened in a fullscreen focus
|
||||
// overlay (←/→/Esc). State persists to a .design-canvas.state.json sidecar
|
||||
// via the host bridge. No assets, no deps.
|
||||
//
|
||||
// Usage:
|
||||
// <DesignCanvas>
|
||||
// <DCSection id="onboarding" title="Onboarding" subtitle="First-run variants">
|
||||
// <DCArtboard id="a" label="A · Dusk" width={260} height={480}>…</DCArtboard>
|
||||
// <DCArtboard id="b" label="B · Minimal" width={260} height={480}>…</DCArtboard>
|
||||
// </DCSection>
|
||||
// </DesignCanvas>
|
||||
|
||||
const DC = {
|
||||
bg: '#f0eee9',
|
||||
grid: 'rgba(0,0,0,0.06)',
|
||||
label: 'rgba(60,50,40,0.7)',
|
||||
title: 'rgba(40,30,20,0.85)',
|
||||
subtitle: 'rgba(60,50,40,0.6)',
|
||||
postitBg: '#fef4a8',
|
||||
postitText: '#5a4a2a',
|
||||
font: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
|
||||
};
|
||||
|
||||
// One-time CSS injection (classes are dc-prefixed so they don't collide with
|
||||
// the hosted design's own styles).
|
||||
if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'dc-styles';
|
||||
s.textContent = [
|
||||
'.dc-editable{cursor:text;outline:none;white-space:nowrap;border-radius:3px;padding:0 2px;margin:0 -2px}',
|
||||
'.dc-editable:focus{background:#fff;box-shadow:0 0 0 1.5px #c96442}',
|
||||
'[data-dc-slot]{transition:transform .18s cubic-bezier(.2,.7,.3,1)}',
|
||||
'[data-dc-slot].dc-dragging{transition:none;z-index:10;pointer-events:none}',
|
||||
'[data-dc-slot].dc-dragging .dc-card{box-shadow:0 12px 40px rgba(0,0,0,.25),0 0 0 2px #c96442;transform:scale(1.02)}',
|
||||
// isolation:isolate contains artboard content's z-indexes so a
|
||||
// z-indexed child (sticky navbar etc.) can't paint over .dc-header or
|
||||
// the .dc-menu popover that drops into the top of the card.
|
||||
'.dc-card{isolation:isolate;transition:box-shadow .15s,transform .15s}',
|
||||
'.dc-card *{scrollbar-width:none}',
|
||||
'.dc-card *::-webkit-scrollbar{display:none}',
|
||||
// Per-artboard header: grip + label on the left, delete/expand on the
|
||||
// right. Single flex row; when the artboard's on-screen width is too
|
||||
// narrow for both the label yields (ellipsis, then hidden entirely below
|
||||
// ~4ch via the container query) and the buttons stay on the row.
|
||||
'.dc-header{position:absolute;bottom:100%;left:-4px;margin-bottom:calc(4px * var(--dc-inv-zoom,1));z-index:2;',
|
||||
' display:flex;align-items:center;container-type:inline-size}',
|
||||
'.dc-labelrow{display:flex;align-items:center;gap:4px;height:24px;flex:1 1 auto;min-width:0}',
|
||||
'.dc-grip{flex:0 0 auto;cursor:grab;display:flex;align-items:center;padding:5px 4px;border-radius:4px;transition:background .12s,opacity .12s}',
|
||||
'.dc-grip:hover{background:rgba(0,0,0,.08)}',
|
||||
'.dc-grip:active{cursor:grabbing}',
|
||||
'.dc-labeltext{flex:1 1 auto;min-width:0;cursor:pointer;border-radius:4px;padding:3px 6px;',
|
||||
' display:flex;align-items:center;transition:background .12s;overflow:hidden}',
|
||||
// Below ~4ch of label room: hide the label entirely, and drop the grip to
|
||||
// hover-only (same reveal rule as .dc-btns) so a narrow header is clean
|
||||
// until the card is moused.
|
||||
'@container (max-width: 110px){',
|
||||
' .dc-labeltext{display:none}',
|
||||
' .dc-grip{opacity:0}',
|
||||
' [data-dc-slot]:hover .dc-grip{opacity:1}',
|
||||
'}',
|
||||
'.dc-labeltext:hover{background:rgba(0,0,0,.05)}',
|
||||
'.dc-labeltext .dc-editable{overflow:hidden;text-overflow:ellipsis;max-width:100%}',
|
||||
'.dc-labeltext .dc-editable:focus{overflow:visible;text-overflow:clip}',
|
||||
'.dc-btns{flex:0 0 auto;margin-left:auto;display:flex;gap:2px;opacity:0;transition:opacity .12s}',
|
||||
'[data-dc-slot]:hover .dc-btns,.dc-btns:has(.dc-menu){opacity:1}',
|
||||
'.dc-expand,.dc-kebab{width:22px;height:22px;border-radius:5px;border:none;cursor:pointer;padding:0;',
|
||||
' background:transparent;color:rgba(60,50,40,.7);display:flex;align-items:center;justify-content:center;',
|
||||
' font:inherit;transition:background .12s,color .12s}',
|
||||
'.dc-expand:hover,.dc-kebab:hover{background:rgba(0,0,0,.06);color:#2a251f}',
|
||||
// Slot hosting an open menu floats above later siblings (which otherwise
|
||||
// paint on top — same z-index:auto, later DOM order) so the popup isn't
|
||||
// clipped by the next card.
|
||||
'[data-dc-slot]:has(.dc-menu){z-index:10}',
|
||||
'.dc-menu{position:absolute;top:100%;right:0;margin-top:4px;background:#fff;border-radius:8px;',
|
||||
' box-shadow:0 8px 28px rgba(0,0,0,.18),0 0 0 1px rgba(0,0,0,.05);padding:4px;min-width:160px;z-index:10}',
|
||||
'.dc-menu button{display:block;width:100%;padding:7px 10px;border:0;background:transparent;',
|
||||
' border-radius:5px;font-family:inherit;font-size:13px;font-weight:500;line-height:1.2;',
|
||||
' color:#29261b;cursor:pointer;text-align:left;transition:background .12s;white-space:nowrap}',
|
||||
'.dc-menu button:hover{background:rgba(0,0,0,.05)}',
|
||||
'.dc-menu hr{border:0;border-top:1px solid rgba(0,0,0,.08);margin:4px 2px}',
|
||||
'.dc-menu .dc-danger{color:#c96442}',
|
||||
'.dc-menu .dc-danger:hover{background:rgba(201,100,66,.1)}',
|
||||
// Chrome (titles / labels / buttons) counter-scales against the viewport
|
||||
// zoom so it stays a constant on-screen size. --dc-inv-zoom is set by
|
||||
// DCViewport on every transform update and inherits to all descendants —
|
||||
// any overlay inside the world (e.g. a TweaksPanel on an artboard) can use
|
||||
// it the same way.
|
||||
//
|
||||
// The header uses transform:scale (out-of-flow, so layout impact doesn't
|
||||
// matter) with its world-space width set to card-width / inv-zoom so that
|
||||
// after counter-scaling its on-screen width exactly matches the card's —
|
||||
// that's what lets the container query + text-overflow behave against the
|
||||
// card's visible edge at every zoom level.
|
||||
//
|
||||
// The section head uses CSS zoom instead of transform so its layout box
|
||||
// grows with the counter-scale, pushing the card row down — otherwise the
|
||||
// constant-screen-size title would overflow into the (shrinking) world-
|
||||
// space gap and overlap the artboard headers at low zoom.
|
||||
'.dc-header{width:calc((100% + 4px) / var(--dc-inv-zoom,1));',
|
||||
' transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom left}',
|
||||
'.dc-sectionhead{zoom:var(--dc-inv-zoom,1)}',
|
||||
].join('\n');
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
const DCCtx = React.createContext(null);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// DesignCanvas — stateful wrapper around the pan/zoom viewport.
|
||||
// Owns runtime state (per-section order, renamed titles/labels, hidden
|
||||
// artboards, focused artboard). Order/titles/labels/hidden persist to a
|
||||
// .design-canvas.state.json
|
||||
// sidecar next to the HTML. Reads go via plain fetch() so the saved
|
||||
// arrangement is visible anywhere the HTML + sidecar are served together
|
||||
// (omelette preview, direct link, downloaded zip). Writes go through the
|
||||
// host's window.omelette bridge — editing requires the omelette runtime.
|
||||
// Focus is ephemeral.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const DC_STATE_FILE = '.design-canvas.state.json';
|
||||
|
||||
function DesignCanvas({ children, minScale, maxScale, style }) {
|
||||
const [state, setState] = React.useState({ sections: {}, focus: null });
|
||||
// Hold rendering until the sidecar read settles so the saved order/titles
|
||||
// appear on first paint (no source-order flash). didRead gates writes until
|
||||
// the read settles so the empty initial state can't clobber a slow read;
|
||||
// skipNextWrite suppresses the one echo-write that would otherwise follow
|
||||
// hydration.
|
||||
const [ready, setReady] = React.useState(false);
|
||||
const didRead = React.useRef(false);
|
||||
const skipNextWrite = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
let off = false;
|
||||
fetch('./' + DC_STATE_FILE)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((saved) => {
|
||||
if (off || !saved || !saved.sections) return;
|
||||
skipNextWrite.current = true;
|
||||
setState((s) => ({ ...s, sections: saved.sections }));
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => { didRead.current = true; if (!off) setReady(true); });
|
||||
const t = setTimeout(() => { if (!off) setReady(true); }, 150);
|
||||
return () => { off = true; clearTimeout(t); };
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!didRead.current) return;
|
||||
if (skipNextWrite.current) { skipNextWrite.current = false; return; }
|
||||
const t = setTimeout(() => {
|
||||
window.omelette?.writeFile(DC_STATE_FILE, JSON.stringify({ sections: state.sections })).catch(() => {});
|
||||
}, 250);
|
||||
return () => clearTimeout(t);
|
||||
}, [state.sections]);
|
||||
|
||||
// Build registries synchronously from children so FocusOverlay can read
|
||||
// them in the same render. Only direct DCSection > DCArtboard children are
|
||||
// walked — wrapping them in other elements opts out of focus/reorder.
|
||||
const registry = {}; // slotId -> { sectionId, artboard }
|
||||
const sectionMeta = {}; // sectionId -> { title, subtitle, slotIds[] }
|
||||
const sectionOrder = [];
|
||||
React.Children.forEach(children, (sec) => {
|
||||
if (!sec || sec.type !== DCSection) return;
|
||||
const sid = sec.props.id ?? sec.props.title;
|
||||
if (!sid) return;
|
||||
sectionOrder.push(sid);
|
||||
const persisted = state.sections[sid] || {};
|
||||
const abs = [];
|
||||
React.Children.forEach(sec.props.children, (ab) => {
|
||||
if (!ab || ab.type !== DCArtboard) return;
|
||||
const aid = ab.props.id ?? ab.props.label;
|
||||
if (aid) abs.push([aid, ab]);
|
||||
});
|
||||
// hidden is scoped to one source revision — when the agent regenerates
|
||||
// (artboard-ID set changes), prior deletes don't apply to new content.
|
||||
const srcKey = abs.map(([k]) => k).join('\x1f');
|
||||
const hidden = persisted.srcKey === srcKey ? (persisted.hidden || []) : [];
|
||||
const srcIds = [];
|
||||
abs.forEach(([aid, ab]) => {
|
||||
if (hidden.includes(aid)) return;
|
||||
registry[`${sid}/${aid}`] = { sectionId: sid, artboard: ab };
|
||||
srcIds.push(aid);
|
||||
});
|
||||
const kept = (persisted.order || []).filter((k) => srcIds.includes(k));
|
||||
sectionMeta[sid] = {
|
||||
title: persisted.title ?? sec.props.title,
|
||||
subtitle: sec.props.subtitle,
|
||||
slotIds: [...kept, ...srcIds.filter((k) => !kept.includes(k))],
|
||||
};
|
||||
});
|
||||
|
||||
const api = React.useMemo(() => ({
|
||||
state,
|
||||
section: (id) => state.sections[id] || {},
|
||||
patchSection: (id, p) => setState((s) => ({
|
||||
...s,
|
||||
sections: { ...s.sections, [id]: { ...s.sections[id], ...(typeof p === 'function' ? p(s.sections[id] || {}) : p) } },
|
||||
})),
|
||||
setFocus: (slotId) => setState((s) => ({ ...s, focus: slotId })),
|
||||
}), [state]);
|
||||
|
||||
// Esc exits focus; any outside pointerdown commits an in-progress rename.
|
||||
React.useEffect(() => {
|
||||
const onKey = (e) => { if (e.key === 'Escape') api.setFocus(null); };
|
||||
const onPd = (e) => {
|
||||
const ae = document.activeElement;
|
||||
if (ae && ae.isContentEditable && !ae.contains(e.target)) ae.blur();
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
document.addEventListener('pointerdown', onPd, true);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKey);
|
||||
document.removeEventListener('pointerdown', onPd, true);
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
return (
|
||||
<DCCtx.Provider value={api}>
|
||||
<DCViewport minScale={minScale} maxScale={maxScale} style={style}>{ready && children}</DCViewport>
|
||||
{state.focus && registry[state.focus] && (
|
||||
<DCFocusOverlay entry={registry[state.focus]} sectionMeta={sectionMeta} sectionOrder={sectionOrder} />
|
||||
)}
|
||||
</DCCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// DCViewport — transform-based pan/zoom (internal)
|
||||
//
|
||||
// Input mapping (Figma-style):
|
||||
// • trackpad pinch → zoom (ctrlKey wheel; Safari gesture* events)
|
||||
// • trackpad scroll → pan (two-finger)
|
||||
// • mouse wheel → zoom (notched; distinguished from trackpad scroll)
|
||||
// • middle-drag / primary-drag-on-bg → pan
|
||||
//
|
||||
// Transform state lives in a ref and is written straight to the DOM
|
||||
// (translate3d + will-change) so wheel ticks don't go through React —
|
||||
// keeps pans at 60fps on dense canvases.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
||||
const vpRef = React.useRef(null);
|
||||
const worldRef = React.useRef(null);
|
||||
const tf = React.useRef({ x: 0, y: 0, scale: 1 });
|
||||
// Persist viewport across reloads so the user lands back where they were
|
||||
// after an agent edit or browser refresh. The sandbox origin is already
|
||||
// per-project; pathname keeps multiple canvas files in one project apart.
|
||||
const tfKey = 'dc-viewport:' + location.pathname;
|
||||
const saveT = React.useRef(0);
|
||||
|
||||
const lastPostedScale = React.useRef();
|
||||
const apply = React.useCallback(() => {
|
||||
const { x, y, scale } = tf.current;
|
||||
const el = worldRef.current;
|
||||
if (!el) return;
|
||||
el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
|
||||
// Exposed for zoom-invariant chrome (labels, buttons, TweaksPanel).
|
||||
el.style.setProperty('--dc-inv-zoom', String(1 / scale));
|
||||
// Keep the host toolbar's % readout in sync with the canvas scale. Pan
|
||||
// ticks leave scale unchanged — skip the cross-frame post for those.
|
||||
if (lastPostedScale.current !== scale) {
|
||||
lastPostedScale.current = scale;
|
||||
window.parent.postMessage({ type: '__dc_zoom', scale }, '*');
|
||||
}
|
||||
clearTimeout(saveT.current);
|
||||
saveT.current = setTimeout(() => {
|
||||
try { localStorage.setItem(tfKey, JSON.stringify(tf.current)); } catch {}
|
||||
}, 200);
|
||||
}, [tfKey]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const flush = () => {
|
||||
clearTimeout(saveT.current);
|
||||
try { localStorage.setItem(tfKey, JSON.stringify(tf.current)); } catch {}
|
||||
};
|
||||
try {
|
||||
const s = JSON.parse(localStorage.getItem(tfKey) || 'null');
|
||||
if (s && Number.isFinite(s.x) && Number.isFinite(s.y) && Number.isFinite(s.scale)) {
|
||||
tf.current = { x: s.x, y: s.y, scale: Math.min(maxScale, Math.max(minScale, s.scale)) };
|
||||
apply();
|
||||
}
|
||||
} catch {}
|
||||
// Flush on pagehide and unmount so a reload within the 200ms debounce
|
||||
// window doesn't drop the last pan/zoom.
|
||||
window.addEventListener('pagehide', flush);
|
||||
return () => { window.removeEventListener('pagehide', flush); flush(); };
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const vp = vpRef.current;
|
||||
if (!vp) return;
|
||||
|
||||
const zoomAt = (cx, cy, factor) => {
|
||||
const r = vp.getBoundingClientRect();
|
||||
const px = cx - r.left, py = cy - r.top;
|
||||
const t = tf.current;
|
||||
const next = Math.min(maxScale, Math.max(minScale, t.scale * factor));
|
||||
const k = next / t.scale;
|
||||
// keep the world point under the cursor fixed
|
||||
t.x = px - (px - t.x) * k;
|
||||
t.y = py - (py - t.y) * k;
|
||||
t.scale = next;
|
||||
apply();
|
||||
};
|
||||
|
||||
// Mouse-wheel vs trackpad-scroll heuristic. A physical wheel sends
|
||||
// line-mode deltas (Firefox) or large integer pixel deltas with no X
|
||||
// component (Chrome/Safari, typically multiples of 100/120). Trackpad
|
||||
// two-finger scroll sends small/fractional pixel deltas, often with
|
||||
// non-zero deltaX. ctrlKey is set by the browser for trackpad pinch.
|
||||
const isMouseWheel = (e) =>
|
||||
e.deltaMode !== 0 ||
|
||||
(e.deltaX === 0 && Number.isInteger(e.deltaY) && Math.abs(e.deltaY) >= 40);
|
||||
|
||||
const onWheel = (e) => {
|
||||
e.preventDefault();
|
||||
if (isGesturing) return; // Safari: gesture* owns the pinch — discard concurrent wheels
|
||||
if ((e.ctrlKey || e.metaKey) && !isMouseWheel(e)) {
|
||||
// trackpad pinch, or ctrl/cmd + smooth-scroll mouse. Notched
|
||||
// wheels fall through to the fixed-step branch below.
|
||||
zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.01));
|
||||
} else if (isMouseWheel(e)) {
|
||||
// notched mouse wheel — fixed-ratio step per click
|
||||
zoomAt(e.clientX, e.clientY, Math.exp(-Math.sign(e.deltaY) * 0.18));
|
||||
} else {
|
||||
// trackpad two-finger scroll — pan
|
||||
tf.current.x -= e.deltaX;
|
||||
tf.current.y -= e.deltaY;
|
||||
apply();
|
||||
}
|
||||
};
|
||||
|
||||
// Safari sends native gesture* events for trackpad pinch with a smooth
|
||||
// e.scale; preferring these over the ctrl+wheel fallback gives a much
|
||||
// better feel there. No-ops on other browsers. Safari also fires
|
||||
// ctrlKey wheel events during the same pinch — isGesturing makes
|
||||
// onWheel drop those entirely so they neither zoom nor pan.
|
||||
let gsBase = 1;
|
||||
let isGesturing = false;
|
||||
const onGestureStart = (e) => { e.preventDefault(); isGesturing = true; gsBase = tf.current.scale; };
|
||||
const onGestureChange = (e) => {
|
||||
e.preventDefault();
|
||||
zoomAt(e.clientX, e.clientY, (gsBase * e.scale) / tf.current.scale);
|
||||
};
|
||||
const onGestureEnd = (e) => { e.preventDefault(); isGesturing = false; };
|
||||
|
||||
// Drag-pan: middle button anywhere, or primary button on canvas
|
||||
// background (anything that isn't an artboard or an inline editor).
|
||||
let drag = null;
|
||||
const onPointerDown = (e) => {
|
||||
const onBg = !e.target.closest('[data-dc-slot], .dc-editable');
|
||||
if (!(e.button === 1 || (e.button === 0 && onBg))) return;
|
||||
e.preventDefault();
|
||||
vp.setPointerCapture(e.pointerId);
|
||||
drag = { id: e.pointerId, lx: e.clientX, ly: e.clientY };
|
||||
vp.style.cursor = 'grabbing';
|
||||
};
|
||||
const onPointerMove = (e) => {
|
||||
if (!drag || e.pointerId !== drag.id) return;
|
||||
tf.current.x += e.clientX - drag.lx;
|
||||
tf.current.y += e.clientY - drag.ly;
|
||||
drag.lx = e.clientX; drag.ly = e.clientY;
|
||||
apply();
|
||||
};
|
||||
const onPointerUp = (e) => {
|
||||
if (!drag || e.pointerId !== drag.id) return;
|
||||
vp.releasePointerCapture(e.pointerId);
|
||||
drag = null;
|
||||
vp.style.cursor = '';
|
||||
};
|
||||
|
||||
// Host-driven zoom (toolbar % menu). Zooms around viewport centre so the
|
||||
// visible midpoint stays fixed — matching the host's iframe-zoom feel.
|
||||
const onHostMsg = (e) => {
|
||||
const d = e.data;
|
||||
if (d && d.type === '__dc_set_zoom' && typeof d.scale === 'number') {
|
||||
const r = vp.getBoundingClientRect();
|
||||
zoomAt(r.left + r.width / 2, r.top + r.height / 2, d.scale / tf.current.scale);
|
||||
} else if (d && d.type === '__dc_probe') {
|
||||
// Host's [readyGen] reset asks whether a canvas is present; it
|
||||
// fires on the iframe's native 'load', which for canvases with
|
||||
// images/fonts is after our mount-time announce, so re-announce.
|
||||
// Clear the pan-tick guard so apply() re-posts the current scale
|
||||
// even if it's unchanged — the host just reset dcScale to 1.
|
||||
window.parent.postMessage({ type: '__dc_present' }, '*');
|
||||
lastPostedScale.current = undefined;
|
||||
apply();
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', onHostMsg);
|
||||
// Announce canvas mode so the host toolbar proxies its % control here
|
||||
// instead of scaling the iframe element (which would just shrink the
|
||||
// viewport window of an infinite canvas). The apply() that follows emits
|
||||
// the initial __dc_zoom so the toolbar % is correct before first pinch.
|
||||
// lastPostedScale reset mirrors the __dc_probe handler: the layout
|
||||
// effect's restore-path apply() may already have posted the restored
|
||||
// scale (before __dc_present), so clear the guard to re-post it in order.
|
||||
window.parent.postMessage({ type: '__dc_present' }, '*');
|
||||
lastPostedScale.current = undefined;
|
||||
apply();
|
||||
|
||||
vp.addEventListener('wheel', onWheel, { passive: false });
|
||||
vp.addEventListener('gesturestart', onGestureStart, { passive: false });
|
||||
vp.addEventListener('gesturechange', onGestureChange, { passive: false });
|
||||
vp.addEventListener('gestureend', onGestureEnd, { passive: false });
|
||||
vp.addEventListener('pointerdown', onPointerDown);
|
||||
vp.addEventListener('pointermove', onPointerMove);
|
||||
vp.addEventListener('pointerup', onPointerUp);
|
||||
vp.addEventListener('pointercancel', onPointerUp);
|
||||
return () => {
|
||||
window.removeEventListener('message', onHostMsg);
|
||||
vp.removeEventListener('wheel', onWheel);
|
||||
vp.removeEventListener('gesturestart', onGestureStart);
|
||||
vp.removeEventListener('gesturechange', onGestureChange);
|
||||
vp.removeEventListener('gestureend', onGestureEnd);
|
||||
vp.removeEventListener('pointerdown', onPointerDown);
|
||||
vp.removeEventListener('pointermove', onPointerMove);
|
||||
vp.removeEventListener('pointerup', onPointerUp);
|
||||
vp.removeEventListener('pointercancel', onPointerUp);
|
||||
};
|
||||
}, [apply, minScale, maxScale]);
|
||||
|
||||
const gridSvg = `url("data:image/svg+xml,%3Csvg width='120' height='120' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M120 0H0v120' fill='none' stroke='${encodeURIComponent(DC.grid)}' stroke-width='1'/%3E%3C/svg%3E")`;
|
||||
return (
|
||||
<div
|
||||
ref={vpRef}
|
||||
className="design-canvas"
|
||||
style={{
|
||||
height: '100vh', width: '100vw',
|
||||
background: DC.bg,
|
||||
overflow: 'hidden',
|
||||
overscrollBehavior: 'none',
|
||||
touchAction: 'none',
|
||||
position: 'relative',
|
||||
fontFamily: DC.font,
|
||||
boxSizing: 'border-box',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={worldRef}
|
||||
style={{
|
||||
position: 'absolute', top: 0, left: 0,
|
||||
transformOrigin: '0 0',
|
||||
willChange: 'transform',
|
||||
width: 'max-content', minWidth: '100%',
|
||||
minHeight: '100%',
|
||||
padding: '60px 0 80px',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'absolute', inset: -6000, backgroundImage: gridSvg, backgroundSize: '120px 120px', pointerEvents: 'none', zIndex: -1 }} />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// DCSection — editable title + h-row of artboards in persisted order
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function DCSection({ id, title, subtitle, children, gap = 48 }) {
|
||||
const ctx = React.useContext(DCCtx);
|
||||
const sid = id ?? title;
|
||||
const all = React.Children.toArray(children);
|
||||
const artboards = all.filter((c) => c && c.type === DCArtboard);
|
||||
const rest = all.filter((c) => !(c && c.type === DCArtboard));
|
||||
const sec = (ctx && sid && ctx.section(sid)) || {};
|
||||
// Must match DesignCanvas's srcKey computation exactly (it filters falsy
|
||||
// IDs), or onDelete persists a srcKey that DesignCanvas never recognizes.
|
||||
const allIds = artboards.map((a) => a.props.id ?? a.props.label).filter(Boolean);
|
||||
const srcKey = allIds.join('\x1f');
|
||||
const hidden = sec.srcKey === srcKey ? (sec.hidden || []) : [];
|
||||
const srcOrder = allIds.filter((k) => !hidden.includes(k));
|
||||
|
||||
const order = React.useMemo(() => {
|
||||
const kept = (sec.order || []).filter((k) => srcOrder.includes(k));
|
||||
return [...kept, ...srcOrder.filter((k) => !kept.includes(k))];
|
||||
}, [sec.order, srcOrder.join('|')]);
|
||||
|
||||
const byId = Object.fromEntries(artboards.map((a) => [a.props.id ?? a.props.label, a]));
|
||||
|
||||
// marginBottom counter-scales so the on-screen gap between sections stays
|
||||
// constant — otherwise at low zoom the (world-space) gap collapses while
|
||||
// the screen-constant sectionhead below it doesn't, and the title reads as
|
||||
// belonging to the section above. paddingBottom below is just enough for
|
||||
// the 24px artboard-header (abs-positioned above each card) plus ~8px, so
|
||||
// the title sits tight against its own row at every zoom.
|
||||
return (
|
||||
<div data-dc-section={sid}
|
||||
style={{ marginBottom: 'calc(80px * var(--dc-inv-zoom, 1))', position: 'relative' }}>
|
||||
<div style={{ padding: '0 60px' }}>
|
||||
<div className="dc-sectionhead" style={{ paddingBottom: 36 }}>
|
||||
<DCEditable tag="div" value={sec.title ?? title}
|
||||
onChange={(v) => ctx && sid && ctx.patchSection(sid, { title: v })}
|
||||
style={{ fontSize: 28, fontWeight: 600, color: DC.title, letterSpacing: -0.4, marginBottom: 6, display: 'inline-block' }} />
|
||||
{subtitle && <div style={{ fontSize: 16, color: DC.subtitle }}>{subtitle}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap, padding: '0 60px', alignItems: 'flex-start', width: 'max-content' }}>
|
||||
{order.map((k) => (
|
||||
<DCArtboardFrame key={k} sectionId={sid} artboard={byId[k]} order={order}
|
||||
label={(sec.labels || {})[k] ?? byId[k].props.label}
|
||||
onRename={(v) => ctx && ctx.patchSection(sid, (x) => ({ labels: { ...x.labels, [k]: v } }))}
|
||||
onReorder={(next) => ctx && ctx.patchSection(sid, { order: next })}
|
||||
onDelete={() => ctx && ctx.patchSection(sid, (x) => ({
|
||||
hidden: [...(x.srcKey === srcKey ? (x.hidden || []) : []), k],
|
||||
srcKey,
|
||||
}))}
|
||||
onFocus={() => ctx && ctx.setFocus(`${sid}/${k}`)} />
|
||||
))}
|
||||
</div>
|
||||
{rest}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// DCArtboard — marker; rendered by DCArtboardFrame via DCSection.
|
||||
function DCArtboard() { return null; }
|
||||
|
||||
// Per-artboard export (kind: 'png' | 'html'). Both paths share the same
|
||||
// self-contained clone: computed styles baked in, @font-face / <img> /
|
||||
// inline-style background-image urls inlined as data URIs. PNG wraps the
|
||||
// clone in foreignObject→canvas at 3× the artboard's natural width×height
|
||||
// (same pipeline the host uses for page captures); HTML wraps it in a
|
||||
// minimal standalone document. Both are independent of viewport zoom.
|
||||
async function dcExport(node, w, h, name, kind) {
|
||||
try { await document.fonts.ready; } catch {}
|
||||
const toDataURL = (url) => fetch(url).then((r) => r.blob()).then((b) => new Promise((res) => {
|
||||
const fr = new FileReader(); fr.onload = () => res(fr.result); fr.onerror = () => res(url); fr.readAsDataURL(b);
|
||||
})).catch(() => url);
|
||||
|
||||
// Collect @font-face rules. ss.cssRules throws SecurityError on
|
||||
// cross-origin sheets (e.g. fonts.googleapis.com) — in that case fetch
|
||||
// the CSS text directly (those endpoints send ACAO:*) and regex-extract
|
||||
// the blocks. @import and @media/@supports are walked so nested
|
||||
// @font-face rules aren't missed.
|
||||
const fontRules = [], pending = [], seen = new Set();
|
||||
const scrapeCss = (href) => {
|
||||
if (seen.has(href)) return; seen.add(href);
|
||||
pending.push(fetch(href).then((r) => r.text()).then((css) => {
|
||||
for (const m of css.match(/@font-face\s*{[^}]*}/g) || []) fontRules.push({ css: m, base: href });
|
||||
for (const m of css.matchAll(/@import\s+(?:url\()?['"]?([^'")\s;]+)/g))
|
||||
scrapeCss(new URL(m[1], href).href);
|
||||
}).catch(() => {}));
|
||||
};
|
||||
const walk = (rules, base) => {
|
||||
for (const r of rules) {
|
||||
if (r.type === CSSRule.FONT_FACE_RULE) fontRules.push({ css: r.cssText, base });
|
||||
else if (r.type === CSSRule.IMPORT_RULE && r.styleSheet) {
|
||||
const ibase = r.styleSheet.href || base;
|
||||
try { walk(r.styleSheet.cssRules, ibase); } catch { scrapeCss(ibase); }
|
||||
} else if (r.cssRules) walk(r.cssRules, base);
|
||||
}
|
||||
};
|
||||
for (const ss of document.styleSheets) {
|
||||
const base = ss.href || location.href;
|
||||
try { walk(ss.cssRules, base); } catch { if (ss.href) scrapeCss(ss.href); }
|
||||
}
|
||||
while (pending.length) await pending.shift();
|
||||
const fontCss = (await Promise.all(fontRules.map(async (rule) => {
|
||||
let out = rule.css, m; const re = /url\((['"]?)([^'")]+)\1\)/g;
|
||||
while ((m = re.exec(rule.css))) {
|
||||
if (m[2].indexOf('data:') === 0) continue;
|
||||
let abs; try { abs = new URL(m[2], rule.base).href; } catch { continue; }
|
||||
out = out.split(m[0]).join('url("' + await toDataURL(abs) + '")');
|
||||
}
|
||||
return out;
|
||||
}))).join('\n');
|
||||
|
||||
const cloneStyled = (src) => {
|
||||
if (src.nodeType === 8 || (src.nodeType === 1 && src.tagName === 'SCRIPT')) return document.createTextNode('');
|
||||
const dst = src.cloneNode(false);
|
||||
if (src.nodeType === 1) {
|
||||
const cs = getComputedStyle(src); let txt = '';
|
||||
for (let i = 0; i < cs.length; i++) txt += cs[i] + ':' + cs.getPropertyValue(cs[i]) + ';';
|
||||
dst.setAttribute('style', txt + 'animation:none;transition:none;');
|
||||
if (src.tagName === 'CANVAS') try { const im = document.createElement('img'); im.src = src.toDataURL(); im.setAttribute('style', txt); return im; } catch {}
|
||||
}
|
||||
for (let c = src.firstChild; c; c = c.nextSibling) dst.appendChild(cloneStyled(c));
|
||||
return dst;
|
||||
};
|
||||
const clone = cloneStyled(node);
|
||||
clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
|
||||
// Drop the card's own shadow/radius so the export is a flush w×h rect;
|
||||
// the artboard's own background (if any) is already in the computed style.
|
||||
clone.style.boxShadow = 'none'; clone.style.borderRadius = '0';
|
||||
|
||||
const jobs = [];
|
||||
clone.querySelectorAll('img').forEach((el) => {
|
||||
const s = el.getAttribute('src');
|
||||
if (s && s.indexOf('data:') !== 0) jobs.push(toDataURL(el.src).then((d) => el.setAttribute('src', d)));
|
||||
});
|
||||
[clone, ...clone.querySelectorAll('*')].forEach((el) => {
|
||||
const bg = el.style.backgroundImage; if (!bg) return;
|
||||
let m; const re = /url\(["']?([^"')]+)["']?\)/g;
|
||||
while ((m = re.exec(bg))) {
|
||||
const tok = m[0], url = m[1];
|
||||
if (url.indexOf('data:') === 0) continue;
|
||||
jobs.push(toDataURL(url).then((d) => { el.style.backgroundImage = el.style.backgroundImage.split(tok).join('url("' + d + '")'); }));
|
||||
}
|
||||
});
|
||||
await Promise.all(jobs);
|
||||
|
||||
const xml = new XMLSerializer().serializeToString(clone);
|
||||
const save = (blob, ext) => {
|
||||
if (!blob) return;
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob); a.download = name + '.' + ext; a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
||||
};
|
||||
|
||||
if (kind === 'html') {
|
||||
const html = '<!doctype html><html><head><meta charset="utf-8"><title>' + name + '</title>' +
|
||||
(fontCss ? '<style>' + fontCss + '</style>' : '') +
|
||||
'</head><body style="margin:0">' + xml + '</body></html>';
|
||||
return save(new Blob([html], { type: 'text/html' }), 'html');
|
||||
}
|
||||
|
||||
// PNG: the SVG's own width/height must be the output resolution — an
|
||||
// <img>-loaded SVG rasterizes at its intrinsic size, so sizing it at 1×
|
||||
// and ctx.scale()-ing up would just upscale a 1× bitmap. viewBox maps the
|
||||
// w×h foreignObject onto the px·w × px·h SVG canvas so the browser renders
|
||||
// the HTML at full resolution.
|
||||
const px = 3;
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + w * px + '" height="' + h * px +
|
||||
'" viewBox="0 0 ' + w + ' ' + h + '"><foreignObject width="' + w + '" height="' + h + '">' +
|
||||
(fontCss ? '<style><![CDATA[' + fontCss + ']]></style>' : '') + xml + '</foreignObject></svg>';
|
||||
const img = new Image();
|
||||
await new Promise((res, rej) => {
|
||||
img.onload = res; img.onerror = () => rej(new Error('svg load failed'));
|
||||
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
||||
});
|
||||
const cv = document.createElement('canvas');
|
||||
cv.width = w * px; cv.height = h * px;
|
||||
cv.getContext('2d').drawImage(img, 0, 0);
|
||||
cv.toBlob((blob) => save(blob, 'png'), 'image/png');
|
||||
}
|
||||
|
||||
function DCArtboardFrame({ sectionId, artboard, label, order, onRename, onReorder, onFocus, onDelete }) {
|
||||
const { id: rawId, label: rawLabel, width = 260, height = 480, children, style = {} } = artboard.props;
|
||||
const id = rawId ?? rawLabel;
|
||||
const ref = React.useRef(null);
|
||||
const cardRef = React.useRef(null);
|
||||
const menuRef = React.useRef(null);
|
||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||
const [confirming, setConfirming] = React.useState(false);
|
||||
|
||||
// ⋯ menu: close on any outside pointerdown. Two-click delete lives inside
|
||||
// the menu — first click arms the row, second commits; closing disarms.
|
||||
React.useEffect(() => {
|
||||
if (!menuOpen) { setConfirming(false); return; }
|
||||
const off = (e) => { if (!menuRef.current || !menuRef.current.contains(e.target)) setMenuOpen(false); };
|
||||
document.addEventListener('pointerdown', off, true);
|
||||
return () => document.removeEventListener('pointerdown', off, true);
|
||||
}, [menuOpen]);
|
||||
|
||||
const doExport = (kind) => {
|
||||
setMenuOpen(false);
|
||||
if (!cardRef.current) return;
|
||||
const name = String(label || id || 'artboard').replace(/[^\w\s.-]+/g, '_');
|
||||
dcExport(cardRef.current, width, height, name, kind)
|
||||
.catch((e) => console.error('[design-canvas] export failed:', e));
|
||||
};
|
||||
|
||||
// Live drag-reorder: dragged card sticks to cursor; siblings slide into
|
||||
// their would-be slots in real time via transforms. DOM order only
|
||||
// changes on drop.
|
||||
const onGripDown = (e) => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const me = ref.current;
|
||||
// translateX is applied in local (pre-scale) space but pointer deltas and
|
||||
// getBoundingClientRect().left are screen-space — divide by the viewport's
|
||||
// current scale so the dragged card tracks the cursor at any zoom level.
|
||||
const scale = me.getBoundingClientRect().width / me.offsetWidth || 1;
|
||||
const peers = Array.from(document.querySelectorAll(`[data-dc-section="${sectionId}"] [data-dc-slot]`));
|
||||
const homes = peers.map((el) => ({ el, id: el.dataset.dcSlot, x: el.getBoundingClientRect().left }));
|
||||
const slotXs = homes.map((h) => h.x);
|
||||
const startIdx = order.indexOf(id);
|
||||
const startX = e.clientX;
|
||||
let liveOrder = order.slice();
|
||||
me.classList.add('dc-dragging');
|
||||
|
||||
const layout = () => {
|
||||
for (const h of homes) {
|
||||
if (h.id === id) continue;
|
||||
const slot = liveOrder.indexOf(h.id);
|
||||
h.el.style.transform = `translateX(${(slotXs[slot] - h.x) / scale}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
const move = (ev) => {
|
||||
const dx = ev.clientX - startX;
|
||||
me.style.transform = `translateX(${dx / scale}px)`;
|
||||
const cur = homes[startIdx].x + dx;
|
||||
let nearest = 0, best = Infinity;
|
||||
for (let i = 0; i < slotXs.length; i++) {
|
||||
const d = Math.abs(slotXs[i] - cur);
|
||||
if (d < best) { best = d; nearest = i; }
|
||||
}
|
||||
if (liveOrder.indexOf(id) !== nearest) {
|
||||
liveOrder = order.filter((k) => k !== id);
|
||||
liveOrder.splice(nearest, 0, id);
|
||||
layout();
|
||||
}
|
||||
};
|
||||
|
||||
const up = () => {
|
||||
document.removeEventListener('pointermove', move);
|
||||
document.removeEventListener('pointerup', up);
|
||||
const finalSlot = liveOrder.indexOf(id);
|
||||
me.classList.remove('dc-dragging');
|
||||
me.style.transform = `translateX(${(slotXs[finalSlot] - homes[startIdx].x) / scale}px)`;
|
||||
// After the settle transition, kill transitions + clear transforms +
|
||||
// commit the reorder in the same frame so there's no visual snap-back.
|
||||
setTimeout(() => {
|
||||
for (const h of homes) { h.el.style.transition = 'none'; h.el.style.transform = ''; }
|
||||
if (liveOrder.join('|') !== order.join('|')) onReorder(liveOrder);
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
for (const h of homes) h.el.style.transition = '';
|
||||
}));
|
||||
}, 180);
|
||||
};
|
||||
document.addEventListener('pointermove', move);
|
||||
document.addEventListener('pointerup', up);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} data-dc-slot={id} style={{ position: 'relative', flexShrink: 0 }}>
|
||||
<div className="dc-header" style={{ color: DC.label }} onPointerDown={(e) => e.stopPropagation()}>
|
||||
<div className="dc-labelrow">
|
||||
<div className="dc-grip" onPointerDown={onGripDown} title="Drag to reorder">
|
||||
<svg width="9" height="13" viewBox="0 0 9 13" fill="currentColor"><circle cx="2" cy="2" r="1.1"/><circle cx="7" cy="2" r="1.1"/><circle cx="2" cy="6.5" r="1.1"/><circle cx="7" cy="6.5" r="1.1"/><circle cx="2" cy="11" r="1.1"/><circle cx="7" cy="11" r="1.1"/></svg>
|
||||
</div>
|
||||
<div className="dc-labeltext" onClick={onFocus} title="Click to focus">
|
||||
<DCEditable value={label} onChange={onRename} onClick={(e) => e.stopPropagation()}
|
||||
style={{ fontSize: 15, fontWeight: 500, color: DC.label, lineHeight: 1 }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="dc-btns">
|
||||
<div ref={menuRef} style={{ position: 'relative' }}>
|
||||
<button className="dc-kebab" title="More" onClick={() => setMenuOpen((o) => !o)}>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><circle cx="2.5" cy="6" r="1.1"/><circle cx="6" cy="6" r="1.1"/><circle cx="9.5" cy="6" r="1.1"/></svg>
|
||||
</button>
|
||||
{menuOpen && (
|
||||
<div className="dc-menu" onPointerDown={(e) => e.stopPropagation()}>
|
||||
<button onClick={() => doExport('png')}>Download PNG</button>
|
||||
<button onClick={() => doExport('html')}>Download HTML</button>
|
||||
<hr />
|
||||
<button className="dc-danger"
|
||||
onClick={() => { if (confirming) { setMenuOpen(false); onDelete(); } else setConfirming(true); }}>
|
||||
{confirming ? 'Click again to delete' : 'Delete'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="dc-expand" onClick={onFocus} title="Focus">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"><path d="M7 1h4v4M5 11H1V7M11 1L7.5 4.5M1 11l3.5-3.5"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={cardRef} className="dc-card"
|
||||
style={{ borderRadius: 2, boxShadow: '0 1px 3px rgba(0,0,0,.08),0 4px 16px rgba(0,0,0,.06)', overflow: 'hidden', width, height, background: '#fff', ...style }}>
|
||||
{children || <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#bbb', fontSize: 13, fontFamily: DC.font }}>{id}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline rename — commits on blur or Enter.
|
||||
function DCEditable({ value, onChange, style, tag = 'span', onClick }) {
|
||||
const T = tag;
|
||||
return (
|
||||
<T className="dc-editable" contentEditable suppressContentEditableWarning
|
||||
onClick={onClick}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onBlur={(e) => onChange && onChange(e.currentTarget.textContent)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
|
||||
style={style}>{value}</T>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Focus mode — overlay one artboard; ←/→ within section, ↑/↓ across
|
||||
// sections, Esc or backdrop click to exit.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) {
|
||||
const ctx = React.useContext(DCCtx);
|
||||
const { sectionId, artboard } = entry;
|
||||
const sec = ctx.section(sectionId);
|
||||
const meta = sectionMeta[sectionId];
|
||||
const peers = meta.slotIds;
|
||||
const aid = artboard.props.id ?? artboard.props.label;
|
||||
const idx = peers.indexOf(aid);
|
||||
const secIdx = sectionOrder.indexOf(sectionId);
|
||||
|
||||
const go = (d) => { const n = peers[(idx + d + peers.length) % peers.length]; if (n) ctx.setFocus(`${sectionId}/${n}`); };
|
||||
const goSection = (d) => {
|
||||
// Sections whose artboards are all deleted have slotIds:[] — step past
|
||||
// them to the next non-empty section so ↑/↓ doesn't dead-end.
|
||||
const n = sectionOrder.length;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const ns = sectionOrder[(((secIdx + d * i) % n) + n) % n];
|
||||
const first = sectionMeta[ns] && sectionMeta[ns].slotIds[0];
|
||||
if (first) { ctx.setFocus(`${ns}/${first}`); return; }
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const k = (e) => {
|
||||
if (e.key === 'ArrowLeft') { e.preventDefault(); go(-1); }
|
||||
if (e.key === 'ArrowRight') { e.preventDefault(); go(1); }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); goSection(-1); }
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); goSection(1); }
|
||||
};
|
||||
document.addEventListener('keydown', k);
|
||||
return () => document.removeEventListener('keydown', k);
|
||||
});
|
||||
|
||||
const { width = 260, height = 480, children } = artboard.props;
|
||||
const [vp, setVp] = React.useState({ w: window.innerWidth, h: window.innerHeight });
|
||||
React.useEffect(() => { const r = () => setVp({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', r); return () => window.removeEventListener('resize', r); }, []);
|
||||
const scale = Math.max(0.1, Math.min((vp.w - 200) / width, (vp.h - 260) / height, 2));
|
||||
|
||||
const [ddOpen, setDd] = React.useState(false);
|
||||
const Arrow = ({ dir, onClick }) => (
|
||||
<button onClick={(e) => { e.stopPropagation(); onClick(); }}
|
||||
style={{ position: 'absolute', top: '50%', [dir]: 28, transform: 'translateY(-50%)',
|
||||
border: 'none', background: 'rgba(255,255,255,.08)', color: 'rgba(255,255,255,.9)',
|
||||
width: 44, height: 44, borderRadius: 22, fontSize: 18, cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background .15s' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.18)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.08)')}>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||
<path d={dir === 'left' ? 'M11 3L5 9l6 6' : 'M7 3l6 6-6 6'} /></svg>
|
||||
</button>
|
||||
);
|
||||
|
||||
// Portal to body so position:fixed is the real viewport regardless of any
|
||||
// transform on DesignCanvas's ancestors (including the canvas zoom itself).
|
||||
return ReactDOM.createPortal(
|
||||
<div onClick={() => ctx.setFocus(null)}
|
||||
onWheel={(e) => e.preventDefault()}
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 100, background: 'rgba(24,20,16,.6)', backdropFilter: 'blur(14px)',
|
||||
fontFamily: DC.font, color: '#fff' }}>
|
||||
|
||||
{/* top bar: section dropdown (left) · close (right) */}
|
||||
<div onClick={(e) => e.stopPropagation()}
|
||||
style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 72, display: 'flex', alignItems: 'flex-start', padding: '16px 20px 0', gap: 16 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button onClick={() => setDd((o) => !o)}
|
||||
style={{ border: 'none', background: 'transparent', color: '#fff', cursor: 'pointer', padding: '6px 8px',
|
||||
borderRadius: 6, textAlign: 'left', fontFamily: 'inherit' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 18, fontWeight: 600, letterSpacing: -0.3 }}>{meta.title}</span>
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" style={{ opacity: .7 }}><path d="M2 4l3.5 3.5L9 4"/></svg>
|
||||
</span>
|
||||
{meta.subtitle && <span style={{ display: 'block', fontSize: 13, opacity: .6, fontWeight: 400, marginTop: 2 }}>{meta.subtitle}</span>}
|
||||
</button>
|
||||
{ddOpen && (
|
||||
<div style={{ position: 'absolute', top: '100%', left: 0, marginTop: 4, background: '#2a251f', borderRadius: 8,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,.4)', padding: 4, minWidth: 200, zIndex: 10 }}>
|
||||
{sectionOrder.filter((sid) => sectionMeta[sid].slotIds.length).map((sid) => (
|
||||
<button key={sid} onClick={() => { setDd(false); const f = sectionMeta[sid].slotIds[0]; if (f) ctx.setFocus(`${sid}/${f}`); }}
|
||||
style={{ display: 'block', width: '100%', textAlign: 'left', border: 'none', cursor: 'pointer',
|
||||
background: sid === sectionId ? 'rgba(255,255,255,.1)' : 'transparent', color: '#fff',
|
||||
padding: '8px 12px', borderRadius: 5, fontSize: 14, fontWeight: sid === sectionId ? 600 : 400, fontFamily: 'inherit' }}>
|
||||
{sectionMeta[sid].title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button onClick={() => ctx.setFocus(null)}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.12)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
style={{ border: 'none', background: 'transparent', color: 'rgba(255,255,255,.7)', width: 32, height: 32,
|
||||
borderRadius: 16, fontSize: 20, cursor: 'pointer', lineHeight: 1, transition: 'background .12s' }}>×</button>
|
||||
</div>
|
||||
|
||||
{/* card centered, label + index below — only the card itself stops
|
||||
propagation so any backdrop click (including the margins around
|
||||
the card) exits focus */}
|
||||
<div
|
||||
style={{ position: 'absolute', top: 64, bottom: 56, left: 100, right: 100, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16 }}>
|
||||
<div onClick={(e) => e.stopPropagation()} style={{ width: width * scale, height: height * scale, position: 'relative' }}>
|
||||
<div style={{ width, height, transform: `scale(${scale})`, transformOrigin: 'top left', background: '#fff', borderRadius: 2, overflow: 'hidden',
|
||||
boxShadow: '0 20px 80px rgba(0,0,0,.4)' }}>
|
||||
{children || <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#bbb' }}>{aid}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} style={{ fontSize: 14, fontWeight: 500, opacity: .85, textAlign: 'center' }}>
|
||||
{(sec.labels || {})[aid] ?? artboard.props.label}
|
||||
<span style={{ opacity: .5, marginLeft: 10, fontVariantNumeric: 'tabular-nums' }}>{idx + 1} / {peers.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Arrow dir="left" onClick={() => go(-1)} />
|
||||
<Arrow dir="right" onClick={() => go(1)} />
|
||||
|
||||
{/* dots */}
|
||||
<div onClick={(e) => e.stopPropagation()}
|
||||
style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 8 }}>
|
||||
{peers.map((p, i) => (
|
||||
<button key={p} onClick={() => ctx.setFocus(`${sectionId}/${p}`)}
|
||||
style={{ border: 'none', padding: 0, cursor: 'pointer', width: 6, height: 6, borderRadius: 3,
|
||||
background: i === idx ? '#fff' : 'rgba(255,255,255,.3)' }} />
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Post-it — absolute-positioned sticky note
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function DCPostIt({ children, top, left, right, bottom, rotate = -2, width = 180 }) {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', top, left, right, bottom, width,
|
||||
background: DC.postitBg, padding: '14px 16px',
|
||||
fontFamily: '"Comic Sans MS", "Marker Felt", "Segoe Print", cursive',
|
||||
fontSize: 14, lineHeight: 1.4, color: DC.postitText,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08)',
|
||||
transform: `rotate(${rotate}deg)`,
|
||||
zIndex: 5,
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { DesignCanvas, DCSection, DCArtboard, DCPostIt });
|
||||
|
||||
523
docs/design-system/source/docs-system.html
Normal file
523
docs/design-system/source/docs-system.html
Normal file
@@ -0,0 +1,523 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nibiru — Docs Design System</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="docs-system/tokens.css" />
|
||||
<link rel="stylesheet" href="docs-system/components.css" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #050208;
|
||||
font-family: "Inter Tight", system-ui, sans-serif;
|
||||
color: #f4eedb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Lotus logo as data URI (so the bundler can inline it & it works with file://) -->
|
||||
<img id="lotus-asset" src="docs-system/assets/lotus.png" style="display:none" alt=""/>
|
||||
<script>
|
||||
// Convert the loaded image to a data URL once, expose globally for components.
|
||||
(function () {
|
||||
const img = document.getElementById('lotus-asset');
|
||||
function publish() {
|
||||
try {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.naturalWidth || 861;
|
||||
c.height = img.naturalHeight || 569;
|
||||
c.getContext('2d').drawImage(img, 0, 0);
|
||||
window.NIBIRU_LOTUS = c.toDataURL('image/png');
|
||||
} catch (e) {
|
||||
window.NIBIRU_LOTUS = img.src;
|
||||
}
|
||||
}
|
||||
if (img.complete && img.naturalWidth) publish();
|
||||
else img.addEventListener('load', publish, { once: true });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#0b0410">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="50%" r="60%">
|
||||
<stop offset="0%" stop-color="#1a0824"/>
|
||||
<stop offset="100%" stop-color="#05020a"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="petalP" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#d8a8ff"/>
|
||||
<stop offset="100%" stop-color="#8a78c4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="petalB" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#bfd4ee"/>
|
||||
<stop offset="100%" stop-color="#7ea0cc"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1200" height="800" fill="url(#bg)"/>
|
||||
<g transform="translate(600 420)" opacity="0.95">
|
||||
<ellipse cx="0" cy="-80" rx="38" ry="140" fill="url(#petalP)"/>
|
||||
<ellipse cx="-130" cy="-30" rx="38" ry="140" fill="url(#petalP)" transform="rotate(-30 -130 -30)"/>
|
||||
<ellipse cx="130" cy="-30" rx="38" ry="140" fill="url(#petalB)" transform="rotate(30 130 -30)"/>
|
||||
<ellipse cx="-230" cy="40" rx="34" ry="120" fill="url(#petalP)" transform="rotate(-55 -230 40)"/>
|
||||
<ellipse cx="230" cy="40" rx="34" ry="120" fill="url(#petalB)" transform="rotate(55 230 40)"/>
|
||||
</g>
|
||||
<text x="600" y="640" text-anchor="middle" font-family="system-ui, sans-serif" font-size="64" font-weight="300" fill="#d8a8ff" letter-spacing="2">Nibiru</text>
|
||||
<text x="600" y="700" text-anchor="middle" font-family="system-ui, sans-serif" font-size="22" fill="#8ea8d4" letter-spacing="3">Docs Design System</text>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
|
||||
<script type="text/babel" src="design-canvas.jsx"></script>
|
||||
<script type="text/babel" src="docs-system/components/navigation.jsx"></script>
|
||||
<script type="text/babel" src="docs-system/components/typography-and-code.jsx"></script>
|
||||
<script type="text/babel" src="docs-system/components/extras.jsx"></script>
|
||||
|
||||
<script type="text/babel" data-presets="react">
|
||||
(() => {
|
||||
const { DesignCanvas, DCSection, DCArtboard } = window;
|
||||
|
||||
// ============================================================
|
||||
// Frame wrapper — gives each artboard the docs-frame baseline
|
||||
// ============================================================
|
||||
function Frame({ theme = "dark", padded = true, children, style }) {
|
||||
return (
|
||||
<div
|
||||
className={"docs-frame " + (theme === "light" ? "theme-light" : "")}
|
||||
style={{ width: "100%", height: "100%", ...style }}
|
||||
>
|
||||
<div style={{ background: "var(--space)", minHeight: "100%", height: "100%", padding: padded ? 32 : 0 }} className="cosmic-bg">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FULL PAGE PREVIEW (the marquee artboard)
|
||||
// ============================================================
|
||||
function FullPagePreview({ theme = "dark" }) {
|
||||
const tocItems = [
|
||||
{ id: "overview", level: 1, label: "Overview" },
|
||||
{ id: "box", level: 1, label: "What's in the box" },
|
||||
{ id: "mmvc", level: 1, label: "What MMVC actually means" },
|
||||
{ id: "lifecycle", level: 1, label: "The request lifecycle" },
|
||||
{ id: "for-whom", level: 1, label: "Who Nibiru is for" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={"docs-frame " + (theme === "light" ? "theme-light" : "")} style={{ height: "100%" }}>
|
||||
<div className="docs-page-preview-shell cosmic-bg">
|
||||
<TopNav theme={theme} />
|
||||
<div className="docs-page-preview-body">
|
||||
<Sidebar />
|
||||
<div className="docs-page-preview-content">
|
||||
<PageHeader
|
||||
crumbs={["Get Started", "What is Nibiru?"]}
|
||||
title="What is Nibiru?"
|
||||
summary="A modular MVC PHP framework — MMVC — built for rapid prototyping without giving up the discipline of a real framework."
|
||||
lastUpdated="2 days ago"
|
||||
/>
|
||||
<Prose>
|
||||
<h2 id="overview">Overview</h2>
|
||||
<p>
|
||||
<strong>Nibiru</strong> is a modular <em>MMVC</em> framework — Model, <strong>Module</strong>, View, Controller — small enough to fit in your head, powerful enough to back production apps.
|
||||
</p>
|
||||
<p>
|
||||
The name is a wink at Babylonian astronomy: <strong>Nibiru</strong> was the celestial crossing-point associated with Marduk. The framework runs on the same idea — a single point through which your modules, controllers, views and data cross paths.
|
||||
</p>
|
||||
|
||||
<h2 id="box">What's in the box</h2>
|
||||
<DocTable
|
||||
headers={["Layer", "Description"]}
|
||||
rows={[
|
||||
["Routing & dispatch", <>URL-pattern + SEO-URL parsing, soft 404, automatic action lookup.</>],
|
||||
["MVC + a second M", <>Controllers, Views (Smarty), Models, plus first-class <strong>Modules</strong> with traits, plugins, interfaces, settings and an observer pattern.</>],
|
||||
["Multi-database", <>Native MySQL, PDO, PostgreSQL via libpq (<code>psql</code> / <code>postgresql</code>) and ODBC, all behind a unified <code>Db</code> adapter.</>],
|
||||
["Forms", <>28+ field types built fluently with <code>Form::addInputType…()</code> and a layout helper for divs.</>],
|
||||
]}
|
||||
/>
|
||||
|
||||
<Callout kind="tip" title="Quick win">
|
||||
Run <code>nibiru new myapp</code> to scaffold a working project in under 30 seconds.
|
||||
</Callout>
|
||||
|
||||
<h2 id="mmvc">What MMVC actually means</h2>
|
||||
<p>
|
||||
The extra <strong>M</strong> stands for <em>Module</em> — a self-contained unit that can declare its own routes, controllers, models, views, and settings. Modules slot into the framework's registry and are wired together at boot.
|
||||
</p>
|
||||
</Prose>
|
||||
<Pagination prev={null} next="Why Nibiru, not Laravel" />
|
||||
</div>
|
||||
<RightTOC items={tocItems} activeId="box" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CANVAS
|
||||
// ============================================================
|
||||
function App() {
|
||||
return (
|
||||
<DesignCanvas title="Nibiru — Docs Design System" defaultZoom={0.65}>
|
||||
|
||||
{/* ============ SECTION: FULL PAGE ============ */}
|
||||
<DCSection id="full" title="Full page · the system in context">
|
||||
<DCArtboard id="full-dark" label="Dark — primary" width={1440} height={840}>
|
||||
<FullPagePreview theme="dark" />
|
||||
</DCArtboard>
|
||||
<DCArtboard id="full-light" label="Light — daylight reading" width={1440} height={840}>
|
||||
<FullPagePreview theme="light" />
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: TOP NAV ============ */}
|
||||
<DCSection id="nav" title="Top navigation">
|
||||
<DCArtboard id="nav-dark" label="Top nav — dark" width={1280} height={120}>
|
||||
<Frame padded={false}><TopNav theme="dark" /></Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="nav-light" label="Top nav — light" width={1280} height={120}>
|
||||
<Frame theme="light" padded={false}><TopNav theme="light" /></Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: SIDEBAR ============ */}
|
||||
<DCSection id="sidebar" title="Sidebar nav · sectioned with collapsibles">
|
||||
<DCArtboard id="sb-dark" label="Sidebar — dark" width={320} height={760}>
|
||||
<Frame padded={false}><Sidebar /></Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="sb-light" label="Sidebar — light" width={320} height={760}>
|
||||
<Frame theme="light" padded={false}><Sidebar /></Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="toc" label="On-this-page TOC" width={280} height={520}>
|
||||
<Frame padded={false}>
|
||||
<RightTOC
|
||||
activeId="lifecycle"
|
||||
items={[
|
||||
{ id: "overview", level: 1, label: "Overview" },
|
||||
{ id: "box", level: 1, label: "What's in the box" },
|
||||
{ id: "mmvc", level: 1, label: "What MMVC means" },
|
||||
{ id: "module", level: 2, label: "The Module layer" },
|
||||
{ id: "lifecycle", level: 1, label: "Request lifecycle" },
|
||||
{ id: "edge", level: 2, label: "Edge cases" },
|
||||
{ id: "for-whom", level: 1, label: "Who it's for" },
|
||||
]}
|
||||
/>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: PAGE HEADER & TYPOGRAPHY ============ */}
|
||||
<DCSection id="prose" title="Page header & body typography">
|
||||
<DCArtboard id="header" label="Page header" width={780} height={280}>
|
||||
<Frame>
|
||||
<PageHeader
|
||||
crumbs={["The Framework", "Architecture (MMVC)"]}
|
||||
title="Architecture (MMVC)"
|
||||
summary="How Model, Module, View, and Controller cross paths through a single dispatcher."
|
||||
lastUpdated="2 days ago"
|
||||
/>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="prose-dark" label="Prose — dark" width={780} height={620}>
|
||||
<Frame>
|
||||
<Prose>
|
||||
<h2>Headings, body, and rhythm</h2>
|
||||
<p>
|
||||
Body text uses <strong>Inter Tight</strong> at 16px with a 1.7 line height — the right balance between density and readability for technical content. Inline accents like <em>emphasized terms</em> use the magenta accent color, and <code>inline code</code> sits in a tinted token.
|
||||
</p>
|
||||
<h3>Lists carry their own rhythm</h3>
|
||||
<ul>
|
||||
<li>List markers pick up the magenta accent.</li>
|
||||
<li>Spacing between items is tight — 6px — so dense reference content stays scannable.</li>
|
||||
<li>Sub-bullets nest cleanly without losing the magenta thread.</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
"MMVC is the discipline of MVC plus the modularity of plugins — without the indirection of either."
|
||||
</blockquote>
|
||||
<p>
|
||||
Block quotes get a magenta left rail with a subtle gradient pulling the eye toward the quote.
|
||||
</p>
|
||||
</Prose>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="prose-light" label="Prose — light" width={780} height={620}>
|
||||
<Frame theme="light">
|
||||
<Prose>
|
||||
<h2>Headings, body, and rhythm</h2>
|
||||
<p>
|
||||
On the <strong>light theme</strong>, accent colors shift to higher-contrast equivalents — purple replaces magenta, teal replaces cyan — keeping <em>emphasized terms</em> and <code>inline code</code> legible against the parchment background.
|
||||
</p>
|
||||
<h3>Code samples stay dark</h3>
|
||||
<p>
|
||||
Code blocks remain on the cosmic plum background even in light mode. The contrast against parchment grounds the page and signals "this is a runnable artifact."
|
||||
</p>
|
||||
</Prose>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: CODE BLOCKS ============ */}
|
||||
<DCSection id="code" title="Code blocks · Mission-Control aesthetic">
|
||||
<DCArtboard id="code-php" label="PHP" width={760} height={440}>
|
||||
<Frame>
|
||||
<CodeBlock filename="app/Controllers/BookingController.php" lang="php">
|
||||
{phpSample}
|
||||
</CodeBlock>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="code-sql" label="SQL" width={760} height={300}>
|
||||
<Frame>
|
||||
<CodeBlock filename="queries/recent-bookings.sql" lang="sql">
|
||||
{sqlSample}
|
||||
</CodeBlock>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="code-yaml" label="YAML config" width={760} height={300}>
|
||||
<Frame>
|
||||
<CodeBlock filename="config/app.yaml" lang="yaml">
|
||||
{yamlSample}
|
||||
</CodeBlock>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="code-tabs" label="Tabbed code (PHP / SQL / YAML)" width={760} height={460}>
|
||||
<Frame>
|
||||
<Tabs tabs={[
|
||||
{ label: "PHP", content: <CodeBlock filename="BookingController.php" lang="php">{phpSample}</CodeBlock> },
|
||||
{ label: "SQL", content: <CodeBlock filename="queries.sql" lang="sql">{sqlSample}</CodeBlock> },
|
||||
{ label: "YAML", content: <CodeBlock filename="config/app.yaml" lang="yaml">{yamlSample}</CodeBlock> },
|
||||
]}/>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: CALLOUTS ============ */}
|
||||
<DCSection id="callouts" title="Callouts · cosmic-coded by severity">
|
||||
<DCArtboard id="callout-stack" label="All four kinds" width={780} height={460}>
|
||||
<Frame>
|
||||
<Callout kind="note" title="Note">
|
||||
The dispatcher resolves the controller and action automatically — you rarely need to wire routes by hand.
|
||||
</Callout>
|
||||
<Callout kind="tip" title="Tip">
|
||||
Use <code>nibiru make:module Blog</code> to scaffold a complete module — routes, controller, model, and views.
|
||||
</Callout>
|
||||
<Callout kind="warning" title="Backwards-compat break">
|
||||
In <code>2.0</code>, <code>Form::addInput()</code> is deprecated. Use <code>Form::addInputType()</code> instead.
|
||||
</Callout>
|
||||
<Callout kind="danger" title="Don't do this in production">
|
||||
Never run <code>nibiru migrate --fresh</code> against a production database — it drops every table.
|
||||
</Callout>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: TABLES ============ */}
|
||||
<DCSection id="tables" title="Reference tables">
|
||||
<DCArtboard id="table-dark" label="Reference table — dark" width={780} height={440}>
|
||||
<Frame>
|
||||
<DocTable
|
||||
headers={["Field type", "Renders", "Validation", "Example"]}
|
||||
rows={[
|
||||
["text", "Single-line input", "string", <code>$form->addInputType('text', 'name')</code>],
|
||||
["email", "Email input + envelope icon", "RFC 5322", <code>$form->addInputType('email', 'address')</code>],
|
||||
["password", "Masked input + reveal toggle", "min length", <code>$form->addInputType('password', 'pw')</code>],
|
||||
["select", "Native dropdown", "value-in-set", <code>$form->addInputType('select', 'role')</code>],
|
||||
["file", "Drag-drop uploader", "mime + size", <code>$form->addInputType('file', 'avatar')</code>],
|
||||
["date", "Native date picker", "ISO-8601", <code>$form->addInputType('date', 'dob')</code>],
|
||||
]}
|
||||
/>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: API REFERENCE ============ */}
|
||||
<DCSection id="api" title="API reference blocks">
|
||||
<DCArtboard id="api-block" label="Method signature + params + returns" width={780} height={620}>
|
||||
<Frame>
|
||||
<ApiBlock
|
||||
signature={{
|
||||
name: "Form::addInputType",
|
||||
args: [
|
||||
{ name: "type", type: "string" },
|
||||
{ name: "name", type: "string" },
|
||||
{ name: "options", type: "array" },
|
||||
],
|
||||
returns: "Form",
|
||||
}}
|
||||
params={[
|
||||
{ name: "type", type: "string", required: true, desc: "The input type — one of 28 supported types (text, email, select, file, date, etc.)." },
|
||||
{ name: "name", type: "string", required: true, desc: "Field name. Becomes the form key and the HTML name attribute." },
|
||||
{ name: "options", type: "array", default: "[]", desc: "Field-specific options: validation rules, placeholder, default value, attributes." },
|
||||
]}
|
||||
returns={{ type: "Form", desc: "The form instance, for fluent chaining." }}
|
||||
/>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: CARDS ============ */}
|
||||
<DCSection id="cards" title="Feature card grid">
|
||||
<DCArtboard id="cards-grid" label="Get-started card grid" width={780} height={320}>
|
||||
<Frame>
|
||||
<CardGrid cards={[
|
||||
{
|
||||
title: "Quick Start",
|
||||
desc: "Scaffold, run, and ship your first Nibiru app in 60 seconds.",
|
||||
glow: "linear-gradient(135deg, #b86bff, #ff7ab8)",
|
||||
icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>,
|
||||
},
|
||||
{
|
||||
title: "Modules",
|
||||
desc: "The fourth letter in MMVC — first-class plugins with their own scope.",
|
||||
glow: "linear-gradient(135deg, #6ad9ff, #b86bff)",
|
||||
icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>,
|
||||
},
|
||||
{
|
||||
title: "Routing",
|
||||
desc: "URL-pattern + SEO-URL parsing with automatic action lookup.",
|
||||
glow: "linear-gradient(135deg, #ffb574, #ff7ab8)",
|
||||
icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M6 16V8a4 4 0 0 1 4-4h4M18 8v8a4 4 0 0 1-4 4h-4"/></svg>,
|
||||
},
|
||||
{
|
||||
title: "Database",
|
||||
desc: "MySQL, PostgreSQL, ODBC — one unified Db adapter.",
|
||||
glow: "linear-gradient(135deg, #6ee7b0, #6ad9ff)",
|
||||
icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5M3 12a9 3 0 0 0 18 0"/></svg>,
|
||||
},
|
||||
]}/>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: SEARCH MODAL ============ */}
|
||||
<DCSection id="search" title="Search modal · ⌘K">
|
||||
<DCArtboard id="search-dark" label="Search modal — dark" width={680} height={580}>
|
||||
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(11,4,16,0.85)", padding: 32 }} className="docs-frame cosmic-bg">
|
||||
<SearchModal />
|
||||
</div>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="search-light" label="Search modal — light" width={680} height={580}>
|
||||
<div className="docs-frame theme-light" style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(250,246,236,0.7)", padding: 32 }}>
|
||||
<SearchModal />
|
||||
</div>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: PAGINATION + FAB + 404 ============ */}
|
||||
<DCSection id="utility" title="Pagination, floating help, empty states">
|
||||
<DCArtboard id="pagination" label="Page navigation footer" width={780} height={160}>
|
||||
<Frame>
|
||||
<Pagination prev="Quick Start" next="Project Structure" />
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="fab" label="Floating help button" width={300} height={240}>
|
||||
<Frame>
|
||||
<div style={{ position: "absolute", bottom: 32, right: 32 }}>
|
||||
<FAB />
|
||||
</div>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="404" label="404 / off-orbit" width={680} height={620}>
|
||||
<Frame>
|
||||
<div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<NotFound />
|
||||
</div>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: MOBILE ============ */}
|
||||
<DCSection id="mobile" title="Mobile drawer">
|
||||
<DCArtboard id="drawer-dark" label="Mobile drawer — dark" width={360} height={680}>
|
||||
<div className="docs-frame" style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", padding: 16, background: "var(--space)" }}>
|
||||
<MobileDrawer />
|
||||
</div>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="drawer-light" label="Mobile drawer — light" width={360} height={680}>
|
||||
<div className="docs-frame theme-light" style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", padding: 16, background: "var(--space)" }}>
|
||||
<MobileDrawer />
|
||||
</div>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
{/* ============ SECTION: COLOR / TYPE TOKENS ============ */}
|
||||
<DCSection id="tokens" title="Tokens · color & type">
|
||||
<DCArtboard id="palette" label="Cosmic palette" width={920} height={360}>
|
||||
<Frame>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(6, 1fr)", gap: 12 }}>
|
||||
{[
|
||||
["space", "#0b0410", "Deepest void"],
|
||||
["space-2", "#120822", "Panel surface"],
|
||||
["space-3", "#1a0c2e", "Raised surface"],
|
||||
["plum", "#2a1545", "Nebula plum"],
|
||||
["star", "#f4eedb", "Primary text"],
|
||||
["muted", "#8b85a3", "Tertiary text"],
|
||||
["nebula-mag", "#b86bff", "Accent · links"],
|
||||
["nebula-cyan", "#6ad9ff", "Accent · note"],
|
||||
["nebula-amber","#ffb574", "Accent · warn"],
|
||||
["nebula-rose", "#ff7ab8", "Accent · danger"],
|
||||
["nebula-green","#6ee7b0", "Accent · tip"],
|
||||
["nebula-mag-2","#d8a8ff", "Accent · hover"],
|
||||
].map(([name, hex, desc]) => (
|
||||
<div key={name} style={{ borderRadius: 12, border: "1px solid var(--line)", overflow: "hidden", background: "var(--space-2)" }}>
|
||||
<div style={{ height: 80, background: hex, position: "relative", boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.04)" }}/>
|
||||
<div style={{ padding: "10px 12px" }}>
|
||||
<div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--star)", letterSpacing: "0.04em" }}>--{name}</div>
|
||||
<div style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--muted)", marginTop: 2 }}>{hex}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--star-soft)", marginTop: 4 }}>{desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
<DCArtboard id="type" label="Type scale" width={780} height={520}>
|
||||
<Frame>
|
||||
<div className="docs-frame">
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
|
||||
{[
|
||||
["48 · page hero", 48, 600, "Architecture (MMVC)"],
|
||||
["36 · page title", 36, 600, "What is Nibiru?"],
|
||||
["28 · h2", 28, 600, "What's in the box"],
|
||||
["22 · h3", 22, 600, "The Module layer"],
|
||||
["18 · summary", 18, 400, "A modular MVC PHP framework — MMVC."],
|
||||
["16 · body", 16, 400, "Body text uses Inter Tight at 16px with 1.7 leading."],
|
||||
["13 · ui", 13, 500, "Sidebar items, buttons, breadcrumbs"],
|
||||
["11 · mono / kbd", 11, 500, "GET STARTED · ⌘K · ROUTING & DISPATCH"],
|
||||
].map(([label, size, weight, sample]) => (
|
||||
<div key={label} style={{ display: "grid", gridTemplateColumns: "180px 1fr", alignItems: "baseline", gap: 24 }}>
|
||||
<div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--muted)", letterSpacing: "0.06em", textTransform: "uppercase" }}>{label}</div>
|
||||
<div style={{
|
||||
fontFamily: typeof label === "string" && label.includes("mono") ? "var(--font-mono)" : "var(--font-sans)",
|
||||
fontSize: size, fontWeight: weight, color: "var(--star)",
|
||||
letterSpacing: size >= 28 ? "-0.02em" : "0",
|
||||
lineHeight: 1.15,
|
||||
}}>{sample}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Frame>
|
||||
</DCArtboard>
|
||||
</DCSection>
|
||||
|
||||
</DesignCanvas>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
BIN
docs/design-system/source/docs-system/assets/lotus.png
Normal file
BIN
docs/design-system/source/docs-system/assets/lotus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
BIN
docs/design-system/source/docs-system/assets/nibiru-full.png
Normal file
BIN
docs/design-system/source/docs-system/assets/nibiru-full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
1103
docs/design-system/source/docs-system/components.css
Normal file
1103
docs/design-system/source/docs-system/components.css
Normal file
File diff suppressed because it is too large
Load Diff
291
docs/design-system/source/docs-system/components/extras.jsx
Normal file
291
docs/design-system/source/docs-system/components/extras.jsx
Normal file
@@ -0,0 +1,291 @@
|
||||
/* global React */
|
||||
const { useState: useEState } = React;
|
||||
|
||||
// ============================================================
|
||||
// TABS
|
||||
// ============================================================
|
||||
function Tabs({ tabs }) {
|
||||
const [active, setActive] = useEState(0);
|
||||
return (
|
||||
<div className="tabs">
|
||||
<div className="tabs-list" role="tablist">
|
||||
{tabs.map((t, i) => (
|
||||
<button
|
||||
key={i}
|
||||
role="tab"
|
||||
aria-selected={i === active}
|
||||
className={"tabs-trigger" + (i === active ? " active" : "")}
|
||||
onClick={() => setActive(i)}
|
||||
>
|
||||
{t.icon && <span className="tabs-icon">{t.icon}</span>}
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="tabs-panel">
|
||||
{tabs[active].content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// API BLOCK (parameter list)
|
||||
// ============================================================
|
||||
function ApiBlock({ signature, params = [], returns }) {
|
||||
return (
|
||||
<div className="api-block">
|
||||
<div className="api-signature">
|
||||
<span className="tk-fn">{signature.name}</span>
|
||||
<span className="api-paren">(</span>
|
||||
{signature.args.map((a, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<span className="tk-var">{a.name}</span>
|
||||
<span className="api-colon">: </span>
|
||||
<span className="tk-cn">{a.type}</span>
|
||||
{i < signature.args.length - 1 && <span className="api-comma">, </span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span className="api-paren">)</span>
|
||||
{signature.returns && (
|
||||
<>
|
||||
<span className="api-arrow"> → </span>
|
||||
<span className="tk-cn">{signature.returns}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="api-section-label">Parameters</div>
|
||||
<ul className="api-params">
|
||||
{params.map((p, i) => (
|
||||
<li key={i} className="api-param">
|
||||
<div className="api-param-head">
|
||||
<span className="api-param-name">{p.name}</span>
|
||||
<span className="api-param-type">{p.type}</span>
|
||||
{p.required && <span className="api-param-required">required</span>}
|
||||
{p.default !== undefined && <span className="api-param-default">default: <code>{p.default}</code></span>}
|
||||
</div>
|
||||
<div className="api-param-desc">{p.desc}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{returns && (
|
||||
<>
|
||||
<div className="api-section-label">Returns</div>
|
||||
<div className="api-returns">
|
||||
<span className="tk-cn">{returns.type}</span>
|
||||
<span className="api-returns-desc"> — {returns.desc}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CARD GRID (feature cards)
|
||||
// ============================================================
|
||||
function CardGrid({ cards }) {
|
||||
return (
|
||||
<div className="card-grid">
|
||||
{cards.map((c, i) => (
|
||||
<a href="#" key={i} className="feature-card">
|
||||
<div className="feature-card-icon" style={{ background: c.glow }}>
|
||||
{c.icon}
|
||||
</div>
|
||||
<div className="feature-card-title">{c.title}</div>
|
||||
<div className="feature-card-desc">{c.desc}</div>
|
||||
<div className="feature-card-arrow">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M7 17 17 7M7 7h10v10"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SEARCH MODAL
|
||||
// ============================================================
|
||||
function SearchModal() {
|
||||
const recent = ["Routing", "Form::addInputType", "Migrations"];
|
||||
const results = [
|
||||
{ section: "The Framework", title: "Routing", excerpt: "URL-pattern + SEO-URL parsing, soft 404, automatic action lookup.", kind: "page" },
|
||||
{ section: "The Framework", title: "Routing › Soft 404", excerpt: "When a route doesn't match, the dispatcher falls through to a soft 404 view.", kind: "section" },
|
||||
{ section: "Get Started", title: "Quick Start › Define a route", excerpt: "Routes are declared in config/routes.php and matched by URL pattern.", kind: "section" },
|
||||
{ section: "API", title: "Router::register()", excerpt: "Register a new route at runtime. Supports pattern matching with named parameters.", kind: "api" },
|
||||
];
|
||||
return (
|
||||
<div className="search-modal">
|
||||
<div className="search-input-wrap">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="search-icon">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<input className="search-input" placeholder="Search docs, API, examples..." defaultValue="rout" />
|
||||
<kbd className="search-kbd">esc</kbd>
|
||||
</div>
|
||||
<div className="search-recent">
|
||||
<div className="search-section-label">Recent</div>
|
||||
<div className="search-recent-list">
|
||||
{recent.map((r, i) => (
|
||||
<button key={i} className="search-chip">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
{r}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="search-results">
|
||||
<div className="search-section-label">Results</div>
|
||||
{results.map((r, i) => (
|
||||
<button key={i} className={"search-result" + (i === 0 ? " active" : "")}>
|
||||
<div className={"search-result-kind kind-" + r.kind}>{r.kind}</div>
|
||||
<div className="search-result-body">
|
||||
<div className="search-result-title">
|
||||
<span className="search-result-section">{r.section}</span>
|
||||
<span className="search-result-sep">›</span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.title.replace(/(rout)/i, "<mark>$1</mark>") }} />
|
||||
</div>
|
||||
<div className="search-result-excerpt">{r.excerpt}</div>
|
||||
</div>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="search-result-go">
|
||||
<path d="M7 17 17 7M7 7h10v10"/>
|
||||
</svg>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="search-footer">
|
||||
<span><kbd>↑↓</kbd> navigate</span>
|
||||
<span><kbd>↵</kbd> open</span>
|
||||
<span><kbd>⌘K</kbd> close</span>
|
||||
<div className="search-credit">Powered by <strong>Nibiru Search</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FAB (floating help button)
|
||||
// ============================================================
|
||||
function FAB() {
|
||||
return (
|
||||
<div className="fab-wrap">
|
||||
<button className="fab" aria-label="Help">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="fab-tooltip">Ask AI assistant</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PAGINATION (prev / next page)
|
||||
// ============================================================
|
||||
function Pagination({ prev, next }) {
|
||||
return (
|
||||
<nav className="page-nav">
|
||||
{prev ? (
|
||||
<a href="#" className="page-nav-link page-nav-prev">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m15 18-6-6 6-6"/></svg>
|
||||
<div className="page-nav-stack">
|
||||
<div className="page-nav-label">Previous</div>
|
||||
<div className="page-nav-title">{prev}</div>
|
||||
</div>
|
||||
</a>
|
||||
) : <div />}
|
||||
{next ? (
|
||||
<a href="#" className="page-nav-link page-nav-next">
|
||||
<div className="page-nav-stack">
|
||||
<div className="page-nav-label">Next</div>
|
||||
<div className="page-nav-title">{next}</div>
|
||||
</div>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m9 18 6-6-6-6"/></svg>
|
||||
</a>
|
||||
) : <div />}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MOBILE DRAWER
|
||||
// ============================================================
|
||||
function MobileDrawer() {
|
||||
return (
|
||||
<div className="mobile-drawer">
|
||||
<div className="mobile-drawer-header">
|
||||
<div className="mobile-drawer-brand">
|
||||
<LotusMark size={26} />
|
||||
<span>Nibiru</span>
|
||||
</div>
|
||||
<button className="mobile-drawer-close" aria-label="Close">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m18 6-12 12M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<button className="mobile-drawer-search">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<span>Search</span>
|
||||
<kbd>⌘K</kbd>
|
||||
</button>
|
||||
<Sidebar />
|
||||
<div className="mobile-drawer-footer">
|
||||
<button className="mobile-drawer-pill">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41"/></svg>
|
||||
Theme
|
||||
</button>
|
||||
<button className="mobile-drawer-pill">EN</button>
|
||||
<a href="#" className="mobile-drawer-pill">GitHub ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 404 / EMPTY STATE
|
||||
// ============================================================
|
||||
function NotFound() {
|
||||
return (
|
||||
<div className="not-found">
|
||||
<div className="not-found-orbit">
|
||||
<svg viewBox="0 0 320 200" width="100%" height="100%" aria-hidden="true">
|
||||
<defs>
|
||||
<radialGradient id="lostStar" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stopColor="#fff4d9"/>
|
||||
<stop offset="50%" stopColor="#ffb574"/>
|
||||
<stop offset="100%" stopColor="#b86bff" stopOpacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
{/* dashed orbit */}
|
||||
<ellipse cx="160" cy="100" rx="140" ry="60" fill="none" stroke="rgba(184,107,255,0.3)" strokeWidth="1" strokeDasharray="3 6"/>
|
||||
<ellipse cx="160" cy="100" rx="100" ry="42" fill="none" stroke="rgba(184,107,255,0.18)" strokeWidth="1" strokeDasharray="3 6"/>
|
||||
{/* central star */}
|
||||
<circle cx="160" cy="100" r="50" fill="url(#lostStar)"/>
|
||||
<circle cx="160" cy="100" r="6" fill="#fff4d9"/>
|
||||
{/* drifting probe (off-orbit) */}
|
||||
<g transform="translate(280 40)">
|
||||
<circle r="3" fill="#6ad9ff"/>
|
||||
<circle r="8" fill="#6ad9ff" opacity="0.2"/>
|
||||
</g>
|
||||
<text x="290" y="35" fill="#6ad9ff" fontSize="10" fontFamily="JetBrains Mono">PROBE-404</text>
|
||||
<line x1="160" y1="100" x2="280" y2="40" stroke="#6ad9ff" strokeWidth="1" strokeDasharray="2 3" opacity="0.4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="not-found-code">404 / off-orbit</div>
|
||||
<h2 className="not-found-title">This page drifted away from Nibiru.</h2>
|
||||
<p className="not-found-desc">
|
||||
The probe couldn't find the page you requested. It may have been moved, renamed, or pulled into a different orbit.
|
||||
</p>
|
||||
<div className="not-found-actions">
|
||||
<a href="#" className="btn btn-primary">Return to docs</a>
|
||||
<a href="#" className="btn btn-ghost">Open search</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { Tabs, ApiBlock, CardGrid, SearchModal, FAB, Pagination, MobileDrawer, NotFound });
|
||||
192
docs/design-system/source/docs-system/components/navigation.jsx
Normal file
192
docs/design-system/source/docs-system/components/navigation.jsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/* global React */
|
||||
const { useState } = React;
|
||||
|
||||
// ============================================================
|
||||
// TOP NAV
|
||||
// ============================================================
|
||||
function TopNav({ theme = "dark", onToggleTheme = () => {}, locale = "EN" }) {
|
||||
return (
|
||||
<header className="topnav">
|
||||
<div className="topnav-brand">
|
||||
<LotusMark />
|
||||
<div className="topnav-brand-text">
|
||||
<div className="topnav-brand-name">Nibiru</div>
|
||||
<div className="topnav-brand-tag">Create, Invent, Impress</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="topnav-search" aria-label="Search docs">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<span>Search</span>
|
||||
<kbd>⌘ K</kbd>
|
||||
</button>
|
||||
|
||||
<div className="topnav-right">
|
||||
<a href="#" className="topnav-icon" aria-label="GitHub">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-1.97c-3.2.69-3.87-1.54-3.87-1.54-.52-1.33-1.27-1.69-1.27-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.68 1.25 3.34.96.1-.74.4-1.25.72-1.54-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.46.11-3.04 0 0 .96-.31 3.15 1.18a10.95 10.95 0 0 1 5.74 0c2.18-1.49 3.14-1.18 3.14-1.18.62 1.58.23 2.75.11 3.04.74.81 1.18 1.84 1.18 3.1 0 4.42-2.69 5.39-5.26 5.68.41.36.78 1.06.78 2.13v3.16c0 .31.21.67.8.56C20.21 21.39 23.5 17.08 23.5 12 23.5 5.65 18.35.5 12 .5Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<button className="topnav-icon" aria-label="Toggle theme" onClick={onToggleTheme}>
|
||||
{theme === "dark" ? (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<button className="topnav-locale" aria-label="Locale">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20"/>
|
||||
</svg>
|
||||
<span>{locale}</span>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function LotusMark({ size = 36 }) {
|
||||
// Real Nibiru logo, sourced from window.NIBIRU_LOTUS so the bundler picks it up
|
||||
// (it would miss a relative path inside a JSX template string).
|
||||
const src = (typeof window !== "undefined" && window.NIBIRU_LOTUS) || "docs-system/assets/lotus.png";
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
width={size}
|
||||
height={Math.round(size * 569 / 861)}
|
||||
alt="Nibiru"
|
||||
className="lotus-mark"
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SIDEBAR
|
||||
// ============================================================
|
||||
function Sidebar() {
|
||||
const [openSections, setOpenSections] = useState({
|
||||
"get-started": true, "framework": true, "cli": false, "advanced": false,
|
||||
});
|
||||
const toggle = (id) => setOpenSections((s) => ({ ...s, [id]: !s[id] }));
|
||||
|
||||
const nav = [
|
||||
{
|
||||
id: "get-started", label: "Get Started",
|
||||
items: [
|
||||
{ label: "What is Nibiru?", active: true },
|
||||
{ label: "Why Nibiru, not Laravel" },
|
||||
{ label: "Installation" },
|
||||
{ label: "Quick Start" },
|
||||
{ label: "Project Structure" },
|
||||
{ label: "Run It Locally" },
|
||||
{ label: "Deployment" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "framework", label: "The Framework",
|
||||
items: [
|
||||
{ label: "Architecture (MMVC)" },
|
||||
{ label: "Bootstrap & Dispatcher" },
|
||||
{ label: "Routing" },
|
||||
{ label: "Controllers" },
|
||||
{ label: "Views & Smarty" },
|
||||
{ label: "Models" },
|
||||
{ label: "Modules", badge: "core" },
|
||||
{ label: "Forms" },
|
||||
{ label: "Database & Migrations" },
|
||||
{ label: "Auth" },
|
||||
{ label: "Config & Settings" },
|
||||
{ label: "Pagination" },
|
||||
{ label: "Registry" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "cli", label: "CLI",
|
||||
items: [
|
||||
{ label: "nibiru new" },
|
||||
{ label: "nibiru migrate" },
|
||||
{ label: "nibiru make" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "advanced", label: "Advanced",
|
||||
items: [
|
||||
{ label: "Plugins" },
|
||||
{ label: "Observer pattern" },
|
||||
{ label: "Caching", badge: "new" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<nav>
|
||||
{nav.map((section) => (
|
||||
<div key={section.id} className="sidebar-section">
|
||||
<button
|
||||
className="sidebar-heading"
|
||||
onClick={() => toggle(section.id)}
|
||||
aria-expanded={openSections[section.id]}
|
||||
>
|
||||
<span>{section.label}</span>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
|
||||
style={{ transform: openSections[section.id] ? "rotate(0deg)" : "rotate(-90deg)", transition: "transform 200ms" }}>
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
{openSections[section.id] && (
|
||||
<ul className="sidebar-list">
|
||||
{section.items.map((item) => (
|
||||
<li key={item.label}>
|
||||
<a href="#" className={"sidebar-item" + (item.active ? " active" : "")}>
|
||||
<span>{item.label}</span>
|
||||
{item.badge && <span className={"sidebar-badge sidebar-badge-" + item.badge}>{item.badge}</span>}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RIGHT TOC ("On this page")
|
||||
// ============================================================
|
||||
function RightTOC({ items, activeId }) {
|
||||
return (
|
||||
<aside className="toc">
|
||||
<div className="toc-label">On this page</div>
|
||||
<ul className="toc-list">
|
||||
{items.map((item) => (
|
||||
<li key={item.id} className={"toc-item toc-level-" + item.level}>
|
||||
<a href={"#" + item.id} className={item.id === activeId ? "active" : ""}>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="toc-edit">
|
||||
<a href="#"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg> Edit this page</a>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { TopNav, Sidebar, RightTOC, LotusMark });
|
||||
@@ -0,0 +1,166 @@
|
||||
/* global React */
|
||||
const { useState: useTState } = React;
|
||||
|
||||
// ============================================================
|
||||
// PAGE HEADER (breadcrumbs, title, last-updated)
|
||||
// ============================================================
|
||||
function PageHeader({ crumbs = [], title, lastUpdated, summary }) {
|
||||
return (
|
||||
<header className="page-header">
|
||||
<nav className="breadcrumbs" aria-label="Breadcrumb">
|
||||
{crumbs.map((c, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<a href="#">{c}</a>
|
||||
{i < crumbs.length - 1 && <span className="breadcrumb-sep">/</span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</nav>
|
||||
<h1 className="page-title">{title}</h1>
|
||||
{summary && <p className="page-summary">{summary}</p>}
|
||||
{lastUpdated && (
|
||||
<div className="page-meta">
|
||||
<span className="page-meta-dot" />
|
||||
Updated {lastUpdated}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PROSE — typography sample block
|
||||
// ============================================================
|
||||
function Prose({ children }) {
|
||||
return <div className="prose">{children}</div>;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CODE BLOCK (Mission Control aesthetic)
|
||||
// ============================================================
|
||||
function CodeBlock({ filename, lang = "php", children, lineNumbers = false }) {
|
||||
const [copied, setCopied] = useTState(false);
|
||||
const onCopy = () => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1400);
|
||||
};
|
||||
|
||||
// children is array of {type, text} tokens, or a string
|
||||
const rendered = Array.isArray(children) ? children : [{ text: children }];
|
||||
|
||||
return (
|
||||
<div className="codeblock">
|
||||
<div className="codeblock-header">
|
||||
<div className="codeblock-dots">
|
||||
<span /><span /><span />
|
||||
</div>
|
||||
{filename && <div className="codeblock-filename">{filename}</div>}
|
||||
<div className="codeblock-lang">{lang}</div>
|
||||
<button className="codeblock-copy" onClick={onCopy} aria-label="Copy">
|
||||
{copied ? (
|
||||
<><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg> Copied</>
|
||||
) : (
|
||||
<><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="codeblock-pre">
|
||||
<code>
|
||||
{rendered.map((line, i) => (
|
||||
<div key={i} className="codeblock-line">
|
||||
{lineNumbers && <span className="codeblock-ln">{i + 1}</span>}
|
||||
<span dangerouslySetInnerHTML={{ __html: line.text }} />
|
||||
</div>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Token helper — wraps text in a syntax-color span
|
||||
function tk(cls, text) {
|
||||
return `<span class="tk-${cls}">${text}</span>`;
|
||||
}
|
||||
|
||||
// Pre-tokenized PHP sample (saves us writing a real lexer)
|
||||
const phpSample = [
|
||||
{ text: `${tk("kw", "namespace")} ${tk("ns", "App\\\\Controllers")};` },
|
||||
{ text: `` },
|
||||
{ text: `${tk("kw", "use")} ${tk("ns", "Nibiru\\\\Controller")};` },
|
||||
{ text: `${tk("kw", "use")} ${tk("ns", "Nibiru\\\\Form")};` },
|
||||
{ text: `` },
|
||||
{ text: `${tk("kw", "class")} ${tk("cn", "BookingController")} ${tk("kw", "extends")} ${tk("cn", "Controller")} {` },
|
||||
{ text: ` ${tk("kw", "public")} ${tk("kw", "function")} ${tk("fn", "create")}(${tk("var", "$req")}) {` },
|
||||
{ text: ` ${tk("var", "$form")} = ${tk("kw", "new")} ${tk("cn", "Form")}();` },
|
||||
{ text: ` ${tk("var", "$form")}->${tk("fn", "addInputType")}(${tk("str", "'email'")}, ${tk("str", "'address'")});` },
|
||||
{ text: ` ${tk("kw", "return")} ${tk("var", "$this")}->${tk("fn", "view")}(${tk("str", "'booking/create'")}, [` },
|
||||
{ text: ` ${tk("str", "'form'")} => ${tk("var", "$form")},` },
|
||||
{ text: ` ]);` },
|
||||
{ text: ` }` },
|
||||
{ text: `}` },
|
||||
];
|
||||
|
||||
const sqlSample = [
|
||||
{ text: `${tk("kw", "SELECT")} ${tk("var", "id")}, ${tk("var", "title")}, ${tk("var", "created_at")}` },
|
||||
{ text: `${tk("kw", "FROM")} ${tk("cn", "bookings")}` },
|
||||
{ text: `${tk("kw", "WHERE")} ${tk("var", "user_id")} = ${tk("num", "42")}` },
|
||||
{ text: ` ${tk("kw", "AND")} ${tk("var", "status")} = ${tk("str", "'confirmed'")}` },
|
||||
{ text: `${tk("kw", "ORDER BY")} ${tk("var", "created_at")} ${tk("kw", "DESC")}` },
|
||||
{ text: `${tk("kw", "LIMIT")} ${tk("num", "20")};` },
|
||||
];
|
||||
|
||||
const yamlSample = [
|
||||
{ text: `${tk("kw", "app")}:` },
|
||||
{ text: ` ${tk("kw", "name")}: ${tk("str", "Nibiru")}` },
|
||||
{ text: ` ${tk("kw", "version")}: ${tk("str", "2.0.0")}` },
|
||||
{ text: ` ${tk("kw", "modules")}:` },
|
||||
{ text: ` - ${tk("str", "auth")}` },
|
||||
{ text: ` - ${tk("str", "blog")}` },
|
||||
{ text: ` - ${tk("str", "api")}` },
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
// CALLOUTS (cosmic-coded)
|
||||
// ============================================================
|
||||
function Callout({ kind = "note", title, children }) {
|
||||
const icons = {
|
||||
note: <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" fill="currentColor"/><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.5" fill="none"/></svg>,
|
||||
tip: <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2 14 8l6 1-4.5 4 1 6-4.5-3-4.5 3 1-6L4 9l6-1z"/></svg>,
|
||||
warning: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v6M12 16h.01"/></svg>,
|
||||
danger: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m12 2 10 18H2L12 2zM12 9v4M12 17h.01"/></svg>,
|
||||
};
|
||||
const labels = { note: "Note", tip: "Tip", warning: "Warning", danger: "Danger" };
|
||||
return (
|
||||
<div className={"callout callout-" + kind}>
|
||||
<div className="callout-glyph">{icons[kind]}</div>
|
||||
<div className="callout-body">
|
||||
<div className="callout-title">{title || labels[kind]}</div>
|
||||
<div className="callout-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TABLE
|
||||
// ============================================================
|
||||
function DocTable({ headers, rows }) {
|
||||
return (
|
||||
<div className="doc-table-wrap">
|
||||
<table className="doc-table">
|
||||
<thead>
|
||||
<tr>{headers.map((h, i) => <th key={i}>{h}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((cell, j) => <td key={j}>{cell}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { PageHeader, Prose, CodeBlock, Callout, DocTable, phpSample, sqlSample, yamlSample, tk });
|
||||
197
docs/design-system/source/docs-system/tokens.css
Normal file
197
docs/design-system/source/docs-system/tokens.css
Normal file
@@ -0,0 +1,197 @@
|
||||
/* ============================================================
|
||||
NIBIRU DOCS — DESIGN TOKENS
|
||||
Dark primary (cosmic), light variant (parchment)
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* ---- Cosmic palette (carried from start page) ---- */
|
||||
--space: #0b0410; /* deepest void */
|
||||
--space-2: #120822; /* panel bg */
|
||||
--space-3: #1a0c2e; /* raised */
|
||||
--plum: #2a1545; /* deep nebula plum */
|
||||
--plum-2: #3a1d5e; /* hover plum */
|
||||
|
||||
--star: #f4eedb; /* cream — primary text */
|
||||
--star-soft: #d8d2c0; /* secondary text */
|
||||
--muted: #8b85a3; /* tertiary / labels */
|
||||
--muted-2: #5e5878; /* very muted */
|
||||
|
||||
--line: rgba(244, 238, 219, 0.08);
|
||||
--line-strong: rgba(244, 238, 219, 0.16);
|
||||
--line-glow: rgba(184, 107, 255, 0.25);
|
||||
|
||||
/* ---- Accents (the "nebula" colors) ---- */
|
||||
--nebula-mag: #b86bff; /* magenta — primary accent, links */
|
||||
--nebula-mag-2: #d8a8ff;
|
||||
--nebula-cyan: #6ad9ff; /* cyan — secondary accent */
|
||||
--nebula-amber: #ffb574; /* warm star highlight */
|
||||
--nebula-rose: #ff7ab8; /* hot pink */
|
||||
--nebula-green: #6ee7b0; /* aurora green */
|
||||
|
||||
/* ---- Semantic (callouts) ---- */
|
||||
--note-fg: #6ad9ff;
|
||||
--note-bg: rgba(106, 217, 255, 0.06);
|
||||
--note-border: rgba(106, 217, 255, 0.30);
|
||||
|
||||
--tip-fg: #6ee7b0;
|
||||
--tip-bg: rgba(110, 231, 176, 0.06);
|
||||
--tip-border: rgba(110, 231, 176, 0.30);
|
||||
|
||||
--warn-fg: #ffb574;
|
||||
--warn-bg: rgba(255, 181, 116, 0.06);
|
||||
--warn-border: rgba(255, 181, 116, 0.32);
|
||||
|
||||
--danger-fg: #ff7ab8;
|
||||
--danger-bg: rgba(255, 122, 184, 0.06);
|
||||
--danger-border:rgba(255, 122, 184, 0.32);
|
||||
|
||||
/* ---- Type ---- */
|
||||
--font-sans: "Inter Tight", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
|
||||
--fs-xs: 12px;
|
||||
--fs-sm: 13px;
|
||||
--fs-base: 15px;
|
||||
--fs-md: 16px;
|
||||
--fs-lg: 18px;
|
||||
--fs-xl: 22px;
|
||||
--fs-2xl: 28px;
|
||||
--fs-3xl: 36px;
|
||||
--fs-4xl: 48px;
|
||||
|
||||
/* ---- Spacing ---- */
|
||||
--space-1: 4px;
|
||||
--space-2x: 8px;
|
||||
--space-3x: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
|
||||
/* ---- Radii ---- */
|
||||
--r-sm: 4px;
|
||||
--r-md: 8px;
|
||||
--r-lg: 12px;
|
||||
--r-xl: 16px;
|
||||
--r-2xl: 20px;
|
||||
--r-pill: 999px;
|
||||
|
||||
/* ---- Shadows ---- */
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
||||
--shadow-md: 0 8px 24px -8px rgba(0,0,0,0.5);
|
||||
--shadow-lg: 0 24px 48px -16px rgba(0,0,0,0.6);
|
||||
--shadow-glow: 0 0 24px rgba(184, 107, 255, 0.25);
|
||||
|
||||
/* ---- Layout ---- */
|
||||
--nav-h: 60px;
|
||||
--sidebar-w: 280px;
|
||||
--toc-w: 240px;
|
||||
--content-max: 760px;
|
||||
}
|
||||
|
||||
/* ---- Light variant (parchment, retained from current docs) ---- */
|
||||
.theme-light {
|
||||
--space: #faf6ec;
|
||||
--space-2: #f3eedc;
|
||||
--space-3: #ece5cf;
|
||||
--plum: #ede4ff;
|
||||
--plum-2: #ddd0f5;
|
||||
|
||||
--star: #1a1330;
|
||||
--star-soft: #4a4360;
|
||||
--muted: #7a7390;
|
||||
--muted-2: #a39db5;
|
||||
|
||||
--line: rgba(26, 19, 48, 0.08);
|
||||
--line-strong: rgba(26, 19, 48, 0.16);
|
||||
--line-glow: rgba(122, 56, 208, 0.30);
|
||||
|
||||
--nebula-mag: #7a38d0;
|
||||
--nebula-mag-2: #9b5fee;
|
||||
--nebula-cyan: #1f8aaa;
|
||||
--nebula-amber: #c4731a;
|
||||
--nebula-rose: #c43e7b;
|
||||
--nebula-green: #2a8b5e;
|
||||
|
||||
--note-fg: #1f6f8a;
|
||||
--note-bg: rgba(31, 138, 170, 0.08);
|
||||
--note-border: rgba(31, 138, 170, 0.30);
|
||||
|
||||
--tip-fg: #2a8b5e;
|
||||
--tip-bg: rgba(42, 139, 94, 0.08);
|
||||
--tip-border: rgba(42, 139, 94, 0.30);
|
||||
|
||||
--warn-fg: #c4731a;
|
||||
--warn-bg: rgba(196, 115, 26, 0.08);
|
||||
--warn-border: rgba(196, 115, 26, 0.32);
|
||||
|
||||
--danger-fg: #c43e7b;
|
||||
--danger-bg: rgba(196, 62, 123, 0.08);
|
||||
--danger-border:rgba(196, 62, 123, 0.32);
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(26, 19, 48, 0.08);
|
||||
--shadow-md: 0 8px 24px -8px rgba(26, 19, 48, 0.12);
|
||||
--shadow-lg: 0 24px 48px -16px rgba(26, 19, 48, 0.16);
|
||||
--shadow-glow: 0 0 24px rgba(122, 56, 208, 0.18);
|
||||
}
|
||||
|
||||
/* ---- Base reset for in-frame components ---- */
|
||||
.docs-frame {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--space);
|
||||
color: var(--star);
|
||||
font-size: var(--fs-base);
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-feature-settings: "ss01", "cv11";
|
||||
}
|
||||
.docs-frame *,
|
||||
.docs-frame *::before,
|
||||
.docs-frame *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.docs-frame button {
|
||||
font-family: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
.docs-frame a {
|
||||
color: var(--nebula-mag);
|
||||
text-decoration: none;
|
||||
text-decoration-color: rgba(184, 107, 255, 0.4);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.docs-frame a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.docs-frame ::selection {
|
||||
background: rgba(184, 107, 255, 0.35);
|
||||
color: var(--star);
|
||||
}
|
||||
|
||||
/* Subtle galaxy texture for atmospheric backgrounds */
|
||||
.cosmic-bg {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cosmic-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 0%, rgba(184, 107, 255, 0.08), transparent 50%),
|
||||
radial-gradient(ellipse at 80% 100%, rgba(106, 217, 255, 0.05), transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.theme-light.cosmic-bg::before,
|
||||
.theme-light .cosmic-bg::before {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 0%, rgba(122, 56, 208, 0.05), transparent 50%),
|
||||
radial-gradient(ellipse at 80% 100%, rgba(31, 138, 170, 0.04), transparent 50%);
|
||||
}
|
||||
1240
docs/design-system/source/index-v2.html
Normal file
1240
docs/design-system/source/index-v2.html
Normal file
File diff suppressed because it is too large
Load Diff
108
docs/design-system/tailwind.preset.js
Normal file
108
docs/design-system/tailwind.preset.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Nibiru Design System — v4.0.0 Tailwind Preset
|
||||
*
|
||||
* // tailwind.config.js
|
||||
* module.exports = {
|
||||
* presets: [require('@nibiru/design-system/tailwind.preset')],
|
||||
* content: ['./src/**\/*.{html,js,ts,jsx,tsx,vue,astro}'],
|
||||
* };
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
nibiru: {
|
||||
void: '#06030f',
|
||||
space: '#0a0414',
|
||||
night: '#120825',
|
||||
plum: '#1c0f3a',
|
||||
'nebula-mag': '#b86bff',
|
||||
'nebula-blue': '#5b8dff',
|
||||
'nebula-amber': '#ffb574',
|
||||
iris: '#b86bff',
|
||||
'iris-deep': '#8a3fd0',
|
||||
'iris-soft': '#d4b4ff',
|
||||
skyfall: '#5b8dff',
|
||||
'skyfall-deep': '#3a6ad0',
|
||||
'skyfall-soft': '#a8c0ff',
|
||||
aurum: '#ffb574',
|
||||
star: '#f4eedb',
|
||||
paper: '#f4eedb',
|
||||
'paper-2': '#ebe3c8',
|
||||
mist: '#f8f3e2',
|
||||
ink: '#0a0414',
|
||||
'ink-2': '#2a2438',
|
||||
'ink-deep': '#06030f',
|
||||
'ink-faint': '#6e6680',
|
||||
muted: '#6e6680',
|
||||
success: '#7ad6a3',
|
||||
rose: '#ff8a9d',
|
||||
'code-bg': '#050208',
|
||||
'code-text': '#f4eedb',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
display: ['"Inter Tight"', 'ui-sans-serif', 'system-ui', 'sans-serif'],
|
||||
sans: ['"Inter Tight"', 'ui-sans-serif', 'system-ui', 'sans-serif'],
|
||||
mono: ['"JetBrains Mono"', 'ui-monospace', '"SF Mono"', 'Menlo', 'monospace'],
|
||||
},
|
||||
fontSize: {
|
||||
hero: ['clamp(3.5rem, 7vw + 0.5rem, 7.75rem)', { lineHeight: '0.95', letterSpacing: '-0.04em' }],
|
||||
section: ['clamp(2.5rem, 4vw + 0.5rem, 5rem)', { lineHeight: '1.02', letterSpacing: '-0.03em' }],
|
||||
},
|
||||
letterSpacing: {
|
||||
display: '-0.04em',
|
||||
heading: '-0.03em',
|
||||
body: '-0.005em',
|
||||
mono: '0.04em',
|
||||
label: '0.10em',
|
||||
eyebrow: '0.18em',
|
||||
},
|
||||
borderRadius: {
|
||||
sm: '6px',
|
||||
md: '10px',
|
||||
lg: '14px',
|
||||
xl: '18px',
|
||||
'2xl':'22px',
|
||||
pill: '999px',
|
||||
},
|
||||
boxShadow: {
|
||||
'nibiru-sm': '0 1px 2px rgba(0, 0, 0, 0.20)',
|
||||
'nibiru-md': '0 8px 24px -8px rgba(0, 0, 0, 0.40)',
|
||||
'nibiru-lg': '0 30px 60px -30px rgba(0, 0, 0, 0.7)',
|
||||
'nibiru-xl': '0 60px 120px -40px rgba(0, 0, 0, 0.7)',
|
||||
'nibiru-cosmos': '0 60px 120px -40px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(184, 107, 255, 0.06) inset',
|
||||
'nibiru-glow-mag': '0 0 24px rgba(184, 107, 255, 0.45)',
|
||||
'nibiru-glow-amb': '0 0 24px rgba(255, 181, 116, 0.45)',
|
||||
'nibiru-glow-blue':'0 0 24px rgba(91, 141, 255, 0.45)',
|
||||
},
|
||||
backgroundImage: {
|
||||
'iris-to-sky': 'linear-gradient(135deg, #b86bff 0%, #5b8dff 100%)',
|
||||
'headline': 'linear-gradient(110deg, #ffb574 0%, #b86bff 50%, #5b8dff 100%)',
|
||||
'accent': 'linear-gradient(110deg, #ffb574, #b86bff 70%)',
|
||||
'accent-light': 'linear-gradient(110deg, #b46500, #5b1f9e 70%)',
|
||||
'nebula': 'radial-gradient(120% 90% at 50% 20%, #2a2156 0%, #1a1442 24%, #0e0a2a 52%, #06050f 82%), linear-gradient(180deg, #06050f 0%, #07060f 100%)',
|
||||
'mmvc-stage': 'radial-gradient(ellipse at 50% 50%, #1c0f3a, #0a0414 60%)',
|
||||
},
|
||||
animation: {
|
||||
'tele-pulse': 'nibiru-tele-pulse 2.4s ease-in-out infinite',
|
||||
blink: 'nibiru-blink 1s steps(1) infinite',
|
||||
breathe: 'nibiru-breathe 18s ease-in-out infinite',
|
||||
},
|
||||
keyframes: {
|
||||
'nibiru-tele-pulse': {
|
||||
'0%, 100%': { opacity: '0.5', transform: 'scale(0.85)' },
|
||||
'50%': { opacity: '1', transform: 'scale(1.15)' },
|
||||
},
|
||||
'nibiru-blink': {
|
||||
'50%': { opacity: '0' },
|
||||
},
|
||||
'nibiru-breathe': {
|
||||
'0%, 100%': { transform: 'translateY(0) rotate(0deg)' },
|
||||
'50%': { transform: 'translateY(-6px) rotate(0.6deg)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
177
docs/design-system/tokens.css
Normal file
177
docs/design-system/tokens.css
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Nibiru Design System — v4.0.0 "Cosmos"
|
||||
*
|
||||
* Canonical source of truth for the brand. Imported globally by the site
|
||||
* (see astro.config.mjs `customCss`) and published verbatim at
|
||||
* https://nibiru-framework.com/design-system/tokens.css
|
||||
*
|
||||
* v4 is dark-first: the page lives in space, headlines glow magenta-amber,
|
||||
* the lotus mark is the star at the centre. Light "paper" surfaces are
|
||||
* available via .alt sections for content-heavy reading.
|
||||
*
|
||||
* All tokens namespaced --nibiru-* so they coexist with other systems.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* === Cosmos surfaces (dark-first page) === */
|
||||
--nibiru-void: #06030f; /* deepest, used for chat/code panes */
|
||||
--nibiru-space: #0a0414; /* the page background */
|
||||
--nibiru-night: #120825; /* card surface on dark */
|
||||
--nibiru-plum: #1c0f3a; /* MMVC stage glow base */
|
||||
|
||||
/* === Nebula brand colours (the petals, retuned) === */
|
||||
--nibiru-nebula-mag: #b86bff; /* primary brand — magenta */
|
||||
--nibiru-nebula-blue: #5b8dff;
|
||||
--nibiru-nebula-amber:#ffb574;
|
||||
|
||||
/* Legacy aliases — same role, mapped to new hexes */
|
||||
--nibiru-iris: #b86bff;
|
||||
--nibiru-iris-deep: #8a3fd0;
|
||||
--nibiru-iris-soft: #d4b4ff;
|
||||
--nibiru-skyfall: #5b8dff;
|
||||
--nibiru-skyfall-deep:#3a6ad0;
|
||||
--nibiru-skyfall-soft:#a8c0ff;
|
||||
--nibiru-aurum: #ffb574; /* warm amber, brighter than v3 */
|
||||
--nibiru-aurum-soft: #ffd0a3;
|
||||
|
||||
/* === Light "paper" surfaces (for .alt sections) === */
|
||||
--nibiru-star: #f4eedb; /* the cream — also used as text on dark */
|
||||
--nibiru-paper: #f4eedb;
|
||||
--nibiru-paper-2: #ebe3c8;
|
||||
--nibiru-bg: #f4eedb; /* legacy alias for content cards */
|
||||
--nibiru-bg-2: #ebe3c8;
|
||||
--nibiru-mist: #f8f3e2;
|
||||
--nibiru-lavender: #ece6f3;
|
||||
--nibiru-lavender-deep:#ddd3eb;
|
||||
|
||||
/* === Ink (text on light surfaces) === */
|
||||
--nibiru-ink: #0a0414; /* deeper than v3, matches space */
|
||||
--nibiru-ink-2: #2a2438;
|
||||
--nibiru-ink-deep: #06030f;
|
||||
--nibiru-ink-soft: #4a4258; /* legacy */
|
||||
--nibiru-ink-faint: #6e6680;
|
||||
--nibiru-muted: #6e6680;
|
||||
|
||||
/* === Hairlines === */
|
||||
--nibiru-line: rgba(244, 238, 219, 0.12); /* on dark surfaces */
|
||||
--nibiru-line-strong: rgba(244, 238, 219, 0.28); /* hover / focus */
|
||||
--nibiru-line-light: rgba(20, 4, 30, 0.10); /* on light surfaces */
|
||||
--nibiru-line-2: rgba(20, 4, 30, 0.18);
|
||||
--nibiru-grid: rgba(244, 238, 219, 0.04); /* faint grid overlay */
|
||||
|
||||
/* === Status / accents === */
|
||||
--nibiru-success: #7ad6a3;
|
||||
--nibiru-rose: #ff8a9d;
|
||||
--nibiru-moss: #94a96e;
|
||||
|
||||
/* === Code surface === */
|
||||
--nibiru-code-bg: #050208;
|
||||
--nibiru-code-line: rgba(244, 238, 219, 0.08);
|
||||
--nibiru-code-text: #f4eedb;
|
||||
--nibiru-code-mute: #6e6680;
|
||||
|
||||
/* === Twilight (legacy dark theme — kept for Starlight compatibility) === */
|
||||
--nibiru-dark-bg: #0a0414;
|
||||
--nibiru-dark-surface: #120825;
|
||||
--nibiru-dark-surface-raised:#1c0f3a;
|
||||
--nibiru-dark-ink: #f4eedb;
|
||||
--nibiru-dark-ink-soft: #c5bfd1;
|
||||
--nibiru-dark-ink-faint: #6e6680;
|
||||
|
||||
/* === Gradients === */
|
||||
--nibiru-gradient-headline:
|
||||
linear-gradient(110deg, #ffb574 0%, #b86bff 50%, #5b8dff 100%);
|
||||
--nibiru-gradient-accent:
|
||||
linear-gradient(110deg, #ffb574 0%, #b86bff 70%);
|
||||
--nibiru-gradient-accent-light:
|
||||
linear-gradient(110deg, #b46500 0%, #5b1f9e 70%);
|
||||
--nibiru-gradient-iris-to-sky:
|
||||
linear-gradient(135deg, #b86bff 0%, #5b8dff 100%);
|
||||
--nibiru-gradient-nebula:
|
||||
radial-gradient(120% 90% at 50% 20%, #2a2156 0%, #1a1442 24%, #0e0a2a 52%, #06050f 82%),
|
||||
linear-gradient(180deg, #06050f 0%, #07060f 100%);
|
||||
--nibiru-gradient-mmvc-stage:
|
||||
radial-gradient(ellipse at 50% 50%, #1c0f3a, #0a0414 60%);
|
||||
--nibiru-gradient-lotus-wash:
|
||||
radial-gradient(ellipse 80% 50% at 0% 0%, rgba(184, 107, 255, 0.10), transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 100% 0%, rgba(91, 141, 255, 0.10), transparent 60%);
|
||||
|
||||
/* === Shadows === */
|
||||
--nibiru-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.20);
|
||||
--nibiru-shadow-md: 0 8px 24px -8px rgba(0, 0, 0, 0.40);
|
||||
--nibiru-shadow-lg: 0 30px 60px -30px rgba(0, 0, 0, 0.7);
|
||||
--nibiru-shadow-xl: 0 60px 120px -40px rgba(0, 0, 0, 0.7);
|
||||
--nibiru-shadow-cosmos: 0 60px 120px -40px rgba(0, 0, 0, 0.7),
|
||||
0 0 0 1px rgba(184, 107, 255, 0.06) inset;
|
||||
--nibiru-shadow-glow-mag: 0 0 24px rgba(184, 107, 255, 0.45);
|
||||
--nibiru-shadow-glow-amb: 0 0 24px rgba(255, 181, 116, 0.45);
|
||||
--nibiru-shadow-glow-blue:0 0 24px rgba(91, 141, 255, 0.45);
|
||||
|
||||
/* === Typography === */
|
||||
--nibiru-font-text: 'Inter Tight', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
--nibiru-font-display: 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
|
||||
--nibiru-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
|
||||
/* Legacy alias — the v3 components reference --nibiru-font-text */
|
||||
--nibiru-font-body: var(--nibiru-font-text);
|
||||
|
||||
/* Weights */
|
||||
--nibiru-weight-light: 300;
|
||||
--nibiru-weight-regular: 400;
|
||||
--nibiru-weight-medium: 500;
|
||||
--nibiru-weight-semibold: 600;
|
||||
--nibiru-weight-bold: 700;
|
||||
|
||||
/* Letter-spacing */
|
||||
--nibiru-tracking-display: -0.04em;
|
||||
--nibiru-tracking-heading: -0.03em;
|
||||
--nibiru-tracking-body: -0.005em;
|
||||
--nibiru-tracking-mono: 0.04em;
|
||||
--nibiru-tracking-label: 0.10em;
|
||||
--nibiru-tracking-eyebrow: 0.18em;
|
||||
|
||||
/* Type scale */
|
||||
--nibiru-text-xs: 0.69rem; /* 11px */
|
||||
--nibiru-text-sm: 0.81rem; /* 13px */
|
||||
--nibiru-text-md: 0.875rem; /* 14px */
|
||||
--nibiru-text-base: 1rem; /* 16px — body */
|
||||
--nibiru-text-lg: 1.125rem; /* 18px */
|
||||
--nibiru-text-xl: 1.25rem; /* 20px */
|
||||
--nibiru-text-2xl: 1.5rem;
|
||||
--nibiru-text-3xl: 2rem;
|
||||
--nibiru-text-section: clamp(2.5rem, 4vw + 0.5rem, 5rem); /* 40-80 */
|
||||
--nibiru-text-hero: clamp(3.5rem, 7vw + 0.5rem, 7.75rem); /* 56-124 */
|
||||
|
||||
/* === Radii === */
|
||||
--nibiru-radius-sm: 6px;
|
||||
--nibiru-radius-md: 10px;
|
||||
--nibiru-radius-lg: 14px;
|
||||
--nibiru-radius-xl: 18px;
|
||||
--nibiru-radius-2xl: 22px;
|
||||
--nibiru-radius-pill: 999px;
|
||||
|
||||
/* === Spacing === */
|
||||
--nibiru-space-0: 0;
|
||||
--nibiru-space-1: 0.25rem;
|
||||
--nibiru-space-2: 0.5rem;
|
||||
--nibiru-space-3: 0.75rem;
|
||||
--nibiru-space-4: 1rem;
|
||||
--nibiru-space-5: 1.5rem;
|
||||
--nibiru-space-6: 2rem;
|
||||
--nibiru-space-8: 3rem;
|
||||
--nibiru-space-10: 4rem;
|
||||
--nibiru-space-12: 6rem;
|
||||
--nibiru-space-section: 10rem;
|
||||
--nibiru-container: 1280px;
|
||||
--nibiru-content: 50rem;
|
||||
|
||||
/* === Motion === */
|
||||
--nibiru-duration-fast: 160ms;
|
||||
--nibiru-duration-normal: 240ms;
|
||||
--nibiru-duration-slow: 500ms;
|
||||
--nibiru-duration-pulse: 2.4s;
|
||||
--nibiru-duration-blink: 1s;
|
||||
--nibiru-duration-breathe: 18s;
|
||||
--nibiru-ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
|
||||
--nibiru-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
153
docs/design-system/tokens.json
Normal file
153
docs/design-system/tokens.json
Normal file
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"$schema": "https://design-tokens.github.io/community-group/format/",
|
||||
"name": "Nibiru Design System",
|
||||
"version": "4.0.0",
|
||||
"description": "Cosmos — dark-first AI-framework brand. Magenta-amber-blue nebula on a deep-space body, all-sans (Inter Tight body + display) + JetBrains Mono. Light paper surfaces available via .alt sections for content-heavy reading.",
|
||||
|
||||
"color": {
|
||||
"void": { "$value": "#06030f", "$type": "color", "$description": "Deepest. Used for chat / code panes." },
|
||||
"space": { "$value": "#0a0414", "$type": "color", "$description": "Page background. The site lives here." },
|
||||
"night": { "$value": "#120825", "$type": "color", "$description": "Card / panel surface on dark." },
|
||||
"plum": { "$value": "#1c0f3a", "$type": "color", "$description": "MMVC-stage glow base." },
|
||||
|
||||
"nebulaMag": { "$value": "#b86bff", "$type": "color", "$description": "Primary brand — electric magenta. Replaces v3 iris." },
|
||||
"nebulaBlue": { "$value": "#5b8dff", "$type": "color", "$description": "Cool counterpoint. Replaces v3 skyfall." },
|
||||
"nebulaAmber": { "$value": "#ffb574", "$type": "color", "$description": "Warm accent. Replaces v3 aurum." },
|
||||
|
||||
"iris": { "$value": "#b86bff", "$type": "color" },
|
||||
"irisDeep": { "$value": "#8a3fd0", "$type": "color" },
|
||||
"irisSoft": { "$value": "#d4b4ff", "$type": "color" },
|
||||
"skyfall": { "$value": "#5b8dff", "$type": "color" },
|
||||
"skyfallDeep": { "$value": "#3a6ad0", "$type": "color" },
|
||||
"skyfallSoft": { "$value": "#a8c0ff", "$type": "color" },
|
||||
"aurum": { "$value": "#ffb574", "$type": "color" },
|
||||
|
||||
"star": { "$value": "#f4eedb", "$type": "color", "$description": "Body text on dark surfaces. Also used as light paper." },
|
||||
"paper": { "$value": "#f4eedb", "$type": "color", "$description": "Light surface for .alt sections." },
|
||||
"paper2": { "$value": "#ebe3c8", "$type": "color" },
|
||||
"mist": { "$value": "#f8f3e2", "$type": "color" },
|
||||
"lavender": { "$value": "#ece6f3", "$type": "color" },
|
||||
"lavenderDeep": { "$value": "#ddd3eb", "$type": "color" },
|
||||
|
||||
"ink": { "$value": "#0a0414", "$type": "color", "$description": "Text on light. Same as space — deliberate." },
|
||||
"ink2": { "$value": "#2a2438", "$type": "color" },
|
||||
"inkDeep": { "$value": "#06030f", "$type": "color" },
|
||||
"inkFaint": { "$value": "#6e6680", "$type": "color" },
|
||||
"muted": { "$value": "#6e6680", "$type": "color" },
|
||||
|
||||
"success": { "$value": "#7ad6a3", "$type": "color" },
|
||||
"rose": { "$value": "#ff8a9d", "$type": "color" },
|
||||
"moss": { "$value": "#94a96e", "$type": "color" }
|
||||
},
|
||||
|
||||
"line": {
|
||||
"default": { "$value": "rgba(244, 238, 219, 0.12)", "$type": "color", "$description": "Hairline on dark." },
|
||||
"strong": { "$value": "rgba(244, 238, 219, 0.28)", "$type": "color", "$description": "Hover / focus on dark." },
|
||||
"light": { "$value": "rgba(20, 4, 30, 0.10)", "$type": "color", "$description": "Hairline on light." },
|
||||
"lightStrong": { "$value": "rgba(20, 4, 30, 0.18)", "$type": "color" },
|
||||
"grid": { "$value": "rgba(244, 238, 219, 0.04)", "$type": "color", "$description": "Faint grid overlay." }
|
||||
},
|
||||
|
||||
"code": {
|
||||
"bg": { "$value": "#050208", "$type": "color" },
|
||||
"line": { "$value": "rgba(244, 238, 219, 0.08)", "$type": "color" },
|
||||
"text": { "$value": "#f4eedb", "$type": "color" },
|
||||
"mute": { "$value": "#6e6680", "$type": "color" }
|
||||
},
|
||||
|
||||
"gradient": {
|
||||
"headline": { "$value": "linear-gradient(110deg, #ffb574 0%, #b86bff 50%, #5b8dff 100%)", "$type": "gradient", "$description": "Hero accent <em> — amber→magenta→blue." },
|
||||
"accent": { "$value": "linear-gradient(110deg, #ffb574, #b86bff 70%)", "$type": "gradient", "$description": "Section title accent on dark." },
|
||||
"accentLight": { "$value": "linear-gradient(110deg, #b46500, #5b1f9e 70%)", "$type": "gradient", "$description": "Section title accent on light (.alt)." },
|
||||
"irisToSky": { "$value": "linear-gradient(135deg, #b86bff 0%, #5b8dff 100%)", "$type": "gradient" },
|
||||
"nebula": { "$value": "radial-gradient(120% 90% at 50% 20%, #2a2156 0%, #1a1442 24%, #0e0a2a 52%, #06050f 82%), linear-gradient(180deg, #06050f 0%, #07060f 100%)", "$type": "gradient" },
|
||||
"mmvcStage": { "$value": "radial-gradient(ellipse at 50% 50%, #1c0f3a, #0a0414 60%)", "$type": "gradient" },
|
||||
"lotusWash": { "$value": "radial-gradient(ellipse 80% 50% at 0% 0%, rgba(184,107,255,0.10), transparent 60%), radial-gradient(ellipse 60% 40% at 100% 0%, rgba(91,141,255,0.10), transparent 60%)", "$type": "gradient" }
|
||||
},
|
||||
|
||||
"shadow": {
|
||||
"sm": { "$value": "0 1px 2px rgba(0, 0, 0, 0.20)", "$type": "shadow" },
|
||||
"md": { "$value": "0 8px 24px -8px rgba(0, 0, 0, 0.40)", "$type": "shadow" },
|
||||
"lg": { "$value": "0 30px 60px -30px rgba(0, 0, 0, 0.7)", "$type": "shadow" },
|
||||
"xl": { "$value": "0 60px 120px -40px rgba(0, 0, 0, 0.7)", "$type": "shadow" },
|
||||
"cosmos": { "$value": "0 60px 120px -40px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(184, 107, 255, 0.06) inset", "$type": "shadow" },
|
||||
"glowMag": { "$value": "0 0 24px rgba(184, 107, 255, 0.45)", "$type": "shadow" },
|
||||
"glowAmber":{ "$value": "0 0 24px rgba(255, 181, 116, 0.45)", "$type": "shadow" },
|
||||
"glowBlue": { "$value": "0 0 24px rgba(91, 141, 255, 0.45)", "$type": "shadow" }
|
||||
},
|
||||
|
||||
"font": {
|
||||
"text": { "$value": "'Inter Tight', ui-sans-serif, system-ui, -apple-system, sans-serif", "$type": "fontFamily" },
|
||||
"display": { "$value": "'Inter Tight', ui-sans-serif, system-ui, sans-serif", "$type": "fontFamily" },
|
||||
"mono": { "$value": "'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace", "$type": "fontFamily" }
|
||||
},
|
||||
|
||||
"weight": {
|
||||
"light": { "$value": 300, "$type": "fontWeight" },
|
||||
"regular": { "$value": 400, "$type": "fontWeight" },
|
||||
"medium": { "$value": 500, "$type": "fontWeight" },
|
||||
"semibold": { "$value": 600, "$type": "fontWeight" },
|
||||
"bold": { "$value": 700, "$type": "fontWeight" }
|
||||
},
|
||||
|
||||
"letterSpacing": {
|
||||
"display": { "$value": "-0.04em", "$type": "dimension" },
|
||||
"heading": { "$value": "-0.03em", "$type": "dimension" },
|
||||
"body": { "$value": "-0.005em", "$type": "dimension" },
|
||||
"mono": { "$value": "0.04em", "$type": "dimension" },
|
||||
"label": { "$value": "0.10em", "$type": "dimension" },
|
||||
"eyebrow": { "$value": "0.18em", "$type": "dimension" }
|
||||
},
|
||||
|
||||
"fontSize": {
|
||||
"xs": { "$value": "0.69rem", "$type": "dimension" },
|
||||
"sm": { "$value": "0.81rem", "$type": "dimension" },
|
||||
"md": { "$value": "0.875rem", "$type": "dimension" },
|
||||
"base": { "$value": "1rem", "$type": "dimension" },
|
||||
"lg": { "$value": "1.125rem", "$type": "dimension" },
|
||||
"xl": { "$value": "1.25rem", "$type": "dimension" },
|
||||
"2xl": { "$value": "1.5rem", "$type": "dimension" },
|
||||
"3xl": { "$value": "2rem", "$type": "dimension" },
|
||||
"section": { "$value": "clamp(2.5rem, 4vw + 0.5rem, 5rem)", "$type": "dimension" },
|
||||
"hero": { "$value": "clamp(3.5rem, 7vw + 0.5rem, 7.75rem)", "$type": "dimension" }
|
||||
},
|
||||
|
||||
"radius": {
|
||||
"sm": { "$value": "6px", "$type": "dimension" },
|
||||
"md": { "$value": "10px", "$type": "dimension" },
|
||||
"lg": { "$value": "14px", "$type": "dimension" },
|
||||
"xl": { "$value": "18px", "$type": "dimension" },
|
||||
"2xl": { "$value": "22px", "$type": "dimension" },
|
||||
"pill": { "$value": "999px", "$type": "dimension" }
|
||||
},
|
||||
|
||||
"space": {
|
||||
"0": { "$value": "0", "$type": "dimension" },
|
||||
"1": { "$value": "0.25rem", "$type": "dimension" },
|
||||
"2": { "$value": "0.5rem", "$type": "dimension" },
|
||||
"3": { "$value": "0.75rem", "$type": "dimension" },
|
||||
"4": { "$value": "1rem", "$type": "dimension" },
|
||||
"5": { "$value": "1.5rem", "$type": "dimension" },
|
||||
"6": { "$value": "2rem", "$type": "dimension" },
|
||||
"8": { "$value": "3rem", "$type": "dimension" },
|
||||
"10": { "$value": "4rem", "$type": "dimension" },
|
||||
"12": { "$value": "6rem", "$type": "dimension" },
|
||||
"section": { "$value": "10rem", "$type": "dimension" },
|
||||
"container":{ "$value": "1280px", "$type": "dimension" },
|
||||
"content": { "$value": "50rem", "$type": "dimension" }
|
||||
},
|
||||
|
||||
"motion": {
|
||||
"fast": { "$value": "160ms", "$type": "duration" },
|
||||
"normal": { "$value": "240ms", "$type": "duration" },
|
||||
"slow": { "$value": "500ms", "$type": "duration" },
|
||||
"pulse": { "$value": "2.4s", "$type": "duration" },
|
||||
"blink": { "$value": "1s", "$type": "duration" },
|
||||
"breathe": { "$value": "18s", "$type": "duration" }
|
||||
},
|
||||
|
||||
"ease": {
|
||||
"out": { "$value": "cubic-bezier(0.2, 0.7, 0.2, 1)", "$type": "cubicBezier" },
|
||||
"spring": { "$value": "cubic-bezier(0.34, 1.56, 0.64, 1)", "$type": "cubicBezier" }
|
||||
}
|
||||
}
|
||||
120
docs/design-system/tokens.scss
Normal file
120
docs/design-system/tokens.scss
Normal file
@@ -0,0 +1,120 @@
|
||||
// Nibiru Design System — v4.0.0 "Cosmos" (SCSS)
|
||||
|
||||
// === Cosmos surfaces ===
|
||||
$void: #06030f;
|
||||
$space: #0a0414;
|
||||
$night: #120825;
|
||||
$plum: #1c0f3a;
|
||||
|
||||
// === Nebula brand ===
|
||||
$nebula-mag: #b86bff;
|
||||
$nebula-blue: #5b8dff;
|
||||
$nebula-amber: #ffb574;
|
||||
|
||||
// Legacy aliases retuned to v4
|
||||
$iris: #b86bff;
|
||||
$iris-deep: #8a3fd0;
|
||||
$iris-soft: #d4b4ff;
|
||||
$skyfall: #5b8dff;
|
||||
$skyfall-deep: #3a6ad0;
|
||||
$skyfall-soft: #a8c0ff;
|
||||
$aurum: #ffb574;
|
||||
|
||||
// === Light surfaces ===
|
||||
$star: #f4eedb;
|
||||
$paper: #f4eedb;
|
||||
$paper-2: #ebe3c8;
|
||||
$bg: #f4eedb;
|
||||
$bg-2: #ebe3c8;
|
||||
$mist: #f8f3e2;
|
||||
$lavender: #ece6f3;
|
||||
$lavender-deep:#ddd3eb;
|
||||
|
||||
// === Ink ===
|
||||
$ink: #0a0414;
|
||||
$ink-2: #2a2438;
|
||||
$ink-deep: #06030f;
|
||||
$ink-soft: #4a4258;
|
||||
$ink-faint: #6e6680;
|
||||
$muted: #6e6680;
|
||||
|
||||
// === Hairlines ===
|
||||
$line: rgba(244, 238, 219, 0.12);
|
||||
$line-strong: rgba(244, 238, 219, 0.28);
|
||||
$line-light: rgba(20, 4, 30, 0.10);
|
||||
$line-2: rgba(20, 4, 30, 0.18);
|
||||
$grid: rgba(244, 238, 219, 0.04);
|
||||
|
||||
// === Status ===
|
||||
$success: #7ad6a3;
|
||||
$rose: #ff8a9d;
|
||||
$moss: #94a96e;
|
||||
|
||||
// === Code surface ===
|
||||
$code-bg: #050208;
|
||||
$code-line: rgba(244, 238, 219, 0.08);
|
||||
$code-text: #f4eedb;
|
||||
$code-mute: #6e6680;
|
||||
|
||||
// === Twilight ===
|
||||
$dark-bg: #0a0414;
|
||||
$dark-surface: #120825;
|
||||
$dark-surface-raised: #1c0f3a;
|
||||
|
||||
// === Typography ===
|
||||
$font-text: ('Inter Tight', ui-sans-serif, system-ui, -apple-system, sans-serif);
|
||||
$font-display: ('Inter Tight', ui-sans-serif, system-ui, sans-serif);
|
||||
$font-mono: ('JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace);
|
||||
|
||||
$weight: (
|
||||
light: 300, regular: 400, medium: 500, semibold: 600, bold: 700,
|
||||
);
|
||||
$tracking: (
|
||||
display: -0.04em, heading: -0.03em, body: -0.005em,
|
||||
mono: 0.04em, label: 0.10em, eyebrow: 0.18em,
|
||||
);
|
||||
$text: (
|
||||
xs: 0.69rem,
|
||||
sm: 0.81rem,
|
||||
md: 0.875rem,
|
||||
base: 1rem,
|
||||
lg: 1.125rem,
|
||||
xl: 1.25rem,
|
||||
2xl: 1.5rem,
|
||||
3xl: 2rem,
|
||||
section: clamp(2.5rem, 4vw + 0.5rem, 5rem),
|
||||
hero: clamp(3.5rem, 7vw + 0.5rem, 7.75rem),
|
||||
);
|
||||
|
||||
// === Radii ===
|
||||
$radius: (
|
||||
sm: 6px, md: 10px, lg: 14px, xl: 18px, 2xl: 22px, pill: 999px,
|
||||
);
|
||||
|
||||
// === Spacing ===
|
||||
$space-scale: (
|
||||
0: 0, 1: 0.25rem, 2: 0.5rem, 3: 0.75rem, 4: 1rem,
|
||||
5: 1.5rem, 6: 2rem, 8: 3rem, 10: 4rem, 12: 6rem,
|
||||
section: 10rem,
|
||||
);
|
||||
|
||||
// === Motion ===
|
||||
$duration: (
|
||||
fast: 160ms, normal: 240ms, slow: 500ms,
|
||||
pulse: 2.4s, blink: 1s, breathe: 18s,
|
||||
);
|
||||
$ease: (
|
||||
out: cubic-bezier(0.2, 0.7, 0.2, 1),
|
||||
spring: cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
);
|
||||
|
||||
// === Shadows ===
|
||||
$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.20);
|
||||
$shadow-md: 0 8px 24px -8px rgba(0, 0, 0, 0.40);
|
||||
$shadow-lg: 0 30px 60px -30px rgba(0, 0, 0, 0.7);
|
||||
$shadow-xl: 0 60px 120px -40px rgba(0, 0, 0, 0.7);
|
||||
$shadow-cosmos: 0 60px 120px -40px rgba(0, 0, 0, 0.7),
|
||||
0 0 0 1px rgba(184, 107, 255, 0.06) inset;
|
||||
$shadow-glow-mag: 0 0 24px rgba(184, 107, 255, 0.45);
|
||||
$shadow-glow-amb: 0 0 24px rgba(255, 181, 116, 0.45);
|
||||
$shadow-glow-blue: 0 0 24px rgba(91, 141, 255, 0.45);
|
||||
Reference in New Issue
Block a user