@dxos/react-ui-pickers 0.7.5-labs.f5080a1 → 0.7.5-main.b19bfc8

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 (33) hide show
  1. package/dist/lib/browser/index.mjs +151 -98
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +182 -134
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +151 -98
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/EmojiPicker.d.ts +3 -3
  11. package/dist/types/src/components/EmojiPicker.d.ts.map +1 -1
  12. package/dist/types/src/components/EmojiPicker.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/HuePicker.d.ts +7 -14
  14. package/dist/types/src/components/HuePicker.d.ts.map +1 -1
  15. package/dist/types/src/components/HuePicker.stories.d.ts +1 -2
  16. package/dist/types/src/components/HuePicker.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/IconPicker.d.ts +11 -0
  18. package/dist/types/src/components/IconPicker.d.ts.map +1 -0
  19. package/dist/types/src/components/IconPicker.stories.d.ts +7 -0
  20. package/dist/types/src/components/IconPicker.stories.d.ts.map +1 -0
  21. package/dist/types/src/components/ToolbarPicker.d.ts +17 -0
  22. package/dist/types/src/components/ToolbarPicker.d.ts.map +1 -0
  23. package/dist/types/src/components/index.d.ts +2 -0
  24. package/dist/types/src/components/index.d.ts.map +1 -1
  25. package/package.json +9 -10
  26. package/src/components/EmojiPicker.stories.tsx +4 -1
  27. package/src/components/EmojiPicker.tsx +5 -5
  28. package/src/components/HuePicker.stories.tsx +14 -29
  29. package/src/components/HuePicker.tsx +24 -165
  30. package/src/components/IconPicker.stories.tsx +39 -0
  31. package/src/components/IconPicker.tsx +92 -0
  32. package/src/components/ToolbarPicker.tsx +112 -0
  33. package/src/components/index.ts +2 -0
@@ -0,0 +1,17 @@
1
+ import { type FC } from 'react';
2
+ import { type ThemedClassName } from '@dxos/react-ui';
3
+ export type ToolbarPickerProps = {
4
+ Component: FC<{
5
+ value: string;
6
+ }>;
7
+ label: string;
8
+ icon: string;
9
+ values: string[];
10
+ disabled?: boolean;
11
+ defaultValue?: string;
12
+ value?: string;
13
+ onChange?: (value: string) => void;
14
+ onReset?: () => void;
15
+ };
16
+ export declare const ToolbarPickerButton: ({ Component, disabled, classNames, defaultValue: _defaultValue, value: _value, values, label, icon, onChange, onReset, }: ThemedClassName<ToolbarPickerProps>) => import("react/jsx-runtime").JSX.Element;
17
+ //# sourceMappingURL=ToolbarPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToolbarPicker.d.ts","sourceRoot":"","sources":["../../../../src/components/ToolbarPicker.tsx"],"names":[],"mappings":"AAKA,OAAc,EAAE,KAAK,EAAE,EAA+B,MAAM,OAAO,CAAC;AAEpE,OAAO,EAAsB,KAAK,eAAe,EAAoB,MAAM,gBAAgB,CAAC;AAG5F,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,EAAE,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,mBAAmB,6HAW7B,eAAe,CAAC,kBAAkB,CAAC,4CA8ErC,CAAC"}
@@ -1,3 +1,5 @@
1
1
  export * from './EmojiPicker';
2
2
  export * from './HuePicker';
3
+ export * from './IconPicker';
4
+ export * from './ToolbarPicker';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAIA,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAIA,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC"}
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-pickers",
3
- "version": "0.7.5-labs.f5080a1",
3
+ "version": "0.7.5-main.b19bfc8",
4
4
  "description": "A collection of picker components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
7
  "license": "MIT",
8
8
  "author": "DXOS.org",
