aplance-ui 1.3.0 → 1.5.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 (44) hide show
  1. package/package.json +1 -1
  2. package/registry/components/primitives/icon/icon.tsx +4 -6
  3. package/registry/components/primitives/modal/modal.module.css +5 -0
  4. package/registry/components/primitives/modal/modal.tsx +32 -34
  5. package/registry/components/primitives/popover/popover-context.ts +33 -29
  6. package/registry/components/primitives/popover/popover.module.css +4 -2
  7. package/registry/components/primitives/popover/popover.tsx +150 -37
  8. package/registry/components/ui/accordion/accordion.module.css +52 -0
  9. package/registry/components/ui/accordion/accordion.tsx +97 -25
  10. package/registry/components/ui/avatar/avatar.tsx +2 -2
  11. package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +9 -9
  12. package/registry/components/ui/button/button.module.css +43 -5
  13. package/registry/components/ui/button/button.tsx +2 -1
  14. package/registry/components/ui/copy-button/copy-button.module.css +2 -0
  15. package/registry/components/ui/copy-button/copy-button.tsx +5 -0
  16. package/registry/components/ui/data-table/data-table.module.css +786 -710
  17. package/registry/components/ui/data-table/data-table.tsx +1238 -1211
  18. package/registry/components/ui/data-table/filter-modal.tsx +86 -0
  19. package/registry/components/ui/data-table/filters.tsx +21 -0
  20. package/registry/components/ui/data-table/pagination-bar.tsx +31 -17
  21. package/registry/components/ui/data-table/types.ts +19 -1
  22. package/registry/components/ui/data-table/useDataTable.tsx +2 -2
  23. package/registry/components/ui/data-table/utils.ts +30 -4
  24. package/registry/components/ui/input-fields/common/input-field-common.module.css +8 -2
  25. package/registry/components/ui/input-fields/common/picker-popover-common.module.css +4 -0
  26. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +118 -76
  27. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +552 -484
  28. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +936 -933
  29. package/registry/components/ui/input-fields/date-picker/types.ts +2 -0
  30. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +8 -0
  31. package/registry/components/ui/input-fields/input/input.tsx +348 -348
  32. package/registry/components/ui/input-fields/media-input/media-input.tsx +3 -1
  33. package/registry/components/ui/input-fields/media-input/types.ts +8 -2
  34. package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +5 -5
  35. package/registry/components/ui/input-fields/media-input/utils.ts +18 -5
  36. package/registry/components/ui/scroll-rail/scroll-rail.module.css +104 -0
  37. package/registry/components/ui/scroll-rail/scroll-rail.tsx +159 -0
  38. package/registry/components/ui/tabs/tabs.module.css +8 -12
  39. package/registry/components/ui/tabs/tabs.tsx +94 -50
  40. package/registry/components/ui/tooltip/tooltip.module.css +8 -3
  41. package/registry/components/ui/tooltip/tooltip.tsx +16 -3
  42. package/registry/hooks/useMediaQuery.ts +38 -0
  43. package/registry/lib/string.ts +3 -3
  44. package/registry/registry.json +54 -4
