@mostrom/app-shell 0.1.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/.claude/ralph-loop.local.md +9 -0
- package/README.md +172 -0
- package/bin/init.js +269 -0
- package/bun.lock +401 -0
- package/components.json +28 -0
- package/package.json +74 -0
- package/scripts/publish-npm.sh +202 -0
- package/src/AppShell.tsx +847 -0
- package/src/components/PageHeader.tsx +160 -0
- package/src/components/data-table/README.md +447 -0
- package/src/components/data-table/data-table-preferences.tsx +184 -0
- package/src/components/data-table/data-table-toolbar.tsx +118 -0
- package/src/components/data-table/data-table.tsx +37 -0
- package/src/components/data-table/index.ts +32 -0
- package/src/components/global-header/AllServicesButton.tsx +127 -0
- package/src/components/global-header/CategoriesButton.tsx +120 -0
- package/src/components/global-header/GlobalHeader.tsx +59 -0
- package/src/components/global-header/GlobalHeaderSearch.tsx +57 -0
- package/src/components/global-header/HeaderUtilities.tsx +243 -0
- package/src/components/global-header/ServicesMenu.tsx +246 -0
- package/src/components/layout/AppBreadcrumb.tsx +70 -0
- package/src/components/layout/AppFlashbar.tsx +95 -0
- package/src/components/layout/AppLayout.tsx +271 -0
- package/src/components/layout/AppNavigation.tsx +313 -0
- package/src/components/layout/AppSidebar.tsx +229 -0
- package/src/components/patterns/index.ts +14 -0
- package/src/components/patterns/p-alert-5.tsx +19 -0
- package/src/components/patterns/p-autocomplete-5.tsx +89 -0
- package/src/components/patterns/p-breadcrumb-1.tsx +28 -0
- package/src/components/patterns/p-button-42.tsx +37 -0
- package/src/components/patterns/p-button-51.tsx +14 -0
- package/src/components/patterns/p-button-6.tsx +5 -0
- package/src/components/patterns/p-calendar-1.tsx +18 -0
- package/src/components/patterns/p-card-1.tsx +33 -0
- package/src/components/patterns/p-card-2.tsx +26 -0
- package/src/components/patterns/p-card-5.tsx +31 -0
- package/src/components/patterns/p-collapsible-7.tsx +121 -0
- package/src/components/patterns/p-command-6.tsx +113 -0
- package/src/components/patterns/p-dialog-1.tsx +56 -0
- package/src/components/patterns/p-dropdown-menu-1.tsx +38 -0
- package/src/components/patterns/p-dropdown-menu-11.tsx +122 -0
- package/src/components/patterns/p-dropdown-menu-14.tsx +165 -0
- package/src/components/patterns/p-dropdown-menu-9.tsx +108 -0
- package/src/components/patterns/p-empty-2.tsx +34 -0
- package/src/components/patterns/p-file-upload-1.tsx +72 -0
- package/src/components/patterns/p-filters-1.tsx +666 -0
- package/src/components/patterns/p-frame-2.tsx +26 -0
- package/src/components/patterns/p-tabs-2.tsx +129 -0
- package/src/components/reui/alert.tsx +92 -0
- package/src/components/reui/autocomplete.tsx +343 -0
- package/src/components/reui/badge.tsx +87 -0
- package/src/components/reui/data-grid/data-grid-column-filter.tsx +165 -0
- package/src/components/reui/data-grid/data-grid-column-header.tsx +339 -0
- package/src/components/reui/data-grid/data-grid-column-visibility.tsx +55 -0
- package/src/components/reui/data-grid/data-grid-pagination.tsx +224 -0
- package/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx +260 -0
- package/src/components/reui/data-grid/data-grid-table-dnd.tsx +253 -0
- package/src/components/reui/data-grid/data-grid-table.tsx +639 -0
- package/src/components/reui/data-grid/data-grid.tsx +209 -0
- package/src/components/reui/date-selector.tsx +1330 -0
- package/src/components/reui/filters.tsx +1869 -0
- package/src/components/reui/frame.tsx +134 -0
- package/src/components/reui/index.ts +17 -0
- package/src/components/reui/timeline.tsx +219 -0
- package/src/components/search/Autocomplete.tsx +183 -0
- package/src/components/search/AutocompleteClient.tsx +293 -0
- package/src/components/search/GlobalSearch.tsx +187 -0
- package/src/components/section-drawer/deal-drawer-content.tsx +891 -0
- package/src/components/section-drawer/index.ts +19 -0
- package/src/components/section-drawer/section-drawer.css +665 -0
- package/src/components/section-drawer/section-drawer.tsx +467 -0
- package/src/components/sectioned-list-board/README.md +78 -0
- package/src/components/sectioned-list-board/board-card-content.tsx +340 -0
- package/src/components/sectioned-list-board/date-range-filter.tsx +249 -0
- package/src/components/sectioned-list-board/index.ts +19 -0
- package/src/components/sectioned-list-board/sectioned-list-board.css +564 -0
- package/src/components/sectioned-list-board/sectioned-list-board.tsx +731 -0
- package/src/components/sectioned-list-board/sortable-card.tsx +314 -0
- package/src/components/sectioned-list-board/sortable-section.tsx +319 -0
- package/src/components/sectioned-list-board/types.ts +216 -0
- package/src/components/sectioned-list-table/README.md +80 -0
- package/src/components/sectioned-list-table/index.ts +14 -0
- package/src/components/sectioned-list-table/sectioned-list-table.css +534 -0
- package/src/components/sectioned-list-table/sectioned-list-table.tsx +740 -0
- package/src/components/sectioned-list-table/sortable-column-header.tsx +120 -0
- package/src/components/sectioned-list-table/sortable-row.tsx +420 -0
- package/src/components/sectioned-list-table/sortable-section.tsx +251 -0
- package/src/components/sectioned-list-table/table-cell-content.tsx +129 -0
- package/src/components/sectioned-list-table/types.ts +120 -0
- package/src/components/sectioned-list-table/use-column-preferences.ts +103 -0
- package/src/components/ui/actions-dropdown.tsx +109 -0
- package/src/components/ui/assignee-selector.tsx +209 -0
- package/src/components/ui/avatar.tsx +107 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/chart.tsx +376 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +182 -0
- package/src/components/ui/context-menu.tsx +250 -0
- package/src/components/ui/create-button-group.tsx +128 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/drawer.tsx +133 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +165 -0
- package/src/components/ui/index.ts +37 -0
- package/src/components/ui/input-group.tsx +168 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/page-header.tsx +80 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +141 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +38 -0
- package/src/components/ui/switch.tsx +33 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/hooks/use-copy-to-clipboard.ts +37 -0
- package/src/hooks/use-file-upload.ts +415 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +95 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +1859 -0
- package/src/urls.ts +83 -0
- package/src/vite.d.ts +22 -0
- package/src/vite.js +241 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import type { Table } from "@tanstack/react-table"
|
|
5
|
+
import { SettingsIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
import { Checkbox } from "@/components/ui/checkbox"
|
|
9
|
+
import { Label } from "@/components/ui/label"
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogClose,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogDescription,
|
|
15
|
+
DialogFooter,
|
|
16
|
+
DialogHeader,
|
|
17
|
+
DialogTitle,
|
|
18
|
+
DialogTrigger,
|
|
19
|
+
} from "@/components/ui/dialog"
|
|
20
|
+
import {
|
|
21
|
+
Select,
|
|
22
|
+
SelectContent,
|
|
23
|
+
SelectItem,
|
|
24
|
+
SelectTrigger,
|
|
25
|
+
SelectValue,
|
|
26
|
+
} from "@/components/ui/select"
|
|
27
|
+
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
|
|
28
|
+
|
|
29
|
+
export interface DataTablePreferences {
|
|
30
|
+
pageSize: number
|
|
31
|
+
columnDisplay: { id: string; visible: boolean }[]
|
|
32
|
+
stripedRows: boolean
|
|
33
|
+
contentDensity: "comfortable" | "compact"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DataTablePreferencesProps<TData> {
|
|
37
|
+
table: Table<TData>
|
|
38
|
+
preferences: DataTablePreferences
|
|
39
|
+
onPreferencesChange: (preferences: DataTablePreferences) => void
|
|
40
|
+
pageSizeOptions?: number[]
|
|
41
|
+
trigger?: React.ReactNode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function DataTablePreferencesDialog<TData>({
|
|
45
|
+
table,
|
|
46
|
+
preferences,
|
|
47
|
+
onPreferencesChange,
|
|
48
|
+
pageSizeOptions = [10, 20, 50],
|
|
49
|
+
trigger,
|
|
50
|
+
}: DataTablePreferencesProps<TData>) {
|
|
51
|
+
const [open, setOpen] = React.useState(false)
|
|
52
|
+
const [localPreferences, setLocalPreferences] = React.useState(preferences)
|
|
53
|
+
|
|
54
|
+
// Sync local preferences when dialog opens
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (open) {
|
|
57
|
+
setLocalPreferences(preferences)
|
|
58
|
+
}
|
|
59
|
+
}, [open, preferences])
|
|
60
|
+
|
|
61
|
+
// Generate column display options from table
|
|
62
|
+
const displayOptions = React.useMemo(() => {
|
|
63
|
+
return table
|
|
64
|
+
.getAllColumns()
|
|
65
|
+
.filter((col) => typeof col.accessorFn !== "undefined" && col.getCanHide())
|
|
66
|
+
.map((col) => ({
|
|
67
|
+
id: col.id,
|
|
68
|
+
label: (col.columnDef.meta as { headerTitle?: string })?.headerTitle || col.id,
|
|
69
|
+
}))
|
|
70
|
+
}, [table])
|
|
71
|
+
|
|
72
|
+
const handleConfirm = () => {
|
|
73
|
+
onPreferencesChange(localPreferences)
|
|
74
|
+
setOpen(false)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const handleColumnVisibilityChange = (columnId: string, visible: boolean) => {
|
|
78
|
+
const newColumnDisplay = localPreferences.columnDisplay.map((col) =>
|
|
79
|
+
col.id === columnId ? { ...col, visible } : col
|
|
80
|
+
)
|
|
81
|
+
setLocalPreferences({ ...localPreferences, columnDisplay: newColumnDisplay })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
86
|
+
<DialogTrigger asChild>
|
|
87
|
+
{trigger || (
|
|
88
|
+
<Button variant="outline" size="icon">
|
|
89
|
+
<SettingsIcon className="size-4" />
|
|
90
|
+
<span className="sr-only">Preferences</span>
|
|
91
|
+
</Button>
|
|
92
|
+
)}
|
|
93
|
+
</DialogTrigger>
|
|
94
|
+
<DialogContent>
|
|
95
|
+
<DialogHeader>
|
|
96
|
+
<DialogTitle>Preferences</DialogTitle>
|
|
97
|
+
<DialogDescription>
|
|
98
|
+
Customize how the table displays data.
|
|
99
|
+
</DialogDescription>
|
|
100
|
+
</DialogHeader>
|
|
101
|
+
|
|
102
|
+
<FieldGroup>
|
|
103
|
+
{/* Page Size */}
|
|
104
|
+
<Field>
|
|
105
|
+
<FieldLabel>Page size</FieldLabel>
|
|
106
|
+
<Select
|
|
107
|
+
value={String(localPreferences.pageSize)}
|
|
108
|
+
onValueChange={(value) =>
|
|
109
|
+
setLocalPreferences({ ...localPreferences, pageSize: Number(value) })
|
|
110
|
+
}
|
|
111
|
+
>
|
|
112
|
+
<SelectTrigger className="w-full">
|
|
113
|
+
<SelectValue />
|
|
114
|
+
</SelectTrigger>
|
|
115
|
+
<SelectContent>
|
|
116
|
+
{pageSizeOptions.map((size) => (
|
|
117
|
+
<SelectItem key={size} value={String(size)}>
|
|
118
|
+
{size} items
|
|
119
|
+
</SelectItem>
|
|
120
|
+
))}
|
|
121
|
+
</SelectContent>
|
|
122
|
+
</Select>
|
|
123
|
+
</Field>
|
|
124
|
+
|
|
125
|
+
{/* Content Density */}
|
|
126
|
+
<Field>
|
|
127
|
+
<FieldLabel>Content density</FieldLabel>
|
|
128
|
+
<Select
|
|
129
|
+
value={localPreferences.contentDensity}
|
|
130
|
+
onValueChange={(value: "comfortable" | "compact") =>
|
|
131
|
+
setLocalPreferences({ ...localPreferences, contentDensity: value })
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
<SelectTrigger className="w-full">
|
|
135
|
+
<SelectValue />
|
|
136
|
+
</SelectTrigger>
|
|
137
|
+
<SelectContent>
|
|
138
|
+
<SelectItem value="comfortable">Comfortable</SelectItem>
|
|
139
|
+
<SelectItem value="compact">Compact</SelectItem>
|
|
140
|
+
</SelectContent>
|
|
141
|
+
</Select>
|
|
142
|
+
</Field>
|
|
143
|
+
|
|
144
|
+
{/* Column Display */}
|
|
145
|
+
{displayOptions.length > 0 && (
|
|
146
|
+
<Field>
|
|
147
|
+
<FieldLabel>Column display</FieldLabel>
|
|
148
|
+
<div className="border-border space-y-2 rounded-lg border p-3">
|
|
149
|
+
{displayOptions.map((option) => {
|
|
150
|
+
const columnPref = localPreferences.columnDisplay.find(
|
|
151
|
+
(col) => col.id === option.id
|
|
152
|
+
)
|
|
153
|
+
const isVisible = columnPref?.visible ?? true
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div key={option.id} className="flex items-center gap-2">
|
|
157
|
+
<Checkbox
|
|
158
|
+
id={`col-${option.id}`}
|
|
159
|
+
checked={isVisible}
|
|
160
|
+
onCheckedChange={(checked) =>
|
|
161
|
+
handleColumnVisibilityChange(option.id, !!checked)
|
|
162
|
+
}
|
|
163
|
+
/>
|
|
164
|
+
<Label htmlFor={`col-${option.id}`} className="text-sm font-normal">
|
|
165
|
+
{option.label}
|
|
166
|
+
</Label>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
})}
|
|
170
|
+
</div>
|
|
171
|
+
</Field>
|
|
172
|
+
)}
|
|
173
|
+
</FieldGroup>
|
|
174
|
+
|
|
175
|
+
<DialogFooter>
|
|
176
|
+
<DialogClose asChild>
|
|
177
|
+
<Button variant="outline">Cancel</Button>
|
|
178
|
+
</DialogClose>
|
|
179
|
+
<Button onClick={handleConfirm}>Confirm</Button>
|
|
180
|
+
</DialogFooter>
|
|
181
|
+
</DialogContent>
|
|
182
|
+
</Dialog>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import type { Table } from "@tanstack/react-table"
|
|
5
|
+
import { Settings2Icon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
import { Button } from "@/components/ui/button"
|
|
9
|
+
import { Input } from "@/components/ui/input"
|
|
10
|
+
import { DataGridColumnVisibility } from "@/components/reui/data-grid/data-grid-column-visibility"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* DataTableToolbar - Toolbar for table-specific controls.
|
|
14
|
+
*
|
|
15
|
+
* IMPORTANT: This toolbar should only contain table-specific controls like:
|
|
16
|
+
* - Search/filter input
|
|
17
|
+
* - Column visibility toggle
|
|
18
|
+
* - Selection-based bulk actions (e.g., "Delete Selected", "Archive Selected")
|
|
19
|
+
*
|
|
20
|
+
* For create/import/export actions, use CreateButtonGroup OUTSIDE the table.
|
|
21
|
+
* Place it above the DataGrid component, not inside the toolbar.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <div className="space-y-4">
|
|
26
|
+
* <div className="flex items-center justify-between">
|
|
27
|
+
* <h1>Items</h1>
|
|
28
|
+
* <CreateButtonGroup
|
|
29
|
+
* createLabel="Create Item"
|
|
30
|
+
* onImport={() => {}}
|
|
31
|
+
* onExport={() => {}}
|
|
32
|
+
* />
|
|
33
|
+
* </div>
|
|
34
|
+
* <DataGrid table={table}>
|
|
35
|
+
* <DataTableToolbar
|
|
36
|
+
* table={table}
|
|
37
|
+
* bulkActions={
|
|
38
|
+
* selectedCount > 0 && (
|
|
39
|
+
* <Button variant="destructive" size="sm">
|
|
40
|
+
* Delete Selected ({selectedCount})
|
|
41
|
+
* </Button>
|
|
42
|
+
* )
|
|
43
|
+
* }
|
|
44
|
+
* />
|
|
45
|
+
* ...
|
|
46
|
+
* </DataGrid>
|
|
47
|
+
* </div>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export interface DataTableToolbarProps<TData> {
|
|
51
|
+
table?: Table<TData>
|
|
52
|
+
filterValue?: string
|
|
53
|
+
onFilterChange?: (value: string) => void
|
|
54
|
+
filterPlaceholder?: string
|
|
55
|
+
showColumnVisibility?: boolean
|
|
56
|
+
selectedCount?: number
|
|
57
|
+
totalCount?: number
|
|
58
|
+
/**
|
|
59
|
+
* Selection-based bulk actions only (e.g., "Delete Selected", "Archive Selected").
|
|
60
|
+
* For create/import/export, use CreateButtonGroup above the table instead.
|
|
61
|
+
*/
|
|
62
|
+
bulkActions?: React.ReactNode
|
|
63
|
+
className?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function DataTableToolbar<TData>({
|
|
67
|
+
table,
|
|
68
|
+
filterValue = "",
|
|
69
|
+
onFilterChange,
|
|
70
|
+
filterPlaceholder = "Search...",
|
|
71
|
+
showColumnVisibility = true,
|
|
72
|
+
selectedCount,
|
|
73
|
+
totalCount,
|
|
74
|
+
bulkActions,
|
|
75
|
+
className,
|
|
76
|
+
}: DataTableToolbarProps<TData>) {
|
|
77
|
+
const showCounter = typeof selectedCount === "number" && typeof totalCount === "number"
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={cn("flex items-center justify-between gap-2", className)}>
|
|
81
|
+
<div className="flex flex-1 items-center gap-2">
|
|
82
|
+
{/* Search Input */}
|
|
83
|
+
<div className="relative max-w-sm flex-1">
|
|
84
|
+
<Input
|
|
85
|
+
placeholder={filterPlaceholder}
|
|
86
|
+
value={filterValue}
|
|
87
|
+
onChange={(e) => onFilterChange?.(e.target.value)}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Selection counter */}
|
|
92
|
+
{showCounter && selectedCount > 0 && (
|
|
93
|
+
<span className="text-muted-foreground text-sm">
|
|
94
|
+
{selectedCount} of {totalCount} selected
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
{/* Selection-based bulk actions */}
|
|
101
|
+
{bulkActions}
|
|
102
|
+
|
|
103
|
+
{/* Column visibility toggle */}
|
|
104
|
+
{showColumnVisibility && table && (
|
|
105
|
+
<DataGridColumnVisibility
|
|
106
|
+
table={table}
|
|
107
|
+
trigger={
|
|
108
|
+
<Button variant="outline" size="sm">
|
|
109
|
+
<Settings2Icon className="mr-2 size-4" />
|
|
110
|
+
Columns
|
|
111
|
+
</Button>
|
|
112
|
+
}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Data Table Components
|
|
5
|
+
*
|
|
6
|
+
* Re-exports from reui/data-grid for convenient access.
|
|
7
|
+
* Use these components directly following the patterns in the examples.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Core data grid components
|
|
11
|
+
export { DataGrid, DataGridContainer } from "@/components/reui/data-grid/data-grid"
|
|
12
|
+
export { DataGridTable, DataGridTableRowSelect, DataGridTableRowSelectAll } from "@/components/reui/data-grid/data-grid-table"
|
|
13
|
+
export { DataGridTableDnd } from "@/components/reui/data-grid/data-grid-table-dnd"
|
|
14
|
+
export { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
|
|
15
|
+
export { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
|
|
16
|
+
export { DataGridColumnVisibility } from "@/components/reui/data-grid/data-grid-column-visibility"
|
|
17
|
+
|
|
18
|
+
// Types from data-grid
|
|
19
|
+
export type { DataGridProps, DataGridApiFetchParams, DataGridApiResponse } from "@/components/reui/data-grid/data-grid"
|
|
20
|
+
|
|
21
|
+
// TanStack Table re-exports
|
|
22
|
+
export {
|
|
23
|
+
useReactTable,
|
|
24
|
+
getCoreRowModel,
|
|
25
|
+
getFilteredRowModel,
|
|
26
|
+
getPaginationRowModel,
|
|
27
|
+
getSortedRowModel,
|
|
28
|
+
} from "@tanstack/react-table"
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
ColumnDef,
|
|
32
|
+
SortingState,
|
|
33
|
+
PaginationState,
|
|
34
|
+
RowSelectionState,
|
|
35
|
+
VisibilityState,
|
|
36
|
+
ColumnFiltersState,
|
|
37
|
+
} from "@tanstack/react-table"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Table Components
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from reui/data-grid for convenient access.
|
|
5
|
+
* Use these components directly following the patterns in the examples.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: CreateButtonGroup should be placed OUTSIDE/ABOVE the table,
|
|
8
|
+
* not inside the toolbar. The toolbar's bulkActions prop is only for
|
|
9
|
+
* selection-based actions like "Delete Selected".
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Re-export core data-grid components
|
|
13
|
+
export * from "./data-table"
|
|
14
|
+
|
|
15
|
+
// Toolbar
|
|
16
|
+
export { DataTableToolbar } from "./data-table-toolbar"
|
|
17
|
+
export type { DataTableToolbarProps } from "./data-table-toolbar"
|
|
18
|
+
|
|
19
|
+
// Preferences dialog
|
|
20
|
+
export { DataTablePreferencesDialog } from "./data-table-preferences"
|
|
21
|
+
export type {
|
|
22
|
+
DataTablePreferences as DataTablePreferencesType,
|
|
23
|
+
DataTablePreferencesProps,
|
|
24
|
+
} from "./data-table-preferences"
|
|
25
|
+
|
|
26
|
+
// Create button group (for use ABOVE the table, not in toolbar)
|
|
27
|
+
export { CreateButtonGroup } from "../ui/create-button-group"
|
|
28
|
+
export type { CreateButtonGroupProps } from "../ui/create-button-group"
|
|
29
|
+
|
|
30
|
+
// Actions dropdown (replaces Cloudscape ButtonDropdown)
|
|
31
|
+
export { ActionsDropdown } from "../ui/actions-dropdown"
|
|
32
|
+
export type { ActionsDropdownProps, ActionsDropdownItem } from "../ui/actions-dropdown"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuGroup,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuLabel,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "../ui/dropdown-menu";
|
|
14
|
+
import type {
|
|
15
|
+
MenuDropdownItems,
|
|
16
|
+
MenuDropdownItem,
|
|
17
|
+
MenuDropdownItemGroup,
|
|
18
|
+
MenuItemClickHandler,
|
|
19
|
+
} from "./ServicesMenu";
|
|
20
|
+
|
|
21
|
+
type AllServicesButtonProps = {
|
|
22
|
+
items: MenuDropdownItems;
|
|
23
|
+
onItemClick?: MenuItemClickHandler;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DotsNineIcon = () => (
|
|
27
|
+
<svg
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
focusable="false"
|
|
30
|
+
width="24"
|
|
31
|
+
height="24"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
>
|
|
34
|
+
{([5, 12, 19] as const).flatMap((cx) =>
|
|
35
|
+
([5, 12, 19] as const).map((cy) => (
|
|
36
|
+
<circle key={`${cx}-${cy}`} cx={cx} cy={cy} r={1.6} fill="currentColor" />
|
|
37
|
+
)),
|
|
38
|
+
)}
|
|
39
|
+
</svg>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const isGroup = (item: MenuDropdownItem | MenuDropdownItemGroup): item is MenuDropdownItemGroup =>
|
|
43
|
+
typeof (item as MenuDropdownItemGroup).items !== "undefined";
|
|
44
|
+
|
|
45
|
+
export function AllServicesButton({ items, onItemClick }: AllServicesButtonProps) {
|
|
46
|
+
const handleItemClick = (item: MenuDropdownItem) => {
|
|
47
|
+
if (item.disabled) return;
|
|
48
|
+
onItemClick?.({
|
|
49
|
+
id: item.id,
|
|
50
|
+
href: item.href,
|
|
51
|
+
external: item.external,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const renderItem = (item: MenuDropdownItem) => {
|
|
56
|
+
const content = (
|
|
57
|
+
<>
|
|
58
|
+
<span>{item.text}</span>
|
|
59
|
+
{item.description && (
|
|
60
|
+
<span className="text-xs text-muted-foreground ml-2">{item.description}</span>
|
|
61
|
+
)}
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (item.href) {
|
|
66
|
+
return (
|
|
67
|
+
<DropdownMenuItem key={item.id} asChild disabled={item.disabled}>
|
|
68
|
+
<a
|
|
69
|
+
href={item.href}
|
|
70
|
+
target={item.external ? "_blank" : undefined}
|
|
71
|
+
rel={item.external ? "noopener noreferrer" : undefined}
|
|
72
|
+
onClick={() => handleItemClick(item)}
|
|
73
|
+
>
|
|
74
|
+
{content}
|
|
75
|
+
</a>
|
|
76
|
+
</DropdownMenuItem>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<DropdownMenuItem
|
|
82
|
+
key={item.id}
|
|
83
|
+
disabled={item.disabled}
|
|
84
|
+
onSelect={() => handleItemClick(item)}
|
|
85
|
+
>
|
|
86
|
+
{content}
|
|
87
|
+
</DropdownMenuItem>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="app-shell-apps-button">
|
|
93
|
+
<DropdownMenu>
|
|
94
|
+
<DropdownMenuTrigger asChild>
|
|
95
|
+
<Button
|
|
96
|
+
variant="ghost"
|
|
97
|
+
size="icon"
|
|
98
|
+
aria-label="Apps"
|
|
99
|
+
className="h-9 w-9 hover:bg-transparent"
|
|
100
|
+
>
|
|
101
|
+
<span className="app-shell-apps-icon">
|
|
102
|
+
<DotsNineIcon />
|
|
103
|
+
</span>
|
|
104
|
+
</Button>
|
|
105
|
+
</DropdownMenuTrigger>
|
|
106
|
+
<DropdownMenuContent align="start" sideOffset={8} className="w-64 max-h-96 overflow-y-auto">
|
|
107
|
+
{items.map((item, index) => {
|
|
108
|
+
if (isGroup(item)) {
|
|
109
|
+
return (
|
|
110
|
+
<React.Fragment key={item.text || `group-${index}`}>
|
|
111
|
+
{index > 0 && <DropdownMenuSeparator />}
|
|
112
|
+
<DropdownMenuGroup>
|
|
113
|
+
{item.text && (
|
|
114
|
+
<DropdownMenuLabel>{item.text}</DropdownMenuLabel>
|
|
115
|
+
)}
|
|
116
|
+
{item.items.map((subItem) => renderItem(subItem as MenuDropdownItem))}
|
|
117
|
+
</DropdownMenuGroup>
|
|
118
|
+
</React.Fragment>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return renderItem(item as MenuDropdownItem);
|
|
122
|
+
})}
|
|
123
|
+
</DropdownMenuContent>
|
|
124
|
+
</DropdownMenu>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "../ui/dropdown-menu";
|
|
11
|
+
import type {
|
|
12
|
+
MenuDropdownItems,
|
|
13
|
+
MenuDropdownItem,
|
|
14
|
+
MenuDropdownItemGroup,
|
|
15
|
+
MenuItemClickHandler,
|
|
16
|
+
} from "./ServicesMenu";
|
|
17
|
+
|
|
18
|
+
type CategoriesButtonProps = {
|
|
19
|
+
items: MenuDropdownItems;
|
|
20
|
+
onItemClick?: MenuItemClickHandler;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const SquaresFourIcon = () => (
|
|
24
|
+
<svg
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
focusable="false"
|
|
27
|
+
width="22"
|
|
28
|
+
height="22"
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
>
|
|
31
|
+
<rect x="4" y="4" width="7" height="7" rx="1.4" fill="currentColor" />
|
|
32
|
+
<rect x="13" y="4" width="7" height="7" rx="1.4" fill="currentColor" />
|
|
33
|
+
<rect x="4" y="13" width="7" height="7" rx="1.4" fill="currentColor" />
|
|
34
|
+
<rect x="13" y="13" width="7" height="7" rx="1.4" fill="currentColor" />
|
|
35
|
+
</svg>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const isGroup = (item: MenuDropdownItem | MenuDropdownItemGroup): item is MenuDropdownItemGroup =>
|
|
39
|
+
typeof (item as MenuDropdownItemGroup).items !== "undefined";
|
|
40
|
+
|
|
41
|
+
const flattenItems = (items: MenuDropdownItems): MenuDropdownItem[] => {
|
|
42
|
+
const result: MenuDropdownItem[] = [];
|
|
43
|
+
items.forEach((item) => {
|
|
44
|
+
if (isGroup(item)) {
|
|
45
|
+
result.push(...flattenItems(item.items));
|
|
46
|
+
} else {
|
|
47
|
+
result.push(item as MenuDropdownItem);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function CategoriesButton({ items, onItemClick }: CategoriesButtonProps) {
|
|
54
|
+
const flatItems = flattenItems(items);
|
|
55
|
+
|
|
56
|
+
const handleItemClick = (item: MenuDropdownItem) => {
|
|
57
|
+
if (item.disabled) return;
|
|
58
|
+
onItemClick?.({
|
|
59
|
+
id: item.id,
|
|
60
|
+
href: item.href,
|
|
61
|
+
external: item.external,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="app-shell-categories-button">
|
|
67
|
+
<DropdownMenu>
|
|
68
|
+
<DropdownMenuTrigger asChild>
|
|
69
|
+
<Button
|
|
70
|
+
variant="ghost"
|
|
71
|
+
size="icon"
|
|
72
|
+
aria-label="Categories"
|
|
73
|
+
className="h-9 w-9 hover:bg-transparent"
|
|
74
|
+
>
|
|
75
|
+
<span className="app-shell-categories-icon">
|
|
76
|
+
<SquaresFourIcon />
|
|
77
|
+
</span>
|
|
78
|
+
</Button>
|
|
79
|
+
</DropdownMenuTrigger>
|
|
80
|
+
<DropdownMenuContent align="start" sideOffset={8} className="w-56">
|
|
81
|
+
{flatItems.map((item) => {
|
|
82
|
+
const content = (
|
|
83
|
+
<>
|
|
84
|
+
<span>{item.text}</span>
|
|
85
|
+
{item.description && (
|
|
86
|
+
<span className="text-xs text-muted-foreground ml-2">{item.description}</span>
|
|
87
|
+
)}
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (item.href) {
|
|
92
|
+
return (
|
|
93
|
+
<DropdownMenuItem key={item.id} asChild disabled={item.disabled}>
|
|
94
|
+
<a
|
|
95
|
+
href={item.href}
|
|
96
|
+
target={item.external ? "_blank" : undefined}
|
|
97
|
+
rel={item.external ? "noopener noreferrer" : undefined}
|
|
98
|
+
onClick={() => handleItemClick(item)}
|
|
99
|
+
>
|
|
100
|
+
{content}
|
|
101
|
+
</a>
|
|
102
|
+
</DropdownMenuItem>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<DropdownMenuItem
|
|
108
|
+
key={item.id}
|
|
109
|
+
disabled={item.disabled}
|
|
110
|
+
onSelect={() => handleItemClick(item)}
|
|
111
|
+
>
|
|
112
|
+
{content}
|
|
113
|
+
</DropdownMenuItem>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</DropdownMenuContent>
|
|
117
|
+
</DropdownMenu>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface GlobalHeaderIdentity {
|
|
4
|
+
/** Logo image configuration */
|
|
5
|
+
logo?: {
|
|
6
|
+
src: string;
|
|
7
|
+
alt: string;
|
|
8
|
+
};
|
|
9
|
+
/** Link when clicking the identity/logo */
|
|
10
|
+
href?: string;
|
|
11
|
+
/** Text to display next to or instead of logo */
|
|
12
|
+
title?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GlobalHeaderProps {
|
|
16
|
+
/** Platform identity/branding shown in the top-left */
|
|
17
|
+
identity?: GlobalHeaderIdentity;
|
|
18
|
+
/** Search area content (center of header) */
|
|
19
|
+
search?: React.ReactNode;
|
|
20
|
+
/** Utility components for the right side of the header */
|
|
21
|
+
utilities?: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function GlobalHeader({ identity, search, utilities }: GlobalHeaderProps) {
|
|
25
|
+
const logoElement = identity?.logo ? (
|
|
26
|
+
<img
|
|
27
|
+
src={identity.logo.src}
|
|
28
|
+
alt={identity.logo.alt}
|
|
29
|
+
className="app-shell-header-logo"
|
|
30
|
+
/>
|
|
31
|
+
) : null;
|
|
32
|
+
|
|
33
|
+
const identityContent = identity?.href ? (
|
|
34
|
+
<a href={identity.href} className="app-shell-header-identity-link">
|
|
35
|
+
{logoElement}
|
|
36
|
+
{identity.title && <span className="app-shell-header-title">{identity.title}</span>}
|
|
37
|
+
</a>
|
|
38
|
+
) : (
|
|
39
|
+
<div className="app-shell-header-identity">
|
|
40
|
+
{logoElement}
|
|
41
|
+
{identity?.title && <span className="app-shell-header-title">{identity.title}</span>}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
// suppressHydrationWarning: browser extensions (Dashlane, 1Password, etc.) inject attributes before hydration
|
|
47
|
+
<header id="global-header" className="app-shell-header" suppressHydrationWarning>
|
|
48
|
+
<div className="app-shell-header-left">
|
|
49
|
+
{identityContent}
|
|
50
|
+
</div>
|
|
51
|
+
<div className="app-shell-header-center">
|
|
52
|
+
{search}
|
|
53
|
+
</div>
|
|
54
|
+
<div className="app-shell-header-right">
|
|
55
|
+
{utilities}
|
|
56
|
+
</div>
|
|
57
|
+
</header>
|
|
58
|
+
);
|
|
59
|
+
}
|