@a-type/ui 2.6.2 → 2.7.1

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 (108) hide show
  1. package/dist/cjs/components/button/Button.d.ts +14 -1
  2. package/dist/cjs/components/button/Button.js +84 -8
  3. package/dist/cjs/components/button/Button.js.map +1 -1
  4. package/dist/cjs/components/button/Button.stories.d.ts +14 -1
  5. package/dist/cjs/components/button/Button.stories.js +27 -6
  6. package/dist/cjs/components/button/Button.stories.js.map +1 -1
  7. package/dist/cjs/components/button/classes.d.ts +2 -2
  8. package/dist/cjs/components/button/classes.js +12 -7
  9. package/dist/cjs/components/button/classes.js.map +1 -1
  10. package/dist/cjs/components/camera/Camera.js +3 -3
  11. package/dist/cjs/components/camera/Camera.js.map +1 -1
  12. package/dist/cjs/components/card/Card.d.ts +2 -2
  13. package/dist/cjs/components/card/Card.stories.js +7 -7
  14. package/dist/cjs/components/card/Card.stories.js.map +1 -1
  15. package/dist/cjs/components/datePicker/DatePicker.js +4 -4
  16. package/dist/cjs/components/datePicker/DatePicker.js.map +1 -1
  17. package/dist/cjs/components/emojiPicker/EmojiPicker.js +2 -2
  18. package/dist/cjs/components/emojiPicker/EmojiPicker.js.map +1 -1
  19. package/dist/cjs/components/horizontalList/HorizontalList.js +1 -1
  20. package/dist/cjs/components/horizontalList/HorizontalList.js.map +1 -1
  21. package/dist/cjs/components/horizontalList/HorizontalList.stories.js +2 -2
  22. package/dist/cjs/components/horizontalList/HorizontalList.stories.js.map +1 -1
  23. package/dist/cjs/components/icon/Icon.js +7 -1
  24. package/dist/cjs/components/icon/Icon.js.map +1 -1
  25. package/dist/cjs/components/icon/IconLoadingContext.d.ts +2 -0
  26. package/dist/cjs/components/icon/IconLoadingContext.js +12 -0
  27. package/dist/cjs/components/icon/IconLoadingContext.js.map +1 -0
  28. package/dist/cjs/components/imageUploader/ImageUploader.js +1 -1
  29. package/dist/cjs/components/imageUploader/ImageUploader.js.map +1 -1
  30. package/dist/cjs/components/particles/ParticleLayer.stories.js +1 -1
  31. package/dist/cjs/components/particles/ParticleLayer.stories.js.map +1 -1
  32. package/dist/cjs/components/select/Select.d.ts +1 -1
  33. package/dist/cjs/components/select/Select.js +6 -5
  34. package/dist/cjs/components/select/Select.js.map +1 -1
  35. package/dist/cjs/components/spinner/Spinner.js +1 -1
  36. package/dist/cjs/components/spinner/Spinner.js.map +1 -1
  37. package/dist/cjs/themes.stories.d.ts +1 -0
  38. package/dist/cjs/themes.stories.js +2 -1
  39. package/dist/cjs/themes.stories.js.map +1 -1
  40. package/dist/cjs/uno/colors.d.ts +1 -0
  41. package/dist/cjs/uno/colors.js +1 -0
  42. package/dist/cjs/uno/colors.js.map +1 -1
  43. package/dist/cjs/uno/shadows.js +11 -8
  44. package/dist/cjs/uno/shadows.js.map +1 -1
  45. package/dist/css/main.css +12 -12
  46. package/dist/esm/components/button/Button.d.ts +14 -1
  47. package/dist/esm/components/button/Button.js +82 -7
  48. package/dist/esm/components/button/Button.js.map +1 -1
  49. package/dist/esm/components/button/Button.stories.d.ts +14 -1
  50. package/dist/esm/components/button/Button.stories.js +27 -6
  51. package/dist/esm/components/button/Button.stories.js.map +1 -1
  52. package/dist/esm/components/button/classes.d.ts +2 -2
  53. package/dist/esm/components/button/classes.js +13 -8
  54. package/dist/esm/components/button/classes.js.map +1 -1
  55. package/dist/esm/components/camera/Camera.js +3 -3
  56. package/dist/esm/components/camera/Camera.js.map +1 -1
  57. package/dist/esm/components/card/Card.d.ts +2 -2
  58. package/dist/esm/components/card/Card.stories.js +7 -7
  59. package/dist/esm/components/card/Card.stories.js.map +1 -1
  60. package/dist/esm/components/datePicker/DatePicker.js +4 -4
  61. package/dist/esm/components/datePicker/DatePicker.js.map +1 -1
  62. package/dist/esm/components/emojiPicker/EmojiPicker.js +2 -2
  63. package/dist/esm/components/emojiPicker/EmojiPicker.js.map +1 -1
  64. package/dist/esm/components/horizontalList/HorizontalList.js +1 -1
  65. package/dist/esm/components/horizontalList/HorizontalList.js.map +1 -1
  66. package/dist/esm/components/horizontalList/HorizontalList.stories.js +2 -2
  67. package/dist/esm/components/horizontalList/HorizontalList.stories.js.map +1 -1
  68. package/dist/esm/components/icon/Icon.js +7 -1
  69. package/dist/esm/components/icon/Icon.js.map +1 -1
  70. package/dist/esm/components/icon/IconLoadingContext.d.ts +2 -0
  71. package/dist/esm/components/icon/IconLoadingContext.js +8 -0
  72. package/dist/esm/components/icon/IconLoadingContext.js.map +1 -0
  73. package/dist/esm/components/imageUploader/ImageUploader.js +1 -1
  74. package/dist/esm/components/imageUploader/ImageUploader.js.map +1 -1
  75. package/dist/esm/components/particles/ParticleLayer.stories.js +1 -1
  76. package/dist/esm/components/particles/ParticleLayer.stories.js.map +1 -1
  77. package/dist/esm/components/select/Select.d.ts +1 -1
  78. package/dist/esm/components/select/Select.js +2 -1
  79. package/dist/esm/components/select/Select.js.map +1 -1
  80. package/dist/esm/components/spinner/Spinner.js +1 -1
  81. package/dist/esm/components/spinner/Spinner.js.map +1 -1
  82. package/dist/esm/themes.stories.d.ts +1 -0
  83. package/dist/esm/themes.stories.js +2 -1
  84. package/dist/esm/themes.stories.js.map +1 -1
  85. package/dist/esm/uno/colors.d.ts +1 -0
  86. package/dist/esm/uno/colors.js +1 -0
  87. package/dist/esm/uno/colors.js.map +1 -1
  88. package/dist/esm/uno/shadows.js +11 -8
  89. package/dist/esm/uno/shadows.js.map +1 -1
  90. package/package.json +2 -1
  91. package/src/components/button/Button.stories.tsx +47 -7
  92. package/src/components/button/Button.tsx +137 -14
  93. package/src/components/button/classes.tsx +19 -11
  94. package/src/components/camera/Camera.tsx +1 -7
  95. package/src/components/card/Card.stories.tsx +10 -10
  96. package/src/components/datePicker/DatePicker.tsx +0 -4
  97. package/src/components/emojiPicker/EmojiPicker.tsx +4 -4
  98. package/src/components/horizontalList/HorizontalList.stories.tsx +0 -2
  99. package/src/components/horizontalList/HorizontalList.tsx +0 -1
  100. package/src/components/icon/Icon.tsx +9 -0
  101. package/src/components/icon/IconLoadingContext.tsx +7 -0
  102. package/src/components/imageUploader/ImageUploader.tsx +0 -1
  103. package/src/components/particles/ParticleLayer.stories.tsx +2 -2
  104. package/src/components/select/Select.tsx +2 -1
  105. package/src/components/spinner/Spinner.tsx +1 -1
  106. package/src/themes.stories.tsx +3 -2
  107. package/src/uno/colors.ts +1 -0
  108. package/src/uno/shadows.ts +11 -8
