@saena-io/create 0.1.0 → 0.2.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 (100) hide show
  1. package/dist/index.js +9 -9
  2. package/package.json +1 -1
  3. package/template/base/package.json +44 -2
  4. package/template/base/scripts/ui-update.ts +83 -0
  5. package/template/base/src/components/ui/accordion.tsx +75 -0
  6. package/template/base/src/components/ui/alert-dialog.tsx +162 -0
  7. package/template/base/src/components/ui/alert.tsx +73 -0
  8. package/template/base/src/components/ui/app-sidebar.tsx +183 -0
  9. package/template/base/src/components/ui/aspect-ratio.tsx +22 -0
  10. package/template/base/src/components/ui/asset-input.tsx +211 -0
  11. package/template/base/src/components/ui/avatar.tsx +91 -0
  12. package/template/base/src/components/ui/badge.tsx +50 -0
  13. package/template/base/src/components/ui/breadcrumb.tsx +104 -0
  14. package/template/base/src/components/ui/button-group.tsx +78 -0
  15. package/template/base/src/components/ui/button.tsx +56 -0
  16. package/template/base/src/components/ui/calendar.tsx +205 -0
  17. package/template/base/src/components/ui/card.tsx +85 -0
  18. package/template/base/src/components/ui/carousel.tsx +232 -0
  19. package/template/base/src/components/ui/chart.tsx +337 -0
  20. package/template/base/src/components/ui/checkbox.tsx +29 -0
  21. package/template/base/src/components/ui/collapsible.tsx +15 -0
  22. package/template/base/src/components/ui/combobox.tsx +276 -0
  23. package/template/base/src/components/ui/command.tsx +190 -0
  24. package/template/base/src/components/ui/context-menu.tsx +243 -0
  25. package/template/base/src/components/ui/dialog.tsx +134 -0
  26. package/template/base/src/components/ui/direction.tsx +4 -0
  27. package/template/base/src/components/ui/drawer.tsx +120 -0
  28. package/template/base/src/components/ui/dropdown-menu.tsx +254 -0
  29. package/template/base/src/components/ui/empty.tsx +94 -0
  30. package/template/base/src/components/ui/field.tsx +222 -0
  31. package/template/base/src/components/ui/focal-point-picker.tsx +175 -0
  32. package/template/base/src/components/ui/hover-card.tsx +46 -0
  33. package/template/base/src/components/ui/input-group.tsx +149 -0
  34. package/template/base/src/components/ui/input-otp.tsx +85 -0
  35. package/template/base/src/components/ui/input.tsx +20 -0
  36. package/template/base/src/components/ui/item.tsx +188 -0
  37. package/template/base/src/components/ui/kbd.tsx +26 -0
  38. package/template/base/src/components/ui/label.tsx +20 -0
  39. package/template/base/src/components/ui/menubar.tsx +268 -0
  40. package/template/base/src/components/ui/native-select.tsx +58 -0
  41. package/template/base/src/components/ui/nav-main.tsx +70 -0
  42. package/template/base/src/components/ui/nav-projects.tsx +97 -0
  43. package/template/base/src/components/ui/nav-secondary.tsx +37 -0
  44. package/template/base/src/components/ui/nav-user.tsx +108 -0
  45. package/template/base/src/components/ui/navigation-menu.tsx +164 -0
  46. package/template/base/src/components/ui/pagination.tsx +123 -0
  47. package/template/base/src/components/ui/popover.tsx +80 -0
  48. package/template/base/src/components/ui/progress.tsx +66 -0
  49. package/template/base/src/components/ui/radio-group.tsx +36 -0
  50. package/template/base/src/components/ui/resizable.tsx +42 -0
  51. package/template/base/src/components/ui/rich-text/ai-chat-editor.tsx +20 -0
  52. package/template/base/src/components/ui/rich-text/ai-command.tsx +90 -0
  53. package/template/base/src/components/ui/rich-text/ai-copilot.tsx +67 -0
  54. package/template/base/src/components/ui/rich-text/ai-menu.tsx +456 -0
  55. package/template/base/src/components/ui/rich-text/ai-node.tsx +42 -0
  56. package/template/base/src/components/ui/rich-text/ai-toolbar-button.tsx +29 -0
  57. package/template/base/src/components/ui/rich-text/block-draggable.tsx +187 -0
  58. package/template/base/src/components/ui/rich-text/block-selection.tsx +17 -0
  59. package/template/base/src/components/ui/rich-text/code-block-node.tsx +204 -0
  60. package/template/base/src/components/ui/rich-text/codec.ts +63 -0
  61. package/template/base/src/components/ui/rich-text/extension.ts +53 -0
  62. package/template/base/src/components/ui/rich-text/ghost-text.tsx +23 -0
  63. package/template/base/src/components/ui/rich-text/import-export-toolbar.tsx +103 -0
  64. package/template/base/src/components/ui/rich-text/link.tsx +18 -0
  65. package/template/base/src/components/ui/rich-text/list-node.tsx +65 -0
  66. package/template/base/src/components/ui/rich-text/nodes.tsx +44 -0
  67. package/template/base/src/components/ui/rich-text/plugins.ts +233 -0
  68. package/template/base/src/components/ui/rich-text/rich-text-editor.tsx +82 -0
  69. package/template/base/src/components/ui/rich-text/static.tsx +117 -0
  70. package/template/base/src/components/ui/rich-text/table-node.tsx +934 -0
  71. package/template/base/src/components/ui/rich-text/table-toolbar.tsx +232 -0
  72. package/template/base/src/components/ui/rich-text/toggle-node.tsx +36 -0
  73. package/template/base/src/components/ui/rich-text/toolbar-slots.ts +41 -0
  74. package/template/base/src/components/ui/rich-text/toolbar.tsx +668 -0
  75. package/template/base/src/components/ui/rich-text/use-ai-chat.ts +35 -0
  76. package/template/base/src/components/ui/rich-text/variable-type.ts +4 -0
  77. package/template/base/src/components/ui/rich-text/variable.tsx +97 -0
  78. package/template/base/src/components/ui/scroll-area.tsx +49 -0
  79. package/template/base/src/components/ui/select.tsx +202 -0
  80. package/template/base/src/components/ui/separator.tsx +19 -0
  81. package/template/base/src/components/ui/sheet.tsx +126 -0
  82. package/template/base/src/components/ui/sidebar.tsx +695 -0
  83. package/template/base/src/components/ui/skeleton.tsx +13 -0
  84. package/template/base/src/components/ui/slider.tsx +52 -0
  85. package/template/base/src/components/ui/sonner.tsx +50 -0
  86. package/template/base/src/components/ui/spinner.tsx +18 -0
  87. package/template/base/src/components/ui/switch.tsx +30 -0
  88. package/template/base/src/components/ui/table.tsx +89 -0
  89. package/template/base/src/components/ui/tabs.tsx +73 -0
  90. package/template/base/src/components/ui/textarea.tsx +18 -0
  91. package/template/base/src/components/ui/toggle-group.tsx +85 -0
  92. package/template/base/src/components/ui/toggle.tsx +45 -0
  93. package/template/base/src/components/ui/toolbar.tsx +451 -0
  94. package/template/base/src/components/ui/tooltip.tsx +52 -0
  95. package/template/base/src/hooks/use-mobile.ts +19 -0
  96. package/template/base/src/lib/utils.ts +6 -0
  97. package/template/base/src/routes/__root.tsx +1 -1
  98. package/template/base/src/server/auth.ts +2 -2
  99. package/template/base/src/styles/globals.css +230 -0
  100. package/template/base/vite.config.ts +15 -1
