diff --git a/Frontend/src/app.tsx b/Frontend/src/app.tsx index 264a699..9d79022 100644 --- a/Frontend/src/app.tsx +++ b/Frontend/src/app.tsx @@ -1,7 +1,11 @@ // Path: Frontend/src/app.tsx +import type { JSX } from "solid-js"; +import { AppShell } from "./components/shell/AppShell/AppShell"; import "./styles/main.scss"; -export default function App() { - return ; -} +const App = (): JSX.Element => { + return ; +}; + +export default App; diff --git a/Frontend/src/components/shell/AppShell/AppShell.module.scss b/Frontend/src/components/shell/AppShell/AppShell.module.scss new file mode 100644 index 0000000..4c1ac03 --- /dev/null +++ b/Frontend/src/components/shell/AppShell/AppShell.module.scss @@ -0,0 +1,135 @@ +.shell { + height: 100dvh; + min-height: 100dvh; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + background: var(--color-canvas); + color: var(--color-text); +} + +.body { + --rail-width: 4.75rem; + --sidebar-width: 16.75rem; + --shell-top-left-radius: calc(var(--radius-xl) + var(--space-1)); + --shell-frame-border: color-mix(in srgb, var(--color-border-strong) 44%, transparent); + --shell-divider-border: color-mix(in srgb, var(--color-border-strong) 34%, transparent); + --sidebar-panel-surface: color-mix(in srgb, var(--color-surface-muted) 92%, transparent); + --workspace-panel-surface: color-mix(in srgb, var(--color-canvas) 94%, var(--color-surface)); + min-height: 0; + display: grid; + grid-template-columns: var(--rail-width) minmax(0, 1fr); + overflow: hidden; + background: var(--color-surface); +} + +.railColumn { + min-height: 0; + display: flex; + position: relative; + z-index: 1; + background: var(--color-surface); +} + +.workspaceRegion { + position: relative; + min-width: 0; + min-height: 0; + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + overflow: visible; + z-index: 1; + isolation: isolate; + border-top-left-radius: var(--shell-top-left-radius); + border-top-right-radius: 0; +} + +.workspaceRegion::before { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient( + to right, + var(--sidebar-panel-surface) 0, + var(--sidebar-panel-surface) calc(var(--sidebar-width) - 0.5px), + var(--workspace-panel-surface) calc(var(--sidebar-width) - 0.5px), + var(--workspace-panel-surface) 100% + ); + border: 1px solid var(--shell-frame-border); + border-top: 0; + border-top-left-radius: var(--shell-top-left-radius); + border-top-right-radius: 0; + box-shadow: inset 0 1px 0 color-mix(in srgb, white 3%, transparent); + pointer-events: none; + z-index: 0; +} + +.sidebarColumn { + position: relative; + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: minmax(0, 1fr); + overflow: visible; + z-index: 1; + background: var(--sidebar-panel-surface); + border-top: 1px solid var(--shell-frame-border); + border-left: 1px solid var(--shell-frame-border); + border-top-left-radius: var(--shell-top-left-radius); +} + +.workspaceMain { + min-width: 0; + min-height: 0; + position: relative; + overflow: hidden; + z-index: 1; + border-top: 1px solid var(--shell-frame-border); + border-left: 1px solid var(--shell-divider-border); + background: var(--workspace-panel-surface); + border-top-right-radius: 0; +} + +.sidebarDock { + position: absolute; + right: var(--space-1); + bottom: var(--space-3); + left: calc(var(--space-1) - (var(--rail-width) * 0.9)); + z-index: calc(var(--z-modal) + 1); + pointer-events: none; + + > * { + pointer-events: auto; + } +} + +@include respond-up(mobile) { + .body { + --rail-width: 5rem; + --sidebar-width: 17.25rem; + } +} + +@include respond-down(tablet) { + .body { + --rail-width: 4.5rem; + --sidebar-width: 13.25rem; + } +} + +@include respond-down(mobile) { + .body { + grid-template-columns: 4.5rem minmax(0, 1fr); + --rail-width: 4.5rem; + } + + .railColumn { + position: sticky; + top: 0; + } + + .workspaceRegion, + .sidebarDock { + display: none; + } +} diff --git a/Frontend/src/components/shell/AppShell/AppShell.tsx b/Frontend/src/components/shell/AppShell/AppShell.tsx new file mode 100644 index 0000000..9d0b58f --- /dev/null +++ b/Frontend/src/components/shell/AppShell/AppShell.tsx @@ -0,0 +1,50 @@ +// Path: Frontend/src/components/shell/AppShell/AppShell.tsx + +import { createSignal, onMount, type JSX } from "solid-js"; +import { getDocumentTheme, setTheme, type Theme } from "../../../helpers/theme"; +import { WorkspaceHome } from "../../workspace-home/WorkspaceHome/WorkspaceHome"; +import { LeftRail } from "../LeftRail/LeftRail"; +import { ProfileDock } from "../ProfileDock/ProfileDock"; +import { TopBar } from "../TopBar/TopBar"; +import { WorkspaceSidebar } from "../WorkspaceSidebar/WorkspaceSidebar"; +import styles from "./AppShell.module.scss"; + +export const AppShell = (): JSX.Element => { + const [themeState, setThemeState] = createSignal("light"); + + onMount((): void => { + setThemeState(getDocumentTheme()); + }); + + const toggleTheme = (): void => { + const next: Theme = themeState() === "dark" ? "light" : "dark"; + + setTheme(next); + setThemeState(next); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/Frontend/src/components/shell/LeftRail/LeftRail.module.scss b/Frontend/src/components/shell/LeftRail/LeftRail.module.scss new file mode 100644 index 0000000..04fbd9c --- /dev/null +++ b/Frontend/src/components/shell/LeftRail/LeftRail.module.scss @@ -0,0 +1,82 @@ +.rail { + --rail-workspace-size: var(--control-size-lg); + --rail-action-size: var(--control-size-md); + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-2); + overflow: hidden; +} + +.topCluster, +.bottomCluster { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); +} + +.items { + width: 100%; + min-height: 0; + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); + overflow-y: auto; + overscroll-behavior: contain; + padding-block: var(--space-1); +} + +.logo { + width: var(--rail-workspace-size); + height: var(--rail-workspace-size); + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-lg); + background: var(--color-accent); + color: var(--color-accent-contrast); + font-weight: 700; + letter-spacing: -0.02em; +} + +.workspaceButton { + width: var(--rail-workspace-size); + height: var(--rail-workspace-size); + display: inline-flex; + align-items: center; + justify-content: center; + @include interactive-frame(var(--color-surface-muted), var(--color-border), var(--color-text-muted), var(--radius-lg)); + @include text-label; + @include interactive-frame-hover(); +} + +.workspaceButtonActive { + background: var(--color-accent); + border-color: transparent; + color: var(--color-accent-contrast); + box-shadow: var(--shadow-soft); +} + +.addButton { + width: var(--rail-action-size); + height: var(--rail-action-size); + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px dashed var(--color-border-strong); + border-radius: var(--radius-pill); + background: transparent; + color: var(--color-text-muted); + + &:hover { + background: var(--color-surface-hover); + color: var(--color-text); + } +} diff --git a/Frontend/src/components/shell/LeftRail/LeftRail.tsx b/Frontend/src/components/shell/LeftRail/LeftRail.tsx new file mode 100644 index 0000000..32034a9 --- /dev/null +++ b/Frontend/src/components/shell/LeftRail/LeftRail.tsx @@ -0,0 +1,42 @@ +// Path: Frontend/src/components/shell/LeftRail/LeftRail.tsx + +import { For, type JSX } from "solid-js"; +import { Plus } from "../../../lib/icons"; +import { railItems } from "../data/shell.data"; +import styles from "./LeftRail.module.scss"; + +export const LeftRail = (): JSX.Element => { + return ( + + ); +}; diff --git a/Frontend/src/components/shell/ProfileDock/ProfileDock.module.scss b/Frontend/src/components/shell/ProfileDock/ProfileDock.module.scss new file mode 100644 index 0000000..11b47a6 --- /dev/null +++ b/Frontend/src/components/shell/ProfileDock/ProfileDock.module.scss @@ -0,0 +1,85 @@ +.panel { + --profile-dock-avatar-size: var(--control-size-md); + --profile-dock-action-min-height: var(--space-8); + --profile-dock-border: color-mix(in srgb, var(--color-border-strong) 75%, transparent); + --profile-dock-surface: color-mix(in srgb, var(--color-surface) 94%, transparent); + --profile-dock-status-ring: 0 0 0 3px color-mix(in srgb, var(--color-success) 18%, transparent); + position: relative; + z-index: 1; + width: 100%; + display: grid; + gap: var(--space-2); + padding: var(--space-3) var(--space-3) var(--space-2); + border: 1px solid var(--profile-dock-border); + border-radius: calc(var(--radius-xl) + var(--space-1)); + background: var(--profile-dock-surface); + box-shadow: + 0 20px 48px color-mix(in srgb, black 16%, transparent), + var(--shadow-strong); + backdrop-filter: blur(var(--blur-overlay)); +} + +.identity { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--space-3); + align-items: center; +} + +.avatar { + width: var(--profile-dock-avatar-size); + height: var(--profile-dock-avatar-size); + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--color-accent-soft); + color: var(--color-accent-strong); + @include text-label; +} + +.copy { + min-width: 0; + display: grid; + gap: 0.15rem; +} + +.name { + @include text-label; +} + +.status { + @include text-caption; + display: inline-flex; + align-items: center; + gap: var(--space-2); + color: var(--color-text-muted); +} + +.statusDot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: var(--color-success); + box-shadow: var(--profile-dock-status-ring); +} + +.actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-2); +} + +.action { + min-height: var(--profile-dock-action-min-height); + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-1); + @include interactive-frame(var(--color-surface-muted)); + @include interactive-frame-hover(); +} + +.actionLabel { + @include text-caption; +} diff --git a/Frontend/src/components/shell/ProfileDock/ProfileDock.tsx b/Frontend/src/components/shell/ProfileDock/ProfileDock.tsx new file mode 100644 index 0000000..1f005a3 --- /dev/null +++ b/Frontend/src/components/shell/ProfileDock/ProfileDock.tsx @@ -0,0 +1,35 @@ +// Path: Frontend/src/components/shell/ProfileDock/ProfileDock.tsx + +import type { JSX } from "solid-js"; +import { Settings, User } from "../../../lib/icons"; +import styles from "./ProfileDock.module.scss"; + +export const ProfileDock = (): JSX.Element => { + return ( + + + + R + + + Ronald + + + Online in Moku + + + + + + + + Account + + + + Prefs + + + + ); +}; diff --git a/Frontend/src/components/shell/TopBar/TopBar.module.scss b/Frontend/src/components/shell/TopBar/TopBar.module.scss new file mode 100644 index 0000000..0436b5f --- /dev/null +++ b/Frontend/src/components/shell/TopBar/TopBar.module.scss @@ -0,0 +1,85 @@ +.topBar { + --topbar-control-size: var(--control-size-md); + min-height: 4rem; + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4) var(--space-3); + background: var(--color-surface); +} + +.identity { + min-width: 0; + display: grid; + gap: 0; +} + +.eyebrow { + @include text-caption; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.title { + @include text-title; + display: flex; + align-items: center; + gap: var(--space-2); + + strong { + font: inherit; + font-weight: var(--font-weight-title); + } +} + +.context { + color: var(--color-text-muted); +} + +.actions { + display: flex; + align-items: center; + gap: var(--space-1); +} + +.actionButton, +.themeButton { + height: var(--topbar-control-size); + display: inline-flex; + align-items: center; + justify-content: center; + @include interactive-frame(); +} + +.actionButton { + width: var(--topbar-control-size); +} + +.themeButton { + width: auto; + padding-inline: var(--space-2); + gap: var(--space-1); + color: var(--color-text); +} + +.actionButton, +.themeButton { + @include interactive-frame-hover(); +} + +.themeLabel { + @include text-label; +} + +@include respond-down(mobile) { + .topBar { + grid-template-columns: minmax(0, 1fr) auto; + padding: var(--space-2) var(--space-3) var(--space-3); + } + + .actions { + display: none; + } +} diff --git a/Frontend/src/components/shell/TopBar/TopBar.tsx b/Frontend/src/components/shell/TopBar/TopBar.tsx new file mode 100644 index 0000000..d143661 --- /dev/null +++ b/Frontend/src/components/shell/TopBar/TopBar.tsx @@ -0,0 +1,45 @@ +// Path: Frontend/src/components/shell/TopBar/TopBar.tsx + +import { For, type JSX } from "solid-js"; +import type { Theme } from "../../../helpers/theme"; +import { ChevronDown } from "../../../lib/icons"; +import { topBarActions } from "../data/shell.data"; +import styles from "./TopBar.module.scss"; + +type TopBarProps = { + theme: Theme; + onToggleTheme: VoidFunction; +}; + +export const TopBar = (props: TopBarProps): JSX.Element => { + return ( + + + Moku Work + + Workspace Shell + Moku / Product + + + + + + {props.theme === "dark" ? "Dark" : "Light"} + + + + + {(item): JSX.Element => { + const Icon = item.icon; + + return ( + + + + ); + }} + + + + ); +}; diff --git a/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.module.scss b/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.module.scss new file mode 100644 index 0000000..789e8da --- /dev/null +++ b/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.module.scss @@ -0,0 +1,103 @@ +.sidebar { + --sidebar-nav-item-min-height: var(--control-size-lg); + --sidebar-dock-clearance: 8rem; + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: auto auto minmax(0, 1fr); + gap: var(--space-4); + padding: var(--space-4); + overflow: hidden; +} + +.header { + display: grid; + gap: 0.2rem; +} + +.eyebrow { + @include text-caption; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.title { + @include text-title; +} + +.meta { + @include text-caption; + color: var(--color-text-muted); + max-width: 28ch; +} + +.section { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: var(--space-2); + min-height: 0; +} + +.navScroller { + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + padding-right: var(--space-1); + padding-bottom: calc(var(--space-4) + var(--sidebar-dock-clearance)); + margin-right: calc(var(--space-1) * -1); +} + +.sectionLabel { + @include text-label; + color: var(--color-text-muted); +} + +.navList { + list-style: none; + display: grid; + gap: var(--space-1); + padding: 0; +} + +.navItem { + width: 100%; + min-width: 0; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: var(--space-2); + min-height: var(--sidebar-nav-item-min-height); + padding: var(--space-2) var(--space-3); + @include interactive-frame(transparent, transparent, var(--color-text-muted), var(--radius-lg)); + text-align: left; + @include interactive-frame-hover(var(--color-surface-hover), transparent, var(--color-text)); +} + +.navItemActive { + border-color: var(--color-border); + background: var(--color-surface); + color: var(--color-text); + box-shadow: var(--shadow-soft); +} + +.icon { + color: inherit; + opacity: 0.85; +} + +.label { + @include text-label; + min-width: 0; +} + +.itemMeta { + @include text-caption; + color: var(--color-text-muted); +} + +@include respond-down(mobile) { + .sidebar { + display: none; + } +} diff --git a/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx b/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx new file mode 100644 index 0000000..579bee5 --- /dev/null +++ b/Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -0,0 +1,48 @@ +// Path: Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx + +import { For, Show, type JSX } from "solid-js"; +import { workspaceSidebarItems } from "../data/shell.data"; +import styles from "./WorkspaceSidebar.module.scss"; + +export const WorkspaceSidebar = (): JSX.Element => { + return ( + + ); +}; diff --git a/Frontend/src/components/shell/data/shell.data.ts b/Frontend/src/components/shell/data/shell.data.ts new file mode 100644 index 0000000..a3b92fd --- /dev/null +++ b/Frontend/src/components/shell/data/shell.data.ts @@ -0,0 +1,53 @@ +// Path: Frontend/src/components/shell/data/shell.data.ts + +import type { Component } from "solid-js"; +import { Bell, Folder, Home, LayoutGrid, Plus, Search, Settings, User } from "../../../lib/icons"; + +type ShellIconProps = { + class?: string; + size?: number; + strokeWidth?: number; +}; + +export type ShellIcon = Component; + +export type RailItem = { + id: string; + label: string; + abbreviation: string; + active?: boolean; +}; + +export type SidebarItem = { + id: string; + label: string; + icon: ShellIcon; + active?: boolean; + meta?: string; +}; + +export type TopBarAction = { + id: string; + label: string; + icon: ShellIcon; +}; + +export const railItems: readonly RailItem[] = [ + { id: "personal", label: "Personal", abbreviation: "P" }, + { id: "moku", label: "Moku", abbreviation: "M", active: true }, + { id: "labs", label: "Labs", abbreviation: "L" }, +] as const; + +export const workspaceSidebarItems: readonly SidebarItem[] = [ + { id: "home", label: "Home", icon: Home, active: true }, + { id: "boards", label: "Boards", icon: LayoutGrid, meta: "0" }, + { id: "docs", label: "Docs", icon: Folder, meta: "0" }, + { id: "settings", label: "Settings", icon: Settings }, +] as const; + +export const topBarActions: readonly TopBarAction[] = [ + { id: "search", label: "Search", icon: Search }, + { id: "create", label: "Create", icon: Plus }, + { id: "inbox", label: "Inbox", icon: Bell }, + { id: "profile", label: "Profile", icon: User }, +] as const; diff --git a/Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.module.scss b/Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.module.scss new file mode 100644 index 0000000..1c4805e --- /dev/null +++ b/Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.module.scss @@ -0,0 +1,79 @@ +.viewport { + --workspace-content-max-width: var(--content-width-wide); + --workspace-card-min-height: calc(var(--space-12) * 3); + min-width: 0; + min-height: 0; + display: grid; + align-content: start; + gap: var(--space-5); + padding: var(--space-5) var(--space-6); +} + +.hero { + display: grid; + gap: var(--space-3); + width: 100%; + max-width: var(--workspace-content-max-width); +} + +.eyebrow { + @include text-caption; + color: var(--color-text-muted); + text-transform: uppercase; +} + +.title { + @include text-display; + font-family: var(--font-family-display); + max-width: 12ch; +} + +.description { + max-width: 64ch; + color: var(--color-text-muted); +} + +.grid { + width: 100%; + max-width: var(--workspace-content-max-width); + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-4); +} + +.card { + display: grid; + gap: var(--space-2); + min-height: var(--workspace-card-min-height); + padding: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + background: var(--color-surface); + box-shadow: var(--shadow-soft); +} + +.cardTitle { + @include text-title; +} + +.cardCopy { + color: var(--color-text-muted); +} + +.cardMeta { + @include text-caption; + color: var(--color-text-muted); +} + +@include respond-down(tablet) { + .grid { + grid-template-columns: 1fr; + } +} + +@include respond-down(mobile) { + .viewport { + gap: var(--space-4); + padding: var(--space-4); + } +} diff --git a/Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.tsx b/Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.tsx new file mode 100644 index 0000000..4fab78f --- /dev/null +++ b/Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.tsx @@ -0,0 +1,54 @@ +// Path: Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.tsx + +import { For, type JSX } from "solid-js"; +import styles from "./WorkspaceHome.module.scss"; + +type ShellCheckpointCard = { + title: string; + copy: string; + meta: string; +}; + +const shellCheckpointCards: readonly ShellCheckpointCard[] = [ + { + title: "App shell", + copy: "Top bar, left rail, workspace sidebar, and content viewport are now split into modular components.", + meta: "Layout foundation", + }, + { + title: "Workspace context", + copy: "The shell already has clear places for org context, workspace switching, and future surface navigation.", + meta: "Navigation foundation", + }, + { + title: "Next build target", + copy: "You can now plug in workspace home content, auth state, and early primitives without redesigning the whole frame.", + meta: "Ready for v0.1.0 work", + }, +]; + +export const WorkspaceHome = (): JSX.Element => { + return ( + + + Workspace home + Moku is ready for its first real shell. + + This is the barebone app frame for v0.1.0 — enough structure to start building real frontend surfaces on top of a real backend core. + + + + + + {(card): JSX.Element => ( + + {card.title} + {card.copy} + {card.meta} + + )} + + + + ); +}; diff --git a/Frontend/src/styles/themes/_tokens.scss b/Frontend/src/styles/themes/_tokens.scss index 435a110..6f4b5b9 100644 --- a/Frontend/src/styles/themes/_tokens.scss +++ b/Frontend/src/styles/themes/_tokens.scss @@ -37,6 +37,11 @@ --radius-xl: 1.25rem; --radius-pill: 999px; + --control-size-md: 2.25rem; + --control-size-lg: 2.5rem; + --content-width-wide: 72rem; + --blur-overlay: 18px; + --shadow-soft: 0 12px 32px hsl(220 30% 10% / 0.08); --shadow-strong: 0 20px 48px hsl(220 30% 10% / 0.16); diff --git a/Frontend/src/styles/tools/_mixins.scss b/Frontend/src/styles/tools/_mixins.scss index adc3d61..fed1701 100644 --- a/Frontend/src/styles/tools/_mixins.scss +++ b/Frontend/src/styles/tools/_mixins.scss @@ -18,6 +18,47 @@ } } +@mixin respond-down($breakpoint) { + @if $breakpoint == mobile { + @media (max-width: calc($breakpoint-mobile - 0.01rem)) { + @content; + } + } @else if $breakpoint == tablet { + @media (max-width: calc($breakpoint-tablet - 0.01rem)) { + @content; + } + } @else if $breakpoint == desktop { + @media (max-width: calc($breakpoint-desktop - 0.01rem)) { + @content; + } + } +} + +@mixin interactive-frame( + $background: var(--color-surface), + $border: var(--color-border), + $color: var(--color-text-muted), + $radius: var(--radius-md) +) { + border: 1px solid $border; + border-radius: $radius; + background: $background; + color: $color; +} + +@mixin interactive-frame-hover( + $background: var(--color-surface-hover), + $border: var(--color-border-strong), + $color: var(--color-text) +) { + &:hover { + background: $background; + border-color: $border; + color: $color; + text-decoration: none; + } +} + @mixin text-caption { font-size: var(--font-size-caption); font-weight: var(--font-weight-caption);
+ This is the barebone app frame for v0.1.0 — enough structure to start building real frontend surfaces on top of a real backend core. +
{card.copy}