@@ -1,5 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { useState } from 'react';
3
+ import { Box } from '../box/Box.js';
3
4
  import { Icon } from '../icon/index.js';
4
5
  import { Button } from './Button.js';
5
6
  import { ConfirmedButton } from './ConfirmedButton.js';
@@ -13,6 +14,12 @@ const meta = {
13
14
  },
14
15
  args: {
15
16
  children: 'Button',
17
+ loading: false,
18
+ color: 'default',
19
+ size: 'default',
20
+ visuallyDisabled: false,
21
+ disabled: false,
22
+ visuallyFocused: false,
16
23
  },
17
24
  } satisfies Meta<typeof Button>;
18
25
 
@@ -20,7 +27,37 @@ export default meta;
20
27
 
21
28
  type Story = StoryObj<typeof Button>;
22
29
 
23
- export const Default: Story = {};
30
+ export const Default: Story = {
31
+ render: (args) => {
32
+ return (
33
+ <Box gap items="center">
34
+ <Button {...args} />
35
+ <Button {...args} color="primary">
36
+ <Icon name="placeholder" />
37
+ {args.children}
38
+ </Button>
39
+ <Button {...args} size="small" />
40
+ </Box>
41
+ );
42
+ },
43
+ };
44
+
45
+ export const WithIcon: Story = {
46
+ args: {
47
+ children: (
48
+ <>
49
+ <Icon name="placeholder" />
50
+ Iconic
51
+ </>
52
+ ),
53
+ },
54
+ };
55
+
56
+ export const IconOnly: Story = {
57
+ args: {
58
+ children: <Icon name="placeholder" />,
59
+ },
60
+ };
24
61
 
