@pep/term-deck 1.0.10

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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +356 -0
  3. package/bin/term-deck.ts +45 -0
  4. package/examples/slides/01-welcome.md +9 -0
  5. package/examples/slides/02-features.md +12 -0
  6. package/examples/slides/03-colors.md +17 -0
  7. package/examples/slides/04-ascii-art.md +11 -0
  8. package/examples/slides/05-gradients.md +14 -0
  9. package/examples/slides/06-themes.md +13 -0
  10. package/examples/slides/07-markdown.md +13 -0
  11. package/examples/slides/08-controls.md +13 -0
  12. package/examples/slides/09-thanks.md +11 -0
  13. package/examples/slides/deck.config.ts +13 -0
  14. package/examples/slides-hacker/01-welcome.md +9 -0
  15. package/examples/slides-hacker/02-features.md +12 -0
  16. package/examples/slides-hacker/03-colors.md +17 -0
  17. package/examples/slides-hacker/04-ascii-art.md +11 -0
  18. package/examples/slides-hacker/05-gradients.md +14 -0
  19. package/examples/slides-hacker/06-themes.md +13 -0
  20. package/examples/slides-hacker/07-markdown.md +13 -0
  21. package/examples/slides-hacker/08-controls.md +13 -0
  22. package/examples/slides-hacker/09-thanks.md +11 -0
  23. package/examples/slides-hacker/deck.config.ts +13 -0
  24. package/examples/slides-matrix/01-welcome.md +9 -0
  25. package/examples/slides-matrix/02-features.md +12 -0
  26. package/examples/slides-matrix/03-colors.md +17 -0
  27. package/examples/slides-matrix/04-ascii-art.md +11 -0
  28. package/examples/slides-matrix/05-gradients.md +14 -0
  29. package/examples/slides-matrix/06-themes.md +13 -0
  30. package/examples/slides-matrix/07-markdown.md +13 -0
  31. package/examples/slides-matrix/08-controls.md +13 -0
  32. package/examples/slides-matrix/09-thanks.md +11 -0
  33. package/examples/slides-matrix/deck.config.ts +13 -0
  34. package/examples/slides-minimal/01-welcome.md +9 -0
  35. package/examples/slides-minimal/02-features.md +12 -0
  36. package/examples/slides-minimal/03-colors.md +17 -0
  37. package/examples/slides-minimal/04-ascii-art.md +11 -0
  38. package/examples/slides-minimal/05-gradients.md +14 -0
  39. package/examples/slides-minimal/06-themes.md +13 -0
  40. package/examples/slides-minimal/07-markdown.md +13 -0
  41. package/examples/slides-minimal/08-controls.md +13 -0
  42. package/examples/slides-minimal/09-thanks.md +11 -0
  43. package/examples/slides-minimal/deck.config.ts +13 -0
  44. package/examples/slides-neon/01-welcome.md +9 -0
  45. package/examples/slides-neon/02-features.md +12 -0
  46. package/examples/slides-neon/03-colors.md +17 -0
  47. package/examples/slides-neon/04-ascii-art.md +11 -0
  48. package/examples/slides-neon/05-gradients.md +14 -0
  49. package/examples/slides-neon/06-themes.md +13 -0
  50. package/examples/slides-neon/07-markdown.md +13 -0
  51. package/examples/slides-neon/08-controls.md +13 -0
  52. package/examples/slides-neon/09-thanks.md +11 -0
  53. package/examples/slides-neon/deck.config.ts +13 -0
  54. package/examples/slides-retro/01-welcome.md +9 -0
  55. package/examples/slides-retro/02-features.md +12 -0
  56. package/examples/slides-retro/03-colors.md +17 -0
  57. package/examples/slides-retro/04-ascii-art.md +11 -0
  58. package/examples/slides-retro/05-gradients.md +14 -0
  59. package/examples/slides-retro/06-themes.md +13 -0
  60. package/examples/slides-retro/07-markdown.md +13 -0
  61. package/examples/slides-retro/08-controls.md +13 -0
  62. package/examples/slides-retro/09-thanks.md +11 -0
  63. package/examples/slides-retro/deck.config.ts +13 -0
  64. package/package.json +66 -0
  65. package/src/cli/__tests__/errors.test.ts +201 -0
  66. package/src/cli/__tests__/help.test.ts +157 -0
  67. package/src/cli/__tests__/init.test.ts +110 -0
  68. package/src/cli/commands/export.ts +33 -0
  69. package/src/cli/commands/init.ts +125 -0
  70. package/src/cli/commands/present.ts +29 -0
  71. package/src/cli/errors.ts +77 -0
  72. package/src/core/__tests__/slide.test.ts +1759 -0
  73. package/src/core/__tests__/theme.test.ts +1103 -0
  74. package/src/core/slide.ts +509 -0
  75. package/src/core/theme.ts +388 -0
  76. package/src/export/__tests__/recorder.test.ts +566 -0
  77. package/src/export/recorder.ts +639 -0
  78. package/src/index.ts +36 -0
  79. package/src/presenter/__tests__/main.test.ts +244 -0
  80. package/src/presenter/main.ts +658 -0
  81. package/src/renderer/__tests__/screen-extended.test.ts +801 -0
  82. package/src/renderer/__tests__/screen.test.ts +525 -0
  83. package/src/renderer/screen.ts +671 -0
  84. package/src/schemas/__tests__/config.test.ts +429 -0
  85. package/src/schemas/__tests__/slide.test.ts +349 -0
  86. package/src/schemas/__tests__/theme.test.ts +970 -0
  87. package/src/schemas/__tests__/validation.test.ts +256 -0
  88. package/src/schemas/config.ts +58 -0
  89. package/src/schemas/slide.ts +56 -0
  90. package/src/schemas/theme.ts +203 -0
  91. package/src/schemas/validation.ts +64 -0
  92. package/src/themes/matrix/index.ts +53 -0
  93. package/themes/hacker.ts +53 -0
  94. package/themes/minimal.ts +53 -0
  95. package/themes/neon.ts +53 -0
  96. package/themes/retro.ts +53 -0
