@enigmax/primitives 0.21.0 → 0.23.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 (83) hide show
  1. package/dist/chunk-3BQVOOAM.js +388 -0
  2. package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
  3. package/dist/chunk-4VUHQFAT.js +130 -0
  4. package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
  5. package/dist/chunk-DSBYVA7V.js +713 -0
  6. package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
  7. package/dist/{chunk-R4ZAEE7V.js → chunk-KEVZ5XQV.js} +1 -1
  8. package/dist/chunk-N6PDHMAX.js +213 -0
  9. package/dist/chunk-NONGREXC.js +248 -0
  10. package/dist/chunk-VKL3DEIQ.js +804 -0
  11. package/dist/chunk-WPTBURIC.js +1627 -0
  12. package/dist/chunk-WSQC3PCC.js +466 -0
  13. package/dist/context-menu-D3FtTn7v.d.ts +174 -0
  14. package/dist/{index-dTdAbOWl.d.ts → index-DQNnohoo.d.ts} +1 -1
  15. package/dist/index.d.ts +5 -1
  16. package/dist/index.js +8 -4
  17. package/dist/keys-D2zJs1uB.d.ts +100 -0
  18. package/dist/next/index.d.ts +12 -5
  19. package/dist/next/index.js +21 -14
  20. package/dist/react/button.d.ts +2 -2
  21. package/dist/react/context-menu.d.ts +202 -0
  22. package/dist/react/context-menu.js +6 -0
  23. package/dist/react/index.d.ts +11 -4
  24. package/dist/react/index.js +20 -13
  25. package/dist/react/input.d.ts +2 -2
  26. package/dist/react/input.js +1 -1
  27. package/dist/react/palette.d.ts +1 -1
  28. package/dist/react/palette.js +3 -3
  29. package/dist/react/search.d.ts +1 -1
  30. package/dist/react/search.js +2 -2
  31. package/dist/react/select.d.ts +217 -0
  32. package/dist/react/select.js +7 -0
  33. package/dist/react/selection.d.ts +104 -0
  34. package/dist/react/selection.js +4 -0
  35. package/dist/react/slot.d.ts +2 -2
  36. package/dist/react/toast.d.ts +165 -32
  37. package/dist/react/toast.js +1 -1
  38. package/dist/react-router/index.d.ts +12 -5
  39. package/dist/react-router/index.js +21 -14
  40. package/dist/search/index.d.ts +2 -2
  41. package/dist/search/index.js +1 -1
  42. package/dist/{search-DXxY8SEH.d.ts → search-DYgqRp37.d.ts} +20 -3
  43. package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
  44. package/dist/select-ClSy-J1f.d.ts +100 -0
  45. package/dist/selection-B_pmzHpy.d.ts +150 -0
  46. package/package.json +21 -2
  47. package/recipes/context-menu/styles.css +177 -0
  48. package/recipes/input/styles.css +9 -0
  49. package/recipes/palette/styles.css +3 -0
  50. package/recipes/search/tailwind.tsx +3 -2
  51. package/recipes/select/styles.css +230 -0
  52. package/recipes/toast/styles.css +688 -192
  53. package/registry.json +434 -29
  54. package/src/core/context-menu.ts +694 -0
  55. package/src/core/keys.ts +264 -0
  56. package/src/core/search.ts +25 -1
  57. package/src/core/select.ts +404 -0
  58. package/src/core/selection.ts +648 -0
  59. package/src/index.ts +60 -1
  60. package/src/react/context-menu/context.ts +57 -0
  61. package/src/react/context-menu/index.tsx +94 -0
  62. package/src/react/context-menu/root.tsx +846 -0
  63. package/src/react/context-menu/styles.ts +186 -0
  64. package/src/react/index.ts +70 -2
  65. package/src/react/palette/root.tsx +2 -2
  66. package/src/react/select/context.ts +56 -0
  67. package/src/react/select/index.tsx +96 -0
  68. package/src/react/select/root.tsx +839 -0
  69. package/src/react/select/styles.ts +238 -0
  70. package/src/react/selection/index.tsx +115 -0
  71. package/src/react/selection/use-selection.ts +260 -0
  72. package/src/react/toast/NOTICE +20 -0
  73. package/src/react/toast/assets.tsx +85 -0
  74. package/src/react/toast/cn.ts +10 -0
  75. package/src/react/toast/hooks.ts +13 -0
  76. package/src/react/toast/index.tsx +736 -0
  77. package/src/react/toast/state.ts +207 -0
  78. package/src/react/toast/styles.ts +738 -0
  79. package/src/react/toast/types.ts +193 -0
  80. package/src/react/toaster.tsx +76 -341
  81. package/dist/chunk-JCCL7XKC.js +0 -465
  82. package/src/react/toast-styles.ts +0 -247
  83. /package/dist/{chunk-U3V4EHOB.js → chunk-3HDEZ2E7.js} +0 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The context menu's baseline look.
