@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.
Files changed (142) hide show
  1. package/.claude/ralph-loop.local.md +9 -0
  2. package/README.md +172 -0
  3. package/bin/init.js +269 -0
  4. package/bun.lock +401 -0
  5. package/components.json +28 -0
  6. package/package.json +74 -0
  7. package/scripts/publish-npm.sh +202 -0
  8. package/src/AppShell.tsx +847 -0
  9. package/src/components/PageHeader.tsx +160 -0
  10. package/src/components/data-table/README.md +447 -0
  11. package/src/components/data-table/data-table-preferences.tsx +184 -0
  12. package/src/components/data-table/data-table-toolbar.tsx +118 -0
  13. package/src/components/data-table/data-table.tsx +37 -0
  14. package/src/components/data-table/index.ts +32 -0
  15. package/src/components/global-header/AllServicesButton.tsx +127 -0
  16. package/src/components/global-header/CategoriesButton.tsx +120 -0
  17. package/src/components/global-header/GlobalHeader.tsx +59 -0
  18. package/src/components/global-header/GlobalHeaderSearch.tsx +57 -0
  19. package/src/components/global-header/HeaderUtilities.tsx +243 -0
  20. package/src/components/global-header/ServicesMenu.tsx +246 -0
  21. package/src/components/layout/AppBreadcrumb.tsx +70 -0
  22. package/src/components/layout/AppFlashbar.tsx +95 -0
  23. package/src/components/layout/AppLayout.tsx +271 -0
  24. package/src/components/layout/AppNavigation.tsx +313 -0
  25. package/src/components/layout/AppSidebar.tsx +229 -0
  26. package/src/components/patterns/index.ts +14 -0
  27. package/src/components/patterns/p-alert-5.tsx +19 -0
  28. package/src/components/patterns/p-autocomplete-5.tsx +89 -0
  29. package/src/components/patterns/p-breadcrumb-1.tsx +28 -0
  30. package/src/components/patterns/p-button-42.tsx +37 -0
  31. package/src/components/patterns/p-button-51.tsx +14 -0
  32. package/src/components/patterns/p-button-6.tsx +5 -0
  33. package/src/components/patterns/p-calendar-1.tsx +18 -0
  34. package/src/components/patterns/p-card-1.tsx +33 -0
  35. package/src/components/patterns/p-card-2.tsx +26 -0
  36. package/src/components/patterns/p-card-5.tsx +31 -0
  37. package/src/components/patterns/p-collapsible-7.tsx +121 -0
  38. package/src/components/patterns/p-command-6.tsx +113 -0
  39. package/src/components/patterns/p-dialog-1.tsx +56 -0
  40. package/src/components/patterns/p-dropdown-menu-1.tsx +38 -0
  41. package/src/components/patterns/p-dropdown-menu-11.tsx +122 -0
  42. package/src/components/patterns/p-dropdown-menu-14.tsx +165 -0
  43. package/src/components/patterns/p-dropdown-menu-9.tsx +108 -0
  44. package/src/components/patterns/p-empty-2.tsx +34 -0
  45. package/src/components/patterns/p-file-upload-1.tsx +72 -0
  46. package/src/components/patterns/p-filters-1.tsx +666 -0
  47. package/src/components/patterns/p-frame-2.tsx +26 -0
  48. package/src/components/patterns/p-tabs-2.tsx +129 -0
  49. package/src/components/reui/alert.tsx +92 -0
  50. package/src/components/reui/autocomplete.tsx +343 -0
  51. package/src/components/reui/badge.tsx +87 -0
  52. package/src/components/reui/data-grid/data-grid-column-filter.tsx +165 -0
  53. package/src/components/reui/data-grid/data-grid-column-header.tsx +339 -0
  54. package/src/components/reui/data-grid/data-grid-column-visibility.tsx +55 -0
  55. package/src/components/reui/data-grid/data-grid-pagination.tsx +224 -0
  56. package/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx +260 -0
  57. package/src/components/reui/data-grid/data-grid-table-dnd.tsx +253 -0
  58. package/src/components/reui/data-grid/data-grid-table.tsx +639 -0
  59. package/src/components/reui/data-grid/data-grid.tsx +209 -0
  60. package/src/components/reui/date-selector.tsx +1330 -0
  61. package/src/components/reui/filters.tsx +1869 -0
  62. package/src/components/reui/frame.tsx +134 -0
  63. package/src/components/reui/index.ts +17 -0
  64. package/src/components/reui/timeline.tsx +219 -0
  65. package/src/components/search/Autocomplete.tsx +183 -0
  66. package/src/components/search/AutocompleteClient.tsx +293 -0
  67. package/src/components/search/GlobalSearch.tsx +187 -0
  68. package/src/components/section-drawer/deal-drawer-content.tsx +891 -0
  69. package/src/components/section-drawer/index.ts +19 -0
  70. package/src/components/section-drawer/section-drawer.css +665 -0
  71. package/src/components/section-drawer/section-drawer.tsx +467 -0
  72. package/src/components/sectioned-list-board/README.md +78 -0
  73. package/src/components/sectioned-list-board/board-card-content.tsx +340 -0
  74. package/src/components/sectioned-list-board/date-range-filter.tsx +249 -0
  75. package/src/components/sectioned-list-board/index.ts +19 -0
  76. package/src/components/sectioned-list-board/sectioned-list-board.css +564 -0
  77. package/src/components/sectioned-list-board/sectioned-list-board.tsx +731 -0
  78. package/src/components/sectioned-list-board/sortable-card.tsx +314 -0
  79. package/src/components/sectioned-list-board/sortable-section.tsx +319 -0
  80. package/src/components/sectioned-list-board/types.ts +216 -0
  81. package/src/components/sectioned-list-table/README.md +80 -0
  82. package/src/components/sectioned-list-table/index.ts +14 -0
  83. package/src/components/sectioned-list-table/sectioned-list-table.css +534 -0
  84. package/src/components/sectioned-list-table/sectioned-list-table.tsx +740 -0
  85. package/src/components/sectioned-list-table/sortable-column-header.tsx +120 -0
  86. package/src/components/sectioned-list-table/sortable-row.tsx +420 -0
  87. package/src/components/sectioned-list-table/sortable-section.tsx +251 -0
  88. package/src/components/sectioned-list-table/table-cell-content.tsx +129 -0
  89. package/src/components/sectioned-list-table/types.ts +120 -0
  90. package/src/components/sectioned-list-table/use-column-preferences.ts +103 -0
  91. package/src/components/ui/actions-dropdown.tsx +109 -0
  92. package/src/components/ui/assignee-selector.tsx +209 -0
  93. package/src/components/ui/avatar.tsx +107 -0
  94. package/src/components/ui/breadcrumb.tsx +109 -0
  95. package/src/components/ui/button-group.tsx +83 -0
  96. package/src/components/ui/button.tsx +64 -0
  97. package/src/components/ui/calendar.tsx +220 -0
  98. package/src/components/ui/card.tsx +92 -0
  99. package/src/components/ui/chart.tsx +376 -0
  100. package/src/components/ui/checkbox.tsx +30 -0
  101. package/src/components/ui/collapsible.tsx +33 -0
  102. package/src/components/ui/command.tsx +182 -0
  103. package/src/components/ui/context-menu.tsx +250 -0
  104. package/src/components/ui/create-button-group.tsx +128 -0
  105. package/src/components/ui/dialog.tsx +156 -0
  106. package/src/components/ui/drawer.tsx +133 -0
  107. package/src/components/ui/dropdown-menu.tsx +255 -0
  108. package/src/components/ui/empty.tsx +104 -0
  109. package/src/components/ui/field.tsx +248 -0
  110. package/src/components/ui/form.tsx +165 -0
  111. package/src/components/ui/index.ts +37 -0
  112. package/src/components/ui/input-group.tsx +168 -0
  113. package/src/components/ui/input.tsx +21 -0
  114. package/src/components/ui/kbd.tsx +28 -0
  115. package/src/components/ui/label.tsx +22 -0
  116. package/src/components/ui/navigation-menu.tsx +168 -0
  117. package/src/components/ui/page-header.tsx +80 -0
  118. package/src/components/ui/popover.tsx +87 -0
  119. package/src/components/ui/scroll-area.tsx +56 -0
  120. package/src/components/ui/select.tsx +190 -0
  121. package/src/components/ui/separator.tsx +26 -0
  122. package/src/components/ui/sheet.tsx +141 -0
  123. package/src/components/ui/sidebar.tsx +726 -0
  124. package/src/components/ui/skeleton.tsx +13 -0
  125. package/src/components/ui/sonner.tsx +38 -0
  126. package/src/components/ui/switch.tsx +33 -0
  127. package/src/components/ui/tabs.tsx +91 -0
  128. package/src/components/ui/textarea.tsx +18 -0
  129. package/src/components/ui/toggle-group.tsx +83 -0
  130. package/src/components/ui/toggle.tsx +45 -0
  131. package/src/components/ui/tooltip.tsx +57 -0
  132. package/src/hooks/use-copy-to-clipboard.ts +37 -0
  133. package/src/hooks/use-file-upload.ts +415 -0
  134. package/src/hooks/use-mobile.ts +19 -0
  135. package/src/index.ts +95 -0
  136. package/src/lib/utils.ts +6 -0
  137. package/src/styles.css +1859 -0
  138. package/src/urls.ts +83 -0
  139. package/src/vite.d.ts +22 -0
  140. package/src/vite.js +241 -0
  141. package/tsconfig.base.json +18 -0
  142. 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
+ }