Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .ai/docs/design-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ Tokens are the single source of truth for shared visual values used by Moonbase

## Source of Truth

- Core token definitions are stored in `app/assets/css/tokens.css`.
- Tailwind token mapping and scale extensions are configured in `tailwind.config.ts`.
- The design source is the **MoonBase Studio Design System** (claude.ai artifact, direction "Orbit", `tokens.json` v2). It is shared with the Moonbase Commander 7" screen (`electronics/moonbase-commander/docs/ekran-7.md`, section 2). Change values there first, then mirror them here.
- Core token definitions are stored in `app/assets/css/tokens.css` (one dark theme; `accent` = signal, `surface-container` = panel, `surface-container-high` = raised, `line`).
- Tailwind token mapping and scale extensions are configured in `tailwind.config.ts` (Space Grotesk / Space Mono / Lalezar, Orbit type scale, radius 0, size tokens `bar`, `control`, `row`, `bracket`, `key`).
- Extra Tailwind behavior can be added in `tailwind/plugins/*` (for example root font sizing).

---
Expand Down
37 changes: 19 additions & 18 deletions .ai/docs/ui-design-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Inconsistent UI is a defect. Visual and behavioral patterns must be uniform acro
- `hover` — visible, subtle feedback
- `focus` — clearly visible focus ring (never removed, only styled)
- `active` / `pressed` — immediate visual response
- `disabled` — reduced opacity, no pointer events, cursor `not-allowed`
- `disabled` — `text-on-surface-dim`, dashed border, no pointer events, cursor `not-allowed` (no opacity)
- Do not mix token-based values with hardcoded values within the same component.

---
Expand All @@ -54,29 +54,30 @@ Tailwind is the **only** permitted styling system. All visual output must be exp
- Do not use default Tailwind spacing numerics (e.g. `p-4`, `gap-2`) — they are overridden by the project scale.
- `1rem = 10px` in this project due to the `root-font-size` plugin.

### Typography
### Typography (Orbit)

- Use the semantic font size scale: `text-heading-x-huge`, `text-heading-x-lg`, `text-heading-lg`, `text-heading-md`, `text-body-lg`, `text-body-md`, `text-body-sm`, `text-body-x-sm`.
- Font families: `font-roboto` for all UI text; `font-arlon` for brand/logo elements only.
- Font weights: `font-normal` (400), `font-medium` (500), `font-semibold` (600).
- Letter spacing: use `tracking-x-sm` through `tracking-xx-lg` from the defined scale.
- Families: `font-sans` (Space Grotesk) for all UI text; `font-mono` (Space Mono) for labels, units, group headings and state words, always `uppercase`; `font-brand` (Lalezar) for the MoonBase wordmark only.
- Weights: `font-light` (300) for sizes ≥ 20px (titles, large numbers), `font-normal` (400) for text, `font-bold` (700) only for state values (`text-state`). Do not use 500 or 600.
- Size scale: `text-display` (96), `text-numeric-xl` (120), `text-numeric-lg` (48), `text-title` (28), `text-numeric-md` (20), `text-body-lg` (18), `text-body-md` (16), `text-body-sm` (14), `text-body-x-sm` (12, the minimum), `text-state` (14/700), `text-label` (12 mono, 0.14em), `text-label-lg` (14 mono, 0.12em), `text-brand-logo` (28 Lalezar).
- Numbers always get `tabular-nums`.
- The inspector uses `inspectorTypography` (`title`, `valueLarge`, `value`, `body`, `label`) from `app/utils/inspector-typography.utils.ts`. Those styles do not include color.
- Never hardcode `font-size`, `line-height`, or `font-family` values.
- The layer properties inspector uses the Figma Special/Standard scale instead of the Roboto body scale: `font-lalezar` with `text-special-high` (48px), `text-special-medium` (40px), `text-special-small` (36px), and `font-inter` with `text-standard-high` (24px), `text-standard-medium` (20px), `text-standard-small` (16px). Prefer `inspectorTypography` from `app/utils/inspector-typography.utils.ts`. Those styles do not include color. The rest of the studio chrome keeps `text-body-*` and `font-roboto`.

### Color Usage
### Color Usage (Orbit)

