@js-empire/emperor-ui 1.2.3 → 1.2.4

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 (157) hide show
  1. package/.cursor/rules/code-conventions.mdc +50 -0
  2. package/README.md +0 -15
  3. package/dist/emperor-ui.js +119 -71
  4. package/dist/emperor-ui.umd.cjs +27 -13
  5. package/dist/globals.css +1 -1
  6. package/dist/index-BXtdEByK.js +5 -0
  7. package/dist/index-CDB93OLO.js +55965 -0
  8. package/dist/index-CYORMghp.js +290 -0
  9. package/dist/index.d.ts +334 -33
  10. package/dist/src-UW24ZMRV-C1Pn8-w8.js +5 -0
  11. package/package.json +32 -2
  12. package/src/animations/blink.ts +26 -0
  13. package/src/animations/floating.ts +12 -0
  14. package/src/animations/index.ts +2 -0
  15. package/src/components/atoms/brand/brand.tsx +1 -1
  16. package/src/components/atoms/color-picker/color-picker.tsx +13 -0
  17. package/src/components/atoms/color-picker/free-color-picker.tsx +60 -0
  18. package/src/components/atoms/color-picker/index.ts +3 -0
  19. package/src/components/atoms/color-picker/preset-color-picker.tsx +64 -0
  20. package/src/components/atoms/color-picker/stories/color-picker.stories.tsx +49 -0
  21. package/src/components/atoms/color-picker/styles/color-picker.css +23 -0
  22. package/src/components/atoms/copy-button/copy-button.tsx +73 -0
  23. package/src/components/atoms/copy-button/index.ts +1 -0
  24. package/src/components/atoms/copy-button/stories/copy-button.stories.tsx +21 -0
  25. package/src/components/atoms/field/field.stories.tsx +27 -0
  26. package/src/components/atoms/field/field.tsx +11 -0
  27. package/src/components/atoms/field/index.ts +1 -0
  28. package/src/components/atoms/field/styles/classes.ts +9 -0
  29. package/src/components/atoms/field/styles/index.ts +1 -0
  30. package/src/components/atoms/filter/filter.tsx +92 -0
  31. package/src/components/atoms/filter/index.ts +3 -0
  32. package/src/components/atoms/filter/stories/filter.stories.tsx +97 -0
  33. package/src/components/atoms/filter/styles/classes.ts +20 -0
  34. package/src/components/atoms/filter/styles/index.ts +1 -0
  35. package/src/components/atoms/filter/units/autocomplete-filter.tsx +39 -0
  36. package/src/components/atoms/filter/units/checkbox-filter.tsx +32 -0
  37. package/src/components/atoms/filter/units/checkbox-group-filter.tsx +37 -0
  38. package/src/components/atoms/filter/units/date-filter.tsx +50 -0
  39. package/src/components/atoms/filter/units/index.ts +9 -0
  40. package/src/components/atoms/filter/units/numeric-filter.tsx +36 -0
  41. package/src/components/atoms/filter/units/range-filter.tsx +36 -0
  42. package/src/components/atoms/filter/units/search-filter.tsx +52 -0
  43. package/src/components/atoms/filter/units/select-filter.tsx +49 -0
  44. package/src/components/atoms/filter/units/switch-filter.tsx +33 -0
  45. package/src/components/atoms/index.ts +5 -0
  46. package/src/components/atoms/theme-switch/index.ts +1 -0
  47. package/src/components/atoms/theme-switch/styles/classes.ts +16 -0
  48. package/src/components/atoms/theme-switch/styles/index.ts +1 -0
  49. package/src/components/atoms/theme-switch/theme-switch.stories.tsx +26 -0
  50. package/src/components/atoms/theme-switch/theme-switch.tsx +54 -0
  51. package/src/components/atoms/uploader/avatar-label.tsx +3 -1
  52. package/src/components/atoms/uploader/stories/uploader.stories.tsx +1 -1
  53. package/src/components/atoms/uploader/upload-file-error-box.tsx +1 -1
  54. package/src/components/atoms/uploader/upload-file-input.tsx +1 -1
  55. package/src/components/atoms/uploader/upload-file-label.tsx +2 -1
  56. package/src/components/atoms/uploader/upload-file-listing.tsx +2 -1
  57. package/src/components/atoms/uploader/view-image-modal.tsx +2 -1
  58. package/src/components/molecules/index.ts +0 -1
  59. package/src/components/molecules/item-card/index.ts +6 -0
  60. package/src/components/molecules/item-card/item-actions-dropdown.tsx +57 -0
  61. package/src/components/molecules/item-card/item-banner.tsx +22 -0
  62. package/src/components/molecules/item-card/item-card-body.tsx +68 -0
  63. package/src/components/molecules/item-card/item-card-footer.tsx +55 -0
  64. package/src/components/molecules/item-card/item-card-header.tsx +61 -0
  65. package/src/components/molecules/item-card/item-card.tsx +83 -3
  66. package/src/components/molecules/item-card/loading-item.tsx +88 -0
  67. package/src/components/molecules/item-card/stories/item-card.stories.tsx +182 -0
  68. package/src/components/molecules/item-card/styles/classes.ts +138 -0
  69. package/src/components/molecules/item-card/styles/index.ts +1 -0
  70. package/src/components/molecules/nav-bar/sub-items-box.tsx +2 -1
  71. package/src/components/molecules/scaffold/index.ts +1 -0
  72. package/src/components/molecules/scaffold/scaffold.tsx +4 -17
  73. package/src/components/molecules/scaffold/styles/index.ts +1 -0
  74. package/src/components/molecules/scaffold/styles/scaffold-classes.ts +10 -0
  75. package/src/components/molecules/side-bar/compact-side-bar.tsx +3 -1
  76. package/src/components/molecules/side-bar/side-bar-drawer.tsx +6 -17
  77. package/src/components/molecules/side-bar/side-bar.stories.tsx +1 -1
  78. package/src/components/organisms/filters/filters.stories.tsx +32 -0
  79. package/src/components/organisms/filters/filters.tsx +36 -0
  80. package/src/components/organisms/filters/index.ts +1 -0
  81. package/src/components/organisms/filters/styles/classes.ts +9 -0
  82. package/src/components/organisms/filters/styles/index.ts +1 -0
  83. package/src/components/organisms/footer/copy-rights-box.tsx +1 -1
  84. package/src/components/organisms/footer/footer.tsx +1 -1
  85. package/src/components/organisms/footer/policies-box.tsx +2 -1
  86. package/src/components/organisms/footer/quick-links-box.tsx +2 -1
  87. package/src/components/organisms/footer/social-links-box.tsx +2 -1
  88. package/src/components/organisms/footer/stories/footer.stories.tsx +1 -1
  89. package/src/components/organisms/header/header.tsx +1 -8
  90. package/src/components/organisms/index.ts +1 -0
  91. package/src/components/organisms/listings/empty-listings.tsx +80 -0
  92. package/src/components/organisms/listings/index.ts +2 -0
  93. package/src/components/organisms/listings/listings.tsx +90 -9
  94. package/src/components/organisms/listings/stories/grid-listings.stories.tsx +153 -0
  95. package/src/components/organisms/listings/stories/list-listings.stories.tsx +171 -0
  96. package/src/components/organisms/listings/styles/classes.ts +41 -3
  97. package/src/constants/animations.ts +14 -0
  98. package/src/constants/card.tsx +26 -0
  99. package/src/constants/defaults.ts +1 -16
  100. package/src/constants/index.ts +2 -0
  101. package/src/hooks/index.ts +3 -0
  102. package/src/hooks/use-filters.ts +20 -0
  103. package/src/hooks/use-search-params-handler.tsx +186 -0
  104. package/src/hooks/use-uploader.tsx +1 -1
  105. package/src/hooks/use-window-size.tsx +53 -0
  106. package/src/i18n/locales/atoms/ar.ts +3 -0
  107. package/src/i18n/locales/atoms/en.ts +3 -0
  108. package/src/i18n/locales/organisms/ar.ts +7 -1
  109. package/src/i18n/locales/organisms/en.ts +7 -1
  110. package/src/mocks/constants.ts +103 -0
  111. package/src/mocks/index.ts +2 -0
  112. package/src/mocks/listings.tsx +154 -0
  113. package/src/mocks/types.ts +64 -0
  114. package/src/providers/config-provider.tsx +0 -8
  115. package/src/providers/emperor-ui-provider.tsx +16 -5
  116. package/src/providers/index.ts +1 -0
  117. package/src/providers/theme-provider.tsx +16 -0
  118. package/src/providers/uploader-provider.tsx +1 -1
  119. package/src/styles/hero.ts +1 -1
  120. package/src/styles/index.css +23 -0
  121. package/src/types/components/atoms/color-picker/color-picker.ts +12 -0
  122. package/src/types/components/atoms/color-picker/index.ts +1 -0
  123. package/src/types/components/atoms/field/field.ts +9 -0
  124. package/src/types/components/atoms/field/index.ts +1 -0
  125. package/src/types/components/atoms/filter/filter.ts +43 -0
  126. package/src/types/components/atoms/filter/index.ts +2 -0
  127. package/src/types/components/atoms/filter/select-filter.ts +8 -0
  128. package/src/types/components/atoms/index.ts +3 -0
  129. package/src/types/components/atoms/uploader.ts +1 -1
  130. package/src/types/components/index.ts +1 -0
  131. package/src/types/components/molecules/index.ts +1 -1
  132. package/src/types/components/molecules/item-card/item-card.ts +50 -0
  133. package/src/types/components/molecules/listings/listings.ts +21 -5
  134. package/src/types/components/molecules/side-bar/side-bar.ts +1 -1
  135. package/src/types/components/molecules/theme-switch/index.ts +1 -0
  136. package/src/types/components/molecules/theme-switch/theme-switch.ts +9 -0
  137. package/src/types/components/organisms/filters/filters.ts +11 -0
  138. package/src/types/components/organisms/filters/index.ts +1 -0
  139. package/src/types/components/organisms/index.ts +1 -0
  140. package/src/types/context/config.ts +3 -4
  141. package/src/types/context/index.ts +0 -1
  142. package/src/types/context/localization.ts +1 -0
  143. package/src/types/shared/components.ts +3 -0
  144. package/src/utils/uploader.ts +1 -1
  145. package/dist/features-animation-w9dWMd15.js +0 -1938
  146. package/dist/index-BY47HgaP.js +0 -26533
  147. package/dist/index-CN4cJ1N7.js +0 -1630
  148. package/dist/index-Cr1mc-d4.js +0 -5
  149. package/dist/src-UW24ZMRV-nsR4cpiy.js +0 -5
  150. package/src/components/molecules/filter/filter.tsx +0 -6
  151. package/src/components/molecules/filter/index.ts +0 -1
  152. package/src/components/organisms/listings/stories/listings.stories.tsx +0 -30
  153. package/src/main.tsx +0 -3
  154. package/src/mocks/listings.ts +0 -200
  155. package/src/types/components/molecules/filter/filter.ts +0 -9
  156. package/src/types/components/molecules/filter/index.ts +0 -1
  157. package/src/types/context/theme.ts +0 -17
