@percepta/create 3.5.1 → 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.
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { expect, test, type Page } from "@playwright/test";
3
+ import { expect, test, type Browser, type Page } from "@playwright/test";
4
4
 
5
5
  const execFileAsync = promisify(execFile);
6
6
  const password = "password";
@@ -13,14 +13,17 @@ const users = {
13
13
  } as const;
14
14
 
15
15
  test.describe("RBAC access", () => {
16
- test("customer admin can manage role mappings but cannot enter the main app", async ({
16
+ test("customer admin can manage role mappings and change app access", async ({
17
+ browser,
17
18
  page,
18
19
  }) => {
19
20
  test.setTimeout(60_000);
20
- await signIn(page, users.customerAdmin, "/admin?tab=assignments");
21
+ await signIn(page, users.customerAdmin, "/settings?tab=assignments");
21
22
 
22
- await expect(page.getByRole("heading", { name: "RBAC" })).toBeVisible();
23
- 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();
24
27
 
25
28
  const userAssignments = page.getByRole("button", {
26
29
  name: "User assignments",
@@ -46,6 +49,8 @@ test.describe("RBAC access", () => {
46
49
  await expect(
47
50
  page.getByRole("button", { name: "User assignments" }),
48
51
  ).toContainText("App Non User");
52
+
53
+ await expectAppAccess(browser, users.nonAppUser, true);
49
54
  } finally {
50
55
  if (shouldResetSeedData) {
51
56
  await resetSeedData();
@@ -61,19 +66,21 @@ test.describe("RBAC access", () => {
61
66
  })
62
67
  .not.toContain("App Non User");
63
68
 
69
+ await expectAppAccess(browser, users.nonAppUser, false);
70
+
64
71
  await page.goto("/");
65
- await expect(page.getByRole("heading", { name: /Welcome to/ })).toHaveCount(
66
- 0,
67
- );
72
+ await expect(getAppHomeHeading(page)).toHaveCount(0);
68
73
  await expectNotFound(page);
69
74
  });
70
75
 
71
- 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 ({
72
77
  page,
73
78
  }) => {
74
- await signIn(page, users.appAdmin, "/admin?tab=assignments");
79
+ await signIn(page, users.appAdmin, "/settings?tab=assignments");
75
80
 
76
- await expect(page.getByRole("heading", { name: "RBAC" })).toBeVisible();
81
+ await expect(
82
+ page.getByRole("heading", { name: "Access control" }),
83
+ ).toBeVisible();
77
84
  await expect(
78
85
  page
79
86
  .getByText("Only customer admins can edit default application roles.")
@@ -88,33 +95,31 @@ test.describe("RBAC access", () => {
88
95
  ).toHaveAttribute("aria-disabled", "true");
89
96
 
90
97
  await page.goto("/");
91
- await expect(
92
- page.getByRole("heading", { name: /Welcome to/ }),
93
- ).toBeVisible();
98
+ await expect(getAppHomeHeading(page)).toBeVisible();
94
99
  });
95
100
 
96
- 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 ({
97
102
  page,
98
103
  }) => {
99
104
  await signIn(page, users.appUser, "/");
100
105
 
101
- await expect(
102
- page.getByRole("heading", { name: /Welcome to/ }),
103
- ).toBeVisible();
104
- 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);
105
108
 
106
- await page.goto("/admin");
107
- 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);
108
113
  await expectNotFound(page);
109
114
  });
110
115
 
111
116
  test("non app user cannot enter the app", async ({ page }) => {
112
117
  await signIn(page, users.nonAppUser, "/");
113
118
 
114
- await expect(page.getByRole("heading", { name: /Welcome to/ })).toHaveCount(
115
- 0,
116
- );
117
- 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);
118
123
  await expectNotFound(page);
119
124
  });
120
125
  });
@@ -135,6 +140,35 @@ async function expectNotFound(page: Page) {
135
140
  ).toBeVisible();
136
141
  }
137
142
 
143
+ function getAppHomeHeading(page: Page) {
144
+ return page.getByRole("heading", { name: /Good afternoon/ });
145
+ }
146
+
147
+ async function expectAppAccess(
148
+ browser: Browser,
149
+ email: string,
150
+ expectedAccess: boolean,
151
+ ) {
152
+ const expectedHeadingCount = expectedAccess ? 1 : 0;
153
+
154
+ await expect
155
+ .poll(
156
+ async () => {
157
+ const context = await browser.newContext();
158
+ const page = await context.newPage();
159
+
160
+ try {
161
+ await signIn(page, email, "/");
162
+ return await getAppHomeHeading(page).count();
163
+ } finally {
164
+ await context.close();
165
+ }
166
+ },
167
+ { timeout: 15_000 },
168
+ )
169
+ .toBe(expectedHeadingCount);
170
+ }
171
+
138
172
  async function resetSeedData() {
139
173
  await execFileAsync("pnpm", ["db:seed"], { cwd: process.cwd() });
140
174
  }
@@ -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
  }