9
+ "type": "module",
9
10
  "exports": {
10
11
  ".": {
11
12
  "types": "./dist/types/src/index.d.ts",
@@ -25,26 +26,24 @@
25
26
  "@emoji-mart/data": "^1.1.2",
26
27
  "@emoji-mart/react": "^1.1.1",
27
28
  "@radix-ui/react-use-controllable-state": "1.1.0",
28
- "@dxos/react-ui-types": "0.7.5-labs.f5080a1",
29
- "@dxos/util": "0.7.5-labs.f5080a1"
29
+ "@dxos/react-ui-types": "0.7.5-main.b19bfc8",
30
+ "@dxos/util": "0.7.5-main.b19bfc8"
30
31
  },
31
32
  "devDependencies": {
32
- "@phosphor-icons/react": "^2.1.5",
33
33
  "@types/react": "~18.2.0",
34
34
  "@types/react-dom": "~18.2.0",
35
35
  "react": "~18.2.0",
36
36
  "react-dom": "~18.2.0",
37
37
  "vite": "5.4.7",
38
- "@dxos/react-ui": "0.7.5-labs.f5080a1",
39
- "@dxos/react-ui-theme": "0.7.5-labs.f5080a1",
40
- "@dxos/storybook-utils": "0.7.5-labs.f5080a1"
38
+ "@dxos/react-ui": "0.7.5-main.b19bfc8",
39
+ "@dxos/react-ui-theme": "0.7.5-main.b19bfc8",
40
+ "@dxos/storybook-utils": "0.7.5-main.b19bfc8"
41
41
  },
42
42
  "peerDependencies": {
43
- "@phosphor-icons/react": "^2.1.5",
44
43
  "react": "~18.2.0",
45
44
  "react-dom": "~18.2.0",
46
- "@dxos/react-ui-theme": "0.7.5-labs.f5080a1",
47
- "@dxos/react-ui": "0.7.5-labs.f5080a1"
45
+ "@dxos/react-ui": "0.7.5-main.b19bfc8",
46
+ "@dxos/react-ui-theme": "0.7.5-main.b19bfc8"
48
47
  },
49
48
  "publishConfig": {
50
49
  "access": "public"
@@ -53,7 +53,10 @@ export const Block: StoryObj<EmojiPickerProps> = {
53
53
 
54
54
  const meta: Meta = {
55
55
  title: 'ui/react-ui-pickers/EmojiPicker',
56
- decorators: [withTheme, withLayout({ fullscreen: false, tooltips: true })],
56
+ decorators: [withTheme, withLayout({ tooltips: true })],
57
+ parameters: {
58
+ layout: 'centered',
59
+ },
57
60
  };
58
61
 
59
62
  export default meta;
@@ -4,7 +4,6 @@
4
4
 
5
5
  import emojiData from '@emoji-mart/data';
6
6
  import EmojiMart from '@emoji-mart/react';
7
- import { ArrowCounterClockwise, CaretDown, UserCircle } from '@phosphor-icons/react';
8
7
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
9
8
  import React, { useRef, useState } from 'react';
10
9
 
@@ -18,8 +17,8 @@ import {
18
17
  useMediaQuery,
19
18
  useThemeContext,
20
19
  useTranslation,
20
+ Icon,
21
21
  } from '@dxos/react-ui';
22
- import { getSize } from '@dxos/react-ui-theme';
23
22
 
24
23
  import './emoji.css';
25
24
 
@@ -77,7 +76,7 @@ export const EmojiPickerToolbarButton = ({
77
76
  <Popover.Trigger asChild>
78
77
  <Toolbar.Button classNames={['gap-2 text-2xl plb-1', classNames]} disabled={disabled}>
79
78
  <span className='sr-only'>{t('select emoji label')}</span>
80
- <UserCircle className={getSize(5)} />
79
+ <Icon icon='ph--user-circle--regular' size={5} />
81
80
  </Toolbar.Button>
82
81
  </Popover.Trigger>
83
82
  </Tooltip.Trigger>
@@ -122,6 +121,7 @@ export const EmojiPickerToolbarButton = ({
122
121
 
123
122
  /**
124
123
  * A button for picking an emoji alongside a button for unsetting it.
124
+ * @deprecated
125
125
  */
126
126
  export const EmojiPickerBlock = ({ disabled, defaultEmoji, emoji, onChangeEmoji, onClickClear }: EmojiPickerProps) => {
127
127
  const { t } = useTranslation('os');
@@ -142,7 +142,7 @@ export const EmojiPickerBlock = ({ disabled, defaultEmoji, emoji, onChangeEmoji,
142
142
  <Button variant='ghost' classNames='gap-2 text-2xl plb-1' disabled={disabled}>
143
143
  <span className='sr-only'>{t('select emoji label')}</span>
144
144
  <span className='grow pis-14'>{emojiValue}</span>
145
- <CaretDown className={getSize(4)} />
145
+ <Icon icon='ph--caret-down--regular' size={4} />
146
146
  </Button>
147
147
  </Popover.Trigger>
148
148
  <Popover.Content
@@ -174,7 +174,7 @@ export const EmojiPickerBlock = ({ disabled, defaultEmoji, emoji, onChangeEmoji,
174
174
  <Tooltip.Trigger asChild>
175
175
  <Button variant='ghost' onClick={onClickClear} disabled={disabled}>
176
176
  <span className='sr-only'>{t('clear label')}</span>
177
- <ArrowCounterClockwise />
177
+ <Icon icon='ph--arrow-counter-clockwise--regular' size={5} />
178
178
  </Button>
179
179
  </Tooltip.Trigger>
180
180
  <Tooltip.Portal>
@@ -10,46 +10,31 @@ import React, { useState } from 'react';
10
10
  import { Toolbar } from '@dxos/react-ui';
11
11
  import { withLayout, withTheme } from '@dxos/storybook-utils';
12
12
 
13
- import { HuePickerBlock, HuePickerToolbarButton, type HuePickerProps } from './HuePicker';
13
+ import { HuePicker, type HuePickerProps } from './HuePicker';
14
14
 
15
15
  const ToolbarStory = (props: HuePickerProps) => {
16
- const [hue, setHue] = useState<string>(props.defaultHue ?? 'red');
16
+ const [hue, setHue] = useState<string | undefined>(props.defaultValue);
17
17
 
18
18
  return (
19
19
  <Toolbar.Root>
20
- <HuePickerToolbarButton {...props} hue={hue} onChangeHue={setHue} />
20
+ <HuePicker {...props} value={hue} onChange={setHue} onReset={() => setHue(undefined)} />
21
21
  </Toolbar.Root>
22
22
  );
23
23
  };
24
24
 
25
- const BlockStory = (props: HuePickerProps) => {
26
- const [hue, setHue] = useState<string>(props.defaultHue ?? 'red');
27
-
28
- return (
29
- <div className='flex gap-2'>
30
- <HuePickerBlock
31
- {...props}
32
- hue={hue}
33
- onChangeHue={setHue}
34
- onClickClear={() => setHue(props.defaultHue ?? 'red')}
35
- />
36
- </div>
37
- );
38
- };
39
-
40
- export const ToolbarButtonStory: StoryObj<HuePickerProps> = {
41
- render: ToolbarStory,
42
- args: { defaultHue: 'red' },
43
- };
44
-
45
- export const BlockPickerStory: StoryObj<HuePickerProps> = {
46
- render: BlockStory,
47
- args: { defaultHue: 'red' },
48
- };
49
-
50
25
  const meta: Meta = {
51
26
  title: 'ui/react-ui-pickers/HuePicker',
52
- decorators: [withTheme, withLayout({ fullscreen: false, tooltips: true })],
27
+ decorators: [withTheme, withLayout({ tooltips: true })],
28
+ parameters: {
29
+ layout: 'centered',
30
+ },
53
31
  };
54
32
 
55
33
  export default meta;
34
+
35
+ export const Default: StoryObj<HuePickerProps> = {
36
+ render: ToolbarStory,
37
+ args: {
38
+ defaultValue: 'red',
39
+ },
40
+ };
@@ -2,183 +2,42 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { ArrowCounterClockwise, CaretDown, Check, Palette } from '@phosphor-icons/react';
6
- import { useControllableState } from '@radix-ui/react-use-controllable-state';
7
- import React, { useRef, useState } from 'react';
5
+ import React from 'react';
8
6
 
9
- import {
10
- Button,
11
- type ButtonProps,
12
- DropdownMenu,
13
- type ThemedClassName,
14
- Toolbar,
15
- Tooltip,
16
- useThemeContext,
17
- useTranslation,
18
- } from '@dxos/react-ui';
19
- import { getSize, hueTokenThemes, mx } from '@dxos/react-ui-theme';
7
+ import { type ButtonProps, type ThemedClassName, useTranslation } from '@dxos/react-ui';
8
+ import { hues } from '@dxos/react-ui-theme';
20
9
 
21
- const HuePreview = ({ hue }: { hue: string }) => {
22
- const { tx } = useThemeContext();
23
- const size = 20;
24
- return (
25
- <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
26
- <rect x={0} y={0} width={size} height={size} className={tx('hue.fill', 'select--hue__preview', { hue })} />
27
- </svg>
28
- );
29
- };
30
-
31
- const hueTokens = Object.keys(hueTokenThemes).slice(0, 16);
10
+ import { ToolbarPickerButton, type ToolbarPickerProps } from './ToolbarPicker';
32
11
 
33
12
  export type HuePickerProps = {
34
13
  disabled?: boolean;
35
- defaultHue?: string;
36
- hue?: string;
37
- onChangeHue?: (nextHue: string) => void;
38
- onClickClear?: ButtonProps['onClick'];
39
- };
14
+ defaultValue?: string;
15
+ value?: string;
16
+ onChange?: (nextHue: string) => void;
17
+ onReset?: ButtonProps['onClick'];
18
+ } & Pick<ToolbarPickerProps, 'disabled' | 'defaultValue' | 'value' | 'onChange' | 'onReset'>;
40
19
 
41
- /**
42
- * A toolbar button for picking hue. Use only in `role=toolbar` elements. Unable to unset the value.
43
- */
44
- export const HuePickerToolbarButton = ({
45
- disabled,
46
- hue,
47
- onChangeHue,
48
- classNames,
49
- defaultHue,
50
- }: ThemedClassName<Omit<HuePickerProps, 'onClickClear'>>) => {
20
+ export const HuePicker = (props: ThemedClassName<HuePickerProps>) => {
51
21
  const { t } = useTranslation('os');
52
22
 
53
- const [hueValue, setHueValue] = useControllableState<string>({
54
- prop: hue,
55
- onChange: onChangeHue,
56
- defaultProp: defaultHue,
57
- });
58
-
59
- const [huePickerOpen, setHuePickerOpen] = useState<boolean>(false);
60
-
61
- const suppressNextTooltip = useRef<boolean>(false);
62
- const [triggerTooltipOpen, setTriggerTooltipOpen] = useState(false);
63
-
64
23
  return (
65
- <Tooltip.Root
66
- open={triggerTooltipOpen}
67
- onOpenChange={(nextOpen) => {
68
- if (suppressNextTooltip.current) {
69
- setTriggerTooltipOpen(false);
70
- suppressNextTooltip.current = false;
71
- } else {
72
- setTriggerTooltipOpen(nextOpen);
73
- }
74
- }}
75
- >
76
- <DropdownMenu.Root
77
- modal={false}
78
- open={huePickerOpen}
79
- onOpenChange={(nextOpen) => {
80
- setHuePickerOpen(nextOpen);
81
- suppressNextTooltip.current = true;
82
- }}
83
- >
84
- <Tooltip.Trigger asChild>
85
- <DropdownMenu.Trigger asChild>
86
- <Toolbar.Button classNames={mx('gap-2 plb-1', classNames)} disabled={disabled}>
87
- <span className='sr-only'>{t('select hue label')}</span>
88
- <Palette className={getSize(5)} />
89
- </Toolbar.Button>
90
- </DropdownMenu.Trigger>
91
- </Tooltip.Trigger>
92
- <Tooltip.Portal>
93
- <Tooltip.Content side='bottom'>
94
- {t('select hue label')}
95
- <Tooltip.Arrow />
96
- </Tooltip.Content>
97
- </Tooltip.Portal>
98
- <DropdownMenu.Portal>
99
- <DropdownMenu.Content side='bottom' classNames='!w-40'>
100
- <DropdownMenu.Viewport classNames='grid grid-cols-4'>
101
- {hueTokens.map((hue) => {
102
- return (
103
- <DropdownMenu.CheckboxItem
104
- key={hue}
105
- checked={hue === hueValue}
106
- onCheckedChange={() => setHueValue(hue)}
107
- classNames={'px-0 py-2 items-center justify-center'}
108
- >
109
- <HuePreview hue={hue} />
110
- </DropdownMenu.CheckboxItem>
111
- );
112
- })}
113
- </DropdownMenu.Viewport>
114
- <DropdownMenu.Arrow />
115
- </DropdownMenu.Content>
116
- </DropdownMenu.Portal>
117
- </DropdownMenu.Root>
118
- </Tooltip.Root>
24
+ <ToolbarPickerButton
25
+ Component={HuePreview}
26
+ label={t('select hue label')}
27
+ icon='ph--palette--regular'
28
+ values={hues}
29
+ {...props}
30
+ />
119
31
  );
120
32
  };
121
33
 
122
- /**
123
- * A button for picking hue alongside a button for unsetting it.
124
- */
125
- export const HuePickerBlock = ({ disabled, hue, onChangeHue, defaultHue, onClickClear }: HuePickerProps) => {
126
- const { t } = useTranslation('os');
127
-
128
- const [hueValue, setHueValue] = useControllableState<string>({
129
- prop: hue,
130
- onChange: onChangeHue,
131
- defaultProp: defaultHue,
132
- });
133
-
34
+ const HuePreview = ({ value }: { value: string }) => {
35
+ const size = 16;
134
36
  return (
135
- <>
136
- <DropdownMenu.Root modal={false}>
137
- <DropdownMenu.Trigger asChild>
138
- <Button variant='ghost' classNames='gap-2 plb-1' disabled={disabled}>
139
- <span className='sr-only'>{t('select hue label')}</span>
140
- <div role='none' className='pis-14 grow flex items-center justify-center gap-2'>
141
- <HuePreview hue={hueValue!} />
142
- <span>{t(`${hueValue} label`)}</span>
143
- </div>
144
- <CaretDown className={getSize(4)} />
145
- </Button>
146
- </DropdownMenu.Trigger>
147
- <DropdownMenu.Content side='right'>
148
- <DropdownMenu.Viewport>
149
- {Object.keys(hueTokenThemes).map((hue) => {
150
- return (
151
- <DropdownMenu.CheckboxItem
152
- key={hue}
153
- checked={hue === hueValue}
154
- onCheckedChange={() => setHueValue(hue)}
155
- >
156
- <HuePreview hue={hue} />
157
- <span className='grow'>{t(`${hue} label`)}</span>
158
- <DropdownMenu.ItemIndicator>
159
- <Check />
160
- </DropdownMenu.ItemIndicator>
161
- </DropdownMenu.CheckboxItem>
162
- );
163
- })}
164
- </DropdownMenu.Viewport>
165
- <DropdownMenu.Arrow />
166
- </DropdownMenu.Content>
167
- </DropdownMenu.Root>
168
- <Tooltip.Root>
169
- <Tooltip.Trigger asChild>
170
- <Button variant='ghost' onClick={onClickClear} disabled={disabled}>
171
- <span className='sr-only'>{t('clear label')}</span>
172
- <ArrowCounterClockwise />
173
- </Button>
174
- </Tooltip.Trigger>
175
- <Tooltip.Portal>
176
- <Tooltip.Content side='right'>
177
- {t('clear label')}
178
- <Tooltip.Arrow />
179
- </Tooltip.Content>
180
- </Tooltip.Portal>
181
- </Tooltip.Root>
182
- </>
37
+ <div className='flex p-[2px] justify-center items-center'>
38
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
39
+ <rect x={0} y={0} width={size} height={size} fill={`var(--dx-${value}Fill)`} strokeWidth={4} />
40
+ </svg>
41
+ </div>
183
42
  );
184
43
  };
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type Meta, type StoryObj } from '@storybook/react';
8
+ import React, { useState } from 'react';
9
+
10
+ import { Toolbar } from '@dxos/react-ui';
11
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
12
+
13
+ import { IconPicker, type IconPickerProps } from './IconPicker';
14
+
15
+ const ToolbarStory = (props: IconPickerProps) => {
16
+ const [icon, setIcon] = useState<string | undefined>(props.value ?? props.defaultValue);
17
+ console.log(icon);
18
+
19
+ return (
20
+ <Toolbar.Root>
21
+ <IconPicker {...props} value={icon} onChange={setIcon} onReset={() => setIcon(undefined)} />
22
+ </Toolbar.Root>
23
+ );
24
+ };
25
+
26
+ const meta: Meta = {
27
+ title: 'ui/react-ui-pickers/IconPicker',
28
+ decorators: [withTheme, withLayout({ tooltips: true })],
29
+ parameters: {
30
+ layout: 'centered',
31
+ },
32
+ };
33
+
34
+ export default meta;
35
+
36
+ export const Default: StoryObj<IconPickerProps> = {
37
+ render: ToolbarStory,
38
+ args: {},
39
+ };
@@ -0,0 +1,92 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type ButtonProps, Icon, type ThemedClassName, useTranslation } from '@dxos/react-ui';
8
+
9
+ import { ToolbarPickerButton, type ToolbarPickerProps } from './ToolbarPicker';
10
+
11
+ export type IconPickerProps = {
12
+ disabled?: boolean;
13
+ defaultValue?: string;
14
+ value?: string;
15
+ onChange?: (nextHue: string) => void;
16
+ onReset?: ButtonProps['onClick'];
17
+ } & Pick<ToolbarPickerProps, 'disabled' | 'defaultValue' | 'value' | 'onChange' | 'onReset'>;
18
+
19
+ export const IconPicker = ({ ...props }: ThemedClassName<IconPickerProps>) => {
20
+ const { t } = useTranslation('os');
21
+
22
+ return (
23
+ <ToolbarPickerButton
24
+ Component={IconPreview}
25
+ label={t('select icon label')}
26
+ icon='ph--selection--regular'
27
+ values={iconValues}
28
+ {...props}
29
+ />
30
+ );
31
+ };
32
+
33
+ const IconPreview = ({ value }: { value: string }) => {
34
+ return <Icon icon={`ph--${value}--regular`} size={5} />;
35
+ };
36
+
37
+ /**
38
+ * https://phosphoricons.com
39
+ * NOTE: Select icons that we are unlikely to use for the UI.
40
+ */
41
+ const icons = [
42
+ 'ph--air-traffic-control--regular',
43
+ 'ph--asterisk--regular',
44
+ 'ph--atom--regular',
45
+ 'ph--basketball--regular',
46
+ 'ph--butterfly--regular',
47
+ 'ph--cactus--regular',
48
+ 'ph--cake--regular',
49
+ 'ph--calendar-dots--regular',
50
+ 'ph--campfire--regular',
51
+ 'ph--command--regular',
52
+ 'ph--confetti--regular',
53
+ 'ph--detective--regular',
54
+ 'ph--disco-ball--regular',
55
+ 'ph--dna--regular',
56
+ 'ph--factory--regular',
57
+ 'ph--flag-banner-fold--regular',
58
+ 'ph--flask--regular',
59
+ 'ph--flower-lotus--regular',
60
+ 'ph--flying-saucer--regular',
61
+ 'ph--game-controller--regular',
62
+ 'ph--gavel--regular',
63
+ 'ph--gift--regular',
64
+ 'ph--guitar--regular',
65
+ 'ph--hamburger--regular',
66
+ 'ph--handshake--regular',
67
+ 'ph--heart--regular',
68
+ 'ph--lightbulb--regular',
69
+ 'ph--lock--regular',
70
+ 'ph--martini--regular',
71
+ 'ph--medal-military--regular',
72
+ 'ph--moped-front--regular',
73
+ 'ph--office-chair--regular',
74
+ 'ph--paint-brush-household--regular',
75
+ 'ph--peace--regular',
76
+ 'ph--person-simple-hike--regular',
77
+ 'ph--piggy-bank--regular',
78
+ 'ph--potted-plant--regular',
79
+ 'ph--radioactive--regular',
80
+ 'ph--rocket-launch--regular',
81
+ 'ph--shield-star--regular',
82
+ 'ph--shopping-cart--regular',
83
+ 'ph--stethoscope--regular',
84
+ 'ph--student--regular',
85
+ 'ph--sun--regular',
86
+ 'ph--tote--regular',
87
+ 'ph--tree--regular',
88
+ 'ph--users-three--regular',
89
+ 'ph--yin-yang--regular',
90
+ ];
91
+
92
+ const iconValues = icons.map((icon) => icon.match(/ph--(.+)--regular/)?.[1] ?? icon);
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
6
+ import React, { type FC, useEffect, useRef, useState } from 'react';
7
+
8
+ import { DropdownMenu, Icon, type ThemedClassName, Toolbar, Tooltip } from '@dxos/react-ui';
9
+ import { mx } from '@dxos/react-ui-theme';
10
+
11
+ export type ToolbarPickerProps = {
12
+ Component: FC<{ value: string }>;
13
+ label: string;
14
+ icon: string;
15
+ values: string[];
16
+ disabled?: boolean;
17
+ defaultValue?: string;
18
+ value?: string;
19
+ onChange?: (value: string) => void;
20
+ onReset?: () => void;
21
+ };
22
+
23
+ export const ToolbarPickerButton = ({
24
+ Component,
25
+ disabled,
26
+ classNames,
27
+ defaultValue: _defaultValue,
28
+ value: _value,
29
+ values,
30
+ label,
31
+ icon,
32
+ onChange,
33
+ onReset,
34
+ }: ThemedClassName<ToolbarPickerProps>) => {
35
+ const [value, setValue] = useControllableState<string>({
36
+ prop: _value,
37
+ defaultProp: _defaultValue,
38
+ onChange,
39
+ });
40
+ // TODO(burdon): useControllableState doesn't update the prop when the value is changed. Replace it.
41
+ useEffect(() => setValue(_value), [_value]);
42
+
43
+ const [open, setOpen] = useState<boolean>(false);
44
+
45
+ const suppressNextTooltip = useRef<boolean>(false);
46
+ const [triggerTooltipOpen, setTriggerTooltipOpen] = useState(false);
47
+
48
+ return (
49
+ <Tooltip.Root
50
+ open={triggerTooltipOpen}
51
+ onOpenChange={(nextOpen) => {
52
+ if (suppressNextTooltip.current) {
53
+ setTriggerTooltipOpen(false);
54
+ suppressNextTooltip.current = false;
55
+ } else {
56
+ setTriggerTooltipOpen(nextOpen);
57
+ }
58
+ }}
59
+ >
60
+ <DropdownMenu.Root
61
+ modal={false}
62
+ open={open}
63
+ onOpenChange={(nextOpen) => {
64
+ setOpen(nextOpen);
65
+ suppressNextTooltip.current = true;
66
+ }}
67
+ >
68
+ <Tooltip.Trigger asChild>
69
+ <DropdownMenu.Trigger asChild>
70
+ <Toolbar.Button classNames={mx('gap-2 plb-1', classNames)} disabled={disabled}>
71
+ <span className='sr-only'>{label}</span>
72
+ {(value && <Component value={value} />) || <Icon icon={icon} size={5} />}
73
+ </Toolbar.Button>
74
+ </DropdownMenu.Trigger>
75
+ </Tooltip.Trigger>
76
+ <Tooltip.Portal>
77
+ <Tooltip.Content side='bottom'>
78
+ {label}
79
+ <Tooltip.Arrow />
80
+ </Tooltip.Content>
81
+ </Tooltip.Portal>
82
+ <DropdownMenu.Portal>
83
+ <DropdownMenu.Content side='bottom' classNames='!w-40'>
84
+ <DropdownMenu.Viewport classNames='grid grid-cols-6'>
85
+ {values.map((_value) => {
86
+ return (
87
+ <DropdownMenu.CheckboxItem
88
+ key={_value}
89
+ checked={_value === value}
90
+ onCheckedChange={() => setValue(_value)}
91
+ classNames={'!p-0 items-center justify-center'}
92
+ >
93
+ <Component value={_value} />
94
+ </DropdownMenu.CheckboxItem>
95
+ );
96
+ })}
97
+ {onReset && (
98
+ <DropdownMenu.CheckboxItem
99
+ onCheckedChange={() => onReset()}
100
+ classNames={'!p-0 items-center justify-center'}
101
+ >
102
+ <Icon icon='ph--x--regular' size={5} />
103
+ </DropdownMenu.CheckboxItem>
104
+ )}
105
+ </DropdownMenu.Viewport>
106
+ <DropdownMenu.Arrow />
107
+ </DropdownMenu.Content>
108
+ </DropdownMenu.Portal>
109
+ </DropdownMenu.Root>
110
+ </Tooltip.Root>
111
+ );
112
+ };
@@ -4,3 +4,5 @@
4
4
 
5
5
  export * from './EmojiPicker';
6
6
  export * from './HuePicker';
7
+ export * from './IconPicker';
8
+ export * from './ToolbarPicker';