@@ -0,0 +1,26 @@
1
+ import { Variants } from "framer-motion";
2
+
3
+ /**
4
+ * ? In order to work, the parent container must have the blinkContainer variants
5
+ * ? and each of the children must have the blinkItem variants
6
+ */
7
+
8
+ export const blinkContainer: Variants = {
9
+ hidden: { opacity: 0 },
10
+ visible: {
11
+ opacity: 1,
12
+ transition: {
13
+ staggerChildren: 0.12,
14
+ delayChildren: 0.08,
15
+ },
16
+ },
17
+ };
18
+
19
+ export const blinkItem: Variants = {
20
+ hidden: { opacity: 0, y: 16 },
21
+ visible: {
22
+ opacity: 1,
23
+ y: 0,
24
+ transition: { duration: 0.4, ease: "easeOut" },
25
+ },
26
+ };
@@ -0,0 +1,12 @@
1
+ import { Variants } from "framer-motion";
2
+
3
+ export const floating: Variants = {
4
+ animate: {
5
+ y: [0, -6, 0],
6
+ transition: {
7
+ duration: 3,
8
+ repeat: Infinity,
9
+ ease: "easeInOut",
10
+ },
11
+ },
12
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./floating";
2
+ export * from "./blink";
@@ -4,7 +4,7 @@ import { VariantProps } from "class-variance-authority";
4
4
  import { ComponentProps, forwardRef } from "react";
5
5
  import { Row } from "@/components";
6
6
  import { brandStyles } from "./styles";
7
- import { Image } from "@heroui/react";
7
+ import { Image } from "@heroui/image";
8
8
 
9
9
  export const Brand = forwardRef<
10
10
  HTMLElement,
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ import type { ColorPickerProps } from "@/types";
4
+ import { FreeColorPicker, PresetColorPicker } from "@/components";
5
+ import "./styles/color-picker.css";
6
+
7
+ export function ColorPicker({ ...props }: ColorPickerProps) {
8
+ if (props?.inputType === "preset") {
9
+ return <PresetColorPicker {...props} />;
10
+ }
11
+
12
+ return <FreeColorPicker {...props} />;
13
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { Input } from "@heroui/input";
5
+ import { cn } from "@/utils";
6
+ import type { ColorPickerProps } from "@/types";
7
+ import { CopyButton } from "@/components";
8
+ import { useEmperorUI } from "@/hooks";
9
+
10
+ const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
11
+
12
+ export function FreeColorPicker({
13
+ className,
14
+ classNames,
15
+ value = "#000000",
16
+ onValueChange,
17
+ inputType = "free",
18
+ ...props
19
+ }: ColorPickerProps) {
20
+ const { config } = useEmperorUI();
21
+
22
+ const locales = config?.interLocalization?.locales;
23
+ const lang = config?.interLocalization?.lang;
24
+ const locale = locales?.[lang ?? "en"];
25
+ const errorMessage = locale?.atoms?.colorPicker?.invalidColorFormat;
26
+
27
+ const isInvalid = useMemo(() => {
28
+ if (!value) return true;
29
+ return HEX_COLOR_REGEX.test(value);
30
+ }, [value]);
31
+
32
+ const handleValueChange = (newValue: string) => {
33
+ onValueChange?.(newValue);
34
+ };
35
+
36
+ return (
37
+ <Input
38
+ labelPlacement="outside"
39
+ variant="faded"
40
+ className={cn(className)}
41
+ classNames={{ label: "font-bold", input: "h-12", ...classNames }}
42
+ value={value}
43
+ onValueChange={handleValueChange}
44
+ isInvalid={isInvalid}
45
+ errorMessage={isInvalid ? errorMessage : undefined}
46
+ endContent={<CopyButton value={value} />}
47
+ startContent={
48
+ <input
49
+ type="color"
50
+ className="color-swatch"
51
+ value={value}
52
+ onChange={(e) => {
53
+ handleValueChange(e.target.value);
54
+ }}
55
+ />
56
+ }
57
+ {...props}
58
+ />
59
+ );
60
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./color-picker";
2
+ export * from "./free-color-picker";
3
+ export * from "./preset-color-picker";
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/utils";
4
+ import type { ColorPickerProps } from "@/types";
5
+ import { Select, SelectItem } from "@heroui/select";
6
+ import { CopyButton } from "@/components";
7
+
8
+ export function PresetColorPicker({
9
+ className,
10
+ classNames,
11
+ value = "#000000",
12
+ onSelectionChange,
13
+ inputType,
14
+ presets = [],
15
+ ...props
16
+ }: ColorPickerProps) {
17
+ if (inputType === "preset" && (!presets || presets?.length === 0))
18
+ throw new Error(
19
+ "Presets array is required in the ColorPicker when the input type is 'preset'.",
20
+ );
21
+
22
+ return (
23
+ <Select
24
+ labelPlacement="outside"
25
+ variant="faded"
26
+ className={cn(className)}
27
+ classNames={{
28
+ label: "font-bold",
29
+ input: "h-12",
30
+ trigger: "min-w-52",
31
+ ...classNames,
32
+ }}
33
+ value={value}
34
+ onSelectionChange={onSelectionChange}
35
+ endContent={<CopyButton value={value} />}
36
+ renderValue={() => {
37
+ return (
38
+ <div className="flex items-center gap-2 text-gray-600">
39
+ <div
40
+ style={{ backgroundColor: value }}
41
+ className="size-4 rounded-full border border-gray-300"
42
+ />
43
+ {value}
44
+ </div>
45
+ );
46
+ }}
47
+ {...props}
48
+ >
49
+ {Array.from(new Set(presets))?.map((preset) => (
50
+ <SelectItem
51
+ key={preset}
52
+ startContent={
53
+ <div
54
+ style={{ backgroundColor: preset }}
55
+ className="size-4 rounded-full border border-gray-300"
56
+ />
57
+ }
58
+ >
59
+ {preset}
60
+ </SelectItem>
61
+ ))}
62
+ </Select>
63
+ );
64
+ }
@@ -0,0 +1,49 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { ColorPicker } from "@/components";
3
+ import { useState } from "react";
4
+ import { getStorybookDecorators } from "@/utils";
5
+
6
+ const meta: Meta<typeof ColorPicker> = {
7
+ title: "Atoms/ColorPicker",
8
+ component: ColorPicker,
9
+ parameters: {
10
+ layout: "centered",
11
+ },
12
+ tags: ["autodocs"],
13
+ decorators: getStorybookDecorators({
14
+ config: {
15
+ layout: {
16
+ withScaffold: false,
17
+ },
18
+ },
19
+ }),
20
+ };
21
+
22
+ export default meta;
23
+
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const Default: Story = {
27
+ render: (args) => {
28
+ const [value, setValue] = useState("#000000");
29
+ return <ColorPicker {...args} value={value} onValueChange={setValue} />;
30
+ },
31
+ };
32
+
33
+ export const WithPresets: Story = {
34
+ render: () => {
35
+ const [value, setValue] = useState("#000000");
36
+
37
+ return (
38
+ <ColorPicker
39
+ inputType="preset"
40
+ presets={["#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF"]}
41
+ value={value}
42
+ onSelectionChange={(keys) => {
43
+ setValue(keys?.currentKey ?? "");
44
+ }}
45
+ defaultSelectedKeys={["#000000"]}
46
+ />
47
+ );
48
+ },
49
+ };
@@ -0,0 +1,23 @@
1
+ /* Rounded corners and compact size for native color input swatch */
2
+ .color-swatch {
3
+ width: 1.25rem;
4
+ height: 1.25rem;
5
+ min-width: 1.25rem;
6
+ min-height: 1.25rem;
7
+ cursor: pointer;
8
+ }
9
+
10
+ .color-swatch::-webkit-color-swatch-wrapper {
11
+ padding: 0;
12
+ border-radius: 0.375rem;
13
+ }
14
+
15
+ .color-swatch::-webkit-color-swatch {
16
+ border: none;
17
+ border-radius: 0.375rem;
18
+ }
19
+
20
+ .color-swatch::-moz-color-swatch {
21
+ border: none;
22
+ border-radius: 0.375rem;
23
+ }
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect } from "react";
4
+ import { Button } from "@heroui/button";
5
+ import { Spinner } from "@heroui/spinner";
6
+ import { Copy, Check } from "lucide-react";
7
+ import { motion, AnimatePresence } from "framer-motion";
8
+
9
+ type CopyState = "copy" | "loading" | "success";
10
+
11
+ export function CopyButton({ value }: { value: string }) {
12
+ const [state, setState] = useState<CopyState>("copy");
13
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
14
+
15
+ const handleCopy = () => {
16
+ if (state !== "copy") return;
17
+
18
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
19
+
20
+ navigator.clipboard.writeText(value);
21
+ setState("loading");
22
+
23
+ timeoutRef.current = setTimeout(() => {
24
+ setState("success");
25
+ timeoutRef.current = setTimeout(() => setState("copy"), 2000);
26
+ }, 500);
27
+ };
28
+
29
+ useEffect(
30
+ () => () => {
31
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
32
+ },
33
+ [],
34
+ );
35
+
36
+ return (
37
+ <Button
38
+ variant="light"
39
+ size="sm"
40
+ isIconOnly
41
+ onPress={handleCopy}
42
+ disabled={state !== "copy"}
43
+ startContent={
44
+ <span className="relative size-4">
45
+ <AnimatePresence mode="wait">
46
+ <motion.span
47
+ key={state}
48
+ initial={{ opacity: 0 }}
49
+ animate={{ opacity: 1 }}
50
+ exit={{ opacity: 0 }}
51
+ transition={{ duration: 0.2 }}
52
+ className="absolute inset-0 flex items-center justify-center"
53
+ >
54
+ {state === "copy" && <Copy className="size-4" />}
55
+
56
+ {state === "loading" && (
57
+ <Spinner
58
+ size="sm"
59
+ classNames={{
60
+ circle1: "border-b-current",
61
+ circle2: "border-current",
62
+ }}
63
+ />
64
+ )}
65
+
66
+ {state === "success" && <Check className="size-4 text-success" />}
67
+ </motion.span>
68
+ </AnimatePresence>
69
+ </span>
70
+ }
71
+ />
72
+ );
73
+ }
@@ -0,0 +1 @@
1
+ export * from "./copy-button";
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { CopyButton } from "@/components";
3
+
4
+ const meta: Meta<typeof CopyButton> = {
5
+ title: "Atoms/CopyButton",
6
+ component: CopyButton,
7
+ parameters: {
8
+ layout: "centered",
9
+ },
10
+ tags: ["autodocs"],
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ value: "Copied to clipboard successfully!",
20
+ },
21
+ };
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Field } from "@/components";
3
+ import { getStorybookDecorators } from "@/utils";
4
+
5
+ const meta: Meta<typeof Field> = {
6
+ title: "Atoms/Field",
7
+ component: Field,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ decorators: getStorybookDecorators({
13
+ config: {
14
+ layout: {
15
+ withScaffold: false,
16
+ },
17
+ },
18
+ }),
19
+ };
20
+
21
+ export default meta;
22
+
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ args: {},
27
+ };
@@ -0,0 +1,11 @@
1
+ import { cn } from "@/utils";
2
+ import { FieldProps } from "@/types";
3
+ import { fieldClasses } from "./styles";
4
+
5
+ export function Field({ className, classNames, ...props }: FieldProps) {
6
+ return (
7
+ <div className={cn(fieldClasses({}), className)} {...props}>
8
+ Sorter Component
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1 @@
1
+ export * from "./field";
@@ -0,0 +1,9 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const fieldClasses = cva([""], {
4
+ variants: {
5
+ variant: {},
6
+ },
7
+ defaultVariants: {},
8
+ compoundVariants: [],
9
+ });
@@ -0,0 +1 @@
1
+ export * from "./classes";
@@ -0,0 +1,92 @@
1
+ import { cn } from "@/utils";
2
+ import { FilterProps, FilterType } from "@/types";
3
+ import { filterClasses } from "./styles";
4
+ import { ReactNode } from "react";
5
+ import {
6
+ SearchFilter,
7
+ SelectFilter,
8
+ AutocompleteFilter,
9
+ DateFilter,
10
+ NumericFilter,
11
+ CheckboxFilter,
12
+ CheckboxGroupFilter,
13
+ SwitchFilter,
14
+ RangeFilter,
15
+ } from "@/components";
16
+
17
+ export function Filter({
18
+ className,
19
+ type,
20
+ searchProps,
21
+ selectProps,
22
+ autocompleteProps,
23
+ dateProps,
24
+ numericProps,
25
+ checkboxProps,
26
+ checkboxGroupProps,
27
+ radioProps,
28
+ switchProps,
29
+ rangeProps,
30
+ paramKey,
31
+ options,
32
+ ...props
33
+ }: FilterProps) {
34
+ const sharedProps = { paramKey };
35
+
36
+ const components: Record<FilterType, ReactNode> = {
37
+ search: (
38
+ <SearchFilter searchProps={searchProps} {...sharedProps} {...props} />
39
+ ),
40
+ select: (
41
+ <SelectFilter
42
+ selectProps={selectProps}
43
+ options={options}
44
+ {...sharedProps}
45
+ {...props}
46
+ />
47
+ ),
48
+ autocomplete: (
49
+ <AutocompleteFilter
50
+ autocompleteProps={autocompleteProps}
51
+ {...sharedProps}
52
+ {...props}
53
+ />
54
+ ),
55
+ date: <DateFilter dateProps={dateProps} {...sharedProps} {...props} />,
56
+ numeric: (
57
+ <NumericFilter numericProps={numericProps} {...sharedProps} {...props} />
58
+ ),
59
+ checkbox: (
60
+ <CheckboxFilter
61
+ checkboxProps={checkboxProps}
62
+ {...sharedProps}
63
+ {...props}
64
+ />
65
+ ),
66
+ checkboxGroup: (
67
+ <CheckboxGroupFilter
68
+ checkboxGroupProps={checkboxGroupProps}
69
+ {...sharedProps}
70
+ {...props}
71
+ />
72
+ ),
73
+ switch: (
74
+ <SwitchFilter switchProps={switchProps} {...sharedProps} {...props} />
75
+ ),
76
+ range: <RangeFilter rangeProps={rangeProps} {...sharedProps} {...props} />,
77
+ };
78
+
79
+ return (
80
+ <div
81
+ data-slot="emperor-ui-filter"
82
+ className={cn(
83
+ filterClasses({ type }),
84
+ className,
85
+ props?.classNames?.base,
86
+ )}
87
+ {...props}
88
+ >
89
+ {components[type]}
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./filter";
2
+ export * from "./units";
3
+ export * from "./styles";
@@ -0,0 +1,97 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Filter } from "@/components";
3
+ import { useFilters } from "@/hooks";
4
+
5
+ const meta: Meta<typeof Filter> = {
6
+ title: "Atoms/Filter",
7
+ component: Filter,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ };
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const SearchFilter: Story = {
19
+ render: () => {
20
+ const { filters } = useFilters<{
21
+ search: string;
22
+ }>();
23
+
24
+ return (
25
+ <section className="flex flex-col gap-2">
26
+ <Filter
27
+ type="search"
28
+ paramKey="search"
29
+ searchProps={{ label: "Search" }}
30
+ />
31
+
32
+ <p className="text-gray-500 text-xs">
33
+ {filters?.search || "No filter applied"}
34
+ </p>
35
+ </section>
36
+ );
37
+ },
38
+ };
39
+
40
+ export const SelectFilter: Story = {
41
+ render: () => {
42
+ const { filters } = useFilters<{
43
+ select: string;
44
+ }>();
45
+
46
+ return (
47
+ <section className="flex flex-col gap-2">
48
+ <Filter
49
+ type="select"
50
+ paramKey="select"
51
+ selectProps={{ label: "Select an option" }}
52
+ options={[
53
+ { key: "option-key-1", label: "Option 1" },
54
+ { key: "option-key-2", label: "Option 2" },
55
+ { key: "option-key-3", label: "Option 3" },
56
+ ]}
57
+ />
58
+
59
+ <p className="text-gray-500 text-xs">
60
+ {filters?.select || "No filter applied"}
61
+ </p>
62
+ </section>
63
+ );
64
+ },
65
+ };
66
+
67
+ export const DateFilter: Story = {
68
+ args: {
69
+ type: "date",
70
+ paramKey: "date",
71
+ },
72
+ };
73
+
74
+ export const CheckboxFilter: Story = {
75
+ render: () => {
76
+ const { filters } = useFilters<{
77
+ isChecked: boolean;
78
+ }>();
79
+
80
+ return (
81
+ <section className="flex flex-col gap-2">
82
+ <Filter
83
+ type="checkbox"
84
+ paramKey="isChecked"
85
+ checkboxProps={{ children: "Is Checked ?" }}
86
+ />
87
+
88
+ <p className="text-gray-500 text-xs">
89
+ <span>Result: </span>
90
+ <span className="text-gray-500">
91
+ {filters?.isChecked ? "Yes" : "No"}
92
+ </span>
93
+ </p>
94
+ </section>
95
+ );
96
+ },
97
+ };
@@ -0,0 +1,20 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const filterClasses = cva([""], {
4
+ variants: {
5
+ type: {
6
+ search: "",
7
+ select: "",
8
+ autocomplete: "",
9
+ date: "",
10
+ numeric: "",
11
+ checkbox: "",
12
+ checkboxGroup: "",
13
+ radio: "",
14
+ switch: "",
15
+ range: "",
16
+ },
17
+ },
18
+ defaultVariants: {},
19
+ compoundVariants: [],
20
+ });
@@ -0,0 +1 @@
1
+ export * from "./classes";
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { FilterProps } from "@/types";
4
+ import { filterClasses } from "@/components";
5
+ import { Autocomplete } from "@heroui/autocomplete";
6
+ import { cn } from "@/utils";
7
+ import { useSearchParamsHandler } from "@/hooks";
8
+
9
+ export function AutocompleteFilter({
10
+ classNames,
11
+ autocompleteProps,
12
+ paramKey,
13
+ ...props
14
+ }: Pick<FilterProps, "classNames" | "autocompleteProps" | "paramKey">) {
15
+ const { getParam, setParams } = useSearchParamsHandler();
16
+
17
+ const value = getParam(paramKey);
18
+
19
+ return (
20
+ <Autocomplete
21
+ labelPlacement="outside-top"
22
+ variant="faded"
23
+ radius="sm"
24
+ selectedKey={value || null}
25
+ onSelectionChange={(selectedKey) =>
26
+ setParams({
27
+ params: {
28
+ [paramKey]: selectedKey ? String(selectedKey) : undefined,
29
+ },
30
+ })
31
+ }
32
+ {...autocompleteProps}
33
+ {...props}
34
+ className={cn(filterClasses({ type: "autocomplete" }), classNames?.field)}
35
+ >
36
+ <div />
37
+ </Autocomplete>
38
+ );
39
+ }