handzon-core 0.8.0 → 0.8.1

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.0",
3
+ "version": "0.8.1",
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"
@@ -106,9 +106,12 @@ const slug = tutorial.id;
106
106
  font-family: var(--font-mono);
107
107
  letter-spacing: 0.04em;
108
108
  }
109
+ /* Horizontal padding lives on the <a>, not on `.sb-steps`, so the
110
+ * current-step background fills the sidebar edge-to-edge instead of
111
+ * leaving a 1rem gutter on either side. The list itself is flush. */
109
112
  .sb-steps {
110
113
  list-style: none;
111
- padding: 0 1rem 2rem;
114
+ padding: 0 0 2rem;
112
115
  margin: 0.75rem 0 0;
113
116
  flex: 1;
114
117
  overflow-y: auto;
@@ -122,7 +125,7 @@ const slug = tutorial.id;
122
125
  grid-template-columns: 18px 1fr auto;
123
126
  align-items: center;
124
127
  gap: 0.6rem;
125
- padding: 0.6rem 0.7rem;
128
+ padding: 0.6rem 1rem;
126
129
  color: var(--color-fg);
127
130
  text-decoration: none;
128
131
  font-size: 0.92em;
@@ -35,9 +35,10 @@ const {
35
35
  align-items: center;
36
36
  gap: clamp(0.6rem, 1.2vw, 1rem);
37
37
  margin: 0 0 0.75rem;
38
- font-size: clamp(2.25rem, 5vw, 3.75rem);
39
- font-weight: 700;
40
- letter-spacing: -0.025em;
38
+ font-family: var(--font-display, var(--font-sans));
39
+ font-size: var(--text-display, clamp(2.25rem, 5vw, 3.75rem));
40
+ font-weight: var(--font-weight-display, 700);
41
+ letter-spacing: var(--tracking-display, -0.025em);
41
42
  line-height: 1.05;
42
43
  }
43
44
  /* Logo height tracks the heading's cap-height (~0.72em of the
@@ -75,6 +75,12 @@ const desc = description ?? tagline;
75
75
  <meta property="og:title" content={pageTitle} />
76
76
  <meta property="og:description" content={desc} />
77
77
  <meta property="og:type" content="website" />
78
+ {/* Optional consumer-side <head> injections. Forwarded by every
79
+ * page wrapper (Home, TutorialLanding, TutorialStep / TutorialLayout)
80
+ * so scaffolds can preload fonts, attach per-page OG images, add
81
+ * JSON-LD structured data, or wire verification meta without
82
+ * forking BaseLayout. See README for usage. */}
83
+ <slot name="head" />
78
84
  </head>
79
85
  <body style={`--hz-page-max-width: ${resolvedMaxWidth}; --hz-page-padding-x: ${pagePaddingX}; --hz-nav-height: 3rem;`}>
