saasaloy 0.2.0 → 0.3.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/README.md +5 -4
- package/dist/index.js +413 -21
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/schemas/manifest.schema.json +2 -1
- package/schemas/registry-item.schema.json +47 -2
- package/templates/base/AGENTS.md +8 -1
- package/templates/base/DESIGN.md +4 -0
- package/templates/base/_agents/skills/saasaloy-design/SKILL.md +1 -1
- package/templates/base/apps/web/package.json +1 -1
- package/templates/base/package.json +4 -4
- package/templates/base/packages/ui/package.json +3 -3
- package/templates/base/packages/ui/src/blocks/footer.tsx +5 -1
- package/templates/base/packages/ui/src/components/avatar.tsx +107 -0
- package/templates/base/packages/ui/src/components/collapsible.tsx +19 -0
- package/templates/base/packages/ui/src/components/dropdown-menu.tsx +270 -0
- package/templates/base/packages/ui/src/components/scroll-area.tsx +52 -0
- package/templates/base/packages/ui/src/components/sheet.tsx +132 -0
- package/templates/base/packages/ui/src/components/table.tsx +114 -0
- package/templates/base/packages/ui/src/components/tabs.tsx +81 -0
- package/templates/base/packages/ui/src/components/tooltip.tsx +64 -0
- package/templates/base/packages/ui/src/lib/sentinel.ts +1 -1
- package/templates/base/packages/ui/src/lib/theme.ts +124 -1
- package/templates/base/packages/ui/src/styles/globals.css +28 -1
- package/templates/base/stylelint.config.js +8 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@repo/ui/lib/utils";
|
|
5
|
+
import { Button } from "@repo/ui/components/button";
|
|
6
|
+
import { XIcon } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
|
|
9
|
+
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
|
|
13
|
+
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
|
|
17
|
+
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
|
|
21
|
+
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
|
|
25
|
+
return (
|
|
26
|
+
<SheetPrimitive.Backdrop
|
|
27
|
+
data-slot="sheet-overlay"
|
|
28
|
+
className={cn(
|
|
29
|
+
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function SheetContent({
|
|
38
|
+
className,
|
|
39
|
+
children,
|
|
40
|
+
side = "right",
|
|
41
|
+
showCloseButton = true,
|
|
42
|
+
...props
|
|
43
|
+
}: SheetPrimitive.Popup.Props & {
|
|
44
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
45
|
+
showCloseButton?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<SheetPortal>
|
|
49
|
+
<SheetOverlay />
|
|
50
|
+
<SheetPrimitive.Popup
|
|
51
|
+
data-slot="sheet-content"
|
|
52
|
+
data-side={side}
|
|
53
|
+
className={cn(
|
|
54
|
+
"bg-popover text-popover-foreground fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
|
|
55
|
+
className
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
{showCloseButton && (
|
|
61
|
+
<SheetPrimitive.Close
|
|
62
|
+
data-slot="sheet-close"
|
|
63
|
+
render={
|
|
64
|
+
<Button
|
|
65
|
+
variant="ghost"
|
|
66
|
+
className="absolute top-3 right-3"
|
|
67
|
+
size="icon-sm"
|
|
68
|
+
/>
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
<XIcon />
|
|
72
|
+
<span className="sr-only">Close</span>
|
|
73
|
+
</SheetPrimitive.Close>
|
|
74
|
+
)}
|
|
75
|
+
</SheetPrimitive.Popup>
|
|
76
|
+
</SheetPortal>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
data-slot="sheet-header"
|
|
84
|
+
className={cn("flex flex-col gap-0.5 p-4", className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
data-slot="sheet-footer"
|
|
94
|
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
|
101
|
+
return (
|
|
102
|
+
<SheetPrimitive.Title
|
|
103
|
+
data-slot="sheet-title"
|
|
104
|
+
className={cn("text-foreground text-base font-medium", className)}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function SheetDescription({
|
|
111
|
+
className,
|
|
112
|
+
...props
|
|
113
|
+
}: SheetPrimitive.Description.Props) {
|
|
114
|
+
return (
|
|
115
|
+
<SheetPrimitive.Description
|
|
116
|
+
data-slot="sheet-description"
|
|
117
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
Sheet,
|
|
125
|
+
SheetTrigger,
|
|
126
|
+
SheetClose,
|
|
127
|
+
SheetContent,
|
|
128
|
+
SheetHeader,
|
|
129
|
+
SheetFooter,
|
|
130
|
+
SheetTitle,
|
|
131
|
+
SheetDescription,
|
|
132
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@repo/ui/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="table-container"
|
|
9
|
+
className="relative w-full overflow-x-auto"
|
|
10
|
+
>
|
|
11
|
+
<table
|
|
12
|
+
data-slot="table"
|
|
13
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
21
|
+
return (
|
|
22
|
+
<thead
|
|
23
|
+
data-slot="table-header"
|
|
24
|
+
className={cn("[&_tr]:border-b", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
31
|
+
return (
|
|
32
|
+
<tbody
|
|
33
|
+
data-slot="table-body"
|
|
34
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
41
|
+
return (
|
|
42
|
+
<tfoot
|
|
43
|
+
data-slot="table-footer"
|
|
44
|
+
className={cn(
|
|
45
|
+
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
54
|
+
return (
|
|
55
|
+
<tr
|
|
56
|
+
data-slot="table-row"
|
|
57
|
+
className={cn(
|
|
58
|
+
"hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
67
|
+
return (
|
|
68
|
+
<th
|
|
69
|
+
data-slot="table-head"
|
|
70
|
+
className={cn(
|
|
71
|
+
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
80
|
+
return (
|
|
81
|
+
<td
|
|
82
|
+
data-slot="table-cell"
|
|
83
|
+
className={cn(
|
|
84
|
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function TableCaption({
|
|
93
|
+
className,
|
|
94
|
+
...props
|
|
95
|
+
}: React.ComponentProps<"caption">) {
|
|
96
|
+
return (
|
|
97
|
+
<caption
|
|
98
|
+
data-slot="table-caption"
|
|
99
|
+
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
Table,
|
|
107
|
+
TableHeader,
|
|
108
|
+
TableBody,
|
|
109
|
+
TableFooter,
|
|
110
|
+
TableHead,
|
|
111
|
+
TableRow,
|
|
112
|
+
TableCell,
|
|
113
|
+
TableCaption,
|
|
114
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import type { VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@repo/ui/lib/utils";
|
|
6
|
+
|
|
7
|
+
function Tabs({
|
|
8
|
+
className,
|
|
9
|
+
orientation = "horizontal",
|
|
10
|
+
...props
|
|
11
|
+
}: TabsPrimitive.Root.Props) {
|
|
12
|
+
return (
|
|
13
|
+
<TabsPrimitive.Root
|
|
14
|
+
data-slot="tabs"
|
|
15
|
+
data-orientation={orientation}
|
|
16
|
+
className={cn(
|
|
17
|
+
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tabsListVariants = cva(
|
|
26
|
+
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-8 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
|
|
27
|
+
{
|
|
28
|
+
variants: {
|
|
29
|
+
variant: {
|
|
30
|
+
default: "bg-muted",
|
|
31
|
+
line: "gap-1 bg-transparent",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: "default",
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function TabsList({
|
|
41
|
+
className,
|
|
42
|
+
variant = "default",
|
|
43
|
+
...props
|
|
44
|
+
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
|
|
45
|
+
return (
|
|
46
|
+
<TabsPrimitive.List
|
|
47
|
+
data-slot="tabs-list"
|
|
48
|
+
data-variant={variant}
|
|
49
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
56
|
+
return (
|
|
57
|
+
<TabsPrimitive.Tab
|
|
58
|
+
data-slot="tabs-trigger"
|
|
59
|
+
className={cn(
|
|
60
|
+
"text-foreground/60 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
61
|
+
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
62
|
+
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
|
63
|
+
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
72
|
+
return (
|
|
73
|
+
<TabsPrimitive.Panel
|
|
74
|
+
data-slot="tabs-content"
|
|
75
|
+
className={cn("flex-1 text-sm outline-none", className)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@repo/ui/lib/utils";
|
|
4
|
+
|
|
5
|
+
function TooltipProvider({
|
|
6
|
+
delay = 0,
|
|
7
|
+
...props
|
|
8
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
9
|
+
return (
|
|
10
|
+
<TooltipPrimitive.Provider
|
|
11
|
+
data-slot="tooltip-provider"
|
|
12
|
+
delay={delay}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
19
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
23
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function TooltipContent({
|
|
27
|
+
className,
|
|
28
|
+
side = "top",
|
|
29
|
+
sideOffset = 4,
|
|
30
|
+
align = "center",
|
|
31
|
+
alignOffset = 0,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: TooltipPrimitive.Popup.Props &
|
|
35
|
+
Pick<
|
|
36
|
+
TooltipPrimitive.Positioner.Props,
|
|
37
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
38
|
+
>) {
|
|
39
|
+
return (
|
|
40
|
+
<TooltipPrimitive.Portal>
|
|
41
|
+
<TooltipPrimitive.Positioner
|
|
42
|
+
align={align}
|
|
43
|
+
alignOffset={alignOffset}
|
|
44
|
+
side={side}
|
|
45
|
+
sideOffset={sideOffset}
|
|
46
|
+
className="isolate z-50"
|
|
47
|
+
>
|
|
48
|
+
<TooltipPrimitive.Popup
|
|
49
|
+
data-slot="tooltip-content"
|
|
50
|
+
className={cn(
|
|
51
|
+
"bg-foreground text-background data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
58
|
+
</TooltipPrimitive.Popup>
|
|
59
|
+
</TooltipPrimitive.Positioner>
|
|
60
|
+
</TooltipPrimitive.Portal>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Canary for the tool repo's `scripts/verify-css` smoke test. Tailwind's class
|
|
2
2
|
// detection is text-based — it scans source files as plain text and never imports them —
|
|
3
3
|
// so this constant needs no consumer to make the utility below reach the built CSS. It
|
|
4
|
-
// reaches it only if globals.css's `@source "
|
|
4
|
+
// reaches it only if globals.css's `@source "../lib/**/*.{ts,tsx}"` glob is actually
|
|
5
5
|
// matching packages/ui, which is the exact failure the smoke test exists to catch.
|
|
6
6
|
//
|
|
7
7
|
// The utility is an arbitrary property (`[--saasaloy-css-probe:1]`) so it compiles to a
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
/** localStorage key holding the visitor's explicit choice. `system` clears it. */
|
|
11
11
|
export const THEME_STORAGE_KEY = "theme";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* localStorage key marking that the visitor has used a toggle at least once.
|
|
15
|
+
*
|
|
16
|
+
* THEME_STORAGE_KEY alone cannot carry that fact, because `system` is spelled as an
|
|
17
|
+
* absent key — so a first visit and a deliberate `system` read the same. A host that
|
|
18
|
+
* wants a default other than the OS preference (the admin app starts on dark) needs to
|
|
19
|
+
* tell those two apart, and this key is the only difference between them.
|
|
20
|
+
*
|
|
21
|
+
* `installThemeToggle` writes it on every press. Nothing clears it.
|
|
22
|
+
*/
|
|
23
|
+
export const THEME_CHOICE_KEY = "theme-chosen";
|
|
24
|
+
|
|
13
25
|
/** Attribute on `<html>` carrying the *chosen* state — also the JS-present marker. */
|
|
14
26
|
export const THEME_ATTRIBUTE = "data-theme";
|
|
15
27
|
|
|
@@ -72,8 +84,119 @@ export function setTheme(theme: Theme): void {
|
|
|
72
84
|
root.setAttribute(THEME_ATTRIBUTE, theme);
|
|
73
85
|
root.classList.toggle("dark", resolveTheme(theme) === "dark");
|
|
74
86
|
|
|
87
|
+
relabelThemeToggles(theme);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Give every `[data-theme-toggle]` in the document the accessible name of the state it is
|
|
92
|
+
* in. `setTheme` calls this, so a host only needs it directly for the one case `setTheme`
|
|
93
|
+
* cannot cover: a toggle that MOUNTS AFTER the theme was applied.
|
|
94
|
+
*
|
|
95
|
+
* That is the ordinary case in a React host. `theme-toggle.tsx` renders a static
|
|
96
|
+
* `aria-label` for `system` — the truthful value for a first-time visitor — and the theme
|
|
97
|
+
* is applied in the entry module, before React has mounted anything. Without this call the
|
|
98
|
+
* button announces "Theme: system" over a painted dark page until the first press.
|
|
99
|
+
*
|
|
100
|
+
* The Astro host gets the same repair from THEME_INIT_SCRIPT's `DOMContentLoaded` handler.
|
|
101
|
+
*
|
|
102
|
+
* `data-theme` is author-writable, so an unrecognised value falls back to the stored
|
|
103
|
+
* choice rather than setting `aria-label="undefined"`.
|
|
104
|
+
*/
|
|
105
|
+
export function relabelThemeToggles(theme?: Theme): void {
|
|
106
|
+
const root = document.documentElement;
|
|
107
|
+
const painted = theme ?? root.getAttribute(THEME_ATTRIBUTE);
|
|
108
|
+
const named =
|
|
109
|
+
painted !== null && THEME_ORDER.includes(painted as Theme)
|
|
110
|
+
? (painted as Theme)
|
|
111
|
+
: getStoredTheme();
|
|
112
|
+
|
|
75
113
|
for (const trigger of root.querySelectorAll(`[${THEME_TOGGLE_ATTRIBUTE}]`)) {
|
|
76
|
-
trigger.setAttribute("aria-label", THEME_LABELS[
|
|
114
|
+
trigger.setAttribute("aria-label", THEME_LABELS[named]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Install the toggle's click behaviour on `document`, and answer with a function that
|
|
120
|
+
* removes it again.
|
|
121
|
+
*
|
|
122
|
+
* A host that inlines THEME_INIT_SCRIPT already has this and must NOT call it — the two
|
|
123
|
+
* listeners would both fire and the cycle would advance twice per press. It exists for the
|
|
124
|
+
* host that cannot inline the script: a Vite SPA's `index.html` substitutes only `%VITE_*%`
|
|
125
|
+
* values, so it has no way to reach a TypeScript constant. Such an app calls this once from
|
|
126
|
+
* its entry module instead of pasting a second copy of the cycle rule.
|
|
127
|
+
*
|
|
128
|
+
* It cycles from what is PAINTED, not from what is stored, for the reason the shared script
|
|
129
|
+
* gives: where storage is unwritable the write is a no-op, so reading storage would answer
|
|
130
|
+
* the same value forever and every press would land on the same theme.
|
|
131
|
+
*/
|
|
132
|
+
export function installThemeToggle(): () => void {
|
|
133
|
+
function onClick(event: MouseEvent): void {
|
|
134
|
+
const { target } = event;
|
|
135
|
+
if (!(target instanceof Element)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!target.closest(`[${THEME_TOGGLE_ATTRIBUTE}]`)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const painted = document.documentElement.getAttribute(THEME_ATTRIBUTE);
|
|
143
|
+
// An unrecognised `data-theme` gives index -1 and starts the cycle at the head of
|
|
144
|
+
// THEME_ORDER.
|
|
145
|
+
const index = painted === null ? -1 : THEME_ORDER.indexOf(painted as Theme);
|
|
146
|
+
const next = THEME_ORDER[(index + 1) % THEME_ORDER.length] ?? "light";
|
|
147
|
+
|
|
148
|
+
setTheme(next);
|
|
149
|
+
|
|
150
|
+
// After `setTheme`, and unconditionally: a `system` press CLEARS THEME_STORAGE_KEY, so
|
|
151
|
+
// this key is the only record that the visitor pressed anything at all.
|
|
152
|
+
try {
|
|
153
|
+
localStorage.setItem(THEME_CHOICE_KEY, "1");
|
|
154
|
+
} catch {
|
|
155
|
+
// Unwritable storage costs persistence, not the current page.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The OS half of the same job, and the reason this is not click-only. THEME_INIT_SCRIPT
|
|
160
|
+
// registers its own matchMedia listener; a host that calls this function instead does
|
|
161
|
+
// not have that script, so without this listener a visitor on `system` keeps the palette
|
|
162
|
+
// resolved at load until the next reload.
|
|
163
|
+
//
|
|
164
|
+
// It repaints only while the state is `system`. A visitor who picked light or dark said
|
|
165
|
+
// so deliberately, and the OS must not overrule that.
|
|
166
|
+
const media = window.matchMedia(OS_DARK_QUERY);
|
|
167
|
+
|
|
168
|
+
function onOsChange(): void {
|
|
169
|
+
if (getStoredTheme() === "system") {
|
|
170
|
+
setTheme("system");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
document.addEventListener("click", onClick);
|
|
175
|
+
media.addEventListener("change", onOsChange);
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
document.removeEventListener("click", onClick);
|
|
179
|
+
media.removeEventListener("change", onOsChange);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether the visitor has ever used a toggle in this origin.
|
|
185
|
+
*
|
|
186
|
+
* A host whose default is the OS preference never needs this — `getStoredTheme()` already
|
|
187
|
+
* answers `system` for an unset key. A host with a different default (the admin app starts
|
|
188
|
+
* on dark) does: without it, a deliberate `system` choice and a first visit are the same
|
|
189
|
+
* absent key, and the default would overwrite the choice on every load.
|
|
190
|
+
*/
|
|
191
|
+
export function hasChosenTheme(): boolean {
|
|
192
|
+
try {
|
|
193
|
+
return (
|
|
194
|
+
localStorage.getItem(THEME_CHOICE_KEY) !== null ||
|
|
195
|
+
localStorage.getItem(THEME_STORAGE_KEY) !== null
|
|
196
|
+
);
|
|
197
|
+
} catch {
|
|
198
|
+
// Unreadable storage keeps nothing, so nothing was ever chosen.
|
|
199
|
+
return false;
|
|
77
200
|
}
|
|
78
201
|
}
|
|
79
202
|
|
|
@@ -11,7 +11,34 @@
|
|
|
11
11
|
`astro dev`/`astro build` run with cwd = apps/web. Without these globs every utility
|
|
12
12
|
class written inside packages/ui is silently dropped — no error, no warning, just
|
|
13
13
|
missing CSS. A non-matching @source glob is equally silent, so both paths matter. */
|
|
14
|
-
@source "
|
|
14
|
+
@source "../*.{ts,tsx}";
|
|
15
|
+
@source "../blocks/**/*.{ts,tsx}";
|
|
16
|
+
@source "../containers/**/*.{ts,tsx}";
|
|
17
|
+
@source "../content/**/*.{ts,tsx}";
|
|
18
|
+
@source "../lib/**/*.{ts,tsx}";
|
|
19
|
+
|
|
20
|
+
/* Components are listed one by one, and that is deliberate. Tailwind scans CONTENT, not
|
|
21
|
+
imports: a primitive nobody renders still pushes every class string it contains into
|
|
22
|
+
every stylesheet that pulls this file in. A blanket `../components/**` glob therefore
|
|
23
|
+
charges the landing page for primitives only an admin screen uses — measured at 71.5 kB
|
|
24
|
+
against 45.7 kB for the same page with the unused ones out of the scan.
|
|
25
|
+
|
|
26
|
+
So this list is the set the landing page and src/blocks/ actually render. Vendor a
|
|
27
|
+
primitive with `shadcn add` and one of two things is true:
|
|
28
|
+
|
|
29
|
+
- a block or an app page renders it → add its line here;
|
|
30
|
+
- only one app renders it → add an `@source` line in THAT app's own stylesheet
|
|
31
|
+
instead, the way apps/admin/src/styles/admin.css does for the shell primitives.
|
|
32
|
+
|
|
33
|
+
Get this wrong and the failure is the silent one the comment above describes: the class
|
|
34
|
+
never reaches the CSS and the component renders unstyled. */
|
|
35
|
+
@source "../components/accordion.tsx";
|
|
36
|
+
@source "../components/badge.tsx";
|
|
37
|
+
@source "../components/button.tsx";
|
|
38
|
+
@source "../components/card.tsx";
|
|
39
|
+
@source "../components/input.tsx";
|
|
40
|
+
@source "../components/label.tsx";
|
|
41
|
+
@source "../components/separator.tsx";
|
|
15
42
|
|
|
16
43
|
/* FOUR levels up, not three. From packages/ui/src/styles/ the path walks
|
|
17
44
|
styles → src → ui → packages → <root>, so `../../../apps/**` would resolve to
|
|
@@ -29,6 +29,14 @@ export default {
|
|
|
29
29
|
"import-notation": "string",
|
|
30
30
|
"hue-degree-notation": "number",
|
|
31
31
|
"lightness-notation": "number",
|
|
32
|
+
|
|
33
|
+
// Stylelint 17.15.0 added `at-rule-prelude-no-invalid`, which checks an at-rule's
|
|
34
|
+
// prelude against the CSS grammar. Tailwind's `@apply` takes a list of utility
|
|
35
|
+
// class names, which no grammar describes, so every use in `globals.css` is
|
|
36
|
+
// reported. Ignore that one at-rule rather than turning the rule off — it still
|
|
37
|
+
// validates `@media`, `@supports` and the rest.
|
|
38
|
+
"at-rule-prelude-no-invalid": [true, { ignoreAtRules: ["apply"] }],
|
|
39
|
+
|
|
32
40
|
"at-rule-no-unknown": [
|
|
33
41
|
true,
|
|
34
42
|
{
|