slidev-theme-practicum 0.1.0 → 0.1.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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -10
  3. package/components/Slide.vue +81 -26
  4. package/components/Slot.vue +33 -48
  5. package/components/Text.vue +68 -2
  6. package/components/Timeline.vue +2 -23
  7. package/composables/deck-slot-markup.cjs +249 -0
  8. package/composables/decor-catalog.mjs +169 -47
  9. package/composables/layout-authoring.ts +6 -2
  10. package/composables/layout-markdown.mjs +33 -0
  11. package/composables/layout-recipes.ts +51 -5
  12. package/composables/layout-shorthands.ts +251 -81
  13. package/composables/markdown-to-vnodes.cjs +1 -0
  14. package/composables/markdown-to-vnodes.mjs +237 -0
  15. package/composables/slot-placement.ts +1 -1
  16. package/composables/slot-spacing.ts +66 -0
  17. package/composables/text-fit-runtime.ts +458 -76
  18. package/composables/text-fit-scope.ts +13 -0
  19. package/composables/text-slot-markdown.mjs +193 -0
  20. package/composables/text-typography.mjs +2 -0
  21. package/composables/theme-assets.mjs +60 -0
  22. package/composables/timeline-grid.ts +27 -0
  23. package/composables/typescript-require.cjs +29 -0
  24. package/composables/validate-deck-layouts.cjs +277 -0
  25. package/decor-library.md +2 -0
  26. package/example.md +132 -10
  27. package/package.json +25 -11
  28. package/public/photos/photo-1.webp +0 -0
  29. package/public/photos/photo-10.webp +0 -0
  30. package/public/photos/photo-11.webp +0 -0
  31. package/public/photos/photo-12.webp +0 -0
  32. package/public/photos/photo-13.webp +0 -0
  33. package/public/photos/photo-14.webp +0 -0
  34. package/public/photos/photo-15.webp +0 -0
  35. package/public/photos/photo-16.webp +0 -0
  36. package/public/photos/photo-17.webp +0 -0
  37. package/public/photos/photo-18.webp +0 -0
  38. package/public/photos/photo-19.webp +0 -0
  39. package/public/photos/photo-2.webp +0 -0
  40. package/public/photos/photo-20.webp +0 -0
  41. package/public/photos/photo-21.webp +0 -0
  42. package/public/photos/photo-22.webp +0 -0
  43. package/public/photos/photo-23.webp +0 -0
  44. package/public/photos/photo-24.webp +0 -0
  45. package/public/photos/photo-25.webp +0 -0
  46. package/public/photos/photo-26.webp +0 -0
  47. package/public/photos/photo-27.webp +0 -0
  48. package/public/photos/photo-28.webp +0 -0
  49. package/public/photos/photo-29.webp +0 -0
  50. package/public/photos/photo-3.webp +0 -0
  51. package/public/photos/photo-30.webp +0 -0
  52. package/public/photos/photo-4.webp +0 -0
  53. package/public/photos/photo-5.webp +0 -0
  54. package/public/photos/photo-6.webp +0 -0
  55. package/public/photos/photo-7.webp +0 -0
  56. package/public/photos/photo-8.webp +0 -0
  57. package/public/photos/photo-9.webp +0 -0
  58. package/scripts/browser-smoke-runtime.mjs +428 -0
  59. package/scripts/browser-smoke.mjs +424 -0
  60. package/scripts/build-artifact-assets.mjs +55 -0
  61. package/scripts/check-build-artifact.mjs +243 -0
  62. package/scripts/check-package.mjs +132 -0
  63. package/scripts/check-test-architecture.mjs +176 -0
  64. package/scripts/npm-spawn.mjs +31 -0
  65. package/scripts/test-architecture-policy.mjs +245 -0
  66. package/scripts/theme-asset-inventory.mjs +83 -0
  67. package/scripts/validate-deck.cjs +18 -0
  68. package/setup/decor-save-middleware.ts +244 -0
  69. package/setup/vite-plugins.ts +14 -84
  70. package/skills/slidev-practicum/SKILL.md +25 -0
  71. package/skills/slidev-practicum/agents/openai.yaml +4 -0
  72. package/styles/index.css +10 -1
  73. package/composables/layout-registry.ts +0 -47
  74. package/env.d.ts +0 -6
  75. package/public/photos/photo-1.png +0 -0
  76. package/public/photos/photo-10.png +0 -0
  77. package/public/photos/photo-2.png +0 -0
  78. package/public/photos/photo-3.png +0 -0
  79. package/public/photos/photo-4.png +0 -0
  80. package/public/photos/photo-5.png +0 -0
  81. package/public/photos/photo-6.png +0 -0
  82. package/public/photos/photo-7.png +0 -0
  83. package/public/photos/photo-8.png +0 -0
  84. package/public/photos/photo-9.png +0 -0
  85. package/slidev-theme-practicum.png +0 -0
  86. package/slidev-theme-practicum.svg +0 -102
