From 1f1db080b71e81c85101dfff21134bd2d2e6911f Mon Sep 17 00:00:00 2001 From: Mateusz Basiaga Date: Sat, 14 Nov 2026 02:59:46 +0100 Subject: [PATCH 1/4] feat(tokens): switch to the Orbit theme Mirror the MoonBase Studio Design System, direction Orbit (tokens.json v2). One dark theme: surface #050004, panel, raised and line surfaces, signal #22FF86 as the accent, warning and error from the Commander theme. Space Grotesk 300/400/700 for the UI, Space Mono for labels, Lalezar only for the wordmark. New type scale (display, title, label, state, numeric-*), radius 0 everywhere, size tokens bar, control, row, bracket and key. inspectorTypography now exposes title, valueLarge, value, body and label. --- app/assets/css/tokens.css | 197 ++++-------------------- app/utils/inspector-typography.utils.ts | 27 ++-- app/utils/tailwind.utils.ts | 19 ++- nuxt.config.ts | 7 +- tailwind.config.ts | 85 ++++++---- 5 files changed, 112 insertions(+), 223 deletions(-) diff --git a/app/assets/css/tokens.css b/app/assets/css/tokens.css index 91abb78..9351ed0 100644 --- a/app/assets/css/tokens.css +++ b/app/assets/css/tokens.css @@ -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; } diff --git a/app/utils/inspector-typography.utils.ts b/app/utils/inspector-typography.utils.ts index 409223e..9d43612 100644 --- a/app/utils/inspector-typography.utils.ts +++ b/app/utils/inspector-typography.utils.ts @@ -1,24 +1,23 @@ /** - * Figma text styles for the layer inspector. + * Orbit text styles for the layer inspector (MoonBase Studio Design System). * Color, alignment, and box size stay on the consumer. */ export const inspectorTypography = { - /** Special High — Lalezar Regular 48px */ - specialHigh: 'font-lalezar text-special-high font-normal tracking-normal', - /** Special Medium — Lalezar Regular 40px */ - specialMedium: 'font-lalezar text-special-medium font-normal tracking-normal', - /** Special Small — Lalezar Regular 36px */ - specialSmall: 'font-lalezar text-special-small font-normal tracking-normal', - /** Standard High — Inter Medium 24px */ - standardHigh: 'font-inter text-standard-high font-medium tracking-normal', - /** Standard Medium — Inter Medium 20px */ - standardMedium: 'font-inter text-standard-medium font-medium tracking-normal', - /** Standard Small — Inter Medium 16px */ - standardSmall: 'font-inter text-standard-small font-medium tracking-normal', + /** Card / view title — Space Grotesk 300, 28px. */ + title: 'font-sans text-title font-light', + /** Large value — Space Grotesk 300, 20px, tabular figures. */ + valueLarge: 'font-sans text-numeric-md font-light tabular-nums', + /** Field value — Space Grotesk 400, 14px, tabular figures. */ + value: 'font-sans text-body-sm font-normal tabular-nums', + /** Body text — Space Grotesk 400, 14px. */ + body: 'font-sans text-body-sm font-normal', + /** Label, unit, group heading — Space Mono 12px, upper case. */ + label: 'font-mono text-label font-normal uppercase', } as const; export type InspectorFieldTypography = 'standard-medium' | 'standard-small'; +/** Both sizes map to the Orbit field value; kept as a prop for API compatibility. */ export function inspectorFieldTypography(kind: InspectorFieldTypography = 'standard-medium') { - return kind === 'standard-small' ? inspectorTypography.standardSmall : inspectorTypography.standardMedium; + return kind === 'standard-small' ? inspectorTypography.value : inspectorTypography.valueLarge; } diff --git a/app/utils/tailwind.utils.ts b/app/utils/tailwind.utils.ts index f1b159b..ace5fd8 100644 --- a/app/utils/tailwind.utils.ts +++ b/app/utils/tailwind.utils.ts @@ -7,16 +7,15 @@ const tailwindMerge = extendTailwindMerge({ 'font-size': [ { text: [ - 'special-high', - 'special-medium', - 'special-small', - 'standard-high', - 'standard-medium', - 'standard-small', - 'heading-md', - 'heading-lg', - 'heading-x-lg', - 'heading-x-huge', + 'display', + 'numeric-xl', + 'numeric-lg', + 'numeric-md', + 'title', + 'state', + 'label', + 'label-lg', + 'brand-logo', 'body-x-sm', 'body-sm', 'body-md', diff --git a/nuxt.config.ts b/nuxt.config.ts index 2114a2d..2d8df69 100755 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -58,8 +58,11 @@ export default defineNuxtConfig({ ], googleFonts: { families: { - Roboto: '400..700', + 'Space Grotesk': [300, 400, 700], + 'Space Mono': [400], + Lalezar: true, }, + subsets: ['latin', 'latin-ext'], preload: true, }, fonts: { @@ -71,6 +74,6 @@ export default defineNuxtConfig({ provider: 'local', }, fontMetrics: { - fonts: ['Roboto', { family: 'Roboto' }], + fonts: ['Space Grotesk', 'Space Mono'], }, }); diff --git a/tailwind.config.ts b/tailwind.config.ts index 9900bb0..5ee4091 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -3,6 +3,10 @@ import type { Config } from 'tailwindcss'; import rootFontSize from './tailwind/plugins/root-font-size'; +/** + * MoonBase — Orbit. Values mirror the "MoonBase Studio Design System" (tokens.json v2). + * 1rem = 10px (root-font-size plugin). + */ const config = { content: [], darkMode: ['class', '.theme-dark'], @@ -10,52 +14,61 @@ const config = { theme: { extend: { fontFamily: { - roboto: ['Roboto', 'sans-serif'], - inter: ['Inter', 'sans-serif'], - lalezar: ['Lalezar', 'sans-serif'], + /** Space Grotesk — the one UI face: 300 for large sizes, 400 text, 700 state. */ + sans: ['"Space Grotesk"', 'system-ui', 'sans-serif'], + /** Space Mono — labels, units, state words, upper case. */ + mono: ['"Space Mono"', 'ui-monospace', 'monospace'], + /** Lalezar — the MoonBase wordmark only. */ + brand: ['Lalezar', '"Space Grotesk"', 'sans-serif'], }, fontSize: { - 'special-high': ['4.8rem', { lineHeight: 'normal', letterSpacing: '0', fontWeight: '400' }], - 'special-medium': ['4rem', { lineHeight: 'normal', letterSpacing: '0', fontWeight: '400' }], - 'special-small': ['3.6rem', { lineHeight: 'normal', letterSpacing: '0', fontWeight: '400' }], - 'standard-high': ['2.4rem', { lineHeight: '2.9rem', letterSpacing: '0', fontWeight: '500' }], - 'standard-medium': ['2rem', { lineHeight: '2.4rem', letterSpacing: '0', fontWeight: '500' }], - 'standard-small': ['1.6rem', { lineHeight: '1.9rem', letterSpacing: '0', fontWeight: '500' }], + /* Telemetry — shared with the Commander 7" screen */ + display: ['9.6rem', { lineHeight: '9.2rem', letterSpacing: '-0.03em', fontWeight: '300' }], + 'numeric-xl': ['12rem', { lineHeight: '11.2rem', letterSpacing: '-0.04em', fontWeight: '300' }], + 'numeric-lg': ['4.8rem', { lineHeight: '5.2rem', fontWeight: '300' }], + title: ['2.8rem', { lineHeight: '3.6rem', fontWeight: '300' }], + 'body-lg': ['1.8rem', { lineHeight: '2.6rem' }], + state: ['1.4rem', { lineHeight: '2rem', fontWeight: '700' }], - 'heading-sm': ['1.6rem', { lineHeight: '2.4rem' }], - 'heading-xs': ['1.4rem', { lineHeight: '2.2rem' }], - 'heading-md': ['2rem', { lineHeight: '2.8rem' }], - 'heading-lg': ['2.8rem', { lineHeight: '3.6rem' }], - 'heading-x-lg': ['3.6rem', { lineHeight: '4.4rem' }], - 'heading-x-huge': ['6.4rem', { lineHeight: '7.6rem' }], + /* Mono labels (use with font-mono uppercase) */ + label: ['1.2rem', { lineHeight: '1.6rem', letterSpacing: '0.14em' }], + 'label-lg': ['1.4rem', { lineHeight: '2rem', letterSpacing: '0.12em' }], - 'body-x-sm': ['1.2rem', { lineHeight: '1.6rem' }], - 'body-sm': ['1.4rem', { lineHeight: '2rem' }], + /* Studio UI */ + 'numeric-md': ['2rem', { lineHeight: '2.4rem', fontWeight: '300' }], 'body-md': ['1.6rem', { lineHeight: '2.4rem' }], - 'body-lg': ['1.8rem', { lineHeight: '2.8rem' }], + 'body-sm': ['1.4rem', { lineHeight: '2rem' }], + 'body-x-sm': ['1.2rem', { lineHeight: '1.6rem' }], + + /* Brand */ + 'brand-logo': ['2.8rem', { lineHeight: '3.6rem', fontWeight: '400' }], }, fontWeight: { + light: '300', normal: '400', - medium: '500', - semibold: '600', + bold: '700', }, letterSpacing: { - 'x-sm': '-0.02rem', - sm: '-0.01rem', - md: '0rem', - lg: '0rem', - 'x-lg': '0.02rem', - 'xx-lg': '0.04rem', + tight: '-0.03em', + label: '0.14em', + 'label-lg': '0.12em', }, borderRadius: { - none: '0rem', - 'x-sm': '0.2rem', - sm: '0.4rem', - md: '0.8rem', - lg: '1.6rem', + /* Orbit: no rounding anywhere. The status dot uses rounded-full. */ + DEFAULT: '0', + none: '0', + 'x-sm': '0', + sm: '0', + md: '0', + lg: '0', full: '9999px', }, spacing: { + /* + * Named scale kept for compatibility. Orbit uses the 4px grid members: + * tiny 4 · xxx-sm 8 · x-sm 12 · md 16 · xx-lg 24 · xxxx-lg 32 · xxx-huge 48 (x-tiny 2 for hairline gaps). + * Size tokens: bar 48 · control 32 · row 28 · bracket 12. + */ none: '0', 'x-tiny': '0.2rem', tiny: '0.4rem', @@ -75,6 +88,11 @@ const config = { 'xx-huge': '4.4rem', 'xxx-huge': '4.8rem', 'xxxx-huge': '5.2rem', + bar: '4.8rem', + control: '3.2rem', + row: '2.8rem', + bracket: '1.2rem', + key: '12.8rem', }, colors: { current: 'currentColor', @@ -83,7 +101,9 @@ const config = { surface: 'rgb(var(--color-surface) / )', 'surface-container': 'rgb(var(--color-surface-container) / )', + 'surface-container-high': 'rgb(var(--color-surface-container-high) / )', 'surface-inverted': 'rgb(var(--color-surface-inverted) / )', + line: 'rgb(var(--color-line) / )', 'on-surface': 'rgb(var(--color-on-surface) / )', 'on-surface-dim': 'rgb(var(--color-on-surface-dim) / )', @@ -91,9 +111,11 @@ const config = { accent: 'rgb(var(--color-accent) / )', 'accent-dim': 'rgb(var(--color-accent-dim) / )', + 'on-accent': 'rgb(var(--color-on-accent) / )', 'category-layout': 'rgb(var(--color-category-layout) / )', 'category-interactive': 'rgb(var(--color-category-interactive) / )', + 'category-interactive-fill': 'rgb(var(--color-category-interactive-fill) / )', info: 'rgb(var(--color-info) / )', 'info-container': 'rgb(var(--color-info-container) / )', @@ -103,6 +125,7 @@ const config = { 'error-container': 'rgb(var(--color-error-container) / )', success: 'rgb(var(--color-success) / )', 'success-container': 'rgb(var(--color-success-container) / )', + 'on-status': 'rgb(var(--color-on-status) / )', }, transitionDuration: { short: '200ms', From 8f409414ceb467700d900b791996a44263ce4db8 Mon Sep 17 00:00:00 2001 From: Mateusz Basiaga Date: Sat, 14 Nov 2026 03:00:46 +0100 Subject: [PATCH 2/4] feat(base): add BaseFrame and BaseStatusBadge BaseFrame is the Orbit block: a 1px line frame that gets signal corner brackets when active. BaseFrameBrackets is exported for cards that draw their own frame. BaseStatusBadge pairs the status dot with a mono state word, so color never carries the state alone. --- app/components/base/base-frame/BaseFrame.vue | 37 ++++++++++++++ .../base/base-frame/BaseFrameBrackets.vue | 40 +++++++++++++++ app/components/base/base-frame/index.ts | 22 ++++++++ .../base-status-badge/BaseStatusBadge.vue | 50 +++++++++++++++++++ .../base/base-status-badge/index.ts | 14 ++++++ 5 files changed, 163 insertions(+) create mode 100644 app/components/base/base-frame/BaseFrame.vue create mode 100644 app/components/base/base-frame/BaseFrameBrackets.vue create mode 100644 app/components/base/base-frame/index.ts create mode 100644 app/components/base/base-status-badge/BaseStatusBadge.vue create mode 100644 app/components/base/base-status-badge/index.ts diff --git a/app/components/base/base-frame/BaseFrame.vue b/app/components/base/base-frame/BaseFrame.vue new file mode 100644 index 0000000..20ef1e7 --- /dev/null +++ b/app/components/base/base-frame/BaseFrame.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/components/base/base-frame/BaseFrameBrackets.vue b/app/components/base/base-frame/BaseFrameBrackets.vue new file mode 100644 index 0000000..1d81dcb --- /dev/null +++ b/app/components/base/base-frame/BaseFrameBrackets.vue @@ -0,0 +1,40 @@ + + + diff --git a/app/components/base/base-frame/index.ts b/app/components/base/base-frame/index.ts new file mode 100644 index 0000000..f99b840 --- /dev/null +++ b/app/components/base/base-frame/index.ts @@ -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'; diff --git a/app/components/base/base-status-badge/BaseStatusBadge.vue b/app/components/base/base-status-badge/BaseStatusBadge.vue new file mode 100644 index 0000000..7167644 --- /dev/null +++ b/app/components/base/base-status-badge/BaseStatusBadge.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/components/base/base-status-badge/index.ts b/app/components/base/base-status-badge/index.ts new file mode 100644 index 0000000..56d0591 --- /dev/null +++ b/app/components/base/base-status-badge/index.ts @@ -0,0 +1,14 @@ +import type { ClassValue } from 'clsx'; + +export type BaseStatusBadgeState = 'idle' | 'live' | 'paused' | 'warning' | 'error' | 'offline' | 'ok' | 'info'; + +export interface BaseStatusBadgeProps { + state?: BaseStatusBadgeState; + /** The word is required: color never carries the state alone. */ + label: string; + /** Announces changes politely (aria-live). */ + live?: boolean; + class?: ClassValue; +} + +export { default as BaseStatusBadge } from './BaseStatusBadge.vue'; From 6733eea642bdf52c5112cc07fbf9b21c3491f926 Mon Sep 17 00:00:00 2001 From: Mateusz Basiaga Date: Sat, 14 Nov 2026 03:00:54 +0100 Subject: [PATCH 3/4] refactor(base): restyle base components to Orbit Drop hardcoded #050004, #22FF86, color-mix and opacity-based disabled states. Fields become 1px line frames with mono labels, the property card gets a thin white title and brackets, the toggle and color swatch are square, popover and option group follow the frame pattern, and icon buttons hover to the signal color. --- .../base-color-swatch/BaseColorSwatch.vue | 4 +-- .../base/base-icon-button/BaseIconButton.vue | 4 +-- .../base/base-icon-rail/BaseIconRail.vue | 7 ++--- .../BaseInspectorField.vue | 11 ++++---- .../base-option-button/BaseOptionButton.vue | 8 +++--- .../base-option-group/BaseOptionGroup.vue | 19 +++---------- .../base/base-popover/BasePopover.vue | 2 +- .../base-property-card/BasePropertyCard.vue | 18 ++++++------- .../base/base-reveal-list/BaseRevealList.vue | 2 +- .../base/base-scrub-field/BaseScrubField.vue | 27 ++++++++++--------- .../base/base-select-pill/BaseSelectPill.vue | 9 ++++--- .../base/base-text-button/BaseTextButton.vue | 7 ++++- .../base/base-toggle-dot/BaseToggleDot.vue | 13 ++++----- .../base/base-tree-item/BaseTreeItem.vue | 2 +- .../base/base-value-field/BaseValueField.vue | 16 +++++------ .../inspector/IconInspectorEyeFilled.vue | 2 +- .../inspector/IconInspectorLockOpenFilled.vue | 2 +- sandbox/app/pages/index.vue | 6 ++--- 18 files changed, 79 insertions(+), 80 deletions(-) diff --git a/app/components/base/base-color-swatch/BaseColorSwatch.vue b/app/components/base/base-color-swatch/BaseColorSwatch.vue index 6c4ed27..d0fcea4 100644 --- a/app/components/base/base-color-swatch/BaseColorSwatch.vue +++ b/app/components/base/base-color-swatch/BaseColorSwatch.vue @@ -32,14 +32,14 @@ function onPickerChange(event: Event) {
{{ label }}