feat: add Card and Navbar components with styles

- Implemented Card component with cover, title, subtitle, content, and footer slots.
- Added styles for Card component in _card.scss.
- Created Navbar component with brand and links, including styles in _navbar.scss.
- Updated index.astro to include new Card and Navbar components.
This commit is contained in:
2026-06-23 11:40:04 +02:00
parent b48581b089
commit 1878ec3a0e
5 changed files with 676 additions and 237 deletions
+53
View File
@@ -0,0 +1,53 @@
@use "../../styles/tokens/typography" as *;
.card {
display: flex;
flex-direction: column;
background-color: var(--nds-surface);
border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-lg);
overflow: hidden;
box-shadow: var(--nds-shadow-sm);
width: fit-content;
transition:
box-shadow 160ms ease,
border-color 160ms ease;
&:hover {
box-shadow: var(--nds-shadow-md);
}
&__cover {
height: 150px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
&__body {
display: flex;
flex-direction: column;
gap: var(--nds-spacing-xs);
padding: var(--nds-spacing-md);
}
&__title {
@include text-xl;
color: var(--nds-text);
margin: 0;
}
&__subtitle {
@include text-base;
color: var(--nds-neutral);
margin: 0;
}
&__content {
@include text-base;
color: var(--nds-text);
&:empty {
display: none;
}
}
&__footer {
margin-top: var(--nds-spacing-2xs);
display: flex;
gap: var(--nds-spacing-xs);
}
}
+33
View File
@@ -0,0 +1,33 @@
---
export interface Props {
title?: string;
subtitle?: string;
cover?: string;
}
const { title, subtitle, cover } = Astro.props;
const hasFooter = Astro.slots.has("footer");
---
<div class="card">
{
cover && (
<div class="card__cover" style={`background-image: url(${cover})`} />
)
}
<div class="card__body">
{title && <p class="card__title">{title}</p>}
{subtitle && <p class="card__subtitle">{subtitle}</p>}
<div class="card__content"><slot /></div>
{
hasFooter && (
<div class="card__footer">
<slot name="footer" />
</div>
)
}
</div>
</div>
<style lang="scss">
@use "./_card.scss";
</style>
+55
View File
@@ -0,0 +1,55 @@
@use "../../styles/tokens/typography" as *;
.navbar {
display: flex;
align-items: center;
gap: var(--nds-spacing-lg);
height: 60px;
padding: 0 var(--nds-spacing-lg);
background-color: var(--nds-surface);
border-bottom: var(--nds-border-width-thin) solid var(--nds-border);
&__brand {
display: flex;
align-items: center;
gap: var(--nds-spacing-xs);
}
&__mark {
width: 24px;
height: 24px;
border-radius: var(--nds-radius-md);
background: linear-gradient(135deg, var(--nds-primary), var(--nds-accent));
}
&__title {
@include text-xl;
font-size: 1rem;
color: var(--nds-text);
}
&__links {
display: flex;
align-items: center;
gap: var(--nds-spacing-2xs);
flex: 1;
}
&__link {
padding: var(--nds-spacing-2xs) var(--nds-spacing-sm);
border-radius: var(--nds-radius-md);
@include text-label;
color: var(--nds-neutral);
text-decoration: none;
transition: all 120ms ease;
&:hover {
color: var(--nds-text);
background-color: var(--nds-surface-hover);
}
&--active {
color: var(--nds-text);
}
}
&__right {
display: flex;
align-items: center;
gap: var(--nds-spacing-sm);
}
}
+19
View File
@@ -0,0 +1,19 @@
---
export interface Props {
brand?: string;
}
const { brand = "Nova" } = Astro.props;
---
<header class="navbar">
<div class="navbar__brand">
<span class="navbar__mark"></span>
<span class="navbar__title">{brand}</span>
</div>
<nav class="navbar__links"><slot /></nav>
<div class="navbar__right"><slot name="right" /></div>
</header>
<style lang="scss">
@use "./_navbar.scss";
</style>
+439 -160
View File
@@ -1,65 +1,108 @@
--- ---
import Layout from '../layouts/Layout.astro'; import Layout from "../layouts/Layout.astro";
// Core // Core
import Button from '../components/Button/button.astro'; import Button from "../components/Button/button.astro";
import Badge from '../components/Badge/badge.astro'; import Badge from "../components/Badge/badge.astro";
import Avatar from '../components/Avatar/avatar.astro'; import Avatar from "../components/Avatar/avatar.astro";
// Forms // Forms
import Toggle from '../components/Toggle/toggle.astro'; import Toggle from "../components/Toggle/toggle.astro";
import Checkbox from '../components/Checkbox/checkbox.astro'; import Checkbox from "../components/Checkbox/checkbox.astro";
import Radio from '../components/Radio/radio.astro'; import Radio from "../components/Radio/radio.astro";
import TextField from '../components/textField/textField.astro'; import TextField from "../components/textField/textField.astro";
import Select from '../components/Select/select.astro'; import Select from "../components/Select/select.astro";
import SelectOption from '../components/Select/selectOption.astro'; import SelectOption from "../components/Select/selectOption.astro";
import NumericStepper from '../components/numericStepper/numericStepper.astro'; import NumericStepper from "../components/numericStepper/numericStepper.astro";
// Navigation // Navigation
import Tab from '../components/Tabs/tab.astro'; import Tab from "../components/Tabs/tab.astro";
import TabItem from '../components/Tabs/tabItem.astro'; import TabItem from "../components/Tabs/tabItem.astro";
import TabContent from '../components/Tabs/tabContent.astro'; import TabContent from "../components/Tabs/tabContent.astro";
import Link from '../components/Link/link.astro'; import Link from "../components/Link/link.astro";
import Breadcrumb from '../components/Breadcrumb/breadcrumb.astro'; import Breadcrumb from "../components/Breadcrumb/breadcrumb.astro";
import BreadcrumbItem from '../components/Breadcrumb/breadcrumbItem.astro'; import BreadcrumbItem from "../components/Breadcrumb/breadcrumbItem.astro";
import Pagination from '../components/pagination/pagination.astro'; import Pagination from "../components/pagination/pagination.astro";
import PaginationNumber from '../components/pagination/paginationNumber.astro'; import PaginationNumber from "../components/pagination/paginationNumber.astro";
import Navbar from '../components/Navbar/navbar.astro'; import Navbar from "../components/Navbar/navbar.astro";
import Sidebar from '../components/Sidebar/sidebar.astro'; import Sidebar from "../components/Sidebar/sidebar.astro";
import SidebarItem from '../components/Sidebar/sidebarItem.astro'; import SidebarItem from "../components/Sidebar/sidebarItem.astro";
// Data // Data
import Card from '../components/Card/card.astro'; import Card from "../components/Card/card.astro";
import ListItem from '../components/ListItem/listItem.astro'; import ListItem from "../components/ListItem/listItem.astro";
import ListItemTitle from '../components/ListItem/listItemTitle.astro'; import ListItemTitle from "../components/ListItem/listItemTitle.astro";
import ListItemSubtitle from '../components/ListItem/listItemSubtitle.astro'; import ListItemSubtitle from "../components/ListItem/listItemSubtitle.astro";
// Feedback // Feedback
import Notification from '../components/Notifications/notification.astro'; import Notification from "../components/Notifications/notification.astro";
import LoadingBar from '../components/LoadingBar/loadingBar.astro'; import LoadingBar from "../components/LoadingBar/loadingBar.astro";
import Tooltip from '../components/Tooltip/tooltip.astro'; import Tooltip from "../components/Tooltip/tooltip.astro";
import Modal from '../components/Modal/modal.astro'; import Modal from "../components/Modal/modal.astro";
import { import {
Arrow2Icon, BinIcon, BurgerIcon, CalendarIcon, CheckIcon, CloseIcon, Arrow2Icon,
CodeIcon, CubeIcon, DownloadIcon, FilterIcon, HelpIcon, HomeIcon, BinIcon,
LinkIcon, MinusIcon, MoreIcon, OverviewIcon, PlusIcon, ProfileIcon, BurgerIcon,
SearchIcon, SettingsIcon, ShareIcon, ShieldIcon, SortIcon, StatsIcon, CalendarIcon,
CheckIcon,
CloseIcon,
CodeIcon,
CubeIcon,
DownloadIcon,
FilterIcon,
HelpIcon,
HomeIcon,
LinkIcon,
MinusIcon,
MoreIcon,
OverviewIcon,
PlusIcon,
ProfileIcon,
SearchIcon,
SettingsIcon,
ShareIcon,
ShieldIcon,
SortIcon,
StatsIcon,
UploadIcon, UploadIcon,
} from '../components/Icons/index.ts'; } from "../components/Icons/index.ts";
const tones = ['primary', 'neutral', 'success', 'warning', 'error', 'info'] as const; const tones = [
const buttonTypes = ['primary', 'secondary', 'ghost', 'danger'] as const; "primary",
const sizes = ['sm', 'md', 'lg'] as const; "neutral",
"success",
"warning",
"error",
"info",
] as const;
const buttonTypes = ["primary", "secondary", "ghost", "danger"] as const;
const sizes = ["sm", "md", "lg"] as const;
const libraryIcons = [ const libraryIcons = [
['Profile', ProfileIcon], ['Search', SearchIcon], ['Settings', SettingsIcon], ["Profile", ProfileIcon],
['Home', HomeIcon], ['Overview', OverviewIcon], ['Stats', StatsIcon], ["Search", SearchIcon],
['Share', ShareIcon], ['Shield', ShieldIcon], ['Download', DownloadIcon], ["Settings", SettingsIcon],
['Upload', UploadIcon], ['Filter', FilterIcon], ['Bin', BinIcon], ["Home", HomeIcon],
['Burger', BurgerIcon], ['Calendar', CalendarIcon], ['Check', CheckIcon], ["Overview", OverviewIcon],
['Close', CloseIcon], ['Code', CodeIcon], ['Cube', CubeIcon], ["Stats", StatsIcon],
['Help', HelpIcon], ['Link', LinkIcon], ['Minus', MinusIcon], ["Share", ShareIcon],
['More', MoreIcon], ['Plus', PlusIcon], ['Sort', SortIcon], ["Shield", ShieldIcon],
["Download", DownloadIcon],
["Upload", UploadIcon],
["Filter", FilterIcon],
["Bin", BinIcon],
["Burger", BurgerIcon],
["Calendar", CalendarIcon],
["Check", CheckIcon],
["Close", CloseIcon],
["Code", CodeIcon],
["Cube", CubeIcon],
["Help", HelpIcon],
["Link", LinkIcon],
["Minus", MinusIcon],
["More", MoreIcon],
["Plus", PlusIcon],
["Sort", SortIcon],
] as const; ] as const;
--- ---
@@ -78,71 +121,147 @@ const libraryIcons = [
<section class="block" id="button"> <section class="block" id="button">
<h2>Button</h2> <h2>Button</h2>
<p class="block__desc">Variants, sizes, icon and disabled states.</p> <p class="block__desc">Variants, sizes, icon and disabled states.</p>
<div class="vgroup"><span class="vgroup__label">Variants × md</span><div class="vgroup__body"> <div class="vgroup">
{buttonTypes.map((t) => <Button type={t} size="md">{t[0].toUpperCase() + t.slice(1)}</Button>)} <span class="vgroup__label">Variants × md</span><div
</div></div> class="vgroup__body"
<div class="vgroup"><span class="vgroup__label">Sizes (primary)</span><div class="vgroup__body"> >
{sizes.map((s) => <Button type="primary" size={s}>Size {s}</Button>)} {
</div></div> buttonTypes.map((t) => (
<div class="vgroup"><span class="vgroup__label">With icon · icon-only</span><div class="vgroup__body"> <Button type={t} size="md">
<Button type="primary" size="md"><PlusIcon slot="icon-left" size={16} />New service</Button> {t[0].toUpperCase() + t.slice(1)}
<Button type="secondary" size="md">Filter<FilterIcon slot="icon-right" size={16} /></Button> </Button>
<Button type="ghost" size="md" icon={true}><MoreIcon size={18} /></Button> ))
</div></div> }
<div class="vgroup"><span class="vgroup__label">Disabled</span><div class="vgroup__body"> </div>
{buttonTypes.map((t) => <Button type={t} size="md" disabled>{t}</Button>)} </div>
</div></div> <div class="vgroup">
<span class="vgroup__label">Sizes (primary)</span><div
class="vgroup__body"
>
{
sizes.map((s) => (
<Button type="primary" size={s}>
Size {s}
</Button>
))
}
</div>
</div>
<div class="vgroup">
<span class="vgroup__label">With icon · icon-only</span><div
class="vgroup__body"
>
<Button type="primary" size="md"
><PlusIcon slot="icon-left" size={16} />New service</Button
>
<Button type="secondary" size="md"
>Filter<FilterIcon slot="icon-right" size={16} /></Button
>
<Button type="ghost" size="md" icon={true}
><MoreIcon size={18} /></Button
>
</div>
</div>
<div class="vgroup">
<span class="vgroup__label">Disabled</span><div class="vgroup__body">
{
buttonTypes.map((t) => (
<Button type={t} size="md" disabled>
{t}
</Button>
))
}
</div>
</div>
</section> </section>
<!-- BADGE --> <!-- BADGE -->
<section class="block" id="badge"> <section class="block" id="badge">
<h2>Badge</h2> <h2>Badge</h2>
<p class="block__desc">Tones × soft / solid.</p> <p class="block__desc">Tones × soft / solid.</p>
<div class="vgroup"><span class="vgroup__label">Soft</span><div class="vgroup__body"> <div class="vgroup">
{tones.map((t) => <Badge type={t} variant="soft">{t}</Badge>)} <span class="vgroup__label">Soft</span><div class="vgroup__body">
</div></div> {
<div class="vgroup"><span class="vgroup__label">Solid</span><div class="vgroup__body"> tones.map((t) => (
{tones.map((t) => <Badge type={t} variant="solid">{t}</Badge>)} <Badge type={t} variant="soft">
</div></div> {t}
</Badge>
))
}
</div>
</div>
<div class="vgroup">
<span class="vgroup__label">Solid</span><div class="vgroup__body">
{
tones.map((t) => (
<Badge type={t} variant="solid">
{t}
</Badge>
))
}
</div>
</div>
</section> </section>
<!-- SELECTION CONTROLS --> <!-- SELECTION CONTROLS -->
<section class="block" id="controls"> <section class="block" id="controls">
<h2>Toggle · Checkbox · Radio</h2> <h2>Toggle · Checkbox · Radio</h2>
<p class="block__desc">Selection controls in every state.</p> <p class="block__desc">Selection controls in every state.</p>
<div class="vgroup"><span class="vgroup__label">Toggle — off / on</span><div class="vgroup__body"> <div class="vgroup">
<span class="vgroup__label">Toggle — off / on</span><div
class="vgroup__body"
>
<Toggle id="tg-off" /> <Toggle id="tg-off" />
<Toggle id="tg-on" data-checked={true} /> <Toggle id="tg-on" data-checked={true} />
</div></div> </div>
<div class="vgroup"><span class="vgroup__label">Checkbox</span><div class="vgroup__body"> </div>
<div class="vgroup">
<span class="vgroup__label">Checkbox</span><div class="vgroup__body">
<Checkbox id="cb1">Unchecked</Checkbox> <Checkbox id="cb1">Unchecked</Checkbox>
<Checkbox id="cb2" checked>Checked</Checkbox> <Checkbox id="cb2" checked>Checked</Checkbox>
<Checkbox id="cb3" checked disabled>Disabled</Checkbox> <Checkbox id="cb3" checked disabled>Disabled</Checkbox>
</div></div> </div>
<div class="vgroup"><span class="vgroup__label">Radio group</span><div class="vgroup__body" style="flex-direction:column;align-items:flex-start;"> </div>
<div class="vgroup">
<span class="vgroup__label">Radio group</span><div
class="vgroup__body"
style="flex-direction:column;align-items:flex-start;"
>
<Radio id="rb1" name="plan" checked>On-demand</Radio> <Radio id="rb1" name="plan" checked>On-demand</Radio>
<Radio id="rb2" name="plan">Reserved</Radio> <Radio id="rb2" name="plan">Reserved</Radio>
<Radio id="rb3" name="plan" disabled>Spot</Radio> <Radio id="rb3" name="plan" disabled>Spot</Radio>
</div></div> </div>
</div>
</section> </section>
<!-- TEXTFIELD --> <!-- TEXTFIELD -->
<section class="block" id="textfield"> <section class="block" id="textfield">
<h2>TextField</h2> <h2>TextField</h2>
<p class="block__desc">Text, email, password, textarea.</p> <p class="block__desc">Text, email, password, textarea.</p>
<div class="vgroup"><span class="vgroup__label">Fields</span><div class="vgroup__body" style="flex-direction:column;align-items:stretch;max-width:360px;"> <div class="vgroup">
<span class="vgroup__label">Fields</span><div
class="vgroup__body"
style="flex-direction:column;align-items:stretch;max-width:360px;"
>
<TextField label="Service name" placeholder="api-gateway" /> <TextField label="Service name" placeholder="api-gateway" />
<TextField label="Email" type="email" placeholder="you@nova.dev" /> <TextField label="Email" type="email" placeholder="you@nova.dev" />
<TextField label="Password" type="password" placeholder="••••••••" /> <TextField label="Password" type="password" placeholder="••••••••" />
<TextField label="Message" type="textarea" placeholder="Type here…" max={120} /> <TextField
</div></div> label="Message"
type="textarea"
placeholder="Type here…"
max={120}
/>
</div>
</div>
</section> </section>
<!-- SELECT --> <!-- SELECT -->
<section class="block" id="select"> <section class="block" id="select">
<h2>Select</h2> <h2>Select</h2>
<p class="block__desc">Default and disabled.</p> <p class="block__desc">Default and disabled.</p>
<div class="vgroup"><span class="vgroup__label">Select</span><div class="vgroup__body"> <div class="vgroup">
<span class="vgroup__label">Select</span><div class="vgroup__body">
<Select default="option3"> <Select default="option3">
<SelectOption value="option1">Option 1</SelectOption> <SelectOption value="option1">Option 1</SelectOption>
<SelectOption value="option2">Option 2</SelectOption> <SelectOption value="option2">Option 2</SelectOption>
@@ -153,44 +272,62 @@ const libraryIcons = [
<Select disabled> <Select disabled>
<SelectOption value="option1">Disabled</SelectOption> <SelectOption value="option1">Disabled</SelectOption>
</Select> </Select>
</div></div> </div>
</div>
</section> </section>
<!-- NUMERIC STEPPER --> <!-- NUMERIC STEPPER -->
<section class="block" id="stepper"> <section class="block" id="stepper">
<h2>Numeric stepper</h2> <h2>Numeric stepper</h2>
<p class="block__desc">Increment / decrement control.</p> <p class="block__desc">Increment / decrement control.</p>
<div class="vgroup"><span class="vgroup__label">Stepper</span><div class="vgroup__body"> <div class="vgroup">
<span class="vgroup__label">Stepper</span><div class="vgroup__body">
<NumericStepper min={-15} max={10} value={5} step={5} id="ns1" /> <NumericStepper min={-15} max={10} value={5} step={5} id="ns1" />
</div></div> </div>
</div>
</section> </section>
<!-- AVATAR --> <!-- AVATAR -->
<section class="block" id="avatar"> <section class="block" id="avatar">
<h2>Avatar</h2> <h2>Avatar</h2>
<p class="block__desc">Photo and initials.</p> <p class="block__desc">Photo and initials.</p>
<div class="vgroup"><span class="vgroup__label">Types</span><div class="vgroup__body"> <div class="vgroup">
<Avatar type="photo" name="John Doe" image="https://i.pravatar.cc/80?img=12" /> <span class="vgroup__label">Types</span><div class="vgroup__body">
<Avatar
type="photo"
name="John Doe"
image="https://i.pravatar.cc/80?img=12"
/>
<Avatar type="initials" name="Ava Stone" /> <Avatar type="initials" name="Ava Stone" />
<Avatar type="initials" name="Jane Smith" /> <Avatar type="initials" name="Jane Smith" />
</div></div> </div>
</div>
</section> </section>
<!-- LOADING BAR --> <!-- LOADING BAR -->
<section class="block" id="loading"> <section class="block" id="loading">
<h2>Loading bar</h2> <h2>Loading bar</h2>
<p class="block__desc">Determinate and indeterminate.</p> <p class="block__desc">Determinate and indeterminate.</p>
<div class="vgroup"><span class="vgroup__label">Known 75% · indeterminate</span><div class="vgroup__body" style="flex-direction:column;align-items:stretch;"> <div class="vgroup">
<span class="vgroup__label">Known 75% · indeterminate</span><div
class="vgroup__body"
style="flex-direction:column;align-items:stretch;"
>
<LoadingBar type="known" percentage={75} width="100%" /> <LoadingBar type="known" percentage={75} width="100%" />
<LoadingBar type="unknown" width="100%" /> <LoadingBar type="unknown" width="100%" />
</div></div> </div>
</div>
</section> </section>
<!-- LIST ITEM --> <!-- LIST ITEM -->
<section class="block" id="listitem"> <section class="block" id="listitem">
<h2>List item</h2> <h2>List item</h2>
<p class="block__desc">Title and subtitle rows.</p> <p class="block__desc">Title and subtitle rows.</p>
<div class="vgroup"><span class="vgroup__label">List</span><div class="vgroup__body" style="flex-direction:column;align-items:stretch;"> <div class="vgroup">
<span class="vgroup__label">List</span><div
class="vgroup__body"
style="flex-direction:column;align-items:stretch;"
>
<ListItem> <ListItem>
<ListItemTitle>api-gateway</ListItemTitle> <ListItemTitle>api-gateway</ListItemTitle>
<ListItemSubtitle>Edge routing · eu-west-1</ListItemSubtitle> <ListItemSubtitle>Edge routing · eu-west-1</ListItemSubtitle>
@@ -199,53 +336,70 @@ const libraryIcons = [
<ListItemTitle>postgres-main</ListItemTitle> <ListItemTitle>postgres-main</ListItemTitle>
<ListItemSubtitle>Database · eu-west-1</ListItemSubtitle> <ListItemSubtitle>Database · eu-west-1</ListItemSubtitle>
</ListItem> </ListItem>
</div></div> </div>
</div>
</section> </section>
<!-- LINK --> <!-- LINK -->
<section class="block" id="link"> <section class="block" id="link">
<h2>Link</h2> <h2>Link</h2>
<p class="block__desc">Inline and external.</p> <p class="block__desc">Inline and external.</p>
<div class="vgroup"><span class="vgroup__label">Links</span><div class="vgroup__body"> <div class="vgroup">
<span class="vgroup__label">Links</span><div class="vgroup__body">
<Link url="https://example.com">Documentation</Link> <Link url="https://example.com">Documentation</Link>
<Link url="https://example.com" blank={true}>Open dashboard</Link> <Link url="https://example.com" blank={true}>Open dashboard</Link>
</div></div> </div>
</div>
</section> </section>
<!-- TABS --> <!-- TABS -->
<section class="block" id="tabs"> <section class="block" id="tabs">
<h2>Tabs</h2> <h2>Tabs</h2>
<p class="block__desc">Underline tabs with panels.</p> <p class="block__desc">Underline tabs with panels.</p>
<div class="vgroup"><span class="vgroup__label">Tabs</span><div class="vgroup__body" style="flex-direction:column;align-items:stretch;"> <div class="vgroup">
<span class="vgroup__label">Tabs</span><div
class="vgroup__body"
style="flex-direction:column;align-items:stretch;"
>
<Tab id="gallery-tab" defaultActive="images"> <Tab id="gallery-tab" defaultActive="images">
<TabItem id="images">Images</TabItem> <TabItem id="images">Images</TabItem>
<TabItem id="videos">Videos</TabItem> <TabItem id="videos">Videos</TabItem>
<TabItem id="docs">Documents</TabItem> <TabItem id="docs">Documents</TabItem>
</Tab> </Tab>
<TabContent id="gallery-tab" itemId="images"><p>🖼️ Image gallery content.</p></TabContent> <TabContent id="gallery-tab" itemId="images"
<TabContent id="gallery-tab" itemId="videos"><p>🎬 Video player here.</p></TabContent> ><p>🖼️ Image gallery content.</p></TabContent
<TabContent id="gallery-tab" itemId="docs"><p>📄 Document list here.</p></TabContent> >
</div></div> <TabContent id="gallery-tab" itemId="videos"
><p>🎬 Video player here.</p></TabContent
>
<TabContent id="gallery-tab" itemId="docs"
><p>📄 Document list here.</p></TabContent
>
</div>
</div>
</section> </section>
<!-- BREADCRUMB --> <!-- BREADCRUMB -->
<section class="block" id="breadcrumb"> <section class="block" id="breadcrumb">
<h2>Breadcrumb</h2> <h2>Breadcrumb</h2>
<p class="block__desc">Hierarchical navigation.</p> <p class="block__desc">Hierarchical navigation.</p>
<div class="vgroup"><span class="vgroup__label">Breadcrumb</span><div class="vgroup__body"> <div class="vgroup">
<span class="vgroup__label">Breadcrumb</span><div class="vgroup__body">
<Breadcrumb> <Breadcrumb>
<BreadcrumbItem href="/">Home</BreadcrumbItem> <BreadcrumbItem href="/">Home</BreadcrumbItem>
<BreadcrumbItem href="/services">Services</BreadcrumbItem> <BreadcrumbItem href="/services">Services</BreadcrumbItem>
<BreadcrumbItem current>api-gateway</BreadcrumbItem> <BreadcrumbItem current>api-gateway</BreadcrumbItem>
</Breadcrumb> </Breadcrumb>
</div></div> </div>
</div>
</section> </section>
<!-- PAGINATION --> <!-- PAGINATION -->
<section class="block" id="pagination"> <section class="block" id="pagination">
<h2>Pagination</h2> <h2>Pagination</h2>
<p class="block__desc">Page numbers with disabled edges.</p> <p class="block__desc">Page numbers with disabled edges.</p>
<div class="vgroup"><span class="vgroup__label">Pagination</span><div class="vgroup__body"> <div class="vgroup">
<span class="vgroup__label">Pagination</span><div class="vgroup__body">
<Pagination defaultPage={3}> <Pagination defaultPage={3}>
<PaginationNumber>1</PaginationNumber> <PaginationNumber>1</PaginationNumber>
<PaginationNumber>2</PaginationNumber> <PaginationNumber>2</PaginationNumber>
@@ -253,80 +407,133 @@ const libraryIcons = [
<PaginationNumber>4</PaginationNumber> <PaginationNumber>4</PaginationNumber>
<PaginationNumber disabled>5</PaginationNumber> <PaginationNumber disabled>5</PaginationNumber>
</Pagination> </Pagination>
</div></div> </div>
</div>
</section> </section>
<!-- NOTIFICATION --> <!-- NOTIFICATION -->
<section class="block" id="notification"> <section class="block" id="notification">
<h2>Notification</h2> <h2>Notification</h2>
<p class="block__desc">Info, success, warning and error.</p> <p class="block__desc">Info, success, warning and error.</p>
<div class="vgroup"><span class="vgroup__label">All types</span><div class="vgroup__body" style="flex-direction:column;align-items:stretch;max-width:440px;"> <div class="vgroup">
<Notification type="info">Information about the operation.</Notification> <span class="vgroup__label">All types</span><div
<Notification type="success">The operation completed successfully.</Notification> class="vgroup__body"
<Notification type="warning">A warning occurred. Please check the details.</Notification> style="flex-direction:column;align-items:stretch;max-width:440px;"
<Notification type="error">An error occurred. Please try again.</Notification> >
</div></div> <Notification type="info"
>Information about the operation.</Notification
>
<Notification type="success"
>The operation completed successfully.</Notification
>
<Notification type="warning"
>A warning occurred. Please check the details.</Notification
>
<Notification type="error"
>An error occurred. Please try again.</Notification
>
</div>
</div>
</section> </section>
<!-- TOOLTIP --> <!-- TOOLTIP -->
<section class="block" id="tooltip"> <section class="block" id="tooltip">
<h2>Tooltip</h2> <h2>Tooltip</h2>
<p class="block__desc">Four positions — hover the buttons.</p> <p class="block__desc">Four positions — hover the buttons.</p>
<div class="vgroup"><span class="vgroup__label">Positions</span><div class="vgroup__body"> <div class="vgroup">
<Tooltip label="Tooltip top" position="top"><Button type="secondary" size="md">Top</Button></Tooltip> <span class="vgroup__label">Positions</span><div class="vgroup__body">
<Tooltip label="Tooltip bottom" position="bottom"><Button type="secondary" size="md">Bottom</Button></Tooltip> <Tooltip label="Tooltip top" position="top"
<Tooltip label="Tooltip left" position="left"><Button type="secondary" size="md">Left</Button></Tooltip> ><Button type="secondary" size="md">Top</Button></Tooltip
<Tooltip label="Tooltip right" position="right"><Button type="secondary" size="md">Right</Button></Tooltip> >
</div></div> <Tooltip label="Tooltip bottom" position="bottom"
><Button type="secondary" size="md">Bottom</Button></Tooltip
>
<Tooltip label="Tooltip left" position="left"
><Button type="secondary" size="md">Left</Button></Tooltip
>
<Tooltip label="Tooltip right" position="right"
><Button type="secondary" size="md">Right</Button></Tooltip
>
</div>
</div>
</section> </section>
<!-- CARD --> <!-- CARD -->
<section class="block" id="card"> <section class="block" id="card">
<h2>Card</h2> <h2>Card</h2>
<p class="block__desc">With cover, plain and with footer.</p> <p class="block__desc">With cover, plain and with footer.</p>
<div class="vgroup"><span class="vgroup__label">Cards</span><div class="vgroup__body" style="align-items:flex-start;"> <div class="vgroup">
<span class="vgroup__label">Cards</span><div
class="vgroup__body"
style="align-items:flex-start;"
>
<Card title="api-gateway" subtitle="Edge routing · 99.98% uptime"> <Card title="api-gateway" subtitle="Edge routing · 99.98% uptime">
<div slot="footer"><Badge type="success" variant="soft">Live</Badge></div> <div slot="footer">
<Badge type="success" variant="soft">Live</Badge>
</div>
</Card> </Card>
<Card title="Quick note" subtitle="A plain card without a cover image." /> <Card
title="Quick note"
subtitle="A plain card without a cover image."
/>
<Card title="Deploy" subtitle="Ship build 4821 to production."> <Card title="Deploy" subtitle="Ship build 4821 to production.">
<div slot="footer"> <div slot="footer">
<Button type="ghost" size="sm">Cancel</Button> <Button type="ghost" size="sm">Cancel</Button>
<Button type="primary" size="sm">Deploy</Button> <Button type="primary" size="sm">Deploy</Button>
</div> </div>
</Card> </Card>
</div></div> </div>
</div>
</section> </section>
<!-- APP CHROME --> <!-- APP CHROME -->
<section class="block" id="chrome"> <section class="block" id="chrome">
<h2>Navbar · Sidebar</h2> <h2>Navbar · Sidebar</h2>
<p class="block__desc">App chrome.</p> <p class="block__desc">App chrome.</p>
<div class="vgroup"><span class="vgroup__label">Navbar</span><div class="vgroup__body" style="align-items:stretch;"> <div class="vgroup">
<span class="vgroup__label">Navbar</span><div
class="vgroup__body"
style="align-items:stretch;"
>
<Navbar brand="Nova"> <Navbar brand="Nova">
<a class="navbar__link navbar__link--active" href="#">Services</a> <a class="navbar__link navbar__link--active" href="#">Services</a>
<a class="navbar__link" href="#">Incidents</a> <a class="navbar__link" href="#">Incidents</a>
<a class="navbar__link" href="#">Settings</a> <a class="navbar__link" href="#">Settings</a>
<Avatar slot="right" type="initials" name="Ava Stone" /> <Avatar slot="right" type="initials" name="Ava Stone" />
</Navbar> </Navbar>
</div></div> </div>
<div class="vgroup"><span class="vgroup__label">Sidebar</span><div class="vgroup__body"> </div>
<div class="vgroup">
<span class="vgroup__label">Sidebar</span><div class="vgroup__body">
<Sidebar header="Nova"> <Sidebar header="Nova">
<SidebarItem href="#" active><OverviewIcon slot="icon" size={18} />Overview</SidebarItem> <SidebarItem href="#" active
<SidebarItem href="#"><StatsIcon slot="icon" size={18} />Metrics</SidebarItem> ><OverviewIcon slot="icon" size={18} />Overview</SidebarItem
<SidebarItem href="#"><ShieldIcon slot="icon" size={18} />Security</SidebarItem> >
<SidebarItem href="#"><SettingsIcon slot="icon" size={18} />Settings</SidebarItem> <SidebarItem href="#"
><StatsIcon slot="icon" size={18} />Metrics</SidebarItem
>
<SidebarItem href="#"
><ShieldIcon slot="icon" size={18} />Security</SidebarItem
>
<SidebarItem href="#"
><SettingsIcon slot="icon" size={18} />Settings</SidebarItem
>
</Sidebar> </Sidebar>
</div></div> </div>
</div>
</section> </section>
<!-- MODAL --> <!-- MODAL -->
<section class="block" id="modal"> <section class="block" id="modal">
<h2>Modal</h2> <h2>Modal</h2>
<p class="block__desc">Overlay dialog.</p> <p class="block__desc">Overlay dialog.</p>
<div class="vgroup"><span class="vgroup__label">Dialog</span><div class="vgroup__body"> <div class="vgroup">
<Button type="primary" size="md" data-modal-open="demo-modal">Open modal</Button> <span class="vgroup__label">Dialog</span><div class="vgroup__body">
</div></div> <Button type="primary" size="md" data-modal-open="demo-modal"
>Open modal</Button
>
</div>
</div>
<Modal id="demo-modal" title="Confirm deploy"> <Modal id="demo-modal" title="Confirm deploy">
Ship build 4821 to production? This rolls out to all edge regions. Ship build 4821 to production? This rolls out to all edge regions.
<Fragment slot="footer"> <Fragment slot="footer">
@@ -339,50 +546,80 @@ const libraryIcons = [
<!-- ICONS --> <!-- ICONS -->
<section class="block" id="icons"> <section class="block" id="icons">
<h2>Icons</h2> <h2>Icons</h2>
<p class="block__desc">Library at 24px, plus arrow orientations and sizes.</p> <p class="block__desc">
<div class="vgroup"><span class="vgroup__label">Arrow orientations × 16/24/32</span><div class="vgroup__body"> Library at 24px, plus arrow orientations and sizes.
{[16, 24, 32].map((s) => ( </p>
<div class="vgroup">
<span class="vgroup__label">Arrow — orientations × 16/24/32</span><div
class="vgroup__body"
>
{
[16, 24, 32].map((s) => (
<span class="icon-line"> <span class="icon-line">
<Arrow2Icon size={s} orientation="up" /> <Arrow2Icon size={s} orientation="up" />
<Arrow2Icon size={s} orientation="right" /> <Arrow2Icon size={s} orientation="right" />
<Arrow2Icon size={s} orientation="down" /> <Arrow2Icon size={s} orientation="down" />
<Arrow2Icon size={s} orientation="left" /> <Arrow2Icon size={s} orientation="left" />
</span> </span>
))} ))
</div></div> }
<div class="vgroup"><span class="vgroup__label">Library (24px)</span><div class="vgroup__body" style="display:block;">
<div class="icon-grid">
{libraryIcons.map(([name, Icon]) => (
<div class="icon-cell"><Icon size={24} /><span>{name}</span></div>
))}
</div> </div>
</div></div> </div>
<div class="vgroup"><span class="vgroup__label">Sizes — 16 / 24 / 32</span><div class="vgroup__body"> <div class="vgroup">
{[SearchIcon, SettingsIcon, ShareIcon, DownloadIcon].map((Icon) => ( <span class="vgroup__label">Library (24px)</span><div
class="vgroup__body"
style="display:block;"
>
<div class="icon-grid">
{
libraryIcons.map(([name, Icon]) => (
<div class="icon-cell">
<>
<Icon size={24} />
<span>{name}</span>
</>
</div>
))
}
</div>
</div>
</div>
<div class="vgroup">
<span class="vgroup__label">Sizes — 16 / 24 / 32</span><div
class="vgroup__body"
>
{
[SearchIcon, SettingsIcon, ShareIcon, DownloadIcon].map((Icon) => (
<span class="icon-line"> <span class="icon-line">
<Icon size={16} /><Icon size={24} /><Icon size={32} /> <>
<Icon size={16} />
<Icon size={24} />
<Icon size={32} />
</>
</span> </span>
))} ))
</div></div> }
</div>
</div>
</section> </section>
</div> </div>
</Layout> </Layout>
<script> <script>
// Theme toggle (persisted) // Theme toggle (persisted)
const KEY = 'nova-theme'; const KEY = "nova-theme";
const saved = localStorage.getItem(KEY); const saved = localStorage.getItem(KEY);
if (saved) document.documentElement.setAttribute('data-theme', saved); if (saved) document.documentElement.setAttribute("data-theme", saved);
document.getElementById('theme-toggle')?.addEventListener('click', () => { document.getElementById("theme-toggle")?.addEventListener("click", () => {
const dark = document.documentElement.getAttribute('data-theme') === 'dark'; const dark = document.documentElement.getAttribute("data-theme") === "dark";
const next = dark ? 'light' : 'dark'; const next = dark ? "light" : "dark";
document.documentElement.setAttribute('data-theme', next); document.documentElement.setAttribute("data-theme", next);
localStorage.setItem(KEY, next); localStorage.setItem(KEY, next);
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
@use '../styles/tokens/typography' as *; @use "../styles/tokens/typography" as *;
body { body {
background-color: var(--nds-surface); background-color: var(--nds-surface);
@@ -405,46 +642,88 @@ const libraryIcons = [
padding-bottom: var(--nds-spacing-md); padding-bottom: var(--nds-spacing-md);
border-bottom: var(--nds-border-width-thin) solid var(--nds-border); border-bottom: var(--nds-border-width-thin) solid var(--nds-border);
} }
.gallery__brand { display: flex; align-items: center; gap: var(--nds-spacing-xs); } .gallery__brand {
display: flex;
align-items: center;
gap: var(--nds-spacing-xs);
}
.gallery__mark { .gallery__mark {
width: 26px; height: 26px; border-radius: var(--nds-radius-md); width: 26px;
height: 26px;
border-radius: var(--nds-radius-md);
background: linear-gradient(135deg, var(--nds-primary), var(--nds-accent)); background: linear-gradient(135deg, var(--nds-primary), var(--nds-accent));
} }
.gallery__name { @include text-xl; } .gallery__name {
@include text-xl;
}
.block { margin-bottom: var(--nds-spacing-2xl); scroll-margin-top: var(--nds-spacing-lg); } .block {
.block > h2 { @include text-2xl; margin: 0 0 var(--nds-spacing-3xs); } margin-bottom: var(--nds-spacing-2xl);
.block__desc { color: var(--nds-neutral); @include text-base; margin: 0 0 var(--nds-spacing-lg); } scroll-margin-top: var(--nds-spacing-lg);
}
.block > h2 {
@include text-2xl;
margin: 0 0 var(--nds-spacing-3xs);
}
.block__desc {
color: var(--nds-neutral);
@include text-base;
margin: 0 0 var(--nds-spacing-lg);
}
.vgroup { .vgroup {
border: var(--nds-border-width-thin) solid var(--nds-border); border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-lg); border-radius: var(--nds-radius-lg);
background: var(--nds-surface); background: var(--nds-surface);
overflow: hidden; overflow: hidden;
& + & { margin-top: var(--nds-spacing-md); } & + & {
margin-top: var(--nds-spacing-md);
}
} }
.vgroup__label { .vgroup__label {
display: block; display: block;
@include text-sm; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; @include text-sm;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--nds-neutral); color: var(--nds-neutral);
padding: var(--nds-spacing-xs) var(--nds-spacing-md); padding: var(--nds-spacing-xs) var(--nds-spacing-md);
border-bottom: var(--nds-border-width-thin) solid var(--nds-border); border-bottom: var(--nds-border-width-thin) solid var(--nds-border);
background: color-mix(in srgb, var(--nds-text) 2.5%, var(--nds-surface)); background: color-mix(in srgb, var(--nds-text) 2.5%, var(--nds-surface));
} }
.vgroup__body { .vgroup__body {
display: flex; flex-wrap: wrap; align-items: center; display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--nds-spacing-md); gap: var(--nds-spacing-md);
padding: var(--nds-spacing-lg) var(--nds-spacing-md); padding: var(--nds-spacing-lg) var(--nds-spacing-md);
} }
.icon-line { display: inline-flex; align-items: center; gap: var(--nds-spacing-md); color: var(--nds-text); } .icon-line {
.icon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--nds-spacing-xs); width: 100%; } display: inline-flex;
align-items: center;
gap: var(--nds-spacing-md);
color: var(--nds-text);
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: var(--nds-spacing-xs);
width: 100%;
}
.icon-cell { .icon-cell {
display: flex; flex-direction: column; align-items: center; gap: var(--nds-spacing-xs); display: flex;
flex-direction: column;
align-items: center;
gap: var(--nds-spacing-xs);
padding: var(--nds-spacing-md) var(--nds-spacing-xs); padding: var(--nds-spacing-md) var(--nds-spacing-xs);
border: var(--nds-border-width-thin) solid var(--nds-border); border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-md); border-radius: var(--nds-radius-md);
color: var(--nds-text); color: var(--nds-text);
span { @include text-sm; color: var(--nds-neutral); font-family: var(--nds-font-mono); } span {
@include text-sm;
color: var(--nds-neutral);
font-family: var(--nds-font-mono);
}
} }
</style> </style>