@hobenakicoffee/libraries 3.4.1 → 4.0.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -246
  3. package/package.json +22 -22
  4. package/src/App.tsx +192 -19
  5. package/src/index.css +0 -1
  6. package/src/utils/get-product-link.ts +1 -1
  7. package/src/components/turnstile-captcha.tsx +0 -47
  8. package/src/components/ui/button.tsx +0 -77
  9. package/src/components/ui/calendar.tsx +0 -235
  10. package/src/components/ui/spinner.tsx +0 -18
  11. package/src/constants/common.test.ts +0 -33
  12. package/src/constants/legal.test.ts +0 -72
  13. package/src/constants/payment.test.ts +0 -259
  14. package/src/constants/platforms.test.ts +0 -66
  15. package/src/constants/services.test.ts +0 -58
  16. package/src/lib/utils.ts +0 -6
  17. package/src/moderation/profanity-service.test.ts +0 -106
  18. package/src/providers/theme-provider.tsx +0 -73
  19. package/src/utils/check-moderation.test.ts +0 -321
  20. package/src/utils/format-amount.test.ts +0 -30
  21. package/src/utils/format-count.test.ts +0 -56
  22. package/src/utils/format-date.test.ts +0 -19
  23. package/src/utils/format-number.test.ts +0 -29
  24. package/src/utils/format-plain-text.test.ts +0 -36
  25. package/src/utils/get-newsletter-post-link.test.ts +0 -27
  26. package/src/utils/get-product-link.test.ts +0 -36
  27. package/src/utils/get-social-handle.test.ts +0 -32
  28. package/src/utils/get-social-link.test.ts +0 -63
  29. package/src/utils/get-user-name-initials.test.ts +0 -34
  30. package/src/utils/get-user-page-link.test.ts +0 -9
  31. package/src/utils/open-to-new-window.test.ts +0 -34
  32. package/src/utils/post-to-facebook.test.ts +0 -43
  33. package/src/utils/post-to-instagram.test.ts +0 -56
  34. package/src/utils/post-to-linkedin.test.ts +0 -43
  35. package/src/utils/post-to-x.test.ts +0 -45
  36. package/src/utils/qr-svg-utils.test.ts +0 -104
  37. package/src/utils/to-human-readable.test.ts +0 -25
  38. package/src/utils/validate-phone-number.test.ts +0 -28
package/src/App.tsx CHANGED
@@ -1,28 +1,201 @@
1
1
  import { useState } from "react";
2
- import { Button } from "./components/ui/button";
3
- import { Calendar } from "./components/ui/calendar";
4
- import { getProductLink } from "./utils/get-product-link";
2
+ import { productInfo } from "@/constants/legal";
3
+
4
+ const EXPORTS = [
5
+ {
6
+ path: "/constants",
7
+ name: "constants",
8
+ desc: "Payment, Platform, Visibility consts",
9
+ },
10
+ {
11
+ path: "/utils",
12
+ name: "utils",
13
+ desc: "Formatters, validators, social helpers",
14
+ },
15
+ {
16
+ path: "/types",
17
+ name: "types",
18
+ desc: "Supabase + custom TypeScript types",
19
+ },
20
+ {
21
+ path: "/moderation",
22
+ name: "moderation",
23
+ desc: "Profanity detection EN/BN",
24
+ },
25
+ {
26
+ path: "/nuqs",
27
+ name: "nuqs",
28
+ desc: "URL state parsers (zod)",
29
+ },
30
+ {
31
+ path: "/scripts",
32
+ name: "scripts",
33
+ desc: "Build utilities",
34
+ },
35
+ { path: "/hooks", name: "hooks", desc: "React hooks" },
36
+ { path: "/docs", name: "docs", desc: "Documentation & guides" },
37
+ ];
5
38
 
