@storyshelf/app 0.5.3
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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/app-types.d.mts +54 -0
- package/dist/asset-manifest.mjs +41 -0
- package/dist/asset-manifest.mjs.map +1 -0
- package/dist/asset-paths.mjs +23 -0
- package/dist/asset-paths.mjs.map +1 -0
- package/dist/assets/htmx.min.js +3304 -0
- package/dist/assets/mark.svg +1 -0
- package/dist/capture-setup.mjs +139 -0
- package/dist/capture-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +20 -0
- package/dist/credential-keys.mjs.map +1 -0
- package/dist/credentials.mjs +56 -0
- package/dist/credentials.mjs.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +310 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lifecycle.mjs +132 -0
- package/dist/lifecycle.mjs.map +1 -0
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +75 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +68 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/middleware/request-log.mjs +30 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +39 -0
- package/dist/middleware/server-timing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs +37 -0
- package/dist/middleware/setup-gate.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +33 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/notify.d.mts +17 -0
- package/dist/notify.mjs +141 -0
- package/dist/notify.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +243 -0
- package/dist/pages/admin-system.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +392 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff-actions.mjs +162 -0
- package/dist/pages/build-diff-actions.mjs.map +1 -0
- package/dist/pages/build-diff-comments.mjs +104 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +117 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +48 -0
- package/dist/pages/build-diff-model.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +122 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +78 -0
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
- package/dist/pages/build-diff-stale.mjs +70 -0
- package/dist/pages/build-diff-stale.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +70 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +110 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +165 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/invite.mjs +91 -0
- package/dist/pages/invite.mjs.map +1 -0
- package/dist/pages/label-detail.mjs +154 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/library.mjs +343 -0
- package/dist/pages/library.mjs.map +1 -0
- package/dist/pages/login.mjs +117 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +97 -0
- package/dist/pages/passkey-ceremony.mjs.map +1 -0
- package/dist/pages/profile.mjs +364 -0
- package/dist/pages/profile.mjs.map +1 -0
- package/dist/pages/project-builds-table.mjs +151 -0
- package/dist/pages/project-builds-table.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +143 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +90 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +95 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects-card.mjs +194 -0
- package/dist/pages/projects-card.mjs.map +1 -0
- package/dist/pages/projects.mjs +129 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +98 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +138 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +107 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +192 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +240 -0
- package/dist/pages/settings-notifications.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +137 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tests.mjs +111 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +90 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +104 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/pages/storybook.mjs +116 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/retention-timer.mjs +65 -0
- package/dist/retention-timer.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +48 -0
- package/dist/routers/admin-pages.mjs.map +1 -0
- package/dist/routers/admin.mjs +113 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +64 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/attempts.mjs +96 -0
- package/dist/routers/attempts.mjs.map +1 -0
- package/dist/routers/auth-sync.mjs +64 -0
- package/dist/routers/auth-sync.mjs.map +1 -0
- package/dist/routers/auth.mjs +236 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/baseline-guard.mjs +83 -0
- package/dist/routers/baseline-guard.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +217 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +381 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +109 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/flash.mjs +35 -0
- package/dist/routers/flash.mjs.map +1 -0
- package/dist/routers/health-report.mjs +159 -0
- package/dist/routers/health-report.mjs.map +1 -0
- package/dist/routers/health.mjs +46 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +110 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +24 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +118 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +53 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +177 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/notification-channels.mjs +190 -0
- package/dist/routers/notification-channels.mjs.map +1 -0
- package/dist/routers/notification-subscriptions.mjs +120 -0
- package/dist/routers/notification-subscriptions.mjs.map +1 -0
- package/dist/routers/profile.mjs +276 -0
- package/dist/routers/profile.mjs.map +1 -0
- package/dist/routers/projects.mjs +133 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/review-next.mjs +47 -0
- package/dist/routers/review-next.mjs.map +1 -0
- package/dist/routers/schemas.mjs +337 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +104 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +52 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +58 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-notifications.mjs +108 -0
- package/dist/routers/settings-notifications.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +77 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +67 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +149 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +25 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +240 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +108 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/storybook.mjs +171 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/tokens.mjs +91 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +166 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +97 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/routers/well-known.mjs +43 -0
- package/dist/routers/well-known.mjs.map +1 -0
- package/dist/runtime.mjs +52 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store.mjs +20 -0
- package/dist/store.mjs.map +1 -0
- package/dist/ui/avatar.mjs +76 -0
- package/dist/ui/avatar.mjs.map +1 -0
- package/dist/ui/buttons.mjs +205 -0
- package/dist/ui/buttons.mjs.map +1 -0
- package/dist/ui/code-block.mjs +47 -0
- package/dist/ui/code-block.mjs.map +1 -0
- package/dist/ui/compare-stage.mjs +344 -0
- package/dist/ui/compare-stage.mjs.map +1 -0
- package/dist/ui/csrf-field.mjs +20 -0
- package/dist/ui/csrf-field.mjs.map +1 -0
- package/dist/ui/css.mjs +36 -0
- package/dist/ui/css.mjs.map +1 -0
- package/dist/ui/document.mjs +130 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/dropdown.mjs +181 -0
- package/dist/ui/dropdown.mjs.map +1 -0
- package/dist/ui/feedback.mjs +341 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/filter-input.mjs +58 -0
- package/dist/ui/filter-input.mjs.map +1 -0
- package/dist/ui/forms.mjs +291 -0
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/icons/icon.mjs +53 -0
- package/dist/ui/icons/icon.mjs.map +1 -0
- package/dist/ui/icons/paths.mjs +61 -0
- package/dist/ui/icons/paths.mjs.map +1 -0
- package/dist/ui/icons/sprite.mjs +26 -0
- package/dist/ui/icons/sprite.mjs.map +1 -0
- package/dist/ui/kbd.mjs +33 -0
- package/dist/ui/kbd.mjs.map +1 -0
- package/dist/ui/layout.mjs +173 -0
- package/dist/ui/layout.mjs.map +1 -0
- package/dist/ui/progress.mjs +44 -0
- package/dist/ui/progress.mjs.map +1 -0
- package/dist/ui/segmented.mjs +77 -0
- package/dist/ui/segmented.mjs.map +1 -0
- package/dist/ui/shell/account.mjs +161 -0
- package/dist/ui/shell/account.mjs.map +1 -0
- package/dist/ui/shell/bare.mjs +69 -0
- package/dist/ui/shell/bare.mjs.map +1 -0
- package/dist/ui/shell/boot-script.mjs +46 -0
- package/dist/ui/shell/boot-script.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +259 -0
- package/dist/ui/shell/client-script.mjs.map +1 -0
- package/dist/ui/shell/nav.mjs +111 -0
- package/dist/ui/shell/nav.mjs.map +1 -0
- package/dist/ui/shell/review-script.mjs +195 -0
- package/dist/ui/shell/review-script.mjs.map +1 -0
- package/dist/ui/shell/sidebar.mjs +323 -0
- package/dist/ui/shell/sidebar.mjs.map +1 -0
- package/dist/ui/shell/topbar.mjs +268 -0
- package/dist/ui/shell/topbar.mjs.map +1 -0
- package/dist/ui/stacks.mjs +82 -0
- package/dist/ui/stacks.mjs.map +1 -0
- package/dist/ui/styles/base.mjs +41 -0
- package/dist/ui/styles/base.mjs.map +1 -0
- package/dist/ui/styles/behavior.mjs +43 -0
- package/dist/ui/styles/behavior.mjs.map +1 -0
- package/dist/ui/styles/review-layout.mjs +347 -0
- package/dist/ui/styles/review-layout.mjs.map +1 -0
- package/dist/ui/styles/review.mjs +86 -0
- package/dist/ui/styles/review.mjs.map +1 -0
- package/dist/ui/styles/tokens.mjs +132 -0
- package/dist/ui/styles/tokens.mjs.map +1 -0
- package/dist/ui/styles.mjs +19 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/subnav.mjs +91 -0
- package/dist/ui/subnav.mjs.map +1 -0
- package/dist/ui/table.mjs +73 -0
- package/dist/ui/table.mjs.map +1 -0
- package/dist/ui/theme.mjs +64 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/ui/thumbnail.mjs +51 -0
- package/dist/ui/thumbnail.mjs.map +1 -0
- package/dist/ui/time.mjs +50 -0
- package/dist/ui/time.mjs.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert, Badge, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { Field, FormActions, SelectField } from "../ui/forms.mjs";
|
|
7
|
+
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
+
import { Table } from "../ui/table.mjs";
|
|
9
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
//#region src/pages/settings-members.tsx
|
|
12
|
+
/** Members settings tab: member roster plus the add-member form. */
|
|
13
|
+
function renderSettingsMembers(project, members, groupMappings, isAdmin, formState) {
|
|
14
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
15
|
+
class: "grid max-w-form",
|
|
16
|
+
children: [
|
|
17
|
+
formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
|
|
18
|
+
tone: "danger",
|
|
19
|
+
children: formState.globalError
|
|
20
|
+
}) : null,
|
|
21
|
+
/* @__PURE__ */ jsxs(Card, { children: [
|
|
22
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "Members" }),
|
|
23
|
+
/* @__PURE__ */ jsx(Meta, { children: "Project members and their roles. Site admins have implicit admin access." }),
|
|
24
|
+
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
25
|
+
/* @__PURE__ */ jsx("th", { children: "User" }),
|
|
26
|
+
/* @__PURE__ */ jsx("th", { children: "Role" }),
|
|
27
|
+
/* @__PURE__ */ jsx("th", { children: "Since" }),
|
|
28
|
+
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
29
|
+
class: "visually-hidden",
|
|
30
|
+
children: "Actions"
|
|
31
|
+
}) })
|
|
32
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: members.map((member) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
33
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("code", { children: member.userId }) }),
|
|
34
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
35
|
+
tone: member.role === "admin" ? "danger" : member.role === "viewer" ? "neutral" : "info",
|
|
36
|
+
children: member.role
|
|
37
|
+
}) }),
|
|
38
|
+
/* @__PURE__ */ jsx("td", { children: new Date(member.createdAt).toLocaleDateString() }),
|
|
39
|
+
/* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
|
|
40
|
+
method: "post",
|
|
41
|
+
action: `/projects/${project.slug}/settings/members/${member.userId}/remove`,
|
|
42
|
+
"hx-post": `/projects/${project.slug}/settings/members/${member.userId}/remove`,
|
|
43
|
+
"hx-target": "body",
|
|
44
|
+
"hx-confirm": "Remove this member from the project?",
|
|
45
|
+
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
46
|
+
variant: "ghost",
|
|
47
|
+
type: "submit",
|
|
48
|
+
children: "Remove"
|
|
49
|
+
})]
|
|
50
|
+
}) : null })
|
|
51
|
+
] }, member.id)) })] }) }),
|
|
52
|
+
members.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No members yet." }) : null
|
|
53
|
+
] }),
|
|
54
|
+
isAdmin ? /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
|
|
55
|
+
level: 3,
|
|
56
|
+
children: "Add member"
|
|
57
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
58
|
+
method: "post",
|
|
59
|
+
action: `/projects/${project.slug}/settings/members`,
|
|
60
|
+
"hx-post": `/projects/${project.slug}/settings/members`,
|
|
61
|
+
"hx-target": "body",
|
|
62
|
+
children: [
|
|
63
|
+
csrfField(),
|
|
64
|
+
/* @__PURE__ */ jsx(Field, {
|
|
65
|
+
label: "User ID",
|
|
66
|
+
name: "userId",
|
|
67
|
+
required: true,
|
|
68
|
+
placeholder: "user_..."
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ jsx(SelectField, {
|
|
71
|
+
label: "Role",
|
|
72
|
+
name: "role",
|
|
73
|
+
options: [
|
|
74
|
+
{
|
|
75
|
+
value: "viewer",
|
|
76
|
+
label: "viewer"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
value: "developer",
|
|
80
|
+
label: "developer"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
value: "approver",
|
|
84
|
+
label: "approver"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
value: "admin",
|
|
88
|
+
label: "admin"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}),
|
|
92
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
93
|
+
variant: "primary",
|
|
94
|
+
type: "submit",
|
|
95
|
+
icon: "plus",
|
|
96
|
+
children: "Add member"
|
|
97
|
+
}) })
|
|
98
|
+
]
|
|
99
|
+
})] }) : null,
|
|
100
|
+
/* @__PURE__ */ jsx("div", {
|
|
101
|
+
class: "mt-1",
|
|
102
|
+
children: /* @__PURE__ */ jsxs(Card, { children: [
|
|
103
|
+
/* @__PURE__ */ jsx(SectionTitle, {
|
|
104
|
+
level: 3,
|
|
105
|
+
children: "Identity-provider group mappings"
|
|
106
|
+
}),
|
|
107
|
+
/* @__PURE__ */ jsx(Meta, { children: "Map an IdP group (name or provider ID, exact match) to a project role. Members sync at next login; removing a mapping revokes synced grants but never manual ones. Wildcards are not expanded." }),
|
|
108
|
+
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
109
|
+
/* @__PURE__ */ jsx("th", { children: "Group" }),
|
|
110
|
+
/* @__PURE__ */ jsx("th", { children: "Role" }),
|
|
111
|
+
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
112
|
+
class: "visually-hidden",
|
|
113
|
+
children: "Actions"
|
|
114
|
+
}) })
|
|
115
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: groupMappings.map((mapping) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
116
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("code", { children: mapping.groupName }) }),
|
|
117
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
118
|
+
tone: mapping.role === "admin" ? "danger" : "info",
|
|
119
|
+
children: mapping.role
|
|
120
|
+
}) }),
|
|
121
|
+
/* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
|
|
122
|
+
method: "post",
|
|
123
|
+
action: `/projects/${project.slug}/settings/members/groups/${mapping.id}/remove`,
|
|
124
|
+
"hx-post": `/projects/${project.slug}/settings/members/groups/${mapping.id}/remove`,
|
|
125
|
+
"hx-target": "body",
|
|
126
|
+
"hx-confirm": "Remove this group mapping?",
|
|
127
|
+
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
128
|
+
variant: "ghost",
|
|
129
|
+
type: "submit",
|
|
130
|
+
children: "Remove"
|
|
131
|
+
})]
|
|
132
|
+
}) : null })
|
|
133
|
+
] }, mapping.id)) })] }) }),
|
|
134
|
+
groupMappings.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No group mappings yet." }) : null
|
|
135
|
+
] })
|
|
136
|
+
}),
|
|
137
|
+
isAdmin ? /* @__PURE__ */ jsx("div", {
|
|
138
|
+
class: "mt-1",
|
|
139
|
+
children: /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
|
|
140
|
+
level: 3,
|
|
141
|
+
children: "Add group mapping"
|
|
142
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
143
|
+
method: "post",
|
|
144
|
+
action: `/projects/${project.slug}/settings/members/groups`,
|
|
145
|
+
"hx-post": `/projects/${project.slug}/settings/members/groups`,
|
|
146
|
+
"hx-target": "body",
|
|
147
|
+
children: [
|
|
148
|
+
csrfField(),
|
|
149
|
+
/* @__PURE__ */ jsx(Field, {
|
|
150
|
+
label: "Group name or ID (exact match)",
|
|
151
|
+
name: "groupName",
|
|
152
|
+
required: true,
|
|
153
|
+
placeholder: "team-design"
|
|
154
|
+
}),
|
|
155
|
+
/* @__PURE__ */ jsx(SelectField, {
|
|
156
|
+
label: "Role",
|
|
157
|
+
name: "role",
|
|
158
|
+
options: [
|
|
159
|
+
{
|
|
160
|
+
value: "viewer",
|
|
161
|
+
label: "viewer"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
value: "developer",
|
|
165
|
+
label: "developer"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
value: "approver",
|
|
169
|
+
label: "approver"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
value: "admin",
|
|
173
|
+
label: "admin"
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}),
|
|
177
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
178
|
+
variant: "primary",
|
|
179
|
+
type: "submit",
|
|
180
|
+
icon: "plus",
|
|
181
|
+
children: "Add mapping"
|
|
182
|
+
}) })
|
|
183
|
+
]
|
|
184
|
+
})] })
|
|
185
|
+
}) : null
|
|
186
|
+
]
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
export { renderSettingsMembers };
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=settings-members.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-members.mjs","names":[],"sources":["../../src/pages/settings-members.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Project member row as rendered in the members settings tab. */\nexport interface SettingsMember {\n id: string;\n projectId: string;\n userId: string;\n role: string;\n createdAt: string;\n}\n\n/** Members settings tab: member roster plus the add-member form. */\nexport function renderSettingsMembers(\n project: Project,\n members: SettingsMember[],\n groupMappings: ProjectGroupMapping[],\n isAdmin: boolean,\n formState?: { globalError?: string },\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>Members</SectionTitle>\n <Meta>Project members and their roles. Site admins have implicit admin access.</Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>User</th>\n <th>Role</th>\n <th>Since</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {members.map((member): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={member.id}>\n <td>\n <code>{member.userId}</code>\n </td>\n <td>\n <Badge\n tone={\n member.role === \"admin\"\n ? \"danger\"\n : member.role === \"viewer\"\n ? \"neutral\"\n : \"info\"\n }\n >\n {member.role}\n </Badge>\n </td>\n <td>{new Date(member.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/members/${member.userId}/remove`}\n hx-post={`/projects/${project.slug}/settings/members/${member.userId}/remove`}\n hx-target=\"body\"\n hx-confirm=\"Remove this member from the project?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" type=\"submit\">\n Remove\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {members.length === 0 ? <Meta>No members yet.</Meta> : null}\n </Card>\n\n {isAdmin ? (\n <Card>\n <SectionTitle level={3}>Add member</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/members`}\n hx-post={`/projects/${project.slug}/settings/members`}\n hx-target=\"body\"\n >\n {csrfField()}\n <Field label=\"User ID\" name=\"userId\" required placeholder=\"user_...\" />\n <SelectField\n label=\"Role\"\n name=\"role\"\n options={[\n { value: \"viewer\", label: \"viewer\" },\n { value: \"developer\", label: \"developer\" },\n { value: \"approver\", label: \"approver\" },\n { value: \"admin\", label: \"admin\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add member\n </Button>\n </FormActions>\n </form>\n </Card>\n ) : null}\n\n <div class=\"mt-1\">\n <Card>\n <SectionTitle level={3}>Identity-provider group mappings</SectionTitle>\n <Meta>\n Map an IdP group (name or provider ID, exact match) to a project role. Members sync at\n next login; removing a mapping revokes synced grants but never manual ones. Wildcards\n are not expanded.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Group</th>\n <th>Role</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {groupMappings.map((mapping): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={mapping.id}>\n <td>\n <code>{mapping.groupName}</code>\n </td>\n <td>\n <Badge tone={mapping.role === \"admin\" ? \"danger\" : \"info\"}>\n {mapping.role}\n </Badge>\n </td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/members/groups/${mapping.id}/remove`}\n hx-post={`/projects/${project.slug}/settings/members/groups/${mapping.id}/remove`}\n hx-target=\"body\"\n hx-confirm=\"Remove this group mapping?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" type=\"submit\">\n Remove\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {groupMappings.length === 0 ? <Meta>No group mappings yet.</Meta> : null}\n </Card>\n </div>\n\n {isAdmin ? (\n <div class=\"mt-1\">\n <Card>\n <SectionTitle level={3}>Add group mapping</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/members/groups`}\n hx-post={`/projects/${project.slug}/settings/members/groups`}\n hx-target=\"body\"\n >\n {csrfField()}\n <Field\n label=\"Group name or ID (exact match)\"\n name=\"groupName\"\n required\n placeholder=\"team-design\"\n />\n <SelectField\n label=\"Role\"\n name=\"role\"\n options={[\n { value: \"viewer\", label: \"viewer\" },\n { value: \"developer\", label: \"developer\" },\n { value: \"approver\", label: \"approver\" },\n { value: \"admin\", label: \"admin\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add mapping\n </Button>\n </FormActions>\n </form>\n </Card>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert, Badge, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { CheckField, Field, FormActions, SelectField } from "../ui/forms.mjs";
|
|
7
|
+
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
+
import { HStack } from "../ui/stacks.mjs";
|
|
9
|
+
import { Table } from "../ui/table.mjs";
|
|
10
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
11
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
|
+
//#region src/pages/settings-notifications.tsx
|
|
13
|
+
/** Notification topic catalog (mirrors the webhook event names). */
|
|
14
|
+
const NOTIFICATION_TOPICS = [
|
|
15
|
+
{
|
|
16
|
+
event: "build:created",
|
|
17
|
+
label: "Build created"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
event: "build:reviewing",
|
|
21
|
+
label: "Build needs review"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
event: "build:approved",
|
|
25
|
+
label: "Build approved"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
event: "build:rejected",
|
|
29
|
+
label: "Build rejected"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
event: "baseline:created",
|
|
33
|
+
label: "Baseline created"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
event: "baseline:updated",
|
|
37
|
+
label: "Baseline updated"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
event: "comment:created",
|
|
41
|
+
label: "Comment posted"
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
/** Whether a topic checkbox renders checked (empty allowlist means all). */
|
|
45
|
+
function topicChecked(events, event) {
|
|
46
|
+
return events.length === 0 || events.includes(event);
|
|
47
|
+
}
|
|
48
|
+
/** The viewer's own preferences card (visible to every member). */
|
|
49
|
+
const MyNotificationsCard = ({ project, mySubscription }) => {
|
|
50
|
+
const events = mySubscription?.events ?? [];
|
|
51
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
52
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "My notifications for this project" }),
|
|
53
|
+
/* @__PURE__ */ jsx(Meta, { children: "Opt in to email alerts for this project. Leave everything off to stay silent." }),
|
|
54
|
+
/* @__PURE__ */ jsxs("form", {
|
|
55
|
+
method: "post",
|
|
56
|
+
action: `/projects/${project.slug}/settings/notifications/me`,
|
|
57
|
+
"hx-post": `/projects/${project.slug}/settings/notifications/me`,
|
|
58
|
+
"hx-target": "body",
|
|
59
|
+
children: [
|
|
60
|
+
csrfField(),
|
|
61
|
+
/* @__PURE__ */ jsx(CheckField, {
|
|
62
|
+
label: "Email me about this project",
|
|
63
|
+
name: "enabled",
|
|
64
|
+
checked: mySubscription?.enabled ?? false
|
|
65
|
+
}),
|
|
66
|
+
NOTIFICATION_TOPICS.map((topic) => /* @__PURE__ */ jsx(CheckField, {
|
|
67
|
+
label: topic.label,
|
|
68
|
+
name: "events",
|
|
69
|
+
value: topic.event,
|
|
70
|
+
checked: topicChecked(events, topic.event)
|
|
71
|
+
})),
|
|
72
|
+
/* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(Meta, {
|
|
73
|
+
as: "span",
|
|
74
|
+
children: "No boxes ticked means all topics."
|
|
75
|
+
}) }),
|
|
76
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
77
|
+
variant: "primary",
|
|
78
|
+
type: "submit",
|
|
79
|
+
icon: "check",
|
|
80
|
+
children: "Save my preferences"
|
|
81
|
+
}) })
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
] });
|
|
85
|
+
};
|
|
86
|
+
/** Human-readable channel target (email address or masked webhook host). */
|
|
87
|
+
function targetOf(channel) {
|
|
88
|
+
const to = channel.config["to"];
|
|
89
|
+
if (typeof to === "string" && to) return to;
|
|
90
|
+
return channel.hasSecret ? `${channel.provider} webhook` : "—";
|
|
91
|
+
}
|
|
92
|
+
/** One channel table row with the admin delete action. */
|
|
93
|
+
const ChannelRow = ({ project, channel }) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
94
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
95
|
+
tone: "neutral",
|
|
96
|
+
children: channel.provider
|
|
97
|
+
}) }),
|
|
98
|
+
/* @__PURE__ */ jsx("td", {
|
|
99
|
+
class: "truncate max-w-cell",
|
|
100
|
+
title: targetOf(channel),
|
|
101
|
+
children: targetOf(channel)
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ jsx("td", { children: channel.events.length === 0 ? /* @__PURE__ */ jsx(Meta, {
|
|
104
|
+
as: "span",
|
|
105
|
+
children: "all events"
|
|
106
|
+
}) : /* @__PURE__ */ jsx(HStack, { children: channel.events.map((event) => /* @__PURE__ */ jsx(Badge, {
|
|
107
|
+
tone: "neutral",
|
|
108
|
+
children: event
|
|
109
|
+
})) }) }),
|
|
110
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("form", {
|
|
111
|
+
method: "post",
|
|
112
|
+
action: `/projects/${project.slug}/settings/notifications/${channel.id}/delete`,
|
|
113
|
+
"hx-post": `/projects/${project.slug}/settings/notifications/${channel.id}/delete`,
|
|
114
|
+
"hx-target": "body",
|
|
115
|
+
"hx-confirm": "Remove this notification channel?",
|
|
116
|
+
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
117
|
+
variant: "ghost",
|
|
118
|
+
size: "sm",
|
|
119
|
+
type: "submit",
|
|
120
|
+
children: "Delete"
|
|
121
|
+
})]
|
|
122
|
+
}) })
|
|
123
|
+
] });
|
|
124
|
+
/** Channel list card (delete action renders for admins only). */
|
|
125
|
+
const ChannelsCard = ({ project, channels, isAdmin }) => /* @__PURE__ */ jsxs(Card, { children: [
|
|
126
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "Channels" }),
|
|
127
|
+
/* @__PURE__ */ jsx(Meta, { children: "Project admins route build events to Slack, Teams, or email. Secrets are stored encrypted and decrypted only in memory at send time." }),
|
|
128
|
+
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
129
|
+
/* @__PURE__ */ jsx("th", { children: "Provider" }),
|
|
130
|
+
/* @__PURE__ */ jsx("th", { children: "Target" }),
|
|
131
|
+
/* @__PURE__ */ jsx("th", { children: "Events" }),
|
|
132
|
+
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
133
|
+
class: "visually-hidden",
|
|
134
|
+
children: "Actions"
|
|
135
|
+
}) })
|
|
136
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: channels.map((channel) => isAdmin ? /* @__PURE__ */ jsx(ChannelRow, {
|
|
137
|
+
project,
|
|
138
|
+
channel
|
|
139
|
+
}) : /* @__PURE__ */ jsxs("tr", { children: [
|
|
140
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
141
|
+
tone: "neutral",
|
|
142
|
+
children: channel.provider
|
|
143
|
+
}) }),
|
|
144
|
+
/* @__PURE__ */ jsx("td", {
|
|
145
|
+
class: "truncate max-w-cell",
|
|
146
|
+
title: targetOf(channel),
|
|
147
|
+
children: targetOf(channel)
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
|
|
150
|
+
as: "span",
|
|
151
|
+
children: channel.events.length === 0 ? "all events" : channel.events.join(", ")
|
|
152
|
+
}) }),
|
|
153
|
+
/* @__PURE__ */ jsx("td", { children: null })
|
|
154
|
+
] }, channel.id)) })] }) }),
|
|
155
|
+
channels.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No channels configured." }) : null
|
|
156
|
+
] });
|
|
157
|
+
/** Channel creation card (admins only). */
|
|
158
|
+
const CreateChannelCard = ({ project, providers, formState }) => /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
|
|
159
|
+
level: 3,
|
|
160
|
+
children: "Create channel"
|
|
161
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
162
|
+
method: "post",
|
|
163
|
+
action: `/projects/${project.slug}/settings/notifications`,
|
|
164
|
+
"hx-post": `/projects/${project.slug}/settings/notifications`,
|
|
165
|
+
"hx-target": "body",
|
|
166
|
+
children: [
|
|
167
|
+
csrfField(),
|
|
168
|
+
/* @__PURE__ */ jsx(SelectField, {
|
|
169
|
+
label: "Provider",
|
|
170
|
+
name: "provider",
|
|
171
|
+
options: providers.map((provider) => ({
|
|
172
|
+
value: provider,
|
|
173
|
+
label: provider
|
|
174
|
+
})),
|
|
175
|
+
hint: formState?.errors?.["provider"]
|
|
176
|
+
}),
|
|
177
|
+
/* @__PURE__ */ jsx(Field, {
|
|
178
|
+
label: "Target",
|
|
179
|
+
name: "target",
|
|
180
|
+
type: "text",
|
|
181
|
+
required: true,
|
|
182
|
+
placeholder: "team@example.com or https://hooks.slack.com/…",
|
|
183
|
+
hint: "Email address for email channels, webhook URL otherwise.",
|
|
184
|
+
error: formState?.errors?.["target"]
|
|
185
|
+
}),
|
|
186
|
+
/* @__PURE__ */ jsx(Field, {
|
|
187
|
+
label: "Events",
|
|
188
|
+
name: "events",
|
|
189
|
+
placeholder: "build.reviewing, build.approved",
|
|
190
|
+
hint: "Comma-separated. Leave blank to receive all events."
|
|
191
|
+
}),
|
|
192
|
+
/* @__PURE__ */ jsx(SelectField, {
|
|
193
|
+
label: "Style",
|
|
194
|
+
name: "style",
|
|
195
|
+
options: [{
|
|
196
|
+
value: "compact",
|
|
197
|
+
label: "Compact"
|
|
198
|
+
}, {
|
|
199
|
+
value: "verbose",
|
|
200
|
+
label: "Verbose"
|
|
201
|
+
}]
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
204
|
+
variant: "primary",
|
|
205
|
+
type: "submit",
|
|
206
|
+
icon: "plus",
|
|
207
|
+
children: "Add channel"
|
|
208
|
+
}) })
|
|
209
|
+
]
|
|
210
|
+
})] });
|
|
211
|
+
/** Notifications settings tab: channel list, create form, and my preferences. */
|
|
212
|
+
function renderSettingsNotifications(project, channels, providers, mySubscription, isAdmin, formState) {
|
|
213
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
214
|
+
class: "grid max-w-form",
|
|
215
|
+
children: [
|
|
216
|
+
formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
|
|
217
|
+
tone: "danger",
|
|
218
|
+
children: formState.globalError
|
|
219
|
+
}) : null,
|
|
220
|
+
/* @__PURE__ */ jsx(MyNotificationsCard, {
|
|
221
|
+
project,
|
|
222
|
+
mySubscription
|
|
223
|
+
}),
|
|
224
|
+
/* @__PURE__ */ jsx(ChannelsCard, {
|
|
225
|
+
project,
|
|
226
|
+
channels,
|
|
227
|
+
isAdmin
|
|
228
|
+
}),
|
|
229
|
+
isAdmin ? /* @__PURE__ */ jsx(CreateChannelCard, {
|
|
230
|
+
project,
|
|
231
|
+
providers,
|
|
232
|
+
formState
|
|
233
|
+
}) : null
|
|
234
|
+
]
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
export { NOTIFICATION_TOPICS, renderSettingsNotifications };
|
|
239
|
+
|
|
240
|
+
//# sourceMappingURL=settings-notifications.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/pages/settings-notifications.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n HStack,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Notification topic catalog (mirrors the webhook event names). */\nexport const NOTIFICATION_TOPICS: { event: string; label: string }[] = [\n { event: \"build:created\", label: \"Build created\" },\n { event: \"build:reviewing\", label: \"Build needs review\" },\n { event: \"build:approved\", label: \"Build approved\" },\n { event: \"build:rejected\", label: \"Build rejected\" },\n { event: \"baseline:created\", label: \"Baseline created\" },\n { event: \"baseline:updated\", label: \"Baseline updated\" },\n { event: \"comment:created\", label: \"Comment posted\" },\n];\n\n/** Channel row as rendered in the notifications settings tab. */\nexport interface SettingsNotificationChannel {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n}\n\n/** The viewer's own subscription for this project (null when opted out). */\nexport interface SettingsMySubscription {\n events: string[];\n via: string[];\n enabled: boolean;\n}\n\n/** Form state for the notifications tab (field errors, global error). */\nexport interface NotificationsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\n/** Whether a topic checkbox renders checked (empty allowlist means all). */\nfunction topicChecked(events: string[], event: string): boolean {\n return events.length === 0 || events.includes(event);\n}\n\n/** The viewer's own preferences card (visible to every member). */\nconst MyNotificationsCard: FC<{\n project: Project;\n mySubscription: SettingsMySubscription | null;\n}> = ({ project, mySubscription }) => {\n const events = mySubscription?.events ?? [];\n return (\n <Card>\n <SectionTitle>My notifications for this project</SectionTitle>\n <Meta>Opt in to email alerts for this project. Leave everything off to stay silent.</Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/me`}\n hx-post={`/projects/${project.slug}/settings/notifications/me`}\n hx-target=\"body\"\n >\n {csrfField()}\n <CheckField\n label=\"Email me about this project\"\n name=\"enabled\"\n checked={mySubscription?.enabled ?? false}\n />\n {NOTIFICATION_TOPICS.map((topic) => (\n <CheckField\n label={topic.label}\n name=\"events\"\n value={topic.event}\n checked={topicChecked(events, topic.event)}\n />\n ))}\n <p>\n <Meta as=\"span\">No boxes ticked means all topics.</Meta>\n </p>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save my preferences\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\n/** Human-readable channel target (email address or masked webhook host). */\nfunction targetOf(channel: SettingsNotificationChannel): string {\n const to = channel.config[\"to\"];\n if (typeof to === \"string\" && to) {\n return to;\n }\n return channel.hasSecret ? `${channel.provider} webhook` : \"—\";\n}\n\n/** One channel table row with the admin delete action. */\nconst ChannelRow: FC<{ project: Project; channel: SettingsNotificationChannel }> = ({\n project,\n channel,\n}) => (\n <tr>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n {channel.events.length === 0 ? (\n <Meta as=\"span\">all events</Meta>\n ) : (\n <HStack>\n {channel.events.map((event) => (\n <Badge tone=\"neutral\">{event}</Badge>\n ))}\n </HStack>\n )}\n </td>\n <td>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this notification channel?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Delete\n </Button>\n </form>\n </td>\n </tr>\n);\n\n/** Channel list card (delete action renders for admins only). */\nconst ChannelsCard: FC<{\n project: Project;\n channels: SettingsNotificationChannel[];\n isAdmin: boolean;\n}> = ({ project, channels, isAdmin }) => (\n <Card>\n <SectionTitle>Channels</SectionTitle>\n <Meta>\n Project admins route build events to Slack, Teams, or email. Secrets are stored encrypted and\n decrypted only in memory at send time.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Target</th>\n <th>Events</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {channels.map((channel) =>\n isAdmin ? (\n <ChannelRow project={project} channel={channel} />\n ) : (\n <tr key={channel.id}>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n <Meta as=\"span\">\n {channel.events.length === 0 ? \"all events\" : channel.events.join(\", \")}\n </Meta>\n </td>\n <td>{null}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n {channels.length === 0 ? <Meta>No channels configured.</Meta> : null}\n </Card>\n);\n\n/** Channel creation card (admins only). */\nconst CreateChannelCard: FC<{\n project: Project;\n providers: string[];\n formState?: NotificationsFormState;\n}> = ({ project, providers, formState }) => (\n <Card>\n <SectionTitle level={3}>Create channel</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications`}\n hx-post={`/projects/${project.slug}/settings/notifications`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providers.map((provider) => ({ value: provider, label: provider }))}\n hint={formState?.errors?.[\"provider\"]}\n />\n <Field\n label=\"Target\"\n name=\"target\"\n type=\"text\"\n required\n placeholder=\"team@example.com or https://hooks.slack.com/…\"\n hint=\"Email address for email channels, webhook URL otherwise.\"\n error={formState?.errors?.[\"target\"]}\n />\n <Field\n label=\"Events\"\n name=\"events\"\n placeholder=\"build.reviewing, build.approved\"\n hint=\"Comma-separated. Leave blank to receive all events.\"\n />\n <SelectField\n label=\"Style\"\n name=\"style\"\n options={[\n { value: \"compact\", label: \"Compact\" },\n { value: \"verbose\", label: \"Verbose\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add channel\n </Button>\n </FormActions>\n </form>\n </Card>\n);\n\n/** Notifications settings tab: channel list, create form, and my preferences. */\nexport function renderSettingsNotifications(\n project: Project,\n channels: SettingsNotificationChannel[],\n providers: string[],\n mySubscription: SettingsMySubscription | null,\n isAdmin: boolean,\n formState?: NotificationsFormState,\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <MyNotificationsCard project={project} mySubscription={mySubscription} />\n <ChannelsCard project={project} channels={channels} isAdmin={isAdmin} />\n {isAdmin ? (\n <CreateChannelCard project={project} providers={providers} formState={formState} />\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert, Badge, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { Field, FormActions, SelectField, TextareaField } from "../ui/forms.mjs";
|
|
7
|
+
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
+
import { Table } from "../ui/table.mjs";
|
|
9
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
//#region src/pages/settings-status.tsx
|
|
12
|
+
function providerOptions(providers) {
|
|
13
|
+
return providers.map((provider) => ({
|
|
14
|
+
value: provider.metadata.kind,
|
|
15
|
+
label: provider.metadata.name
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
const StatusConfigRow = ({ config, provider, project, isAdmin }) => {
|
|
19
|
+
const rendered = JSON.stringify(config.config);
|
|
20
|
+
return /* @__PURE__ */ jsxs("tr", { children: [
|
|
21
|
+
/* @__PURE__ */ jsx("td", { children: provider?.metadata.name ?? config.provider }),
|
|
22
|
+
/* @__PURE__ */ jsx("td", {
|
|
23
|
+
class: "truncate max-w-cell",
|
|
24
|
+
title: rendered,
|
|
25
|
+
children: /* @__PURE__ */ jsx("code", { children: rendered })
|
|
26
|
+
}),
|
|
27
|
+
/* @__PURE__ */ jsx("td", { children: config.hasToken ? /* @__PURE__ */ jsx(Badge, {
|
|
28
|
+
tone: "success",
|
|
29
|
+
children: "configured"
|
|
30
|
+
}) : /* @__PURE__ */ jsx(Badge, {
|
|
31
|
+
tone: "danger",
|
|
32
|
+
children: "missing"
|
|
33
|
+
}) }),
|
|
34
|
+
/* @__PURE__ */ jsx("td", { children: new Date(config.createdAt).toLocaleDateString() }),
|
|
35
|
+
/* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
|
|
36
|
+
method: "post",
|
|
37
|
+
action: `/projects/${project.slug}/settings/status/${config.id}/delete`,
|
|
38
|
+
"hx-post": `/projects/${project.slug}/settings/status/${config.id}/delete`,
|
|
39
|
+
"hx-target": "body",
|
|
40
|
+
"hx-confirm": "Remove this git status config?",
|
|
41
|
+
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
42
|
+
variant: "ghost",
|
|
43
|
+
type: "submit",
|
|
44
|
+
children: "Delete"
|
|
45
|
+
})]
|
|
46
|
+
}) : null })
|
|
47
|
+
] }, config.id);
|
|
48
|
+
};
|
|
49
|
+
const StatusCreateCard = ({ project, providers, formState }) => {
|
|
50
|
+
return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
|
|
51
|
+
level: 3,
|
|
52
|
+
children: "Add git provider"
|
|
53
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
54
|
+
method: "post",
|
|
55
|
+
action: `/projects/${project.slug}/settings/status`,
|
|
56
|
+
"hx-post": `/projects/${project.slug}/settings/status`,
|
|
57
|
+
"hx-target": "body",
|
|
58
|
+
children: [
|
|
59
|
+
csrfField(),
|
|
60
|
+
/* @__PURE__ */ jsx(SelectField, {
|
|
61
|
+
label: "Provider",
|
|
62
|
+
name: "provider",
|
|
63
|
+
options: providerOptions(providers),
|
|
64
|
+
hint: "Integration that posts commit statuses for this project."
|
|
65
|
+
}),
|
|
66
|
+
/* @__PURE__ */ jsx(Field, {
|
|
67
|
+
label: "Token",
|
|
68
|
+
name: "token",
|
|
69
|
+
type: "password",
|
|
70
|
+
required: true,
|
|
71
|
+
placeholder: "ghp_…",
|
|
72
|
+
error: formState?.errors?.["token"],
|
|
73
|
+
hint: "Scoped token for the provider (e.g. a GitHub PAT with repo:status)."
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ jsx(TextareaField, {
|
|
76
|
+
label: "Config",
|
|
77
|
+
name: "config",
|
|
78
|
+
rows: 6,
|
|
79
|
+
placeholder: "{\"owner\":\"my-org\",\"repo\":\"my-repo\"}",
|
|
80
|
+
error: formState?.errors?.["config"],
|
|
81
|
+
hint: "JSON configuration for the provider. See the provider documentation for its fields."
|
|
82
|
+
}),
|
|
83
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
84
|
+
variant: "primary",
|
|
85
|
+
type: "submit",
|
|
86
|
+
icon: "plus",
|
|
87
|
+
children: "Add git provider"
|
|
88
|
+
}) })
|
|
89
|
+
]
|
|
90
|
+
})] });
|
|
91
|
+
};
|
|
92
|
+
function renderCreateSection(project, providers, formState) {
|
|
93
|
+
if (providers.length === 0) return /* @__PURE__ */ jsx(Meta, { children: "No git providers registered on this server." });
|
|
94
|
+
return /* @__PURE__ */ jsx(StatusCreateCard, {
|
|
95
|
+
project,
|
|
96
|
+
providers,
|
|
97
|
+
formState
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Git status settings tab: configured providers plus the add-provider form. */
|
|
101
|
+
function renderSettingsStatus(project, statusConfigs, providers, isAdmin, formState) {
|
|
102
|
+
const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));
|
|
103
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
104
|
+
class: "grid max-w-form",
|
|
105
|
+
children: [
|
|
106
|
+
formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
|
|
107
|
+
tone: "danger",
|
|
108
|
+
children: formState.globalError
|
|
109
|
+
}) : null,
|
|
110
|
+
/* @__PURE__ */ jsxs(Card, { children: [
|
|
111
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "Git status" }),
|
|
112
|
+
/* @__PURE__ */ jsx(Meta, { children: "Post commit statuses to a git provider so visual tests show up in your PR checks. Status is reported for each configured provider: pending while capturing, success on approval, failure on rejection or capture errors." }),
|
|
113
|
+
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
114
|
+
/* @__PURE__ */ jsx("th", { children: "Provider" }),
|
|
115
|
+
/* @__PURE__ */ jsx("th", { children: "Config" }),
|
|
116
|
+
/* @__PURE__ */ jsx("th", { children: "Token" }),
|
|
117
|
+
/* @__PURE__ */ jsx("th", { children: "Created" }),
|
|
118
|
+
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
119
|
+
class: "visually-hidden",
|
|
120
|
+
children: "Actions"
|
|
121
|
+
}) })
|
|
122
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: statusConfigs.map((config) => /* @__PURE__ */ jsx(StatusConfigRow, {
|
|
123
|
+
config,
|
|
124
|
+
provider: byProvider.get(config.provider),
|
|
125
|
+
project,
|
|
126
|
+
isAdmin
|
|
127
|
+
}, config.id)) })] }) }),
|
|
128
|
+
statusConfigs.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No git providers configured for this project." }) : null
|
|
129
|
+
] }),
|
|
130
|
+
isAdmin ? renderCreateSection(project, providers, formState) : null
|
|
131
|
+
]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { renderSettingsStatus };
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=settings-status.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-status.mjs","names":[],"sources":["../../src/pages/settings-status.tsx"],"sourcesContent":["import type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n TextareaField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Git status configuration row as rendered in the status settings tab. */\nexport interface SettingsStatusConfig {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasToken: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n/** Form state for the git status settings tab. */\nexport interface StatusFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nfunction providerOptions(providers: GitHostProvider[]): { value: string; label: string }[] {\n return providers.map((provider) => ({\n value: provider.metadata.kind,\n label: provider.metadata.name,\n }));\n}\n\nconst StatusConfigRow: FC<{\n config: SettingsStatusConfig;\n provider: GitHostProvider | undefined;\n project: Project;\n isAdmin: boolean;\n}> = ({ config, provider, project, isAdmin }) => {\n const rendered = JSON.stringify(config.config);\n return (\n <tr key={config.id}>\n <td>{provider?.metadata.name ?? config.provider}</td>\n <td class=\"truncate max-w-cell\" title={rendered}>\n <code>{rendered}</code>\n </td>\n <td>\n {config.hasToken ? (\n <Badge tone=\"success\">configured</Badge>\n ) : (\n <Badge tone=\"danger\">missing</Badge>\n )}\n </td>\n <td>{new Date(config.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/status/${config.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/status/${config.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this git status config?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" type=\"submit\">\n Delete\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n );\n};\n\nconst StatusCreateCard: FC<{\n project: Project;\n providers: GitHostProvider[];\n formState?: StatusFormState;\n}> = ({ project, providers, formState }) => {\n return (\n <Card>\n <SectionTitle level={3}>Add git provider</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/status`}\n hx-post={`/projects/${project.slug}/settings/status`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providerOptions(providers)}\n hint=\"Integration that posts commit statuses for this project.\"\n />\n <Field\n label=\"Token\"\n name=\"token\"\n type=\"password\"\n required\n placeholder=\"ghp_…\"\n error={formState?.errors?.[\"token\"]}\n hint=\"Scoped token for the provider (e.g. a GitHub PAT with repo:status).\"\n />\n <TextareaField\n label=\"Config\"\n name=\"config\"\n rows={6}\n placeholder='{\"owner\":\"my-org\",\"repo\":\"my-repo\"}'\n error={formState?.errors?.[\"config\"]}\n hint=\"JSON configuration for the provider. See the provider documentation for its fields.\"\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add git provider\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\nfunction renderCreateSection(\n project: Project,\n providers: GitHostProvider[],\n formState?: StatusFormState,\n): unknown {\n if (providers.length === 0) {\n return <Meta>No git providers registered on this server.</Meta>;\n }\n return <StatusCreateCard project={project} providers={providers} formState={formState} />;\n}\n\n/** Git status settings tab: configured providers plus the add-provider form. */\n// eslint-disable-next-line max-params -- matches sibling settings render functions\nexport function renderSettingsStatus(\n project: Project,\n statusConfigs: SettingsStatusConfig[],\n providers: GitHostProvider[],\n isAdmin: boolean,\n formState?: StatusFormState,\n): unknown {\n const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n\n <Card>\n <SectionTitle>Git status</SectionTitle>\n <Meta>\n Post commit statuses to a git provider so visual tests show up in your PR checks. Status\n is reported for each configured provider: pending while capturing, success on approval,\n failure on rejection or capture errors.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Config</th>\n <th>Token</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {statusConfigs.map((config) => (\n <StatusConfigRow\n key={config.id}\n config={config}\n provider={byProvider.get(config.provider)}\n project={project}\n isAdmin={isAdmin}\n />\n ))}\n </tbody>\n </table>\n </Table>\n {statusConfigs.length === 0 ? (\n <Meta>No git providers configured for this project.</Meta>\n ) : null}\n </Card>\n\n {isAdmin ? renderCreateSection(project, providers, formState) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAoCA,SAAS,gBAAgB,WAAkE;CACzF,OAAO,UAAU,KAAK,cAAc;EAClC,OAAO,SAAS,SAAS;EACzB,OAAO,SAAS,SAAS;CAC3B,EAAE;AACJ;AAEA,MAAM,mBAKA,EAAE,QAAQ,UAAU,SAAS,cAAc;CAC/C,MAAM,WAAW,KAAK,UAAU,OAAO,MAAM;CAC7C,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,MAAD,EAAA,UAAK,UAAU,SAAS,QAAQ,OAAO,SAAa,CAAA;EACpD,oBAAC,MAAD;GAAI,OAAM;GAAsB,OAAO;GACrC,UAAA,oBAAC,QAAD,EAAA,UAAO,SAAe,CAAA;EACpB,CAAA;EACJ,oBAAC,MAAD,EAAA,UACG,OAAO,WACN,oBAAC,OAAD;GAAO,MAAK;GAAU,UAAA;EAAiB,CAAA,IAEvC,oBAAC,OAAD;GAAO,MAAK;GAAS,UAAA;EAAc,CAAA,EAEnC,CAAA;EACJ,oBAAC,MAAD,EAAA,UAAK,IAAI,KAAK,OAAO,SAAS,CAAC,CAAC,mBAAmB,EAAM,CAAA;EACzD,oBAAC,MAAD,EAAA,UACG,UACC,qBAAC,QAAD;GACE,QAAO;GACP,QAAQ,aAAa,QAAQ,KAAK,mBAAmB,OAAO,GAAG;GAC/D,WAAS,aAAa,QAAQ,KAAK,mBAAmB,OAAO,GAAG;GAChE,aAAU;GACV,cAAW;GALb,UAAA,CAOG,UAAU,GACX,oBAAC,QAAD;IAAQ,SAAQ;IAAQ,MAAK;IAAS,UAAA;GAE9B,CAAA,CACJ;EACJ,CAAA,IAAA,KACF,CAAA;CACF,EAAA,GA7BK,OAAO,EA6BZ;AAER;AAEA,MAAM,oBAIA,EAAE,SAAS,WAAW,gBAAgB;CAC1C,OACE,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD;EAAc,OAAO;EAAG,UAAA;CAA8B,CAAA,GACtD,qBAAC,QAAD;EACE,QAAO;EACP,QAAQ,aAAa,QAAQ,KAAK;EAClC,WAAS,aAAa,QAAQ,KAAK;EACnC,aAAU;EAJZ,UAAA;GAMG,UAAU;GACX,oBAAC,aAAD;IACE,OAAM;IACN,MAAK;IACL,SAAS,gBAAgB,SAAS;IAClC,MAAK;GACN,CAAA;GACD,oBAAC,OAAD;IACE,OAAM;IACN,MAAK;IACL,MAAK;IACL,UAAA;IACA,aAAY;IACZ,OAAO,WAAW,SAAS;IAC3B,MAAK;GACN,CAAA;GACD,oBAAC,eAAD;IACE,OAAM;IACN,MAAK;IACL,MAAM;IACN,aAAY;IACZ,OAAO,WAAW,SAAS;IAC3B,MAAK;GACN,CAAA;GACD,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,MAAK;IAAO,UAAA;GAE5C,CAAA,EACG,CAAA;EACT;CACF,CAAA,CAAA,EAAA,CAAA;AAEV;AAEA,SAAS,oBACP,SACA,WACA,WACS;CACT,IAAI,UAAU,WAAW,GACvB,OAAO,oBAAC,MAAD,EAAA,UAAM,8CAAiD,CAAA;CAEhE,OAAO,oBAAC,kBAAD;EAA2B;EAAoB;EAAsB;CAAY,CAAA;AAC1F;;AAIA,SAAgB,qBACd,SACA,eACA,WACA,SACA,WACS;CACT,MAAM,aAAa,IAAI,IAAI,UAAU,KAAK,aAAa,CAAC,SAAS,SAAS,MAAM,QAAQ,CAAC,CAAC;CAC1F,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACG,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GAEjF,qBAAC,MAAD,EAAA,UAAA;IACE,oBAAC,cAAD,EAAA,UAAc,aAAwB,CAAA;IACtC,oBAAC,MAAD,EAAA,UAAM,2NAIA,CAAA;IACN,oBAAC,OAAD,EAAA,UACE,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;KACE,oBAAC,MAAD,EAAA,UAAI,WAAY,CAAA;KAChB,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;KACd,oBAAC,MAAD,EAAA,UAAI,QAAS,CAAA;KACb,oBAAC,MAAD,EAAA,UAAI,UAAW,CAAA;KACf,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;MAAM,OAAM;MAAkB,UAAA;KAAa,CAAA,EACzC,CAAA;IACF,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,cAAc,KAAK,WAClB,oBAAC,iBAAD;KAEU;KACR,UAAU,WAAW,IAAI,OAAO,QAAQ;KAC/B;KACA;IACV,GALM,OAAO,EAKb,CACF,EACI,CAAA,CACF,EAAA,CAAA,EACF,CAAA;IACN,cAAc,WAAW,IACxB,oBAAC,MAAD,EAAA,UAAM,gDAAmD,CAAA,IACvD;GACA,EAAA,CAAA;GAEL,UAAU,oBAAoB,SAAS,WAAW,SAAS,IAAI;EAC7D;;AAET"}
|