3
+ *
4
+ * A string rather than a stylesheet because `<ContextMenu.Root>` injects it, for the same
5
+ * reason the select does: a popup with no styles is not a plain-looking control, it is
6
+ * transparent text lying on top of the page - and this one is drawn at the pointer, over
7
+ * whatever happens to be under it. `scripts/sync-recipes.mjs` generates
8
+ * `recipes/context-menu/styles.css` from here, and CI fails when the two drift.
9
+ *
10
+ * Everything below is either a custom property or an attribute selector: override the
11
+ * properties on `:root` for a different-looking menu without writing one selector. The sheet
12
+ * is PREPENDED to `<head>`, so anything the document already has wins on source order at
13
+ * equal specificity.
14
+ */
15
+
16
+ export const CONTEXT_MENU_STYLES = `
17
+ :root {
18
+ --enigma-menu-bg: #1c1c1c;
19
+ --enigma-menu-border: #333333;
20
+ --enigma-menu-radius: 0.625rem;
21
+ --enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
22
+ --enigma-menu-text: #f5f5f5;
23
+ --enigma-menu-muted: #a3a3a3;
24
+ --enigma-menu-font-size: 0.8125rem;
25
+ --enigma-menu-min-width: 12rem;
26
+ --enigma-menu-max-height: 20rem;
27
+
28
+ --enigma-menu-active-bg: #2a2a2a;
29
+ --enigma-menu-accent: #fbbf24;
30
+ --enigma-menu-danger: #f87171;
31
+ --enigma-menu-danger-bg: rgba(248, 113, 113, 0.12);
32
+ }
33
+
34
+ @media (prefers-color-scheme: light) {
35
+ :root {
36
+ --enigma-menu-bg: #ffffff;
37
+ --enigma-menu-border: #e5e5e5;
38
+ --enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
39
+ --enigma-menu-text: #171717;
40
+ --enigma-menu-muted: #737373;
41
+ --enigma-menu-active-bg: #f5f5f5;
42
+ --enigma-menu-accent: #b45309;
43
+ --enigma-menu-danger: #b91c1c;
44
+ --enigma-menu-danger-bg: rgba(185, 28, 28, 0.08);
45
+ }
46
+ }
47
+
48
+ /* Every panel is positioned in VIEWPORT coordinates, which is what a pointer event reports
49
+ and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling
50
+ ancestor cannot drag it away from the place it was opened. */
51
+ [data-enigma-menu-panel] {
52
+ position: fixed; z-index: 60;
53
+ box-sizing: border-box;
54
+ min-width: var(--enigma-menu-min-width);
55
+ max-width: min(22rem, calc(100vw - 1rem));
56
+ padding: 0.25rem;
57
+ color: var(--enigma-menu-text);
58
+ background: var(--enigma-menu-bg);
59
+ border: 1px solid var(--enigma-menu-border);
60
+ border-radius: var(--enigma-menu-radius);
61
+ box-shadow: var(--enigma-menu-shadow);
62
+ font-size: var(--enigma-menu-font-size);
63
+ animation: enigma-menu-in 90ms ease-out;
64
+ /* Measured before it is placed, and placed before it is seen: without this the panel is
65
+ painted at 0,0 for one frame and jumps to the pointer on the next. */
66
+ visibility: hidden;
67
+ }
68
+ [data-enigma-menu-panel][data-placed] { visibility: visible; }
69
+ @keyframes enigma-menu-in {
70
+ from { opacity: 0; transform: scale(0.98); }
71
+ to { opacity: 1; transform: none; }
72
+ }
73
+ @media (prefers-reduced-motion: reduce) {
74
+ [data-enigma-menu-panel] { animation: none; }
75
+ }
76
+
77
+ /* The heading: what the menu is acting ON. A long file name is cut rather than allowed to
78
+ set the panel's width, and the full one stays reachable through its title attribute. */
79
+ [data-enigma-menu-title] {
80
+ margin: 0; padding: 0.375rem 0.5rem 0.25rem;
81
+ font-size: 0.6875rem; font-weight: 600;
82
+ color: var(--enigma-menu-muted);
83
+ overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
84
+ }
85
+
86
+ [data-enigma-menu-search] {
87
+ width: 100%; box-sizing: border-box;
88
+ padding: 0.375rem 0.5rem 0.5rem;
89
+ font: inherit; font-size: var(--enigma-menu-font-size);
90
+ color: var(--enigma-menu-text); background: transparent;
91
+ border: 0; border-bottom: 1px solid var(--enigma-menu-border);
92
+ outline: none;
93
+ }
94
+ [data-enigma-menu-search]::-webkit-search-cancel-button,
95
+ [data-enigma-menu-search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
96
+
97
+ [data-enigma-menu-list] {
98
+ max-height: var(--enigma-menu-max-height);
99
+ overflow-y: auto; overscroll-behavior: contain;
100
+ }
101
+
102
+ [data-enigma-menu-item] {
103
+ display: flex; align-items: center; gap: 0.5rem;
104
+ padding: 0.3125rem 0.5rem; border-radius: 0.375rem;
105
+ line-height: 1.4;
106
+ color: var(--enigma-menu-text);
107
+ /* The hand, not the arrow. A native menu uses the arrow because the whole surface is
108
+ drawn by the OS and nothing on a page looks like it; here the rows sit on a web page,
109
+ where the hand is what says "this is a thing you press". */
110
+ cursor: pointer; user-select: none;
111
+ }
112
+ [data-enigma-menu-item][data-active] { background: var(--enigma-menu-active-bg); }
113
+ [data-enigma-menu-item][data-disabled] { opacity: 0.45; cursor: not-allowed; }
114
+ [data-enigma-menu-item][data-destructive] { color: var(--enigma-menu-danger); }
115
+ [data-enigma-menu-item][data-destructive][data-active] { background: var(--enigma-menu-danger-bg); }
116
+
117
+ [data-enigma-menu-item-text] { display: grid; min-width: 0; }
118
+ [data-enigma-menu-item-label] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
119
+ [data-enigma-menu-item-description] {
120
+ font-size: 0.6875rem; color: var(--enigma-menu-muted);
121
+ overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
122
+ }
123
+
124
+ [data-enigma-menu-icon] { display: inline-flex; align-items: center; flex: none; width: 1rem; max-height: 1.25em; line-height: 0; }
125
+
126
+ /* The shortcut, on the right, the way every desktop menu prints it. margin-left: auto is
127
+ what pushes it there, and flex: none is what stops a long label squashing it. */
128
+ [data-enigma-menu-shortcut] {
129
+ display: flex; align-items: center; gap: 0.125rem;
130
+ flex: none; margin-left: auto; padding-left: 1.5rem;
131
+ font-size: 0.6875rem; color: var(--enigma-menu-muted);
132
+ font-variant-numeric: tabular-nums;
133
+ }
134
+ [data-enigma-menu-shortcut] kbd { font: inherit; }
135
+
136
+ /* The submenu arrow shares the shortcut's slot, so a row never has both pushed apart. */
137
+ [data-enigma-menu-arrow] {
138
+ flex: none; margin-left: auto;
139
+ width: 0.35rem; height: 0.35rem;
140
+ border-right: 1.5px solid currentColor; border-top: 1.5px solid currentColor;
141
+ transform: rotate(45deg);
142
+ opacity: 0.6;
143
+ }
144
+ [data-enigma-menu-shortcut] + [data-enigma-menu-arrow] { margin-left: 0.5rem; }
145
+
146
+ /* Check and radio: drawn rather than loaded, and reserving their space either way so
147
+ choosing a row does not shift the label under the pointer. */
148
+ [data-enigma-menu-check] {
149
+ flex: none; width: 1rem; height: 1rem;
150
+ display: grid; place-items: center;
151
+ color: var(--enigma-menu-accent);
152
+ }
153
+ [data-enigma-menu-check]::after { content: ""; width: 0.35rem; height: 0.6rem; opacity: 0; }
154
+ [data-enigma-menu-item][aria-checked="true"] [data-enigma-menu-check]::after {
155
+ opacity: 1;
156
+ border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
157
+ transform: rotate(45deg) translate(-0.05rem, -0.05rem);
158
+ }
159
+ [data-enigma-menu-item][role="menuitemradio"] [data-enigma-menu-check]::after {
160
+ width: 0.4rem; height: 0.4rem; border-radius: 999px; background: currentColor;
161
+ transform: none; border: 0;
162
+ }
163
+
164
+ [data-enigma-menu-separator] {
165
+ height: 1px; margin: 0.25rem 0.25rem;
166
+ background: var(--enigma-menu-border);
167
+ }
168
+
169
+ [data-enigma-menu-label] {
170
+ margin: 0.375rem 0 0.125rem; padding: 0 0.5rem;
171
+ font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
172
+ color: var(--enigma-menu-muted);
173
+ }
174
+
175
+ [data-enigma-menu-status] {
176
+ margin: 0; padding: 0.625rem 0.5rem;
177
+ font-size: var(--enigma-menu-font-size); color: var(--enigma-menu-muted);
178
+ /* The quoted query is cut in the text as well, but a pasted string with no spaces has no
179
+ break opportunity at all - so this is what stops it stretching the panel. */
180
+ overflow-wrap: anywhere;
181
+ }
182
+ [data-enigma-menu-status][data-error] { color: var(--enigma-menu-danger); }
183
+
184
+ /* The end-of-window marker: one pixel, so it has a box to be scrolled into. */
185
+ [data-enigma-menu-more] { height: 1px; }
186
+ `;
@@ -5,7 +5,7 @@ export { type MarqueeOptions, type MarqueeInstance, type MarqueeHover } from "@/
5
5
  export { useInput, type UseInputResult } from "@/react/use-input";
