noecosystem-design 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
 
3
3
  import { AppFrame } from '../../../../packages/ui/src/app-frame';
4
+ import { ChartSquare, ReceiptSearch, WalletMoney } from '../../../../packages/icons/src';
4
5
  import { SidebarNavigation } from '../../../../packages/ui/src/sidebar-navigation';
5
6
 
6
7
  const navigation = (
@@ -36,6 +37,44 @@ function AppFrameDemo() {
36
37
  );
37
38
  }
38
39
 
40
+ function FinanceAppFrameDemo() {
41
+ return (
42
+ <div style={{ height: '28rem' }}>
43
+ <AppFrame
44
+ header={
45
+ <div className="flex min-h-14 items-center bg-surface-raised px-4 text-sm font-medium">
46
+ NOCFO
47
+ </div>
48
+ }
49
+ navigation={
50
+ <SidebarNavigation
51
+ label="NOCFO navigation"
52
+ sections={[
53
+ {
54
+ id: 'finance',
55
+ label: 'Finance',
56
+ items: [
57
+ { href: '#dashboard', icon: <ChartSquare />, label: 'Dashboard', current: true },
58
+ { href: '#contracts', icon: <ReceiptSearch />, label: 'Contracts' },
59
+ { href: '#cashflow', icon: <WalletMoney />, label: 'Cash flow', badge: '12' },
60
+ ],
61
+ },
62
+ ]}
63
+ />
64
+ }
65
+ variant="finance"
66
+ >
67
+ <section className="rounded-lg bg-surface p-5">
68
+ <h2 className="m-0 text-lg font-semibold">Financial overview</h2>
69
+ <p className="mb-0 mt-2 text-sm text-muted-foreground">
70
+ Flat surfaces are separated by semantic color and spacing, not shadows or card strokes.
71
+ </p>
72
+ </section>
73
+ </AppFrame>
74
+ </div>
75
+ );
76
+ }
77
+
39
78
  const meta = {
40
79
  title: 'Catalog/Patterns/Pages/App Frame',
41
80
  component: AppFrameDemo,
@@ -45,3 +84,7 @@ export default meta;
45
84
  type Story = StoryObj<typeof meta>;
46
85
 
47
86
  export const Default: Story = {};
87
+
88
+ export const Finance: Story = {
89
+ render: () => <FinanceAppFrameDemo />,
90
+ };
@@ -22,6 +22,7 @@ export const Default: Story = {
22
22
  };
23
23
 
24
24
  export const Outline: Story = { args: { variant: 'outline', children: 'Outline' } };
25
+ export const Secondary: Story = { args: { variant: 'secondary', children: 'Secondary' } };
25
26
  export const Ghost: Story = { args: { variant: 'ghost', children: 'Ghost' } };
26
27
  export const Destructive: Story = { args: { variant: 'destructive', children: 'Delete' } };
27
28
  export const Loading: Story = { args: { loading: true, children: 'Saving' } };
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
 
3
3
  import { SidebarNavigation } from '../../../../packages/ui/src/sidebar-navigation';
4
+ import { ChartSquare, ReceiptSearch, WalletMoney } from '../../../../packages/icons/src';
4
5
 
5
6
  const sections = [
6
7
  {
@@ -38,3 +39,24 @@ export default meta;
38
39
  type Story = StoryObj<typeof meta>;
39
40
 
40
41
  export const Default: Story = {};
42
+
43
+ export const PersianRtl: Story = {
44
+ render: () => (
45
+ <div dir="rtl" lang="fa" style={{ maxWidth: '17.5rem' }}>
46
+ <SidebarNavigation
47
+ label="ناوبری نوکفو"
48
+ sections={[
49
+ {
50
+ id: 'finance',
51
+ label: 'مدیریت مالی',
52
+ items: [
53
+ { href: '#dashboard', icon: <ChartSquare />, label: 'داشبورد', current: true },
54
+ { href: '#contracts', icon: <ReceiptSearch />, label: 'قراردادها' },
55
+ { href: '#cashflow', icon: <WalletMoney />, label: 'جریان نقدی', badge: '۱۲' },
56
+ ],
57
+ },
58
+ ]}
59
+ />
60
+ </div>
61
+ ),
62
+ };
@@ -18,8 +18,9 @@ interfaces use the same language rather than a related palette.
18
18
  Use large, confident headings; quiet metadata with tracked uppercase only for
19
19
  Latin labels; generous spacing; and low-contrast graphite/paper layers. Interactive
20
20
  boundaries use the semantic border token and the shared rounded geometry. Floating
21
- chat and modal surfaces use `shadow.raised` or `shadow.overlay` plus the quiet mint
22
- ambient gradient from the official reference. Do not create a separate app palette.
21
+ chat and modal surfaces remain flat, using a quiet mint ambient layer or overlay
22
+ scrim when separation is needed; `shadow.raised` and `shadow.overlay` stay `none`.
23
+ Do not create a separate app palette.
23
24
 
24
25
  ## Product mapping
25
26
 
@@ -47,6 +47,19 @@ For application pages, compose `app-frame`, `page-header`, `sidebar-navigation`,
47
47
  Use the [flat surface and navigation policy](surface-policy.md) to decide surface
48
48
  tone, CTA hierarchy, adaptive navigation, icon coloring, and review evidence.
49
49
 
50
+ ### Finance application shell
51
+
52
+ For NOCFO-class workspaces use `AppFrame variant="finance"` with
53
+ `HeaderNavigation` and `SidebarNavigation`. It uses a 280px desktop navigation
54
+ column, a compact 56px header, and semantic background/surface/sunken layers for
55
+ separation. It never solves Persian or English layout with `row-reverse`: icons
56
+ render at the logical start and end adornments use `margin-inline-start`.
57
+
58
+ The shell is deliberately flat. Sections and cards are differentiated by the
59
+ surface tokens and spacing; their outer stroke and every shadow are absent. Use
60
+ the standard `Button variant="secondary"` for transparent, currentColor outline
61
+ actions. It is white on dark/colored surfaces and graphite on paper.
62
+
50
63
  Mapping to the Untitled UI reference taxonomy (honest as of 2026-08-27 — foundation stabilization):
51
64
 
52
65
  | Untitled UI category | NOE coverage |
@@ -14,16 +14,18 @@ passes caused by arbitrary shadows, card borders, or ambiguous mobile navigation
14
14
  `appearance="outlined"` is reserved for a selection, data-entry, or comparison
15
15
  boundary where a stroke conveys real meaning.
16
16
 
17
- Use the shared shadow tokens only for floating chat, drawer, popover and modal
18
- layers. Sections and ordinary cards do not receive ad-hoc elevation. A focus state
19
- is an outline/ring, not elevation.
17
+ NOE surfaces are fully flat: `shadow.raised` and `shadow.overlay` are `none` in
18
+ every theme, including dialogs, drawers, menus and popovers. Separation comes from
19
+ semantic surface color, radius, spacing and an overlay scrim—not shadow. Sections
20
+ and ordinary cards do not receive ad-hoc elevation. A focus state is an
21
+ outline/ring, not elevation.
20
22
 
21
23
  ## Actions
22
24
 
23
25
  - One screen has one contained primary CTA.
24
- - Secondary actions are outline or ghost actions. On a dark or colored surface,
25
- a white outline action remains transparent with a white label and stroke; never
26
- give it an opaque white fill that removes its hierarchy.
26
+ - Secondary actions are transparent outline actions. Their label and stroke use
27
+ `currentColor`; therefore the standard foreground is white in dark/colored
28
+ contexts and graphite on paper. Never give a secondary action an opaque fill.
27
29
  - Destructive actions retain an explicit label and confirmation/recovery path;
28
30
  color alone is never the signal.
29
31
  - Every focusable action keeps a visible `focus-visible` outline.
@@ -52,8 +54,9 @@ is an outline/ring, not elevation.
52
54
 
53
55
  - Test every route in Persian/RTL and English/LTR, in light and dark themes.
54
56
  - Verify no page-level horizontal overflow at the mobile breakpoint.
55
- - Verify sections/cards have no visible outer stroke or ad-hoc shadow unless they
56
- are an explicitly outlined control or a floating overlay using `shadow.overlay`.
57
+ - Verify sections/cards have no visible outer stroke or shadow unless they are an
58
+ explicitly outlined control (for example a secondary action, input or table
59
+ affordance).
57
60
  - Verify interactive targets are at least 44 px on touch devices and have
58
61
  accessible names.
59
62
  - Test desktop sidebar and mobile drawer independently, including keyboard focus
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noecosystem-design",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "Agent-readable NOE design-system source, registry metadata, and verification contracts.",
6
6
  "keywords": [
@@ -159,8 +159,8 @@
159
159
  --noe-color-info-foreground: var(--noe-primitive-color-paper-0);
160
160
  --noe-color-focus-ring: var(--noe-primitive-color-alpha-focus-light);
161
161
  --noe-color-overlay: var(--noe-primitive-color-alpha-overlay-light);
162
- --noe-shadow-raised: 0 8px 20px rgba(23, 25, 24, 0.055);
163
- --noe-shadow-overlay: 0 36px 100px rgba(23, 25, 24, 0.28);
162
+ --noe-shadow-raised: none;
163
+ --noe-shadow-overlay: none;
164
164
  }
165
165
 
166
166
  /* Semantic tokens: explicit dark theme. */
@@ -189,8 +189,8 @@
189
189
  --noe-color-info-foreground: var(--noe-primitive-color-ink-950);
190
190
  --noe-color-focus-ring: var(--noe-primitive-color-alpha-focus-dark);
191
191
  --noe-color-overlay: var(--noe-primitive-color-alpha-overlay-dark);
192
- --noe-shadow-raised: 0 8px 20px rgba(0, 0, 0, 0.14);
193
- --noe-shadow-overlay: 0 36px 100px rgba(0, 0, 0, 0.42);
192
+ --noe-shadow-raised: none;
193
+ --noe-shadow-overlay: none;
194
194
  }
195
195
 
196
196
  /* Semantic tokens: follow the OS only when no explicit theme is set. */
@@ -220,8 +220,8 @@
220
220
  --noe-color-info-foreground: var(--noe-primitive-color-ink-950);
221
221
  --noe-color-focus-ring: var(--noe-primitive-color-alpha-focus-dark);
222
222
  --noe-color-overlay: var(--noe-primitive-color-alpha-overlay-dark);
223
- --noe-shadow-raised: 0 8px 20px rgba(0, 0, 0, 0.14);
224
- --noe-shadow-overlay: 0 36px 100px rgba(0, 0, 0, 0.42);
223
+ --noe-shadow-raised: none;
224
+ --noe-shadow-overlay: none;
225
225
  }
226
226
  }
