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 CHANGED
@@ -1,16 +1,78 @@
1
1
  # Cellery
2
2
 
3
- > **WIP** - A deliberately simple cross-platform UI framework
3
+ > **WIP** - A stream-driven cross-platform UI framework
4
4
 
5
- Cellery is a minimalist framework for cross-platform user interfaces. It keeps component internals simple while allowing renderers flexibility in how they display content. The framework provides essential building blocks called "Cells" - stateless, minimal components that renderers implement to allow complex applications to be built through composition.
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
- Core philosophy: cells are stateless and minimal, renderers handle display, applications control behavior.
7
+ ## How it works
8
8
 
9
- ## Why?
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
- GUI, TUI, mobile, browser - all have unique needs. Rather than trying to solve all of these, `Cellery` lets you build functional components, similar to Flutter. Renderers can then choose to implement as much or as little as needed for their use case.
11
+ This publishes events to `Cellery` which you can subscribe to.
12
12
 
13
- Want to render to eInk? Native components rather than React Native? TUI? Implement the `Cells` as you need for your use case while targetting a consistent UX across devices.
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
@@ -0,0 +1,9 @@
1
+ const Cellery = require('./lib/cellery')
2
+ const cells = require('./lib/cells')
3
+ const decoration = require('./lib/decorations')
4
+
5
+ module.exports = {
6
+ Cellery,
7
+ ...cells,
8
+ ...decoration
9
+ }
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
- const { EventEmitter } = require('events')
1
+ class Alignment {
2
+ direction = ''
3
+ justify = ''
4
+ items = ''
2
5
 
3
- const Alignment = {
4
- Left: 'left',
5
- Center: 'center',
6
- Right: 'right'
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 EdgeInsets {
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 EdgeInsets(value, value, value, value)
35
+ return new Spacing(value, value, value, value)
24
36
  }
25
37
 
26
38
  static symmetric({ vertical, horizontal }) {
27
- return new EdgeInsets(horizontal, vertical, horizontal, vertical)
39
+ return new Spacing(horizontal, vertical, horizontal, vertical)
28
40
  }
29
41
 
30
42
  static only({ left, right, top, bottom }) {
31
- return new EdgeInsets(left, top, right, bottom)
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 `BoxDecoration(${Object.entries(this).map(([k, v]) => `${k}: ${v}`)})`
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 HotKey {
129
- constructor(opts = {}) {
130
- this.key = opts.key
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
- class Cell extends EventEmitter {
137
- constructor() {
138
- super()
114
+ constructor(opts = {}) {
115
+ this.width = opts.width || 1
116
+ this.color = opts.color
139
117
  }
140
118
 
141
- setAttribute(key, value) {
142
- const oldValue = this[key]
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
- connectedCallback() {}
151
- disconnectedCallback() {}
152
- adoptedCallback() {}
153
- attributeChangedCallback(name, oldValue, newValue) {}
123
+ // todo: support trlb
154
124
 
155
125
  toString() {
156
- return `${this.constructor.name}(${Object.entries(this)
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 Cellery {
164
- #renderer = null
165
- #child = null
130
+ class BoxDecoration {
131
+ border = null
166
132
 
167
133
  constructor(opts = {}) {
168
- this.#renderer = opts.renderer
169
- this.#child = opts.child
134
+ this.border = opts.border
170
135
  }
171
136
 
172
- update(child) {
173
- this.#child = child
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
- render() {
178
- if (!this.#renderer) {
179
- return this.#child.toString()
180
- }
181
-
182
- return this.#renderer.render(this.#child)
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
- EdgeInsets,
192
- Cell,
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.4",
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": "./lib/index.js"
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
- "bare-ansi-escapes": "^2.2.3",
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
  }