@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,293 @@
1
+ // Client-only implementation - this file imports @base-ui/react directly
2
+ // It should only be dynamically imported on the client
3
+ import React from "react";
4
+ import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
5
+
6
+ // Autocomplete Root
7
+ export const AutocompleteRoot = AutocompletePrimitive.Root;
8
+
9
+ // Autocomplete Value
10
+ export function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
11
+ return (
12
+ <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
13
+ );
14
+ }
15
+
16
+ // Autocomplete Input
17
+ interface AutocompleteInputProps extends Omit<AutocompletePrimitive.Input.Props, "size"> {
18
+ showClear?: boolean;
19
+ showTrigger?: boolean;
20
+ icon?: React.ReactNode;
21
+ loading?: boolean;
22
+ }
23
+
24
+ export function AutocompleteInput({
25
+ className,
26
+ showClear = false,
27
+ showTrigger = false,
28
+ icon,
29
+ loading = false,
30
+ ...props
31
+ }: AutocompleteInputProps) {
32
+ return (
33
+ // suppressHydrationWarning: browser extensions (Dashlane, 1Password, etc.) inject attributes before hydration
34
+ <div className={`app-shell-search-input-container ${className ?? ""}`} suppressHydrationWarning>
35
+ {icon && (
36
+ <span className="app-shell-search-icon">
37
+ {loading ? <LoadingSpinner /> : icon}
38
+ </span>
39
+ )}
40
+ <AutocompletePrimitive.Input
41
+ data-slot="autocomplete-input"
42
+ className="app-shell-search-input"
43
+ {...props}
44
+ />
45
+ {showTrigger && <AutocompleteTrigger />}
46
+ {showClear && <AutocompleteClear />}
47
+ </div>
48
+ );
49
+ }
50
+
51
+ // Loading Spinner
52
+ const LoadingSpinner = () => (
53
+ <svg
54
+ width="16"
55
+ height="16"
56
+ viewBox="0 0 16 16"
57
+ fill="none"
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ className="app-shell-search-spinner"
60
+ aria-hidden="true"
61
+ >
62
+ <circle
63
+ cx="8"
64
+ cy="8"
65
+ r="6"
66
+ stroke="currentColor"
67
+ strokeWidth="2"
68
+ strokeLinecap="round"
69
+ strokeDasharray="32"
70
+ strokeDashoffset="12"
71
+ />
72
+ </svg>
73
+ );
74
+
75
+ // Portal
76
+ export function AutocompletePortal({ ...props }: AutocompletePrimitive.Portal.Props) {
77
+ return (
78
+ <AutocompletePrimitive.Portal data-slot="autocomplete-portal" {...props} />
79
+ );
80
+ }
81
+
82
+ // Backdrop
83
+ export function AutocompleteBackdrop({
84
+ ...props
85
+ }: AutocompletePrimitive.Backdrop.Props) {
86
+ return (
87
+ <AutocompletePrimitive.Backdrop
88
+ data-slot="autocomplete-backdrop"
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ // Positioner
95
+ export function AutocompletePositioner({
96
+ className,
97
+ ...props
98
+ }: AutocompletePrimitive.Positioner.Props) {
99
+ return (
100
+ <AutocompletePrimitive.Positioner
101
+ data-slot="autocomplete-positioner"
102
+ className={`app-shell-search-positioner ${className ?? ""}`}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ // List
109
+ export function AutocompleteList({
110
+ className,
111
+ ...props
112
+ }: AutocompletePrimitive.List.Props) {
113
+ return (
114
+ <AutocompletePrimitive.List
115
+ data-slot="autocomplete-list"
116
+ className={`app-shell-search-options ${className ?? ""}`}
117
+ {...props}
118
+ />
119
+ );
120
+ }
121
+
122
+ // Item
123
+ export function AutocompleteItem({
124
+ className,
125
+ ...props
126
+ }: React.ComponentProps<typeof AutocompletePrimitive.Item>) {
127
+ return (
128
+ <AutocompletePrimitive.Item
129
+ data-slot="autocomplete-item"
130
+ className={`app-shell-search-option ${className ?? ""}`}
131
+ {...props}
132
+ />
133
+ );
134
+ }
135
+
136
+ // Content (combines Portal, Positioner, and Popup)
137
+ export interface AutocompleteContentProps extends React.ComponentProps<
138
+ typeof AutocompletePrimitive.Popup
139
+ > {
140
+ align?: AutocompletePrimitive.Positioner.Props["align"];
141
+ sideOffset?: AutocompletePrimitive.Positioner.Props["sideOffset"];
142
+ alignOffset?: AutocompletePrimitive.Positioner.Props["alignOffset"];
143
+ side?: AutocompletePrimitive.Positioner.Props["side"];
144
+ anchor?: AutocompletePrimitive.Positioner.Props["anchor"];
145
+ showBackdrop?: boolean;
146
+ }
147
+
148
+ export function AutocompleteContent({
149
+ className,
150
+ children,
151
+ showBackdrop = false,
152
+ align = "start",
153
+ sideOffset = 4,
154
+ alignOffset = 0,
155
+ side = "bottom",
156
+ anchor,
157
+ ...props
158
+ }: AutocompleteContentProps) {
159
+ const useServicesLayout = Boolean(
160
+ typeof className === "string" &&
161
+ className.includes("app-shell-services-dropdown")
162
+ );
163
+
164
+ return (
165
+ <AutocompletePortal>
166
+ {showBackdrop && <AutocompleteBackdrop />}
167
+ <AutocompletePositioner
168
+ className={useServicesLayout ? "app-shell-search-positioner-services" : undefined}
169
+ align={align}
170
+ sideOffset={sideOffset}
171
+ alignOffset={alignOffset}
172
+ side={side}
173
+ anchor={anchor}
174
+ >
175
+ <AutocompletePrimitive.Popup
176
+ data-slot="autocomplete-popup"
177
+ className={`app-shell-search-dropdown ${className ?? ""}`}
178
+ {...props}
179
+ >
180
+ {children}
181
+ </AutocompletePrimitive.Popup>
182
+ </AutocompletePositioner>
183
+ </AutocompletePortal>
184
+ );
185
+ }
186
+
187
+ // Group
188
+ export function AutocompleteGroup({
189
+ ...props
190
+ }: React.ComponentProps<typeof AutocompletePrimitive.Group>) {
191
+ return (
192
+ <AutocompletePrimitive.Group data-slot="autocomplete-group" {...props} />
193
+ );
194
+ }
195
+
196
+ // Group Label
197
+ export function AutocompleteGroupLabel({
198
+ className,
199
+ ...props
200
+ }: React.ComponentProps<typeof AutocompletePrimitive.GroupLabel>) {
201
+ return (
202
+ <AutocompletePrimitive.GroupLabel
203
+ data-slot="autocomplete-group-label"
204
+ className={`app-shell-search-group-label ${className ?? ""}`}
205
+ {...props}
206
+ />
207
+ );
208
+ }
209
+
210
+ // Empty state
211
+ export function AutocompleteEmpty({
212
+ className,
213
+ ...props
214
+ }: React.ComponentProps<typeof AutocompletePrimitive.Empty>) {
215
+ return (
216
+ <AutocompletePrimitive.Empty
217
+ data-slot="autocomplete-empty"
218
+ className={`app-shell-search-empty ${className ?? ""}`}
219
+ {...props}
220
+ />
221
+ );
222
+ }
223
+
224
+ // Clear button
225
+ export function AutocompleteClear({
226
+ className,
227
+ ...props
228
+ }: React.ComponentProps<typeof AutocompletePrimitive.Clear>) {
229
+ return (
230
+ <AutocompletePrimitive.Clear
231
+ data-slot="autocomplete-clear"
232
+ className={`app-shell-search-clear ${className ?? ""}`}
233
+ {...props}
234
+ >
235
+ <ClearIcon />
236
+ </AutocompletePrimitive.Clear>
237
+ );
238
+ }
239
+
240
+ // Trigger button
241
+ export function AutocompleteTrigger({
242
+ className,
243
+ ...props
244
+ }: React.ComponentProps<typeof AutocompletePrimitive.Trigger>) {
245
+ return (
246
+ <AutocompletePrimitive.Trigger
247
+ data-slot="autocomplete-trigger"
248
+ className={`app-shell-search-trigger ${className ?? ""}`}
249
+ {...props}
250
+ >
251
+ <ChevronIcon />
252
+ </AutocompletePrimitive.Trigger>
253
+ );
254
+ }
255
+
256
+ // Icons
257
+ const ClearIcon = () => (
258
+ <svg
259
+ width="16"
260
+ height="16"
261
+ viewBox="0 0 16 16"
262
+ fill="none"
263
+ xmlns="http://www.w3.org/2000/svg"
264
+ aria-hidden="true"
265
+ >
266
+ <path
267
+ d="M12 4L4 12M4 4L12 12"
268
+ stroke="currentColor"
269
+ strokeWidth="1.5"
270
+ strokeLinecap="round"
271
+ strokeLinejoin="round"
272
+ />
273
+ </svg>
274
+ );
275
+
276
+ const ChevronIcon = () => (
277
+ <svg
278
+ width="16"
279
+ height="16"
280
+ viewBox="0 0 16 16"
281
+ fill="none"
282
+ xmlns="http://www.w3.org/2000/svg"
283
+ aria-hidden="true"
284
+ >
285
+ <path
286
+ d="M4 6L8 10L12 6"
287
+ stroke="currentColor"
288
+ strokeWidth="1.5"
289
+ strokeLinecap="round"
290
+ strokeLinejoin="round"
291
+ />
292
+ </svg>
293
+ );
@@ -0,0 +1,187 @@
1
+ import React, { useCallback, useMemo, useRef } from "react";
2
+ import {
3
+ Autocomplete,
4
+ AutocompleteContent,
5
+ AutocompleteEmpty,
6
+ AutocompleteInput,
7
+ AutocompleteItem,
8
+ AutocompleteList,
9
+ } from "./Autocomplete";
10
+
11
+ // Search icon
12
+ const SearchIcon = () => (
13
+ <svg
14
+ width="16"
15
+ height="16"
16
+ viewBox="0 0 16 16"
17
+ fill="none"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ aria-hidden="true"
20
+ >
21
+ <path
22
+ d="M11.5 11.5L14.5 14.5M7.5 13C4.18629 13 1.5 10.3137 1.5 7C1.5 3.68629 4.18629 1 7.5 1C10.8137 1 13.5 3.68629 13.5 7C13.5 10.3137 10.8137 13 7.5 13Z"
23
+ stroke="currentColor"
24
+ strokeWidth="1.5"
25
+ strokeLinecap="round"
26
+ strokeLinejoin="round"
27
+ />
28
+ </svg>
29
+ );
30
+
31
+ export interface SearchOption {
32
+ value: string;
33
+ label?: string;
34
+ description?: string;
35
+ tags?: string[];
36
+ }
37
+
38
+ export interface GlobalSearchProps {
39
+ /** Current search input value */
40
+ value: string;
41
+ /** Callback when input value changes */
42
+ onChange: (value: string) => void;
43
+ /** Callback when an option is selected */
44
+ onSelect: (option: SearchOption) => void;
45
+ /** Placeholder text for the search input */
46
+ placeholder?: string;
47
+ /** List of search suggestions/options */
48
+ options: SearchOption[];
49
+ /** Content to show when there are no results */
50
+ empty?: React.ReactNode;
51
+ /** Loading state */
52
+ loading?: boolean;
53
+ /** Aria label for accessibility */
54
+ ariaLabel?: string;
55
+ }
56
+
57
+ export function GlobalSearch({
58
+ value,
59
+ onChange,
60
+ onSelect,
61
+ placeholder = "Search",
62
+ options,
63
+ empty = "No results found",
64
+ loading = false,
65
+ ariaLabel = "Search",
66
+ }: GlobalSearchProps) {
67
+ // Track last typed value to detect selection
68
+ const lastTypedValue = useRef(value);
69
+ const containerRef = useRef<HTMLDivElement>(null);
70
+
71
+ // Options are already filtered by the parent component (AppShell uses searchServices)
72
+ // No need to re-filter here - just use the options as-is
73
+ const filteredOptions = options;
74
+ const hasQuery = value.trim().length > 0;
75
+ const groupedOptions = useMemo(() => {
76
+ const groups = new Map<string, SearchOption[]>();
77
+
78
+ filteredOptions.forEach((option) => {
79
+ const groupName = option.tags?.[0] ?? "Services";
80
+ const existing = groups.get(groupName) ?? [];
81
+ existing.push(option);
82
+ groups.set(groupName, existing);
83
+ });
84
+
85
+ return Array.from(groups.entries()).map(([title, items]) => ({ title, items }));
86
+ }, [filteredOptions]);
87
+
88
+ // Convert option to display string
89
+ const itemToString = useCallback((item: unknown): string => {
90
+ const option = item as SearchOption | null;
91
+ return option?.label ?? option?.value ?? "";
92
+ }, []);
93
+
94
+ // Handle value changes - detect if an option was selected
95
+ const handleValueChange = useCallback(
96
+ (newValue: string) => {
97
+ // Check if the new value matches an option exactly (indicating selection)
98
+ const matchedOption = options.find(
99
+ (option) => (option.label ?? option.value) === newValue
100
+ );
101
+
102
+ if (matchedOption && newValue !== lastTypedValue.current) {
103
+ // Option was selected
104
+ onSelect(matchedOption);
105
+ }
106
+
107
+ lastTypedValue.current = newValue;
108
+ onChange(newValue);
109
+ },
110
+ [options, onChange, onSelect]
111
+ );
112
+
113
+ return (
114
+ // suppressHydrationWarning: browser extensions (Dashlane, 1Password, etc.) inject attributes before hydration
115
+ <div
116
+ ref={containerRef}
117
+ className="app-shell-search-input-wrapper"
118
+ suppressHydrationWarning
119
+ >
120
+ <Autocomplete
121
+ value={value}
122
+ onValueChange={handleValueChange}
123
+ items={filteredOptions}
124
+ itemToStringValue={itemToString}
125
+ mode="none"
126
+ >
127
+ <AutocompleteInput
128
+ placeholder={placeholder}
129
+ aria-label={ariaLabel}
130
+ icon={<SearchIcon />}
131
+ loading={loading}
132
+ showClear
133
+ />
134
+ <AutocompleteContent className="app-shell-services-dropdown">
135
+ <AutocompleteEmpty>{hasQuery ? empty : ""}</AutocompleteEmpty>
136
+ {filteredOptions.length > 0 && (
137
+ <div className="app-shell-services-menu" role="menu">
138
+ <div className="app-shell-services-categories" role="presentation">
139
+ <div className="app-shell-services-categories-title">Categories</div>
140
+ <ul>
141
+ {groupedOptions.map((group) => (
142
+ <li key={group.title}>
143
+ <button
144
+ type="button"
145
+ className="app-shell-services-category"
146
+ onMouseDown={(event) => event.preventDefault()}
147
+ aria-label={group.title}
148
+ >
149
+ {group.title}
150
+ </button>
151
+ </li>
152
+ ))}
153
+ </ul>
154
+ </div>
155
+ <div className="app-shell-services-divider" aria-hidden="true" />
156
+ <div className="app-shell-services-list" role="presentation">
157
+ {groupedOptions.map((group) => (
158
+ <div className="app-shell-services-group" key={group.title}>
159
+ <div className="app-shell-services-group-title">{group.title}</div>
160
+ <AutocompleteList>
161
+ {group.items.map((option) => (
162
+ <AutocompleteItem
163
+ key={option.value}
164
+ value={option}
165
+ className="app-shell-services-item"
166
+ >
167
+ <span className="app-shell-services-item-text">
168
+ {option.label ?? option.value}
169
+ </span>
170
+ {option.description ? (
171
+ <span className="app-shell-services-item-description">
172
+ {option.description}
173
+ </span>
174
+ ) : null}
175
+ </AutocompleteItem>
176
+ ))}
177
+ </AutocompleteList>
178
+ </div>
179
+ ))}
180
+ </div>
181
+ </div>
182
+ )}
183
+ </AutocompleteContent>
184
+ </Autocomplete>
185
+ </div>
186
+ );
187
+ }