handzon-core 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/Navbar.astro +23 -2
- package/src/components/Sidebar.astro +16 -15
- package/src/components/auth/UserMenu.tsx +21 -0
- package/src/components/home/Hero.astro +20 -1
- package/src/layouts/BaseLayout.astro +17 -1
- package/src/layouts/TutorialLayout.astro +16 -0
- package/src/pages/Home.astro +13 -1
- package/src/pages/TutorialLanding.astro +14 -1
- package/src/pages/TutorialStep.astro +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handzon-core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Core framework for Handzon — layouts, components, content + AI libs, and server runtime (handlers, DB, auth, migration runner) consumed by Handzon scaffolds.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -12,15 +12,36 @@ import UserMenu from "./auth/UserMenu.astro";
|
|
|
12
12
|
interface Props {
|
|
13
13
|
logoUrl?: string;
|
|
14
14
|
siteName?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Intrinsic aspect ratio of the logo (in the image's native units —
|
|
17
|
+
* only the ratio matters). Forwarded to the <img> element's
|
|
18
|
+
* `width`/`height` attributes so the browser reserves the correct
|
|
19
|
+
* box before the SVG loads, preventing CLS in the navbar row.
|
|
20
|
+
* Defaults track the shipped `public/logo.svg`. Override both when
|
|
21
|
+
* the consumer swaps in a non-square logo.
|
|
22
|
+
*/
|
|
23
|
+
logoWidth?: number;
|
|
24
|
+
logoHeight?: number;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
logoUrl = "/logo.svg",
|
|
29
|
+
siteName = "Handzon",
|
|
30
|
+
logoWidth = 76,
|
|
31
|
+
logoHeight = 58,
|
|
32
|
+
} = Astro.props;
|
|
18
33
|
---
|
|
19
34
|
<header class="hz-nav">
|
|
20
35
|
<div class="hz-nav-inner">
|
|
21
36
|
{logoUrl && (
|
|
22
37
|
<a href="/" class="hz-nav-brand" aria-label={`${siteName} home`}>
|
|
23
|
-
<img
|
|
38
|
+
<img
|
|
39
|
+
src={logoUrl}
|
|
40
|
+
alt={siteName}
|
|
41
|
+
class="hz-nav-logo"
|
|
42
|
+
width={logoWidth}
|
|
43
|
+
height={logoHeight}
|
|
44
|
+
/>
|
|
24
45
|
</a>
|
|
25
46
|
)}
|
|
26
47
|
<div class="hz-nav-actions">
|
|
@@ -120,32 +120,33 @@ const slug = tutorial.id;
|
|
|
120
120
|
.sb-steps li + li {
|
|
121
121
|
border-top: var(--border-default) solid var(--color-border);
|
|
122
122
|
}
|
|
123
|
-
/*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
123
|
+
/* Single-row grid (check + name) with the duration pinned absolutely
|
|
124
|
+
* to the top-right corner. Pulling it out of the grid flow means the
|
|
125
|
+
* title row gets the full cell height — checkbox and title stay
|
|
126
|
+
* vertically centered together, and the duration doesn't crowd the
|
|
127
|
+
* top of long, wrapping titles the way a stacked grid track did.
|
|
128
|
+
* `padding-right` on `.sb-name` reserves space so wrapping titles
|
|
129
|
+
* never collide with the corner-stamped duration. */
|
|
128
130
|
.sb-steps a {
|
|
131
|
+
position: relative;
|
|
129
132
|
display: grid;
|
|
130
133
|
grid-template-columns: 18px 1fr;
|
|
131
|
-
grid-template-areas:
|
|
132
|
-
". dur"
|
|
133
|
-
"check name";
|
|
134
134
|
align-items: center;
|
|
135
135
|
column-gap: 0.6rem;
|
|
136
|
-
|
|
137
|
-
/* Heavier bottom padding counter-balances the duration row above
|
|
138
|
-
* so the title feels visually centered in the row. */
|
|
139
|
-
padding: 0.5rem 1rem 0.95rem;
|
|
136
|
+
padding: 1.05rem 1rem;
|
|
140
137
|
color: var(--color-fg);
|
|
141
138
|
text-decoration: none;
|
|
142
139
|
font-size: 0.92em;
|
|
143
140
|
font-weight: 500;
|
|
144
141
|
letter-spacing: -0.005em;
|
|
145
142
|
}
|
|
146
|
-
.sb-
|
|
147
|
-
.sb-
|
|
148
|
-
|
|
143
|
+
.sb-name { line-height: 1.3; padding-right: 3.25rem; }
|
|
144
|
+
.sb-dur {
|
|
145
|
+
position: absolute;
|
|
146
|
+
top: 0.55rem;
|
|
147
|
+
right: 1rem;
|
|
148
|
+
line-height: 1;
|
|
149
|
+
}
|
|
149
150
|
.sb-steps a:hover {
|
|
150
151
|
background: var(--color-surface);
|
|
151
152
|
color: var(--color-fg);
|
|
@@ -98,6 +98,27 @@ export default function UserMenu() {
|
|
|
98
98
|
<span className="um-name" title={fullLabel}>
|
|
99
99
|
{displayName}
|
|
100
100
|
</span>
|
|
101
|
+
<a
|
|
102
|
+
className="um-btn um-btn-icon"
|
|
103
|
+
href="/settings/tokens"
|
|
104
|
+
title="Access tokens for editor MCP"
|
|
105
|
+
>
|
|
106
|
+
<svg
|
|
107
|
+
viewBox="0 0 24 24"
|
|
108
|
+
width="14"
|
|
109
|
+
height="14"
|
|
110
|
+
fill="none"
|
|
111
|
+
stroke="currentColor"
|
|
112
|
+
strokeWidth="2"
|
|
113
|
+
strokeLinecap="round"
|
|
114
|
+
strokeLinejoin="round"
|
|
115
|
+
aria-hidden="true"
|
|
116
|
+
>
|
|
117
|
+
<circle cx="7.5" cy="15.5" r="4.5" />
|
|
118
|
+
<path d="M11 12L20 3l1.5 1.5L20 6l1.5 1.5L19 9l-1.5-1.5L16 9" />
|
|
119
|
+
</svg>
|
|
120
|
+
<span className="sr-only">Access tokens</span>
|
|
121
|
+
</a>
|
|
101
122
|
<form method="post" action="/api/auth/signout">
|
|
102
123
|
<input type="hidden" name="csrfToken" value={csrfToken} />
|
|
103
124
|
<input type="hidden" name="callbackUrl" value={callbackUrl} />
|
|
@@ -8,17 +8,36 @@ interface Props {
|
|
|
8
8
|
* to hide the logo entirely.
|
|
9
9
|
*/
|
|
10
10
|
logoUrl?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Intrinsic aspect ratio of the logo, supplied as the SVG/image's
|
|
13
|
+
* native width and height (any unit — only the ratio matters). The
|
|
14
|
+
* browser uses these to reserve the correct box size before the
|
|
15
|
+
* image loads, preventing CLS as surrounding text reflows. Defaults
|
|
16
|
+
* track the shipped `public/logo.svg` (`viewBox="10 20 76 58"`).
|
|
17
|
+
* Override both when swapping in a non-square logo.
|
|
18
|
+
*/
|
|
19
|
+
logoWidth?: number;
|
|
20
|
+
logoHeight?: number;
|
|
11
21
|
}
|
|
12
22
|
const {
|
|
13
23
|
title = "Handzon.",
|
|
14
24
|
subtitle = "Step-by-step tutorials.",
|
|
15
25
|
logoUrl = "/logo.svg",
|
|
26
|
+
logoWidth = 76,
|
|
27
|
+
logoHeight = 58,
|
|
16
28
|
} = Astro.props;
|
|
17
29
|
---
|
|
18
30
|
<header class="hero">
|
|
19
31
|
<h1 class="hero-headline">
|
|
20
32
|
{logoUrl && (
|
|
21
|
-
<img
|
|
33
|
+
<img
|
|
34
|
+
class="hero-logo"
|
|
35
|
+
src={logoUrl}
|
|
36
|
+
alt=""
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
width={logoWidth}
|
|
39
|
+
height={logoHeight}
|
|
40
|
+
/>
|
|
22
41
|
)}
|
|
23
42
|
<span>{title}</span>
|
|
24
43
|
</h1>
|
|
@@ -18,6 +18,15 @@ interface Props {
|
|
|
18
18
|
nav?: "full" | "userMenu" | "none";
|
|
19
19
|
/** Logo URL passed to Navbar (when nav === "full"). */
|
|
20
20
|
logoUrl?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Intrinsic logo aspect ratio (in the image's native units, ratio
|
|
23
|
+
* only). Forwarded to the Navbar's <img> as `width`/`height` so the
|
|
24
|
+
* browser reserves correct box size before the SVG arrives,
|
|
25
|
+
* preventing CLS in the navbar row. Override both for non-square
|
|
26
|
+
* logos.
|
|
27
|
+
*/
|
|
28
|
+
logoWidth?: number;
|
|
29
|
+
logoHeight?: number;
|
|
21
30
|
/** Favicon URL injected into <head>. */
|
|
22
31
|
faviconUrl?: string;
|
|
23
32
|
/** Show the site footer ("Built with Handzon" + repo link). */
|
|
@@ -48,6 +57,8 @@ const {
|
|
|
48
57
|
tagline = "Hands-on tutorials",
|
|
49
58
|
nav = "full",
|
|
50
59
|
logoUrl = "/logo.svg",
|
|
60
|
+
logoWidth = 76,
|
|
61
|
+
logoHeight = 58,
|
|
51
62
|
faviconUrl = "/favicon.svg",
|
|
52
63
|
showFooter = true,
|
|
53
64
|
repoUrl,
|
|
@@ -84,7 +95,12 @@ const desc = description ?? tagline;
|
|
|
84
95
|
</head>
|
|
85
96
|
<body style={`--hz-page-max-width: ${resolvedMaxWidth}; --hz-page-padding-x: ${pagePaddingX}; --hz-nav-height: 3rem;`}>
|
|
86
97
|
{nav === "full" && (
|
|
87
|
-
<Navbar
|
|
98
|
+
<Navbar
|
|
99
|
+
logoUrl={logoUrl}
|
|
100
|
+
siteName={siteName}
|
|
101
|
+
logoWidth={logoWidth}
|
|
102
|
+
logoHeight={logoHeight}
|
|
103
|
+
/>
|
|
88
104
|
)}
|
|
89
105
|
{nav === "userMenu" && (
|
|
90
106
|
<div class="hz-topbar">
|
|
@@ -14,6 +14,8 @@ interface Props {
|
|
|
14
14
|
siteName?: string;
|
|
15
15
|
tagline?: string;
|
|
16
16
|
logoUrl?: string;
|
|
17
|
+
logoWidth?: number;
|
|
18
|
+
logoHeight?: number;
|
|
17
19
|
faviconUrl?: string;
|
|
18
20
|
repoUrl?: string;
|
|
19
21
|
}
|
|
@@ -27,6 +29,8 @@ const {
|
|
|
27
29
|
siteName,
|
|
28
30
|
tagline,
|
|
29
31
|
logoUrl,
|
|
32
|
+
logoWidth,
|
|
33
|
+
logoHeight,
|
|
30
34
|
faviconUrl,
|
|
31
35
|
repoUrl,
|
|
32
36
|
} = Astro.props;
|
|
@@ -39,6 +43,8 @@ const stepSlugs = steps.map((s) => parseStepId(s.id).stepSlug);
|
|
|
39
43
|
siteName={siteName}
|
|
40
44
|
tagline={tagline}
|
|
41
45
|
logoUrl={logoUrl}
|
|
46
|
+
logoWidth={logoWidth}
|
|
47
|
+
logoHeight={logoHeight}
|
|
42
48
|
faviconUrl={faviconUrl}
|
|
43
49
|
repoUrl={repoUrl}
|
|
44
50
|
>
|
|
@@ -209,8 +215,18 @@ const stepSlugs = steps.map((s) => parseStepId(s.id).stepSlug);
|
|
|
209
215
|
}
|
|
210
216
|
.main {
|
|
211
217
|
padding: 2rem clamp(1rem, 4vw, 3rem);
|
|
218
|
+
/* 80ch is the upper end of comfortable prose width; on wide
|
|
219
|
+
* viewports we trade a little of that for code-block headroom so
|
|
220
|
+
* fenced blocks wrap less aggressively. Capped at 92ch — past
|
|
221
|
+
* that body prose starts to feel like a magazine column. */
|
|
212
222
|
max-width: 80ch;
|
|
213
223
|
}
|
|
224
|
+
@media (min-width: 1280px) {
|
|
225
|
+
.main { max-width: 88ch; }
|
|
226
|
+
}
|
|
227
|
+
@media (min-width: 1600px) {
|
|
228
|
+
.main { max-width: 92ch; }
|
|
229
|
+
}
|
|
214
230
|
.crumb {
|
|
215
231
|
font-family: var(--font-mono);
|
|
216
232
|
font-size: 0.75em;
|
package/src/pages/Home.astro
CHANGED
|
@@ -12,6 +12,8 @@ interface Props {
|
|
|
12
12
|
tagline?: string;
|
|
13
13
|
hero?: { title?: string; subtitle?: string };
|
|
14
14
|
logoUrl?: string;
|
|
15
|
+
logoWidth?: number;
|
|
16
|
+
logoHeight?: number;
|
|
15
17
|
faviconUrl?: string;
|
|
16
18
|
repoUrl?: string;
|
|
17
19
|
showResumeRail?: boolean;
|
|
@@ -25,6 +27,8 @@ const {
|
|
|
25
27
|
tagline = "Hands-on tutorials",
|
|
26
28
|
hero,
|
|
27
29
|
logoUrl,
|
|
30
|
+
logoWidth,
|
|
31
|
+
logoHeight,
|
|
28
32
|
faviconUrl,
|
|
29
33
|
repoUrl,
|
|
30
34
|
showResumeRail = true,
|
|
@@ -56,13 +60,21 @@ for (const t of tutorials) {
|
|
|
56
60
|
siteName={siteName}
|
|
57
61
|
tagline={tagline}
|
|
58
62
|
logoUrl={logoUrl}
|
|
63
|
+
logoWidth={logoWidth}
|
|
64
|
+
logoHeight={logoHeight}
|
|
59
65
|
faviconUrl={faviconUrl}
|
|
60
66
|
repoUrl={repoUrl}
|
|
61
67
|
nav="userMenu"
|
|
62
68
|
>
|
|
63
69
|
<slot name="head" slot="head" />
|
|
64
70
|
<div class="home">
|
|
65
|
-
<Hero
|
|
71
|
+
<Hero
|
|
72
|
+
title={hero?.title}
|
|
73
|
+
subtitle={hero?.subtitle}
|
|
74
|
+
logoUrl={logoUrl}
|
|
75
|
+
logoWidth={logoWidth}
|
|
76
|
+
logoHeight={logoHeight}
|
|
77
|
+
/>
|
|
66
78
|
|
|
67
79
|
{showResumeRail && (
|
|
68
80
|
<ResumeRail client:load tutorials={compactTutorials} />
|
|
@@ -8,11 +8,22 @@ interface Props {
|
|
|
8
8
|
siteName?: string;
|
|
9
9
|
tagline?: string;
|
|
10
10
|
logoUrl?: string;
|
|
11
|
+
logoWidth?: number;
|
|
12
|
+
logoHeight?: number;
|
|
11
13
|
faviconUrl?: string;
|
|
12
14
|
repoUrl?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
tutorial,
|
|
19
|
+
siteName,
|
|
20
|
+
tagline,
|
|
21
|
+
logoUrl,
|
|
22
|
+
logoWidth,
|
|
23
|
+
logoHeight,
|
|
24
|
+
faviconUrl,
|
|
25
|
+
repoUrl,
|
|
26
|
+
} = Astro.props;
|
|
16
27
|
const steps = await getStepsForTutorial(tutorial.id);
|
|
17
28
|
const duration = tutorial.data.estimatedDuration ?? sumDurations(steps) ?? "";
|
|
18
29
|
const firstStepSlug = steps[0] ? parseStepId(steps[0].id).stepSlug : null;
|
|
@@ -23,6 +34,8 @@ const firstStepSlug = steps[0] ? parseStepId(steps[0].id).stepSlug : null;
|
|
|
23
34
|
siteName={siteName}
|
|
24
35
|
tagline={tagline}
|
|
25
36
|
logoUrl={logoUrl}
|
|
37
|
+
logoWidth={logoWidth}
|
|
38
|
+
logoHeight={logoHeight}
|
|
26
39
|
faviconUrl={faviconUrl}
|
|
27
40
|
repoUrl={repoUrl}
|
|
28
41
|
>
|
|
@@ -20,6 +20,8 @@ interface Props {
|
|
|
20
20
|
siteName?: string;
|
|
21
21
|
tagline?: string;
|
|
22
22
|
logoUrl?: string;
|
|
23
|
+
logoWidth?: number;
|
|
24
|
+
logoHeight?: number;
|
|
23
25
|
faviconUrl?: string;
|
|
24
26
|
repoUrl?: string;
|
|
25
27
|
}
|
|
@@ -32,6 +34,8 @@ const {
|
|
|
32
34
|
siteName,
|
|
33
35
|
tagline,
|
|
34
36
|
logoUrl,
|
|
37
|
+
logoWidth,
|
|
38
|
+
logoHeight,
|
|
35
39
|
faviconUrl,
|
|
36
40
|
repoUrl,
|
|
37
41
|
} = Astro.props;
|
|
@@ -60,6 +64,8 @@ const initialContext = buildContext({
|
|
|
60
64
|
siteName={siteName}
|
|
61
65
|
tagline={tagline}
|
|
62
66
|
logoUrl={logoUrl}
|
|
67
|
+
logoWidth={logoWidth}
|
|
68
|
+
logoHeight={logoHeight}
|
|
63
69
|
faviconUrl={faviconUrl}
|
|
64
70
|
repoUrl={repoUrl}
|
|
65
71
|
>
|