@dxos/react-ui-pickers 0.7.5-labs.071a3e2

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 (38) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/lib/browser/index.css +7 -0
  4. package/dist/lib/browser/index.css.map +7 -0
  5. package/dist/lib/browser/index.mjs +268 -0
  6. package/dist/lib/browser/index.mjs.map +7 -0
  7. package/dist/lib/browser/meta.json +1 -0
  8. package/dist/lib/node/index.cjs +302 -0
  9. package/dist/lib/node/index.cjs.map +7 -0
  10. package/dist/lib/node/index.css +7 -0
  11. package/dist/lib/node/index.css.map +7 -0
  12. package/dist/lib/node/meta.json +1 -0
  13. package/dist/lib/node-esm/index.css +7 -0
  14. package/dist/lib/node-esm/index.css.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +270 -0
  16. package/dist/lib/node-esm/index.mjs.map +7 -0
  17. package/dist/lib/node-esm/meta.json +1 -0
  18. package/dist/types/src/components/EmojiPicker.d.ts +19 -0
  19. package/dist/types/src/components/EmojiPicker.d.ts.map +1 -0
  20. package/dist/types/src/components/EmojiPicker.stories.d.ts +8 -0
  21. package/dist/types/src/components/EmojiPicker.stories.d.ts.map +1 -0
  22. package/dist/types/src/components/HuePicker.d.ts +18 -0
  23. package/dist/types/src/components/HuePicker.d.ts.map +1 -0
  24. package/dist/types/src/components/HuePicker.stories.d.ts +8 -0
  25. package/dist/types/src/components/HuePicker.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/index.d.ts +3 -0
  27. package/dist/types/src/components/index.d.ts.map +1 -0
  28. package/dist/types/src/index.d.ts +2 -0
  29. package/dist/types/src/index.d.ts.map +1 -0
  30. package/dist/types/tsconfig.tsbuildinfo +1 -0
  31. package/package.json +52 -0
  32. package/src/components/EmojiPicker.stories.tsx +59 -0
  33. package/src/components/EmojiPicker.tsx +189 -0
  34. package/src/components/HuePicker.stories.tsx +55 -0
  35. package/src/components/HuePicker.tsx +184 -0
  36. package/src/components/emoji.css +25 -0
  37. package/src/components/index.ts +6 -0
  38. package/src/index.ts +5 -0
