@percepta/create 3.5.2 → 3.6.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.
@@ -18,10 +18,12 @@ test.describe("RBAC access", () => {
18
18
  page,
19
19
  }) => {
20
20
  test.setTimeout(60_000);
21
- await signIn(page, users.customerAdmin, "/admin?tab=assignments");
21
+ await signIn(page, users.customerAdmin, "/settings?tab=assignments");
22
22
 
23
- await expect(page.getByRole("heading", { name: "RBAC" })).toBeVisible();
24
- await expect(page.getByRole("link", { name: "Admin" })).toBeVisible();
23
+ await expect(
24
+ page.getByRole("heading", { name: "Access control" }),
25
+ ).toBeVisible();
26
+ await expect(page.getByRole("link", { name: "Settings" })).toBeVisible();
25
27
 
26
28
  const userAssignments = page.getByRole("button", {
27
29
  name: "User assignments",
@@ -67,18 +69,18 @@ test.describe("RBAC access", () => {
67
69
  await expectAppAccess(browser, users.nonAppUser, false);
68
70
 
69
71
  await page.goto("/");
70
- await expect(page.getByRole("heading", { name: /Welcome to/ })).toHaveCount(
71
- 0,
72
- );
72
+ await expect(getAppHomeHeading(page)).toHaveCount(0);
73
73
  await expectNotFound(page);
74
74
  });
75
75
 
76
- test("app admin can see RBAC and main app but cannot edit default role mappings", async ({
76
+ test("app admin can see access control and main app but cannot edit default role mappings", async ({
77
77
  page,
78
78
  }) => {
79
- await signIn(page, users.appAdmin, "/admin?tab=assignments");
79
+ await signIn(page, users.appAdmin, "/settings?tab=assignments");
80
80
 
81
- await expect(page.getByRole("heading", { name: "RBAC" })).toBeVisible();
81
+ await expect(
82
+ page.getByRole("heading", { name: "Access control" }),
83
+ ).toBeVisible();
82
84
  await expect(
83
85
  page
84
86
  .getByText("Only customer admins can edit default application roles.")
@@ -93,33 +95,31 @@ test.describe("RBAC access", () => {
93
95
  ).toHaveAttribute("aria-disabled", "true");
94
96
 
95
97
  await page.goto("/");
96
- await expect(
97
- page.getByRole("heading", { name: /Welcome to/ }),
98
- ).toBeVisible();
98
+ await expect(getAppHomeHeading(page)).toBeVisible();
99
99
  });
100
100
 
101
- test("app user can enter the main app but cannot reach admin", async ({
101
+ test("app user can enter the main app but cannot reach settings", async ({
102
102
  page,
103
103
  }) => {
104
104
  await signIn(page, users.appUser, "/");
105
105
 
106
- await expect(
107
- page.getByRole("heading", { name: /Welcome to/ }),
108
- ).toBeVisible();
109
- await expect(page.getByRole("link", { name: "Admin" })).toHaveCount(0);
106
+ await expect(getAppHomeHeading(page)).toBeVisible();
107
+ await expect(page.getByRole("link", { name: "Settings" })).toHaveCount(0);
110
108
 
111
- await page.goto("/admin");
112
- await expect(page.getByRole("heading", { name: "RBAC" })).toHaveCount(0);
109
+ await page.goto("/settings");
110
+ await expect(
111
+ page.getByRole("heading", { name: "Access control" }),
112
+ ).toHaveCount(0);
113
113
  await expectNotFound(page);
114
114
  });
115
115
 
116
116
  test("non app user cannot enter the app", async ({ page }) => {
117
117
  await signIn(page, users.nonAppUser, "/");
118
118
 
119
- await expect(page.getByRole("heading", { name: /Welcome to/ })).toHaveCount(
120
- 0,
121
- );
122
- await expect(page.getByRole("heading", { name: "RBAC" })).toHaveCount(0);
119
+ await expect(getAppHomeHeading(page)).toHaveCount(0);
120
+ await expect(
121
+ page.getByRole("heading", { name: "Access control" }),
122
+ ).toHaveCount(0);
123
123
  await expectNotFound(page);
124
124
  });
125
125
  });
@@ -140,6 +140,10 @@ async function expectNotFound(page: Page) {
140
140
  ).toBeVisible();
141
141
  }
142
142
 
143
+ function getAppHomeHeading(page: Page) {
144
+ return page.getByRole("heading", { name: /Good afternoon/ });
145
+ }
146
+
143
147
  async function expectAppAccess(
144
148
  browser: Browser,
145
149
  email: string,
@@ -155,9 +159,7 @@ async function expectAppAccess(
155
159
 
156
160
  try {
157
161
  await signIn(page, email, "/");
158
- return await page
159
- .getByRole("heading", { name: /Welcome to/ })
160
- .count();
162
+ return await getAppHomeHeading(page).count();
161
163
  } finally {
162
164
  await context.close();
163
165
  }
@@ -52,10 +52,10 @@
52
52
  "@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
53
53
  "@opentelemetry/sdk-node": "^0.203.0",
54
54
  "@__REPO_NAME__/auth": "workspace:*",
55
- "@percepta/access-control": "0.7.0",
55
+ "@percepta/access-control": "0.8.0",
56
56
  "@percepta/ai": "0.1.0",
57
57
  "@percepta/database": "0.1.1",
58
- "@percepta/design": "0.3.2",
58
+ "@percepta/design": "0.4.0",
59
59
  "@percepta/inngest": "0.1.0",
60
60
  "@percepta/logger": "0.1.0",
61
61
  "@percepta/next-utils": "0.2.1",
@@ -20,7 +20,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
20
20
  canManageDefaultRoles(session.user.id),
21
21
  canManageAppRoles(session.user.id),
22
22
  ]);
23
- const showAdminLink = canManageDefaults || canManageRoles;
23
+ const showSettingsLink = canManageDefaults || canManageRoles;
24
24
 
25
25
  if (!canAccessApp) {
26
26
  notFound();
@@ -28,13 +28,9 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
28
28
 
29
29
  return (
30
30
  <>
31
- <Header showAdminLink={showAdminLink} />
32
- <main>
33
- <div className="py-8">
34
- <div className="mx-auto max-w-6xl">
35
- <div className="rounded-lg bg-white p-8">{children}</div>
36
- </div>
37
- </div>
31
+ <Header showSettingsLink={showSettingsLink} />
32
+ <main className="app-shell-main">
33
+ <div className="app-home">{children}</div>
38
34
  </main>
39
35
  </>
40
36
  );
@@ -5,15 +5,156 @@ export const metadata: Metadata = {
5
5
  description: "__APP_TITLE__",
6
6
  };
7
7
 
8
+ const metrics = [
9
+ { label: "Open projects", value: "12", delta: "+2", tone: "good" },
10
+ { label: "Tasks this week", value: "47", delta: "+8%", tone: "good" },
11
+ { label: "Time saved", value: "36h", delta: "+12%", tone: "good" },
12
+ { label: "On-track rate", value: "91%", delta: "-2%", tone: "warn" },
13
+ ] as const;
14
+
15
+ const activity = [
16
+ {
17
+ item: "Q2 procurement plan",
18
+ owner: "Mira K.",
19
+ status: "Approved",
20
+ updated: "12m ago",
21
+ },
22
+ {
23
+ item: "Inventory forecast - West",
24
+ owner: "System",
25
+ status: "Running",
26
+ updated: "24m ago",
27
+ },
28
+ {
29
+ item: "Allergen contamination rules",
30
+ owner: "Devon A.",
31
+ status: "Review",
32
+ updated: "1h ago",
33
+ },
34
+ {
35
+ item: "Cocoa supplier - ABC Inc.",
36
+ owner: "System",
37
+ status: "Flagged",
38
+ updated: "2h ago",
39
+ },
40
+ {
41
+ item: "New brand line variant",
42
+ owner: "Priya S.",
43
+ status: "Draft",
44
+ updated: "3h ago",
45
+ },
46
+ ] as const;
47
+
48
+ const reviews = [
49
+ { title: "Supplier change - Cocoa", due: "Due in 2 days" },
50
+ { title: "Allergen rule update", due: "Due in 5 days" },
51
+ { title: "Q2 production sequence", due: "Due in 9 days" },
52
+ ] as const;
53
+
8
54
  export default function HomePage() {
9
55
  return (
10
- <div className="space-y-8">
11
- <h1 className="text-center text-2xl font-bold text-foreground">
12
- Welcome to __APP_TITLE__
13
- </h1>
14
- <p className="mx-auto max-w-xl text-center text-sm text-muted-foreground">
15
- You are logged in. Start building your application!
16
- </p>
56
+ <div className="app-home-screen">
57
+ <section className="app-home-hero">
58
+ <div className="app-home-heading">
59
+ <p className="app-kicker">Overview</p>
60
+ <h1 className="app-title">Good afternoon, App Admin</h1>
61
+ <p className="app-subtitle">
62
+ Welcome to __APP_TITLE__. Three items want attention; the workspace
63
+ is ready for your team&apos;s first workflows.
64
+ </p>
65
+ </div>
66
+ <div className="app-actions">
67
+ <span className="app-button-secondary">Last 7 days</span>
68
+ <span className="app-button-primary">New workflow</span>
69
+ </div>
70
+ </section>
71
+
72
+ <section className="app-dashboard" aria-label="Workspace overview">
73
+ <div className="app-metrics">
74
+ {metrics.map((metric) => (
75
+ <article className="app-metric-card" key={metric.label}>
76
+ <div className="app-metric-top">
77
+ <span className="app-metric-label">{metric.label}</span>
78
+ <span
79
+ className={
80
+ metric.tone === "warn" ? "app-chip app-chip-warn" : "app-chip"
81
+ }
82
+ >
83
+ {metric.delta}
84
+ </span>
85
+ </div>
86
+ <div className="app-metric-value">{metric.value}</div>
87
+ <div className="app-sparkline" aria-hidden={true} />
88
+ </article>
89
+ ))}
90
+ </div>
91
+
92
+ <div className="app-content-grid">
93
+ <section className="app-panel">
94
+ <div className="app-panel-header">
95
+ <h2 className="app-panel-title">Recent activity</h2>
96
+ <span className="app-review-meta">View all</span>
97
+ </div>
98
+ <table className="app-table">
99
+ <thead>
100
+ <tr>
101
+ <th>Item</th>
102
+ <th>Owner</th>
103
+ <th>Status</th>
104
+ <th>Updated</th>
105
+ </tr>
106
+ </thead>
107
+ <tbody>
108
+ {activity.map((row) => (
109
+ <tr key={row.item}>
110
+ <td>{row.item}</td>
111
+ <td className="app-table-muted">{row.owner}</td>
112
+ <td>
113
+ <span className="app-status">{row.status}</span>
114
+ </td>
115
+ <td className="app-table-muted">{row.updated}</td>
116
+ </tr>
117
+ ))}
118
+ </tbody>
119
+ </table>
120
+ </section>
121
+
122
+ <div className="grid gap-5">
123
+ <section className="app-panel">
124
+ <div className="app-panel-header">
125
+ <h2 className="app-panel-title">Needs your review</h2>
126
+ </div>
127
+ <div className="app-review-list">
128
+ {reviews.map((review) => (
129
+ <div className="app-review-item" key={review.title}>
130
+ <div>
131
+ <div className="app-review-title">{review.title}</div>
132
+ <div className="app-review-meta">{review.due}</div>
133
+ </div>
134
+ <span className="app-review-action">Review</span>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </section>
139
+
140
+ <section className="app-panel">
141
+ <div className="app-panel-header">
142
+ <h2 className="app-panel-title">On call today</h2>
143
+ </div>
144
+ <div className="app-oncall-list">
145
+ <div className="app-avatar-stack" aria-label="On call users">
146
+ <span>MK</span>
147
+ <span>DA</span>
148
+ <span>PS</span>
149
+ <span>AL</span>
150
+ <span>JR</span>
151
+ </div>
152
+ <p className="app-review-meta">4 more teammates available</p>
153
+ </div>
154
+ </section>
155
+ </div>
156
+ </div>
157
+ </section>
17
158
  </div>
18
159
  );
19
160
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
4
  import { Button, Input } from "@percepta/design";
5
+ import { ArrowRight } from "lucide-react";
5
6
  import Link from "next/link";
6
7
  import { useRouter, useSearchParams } from "next/navigation";
7
8
  import React, { useCallback } from "react";
@@ -64,26 +65,34 @@ export function CredentialsSignInForm() {
64
65
  );
65
66
 
66
67
  return (
67
- <div className="space-y-8">
68
- <div className="text-center">
69
- <h1 className="text-2xl font-bold text-foreground">Sign In</h1>
70
- <p className="mt-2 text-sm text-muted-foreground">
68
+ <div className="grid gap-8">
69
+ <div className="grid gap-3">
70
+ <p className="app-auth-kicker">
71
+ <span>01</span>
72
+ <span>Sign in</span>
73
+ </p>
74
+ <h1 className="app-auth-title">Welcome back.</h1>
75
+ <p className="app-auth-copy text-sm">
71
76
  Need an account?{" "}
72
77
  <Link
73
- className="font-medium text-primary underline-offset-4 hover:underline"
78
+ className="app-auth-link"
74
79
  href={`/auth/signup?callbackUrl=${encodeURIComponent(callbackUrl)}`}
75
80
  >
76
81
  Create one
77
82
  </Link>
78
83
  </p>
79
84
  </div>
80
- <form className="space-y-6" onSubmit={handleSubmit(submit)}>
81
- <div className="space-y-4">
85
+ <form className="app-auth-form" onSubmit={handleSubmit(submit)}>
86
+ <div className="app-auth-fields">
82
87
  <Controller
83
88
  control={control}
84
89
  name="email"
85
90
  render={({ field, fieldState }) => (
86
- <FormItem label="Email" fieldState={fieldState}>
91
+ <FormItem
92
+ className="app-auth-field"
93
+ label="Email"
94
+ fieldState={fieldState}
95
+ >
87
96
  <Input {...field} type="email" autoComplete="email" />
88
97
  </FormItem>
89
98
  )}
@@ -92,7 +101,11 @@ export function CredentialsSignInForm() {
92
101
  control={control}
93
102
  name="password"
94
103
  render={({ field, fieldState }) => (
95
- <FormItem label="Password" fieldState={fieldState}>
104
+ <FormItem
105
+ className="app-auth-field"
106
+ label="Password"
107
+ fieldState={fieldState}
108
+ >
96
109
  <Input
97
110
  {...field}
98
111
  type="password"
@@ -103,8 +116,13 @@ export function CredentialsSignInForm() {
103
116
  />
104
117
  </div>
105
118
  <div className="flex justify-end">
106
- <Button type="submit" loading={isSubmitting}>
107
- Sign In
119
+ <Button
120
+ className="app-auth-submit"
121
+ type="submit"
122
+ loading={isSubmitting}
123
+ >
124
+ <span>Sign In</span>
125
+ <ArrowRight aria-hidden={true} />
108
126
  </Button>
109
127
  </div>
110
128
  </form>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
4
  import { Button, Input } from "@percepta/design";
5
+ import { ArrowRight } from "lucide-react";
5
6
  import Link from "next/link";
6
7
  import { useRouter, useSearchParams } from "next/navigation";
7
8
  import React, { useCallback } from "react";
@@ -59,26 +60,34 @@ export function CredentialsSignUpForm() {
59
60
  );
60
61
 
61
62
  return (
62
- <div className="space-y-8">
63
- <div className="text-center">
64
- <h1 className="text-2xl font-bold text-foreground">Create Account</h1>
65
- <p className="mt-2 text-sm text-muted-foreground">
63
+ <div className="grid gap-8">
64
+ <div className="grid gap-3">
65
+ <p className="app-auth-kicker">
66
+ <span>01</span>
67
+ <span>Request access</span>
68
+ </p>
69
+ <h1 className="app-auth-title">Create account.</h1>
70
+ <p className="app-auth-copy text-sm">
66
71
  Already have an account?{" "}
67
72
  <Link
68
- className="font-medium text-primary underline-offset-4 hover:underline"
73
+ className="app-auth-link"
69
74
  href={`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`}
70
75
  >
71
76
  Sign in
72
77
  </Link>
73
78
  </p>
74
79
  </div>
75
- <form className="space-y-6" onSubmit={handleSubmit(submit)}>
76
- <div className="space-y-4">
80
+ <form className="app-auth-form" onSubmit={handleSubmit(submit)}>
81
+ <div className="app-auth-fields">
77
82
  <Controller
78
83
  control={control}
79
84
  name="name"
80
85
  render={({ field, fieldState }) => (
81
- <FormItem label="Name" fieldState={fieldState}>
86
+ <FormItem
87
+ className="app-auth-field"
88
+ label="Name"
89
+ fieldState={fieldState}
90
+ >
82
91
  <Input {...field} autoComplete="name" />
83
92
  </FormItem>
84
93
  )}
@@ -87,7 +96,11 @@ export function CredentialsSignUpForm() {
87
96
  control={control}
88
97
  name="email"
89
98
  render={({ field, fieldState }) => (
90
- <FormItem label="Email" fieldState={fieldState}>
99
+ <FormItem
100
+ className="app-auth-field"
101
+ label="Email"
102
+ fieldState={fieldState}
103
+ >
91
104
  <Input {...field} type="email" autoComplete="email" />
92
105
  </FormItem>
93
106
  )}
@@ -96,15 +109,24 @@ export function CredentialsSignUpForm() {
96
109
  control={control}
97
110
  name="password"
98
111
  render={({ field, fieldState }) => (
99
- <FormItem label="Password" fieldState={fieldState}>
112
+ <FormItem
113
+ className="app-auth-field"
114
+ label="Password"
115
+ fieldState={fieldState}
116
+ >
100
117
  <Input {...field} type="password" autoComplete="new-password" />
101
118
  </FormItem>
102
119
  )}
103
120
  />
104
121
  </div>
105
122
  <div className="flex justify-end">
106
- <Button type="submit" loading={isSubmitting}>
107
- Create Account
123
+ <Button
124
+ className="app-auth-submit"
125
+ type="submit"
126
+ loading={isSubmitting}
127
+ >
128
+ <span>Create Account</span>
129
+ <ArrowRight aria-hidden={true} />
108
130
  </Button>
109
131
  </div>
110
132
  </form>
@@ -5,11 +5,37 @@ export default function AuthLayout({
5
5
  }: {
6
6
  children: React.ReactNode;
7
7
  }) {
8
+ const appTitle = "__APP_TITLE__";
9
+ const appInitial = appTitle.slice(0, 1).toUpperCase();
10
+
8
11
  return (
9
- <div className="flex min-h-screen flex-col items-center justify-center bg-muted/40 p-4">
10
- <div className="w-full max-w-md rounded-lg border border-border bg-card p-8 shadow-sm">
11
- {children}
12
- </div>
12
+ <div className="app-auth-shell">
13
+ <section className="app-auth-story" aria-label={`${appTitle} starter`}>
14
+ <div className="app-auth-brand">
15
+ <span className="app-auth-mark" aria-hidden={true}>
16
+ {appInitial}
17
+ </span>
18
+ <span>{appTitle}</span>
19
+ </div>
20
+
21
+ <div className="grid max-w-xl gap-6">
22
+ <p className="app-auth-kicker">The workspace</p>
23
+ <h2 className="app-auth-title">
24
+ A quieter place to do your team&apos;s most considered work.
25
+ </h2>
26
+ <p className="app-auth-copy">
27
+ Keep decisions, handoffs, and daily operations in one calm
28
+ workspace built for focused teams.
29
+ </p>
30
+ </div>
31
+
32
+ <div className="flex justify-between gap-4 text-xs uppercase text-muted-foreground">
33
+ <span>Workspace 1.0</span>
34
+ <span>Made for teams</span>
35
+ </div>
36
+ </section>
37
+
38
+ <div className="app-auth-card">{children}</div>
13
39
  </div>
14
40
  );
15
41
  }
@@ -7,7 +7,7 @@ import {
7
7
  canManageDefaultRoles,
8
8
  } from "../../services/access/AppAccessControl";
9
9
 
10
- export default async function AdminLayout({
10
+ export default async function SettingsLayout({
11
11
  children,
12
12
  }: {
13
13
  children: ReactNode;
@@ -22,21 +22,17 @@ export default async function AdminLayout({
22
22
  canManageDefaultRoles(session.user.id),
23
23
  canManageAppRoles(session.user.id),
24
24
  ]);
25
- const showAdminLink = canManageDefaults || canManageRoles;
25
+ const showSettingsLink = canManageDefaults || canManageRoles;
26
26
 
27
- if (!showAdminLink) {
27
+ if (!showSettingsLink) {
28
28
  notFound();
29
29
  }
30
30
 
31
31
  return (
32
32
  <>
33
- <Header showAdminLink={showAdminLink} />
34
- <main>
35
- <div className="py-8">
36
- <div className="mx-auto max-w-6xl">
37
- <div className="rounded-lg bg-white p-8">{children}</div>
38
- </div>
39
- </div>
33
+ <Header showSettingsLink={showSettingsLink} />
34
+ <main className="app-shell-main">
35
+ <div className="app-home">{children}</div>
40
36
  </main>
41
37
  </>
42
38
  );
@@ -3,25 +3,29 @@
3
3
  import { Tabs, TabsList, TabsTrigger } from "@percepta/design";
4
4
  import Link from "next/link";
5
5
 
6
- export type AdminTab = "assignments" | "roles";
6
+ export type AccessControlTab = "assignments" | "roles";
7
7
 
8
8
  const tabs = [
9
- { href: "/admin?tab=roles", label: "Roles", tab: "roles" },
9
+ { href: "/settings?tab=roles", label: "Roles", tab: "roles" },
10
10
  {
11
- href: "/admin?tab=assignments",
11
+ href: "/settings?tab=assignments",
12
12
  label: "Assignments",
13
13
  tab: "assignments",
14
14
  },
15
15
  ] as const satisfies ReadonlyArray<{
16
16
  readonly href: string;
17
17
  readonly label: string;
18
- readonly tab: AdminTab;
18
+ readonly tab: AccessControlTab;
19
19
  }>;
20
20
 
21
- export function AdminTabs({ activeTab }: { readonly activeTab: AdminTab }) {
21
+ export function AccessControlTabs({
22
+ activeTab,
23
+ }: {
24
+ readonly activeTab: AccessControlTab;
25
+ }) {
22
26
  return (
23
27
  <Tabs value={activeTab}>
24
- <TabsList aria-label="RBAC views">
28
+ <TabsList aria-label="Access control views">
25
29
  {tabs.map((tab) => (
26
30
  <TabsTrigger asChild={true} key={tab.tab} value={tab.tab}>
27
31
  <Link href={tab.href}>{tab.label}</Link>
@@ -12,13 +12,13 @@ export type AccessAppRole = AppRole<typeof accessManifest>;
12
12
  export const assignableRoles: readonly AccessAppRole[] =
13
13
  accessManifest.system.assignableRoles ?? [];
14
14
 
15
- export interface AdminPermissions {
15
+ export interface SettingsPermissions {
16
16
  readonly canManageAppRoles: boolean;
17
17
  readonly canManageDefaultRoles: boolean;
18
18
  }
19
19
 
20
- export async function requireAnyAdminPermission(): Promise<AdminPermissions> {
21
- if (!isAdminUiEnabled()) {
20
+ export async function requireAnySettingsPermission(): Promise<SettingsPermissions> {
21
+ if (!isAccessControlUiEnabled()) {
22
22
  notFound();
23
23
  }
24
24
 
@@ -55,8 +55,8 @@ export function readAssignableRole(formData: FormData): AccessAppRole {
55
55
  return role as AccessAppRole;
56
56
  }
57
57
 
58
- function isAdminUiEnabled(): boolean {
59
- const adminUI: { readonly enabled?: boolean } | undefined =
58
+ function isAccessControlUiEnabled(): boolean {
59
+ const accessControlUI: { readonly enabled?: boolean } | undefined =
60
60
  accessManifest.adminUI;
61
- return adminUI?.enabled !== false;
61
+ return accessControlUI?.enabled !== false;
62
62
  }