6
6
  export { useSearch, type UseSearchResult } from "@/react/use-search";
7
7
  export { type InputOptions, type InputAction, type InputIcon } from "@/core/input";
8
- export { type SearchOptions, type SearchMatch, type FuseConstructor } from "@/core/search";
8
+ export { shortenQuery, type SearchOptions, type SearchMatch, type FuseConstructor } from "@/core/search";
9
9
  export { useButton, type UseButtonResult } from "@/react/use-button";
10
10
  export { Button, type ButtonProps } from "@/react/button";
11
11
  export { setLinkComponent, getLinkComponent } from "@/react/link";
@@ -36,7 +36,8 @@ export {
36
36
  type PasswordScore
37
37
  } from "@/core/password";
38
38
  export { useNotifications, createNotificationQueue, defaultQueue, type UseNotificationsResult } from "@/react/use-notifications";
39
- export { Toaster, type ToasterProps, type ToastPosition, type ToastControls } from "@/react/toaster";
39
+ export { Toaster, toast, useSonner, type ToasterProps } from "@/react/toaster";
40
+ export type { ToastT, ExternalToast, ToastClassnames, Action } from "@/react/toast/types";
40
41
  export { useNetworkState } from "@/react/use-network-state";
41
42
  export { type NetworkState } from "@/core/network";
