cellery 0.0.2 → 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/decorations.js +157 -0
- package/package.json +6 -24
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
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
class Alignment {
|
|
2
|
+
direction = ''
|
|
3
|
+
justify = ''
|
|
4
|
+
items = ''
|
|
5
|
+
|
|
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
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class Spacing {
|
|
22
|
+
left = 0
|
|
23
|
+
top = 0
|
|
24
|
+
right = 0
|
|
25
|
+
bottom = 0
|
|
26
|
+
|
|
27
|
+
constructor(left, top, right, bottom) {
|
|
28
|
+
this.left = left
|
|
29
|
+
this.top = top
|
|
30
|
+
this.right = right
|
|
31
|
+
this.bottom = bottom
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static all(value) {
|
|
35
|
+
return new Spacing(value, value, value, value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static symmetric({ vertical, horizontal }) {
|
|
39
|
+
return new Spacing(horizontal, vertical, horizontal, vertical)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static only({ left, right, top, bottom }) {
|
|
43
|
+
return new Spacing(left, top, right, bottom)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toString() {
|
|
47
|
+
return `Spacing(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class Color {
|
|
52
|
+
red = 0
|
|
53
|
+
green = 0
|
|
54
|
+
blue = 0
|
|
55
|
+
alpha = 0
|
|
56
|
+
|
|
57
|
+
constructor(red, green, blue, alpha) {
|
|
58
|
+
this.red = red || 0
|
|
59
|
+
this.green = green || 0
|
|
60
|
+
this.blue = blue || 0
|
|
61
|
+
this.alpha = alpha || 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
toString() {
|
|
65
|
+
return `Color(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
toRGBA() {
|
|
69
|
+
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
toRGB() {
|
|
73
|
+
return `rgba(${this.red}, ${this.green}, ${this.blue})`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static from(value, alpha) {
|
|
77
|
+
if (typeof value === 'string' && value.startsWith('#')) {
|
|
78
|
+
return this.#fromHex(value, alpha)
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === 'object') {
|
|
81
|
+
const { red, green, blue, alpha } = value
|
|
82
|
+
return new Color(red, green, blue, alpha)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static #fromHex(hex, alpha = 1) {
|
|
87
|
+
if (typeof hex !== 'string') return null
|
|
88
|
+
|
|
89
|
+
hex = hex.trim().replace(/^#/, '').toLowerCase()
|
|
90
|
+
|
|
91
|
+
if (hex.length === 3) {
|
|
92
|
+
const r = hex[0] + hex[0]
|
|
93
|
+
const g = hex[1] + hex[1]
|
|
94
|
+
const b = hex[2] + hex[2]
|
|
95
|
+
hex = r + g + b
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (hex.length !== 6 || !/^[0-9a-f]{6}$/.test(hex)) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const r = parseInt(hex.slice(0, 2), 16)
|
|
103
|
+
const g = parseInt(hex.slice(2, 4), 16)
|
|
104
|
+
const b = parseInt(hex.slice(4, 6), 16)
|
|
105
|
+
|
|
106
|
+
return new Color(r, g, b, alpha)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class Border {
|
|
111
|
+
width = 0
|
|
112
|
+
color = null
|
|
113
|
+
|
|
114
|
+
constructor(opts = {}) {
|
|
115
|
+
this.width = opts.width || 1
|
|
116
|
+
this.color = opts.color
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static all(opts) {
|
|
120
|
+
return new Border(opts)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// todo: support trlb
|
|
124
|
+
|
|
125
|
+
toString() {
|
|
126
|
+
return `Border(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class BoxDecoration {
|
|
131
|
+
border = null
|
|
132
|
+
|
|
133
|
+
constructor(opts = {}) {
|
|
134
|
+
this.border = opts.border
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
toString() {
|
|
138
|
+
return `BoxDecoration(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const Size = {
|
|
143
|
+
XS: 'xs',
|
|
144
|
+
S: 's',
|
|
145
|
+
M: 'm',
|
|
146
|
+
L: 'l',
|
|
147
|
+
XL: 'xl'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
Alignment,
|
|
152
|
+
BoxDecoration,
|
|
153
|
+
Border,
|
|
154
|
+
Color,
|
|
155
|
+
Spacing,
|
|
156
|
+
Size
|
|
157
|
+
}
|
package/package.json
CHANGED
|
@@ -1,37 +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
|
-
"index.d.ts"
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"lib"
|
|
32
18
|
],
|
|
33
19
|
"devDependencies": {
|
|
34
|
-
"bare-fs": "^4.5.2",
|
|
35
20
|
"brittle": "^3.19.0",
|
|
36
21
|
"lunte": "^1.2.0",
|
|
37
22
|
"prettier": "^3.6.2",
|
|
@@ -53,9 +38,6 @@
|
|
|
53
38
|
},
|
|
54
39
|
"homepage": "https://github.com/holepunchto/cellery",
|
|
55
40
|
"dependencies": {
|
|
56
|
-
"
|
|
57
|
-
"bare-events": "^2.8.2",
|
|
58
|
-
"bare-process": "^4.2.2",
|
|
59
|
-
"graceful-goodbye": "^1.3.3"
|
|
41
|
+
"iambus": "^2.0.6"
|
|
60
42
|
}
|
|
61
43
|
}
|