@@ -0,0 +1,189 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import emojiData from '@emoji-mart/data';
6
+ import EmojiMart from '@emoji-mart/react';
7
+ import { ArrowCounterClockwise, CaretDown, UserCircle } from '@phosphor-icons/react';
8
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
9
+ import React, { useRef, useState } from 'react';
10
+
11
+ import {
12
+ Button,
13
+ type ButtonProps,
14
+ Popover,
15
+ type ThemedClassName,
16
+ Toolbar,
17
+ Tooltip,
18
+ useMediaQuery,
19
+ useThemeContext,
20
+ useTranslation,
21
+ } from '@dxos/react-ui';
22
+ import { getSize } from '@dxos/react-ui-theme';
23
+
24
+ import './emoji.css';
25
+
26
+ export type EmojiPickerProps = {
27
+ disabled?: boolean;
28
+ defaultEmoji?: string;
29
+ emoji?: string;
30
+ onChangeEmoji?: (nextEmoji: string) => void;
31
+ onClickClear?: ButtonProps['onClick'];
32
+ };
33
+
34
+ /**
35
+ * A toolbar button for picking an emoji. Use only in `role=toolbar` elements. Unable to unset the value.
36
+ */
37
+ export const EmojiPickerToolbarButton = ({
38
+ disabled,
39
+ defaultEmoji,
40
+ emoji,
41
+ onChangeEmoji,
42
+ classNames,
43
+ }: ThemedClassName<Omit<EmojiPickerProps, 'onClickClear'>>) => {
44
+ const { t } = useTranslation('os');
45
+ const { themeMode } = useThemeContext();
46
+
47
+ const [_emojiValue, setEmojiValue] = useControllableState<string>({
48
+ prop: emoji,
49
+ onChange: onChangeEmoji,
50
+ defaultProp: defaultEmoji,
51
+ });
52
+
53
+ const [emojiPickerOpen, setEmojiPickerOpen] = useState<boolean>(false);
54
+ const suppressNextTooltip = useRef<boolean>(false);
55
+ const [triggerTooltipOpen, setTriggerTooltipOpen] = useState(false);
56
+
57
+ return (
58
+ <Tooltip.Root
59
+ open={triggerTooltipOpen}
60
+ onOpenChange={(nextOpen) => {
61
+ if (suppressNextTooltip.current) {
62
+ setTriggerTooltipOpen(false);
63
+ suppressNextTooltip.current = false;
64
+ } else {
65
+ setTriggerTooltipOpen(nextOpen);
66
+ }
67
+ }}
68
+ >
69
+ <Popover.Root
70
+ open={emojiPickerOpen}
71
+ onOpenChange={(nextOpen) => {
72
+ setEmojiPickerOpen(nextOpen);
73
+ suppressNextTooltip.current = true;
74
+ }}
75
+ >
76
+ <Tooltip.Trigger asChild>
77
+ <Popover.Trigger asChild>
78
+ <Toolbar.Button classNames={['gap-2 text-2xl plb-1', classNames]} disabled={disabled}>
79
+ <span className='sr-only'>{t('select emoji label')}</span>
80
+ <UserCircle className={getSize(5)} />
81
+ </Toolbar.Button>
82
+ </Popover.Trigger>
83
+ </Tooltip.Trigger>
84
+ <Tooltip.Portal>
85
+ <Tooltip.Content side='bottom'>
86
+ {t('select emoji label')}
87
+ <Tooltip.Arrow />
88
+ </Tooltip.Content>
89
+ </Tooltip.Portal>
90
+ <Popover.Portal>
91
+ <Popover.Content
92
+ side='bottom'
93
+ onKeyDownCapture={(event) => {
94
+ if (event.key === 'Escape') {
95
+ event.stopPropagation();
96
+ setEmojiPickerOpen(false);
97
+ suppressNextTooltip.current = true;
98
+ }
99
+ }}
100
+ >
101
+ {/* https://github.com/missive/emoji-mart?tab=readme-ov-file#options--props */}
102
+ <EmojiMart
103
+ data={emojiData}
104
+ onEmojiSelect={({ native }: { native?: string }) => {
105
+ if (native) {
106
+ setEmojiValue(native);
107
+ setEmojiPickerOpen(false);
108
+ }
109
+ }}
110
+ autoFocus={true}
111
+ maxFrequentRows={0}
112
+ noCountryFlags={true}
113
+ theme={themeMode}
114
+ />
115
+ <Popover.Arrow />
116
+ </Popover.Content>
117
+ </Popover.Portal>
118
+ </Popover.Root>
119
+ </Tooltip.Root>
120
+ );
121
+ };
122
+
123
+ /**
124
+ * A button for picking an emoji alongside a button for unsetting it.
125
+ */
126
+ export const EmojiPickerBlock = ({ disabled, defaultEmoji, emoji, onChangeEmoji, onClickClear }: EmojiPickerProps) => {
127
+ const { t } = useTranslation('os');
128
+ const [isMd] = useMediaQuery('md', { ssr: false });
129
+
130
+ const [emojiValue, setEmojiValue] = useControllableState<string>({
131
+ prop: emoji,
132
+ onChange: onChangeEmoji,
133
+ defaultProp: defaultEmoji,
134
+ });
135
+
136
+ const [emojiPickerOpen, setEmojiPickerOpen] = useState<boolean>(false);
137
+
138
+ return (
139
+ <>
140
+ <Popover.Root open={emojiPickerOpen} onOpenChange={setEmojiPickerOpen}>
141
+ <Popover.Trigger asChild>
142
+ <Button variant='ghost' classNames='gap-2 text-2xl plb-1' disabled={disabled}>
143
+ <span className='sr-only'>{t('select emoji label')}</span>
144
+ <span className='grow pis-14'>{emojiValue}</span>
145
+ <CaretDown className={getSize(4)} />
146
+ </Button>
147
+ </Popover.Trigger>
148
+ <Popover.Content
149
+ side='right'
150
+ sideOffset={isMd ? 0 : -310}
151
+ onKeyDownCapture={(event) => {
152
+ if (event.key === 'Escape') {
153
+ event.stopPropagation();
154
+ setEmojiPickerOpen(false);
155
+ }
156
+ }}
157
+ >
158
+ <EmojiMart
159
+ data={emojiData}
160
+ onEmojiSelect={({ native }: { native?: string }) => {
161
+ if (native) {
162
+ setEmojiValue(native);
163
+ setEmojiPickerOpen(false);
164
+ }
165
+ }}
166
+ autoFocus={true}
167
+ maxFrequentRows={0}
168
+ noCountryFlags={true}
169
+ />
170
+ <Popover.Arrow />
171
+ </Popover.Content>
172
+ </Popover.Root>
173
+ <Tooltip.Root>
174
+ <Tooltip.Trigger asChild>
175
+ <Button variant='ghost' onClick={onClickClear} disabled={disabled}>
176
+ <span className='sr-only'>{t('clear label')}</span>
177
+ <ArrowCounterClockwise />
178
+ </Button>
179
+ </Tooltip.Trigger>
180
+ <Tooltip.Portal>
181
+ <Tooltip.Content side='right'>
182
+ {t('clear label')}
183
+ <Tooltip.Arrow />
184
+ </Tooltip.Content>
185
+ </Tooltip.Portal>
186
+ </Tooltip.Root>
187
+ </>
188
+ );
189
+ };
@@ -0,0 +1,55 @@
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 { HuePickerBlock, HuePickerToolbarButton, type HuePickerProps } from './HuePicker';
14
+
15
+ const ToolbarStory = (props: HuePickerProps) => {
16
+ const [hue, setHue] = useState<string>(props.defaultHue ?? 'red');
17
+
18
+ return (
19
+ <Toolbar.Root>
20
+ <HuePickerToolbarButton {...props} hue={hue} onChangeHue={setHue} />
21
+ </Toolbar.Root>
22
+ );
23
+ };
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
+ const meta: Meta = {
51
+ title: 'ui/react-ui-pickers/HuePicker',
52
+ decorators: [withTheme, withLayout({ fullscreen: false, tooltips: true })],
53
+ };
54
+
55
+ export default meta;
@@ -0,0 +1,184 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
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';
8
+
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';
20
+
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);
32
+
33
+ export type HuePickerProps = {
34
+ disabled?: boolean;
35
+ defaultHue?: string;
36
+ hue?: string;
37
+ onChangeHue?: (nextHue: string) => void;
38
+ onClickClear?: ButtonProps['onClick'];
39
+ };
40
+
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'>>) => {
51
+ const { t } = useTranslation('os');
52
+
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
+ 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>
119
+ );
120
+ };
121
+
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
+
134
+ 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
+ </>
183
+ );
184
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * https://github.com/missive/emoji-mart/blob/main/packages/emoji-mart-website/example-custom-styles.html
3
+ */
4
+ [data-theme="dark"] em-emoji-picker {
5
+ /* TODO(burdon): Get from TW defs: modal-surface? */
6
+ --background-rgb: #3B3B3F;
7
+ --rgb-background: #3B3B3F;
8
+
9
+ --font-family: "Inter Variable", ui-sans-serif;
10
+
11
+ /*--font-size: 16px;*/
12
+ /*--color-border-over: rgba(0, 0, 0, 0.1);*/
13
+ /*--color-border: rgba(0, 0, 0, 0.05);*/
14
+ /*--category-icon-size: 24px;*/
15
+ /*--border-radius: 24px;*/
16
+ /*--font-family: 'Comic Sans MS', 'Chalkboard SE', cursive;*/
17
+ /*--rgb-accent: 255, 105, 180;*/
18
+ /*--rgb-color: 102, 51, 153;*/
19
+ /*--rgb-input: 255, 235, 235;*/
20
+ /*--shadow: 5px 5px 15px -8px rebeccapurple;*/
21
+
22
+ /*height: 50vh;*/
23
+ /*min-height: 400px;*/
24
+ /*max-height: 800px;*/
25
+ }
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './EmojiPicker';
6
+ export * from './HuePicker';
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './components';