@a-type/ui 2.0.11 → 2.1.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.
- package/dist/cjs/colors.stories.d.ts +20 -1
- package/dist/cjs/colors.stories.js +30 -5
- package/dist/cjs/colors.stories.js.map +1 -1
- package/dist/cjs/components/emojiPicker/EmojiPicker.d.ts +28 -0
- package/dist/cjs/components/emojiPicker/EmojiPicker.js +79 -0
- package/dist/cjs/components/emojiPicker/EmojiPicker.js.map +1 -0
- package/dist/cjs/components/emojiPicker/EmojiPicker.stories.d.ts +25 -0
- package/dist/cjs/components/emojiPicker/EmojiPicker.stories.js +21 -0
- package/dist/cjs/components/emojiPicker/EmojiPicker.stories.js.map +1 -0
- package/dist/cjs/components/index.d.ts +1 -0
- package/dist/cjs/components/index.js +1 -0
- package/dist/cjs/components/index.js.map +1 -1
- package/dist/cjs/hooks/useStorage.d.ts +2 -0
- package/dist/cjs/hooks/useStorage.js +80 -0
- package/dist/cjs/hooks/useStorage.js.map +1 -0
- package/dist/cjs/hooks/withProps.d.ts +5 -0
- package/dist/cjs/hooks/withProps.js +7 -1
- package/dist/cjs/hooks/withProps.js.map +1 -1
- package/dist/cjs/uno/colors.js +1 -1
- package/dist/css/main.css +34 -34
- package/dist/esm/colors.stories.d.ts +20 -1
- package/dist/esm/colors.stories.js +30 -5
- package/dist/esm/colors.stories.js.map +1 -1
- package/dist/esm/components/emojiPicker/EmojiPicker.d.ts +28 -0
- package/dist/esm/components/emojiPicker/EmojiPicker.js +68 -0
- package/dist/esm/components/emojiPicker/EmojiPicker.js.map +1 -0
- package/dist/esm/components/emojiPicker/EmojiPicker.stories.d.ts +25 -0
- package/dist/esm/components/emojiPicker/EmojiPicker.stories.js +18 -0
- package/dist/esm/components/emojiPicker/EmojiPicker.stories.js.map +1 -0
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/index.js +1 -0
- package/dist/esm/components/index.js.map +1 -1
- package/dist/esm/hooks/useStorage.d.ts +2 -0
- package/dist/esm/hooks/useStorage.js +77 -0
- package/dist/esm/hooks/useStorage.js.map +1 -0
- package/dist/esm/hooks/withProps.d.ts +5 -0
- package/dist/esm/hooks/withProps.js +5 -0
- package/dist/esm/hooks/withProps.js.map +1 -1
- package/dist/esm/uno/colors.js +1 -1
- package/package.json +4 -2
- package/src/colors.stories.tsx +30 -4
- package/src/components/emojiPicker/EmojiPicker.stories.tsx +21 -0
- package/src/components/emojiPicker/EmojiPicker.tsx +170 -0
- package/src/components/index.ts +1 -0
- package/src/hooks/useStorage.ts +107 -0
- package/src/hooks/withProps.tsx +12 -0
- package/src/uno/colors.ts +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import {
|
|
3
|
+
EmojiPicker as Core,
|
|
4
|
+
EmojiPickerListCategoryHeaderProps,
|
|
5
|
+
EmojiPickerListEmojiProps,
|
|
6
|
+
EmojiPickerListProps,
|
|
7
|
+
EmojiPickerRootProps,
|
|
8
|
+
EmojiPickerViewportProps,
|
|
9
|
+
useSkinTone,
|
|
10
|
+
} from 'frimousse';
|
|
11
|
+
import { withClassName, withProps } from '../../hooks.js';
|
|
12
|
+
import { useLocalStorage } from '../../hooks/useStorage.js';
|
|
13
|
+
import { Box, BoxProps } from '../box/Box.js';
|
|
14
|
+
import { Button } from '../button/Button.js';
|
|
15
|
+
import { inputClassName } from '../input/Input.js';
|
|
16
|
+
import { Spinner } from '../spinner/Spinner.js';
|
|
17
|
+
|
|
18
|
+
export const EmojiPickerRoot = withClassName(
|
|
19
|
+
Core.Root,
|
|
20
|
+
'layer-components:(isolate flex flex-col w-fit h-368px bg-white gap-sm)',
|
|
21
|
+
);
|
|
22
|
+
export const EmojiPickerSearch = withClassName(
|
|
23
|
+
Core.Search,
|
|
24
|
+
'layer-components:(z-10)',
|
|
25
|
+
inputClassName,
|
|
26
|
+
);
|
|
27
|
+
export const EmojiPickerViewport = ({
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: EmojiPickerViewportProps) => (
|
|
31
|
+
<Box border className="flex-1 min-h-0 overflow-hidden">
|
|
32
|
+
<Core.Viewport
|
|
33
|
+
className="layer-components:(relative outline-hidden)"
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
export const EmojiPickerLoading = withClassName(
|
|
39
|
+
withProps(Core.Loading, {
|
|
40
|
+
children: <Spinner />,
|
|
41
|
+
}),
|
|
42
|
+
'layer-compoennts:(absolute inset-0 flex items-center justify-center bg-inherit)',
|
|
43
|
+
);
|
|
44
|
+
export const EmojiPickerEmpty = withClassName(
|
|
45
|
+
withProps(Core.Empty, {
|
|
46
|
+
children: <>No emoji found</>,
|
|
47
|
+
}),
|
|
48
|
+
'layer-components:(absolute inset-0 flex items-center justify-center bg-inherit color-gray-dark text-xs)',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const EmojiPickerCategoryHeader = (
|
|
52
|
+
props: EmojiPickerListCategoryHeaderProps,
|
|
53
|
+
) => (
|
|
54
|
+
<div
|
|
55
|
+
className={clsx(
|
|
56
|
+
'layer-components:(bg-inherit px-md py-sm text-xs font-semibold text-gray-dark sticky top-0)',
|
|
57
|
+
props.className,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{props.category.label}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
export const EmojiPickerRow = withClassName(
|
|
64
|
+
'div',
|
|
65
|
+
'layer-components:(scroll-my-xs px-xs)',
|
|
66
|
+
);
|
|
67
|
+
export const EmojiPickerEmoji = withClassName(
|
|
68
|
+
(p: EmojiPickerListEmojiProps) => (
|
|
69
|
+
<Button
|
|
70
|
+
{...p}
|
|
71
|
+
color="ghost"
|
|
72
|
+
toggled={p.emoji.isActive}
|
|
73
|
+
toggleMode="color"
|
|
74
|
+
size="icon-small"
|
|
75
|
+
aria-label={p.emoji.label}
|
|
76
|
+
className="text-lg p-xs"
|
|
77
|
+
>
|
|
78
|
+
{p.emoji.emoji}
|
|
79
|
+
</Button>
|
|
80
|
+
),
|
|
81
|
+
'',
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const defaultListComponents = {
|
|
85
|
+
CategoryHeader: EmojiPickerCategoryHeader,
|
|
86
|
+
Row: EmojiPickerRow,
|
|
87
|
+
Emoji: EmojiPickerEmoji,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const EmojiPickerList = ({
|
|
91
|
+
className,
|
|
92
|
+
components,
|
|
93
|
+
}: EmojiPickerListProps) => {
|
|
94
|
+
return (
|
|
95
|
+
<Core.List
|
|
96
|
+
className={clsx('layer-components:(select-none pb-md)', className)}
|
|
97
|
+
components={
|
|
98
|
+
components
|
|
99
|
+
? {
|
|
100
|
+
...defaultListComponents,
|
|
101
|
+
...components,
|
|
102
|
+
}
|
|
103
|
+
: defaultListComponents
|
|
104
|
+
}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const useEmojiSkinTone = () =>
|
|
110
|
+
useLocalStorage<SkinTone | undefined>('emoji-skin-tone', undefined, false);
|
|
111
|
+
|
|
112
|
+
export type SkinTone = ReturnType<typeof useSkinTone>[0];
|
|
113
|
+
export const EmojiPickerSkinToneSelector = (props: BoxProps) => {
|
|
114
|
+
const [_, __, options] = useSkinTone();
|
|
115
|
+
const [skinTone, setSkinTone] = useEmojiSkinTone();
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Box d="row" gap border {...props}>
|
|
119
|
+
{options.map((option) => (
|
|
120
|
+
<Button
|
|
121
|
+
key={option.skinTone}
|
|
122
|
+
color="ghost"
|
|
123
|
+
toggled={option.skinTone === skinTone}
|
|
124
|
+
toggleMode="color"
|
|
125
|
+
size="icon-small"
|
|
126
|
+
aria-label={`Skin tone ${option}`}
|
|
127
|
+
className="text-md p-xs"
|
|
128
|
+
onClick={() => setSkinTone(option.skinTone)}
|
|
129
|
+
>
|
|
130
|
+
{option.emoji}
|
|
131
|
+
</Button>
|
|
132
|
+
))}
|
|
133
|
+
</Box>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export interface EmojiPickerProps
|
|
138
|
+
extends Omit<EmojiPickerRootProps, 'emoji' | 'onEmojiSelect'> {
|
|
139
|
+
onValueChange: (value: string, label: string) => void;
|
|
140
|
+
}
|
|
141
|
+
const EmojiPickerPrefab = ({ onValueChange, ...props }: EmojiPickerProps) => {
|
|
142
|
+
const [skinTone] = useEmojiSkinTone();
|
|
143
|
+
return (
|
|
144
|
+
<EmojiPickerRoot
|
|
145
|
+
{...props}
|
|
146
|
+
onEmojiSelect={(emoji) => onValueChange(emoji.emoji, emoji.label)}
|
|
147
|
+
skinTone={skinTone}
|
|
148
|
+
>
|
|
149
|
+
<EmojiPickerSearch />
|
|
150
|
+
<EmojiPickerViewport>
|
|
151
|
+
<EmojiPickerList />
|
|
152
|
+
<EmojiPickerLoading />
|
|
153
|
+
<EmojiPickerEmpty />
|
|
154
|
+
</EmojiPickerViewport>
|
|
155
|
+
<EmojiPickerSkinToneSelector className="mr-auto" />
|
|
156
|
+
</EmojiPickerRoot>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const EmojiPicker = Object.assign(EmojiPickerPrefab, {
|
|
161
|
+
Root: EmojiPickerRoot,
|
|
162
|
+
Search: EmojiPickerSearch,
|
|
163
|
+
Viewport: EmojiPickerViewport,
|
|
164
|
+
List: EmojiPickerList,
|
|
165
|
+
Loading: EmojiPickerLoading,
|
|
166
|
+
Empty: EmojiPickerEmpty,
|
|
167
|
+
CategoryHeader: EmojiPickerCategoryHeader,
|
|
168
|
+
Row: EmojiPickerRow,
|
|
169
|
+
Emoji: EmojiPickerEmoji,
|
|
170
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export * from './dialog/index.js';
|
|
|
15
15
|
export * from './divider/index.js';
|
|
16
16
|
export * from './dropdownMenu/index.js';
|
|
17
17
|
export * from './editableText/EditableText.js';
|
|
18
|
+
export * from './emojiPicker/EmojiPicker.js';
|
|
18
19
|
export * from './errorBoundary/index.js';
|
|
19
20
|
export * from './forms/index.js';
|
|
20
21
|
export * from './horizontalList/HorizontalList.js';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react';
|
|
2
|
+
import { proxy, useSnapshot } from 'valtio';
|
|
3
|
+
|
|
4
|
+
function makeUseStorage(
|
|
5
|
+
storage: Storage,
|
|
6
|
+
cache: Record<string, any>,
|
|
7
|
+
name: string = storage.constructor.name,
|
|
8
|
+
) {
|
|
9
|
+
return function useStorage<T>(
|
|
10
|
+
key: string,
|
|
11
|
+
initialValue: T,
|
|
12
|
+
writeInitialValue = false,
|
|
13
|
+
) {
|
|
14
|
+
// using useMemo to execute synchronous code in render just once.
|
|
15
|
+
// this hook comes before useLocalStorageCache because we want to load
|
|
16
|
+
// values into the cache before accessing them.
|
|
17
|
+
useMemo(() => {
|
|
18
|
+
if (typeof window === 'undefined') return;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const stored = storage.getItem(key);
|
|
22
|
+
if (stored) {
|
|
23
|
+
cache[key] = JSON.parse(stored);
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error(`Error loading use-${name} value for ${key}: ${err}`);
|
|
27
|
+
storage.removeItem(key);
|
|
28
|
+
}
|
|
29
|
+
}, [key]);
|
|
30
|
+
const snapshot = useSnapshot(cache);
|
|
31
|
+
const storedValue = (snapshot[key] ?? initialValue) as T;
|
|
32
|
+
|
|
33
|
+
const hasValue = snapshot[key] !== undefined;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!hasValue && writeInitialValue) {
|
|
36
|
+
storage.setItem(key, JSON.stringify(initialValue));
|
|
37
|
+
}
|
|
38
|
+
}, [hasValue, initialValue, writeInitialValue, key]);
|
|
39
|
+
|
|
40
|
+
// Return a wrapped version of useState's setter function that
|
|
41
|
+
// persists the new value to localStorage. It's throttled to prevent
|
|
42
|
+
// frequent writes to localStorage, which can be costly.
|
|
43
|
+
const setValue = useMemo(
|
|
44
|
+
() =>
|
|
45
|
+
throttle(
|
|
46
|
+
(value: T | ((current: T) => T)) => {
|
|
47
|
+
if (typeof window === 'undefined') return;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Allow value to be a function so we have same API as useState
|
|
51
|
+
const valueToStore =
|
|
52
|
+
value instanceof Function ? value(storedValue) : value;
|
|
53
|
+
// Save to local storage
|
|
54
|
+
storage.setItem(key, JSON.stringify(valueToStore));
|
|
55
|
+
// sync it to other instances of the hook via the global cache
|
|
56
|
+
cache[key] = valueToStore;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(
|
|
59
|
+
`Error setting use-${name} value for ${key}: ${value}: ${error}`,
|
|
60
|
+
);
|
|
61
|
+
throw new Error('Error setting value');
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
300,
|
|
65
|
+
{ trailing: true, leading: true },
|
|
66
|
+
),
|
|
67
|
+
[key, storedValue],
|
|
68
|
+
) as (value: T | ((current: T) => T)) => void;
|
|
69
|
+
|
|
70
|
+
return [storedValue, setValue] as const;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const useLocalStorage = makeUseStorage(
|
|
75
|
+
localStorage,
|
|
76
|
+
proxy({}),
|
|
77
|
+
'LocalStorage',
|
|
78
|
+
);
|
|
79
|
+
export const useSessionStorage = makeUseStorage(
|
|
80
|
+
sessionStorage,
|
|
81
|
+
proxy({}),
|
|
82
|
+
'SessionStorage',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
function throttle(
|
|
86
|
+
func: (...args: any[]) => any,
|
|
87
|
+
wait: number,
|
|
88
|
+
options?: { trailing?: boolean; leading?: boolean },
|
|
89
|
+
): (...args: any[]) => any {
|
|
90
|
+
let previous = 0;
|
|
91
|
+
return function (this: any, ...args: any[]) {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (!previous && options?.leading === false) previous = now;
|
|
94
|
+
const remaining = wait - (now - previous);
|
|
95
|
+
if (remaining <= 0) {
|
|
96
|
+
if (options?.trailing === false) previous = now;
|
|
97
|
+
return func(...args);
|
|
98
|
+
}
|
|
99
|
+
if (options?.trailing === false) {
|
|
100
|
+
return func(...args);
|
|
101
|
+
}
|
|
102
|
+
return setTimeout(() => {
|
|
103
|
+
previous = options?.leading === false ? 0 : Date.now();
|
|
104
|
+
func(...args);
|
|
105
|
+
}, remaining);
|
|
106
|
+
};
|
|
107
|
+
}
|
package/src/hooks/withProps.tsx
CHANGED
|
@@ -8,3 +8,15 @@ export const withProps = <T extends {}>(
|
|
|
8
8
|
return <Component {...props} {...extras} />;
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
|
+
|
|
12
|
+
type OptionalKeys<T> = {
|
|
13
|
+
[K in keyof T]-?: undefined extends T[K] ? K : never;
|
|
14
|
+
}[keyof T];
|
|
15
|
+
export const withoutProps = <T extends {}, P extends OptionalKeys<T>>(
|
|
16
|
+
Component: React.ComponentType<T>,
|
|
17
|
+
remove: P[],
|
|
18
|
+
) => {
|
|
19
|
+
return (props: Omit<T, P>) => {
|
|
20
|
+
return <Component {...(props as any)} />;
|
|
21
|
+
};
|
|
22
|
+
};
|
package/src/uno/colors.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const colorConstants = `
|
|
|
7
7
|
|
|
8
8
|
export const dynamicThemeComputedColors = (name: string) => `
|
|
9
9
|
--color-${name}: oklch(calc(90% - 35% * var(--dyn-source-mode-adjust, 0) - (var(--dyn-mode-sign, 1) * var(--dyn-${name}-base-dim, 0%))) calc(var(--dyn-${name}-sat-mult,1) * (35% - 2% * var(--dyn-source-mode-adjust, 0))) var(--dyn-${name}-source, 0));
|
|
10
|
-
--color-${name}-wash: oklch(from var(--color-${name}) calc(min(0.999,max(0.15, l + 0.15 * var(--dyn-mode-mult, 1)))) calc(var(--dyn-${name}-sat-mult) * (c * var(--dyn-saturation-x-wash, 1) - 0.
|
|
10
|
+
--color-${name}-wash: oklch(from var(--color-${name}) calc(min(0.999,max(0.15, l + 0.15 * var(--dyn-mode-mult, 1)))) calc(var(--dyn-${name}-sat-mult) * (c * var(--dyn-saturation-x-wash, 1) - 0.06)) calc(h - 5 * var(--dyn-${name}-hue-rotate, 0) * var(--dyn-${name}-hue-rotate-mult, 1)));
|
|
11
11
|
--color-${name}-light: oklch(from var(--color-${name}) calc(l + 0.08 * var(--dyn-mode-mult, 1)) calc(var(--dyn-${name}-sat-mult) * (c * var(--dyn-saturation-x-light, 1) - 0.03)) calc(h - 0.5 * var(--dyn-${name}-hue-rotate, 0) * var(--dyn-${name}-hue-rotate-mult, 1)));
|
|
12
12
|
--color-${name}-dark: oklch(from var(--color-${name}) calc(l - 0.26 * var(--dyn-mode-mult, 1)) calc(var(--dyn-${name}-sat-mult) * (c * var(--dyn-saturation-x-dark, 1) + 0.01)) calc(h + 0.2 * var(--dyn-${name}-hue-rotate, 0) * var(--dyn-${name}-hue-rotate-mult, 1)));
|
|
13
13
|
--color-${name}-ink: oklch(from var(--color-${name}) calc(l - 0.45 * var(--dyn-mode-mult, 1)) calc(var(--dyn-${name}-sat-mult) * (c * var(--dyn-saturation-x-ink, 1) + 0.01)) calc(h + 1 * var(--dyn-${name}-hue-rotate, 0) * var(--dyn-${name}-hue-rotate-mult, 1)));
|