@emara/ui 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ui/.gitkeep +0 -0
- package/components/ui/accordion.stories.tsx +231 -0
- package/components/ui/accordion.tsx +250 -0
- package/components/ui/app-shell.stories.tsx +270 -0
- package/components/ui/app-shell.tsx +491 -0
- package/components/ui/avatar.stories.tsx +174 -0
- package/components/ui/avatar.tsx +257 -0
- package/components/ui/badge.stories.tsx +127 -0
- package/components/ui/badge.tsx +146 -0
- package/components/ui/breadcrumb.stories.tsx +92 -0
- package/components/ui/breadcrumb.tsx +302 -0
- package/components/ui/button.stories.tsx +186 -0
- package/components/ui/button.tsx +128 -0
- package/components/ui/card.stories.tsx +279 -0
- package/components/ui/card.tsx +250 -0
- package/components/ui/checkbox.stories.tsx +93 -0
- package/components/ui/checkbox.tsx +131 -0
- package/components/ui/combobox.stories.tsx +489 -0
- package/components/ui/combobox.tsx +874 -0
- package/components/ui/context-menu.stories.tsx +202 -0
- package/components/ui/context-menu.tsx +309 -0
- package/components/ui/data-table.stories.tsx +227 -0
- package/components/ui/data-table.tsx +539 -0
- package/components/ui/date-picker.stories.tsx +225 -0
- package/components/ui/date-picker.tsx +597 -0
- package/components/ui/dialog.stories.tsx +193 -0
- package/components/ui/dialog.tsx +262 -0
- package/components/ui/divider.stories.tsx +84 -0
- package/components/ui/divider.tsx +135 -0
- package/components/ui/drawer.stories.tsx +218 -0
- package/components/ui/drawer.tsx +329 -0
- package/components/ui/dropdown-menu.stories.tsx +270 -0
- package/components/ui/dropdown-menu.tsx +353 -0
- package/components/ui/empty-state.stories.tsx +121 -0
- package/components/ui/empty-state.tsx +289 -0
- package/components/ui/field-group.stories.tsx +201 -0
- package/components/ui/field-group.tsx +276 -0
- package/components/ui/form.stories.tsx +219 -0
- package/components/ui/form.tsx +542 -0
- package/components/ui/input.stories.tsx +154 -0
- package/components/ui/input.tsx +208 -0
- package/components/ui/label.stories.tsx +84 -0
- package/components/ui/label.tsx +98 -0
- package/components/ui/page-header.stories.tsx +136 -0
- package/components/ui/page-header.tsx +315 -0
- package/components/ui/pagination.stories.tsx +136 -0
- package/components/ui/pagination.tsx +427 -0
- package/components/ui/popover.stories.tsx +212 -0
- package/components/ui/popover.tsx +167 -0
- package/components/ui/radio-group.stories.tsx +96 -0
- package/components/ui/radio-group.tsx +250 -0
- package/components/ui/select.stories.tsx +203 -0
- package/components/ui/select.tsx +318 -0
- package/components/ui/sidebar.stories.tsx +186 -0
- package/components/ui/sidebar.tsx +623 -0
- package/components/ui/skeleton.stories.tsx +131 -0
- package/components/ui/skeleton.tsx +311 -0
- package/components/ui/switch.stories.tsx +74 -0
- package/components/ui/switch.tsx +186 -0
- package/components/ui/table.stories.tsx +107 -0
- package/components/ui/table.tsx +285 -0
- package/components/ui/tabs.stories.tsx +222 -0
- package/components/ui/tabs.tsx +287 -0
- package/components/ui/textarea.stories.tsx +96 -0
- package/components/ui/textarea.tsx +182 -0
- package/components/ui/toast.stories.tsx +169 -0
- package/components/ui/toast.tsx +250 -0
- package/components/ui/tooltip.stories.tsx +146 -0
- package/components/ui/tooltip.tsx +156 -0
- package/components/ui/top-bar.stories.tsx +182 -0
- package/components/ui/top-bar.tsx +155 -0
- package/dist/components/ui/accordion.d.ts +45 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +99 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/app-shell.d.ts +70 -0
- package/dist/components/ui/app-shell.d.ts.map +1 -0
- package/dist/components/ui/app-shell.js +199 -0
- package/dist/components/ui/app-shell.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +41 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +104 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +27 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +65 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +35 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +88 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +26 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +73 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +52 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +96 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +18 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +59 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +194 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +361 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +46 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +95 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +53 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +163 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/date-picker.d.ts +103 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +306 -0
- package/dist/components/ui/date-picker.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +40 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +110 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/divider.d.ts +30 -0
- package/dist/components/ui/divider.d.ts.map +1 -0
- package/dist/components/ui/divider.js +62 -0
- package/dist/components/ui/divider.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +56 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +147 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +63 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +116 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty-state.d.ts +43 -0
- package/dist/components/ui/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty-state.js +128 -0
- package/dist/components/ui/empty-state.js.map +1 -0
- package/dist/components/ui/field-group.d.ts +38 -0
- package/dist/components/ui/field-group.d.ts.map +1 -0
- package/dist/components/ui/field-group.js +107 -0
- package/dist/components/ui/field-group.js.map +1 -0
- package/dist/components/ui/form.d.ts +67 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +286 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/input.d.ts +36 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +99 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +37 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +34 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +65 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +140 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +67 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +109 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +28 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +85 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +35 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +103 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +42 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +86 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +59 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +189 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +77 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +115 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/switch.d.ts +26 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +84 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +52 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +109 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +42 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +163 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +26 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +96 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +77 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +141 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +31 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +71 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/top-bar.d.ts +30 -0
- package/dist/components/ui/top-bar.d.ts.map +1 -0
- package/dist/components/ui/top-bar.js +64 -0
- package/dist/components/ui/top-bar.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/lib/utils.ts +6 -0
- package/package.json +112 -0
- package/styles/globals.css +685 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useId, useRef, useState } from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { RiCloseLine, RiLoader2Line } from "@remixicon/react";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
// Per docs/emara-ui-phase-1-components.md §2.
|
|
11
|
+
|
|
12
|
+
const inputWrapperVariants = cva(
|
|
13
|
+
[
|
|
14
|
+
"relative flex items-center w-full gap-2 ps-3 pe-3",
|
|
15
|
+
"rounded-md border border-input bg-background text-foreground",
|
|
16
|
+
"transition-colors",
|
|
17
|
+
"focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background",
|
|
18
|
+
"has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50",
|
|
19
|
+
].join(" "),
|
|
20
|
+
{
|
|
21
|
+
variants: {
|
|
22
|
+
size: {
|
|
23
|
+
xs: "h-7 text-xs",
|
|
24
|
+
sm: "h-8 text-xs",
|
|
25
|
+
md: "h-9 text-sm",
|
|
26
|
+
lg: "h-10 text-base",
|
|
27
|
+
xl: "h-12 text-base",
|
|
28
|
+
},
|
|
29
|
+
invalid: {
|
|
30
|
+
true: "border-destructive focus-within:ring-destructive",
|
|
31
|
+
false: "",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
size: "md",
|
|
36
|
+
invalid: false,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const inputFieldVariants = cva(
|
|
42
|
+
[
|
|
43
|
+
// The wrapper owns horizontal padding + gap. The field itself is flush
|
|
44
|
+
// inside the wrapper so adornments sit at the natural visual distance.
|
|
45
|
+
"block w-full min-w-0 bg-transparent text-foreground placeholder:text-muted-foreground",
|
|
46
|
+
"focus:outline-none disabled:cursor-not-allowed",
|
|
47
|
+
// Hide the spinner only for type=number. Other types keep their native
|
|
48
|
+
// chrome (search's clear button, date pickers, etc.).
|
|
49
|
+
"[&[type='number']]:[appearance:textfield]",
|
|
50
|
+
"[&[type='number']::-webkit-outer-spin-button]:appearance-none",
|
|
51
|
+
"[&[type='number']::-webkit-inner-spin-button]:appearance-none",
|
|
52
|
+
].join(" "),
|
|
53
|
+
{
|
|
54
|
+
variants: {
|
|
55
|
+
size: {
|
|
56
|
+
xs: "h-7",
|
|
57
|
+
sm: "h-8",
|
|
58
|
+
md: "h-9",
|
|
59
|
+
lg: "h-10",
|
|
60
|
+
xl: "h-12",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultVariants: {
|
|
64
|
+
size: "md",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
type InputWrapperVariants = VariantProps<typeof inputWrapperVariants>;
|
|
70
|
+
|
|
71
|
+
type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> & {
|
|
72
|
+
size?: InputWrapperVariants["size"];
|
|
73
|
+
type?: "text" | "email" | "password" | "number" | "tel" | "url" | "search";
|
|
74
|
+
invalid?: boolean;
|
|
75
|
+
startAdornment?: React.ReactNode;
|
|
76
|
+
endAdornment?: React.ReactNode;
|
|
77
|
+
clearable?: boolean;
|
|
78
|
+
loading?: boolean;
|
|
79
|
+
onClear?: () => void;
|
|
80
|
+
asChild?: boolean;
|
|
81
|
+
wrapperClassName?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
85
|
+
{
|
|
86
|
+
className,
|
|
87
|
+
wrapperClassName,
|
|
88
|
+
size,
|
|
89
|
+
type = "text",
|
|
90
|
+
invalid = false,
|
|
91
|
+
startAdornment,
|
|
92
|
+
endAdornment,
|
|
93
|
+
clearable = false,
|
|
94
|
+
loading = false,
|
|
95
|
+
onClear,
|
|
96
|
+
asChild = false,
|
|
97
|
+
value,
|
|
98
|
+
defaultValue,
|
|
99
|
+
onChange,
|
|
100
|
+
disabled,
|
|
101
|
+
readOnly,
|
|
102
|
+
id,
|
|
103
|
+
children,
|
|
104
|
+
"aria-describedby": ariaDescribedBy,
|
|
105
|
+
...props
|
|
106
|
+
},
|
|
107
|
+
forwardedRef,
|
|
108
|
+
) {
|
|
109
|
+
const isControlled = value !== undefined;
|
|
110
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<string>(
|
|
111
|
+
typeof defaultValue === "string" ? defaultValue : "",
|
|
112
|
+
);
|
|
113
|
+
const currentValue = isControlled ? String(value ?? "") : uncontrolledValue;
|
|
114
|
+
const showClear = clearable && !disabled && !readOnly && !loading && currentValue.length > 0;
|
|
115
|
+
|
|
116
|
+
const innerRef = useRef<HTMLInputElement>(null);
|
|
117
|
+
const setRefs = (node: HTMLInputElement | null) => {
|
|
118
|
+
innerRef.current = node;
|
|
119
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
120
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const reactId = useId();
|
|
124
|
+
const inputId = id ?? reactId;
|
|
125
|
+
|
|
126
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
127
|
+
if (!isControlled) setUncontrolledValue(e.target.value);
|
|
128
|
+
onChange?.(e);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleClear = () => {
|
|
132
|
+
if (!isControlled) setUncontrolledValue("");
|
|
133
|
+
onClear?.();
|
|
134
|
+
const node = innerRef.current;
|
|
135
|
+
if (node) {
|
|
136
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
137
|
+
window.HTMLInputElement.prototype,
|
|
138
|
+
"value",
|
|
139
|
+
)?.set;
|
|
140
|
+
nativeSetter?.call(node, "");
|
|
141
|
+
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
142
|
+
node.focus();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const hasEndContent = showClear || loading || endAdornment;
|
|
147
|
+
// When `asChild` is true the consumer provides their own input-like element
|
|
148
|
+
// as `children`; Slot forwards Input's props onto it. Otherwise we render a
|
|
149
|
+
// native <input>.
|
|
150
|
+
const Field = asChild ? Slot : "input";
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className={cn(inputWrapperVariants({ size, invalid }), wrapperClassName)}>
|
|
154
|
+
{startAdornment ? (
|
|
155
|
+
<span
|
|
156
|
+
className="text-muted-foreground pointer-events-none flex shrink-0 items-center [&_svg]:size-4 [&_svg]:shrink-0"
|
|
157
|
+
aria-hidden={typeof startAdornment !== "string"}
|
|
158
|
+
>
|
|
159
|
+
{startAdornment}
|
|
160
|
+
</span>
|
|
161
|
+
) : null}
|
|
162
|
+
|
|
163
|
+
<Field
|
|
164
|
+
ref={setRefs}
|
|
165
|
+
id={inputId}
|
|
166
|
+
type={type}
|
|
167
|
+
className={cn(inputFieldVariants({ size }), className)}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
readOnly={readOnly}
|
|
170
|
+
aria-invalid={invalid || undefined}
|
|
171
|
+
aria-describedby={ariaDescribedBy}
|
|
172
|
+
value={isControlled ? currentValue : undefined}
|
|
173
|
+
defaultValue={isControlled ? undefined : defaultValue}
|
|
174
|
+
onChange={handleChange}
|
|
175
|
+
{...props}
|
|
176
|
+
>
|
|
177
|
+
{asChild ? children : null}
|
|
178
|
+
</Field>
|
|
179
|
+
|
|
180
|
+
{hasEndContent ? (
|
|
181
|
+
<span className="text-muted-foreground flex shrink-0 items-center gap-1 [&_svg]:size-4 [&_svg]:shrink-0">
|
|
182
|
+
{showClear ? (
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={handleClear}
|
|
186
|
+
aria-label="Clear"
|
|
187
|
+
className={cn(
|
|
188
|
+
"inline-flex items-center justify-center rounded p-0.5",
|
|
189
|
+
"text-muted-foreground hover:text-foreground",
|
|
190
|
+
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
|
|
191
|
+
)}
|
|
192
|
+
>
|
|
193
|
+
<RiCloseLine />
|
|
194
|
+
</button>
|
|
195
|
+
) : null}
|
|
196
|
+
{loading ? <RiLoader2Line className="animate-spin" /> : null}
|
|
197
|
+
{endAdornment ? (
|
|
198
|
+
<span aria-hidden={typeof endAdornment !== "string"}>{endAdornment}</span>
|
|
199
|
+
) : null}
|
|
200
|
+
</span>
|
|
201
|
+
) : null}
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
Input.displayName = "Input";
|
|
206
|
+
|
|
207
|
+
export { Input, inputWrapperVariants, inputFieldVariants };
|
|
208
|
+
export type { InputProps };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Input } from "./input";
|
|
4
|
+
import { Label } from "./label";
|
|
5
|
+
import { TooltipProvider } from "./tooltip";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Label> = {
|
|
8
|
+
title: "Foundations/Label",
|
|
9
|
+
component: Label,
|
|
10
|
+
parameters: { layout: "centered" },
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<TooltipProvider delayDuration={200}>
|
|
14
|
+
<Story />
|
|
15
|
+
</TooltipProvider>
|
|
16
|
+
),
|
|
17
|
+
],
|
|
18
|
+
argTypes: {
|
|
19
|
+
size: { control: "select", options: ["sm", "md"] },
|
|
20
|
+
required: { control: "boolean" },
|
|
21
|
+
optional: { control: "boolean" },
|
|
22
|
+
},
|
|
23
|
+
args: { children: "Email address" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof Label>;
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {};
|
|
30
|
+
|
|
31
|
+
export const Sizes: Story = {
|
|
32
|
+
render: (args) => (
|
|
33
|
+
<div className="space-y-2">
|
|
34
|
+
<Label {...args} size="sm">
|
|
35
|
+
Small label
|
|
36
|
+
</Label>
|
|
37
|
+
<Label {...args} size="md">
|
|
38
|
+
Medium label
|
|
39
|
+
</Label>
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Required: Story = {
|
|
45
|
+
args: { required: true, children: "Email address" },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const Optional: Story = {
|
|
49
|
+
args: { optional: true, children: "Phone number" },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const PairedWithInput: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<div className="w-72 space-y-1.5">
|
|
55
|
+
<Label htmlFor="email" required>
|
|
56
|
+
Email address
|
|
57
|
+
</Label>
|
|
58
|
+
<Input id="email" type="email" placeholder="alice@example.com" />
|
|
59
|
+
</div>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const InheritsDisabled: Story = {
|
|
64
|
+
render: () => (
|
|
65
|
+
<div className="w-72 space-y-1.5">
|
|
66
|
+
<Label htmlFor="dis">
|
|
67
|
+
Username <span className="text-muted-foreground font-normal">(disabled control below)</span>
|
|
68
|
+
</Label>
|
|
69
|
+
<Input id="dis" disabled defaultValue="locked" className="peer" />
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** `hint` adds a focusable info-icon at the end of the label with a tooltip. */
|
|
75
|
+
export const WithHint: Story = {
|
|
76
|
+
render: () => (
|
|
77
|
+
<div className="w-72 space-y-1.5">
|
|
78
|
+
<Label htmlFor="pw" required hint="Must be 8+ characters and include a number.">
|
|
79
|
+
Password
|
|
80
|
+
</Label>
|
|
81
|
+
<Input id="pw" type="password" />
|
|
82
|
+
</div>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
5
|
+
import { RiInformationLine } from "@remixicon/react";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
10
|
+
|
|
11
|
+
// Per docs/emara-ui-phase-1-components.md §4.
|
|
12
|
+
|
|
13
|
+
const labelVariants = cva(
|
|
14
|
+
[
|
|
15
|
+
"inline-flex items-center gap-1 font-medium text-foreground select-none",
|
|
16
|
+
// Inherits disabled appearance from the associated control.
|
|
17
|
+
"peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
18
|
+
"group-data-[disabled=true]:cursor-not-allowed group-data-[disabled=true]:opacity-50",
|
|
19
|
+
].join(" "),
|
|
20
|
+
{
|
|
21
|
+
variants: {
|
|
22
|
+
size: {
|
|
23
|
+
sm: "text-xs",
|
|
24
|
+
md: "text-sm",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
size: "md",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
type LabelVariants = VariantProps<typeof labelVariants>;
|
|
34
|
+
|
|
35
|
+
type LabelProps = Omit<React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>, "asChild"> &
|
|
36
|
+
LabelVariants & {
|
|
37
|
+
required?: boolean;
|
|
38
|
+
optional?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Localized text shown when `optional` is true. Defaults to "(optional)".
|
|
41
|
+
*/
|
|
42
|
+
optionalText?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Info-icon-with-tooltip rendered at the end of the label. The hint must
|
|
45
|
+
* itself sit inside a `<TooltipProvider>` somewhere up the tree —
|
|
46
|
+
* `AppShell` provides one for the whole app; otherwise wrap a section.
|
|
47
|
+
*/
|
|
48
|
+
hint?: React.ReactNode;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const Label = forwardRef<React.ElementRef<typeof LabelPrimitive.Root>, LabelProps>(function Label(
|
|
52
|
+
{ className, size, required, optional, optionalText = "(optional)", hint, children, ...props },
|
|
53
|
+
ref,
|
|
54
|
+
) {
|
|
55
|
+
if (required && optional) {
|
|
56
|
+
throw new Error("Label: `required` and `optional` are mutually exclusive.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<LabelPrimitive.Root ref={ref} className={cn(labelVariants({ size }), className)} {...props}>
|
|
61
|
+
<span>{children}</span>
|
|
62
|
+
{required ? (
|
|
63
|
+
<span aria-hidden="true" className="text-destructive ms-0.5">
|
|
64
|
+
*
|
|
65
|
+
</span>
|
|
66
|
+
) : null}
|
|
67
|
+
{optional ? (
|
|
68
|
+
<span className="text-muted-foreground ms-1 font-normal">{optionalText}</span>
|
|
69
|
+
) : null}
|
|
70
|
+
{hint !== undefined && hint !== null ? (
|
|
71
|
+
<Tooltip>
|
|
72
|
+
<TooltipTrigger asChild>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
tabIndex={0}
|
|
76
|
+
aria-label="More info"
|
|
77
|
+
className={cn(
|
|
78
|
+
"ms-1 inline-flex items-center justify-center rounded-full",
|
|
79
|
+
"text-muted-foreground hover:text-foreground",
|
|
80
|
+
"focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
81
|
+
"[&_svg]:size-3.5 [&_svg]:shrink-0",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<RiInformationLine />
|
|
85
|
+
</button>
|
|
86
|
+
</TooltipTrigger>
|
|
87
|
+
<TooltipContent multiline maxWidth={240}>
|
|
88
|
+
{hint}
|
|
89
|
+
</TooltipContent>
|
|
90
|
+
</Tooltip>
|
|
91
|
+
) : null}
|
|
92
|
+
</LabelPrimitive.Root>
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
Label.displayName = "Label";
|
|
96
|
+
|
|
97
|
+
export { Label, labelVariants };
|
|
98
|
+
export type { LabelProps };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { RiAddLine, RiHome4Line } from "@remixicon/react";
|
|
3
|
+
|
|
4
|
+
import { Badge } from "./badge";
|
|
5
|
+
import { Breadcrumbs } from "./breadcrumb";
|
|
6
|
+
import { Button } from "./button";
|
|
7
|
+
import {
|
|
8
|
+
PageHeader,
|
|
9
|
+
PageHeaderActions,
|
|
10
|
+
PageHeaderBadge,
|
|
11
|
+
PageHeaderBreadcrumb,
|
|
12
|
+
PageHeaderDescription,
|
|
13
|
+
PageHeaderTitle,
|
|
14
|
+
PageHeaderTop,
|
|
15
|
+
} from "./page-header";
|
|
16
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
17
|
+
|
|
18
|
+
const meta: Meta<typeof PageHeader> = {
|
|
19
|
+
title: "Layout/PageHeader",
|
|
20
|
+
component: PageHeader,
|
|
21
|
+
parameters: { layout: "padded" },
|
|
22
|
+
argTypes: {
|
|
23
|
+
variant: { control: "select", options: ["default", "compact", "splash"] },
|
|
24
|
+
size: { control: "select", options: ["sm", "md", "lg"] },
|
|
25
|
+
divider: { control: "boolean" },
|
|
26
|
+
sticky: { control: "boolean" },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<typeof PageHeader>;
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
title: "Customers",
|
|
36
|
+
description: "Manage customers and their billing details.",
|
|
37
|
+
actions: (
|
|
38
|
+
<>
|
|
39
|
+
<Button variant="outline">Import</Button>
|
|
40
|
+
<Button leftIcon={<RiAddLine />}>Add customer</Button>
|
|
41
|
+
</>
|
|
42
|
+
),
|
|
43
|
+
divider: true,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Compact: Story = {
|
|
48
|
+
args: { variant: "compact", title: "Settings", divider: true },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const Splash: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
variant: "splash",
|
|
54
|
+
title: "Welcome back",
|
|
55
|
+
description: "Here's what's happening with your business today.",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const WithBreadcrumb: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
title: "Order #1248",
|
|
62
|
+
description: "Created 3 days ago",
|
|
63
|
+
breadcrumb: (
|
|
64
|
+
<Breadcrumbs
|
|
65
|
+
items={[
|
|
66
|
+
{ label: "Home", href: "/", icon: <RiHome4Line /> },
|
|
67
|
+
{ label: "Orders", href: "/orders" },
|
|
68
|
+
{ label: "Order #1248" },
|
|
69
|
+
]}
|
|
70
|
+
/>
|
|
71
|
+
),
|
|
72
|
+
actions: <Button>Update</Button>,
|
|
73
|
+
divider: true,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const WithBackButton: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
title: "Edit customer",
|
|
80
|
+
description: "Update billing and contact info.",
|
|
81
|
+
backHref: "/customers",
|
|
82
|
+
actions: <Button>Save</Button>,
|
|
83
|
+
divider: true,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const WithTabs: Story = {
|
|
88
|
+
render: () => (
|
|
89
|
+
<PageHeader
|
|
90
|
+
title="Project Apollo"
|
|
91
|
+
description="Long-running platform initiative."
|
|
92
|
+
divider
|
|
93
|
+
tabs={
|
|
94
|
+
<Tabs defaultValue="overview" variant="line">
|
|
95
|
+
<TabsList>
|
|
96
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
97
|
+
<TabsTrigger value="activity">Activity</TabsTrigger>
|
|
98
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
99
|
+
</TabsList>
|
|
100
|
+
<TabsContent value="overview" />
|
|
101
|
+
<TabsContent value="activity" />
|
|
102
|
+
<TabsContent value="settings" />
|
|
103
|
+
</Tabs>
|
|
104
|
+
}
|
|
105
|
+
actions={<Button>Invite</Button>}
|
|
106
|
+
/>
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const Composed: Story = {
|
|
111
|
+
render: () => (
|
|
112
|
+
<PageHeader variant="default" size="md" divider>
|
|
113
|
+
<PageHeaderBreadcrumb>
|
|
114
|
+
<Breadcrumbs
|
|
115
|
+
items={[{ label: "Home", href: "/", icon: <RiHome4Line /> }, { label: "Team" }]}
|
|
116
|
+
/>
|
|
117
|
+
</PageHeaderBreadcrumb>
|
|
118
|
+
<PageHeaderTop>
|
|
119
|
+
<PageHeaderTitle as="h1">Team</PageHeaderTitle>
|
|
120
|
+
<PageHeaderBadge>
|
|
121
|
+
<Badge size="sm" variant="info">
|
|
122
|
+
12 members
|
|
123
|
+
</Badge>
|
|
124
|
+
</PageHeaderBadge>
|
|
125
|
+
<span className="flex-1" />
|
|
126
|
+
<PageHeaderActions>
|
|
127
|
+
<Button variant="outline">Filter</Button>
|
|
128
|
+
<Button>Invite</Button>
|
|
129
|
+
</PageHeaderActions>
|
|
130
|
+
</PageHeaderTop>
|
|
131
|
+
<PageHeaderDescription>
|
|
132
|
+
Manage roles and access for everyone on the workspace.
|
|
133
|
+
</PageHeaderDescription>
|
|
134
|
+
</PageHeader>
|
|
135
|
+
),
|
|
136
|
+
};
|