@rlabs-inc/create-tui 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/dist/index.js +221 -0
- package/package.json +20 -15
- package/templates/counter/README.md +88 -0
- package/templates/counter/package.json +18 -0
- package/templates/counter/src/App.ts +65 -0
- package/templates/counter/src/components/Counter.ts +78 -0
- package/templates/counter/src/components/CounterPanel.ts +74 -0
- package/templates/counter/src/components/Header.ts +55 -0
- package/templates/counter/src/components/HistoryPanel.ts +96 -0
- package/templates/counter/src/components/KeyBindings.ts +60 -0
- package/templates/counter/src/components/StatsPanel.ts +101 -0
- package/templates/counter/src/main.ts +87 -0
- package/templates/counter/src/state/counters.ts +121 -0
- package/templates/counter/tsconfig.json +16 -0
- package/templates/dashboard/README.md +95 -0
- package/templates/dashboard/package.json +18 -0
- package/templates/dashboard/src/App.ts +72 -0
- package/templates/dashboard/src/components/Footer.ts +102 -0
- package/templates/dashboard/src/components/Header.ts +108 -0
- package/templates/dashboard/src/components/LogsPanel.ts +98 -0
- package/templates/dashboard/src/components/MetricsPanel.ts +145 -0
- package/templates/dashboard/src/components/Sidebar.ts +162 -0
- package/templates/dashboard/src/components/TrafficPanel.ts +129 -0
- package/templates/dashboard/src/main.ts +66 -0
- package/templates/dashboard/src/state/logs.ts +42 -0
- package/templates/dashboard/src/state/metrics.ts +129 -0
- package/templates/dashboard/src/state/theme.ts +20 -0
- package/templates/dashboard/tsconfig.json +16 -0
- package/templates/minimal/README.md +98 -0
- package/templates/minimal/package.json +18 -0
- package/templates/minimal/src/App.ts +108 -0
- package/templates/minimal/src/components/Header.ts +52 -0
- package/templates/minimal/src/main.ts +24 -0
- package/templates/minimal/tsconfig.json +16 -0
- package/src/commands/create.ts +0 -300
- package/src/index.ts +0 -75
- package/src/utils/colors.ts +0 -132
- package/src/utils/prompts.ts +0 -273
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counter Panel Component
|
|
3
|
+
*
|
|
4
|
+
* Displays all counters using the `each` template primitive.
|
|
5
|
+
* Demonstrates:
|
|
6
|
+
* - Reactive list rendering with each()
|
|
7
|
+
* - Focus tracking per item
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { derived } from '@rlabs-inc/signals'
|
|
11
|
+
import { box, text, each, t, BorderStyle, Attr, focusManager } from '@rlabs-inc/tui'
|
|
12
|
+
import { counters } from '../state/counters'
|
|
13
|
+
import { Counter } from './Counter'
|
|
14
|
+
|
|
15
|
+
export function CounterPanel() {
|
|
16
|
+
box({
|
|
17
|
+
border: BorderStyle.SINGLE,
|
|
18
|
+
borderColor: t.border,
|
|
19
|
+
flexDirection: 'column',
|
|
20
|
+
grow: 1,
|
|
21
|
+
children: () => {
|
|
22
|
+
// Panel header
|
|
23
|
+
box({
|
|
24
|
+
padding: 1,
|
|
25
|
+
borderBottom: BorderStyle.SINGLE,
|
|
26
|
+
borderColor: t.border,
|
|
27
|
+
children: () => {
|
|
28
|
+
text({
|
|
29
|
+
content: 'COUNTERS',
|
|
30
|
+
fg: t.textBright,
|
|
31
|
+
attrs: Attr.BOLD,
|
|
32
|
+
})
|
|
33
|
+
text({
|
|
34
|
+
content: 'Tab to navigate, +/- to change value',
|
|
35
|
+
fg: t.textDim,
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Counter grid using each() primitive
|
|
41
|
+
box({
|
|
42
|
+
padding: 1,
|
|
43
|
+
gap: 1,
|
|
44
|
+
flexDirection: 'row',
|
|
45
|
+
flexWrap: 'wrap',
|
|
46
|
+
children: () => {
|
|
47
|
+
each(
|
|
48
|
+
() => counters.value,
|
|
49
|
+
(getCounter, key) => {
|
|
50
|
+
const counter = getCounter()
|
|
51
|
+
// Find index for focus tracking
|
|
52
|
+
const counterIndex = counters.value.findIndex(c => String(c.id) === key)
|
|
53
|
+
const isFocused = derived(
|
|
54
|
+
() => focusManager.focusedIndex === counterIndex
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return box({
|
|
58
|
+
width: '48%',
|
|
59
|
+
minWidth: 20,
|
|
60
|
+
children: () => {
|
|
61
|
+
Counter({
|
|
62
|
+
counter,
|
|
63
|
+
focused: isFocused.value,
|
|
64
|
+
})
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
{ key: (c) => String(c.id) }
|
|
69
|
+
)
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header Component
|
|
3
|
+
*
|
|
4
|
+
* Shows app title and current theme.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { box, text, t, BorderStyle, Attr } from '@rlabs-inc/tui'
|
|
8
|
+
import { currentThemeName } from '../state/counters'
|
|
9
|
+
|
|
10
|
+
export function Header() {
|
|
11
|
+
box({
|
|
12
|
+
height: 3,
|
|
13
|
+
width: '100%',
|
|
14
|
+
border: BorderStyle.SINGLE,
|
|
15
|
+
borderColor: t.primary,
|
|
16
|
+
paddingLeft: 1,
|
|
17
|
+
paddingRight: 1,
|
|
18
|
+
flexDirection: 'row',
|
|
19
|
+
justifyContent: 'space-between',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
children: () => {
|
|
22
|
+
// Left: Title
|
|
23
|
+
box({
|
|
24
|
+
flexDirection: 'row',
|
|
25
|
+
gap: 2,
|
|
26
|
+
alignItems: 'center',
|
|
27
|
+
children: () => {
|
|
28
|
+
text({
|
|
29
|
+
content: '{{PROJECT_NAME}}',
|
|
30
|
+
fg: t.primary,
|
|
31
|
+
attrs: Attr.BOLD,
|
|
32
|
+
})
|
|
33
|
+
text({ content: '|', fg: t.textDim })
|
|
34
|
+
text({ content: 'Reactive Counter Showcase', fg: t.textMuted })
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Right: Theme indicator
|
|
39
|
+
box({
|
|
40
|
+
flexDirection: 'row',
|
|
41
|
+
gap: 1,
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
children: () => {
|
|
44
|
+
text({ content: 'Theme:', fg: t.textDim })
|
|
45
|
+
text({
|
|
46
|
+
content: currentThemeName,
|
|
47
|
+
fg: t.accent,
|
|
48
|
+
attrs: Attr.BOLD,
|
|
49
|
+
})
|
|
50
|
+
text({ content: '[t]', fg: t.textDim })
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History Panel Component
|
|
3
|
+
*
|
|
4
|
+
* Scrollable history log demonstrating auto-scroll detection.
|
|
5
|
+
* When content exceeds the container height, TUI automatically
|
|
6
|
+
* enables scrolling.
|
|
7
|
+
*
|
|
8
|
+
* Demonstrates:
|
|
9
|
+
* - Auto-scroll when content overflows
|
|
10
|
+
* - each() for dynamic list rendering
|
|
11
|
+
* - show() for conditional empty state
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { derived } from '@rlabs-inc/signals'
|
|
15
|
+
import { box, text, each, show, t, BorderStyle, Attr } from '@rlabs-inc/tui'
|
|
16
|
+
import { history } from '../state/counters'
|
|
17
|
+
|
|
18
|
+
export function HistoryPanel() {
|
|
19
|
+
box({
|
|
20
|
+
border: BorderStyle.SINGLE,
|
|
21
|
+
borderColor: t.border,
|
|
22
|
+
grow: 1,
|
|
23
|
+
flexDirection: 'column',
|
|
24
|
+
children: () => {
|
|
25
|
+
// Header
|
|
26
|
+
box({
|
|
27
|
+
padding: 1,
|
|
28
|
+
borderBottom: BorderStyle.SINGLE,
|
|
29
|
+
borderColor: t.border,
|
|
30
|
+
flexDirection: 'row',
|
|
31
|
+
justifyContent: 'space-between',
|
|
32
|
+
children: () => {
|
|
33
|
+
text({
|
|
34
|
+
content: 'HISTORY',
|
|
35
|
+
fg: t.textBright,
|
|
36
|
+
attrs: Attr.BOLD,
|
|
37
|
+
})
|
|
38
|
+
text({
|
|
39
|
+
content: derived(() => `${history.value.length} entries`),
|
|
40
|
+
fg: t.textDim,
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Scrollable content area
|
|
46
|
+
box({
|
|
47
|
+
grow: 1,
|
|
48
|
+
padding: 1,
|
|
49
|
+
overflow: 'scroll', // Enable scrolling
|
|
50
|
+
children: () => {
|
|
51
|
+
// Empty state using show()
|
|
52
|
+
show(
|
|
53
|
+
() => history.value.length === 0,
|
|
54
|
+
() => {
|
|
55
|
+
return box({
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
padding: 2,
|
|
58
|
+
children: () => {
|
|
59
|
+
text({ content: 'No history yet', fg: t.textDim })
|
|
60
|
+
text({
|
|
61
|
+
content: 'Actions will appear here',
|
|
62
|
+
fg: t.textDim,
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// History entries using each()
|
|
70
|
+
each(
|
|
71
|
+
() => history.value,
|
|
72
|
+
(getEntry) => {
|
|
73
|
+
const entry = getEntry()
|
|
74
|
+
return box({
|
|
75
|
+
flexDirection: 'row',
|
|
76
|
+
gap: 1,
|
|
77
|
+
children: () => {
|
|
78
|
+
text({
|
|
79
|
+
content: entry.timestamp,
|
|
80
|
+
fg: t.textDim,
|
|
81
|
+
width: 10,
|
|
82
|
+
})
|
|
83
|
+
text({
|
|
84
|
+
content: entry.message,
|
|
85
|
+
fg: t.text,
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
{ key: (e) => String(e.id) }
|
|
91
|
+
)
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key Bindings Component
|
|
3
|
+
*
|
|
4
|
+
* Footer showing available keyboard shortcuts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { box, text, t, BorderStyle } from '@rlabs-inc/tui'
|
|
8
|
+
|
|
9
|
+
export function KeyBindings() {
|
|
10
|
+
box({
|
|
11
|
+
height: 3,
|
|
12
|
+
width: '100%',
|
|
13
|
+
border: BorderStyle.SINGLE,
|
|
14
|
+
borderColor: t.border,
|
|
15
|
+
paddingLeft: 1,
|
|
16
|
+
paddingRight: 1,
|
|
17
|
+
flexDirection: 'row',
|
|
18
|
+
justifyContent: 'space-between',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
children: () => {
|
|
21
|
+
// Navigation keys
|
|
22
|
+
box({
|
|
23
|
+
flexDirection: 'row',
|
|
24
|
+
gap: 2,
|
|
25
|
+
children: () => {
|
|
26
|
+
KeyHint({ key: 'Tab', action: 'Focus' })
|
|
27
|
+
KeyHint({ key: '+/-', action: 'Change' })
|
|
28
|
+
KeyHint({ key: 'r', action: 'Reset' })
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Action keys
|
|
33
|
+
box({
|
|
34
|
+
flexDirection: 'row',
|
|
35
|
+
gap: 2,
|
|
36
|
+
children: () => {
|
|
37
|
+
KeyHint({ key: 'R', action: 'Reset All' })
|
|
38
|
+
KeyHint({ key: 't', action: 'Theme' })
|
|
39
|
+
KeyHint({ key: 'q', action: 'Quit' })
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface KeyHintProps {
|
|
47
|
+
key: string
|
|
48
|
+
action: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function KeyHint({ key, action }: KeyHintProps) {
|
|
52
|
+
box({
|
|
53
|
+
flexDirection: 'row',
|
|
54
|
+
gap: 1,
|
|
55
|
+
children: () => {
|
|
56
|
+
text({ content: `[${key}]`, fg: t.info })
|
|
57
|
+
text({ content: action, fg: t.textDim })
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats Panel Component
|
|
3
|
+
*
|
|
4
|
+
* Displays derived statistics computed from counter values.
|
|
5
|
+
* Demonstrates reactive derived state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { derived } from '@rlabs-inc/signals'
|
|
9
|
+
import { box, text, t, BorderStyle, Attr } from '@rlabs-inc/tui'
|
|
10
|
+
import { total, average, min, max, counters } from '../state/counters'
|
|
11
|
+
|
|
12
|
+
export function StatsPanel() {
|
|
13
|
+
box({
|
|
14
|
+
border: BorderStyle.SINGLE,
|
|
15
|
+
borderColor: t.border,
|
|
16
|
+
children: () => {
|
|
17
|
+
// Header
|
|
18
|
+
box({
|
|
19
|
+
padding: 1,
|
|
20
|
+
borderBottom: BorderStyle.SINGLE,
|
|
21
|
+
borderColor: t.border,
|
|
22
|
+
children: () => {
|
|
23
|
+
text({
|
|
24
|
+
content: 'STATISTICS',
|
|
25
|
+
fg: t.textBright,
|
|
26
|
+
attrs: Attr.BOLD,
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Stats grid
|
|
32
|
+
box({
|
|
33
|
+
padding: 1,
|
|
34
|
+
gap: 1,
|
|
35
|
+
children: () => {
|
|
36
|
+
// Row 1: Total and Average
|
|
37
|
+
box({
|
|
38
|
+
flexDirection: 'row',
|
|
39
|
+
gap: 2,
|
|
40
|
+
children: () => {
|
|
41
|
+
StatItem({ label: 'Total', value: total, color: t.primary })
|
|
42
|
+
StatItem({
|
|
43
|
+
label: 'Average',
|
|
44
|
+
value: derived(() => average.value.toFixed(1)),
|
|
45
|
+
color: t.secondary,
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Row 2: Min and Max
|
|
51
|
+
box({
|
|
52
|
+
flexDirection: 'row',
|
|
53
|
+
gap: 2,
|
|
54
|
+
children: () => {
|
|
55
|
+
StatItem({ label: 'Min', value: min, color: t.error })
|
|
56
|
+
StatItem({ label: 'Max', value: max, color: t.success })
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Row 3: Count
|
|
61
|
+
box({
|
|
62
|
+
flexDirection: 'row',
|
|
63
|
+
gap: 2,
|
|
64
|
+
children: () => {
|
|
65
|
+
StatItem({
|
|
66
|
+
label: 'Counters',
|
|
67
|
+
value: derived(() => counters.value.length),
|
|
68
|
+
color: t.info,
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface StatItemProps {
|
|
79
|
+
label: string
|
|
80
|
+
value: { value: number | string } | (() => number | string)
|
|
81
|
+
color: { value: import('@rlabs-inc/tui').RGBA }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function StatItem({ label, value, color }: StatItemProps) {
|
|
85
|
+
box({
|
|
86
|
+
flexDirection: 'row',
|
|
87
|
+
gap: 1,
|
|
88
|
+
width: '45%',
|
|
89
|
+
children: () => {
|
|
90
|
+
text({ content: `${label}:`, fg: t.textDim, width: 10 })
|
|
91
|
+
text({
|
|
92
|
+
content: derived(() => {
|
|
93
|
+
const v = typeof value === 'function' ? value() : value.value
|
|
94
|
+
return String(v)
|
|
95
|
+
}),
|
|
96
|
+
fg: color,
|
|
97
|
+
attrs: Attr.BOLD,
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{PROJECT_NAME}} - Reactive Counter Showcase
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates TUI Framework's key features:
|
|
5
|
+
* - Fine-grained reactivity with signals
|
|
6
|
+
* - Template primitives (each, show)
|
|
7
|
+
* - Focus management
|
|
8
|
+
* - Auto-scroll detection
|
|
9
|
+
* - Theme switching
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mount, keyboard, focusManager } from '@rlabs-inc/tui'
|
|
13
|
+
import { App } from './App'
|
|
14
|
+
import { counters, resetAll, addHistoryEntry, cycleTheme } from './state/counters'
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const cleanup = await mount(
|
|
18
|
+
() => App(),
|
|
19
|
+
{ mode: 'fullscreen', mouse: true }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// ==========================================================================
|
|
23
|
+
// KEYBOARD HANDLERS
|
|
24
|
+
// ==========================================================================
|
|
25
|
+
|
|
26
|
+
// Counter controls (apply to focused counter)
|
|
27
|
+
keyboard.onKey(['+', '='], () => {
|
|
28
|
+
const idx = focusManager.focusedIndex
|
|
29
|
+
if (idx >= 0 && idx < counters.value.length) {
|
|
30
|
+
const counter = counters.value[idx]
|
|
31
|
+
counter.value.value++
|
|
32
|
+
addHistoryEntry(`Counter ${idx + 1}: +1 = ${counter.value.value}`)
|
|
33
|
+
}
|
|
34
|
+
return true
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
keyboard.onKey('-', () => {
|
|
38
|
+
const idx = focusManager.focusedIndex
|
|
39
|
+
if (idx >= 0 && idx < counters.value.length) {
|
|
40
|
+
const counter = counters.value[idx]
|
|
41
|
+
counter.value.value--
|
|
42
|
+
addHistoryEntry(`Counter ${idx + 1}: -1 = ${counter.value.value}`)
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
keyboard.onKey('r', () => {
|
|
48
|
+
const idx = focusManager.focusedIndex
|
|
49
|
+
if (idx >= 0 && idx < counters.value.length) {
|
|
50
|
+
const counter = counters.value[idx]
|
|
51
|
+
counter.value.value = 0
|
|
52
|
+
addHistoryEntry(`Counter ${idx + 1}: reset to 0`)
|
|
53
|
+
}
|
|
54
|
+
return true
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
keyboard.onKey('R', () => {
|
|
58
|
+
resetAll()
|
|
59
|
+
addHistoryEntry('All counters reset')
|
|
60
|
+
return true
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Theme cycling
|
|
64
|
+
keyboard.onKey('t', () => {
|
|
65
|
+
const themeName = cycleTheme()
|
|
66
|
+
addHistoryEntry(`Theme: ${themeName}`)
|
|
67
|
+
return true
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Focus navigation
|
|
71
|
+
keyboard.onKey('Tab', () => {
|
|
72
|
+
focusManager.focusNext()
|
|
73
|
+
return true
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
keyboard.onKey('Shift+Tab', () => {
|
|
77
|
+
focusManager.focusPrevious()
|
|
78
|
+
return true
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Quit
|
|
82
|
+
keyboard.onKey(['q', 'Q', 'Escape'], () => {
|
|
83
|
+
cleanup()
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch(console.error)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counter State Management
|
|
3
|
+
*
|
|
4
|
+
* Centralized state using signals with derived computations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { signal, derived, type WritableSignal } from '@rlabs-inc/signals'
|
|
8
|
+
import { setTheme, themes } from '@rlabs-inc/tui'
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// COUNTER STATE
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export interface Counter {
|
|
15
|
+
id: number
|
|
16
|
+
name: string
|
|
17
|
+
value: WritableSignal<number>
|
|
18
|
+
variant: 'primary' | 'secondary' | 'success' | 'warning' | 'info'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let nextId = 1
|
|
22
|
+
|
|
23
|
+
function createCounter(
|
|
24
|
+
name: string,
|
|
25
|
+
variant: Counter['variant'],
|
|
26
|
+
initial = 0
|
|
27
|
+
): Counter {
|
|
28
|
+
return {
|
|
29
|
+
id: nextId++,
|
|
30
|
+
name,
|
|
31
|
+
value: signal(initial),
|
|
32
|
+
variant,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Initial counters with different variants
|
|
37
|
+
export const counters = signal<Counter[]>([
|
|
38
|
+
createCounter('Alpha', 'primary', 0),
|
|
39
|
+
createCounter('Beta', 'secondary', 10),
|
|
40
|
+
createCounter('Gamma', 'success', 5),
|
|
41
|
+
createCounter('Delta', 'warning', -3),
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// DERIVED STATE
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
/** Total of all counter values */
|
|
49
|
+
export const total = derived(() =>
|
|
50
|
+
counters.value.reduce((sum, c) => sum + c.value.value, 0)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/** Average of all counter values */
|
|
54
|
+
export const average = derived(() => {
|
|
55
|
+
const list = counters.value
|
|
56
|
+
if (list.length === 0) return 0
|
|
57
|
+
return Math.round(total.value / list.length * 10) / 10
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
/** Minimum counter value */
|
|
61
|
+
export const min = derived(() => {
|
|
62
|
+
const values = counters.value.map(c => c.value.value)
|
|
63
|
+
return values.length > 0 ? Math.min(...values) : 0
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/** Maximum counter value */
|
|
67
|
+
export const max = derived(() => {
|
|
68
|
+
const values = counters.value.map(c => c.value.value)
|
|
69
|
+
return values.length > 0 ? Math.max(...values) : 0
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// HISTORY (demonstrates auto-scroll)
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
export interface HistoryEntry {
|
|
77
|
+
id: number
|
|
78
|
+
timestamp: string
|
|
79
|
+
message: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let historyId = 1
|
|
83
|
+
export const history = signal<HistoryEntry[]>([])
|
|
84
|
+
|
|
85
|
+
export function addHistoryEntry(message: string) {
|
|
86
|
+
const entry: HistoryEntry = {
|
|
87
|
+
id: historyId++,
|
|
88
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
89
|
+
message,
|
|
90
|
+
}
|
|
91
|
+
// Keep last 50 entries
|
|
92
|
+
history.value = [...history.value.slice(-49), entry]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// ACTIONS
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export function resetAll() {
|
|
100
|
+
counters.value.forEach(c => {
|
|
101
|
+
c.value.value = 0
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// =============================================================================
|
|
106
|
+
// THEME MANAGEMENT
|
|
107
|
+
// =============================================================================
|
|
108
|
+
|
|
109
|
+
const themeNames = Object.keys(themes)
|
|
110
|
+
export const currentThemeIndex = signal(0)
|
|
111
|
+
export const currentThemeName = derived(() => themeNames[currentThemeIndex.value])
|
|
112
|
+
|
|
113
|
+
export function cycleTheme(): string {
|
|
114
|
+
currentThemeIndex.value = (currentThemeIndex.value + 1) % themeNames.length
|
|
115
|
+
const themeName = themeNames[currentThemeIndex.value]
|
|
116
|
+
setTheme(themeName as any)
|
|
117
|
+
return themeName
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Initialize with first entry
|
|
121
|
+
addHistoryEntry('Application started')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src",
|
|
12
|
+
"types": ["bun-types"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A comprehensive system dashboard demonstrating all TUI Framework features.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-panel layout** with header, sidebar, main content, and footer
|
|
8
|
+
- **Real-time reactive updates** with simulated system metrics
|
|
9
|
+
- **Theme system** with 12+ built-in themes and live switching
|
|
10
|
+
- **Auto-scrolling logs** that track activity
|
|
11
|
+
- **Progress bars** and status indicators
|
|
12
|
+
- **Variant-based styling** for semantic colors
|
|
13
|
+
- **Keyboard navigation** and shortcuts
|
|
14
|
+
- **Component composition** patterns
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install dependencies
|
|
20
|
+
bun install
|
|
21
|
+
|
|
22
|
+
# Run the application
|
|
23
|
+
bun run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Controls
|
|
27
|
+
|
|
28
|
+
| Key | Action |
|
|
29
|
+
|-----|--------|
|
|
30
|
+
| `1-4` | Switch between panels |
|
|
31
|
+
| `t` | Cycle through themes |
|
|
32
|
+
| `r` | Refresh metrics |
|
|
33
|
+
| `p` | Pause/Resume updates |
|
|
34
|
+
| `q` / `Escape` | Quit |
|
|
35
|
+
|
|
36
|
+
## Project Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
{{PROJECT_NAME}}/
|
|
40
|
+
├── src/
|
|
41
|
+
│ ├── main.ts # Entry point and keyboard handling
|
|
42
|
+
│ ├── App.ts # Root layout component
|
|
43
|
+
│ ├── state/
|
|
44
|
+
│ │ ├── metrics.ts # System metrics state
|
|
45
|
+
│ │ ├── theme.ts # Theme management
|
|
46
|
+
│ │ └── logs.ts # Activity log state
|
|
47
|
+
│ └── components/
|
|
48
|
+
│ ├── Header.ts # Top bar with status
|
|
49
|
+
│ ├── Sidebar.ts # Navigation + quick stats
|
|
50
|
+
│ ├── MetricsPanel.ts # CPU, Memory, Disk meters
|
|
51
|
+
│ ├── TrafficPanel.ts # Network and request stats
|
|
52
|
+
│ ├── ServicesPanel.ts # Service status indicators
|
|
53
|
+
│ ├── LogsPanel.ts # Scrollable activity logs
|
|
54
|
+
│ └── Footer.ts # Bottom bar with shortcuts
|
|
55
|
+
├── package.json
|
|
56
|
+
└── README.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Architecture Highlights
|
|
60
|
+
|
|
61
|
+
### Reactive State
|
|
62
|
+
All metrics use signals that automatically propagate changes:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const cpuUsage = signal(45)
|
|
66
|
+
const cpuBar = derived(() => progressBar(cpuUsage.value, 20))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Component Composition
|
|
70
|
+
Panels are composed from smaller, reusable components:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
function Sidebar() {
|
|
74
|
+
box({ children: () => {
|
|
75
|
+
QuickStats()
|
|
76
|
+
Navigation()
|
|
77
|
+
}})
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Theme Integration
|
|
82
|
+
Components use semantic theme colors for consistent styling:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
box({
|
|
86
|
+
variant: 'primary',
|
|
87
|
+
borderColor: t.primary,
|
|
88
|
+
fg: t.text,
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Learn More
|
|
93
|
+
|
|
94
|
+
- [TUI Framework](https://github.com/rlabs-inc/tui)
|
|
95
|
+
- [Signals Library](https://github.com/rlabs-inc/signals)
|