@a-type/ui 2.0.10 → 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.
Files changed (61) hide show
  1. package/dist/cjs/colors.stories.d.ts +20 -1
  2. package/dist/cjs/colors.stories.js +30 -5
  3. package/dist/cjs/colors.stories.js.map +1 -1
  4. package/dist/cjs/components/emojiPicker/EmojiPicker.d.ts +28 -0
  5. package/dist/cjs/components/emojiPicker/EmojiPicker.js +79 -0
  6. package/dist/cjs/components/emojiPicker/EmojiPicker.js.map +1 -0
  7. package/dist/cjs/components/emojiPicker/EmojiPicker.stories.d.ts +25 -0
  8. package/dist/cjs/components/emojiPicker/EmojiPicker.stories.js +21 -0
  9. package/dist/cjs/components/emojiPicker/EmojiPicker.stories.js.map +1 -0
  10. package/dist/cjs/components/index.d.ts +1 -0
  11. package/dist/cjs/components/index.js +1 -0
  12. package/dist/cjs/components/index.js.map +1 -1
  13. package/dist/cjs/components/relativeTime/RelativeTime.d.ts +10 -1
  14. package/dist/cjs/components/relativeTime/RelativeTime.js +17 -9
  15. package/dist/cjs/components/relativeTime/RelativeTime.js.map +1 -1
  16. package/dist/cjs/components/relativeTime/RelativeTime.stories.d.ts +19 -0
  17. package/dist/cjs/components/relativeTime/RelativeTime.stories.js +20 -0
  18. package/dist/cjs/components/relativeTime/RelativeTime.stories.js.map +1 -0
  19. package/dist/cjs/hooks/useStorage.d.ts +2 -0
  20. package/dist/cjs/hooks/useStorage.js +80 -0
  21. package/dist/cjs/hooks/useStorage.js.map +1 -0
  22. package/dist/cjs/hooks/withProps.d.ts +5 -0
  23. package/dist/cjs/hooks/withProps.js +7 -1
  24. package/dist/cjs/hooks/withProps.js.map +1 -1
  25. package/dist/cjs/uno/colors.js +1 -1
  26. package/dist/css/main.css +34 -34
  27. package/dist/esm/colors.stories.d.ts +20 -1
  28. package/dist/esm/colors.stories.js +30 -5
  29. package/dist/esm/colors.stories.js.map +1 -1
  30. package/dist/esm/components/emojiPicker/EmojiPicker.d.ts +28 -0
  31. package/dist/esm/components/emojiPicker/EmojiPicker.js +68 -0
  32. package/dist/esm/components/emojiPicker/EmojiPicker.js.map +1 -0
  33. package/dist/esm/components/emojiPicker/EmojiPicker.stories.d.ts +25 -0
  34. package/dist/esm/components/emojiPicker/EmojiPicker.stories.js +18 -0
  35. package/dist/esm/components/emojiPicker/EmojiPicker.stories.js.map +1 -0
  36. package/dist/esm/components/index.d.ts +1 -0
  37. package/dist/esm/components/index.js +1 -0
  38. package/dist/esm/components/index.js.map +1 -1
  39. package/dist/esm/components/relativeTime/RelativeTime.d.ts +10 -1
  40. package/dist/esm/components/relativeTime/RelativeTime.js +17 -9
  41. package/dist/esm/components/relativeTime/RelativeTime.js.map +1 -1
  42. package/dist/esm/components/relativeTime/RelativeTime.stories.d.ts +19 -0
  43. package/dist/esm/components/relativeTime/RelativeTime.stories.js +17 -0
  44. package/dist/esm/components/relativeTime/RelativeTime.stories.js.map +1 -0
  45. package/dist/esm/hooks/useStorage.d.ts +2 -0
  46. package/dist/esm/hooks/useStorage.js +77 -0
  47. package/dist/esm/hooks/useStorage.js.map +1 -0
  48. package/dist/esm/hooks/withProps.d.ts +5 -0
  49. package/dist/esm/hooks/withProps.js +5 -0
  50. package/dist/esm/hooks/withProps.js.map +1 -1
  51. package/dist/esm/uno/colors.js +1 -1
  52. package/package.json +4 -2
  53. package/src/colors.stories.tsx +30 -4
  54. package/src/components/emojiPicker/EmojiPicker.stories.tsx +21 -0
  55. package/src/components/emojiPicker/EmojiPicker.tsx +170 -0
  56. package/src/components/index.ts +1 -0
  57. package/src/components/relativeTime/RelativeTime.stories.tsx +21 -0
  58. package/src/components/relativeTime/RelativeTime.tsx +32 -9
  59. package/src/hooks/useStorage.ts +107 -0
  60. package/src/hooks/withProps.tsx +12 -0
  61. package/src/uno/colors.ts +1 -1
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { EmojiPicker } from './EmojiPicker.js';
3
+
4
+ const meta = {
5
+ title: 'EmojiPicker',
6
+ component: EmojiPicker,
7
+ argTypes: {},
8
+ parameters: {
9
+ controls: { expanded: true },
10
+ },
11
+ } satisfies Meta<typeof EmojiPicker>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof EmojiPicker>;
16
+
17
+ export const Default: Story = {
18
+ render(args) {
19
+ return <EmojiPicker {...args} />;
20
+ },
21
+ };
@@ -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
+ });
@@ -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,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { RelativeTime } from './RelativeTime.js';
3
+
4
+ const meta = {
5
+ title: 'RelativeTime',
6
+ component: RelativeTime,
7
+ argTypes: {},
8
+ args: {
9
+ value: Date.now() + 60 * 1000 * 2,
10
+ abbreviate: false,
11
+ },
12
+ parameters: {
13
+ controls: { expanded: true },
14
+ },
15
+ } satisfies Meta<typeof RelativeTime>;
16
+
17
+ export default meta;
18
+
19
+ type Story = StoryObj<typeof RelativeTime>;
20
+
21
+ export const Default: Story = {};
@@ -1,45 +1,68 @@
1
1
  'use client';
