slidev-theme-tud-db 0.0.6 → 0.0.8

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/README.md CHANGED
@@ -29,7 +29,7 @@ pnpm dev
29
29
 
30
30
  No `addons:` line needed — the theme enables `slidev-addon-gsap` via its
31
31
  defaults, so `import { useTl } from 'slidev-addon-gsap'` works in any slide. The
32
- theme also sets `canvasWidth: 960` (1px == 1pt), Noto Sans, light scheme, and
32
+ theme also sets `canvasWidth: 960` (the 720pt PowerPoint slide, so 1pt == 4/3 px), Noto Sans, light scheme, and
33
33
  Comark on; override any in your own headmatter.
34
34
 
35
35
  ## Example
@@ -55,5 +55,7 @@ cp -r node_modules/slidev-theme-db/skills/slidev-theme-db .claude/skills/
55
55
  - `styles/db-theme.css` — tokens, typography, lists, tables (keyed on `.db-default`).
56
56
  - `setup/shiki.ts` (fenced) and the `THEME` in `vite.config.ts` (inline `{lang}`) —
57
57
  the **two** highlighters; keep them in sync (both `catppuccin-latte`).
58
+ `setup/shiki.ts` also holds the transformer behind `⟦name:text⟧` code marks
59
+ (spans a slide's timeline can recolour).
58
60
  - Link the theme with `link:` (not `file:`), and **restart** the dev server after
59
61
  editing theme files (Vite doesn't hot-reload `node_modules`).
@@ -11,7 +11,8 @@ const props = defineProps({
11
11
  class="slidev-layout default db-default relative"
12
12
  :style="props.codeSize ? { '--slidev-code-font-size': props.codeSize } : undefined"
13
13
  >
14
- <div class="db-default__body px-[30px] py-[33.5px]">
14
+ <!-- top padding puts the title's baseline at 41.9pt, as in the template -->
15
+ <div class="db-default__body px-[30px] pt-[26.5px] pb-[33.5px]">
15
16
  <slot />
16
17
  </div>
17
18
  </div>
package/global-bottom.vue CHANGED
@@ -42,7 +42,7 @@ const logos = computed(() => slideLogos[$nav.value.currentPage] ?? {})
42
42
  class="db-footer absolute inset-x-0 bottom-0 h-[40px] px-[30px] flex items-center justify-between bg-[var(--db-footer-bg)] pointer-events-auto transition-opacity duration-500"
43
43
  >
44
44
  <img class="h-[25px]" :src="tudBlack" alt="TU Dresden" />
45
- <span class="text-[12px] text-[var(--db-text)]">
45
+ <span class="db-footer__page">
46
46
  <span class="font-bold">{{ $nav.currentPage }}</span>
47
47
  <template v-if="$nav.clicksTotal > 0">
48
48
  <span class="text-[0.7em]">.{{ $nav.clicks + 1 }}</span>
package/layouts/cover.vue CHANGED
@@ -21,12 +21,12 @@ useLogo({ ddrg: 'white', tud: true })
21
21
 
22
22
  <style scoped>
23
23
  .db-cover__title :deep(h1) {
24
- @apply text-4xl! font-700;
24
+ @apply font-700 leading-10;
25
+ font-size: var(--db-fs-cover-title) !important;
25
26
  }
26
27
  .db-cover__title :deep(p) {
27
- @apply absolute top-[95px] m-0 text-md;
28
- /* margin: 16px 0 0; */
29
- /* font-size: 15px; */
28
+ @apply absolute top-[95px] m-0;
29
+ font-size: var(--db-fs-subtitle);
30
30
  color: var(--db-text-soft);
31
31
  }
32
32
  </style>
@@ -60,10 +60,12 @@ const num = computed(() => {
60
60
 
61
61
  <style scoped>
62
62
  .db-cover__title :deep(h1) {
63
- @apply text-4xl! font-700 text-[var(--db-heading-blue)];
63
+ @apply font-700 leading-10 text-[var(--db-heading-blue)];
64
+ font-size: var(--db-fs-cover-title) !important;
64
65
  }
65
66
  .db-cover__title :deep(p) {
66
- @apply absolute top-[95px] m-0 text-md;
67
+ @apply absolute top-[95px] m-0;
68
+ font-size: var(--db-fs-subtitle);
67
69
  color: var(--db-text-soft);
68
70
  }
69
71
  </style>
@@ -21,12 +21,12 @@ useLogo({ ddrg: 'color', footer: true })
21
21
 
22
22
  <style scoped>
23
23
  .db-cover__title :deep(h1) {
24
- @apply text-4xl! font-700 text-[var(--db-heading-blue)];
24
+ @apply font-700 leading-10 text-[var(--db-heading-blue)];
25
+ font-size: var(--db-fs-section-title) !important;
25
26
  }
26
27
  .db-cover__title :deep(p) {
27
- @apply absolute top-[95px] m-0 text-md;
28
- /* margin: 16px 0 0; */
29
- /* font-size: 15px; */
28
+ @apply absolute top-[95px] m-0;
29
+ font-size: var(--db-fs-subtitle);
30
30
  color: var(--db-text-soft);
31
31
  }
32
32
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-theme-tud-db",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "description": "Slidev theme cloning the TU Dresden / Dresden Database Research Group PowerPoint template",
6
6
  "license": "GPL-3.0-or-later",
package/setup/shiki.ts CHANGED
@@ -5,9 +5,179 @@
5
5
  // NB: no `import { defineShikiSetup } from '@slidev/types'` on purpose — it's
6
6
  // only a type helper, and Slidev calls this default export as `setup(ctx)`.
7
7
  // catppuccin-latte / -mocha are bundled with Shiki, so no extra deps needed.
8
+
9
+ // Code marks: wrap exact character ranges of a fenced block so a slide can
10
+ // recolour that text, e.g. at a click. Inside the code, write ⟦name:text⟧: the
11
+ // markers are removed and `text` is wrapped in <span class="code-mark mark-name">.
12
+ //
13
+ // WHERE salary > ⟦paren:(⟧
14
+ //
15
+ // A mark looks like the code around it until something recolours it, typically
16
+ // a gsap timeline at a click. A mark can span several syntax-coloured tokens,
17
+ // so recolour the tokens inside it too:
18
+ //
19
+ // tl.to('.mark-paren, .mark-paren span', { color: '#f9c611' })
20
+ //
21
+ // A mark must stay within one line. Magic-move blocks don't run Shiki
22
+ // transformers, so marks only work in plain fenced blocks.
23
+ const MARK = /⟦([\w-]+):([^⟧\n]*)⟧/g
24
+
25
+ interface Decoration { start: number, end: number, properties: Record<string, string> }
26
+
27
+ export const codeMarks = {
28
+ name: 'tud-db:code-marks',
29
+ preprocess(code: string, options: { decorations?: Decoration[] }) {
30
+ if (!code.includes('⟦'))
31
+ return
32
+ const marks: Decoration[] = []
33
+ let out = ''
34
+ let last = 0
35
+ for (const m of code.matchAll(MARK)) {
36
+ out += code.slice(last, m.index)
37
+ const start = out.length
38
+ out += m[2]
39
+ marks.push({ start, end: out.length, properties: { class: `code-mark mark-${m[1]}` } })
40
+ last = m.index! + m[0].length
41
+ }
42
+ out += code.slice(last)
43
+ // Shiki reads the decorations from these options when it renders
44
+ options.decorations = [...(options.decorations ?? []), ...marks]
45
+ return out
46
+ },
47
+ }
48
+
49
+ const SQL_FUNCS = /^(COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)$/i
50
+ const SQL_CONSTANTS = /^(TRUE|FALSE|UNKNOWN|NULL)$/i
51
+ const SQL_KEYWORDS = /^(EXPLAIN|ANALYZE)$/i
52
+ const SQL_ALL = /^(TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)$/i
53
+ const SQL_SPLIT = /\b(TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)\b/i
54
+
55
+ function getSqlStyle(w: string) {
56
+ if (SQL_FUNCS.test(w)) return { color: '#1E66F5', light: '#1E66F5', dark: '#89B4FA' }
57
+ if (SQL_CONSTANTS.test(w)) return { color: '#FE640B', light: '#FE640B', dark: '#FAB387' }
58
+ return { color: '#8839EF', light: '#8839EF', dark: '#CBA6F7' }
59
+ }
60
+
61
+ export const sqlKeywords = {
62
+ name: 'tud-db:sql-keywords',
63
+ tokens(lines: any[]) {
64
+ if ((this as any).options?.lang !== 'sql') return
65
+ for (let l = 0; l < lines.length; l++) {
66
+ const line = lines[l]
67
+ const newTokens: any[] = []
68
+ for (const token of line) {
69
+ const trimmed = token.content.trim()
70
+ const upper = trimmed.toUpperCase()
71
+
72
+ // 0. Intercept numeric tokens -> light emerald green (#059669)
73
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
74
+ const leading = token.content.slice(0, token.content.indexOf(trimmed))
75
+ const trailing = token.content.slice(token.content.indexOf(trimmed) + trimmed.length)
76
+ if (leading) newTokens.push({ ...token, content: leading, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
77
+ newTokens.push({
78
+ ...token,
79
+ content: trimmed,
80
+ color: '#059669',
81
+ fontStyle: 0,
82
+ htmlStyle: { color: '#059669', '--shiki-light': '#059669', '--shiki-dark': '#34D399' },
83
+ })
84
+ if (trailing) newTokens.push({ ...token, content: trailing, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
85
+ continue
86
+ }
87
+
88
+ // 1. Intercept NULL if Shiki tagged it as keyword purple (including combined tokens like "IS NOT NULL")
89
+ if (/\bNULL\b/i.test(token.content)) {
90
+ const s = getSqlStyle('NULL')
91
+ const parts = token.content.split(/\b(NULL)\b/i)
92
+ let currOffset = token.offset
93
+ for (const part of parts) {
94
+ if (!part) continue
95
+ if (/^NULL$/i.test(part)) {
96
+ newTokens.push({
97
+ ...token,
98
+ content: part,
99
+ offset: currOffset,
100
+ color: s.color,
101
+ fontStyle: 0,
102
+ htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
103
+ })
104
+ }
105
+ else {
106
+ newTokens.push({ ...token, content: part, offset: currOffset })
107
+ }
108
+ currOffset += part.length
109
+ }
110
+ continue
111
+ }
112
+
113
+ // 2. Intercept function tokens (AVG, COUNT, COALESCE, etc.) -> upright blue
114
+ // and normalize misclassified identifiers (e.g. `movies` in `CREATE TABLE movies`) -> plain text upright
115
+ const hasItalic = token.fontStyle === 1
116
+ || token.htmlStyle?.['--shiki-light-font-style'] === 'italic'
117
+ || token.htmlStyle?.['--shiki-dark-font-style'] === 'italic'
118
+ const isBlue = (token.color?.toLowerCase() === '#1e66f5' || token.htmlStyle?.['--shiki-light']?.toLowerCase() === '#1e66f5')
119
+ if (hasItalic && isBlue) {
120
+ const s = SQL_ALL.test(trimmed)
121
+ ? getSqlStyle(trimmed)
122
+ : { color: '#4C4F69', light: '#4C4F69', dark: '#CDD6F4' }
123
+ const leading = token.content.slice(0, token.content.indexOf(trimmed))
124
+ const trailing = token.content.slice(token.content.indexOf(trimmed) + trimmed.length)
125
+ if (leading) newTokens.push({ ...token, content: leading, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
126
+ newTokens.push({
127
+ ...token,
128
+ content: trimmed,
129
+ color: s.color,
130
+ fontStyle: 0,
131
+ htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
132
+ })
133
+ if (trailing) newTokens.push({ ...token, content: trailing, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
134
+ continue
135
+ }
136
+
137
+ // 3. Intercept plain text tokens containing SQL words (TRUE, FALSE, UNKNOWN, EXPLAIN, ANALYZE, functions, etc.)
138
+ const lightColor = token.htmlStyle?.['--shiki-light'] || token.htmlStyle?.color || token.color
139
+ const isPlain = !lightColor || lightColor.toLowerCase() === '#4c4f69'
140
+ if (isPlain && SQL_SPLIT.test(token.content)) {
141
+ const parts = token.content.split(SQL_SPLIT)
142
+ let currOffset = token.offset
143
+ for (const part of parts) {
144
+ if (!part) continue
145
+ if (SQL_ALL.test(part)) {
146
+ const s = getSqlStyle(part)
147
+ newTokens.push({
148
+ ...token,
149
+ content: part,
150
+ offset: currOffset,
151
+ color: s.color,
152
+ fontStyle: 0,
153
+ htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
154
+ })
155
+ }
156
+ else {
157
+ newTokens.push({
158
+ ...token,
159
+ content: part,
160
+ offset: currOffset,
161
+ fontStyle: 0,
162
+ htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' },
163
+ })
164
+ }
165
+ currOffset += part.length
166
+ }
167
+ continue
168
+ }
169
+
170
+ newTokens.push(token)
171
+ }
172
+ lines[l] = newTokens
173
+ }
174
+ },
175
+ }
176
+
8
177
  export default () => ({
9
178
  themes: {
10
179
  light: 'catppuccin-latte',
11
180
  dark: 'catppuccin-mocha',
12
181
  },
182
+ transformers: [codeMarks, sqlKeywords],
13
183
  })
@@ -20,6 +20,21 @@
20
20
  --db-table-stripe: #dbdbdb; /* zebra-striped table rows */
21
21
  --db-table-rule: #b9b9b9; /* table header/footer rule lines */
22
22
 
23
+ /* Type scale, in PowerPoint points. The template slide is 720×405pt but the
24
+ theme renders it on Slidev's 960×540px canvas, so 1pt == 4/3 px — never
25
+ write a pt value as px. Sizes measured from resource/template.pdf. */
26
+ --db-pt: calc(4px / 3);
27
+ --db-fs-cover-title: calc(28 * var(--db-pt)); /* cover / section-num title */
28
+ --db-fs-section-title: calc(24 * var(--db-pt)); /* section title */
29
+ --db-fs-subtitle: calc(14 * var(--db-pt)); /* cover / section subtitle */
30
+ --db-fs-title: calc(22 * var(--db-pt)); /* content slide title (h1) */
31
+ --db-fs-heading: calc(15 * var(--db-pt)); /* blue sub-header (h2/h3) */
32
+ --db-fs-body: calc(13 * var(--db-pt)); /* body text, 1st-level bullets */
33
+ --db-fs-small: calc(12 * var(--db-pt)); /* 2nd-level bullets, page number */
34
+ --db-fs-code: calc(11 * var(--db-pt)); /* code blocks */
35
+ --db-fs-caption: calc(11 * var(--db-pt)); /* table / code captions */
36
+ --db-fs-table: calc(10 * var(--db-pt)); /* table cells */
37
+
23
38
  /* Slidev theme variables */
24
39
  --slidev-theme-primary: var(--db-accent3);
25
40
  }
@@ -43,14 +58,21 @@
43
58
 
44
59
  /* Inline code "pills": slimmer so they don't bloat line height in bullets.
45
60
  (Slidev default is ~2px 6px / 0.9em.) Fenced code (pre > code) is untouched. */
46
- .slidev-layout.db-default :not(pre) > code {
61
+ .slidev-layout.db-default :not(pre) > code,
62
+ .slidev-layout :not(pre) > code,
63
+ code.db-inline-code {
47
64
  padding: 1px 0; /* no horizontal padding by default so it sits flush with text */
48
65
  font-size: 0.85em;
66
+ font-weight: 400; /* regular weight — override Slidev's thin 300 default */
49
67
  line-height: 1;
50
68
  background: transparent !important; /* transparent by default */
69
+ font-variant-ligatures: none;
70
+ white-space: pre-wrap;
51
71
  }
52
72
  /* Opt-in faint background for inline code */
53
- .slidev-layout.db-default :not(pre) > code.with-bg {
73
+ .slidev-layout.db-default :not(pre) > code.with-bg,
74
+ .slidev-layout :not(pre) > code.with-bg,
75
+ code.db-inline-code.with-bg {
54
76
  padding: 1px 4px; /* add horizontal padding back for the background box */
55
77
  background: rgba(0, 0, 0, 0.03) !important;
56
78
  }
@@ -59,27 +81,36 @@
59
81
  color: var(--db-accent3);
60
82
  }
61
83
 
62
- /* Content slides ("Titel und Inhalt").
63
- * Canvas is 720px wide (== 720pt real slide), so px values below equal PowerPoint pt. */
84
+ /* Content slides ("Titel und Inhalt"). Font sizes come from the pt type scale
85
+ * on :root; spacing below is in canvas px, chosen so the baselines land where
86
+ * resource/template.pdf has them: title 41.9pt, first sub-header 92.5pt, then
87
+ * bullets every 18pt, and 27.9pt from a bullet to the next sub-header. (Noto
88
+ * Sans puts the baseline 0.388em below the middle of its line box.) */
64
89
  .slidev-layout.db-default {
65
- @apply text-md p-0;
90
+ font-size: var(--db-fs-body);
91
+ padding: 0;
66
92
  }
67
93
  .slidev-layout.db-default h1 {
68
94
  color: var(--db-text-soft); /* black/gray page heading */
69
- @apply text-3xl mb-[46px]; /* gap to first sub-header (~46pt in template) */
95
+ font-size: var(--db-fs-title);
96
+ line-height: 2.25rem;
97
+ margin-bottom: 39px; /* gap to first sub-header (baseline at 92.5pt) */
70
98
  }
71
99
  .slidev-layout.db-default h2,
72
100
  .slidev-layout.db-default h3 {
73
101
  color: var(--db-heading-blue); /* blue sub-headers */
74
- @apply text-xl mt-[45px]; /* space ABOVE a sub-header, whatever precedes it
75
- (bullet list, code block, paragraph, …) */
102
+ font-size: var(--db-fs-heading);
103
+ line-height: 1.75rem;
104
+ margin-top: 11px; /* space ABOVE a sub-header, whatever precedes it
105
+ (bullet list, code block, paragraph, …);
106
+ 27.9pt baseline-to-baseline after a bullet */
76
107
  }
77
108
  /* …except a sub-header that is the FIRST content of its area (right under the
78
109
  page title, or the top of a column / grid cell) — the title's margin-bottom
79
110
  or the grid gap already provides that separation.
80
111
  This is wrap-proof: a section wrapped in a <div> (e.g. a GSAP animation
81
112
  anchor) is still treated as "top" when its wrapper is the first child, but a
82
- wrapped section that FOLLOWS other content keeps the 24px gap. So adding a
113
+ wrapped section that FOLLOWS other content keeps its top gap. So adding a
83
114
  wrapper div later does NOT collapse the spacing. (default layout: h1 is a
84
115
  sibling of the content; two-cols-header: .db-grid__col; two-cols-grid:
85
116
  .db-rowgrid cells.) */
@@ -105,7 +136,7 @@
105
136
  .slidev-layout.db-default ul ul,
106
137
  .slidev-layout.db-default ol ol {
107
138
  margin-left: 0; /* 2nd-level marker aligns with 1st-level text (no extra indent) */
108
- font-size: 12px; /* 2nd-level text a touch smaller (12pt vs 13pt) */
139
+ font-size: var(--db-fs-small); /* 2nd-level text a touch smaller (12pt vs 13pt) */
109
140
  }
110
141
  .slidev-layout.db-default ul ul { list-style-type: square; }
111
142
  /* Tight vertical rhythm (theme defaults to ~1.8 line-height) */
@@ -175,7 +206,7 @@
175
206
  }
176
207
  .slidev-layout.db-default .db-takeaway__body p { margin: 0; }
177
208
  .slidev-layout.db-default .db-takeaway__body strong { font-weight: 700; }
178
- .slidev-layout.db-default .db-takeaway__body code { font-weight: 500; }
209
+ .slidev-layout.db-default .db-takeaway__body code { font-weight: 400; }
179
210
 
180
211
  /* Fenced code blocks: box only as wide as its content, not full slide width.
181
212
  fit-content on the WRAPPER so an optional caption + the code share one width.
@@ -192,24 +223,39 @@
192
223
  .slidev-layout.db-default .with-bg pre.slidev-code {
193
224
  background: rgba(0, 0, 0, 0.03) !important;
194
225
  }
195
- /* Code sizing (Slidev defaults: 12px / 18px / 8px). Slightly smaller font and
196
- tighter vertical padding so the box hugs the code top and bottom. */
226
+ /* Code sizing (Slidev defaults: 12px / 18px / 8px). 11pt like the template's
227
+ listings, with tighter vertical padding so the box hugs the code top and bottom.
228
+ No left padding: without a background box, the code text lines up with the
229
+ text, captions and tables around it, as in the template. A .with-bg box gets
230
+ its inset back. */
197
231
  .slidev-layout.db-default {
198
- --slidev-code-font-size: 11px;
232
+ --slidev-code-font-size: var(--db-fs-code);
199
233
  --slidev-code-line-height: 1.3;
234
+ --slidev-code-padding: 4px 10px 4px 0;
235
+ }
236
+ .slidev-layout.db-default .with-bg {
200
237
  --slidev-code-padding: 4px 10px;
201
238
  }
239
+ /* No programming ligatures: Fira Code would draw <> as a diamond and <=, >=,
240
+ != as ≤, ≥, ≠, which readers then can't type. Code shows the characters
241
+ as written. (The theme's --> arrows in prose are a text replacement, not a
242
+ ligature, so they are unaffected.) */
243
+ .slidev-layout code,
244
+ .slidev-layout pre {
245
+ font-variant-ligatures: none;
246
+ }
202
247
 
203
248
  /* Line numbers (```lang {lines:true}```): tighten the gutter. Slidev's defaults
204
249
  are a wide 24px gap between the number and the code, plus the block's 10px
205
250
  left padding before the number. Both are scoped to line-numbered blocks so
206
251
  plain code blocks keep their normal padding. */
207
252
  .slidev-layout.db-default .slidev-code-line-numbers .slidev-code,
208
- .slidev-layout.db-default .shiki-magic-move-container.slidev-code {
253
+ .slidev-layout.db-default .shiki-magic-move-container.slidev-code:has(.shiki-magic-move-line-number) {
209
254
  /* override the padding *variable* (Slidev applies padding via it, which beats
210
255
  a plain padding-left here). top right bottom left — only the left shrinks.
211
256
  The Magic Move container isn't wrapped in .slidev-code-line-numbers, so it
212
- needs the same override to keep its gutter rule aligned with plain blocks. */
257
+ needs the same override to keep its gutter rule aligned with plain blocks;
258
+ one without line numbers keeps the plain blocks' zero inset. */
213
259
  --slidev-code-padding: 4px 10px 4px 6px; /* less space left of the numbers (was 10px) */
214
260
  }
215
261
  .slidev-layout.db-default .slidev-code-line-numbers .slidev-code code .line::before {
@@ -251,13 +297,16 @@
251
297
  /* Optional caption above a code block.
252
298
  Authoring: ```python [What this snippet does] */
253
299
  .slidev-layout.db-default .slidev-code-block-title {
254
- padding: 3px 10px;
300
+ padding: 3px 0; /* flush with the code text below (see above) */
255
301
  background: transparent; /* drop Slidev's heavy gray title bar */
256
- font-size: 11px;
302
+ font-size: var(--db-fs-caption);
257
303
  font-style: italic;
258
304
  color: var(--db-text);
259
305
  opacity: 0.85;
260
306
  }
307
+ .slidev-layout.db-default .with-bg .slidev-code-block-title {
308
+ padding-inline: 10px; /* match the boxed code's inset */
309
+ }
261
310
 
262
311
  /* Optional table captions (need Comark, enabled by this theme).
263
312
  ABOVE the table: [Title]{.table-caption} -> black, non-italic
@@ -270,7 +319,7 @@
270
319
  .slidev-layout.db-default p:has(.table-caption),
271
320
  .slidev-layout.db-default .table-footer,
272
321
  .slidev-layout.db-default p:has(.table-footer) {
273
- font-size: 11px;
322
+ font-size: var(--db-fs-caption);
274
323
  background: transparent !important;
275
324
  }
276
325
  .slidev-layout.db-default .table-caption,
@@ -310,7 +359,7 @@
310
359
  .slidev-layout.db-default table {
311
360
  border-collapse: collapse;
312
361
  width: auto; /* default: hug content */
313
- font-size: 10px; /* 10pt (canvas 720 => px == pt) */
362
+ font-size: var(--db-fs-table); /* 10pt */
314
363
  }
315
364
  /* Width control: tables hug content by default. To pin a width, wrap in
316
365
  <div class="table-w" style="width: …"> — the table then fills that box, so
@@ -349,6 +398,7 @@
349
398
  border: none; /* no vertical/cell borders */
350
399
  padding: 2px 8px 4px; /* tight padding; bottom>top optically centers
351
400
  single-line text (nudges it up) */
401
+ line-height: 1.4; /* rows every 19.2pt (25.6px), as in the template */
352
402
  vertical-align: middle; /* center cell text vertically */
353
403
  text-align: center; /* center by default; :--/--: in markdown override */
354
404
  }
@@ -372,6 +422,12 @@
372
422
  .slidev-layout.db-default tbody tr:last-child td {
373
423
  border-bottom: 1px solid var(--db-table-rule);
374
424
  }
425
+ /* Footer page number (global-bottom.vue): 12pt like the template. */
426
+ .db-footer__page {
427
+ font-size: var(--db-fs-small);
428
+ color: var(--db-text);
429
+ }
430
+
375
431
  /* Inline code inside tables AND headings: keep the syntax colors but drop the
376
432
  gray "pill" background/padding so it reads like plain text. */
377
433
  .slidev-layout.db-default th code,
@@ -382,3 +438,20 @@
382
438
  background: transparent;
383
439
  padding: 0;
384
440
  }
441
+ /* In a heading, code takes the heading's size and weight (the template's
442
+ "1.1 DISTINCT" is bold mono), not the light, smaller inline-code style. */
443
+ .slidev-layout.db-default :is(h1, h2, h3) code {
444
+ font-size: 1em;
445
+ font-weight: inherit;
446
+ }
447
+
448
+ /* Inline math: KaTeX's default 1.21em makes capitals 15% taller than Noto
449
+ Sans ones, and its 1.2 line height stretches the bullet it sits in. At 1.05em
450
+ the capitals match; line-height 1 keeps a formula's bullet within 0.5px of its
451
+ neighbours. (Not 0: that misplaces overlays such as the slash of \notin.) */
452
+ .slidev-layout.db-default .katex {
453
+ font-size: 1.05em;
454
+ }
455
+ .slidev-layout.db-default :not(.katex-display) > .katex {
456
+ line-height: 1;
457
+ }
package/vite.config.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHighlighter } from 'shiki'
2
- import { setupTableCaptions } from './markdown'
2
+ import { setupTableCaptions } from './markdown.ts'
3
+ import { sqlKeywords } from './setup/shiki.ts'
3
4
 
4
5
  // Shipped WITH the theme: Slidev merges vite.config from every root (theme +
5
6
  // addons + deck), so installing this theme is enough — no need to copy a file.
@@ -9,15 +10,22 @@ import { setupTableCaptions } from './markdown'
9
10
  // The leading `{lang}` token selects the language; everything after the first
10
11
  // space is highlighted with Shiki and rendered inside a normal inline <code>.
11
12
  //
13
+ // Common SQL keywords/constants/functions (e.g. `NULL`, `TRUE`, `COUNT`) are
14
+ // automatically detected and highlighted with the theme's SQL grammar.
15
+ //
12
16
  // A deck can still override THEME, LANGS, or the rule by defining its own
13
17
  // vite.config.ts (the deck's config merges last and wins).
14
- //
15
- // NB: no `import ... from 'vite'` here on purpose — `defineConfig` is only a
16
- // type helper, and importing it would require `vite` to be resolvable from this
17
- // file's location (it isn't, when the theme is loaded from its own directory).
18
18
  const THEME = 'catppuccin-latte'
19
19
  const LANGS = ['json', 'yaml', 'sql', 'c', 'cpp', 'python']
20
20
 
21
+ const AUTO_SQL = /\b(TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|SELECT|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|JOIN|LEFT|RIGHT|INNER|OUTER|FULL|CROSS|ON|USING|LIMIT|OFFSET|UNION|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TABLE|INTO|VALUES|SET|BETWEEN|IN|NOT|AND|OR|IS|LIKE|ILIKE|AS|CASE|WHEN|THEN|ELSE|END)\b/i
22
+ const AUTO_SQL_OPS = /^(?:<=>|<>|<=|>=|!=|=|<|>)$/
23
+
24
+ function isAutoSql(code: string): boolean {
25
+ const trimmed = code.trim()
26
+ return AUTO_SQL_OPS.test(trimmed) || AUTO_SQL.test(trimmed)
27
+ }
28
+
21
29
  export default async () => {
22
30
  const highlighter = await createHighlighter({ themes: [THEME], langs: LANGS })
23
31
 
@@ -29,12 +37,42 @@ export default async () => {
29
37
  setupTableCaptions(md)
30
38
  const fallback = md.renderer.rules.code_inline
31
39
  md.renderer.rules.code_inline = (tokens: any[], idx: number, options: any, env: any, self: any) => {
32
- const m = String(tokens[idx].content).match(/^\{([\w-]+)\}\s+([\s\S]+)$/)
33
- if (m && highlighter.getLoadedLanguages().includes(m[1])) {
34
- const inner = highlighter
35
- .codeToHtml(m[2], { lang: m[1], theme: THEME })
40
+ const raw = String(tokens[idx].content)
41
+ const m = raw.match(/^\{([\w-]+)\}\s+([\s\S]+)$/)
42
+ let lang = m ? m[1] : null
43
+ let code = m ? m[2] : raw
44
+
45
+ // Auto-detect common SQL keywords/constants when untagged
46
+ if (!lang && isAutoSql(code)) {
47
+ lang = 'sql'
48
+ }
49
+
50
+ // Auto-detect function calls like `next()` or `child.next()` when untagged -> blue upright
51
+ const fnMatch = !lang && code.trim().match(/^((?:[a-zA-Z_]\w*\.)*)([a-zA-Z_]\w*)\s*(\(.*?\))$/)
52
+ if (fnMatch) {
53
+ const prefix = fnMatch[1]
54
+ const fnName = fnMatch[2]
55
+ const rest = fnMatch[3].replace(/ /g, '&nbsp;')
56
+ let inner = ''
57
+ if (prefix) {
58
+ inner += `<span style="color:#4C4F69;--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">${prefix}</span>`
59
+ }
60
+ inner += `<span style="color:#1E66F5;--shiki-light:#1E66F5;--shiki-dark:#89B4FA">${fnName}</span>`
61
+ inner += `<span style="color:#7C7F93;--shiki-light:#7C7F93;--shiki-dark:#9399B2">${rest}</span>`
62
+ return `<code class="db-inline-code">${inner}</code>`
63
+ }
64
+
65
+ if (lang && highlighter.getLoadedLanguages().includes(lang)) {
66
+ let inner = highlighter
67
+ .codeToHtml(code, { lang, theme: THEME, transformers: [sqlKeywords] })
36
68
  .replace(/^<pre[^>]*><code[^>]*>/, '')
37
69
  .replace(/<\/code><\/pre>\s*$/, '')
70
+
71
+ // Preserve all spaces across tokens so Vue's template compiler never condenses/strips them
72
+ inner = inner.replace(/<span([^>]*)>([^<]*)<\/span>/g, (_m, attrs, text) => {
73
+ return `<span${attrs}>${text.replace(/ /g, '&nbsp;')}</span>`
74
+ })
75
+
38
76
  return `<code class="db-inline-code">${inner}</code>`
39
77
  }
40
78
  return fallback