@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podoba/react",
3
- "version": "0.0.14",
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.14",
29
- "@podoba/tailwind": "^0.0.14",
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-[-0.56px] text-fg">
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-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
@@ -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-xl bg-gradient-to-b from-[#242424] to-[#1f1f1f] ' +
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-[-0.2px] text-[#c8c8ca]">
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-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-[-0.02em] 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>
@@ -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
+ }
@@ -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
@@ -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-panel p-6'
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";
@@ -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
- * - `--radius-lg` (8px) → `rounded-lg` (our `--radius-lg` is 0.5rem = 8px). NB the
8
- * `rounded-panel` token is 1.1rem (~17.6px), NOT the 8px card radius gs's Card
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`.