42
43
  export { RelativeTime, type RelativeTimeProps } from "@/react/relative-time";
@@ -90,3 +91,70 @@ export {
90
91
  type RecentEntry,
91
92
  type RecentStore
92
93
  } from "@/core/palette";
94
+ export {
95
+ Select,
96
+ SelectRoot,
97
+ SelectTrigger,
98
+ SelectValue,
99
+ SelectContent,
100
+ SelectSearch,
101
+ SelectList,
102
+ SelectOptionRow,
103
+ useSelectContext,
104
+ type SelectProps,
105
+ type SelectRootProps,
106
+ type SelectSingleProps,
107
+ type SelectMultipleProps,
108
+ type SelectItem,
109
+ type SelectContextValue
110
+ } from "@/react/select";
111
+ export { createSelect, type SelectOption, type SelectOptions, type SelectState, type SelectInstance } from "@/core/select";
112
+ export {
113
+ ContextMenu,
114
+ ContextMenuRoot,
115
+ ContextMenuTrigger,
116
+ ContextMenuContent,
117
+ ContextMenuPanel,
118
+ ContextMenuSearch,
119
+ ContextMenuList,
120
+ ContextMenuRow,
121
+ useContextMenuContext,
122
+ type ContextMenuProps,
123
+ type ContextMenuRootProps,
124
+ type ContextMenuTriggerProps,
125
+ type ContextMenuContentProps,
126
+ type ContextMenuItem,
127
+ type ContextMenuNode,
128
+ type ContextMenuContextValue
129
+ } from "@/react/context-menu";
130
+ export {
131
+ createContextMenu,
132
+ isAction,
133
+ type ContextMenuEntry,
134
+ type ContextMenuAction,
135
+ type ContextMenuOptions,
136
+ type ContextMenuState,
137
+ type ContextMenuInstance
138
+ } from "@/core/context-menu";
139
+ export {
140
+ SelectionList,
141
+ useSelection,
142
+ type SelectionListProps,
143
+ type UseSelectionOptions,
144
+ type UseSelectionResult,
145
+ type SelectionRenderer,
146
+ type SelectionRowRender,
147
+ type MarqueeRect
148
+ } from "@/react/selection";
149
+ export {
150
+ createSelection,
151
+ DEFAULT_SELECTION_SHORTCUTS,
152
+ type SelectionCommand,
153
+ type SelectionCommandName,
154
+ type SelectionCommandEvent,
155
+ type SelectionShortcuts,
156
+ type SelectionOptions,
157
+ type SelectionState,
158
+ type SelectionInstance
159
+ } from "@/core/selection";
160
+ export { parseShortcut, matchesShortcut, shortcutTokens, shortcutText, isApplePlatform, type Shortcut, type ShortcutSpec } from "@/core/keys";
@@ -3,7 +3,7 @@
3
3
  import { Slot } from "@/react/slot";
