@protoboardai/views 0.0.0-stage → 0.2.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 (56) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +21 -2
  4. package/package.json +46 -4
  5. package/src/applies.ts +54 -0
  6. package/src/builtins.tsx +89 -0
  7. package/src/drawing/BoardDrawingView.tsx +515 -0
  8. package/src/drawing/CircuitPcbView.tsx +494 -0
  9. package/src/drawing/DrawingControls.tsx +39 -0
  10. package/src/drawing/boards.ts +82 -0
  11. package/src/drawing/circuit.ts +413 -0
  12. package/src/drawing/kicadSchematic.ts +277 -0
  13. package/src/drawing/palettes.ts +150 -0
  14. package/src/drawing/svg.ts +445 -0
  15. package/src/drawing/usePanZoom.ts +400 -0
  16. package/src/icons.tsx +69 -0
  17. package/src/index.ts +33 -0
  18. package/src/inspector/DrcPanel.tsx +302 -0
  19. package/src/inspector/SelectionInspector.tsx +392 -0
  20. package/src/inspector/index.ts +9 -0
  21. package/src/inspector/model.ts +224 -0
  22. package/src/pair/PairView.tsx +314 -0
  23. package/src/pair/model.ts +206 -0
  24. package/src/parts/BomEditor.tsx +564 -0
  25. package/src/parts/CardPages.tsx +317 -0
  26. package/src/parts/CategoryList.tsx +72 -0
  27. package/src/parts/CloudPanel.tsx +160 -0
  28. package/src/parts/GlbPreview.tsx +116 -0
  29. package/src/parts/LibrariesManager.tsx +251 -0
  30. package/src/parts/LibraryBrowser.tsx +191 -0
  31. package/src/parts/LibraryPage.tsx +168 -0
  32. package/src/parts/PartCard.tsx +274 -0
  33. package/src/parts/PartDetail.tsx +613 -0
  34. package/src/parts/PartStatus.tsx +54 -0
  35. package/src/parts/Prices.tsx +241 -0
  36. package/src/parts/ProjectParts.tsx +174 -0
  37. package/src/parts/ProjectPartsPage.tsx +264 -0
  38. package/src/parts/SettingsPage.tsx +268 -0
  39. package/src/parts/Surface.tsx +51 -0
  40. package/src/parts/Windowed.tsx +70 -0
  41. package/src/parts/carousel.tsx +344 -0
  42. package/src/parts/index.ts +32 -0
  43. package/src/parts/library-data.ts +726 -0
  44. package/src/parts/matte.ts +69 -0
  45. package/src/parts/model.ts +590 -0
  46. package/src/parts/part-pages.ts +379 -0
  47. package/src/parts/part-status.ts +88 -0
  48. package/src/parts/revision.ts +11 -0
  49. package/src/parts/three.d.ts +5 -0
  50. package/src/parts/ui.tsx +335 -0
  51. package/src/parts/views.tsx +433 -0
  52. package/src/registry.ts +107 -0
  53. package/src/tabs.ts +118 -0
  54. package/src/theme.ts +225 -0
  55. package/src/types.ts +93 -0
  56. package/src/ui/Select.tsx +99 -0
