slidev-theme-practicum 0.1.0

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 (88) hide show
  1. package/README.md +270 -0
  2. package/components/DebugGrid.vue +50 -0
  3. package/components/DecorLibrary.vue +1117 -0
  4. package/components/Header.vue +124 -0
  5. package/components/Image.vue +82 -0
  6. package/components/ImageRenderer.vue +126 -0
  7. package/components/Logo.vue +142 -0
  8. package/components/Person.vue +89 -0
  9. package/components/Slide.vue +324 -0
  10. package/components/Slot.vue +419 -0
  11. package/components/Text.vue +216 -0
  12. package/components/Timeline.vue +202 -0
  13. package/composables/decor-catalog.mjs +339 -0
  14. package/composables/decor-config.mjs +488 -0
  15. package/composables/decor-file-store.ts +192 -0
  16. package/composables/decor-http-store.ts +25 -0
  17. package/composables/decor-store.ts +39 -0
  18. package/composables/decor-tuning-overrides.mjs +2 -0
  19. package/composables/decor-workbench.ts +799 -0
  20. package/composables/grid-placement.ts +396 -0
  21. package/composables/image-config.mjs +484 -0
  22. package/composables/layout-authoring.ts +372 -0
  23. package/composables/layout-markdown.mjs +56 -0
  24. package/composables/layout-recipes.ts +347 -0
  25. package/composables/layout-registry.ts +47 -0
  26. package/composables/layout-shorthands.ts +891 -0
  27. package/composables/layout-vnode.ts +43 -0
  28. package/composables/logo-mark.mjs +5 -0
  29. package/composables/slide-layout.ts +43 -0
  30. package/composables/slot-placement.ts +195 -0
  31. package/composables/slot-rules.ts +147 -0
  32. package/composables/text-fit-runtime.ts +577 -0
  33. package/composables/text-priority-fit.mjs +166 -0
  34. package/composables/text-typography.mjs +138 -0
  35. package/composables/theme-foundation.ts +192 -0
  36. package/composables/theme-media.mjs +202 -0
  37. package/composables/use-theme-config.ts +71 -0
  38. package/decor-library.md +10 -0
  39. package/env.d.ts +6 -0
  40. package/example.md +786 -0
  41. package/layouts/collection.vue +5 -0
  42. package/layouts/cover.vue +5 -0
  43. package/layouts/default.vue +5 -0
  44. package/layouts/explainer.vue +5 -0
  45. package/layouts/message.vue +5 -0
  46. package/layouts/none.vue +5 -0
  47. package/layouts/section.vue +5 -0
  48. package/layouts/two-cols.vue +11 -0
  49. package/package.json +84 -0
  50. package/public/decor/decor-1.svg +83 -0
  51. package/public/decor/decor-10.svg +40 -0
  52. package/public/decor/decor-11.svg +162 -0
  53. package/public/decor/decor-12.svg +83 -0
  54. package/public/decor/decor-13.svg +5 -0
  55. package/public/decor/decor-14.svg +7 -0
  56. package/public/decor/decor-15.svg +10 -0
  57. package/public/decor/decor-16.svg +9 -0
  58. package/public/decor/decor-17.png +0 -0
  59. package/public/decor/decor-18.png +0 -0
  60. package/public/decor/decor-19.svg +5 -0
  61. package/public/decor/decor-2.png +0 -0
  62. package/public/decor/decor-3.svg +9 -0
  63. package/public/decor/decor-4.svg +31 -0
  64. package/public/decor/decor-5.png +0 -0
  65. package/public/decor/decor-6.png +0 -0
  66. package/public/decor/decor-7.png +0 -0
  67. package/public/decor/decor-8.svg +21 -0
  68. package/public/decor/decor-9.svg +38 -0
  69. package/public/favicon.svg +11 -0
  70. package/public/photos/photo-1.png +0 -0
  71. package/public/photos/photo-10.png +0 -0
  72. package/public/photos/photo-2.png +0 -0
  73. package/public/photos/photo-3.png +0 -0
  74. package/public/photos/photo-4.png +0 -0
  75. package/public/photos/photo-5.png +0 -0
  76. package/public/photos/photo-6.png +0 -0
  77. package/public/photos/photo-7.png +0 -0
  78. package/public/photos/photo-8.png +0 -0
  79. package/public/photos/photo-9.png +0 -0
  80. package/setup/main.ts +3 -0
  81. package/setup/shiki.ts +10 -0
  82. package/setup/vite-plugins.ts +106 -0
  83. package/slidev-theme-practicum.png +0 -0
  84. package/slidev-theme-practicum.svg +102 -0
  85. package/styles/example.css +0 -0
  86. package/styles/index.css +192 -0
  87. package/styles/vars.css +119 -0
  88. package/types/slidev-client.d.ts +15 -0
