noecosystem-design 0.2.0 → 0.2.2
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/card.stories.tsx +13 -0
- package/docs/design-system/brand-theme.md +31 -0
- package/docs/design-system/components.md +5 -0
- package/docs/design-system/foundations.md +11 -0
- package/docs/design-system/surface-policy.md +60 -0
- package/llms.txt +2 -0
- package/package.json +1 -1
- package/packages/design-tokens/src/tokens.css +13 -13
- package/packages/design-tokens/src/tokens.json +9 -9
- package/packages/design-tokens/src/tokens.mjs +9 -9
- package/packages/registry/catalog-index.json +2 -2
- package/packages/registry/manifest.json +27 -14
- package/packages/registry/r/card.json +11 -5
- package/packages/registry/registry.json +8 -2
- package/packages/registry/search-index.json +21 -8
- package/packages/ui/src/card.tsx +14 -2
- package/packages/ui/src/color-picker.tsx +4 -6
- package/scripts/generate-manifest.mjs +36 -1
- package/tests/tokens.test.mjs +20 -0
|
@@ -38,3 +38,16 @@ export const Default: Story = {
|
|
|
38
38
|
</Card>
|
|
39
39
|
),
|
|
40
40
|
};
|
|
41
|
+
|
|
42
|
+
export const Outlined: Story = {
|
|
43
|
+
render: () => (
|
|
44
|
+
<Card appearance="outlined" style={{ maxWidth: '24rem' }}>
|
|
45
|
+
<CardHeader>
|
|
46
|
+
<CardTitle>Selection boundary</CardTitle>
|
|
47
|
+
<CardDescription>
|
|
48
|
+
Use this only when a visible boundary conveys interaction or data-entry state.
|
|
49
|
+
</CardDescription>
|
|
50
|
+
</CardHeader>
|
|
51
|
+
</Card>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
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 use `shadow.raised` or `shadow.overlay` plus the quiet mint
|
|
22
|
+
ambient gradient from the official reference. Do not create a separate app palette.
|
|
23
|
+
|
|
24
|
+
## Product mapping
|
|
25
|
+
|
|
26
|
+
- Chat composer/send action: mint fill, graphite icon/text, deep-teal focus ring.
|
|
27
|
+
- Assistant/AI surfaces: graphite/paper raised surface with a quiet mint radial
|
|
28
|
+
ambient layer; do not use blue/cyan as the product identity.
|
|
29
|
+
- Admin, expert and profile shells: the same paper/graphite canvas, thin utility
|
|
30
|
+
dividers, and one mint primary action per region.
|
|
31
|
+
- Destructive paths continue to use coral and must retain explicit labels.
|
|
@@ -42,6 +42,11 @@ matching the direction contract in `rtl.md`. Products consume the highest
|
|
|
42
42
|
layer that fits; screens should not reassemble primitives into ad-hoc page
|
|
43
43
|
scaffolds when a registered block exists.
|
|
44
44
|
|
|
45
|
+
For application pages, compose `app-frame`, `page-header`, `sidebar-navigation`,
|
|
46
|
+
`bottom-navigation`, `drawer`, and `Card` before creating a page-local shell.
|
|
47
|
+
Use the [flat surface and navigation policy](surface-policy.md) to decide surface
|
|
48
|
+
tone, CTA hierarchy, adaptive navigation, icon coloring, and review evidence.
|
|
49
|
+
|
|
45
50
|
Mapping to the Untitled UI reference taxonomy (honest as of 2026-08-27 — foundation stabilization):
|
|
46
51
|
|
|
47
52
|
| Untitled UI category | NOE coverage |
|
|
@@ -35,6 +35,17 @@ Typography is a language-aware semantic contract, not a per-component choice.
|
|
|
35
35
|
|
|
36
36
|
Radii are compact and semantic. Controls use shared heights. Cards are not nested merely to create hierarchy. Dense professional surfaces may use tighter spacing than consumer surfaces but share the same token scale.
|
|
37
37
|
|
|
38
|
+
## Surface hierarchy
|
|
39
|
+
|
|
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.
|
|
44
|
+
`Card` therefore defaults to a raised, borderless appearance. Its outlined
|
|
45
|
+
appearance is only for controls or data boundaries with a meaningful selection
|
|
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.
|
|
48
|
+
|
|
38
49
|
## Responsive behavior
|
|
39
50
|
|
|
40
51
|
The reference app validates 390, 768, 1024, 1280 and 1440 pixel widths. Sidebars collapse by capability; data remains in logical DOM order; tables scroll rather than reverse data; agent artifacts progressively disclose on smaller layouts.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Flat surface and navigation policy
|
|
2
|
+
|
|
3
|
+
This is the default NOE policy for application pages. It prevents corrective UI
|
|
4
|
+
passes caused by arbitrary shadows, card borders, or ambiguous mobile navigation.
|
|
5
|
+
|
|
6
|
+
## Surface hierarchy
|
|
7
|
+
|
|
8
|
+
1. The page canvas uses `color.background` or `color.surface`.
|
|
9
|
+
2. A top-level section or card uses `color.surfaceRaised`.
|
|
10
|
+
3. Grouping comes from the difference between those two tones, radius, and the
|
|
11
|
+
spacing scale—not a border or a shadow.
|
|
12
|
+
4. A nested region uses spacing first. Do not nest cards merely to create depth.
|
|
13
|
+
5. `Card` defaults to `appearance="raised"`, which has no visible stroke.
|
|
14
|
+
`appearance="outlined"` is reserved for a selection, data-entry, or comparison
|
|
15
|
+
boundary where a stroke conveys real meaning.
|
|
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.
|
|
20
|
+
|
|
21
|
+
## Actions
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
27
|
+
- Destructive actions retain an explicit label and confirmation/recovery path;
|
|
28
|
+
color alone is never the signal.
|
|
29
|
+
- Every focusable action keeps a visible `focus-visible` outline.
|
|
30
|
+
|
|
31
|
+
## Adaptive navigation
|
|
32
|
+
|
|
33
|
+
- Use a sidebar for application navigation at desktop widths and a labelled drawer
|
|
34
|
+
trigger at smaller widths.
|
|
35
|
+
- Mobile bottom navigation is only for top-level destinations and has at most five
|
|
36
|
+
labelled items. The active item has a text and icon state, not color alone.
|
|
37
|
+
- A drawer is secondary navigation, not the only route to a primary action.
|
|
38
|
+
- Every page frame exposes a skip link to its main landmark; a route change places
|
|
39
|
+
the reading start at that main region.
|
|
40
|
+
- Menu triggers include an icon and a visible label when surrounding layout does
|
|
41
|
+
not already make their purpose obvious.
|
|
42
|
+
|
|
43
|
+
## Icons and direction
|
|
44
|
+
|
|
45
|
+
- Use `@noe/icons` only. Icons inherit `currentColor` from their interactive
|
|
46
|
+
parent, with explicit `color` only for a semantic status.
|
|
47
|
+
- Keep icon size on the NOE token scale and icon-plus-label gaps logical.
|
|
48
|
+
- Use logical CSS properties. Do not solve RTL with global `row-reverse` or
|
|
49
|
+
blanket SVG mirroring.
|
|
50
|
+
|
|
51
|
+
## Product review checklist
|
|
52
|
+
|
|
53
|
+
- Test every route in Persian/RTL and English/LTR, in light and dark themes.
|
|
54
|
+
- 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 interactive targets are at least 44 px on touch devices and have
|
|
58
|
+
accessible names.
|
|
59
|
+
- Test desktop sidebar and mobile drawer independently, including keyboard focus
|
|
60
|
+
and Escape dismissal.
|
package/llms.txt
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
- Agent workflow and retrieval: `docs/design-system/agent-guide.md`
|
|
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
|
+
- 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`
|
|
35
37
|
- Locale, RTL, and bidi: `docs/design-system/i18n.md`, `docs/design-system/rtl.md`
|
|
36
38
|
- Accessibility: `docs/design-system/accessibility.md`
|
|
37
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);
|
|
@@ -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
|
|
163
|
-
--noe-shadow-overlay: 0
|
|
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);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/* Semantic tokens: explicit dark theme. */
|
|
@@ -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);
|
|
@@ -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
|
|
193
|
-
--noe-shadow-overlay: 0
|
|
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);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/* Semantic tokens: follow the OS only when no explicit theme is set. */
|
|
@@ -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);
|
|
@@ -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
|
|
224
|
-
--noe-shadow-overlay: 0
|
|
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);
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -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}",
|
|
@@ -154,12 +154,12 @@
|
|
|
154
154
|
},
|
|
155
155
|
"shadow": {
|
|
156
156
|
"light": {
|
|
157
|
-
"raised": "0
|
|
158
|
-
"overlay": "0
|
|
157
|
+
"raised": "0 8px 20px rgba(23, 25, 24, 0.055)",
|
|
158
|
+
"overlay": "0 36px 100px rgba(23, 25, 24, 0.28)"
|
|
159
159
|
},
|
|
160
160
|
"dark": {
|
|
161
|
-
"raised": "0
|
|
162
|
-
"overlay": "0
|
|
161
|
+
"raised": "0 8px 20px rgba(0, 0, 0, 0.14)",
|
|
162
|
+
"overlay": "0 36px 100px rgba(0, 0, 0, 0.42)"
|
|
163
163
|
}
|
|
164
164
|
},
|
|
165
165
|
"font": {
|
|
@@ -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}",
|
|
@@ -155,12 +155,12 @@ export const tokens = {
|
|
|
155
155
|
},
|
|
156
156
|
"shadow": {
|
|
157
157
|
"light": {
|
|
158
|
-
"raised": "0
|
|
159
|
-
"overlay": "0
|
|
158
|
+
"raised": "0 8px 20px rgba(23, 25, 24, 0.055)",
|
|
159
|
+
"overlay": "0 36px 100px rgba(23, 25, 24, 0.28)"
|
|
160
160
|
},
|
|
161
161
|
"dark": {
|
|
162
|
-
"raised": "0
|
|
163
|
-
"overlay": "0
|
|
162
|
+
"raised": "0 8px 20px rgba(0, 0, 0, 0.14)",
|
|
163
|
+
"overlay": "0 36px 100px rgba(0, 0, 0, 0.42)"
|
|
164
164
|
}
|
|
165
165
|
},
|
|
166
166
|
"font": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"generatedAt": "2026-08-29T09:30:43.936Z",
|
|
3
3
|
"items": [
|
|
4
4
|
{
|
|
5
5
|
"id": "bidi-text",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"package": "@noe/ui",
|
|
89
89
|
"status": "available",
|
|
90
90
|
"maturity": "beta",
|
|
91
|
-
"description": "
|
|
91
|
+
"description": "Flat raised section primitive. It separates content from its page canvas through semantic surface tone, spacing and radius—not decorative borders or shadows. Use outlined only where a boundary conveys selection or data-entry state.",
|
|
92
92
|
"registryItem": "card"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifestVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-29T09:30:43.923Z",
|
|
4
4
|
"generatedNote": "Canonical NOE manifest — single source of truth for catalog, audit, registry, stories, docs and agent search. Generated by pnpm gen:manifest; do not edit by hand for automated fields.",
|
|
5
5
|
"items": [
|
|
6
6
|
{
|
|
@@ -428,12 +428,25 @@
|
|
|
428
428
|
"status": "available",
|
|
429
429
|
"maturity": "beta",
|
|
430
430
|
"category": "Primitives / Layout",
|
|
431
|
-
"description": "
|
|
432
|
-
"capabilityAliases": ["card", "card"],
|
|
433
|
-
"searchTerms": [
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
431
|
+
"description": "Flat raised section primitive. It separates content from its page canvas through semantic surface tone, spacing and radius—not decorative borders or shadows. Use outlined only where a boundary conveys selection or data-entry state.",
|
|
432
|
+
"capabilityAliases": ["flat card", "raised surface", "borderless section", "outlined card"],
|
|
433
|
+
"searchTerms": [
|
|
434
|
+
"card",
|
|
435
|
+
"raised surface",
|
|
436
|
+
"flat section",
|
|
437
|
+
"borderless panel",
|
|
438
|
+
"outlined boundary"
|
|
439
|
+
],
|
|
440
|
+
"useWhen": [
|
|
441
|
+
"grouping a top-level application region with a semantic raised surface",
|
|
442
|
+
"separating sections by color tone rather than elevation or decorative chrome",
|
|
443
|
+
"using appearance=\"outlined\" only for selection, data-entry or comparison boundaries"
|
|
444
|
+
],
|
|
445
|
+
"doNotUseWhen": [
|
|
446
|
+
"adding a nested card solely to create visual depth",
|
|
447
|
+
"using an outlined boundary when spacing and surface tone already express grouping"
|
|
448
|
+
],
|
|
449
|
+
"alternatives": ["SectionHeader", "Field"],
|
|
437
450
|
"dependencies": [],
|
|
438
451
|
"registryDependencies": [],
|
|
439
452
|
"peerDependencies": ["react"],
|
|
@@ -441,20 +454,20 @@
|
|
|
441
454
|
"label": "string | ReactNode",
|
|
442
455
|
"children": "ReactNode"
|
|
443
456
|
},
|
|
444
|
-
"variants": ["
|
|
457
|
+
"variants": ["raised", "outlined"],
|
|
445
458
|
"sizes": [],
|
|
446
459
|
"slots": [],
|
|
447
|
-
"states": ["default"
|
|
448
|
-
"tokenContracts": ["color.
|
|
460
|
+
"states": ["default"],
|
|
461
|
+
"tokenContracts": ["color.background", "color.surface", "color.surfaceRaised", "radius.lg"],
|
|
449
462
|
"accessibility": {
|
|
450
|
-
"keyboard": [
|
|
463
|
+
"keyboard": [],
|
|
451
464
|
"announcements": [],
|
|
452
|
-
"focusBehavior": ["
|
|
465
|
+
"focusBehavior": ["not focusable unless its content contains an interactive control"]
|
|
453
466
|
},
|
|
454
467
|
"internationalization": {
|
|
455
468
|
"messageKeys": [],
|
|
456
469
|
"rtl": "required",
|
|
457
|
-
"bidiNotes": ["logical
|
|
470
|
+
"bidiNotes": ["logical spacing; no directional layout override"]
|
|
458
471
|
},
|
|
459
472
|
"examples": [
|
|
460
473
|
{
|
|
@@ -463,7 +476,7 @@
|
|
|
463
476
|
"files": ["apps/storybook/stories/catalog/card.stories.tsx"]
|
|
464
477
|
}
|
|
465
478
|
],
|
|
466
|
-
"relatedPatterns": [],
|
|
479
|
+
"relatedPatterns": ["app-frame", "dashboard-shell", "section-header"],
|
|
467
480
|
"untitledMapping": null,
|
|
468
481
|
"storyId": "catalog-primitives-card--default",
|
|
469
482
|
"registryItem": "card"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "card",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "Card",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Flat raised section primitive. It separates content through semantic surface tone, spacing and radius—not decorative borders or shadows. Use an outlined boundary only for selection or data-entry state.",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "packages/ui/src/card.tsx",
|
|
@@ -37,8 +37,14 @@
|
|
|
37
37
|
"announcements": [],
|
|
38
38
|
"focusBehavior": ["visible ring 2px"]
|
|
39
39
|
},
|
|
40
|
-
"tokenContracts": ["color.
|
|
41
|
-
"capabilityAliases": ["card", "card"],
|
|
42
|
-
"useWhen": [
|
|
43
|
-
|
|
40
|
+
"tokenContracts": ["color.background", "color.surface", "color.surfaceRaised", "radius.lg"],
|
|
41
|
+
"capabilityAliases": ["flat card", "raised surface", "borderless section", "outlined card"],
|
|
42
|
+
"useWhen": [
|
|
43
|
+
"grouping a top-level application region with a semantic raised surface",
|
|
44
|
+
"using an outlined boundary only for selection, data-entry or comparison state"
|
|
45
|
+
],
|
|
46
|
+
"doNotUseWhen": [
|
|
47
|
+
"adding a nested card solely to create visual depth",
|
|
48
|
+
"using a decorative stroke when spacing and surface tone already express grouping"
|
|
49
|
+
]
|
|
44
50
|
}
|
|
@@ -1040,8 +1040,14 @@
|
|
|
1040
1040
|
"dependencies": ["clsx", "tailwind-merge"],
|
|
1041
1041
|
"peerDependencies": ["react"],
|
|
1042
1042
|
"manifestId": "card",
|
|
1043
|
-
"capabilityAliases": ["card", "card"],
|
|
1044
|
-
"searchTerms": [
|
|
1043
|
+
"capabilityAliases": ["flat card", "raised surface", "borderless section", "outlined card"],
|
|
1044
|
+
"searchTerms": [
|
|
1045
|
+
"card",
|
|
1046
|
+
"raised surface",
|
|
1047
|
+
"flat section",
|
|
1048
|
+
"borderless panel",
|
|
1049
|
+
"outlined boundary"
|
|
1050
|
+
]
|
|
1045
1051
|
},
|
|
1046
1052
|
{
|
|
1047
1053
|
"name": "alert",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"generatedAt": "2026-08-29T09:30:43.933Z",
|
|
3
3
|
"items": [
|
|
4
4
|
{
|
|
5
5
|
"id": "bidi-text",
|
|
@@ -160,15 +160,28 @@
|
|
|
160
160
|
"id": "card",
|
|
161
161
|
"package": "@noe/ui",
|
|
162
162
|
"title": "Card",
|
|
163
|
-
"description": "
|
|
163
|
+
"description": "Flat raised section primitive. It separates content from its page canvas through semantic surface tone, spacing and radius—not decorative borders or shadows. Use outlined only where a boundary conveys selection or data-entry state.",
|
|
164
164
|
"category": "Primitives / Layout",
|
|
165
165
|
"status": "available",
|
|
166
|
-
"capabilityAliases": ["card", "card"],
|
|
167
|
-
"searchTerms": [
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
"capabilityAliases": ["flat card", "raised surface", "borderless section", "outlined card"],
|
|
167
|
+
"searchTerms": [
|
|
168
|
+
"card",
|
|
169
|
+
"raised surface",
|
|
170
|
+
"flat section",
|
|
171
|
+
"borderless panel",
|
|
172
|
+
"outlined boundary"
|
|
173
|
+
],
|
|
174
|
+
"useWhen": [
|
|
175
|
+
"grouping a top-level application region with a semantic raised surface",
|
|
176
|
+
"separating sections by color tone rather than elevation or decorative chrome",
|
|
177
|
+
"using appearance=\"outlined\" only for selection, data-entry or comparison boundaries"
|
|
178
|
+
],
|
|
179
|
+
"doNotUseWhen": [
|
|
180
|
+
"adding a nested card solely to create visual depth",
|
|
181
|
+
"using an outlined boundary when spacing and surface tone already express grouping"
|
|
182
|
+
],
|
|
183
|
+
"alternatives": ["SectionHeader", "Field"],
|
|
184
|
+
"relatedPatterns": ["app-frame", "dashboard-shell", "section-header"],
|
|
172
185
|
"exportName": "Card",
|
|
173
186
|
"registryItem": "card",
|
|
174
187
|
"maturity": "beta"
|
package/packages/ui/src/card.tsx
CHANGED
|
@@ -2,13 +2,25 @@ import type * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { cn } from './lib/cn';
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export type CardAppearance = 'raised' | 'outlined';
|
|
6
|
+
|
|
7
|
+
export interface CardProps extends React.ComponentProps<'section'> {
|
|
8
|
+
/**
|
|
9
|
+
* Raised surfaces are separated from the page by a semantic surface tone only.
|
|
10
|
+
* Use outlined only where a visible boundary conveys data-entry or selection state.
|
|
11
|
+
*/
|
|
12
|
+
appearance?: CardAppearance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Card({ appearance = 'raised', className, ...props }: CardProps) {
|
|
6
16
|
return (
|
|
7
17
|
<section
|
|
8
18
|
className={cn(
|
|
9
|
-
'flex flex-col overflow-hidden rounded-lg
|
|
19
|
+
'flex flex-col overflow-hidden rounded-lg bg-surface-raised text-foreground',
|
|
20
|
+
appearance === 'outlined' && 'border border-border',
|
|
10
21
|
className,
|
|
11
22
|
)}
|
|
23
|
+
data-appearance={appearance}
|
|
12
24
|
data-slot="card"
|
|
13
25
|
{...props}
|
|
14
26
|
/>
|
|
@@ -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
|
}
|
|
@@ -218,6 +218,41 @@ const enrichments = {
|
|
|
218
218
|
notes: 'Variants parity full; loading and destructive covered',
|
|
219
219
|
},
|
|
220
220
|
},
|
|
221
|
+
card: {
|
|
222
|
+
description:
|
|
223
|
+
'Flat raised section primitive. It separates content from its page canvas through semantic surface tone, spacing and radius—not decorative borders or shadows. Use outlined only where a boundary conveys selection or data-entry state.',
|
|
224
|
+
capabilityAliases: ['flat card', 'raised surface', 'borderless section', 'outlined card'],
|
|
225
|
+
searchTerms: [
|
|
226
|
+
'card',
|
|
227
|
+
'raised surface',
|
|
228
|
+
'flat section',
|
|
229
|
+
'borderless panel',
|
|
230
|
+
'outlined boundary',
|
|
231
|
+
],
|
|
232
|
+
useWhen: [
|
|
233
|
+
'grouping a top-level application region with a semantic raised surface',
|
|
234
|
+
'separating sections by color tone rather than elevation or decorative chrome',
|
|
235
|
+
'using appearance="outlined" only for selection, data-entry or comparison boundaries',
|
|
236
|
+
],
|
|
237
|
+
doNotUseWhen: [
|
|
238
|
+
'adding a nested card solely to create visual depth',
|
|
239
|
+
'using an outlined boundary when spacing and surface tone already express grouping',
|
|
240
|
+
],
|
|
241
|
+
alternatives: ['SectionHeader', 'Field'],
|
|
242
|
+
variants: ['raised', 'outlined'],
|
|
243
|
+
states: ['default'],
|
|
244
|
+
tokenContracts: ['color.background', 'color.surface', 'color.surfaceRaised', 'radius.lg'],
|
|
245
|
+
accessibility: {
|
|
246
|
+
keyboard: [],
|
|
247
|
+
announcements: [],
|
|
248
|
+
focusBehavior: ['not focusable unless its content contains an interactive control'],
|
|
249
|
+
},
|
|
250
|
+
internationalization: {
|
|
251
|
+
rtl: 'required',
|
|
252
|
+
bidiNotes: ['logical spacing; no directional layout override'],
|
|
253
|
+
},
|
|
254
|
+
relatedPatterns: ['app-frame', 'dashboard-shell', 'section-header'],
|
|
255
|
+
},
|
|
221
256
|
dialog: {
|
|
222
257
|
description:
|
|
223
258
|
'Direction-preserving modal primitive with native focus semantics and portal propagation. Share foundation with Modal/Slideout.',
|
|
@@ -826,7 +861,7 @@ function defaultEnrichment(auditItem) {
|
|
|
826
861
|
dependencies: deps,
|
|
827
862
|
registryDependencies: registryDeps,
|
|
828
863
|
peerDependencies: peerDeps,
|
|
829
|
-
propsSummary: { label: 'string | ReactNode', children: 'ReactNode' },
|
|
864
|
+
propsSummary: base.propsSummary ?? { label: 'string | ReactNode', children: 'ReactNode' },
|
|
830
865
|
variants,
|
|
831
866
|
sizes,
|
|
832
867
|
slots,
|
package/tests/tokens.test.mjs
CHANGED
|
@@ -22,3 +22,23 @@ test('token CSS includes theme and reduced-motion contracts', async () => {
|
|
|
22
22
|
assert.match(css, /--noe-primitive-color-paper-100:\s*#[0-9a-f]{6}/i);
|
|
23
23
|
assert.match(css, /--noe-primitive-color-mint-100:\s*#[0-9a-f]{6}/i);
|
|
24
24
|
});
|
|
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\)/);
|
|
31
|
+
|
|
32
|
+
const css = await readFile(
|
|
33
|
+
new URL('../packages/design-tokens/src/tokens.css', import.meta.url),
|
|
34
|
+
'utf8',
|
|
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
|
+
);
|
|
44
|
+
});
|