@@ -9,6 +9,7 @@ import {
9
9
  import clsx from "clsx";
10
10
  import { IconChevronDown } from "@tabler/icons-react";
11
11
  import { useResizeObserver } from "../../../hooks/useResizeObserver";
12
+ import { Button } from "../button/button";
12
13
  import s from "./accordion.module.css";
13
14
 
14
15
  export type AccordionVariant = "bordered" | "minimal";
@@ -17,12 +18,23 @@ export type AccordionMode = "single" | "multiple";
17
18
 
18
19
  export type AccordionSize = "sm" | "md" | "lg";
19
20
 
21
+ /**
22
+ * How an item opens. icon: the whole heading, with a chevron in it. button:
23
+ * the heading too, with a bordered chevron button at its end, as a table row
24
+ * ends in its menu button.
25
+ */
26
+ export type AccordionToggle = "icon" | "button";
27
+
20
28
  export type AccordionItem = {
21
29
  id: string;
22
30
  title: ReactNode;
23
31
  content: ReactNode;
24
32
  disabled?: boolean;
25
33
  defaultOpen?: boolean;
34
+ /** Controls beside the heading, outside the button that opens it: a row's menu. */
35
+ actions?: ReactNode;
36
+ /** This item's own class, beside `itemClassName`: one marked apart from the rest. */
37
+ className?: string;
26
38
  };
27
39
 
28
40
  type AccordionBaseProps = {
@@ -35,6 +47,7 @@ type AccordionBaseProps = {
35
47
  size?: AccordionSize;
36
48
  icon?: ReactNode;
37
49
  iconRotation?: number;
50
+ toggle?: AccordionToggle;
38
51
  };
39
52
 
40
53
  type AccordionSingleProps = {
@@ -80,6 +93,7 @@ type ItemViewProps = {
80
93
  size: AccordionSize;
81
94
  variant: AccordionVariant;
82
95
  icon?: ReactNode;
96
+ toggle: AccordionToggle;
83
97
  itemClassName?: string;
84
98
  triggerClassName?: string;
85
99
  contentClassName?: string;
@@ -114,6 +128,7 @@ const AccordionItemView = ({
114
128
  size,
115
129
  variant,
116
130
  icon,
131
+ toggle,
117
132
  itemClassName,
118
133
  triggerClassName,
119
134
  contentClassName,
@@ -138,6 +153,13 @@ const AccordionItemView = ({
138
153
 
139
154
  useResizeObserver(contentRef, updateHeight, { enabled: isOpen });
140
155
 
156
+ const chevron = icon ?? (
157
+ <IconChevronDown size={size === "sm" ? 16 : size === "lg" ? 20 : 18} />
158
+ );
159
+ // With actions or a toggle button, the heading is a row: the button that
160
+ // opens the item, then the controls beside it, outside that button.
161
+ const row = toggle === "button" || item.actions !== undefined;
162
+
141
163
  return (
142
164
  <section
143
165
  className={clsx(
@@ -146,42 +168,90 @@ const AccordionItemView = ({
146
168
  isOpen && s.item_open,
147
169
  item.disabled && s.item_disabled,
148
170
  itemClassName,
171
+ item.className,
149
172
  )}
150
173
  data-state={isOpen ? "open" : "closed"}
151
174
  >
152
- <button
153
- id={triggerId}
154
- type="button"
155
- disabled={item.disabled}
156
- aria-expanded={isOpen}
157
- aria-controls={panelId}
158
- onClick={onToggle}
159
- className={clsx(
160
- s.trigger,
161
- s[`trigger_${size}`],
162
- s[`trigger_${variant}`],
163
- isOpen && s.trigger_open,
164
- triggerClassName,
165
- )}
166
- >
167
- <span className={clsx(s.title, s[`title_${size}`])}>{item.title}</span>
168
- <span
169
- className={clsx(s.icon, isOpen && s.icon_open)}
170
- aria-hidden="true"
175
+ {row ? (
176
+ <div
177
+ className={clsx(
178
+ s.header,
179
+ s[`trigger_${size}`],
180
+ s[`header_${variant}`],
181
+ isOpen && s.trigger_open,
182
+ )}
171
183
  >
172
- {icon ?? (
173
- <IconChevronDown
174
- size={size === "sm" ? 16 : size === "lg" ? 20 : 18}
175
- />
184
+ <button
185
+ id={triggerId}
186
+ type="button"
187
+ disabled={item.disabled}
188
+ aria-expanded={isOpen}
189
+ aria-controls={panelId}
190
+ onClick={onToggle}
191
+ className={clsx(s.trigger, s.header_trigger, triggerClassName)}
192
+ >
193
+ <span className={clsx(s.title, s[`title_${size}`])}>{item.title}</span>
194
+ {toggle === "icon" ? (
195
+ <span
196
+ className={clsx(s.icon, isOpen && s.icon_open)}
197
+ aria-hidden="true"
198
+ >
199
+ {chevron}
200
+ </span>
201
+ ) : null}
202
+ </button>
203
+ {item.actions !== undefined ? (
204
+ <div className={s.actions}>{item.actions}</div>
205
+ ) : null}
206
+ {toggle === "button" ? (
207
+ <Button
208
+ type="button"
209
+ variant="bordered"
210
+ size="sm"
211
+ layout="box"
212
+ aria-label={isOpen ? "Collapse section" : "Expand section"}
213
+ aria-expanded={isOpen}
214
+ aria-controls={panelId}
215
+ disabled={item.disabled}
216
+ onClick={onToggle}
217
+ className={clsx(s.toggle, isOpen && s.toggle_open)}
218
+ >
219
+ {chevron}
220
+ </Button>
221
+ ) : null}
222
+ </div>
223
+ ) : (
224
+ <button
225
+ id={triggerId}
226
+ type="button"
227
+ disabled={item.disabled}
228
+ aria-expanded={isOpen}
229
+ aria-controls={panelId}
230
+ onClick={onToggle}
231
+ className={clsx(
232
+ s.trigger,
233
+ s[`trigger_${size}`],
234
+ s[`trigger_${variant}`],
235
+ isOpen && s.trigger_open,
236
+ triggerClassName,
176
237
  )}
177
- </span>
178
- </button>
238
+ >
239
+ <span className={clsx(s.title, s[`title_${size}`])}>{item.title}</span>
240
+ <span
241
+ className={clsx(s.icon, isOpen && s.icon_open)}
242
+ aria-hidden="true"
243
+ >
244
+ {chevron}
245
+ </span>
246
+ </button>
247
+ )}
179
248
 
180
249
  <div
181
250
  id={panelId}
182
251
  role="region"
183
252
  aria-labelledby={triggerId}
184
253
  aria-hidden={!isOpen}
254
+ inert={!isOpen}
185
255
  className={clsx(s.panel, isOpen && s.panel_open)}
186
256
  style={{ height: `${panelHeight}px` }}
187
257
  >
@@ -212,6 +282,7 @@ export const Accordion = ({
212
282
  size = "md",
213
283
  icon,
214
284
  iconRotation = 180,
285
+ toggle = "icon",
215
286
  ...variantProps
216
287
  }: AccordionProps) => {
217
288
  const uid = useId();
@@ -317,6 +388,7 @@ export const Accordion = ({
317
388
  size={size}
318
389
  variant={variant}
319
390
  icon={icon}
391
+ toggle={toggle}
320
392
  itemClassName={itemClassName}
321
393
  triggerClassName={triggerClassName}
322
394
  contentClassName={contentClassName}
@@ -17,10 +17,10 @@ const COLORS = [
17
17
  { bg: "oklch(0.92 0.06 85)", fg: "oklch(0.4 0.15 85)" },
18
18
  { bg: "oklch(0.92 0.05 25)", fg: "oklch(0.4 0.15 25)" },
19
19
  { bg: "oklch(0.92 0.05 330)", fg: "oklch(0.4 0.15 330)" },
20
- ];
20
+ ] as const;
21
21
 
22
22
  export function Avatar({ name, size = "md", className }: AvatarProps) {
23
- const color = COLORS[hashName(name) % COLORS.length];
23
+ const color = COLORS[hashName(name) % COLORS.length] ?? COLORS[0];
24
24
  const initials = getInitials(name) || "?";
25
25
  return (
26
26
  <div
@@ -9,8 +9,7 @@
9
9
  --breadcrumbs-color-current: var(--color-foreground);
10
10
  --breadcrumbs-color-separator: var(--color-muted-foreground);
11
11
  --breadcrumbs-font-size: var(--font-size-sm);
12
- --breadcrumbs-gap: var(--spacing-3);
13
- --breadcrumbs-padding: var(--spacing-2) var(--spacing-3);
12
+ --breadcrumbs-gap: var(--spacing-5);
14
13
 
15
14
  display: block;
16
15
  width: 100%;
@@ -22,8 +21,7 @@
22
21
 
23
22
  .size_sm {
24
23
  --breadcrumbs-font-size: var(--font-size-xs);
25
- --breadcrumbs-gap: var(--spacing-2);
26
- --breadcrumbs-padding: var(--spacing-1) var(--spacing-2);
24
+ --breadcrumbs-gap: var(--spacing-4);
27
25
  }
28
26
 
29
27
  /* ─── List ───────────────────────────────────────────────────────────────── */
@@ -45,15 +43,18 @@
45
43
  min-width: 0;
46
44
  }
47
45
 
48
- /* ─── Crumb (link / button / span) ───────────────────────────────────────── */
46
+ /* ─── Crumb (link / button / span) ───────────────────────────────────────────
47
+ A text link, not a button: no padding or hover fill, so every crumb sits on
48
+ the same edge as the content under it. Hover darkens the text.
49
+ ─────────────────────────────────────────────────────────────────────────── */
49
50
 
50
51
  .crumb {
51
52
  display: inline-flex;
52
53
  align-items: center;
53
54
  gap: var(--spacing-2);
54
55
  min-width: 0;
55
- padding: var(--breadcrumbs-padding);
56
- border-radius: var(--radius-md);
56
+ padding: 0;
57
+ border-radius: var(--radius-sm);
57
58
  color: inherit;
58
59
  background-color: transparent;
59
60
  font: inherit;
@@ -62,7 +63,7 @@
62
63
  white-space: nowrap;
63
64
  cursor: default;
64
65
  transition: var(--transition-fast);
65
- transition-property: background-color, color;
66
+ transition-property: color;
66
67
  }
67
68
 
68
69
  .crumb_link {
@@ -71,7 +72,6 @@
71
72
 
72
73
  &:hover {
73
74
  color: var(--breadcrumbs-color-hover);
74
- background-color: var(--color-muted);
75
75
  }
76
76
 
77
77
  &:focus-visible {
@@ -1,4 +1,4 @@
1
- .base {
1
+ .base {
2
2
  --button-fg: var(--color-foreground);
3
3
  --button-placeholder: var(--button-fg);
4
4
  --button-icon: var(--button-fg);
@@ -11,7 +11,9 @@
11
11
  min-height: var(--spacing-11);
12
12
  border-radius: var(--radius-md);
13
13
  display: flex;
14
- min-width: fit-content;
14
+ /* Never narrower than its label, unless the host sets the variable (a
15
+ full-width field trigger sets 0, so its label truncates). */
16
+ min-width: var(--button-min-width, fit-content);
15
17
  align-items: center;
16
18
  justify-content: space-between;
17
19
  gap: var(--spacing-4);
@@ -24,7 +26,7 @@
24
26
  outline: none;
25
27
  border: none;
26
28
 
27
- line-height: 1.125;
29
+ line-height: 1.125;
28
30
 
29
31
  svg {
30
32
  cursor: pointer;
@@ -36,12 +38,16 @@
36
38
  font-size: var(--font-size-xs);
37
39
  }
38
40
 
39
- &.box {
41
+ /* An icon button: as wide as it is high, its own size and never its
42
+ content's (the base keeps a button at least as wide as its content), so
43
+ a round one stays round; the icon sits in its middle. */
44
+ &.box {
40
45
  width: var(--spacing-11);
46
+ min-width: 0;
41
47
  min-height: unset;
42
48
  justify-content: center;
43
49
  height: var(--spacing-11);
44
- padding: var(--spacing-2);
50
+ padding: 0;
45
51
 
46
52
  &.sm {
47
53
  width: var(--spacing-10);
@@ -183,6 +189,38 @@
183
189
  }
184
190
  }
185
191
 
192
+ /* Same pattern - danger is the soft red a Badge's error tone uses, for an
193
+ action that cannot be taken back: deleting, purging, removing. It reads in
194
+ both themes from the error tokens alone. */
195
+ :is(.danger, .tone_danger) {
196
+ --button-fg: var(--color-error-foreground);
197
+ --button-placeholder: var(--color-error-foreground);
198
+ --button-icon: var(--color-error-foreground);
199
+ }
200
+
201
+ .danger {
202
+ background-color: var(--color-error-subtle);
203
+ border: 1px solid
204
+ color-mix(in oklch, var(--color-error-foreground) 24%, var(--color-border));
205
+ color: var(--button-fg);
206
+ box-shadow: var(--shadow-sm);
207
+
208
+ svg {
209
+ color: var(--button-icon);
210
+ }
211
+
212
+ &:hover:not(.disabled),
213
+ &:active:not(.disabled),
214
+ &.input_focus:focus,
215
+ &.active {
216
+ background-color: color-mix(
217
+ in oklch,
218
+ var(--color-error-foreground) 14%,
219
+ var(--color-error-subtle)
220
+ );
221
+ }
222
+ }
223
+
186
224
  /* Same pattern - ghost is transparent background; tone alias lets composing
187
225
  components inherit these vars without applying the full ghost layout rules. */
188
226
  :is(.ghost, .tone_ghost) {
@@ -12,7 +12,8 @@ export type ButtonVariant =
12
12
  | "secondary"
13
13
  | "bordered"
14
14
  | "border_b"
15
- | "ghost";
15
+ | "ghost"
16
+ | "danger";
16
17
  export type ButtonSize = "default" | "sm";
17
18
  export type ButtonLayout = "default" | "box";
18
19
  export type AsProp = "button" | "div" | "span" | "link";
@@ -1,6 +1,8 @@
1
+ /* A fixed control: long text beside it, as in a table cell, never squeezes it. */
1
2
  .base {
2
3
  display: inline-flex;
3
4
  align-items: center;
5
+ flex-shrink: 0;
4
6
  }
5
7
 
6
8
  .icon {
@@ -18,6 +18,9 @@ export type CopyButtonProps = {
18
18
  layout?: ButtonLayout;
19
19
  preview?: CopyPreview;
20
20
  previewMaxLength?: number;
21
+ /** Called with the value after a successful copy (e.g. to toast, or clear a
22
+ * one-time reveal). Not called if the copy fails. */
23
+ onCopy?: (value: string) => void;
21
24
  };
22
25
 
23
26
  const maskToken = (raw: string): string => {
@@ -44,6 +47,7 @@ export function CopyButton({
44
47
  layout,
45
48
  preview = "none",
46
49
  previewMaxLength = 24,
50
+ onCopy,
47
51
  }: CopyButtonProps) {
48
52
  const [copied, setCopied] = useState(false);
49
53
  const timeoutRef = useRef<number | null>(null);
@@ -82,6 +86,7 @@ export function CopyButton({
82
86
  const success = await copyWithFallback(value);
83
87
  if (!success) return;
84
88
 
89
+ onCopy?.(value);
85
90
  setCopied(true);
86
91
  if (timeoutRef.current !== null) {
87
92
  window.clearTimeout(timeoutRef.current);