@podoba/react 0.0.17 → 0.0.19
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 +2 -2
- package/src/components/checkbox.tsx +1 -1
- package/src/components/combobox.tsx +7 -7
- package/src/components/count-up.tsx +59 -0
- package/src/components/date-field.tsx +5 -5
- package/src/components/date-picker.tsx +4 -4
- package/src/components/delivery/delivery-status-module.tsx +3 -3
- package/src/components/delivery/download-modal.tsx +7 -7
- package/src/components/delivery/publish-modal.tsx +2 -2
- package/src/components/delivery/send-to-print-modal.tsx +2 -2
- package/src/components/dialog.tsx +1 -1
- package/src/components/dropdown-menu.tsx +2 -2
- package/src/components/feature-tile.tsx +1 -1
- package/src/components/file-upload.tsx +3 -3
- package/src/components/focus-field.tsx +4 -4
- package/src/components/input.tsx +3 -3
- package/src/components/multiselect.tsx +6 -6
- package/src/components/number-field.tsx +5 -5
- package/src/components/radio.tsx +4 -4
- package/src/components/request-changes-modal.tsx +4 -4
- package/src/components/rich-text-editor.tsx +4 -4
- package/src/components/search-field.tsx +2 -2
- package/src/components/select.tsx +4 -4
- package/src/components/slider.tsx +1 -1
- package/src/components/stat-stack.tsx +61 -0
- package/src/components/switch.tsx +1 -1
- package/src/components/tabs.tsx +2 -2
- package/src/components/tag-group.tsx +2 -2
- package/src/components/task-approval-modal.tsx +4 -4
- package/src/components/text.tsx +5 -5
- package/src/components/textarea.tsx +4 -4
- package/src/index.ts +2 -0
- package/src/layout/persistent-page-shell.tsx +3 -3
- package/src/layout/topbar.tsx +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podoba/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
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.19",
|
|
29
|
+
"@podoba/tailwind": "^0.0.19",
|
|
30
30
|
"react-aria-components": "1.18.0",
|
|
31
31
|
"class-variance-authority": "0.7.1",
|
|
32
32
|
"clsx": "2.1.1",
|
|
@@ -141,12 +141,12 @@ export function BrandPageHeader({
|
|
|
141
141
|
onPress={() => setExpanded(!expanded)}
|
|
142
142
|
aria-expanded={expanded}
|
|
143
143
|
aria-controls={PANEL_ID}
|
|
144
|
-
className="h-10 rounded-full bg-brand-secondary px-5 text-
|
|
144
|
+
className="h-10 rounded-full bg-brand-secondary px-5 text-small font-medium text-fg data-[hovered]:opacity-90 data-[pressed]:opacity-80"
|
|
145
145
|
>
|
|
146
146
|
{ctaLabel}
|
|
147
147
|
</Button>
|
|
148
148
|
) : (
|
|
149
|
-
<Button className="h-10 rounded-full bg-brand-secondary px-5 text-
|
|
149
|
+
<Button className="h-10 rounded-full bg-brand-secondary px-5 text-small font-medium text-fg data-[hovered]:opacity-90 data-[pressed]:opacity-80">
|
|
150
150
|
{ctaLabel}
|
|
151
151
|
</Button>
|
|
152
152
|
)}
|
|
@@ -37,7 +37,7 @@ export type CheckboxProps = Omit<RACCheckboxProps, 'children'> & {
|
|
|
37
37
|
export const Checkbox = ({ children, ...props }: CheckboxProps) => (
|
|
38
38
|
<RACCheckbox
|
|
39
39
|
{...props}
|
|
40
|
-
className="group flex cursor-pointer select-none items-center gap-3 text-
|
|
40
|
+
className="group flex cursor-pointer select-none items-center gap-3 text-small text-fg data-[disabled]:cursor-not-allowed"
|
|
41
41
|
>
|
|
42
42
|
{({ isSelected, isIndeterminate }) => (
|
|
43
43
|
<>
|
|
@@ -36,7 +36,7 @@ const Chevron = () => (
|
|
|
36
36
|
const ComboBoxInput = uic(RACInput, {
|
|
37
37
|
displayName: 'ComboBoxInput',
|
|
38
38
|
baseClass:
|
|
39
|
-
'h-12 w-full rounded-lg border border-border bg-surface pl-4 pr-11 text-
|
|
39
|
+
'h-12 w-full rounded-lg border border-border bg-surface pl-4 pr-11 text-small text-fg ' +
|
|
40
40
|
'outline-none transition-colors placeholder:text-fg-muted ' +
|
|
41
41
|
'data-[hovered]:border-fg-subtle ' +
|
|
42
42
|
'data-[focused]:border-brand-green data-[focused]:ring-2 data-[focused]:ring-ring ' +
|
|
@@ -48,7 +48,7 @@ export const ComboBoxItem = uic(ListBoxItem, {
|
|
|
48
48
|
displayName: 'ComboBoxItem',
|
|
49
49
|
// Matches SelectItem so the two dropdowns are indistinguishable.
|
|
50
50
|
baseClass:
|
|
51
|
-
'flex cursor-pointer select-none items-center rounded-md px-3 py-2 text-
|
|
51
|
+
'flex cursor-pointer select-none items-center rounded-md px-3 py-2 text-small text-fg outline-none ' +
|
|
52
52
|
'data-[hovered]:bg-surface-muted data-[focused]:bg-surface-muted data-[selected]:font-medium ' +
|
|
53
53
|
'data-[disabled]:opacity-50 data-[disabled]:pointer-events-none',
|
|
54
54
|
}) as (props: ListBoxItemProps) => ReactNode
|
|
@@ -76,7 +76,7 @@ export const ComboBox = <T extends object>({
|
|
|
76
76
|
const inFocus = useInFocusOverlay()
|
|
77
77
|
const { contains } = useFilter({ sensitivity: 'base' })
|
|
78
78
|
const desc = description ? (
|
|
79
|
-
<Text slot="description" className="text-
|
|
79
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
80
80
|
{description}
|
|
81
81
|
</Text>
|
|
82
82
|
) : null
|
|
@@ -101,14 +101,14 @@ export const ComboBox = <T extends object>({
|
|
|
101
101
|
const k = keys === 'all' ? undefined : [...keys][0]
|
|
102
102
|
onSelectionChange?.(k ?? null)
|
|
103
103
|
}}
|
|
104
|
-
renderEmptyState={() => <div className="px-3 py-2 text-
|
|
104
|
+
renderEmptyState={() => <div className="px-3 py-2 text-small text-fg-muted">No results</div>}
|
|
105
105
|
className="mt-2 flex max-h-72 flex-col gap-0.5 overflow-auto overscroll-contain p-1 outline-none"
|
|
106
106
|
>
|
|
107
107
|
{children}
|
|
108
108
|
</ListBox>
|
|
109
109
|
</Autocomplete>
|
|
110
110
|
{desc}
|
|
111
|
-
{errorMessage ? <span className="text-
|
|
111
|
+
{errorMessage ? <span className="text-label text-danger">{errorMessage}</span> : null}
|
|
112
112
|
</div>
|
|
113
113
|
)
|
|
114
114
|
}
|
|
@@ -132,11 +132,11 @@ export const ComboBox = <T extends object>({
|
|
|
132
132
|
</RACButton>
|
|
133
133
|
</div>
|
|
134
134
|
{desc}
|
|
135
|
-
<FieldError className="text-
|
|
135
|
+
<FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
136
136
|
<Popover className="min-w-[var(--trigger-width)] overflow-hidden rounded-lg bg-surface-card shadow-lg">
|
|
137
137
|
<ListBox
|
|
138
138
|
className="flex max-h-72 flex-col gap-0.5 overflow-auto overscroll-contain p-1 outline-none"
|
|
139
|
-
renderEmptyState={() => <div className="px-3 py-2 text-
|
|
139
|
+
renderEmptyState={() => <div className="px-3 py-2 text-small text-fg-muted">No results</div>}
|
|
140
140
|
>
|
|
141
141
|
{children}
|
|
142
142
|
</ListBox>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ElementType } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CountUp — animates a number from 0 up to `value` on mount (and whenever `value`
|
|
5
|
+
* changes), for the subtle "counting up" effect on dashboard stats. easeOutCubic over
|
|
6
|
+
* ~900ms by default. Respects `prefers-reduced-motion` (renders the final value
|
|
7
|
+
* immediately). Presentational only — pass formatting via `format`.
|
|
8
|
+
*/
|
|
9
|
+
export interface CountUpProps {
|
|
10
|
+
/** Target value to count up to. */
|
|
11
|
+
value: number
|
|
12
|
+
/** Animation duration in ms (default 900). */
|
|
13
|
+
durationMs?: number
|
|
14
|
+
/** Format the displayed number (default `toLocaleString`). */
|
|
15
|
+
format?: (n: number) => string
|
|
16
|
+
/** Element to render as (default `span`). */
|
|
17
|
+
as?: ElementType
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3)
|
|
22
|
+
|
|
23
|
+
export function CountUp({
|
|
24
|
+
value,
|
|
25
|
+
durationMs = 900,
|
|
26
|
+
format = (n) => n.toLocaleString(),
|
|
27
|
+
as: Tag = 'span',
|
|
28
|
+
className,
|
|
29
|
+
}: CountUpProps) {
|
|
30
|
+
const [display, setDisplay] = useState(value)
|
|
31
|
+
const rafRef = useRef<number | null>(null)
|
|
32
|
+
const startRef = useRef<number | null>(null)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (typeof window === 'undefined') return
|
|
36
|
+
const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
37
|
+
if (reduce || durationMs <= 0) {
|
|
38
|
+
setDisplay(value)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
startRef.current = null
|
|
42
|
+
const tick = (now: number) => {
|
|
43
|
+
if (startRef.current === null) startRef.current = now
|
|
44
|
+
const progress = Math.min(1, (now - startRef.current) / durationMs)
|
|
45
|
+
setDisplay(Math.round(value * easeOutCubic(progress)))
|
|
46
|
+
if (progress < 1) rafRef.current = requestAnimationFrame(tick)
|
|
47
|
+
}
|
|
48
|
+
rafRef.current = requestAnimationFrame(tick)
|
|
49
|
+
return () => {
|
|
50
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
|
|
51
|
+
}
|
|
52
|
+
}, [value, durationMs])
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Tag className={className} aria-label={format(value)}>
|
|
56
|
+
{format(display)}
|
|
57
|
+
</Tag>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -28,7 +28,7 @@ export const segmentClass =
|
|
|
28
28
|
'data-[disabled]:opacity-50 data-[type=literal]:px-0 data-[type=literal]:text-fg-muted'
|
|
29
29
|
|
|
30
30
|
export const dateInputClass =
|
|
31
|
-
'flex h-12 w-full items-center gap-0.5 rounded-lg border border-border bg-surface px-4 text-
|
|
31
|
+
'flex h-12 w-full items-center gap-0.5 rounded-lg border border-border bg-surface px-4 text-small text-fg transition-colors ' +
|
|
32
32
|
'hover:border-fg-subtle focus-within:border-brand-green focus-within:ring-2 focus-within:ring-ring ' +
|
|
33
33
|
'group-data-[invalid]:border-danger group-data-[invalid]:ring-2 group-data-[invalid]:ring-danger'
|
|
34
34
|
|
|
@@ -43,11 +43,11 @@ export const DateField = <T extends DateValue>({ label, description, errorMessag
|
|
|
43
43
|
<Label className="text-heading5 font-medium text-fg">{label}</Label>
|
|
44
44
|
<DateInput className={dateInputClass}>{(segment) => <DateSegment segment={segment} className={segmentClass} />}</DateInput>
|
|
45
45
|
{description ? (
|
|
46
|
-
<Text slot="description" className="text-
|
|
46
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
47
47
|
{description}
|
|
48
48
|
</Text>
|
|
49
49
|
) : null}
|
|
50
|
-
<FieldError className="text-
|
|
50
|
+
<FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
51
51
|
</RACDateField>
|
|
52
52
|
)
|
|
53
53
|
|
|
@@ -62,10 +62,10 @@ export const TimeField = <T extends TimeValue>({ label, description, errorMessag
|
|
|
62
62
|
<Label className="text-heading5 font-medium text-fg">{label}</Label>
|
|
63
63
|
<DateInput className={dateInputClass}>{(segment) => <DateSegment segment={segment} className={segmentClass} />}</DateInput>
|
|
64
64
|
{description ? (
|
|
65
|
-
<Text slot="description" className="text-
|
|
65
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
66
66
|
{description}
|
|
67
67
|
</Text>
|
|
68
68
|
) : null}
|
|
69
|
-
<FieldError className="text-
|
|
69
|
+
<FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
70
70
|
</RACTimeField>
|
|
71
71
|
)
|
|
@@ -41,7 +41,7 @@ const Chevron = ({ dir }: { dir: 'left' | 'right' }) => (
|
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
const calendarCellClass =
|
|
44
|
-
'flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-
|
|
44
|
+
'flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-small text-fg outline-none transition-colors ' +
|
|
45
45
|
'data-[outside-month]:text-fg-subtle data-[hovered]:bg-surface-muted ' +
|
|
46
46
|
'data-[selected]:bg-fg data-[selected]:text-fg-inverted data-[selected]:font-medium ' +
|
|
47
47
|
'data-[unavailable]:text-fg-subtle data-[unavailable]:line-through ' +
|
|
@@ -54,7 +54,7 @@ function CalendarBody() {
|
|
|
54
54
|
<RACButton slot="previous" className={navButton}>
|
|
55
55
|
<Chevron dir="left" />
|
|
56
56
|
</RACButton>
|
|
57
|
-
<Heading className="text-
|
|
57
|
+
<Heading className="text-small font-medium text-fg" />
|
|
58
58
|
<RACButton slot="next" className={navButton}>
|
|
59
59
|
<Chevron dir="right" />
|
|
60
60
|
</RACButton>
|
|
@@ -104,11 +104,11 @@ export const DatePicker = <T extends DateValue>({ label, description, errorMessa
|
|
|
104
104
|
)}
|
|
105
105
|
</Group>
|
|
106
106
|
{description ? (
|
|
107
|
-
<Text slot="description" className="text-
|
|
107
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
108
108
|
{description}
|
|
109
109
|
</Text>
|
|
110
110
|
) : null}
|
|
111
|
-
<FieldError className="text-
|
|
111
|
+
<FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
112
112
|
{inFocus ? (
|
|
113
113
|
<div className="mt-2 -ml-2">
|
|
114
114
|
<CalendarBody />
|
|
@@ -75,13 +75,13 @@ export function DeliveryStatusModule({
|
|
|
75
75
|
|
|
76
76
|
return (
|
|
77
77
|
<DeliveryStatusModuleRoot data-testid={testId}>
|
|
78
|
-
<div className="flex items-center justify-between gap-4 text-
|
|
78
|
+
<div className="flex items-center justify-between gap-4 text-small font-medium">
|
|
79
79
|
<span>{heading}</span>
|
|
80
80
|
{headingMeta != null ? <span>{headingMeta}</span> : null}
|
|
81
81
|
</div>
|
|
82
82
|
|
|
83
83
|
{statusText != null ? (
|
|
84
|
-
<p className="m-0 text-
|
|
84
|
+
<p className="m-0 text-label text-white/70">{statusText}</p>
|
|
85
85
|
) : null}
|
|
86
86
|
|
|
87
87
|
{hasProgress ? (
|
|
@@ -97,7 +97,7 @@ export function DeliveryStatusModule({
|
|
|
97
97
|
) : null}
|
|
98
98
|
|
|
99
99
|
{children != null ? (
|
|
100
|
-
<div className="flex flex-wrap items-center gap-2 text-
|
|
100
|
+
<div className="flex flex-wrap items-center gap-2 text-label text-white/80">
|
|
101
101
|
{children}
|
|
102
102
|
</div>
|
|
103
103
|
) : null}
|
|
@@ -109,11 +109,11 @@ export function DownloadModal({
|
|
|
109
109
|
onOpenChange={onOpenChange}
|
|
110
110
|
>
|
|
111
111
|
<div className="flex flex-col gap-3">
|
|
112
|
-
<p className="text-
|
|
112
|
+
<p className="text-small text-fg-muted">{labels.body}</p>
|
|
113
113
|
|
|
114
114
|
{hasArtifact ? (
|
|
115
115
|
<>
|
|
116
|
-
<p data-testid="delivery-download-ready" role="status" className="text-
|
|
116
|
+
<p data-testid="delivery-download-ready" role="status" className="text-small text-fg">
|
|
117
117
|
{labels.ready}
|
|
118
118
|
</p>
|
|
119
119
|
<div className="mt-2 flex justify-end gap-2">
|
|
@@ -126,7 +126,7 @@ export function DownloadModal({
|
|
|
126
126
|
data-testid="delivery-download-link"
|
|
127
127
|
href={artifactUrl ?? undefined}
|
|
128
128
|
download
|
|
129
|
-
className="inline-flex h-8 items-center justify-center gap-2 rounded-md bg-brand-primary px-3 text-
|
|
129
|
+
className="inline-flex h-8 items-center justify-center gap-2 rounded-md bg-brand-primary px-3 text-small font-medium text-fg-inverted no-underline outline-none transition-colors hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
130
130
|
>
|
|
131
131
|
{labels.download}
|
|
132
132
|
</a>
|
|
@@ -134,7 +134,7 @@ export function DownloadModal({
|
|
|
134
134
|
</>
|
|
135
135
|
) : isPreparing ? (
|
|
136
136
|
<>
|
|
137
|
-
<p data-testid="delivery-download-preparing" role="status" className="text-
|
|
137
|
+
<p data-testid="delivery-download-preparing" role="status" className="text-small text-fg-muted">
|
|
138
138
|
{labels.preparing}
|
|
139
139
|
</p>
|
|
140
140
|
<div className="mt-2 flex justify-end gap-2">
|
|
@@ -151,7 +151,7 @@ export function DownloadModal({
|
|
|
151
151
|
onChange={(value: string) => setFormat(value as DownloadFormat)}
|
|
152
152
|
className="flex flex-col gap-2"
|
|
153
153
|
>
|
|
154
|
-
<Label className="text-
|
|
154
|
+
<Label className="text-small font-medium text-fg">{labels.formatLabel}</Label>
|
|
155
155
|
<div className="flex gap-2">
|
|
156
156
|
<DownloadFormatOption value="png" label={labels.formatPng} />
|
|
157
157
|
<DownloadFormatOption value="pdf" label={labels.formatPdf} />
|
|
@@ -159,7 +159,7 @@ export function DownloadModal({
|
|
|
159
159
|
</RadioGroup>
|
|
160
160
|
|
|
161
161
|
{error ? (
|
|
162
|
-
<p data-testid="delivery-download-error" role="alert" className="text-
|
|
162
|
+
<p data-testid="delivery-download-error" role="alert" className="text-small text-danger">
|
|
163
163
|
{error}
|
|
164
164
|
</p>
|
|
165
165
|
) : null}
|
|
@@ -196,7 +196,7 @@ function DownloadFormatOption({ value, label }: { value: Key; label: string }):
|
|
|
196
196
|
return (
|
|
197
197
|
<Radio
|
|
198
198
|
value={String(value)}
|
|
199
|
-
className="flex cursor-pointer items-center gap-2 rounded-md border border-border bg-surface px-3 py-2 text-
|
|
199
|
+
className="flex cursor-pointer items-center gap-2 rounded-md border border-border bg-surface px-3 py-2 text-small text-fg outline-none transition-colors data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[selected]:border-brand-primary data-[selected]:font-medium"
|
|
200
200
|
>
|
|
201
201
|
{label}
|
|
202
202
|
</Radio>
|
|
@@ -115,7 +115,7 @@ export function PublishModal({
|
|
|
115
115
|
onOpenChange={handleOpenChange}
|
|
116
116
|
>
|
|
117
117
|
<div className="flex flex-col gap-3">
|
|
118
|
-
<p className="text-
|
|
118
|
+
<p className="text-small text-fg-muted">{labels.body}</p>
|
|
119
119
|
|
|
120
120
|
<Input
|
|
121
121
|
label={labels.destinationLabel}
|
|
@@ -139,7 +139,7 @@ export function PublishModal({
|
|
|
139
139
|
</Select>
|
|
140
140
|
|
|
141
141
|
{error ? (
|
|
142
|
-
<p data-testid="delivery-publish-error" role="alert" className="text-
|
|
142
|
+
<p data-testid="delivery-publish-error" role="alert" className="text-small text-danger">
|
|
143
143
|
{error}
|
|
144
144
|
</p>
|
|
145
145
|
) : null}
|
|
@@ -149,7 +149,7 @@ export function SendToPrintModal({
|
|
|
149
149
|
onOpenChange={handleOpenChange}
|
|
150
150
|
>
|
|
151
151
|
<div className="flex flex-col gap-3">
|
|
152
|
-
<p className="text-
|
|
152
|
+
<p className="text-small text-fg-muted">{labels.body}</p>
|
|
153
153
|
|
|
154
154
|
<Input
|
|
155
155
|
label={labels.sizeLabel}
|
|
@@ -185,7 +185,7 @@ export function SendToPrintModal({
|
|
|
185
185
|
/>
|
|
186
186
|
|
|
187
187
|
{error ? (
|
|
188
|
-
<p data-testid="delivery-print-error" role="alert" className="text-
|
|
188
|
+
<p data-testid="delivery-print-error" role="alert" className="text-small text-danger">
|
|
189
189
|
{error}
|
|
190
190
|
</p>
|
|
191
191
|
) : null}
|
|
@@ -152,7 +152,7 @@ export const Dialog = ({
|
|
|
152
152
|
{title}
|
|
153
153
|
</Heading>
|
|
154
154
|
{badge ? (
|
|
155
|
-
<span className="inline-flex shrink-0 items-center rounded-full bg-surface-muted px-2 py-0.5 text-
|
|
155
|
+
<span className="inline-flex shrink-0 items-center rounded-full bg-surface-muted px-2 py-0.5 text-label font-medium text-fg-muted">
|
|
156
156
|
{badge}
|
|
157
157
|
</span>
|
|
158
158
|
) : null}
|
|
@@ -57,13 +57,13 @@ export const DropdownMenuTrigger = RACMenuTrigger
|
|
|
57
57
|
// gs `.item`: 12/16px padding, 6px radius, base text on white; hover/focus wash
|
|
58
58
|
// `#f7f6f2` (→ surface-card). We light up both `data-[focused]` (keyboard) and
|
|
59
59
|
// `data-[hovered]` (pointer) so keyboard focus stays clearly visible. Item text
|
|
60
|
-
// size follows the house light Select (`text-
|
|
60
|
+
// size follows the house light Select (`text-small`) rather than gs's 16px base so
|
|
61
61
|
// the two light dropdowns stay consistent.
|
|
62
62
|
export const DropdownMenuItem = uic(RACMenuItem, {
|
|
63
63
|
displayName: 'DropdownMenuItem',
|
|
64
64
|
baseClass:
|
|
65
65
|
'flex cursor-pointer select-none items-center gap-2.5 rounded-md px-4 py-3 ' +
|
|
66
|
-
'text-
|
|
66
|
+
'text-small text-fg outline-none ' +
|
|
67
67
|
'data-[focused]:bg-surface-card data-[hovered]:bg-surface-card ' +
|
|
68
68
|
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
|
69
69
|
variants: {
|
|
@@ -125,7 +125,7 @@ export function FeatureTile({
|
|
|
125
125
|
or badge falls through to the play/press overlay; only `action`
|
|
126
126
|
re-enables its own pointer events. */}
|
|
127
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
|
|
128
|
+
<h2 className="max-w-md text-display font-medium leading-tight text-white">
|
|
129
129
|
{title}
|
|
130
130
|
</h2>
|
|
131
131
|
<div className="flex items-center justify-between gap-4">
|
|
@@ -47,7 +47,7 @@ export const FileUpload = ({ label, description, accept, allowsMultiple, onFiles
|
|
|
47
47
|
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring'
|
|
48
48
|
}
|
|
49
49
|
>
|
|
50
|
-
<Text slot="label" className="text-
|
|
50
|
+
<Text slot="label" className="text-small text-fg-muted">
|
|
51
51
|
Drag & drop {allowsMultiple ? 'files' : 'a file'} here, or
|
|
52
52
|
</Text>
|
|
53
53
|
<FileTrigger
|
|
@@ -59,9 +59,9 @@ export const FileUpload = ({ label, description, accept, allowsMultiple, onFiles
|
|
|
59
59
|
Choose {allowsMultiple ? 'files' : 'file'}
|
|
60
60
|
</Button>
|
|
61
61
|
</FileTrigger>
|
|
62
|
-
{names.length > 0 ? <span className="max-w-full truncate text-
|
|
62
|
+
{names.length > 0 ? <span className="max-w-full truncate text-label text-fg">{names.join(', ')}</span> : null}
|
|
63
63
|
</DropZone>
|
|
64
|
-
{description ? <span className="text-
|
|
64
|
+
{description ? <span className="text-label text-fg-muted">{description}</span> : null}
|
|
65
65
|
</div>
|
|
66
66
|
)
|
|
67
67
|
}
|
|
@@ -90,7 +90,7 @@ const OVERLAY_EDITOR =
|
|
|
90
90
|
|
|
91
91
|
function IconBadge({ icon }: { icon: ReactNode }) {
|
|
92
92
|
return (
|
|
93
|
-
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-surface-muted text-
|
|
93
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-surface-muted text-small font-medium text-fg">
|
|
94
94
|
{icon}
|
|
95
95
|
</div>
|
|
96
96
|
)
|
|
@@ -164,8 +164,8 @@ export function FocusField({
|
|
|
164
164
|
>
|
|
165
165
|
{icon ? <IconBadge icon={icon} /> : null}
|
|
166
166
|
<div className="min-w-0 flex-1">
|
|
167
|
-
<div className="text-
|
|
168
|
-
<div className="mt-1 truncate text-
|
|
167
|
+
<div className="text-small text-fg-muted">{label}</div>
|
|
168
|
+
<div className="mt-1 truncate text-body text-fg">
|
|
169
169
|
{hasValue ? preview : <span className="text-fg-subtle">{placeholder}</span>}
|
|
170
170
|
</div>
|
|
171
171
|
</div>
|
|
@@ -198,7 +198,7 @@ export function FocusField({
|
|
|
198
198
|
<div className="flex items-start gap-3">
|
|
199
199
|
{icon ? <IconBadge icon={icon} /> : null}
|
|
200
200
|
<div className="min-w-0 flex-1 pr-10">
|
|
201
|
-
<div className="text-
|
|
201
|
+
<div className="text-small text-fg-muted">{label}</div>
|
|
202
202
|
<div className={OVERLAY_EDITOR}>{children}</div>
|
|
203
203
|
</div>
|
|
204
204
|
</div>
|
package/src/components/input.tsx
CHANGED
|
@@ -28,7 +28,7 @@ const StyledInput = uic(RACInput, {
|
|
|
28
28
|
// Identical to textarea.tsx's filled-field skin; `fieldSize` adds the
|
|
29
29
|
// single-line height (gs sizes the field via padding only).
|
|
30
30
|
baseClass:
|
|
31
|
-
'w-full rounded-lg border border-border bg-surface px-4 text-
|
|
31
|
+
'w-full rounded-lg border border-border bg-surface px-4 text-small text-fg ' +
|
|
32
32
|
'outline-none transition-colors duration-200 placeholder:text-fg-muted ' +
|
|
33
33
|
'data-[hovered]:border-fg-subtle ' +
|
|
34
34
|
'data-[focused]:border-brand-green data-[focused]:ring-2 data-[focused]:ring-ring ' +
|
|
@@ -64,10 +64,10 @@ export const Input = ({ label, description, errorMessage, placeholder, size, ...
|
|
|
64
64
|
<Label className="text-heading5 font-medium text-fg">{label}</Label>
|
|
65
65
|
<StyledInput placeholder={placeholder} fieldSize={size} />
|
|
66
66
|
{description ? (
|
|
67
|
-
<Text slot="description" className="text-
|
|
67
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
68
68
|
{description}
|
|
69
69
|
</Text>
|
|
70
70
|
) : null}
|
|
71
|
-
<FieldError className="text-
|
|
71
|
+
<FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
72
72
|
</TextField>
|
|
73
73
|
)
|
|
@@ -51,7 +51,7 @@ const MultiSelectItem = uic(ListBoxItem, {
|
|
|
51
51
|
displayName: 'MultiSelectItem',
|
|
52
52
|
// `group` so the leading Check can react to this item's data-selected.
|
|
53
53
|
baseClass:
|
|
54
|
-
'group flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-
|
|
54
|
+
'group flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-small text-fg outline-none ' +
|
|
55
55
|
'data-[hovered]:bg-surface-muted data-[focused]:bg-surface-muted data-[selected]:font-medium ' +
|
|
56
56
|
'data-[disabled]:opacity-50 data-[disabled]:pointer-events-none',
|
|
57
57
|
}) as (props: ListBoxItemProps) => ReactNode
|
|
@@ -116,7 +116,7 @@ export const MultiSelect = ({
|
|
|
116
116
|
selectionMode="multiple"
|
|
117
117
|
selectedKeys={selected}
|
|
118
118
|
onSelectionChange={handleChange}
|
|
119
|
-
renderEmptyState={() => <div className="px-3 py-2 text-
|
|
119
|
+
renderEmptyState={() => <div className="px-3 py-2 text-small text-fg-muted">No matches</div>}
|
|
120
120
|
className="flex max-h-64 flex-col gap-0.5 overflow-auto overscroll-contain p-1 outline-none"
|
|
121
121
|
>
|
|
122
122
|
{options.map((o) => (
|
|
@@ -133,7 +133,7 @@ export const MultiSelect = ({
|
|
|
133
133
|
<SearchField aria-label="Filter options" autoFocus className="border-b border-border p-1">
|
|
134
134
|
<RACInput
|
|
135
135
|
placeholder="Search…"
|
|
136
|
-
className="w-full rounded-md bg-surface px-3 py-2 text-
|
|
136
|
+
className="w-full rounded-md bg-surface px-3 py-2 text-small text-fg outline-none placeholder:text-fg-muted"
|
|
137
137
|
/>
|
|
138
138
|
</SearchField>
|
|
139
139
|
{list}
|
|
@@ -142,8 +142,8 @@ export const MultiSelect = ({
|
|
|
142
142
|
list
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
const desc = description ? <span className="text-
|
|
146
|
-
const err = isInvalid && errorMessage ? <span className="text-
|
|
145
|
+
const desc = description ? <span className="text-label text-fg-muted">{description}</span> : null
|
|
146
|
+
const err = isInvalid && errorMessage ? <span className="text-label text-danger">{errorMessage}</span> : null
|
|
147
147
|
|
|
148
148
|
return (
|
|
149
149
|
<div className={clsx('flex flex-col gap-2', className)}>
|
|
@@ -165,7 +165,7 @@ export const MultiSelect = ({
|
|
|
165
165
|
isDisabled={isDisabled}
|
|
166
166
|
onPress={() => setOpen(true)}
|
|
167
167
|
className={clsx(
|
|
168
|
-
'flex h-12 w-full items-center justify-between gap-2.5 rounded-lg border bg-surface px-4 text-
|
|
168
|
+
'flex h-12 w-full items-center justify-between gap-2.5 rounded-lg border bg-surface px-4 text-small text-fg outline-none transition-colors',
|
|
169
169
|
'data-[hovered]:border-fg-subtle data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring',
|
|
170
170
|
'data-[disabled]:bg-surface-muted data-[disabled]:opacity-60 data-[disabled]:pointer-events-none',
|
|
171
171
|
isInvalid ? 'border-danger ring-2 ring-danger' : 'border-border',
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
* Components `NumberField`; styled to match the other form fields.
|
|
17
17
|
*/
|
|
18
18
|
const stepper =
|
|
19
|
-
'flex h-full w-9 shrink-0 items-center justify-center text-
|
|
19
|
+
'flex h-full w-9 shrink-0 items-center justify-center text-body text-fg-muted outline-none transition-colors ' +
|
|
20
20
|
'hover:bg-surface-muted hover:text-fg data-[pressed]:bg-surface-muted data-[disabled]:opacity-40'
|
|
21
21
|
|
|
22
22
|
export type NumberFieldProps = RACNumberFieldProps & {
|
|
@@ -32,7 +32,7 @@ export const NumberField = ({ label, description, errorMessage, placeholder, ...
|
|
|
32
32
|
<Label className="text-heading5 font-medium text-fg">{label}</Label>
|
|
33
33
|
<Group
|
|
34
34
|
className={
|
|
35
|
-
'flex h-12 w-full items-center overflow-hidden rounded-lg border border-border bg-surface text-
|
|
35
|
+
'flex h-12 w-full items-center overflow-hidden rounded-lg border border-border bg-surface text-small text-fg transition-colors ' +
|
|
36
36
|
'data-[hovered]:border-fg-subtle ' +
|
|
37
37
|
'data-[focus-within]:border-brand-green data-[focus-within]:ring-2 data-[focus-within]:ring-ring ' +
|
|
38
38
|
'group-data-[invalid]:border-danger group-data-[invalid]:ring-2 group-data-[invalid]:ring-danger ' +
|
|
@@ -44,17 +44,17 @@ export const NumberField = ({ label, description, errorMessage, placeholder, ...
|
|
|
44
44
|
</RACButton>
|
|
45
45
|
<RACInput
|
|
46
46
|
placeholder={placeholder}
|
|
47
|
-
className="min-w-0 flex-1 bg-transparent px-4 text-
|
|
47
|
+
className="min-w-0 flex-1 bg-transparent px-4 text-small tabular-nums text-fg outline-none placeholder:text-fg-muted"
|
|
48
48
|
/>
|
|
49
49
|
<RACButton slot="increment" className={`${stepper} border-l border-border`}>
|
|
50
50
|
+
|
|
51
51
|
</RACButton>
|
|
52
52
|
</Group>
|
|
53
53
|
{description ? (
|
|
54
|
-
<Text slot="description" className="text-
|
|
54
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
55
55
|
{description}
|
|
56
56
|
</Text>
|
|
57
57
|
) : null}
|
|
58
|
-
<FieldError className="text-
|
|
58
|
+
<FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
59
59
|
</RACNumberField>
|
|
60
60
|
)
|
package/src/components/radio.tsx
CHANGED
|
@@ -31,14 +31,14 @@ export type RadioGroupProps = RACRadioGroupProps & {
|
|
|
31
31
|
|
|
32
32
|
export const RadioGroup = ({ label, description, errorMessage, children, ...props }: RadioGroupProps) => (
|
|
33
33
|
<RACRadioGroup {...props} className="flex flex-col gap-2">
|
|
34
|
-
{label ? <Label className="text-
|
|
34
|
+
{label ? <Label className="text-small font-medium text-fg">{label}</Label> : null}
|
|
35
35
|
<div className="flex flex-col gap-3">{children}</div>
|
|
36
36
|
{description ? (
|
|
37
|
-
<Text slot="description" className="text-
|
|
37
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
38
38
|
{description}
|
|
39
39
|
</Text>
|
|
40
40
|
) : null}
|
|
41
|
-
<FieldError className="text-
|
|
41
|
+
<FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
42
42
|
</RACRadioGroup>
|
|
43
43
|
)
|
|
44
44
|
|
|
@@ -50,7 +50,7 @@ export type RadioProps = Omit<RACRadioProps, 'children'> & {
|
|
|
50
50
|
export const Radio = ({ children, ...props }: RadioProps) => (
|
|
51
51
|
<RACRadio
|
|
52
52
|
{...props}
|
|
53
|
-
className="group flex cursor-pointer select-none items-center gap-3 text-
|
|
53
|
+
className="group flex cursor-pointer select-none items-center gap-3 text-small text-fg data-[disabled]:cursor-not-allowed"
|
|
54
54
|
>
|
|
55
55
|
{/* gs token map: border #eceae1 → border · hover #aba89c → fg-subtle ·
|
|
56
56
|
selected dot/border #0d0d0d → fg · error → danger. */}
|
|
@@ -87,8 +87,8 @@ export function RequestChangesModal({
|
|
|
87
87
|
}}
|
|
88
88
|
>
|
|
89
89
|
<div className="flex flex-col gap-3">
|
|
90
|
-
<p className="text-
|
|
91
|
-
<label htmlFor={noteId} className="text-
|
|
90
|
+
<p className="text-small text-fg-muted">{labels.body}</p>
|
|
91
|
+
<label htmlFor={noteId} className="text-small font-medium text-fg">
|
|
92
92
|
{labels.noteLabel}
|
|
93
93
|
</label>
|
|
94
94
|
<textarea
|
|
@@ -101,10 +101,10 @@ export function RequestChangesModal({
|
|
|
101
101
|
required
|
|
102
102
|
aria-invalid={isEmpty ? true : undefined}
|
|
103
103
|
placeholder={labels.notePlaceholder}
|
|
104
|
-
className="w-full rounded-md border border-border bg-surface px-3 py-2 text-
|
|
104
|
+
className="w-full rounded-md border border-border bg-surface px-3 py-2 text-small text-fg outline-none transition-colors placeholder:text-fg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
105
105
|
/>
|
|
106
106
|
{error ? (
|
|
107
|
-
<p data-testid="request-changes-error" role="alert" className="text-
|
|
107
|
+
<p data-testid="request-changes-error" role="alert" className="text-small text-danger">
|
|
108
108
|
{error}
|
|
109
109
|
</p>
|
|
110
110
|
) : null}
|
|
@@ -137,7 +137,7 @@ export function RichTextEditor({
|
|
|
137
137
|
title={t.title}
|
|
138
138
|
onMouseDown={(e) => e.preventDefault()}
|
|
139
139
|
onClick={() => t.run(exec)}
|
|
140
|
-
className="rounded border-0 bg-transparent px-2.5 py-0.5 text-
|
|
140
|
+
className="rounded border-0 bg-transparent px-2.5 py-0.5 text-label text-fg-muted transition-colors hover:bg-surface hover:text-fg"
|
|
141
141
|
>
|
|
142
142
|
{t.label}
|
|
143
143
|
</button>
|
|
@@ -145,7 +145,7 @@ export function RichTextEditor({
|
|
|
145
145
|
</div>
|
|
146
146
|
<div
|
|
147
147
|
ref={ref}
|
|
148
|
-
className="prose prose-sm max-w-none px-4 py-3.5 text-
|
|
148
|
+
className="prose prose-sm max-w-none px-4 py-3.5 text-small leading-relaxed text-fg outline-none empty:before:text-fg-subtle empty:before:content-[attr(data-placeholder)]"
|
|
149
149
|
style={{ minHeight: bodyMinHeight }}
|
|
150
150
|
contentEditable
|
|
151
151
|
suppressContentEditableWarning
|
|
@@ -155,8 +155,8 @@ export function RichTextEditor({
|
|
|
155
155
|
onPaste={onPaste}
|
|
156
156
|
/>
|
|
157
157
|
</div>
|
|
158
|
-
{description ? <span className="text-
|
|
159
|
-
{isInvalid && errorMessage ? <span className="text-
|
|
158
|
+
{description ? <span className="text-label text-fg-muted">{description}</span> : null}
|
|
159
|
+
{isInvalid && errorMessage ? <span className="text-label text-danger">{errorMessage}</span> : null}
|
|
160
160
|
</div>
|
|
161
161
|
)
|
|
162
162
|
}
|
|
@@ -44,7 +44,7 @@ export const SearchField = ({ label, description, placeholder, ...props }: Searc
|
|
|
44
44
|
<RACInput
|
|
45
45
|
placeholder={placeholder}
|
|
46
46
|
className={
|
|
47
|
-
'h-12 w-full rounded-lg border border-border bg-surface pl-10 pr-10 text-
|
|
47
|
+
'h-12 w-full rounded-lg border border-border bg-surface pl-10 pr-10 text-small text-fg outline-none transition-colors ' +
|
|
48
48
|
'placeholder:text-fg-muted data-[hovered]:border-fg-subtle ' +
|
|
49
49
|
'data-[focused]:border-brand-green data-[focused]:ring-2 data-[focused]:ring-ring ' +
|
|
50
50
|
'[&::-webkit-search-cancel-button]:hidden'
|
|
@@ -58,7 +58,7 @@ export const SearchField = ({ label, description, placeholder, ...props }: Searc
|
|
|
58
58
|
</RACButton>
|
|
59
59
|
</div>
|
|
60
60
|
{description ? (
|
|
61
|
-
<Text slot="description" className="text-
|
|
61
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
62
62
|
{description}
|
|
63
63
|
</Text>
|
|
64
64
|
) : null}
|
|
@@ -36,7 +36,7 @@ const SelectTrigger = uic(RACButton, {
|
|
|
36
36
|
// Error → 2px danger ring driven off the RACSelect root's `data-invalid`.
|
|
37
37
|
baseClass:
|
|
38
38
|
'flex h-12 w-full items-center justify-between gap-2.5 rounded-lg border border-border bg-surface px-4 ' +
|
|
39
|
-
'text-
|
|
39
|
+
'text-small text-fg outline-none transition-colors ' +
|
|
40
40
|
'data-[hovered]:border-fg-subtle ' +
|
|
41
41
|
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring ' +
|
|
42
42
|
'group-data-[invalid]:border-danger group-data-[invalid]:ring-2 group-data-[invalid]:ring-danger ' +
|
|
@@ -49,7 +49,7 @@ export const SelectItem = uic(ListBoxItem, {
|
|
|
49
49
|
// medium weight. We add a `surface-muted` focus background (gs highlights with
|
|
50
50
|
// weight only) so keyboard focus stays clearly visible on the cream content.
|
|
51
51
|
baseClass:
|
|
52
|
-
'flex cursor-pointer select-none items-center rounded-md px-3 py-2 text-
|
|
52
|
+
'flex cursor-pointer select-none items-center rounded-md px-3 py-2 text-small text-fg outline-none ' +
|
|
53
53
|
'data-[hovered]:bg-surface-muted data-[focused]:bg-surface-muted data-[selected]:font-medium ' +
|
|
54
54
|
'data-[disabled]:opacity-50 data-[disabled]:pointer-events-none',
|
|
55
55
|
}) as (props: ListBoxItemProps) => ReactNode
|
|
@@ -84,11 +84,11 @@ export const Select = <T extends object>({
|
|
|
84
84
|
</ListBox>
|
|
85
85
|
)
|
|
86
86
|
const desc = description ? (
|
|
87
|
-
<Text slot="description" className="text-
|
|
87
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
88
88
|
{description}
|
|
89
89
|
</Text>
|
|
90
90
|
) : null
|
|
91
|
-
const err = <FieldError className="text-
|
|
91
|
+
const err = <FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
92
92
|
|
|
93
93
|
return (
|
|
94
94
|
<RACSelect {...props} placeholder={placeholder} className="group flex flex-col gap-2">
|
|
@@ -37,7 +37,7 @@ export const Slider = <T extends number | number[]>({ label, hideValue, ...props
|
|
|
37
37
|
className="h-4 w-4 rounded-full border-2 border-fg bg-surface outline-none transition-transform data-[dragging]:scale-110 data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring"
|
|
38
38
|
>
|
|
39
39
|
{hideValue ? null : (
|
|
40
|
-
<span className="pointer-events-none absolute -top-6 left-1/2 -translate-x-1/2 whitespace-nowrap text-
|
|
40
|
+
<span className="pointer-events-none absolute -top-6 left-1/2 -translate-x-1/2 whitespace-nowrap text-label font-medium tabular-nums text-fg">
|
|
41
41
|
{state.getThumbValueLabel(i)}
|
|
42
42
|
</span>
|
|
43
43
|
)}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { CountUp } from './count-up'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* StatStack — a "production compass" infographic: a fanned deck of coloured rounded
|
|
6
|
+
* bars, each showing an animated count + label (e.g. 55 Progress · 36 Done · 26
|
|
7
|
+
* Review). Bars cascade with a left inset that shrinks top→bottom and overlap
|
|
8
|
+
* vertically, later bars in front. The counts animate up via <CountUp>. Sits inside a
|
|
9
|
+
* dark data tile. Presentational only.
|
|
10
|
+
*/
|
|
11
|
+
export type StatStackTone = 'neutral' | 'green' | 'mint' | 'yellow' | 'pink' | 'blue'
|
|
12
|
+
|
|
13
|
+
const TONE: Record<StatStackTone, string> = {
|
|
14
|
+
// Fixed decorative chip colours (they sit on a dark tile in both themes, so they
|
|
15
|
+
// do NOT theme-flip); dark text reads on all of them.
|
|
16
|
+
neutral: 'bg-[#eae7e0] text-fg',
|
|
17
|
+
green: 'bg-brand-green text-fg',
|
|
18
|
+
mint: 'bg-accent-mint text-fg',
|
|
19
|
+
yellow: 'bg-accent-yellow text-fg',
|
|
20
|
+
pink: 'bg-accent-pink text-fg',
|
|
21
|
+
blue: 'bg-accent-blue text-white',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface StatStackSegment {
|
|
25
|
+
value: number
|
|
26
|
+
label: ReactNode
|
|
27
|
+
tone: StatStackTone
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StatStackProps {
|
|
31
|
+
segments: StatStackSegment[]
|
|
32
|
+
className?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function StatStack({ segments, className }: StatStackProps) {
|
|
36
|
+
const n = segments.length
|
|
37
|
+
return (
|
|
38
|
+
<div className={['flex flex-col', className].filter(Boolean).join(' ')}>
|
|
39
|
+
{segments.map((s, i) => (
|
|
40
|
+
<div
|
|
41
|
+
key={i}
|
|
42
|
+
className={[
|
|
43
|
+
'flex items-baseline gap-2 rounded-lg px-5 py-4 shadow-sm',
|
|
44
|
+
TONE[s.tone],
|
|
45
|
+
].join(' ')}
|
|
46
|
+
style={{
|
|
47
|
+
// Cascade: most-inset at the top, flush-left at the bottom; overlap the
|
|
48
|
+
// bar above; later bars sit in front.
|
|
49
|
+
marginLeft: `${(n - 1 - i) * 1.75}rem`,
|
|
50
|
+
marginRight: `${i * 1.25}rem`,
|
|
51
|
+
marginTop: i === 0 ? 0 : '-1rem',
|
|
52
|
+
zIndex: i + 1,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<CountUp value={s.value} className="text-heading1 font-medium leading-none" />
|
|
56
|
+
<span className="text-heading3 font-medium leading-none">{s.label}</span>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -19,7 +19,7 @@ export type SwitchProps = Omit<RACSwitchProps, 'children'> & {
|
|
|
19
19
|
export const Switch = ({ children, ...props }: SwitchProps) => (
|
|
20
20
|
<RACSwitch
|
|
21
21
|
{...props}
|
|
22
|
-
className="group flex cursor-pointer select-none items-center gap-3 text-
|
|
22
|
+
className="group flex cursor-pointer select-none items-center gap-3 text-small text-fg data-[disabled]:cursor-not-allowed"
|
|
23
23
|
>
|
|
24
24
|
{/* gs token map: off track #eceae1 → surface-muted · on track #0d0d0d → fg ·
|
|
25
25
|
thumb #ffffff → surface. gs hovers the track at opacity .9 (not a border). */}
|
package/src/components/tabs.tsx
CHANGED
|
@@ -26,7 +26,7 @@ export const TabList = uic(RACTabList, {
|
|
|
26
26
|
export const Tab = uic(RACTab, {
|
|
27
27
|
displayName: 'Tab',
|
|
28
28
|
baseClass:
|
|
29
|
-
'cursor-pointer select-none border-b-2 border-transparent px-4 py-3 text-
|
|
29
|
+
'cursor-pointer select-none border-b-2 border-transparent px-4 py-3 text-small font-medium text-fg-muted ' +
|
|
30
30
|
'outline-none transition-colors ' +
|
|
31
31
|
'data-[hovered]:text-fg ' +
|
|
32
32
|
'data-[selected]:border-brand-primary data-[selected]:text-fg ' +
|
|
@@ -36,5 +36,5 @@ export const Tab = uic(RACTab, {
|
|
|
36
36
|
|
|
37
37
|
export const TabPanel = uic(RACTabPanel, {
|
|
38
38
|
displayName: 'TabPanel',
|
|
39
|
-
baseClass: 'py-4 text-
|
|
39
|
+
baseClass: 'py-4 text-small text-fg outline-none data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring',
|
|
40
40
|
}) as (props: TabPanelProps) => ReturnType<typeof RACTabPanel>
|
|
@@ -26,7 +26,7 @@ export const TagGroup = ({ label, description, children, ...props }: TagGroupPro
|
|
|
26
26
|
{label ? <Label className="text-heading5 font-medium text-fg">{label}</Label> : null}
|
|
27
27
|
<TagList className="flex flex-wrap gap-2 outline-none">{children}</TagList>
|
|
28
28
|
{description ? (
|
|
29
|
-
<Text slot="description" className="text-
|
|
29
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
30
30
|
{description}
|
|
31
31
|
</Text>
|
|
32
32
|
) : null}
|
|
@@ -40,7 +40,7 @@ export const Tag = ({ children, textValue, ...props }: RACTagProps) => (
|
|
|
40
40
|
// the tag's text — derive it from a string child for accessibility.
|
|
41
41
|
textValue={textValue ?? (typeof children === 'string' ? children : undefined)}
|
|
42
42
|
className={
|
|
43
|
-
'inline-flex cursor-default select-none items-center gap-1.5 rounded-full border border-border bg-surface-card px-3 py-1 text-
|
|
43
|
+
'inline-flex cursor-default select-none items-center gap-1.5 rounded-full border border-border bg-surface-card px-3 py-1 text-small text-fg outline-none transition-colors ' +
|
|
44
44
|
'data-[hovered]:border-fg-subtle ' +
|
|
45
45
|
'data-[selected]:border-fg data-[selected]:bg-surface-muted ' +
|
|
46
46
|
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring ' +
|
|
@@ -79,8 +79,8 @@ export function TaskApprovalModal({
|
|
|
79
79
|
onOpenChange={onOpenChange}
|
|
80
80
|
>
|
|
81
81
|
<div className="flex flex-col gap-3">
|
|
82
|
-
<p className="text-
|
|
83
|
-
<label htmlFor={noteId} className="text-
|
|
82
|
+
<p className="text-small text-fg-muted">{labels.body}</p>
|
|
83
|
+
<label htmlFor={noteId} className="text-small font-medium text-fg">
|
|
84
84
|
{labels.noteLabel}
|
|
85
85
|
</label>
|
|
86
86
|
<textarea
|
|
@@ -90,10 +90,10 @@ export function TaskApprovalModal({
|
|
|
90
90
|
maxLength={10_000}
|
|
91
91
|
rows={3}
|
|
92
92
|
placeholder={labels.notePlaceholder}
|
|
93
|
-
className="w-full rounded-md border border-border bg-surface px-3 py-2 text-
|
|
93
|
+
className="w-full rounded-md border border-border bg-surface px-3 py-2 text-small text-fg outline-none transition-colors placeholder:text-fg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
94
94
|
/>
|
|
95
95
|
{error ? (
|
|
96
|
-
<p data-testid="task-approval-error" role="alert" className="text-
|
|
96
|
+
<p data-testid="task-approval-error" role="alert" className="text-small text-danger">
|
|
97
97
|
{error}
|
|
98
98
|
</p>
|
|
99
99
|
) : null}
|
package/src/components/text.tsx
CHANGED
|
@@ -27,15 +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-label',
|
|
31
31
|
compact: 'text-compact',
|
|
32
|
+
small: 'text-small',
|
|
32
33
|
callout: 'text-callout',
|
|
33
34
|
body: 'text-body',
|
|
34
|
-
subtitle: 'text-
|
|
35
|
-
title: 'text-
|
|
36
|
-
headline: 'text-
|
|
35
|
+
subtitle: 'text-subtitle',
|
|
36
|
+
title: 'text-title',
|
|
37
|
+
headline: 'text-headline',
|
|
37
38
|
display: 'text-display',
|
|
38
|
-
'display-xl': 'text-display-xl',
|
|
39
39
|
},
|
|
40
40
|
// NC Fontina caps at medium (500) as the heaviest weight in the system, so
|
|
41
41
|
// `semibold` and `bold` are kept as API-compatible aliases that render
|
|
@@ -27,7 +27,7 @@ const StyledTextArea = uic(RACTextArea, {
|
|
|
27
27
|
// fg-subtle · focus #75e7b8 → brand-green · error → danger. min-h-[120px] is a
|
|
28
28
|
// control dimension (not a design token) — gs uses a literal 120px here too.
|
|
29
29
|
baseClass:
|
|
30
|
-
'min-h-[120px] w-full resize-y rounded-lg border border-border bg-surface px-4 py-3 text-
|
|
30
|
+
'min-h-[120px] w-full resize-y rounded-lg border border-border bg-surface px-4 py-3 text-small text-fg ' +
|
|
31
31
|
'outline-none transition-colors duration-200 placeholder:text-fg-muted ' +
|
|
32
32
|
'data-[hovered]:border-fg-subtle ' +
|
|
33
33
|
'data-[focused]:border-brand-green data-[focused]:ring-2 data-[focused]:ring-ring ' +
|
|
@@ -49,13 +49,13 @@ export type TextareaProps = TextFieldProps & {
|
|
|
49
49
|
|
|
50
50
|
export const Textarea = ({ label, description, errorMessage, placeholder, rows, ...props }: TextareaProps) => (
|
|
51
51
|
<TextField {...props} className="flex w-full flex-col gap-2">
|
|
52
|
-
<Label className="text-
|
|
52
|
+
<Label className="text-small font-medium text-fg">{label}</Label>
|
|
53
53
|
<StyledTextArea placeholder={placeholder} rows={rows} />
|
|
54
54
|
{description ? (
|
|
55
|
-
<Text slot="description" className="text-
|
|
55
|
+
<Text slot="description" className="text-label text-fg-muted">
|
|
56
56
|
{description}
|
|
57
57
|
</Text>
|
|
58
58
|
) : null}
|
|
59
|
-
<FieldError className="text-
|
|
59
|
+
<FieldError className="text-label text-danger">{errorMessage}</FieldError>
|
|
60
60
|
</TextField>
|
|
61
61
|
)
|
package/src/index.ts
CHANGED
|
@@ -52,8 +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/count-up";
|
|
55
56
|
export * from "./components/cta-pill";
|
|
56
57
|
export * from "./components/feature-tile";
|
|
58
|
+
export * from "./components/stat-stack";
|
|
57
59
|
export * from "./components/icons";
|
|
58
60
|
export * from "./components/subtle";
|
|
59
61
|
export * from "./components/stats-card";
|
|
@@ -51,9 +51,9 @@ export type PersistentPageShellProps = {
|
|
|
51
51
|
title?: string
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const defaultLoadingSlot = <div className="text-
|
|
55
|
-
const defaultEmptySlot = <div className="text-
|
|
56
|
-
const defaultErrorSlot = <div className="text-
|
|
54
|
+
const defaultLoadingSlot = <div className="text-small text-fg-muted">Loading…</div>
|
|
55
|
+
const defaultEmptySlot = <div className="text-small text-fg-muted">No content available.</div>
|
|
56
|
+
const defaultErrorSlot = <div className="text-small text-danger">Failed to load content.</div>
|
|
57
57
|
|
|
58
58
|
const Header = uic('header', {
|
|
59
59
|
displayName: 'PersistentPageShell.Header',
|
package/src/layout/topbar.tsx
CHANGED
|
@@ -138,11 +138,11 @@ export const WorkspaceSwitcher = ({
|
|
|
138
138
|
...props
|
|
139
139
|
}: WorkspaceSwitcherProps) => (
|
|
140
140
|
<RACComboBox {...props} items={workspaces} className="flex flex-col gap-1">
|
|
141
|
-
<Label className={hideLabel ? 'sr-only' : 'text-
|
|
141
|
+
<Label className={hideLabel ? 'sr-only' : 'text-small font-medium text-fg'}>{label}</Label>
|
|
142
142
|
<div className="flex items-center">
|
|
143
143
|
<RACInput
|
|
144
144
|
className={
|
|
145
|
-
'h-9 w-48 rounded-md border border-border bg-surface px-3 text-
|
|
145
|
+
'h-9 w-48 rounded-md border border-border bg-surface px-3 text-small text-fg outline-none ' +
|
|
146
146
|
'data-[focused]:ring-2 data-[focused]:ring-ring'
|
|
147
147
|
}
|
|
148
148
|
/>
|
|
@@ -162,7 +162,7 @@ export const WorkspaceSwitcher = ({
|
|
|
162
162
|
const WorkspaceSwitcherItem = uic(ListBoxItem, {
|
|
163
163
|
displayName: 'WorkspaceSwitcher.Item',
|
|
164
164
|
baseClass:
|
|
165
|
-
'flex cursor-pointer select-none items-center rounded-sm px-3 py-2 text-
|
|
165
|
+
'flex cursor-pointer select-none items-center rounded-sm px-3 py-2 text-small text-fg outline-none ' +
|
|
166
166
|
'data-[focused]:bg-surface-muted data-[selected]:font-medium',
|
|
167
167
|
}) as (props: ListBoxItemProps) => ReactNode
|
|
168
168
|
|
|
@@ -190,7 +190,7 @@ export const UserMenu = ({ trigger, triggerLabel, onAction, children }: UserMenu
|
|
|
190
190
|
<RACButton
|
|
191
191
|
aria-label={triggerLabel}
|
|
192
192
|
className={
|
|
193
|
-
'flex h-9 items-center gap-2 rounded-md px-2 text-
|
|
193
|
+
'flex h-9 items-center gap-2 rounded-md px-2 text-small font-medium text-fg outline-none ' +
|
|
194
194
|
'transition-colors hover:bg-surface-muted ' +
|
|
195
195
|
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring'
|
|
196
196
|
}
|
|
@@ -208,7 +208,7 @@ export const UserMenu = ({ trigger, triggerLabel, onAction, children }: UserMenu
|
|
|
208
208
|
export const UserMenuItem = uic(RACMenuItem, {
|
|
209
209
|
displayName: 'UserMenu.Item',
|
|
210
210
|
baseClass:
|
|
211
|
-
'flex cursor-pointer select-none items-center rounded-sm px-3 py-2 text-
|
|
211
|
+
'flex cursor-pointer select-none items-center rounded-sm px-3 py-2 text-small text-fg outline-none ' +
|
|
212
212
|
'data-[focused]:bg-surface-muted data-[disabled]:opacity-50 data-[disabled]:pointer-events-none',
|
|
213
213
|
}) as (props: MenuItemProps) => ReactNode
|
|
214
214
|
|