@rudderhq/agent-runtime-opencode-local 0.7.2 → 0.7.4
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/package.json +2 -2
- package/skills/app-builder/SKILL.md +39 -9
- package/skills/app-builder/assets/scaffold/app/globals.css +109 -16
- package/skills/app-builder/assets/scaffold/components/contacts-workspace.tsx +176 -152
- package/skills/app-builder/assets/scaffold/components/ui/alert.tsx +12 -0
- package/skills/app-builder/assets/scaffold/components/ui/badge.tsx +27 -0
- package/skills/app-builder/assets/scaffold/components/ui/button.tsx +9 -7
- package/skills/app-builder/assets/scaffold/components/ui/card.tsx +15 -3
- package/skills/app-builder/assets/scaffold/components/ui/empty.tsx +18 -0
- package/skills/app-builder/assets/scaffold/components/ui/field.tsx +23 -0
- package/skills/app-builder/assets/scaffold/components/ui/input.tsx +1 -1
- package/skills/app-builder/assets/scaffold/components/ui/label.tsx +1 -1
- package/skills/app-builder/assets/scaffold/components/ui/separator.tsx +6 -0
- package/skills/app-builder/assets/scaffold/components/ui/skeleton.tsx +6 -0
- package/skills/app-builder/assets/scaffold/components/ui/table.tsx +30 -0
- package/skills/app-builder/assets/scaffold/components.json +20 -0
- package/skills/app-builder/assets/scaffold/next-env.d.ts +1 -1
- package/skills/app-builder/assets/scaffold/next.config.ts +19 -0
- package/skills/app-builder/assets/scaffold/package.json +2 -1
- package/skills/app-builder/assets/scaffold/rudder.app.json +1 -1
- package/skills/app-builder/assets/scaffold/rudder.ui.json +13 -0
- package/skills/app-builder/assets/scaffold/scripts/validate-rudder-ui.mjs +50 -0
- package/skills/app-builder/assets/scaffold/tests/e2e/app.spec.ts +35 -1
- package/skills/app-builder/evals/evals.json +2 -1
- package/skills/app-builder/references/design-guidelines.md +19 -2
- package/skills/app-builder/references/rudder-ui-preset.md +73 -0
- package/skills/app-builder/references/scaffold-contract.md +12 -0
- package/skills/app-builder/references/verification.md +6 -0
- package/skills/app-builder/scripts/report-build-status.mjs +83 -0
- package/skills/rudder-docs/SKILL.md +2 -2
- package/skills/rudder-docs/evals/retrieval-authority-evals.json +40 -0
- package/skills/rudder-docs/evals/trigger-evals.json +2 -2
- package/skills/rudder-docs/references/api-reference.md +0 -2
- package/skills/rudder-docs/references/cli-reference.md +7 -2
- package/skills/rudder-docs/references/operating-practices.md +10 -6
- package/skills/rudder-docs/references/plugin-authoring.md +38 -114
- package/skills/rudder-docs/references/source-map.md +2 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
"inline-flex items-center rounded-sm border px-1.5 py-0.5 text-xs font-medium",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
11
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
12
|
+
outline: "text-foreground",
|
|
13
|
+
success: "border-primary/20 bg-accent text-accent-foreground",
|
|
14
|
+
destructive: "border-destructive/20 bg-destructive/10 text-destructive",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: { variant: "secondary" },
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export function Badge({
|
|
22
|
+
className,
|
|
23
|
+
variant,
|
|
24
|
+
...props
|
|
25
|
+
}: React.HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants>) {
|
|
26
|
+
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
27
|
+
}
|
|
@@ -4,18 +4,20 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
|
|
6
6
|
const buttonVariants = cva(
|
|
7
|
-
"inline-flex
|
|
7
|
+
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-foreground text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4",
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: "bg-
|
|
12
|
-
outline: "border bg-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
12
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
13
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
14
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
15
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
15
16
|
},
|
|
16
17
|
size: {
|
|
17
|
-
default: "h-
|
|
18
|
-
sm: "h-
|
|
18
|
+
default: "h-9 px-3.5",
|
|
19
|
+
sm: "h-8 px-3 text-xs",
|
|
20
|
+
"icon-sm": "size-8",
|
|
19
21
|
},
|
|
20
22
|
},
|
|
21
23
|
defaultVariants: {
|
|
@@ -4,16 +4,28 @@ import * as React from "react";
|
|
|
4
4
|
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
5
5
|
return (
|
|
6
6
|
<section
|
|
7
|
-
className={cn("rounded-
|
|
7
|
+
className={cn("rounded-lg border bg-card text-card-foreground", className)}
|
|
8
8
|
{...props}
|
|
9
9
|
/>
|
|
10
10
|
);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
14
|
-
return <div className={cn("border-b px-
|
|
14
|
+
return <div className={cn("flex flex-col gap-1 border-b px-4 py-3", className)} {...props} />;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
18
|
-
return <div className={cn("p-
|
|
18
|
+
return <div className={cn("p-4", className)} {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
22
|
+
return <h2 className={cn("m-0 text-base font-semibold", className)} {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
26
|
+
return <p className={cn("m-0 text-sm text-muted-foreground", className)} {...props} />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CardFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
30
|
+
return <div className={cn("flex items-center border-t px-4 py-3", className)} {...props} />;
|
|
19
31
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export function Empty({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
5
|
+
return <div className={cn("flex min-h-44 flex-col items-center justify-center gap-3 p-6 text-center", className)} {...props} />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function EmptyHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
9
|
+
return <div className={cn("flex max-w-sm flex-col gap-1", className)} {...props} />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function EmptyTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
13
|
+
return <h3 className={cn("m-0 text-sm font-semibold", className)} {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function EmptyDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
17
|
+
return <p className={cn("m-0 text-sm text-muted-foreground", className)} {...props} />;
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Label } from "@/components/ui/label";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
export function FieldGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
6
|
+
return <div className={cn("flex flex-col gap-4", className)} {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Field({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
10
|
+
return <div className={cn("flex flex-col gap-1.5", className)} {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
|
|
14
|
+
return <Label className={className} {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function FieldDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
18
|
+
return <p className={cn("m-0 text-xs text-muted-foreground", className)} {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function FieldError({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
22
|
+
return <p className={cn("m-0 text-xs text-destructive", className)} {...props} />;
|
|
23
|
+
}
|
|
@@ -7,7 +7,7 @@ export const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"in
|
|
|
7
7
|
ref={ref}
|
|
8
8
|
type={type}
|
|
9
9
|
className={cn(
|
|
10
|
-
"flex h-
|
|
10
|
+
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm outline-none transition-shadow placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring/60 disabled:cursor-not-allowed disabled:opacity-50",
|
|
11
11
|
className,
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|
|
@@ -2,5 +2,5 @@ import { cn } from "@/lib/utils";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
4
|
export function Label({ className, ...props }: React.LabelHTMLAttributes<HTMLLabelElement>) {
|
|
5
|
-
return <label className={cn("text-sm font-medium", className)} {...props} />;
|
|
5
|
+
return <label className={cn("text-sm font-medium leading-none", className)} {...props} />;
|
|
6
6
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export function Table({ className, ...props }: React.TableHTMLAttributes<HTMLTableElement>) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="w-full overflow-x-auto">
|
|
7
|
+
<table className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function TableHeader({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) {
|
|
13
|
+
return <thead className={cn("bg-muted/60 [&_tr]:border-b", className)} {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TableBody({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) {
|
|
17
|
+
return <tbody className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function TableRow({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) {
|
|
21
|
+
return <tr className={cn("border-b transition-colors hover:bg-muted/40", className)} {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TableHead({ className, ...props }: React.ThHTMLAttributes<HTMLTableCellElement>) {
|
|
25
|
+
return <th className={cn("h-9 px-4 text-left align-middle text-xs font-medium text-muted-foreground", className)} {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function TableCell({ className, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) {
|
|
29
|
+
return <td className={cn("px-4 py-3 align-middle", className)} {...props} />;
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"iconLibrary": "lucide",
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
2
|
|
|
3
3
|
const nextConfig: NextConfig = {
|
|
4
|
+
devIndicators: false,
|
|
4
5
|
output: "standalone",
|
|
5
6
|
poweredByHeader: false,
|
|
7
|
+
turbopack: {
|
|
8
|
+
root: process.cwd(),
|
|
9
|
+
},
|
|
10
|
+
webpack(config, { isServer }) {
|
|
11
|
+
if (isServer) {
|
|
12
|
+
config.externals.push((
|
|
13
|
+
{ request }: { request?: string },
|
|
14
|
+
callback: (error?: Error | null, result?: string) => void,
|
|
15
|
+
) => {
|
|
16
|
+
if (request?.startsWith("node:")) {
|
|
17
|
+
callback(null, `commonjs ${request}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
callback();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return config;
|
|
24
|
+
},
|
|
6
25
|
};
|
|
7
26
|
|
|
8
27
|
export default nextConfig;
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"db:migrate": "tsx scripts/migrate.ts",
|
|
18
18
|
"db:seed": "tsx scripts/seed.ts",
|
|
19
19
|
"data:snapshot": "tsx scripts/snapshot.ts",
|
|
20
|
-
"
|
|
20
|
+
"ui:check": "node scripts/validate-rudder-ui.mjs",
|
|
21
|
+
"verify": "pnpm ui:check && tsc --noEmit && vitest run && next build",
|
|
21
22
|
"predev": "tsx scripts/migrate.ts && tsx scripts/seed.ts"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const root = process.cwd();
|
|
5
|
+
const requiredFiles = [
|
|
6
|
+
"components.json",
|
|
7
|
+
"app/globals.css",
|
|
8
|
+
"components/ui/button.tsx",
|
|
9
|
+
"components/ui/field.tsx",
|
|
10
|
+
"components/ui/table.tsx",
|
|
11
|
+
"components/ui/empty.tsx",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function fail(message) {
|
|
15
|
+
console.error(`Rudder UI preset invalid: ${message}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let preset;
|
|
20
|
+
try {
|
|
21
|
+
preset = JSON.parse(await readFile(path.join(root, "rudder.ui.json"), "utf8"));
|
|
22
|
+
} catch {
|
|
23
|
+
fail("rudder.ui.json is missing or invalid JSON");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (preset?.schemaVersion !== 1 || preset?.preset !== "rudder") {
|
|
27
|
+
fail("unsupported preset identity");
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isInteger(preset?.revision) || preset.revision < 1) {
|
|
30
|
+
fail("revision must be a positive integer");
|
|
31
|
+
}
|
|
32
|
+
if (preset?.sourceOwnership !== "app") {
|
|
33
|
+
fail("generated Apps must own their component source");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const relativePath of requiredFiles) {
|
|
37
|
+
try {
|
|
38
|
+
await access(path.join(root, relativePath));
|
|
39
|
+
} catch {
|
|
40
|
+
fail(`required file is missing: ${relativePath}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const css = await readFile(path.join(root, "app/globals.css"), "utf8");
|
|
45
|
+
for (const token of ["--background", "--foreground", "--primary", "--destructive", "--border", "--ring", "--radius"]) {
|
|
46
|
+
if (!css.includes(`${token}:`)) fail(`semantic token is missing: ${token}`);
|
|
47
|
+
}
|
|
48
|
+
if (!css.includes("@theme inline")) fail("Tailwind semantic token mapping is missing");
|
|
49
|
+
|
|
50
|
+
console.log(`Rudder UI preset ${preset.revision}: ok`);
|
|
@@ -4,7 +4,7 @@ test("creates a contact and keeps it after reload", async ({ page }) => {
|
|
|
4
4
|
const suffix = crypto.randomUUID();
|
|
5
5
|
const name = `Avery ${suffix.slice(0, 8)}`;
|
|
6
6
|
await page.goto("/");
|
|
7
|
-
await expect(page.getByRole("heading", { name: "
|
|
7
|
+
await expect(page.getByRole("heading", { name: "Contacts", exact: true })).toBeVisible();
|
|
8
8
|
await page.getByLabel("Name").fill(name);
|
|
9
9
|
await page.getByLabel("Email").fill(`${suffix}@example.test`);
|
|
10
10
|
await page.getByLabel("Company").fill("Acme");
|
|
@@ -14,6 +14,40 @@ test("creates a contact and keeps it after reload", async ({ page }) => {
|
|
|
14
14
|
await expect(page.getByText(name)).toBeVisible();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
test("ships the Rudder UI preset in system light and dark modes", async ({ browser }) => {
|
|
18
|
+
const lightPage = await browser.newPage({ colorScheme: "light" });
|
|
19
|
+
await lightPage.goto("/");
|
|
20
|
+
await expect(lightPage.getByText("Pipeline Desk")).toBeVisible();
|
|
21
|
+
const lightStyle = await lightPage.evaluate(() => {
|
|
22
|
+
const style = getComputedStyle(document.documentElement);
|
|
23
|
+
return {
|
|
24
|
+
background: getComputedStyle(document.body).backgroundColor,
|
|
25
|
+
radius: style.getPropertyValue("--radius"),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
expect(Number.parseFloat(lightStyle.radius)).toBe(0.375);
|
|
29
|
+
|
|
30
|
+
const darkPage = await browser.newPage({ colorScheme: "dark" });
|
|
31
|
+
await darkPage.goto("/");
|
|
32
|
+
const darkBackground = await darkPage.evaluate(() => (
|
|
33
|
+
getComputedStyle(document.body).backgroundColor
|
|
34
|
+
));
|
|
35
|
+
expect(darkBackground).not.toBe(lightStyle.background);
|
|
36
|
+
await lightPage.close();
|
|
37
|
+
await darkPage.close();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("keeps the primary workflow usable at 390px", async ({ page }) => {
|
|
41
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
42
|
+
await page.goto("/");
|
|
43
|
+
await expect(page.getByRole("heading", { name: "Contacts", exact: true })).toBeVisible();
|
|
44
|
+
await expect(page.getByRole("heading", { name: "Add contact" })).toBeVisible();
|
|
45
|
+
await expect(page.getByLabel("Search contacts")).toBeVisible();
|
|
46
|
+
await expect(page.getByRole("button", { name: "Delete Maya Chen" })).toBeVisible();
|
|
47
|
+
await expect(page.getByText("Northwind").first()).toBeVisible();
|
|
48
|
+
await expect(page.locator("nextjs-portal")).toBeHidden();
|
|
49
|
+
});
|
|
50
|
+
|
|
17
51
|
test("shows the empty-search state", async ({ page }) => {
|
|
18
52
|
await page.goto("/");
|
|
19
53
|
await page.getByLabel("Search contacts").fill("no matching record");
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"assertions": [
|
|
10
10
|
"Does not ask the user to choose a framework, database, package manager, or process topology.",
|
|
11
11
|
"Uses the maintained App Builder scaffold and SQLite persistence.",
|
|
12
|
+
"Uses the versioned Rudder UI preset and its semantic shadcn primitives instead of inventing a generic dashboard theme.",
|
|
12
13
|
"Does not ask for or place email credential values in Chat or source.",
|
|
13
14
|
"Includes the primary contact workflow plus import, persistence, and a production-shaped edge case in verification."
|
|
14
15
|
]
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"files": [],
|
|
33
34
|
"assertions": [
|
|
34
35
|
"Does not inspect or mutate real customer records merely to design the table.",
|
|
35
|
-
"Uses the existing
|
|
36
|
+
"Uses the existing Rudder UI preset components and semantic tokens, preserves readable density, and avoids decorative dashboard clutter.",
|
|
36
37
|
"Verifies the table at desktop width and a 390px viewport.",
|
|
37
38
|
"Captures rendered screenshot evidence and checks an empty or error state."
|
|
38
39
|
]
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Design for the user's actual workflow rather than for a technology demo.
|
|
4
4
|
|
|
5
|
+
For new maintained Apps, begin with the scaffold's versioned Rudder UI preset.
|
|
6
|
+
Do not replace its semantic tokens with a generic shadcn preset, a one-off
|
|
7
|
+
palette, or copied host application CSS. The generated App owns its component
|
|
8
|
+
source and must remain independently runnable outside Rudder.
|
|
9
|
+
|
|
5
10
|
- Put the primary action and current work state above decorative summaries.
|
|
6
11
|
- Use tables for operational lists, with search, meaningful filters, sorting,
|
|
7
12
|
pagination when needed, empty states, and clear row actions.
|
|
@@ -11,10 +16,22 @@ Design for the user's actual workflow rather than for a technology demo.
|
|
|
11
16
|
navigation, not both.
|
|
12
17
|
- Prefer readable density for CRM and marketing data. Avoid oversized KPI cards,
|
|
13
18
|
excessive gradients, glass panels, and placeholder charts.
|
|
14
|
-
- Use the scaffold's
|
|
15
|
-
one-off styling.
|
|
19
|
+
- Use the scaffold's shadcn primitives and semantic theme tokens before adding
|
|
20
|
+
new components or one-off styling. Compose `Button`, `Input`, `Field`,
|
|
21
|
+
`Table`, `Badge`, `Alert`, `Empty`, `Skeleton`, and `Separator` instead of
|
|
22
|
+
recreating those patterns with styled `div` elements.
|
|
23
|
+
- Keep Rudder's default visual character: calm, dense, operational, quiet, and
|
|
24
|
+
precise. Use compact radii, low-contrast borders, restrained elevation, one
|
|
25
|
+
clear primary action, and color for state or emphasis rather than decoration.
|
|
26
|
+
- Use semantic utilities such as `bg-background`, `text-foreground`,
|
|
27
|
+
`bg-muted`, and `text-destructive`. Do not hardcode product colors in
|
|
28
|
+
workflow components or add component-local dark-mode colors.
|
|
29
|
+
- Treat the preset as a starting system, not a brand lock. When the brief calls
|
|
30
|
+
for a distinct brand, keep the component structure, density, accessibility,
|
|
31
|
+
and state behavior while changing the named token layer deliberately.
|
|
16
32
|
- Support keyboard use, visible focus, semantic headings, accessible names, and
|
|
17
33
|
error text that does not rely on color.
|
|
18
34
|
- Verify at desktop width and at 390px mobile width unless the brief excludes
|
|
19
35
|
mobile.
|
|
20
36
|
- Show loading, empty, error, populated, and destructive-operation states.
|
|
37
|
+
- Run `pnpm ui:check` before final verification for new maintained Apps.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Rudder UI Preset
|
|
2
|
+
|
|
3
|
+
The maintained scaffold includes a versioned, source-owned shadcn preset for
|
|
4
|
+
building local operator tools that feel immediately at home in Rudder.
|
|
5
|
+
|
|
6
|
+
## Source Of Truth
|
|
7
|
+
|
|
8
|
+
- `rudder.ui.json` identifies the preset and revision.
|
|
9
|
+
- `components.json` makes the generated project a conventional shadcn project.
|
|
10
|
+
- `app/globals.css` owns semantic color, radius, typography, and light/dark
|
|
11
|
+
tokens.
|
|
12
|
+
- `components/ui/` owns copied component source. Generated Apps do not import
|
|
13
|
+
components or CSS from the Rudder repository at runtime.
|
|
14
|
+
- `scripts/validate-rudder-ui.mjs` checks that the preset contract remains
|
|
15
|
+
present before handoff.
|
|
16
|
+
|
|
17
|
+
For new Apps, use this preset without asking the user to select a shadcn style,
|
|
18
|
+
base palette, accent, radius, or component library. The default is Rudder's
|
|
19
|
+
spacious-but-compact `luma` character, neutral surfaces, emerald action color,
|
|
20
|
+
and system light/dark mode.
|
|
21
|
+
|
|
22
|
+
## Composition Rules
|
|
23
|
+
|
|
24
|
+
1. Start with the shipped primitives. Extend them with variants when the same
|
|
25
|
+
need recurs; do not restyle each call site.
|
|
26
|
+
2. Use semantic Tailwind utilities rather than raw color utilities. New named
|
|
27
|
+
product colors belong in the token layer.
|
|
28
|
+
3. Keep normal controls between 32 and 36 pixels high, use compact radii, and
|
|
29
|
+
reserve pills for true circular or exceptional status affordances.
|
|
30
|
+
4. Bind native `button`, `input`, `select`, and `textarea` text to the inherited
|
|
31
|
+
semantic foreground color so system dark mode cannot reintroduce browser-UA
|
|
32
|
+
contrast regressions.
|
|
33
|
+
5. Use one primary panel or table region. Cards group independent objects or
|
|
34
|
+
actions; they are not wrappers for every section.
|
|
35
|
+
6. Operational lists require search or filtering when useful, readable row
|
|
36
|
+
actions, empty/loading/error states, and horizontal containment on narrow
|
|
37
|
+
screens.
|
|
38
|
+
7. Use Lucide icons for familiar actions. Icon-only controls need accessible
|
|
39
|
+
labels; icons inside buttons inherit their size from `Button`.
|
|
40
|
+
8. Preserve keyboard focus, semantic headings, persistent labels, inline
|
|
41
|
+
validation, and destructive confirmation.
|
|
42
|
+
|
|
43
|
+
## Appearance Boundary
|
|
44
|
+
|
|
45
|
+
The preset is copied into the App as a stable creation-time baseline. It does
|
|
46
|
+
not read Rudder's browser-local Appearance preferences or import host CSS at
|
|
47
|
+
runtime. This keeps the App portable and prevents a later Rudder theme change
|
|
48
|
+
from silently changing an independently branded product.
|
|
49
|
+
|
|
50
|
+
An App may add a documented host-theme bridge later, but light/dark following
|
|
51
|
+
must be optional and the standalone URL must still render correctly. Changing
|
|
52
|
+
that boundary is product behavior, not an incidental styling edit.
|
|
53
|
+
|
|
54
|
+
## Existing Projects
|
|
55
|
+
|
|
56
|
+
Do not install or overwrite the preset in an existing project by default.
|
|
57
|
+
Inspect its framework, component library, tokens, and brand first. If the user
|
|
58
|
+
explicitly asks to align it with Rudder, preserve application behavior and
|
|
59
|
+
choose a bounded migration:
|
|
60
|
+
|
|
61
|
+
- token-only alignment when components are already coherent;
|
|
62
|
+
- primitive-by-primitive alignment when a source-owned component system exists;
|
|
63
|
+
- full preset adoption only when the user accepts the wider visual change.
|
|
64
|
+
|
|
65
|
+
Never run a shadcn overwrite command over locally modified components without
|
|
66
|
+
explicit approval and a reviewed diff.
|
|
67
|
+
|
|
68
|
+
## Verification
|
|
69
|
+
|
|
70
|
+
Run `pnpm ui:check`, typecheck, tests, build, and the App's Playwright suite.
|
|
71
|
+
In Browser, inspect the primary workflow, loading or empty state, inline error,
|
|
72
|
+
destructive action, desktop layout, 390px layout, and light/dark contrast.
|
|
73
|
+
Capture the useful populated state rather than a decorative empty dashboard.
|
|
@@ -25,6 +25,8 @@ definition through **Apps + > Add local web project** before anything runs.
|
|
|
25
25
|
|
|
26
26
|
- Next.js App Router, React, and TypeScript
|
|
27
27
|
- Tailwind CSS with shadcn-style component source owned by the project
|
|
28
|
+
- a versioned Rudder UI preset with semantic light/dark tokens and compact
|
|
29
|
+
operator-tool primitives
|
|
28
30
|
- SQLite with Drizzle ORM
|
|
29
31
|
- Zod at API, import, and form boundaries
|
|
30
32
|
- Vitest for unit tests and Playwright for browser tests
|
|
@@ -36,6 +38,8 @@ implementation detail and must not become a user decision.
|
|
|
36
38
|
## Required Files And Behaviors
|
|
37
39
|
|
|
38
40
|
- `rudder.app.json` uses schema version 1 and a maintained template revision.
|
|
41
|
+
- `rudder.ui.json` identifies the source-owned Rudder UI preset revision;
|
|
42
|
+
`components.json` keeps the project compatible with the normal shadcn CLI.
|
|
39
43
|
- `/api/__rudder/health` returns readiness only after the app can open its
|
|
40
44
|
selected database.
|
|
41
45
|
- `RUDDER_APP_DATA_MODE=development|production` selects the database without
|
|
@@ -64,6 +68,12 @@ manifest.
|
|
|
64
68
|
## Commands
|
|
65
69
|
|
|
66
70
|
- `pnpm dev`: migrate the development database, then run loopback development.
|
|
71
|
+
- On Windows, Rudder's managed preview may select Next.js webpack while keeping
|
|
72
|
+
`node:*` built-ins external to the server bundle. This preserves the short
|
|
73
|
+
managed dependency layout required by native package executables without
|
|
74
|
+
changing the App's website output. The managed runtime supplies the same
|
|
75
|
+
compatibility to existing revision-1 Apps without rewriting their
|
|
76
|
+
`next.config.ts`, `next-env.d.ts`, or other App-owned source files.
|
|
67
77
|
- `pnpm typecheck`: TypeScript validation.
|
|
68
78
|
- `pnpm test`: unit and API-contract tests.
|
|
69
79
|
- `pnpm build`: production build.
|
|
@@ -73,5 +83,7 @@ manifest.
|
|
|
73
83
|
- `pnpm data:snapshot`: copy the selected SQLite database to `data/snapshots/`
|
|
74
84
|
using SQLite's backup API.
|
|
75
85
|
- `pnpm verify`: typecheck, tests, and build.
|
|
86
|
+
- `pnpm ui:check`: validate the maintained Rudder UI preset files and semantic
|
|
87
|
+
token contract.
|
|
76
88
|
|
|
77
89
|
Formal app start must not silently migrate real data. Promotion owns that step.
|
|
@@ -18,6 +18,12 @@ substitute a shell HTTP response for UI acceptance.
|
|
|
18
18
|
8. Capture current screenshots of the useful final state.
|
|
19
19
|
9. Materialize screenshots in Chat, Run, or Library evidence.
|
|
20
20
|
|
|
21
|
+
Only after all required checks pass may the Agent report
|
|
22
|
+
`verified_source_ready` for the originating App. That report is not a prose
|
|
23
|
+
claim: it is a run-scoped handoff that causes Desktop to repeat its maintained
|
|
24
|
+
checks, start the attested loopback process, and open the App for the user.
|
|
25
|
+
Questions, partial results, and blockers must never emit the handoff.
|
|
26
|
+
|
|
21
27
|
Close temporary run-owned tabs after verification. Never use the user's
|
|
22
28
|
persistent app view as an Agent test fixture when that could mutate formal
|
|
23
29
|
data.
|