@podoba/react 0.0.14 → 0.0.16
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/package.json +3 -3
- package/src/components/brand-page-header.tsx +1 -1
- package/src/components/combobox.tsx +1 -1
- package/src/components/context-menu.tsx +2 -2
- package/src/components/cta-pill.tsx +36 -0
- package/src/components/date-picker.tsx +1 -1
- package/src/components/feature-tile.tsx +93 -0
- package/src/components/focus-field.tsx +2 -2
- package/src/components/icons.tsx +142 -0
- package/src/components/stats-card.tsx +1 -1
- package/src/components/subtle.tsx +21 -0
- package/src/components/text.tsx +5 -4
- package/src/components/tile.tsx +1 -1
- package/src/index.ts +4 -0
- package/src/layout/card.tsx +2 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podoba/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "podoba React components — React Aria Components + Tailwind primitives + layout, built with uic.",
|
|
6
6
|
"repository": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"typecheck": "tsc --build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@podoba/tokens": "^0.0.
|
|
29
|
-
"@podoba/tailwind": "^0.0.
|
|
28
|
+
"@podoba/tokens": "^0.0.16",
|
|
29
|
+
"@podoba/tailwind": "^0.0.16",
|
|
30
30
|
"react-aria-components": "1.18.0",
|
|
31
31
|
"class-variance-authority": "0.7.1",
|
|
32
32
|
"clsx": "2.1.1",
|
|
@@ -116,7 +116,7 @@ export function BrandPageHeader({
|
|
|
116
116
|
))}
|
|
117
117
|
</nav>
|
|
118
118
|
) : null}
|
|
119
|
-
<h1 className="text-display font-medium leading-[1.12] tracking-
|
|
119
|
+
<h1 className="text-display font-medium leading-[1.12] tracking-wide text-fg">
|
|
120
120
|
{parentLink ? (
|
|
121
121
|
// gs "title to go back": muted clickable parent line above the title.
|
|
122
122
|
// `[&_a]` styles the nested router <Link> (anchor) without @app/ui
|
|
@@ -91,7 +91,7 @@ export const ComboBox = <T extends object>({
|
|
|
91
91
|
<SearchField aria-label={typeof label === 'string' ? label : 'Search'}>
|
|
92
92
|
<RACInput
|
|
93
93
|
placeholder={placeholder}
|
|
94
|
-
className="w-full border-0 bg-transparent p-0 text-
|
|
94
|
+
className="w-full border-0 bg-transparent p-0 text-display font-medium text-fg outline-none placeholder:text-fg-subtle"
|
|
95
95
|
/>
|
|
96
96
|
</SearchField>
|
|
97
97
|
<ListBox
|
|
@@ -126,7 +126,7 @@ const MenuItem = uic(RACMenuItem, {
|
|
|
126
126
|
// gs `.panel`: 258px, radius 12px, 8px/6px padding, 180deg #242424→#1f1f1f
|
|
127
127
|
// gradient, 0 12px 28px rgba(0,0,0,.22) shadow. No token covers the gradient/shadow.
|
|
128
128
|
const panelClass =
|
|
129
|
-
'w-[258px] max-w-[calc(100vw-16px)] rounded-
|
|
129
|
+
'w-[258px] max-w-[calc(100vw-16px)] rounded-lg bg-gradient-to-b from-[#242424] to-[#1f1f1f] ' +
|
|
130
130
|
'px-1.5 py-2 shadow-[0_12px_28px_rgba(0,0,0,0.22)] outline-none'
|
|
131
131
|
const menuClass = 'grid max-h-[calc(100vh-16px)] gap-0.5 overflow-y-auto outline-none'
|
|
132
132
|
|
|
@@ -178,7 +178,7 @@ function ContextMenuBody({
|
|
|
178
178
|
<span className="min-w-0 flex-1 truncate">{item.label}</span>
|
|
179
179
|
{badge ? (
|
|
180
180
|
// gs `.badge`: 24px pill, #333437 bg / #c8c8ca text, 10px (text-micro).
|
|
181
|
-
<span className="ml-auto inline-flex h-6 items-center justify-center rounded-full bg-[#333437] px-3 py-0.5 text-micro font-medium leading-5 tracking-
|
|
181
|
+
<span className="ml-auto inline-flex h-6 items-center justify-center rounded-full bg-[#333437] px-3 py-0.5 text-micro font-medium leading-5 tracking-tight text-[#c8c8ca]">
|
|
182
182
|
{badge}
|
|
183
183
|
</span>
|
|
184
184
|
) : null}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CtaPill — the gs Brand Core hero CTA: a GS-green bar reading "Let's create
|
|
5
|
+
* something" (the middle word emphasised) with an action control on the right.
|
|
6
|
+
*
|
|
7
|
+
* The copy is three fragments (`lead` · `emphasis` · `tail`) so a consumer can
|
|
8
|
+
* translate each per-locale while keeping the middle-word highlight. Presentational
|
|
9
|
+
* only (hard rule #1): every string arrives via props — no i18n, no domain data.
|
|
10
|
+
*
|
|
11
|
+
* a11y NOTE: the background is the light brand-green (#6eddb1); the emphasised word is
|
|
12
|
+
* rendered WHITE per the gs design — white-on-green is ~1.66:1, below WCAG AA, so it
|
|
13
|
+
* reads as a decorative highlight (it also carries a bold weight), not load-bearing
|
|
14
|
+
* copy. The surrounding text stays on the dark `fg` (~10:1).
|
|
15
|
+
*/
|
|
16
|
+
export interface CtaPillProps {
|
|
17
|
+
/** Regular lead-in before the emphasised word (e.g. "Let's"). */
|
|
18
|
+
lead: ReactNode
|
|
19
|
+
/** The emphasised bold, white middle word (e.g. "create"). */
|
|
20
|
+
emphasis: ReactNode
|
|
21
|
+
/** Regular trailing text after the emphasis (e.g. "something"). */
|
|
22
|
+
tail: ReactNode
|
|
23
|
+
/** Right-side action control (e.g. a Create button). */
|
|
24
|
+
children: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CtaPill({ lead, emphasis, tail, children }: CtaPillProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex min-w-[560px] items-center justify-between gap-4 rounded-lg bg-brand-secondary px-6 py-4">
|
|
30
|
+
<p className="text-heading4 font-medium leading-[22px] tracking-normal text-fg">
|
|
31
|
+
{lead} <span className="font-semibold text-white">{emphasis}</span> {tail}
|
|
32
|
+
</p>
|
|
33
|
+
<div className="shrink-0">{children}</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -88,7 +88,7 @@ export const DatePicker = <T extends DateValue>({ label, description, errorMessa
|
|
|
88
88
|
{/* In focus mode the field is bare + large and the calendar is borderless,
|
|
89
89
|
so field + calendar read as one seamless editor rather than boxes. */}
|
|
90
90
|
<Group className={inFocus ? 'flex w-full items-center' : `${dateInputClass} pr-2`}>
|
|
91
|
-
<DateInput className={clsx('flex flex-1 items-center gap-0.5', inFocus && 'text-
|
|
91
|
+
<DateInput className={clsx('flex flex-1 items-center gap-0.5', inFocus && 'text-display font-medium')}>
|
|
92
92
|
{(segment) => <DateSegment segment={segment} className={segmentClass} />}
|
|
93
93
|
</DateInput>
|
|
94
94
|
{inFocus ? null : (
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FeatureTile — a full-width media hero tile: a background image with an overlaid
|
|
5
|
+
* eyebrow (top-left), title (bottom-left), an optional action control and a badge.
|
|
6
|
+
* Generic — the common use is a "start here" tutorial banner, but it fits any
|
|
7
|
+
* promo/feature spotlight. Presentational only (hard rule #1): text arrives via props.
|
|
8
|
+
*
|
|
9
|
+
* The whole tile is pressable via `onPress` (rendered as a full-bleed overlay button
|
|
10
|
+
* labelled by `title`); `action` / `badge` sit ABOVE it (z-10) so their own handlers
|
|
11
|
+
* still work. Falls back to a dark surface when no `image` is given.
|
|
12
|
+
*/
|
|
13
|
+
export interface FeatureTileProps {
|
|
14
|
+
/** Background image URL. Falls back to a dark surface when omitted. */
|
|
15
|
+
image?: string
|
|
16
|
+
imageAlt?: string
|
|
17
|
+
/** Small category label, top-left (e.g. "Tutorial"). */
|
|
18
|
+
eyebrow?: ReactNode
|
|
19
|
+
/** Large title, bottom-left. */
|
|
20
|
+
title: ReactNode
|
|
21
|
+
/** Bottom-left action control (e.g. a round icon button). */
|
|
22
|
+
action?: ReactNode
|
|
23
|
+
/** Bottom-right badge (e.g. a duration pill). */
|
|
24
|
+
badge?: ReactNode
|
|
25
|
+
/** Makes the whole tile pressable. `pressLabel` names it for assistive tech. */
|
|
26
|
+
onPress?: () => void
|
|
27
|
+
pressLabel?: string
|
|
28
|
+
/** Override the default `aspect-[2/1]` ratio / sizing. */
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function FeatureTile({
|
|
33
|
+
image,
|
|
34
|
+
imageAlt,
|
|
35
|
+
eyebrow,
|
|
36
|
+
title,
|
|
37
|
+
action,
|
|
38
|
+
badge,
|
|
39
|
+
onPress,
|
|
40
|
+
pressLabel,
|
|
41
|
+
className,
|
|
42
|
+
}: FeatureTileProps) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className={[
|
|
46
|
+
// A large fixed-aspect banner (default ~2:1); the image covers it. Override
|
|
47
|
+
// the ratio/size via `className` (e.g. `aspect-[1440/700]`). Not tied to the
|
|
48
|
+
// viewport, so it stays a predictable min size rather than reflowing.
|
|
49
|
+
'relative isolate aspect-[2/1] w-full overflow-hidden rounded-lg bg-surface-inverted',
|
|
50
|
+
className,
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(' ')}
|
|
54
|
+
>
|
|
55
|
+
{image ? (
|
|
56
|
+
<img
|
|
57
|
+
src={image}
|
|
58
|
+
alt={imageAlt ?? ''}
|
|
59
|
+
className="absolute inset-0 h-full w-full object-cover"
|
|
60
|
+
/>
|
|
61
|
+
) : null}
|
|
62
|
+
{/* Legibility scrim — darkens the top + bottom where the text sits. */}
|
|
63
|
+
<div
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
className="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-black/60"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{/* Full-bleed press target (behind the content controls). */}
|
|
69
|
+
{onPress ? (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={onPress}
|
|
73
|
+
aria-label={pressLabel}
|
|
74
|
+
className="absolute inset-0 z-0 cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/70"
|
|
75
|
+
/>
|
|
76
|
+
) : null}
|
|
77
|
+
|
|
78
|
+
{eyebrow ? (
|
|
79
|
+
<span className="pointer-events-none absolute left-6 top-6 z-10 text-compact font-medium text-white">
|
|
80
|
+
{eyebrow}
|
|
81
|
+
</span>
|
|
82
|
+
) : null}
|
|
83
|
+
|
|
84
|
+
<div className="absolute inset-x-6 bottom-6 z-10 flex flex-col gap-4">
|
|
85
|
+
<h2 className="max-w-md text-display-xl font-medium leading-tight text-white">{title}</h2>
|
|
86
|
+
<div className="flex items-center justify-between gap-4">
|
|
87
|
+
<span className="pointer-events-auto">{action}</span>
|
|
88
|
+
<span className="pointer-events-auto">{badge}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -85,8 +85,8 @@ export function FocusFields({ children, className }: { children: ReactNode; clas
|
|
|
85
85
|
|
|
86
86
|
// In the overlay, enlarge and de-chrome text controls into a bare headline editor.
|
|
87
87
|
const OVERLAY_EDITOR =
|
|
88
|
-
'[&_input]:w-full [&_input]:border-0 [&_input]:bg-transparent [&_input]:p-0 [&_input]:text-
|
|
89
|
-
'[&_textarea]:min-h-40 [&_textarea]:w-full [&_textarea]:resize-none [&_textarea]:border-0 [&_textarea]:bg-transparent [&_textarea]:p-0 [&_textarea]:text-
|
|
88
|
+
'[&_input]:w-full [&_input]:border-0 [&_input]:bg-transparent [&_input]:p-0 [&_input]:text-display [&_input]:font-medium [&_input]:text-fg [&_input]:outline-none [&_input]:shadow-none [&_input]:ring-0 [&_input]:placeholder:text-fg-subtle ' +
|
|
89
|
+
'[&_textarea]:min-h-40 [&_textarea]:w-full [&_textarea]:resize-none [&_textarea]:border-0 [&_textarea]:bg-transparent [&_textarea]:p-0 [&_textarea]:text-display [&_textarea]:font-medium [&_textarea]:text-fg [&_textarea]:outline-none [&_textarea]:placeholder:text-fg-subtle'
|
|
90
90
|
|
|
91
91
|
function IconBadge({ icon }: { icon: ReactNode }) {
|
|
92
92
|
return (
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Icon set — the design system's curated line icons. One `<XxxIcon>` per glyph, all
|
|
5
|
+
* on a shared 24×24 grid, `currentColor`, 2px round stroke. Size via `className`
|
|
6
|
+
* (`h-4 w-4`, …) or width/height; colour via `text-*`. This replaces the ad-hoc inline
|
|
7
|
+
* `<svg>`s scattered across the app so every icon reads as one family.
|
|
8
|
+
*
|
|
9
|
+
* Fill-based glyphs (Play, Triangle) set `fill="currentColor" stroke="none"`; the rest
|
|
10
|
+
* are stroked. Add a glyph here (not inline) when a new one is needed.
|
|
11
|
+
*/
|
|
12
|
+
export type IconProps = SVGProps<SVGSVGElement>
|
|
13
|
+
|
|
14
|
+
const STROKE: IconProps = {
|
|
15
|
+
width: 24,
|
|
16
|
+
height: 24,
|
|
17
|
+
viewBox: '0 0 24 24',
|
|
18
|
+
fill: 'none',
|
|
19
|
+
stroke: 'currentColor',
|
|
20
|
+
strokeWidth: 2,
|
|
21
|
+
strokeLinecap: 'round',
|
|
22
|
+
strokeLinejoin: 'round',
|
|
23
|
+
'aria-hidden': true,
|
|
24
|
+
}
|
|
25
|
+
const FILL: IconProps = {
|
|
26
|
+
width: 24,
|
|
27
|
+
height: 24,
|
|
28
|
+
viewBox: '0 0 24 24',
|
|
29
|
+
fill: 'currentColor',
|
|
30
|
+
stroke: 'none',
|
|
31
|
+
'aria-hidden': true,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- navigation / chevrons ---
|
|
35
|
+
export const ChevronDownIcon = (p: IconProps) => (
|
|
36
|
+
<svg {...STROKE} {...p}><path d="M6 9l6 6 6-6" /></svg>
|
|
37
|
+
)
|
|
38
|
+
export const ChevronUpIcon = (p: IconProps) => (
|
|
39
|
+
<svg {...STROKE} {...p}><path d="M18 15l-6-6-6 6" /></svg>
|
|
40
|
+
)
|
|
41
|
+
export const ChevronRightIcon = (p: IconProps) => (
|
|
42
|
+
<svg {...STROKE} {...p}><path d="M9 6l6 6-6 6" /></svg>
|
|
43
|
+
)
|
|
44
|
+
export const ChevronLeftIcon = (p: IconProps) => (
|
|
45
|
+
<svg {...STROKE} {...p}><path d="M15 6l-6 6 6 6" /></svg>
|
|
46
|
+
)
|
|
47
|
+
export const ArrowRightIcon = (p: IconProps) => (
|
|
48
|
+
<svg {...STROKE} {...p}><path d="M5 12h14M13 6l6 6-6 6" /></svg>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// --- actions ---
|
|
52
|
+
export const CloseIcon = (p: IconProps) => (
|
|
53
|
+
<svg {...STROKE} {...p}><path d="M6 6l12 12M18 6L6 18" /></svg>
|
|
54
|
+
)
|
|
55
|
+
export const CheckIcon = (p: IconProps) => (
|
|
56
|
+
<svg {...STROKE} {...p}><path d="M20 6L9 17l-5-5" /></svg>
|
|
57
|
+
)
|
|
58
|
+
export const PlusIcon = (p: IconProps) => (
|
|
59
|
+
<svg {...STROKE} {...p}><path d="M12 5v14M5 12h14" /></svg>
|
|
60
|
+
)
|
|
61
|
+
export const MinusIcon = (p: IconProps) => (
|
|
62
|
+
<svg {...STROKE} {...p}><path d="M5 12h14" /></svg>
|
|
63
|
+
)
|
|
64
|
+
export const SearchIcon = (p: IconProps) => (
|
|
65
|
+
<svg {...STROKE} {...p}>
|
|
66
|
+
<circle cx="11" cy="11" r="7" />
|
|
67
|
+
<path d="M21 21l-4.3-4.3" />
|
|
68
|
+
</svg>
|
|
69
|
+
)
|
|
70
|
+
export const PlayIcon = (p: IconProps) => (
|
|
71
|
+
<svg {...FILL} {...p}><path d="M8 5v14l11-7z" /></svg>
|
|
72
|
+
)
|
|
73
|
+
export const MenuIcon = (p: IconProps) => (
|
|
74
|
+
<svg {...STROKE} {...p}><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
75
|
+
)
|
|
76
|
+
export const ExternalLinkIcon = (p: IconProps) => (
|
|
77
|
+
<svg {...STROKE} {...p}>
|
|
78
|
+
<path d="M15 3h6v6M21 3l-9 9M10 5H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5" />
|
|
79
|
+
</svg>
|
|
80
|
+
)
|
|
81
|
+
export const ExpandIcon = (p: IconProps) => (
|
|
82
|
+
<svg {...STROKE} {...p}><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg>
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// --- theme ---
|
|
86
|
+
export const SunIcon = (p: IconProps) => (
|
|
87
|
+
<svg {...STROKE} {...p}>
|
|
88
|
+
<circle cx="12" cy="12" r="4" />
|
|
89
|
+
<path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M19.1 4.9l-1.4 1.4M6.3 17.7l-1.4 1.4" />
|
|
90
|
+
</svg>
|
|
91
|
+
)
|
|
92
|
+
export const MoonIcon = (p: IconProps) => (
|
|
93
|
+
<svg {...STROKE} {...p}><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></svg>
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// --- objects ---
|
|
97
|
+
export const FileTextIcon = (p: IconProps) => (
|
|
98
|
+
<svg {...STROKE} {...p}>
|
|
99
|
+
<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
|
|
100
|
+
<path d="M14 3v5h5M9 13h6M9 17h4" />
|
|
101
|
+
</svg>
|
|
102
|
+
)
|
|
103
|
+
export const FolderIcon = (p: IconProps) => (
|
|
104
|
+
<svg {...STROKE} {...p}>
|
|
105
|
+
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
106
|
+
</svg>
|
|
107
|
+
)
|
|
108
|
+
export const ImageIcon = (p: IconProps) => (
|
|
109
|
+
<svg {...STROKE} {...p}>
|
|
110
|
+
<rect x="3" y="4" width="18" height="16" rx="2" />
|
|
111
|
+
<circle cx="8.5" cy="9.5" r="1.5" />
|
|
112
|
+
<path d="M4 17l5-4 4 3 3-2 4 3" />
|
|
113
|
+
</svg>
|
|
114
|
+
)
|
|
115
|
+
export const CalendarIcon = (p: IconProps) => (
|
|
116
|
+
<svg {...STROKE} {...p}>
|
|
117
|
+
<rect x="3" y="5" width="18" height="16" rx="2" />
|
|
118
|
+
<path d="M16 3v4M8 3v4M3 10h18" />
|
|
119
|
+
</svg>
|
|
120
|
+
)
|
|
121
|
+
export const TrashIcon = (p: IconProps) => (
|
|
122
|
+
<svg {...STROKE} {...p}>
|
|
123
|
+
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M10 11v6M14 11v6" />
|
|
124
|
+
</svg>
|
|
125
|
+
)
|
|
126
|
+
export const StarIcon = (p: IconProps) => (
|
|
127
|
+
<svg {...STROKE} {...p}>
|
|
128
|
+
<path d="M12 3l2.7 5.5 6 .9-4.4 4.2 1 6L12 17l-5.4 2.8 1-6L3.2 9.4l6-.9z" />
|
|
129
|
+
</svg>
|
|
130
|
+
)
|
|
131
|
+
export const UserIcon = (p: IconProps) => (
|
|
132
|
+
<svg {...STROKE} {...p}>
|
|
133
|
+
<circle cx="12" cy="8" r="4" />
|
|
134
|
+
<path d="M4 20a8 8 0 0 1 16 0" />
|
|
135
|
+
</svg>
|
|
136
|
+
)
|
|
137
|
+
export const UploadIcon = (p: IconProps) => (
|
|
138
|
+
<svg {...STROKE} {...p}><path d="M12 16V4M8 8l4-4 4 4M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" /></svg>
|
|
139
|
+
)
|
|
140
|
+
export const DownloadIcon = (p: IconProps) => (
|
|
141
|
+
<svg {...STROKE} {...p}><path d="M12 4v12M8 12l4 4 4-4M4 18v0a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2" /></svg>
|
|
142
|
+
)
|
|
@@ -85,7 +85,7 @@ function StatsCardBody({
|
|
|
85
85
|
* `-0.02em` (`AssetsTile.module.scss .count`).
|
|
86
86
|
*/}
|
|
87
87
|
<span
|
|
88
|
-
className={`flex flex-1 items-center text-
|
|
88
|
+
className={`flex flex-1 items-center text-display font-medium leading-[2rem] ${dark ? 'tracking-normal text-white' : 'tracking-wide text-fg'}`}
|
|
89
89
|
>
|
|
90
90
|
{value}
|
|
91
91
|
</span>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Subtle — inline de-emphasised text (the light `fg-subtle` grey, #b3b3b3). Wraps its
|
|
5
|
+
* children in a `text-fg-subtle` span; anything OUTSIDE it stays at the surrounding
|
|
6
|
+
* full-contrast `fg`. Compose it freely to two-tone arbitrary runs of a line — unlike
|
|
7
|
+
* a fixed-slot header it handles mid-line emphasis in ANY order and ANY language:
|
|
8
|
+
*
|
|
9
|
+
* <h1>
|
|
10
|
+
* <Subtle>Good afternoon</Subtle> {name} 👋
|
|
11
|
+
* <br />
|
|
12
|
+
* <Subtle>You have</Subtle> {count} planned tasks <Subtle>today</Subtle>
|
|
13
|
+
* </h1>
|
|
14
|
+
*
|
|
15
|
+
* With i18n, put the `<subtle>…</subtle>` runs INSIDE the translation string and render
|
|
16
|
+
* it via `<Trans components={{ subtle: <Subtle /> }} />`, so each locale places the
|
|
17
|
+
* emphasis where its own word order needs it. Presentational only (hard rule #1).
|
|
18
|
+
*/
|
|
19
|
+
export function Subtle({ children }: { children: ReactNode }) {
|
|
20
|
+
return <span className="text-fg-subtle">{children}</span>
|
|
21
|
+
}
|
package/src/components/text.tsx
CHANGED
|
@@ -27,14 +27,15 @@ export const Text = uic('span', {
|
|
|
27
27
|
size: {
|
|
28
28
|
micro: 'text-micro',
|
|
29
29
|
caption: 'text-caption',
|
|
30
|
-
label: 'text-
|
|
30
|
+
label: 'text-compact',
|
|
31
31
|
compact: 'text-compact',
|
|
32
32
|
callout: 'text-callout',
|
|
33
33
|
body: 'text-body',
|
|
34
|
-
subtitle: 'text-
|
|
35
|
-
title: 'text-
|
|
36
|
-
headline: 'text-
|
|
34
|
+
subtitle: 'text-heading4',
|
|
35
|
+
title: 'text-heading3',
|
|
36
|
+
headline: 'text-heading2',
|
|
37
37
|
display: 'text-display',
|
|
38
|
+
'display-xl': 'text-display-xl',
|
|
38
39
|
},
|
|
39
40
|
// NC Fontina caps at medium (500) as the heaviest weight in the system, so
|
|
40
41
|
// `semibold` and `bold` are kept as API-compatible aliases that render
|
package/src/components/tile.tsx
CHANGED
|
@@ -64,7 +64,7 @@ function TileBody({ head, children, tail }: Pick<TileProps, 'head' | 'children'
|
|
|
64
64
|
)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const baseSurface = 'flex h-full min-h-[200px] flex-col gap-6 rounded-
|
|
67
|
+
const baseSurface = 'flex h-full min-h-[200px] flex-col gap-6 rounded-lg p-6'
|
|
68
68
|
|
|
69
69
|
export function Tile({ theme = 'light', head, children, tail, link, className }: TileProps) {
|
|
70
70
|
const surface = [baseSurface, THEME[theme], className].filter(Boolean).join(' ')
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,10 @@ export * from "./layout/persistent-page-shell";
|
|
|
52
52
|
|
|
53
53
|
// --- product patterns (label-driven; no domain coupling) ---
|
|
54
54
|
export * from "./components/brand-page-header";
|
|
55
|
+
export * from "./components/cta-pill";
|
|
56
|
+
export * from "./components/feature-tile";
|
|
57
|
+
export * from "./components/icons";
|
|
58
|
+
export * from "./components/subtle";
|
|
55
59
|
export * from "./components/stats-card";
|
|
56
60
|
export * from "./components/tile";
|
|
57
61
|
export * from "./components/dashboard-grid";
|
package/src/layout/card.tsx
CHANGED
|
@@ -4,9 +4,8 @@ import { uic } from '../utils/uic'
|
|
|
4
4
|
* Card — surface panel (port of gs-manager's `Card`).
|
|
5
5
|
*
|
|
6
6
|
* gs SCSS → our tokens (Tailwind + `@app/tokens` CSS vars, no SCSS — hard rule #3):
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
* used `--radius-lg`, so this maps to `rounded-lg`.
|
|
7
|
+
* - `rounded-lg` (`--radius-lg` = 0.5rem = 8px) — the single unified card/panel
|
|
8
|
+
* radius across the design system (the old 1.1rem soft-panel radius was retired).
|
|
10
9
|
* - `--color-background-secondary` (the light card fill) → `bg-surface-card`
|
|
11
10
|
* (our `--color-surface-card` = #f7f6f2).
|
|
12
11
|
* - `outlined` → 1px `--color-border` (#eceae1) → `border border-border`.
|