@podoba/react 0.0.15 → 0.0.17
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/combobox.tsx +1 -1
- package/src/components/cta-pill.tsx +1 -1
- package/src/components/date-picker.tsx +1 -1
- package/src/components/feature-tile.tsx +140 -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/text.tsx +5 -4
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podoba/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
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.17",
|
|
29
|
+
"@podoba/tailwind": "^0.0.17",
|
|
30
30
|
"react-aria-components": "1.18.0",
|
|
31
31
|
"class-variance-authority": "0.7.1",
|
|
32
32
|
"clsx": "2.1.1",
|
|
@@ -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
|
|
@@ -27,7 +27,7 @@ export interface CtaPillProps {
|
|
|
27
27
|
export function CtaPill({ lead, emphasis, tail, children }: CtaPillProps) {
|
|
28
28
|
return (
|
|
29
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-
|
|
30
|
+
<p className="text-heading4 font-medium leading-[22px] tracking-normal text-fg">
|
|
31
31
|
{lead} <span className="font-semibold text-white">{emphasis}</span> {tail}
|
|
32
32
|
</p>
|
|
33
33
|
<div className="shrink-0">{children}</div>
|
|
@@ -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,140 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
|
+
import { CloseIcon } from './icons'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FeatureTile — a full-width media hero tile: a background image with an overlaid
|
|
6
|
+
* eyebrow (top-left), title (bottom-left), an optional action control and a badge.
|
|
7
|
+
* Generic — the common use is a "start here" tutorial banner, but it fits any
|
|
8
|
+
* promo/feature spotlight. Presentational only (hard rule #1): text arrives via props.
|
|
9
|
+
*
|
|
10
|
+
* Pressing the tile either PLAYS an inline video (when `video` is set — the poster
|
|
11
|
+
* image + overlays are replaced by a `<video controls autoPlay>` filling the tile, no
|
|
12
|
+
* modal) or fires `onPress` (e.g. navigate). `action` / `badge` sit ABOVE the press
|
|
13
|
+
* target (z-10) so their own handlers still work. Falls back to a dark surface when no
|
|
14
|
+
* `image` is given.
|
|
15
|
+
*/
|
|
16
|
+
export interface FeatureTileProps {
|
|
17
|
+
/** Background/poster image URL. Falls back to a dark surface when omitted. */
|
|
18
|
+
image?: string
|
|
19
|
+
imageAlt?: string
|
|
20
|
+
/** When set, pressing the tile plays this video INLINE (no modal). */
|
|
21
|
+
video?: string
|
|
22
|
+
/** Small category label, top-left (e.g. "Tutorial"). */
|
|
23
|
+
eyebrow?: ReactNode
|
|
24
|
+
/** Large title, bottom-left. */
|
|
25
|
+
title: ReactNode
|
|
26
|
+
/** Bottom-left action control (e.g. a round icon button). */
|
|
27
|
+
action?: ReactNode
|
|
28
|
+
/** Bottom-right badge (e.g. a duration pill). */
|
|
29
|
+
badge?: ReactNode
|
|
30
|
+
/** Press behaviour when there is NO `video`. `pressLabel` names it for a11y. */
|
|
31
|
+
onPress?: () => void
|
|
32
|
+
pressLabel?: string
|
|
33
|
+
/** Accessible label for the "stop / back to poster" control while playing. */
|
|
34
|
+
closeLabel?: string
|
|
35
|
+
/** Override the default `aspect-[2/1]` ratio / sizing. */
|
|
36
|
+
className?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function FeatureTile({
|
|
40
|
+
image,
|
|
41
|
+
imageAlt,
|
|
42
|
+
video,
|
|
43
|
+
eyebrow,
|
|
44
|
+
title,
|
|
45
|
+
action,
|
|
46
|
+
badge,
|
|
47
|
+
onPress,
|
|
48
|
+
pressLabel,
|
|
49
|
+
closeLabel = 'Close video',
|
|
50
|
+
className,
|
|
51
|
+
}: FeatureTileProps) {
|
|
52
|
+
const [playing, setPlaying] = useState(false)
|
|
53
|
+
const hasVideo = Boolean(video)
|
|
54
|
+
const pressable = hasVideo || Boolean(onPress)
|
|
55
|
+
|
|
56
|
+
const handlePress = () => {
|
|
57
|
+
if (hasVideo) setPlaying(true)
|
|
58
|
+
else onPress?.()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={[
|
|
64
|
+
// A large fixed-aspect banner (default ~2:1); media covers it. Override the
|
|
65
|
+
// ratio/size via `className` (e.g. `aspect-[1440/700]`).
|
|
66
|
+
'relative isolate aspect-[2/1] w-full overflow-hidden rounded-lg bg-surface-inverted',
|
|
67
|
+
className,
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join(' ')}
|
|
71
|
+
>
|
|
72
|
+
{playing && video ? (
|
|
73
|
+
<>
|
|
74
|
+
{/* eslint-disable-next-line jsx-a11y/media-has-caption -- caption track is
|
|
75
|
+
supplied by the consumer via a fuller player when needed. */}
|
|
76
|
+
<video
|
|
77
|
+
src={video}
|
|
78
|
+
poster={image}
|
|
79
|
+
className="absolute inset-0 z-0 h-full w-full bg-black object-cover"
|
|
80
|
+
controls
|
|
81
|
+
autoPlay
|
|
82
|
+
playsInline
|
|
83
|
+
/>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
aria-label={closeLabel}
|
|
87
|
+
onClick={() => setPlaying(false)}
|
|
88
|
+
className="absolute right-4 top-4 z-20 flex h-8 w-8 items-center justify-center rounded-full bg-black/60 text-white outline-none transition-colors hover:bg-black/80 focus-visible:ring-2 focus-visible:ring-white/70"
|
|
89
|
+
>
|
|
90
|
+
<CloseIcon className="h-4 w-4" />
|
|
91
|
+
</button>
|
|
92
|
+
</>
|
|
93
|
+
) : (
|
|
94
|
+
<>
|
|
95
|
+
{image ? (
|
|
96
|
+
<img
|
|
97
|
+
src={image}
|
|
98
|
+
alt={imageAlt ?? ''}
|
|
99
|
+
className="absolute inset-0 h-full w-full object-cover"
|
|
100
|
+
/>
|
|
101
|
+
) : null}
|
|
102
|
+
{/* Legibility scrim — darkens the top + bottom where the text sits. */}
|
|
103
|
+
<div
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
className="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-black/60"
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
{/* Full-bleed press target (behind the content controls). */}
|
|
109
|
+
{pressable ? (
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
onClick={handlePress}
|
|
113
|
+
aria-label={pressLabel}
|
|
114
|
+
className="absolute inset-0 z-0 cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/70"
|
|
115
|
+
/>
|
|
116
|
+
) : null}
|
|
117
|
+
|
|
118
|
+
{eyebrow ? (
|
|
119
|
+
<span className="pointer-events-none absolute left-6 top-6 z-10 text-compact font-medium text-white">
|
|
120
|
+
{eyebrow}
|
|
121
|
+
</span>
|
|
122
|
+
) : null}
|
|
123
|
+
|
|
124
|
+
{/* Content is click-through (pointer-events-none) so pressing the title
|
|
125
|
+
or badge falls through to the play/press overlay; only `action`
|
|
126
|
+
re-enables its own pointer events. */}
|
|
127
|
+
<div className="pointer-events-none absolute inset-x-6 bottom-6 z-10 flex flex-col gap-4">
|
|
128
|
+
<h2 className="max-w-md text-display-xl font-medium leading-tight text-white">
|
|
129
|
+
{title}
|
|
130
|
+
</h2>
|
|
131
|
+
<div className="flex items-center justify-between gap-4">
|
|
132
|
+
<span className="pointer-events-auto">{action}</span>
|
|
133
|
+
<span>{badge}</span>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -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>
|
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/index.ts
CHANGED
|
@@ -53,6 +53,8 @@ export * from "./layout/persistent-page-shell";
|
|
|
53
53
|
// --- product patterns (label-driven; no domain coupling) ---
|
|
54
54
|
export * from "./components/brand-page-header";
|
|
55
55
|
export * from "./components/cta-pill";
|
|
56
|
+
export * from "./components/feature-tile";
|
|
57
|
+
export * from "./components/icons";
|
|
56
58
|
export * from "./components/subtle";
|
|
57
59
|
export * from "./components/stats-card";
|
|
58
60
|
export * from "./components/tile";
|