@podoba/react 0.0.15 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podoba/react",
3
- "version": "0.0.15",
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.15",
29
- "@podoba/tailwind": "^0.0.15",
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",
@@ -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-3xl font-medium text-fg outline-none placeholder:text-fg-subtle"
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-subtitle font-medium leading-[22px] tracking-normal text-fg">
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-3xl font-medium')}>
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-3xl [&_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-3xl [&_textarea]:font-medium [&_textarea]:text-fg [&_textarea]:outline-none [&_textarea]:placeholder:text-fg-subtle'
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-[1.875rem] font-medium leading-[2rem] ${dark ? 'tracking-normal text-white' : 'tracking-wide text-fg'}`}
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>
@@ -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-label',
30
+ label: 'text-compact',
31
31
  compact: 'text-compact',
32
32
  callout: 'text-callout',
33
33
  body: 'text-body',
34
- subtitle: 'text-subtitle',
35
- title: 'text-title',
36
- headline: 'text-headline',
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";