80
86
  {nav === "full" && (
@@ -42,6 +42,7 @@ const stepSlugs = steps.map((s) => parseStepId(s.id).stepSlug);
42
42
  faviconUrl={faviconUrl}
43
43
  repoUrl={repoUrl}
44
44
  >
45
+ <slot name="head" slot="head" />
45
46
  <div class="layout">
46
47
  <div class="sidebar-wrap">
47
48
  <Sidebar tutorial={tutorial} steps={steps} currentStepSlug={currentStepSlug} />
@@ -177,23 +178,30 @@ const stepSlugs = steps.map((s) => parseStepId(s.id).stepSlug);
177
178
 
178
179
  <style>
179
180
  /* Draw the sidebar/main divider as a background line on the grid
180
- * itself: 1px-thin vertical strip at the 280px column boundary,
181
+ * itself: 1px-thin vertical strip at the sidebar column boundary,
181
182
  * from the top of the layout to the bottom. The grid auto-stretches
182
183
  * to fit its content, so the line runs all the way down to the
183
184
  * footer regardless of step length — without piggybacking on the
184
- * (sticky, viewport-height) sidebar's own border. */
185
+ * (sticky, viewport-height) sidebar's own border.
186
+ *
187
+ * `--sb-w` keeps the column track and the divider stop in lockstep
188
+ * so widening the sidebar at one breakpoint doesn't desync the line. */
185
189
  .layout {
190
+ --sb-w: 280px;
186
191
  display: grid;
187
- grid-template-columns: 280px minmax(0, 1fr);
192
+ grid-template-columns: var(--sb-w) minmax(0, 1fr);
188
193
  min-height: 100dvh;
189
194
  background: linear-gradient(
190
195
  to right,
191
- transparent 280px,
192
- var(--color-border) 280px,
193
- var(--color-border) 281px,
194
- transparent 281px
196
+ transparent var(--sb-w),
197
+ var(--color-border) var(--sb-w),
198
+ var(--color-border) calc(var(--sb-w) + 1px),
199
+ transparent calc(var(--sb-w) + 1px)
195
200
  ) no-repeat;
196
201
  }
202
+ @media (min-width: 1280px) {
203
+ .layout { --sb-w: 320px; }
204
+ }
197
205
  .sidebar-wrap {
198
206
  position: sticky;
199
207
  top: var(--hz-nav-height, 3rem);
@@ -211,10 +219,11 @@ const stepSlugs = steps.map((s) => parseStepId(s.id).stepSlug);
211
219
  color: var(--color-muted);
212
220
  }
213
221
  .step-title {
222
+ font-family: var(--font-display, var(--font-sans));
214
223
  font-size: clamp(1.75rem, 3vw, 2.25rem);
215
- font-weight: 700;
224
+ font-weight: var(--font-weight-display, 700);
216
225
  margin: 0.3rem 0 0.5rem;
217
- letter-spacing: -0.02em;
226
+ letter-spacing: var(--tracking-display, -0.02em);
218
227
  line-height: 1.15;
219
228
  }
220
229
  .step-dur {
@@ -60,6 +60,7 @@ for (const t of tutorials) {
60
60
  repoUrl={repoUrl}
61
61
  nav="userMenu"
62
62
  >
63
+ <slot name="head" slot="head" />
63
64
  <div class="home">
64
65
  <Hero title={hero?.title} subtitle={hero?.subtitle} logoUrl={logoUrl} />
65
66
 
@@ -238,7 +239,12 @@ for (const t of tutorials) {
238
239
  }
239
240
  .grid {
240
241
  display: grid;
241
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
242
+ /* Cap at 3 columns max: the lower-bound of each track is the larger
243
+ * of 280px or one-third of the row (minus the two 1rem gaps). On
244
+ * wide viewports the (100% - 2rem)/3 term wins and forces exactly
245
+ * 3 columns; on narrow viewports 280px wins and the grid wraps to
246
+ * 2 or 1 columns as usual. */
247
+ grid-template-columns: repeat(auto-fill, minmax(max(280px, (100% - 2rem) / 3), 1fr));
242
248
  gap: 1rem;
243
249
  margin-top: 0.75rem;
244
250
  }
@@ -26,6 +26,7 @@ const firstStepSlug = steps[0] ? parseStepId(steps[0].id).stepSlug : null;
26
26
  faviconUrl={faviconUrl}
27
27
  repoUrl={repoUrl}
28
28
  >
29
+ <slot name="head" slot="head" />
29
30
  <div class="landing">
30
31
  <a class="back" href="/">← All tutorials</a>
31
32
 
@@ -58,7 +59,7 @@ const firstStepSlug = steps[0] ? parseStepId(steps[0].id).stepSlug : null;
58
59
 
59
60
  {tutorial.data.tags.length > 0 && (
60
61
  <div class="hero-tags" aria-label="Topics">
61
- {tutorial.data.tags.map((tag) => (
62
+ {tutorial.data.tags.map((tag: string) => (
62
63
  <a class="hero-tag" href={`/?tag=${encodeURIComponent(tag)}`}>#{tag}</a>
63
64
  ))}
64
65
  </div>
@@ -68,7 +69,7 @@ const firstStepSlug = steps[0] ? parseStepId(steps[0].id).stepSlug : null;
68
69
  {tutorial.data.prerequisites.length > 0 && (
69
70
  <section class="block">
70
71
  <h2>Prerequisites</h2>
71
- <ul class="prereqs">{tutorial.data.prerequisites.map((p) => <li>{p}</li>)}</ul>
72
+ <ul class="prereqs">{tutorial.data.prerequisites.map((p: string) => <li>{p}</li>)}</ul>
72
73
  </section>
73
74
  )}
74
75
 
@@ -188,10 +189,11 @@ const firstStepSlug = steps[0] ? parseStepId(steps[0].id).stepSlug : null;
188
189
  }
189
190
  .hero-tag:hover { color: var(--color-accent); }
190
191
  h1 {
192
+ font-family: var(--font-display, var(--font-sans));
191
193
  font-size: clamp(2.1rem, 4.5vw, 3rem);
192
- font-weight: 800;
194
+ font-weight: var(--font-weight-display, 800);
193
195
  margin: 0 0 0.75rem;
194
- letter-spacing: -0.025em;
196
+ letter-spacing: var(--tracking-display, -0.025em);
195
197
  line-height: 1.05;
196
198
  }
197
199
  .desc {
@@ -63,6 +63,7 @@ const initialContext = buildContext({
63
63
  faviconUrl={faviconUrl}
64
64
  repoUrl={repoUrl}
65
65
  >
66
+ <slot name="head" slot="head" />
66
67
  <Content components={components} />
67
68
  {aiConfig.enabled && aiConfig.autoStepHelp && (
68
69
  <StepHelp client:visible stepTitle={currentStep.data.title} />
package/styles/base.css CHANGED
@@ -36,13 +36,14 @@ a:hover {
36
36
  }
37
37
 
38
38
  /* Tutorial prose — consistent type scale, single weight family.
39
- * Same weight (700) across every heading; size + tracking handles the
40
- * hierarchy so it doesn't look "h1 thin / h2 super heavy".
39
+ * Defaults preserve the previous hardcoded values; themes can re-skin
40
+ * via the typography tokens documented in AGENTS.md (font weights,
41
+ * tracking, leading, type scale) without resorting to !important.
41
42
  */
42
43
  .prose {
43
44
  font-family: var(--font-sans);
44
- line-height: 1.65;
45
- font-size: 1rem;
45
+ line-height: var(--leading-body, 1.65);
46
+ font-size: var(--text-body, 1rem);
46
47
  max-width: 72ch;
47
48
  }
48
49
 
@@ -52,22 +53,25 @@ a:hover {
52
53
  .prose h4,
53
54
  .prose h5,
54
55
  .prose h6 {
55
- font-weight: 700;
56
- letter-spacing: -0.015em;
57
- line-height: 1.2;
56
+ font-weight: var(--font-weight-heading, 700);
57
+ letter-spacing: var(--tracking-heading, -0.015em);
58
+ line-height: var(--leading-heading, 1.2);
58
59
  margin-top: 2rem;
59
60
  margin-bottom: 0.6rem;
60
61
  color: var(--color-fg);
61
62
  }
62
63
 
63
- .prose h1 { font-size: 1.875rem; letter-spacing: -0.02em; }
64
+ .prose h1 {
65
+ font-size: var(--text-h1, 1.875rem);
66
+ letter-spacing: var(--tracking-display, -0.02em);
67
+ }
64
68
  .prose h2 {
65
- font-size: 1.4rem;
69
+ font-size: var(--text-h2, 1.4rem);
66
70
  padding-top: 1.25rem;
67
71
  border-top: var(--border-default) solid var(--color-border);
68
72
  }
69
- .prose h3 { font-size: 1.15rem; }
70
- .prose h4 { font-size: 1rem; }
73
+ .prose h3 { font-size: var(--text-h3, 1.15rem); }
74
+ .prose h4 { font-size: var(--text-h4, 1rem); }
71
75
 
72
76
  .prose p { margin: 0.75em 0; }
73
77
 
@@ -133,5 +137,5 @@ a:hover {
133
137
 
134
138
  .prose th {
135
139
  background: var(--color-surface);
136
- font-weight: 700;
140
+ font-weight: var(--font-weight-strong, 700);
137
141
  }