6
39
  const App = () => {
7
- const [date, setDate] = useState<Date | undefined>(new Date());
40
+ const [copied, setCopied] = useState(false);
41
+
42
+ const handleCopy = () => {
43
+ navigator.clipboard.writeText("bun add @hobenakicoffee/libraries");
44
+ setCopied(true);
45
+ setTimeout(() => setCopied(false), 2000);
46
+ };
8
47
 
9
48
  return (
10
- <div className="flex min-h-dvh flex-col gap-y-6 p-5 md:p-8">
11
- <h1 className="font-bold text-lg">Welcome to library playground!</h1>
12
- <p>This is the core library package for "হবে নাকি Coffee?" projects.</p>
13
-
14
- <div>
15
- <Button>Click me</Button>
16
-
17
- <Calendar
18
- captionLayout="dropdown"
19
- className="rounded-lg border"
20
- mode="single"
21
- onSelect={setDate}
22
- selected={date}
23
- />
24
- {getProductLink("leo", "example-product-slug")}
49
+ <div className="min-dvh relative flex flex-col overflow-hidden bg-[#0a0c10] text-slate-200">
50
+ <div className="pointer-events-none absolute inset-0 overflow-hidden">
51
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,#1e293b_0.5px,transparent_0.5px),linear-gradient(to_bottom,#1e293b_0.5px,transparent_0.5px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)]" />
52
+ <div className="absolute top-0 left-1/4 h-[500px] w-[500px] -translate-x-1/2 rounded-full bg-indigo-500/[0.03] blur-3xl" />
53
+ <div className="absolute right-1/4 bottom-0 h-[400px] w-[400px] translate-y-1/2 rounded-full bg-cyan-500/[0.03] blur-3xl" />
25
54
  </div>
55
+
56
+ <header className="relative z-10 flex items-center justify-between px-6 py-4 md:px-8">
57
+ <div className="flex items-center gap-3">
58
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-700 bg-slate-900 shadow-[0_0_20px_rgba(99,102,241,0.2)]">
59
+ <span className="font-bold font-mono text-cyan-400 text-lg">H</span>
60
+ </div>
61
+ <div>
62
+ <h1 className="font-mono font-semibold text-slate-100 text-sm tracking-tight">
63
+ @hobenakicoffee/libraries
64
+ </h1>
65
+ <p className="font-mono text-slate-500 text-xs">v3.4.2</p>
66
+ </div>
67
+ </div>
68
+ <div className="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/50 px-2 py-1 font-mono text-slate-400 text-xs">
69
+ <span className="h-2 w-2 rounded-full bg-green-500" />
70
+ TypeScript
71
+ </div>
72
+ </header>
73
+
74
+ <main className="relative z-10 flex flex-1 flex-col items-center justify-center px-4 py-12 md:px-6">
75
+ <div className="mx-auto max-w-2xl text-center">
76
+ <div className="mb-6 inline-flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/30 px-3 py-1 font-mono text-slate-400 text-xs">
77
+ <span>framework-agnostic</span>
78
+ <span className="text-slate-600">{"//"}</span>
79
+ <span>tree-shakeable</span>
80
+ <span className="text-slate-600">{"//"}</span>
81
+ <span>zod-powered</span>
82
+ </div>
83
+
84
+ <h2 className="mb-4 font-bold font-mono text-3xl tracking-tight md:text-5xl">
85
+ <span className="text-slate-100">Shared </span>
86
+ <span className="bg-linear-to-r from-cyan-400 to-indigo-400 bg-clip-text text-transparent">
87
+ constants,
88
+ </span>
89
+ <br />
90
+ <span className="text-slate-100">utilities & </span>
91
+ <span className="bg-linear-to-r from-cyan-400 to-indigo-400 bg-clip-text text-transparent">
92
+ types
93
+ </span>
94
+ </h2>
95
+
96
+ <p className="mx-auto mb-8 max-w-lg font-mono text-slate-400 text-sm leading-relaxed">
97
+ The core package for{" "}
98
+ <span className="text-cyan-400">"{productInfo.name}"</span>{" "}
99
+ projects. Build faster with pre-built constants, utilities, types,
100
+ and moderation tools.
101
+ </p>
102
+
103
+ <div className="group relative mx-auto inline-flex cursor-pointer flex-col items-center gap-3 md:flex-row">
104
+ <div className="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 p-1 pr-3 font-mono text-sm shadow-[0_0_40px_rgba(99,102,241,0.15)] transition-all group-hover:border-cyan-500/50 group-hover:shadow-[0_0_60px_rgba(99,102,241,0.25)]">
105
+ <button
106
+ className="flex items-center gap-2 rounded-md bg-slate-800 px-3 py-2 text-slate-200 transition-colors hover:bg-slate-700"
107
+ onClick={handleCopy}
108
+ type="button"
109
+ >
110
+ <span className="text-cyan-400">$</span>
111
+ <span>
112
+ {copied ? " Copied!" : " bun add @hobenakicoffee/libraries"}
113
+ </span>
114
+ </button>
115
+ <span className="text-slate-500">{copied ? "✓" : "›"}</span>
116
+ </div>
117
+ <a
118
+ className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-3 font-mono text-sm text-white transition-all hover:bg-indigo-600/90"
119
+ href="/libraries/docs/"
120
+ >
121
+ <span>Documentation</span>
122
+ <span className="text-indigo-500">→</span>
123
+ </a>
124
+ </div>
125
+ </div>
126
+
127
+ <div className="mt-16 w-full max-w-4xl">
128
+ <div className="mb-3 font-mono text-slate-500 text-xs">
129
+ {"// exports"}
130
+ </div>
131
+ <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
132
+ {EXPORTS.map((exp, i) => (
133
+ <a
134
+ className="group relative overflow-hidden rounded-lg border border-slate-800 bg-slate-900/50 p-3 font-mono transition-all hover:border-slate-600"
135
+ href={
136
+ exp.path === "/docs"
137
+ ? `/libraries${exp.path}`
138
+ : `/libraries${exp.path}/overview`
139
+ }
140
+ key={exp.path}
141
+ style={{ animationDelay: `${i * 50}ms` }}
142
+ >
143
+ <div className="absolute inset-0 bg-linear-to-r from-cyan-500/0 to-indigo-500/0 opacity-0 transition-opacity group-hover:from-cyan-500/[0.03] group-hover:to-indigo-500/[0.03] group-hover:opacity-100" />
144
+ <div className="relative">
145
+ <span className="text-cyan-400 text-xs">
146
+ @hobenakicoffee/libraries
147
+ </span>
148
+ <span className="text-slate-500 text-xs">{exp.path}</span>
149
+ </div>
150
+ <div className="relative mt-1 text-slate-500 text-xs">
151
+ {exp.desc}
152
+ </div>
153
+ </a>
154
+ ))}
155
+ </div>
156
+ </div>
157
+
158
+ <div className="mt-12 w-full max-w-2xl overflow-x-auto rounded-lg border border-slate-800 bg-slate-950 p-4 font-mono text-xs">
159
+ <div className="tems-center mb-2 gap-2 text-slate-500">
160
+ <span className="h-3 w-3 rounded-full bg-emerald-500/20" />
161
+ main.ts
162
+ </div>
163
+ <pre className="text-slate-300">
164
+ <code>
165
+ <span className="text-purple-400">import</span> {"{"}{" "}
166
+ <span className="text-cyan-400">PaymentStatuses</span>, {""}
167
+ <span className="text-cyan-400">SupporterPlatforms</span>
168
+ {"}"} <span className="text-purple-400">from</span>{" "}
169
+ <span className="text-amber-300">
170
+ "@hobenakicoffee/libraries"
171
+ </span>
172
+ {"\n"}
173
+ <span className="text-purple-400">import</span> {"{"}{" "}
174
+ <span className="text-cyan-400">formatAmount</span>, {""}
175
+ <span className="text-cyan-400">getUserPageLink</span>
176
+ {"}"} <span className="text-purple-400">from</span>{" "}
177
+ <span className="text-amber-300">
178
+ "@hobenakicoffee/libraries/utils"
179
+ </span>
180
+ {"\n"}
181
+ <span className="text-purple-400">import</span> type {"{"}{" "}
182
+ <span className="text-cyan-400">Database</span>
183
+ {"}"} <span className="text-purple-400">from</span>{" "}
184
+ <span className="text-amber-300">
185
+ "@hobenakicoffee/libraries/types"
186
+ </span>
187
+ </code>
188
+ </pre>
189
+ </div>
190
+ </main>
191
+
192
+ <footer className="relative z-10 px-6 py-4 text-center">
193
+ <p className="font-mono text-slate-600 text-xs">
194
+ <span>© {new Date().getFullYear()}</span>
195
+ <span className="text-slate-700">{" // "}</span>
196
+ <span className="text-slate-500">{productInfo.name}</span>
197
+ </p>
198
+ </footer>
26
199
  </div>
27
200
  );
28
201
  };
