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.
Files changed (114) hide show
  1. package/AGENTS.md +9 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +45 -0
  5. package/app/docs/[slug]/page.tsx +147 -0
  6. package/app/docs/layout.tsx +26 -0
  7. package/app/docs/page.tsx +148 -0
  8. package/app/favicon.ico +0 -0
  9. package/app/globals.css +168 -0
  10. package/app/icon.png +0 -0
  11. package/app/layout.tsx +90 -0
  12. package/app/opengraph-image.tsx +10 -0
  13. package/app/page.tsx +34 -0
  14. package/app/twitter-image.tsx +10 -0
  15. package/components/docs/accordion-doc-page.tsx +352 -0
  16. package/components/docs/alert-doc-page.tsx +293 -0
  17. package/components/docs/allinput-doc-page.tsx +183 -0
  18. package/components/docs/avatar-doc-page.tsx +424 -0
  19. package/components/docs/badge-doc-page.tsx +363 -0
  20. package/components/docs/button-doc-page.tsx +406 -0
  21. package/components/docs/card-doc-page.tsx +364 -0
  22. package/components/docs/checkbox-doc-page.tsx +277 -0
  23. package/components/docs/colors-doc-page.tsx +430 -0
  24. package/components/docs/dialog-doc-page.tsx +364 -0
  25. package/components/docs/docs-footer.tsx +11 -0
  26. package/components/docs/docs-mobile-menu-bar.tsx +22 -0
  27. package/components/docs/docs-mobile-nav-context.tsx +27 -0
  28. package/components/docs/docs-page.tsx +263 -0
  29. package/components/docs/docs-search.tsx +303 -0
  30. package/components/docs/docs-sidebar.tsx +165 -0
  31. package/components/docs/docs-topbar.tsx +32 -0
  32. package/components/docs/dropdown-doc-page.tsx +522 -0
  33. package/components/docs/getting-started-doc-page.tsx +205 -0
  34. package/components/docs/label-doc-page.tsx +173 -0
  35. package/components/docs/loaders-doc-page.tsx +244 -0
  36. package/components/docs/menu-doc-page.tsx +373 -0
  37. package/components/docs/modal-doc-page.tsx +436 -0
  38. package/components/docs/next-steps.tsx +53 -0
  39. package/components/docs/pagination-doc-page.tsx +316 -0
  40. package/components/docs/progress-doc-page.tsx +387 -0
  41. package/components/docs/radio-doc-page.tsx +270 -0
  42. package/components/docs/skeleton-doc-page.tsx +230 -0
  43. package/components/docs/stepper-doc-page.tsx +309 -0
  44. package/components/docs/switch-doc-page.tsx +264 -0
  45. package/components/docs/table-doc-page.tsx +689 -0
  46. package/components/docs/tabs-doc-page.tsx +359 -0
  47. package/components/docs/theming-doc-page.tsx +243 -0
  48. package/components/docs/tooltip-doc-page.tsx +357 -0
  49. package/components/docs/usage-section.tsx +21 -0
  50. package/components/icons/discord.tsx +9 -0
  51. package/components/icons/github.tsx +9 -0
  52. package/components/icons/x.tsx +9 -0
  53. package/components/installation-section.tsx +34 -0
  54. package/components/landing/feature-highlights.tsx +105 -0
  55. package/components/landing/footer-strip.tsx +45 -0
  56. package/components/landing/hero-card.tsx +14 -0
  57. package/components/landing/hero-spotlight.tsx +126 -0
  58. package/components/landing/hero.tsx +64 -0
  59. package/components/landing/navbar.tsx +44 -0
  60. package/components/landing/quick-start-section.tsx +59 -0
  61. package/components/landing/site-footer.tsx +55 -0
  62. package/components/theme-provider.tsx +60 -0
  63. package/components/ui/accordion.tsx +174 -0
  64. package/components/ui/alert.tsx +103 -0
  65. package/components/ui/allinput.tsx +210 -0
  66. package/components/ui/avatar.tsx +179 -0
  67. package/components/ui/badge.tsx +52 -0
  68. package/components/ui/button.tsx +75 -0
  69. package/components/ui/card.tsx +105 -0
  70. package/components/ui/checkbox.tsx +130 -0
  71. package/components/ui/code-block.tsx +60 -0
  72. package/components/ui/data-table.tsx +235 -0
  73. package/components/ui/dialog.tsx +173 -0
  74. package/components/ui/dropdown.tsx +78 -0
  75. package/components/ui/feature-card.tsx +38 -0
  76. package/components/ui/input.tsx +44 -0
  77. package/components/ui/label.tsx +110 -0
  78. package/components/ui/loaders.tsx +139 -0
  79. package/components/ui/menu.tsx +118 -0
  80. package/components/ui/modal.tsx +399 -0
  81. package/components/ui/package-manager-tabs.tsx +40 -0
  82. package/components/ui/pagination.tsx +200 -0
  83. package/components/ui/progress.tsx +134 -0
  84. package/components/ui/radio.tsx +262 -0
  85. package/components/ui/skeleton.tsx +102 -0
  86. package/components/ui/steppers.tsx +309 -0
  87. package/components/ui/switch.tsx +153 -0
  88. package/components/ui/table.tsx +58 -0
  89. package/components/ui/tabs.tsx +166 -0
  90. package/components/ui/theme-toggle.tsx +60 -0
  91. package/components/ui/tooltip.tsx +238 -0
  92. package/data/color-palette.ts +35 -0
  93. package/data/docs-nav.ts +84 -0
  94. package/eslint.config.mjs +18 -0
  95. package/index.ts +8 -0
  96. package/lib/cn.ts +4 -0
  97. package/lib/color.ts +85 -0
  98. package/lib/og-image.tsx +138 -0
  99. package/lib/theme.ts +33 -0
  100. package/next.config.ts +7 -0
  101. package/package.json +30 -0
  102. package/postcss.config.mjs +7 -0
  103. package/public/dark.png +0 -0
  104. package/public/dark1.png +0 -0
  105. package/public/dark2.png +0 -0
  106. package/public/file.svg +1 -0
  107. package/public/globe.svg +1 -0
  108. package/public/light.png +0 -0
  109. package/public/light1.png +0 -0
  110. package/public/next.svg +1 -0
  111. package/public/vercel.svg +1 -0
  112. package/public/video/hero-demo.mp4 +0 -0
  113. package/public/window.svg +1 -0
  114. 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
+ ];
@@ -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
@@ -0,0 +1,4 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
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
+ }
@@ -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
+ }