- Use semantic color tokens only. Never use Tailwind's default palette (e.g. `bg-blue-500`).
- Surface colors: `bg-surface`, `bg-surface-container`, `bg-surface-inverted`.
- Text colors: `text-on-surface`, `text-on-surface-dim`, `text-on-surface-inverted`.
- Accent: `bg-accent`, `text-accent`, `bg-accent-dim`, `text-accent-dim`.
- Semantic: `bg-info`, `bg-warning`, `bg-error`, `bg-success` (and their `-container` variants).
- Opacity modifiers are allowed: `bg-accent/80`, `text-error/30`.
- Theme is applied via `theme-light`, `theme-dark`, or `theme-blue` on `<html>` — do not override theme-sensitive tokens per component.
- Use semantic color tokens only. Never use Tailwind's default palette (e.g. `bg-blue-500`) or hex values.
- One dark theme. `bg-surface` is the page; a block is `border border-line`, not a filled panel. `bg-surface-container` (panel) only for overlays (menus, dialogs, popovers) and hardware keys. `bg-surface-container-high` (raised) for hover rows and unlit segments.
- Text: `text-on-surface`; labels, units and disabled: `text-on-surface-dim`; on green fills: `text-on-accent`.
- `accent` is the signal green: live, active, selected, focus, corner brackets. It is a stroke, dot, segment or number — a fill only for “Start / Resume” and the on-air key. Hover/pressed: `accent-dim`.
- Semantic: `warning`, `error`, `info` (`success` equals `accent`). Every state also carries a word or icon.
- Element categories (Studio): `category-layout`, `category-interactive` (`category-interactive-fill` for hover).
- Avoid opacity modifiers and transparency; disabled is `text-on-surface-dim` plus a dashed border, not `opacity-50`.
- Active blocks get corner brackets: `BaseFrame active` or `BaseFrameBrackets`.

### Border Radius
### Border Radius (Orbit)

- Use the defined scale: `rounded-none`, `rounded-x-sm`, `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-full`.
- Do not use Tailwind's default radius values (e.g. `rounded`, `rounded-2xl`).
- No rounding: every radius token is 0. `rounded-full` only for the status dot (`BaseStatusBadge`).
- No shadows, gradients, blur or glass. The boot screen is the only exception.

### Transitions

Expand Down
197 changes: 31 additions & 166 deletions app/assets/css/tokens.css
Original file line number Diff line number Diff line change
@@ -1,180 +1,45 @@
/*
* MoonBase — Orbit (design system "MoonBase Studio Design System", tokens.json v2).
* One dark theme. Channels are "r g b" so Tailwind can add alpha, but Orbit
* avoids transparency: prefer the solid tokens below.
* Token → design system name:
* surface → surface, surface-container → panel, surface-container-high → raised,
* line → line, on-surface → on-surface, on-surface-dim → on-surface-dim,
* accent → signal, accent-dim → signal-dim, on-accent → on-signal.
*/
:root,
.theme-light {
/* Surfaces */
--color-surface: 242 242 242;
--color-surface-container: 233 233 233;
--color-surface-inverted: 0 0 0;

/* Content / Text */
--color-on-surface: 5 3 21;
--color-on-surface-dim: 90 90 90;
--color-on-surface-inverted: 255 255 255;

/* Accent */
--color-accent: 0 114 245;
--color-accent-dim: 121 180 239;

/* Panel categories */
--color-category-layout: 0 201 228;
--color-category-interactive: 255 0 204;

/* Semantic */
--color-info: 82 128 234;
--color-info-container: 82 128 234;
--color-warning: 255 106 0;
--color-warning-container: 255 106 0;
--color-error: 220 38 38;
--color-error-container: 220 38 38;
--color-success: 132 204 22;
--color-success-container: 132 204 22;
}

.theme-dark {
/* Surfaces */
--color-surface: 11 13 17;
--color-surface-container: 20 24 29;
--color-surface-inverted: 231 231 231;

/* Content / Text */
--color-on-surface: 255 255 255;
--color-on-surface-dim: 179 179 179;
--color-on-surface-inverted: 0 0 0;

/* Accent */
--color-accent: 59 130 246;
--color-accent-dim: 35 78 148;

/* Panel categories */
--color-category-layout: 0 201 228;
--color-category-interactive: 255 0 204;

/* Semantic */
--color-info: 82 128 234;
--color-info-container: 82 128 234;
--color-warning: 255 106 0;
--color-warning-container: 255 106 0;
--color-error: 220 38 38;
--color-error-container: 220 38 38;
--color-success: 132 204 22;
--color-success-container: 132 204 22;
}

