@exegia/corpora-ui 0.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 (71) hide show
  1. package/README.md +21 -0
  2. package/dist-lib/components/blocks/auth/auth-shell.d.ts +56 -0
  3. package/dist-lib/components/blocks/auth/code-auth-block.d.ts +22 -0
  4. package/dist-lib/components/blocks/auth/forgot-password-block.d.ts +15 -0
  5. package/dist-lib/components/blocks/auth/login-block.d.ts +25 -0
  6. package/dist-lib/components/blocks/auth/signup-block.d.ts +26 -0
  7. package/dist-lib/components/composed/password-input.d.ts +30 -0
  8. package/dist-lib/components/composed/social-providers.d.ts +22 -0
  9. package/dist-lib/components/docs/code-block.d.ts +4 -0
  10. package/dist-lib/components/docs/component-preview.d.ts +10 -0
  11. package/dist-lib/components/docs/demo-controls.d.ts +28 -0
  12. package/dist-lib/components/docs/props-table.d.ts +4 -0
  13. package/dist-lib/components/theme-provider.d.ts +15 -0
  14. package/dist-lib/components/ui/button.d.ts +28 -0
  15. package/dist-lib/components/ui/card.d.ts +16 -0
  16. package/dist-lib/components/ui/checkbox.d.ts +7 -0
  17. package/dist-lib/components/ui/field.d.ts +10 -0
  18. package/dist-lib/components/ui/frame.d.ts +7 -0
  19. package/dist-lib/components/ui/glasscn/glass-button.d.ts +6 -0
  20. package/dist-lib/components/ui/glasscn/liquid-glass.d.ts +31 -0
  21. package/dist-lib/components/ui/input-group.d.ts +13 -0
  22. package/dist-lib/components/ui/input.d.ts +9 -0
  23. package/dist-lib/components/ui/label.d.ts +11 -0
  24. package/dist-lib/components/ui/otp-field.d.ts +11 -0
  25. package/dist-lib/components/ui/separator.d.ts +4 -0
  26. package/dist-lib/components/ui/spinner.d.ts +3 -0
  27. package/dist-lib/components/ui/textarea.d.ts +8 -0
  28. package/dist-lib/index.d.ts +29 -0
  29. package/dist-lib/index.js +1617 -0
  30. package/dist-lib/index.js.map +1 -0
  31. package/dist-lib/lib/auth-accent.d.ts +33 -0
  32. package/dist-lib/lib/glass-variants.d.ts +6 -0
  33. package/dist-lib/lib/sound.d.ts +13 -0
  34. package/dist-lib/lib/utils.d.ts +2 -0
  35. package/dist-lib/lib/utils.test.d.ts +1 -0
  36. package/package.json +80 -0
  37. package/src/components/blocks/.gitkeep +0 -0
  38. package/src/components/blocks/auth/auth-shell.tsx +266 -0
  39. package/src/components/blocks/auth/code-auth-block.tsx +190 -0
  40. package/src/components/blocks/auth/forgot-password-block.tsx +123 -0
  41. package/src/components/blocks/auth/login-block.tsx +239 -0
  42. package/src/components/blocks/auth/signup-block.tsx +260 -0
  43. package/src/components/composed/.gitkeep +0 -0
  44. package/src/components/composed/password-input.tsx +179 -0
  45. package/src/components/composed/social-providers.tsx +89 -0
  46. package/src/components/docs/code-block.tsx +8 -0
  47. package/src/components/docs/component-preview.tsx +28 -0
  48. package/src/components/docs/demo-controls.tsx +118 -0
  49. package/src/components/docs/props-table.tsx +35 -0
  50. package/src/components/theme-provider.tsx +230 -0
  51. package/src/components/ui/button.tsx +199 -0
  52. package/src/components/ui/card.tsx +253 -0
  53. package/src/components/ui/checkbox.tsx +83 -0
  54. package/src/components/ui/field.tsx +89 -0
  55. package/src/components/ui/frame.tsx +87 -0
  56. package/src/components/ui/glasscn/glass-button.tsx +25 -0
  57. package/src/components/ui/glasscn/liquid-glass.tsx +276 -0
  58. package/src/components/ui/input-group.tsx +105 -0
  59. package/src/components/ui/input.tsx +70 -0
  60. package/src/components/ui/label.tsx +36 -0
  61. package/src/components/ui/otp-field.tsx +88 -0
  62. package/src/components/ui/separator.tsx +23 -0
  63. package/src/components/ui/spinner.tsx +17 -0
  64. package/src/components/ui/textarea.tsx +58 -0
  65. package/src/index.css +171 -0
  66. package/src/index.ts +37 -0
  67. package/src/lib/auth-accent.ts +49 -0
  68. package/src/lib/glass-variants.ts +57 -0
  69. package/src/lib/sound.ts +39 -0
  70. package/src/lib/utils.test.ts +22 -0
  71. package/src/lib/utils.ts +6 -0
