@rovula/ui 0.1.21 → 0.1.23
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/bundle.css +204 -26
- package/dist/cjs/bundle.js +675 -675
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Badge/Badge.d.ts +40 -0
- package/dist/cjs/types/components/Badge/Badge.stories.d.ts +295 -0
- package/dist/cjs/types/components/Badge/Badge.styles.d.ts +7 -0
- package/dist/cjs/types/components/Badge/index.d.ts +2 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +4 -8
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.stories.d.ts +16 -0
- package/dist/cjs/types/index.d.ts +3 -1
- package/dist/cjs/types/patterns/menu/Menu.d.ts +72 -0
- package/dist/cjs/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +18 -10
- package/dist/cjs/types/utils/mergeRefs.d.ts +20 -0
- package/dist/components/ActionButton/ActionButton.styles.js +9 -1
- package/dist/components/Avatar/Avatar.styles.js +2 -2
- package/dist/components/Badge/Badge.js +36 -0
- package/dist/components/Badge/Badge.stories.js +51 -0
- package/dist/components/Badge/Badge.styles.js +62 -0
- package/dist/components/Badge/index.js +2 -0
- package/dist/components/Dropdown/Dropdown.js +54 -163
- package/dist/components/Dropdown/Dropdown.stories.js +29 -0
- package/dist/components/DropdownMenu/DropdownMenu.js +24 -11
- package/dist/components/DropdownMenu/DropdownMenu.stories.js +54 -10
- package/dist/components/TextInput/TextInput.js +9 -4
- package/dist/esm/bundle.css +204 -26
- package/dist/esm/bundle.js +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Badge/Badge.d.ts +40 -0
- package/dist/esm/types/components/Badge/Badge.stories.d.ts +295 -0
- package/dist/esm/types/components/Badge/Badge.styles.d.ts +7 -0
- package/dist/esm/types/components/Badge/index.d.ts +2 -0
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +4 -8
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.stories.d.ts +16 -0
- package/dist/esm/types/index.d.ts +3 -1
- package/dist/esm/types/patterns/menu/Menu.d.ts +72 -0
- package/dist/esm/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +18 -10
- package/dist/esm/types/utils/mergeRefs.d.ts +20 -0
- package/dist/index.d.ts +118 -73
- package/dist/index.js +2 -1
- package/dist/patterns/menu/Menu.js +95 -0
- package/dist/patterns/menu/Menu.stories.js +611 -0
- package/dist/src/theme/global.css +393 -43
- package/dist/utils/mergeRefs.js +42 -0
- package/package.json +1 -1
- package/src/components/ActionButton/ActionButton.styles.ts +9 -1
- package/src/components/Avatar/Avatar.styles.ts +2 -2
- package/src/components/Badge/Badge.stories.tsx +128 -0
- package/src/components/Badge/Badge.styles.ts +70 -0
- package/src/components/Badge/Badge.tsx +103 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +170 -1
- package/src/components/Dropdown/Dropdown.tsx +186 -276
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +1050 -113
- package/src/components/DropdownMenu/DropdownMenu.tsx +117 -56
- package/src/components/TextInput/TextInput.tsx +42 -32
- package/src/index.ts +3 -1
- package/src/patterns/menu/Menu.stories.tsx +1100 -0
- package/src/patterns/menu/Menu.tsx +286 -0
- package/src/theme/presets/colors.js +14 -0
- package/src/theme/themes/variable-mapping.css +30 -0
- package/src/theme/themes/variable.css +37 -6
- package/src/theme/themes/xspector/baseline.css +0 -1
- package/src/theme/tokens/baseline.css +2 -1
- package/src/theme/tokens/components/badge.css +54 -0
- package/src/theme/tokens/components/dropdown-menu.css +15 -4
- package/src/utils/mergeRefs.ts +46 -0
- package/dist/cjs/types/components/Menu/Menu.d.ts +0 -65
- package/dist/cjs/types/components/Menu/helpers.d.ts +0 -19
- package/dist/cjs/types/components/Menu/index.d.ts +0 -4
- package/dist/components/Menu/Menu.js +0 -64
- package/dist/components/Menu/Menu.stories.js +0 -406
- package/dist/components/Menu/helpers.js +0 -28
- package/dist/components/Menu/index.js +0 -3
- package/dist/esm/types/components/Menu/Menu.d.ts +0 -65
- package/dist/esm/types/components/Menu/helpers.d.ts +0 -19
- package/dist/esm/types/components/Menu/index.d.ts +0 -4
- package/src/components/Menu/Menu.stories.tsx +0 -586
- package/src/components/Menu/Menu.tsx +0 -235
- package/src/components/Menu/helpers.ts +0 -45
- package/src/components/Menu/index.ts +0 -7
- package/src/theme/themes/xspector/components/dropdown-menu.css +0 -28
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Merges multiple refs into a single callback ref.
|
|
4
|
+
* NOTE: This creates a new function on every call — do NOT use inline in render.
|
|
5
|
+
* Use `useStableMergedRef` instead when you need a stable ref identity.
|
|
6
|
+
*/
|
|
7
|
+
export function mergeRefs(...refs) {
|
|
8
|
+
return (node) => {
|
|
9
|
+
refs.forEach((ref) => {
|
|
10
|
+
if (!ref)
|
|
11
|
+
return;
|
|
12
|
+
if (typeof ref === "function") {
|
|
13
|
+
ref(node);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
ref.current = node;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns a **stable** callback ref (never changes identity) that forwards the
|
|
23
|
+
* node to all given refs. Safe to use inline in JSX — will not cause
|
|
24
|
+
* detach/re-attach loops in libraries like Headless UI that watch refs.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const MyInput = forwardRef((props, ref) => {
|
|
28
|
+
* const internalRef = useRef(null);
|
|
29
|
+
* const stableRef = useStableMergedRef(ref, internalRef);
|
|
30
|
+
* return <input ref={stableRef} />;
|
|
31
|
+
* });
|
|
32
|
+
*/
|
|
33
|
+
export function useStableMergedRef(...refs) {
|
|
34
|
+
// Store the latest merge logic in a ref so the callback never goes stale,
|
|
35
|
+
// while the callback itself keeps a stable identity (empty useCallback deps).
|
|
36
|
+
const latestImpl = useRef();
|
|
37
|
+
latestImpl.current = mergeRefs(...refs);
|
|
38
|
+
return useCallback((node) => {
|
|
39
|
+
var _a;
|
|
40
|
+
(_a = latestImpl.current) === null || _a === void 0 ? void 0 : _a.call(latestImpl, node);
|
|
41
|
+
}, []); // ← empty deps = identity never changes = no detach/re-attach loop
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { cva } from "class-variance-authority";
|
|
2
2
|
|
|
3
3
|
export const actionButtonVariants = cva(
|
|
4
|
-
[
|
|
4
|
+
[
|
|
5
|
+
"box-border flex items-center justify-center outline-none focus-visible:outline-none",
|
|
6
|
+
],
|
|
5
7
|
{
|
|
6
8
|
variants: {
|
|
7
9
|
variant: {
|
|
@@ -9,17 +11,20 @@ export const actionButtonVariants = cva(
|
|
|
9
11
|
"bg-action-button-solid-default border-action-button-solid-default text-action-button-solid-default fill-action-button-solid-default",
|
|
10
12
|
"hover:bg-action-button-solid-hover hover:border-action-button-solid-hover hover:text-action-button-solid-hover hover:fill-action-button-solid-hover",
|
|
11
13
|
"active:bg-action-button-solid-pressed active:border-action-button-solid-pressed active:text-action-button-solid-pressed active:fill-action-button-solid-pressed",
|
|
14
|
+
"focus-visible:ring-none focus-visible:ring-offset-2 focus-visible:ring-[var(--action-button-solid-default-border)]",
|
|
12
15
|
],
|
|
13
16
|
outline: [
|
|
14
17
|
"ring-1 ring-inset",
|
|
15
18
|
"bg-action-button-outline-default text-action-button-outline-default fill-action-button-outline-default ring-[var(--action-button-outline-default-border)]",
|
|
16
19
|
"hover:bg-action-button-outline-hover hover:text-action-button-outline-hover hover:fill-action-button-outline-hover hover:ring-[var(--action-button-outline-hover-border)]",
|
|
17
20
|
"active:bg-action-button-outline-pressed active:text-action-button-outline-pressed active:fill-action-button-outline-pressed active:ring-[var(--action-button-outline-pressed-border)]",
|
|
21
|
+
"focus-visible:ring-none focus-visible:ring-[var(--action-button-outline-default-border)]",
|
|
18
22
|
],
|
|
19
23
|
icon: [
|
|
20
24
|
"bg-action-button-icon-default border-action-button-icon-default text-action-button-icon-default fill-action-button-icon-default",
|
|
21
25
|
"hover:bg-action-button-icon-hover hover:border-action-button-icon-hover hover:text-action-button-icon-hover hover:fill-action-button-icon-hover",
|
|
22
26
|
"active:bg-action-button-icon-pressed active:border-action-button-icon-pressed active:text-action-button-icon-pressed active:fill-action-button-icon-pressed",
|
|
27
|
+
"focus-visible:ring-none focus-visible:ring-offset-2 focus-visible:ring-[var(--action-button-icon-default-border)]",
|
|
23
28
|
],
|
|
24
29
|
},
|
|
25
30
|
size: {
|
|
@@ -48,6 +53,7 @@ export const actionButtonVariants = cva(
|
|
|
48
53
|
"bg-action-button-solid-active border-action-button-solid-active text-action-button-solid-active fill-action-button-solid-active",
|
|
49
54
|
"hover:bg-action-button-solid-active-hover hover:border-action-button-solid-active-hover hover:text-action-button-solid-active-hover hover:fill-action-button-solid-active-hover",
|
|
50
55
|
"active:bg-action-button-solid-active-pressed active:border-action-button-solid-active-pressed active:text-action-button-solid-active-pressed active:fill-action-button-solid-active-pressed",
|
|
56
|
+
"focus-visible:ring-[var(--action-button-solid-active-border)]",
|
|
51
57
|
],
|
|
52
58
|
},
|
|
53
59
|
{
|
|
@@ -57,6 +63,7 @@ export const actionButtonVariants = cva(
|
|
|
57
63
|
"bg-action-button-outline-active text-action-button-outline-active fill-action-button-outline-active ring-[var(--action-button-outline-active-border)]",
|
|
58
64
|
"hover:bg-action-button-outline-active-hover hover:text-action-button-outline-active-hover hover:fill-action-button-outline-active-hover hover:ring-[var(--action-button-outline-active-hover-border)]",
|
|
59
65
|
"active:bg-action-button-outline-active-pressed active:text-action-button-outline-active-pressed active:fill-action-button-outline-active-pressed active:ring-[var(--action-button-outline-active-pressed-border)]",
|
|
66
|
+
"focus-visible:ring-[var(--action-button-outline-active-border)]",
|
|
60
67
|
],
|
|
61
68
|
},
|
|
62
69
|
{
|
|
@@ -66,6 +73,7 @@ export const actionButtonVariants = cva(
|
|
|
66
73
|
"bg-action-button-icon-active border-action-button-icon-active text-action-button-icon-active fill-action-button-icon-active",
|
|
67
74
|
"hover:bg-action-button-icon-active-hover hover:border-action-button-icon-active-hover hover:text-action-button-icon-active-hover hover:fill-action-button-icon-active-hover",
|
|
68
75
|
"active:bg-action-button-icon-active-pressed active:border-action-button-icon-active-pressed active:text-action-button-icon-active-pressed active:fill-action-button-icon-active-pressed",
|
|
76
|
+
"focus-visible:ring-[var(--action-button-icon-active-border)]",
|
|
69
77
|
],
|
|
70
78
|
},
|
|
71
79
|
{
|
|
@@ -2,12 +2,12 @@ import { cva } from "class-variance-authority";
|
|
|
2
2
|
|
|
3
3
|
export const avatarVariants = cva(
|
|
4
4
|
[
|
|
5
|
-
"flex items-center justify-center bg-grey2-
|
|
5
|
+
"flex items-center justify-center bg-grey2-900 text-text-white typography-subtitle6 truncate",
|
|
6
6
|
],
|
|
7
7
|
{
|
|
8
8
|
variants: {
|
|
9
9
|
size: {
|
|
10
|
-
xxs: "w-[24px] h-[24px] typography-
|
|
10
|
+
xxs: "w-[24px] h-[24px] typography-small3",
|
|
11
11
|
xs: "w-[32px] h-[32px]",
|
|
12
12
|
sm: "w-[40px] h-[40px]",
|
|
13
13
|
md: "w-[48px] h-[48px]",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Badge, SeverityBadge } from "./Badge";
|
|
4
|
+
import type { BadgeColor, SeverityLevel } from "./Badge";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Badge",
|
|
8
|
+
component: Badge,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: "fullscreen",
|
|
12
|
+
},
|
|
13
|
+
decorators: [
|
|
14
|
+
(Story) => (
|
|
15
|
+
<div className="p-8 bg-bg-bg1">
|
|
16
|
+
<Story />
|
|
17
|
+
</div>
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
} satisfies Meta<typeof Badge>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Default — single badge with controls
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
label: "To do",
|
|
32
|
+
color: "default",
|
|
33
|
+
clickable: false,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Status Badges — all colors × clickable / static
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const COLORS: BadgeColor[] = ["default", "warning", "info", "error", "success"];
|
|
42
|
+
|
|
43
|
+
const COLOR_LABELS: Record<BadgeColor, string> = {
|
|
44
|
+
default: "To do",
|
|
45
|
+
warning: "In Progress",
|
|
46
|
+
info: "Ready to review",
|
|
47
|
+
error: "In review",
|
|
48
|
+
success: "Completed",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const StatusBadges: StoryObj = {
|
|
52
|
+
render: () => (
|
|
53
|
+
<div className="flex flex-col gap-6">
|
|
54
|
+
{/* Clickable (with border + chevron) */}
|
|
55
|
+
<div>
|
|
56
|
+
<p className="typography-small1 text-text-g-contrast-low mb-3">
|
|
57
|
+
Clickable
|
|
58
|
+
</p>
|
|
59
|
+
<div className="flex flex-wrap gap-3">
|
|
60
|
+
{COLORS.map((color) => (
|
|
61
|
+
<Badge
|
|
62
|
+
key={color}
|
|
63
|
+
color={color}
|
|
64
|
+
label={COLOR_LABELS[color]}
|
|
65
|
+
clickable
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Static (no border, no chevron) */}
|
|
72
|
+
<div>
|
|
73
|
+
<p className="typography-small1 text-text-g-contrast-low mb-3">
|
|
74
|
+
Static
|
|
75
|
+
</p>
|
|
76
|
+
<div className="flex flex-wrap gap-3">
|
|
77
|
+
{COLORS.map((color) => (
|
|
78
|
+
<Badge key={color} color={color} label={COLOR_LABELS[color]} />
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Percent only */}
|
|
84
|
+
<div>
|
|
85
|
+
<p className="typography-small1 text-text-g-contrast-low mb-3">
|
|
86
|
+
With percentage
|
|
87
|
+
</p>
|
|
88
|
+
<div className="flex flex-wrap gap-3">
|
|
89
|
+
{COLORS.map((color, i) => (
|
|
90
|
+
<Badge
|
|
91
|
+
key={color}
|
|
92
|
+
color={color}
|
|
93
|
+
label={COLOR_LABELS[color]}
|
|
94
|
+
percent={i === 0 ? 0 : i === 4 ? 100 : 50}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Severity Badges
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
const SEVERITIES: SeverityLevel[] = [
|
|
108
|
+
"highest",
|
|
109
|
+
"high",
|
|
110
|
+
"medium",
|
|
111
|
+
"low",
|
|
112
|
+
"lowest",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export const SeverityBadges: StoryObj = {
|
|
116
|
+
render: () => (
|
|
117
|
+
<div className="flex flex-col gap-3">
|
|
118
|
+
<p className="typography-small1 text-text-g-contrast-low mb-1">
|
|
119
|
+
Severity levels
|
|
120
|
+
</p>
|
|
121
|
+
<div className="flex flex-wrap gap-3">
|
|
122
|
+
{SEVERITIES.map((severity) => (
|
|
123
|
+
<SeverityBadge key={severity} severity={severity} />
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
),
|
|
128
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
export const badgeVariants = cva(
|
|
4
|
+
[
|
|
5
|
+
"inline-flex items-center justify-center rounded-lg px-3 py-1",
|
|
6
|
+
"typography-body3",
|
|
7
|
+
],
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
color: {
|
|
11
|
+
default: [
|
|
12
|
+
"bg-[var(--badge-default-bg)]",
|
|
13
|
+
"text-[var(--badge-default-text)]",
|
|
14
|
+
],
|
|
15
|
+
success: [
|
|
16
|
+
"bg-[var(--badge-success-bg)]",
|
|
17
|
+
"text-[var(--badge-success-text)]",
|
|
18
|
+
],
|
|
19
|
+
warning: [
|
|
20
|
+
"bg-[var(--badge-warning-bg)]",
|
|
21
|
+
"text-[var(--badge-warning-text)]",
|
|
22
|
+
],
|
|
23
|
+
info: [
|
|
24
|
+
"bg-[var(--badge-info-bg)]",
|
|
25
|
+
"text-[var(--badge-info-text)]",
|
|
26
|
+
],
|
|
27
|
+
error: [
|
|
28
|
+
"bg-[var(--badge-error-bg)]",
|
|
29
|
+
"text-[var(--badge-error-text)]",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
clickable: {
|
|
33
|
+
true: "border border-solid",
|
|
34
|
+
false: "",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
compoundVariants: [
|
|
38
|
+
{ color: "default", clickable: true, className: "border-[var(--badge-default-border)]" },
|
|
39
|
+
{ color: "success", clickable: true, className: "border-[var(--badge-success-border)]" },
|
|
40
|
+
{ color: "warning", clickable: true, className: "border-[var(--badge-warning-border)]" },
|
|
41
|
+
{ color: "info", clickable: true, className: "border-[var(--badge-info-border)]" },
|
|
42
|
+
{ color: "error", clickable: true, className: "border-[var(--badge-error-border)]" },
|
|
43
|
+
],
|
|
44
|
+
defaultVariants: {
|
|
45
|
+
color: "default",
|
|
46
|
+
clickable: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const severityBadgeVariants = cva(
|
|
52
|
+
[
|
|
53
|
+
"inline-flex items-center justify-center rounded px-1 py-0.5",
|
|
54
|
+
"typography-small6 text-[var(--badge-severity-text)]",
|
|
55
|
+
],
|
|
56
|
+
{
|
|
57
|
+
variants: {
|
|
58
|
+
severity: {
|
|
59
|
+
highest: "bg-[var(--badge-severity-highest-bg)]",
|
|
60
|
+
high: "bg-[var(--badge-severity-high-bg)]",
|
|
61
|
+
medium: "bg-[var(--badge-severity-medium-bg)]",
|
|
62
|
+
low: "bg-[var(--badge-severity-low-bg)]",
|
|
63
|
+
lowest: "bg-[var(--badge-severity-lowest-bg)]",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
defaultVariants: {
|
|
67
|
+
severity: "medium",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
5
|
+
import { cn } from "@/utils/cn";
|
|
6
|
+
import { badgeVariants, severityBadgeVariants } from "./Badge.styles";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Status Badge
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export type BadgeColor = "default" | "success" | "warning" | "info" | "error";
|
|
13
|
+
|
|
14
|
+
export type BadgeProps = {
|
|
15
|
+
/** Badge text label */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Color variant */
|
|
18
|
+
color?: BadgeColor;
|
|
19
|
+
/**
|
|
20
|
+
* Show a dropdown chevron — use when the badge acts as a clickable trigger.
|
|
21
|
+
* Automatically adds a border to indicate interactivity.
|
|
22
|
+
*/
|
|
23
|
+
clickable?: boolean;
|
|
24
|
+
/** Optional percentage value shown below the label */
|
|
25
|
+
percent?: number;
|
|
26
|
+
className?: string;
|
|
27
|
+
} & React.HTMLAttributes<HTMLSpanElement>;
|
|
28
|
+
|
|
29
|
+
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
30
|
+
(
|
|
31
|
+
{
|
|
32
|
+
label,
|
|
33
|
+
color = "default",
|
|
34
|
+
clickable = false,
|
|
35
|
+
percent,
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
},
|
|
39
|
+
ref,
|
|
40
|
+
) => {
|
|
41
|
+
const hasPercent = percent !== undefined;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<span
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn(
|
|
47
|
+
badgeVariants({ color, clickable }),
|
|
48
|
+
hasPercent && "flex-col gap-0.5",
|
|
49
|
+
clickable && "cursor-pointer gap-1",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
<span className="flex items-center gap-1">
|
|
55
|
+
{label}
|
|
56
|
+
{clickable && (
|
|
57
|
+
<ChevronDownIcon className="size-4 shrink-0" aria-hidden />
|
|
58
|
+
)}
|
|
59
|
+
</span>
|
|
60
|
+
{hasPercent && (
|
|
61
|
+
<span className="tabular-nums">
|
|
62
|
+
{percent}%
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</span>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
Badge.displayName = "Badge";
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Severity Badge
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export type SeverityLevel = "highest" | "high" | "medium" | "low" | "lowest";
|
|
76
|
+
|
|
77
|
+
export type SeverityBadgeProps = {
|
|
78
|
+
severity: SeverityLevel;
|
|
79
|
+
className?: string;
|
|
80
|
+
} & React.HTMLAttributes<HTMLSpanElement>;
|
|
81
|
+
|
|
82
|
+
const SEVERITY_LABELS: Record<SeverityLevel, string> = {
|
|
83
|
+
highest: "Highest",
|
|
84
|
+
high: "High",
|
|
85
|
+
medium: "Medium",
|
|
86
|
+
low: "Low",
|
|
87
|
+
lowest: "Lowest",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const SeverityBadge = React.forwardRef<HTMLSpanElement, SeverityBadgeProps>(
|
|
91
|
+
({ severity, className, ...props }, ref) => (
|
|
92
|
+
<span
|
|
93
|
+
ref={ref}
|
|
94
|
+
className={cn(severityBadgeVariants({ severity }), className)}
|
|
95
|
+
{...props}
|
|
96
|
+
>
|
|
97
|
+
{SEVERITY_LABELS[severity]}
|
|
98
|
+
</span>
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
SeverityBadge.displayName = "SeverityBadge";
|
|
102
|
+
|
|
103
|
+
export { Badge, SeverityBadge };
|
|
@@ -3,8 +3,18 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|
|
3
3
|
import Dropdown, { Options } from "./Dropdown";
|
|
4
4
|
import Button from "../Button/Button";
|
|
5
5
|
import { cn } from "@/utils/cn";
|
|
6
|
-
import { MenuItemType } from "../Menu/Menu";
|
|
7
6
|
import Icon from "../Icon/Icon";
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogBody,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogTrigger,
|
|
16
|
+
DialogClose,
|
|
17
|
+
} from "../Dialog/Dialog";
|
|
8
18
|
|
|
9
19
|
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
|
10
20
|
const meta = {
|
|
@@ -198,3 +208,162 @@ export const WithIcons: StoryObj<typeof Dropdown> = {
|
|
|
198
208
|
);
|
|
199
209
|
},
|
|
200
210
|
};
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Dropdown inside Dialog — showcases the fix for overflow:hidden bug
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
const dialogOptions: Options[] = [
|
|
217
|
+
{ value: "design", label: "Design" },
|
|
218
|
+
{ value: "engineering", label: "Engineering" },
|
|
219
|
+
{ value: "product", label: "Product" },
|
|
220
|
+
{ value: "marketing", label: "Marketing" },
|
|
221
|
+
{ value: "data", label: "Data & Analytics" },
|
|
222
|
+
{ value: "ops", label: "Operations" },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const filterableOptions: Options[] = new Array(20).fill("").map((_, i) => ({
|
|
226
|
+
value: `member-${i + 1}`,
|
|
227
|
+
label: `Team Member ${i + 1}`,
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
export const InsideDialog: StoryObj<typeof Dropdown> = {
|
|
231
|
+
name: "Inside Dialog",
|
|
232
|
+
render: () => {
|
|
233
|
+
const [department, setDepartment] = useState<Options | undefined>();
|
|
234
|
+
const [member, setMember] = useState<Options | undefined>();
|
|
235
|
+
const [role, setRole] = useState<Options | undefined>();
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="flex gap-4 flex-wrap">
|
|
239
|
+
{/* Demo 1: Single dropdown in dialog */}
|
|
240
|
+
<div>
|
|
241
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
242
|
+
Single Dropdown
|
|
243
|
+
</p>
|
|
244
|
+
<Dialog>
|
|
245
|
+
<DialogTrigger asChild>
|
|
246
|
+
<Button variant="outline">Open Dialog</Button>
|
|
247
|
+
</DialogTrigger>
|
|
248
|
+
<DialogContent showCloseButton>
|
|
249
|
+
<DialogHeader>
|
|
250
|
+
<DialogTitle>Assign to Department</DialogTitle>
|
|
251
|
+
<DialogDescription>
|
|
252
|
+
Dropdown popup appears above the dialog overlay — not clipped
|
|
253
|
+
by overflow.
|
|
254
|
+
</DialogDescription>
|
|
255
|
+
</DialogHeader>
|
|
256
|
+
<DialogBody className="gap-4 py-2">
|
|
257
|
+
<Dropdown
|
|
258
|
+
id="dept"
|
|
259
|
+
label="Department"
|
|
260
|
+
size="md"
|
|
261
|
+
fullwidth
|
|
262
|
+
options={dialogOptions}
|
|
263
|
+
value={department}
|
|
264
|
+
onSelect={setDepartment}
|
|
265
|
+
/>
|
|
266
|
+
</DialogBody>
|
|
267
|
+
<DialogFooter>
|
|
268
|
+
<DialogClose asChild>
|
|
269
|
+
<Button variant="outline">Cancel</Button>
|
|
270
|
+
</DialogClose>
|
|
271
|
+
<Button>Confirm</Button>
|
|
272
|
+
</DialogFooter>
|
|
273
|
+
</DialogContent>
|
|
274
|
+
</Dialog>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{/* Demo 2: Multiple dropdowns — each opens independently */}
|
|
278
|
+
<div>
|
|
279
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
280
|
+
Multiple Dropdowns
|
|
281
|
+
</p>
|
|
282
|
+
<Dialog>
|
|
283
|
+
<DialogTrigger asChild>
|
|
284
|
+
<Button variant="outline">Open Dialog</Button>
|
|
285
|
+
</DialogTrigger>
|
|
286
|
+
<DialogContent showCloseButton>
|
|
287
|
+
<DialogHeader>
|
|
288
|
+
<DialogTitle>Invite Team Member</DialogTitle>
|
|
289
|
+
<DialogDescription>
|
|
290
|
+
Multiple dropdowns — each opens its own popup independently.
|
|
291
|
+
</DialogDescription>
|
|
292
|
+
</DialogHeader>
|
|
293
|
+
<DialogBody className="gap-4 py-2">
|
|
294
|
+
<Dropdown
|
|
295
|
+
id="member"
|
|
296
|
+
label="Member"
|
|
297
|
+
size="md"
|
|
298
|
+
fullwidth
|
|
299
|
+
options={filterableOptions}
|
|
300
|
+
value={member}
|
|
301
|
+
onSelect={setMember}
|
|
302
|
+
filterMode
|
|
303
|
+
/>
|
|
304
|
+
<Dropdown
|
|
305
|
+
id="role"
|
|
306
|
+
label="Role"
|
|
307
|
+
size="md"
|
|
308
|
+
fullwidth
|
|
309
|
+
options={[
|
|
310
|
+
{ value: "viewer", label: "Viewer" },
|
|
311
|
+
{ value: "editor", label: "Editor" },
|
|
312
|
+
{ value: "admin", label: "Admin" },
|
|
313
|
+
]}
|
|
314
|
+
value={role}
|
|
315
|
+
onSelect={setRole}
|
|
316
|
+
/>
|
|
317
|
+
</DialogBody>
|
|
318
|
+
<DialogFooter>
|
|
319
|
+
<DialogClose asChild>
|
|
320
|
+
<Button variant="outline">Cancel</Button>
|
|
321
|
+
</DialogClose>
|
|
322
|
+
<Button disabled={!member || !role}>Send Invite</Button>
|
|
323
|
+
</DialogFooter>
|
|
324
|
+
</DialogContent>
|
|
325
|
+
</Dialog>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{/* Demo 3: Filterable dropdown in dialog */}
|
|
329
|
+
<div>
|
|
330
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
331
|
+
Filter Mode
|
|
332
|
+
</p>
|
|
333
|
+
<Dialog>
|
|
334
|
+
<DialogTrigger asChild>
|
|
335
|
+
<Button variant="outline">Open Dialog</Button>
|
|
336
|
+
</DialogTrigger>
|
|
337
|
+
<DialogContent showCloseButton>
|
|
338
|
+
<DialogHeader>
|
|
339
|
+
<DialogTitle>Search & Select</DialogTitle>
|
|
340
|
+
<DialogDescription>
|
|
341
|
+
filterMode=true — type to filter options, popup stays properly
|
|
342
|
+
positioned.
|
|
343
|
+
</DialogDescription>
|
|
344
|
+
</DialogHeader>
|
|
345
|
+
<DialogBody className="gap-4 py-2">
|
|
346
|
+
<Dropdown
|
|
347
|
+
id="member-filter"
|
|
348
|
+
label="Search member"
|
|
349
|
+
size="md"
|
|
350
|
+
fullwidth
|
|
351
|
+
filterMode
|
|
352
|
+
options={filterableOptions}
|
|
353
|
+
value={member}
|
|
354
|
+
onSelect={setMember}
|
|
355
|
+
/>
|
|
356
|
+
</DialogBody>
|
|
357
|
+
<DialogFooter>
|
|
358
|
+
<DialogClose asChild>
|
|
359
|
+
<Button variant="outline">Cancel</Button>
|
|
360
|
+
</DialogClose>
|
|
361
|
+
<Button disabled={!member}>Select</Button>
|
|
362
|
+
</DialogFooter>
|
|
363
|
+
</DialogContent>
|
|
364
|
+
</Dialog>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
};
|