@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.
- package/CHANGELOG.md +133 -0
- package/README.md +6 -0
- package/docs/CAPABILITIES.md +209 -0
- package/docs/CHARACTER_LEVEL_CORRUPTION.md +264 -0
- package/docs/CORRUPTION_PHRASES.md +529 -0
- package/docs/FUTURE_WORK.md +189 -0
- package/docs/IMPLEMENTATION_VALIDATION.md +401 -0
- package/docs/LLM_PROVIDERS.md +345 -0
- package/docs/PERSONALITY.md +128 -0
- package/docs/ROADMAP.md +266 -0
- package/docs/ROUTING.md +324 -0
- package/docs/STYLE_GUIDE.md +605 -0
- package/docs/brand/BRAND_OVERVIEW.md +413 -0
- package/docs/brand/COLOR_SYSTEM.md +583 -0
- package/docs/brand/DESIGN_TOKENS.md +1009 -0
- package/docs/brand/TRANSLATION_FAILURE_AESTHETIC.md +525 -0
- package/docs/brand/TYPOGRAPHY.md +624 -0
- package/docs/components/ANIMATION_GUIDELINES.md +901 -0
- package/docs/components/COMPONENT_LIBRARY.md +1061 -0
- package/docs/components/GLASSMORPHISM.md +602 -0
- package/docs/components/INTERACTIVE_STATES.md +766 -0
- package/docs/governance/CONTRIBUTION_GUIDELINES.md +593 -0
- package/docs/governance/DESIGN_SYSTEM_GOVERNANCE.md +451 -0
- package/docs/governance/VERSION_MANAGEMENT.md +447 -0
- package/docs/governance/VERSION_REFERENCES.md +229 -0
- package/docs/platforms/CLI_IMPLEMENTATION.md +1025 -0
- package/docs/platforms/COMPONENT_MAPPING.md +579 -0
- package/docs/platforms/NPM_PACKAGE.md +854 -0
- package/docs/platforms/WEB_IMPLEMENTATION.md +1221 -0
- package/docs/standards/ACCESSIBILITY.md +715 -0
- package/docs/standards/ANTI_PATTERNS.md +554 -0
- package/docs/standards/SPACING_SYSTEM.md +549 -0
- package/examples/button.html +1 -1
- package/examples/card.html +1 -1
- package/examples/form.html +1 -1
- package/examples/index.html +2 -2
- package/examples/layout.html +1 -1
- package/examples/nikke-team-builder.html +1 -1
- package/examples/showcase-complete.html +840 -15
- package/examples/showcase.html +1 -1
- package/package.json +4 -2
- package/src/css/components.css +676 -0
- package/src/lib/character-corruption.js +563 -0
- 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
|