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 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
+ }
@@ -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.2",
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
- "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
- "bare-ansi-escapes": "^2.2.3",
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
  }