@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.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/THIRD_PARTY_NOTICES.md +230 -0
- package/app.tsx +12 -0
- package/components/ui/alert-dialog.tsx +142 -0
- package/components/ui/button.tsx +59 -0
- package/components/ui/card.tsx +83 -0
- package/components/ui/coarse-pointer-sizing.ts +66 -0
- package/components/ui/context-menu.tsx +355 -0
- package/components/ui/dialog.tsx +416 -0
- package/components/ui/dropdown-menu.tsx +640 -0
- package/components/ui/hooks/use-compact-viewport.tsx +37 -0
- package/components/ui/hooks/use-media-query.ts +70 -0
- package/components/ui/hooks/use-pointer-coarse.ts +7 -0
- package/components/ui/hover-card.tsx +36 -0
- package/components/ui/icon-extended.tsx +333 -0
- package/components/ui/icon-registry.ts +189 -0
- package/components/ui/icon.tsx +444 -0
- package/components/ui/input.tsx +30 -0
- package/components/ui/menu-item-hover.tsx +93 -0
- package/components/ui/motion.ts +4 -0
- package/components/ui/overlay-trigger.ts +84 -0
- package/components/ui/popover.tsx +223 -0
- package/components/ui/responsive-overlay.tsx +823 -0
- package/components/ui/tooltip.tsx +63 -0
- package/dist/app.css +2 -0
- package/dist/app.js +66 -0
- package/dist/app.meta.json +11 -0
- package/dist/server.js +71 -0
- package/dist/server.js.map +6 -0
- package/dist/server.meta.json +11 -0
- package/features/thread-list/components/Dialogs.tsx +299 -0
- package/features/thread-list/components/FoldRows.tsx +147 -0
- package/features/thread-list/components/GroupSection.tsx +370 -0
- package/features/thread-list/components/ProviderBadge.tsx +57 -0
- package/features/thread-list/components/PullRequestBadge.tsx +50 -0
- package/features/thread-list/components/Rails.tsx +28 -0
- package/features/thread-list/components/RenameEditor.tsx +118 -0
- package/features/thread-list/components/RowMenu.tsx +168 -0
- package/features/thread-list/components/SplitMiniMap.tsx +55 -0
- package/features/thread-list/components/ThreadDetails.tsx +138 -0
- package/features/thread-list/components/ThreadList.tsx +855 -0
- package/features/thread-list/components/ThreadRowView.tsx +631 -0
- package/features/thread-list/components/Toolbar.tsx +296 -0
- package/features/thread-list/components/controller.ts +64 -0
- package/features/thread-list/components/glyphs.tsx +129 -0
- package/features/thread-list/components/row-card.ts +127 -0
- package/features/thread-list/data/storage.ts +18 -0
- package/features/thread-list/data/useAutoExpand.ts +53 -0
- package/features/thread-list/data/useClientPreferences.ts +20 -0
- package/features/thread-list/data/useNotes.ts +41 -0
- package/features/thread-list/data/useNow.ts +19 -0
- package/features/thread-list/data/usePreferences.ts +126 -0
- package/features/thread-list/data/useScheduled.ts +43 -0
- package/features/thread-list/data/useStamps.ts +68 -0
- package/features/thread-list/icons.ts +44 -0
- package/features/thread-list/index.ts +2 -0
- package/features/thread-list/model/attention.ts +89 -0
- package/features/thread-list/model/counters.ts +67 -0
- package/features/thread-list/model/details.ts +79 -0
- package/features/thread-list/model/drag.ts +104 -0
- package/features/thread-list/model/expansion.ts +71 -0
- package/features/thread-list/model/families.ts +297 -0
- package/features/thread-list/model/groups.ts +263 -0
- package/features/thread-list/model/labels.ts +50 -0
- package/features/thread-list/model/layout.ts +61 -0
- package/features/thread-list/model/menu.ts +78 -0
- package/features/thread-list/model/move.ts +46 -0
- package/features/thread-list/model/notes.ts +52 -0
- package/features/thread-list/model/provider-mark.ts +59 -0
- package/features/thread-list/model/share.ts +101 -0
- package/features/thread-list/model/sort.ts +78 -0
- package/features/thread-list/model/state.ts +330 -0
- package/features/thread-list/model/time.ts +107 -0
- package/features/thread-list/model/toggles.ts +99 -0
- package/features/thread-list/model/view.ts +669 -0
- package/features/thread-list/model/windowing.ts +27 -0
- package/features/thread-list/testing/fixtures.ts +162 -0
- package/hooks/useBrowserDimmingModal.ts +5 -0
- package/lib/portal-scope.ts +15 -0
- package/lib/utils.ts +17 -0
- package/package.json +96 -0
- package/server/cli.ts +107 -0
- package/server/failures.ts +136 -0
- package/server/import.ts +144 -0
- package/server/notes.ts +236 -0
- package/server/preference-store.ts +96 -0
- package/server/scheduled.ts +129 -0
- package/server/serial.ts +12 -0
- package/server/stamps.ts +142 -0
- package/server/startup.ts +121 -0
- package/server.ts +126 -0
- package/shared/contract.ts +73 -0
- package/shared/preferences.ts +241 -0
- package/shared/signals.ts +71 -0
- package/skills/thread-glance/SKILL.md +69 -0
- package/tsconfig.json +36 -0
- 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,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
|
+
}
|