@podoba/react 0.0.34 → 0.0.36
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/asset-masonry-grid.examples.tsx +9 -0
- package/src/components/asset-masonry-grid.tsx +62 -0
- package/src/components/asset-selection-surface.examples.tsx +33 -0
- package/src/components/asset-selection-surface.tsx +184 -0
- package/src/components/brand-page-header.tsx +60 -29
- package/src/components/button.tsx +27 -4
- package/src/components/combobox.tsx +1 -1
- package/src/components/compact-action-button.examples.tsx +7 -0
- package/src/components/compact-action-button.tsx +21 -0
- package/src/components/compact-settings-dialog.examples.tsx +15 -0
- package/src/components/compact-settings-dialog.tsx +50 -0
- package/src/components/context-action-glyph.examples.tsx +9 -0
- package/src/components/context-action-glyph.tsx +61 -0
- package/src/components/context-menu.tsx +191 -107
- package/src/components/context-search-panel.examples.tsx +9 -0
- package/src/components/context-search-panel.tsx +50 -0
- package/src/components/csv-binding-presentation.examples.tsx +14 -0
- package/src/components/csv-binding-presentation.tsx +40 -0
- package/src/components/dashboard-grid.tsx +1 -1
- package/src/components/date-picker.tsx +5 -3
- package/src/components/date-selection-calendar.examples.tsx +8 -0
- package/src/components/date-selection-calendar.tsx +47 -0
- package/src/components/delivery/send-to-print-modal.tsx +351 -84
- package/src/components/dialog-action-button.examples.tsx +7 -0
- package/src/components/dialog-action-button.tsx +22 -0
- package/src/components/dialog.tsx +32 -12
- package/src/components/document-upload-panel.examples.tsx +12 -0
- package/src/components/document-upload-panel.tsx +48 -0
- package/src/components/dropdown-menu.tsx +5 -3
- package/src/components/empty-panel-action.examples.tsx +7 -0
- package/src/components/empty-panel-action.tsx +8 -0
- package/src/components/field-appearance.ts +14 -0
- package/src/components/focus-field.tsx +3 -3
- package/src/components/icons.tsx +30 -0
- package/src/components/input.examples.tsx +8 -0
- package/src/components/input.tsx +13 -5
- package/src/components/media-asset-panel.examples.tsx +11 -0
- package/src/components/media-asset-panel.tsx +58 -0
- package/src/components/media-gallery.examples.tsx +12 -0
- package/src/components/media-gallery.tsx +53 -0
- package/src/components/media-settings-dialog.examples.tsx +21 -0
- package/src/components/media-settings-dialog.tsx +83 -0
- package/src/components/preview-info-card.examples.tsx +18 -0
- package/src/components/preview-info-card.tsx +27 -0
- package/src/components/reload-icon.examples.tsx +7 -0
- package/src/components/reload-icon.tsx +6 -0
- package/src/components/request-changes-modal.examples.tsx +26 -0
- package/src/components/request-changes-modal.tsx +36 -19
- package/src/components/rich-text-editor.tsx +1 -1
- package/src/components/select.examples.tsx +17 -0
- package/src/components/select.tsx +78 -22
- package/src/components/settings-dialog-surface.examples.tsx +44 -0
- package/src/components/settings-dialog-surface.tsx +240 -0
- package/src/components/subtle.tsx +64 -8
- package/src/components/table.examples.tsx +9 -0
- package/src/components/table.tsx +29 -12
- package/src/components/template-catalog-card.examples.tsx +25 -0
- package/src/components/template-catalog-card.tsx +179 -0
- package/src/components/text.tsx +14 -1
- package/src/components/textarea.examples.tsx +8 -0
- package/src/components/textarea.tsx +16 -6
- package/src/components/tile.tsx +16 -17
- package/src/editor/block-editor.tsx +56 -14
- package/src/index.ts +19 -0
- package/src/layout/app-shell.tsx +13 -9
- package/src/layout/topbar.tsx +9 -12
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
+
import { Button as AriaButton } from 'react-aria-components'
|
|
3
|
+
import { uic } from '../utils/uic'
|
|
4
|
+
import { Tile } from './tile'
|
|
5
|
+
|
|
6
|
+
export const TEMPLATE_CATALOG_SELECTIONS = ['available', 'selected'] as const
|
|
7
|
+
export type TemplateCatalogSelection = (typeof TEMPLATE_CATALOG_SELECTIONS)[number]
|
|
8
|
+
|
|
9
|
+
/** Legacy StatsCard label-1 value, distinct from the Tile heading ramp. */
|
|
10
|
+
export const CatalogMetricValue = uic('span', {
|
|
11
|
+
displayName: 'CatalogMetricValue',
|
|
12
|
+
baseClass: 'block w-full text-center text-display-large font-medium leading-(--line-height-display-large) tracking-wider',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const CATALOG_METRIC_FOOTERS = ['present', 'missing'] as const
|
|
16
|
+
export type CatalogMetricFooter = (typeof CATALOG_METRIC_FOOTERS)[number]
|
|
17
|
+
const MetricContent = uic('div', {
|
|
18
|
+
displayName: 'CatalogMetricContent',
|
|
19
|
+
baseClass: 'flex min-h-0 w-full flex-1 flex-col items-center justify-center text-center',
|
|
20
|
+
variants: { footer: { present: '', missing: 'pb-12' } },
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
/** Compact source statistics: preserve the centered content band without a tail. */
|
|
24
|
+
export function CatalogMetricCard({ title, value, footer, onPress, 'aria-label': ariaLabel }: {
|
|
25
|
+
title: string; value: ReactNode; footer?: string; onPress?: () => void; 'aria-label'?: string
|
|
26
|
+
}) {
|
|
27
|
+
return <Tile theme="light" eyebrow={title} footer={footer || undefined} onPress={onPress}
|
|
28
|
+
aria-label={ariaLabel} className="h-52 min-h-52 max-h-52">
|
|
29
|
+
<MetricContent data-slot="catalog-metric-content" footer={footer ? 'present' : 'missing'}>
|
|
30
|
+
<CatalogMetricValue>{value}</CatalogMetricValue>
|
|
31
|
+
</MetricContent>
|
|
32
|
+
</Tile>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SummaryCard = uic('article', { displayName: 'CatalogSummaryCard', baseClass: 'flex h-95 w-full min-w-0 flex-col gap-6 overflow-hidden rounded-lg bg-surface-card p-4 text-fg' })
|
|
36
|
+
export const CatalogSummaryGrid = uic('div', { displayName: 'CatalogSummaryGrid', baseClass: 'grid w-full min-w-0 grid-cols-1 gap-x-2 gap-y-4 overflow-x-hidden min-[768px]:grid-cols-3 min-[768px]:gap-x-3' })
|
|
37
|
+
const SummaryHead = uic('div', { displayName: 'CatalogSummaryHead', baseClass: 'flex w-full min-w-0 items-center justify-between gap-3' })
|
|
38
|
+
export const CatalogSummaryBadge = uic('span', { displayName: 'CatalogSummaryBadge', baseClass: 'ml-3 flex h-6 shrink-0 items-center rounded-2xl px-2.5 text-micro font-medium leading-5', style: { fontFeatureSettings: '"liga" off, "clig" off' }, variants: { color: { green: 'bg-brand-green text-fg-on-brand', grey: 'bg-surface-muted text-(--color-neutral-600)' } }, defaultVariants: { color: 'grey' } })
|
|
39
|
+
const SummaryTitle = uic('h2', { displayName: 'CatalogSummaryTitle', baseClass: 'm-0 min-w-0 truncate text-heading5 font-medium' })
|
|
40
|
+
const SummaryTail = uic('div', { displayName: 'CatalogSummaryTail', baseClass: 'mt-auto flex flex-col gap-1 text-small font-normal text-fg-workflow-muted' })
|
|
41
|
+
/** Display-only catalog tile. No synthetic imagery when a preview is absent. */
|
|
42
|
+
export function CatalogSummaryCard({ title, badge, description, details }: { title: string; badge: ReactNode; description?: string; details: ReactNode }) {
|
|
43
|
+
return <SummaryCard><SummaryHead><SummaryTitle>{title}</SummaryTitle><span className="shrink-0">{badge}</span></SummaryHead><SummaryTail>{description ? <p className="m-0">{description}</p> : null}<span>{details}</span></SummaryTail></SummaryCard>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Compact launcher card. Missing artwork is not replaced by synthetic imagery. */
|
|
47
|
+
export function CatalogLaunchCard({ title, description, details, action }: { title: string; description?: string; details: ReactNode; action: ReactNode }) {
|
|
48
|
+
return <SummaryCard data-testid="scenario-launch-card" className="relative h-70 min-h-70 max-h-70 transition-colors duration-200 focus-within:ring-2 focus-within:ring-ring">
|
|
49
|
+
<SummaryHead><SummaryTitle asChild><h3>{title}</h3></SummaryTitle></SummaryHead>
|
|
50
|
+
<SummaryTail>{description ? <p className="m-0">{description}</p> : null}<span>{details}</span></SummaryTail>
|
|
51
|
+
{action}
|
|
52
|
+
</SummaryCard>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MetadataSection = uic('section', { displayName: 'CatalogMetadataSection', baseClass: 'm-0 w-full' })
|
|
56
|
+
/** Source metadata layout: full-width identity, paired attributes, wrapping IDs. */
|
|
57
|
+
export function CatalogMetadataSummary({ title, ariaLabel, items }: {
|
|
58
|
+
title: string; ariaLabel: string; items: { label: string; value: ReactNode; fullWidth?: boolean }[]
|
|
59
|
+
}) {
|
|
60
|
+
let column = 0
|
|
61
|
+
return <MetadataSection aria-label={ariaLabel}>
|
|
62
|
+
<h2 className="mb-3 mt-0 text-heading3 font-medium text-fg">{title}</h2>
|
|
63
|
+
<dl className="m-0 grid w-full grid-cols-1 border-t border-border min-[641px]:grid-cols-2">
|
|
64
|
+
{items.map(item => {
|
|
65
|
+
const second = !item.fullWidth && column % 2 === 1
|
|
66
|
+
column = item.fullWidth ? 0 : column + 1
|
|
67
|
+
return <div key={item.label} className={`flex min-w-0 flex-col gap-1 border-b border-border py-3 ${item.fullWidth ? 'col-span-full' : ''} ${second ? 'min-[641px]:border-l min-[641px]:pl-3' : ''}`}>
|
|
68
|
+
{/* #25: the term of a definition list is meaningful copy — AA `fg-muted`. */}
|
|
69
|
+
<dt className="m-0 text-body font-normal text-fg-muted">{item.label}</dt>
|
|
70
|
+
<dd className="m-0 text-body font-normal text-fg [overflow-wrap:anywhere]">{item.value}</dd>
|
|
71
|
+
</div>
|
|
72
|
+
})}
|
|
73
|
+
</dl>
|
|
74
|
+
</MetadataSection>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const TEMPLATE_REFERENCE_VIEWS = ['preview_grid', 'compact_list'] as const
|
|
78
|
+
export type TemplateReferenceView = (typeof TEMPLATE_REFERENCE_VIEWS)[number]
|
|
79
|
+
export const TemplateReferenceGrid = uic('div', {
|
|
80
|
+
displayName: 'TemplateReferenceGrid',
|
|
81
|
+
baseClass: 'grid w-full grid-cols-1 gap-3',
|
|
82
|
+
variants: { columns: { 1: 'grid-cols-1', 2: 'min-[761px]:grid-cols-2', 3: 'min-[761px]:grid-cols-3' } },
|
|
83
|
+
defaultVariants: { columns: 1 },
|
|
84
|
+
})
|
|
85
|
+
export const TemplateReferenceList = uic('div', {
|
|
86
|
+
displayName: 'TemplateReferenceList', baseClass: 'flex w-full flex-col gap-2',
|
|
87
|
+
})
|
|
88
|
+
const ReferenceButton = uic(AriaButton, {
|
|
89
|
+
displayName: 'TemplateReferenceButton',
|
|
90
|
+
baseClass: 'w-full cursor-pointer border-0 p-0 text-start outline-none disabled:cursor-default data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[focus-visible]:ring-offset-2',
|
|
91
|
+
variants: { view: {
|
|
92
|
+
preview_grid: 'flex aspect-square items-center justify-center overflow-hidden rounded-lg bg-surface-card',
|
|
93
|
+
compact_list: 'grid min-h-14 grid-cols-[3rem_minmax(0,1fr)] items-center gap-3 border-b border-border bg-transparent last:border-b-0',
|
|
94
|
+
} },
|
|
95
|
+
})
|
|
96
|
+
/** Read-only browsing surfaces: activation opens a preview, never selects it. */
|
|
97
|
+
export function TemplateReferenceItem({ title, meta, preview, view, isDisabled, onOpen, templateId }: {
|
|
98
|
+
title: string; meta: string; preview: ReactNode; view: TemplateReferenceView
|
|
99
|
+
isDisabled?: boolean; onOpen: () => void; templateId: string
|
|
100
|
+
}) {
|
|
101
|
+
return <ReferenceButton type="button" view={view} isDisabled={isDisabled} onPress={onOpen} aria-label={title} data-template-id={templateId}>
|
|
102
|
+
<span aria-hidden="true" className={view === 'compact_list' ? 'flex size-12 items-center justify-center overflow-hidden rounded-sm bg-surface-card [&>div]:w-full' : 'flex h-full w-full items-center justify-center [&>div]:w-full'}>{preview}</span>
|
|
103
|
+
{view === 'compact_list' ? <span className="flex min-w-0 flex-col gap-1"><span className="truncate text-small font-medium text-fg">{title}</span><span className="truncate text-label font-medium text-fg-muted">{meta}</span></span> : null}
|
|
104
|
+
</ReferenceButton>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const TemplateCatalogGrid = uic('div', {
|
|
108
|
+
displayName: 'TemplateCatalogGrid',
|
|
109
|
+
baseClass: 'grid w-full min-w-0 grid-cols-1 items-stretch gap-2 p-0 md:grid-cols-2 md:gap-3 lg:grid-cols-3',
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const Card = uic(AriaButton, {
|
|
113
|
+
displayName: 'TemplateCatalogCardSurface',
|
|
114
|
+
baseClass: 'group box-border flex h-(--template-catalog-height) min-w-0 w-full cursor-pointer flex-col gap-6 overflow-hidden rounded-lg border-0 p-4 text-start outline-none transition-colors duration-200 motion-reduce:transition-none hover:bg-brand-green hover:text-fg-on-brand data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[focus-visible]:ring-offset-2',
|
|
115
|
+
variants: {
|
|
116
|
+
selection: {
|
|
117
|
+
available: 'bg-surface-card text-fg',
|
|
118
|
+
selected: 'bg-brand-green text-fg-on-brand',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
defaultVariants: { selection: 'available' },
|
|
122
|
+
})
|
|
123
|
+
const Title = uic('span', {
|
|
124
|
+
displayName: 'TemplateCatalogCardTitle',
|
|
125
|
+
baseClass: 'block min-h-5 min-w-0 shrink-0 text-body font-medium leading-5',
|
|
126
|
+
})
|
|
127
|
+
const Preview = uic('span', {
|
|
128
|
+
displayName: 'TemplateCatalogCardPreview',
|
|
129
|
+
baseClass: 'box-border flex min-h-0 w-full flex-1 items-center justify-center overflow-hidden rounded-md bg-transparent p-3 [&>div]:w-full',
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const geometry: CSSProperties & { '--template-catalog-height': string } = {
|
|
133
|
+
'--template-catalog-height': 'clamp(calc(var(--spacing) * 64), 30vw, calc(var(--spacing) * 76))',
|
|
134
|
+
letterSpacing: 0,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const TemplateCatalogBrowseGrid = uic('div', {
|
|
138
|
+
displayName: 'TemplateCatalogBrowseGrid',
|
|
139
|
+
baseClass: 'grid w-full min-w-0 grid-cols-1 items-stretch gap-2 p-0 md:grid-cols-2 md:gap-3 lg:grid-cols-4',
|
|
140
|
+
})
|
|
141
|
+
const BrowseCard = uic('div', {
|
|
142
|
+
displayName: 'TemplateCatalogBrowseSurface',
|
|
143
|
+
baseClass: 'box-border flex h-(--template-catalog-height) min-w-0 w-full cursor-default flex-col gap-6 overflow-hidden rounded-lg border-0 bg-surface-card p-4 text-start text-fg outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
144
|
+
})
|
|
145
|
+
/** Legacy readOnly catalog: no activation, hover shadow or extra card CTA. */
|
|
146
|
+
export function TemplateCatalogBrowseCard({ title, preview, templateId, id, testId }: {
|
|
147
|
+
title: string; preview: ReactNode; templateId: string; id?: string; testId?: string
|
|
148
|
+
}) {
|
|
149
|
+
return <BrowseCard role="group" tabIndex={0} aria-label={title} id={id} data-catalog-template-id={templateId} data-testid={testId} style={geometry}>
|
|
150
|
+
<Title>{title}</Title><Preview aria-hidden="true" className="pointer-events-none">{preview}</Preview>
|
|
151
|
+
</BrowseCard>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Selection-only catalog tile. No badge, selected ring, nested action, or artwork
|
|
155
|
+
* rounding is added. A locked assignment stays focusable and selected, just as in
|
|
156
|
+
* the original catalog; aria-disabled and the press guard prevent its removal.
|
|
157
|
+
* The caller owns selection and supplies a non-interactive, contain-fit preview.
|
|
158
|
+
*/
|
|
159
|
+
export function TemplateCatalogCard({ title, preview, isSelected = false, isSelectionLocked = false,
|
|
160
|
+
onToggle, onOpen, templateId, testId,
|
|
161
|
+
}: {
|
|
162
|
+
title: string
|
|
163
|
+
preview: ReactNode
|
|
164
|
+
isSelected?: boolean
|
|
165
|
+
isSelectionLocked?: boolean
|
|
166
|
+
onToggle?: () => void
|
|
167
|
+
/** Browsing mode opens a preview, without announcing a toggle state. */
|
|
168
|
+
onOpen?: () => void
|
|
169
|
+
templateId?: string
|
|
170
|
+
testId?: string
|
|
171
|
+
}) {
|
|
172
|
+
return <Card type="button" selection={isSelected ? 'selected' : 'available'}
|
|
173
|
+
aria-label={title} aria-pressed={onOpen ? undefined : isSelected} aria-disabled={isSelectionLocked}
|
|
174
|
+
data-template-id={templateId} data-testid={testId} style={geometry}
|
|
175
|
+
onPress={() => { if (!isSelectionLocked) { if (onOpen) onOpen(); else onToggle?.() } }}>
|
|
176
|
+
<Title>{title}</Title>
|
|
177
|
+
<Preview aria-hidden="true">{preview}</Preview>
|
|
178
|
+
</Card>
|
|
179
|
+
}
|
package/src/components/text.tsx
CHANGED
|
@@ -68,11 +68,24 @@ export const Text = uic('span', {
|
|
|
68
68
|
semibold: 'font-medium',
|
|
69
69
|
bold: 'font-medium',
|
|
70
70
|
},
|
|
71
|
+
// `fg-subtle` (#b3b3b3) is 2.10:1 on `surface` — below even the 3:1
|
|
72
|
+
// large-text floor — so it can never carry meaningful copy. `subtle` is
|
|
73
|
+
// therefore an AA-safe alias of `muted` (same approach as the semibold /
|
|
74
|
+
// bold aliases above: the key survives so consumers don't break), and the
|
|
75
|
+
// light grey moved behind the explicit `decorative` key. See <Subtle>.
|
|
76
|
+
// NOTE: `default`/`muted` are the READING-surface inks. Surfaces that carry
|
|
77
|
+
// their own ink need the matching tone — `fg-muted` is 2.60:1 on
|
|
78
|
+
// `surface-inverted` and 3.60–4.01:1 on the fixed brand fills. Pair
|
|
79
|
+
// `inverted` there, `on-brand` on brand-green / accent-yellow /
|
|
80
|
+
// brand-secondary. (`Subtle` has de-emphasised /60 · /70 variants of the
|
|
81
|
+
// same two for a softened run.)
|
|
71
82
|
tone: {
|
|
72
83
|
default: 'text-fg',
|
|
73
84
|
muted: 'text-fg-muted',
|
|
74
|
-
subtle: 'text-fg-
|
|
85
|
+
subtle: 'text-fg-muted',
|
|
86
|
+
decorative: 'text-fg-subtle',
|
|
75
87
|
inverted: 'text-fg-inverted',
|
|
88
|
+
'on-brand': 'text-fg-on-brand',
|
|
76
89
|
},
|
|
77
90
|
},
|
|
78
91
|
defaultVariants: { size: 'body', weight: 'regular', tone: 'default' },
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Textarea } from './textarea'
|
|
2
|
+
|
|
3
|
+
export const examples = {
|
|
4
|
+
default: () => <Textarea label="Project name" placeholder="Annual report" />,
|
|
5
|
+
variants: () => <><Textarea label="Outlined" /><Textarea appearance="filled" label="Filled" placeholder="Annual report" /></>,
|
|
6
|
+
states: () => <><Textarea appearance="filled" label="Disabled" defaultValue="Saved content" isDisabled /><Textarea appearance="filled" label="Invalid" isInvalid errorMessage="Check the value." /></>,
|
|
7
|
+
}
|
|
8
|
+
export const meta = { category: 'Form', description: 'Labelled textarea with outlined and Manager-style filled appearances, disabled/error states and keyboard focus.' }
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type TextFieldProps,
|
|
9
9
|
} from 'react-aria-components'
|
|
10
10
|
import { uic } from '../utils/uic'
|
|
11
|
+
import { filledFieldClasses, type FieldAppearance } from './field-appearance'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Textarea — labelled multi-line text field.
|
|
@@ -26,16 +27,23 @@ const StyledTextArea = uic(RACTextArea, {
|
|
|
26
27
|
// (surface), disabled = muted cream. border #eceae1 → border · hover #aba89c →
|
|
27
28
|
// fg-subtle · focus #75e7b8 → brand-green · error → danger. min-h-[120px] is a
|
|
28
29
|
// control dimension (not a design token) — gs uses a literal 120px here too.
|
|
29
|
-
baseClass:
|
|
30
|
-
|
|
30
|
+
baseClass: 'min-h-30 w-full resize-y rounded-lg px-4 py-3',
|
|
31
|
+
variants: {
|
|
32
|
+
appearance: {
|
|
33
|
+
filled: filledFieldClasses,
|
|
34
|
+
outlined: 'border border-border bg-surface text-small text-fg ' +
|
|
31
35
|
'outline-none transition-colors duration-200 placeholder:text-fg-muted ' +
|
|
32
36
|
'data-[hovered]:border-fg-subtle ' +
|
|
33
37
|
'data-[focused]:border-brand-green data-[focused]:ring-2 data-[focused]:ring-ring ' +
|
|
34
38
|
'data-[invalid]:border-danger data-[invalid]:ring-danger ' +
|
|
35
39
|
'data-[disabled]:bg-surface-muted data-[disabled]:opacity-60 data-[disabled]:pointer-events-none',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultVariants: { appearance: 'outlined' },
|
|
36
43
|
})
|
|
37
44
|
|
|
38
45
|
export type TextareaProps = TextFieldProps & {
|
|
46
|
+
appearance?: FieldAppearance
|
|
39
47
|
/** Visible field label (required for accessibility). */
|
|
40
48
|
label: ReactNode
|
|
41
49
|
/** Helper text rendered under the field. */
|
|
@@ -45,12 +53,14 @@ export type TextareaProps = TextFieldProps & {
|
|
|
45
53
|
placeholder?: string
|
|
46
54
|
/** Initial visible row count (the field still grows / resizes vertically). */
|
|
47
55
|
rows?: number
|
|
56
|
+
/** Optional class for the inner native textarea, used by source-specific modal layouts. */
|
|
57
|
+
textAreaClassName?: string
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
export const Textarea = ({ label, description, errorMessage, placeholder, rows, ...props }: TextareaProps) => (
|
|
51
|
-
<TextField {...props} className=
|
|
52
|
-
<Label className=
|
|
53
|
-
<StyledTextArea placeholder={placeholder} rows={rows} />
|
|
60
|
+
export const Textarea = ({ label, description, errorMessage, placeholder, rows, textAreaClassName, appearance = 'outlined', ...props }: TextareaProps) => (
|
|
61
|
+
<TextField {...props} className={`flex w-full flex-col ${appearance === 'filled' ? 'gap-3' : 'gap-2'}`}>
|
|
62
|
+
<Label className={`${appearance === 'filled' ? 'text-panel-heading' : 'text-small'} font-medium text-fg`}>{label}</Label>
|
|
63
|
+
<StyledTextArea className={textAreaClassName} placeholder={placeholder} rows={rows} appearance={appearance} />
|
|
54
64
|
{description ? (
|
|
55
65
|
<Text slot="description" className="text-label text-fg-muted">
|
|
56
66
|
{description}
|
package/src/components/tile.tsx
CHANGED
|
@@ -29,16 +29,14 @@ export type TileTheme = 'light' | 'dark' | 'teal' | 'yellow'
|
|
|
29
29
|
export type TileAlign = 'top' | 'center'
|
|
30
30
|
|
|
31
31
|
const THEME: Record<TileTheme, string> = {
|
|
32
|
-
// Light tiles carry a visible hairline; dark/colored use a transparent border so
|
|
33
|
-
// every tile keeps the same box size (no 1px shift between themes).
|
|
34
32
|
// Every surface pairs with ITS semantic foreground (never raw white / bare fg):
|
|
35
33
|
// `surface-inverted` flips with the theme so it needs the flipping `fg-inverted`;
|
|
36
34
|
// teal/yellow are FIXED light surfaces so they need the stable `fg-on-brand` ink
|
|
37
35
|
// (theme-flipping `fg` went white-on-pastel in a dark shell).
|
|
38
|
-
light: '
|
|
39
|
-
dark: '
|
|
40
|
-
teal: '
|
|
41
|
-
yellow: '
|
|
36
|
+
light: 'bg-surface-card text-fg',
|
|
37
|
+
dark: 'bg-surface-inverted text-fg-inverted',
|
|
38
|
+
teal: 'bg-brand-green text-fg-on-brand',
|
|
39
|
+
yellow: 'bg-accent-yellow text-fg-on-brand',
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
// The shared inner for EVERY tile (gs `DashboardContentCard`): 16px padding
|
|
@@ -46,10 +44,8 @@ const THEME: Record<TileTheme, string> = {
|
|
|
46
44
|
// grid cell; `min-h` floors it.
|
|
47
45
|
const baseSurface = 'flex h-full min-h-[200px] flex-col gap-6 overflow-hidden rounded-lg p-4'
|
|
48
46
|
|
|
49
|
-
/**
|
|
50
|
-
const interactive =
|
|
51
|
-
'no-underline outline-none transition-[transform,box-shadow] duration-150 ' +
|
|
52
|
-
'hover:-translate-y-1 hover:shadow-lg focus-visible:ring-2 focus-visible:ring-ring'
|
|
47
|
+
/** Focus treatment for interactive tiles; generic tiles do not lift or cast a shadow. */
|
|
48
|
+
const interactive = 'no-underline outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
|
53
49
|
|
|
54
50
|
export type TileProps = {
|
|
55
51
|
/** Surface theme (gs's Figma palette). */
|
|
@@ -105,10 +101,12 @@ function titleTone(theme: TileTheme, subtle: boolean): string {
|
|
|
105
101
|
}
|
|
106
102
|
|
|
107
103
|
/**
|
|
108
|
-
* Muted supporting text / footer tone per surface. `fg-muted` is tuned for
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
104
|
+
* Muted supporting text / footer tone per surface. `fg-muted` is tuned for the READING
|
|
105
|
+
* surfaces — after #19 it clears AA on all of them, white included, but the binding one
|
|
106
|
+
* is `surface-muted`/`border` #eceae1, not white. On the colored and inverted surfaces
|
|
107
|
+
* it still drops below AA (2.60:1 on `surface-inverted`), so those blend their own
|
|
108
|
+
* paired ink instead (/60 · /70 keep ≥4.5:1 in both themes — verified by the contrast
|
|
109
|
+
* test in @podoba/tokens).
|
|
112
110
|
*/
|
|
113
111
|
function mutedTone(theme: TileTheme): string {
|
|
114
112
|
if (theme === 'dark') return 'text-fg-inverted/60'
|
|
@@ -134,7 +132,7 @@ function TileBands({
|
|
|
134
132
|
> & { theme: TileTheme }) {
|
|
135
133
|
const titleNode =
|
|
136
134
|
title != null ? (
|
|
137
|
-
<p className={`${tileStatClass} ${
|
|
135
|
+
<p className={`${tileStatClass} ${titleTone(theme, subtleTitle)}`}>
|
|
138
136
|
{title}
|
|
139
137
|
</p>
|
|
140
138
|
) : null
|
|
@@ -386,8 +384,9 @@ const BADGE: Record<BadgeColor, string> = {
|
|
|
386
384
|
// brand green for every green tag (New, Active, Published), matching gs 1:1.
|
|
387
385
|
// Pairing rule (same as Tile THEME): fixed light surfaces take the stable
|
|
388
386
|
// `fg-on-brand` ink, the flipping inverted surface takes `fg-inverted`, and grey
|
|
389
|
-
// takes full `fg
|
|
390
|
-
//
|
|
387
|
+
// takes full `fg`. #19 lifted `fg-muted` on `surface-muted` from 3.64:1 to 4.96:1,
|
|
388
|
+
// so it is no longer disqualified outright — a Badge is caption-size and keeps the
|
|
389
|
+
// stronger ink deliberately, for the margin rather than for compliance.
|
|
391
390
|
green: 'bg-brand-green text-fg-on-brand',
|
|
392
391
|
yellow: 'bg-accent-yellow text-fg-on-brand',
|
|
393
392
|
grey: 'bg-surface-muted text-fg',
|
|
@@ -84,6 +84,36 @@ const SLASH_COMMANDS: readonly SlashCommand[] = [
|
|
|
84
84
|
* stored — so the menu can be re-placed on scroll/resize without stale coordinates. */
|
|
85
85
|
type SlashState = { from: number; query: string; index: number }
|
|
86
86
|
|
|
87
|
+
/** Tallest the palette is ever allowed to grow; past this it scrolls. */
|
|
88
|
+
const MENU_MAX_H = 288
|
|
89
|
+
|
|
90
|
+
/** Caret box + palette size + viewport, in viewport coords. */
|
|
91
|
+
type PlaceInput = { caretTop: number; caretBottom: number; caretLeft: number; wanted: number; width: number; viewportW: number; viewportH: number }
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the palette's fixed position from the caret. Below the caret by default;
|
|
95
|
+
* flipped above only when above is genuinely roomier; height always capped to the
|
|
96
|
+
* room on the chosen side so it scrolls rather than overflowing. Both branches are
|
|
97
|
+
* anchored to the caret, so no result ever needs clamping away from it.
|
|
98
|
+
* Exported for tests.
|
|
99
|
+
*/
|
|
100
|
+
export function placeSlashMenu({ caretTop, caretBottom, caretLeft, wanted, width, viewportW, viewportH }: PlaceInput): { left: number; top: number; maxHeight: number } {
|
|
101
|
+
const gap = 6
|
|
102
|
+
const edge = 8
|
|
103
|
+
const below = viewportH - edge - (caretBottom + gap)
|
|
104
|
+
const above = caretTop - gap - edge
|
|
105
|
+
// Flip only when it buys room. Flipping on any bottom overflow and clamping the
|
|
106
|
+
// result to `edge` parked the palette at the top of the viewport — over the page
|
|
107
|
+
// header — whenever the caret sat high in a short viewport.
|
|
108
|
+
const flip = wanted > below && above > below
|
|
109
|
+
const maxHeight = Math.max(0, Math.min(wanted, flip ? above : below))
|
|
110
|
+
return {
|
|
111
|
+
left: Math.max(edge, Math.min(caretLeft, viewportW - width - edge)),
|
|
112
|
+
top: flip ? caretTop - gap - maxHeight : caretBottom + gap,
|
|
113
|
+
maxHeight,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
87
117
|
/** Filter the palette by title or keyword. Exported for tests. */
|
|
88
118
|
export function filterCommands(query: string): SlashCommand[] {
|
|
89
119
|
const q = query.trim().toLowerCase()
|
|
@@ -154,7 +184,7 @@ export function BlockEditor({ value, onChange, placeholder = "Write, or press '/
|
|
|
154
184
|
const linkInputRef = useRef<HTMLInputElement>(null)
|
|
155
185
|
|
|
156
186
|
const menuRef = useRef<HTMLDivElement>(null)
|
|
157
|
-
const [menuPos, setMenuPos] = useState<{ left: number; top: number } | null>(null)
|
|
187
|
+
const [menuPos, setMenuPos] = useState<{ left: number; top: number; maxHeight: number } | null>(null)
|
|
158
188
|
// The palette portals to <body>, so mount first — a portal has no server render.
|
|
159
189
|
const [mounted, setMounted] = useState(false)
|
|
160
190
|
useEffect(() => setMounted(true), [])
|
|
@@ -303,8 +333,7 @@ export function BlockEditor({ value, onChange, placeholder = "Write, or press '/
|
|
|
303
333
|
)
|
|
304
334
|
runSlashRef.current = runSlash
|
|
305
335
|
|
|
306
|
-
// Place the palette from the caret's CURRENT viewport coords
|
|
307
|
-
// viewport and flipped above the caret when it would overflow the bottom.
|
|
336
|
+
// Place the palette from the caret's CURRENT viewport coords.
|
|
308
337
|
const placeMenu = useCallback(() => {
|
|
309
338
|
const s = slashRef.current
|
|
310
339
|
const el = menuRef.current
|
|
@@ -312,13 +341,19 @@ export function BlockEditor({ value, onChange, placeholder = "Write, or press '/
|
|
|
312
341
|
const size = editor.state.doc.content.size
|
|
313
342
|
if (s.from > size) return setSlash(null)
|
|
314
343
|
const caret = editor.view.coordsAtPos(s.from)
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
344
|
+
const next = placeSlashMenu({
|
|
345
|
+
caretTop: caret.top,
|
|
346
|
+
caretBottom: caret.bottom,
|
|
347
|
+
caretLeft: caret.left,
|
|
348
|
+
// Measure the CONTENT, not the box — the box already carries the cap from the
|
|
349
|
+
// last placement, so measuring it would let one tight spot shrink every
|
|
350
|
+
// placement after it. +2 for the 1px border (scrollHeight omits borders).
|
|
351
|
+
wanted: Math.min(MENU_MAX_H, el.scrollHeight + 2),
|
|
352
|
+
width: el.getBoundingClientRect().width,
|
|
353
|
+
viewportW: window.innerWidth,
|
|
354
|
+
viewportH: window.innerHeight,
|
|
355
|
+
})
|
|
356
|
+
setMenuPos((prev) => (prev && prev.left === next.left && prev.top === next.top && prev.maxHeight === next.maxHeight ? prev : next))
|
|
322
357
|
}, [editor, setSlash])
|
|
323
358
|
|
|
324
359
|
useLayoutEffect(() => {
|
|
@@ -424,7 +459,7 @@ export function BlockEditor({ value, onChange, placeholder = "Write, or press '/
|
|
|
424
459
|
aria-label="Link URL"
|
|
425
460
|
aria-invalid={linkError ? true : undefined}
|
|
426
461
|
placeholder="https://, mailto:, /path"
|
|
427
|
-
className="h-8 w-56 rounded-md border border-border bg-surface px-2 text-small text-fg outline-none placeholder:text-fg-
|
|
462
|
+
className="h-8 w-56 rounded-md border border-border bg-surface px-2 text-small text-fg outline-none placeholder:text-fg-muted focus:border-brand-green"
|
|
428
463
|
onChange={(e) => {
|
|
429
464
|
setLinkDraft(e.target.value)
|
|
430
465
|
setLinkError(null)
|
|
@@ -493,8 +528,15 @@ export function BlockEditor({ value, onChange, placeholder = "Write, or press '/
|
|
|
493
528
|
id={listboxId}
|
|
494
529
|
role="listbox"
|
|
495
530
|
aria-label="Insert block"
|
|
496
|
-
className="fixed z-50
|
|
497
|
-
style={{
|
|
531
|
+
className="fixed z-50 w-64 overflow-auto rounded-lg border border-border bg-surface-card p-1 shadow-md"
|
|
532
|
+
style={{
|
|
533
|
+
left: menuPos?.left ?? 0,
|
|
534
|
+
top: menuPos?.top ?? 0,
|
|
535
|
+
// The cap is placement-derived (room on the chosen side), so it lives here
|
|
536
|
+
// rather than in a max-h-* class — MENU_MAX_H is the ceiling it clamps to.
|
|
537
|
+
maxHeight: menuPos?.maxHeight ?? MENU_MAX_H,
|
|
538
|
+
visibility: menuPos ? 'visible' : 'hidden',
|
|
539
|
+
}}
|
|
498
540
|
>
|
|
499
541
|
{filtered.map((cmd, i) => (
|
|
500
542
|
// role=option must sit on a plain element — a <button> would override it.
|
|
@@ -509,7 +551,7 @@ export function BlockEditor({ value, onChange, placeholder = "Write, or press '/
|
|
|
509
551
|
onClick={() => runSlash(cmd)}
|
|
510
552
|
>
|
|
511
553
|
<span className="text-small text-fg">{cmd.title}</span>
|
|
512
|
-
<span className="text-caption text-fg-
|
|
554
|
+
<span className="text-caption text-fg-muted">{cmd.hint}</span>
|
|
513
555
|
</div>
|
|
514
556
|
))}
|
|
515
557
|
</div>,
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,9 @@ export { uic, uiconfig, type ConfigVariants, type NoInfer } from "./utils/uic";
|
|
|
11
11
|
|
|
12
12
|
// --- primitives ---
|
|
13
13
|
export * from "./components/button";
|
|
14
|
+
export * from "./components/compact-action-button";
|
|
14
15
|
export * from "./components/input";
|
|
16
|
+
export * from "./components/field-appearance";
|
|
15
17
|
export * from "./components/textarea";
|
|
16
18
|
export * from "./components/rich-text-editor";
|
|
17
19
|
export * from "./components/focus-field";
|
|
@@ -27,12 +29,22 @@ export * from "./components/search-field";
|
|
|
27
29
|
export * from "./components/slider";
|
|
28
30
|
export * from "./components/tag-group";
|
|
29
31
|
export * from "./components/file-upload";
|
|
32
|
+
export * from "./components/document-upload-panel";
|
|
33
|
+
export * from "./components/csv-binding-presentation";
|
|
34
|
+
export * from "./components/dialog-action-button";
|
|
35
|
+
export * from "./components/reload-icon";
|
|
30
36
|
export * from "./components/date-field";
|
|
31
37
|
export * from "./components/date-picker";
|
|
32
38
|
export * from "./components/dialog";
|
|
39
|
+
export { AssetSelectionSurface, AssetSelectionEmpty, AssetLibraryPreview, AssetSelectionFilters, AssetSelectionGrid, type AssetSelectionSurfaceProps } from "./components/asset-selection-surface";
|
|
40
|
+
export { AssetMasonryGrid, assetMasonryColumns, assetMasonryPositions } from './components/asset-masonry-grid';
|
|
33
41
|
export * from "./components/side-panel";
|
|
34
42
|
export * from "./components/dropdown-menu";
|
|
35
43
|
export * from "./components/context-menu";
|
|
44
|
+
export * from "./components/context-search-panel";
|
|
45
|
+
export * from "./components/media-gallery";
|
|
46
|
+
export * from "./components/media-settings-dialog";
|
|
47
|
+
export * from "./components/media-asset-panel";
|
|
36
48
|
export * from "./components/tooltip";
|
|
37
49
|
export * from "./components/toast";
|
|
38
50
|
export * from "./components/disclosure";
|
|
@@ -58,9 +70,14 @@ export * from "./components/count-up";
|
|
|
58
70
|
export * from "./components/cta-pill";
|
|
59
71
|
export * from "./components/stat-stack";
|
|
60
72
|
export * from "./components/icons";
|
|
73
|
+
export * from "./components/context-action-glyph";
|
|
61
74
|
export * from "./components/subtle";
|
|
62
75
|
export * from "./components/stats-card";
|
|
76
|
+
export * from "./components/settings-dialog-surface";
|
|
77
|
+
export * from "./components/template-catalog-card";
|
|
63
78
|
export * from "./components/tile";
|
|
79
|
+
export * from "./components/preview-info-card";
|
|
80
|
+
export * from "./components/empty-panel-action";
|
|
64
81
|
export * from "./components/table";
|
|
65
82
|
export * from "./components/dashboard-grid";
|
|
66
83
|
export * from "./components/task-approval-modal";
|
|
@@ -69,3 +86,5 @@ export * from "./components/delivery/download-modal";
|
|
|
69
86
|
export * from "./components/delivery/send-to-print-modal";
|
|
70
87
|
export * from "./components/delivery/publish-modal";
|
|
71
88
|
export * from "./components/delivery/delivery-status-module";
|
|
89
|
+
export * from './components/date-selection-calendar'
|
|
90
|
+
export * from './components/compact-settings-dialog'
|
package/src/layout/app-shell.tsx
CHANGED
|
@@ -151,14 +151,18 @@ export const AppShell = ({
|
|
|
151
151
|
{banner ? <div className="shrink-0">{banner}</div> : null}
|
|
152
152
|
|
|
153
153
|
{/* Sticky topbar — pins to the top of the viewport once the banner
|
|
154
|
-
scrolls past.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
154
|
+
scrolls past. The Topbar header now carries its own 24px horizontal
|
|
155
|
+
padding (matching `Main`) plus its height and bottom border, so this row
|
|
156
|
+
supplies NO padding of its own — it would double up. The drawer toggle is
|
|
157
|
+
a sibling of the topbar, so it takes the same 24px gutter explicitly;
|
|
158
|
+
without it the toggle sat at 8px while every other page element started
|
|
159
|
+
at 24px. KNOWN SEAM: because the toggle sits outside the Topbar, the
|
|
160
|
+
Topbar's bottom border starts after it on mobile. Closing that would mean
|
|
161
|
+
restyling `topbar`, which is an opaque ReactNode here. `bg-surface` keeps
|
|
162
|
+
content from showing through while pinned. z-20 sits above scrolling
|
|
163
|
+
content but below the mobile drawer (z-40/z-50); gs's literal z-10001 is
|
|
164
|
+
to clear a fullscreen overlay we don't have. */}
|
|
165
|
+
<div className="sticky top-0 z-20 flex shrink-0 items-center bg-surface">
|
|
162
166
|
{hasSidebar ? (
|
|
163
167
|
<RACButton
|
|
164
168
|
aria-label={drawerToggleLabel}
|
|
@@ -166,7 +170,7 @@ export const AppShell = ({
|
|
|
166
170
|
aria-controls={drawerOpen ? drawerId : undefined}
|
|
167
171
|
onPress={() => setDrawerOpen((open) => !open)}
|
|
168
172
|
className={
|
|
169
|
-
'
|
|
173
|
+
'ms-6 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-fg outline-none ' +
|
|
170
174
|
'transition-colors hover:bg-surface-muted md:hidden ' +
|
|
171
175
|
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring'
|
|
172
176
|
}
|
package/src/layout/topbar.tsx
CHANGED
|
@@ -39,26 +39,25 @@ import { uic } from '../utils/uic'
|
|
|
39
39
|
// right — with a 1px bottom border (`--color-border`). gs sets
|
|
40
40
|
// `padding: 0 spacing-6`; the horizontal page padding is supplied by AppShell's
|
|
41
41
|
// sticky topbar row, so only the fixed height + border live here. The border sits
|
|
42
|
-
// on this header so it aligns (inset) with the padded content below.
|
|
43
|
-
//
|
|
42
|
+
// on this header so it aligns (inset) with the padded content below. The source
|
|
43
|
+
// Embedded Manager topbar contract is exactly 77px.
|
|
44
44
|
const TopbarRoot = uic('header', {
|
|
45
45
|
displayName: 'Topbar',
|
|
46
|
-
baseClass: 'flex h-
|
|
46
|
+
baseClass: 'flex h-[77px] w-full items-center gap-5 border-b border-border px-6',
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
//
|
|
50
|
-
// tracking) — matches the AppHeader nav-item scale, NOT a page heading.
|
|
49
|
+
// Embedded Hub brand treatment: 14/18, medium, normal tracking.
|
|
51
50
|
const TopbarBrand = uic('div', {
|
|
52
51
|
displayName: 'Topbar.Brand',
|
|
53
52
|
baseClass:
|
|
54
|
-
'flex min-w-0 items-center gap-3 text-
|
|
53
|
+
'flex min-w-0 items-center gap-3 text-small leading-[18px] font-medium tracking-[0] text-fg',
|
|
55
54
|
})
|
|
56
55
|
|
|
57
56
|
// `ml-auto` pushes the nav (and the actions after it) to the right, matching gs's
|
|
58
57
|
// title-left / nav-right layout.
|
|
59
58
|
const TopbarNavBase = uic('nav', {
|
|
60
59
|
displayName: 'Topbar.Nav',
|
|
61
|
-
baseClass: 'ml-auto flex min-w-0 items-
|
|
60
|
+
baseClass: 'ml-auto flex h-9 min-w-0 items-start gap-2 overflow-x-auto',
|
|
62
61
|
})
|
|
63
62
|
|
|
64
63
|
// `@app/ui` ships no i18n — the accessible name of the nav landmark is REQUIRED
|
|
@@ -73,10 +72,8 @@ const TopbarActions = uic('div', {
|
|
|
73
72
|
|
|
74
73
|
/**
|
|
75
74
|
* NavLink — a single primary-navigation item. Mirrors gs-manager's AppHeader
|
|
76
|
-
* nav-tab styling
|
|
77
|
-
*
|
|
78
|
-
* foreground → neutral-100 fill on hover/active. The active tab is the SAME weight
|
|
79
|
-
* as the resting tab (only the fill + color change — gs `_tab` has no weight bump).
|
|
75
|
+
* nav-tab styling: 6px × 13px padding, 2px corners and 13/16 typography.
|
|
76
|
+
* Hover and active share the neutral-100 fill without changing font weight.
|
|
80
77
|
* gs maps BOTH `--color-background-hover` and `--color-background-active` to
|
|
81
78
|
* `--color-neutral-100`, so a single `surface-muted` token covers both states
|
|
82
79
|
* (no distinct active token to map). Renders an `<a>` by default; pass `asChild`
|
|
@@ -90,7 +87,7 @@ const TopbarActions = uic('div', {
|
|
|
90
87
|
const TopbarNavLink = uic('a', {
|
|
91
88
|
displayName: 'Topbar.NavLink',
|
|
92
89
|
baseClass:
|
|
93
|
-
'inline-flex items-center rounded-sm px-[13px] py-[6px] text-compact leading-4 whitespace-nowrap ' +
|
|
90
|
+
'inline-flex items-center rounded-sm px-[13px] py-[6px] text-compact leading-4 tracking-[0] whitespace-nowrap ' +
|
|
94
91
|
'text-fg no-underline transition-colors hover:bg-surface-muted hover:text-fg outline-none ' +
|
|
95
92
|
'focus-visible:ring-2 focus-visible:ring-ring ' +
|
|
96
93
|
'data-[active]:bg-surface-muted data-[active]:text-fg',
|