noecosystem-design 0.2.1 → 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.
- package/apps/storybook/stories/catalog/app-frame.stories.tsx +43 -0
- package/apps/storybook/stories/catalog/button.stories.tsx +1 -0
- package/apps/storybook/stories/catalog/sidebar-navigation.stories.tsx +22 -0
- package/docs/design-system/brand-theme.md +32 -0
- package/docs/design-system/components.md +13 -0
- package/docs/design-system/foundations.md +6 -4
- package/docs/design-system/surface-policy.md +11 -7
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/packages/design-tokens/src/tokens.css +7 -7
- package/packages/design-tokens/src/tokens.json +5 -5
- package/packages/design-tokens/src/tokens.mjs +5 -5
- package/packages/registry/r/app-frame.json +12 -4
- package/packages/registry/r/button.json +1 -1
- package/packages/registry/r/header-navigation.json +3 -3
- package/packages/registry/r/sidebar-navigation.json +8 -3
- package/packages/ui/src/app-frame.browser.test.tsx +47 -0
- package/packages/ui/src/app-frame.tsx +33 -6
- package/packages/ui/src/button.browser.test.tsx +8 -0
- package/packages/ui/src/button.tsx +1 -1
- package/packages/ui/src/color-picker.tsx +4 -6
- package/packages/ui/src/header-navigation.tsx +1 -1
- package/packages/ui/src/sidebar-navigation.tsx +13 -2
- package/tests/tokens.test.mjs +1 -1
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Official NOE brand theme
|
|
2
|
+
|
|
3
|
+
The NOEcosystem landing is the canonical visual identity for NOE products. Product
|
|
4
|
+
interfaces use the same language rather than a related palette.
|
|
5
|
+
|
|
6
|
+
## Foundation
|
|
7
|
+
|
|
8
|
+
- Light canvas: paper `#f3f0ea`, raised paper `#f6f4f2` and bright reading surface.
|
|
9
|
+
- Dark canvas: graphite `#252423`, raised graphite `#302f2e`.
|
|
10
|
+
- Primary action: mint `#98e8cf`; hover mint `#abefd9`; the label is graphite.
|
|
11
|
+
- Supporting interaction and focus: deep teal `#0f6d5b` / `#0b6f6b`.
|
|
12
|
+
- Signal: coral `#ff5b57` / `#c43f3c`; cyan, amber and violet remain status/data
|
|
13
|
+
signals, not the default product CTA.
|
|
14
|
+
- Latin copy uses the SF system stack; Persian copy uses IRANYekanXFaNum.
|
|
15
|
+
|
|
16
|
+
## Composition
|
|
17
|
+
|
|
18
|
+
Use large, confident headings; quiet metadata with tracked uppercase only for
|
|
19
|
+
Latin labels; generous spacing; and low-contrast graphite/paper layers. Interactive
|
|
20
|
+
boundaries use the semantic border token and the shared rounded geometry. Floating
|
|
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.
|
|
24
|
+
|
|
25
|
+
## Product mapping
|
|
26
|
+
|
|
27
|
+
- Chat composer/send action: mint fill, graphite icon/text, deep-teal focus ring.
|
|
28
|
+
- Assistant/AI surfaces: graphite/paper raised surface with a quiet mint radial
|
|
29
|
+
ambient layer; do not use blue/cyan as the product identity.
|
|
30
|
+
- Admin, expert and profile shells: the same paper/graphite canvas, thin utility
|
|
31
|
+
dividers, and one mint primary action per region.
|
|
32
|
+
- Destructive paths continue to use coral and must retain explicit labels.
|
|
@@ -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 |
|
|
@@ -37,12 +37,14 @@ Radii are compact and semantic. Controls use shared heights. Cards are not neste
|
|
|
37
37
|
|
|
38
38
|
## Surface hierarchy
|
|
39
39
|
|
|
40
|
-
NOE uses
|
|
41
|
-
semantic background tone, spacing, and radius
|
|
40
|
+
NOE uses graphite/paper surface hierarchy: page canvas and raised regions are
|
|
41
|
+
separated first by semantic background tone, spacing, and radius. The official
|
|
42
|
+
NOEcosystem visual language adds soft ambient elevation only to floating chat,
|
|
43
|
+
drawers and modal layers; it is never used as chrome around every section.
|
|
42
44
|
`Card` therefore defaults to a raised, borderless appearance. Its outlined
|
|
43
45
|
appearance is only for controls or data boundaries with a meaningful selection
|
|
44
|
-
state. See [
|
|
45
|
-
product-level contract.
|
|
46
|
+
state. See [Official NOE brand theme](brand-theme.md) and [Flat surface and
|
|
47
|
+
navigation policy](surface-policy.md) for the product-level contract.
|
|
46
48
|
|
|
47
49
|
## Responsive behavior
|
|
48
50
|
|
|
@@ -14,15 +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
|
-
NOE
|
|
18
|
-
|
|
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.
|
|
19
22
|
|
|
20
23
|
## Actions
|
|
21
24
|
|
|
22
25
|
- One screen has one contained primary CTA.
|
|
23
|
-
- Secondary actions are outline
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
26
29
|
- Destructive actions retain an explicit label and confirmation/recovery path;
|
|
27
30
|
color alone is never the signal.
|
|
28
31
|
- Every focusable action keeps a visible `focus-visible` outline.
|
|
@@ -51,8 +54,9 @@ to product styles. A focus state is an outline/ring, not elevation.
|
|
|
51
54
|
|
|
52
55
|
- Test every route in Persian/RTL and English/LTR, in light and dark themes.
|
|
53
56
|
- Verify no page-level horizontal overflow at the mobile breakpoint.
|
|
54
|
-
- Verify
|
|
55
|
-
|
|
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).
|
|
56
60
|
- Verify interactive targets are at least 44 px on touch devices and have
|
|
57
61
|
accessible names.
|
|
58
62
|
- Test desktop sidebar and mobile drawer independently, including keyboard focus
|
package/llms.txt
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
- Components and catalog terms: `docs/design-system/components.md`
|
|
34
34
|
- Tokens and foundations: `docs/design-system/foundations.md`, `docs/design-system/tokens.md`
|
|
35
35
|
- Flat cards, action hierarchy and adaptive application navigation: `docs/design-system/surface-policy.md`
|
|
36
|
+
- Official NOEcosystem visual-language contract: `docs/design-system/brand-theme.md`
|
|
36
37
|
- Locale, RTL, and bidi: `docs/design-system/i18n.md`, `docs/design-system/rtl.md`
|
|
37
38
|
- Accessibility: `docs/design-system/accessibility.md`
|
|
38
39
|
- Agent and tool-state UI: `docs/design-system/agentic-ui.md`
|
package/package.json
CHANGED
|
@@ -145,9 +145,9 @@
|
|
|
145
145
|
--noe-color-muted-foreground: var(--noe-primitive-color-ink-500);
|
|
146
146
|
--noe-color-border: rgba(23, 25, 24, 0.16);
|
|
147
147
|
--noe-color-border-strong: var(--noe-primitive-color-ink-300);
|
|
148
|
-
--noe-color-accent: var(--noe-primitive-color-
|
|
149
|
-
--noe-color-accent-hover: var(--noe-primitive-color-
|
|
150
|
-
--noe-color-accent-foreground: var(--noe-primitive-color-
|
|
148
|
+
--noe-color-accent: var(--noe-primitive-color-mint-100);
|
|
149
|
+
--noe-color-accent-hover: var(--noe-primitive-color-mint-200);
|
|
150
|
+
--noe-color-accent-foreground: var(--noe-primitive-color-ink-900);
|
|
151
151
|
--noe-color-signal: var(--noe-primitive-color-coral-500);
|
|
152
152
|
--noe-color-success: var(--noe-primitive-color-green-600);
|
|
153
153
|
--noe-color-success-foreground: var(--noe-primitive-color-paper-0);
|
|
@@ -175,8 +175,8 @@
|
|
|
175
175
|
--noe-color-muted-foreground: var(--noe-primitive-color-ink-400);
|
|
176
176
|
--noe-color-border: rgba(246, 244, 242, 0.14);
|
|
177
177
|
--noe-color-border-strong: var(--noe-primitive-color-ink-600);
|
|
178
|
-
--noe-color-accent: var(--noe-primitive-color-
|
|
179
|
-
--noe-color-accent-hover: var(--noe-primitive-color-
|
|
178
|
+
--noe-color-accent: var(--noe-primitive-color-mint-100);
|
|
179
|
+
--noe-color-accent-hover: var(--noe-primitive-color-mint-200);
|
|
180
180
|
--noe-color-accent-foreground: var(--noe-primitive-color-ink-950);
|
|
181
181
|
--noe-color-signal: var(--noe-primitive-color-coral-400);
|
|
182
182
|
--noe-color-success: var(--noe-primitive-color-green-500);
|
|
@@ -206,8 +206,8 @@
|
|
|
206
206
|
--noe-color-muted-foreground: var(--noe-primitive-color-ink-400);
|
|
207
207
|
--noe-color-border: rgba(246, 244, 242, 0.14);
|
|
208
208
|
--noe-color-border-strong: var(--noe-primitive-color-ink-600);
|
|
209
|
-
--noe-color-accent: var(--noe-primitive-color-
|
|
210
|
-
--noe-color-accent-hover: var(--noe-primitive-color-
|
|
209
|
+
--noe-color-accent: var(--noe-primitive-color-mint-100);
|
|
210
|
+
--noe-color-accent-hover: var(--noe-primitive-color-mint-200);
|
|
211
211
|
--noe-color-accent-foreground: var(--noe-primitive-color-ink-950);
|
|
212
212
|
--noe-color-signal: var(--noe-primitive-color-coral-400);
|
|
213
213
|
--noe-color-success: var(--noe-primitive-color-green-500);
|
|
@@ -111,9 +111,9 @@
|
|
|
111
111
|
"mutedForeground": "{primitive.color.ink.500}",
|
|
112
112
|
"border": "rgba(23, 25, 24, 0.16)",
|
|
113
113
|
"borderStrong": "{primitive.color.ink.300}",
|
|
114
|
-
"accent": "{primitive.color.
|
|
115
|
-
"accentHover": "{primitive.color.
|
|
116
|
-
"accentForeground": "{primitive.color.
|
|
114
|
+
"accent": "{primitive.color.mint.100}",
|
|
115
|
+
"accentHover": "{primitive.color.mint.200}",
|
|
116
|
+
"accentForeground": "{primitive.color.ink.900}",
|
|
117
117
|
"signal": "{primitive.color.coral.500}",
|
|
118
118
|
"success": "{primitive.color.green.600}",
|
|
119
119
|
"successForeground": "{primitive.color.paper.0}",
|
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
"mutedForeground": "{primitive.color.ink.400}",
|
|
137
137
|
"border": "rgba(246, 244, 242, 0.14)",
|
|
138
138
|
"borderStrong": "{primitive.color.ink.600}",
|
|
139
|
-
"accent": "{primitive.color.
|
|
140
|
-
"accentHover": "{primitive.color.
|
|
139
|
+
"accent": "{primitive.color.mint.100}",
|
|
140
|
+
"accentHover": "{primitive.color.mint.200}",
|
|
141
141
|
"accentForeground": "{primitive.color.ink.950}",
|
|
142
142
|
"signal": "{primitive.color.coral.400}",
|
|
143
143
|
"success": "{primitive.color.green.500}",
|
|
@@ -112,9 +112,9 @@ export const tokens = {
|
|
|
112
112
|
"mutedForeground": "{primitive.color.ink.500}",
|
|
113
113
|
"border": "rgba(23, 25, 24, 0.16)",
|
|
114
114
|
"borderStrong": "{primitive.color.ink.300}",
|
|
115
|
-
"accent": "{primitive.color.
|
|
116
|
-
"accentHover": "{primitive.color.
|
|
117
|
-
"accentForeground": "{primitive.color.
|
|
115
|
+
"accent": "{primitive.color.mint.100}",
|
|
116
|
+
"accentHover": "{primitive.color.mint.200}",
|
|
117
|
+
"accentForeground": "{primitive.color.ink.900}",
|
|
118
118
|
"signal": "{primitive.color.coral.500}",
|
|
119
119
|
"success": "{primitive.color.green.600}",
|
|
120
120
|
"successForeground": "{primitive.color.paper.0}",
|
|
@@ -137,8 +137,8 @@ export const tokens = {
|
|
|
137
137
|
"mutedForeground": "{primitive.color.ink.400}",
|
|
138
138
|
"border": "rgba(246, 244, 242, 0.14)",
|
|
139
139
|
"borderStrong": "{primitive.color.ink.600}",
|
|
140
|
-
"accent": "{primitive.color.
|
|
141
|
-
"accentHover": "{primitive.color.
|
|
140
|
+
"accent": "{primitive.color.mint.100}",
|
|
141
|
+
"accentHover": "{primitive.color.mint.200}",
|
|
142
142
|
"accentForeground": "{primitive.color.ink.950}",
|
|
143
143
|
"signal": "{primitive.color.coral.400}",
|
|
144
144
|
"success": "{primitive.color.green.500}",
|
|
@@ -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.
|
|
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": [
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": [
|
|
41
|
-
|
|
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(
|
|
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
|
|
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="
|
|
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=
|
|
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
|
|
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
|
|
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-
|
|
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',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type * as React from 'react';
|
|
2
2
|
import { cn } from './lib/cn';
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
const DEFAULT_ACCENT_HEX = '#0b6f6b'; // primitive mint-500
|
|
4
|
+
// The native picker needs a literal hex. Its swatch demonstrates deep mint while
|
|
5
|
+
// the adjacent value keeps the standard readable text foreground.
|
|
6
|
+
const DEFAULT_ACCENT_HEX = '#0b6f6b'; // primitive mint-500 — keep in sync with tokens.json
|
|
7
7
|
|
|
8
8
|
export function ColorPicker({ className, ...props }: React.ComponentProps<'div'>) {
|
|
9
9
|
return (
|
|
@@ -14,9 +14,7 @@ export function ColorPicker({ className, ...props }: React.ComponentProps<'div'>
|
|
|
14
14
|
className="size-8 rounded-md border border-border"
|
|
15
15
|
defaultValue={DEFAULT_ACCENT_HEX}
|
|
16
16
|
/>
|
|
17
|
-
<span className="text-sm text-muted-foreground"
|
|
18
|
-
{DEFAULT_ACCENT_HEX}
|
|
19
|
-
</span>
|
|
17
|
+
<span className="text-sm text-muted-foreground">{DEFAULT_ACCENT_HEX}</span>
|
|
20
18
|
</div>
|
|
21
19
|
);
|
|
22
20
|
}
|
|
@@ -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-
|
|
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
|
|
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-
|
|
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}
|
package/tests/tokens.test.mjs
CHANGED
|
@@ -23,7 +23,7 @@ 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('
|
|
26
|
+
test('surfaces remain flat in every NOE theme', async () => {
|
|
27
27
|
assert.equal(tokens.shadow.light.raised, 'none');
|
|
28
28
|
assert.equal(tokens.shadow.light.overlay, 'none');
|
|
29
29
|
assert.equal(tokens.shadow.dark.raised, 'none');
|