.theme-blue {
/* Surfaces */
--color-surface: 14 19 27;
--color-surface-container: 29 33 41;
--color-surface-inverted: 231 231 232;

/* Content / Text */
--color-on-surface: 255 255 255;
--color-on-surface-dim: 179 179 179;
--color-on-surface-inverted: 0 0 0;

/* Accent */
--color-accent: 0 201 228;
--color-accent-dim: 0 121 137;

/* Panel categories */
--color-category-layout: 0 201 228;
--color-category-interactive: 255 0 204;

/* Semantic */
--color-info: 82 128 234;
--color-info-container: 82 128 234;
--color-warning: 255 106 0;
--color-warning-container: 255 106 0;
--color-error: 220 38 38;
--color-error-container: 220 38 38;
--color-success: 132 204 22;
--color-success-container: 132 204 22;
}

html[data-theme='light'] {
/* Surfaces */
--color-surface: 242 242 242;
--color-surface-container: 233 233 233;
--color-surface-inverted: 0 0 0;

/* Content / Text */
--color-on-surface: 5 3 21;
--color-on-surface-dim: 90 90 90;
--color-on-surface-inverted: 255 255 255;

/* Accent */
--color-accent: 0 114 245;
--color-accent-dim: 121 180 239;

/* Panel categories */
--color-category-layout: 0 201 228;
--color-category-interactive: 255 0 204;

/* Semantic */
--color-info: 82 128 234;
--color-info-container: 82 128 234;
--color-warning: 255 106 0;
--color-warning-container: 255 106 0;
--color-error: 220 38 38;
--color-error-container: 220 38 38;
--color-success: 132 204 22;
--color-success-container: 132 204 22;
}

.theme-dark,
html[data-theme='dark'] {
/* Surfaces */
--color-surface: 11 13 17;
--color-surface-container: 20 24 29;
--color-surface-inverted: 231 231 231;

/* Content / Text */
--color-on-surface: 255 255 255;
--color-on-surface-dim: 179 179 179;
--color-on-surface-inverted: 0 0 0;

/* Accent */
--color-accent: 59 130 246;
--color-accent-dim: 35 78 148;

/* Panel categories */
--color-category-layout: 0 201 228;
--color-category-interactive: 255 0 204;

/* Semantic */
--color-info: 82 128 234;
--color-info-container: 82 128 234;
--color-warning: 255 106 0;
--color-warning-container: 255 106 0;
--color-error: 220 38 38;
--color-error-container: 220 38 38;
--color-success: 132 204 22;
--color-success-container: 132 204 22;
}