@@ -0,0 +1,87 @@
1
+ import type * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export function Frame({
5
+ className,
6
+ ...props
7
+ }: React.ComponentProps<"div">): React.ReactElement {
8
+ return (
9
+ <div
10
+ className={cn(
11
+ "relative flex flex-col rounded-2xl bg-muted/72 p-1",
12
+ "*:[[data-slot=frame-panel]+[data-slot=frame-panel]]:mt-1",
13
+ className,
14
+ )}
15
+ data-slot="frame"
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export function FramePanel({
22
+ className,
23
+ ...props
24
+ }: React.ComponentProps<"div">): React.ReactElement {
25
+ return (
26
+ <div
27
+ className={cn(
28
+ "relative rounded-xl border bg-background bg-clip-padding p-5 shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
29
+ className,
30
+ )}
31
+ data-slot="frame-panel"
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ export function FrameHeader({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<"header">): React.ReactElement {
41
+ return (
42
+ <header
43
+ className={cn("flex flex-col px-5 py-4", className)}
44
+ data-slot="frame-panel-header"
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+
50
+ export function FrameTitle({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">): React.ReactElement {
54
+ return (
55
+ <div
56
+ className={cn("font-semibold text-sm", className)}
57
+ data-slot="frame-panel-title"
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+
63
+ export function FrameDescription({
64
+ className,
65
+ ...props
66
+ }: React.ComponentProps<"div">): React.ReactElement {
67
+ return (
68
+ <div
69
+ className={cn("text-muted-foreground text-sm", className)}
70
+ data-slot="frame-panel-description"
71
+ {...props}
72
+ />
73
+ );
74
+ }
75
+
76
+ export function FrameFooter({
77
+ className,
78
+ ...props
79
+ }: React.ComponentProps<"footer">): React.ReactElement {
80
+ return (
81
+ <footer
82
+ className={cn("px-5 py-4", className)}
83
+ data-slot="frame-panel-footer"
84
+ {...props}
85
+ />
86
+ );
87
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import type { FrostGlassVariantProp } from "@/lib/glass-variants";
4
+
5
+ import { Button, type ButtonProps } from "../button";
6
+
7
+ type GlassButtonProps = Omit<ButtonProps, "variant" | "glassVariant"> &
8
+ FrostGlassVariantProp;
9
+
10
+ /** @deprecated Use `<Button variant="glass" glassVariant="…">` instead. */
11
+ function GlassButton({
12
+ glassVariant = "liquid-refract",
13
+ ...props
14
+ }: GlassButtonProps) {
15
+ return (
16
+ <Button
17
+ data-slot="glass-button"
18
+ variant="glass"
19
+ glassVariant={glassVariant}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ export { GlassButton };
@@ -0,0 +1,276 @@
1
+ "use client";
2
+
3
+ import {
4
+ type CSSProperties,
5
+ type HTMLAttributes,
6
+ forwardRef,
7
+ useCallback,
8
+ useEffect,
9
+ useId,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from "react";
14
+
15
+ import { cn } from "@/lib/utils";
16
+
17
+ type MapGeometry = {
18
+ width: number; // element width, CSS px, integer
19
+ height: number; // element height, CSS px, integer
20
+ radius: number; // corner radius, CSS px, already clamped to min(width, height) / 2
21
+ bezel: number; // fraction (0..1] of the half-min-dimension used as the refractive band
22
+ };
23
+
24
+ const MAX_TEXTURE_SIZE = 480;
25
+ const EDGE_TAPER_PX = 1.25;
26
+
27
+ const displacementMapCache = new Map<string, string>();
28
+
29
+ // CSS.supports("backdrop-filter", "url(#f)") returns true in Safari and
30
+ // Firefox (they parse the value) but neither browser actually renders SVG
31
+ // filters through backdrop-filter. UA sniffing is the only reliable approach.
32
+ // When WebKit ships support (track WebKit bug 245510), loosen this function.
33
+ function supportsSvgBackdropFilter() {
34
+ if (typeof navigator === "undefined") return false;
35
+ const ua = navigator.userAgent;
36
+ const isChromium = /Chrom(e|ium)/.test(ua) || /Edg\//.test(ua);
37
+ return isChromium && !/Firefox/.test(ua);
38
+ }
39
+
40
+ // NOTE: Per-corner radii and percentage radii are NOT supported. The map
41
+ // reads only borderTopLeftRadius in px. If a future consumer needs
42
+ // asymmetric corners, extend the corner SDF branch to accept four radii.
43
+ //
44
+ // NOTE: Map regeneration renders a ≤480 px texture and encodes a PNG for
45
+ // each unique (width, height, radius, bezel). Continuously animating the
46
+ // element's SIZE will thrash the cache; animate transform: scale instead.
47
+ function createDisplacementMap({ width, height, radius, bezel }: MapGeometry) {
48
+ const cacheKey = `${width}:${height}:${radius}:${bezel}`;
49
+ const cached = displacementMapCache.get(cacheKey);
50
+ if (cached !== undefined) return cached;
51
+
52
+ const downscale = Math.min(1, MAX_TEXTURE_SIZE / Math.max(width, height));
53
+ const texWidth = Math.max(2, Math.round(width * downscale));
54
+ const texHeight = Math.max(2, Math.round(height * downscale));
55
+
56
+ const canvas = document.createElement("canvas");
57
+ canvas.width = texWidth;
58
+ canvas.height = texHeight;
59
+ const ctx = canvas.getContext("2d");
60
+ if (!ctx) return "";
61
+
62
+ const image = ctx.createImageData(texWidth, texHeight);
63
+ const data = image.data;
64
+ const halfWidth = width / 2;
65
+ const halfHeight = height / 2;
66
+ const r = Math.min(radius, halfWidth, halfHeight);
67
+ const bezelPx = Math.max(1, bezel * Math.min(halfWidth, halfHeight));
68
+
69
+ for (let ty = 0; ty < texHeight; ty += 1) {
70
+ for (let tx = 0; tx < texWidth; tx += 1) {
71
+ // Texel center in element coordinates, origin at element center.
72
+ const px = ((tx + 0.5) / texWidth) * width - halfWidth;
73
+ const py = ((ty + 0.5) / texHeight) * height - halfHeight;
74
+
75
+ // Signed distance + outward normal of the rounded-rectangle contour.
76
+ const qx = Math.abs(px) - (halfWidth - r);
77
+ const qy = Math.abs(py) - (halfHeight - r);
78
+ let signedDistance: number;
79
+ let dirX = 0;
80
+ let dirY = 0;
81
+ if (qx > 0 && qy > 0) {
82
+ const len = Math.hypot(qx, qy);
83
+ signedDistance = len - r;
84
+ dirX = (Math.sign(px) * qx) / len;
85
+ dirY = (Math.sign(py) * qy) / len;
86
+ } else if (qx > qy) {
87
+ signedDistance = qx - r;
88
+ dirX = Math.sign(px);
89
+ } else {
90
+ signedDistance = qy - r;
91
+ dirY = Math.sign(py);
92
+ }
93
+
94
+ const index = (ty * texWidth + tx) * 4;
95
+ const inside = -signedDistance; // px from the contour, positive inside
96
+
97
+ if (inside <= 0) {
98
+ data[index] = 128;
99
+ data[index + 1] = 128;
100
+ data[index + 2] = 128;
101
+ data[index + 3] = 255;
102
+ continue;
103
+ }
104
+
105
+ const t = Math.min(1, inside / bezelPx); // 0 at the edge → 1 at the bezel's inner end
106
+ let magnitude = 1 - convexSquircle(t); // max refraction at the edge, optically flat interior
107
+ magnitude *= Math.min(1, inside / EDGE_TAPER_PX); // avoid sampling artifacts on the outermost pixels
108
+
109
+ data[index] = Math.round(128 + dirX * magnitude * 127);
110
+ data[index + 1] = Math.round(128 + dirY * magnitude * 127);
111
+ data[index + 2] = 128;
112
+ data[index + 3] = 255;
113
+ }
114
+ }
115
+
116
+ ctx.putImageData(image, 0, 0);
117
+ const mapUrl = canvas.toDataURL("image/png");
118
+ displacementMapCache.set(cacheKey, mapUrl);
119
+ return mapUrl;
120
+ }
121
+
122
+ function convexSquircle(x: number) {
123
+ return Math.pow(1 - Math.pow(1 - x, 4), 0.25);
124
+ }
125
+
126
+ export type LiquidGlassProps = HTMLAttributes<HTMLDivElement> & {
127
+ /** Extra blur mixed into the backdrop-filter after the SVG refraction. */
128
+ blur?: number;
129
+ /** SVG displacement strength. Higher values bend the sampled backdrop more. */
130
+ refraction?: number;
131
+ /**
132
+ * @deprecated Texture size now derives from element size. Accepted for
133
+ * backwards-compatibility but has no effect.
134
+ */
135
+ mapSize?: number;
136
+ /** Fraction of the radius used as the curved edge where most refraction happens. */
137
+ bezel?: number;
138
+ /** Backdrop saturation. */
139
+ saturation?: number;
140
+ };
141
+
142
+ export const LiquidGlass = forwardRef<HTMLDivElement, LiquidGlassProps>(function LiquidGlass(
143
+ {
144
+ blur = 2,
145
+ refraction = 15,
146
+ mapSize: _mapSize,
147
+ bezel = 0.34,
148
+ saturation = 1.28,
149
+ className,
150
+ style,
151
+ children,
152
+ ...props
153
+ },
154
+ ref,
155
+ ) {
156
+ const rawId = useId();
157
+ const filterId = useMemo(() => `liquid-glass-${rawId.replace(/:/g, "")}`, [rawId]);
158
+ console.log(_mapSize)
159
+ // -------------------------------------------------------------------------
160
+ // Ref merge — we need the DOM node to measure geometry while still
161
+ // forwarding the ref to the consumer.
162
+ // -------------------------------------------------------------------------
163
+
164
+ const localRef = useRef<HTMLDivElement | null>(null);
165
+ const setRefs = useCallback(
166
+ (node: HTMLDivElement | null) => {
167
+ localRef.current = node;
168
+ if (typeof ref === "function") ref(node);
169
+ else if (ref) ref.current = node;
170
+ },
171
+ [ref],
172
+ );
173
+
174
+ // -------------------------------------------------------------------------
175
+ // Geometry measurement via ResizeObserver
176
+ // -------------------------------------------------------------------------
177
+
178
+ const [geometry, setGeometry] = useState<MapGeometry | null>(null);
179
+
180
+ useEffect(() => {
181
+ const el = localRef.current;
182
+ if (!el) return;
183
+ const measure = () => {
184
+ const rect = el.getBoundingClientRect();
185
+ const width = Math.round(rect.width);
186
+ const height = Math.round(rect.height);
187
+ if (!width || !height) return;
188
+ const parsed = Number.parseFloat(getComputedStyle(el).borderTopLeftRadius);
189
+ const radius = Math.min(Number.isFinite(parsed) ? parsed : Math.min(width, height) / 2, width / 2, height / 2);
190
+ setGeometry((prev) =>
191
+ prev && prev.width === width && prev.height === height && prev.radius === radius && prev.bezel === bezel
192
+ ? prev
193
+ : { width, height, radius, bezel },
194
+ );
195
+ };
196
+ measure();
197
+ const observer = new ResizeObserver(measure);
198
+ observer.observe(el);
199
+ return () => observer.disconnect();
200
+ }, [bezel]);
201
+
202
+ const mapUrl = useMemo(() => (geometry ? createDisplacementMap(geometry) : ""), [geometry]);
203
+
204
+ // -------------------------------------------------------------------------
205
+ // Browser capability gate
206
+ //
207
+ // Both SSR and the first client render take the fallback branch so hydration
208
+ // never mismatches. The state is set in an effect (client-only).
209
+ // -------------------------------------------------------------------------
210
+
211
+ const [supported, setSupported] = useState(false);
212
+
213
+ useEffect(() => {
214
+ setSupported(supportsSvgBackdropFilter());
215
+ }, []);
216
+
217
+ const refractionActive = supported && mapUrl !== "";
218
+ const backdropFilter = refractionActive
219
+ ? `url(#${filterId}) blur(${blur}px) saturate(${saturation})`
220
+ : `blur(${blur + 2}px) saturate(${saturation})`;
221
+
222
+ return (
223
+ <>
224
+ <div
225
+ ref={setRefs}
226
+ className={cn(
227
+ "relative overflow-hidden rounded-full bg-white/[0.08]",
228
+ className,
229
+ )}
230
+ style={{ ...style, backdropFilter, WebkitBackdropFilter: backdropFilter } as CSSProperties}
231
+ {...props}
232
+ >
233
+ {children}
234
+ <span
235
+ aria-hidden
236
+ className="pointer-events-none absolute inset-0 rounded-[inherit]"
237
+ style={{
238
+ // Rim thickness, overridable per-consumer via --liquid-glass-rim-width.
239
+ padding: "var(--liquid-glass-rim-width, 0.5px)",
240
+ background:
241
+ // iOS 27 liquid-glass rim. Each gradient runs ALONG its edges and
242
+ // fades to nothing at the corners, uniform in between:
243
+ // - to right → white streak on the top + bottom runs
244
+ // - to bottom → dark streak on the left + right runs
245
+ // Streak colors are overridable per-consumer via the
246
+ // --liquid-glass-rim-light / --liquid-glass-rim-dark variables;
247
+ // --liquid-glass-rim-fade sets how far from each corner the
248
+ // streak takes to reach full strength (smaller = longer streak).
249
+ "linear-gradient(to right, rgba(255,255,255,0), var(--liquid-glass-rim-light, rgba(255,255,255,0.25)) var(--liquid-glass-rim-fade, 18%), var(--liquid-glass-rim-light, rgba(255,255,255,0.25)) calc(100% - var(--liquid-glass-rim-fade, 18%)), rgba(255,255,255,0)), " +
250
+ "linear-gradient(to bottom, rgba(0,0,0,0), var(--liquid-glass-rim-dark, rgba(0,0,0,0.2)) var(--liquid-glass-rim-fade, 18%), var(--liquid-glass-rim-dark, rgba(0,0,0,0.2)) calc(100% - var(--liquid-glass-rim-fade, 18%)), rgba(0,0,0,0))",
251
+ WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
252
+ WebkitMaskComposite: "xor",
253
+ mask: "linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)",
254
+ }}
255
+ />
256
+ </div>
257
+
258
+ {refractionActive && (
259
+ <svg className="absolute size-0 overflow-hidden" aria-hidden>
260
+ <filter id={filterId} x="0" y="0" width="100%" height="100%" colorInterpolationFilters="sRGB">
261
+ <feImage href={mapUrl} x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map" />
262
+ <feDisplacementMap
263
+ in="SourceGraphic"
264
+ in2="map"
265
+ scale={refraction}
266
+ xChannelSelector="R"
267
+ yChannelSelector="G"
268
+ result="displaced"
269
+ />
270
+ <feGaussianBlur in="displaced" stdDeviation="0.15" />
271
+ </filter>
272
+ </svg>
273
+ )}
274
+ </>
275
+ );
276
+ });
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import type * as React from "react";
5
+ import { cn } from "@/lib/utils";
6
+ import { Input, type InputProps } from "@/components/ui/input";
7
+ import { Textarea, type TextareaProps } from "@/components/ui/textarea";
8
+
9
+ const inputGroupAddonVariants = cva(
10
+ "flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80",
11
+ {
12
+ defaultVariants: {
13
+ align: "inline-start",
14
+ },
15
+ variants: {
16
+ align: {
17
+ "block-end":
18
+ "order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
19
+ "block-start":
20
+ "order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
21
+ "inline-end":
22
+ "order-last pe-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
23
+ "inline-start":
24
+ "order-first ps-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
25
+ },
26
+ },
27
+ },
28
+ );
29
+
30
+ export function InputGroup({
31
+ className,
32
+ ...props
33
+ }: React.ComponentProps<"div">): React.ReactElement {
34
+ return (
35
+ <div
36
+ className={cn(
37
+ "relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
38
+ className,
39
+ )}
40
+ data-slot="input-group"
41
+ role="group"
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ export function InputGroupAddon({
48
+ className,
49
+ align = "inline-start",
50
+ ...props
51
+ }: React.ComponentProps<"div"> &
52
+ VariantProps<typeof inputGroupAddonVariants>): React.ReactElement {
53
+ return (
54
+ <div
55
+ className={cn(inputGroupAddonVariants({ align }), className)}
56
+ data-align={align}
57
+ data-slot="input-group-addon"
58
+ onMouseDown={(e: React.MouseEvent<HTMLDivElement>) => {
59
+ const target = e.target as HTMLElement;
60
+ const isInteractive = target.closest(
61
+ "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']",
62
+ );
63
+ if (isInteractive) return;
64
+ e.preventDefault();
65
+ const parent = e.currentTarget.parentElement;
66
+ const input = parent?.querySelector<
67
+ HTMLInputElement | HTMLTextAreaElement
68
+ >("input, textarea");
69
+ if (input && !parent?.querySelector("input:focus, textarea:focus")) {
70
+ input.focus();
71
+ }
72
+ }}
73
+ {...props}
74
+ />
75
+ );
76
+ }
77
+
78
+ export function InputGroupText({
79
+ className,
80
+ ...props
81
+ }: React.ComponentProps<"span">): React.ReactElement {
82
+ return (
83
+ <span
84
+ className={cn(
85
+ "line-clamp-1 flex items-center gap-2 whitespace-nowrap text-muted-foreground leading-none in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5",
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ export function InputGroupInput({
94
+ className,
95
+ ...props
96
+ }: InputProps): React.ReactElement {
97
+ return <Input className={className} unstyled {...props} />;
98
+ }
99
+
100
+ export function InputGroupTextarea({
101
+ className,
102
+ ...props
103
+ }: TextareaProps): React.ReactElement {
104
+ return <Textarea className={className} unstyled {...props} />;
105
+ }
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { Input as InputPrimitive } from "@base-ui/react/input";
4
+ import type * as React from "react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export type InputProps = Omit<
8
+ InputPrimitive.Props & React.RefAttributes<HTMLInputElement>,
9
+ "size"
10
+ > & {
11
+ size?: "sm" | "default" | "lg" | number;
12
+ unstyled?: boolean;
13
+ nativeInput?: boolean;
14
+ };
15
+
16
+ export function Input({
17
+ className,
18
+ size = "default",
19
+ unstyled = false,
20
+ nativeInput = false,
21
+ style,
22
+ ...props
23
+ }: InputProps): React.ReactElement {
24
+ const inputClassName = cn(
25
+ // The 5000000s background-color entry is the autofill-flash suppression
26
+ // hack; height/line-height/padding tween size-prop changes.
27
+ "h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none [transition:background-color_5000000s_ease-in-out_0s,height_300ms_var(--ease-smooth-out),line-height_300ms_var(--ease-smooth-out),padding_300ms_var(--ease-smooth-out),font-size_300ms_var(--ease-smooth-out)] placeholder:text-muted-foreground/72 motion-reduce:[transition:background-color_5000000s_ease-in-out_0s] sm:h-7.5 sm:leading-7.5",
28
+ size === "sm" &&
29
+ "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
30
+ size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
31
+ props.type === "search" &&
32
+ "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
33
+ props.type === "file" &&
34
+ "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm",
35
+ );
36
+
37
+ return (
38
+ <span
39
+ className={
40
+ cn(
41
+ !unstyled &&
42
+ "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-[box-shadow,border-color] duration-150 ease-smooth-out has-aria-invalid:animate-input-shake motion-reduce:animate-none motion-reduce:transition-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
43
+ className,
44
+ ) || undefined
45
+ }
46
+ data-size={size}
47
+ data-slot="input-control"
48
+ >
49
+ {nativeInput ? (
50
+ <input
51
+ className={inputClassName}
52
+ data-slot="input"
53
+ size={typeof size === "number" ? size : undefined}
54
+ style={typeof style === "function" ? undefined : style}
55
+ {...props}
56
+ />
57
+ ) : (
58
+ <InputPrimitive
59
+ className={inputClassName}
60
+ data-slot="input"
61
+ size={typeof size === "number" ? size : undefined}
62
+ style={style}
63
+ {...props}
64
+ />
65
+ )}
66
+ </span>
67
+ );
68
+ }
69
+
70
+ export { InputPrimitive };
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { mergeProps } from "@base-ui/react/merge-props";
4
+ import { useRender } from "@base-ui/react/use-render";
5
+ import type React from "react";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ export function Label({
9
+ className,
10
+ render,
11
+ sound = false,
12
+ ...props
13
+ }: useRender.ComponentProps<"label"> & {
14
+ /**
15
+ * Emit the cuelume hover tick. Opt-in: a label is a click target for its
16
+ * control, and that control already owns the interaction cue (Checkbox
17
+ * plays `toggle` on check/uncheck), so hover-ticking here double-sounds.
18
+ * Inert until the app calls bindSounds().
19
+ */
20
+ sound?: boolean;
21
+ }): React.ReactElement {
22
+ const defaultProps = {
23
+ className: cn(
24
+ "inline-flex cursor-pointer items-center gap-2 font-medium text-base/4.5 text-foreground sm:text-sm/4",
25
+ className,
26
+ ),
27
+ "data-cuelume-hover": sound ? "tick" : undefined,
28
+ "data-slot": "label",
29
+ };
30
+
31
+ return useRender({
32
+ defaultTagName: "label",
33
+ props: mergeProps<"label">(defaultProps, props),
34
+ render,
35
+ });
36
+ }