4
4
  import { createPortal } from "react-dom";
5
5
  import { PaletteContext, usePaletteContext, type PaletteRow } from "@/react/palette/context";
6
- import { createSearch, subsequenceMatcher, type SearchInstance, type SearchMatch, type SearchOptions } from "@/core/search";
6
+ import { createSearch, subsequenceMatcher, shortenQuery, type SearchInstance, type SearchMatch, type SearchOptions } from "@/core/search";
7
7
  import { createRecentStore, groupRows, moveActive, shortcutLabel, isPaletteShortcut, type RecentEntry, type PaletteKey } from "@/core/palette";
8
8
  import {
9
9
  useCallback, useEffect, useId, useMemo, useRef, useState,
@@ -576,7 +576,7 @@ export function PaletteList<Item>({ children, empty, heading, ...props }: Palett
576
576
  data-enigma-palette-list=""
577
577
  >
578
578
  {palette.rows.length === 0
579
- ? empty ?? <p data-enigma-palette-empty="">{palette.query.trim() ? `Nothing matches "${palette.query.trim()}".` : "Type to search."}</p>
579
+ ? empty ?? <p data-enigma-palette-empty="">{palette.query.trim() ? `Nothing matches "${shortenQuery(palette.query)}".` : "Type to search."}</p>
580
580
  : groups.map((group) => (
581
581
  <div key={group.label} role="group" aria-label={group.label} data-enigma-palette-group="">
582
582
  {heading !== null && (
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import type { ReactNode, RefObject } from "react";
5
+ import type { SelectInstance, SelectOption, SelectState } from "@/core/select";
6
+
7
+ /**
8
+ * One option, as React draws it. The core carries `icon` as `unknown` because it cannot
9
+ * know what a node is; here it is one.
10
+ */
11
+ export interface SelectItem extends SelectOption {
12
+ icon?: ReactNode;
13
+ }
14
+
15
+ export interface SelectContextValue {
16
+ instance: SelectInstance;
17
+ state: SelectState;
18
+ /** Open or close the panel. React owns that flag; the instance follows it. */
19
+ setOpen: (open: boolean) => void;
20
+ disabled: boolean;
21
+ /** Whether the trigger offers a × that empties the selection. */
22
+ clearable: boolean;
23
+ searchable: boolean;
24
+ /**
25
+ * There is nothing to choose from at all - not a filter that matched nothing, an option
26
+ * list that is empty. The trigger says so and refuses to open, because a panel holding one
27
+ * line of apology is worse than a control that reads as unavailable.
28
+ */
29
+ empty: boolean;
30
+ /** The options have not arrived yet, which is a different thing from there being none. */
31
+ loading: boolean;
32
+ emptyLabel: ReactNode;
33
+ loadingLabel: ReactNode;
34
+ /** ids the parts need to point at each other. */
35
+ ids: { trigger: string; list: string; field: string; };
36
+ optionId: (index: number) => string;
37
+ triggerRef: RefObject<HTMLButtonElement | null>;
38
+ fieldRef: RefObject<HTMLInputElement | null>;
39
+ /** Close and put focus back where it came from - the trigger, always. */
40
+ close: () => void;
41
+ /** The keys the list answers to, shared by the trigger and the search field. */
42
+ onListKeyDown: (event: React.KeyboardEvent) => void;
43
+ }
44
+
45
+ /**
46
+ * Null rather than a default: a part rendered outside its Root is a mistake with an obvious
47
+ * fix, and a silent default would leave it half-working - a trigger that opens nothing, an
48
+ * option that selects into the void.
49
+ */
50
+ export const SelectContext = createContext<SelectContextValue | null>(null);
51
+
52
+ export function useSelectContext(part: string): SelectContextValue {
53
+ const value = useContext(SelectContext);
54
+ if (!value) throw new Error(`<${part}> must be rendered inside <Select.Root>.`);
55
+ return value;
56
+ }
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import * as parts from "@/react/select/root";
5
+ import { useSelectContext } from "@/react/select/context";
6
+ import type { SelectRootProps, SelectTriggerProps, SelectContentProps, SelectListProps } from "@/react/select/root";
7
+
8
+ export type SelectProps = SelectRootProps & {
9
+ /** What the trigger says with nothing chosen. */
10
+ placeholder?: ReactNode;
11
+ searchPlaceholder?: string;
12
+ /** Removable tags instead of a comma-separated line. Default: on when `multiple`. */
13
+ tags?: boolean;
14
+ maxTags?: number;
15
+ /** Shown when the filter matches nothing. */
16
+ empty?: ReactNode;
17
+ /** Draw a row yourself: the icon, label, description and check are the default. */
18
+ renderOption?: SelectListProps["children"];
19
+ /** On the root, which is the element with a size. `triggerProps` dresses the button. */
20
+ className?: string;
21
+ triggerProps?: SelectTriggerProps;
22
+ contentProps?: SelectContentProps;
23
+ };
24
+
25
+ /**
26
+ * The select, assembled.
27
+ *
28
+ * ```tsx
29
+ * <Select
30
+ * options={[{ value: "es", label: "Spain", icon: <Flag code="es" /> }]}
31
+ * value={country}
32
+ * onValueChange={setCountry}
33
+ * />
34
+ * ```
35
+ *
36
+ * Everything it renders is a part you can render yourself instead - see `Select.Root` for
37
+ * the anatomy. This is those parts in the order they belong in, not a different component.
38
+ *
39
+ * `multiple` changes the types with it: `value` and what the change reports are a list, and
40
+ * the compiler says so at the call site. A filter appears on its own once the list is long
41
+ * enough to need one, fuzzy if you hand over Fuse's constructor and plain otherwise.
42
+ */
43
+ export function Select(props: SelectProps): ReactNode {
44
+ const {
45
+ placeholder = "Select",
46
+ searchPlaceholder = "Search",
47
+ tags,
48
+ maxTags,
49
+ empty,
50
+ renderOption,
51
+ className,
52
+ triggerProps,
53
+ contentProps,
54
+ children,
55
+ ...rest
56
+ } = props;
57
+
58
+ // The union survives as far as the props object; the rest spread flattens it, so the
59
+ // cast puts back what TypeScript dropped rather than widening anything.
60
+ const root = rest as SelectRootProps;
61
+
62
+ return (
63
+ <parts.SelectRoot {...root} className={className}>
64
+ <parts.SelectTrigger {...triggerProps}>
65
+ <parts.SelectValue placeholder={placeholder} tags={tags} maxTags={maxTags} />
66
+ </parts.SelectTrigger>
67
+ <parts.SelectContent {...contentProps}>
68
+ {/* Rendered on the root's own decision: `searchable="auto"` is a count, and
69
+ the field has to appear on the same rule the keyboard follows. */}
70
+ <SearchSlot placeholder={searchPlaceholder} />
71
+ <parts.SelectList empty={empty}>{renderOption}</parts.SelectList>
72
+ </parts.SelectContent>
73
+ {children}
74
+ </parts.SelectRoot>
75
+ );
76
+ }
77
+
78
+ /** The filter, only where the root decided there is one. */
79
+ function SearchSlot({ placeholder }: { placeholder: string; }): ReactNode {
80
+ const { searchable } = useSelectContext("Select");
81
+ return searchable ? <parts.SelectSearch placeholder={placeholder} /> : null;
82
+ }
83
+
84
+ Select.Root = parts.SelectRoot;
85
+ Select.Trigger = parts.SelectTrigger;
86
+ Select.Value = parts.SelectValue;
87
+ Select.Content = parts.SelectContent;
88
+ Select.Search = parts.SelectSearch;
89
+ Select.List = parts.SelectList;
90
+ Select.Option = parts.SelectOptionRow;
91
+
92
+ export * from "@/react/select/root";
93
+ export { useSelectContext, type SelectItem, type SelectContextValue } from "@/react/select/context";
94
+ export { SELECT_STYLES } from "@/react/select/styles";
95
+ export { createSelect, SELECT_SEARCH_KEYS } from "@/core/select";
96
+ export type { SelectOption, SelectOptions, SelectState, SelectInstance, SelectMoveKey } from "@/core/select";