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.
Files changed (64) hide show
  1. package/dist/components/A2UI/catalog.js +98 -0
  2. package/dist/components/A2UI/mapping.js +5 -0
  3. package/dist/components/Checkbox/Checkbox.d.ts +1 -0
  4. package/dist/components/Checkbox/Checkbox.js +20 -4
  5. package/dist/components/FileUpload/FileUpload.js +2 -1
  6. package/dist/components/FileUpload/FileUpload.module.css +18 -14
  7. package/dist/components/Page/Page.module.css +9 -3
  8. package/dist/components/Popover/Popover.d.ts +1 -1
  9. package/dist/components/Popover/Popover.js +53 -23
  10. package/dist/components/Rating/Rating.d.ts +17 -0
  11. package/dist/components/Rating/Rating.js +126 -0
  12. package/dist/components/Rating/Rating.module.css +131 -0
  13. package/dist/components/Rating/index.d.ts +1 -0
  14. package/dist/components/Rating/index.js +1 -0
  15. package/dist/components/Table/Table.d.ts +2 -3
  16. package/dist/components/Table/Table.js +2 -20
  17. package/dist/components/ToggleGroup/ToggleGroup.d.ts +22 -0
  18. package/dist/components/ToggleGroup/ToggleGroup.js +157 -0
  19. package/dist/components/ToggleGroup/ToggleGroup.module.css +81 -0
  20. package/dist/components/ToggleGroup/index.d.ts +1 -0
  21. package/dist/components/ToggleGroup/index.js +1 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +2 -0
  24. package/dist/lib/filter/ast/array-filter.d.ts +7 -0
  25. package/dist/lib/filter/ast/array-filter.js +16 -0
  26. package/dist/lib/filter/ast/evaluate.d.ts +6 -0
  27. package/dist/lib/filter/ast/evaluate.js +35 -0
  28. package/dist/lib/filter/ast/index.d.ts +5 -0
  29. package/dist/lib/filter/ast/index.js +4 -0
  30. package/dist/lib/filter/ast/operators.d.ts +7 -0
  31. package/dist/{components/Table/utils/filterAst.js → lib/filter/ast/operators.js} +0 -52
  32. package/dist/lib/filter/ast/simple.d.ts +7 -0
  33. package/dist/lib/filter/ast/simple.js +26 -0
  34. package/dist/lib/filter/ast/types.d.ts +24 -0
  35. package/dist/lib/filter/ast/types.js +1 -0
  36. package/dist/lib/filter/index.d.ts +7 -0
  37. package/dist/lib/filter/index.js +7 -0
  38. package/dist/lib/filter/ui/FilterBuilder.d.ts +25 -0
  39. package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterBuilder.js +3 -3
  40. package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterConditionRow.d.ts +1 -1
  41. package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterConditionRow.js +4 -4
  42. package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterGroup.d.ts +9 -9
  43. package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterGroup.js +7 -7
  44. package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterSheetNested.d.ts +3 -3
  45. package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterSheetNested.js +4 -4
  46. package/dist/lib/filter/ui/convert.d.ts +16 -0
  47. package/dist/lib/filter/ui/convert.js +60 -0
  48. package/dist/lib/filter/ui/index.d.ts +5 -0
  49. package/dist/lib/filter/ui/index.js +5 -0
  50. package/dist/lib/filter/ui/utils/tree-utils.d.ts +15 -0
  51. package/dist/styles/globals.css +3 -1
  52. package/dist/tsconfig.build.tsbuildinfo +1 -1
  53. package/dist/vitest.config.js +6 -1
  54. package/package.json +10 -3
  55. package/dist/components/Table/FilterBuilder/FilterBuilder.d.ts +0 -20
  56. package/dist/components/Table/FilterBuilder/utils/tree-utils.d.ts +0 -15
  57. package/dist/components/Table/utils/arrayFilter.d.ts +0 -7
  58. package/dist/components/Table/utils/arrayFilter.js +0 -21
  59. package/dist/components/Table/utils/filterAst.d.ts +0 -33
  60. /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterBuilder.module.css +0 -0
  61. /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterConditionRow.module.css +0 -0
  62. /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterGroup.module.css +0 -0
  63. /package/dist/{components/Table/FilterBuilder → lib/filter/ui}/FilterSheet.module.css +0 -0
  64. /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
- return (_jsxs(Label, { className: clsx(styles.checkboxWrapper, disabled && styles.disabled, className), htmlFor: id, children: [_jsx("input", Object.assign({ ref: ref, 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: _jsx(Check, { className: styles.icon }) }), label && _jsx("span", { className: styles.labelOverride, children: label })] }));
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
- inset: -50%;
64
- width: 200%;
65
- height: 200%;
66
- background-image: radial-gradient(1px 1px at 20% 30%, #fff 100%, transparent), radial-gradient(1px 1px at 40% 70%, #fff 100%, transparent), radial-gradient(1px 1px at 60% 40%, #fff 100%, transparent), radial-gradient(1px 1px at 80% 80%, #fff 100%, transparent), radial-gradient(1px 1px at 10% 10%, #fff 100%, transparent), radial-gradient(1px 1px at 30% 50%, #fff 100%, transparent), radial-gradient(1px 1px at 50% 20%, #fff 100%, transparent), radial-gradient(1px 1px at 70% 90%, #fff 100%, transparent), radial-gradient(1px 1px at 90% 30%, #fff 100%, transparent), radial-gradient(1.5px 1.5px at 15% 75%, #fff 100%, transparent), radial-gradient(1px 1px at 85% 15%, #fff 100%, transparent), radial-gradient(1px 1px at 55% 65%, #fff 100%, transparent);
67
- background-size: 50% 50%;
68
- opacity: 0.5;
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
- background-position: 33% 33%;
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
- inset: -50%;
90
- width: 200%;
91
- height: 200%;
92
- background-image: radial-gradient(1px 1px at 25% 35%, #fff 100%, transparent), radial-gradient(1px 1px at 45% 75%, #fff 100%, transparent), radial-gradient(1px 1px at 65% 45%, #fff 100%, transparent), radial-gradient(1px 1px at 85% 85%, #fff 100%, transparent), radial-gradient(1px 1px at 15% 15%, #fff 100%, transparent), radial-gradient(1px 1px at 35% 55%, #fff 100%, transparent), radial-gradient(1px 1px at 55% 25%, #fff 100%, transparent), radial-gradient(1px 1px at 75% 95%, #fff 100%, transparent), radial-gradient(1px 1px at 95% 35%, #fff 100%, transparent), radial-gradient(1.5px 1.5px at 20% 80%, #fff 100%, transparent), radial-gradient(1px 1px at 90% 20%, #fff 100%, transparent), radial-gradient(1px 1px at 60% 70%, #fff 100%, transparent);
93
- background-size: 50% 50%;
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-8) var(--space-4);
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 isTop = placement.startsWith("top");
25
- if (isTop) {
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
- // Vertical Clamping (Fail-safe)
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
- const align = placement.split("-")[1];
52
- if (align === "start") {
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";