diff --git a/.impeccable/design.json b/.impeccable/design.json index e397cf82..d60ee19d 100644 --- a/.impeccable/design.json +++ b/.impeccable/design.json @@ -1,53 +1,53 @@ { "schemaVersion": 2, - "generatedAt": "2026-05-04T00:00:00Z", + "generatedAt": "2026-08-11T08:18:00Z", "title": "Design System: Orva", "extensions": { "colorMeta": { "background": { "role": "neutral", - "displayName": "Inkwell Violet", - "canonical": "oklch(13% 0.029 277)", + "displayName": "Graphite Canvas", + "canonical": "#0B0D10", "tonalRamp": [ - "oklch(8% 0.020 277)", - "oklch(13% 0.029 277)", - "oklch(18% 0.035 277)", - "oklch(24% 0.042 277)", - "oklch(34% 0.040 277)", - "oklch(48% 0.035 277)", - "oklch(64% 0.025 277)", - "oklch(82% 0.012 277)" + "#07090B", + "#0B0D10", + "#15181D", + "#23272E", + "#343A44", + "#59616D", + "#89919C", + "#C5CAD1" ] }, "surface": { "role": "neutral", - "displayName": "Surface Violet", - "canonical": "oklch(18% 0.035 277)", + "displayName": "Graphite Surface", + "canonical": "#15181D", "tonalRamp": [ - "oklch(10% 0.025 277)", - "oklch(18% 0.035 277)", - "oklch(24% 0.042 277)", - "oklch(32% 0.045 277)", - "oklch(42% 0.045 277)", - "oklch(56% 0.040 277)", - "oklch(72% 0.025 277)", - "oklch(88% 0.010 277)" + "#0B0D10", + "#15181D", + "#23272E", + "#343A44", + "#4A515C", + "#6C7480", + "#9DA4AE", + "#D9DCE1" ] }, "surface-hover": { "role": "neutral", - "displayName": "Active Violet", - "canonical": "oklch(24% 0.042 277)" + "displayName": "Graphite Hover", + "canonical": "#23272E" }, "border": { "role": "neutral", - "displayName": "Border Violet", - "canonical": "oklch(28% 0.045 277)" + "displayName": "Graphite Border", + "canonical": "#343A44" }, "foreground": { "role": "neutral", "displayName": "Page Foreground", - "canonical": "oklch(96% 0.010 290)", + "canonical": "#F4F6F8", "tonalRamp": [ "oklch(20% 0.005 290)", "oklch(34% 0.008 290)", @@ -61,8 +61,8 @@ }, "foreground-muted": { "role": "neutral", - "displayName": "Cool Gray-Violet", - "canonical": "oklch(70% 0.018 280)" + "displayName": "Cool Light Gray", + "canonical": "#ADB4BE" }, "primary": { "role": "primary", @@ -87,7 +87,7 @@ "secondary": { "role": "neutral", "displayName": "Secondary Surface", - "canonical": "oklch(28% 0.045 277)" + "canonical": "#282E36" }, "danger": { "role": "danger", @@ -176,7 +176,7 @@ "refersTo": "chip-active", "description": "Currently-selected filter pill on Jobs / Webhooks / CronJobs status strips.", "html": "", - "css": ".ds-chip { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: 6px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-weight: 500; line-height: 1; background: var(--color-surface, #1A1929); color: var(--color-foreground-muted, #A3A3B3); border: 1px solid var(--color-border, #2D2B42); transition: color 150ms ease-out, border-color 150ms ease-out, background-color 150ms ease-out; cursor: pointer; } .ds-chip:hover { color: #FFFFFF; border-color: var(--color-foreground-muted, #A3A3B3); } .ds-chip-active { background: var(--color-primary, #553F83); color: var(--color-primary-foreground, #FFFFFF); border-color: var(--color-primary, #553F83); }" + "css": ".ds-chip { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: 6px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-weight: 500; line-height: 1; background: var(--color-surface, #15181D); color: var(--color-foreground-muted, #ADB4BE); border: 1px solid var(--color-border, #343A44); transition: color 150ms ease-out, border-color 150ms ease-out, background-color 150ms ease-out; cursor: pointer; } .ds-chip:hover { color: #F4F6F8; border-color: var(--color-foreground-muted, #ADB4BE); } .ds-chip-active { background: var(--color-primary, #553F83); color: var(--color-primary-foreground, #FFFFFF); border-color: var(--color-primary, #553F83); }" }, { "name": "Status Pill (success)", @@ -190,9 +190,9 @@ "name": "Page Card", "kind": "card", "refersTo": "card", - "description": "Dashboard tile / panel. Sits one step deeper than the page surface to read as a content well.", - "html": "
Response time
How long calls take to come back. p99 is the worst-case 1-in-100.
42 ms
", - "css": ".ds-card { background: var(--color-background, #12111C); border: 1px solid var(--color-border, #2D2B42); border-radius: 8px; padding: 20px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; } .ds-card-caption { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #FFFFFF; } .ds-card-hint { margin-top: 4px; font-size: 0.6875rem; color: var(--color-foreground-muted, #A3A3B3); line-height: 1.55; } .ds-card-value { margin-top: 16px; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 1.5rem; font-weight: 500; color: #FFFFFF; } .ds-card-unit { font-size: 0.875rem; color: var(--color-foreground-muted, #A3A3B3); margin-left: 4px; }" + "description": "Dashboard tile or panel. Shows decision-ready content first; optional context appears only when the metric is unfamiliar.", + "html": "

Response time

42 ms
", + "css": ".ds-card { background: var(--color-background, #0B0D10); border: 1px solid var(--color-border, #343A44); border-radius: 8px; padding: 20px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; } .ds-card-title { margin: 0; font-size: 0.875rem; font-weight: 600; color: #F4F6F8; } .ds-card-value { margin-top: 16px; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 1.5rem; font-weight: 500; color: #F4F6F8; } .ds-card-unit { font-size: 0.875rem; color: var(--color-foreground-muted, #ADB4BE); margin-left: 4px; }" }, { "name": "Text Input", @@ -200,37 +200,38 @@ "refersTo": "input", "description": "Default form field. Sits a step deeper than the surrounding card so the eye reads 'this is where you type'.", "html": "", - "css": ".ds-input-wrap { display: flex; flex-direction: column; gap: 6px; } .ds-input-label { font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-foreground-muted, #A3A3B3); } .ds-input { height: 36px; padding: 0 12px; background: var(--color-background, #12111C); color: #FFFFFF; border: 1px solid var(--color-border, #2D2B42); border-radius: 6px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; line-height: 1; transition: border-color 150ms ease-out, box-shadow 150ms ease-out; outline: none; } .ds-input::placeholder { color: rgba(163,163,179,0.5); } .ds-input:focus { border-color: #FFFFFF; box-shadow: inset 0 0 0 1px #FFFFFF; }" + "css": ".ds-input-wrap { display: flex; flex-direction: column; gap: 6px; } .ds-input-label { font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-foreground-muted, #ADB4BE); } .ds-input { height: 36px; padding: 0 12px; background: var(--color-background, #0B0D10); color: #F4F6F8; border: 1px solid var(--color-border, #343A44); border-radius: 6px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; line-height: 1; transition: border-color 150ms ease-out, box-shadow 150ms ease-out; outline: none; } .ds-input::placeholder { color: rgba(173,180,190,0.5); } .ds-input:focus { border-color: #FFFFFF; box-shadow: inset 0 0 0 1px #FFFFFF; }" }, { - "name": "Sidebar Item (active)", + "name": "Sidebar Group and Active Item", "kind": "nav", "refersTo": "sidebar-item", - "description": "Currently-routed nav item in the dashboard sidebar. Anchors 'you are here' with hue-matched soft shadow.", - "html": "Functions", - "css": ".ds-nav-item { display: inline-flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 6px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; font-weight: 500; color: var(--color-foreground-muted, #A3A3B3); text-decoration: none; transition: color 150ms ease-out, background-color 150ms ease-out; } .ds-nav-item:hover { color: #FFFFFF; background: var(--color-surface-hover, #252438); } .ds-nav-icon { width: 16px; height: 16px; } .ds-nav-active { color: #FFFFFF; background: var(--color-primary, #553F83); box-shadow: 0 10px 15px -3px rgba(38,18,87,0.20); } .ds-nav-active:hover { color: #FFFFFF; background: var(--color-primary, #553F83); }" + "description": "Primary routes stay visible; lower-frequency routes sit in collapsible groups. The current route remains visually anchored.", + "html": "
ObserveActivityInvocations
", + "css": ".ds-nav-item { display: inline-flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 6px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; font-weight: 500; color: var(--color-foreground-muted, #ADB4BE); text-decoration: none; transition: color 150ms ease-out, background-color 150ms ease-out; } .ds-nav-item:hover { color: #F4F6F8; background: var(--color-surface-hover, #23272E); } .ds-nav-icon { width: 16px; height: 16px; } .ds-nav-active { color: #FFFFFF; background: var(--color-primary, #553F83); box-shadow: 0 10px 15px -3px rgba(38,18,87,0.20); } .ds-nav-active:hover { color: #FFFFFF; background: var(--color-primary, #553F83); }" } ], "narrative": { "northStar": "The Late-Night Lab Notebook", - "overview": "Orva is the control plane for someone running a function platform on their own machine. Probably in a dim home office, often after-hours, more curious than time-pressured. The interface earns trust by behaving like a serious infrastructure tool: dense, unfussy, comfortable with code, willing to show numbers without dressing them up. The palette is the deep purple-tinted near-black of an editor at midnight, paired with a single muted violet accent that carries every CTA. JetBrains Mono runs alongside Inter so paths, IDs, byte counts, and CPU readings sit in their natural register without arguing for attention.", + "overview": "Orva is the control plane for someone running a function platform on their own machine. Probably in a dim home office, often after-hours, more curious than time-pressured. The interface earns trust by behaving like a serious infrastructure tool: dense, unfussy, comfortable with code, willing to show numbers without dressing them up. The palette is neutral graphite-black with soft light-gray text, paired with a single muted violet accent for action and selection. JetBrains Mono runs alongside Inter so paths, IDs, byte counts, and CPU readings sit in their natural register without arguing for attention.", "keyCharacteristics": [ - "Deep violet-tinted dark surfaces; one muted purple accent carries identity.", + "Neutral graphite-black surfaces; one muted violet accent carries identity.", "Inter for prose, JetBrains Mono for any data the operator might compare digit-to-digit.", "Standardised page heads: text-xl semibold tracking-tight over a one-line subhead in muted text, max-w-prose.", - "Hidden scrollbars by intent: nested overflow areas (modals, drawers, code blocks) feel calmer without 6px tracks in every region.", + "Thin, low-contrast scrollbars keep nested overflow discoverable without dominating the surface.", "Status colour is semantic, never decorative.", "No copy uses em dashes." ], "rules": [ { "name": "The One Accent Rule", "section": "colors", "body": "Muted violet appears on roughly 5 to 10 percent of any given screen. Primary CTAs, the active sidebar item, the selected filter chip. Anywhere else, ask whether the page actually needs an accent or whether a border + foreground-muted will read more confidently." }, - { "name": "The Tinted Neutral Rule", "section": "colors", "body": "Pure #000 and #FFF are forbidden. Every neutral carries the violet hue with chroma in the 0.005 to 0.045 band." }, + { "name": "The Graphite Neutral Rule", "section": "colors", "body": "Canvas and elevated surfaces stay neutral graphite. Violet communicates action, selection, and focus; routine text uses soft light gray." }, { "name": "The Semantic Status Rule", "section": "colors", "body": "Status colour is reserved for status. Use the semantic tokens (success, warning, danger) and their /15 tint backgrounds and /30 borders. Reaching for raw Tailwind palette colours is forbidden." }, { "name": "The Operator's Mono Rule", "section": "typography", "body": "If two characters at the same column should be visually compared (a UUID against another UUID, a memory reading against the limit), they are mono." }, { "name": "The No-Em-Dash Rule", "section": "typography", "body": "Em dashes are forbidden in any user-facing string. Use periods, commas, colons, semicolons, or parentheses." }, { "name": "The Heading Hierarchy Rule", "section": "typography", "body": "Every page has exactly one H1 (the standardised page header). Section captions inside the page must be real H2 / H3 in markup, even when styled as small uppercase tracked labels." }, { "name": "The Flat-By-Default Rule", "section": "elevation", "body": "Cards, list items, drawers, and inline panels carry no shadow at rest. Depth is read from borders and the background -> surface -> surface-hover ladder." }, - { "name": "The Earned Blur Rule", "section": "elevation", "body": "backdrop-blur exists only on the dimmed background that sits behind a modal or drawer. It is not decoration." } + { "name": "The Earned Blur Rule", "section": "elevation", "body": "backdrop-blur exists only on the dimmed background that sits behind a modal or drawer. It is not decoration." }, + { "name": "The Progressive Disclosure Rule", "section": "components", "body": "Keep decision-ready state visible. Place setup forms, implementation metadata, and advanced detail behind native disclosure controls." } ], "dos": [ "Do keep page heads to one line above one body subhead, both standardised: text-xl font-semibold text-white tracking-tight over text-sm text-foreground-muted mt-1.5 max-w-prose leading-relaxed.", @@ -239,13 +240,14 @@ "Do promote section captions inside cards to real h2 / h3 while keeping the text-xs font-bold uppercase tracking-wider styling.", "Do reach for bg-background border border-border for cards-on-page and bg-surface border border-border for cards-on-deeper-surface.", "Do add aria-label to every icon-only button.", - "Do keep CTAs on roughly 5 to 10 percent of any screen." + "Do keep CTAs on roughly 5 to 10 percent of any screen.", + "Do keep the decision-ready state visible and disclose setup forms, implementation metadata, and advanced detail on demand." ], "donts": [ "Don't use em dashes in any user-facing string. Subheads, alerts, empty-state copy, toast messages: rewrite with periods, commas, colons, semicolons, or parentheses.", "Don't reach for raw Tailwind palette colours (bg-blue-500/70, text-emerald-300, bg-amber-500/15, text-sky-300) as a substitute for status meaning.", "Don't hex-code colours inside a Vue component's scoped CSS.", - "Don't use #000 or #FFF. Foreground should tint toward the brand violet.", + "Don't use pure #000 for surfaces or pure #FFF for routine text. Use the graphite ladder and soft-gray foreground tokens.", "Don't apply gradient backgrounds to surface-level chrome.", "Don't use backdrop-blur decoratively. Blur is reserved for 'the page underneath is no longer interactive'.", "Don't lay out three or four identical icon-+-heading-+-paragraph cards in a feature grid.", @@ -253,6 +255,7 @@ "Don't style section captions as divs and skip the heading.", "Don't introduce border-left-N or border-right-N colored stripes on cards, list items, or alerts.", "Don't animate layout properties. Transforms and opacity only.", + "Don't turn onboarding into a product demo or place decorative terminal output beside account creation.", "Don't ease with bounce or elastic. Exponential ease-out only." ] } diff --git a/DESIGN.md b/DESIGN.md index 81f2e2f7..4baa87d7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2,17 +2,17 @@ name: Orva description: Self-hosted Function-as-a-Service for homelab operators and on-prem teams. colors: - background: "#12111C" - surface: "#1A1929" - surface-hover: "#252438" - border: "#2D2B42" - foreground: "#FFFFFF" - foreground-muted: "#A3A3B3" + background: "#0B0D10" + surface: "#15181D" + surface-hover: "#23272E" + border: "#343A44" + foreground: "#F4F6F8" + foreground-muted: "#ADB4BE" primary: "#553F83" primary-hover: "#684D9E" primary-foreground: "#FFFFFF" - secondary: "#2D2B42" - secondary-hover: "#3E3B5A" + secondary: "#282E36" + secondary-hover: "#353D47" danger: "#EF4444" warning: "#EAB308" success: "#22C55E" @@ -133,33 +133,33 @@ components: Orva is the control plane for someone running a function platform on their own machine. Probably in a dim home office, often after-hours, more curious than time-pressured. The interface earns trust by behaving like a serious infrastructure tool: dense, unfussy, comfortable with code, willing to show numbers without dressing them up. It doesn't perform competence; it embodies it. Every page subhead reads as a one-line operator's note from a colleague who already knows the system: short, factual, no marketing flair. -The palette is the deep purple-tinted near-black of an editor at midnight, paired with a single muted violet accent that carries every CTA. JetBrains Mono runs alongside Inter so paths, IDs, byte counts, and CPU readings sit in their natural register without arguing for attention. Rhythm is built from quiet density: small type, generous gaps where ideas separate, tight gaps within a single thought, no empty-space-as-decoration. Surfaces are flat. Shadows whisper, never advertise. +The palette is the neutral graphite-black of an editor at midnight, paired with a single muted violet accent that carries every CTA. JetBrains Mono runs alongside Inter so paths, IDs, byte counts, and CPU readings sit in their natural register without arguing for attention. Rhythm is built from quiet density: small type, generous gaps where ideas separate, tight gaps within a single thought, no empty-space-as-decoration. Surfaces are flat. Shadows whisper, never advertise. What this system rejects: the SaaS-marketing dashboard family (gradient hero panels, three-up icon-feature cards, glassy decorative blurs), category clichés (cloud-vendor purple gradients, "modern" minimal-cream-with-emerald, observability navy), and copy that sounds written rather than spoken. Orva is operator software for people who chose to self-host, not a sales pitch for becoming a self-hoster. **Key Characteristics:** -- Deep violet-tinted dark surfaces; one muted purple accent carries identity. +- Neutral graphite-black surfaces; one muted violet accent carries identity. - Inter for prose, JetBrains Mono for any data the operator might compare digit-to-digit. - Standardised page heads: `text-xl semibold tracking-tight` over a one-line subhead in muted text, max-w-prose. -- Hidden scrollbars by intent: nested overflow areas (modals, drawers, code blocks) feel calmer without 6px tracks in every region. +- Thin, low-contrast scrollbars keep nested overflow discoverable without dominating the surface. - Status colour is semantic, never decorative. A pill that's amber means in-flight; an amber accent that means nothing is forbidden. - No copy uses em dashes. Periods, commas, colons, semicolons, parentheses. The voice is operator-spoken. ## 2. Colors -A single muted violet accent on a deep violet-tinted near-black, with cool gray-violet text and three semantic status colours. The neutrals all carry a faint hue tilt toward the brand violet so nothing in the UI looks plastic-gray. +A single muted violet accent sits on neutral graphite-black surfaces with cool light-gray text and three semantic status colours. Neutral surfaces maximize reading contrast; brand color is reserved for actions and selection. ### Primary - **Muted Violet** (`#553F83`, ≈`oklch(40% 0.10 295)`): the only identity colour. Carries primary CTAs (Deploy, Save, Confirm), the active sidebar item, status-running indicators, and selected chip filters. Reserved enough that when it appears, the eye knows where to land. - **Muted Violet Hover** (`#684D9E`, ≈`oklch(48% 0.12 295)`): hover state for primary surfaces. Lightness step only; same hue and chroma family. ### Neutral -- **Background** (`#12111C`, ≈`oklch(13% 0.029 277)`): page surface and the body of cards. Almost black, faintly violet so it never reads as a flat #0d0d0d void. -- **Surface** (`#1A1929`, ≈`oklch(18% 0.035 277)`): one elevation step up. Used for headers/footers inside cards, modal backdrops behind content rows, and inline code blocks. -- **Surface Hover** (`#252438`, ≈`oklch(24% 0.042 277)`): hover state for rows, list items, and ghost buttons. The first frame of "I noticed you". -- **Border** (`#2D2B42`, ≈`oklch(28% 0.045 277)`): every divider, every card outline, every input stroke. The system reads layers through borders, not shadows. -- **Foreground** (`#FFFFFF`): currently pure white. **Tint to `#F4F2FA` (≈`oklch(96% 0.01 290)`)** to match the rest of the palette's hue discipline; the eye reads pure white as harsh against a violet-tinted dark. The `--color-foreground-strong: #FFFFFF` token is reserved for text on saturated brand-colour surfaces (primary CTA labels, on-violet badges); the default `--color-foreground` may shift to a tinted off-white in a future palette pass without breaking those high-contrast sites. -- **Foreground Muted** (`#A3A3B3`, ≈`oklch(70% 0.018 280)`): all secondary text, icon defaults, table cell content one rank below primary. The cool gray-violet is intentional. +- **Background** (`#0B0D10`): the near-black application canvas and card body. It is neutral graphite, not violet. +- **Surface** (`#15181D`): one graphite elevation step up. Used for headers, footers, modal layers, and inline code blocks. +- **Surface Hover** (`#23272E`): hover state for rows, list items, and ghost buttons. It stays neutral so interaction does not compete with semantic color. +- **Border** (`#343A44`): dividers, card outlines, and input strokes. Slightly cool gray keeps dense structure visible without drawing a grid over the page. +- **Foreground** (`#F4F6F8`): soft light gray for primary reading text. `--color-foreground-strong: #FFFFFF` remains reserved for saturated brand surfaces. +- **Foreground Muted** (`#ADB4BE`): secondary text, icon defaults, and lower-ranked table content. It maintains at least 8.6:1 contrast against both primary surfaces. - **Link** (`--color-link: #8b7bd8`): hyperlinks inside rendered prose (the AI chat markdown, Docs). A lighter, less-saturated lift of the violet accent so links read as interactive without competing with primary CTAs. Inline code in the same prose uses `--color-surface-hover` as its chip background. ### Status (semantic, used at low chroma) @@ -171,7 +171,7 @@ A single muted violet accent on a deep violet-tinted near-black, with cool gray- **The One Accent Rule.** Muted violet appears on roughly 5 to 10 percent of any given screen. Primary CTAs, the active sidebar item, the selected filter chip. Anywhere else, ask whether the page actually needs an accent or whether a border + foreground-muted will read more confidently. Restraint is the point. -**The Tinted Neutral Rule.** Pure `#000` and `#fff` are forbidden. Every neutral carries the violet hue with chroma in the 0.005 to 0.045 band. The background is not "dark gray", it is dark violet at low chroma; the muted text is not "neutral gray", it is cool gray-violet. The forensic eye sees this; the casual eye feels it. +**The Graphite Neutral Rule.** Canvas and elevated surfaces stay neutral graphite. Violet communicates action, selection, and focus only. Primary text is soft gray; pure white is reserved for saturated action surfaces. **The Semantic Status Rule.** Status colour is reserved for status. Use the semantic tokens (`success`, `warning`, `danger`) and their `/15` tint backgrounds and `/30` borders. Reaching for `bg-emerald-500/40` or `text-sky-300` is forbidden, even when it looks "right" in isolation: that path forks the palette across views and a future theme change becomes a 125-site rewrite. @@ -235,7 +235,7 @@ The dashboard's interactive primitive. Five variants, four sizes. - **Chip:** unfilled border at rest; flips to primary fill when active. Filter pill toggles on Jobs/Webhooks/CronJobs status strips. - **States:** `:hover` lightens fill by one step; `:focus-visible` shows a 2px primary ring offset by `--color-background`; `:disabled` 50% opacity + not-allowed cursor. -**Sizes:** xs `h-7 px-2.5` (28px), sm `h-8 px-3` (32px), md `h-10 px-4` (40px, default), lg `h-12 px-6` (48px). The 28px / 32px sizes need their *hit area* (not visible height) extended to 44px for mobile touch — see the Touch-Target Rule below. +**Sizes:** xs `h-7 px-2.5` (28px), sm `h-8 px-3` (32px), md `h-10 px-4` (40px, default), lg `h-12 px-6` (48px). On coarse pointers, compact controls grow to a real, non-overlapping 44px minimum target. ### Filter Chips @@ -249,7 +249,7 @@ Filter pills on `Jobs.vue`, `Webhooks.vue`, `CronJobs.vue` status strips. Same c Two parallel conventions exist in the codebase; document both honestly. -- **Page-Level Card** (used in `Dashboard.vue` for tiles, `Activity.vue` for stat strips, etc.): `bg-background border border-border rounded-lg p-5`. The page itself is `bg-surface`-shaped (via the layout shell), so cards are a step *down* into the deep background. Headline group uses `text-xs font-bold uppercase tracking-wider` followed by `text-[11px] text-foreground-muted` hint copy. +- **Page-Level Card** (used in `Dashboard.vue` for tiles, `Activity.vue` for stat strips, etc.): `bg-background border border-border rounded-lg p-5`. The page itself is `bg-surface`-shaped (via the layout shell), so cards are a step *down* into the deep background. Use a semantic `h2` or `h3` at `text-sm font-semibold`; add hint copy only when the metric cannot explain itself. - **Component Card** (`components/common/Card.vue`): `bg-surface border border-border rounded-lg`, optional header/footer slots with `border-b/t border-border` dividers and a `bg-surface/50` footer tint. Hoverable variant adds `hover:border-foreground-muted`. **Padding scale:** `none / sm: p-4 / normal: px-6 py-4 / lg: p-8`. Dashboard tiles use `p-5` (20px) inline, which is the rhythm step between sm and normal — keep. @@ -258,6 +258,7 @@ Two parallel conventions exist in the codebase; document both honestly. - **Style:** `bg-background border border-border rounded-md px-3 py-2 text-sm`. Sits one step deeper than the surface, so the eye reads "this is where you type". - **Label:** `text-xs font-medium text-foreground-muted uppercase tracking-wide` above the field. Required indicator is a single `*` in danger color. +- **Association:** every label uses `for` with a stable input `id`; hint and error text are connected with `aria-describedby`, and errors set `aria-invalid`. - **Focus:** `focus:ring-1 focus:ring-white focus:border-white`. The ring is white, not primary — it's a "cursor's-here" marker, not an accent. - **Optional leading icon** (Lucide): `pl-9`, icon at `absolute left-3 top-1/2 -translate-y-1/2 text-foreground-muted`. - **Error:** error string in `text-xs text-danger` directly below the field. @@ -277,7 +278,7 @@ The codebase has two: `Badge.vue` (semantic-token-driven, canonical) and `Status - **Width:** `w-64` mobile drawer, `lg:w-52` (208px) desktop inline. - **Background:** `bg-background border-r border-border`. Same as page; only the right border separates it. - **Brand block:** `h-16` with the Orva mark + wordmark in `font-mono` (yes, mono — the brand wears its operator's clothes). -- **Items:** `flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium`. Single-word labels, distinct icons by silhouette (Gauge, Boxes, CalendarClock, ListChecks, Activity, ListTree, Network, Fingerprint, Plug, Webhook, ShieldHalf, Settings, LibraryBig). +- **Items:** Overview, Chat, and Functions stay visible. Lower-frequency routes are grouped under collapsible Automation, Observe, and Connect disclosures; Settings and Docs remain direct links. Rows use `flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium` with distinct icons by silhouette. - **Active:** `bg-primary text-white shadow-lg shadow-purple-900/20`. Hover: `text-white bg-surface-hover`. - **Mobile:** the desktop sidebar transforms into an off-canvas drawer toggled from a `lg:hidden` top bar with a hamburger icon. @@ -304,6 +305,7 @@ A side panel for inspector-style content (invocation request panel, activity row - **Do** reach for `bg-background border border-border` for cards-on-page and `bg-surface border border-border` for cards-on-deeper-surface. The system reads depth through borders + step shifts, not shadow. - **Do** add `aria-label` to every icon-only button (delete, refresh, close, filter chips). Lucide icons announce nothing on their own. - **Do** keep CTAs on roughly 5 to 10 percent of any screen. Most surfaces have one primary action; some have none. +- **Do** keep the decision-ready state visible and place setup forms, implementation metadata, and advanced detail behind native disclosure controls. ### Don't @@ -311,19 +313,19 @@ PRODUCT.md names five anti-references. Each is below as a Don't. - **Don't look like AWS Console.** No region selectors, no every-feature-on-screen surface, no low-density noise. Orva sits on one host; the UI should feel like one host. If a page wants a settings panel that lists ten unrelated knobs, push back: split it, or fold it into context where each knob lives. - **Don't fall into the generic SaaS dashboard template.** No big-number-small-label-with-gradient-accent hero metrics. The shared design law spells this out as the "hero-metric template" ban; PRODUCT.md repeats the rule. Surface metrics on bars, sparklines, and stacked-bar viz, not on template-shaped tiles. -- **Don't lift Vercel / Railway / landing-page onboarding panels.** Diagonal gradient backgrounds, decorative blurred circles, three identical glassmorphic feature chips with icon + heading + short description. The `Onboarding.vue:4` panel is exactly this template today; replace it with a register native to Orva (live terminal output, real curl→response trace, an editor preview of a deployed function). +- **Don't turn onboarding into a product demo.** No gradients, feature panels, terminal theatre, or secondary education beside the account-creation task. - **Don't apply cloud-vendor branding.** No clouds, no sky gradients, no "scale instantly" copy, no planet-scale rhetoric. Orva runs on one box you can touch. -- **Don't read as an AI-generated control plane.** The codebase already uses violet on near-black, so the discipline that keeps it from reading like that template comes from the rest of these rules. Specifically: no glowing borders, no gradient text, no animated mesh backgrounds, no decorative blurs. +- **Don't read as an AI-generated control plane.** Violet is an accent on graphite, not the atmosphere of every surface. No glowing borders, gradient text, animated mesh backgrounds, or decorative blurs. The rest of the Don'ts apply across every register: - **Don't** use em dashes in any user-facing string. Subheads, alerts, empty-state copy, toast messages: rewrite with periods, commas, colons, semicolons, or parentheses. Also no `--`. The recent header standardisation pass left ≈16 in template bodies and more in JS-built strings; sweep them. - **Don't** reach for raw Tailwind palette colours (`bg-blue-500/70`, `text-emerald-300`, `bg-amber-500/15`, `text-sky-300`) as a substitute for status meaning. The codebase has 125 of these and every one is a pending palette migration. - **Don't** hex-code colours inside a Vue component's scoped CSS. `Docs.vue` still carries hex literals (its Python/Node brand-logo SVG stops, which are a legitimate exemption); map any non-brand hex to `var(--color-…)` so a future theme change works. `Firewall.vue` has already been migrated to the semantic status tokens — use it as the reference. -- **Don't** use `#000` or `#fff`. Foreground should tint toward the brand violet (target `#F4F2FA`); background and surfaces are already correctly tinted. +- **Don't** use pure `#000` for surfaces or pure `#fff` for routine text. Use the graphite surface ladder and soft-gray foreground tokens; pure white is reserved for saturated action surfaces. - **Don't** use `backdrop-blur` decoratively. The three glassmorphic icon chips on Onboarding are the exact pattern PRODUCT.md's Vercel/Railway anti-reference rejects. Blur is reserved for "the page underneath is no longer interactive". - **Don't** lay out three or four identical icon-+-heading-+-paragraph cards in a feature grid. That template is the absolute ban "identical card grids" by name; PRODUCT.md flags the same shape under "Vercel / Railway / landing-page onboarding panels". -- **Don't** size ` - - - - {entries_html} - - - - + - - + + - +
diff --git a/docs/API.md b/docs/API.md index 65ac5276..d6fd560f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -516,6 +516,12 @@ List conversations (most-recently-updated first). ### `POST /api/v1/ai/conversations` Create an empty conversation. +### `DELETE /api/v1/ai/conversations` +Delete every conversation and cascade-delete their messages and tool calls in +one operation. Returns `{"deleted": N}`. Responds with `409 +CONVERSATION_BUSY` without deleting anything when any conversation has a turn +in progress. + ### `GET /api/v1/ai/conversations/{id}` Fetch one conversation with its full message + tool-call timeline. @@ -559,8 +565,10 @@ List the models the configured provider/endpoint reports. ### `GET /api/v1/ai/settings` ### `PUT /api/v1/ai/settings` Read/update assistant settings: default provider/model, thinking level, -approval policy (`all_writes` / `destructive_only` / `auto`), and the -per-reply tool-step cap. +and approval policy (`all_writes` / `destructive_only` / `auto`). The +`max_tool_iterations` response field is retained for API compatibility but is +an internal runaway-work guard fixed at `25`; values supplied by `PUT` are +ignored and normalized to `25`. ### `PUT /api/v1/ai/selection` Persist the dashboard's active provider/model/thinking selection. diff --git a/docs/TESTING.md b/docs/TESTING.md index 1bcc3095..4068e35f 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -704,7 +704,8 @@ call sites. They clean up their functions and leave the conversation and message rows. **All 7 AI modules mutate the shared `ai_settings` singleton** (provider, model, -approval policy, `max_tool_iterations`). `remove_mock_provider()` restores a +and approval policy). `max_tool_iterations` remains fixed internally at `25`. +`remove_mock_provider()` restores a snapshot on the normal path; a crash between configure and `finally` leaves the instance pointed at a dead `http://127.0.0.1:11434/v1` provider. On an instance holding a real provider key this also means real money, so use a throwaway. @@ -1533,7 +1534,7 @@ curl -X POST $B/api/v1/ai/providers -H "X-Orva-API-Key: $K" -H 'Content-Type: ap "base_url":"http://host.docker.internal:11500/v1","enabled":true}' curl -X PUT $B/api/v1/ai/settings -H "X-Orva-API-Key: $K" -H 'Content-Type: application/json' \ -d '{"provider":"openai","model":"gpt-4o","thinking_level":"off", - "approval_policy":"auto","max_tool_iterations":10}' + "approval_policy":"auto"}' # FIELD IS "content", NOT "message" curl -sN -X POST $B/api/v1/ai/chat -H "X-Orva-API-Key: $K" \ diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 4c6a774a..dd3176c6 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -37,7 +37,7 @@ After `npm run build`, run `make embed` from the repo root to copy `dist/` into | `src/stores/confirm.js` | Global confirmation modal store | | `src/stores/events.js` | Persistent SSE connection to `/api/v1/events` | | `src/stores/ai.js` | AI chat store: conversations, timeline, streaming client (fetch + ReadableStream, NOT EventSource — the chat POST carries a body), provider/model/settings, + message actions (regenerate / editAndResend / deleteMessageFrom / retry / stop / renameConversation / exportActive) | -| `src/views/AI.vue` + `src/components/ai/*` | Native Vue agentic chat UI; talks to `/api/v1/ai/*`. Components: `ConversationRail`, `ChatHeader`, `EmptyState` (greeting + click-to-fill starter prompts), `Composer` (textarea + `ReasoningMenu` + `ModelMenu` + Send/Stop), `MessageList`, `Message`, `MessagePart` (markdown-it + fenced → `CodeBlock`, thinking → `ThinkingBlock`), `ToolCallCard`, `TypingIndicator`, `ScrollToBottom`, `ErrorCard`, `AISettingsPanel`. AI **configuration** (providers, keys, defaults) is centralized on the Settings page via `AISettingsPanel` (rendered in `Settings.vue`'s "AI assistant" card, anchor `#ai`); the chat's gear + no-provider banner deep-link there (`router.push({name:'settings', hash:'#ai'})`). The chat composer keeps the active provider/model/reasoning pickers (per-conversation controls, persisted server-side via `PUT /api/v1/ai/selection`). | +| `src/views/AI.vue` + `src/components/ai/*` | Native Vue agentic chat UI; talks to `/api/v1/ai/*`. Components: `ConversationRail`, `ChatHeader`, `EmptyState` (greeting + click-to-fill starter prompts), `Composer` (textarea + `ReasoningMenu` + `ModelMenu` + Send/Stop), `MessageList`, `Message`, `MessagePart` (markdown-it + fenced → `CodeBlock`, thinking → `ThinkingBlock`), `ToolCallCard`, `TypingIndicator`, `ScrollToBottom`, `ErrorCard`, `AISettingsPanel`. AI **configuration** (providers, keys, defaults) is centralized on the Settings page via `AISettingsPanel` (rendered in `Settings.vue`'s "AI assistant" card, anchor `#ai`); the chat's gear + no-provider banner deep-link there (`router.push({name:'settings', hash:'#ai'})`). Settings and the chat composer share the active provider/model selection, persisted server-side via `PUT /api/v1/ai/selection`; reasoning remains available in the composer. | | `src/stores/system.js` | System info (version, runtime stats) | | `src/views/Editor.vue` | Function editor + test pane (method/path/headers/body) + saved fixtures + suggest-fix | | `src/views/InvocationsLog.vue` | Execution history drawer + request panel + replay button + suggest-fix | diff --git a/frontend/src/components/ai/AISettingsPanel.vue b/frontend/src/components/ai/AISettingsPanel.vue index 63b75339..de4f32a2 100644 --- a/frontend/src/components/ai/AISettingsPanel.vue +++ b/frontend/src/components/ai/AISettingsPanel.vue @@ -3,27 +3,73 @@ AISettingsPanel — the AI assistant's operator configuration, embedded as a card body in the centralized Settings page (migrated out of the old in-chat modal). Covers everything that is *configuration*: providers + encrypted API - keys + base URL, and the assistant's defaults (approval policy, tool steps). - The *active* provider/model/reasoning selection is NOT here — that's a - per-conversation control and lives in the chat composer. + keys + base URL, the active provider/model, and the assistant's defaults + (approval policy). The same active selection also remains + available in the chat composer for quick switching while working. -->
-

+

Providers

- Bring your own keys. Keys are encrypted at rest; models are listed live from each provider. Pick the provider + model inside the chat composer. + Keys are encrypted at rest. Choose which provider and model Chat uses.

-
+
+
+ + +
+
+ +
+ +
+
+

+ Models could not be loaded. Check the provider endpoint and credentials. +

+
+ +
{{ p.provider }} @@ -32,18 +78,14 @@ class="text-xs text-foreground-muted" >{{ p.label }} active + {{ p.has_key ? 'key set' : 'no key' }} -
- -
-

- {{ modelState(p.id).error }} -

-

- No models reported by this endpoint. -

-
- {{ m.id }} -
-

-

-

+
+ Add provider -

-

- For any OpenAI-compatible endpoint (self-hosted, vLLM, Together, …) choose openai and set the Base URL. -

-
- - - {{ opt }} - - + + +
+ + + +
- - - - -
+
-

+

Defaults

@@ -156,15 +180,12 @@ Approval policy

- Reads always run on their own. This controls when the assistant pauses for your OK before it changes anything. + Choose when changes require confirmation.

-
+
+ + +
diff --git a/frontend/src/components/ai/ModelMenu.vue b/frontend/src/components/ai/ModelMenu.vue index af7737d6..b7d799a7 100644 --- a/frontend/src/components/ai/ModelMenu.vue +++ b/frontend/src/components/ai/ModelMenu.vue @@ -1,19 +1,25 @@