html[data-theme='blue'] {
/* Surfaces */
--color-surface: 14 19 27;
--color-surface-container: 29 33 41;
--color-surface-inverted: 231 231 232;
--color-surface: 5 0 4;
--color-surface-container: 18 18 18;
--color-surface-container-high: 28 28 28;
--color-surface-inverted: 255 255 255;
--color-line: 42 42 42;

/* Content / Text */
--color-on-surface: 255 255 255;
--color-on-surface-dim: 179 179 179;
--color-on-surface-inverted: 0 0 0;
--color-on-surface-dim: 138 138 138;
--color-on-surface-inverted: 5 0 4;

/* Accent */
--color-accent: 0 201 228;
--color-accent-dim: 0 121 137;
/* Signal (accent) */
--color-accent: 34 255 134;
--color-accent-dim: 24 184 95;
--color-on-accent: 5 0 4;

/* Panel categories */
--color-category-layout: 0 201 228;
/* Element categories (Studio only) */
--color-category-layout: 0 229 255;
--color-category-interactive: 255 0 204;
--color-category-interactive-fill: 55 0 44;

/* Semantic */
--color-info: 82 128 234;
--color-info-container: 82 128 234;
--color-warning: 255 106 0;
--color-warning-container: 255 106 0;
--color-error: 220 38 38;
--color-error-container: 220 38 38;
--color-success: 132 204 22;
--color-success-container: 132 204 22;
--color-warning: 230 161 92;
--color-warning-container: 230 161 92;
--color-error: 232 93 76;
--color-error-container: 232 93 76;
--color-success: 34 255 134;
--color-success-container: 34 255 134;
--color-on-status: 5 0 4;
}
4 changes: 2 additions & 2 deletions app/components/base/base-color-swatch/BaseColorSwatch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ function onPickerChange(event: Event) {
<div :class="cn('flex flex-col items-start gap-x-tiny', props.class)">
<span
v-if="label"
class="font-roboto text-body-x-sm lowercase leading-none text-on-surface-dim"
class="font-mono text-label uppercase text-on-surface-dim"
>
{{ label }}
</span>
<button
type="button"
:disabled="disabled"
class="size-6 shrink-0 rounded-x-sm border border-on-surface-dim/40 p-0 disabled:opacity-50"
class="size-xx-lg shrink-0 border border-on-surface-dim p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-surface disabled:cursor-not-allowed disabled:border-dashed"
:style="{ backgroundColor: modelValue }"
:aria-label="label ?? modelValue"
@click="openPicker"
Expand Down
37 changes: 37 additions & 0 deletions app/components/base/base-frame/BaseFrame.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { cn } from '../../../utils/tailwind.utils';
import type { BaseFrameProps } from '.';
import BaseFrameBrackets from './BaseFrameBrackets.vue';

const props = withDefaults(defineProps<BaseFrameProps>(), {
label: undefined,
active: false,
corners: 'diagonal',
tone: 'accent',
filled: false,
as: 'section',
class: undefined,
});
</script>

<template>
<component
:is="props.as"
:class="
cn('relative min-w-0 border border-line', props.filled ? 'bg-surface-container' : 'bg-transparent', props.class)
"
>
<BaseFrameBrackets
v-if="active"
:corners="corners"
:tone="tone"
/>
<span
v-if="label"
class="mb-xxx-sm block font-mono text-label uppercase text-on-surface-dim"
>
{{ label }}
</span>
<slot />
</component>
</template>
40 changes: 40 additions & 0 deletions app/components/base/base-frame/BaseFrameBrackets.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { cva } from 'class-variance-authority';

import type { BaseFrameCorners, BaseFrameTone } from '.';

const props = withDefaults(defineProps<{ corners?: BaseFrameCorners; tone?: BaseFrameTone }>(), {
corners: 'diagonal',
tone: 'accent',
});

const bracketVariants = cva('pointer-events-none absolute size-bracket border-0', {
variants: {
tone: {
accent: 'border-accent',
warning: 'border-warning',
error: 'border-error',
layout: 'border-category-layout',
interactive: 'border-category-interactive',
},
},
defaultVariants: { tone: 'accent' },
});

const positions = computed(() => {
const diagonal = ['-left-px -top-px border-l border-t', '-bottom-px -right-px border-b border-r'];
if (props.corners === 'diagonal') {
return diagonal;
}
return [...diagonal, '-right-px -top-px border-r border-t', '-bottom-px -left-px border-b border-l'];
});
</script>

<template>
<span
v-for="position in positions"
:key="position"
:class="[bracketVariants({ tone: props.tone }), position]"
aria-hidden="true"
/>
</template>
22 changes: 22 additions & 0 deletions app/components/base/base-frame/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ClassValue } from 'clsx';

export type BaseFrameTone = 'accent' | 'warning' | 'error' | 'layout' | 'interactive';
export type BaseFrameCorners = 'diagonal' | 'all';

export interface BaseFrameProps {
/** Mono label in the top-left corner of the frame. */
label?: string;
/** Adds the corner brackets (Orbit "active block"). */
active?: boolean;
/** `diagonal`: top-left + bottom-right; `all`: four corners. */
corners?: BaseFrameCorners;
tone?: BaseFrameTone;
/** Panel fill. Only for overlays (menus, dialogs, popovers) and hardware keys. */
filled?: boolean;
/** Rendered element. */
as?: string;
class?: ClassValue;
}

export { default as BaseFrame } from './BaseFrame.vue';
export { default as BaseFrameBrackets } from './BaseFrameBrackets.vue';
Loading
Loading