25
62
  export const Toggled: Story = {
26
63
  render: (args) => {
@@ -31,18 +68,20 @@ export const Toggled: Story = {
31
68
  };
32
69
 
33
70
  export const Alignment: Story = {
34
- render() {
71
+ render(args) {
35
72
  return (
36
73
  <div className="col">
37
74
  <div className="row border-default">
38
- <Button size="small">Button</Button>
39
- <Button size="icon-small">
75
+ <Button size="small" {...args}>
76
+ Button
77
+ </Button>
78
+ <Button size="small" {...args}>
40
79
  <Icon name="placeholder" />
41
80
  </Button>
42
81
  </div>
43
82
  <div className="row border-default">
44
- <Button>Button</Button>
45
- <Button size="icon">
83
+ <Button {...args}>Button</Button>
84
+ <Button {...args}>
46
85
  <Icon name="placeholder" />
47
86
  </Button>
48
87
  </div>
@@ -52,12 +91,13 @@ export const Alignment: Story = {
52
91
  };
53
92
 
54
93
  export const ConfirmedButtonDemo: Story = {
55
- render() {
94
+ render(args) {
56
95
  return (
57
96
  <ConfirmedButton
58
97
  confirmText="Are you sure?"
59
98
  confirmTitle="Confirm"
60
99
  onConfirm={() => console.log('confirmed')}
100
+ {...args}
61
101
  >
62
102
  Confirm
63
103
  </ConfirmedButton>
@@ -1,6 +1,17 @@
1
1
  import { Slot } from '@radix-ui/react-slot';
2
2
  import classNames from 'clsx';
3
- import { ButtonHTMLAttributes, memo, Ref } from 'react';
3
+ import { AnimatePresence, motion } from 'motion/react';
4
+ import {
5
+ ButtonHTMLAttributes,
6
+ Children,
7
+ memo,
8
+ Ref,
9
+ useCallback,
10
+ useState,
11
+ } from 'react';
12
+ import { withClassName } from '../../hooks.js';
13
+ import useMergedRef from '../../hooks/useMergedRef.js';
14
+ import { IconLoadingProvider } from '../icon/IconLoadingContext.js';
4
15
  import { Icon } from '../icon/index.js';
5
16
  import { Spinner } from '../spinner/index.js';
6
17
  import { getButtonClassName } from './classes.js';
@@ -16,6 +27,9 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
16
27
  | 'accent'
17
28
  | 'contrast'
18
29
  | 'unstyled';
30
+ /**
31
+ * icon and icon-small are deprecated.
32
+ */
19
33
  size?: 'default' | 'small' | 'icon' | 'icon-small';
20
34
  toggled?: boolean;
21
35
  toggleMode?: 'color-and-indicator' | 'color' | 'indicator' | 'state-only';
@@ -27,7 +41,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
27
41
  ref?: Ref<HTMLButtonElement>;
28
42
  }
29
43
 
30
- export function Button({
44
+ export function ButtonRoot({
31
45
  className,
32
46
  color,
33
47
  size,
@@ -44,13 +58,21 @@ export function Button({
44
58
  ...props
45
59
  }: ButtonProps) {
46
60
  const Comp = asChild ? Slot : 'button';
61
+
62
+ const isFormSubmitting = false;
63
+ const isSubmitLoading = props.type === 'submit' && isFormSubmitting;
64
+ const isLoading = loading || isSubmitLoading;
65
+
66
+ const finalRef = useMergedRef(useAnnotateWithChildParts(), ref);
67
+
47
68
  const buttonProps = {
48
- ref: ref,
69
+ ref: finalRef,
49
70
  ...props,
50
- disabled: disabled || loading,
71
+ disabled: disabled || isLoading,
51
72
  'data-disabled': visuallyDisabled,
52
73
  'data-focus': visuallyFocused,
53
74
  'data-size': size,
75
+ 'data-loading': isLoading,
54
76
  tabIndex: visuallyDisabled ? -1 : undefined,
55
77
  className: classNames(
56
78
  getButtonClassName({
@@ -69,25 +91,68 @@ export function Button({
69
91
  buttonProps['aria-pressed'] = !!toggled;
70
92
  }
71
93
 
94
+ // for asChild, no need to do the rest of this stuff.
72
95
  if (asChild) {
73
96
  // avoid rendering loading spinner with asChild
74
97
  return <Comp {...buttonProps}>{children}</Comp>;
75
98
  }
76
99
 
100
+ // wrap and inspect children
101
+ let hasLabel = false;
102
+ let hasIcon = false;
103
+ const wrappedChildren = Children.toArray(children).map((child, index) => {
104
+ if (child && typeof child === 'object' && 'type' in child) {
105
+ const isIcon = child.type === Icon || child.type;
106
+ if (isIcon) {
107
+ hasIcon = true;
108
+ return child; // return icon as is
109
+ }
110
+ }
111
+
112
+ hasLabel = true; // mark that we have a label
113
+
114
+ if ((!!child && typeof child === 'string') || typeof child === 'number') {
115
+ return (
116
+ <span key={`text-${index}`} data-auto-wrapped-label className="flex">
117
+ {child}
118
+ </span>
119
+ );
120
+ }
121
+ return child;
122
+ });
123
+
77
124
  return (
78
- <Comp {...buttonProps}>
79
- {loading && <Spinner size={16} className="inline-block w-1em h-1em" />}
80
- {toggled !== undefined &&
81
- (toggleMode === 'indicator' ||
82
- toggleMode === 'color-and-indicator') && (
83
- <ToggleIndicator value={toggled} />
84
- )}
85
- {children}
86
- </Comp>
125
+ <IconLoadingProvider value={isLoading}>
126
+ <Comp
127
+ {...buttonProps}
128
+ data-has-icon={String(hasIcon || isLoading)}
129
+ data-has-label={String(hasLabel)}
130
+ >
131
+ <AnimatePresence>
132
+ {isLoading && !hasIcon && (
133
+ <motion.div
134
+ key="spinner"
135
+ initial={{ width: 0, marginLeft: '-0.5rem' }}
136
+ animate={{ width: 'auto', marginLeft: 0 }}
137
+ exit={{ width: 0, marginLeft: '-0.5rem' }}
138
+ className="flex-shrink-0 inline-block overflow-hidden my-auto flex"
139
+ >
140
+ <Spinner size={15} className="inline-block w-1em h-1em" />
141
+ </motion.div>
142
+ )}
143
+ </AnimatePresence>
144
+ {toggled !== undefined &&
145
+ (toggleMode === 'indicator' ||
146
+ toggleMode === 'color-and-indicator') && (
147
+ <ButtonToggleIndicator value={toggled} />
148
+ )}
149
+ {wrappedChildren}
150
+ </Comp>
151
+ </IconLoadingProvider>
87
152
  );
88
153
  }
89
154
 
90
- const ToggleIndicator = memo(function ToggleIndicator({
155
+ export const ButtonToggleIndicator = memo(function ToggleIndicator({
91
156
  value,
92
157
  }: {
93
158
  value: boolean;
@@ -103,3 +168,61 @@ const ToggleIndicator = memo(function ToggleIndicator({
103
168
  />
104
169
  );
105
170
  });
171
+
172
+ // allows custom icons to trigger icon button behavior
173
+ export const ButtonIcon = withClassName(
174
+ 'div',
175
+ 'icon flex-shrink-0 inline-block',
176
+ );
177
+
178
+ export const Button = Object.assign(ButtonRoot, {
179
+ ToggleIndicator: ButtonToggleIndicator,
180
+ Icon: ButtonIcon,
181
+ });
182
+
183
+ function useAnnotateWithChildParts() {
184
+ const mutationObserver = useState(() => {
185
+ if (typeof window === 'undefined') return null!;
186
+ return new MutationObserver((entries) => {
187
+ applyPartAttributes(entries[0].target as HTMLButtonElement);
188
+ });
189
+ })[0];
190
+
191
+ const ref = useCallback(
192
+ (node: HTMLButtonElement | null) => {
193
+ if (node && mutationObserver) {
194
+ mutationObserver.disconnect();
195
+ mutationObserver.observe(node, { childList: true, subtree: true });
196
+ applyPartAttributes(node);
197
+ } else if (mutationObserver) {
198
+ mutationObserver.disconnect();
199
+ }
200
+ },
201
+ [mutationObserver],
202
+ );
203
+
204
+ return ref;
205
+ }
206
+
207
+ function applyPartAttributes(button: HTMLButtonElement) {
208
+ // each child node that's not an icon counts as a label
209
+ const registry = {
210
+ icon: 0,
211
+ label: 0,
212
+ };
213
+ button.childNodes.forEach((child) => {
214
+ if (!(child instanceof HTMLElement || child instanceof SVGElement)) return;
215
+ if (child.style.display === 'none' || child.style.width === '0') return; // skip hidden elements
216
+ if (
217
+ (child instanceof HTMLElement || child instanceof SVGElement) &&
218
+ child.classList.contains('icon')
219
+ ) {
220
+ registry.icon++;
221
+ } else {
222
+ registry.label++;
223
+ }
224
+ });
225
+ button.setAttribute('data-has-icon', String(registry.icon > 0));
226
+ button.setAttribute('data-has-label', String(registry.label > 0));
227
+ button.setAttribute('data-icon-count', String(registry.icon));
228
+ }
@@ -1,9 +1,16 @@
1
- import classNames from 'clsx';
2
- import type { ButtonProps } from './Button.jsx';
1
+ import clsx from 'clsx';
2
+ import { type ButtonProps } from './Button.jsx';
3
+
4
+ const sizeMap = {
5
+ default: 'default',
6
+ small: 'small',
7
+ icon: 'default',
8
+ 'icon-small': 'small',
9
+ } satisfies Record<string, 'default' | 'small'>;
3
10
 
4
11
  export function getButtonClassName({
5
12
  color,
6
- size,
13
+ size: rawSize,
7
14
  toggleable,
8
15
  align,
9
16
  }: {
@@ -12,8 +19,10 @@ export function getButtonClassName({
12
19
  toggleable?: boolean;
13
20
  align?: ButtonProps['align'];
14
21
  }) {
15
- return classNames(
16
- 'layer-components:(px-4 py-2 bg-[var(--bg-neutral,var(--bg))] [--webkit-tap-highlight-color:transparent] [line-height:1] text-size-md font-inherit border-solid border-thin border-transparent rounded-lg cursor-pointer font-bold flex flex-row gap-1 items-center relative overflow-visible select-none all:transition duration-200 whitespace-nowrap ring-bg)',
22
+ const size = sizeMap[rawSize ?? 'default'];
23
+
24
+ return clsx(
25
+ 'layer-components:(px-4 py-2 bg-[var(--bg-neutral,var(--bg))] [--webkit-tap-highlight-color:transparent] [line-height:1] text-size-md font-inherit border-solid border-thin border-transparent rounded-lg cursor-pointer font-bold flex flex-row gap-sm items-center relative overflow-visible select-none all:transition duration-200 whitespace-nowrap ring-bg)',
17
26
  'layer-components:hover:(bg-[var(--bg)] bg-darken-1 ring-4)',
18
27
  'layer-components:focus:outline-off',
19
28
  'layer-components:focus-visible:(bg-[var(--bg)] outline-off bg-darken-1 ring-6)',
@@ -37,7 +46,7 @@ export function getButtonClassName({
37
46
  const colors = {
38
47
  primary: `layer-variants:[&.btn-color-primary]:([--bg:var(--color-primary)] shadow-sm color-black border-primary-dark)`,
39
48
  accent: `layer-variants:[&.btn-color-accent]:([--bg-neutral:var(--color-accent-wash)] [--bg:var(--color-accent-light)] shadow-sm color-black border-accent-dark)`,
40
- default: `layer-variants:[&.btn-color-default]:([--bg-neutral:var(--color-white)] [--bg:var(--color-gray-light)] shadow-sm color-black border-gray-dark)`,
49
+ default: `layer-variants:[&.btn-color-default]:([--bg-neutral:var(--color-white)] [--bg:var(--color-gray)] shadow-sm color-black border-gray-dark focus-visible:bg-lighten-1 hover:bg-lighten-1)`,
41
50
  ghost: `layer-variants:[&.btn-color-ghost]:([--bg-neutral:transparent] [--bg:oklch(from_var(--color-gray)_l_c_h/50%)] color-dark-blend)`,
42
51
  destructive: `layer-variants:[&.btn-color-destructive]:([--bg:var(--color-attention)] shadow-sm border-attention-dark color-black)`,
43
52
  ghostDestructive: `layer-variants:[&.btn-color-ghostDestructive]:([--bg-neutral:transparent] [--bg:var(--color-attention-light)] color-attention-dark hover:(color-black) focus-visible:(color-black))`,
@@ -48,11 +57,10 @@ const colors = {
48
57
  export const buttonColorClasses = colors;
49
58
 
50
59
  const sizes = {
51
- default: '',
52
- small: 'layer-variants:[&.size-small]:(px-4 py-1 text-sm rounded-md)',
53
- icon: 'layer-variants:[&.size-icon]:(p-2.35 text-sm rounded-lg)',
54
- 'icon-small':
55
- 'layer-variants:[&.size-icon-small]:(p-2 text-xs rounded-lg -m-y-0.5)',
60
+ default:
61
+ 'layer-variants:[&[data-has-icon=true][data-has-label=false]]:(p-2.35 text-sm rounded-lg)',
62
+ small:
63
+ 'layer-variants:[&.size-small]:(px-4 py-1 text-sm rounded-md) layer-variants:[&.size-small[data-has-icon=true][data-has-label=false]]:(p-2 text-xs rounded-lg -m-y-0.5)',
56
64
  };
57
65
 
58
66
  const toggledClass =
@@ -244,7 +244,6 @@ export const CameraDeviceSelector = (props: CameraDeviceSelectorProps) => {
244
244
  if (devices.length === 2) {
245
245
  return (
246
246
  <Button
247
- size="icon"
248
247
  color="ghost"
249
248
  className="absolute bottom-2 left-2 color-white"
250
249
  onClick={swapCamera}
@@ -260,11 +259,7 @@ export const CameraDeviceSelector = (props: CameraDeviceSelectorProps) => {
260
259
  onValueChange={selectDeviceId}
261
260
  >
262
261
  <Select.Trigger asChild>
263
- <Button
264
- size="icon"
265
- color="ghost"
266
- className="absolute bottom-2 left-2 color-white"
267
- >
262
+ <Button color="ghost" className="absolute bottom-2 left-2 color-white">
268
263
  <Icon name="refresh" />
269
264
  </Button>
270
265
  </Select.Trigger>
@@ -284,7 +279,6 @@ export const CameraFullscreenButton = (props: ButtonProps) => {
284
279
  return (
285
280
  <Button
286
281
  {...props}
287
- size="icon"
288
282
  color="ghost"
289
283
  className="absolute top-2 right-2 color-white"
290
284
  onClick={() => setFullscreen(!fullscreen)}
@@ -46,12 +46,12 @@ export const Default: Story = {
46
46
  <CardFooter>
47
47
  <CardActions>
48
48
  <Button size="small">Button</Button>
49
- <Button size="icon-small" color="primary">
49
+ <Button size="small" color="primary">
50
50
  <Icon name="placeholder" />
51
51
  </Button>
52
52
  </CardActions>
53
53
  <CardMenu>
54
- <Button size="icon-small" color="ghost">
54
+ <Button size="small" color="ghost">
55
55
  <Icon name="dots" />
56
56
  </Button>
57
57
  </CardMenu>
@@ -68,7 +68,7 @@ export const Default: Story = {
68
68
  <CardFooter>
69
69
  <CardActions>
70
70
  <Button size="small">Button</Button>
71
- <Button size="icon-small" color="ghost">
71
+ <Button size="small" color="ghost">
72
72
  <Icon name="placeholder" />
73
73
  </Button>
74
74
  </CardActions>
@@ -89,7 +89,7 @@ export const Compact: Story = {
89
89
  <CardFooter>
90
90
  <CardActions>
91
91
  <Button size="small">Button</Button>
92
- <Button size="icon-small" color="ghost">
92
+ <Button size="small" color="ghost">
93
93
  <Icon name="placeholder" />
94
94
  </Button>
95
95
  </CardActions>
@@ -108,7 +108,7 @@ export const NonInteractive: Story = {
108
108
  <CardFooter>
109
109
  <CardActions>
110
110
  <Button size="small">Button</Button>
111
- <Button size="icon" color="ghost">
111
+ <Button color="ghost">
112
112
  <Icon name="placeholder" />
113
113
  </Button>
114
114
  </CardActions>
@@ -129,7 +129,7 @@ export const AsChild: Story = {
129
129
  <CardFooter>
130
130
  <CardActions>
131
131
  <Button size="small">Button</Button>
132
- <Button size="icon" color="ghost">
132
+ <Button color="ghost">
133
133
  <Icon name="placeholder" />
134
134
  </Button>
135
135
  </CardActions>
@@ -150,7 +150,7 @@ export const AsChildNonInteractive: Story = {
150
150
  <CardFooter>
151
151
  <CardActions>
152
152
  <Button size="small">Button</Button>
153
- <Button size="icon" color="ghost">
153
+ <Button color="ghost">
154
154
  <Icon name="placeholder" />
155
155
  </Button>
156
156
  </CardActions>
@@ -249,7 +249,7 @@ export const VisuallyFocused: Story = {
249
249
  <CardFooter>
250
250
  <CardActions>
251
251
  <Button size="small">Button</Button>
252
- <Button size="icon" color="ghost">
252
+ <Button color="ghost">
253
253
  <Icon name="placeholder" />
254
254
  </Button>
255
255
  </CardActions>
@@ -270,7 +270,7 @@ export const CardsInBox: Story = {
270
270
  <CardFooter>
271
271
  <CardActions>
272
272
  <Button size="small">Button</Button>
273
- <Button size="icon" color="ghost">
273
+ <Button color="ghost">
274
274
  <Icon name="placeholder" />
275
275
  </Button>
276
276
  </CardActions>
@@ -284,7 +284,7 @@ export const CardsInBox: Story = {
284
284
  <CardFooter>
285
285
  <CardActions>
286
286
  <Button size="small">Button</Button>
287
- <Button size="icon" color="ghost">
287
+ <Button color="ghost">
288
288
  <Icon name="placeholder" />
289
289
  </Button>
290
290
  </CardActions>
@@ -40,7 +40,6 @@ export function DatePicker({
40
40
  >
41
41
  <MonthRow>
42
42
  <MonthButton
43
- size="icon"
44
43
  color="ghost"
45
44
  onClick={() =>
46
45
  setDisplay((cur) => ({
@@ -53,7 +52,6 @@ export function DatePicker({
53
52
  </MonthButton>
54
53
  <MonthLabel>{monthLabel}</MonthLabel>
55
54
  <MonthButton
56
- size="icon"
57
55
  color="ghost"
58
56
  onClick={() =>
59
57
  setDisplay((cur) => ({
@@ -143,7 +141,6 @@ export function DateRangePicker({
143
141
  >
144
142
  <RangeLayout>
145
143
  <MonthButton
146
- size="icon"
147
144
  color="ghost"
148
145
  className="[grid-area:prevMonth]"
149
146
  onClick={() =>
@@ -160,7 +157,6 @@ export function DateRangePicker({
160
157
  {nextMonthLabel}
161
158
  </MonthLabel>
162
159
  <MonthButton
163
- size="icon"
164
160
  color="ghost"
165
161
  className="[grid-area:nextMonth]"
166
162
  onClick={() =>
@@ -71,11 +71,11 @@ export const EmojiPickerEmoji = withClassName(
71
71
  color="ghost"
72
72
  toggled={p.emoji.isActive}
73
73
  toggleMode="color"
74
- size="icon-small"
74
+ size="small"
75
75
  aria-label={p.emoji.label}
76
76
  className="text-lg p-xs"
77
77
  >
78
- {p.emoji.emoji}
78
+ <Button.Icon>{p.emoji.emoji}</Button.Icon>
79
79
  </Button>
80
80
  ),
81
81
  '',
@@ -122,12 +122,12 @@ export const EmojiPickerSkinToneSelector = (props: BoxProps) => {
122
122
  color="ghost"
123
123
  toggled={option.skinTone === skinTone}
124
124
  toggleMode="color"
125
- size="icon-small"
125
+ size="small"
126
126
  aria-label={`Skin tone ${option}`}
127
127
  className="text-md p-xs"
128
128
  onClick={() => setSkinTone(option.skinTone)}
129
129
  >
130
- {option.emoji}
130
+ <Button.Icon>{option.emoji}</Button.Icon>
131
131
  </Button>
132
132
  ))}
133
133
  </Box>
@@ -36,7 +36,6 @@ const meta = {
36
36
  <Button size="small">Twenty three</Button>
37
37
  <Button size="small">Twenty four</Button>
38
38
  <Button
39
- size="icon"
40
39
  color="primary"
41
40
  className="sticky right-2 bottom-2 flex-shrink-0 shadow-sm ml-auto"
42
41
  >
@@ -113,7 +112,6 @@ export const CantOpen: Story = {
113
112
  </>
114
113
  )}
115
114
  <Button
116
- size="icon"
117
115
  color="primary"
118
116
  className="sticky right-2 bottom-2 flex-shrink-0 shadow-sm ml-auto"
119
117
  >
@@ -169,7 +169,6 @@ export function HorizontalList({
169
169
  >
170
170
  <Button
171
171
  onClick={toggleOpen}
172
- size="icon"
173
172
  color="ghost"
174
173
  className={clsx(
175
174
  'flex-shrink-0 bg-[var(--scroll-bg)] sticky left-0 top-2 z-1',
@@ -1,6 +1,8 @@
1
1
  import classNames from 'clsx';
2
2
  import { HTMLAttributes } from 'react';
3
+ import { Spinner } from '../spinner/Spinner.js';
3
4
  import { IconName } from './generated/iconNames.js';
5
+ import { useIconLoading } from './IconLoadingContext.js';
4
6
 
5
7
  export interface IconProps extends HTMLAttributes<SVGSVGElement> {
6
8
  name: IconName;
@@ -16,10 +18,17 @@ export const Icon = function Icon({
16
18
  }: IconProps & {
17
19
  ref?: React.Ref<SVGSVGElement>;
18
20
  }) {
21
+ const loading = useIconLoading();
22
+
23
+ if (loading) {
24
+ return <Spinner size={size} className="inline-block" />;
25
+ }
26
+
19
27
  return (
20
28
  <svg
21
29
  ref={ref}
22
30
  className={classNames(
31
+ 'icon',
23
32
  'flex-shrink-0 layer-components:fill-none',
24
33
  className,
25
34
  )}
@@ -0,0 +1,7 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ const IconLoadingContext = createContext(false);
4
+ export const IconLoadingProvider = IconLoadingContext.Provider;
5
+ export function useIconLoading() {
6
+ return useContext(IconLoadingContext);
7
+ }
@@ -263,7 +263,6 @@ export function ImageUploaderRemoveButton({ className, ...rest }: ButtonProps) {
263
263
  return (
264
264
  <Button
265
265
  color="ghost"
266
- size="icon"
267
266
  className={clsx(
268
267
  'layer-variants:(absolute top-2 right-2 w-32px h-32px border-none p-2 cursor-pointer bg-white color-black rounded-lg transition-colors shadow-sm z-10)',
269
268
  className,
@@ -39,8 +39,8 @@ function ExplodeButton() {
39
39
  );
40
40
  };
41
41
  return (
42
- <Button size="icon" onClick={burst}>
43
- 💥
42
+ <Button onClick={burst}>
43
+ <Button.Icon>💥</Button.Icon>
44
44
  </Button>
45
45
  );
46
46
  }
@@ -18,7 +18,8 @@ import {
18
18
  } from 'react';
19
19
  import { withClassName } from '../../hooks/withClassName.js';
20
20
  import { BoxContext } from '../box/Box.js';
21
- import { Button, ButtonProps, getButtonClassName } from '../button/index.js';
21
+ import { Button, ButtonProps } from '../button/Button.js';
22
+ import { getButtonClassName } from '../button/classes.js';
22
23
  import { Icon } from '../icon/index.js';
23
24
 
24
25
  export const SelectItem = ({
@@ -25,7 +25,7 @@ export const Spinner = function Spinner({
25
25
  role="progressbar"
26
26
  {...props}
27
27
  className={classNames(
28
- 'inline-block animate-spin animate-ease-linear animate-iteration-infinite color-inherit animate-duration-1400 transform-origin-[50%_50%]',
28
+ 'flex-shrink-0 inline-block animate-spin animate-ease-linear animate-iteration-infinite color-inherit animate-duration-1400 transform-origin-[50%_50%]',
29
29
  className,
30
30
  )}
31
31
  style={{ width: size, height: size, ...style }}
@@ -53,6 +53,7 @@ const meta = {
53
53
  controls: { expanded: true },
54
54
  layout: 'fullscreen',
55
55
  },
56
+ tags: [],
56
57
  } satisfies Meta;
57
58
 
58
59
  export default meta;
@@ -121,7 +122,7 @@ function DemoUI({ className }: { className?: string }) {
121
122
  <Card.Footer>
122
123
  <Card.Actions>
123
124
  <Button size="small">Action 1</Button>
124
- <Button size="icon-small" color="ghost">
125
+ <Button size="small" color="ghost">
125
126
  <Icon name="placeholder" />
126
127
  </Button>
127
128
  </Card.Actions>
@@ -135,7 +136,7 @@ function DemoUI({ className }: { className?: string }) {
135
136
  <Card.Footer>
136
137
  <Card.Actions>
137
138
  <Button size="small">Action 1</Button>
138
- <Button size="icon-small" color="ghost">
139
+ <Button size="small" color="ghost">
139
140
  <Icon name="placeholder" />
140
141
  </Button>
141
142
  </Card.Actions>
package/src/uno/colors.ts CHANGED
@@ -88,6 +88,7 @@ export const themeColors = {
88
88
  white: 'var(--color-white)',
89
89
  wash: 'var(--color-wash)',
90
90
  transparent: 'transparent',
91
+ current: 'currentColor',
91
92
 
92
93
  // magic tokens
93
94
  bg: 'var(--v-bg-altered, var(--v-bg, transparent))',