cellery 0.0.4 → 1.0.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/example.js DELETED
@@ -1,258 +0,0 @@
1
- const { EdgeInsets, Color, Cellery, BoxDecoration, Border, Alignment, HotKey, keys } = require('.')
2
- const { Container, Text, Center, Pressable, Scrollable } = require('cellery/components')
3
- const { CelleryRendererTUI } = require('cellery/renderers')
4
- const fs = require('fs')
5
- const repos = [
6
- 'my-first-repo',
7
- 'cellery',
8
- 'git-remote-pear-transport',
9
- 'pear-desktop',
10
- 'pear-runtime',
11
- 'bare-kit',
12
- 'bare-dev',
13
- 'hypercore',
14
- 'hyperswarm',
15
- 'hyperdht',
16
- 'hypercore-crypto',
17
- 'compact-encoding',
18
- 'protomux',
19
- 'b4a',
20
- 'random-access-storage',
21
- 'random-access-file',
22
- 'brittle',
23
- 'quickbit',
24
- 'safety-catch'
25
- ]
26
- // Load file content once
27
- const fileContent = fs.readFileSync('./example.js', 'utf8')
28
- const lines = fileContent.split('\n')
29
-
30
- // Create persistent stateful components outside render function
31
- let selected = 0
32
- let currentView = 'list' // 'list' or 'file'
33
-
34
- // Scrollable for the repo list - maintains its own scroll state
35
- const listScrollable = new Scrollable({
36
- width: '100%',
37
- height: 'calc(100% - 1)',
38
- scrollOffset: 0,
39
- child: new Container({
40
- width: '100%',
41
- height: '100%',
42
- alignment: Alignment.Center,
43
- children: [] // Will be populated in render
44
- })
45
- })
46
-
47
- // Scrollable for file viewer - maintains its own scroll state
48
- const fileScrollable = new Scrollable({
49
- width: '100%',
50
- height: '100%',
51
- scrollOffset: 0,
52
- child: new Container({
53
- width: '100%',
54
- height: '100%',
55
- children: lines.map(
56
- (line) =>
57
- new Text({
58
- value: line,
59
- width: '100%'
60
- })
61
- )
62
- })
63
- })
64
-
65
- // Navigation controls for list view
66
- const listUpControl = new Pressable({
67
- hotkey: new HotKey({ key: keys.ARROW_UP }),
68
- onPress: function () {
69
- selected = selected === 0 ? repos.length - 1 : selected - 1
70
- updateListView()
71
- cellery.render()
72
- }
73
- })
74
-
75
- const listDownControl = new Pressable({
76
- hotkey: new HotKey({ key: keys.ARROW_DOWN }),
77
- onPress: function () {
78
- selected = selected === repos.length - 1 ? 0 : selected + 1
79
- updateListView()
80
- cellery.render()
81
- }
82
- })
83
-
84
- const listEnterControl = new Pressable({
85
- hotkey: new HotKey({ key: keys.ENTER }),
86
- onPress: function () {
87
- currentView = 'file'
88
- fileScrollable.scrollOffset = 0
89
- cellery.update(App())
90
- }
91
- })
92
-
93
- // Navigation controls for file view
94
- const fileUpControl = new Pressable({
95
- hotkey: new HotKey({ key: keys.ARROW_UP }),
96
- onPress: function () {
97
- if (fileScrollable.scrollOffset > 0) {
98
- fileScrollable.scrollOffset--
99
- cellery.render()
100
- }
101
- }
102
- })
103
-
104
- const fileDownControl = new Pressable({
105
- hotkey: new HotKey({ key: keys.ARROW_DOWN }),
106
- onPress: function () {
107
- const viewport = fileScrollable._renderedViewport
108
- if (viewport && fileScrollable.scrollOffset + viewport.itemCount < lines.length) {
109
- fileScrollable.scrollOffset++
110
- cellery.render()
111
- }
112
- }
113
- })
114
-
115
- const fileBackControl = new Pressable({
116
- hotkey: new HotKey({ key: keys.ESC }),
117
- onPress: function () {
118
- currentView = 'list'
119
- cellery.update(App())
120
- }
121
- })
122
-
123
- // Update list view children based on current selection
124
- function updateListView() {
125
- // Auto-scroll to keep selection visible
126
- const viewport = listScrollable._renderedViewport
127
- if (viewport && viewport.itemCount > 0) {
128
- const positionInViewport = selected - listScrollable.scrollOffset
129
- const triggerDistance = 1
130
- // Scroll down if selection is too close to bottom
131
- if (positionInViewport >= viewport.itemCount - triggerDistance) {
132
- listScrollable.scrollOffset = Math.min(
133
- repos.length - viewport.itemCount,
134
- selected - viewport.itemCount + triggerDistance + 1
135
- )
136
- }
137
- // Scroll up if selection is too close to top
138
- if (positionInViewport < triggerDistance) {
139
- listScrollable.scrollOffset = Math.max(0, selected - triggerDistance)
140
- }
141
- }
142
- // Update list children with current selection highlighting
143
- listScrollable.child.children = repos.map(
144
- (name, i) =>
145
- new Container({
146
- width: '50%',
147
- height: 3,
148
- decoration: new BoxDecoration({
149
- border: Border.all({
150
- color: selected === i ? Color.from('#fa0') : Color.from('#bade5b')
151
- })
152
- }),
153
- children: [
154
- new Text({
155
- value: name,
156
- color: Color.from('#fff')
157
- })
158
- ]
159
- })
160
- )
161
- }
162
-
163
- function App() {
164
- // Initialize list on first render
165
- if (listScrollable.child.children.length === 0) {
166
- updateListView()
167
- }
168
-
169
- const header = new Container({
170
- width: '100%',
171
- height: 3,
172
- decoration: new BoxDecoration({
173
- border: Border.all({
174
- color: Color.from('#bade5b')
175
- })
176
- }),
177
- children: [
178
- new Center({
179
- width: '100%',
180
- height: 3,
181
- child: new Text({
182
- value: currentView === 'list' ? 'Pear Git' : `Pear Git - ${repos[selected]}`,
183
- color: Color.from('#fff')
184
- })
185
- })
186
- ]
187
- })
188
-
189
- if (currentView === 'file') {
190
- return new Container({
191
- width: '100%',
192
- height: '100%',
193
- margin: EdgeInsets.all(2),
194
- alignment: Alignment.Center,
195
- decoration: new BoxDecoration({
196
- border: Border.all()
197
- }),
198
- children: [
199
- fileUpControl,
200
- fileDownControl,
201
- fileBackControl,
202
- header,
203
- new Text({
204
- value: 'Use ↑/↓ to scroll, ESC to go back',
205
- color: Color.from({ red: 200, green: 200, blue: 200 })
206
- }),
207
- new Container({
208
- width: '100%',
209
- height: 'calc(100% - 5)',
210
- decoration: new BoxDecoration({
211
- border: Border.all({
212
- color: Color.from('#bade5b')
213
- })
214
- }),
215
- children: [fileScrollable]
216
- })
217
- ]
218
- })
219
- }
220
-
221
- // List view
222
- const footer = new Text({
223
- value: `${selected + 1}/${repos.length} | scroll: ${listScrollable.scrollOffset}`,
224
- color: Color.from({ red: 100, green: 100, blue: 100 })
225
- })
226
-
227
- return new Container({
228
- width: '100%',
229
- height: '100%',
230
- margin: EdgeInsets.all(2),
231
- alignment: Alignment.Center,
232
- decoration: new BoxDecoration({
233
- border: Border.all()
234
- }),
235
- children: [
236
- listUpControl,
237
- listDownControl,
238
- listEnterControl,
239
- header,
240
- new Text({
241
- value: 'Use ↑/↓ to navigate, ENTER to view file',
242
- color: Color.from({ red: 200, green: 200, blue: 200 })
243
- }),
244
- new Container({
245
- width: '100%',
246
- height: '70%',
247
- alignment: Alignment.Center,
248
- children: [listScrollable, footer]
249
- })
250
- ]
251
- })
252
- }
253
- const cellery = new Cellery({
254
- renderer: new CelleryRendererTUI(),
255
- child: App()
256
- })
257
-
258
- cellery.render()
@@ -1,165 +0,0 @@
1
- const { Cell } = require('../base')
2
-
3
- class Center extends Cell {
4
- constructor(opts = {}) {
5
- super(opts)
6
-
7
- this.child = opts.child
8
- }
9
- }
10
-
11
- /**
12
- * Base container class for holding and aligning multiple children
13
- */
14
- class Container extends Cell {
15
- static observedAttributes = ['width', 'height']
16
-
17
- constructor(opts = {}) {
18
- super(opts)
19
- this.width = opts.width
20
- this.height = opts.height
21
- this.alignment = opts.alignment
22
- this.margin = opts.margin
23
- this.padding = opts.padding
24
- this.decoration = opts.decoration
25
- this.color = opts.color
26
- this.children = opts.children || []
27
- }
28
-
29
- attributeChangedCallback(name, oldValue, newValue) {
30
- // console.log('attributeChangedCallback', name, oldValue, newValue)
31
- }
32
- }
33
-
34
- const TextAlign = {
35
- Left: 'left',
36
- Right: 'right',
37
- Center: 'center'
38
- }
39
-
40
- class Text extends Cell {
41
- constructor(opts = {}) {
42
- super(opts)
43
-
44
- this.value = opts.value || ''
45
- this.color = opts.color
46
- this.textAlign = opts.textAlign
47
- }
48
- }
49
-
50
- class Pressable extends Cell {
51
- hotkey = null
52
- #onPress = null
53
-
54
- constructor(opts = {}) {
55
- super(opts)
56
-
57
- this.child = opts.child
58
- this.#onPress = opts.onPress
59
- this.hotkey = opts.hotkey
60
- }
61
-
62
- async onPress() {
63
- if (!this.#onPress) return
64
-
65
- await this.#onPress()
66
- }
67
- }
68
-
69
- /**
70
- * Scrollable component - manages viewport and scroll offset
71
- *
72
- * The framework keeps it simple: this component just tracks scroll state.
73
- * Renderers implement their own scroll triggering logic while maintaining
74
- * consistent UX.
75
- *
76
- * Takes a single child (typically a Container with children array)
77
- */
78
- class Scrollable extends Cell {
79
- constructor(opts = {}) {
80
- super(opts)
81
-
82
- this.width = opts.width
83
- this.height = opts.height
84
- this.child = opts.child // Single child, not children array
85
-
86
- // Scroll state - managed by the component consumer (e.g., your app logic)
87
- this.scrollOffset = opts.scrollOffset || 0
88
-
89
- // Optional: callback when scroll would be useful (renderer can trigger this)
90
- this.onScrollRequest = opts.onScrollRequest || null
91
-
92
- // Renderer will populate this after rendering
93
- this._renderedViewport = null
94
- }
95
-
96
- /**
97
- * Get viewport info from last render
98
- * Renderer populates this during render
99
- */
100
- getViewportInfo() {
101
- return this._renderedViewport || { itemCount: 0, height: 0 }
102
- }
103
-
104
- /**
105
- * Request scroll by delta
106
- * This is called by renderers or application logic to update scroll position
107
- */
108
- requestScroll(delta) {
109
- const newOffset = this.scrollOffset + delta
110
-
111
- if (this.onScrollRequest) {
112
- this.onScrollRequest(newOffset, delta)
113
- }
114
-
115
- return newOffset
116
- }
117
-
118
- /**
119
- * Scroll to make a specific child index visible
120
- * Returns the new scroll offset
121
- */
122
- scrollToIndex(index, viewportHeight) {
123
- if (index < this.scrollOffset) {
124
- // Scroll up to show this item
125
- return index
126
- } else if (index >= this.scrollOffset + viewportHeight) {
127
- // Scroll down to show this item
128
- return index - viewportHeight + 1
129
- }
130
-
131
- return this.scrollOffset
132
- }
133
-
134
- /**
135
- * Check if we can scroll in a direction
136
- * childCount is the number of items in the scrollable content
137
- */
138
- canScroll(direction, viewportHeight, childCount) {
139
- if (direction < 0) {
140
- return this.scrollOffset > 0
141
- } else {
142
- return this.scrollOffset + viewportHeight < childCount
143
- }
144
- }
145
-
146
- /**
147
- * Get the visible range of children for current scroll position
148
- * childCount is the total number of items
149
- */
150
- getVisibleRange(viewportHeight, childCount) {
151
- const start = Math.max(0, this.scrollOffset)
152
- const end = Math.min(childCount, start + viewportHeight)
153
-
154
- return { start, end }
155
- }
156
- }
157
-
158
- module.exports = {
159
- Center,
160
- Container,
161
- Pressable,
162
- Scrollable,
163
- Text,
164
- TextAlign
165
- }
@@ -1,7 +0,0 @@
1
- const base = require('./base')
2
- const List = require('./list')
3
-
4
- module.exports = {
5
- ...base,
6
- List
7
- }
@@ -1,107 +0,0 @@
1
- const { Cell, HotKey, Alignment } = require('../base')
2
- const keys = require('../keys')
3
- const { Scrollable, Pressable, Container } = require('./base')
4
-
5
- class List extends Cell {
6
- constructor(opts = {}) {
7
- super(opts)
8
-
9
- this.children = opts.children || []
10
- this.selected = opts.selected
11
- this.scrollOffset = opts.scrollOffset || 0
12
- this.triggerDistance = opts.triggerDistance || 0
13
- this.viewportItemCount = opts.viewportItemCount || 0
14
- }
15
-
16
- render() {
17
- // Calculate new scroll offset based on viewport info from last render
18
- let newScrollOffset = this.scrollOffset
19
-
20
- if (this.selected >= 0 && this.viewportItemCount) {
21
- if (this.viewportItemCount > 0) {
22
- const positionInViewport = this.selected - this.scrollOffset
23
-
24
- // Scroll down if we're too close to bottom
25
- if (positionInViewport >= this.viewportItemCount - this.triggerDistance) {
26
- newScrollOffset = Math.min(
27
- this.children.length - this.viewportItemCount,
28
- this.selected - this.viewportItemCount + this.triggerDistance + 1
29
- )
30
- }
31
-
32
- // Scroll up if we're too close to top
33
- if (positionInViewport < this.triggerDistance) {
34
- newScrollOffset = Math.max(0, this.selected - this.triggerDistance)
35
- }
36
-
37
- // Clamp to valid range
38
- newScrollOffset = Math.max(
39
- 0,
40
- Math.min(Math.max(0, this.children.length - this.viewportItemCount), newScrollOffset)
41
- )
42
- }
43
- }
44
-
45
- // Navigation controls (invisible, just for hotkeys)
46
- const upControl = new Pressable({
47
- hotkey: new HotKey({ key: keys.ARROW_UP }),
48
- onPress: () => {
49
- const newSelected = this.selected === 0 ? this.children.length - 1 : this.selected - 1
50
- this.emit('navigate', {
51
- selected: newSelected,
52
- scrollOffset: newScrollOffset,
53
- viewportItemCount: this._scrollableRef._renderedViewport.itemCount
54
- })
55
- }
56
- })
57
-
58
- const downControl = new Pressable({
59
- hotkey: new HotKey({ key: keys.ARROW_DOWN }),
60
- onPress: () => {
61
- const newSelected = this.selected === this.children.length - 1 ? 0 : this.selected + 1
62
- this.emit('navigate', {
63
- selected: newSelected,
64
- scrollOffset: newScrollOffset,
65
- viewportItemCount: this._scrollableRef._renderedViewport.itemCount
66
- })
67
- }
68
- })
69
-
70
- // Create container with all children and their pressables
71
- const listContainer = new Container({
72
- width: '100%',
73
- height: '100%', // Use full height available from Scrollable
74
- alignment: Alignment.Center,
75
- children: this.children.map(
76
- (child, i) =>
77
- new Pressable({
78
- hotkey: i === this.selected ? new HotKey({ key: keys.ENTER }) : null,
79
- onPress: () => {
80
- this.emit('select', { selected: this.selected })
81
- },
82
- child
83
- })
84
- )
85
- })
86
-
87
- // Wrap container in scrollable
88
- const scrollable = new Scrollable({
89
- width: '100%',
90
- height: 'calc(100% - 1)', // Reserve 1 row for footer
91
- scrollOffset: newScrollOffset,
92
- child: listContainer
93
- })
94
-
95
- // Store ref for next render
96
- this._scrollableRef = scrollable
97
-
98
- return new Container({
99
- width: '100%',
100
- height: '70%',
101
- alignment: Alignment.Center,
102
- children: [upControl, downControl, scrollable]
103
- })
104
- }
105
- }
106
-
107
- module.exports = List
package/lib/index.js DELETED
@@ -1,7 +0,0 @@
1
- const base = require('./base')
2
- const keys = require('./keys')
3
-
4
- module.exports = {
5
- ...base,
6
- keys
7
- }
package/lib/keys.js DELETED
@@ -1,9 +0,0 @@
1
- module.exports = {
2
- CTRL_C: '\u0003',
3
- ESC: '\u001b',
4
- ENTER: '\r',
5
- ARROW_UP: '\u001b[A',
6
- ARROW_DOWN: '\u001b[B',
7
- ARROW_LEFT: '\u001b[C',
8
- ARROW_RIGHT: '\u001b[D'
9
- }
@@ -1,133 +0,0 @@
1
- function escapeHTML(str) {
2
- return str
3
- .replace(/&/g, '&amp;')
4
- .replace(/</g, '&lt;')
5
- .replace(/>/g, '&gt;')
6
- .replace(/"/g, '&quot;')
7
- .replace(/'/g, '&#039;')
8
- }
9
-
10
- class CelleryRendererHTML {
11
- components = {
12
- Container: function () {
13
- const width = this.width
14
- const height = this.height
15
-
16
- // Calculate margins (outside everything)
17
- const margin = this.margin || { left: 0, top: 0, right: 0, bottom: 0 }
18
- const marginLeft = margin.left
19
- const marginTop = margin.top
20
- const marginRight = margin.right
21
- const marginBottom = margin.bottom
22
-
23
- // Check if border exists
24
- const hasBorder = this.decoration && this.decoration.border
25
- const borderWidth = hasBorder ? 1 : 0
26
-
27
- // Calculate padding (inside decoration/border)
28
- const padding = this.padding || { left: 0, top: 0, right: 0, bottom: 0 }
29
- const paddingLeft = padding.left
30
- const paddingTop = padding.top
31
- const paddingRight = padding.right
32
- const paddingBottom = padding.bottom
33
-
34
- // Build styles
35
- const styles = {
36
- width: `${width}px`,
37
- height: `${height}px`,
38
- margin: `${marginTop}px ${marginRight}px ${marginBottom}px ${marginLeft}px`,
39
- padding: `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px`,
40
- boxSizing: 'border-box',
41
- display: 'flex',
42
- flexDirection: 'column'
43
- }
44
-
45
- if (hasBorder) {
46
- const borderColor = this.decoration.border.color?.toRGBA() || '#000'
47
- styles.border = `${borderWidth}px solid ${borderColor}`
48
- }
49
-
50
- if (this.color) {
51
- styles.backgroundColor = this.color.toRGBA()
52
- }
53
-
54
- // Render children
55
- let childrenHTML = ''
56
- if (this.children && this.children.length > 0) {
57
- for (const child of this.children) {
58
- childrenHTML += this.renderer._renderComponent(child)
59
- }
60
- }
61
-
62
- const styleStr = Object.entries(styles)
63
- .map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${v}`)
64
- .join('; ')
65
-
66
- return `<div style="${styleStr}">${childrenHTML}</div>`
67
- },
68
-
69
- Center: function () {
70
- const width = this.width
71
- const height = this.height
72
-
73
- const styles = {
74
- width: `${width}px`,
75
- height: `${height}px`,
76
- display: 'flex',
77
- justifyContent: 'center',
78
- alignItems: 'center'
79
- }
80
-
81
- // Render child if exists
82
- let childHTML = ''
83
- if (this.child) {
84
- childHTML = this.renderer._renderComponent(this.child)
85
- }
86
-
87
- const styleStr = Object.entries(styles)
88
- .map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${v}`)
89
- .join('; ')
90
-
91
- return `<div style="${styleStr}">${childHTML}</div>`
92
- },
93
-
94
- Text: function () {
95
- const text = String(this.value)
96
- const color = this.color
97
- const colorStr = color
98
- ? `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha || 1})`
99
- : 'inherit'
100
-
101
- return `<span style="color: ${colorStr}; textAlign: ${this.textAlign || 'left'}">${escapeHTML(text)}</span>`
102
- }
103
- }
104
-
105
- _renderComponent(component) {
106
- component.renderer = this
107
- const rendererFn = this.components[component.constructor.name]
108
- return rendererFn.call(component)
109
- }
110
-
111
- render(component) {
112
- const html = this._renderComponent(component)
113
- return `<!DOCTYPE html>
114
- <html>
115
- <head>
116
- <meta charset="UTF-8">
117
- <style>
118
- body {
119
- margin: 0;
120
- padding: 20px;
121
- font-family: monospace;
122
- background: #f0f0f0;
123
- }
124
- </style>
125
- </head>
126
- <body>
127
- ${html}
128
- </body>
129
- </html>`
130
- }
131
- }
132
-
133
- module.exports = { CelleryRendererHTML }
@@ -1,4 +0,0 @@
1
- const html = require('./html-renderer')
2
- const tui = require('./tui-renderer')
3
-
4
- module.exports = { ...html, ...tui }