handzon-core 0.8.0 → 0.8.2

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.2",
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;
@@ -117,18 +120,32 @@ const slug = tutorial.id;
117
120
  .sb-steps li + li {
118
121
  border-top: var(--border-default) solid var(--color-border);
119
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. */
120
128
  .sb-steps a {
121
129
  display: grid;
122
- grid-template-columns: 18px 1fr auto;
130
+ grid-template-columns: 18px 1fr;
131
+ grid-template-areas:
132
+ ". dur"
133
+ "check name";
123
134
  align-items: center;
124
- gap: 0.6rem;
125
- padding: 0.6rem 0.7rem;
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;
126
140
  color: var(--color-fg);
127
141
  text-decoration: none;
128
142
  font-size: 0.92em;
129
143
  font-weight: 500;
130
144
  letter-spacing: -0.005em;
131
145
  }
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; }
132
149
  .sb-steps a:hover {
133
150
  background: var(--color-surface);
134
151
  color: var(--color-fg);
@@ -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
  }