@podoba/react 0.0.36 → 0.0.37
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/button.tsx +4 -3
- package/src/components/context-menu.tsx +10 -7
- package/src/components/count-up.tsx +7 -2
- package/src/components/dropdown-menu.tsx +27 -17
- package/src/components/field-appearance.ts +4 -1
- package/src/components/section-tabs.tsx +6 -3
- package/src/components/select.tsx +2 -2
- package/src/components/settings-dialog-surface.tsx +4 -3
- package/src/components/table.tsx +2 -2
- package/src/components/tabs.tsx +5 -2
- package/src/components/textarea.tsx +4 -2
- package/src/components/tile.tsx +35 -1
- package/src/layout/topbar.tsx +48 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podoba/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "podoba React components — React Aria Components + Tailwind primitives + layout, built with uic.",
|
|
6
6
|
"repository": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"typecheck": "tsc --build"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@podoba/tokens": "^0.0.
|
|
32
|
-
"@podoba/tailwind": "^0.0.
|
|
31
|
+
"@podoba/tokens": "^0.0.37",
|
|
32
|
+
"@podoba/tailwind": "^0.0.37",
|
|
33
33
|
"react-aria-components": "1.18.0",
|
|
34
34
|
"class-variance-authority": "0.7.1",
|
|
35
35
|
"clsx": "2.1.1",
|
|
@@ -10,10 +10,11 @@ import { uic } from '../utils/uic'
|
|
|
10
10
|
export const Button = uic(RACButton, {
|
|
11
11
|
displayName: 'Button',
|
|
12
12
|
// gs `Button.module.scss`: pill radius, FIXED 13px / weight-400 text (`text-compact`)
|
|
13
|
-
// on every size — size variants change PADDING ONLY, never the text scale.
|
|
14
|
-
//
|
|
13
|
+
// on every size — size variants change PADDING ONLY, never the text scale. The
|
|
14
|
+
// source eases background-color, color, box-shadow and border-color over 120ms `ease`.
|
|
15
15
|
baseClass:
|
|
16
|
-
'inline-flex items-center justify-center gap-2 rounded-full text-compact leading-4
|
|
16
|
+
'inline-flex items-center justify-center gap-2 rounded-full text-compact leading-4 ' +
|
|
17
|
+
'transition-[background-color,color,box-shadow,border-color] duration-120 ease-[ease] motion-reduce:transition-none ' +
|
|
17
18
|
'outline-none data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[focus-visible]:ring-offset-2 ' +
|
|
18
19
|
// Inactive states (disabled / pending) soften the LABEL ONLY and leave every
|
|
19
20
|
// variant on its own surface. Two reasons:
|
|
@@ -118,16 +118,17 @@ const MenuItem = uic(RACMenuItem, {
|
|
|
118
118
|
// upstream API — a hand-written custom property has nowhere to be defined and
|
|
119
119
|
// silently resolves to nothing.
|
|
120
120
|
baseClass:
|
|
121
|
-
'flex min-h-8.5 cursor-pointer select-none items-center gap-2.5 rounded-[10px] px-3 py-1.5 ' +
|
|
121
|
+
'flex min-h-8.5 min-w-0 cursor-pointer select-none items-center gap-2.5 rounded-[10px] px-3 py-1.5 ' +
|
|
122
122
|
'text-compact leading-4 tracking-normal font-normal text-white outline-none ' +
|
|
123
123
|
'data-[focused]:bg-[#2f2f2f] data-[focused]:font-medium data-[hovered]:bg-[#2f2f2f] data-[hovered]:font-medium ' +
|
|
124
124
|
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-45',
|
|
125
125
|
variants: {
|
|
126
|
-
// gs
|
|
127
|
-
//
|
|
128
|
-
//
|
|
126
|
+
// gs `var(--color-error-light, #ffb5b5)`: the Manager defines `--color-error-light`
|
|
127
|
+
// as #fee2e2, so the rendered source ink is #fee2e2 (the #ffb5b5 fallback never
|
|
128
|
+
// applies). `--color-danger-light` DOES exist, but it flips to a ~15% translucent
|
|
129
|
+
// red under `[data-theme="dark"]` and would all but vanish here; `text-danger`
|
|
129
130
|
// (#dc2626) is a light-surface red that fails contrast on #242424.
|
|
130
|
-
destructive: { true: 'text-[#
|
|
131
|
+
destructive: { true: 'text-[#fee2e2] data-[focused]:text-[#fee2e2]' },
|
|
131
132
|
},
|
|
132
133
|
}) as (
|
|
133
134
|
props: React.ComponentProps<typeof RACMenuItem> & { destructive?: boolean },
|
|
@@ -142,7 +143,9 @@ const panelClass =
|
|
|
142
143
|
'w-[258px] max-w-[calc(100vw-16px)] max-h-[calc(100vh-16px)] box-border overflow-y-auto rounded-xl ' +
|
|
143
144
|
'bg-gradient-to-b from-[#242424] to-[#1f1f1f] ' +
|
|
144
145
|
'px-1.5 py-2 shadow-[0_12px_28px_rgba(0,0,0,0.22)] outline-none'
|
|
145
|
-
|
|
146
|
+
// `minmax(0, 1fr)` tracks: an `auto` grid column grows to the items' nowrap
|
|
147
|
+
// min-content, so a long label plus a trailing pill pushed rows past the 258px panel.
|
|
148
|
+
const menuClass = 'grid grid-cols-[minmax(0,1fr)] gap-0.5 outline-none'
|
|
146
149
|
|
|
147
150
|
const CursorPopoverSurface = uic(RACPopover, {
|
|
148
151
|
displayName: 'ContextMenuPopoverSurface',
|
|
@@ -264,7 +267,7 @@ function ContextMenuBody({
|
|
|
264
267
|
{groups.map((group, gi) => (
|
|
265
268
|
// gs separates groups with an 18px top margin (`.group + .group`), not a
|
|
266
269
|
// rule line — so the first group sits flush, the rest gain the gap.
|
|
267
|
-
<RACMenuSection key={groupId(group, gi)} className={gi > 0 ? 'mt-4.5 grid gap-0.5' : 'grid gap-0.5'}>
|
|
270
|
+
<RACMenuSection key={groupId(group, gi)} className={gi > 0 ? 'mt-4.5 grid min-w-0 grid-cols-[minmax(0,1fr)] gap-0.5' : 'grid min-w-0 grid-cols-[minmax(0,1fr)] gap-0.5'}>
|
|
268
271
|
{group.label ? (
|
|
269
272
|
// The grid contributes 2px: 2px margin + 2px gap = source 4px below label.
|
|
270
273
|
<Header className="mx-2 mt-1.5 mb-0.5 text-compact leading-4 tracking-normal font-medium text-white">{group.label}</Header>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ElementType } from 'react'
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState, type ElementType } from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* CountUp — animates a number from 0 up to `value` on mount (and whenever `value`
|
|
@@ -20,6 +20,10 @@ export interface CountUpProps {
|
|
|
20
20
|
|
|
21
21
|
const easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3)
|
|
22
22
|
|
|
23
|
+
// Layout effect in the browser so the reset to 0 commits before the first paint
|
|
24
|
+
// (a passive effect painted the final value for one frame, then jumped to 0).
|
|
25
|
+
const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
|
|
26
|
+
|
|
23
27
|
export function CountUp({
|
|
24
28
|
value,
|
|
25
29
|
durationMs = 900,
|
|
@@ -31,13 +35,14 @@ export function CountUp({
|
|
|
31
35
|
const rafRef = useRef<number | null>(null)
|
|
32
36
|
const startRef = useRef<number | null>(null)
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
useBrowserLayoutEffect(() => {
|
|
35
39
|
if (typeof window === 'undefined') return
|
|
36
40
|
const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
37
41
|
if (reduce || durationMs <= 0) {
|
|
38
42
|
setDisplay(value)
|
|
39
43
|
return
|
|
40
44
|
}
|
|
45
|
+
setDisplay(0)
|
|
41
46
|
startRef.current = null
|
|
42
47
|
const tick = (now: number) => {
|
|
43
48
|
if (startRef.current === null) startRef.current = now
|
|
@@ -40,14 +40,17 @@ import { uic } from '../utils/uic'
|
|
|
40
40
|
* ```
|
|
41
41
|
*
|
|
42
42
|
* gs token map (this is the LIGHT menu, so the gs literals map cleanly to our
|
|
43
|
-
* tokens): content bg
|
|
44
|
-
* radius → `rounded-lg` · `shadow-lg` · item
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* tokens): content bg `--color-background-secondary` (#f7f6f2) → `surface-card` ·
|
|
44
|
+
* 1px `#eceae1` border → `border` · 8px radius → `rounded-lg` · `shadow-lg` · item
|
|
45
|
+
* 12/16px padding → `py-3 px-4` · 16px/22px base text → `text-body leading-5.5` ·
|
|
46
|
+
* 6px item radius → `rounded-md` · item highlight `--color-background-hover`
|
|
47
|
+
* (#eceae1) → `surface-muted` · item text `#0d0d0d` → `fg` · section label 13px →
|
|
48
|
+
* `text-fg-muted text-compact`. Motion: gs `slideDownAndFade` in / `slideUpAndFade`
|
|
49
|
+
* out, 200ms `cubic-bezier(0.16, 1, 0.3, 1)` (`animate-dropdown-in` / `-out`).
|
|
47
50
|
* (#25: the source grey is 2.10:1 on `surface`; a section label is read, not ornament)
|
|
48
51
|
* · separator `#eceae1` → `border`. The destructive item uses `text-danger`
|
|
49
52
|
* (`#dc2626`) — a light-surface red that reads correctly here (unlike the dark
|
|
50
|
-
* ContextMenu, which needs the lighter `#
|
|
53
|
+
* ContextMenu, which needs the lighter `#fee2e2`).
|
|
51
54
|
*
|
|
52
55
|
* Presentational only (hard rule #1) — no app imports.
|
|
53
56
|
*/
|
|
@@ -55,18 +58,25 @@ import { uic } from '../utils/uic'
|
|
|
55
58
|
/** Re-export of RAC `MenuTrigger`; owns the open/close state for the pair. */
|
|
56
59
|
export const DropdownMenuTrigger = RACMenuTrigger
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
/** Shared gs dropdown panel skin (also used by the Topbar `UserMenu`). */
|
|
62
|
+
export const dropdownMenuPanelClass =
|
|
63
|
+
'min-w-[220px] rounded-lg border border-border bg-surface-card p-2 shadow-lg outline-none ' +
|
|
64
|
+
'data-[entering]:animate-dropdown-in data-[exiting]:animate-dropdown-out motion-reduce:animate-none'
|
|
65
|
+
|
|
66
|
+
// gs `.item`: 12/16px padding, 6px radius, 16px/22px base text; the Radix
|
|
67
|
+
// `[data-highlighted]` wash is `#eceae1` (→ surface-muted). Radix highlights on
|
|
68
|
+
// pointer hover and keyboard focus, never on a pointer open, so we key on
|
|
69
|
+
// `data-[hovered]` + `data-[focus-visible]` rather than RAC's plain `data-[focused]`
|
|
70
|
+
// (which also marks the first item when a mouse press opens the menu).
|
|
71
|
+
export const dropdownMenuItemClass =
|
|
72
|
+
'flex cursor-pointer select-none items-center gap-2.5 rounded-md px-4 py-3 ' +
|
|
73
|
+
'text-body leading-5.5 font-normal text-fg outline-none ' +
|
|
74
|
+
'data-[focus-visible]:bg-surface-muted data-[hovered]:bg-surface-muted ' +
|
|
75
|
+
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50'
|
|
76
|
+
|
|
63
77
|
export const DropdownMenuItem = uic(RACMenuItem, {
|
|
64
78
|
displayName: 'DropdownMenuItem',
|
|
65
|
-
baseClass:
|
|
66
|
-
'flex cursor-pointer select-none items-center gap-2.5 rounded-md px-4 py-3 ' +
|
|
67
|
-
'text-small text-fg outline-none ' +
|
|
68
|
-
'data-[focused]:bg-surface-card data-[hovered]:bg-surface-card ' +
|
|
69
|
-
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
|
79
|
+
baseClass: dropdownMenuItemClass,
|
|
70
80
|
variants: {
|
|
71
81
|
// gs has no destructive item style on the light menu; we add the house danger
|
|
72
82
|
// token (red text), which reads correctly on the white / cream surface.
|
|
@@ -128,11 +138,11 @@ export function DropdownMenu<T extends object>({
|
|
|
128
138
|
}: DropdownMenuProps<T>) {
|
|
129
139
|
return (
|
|
130
140
|
// gs `.content`: min-width 220px, 8px padding (`p-2`), 8px radius, 1px
|
|
131
|
-
// `#eceae1` border,
|
|
141
|
+
// `#eceae1` border, `#f7f6f2` fill, `shadow-lg`, 200ms slide/fade both ways.
|
|
132
142
|
<RACPopover
|
|
133
143
|
placement={placement}
|
|
134
144
|
className={
|
|
135
|
-
|
|
145
|
+
dropdownMenuPanelClass +
|
|
136
146
|
(popoverClassName ? ` ${popoverClassName}` : '')
|
|
137
147
|
}
|
|
138
148
|
>
|
|
@@ -9,6 +9,9 @@ export const filledFieldClasses =
|
|
|
9
9
|
'border-0 bg-surface-card text-small font-normal leading-4.5 text-fg ' +
|
|
10
10
|
'outline-none transition-colors duration-200 motion-reduce:transition-none placeholder:text-fg-muted placeholder:font-normal ' +
|
|
11
11
|
'data-[hovered]:bg-surface-muted data-[focused]:bg-surface-card ' +
|
|
12
|
-
|
|
12
|
+
// `outline-none` above zeroes Tailwind v4's `--tw-outline-style`, so the focus
|
|
13
|
+
// width alone would still resolve to `outline-style: none`. Restate the solid
|
|
14
|
+
// style or the gs 2px focus outline never paints (WCAG 2.4.7).
|
|
15
|
+
'focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 ' +
|
|
13
16
|
'data-[invalid]:ring-1 data-[invalid]:ring-danger data-[invalid]:outline-danger ' +
|
|
14
17
|
'data-[disabled]:bg-surface-muted data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed'
|
|
@@ -52,9 +52,12 @@ const GridGlyph = () => (
|
|
|
52
52
|
</svg>
|
|
53
53
|
)
|
|
54
54
|
|
|
55
|
+
// gs `.tabButton`: inactive ink `#242423` (the light-theme neutral that
|
|
56
|
+
// `surface-inverted` carries; it flips to a light ink on the dark shell), selected
|
|
57
|
+
// and hovered `fg` on `surface-muted`, background + colour eased over 120ms `ease`.
|
|
55
58
|
const tabBase =
|
|
56
|
-
'inline-flex h-7 shrink-0 items-center gap-2 rounded-sm px-nav-x py-1.5 text-compact font-normal text-
|
|
57
|
-
'transition-colors duration-
|
|
59
|
+
'inline-flex h-7 shrink-0 items-center gap-2 rounded-sm px-nav-x py-1.5 text-compact font-normal text-surface-inverted outline-none ' +
|
|
60
|
+
'transition-colors duration-120 ease-[ease] motion-reduce:transition-none data-[hovered]:bg-surface-muted data-[hovered]:text-fg ' +
|
|
58
61
|
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring ' +
|
|
59
62
|
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:bg-transparent data-[disabled]:text-fg-muted'
|
|
60
63
|
|
|
@@ -90,7 +93,7 @@ export function SectionTabs({
|
|
|
90
93
|
aria-pressed={isResetSelected}
|
|
91
94
|
onPress={onReset}
|
|
92
95
|
className={[
|
|
93
|
-
resetContent ? tabBase : 'h-7 w-7 rounded-sm p-0 text-
|
|
96
|
+
resetContent ? tabBase : 'h-7 w-7 rounded-sm p-0 text-surface-inverted data-[hovered]:text-fg',
|
|
94
97
|
isResetSelected ? tabActive : '',
|
|
95
98
|
]
|
|
96
99
|
.filter(Boolean)
|
|
@@ -43,7 +43,7 @@ const SelectTrigger = uic(RACButton, {
|
|
|
43
43
|
appearance: {
|
|
44
44
|
filled: 'min-h-control-tall border-0 bg-surface-card py-5 font-normal leading-4.5 duration-200 motion-reduce:transition-none ' +
|
|
45
45
|
'data-[hovered]:bg-surface-muted group-data-[open]:bg-surface-card ' +
|
|
46
|
-
'data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:outline-offset-2 ' +
|
|
46
|
+
'data-[focus-visible]:outline-solid data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:outline-offset-2 ' +
|
|
47
47
|
'group-data-[invalid]:ring-1 group-data-[invalid]:ring-danger ' +
|
|
48
48
|
'data-[disabled]:bg-surface-card data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed',
|
|
49
49
|
outlined: 'h-control-tall border border-border bg-surface ' +
|
|
@@ -66,7 +66,7 @@ const StyledSelectItem = uic(ListBoxItem, {
|
|
|
66
66
|
appearance: {
|
|
67
67
|
filled: 'min-w-0 overflow-hidden px-5 py-0 font-normal leading-4.5 max-md:min-h-11 ' +
|
|
68
68
|
'data-[hovered]:font-medium data-[focused]:font-medium data-[selected]:font-medium ' +
|
|
69
|
-
'data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:-outline-offset-2',
|
|
69
|
+
'data-[focus-visible]:outline-solid data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:-outline-offset-2',
|
|
70
70
|
outlined: 'px-3 py-2 ' +
|
|
71
71
|
'data-[hovered]:bg-surface-muted data-[focused]:bg-surface-muted data-[selected]:font-medium ' +
|
|
72
72
|
'data-[disabled]:opacity-50 data-[disabled]:pointer-events-none',
|
|
@@ -98,7 +98,8 @@ export function SettingsDialogSurface({ variant, isOpen, onOpenChange, isPending
|
|
|
98
98
|
<Body ref={bodyRef} className={bodyClass} data-testid={bodyTestId}>{children}</Body>
|
|
99
99
|
</Container>
|
|
100
100
|
</ScrollBody>
|
|
101
|
-
|
|
101
|
+
{/* gs dialog footers sit on the white surface with no rule above them. */}
|
|
102
|
+
<footer className={catalog ? 'shrink-0 bg-surface pt-3' : create ? 'shrink-0 bg-surface pt-4' : 'shrink-0 bg-surface pt-5'}>
|
|
102
103
|
<Container className={containerClass}>
|
|
103
104
|
<div className={`flex justify-end ${csv || basic ? 'items-center gap-1' : variant === 'edit' ? 'gap-3' : 'gap-2'}`}>{footer}</div>
|
|
104
105
|
</Container>
|
|
@@ -130,7 +131,7 @@ export function WizardFormDialog({ isOpen, onOpenChange, isPending = false, isEx
|
|
|
130
131
|
</Button>
|
|
131
132
|
<header className="shrink-0"><Container className="max-w-384 px-5 min-[901px]:px-6"><DisplayHeading id={titleId} className="m-0 whitespace-pre-line pb-0.5 text-fg-muted" style={{ letterSpacing: 0 }}>{title}</DisplayHeading></Container></header>
|
|
132
133
|
<ScrollBody style={{ scrollbarGutter: 'stable' }}><Container className="max-w-384 px-5 min-[901px]:px-6"><Body className="gap-8">{children}</Body></Container></ScrollBody>
|
|
133
|
-
<footer className="shrink-0
|
|
134
|
+
<footer className="shrink-0 bg-surface pt-5"><Container className="max-w-384 px-5 min-[901px]:px-6"><div className="flex flex-wrap justify-end gap-2">{footer}</div></Container></footer>
|
|
134
135
|
</Layout>
|
|
135
136
|
</Panel>
|
|
136
137
|
</ModalOverlay>
|
|
@@ -220,7 +221,7 @@ export function ProductionSettingsDialog({ isOpen, onOpenChange, prefix, title,
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
const DisclosureRoot = uic(AriaDisclosure, { displayName: 'SettingsDisclosureRoot', baseClass: 'group/settings-disclosure flex min-w-0 flex-col gap-3' })
|
|
223
|
-
const DisclosureTrigger = uic(AriaButton, { displayName: 'SettingsDisclosureTrigger', baseClass: 'flex w-full cursor-pointer items-start justify-between gap-3 bg-transparent p-0 text-left outline-none data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:outline-offset-2' })
|
|
224
|
+
const DisclosureTrigger = uic(AriaButton, { displayName: 'SettingsDisclosureTrigger', baseClass: 'flex w-full cursor-pointer items-start justify-between gap-3 bg-transparent p-0 text-left outline-none data-[focus-visible]:outline-solid data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:outline-offset-2' })
|
|
224
225
|
const DisclosureCopy = uic('span', { displayName: 'SettingsDisclosureCopy', baseClass: 'flex min-w-0 flex-col gap-1' })
|
|
225
226
|
const DisclosureTitle = uic('strong', { displayName: 'SettingsDisclosureTitle', baseClass: 'text-body font-medium leading-normal text-fg' })
|
|
226
227
|
const DisclosureHint = uic('span', { displayName: 'SettingsDisclosureHint', baseClass: 'text-body font-normal leading-normal text-fg-workflow-muted' })
|
package/src/components/table.tsx
CHANGED
|
@@ -116,7 +116,7 @@ export function Table<Row>({
|
|
|
116
116
|
|
|
117
117
|
return (
|
|
118
118
|
<div className={['w-full overflow-x-auto', className].filter(Boolean).join(' ')}>
|
|
119
|
-
<table style={team ? { '--table-columns': gridTemplate } as CSSProperties : undefined} className={`w-full border-collapse text-small ${worksheet ? 'border-b border-border font-normal leading-4.5' : ''} ${team ? 'block
|
|
119
|
+
<table style={team ? { '--table-columns': gridTemplate } as CSSProperties : undefined} className={`w-full border-collapse text-small ${worksheet ? 'border-b border-border font-normal leading-4.5' : ''} ${team ? 'block font-normal leading-4.5' : ''}`} {...aria}>
|
|
120
120
|
<thead className={team ? 'block' : undefined}>
|
|
121
121
|
<tr className={`${worksheet ? '' : 'border-b border-border'} ${team ? 'grid grid-cols-[var(--table-columns)] gap-3 bg-surface-muted px-4 max-[1023px]:hidden' : ''}`}>
|
|
122
122
|
{columns.map((column) => {
|
|
@@ -131,7 +131,7 @@ export function Table<Row>({
|
|
|
131
131
|
aria-sort={
|
|
132
132
|
canSort ? (active ? (sort?.dir === 'asc' ? 'ascending' : 'descending') : 'none') : undefined
|
|
133
133
|
}
|
|
134
|
-
className={`${ALIGN_CLASS[align]} ${worksheet ? 'border-0 px-3 py-4 align-middle font-mono text-small font-normal leading-4.5 tracking-normal text-fg normal-case bg-surface whitespace-nowrap' : team ? 'border-0 px-
|
|
134
|
+
className={`${ALIGN_CLASS[align]} ${worksheet ? 'border-0 px-3 py-4 align-middle font-mono text-small font-normal leading-4.5 tracking-normal text-fg normal-case bg-surface whitespace-nowrap' : team ? 'border-0 px-0 py-3 align-middle font-mono text-small font-normal leading-4.5 tracking-normal text-fg normal-case whitespace-nowrap' : 'px-4 py-3 text-label font-medium tracking-wide text-fg-muted uppercase'}`}
|
|
135
135
|
>
|
|
136
136
|
{canSort ? (
|
|
137
137
|
<button
|
package/src/components/tabs.tsx
CHANGED
|
@@ -25,9 +25,12 @@ export const TabList = uic(RACTabList, {
|
|
|
25
25
|
|
|
26
26
|
export const Tab = uic(RACTab, {
|
|
27
27
|
displayName: 'Tab',
|
|
28
|
+
// gs `Tabs.module.scss` `.trigger`: 12/16px padding, heading-5 (16px/20px) medium,
|
|
29
|
+
// `--color-text-secondary` ink (#242423, the light neutral `surface-inverted`
|
|
30
|
+
// carries), 2px underline, `all 200ms ease-in-out`.
|
|
28
31
|
baseClass:
|
|
29
|
-
'cursor-pointer select-none border-b-2 border-transparent px-4 py-3 text-
|
|
30
|
-
'outline-none transition-
|
|
32
|
+
'cursor-pointer select-none border-b-2 border-transparent px-4 py-3 text-heading5 font-medium text-surface-inverted ' +
|
|
33
|
+
'outline-none transition-all duration-200 ease-[ease-in-out] motion-reduce:transition-none ' +
|
|
31
34
|
'data-[hovered]:text-fg ' +
|
|
32
35
|
'data-[selected]:border-brand-primary data-[selected]:text-fg ' +
|
|
33
36
|
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring ' +
|
|
@@ -58,8 +58,10 @@ export type TextareaProps = TextFieldProps & {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export const Textarea = ({ label, description, errorMessage, placeholder, rows, textAreaClassName, appearance = 'outlined', ...props }: TextareaProps) => (
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Label and gap match `Input` in both appearances (gs 17px/20px field label,
|
|
62
|
+
// 12px to the control), so a Title + Description pair reads as one form.
|
|
63
|
+
<TextField {...props} className="flex w-full flex-col gap-3">
|
|
64
|
+
<Label className="text-panel-heading font-medium text-fg">{label}</Label>
|
|
63
65
|
<StyledTextArea className={textAreaClassName} placeholder={placeholder} rows={rows} appearance={appearance} />
|
|
64
66
|
{description ? (
|
|
65
67
|
<Text slot="description" className="text-label text-fg-muted">
|
package/src/components/tile.tsx
CHANGED
|
@@ -395,10 +395,44 @@ const BADGE: Record<BadgeColor, string> = {
|
|
|
395
395
|
|
|
396
396
|
export function Badge({ label, color = 'grey' }: { label: ReactNode; color?: BadgeColor }) {
|
|
397
397
|
return (
|
|
398
|
+
// gs `Tag.module.scss`: 24px tall, 0 10px padding, 16px (`radius-2xl`) corners,
|
|
399
|
+
// 10px/20px medium label (Label 4). The source's `margin-left: 12px` is layout
|
|
400
|
+
// the caller owns, so it is not baked in here.
|
|
398
401
|
<span
|
|
399
|
-
className={`inline-flex items-center rounded-
|
|
402
|
+
className={`inline-flex h-6 shrink-0 items-center whitespace-nowrap rounded-2xl px-2.5 text-micro font-medium leading-5 ${BADGE[color]}`}
|
|
400
403
|
>
|
|
401
404
|
{label}
|
|
402
405
|
</span>
|
|
403
406
|
)
|
|
404
407
|
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Chip: the gs `GSChip` dot pill used for task status and priority cells.
|
|
411
|
+
*
|
|
412
|
+
* Distinct from {@link Badge} (gs `Tag`): a Chip is the table-cell pill with a
|
|
413
|
+
* leading 8px dot, 13px/16px regular label, 24px height and 0 12px padding. gs has
|
|
414
|
+
* two tones only: `green` (translucent mint surface, brand-green dot) for work that
|
|
415
|
+
* is moving or finished, and `grey` (neutral surface, neutral dot) for everything
|
|
416
|
+
* else. The dot is decorative; the label carries the meaning.
|
|
417
|
+
*/
|
|
418
|
+
export type ChipColor = 'green' | 'grey'
|
|
419
|
+
|
|
420
|
+
const CHIP: Record<ChipColor, { surface: string; dot: string }> = {
|
|
421
|
+
// gs `rgba(150, 246, 194, 0.8)` → accent-green-lighter at 80%; dot brand green.
|
|
422
|
+
green: { surface: 'bg-accent-green-lighter/80 text-fg-on-brand', dot: 'bg-brand-green' },
|
|
423
|
+
// gs neutral-100 surface; the source neutral-200 dot (#aba89c) maps to the
|
|
424
|
+
// nearest shared neutral. Ornamental: the adjacent label is the content.
|
|
425
|
+
grey: { surface: 'bg-surface-muted text-fg', dot: 'bg-neutral-300' },
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function Chip({ label, color = 'grey' }: { label: ReactNode; color?: ChipColor }) {
|
|
429
|
+
const tone = CHIP[color]
|
|
430
|
+
return (
|
|
431
|
+
<span
|
|
432
|
+
className={`inline-flex h-6 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-2xl px-3 text-compact font-normal leading-4 ${tone.surface}`}
|
|
433
|
+
>
|
|
434
|
+
<span aria-hidden="true" className={`size-2 shrink-0 rounded-full ${tone.dot}`} />
|
|
435
|
+
{label}
|
|
436
|
+
</span>
|
|
437
|
+
)
|
|
438
|
+
}
|
package/src/layout/topbar.tsx
CHANGED
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
ListBox,
|
|
9
9
|
ListBoxItem,
|
|
10
10
|
type ListBoxItemProps,
|
|
11
|
+
Header,
|
|
11
12
|
Menu as RACMenu,
|
|
12
13
|
MenuItem as RACMenuItem,
|
|
13
14
|
type MenuItemProps,
|
|
15
|
+
MenuSection,
|
|
14
16
|
MenuTrigger,
|
|
15
17
|
Popover,
|
|
16
18
|
} from 'react-aria-components'
|
|
17
19
|
import { uic } from '../utils/uic'
|
|
20
|
+
import { dropdownMenuItemClass, dropdownMenuPanelClass, DropdownMenuSeparator } from '../components/dropdown-menu'
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Topbar — sticky application header (ported pattern from gs-manager's
|
|
@@ -87,8 +90,12 @@ const TopbarActions = uic('div', {
|
|
|
87
90
|
const TopbarNavLink = uic('a', {
|
|
88
91
|
displayName: 'Topbar.NavLink',
|
|
89
92
|
baseClass:
|
|
93
|
+
// gs inactive ink is `#242423` (the neutral that `surface-inverted` carries in
|
|
94
|
+
// light theme and flips to a light ink on the dark shell); hover/active darken to
|
|
95
|
+
// `fg`. gs `Button.module.scss` eases bg + colour over 120ms `ease`.
|
|
90
96
|
'inline-flex items-center rounded-sm px-[13px] py-[6px] text-compact leading-4 tracking-[0] whitespace-nowrap ' +
|
|
91
|
-
'text-
|
|
97
|
+
'text-surface-inverted no-underline transition-colors duration-120 ease-[ease] motion-reduce:transition-none ' +
|
|
98
|
+
'hover:bg-surface-muted hover:text-fg outline-none ' +
|
|
92
99
|
'focus-visible:ring-2 focus-visible:ring-ring ' +
|
|
93
100
|
'data-[active]:bg-surface-muted data-[active]:text-fg',
|
|
94
101
|
// `active` is accepted both as an explicit prop (uic emits `data-active=""` when
|
|
@@ -194,7 +201,8 @@ export const UserMenu = ({ trigger, triggerLabel, onAction, children }: UserMenu
|
|
|
194
201
|
>
|
|
195
202
|
{trigger}
|
|
196
203
|
</RACButton>
|
|
197
|
-
|
|
204
|
+
{/* gs Hub account menu = the shared gs DropdownMenu panel, bottom-end aligned. */}
|
|
205
|
+
<Popover placement="bottom end" className={dropdownMenuPanelClass}>
|
|
198
206
|
<RACMenu onAction={onAction} className="outline-none">
|
|
199
207
|
{children}
|
|
200
208
|
</RACMenu>
|
|
@@ -204,11 +212,46 @@ export const UserMenu = ({ trigger, triggerLabel, onAction, children }: UserMenu
|
|
|
204
212
|
|
|
205
213
|
export const UserMenuItem = uic(RACMenuItem, {
|
|
206
214
|
displayName: 'UserMenu.Item',
|
|
207
|
-
baseClass:
|
|
208
|
-
'flex cursor-pointer select-none items-center rounded-sm px-3 py-2 text-small text-fg outline-none ' +
|
|
209
|
-
'data-[focused]:bg-surface-muted data-[disabled]:opacity-50 data-[disabled]:pointer-events-none',
|
|
215
|
+
baseClass: `${dropdownMenuItemClass} data-[disabled]:pointer-events-none`,
|
|
210
216
|
}) as (props: MenuItemProps) => ReactNode
|
|
211
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Two-line item copy (gs `.menuItemStack`): a primary label with a small muted hint
|
|
220
|
+
* underneath, e.g. "Manager / Current" or "Settings / Opens in GS Manager".
|
|
221
|
+
*/
|
|
222
|
+
export const UserMenuItemText = ({ label, hint }: { label: ReactNode; hint?: ReactNode }) => (
|
|
223
|
+
<span className="flex min-w-0 flex-col items-start gap-1">
|
|
224
|
+
<span>{label}</span>
|
|
225
|
+
{hint ? <span className="text-micro leading-5 font-normal text-fg-muted">{hint}</span> : null}
|
|
226
|
+
</span>
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Non-interactive identity block at the top of the account menu (gs
|
|
231
|
+
* `.userMenuLabel`): the person's name (13px medium) over a muted detail line.
|
|
232
|
+
*/
|
|
233
|
+
export const UserMenuIdentity = ({ name, detail }: { name: ReactNode; detail?: ReactNode }) => (
|
|
234
|
+
<MenuSection>
|
|
235
|
+
<Header className="flex flex-col gap-1 px-4 py-3">
|
|
236
|
+
<span className="text-compact font-medium text-fg">{name}</span>
|
|
237
|
+
{detail ? <span className="mt-1 text-micro leading-5 font-medium text-fg-muted">{detail}</span> : null}
|
|
238
|
+
</Header>
|
|
239
|
+
</MenuSection>
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
export type UserMenuSectionProps = { label?: ReactNode; children: ReactNode; 'aria-label'?: string }
|
|
243
|
+
|
|
244
|
+
/** A labelled group of account-menu items (gs `.menuSectionLabel`, e.g. "Apps"). */
|
|
245
|
+
export const UserMenuSection = ({ label, children, ...props }: UserMenuSectionProps) => (
|
|
246
|
+
<MenuSection {...props}>
|
|
247
|
+
{label ? <Header className="px-2 pt-2 pb-1 text-micro leading-5 font-medium text-fg-muted">{label}</Header> : null}
|
|
248
|
+
{children}
|
|
249
|
+
</MenuSection>
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
/** 1px rule between account-menu groups (gs `.separator`). */
|
|
253
|
+
export const UserMenuSeparator = DropdownMenuSeparator
|
|
254
|
+
|
|
212
255
|
export const Topbar = Object.assign(TopbarRoot, {
|
|
213
256
|
Brand: TopbarBrand,
|
|
214
257
|
Nav: TopbarNav,
|