227
227
 
@@ -154,12 +154,12 @@
154
154
  },
155
155
  "shadow": {
156
156
  "light": {
157
- "raised": "0 8px 20px rgba(23, 25, 24, 0.055)",
158
- "overlay": "0 36px 100px rgba(23, 25, 24, 0.28)"
157
+ "raised": "none",
158
+ "overlay": "none"
159
159
  },
160
160
  "dark": {
161
- "raised": "0 8px 20px rgba(0, 0, 0, 0.14)",
162
- "overlay": "0 36px 100px rgba(0, 0, 0, 0.42)"
161
+ "raised": "none",
162
+ "overlay": "none"
163
163
  }
164
164
  },
165
165
  "font": {
@@ -155,12 +155,12 @@ export const tokens = {
155
155
  },
156
156
  "shadow": {
157
157
  "light": {
158
- "raised": "0 8px 20px rgba(23, 25, 24, 0.055)",
159
- "overlay": "0 36px 100px rgba(23, 25, 24, 0.28)"
158
+ "raised": "none",
159
+ "overlay": "none"
160
160
  },
161
161
  "dark": {
162
- "raised": "0 8px 20px rgba(0, 0, 0, 0.14)",
163
- "overlay": "0 36px 100px rgba(0, 0, 0, 0.42)"
162
+ "raised": "none",
163
+ "overlay": "none"
164
164
  }
165
165
  },
166
166
  "font": {
@@ -3,7 +3,7 @@
3
3
  "name": "app-frame",
4
4
  "type": "registry:block",
5
5
  "title": "App Frame",
6
- "description": "App Frame — Patterns / Pages component. app-frame provides patterns / pages behavior with RTL, i18n and accessibility built-in.",
6
+ "description": "App Frame — Patterns / Pages component. Includes a flat finance variant with a 280px logical sidebar, compact header and color-separated surfaces; RTL, i18n and accessibility are built in.",
7
7
  "files": [
8
8
  {
9
9
  "path": "packages/ui/src/app-frame.tsx",
@@ -37,8 +37,16 @@
37
37
  "announcements": [],
38
38
  "focusBehavior": ["visible ring 2px"]
39
39
  },
40
- "tokenContracts": ["color.foreground", "color.border"],
41
- "capabilityAliases": ["app frame", "app-frame"],
42
- "useWhen": ["using app frame in Patterns / Pages"],
40
+ "tokenContracts": [
41
+ "color.background",
42
+ "color.surface",
43
+ "color.surfaceRaised",
44
+ "color.surfaceSunken"
45
+ ],
46
+ "capabilityAliases": ["app frame", "app-frame", "finance application shell", "admin shell"],
47
+ "useWhen": [
48
+ "using app frame in Patterns / Pages",
49
+ "building a flat finance or admin application shell"
50
+ ],
43
51
  "doNotUseWhen": ["when app frame semantics do not match"]
44
52
  }
@@ -3,7 +3,7 @@
3
3
  "name": "button",
4
4
  "type": "registry:ui",
5
5
  "title": "Button",
6
- "description": "Primary action primitive with semantic variants, focus visibility, loading state and bidirectional layout. Use for all interactive actions.",
6
+ "description": "Primary action primitive with semantic variants, focus visibility, loading state and bidirectional layout. Secondary is transparent with a currentColor stroke and label.",
7
7
  "files": [
8
8
  {
9
9
  "path": "packages/ui/src/button.tsx",
@@ -3,7 +3,7 @@
3
3
  "name": "header-navigation",
4
4
  "type": "registry:block",
5
5
  "title": "Header Navigation",
6
- "description": "Header Navigation — Patterns / Application component. header-navigation provides patterns / application behavior with RTL, i18n and accessibility built-in.",
6
+ "description": "Header Navigation — Patterns / Application component. Provides a compact 56px application header with RTL, i18n and accessibility built in.",
7
7
  "files": [
8
8
  {
9
9
  "path": "packages/ui/src/header-navigation.tsx",
@@ -37,8 +37,8 @@
37
37
  "announcements": [],
38
38
  "focusBehavior": ["visible ring 2px"]
39
39
  },
40
- "tokenContracts": ["color.foreground", "color.border"],
41
- "capabilityAliases": ["header navigation", "header-navigation"],
40
+ "tokenContracts": ["color.foreground", "color.surfaceRaised", "control.height.md"],
41
+ "capabilityAliases": ["header navigation", "header-navigation", "compact app header"],
42
42
  "useWhen": ["using header navigation in Patterns / Application"],
43
43
  "doNotUseWhen": ["when header navigation semantics do not match"]
44
44
  }
@@ -3,7 +3,7 @@
3
3
  "name": "sidebar-navigation",
4
4
  "type": "registry:block",
5
5
  "title": "Sidebar Navigation",
6
- "description": "Sidebar Navigation — Patterns / Application component. sidebar-navigation provides patterns / application behavior with RTL, i18n and accessibility built-in.",
6
+ "description": "Sidebar Navigation — Patterns / Application component. Uses logical icon, label and end-adornment order without row reversal; RTL, i18n and accessibility are built in.",
7
7
  "files": [
8
8
  {
9
9
  "path": "packages/ui/src/lib/cn.ts",
@@ -37,8 +37,13 @@
37
37
  "announcements": [],
38
38
  "focusBehavior": ["visible ring 2px"]
39
39
  },
40
- "tokenContracts": ["color.foreground", "color.border"],
41
- "capabilityAliases": ["sidebar navigation", "sidebar-navigation"],
40
+ "tokenContracts": [
41
+ "color.foreground",
42
+ "color.surface",
43
+ "color.surfaceRaised",
44
+ "color.mutedForeground"
45
+ ],
46
+ "capabilityAliases": ["sidebar navigation", "sidebar-navigation", "finance sidebar"],
42
47
  "useWhen": ["using sidebar navigation in Patterns / Application"],
43
48
  "doNotUseWhen": ["when sidebar navigation semantics do not match"]
44
49
  }
@@ -0,0 +1,47 @@
1
+ import { expect, test } from 'vitest';
2
+ import { render } from 'vitest-browser-react';
3
+
4
+ import { DirectionProvider } from './direction';
5
+ import { AppFrame } from './app-frame';
6
+ import { SidebarNavigation } from './sidebar-navigation';
7
+
8
+ test('finance app frame keeps a flat, logical RTL sidebar structure', async () => {
9
+ const screen = await render(
10
+ <DirectionProvider direction="rtl">
11
+ <AppFrame
12
+ contentId="finance-main"
13
+ data-testid="finance-app-frame"
14
+ navigation={
15
+ <SidebarNavigation
16
+ label="ناوبری"
17
+ sections={[
18
+ {
19
+ id: 'finance',
20
+ items: [
21
+ {
22
+ href: '#dashboard',
23
+ icon: <svg data-testid="dashboard-icon" />,
24
+ label: 'داشبورد',
25
+ current: true,
26
+ },
27
+ ],
28
+ },
29
+ ]}
30
+ />
31
+ }
32
+ variant="finance"
33
+ >
34
+ <p>محتوا</p>
35
+ </AppFrame>
36
+ </DirectionProvider>,
37
+ );
38
+
39
+ const frame = screen.getByTestId('finance-app-frame');
40
+ await expect.element(frame).toHaveAttribute('data-variant', 'finance');
41
+ await expect.element(screen.getByRole('navigation', { name: 'ناوبری' })).toBeVisible();
42
+ await expect
43
+ .element(screen.getByRole('link', { name: 'داشبورد' }))
44
+ .toHaveAttribute('aria-current', 'page');
45
+ await expect.element(screen.getByTestId('dashboard-icon')).toBeVisible();
46
+ await expect.element(screen.getByRole('main')).toHaveAttribute('id', 'finance-main');
47
+ });
@@ -1,36 +1,55 @@
1
+ import { useId } from 'react';
1
2
  import type * as React from 'react';
2
3
 
3
4
  import { cn } from './lib/cn';
4
5
 
5
6
  export interface AppFrameProps extends React.ComponentProps<'div'> {
7
+ /** `finance` is the compact, flat shell for dense business applications. */
8
+ variant?: 'default' | 'finance';
6
9
  header?: React.ReactNode;
7
10
  navigation?: React.ReactNode;
8
11
  mobileNavigation?: React.ReactNode;
9
12
  inspector?: React.ReactNode;
13
+ contentId?: string;
10
14
  children: React.ReactNode;
11
15
  }
12
16
 
13
17
  export function AppFrame({
18
+ variant = 'default',
14
19
  header,
15
20
  navigation,
16
21
  mobileNavigation,
17
22
  inspector,
23
+ contentId,
18
24
  children,
19
25
  className,
20
26
  ...props
21
27
  }: AppFrameProps) {
28
+ const isFinanceShell = variant === 'finance';
29
+ const generatedContentId = useId();
22
30
  return (
23
31
  <div
24
- className={cn('grid min-h-dvh w-full grid-rows-[auto_1fr]', className)}
32
+ className={cn(
33
+ 'grid min-h-dvh w-full grid-rows-[auto_1fr] bg-background font-sans text-foreground',
34
+ className,
35
+ )}
25
36
  data-slot="app-frame"
37
+ data-variant={variant}
26
38
  {...props}
27
39
  >
28
40
  {header ? <div data-slot="app-frame-header">{header}</div> : null}
29
- <div className="grid lg:grid-cols-[15rem_minmax(0,1fr)] xl:grid-cols-[15rem_minmax(0,1fr)_18rem]">
41
+ <div
42
+ className={cn(
43
+ 'grid',
44
+ isFinanceShell
45
+ ? 'lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_18rem]'
46
+ : 'lg:grid-cols-[15rem_minmax(0,1fr)] xl:grid-cols-[15rem_minmax(0,1fr)_18rem]',
47
+ )}
48
+ >
30
49
  {mobileNavigation ? (
31
50
  <nav
32
51
  aria-label="Primary"
33
- className="border-b border-border bg-surface-raised p-4 lg:hidden"
52
+ className="bg-surface p-4 lg:hidden"
34
53
  data-slot="app-frame-mobile-navigation"
35
54
  >
36
55
  {mobileNavigation}
@@ -39,19 +58,27 @@ export function AppFrame({
39
58
  {navigation ? (
40
59
  <aside
41
60
  aria-label="Primary"
42
- className="hidden border-e border-border bg-surface-raised p-4 lg:block"
61
+ className={cn(
62
+ 'hidden p-4 lg:block',
63
+ isFinanceShell ? 'bg-surface-sunken px-3 py-4' : 'bg-surface-raised',
64
+ )}
43
65
  data-slot="app-frame-navigation"
44
66
  >
45
67
  {navigation}
46
68
  </aside>
47
69
  ) : null}
48
- <main className="min-w-0 p-4 sm:p-6" data-slot="app-frame-content">
70
+ <main
71
+ className={cn('min-w-0 bg-background p-4 sm:p-6', isFinanceShell && 'sm:p-5')}
72
+ data-slot="app-frame-content"
73
+ id={contentId ?? generatedContentId}
74
+ tabIndex={-1}
75
+ >
49
76
  {children}
50
77
  </main>
51
78
  {inspector ? (
52
79
  <aside
53
80
  aria-label="Inspector"
54
- className="hidden border-s border-border bg-surface-raised p-5 xl:block"
81
+ className="hidden bg-surface p-5 xl:block"
55
82
  data-slot="app-frame-inspector"
56
83
  >
57
84
  {inspector}
@@ -36,3 +36,11 @@ test('a disabled polymorphic Button cannot navigate or receive focus', async ()
36
36
  expect(onClick).not.toHaveBeenCalled();
37
37
  expect(window.location.hash).not.toBe('#danger');
38
38
  });
39
+
40
+ test('secondary Button remains a transparent currentColor outline', async () => {
41
+ const screen = await render(<Button variant="secondary">Reject</Button>);
42
+ const button = screen.getByRole('button', { name: 'Reject' });
43
+ await expect.element(button).toHaveClass('border-current');
44
+ await expect.element(button).toHaveClass('bg-transparent');
45
+ await expect.element(button).toHaveClass('text-foreground');
46
+ });
@@ -12,7 +12,7 @@ export const buttonVariants = cva(
12
12
  variants: {
13
13
  variant: {
14
14
  primary: 'border-accent bg-accent text-accent-foreground hover:bg-accent-hover',
15
- secondary: 'border-border bg-surface-raised text-foreground hover:bg-muted',
15
+ secondary: 'border-current bg-transparent text-foreground hover:bg-muted/70',
16
16
  outline: 'border-border bg-transparent text-foreground hover:bg-muted',
17
17
  ghost: 'border-transparent bg-transparent text-foreground hover:bg-muted',
18
18
  quiet: 'border-transparent bg-transparent text-foreground hover:bg-muted',
@@ -29,7 +29,7 @@ export function HeaderNavigation({
29
29
  return (
30
30
  <header
31
31
  className={cn(
32
- 'flex w-full items-center gap-x-6 gap-y-2 bg-surface-raised px-4 py-3',
32
+ 'flex min-h-14 w-full items-center gap-x-4 gap-y-2 bg-surface-raised px-4 py-2',
33
33
  className,
34
34
  )}
35
35
  data-slot="header-navigation"
@@ -6,8 +6,11 @@ export interface SidebarNavigationItem {
6
6
  id?: string;
7
7
  href: string;
8
8
  label: React.ReactNode;
9
+ /** Rendered at the logical start; follows document direction without row reversal. */
10
+ icon?: React.ReactNode;
9
11
  current?: boolean;
10
12
  badge?: React.ReactNode;
13
+ endAdornment?: React.ReactNode;
11
14
  }
12
15
 
13
16
  export interface SidebarNavigationSection {
@@ -53,16 +56,24 @@ export function SidebarNavigation({
53
56
  <a
54
57
  aria-current={item.current ? 'page' : undefined}
55
58
  className={cn(
56
- 'flex items-center gap-2 rounded-md px-3 py-2 text-sm leading-normal no-underline hover:bg-surface-sunken/60',
59
+ 'flex min-h-11 items-center gap-2 rounded-md px-3 py-2 text-sm leading-normal no-underline hover:bg-surface/80',
57
60
  item.current
58
- ? 'bg-surface-sunken font-medium text-foreground'
61
+ ? 'bg-surface-raised font-medium text-foreground'
59
62
  : 'text-muted-foreground',
60
63
  )}
61
64
  data-current={item.current || undefined}
62
65
  data-slot="sidebar-navigation-item"
63
66
  href={item.href}
64
67
  >
68
+ {item.icon ? (
69
+ <span aria-hidden="true" className="shrink-0 text-current [&_svg]:size-5">
70
+ {item.icon}
71
+ </span>
72
+ ) : null}
65
73
  <span className="min-w-0 flex-1 truncate">{item.label}</span>
74
+ {item.endAdornment ? (
75
+ <span className="ms-auto shrink-0 text-current">{item.endAdornment}</span>
76
+ ) : null}
66
77
  {item.badge ? (
67
78
  <span className="ms-auto shrink-0 text-xs tabular-nums">{item.badge}</span>
68
79
  ) : null}
@@ -23,22 +23,16 @@ test('token CSS includes theme and reduced-motion contracts', async () => {
23
23
  assert.match(css, /--noe-primitive-color-mint-100:\s*#[0-9a-f]{6}/i);
24
24
  });
25
25
 
26
- test('surface elevation follows the official NOE soft-depth contract', async () => {
27
- assert.match(tokens.shadow.light.raised, /rgba\(23, 25, 24, 0\.055\)/);
28
- assert.match(tokens.shadow.light.overlay, /rgba\(23, 25, 24, 0\.28\)/);
29
- assert.match(tokens.shadow.dark.raised, /rgba\(0, 0, 0, 0\.14\)/);
30
- assert.match(tokens.shadow.dark.overlay, /rgba\(0, 0, 0, 0\.42\)/);
26
+ test('surfaces remain flat in every NOE theme', async () => {
27
+ assert.equal(tokens.shadow.light.raised, 'none');
28
+ assert.equal(tokens.shadow.light.overlay, 'none');
29
+ assert.equal(tokens.shadow.dark.raised, 'none');
30
+ assert.equal(tokens.shadow.dark.overlay, 'none');
31
31
 
32
32
  const css = await readFile(
33
33
  new URL('../packages/design-tokens/src/tokens.css', import.meta.url),
34
34
  'utf8',
35
35
  );
36
- assert.match(
37
- css,
38
- /\[data-theme="light"\][\s\S]*?--noe-shadow-raised:\s*0 8px 20px rgba\(23, 25, 24, 0\.055\)/,
39
- );
40
- assert.match(
41
- css,
42
- /\[data-theme="dark"\][\s\S]*?--noe-shadow-overlay:\s*0 36px 100px rgba\(0, 0, 0, 0\.42\)/,
43
- );
36
+ assert.match(css, /\[data-theme="light"\][\s\S]*?--noe-shadow-raised:\s*none/);
37
+ assert.match(css, /\[data-theme="dark"\][\s\S]*?--noe-shadow-overlay:\s*none/);
44
38
  });