handzon-core 0.8.1 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handzon-core",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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,18 +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
+ /* 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. */
123
130
  .sb-steps a {
131
+ position: relative;
124
132
  display: grid;
125
- grid-template-columns: 18px 1fr auto;
133
+ grid-template-columns: 18px 1fr;
126
134
  align-items: center;
127
- gap: 0.6rem;
128
- padding: 0.6rem 1rem;
135
+ column-gap: 0.6rem;
136
+ padding: 1.05rem 1rem;
129
137
  color: var(--color-fg);
130
138
  text-decoration: none;
131
139
  font-size: 0.92em;
132
140
  font-weight: 500;
133
141
  letter-spacing: -0.005em;
134
142
  }
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
+ }
135
150
  .sb-steps a:hover {
136
151
  background: var(--color-surface);
137
152
  color: var(--color-fg);
@@ -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
  >