ruler-components 0.1.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/AGENTS.md +9 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/app/docs/[slug]/page.tsx +147 -0
- package/app/docs/layout.tsx +26 -0
- package/app/docs/page.tsx +148 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +168 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +90 -0
- package/app/opengraph-image.tsx +10 -0
- package/app/page.tsx +34 -0
- package/app/twitter-image.tsx +10 -0
- package/components/docs/accordion-doc-page.tsx +352 -0
- package/components/docs/alert-doc-page.tsx +293 -0
- package/components/docs/allinput-doc-page.tsx +183 -0
- package/components/docs/avatar-doc-page.tsx +424 -0
- package/components/docs/badge-doc-page.tsx +363 -0
- package/components/docs/button-doc-page.tsx +406 -0
- package/components/docs/card-doc-page.tsx +364 -0
- package/components/docs/checkbox-doc-page.tsx +277 -0
- package/components/docs/colors-doc-page.tsx +430 -0
- package/components/docs/dialog-doc-page.tsx +364 -0
- package/components/docs/docs-footer.tsx +11 -0
- package/components/docs/docs-mobile-menu-bar.tsx +22 -0
- package/components/docs/docs-mobile-nav-context.tsx +27 -0
- package/components/docs/docs-page.tsx +263 -0
- package/components/docs/docs-search.tsx +303 -0
- package/components/docs/docs-sidebar.tsx +165 -0
- package/components/docs/docs-topbar.tsx +32 -0
- package/components/docs/dropdown-doc-page.tsx +522 -0
- package/components/docs/getting-started-doc-page.tsx +205 -0
- package/components/docs/label-doc-page.tsx +173 -0
- package/components/docs/loaders-doc-page.tsx +244 -0
- package/components/docs/menu-doc-page.tsx +373 -0
- package/components/docs/modal-doc-page.tsx +436 -0
- package/components/docs/next-steps.tsx +53 -0
- package/components/docs/pagination-doc-page.tsx +316 -0
- package/components/docs/progress-doc-page.tsx +387 -0
- package/components/docs/radio-doc-page.tsx +270 -0
- package/components/docs/skeleton-doc-page.tsx +230 -0
- package/components/docs/stepper-doc-page.tsx +309 -0
- package/components/docs/switch-doc-page.tsx +264 -0
- package/components/docs/table-doc-page.tsx +689 -0
- package/components/docs/tabs-doc-page.tsx +359 -0
- package/components/docs/theming-doc-page.tsx +243 -0
- package/components/docs/tooltip-doc-page.tsx +357 -0
- package/components/docs/usage-section.tsx +21 -0
- package/components/icons/discord.tsx +9 -0
- package/components/icons/github.tsx +9 -0
- package/components/icons/x.tsx +9 -0
- package/components/installation-section.tsx +34 -0
- package/components/landing/feature-highlights.tsx +105 -0
- package/components/landing/footer-strip.tsx +45 -0
- package/components/landing/hero-card.tsx +14 -0
- package/components/landing/hero-spotlight.tsx +126 -0
- package/components/landing/hero.tsx +64 -0
- package/components/landing/navbar.tsx +44 -0
- package/components/landing/quick-start-section.tsx +59 -0
- package/components/landing/site-footer.tsx +55 -0
- package/components/theme-provider.tsx +60 -0
- package/components/ui/accordion.tsx +174 -0
- package/components/ui/alert.tsx +103 -0
- package/components/ui/allinput.tsx +210 -0
- package/components/ui/avatar.tsx +179 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +75 -0
- package/components/ui/card.tsx +105 -0
- package/components/ui/checkbox.tsx +130 -0
- package/components/ui/code-block.tsx +60 -0
- package/components/ui/data-table.tsx +235 -0
- package/components/ui/dialog.tsx +173 -0
- package/components/ui/dropdown.tsx +78 -0
- package/components/ui/feature-card.tsx +38 -0
- package/components/ui/input.tsx +44 -0
- package/components/ui/label.tsx +110 -0
- package/components/ui/loaders.tsx +139 -0
- package/components/ui/menu.tsx +118 -0
- package/components/ui/modal.tsx +399 -0
- package/components/ui/package-manager-tabs.tsx +40 -0
- package/components/ui/pagination.tsx +200 -0
- package/components/ui/progress.tsx +134 -0
- package/components/ui/radio.tsx +262 -0
- package/components/ui/skeleton.tsx +102 -0
- package/components/ui/steppers.tsx +309 -0
- package/components/ui/switch.tsx +153 -0
- package/components/ui/table.tsx +58 -0
- package/components/ui/tabs.tsx +166 -0
- package/components/ui/theme-toggle.tsx +60 -0
- package/components/ui/tooltip.tsx +238 -0
- package/data/color-palette.ts +35 -0
- package/data/docs-nav.ts +84 -0
- package/eslint.config.mjs +18 -0
- package/index.ts +8 -0
- package/lib/cn.ts +4 -0
- package/lib/color.ts +85 -0
- package/lib/og-image.tsx +138 -0
- package/lib/theme.ts +33 -0
- package/next.config.ts +7 -0
- package/package.json +30 -0
- package/postcss.config.mjs +7 -0
- package/public/dark.png +0 -0
- package/public/dark1.png +0 -0
- package/public/dark2.png +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/light.png +0 -0
- package/public/light1.png +0 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/video/hero-demo.mp4 +0 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import { useTheme } from "@/components/theme-provider";
|
|
5
|
+
import { Moon, Sun } from "lucide-react";
|
|
6
|
+
import { cn } from "@/lib/cn";
|
|
7
|
+
|
|
8
|
+
const emptySubscribe = () => () => {};
|
|
9
|
+
function useMounted() {
|
|
10
|
+
return useSyncExternalStore(emptySubscribe, () => true, () => false);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ThemeToggle({ inverted = false }: { inverted?: boolean }) {
|
|
14
|
+
const { theme, setTheme } = useTheme();
|
|
15
|
+
const mounted = useMounted();
|
|
16
|
+
const current = mounted ? theme : "light";
|
|
17
|
+
|
|
18
|
+
const track = inverted ? "bg-zinc-800" : "bg-zinc-100 dark:bg-zinc-800";
|
|
19
|
+
const inactive = inverted
|
|
20
|
+
? "text-zinc-400 hover:text-zinc-200"
|
|
21
|
+
: "text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300";
|
|
22
|
+
const active = inverted
|
|
23
|
+
? "bg-zinc-700 text-white"
|
|
24
|
+
: "bg-white text-zinc-900 shadow-sm dark:bg-zinc-950 dark:text-zinc-50";
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn("inline-flex items-center gap-0.5 rounded-full p-0.5", track)}
|
|
29
|
+
role="radiogroup"
|
|
30
|
+
aria-label="Theme"
|
|
31
|
+
>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
role="radio"
|
|
35
|
+
aria-checked={current === "light"}
|
|
36
|
+
aria-label="Light theme"
|
|
37
|
+
onClick={() => setTheme("light")}
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex h-6 w-6 items-center justify-center rounded-full transition-colors",
|
|
40
|
+
current === "light" ? active : inactive,
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<Sun className="h-3.5 w-3.5" />
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
role="radio"
|
|
48
|
+
aria-checked={current === "dark"}
|
|
49
|
+
aria-label="Dark theme"
|
|
50
|
+
onClick={() => setTheme("dark")}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex h-6 w-6 items-center justify-center rounded-full transition-colors",
|
|
53
|
+
current === "dark" ? active : inactive,
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
<Moon className="h-3.5 w-3.5" />
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { cn } from "@/lib/cn";
|
|
7
|
+
|
|
8
|
+
const tooltipVariants = cva(
|
|
9
|
+
[
|
|
10
|
+
"pointer-events-none fixed z-50 max-w-[260px] rounded-lg px-3 py-2",
|
|
11
|
+
"text-[13px] font-medium leading-normal tracking-tight",
|
|
12
|
+
"bg-zinc-900 text-zinc-50",
|
|
13
|
+
"shadow-[0_4px_16px_rgba(0,0,0,0.14),0_0_0_1px_rgba(255,255,255,0.06)]",
|
|
14
|
+
"dark:bg-zinc-100 dark:text-zinc-900",
|
|
15
|
+
"dark:shadow-[0_4px_16px_rgba(0,0,0,0.35),0_0_0_1px_rgba(0,0,0,0.06)]",
|
|
16
|
+
"transition-[opacity,transform] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]",
|
|
17
|
+
"will-change-[opacity,transform]",
|
|
18
|
+
],
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
visible: {
|
|
22
|
+
true: "opacity-100 scale-100",
|
|
23
|
+
false: "opacity-0 scale-[0.97]",
|
|
24
|
+
},
|
|
25
|
+
side: {
|
|
26
|
+
top: "origin-bottom",
|
|
27
|
+
bottom: "origin-top",
|
|
28
|
+
left: "origin-right",
|
|
29
|
+
right: "origin-left",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: { visible: false, side: "top" },
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
type TooltipSide = "top" | "bottom" | "left" | "right";
|
|
37
|
+
type TooltipAlign = "start" | "center" | "end";
|
|
38
|
+
|
|
39
|
+
const VIEWPORT_PADDING = 10;
|
|
40
|
+
const ARROW_SIZE = 6;
|
|
41
|
+
|
|
42
|
+
export interface TooltipProps extends VariantProps<typeof tooltipVariants> {
|
|
43
|
+
content: React.ReactNode;
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
side?: TooltipSide;
|
|
46
|
+
align?: TooltipAlign;
|
|
47
|
+
sideOffset?: number;
|
|
48
|
+
delayDuration?: number;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getTooltipPosition(
|
|
54
|
+
triggerRect: DOMRect,
|
|
55
|
+
tooltipRect: DOMRect,
|
|
56
|
+
side: TooltipSide,
|
|
57
|
+
align: TooltipAlign,
|
|
58
|
+
sideOffset: number,
|
|
59
|
+
) {
|
|
60
|
+
const offset = sideOffset + ARROW_SIZE;
|
|
61
|
+
let top = 0;
|
|
62
|
+
let left = 0;
|
|
63
|
+
|
|
64
|
+
if (side === "top") {
|
|
65
|
+
top = triggerRect.top - tooltipRect.height - offset;
|
|
66
|
+
left = alignTooltipHorizontal(triggerRect, tooltipRect, align);
|
|
67
|
+
} else if (side === "bottom") {
|
|
68
|
+
top = triggerRect.bottom + offset;
|
|
69
|
+
left = alignTooltipHorizontal(triggerRect, tooltipRect, align);
|
|
70
|
+
} else if (side === "left") {
|
|
71
|
+
top = alignTooltipVertical(triggerRect, tooltipRect, align);
|
|
72
|
+
left = triggerRect.left - tooltipRect.width - offset;
|
|
73
|
+
} else {
|
|
74
|
+
top = alignTooltipVertical(triggerRect, tooltipRect, align);
|
|
75
|
+
left = triggerRect.right + offset;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const maxLeft = window.innerWidth - tooltipRect.width - VIEWPORT_PADDING;
|
|
79
|
+
const maxTop = window.innerHeight - tooltipRect.height - VIEWPORT_PADDING;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
top: Math.min(Math.max(top, VIEWPORT_PADDING), maxTop),
|
|
83
|
+
left: Math.min(Math.max(left, VIEWPORT_PADDING), maxLeft),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function alignTooltipHorizontal(
|
|
88
|
+
triggerRect: DOMRect,
|
|
89
|
+
tooltipRect: DOMRect,
|
|
90
|
+
align: TooltipAlign,
|
|
91
|
+
) {
|
|
92
|
+
if (align === "start") return triggerRect.left;
|
|
93
|
+
if (align === "end") return triggerRect.right - tooltipRect.width;
|
|
94
|
+
return triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function alignTooltipVertical(
|
|
98
|
+
triggerRect: DOMRect,
|
|
99
|
+
tooltipRect: DOMRect,
|
|
100
|
+
align: TooltipAlign,
|
|
101
|
+
) {
|
|
102
|
+
if (align === "start") return triggerRect.top;
|
|
103
|
+
if (align === "end") return triggerRect.bottom - tooltipRect.height;
|
|
104
|
+
return triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function TooltipArrow({ side }: { side: TooltipSide }) {
|
|
108
|
+
const position = {
|
|
109
|
+
top: "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%-1px)]",
|
|
110
|
+
bottom: "top-0 left-1/2 -translate-x-1/2 -translate-y-[calc(100%-1px)] rotate-180",
|
|
111
|
+
left: "right-0 top-1/2 -translate-y-1/2 translate-x-[calc(100%-1px)] -rotate-90",
|
|
112
|
+
right: "left-0 top-1/2 -translate-y-1/2 -translate-x-[calc(100%-1px)] rotate-90",
|
|
113
|
+
}[side];
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<svg
|
|
117
|
+
aria-hidden
|
|
118
|
+
width="10"
|
|
119
|
+
height="5"
|
|
120
|
+
viewBox="0 0 10 5"
|
|
121
|
+
className={cn(
|
|
122
|
+
"absolute text-zinc-900 dark:text-zinc-100",
|
|
123
|
+
position,
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<path fill="currentColor" d="M0 0h10L5 5 0 0z" />
|
|
127
|
+
</svg>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function Tooltip({
|
|
132
|
+
content,
|
|
133
|
+
children,
|
|
134
|
+
side = "top",
|
|
135
|
+
align = "center",
|
|
136
|
+
sideOffset = 8,
|
|
137
|
+
delayDuration = 250,
|
|
138
|
+
disabled = false,
|
|
139
|
+
className,
|
|
140
|
+
}: TooltipProps) {
|
|
141
|
+
const [open, setOpen] = React.useState(false);
|
|
142
|
+
const [visible, setVisible] = React.useState(false);
|
|
143
|
+
const [mounted, setMounted] = React.useState(false);
|
|
144
|
+
const [position, setPosition] = React.useState({ top: -9999, left: -9999 });
|
|
145
|
+
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
146
|
+
const triggerRef = React.useRef<HTMLSpanElement>(null);
|
|
147
|
+
const tooltipRef = React.useRef<HTMLDivElement>(null);
|
|
148
|
+
const id = React.useId();
|
|
149
|
+
|
|
150
|
+
React.useEffect(() => setMounted(true), []);
|
|
151
|
+
|
|
152
|
+
const clearTimer = () => {
|
|
153
|
+
if (timeoutRef.current) {
|
|
154
|
+
clearTimeout(timeoutRef.current);
|
|
155
|
+
timeoutRef.current = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const updatePosition = React.useCallback(() => {
|
|
160
|
+
const trigger = triggerRef.current;
|
|
161
|
+
const tooltip = tooltipRef.current;
|
|
162
|
+
if (!trigger || !tooltip) return;
|
|
163
|
+
|
|
164
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
165
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
166
|
+
setPosition(getTooltipPosition(triggerRect, tooltipRect, side, align, sideOffset));
|
|
167
|
+
requestAnimationFrame(() => setVisible(true));
|
|
168
|
+
}, [side, align, sideOffset]);
|
|
169
|
+
|
|
170
|
+
const show = () => {
|
|
171
|
+
if (disabled) return;
|
|
172
|
+
clearTimer();
|
|
173
|
+
timeoutRef.current = setTimeout(() => {
|
|
174
|
+
setVisible(false);
|
|
175
|
+
setOpen(true);
|
|
176
|
+
}, delayDuration);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const hide = () => {
|
|
180
|
+
clearTimer();
|
|
181
|
+
setVisible(false);
|
|
182
|
+
setOpen(false);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
React.useEffect(() => clearTimer, []);
|
|
186
|
+
|
|
187
|
+
React.useLayoutEffect(() => {
|
|
188
|
+
if (!open) return;
|
|
189
|
+
updatePosition();
|
|
190
|
+
const handleUpdate = () => updatePosition();
|
|
191
|
+
window.addEventListener("scroll", handleUpdate, true);
|
|
192
|
+
window.addEventListener("resize", handleUpdate);
|
|
193
|
+
return () => {
|
|
194
|
+
window.removeEventListener("scroll", handleUpdate, true);
|
|
195
|
+
window.removeEventListener("resize", handleUpdate);
|
|
196
|
+
};
|
|
197
|
+
}, [open, updatePosition, content]);
|
|
198
|
+
|
|
199
|
+
const child = React.Children.only(children);
|
|
200
|
+
const trigger = React.isValidElement(child)
|
|
201
|
+
? React.cloneElement(
|
|
202
|
+
child as React.ReactElement<{ "aria-describedby"?: string }>,
|
|
203
|
+
{ "aria-describedby": open && visible ? id : undefined },
|
|
204
|
+
)
|
|
205
|
+
: children;
|
|
206
|
+
|
|
207
|
+
const tooltipNode = open ? (
|
|
208
|
+
<div
|
|
209
|
+
ref={tooltipRef}
|
|
210
|
+
id={id}
|
|
211
|
+
role="tooltip"
|
|
212
|
+
aria-hidden={!visible}
|
|
213
|
+
style={{ top: position.top, left: position.left }}
|
|
214
|
+
className={cn(tooltipVariants({ visible, side }), className)}
|
|
215
|
+
>
|
|
216
|
+
{content}
|
|
217
|
+
<TooltipArrow side={side} />
|
|
218
|
+
</div>
|
|
219
|
+
) : null;
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<>
|
|
223
|
+
<span
|
|
224
|
+
ref={triggerRef}
|
|
225
|
+
className="inline-flex"
|
|
226
|
+
onMouseEnter={show}
|
|
227
|
+
onMouseLeave={hide}
|
|
228
|
+
onFocus={show}
|
|
229
|
+
onBlur={hide}
|
|
230
|
+
>
|
|
231
|
+
{trigger}
|
|
232
|
+
</span>
|
|
233
|
+
{mounted && tooltipNode ? createPortal(tooltipNode, document.body) : null}
|
|
234
|
+
</>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { Tooltip, tooltipVariants };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ColorShade {
|
|
2
|
+
step: number;
|
|
3
|
+
hex: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ColorHue {
|
|
7
|
+
name: string;
|
|
8
|
+
shades: ColorShade[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
12
|
+
|
|
13
|
+
function hue(name: string, hexes: string[]): ColorHue {
|
|
14
|
+
return {
|
|
15
|
+
name,
|
|
16
|
+
shades: STEPS.map((step, i) => ({ step, hex: hexes[i] })),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Curated primary set — one clearly distinct hue per color family,
|
|
22
|
+
* so the overview strip never shows two near-identical colors side by side.
|
|
23
|
+
*/
|
|
24
|
+
export const COLOR_PALETTE: ColorHue[] = [
|
|
25
|
+
hue("Red", ["#fef2f2", "#fee2e2", "#fecaca", "#fca5a5", "#f87171", "#ef4444", "#dc2626", "#b91c1c", "#991b1b", "#7f1d1d", "#450a0a"]),
|
|
26
|
+
hue("Orange", ["#fff7ed", "#ffedd5", "#fed7aa", "#fdba74", "#fb923c", "#f97316", "#ea580c", "#c2410c", "#9a3412", "#7c2d12", "#431407"]),
|
|
27
|
+
hue("Yellow", ["#fefce8", "#fef9c3", "#fef08a", "#fde047", "#facc15", "#eab308", "#ca8a04", "#a16207", "#854d0e", "#713f12", "#422006"]),
|
|
28
|
+
hue("Green", ["#f0fdf4", "#dcfce7", "#bbf7d0", "#86efac", "#4ade80", "#22c55e", "#16a34a", "#15803d", "#166534", "#14532d", "#052e16"]),
|
|
29
|
+
hue("Teal", ["#f0fdfa", "#ccfbf1", "#99f6e4", "#5eead4", "#2dd4bf", "#14b8a6", "#0d9488", "#0f766e", "#115e59", "#134e4a", "#042f2e"]),
|
|
30
|
+
hue("Blue", ["#eff6ff", "#dbeafe", "#bfdbfe", "#93c5fd", "#60a5fa", "#3b82f6", "#2563eb", "#1d4ed8", "#1e40af", "#1e3a8a", "#172554"]),
|
|
31
|
+
hue("Indigo", ["#eef2ff", "#e0e7ff", "#c7d2fe", "#a5b4fc", "#818cf8", "#6366f1", "#4f46e5", "#4338ca", "#3730a3", "#312e81", "#1e1b4b"]),
|
|
32
|
+
hue("Purple", ["#faf5ff", "#f3e8ff", "#e9d5ff", "#d8b4fe", "#c084fc", "#a855f7", "#9333ea", "#7e22ce", "#6b21a8", "#581c87", "#3b0764"]),
|
|
33
|
+
hue("Pink", ["#fdf2f8", "#fce7f3", "#fbcfe8", "#f9a8d4", "#f472b6", "#ec4899", "#db2777", "#be185d", "#9d174d", "#831843", "#500724"]),
|
|
34
|
+
hue("Gray", ["#fafafa", "#f4f4f5", "#e4e4e7", "#d4d4d8", "#a1a1aa", "#71717a", "#52525b", "#3f3f46", "#27272a", "#18181b", "#09090b"]),
|
|
35
|
+
];
|
package/data/docs-nav.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppWindow,
|
|
3
|
+
BadgeCheck,
|
|
4
|
+
ChevronDown,
|
|
5
|
+
ChevronsUpDown,
|
|
6
|
+
CircleDot,
|
|
7
|
+
CircleUserRound,
|
|
8
|
+
Columns3,
|
|
9
|
+
Compass,
|
|
10
|
+
GaugeCircle,
|
|
11
|
+
Home,
|
|
12
|
+
Layers,
|
|
13
|
+
LayoutTemplate,
|
|
14
|
+
ListFilter,
|
|
15
|
+
Loader2,
|
|
16
|
+
Menu,
|
|
17
|
+
MessageCircle,
|
|
18
|
+
MessageSquareText,
|
|
19
|
+
MoreHorizontal,
|
|
20
|
+
MousePointerClick,
|
|
21
|
+
Move,
|
|
22
|
+
Palette,
|
|
23
|
+
Pipette,
|
|
24
|
+
Rows3,
|
|
25
|
+
Shapes,
|
|
26
|
+
SquareCheck,
|
|
27
|
+
Table2,
|
|
28
|
+
Tag,
|
|
29
|
+
TextCursorInput,
|
|
30
|
+
TextQuote,
|
|
31
|
+
ToggleLeft,
|
|
32
|
+
TriangleAlert,
|
|
33
|
+
type LucideIcon,
|
|
34
|
+
} from "lucide-react";
|
|
35
|
+
|
|
36
|
+
export interface DocsNavItem {
|
|
37
|
+
title: string;
|
|
38
|
+
slug: string;
|
|
39
|
+
icon: LucideIcon;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DocsNavGroup {
|
|
43
|
+
title: string;
|
|
44
|
+
items: DocsNavItem[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const DOCS_NAV: DocsNavGroup[] = [
|
|
48
|
+
{
|
|
49
|
+
title: "Components",
|
|
50
|
+
items: [
|
|
51
|
+
{ title: "Introduction", slug: "", icon: Home },
|
|
52
|
+
{ title: "Getting Started", slug: "getting-started", icon: Compass },
|
|
53
|
+
{ title: "Accordion", slug: "accordion", icon: ChevronsUpDown },
|
|
54
|
+
{ title: "Alert", slug: "alert", icon: TriangleAlert },
|
|
55
|
+
{ title: "Avatar", slug: "avatar", icon: CircleUserRound },
|
|
56
|
+
{ title: "Badge", slug: "badge", icon: BadgeCheck },
|
|
57
|
+
{ title: "Button", slug: "button", icon: MousePointerClick },
|
|
58
|
+
{ title: "Card", slug: "card", icon: LayoutTemplate },
|
|
59
|
+
{ title: "Checkbox", slug: "checkbox", icon: SquareCheck },
|
|
60
|
+
{ title: "Dialog", slug: "dialog", icon: AppWindow },
|
|
61
|
+
{ title: "Loader", slug: "loader", icon: Loader2 },
|
|
62
|
+
{ title: "Dropdown", slug: "dropdown", icon: ChevronDown },
|
|
63
|
+
{ title: "Input", slug: "input", icon: TextCursorInput },
|
|
64
|
+
{ title: "Label", slug: "label", icon: Tag },
|
|
65
|
+
{ title: "Modal", slug: "modal", icon: Layers },
|
|
66
|
+
{ title: "Pagination", slug: "pagination", icon: MoreHorizontal },
|
|
67
|
+
{ title: "Progress", slug: "progress", icon: GaugeCircle },
|
|
68
|
+
{ title: "Radio", slug: "radio", icon: CircleDot },
|
|
69
|
+
{ title: "Skeleton", slug: "skeleton", icon: Rows3 },
|
|
70
|
+
{ title: "Switch", slug: "switch", icon: ToggleLeft },
|
|
71
|
+
{ title: "Table", slug: "table", icon: Table2 },
|
|
72
|
+
{ title: "Tabs", slug: "tabs", icon: Columns3 },
|
|
73
|
+
{ title: "Stepper", slug: "stepper", icon: TextQuote },
|
|
74
|
+
{ title: "Tooltip", slug: "tooltip", icon: MessageCircle },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
title: "Theming",
|
|
79
|
+
items: [
|
|
80
|
+
{ title: "Theming", slug: "theming", icon: Palette },
|
|
81
|
+
{ title: "Colors", slug: "colors", icon: Pipette },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
package/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Progress } from "./components/ui/progress";
|
|
2
|
+
export type { ProgressProps } from "./components/ui/progress";
|
|
3
|
+
|
|
4
|
+
export { Switch } from "./components/ui/switch";
|
|
5
|
+
export type { SwitchProps } from "./components/ui/switch";
|
|
6
|
+
|
|
7
|
+
export { Tooltip } from "./components/ui/tooltip";
|
|
8
|
+
export type { TooltipProps } from "./components/ui/tooltip";
|
package/lib/cn.ts
ADDED
package/lib/color.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function hexToRgb(hex: string): [number, number, number] {
|
|
2
|
+
const clean = hex.replace("#", "");
|
|
3
|
+
const r = parseInt(clean.slice(0, 2), 16);
|
|
4
|
+
const g = parseInt(clean.slice(2, 4), 16);
|
|
5
|
+
const b = parseInt(clean.slice(4, 6), 16);
|
|
6
|
+
return [r, g, b];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
|
10
|
+
const rn = r / 255;
|
|
11
|
+
const gn = g / 255;
|
|
12
|
+
const bn = b / 255;
|
|
13
|
+
const max = Math.max(rn, gn, bn);
|
|
14
|
+
const min = Math.min(rn, gn, bn);
|
|
15
|
+
const l = (max + min) / 2;
|
|
16
|
+
|
|
17
|
+
if (max === min) return [0, 0, Math.round(l * 100)];
|
|
18
|
+
|
|
19
|
+
const d = max - min;
|
|
20
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
21
|
+
|
|
22
|
+
let h = 0;
|
|
23
|
+
switch (max) {
|
|
24
|
+
case rn:
|
|
25
|
+
h = (gn - bn) / d + (gn < bn ? 6 : 0);
|
|
26
|
+
break;
|
|
27
|
+
case gn:
|
|
28
|
+
h = (bn - rn) / d + 2;
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
h = (rn - gn) / d + 4;
|
|
32
|
+
}
|
|
33
|
+
h /= 6;
|
|
34
|
+
|
|
35
|
+
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ColorFormat = "hex" | "rgb" | "hsl" | "css";
|
|
39
|
+
|
|
40
|
+
export function formatColor(hex: string, format: ColorFormat): string {
|
|
41
|
+
const [r, g, b] = hexToRgb(hex);
|
|
42
|
+
|
|
43
|
+
switch (format) {
|
|
44
|
+
case "hex":
|
|
45
|
+
return hex.toUpperCase();
|
|
46
|
+
case "rgb":
|
|
47
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
48
|
+
case "hsl": {
|
|
49
|
+
const [h, s, l] = rgbToHsl(r, g, b);
|
|
50
|
+
return `hsl(${h}, ${s}%, ${l}%)`;
|
|
51
|
+
}
|
|
52
|
+
case "css":
|
|
53
|
+
return `rgb(${r} ${g} ${b} / 1)`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getLuminance(hex: string): number {
|
|
58
|
+
const [r, g, b] = hexToRgb(hex).map((channel) => {
|
|
59
|
+
const s = channel / 255;
|
|
60
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
61
|
+
});
|
|
62
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getContrastRatio(hexA: string, hexB: string): number {
|
|
66
|
+
const a = getLuminance(hexA);
|
|
67
|
+
const b = getLuminance(hexB);
|
|
68
|
+
const lighter = Math.max(a, b);
|
|
69
|
+
const darker = Math.min(a, b);
|
|
70
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ContrastLabel = "AAA" | "AA" | "Fail";
|
|
74
|
+
|
|
75
|
+
export function contrastLabel(ratio: number): ContrastLabel {
|
|
76
|
+
if (ratio >= 7) return "AAA";
|
|
77
|
+
if (ratio >= 4.5) return "AA";
|
|
78
|
+
return "Fail";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function readableTextColor(hex: string): "#000000" | "#ffffff" {
|
|
82
|
+
return getContrastRatio(hex, "#000000") >= getContrastRatio(hex, "#ffffff")
|
|
83
|
+
? "#000000"
|
|
84
|
+
: "#ffffff";
|
|
85
|
+
}
|
package/lib/og-image.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ImageResponse } from "next/og";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const ogSize = { width: 1200, height: 630 };
|
|
6
|
+
|
|
7
|
+
export const ogAlt =
|
|
8
|
+
"Ruler.ui — A sharper way to build interfaces. Precision-crafted React components for modern products.";
|
|
9
|
+
|
|
10
|
+
async function loadGoogleFont(family: string, weight: number) {
|
|
11
|
+
const query = family.replace(/ /g, "+");
|
|
12
|
+
const css = await fetch(
|
|
13
|
+
`https://fonts.googleapis.com/css2?family=${query}:wght@${weight}&display=swap`,
|
|
14
|
+
{
|
|
15
|
+
headers: {
|
|
16
|
+
"User-Agent":
|
|
17
|
+
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
).then((res) => res.text());
|
|
21
|
+
|
|
22
|
+
const match = css.match(/src: url\((.+?)\) format\('(opentype|truetype)'\)/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error(`Could not load ${family}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return fetch(match[1]).then((res) => res.arrayBuffer());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function createOgImage() {
|
|
31
|
+
const [iconData, orbitron, inter] = await Promise.all([
|
|
32
|
+
readFile(join(process.cwd(), "app/icon.png")),
|
|
33
|
+
loadGoogleFont("Orbitron", 700),
|
|
34
|
+
loadGoogleFont("Inter", 500),
|
|
35
|
+
]);
|
|
36
|
+
const iconSrc = `data:image/png;base64,${iconData.toString("base64")}`;
|
|
37
|
+
|
|
38
|
+
return new ImageResponse(
|
|
39
|
+
(
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
width: "100%",
|
|
43
|
+
height: "100%",
|
|
44
|
+
display: "flex",
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
justifyContent: "center",
|
|
47
|
+
backgroundColor: "#09090b",
|
|
48
|
+
backgroundImage:
|
|
49
|
+
"linear-gradient(to right, rgba(255,255,255,0.05) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.05) 1px, transparent 1px)",
|
|
50
|
+
backgroundSize: "56px 56px",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
display: "flex",
|
|
56
|
+
flexDirection: "column",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
width: 1040,
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
display: "flex",
|
|
64
|
+
width: 112,
|
|
65
|
+
height: 112,
|
|
66
|
+
borderRadius: 28,
|
|
67
|
+
overflow: "hidden",
|
|
68
|
+
marginBottom: 36,
|
|
69
|
+
boxShadow: "0 12px 48px rgba(0,0,0,0.5)",
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
73
|
+
<img src={iconSrc} width={112} height={112} alt="" />
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
display: "flex",
|
|
79
|
+
fontFamily: "Orbitron",
|
|
80
|
+
fontSize: 84,
|
|
81
|
+
fontWeight: 700,
|
|
82
|
+
color: "#fafafa",
|
|
83
|
+
letterSpacing: "0.04em",
|
|
84
|
+
lineHeight: 1,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
Ruler.ui
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div
|
|
91
|
+
style={{
|
|
92
|
+
display: "flex",
|
|
93
|
+
marginTop: 28,
|
|
94
|
+
fontFamily: "Inter",
|
|
95
|
+
fontSize: 34,
|
|
96
|
+
fontWeight: 500,
|
|
97
|
+
color: "#e4e4e7",
|
|
98
|
+
letterSpacing: "-0.03em",
|
|
99
|
+
lineHeight: 1.2,
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
A sharper way to build interfaces.
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div
|
|
106
|
+
style={{
|
|
107
|
+
display: "flex",
|
|
108
|
+
marginTop: 12,
|
|
109
|
+
fontFamily: "Inter",
|
|
110
|
+
fontSize: 24,
|
|
111
|
+
color: "#a1a1aa",
|
|
112
|
+
letterSpacing: "-0.01em",
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
Precision-crafted React components for modern products.
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
),
|
|
120
|
+
{
|
|
121
|
+
...ogSize,
|
|
122
|
+
fonts: [
|
|
123
|
+
{
|
|
124
|
+
name: "Orbitron",
|
|
125
|
+
data: orbitron,
|
|
126
|
+
style: "normal",
|
|
127
|
+
weight: 700,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "Inter",
|
|
131
|
+
data: inter,
|
|
132
|
+
style: "normal",
|
|
133
|
+
weight: 500,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|