package/src/theme.ts ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Theme adapters for the shells. The tokens and the neutral and VS Code
3
+ * adapters come from @deltarobotics/uhd-viewer; Protoboard's own theme lives
4
+ * here, so the open viewer never carries the brand. Protoboard orange exists
5
+ * only in these two themes: nothing else in the stack hard-codes it.
6
+ */
7
+ import {
8
+ defaultDark,
9
+ defaultLight,
10
+ defaultThemeAdapter,
11
+ defineTheme,
12
+ staticTheme,
13
+ vscodeThemeAdapter,
14
+ type Theme,
15
+ type ThemeAdapter,
16
+ } from "@deltarobotics/uhd-viewer/components";
17
+
18
+ /**
19
+ * The Protoboard brand palette, light and dark. The themes below map it onto
20
+ * the viewer's tokens, so every Protoboard surface reads as one product.
21
+ * Design history: the values and their names were taken from the alpha app's stylesheet (next-alpha src/index.css, `:root` and `.dark`).
22
+ */
23
+ export const BRAND_TOKENS = {
24
+ light: {
25
+ background: "oklch(0.9551 0 0)",
26
+ foreground: "oklch(0.3211 0 0)",
27
+ card: "oklch(0.97 0 0)",
28
+ popover: "oklch(0.9702 0 0)",
29
+ primary: "oklch(0.6837 0.212 40.5923)",
30
+ primaryForeground: "oklch(1 0 0)",
31
+ secondary: "oklch(0.9067 0 0)",
32
+ muted: "oklch(0.872 0 0)",
33
+ mutedForeground: "oklch(0.45 0 0)",
34
+ accent: "oklch(0.8078 0 0)",
35
+ border: "oklch(0.8576 0 0)",
36
+ input: "oklch(0.9067 0 0)",
37
+ ring: "oklch(0.4891 0 0)",
38
+ sidebar: "oklch(0.937 0 0)",
39
+ canvasBackground: "oklch(0.94 0 0)",
40
+ canvasGrid: "oklch(0.76 0 0)",
41
+ },
42
+ dark: {
43
+ background: "oklch(0.2178 0 0)",
44
+ foreground: "oklch(0.8853 0 0)",
45
+ card: "oklch(0.251 0 0)",
46
+ popover: "oklch(0.2435 0 0)",
47
+ primary: "oklch(0.6837 0.212 40.5923)",
48
+ primaryForeground: "oklch(0.922 0 0)",
49
+ secondary: "oklch(0.3092 0 0)",
50
+ muted: "oklch(0.285 0 0)",
51
+ mutedForeground: "oklch(0.68 0 0)",
52
+ accent: "oklch(0.3715 0 0)",
53
+ border: "oklch(0.329 0 0)",
54
+ input: "oklch(0.3092 0 0)",
55
+ ring: "oklch(0.7058 0 0)",
56
+ sidebar: "oklch(0.2393 0 0)",
57
+ canvasBackground: "oklch(0.2178 0 0)",
58
+ canvasGrid: "oklch(0.285 0 0)",
59
+ },
60
+ } as const;
61
+
62
+ /** The brand's shadows: a 2px y-offset step under a soft blur. */
63
+ const BRAND_SHADOWS = {
64
+ shadowSm: "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 1px 2px -1px hsl(0 0% 20% / 0.15)",
65
+ shadowMd: "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 2px 4px -1px hsl(0 0% 20% / 0.15)",
66
+ shadowLg: "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 4px 6px -1px hsl(0 0% 20% / 0.15)",
67
+ };
68
+
69
+ /** The brand palette on the viewer's surfaces, text, borders and accent. */
70
+ function brandTokens(a: (typeof BRAND_TOKENS)["light" | "dark"]) {
71
+ return {
72
+ background: a.background,
73
+ canvas: a.canvasBackground,
74
+ canvasDot: a.canvasGrid,
75
+ surface: a.card,
76
+ surfaceRaised: a.popover,
77
+ surfaceSunken: a.sidebar,
78
+ muted: a.muted,
79
+ active: a.accent,
80
+ text: a.foreground,
81
+ textMuted: a.mutedForeground,
82
+ textOnAccent: a.primaryForeground,
83
+ border: a.border,
84
+ borderSubtle: a.input,
85
+ borderStrong: a.ring,
86
+ nodeBorder: a.border,
87
+ accent: a.primary,
88
+ textLink: a.primary,
89
+ radius: "0.5rem",
90
+ };
91
+ }
92
+
93
+ /** The dark Protoboard theme: the palette's dark tokens. */
94
+ export const protoboardDark: Theme = defineTheme(defaultDark, {
95
+ id: "protoboard-dark",
96
+ label: "Protoboard dark",
97
+ tokens: {
98
+ ...brandTokens(BRAND_TOKENS.dark),
99
+ // the canvas's nodes and the 3D scene sit a step off the palette's background
100
+ surfaceSunken: "oklch(0.2 0 0)",
101
+ textFaint: "oklch(0.52 0 0)",
102
+ node: "oklch(0.2768 0 0)",
103
+ nodeSelected: "oklch(0.2386 0 0)",
104
+ nodeBorder: "oklch(0.4 0 0)",
105
+ nodeBorderHover: BRAND_TOKENS.dark.foreground,
106
+ selection: "rgba(168, 162, 158, 0.32)",
107
+ accentHover: "oklch(0.72 0.19 42)",
108
+ ...BRAND_SHADOWS,
109
+ },
110
+ });
111
+
112
+ /** The light Protoboard theme: the palette's light tokens. */
113
+ export const protoboardLight: Theme = defineTheme(defaultLight, {
114
+ id: "protoboard-light",
115
+ label: "Protoboard light",
116
+ tokens: {
117
+ ...brandTokens(BRAND_TOKENS.light),
118
+ textFaint: "oklch(0.6 0 0)",
119
+ node: BRAND_TOKENS.light.card,
120
+ nodeSelected: `color-mix(in oklab, var(--uhd-accent) 6%, ${BRAND_TOKENS.light.card})`,
121
+ nodeBorder: BRAND_TOKENS.light.border,
122
+ nodeBorderHover: BRAND_TOKENS.light.mutedForeground,
123
+ overlay: "color-mix(in oklab, var(--uhd-surface) 92%, transparent)",
124
+ scrim: "oklch(0.2 0 0 / 0.4)",
125
+ accentHover: "oklch(0.63 0.2 40.6)",
126
+ sceneBackground: BRAND_TOKENS.light.canvasBackground,
127
+ sceneGrid: "oklch(0.8 0 0)",
128
+ sceneGridMinor: "oklch(0.88 0 0)",
129
+ ...BRAND_SHADOWS,
130
+ },
131
+ });
132
+
133
+ export const PROTOBOARD_THEMES: readonly Theme[] = [protoboardDark, protoboardLight];
134
+
135
+ /** Protoboard's theme: dark, light, or whichever the system prefers. */
136
+ export function protoboardThemeAdapter(scheme: "dark" | "light" | "system" = "dark", win: Window | undefined = typeof window === "undefined" ? undefined : window): ThemeAdapter {
137
+ if (scheme !== "system") return staticTheme(scheme === "light" ? protoboardLight : protoboardDark);
138
+ const query = win?.matchMedia?.("(prefers-color-scheme: light)");
139
+ const current = () => (query?.matches ? protoboardLight : protoboardDark);
140
+ return {
141
+ id: "protoboard",
142
+ current,
143
+ subscribe: query
144
+ ? (onChange) => {
145
+ const h = () => onChange(current());
146
+ query.addEventListener("change", h);
147
+ return () => query.removeEventListener("change", h);
148
+ }
149
+ : undefined,
150
+ };
151
+ }
152
+
153
+ /** The colour theme VS Code applies is a Protoboard one (the extension's Protoboard Dark or Light). */
154
+ export const isProtoboardColourTheme = (doc: Document | undefined = typeof document === "undefined" ? undefined : document): boolean =>
155
+ /^protoboard\b/i.test(doc?.body?.dataset.vscodeThemeId ?? doc?.body?.dataset.vscodeThemeName ?? "");
156
+
157
+ /** VS Code's faces, or the Protoboard ones under a Protoboard colour theme (the only place they apply). */
158
+ const VSCODE_FACES = { fontUi: "var(--vscode-font-family)", fontLabel: "var(--vscode-font-family)", fontMono: "var(--vscode-editor-font-family, ui-monospace, monospace)" };
159
+ const PROTOBOARD_FACES = { fontUi: '"Kode Mono", var(--vscode-font-family)', fontLabel: '"Kode Mono Bold", "Kode Mono", var(--vscode-font-family)', fontMono: "var(--vscode-editor-font-family, ui-monospace, monospace)" };
160
+
161
+ /** Faint text in a native view: readable as text on the side bar's and the panel's background in a light and a dark theme. */
162
+ const VIEW_FAINT = "color-mix(in oklab, var(--vscode-foreground) 72%, var(--vscode-sideBar-background, var(--vscode-editor-background)))";
163
+
164
+ /**
165
+ * VS Code's theme for a native view (the Inspector's selection, the DRC
166
+ * panel): the viewer's VS Code adapter, every token on `--vscode-*`, with VS
167
+ * Code's own faces, and Kode Mono (bold for labels) only under a Protoboard
168
+ * colour theme. The viewer tab's shell has the same rule
169
+ * (`vscodeShellThemeAdapter` in @protoboardai/web-viewer). A change of theme
170
+ * (the body's class or its theme id) re-themes.
171
+ */
172
+ export function vscodeViewThemeAdapter(doc: Document | undefined = typeof document === "undefined" ? undefined : document): ThemeAdapter {
173
+ const base = vscodeThemeAdapter(doc);
174
+ const current = () => {
175
+ const t = base.current();
176
+ // faint text (the keys of the details' facts) is much of what a native view shows, and VS Code's disabledForeground is
177
+ // too pale to read as text (2.2:1 in Light Modern): the foreground stepped towards the view's background keeps 4.5:1;
178
+ // high contrast has one text colour, the foreground
179
+ const textFaint = t.scheme.startsWith("high-contrast") ? "var(--vscode-foreground)" : VIEW_FAINT;
180
+ return defineTheme(t, { id: t.id, label: t.label, tokens: { ...(isProtoboardColourTheme(doc) ? PROTOBOARD_FACES : VSCODE_FACES), textFaint } });
181
+ };
182
+ return {
183
+ id: "vscode",
184
+ current,
185
+ subscribe: doc?.body
186
+ ? (onChange) => {
187
+ let key = `${doc.body.className}|${isProtoboardColourTheme(doc)}`;
188
+ const observer = new MutationObserver(() => {
189
+ const next = `${doc.body.className}|${isProtoboardColourTheme(doc)}`;
190
+ if (next !== key) {
191
+ key = next;
192
+ onChange(current());
193
+ }
194
+ });
195
+ observer.observe(doc.body, { attributes: true, attributeFilter: ["class", "data-vscode-theme-id", "data-vscode-theme-name"] });
196
+ return () => observer.disconnect();
197
+ }
198
+ : undefined,
199
+ };
200
+ }
201
+
202
+ /** The theme choices a shell offers by id (`?theme=` in the web viewer). */
203
+ export type ThemeChoice = "protoboard-dark" | "protoboard-light" | "protoboard-system" | "vscode" | "default-dark" | "default-light";
204
+
205
+ export const THEME_CHOICES: readonly ThemeChoice[] = ["protoboard-dark", "protoboard-light", "protoboard-system", "vscode", "default-dark", "default-light"];
206
+
207
+ /** The adapter for a choice (unknown: Protoboard dark). */
208
+ export function themeAdapterFor(choice: string | null | undefined): ThemeAdapter {
209
+ switch (choice) {
210
+ case "protoboard-light":
211
+ return protoboardThemeAdapter("light");
212
+ case "protoboard-system":
213
+ return protoboardThemeAdapter("system");
214
+ case "vscode":
215
+ return vscodeThemeAdapter();
216
+ case "default-dark":
217
+ return defaultThemeAdapter("dark");
218
+ case "default-light":
219
+ return defaultThemeAdapter("light");
220
+ default:
221
+ return protoboardThemeAdapter("dark");
222
+ }
223
+ }
224
+
225
+ export { defaultThemeAdapter, staticTheme, vscodeThemeAdapter, type Theme, type ThemeAdapter };
package/src/types.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The view contract: what a view is, what it is given, and the host and theme
3
+ * adapters a shell (web, VS Code, desktop) implements so the same views run
4
+ * in each without rebuilding (docs/specs/viewer-stack.md).
5
+ */
6
+ import type { ComponentType } from "react";
7
+ import type { ModuleDef, UhdBundle } from "@deltarobotics/uhd";
8
+ import type { Theme, ThemeAdapter, ViewerSelection } from "@deltarobotics/uhd-viewer/components";
9
+
10
+ export type { Theme, ThemeAdapter };
11
+
12
+ /** Where a view is opened: the focused module, by instance path, in the loaded bundle. */
13
+ export interface ViewTarget {
14
+ /** The focused module's definition. */
15
+ module: ModuleDef;
16
+ /** Its instance path from the root system ("" is the root). */
17
+ path: string;
18
+ bundle: UhdBundle;
19
+ }
20
+
21
+ /** What a view's component receives. */
22
+ export interface ViewProps extends ViewTarget {
23
+ host: HostAdapter;
24
+ /** The view's own registration (its id, title, icon). */
25
+ view: ViewDefinition;
26
+ /**
27
+ * What the view was opened with (`openView`'s `params`): the pair view's
28
+ * two modules (`a`, `b`) or link (`link`), a board (`board`). Each view
29
+ * documents its own; absent keys mean its default.
30
+ */
31
+ params?: Readonly<Record<string, string>>;
32
+ }
33
+
34
+ /** `ready`: a working view. `planned`: registered so the tab exists; the shells show what is coming. */
35
+ export type ViewStatus = "ready" | "planned";
36
+
37
+ export interface ViewDefinition {
38
+ /** Unique: `diagram`, `3d`, `schematic`, or `<extension>.<view>` for an extension's own. */
39
+ id: string;
40
+ title: string;
41
+ /** A Lucide icon name (kebab-case). */
42
+ icon: string;
43
+ /** Whether the view has anything to show for a module (its tab is offered only then). */
44
+ applies(module: ModuleDef, bundle: UhdBundle, at?: { path: string }): boolean;
45
+ /** The view. Absent: a planned view (the shell shows its placeholder). */
46
+ component?: ComponentType<ViewProps>;
47
+ status?: ViewStatus;
48
+ /** Sort key among a module's tabs (lower first: diagram 0, 3d 10, schematic 20, pcb 30, bom 40, pair 50, library 60). */
49
+ order?: number;
50
+ /** One sentence for the tab's tooltip and the placeholder. */
51
+ description?: string;
52
+ /** Who registered it: `builtin`, or the contributing extension's id. */
53
+ source?: string;
54
+ /**
55
+ * The view draws the shared UHD viewer (diagram, 3D): shells keep one
56
+ * viewer state for all of these, so selection and drill level carry across.
57
+ */
58
+ uhd?: boolean;
59
+ }
60
+
61
+ /**
62
+ * What a view may ask of the shell around it. Every method is optional: a
63
+ * shell offers what it can (VS Code opens a source file in an editor, the
64
+ * web shell in a new tab, a static page not at all).
65
+ */
66
+ export interface HostAdapter {
67
+ /** `web`, `vscode`, `desktop`, … (for views that word things per host). */
68
+ kind: string;
69
+ /** Show a module's definition (or its instance's line) in source. */
70
+ openSource?(request: { defId: string; instancePath?: string }): void;
71
+ /** Open one of a module's artifact files (VS Code: STEP and GLB in its CAD viewer, the rest in an editor). */
72
+ openArtifact?(request: { defId: string; artifactId: string }): void;
73
+ /**
74
+ * Open a view for a module: a new editor tab in VS Code, the tab in the web
75
+ * shell. `params` reach the view as its `params` (see ViewProps).
76
+ */
77
+ openView?(viewId: string, path: string, params?: Record<string, string>): void;
78
+ /**
79
+ * A URL the view can fetch a module's artifact file from (a drawing, a
80
+ * pick-and-place file): the dev server's, a webview resource. Absent: the
81
+ * view uses the artifact's own `url` when it has one.
82
+ */
83
+ artifactUrl?(defId: string, filePath: string): string | undefined;
84
+ /** The user's selection changed (VS Code's native Inspector follows it). */
85
+ selectionChanged?(selection: ViewerSelection | null): void;
86
+ /** A short status message ("layout saved"). */
87
+ notify?(message: string, severity?: "info" | "warning" | "error"): void;
88
+ /** Add the library parts the project names and does not hold (VS Code: Fetch Parts, B6). */
89
+ fetchParts?(): void;
90
+ }
91
+
92
+ /** A host that offers nothing (tests, static pages). */
93
+ export const NULL_HOST: HostAdapter = { kind: "none" };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * A themed dropdown (Radix Select) for the views: the trigger and the list
3
+ * draw with the theme's tokens (surface, line, text, focus, the contrast
4
+ * outline in high contrast), and the list portals into the themed root, so it
5
+ * follows VS Code's theme like everything else. Keyboard and screen readers
6
+ * get Radix's listbox. A native <select> draws its list with the operating
7
+ * system's colours, whatever the theme.
8
+ */
9
+ import * as RadixSelect from "@radix-ui/react-select";
10
+ import type { ReactNode } from "react";
11
+ import { Icon, cx, useTheme } from "@deltarobotics/uhd-viewer/components";
12
+
13
+ export interface SelectOption {
14
+ value: string;
15
+ label: ReactNode;
16
+ /** What the trigger shows when this option is picked (defaults to the label). */
17
+ text?: string;
18
+ }
19
+
20
+ export interface SelectProps {
21
+ value: string;
22
+ options: readonly SelectOption[];
23
+ onChange(value: string): void;
24
+ /** The accessible name. */
25
+ label: string;
26
+ /** A tooltip on the trigger. */
27
+ tooltip?: string;
28
+ className?: string;
29
+ /** Draw the trigger yourself (its content; `className` then styles the whole trigger). */
30
+ trigger?: ReactNode;
31
+ }
32
+
33
+ export function Select({ value, options, onChange, label, tooltip, className, trigger }: SelectProps) {
34
+ const { root } = useTheme();
35
+ const picked = options.find((o) => o.value === value);
36
+ return (
37
+ <RadixSelect.Root value={value} onValueChange={onChange}>
38
+ <RadixSelect.Trigger
39
+ aria-label={label}
40
+ data-tooltip={tooltip}
41
+ className={
42
+ trigger
43
+ ? cx("cursor-pointer outline-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-focus", className)
44
+ : cx(
45
+ "inline-flex h-8 max-w-[420px] min-w-0 cursor-pointer items-center gap-2 rounded-md border border-line bg-sunken px-2 font-ui text-[12px] text-fg outline-1 -outline-offset-1 outline-contrast",
46
+ "hover:border-line-strong focus-visible:outline focus-visible:outline-1 focus-visible:outline-focus data-[state=open]:border-line-strong",
47
+ className,
48
+ )
49
+ }
50
+ >
51
+ {trigger ?? (
52
+ <>
53
+ <span className="min-w-0 truncate">
54
+ <RadixSelect.Value>{picked?.text ?? picked?.label}</RadixSelect.Value>
55
+ </span>
56
+ <RadixSelect.Icon asChild>
57
+ <Icon name="chevron-down" size={13} className="shrink-0 text-fg-muted" aria-hidden />
58
+ </RadixSelect.Icon>
59
+ </>
60
+ )}
61
+ </RadixSelect.Trigger>
62
+ <RadixSelect.Portal container={root ?? undefined}>
63
+ <RadixSelect.Content
64
+ position="popper"
65
+ sideOffset={4}
66
+ align="start"
67
+ collisionPadding={8}
68
+ className="z-[1000] flex max-h-[min(360px,var(--radix-select-content-available-height))] min-w-[var(--radix-select-trigger-width)] overflow-hidden rounded-md border border-line bg-surface text-fg flex-col shadow-panel outline outline-1 -outline-offset-1 outline-contrast"
69
+ >
70
+ {/* the list scrolls without a scrollbar (Radix hides it), with arrows at the ends when it overflows */}
71
+ <RadixSelect.ScrollUpButton className="flex h-5 cursor-default items-center justify-center text-fg-muted">
72
+ <Icon name="chevron-up" size={13} aria-hidden />
73
+ </RadixSelect.ScrollUpButton>
74
+ <RadixSelect.Viewport className="p-1">
75
+ {options.map((o) => (
76
+ <RadixSelect.Item
77
+ key={o.value}
78
+ value={o.value}
79
+ className={cx(
80
+ "relative flex h-7 cursor-pointer items-center gap-2 rounded-[4px] pr-2 pl-7 font-ui text-[12px] whitespace-nowrap text-fg outline-none select-none",
81
+ "data-[highlighted]:bg-hover data-[highlighted]:outline data-[highlighted]:outline-1 data-[highlighted]:-outline-offset-1 data-[highlighted]:outline-contrast-active",
82
+ "data-[state=checked]:font-semibold",
83
+ )}
84
+ >
85
+ <RadixSelect.ItemIndicator className="absolute left-2 inline-flex">
86
+ <Icon name="check" size={13} className="text-accent" aria-hidden />
87
+ </RadixSelect.ItemIndicator>
88
+ <RadixSelect.ItemText>{o.label}</RadixSelect.ItemText>
89
+ </RadixSelect.Item>
90
+ ))}
91
+ </RadixSelect.Viewport>
92
+ <RadixSelect.ScrollDownButton className="flex h-5 cursor-default items-center justify-center text-fg-muted">
93
+ <Icon name="chevron-down" size={13} aria-hidden />
94
+ </RadixSelect.ScrollDownButton>
95
+ </RadixSelect.Content>
96
+ </RadixSelect.Portal>
97
+ </RadixSelect.Root>
98
+ );
99
+ }