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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handzon-core",
3
- "version": "0.8.2",
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 { logoUrl = "/logo.svg", siteName = "Handzon" } = Astro.props;
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 src={logoUrl} alt={siteName} class="hz-nav-logo" />
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
- /* Two-row layout: duration sits flush-right on its own tight row
124
- * above the title. The checkbox sits in the title row only (not
125
- * spanning) so it vertically aligns with the name's center,
126
- * independent of whether a duration row is present above. The first
127
- * column of the duration row is empty and collapses harmlessly. */
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
- row-gap: 0;
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-check { grid-area: check; }
147
- .sb-name { grid-area: name; line-height: 1.25; }
148
- .sb-dur { grid-area: dur; justify-self: end; line-height: 1; }
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 class="hero-logo" src={logoUrl} alt="" aria-hidden="true" />
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 logoUrl={logoUrl} siteName={siteName} />
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;
@@ -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 title={hero?.title} subtitle={hero?.subtitle} logoUrl={logoUrl} />
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 { tutorial, siteName, tagline, logoUrl, faviconUrl, repoUrl } = Astro.props;
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
  >