@@ -0,0 +1,249 @@
1
+ const MarkdownIt = require('markdown-it')
2
+ const { parse } = require('vue/compiler-sfc')
3
+
4
+ /** @typedef {import('@vue/compiler-core').TemplateChildNode} TemplateChildNode */
5
+ /** @typedef {{ role: string, hasCoordinates: boolean, label: string }} ExplicitSlotIntent */
6
+ /**
7
+ * @typedef {(
8
+ * | { kind: 'comment' }
9
+ * | { kind: 'text', content: string }
10
+ * | { kind: 'element', tag: string, props: Record<string, unknown>, children: DeckLiveNode[] }
11
+ * )} DeckLiveNode
12
+ */
13
+ /** @typedef {{ children: DeckLiveNode[], intents: ExplicitSlotIntent[] }} DeckLiveStructure */
14
+
15
+ const SLOT_CONTRACT_PROPS = new Set(['role', 'area', 'col', 'row'])
16
+ const SLOT_CONTROL_FLOW_DIRECTIVES = new Set(['if', 'else-if', 'else', 'for'])
17
+ const markdown = new MarkdownIt({ html: true })
18
+
19
+ /** @param {unknown} error */
20
+ function formatCompilerError(error) {
21
+ return error instanceof Error ? error.message : String(error)
22
+ }
23
+
24
+ /**
25
+ * @param {import('@vue/compiler-core').DirectiveNode} prop
26
+ * @returns {unknown}
27
+ */
28
+ function readLiteralBinding(prop) {
29
+ const ast = prop.exp?.ast
30
+ if (!ast)
31
+ return undefined
32
+
33
+ if (ast.type === 'StringLiteral'
34
+ || ast.type === 'NumericLiteral'
35
+ || ast.type === 'BooleanLiteral') {
36
+ return ast.value
37
+ }
38
+
39
+ if (ast.type === 'NullLiteral')
40
+ return null
41
+
42
+ if (ast.type === 'TemplateLiteral' && ast.expressions.length === 0)
43
+ return ast.quasis[0]?.value?.cooked ?? ''
44
+
45
+ return undefined
46
+ }
47
+
48
+ /** @param {string} directive */
49
+ function failSlotControlFlow(directive) {
50
+ const error = /** @type {Error & { hint: string }} */ (
51
+ new Error(
52
+ `[DeckValidator] Slot не поддерживает v-${directive} `
53
+ + 'на самом компоненте или его предке.',
54
+ )
55
+ )
56
+ error.hint = 'Разверните конечное количество Slot явно, чтобы validator мог проверить role и capacity.'
57
+ throw error
58
+ }
59
+
60
+ /** @param {import('@vue/compiler-core').ElementNode} node */
61
+ function controlFlowDirective(node) {
62
+ return node.props.find(prop =>
63
+ prop.type === 7
64
+ && SLOT_CONTROL_FLOW_DIRECTIVES.has(prop.name),
65
+ )?.name ?? ''
66
+ }
67
+
68
+ /** @param {TemplateChildNode} node */
69
+ function containsSlot(node) {
70
+ if (node.type !== 1)
71
+ return false
72
+
73
+ return node.tag === 'Slot' || node.children.some(containsSlot)
74
+ }
75
+
76
+ /**
77
+ * @param {import('@vue/compiler-core').ElementNode} node
78
+ * @returns {Record<string, unknown>}
79
+ */
80
+ function elementProps(node) {
81
+ /** @type {Record<string, unknown>} */
82
+ const props = {}
83
+
84
+ for (const prop of node.props) {
85
+ if (prop.type === 6) {
86
+ props[prop.name] = prop.value?.content ?? ''
87
+ continue
88
+ }
89
+
90
+ if (prop.name !== 'bind')
91
+ continue
92
+
93
+ const key = prop.arg?.type === 4 && prop.arg.isStatic
94
+ ? prop.arg.content
95
+ : ''
96
+
97
+ if (node.tag !== 'Slot'
98
+ || (!SLOT_CONTRACT_PROPS.has(key) && key !== 'label' && key)) {
99
+ continue
100
+ }
101
+
102
+ if (!key) {
103
+ throw new Error(
104
+ '[DeckValidator] Slot не поддерживает динамический v-bind без статического имени prop.',
105
+ )
106
+ }
107
+
108
+ const value = readLiteralBinding(prop)
109
+ if (key === 'label' && value === undefined)
110
+ continue
111
+
112
+ if (value === undefined) {
113
+ throw new Error(
114
+ `[DeckValidator] Slot не поддерживает динамический v-bind:${key}; укажите литеральное значение.`,
115
+ )
116
+ }
117
+
118
+ props[key] = value
119
+ }
120
+
121
+ return props
122
+ }
123
+
124
+ /**
125
+ * @param {TemplateChildNode} node
126
+ * @returns {DeckLiveNode | null}
127
+ */
128
+ function liveNode(node) {
129
+ if (node.type === 1) {
130
+ const directive = controlFlowDirective(node)
131
+ if (directive && containsSlot(node))
132
+ failSlotControlFlow(directive)
133
+
134
+ return {
135
+ kind: 'element',
136
+ tag: node.tag,
137
+ props: elementProps(node),
138
+ children: node.children
139
+ .map(liveNode)
140
+ .filter(isLiveNode),
141
+ }
142
+ }
143
+
144
+ if (node.type === 2)
145
+ return { kind: 'text', content: node.content }
146
+
147
+ if (node.type === 3)
148
+ return { kind: 'comment' }
149
+
150
+ if (node.type === 5)
151
+ return { kind: 'text', content: `{{ ${node.content.loc.source} }}` }
152
+
153
+ return null
154
+ }
155
+
156
+ /**
157
+ * @param {DeckLiveNode | null} node
158
+ * @returns {node is DeckLiveNode}
159
+ */
160
+ function isLiveNode(node) {
161
+ return node !== null
162
+ }
163
+
164
+ /**
165
+ * @param {DeckLiveNode} node
166
+ * @returns {ExplicitSlotIntent[]}
167
+ */
168
+ function collectSlotIntents(node) {
169
+ const nested = node.kind === 'element'
170
+ ? node.children.flatMap(collectSlotIntents)
171
+ : []
172
+
173
+ if (node.kind !== 'element' || node.tag !== 'Slot')
174
+ return nested
175
+
176
+ const role = String(node.props.role ?? '').trim()
177
+ const label = String(node.props.label ?? '').trim()
178
+ const hasCoordinates = ['area', 'col', 'row']
179
+ .some(key => Boolean(String(node.props[key] ?? '').trim()))
180
+
181
+ return [{ role, hasCoordinates, label }, ...nested]
182
+ }
183
+
184
+ /**
185
+ * MarkdownIt оборачивает одиночные inline HTML components в `<p>`. Slidev
186
+ * поднимает component-only параграф обратно в default slot, поэтому validator
187
+ * нормализует ту же границу.
188
+ *
189
+ * @param {DeckLiveNode} node
190
+ * @returns {DeckLiveNode[]}
191
+ */
192
+ function normalizeTopLevelNode(node) {
193
+ if (node.kind !== 'element' || node.tag !== 'p')
194
+ return [node]
195
+
196
+ const meaningful = node.children.filter(child =>
197
+ child.kind !== 'comment'
198
+ && !(child.kind === 'text' && !child.content.trim()),
199
+ )
200
+
201
+ if (meaningful.length > 0
202
+ && meaningful.every(child => child.kind === 'element' && child.tag === 'Slot')) {
203
+ return meaningful
204
+ }
205
+
206
+ return [node]
207
+ }
208
+
209
+ /**
210
+ * Компилирует ровно ту top-level структуру, которую Slidev передаст layout-компоненту:
211
+ * Markdown уже превращён в HTML, а live Vue-компоненты сохранены как элементы.
212
+ *
213
+ * @param {string} content
214
+ * @returns {DeckLiveStructure}
215
+ */
216
+ function extractDeckLiveStructure(content) {
217
+ const rendered = markdown.render(String(content))
218
+ const source = `<template>${rendered}</template>`
219
+ const { descriptor, errors } = parse(source, { filename: 'deck-slide.vue' })
220
+
221
+ if (errors.length) {
222
+ throw new Error(
223
+ `[DeckValidator] Не удалось разобрать live markup слайда: ${errors.map(formatCompilerError).join('; ')}`,
224
+ )
225
+ }
226
+
227
+ const children = (descriptor.template?.ast?.children ?? [])
228
+ .map(liveNode)
229
+ .filter(isLiveNode)
230
+ .flatMap(normalizeTopLevelNode)
231
+
232
+ return {
233
+ children,
234
+ intents: children.flatMap(collectSlotIntents),
235
+ }
236
+ }
237
+
238
+ /**
239
+ * @param {string} content
240
+ * @returns {ExplicitSlotIntent[]}
241
+ */
242
+ function extractExplicitSlotIntents(content) {
243
+ return extractDeckLiveStructure(content).intents
244
+ }
245
+
246
+ module.exports = {
247
+ extractDeckLiveStructure,
248
+ extractExplicitSlotIntents,
249
+ }
@@ -1,3 +1,5 @@
1
+ import { themeDecorPath, themePhotoPath } from './theme-assets.mjs'
2
+
1
3
  const PHOTO_DECOR_SIZES = Object.freeze([
2
4
  { cols: [4, 6], rows: [4, 6], ratio: [0.85, 1.2] },
3
5
  { cols: [5, 12], rows: [3, 8], ratio: [1.15, 4] },
@@ -12,7 +14,7 @@ const PHOTO_DECOR_SIZES = Object.freeze([
12
14
  function photoDecor(number, meanings, tones, image = {}) {
13
15
  return {
14
16
  id: `decor-photo-${String(number).padStart(2, '0')}`,
15
- src: `/photos/photo-${number}.png`,
17
+ src: themePhotoPath(number),
16
18
  sizes: PHOTO_DECOR_SIZES,
17
19
  meanings: ['photo', ...meanings],
18
20
  tones,
@@ -28,7 +30,7 @@ function photoDecor(number, meanings, tones, image = {}) {
28
30
  export const DEFAULT_DECOR_DATA = Object.freeze([
29
31
  {
30
32
  id: 'decor-cover-band-01',
31
- src: '/decor/decor-1.svg',
33
+ src: themeDecorPath('decor-1.svg'),
32
34
  sizes: [{ cols: [3, 8], rows: [2, 5], ratio: [1, 1.5] }],
33
35
  meanings: ['cover', 'hero', 'course'],
34
36
  tones: ['green', 'orange', 'blue'],
@@ -40,7 +42,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
40
42
  },
41
43
  {
42
44
  id: 'decor-support-raster-01',
43
- src: '/decor/decor-2.png',
45
+ src: themeDecorPath('decor-2.png'),
44
46
  sizes: [{ cols: [2, 6], rows: [2, 6], ratio: [0.75, 1.2] }],
45
47
  meanings: ['support', 'media'],
46
48
  tones: ['orange', 'light', 'warm'],
@@ -53,7 +55,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
53
55
  },
54
56
  {
55
57
  id: 'decor-square-orbit-01',
56
- src: '/decor/decor-3.svg',
58
+ src: themeDecorPath('decor-3.svg'),
57
59
  sizes: [{ cols: [3, 5], rows: [3, 5], ratio: [0.8, 1.25] }],
58
60
  meanings: ['qa', 'testing', 'practice'],
59
61
  tones: ['blue', 'orange', 'green'],
@@ -65,7 +67,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
65
67
  },
66
68
  {
67
69
  id: 'decor-support-wide-01',
68
- src: '/decor/decor-4.svg',
70
+ src: themeDecorPath('decor-4.svg'),
69
71
  sizes: [{ cols: [4, 8], rows: [2, 8], ratio: [1, 4] }],
70
72
  meanings: ['support', 'learning'],
71
73
  tones: ['green', 'blue', 'orange'],
@@ -77,7 +79,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
77
79
  },
78
80
  {
79
81
  id: 'decor-support-raster-02',
80
- src: '/decor/decor-5.png',
82
+ src: themeDecorPath('decor-5.png'),
81
83
  sizes: [
82
84
  { cols: 6, rows: 5 },
83
85
  { cols: 6, rows: 6 },
@@ -97,7 +99,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
97
99
  },
98
100
  {
99
101
  id: 'decor-support-raster-03',
100
- src: '/decor/decor-6.png',
102
+ src: themeDecorPath('decor-6.png'),
101
103
  sizes: [{ cols: [3, 5], rows: [3, 5], ratio: [0.8, 1.25] }],
102
104
  meanings: ['support', 'media'],
103
105
  tones: ['orange', 'warm'],
@@ -109,7 +111,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
109
111
  },
110
112
  {
111
113
  id: 'decor-media-square-01',
112
- src: '/decor/decor-7.png',
114
+ src: themeDecorPath('decor-7.png'),
113
115
  sizes: [{ cols: [3, 5], rows: [3, 5], ratio: [0.8, 1.25] }],
114
116
  meanings: ['media', 'practice', 'support'],
115
117
  tones: ['blue', 'orange', 'green'],
@@ -122,7 +124,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
122
124
  },
123
125
  {
124
126
  id: 'decor-cover-vertical-01',
125
- src: '/decor/decor-8.svg',
127
+ src: themeDecorPath('decor-8.svg'),
126
128
  sizes: [{ cols: [2, 4], rows: [3, 8], ratio: [0.25, 0.95] }],
127
129
  meanings: ['cover', 'hero', 'course'],
128
130
  tones: ['orange', 'green', 'blue'],
@@ -134,7 +136,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
134
136
  },
135
137
  {
136
138
  id: 'decor-cover-ribbon-01',
137
- src: '/decor/decor-9.svg',
139
+ src: themeDecorPath('decor-9.svg'),
138
140
  sizes: [{ cols: [4, 8], rows: [2, 4], ratio: [1.4, 4] }],
139
141
  meanings: ['cover', 'hero', 'support'],
140
142
  tones: ['orange', 'blue', 'green'],
@@ -146,7 +148,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
146
148
  },
147
149
  {
148
150
  id: 'decor-rules-wide-01',
149
- src: '/decor/decor-10.svg',
151
+ src: themeDecorPath('decor-10.svg'),
150
152
  sizes: [{ cols: [4, 12], rows: [3, 6], ratio: [1.4, 4] }],
151
153
  meanings: ['rules', 'webinar', 'qa'],
152
154
  tones: ['green', 'dark', 'blue', 'orange'],
@@ -163,7 +165,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
163
165
  },
164
166
  {
165
167
  id: 'decor-agenda-tall-01',
166
- src: '/decor/decor-11.svg',
168
+ src: themeDecorPath('decor-11.svg'),
167
169
  sizes: [{ cols: [3, 4], rows: [8, 12], ratio: [0.25, 0.55] }],
168
170
  meanings: ['agenda', 'toc'],
169
171
  tones: ['blue', 'orange', 'green', 'dark'],
@@ -176,7 +178,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
176
178
  },
177
179
  {
178
180
  id: 'decor-signal-tall-01',
179
- src: '/decor/decor-12.svg',
181
+ src: themeDecorPath('decor-12.svg'),
180
182
  sizes: [{ cols: [2, 4], rows: [4, 12], ratio: [0.2, 0.8] }],
181
183
  meanings: ['signal', 'cover', 'hero'],
182
184
  tones: ['blue', 'orange', 'green'],
@@ -188,7 +190,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
188
190
  },
189
191
  {
190
192
  id: 'decor-support-compact-01',
191
- src: '/decor/decor-13.svg',
193
+ src: themeDecorPath('decor-13.svg'),
192
194
  sizes: [{ cols: [2, 6], rows: [2, 6], ratio: [0.75, 1.35] }],
193
195
  meanings: ['support', 'practice'],
194
196
  tones: ['green', 'blue', 'orange'],
@@ -200,7 +202,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
200
202
  },
201
203
  {
202
204
  id: 'decor-panel-vertical-01',
203
- src: '/decor/decor-14.svg',
205
+ src: themeDecorPath('decor-14.svg'),
204
206
  sizes: [{ cols: [3, 5], rows: [4, 7], ratio: [0.45, 0.95] }],
205
207
  meanings: ['support', 'rules', 'qa'],
206
208
  tones: ['orange', 'blue', 'green'],
@@ -214,7 +216,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
214
216
  },
215
217
  {
216
218
  id: 'decor-support-wide-02',
217
- src: '/decor/decor-15.svg',
219
+ src: themeDecorPath('decor-15.svg'),
218
220
  sizes: [{ cols: [4, 8], rows: [2, 5], ratio: [1.2, 4] }],
219
221
  meanings: ['support', 'learning'],
220
222
  tones: ['orange', 'green', 'blue'],
@@ -226,7 +228,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
226
228
  },
227
229
  {
228
230
  id: 'decor-support-wide-03',
229
- src: '/decor/decor-16.svg',
231
+ src: themeDecorPath('decor-16.svg'),
230
232
  sizes: [{ cols: [5, 9], rows: [2, 4], ratio: [1.4, 4.5] }],
231
233
  meanings: ['support', 'learning'],
232
234
  tones: ['blue', 'green', 'orange'],
@@ -238,7 +240,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
238
240
  },
239
241
  {
240
242
  id: 'decor-media-square-02',
241
- src: '/decor/decor-17.png',
243
+ src: themeDecorPath('decor-17.png'),
242
244
  sizes: [{ cols: [3, 5], rows: [3, 5], ratio: [0.8, 1.25] }],
243
245
  meanings: ['media', 'support'],
244
246
  tones: ['green', 'orange', 'blue'],
@@ -251,7 +253,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
251
253
  },
252
254
  {
253
255
  id: 'decor-media-square-03',
254
- src: '/decor/decor-18.png',
256
+ src: themeDecorPath('decor-18.png'),
255
257
  sizes: [{ cols: [3, 6], rows: [3, 6], ratio: [0.75, 1.35] }],
256
258
  meanings: ['media', 'support'],
257
259
  tones: ['orange', 'blue', 'green'],
@@ -265,7 +267,7 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
265
267
  },
266
268
  {
267
269
  id: 'decor-support-tall-01',
268
- src: '/decor/decor-19.svg',
270
+ src: themeDecorPath('decor-19.svg'),
269
271
  sizes: [{ cols: [3, 5], rows: [3, 9], ratio: [0.35, 1.1] }],
270
272
  meanings: ['support', 'practice'],
271
273
  tones: ['blue', 'orange', 'green'],
@@ -277,62 +279,182 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
277
279
  photoDecor(
278
280
  1,
279
281
  ['portrait', 'workspace', 'home', 'learning'],
280
- ['blue', 'light', 'neutral', 'yellow'],
282
+ ['orange', 'yellow', 'light', 'neutral', 'warm'],
281
283
  { position: '50% 44%', zoom: 1.06 },
282
284
  ),
283
285
  photoDecor(
284
286
  2,
285
- ['portrait', 'workspace', 'learning'],
286
- ['blue', 'green', 'dark', 'cool'],
287
+ ['portrait', 'workspace', 'outdoor', 'learning'],
288
+ ['orange', 'blue', 'green', 'yellow', 'warm'],
287
289
  { position: '50% 42%', zoom: 1.06 },
288
290
  ),
289
291
  photoDecor(
290
292
  3,
291
- ['portrait', 'home', 'learning'],
292
- ['orange', 'yellow', 'green', 'dark', 'warm'],
293
- { position: '52% 45%', zoom: 1.04 },
293
+ ['portrait', 'workspace', 'office', 'learning'],
294
+ ['orange', 'yellow', 'dark', 'warm', 'neutral'],
295
+ { position: '50% 44%', zoom: 1.04 },
294
296
  ),
295
297
  photoDecor(
296
298
  4,
297
- ['portrait', 'workspace', 'learning'],
298
- ['blue', 'dark', 'neutral'],
299
- { position: '50% 44%', zoom: 1.06 },
299
+ ['portrait', 'transit', 'workspace', 'learning'],
300
+ ['blue', 'green', 'light', 'yellow', 'cool'],
301
+ { position: '42% 42%', zoom: 1.06 },
300
302
  ),
301
303
  photoDecor(
302
304
  5,
303
- ['portrait', 'outdoor', 'learning'],
304
- ['blue', 'green', 'light', 'yellow'],
305
- { position: '48% 44%', zoom: 1.08 },
305
+ ['portrait', 'workshop', 'outdoor', 'learning'],
306
+ ['blue', 'dark', 'cool', 'neutral'],
307
+ { position: '50% 40%', zoom: 1.08 },
306
308
  ),
307
309
  photoDecor(
308
310
  6,
309
- ['portrait', 'home', 'learning'],
310
- ['green', 'light', 'yellow', 'neutral'],
311
- { position: '50% 40%', zoom: 1.04 },
311
+ ['portrait', 'workspace', 'home', 'learning'],
312
+ ['blue', 'dark', 'cool', 'neutral'],
313
+ { position: '50% 44%', zoom: 1.04 },
312
314
  ),
313
315
  photoDecor(
314
316
  7,
315
- ['portrait', 'home', 'learning'],
316
- ['orange', 'blue', 'yellow', 'dark', 'warm'],
317
- { position: '50% 0%', zoom: 1.2 },
317
+ ['portrait', 'workspace', 'outdoor', 'learning'],
318
+ ['green', 'yellow', 'neutral', 'warm'],
319
+ { position: '50% 45%', zoom: 1.04 },
318
320
  ),
319
321
  photoDecor(
320
322
  8,
321
- ['portrait', 'campus', 'learning'],
322
- ['orange', 'yellow', 'green', 'neutral', 'warm'],
323
- { position: '50% 45%', zoom: 1.06 },
323
+ ['workspace', 'programming', 'home', 'learning'],
324
+ ['orange', 'yellow', 'light', 'neutral', 'warm'],
325
+ { position: '50% 44%', zoom: 1.02 },
324
326
  ),
325
327
  photoDecor(
326
328
  9,
327
- ['portrait', 'home', 'learning'],
328
- ['blue', 'light', 'neutral', 'green'],
329
- { position: '50% 45%', zoom: 1.05 },
329
+ ['portrait', 'workspace', 'learning'],
330
+ ['orange', 'yellow', 'light', 'neutral', 'warm'],
331
+ { position: '50% 44%', zoom: 1.05 },
330
332
  ),
331
333
  photoDecor(
332
334
  10,
333
- ['portrait', 'home', 'learning'],
334
- ['orange', 'yellow', 'dark', 'warm'],
335
- { position: '50% 46%', zoom: 1.08 },
335
+ ['portrait', 'outdoor', 'mobile', 'learning'],
336
+ ['orange', 'green', 'yellow', 'warm', 'neutral'],
337
+ { position: '50% 44%', zoom: 1.05 },
338
+ ),
339
+ photoDecor(
340
+ 11,
341
+ ['portrait', 'outdoor', 'mobile', 'learning'],
342
+ ['blue', 'dark', 'cool', 'neutral'],
343
+ { position: '50% 42%', zoom: 1.06 },
344
+ ),
345
+ photoDecor(
346
+ 12,
347
+ ['portrait', 'outdoor', 'mobile', 'learning'],
348
+ ['green', 'blue', 'yellow', 'cool', 'neutral'],
349
+ { position: '50% 44%', zoom: 1.06 },
350
+ ),
351
+ photoDecor(
352
+ 13,
353
+ ['portrait', 'outdoor', 'mobile', 'learning'],
354
+ ['blue', 'light', 'cool', 'neutral'],
355
+ { position: '50% 43%', zoom: 1.06 },
356
+ ),
357
+ photoDecor(
358
+ 14,
359
+ ['portrait', 'outdoor', 'hero'],
360
+ ['blue', 'green', 'light', 'cool', 'neutral'],
361
+ { position: '50% 42%', zoom: 1.05 },
362
+ ),
363
+ photoDecor(
364
+ 15,
365
+ ['portrait', 'outdoor', 'hero'],
366
+ ['orange', 'blue', 'warm', 'light'],
367
+ { position: '50% 47%', zoom: 1.04 },
368
+ ),
369
+ photoDecor(
370
+ 16,
371
+ ['portrait', 'outdoor', 'hero'],
372
+ ['blue', 'dark', 'cool'],
373
+ { position: '50% 45%', zoom: 1.04 },
374
+ ),
375
+ photoDecor(
376
+ 17,
377
+ ['portrait', 'outdoor', 'mobile', 'learning'],
378
+ ['blue', 'green', 'cool', 'neutral'],
379
+ { position: '50% 44%', zoom: 1.04 },
380
+ ),
381
+ photoDecor(
382
+ 18,
383
+ ['portrait', 'outdoor', 'mobile', 'community'],
384
+ ['blue', 'green', 'neutral', 'cool'],
385
+ { position: '50% 45%', zoom: 1.04 },
386
+ ),
387
+ photoDecor(
388
+ 19,
389
+ ['portrait', 'outdoor', 'learning'],
390
+ ['blue', 'light', 'cool', 'neutral'],
391
+ { position: '50% 45%', zoom: 1.06 },
392
+ ),
393
+ photoDecor(
394
+ 20,
395
+ ['group', 'community', 'collaboration'],
396
+ ['orange', 'warm', 'light', 'neutral'],
397
+ { position: '50% 45%', zoom: 1.05 },
398
+ ),
399
+ photoDecor(
400
+ 21,
401
+ ['group', 'workspace', 'collaboration', 'learning'],
402
+ ['orange', 'warm', 'light', 'neutral'],
403
+ { position: '50% 45%', zoom: 1.03 },
404
+ ),
405
+ photoDecor(
406
+ 22,
407
+ ['group', 'community', 'outdoor'],
408
+ ['blue', 'green', 'light', 'cool', 'neutral'],
409
+ { position: '50% 43%', zoom: 1.04 },
410
+ ),
411
+ photoDecor(
412
+ 23,
413
+ ['group', 'community', 'outdoor'],
414
+ ['blue', 'light', 'cool', 'neutral'],
415
+ { position: '50% 46%', zoom: 1.04 },
416
+ ),
417
+ photoDecor(
418
+ 24,
419
+ ['group', 'community', 'collaboration'],
420
+ ['green', 'orange', 'warm', 'light', 'neutral'],
421
+ { position: '50% 45%', zoom: 1.04 },
422
+ ),
423
+ photoDecor(
424
+ 25,
425
+ ['group', 'workshop', 'collaboration', 'learning'],
426
+ ['green', 'orange', 'warm', 'light'],
427
+ { position: '50% 44%', zoom: 1.03 },
428
+ ),
429
+ photoDecor(
430
+ 26,
431
+ ['group', 'community', 'office', 'collaboration'],
432
+ ['blue', 'green', 'light', 'neutral', 'cool'],
433
+ { position: '50% 45%', zoom: 1.04 },
434
+ ),
435
+ photoDecor(
436
+ 27,
437
+ ['group', 'community', 'mobile', 'outdoor'],
438
+ ['blue', 'light', 'cool', 'neutral'],
439
+ { position: '50% 46%', zoom: 1.04 },
440
+ ),
441
+ photoDecor(
442
+ 28,
443
+ ['portrait', 'workspace', 'office', 'learning'],
444
+ ['light', 'neutral', 'orange', 'warm'],
445
+ { position: '50% 45%', zoom: 1.05 },
446
+ ),
447
+ photoDecor(
448
+ 29,
449
+ ['portrait', 'outdoor', 'hero'],
450
+ ['orange', 'blue', 'green', 'warm', 'light'],
451
+ { position: '50% 45%', zoom: 1.06 },
452
+ ),
453
+ photoDecor(
454
+ 30,
455
+ ['portrait', 'outdoor', 'hero'],
456
+ ['blue', 'orange', 'cool', 'neutral'],
457
+ { position: '62% 42%', zoom: 1.08 },
336
458
  ),
337
459
  ])
338
460
 
@@ -10,12 +10,16 @@ import {
10
10
  import {
11
11
  resolveLayoutSpec,
12
12
  resolveLayoutVariant,
13
+ resolveLayoutRecipe,
14
+ resolveLayoutSlotProps,
15
+ THEME_LAYOUT_ROLES,
13
16
  type ThemeLayout,
14
17
  type ThemeLayoutHeader,
15
18
  type ThemeLayoutRole,
19
+ type ThemeLayoutRecipe,
20
+ type ThemeLayoutSpec,
16
21
  type ThemeLayoutSlotSpec,
17
- } from './layout-registry'
18
- import { resolveLayoutRecipe, resolveLayoutSlotProps, THEME_LAYOUT_ROLES, type ThemeLayoutRecipe, type ThemeLayoutSpec } from './layout-recipes'
22
+ } from './layout-recipes'
19
23
  import { isBlankTextVNode, isCommentVNode } from './layout-vnode'
20
24
  import { createSlotRules, type SlotRules } from './slot-rules'
21
25
  import {
@@ -1,3 +1,36 @@
1
+ const INLINE_WHITESPACE = /[ \t\r\n\f\v]+/g
2
+
3
+ /**
4
+ * @param {string} value
5
+ */
6
+ export function normalizeInlineText(value) {
7
+ return value.replace(INLINE_WHITESPACE, ' ').trim()
8
+ }
9
+
10
+ /**
11
+ * @param {string} source
12
+ */
13
+ export function dedentMarkdownSource(source) {
14
+ const lines = String(source ?? '').replace(/\r\n/g, '\n').split('\n')
15
+ const nonEmpty = lines.filter(line => line.trim())
16
+
17
+ if (!nonEmpty.length)
18
+ return ''
19
+
20
+ const minIndent = Math.min(...nonEmpty.map((line) => {
21
+ const match = line.match(/^(\s*)/)
22
+ return match ? match[1].length : 0
23
+ }))
24
+
25
+ if (!minIndent)
26
+ return source.trim()
27
+
28
+ return lines
29
+ .map(line => (line.trim() ? line.slice(minIndent) : line))
30
+ .join('\n')
31
+ .trim()
32
+ }
33
+
1
34
  /**
2
35
  * @param {unknown} input
3
36
  * @returns {{ level: number, text: string } | null}