package/src/index.css CHANGED
@@ -1,6 +1,5 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
- @import "shadcn/tailwind.css";
4
3
  @import "@fontsource-variable/noto-sans-bengali";
5
4
 
6
5
  @custom-variant dark (&:is(.dark *));
@@ -6,5 +6,5 @@ export function getProductLink(
6
6
  baseUrl = "https://hobenakicoffee.com"
7
7
  ) {
8
8
  const sanitizedSlug = slug.trim().replace(/\s+/g, "-");
9
- return `${getUserPageLink(username, baseUrl)}/shops/products/${sanitizedSlug}`;
9
+ return `${getUserPageLink(username, baseUrl)}/shop/products/${sanitizedSlug}`;
10
10
  }
@@ -1,47 +0,0 @@
1
- import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile";
2
- import { useRef } from "react";
3
- import { useTheme } from "@/providers/theme-provider";
4
-
5
- interface TurnstileCaptchaProps {
6
- onError?: (error: string) => void;
7
- onTokenChange: (token: string) => void;
8
- }
9
-
10
- export function TurnstileCaptcha({
11
- onTokenChange,
12
- onError,
13
- }: TurnstileCaptchaProps) {
14
- const { theme } = useTheme();
15
- const turnstileRef = useRef<TurnstileInstance | null>(null);
16
-
17
- function handleSuccess(token: string) {
18
- onTokenChange(token);
19
- }
20
-
21
- function handleExpire() {
22
- onTokenChange("");
23
- onError?.("CAPTCHA has expired. Please verify again.");
24
- }
25
-
26
- function handleError() {
27
- onTokenChange("");
28
- onError?.("CAPTCHA verification failed. Please try again.");
29
- }
30
-
31
- return (
32
- <div className="rounded-xl border border-input bg-input">
33
- <Turnstile
34
- className="overflow-hidden rounded-[calc(var(--radius)+5px)]"
35
- onError={handleError}
36
- onExpire={handleExpire}
37
- onSuccess={handleSuccess}
38
- options={{
39
- theme: theme === "system" ? "auto" : theme,
40
- size: "flexible",
41
- }}
42
- ref={turnstileRef}
43
- siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY || ""}
44
- />
45
- </div>
46
- );
47
- }
@@ -1,77 +0,0 @@
1
- import { cva, type VariantProps } from "class-variance-authority";
2
- import { Slot } from "radix-ui";
3
- import type * as React from "react";
4
-
5
- import { cn } from "@/lib/utils";
6
- import { Spinner } from "./spinner";
7
-
8
- const buttonVariants = cva(
9
- "group/button inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-xl border border-transparent bg-clip-padding font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-[0.98] disabled:pointer-events-none disabled:opacity-70 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
10
- {
11
- variants: {
12
- variant: {
13
- default: "bg-primary text-primary-foreground hover:bg-primary/80",
14
- outline:
15
- "border-border bg-input/30 hover:border-primary/50 hover:bg-input/20 hover:bg-primary/10 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
16
- secondary:
17
- "bg-secondary text-secondary-foreground hover:bg-primary/5 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
18
- ghost:
19
- "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
20
- destructive:
21
- "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:bg-destructive/30",
22
- link: "underline-offset-4 hover:underline",
23
- inverted:
24
- "border-border bg-background text-foreground hover:border-primary/50 hover:bg-primary/5",
25
- dottedUnderline:
26
- "rounded-none border-0 border-muted-foreground/50 border-b border-dotted bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground",
27
- },
28
- size: {
29
- default:
30
- "h-12 gap-1.5 px-4 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
31
- xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
32
- sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
33
- lg: "h-16 gap-1.5 px-5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
34
- icon: "size-9",
35
- "icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
36
- "icon-sm": "size-8",
37
- "icon-lg": "size-12",
38
- },
39
- },
40
- defaultVariants: {
41
- variant: "default",
42
- size: "default",
43
- },
44
- }
45
- );
46
-
47
- function Button({
48
- className,
49
- variant = "default",
50
- size = "default",
51
- asChild = false,
52
- loading,
53
- disabled,
54
- children,
55
- ...props
56
- }: React.ComponentProps<"button"> &
57
- VariantProps<typeof buttonVariants> & {
58
- asChild?: boolean;
59
- loading?: boolean;
60
- }) {
61
- const Comp = asChild ? Slot.Root : "button";
62
-
63
- return (
64
- <Comp
65
- className={cn(buttonVariants({ variant, size, className }))}
66
- data-size={size}
67
- data-slot="button"
68
- data-variant={variant}
69
- disabled={loading || disabled}
70
- {...props}
71
- >
72
- {loading ? <Spinner /> : children}
73
- </Comp>
74
- );
75
- }
76
-
77
- export { Button, buttonVariants };
@@ -1,235 +0,0 @@
1
- import {
2
- ArrowDownIcon,
3
- ArrowLeftIcon,
4
- ArrowRightIcon,
5
- } from "@hugeicons/core-free-icons";
6
- import { HugeiconsIcon } from "@hugeicons/react";
7
- import { type ComponentProps, useEffect, useRef } from "react";
8
- import {
9
- type DayButton,
10
- DayPicker,
11
- getDefaultClassNames,
12
- } from "react-day-picker";
13
- import { Button, buttonVariants } from "@/components/ui/button";
14
- import { cn } from "@/lib/utils";
15
-
16
- function Calendar({
17
- className,
18
- classNames,
19
- showOutsideDays = true,
20
- captionLayout = "label",
21
- buttonVariant = "ghost",
22
- formatters,
23
- components,
24
- ...props
25
- }: ComponentProps<typeof DayPicker> & {
26
- buttonVariant?: ComponentProps<typeof Button>["variant"];
27
- }) {
28
- const defaultClassNames = getDefaultClassNames();
29
-
30
- return (
31
- <DayPicker
32
- captionLayout={captionLayout}
33
- className={cn(
34
- "group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)]",
35
- String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
36
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
37
- className
38
- )}
39
- classNames={{
40
- root: cn("w-fit", defaultClassNames.root),
41
- months: cn(
42
- "relative flex flex-col gap-4 md:flex-row",
43
- defaultClassNames.months
44
- ),
45
- month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
46
- nav: cn(
47
- "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
48
- defaultClassNames.nav
49
- ),
50
- button_previous: cn(
51
- buttonVariants({ variant: buttonVariant }),
52
- "size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
53
- defaultClassNames.button_previous
54
- ),
55
- button_next: cn(
56
- buttonVariants({ variant: buttonVariant }),
57
- "size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
58
- defaultClassNames.button_next
59
- ),
60
- month_caption: cn(
61
- "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
62
- defaultClassNames.month_caption
63
- ),
64
- dropdowns: cn(
65
- "flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm",
66
- defaultClassNames.dropdowns
67
- ),
68
- dropdown_root: cn(
69
- "cn-calendar-dropdown-root relative rounded-(--cell-radius)",
70
- defaultClassNames.dropdown_root
71
- ),
72
- dropdown: cn(
73
- "absolute inset-0 bg-popover opacity-0",
74
- defaultClassNames.dropdown
75
- ),
76
- caption_label: cn(
77
- "select-none font-medium",
78
- captionLayout === "label"
79
- ? "text-sm"
80
- : "cn-calendar-caption-label flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
81
- defaultClassNames.caption_label
82
- ),
83
- table: "w-full border-collapse",
84
- weekdays: cn("flex", defaultClassNames.weekdays),
85
- weekday: cn(
86
- "flex-1 select-none rounded-(--cell-radius) font-normal text-[0.8rem] text-muted-foreground",
87
- defaultClassNames.weekday
88
- ),
89
- week: cn("mt-2 flex w-full", defaultClassNames.week),
90
- week_number_header: cn(
91
- "w-(--cell-size) select-none",
92
- defaultClassNames.week_number_header
93
- ),
94
- week_number: cn(
95
- "select-none text-[0.8rem] text-muted-foreground",
96
- defaultClassNames.week_number
97
- ),
98
- day: cn(
99
- "group/day relative aspect-square h-full w-full select-none rounded-(--cell-radius) p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
100
- props.showWeekNumber
101
- ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
102
- : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
103
- defaultClassNames.day
104
- ),
105
- range_start: cn(
106
- "relative isolate -z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
107
- defaultClassNames.range_start
108
- ),
109
- range_middle: cn("rounded-none", defaultClassNames.range_middle),
110
- range_end: cn(
111
- "relative isolate -z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted-200",
112
- defaultClassNames.range_end
113
- ),
114
- today: cn(
115
- "rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none",
116
- defaultClassNames.today
117
- ),
118
- outside: cn(
119
- "text-muted-foreground aria-selected:text-muted-foreground",
120
- defaultClassNames.outside
121
- ),
122
- disabled: cn(
123
- "text-muted-foreground opacity-50",
124
- defaultClassNames.disabled
125
- ),
126
- hidden: cn("invisible", defaultClassNames.hidden),
127
- ...classNames,
128
- }}
129
- components={{
130
- Root: ({ className, rootRef, ...props }) => {
131
- return (
132
- <div
133
- className={cn(className)}
134
- data-slot="calendar"
135
- ref={rootRef}
136
- {...props}
137
- />
138
- );
139
- },
140
- Chevron: ({ className, orientation, ...props }) => {
141
- if (orientation === "left") {
142
- return (
143
- <HugeiconsIcon
144
- className={cn("size-4", className)}
145
- icon={ArrowLeftIcon}
146
- strokeWidth={2}
147
- {...props}
148
- />
149
- );
150
- }
151
-
152
- if (orientation === "right") {
153
- return (
154
- <HugeiconsIcon
155
- className={cn("size-4", className)}
156
- icon={ArrowRightIcon}
157
- strokeWidth={2}
158
- {...props}
159
- />
160
- );
161
- }
162
-
163
- return (
164
- <HugeiconsIcon
165
- className={cn("size-4", className)}
166
- icon={ArrowDownIcon}
167
- strokeWidth={2}
168
- {...props}
169
- />
170
- );
171
- },
172
- DayButton: CalendarDayButton,
173
- WeekNumber: ({ children, ...props }) => {
174
- return (
175
- <td {...props}>
176
- <div className="flex size-(--cell-size) items-center justify-center text-center">
177
- {children}
178
- </div>
179
- </td>
180
- );
181
- },
182
- ...components,
183
- }}
184
- formatters={{
185
- formatMonthDropdown: (date) =>
186
- date.toLocaleString("default", { month: "short" }),
187
- ...formatters,
188
- }}
189
- showOutsideDays={showOutsideDays}
190
- {...props}
191
- />
192
- );
193
- }
194
-
195
- function CalendarDayButton({
196
- className,
197
- day,
198
- modifiers,
199
- ...props
200
- }: ComponentProps<typeof DayButton>) {
201
- const defaultClassNames = getDefaultClassNames();
202
-
203
- const ref = useRef<HTMLButtonElement>(null);
204
- useEffect(() => {
205
- if (modifiers.focused) {
206
- ref.current?.focus();
207
- }
208
- }, [modifiers.focused]);
209
-
210
- return (
211
- <Button
212
- className={cn(
213
- "relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 font-normal leading-none data-[range-end=true]:rounded-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-end=true]:bg-primary data-[range-middle=true]:bg-muted data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
214
- defaultClassNames.day,
215
- className
216
- )}
217
- data-day={day.date.toLocaleDateString()}
218
- data-range-end={modifiers.range_end}
219
- data-range-middle={modifiers.range_middle}
220
- data-range-start={modifiers.range_start}
221
- data-selected-single={
222
- modifiers.selected &&
223
- !modifiers.range_start &&
224
- !modifiers.range_end &&
225
- !modifiers.range_middle
226
- }
227
- ref={ref}
228
- size="icon"
229
- variant="ghost"
230
- {...props}
231
- />
232
- );
233
- }
234
-
235
- export { Calendar, CalendarDayButton };
@@ -1,18 +0,0 @@
1
- import { Loading03Icon } from "@hugeicons/core-free-icons";
2
- import { HugeiconsIcon } from "@hugeicons/react";
3
- import { cn } from "@/lib/utils";
4
-
5
- function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
6
- return (
7
- <HugeiconsIcon
8
- aria-label="Loading"
9
- className={cn("size-4 animate-spin", className)}
10
- icon={Loading03Icon}
11
- role="status"
12
- {...props}
13
- strokeWidth={2}
14
- />
15
- );
16
- }
17
-
18
- export { Spinner };
@@ -1,33 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { Visibility as VisibilityType } from "./common";
3
- import { Visibility } from "./common";
4
-
5
- describe("Visibility", () => {
6
- test("should contain all expected keys", () => {
7
- const expectedKeys = ["PUBLIC", "PRIVATE"];
8
- expect(Object.keys(Visibility)).toEqual(expectedKeys);
9
- });
10
-
11
- test("should have correct values for each visibility", () => {
12
- expect(Visibility.PUBLIC).toBe("public");
13
- expect(Visibility.PRIVATE).toBe("private");
14
- });
15
-
16
- test("should be usable as Visibility type", () => {
17
- const pub: VisibilityType = "public";
18
- const priv: VisibilityType = "private";
19
- expect(pub).toBe("public");
20
- expect(priv).toBe("private");
21
- });
22
-
23
- test("should have 2 visibilities", () => {
24
- expect(Object.keys(Visibility).length).toBe(2);
25
- });
26
-
27
- test("all values should be lowercase strings", () => {
28
- Object.values(Visibility).forEach((v) => {
29
- expect(v).toBe(v.toLowerCase() as VisibilityType);
30
- expect(typeof v).toBe("string");
31
- });
32
- });
33
- });