@whykusanagi/corrupted-theme 0.1.2 → 0.1.3

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 (44) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +6 -0
  3. package/docs/CAPABILITIES.md +209 -0
  4. package/docs/CHARACTER_LEVEL_CORRUPTION.md +264 -0
  5. package/docs/CORRUPTION_PHRASES.md +529 -0
  6. package/docs/FUTURE_WORK.md +189 -0
  7. package/docs/IMPLEMENTATION_VALIDATION.md +401 -0
  8. package/docs/LLM_PROVIDERS.md +345 -0
  9. package/docs/PERSONALITY.md +128 -0
  10. package/docs/ROADMAP.md +266 -0
  11. package/docs/ROUTING.md +324 -0
  12. package/docs/STYLE_GUIDE.md +605 -0
  13. package/docs/brand/BRAND_OVERVIEW.md +413 -0
  14. package/docs/brand/COLOR_SYSTEM.md +583 -0
  15. package/docs/brand/DESIGN_TOKENS.md +1009 -0
  16. package/docs/brand/TRANSLATION_FAILURE_AESTHETIC.md +525 -0
  17. package/docs/brand/TYPOGRAPHY.md +624 -0
  18. package/docs/components/ANIMATION_GUIDELINES.md +901 -0
  19. package/docs/components/COMPONENT_LIBRARY.md +1061 -0
  20. package/docs/components/GLASSMORPHISM.md +602 -0
  21. package/docs/components/INTERACTIVE_STATES.md +766 -0
  22. package/docs/governance/CONTRIBUTION_GUIDELINES.md +593 -0
  23. package/docs/governance/DESIGN_SYSTEM_GOVERNANCE.md +451 -0
  24. package/docs/governance/VERSION_MANAGEMENT.md +447 -0
  25. package/docs/governance/VERSION_REFERENCES.md +229 -0
  26. package/docs/platforms/CLI_IMPLEMENTATION.md +1025 -0
  27. package/docs/platforms/COMPONENT_MAPPING.md +579 -0
  28. package/docs/platforms/NPM_PACKAGE.md +854 -0
  29. package/docs/platforms/WEB_IMPLEMENTATION.md +1221 -0
  30. package/docs/standards/ACCESSIBILITY.md +715 -0
  31. package/docs/standards/ANTI_PATTERNS.md +554 -0
  32. package/docs/standards/SPACING_SYSTEM.md +549 -0
  33. package/examples/button.html +1 -1
  34. package/examples/card.html +1 -1
  35. package/examples/form.html +1 -1
  36. package/examples/index.html +2 -2
  37. package/examples/layout.html +1 -1
  38. package/examples/nikke-team-builder.html +1 -1
  39. package/examples/showcase-complete.html +840 -15
  40. package/examples/showcase.html +1 -1
  41. package/package.json +4 -2
  42. package/src/css/components.css +676 -0
  43. package/src/lib/character-corruption.js +563 -0
  44. package/src/lib/components.js +283 -0
