@ctrl-ui/react 0.8.9 → 0.8.11

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 (40) hide show
  1. package/dist/knob-contracts/app-shell-header-knobs.d.ts +4 -0
  2. package/dist/knob-contracts/app-shell-header-knobs.js +10 -0
  3. package/dist/knob-contracts/emoji-picker-knobs.d.ts +1 -1
  4. package/dist/knob-contracts/emoji-picker-knobs.js +1 -0
  5. package/dist/knob-contracts/icon-picker-knobs.d.ts +4 -0
  6. package/dist/knob-contracts/icon-picker-knobs.js +9 -0
  7. package/dist/knob-contracts/markdown-knobs.d.ts +1 -1
  8. package/dist/knob-contracts/markdown-knobs.js +3 -1
  9. package/dist/skin.d.ts +2 -0
  10. package/dist/styles/index.css +8 -6
  11. package/dist/styles/recipes/accordion.css +13 -1
  12. package/dist/styles/recipes/app-shell-header.css +43 -0
  13. package/dist/styles/recipes/collapsible.css +15 -0
  14. package/dist/styles/recipes/emoji-picker.css +84 -1
  15. package/dist/styles/recipes/icon-picker.css +101 -0
  16. package/dist/styles/recipes/markdown.css +66 -0
  17. package/dist/styles/recipes/popup-motion.css +10 -0
  18. package/dist/styles/recipes/scroll-area.css +53 -27
  19. package/dist/styles/recipes/tabs.css +104 -0
  20. package/dist/styles/recipes/tree.css +13 -1
  21. package/dist/ui/app-shell.d.ts +16 -0
  22. package/dist/ui/app-shell.js +39 -0
  23. package/dist/ui/collapsible.js +0 -1
  24. package/dist/ui/emoji-icon-picker.d.ts +13 -0
  25. package/dist/ui/emoji-icon-picker.js +50 -0
  26. package/dist/ui/emoji-picker.d.ts +14 -3
  27. package/dist/ui/emoji-picker.js +194 -15
  28. package/dist/ui/icon-picker.d.ts +35 -0
  29. package/dist/ui/icon-picker.js +140 -0
  30. package/dist/ui/markdown-elements.js +6 -10
  31. package/dist/ui/markdown.d.ts +6 -5
  32. package/dist/ui/markdown.js +12 -4
  33. package/dist/ui/page-layout.d.ts +1 -0
  34. package/dist/ui/page-layout.js +6 -2
  35. package/dist/ui/popover.d.ts +3 -0
  36. package/dist/ui/popover.js +36 -1
  37. package/dist/ui/scroll-area.d.ts +4 -1
  38. package/dist/ui/scroll-area.js +21 -6
  39. package/dist/ui/tabs.js +58 -22
  40. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  @layer components {
2
2
  :where([data-control-family="tabs"][data-slot="root"]) {
3
+ position: relative;
3
4
  --cui-tabs-trigger-radius: var(--radius-control);
4
5
  --cui-tabs-list-padding: calc(var(--spacing) * 1);
5
6
  --cui-tabs-list-radius: calc(
@@ -178,6 +179,109 @@
178
179
  outline-style: none;
179
180
  }
180
181
 
182
+ @supports ((anchor-name: --aui-slide-panel) and (anchor-scope: --aui-slide-panel)) {
183
+ :where([data-control-family="tabs"][data-slot="panel"][data-slide="panel"][inert][data-slide-exiting][data-ending-style]) {
184
+ display: block;
185
+ position: absolute;
186
+ top: var(--_tabs-panel-top);
187
+ left: var(--_tabs-panel-left);
188
+ right: auto;
189
+ bottom: auto;
190
+ width: var(--_tabs-panel-width);
191
+ height: var(--_tabs-panel-height);
192
+ margin: 0;
193
+ overflow: clip;
194
+ animation: aui-slide-exit-hold var(--duration-slow) var(--ease-emphasized) both;
195
+ }
196
+
197
+ @supports (height: anchor-size(height)) {
198
+ :where([data-control-family="tabs"][data-slot="panel"][data-slide="panel"][inert][data-slide-exiting][data-ending-style]) {
199
+ height: anchor-size(height);
200
+ }
201
+ }
202
+
203
+ :where(
204
+ [data-control-family="tabs"][data-slot="panel"][data-slide="panel"][inert][data-ending-style][data-activation-direction="right"]
205
+ ) {
206
+ translate: -100% 0;
207
+ }
208
+
209
+ :where(
210
+ [data-control-family="tabs"][data-slot="panel"][data-slide="panel"][inert][data-ending-style][data-activation-direction="left"]
211
+ ) {
212
+ translate: 100% 0;
213
+ }
214
+
215
+ :where(
216
+ [data-control-family="tabs"][data-slot="panel"][data-slide="panel"][inert][data-ending-style][data-activation-direction="down"]
217
+ ) {
218
+ translate: 0 -100%;
219
+ }
220
+
221
+ :where([data-control-family="tabs"][data-slot="panel"][data-slide="panel"][inert][data-ending-style][data-activation-direction="up"]) {
222
+ translate: 0 100%;
223
+ }
224
+
225
+ :where(
226
+ [data-control-family="tabs"][data-slot="root"]:has(> [data-slot="panel"][data-starting-style])
227
+ > [data-slot="panel"][inert][data-ending-style]
228
+ ) {
229
+ translate: 0 0;
230
+ transition: none;
231
+ }
232
+
233
+ :where([data-control-family="tabs"][data-slot="panel"][data-slide="panel"][data-starting-style]) {
234
+ transition: none;
235
+ }
236
+
237
+ :where([data-control-family="tabs"][data-slot="root"][data-slide-starting] > [data-slot="panel"][data-slide="panel"]) {
238
+ transition: none;
239
+ }
240
+
241
+ :where([data-control-family="tabs"][data-slot="root"][data-slide-starting] > [data-slot="panel"][inert]) {
242
+ translate: 0 0;
243
+ }
244
+
245
+ :where(
246
+ [data-control-family="tabs"][data-slot="root"][data-slide-starting]
247
+ > [data-slot="panel"]:not([inert])[data-activation-direction="right"]
248
+ ) {
249
+ translate: 100% 0;
250
+ }
251
+
252
+ :where(
253
+ [data-control-family="tabs"][data-slot="root"][data-slide-starting]
254
+ > [data-slot="panel"]:not([inert])[data-activation-direction="left"]
255
+ ) {
256
+ translate: -100% 0;
257
+ }
258
+
259
+ :where(
260
+ [data-control-family="tabs"][data-slot="root"][data-slide-starting]
261
+ > [data-slot="panel"]:not([inert])[data-activation-direction="down"]
262
+ ) {
263
+ translate: 0 100%;
264
+ }
265
+
266
+ :where(
267
+ [data-control-family="tabs"][data-slot="root"][data-slide-starting]
268
+ > [data-slot="panel"]:not([inert])[data-activation-direction="up"]
269
+ ) {
270
+ translate: 0 -100%;
271
+ }
272
+
273
+ @supports (height: calc-size(auto, size)) {
274
+ :where([data-control-family="tabs"][data-slot="root"][data-slide-starting] > [data-slot="panel"][data-slide="panel"]:not([inert])) {
275
+ height: var(--aui-slide-prev-height, calc-size(auto, size));
276
+ }
277
+ }
278
+ }
279
+
280
+ :where([data-control-family="tabs"][data-slot="root"][data-orientation="vertical"] > [data-slot="panel"]) {
281
+ min-width: 0;
282
+ flex: 1;
283
+ }
284
+
181
285
  :where(
182
286
  [data-control-family="tabs"][data-slot="list"][data-orientation="horizontal"]:not([data-variant="browser"]) ~ [data-slot="panel"]
183
287
  ) {
@@ -93,7 +93,6 @@
93
93
 
94
94
  :where([data-control-family="tree"][data-slot="item-content"]) {
95
95
  height: var(--collapsible-panel-height);
96
- overflow: hidden;
97
96
  opacity: 1;
98
97
  transition-property: height, opacity;
99
98
  transition-duration: var(--duration-base);
@@ -102,9 +101,22 @@
102
101
 
103
102
  :where([data-control-family="tree"][data-slot="item-content"]:is([data-starting-style], [data-ending-style])) {
104
103
  height: 0;
104
+ overflow: clip;
105
105
  opacity: 0;
106
106
  }
107
107
 
108
+ @supports (transition-behavior: allow-discrete) {
109
+ :where([data-control-family="tree"][data-slot="item-content"]) {
110
+ transition-property: height, opacity, overflow;
111
+ transition-timing-function: var(--ease-emphasized), var(--ease-emphasized), step-end;
112
+ transition-behavior: normal, normal, allow-discrete;
113
+ }
114
+
115
+ :where([data-control-family="tree"][data-slot="item-content"][data-ending-style]) {
116
+ transition-timing-function: var(--ease-emphasized), var(--ease-emphasized), step-start;
117
+ }
118
+ }
119
+
108
120
  :where([data-control-family="tree"][data-slot="item-group"]) {
109
121
  translate: 0 0;
110
122
  transition: translate var(--duration-base) var(--ease-emphasized);
@@ -0,0 +1,16 @@
1
+ import { SidebarProviderProps } from "./sidebar-provider.js";
2
+ import { SidebarInsetProps } from "./sidebar.js";
3
+ import { AppShellHeaderKnobStyle } from "../knob-contracts/app-shell-header-knobs.js";
4
+ import { PageScroll } from "./page-layout.js";
5
+ import { CSSProperties, ComponentProps } from "react";
6
+ //#region src/ui/app-shell.d.ts
7
+ export type AppShellProps = SidebarProviderProps & {
8
+ scroll?: PageScroll;
9
+ };
10
+ export declare function AppShell({ scroll, layout, className, ...props }: AppShellProps): import("react").JSX.Element;
11
+ export declare function AppShellContent({ className, ...props }: SidebarInsetProps): import("react").JSX.Element;
12
+ export type AppShellHeaderProps = Omit<ComponentProps<"header">, "style"> & {
13
+ style?: CSSProperties & AppShellHeaderKnobStyle;
14
+ };
15
+ export declare function AppShellHeader({ className, ...props }: AppShellHeaderProps): import("react").JSX.Element;
16
+ //#endregion
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { SidebarProvider } from "./sidebar-provider.js";
4
+ import { SidebarInset } from "./sidebar.js";
5
+ import { PageScrollContext, usePageScroll } from "./page-layout.js";
6
+ import { jsx } from "react/jsx-runtime";
7
+ //#region src/ui/app-shell.tsx
8
+ function AppShell({ scroll = "auto", layout = "viewport", className, ...props }) {
9
+ const resolvedScroll = usePageScroll(scroll);
10
+ return /* @__PURE__ */ jsx(PageScrollContext.Provider, {
11
+ value: resolvedScroll,
12
+ children: /* @__PURE__ */ jsx(SidebarProvider, {
13
+ "data-app-shell": "",
14
+ "data-scroll": resolvedScroll,
15
+ layout,
16
+ className: cn(layout === "viewport" && (resolvedScroll === "page" ? "min-h-dvh" : "h-dvh min-h-0 overflow-hidden"), className),
17
+ ...props
18
+ })
19
+ });
20
+ }
21
+ function AppShellContent({ className, ...props }) {
22
+ const scroll = usePageScroll();
23
+ return /* @__PURE__ */ jsx(SidebarInset, {
24
+ "data-app-shell-content": "",
25
+ className: cn("min-h-0", scroll !== "page" && "overflow-hidden", className),
26
+ ...props
27
+ });
28
+ }
29
+ function AppShellHeader({ className, ...props }) {
30
+ return /* @__PURE__ */ jsx("header", {
31
+ "data-control-ui": "app-shell",
32
+ "data-control-family": "app-shell-header",
33
+ "data-slot": "root",
34
+ className: cn("sticky top-0 z-30 flex shrink-0 items-center gap-2", className),
35
+ ...props
36
+ });
37
+ }
38
+ //#endregion
39
+ export { AppShell, AppShellContent, AppShellHeader };
@@ -52,7 +52,6 @@ function CollapsibleTrigger({ render, className, children, ...props }) {
52
52
  }
53
53
  function CollapsibleContent({ className, children, ...props }) {
54
54
  return /* @__PURE__ */ jsx(Collapsible$1.Panel, {
55
- className: "h-[var(--collapsible-panel-height)] overflow-hidden data-ending-style:h-0 data-starting-style:h-0",
56
55
  ...props,
57
56
  render: (renderProps, state) => /* @__PURE__ */ jsx("div", {
58
57
  "data-control-ui": "collapsible",
@@ -0,0 +1,13 @@
1
+ import { EmojiPicker } from "./emoji-picker.js";
2
+ import { IconPicker } from "./icon-picker.js";
3
+ import { Tabs, TabsList } from "./tabs.js";
4
+ import { ComponentProps } from "react";
5
+ //#region src/ui/emoji-icon-picker.d.ts
6
+ export declare function EmojiIconPicker({ children, defaultValue, ...props }: ComponentProps<typeof Tabs<"emoji" | "icons">>): import("react").JSX.Element;
7
+ export declare function EmojiIconPickerTabs({ emojiLabel, iconsLabel, ...props }: Omit<ComponentProps<typeof TabsList>, "children"> & {
8
+ emojiLabel?: string;
9
+ iconsLabel?: string;
10
+ }): import("react").JSX.Element;
11
+ export declare function EmojiIconPickerEmoji(props: ComponentProps<typeof EmojiPicker>): import("react").JSX.Element;
12
+ export declare function EmojiIconPickerIcons(props: ComponentProps<typeof IconPicker>): import("react").JSX.Element;
13
+ //#endregion
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { PopoverViewport } from "./popover.js";
3
+ import { Tabs, TabsList, TabsPanel, TabsTab } from "./tabs.js";
4
+ import { EmojiPicker } from "./emoji-picker.js";
5
+ import { IconPicker } from "./icon-picker.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/ui/emoji-icon-picker.tsx
8
+ function EmojiIconPicker({ children, defaultValue = "emoji", ...props }) {
9
+ return /* @__PURE__ */ jsx(Tabs, {
10
+ "data-control-ui": "emoji-icon-picker",
11
+ "data-control-family": "tabs",
12
+ "data-slot": "root",
13
+ defaultValue,
14
+ ...props,
15
+ children: /* @__PURE__ */ jsx(PopoverViewport, { children })
16
+ });
17
+ }
18
+ function EmojiIconPickerTabs({ emojiLabel = "Emoji", iconsLabel = "Icons", ...props }) {
19
+ return /* @__PURE__ */ jsxs(TabsList, {
20
+ size: "xs",
21
+ className: "mx-2 mt-2",
22
+ "aria-label": "Choose emoji or icon",
23
+ ...props,
24
+ children: [/* @__PURE__ */ jsx(TabsTab, {
25
+ value: "emoji",
26
+ children: emojiLabel
27
+ }), /* @__PURE__ */ jsx(TabsTab, {
28
+ value: "icons",
29
+ children: iconsLabel
30
+ })]
31
+ });
32
+ }
33
+ function EmojiIconPickerEmoji(props) {
34
+ return /* @__PURE__ */ jsx(TabsPanel, {
35
+ value: "emoji",
36
+ "data-slide": void 0,
37
+ className: "mt-0",
38
+ children: /* @__PURE__ */ jsx(EmojiPicker, { ...props })
39
+ });
40
+ }
41
+ function EmojiIconPickerIcons(props) {
42
+ return /* @__PURE__ */ jsx(TabsPanel, {
43
+ value: "icons",
44
+ "data-slide": void 0,
45
+ className: "mt-0",
46
+ children: /* @__PURE__ */ jsx(IconPicker, { ...props })
47
+ });
48
+ }
49
+ //#endregion
50
+ export { EmojiIconPicker, EmojiIconPickerEmoji, EmojiIconPickerIcons, EmojiIconPickerTabs };
@@ -1,16 +1,27 @@
1
1
  import { EmojiPickerKnobStyle } from "../knob-contracts/emoji-picker-knobs.js";
2
2
  import { CSSProperties, ComponentProps } from "react";
3
- import { EmojiPicker as EmojiPicker$1 } from "frimousse";
3
+ import { Emoji, EmojiPicker as EmojiPicker$1 } from "frimousse";
4
4
  //#region src/ui/emoji-picker.d.ts
5
5
  type EmojiPickerStyle = {
6
6
  style?: CSSProperties & EmojiPickerKnobStyle;
7
7
  };
8
8
  export type EmojiPickerProps = Omit<ComponentProps<typeof EmojiPicker$1.Root>, "style"> & EmojiPickerStyle;
9
- export declare function EmojiPicker({ className, columns, ...props }: EmojiPickerProps): import("react").JSX.Element;
9
+ export declare function EmojiPicker({ className, columns, style, ref, onEmojiSelect, resolveEmojiData, ...props }: EmojiPickerProps): import("react").JSX.Element;
10
+ export declare function EmojiPickerCategories({ "aria-label": ariaLabel, onKeyDown, ...props }: ComponentProps<"div">): import("react").JSX.Element | null;
11
+ export type EmojiPickerRecentProps = ComponentProps<"div"> & {
12
+ emojis: readonly Emoji[];
13
+ label?: string;
14
+ };
15
+ export type EmojiPickerReactionsProps = ComponentProps<"div"> & {
16
+ emojis: readonly Emoji[];
17
+ onEmojiSelect: (emoji: Emoji) => void;
18
+ };
19
+ export declare function EmojiPickerReactions({ emojis, onEmojiSelect, "aria-label": ariaLabel, onKeyDown, ...props }: EmojiPickerReactionsProps): import("react").JSX.Element;
20
+ export declare function EmojiPickerRecent({ emojis, label, ...props }: EmojiPickerRecentProps): import("react").JSX.Element | null;
10
21
  export type EmojiPickerSearchProps = Omit<ComponentProps<typeof EmojiPicker$1.Search>, "style"> & EmojiPickerStyle;
11
22
  export declare function EmojiPickerSearch({ className, placeholder, "aria-label": ariaLabel, ...props }: EmojiPickerSearchProps): import("react").JSX.Element;
12
23
  export type EmojiPickerContentProps = Omit<ComponentProps<typeof EmojiPicker$1.Viewport>, "style"> & EmojiPickerStyle;
13
- export declare function EmojiPickerContent({ className, ...props }: EmojiPickerContentProps): import("react").JSX.Element;
24
+ export declare function EmojiPickerContent({ className, ref, ...props }: EmojiPickerContentProps): import("react").JSX.Element;
14
25
  export type EmojiPickerFooterProps = Omit<ComponentProps<"div">, "style" | "children"> & EmojiPickerStyle & {
15
26
  placeholder?: string;
16
27
  };
@@ -1,18 +1,180 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { Spinner } from "./spinner.js";
4
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
- import { SearchIcon } from "lucide-react";
6
- import { EmojiPicker as EmojiPicker$1 } from "frimousse";
4
+ import { createContext, useContext, useImperativeHandle, useRef, useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ import { FlagIcon, HandIcon, HashIcon, HeartIcon, LeafIcon, LightbulbIcon, PizzaIcon, PlaneIcon, SearchIcon, SmileIcon, VolleyballIcon } from "lucide-react";
7
+ import { Toolbar } from "@base-ui/react/toolbar";
8
+ import { EmojiPicker as EmojiPicker$1, defaultEmojiDataResolver } from "frimousse";
7
9
  //#region src/ui/emoji-picker.tsx
8
- function EmojiPicker({ className, columns = 9, ...props }) {
9
- return /* @__PURE__ */ jsx(EmojiPicker$1.Root, {
10
+ const EmojiPickerContext = createContext(null);
11
+ function useEmojiPicker() {
12
+ const picker = useContext(EmojiPickerContext);
13
+ if (!picker) throw new Error("EmojiPicker parts must be rendered inside <EmojiPicker>.");
14
+ return picker;
15
+ }
16
+ function EmojiPicker({ className, columns = 8, style, ref, onEmojiSelect, resolveEmojiData = defaultEmojiDataResolver, ...props }) {
17
+ const rootRef = useRef(null);
18
+ const viewportRef = useRef(null);
19
+ const [loadState, setLoadState] = useState({ status: "loading" });
20
+ const [retryAttempt, setRetryAttempt] = useState(0);
21
+ useImperativeHandle(ref, () => {
22
+ if (!rootRef.current) throw new Error("EmojiPicker root is not mounted.");
23
+ return rootRef.current;
24
+ });
25
+ const loadEmojiData = async (locale, options) => {
26
+ setLoadState({ status: "loading" });
27
+ try {
28
+ const data = await resolveEmojiData(locale, options);
29
+ if (!options.signal?.aborted) setLoadState({
30
+ status: "ready",
31
+ data
32
+ });
33
+ return data;
34
+ } catch (error) {
35
+ if (!options.signal?.aborted) setLoadState({ status: "error" });
36
+ throw error;
37
+ }
38
+ };
39
+ const retryEmojiData = () => {
40
+ setLoadState({ status: "loading" });
41
+ setRetryAttempt((attempt) => attempt + 1);
42
+ };
43
+ const selectEmoji = (emoji) => onEmojiSelect?.(emoji);
44
+ const pickerStyle = {
45
+ "--_emoji-picker-columns": columns,
46
+ ...style
47
+ };
48
+ return /* @__PURE__ */ jsx(EmojiPickerContext.Provider, {
49
+ value: {
50
+ columns,
51
+ rootRef,
52
+ viewportRef,
53
+ loadState,
54
+ retryEmojiData,
55
+ selectEmoji
56
+ },
57
+ children: /* @__PURE__ */ jsx(EmojiPicker$1.Root, {
58
+ "data-control-ui": "emoji-picker",
59
+ "data-control-family": "emoji-picker",
60
+ "data-slot": "root",
61
+ columns,
62
+ className: cn("isolate flex flex-col", className),
63
+ ...props,
64
+ ref: rootRef,
65
+ style: pickerStyle,
66
+ onEmojiSelect: selectEmoji,
67
+ resolveEmojiData: loadEmojiData
68
+ }, retryAttempt)
69
+ });
70
+ }
71
+ function stopPickerNavigation(event) {
72
+ if (event.key !== "Escape" && event.key !== "Tab") event.stopPropagation();
73
+ }
74
+ const categoryIcons = /* @__PURE__ */ new Map([
75
+ [0, SmileIcon],
76
+ [1, HandIcon],
77
+ [3, LeafIcon],
78
+ [4, PizzaIcon],
79
+ [5, PlaneIcon],
80
+ [6, VolleyballIcon],
81
+ [7, LightbulbIcon],
82
+ [8, HeartIcon],
83
+ [9, FlagIcon]
84
+ ]);
85
+ function emojiCategoryRows(emojiData, columns) {
86
+ let rowOffset = 0;
87
+ return emojiData.categories.flatMap((category) => {
88
+ const emojiCount = emojiData.emojis.filter((emoji) => emoji.category === category.index).length;
89
+ if (emojiCount === 0) return [];
90
+ const startRow = rowOffset;
91
+ rowOffset += Math.ceil(emojiCount / columns);
92
+ return [{
93
+ ...category,
94
+ startRow
95
+ }];
96
+ });
97
+ }
98
+ function EmojiPickerCategories({ "aria-label": ariaLabel = "Emoji categories", onKeyDown, ...props }) {
99
+ const { columns, loadState, rootRef, viewportRef } = useEmojiPicker();
100
+ if (loadState.status !== "ready") return null;
101
+ const categories = emojiCategoryRows(loadState.data, columns);
102
+ const scrollToCategory = (startRow, categoryIndex) => {
103
+ const root = rootRef.current;
104
+ const viewport = viewportRef.current;
105
+ if (!root || !viewport) return;
106
+ const metrics = getComputedStyle(root);
107
+ const rowHeight = Number.parseFloat(metrics.getPropertyValue("--frimousse-row-height"));
108
+ const headerHeight = Number.parseFloat(metrics.getPropertyValue("--frimousse-category-header-height"));
109
+ viewport.scrollTo({ top: startRow * rowHeight + categoryIndex * headerHeight });
110
+ };
111
+ return /* @__PURE__ */ jsx(Toolbar.Root, {
112
+ "data-control-ui": "emoji-picker",
113
+ "data-control-family": "emoji-picker",
114
+ "data-slot": "categories",
115
+ "aria-label": ariaLabel,
116
+ ...props,
117
+ onKeyDown: (event) => {
118
+ onKeyDown?.(event);
119
+ stopPickerNavigation(event);
120
+ },
121
+ children: categories.map((category, categoryIndex) => {
122
+ const CategoryIcon = categoryIcons.get(category.index) ?? HashIcon;
123
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
124
+ type: "button",
125
+ "aria-label": category.label,
126
+ "data-control-ui": "emoji-picker",
127
+ "data-control-family": "emoji-picker",
128
+ "data-slot": "category",
129
+ onClick: () => scrollToCategory(category.startRow, categoryIndex),
130
+ children: /* @__PURE__ */ jsx(CategoryIcon, { "aria-hidden": "true" })
131
+ }, category.index);
132
+ })
133
+ });
134
+ }
135
+ function EmojiPickerReactions({ emojis, onEmojiSelect, "aria-label": ariaLabel = "Reactions", onKeyDown, ...props }) {
136
+ return /* @__PURE__ */ jsx(Toolbar.Root, {
137
+ "aria-label": ariaLabel,
10
138
  "data-control-ui": "emoji-picker",
11
139
  "data-control-family": "emoji-picker",
12
140
  "data-slot": "root",
13
- columns,
14
- className: cn("isolate flex h-72 w-fit flex-col", className),
15
- ...props
141
+ "data-variant": "reactions",
142
+ ...props,
143
+ onKeyDown: (event) => {
144
+ onKeyDown?.(event);
145
+ stopPickerNavigation(event);
146
+ },
147
+ children: emojis.map((emoji) => /* @__PURE__ */ jsx(Toolbar.Button, {
148
+ type: "button",
149
+ "aria-label": emoji.label,
150
+ "data-control-ui": "emoji-picker",
151
+ "data-control-family": "emoji-picker",
152
+ "data-slot": "emoji",
153
+ onClick: () => onEmojiSelect(emoji),
154
+ children: emoji.emoji
155
+ }, emoji.emoji))
156
+ });
157
+ }
158
+ function EmojiPickerRecent({ emojis, label = "Recently used", ...props }) {
159
+ const picker = useEmojiPicker();
160
+ if (emojis.length === 0) return null;
161
+ return /* @__PURE__ */ jsxs("div", {
162
+ "data-control-ui": "emoji-picker",
163
+ "data-control-family": "emoji-picker",
164
+ "data-slot": "recent",
165
+ ...props,
166
+ children: [/* @__PURE__ */ jsx("div", {
167
+ "data-control-ui": "emoji-picker",
168
+ "data-control-family": "emoji-picker",
169
+ "data-slot": "recent-label",
170
+ children: label
171
+ }), /* @__PURE__ */ jsx(EmojiPickerReactions, {
172
+ "data-control-ui": "emoji-picker",
173
+ "data-slot": "recent-grid",
174
+ emojis: emojis.slice(0, picker.columns),
175
+ "aria-label": label,
176
+ onEmojiSelect: picker.selectEmoji
177
+ })]
16
178
  });
17
179
  }
18
180
  function EmojiPickerSearch({ className, placeholder = "Search emoji…", "aria-label": ariaLabel = "Search emoji", ...props }) {
@@ -49,6 +211,7 @@ function EmojiPickerRow({ children, className, ...props }) {
49
211
  }
50
212
  function EmojiPickerEmoji({ emoji, className, ...props }) {
51
213
  return /* @__PURE__ */ jsx("button", {
214
+ type: "button",
52
215
  "data-control-ui": "emoji-picker",
53
216
  "data-control-family": "emoji-picker",
54
217
  "data-slot": "emoji",
@@ -68,14 +231,30 @@ function EmojiPickerCategoryHeader({ category, className, ...props }) {
68
231
  children: category.label
69
232
  });
70
233
  }
71
- function EmojiPickerContent({ className, ...props }) {
72
- return /* @__PURE__ */ jsxs(EmojiPicker$1.Viewport, {
234
+ function EmojiPickerContent({ className, ref, ...props }) {
235
+ const { viewportRef, loadState, retryEmojiData } = useEmojiPicker();
236
+ useImperativeHandle(ref, () => {
237
+ if (!viewportRef.current) throw new Error("EmojiPicker viewport is not mounted.");
238
+ return viewportRef.current;
239
+ });
240
+ return /* @__PURE__ */ jsx(EmojiPicker$1.Viewport, {
73
241
  "data-control-ui": "emoji-picker",
74
242
  "data-control-family": "emoji-picker",
75
243
  "data-slot": "content",
76
- className: cn("relative flex-1 outline-none", className),
244
+ className: cn("relative min-h-0 flex-1 outline-none", className),
77
245
  ...props,
78
- children: [
246
+ ref: viewportRef,
247
+ children: loadState.status === "error" ? /* @__PURE__ */ jsxs("div", {
248
+ "data-control-ui": "emoji-picker",
249
+ "data-control-family": "emoji-picker",
250
+ "data-slot": "error",
251
+ role: "alert",
252
+ children: [/* @__PURE__ */ jsx("span", { children: "Couldn’t load emoji." }), /* @__PURE__ */ jsx("button", {
253
+ type: "button",
254
+ onClick: retryEmojiData,
255
+ children: "Try again"
256
+ })]
257
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
79
258
  /* @__PURE__ */ jsx(EmojiPicker$1.Loading, {
80
259
  "data-control-ui": "emoji-picker",
81
260
  "data-control-family": "emoji-picker",
@@ -101,7 +280,7 @@ function EmojiPickerContent({ className, ...props }) {
101
280
  CategoryHeader: EmojiPickerCategoryHeader
102
281
  }
103
282
  })
104
- ]
283
+ ] })
105
284
  });
106
285
  }
107
286
  function EmojiPickerFooter({ className, placeholder = "Pick an emoji…", ...props }) {
@@ -111,7 +290,7 @@ function EmojiPickerFooter({ className, placeholder = "Pick an emoji…", ...pro
111
290
  "data-slot": "footer",
112
291
  className: cn("flex min-w-0 items-center", className),
113
292
  ...props,
114
- children: /* @__PURE__ */ jsx(EmojiPicker$1.ActiveEmoji, { children: ({ emoji }) => emoji ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
293
+ children: /* @__PURE__ */ jsx(EmojiPicker$1.ActiveEmoji, { children: ({ emoji }) => emoji ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
115
294
  "data-control-ui": "emoji-picker",
116
295
  "data-control-family": "emoji-picker",
117
296
  "data-slot": "footer-emoji",
@@ -133,4 +312,4 @@ function EmojiPickerFooter({ className, placeholder = "Pick an emoji…", ...pro
133
312
  });
134
313
  }
135
314
  //#endregion
136
- export { EmojiPicker, EmojiPickerContent, EmojiPickerFooter, EmojiPickerSearch };
315
+ export { EmojiPicker, EmojiPickerCategories, EmojiPickerContent, EmojiPickerFooter, EmojiPickerReactions, EmojiPickerRecent, EmojiPickerSearch };
@@ -0,0 +1,35 @@
1
+ import { ControlledChoice } from "../control-props.js";
2
+ import { IconPickerKnobStyle } from "../knob-contracts/icon-picker-knobs.js";
3
+ import { Input } from "./input.js";
4
+ import { CSSProperties, ComponentProps, ReactNode } from "react";
5
+ //#region src/ui/icon-picker.d.ts
6
+ export type IconPickerItem = {
7
+ value: string;
8
+ label: string;
9
+ icon: ReactNode;
10
+ keywords?: readonly string[];
11
+ };
12
+ export type IconPickerColor = {
13
+ value: string;
14
+ label: string;
15
+ color: string;
16
+ };
17
+ type IconPickerStyle = {
18
+ style?: CSSProperties & IconPickerKnobStyle;
19
+ };
20
+ export type IconPickerProps = Omit<ComponentProps<"div">, "style" | "defaultValue" | "onChange"> & IconPickerStyle & ControlledChoice & {
21
+ items: readonly IconPickerItem[];
22
+ columns?: number;
23
+ };
24
+ export declare function IconPicker({ items, columns, value, defaultValue, onValueChange, className, style, children, ...props }: IconPickerProps): import("react").JSX.Element;
25
+ export type IconPickerSearchProps = Omit<ComponentProps<typeof Input>, "value" | "defaultValue" | "onValueChange">;
26
+ export declare function IconPickerSearch({ placeholder, "aria-label": ariaLabel, className, onChange, ...props }: IconPickerSearchProps): import("react").JSX.Element;
27
+ export type IconPickerContentProps = Omit<ComponentProps<"div">, "style" | "children"> & IconPickerStyle & {
28
+ empty?: ReactNode;
29
+ };
30
+ export declare function IconPickerContent({ className, empty, "aria-label": ariaLabel, ...props }: IconPickerContentProps): import("react").JSX.Element;
31
+ export type IconPickerColorsProps = Omit<ComponentProps<"div">, "style" | "defaultValue" | "onChange" | "children"> & IconPickerStyle & ControlledChoice & {
32
+ colors: readonly IconPickerColor[];
33
+ };
34
+ export declare function IconPickerColors({ colors, "aria-label": ariaLabel, ...props }: IconPickerColorsProps): import("react").JSX.Element;
35
+ //#endregion