@@ -0,0 +1,43 @@
1
+ import { Comment, Text as NativeText, type VNode } from 'vue'
2
+
3
+ export function camelToKebab(key: string) {
4
+ return key.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
5
+ }
6
+
7
+ export function readVNodeProp<T = unknown>(node: VNode | null | undefined, key: string) {
8
+ const nodeProps = (node?.props ?? {}) as Record<string, T | undefined>
9
+ if (key in nodeProps)
10
+ return nodeProps[key]
11
+
12
+ const kebabKey = camelToKebab(key)
13
+ if (kebabKey !== key && kebabKey in nodeProps)
14
+ return nodeProps[kebabKey]
15
+
16
+ return undefined
17
+ }
18
+
19
+ export function readVNodeTypeName(node: VNode | null | undefined) {
20
+ if (typeof node?.type === 'string')
21
+ return node.type
22
+
23
+ if (typeof node?.type === 'object' && node.type) {
24
+ const component = node.type as { name?: string, __name?: string }
25
+ return component.name ?? component.__name ?? 'anonymous-component'
26
+ }
27
+
28
+ return 'anonymous-node'
29
+ }
30
+
31
+ export function isBlankTextVNode(node: VNode | null | undefined) {
32
+ return node?.type === NativeText
33
+ && typeof node.children === 'string'
34
+ && !node.children.trim()
35
+ }
36
+
37
+ export function isCommentVNode(node: VNode | null | undefined) {
38
+ return node?.type === Comment
39
+ }
40
+
41
+ export function isNamedComponentVNode(node: VNode | null | undefined, name: string, component?: unknown) {
42
+ return node?.type === component || readVNodeTypeName(node) === name
43
+ }
@@ -0,0 +1,5 @@
1
+ export const shortLogoMarkRectX = 45.8672
2
+ export const shortLogoMarkSize = 43
3
+ export const shortLogoMarkRadius = 16.125
4
+ export const shortLogoMarkTransform = `translate(-${shortLogoMarkRectX} 0)`
5
+ export const shortLogoMarkPath = 'M68.1769 5.26562L68.1769 19.9499L80.117 11.5894L81.0419 12.9103L68.978 21.3575L82.3292 27.5833L81.6477 29.0447L68.1761 22.7627V37.623H66.5636V30.7105L65.3632 37.5179L63.7752 37.2379L65.4808 27.5654L61.3627 36.3967L59.9012 35.7152L65.1864 24.3812L55.5328 32.4815L54.4963 31.2463L64.8914 22.5238L51.3749 23.7063L51.2344 22.1L65.3588 20.8643L54.4989 11.7517L55.5354 10.5164L66.5644 19.7709L66.5644 5.26562H68.1769Z'
@@ -0,0 +1,43 @@
1
+ import {
2
+ compileLayoutAuthoring,
3
+ createLayoutSlotPlanState,
4
+ type CompiledLayoutAuthoring,
5
+ type LayoutAuthoringInput,
6
+ } from './layout-authoring'
7
+ import type { LayoutShorthandComponents } from './layout-shorthands'
8
+ import {
9
+ createSlotPlacementSession,
10
+ type SlotPlacementSession,
11
+ } from './slot-placement'
12
+
13
+ export type SlideLayoutInput = Omit<LayoutAuthoringInput, 'components' | 'slotPlanState'>
14
+ export type CompiledSlideLayout = CompiledLayoutAuthoring
15
+
16
+ export type SlideLayout = SlotPlacementSession & {
17
+ compile(input: SlideLayoutInput): CompiledSlideLayout
18
+ }
19
+
20
+ export function createSlideLayout(input: {
21
+ components: LayoutShorthandComponents
22
+ }): SlideLayout {
23
+ const slotPlanState = createLayoutSlotPlanState()
24
+ let current: CompiledSlideLayout | null = null
25
+ const placement = createSlotPlacementSession({
26
+ layout: () => current?.slotPlan ?? null,
27
+ })
28
+
29
+ return {
30
+ compile(authoringInput) {
31
+ const compiled = compileLayoutAuthoring({
32
+ ...authoringInput,
33
+ components: input.components,
34
+ slotPlanState,
35
+ })
36
+
37
+ current = compiled
38
+ return compiled
39
+ },
40
+ register: placement.register,
41
+ snapshot: placement.snapshot,
42
+ }
43
+ }
@@ -0,0 +1,195 @@
1
+ import { inject, provide } from 'vue'
2
+ import {
3
+ resolveGridPlacement,
4
+ type GridCursor,
5
+ type GridPlacedItem,
6
+ type GridPlacementInput,
7
+ type GridPlacementStyle,
8
+ type GridRect,
9
+ } from './grid-placement'
10
+ import type { LayoutSlotPlan, ResolvedLayoutSlot } from './layout-authoring'
11
+ import type { ThemeLayoutRole, ThemeLayoutSlotSpec } from './layout-registry'
12
+ import { createSlotRules } from './slot-rules'
13
+
14
+ export type GridFootprint = {
15
+ cols: number
16
+ rows: number
17
+ label: string
18
+ }
19
+
20
+ export type SlotPlacementInput = {
21
+ id: string
22
+ label?: string
23
+ role?: ThemeLayoutRole | ''
24
+ area?: string
25
+ col?: string
26
+ row?: string
27
+ overrides?: Partial<ThemeLayoutSlotSpec>
28
+ }
29
+
30
+ export type ResolvedSlotPlacement = {
31
+ rect: GridRect
32
+ style: GridPlacementStyle
33
+ footprint: GridFootprint
34
+ layoutSlot: ResolvedLayoutSlot | null
35
+ }
36
+
37
+ export type SlotPlacementHandle = {
38
+ update(input: SlotPlacementInput): void
39
+ dispose(): void
40
+ }
41
+
42
+ export type SlotPlacementSnapshot = {
43
+ placements: readonly (ResolvedSlotPlacement & { id: string, label: string })[]
44
+ }
45
+
46
+ export type SlotPlacementSession = {
47
+ register(input: SlotPlacementInput, onResolve: (result: ResolvedSlotPlacement) => void): SlotPlacementHandle
48
+ snapshot(): SlotPlacementSnapshot
49
+ }
50
+
51
+ type SlotPlacementEntry = {
52
+ input: SlotPlacementInput
53
+ onResolve: (result: ResolvedSlotPlacement) => void
54
+ order: number
55
+ }
56
+
57
+ const SlotPlacementSessionKey = Symbol('theme-slot-placement-session')
58
+ const slotRules = createSlotRules({ Slot: null })
59
+
60
+ function resolveEntry(input: SlotPlacementInput, layout: LayoutSlotPlan | null) {
61
+ const intent = slotRules.normalize(input, { layoutActive: Boolean(layout) })
62
+
63
+ if (intent.kind === 'manual') {
64
+ return {
65
+ label: intent.label,
66
+ layoutSlot: null,
67
+ placement: {
68
+ area: intent.area,
69
+ col: intent.col,
70
+ row: intent.row,
71
+ label: intent.label,
72
+ } satisfies GridPlacementInput,
73
+ }
74
+ }
75
+
76
+ const layoutSlot = layout!.resolveRoleSlot({
77
+ id: intent.id,
78
+ role: intent.role,
79
+ overrides: intent.overrides,
80
+ label: intent.label,
81
+ })
82
+
83
+ return {
84
+ label: intent.label,
85
+ layoutSlot,
86
+ placement: {
87
+ area: layoutSlot.area,
88
+ label: intent.label,
89
+ } satisfies GridPlacementInput,
90
+ }
91
+ }
92
+
93
+ function footprintFromRect(rect: GridRect, label: string): GridFootprint {
94
+ return {
95
+ cols: rect.colEnd - rect.colStart,
96
+ rows: rect.rowEnd - rect.rowStart,
97
+ label,
98
+ }
99
+ }
100
+
101
+ export function createSlotPlacementSession(input: {
102
+ layout: () => LayoutSlotPlan | null
103
+ }): SlotPlacementSession {
104
+ const entries = new Map<string, SlotPlacementEntry>()
105
+ let nextOrder = 0
106
+ let currentSnapshot: SlotPlacementSnapshot = { placements: [] }
107
+
108
+ function recompute() {
109
+ const layout = input.layout()
110
+ const occupied: GridPlacedItem[] = []
111
+ let cursor: GridCursor = { row: 1, col: 1 }
112
+ const placements: (ResolvedSlotPlacement & { id: string, label: string })[] = []
113
+
114
+ const orderedEntries = [...entries.values()].sort((a, b) => a.order - b.order)
115
+
116
+ for (const entry of orderedEntries) {
117
+ const resolved = resolveEntry(entry.input, layout)
118
+ const result = resolveGridPlacement(resolved.placement, {
119
+ occupied,
120
+ cursor,
121
+ label: resolved.label,
122
+ })
123
+ const placement = {
124
+ rect: result.rect,
125
+ style: result.style,
126
+ footprint: footprintFromRect(result.rect, resolved.label),
127
+ layoutSlot: resolved.layoutSlot,
128
+ }
129
+
130
+ occupied.push({
131
+ id: entry.input.id,
132
+ label: resolved.label,
133
+ ...result.rect,
134
+ })
135
+
136
+ entry.onResolve(placement)
137
+ placements.push({
138
+ id: entry.input.id,
139
+ label: resolved.label,
140
+ ...placement,
141
+ })
142
+ cursor = result.nextCursor
143
+ }
144
+
145
+ currentSnapshot = { placements }
146
+ }
147
+
148
+ return {
149
+ register(placementInput, onResolve) {
150
+ entries.set(placementInput.id, {
151
+ input: placementInput,
152
+ onResolve,
153
+ order: nextOrder++,
154
+ })
155
+ recompute()
156
+
157
+ return {
158
+ update(nextInput) {
159
+ const current = entries.get(placementInput.id)
160
+ if (!current)
161
+ return
162
+
163
+ entries.set(placementInput.id, {
164
+ ...current,
165
+ input: nextInput,
166
+ })
167
+ recompute()
168
+ },
169
+ dispose() {
170
+ input.layout()?.unregisterRoleSlot(placementInput.id)
171
+ if (!entries.delete(placementInput.id))
172
+ return
173
+
174
+ recompute()
175
+ },
176
+ }
177
+ },
178
+ snapshot() {
179
+ return currentSnapshot
180
+ },
181
+ }
182
+ }
183
+
184
+ export function provideSlotPlacementSession(session: SlotPlacementSession) {
185
+ provide(SlotPlacementSessionKey, session)
186
+ }
187
+
188
+ export function useSlotPlacementSession() {
189
+ const session = inject<SlotPlacementSession | null>(SlotPlacementSessionKey, null)
190
+
191
+ if (!session)
192
+ throw new Error('[Slot] Slot должен находиться внутри Slide.')
193
+
194
+ return session
195
+ }
@@ -0,0 +1,147 @@
1
+ import type { VNode } from 'vue'
2
+ import { isBlankTextVNode, isCommentVNode, isNamedComponentVNode, readVNodeProp, readVNodeTypeName } from './layout-vnode'
3
+ import { THEME_LAYOUT_ROLES, type ThemeLayoutRole, type ThemeLayoutSlotSpec } from './layout-recipes'
4
+ import type { SlotPlacementInput } from './slot-placement'
5
+
6
+ export type SlotNodeIntent = {
7
+ role: string
8
+ hasCoordinates: boolean
9
+ label: string
10
+ }
11
+
12
+ export type SlotChildrenRuleInput = {
13
+ label: string
14
+ }
15
+
16
+ export type SlotLayoutChildRuleInput = {
17
+ label: string
18
+ }
19
+
20
+ export type SlotIntent =
21
+ | { kind: 'manual', id: string, label: string, area: string, col: string, row: string }
22
+ | { kind: 'layout', id: string, label: string, role: ThemeLayoutRole, overrides: Partial<ThemeLayoutSlotSpec> }
23
+
24
+ export type SlotRules = {
25
+ read(node: VNode): SlotNodeIntent | null
26
+ assertChildren(children: readonly VNode[], input: SlotChildrenRuleInput): VNode[]
27
+ assertLayoutChild(node: VNode, input: SlotLayoutChildRuleInput): VNode
28
+ normalize(input: SlotPlacementInput, context: { layoutActive: boolean }): SlotIntent
29
+ }
30
+
31
+ const THEME_LAYOUT_ROLE_SET = new Set<ThemeLayoutRole>(THEME_LAYOUT_ROLES)
32
+
33
+ export function isThemeLayoutRole(value: unknown): value is ThemeLayoutRole {
34
+ return typeof value === 'string' && THEME_LAYOUT_ROLE_SET.has(value as ThemeLayoutRole)
35
+ }
36
+
37
+ function readStringProp(node: VNode, key: string) {
38
+ const value = readVNodeProp<string>(node, key)
39
+ return typeof value === 'string' ? value.trim() : ''
40
+ }
41
+
42
+ function normalizeLabel(input: SlotPlacementInput) {
43
+ return input.label?.trim() || input.id
44
+ }
45
+
46
+ function hasCoordinates(input: SlotPlacementInput | VNode) {
47
+ if ('props' in input) {
48
+ return Boolean(
49
+ readVNodeProp<string>(input, 'area')
50
+ || readVNodeProp<string>(input, 'col')
51
+ || readVNodeProp<string>(input, 'row'),
52
+ )
53
+ }
54
+
55
+ return Boolean(input.area || input.col || input.row)
56
+ }
57
+
58
+ export function createSlotRules(input: { Slot: unknown }): SlotRules {
59
+ function read(node: VNode): SlotNodeIntent | null {
60
+ if (!isNamedComponentVNode(node, 'Slot', input.Slot))
61
+ return null
62
+
63
+ return {
64
+ role: readStringProp(node, 'role'),
65
+ hasCoordinates: hasCoordinates(node),
66
+ label: readStringProp(node, 'label'),
67
+ }
68
+ }
69
+
70
+ return {
71
+ read,
72
+ assertChildren(children, options) {
73
+ const unsupportedNodes = children.filter(node =>
74
+ !isCommentVNode(node)
75
+ && !isBlankTextVNode(node)
76
+ && !read(node)?.role,
77
+ )
78
+
79
+ if (unsupportedNodes.length) {
80
+ const unsupportedLabels = unsupportedNodes.map(readVNodeTypeName).join(', ')
81
+ throw new Error(
82
+ `[Slide] ${options.label} ожидает только Slot role="${THEME_LAYOUT_ROLES.join('|')}" children в explicit layout mode. Неподдерживаемые children: ${unsupportedLabels}.`,
83
+ )
84
+ }
85
+
86
+ return [...children]
87
+ },
88
+ assertLayoutChild(node, options) {
89
+ const intent = read(node)
90
+
91
+ if (!intent?.role)
92
+ return node
93
+
94
+ const { role } = intent
95
+
96
+ if (!isThemeLayoutRole(role)) {
97
+ throw new Error(
98
+ `[Slide] ${options.label} получил неизвестную Slot role "${role}". Разрешены только: ${THEME_LAYOUT_ROLES.join(', ')}.`,
99
+ )
100
+ }
101
+
102
+ if (intent.hasCoordinates) {
103
+ throw new Error('[Slide] Slot внутри layout-слайда не должен получать area, col или row напрямую.')
104
+ }
105
+
106
+ return node
107
+ },
108
+ normalize(placementInput, context) {
109
+ const label = normalizeLabel(placementInput)
110
+
111
+ if (placementInput.role && !context.layoutActive) {
112
+ throw new Error('[Slot] role разрешён только внутри layout-слайда; вне layout-слайда используйте area, col или row.')
113
+ }
114
+
115
+ if (context.layoutActive && placementInput.role && !isThemeLayoutRole(placementInput.role)) {
116
+ throw new Error(`[Slot] role внутри layout-слайда должен быть одним из: ${THEME_LAYOUT_ROLES.join(', ')}.`)
117
+ }
118
+
119
+ if (context.layoutActive && placementInput.role && hasCoordinates(placementInput)) {
120
+ throw new Error('[Slot] area, col и row запрещены внутри layout-слайда; задайте role и выберите variant во frontmatter.')
121
+ }
122
+
123
+ if (context.layoutActive && !placementInput.role) {
124
+ throw new Error('[Slot] area, col и row запрещены внутри layout-слайда; все Slot внутри layout должны использовать role.')
125
+ }
126
+
127
+ if (!context.layoutActive || !placementInput.role) {
128
+ return {
129
+ kind: 'manual',
130
+ id: placementInput.id,
131
+ label,
132
+ area: placementInput.area ?? '',
133
+ col: placementInput.col ?? '',
134
+ row: placementInput.row ?? '',
135
+ }
136
+ }
137
+
138
+ return {
139
+ kind: 'layout',
140
+ id: placementInput.id,
141
+ label,
142
+ role: placementInput.role,
143
+ overrides: placementInput.overrides ?? {},
144
+ }
145
+ },
146
+ }
147
+ }