package/dist/index.js CHANGED
@@ -31,8 +31,8 @@ var SAENA_VERSION = "^0.1.1";
31
31
  var PLUGINS = [
32
32
  {
33
33
  id: "customers",
34
- name: "Customers",
35
- hint: "storefront identity (customer accounts + auth)",
34
+ name: "Customer Accounts",
35
+ hint: "customer accounts + login (identity for commerce)",
36
36
  pkg: "@saena-io/customers",
37
37
  version: SAENA_VERSION,
38
38
  dependsOn: [],
@@ -41,8 +41,8 @@ var PLUGINS = [
41
41
  },
42
42
  {
43
43
  id: "ai",
44
- name: "AI",
45
- hint: "AI capability (models + credentials) other plugins build on",
44
+ name: "AI Secrets",
45
+ hint: "encrypted storage for AI provider keys, shared by AI features",
46
46
  pkg: "@saena-io/ai",
47
47
  version: SAENA_VERSION,
48
48
  dependsOn: [],
@@ -54,8 +54,8 @@ var PLUGINS = [
54
54
  },
55
55
  {
56
56
  id: "translation",
57
- name: "Translation",
58
- hint: "AI machine-translation in the Translations grid (needs AI)",
57
+ name: "AI Translate",
58
+ hint: "machine-translate your content (needs AI Secrets)",
59
59
  pkg: "@saena-io/translation",
60
60
  version: SAENA_VERSION,
61
61
  dependsOn: ["ai"],
@@ -65,8 +65,8 @@ var PLUGINS = [
65
65
  },
66
66
  {
67
67
  id: "content",
68
- name: "Content (CMS)",
69
- hint: "no-code content editor \u2014 pages, sections, collections",
68
+ name: "CMS",
69
+ hint: "client-editable pages, sections & collections",
70
70
  pkg: "@saena-io/content",
71
71
  version: SAENA_VERSION,
72
72
  dependsOn: [],
@@ -212,7 +212,7 @@ async function main() {
212
212
  selectedIds = ["content", "ai", "translation"];
213
213
  } else {
214
214
  const res = await multiselect({
215
- message: "Which plugins? (Core already includes auth, RBAC, settings, the translations grid, and Help)",
215
+ message: "Which plugins? Space toggles, enter confirms \u2014 or select none for a core-only app. (Core already includes auth, RBAC, settings, the translations grid, and Help.)",
216
216
  options: PLUGINS.map((p) => ({ value: p.id, label: p.name, hint: p.hint })),
217
217
  required: false,
218
218
  initialValues: ["content", "ai", "translation"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saena-io/create",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a SAENA app (admin + plugins) from the published @saena-io packages.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,23 +8,65 @@
8
8
  "build": "vite build",
9
9
  "start": "vite preview",
10
10
  "typecheck": "tsc --noEmit",
11
+ "ui:update": "bun scripts/ui-update.ts",
11
12
  "db:migrate": "bun --env-file=.env scripts/migrate.ts",
12
13
  "db:migrate:deploy": "bun scripts/migrate.ts",
13
14
  "db:seed": "bun --env-file=.env scripts/seed-staff.ts"
14
15
  },
15
16
  "dependencies": {
17
+ "@ai-sdk/react": "^4.0.2",
18
+ "@base-ui/react": "^1.5.0",
19
+ "@fontsource-variable/noto-sans": "^5.2.10",
20
+ "@hugeicons/core-free-icons": "^4.2.0",
21
+ "@hugeicons/react": "^1.1.6",
22
+ "@platejs/ai": "^53.2.2",
23
+ "@platejs/basic-nodes": "^53.0.0",
24
+ "@platejs/basic-styles": "^53.0.0",
25
+ "@platejs/code-block": "^53.0.0",
26
+ "@platejs/dnd": "^53.1.0",
27
+ "@platejs/docx": "^53.0.0",
28
+ "@platejs/floating": "^53.0.0",
29
+ "@platejs/indent": "^53.0.0",
30
+ "@platejs/juice": "^53.0.0",
31
+ "@platejs/link": "^53.0.3",
32
+ "@platejs/list": "^53.1.3",
33
+ "@platejs/markdown": "^53.2.2",
34
+ "@platejs/resizable": "^53.0.0",
35
+ "@platejs/selection": "^53.1.6",
36
+ "@platejs/table": "^53.0.0",
37
+ "@platejs/toggle": "^53.0.0",
16
38
  "@saena-io/admin": "^0.1.1",
17
39
  "@saena-io/catalog": "^0.1.0",
18
40
  "@saena-io/core": "^0.1.1",
19
41
  "@saena-io/plugin-sdk": "^0.1.0",
20
- "@saena-io/ui": "^0.1.1",
42
+ "@saena-io/ui": "^0.1.2",
21
43
  "@tanstack/db": "^0.6.8",
22
44
  "@tanstack/query-core": "^5.101.0",
23
45
  "@tanstack/query-db-collection": "^1.0.40",
24
46
  "@tanstack/react-router": "^1.170.0",
25
47
  "@tanstack/react-start": "^1.168.0",
48
+ "ai": "^7.0.2",
49
+ "class-variance-authority": "^0.7.1",
50
+ "clsx": "^2.1.1",
51
+ "cmdk": "^1.1.1",
52
+ "embla-carousel-react": "^8.6.0",
53
+ "input-otp": "^1.4.2",
54
+ "lowlight": "^3.3.0",
55
+ "next-themes": "^0.4.6",
56
+ "platejs": "^53.2.1",
26
57
  "react": "^19.2.6",
27
- "react-dom": "^19.2.6"
58
+ "react-day-picker": "^10.0.1",
59
+ "react-dnd": "^16.0.1",
60
+ "react-dnd-html5-backend": "^16.0.1",
61
+ "react-dom": "^19.2.6",
62
+ "react-resizable-panels": "^4.11.2",
63
+ "recharts": "3.8.0",
64
+ "remark-gfm": "^4.0.1",
65
+ "shadcn": "^4.11.0",
66
+ "sonner": "^2.0.7",
67
+ "tailwind-merge": "^3.6.0",
68
+ "tw-animate-css": "^1.4.0",
69
+ "vaul": "^1.1.2"
28
70
  },
29
71
  "devDependencies": {
30
72
  "@tailwindcss/vite": "^4",
@@ -0,0 +1,83 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
2
+ import { dirname, join, relative } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // Re-fetch ejected UI components from the installed @saena-io/ui into src/components/ui (plus src/lib
6
+ // and src/hooks). You OWN these files, so this OVERWRITES the named one(s) with the package's current
7
+ // version — review with `git diff` afterwards and re-apply any edits you'd made. (globals.css is NOT
8
+ // touched here: it carries your own @source/theme customizations.)
9
+ //
10
+ // bun run ui:update button # one component
11
+ // bun run ui:update rich-text/toolbar # a nested one
12
+ // bun run ui:update --all # re-sync everything you've ejected
13
+
14
+ const appRoot = fileURLToPath(new URL('..', import.meta.url));
15
+ const pkgRoot = join(appRoot, 'node_modules', '@saena-io', 'ui', 'src');
16
+ const localRoot = join(appRoot, 'src');
17
+ const uiDir = join(localRoot, 'components', 'ui');
18
+
19
+ if (!existsSync(pkgRoot)) {
20
+ console.error('@saena-io/ui source not found — run `bun install` first.');
21
+ process.exit(1);
22
+ }
23
+
24
+ /** Map a local file (under src/) to its @saena-io/ui source counterpart, or null if it isn't an ejected one. */
25
+ function pkgPathFor(localPath: string): string | null {
26
+ const rel = relative(localRoot, localPath).replace(/\\/g, '/');
27
+ if (rel.startsWith('components/ui/'))
28
+ return join(pkgRoot, 'components', rel.slice('components/ui/'.length));
29
+ if (rel.startsWith('lib/') || rel.startsWith('hooks/')) return join(pkgRoot, rel);
30
+ return null;
31
+ }
32
+
33
+ function update(localPath: string): boolean {
34
+ const pkgPath = pkgPathFor(localPath);
35
+ if (!pkgPath || !existsSync(pkgPath)) return false;
36
+ mkdirSync(dirname(localPath), { recursive: true });
37
+ copyFileSync(pkgPath, localPath);
38
+ console.log(` updated ${relative(appRoot, localPath).replace(/\\/g, '/')}`);
39
+ return true;
40
+ }
41
+
42
+ function walk(dir: string, out: string[] = []): string[] {
43
+ if (!existsSync(dir)) return out;
44
+ for (const entry of readdirSync(dir)) {
45
+ const full = join(dir, entry);
46
+ if (statSync(full).isDirectory()) walk(full, out);
47
+ else if (/\.(ts|tsx)$/.test(entry)) out.push(full);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ const arg = process.argv[2];
53
+ if (!arg) {
54
+ console.log('Re-fetch ejected UI from @saena-io/ui.\n');
55
+ console.log(' bun run ui:update <component> e.g. button · card · rich-text/toolbar');
56
+ console.log(' bun run ui:update --all re-sync everything you have ejected');
57
+ process.exit(0);
58
+ }
59
+
60
+ let count = 0;
61
+ if (arg === '--all') {
62
+ console.log('Re-fetching all ejected UI from @saena-io/ui…');
63
+ const targets = walk(uiDir);
64
+ const utils = join(localRoot, 'lib', 'utils.ts');
65
+ if (existsSync(utils)) targets.push(utils);
66
+ targets.push(...walk(join(localRoot, 'hooks')));
67
+ for (const t of targets) if (update(t)) count++;
68
+ } else {
69
+ const name = arg.replace(/\.tsx?$/, '');
70
+ const ext = existsSync(join(pkgRoot, 'components', `${name}.tsx`))
71
+ ? 'tsx'
72
+ : existsSync(join(pkgRoot, 'components', `${name}.ts`))
73
+ ? 'ts'
74
+ : null;
75
+ if (!ext) {
76
+ console.error(`No component "${name}" in @saena-io/ui.`);
77
+ process.exit(1);
78
+ }
79
+ if (update(join(uiDir, `${name}.${ext}`))) count++;
80
+ }
81
+
82
+ console.log(`\nDone — ${count} file(s) updated. Review with \`git diff\` and re-apply any local edits.`);
83
+ process.exit(0);
@@ -0,0 +1,75 @@
1
+ import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
2
+
3
+ import { ArrowDown01Icon, ArrowUp01Icon } from '@hugeicons/core-free-icons';
4
+ import { HugeiconsIcon } from '@hugeicons/react';
5
+ import { cn } from '@saena-io/ui/lib/utils';
6
+
7
+ function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
8
+ return (
9
+ <AccordionPrimitive.Root
10
+ data-slot="accordion"
11
+ className={cn('flex w-full flex-col overflow-hidden rounded-md border', className)}
12
+ {...props}
13
+ />
14
+ );
15
+ }
16
+
17
+ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
18
+ return (
19
+ <AccordionPrimitive.Item
20
+ data-slot="accordion-item"
21
+ className={cn('not-last:border-b data-open:bg-muted/50', className)}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
28
+ return (
29
+ <AccordionPrimitive.Header className="flex">
30
+ <AccordionPrimitive.Trigger
31
+ data-slot="accordion-trigger"
32
+ className={cn(
33
+ 'group/accordion-trigger relative flex flex-1 items-start justify-between gap-6 border border-transparent p-2 text-left text-xs/relaxed font-medium transition-all outline-none hover:underline aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground',
34
+ className,
35
+ )}
36
+ {...props}
37
+ >
38
+ {children}
39
+ <HugeiconsIcon
40
+ icon={ArrowDown01Icon}
41
+ strokeWidth={2}
42
+ data-slot="accordion-trigger-icon"
43
+ className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
44
+ />
45
+ <HugeiconsIcon
46
+ icon={ArrowUp01Icon}
47
+ strokeWidth={2}
48
+ data-slot="accordion-trigger-icon"
49
+ className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
50
+ />
51
+ </AccordionPrimitive.Trigger>
52
+ </AccordionPrimitive.Header>
53
+ );
54
+ }
55
+
56
+ function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
57
+ return (
58
+ <AccordionPrimitive.Panel
59
+ data-slot="accordion-content"
60
+ className="overflow-hidden px-2 text-xs/relaxed data-open:animate-accordion-down data-closed:animate-accordion-up"
61
+ {...props}
62
+ >
63
+ <div
64
+ className={cn(
65
+ 'h-(--accordion-panel-height) pt-0 pb-4 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
66
+ className,
67
+ )}
68
+ >
69
+ {children}
70
+ </div>
71
+ </AccordionPrimitive.Panel>
72
+ );
73
+ }
74
+
75
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,162 @@
1
+ 'use client';
2
+
3
+ import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog';
4
+ import type * as React from 'react';
5
+
6
+ import { Button } from '@saena-io/ui/components/button';
7
+ import { cn } from '@saena-io/ui/lib/utils';
8
+
9
+ function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
10
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
11
+ }
12
+
13
+ function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
14
+ return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
15
+ }
16
+
17
+ function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
18
+ return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
19
+ }
20
+
21
+ function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
22
+ return (
23
+ <AlertDialogPrimitive.Backdrop
24
+ data-slot="alert-dialog-overlay"
25
+ className={cn(
26
+ 'fixed inset-0 isolate z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function AlertDialogContent({
35
+ className,
36
+ size = 'default',
37
+ ...props
38
+ }: AlertDialogPrimitive.Popup.Props & {
39
+ size?: 'default' | 'sm';
40
+ }) {
41
+ return (
42
+ <AlertDialogPortal>
43
+ <AlertDialogOverlay />
44
+ <AlertDialogPrimitive.Popup
45
+ data-slot="alert-dialog-content"
46
+ data-size={size}
47
+ className={cn(
48
+ 'group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-64 data-[size=default]:sm:max-w-sm 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',
49
+ className,
50
+ )}
51
+ {...props}
52
+ />
53
+ </AlertDialogPortal>
54
+ );
55
+ }
56
+
57
+ function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
58
+ return (
59
+ <div
60
+ data-slot="alert-dialog-header"
61
+ className={cn(
62
+ 'grid grid-rows-[auto_1fr] place-items-center gap-1 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
63
+ className,
64
+ )}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
71
+ return (
72
+ <div
73
+ data-slot="alert-dialog-footer"
74
+ className={cn(
75
+ 'flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
76
+ className,
77
+ )}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ function AlertDialogMedia({ className, ...props }: React.ComponentProps<'div'>) {
84
+ return (
85
+ <div
86
+ data-slot="alert-dialog-media"
87
+ className={cn(
88
+ "mb-2 inline-flex size-8 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-4",
89
+ className,
90
+ )}
91
+ {...props}
92
+ />
93
+ );
94
+ }
95
+
96
+ function AlertDialogTitle({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
100
+ return (
101
+ <AlertDialogPrimitive.Title
102
+ data-slot="alert-dialog-title"
103
+ className={cn(
104
+ 'font-heading text-sm font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
105
+ className,
106
+ )}
107
+ {...props}
108
+ />
109
+ );
110
+ }
111
+
112
+ function AlertDialogDescription({
113
+ className,
114
+ ...props
115
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
116
+ return (
117
+ <AlertDialogPrimitive.Description
118
+ data-slot="alert-dialog-description"
119
+ className={cn(
120
+ 'text-xs/relaxed text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
121
+ className,
122
+ )}
123
+ {...props}
124
+ />
125
+ );
126
+ }
127
+
128
+ function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>) {
129
+ return <Button data-slot="alert-dialog-action" className={cn(className)} {...props} />;
130
+ }
131
+
132
+ function AlertDialogCancel({
133
+ className,
134
+ variant = 'outline',
135
+ size = 'default',
136
+ ...props
137
+ }: AlertDialogPrimitive.Close.Props &
138
+ Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
139
+ return (
140
+ <AlertDialogPrimitive.Close
141
+ data-slot="alert-dialog-cancel"
142
+ className={cn(className)}
143
+ render={<Button variant={variant} size={size} />}
144
+ {...props}
145
+ />
146
+ );
147
+ }
148
+
149
+ export {
150
+ AlertDialog,
151
+ AlertDialogAction,
152
+ AlertDialogCancel,
153
+ AlertDialogContent,
154
+ AlertDialogDescription,
155
+ AlertDialogFooter,
156
+ AlertDialogHeader,
157
+ AlertDialogMedia,
158
+ AlertDialogOverlay,
159
+ AlertDialogPortal,
160
+ AlertDialogTitle,
161
+ AlertDialogTrigger,
162
+ };
@@ -0,0 +1,73 @@
1
+ import { type VariantProps, cva } from 'class-variance-authority';
2
+ import type * as React from 'react';
3
+
4
+ import { cn } from '@saena-io/ui/lib/utils';
5
+
6
+ const alertVariants = cva(
7
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2 py-1.5 text-left text-xs/relaxed has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-1.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-3.5",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-card text-card-foreground',
12
+ destructive:
13
+ 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: 'default',
18
+ },
19
+ },
20
+ );
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ 'font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+
50
+ function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
51
+ return (
52
+ <div
53
+ data-slot="alert-description"
54
+ className={cn(
55
+ 'text-xs/relaxed text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+
63
+ function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
64
+ return (
65
+ <div
66
+ data-slot="alert-action"
67
+ className={cn('absolute top-1.5 right-2', className)}
68
+ {...props}
69
+ />
70
+ );
71
+ }
72
+
73
+ export { Alert, AlertTitle, AlertDescription, AlertAction };