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/README.md +68 -6
- package/index.js +9 -0
- package/lib/cellery.js +24 -0
- package/lib/cells.js +116 -0
- package/lib/{base.js → decorations.js} +47 -85
- package/package.json +5 -26
- package/example.js +0 -258
- package/lib/components/base.js +0 -165
- package/lib/components/index.js +0 -7
- package/lib/components/list.js +0 -107
- package/lib/index.js +0 -7
- package/lib/keys.js +0 -9
- package/lib/renderers/html-renderer.js +0 -133
- package/lib/renderers/index.js +0 -4
- package/lib/renderers/tui-renderer.js +0 -480
package/README.md
CHANGED
|
@@ -1,16 +1,78 @@
|
|
|
1
1
|
# Cellery
|
|
2
2
|
|
|
3
|
-
> **WIP** - A
|
|
3
|
+
> **WIP** - A stream-driven cross-platform UI framework
|
|
4
4
|
|
|
5
|
-
Cellery is a
|
|
5
|
+
Cellery is a minimal UI framework built around streams. Components called **Cells** subscribe to events and emit render instructions — no virtual DOM, no framework lock-in. Any Adapter can consume the output: HTML, TUI, native, mobile, whatever fits your use case.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## How it works
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Cellery sits at the end of a pipeline. Writers (state machines, database change feeds, RPC streams) push events in, and Cells react by emitting render instructions out.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This publishes events to `Cellery` which you can subscribe to.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
```js
|
|
14
|
+
const { Cellery } = require('cellery')
|
|
15
|
+
|
|
16
|
+
pipeline(
|
|
17
|
+
myWriter,
|
|
18
|
+
new Transform({
|
|
19
|
+
transform(status, cb) {
|
|
20
|
+
this.push({ event: 'something-happened', status })
|
|
21
|
+
cb()
|
|
22
|
+
}
|
|
23
|
+
}),
|
|
24
|
+
cellery
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Cells subscribe to events and re-render reactively:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const welcome = new Message({ id: 'welcome', value: 'Welcome', cellery })
|
|
32
|
+
|
|
33
|
+
welcome.sub({ event: 'login' }, (cell, { user }) => {
|
|
34
|
+
cell.value = `Welcome back ${user.displayName}`
|
|
35
|
+
cell.render({ id: 'messages', insert: 'beforeend' })
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or you render Cells at will, passing details how they should render:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const msg = new Message({ value: 'Use /join <invite> to join a room', cellery })
|
|
43
|
+
msg.render({ id: 'messages', insert: 'beforeend' })
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Rendering is handled by `Adapters`. Currently these are simply called by `Cellery` to render components when components manually choose to be re-rendered. By default they pass along meta so your `adapter` can figure out what to do with them. But there's no rules, no lifecycles. Just streams of content and events for your to react to as you need.
|
|
47
|
+
|
|
48
|
+
## Cells
|
|
49
|
+
|
|
50
|
+
We're trying to keep Cells simple. A super basic set of low level components to see how much can be achieved with little.
|
|
51
|
+
|
|
52
|
+
| Cell | Description |
|
|
53
|
+
|-------------|------------------------------------------|
|
|
54
|
+
| `Cell` | Base class for all components |
|
|
55
|
+
| `MultiCell` | Composes multiple cells into one render |
|
|
56
|
+
| `Container` | Layout wrapper with scroll and flex opts |
|
|
57
|
+
| `App` | Root cell, id is always `'app'` |
|
|
58
|
+
| `Text` | Inline text content |
|
|
59
|
+
| `Paragraph` | Block text content |
|
|
60
|
+
| `Input` | Text input, single or multiline |
|
|
61
|
+
|
|
62
|
+
## Decorations
|
|
63
|
+
|
|
64
|
+
Style primitives passed to cells — `Adapters` decide how to apply them. Just more meta for you to render with.
|
|
65
|
+
|
|
66
|
+
- `Color` — RGBA color, construct from hex or object
|
|
67
|
+
- `Spacing` — padding/margin with `all`, `symmetric`, or `only`
|
|
68
|
+
- `Border` — border width and color
|
|
69
|
+
- `BoxDecoration` — wraps border (and future decoration props)
|
|
70
|
+
- `Alignment` — horizontal or vertical layout direction with justify/items
|
|
71
|
+
- `Size` — named size tokens: `xs`, `s`, `m`, `l`, `xl`
|
|
72
|
+
|
|
73
|
+
## Renderers
|
|
74
|
+
|
|
75
|
+
`Adapters` must implement a single `render(cell)` method and return content in whatever format they need. The framework makes no assumptions about output format.
|
|
14
76
|
|
|
15
77
|
## Status
|
|
16
78
|
|
package/index.js
ADDED
package/lib/cellery.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const Iambus = require('iambus')
|
|
2
|
+
|
|
3
|
+
class Cellery extends Iambus {
|
|
4
|
+
constructor(app, adapter) {
|
|
5
|
+
super()
|
|
6
|
+
this.app = app
|
|
7
|
+
this.adapter = adapter
|
|
8
|
+
|
|
9
|
+
this.app.register(this)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// TODO: pipe compat...
|
|
13
|
+
write(data) {
|
|
14
|
+
this.pub(data)
|
|
15
|
+
}
|
|
16
|
+
on(type, cb) {}
|
|
17
|
+
emit(type, stream) {}
|
|
18
|
+
|
|
19
|
+
render() {
|
|
20
|
+
this.app.render()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = Cellery
|
package/lib/cells.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
class Cell {
|
|
2
|
+
constructor(opts = {}) {
|
|
3
|
+
this.id = opts.id
|
|
4
|
+
this.children = opts.children || []
|
|
5
|
+
this.cellery = opts.cellery
|
|
6
|
+
this.padding = opts.padding
|
|
7
|
+
this.margin = opts.margin
|
|
8
|
+
this.color = opts.color
|
|
9
|
+
this.alignment = opts.alignment
|
|
10
|
+
this.decoration = opts.decoration
|
|
11
|
+
this.size = opts.size
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
sub(pattern, cb) {
|
|
15
|
+
this.cellery.sub(pattern).on('data', (d) => cb(this, d))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render(opts = {}) {
|
|
19
|
+
this.cellery.pub({
|
|
20
|
+
event: 'render',
|
|
21
|
+
id: this.id,
|
|
22
|
+
content: this.cellery.adapter.render(this),
|
|
23
|
+
...opts
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
destroy() {
|
|
28
|
+
this.cellery.pub({
|
|
29
|
+
event: 'render',
|
|
30
|
+
id: this.id,
|
|
31
|
+
destroy: true
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
register(cellery) {
|
|
36
|
+
this.cellery = cellery
|
|
37
|
+
for (const c of this.children) {
|
|
38
|
+
c.register(cellery)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class MultiCell {
|
|
44
|
+
constructor(opts = {}) {
|
|
45
|
+
this.id = opts.id
|
|
46
|
+
this.cellery = opts.cellery
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sub(pattern, cb) {
|
|
50
|
+
this.cellery.sub(pattern).on('data', (d) => cb(this, d))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_render() {
|
|
54
|
+
// impl
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render(opts = {}) {
|
|
58
|
+
const cell = this._render()
|
|
59
|
+
cell.register(this.cellery)
|
|
60
|
+
cell.render(opts)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Container extends Cell {
|
|
65
|
+
// TODO: replace with classes
|
|
66
|
+
static ScrollAll = 'all'
|
|
67
|
+
static ScrollVertical = 'vertical'
|
|
68
|
+
static ScrollHorizontal = 'horizontal'
|
|
69
|
+
static ScrollNone = 'none'
|
|
70
|
+
static FlexAuto = 'auto'
|
|
71
|
+
static FlexNone = 'none'
|
|
72
|
+
|
|
73
|
+
constructor(opts = {}) {
|
|
74
|
+
super(opts)
|
|
75
|
+
this.scroll = opts.scroll || Container.ScrollNone
|
|
76
|
+
this.flex = opts.flex || Container.FlexNone
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class App extends Cell {
|
|
81
|
+
constructor(opts = {}) {
|
|
82
|
+
super({ ...opts, id: 'app' })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class Text extends Cell {
|
|
87
|
+
constructor(opts = {}) {
|
|
88
|
+
super(opts)
|
|
89
|
+
this.value = opts.value || ''
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class Paragraph extends Cell {
|
|
94
|
+
constructor(opts = {}) {
|
|
95
|
+
super(opts)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class Input extends Cell {
|
|
100
|
+
constructor(opts = {}) {
|
|
101
|
+
super(opts)
|
|
102
|
+
this.multiline = !!opts.multiline
|
|
103
|
+
this.placeholder = opts.placeholder
|
|
104
|
+
this.type = opts.type || 'text'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
Cell,
|
|
110
|
+
MultiCell,
|
|
111
|
+
Container,
|
|
112
|
+
App,
|
|
113
|
+
Text,
|
|
114
|
+
Paragraph,
|
|
115
|
+
Input
|
|
116
|
+
}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
class Alignment {
|
|
2
|
+
direction = ''
|
|
3
|
+
justify = ''
|
|
4
|
+
items = ''
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
constructor(direction, justify, items) {
|
|
7
|
+
this.direction = direction
|
|
8
|
+
this.justify = justify
|
|
9
|
+
this.items = items
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static Horizontal({ justify, items }) {
|
|
13
|
+
return new Alignment('horizontal', justify, items)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static Vertical({ justify, items }) {
|
|
17
|
+
return new Alignment('vertical', justify, items)
|
|
18
|
+
}
|
|
7
19
|
}
|
|
8
20
|
|
|
9
|
-
class
|
|
21
|
+
class Spacing {
|
|
10
22
|
left = 0
|
|
11
23
|
top = 0
|
|
12
24
|
right = 0
|
|
@@ -20,49 +32,19 @@ class EdgeInsets {
|
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
static all(value) {
|
|
23
|
-
return new
|
|
35
|
+
return new Spacing(value, value, value, value)
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
static symmetric({ vertical, horizontal }) {
|
|
27
|
-
return new
|
|
39
|
+
return new Spacing(horizontal, vertical, horizontal, vertical)
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
static only({ left, right, top, bottom }) {
|
|
31
|
-
return new
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
toString() {
|
|
35
|
-
return `EdgeInsets(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
class Border {
|
|
40
|
-
width = 0
|
|
41
|
-
color = null
|
|
42
|
-
|
|
43
|
-
constructor(opts = {}) {
|
|
44
|
-
this.width = opts.width || 1
|
|
45
|
-
this.color = opts.color
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
static all(opts) {
|
|
49
|
-
return new Border(opts)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
toString() {
|
|
53
|
-
return `Border(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
class BoxDecoration {
|
|
58
|
-
border = null
|
|
59
|
-
|
|
60
|
-
constructor(opts = {}) {
|
|
61
|
-
this.border = opts.border
|
|
43
|
+
return new Spacing(left, top, right, bottom)
|
|
62
44
|
}
|
|
63
45
|
|
|
64
46
|
toString() {
|
|
65
|
-
return `
|
|
47
|
+
return `Spacing(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
66
48
|
}
|
|
67
49
|
}
|
|
68
50
|
|
|
@@ -125,71 +107,51 @@ class Color {
|
|
|
125
107
|
}
|
|
126
108
|
}
|
|
127
109
|
|
|
128
|
-
class
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.ctrl = opts.ctrl ?? false
|
|
132
|
-
this.shift = opts.shift ?? false
|
|
133
|
-
}
|
|
134
|
-
}
|
|
110
|
+
class Border {
|
|
111
|
+
width = 0
|
|
112
|
+
color = null
|
|
135
113
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
114
|
+
constructor(opts = {}) {
|
|
115
|
+
this.width = opts.width || 1
|
|
116
|
+
this.color = opts.color
|
|
139
117
|
}
|
|
140
118
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this[key] = value
|
|
144
|
-
|
|
145
|
-
if (this.constructor.observedAttributes && this.constructor.observedAttributes.includes(key)) {
|
|
146
|
-
this.attributeChangedCallback(key, oldValue, value)
|
|
147
|
-
}
|
|
119
|
+
static all(opts) {
|
|
120
|
+
return new Border(opts)
|
|
148
121
|
}
|
|
149
122
|
|
|
150
|
-
|
|
151
|
-
disconnectedCallback() {}
|
|
152
|
-
adoptedCallback() {}
|
|
153
|
-
attributeChangedCallback(name, oldValue, newValue) {}
|
|
123
|
+
// todo: support trlb
|
|
154
124
|
|
|
155
125
|
toString() {
|
|
156
|
-
return
|
|
157
|
-
.filter(([_, v]) => (Array.isArray(v) ? v.length : v))
|
|
158
|
-
.map(([k, v]) => `${k}: ${Array.isArray(v) ? `[${v.map((vc) => vc.toString())}]` : v}`)
|
|
159
|
-
.join(', ')})`
|
|
126
|
+
return `Border(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
160
127
|
}
|
|
161
128
|
}
|
|
162
129
|
|
|
163
|
-
class
|
|
164
|
-
|
|
165
|
-
#child = null
|
|
130
|
+
class BoxDecoration {
|
|
131
|
+
border = null
|
|
166
132
|
|
|
167
133
|
constructor(opts = {}) {
|
|
168
|
-
this
|
|
169
|
-
this.#child = opts.child
|
|
134
|
+
this.border = opts.border
|
|
170
135
|
}
|
|
171
136
|
|
|
172
|
-
|
|
173
|
-
this
|
|
174
|
-
return this.#renderer.render(child)
|
|
137
|
+
toString() {
|
|
138
|
+
return `BoxDecoration(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
175
139
|
}
|
|
140
|
+
}
|
|
176
141
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
142
|
+
const Size = {
|
|
143
|
+
XS: 'xs',
|
|
144
|
+
S: 's',
|
|
145
|
+
M: 'm',
|
|
146
|
+
L: 'l',
|
|
147
|
+
XL: 'xl'
|
|
184
148
|
}
|
|
185
149
|
|
|
186
150
|
module.exports = {
|
|
187
151
|
Alignment,
|
|
188
|
-
Border,
|
|
189
152
|
BoxDecoration,
|
|
153
|
+
Border,
|
|
190
154
|
Color,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
Cellery,
|
|
194
|
-
HotKey
|
|
155
|
+
Spacing,
|
|
156
|
+
Size
|
|
195
157
|
}
|
package/package.json
CHANGED
|
@@ -1,40 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cellery",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "cellery",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package": "./package.json",
|
|
7
|
-
"./renderers": "./lib/renderers/index.js",
|
|
8
|
-
"./components": "./lib/components/index.js",
|
|
9
7
|
".": {
|
|
10
8
|
"types": "./index.d.ts",
|
|
11
|
-
"default": "./
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"imports": {
|
|
15
|
-
"process": {
|
|
16
|
-
"bare": "bare-process",
|
|
17
|
-
"default": "process"
|
|
18
|
-
},
|
|
19
|
-
"fs": {
|
|
20
|
-
"bare": "bare-fs",
|
|
21
|
-
"default": "fs"
|
|
22
|
-
},
|
|
23
|
-
"events": {
|
|
24
|
-
"bare": "bare-events",
|
|
25
|
-
"default": "events"
|
|
9
|
+
"default": "./index.js"
|
|
26
10
|
}
|
|
27
11
|
},
|
|
12
|
+
"type": "commonjs",
|
|
28
13
|
"files": [
|
|
29
14
|
"package.json",
|
|
30
15
|
"index.js",
|
|
31
16
|
"index.d.ts",
|
|
32
|
-
"lib"
|
|
33
|
-
"demo",
|
|
34
|
-
"example.js"
|
|
17
|
+
"lib"
|
|
35
18
|
],
|
|
36
19
|
"devDependencies": {
|
|
37
|
-
"bare-fs": "^4.5.2",
|
|
38
20
|
"brittle": "^3.19.0",
|
|
39
21
|
"lunte": "^1.2.0",
|
|
40
22
|
"prettier": "^3.6.2",
|
|
@@ -56,9 +38,6 @@
|
|
|
56
38
|
},
|
|
57
39
|
"homepage": "https://github.com/holepunchto/cellery",
|
|
58
40
|
"dependencies": {
|
|
59
|
-
"
|
|
60
|
-
"bare-events": "^2.8.2",
|
|
61
|
-
"bare-process": "^4.2.2",
|
|
62
|
-
"graceful-goodbye": "^1.3.3"
|
|
41
|
+
"iambus": "^2.0.6"
|
|
63
42
|
}
|
|
64
43
|
}
|