@gperezmz/bb-plugin-thread-glance 0.1.2

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -0
  3. package/THIRD_PARTY_NOTICES.md +230 -0
  4. package/app.tsx +12 -0
  5. package/components/ui/alert-dialog.tsx +142 -0
  6. package/components/ui/button.tsx +59 -0
  7. package/components/ui/card.tsx +83 -0
  8. package/components/ui/coarse-pointer-sizing.ts +66 -0
  9. package/components/ui/context-menu.tsx +355 -0
  10. package/components/ui/dialog.tsx +416 -0
  11. package/components/ui/dropdown-menu.tsx +640 -0
  12. package/components/ui/hooks/use-compact-viewport.tsx +37 -0
  13. package/components/ui/hooks/use-media-query.ts +70 -0
  14. package/components/ui/hooks/use-pointer-coarse.ts +7 -0
  15. package/components/ui/hover-card.tsx +36 -0
  16. package/components/ui/icon-extended.tsx +333 -0
  17. package/components/ui/icon-registry.ts +189 -0
  18. package/components/ui/icon.tsx +444 -0
  19. package/components/ui/input.tsx +30 -0
  20. package/components/ui/menu-item-hover.tsx +93 -0
  21. package/components/ui/motion.ts +4 -0
  22. package/components/ui/overlay-trigger.ts +84 -0
  23. package/components/ui/popover.tsx +223 -0
  24. package/components/ui/responsive-overlay.tsx +823 -0
  25. package/components/ui/tooltip.tsx +63 -0
  26. package/dist/app.css +2 -0
  27. package/dist/app.js +66 -0
  28. package/dist/app.meta.json +11 -0
  29. package/dist/server.js +71 -0
  30. package/dist/server.js.map +6 -0
  31. package/dist/server.meta.json +11 -0
  32. package/features/thread-list/components/Dialogs.tsx +299 -0
  33. package/features/thread-list/components/FoldRows.tsx +147 -0
  34. package/features/thread-list/components/GroupSection.tsx +370 -0
  35. package/features/thread-list/components/ProviderBadge.tsx +57 -0
  36. package/features/thread-list/components/PullRequestBadge.tsx +50 -0
  37. package/features/thread-list/components/Rails.tsx +28 -0
  38. package/features/thread-list/components/RenameEditor.tsx +118 -0
  39. package/features/thread-list/components/RowMenu.tsx +168 -0
  40. package/features/thread-list/components/SplitMiniMap.tsx +55 -0
  41. package/features/thread-list/components/ThreadDetails.tsx +138 -0
  42. package/features/thread-list/components/ThreadList.tsx +855 -0
  43. package/features/thread-list/components/ThreadRowView.tsx +631 -0
  44. package/features/thread-list/components/Toolbar.tsx +296 -0
  45. package/features/thread-list/components/controller.ts +64 -0
  46. package/features/thread-list/components/glyphs.tsx +129 -0
  47. package/features/thread-list/components/row-card.ts +127 -0
  48. package/features/thread-list/data/storage.ts +18 -0
  49. package/features/thread-list/data/useAutoExpand.ts +53 -0
  50. package/features/thread-list/data/useClientPreferences.ts +20 -0
  51. package/features/thread-list/data/useNotes.ts +41 -0
  52. package/features/thread-list/data/useNow.ts +19 -0
  53. package/features/thread-list/data/usePreferences.ts +126 -0
  54. package/features/thread-list/data/useScheduled.ts +43 -0
  55. package/features/thread-list/data/useStamps.ts +68 -0
  56. package/features/thread-list/icons.ts +44 -0
  57. package/features/thread-list/index.ts +2 -0
  58. package/features/thread-list/model/attention.ts +89 -0
  59. package/features/thread-list/model/counters.ts +67 -0
  60. package/features/thread-list/model/details.ts +79 -0
  61. package/features/thread-list/model/drag.ts +104 -0
  62. package/features/thread-list/model/expansion.ts +71 -0
  63. package/features/thread-list/model/families.ts +297 -0
  64. package/features/thread-list/model/groups.ts +263 -0
  65. package/features/thread-list/model/labels.ts +50 -0
  66. package/features/thread-list/model/layout.ts +61 -0
  67. package/features/thread-list/model/menu.ts +78 -0
  68. package/features/thread-list/model/move.ts +46 -0
  69. package/features/thread-list/model/notes.ts +52 -0
  70. package/features/thread-list/model/provider-mark.ts +59 -0
  71. package/features/thread-list/model/share.ts +101 -0
  72. package/features/thread-list/model/sort.ts +78 -0
  73. package/features/thread-list/model/state.ts +330 -0
  74. package/features/thread-list/model/time.ts +107 -0
  75. package/features/thread-list/model/toggles.ts +99 -0
  76. package/features/thread-list/model/view.ts +669 -0
  77. package/features/thread-list/model/windowing.ts +27 -0
  78. package/features/thread-list/testing/fixtures.ts +162 -0
  79. package/hooks/useBrowserDimmingModal.ts +5 -0
  80. package/lib/portal-scope.ts +15 -0
  81. package/lib/utils.ts +17 -0
  82. package/package.json +96 -0
  83. package/server/cli.ts +107 -0
  84. package/server/failures.ts +136 -0
  85. package/server/import.ts +144 -0
  86. package/server/notes.ts +236 -0
  87. package/server/preference-store.ts +96 -0
  88. package/server/scheduled.ts +129 -0
  89. package/server/serial.ts +12 -0
  90. package/server/stamps.ts +142 -0
  91. package/server/startup.ts +121 -0
  92. package/server.ts +126 -0
  93. package/shared/contract.ts +73 -0
  94. package/shared/preferences.ts +241 -0
  95. package/shared/signals.ts +71 -0
  96. package/skills/thread-glance/SKILL.md +69 -0
  97. package/tsconfig.json +36 -0
  98. package/types/global-jsx.d.ts +10 -0
