doom-design-system 0.6.0 → 0.7.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/dist/components/A2UI/catalog.js +98 -0
- package/dist/components/A2UI/mapping.js +5 -0
- package/dist/components/Checkbox/Checkbox.d.ts +1 -0
- package/dist/components/Checkbox/Checkbox.js +20 -4
- package/dist/components/FileUpload/FileUpload.js +2 -1
- package/dist/components/FileUpload/FileUpload.module.css +18 -14
- package/dist/components/Page/Page.module.css +9 -3
- package/dist/components/Popover/Popover.d.ts +1 -1
- package/dist/components/Popover/Popover.js +53 -23
- package/dist/components/Rating/Rating.d.ts +17 -0
- package/dist/components/Rating/Rating.js +126 -0
- package/dist/components/Rating/Rating.module.css +131 -0
- package/dist/components/Rating/index.d.ts +1 -0
- package/dist/components/Rating/index.js +1 -0
- package/dist/components/Table/Table.d.ts +2 -3
- package/dist/components/Table/Table.js +2 -20
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +22 -0
- package/dist/components/ToggleGroup/ToggleGroup.js +157 -0
- package/dist/components/ToggleGroup/ToggleGroup.module.css +81 -0
- package/dist/components/ToggleGroup/index.d.ts +1 -0
- package/dist/components/ToggleGroup/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/filter/ast/array-filter.d.ts +7 -0
- package/dist/lib/filter/ast/array-filter.js +16 -0
- package/dist/lib/filter/ast/evaluate.d.ts +6 -0
- package/dist/lib/filter/ast/evaluate.js +35 -0
- package/dist/lib/filter/ast/index.d.ts +5 -0
- package/dist/lib/filter/ast/index.js +4 -0
- package/dist/lib/filter/ast/operators.d.ts +7 -0
- package/dist/{components/Table/utils/filterAst.js → lib/filter/ast/operators.js} +0 -52
- package/dist/lib/filter/ast/simple.d.ts +7 -0
- package/dist/lib/filter/ast/simple.js +26 -0
- package/dist/lib/filter/ast/types.d.ts +24 -0
- package/dist/lib/filter/ast/types.js +1 -0
- package/dist/lib/filter/index.d.ts +7 -0
- package/dist/lib/filter/index.js +7 -0
- package/dist/lib/filter/ui/FilterBuilder.d.ts +25 -0
- package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterBuilder.js +3 -3
- package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterConditionRow.d.ts +1 -1
- package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterConditionRow.js +4 -4
- package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterGroup.d.ts +9 -9
- package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterGroup.js +7 -7
- package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterSheetNested.d.ts +3 -3
- package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterSheetNested.js +4 -4
- package/dist/lib/filter/ui/convert.d.ts +16 -0
- package/dist/lib/filter/ui/convert.js +60 -0
- package/dist/lib/filter/ui/index.d.ts +5 -0
- package/dist/lib/filter/ui/index.js +5 -0
- package/dist/lib/filter/ui/utils/tree-utils.d.ts +15 -0
- package/dist/styles/globals.css +3 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/vitest.config.js +6 -1
- package/package.json +10 -3
- package/dist/components/Table/FilterBuilder/FilterBuilder.d.ts +0 -20
- package/dist/components/Table/FilterBuilder/utils/tree-utils.d.ts +0 -15
- package/dist/components/Table/utils/arrayFilter.d.ts +0 -7
- package/dist/components/Table/utils/arrayFilter.js +0 -21
- package/dist/components/Table/utils/filterAst.d.ts +0 -33
- /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterBuilder.module.css +0 -0
- /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterConditionRow.module.css +0 -0
- /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterGroup.module.css +0 -0
- /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterSheet.module.css +0 -0
- /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/utils/tree-utils.js +0 -0
|
@@ -408,6 +408,104 @@ export const componentCatalog = [
|
|
|
408
408
|
{ name: "disabled", type: "boolean", description: "Disable slider" },
|
|
409
409
|
],
|
|
410
410
|
},
|
|
411
|
+
{
|
|
412
|
+
type: "rating",
|
|
413
|
+
name: "Rating",
|
|
414
|
+
category: "primitives",
|
|
415
|
+
description: "Icon-based rating with half-value support",
|
|
416
|
+
props: [
|
|
417
|
+
{
|
|
418
|
+
name: "value",
|
|
419
|
+
type: "number",
|
|
420
|
+
description: "Controlled rating value",
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "defaultValue",
|
|
424
|
+
type: "number",
|
|
425
|
+
description: "Uncontrolled initial value",
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: "count",
|
|
429
|
+
type: "number",
|
|
430
|
+
description: "Number of icons",
|
|
431
|
+
default: "5",
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "allowHalf",
|
|
435
|
+
type: "boolean",
|
|
436
|
+
description: "Enable half-value selection",
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "size",
|
|
440
|
+
type: "'sm' | 'md' | 'lg'",
|
|
441
|
+
description: "Icon size",
|
|
442
|
+
default: "md",
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: "readOnly",
|
|
446
|
+
type: "boolean",
|
|
447
|
+
description: "Read-only display mode",
|
|
448
|
+
},
|
|
449
|
+
{ name: "disabled", type: "boolean", description: "Disable rating" },
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
type: "toggle-group",
|
|
454
|
+
name: "ToggleGroup",
|
|
455
|
+
category: "primitives",
|
|
456
|
+
description: "Grouped toggle buttons with single or multi select",
|
|
457
|
+
props: [
|
|
458
|
+
{
|
|
459
|
+
name: "type",
|
|
460
|
+
type: "'single' | 'multiple'",
|
|
461
|
+
required: true,
|
|
462
|
+
description: "Selection mode",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "value",
|
|
466
|
+
type: "string | string[]",
|
|
467
|
+
description: "Controlled value",
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "defaultValue",
|
|
471
|
+
type: "string | string[]",
|
|
472
|
+
description: "Default value (uncontrolled)",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "size",
|
|
476
|
+
type: "'sm' | 'md' | 'lg'",
|
|
477
|
+
description: "Control size",
|
|
478
|
+
default: "md",
|
|
479
|
+
},
|
|
480
|
+
{ name: "disabled", type: "boolean", description: "Disable all items" },
|
|
481
|
+
{
|
|
482
|
+
name: "children",
|
|
483
|
+
type: "A2UIChildRef",
|
|
484
|
+
description: "toggle-group-item children",
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
type: "toggle-group-item",
|
|
490
|
+
name: "ToggleGroupItem",
|
|
491
|
+
category: "primitives",
|
|
492
|
+
description: "Single toggle option within a ToggleGroup",
|
|
493
|
+
usesTextProp: true,
|
|
494
|
+
props: [
|
|
495
|
+
{
|
|
496
|
+
name: "value",
|
|
497
|
+
type: "string",
|
|
498
|
+
required: true,
|
|
499
|
+
description: "Item value",
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: "text",
|
|
503
|
+
type: "A2UITextValue",
|
|
504
|
+
description: "Label text",
|
|
505
|
+
},
|
|
506
|
+
{ name: "disabled", type: "boolean", description: "Disable this item" },
|
|
507
|
+
],
|
|
508
|
+
},
|
|
411
509
|
{
|
|
412
510
|
type: "link",
|
|
413
511
|
name: "Link",
|
|
@@ -22,6 +22,7 @@ import { Link } from "../Link/Link.js";
|
|
|
22
22
|
// Feedback - Static display
|
|
23
23
|
import { ProgressBar } from "../ProgressBar/ProgressBar.js";
|
|
24
24
|
import { RadioGroup, RadioGroupItem } from "../RadioGroup/RadioGroup.js";
|
|
25
|
+
import { Rating } from "../Rating/Rating.js";
|
|
25
26
|
import { Select } from "../Select/Select.js";
|
|
26
27
|
import { Sidebar, SidebarFooter, SidebarGroup, SidebarHeader, SidebarItem, SidebarMobileTrigger, SidebarNav, SidebarSection, } from "../Sidebar/Sidebar.js";
|
|
27
28
|
import { Skeleton } from "../Skeleton/Skeleton.js";
|
|
@@ -34,6 +35,7 @@ import { Table } from "../Table/Table.js";
|
|
|
34
35
|
import { Tabs, TabsBody, TabsContent, TabsList, TabsTrigger, } from "../Tabs/Tabs.js";
|
|
35
36
|
import { Text } from "../Text/Text.js";
|
|
36
37
|
import { Textarea } from "../Textarea/Textarea.js";
|
|
38
|
+
import { ToggleGroup, ToggleGroupItem } from "../ToggleGroup/ToggleGroup.js";
|
|
37
39
|
import { Tooltip } from "../Tooltip/Tooltip.js";
|
|
38
40
|
// Wrappers (for components needing JSON-to-function adaptation)
|
|
39
41
|
import { ChartWrapper } from "./wrappers/index.js";
|
|
@@ -52,11 +54,14 @@ export const componentMap = {
|
|
|
52
54
|
link: Link,
|
|
53
55
|
"radio-group": RadioGroup,
|
|
54
56
|
"radio-group-item": RadioGroupItem,
|
|
57
|
+
rating: Rating,
|
|
55
58
|
slider: Slider,
|
|
56
59
|
spinner: Spinner,
|
|
57
60
|
switch: Switch,
|
|
58
61
|
text: Text,
|
|
59
62
|
textarea: Textarea,
|
|
63
|
+
"toggle-group": ToggleGroup,
|
|
64
|
+
"toggle-group-item": ToggleGroupItem,
|
|
60
65
|
tooltip: Tooltip,
|
|
61
66
|
// Layout
|
|
62
67
|
box: "div",
|
|
@@ -2,5 +2,6 @@ import React from "react";
|
|
|
2
2
|
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
3
3
|
label?: string;
|
|
4
4
|
error?: boolean;
|
|
5
|
+
indeterminate?: boolean;
|
|
5
6
|
}
|
|
6
7
|
export declare const Checkbox: React.ForwardRefExoticComponent<CheckboxProps & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -12,14 +12,30 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
};
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
14
|
import clsx from "clsx";
|
|
15
|
-
import { Check } from "lucide-react";
|
|
16
|
-
import { forwardRef, useId } from "react";
|
|
15
|
+
import { Check, Minus } from "lucide-react";
|
|
16
|
+
import { forwardRef, useCallback, useEffect, useId, useRef, } from "react";
|
|
17
17
|
import { Label } from "../Label/index.js";
|
|
18
18
|
import styles from "./Checkbox.module.css";
|
|
19
19
|
export const Checkbox = forwardRef((_a, ref) => {
|
|
20
|
-
var { className, label, error, disabled, checked, defaultChecked, onChange, id: propsId } = _a, props = __rest(_a, ["className", "label", "error", "disabled", "checked", "defaultChecked", "onChange", "id"]);
|
|
20
|
+
var { className, label, error, disabled, checked, defaultChecked, indeterminate, onChange, id: propsId } = _a, props = __rest(_a, ["className", "label", "error", "disabled", "checked", "defaultChecked", "indeterminate", "onChange", "id"]);
|
|
21
21
|
const generatedId = useId();
|
|
22
22
|
const id = propsId || generatedId;
|
|
23
|
-
|
|
23
|
+
const internalRef = useRef(null);
|
|
24
|
+
const mergedRef = useCallback((node) => {
|
|
25
|
+
internalRef.current = node;
|
|
26
|
+
if (typeof ref === "function") {
|
|
27
|
+
ref(node);
|
|
28
|
+
}
|
|
29
|
+
else if (ref) {
|
|
30
|
+
ref.current =
|
|
31
|
+
node;
|
|
32
|
+
}
|
|
33
|
+
}, [ref]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (internalRef.current) {
|
|
36
|
+
internalRef.current.indeterminate = !!(indeterminate && !checked);
|
|
37
|
+
}
|
|
38
|
+
}, [indeterminate, checked]);
|
|
39
|
+
return (_jsxs(Label, { className: clsx(styles.checkboxWrapper, disabled && styles.disabled, className), htmlFor: id, children: [_jsx("input", Object.assign({ ref: mergedRef, checked: checked, className: styles.checkboxInput, defaultChecked: defaultChecked, disabled: disabled, id: id, type: "checkbox", onChange: onChange }, props)), _jsx("span", { "aria-hidden": "true", className: clsx(styles.checkboxDisplay), children: indeterminate && !checked ? (_jsx(Minus, { className: styles.icon, "data-testid": "minus-icon" })) : (_jsx(Check, { className: styles.icon })) }), label && _jsx("span", { className: styles.labelOverride, children: label })] }));
|
|
24
40
|
});
|
|
25
41
|
Checkbox.displayName = "Checkbox";
|
|
@@ -92,8 +92,9 @@ export const FileUpload = ({ label, helperText, accept, maxSize, multiple = fals
|
|
|
92
92
|
}
|
|
93
93
|
}, [showPreview]);
|
|
94
94
|
const isFileAccepted = (file) => {
|
|
95
|
-
if (!accept)
|
|
95
|
+
if (!accept) {
|
|
96
96
|
return true;
|
|
97
|
+
}
|
|
97
98
|
const acceptedTypes = accept.split(",").map((t) => t.trim().toLowerCase());
|
|
98
99
|
const fileName = file.name.toLowerCase();
|
|
99
100
|
const mimeType = file.type.toLowerCase();
|
|
@@ -60,21 +60,23 @@
|
|
|
60
60
|
.window.void .body::before, .window.void .body::after {
|
|
61
61
|
content: "";
|
|
62
62
|
position: absolute;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
background
|
|
68
|
-
|
|
63
|
+
top: 50%;
|
|
64
|
+
left: 50%;
|
|
65
|
+
width: 2px;
|
|
66
|
+
height: 2px;
|
|
67
|
+
background: transparent;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
box-shadow: -80px -120px #fff, 40px -60px #fff, -150px 80px #fff, 120px 40px #fff, -30px -180px #fff, 90px 130px #fff, -170px -20px #fff, 60px -140px #fff, 180px -80px #fff, -100px 160px #fff, 140px 100px #fff, -50px -40px #fff, 30px 170px #fff, -140px -150px #fff, 170px 60px #fff, -60px 100px #fff, 100px -170px #fff, -180px 140px #fff, 50px 50px #fff, -120px -90px #fff, 160px -30px #fff, -20px 130px #fff, 80px -100px #fff, -160px 40px #fff;
|
|
70
|
+
opacity: 0.6;
|
|
69
71
|
z-index: 0;
|
|
70
72
|
animation: consume-stars 6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
71
73
|
pointer-events: none;
|
|
72
74
|
}
|
|
73
75
|
.window.void .body::after {
|
|
76
|
+
box-shadow: -70px -90px #fff, 50px -130px #fff, -130px 60px #fff, 110px 70px #fff, -20px -160px #fff, 80px 150px #fff, -160px -50px #fff, 70px -110px #fff, 160px -60px #fff, -90px 140px #fff, 130px 120px #fff, -40px -30px #fff, 20px 160px #fff, -150px -130px #fff, 150px 40px #fff, -50px 80px #fff, 90px -150px #fff, -170px 120px #fff;
|
|
74
77
|
animation-name: consume-stars-alt;
|
|
75
78
|
animation-delay: -3s;
|
|
76
|
-
|
|
77
|
-
opacity: 0.3;
|
|
79
|
+
opacity: 0.35;
|
|
78
80
|
}
|
|
79
81
|
.window.void .body .starField {
|
|
80
82
|
position: absolute;
|
|
@@ -86,19 +88,21 @@
|
|
|
86
88
|
.window.void .body .starField::before, .window.void .body .starField::after {
|
|
87
89
|
content: "";
|
|
88
90
|
position: absolute;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
background
|
|
91
|
+
top: 50%;
|
|
92
|
+
left: 50%;
|
|
93
|
+
width: 2px;
|
|
94
|
+
height: 2px;
|
|
95
|
+
background: transparent;
|
|
96
|
+
border-radius: 50%;
|
|
97
|
+
box-shadow: -95px -105px #fff, 55px -75px #fff, -140px 95px #fff, 105px 55px #fff, -45px -170px #fff, 75px 145px #fff, -155px -35px #fff, 45px -125px #fff, 165px -70px #fff, -85px 155px #fff, 125px 85px #fff, -35px -55px #fff, 15px 175px #fff, -125px -140px #fff, 155px 50px #fff, -75px 90px #fff, 85px -160px #fff, -165px 130px #fff, 35px 65px #fff, -110px -80px #fff, 145px -15px #fff, -15px 115px #fff, 65px -85px #fff, -145px 55px #fff;
|
|
94
98
|
opacity: 0.5;
|
|
95
99
|
animation: consume-stars 6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
96
100
|
animation-delay: -1.5s;
|
|
97
101
|
}
|
|
98
102
|
.window.void .body .starField::after {
|
|
103
|
+
box-shadow: -65px -80px #fff, 45px -140px #fff, -120px 70px #fff, 100px 80px #fff, -35px -150px #fff, 70px 160px #fff, -150px -40px #fff, 55px -100px #fff, 150px -50px #fff, -80px 130px #fff, 120px 110px #fff, -55px -45px #fff;
|
|
99
104
|
animation-name: consume-stars-alt;
|
|
100
105
|
animation-delay: -4.5s;
|
|
101
|
-
background-position: 40% 40%;
|
|
102
106
|
opacity: 0.3;
|
|
103
107
|
}
|
|
104
108
|
.window.void .headerContent,
|
|
@@ -4,16 +4,22 @@
|
|
|
4
4
|
width: 100%;
|
|
5
5
|
}
|
|
6
6
|
.default {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
7
9
|
width: 90%;
|
|
8
10
|
max-width: var(--page-max-width);
|
|
9
|
-
backdrop-filter: blur(var(--blur-standard));
|
|
10
11
|
margin: 0 auto;
|
|
11
|
-
padding: var(--space-
|
|
12
|
+
padding: 0 var(--space-4) var(--space-4);
|
|
13
|
+
}
|
|
14
|
+
.default > *:first-child {
|
|
15
|
+
min-height: var(--header-height);
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
flex-shrink: 0;
|
|
12
19
|
}
|
|
13
20
|
@media (max-width: 1024px) {
|
|
14
21
|
.default {
|
|
15
22
|
width: 95%;
|
|
16
|
-
padding: var(--space-4);
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
.fullWidth {
|
|
@@ -4,7 +4,7 @@ interface PopoverProps {
|
|
|
4
4
|
content: React.ReactNode;
|
|
5
5
|
isOpen: boolean;
|
|
6
6
|
onClose: () => void;
|
|
7
|
-
placement?: "bottom-start" | "bottom-end" | "bottom-center" | "top-start" | "top-end" | "top-center";
|
|
7
|
+
placement?: "bottom-start" | "bottom-end" | "bottom-center" | "top-start" | "top-end" | "top-center" | "right-start" | "right-end" | "right-center" | "left-start" | "left-end" | "left-center";
|
|
8
8
|
offset?: number;
|
|
9
9
|
}
|
|
10
10
|
export declare function Popover({ trigger, content, isOpen, onClose, placement, offset, }: PopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -19,10 +19,10 @@ export function Popover({ trigger, content, isOpen, onClose, placement = "bottom
|
|
|
19
19
|
let top = 0;
|
|
20
20
|
let left = 0;
|
|
21
21
|
let origin = "top center";
|
|
22
|
-
// Edge Config
|
|
23
22
|
const padding = 16;
|
|
24
|
-
const
|
|
25
|
-
|
|
23
|
+
const side = placement.split("-")[0];
|
|
24
|
+
const align = placement.split("-")[1];
|
|
25
|
+
if (side === "top") {
|
|
26
26
|
top = triggerRect.top - contentRect.height - offset;
|
|
27
27
|
origin = "bottom";
|
|
28
28
|
if (top < 0) {
|
|
@@ -30,7 +30,7 @@ export function Popover({ trigger, content, isOpen, onClose, placement = "bottom
|
|
|
30
30
|
origin = "top";
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
else {
|
|
33
|
+
else if (side === "bottom") {
|
|
34
34
|
top = triggerRect.bottom + offset;
|
|
35
35
|
origin = "top";
|
|
36
36
|
if (top + contentRect.height > viewportHeight) {
|
|
@@ -41,37 +41,68 @@ export function Popover({ trigger, content, isOpen, onClose, placement = "bottom
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
else if (side === "right") {
|
|
45
|
+
left = triggerRect.right + offset;
|
|
46
|
+
origin = "left";
|
|
47
|
+
if (left + contentRect.width > viewportWidth) {
|
|
48
|
+
left = triggerRect.left - contentRect.width - offset;
|
|
49
|
+
origin = "right";
|
|
50
|
+
if (left < 0) {
|
|
51
|
+
left = padding;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (side === "left") {
|
|
56
|
+
left = triggerRect.left - contentRect.width - offset;
|
|
57
|
+
origin = "right";
|
|
58
|
+
if (left < 0) {
|
|
59
|
+
left = triggerRect.right + offset;
|
|
60
|
+
origin = "left";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (side === "top" || side === "bottom") {
|
|
64
|
+
if (align === "start") {
|
|
65
|
+
left = triggerRect.left;
|
|
66
|
+
origin += " left";
|
|
67
|
+
}
|
|
68
|
+
else if (align === "end") {
|
|
69
|
+
left = triggerRect.right - contentRect.width;
|
|
70
|
+
origin += " right";
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
left = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2;
|
|
74
|
+
origin += " center";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
if (align === "start") {
|
|
79
|
+
top = triggerRect.top;
|
|
80
|
+
origin = "top " + origin;
|
|
81
|
+
}
|
|
82
|
+
else if (align === "end") {
|
|
83
|
+
top = triggerRect.bottom - contentRect.height;
|
|
84
|
+
origin = "bottom " + origin;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
top = triggerRect.top + triggerRect.height / 2 - contentRect.height / 2;
|
|
88
|
+
origin = "center " + origin;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
45
91
|
if (top < padding) {
|
|
46
92
|
top = padding;
|
|
47
93
|
}
|
|
48
94
|
if (top + contentRect.height > viewportHeight - padding) {
|
|
49
95
|
top = viewportHeight - contentRect.height - padding;
|
|
50
96
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
left = triggerRect.left;
|
|
54
|
-
origin += " left";
|
|
55
|
-
}
|
|
56
|
-
else if (align === "end") {
|
|
57
|
-
left = triggerRect.right - contentRect.width;
|
|
58
|
-
origin += " right";
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
left = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2;
|
|
62
|
-
origin += " center";
|
|
97
|
+
if (left < padding) {
|
|
98
|
+
left = padding;
|
|
63
99
|
}
|
|
64
|
-
// Horizontal Clamping
|
|
65
100
|
if (left + contentRect.width > viewportWidth - padding) {
|
|
66
101
|
left = viewportWidth - contentRect.width - padding;
|
|
67
102
|
}
|
|
68
|
-
if (left < padding) {
|
|
69
|
-
left = padding;
|
|
70
|
-
}
|
|
71
103
|
setPosition({ top, left });
|
|
72
104
|
setTransformOrigin(origin);
|
|
73
105
|
}, [isOpen, placement, offset]);
|
|
74
|
-
// Use useLayoutEffect for layout measurements to prevent flash
|
|
75
106
|
useLayoutEffect(() => {
|
|
76
107
|
if (isOpen) {
|
|
77
108
|
updatePosition();
|
|
@@ -83,7 +114,6 @@ export function Popover({ trigger, content, isOpen, onClose, placement = "bottom
|
|
|
83
114
|
window.removeEventListener("scroll", updatePosition, true);
|
|
84
115
|
};
|
|
85
116
|
}, [isOpen, updatePosition]);
|
|
86
|
-
// Handle click outside
|
|
87
117
|
useEffect(() => {
|
|
88
118
|
if (!isOpen) {
|
|
89
119
|
return;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type { ControlSize } from "../../styles/types";
|
|
4
|
+
export interface RatingProps {
|
|
5
|
+
value?: number;
|
|
6
|
+
defaultValue?: number;
|
|
7
|
+
onValueChange?: (value: number) => void;
|
|
8
|
+
count?: number;
|
|
9
|
+
icon?: LucideIcon;
|
|
10
|
+
allowHalf?: boolean;
|
|
11
|
+
size?: ControlSize;
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
"aria-label"?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const Rating: React.ForwardRefExoticComponent<RatingProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { Star } from "lucide-react";
|
|
5
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
6
|
+
import { Tooltip } from "../Tooltip/index.js";
|
|
7
|
+
import styles from "./Rating.module.css";
|
|
8
|
+
const iconSizeMap = {
|
|
9
|
+
sm: 16,
|
|
10
|
+
md: 20,
|
|
11
|
+
lg: 32,
|
|
12
|
+
};
|
|
13
|
+
export const Rating = React.forwardRef(function Rating({ value: controlledValue, defaultValue = 0, onValueChange, count = 5, icon: IconComponent = Star, allowHalf = false, size = "md", readOnly = false, disabled = false, className, "aria-label": ariaLabel, }, ref) {
|
|
14
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
15
|
+
const [hoverValue, setHoverValue] = useState(null);
|
|
16
|
+
const containerRef = useRef(null);
|
|
17
|
+
const mergedRef = React.useCallback((node) => {
|
|
18
|
+
containerRef.current = node;
|
|
19
|
+
if (typeof ref === "function") {
|
|
20
|
+
ref(node);
|
|
21
|
+
}
|
|
22
|
+
else if (ref) {
|
|
23
|
+
ref.current = node;
|
|
24
|
+
}
|
|
25
|
+
}, [ref]);
|
|
26
|
+
const isControlled = controlledValue !== undefined;
|
|
27
|
+
const currentValue = isControlled ? controlledValue : internalValue;
|
|
28
|
+
const displayValue = hoverValue !== null && !readOnly && !disabled ? hoverValue : currentValue;
|
|
29
|
+
const iconSize = iconSizeMap[size];
|
|
30
|
+
const setValue = useCallback((next) => {
|
|
31
|
+
if (disabled) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!isControlled) {
|
|
35
|
+
setInternalValue(next);
|
|
36
|
+
}
|
|
37
|
+
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(next);
|
|
38
|
+
}, [disabled, isControlled, onValueChange]);
|
|
39
|
+
const handleKeyDown = useCallback((e) => {
|
|
40
|
+
if (disabled || readOnly) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const step = allowHalf ? 0.5 : 1;
|
|
44
|
+
let newValue = currentValue;
|
|
45
|
+
switch (e.key) {
|
|
46
|
+
case "ArrowRight":
|
|
47
|
+
case "ArrowUp":
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
newValue = Math.min(currentValue + step, count);
|
|
50
|
+
break;
|
|
51
|
+
case "ArrowLeft":
|
|
52
|
+
case "ArrowDown":
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
newValue = Math.max(currentValue - step, 0);
|
|
55
|
+
break;
|
|
56
|
+
case "Home":
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
newValue = 0;
|
|
59
|
+
break;
|
|
60
|
+
case "End":
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
newValue = count;
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setValue(newValue);
|
|
68
|
+
if (containerRef.current) {
|
|
69
|
+
const selector = `[data-value="${newValue}"]`;
|
|
70
|
+
const target = containerRef.current.querySelector(selector);
|
|
71
|
+
target === null || target === void 0 ? void 0 : target.focus();
|
|
72
|
+
}
|
|
73
|
+
}, [disabled, readOnly, allowHalf, currentValue, count, setValue]);
|
|
74
|
+
if (readOnly) {
|
|
75
|
+
return (_jsx("div", { ref: ref, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : `${currentValue} out of ${count}`, className: clsx(styles.rating, styles[size], className), role: "img", children: Array.from({ length: count }, (_, i) => {
|
|
76
|
+
const position = i + 1;
|
|
77
|
+
const isFilled = currentValue >= position;
|
|
78
|
+
const isHalf = !isFilled && currentValue >= position - 0.5;
|
|
79
|
+
return (_jsx("span", { className: styles.iconWrapper, children: isHalf ? (_jsxs(_Fragment, { children: [_jsx("span", { className: clsx(styles.icon, styles.unfilled), children: React.createElement(IconComponent, {
|
|
80
|
+
size: iconSize,
|
|
81
|
+
strokeWidth: 2.5,
|
|
82
|
+
fill: "none",
|
|
83
|
+
}) }), _jsx("span", { className: clsx(styles.icon, styles.filled, styles.halfClip), children: React.createElement(IconComponent, {
|
|
84
|
+
size: iconSize,
|
|
85
|
+
strokeWidth: 2.5,
|
|
86
|
+
fill: "currentColor",
|
|
87
|
+
}) })] })) : (_jsx("span", { className: clsx(styles.icon, isFilled ? styles.filled : styles.unfilled), children: React.createElement(IconComponent, {
|
|
88
|
+
size: iconSize,
|
|
89
|
+
strokeWidth: 2.5,
|
|
90
|
+
fill: isFilled ? "currentColor" : "none",
|
|
91
|
+
}) })) }, position));
|
|
92
|
+
}) }));
|
|
93
|
+
}
|
|
94
|
+
return (_jsx("div", { ref: mergedRef, "aria-label": ariaLabel, className: clsx(styles.rating, styles[size], disabled && styles.disabled, className), role: "radiogroup", onMouseLeave: () => setHoverValue(null), children: allowHalf
|
|
95
|
+
? Array.from({ length: count }, (_, i) => {
|
|
96
|
+
const position = i + 1;
|
|
97
|
+
const halfValue = position - 0.5;
|
|
98
|
+
const fullValue = position;
|
|
99
|
+
return (_jsxs("span", { className: styles.iconWrapper, children: [_jsx("span", { "aria-hidden": "true", className: clsx(styles.icon, styles.unfilled), children: React.createElement(IconComponent, {
|
|
100
|
+
size: iconSize,
|
|
101
|
+
strokeWidth: 2.5,
|
|
102
|
+
fill: "none",
|
|
103
|
+
}) }), displayValue >= halfValue && (_jsx("span", { "aria-hidden": "true", className: clsx(styles.icon, styles.filled, styles.filledOverlay, displayValue >= fullValue ? undefined : styles.halfClip), children: React.createElement(IconComponent, {
|
|
104
|
+
size: iconSize,
|
|
105
|
+
strokeWidth: 2.5,
|
|
106
|
+
fill: "currentColor",
|
|
107
|
+
}) })), _jsx(Tooltip, { content: `${halfValue} out of ${count}`, placement: "top", children: _jsx("button", { "aria-checked": currentValue >= halfValue, "aria-label": `Rate ${halfValue} out of ${count}`, className: clsx(styles.radioButton, styles.halfButton, styles.halfLeft), "data-value": halfValue, disabled: disabled, role: "radio", tabIndex: currentValue === halfValue ||
|
|
108
|
+
(currentValue === 0 && halfValue === 0.5)
|
|
109
|
+
? 0
|
|
110
|
+
: -1, type: "button", onClick: () => setValue(halfValue), onKeyDown: handleKeyDown, onMouseEnter: () => setHoverValue(halfValue) }) }), _jsx(Tooltip, { content: `${fullValue} out of ${count}`, placement: "top", children: _jsx("button", { "aria-checked": currentValue >= fullValue, "aria-label": `Rate ${fullValue} out of ${count}`, className: clsx(styles.radioButton, styles.halfButton, styles.halfRight), "data-value": fullValue, disabled: disabled, role: "radio", tabIndex: currentValue === fullValue ? 0 : -1, type: "button", onClick: () => setValue(fullValue), onKeyDown: handleKeyDown, onMouseEnter: () => setHoverValue(fullValue) }) })] }, position));
|
|
111
|
+
})
|
|
112
|
+
: Array.from({ length: count }, (_, i) => {
|
|
113
|
+
const position = i + 1;
|
|
114
|
+
const isFilled = displayValue >= position;
|
|
115
|
+
const label = `${position} out of ${count}`;
|
|
116
|
+
return (_jsx(Tooltip, { content: label, placement: "top", children: _jsx("button", { "aria-checked": currentValue >= position, "aria-label": `Rate ${position} out of ${count}`, className: clsx(styles.iconButton, isFilled && styles.filled, !isFilled && styles.unfilled), "data-value": position, disabled: disabled, role: "radio", tabIndex: currentValue === position ||
|
|
117
|
+
(currentValue === 0 && position === 1)
|
|
118
|
+
? 0
|
|
119
|
+
: -1, type: "button", onClick: () => setValue(position), onKeyDown: handleKeyDown, onMouseEnter: () => setHoverValue(position), children: React.createElement(IconComponent, {
|
|
120
|
+
size: iconSize,
|
|
121
|
+
strokeWidth: 2.5,
|
|
122
|
+
fill: isFilled ? "currentColor" : "none",
|
|
123
|
+
}) }) }, position));
|
|
124
|
+
}) }));
|
|
125
|
+
});
|
|
126
|
+
Rating.displayName = "Rating";
|