@proteos/ui 0.31.0
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/LICENSE +105 -0
- package/dist/chunk-IKAX76MB.js +113 -0
- package/dist/chunk-IKAX76MB.js.map +1 -0
- package/dist/chunk-MWAXK66H.cjs +119 -0
- package/dist/chunk-MWAXK66H.cjs.map +1 -0
- package/dist/index.cjs +6246 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2126 -0
- package/dist/index.d.ts +2126 -0
- package/dist/index.js +6069 -0
- package/dist/index.js.map +1 -0
- package/dist/pdf-view-3ZMMS6ML.cjs +95 -0
- package/dist/pdf-view-3ZMMS6ML.cjs.map +1 -0
- package/dist/pdf-view-BV5ZD6OC.js +93 -0
- package/dist/pdf-view-BV5ZD6OC.js.map +1 -0
- package/package.json +96 -0
- package/src/components/attachment-chip/attachment-chip.tsx +129 -0
- package/src/components/attachment-chip/index.ts +2 -0
- package/src/components/attribute-type-icon/attribute-type-icon.stories.tsx +70 -0
- package/src/components/attribute-type-icon/attribute-type-icon.tsx +125 -0
- package/src/components/attribute-type-icon/index.ts +8 -0
- package/src/components/avatar/avatar.stories.tsx +42 -0
- package/src/components/avatar/avatar.tsx +48 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/avatar-group/avatar-group.test.tsx +38 -0
- package/src/components/avatar-group/avatar-group.tsx +60 -0
- package/src/components/avatar-group/index.ts +2 -0
- package/src/components/badge/badge.stories.tsx +62 -0
- package/src/components/badge/badge.test.tsx +36 -0
- package/src/components/badge/badge.tsx +54 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/badge-multi-select/badge-multi-select.stories.tsx +177 -0
- package/src/components/badge-multi-select/badge-multi-select.test.tsx +155 -0
- package/src/components/badge-multi-select/badge-multi-select.tsx +272 -0
- package/src/components/badge-multi-select/index.ts +2 -0
- package/src/components/breadcrumbs/breadcrumbs.stories.tsx +43 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +86 -0
- package/src/components/breadcrumbs/index.ts +2 -0
- package/src/components/button/button.stories.tsx +290 -0
- package/src/components/button/button.test.tsx +59 -0
- package/src/components/button/button.tsx +86 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/card/card.stories.tsx +351 -0
- package/src/components/card/card.test.tsx +141 -0
- package/src/components/card/card.tsx +173 -0
- package/src/components/card/index.ts +13 -0
- package/src/components/checkbox/checkbox-group.tsx +52 -0
- package/src/components/checkbox/checkbox.stories.tsx +48 -0
- package/src/components/checkbox/checkbox.test.tsx +50 -0
- package/src/components/checkbox/checkbox.tsx +71 -0
- package/src/components/checkbox/index.ts +4 -0
- package/src/components/combobox/combobox.stories.tsx +97 -0
- package/src/components/combobox/combobox.test.tsx +123 -0
- package/src/components/combobox/combobox.tsx +239 -0
- package/src/components/combobox/index.ts +2 -0
- package/src/components/command/command-hint.tsx +46 -0
- package/src/components/command/command.stories.tsx +88 -0
- package/src/components/command/command.test.tsx +102 -0
- package/src/components/command/command.tsx +76 -0
- package/src/components/command/index.ts +16 -0
- package/src/components/currency-input/currency-input.tsx +192 -0
- package/src/components/currency-input/index.ts +6 -0
- package/src/components/data-table/cells.stories.tsx +120 -0
- package/src/components/data-table/cells.tsx +165 -0
- package/src/components/data-table/data-table.stories.tsx +371 -0
- package/src/components/data-table/data-table.tsx +556 -0
- package/src/components/data-table/index.ts +22 -0
- package/src/components/data-table/make-actions-column.ts +30 -0
- package/src/components/data-table/states/data-table-empty.tsx +40 -0
- package/src/components/data-table/states/data-table-error.tsx +30 -0
- package/src/components/data-table/states/data-table-skeleton.tsx +42 -0
- package/src/components/data-table/use-infinite-scroll-sentinel.ts +51 -0
- package/src/components/date-picker/calendar-internals.tsx +138 -0
- package/src/components/date-picker/date-picker.stories.tsx +78 -0
- package/src/components/date-picker/date-picker.test.tsx +71 -0
- package/src/components/date-picker/date-picker.tsx +140 -0
- package/src/components/date-picker/index.ts +2 -0
- package/src/components/date-time-picker/date-time-picker.stories.tsx +96 -0
- package/src/components/date-time-picker/date-time-picker.test.tsx +87 -0
- package/src/components/date-time-picker/date-time-picker.tsx +203 -0
- package/src/components/date-time-picker/index.ts +2 -0
- package/src/components/empty-state/empty-state.tsx +63 -0
- package/src/components/empty-state/index.ts +2 -0
- package/src/components/error-animation/error-animation.stories.tsx +50 -0
- package/src/components/error-animation/error-animation.test.tsx +22 -0
- package/src/components/error-animation/error-animation.tsx +26 -0
- package/src/components/error-animation/index.ts +2 -0
- package/src/components/field/field.tsx +142 -0
- package/src/components/field/index.ts +8 -0
- package/src/components/file-field/file-field.tsx +285 -0
- package/src/components/file-field/index.ts +2 -0
- package/src/components/file-viewer/fallback-card.tsx +68 -0
- package/src/components/file-viewer/file-viewer.stories.tsx +67 -0
- package/src/components/file-viewer/file-viewer.test.tsx +81 -0
- package/src/components/file-viewer/file-viewer.tsx +116 -0
- package/src/components/file-viewer/image-view.tsx +38 -0
- package/src/components/file-viewer/index.ts +7 -0
- package/src/components/file-viewer/pdf-view.tsx +122 -0
- package/src/components/file-viewer/video-view.tsx +16 -0
- package/src/components/image-attachment/image-attachment.stories.tsx +39 -0
- package/src/components/image-attachment/image-attachment.test.tsx +54 -0
- package/src/components/image-attachment/image-attachment.tsx +77 -0
- package/src/components/image-attachment/index.ts +3 -0
- package/src/components/image-attachment/is-previewable-image-name.ts +14 -0
- package/src/components/image-lightbox/image-lightbox.stories.tsx +38 -0
- package/src/components/image-lightbox/image-lightbox.test.tsx +72 -0
- package/src/components/image-lightbox/image-lightbox.tsx +92 -0
- package/src/components/image-lightbox/index.ts +2 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/input.stories.tsx +71 -0
- package/src/components/input/input.test.tsx +36 -0
- package/src/components/input/input.tsx +76 -0
- package/src/components/kbd/index.ts +2 -0
- package/src/components/kbd/kbd.stories.tsx +34 -0
- package/src/components/kbd/kbd.tsx +27 -0
- package/src/components/menu/index.ts +2 -0
- package/src/components/menu/menu.tsx +121 -0
- package/src/components/modal/index.ts +10 -0
- package/src/components/modal/modal.stories.tsx +153 -0
- package/src/components/modal/modal.test.tsx +93 -0
- package/src/components/modal/modal.tsx +149 -0
- package/src/components/multi-select/index.ts +2 -0
- package/src/components/multi-select/multi-select.stories.tsx +211 -0
- package/src/components/multi-select/multi-select.test.tsx +151 -0
- package/src/components/multi-select/multi-select.tsx +339 -0
- package/src/components/nav/index.ts +7 -0
- package/src/components/nav/nav.stories.tsx +139 -0
- package/src/components/nav/nav.tsx +246 -0
- package/src/components/popover/index.ts +8 -0
- package/src/components/popover/popover.stories.tsx +92 -0
- package/src/components/popover/popover.tsx +135 -0
- package/src/components/progress-bar/index.ts +2 -0
- package/src/components/progress-bar/progress-bar.stories.tsx +75 -0
- package/src/components/progress-bar/progress-bar.test.tsx +29 -0
- package/src/components/progress-bar/progress-bar.tsx +57 -0
- package/src/components/radio-group/index.ts +2 -0
- package/src/components/radio-group/radio-group.stories.tsx +113 -0
- package/src/components/radio-group/radio-group.test.tsx +87 -0
- package/src/components/radio-group/radio-group.tsx +104 -0
- package/src/components/record-filter/attribute-picker.tsx +74 -0
- package/src/components/record-filter/chip-value.tsx +171 -0
- package/src/components/record-filter/filter-row.tsx +262 -0
- package/src/components/record-filter/filter-value-input.tsx +351 -0
- package/src/components/record-filter/group-add-controls.tsx +77 -0
- package/src/components/record-filter/group-body.tsx +173 -0
- package/src/components/record-filter/group-mutations.test.ts +104 -0
- package/src/components/record-filter/group-mutations.ts +116 -0
- package/src/components/record-filter/index.ts +58 -0
- package/src/components/record-filter/nested-group.tsx +138 -0
- package/src/components/record-filter/operator-picker.tsx +62 -0
- package/src/components/record-filter/operators.test.ts +105 -0
- package/src/components/record-filter/operators.ts +97 -0
- package/src/components/record-filter/record-filter-builder-panel.tsx +236 -0
- package/src/components/record-filter/record-filter-button.tsx +126 -0
- package/src/components/record-filter/record-filter-chips.tsx +527 -0
- package/src/components/record-filter/record-filter-editor.tsx +91 -0
- package/src/components/record-filter/record-filter.stories.tsx +196 -0
- package/src/components/record-filter/record-filter.test.tsx +437 -0
- package/src/components/record-filter/show-details-store.ts +72 -0
- package/src/components/record-filter/types.ts +150 -0
- package/src/components/segmented-control/index.ts +6 -0
- package/src/components/segmented-control/segmented-control.stories.tsx +67 -0
- package/src/components/segmented-control/segmented-control.test.tsx +47 -0
- package/src/components/segmented-control/segmented-control.tsx +105 -0
- package/src/components/select/index.ts +2 -0
- package/src/components/select/select.stories.tsx +76 -0
- package/src/components/select/select.test.tsx +56 -0
- package/src/components/select/select.tsx +159 -0
- package/src/components/separator/index.ts +2 -0
- package/src/components/separator/separator.stories.tsx +73 -0
- package/src/components/separator/separator.test.tsx +29 -0
- package/src/components/separator/separator.tsx +51 -0
- package/src/components/skeleton/index.ts +2 -0
- package/src/components/skeleton/skeleton.stories.tsx +68 -0
- package/src/components/skeleton/skeleton.test.tsx +38 -0
- package/src/components/skeleton/skeleton.tsx +43 -0
- package/src/components/success-animation/index.ts +2 -0
- package/src/components/success-animation/success-animation.stories.tsx +50 -0
- package/src/components/success-animation/success-animation.test.tsx +22 -0
- package/src/components/success-animation/success-animation.tsx +26 -0
- package/src/components/switch/index.ts +2 -0
- package/src/components/switch/switch.stories.tsx +60 -0
- package/src/components/switch/switch.test.tsx +50 -0
- package/src/components/switch/switch.tsx +49 -0
- package/src/components/table/index.ts +25 -0
- package/src/components/table/table.stories.tsx +306 -0
- package/src/components/table/table.test.tsx +115 -0
- package/src/components/table/table.tsx +543 -0
- package/src/components/tabs/index.ts +8 -0
- package/src/components/tabs/tabs.tsx +141 -0
- package/src/components/tag-input/index.ts +2 -0
- package/src/components/tag-input/tag-input.stories.tsx +73 -0
- package/src/components/tag-input/tag-input.test.tsx +80 -0
- package/src/components/tag-input/tag-input.tsx +193 -0
- package/src/components/textarea/index.ts +2 -0
- package/src/components/textarea/textarea.stories.tsx +75 -0
- package/src/components/textarea/textarea.test.tsx +41 -0
- package/src/components/textarea/textarea.tsx +86 -0
- package/src/components/time-field/index.ts +2 -0
- package/src/components/time-field/time-field.stories.tsx +76 -0
- package/src/components/time-field/time-field.test.tsx +51 -0
- package/src/components/time-field/time-field.tsx +98 -0
- package/src/components/tooltip/index.ts +2 -0
- package/src/components/tooltip/tooltip.stories.tsx +176 -0
- package/src/components/tooltip/tooltip.test.tsx +52 -0
- package/src/components/tooltip/tooltip.tsx +56 -0
- package/src/index.ts +47 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/commands/discovery-store.test.tsx +76 -0
- package/src/lib/commands/discovery-store.ts +110 -0
- package/src/lib/commands/index.ts +9 -0
- package/src/lib/commands/match-event.test.ts +99 -0
- package/src/lib/commands/match-event.ts +31 -0
- package/src/lib/commands/parse-combo.test.ts +65 -0
- package/src/lib/commands/parse-combo.ts +48 -0
- package/src/lib/commands/platform.ts +27 -0
- package/src/lib/commands/protected-combos.test.ts +18 -0
- package/src/lib/commands/protected-combos.ts +38 -0
- package/src/lib/commands/registry.test.ts +97 -0
- package/src/lib/commands/registry.ts +59 -0
- package/src/lib/commands/use-command.test.tsx +91 -0
- package/src/lib/commands/use-command.ts +32 -0
- package/src/lib/commands/use-escape-key.test.tsx +97 -0
- package/src/lib/commands/use-escape-key.ts +62 -0
- package/src/styles.css +321 -0
- package/src/test/setup.ts +9 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { NavGroup, NavItem, NavSection } from './nav'
|
|
4
|
+
|
|
5
|
+
/* =========================================================================
|
|
6
|
+
Nav primitives — render inside a sidebar-shaped frame to show realistic
|
|
7
|
+
density (256px column, paper bg, hairline border).
|
|
8
|
+
========================================================================= */
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof NavSection> = {
|
|
11
|
+
title: 'Components/Nav',
|
|
12
|
+
component: NavSection,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
decorators: [
|
|
15
|
+
(Story) => (
|
|
16
|
+
<div className="w-[256px] border border-rule bg-[color-mix(in_oklab,var(--color-surface-2)_50%,var(--color-surface))] p-2.5">
|
|
17
|
+
<Story />
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default meta
|
|
24
|
+
type Story = StoryObj<typeof NavSection>
|
|
25
|
+
|
|
26
|
+
const HomeIcon = (
|
|
27
|
+
<svg viewBox="0 0 16 16" aria-hidden>
|
|
28
|
+
<path
|
|
29
|
+
d="M2 7.5 8 2.5l6 5V13a1 1 0 0 1-1 1h-3v-4H7v4H4a1 1 0 0 1-1-1V7.5h-1Z"
|
|
30
|
+
fill="none"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
strokeWidth="1.25"
|
|
33
|
+
strokeLinejoin="round"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const TruckIcon = (
|
|
39
|
+
<svg viewBox="0 0 16 16" aria-hidden>
|
|
40
|
+
<path
|
|
41
|
+
d="M1.5 4h7v7h-7zM8.5 6h3l2 2v3h-5zM4 13.5a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm7 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"
|
|
42
|
+
fill="none"
|
|
43
|
+
stroke="currentColor"
|
|
44
|
+
strokeWidth="1.2"
|
|
45
|
+
strokeLinejoin="round"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const UsersIcon = (
|
|
51
|
+
<svg viewBox="0 0 16 16" aria-hidden>
|
|
52
|
+
<circle cx="6" cy="5.5" r="2.25" fill="none" stroke="currentColor" strokeWidth="1.2" />
|
|
53
|
+
<path
|
|
54
|
+
d="M2 13c.5-2 2-3.25 4-3.25S9.5 11 10 13"
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
strokeWidth="1.2"
|
|
58
|
+
strokeLinecap="round"
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
export const Playground: Story = {
|
|
64
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
65
|
+
render: () => (
|
|
66
|
+
<NavSection label="Workspace" count={3}>
|
|
67
|
+
<NavItem icon={HomeIcon}>Home</NavItem>
|
|
68
|
+
<NavItem icon={UsersIcon} badge="418">
|
|
69
|
+
Customers
|
|
70
|
+
</NavItem>
|
|
71
|
+
<NavItem icon={TruckIcon} isActive>
|
|
72
|
+
Shipments
|
|
73
|
+
</NavItem>
|
|
74
|
+
</NavSection>
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const ItemStates: Story = {
|
|
79
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
80
|
+
render: () => (
|
|
81
|
+
<NavSection label="States">
|
|
82
|
+
<NavItem icon={HomeIcon}>Default</NavItem>
|
|
83
|
+
<NavItem icon={HomeIcon} isActive>
|
|
84
|
+
Active
|
|
85
|
+
</NavItem>
|
|
86
|
+
<NavItem icon={HomeIcon} badge="12">
|
|
87
|
+
With badge
|
|
88
|
+
</NavItem>
|
|
89
|
+
<NavItem icon={HomeIcon} isDisabled>
|
|
90
|
+
Disabled
|
|
91
|
+
</NavItem>
|
|
92
|
+
</NavSection>
|
|
93
|
+
),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const WithGroup: Story = {
|
|
97
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
98
|
+
render: () => (
|
|
99
|
+
<NavSection label="Operations" count={5}>
|
|
100
|
+
<NavItem icon={UsersIcon} badge="418">
|
|
101
|
+
Customers
|
|
102
|
+
</NavItem>
|
|
103
|
+
<NavGroup icon={TruckIcon} label="Shipments" defaultExpanded>
|
|
104
|
+
<NavItem isActive badge="1,284">
|
|
105
|
+
Open
|
|
106
|
+
</NavItem>
|
|
107
|
+
<NavItem badge="37">Dispatching</NavItem>
|
|
108
|
+
<NavItem badge="208">In transit</NavItem>
|
|
109
|
+
<NavItem>Delivered</NavItem>
|
|
110
|
+
<NavItem>Reconciled</NavItem>
|
|
111
|
+
</NavGroup>
|
|
112
|
+
<NavGroup icon={UsersIcon} label="Drivers">
|
|
113
|
+
<NavItem>Roster</NavItem>
|
|
114
|
+
<NavItem>Schedules</NavItem>
|
|
115
|
+
<NavItem badge="3">Compliance</NavItem>
|
|
116
|
+
</NavGroup>
|
|
117
|
+
</NavSection>
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const MultipleSections: Story = {
|
|
122
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
123
|
+
render: () => (
|
|
124
|
+
<>
|
|
125
|
+
<NavSection label="Workspace">
|
|
126
|
+
<NavItem icon={HomeIcon}>Home</NavItem>
|
|
127
|
+
<NavItem icon={HomeIcon} badge="12">
|
|
128
|
+
Inbox
|
|
129
|
+
</NavItem>
|
|
130
|
+
</NavSection>
|
|
131
|
+
<NavSection label="Operations" count={2}>
|
|
132
|
+
<NavItem icon={UsersIcon}>Customers</NavItem>
|
|
133
|
+
<NavItem icon={TruckIcon} isActive>
|
|
134
|
+
Shipments
|
|
135
|
+
</NavItem>
|
|
136
|
+
</NavSection>
|
|
137
|
+
</>
|
|
138
|
+
),
|
|
139
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button as AriaButton,
|
|
4
|
+
type ButtonProps as AriaButtonProps,
|
|
5
|
+
type DisclosureProps as AriaDisclosureProps,
|
|
6
|
+
Disclosure,
|
|
7
|
+
DisclosurePanel,
|
|
8
|
+
Heading,
|
|
9
|
+
} from 'react-aria-components'
|
|
10
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
11
|
+
|
|
12
|
+
import { cn } from '../../lib/cn'
|
|
13
|
+
|
|
14
|
+
/* =========================================================================
|
|
15
|
+
Nav — vertical navigation primitives for sidebars / drawers. Three pieces:
|
|
16
|
+
- <NavSection> — labelled grouping with optional count + trailing slot.
|
|
17
|
+
- <NavItem> — leaf entry (RAC Button) with icon, label, badge slot,
|
|
18
|
+
and an `isActive` state (raised paper card + accent rail).
|
|
19
|
+
- <NavGroup> — collapsible parent (RAC Disclosure + DisclosurePanel)
|
|
20
|
+
that auto-tags its children as nested via context.
|
|
21
|
+
|
|
22
|
+
The primitives don't know about "collapsed sidebar" — that visual cascade
|
|
23
|
+
lives in the consuming app, which can hide labels / center icons via CSS
|
|
24
|
+
on a wrapping element.
|
|
25
|
+
========================================================================= */
|
|
26
|
+
|
|
27
|
+
/* ---------- nested context ----------------------------------------------- */
|
|
28
|
+
|
|
29
|
+
const NavNestedContext = createContext(false)
|
|
30
|
+
/** True when a NavItem is rendered inside a NavGroup's panel. */
|
|
31
|
+
function useIsNested() {
|
|
32
|
+
return useContext(NavNestedContext)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ---------- NavSection --------------------------------------------------- */
|
|
36
|
+
|
|
37
|
+
export type NavSectionProps = {
|
|
38
|
+
/** Uppercase mono label rendered above the items. */
|
|
39
|
+
label?: ReactNode
|
|
40
|
+
/** Counter rendered at the right of the label (e.g. `5`). */
|
|
41
|
+
count?: ReactNode
|
|
42
|
+
/** Trailing slot at the right of the heading row, e.g. an action button. */
|
|
43
|
+
action?: ReactNode
|
|
44
|
+
className?: string
|
|
45
|
+
children?: ReactNode
|
|
46
|
+
/** Stable test/automation id, forwarded to the section root. */
|
|
47
|
+
'data-testid'?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Visually-grouped block of nav entries. Use one per logical section
|
|
51
|
+
* ("Workspace", "Operations", "Finance"). */
|
|
52
|
+
export function NavSection({
|
|
53
|
+
label,
|
|
54
|
+
count,
|
|
55
|
+
action,
|
|
56
|
+
className,
|
|
57
|
+
children,
|
|
58
|
+
'data-testid': dataTestid,
|
|
59
|
+
}: NavSectionProps) {
|
|
60
|
+
const hasHead = label !== undefined || count !== undefined || action !== undefined
|
|
61
|
+
return (
|
|
62
|
+
<div className={cn('mt-[18px] first:mt-1', className)} data-testid={dataTestid}>
|
|
63
|
+
{hasHead ? (
|
|
64
|
+
<div className="flex items-center justify-between px-2 pt-1 pb-2">
|
|
65
|
+
{label !== undefined ? (
|
|
66
|
+
<span className="font-mono text-[10.5px] uppercase tracking-[0.08em] text-ink-4">
|
|
67
|
+
{label}
|
|
68
|
+
</span>
|
|
69
|
+
) : (
|
|
70
|
+
<span />
|
|
71
|
+
)}
|
|
72
|
+
<div className="flex items-center gap-2">
|
|
73
|
+
{count !== undefined ? (
|
|
74
|
+
<span className="font-mono text-[10px] text-ink-4">{count}</span>
|
|
75
|
+
) : null}
|
|
76
|
+
{action}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
) : null}
|
|
80
|
+
{children}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ---------- NavItem ------------------------------------------------------ */
|
|
86
|
+
|
|
87
|
+
export const navItemStyles = tv({
|
|
88
|
+
base: [
|
|
89
|
+
'group/nav-item relative w-full',
|
|
90
|
+
'flex items-center gap-2.5',
|
|
91
|
+
'px-2 py-[7px] rounded-[7px]',
|
|
92
|
+
'text-[13.5px] tracking-[-0.005em] text-ink-2',
|
|
93
|
+
'cursor-pointer outline-none text-left',
|
|
94
|
+
'transition-[background-color,color,border-color,box-shadow] duration-150',
|
|
95
|
+
'border border-transparent',
|
|
96
|
+
'hover:bg-surface-2 hover:text-ink',
|
|
97
|
+
'focus-visible:shadow-[var(--shadow-focus)]',
|
|
98
|
+
'disabled:opacity-45 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
99
|
+
],
|
|
100
|
+
variants: {
|
|
101
|
+
isActive: {
|
|
102
|
+
// Match the hover state's fill (bg-surface-2 + ink) so the selected color
|
|
103
|
+
// doesn't read brighter than the surrounding chrome. The hairline
|
|
104
|
+
// border + accent rail still mark the current page.
|
|
105
|
+
true: [
|
|
106
|
+
'bg-surface-2 text-ink border-rule',
|
|
107
|
+
// accent rail anchored to the section's left edge
|
|
108
|
+
'before:content-[""] before:absolute before:-left-2.5 before:top-2 before:bottom-2',
|
|
109
|
+
'before:w-0.5 before:rounded-[2px] before:bg-accent',
|
|
110
|
+
],
|
|
111
|
+
false: '',
|
|
112
|
+
},
|
|
113
|
+
isNested: {
|
|
114
|
+
true: 'text-[13px] text-ink-3 py-1.5',
|
|
115
|
+
false: '',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
compoundVariants: [
|
|
119
|
+
// Nested + active: raise to ink rather than accent-ink so the indented
|
|
120
|
+
// hierarchy still reads "softer than top-level".
|
|
121
|
+
{ isNested: true, isActive: true, class: 'text-ink' },
|
|
122
|
+
],
|
|
123
|
+
defaultVariants: { isActive: false, isNested: false },
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
export type NavItemVariants = VariantProps<typeof navItemStyles>
|
|
127
|
+
|
|
128
|
+
export type NavItemProps = Omit<AriaButtonProps, 'children'> & {
|
|
129
|
+
/** Leading icon. Pass an SVG element; it's wrapped in a 16px box. */
|
|
130
|
+
icon?: ReactNode
|
|
131
|
+
/** Trailing slot — typically a count or status dot. */
|
|
132
|
+
badge?: ReactNode
|
|
133
|
+
/** Mark the item as the current page. */
|
|
134
|
+
isActive?: boolean
|
|
135
|
+
children?: ReactNode
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Leaf nav entry. RAC Button under the hood — full keyboard, focus-visible
|
|
139
|
+
* ring, and disabled state included. For routing, apply `navItemStyles({...})`
|
|
140
|
+
* to a RAC `<Link>` instead. */
|
|
141
|
+
export function NavItem({ icon, badge, isActive, className, children, ...props }: NavItemProps) {
|
|
142
|
+
const isNested = useIsNested()
|
|
143
|
+
return (
|
|
144
|
+
<AriaButton
|
|
145
|
+
{...props}
|
|
146
|
+
className={(renderProps) =>
|
|
147
|
+
cn(
|
|
148
|
+
navItemStyles({ isActive, isNested }),
|
|
149
|
+
typeof className === 'function' ? className(renderProps) : className,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
{icon !== undefined ? (
|
|
154
|
+
<span
|
|
155
|
+
aria-hidden
|
|
156
|
+
className={cn(
|
|
157
|
+
'grid place-items-center shrink-0 size-4',
|
|
158
|
+
isActive ? 'text-accent' : 'text-ink-3',
|
|
159
|
+
'[&>svg]:size-4',
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
{icon}
|
|
163
|
+
</span>
|
|
164
|
+
) : null}
|
|
165
|
+
<span className="flex-1 min-w-0 truncate">{children}</span>
|
|
166
|
+
{badge !== undefined ? (
|
|
167
|
+
<span className="shrink-0 font-mono text-[10.5px] tracking-[0.02em] text-ink-4">
|
|
168
|
+
{badge}
|
|
169
|
+
</span>
|
|
170
|
+
) : null}
|
|
171
|
+
</AriaButton>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ---------- NavGroup ----------------------------------------------------- */
|
|
176
|
+
|
|
177
|
+
export type NavGroupProps = Omit<AriaDisclosureProps, 'children'> & {
|
|
178
|
+
/** Leading icon for the group head. */
|
|
179
|
+
icon?: ReactNode
|
|
180
|
+
/** Group label rendered next to the icon. */
|
|
181
|
+
label: ReactNode
|
|
182
|
+
/** Children get auto-styled as nested NavItems via context. */
|
|
183
|
+
children: ReactNode
|
|
184
|
+
className?: string
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Collapsible parent. RAC Disclosure handles aria-expanded / aria-controls
|
|
188
|
+
* and keyboard activation. Children render inside an indented panel with a
|
|
189
|
+
* vertical guide rule and pick up nested styling automatically. */
|
|
190
|
+
export function NavGroup({ icon, label, children, className, ...props }: NavGroupProps) {
|
|
191
|
+
return (
|
|
192
|
+
<Disclosure {...props} className={cn('group/nav-group my-px', className)}>
|
|
193
|
+
<Heading className="m-0">
|
|
194
|
+
<AriaButton
|
|
195
|
+
slot="trigger"
|
|
196
|
+
className={cn(
|
|
197
|
+
'flex w-full items-center gap-2.5',
|
|
198
|
+
'px-2 py-[7px] rounded-[7px]',
|
|
199
|
+
'text-[13.5px] tracking-[-0.005em] text-ink-2 text-left',
|
|
200
|
+
'cursor-pointer outline-none border border-transparent',
|
|
201
|
+
'transition-colors duration-150',
|
|
202
|
+
'hover:bg-surface-2 hover:text-ink',
|
|
203
|
+
'focus-visible:shadow-[var(--shadow-focus)]',
|
|
204
|
+
)}
|
|
205
|
+
>
|
|
206
|
+
{icon !== undefined ? (
|
|
207
|
+
<span
|
|
208
|
+
aria-hidden
|
|
209
|
+
className="grid place-items-center shrink-0 size-4 text-ink-3 [&>svg]:size-4"
|
|
210
|
+
>
|
|
211
|
+
{icon}
|
|
212
|
+
</span>
|
|
213
|
+
) : null}
|
|
214
|
+
<span className="flex-1 min-w-0 truncate">{label}</span>
|
|
215
|
+
<svg
|
|
216
|
+
viewBox="0 0 12 12"
|
|
217
|
+
aria-hidden
|
|
218
|
+
className="size-2.5 shrink-0 text-ink-4 transition-transform duration-200 group-data-[expanded]/nav-group:rotate-90"
|
|
219
|
+
>
|
|
220
|
+
<path
|
|
221
|
+
d="m4.5 3 3 3-3 3"
|
|
222
|
+
fill="none"
|
|
223
|
+
stroke="currentColor"
|
|
224
|
+
strokeWidth="1.4"
|
|
225
|
+
strokeLinecap="round"
|
|
226
|
+
strokeLinejoin="round"
|
|
227
|
+
/>
|
|
228
|
+
</svg>
|
|
229
|
+
</AriaButton>
|
|
230
|
+
</Heading>
|
|
231
|
+
<DisclosurePanel>
|
|
232
|
+
<div
|
|
233
|
+
className={cn(
|
|
234
|
+
'relative pl-[22px] py-px',
|
|
235
|
+
// Vertical guide rule — sits under where the parent's icon center
|
|
236
|
+
// would be (14px from the panel's left edge).
|
|
237
|
+
'before:content-[""] before:absolute before:left-[14px] before:top-1 before:bottom-1',
|
|
238
|
+
'before:w-px before:bg-rule',
|
|
239
|
+
)}
|
|
240
|
+
>
|
|
241
|
+
<NavNestedContext.Provider value={true}>{children}</NavNestedContext.Provider>
|
|
242
|
+
</div>
|
|
243
|
+
</DisclosurePanel>
|
|
244
|
+
</Disclosure>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button } from '../button/button'
|
|
5
|
+
import {
|
|
6
|
+
Popover,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverPanelBody,
|
|
9
|
+
PopoverPanelFooter,
|
|
10
|
+
PopoverPanelHeader,
|
|
11
|
+
} from './popover'
|
|
12
|
+
|
|
13
|
+
const meta: Meta = {
|
|
14
|
+
title: 'Components/Popover',
|
|
15
|
+
decorators: [
|
|
16
|
+
(Story) => (
|
|
17
|
+
<div className="p-12">
|
|
18
|
+
<Story />
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|
|
25
|
+
type Story = StoryObj
|
|
26
|
+
|
|
27
|
+
export const DialogTriggerMode: Story = {
|
|
28
|
+
render: () => (
|
|
29
|
+
<Popover>
|
|
30
|
+
<Button variant="ghost">Open menu</Button>
|
|
31
|
+
<PopoverContent panelClassName="w-72">
|
|
32
|
+
<PopoverPanelHeader>
|
|
33
|
+
<span className="font-mono text-[10.5px] uppercase tracking-[0.08em] text-ink-4">
|
|
34
|
+
Quick actions
|
|
35
|
+
</span>
|
|
36
|
+
</PopoverPanelHeader>
|
|
37
|
+
<PopoverPanelBody>
|
|
38
|
+
<div className="grid gap-1.5 text-[13px] text-ink-2">
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
className="rounded-sm px-2.5 py-1.5 text-left hover:bg-surface-2 cursor-pointer"
|
|
42
|
+
>
|
|
43
|
+
Edit profile
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
className="rounded-sm px-2.5 py-1.5 text-left hover:bg-surface-2 cursor-pointer"
|
|
48
|
+
>
|
|
49
|
+
Manage workspace
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
className="rounded-sm px-2.5 py-1.5 text-left hover:bg-surface-2 cursor-pointer"
|
|
54
|
+
>
|
|
55
|
+
Sign out
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
</PopoverPanelBody>
|
|
59
|
+
<PopoverPanelFooter>
|
|
60
|
+
<span className="font-mono text-[11px] text-ink-4">⌘K</span>
|
|
61
|
+
</PopoverPanelFooter>
|
|
62
|
+
</PopoverContent>
|
|
63
|
+
</Popover>
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const ControlledMode: Story = {
|
|
68
|
+
render: () => {
|
|
69
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
70
|
+
const [open, setOpen] = useState(false)
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<button
|
|
74
|
+
ref={ref}
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={() => setOpen((v) => !v)}
|
|
77
|
+
className="inline-flex items-center gap-2 rounded-md border border-rule bg-surface px-3 py-2 text-[13px] text-ink-2 cursor-pointer hover:border-ink-4"
|
|
78
|
+
>
|
|
79
|
+
Custom trigger (chip-style)
|
|
80
|
+
</button>
|
|
81
|
+
<PopoverContent triggerRef={ref} isOpen={open} onOpenChange={setOpen} panelClassName="w-72">
|
|
82
|
+
<PopoverPanelBody>
|
|
83
|
+
<p className="text-[13px] text-ink-2">
|
|
84
|
+
This popover is anchored to a non-Button trigger via
|
|
85
|
+
<code className="font-mono text-[12px] mx-1">triggerRef</code>.
|
|
86
|
+
</p>
|
|
87
|
+
</PopoverPanelBody>
|
|
88
|
+
</PopoverContent>
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
},
|
|
92
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ReactNode, RefObject } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Dialog as AriaDialog,
|
|
4
|
+
type DialogProps as AriaDialogProps,
|
|
5
|
+
DialogTrigger as AriaDialogTrigger,
|
|
6
|
+
type DialogTriggerProps as AriaDialogTriggerProps,
|
|
7
|
+
Popover as AriaPopover,
|
|
8
|
+
type PopoverProps as AriaPopoverProps,
|
|
9
|
+
} from 'react-aria-components'
|
|
10
|
+
|
|
11
|
+
import { cn } from '../../lib/cn'
|
|
12
|
+
|
|
13
|
+
/* =========================================================================
|
|
14
|
+
Popover — generic anchored panel. Two usage modes:
|
|
15
|
+
|
|
16
|
+
1. DialogTrigger mode (simplest, for Button-driven popovers):
|
|
17
|
+
<Popover>
|
|
18
|
+
<Button>Open</Button> // first child = trigger
|
|
19
|
+
<PopoverContent>…panel…</PopoverContent>
|
|
20
|
+
</Popover>
|
|
21
|
+
|
|
22
|
+
2. Controlled mode (for non-Button triggers, e.g. clickable chips):
|
|
23
|
+
const ref = useRef<HTMLSpanElement>(null)
|
|
24
|
+
const [open, setOpen] = useState(false)
|
|
25
|
+
…
|
|
26
|
+
<span ref={ref} onClick={…}>chip</span>
|
|
27
|
+
<PopoverContent
|
|
28
|
+
triggerRef={ref}
|
|
29
|
+
isOpen={open}
|
|
30
|
+
onOpenChange={setOpen}
|
|
31
|
+
>…panel…</PopoverContent>
|
|
32
|
+
|
|
33
|
+
Internally wraps RAC `Popover` + `Dialog` so focus management, outside-
|
|
34
|
+
click, and Escape are handled correctly.
|
|
35
|
+
========================================================================= */
|
|
36
|
+
|
|
37
|
+
export function Popover(props: AriaDialogTriggerProps) {
|
|
38
|
+
return <AriaDialogTrigger {...props} />
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type PopoverContentProps = Omit<AriaPopoverProps, 'children'> & {
|
|
42
|
+
/** Dialog children. Supports the render-prop form `({ close }) => …`. */
|
|
43
|
+
children: AriaDialogProps['children']
|
|
44
|
+
/** Visual override for the floating panel. */
|
|
45
|
+
panelClassName?: string
|
|
46
|
+
/** When in controlled mode, the element the popover should anchor to. */
|
|
47
|
+
triggerRef?: RefObject<Element | null>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function PopoverContent({
|
|
51
|
+
children,
|
|
52
|
+
className,
|
|
53
|
+
panelClassName,
|
|
54
|
+
offset = 6,
|
|
55
|
+
placement = 'bottom start',
|
|
56
|
+
...props
|
|
57
|
+
}: PopoverContentProps) {
|
|
58
|
+
return (
|
|
59
|
+
<AriaPopover
|
|
60
|
+
offset={offset}
|
|
61
|
+
placement={placement}
|
|
62
|
+
{...props}
|
|
63
|
+
className={(renderProps) =>
|
|
64
|
+
cn(
|
|
65
|
+
'rounded-md border border-rule bg-surface',
|
|
66
|
+
'shadow-[var(--shadow-float)]',
|
|
67
|
+
'entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1',
|
|
68
|
+
'exiting:animate-out exiting:fade-out-0 exiting:slide-out-to-top-1',
|
|
69
|
+
panelClassName,
|
|
70
|
+
typeof className === 'function' ? className(renderProps) : className,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
<AriaDialog className="outline-none">{children}</AriaDialog>
|
|
75
|
+
</AriaPopover>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* =========================================================================
|
|
80
|
+
PopoverPanelHeader / PopoverPanelBody / PopoverPanelFooter — slot
|
|
81
|
+
primitives mirroring Modal's split. Consumers compose these to keep
|
|
82
|
+
padding consistent across popovers.
|
|
83
|
+
========================================================================= */
|
|
84
|
+
|
|
85
|
+
export function PopoverPanelHeader({
|
|
86
|
+
className,
|
|
87
|
+
children,
|
|
88
|
+
}: {
|
|
89
|
+
className?: string
|
|
90
|
+
children?: ReactNode
|
|
91
|
+
}) {
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
className={cn(
|
|
95
|
+
'flex items-center justify-between gap-2',
|
|
96
|
+
'px-3.5 py-2.5 border-b border-rule',
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function PopoverPanelBody({
|
|
106
|
+
className,
|
|
107
|
+
children,
|
|
108
|
+
}: {
|
|
109
|
+
className?: string
|
|
110
|
+
children?: ReactNode
|
|
111
|
+
}) {
|
|
112
|
+
return <div className={cn('p-2', className)}>{children}</div>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function PopoverPanelFooter({
|
|
116
|
+
className,
|
|
117
|
+
children,
|
|
118
|
+
}: {
|
|
119
|
+
className?: string
|
|
120
|
+
children?: ReactNode
|
|
121
|
+
}) {
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
className={cn(
|
|
125
|
+
'flex items-center justify-between gap-2',
|
|
126
|
+
'px-3.5 py-2.5 border-t border-rule',
|
|
127
|
+
'bg-[color-mix(in_oklab,var(--color-surface-2)_60%,var(--color-surface))]',
|
|
128
|
+
'rounded-b-md',
|
|
129
|
+
className,
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|