@@ -0,0 +1,444 @@
1
+ import {
2
+ Component,
3
+ createContext,
4
+ useContext,
5
+ type CSSProperties,
6
+ type ReactNode,
7
+ } from "react";
8
+ import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
9
+ import Alert02Icon from "@hugeicons/core-free-icons/Alert02Icon";
10
+ import AlertCircleIcon from "@hugeicons/core-free-icons/AlertCircleIcon";
11
+ import Archive03Icon from "@hugeicons/core-free-icons/Archive03Icon";
12
+ import ArrowDown01Icon from "@hugeicons/core-free-icons/ArrowDown01Icon";
13
+ import ArrowLeft01Icon from "@hugeicons/core-free-icons/ArrowLeft01Icon";
14
+ import ArrowRight01Icon from "@hugeicons/core-free-icons/ArrowRight01Icon";
15
+ import BotIcon from "@hugeicons/core-free-icons/BotIcon";
16
+ import BubbleChatAddIcon from "@hugeicons/core-free-icons/BubbleChatAddIcon";
17
+ import BubbleChatIcon from "@hugeicons/core-free-icons/BubbleChatIcon";
18
+ import Bug01Icon from "@hugeicons/core-free-icons/Bug01Icon";
19
+ import Cancel01Icon from "@hugeicons/core-free-icons/Cancel01Icon";
20
+ import CancelCircleIcon from "@hugeicons/core-free-icons/CancelCircleIcon";
21
+ import CheckListIcon from "@hugeicons/core-free-icons/CheckListIcon";
22
+ import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02Icon";
23
+ import CircleIcon from "@hugeicons/core-free-icons/CircleIcon";
24
+ import ComputerTerminal01Icon from "@hugeicons/core-free-icons/ComputerTerminal01Icon";
25
+ import Copy01Icon from "@hugeicons/core-free-icons/Copy01Icon";
26
+ import DashedLineCircleIcon from "@hugeicons/core-free-icons/DashedLineCircleIcon";
27
+ import Delete02Icon from "@hugeicons/core-free-icons/Delete02Icon";
28
+ import Download01Icon from "@hugeicons/core-free-icons/Download01Icon";
29
+ import Edit02Icon from "@hugeicons/core-free-icons/Edit02Icon";
30
+ import FilterHorizontalIcon from "@hugeicons/core-free-icons/FilterHorizontalIcon";
31
+ import FolderAddIcon from "@hugeicons/core-free-icons/FolderAddIcon";
32
+ import FolderExportIcon from "@hugeicons/core-free-icons/FolderExportIcon";
33
+ import FolderGitTwoIcon from "@hugeicons/core-free-icons/FolderGit2Icon";
34
+ import Folder02Icon from "@hugeicons/core-free-icons/Folder02Icon";
35
+ import FolderIcon from "@hugeicons/core-free-icons/Folder01Icon";
36
+ import FolderSyncIcon from "@hugeicons/core-free-icons/FolderSyncIcon";
37
+ import FolderUnknownIcon from "@hugeicons/core-free-icons/FolderUnknownIcon";
38
+ import HelpCircleIcon from "@hugeicons/core-free-icons/HelpCircleIcon";
39
+ import InformationCircleIcon from "@hugeicons/core-free-icons/InformationCircleIcon";
40
+ import Loading03Icon from "@hugeicons/core-free-icons/Loading03Icon";
41
+ import MessageQuestionIcon from "@hugeicons/core-free-icons/MessageQuestionIcon";
42
+ import MoreHorizontalIcon from "@hugeicons/core-free-icons/MoreHorizontalIcon";
43
+ import Search01Icon from "@hugeicons/core-free-icons/Search01Icon";
44
+ import Settings01Icon from "@hugeicons/core-free-icons/Settings01Icon";
45
+ import SidebarLeftIcon from "@hugeicons/core-free-icons/SidebarLeftIcon";
46
+ import SlidersHorizontalIcon from "@hugeicons/core-free-icons/SlidersHorizontalIcon";
47
+ import SourceCodeIcon from "@hugeicons/core-free-icons/SourceCodeIcon";
48
+ import Target02Icon from "@hugeicons/core-free-icons/Target02Icon";
49
+ import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
50
+ import ToolboxIcon from "@hugeicons/core-free-icons/ToolboxIcon";
51
+ import ToolCaseIcon from "@hugeicons/core-free-icons/ToolCaseIcon";
52
+ import UserAdd01Icon from "@hugeicons/core-free-icons/UserAdd01Icon";
53
+ import UnavailableIcon from "@hugeicons/core-free-icons/UnavailableIcon";
54
+ import WorkflowCircle03Icon from "@hugeicons/core-free-icons/WorkflowCircle03Icon";
55
+ import ZapIcon from "@hugeicons/core-free-icons/ZapIcon";
56
+ import { useSyncExternalStore } from "react";
57
+ import { cn } from "../../lib/utils";
58
+ import {
59
+ EXTENDED_ICON_NAMES,
60
+ getAppIcon,
61
+ getPluginAssetIcon,
62
+ subscribeAppIcons,
63
+ subscribePluginAssetIcons,
64
+ type ExtendedIconName,
65
+ getExtendedIcons,
66
+ subscribeExtendedIcons,
67
+ } from "./icon-registry";
68
+
69
+ const SectionAddStrokeRoundedIcon: IconSvgElement = [
70
+ [
71
+ "path",
72
+ {
73
+ d: "M2 3.4C2 2.24173 2.24173 2 3.4 2H20.6C21.7583 2 22 2.24173 22 3.4V4.6C22 5.75827 21.7583 6 20.6 6H3.4C2.24173 6 2 5.75827 2 4.6V3.4Z",
74
+ stroke: "currentColor",
75
+ strokeLinecap: "round",
76
+ strokeWidth: "1.5",
77
+ key: "0",
78
+ },
79
+ ],
80
+ [
81
+ "path",
82
+ {
83
+ d: "M2 11.4C2 10.2417 2.24173 10 3.4 10H10.6C11.7583 10 12 10.2417 12 11.4V12.6C12 13.7583 11.7583 14 10.6 14H3.4C2.24173 14 2 13.7583 2 12.6V11.4Z",
84
+ stroke: "currentColor",
85
+ strokeLinecap: "round",
86
+ strokeWidth: "1.5",
87
+ key: "1",
88
+ },
89
+ ],
90
+ [
91
+ "path",
92
+ {
93
+ d: "M2 19.4C2 18.2417 2.24173 18 3.4 18H10.6C11.7583 18 12 18.2417 12 19.4V20.6C12 21.7583 11.7583 22 10.6 22H3.4C2.24173 22 2 21.7583 2 20.6V19.4Z",
94
+ stroke: "currentColor",
95
+ strokeLinecap: "round",
96
+ strokeWidth: "1.5",
97
+ key: "2",
98
+ },
99
+ ],
100
+ [
101
+ "path",
102
+ {
103
+ d: "M18 13V21M22 17H14",
104
+ stroke: "currentColor",
105
+ strokeLinecap: "round",
106
+ strokeWidth: "1.5",
107
+ key: "3",
108
+ },
109
+ ],
110
+ ];
111
+
112
+ const SectionMoveStrokeRoundedIcon: IconSvgElement = [
113
+ [
114
+ "path",
115
+ {
116
+ d: "M2 3.4C2 2.24173 2.24173 2 3.4 2H20.6C21.7583 2 22 2.24173 22 3.4V4.6C22 5.75827 21.7583 6 20.6 6H3.4C2.24173 6 2 5.75827 2 4.6V3.4Z",
117
+ stroke: "currentColor",
118
+ strokeLinecap: "round",
119
+ strokeWidth: "1.5",
120
+ key: "0",
121
+ },
122
+ ],
123
+ [
124
+ "path",
125
+ {
126
+ d: "M2 11.4C2 10.2417 2.24173 10 3.4 10H10.6C11.7583 10 12 10.2417 12 11.4V12.6C12 13.7583 11.7583 14 10.6 14H3.4C2.24173 14 2 13.7583 2 12.6V11.4Z",
127
+ stroke: "currentColor",
128
+ strokeLinecap: "round",
129
+ strokeWidth: "1.5",
130
+ key: "1",
131
+ },
132
+ ],
133
+ [
134
+ "path",
135
+ {
136
+ d: "M2 19.4C2 18.2417 2.24173 18 3.4 18H10.6C11.7583 18 12 18.2417 12 19.4V20.6C12 21.7583 11.7583 22 10.6 22H3.4C2.24173 22 2 21.7583 2 20.6V19.4Z",
137
+ stroke: "currentColor",
138
+ strokeLinecap: "round",
139
+ strokeWidth: "1.5",
140
+ key: "2",
141
+ },
142
+ ],
143
+ [
144
+ "path",
145
+ {
146
+ d: "M14 17H22M19 14L22 17L19 20",
147
+ stroke: "currentColor",
148
+ strokeLinecap: "round",
149
+ strokeLinejoin: "round",
150
+ strokeWidth: "1.5",
151
+ key: "3",
152
+ },
153
+ ],
154
+ ];
155
+
156
+ const CORE_ICON_MAP = {
157
+ AlertCircle: AlertCircleIcon,
158
+ AlertTriangle: Alert02Icon,
159
+ Archive: Archive03Icon,
160
+ Bot: BotIcon,
161
+ Bug: Bug01Icon,
162
+ Check: Tick02Icon,
163
+ ChevronDown: ArrowDown01Icon,
164
+ ChevronLeft: ArrowLeft01Icon,
165
+ ChevronRight: ArrowRight01Icon,
166
+ Circle: CircleIcon,
167
+ CircleCheck: CheckmarkCircle02Icon,
168
+ CircleQuestion: HelpCircleIcon,
169
+ CircleX: CancelCircleIcon,
170
+ ClosePluginPane: Cancel01Icon,
171
+ CloseThreadPane: Cancel01Icon,
172
+ Code: SourceCodeIcon,
173
+ ComputerTerminal01: ComputerTerminal01Icon,
174
+ Copy: Copy01Icon,
175
+ Download: Download01Icon,
176
+ Edit: Edit02Icon,
177
+ FilterHorizontal: FilterHorizontalIcon,
178
+ Folder: FolderIcon,
179
+ FolderExport: FolderExportIcon,
180
+ FolderGit: FolderGitTwoIcon,
181
+ FolderPlus: FolderAddIcon,
182
+ FolderSync: FolderSyncIcon,
183
+ FolderUnknown: FolderUnknownIcon,
184
+ Folder02: Folder02Icon,
185
+ Info: InformationCircleIcon,
186
+ ListTodo: CheckListIcon,
187
+ Loading: Loading03Icon,
188
+ MessageQuestion: MessageQuestionIcon,
189
+ MessageCirclePlus: BubbleChatAddIcon,
190
+ MessageSquarePlus: BubbleChatAddIcon,
191
+ MessageSquare: BubbleChatIcon,
192
+ MoreHorizontal: MoreHorizontalIcon,
193
+ PanelLeft: SidebarLeftIcon,
194
+ Search: Search01Icon,
195
+ SectionAdd: SectionAddStrokeRoundedIcon,
196
+ SectionMove: SectionMoveStrokeRoundedIcon,
197
+ Settings: Settings01Icon,
198
+ SlidersHorizontal: SlidersHorizontalIcon,
199
+ Spinner: DashedLineCircleIcon,
200
+ Target: Target02Icon,
201
+ Terminal: ComputerTerminal01Icon,
202
+ Toolbox: ToolboxIcon,
203
+ ToolCase: ToolCaseIcon,
204
+ Trash2: Delete02Icon,
205
+ Unavailable: UnavailableIcon,
206
+ UserRoundPlus: UserAdd01Icon,
207
+ Workflow: WorkflowCircle03Icon,
208
+ X: Cancel01Icon,
209
+ Zap: ZapIcon,
210
+ } as const satisfies Record<string, IconSvgElement>;
211
+
212
+ type CoreIconName = keyof typeof CORE_ICON_MAP;
213
+
214
+ export type BuiltinIconName = CoreIconName | ExtendedIconName;
215
+ export type IconName = string;
216
+
217
+ const CORE_ICON_NAMES = Object.keys(CORE_ICON_MAP) as readonly CoreIconName[];
218
+
219
+ export const ICON_NAMES: readonly BuiltinIconName[] = [
220
+ ...CORE_ICON_NAMES,
221
+ ...EXTENDED_ICON_NAMES,
222
+ ];
223
+
224
+ const CORE_ICON_LOOKUP: Readonly<Record<string, IconSvgElement | undefined>> =
225
+ CORE_ICON_MAP;
226
+
227
+ let extendedIconsLoad: Promise<void> | null = null;
228
+
229
+ export function preloadExtendedIcons(): Promise<void> {
230
+ if (getExtendedIcons() !== null) return Promise.resolve();
231
+ extendedIconsLoad ??= import("./icon-extended").then(
232
+ () => undefined,
233
+ (error: unknown) => {
234
+ extendedIconsLoad = null;
235
+ throw error;
236
+ },
237
+ );
238
+ return extendedIconsLoad;
239
+ }
240
+
241
+ const EMPTY_ICON: IconSvgElement = [];
242
+
243
+ export interface IconProps {
244
+ name: IconName;
245
+ fallback?: string;
246
+ className?: string;
247
+ style?: CSSProperties;
248
+ "aria-hidden"?: boolean | "true" | "false";
249
+ "aria-label"?: string;
250
+ }
251
+
252
+ const ICON_NAME_SET: ReadonlySet<string> = new Set(ICON_NAMES);
253
+ const IconAncestors = createContext<readonly string[]>([]);
254
+
255
+ export function isBuiltinIconName(name: string): name is BuiltinIconName {
256
+ return ICON_NAME_SET.has(name);
257
+ }
258
+
259
+ class IconErrorBoundary extends Component<
260
+ { children: ReactNode; fallback: ReactNode },
261
+ { failed: boolean }
262
+ > {
263
+ state = { failed: false };
264
+
265
+ static getDerivedStateFromError() {
266
+ return { failed: true };
267
+ }
268
+
269
+ render() {
270
+ return this.state.failed ? this.props.fallback : this.props.children;
271
+ }
272
+ }
273
+
274
+ export function Icon({ name, fallback = "Zap", ...props }: IconProps) {
275
+ const ancestors = useContext(IconAncestors);
276
+ const custom = useSyncExternalStore(
277
+ subscribeAppIcons,
278
+ () => getAppIcon(name),
279
+ () => getAppIcon(name),
280
+ );
281
+ const fallbackCustom = useSyncExternalStore(
282
+ subscribeAppIcons,
283
+ () => getAppIcon(fallback),
284
+ () => getAppIcon(fallback),
285
+ );
286
+ const asset = useSyncExternalStore(
287
+ subscribePluginAssetIcons,
288
+ () => getPluginAssetIcon(name),
289
+ () => getPluginAssetIcon(name),
290
+ );
291
+ const fallbackAsset = useSyncExternalStore(
292
+ subscribePluginAssetIcons,
293
+ () => getPluginAssetIcon(fallback),
294
+ () => getPluginAssetIcon(fallback),
295
+ );
296
+ const requestedExists =
297
+ custom !== undefined || isBuiltinIconName(name) || asset !== undefined;
298
+ const resolved = requestedExists ? name : fallback;
299
+ const definition = requestedExists ? custom : fallbackCustom;
300
+ const resolvedAsset = requestedExists ? asset : fallbackAsset;
301
+ const CustomIcon = definition?.component;
302
+ if (ancestors.includes(resolved)) {
303
+ return (
304
+ <BuiltinIcon
305
+ name={isBuiltinIconName(resolved) ? resolved : "Zap"}
306
+ {...props}
307
+ />
308
+ );
309
+ }
310
+ if (CustomIcon !== undefined && definition !== undefined) {
311
+ return (
312
+ <IconAncestors.Provider value={[...ancestors, resolved]}>
313
+ <IconErrorBoundary
314
+ key={definition.key}
315
+ fallback={<BuiltinIcon name="Zap" {...props} />}
316
+ >
317
+ <span
318
+ className={cn("inline-flex size-6 shrink-0", props.className)}
319
+ style={props.style}
320
+ aria-hidden={props["aria-hidden"]}
321
+ aria-label={props["aria-label"]}
322
+ role={props["aria-label"] ? "img" : undefined}
323
+ data-icon={resolved}
324
+ data-icon-root=""
325
+ >
326
+ <CustomIcon className="size-full" />
327
+ </span>
328
+ </IconErrorBoundary>
329
+ </IconAncestors.Provider>
330
+ );
331
+ }
332
+ if (CustomIcon === undefined && resolvedAsset !== undefined) {
333
+ return (
334
+ <PluginAssetIcon url={resolvedAsset} resolved={resolved} {...props} />
335
+ );
336
+ }
337
+ return (
338
+ <BuiltinIcon
339
+ name={isBuiltinIconName(resolved) ? resolved : "Zap"}
340
+ {...props}
341
+ />
342
+ );
343
+ }
344
+
345
+ function PluginAssetIcon({
346
+ url,
347
+ resolved,
348
+ className,
349
+ style,
350
+ "aria-hidden": ariaHidden,
351
+ "aria-label": ariaLabel,
352
+ }: Omit<IconProps, "name" | "fallback"> & {
353
+ url: string;
354
+ resolved: string;
355
+ }) {
356
+ const image = `url("${url.replace(/["\\]/gu, "\\$&")}")`;
357
+ return (
358
+ <span
359
+ className={cn("inline-block size-6 shrink-0", className)}
360
+ style={{
361
+ ...style,
362
+ backgroundColor: "currentColor",
363
+ maskImage: image,
364
+ maskPosition: "center",
365
+ maskRepeat: "no-repeat",
366
+ maskSize: "contain",
367
+ WebkitMaskImage: image,
368
+ WebkitMaskPosition: "center",
369
+ WebkitMaskRepeat: "no-repeat",
370
+ WebkitMaskSize: "contain",
371
+ }}
372
+ aria-hidden={ariaHidden}
373
+ aria-label={ariaLabel}
374
+ role={ariaLabel ? "img" : undefined}
375
+ data-icon={resolved}
376
+ data-icon-root=""
377
+ data-plugin-icon-asset={url}
378
+ />
379
+ );
380
+ }
381
+
382
+ function BuiltinIcon({
383
+ name,
384
+ className,
385
+ style,
386
+ "aria-hidden": ariaHidden,
387
+ "aria-label": ariaLabel,
388
+ }: IconProps) {
389
+ const coreIcon = CORE_ICON_LOOKUP[name];
390
+ if (coreIcon !== undefined) {
391
+ return (
392
+ <HugeiconsIcon
393
+ icon={coreIcon}
394
+ className={cn(className)}
395
+ style={style}
396
+ aria-hidden={ariaHidden}
397
+ aria-label={ariaLabel}
398
+ data-icon={name}
399
+ data-icon-root=""
400
+ />
401
+ );
402
+ }
403
+ return (
404
+ <ExtendedIcon
405
+ name={name}
406
+ className={className}
407
+ style={style}
408
+ aria-hidden={ariaHidden}
409
+ aria-label={ariaLabel}
410
+ />
411
+ );
412
+ }
413
+
414
+ function ExtendedIcon({
415
+ name,
416
+ className,
417
+ style,
418
+ "aria-hidden": ariaHidden,
419
+ "aria-label": ariaLabel,
420
+ }: IconProps) {
421
+ const extendedIcons: Readonly<
422
+ Record<string, IconSvgElement | undefined>
423
+ > | null = useSyncExternalStore(
424
+ subscribeExtendedIcons,
425
+ getExtendedIcons,
426
+ getExtendedIcons,
427
+ );
428
+ const icon = extendedIcons?.[name];
429
+ if (icon === undefined) {
430
+ void preloadExtendedIcons().catch(() => undefined);
431
+ }
432
+ return (
433
+ <HugeiconsIcon
434
+ icon={icon ?? EMPTY_ICON}
435
+ className={cn(className)}
436
+ style={style}
437
+ aria-hidden={ariaHidden}
438
+ aria-label={ariaLabel}
439
+ data-icon={name}
440
+ data-icon-root=""
441
+ data-icon-pending={icon === undefined ? "" : undefined}
442
+ />
443
+ );
444
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+
3
+ import {
4
+ COARSE_POINTER_INPUT_HEIGHT_CLASS,
5
+ COARSE_POINTER_TEXT_BASE_CLASS,
6
+ } from "./coarse-pointer-sizing.js";
7
+ import { cn } from "../../lib/utils";
8
+ import { CONTROL_HOVER_TRANSITION } from "./motion.js";
9
+
10
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
11
+ ({ className, type, ...props }, ref) => {
12
+ return (
13
+ <input
14
+ type={type}
15
+ autoComplete="off"
16
+ className={cn(
17
+ `flex w-full rounded-md border border-input bg-transparent pl-3 pr-3 py-1 ${CONTROL_HOVER_TRANSITION} file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50`,
18
+ COARSE_POINTER_INPUT_HEIGHT_CLASS,
19
+ COARSE_POINTER_TEXT_BASE_CLASS,
20
+ className,
21
+ )}
22
+ ref={ref}
23
+ {...props}
24
+ />
25
+ );
26
+ },
27
+ );
28
+ Input.displayName = "Input";
29
+
30
+ export { Input };
@@ -0,0 +1,93 @@
1
+ import * as React from "react";
2
+
3
+ const MENU_NAV_KEYS = new Set([
4
+ "ArrowDown",
5
+ "ArrowUp",
6
+ "Home",
7
+ "End",
8
+ "PageDown",
9
+ "PageUp",
10
+ ]);
11
+
12
+ export const MENU_ITEM_LAST_HOVERED_CLASS =
13
+ "data-[last-hovered]:bg-state-hover data-[last-hovered]:text-foreground";
14
+
15
+ interface MenuHoverContextValue {
16
+ lastHoveredId: string | null;
17
+ setLastHovered: (id: string) => void;
18
+ clearLastHovered: () => void;
19
+ }
20
+
21
+ const MenuHoverContext = React.createContext<MenuHoverContextValue>({
22
+ lastHoveredId: null,
23
+ setLastHovered: () => {},
24
+ clearLastHovered: () => {},
25
+ });
26
+
27
+ export function MenuHoverProvider({ children }: { children: React.ReactNode }) {
28
+ const [lastHoveredId, setLastHoveredId] = React.useState<string | null>(null);
29
+ const value = React.useMemo<MenuHoverContextValue>(
30
+ () => ({
31
+ lastHoveredId,
32
+ setLastHovered: setLastHoveredId,
33
+ clearLastHovered: () => setLastHoveredId(null),
34
+ }),
35
+ [lastHoveredId],
36
+ );
37
+ return (
38
+ <MenuHoverContext.Provider value={value}>
39
+ {children}
40
+ </MenuHoverContext.Provider>
41
+ );
42
+ }
43
+
44
+ export interface MenuItemHoverProps {
45
+ "data-last-hovered": "" | undefined;
46
+ onPointerEnter: React.PointerEventHandler;
47
+ onKeyDown: React.KeyboardEventHandler;
48
+ }
49
+
50
+ interface MenuItemHoverHandlers {
51
+ onPointerEnter?: React.PointerEventHandler;
52
+ onKeyDown?: React.KeyboardEventHandler;
53
+ }
54
+
55
+ export function useMenuItemHover(handlers?: MenuItemHoverHandlers): {
56
+ isLastHovered: boolean;
57
+ hoverProps: MenuItemHoverProps;
58
+ } {
59
+ const id = React.useId();
60
+ const { lastHoveredId, setLastHovered, clearLastHovered } =
61
+ React.useContext(MenuHoverContext);
62
+ const isLastHovered = lastHoveredId === id;
63
+
64
+ const handlersRef = React.useRef(handlers);
65
+ handlersRef.current = handlers;
66
+
67
+ const onPointerEnter = React.useCallback(
68
+ (event: React.PointerEvent) => {
69
+ handlersRef.current?.onPointerEnter?.(event);
70
+ setLastHovered(id);
71
+ },
72
+ [id, setLastHovered],
73
+ );
74
+
75
+ const onKeyDown = React.useCallback(
76
+ (event: React.KeyboardEvent) => {
77
+ handlersRef.current?.onKeyDown?.(event);
78
+ if (MENU_NAV_KEYS.has(event.key)) {
79
+ clearLastHovered();
80
+ }
81
+ },
82
+ [clearLastHovered],
83
+ );
84
+
85
+ return {
86
+ isLastHovered,
87
+ hoverProps: {
88
+ "data-last-hovered": isLastHovered ? "" : undefined,
89
+ onPointerEnter,
90
+ onKeyDown,
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,4 @@
1
+ export const CONTROL_HOVER_TRANSITION =
2
+ "transition-colors duration-150 hover:duration-0";
3
+
4
+ export const LIST_HOVER_TRANSITION = "transition-none";
@@ -0,0 +1,84 @@
1
+ import type { MouseEvent } from "react";
2
+ import { cn } from "../../lib/utils";
3
+
4
+ const OVERLAY_TRIGGER_CLASS_NAME = "select-none";
5
+
6
+ type OverlayTriggerClassNameResolver = (className?: string) => string;
7
+
8
+ const NON_TEXT_INPUT_TYPES = new Set([
9
+ "button",
10
+ "checkbox",
11
+ "file",
12
+ "hidden",
13
+ "image",
14
+ "radio",
15
+ "range",
16
+ "reset",
17
+ "submit",
18
+ ]);
19
+
20
+ export const getOverlayTriggerClassName: OverlayTriggerClassNameResolver = (
21
+ className,
22
+ ) => cn(OVERLAY_TRIGGER_CLASS_NAME, className);
23
+
24
+ function isKeyboardInputElement(element: Element): element is HTMLElement {
25
+ if (element instanceof HTMLTextAreaElement) return true;
26
+ if (element instanceof HTMLInputElement) {
27
+ return (
28
+ !element.disabled &&
29
+ !element.readOnly &&
30
+ !NON_TEXT_INPUT_TYPES.has(element.type)
31
+ );
32
+ }
33
+ if (!(element instanceof HTMLElement)) return false;
34
+
35
+ return (
36
+ element.isContentEditable ||
37
+ element.closest("[contenteditable='true']") !== null
38
+ );
39
+ }
40
+
41
+ export function blurActiveKeyboardInputWithin(container: Element | null): void {
42
+ if (typeof document === "undefined") return;
43
+
44
+ const activeElement = document.activeElement;
45
+ if (!activeElement || !isKeyboardInputElement(activeElement)) return;
46
+ if (container !== null && !container.contains(activeElement)) return;
47
+
48
+ activeElement.blur();
49
+ }
50
+
51
+ export function blurActiveKeyboardInputBeforeOverlayOpen(): void {
52
+ blurActiveKeyboardInputWithin(null);
53
+ }
54
+
55
+ export function blurActiveKeyboardInputBeforeOverlayClose(): void {
56
+ blurActiveKeyboardInputWithin(null);
57
+ }
58
+
59
+ export function preventOverlayTriggerSelection(event: MouseEvent): void {
60
+ event.preventDefault();
61
+ }
62
+
63
+ let lastInputModality: "pointer" | "keyboard" = "pointer";
64
+
65
+ if (typeof document !== "undefined") {
66
+ document.addEventListener(
67
+ "keydown",
68
+ () => {
69
+ lastInputModality = "keyboard";
70
+ },
71
+ { capture: true },
72
+ );
73
+ document.addEventListener(
74
+ "pointerdown",
75
+ () => {
76
+ lastInputModality = "pointer";
77
+ },
78
+ { capture: true },
79
+ );
80
+ }
81
+
82
+ export function isLastInputKeyboard(): boolean {
83
+ return lastInputModality === "keyboard";
84
+ }