refactor: reorganize component imports and enhance index page layout

- Grouped component imports into categories for better organization.
- Updated the layout of the index page to include a gallery structure.
- Added new sections for Button, Badge, Selection Controls, TextField, Select, Numeric Stepper, Avatar, Loading Bar, List Item, Link, Tabs, Breadcrumb, Pagination, Notification, Tooltip, Card, Navbar, Sidebar, Modal, and Icons.
- Improved styling for better visual hierarchy and spacing.
- Implemented a theme toggle feature with persistent state using localStorage.
This commit is contained in:
2026-06-23 10:51:40 +02:00
parent dea517fbe5
commit 6967f3c7cd
14 changed files with 1741 additions and 582 deletions
+18 -20
View File
@@ -1,30 +1,28 @@
@use '../../styles/tokens/typography' as *;
@use "../../styles/tokens/typography" as *;
.link__container {
width: fit-content;
display: inline-flex;
align-items: center;
}
.link {
text-decoration: none;
color: var(--nds-primary);
@include text-base;
&:hover {
color: var(--nds-text);
opacity: 0.25;
}
&:active {
color: var(--nds-text);
opacity: 0.25;
}
&__container {
width: fit-content;
display: flex;
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
border-bottom: var(--nds-border-width-medium) dashed var(--nds-secondary);
gap: var(--nds-spacing-3xs);
border-bottom: 1px solid transparent;
transition:
border-color 130ms ease,
color 130ms ease;
&:hover {
background-color: var(--nds-secondary);
border-bottom: var(--nds-border-width-medium) dashed var(--nds-accent);
}
&:active {
background-color: var(--nds-primary);
border-bottom: var(--nds-border-width-medium) dashed var(--nds-secondary);
border-bottom-color: var(--nds-primary);
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--nds-ring);
border-radius: var(--nds-radius-xs);
}
}
+17 -20
View File
@@ -1,37 +1,34 @@
@use '../../styles/tokens/typography' as *;
@use "../../styles/tokens/typography" as *;
.loading-bar {
position: relative;
width: 100%;
height: 6px;
background-color: var(--nds-text);
background-color: var(--nds-surface-2);
overflow: hidden;
border-radius: var(--nds-radius-sm);
border-radius: var(--nds-radius-full);
&__progress {
height: 100%;
background-color: var(--nds-primary);
transition: width 0.3s ease;
border-radius: var(--nds-radius-sm);
border-radius: var(--nds-radius-full);
transition: width 0.35s ease;
}
&__unknown {
position: absolute;
top: 0;
left: -50%;
width: 50%;
left: 0;
width: 40%;
height: 100%;
background-color: var(--nds-primary);
border-radius: var(--nds-radius-sm);
animation: loadingBarIndeterminate 1.5s infinite;
}
@keyframes loadingBarIndeterminate {
from {
left: -50%;
}
to {
left: 100%;
}
border-radius: var(--nds-radius-full);
animation: nds-lb 1.3s ease-in-out infinite;
}
}
@keyframes nds-lb {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
+13 -7
View File
@@ -1,19 +1,25 @@
---
export interface Props {
type: 'known' | 'unknown';
type: "known" | "unknown";
percentage?: number;
width?: string;
}
const { type, percentage, width } = Astro.props;
---
<div class='loading-bar loading-bar--${type}' style={width ? `width: ${width}` : undefined}>
{type === 'known' && percentage !== undefined ? (
<div class='loading-bar__progress' style={`width: ${percentage}%`}></div>
<div
class={`loading-bar loading-bar--${type}`}
style={width ? `width: ${width}` : undefined}
>
{
type === "known" && percentage !== undefined ? (
<div class="loading-bar__progress" style={`width: ${percentage}%`} />
) : (
<div class='loading-bar__unknown'></div>
)}
<div class="loading-bar__unknown" />
)
}
</div>
<style lang="scss">
@use './loadingBar';
@use "./_loadingBar.scss";
</style>
+39 -50
View File
@@ -1,61 +1,50 @@
@use '../../styles/tokens/typography' as *;
@use "../../styles/tokens/typography" as *;
.notification {
display: flex;
align-items: flex-start;
justify-content: space-between;
align-items: center;
padding: var(--nds-spacing-sm) var(--nds-spacing-xs) ;
border-radius: var(--nds-radius-md);
@include text-sm;
&__error {
background-color: var(--nds-error-low);
&__close {
background: transparent;
border: none;
color: var(--nds-error-medium);
cursor: pointer;
}
&__content {
color: var(--nds-error-high);
}
}
&__success {
background-color: var(--nds-success-low);
&__close {
background: transparent;
border: none;
color: var(--nds-success-medium);
cursor: pointer;
}
&__content {
color: var(--nds-success-high);
}
}
&__warning {
background-color: var(--nds-warning-low);
&__close {
background: transparent;
border: none;
color: var(--nds-warning-medium);
cursor: pointer;
}
&__content {
color: var(--nds-warning-high);
}
}
gap: var(--nds-spacing-sm);
padding: var(--nds-spacing-sm) var(--nds-spacing-md);
border-radius: var(--nds-radius-lg);
background-color: var(--nds-surface);
box-shadow: var(--nds-shadow-md);
border-left: 3px solid var(--nds-info-medium);
@include text-base;
max-width: 400px;
&__info {
background-color: var(--nds-info-low);
&__close {
border-left-color: var(--nds-info-medium);
}
&__success {
border-left-color: var(--nds-success-medium);
}
&__warning {
border-left-color: var(--nds-warning-medium);
}
&__error {
border-left-color: var(--nds-error-medium);
}
&__info__content,
&__success__content,
&__warning__content,
&__error__content {
color: var(--nds-text);
flex: 1;
}
&__info__close,
&__success__close,
&__warning__close,
&__error__close {
background: transparent;
border: none;
color: var(--nds-info-medium);
cursor: pointer;
}
&__content {
color: var(--nds-info-high);
display: flex;
color: var(--nds-neutral);
padding: 0;
border-radius: var(--nds-radius-sm);
transition: color 120ms ease;
&:hover {
color: var(--nds-text);
}
}
}
@@ -1,23 +1,30 @@
---
import { CloseIcon } from "../Icons";
export interface Props {
type: 'info' | 'success' | 'warning' | 'error';
type: "info" | "success" | "warning" | "error";
}
const notificationUUID = `notification-${Math.random().toString(36).substr(2, 9)}`;
const notificationUUID = `notification-${Math.random().toString(36).slice(2, 11)}`;
const { type } = Astro.props;
---
<div class={`notification notification__${type}`} role="alert" id={notificationUUID}>
<div class=`notification__${type}__content`>
<div
class={`notification notification__${type}`}
role="alert"
id={notificationUUID}
>
<div class={`notification__${type}__content`}>
<slot />
</div>
<button class={`notification__${type}__close`} aria-label="Close notification" onclick={`document.getElementById('${notificationUUID}')?.remove()`}>
<button
class={`notification__${type}__close`}
aria-label="Close notification"
onclick={`document.getElementById('${notificationUUID}')?.remove()`}
>
<CloseIcon size={16} label="Close notification" />
</button>
</div>
<style lang="scss">
@use './_notification.scss';
@use "./_notification.scss";
</style>
+29 -22
View File
@@ -1,33 +1,40 @@
@use '../../styles/tokens/typography' as *;
@use "../../styles/tokens/typography" as *;
.tab {
display: flex;
align-items: flex-start;
gap: var(--nds-spacing-xs);
justify-content: center;
align-items: stretch;
gap: var(--nds-spacing-2xs);
border-bottom: 1px solid var(--nds-border);
width: fit-content;
&__item {
display: flex;
flex-direction: column;
display: inline-flex;
align-items: center;
justify-content: center;
height: fit-content;
gap: var(--nds-spacing-2xs);
padding: var(--nds-spacing-xs) var(--nds-spacing-sm);
cursor: pointer;
padding: var(--nds-spacing-2xs);
@include text-base;
&--active {
color: var(--nds-primary);
&::after {
content: '';
display: block;
width: 120%;
height: 1px;
background-color: var(--nds-primary);
border-radius: var(--nds-radius-sm);
@include text-label;
color: var(--nds-neutral);
background: transparent;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition:
color 130ms ease,
border-color 130ms ease;
&:hover {
color: var(--nds-text);
}
&--active {
color: var(--nds-text);
border-bottom-color: var(--nds-primary);
}
}
}
.tab__content {
padding-top: var(--nds-spacing-md);
@include text-base;
color: var(--nds-text);
&[hidden] {
display: none;
}
}
+11 -31
View File
@@ -1,33 +1,13 @@
.toggle {
background-color: var(--nds-disabled);
border-radius: var(--nds-radius-full);
padding: var(--nds-spacing-2xs);
width: 50px;
height: 25px;
display: flex;
align-items: center;
justify-content: left;
cursor: pointer;
transition: background-color 160ms ease;
position: relative;
&-switch {
color: var(--nds-text);
width: 15px;
height: 15px;
background-color: var(--nds-text);
border-radius: var(--nds-radius-full);
position: absolute;
left: var(--nds-spacing-2xs);
top: 50%;
transform: translateY(-50%);
transition: left 160ms ease, background-color 160ms ease, transform 160ms ease;
}
&-active {
display: flex;
justify-content: right;
background-color: var(--nds-primary);
.toggle-switch {
left: calc(100% - var(--nds-spacing-2xs) - 15px);
}
}
--w: 44px; --h: 24px; --pad: 3px;
position: relative; width: var(--w); height: var(--h);
background-color: var(--nds-disabled); border-radius: var(--nds-radius-full);
padding: var(--pad); display: flex; align-items: center; cursor: pointer;
transition: background-color 180ms ease;
&-switch { width: calc(var(--h) - var(--pad) * 2); height: calc(var(--h) - var(--pad) * 2);
background-color: #fff; border-radius: var(--nds-radius-full); box-shadow: var(--nds-shadow-sm);
transition: transform 180ms cubic-bezier(.34,1.4,.6,1); }
&-active { background-color: var(--nds-primary); }
&-active .toggle-switch { transform: translateX(calc(var(--w) - var(--h))); }
}
toggle-switch:focus-visible .toggle { box-shadow: 0 0 0 3px var(--nds-ring); outline: none; }
+2 -4
View File
@@ -1,8 +1,6 @@
---
import { boolean } from 'astro:schema';
export interface Props {
'data-checked': boolean;
'data-checked'?: boolean | string;
name?: string;
id: string;
}
@@ -10,7 +8,7 @@ export interface Props {
const { 'data-checked': dataChecked = 'false', name = 'toggle', id } = Astro.props;
---
<toggle-switch data-checked={dataChecked} data-name={name} id={id}>
<toggle-switch data-checked={String(dataChecked)} data-name={name} id={id}>
<div class="toggle">
<div class="toggle-switch"></div>
</div>
+3 -2
View File
@@ -1,6 +1,7 @@
@use '../../styles/tokens/typography' as *;
@use "../../styles/tokens/typography" as *;
.pagination {
display: flex;
gap: var(--nds-spacing-sm);
align-items: center;
gap: var(--nds-spacing-2xs);
}
@@ -1,26 +1,30 @@
@use '../../styles/tokens/typography' as *;
@use "../../styles/tokens/typography" as *;
.pagination-number {
width: 30px;
height: 30px;
display: flex;
min-width: 32px;
height: 32px;
padding: 0 var(--nds-spacing-2xs);
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--nds-radius-full);
border-radius: var(--nds-radius-md);
cursor: pointer;
@include text-label;
color: var(--nds-text);
background: transparent;
border: 1px solid transparent;
transition: all 120ms ease;
user-select: none;
&:hover:not(.pagination-number__disabled):not(.pagination-number__selected) {
background-color: var(--nds-secondary);
cursor: pointer;
}
&:active:not(.pagination-number__disabled):not(.pagination-number__selected) {
background-color: var(--nds-accent);
cursor: pointer;
}
&__disabled {
background-color: var(--nds-disabled);
cursor: not-allowed;
border-color: var(--nds-border-strong);
background-color: var(--nds-surface-hover);
}
&__selected {
background-color: var(--nds-primary);
cursor: pointer;
color: var(--nds-on-primary);
}
&__disabled {
color: var(--nds-disabled);
cursor: not-allowed;
}
}
+1
View File
@@ -42,6 +42,7 @@
min-height: 84px;
padding: var(--nds-spacing-sm);
resize: vertical;
width: 100% !important;
line-height: 1.5;
}
&__charCount {
+6 -18
View File
@@ -1,29 +1,17 @@
---
import '../styles/index.scss';
interface Props { title?: string; }
const { title = 'Nova Design System' } = Astro.props;
---
<!doctype html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" href="/favicon.ico" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} />
<title>Nova Design System</title>
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
<style>
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
font-family: var(--nds-font-family-base);
background-color: var(--nds-page-low);
color: var(--nds-default);
}
</style>
File diff suppressed because it is too large Load Diff
+389 -350
View File
@@ -1,23 +1,45 @@
---
import Layout from '../layouts/Layout.astro';
import Notification from '../components/Notifications/notification.astro';
// Core
import Button from '../components/Button/button.astro';
import Badge from '../components/Badge/badge.astro';
import Avatar from '../components/Avatar/avatar.astro';
// Forms
import Toggle from '../components/Toggle/toggle.astro';
import Checkbox from '../components/Checkbox/checkbox.astro';
import Radio from '../components/Radio/radio.astro';
import TextField from '../components/textField/textField.astro';
import Select from '../components/Select/select.astro';
import SelectOption from '../components/Select/selectOption.astro';
import NumericStepper from '../components/numericStepper/numericStepper.astro';
// Navigation
import Tab from '../components/Tabs/tab.astro';
import TabItem from '../components/Tabs/tabItem.astro';
import TabContent from '../components/Tabs/tabContent.astro';
import Button from '../components/Button/button.astro';
import Link from '../components/Link/link.astro';
import Breadcrumb from '../components/Breadcrumb/breadcrumb.astro';
import BreadcrumbItem from '../components/Breadcrumb/breadcrumbItem.astro';
import Pagination from '../components/pagination/pagination.astro';
import PaginationNumber from '../components/pagination/paginationNumber.astro';
import Navbar from '../components/Navbar/navbar.astro';
import Sidebar from '../components/Sidebar/sidebar.astro';
import SidebarItem from '../components/Sidebar/sidebarItem.astro';
// Data
import Card from '../components/Card/card.astro';
import ListItem from '../components/ListItem/listItem.astro';
import ListItemTitle from '../components/ListItem/listItemTitle.astro';
import ListItemSubtitle from '../components/ListItem/listItemSubtitle.astro';
// Feedback
import Notification from '../components/Notifications/notification.astro';
import LoadingBar from '../components/LoadingBar/loadingBar.astro';
import NumericStepper from '../components/numericStepper/numericStepper.astro';
import Avatar from '../components/Avatar/avatar.astro';
import Select from '../components/Select/select.astro';
import SelectOption from '../components/Select/selectOption.astro';
import TextField from '../components/textField/textField.astro';
import Pagination from '../components/pagination/pagination.astro';
import PaginationNumber from '../components/pagination/paginationNumber.astro';
import Tooltip from '../components/Tooltip/tooltip.astro';
import Modal from '../components/Modal/modal.astro';
import {
Arrow2Icon, BinIcon, BurgerIcon, CalendarIcon, CheckIcon, CloseIcon,
CodeIcon, CubeIcon, DownloadIcon, FilterIcon, HelpIcon, HomeIcon,
@@ -26,15 +48,204 @@ import {
UploadIcon,
} from '../components/Icons/index.ts';
const initialChecked = true;
const tones = ['primary', 'neutral', 'success', 'warning', 'error', 'info'] as const;
const buttonTypes = ['primary', 'secondary', 'ghost', 'danger'] as const;
const sizes = ['sm', 'md', 'lg'] as const;
const libraryIcons = [
['Profile', ProfileIcon], ['Search', SearchIcon], ['Settings', SettingsIcon],
['Home', HomeIcon], ['Overview', OverviewIcon], ['Stats', StatsIcon],
['Share', ShareIcon], ['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;
---
<Layout>
<main>
<h1>Nova Design System</h1>
<section>
<div class="gallery">
<header class="gallery__head">
<div class="gallery__brand">
<span class="gallery__mark"></span>
<span class="gallery__name">Nova Design System</span>
<Badge type="primary" variant="soft">v0.06</Badge>
</div>
<Button type="secondary" size="sm" id="theme-toggle">Toggle theme</Button>
</header>
<!-- BUTTON -->
<section class="block" id="button">
<h2>Button</h2>
<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">
{buttonTypes.map((t) => <Button type={t} size="md">{t[0].toUpperCase() + t.slice(1)}</Button>)}
</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>
<!-- BADGE -->
<section class="block" id="badge">
<h2>Badge</h2>
<p class="block__desc">Tones × soft / solid.</p>
<div class="vgroup"><span class="vgroup__label">Soft</span><div class="vgroup__body">
{tones.map((t) => <Badge type={t} variant="soft">{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>
<!-- SELECTION CONTROLS -->
<section class="block" id="controls">
<h2>Toggle · Checkbox · Radio</h2>
<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">
<Toggle id="tg-off" />
<Toggle id="tg-on" data-checked={true} />
</div></div>
<div class="vgroup"><span class="vgroup__label">Checkbox</span><div class="vgroup__body">
<Checkbox id="cb1">Unchecked</Checkbox>
<Checkbox id="cb2" checked>Checked</Checkbox>
<Checkbox id="cb3" checked disabled>Disabled</Checkbox>
</div></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="rb2" name="plan">Reserved</Radio>
<Radio id="rb3" name="plan" disabled>Spot</Radio>
</div></div>
</section>
<!-- TEXTFIELD -->
<section class="block" id="textfield">
<h2>TextField</h2>
<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;">
<TextField label="Service name" placeholder="api-gateway" />
<TextField label="Email" type="email" placeholder="you@nova.dev" />
<TextField label="Password" type="password" placeholder="••••••••" />
<TextField label="Message" type="textarea" placeholder="Type here…" max={120} />
</div></div>
</section>
<!-- SELECT -->
<section class="block" id="select">
<h2>Select</h2>
<p class="block__desc">Default and disabled.</p>
<div class="vgroup"><span class="vgroup__label">Select</span><div class="vgroup__body">
<Select default="option3">
<SelectOption value="option1">Option 1</SelectOption>
<SelectOption value="option2">Option 2</SelectOption>
<SelectOption value="option3">Option 3</SelectOption>
<SelectOption value="option4">Option 4</SelectOption>
<SelectOption value="option5" disabled>Option 5</SelectOption>
</Select>
<Select disabled>
<SelectOption value="option1">Disabled</SelectOption>
</Select>
</div></div>
</section>
<!-- NUMERIC STEPPER -->
<section class="block" id="stepper">
<h2>Numeric stepper</h2>
<p class="block__desc">Increment / decrement control.</p>
<div class="vgroup"><span class="vgroup__label">Stepper</span><div class="vgroup__body">
<NumericStepper min={-15} max={10} value={5} step={5} id="ns1" />
</div></div>
</section>
<!-- AVATAR -->
<section class="block" id="avatar">
<h2>Avatar</h2>
<p class="block__desc">Photo and initials.</p>
<div class="vgroup"><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="Jane Smith" />
</div></div>
</section>
<!-- LOADING BAR -->
<section class="block" id="loading">
<h2>Loading bar</h2>
<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;">
<LoadingBar type="known" percentage={75} width="100%" />
<LoadingBar type="unknown" width="100%" />
</div></div>
</section>
<!-- LIST ITEM -->
<section class="block" id="listitem">
<h2>List item</h2>
<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;">
<ListItem>
<ListItemTitle>api-gateway</ListItemTitle>
<ListItemSubtitle>Edge routing · eu-west-1</ListItemSubtitle>
</ListItem>
<ListItem>
<ListItemTitle>postgres-main</ListItemTitle>
<ListItemSubtitle>Database · eu-west-1</ListItemSubtitle>
</ListItem>
</div></div>
</section>
<!-- LINK -->
<section class="block" id="link">
<h2>Link</h2>
<p class="block__desc">Inline and external.</p>
<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" blank={true}>Open dashboard</Link>
</div></div>
</section>
<!-- TABS -->
<section class="block" id="tabs">
<h2>Tabs</h2>
<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;">
<Tab id="gallery-tab" defaultActive="images">
<TabItem id="images">Images</TabItem>
<TabItem id="videos">Videos</TabItem>
<TabItem id="docs">Documents</TabItem>
</Tab>
<TabContent id="gallery-tab" itemId="images"><p>🖼️ Image gallery content.</p></TabContent>
<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>
<!-- BREADCRUMB -->
<section class="block" id="breadcrumb">
<h2>Breadcrumb</h2>
<p class="block__desc">Hierarchical navigation.</p>
<div class="vgroup"><span class="vgroup__label">Breadcrumb</span><div class="vgroup__body">
<Breadcrumb>
<BreadcrumbItem href="/">Home</BreadcrumbItem>
<BreadcrumbItem href="/services">Services</BreadcrumbItem>
<BreadcrumbItem current>api-gateway</BreadcrumbItem>
</Breadcrumb>
</div></div>
</section>
<!-- PAGINATION -->
<section class="block" id="pagination">
<h2>Pagination</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<p class="block__desc">Page numbers with disabled edges.</p>
<div class="vgroup"><span class="vgroup__label">Pagination</span><div class="vgroup__body">
<Pagination defaultPage={3}>
<PaginationNumber>1</PaginationNumber>
<PaginationNumber>2</PaginationNumber>
@@ -42,364 +253,192 @@ const initialChecked = true;
<PaginationNumber>4</PaginationNumber>
<PaginationNumber disabled>5</PaginationNumber>
</Pagination>
</div>
</div></div>
</section>
<section>
<h2>TextField</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<TextField label="Email" type="email" placeholder="Enter your email" />
<TextField label="Password" type="password" placeholder="Enter your password" />
<TextField label="Textarea" type="textarea" placeholder="Enter your message" max={100} />
</div>
</section>
<section>
<h2>Select</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<Select default="option3">
<SelectOption value="option1">Option 1</SelectOption>
<SelectOption value="option2">Option 2</SelectOption>
<SelectOption value="option3" >Option 3</SelectOption>
<SelectOption value="option4" >Option 4</SelectOption>
<SelectOption value="option5" >Option 5</SelectOption>
<SelectOption value="option6" >Option 6</SelectOption>
<SelectOption value="option7" >Option 7</SelectOption>
<SelectOption value="option8" >Option 8</SelectOption>
<SelectOption value="option9" disabled>Option 9</SelectOption>
</Select>
<Select disabled>
<SelectOption value="option1">Option 1</SelectOption>
</Select>
</div>
</section>
<section>
<h2>Avatar</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<Avatar image="https://static.vecteezy.com/system/resources/thumbnails/057/068/323/small/single-fresh-red-strawberry-on-table-green-background-food-fruit-sweet-macro-juicy-plant-image-photo.jpg" name="John Doe" type="photo"/>
<Avatar name="Jane Smith" type="initials"/>
</div>
</section>
<section>
<h2>Numeric Stepper</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<NumericStepper min={-15} max={10} value={5} step={5} id="my-numeric-stepper"/>
</div>
</section>
<section>
<h2>Loading Bar</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<LoadingBar type="known" percentage={75} width="500px" />
<LoadingBar type="unknown" width="500px" />
</div>
</section>
<section>
<h2>List Item</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<ListItem>
<ListItemTitle>List Item Title</ListItemTitle>
<ListItemSubtitle>List Item Subtitle</ListItemSubtitle>
</ListItem>
</div>
</section>
<section>
<h2>Link</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<Link url="https://example.com">Example Link</Link>
<Link url="https://example.com" blank={true}>Example Link (opens in new tab)</Link>
</div>
</section>
<section>
<h2>Button</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<Button type="primary">Primary Button</Button>
<Button type="secondary">Secondary Button</Button>
<Button type="primary" disabled>disabled</Button>
</div>
</section>
<section>
<h2>Tabs</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<Tab id="my-tab" defaultActive="images">
<TabItem id="images">images</TabItem>
<TabItem id="videos">Videos</TabItem>
<TabItem id="docs">Documents</TabItem>
</Tab>
<TabContent id="my-tab" itemId="images">
<p>my images</p>
</TabContent>
<TabContent id="my-tab" itemId="videos">
<p>Video player here</p>
</TabContent>
<!-- NOTIFICATION -->
<section class="block" id="notification">
<h2>Notification</h2>
<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;">
<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>
<TabContent id="my-tab" itemId="docs">
<p>Document list here</p>
</TabContent>
</div>
<!-- TOOLTIP -->
<section class="block" id="tooltip">
<h2>Tooltip</h2>
<p class="block__desc">Four positions — hover the buttons.</p>
<div class="vgroup"><span class="vgroup__label">Positions</span><div class="vgroup__body">
<Tooltip label="Tooltip top" position="top"><Button type="secondary" size="md">Top</Button></Tooltip>
<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>
<h2>Toggle</h2>
<div style="display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<Toggle data-checked={initialChecked} data-name="notifications" id="my-toggle" />
<p id="toggle-state">State: {initialChecked ? 'Checked' : 'Unchecked'}</p>
<!-- CARD -->
<section class="block" id="card">
<h2>Card</h2>
<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;">
<Card title="api-gateway" subtitle="Edge routing · 99.98% uptime">
<div slot="footer"><Badge type="success" variant="soft">Live</Badge></div>
</Card>
<Card title="Quick note" subtitle="A plain card without a cover image." />
<Card title="Deploy" subtitle="Ship build 4821 to production.">
<div slot="footer">
<Button type="ghost" size="sm">Cancel</Button>
<Button type="primary" size="sm">Deploy</Button>
</div>
</Card>
</div></div>
</section>
<section>
<h2>Notifications — Error / Warning / Success / Info</h2>
<div style="max-width: 400px; display: flex; flex-direction: column; gap: var(--nds-spacing-md);">
<Notification type="error">
An error occurred during the operation. Please try again.
</Notification>
<Notification type="success">
The operation was completed successfully.
</Notification>
<Notification type="warning">
A warning occurred during the operation. Please check the details.
</Notification>
<Notification type="info">
Information about the operation.
</Notification>
</div>
<!-- APP CHROME -->
<section class="block" id="chrome">
<h2>Navbar · Sidebar</h2>
<p class="block__desc">App chrome.</p>
<div class="vgroup"><span class="vgroup__label">Navbar</span><div class="vgroup__body" style="align-items:stretch;">
<Navbar brand="Nova">
<a class="navbar__link navbar__link--active" href="#">Services</a>
<a class="navbar__link" href="#">Incidents</a>
<a class="navbar__link" href="#">Settings</a>
<Avatar slot="right" type="initials" name="Ava Stone" />
</Navbar>
</div></div>
<div class="vgroup"><span class="vgroup__label">Sidebar</span><div class="vgroup__body">
<Sidebar header="Nova">
<SidebarItem href="#" active><OverviewIcon slot="icon" size={18} />Overview</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>
</div></div>
</section>
<section>
<h2>Icon — 16 / 24 / 32</h2>
<!-- MODAL -->
<section class="block" id="modal">
<h2>Modal</h2>
<p class="block__desc">Overlay dialog.</p>
<div class="vgroup"><span class="vgroup__label">Dialog</span><div class="vgroup__body">
<Button type="primary" size="md" data-modal-open="demo-modal">Open modal</Button>
</div></div>
<Modal id="demo-modal" title="Confirm deploy">
Ship build 4821 to production? This rolls out to all edge regions.
<Fragment slot="footer">
<Button type="ghost" size="md" data-modal-close>Cancel</Button>
<Button type="primary" size="md" data-modal-close>Deploy</Button>
</Fragment>
</Modal>
</section>
<!-- ICONS -->
<section class="block" id="icons">
<h2>Icons</h2>
<p class="block__desc">Library at 24px, plus arrow orientations and sizes.</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">
<Arrow2Icon size={s} orientation="up" />
<Arrow2Icon size={s} orientation="right" />
<Arrow2Icon size={s} orientation="down" />
<Arrow2Icon size={s} orientation="left" />
</span>
))}
</div></div>
<div class="vgroup"><span class="vgroup__label">Library (24px)</span><div class="vgroup__body" style="display:block;">
<div class="icon-grid">
<div class="icon-row">
<Arrow2Icon size={16} orientation="up" />
<Arrow2Icon size={16} orientation="right" />
<Arrow2Icon size={16} orientation="down" />
<Arrow2Icon size={16} orientation="left" />
</div>
<div class="icon-row">
<Arrow2Icon size={24} orientation="up" />
<Arrow2Icon size={24} orientation="right" />
<Arrow2Icon size={24} orientation="down" />
<Arrow2Icon size={24} orientation="left" />
</div>
<div class="icon-row">
<Arrow2Icon size={32} orientation="up" />
<Arrow2Icon size={32} orientation="right" />
<Arrow2Icon size={32} orientation="down" />
<Arrow2Icon size={32} orientation="left" />
</div>
<div class="icon-row">
<ProfileIcon size={16} />
<ProfileIcon size={24} />
<ProfileIcon size={32} />
</div>
<div class="icon-row">
<ShareIcon size={16} />
<ShareIcon size={24} />
<ShareIcon size={32} />
</div>
<div class="icon-row">
<SearchIcon size={16} />
<SearchIcon size={24} />
<SearchIcon size={32} />
</div>
<div class="icon-row">
<CloseIcon size={16} />
<CloseIcon size={24} />
<CloseIcon size={32} />
</div>
<div class="icon-row">
<UploadIcon size={16} />
<UploadIcon size={24} />
<UploadIcon size={32} />
</div>
<div class="icon-row">
<BinIcon size={16} />
<BinIcon size={24} />
<BinIcon size={32} />
</div>
<div class="icon-row">
<BurgerIcon size={16} />
<BurgerIcon size={24} />
<BurgerIcon size={32} />
</div>
<div class="icon-row">
<CalendarIcon size={16} />
<CalendarIcon size={24} />
<CalendarIcon size={32} />
</div>
<div class="icon-row">
<CheckIcon size={16} />
<CheckIcon size={24} />
<CheckIcon size={32} />
</div>
<div class="icon-row">
<CodeIcon size={16} />
<CodeIcon size={24} />
<CodeIcon size={32} />
</div>
<div class="icon-row">
<CubeIcon size={16} />
<CubeIcon size={24} />
<CubeIcon size={32} />
</div>
<div class="icon-row">
<DownloadIcon size={16} />
<DownloadIcon size={24} />
<DownloadIcon size={32} />
</div>
<div class="icon-row">
<FilterIcon size={16} />
<FilterIcon size={24} />
<FilterIcon size={32} />
</div>
<div class="icon-row">
<HelpIcon size={16} />
<HelpIcon size={24} />
<HelpIcon size={32} />
</div>
<div class="icon-row">
<HomeIcon size={16} />
<HomeIcon size={24} />
<HomeIcon size={32} />
</div>
<div class="icon-row">
<LinkIcon size={16} />
<LinkIcon size={24} />
<LinkIcon size={32} />
</div>
<div class="icon-row">
<MinusIcon size={16} />
<MinusIcon size={24} />
<MinusIcon size={32} />
</div>
<div class="icon-row">
<MoreIcon size={16} />
<MoreIcon size={24} />
<MoreIcon size={32} />
</div>
<div class="icon-row">
<OverviewIcon size={16} />
<OverviewIcon size={24} />
<OverviewIcon size={32} />
</div>
<div class="icon-row">
<PlusIcon size={16} />
<PlusIcon size={24} />
<PlusIcon size={32} />
</div>
<div class="icon-row">
<SettingsIcon size={16} />
<SettingsIcon size={24} />
<SettingsIcon size={32} />
</div>
<div class="icon-row">
<ShieldIcon size={16} />
<ShieldIcon size={24} />
<ShieldIcon size={32} />
</div>
<div class="icon-row">
<SortIcon size={16} variant="default" />
<SortIcon size={16} variant="ascend" />
<SortIcon size={16} variant="descend" />
<SortIcon size={24} variant="default" />
<SortIcon size={24} variant="ascend" />
<SortIcon size={24} variant="descend" />
<SortIcon size={32} variant="default" />
<SortIcon size={32} variant="ascend" />
<SortIcon size={32} variant="descend" />
</div>
<div class="icon-row">
<StatsIcon size={16} />
<StatsIcon size={24} />
<StatsIcon size={32} />
</div>
{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">
<Icon size={16} /><Icon size={24} /><Icon size={32} />
</span>
))}
</div></div>
</section>
</main>
</div>
</Layout>
<script>
const toggle = document.getElementById('my-toggle') as any;
const stateLabel = document.getElementById('toggle-state');
// Défaut: thème sombre
if (!document.documentElement.hasAttribute('data-theme')) {
document.documentElement.setAttribute('data-theme', 'dark');
}
if (stateLabel) stateLabel.textContent = 'Thème : Sombre';
toggle?.addEventListener('change', (e: CustomEvent) => {
const { checked } = e.detail;
if (checked) {
document.documentElement.setAttribute('data-theme', 'dark');
if (stateLabel) stateLabel.textContent = 'Thème : Sombre';
} else {
document.documentElement.setAttribute('data-theme', 'light');
if (stateLabel) stateLabel.textContent = 'Thème : Clair';
}
// Theme toggle (persisted)
const KEY = 'nova-theme';
const saved = localStorage.getItem(KEY);
if (saved) document.documentElement.setAttribute('data-theme', saved);
document.getElementById('theme-toggle')?.addEventListener('click', () => {
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
const next = dark ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem(KEY, next);
});
</script>
<style lang="scss" scoped>
<style lang="scss">
@use '../styles/tokens/typography' as *;
body {
background-color: var(--nds-background);
color: var(--nds-text);
}
main {
max-width: 900px;
.gallery {
max-width: 1000px;
margin: 0 auto;
padding: var(--nds-spacing-xl) var(--nds-spacing-md);
padding: var(--nds-spacing-xl) var(--nds-spacing-lg) var(--nds-spacing-3xl);
}
h1 {
font-size: var(--nds-type-h1-size);
font-weight: var(--nds-type-h1-weight);
line-height: var(--nds-type-h1-line-height);
letter-spacing: var(--nds-type-h1-letter-spacing);
margin-bottom: var(--nds-spacing-xl);
}
h2 {
font-size: var(--nds-type-h2-size);
font-weight: var(--nds-type-h2-weight);
line-height: var(--nds-type-h2-line-height);
letter-spacing: var(--nds-type-h2-letter-spacing);
color: var(--nds-text-muted);
margin-bottom: var(--nds-spacing-md);
}
section {
margin-bottom: var(--nds-spacing-2xl);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--nds-spacing-sm);
}
.icon-grid {
display: flex;
flex-direction: column;
gap: var(--nds-spacing-xs);
}
.icon-row {
display: flex;
align-items: center;
gap: var(--nds-spacing-lg);
color: var(--nds-text);
}
.icon-name {
font-size: var(--nds-type-body-small-size);
color: var(--nds-text-muted);
width: 100px;
flex-shrink: 0;
}
.icon-sizes {
.gallery__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--nds-spacing-md);
color: var(--nds-default);
margin-bottom: var(--nds-spacing-2xl);
padding-bottom: var(--nds-spacing-md);
border-bottom: var(--nds-border-width-thin) solid var(--nds-border);
}
.gallery__brand { display: flex; align-items: center; gap: var(--nds-spacing-xs); }
.gallery__mark {
width: 26px; height: 26px; border-radius: var(--nds-radius-md);
background: linear-gradient(135deg, var(--nds-primary), var(--nds-accent));
}
.gallery__name { @include text-xl; }
.block { margin-bottom: var(--nds-spacing-2xl); 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 {
border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-lg);
background: var(--nds-surface);
overflow: hidden;
& + & { margin-top: var(--nds-spacing-md); }
}
.vgroup__label {
display: block;
@include text-sm; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
color: var(--nds-neutral);
padding: var(--nds-spacing-xs) var(--nds-spacing-md);
border-bottom: var(--nds-border-width-thin) solid var(--nds-border);
background: color-mix(in srgb, var(--nds-text) 2.5%, var(--nds-surface));
}
.vgroup__body {
display: flex; flex-wrap: wrap; align-items: center;
gap: 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-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--nds-spacing-xs); width: 100%; }
.icon-cell {
display: flex; flex-direction: column; align-items: center; gap: var(--nds-spacing-xs);
padding: var(--nds-spacing-md) var(--nds-spacing-xs);
border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-md);
color: var(--nds-text);
span { @include text-sm; color: var(--nds-neutral); font-family: var(--nds-font-mono); }
}
</style>