aplance-ui 1.0.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 (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -0
  3. package/dist/api.d.ts +175 -0
  4. package/dist/api.js +1954 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/frameworks/astro/astro.config.snippet.md +23 -0
  7. package/dist/frameworks/astro/postcss.config.mjs.template +20 -0
  8. package/dist/frameworks/astro/vite-inline.snippet.md +22 -0
  9. package/dist/frameworks/electron-vite/vite.config.snippet.md +44 -0
  10. package/dist/frameworks/next/layout-import.snippet.md +19 -0
  11. package/dist/frameworks/next/next.config.snippet.md +15 -0
  12. package/dist/frameworks/next/postcss.config.js.template +16 -0
  13. package/dist/frameworks/tanstack-start/postcss.config.ts.template +17 -0
  14. package/dist/frameworks/tanstack-start/vite-inline.snippet.md +22 -0
  15. package/dist/frameworks/vite-react/postcss.config.ts.template +16 -0
  16. package/dist/frameworks/vite-react/vite-inline.snippet.md +20 -0
  17. package/dist/frameworks/vite-react/vite.config.ts.template +48 -0
  18. package/dist/index.js +2047 -0
  19. package/dist/index.js.map +1 -0
  20. package/package.json +63 -0
  21. package/registry/components/primitives/icon/icon.module.css +63 -0
  22. package/registry/components/primitives/icon/icon.tsx +53 -0
  23. package/registry/components/primitives/modal/modal-context.ts +19 -0
  24. package/registry/components/primitives/modal/modal.module.css +415 -0
  25. package/registry/components/primitives/modal/modal.tsx +550 -0
  26. package/registry/components/primitives/popover/popover-context.ts +28 -0
  27. package/registry/components/primitives/popover/popover.module.css +28 -0
  28. package/registry/components/primitives/popover/popover.tsx +465 -0
  29. package/registry/components/ui/accordion/accordion.module.css +208 -0
  30. package/registry/components/ui/accordion/accordion.tsx +327 -0
  31. package/registry/components/ui/avatar/avatar.module.css +32 -0
  32. package/registry/components/ui/avatar/avatar.tsx +37 -0
  33. package/registry/components/ui/badge/badge.module.css +103 -0
  34. package/registry/components/ui/badge/badge.tsx +29 -0
  35. package/registry/components/ui/button/button.module.css +216 -0
  36. package/registry/components/ui/button/button.tsx +133 -0
  37. package/registry/components/ui/chart/chart.module.css +168 -0
  38. package/registry/components/ui/chart/chart.tsx +1293 -0
  39. package/registry/components/ui/chart/types.ts +210 -0
  40. package/registry/components/ui/chart/utils.ts +59 -0
  41. package/registry/components/ui/copy-button/copy-button.module.css +21 -0
  42. package/registry/components/ui/copy-button/copy-button.tsx +123 -0
  43. package/registry/components/ui/countdown/countdown.module.css +5 -0
  44. package/registry/components/ui/countdown/countdown.tsx +152 -0
  45. package/registry/components/ui/data-table/checkbox-column.tsx +59 -0
  46. package/registry/components/ui/data-table/data-table.module.css +594 -0
  47. package/registry/components/ui/data-table/data-table.tsx +1109 -0
  48. package/registry/components/ui/data-table/filter-bar.tsx +33 -0
  49. package/registry/components/ui/data-table/pagination-bar.tsx +125 -0
  50. package/registry/components/ui/data-table/types.ts +594 -0
  51. package/registry/components/ui/data-table/useDataTable.tsx +573 -0
  52. package/registry/components/ui/data-table/utils.ts +445 -0
  53. package/registry/components/ui/empty-state/empty-state.module.css +26 -0
  54. package/registry/components/ui/empty-state/empty-state.tsx +32 -0
  55. package/registry/components/ui/input-fields/color-picker/color-palette.module.css +102 -0
  56. package/registry/components/ui/input-fields/color-picker/color-palette.tsx +92 -0
  57. package/registry/components/ui/input-fields/color-picker/color-picker.module.css +29 -0
  58. package/registry/components/ui/input-fields/color-picker/color-picker.tsx +168 -0
  59. package/registry/components/ui/input-fields/common/field-message/field-message.module.css +28 -0
  60. package/registry/components/ui/input-fields/common/field-message/field-message.tsx +39 -0
  61. package/registry/components/ui/input-fields/common/input-field-common.module.css +318 -0
  62. package/registry/components/ui/input-fields/common/input-field-native.module.css +267 -0
  63. package/registry/components/ui/input-fields/common/picker-popover-common.module.css +40 -0
  64. package/registry/components/ui/input-fields/common/select-button/select-button.module.css +9 -0
  65. package/registry/components/ui/input-fields/common/select-button/select-button.tsx +171 -0
  66. package/registry/components/ui/input-fields/common/types.ts +38 -0
  67. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +56 -0
  68. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +403 -0
  69. package/registry/components/ui/input-fields/date-picker/calendar.module.css +311 -0
  70. package/registry/components/ui/input-fields/date-picker/calendar.tsx +186 -0
  71. package/registry/components/ui/input-fields/date-picker/date-picker.module.css +57 -0
  72. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +945 -0
  73. package/registry/components/ui/input-fields/date-picker/types.ts +102 -0
  74. package/registry/components/ui/input-fields/date-picker/utils.ts +236 -0
  75. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.module.css +90 -0
  76. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +168 -0
  77. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.module.css +8 -0
  78. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +102 -0
  79. package/registry/components/ui/input-fields/emoji-picker/utils.ts +505 -0
  80. package/registry/components/ui/input-fields/field-group/field-group.module.css +86 -0
  81. package/registry/components/ui/input-fields/field-group/field-group.tsx +86 -0
  82. package/registry/components/ui/input-fields/input/input.tsx +319 -0
  83. package/registry/components/ui/input-fields/media-input/kind-icons.tsx +62 -0
  84. package/registry/components/ui/input-fields/media-input/media-input.module.css +294 -0
  85. package/registry/components/ui/input-fields/media-input/media-input.tsx +327 -0
  86. package/registry/components/ui/input-fields/media-input/media-preview.tsx +101 -0
  87. package/registry/components/ui/input-fields/media-input/types.ts +129 -0
  88. package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +302 -0
  89. package/registry/components/ui/input-fields/media-input/utils.ts +325 -0
  90. package/registry/components/ui/input-fields/otp-input/otp-input.module.css +24 -0
  91. package/registry/components/ui/input-fields/otp-input/otp-input.tsx +291 -0
  92. package/registry/components/ui/input-fields/select/select.tsx +118 -0
  93. package/registry/components/ui/input-fields/switch/switch.module.css +70 -0
  94. package/registry/components/ui/input-fields/switch/switch.tsx +59 -0
  95. package/registry/components/ui/input-fields/textarea/textarea.tsx +133 -0
  96. package/registry/components/ui/input-fields/time-picker/time-picker.module.css +57 -0
  97. package/registry/components/ui/input-fields/time-picker/time-picker.tsx +437 -0
  98. package/registry/components/ui/loader/loader.module.css +156 -0
  99. package/registry/components/ui/loader/loader.tsx +102 -0
  100. package/registry/components/ui/stepper/stepper.module.css +294 -0
  101. package/registry/components/ui/stepper/stepper.tsx +197 -0
  102. package/registry/components/ui/tabs/tabs.module.css +150 -0
  103. package/registry/components/ui/tabs/tabs.tsx +134 -0
  104. package/registry/components/ui/toast/toast.module.css +159 -0
  105. package/registry/components/ui/toast/toast.tsx +104 -0
  106. package/registry/components/ui/tooltip/tooltip.module.css +82 -0
  107. package/registry/components/ui/tooltip/tooltip.tsx +72 -0
  108. package/registry/global.d.ts +4 -0
  109. package/registry/hooks/useFocusOutside.tsx +33 -0
  110. package/registry/hooks/useResizeObserver.tsx +32 -0
  111. package/registry/lib/dom.ts +25 -0
  112. package/registry/lib/string.ts +23 -0
  113. package/registry/registry.json +719 -0
  114. package/registry/styles/app.css +240 -0
  115. package/registry/styles/components.css +78 -0
  116. package/registry/styles/fonts/geist-regular.woff2 +0 -0
  117. package/registry/styles/globals.css +7 -0
  118. package/registry/styles/media.css +25 -0
  119. package/registry/styles/utilities.css +90 -0
  120. package/registry/themes/colors/blue.css +37 -0
  121. package/registry/themes/colors/green.css +37 -0
  122. package/registry/themes/colors/rose.css +37 -0
  123. package/registry/themes/colors/slate.css +37 -0
  124. package/registry/themes/colors/stone.css +37 -0
  125. package/registry/themes/colors/violet.css +37 -0
  126. package/registry/themes/colors/zinc.css +37 -0
  127. package/registry/themes/radius/pill.css +8 -0
  128. package/registry/themes/radius/rounded.css +8 -0
  129. package/registry/themes/radius/sharp.css +8 -0
  130. package/registry/themes/spacing/compact.css +17 -0
  131. package/registry/themes/spacing/spacious.css +17 -0
  132. package/registry/tsconfig.json +25 -0
@@ -0,0 +1,327 @@
1
+ import {
2
+ useEffect,
3
+ useId,
4
+ useRef,
5
+ useState,
6
+ type CSSProperties,
7
+ type ReactNode,
8
+ } from "react";
9
+ import clsx from "clsx";
10
+ import { IconChevronDown } from "@tabler/icons-react";
11
+ import { useResizeObserver } from "../../../hooks/useResizeObserver";
12
+ import s from "./accordion.module.css";
13
+
14
+ export type AccordionVariant = "bordered" | "minimal";
15
+
16
+ export type AccordionMode = "single" | "multiple";
17
+
18
+ export type AccordionSize = "sm" | "md" | "lg";
19
+
20
+ export type AccordionItem = {
21
+ id: string;
22
+ title: ReactNode;
23
+ content: ReactNode;
24
+ disabled?: boolean;
25
+ defaultOpen?: boolean;
26
+ };
27
+
28
+ type AccordionBaseProps = {
29
+ className?: string;
30
+ style?: CSSProperties;
31
+ itemClassName?: string;
32
+ triggerClassName?: string;
33
+ contentClassName?: string;
34
+ variant?: AccordionVariant;
35
+ size?: AccordionSize;
36
+ icon?: ReactNode;
37
+ iconRotation?: number;
38
+ };
39
+
40
+ type AccordionSingleProps = {
41
+ title: ReactNode;
42
+ children: ReactNode;
43
+ open?: boolean;
44
+ defaultOpen?: boolean;
45
+ onToggle?: (open: boolean) => void;
46
+ disabled?: boolean;
47
+ id?: string;
48
+ items?: never;
49
+ mode?: never;
50
+ value?: never;
51
+ defaultValue?: never;
52
+ onValueChange?: never;
53
+ collapsible?: never;
54
+ };
55
+
56
+ type AccordionGroupProps = {
57
+ items: AccordionItem[];
58
+ mode?: AccordionMode;
59
+ value?: string | string[];
60
+ defaultValue?: string | string[];
61
+ onValueChange?: (openIds: string[]) => void;
62
+ collapsible?: boolean;
63
+ title?: never;
64
+ children?: never;
65
+ open?: never;
66
+ defaultOpen?: never;
67
+ onToggle?: never;
68
+ disabled?: never;
69
+ id?: never;
70
+ };
71
+
72
+ type VariantProps = AccordionSingleProps | AccordionGroupProps;
73
+ export type AccordionProps = AccordionBaseProps & VariantProps;
74
+
75
+ type ItemViewProps = {
76
+ uid: string;
77
+ item: AccordionItem;
78
+ isOpen: boolean;
79
+ onToggle: () => void;
80
+ size: AccordionSize;
81
+ variant: AccordionVariant;
82
+ icon?: ReactNode;
83
+ itemClassName?: string;
84
+ triggerClassName?: string;
85
+ contentClassName?: string;
86
+ };
87
+
88
+ const isGroupProps = (props: VariantProps): props is AccordionGroupProps =>
89
+ "items" in props;
90
+
91
+ const normalizeIds = (
92
+ value: string | string[] | undefined,
93
+ mode: AccordionMode,
94
+ ): string[] => {
95
+ if (value === undefined) return [];
96
+ const ids = Array.isArray(value) ? value : [value];
97
+ const deduped = Array.from(new Set(ids.filter(Boolean)));
98
+ return mode === "single" ? deduped.slice(0, 1) : deduped;
99
+ };
100
+
101
+ const toDomSafeId = (value: string): string => {
102
+ const safe = value
103
+ .trim()
104
+ .toLowerCase()
105
+ .replace(/[^a-z0-9_-]+/g, "-");
106
+ return safe.length > 0 ? safe : "item";
107
+ };
108
+
109
+ const AccordionItemView = ({
110
+ uid,
111
+ item,
112
+ isOpen,
113
+ onToggle,
114
+ size,
115
+ variant,
116
+ icon,
117
+ itemClassName,
118
+ triggerClassName,
119
+ contentClassName,
120
+ }: ItemViewProps) => {
121
+ const safeId = toDomSafeId(item.id);
122
+ const triggerId = `${uid}-trigger-${safeId}`;
123
+ const panelId = `${uid}-content-${safeId}`;
124
+
125
+ const contentRef = useRef<HTMLDivElement | null>(null);
126
+ const [panelHeight, setPanelHeight] = useState(0);
127
+
128
+ const updateHeight = () => {
129
+ const element = contentRef.current;
130
+ if (!element) return;
131
+ setPanelHeight(isOpen ? element.scrollHeight : 0);
132
+ };
133
+
134
+ useEffect(() => {
135
+ updateHeight();
136
+ // eslint-disable-next-line react-hooks/exhaustive-deps
137
+ }, [isOpen]);
138
+
139
+ useResizeObserver(contentRef, updateHeight, { enabled: isOpen });
140
+
141
+ return (
142
+ <section
143
+ className={clsx(
144
+ s.item,
145
+ s[`item_${variant}`],
146
+ isOpen && s.item_open,
147
+ item.disabled && s.item_disabled,
148
+ itemClassName,
149
+ )}
150
+ data-state={isOpen ? "open" : "closed"}
151
+ >
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"
171
+ >
172
+ {icon ?? (
173
+ <IconChevronDown
174
+ size={size === "sm" ? 16 : size === "lg" ? 20 : 18}
175
+ />
176
+ )}
177
+ </span>
178
+ </button>
179
+
180
+ <div
181
+ id={panelId}
182
+ role="region"
183
+ aria-labelledby={triggerId}
184
+ aria-hidden={!isOpen}
185
+ className={clsx(s.panel, isOpen && s.panel_open)}
186
+ style={{ height: `${panelHeight}px` }}
187
+ >
188
+ <div
189
+ ref={contentRef}
190
+ className={clsx(
191
+ s.panel_inner,
192
+ s[`panel_inner_${size}`],
193
+ s[`panel_inner_${variant}`],
194
+ isOpen && s.panel_inner_open,
195
+ contentClassName,
196
+ )}
197
+ >
198
+ {item.content}
199
+ </div>
200
+ </div>
201
+ </section>
202
+ );
203
+ };
204
+
205
+ export const Accordion = ({
206
+ className,
207
+ style,
208
+ itemClassName,
209
+ triggerClassName,
210
+ contentClassName,
211
+ variant = "bordered",
212
+ size = "md",
213
+ icon,
214
+ iconRotation = 180,
215
+ ...variantProps
216
+ }: AccordionProps) => {
217
+ const uid = useId();
218
+ const rootStyle = {
219
+ ...style,
220
+ "--accordion-icon-rotation": `${iconRotation}deg`,
221
+ } as CSSProperties;
222
+
223
+ const isGroup = isGroupProps(variantProps);
224
+ const mode: AccordionMode = isGroup
225
+ ? (variantProps.mode ?? "single")
226
+ : "single";
227
+
228
+ const singleItemId = isGroup ? "" : (variantProps.id ?? "item-1");
229
+ const items: AccordionItem[] = isGroup
230
+ ? variantProps.items
231
+ : [
232
+ {
233
+ id: singleItemId,
234
+ title: variantProps.title,
235
+ content: variantProps.children,
236
+ disabled: variantProps.disabled,
237
+ defaultOpen: variantProps.defaultOpen,
238
+ },
239
+ ];
240
+
241
+ const controlledOpenIds = isGroup
242
+ ? variantProps.value !== undefined
243
+ ? normalizeIds(variantProps.value, mode)
244
+ : undefined
245
+ : variantProps.open !== undefined
246
+ ? variantProps.open
247
+ ? [singleItemId]
248
+ : []
249
+ : undefined;
250
+
251
+ const initialOpenIds = isGroup
252
+ ? variantProps.defaultValue !== undefined
253
+ ? normalizeIds(variantProps.defaultValue, mode)
254
+ : normalizeIds(
255
+ items
256
+ .filter((item) => item.defaultOpen && !item.disabled)
257
+ .map((item) => item.id),
258
+ mode,
259
+ )
260
+ : variantProps.defaultOpen && !variantProps.disabled
261
+ ? [singleItemId]
262
+ : [];
263
+
264
+ const [internalOpenIds, setInternalOpenIds] = useState<string[]>(
265
+ () => initialOpenIds,
266
+ );
267
+
268
+ const isControlled = controlledOpenIds !== undefined;
269
+ const openIds = normalizeIds(
270
+ isControlled ? controlledOpenIds : internalOpenIds,
271
+ mode,
272
+ );
273
+
274
+ const emitChange = (nextOpenIds: string[]) => {
275
+ const normalized = normalizeIds(nextOpenIds, mode);
276
+ if (!isControlled) {
277
+ setInternalOpenIds(normalized);
278
+ }
279
+
280
+ if (isGroup) {
281
+ variantProps.onValueChange?.(normalized);
282
+ return;
283
+ }
284
+
285
+ variantProps.onToggle?.(normalized.includes(singleItemId));
286
+ };
287
+
288
+ const toggleItem = (item: AccordionItem) => {
289
+ if (item.disabled) return;
290
+
291
+ const isItemOpen = openIds.includes(item.id);
292
+ let nextOpenIds: string[];
293
+
294
+ if (!isGroup) {
295
+ nextOpenIds = isItemOpen ? [] : [item.id];
296
+ } else if (mode === "multiple") {
297
+ nextOpenIds = isItemOpen
298
+ ? openIds.filter((openId) => openId !== item.id)
299
+ : [...openIds, item.id];
300
+ } else {
301
+ const collapsible = variantProps.collapsible ?? true;
302
+ nextOpenIds = isItemOpen ? (collapsible ? [] : [item.id]) : [item.id];
303
+ }
304
+
305
+ emitChange(normalizeIds(nextOpenIds, mode));
306
+ };
307
+
308
+ return (
309
+ <div className={clsx(s.root, className)} style={rootStyle}>
310
+ {items.map((item) => (
311
+ <AccordionItemView
312
+ key={item.id}
313
+ uid={uid}
314
+ item={item}
315
+ isOpen={openIds.includes(item.id)}
316
+ onToggle={() => toggleItem(item)}
317
+ size={size}
318
+ variant={variant}
319
+ icon={icon}
320
+ itemClassName={itemClassName}
321
+ triggerClassName={triggerClassName}
322
+ contentClassName={contentClassName}
323
+ />
324
+ ))}
325
+ </div>
326
+ );
327
+ };
@@ -0,0 +1,32 @@
1
+ .base {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ border-radius: var(--radius-full);
6
+ font-weight: var(--font-weight-semibold);
7
+ line-height: 1;
8
+ letter-spacing: var(--letter-spacing-tight);
9
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
10
+ box-shadow: var(--shadow-sm);
11
+ text-transform: uppercase;
12
+ flex-shrink: 0;
13
+ user-select: none;
14
+ }
15
+
16
+ .sm {
17
+ width: var(--spacing-10);
18
+ height: var(--spacing-10);
19
+ font-size: var(--font-size-xs);
20
+ }
21
+
22
+ .md {
23
+ width: var(--spacing-12);
24
+ height: var(--spacing-12);
25
+ font-size: var(--font-size-sm);
26
+ }
27
+
28
+ .lg {
29
+ width: 3.25rem;
30
+ height: 3.25rem;
31
+ font-size: var(--font-size-lg);
32
+ }
@@ -0,0 +1,37 @@
1
+ import clsx from "clsx";
2
+ import s from "./avatar.module.css";
3
+ import { getInitials, hashName } from "../../../lib/string";
4
+
5
+ type AvatarSize = "sm" | "md" | "lg";
6
+
7
+ type AvatarProps = {
8
+ name: string;
9
+ size?: AvatarSize;
10
+ className?: string;
11
+ };
12
+
13
+ const COLORS = [
14
+ { bg: "oklch(0.92 0.05 280)", fg: "oklch(0.4 0.15 280)" },
15
+ { bg: "oklch(0.92 0.05 200)", fg: "oklch(0.4 0.15 200)" },
16
+ { bg: "oklch(0.92 0.05 155)", fg: "oklch(0.4 0.15 155)" },
17
+ { bg: "oklch(0.92 0.06 85)", fg: "oklch(0.4 0.15 85)" },
18
+ { bg: "oklch(0.92 0.05 25)", fg: "oklch(0.4 0.15 25)" },
19
+ { bg: "oklch(0.92 0.05 330)", fg: "oklch(0.4 0.15 330)" },
20
+ ];
21
+
22
+
23
+
24
+ export function Avatar({ name, size = "md", className }: AvatarProps) {
25
+ const color = COLORS[hashName(name) % COLORS.length];
26
+ const initials = getInitials(name) || "?";
27
+ return (
28
+ <div
29
+ className={clsx(s.base, s[size], className)}
30
+ style={{ backgroundColor: color.bg, color: color.fg }}
31
+ title={name}
32
+ aria-label={`Avatar for ${name}`}
33
+ >
34
+ {initials}
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,103 @@
1
+ .base {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ font-size: var(--font-size-sm);
6
+ font-weight: var(--font-weight-medium);
7
+ line-height: 1;
8
+ text-align: center;
9
+ letter-spacing: var(--letter-spacing-normal);
10
+ gap: var(--spacing-2);
11
+ white-space: nowrap;
12
+ vertical-align: middle;
13
+ border-radius: var(--radius-md);
14
+ min-height: var(--spacing-7);
15
+ padding: var(--spacing-1) var(--spacing-5);
16
+ border: 1px solid var(--color-border);
17
+ box-shadow:
18
+ inset 0 1px 0 color-mix(in oklch, var(--color-background) 70%, transparent),
19
+ var(--shadow-sm);
20
+ }
21
+
22
+ .sm {
23
+ font-size: var(--font-size-xs);
24
+ min-height: var(--spacing-6);
25
+ padding: var(--spacing-1) var(--spacing-4);
26
+ }
27
+
28
+ .default {
29
+ background-color: var(--color-muted);
30
+ color: var(--color-foreground);
31
+ border-color: var(--color-border);
32
+ }
33
+
34
+ .primary {
35
+ background-color: color-mix(
36
+ in oklch,
37
+ var(--color-primary) 15%,
38
+ var(--color-background)
39
+ );
40
+ color: var(--color-primary);
41
+ border-color: color-mix(
42
+ in oklch,
43
+ var(--color-primary) 30%,
44
+ var(--color-border)
45
+ );
46
+ }
47
+
48
+ .success {
49
+ background-color: var(--color-success-subtle);
50
+ color: var(--color-success-foreground);
51
+ border-color: color-mix(
52
+ in oklch,
53
+ var(--color-success-foreground) 24%,
54
+ var(--color-border)
55
+ );
56
+ }
57
+
58
+ .warning {
59
+ background-color: var(--color-warning-subtle);
60
+ color: var(--color-warning-foreground);
61
+ border-color: color-mix(
62
+ in oklch,
63
+ var(--color-warning-foreground) 24%,
64
+ var(--color-border)
65
+ );
66
+ }
67
+
68
+ .error {
69
+ background-color: var(--color-error-subtle);
70
+ color: var(--color-error-foreground);
71
+ border-color: color-mix(
72
+ in oklch,
73
+ var(--color-error-foreground) 24%,
74
+ var(--color-border)
75
+ );
76
+ }
77
+
78
+ .info {
79
+ background-color: var(--color-info-subtle);
80
+ color: var(--color-info-foreground);
81
+ border-color: color-mix(
82
+ in oklch,
83
+ var(--color-info-foreground) 24%,
84
+ var(--color-border)
85
+ );
86
+ }
87
+
88
+ /* Count badge (small circle) */
89
+ .count {
90
+ min-width: var(--spacing-7);
91
+ min-height: var(--spacing-7);
92
+ padding: var(--spacing-1) var(--spacing-3);
93
+ border-radius: var(--radius-md);
94
+ background-color: var(--color-primary);
95
+ color: var(--color-primary-foreground);
96
+ font-size: var(--font-size-xs);
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ line-height: 1;
101
+ letter-spacing: var(--letter-spacing-normal);
102
+ box-shadow: var(--shadow-sm);
103
+ }
@@ -0,0 +1,29 @@
1
+ import clsx from "clsx";
2
+ import s from "./badge.module.css";
3
+
4
+ type BadgeVariant = "default" | "primary" | "success" | "warning" | "error" | "info";
5
+ type BadgeSize = "default" | "sm";
6
+
7
+ type BadgeProps = {
8
+ variant?: BadgeVariant;
9
+ size?: BadgeSize;
10
+ className?: string;
11
+ children: React.ReactNode;
12
+ };
13
+
14
+ export function Badge({ variant = "default", size = "default", className, children }: BadgeProps) {
15
+ return (
16
+ <span className={clsx(s.base, s[variant], size === "sm" && s.sm, className)}>
17
+ {children}
18
+ </span>
19
+ );
20
+ }
21
+
22
+ export function CountBadge({ count, className }: { count: number; className?: string }) {
23
+ if (count <= 0) return null;
24
+ return (
25
+ <span className={clsx(s.count, className)} aria-label={`${count} notifications`}>
26
+ {count > 99 ? "99+" : count}
27
+ </span>
28
+ );
29
+ }