@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,173 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { css } from "./css.mjs";
|
|
5
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
6
|
+
//#region src/ui/layout.tsx
|
|
7
|
+
const cardBase = css`
|
|
8
|
+
/* card */
|
|
9
|
+
background: var(--surface-card);
|
|
10
|
+
border: 1px solid var(--border);
|
|
11
|
+
border-radius: var(--radius-lg);
|
|
12
|
+
box-shadow: var(--shadow);
|
|
13
|
+
`;
|
|
14
|
+
const cardColumn = css`
|
|
15
|
+
/* card-column */
|
|
16
|
+
${cardBase}
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
height: 100%;
|
|
20
|
+
`;
|
|
21
|
+
const cardPadded = css`
|
|
22
|
+
/* card-padded */
|
|
23
|
+
${cardBase}
|
|
24
|
+
padding: 1rem;
|
|
25
|
+
`;
|
|
26
|
+
const cardDanger = css`
|
|
27
|
+
/* card-danger */
|
|
28
|
+
${cardPadded}
|
|
29
|
+
border-color: var(--status-rejected);
|
|
30
|
+
`;
|
|
31
|
+
const cardSection = css`
|
|
32
|
+
/* card-section */
|
|
33
|
+
padding: 1rem;
|
|
34
|
+
`;
|
|
35
|
+
const cardSectionDivider = css`
|
|
36
|
+
/* card-section-divider */
|
|
37
|
+
${cardSection}
|
|
38
|
+
border-bottom: 1px solid var(--border);
|
|
39
|
+
`;
|
|
40
|
+
const pageHeader = css`
|
|
41
|
+
/* page-header */
|
|
42
|
+
margin-bottom: 1.25rem;
|
|
43
|
+
`;
|
|
44
|
+
const pageHeaderRow = css`
|
|
45
|
+
/* page-header-row */
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 1rem;
|
|
48
|
+
align-items: flex-start;
|
|
49
|
+
justify-content: space-between;
|
|
50
|
+
flex-wrap: wrap;
|
|
51
|
+
`;
|
|
52
|
+
const pageHeaderTitle = css`
|
|
53
|
+
/* page-header-title */
|
|
54
|
+
margin: 0;
|
|
55
|
+
font-size: 1.45rem;
|
|
56
|
+
line-height: 1.2;
|
|
57
|
+
letter-spacing: -0.02em;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
`;
|
|
60
|
+
const pageHeaderDesc = css`
|
|
61
|
+
/* page-header-desc */
|
|
62
|
+
margin: 0.35rem 0 0;
|
|
63
|
+
color: var(--text-secondary);
|
|
64
|
+
font-size: 0.9rem;
|
|
65
|
+
max-width: 65ch;
|
|
66
|
+
/* Links inside running text must not rely on colour alone. */
|
|
67
|
+
& a {
|
|
68
|
+
text-decoration: underline;
|
|
69
|
+
text-underline-offset: 2px;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
const pageHeaderMeta = css`
|
|
73
|
+
/* page-header-meta */
|
|
74
|
+
margin: 0.4rem 0 0;
|
|
75
|
+
color: var(--text-secondary);
|
|
76
|
+
font-size: 0.82rem;
|
|
77
|
+
display: flex;
|
|
78
|
+
gap: 0.4rem;
|
|
79
|
+
align-items: center;
|
|
80
|
+
flex-wrap: wrap;
|
|
81
|
+
font-variant-numeric: tabular-nums;
|
|
82
|
+
`;
|
|
83
|
+
const pageHeaderActions = css`
|
|
84
|
+
/* page-header-actions */
|
|
85
|
+
display: flex;
|
|
86
|
+
gap: 0.5rem;
|
|
87
|
+
flex-wrap: wrap;
|
|
88
|
+
align-items: center;
|
|
89
|
+
`;
|
|
90
|
+
const sectionTitleH2 = css`
|
|
91
|
+
/* section-title */
|
|
92
|
+
margin: 0 0 0.5rem;
|
|
93
|
+
font-size: 1.05rem;
|
|
94
|
+
font-weight: 650;
|
|
95
|
+
letter-spacing: -0.01em;
|
|
96
|
+
`;
|
|
97
|
+
const sectionTitleH3 = css`
|
|
98
|
+
/* section-title-sm */
|
|
99
|
+
${sectionTitleH2}
|
|
100
|
+
font-size: 0.95rem;
|
|
101
|
+
`;
|
|
102
|
+
const sectionTitleDanger = css`
|
|
103
|
+
/* section-title-danger */
|
|
104
|
+
${sectionTitleH3}
|
|
105
|
+
color: var(--status-rejected);
|
|
106
|
+
`;
|
|
107
|
+
/** Card or section heading (h2 by default, h3 for subsections). */
|
|
108
|
+
/** Card container, padded by default, with an optional danger tone. */
|
|
109
|
+
const Card = ({ children, padded = true, tone = "neutral", fill = false, ...rest }) => {
|
|
110
|
+
if (fill) return /* @__PURE__ */ jsx("div", {
|
|
111
|
+
class: cardColumn,
|
|
112
|
+
...rest,
|
|
113
|
+
children
|
|
114
|
+
});
|
|
115
|
+
if (!padded) return /* @__PURE__ */ jsx("div", {
|
|
116
|
+
class: cardBase,
|
|
117
|
+
...rest,
|
|
118
|
+
children
|
|
119
|
+
});
|
|
120
|
+
return /* @__PURE__ */ jsx("div", {
|
|
121
|
+
class: tone === "danger" ? cardDanger : cardPadded,
|
|
122
|
+
...rest,
|
|
123
|
+
children
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
/** Padded card section, with an optional bottom divider. */
|
|
127
|
+
const CardSection = ({ divider = false, children }) => {
|
|
128
|
+
return /* @__PURE__ */ jsx("div", {
|
|
129
|
+
class: divider ? cardSectionDivider : cardSection,
|
|
130
|
+
children
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
/** Page header with title, description, meta line, and actions (breadcrumbs live in the top bar). */
|
|
134
|
+
const PageHeader = ({ title, description, meta, actions }) => {
|
|
135
|
+
return /* @__PURE__ */ jsx("div", {
|
|
136
|
+
class: pageHeader,
|
|
137
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
138
|
+
class: pageHeaderRow,
|
|
139
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [
|
|
140
|
+
/* @__PURE__ */ jsx("h1", {
|
|
141
|
+
class: pageHeaderTitle,
|
|
142
|
+
children: title
|
|
143
|
+
}),
|
|
144
|
+
description ? /* @__PURE__ */ jsx("p", {
|
|
145
|
+
class: pageHeaderDesc,
|
|
146
|
+
children: description
|
|
147
|
+
}) : null,
|
|
148
|
+
meta ? /* @__PURE__ */ jsx("p", {
|
|
149
|
+
class: pageHeaderMeta,
|
|
150
|
+
children: meta
|
|
151
|
+
}) : null
|
|
152
|
+
] }), actions ? /* @__PURE__ */ jsx("div", {
|
|
153
|
+
class: pageHeaderActions,
|
|
154
|
+
children: actions
|
|
155
|
+
}) : null]
|
|
156
|
+
})
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
/** Card or section heading (h2 by default, h3 for subsections). */
|
|
160
|
+
const SectionTitle = ({ level = 2, tone = "neutral", children }) => {
|
|
161
|
+
if (level === 3) return /* @__PURE__ */ jsx("h3", {
|
|
162
|
+
class: tone === "danger" ? sectionTitleDanger : sectionTitleH3,
|
|
163
|
+
children
|
|
164
|
+
});
|
|
165
|
+
return /* @__PURE__ */ jsx("h2", {
|
|
166
|
+
class: sectionTitleH2,
|
|
167
|
+
children
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
//#endregion
|
|
171
|
+
export { Card, CardSection, PageHeader, SectionTitle };
|
|
172
|
+
|
|
173
|
+
//# sourceMappingURL=layout.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.mjs","names":[],"sources":["../../src/ui/layout.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst cardBase = css`\n /* card */\n background: var(--surface-card);\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow);\n`;\n\nconst cardColumn = css`\n /* card-column */\n ${cardBase}\n display: flex;\n flex-direction: column;\n height: 100%;\n`;\n\nconst cardPadded = css`\n /* card-padded */\n ${cardBase}\n padding: 1rem;\n`;\n\nconst cardDanger = css`\n /* card-danger */\n ${cardPadded}\n border-color: var(--status-rejected);\n`;\n\nconst cardSection = css`\n /* card-section */\n padding: 1rem;\n`;\n\nconst cardSectionDivider = css`\n /* card-section-divider */\n ${cardSection}\n border-bottom: 1px solid var(--border);\n`;\n\nconst pageHeader = css`\n /* page-header */\n margin-bottom: 1.25rem;\n`;\n\nconst pageHeaderRow = css`\n /* page-header-row */\n display: flex;\n gap: 1rem;\n align-items: flex-start;\n justify-content: space-between;\n flex-wrap: wrap;\n`;\n\nconst pageHeaderTitle = css`\n /* page-header-title */\n margin: 0;\n font-size: 1.45rem;\n line-height: 1.2;\n letter-spacing: -0.02em;\n font-weight: 700;\n`;\n\nconst pageHeaderDesc = css`\n /* page-header-desc */\n margin: 0.35rem 0 0;\n color: var(--text-secondary);\n font-size: 0.9rem;\n max-width: 65ch;\n /* Links inside running text must not rely on colour alone. */\n & a {\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n`;\n\nconst pageHeaderMeta = css`\n /* page-header-meta */\n margin: 0.4rem 0 0;\n color: var(--text-secondary);\n font-size: 0.82rem;\n display: flex;\n gap: 0.4rem;\n align-items: center;\n flex-wrap: wrap;\n font-variant-numeric: tabular-nums;\n`;\n\nconst pageHeaderActions = css`\n /* page-header-actions */\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n align-items: center;\n`;\n\nconst sectionTitleH2 = css`\n /* section-title */\n margin: 0 0 0.5rem;\n font-size: 1.05rem;\n font-weight: 650;\n letter-spacing: -0.01em;\n`;\n\nconst sectionTitleH3 = css`\n /* section-title-sm */\n ${sectionTitleH2}\n font-size: 0.95rem;\n`;\n\nconst sectionTitleDanger = css`\n /* section-title-danger */\n ${sectionTitleH3}\n color: var(--status-rejected);\n`;\n\n/** Card or section heading (h2 by default, h3 for subsections). */\n\n/** Card container, padded by default, with an optional danger tone. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Card: FC<{\n children?: unknown;\n padded?: boolean;\n tone?: \"neutral\" | \"danger\";\n /** Flex column filling its grid cell, so a footer can sit at the bottom of equal-height cards. */\n fill?: boolean;\n [key: string]: unknown;\n}> = ({ children, padded = true, tone = \"neutral\", fill = false, ...rest }) => {\n if (fill) {\n return (\n <div class={cardColumn} {...rest}>\n {children}\n </div>\n );\n }\n if (!padded) {\n return (\n <div class={cardBase} {...rest}>\n {children}\n </div>\n );\n }\n return (\n <div class={tone === \"danger\" ? cardDanger : cardPadded} {...rest}>\n {children}\n </div>\n );\n};\n\n/** Padded card section, with an optional bottom divider. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const CardSection: FC<{ divider?: boolean; children?: unknown }> = ({\n divider = false,\n children,\n}) => {\n return <div class={divider ? cardSectionDivider : cardSection}>{children}</div>;\n};\n\n/** Page header with title, description, meta line, and actions (breadcrumbs live in the top bar). */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const PageHeader: FC<{\n title: unknown;\n description?: unknown;\n meta?: unknown;\n actions?: unknown;\n}> = ({ title, description, meta, actions }) => {\n return (\n <div class={pageHeader}>\n <div class={pageHeaderRow}>\n <div>\n <h1 class={pageHeaderTitle}>{title}</h1>\n {description ? <p class={pageHeaderDesc}>{description}</p> : null}\n {meta ? <p class={pageHeaderMeta}>{meta}</p> : null}\n </div>\n {actions ? <div class={pageHeaderActions}>{actions}</div> : null}\n </div>\n </div>\n );\n};\n\n/** Card or section heading (h2 by default, h3 for subsections). */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const SectionTitle: FC<{\n level?: 2 | 3;\n tone?: \"neutral\" | \"danger\";\n children?: unknown;\n}> = ({ level = 2, tone = \"neutral\", children }) => {\n if (level === 3) {\n return <h3 class={tone === \"danger\" ? sectionTitleDanger : sectionTitleH3}>{children}</h3>;\n }\n return <h2 class={sectionTitleH2}>{children}</h2>;\n};\n"],"mappings":";;;;;;AAKA,MAAM,WAAW,GAAG;;;;;;;AAQpB,MAAM,aAAa,GAAG;;IAElB,SAAS;;;;;AAMb,MAAM,aAAa,GAAG;;IAElB,SAAS;;;AAIb,MAAM,aAAa,GAAG;;IAElB,WAAW;;;AAIf,MAAM,cAAc,GAAG;;;;AAKvB,MAAM,qBAAqB,GAAG;;IAE1B,YAAY;;;AAIhB,MAAM,aAAa,GAAG;;;;AAKtB,MAAM,gBAAgB,GAAG;;;;;;;;AASzB,MAAM,kBAAkB,GAAG;;;;;;;;AAS3B,MAAM,iBAAiB,GAAG;;;;;;;;;;;;AAa1B,MAAM,iBAAiB,GAAG;;;;;;;;;;;AAY1B,MAAM,oBAAoB,GAAG;;;;;;;AAQ7B,MAAM,iBAAiB,GAAG;;;;;;;AAQ1B,MAAM,iBAAiB,GAAG;;IAEtB,eAAe;;;AAInB,MAAM,qBAAqB,GAAG;;IAE1B,eAAe;;;;;AAQnB,MAAa,QAOP,EAAE,UAAU,SAAS,MAAM,OAAO,WAAW,OAAO,OAAO,GAAG,WAAW;CAC7E,IAAI,MACF,OACE,oBAAC,OAAD;EAAK,OAAO;EAAY,GAAI;EACzB;CACE,CAAA;CAGT,IAAI,CAAC,QACH,OACE,oBAAC,OAAD;EAAK,OAAO;EAAU,GAAI;EACvB;CACE,CAAA;CAGT,OACE,oBAAC,OAAD;EAAK,OAAO,SAAS,WAAW,aAAa;EAAY,GAAI;EAC1D;CACE,CAAA;AAET;;AAIA,MAAa,eAA8D,EACzE,UAAU,OACV,eACI;CACJ,OAAO,oBAAC,OAAD;EAAK,OAAO,UAAU,qBAAqB;EAAc;CAAc,CAAA;AAChF;;AAIA,MAAa,cAKP,EAAE,OAAO,aAAa,MAAM,cAAc;CAC9C,OACE,oBAAC,OAAD;EAAK,OAAO;EACV,UAAA,qBAAC,OAAD;GAAK,OAAO;GAAZ,UAAA,CACE,qBAAC,OAAD,EAAA,UAAA;IACE,oBAAC,MAAD;KAAI,OAAO;KAAkB,UAAA;IAAU,CAAA;IACtC,cAAc,oBAAC,KAAD;KAAG,OAAO;KAAiB,UAAA;IAAe,CAAA,IAAI;IAC5D,OAAO,oBAAC,KAAD;KAAG,OAAO;KAAiB,UAAA;IAAQ,CAAA,IAAI;GAC5C,EAAA,CAAA,GACJ,UAAU,oBAAC,OAAD;IAAK,OAAO;IAAoB,UAAA;GAAa,CAAA,IAAI,IACzD;;CACF,CAAA;AAET;;AAIA,MAAa,gBAIP,EAAE,QAAQ,GAAG,OAAO,WAAW,eAAe;CAClD,IAAI,UAAU,GACZ,OAAO,oBAAC,MAAD;EAAI,OAAO,SAAS,WAAW,qBAAqB;EAAiB;CAAa,CAAA;CAE3F,OAAO,oBAAC,MAAD;EAAI,OAAO;EAAiB;CAAa,CAAA;AAClD"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { css } from "./css.mjs";
|
|
5
|
+
import { jsx } from "hono/jsx/jsx-runtime";
|
|
6
|
+
//#region src/ui/progress.tsx
|
|
7
|
+
const track = css`
|
|
8
|
+
/* progress */
|
|
9
|
+
height: 6px;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
background: var(--surface-muted);
|
|
12
|
+
border-radius: var(--radius-pill);
|
|
13
|
+
& > span {
|
|
14
|
+
display: block;
|
|
15
|
+
height: 100%;
|
|
16
|
+
background: var(--accent);
|
|
17
|
+
border-radius: inherit;
|
|
18
|
+
transition: width var(--dur-base) var(--ease);
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
const trackDone = css`
|
|
22
|
+
/* progress-done */
|
|
23
|
+
${track}
|
|
24
|
+
& > span {
|
|
25
|
+
background: var(--status-approved);
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
/** Determinate progress bar; turns green when `value` reaches `max`. */
|
|
29
|
+
const Progress = ({ value, max, label }) => {
|
|
30
|
+
const percent = max > 0 ? Math.min(100, Math.round(value / max * 100)) : 0;
|
|
31
|
+
return /* @__PURE__ */ jsx("div", {
|
|
32
|
+
class: max > 0 && value >= max ? trackDone : track,
|
|
33
|
+
role: "progressbar",
|
|
34
|
+
"aria-label": label,
|
|
35
|
+
"aria-valuemin": 0,
|
|
36
|
+
"aria-valuemax": max,
|
|
37
|
+
"aria-valuenow": Math.min(value, max),
|
|
38
|
+
children: /* @__PURE__ */ jsx("span", { style: `width:${percent}%` })
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { Progress };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=progress.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.mjs","names":[],"sources":["../../src/ui/progress.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst track = css`\n /* progress */\n height: 6px;\n overflow: hidden;\n background: var(--surface-muted);\n border-radius: var(--radius-pill);\n & > span {\n display: block;\n height: 100%;\n background: var(--accent);\n border-radius: inherit;\n transition: width var(--dur-base) var(--ease);\n }\n`;\n\nconst trackDone = css`\n /* progress-done */\n ${track}\n & > span {\n background: var(--status-approved);\n }\n`;\n\n/** Determinate progress bar; turns green when `value` reaches `max`. */\nexport const Progress: FC<{ value: number; max: number; label: string }> = ({\n value,\n max,\n label,\n}) => {\n const percent = max > 0 ? Math.min(100, Math.round((value / max) * 100)) : 0;\n return (\n <div\n class={max > 0 && value >= max ? trackDone : track}\n role=\"progressbar\"\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-valuenow={Math.min(value, max)}\n >\n <span style={`width:${percent}%`} />\n </div>\n );\n};\n"],"mappings":";;;;;;AAKA,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;AAejB,MAAM,YAAY,GAAG;;IAEjB,MAAM;;;;;;AAOV,MAAa,YAA+D,EAC1E,OACA,KACA,YACI;CACJ,MAAM,UAAU,MAAM,IAAI,KAAK,IAAI,KAAK,KAAK,MAAO,QAAQ,MAAO,GAAG,CAAC,IAAI;CAC3E,OACE,oBAAC,OAAD;EACE,OAAO,MAAM,KAAK,SAAS,MAAM,YAAY;EAC7C,MAAK;EACL,cAAY;EACZ,iBAAe;EACf,iBAAe;EACf,iBAAe,KAAK,IAAI,OAAO,GAAG;EAElC,UAAA,oBAAC,QAAD,EAAM,OAAO,SAAS,QAAQ,GAAK,CAAA;CAChC,CAAA;AAET"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { css } from "./css.mjs";
|
|
5
|
+
import { jsx } from "hono/jsx/jsx-runtime";
|
|
6
|
+
//#region src/ui/segmented.tsx
|
|
7
|
+
const segmentedRoot = css`
|
|
8
|
+
/* segmented */
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
padding: 2px;
|
|
11
|
+
gap: 2px;
|
|
12
|
+
background: var(--surface-muted);
|
|
13
|
+
border: 1px solid var(--border);
|
|
14
|
+
border-radius: var(--radius);
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
overflow-x: auto;
|
|
17
|
+
`;
|
|
18
|
+
const segmentedItem = css`
|
|
19
|
+
/* segmented-item */
|
|
20
|
+
appearance: none;
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 0.35rem;
|
|
24
|
+
min-height: 34px;
|
|
25
|
+
padding: 0.2rem 0.7rem;
|
|
26
|
+
border: 0;
|
|
27
|
+
border-radius: var(--radius-sm);
|
|
28
|
+
background: transparent;
|
|
29
|
+
color: var(--text-secondary);
|
|
30
|
+
font: inherit;
|
|
31
|
+
font-size: var(--text-sm);
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
text-decoration: none;
|
|
36
|
+
transition:
|
|
37
|
+
background var(--dur-fast) var(--ease),
|
|
38
|
+
color var(--dur-fast) var(--ease);
|
|
39
|
+
&:hover {
|
|
40
|
+
color: var(--text-primary);
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
}
|
|
43
|
+
&[aria-pressed="true"],
|
|
44
|
+
&[aria-current="page"] {
|
|
45
|
+
background: var(--surface-card);
|
|
46
|
+
color: var(--text-primary);
|
|
47
|
+
box-shadow: var(--shadow);
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
/**
|
|
51
|
+
* Compact single-choice control. Items with `href` navigate (filters, tabs);
|
|
52
|
+
* items without toggle in place and carry `data-view-value` for client code.
|
|
53
|
+
*/
|
|
54
|
+
const Segmented = ({ items, label, ...rest }) => {
|
|
55
|
+
return /* @__PURE__ */ jsx("div", {
|
|
56
|
+
class: segmentedRoot,
|
|
57
|
+
role: "group",
|
|
58
|
+
"aria-label": label,
|
|
59
|
+
...rest,
|
|
60
|
+
children: items.map((item) => item.href ? /* @__PURE__ */ jsx("a", {
|
|
61
|
+
class: segmentedItem,
|
|
62
|
+
href: item.href,
|
|
63
|
+
"aria-current": item.active ? "page" : void 0,
|
|
64
|
+
children: item.label
|
|
65
|
+
}, item.value) : /* @__PURE__ */ jsx("button", {
|
|
66
|
+
class: segmentedItem,
|
|
67
|
+
type: "button",
|
|
68
|
+
"data-view-value": item.value,
|
|
69
|
+
"aria-pressed": item.active ? "true" : "false",
|
|
70
|
+
children: item.label
|
|
71
|
+
}, item.value))
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { Segmented };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=segmented.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segmented.mjs","names":[],"sources":["../../src/ui/segmented.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst segmentedRoot = css`\n /* segmented */\n display: inline-flex;\n padding: 2px;\n gap: 2px;\n background: var(--surface-muted);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n max-width: 100%;\n overflow-x: auto;\n`;\n\nconst segmentedItem = css`\n /* segmented-item */\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n min-height: 34px;\n padding: 0.2rem 0.7rem;\n border: 0;\n border-radius: var(--radius-sm);\n background: transparent;\n color: var(--text-secondary);\n font: inherit;\n font-size: var(--text-sm);\n font-weight: 600;\n white-space: nowrap;\n cursor: pointer;\n text-decoration: none;\n transition:\n background var(--dur-fast) var(--ease),\n color var(--dur-fast) var(--ease);\n &:hover {\n color: var(--text-primary);\n text-decoration: none;\n }\n &[aria-pressed=\"true\"],\n &[aria-current=\"page\"] {\n background: var(--surface-card);\n color: var(--text-primary);\n box-shadow: var(--shadow);\n }\n`;\n\n/** One option in a `Segmented` control. */\nexport interface SegmentedItem {\n label: string;\n value: string;\n /** Renders a link (navigation) instead of a toggle button. */\n href?: string;\n active?: boolean;\n}\n\n/**\n * Compact single-choice control. Items with `href` navigate (filters, tabs);\n * items without toggle in place and carry `data-view-value` for client code.\n */\nexport const Segmented: FC<{\n items: SegmentedItem[];\n label: string;\n [key: string]: unknown;\n}> = ({ items, label, ...rest }) => {\n return (\n <div class={segmentedRoot} role=\"group\" aria-label={label} {...rest}>\n {items.map((item) =>\n item.href ? (\n <a\n key={item.value}\n class={segmentedItem}\n href={item.href}\n aria-current={item.active ? \"page\" : undefined}\n >\n {item.label}\n </a>\n ) : (\n <button\n key={item.value}\n class={segmentedItem}\n type=\"button\"\n data-view-value={item.value}\n aria-pressed={item.active ? \"true\" : \"false\"}\n >\n {item.label}\n </button>\n ),\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAKA,MAAM,gBAAgB,GAAG;;;;;;;;;;;AAYzB,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CzB,MAAa,aAIP,EAAE,OAAO,OAAO,GAAG,WAAW;CAClC,OACE,oBAAC,OAAD;EAAK,OAAO;EAAe,MAAK;EAAQ,cAAY;EAAO,GAAI;EAC5D,UAAA,MAAM,KAAK,SACV,KAAK,OACH,oBAAC,KAAD;GAEE,OAAO;GACP,MAAM,KAAK;GACX,gBAAc,KAAK,SAAS,SAAS,KAAA;GAEpC,UAAA,KAAK;EACL,GANI,KAAK,KAMT,IAEH,oBAAC,UAAD;GAEE,OAAO;GACP,MAAK;GACL,mBAAiB,KAAK;GACtB,gBAAc,KAAK,SAAS,SAAS;GAEpC,UAAA,KAAK;EACA,GAPD,KAAK,KAOJ,CAEZ;CACG,CAAA;AAET"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../../store.mjs";
|
|
5
|
+
import { css } from "../css.mjs";
|
|
6
|
+
import { Icon } from "../icons/icon.mjs";
|
|
7
|
+
import { Button } from "../buttons.mjs";
|
|
8
|
+
import { Avatar } from "../avatar.mjs";
|
|
9
|
+
import { Dropdown, DropdownDivider, DropdownItem } from "../dropdown.mjs";
|
|
10
|
+
import { csrfField } from "../csrf-field.mjs";
|
|
11
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
|
+
import { safeImageUrl } from "@storyshelf/core/urls";
|
|
13
|
+
//#region src/ui/shell/account.tsx
|
|
14
|
+
/** Styles for the account widgets that live in the sidebar footer. */
|
|
15
|
+
const accountRoot = css`
|
|
16
|
+
/* account */
|
|
17
|
+
display: grid;
|
|
18
|
+
gap: var(--space-1);
|
|
19
|
+
.account__who {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: var(--space-2);
|
|
23
|
+
min-width: 0;
|
|
24
|
+
}
|
|
25
|
+
.account__text {
|
|
26
|
+
display: grid;
|
|
27
|
+
min-width: 0;
|
|
28
|
+
text-align: left;
|
|
29
|
+
line-height: 1.2;
|
|
30
|
+
}
|
|
31
|
+
.account__name {
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
font-size: var(--text-base);
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
text-overflow: ellipsis;
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
}
|
|
38
|
+
.account__role {
|
|
39
|
+
color: var(--text-secondary);
|
|
40
|
+
font-size: var(--text-xs);
|
|
41
|
+
text-transform: capitalize;
|
|
42
|
+
}
|
|
43
|
+
.account__meta {
|
|
44
|
+
padding: 0.4rem 0.6rem 0.5rem;
|
|
45
|
+
color: var(--text-secondary);
|
|
46
|
+
font-size: var(--text-sm);
|
|
47
|
+
line-height: 1.3;
|
|
48
|
+
}
|
|
49
|
+
.account__meta strong {
|
|
50
|
+
display: block;
|
|
51
|
+
color: var(--text-primary);
|
|
52
|
+
font-size: var(--text-base);
|
|
53
|
+
}
|
|
54
|
+
.account__logout {
|
|
55
|
+
margin: 0;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
/** Light / Dark / System menu; the trigger shows the active mode's icon. */
|
|
59
|
+
const ThemeMenu = () => {
|
|
60
|
+
return /* @__PURE__ */ jsxs(Dropdown, {
|
|
61
|
+
iconOnly: true,
|
|
62
|
+
placement: "above",
|
|
63
|
+
ariaLabel: "Theme",
|
|
64
|
+
label: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
65
|
+
/* @__PURE__ */ jsx("span", {
|
|
66
|
+
"data-theme-icon": "light",
|
|
67
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "sun" })
|
|
68
|
+
}),
|
|
69
|
+
/* @__PURE__ */ jsx("span", {
|
|
70
|
+
"data-theme-icon": "dark",
|
|
71
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "moon" })
|
|
72
|
+
}),
|
|
73
|
+
/* @__PURE__ */ jsx("span", {
|
|
74
|
+
"data-theme-icon": "system",
|
|
75
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "monitor" })
|
|
76
|
+
})
|
|
77
|
+
] }),
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ jsx(DropdownItem, {
|
|
80
|
+
icon: "sun",
|
|
81
|
+
"data-theme-set": "light",
|
|
82
|
+
children: "Light"
|
|
83
|
+
}),
|
|
84
|
+
/* @__PURE__ */ jsx(DropdownItem, {
|
|
85
|
+
icon: "moon",
|
|
86
|
+
"data-theme-set": "dark",
|
|
87
|
+
children: "Dark"
|
|
88
|
+
}),
|
|
89
|
+
/* @__PURE__ */ jsx(DropdownItem, {
|
|
90
|
+
icon: "monitor",
|
|
91
|
+
"data-theme-set": "system",
|
|
92
|
+
children: "System"
|
|
93
|
+
})
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
/** Signed-in user row (name, role, profile, sign out); a sign-in button when signed out. */
|
|
98
|
+
const UserMenu = () => {
|
|
99
|
+
const { user, authEnabled } = getStore();
|
|
100
|
+
if (!authEnabled) return null;
|
|
101
|
+
if (!user) return /* @__PURE__ */ jsx(Button, {
|
|
102
|
+
href: "/auth/login",
|
|
103
|
+
size: "sm",
|
|
104
|
+
icon: "user",
|
|
105
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
106
|
+
class: "sidebar__label",
|
|
107
|
+
children: "Sign in"
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
return /* @__PURE__ */ jsx("div", {
|
|
111
|
+
class: accountRoot,
|
|
112
|
+
children: /* @__PURE__ */ jsxs(Dropdown, {
|
|
113
|
+
block: true,
|
|
114
|
+
placement: "above",
|
|
115
|
+
ariaLabel: `Account: ${user.name}`,
|
|
116
|
+
label: /* @__PURE__ */ jsxs("span", {
|
|
117
|
+
class: "account__who",
|
|
118
|
+
children: [/* @__PURE__ */ jsx(Avatar, {
|
|
119
|
+
name: user.name,
|
|
120
|
+
src: safeImageUrl(user.avatarUrl),
|
|
121
|
+
size: "sm"
|
|
122
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
123
|
+
class: "account__text sidebar__label",
|
|
124
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
125
|
+
class: "account__name",
|
|
126
|
+
children: user.name
|
|
127
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
128
|
+
class: "account__role",
|
|
129
|
+
children: user.role
|
|
130
|
+
})]
|
|
131
|
+
})]
|
|
132
|
+
}),
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsxs("div", {
|
|
135
|
+
class: "account__meta",
|
|
136
|
+
children: [/* @__PURE__ */ jsx("strong", { children: user.name }), user.role]
|
|
137
|
+
}),
|
|
138
|
+
/* @__PURE__ */ jsx(DropdownDivider, {}),
|
|
139
|
+
/* @__PURE__ */ jsx(DropdownItem, {
|
|
140
|
+
href: "/profile",
|
|
141
|
+
icon: "user",
|
|
142
|
+
children: "Profile"
|
|
143
|
+
}),
|
|
144
|
+
/* @__PURE__ */ jsxs("form", {
|
|
145
|
+
class: "account__logout",
|
|
146
|
+
method: "post",
|
|
147
|
+
action: "/auth/logout",
|
|
148
|
+
children: [csrfField(), /* @__PURE__ */ jsx(DropdownItem, {
|
|
149
|
+
type: "submit",
|
|
150
|
+
icon: "log-out",
|
|
151
|
+
children: "Sign out"
|
|
152
|
+
})]
|
|
153
|
+
})
|
|
154
|
+
]
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
export { ThemeMenu, UserMenu };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=account.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.mjs","names":[],"sources":["../../../src/ui/shell/account.tsx"],"sourcesContent":["import { safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { getStore } from \"../../store.ts\";\nimport { Avatar } from \"../avatar.tsx\";\nimport { Button } from \"../buttons.tsx\";\nimport { csrfField } from \"../csrf-field.tsx\";\nimport { css } from \"../css.ts\";\nimport { Dropdown, DropdownDivider, DropdownItem } from \"../dropdown.tsx\";\nimport { Icon } from \"../icons/icon.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Styles for the account widgets that live in the sidebar footer. */\nconst accountRoot = css`\n /* account */\n display: grid;\n gap: var(--space-1);\n .account__who {\n display: flex;\n align-items: center;\n gap: var(--space-2);\n min-width: 0;\n }\n .account__text {\n display: grid;\n min-width: 0;\n text-align: left;\n line-height: 1.2;\n }\n .account__name {\n overflow: hidden;\n font-size: var(--text-base);\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .account__role {\n color: var(--text-secondary);\n font-size: var(--text-xs);\n text-transform: capitalize;\n }\n .account__meta {\n padding: 0.4rem 0.6rem 0.5rem;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n line-height: 1.3;\n }\n .account__meta strong {\n display: block;\n color: var(--text-primary);\n font-size: var(--text-base);\n }\n .account__logout {\n margin: 0;\n }\n`;\n\n/** Light / Dark / System menu; the trigger shows the active mode's icon. */\nexport const ThemeMenu: FC = () => {\n return (\n <Dropdown\n iconOnly\n placement=\"above\"\n ariaLabel=\"Theme\"\n label={\n <>\n <span data-theme-icon=\"light\">\n <Icon name=\"sun\" />\n </span>\n <span data-theme-icon=\"dark\">\n <Icon name=\"moon\" />\n </span>\n <span data-theme-icon=\"system\">\n <Icon name=\"monitor\" />\n </span>\n </>\n }\n >\n <DropdownItem icon=\"sun\" data-theme-set=\"light\">\n Light\n </DropdownItem>\n <DropdownItem icon=\"moon\" data-theme-set=\"dark\">\n Dark\n </DropdownItem>\n <DropdownItem icon=\"monitor\" data-theme-set=\"system\">\n System\n </DropdownItem>\n </Dropdown>\n );\n};\n\n/** Signed-in user row (name, role, profile, sign out); a sign-in button when signed out. */\nexport const UserMenu: FC = () => {\n const { user, authEnabled } = getStore();\n if (!authEnabled) {\n return null;\n }\n if (!user) {\n return (\n <Button href=\"/auth/login\" size=\"sm\" icon=\"user\">\n <span class=\"sidebar__label\">Sign in</span>\n </Button>\n );\n }\n return (\n <div class={accountRoot}>\n <Dropdown\n block\n placement=\"above\"\n ariaLabel={`Account: ${user.name}`}\n label={\n <span class=\"account__who\">\n <Avatar name={user.name} src={safeImageUrl(user.avatarUrl)} size=\"sm\" />\n <span class=\"account__text sidebar__label\">\n <span class=\"account__name\">{user.name}</span>\n <span class=\"account__role\">{user.role}</span>\n </span>\n </span>\n }\n >\n <div class=\"account__meta\">\n <strong>{user.name}</strong>\n {user.role}\n </div>\n <DropdownDivider />\n <DropdownItem href=\"/profile\" icon=\"user\">\n Profile\n </DropdownItem>\n <form class=\"account__logout\" method=\"post\" action=\"/auth/logout\">\n {csrfField()}\n <DropdownItem type=\"submit\" icon=\"log-out\">\n Sign out\n </DropdownItem>\n </form>\n </Dropdown>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CvB,MAAa,kBAAsB;CACjC,OACE,qBAAC,UAAD;EACE,UAAA;EACA,WAAU;EACV,WAAU;EACV,OACE,qBAAA,UAAA,EAAA,UAAA;GACE,oBAAC,QAAD;IAAM,mBAAgB;IACpB,UAAA,oBAAC,MAAD,EAAM,MAAK,MAAO,CAAA;GACd,CAAA;GACN,oBAAC,QAAD;IAAM,mBAAgB;IACpB,UAAA,oBAAC,MAAD,EAAM,MAAK,OAAQ,CAAA;GACf,CAAA;GACN,oBAAC,QAAD;IAAM,mBAAgB;IACpB,UAAA,oBAAC,MAAD,EAAM,MAAK,UAAW,CAAA;GAClB,CAAA;EACN,EAAA,CAAA;EAfN,UAAA;GAkBE,oBAAC,cAAD;IAAc,MAAK;IAAM,kBAAe;IAAQ,UAAA;GAElC,CAAA;GACd,oBAAC,cAAD;IAAc,MAAK;IAAO,kBAAe;IAAO,UAAA;GAElC,CAAA;GACd,oBAAC,cAAD;IAAc,MAAK;IAAU,kBAAe;IAAS,UAAA;GAEvC,CAAA;EACN;;AAEd;;AAGA,MAAa,iBAAqB;CAChC,MAAM,EAAE,MAAM,gBAAgB,SAAS;CACvC,IAAI,CAAC,aACH,OAAO;CAET,IAAI,CAAC,MACH,OACE,oBAAC,QAAD;EAAQ,MAAK;EAAc,MAAK;EAAK,MAAK;EACxC,UAAA,oBAAC,QAAD;GAAM,OAAM;GAAiB,UAAA;EAAa,CAAA;CACpC,CAAA;CAGZ,OACE,oBAAC,OAAD;EAAK,OAAO;EACV,UAAA,qBAAC,UAAD;GACE,OAAA;GACA,WAAU;GACV,WAAW,YAAY,KAAK;GAC5B,OACE,qBAAC,QAAD;IAAM,OAAM;IAAZ,UAAA,CACE,oBAAC,QAAD;KAAQ,MAAM,KAAK;KAAM,KAAK,aAAa,KAAK,SAAS;KAAG,MAAK;IAAM,CAAA,GACvE,qBAAC,QAAD;KAAM,OAAM;KAAZ,UAAA,CACE,oBAAC,QAAD;MAAM,OAAM;MAAiB,UAAA,KAAK;KAAW,CAAA,GAC7C,oBAAC,QAAD;MAAM,OAAM;MAAiB,UAAA,KAAK;KAAW,CAAA,CACzC;IACF,CAAA,CAAA;;GAXV,UAAA;IAcE,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,UAAD,EAAA,UAAS,KAAK,KAAa,CAAA,GAC1B,KAAK,IACH;;IACL,oBAAC,iBAAD,CAAkB,CAAA;IAClB,oBAAC,cAAD;KAAc,MAAK;KAAW,MAAK;KAAO,UAAA;IAE5B,CAAA;IACd,qBAAC,QAAD;KAAM,OAAM;KAAkB,QAAO;KAAO,QAAO;KAAnD,UAAA,CACG,UAAU,GACX,oBAAC,cAAD;MAAc,MAAK;MAAS,MAAK;MAAU,UAAA;KAE7B,CAAA,CACV;;GACE;;CACP,CAAA;AAET"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { css } from "../css.mjs";
|
|
5
|
+
import { Icon } from "../icons/icon.mjs";
|
|
6
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
7
|
+
import { safeImageUrl } from "@storyshelf/core/urls";
|
|
8
|
+
//#region src/ui/shell/bare.tsx
|
|
9
|
+
/** Chrome-free page for signed-out flows: brand over a centered column. */
|
|
10
|
+
const bareRoot = css`
|
|
11
|
+
/* bare */
|
|
12
|
+
display: grid;
|
|
13
|
+
place-content: center;
|
|
14
|
+
justify-items: center;
|
|
15
|
+
gap: var(--space-5);
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
padding: var(--space-6) var(--space-4);
|
|
18
|
+
background: radial-gradient(60rem 30rem at 50% -10%, var(--accent-subtle), transparent 70%);
|
|
19
|
+
.bare__brand {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 0.6rem;
|
|
23
|
+
color: var(--text-primary);
|
|
24
|
+
font-size: var(--text-xl);
|
|
25
|
+
font-weight: 700;
|
|
26
|
+
letter-spacing: -0.02em;
|
|
27
|
+
}
|
|
28
|
+
.bare__mark {
|
|
29
|
+
display: inline-grid;
|
|
30
|
+
place-items: center;
|
|
31
|
+
width: 34px;
|
|
32
|
+
height: 34px;
|
|
33
|
+
border-radius: var(--radius);
|
|
34
|
+
background: var(--accent);
|
|
35
|
+
color: var(--accent-contrast);
|
|
36
|
+
}
|
|
37
|
+
.bare__body {
|
|
38
|
+
width: min(400px, 100%);
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
/** Brand header plus a narrow column, used instead of the app shell. */
|
|
42
|
+
const BareShell = ({ name, logo, children }) => {
|
|
43
|
+
const safeLogo = safeImageUrl(logo);
|
|
44
|
+
return /* @__PURE__ */ jsxs("main", {
|
|
45
|
+
id: "main-content",
|
|
46
|
+
class: bareRoot,
|
|
47
|
+
tabindex: -1,
|
|
48
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
49
|
+
class: "bare__brand",
|
|
50
|
+
children: [safeLogo ? /* @__PURE__ */ jsx("img", {
|
|
51
|
+
src: safeLogo,
|
|
52
|
+
alt: "",
|
|
53
|
+
width: "34",
|
|
54
|
+
height: "34"
|
|
55
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
56
|
+
class: "bare__mark",
|
|
57
|
+
"aria-hidden": "true",
|
|
58
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "layers" })
|
|
59
|
+
}), /* @__PURE__ */ jsx("span", { children: name })]
|
|
60
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
61
|
+
class: "bare__body",
|
|
62
|
+
children
|
|
63
|
+
})]
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
export { BareShell };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=bare.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bare.mjs","names":[],"sources":["../../../src/ui/shell/bare.tsx"],"sourcesContent":["import { safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { css } from \"../css.ts\";\nimport { Icon } from \"../icons/icon.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Chrome-free page for signed-out flows: brand over a centered column. */\nconst bareRoot = css`\n /* bare */\n display: grid;\n place-content: center;\n justify-items: center;\n gap: var(--space-5);\n min-height: 100vh;\n padding: var(--space-6) var(--space-4);\n background: radial-gradient(60rem 30rem at 50% -10%, var(--accent-subtle), transparent 70%);\n .bare__brand {\n display: inline-flex;\n align-items: center;\n gap: 0.6rem;\n color: var(--text-primary);\n font-size: var(--text-xl);\n font-weight: 700;\n letter-spacing: -0.02em;\n }\n .bare__mark {\n display: inline-grid;\n place-items: center;\n width: 34px;\n height: 34px;\n border-radius: var(--radius);\n background: var(--accent);\n color: var(--accent-contrast);\n }\n .bare__body {\n width: min(400px, 100%);\n }\n`;\n\n/** Brand header plus a narrow column, used instead of the app shell. */\nexport const BareShell: FC<{ name: string; logo?: string; children?: unknown }> = ({\n name,\n logo,\n children,\n}) => {\n const safeLogo = safeImageUrl(logo);\n return (\n <main id=\"main-content\" class={bareRoot} tabindex={-1}>\n <div class=\"bare__brand\">\n {safeLogo ? (\n <img src={safeLogo} alt=\"\" width=\"34\" height=\"34\" />\n ) : (\n <span class=\"bare__mark\" aria-hidden=\"true\">\n <Icon name=\"layers\" />\n </span>\n )}\n <span>{name}</span>\n </div>\n <div class=\"bare__body\">{children}</div>\n </main>\n );\n};\n"],"mappings":";;;;;;;;;AAQA,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCpB,MAAa,aAAsE,EACjF,MACA,MACA,eACI;CACJ,MAAM,WAAW,aAAa,IAAI;CAClC,OACE,qBAAC,QAAD;EAAM,IAAG;EAAe,OAAO;EAAU,UAAU;EAAnD,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA,CACG,WACC,oBAAC,OAAD;IAAK,KAAK;IAAU,KAAI;IAAG,OAAM;IAAK,QAAO;GAAM,CAAA,IAEnD,oBAAC,QAAD;IAAM,OAAM;IAAa,eAAY;IACnC,UAAA,oBAAC,MAAD,EAAM,MAAK,SAAU,CAAA;GACjB,CAAA,GAER,oBAAC,QAAD,EAAA,UAAO,KAAW,CAAA,CACf;EACL,CAAA,GAAA,oBAAC,OAAD;GAAK,OAAM;GAAc;EAAc,CAAA,CACnC;;AAEV"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/ui/shell/boot-script.ts
|
|
5
|
+
/**
|
|
6
|
+
* Runs in `<head>` before first paint so the saved theme and sidebar state
|
|
7
|
+
* apply without a flash. Exposes cookie helpers to the client script.
|
|
8
|
+
*/
|
|
9
|
+
function bootScript() {
|
|
10
|
+
return `
|
|
11
|
+
(function(){
|
|
12
|
+
var themeCookie='storyshelf_theme';
|
|
13
|
+
var sidebarCookie='storyshelf_sidebar';
|
|
14
|
+
function readCookie(name){
|
|
15
|
+
var parts=document.cookie.split('; ');
|
|
16
|
+
for(var i=0;i<parts.length;i++){
|
|
17
|
+
var at=parts[i].indexOf('=');
|
|
18
|
+
if(at>0&&parts[i].slice(0,at)===name){ return decodeURIComponent(parts[i].slice(at+1)); }
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function writeCookie(name,value){
|
|
23
|
+
document.cookie=name+'='+encodeURIComponent(value)+'; Path=/; Max-Age=31536000; SameSite=Lax';
|
|
24
|
+
}
|
|
25
|
+
function applyTheme(value){
|
|
26
|
+
var ok=value==='light'||value==='dark'||value==='system';
|
|
27
|
+
document.documentElement.setAttribute('data-theme',ok?value:'system');
|
|
28
|
+
}
|
|
29
|
+
function applySidebar(value){
|
|
30
|
+
if(value==='rail'){ document.documentElement.setAttribute('data-sidebar','rail'); }
|
|
31
|
+
else { document.documentElement.removeAttribute('data-sidebar'); }
|
|
32
|
+
}
|
|
33
|
+
var theme=readCookie(themeCookie);
|
|
34
|
+
if(theme){ applyTheme(theme); }
|
|
35
|
+
applySidebar(readCookie(sidebarCookie));
|
|
36
|
+
window.__storyshelfShell={
|
|
37
|
+
readCookie:readCookie,writeCookie:writeCookie,applyTheme:applyTheme,applySidebar:applySidebar,
|
|
38
|
+
themeCookie:themeCookie,sidebarCookie:sidebarCookie
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
`.trim();
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { bootScript };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=boot-script.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boot-script.mjs","names":[],"sources":["../../../src/ui/shell/boot-script.ts"],"sourcesContent":["/**\n * Runs in `<head>` before first paint so the saved theme and sidebar state\n * apply without a flash. Exposes cookie helpers to the client script.\n */\nexport function bootScript(): string {\n return `\n (function(){\n var themeCookie='storyshelf_theme';\n var sidebarCookie='storyshelf_sidebar';\n function readCookie(name){\n var parts=document.cookie.split('; ');\n for(var i=0;i<parts.length;i++){\n var at=parts[i].indexOf('=');\n if(at>0&&parts[i].slice(0,at)===name){ return decodeURIComponent(parts[i].slice(at+1)); }\n }\n return null;\n }\n function writeCookie(name,value){\n document.cookie=name+'='+encodeURIComponent(value)+'; Path=/; Max-Age=31536000; SameSite=Lax';\n }\n function applyTheme(value){\n var ok=value==='light'||value==='dark'||value==='system';\n document.documentElement.setAttribute('data-theme',ok?value:'system');\n }\n function applySidebar(value){\n if(value==='rail'){ document.documentElement.setAttribute('data-sidebar','rail'); }\n else { document.documentElement.removeAttribute('data-sidebar'); }\n }\n var theme=readCookie(themeCookie);\n if(theme){ applyTheme(theme); }\n applySidebar(readCookie(sidebarCookie));\n window.__storyshelfShell={\n readCookie:readCookie,writeCookie:writeCookie,applyTheme:applyTheme,applySidebar:applySidebar,\n themeCookie:themeCookie,sidebarCookie:sidebarCookie\n };\n })();\n `.trim();\n}\n"],"mappings":";;;;;;;;AAIA,SAAgB,aAAqB;CACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA+BL,KAAK;AACT"}
|