@@ -0,0 +1,1025 @@
1
+ # CLI Implementation Guide
2
+
3
+ > **Celeste Brand System** | Platform Documentation
4
+ > **Document**: CLI (Terminal) Implementation Guide
5
+ > **Version**: 1.0.0
6
+ > **Last Updated**: 2025-12-13
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Overview](#overview)
13
+ 2. [Terminal Constraints](#terminal-constraints)
14
+ 3. [Tech Stack](#tech-stack)
15
+ 4. [Color System](#color-system)
16
+ 5. [Typography & Characters](#typography--characters)
17
+ 6. [TUI Patterns](#tui-patterns)
18
+ 7. [Animation Techniques](#animation-techniques)
19
+ 8. [Corruption Implementation](#corruption-implementation)
20
+ 9. [Layout Strategies](#layout-strategies)
21
+ 10. [Performance Optimization](#performance-optimization)
22
+ 11. [Testing](#testing)
23
+
24
+ ---
25
+
26
+ ## Overview
27
+
28
+ The Celeste CLI brings the **translation-failure corruption aesthetic** to terminal interfaces using the Bubble Tea TUI framework. Unlike web implementations, CLI must work within strict terminal constraints while maintaining the same premium glassmorphism feel.
29
+
30
+ ### CLI Philosophy
31
+
32
+ - **Terminal-Native**: Embrace terminal limitations, don't fight them
33
+ - **Performance First**: 60fps rendering even on remote connections
34
+ - **Keyboard-Driven**: All interactions via keyboard (no mouse required)
35
+ - **Readable Corruption**: 25-40% intensity maximum for terminal text
36
+ - **Cross-Platform**: Works on macOS, Linux, Windows (PowerShell/WSL)
37
+
38
+ ### Quick Reference
39
+
40
+ ```go
41
+ // Celeste CLI structure
42
+ celeste/
43
+ ├── cmd/celeste/
44
+ │ ├── main.go // Entry point
45
+ │ ├── commands/ // Command definitions
46
+ │ │ ├── commands.go // Command registry
47
+ │ │ ├── stats.go // /stats dashboard
48
+ │ │ └── ...
49
+ │ ├── tui/ // Bubble Tea UI
50
+ │ │ ├── app.go // Main TUI model
51
+ │ │ ├── chat.go // Chat interface
52
+ │ │ └── ...
53
+ │ └── config/ // Session, settings
54
+ │ └── session.go // Session management
55
+ └── docs/ // This documentation
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Terminal Constraints
61
+
62
+ ### Physical Limitations
63
+
64
+ | Constraint | Typical Value | Impact |
65
+ |------------|---------------|--------|
66
+ | **Width** | 80-120 chars | Content must fit within window |
67
+ | **Height** | 24-40 lines | Vertical scrolling needed for long content |
68
+ | **Refresh Rate** | 60Hz (16.67ms) | Maximum animation frame rate |
69
+ | **Color Depth** | 256 colors (8-bit) | Limited color palette |
70
+ | **Font** | Monospace only | Fixed-width character grid |
71
+ | **Resolution** | Character-level | No sub-pixel positioning |
72
+
73
+ ### Terminal Compatibility
74
+
75
+ ```go
76
+ // Detect terminal capabilities
77
+ import "github.com/muesli/termenv"
78
+
79
+ func DetectTerminal() {
80
+ output := termenv.DefaultOutput()
81
+
82
+ // Check color support
83
+ profile := output.ColorProfile()
84
+ // Returns: termenv.Ascii, termenv.ANSI, termenv.ANSI256, termenv.TrueColor
85
+
86
+ // Check terminal features
87
+ hasMouse := termenv.HasMouseSupport()
88
+ hasAltScreen := termenv.HasAltScreen()
89
+ hasDarkBackground := termenv.HasDarkBackground()
90
+ }
91
+ ```
92
+
93
+ ### Safe Width Guidelines
94
+
95
+ ```go
96
+ // Recommended content widths
97
+ const (
98
+ MinWidth = 80 // Minimum supported terminal width
99
+ SafeWidth = 100 // Safe width for most terminals
100
+ MaxWidth = 120 // Maximum before line wrapping
101
+ ContentWidth = 96 // Content area (padding for borders)
102
+ )
103
+
104
+ // Responsive layout
105
+ func GetContentWidth() int {
106
+ width, _, _ := term.GetSize(int(os.Stdout.Fd()))
107
+
108
+ switch {
109
+ case width < MinWidth:
110
+ return width - 4 // Minimal padding
111
+ case width < SafeWidth:
112
+ return width - 8 // Moderate padding
113
+ default:
114
+ return min(ContentWidth, width-12) // Full padding
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Tech Stack
122
+
123
+ ### Core Dependencies
124
+
125
+ ```go
126
+ // go.mod
127
+ module github.com/whykusanagi/celeste-cli
128
+
129
+ go 1.22
130
+
131
+ require (
132
+ github.com/charmbracelet/bubbletea v1.3.10 // TUI framework
133
+ github.com/charmbracelet/lipgloss v1.0.0 // Styling
134
+ github.com/muesli/termenv v0.15.2 // Terminal detection
135
+ )
136
+ ```
137
+
138
+ ### Bubble Tea Basics
139
+
140
+ ```go
141
+ // Minimal Bubble Tea app
142
+ package main
143
+
144
+ import (
145
+ "fmt"
146
+ tea "github.com/charmbracelet/bubbletea"
147
+ )
148
+
149
+ // Model holds application state
150
+ type Model struct {
151
+ tick int
152
+ }
153
+
154
+ // Init returns initial command
155
+ func (m Model) Init() tea.Cmd {
156
+ return nil
157
+ }
158
+
159
+ // Update handles messages and updates state
160
+ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
161
+ switch msg := msg.(type) {
162
+ case tea.KeyMsg:
163
+ if msg.String() == "q" {
164
+ return m, tea.Quit
165
+ }
166
+ }
167
+ return m, nil
168
+ }
169
+
170
+ // View renders the UI
171
+ func (m Model) View() string {
172
+ return "Press 'q' to quit"
173
+ }
174
+
175
+ func main() {
176
+ p := tea.NewProgram(Model{})
177
+ if _, err := p.Run(); err != nil {
178
+ fmt.Printf("Error: %v", err)
179
+ }
180
+ }
181
+ ```
182
+
183
+ ### Lip Gloss Styling
184
+
185
+ ```go
186
+ import "github.com/charmbracelet/lipgloss"
187
+
188
+ // Define reusable styles
189
+ var (
190
+ accentColor = lipgloss.Color("#d94f90")
191
+ bgColor = lipgloss.Color("#0a0612")
192
+
193
+ titleStyle = lipgloss.NewStyle().
194
+ Foreground(accentColor).
195
+ Bold(true).
196
+ Padding(0, 1)
197
+
198
+ boxStyle = lipgloss.NewStyle().
199
+ Border(lipgloss.RoundedBorder()).
200
+ BorderForeground(accentColor).
201
+ Padding(1, 2)
202
+ )
203
+
204
+ // Apply styles
205
+ func RenderTitle(text string) string {
206
+ return titleStyle.Render(text)
207
+ }
208
+
209
+ func RenderBox(content string) string {
210
+ return boxStyle.Render(content)
211
+ }
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Color System
217
+
218
+ ### ANSI Color Palette
219
+
220
+ Celeste uses **256-color ANSI palette** for maximum terminal compatibility:
221
+
222
+ ```go
223
+ // Primary colors (Lip Gloss format)
224
+ var Colors = struct {
225
+ // Brand colors
226
+ Accent lipgloss.Color // #d94f90 (pink)
227
+ AccentLight lipgloss.Color // #e86ca8 (light pink)
228
+ AccentDark lipgloss.Color // #b61b70 (dark pink)
229
+
230
+ SecondaryPurple lipgloss.Color // #8b5cf6 (purple)
231
+ SecondaryCyan lipgloss.Color // #00d4ff (cyan)
232
+
233
+ // Background colors
234
+ BgDark lipgloss.Color // #0a0612 (dark purple-black)
235
+ BgMedium lipgloss.Color // #140c28 (medium purple)
236
+ BgLight lipgloss.Color // #1c1230 (light purple)
237
+
238
+ // Text colors
239
+ TextPrimary lipgloss.Color // #ffffff (white)
240
+ TextSecondary lipgloss.Color // #a0a0a0 (gray)
241
+ TextMuted lipgloss.Color // #606060 (dark gray)
242
+
243
+ // Status colors
244
+ Success lipgloss.Color // #10b981 (green)
245
+ Warning lipgloss.Color // #f59e0b (orange)
246
+ Error lipgloss.Color // #ef4444 (red)
247
+ Info lipgloss.Color // #3b82f6 (blue)
248
+ }{
249
+ Accent: lipgloss.Color("#d94f90"),
250
+ AccentLight: lipgloss.Color("#e86ca8"),
251
+ AccentDark: lipgloss.Color("#b61b70"),
252
+
253
+ SecondaryPurple: lipgloss.Color("#8b5cf6"),
254
+ SecondaryCyan: lipgloss.Color("#00d4ff"),
255
+
256
+ BgDark: lipgloss.Color("#0a0612"),
257
+ BgMedium: lipgloss.Color("#140c28"),
258
+ BgLight: lipgloss.Color("#1c1230"),
259
+
260
+ TextPrimary: lipgloss.Color("#ffffff"),
261
+ TextSecondary: lipgloss.Color("#a0a0a0"),
262
+ TextMuted: lipgloss.Color("#606060"),
263
+
264
+ Success: lipgloss.Color("#10b981"),
265
+ Warning: lipgloss.Color("#f59e0b"),
266
+ Error: lipgloss.Color("#ef4444"),
267
+ Info: lipgloss.Color("#3b82f6"),
268
+ }
269
+ ```
270
+
271
+ ### Color Application
272
+
273
+ ```go
274
+ // Apply colors to styles
275
+ var (
276
+ headerStyle = lipgloss.NewStyle().
277
+ Foreground(Colors.Accent).
278
+ Background(Colors.BgDark).
279
+ Bold(true).
280
+ Padding(0, 2)
281
+
282
+ successStyle = lipgloss.NewStyle().
283
+ Foreground(Colors.Success).
284
+ Bold(true)
285
+
286
+ errorStyle = lipgloss.NewStyle().
287
+ Foreground(Colors.Error).
288
+ Bold(true)
289
+ )
290
+
291
+ // Usage
292
+ fmt.Println(headerStyle.Render("US使AGE STAT統ISTICS"))
293
+ fmt.Println(successStyle.Render("✓ Success"))
294
+ fmt.Println(errorStyle.Render("✗ Error"))
295
+ ```
296
+
297
+ ### Gradient Effects (Terminal-Safe)
298
+
299
+ ```go
300
+ // Simulate gradient with ANSI color steps
301
+ func RenderGradient(text string, startColor, endColor lipgloss.Color) string {
302
+ // For short text, use start color only
303
+ if len(text) < 10 {
304
+ return lipgloss.NewStyle().Foreground(startColor).Render(text)
305
+ }
306
+
307
+ // For longer text, alternate colors to simulate gradient
308
+ var result strings.Builder
309
+ for i, char := range text {
310
+ ratio := float64(i) / float64(len(text))
311
+ if ratio < 0.5 {
312
+ result.WriteString(lipgloss.NewStyle().Foreground(startColor).Render(string(char)))
313
+ } else {
314
+ result.WriteString(lipgloss.NewStyle().Foreground(endColor).Render(string(char)))
315
+ }
316
+ }
317
+ return result.String()
318
+ }
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Typography & Characters
324
+
325
+ ### Unicode Block Characters
326
+
327
+ Block characters create visual weight and simulate glassmorphism:
328
+
329
+ ```go
330
+ // Border characters (box drawing)
331
+ const (
332
+ BorderTopLeft = "╭"
333
+ BorderTopRight = "╮"
334
+ BorderBottomLeft = "╰"
335
+ BorderBottomRight = "╯"
336
+ BorderHorizontal = "─"
337
+ BorderVertical = "│"
338
+ BorderTJunctionDown = "┬"
339
+ BorderTJunctionUp = "┴"
340
+ BorderTJunctionRight = "├"
341
+ BorderTJunctionLeft = "┤"
342
+ BorderCross = "┼"
343
+ )
344
+
345
+ // Block characters (shading)
346
+ const (
347
+ BlockFull = "█" // 100% filled
348
+ BlockHeavy = "▓" // 75% filled
349
+ BlockMedium = "▒" // 50% filled
350
+ BlockLight = "░" // 25% filled
351
+ )
352
+
353
+ // Progress bar example
354
+ func RenderProgressBar(percent float64, width int) string {
355
+ filled := int(percent * float64(width))
356
+ bar := strings.Repeat(BlockFull, filled)
357
+ bar += strings.Repeat(BlockLight, width-filled)
358
+ return bar
359
+ }
360
+
361
+ // Output: "████████▓▓▒▒░░░░"
362
+ ```
363
+
364
+ ### Japanese Character Support
365
+
366
+ ```go
367
+ // Ensure UTF-8 encoding
368
+ import "unicode/utf8"
369
+
370
+ // Japanese character sets for corruption
371
+ var (
372
+ Katakana = []rune{'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン', 'ー'}
373
+
374
+ Hiragana = []rune{'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'り', 'る', 'れ', 'ろ', 'わ', 'を', 'ん'}
375
+
376
+ Kanji = []rune{'使', '用', '統', '計', '理', '管', '埋', '設', '定', '化', '変', '換', '状', '態', '処', '期', '待'}
377
+ )
378
+
379
+ // Character width detection (CJK characters are typically 2 cells wide)
380
+ func DisplayWidth(s string) int {
381
+ width := 0
382
+ for _, r := range s {
383
+ width += runewidth.RuneWidth(r)
384
+ }
385
+ return width
386
+ }
387
+ ```
388
+
389
+ ### Text Alignment
390
+
391
+ ```go
392
+ import "github.com/muesli/reflow/padding"
393
+
394
+ // Center text within width
395
+ func CenterText(text string, width int) string {
396
+ textWidth := DisplayWidth(text)
397
+ if textWidth >= width {
398
+ return text
399
+ }
400
+
401
+ leftPad := (width - textWidth) / 2
402
+ return padding.String(text, uint(width)).Padding(uint(leftPad), 0)
403
+ }
404
+
405
+ // Right-align text
406
+ func RightAlign(text string, width int) string {
407
+ textWidth := DisplayWidth(text)
408
+ if textWidth >= width {
409
+ return text
410
+ }
411
+
412
+ leftPad := width - textWidth
413
+ return strings.Repeat(" ", leftPad) + text
414
+ }
415
+ ```
416
+
417
+ ---
418
+
419
+ ## TUI Patterns
420
+
421
+ ### Dashboard Header
422
+
423
+ ```go
424
+ // /stats dashboard header pattern
425
+ func RenderDashboardHeader(title string) string {
426
+ corruptedTitle := CorruptTextJapanese(title, 0.3)
427
+
428
+ header := lipgloss.NewStyle().
429
+ Foreground(Colors.Accent).
430
+ Bold(true).
431
+ Padding(1, 2).
432
+ Border(lipgloss.RoundedBorder()).
433
+ BorderForeground(Colors.AccentLight).
434
+ Render(corruptedTitle)
435
+
436
+ return header
437
+ }
438
+
439
+ // Output:
440
+ // ╭──────────────────────╮
441
+ // │ US使AGE STAT統ISTICS │
442
+ // ╰──────────────────────╯
443
+ ```
444
+
445
+ ### Status Line
446
+
447
+ ```go
448
+ // Bottom status line with indicators
449
+ func RenderStatusLine(width int) string {
450
+ left := lipgloss.NewStyle().
451
+ Foreground(Colors.TextSecondary).
452
+ Render("Press 'q' to quit")
453
+
454
+ right := lipgloss.NewStyle().
455
+ Foreground(Colors.Accent).
456
+ Render("Celeste CLI v1.0.0")
457
+
458
+ // Calculate spacing
459
+ gap := width - DisplayWidth(left) - DisplayWidth(right)
460
+ if gap < 0 {
461
+ gap = 0
462
+ }
463
+
464
+ return left + strings.Repeat(" ", gap) + right
465
+ }
466
+ ```
467
+
468
+ ### Progress Indicators
469
+
470
+ ```go
471
+ // Block character progress bar
472
+ func RenderProgress(label string, percent float64, width int) string {
473
+ barWidth := width - DisplayWidth(label) - 10 // Space for label + percentage
474
+
475
+ filled := int(percent * float64(barWidth))
476
+ bar := strings.Repeat(BlockFull, filled)
477
+ bar += strings.Repeat(BlockLight, barWidth-filled)
478
+
479
+ percentText := fmt.Sprintf("%3.0f%%", percent*100)
480
+
481
+ return fmt.Sprintf("%s [%s] %s",
482
+ lipgloss.NewStyle().Foreground(Colors.TextPrimary).Render(label),
483
+ lipgloss.NewStyle().Foreground(Colors.Accent).Render(bar),
484
+ lipgloss.NewStyle().Foreground(Colors.TextSecondary).Render(percentText),
485
+ )
486
+ }
487
+
488
+ // Output: "Loading [████████░░░░] 67%"
489
+ ```
490
+
491
+ ### Spinner Animation
492
+
493
+ ```go
494
+ // Frame-based spinner
495
+ type Spinner struct {
496
+ frames []string
497
+ index int
498
+ }
499
+
500
+ func NewSpinner() *Spinner {
501
+ return &Spinner{
502
+ frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
503
+ index: 0,
504
+ }
505
+ }
506
+
507
+ func (s *Spinner) Next() string {
508
+ frame := s.frames[s.index]
509
+ s.index = (s.index + 1) % len(s.frames)
510
+
511
+ return lipgloss.NewStyle().
512
+ Foreground(Colors.Accent).
513
+ Render(frame)
514
+ }
515
+
516
+ // Update every 150ms
517
+ func tickCmd() tea.Cmd {
518
+ return tea.Tick(150*time.Millisecond, func(t time.Time) tea.Msg {
519
+ return tickMsg(t)
520
+ })
521
+ }
522
+ ```
523
+
524
+ ### Table Rendering
525
+
526
+ ```go
527
+ // Simple table with borders
528
+ func RenderTable(headers []string, rows [][]string) string {
529
+ // Calculate column widths
530
+ widths := make([]int, len(headers))
531
+ for i, header := range headers {
532
+ widths[i] = DisplayWidth(header)
533
+ }
534
+ for _, row := range rows {
535
+ for i, cell := range row {
536
+ if w := DisplayWidth(cell); w > widths[i] {
537
+ widths[i] = w
538
+ }
539
+ }
540
+ }
541
+
542
+ var result strings.Builder
543
+
544
+ // Top border
545
+ result.WriteString(BorderTopLeft)
546
+ for i, width := range widths {
547
+ result.WriteString(strings.Repeat(BorderHorizontal, width+2))
548
+ if i < len(widths)-1 {
549
+ result.WriteString(BorderTJunctionDown)
550
+ }
551
+ }
552
+ result.WriteString(BorderTopRight + "\n")
553
+
554
+ // Headers
555
+ result.WriteString(BorderVertical + " ")
556
+ for i, header := range headers {
557
+ result.WriteString(lipgloss.NewStyle().
558
+ Foreground(Colors.Accent).
559
+ Bold(true).
560
+ Width(widths[i]).
561
+ Render(header))
562
+ result.WriteString(" " + BorderVertical + " ")
563
+ }
564
+ result.WriteString("\n")
565
+
566
+ // Middle border
567
+ result.WriteString(BorderTJunctionRight)
568
+ for i, width := range widths {
569
+ result.WriteString(strings.Repeat(BorderHorizontal, width+2))
570
+ if i < len(widths)-1 {
571
+ result.WriteString(BorderCross)
572
+ }
573
+ }
574
+ result.WriteString(BorderTJunctionLeft + "\n")
575
+
576
+ // Rows
577
+ for _, row := range rows {
578
+ result.WriteString(BorderVertical + " ")
579
+ for i, cell := range row {
580
+ result.WriteString(lipgloss.NewStyle().
581
+ Foreground(Colors.TextPrimary).
582
+ Width(widths[i]).
583
+ Render(cell))
584
+ result.WriteString(" " + BorderVertical + " ")
585
+ }
586
+ result.WriteString("\n")
587
+ }
588
+
589
+ // Bottom border
590
+ result.WriteString(BorderBottomLeft)
591
+ for i, width := range widths {
592
+ result.WriteString(strings.Repeat(BorderHorizontal, width+2))
593
+ if i < len(widths)-1 {
594
+ result.WriteString(BorderTJunctionUp)
595
+ }
596
+ }
597
+ result.WriteString(BorderBottomRight + "\n")
598
+
599
+ return result.String()
600
+ }
601
+ ```
602
+
603
+ ---
604
+
605
+ ## Animation Techniques
606
+
607
+ ### Frame-Based Animation
608
+
609
+ ```go
610
+ // Ticker for animations (matches web 150ms/300ms timing)
611
+ type tickMsg time.Time
612
+
613
+ func tick(d time.Duration) tea.Cmd {
614
+ return tea.Tick(d, func(t time.Time) tea.Msg {
615
+ return tickMsg(t)
616
+ })
617
+ }
618
+
619
+ // In Update()
620
+ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
621
+ switch msg := msg.(type) {
622
+ case tickMsg:
623
+ m.frame++
624
+ return m, tick(150 * time.Millisecond) // Match web timing
625
+ }
626
+ return m, nil
627
+ }
628
+ ```
629
+
630
+ ### Flicker Animation (Eyes)
631
+
632
+ ```go
633
+ // Eye flicker (signature Celeste animation)
634
+ type Eye struct {
635
+ open bool
636
+ nextBlink int
637
+ frame int
638
+ }
639
+
640
+ func (e *Eye) Update() {
641
+ e.frame++
642
+
643
+ if e.frame >= e.nextBlink {
644
+ e.open = !e.open
645
+
646
+ if e.open {
647
+ // Next blink in 2-6 seconds (random)
648
+ e.nextBlink = e.frame + rand.Intn(30) + 10
649
+ } else {
650
+ // Blink duration: 150-300ms (1-2 frames at 150ms)
651
+ e.nextBlink = e.frame + rand.Intn(2) + 1
652
+ }
653
+ }
654
+ }
655
+
656
+ func (e *Eye) Render() string {
657
+ icon := "👁"
658
+ if !e.open {
659
+ icon = "━" // Closed eye
660
+ }
661
+
662
+ style := lipgloss.NewStyle().Foreground(Colors.Accent)
663
+
664
+ // Add opacity effect by dimming color when blinking
665
+ if !e.open {
666
+ style = style.Foreground(Colors.TextMuted)
667
+ }
668
+
669
+ return style.Render(icon)
670
+ }
671
+ ```
672
+
673
+ ### Text Corruption Animation
674
+
675
+ ```go
676
+ // Gradually corrupt text over time
677
+ func AnimateCorruption(text string, frame int, maxIntensity float64) string {
678
+ // Sine wave intensity (0 → maxIntensity → 0)
679
+ intensity := maxIntensity * math.Sin(float64(frame)*0.1)
680
+ if intensity < 0 {
681
+ intensity = 0
682
+ }
683
+
684
+ return CorruptTextJapanese(text, intensity)
685
+ }
686
+
687
+ // In Update()
688
+ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
689
+ switch msg := msg.(type) {
690
+ case tickMsg:
691
+ m.frame++
692
+ m.title = AnimateCorruption("USAGE STATISTICS", m.frame, 0.35)
693
+ return m, tick(300 * time.Millisecond) // Slow animation
694
+ }
695
+ return m, nil
696
+ }
697
+ ```
698
+
699
+ ---
700
+
701
+ ## Corruption Implementation
702
+
703
+ ### Character-Level Corruption
704
+
705
+ ```go
706
+ // Core corruption function (character-level mixing)
707
+ func CorruptTextJapanese(text string, intensity float64) string {
708
+ if intensity <= 0 {
709
+ return text
710
+ }
711
+
712
+ // Combine all Japanese character sets
713
+ allChars := append(append(Katakana, Hiragana...), Kanji...)
714
+
715
+ runes := []rune(text)
716
+ for i, r := range runes {
717
+ // Only corrupt letters (preserve spaces, punctuation)
718
+ if !unicode.IsLetter(r) {
719
+ continue
720
+ }
721
+
722
+ // Random chance based on intensity
723
+ if rand.Float64() < intensity {
724
+ runes[i] = allChars[rand.Intn(len(allChars))]
725
+ }
726
+ }
727
+
728
+ return string(runes)
729
+ }
730
+
731
+ // Usage
732
+ title := CorruptTextJapanese("USER MANAGEMENT", 0.3)
733
+ // Output: "US使R MA埋AGE統ENT" (30% corrupted)
734
+ ```
735
+
736
+ ### Intensity Guidelines (CLI)
737
+
738
+ ```go
739
+ // Recommended intensity levels for CLI
740
+ const (
741
+ IntensityNone = 0.0 // No corruption
742
+ IntensityMinimal = 0.15 // Decorative only (status line)
743
+ IntensityLow = 0.25 // Dashboard headers
744
+ IntensityMedium = 0.35 // Brand elements
745
+ IntensityHigh = 0.45 // Loading screens (max readable)
746
+ IntensityExtreme = 0.60 // ⚠️ Unreadable - never use
747
+ )
748
+
749
+ // Usage
750
+ func RenderHeader(text string) string {
751
+ return CorruptTextJapanese(text, IntensityLow) // 25% corruption
752
+ }
753
+ ```
754
+
755
+ ### Anti-Patterns (CLI-Specific)
756
+
757
+ ```go
758
+ // ❌ DON'T: Over-corrupt interactive elements
759
+ input := CorruptTextJapanese("Enter your name:", 0.5) // TOO HIGH - unreadable
760
+
761
+ // ✅ DO: Keep prompts readable
762
+ input := "Enter your name:" // No corruption for critical UI
763
+
764
+ // ❌ DON'T: Corrupt every text element
765
+ title := CorruptTextJapanese("Dashboard", 0.3)
766
+ label1 := CorruptTextJapanese("Users", 0.3)
767
+ label2 := CorruptTextJapanese("Active", 0.3)
768
+ // Too much corruption = visual noise
769
+
770
+ // ✅ DO: Selective corruption
771
+ title := CorruptTextJapanese("Dashboard", 0.3) // Header only
772
+ label1 := "Users" // Plain labels
773
+ label2 := "Active" // Plain labels
774
+ ```
775
+
776
+ ---
777
+
778
+ ## Layout Strategies
779
+
780
+ ### Responsive Terminal Layout
781
+
782
+ ```go
783
+ // Adjust layout based on terminal size
784
+ func (m Model) View() string {
785
+ width, height, _ := term.GetSize(int(os.Stdout.Fd()))
786
+
787
+ // Narrow terminal (<80 chars)
788
+ if width < 80 {
789
+ return m.renderCompactView(width, height)
790
+ }
791
+
792
+ // Standard terminal (80-120 chars)
793
+ if width < 120 {
794
+ return m.renderStandardView(width, height)
795
+ }
796
+
797
+ // Wide terminal (120+ chars)
798
+ return m.renderWideView(width, height)
799
+ }
800
+
801
+ func (m Model) renderCompactView(w, h int) string {
802
+ // Stack elements vertically
803
+ return lipgloss.JoinVertical(
804
+ lipgloss.Left,
805
+ m.renderHeader(),
806
+ m.renderStats(),
807
+ m.renderFooter(),
808
+ )
809
+ }
810
+
811
+ func (m Model) renderWideView(w, h int) string {
812
+ // Horizontal layout with columns
813
+ leftColumn := m.renderStats()
814
+ rightColumn := m.renderActivity()
815
+
816
+ return lipgloss.JoinHorizontal(
817
+ lipgloss.Top,
818
+ leftColumn,
819
+ rightColumn,
820
+ )
821
+ }
822
+ ```
823
+
824
+ ### Vertical Scrolling
825
+
826
+ ```go
827
+ import "github.com/charmbracelet/bubbles/viewport"
828
+
829
+ type Model struct {
830
+ viewport viewport.Model
831
+ content string
832
+ }
833
+
834
+ func (m Model) Init() tea.Cmd {
835
+ return nil
836
+ }
837
+
838
+ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
839
+ switch msg := msg.(type) {
840
+ case tea.WindowSizeMsg:
841
+ m.viewport.Width = msg.Width
842
+ m.viewport.Height = msg.Height - 4 // Reserve space for header/footer
843
+ m.viewport.SetContent(m.content)
844
+ }
845
+
846
+ var cmd tea.Cmd
847
+ m.viewport, cmd = m.viewport.Update(msg)
848
+ return m, cmd
849
+ }
850
+
851
+ func (m Model) View() string {
852
+ return lipgloss.JoinVertical(
853
+ lipgloss.Left,
854
+ m.renderHeader(),
855
+ m.viewport.View(),
856
+ m.renderFooter(),
857
+ )
858
+ }
859
+ ```
860
+
861
+ ---
862
+
863
+ ## Performance Optimization
864
+
865
+ ### Minimize Redraws
866
+
867
+ ```go
868
+ // Cache rendered elements that don't change
869
+ type Model struct {
870
+ headerCache string
871
+ footerCache string
872
+ dirty bool
873
+ }
874
+
875
+ func (m Model) View() string {
876
+ // Rebuild cache only when dirty
877
+ if m.dirty {
878
+ m.headerCache = m.renderHeader()
879
+ m.footerCache = m.renderFooter()
880
+ m.dirty = false
881
+ }
882
+
883
+ // Always render dynamic content
884
+ body := m.renderBody()
885
+
886
+ return lipgloss.JoinVertical(
887
+ lipgloss.Left,
888
+ m.headerCache,
889
+ body,
890
+ m.footerCache,
891
+ )
892
+ }
893
+ ```
894
+
895
+ ### Batch Updates
896
+
897
+ ```go
898
+ // Batch updates to reduce message passing
899
+ type Model struct {
900
+ pendingUpdates []Update
901
+ lastRender time.Time
902
+ }
903
+
904
+ const renderInterval = 16 * time.Millisecond // 60fps
905
+
906
+ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
907
+ // Accumulate updates
908
+ m.pendingUpdates = append(m.pendingUpdates, msg)
909
+
910
+ // Render at 60fps maximum
911
+ if time.Since(m.lastRender) < renderInterval {
912
+ return m, nil // Skip render
913
+ }
914
+
915
+ // Process all pending updates
916
+ for _, update := range m.pendingUpdates {
917
+ m.applyUpdate(update)
918
+ }
919
+ m.pendingUpdates = nil
920
+ m.lastRender = time.Now()
921
+
922
+ return m, nil
923
+ }
924
+ ```
925
+
926
+ ### Efficient String Building
927
+
928
+ ```go
929
+ // ❌ Slow: String concatenation
930
+ func renderList(items []string) string {
931
+ result := ""
932
+ for _, item := range items {
933
+ result += "• " + item + "\n"
934
+ }
935
+ return result
936
+ }
937
+
938
+ // ✅ Fast: strings.Builder
939
+ func renderList(items []string) string {
940
+ var b strings.Builder
941
+ b.Grow(len(items) * 50) // Pre-allocate capacity
942
+
943
+ for _, item := range items {
944
+ b.WriteString("• ")
945
+ b.WriteString(item)
946
+ b.WriteRune('\n')
947
+ }
948
+ return b.String()
949
+ }
950
+ ```
951
+
952
+ ---
953
+
954
+ ## Testing
955
+
956
+ ### Unit Tests
957
+
958
+ ```go
959
+ // Test corruption intensity
960
+ func TestCorruptionIntensity(t *testing.T) {
961
+ text := "HELLO WORLD"
962
+ intensity := 0.3
963
+
964
+ corrupted := CorruptTextJapanese(text, intensity)
965
+
966
+ // Count corrupted characters
967
+ differences := 0
968
+ for i, r := range text {
969
+ if corrupted[i] != byte(r) {
970
+ differences++
971
+ }
972
+ }
973
+
974
+ actualIntensity := float64(differences) / float64(len(text))
975
+
976
+ // Allow ±10% variance (randomness)
977
+ if actualIntensity < 0.2 || actualIntensity > 0.4 {
978
+ t.Errorf("Expected ~30%% corruption, got %.1f%%", actualIntensity*100)
979
+ }
980
+ }
981
+ ```
982
+
983
+ ### Terminal Output Testing
984
+
985
+ ```go
986
+ // Use golden files for visual regression testing
987
+ func TestDashboardRender(t *testing.T) {
988
+ m := Model{
989
+ stats: Stats{Users: 1234, Active: 567},
990
+ }
991
+
992
+ output := m.View()
993
+
994
+ // Strip ANSI codes for comparison
995
+ plainOutput := stripAnsi(output)
996
+
997
+ // Compare with golden file
998
+ golden, _ := os.ReadFile("testdata/dashboard.golden")
999
+ if plainOutput != string(golden) {
1000
+ t.Errorf("Output mismatch. Run `make golden` to update.")
1001
+ }
1002
+ }
1003
+
1004
+ func stripAnsi(s string) string {
1005
+ re := regexp.MustCompile(`\x1b\[[0-9;]*m`)
1006
+ return re.ReplaceAllString(s, "")
1007
+ }
1008
+ ```
1009
+
1010
+ ---
1011
+
1012
+ ## Related Documentation
1013
+
1014
+ - [WEB_IMPLEMENTATION.md](./WEB_IMPLEMENTATION.md) - Web platform comparison
1015
+ - [COMPONENT_MAPPING.md](./COMPONENT_MAPPING.md) - CLI ↔ Web equivalents
1016
+ - [ANIMATION_GUIDELINES.md](../components/ANIMATION_GUIDELINES.md) - Animation timing reference
1017
+ - [COLOR_SYSTEM.md](../brand/COLOR_SYSTEM.md) - Color specifications
1018
+ - [TRANSLATION_FAILURE_AESTHETIC.md](../brand/TRANSLATION_FAILURE_AESTHETIC.md) - Corruption rules
1019
+
1020
+ ---
1021
+
1022
+ **Last Updated**: 2025-12-13
1023
+ **Version**: 1.0.0
1024
+ **Maintainer**: Celeste Brand System
1025
+ **Status**: ✅ Ready for Production