2
2
 
3
3
  import { shortenTimeUnits } from '@a-type/utils';
4
+ import { differenceInMinutes } from 'date-fns/differenceInMinutes';
4
5
  import { formatDistanceToNowStrict } from 'date-fns/formatDistanceToNowStrict';
5
6
  import { useEffect, useMemo, useState } from 'react';
6
7
 
7
8
  export interface RelativeTimeProps {
8
9
  value: number;
9
10
  abbreviate?: boolean;
11
+ /**
12
+ * Makes it count down seconds for a future date value as it approaches.
13
+ * Does not affect past dates.
14
+ */
15
+ countdownSeconds?: boolean;
16
+ /**
17
+ * Remove "from now" or "ago" from the output.
18
+ */
19
+ disableRelativeText?: boolean;
10
20
  }
11
21
 
12
- function formatDistanceToNow(date: Date) {
22
+ function formatDistanceToNow(date: Date, relativeText = true) {
13
23
  const now = Date.now();
14
24
  if (Math.abs(date.getTime() - now) < 1000) {
15
25
  return 'just now';
16
26
  }
17
27
  return (
18
28
  formatDistanceToNowStrict(date) +
19
- (date.getTime() < now ? ' ago' : ' from now')
29
+ (relativeText ? (date.getTime() < now ? ' ago' : ' from now') : '')
20
30
  );
21
31
  }
22
32
 
23
- export function RelativeTime({ value, abbreviate }: RelativeTimeProps) {
33
+ export function RelativeTime({
34
+ value,
35
+ abbreviate,
36
+ countdownSeconds,
37
+ disableRelativeText,
38
+ }: RelativeTimeProps) {
24
39
  const asDate = useMemo(() => new Date(value), [value]);
25
40
  const [time, setTime] = useState(() =>
26
41
  abbreviate
27
- ? shortenTimeUnits(formatDistanceToNow(asDate))
28
- : formatDistanceToNow(asDate),
42
+ ? shortenTimeUnits(formatDistanceToNow(asDate, !disableRelativeText))
43
+ : formatDistanceToNow(asDate, !disableRelativeText),
29
44
  );
45
+ // increase update rate if the date is less than 1 minute away and in the future
46
+ // (past is ok to just leave ~1 minute)
47
+ const updateRate =
48
+ countdownSeconds &&
49
+ asDate.getTime() > Date.now() &&
50
+ Math.abs(differenceInMinutes(asDate, new Date())) < 1
51
+ ? 1000
52
+ : 60 * 1000;
30
53
 
31
54
  useEffect(() => {
32
55
  const update = () => {
33
56
  setTime(
34
57
  abbreviate
35
- ? shortenTimeUnits(formatDistanceToNow(asDate))
36
- : formatDistanceToNow(asDate),
58
+ ? shortenTimeUnits(formatDistanceToNow(asDate, !disableRelativeText))
59
+ : formatDistanceToNow(asDate, !disableRelativeText),
37
60
  );
38
61
  };
39
- const interval = setInterval(update, 60 * 1000);
62
+ const interval = setInterval(update, updateRate);
40
63
  update();
41
64
  return () => clearInterval(interval);
42
- }, [asDate, abbreviate]);
65
+ }, [asDate, abbreviate, updateRate, disableRelativeText]);
43
66
 
44
67
  return <>{time}</>;
45
68
  }
@@ -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
+ }
@@ -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.03)) calc(h - 5 * var(--dyn-${name}-hue-rotate, 0) * var(--dyn-${name}-hue-rotate-mult, 1)));
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)));