@@ -0,0 +1,256 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { z, ZodError } from 'zod'
3
+ import { ValidationError, formatZodError, safeParse } from '../validation'
4
+
5
+ describe('ValidationError', () => {
6
+ it('extends Error', () => {
7
+ const error = new ValidationError('test message')
8
+ expect(error).toBeInstanceOf(Error)
9
+ })
10
+
11
+ it('has name "ValidationError"', () => {
12
+ const error = new ValidationError('test message')
13
+ expect(error.name).toBe('ValidationError')
14
+ })
15
+
16
+ it('stores the message', () => {
17
+ const error = new ValidationError('Something went wrong')
18
+ expect(error.message).toBe('Something went wrong')
19
+ })
20
+
21
+ it('can be caught as Error', () => {
22
+ let caught: Error | null = null
23
+ try {
24
+ throw new ValidationError('test')
25
+ } catch (e) {
26
+ if (e instanceof Error) {
27
+ caught = e
28
+ }
29
+ }
30
+ expect(caught).not.toBeNull()
31
+ expect(caught?.name).toBe('ValidationError')
32
+ })
33
+
34
+ it('can be identified by instanceof', () => {
35
+ const error = new ValidationError('test')
36
+ expect(error instanceof ValidationError).toBe(true)
37
+ expect(error instanceof Error).toBe(true)
38
+ })
39
+ })
40
+
41
+ describe('formatZodError', () => {
42
+ it('formats single field error with path', () => {
43
+ const schema = z.object({
44
+ name: z.string().min(1, { message: 'Name is required' }),
45
+ })
46
+ const result = schema.safeParse({ name: '' })
47
+ expect(result.success).toBe(false)
48
+ if (!result.success) {
49
+ const formatted = formatZodError(result.error, 'config')
50
+ expect(formatted).toBe('Invalid config:\n - name: Name is required')
51
+ }
52
+ })
53
+
54
+ it('formats nested field error with dotted path', () => {
55
+ const schema = z.object({
56
+ colors: z.object({
57
+ primary: z.string().regex(/^#[0-9a-fA-F]{6}$/, {
58
+ message: 'Must be a valid hex color',
59
+ }),
60
+ }),
61
+ })
62
+ const result = schema.safeParse({ colors: { primary: 'red' } })
63
+ expect(result.success).toBe(false)
64
+ if (!result.success) {
65
+ const formatted = formatZodError(result.error, 'theme')
66
+ expect(formatted).toBe('Invalid theme:\n - colors.primary: Must be a valid hex color')
67
+ }
68
+ })
69
+
70
+ it('formats multiple errors', () => {
71
+ const schema = z.object({
72
+ name: z.string().min(1, { message: 'Name is required' }),
73
+ age: z.number().min(0, { message: 'Age must be positive' }),
74
+ })
75
+ const result = schema.safeParse({ name: '', age: -1 })
76
+ expect(result.success).toBe(false)
77
+ if (!result.success) {
78
+ const formatted = formatZodError(result.error, 'person')
79
+ expect(formatted).toContain('Invalid person:')
80
+ expect(formatted).toContain('name: Name is required')
81
+ expect(formatted).toContain('age: Age must be positive')
82
+ }
83
+ })
84
+
85
+ it('formats error without path (root level)', () => {
86
+ const schema = z.string().min(1, { message: 'Value is required' })
87
+ const result = schema.safeParse('')
88
+ expect(result.success).toBe(false)
89
+ if (!result.success) {
90
+ const formatted = formatZodError(result.error, 'input')
91
+ expect(formatted).toBe('Invalid input:\n - Value is required')
92
+ }
93
+ })
94
+
95
+ it('formats deeply nested paths', () => {
96
+ const schema = z.object({
97
+ settings: z.object({
98
+ display: z.object({
99
+ theme: z.object({
100
+ color: z.string().min(1, { message: 'Color is required' }),
101
+ }),
102
+ }),
103
+ }),
104
+ })
105
+ const result = schema.safeParse({
106
+ settings: { display: { theme: { color: '' } } },
107
+ })
108
+ expect(result.success).toBe(false)
109
+ if (!result.success) {
110
+ const formatted = formatZodError(result.error, 'config')
111
+ expect(formatted).toContain('settings.display.theme.color: Color is required')
112
+ }
113
+ })
114
+
115
+ it('formats array index in path', () => {
116
+ const schema = z.object({
117
+ items: z.array(z.string().min(1, { message: 'Item cannot be empty' })),
118
+ })
119
+ const result = schema.safeParse({ items: ['valid', ''] })
120
+ expect(result.success).toBe(false)
121
+ if (!result.success) {
122
+ const formatted = formatZodError(result.error, 'list')
123
+ expect(formatted).toContain('items.1: Item cannot be empty')
124
+ }
125
+ })
126
+
127
+ it('includes context in error message', () => {
128
+ const schema = z.object({ name: z.string() })
129
+ const result = schema.safeParse({ name: 123 })
130
+ expect(result.success).toBe(false)
131
+ if (!result.success) {
132
+ const formatted = formatZodError(result.error, 'slide frontmatter')
133
+ expect(formatted).toStartWith('Invalid slide frontmatter:')
134
+ }
135
+ })
136
+ })
137
+
138
+ describe('safeParse', () => {
139
+ it('returns parsed data for valid input', () => {
140
+ const schema = z.object({
141
+ name: z.string(),
142
+ count: z.number().default(0),
143
+ })
144
+ const result = safeParse(schema, { name: 'test' }, 'config')
145
+ expect(result).toEqual({ name: 'test', count: 0 })
146
+ })
147
+
148
+ it('applies schema defaults', () => {
149
+ const schema = z.object({
150
+ enabled: z.boolean().default(true),
151
+ value: z.number().default(42),
152
+ })
153
+ const result = safeParse(schema, {}, 'settings')
154
+ expect(result).toEqual({ enabled: true, value: 42 })
155
+ })
156
+
157
+ it('throws ValidationError for invalid input', () => {
158
+ const schema = z.object({
159
+ name: z.string().min(1),
160
+ })
161
+ expect(() => safeParse(schema, { name: '' }, 'config')).toThrow(ValidationError)
162
+ })
163
+
164
+ it('thrown error contains formatted message', () => {
165
+ const schema = z.object({
166
+ email: z.string().email({ message: 'Invalid email format' }),
167
+ })
168
+ try {
169
+ safeParse(schema, { email: 'not-an-email' }, 'user')
170
+ expect(true).toBe(false) // Should not reach here
171
+ } catch (e) {
172
+ expect(e).toBeInstanceOf(ValidationError)
173
+ if (e instanceof ValidationError) {
174
+ expect(e.message).toContain('Invalid user:')
175
+ expect(e.message).toContain('email: Invalid email format')
176
+ }
177
+ }
178
+ })
179
+
180
+ it('thrown error has name ValidationError', () => {
181
+ const schema = z.number()
182
+ try {
183
+ safeParse(schema, 'not a number', 'value')
184
+ expect(true).toBe(false) // Should not reach here
185
+ } catch (e) {
186
+ expect(e).toBeInstanceOf(ValidationError)
187
+ if (e instanceof ValidationError) {
188
+ expect(e.name).toBe('ValidationError')
189
+ }
190
+ }
191
+ })
192
+
193
+ it('works with complex schemas', () => {
194
+ const schema = z.object({
195
+ name: z.string().min(1),
196
+ colors: z.object({
197
+ primary: z.string().regex(/^#[0-9a-fA-F]{6}$/),
198
+ accent: z.string().regex(/^#[0-9a-fA-F]{6}$/),
199
+ }),
200
+ settings: z.object({
201
+ enabled: z.boolean().default(false),
202
+ }).optional(),
203
+ })
204
+
205
+ const validData = {
206
+ name: 'test',
207
+ colors: {
208
+ primary: '#ff0066',
209
+ accent: '#00ff66',
210
+ },
211
+ }
212
+
213
+ const result = safeParse(schema, validData, 'theme')
214
+ expect(result.name).toBe('test')
215
+ expect(result.colors.primary).toBe('#ff0066')
216
+ expect(result.settings).toBeUndefined()
217
+ })
218
+
219
+ it('preserves type inference', () => {
220
+ const schema = z.object({
221
+ count: z.number(),
222
+ name: z.string(),
223
+ items: z.array(z.string()),
224
+ })
225
+ const result = safeParse(schema, { count: 5, name: 'test', items: ['a', 'b'] }, 'data')
226
+
227
+ // Type inference test - these should compile without errors
228
+ const count: number = result.count
229
+ const name: string = result.name
230
+ const items: string[] = result.items
231
+
232
+ expect(count).toBe(5)
233
+ expect(name).toBe('test')
234
+ expect(items).toEqual(['a', 'b'])
235
+ })
236
+
237
+ it('handles union types', () => {
238
+ const schema = z.object({
239
+ value: z.union([z.string(), z.array(z.string())]),
240
+ })
241
+
242
+ const stringResult = safeParse(schema, { value: 'hello' }, 'config')
243
+ expect(stringResult.value).toBe('hello')
244
+
245
+ const arrayResult = safeParse(schema, { value: ['a', 'b'] }, 'config')
246
+ expect(arrayResult.value).toEqual(['a', 'b'])
247
+ })
248
+
249
+ it('rejects invalid union types with clear error', () => {
250
+ const schema = z.object({
251
+ value: z.union([z.string(), z.number()]),
252
+ })
253
+
254
+ expect(() => safeParse(schema, { value: [] }, 'config')).toThrow(ValidationError)
255
+ })
256
+ })
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod'
2
+ import { ThemeSchema } from './theme'
3
+
4
+ /**
5
+ * Schema for presentation settings.
6
+ * Controls how the presentation behaves during runtime.
7
+ */
8
+ export const SettingsSchema = z.object({
9
+ // Start slide (0-indexed)
10
+ startSlide: z.number().min(0).default(0),
11
+ // Loop back to first slide after last
12
+ loop: z.boolean().default(false),
13
+ // Auto-advance slides (ms, 0 = disabled)
14
+ autoAdvance: z.number().min(0).default(0),
15
+ // Show slide numbers
16
+ showSlideNumbers: z.boolean().default(false),
17
+ // Show progress bar
18
+ showProgress: z.boolean().default(false),
19
+ })
20
+
21
+ export type Settings = z.infer<typeof SettingsSchema>
22
+
23
+ /**
24
+ * Schema for export settings.
25
+ * Controls the output dimensions and quality of exported videos/GIFs.
26
+ */
27
+ export const ExportSettingsSchema = z.object({
28
+ // Output width in characters (min 80, max 400)
29
+ width: z.number().min(80).max(400).default(120),
30
+ // Output height in characters (min 24, max 100)
31
+ height: z.number().min(24).max(100).default(40),
32
+ // Frames per second for video (min 10, max 60)
33
+ fps: z.number().min(10).max(60).default(30),
34
+ })
35
+
36
+ export type ExportSettings = z.infer<typeof ExportSettingsSchema>
37
+
38
+ /**
39
+ * Schema for validating deck configuration (deck.config.ts).
40
+ * Defines the complete configuration for a presentation deck.
41
+ */
42
+ export const DeckConfigSchema = z.object({
43
+ // Presentation metadata
44
+ title: z.string().optional(),
45
+ author: z.string().optional(),
46
+ date: z.string().optional(),
47
+
48
+ // Theme (already validated Theme object)
49
+ theme: ThemeSchema,
50
+
51
+ // Presentation settings
52
+ settings: SettingsSchema.optional(),
53
+
54
+ // Export settings
55
+ export: ExportSettingsSchema.optional(),
56
+ })
57
+
58
+ export type DeckConfig = z.infer<typeof DeckConfigSchema>
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Schema for validating slide frontmatter.
5
+ * Defines the metadata for a single slide.
6
+ */
7
+ export const SlideFrontmatterSchema = z.object({
8
+ // Required: window title
9
+ title: z.string().min(1, {
10
+ message: 'Slide must have a title',
11
+ }),
12
+
13
+ // ASCII art text (figlet) - can be a single line or multiple lines
14
+ bigText: z.union([
15
+ z.string(),
16
+ z.array(z.string()),
17
+ ]).optional(),
18
+
19
+ // Which gradient to use for bigText
20
+ gradient: z.string().optional(),
21
+
22
+ // Override theme for this slide
23
+ theme: z.string().optional(),
24
+
25
+ // Transition effect
26
+ transition: z.enum([
27
+ 'glitch', // Default: glitch reveal line by line
28
+ 'fade', // Fade in
29
+ 'instant', // No animation
30
+ 'typewriter', // Character by character
31
+ ]).default('glitch'),
32
+
33
+ // Custom metadata (ignored by renderer, useful for tooling)
34
+ meta: z.record(z.string(), z.unknown()).optional(),
35
+ })
36
+
37
+ export type SlideFrontmatter = z.infer<typeof SlideFrontmatterSchema>
38
+
39
+ /**
40
+ * Schema for a complete slide after parsing.
41
+ * Includes the parsed frontmatter, body content, optional notes, and metadata.
42
+ */
43
+ export const SlideSchema = z.object({
44
+ // Parsed frontmatter
45
+ frontmatter: SlideFrontmatterSchema,
46
+ // Markdown body content
47
+ body: z.string(),
48
+ // Presenter notes (extracted from <!-- notes --> block)
49
+ notes: z.string().optional(),
50
+ // Source file path
51
+ sourcePath: z.string(),
52
+ // Slide index in deck (0-indexed)
53
+ index: z.number(),
54
+ })
55
+
56
+ export type Slide = z.infer<typeof SlideSchema>
@@ -0,0 +1,203 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Schema for validating hex color strings.
5
+ * Accepts 6-digit hex colors with # prefix (e.g., #ff0066)
6
+ */
7
+ export const HexColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/, {
8
+ message: 'Color must be a valid hex color (e.g., #ff0066)',
9
+ })
10
+
11
+ export type HexColor = z.infer<typeof HexColorSchema>
12
+
13
+ /**
14
+ * Schema for validating gradient color arrays.
15
+ * A gradient requires at least 2 hex colors.
16
+ */
17
+ export const GradientSchema = z.array(HexColorSchema).min(2, {
18
+ message: 'Gradient must have at least 2 colors',
19
+ })
20
+
21
+ export type Gradient = z.infer<typeof GradientSchema>
22
+
23
+ /**
24
+ * Schema for validating theme objects.
25
+ * Defines the visual appearance of the presentation deck.
26
+ */
27
+ export const ThemeSchema = z.object({
28
+ // Theme metadata
29
+ name: z.string().min(1, { message: 'Theme name is required' }),
30
+ description: z.string().optional(),
31
+ author: z.string().optional(),
32
+ version: z.string().optional(),
33
+
34
+ // Color palette
35
+ colors: z.object({
36
+ primary: HexColorSchema,
37
+ secondary: HexColorSchema.optional(),
38
+ accent: HexColorSchema,
39
+ background: HexColorSchema,
40
+ text: HexColorSchema,
41
+ muted: HexColorSchema,
42
+ success: HexColorSchema.optional(),
43
+ warning: HexColorSchema.optional(),
44
+ error: HexColorSchema.optional(),
45
+ }),
46
+
47
+ // Named gradients for bigText
48
+ gradients: z.record(z.string(), GradientSchema).refine(
49
+ (g) => Object.keys(g).length >= 1,
50
+ { message: 'At least one gradient must be defined' }
51
+ ),
52
+
53
+ // Glyph set for matrix rain background
54
+ glyphs: z.string().min(10, {
55
+ message: 'Glyph set must have at least 10 characters',
56
+ }),
57
+
58
+ // Animation settings
59
+ animations: z.object({
60
+ // Speed multiplier (1.0 = normal, 0.5 = half speed, 2.0 = double speed)
61
+ revealSpeed: z.number().min(0.1).max(5.0).default(1.0),
62
+ // Matrix rain density (number of drops)
63
+ matrixDensity: z.number().min(10).max(200).default(50),
64
+ // Glitch effect iterations
65
+ glitchIterations: z.number().min(1).max(20).default(5),
66
+ // Delay between lines during reveal (ms)
67
+ lineDelay: z.number().min(0).max(500).default(30),
68
+ // Matrix rain update interval (ms)
69
+ matrixInterval: z.number().min(20).max(200).default(80),
70
+ }),
71
+
72
+ // Window appearance
73
+ window: z.object({
74
+ // Border style
75
+ borderStyle: z.enum(['line', 'double', 'rounded', 'none']).default('line'),
76
+ // Shadow effect
77
+ shadow: z.boolean().default(true),
78
+ // Padding inside windows
79
+ padding: z.object({
80
+ top: z.number().min(0).max(5).default(1),
81
+ bottom: z.number().min(0).max(5).default(1),
82
+ left: z.number().min(0).max(10).default(2),
83
+ right: z.number().min(0).max(10).default(2),
84
+ }).optional(),
85
+ }).optional(),
86
+ })
87
+
88
+ export type Theme = z.infer<typeof ThemeSchema>
89
+
90
+ // ============================================================================
91
+ // Color Token System
92
+ // ============================================================================
93
+
94
+ /**
95
+ * Valid color tokens for inline styling in slide body content.
96
+ * Tokens can be either:
97
+ * - Built-in colors: GREEN, ORANGE, CYAN, PINK, WHITE, GRAY
98
+ * - Theme-mapped colors: PRIMARY, SECONDARY, ACCENT, MUTED, TEXT, BACKGROUND
99
+ *
100
+ * Usage in slides: {GREEN}colored text{/}
101
+ */
102
+ export const ColorTokens = [
103
+ 'GREEN',
104
+ 'ORANGE',
105
+ 'CYAN',
106
+ 'PINK',
107
+ 'WHITE',
108
+ 'GRAY',
109
+ 'PRIMARY', // Maps to theme.colors.primary
110
+ 'SECONDARY', // Maps to theme.colors.secondary
111
+ 'ACCENT', // Maps to theme.colors.accent
112
+ 'MUTED', // Maps to theme.colors.muted
113
+ 'TEXT', // Maps to theme.colors.text
114
+ 'BACKGROUND', // Maps to theme.colors.background
115
+ ] as const
116
+
117
+ /**
118
+ * Type for valid color token names.
119
+ */
120
+ export type ColorToken = typeof ColorTokens[number]
121
+
122
+ /**
123
+ * Pattern for matching color tokens in slide content.
124
+ * Matches: {GREEN}, {ORANGE}, {CYAN}, {PINK}, {WHITE}, {GRAY},
125
+ * {PRIMARY}, {SECONDARY}, {ACCENT}, {MUTED}, {TEXT}, {BACKGROUND}, {/}
126
+ *
127
+ * The {/} token closes any open color tag.
128
+ */
129
+ export const COLOR_TOKEN_PATTERN = /\{(GREEN|ORANGE|CYAN|PINK|WHITE|GRAY|PRIMARY|SECONDARY|ACCENT|MUTED|TEXT|BACKGROUND|\/)\}/g
130
+
131
+ // ============================================================================
132
+ // Partial Theme for Extension
133
+ // ============================================================================
134
+
135
+ /**
136
+ * Deep partial utility type that makes all nested properties optional.
137
+ * Used for theme extension where only specific fields need to be overridden.
138
+ */
139
+ export type DeepPartial<T> = T extends object ? {
140
+ [P in keyof T]?: DeepPartial<T[P]>
141
+ } : T
142
+
143
+ /**
144
+ * Partial theme type for use with theme.extend() functionality.
145
+ * All fields (including nested) become optional.
146
+ */
147
+ export type PartialTheme = DeepPartial<Theme>
148
+
149
+ /**
150
+ * Schema for validating partial theme objects.
151
+ * All fields become optional recursively, allowing partial overrides.
152
+ * Used for theme extension validation.
153
+ */
154
+ export const PartialThemeSchema = ThemeSchema.deepPartial()
155
+
156
+ // ============================================================================
157
+ // Default Theme
158
+ // ============================================================================
159
+
160
+ /**
161
+ * Default matrix/cyberpunk theme.
162
+ * Used when no theme is specified or as a base for theme extension.
163
+ */
164
+ export const DEFAULT_THEME: Theme = {
165
+ name: 'matrix',
166
+ description: 'Default cyberpunk/matrix theme',
167
+
168
+ colors: {
169
+ primary: '#00cc66',
170
+ accent: '#ff6600',
171
+ background: '#0a0a0a',
172
+ text: '#ffffff',
173
+ muted: '#666666',
174
+ },
175
+
176
+ gradients: {
177
+ fire: ['#ff6600', '#ff3300', '#ff0066'],
178
+ cool: ['#00ccff', '#0066ff', '#6600ff'],
179
+ pink: ['#ff0066', '#ff0099', '#cc00ff'],
180
+ hf: ['#99cc00', '#00cc66', '#00cccc'],
181
+ },
182
+
183
+ glyphs: 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン0123456789',
184
+
185
+ animations: {
186
+ revealSpeed: 1.0,
187
+ matrixDensity: 50,
188
+ glitchIterations: 5,
189
+ lineDelay: 30,
190
+ matrixInterval: 80,
191
+ },
192
+
193
+ window: {
194
+ borderStyle: 'line',
195
+ shadow: true,
196
+ padding: {
197
+ top: 1,
198
+ bottom: 1,
199
+ left: 2,
200
+ right: 2,
201
+ },
202
+ },
203
+ }
@@ -0,0 +1,64 @@
1
+ import { z, ZodError } from 'zod'
2
+
3
+ /**
4
+ * Custom error class for validation failures.
5
+ * Extends Error with a specific name for easy identification.
6
+ */
7
+ export class ValidationError extends Error {
8
+ constructor(message: string) {
9
+ super(message)
10
+ this.name = 'ValidationError'
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Format Zod errors into user-friendly messages.
16
+ * Formats each issue with its field path and message.
17
+ *
18
+ * @param error - The ZodError to format
19
+ * @param context - A description of what was being validated (e.g., "theme", "slide frontmatter")
20
+ * @returns A formatted string with all validation issues
21
+ *
22
+ * @example
23
+ * const error = new ZodError([...])
24
+ * formatZodError(error, 'theme')
25
+ * // Returns:
26
+ * // "Invalid theme:
27
+ * // - colors.primary: Color must be a valid hex color (e.g., #ff0066)
28
+ * // - name: Theme name is required"
29
+ */
30
+ export function formatZodError(error: ZodError, context: string): string {
31
+ const issues = error.issues.map((issue) => {
32
+ const path = issue.path.join('.')
33
+ return ` - ${path ? `${path}: ` : ''}${issue.message}`
34
+ })
35
+
36
+ return `Invalid ${context}:\n${issues.join('\n')}`
37
+ }
38
+
39
+ /**
40
+ * Parse data with a Zod schema and throw a ValidationError with friendly messages on failure.
41
+ *
42
+ * @param schema - The Zod schema to validate against
43
+ * @param data - The data to validate
44
+ * @param context - A description of what's being validated (e.g., "theme", "slide frontmatter")
45
+ * @returns The parsed and validated data
46
+ * @throws {ValidationError} If validation fails
47
+ *
48
+ * @example
49
+ * const theme = safeParse(ThemeSchema, rawData, 'theme')
50
+ * // Throws ValidationError with formatted message if invalid
51
+ */
52
+ export function safeParse<T>(
53
+ schema: z.ZodSchema<T>,
54
+ data: unknown,
55
+ context: string
56
+ ): T {
57
+ const result = schema.safeParse(data)
58
+
59
+ if (!result.success) {
60
+ throw new ValidationError(formatZodError(result.error, context))
61
+ }
62
+
63
+ return result.data
64
+ }
@@ -0,0 +1,53 @@
1
+ import { createTheme } from '../../core/theme'
2
+
3
+ const yaml = `
4
+ name: matrix
5
+ description: Default cyberpunk/matrix theme
6
+ author: term-deck
7
+ version: 1.0.0
8
+
9
+ colors:
10
+ primary: "#00cc66"
11
+ accent: "#ff6600"
12
+ background: "#0a0a0a"
13
+ text: "#ffffff"
14
+ muted: "#666666"
15
+
16
+ gradients:
17
+ fire:
18
+ - "#ff6600"
19
+ - "#ff3300"
20
+ - "#ff0066"
21
+ cool:
22
+ - "#00ccff"
23
+ - "#0066ff"
24
+ - "#6600ff"
25
+ pink:
26
+ - "#ff0066"
27
+ - "#ff0099"
28
+ - "#cc00ff"
29
+ hf:
30
+ - "#99cc00"
31
+ - "#00cc66"
32
+ - "#00cccc"
33
+
34
+ glyphs: "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン0123456789"
35
+
36
+ animations:
37
+ revealSpeed: 1.0
38
+ matrixDensity: 50
39
+ glitchIterations: 5
40
+ lineDelay: 30
41
+ matrixInterval: 80
42
+
43
+ window:
44
+ borderStyle: line
45
+ shadow: true
46
+ padding:
47
+ top: 1
48
+ bottom: 1
49
+ left: 2
50
+ right: 2
51
+ `
52
+
53
+ export default createTheme(yaml)