@brika/ui-kit 0.3.0 → 0.3.1
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/package.json +9 -9
- package/src/__tests__/nodes.test.ts +811 -180
- package/src/descriptors.ts +0 -8
- package/src/index.ts +2 -24
- package/src/nodes/_shared.ts +30 -9
- package/src/nodes/avatar.ts +8 -2
- package/src/nodes/badge.ts +8 -2
- package/src/nodes/button.ts +5 -1
- package/src/nodes/callout.ts +4 -1
- package/src/nodes/chart.ts +4 -1
- package/src/nodes/checkbox.ts +8 -2
- package/src/nodes/code-block.ts +4 -1
- package/src/nodes/divider.ts +4 -1
- package/src/nodes/icon.ts +8 -2
- package/src/nodes/image.ts +8 -2
- package/src/nodes/key-value.ts +4 -1
- package/src/nodes/link.ts +4 -1
- package/src/nodes/markdown.ts +4 -1
- package/src/nodes/progress.ts +4 -1
- package/src/nodes/select.ts +5 -1
- package/src/nodes/skeleton.ts +4 -1
- package/src/nodes/slider.ts +5 -1
- package/src/nodes/spacer.ts +4 -1
- package/src/nodes/stat-value.ts +4 -1
- package/src/nodes/status.ts +4 -1
- package/src/nodes/table.ts +8 -2
- package/src/nodes/tabs.ts +6 -1
- package/src/nodes/text.ts +16 -3
- package/src/nodes/toggle.ts +5 -1
- package/src/nodes/video.ts +4 -1
- package/tailwind-theme.css +115 -0
- package/src/__tests__/define-brick.test.ts +0 -125
- package/src/__tests__/mutations.test.ts +0 -211
- package/src/define-brick.ts +0 -92
- package/src/jsx-dev-runtime.ts +0 -3
- package/src/jsx-runtime.ts +0 -60
- package/src/mutations.ts +0 -79
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for defineBrick
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, expect, test } from 'bun:test';
|
|
6
|
-
import type {
|
|
7
|
-
BrickComponent,
|
|
8
|
-
BrickInstanceContext,
|
|
9
|
-
BrickTypeSpec,
|
|
10
|
-
CompiledBrickType,
|
|
11
|
-
} from '../define-brick';
|
|
12
|
-
import { defineBrick } from '../define-brick';
|
|
13
|
-
import { Stat, Text } from '../nodes';
|
|
14
|
-
|
|
15
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
-
// Helpers
|
|
17
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
const minimalSpec: BrickTypeSpec = {
|
|
20
|
-
id: 'test-brick',
|
|
21
|
-
families: ['sm'],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const fullSpec: BrickTypeSpec = {
|
|
25
|
-
id: 'thermostat',
|
|
26
|
-
name: 'Thermostat',
|
|
27
|
-
description: 'Shows temperature and allows control',
|
|
28
|
-
icon: 'thermometer',
|
|
29
|
-
color: '#ff6b35',
|
|
30
|
-
category: 'climate',
|
|
31
|
-
families: ['sm', 'md', 'lg'],
|
|
32
|
-
minSize: { w: 1, h: 1 },
|
|
33
|
-
maxSize: { w: 6, h: 6 },
|
|
34
|
-
config: [{ name: 'room', type: 'text', label: 'Room name', default: 'Living Room' }],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
-
// Tests
|
|
39
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
describe('defineBrick', () => {
|
|
42
|
-
test('returns a CompiledBrickType with spec and component', () => {
|
|
43
|
-
const component: BrickComponent = () => Text({ content: 'hello' });
|
|
44
|
-
const result = defineBrick(minimalSpec, component);
|
|
45
|
-
|
|
46
|
-
expect(result).toHaveProperty('spec');
|
|
47
|
-
expect(result).toHaveProperty('component');
|
|
48
|
-
expect(result.spec).toBe(minimalSpec);
|
|
49
|
-
expect(result.component).toBe(component);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('preserves the exact spec reference', () => {
|
|
53
|
-
const component: BrickComponent = () => [];
|
|
54
|
-
const result = defineBrick(fullSpec, component);
|
|
55
|
-
expect(result.spec).toBe(fullSpec);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('preserves the exact component function reference', () => {
|
|
59
|
-
const component: BrickComponent = () => Text({ content: 'x' });
|
|
60
|
-
const result = defineBrick(minimalSpec, component);
|
|
61
|
-
expect(result.component).toBe(component);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('component can return a single ComponentNode', () => {
|
|
65
|
-
const component: BrickComponent = (ctx) =>
|
|
66
|
-
Stat({ label: 'Temp', value: ctx.config.temp as number, unit: '°C' });
|
|
67
|
-
const result = defineBrick(minimalSpec, component);
|
|
68
|
-
|
|
69
|
-
const ctx: BrickInstanceContext = {
|
|
70
|
-
instanceId: 'inst-1',
|
|
71
|
-
config: { temp: 21.5 },
|
|
72
|
-
};
|
|
73
|
-
const output = result.component(ctx);
|
|
74
|
-
expect(output).toHaveProperty('type', 'stat-value');
|
|
75
|
-
expect(output).toHaveProperty('value', 21.5);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('component can return an array of ComponentNodes', () => {
|
|
79
|
-
const component: BrickComponent = () => [
|
|
80
|
-
Stat({ label: 'A', value: 1 }),
|
|
81
|
-
Stat({ label: 'B', value: 2 }),
|
|
82
|
-
];
|
|
83
|
-
const result = defineBrick(minimalSpec, component);
|
|
84
|
-
|
|
85
|
-
const ctx: BrickInstanceContext = { instanceId: 'inst-2', config: {} };
|
|
86
|
-
const output = result.component(ctx);
|
|
87
|
-
expect(Array.isArray(output)).toBe(true);
|
|
88
|
-
expect((output as unknown[]).length).toBe(2);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('spec with all fields is preserved', () => {
|
|
92
|
-
const component: BrickComponent = () => [];
|
|
93
|
-
const result = defineBrick(fullSpec, component);
|
|
94
|
-
|
|
95
|
-
expect(result.spec.id).toBe('thermostat');
|
|
96
|
-
expect(result.spec.name).toBe('Thermostat');
|
|
97
|
-
expect(result.spec.description).toBe('Shows temperature and allows control');
|
|
98
|
-
expect(result.spec.icon).toBe('thermometer');
|
|
99
|
-
expect(result.spec.color).toBe('#ff6b35');
|
|
100
|
-
expect(result.spec.category).toBe('climate');
|
|
101
|
-
expect(result.spec.families).toEqual(['sm', 'md', 'lg']);
|
|
102
|
-
expect(result.spec.minSize).toEqual({ w: 1, h: 1 });
|
|
103
|
-
expect(result.spec.maxSize).toEqual({ w: 6, h: 6 });
|
|
104
|
-
expect(result.spec.config).toHaveLength(1);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('component receives instanceId and config', () => {
|
|
108
|
-
let receivedCtx: BrickInstanceContext | null = null;
|
|
109
|
-
const component: BrickComponent = (ctx) => {
|
|
110
|
-
receivedCtx = ctx;
|
|
111
|
-
return Text({ content: 'test' });
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const result = defineBrick(minimalSpec, component);
|
|
115
|
-
const ctx: BrickInstanceContext = {
|
|
116
|
-
instanceId: 'brick-abc',
|
|
117
|
-
config: { key1: 'value1', key2: 42 },
|
|
118
|
-
};
|
|
119
|
-
result.component(ctx);
|
|
120
|
-
|
|
121
|
-
expect(receivedCtx).not.toBeNull();
|
|
122
|
-
expect(receivedCtx!.instanceId).toBe('brick-abc');
|
|
123
|
-
expect(receivedCtx!.config).toEqual({ key1: 'value1', key2: 42 });
|
|
124
|
-
});
|
|
125
|
-
});
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for applyMutations
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, expect, test } from 'bun:test';
|
|
6
|
-
import { MUT } from '../descriptors';
|
|
7
|
-
import type { BoxNode, ButtonNode, ColumnNode, ComponentNode, Mutation, TextNode } from '../index';
|
|
8
|
-
import { applyMutations } from '../mutations';
|
|
9
|
-
|
|
10
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
-
// Helpers
|
|
12
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
const text = (content: string): TextNode => ({ type: 'text', content });
|
|
15
|
-
|
|
16
|
-
const column = (children: ComponentNode[]): ColumnNode => ({ type: 'column', children });
|
|
17
|
-
|
|
18
|
-
/** Extract children from a container node at a given index in the result array. */
|
|
19
|
-
function childrenAt(nodes: ComponentNode[], index: number): ComponentNode[] {
|
|
20
|
-
const node = nodes[index];
|
|
21
|
-
if (node && 'children' in node) return node.children;
|
|
22
|
-
throw new Error(`Node at index ${index} has no children`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
-
// Tests
|
|
27
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
describe('applyMutations', () => {
|
|
30
|
-
describe('create', () => {
|
|
31
|
-
test('appends node when index >= length', () => {
|
|
32
|
-
const body = [text('A')];
|
|
33
|
-
const result = applyMutations(body, [[MUT.CREATE, '1', text('B')]]);
|
|
34
|
-
|
|
35
|
-
expect(result).toHaveLength(2);
|
|
36
|
-
expect(result[1]).toHaveProperty('content', 'B');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('inserts node at index', () => {
|
|
40
|
-
const body = [text('A'), text('C')];
|
|
41
|
-
const result = applyMutations(body, [[MUT.CREATE, '1', text('B')]]);
|
|
42
|
-
|
|
43
|
-
expect(result).toHaveLength(3);
|
|
44
|
-
expect(result[0]).toHaveProperty('content', 'A');
|
|
45
|
-
expect(result[1]).toHaveProperty('content', 'B');
|
|
46
|
-
expect(result[2]).toHaveProperty('content', 'C');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('inserts at beginning', () => {
|
|
50
|
-
const body = [text('B')];
|
|
51
|
-
const result = applyMutations(body, [[MUT.CREATE, '0', text('A')]]);
|
|
52
|
-
|
|
53
|
-
expect(result).toHaveLength(2);
|
|
54
|
-
expect(result[0]).toHaveProperty('content', 'A');
|
|
55
|
-
expect(result[1]).toHaveProperty('content', 'B');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('replace', () => {
|
|
60
|
-
test('replaces node at index in-place', () => {
|
|
61
|
-
const button: ButtonNode = { type: 'button', label: 'Click' };
|
|
62
|
-
const body = [text('A'), text('B')];
|
|
63
|
-
const result = applyMutations(body, [[MUT.REPLACE, '0', button]]);
|
|
64
|
-
|
|
65
|
-
expect(result).toHaveLength(2);
|
|
66
|
-
expect(result[0]).toBe(button);
|
|
67
|
-
expect(result[1]).toHaveProperty('content', 'B');
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('update', () => {
|
|
72
|
-
test('merges props into existing node', () => {
|
|
73
|
-
const body = [text('Hello')];
|
|
74
|
-
const result = applyMutations(body, [[MUT.UPDATE, '0', { content: 'Updated' }]]);
|
|
75
|
-
|
|
76
|
-
expect(result[0]).toHaveProperty('content', 'Updated');
|
|
77
|
-
expect(result[0]).toHaveProperty('type', 'text');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('adds new props without removing existing ones', () => {
|
|
81
|
-
const body = [text('Hello')];
|
|
82
|
-
const result = applyMutations(body, [[MUT.UPDATE, '0', { variant: 'heading' }]]);
|
|
83
|
-
|
|
84
|
-
expect(result[0]).toHaveProperty('content', 'Hello');
|
|
85
|
-
expect(result[0]).toHaveProperty('variant', 'heading');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('removes props listed in removed array', () => {
|
|
89
|
-
const box: BoxNode = { type: 'box', blur: 'sm', padding: 'lg', children: [] };
|
|
90
|
-
const result = applyMutations([box], [[MUT.UPDATE, '0', {}, ['blur']]]);
|
|
91
|
-
|
|
92
|
-
expect(result[0]).not.toHaveProperty('blur');
|
|
93
|
-
expect(result[0]).toHaveProperty('padding', 'lg');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('preserves null as a legitimate prop value', () => {
|
|
97
|
-
const body = [text('Hello')];
|
|
98
|
-
const result = applyMutations(body, [[MUT.UPDATE, '0', { color: null }]]);
|
|
99
|
-
|
|
100
|
-
expect(result[0]).toHaveProperty('color', null);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe('remove', () => {
|
|
105
|
-
test('removes node at index', () => {
|
|
106
|
-
const body = [text('A'), text('B'), text('C')];
|
|
107
|
-
const result = applyMutations(body, [[MUT.REMOVE, '1']]);
|
|
108
|
-
|
|
109
|
-
expect(result).toHaveLength(2);
|
|
110
|
-
expect(result[0]).toHaveProperty('content', 'A');
|
|
111
|
-
expect(result[1]).toHaveProperty('content', 'C');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('removes first node', () => {
|
|
115
|
-
const body = [text('A'), text('B')];
|
|
116
|
-
const result = applyMutations(body, [[MUT.REMOVE, '0']]);
|
|
117
|
-
|
|
118
|
-
expect(result).toHaveLength(1);
|
|
119
|
-
expect(result[0]).toHaveProperty('content', 'B');
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('nested paths', () => {
|
|
124
|
-
test('updates a nested child', () => {
|
|
125
|
-
const body = [column([text('inner')])];
|
|
126
|
-
const result = applyMutations(body, [[MUT.UPDATE, '0.0', { content: 'updated-inner' }]]);
|
|
127
|
-
|
|
128
|
-
const children = childrenAt(result, 0);
|
|
129
|
-
expect(children[0]).toHaveProperty('content', 'updated-inner');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('creates a nested child', () => {
|
|
133
|
-
const body = [column([text('first')])];
|
|
134
|
-
const result = applyMutations(body, [[MUT.CREATE, '0.1', text('second')]]);
|
|
135
|
-
|
|
136
|
-
const children = childrenAt(result, 0);
|
|
137
|
-
expect(children).toHaveLength(2);
|
|
138
|
-
expect(children[1]).toHaveProperty('content', 'second');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('removes a nested child', () => {
|
|
142
|
-
const body = [column([text('A'), text('B')])];
|
|
143
|
-
const result = applyMutations(body, [[MUT.REMOVE, '0.0']]);
|
|
144
|
-
|
|
145
|
-
const children = childrenAt(result, 0);
|
|
146
|
-
expect(children).toHaveLength(1);
|
|
147
|
-
expect(children[0]).toHaveProperty('content', 'B');
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('structural sharing', () => {
|
|
152
|
-
test('siblings keep original references after update', () => {
|
|
153
|
-
const a = text('A');
|
|
154
|
-
const b = text('B');
|
|
155
|
-
const body = [a, b];
|
|
156
|
-
|
|
157
|
-
const result = applyMutations(body, [[MUT.UPDATE, '0', { content: 'A2' }]]);
|
|
158
|
-
|
|
159
|
-
// Updated node is a new reference
|
|
160
|
-
expect(result[0]).not.toBe(a);
|
|
161
|
-
// Sibling keeps original reference
|
|
162
|
-
expect(result[1]).toBe(b);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('parent is new reference but sibling subtrees are shared', () => {
|
|
166
|
-
const child0 = text('unchanged');
|
|
167
|
-
const child1 = text('will-change');
|
|
168
|
-
const body = [column([child0, child1])];
|
|
169
|
-
|
|
170
|
-
const result = applyMutations(body, [[MUT.UPDATE, '0.1', { content: 'changed' }]]);
|
|
171
|
-
|
|
172
|
-
// Parent container is a new reference
|
|
173
|
-
expect(result[0]).not.toBe(body[0]);
|
|
174
|
-
// Unchanged sibling keeps reference
|
|
175
|
-
expect(childrenAt(result, 0)[0]).toBe(child0);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe('batch mutations', () => {
|
|
180
|
-
test('applies multiple mutations in sequence', () => {
|
|
181
|
-
const body = [text('A')];
|
|
182
|
-
const mutations: Mutation[] = [
|
|
183
|
-
[MUT.CREATE, '1', text('B')],
|
|
184
|
-
[MUT.CREATE, '2', text('C')],
|
|
185
|
-
[MUT.UPDATE, '0', { content: 'A2' }],
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
const result = applyMutations(body, mutations);
|
|
189
|
-
|
|
190
|
-
expect(result).toHaveLength(3);
|
|
191
|
-
expect(result[0]).toHaveProperty('content', 'A2');
|
|
192
|
-
expect(result[1]).toHaveProperty('content', 'B');
|
|
193
|
-
expect(result[2]).toHaveProperty('content', 'C');
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('edge cases', () => {
|
|
198
|
-
test('empty mutations returns same array', () => {
|
|
199
|
-
const body = [text('A')];
|
|
200
|
-
const result = applyMutations(body, []);
|
|
201
|
-
expect(result).toBe(body);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('returns nodes unchanged when targeting non-existent nested path on leaf', () => {
|
|
205
|
-
const body = [text('leaf')];
|
|
206
|
-
// text node has no children, so path "0.0" should return nodes unchanged
|
|
207
|
-
const result = applyMutations(body, [[MUT.UPDATE, '0.0', { content: 'nope' }]]);
|
|
208
|
-
expect(result).toBe(body);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
});
|
package/src/define-brick.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* defineBrick — Brick Type Registration
|
|
3
|
-
*
|
|
4
|
-
* Plugins register brick **types** via `defineBrick()`. Each type can be placed
|
|
5
|
-
* multiple times on boards as independent **instances**, each with its own
|
|
6
|
-
* size (w/h grid units), config values, and isolated hooks state.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { PreferenceDefinition } from '@brika/shared';
|
|
10
|
-
import type { ComponentNode } from './nodes';
|
|
11
|
-
|
|
12
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
-
// Types
|
|
14
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
/** Supported brick size families (convention sizes for catalog display) */
|
|
17
|
-
export type BrickFamily = 'sm' | 'md' | 'lg';
|
|
18
|
-
|
|
19
|
-
/** Brick type spec — static metadata for type registration */
|
|
20
|
-
export interface BrickTypeSpec {
|
|
21
|
-
id: string;
|
|
22
|
-
name?: string;
|
|
23
|
-
description?: string;
|
|
24
|
-
icon?: string;
|
|
25
|
-
color?: string;
|
|
26
|
-
category?: string;
|
|
27
|
-
/** Convention sizes for catalog display */
|
|
28
|
-
families: BrickFamily[];
|
|
29
|
-
/** Minimum grid size (default: { w: 1, h: 1 }) */
|
|
30
|
-
minSize?: { w: number; h: number };
|
|
31
|
-
/** Maximum grid size (default: { w: 12, h: 8 }) */
|
|
32
|
-
maxSize?: { w: number; h: number };
|
|
33
|
-
config?: PreferenceDefinition[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Runtime context provided to each brick instance on every render */
|
|
37
|
-
export interface BrickInstanceContext {
|
|
38
|
-
instanceId: string;
|
|
39
|
-
config: Record<string, unknown>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Action handler receives optional payload from the UI */
|
|
43
|
-
export type { ActionHandler as BrickActionHandler } from './nodes';
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Brick component function — called on every render.
|
|
47
|
-
* Receives instance context (config, instanceId).
|
|
48
|
-
* Use hooks (useState, useEffect, useBrickSize, etc.) inside.
|
|
49
|
-
* Pass handler functions directly to component props (onToggle, onPress, onChange).
|
|
50
|
-
* Returns JSX / ComponentNode(s) describing the brick body.
|
|
51
|
-
*/
|
|
52
|
-
export type BrickComponent = (ctx: BrickInstanceContext) => ComponentNode | ComponentNode[];
|
|
53
|
-
|
|
54
|
-
/** Compiled brick type — ready for SDK registration */
|
|
55
|
-
export interface CompiledBrickType {
|
|
56
|
-
spec: BrickTypeSpec;
|
|
57
|
-
component: BrickComponent;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
-
// defineBrick
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Define a board brick type with hooks.
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```tsx
|
|
69
|
-
* export const thermostat = defineBrick({
|
|
70
|
-
* id: 'thermostat',
|
|
71
|
-
* name: 'Thermostat',
|
|
72
|
-
* icon: 'thermometer',
|
|
73
|
-
* families: ['sm', 'md', 'lg'],
|
|
74
|
-
* minSize: { w: 1, h: 1 },
|
|
75
|
-
* maxSize: { w: 6, h: 6 },
|
|
76
|
-
* }, ({ config }) => {
|
|
77
|
-
* const { width, height } = useBrickSize();
|
|
78
|
-
* if (width <= 2 && height <= 2) {
|
|
79
|
-
* return <Stat label="Temp" value="21.5°C" />;
|
|
80
|
-
* }
|
|
81
|
-
* return (
|
|
82
|
-
* <>
|
|
83
|
-
* <Stat label={config.room as string} value={21.5} unit="°C" />
|
|
84
|
-
* <Toggle label="Heating" checked={heating} onToggle="toggle-heat" />
|
|
85
|
-
* </>
|
|
86
|
-
* );
|
|
87
|
-
* });
|
|
88
|
-
* ```
|
|
89
|
-
*/
|
|
90
|
-
export function defineBrick(spec: BrickTypeSpec, component: BrickComponent): CompiledBrickType {
|
|
91
|
-
return { spec, component };
|
|
92
|
-
}
|
package/src/jsx-dev-runtime.ts
DELETED
package/src/jsx-runtime.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom JSX Runtime for BRIKA Card Descriptors
|
|
3
|
-
*
|
|
4
|
-
* Enables Raycast-style JSX DX in plugins:
|
|
5
|
-
*
|
|
6
|
-
* ctx.update(
|
|
7
|
-
* <>
|
|
8
|
-
* <Section title="Status">
|
|
9
|
-
* <Stat label="Temp" value={21} unit="°C" />
|
|
10
|
-
* </Section>
|
|
11
|
-
* <Toggle label="Heat" checked={on} onToggle="toggle" />
|
|
12
|
-
* </>
|
|
13
|
-
* );
|
|
14
|
-
*
|
|
15
|
-
* This is NOT React — JSX compiles to plain ComponentNode descriptors.
|
|
16
|
-
* Configure with: { "jsx": "react-jsx", "jsxImportSource": "@brika/ui-kit" }
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { ComponentNode } from './nodes';
|
|
20
|
-
|
|
21
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
-
// JSX Factory
|
|
23
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
type NodeOrNodes = ComponentNode | ComponentNode[];
|
|
26
|
-
|
|
27
|
-
export function jsx(
|
|
28
|
-
type: ((props: Record<string, unknown>) => NodeOrNodes) | typeof Fragment,
|
|
29
|
-
props: Record<string, unknown>,
|
|
30
|
-
_key?: string
|
|
31
|
-
): NodeOrNodes {
|
|
32
|
-
return (type as (props: Record<string, unknown>) => NodeOrNodes)(props);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const jsxs = jsx;
|
|
36
|
-
|
|
37
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
-
// Fragment
|
|
39
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
export function Fragment(props: {
|
|
42
|
-
children?: NodeOrNodes | (NodeOrNodes | false | null | undefined)[];
|
|
43
|
-
}): ComponentNode[] {
|
|
44
|
-
const { children } = props;
|
|
45
|
-
if (!children && children !== 0) return [];
|
|
46
|
-
if (!Array.isArray(children)) return [children as ComponentNode];
|
|
47
|
-
return (children as unknown[]).flat(Infinity).filter(Boolean) as ComponentNode[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
-
// JSX Type Declarations
|
|
52
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
export namespace JSX {
|
|
55
|
-
export type Element = ComponentNode | ComponentNode[];
|
|
56
|
-
export interface ElementChildrenAttribute {
|
|
57
|
-
children: {};
|
|
58
|
-
}
|
|
59
|
-
export interface IntrinsicElements {}
|
|
60
|
-
}
|
package/src/mutations.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { MUT, type Mutation } from './descriptors';
|
|
2
|
-
import type { ComponentNode } from './nodes';
|
|
3
|
-
|
|
4
|
-
function hasChildren(node: ComponentNode): node is ComponentNode & { children: ComponentNode[] } {
|
|
5
|
-
return 'children' in node;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function applyChanges(
|
|
9
|
-
node: ComponentNode,
|
|
10
|
-
changes: Record<string, unknown>,
|
|
11
|
-
removed?: string[]
|
|
12
|
-
): ComponentNode {
|
|
13
|
-
const updated = { ...node, ...changes };
|
|
14
|
-
if (removed) {
|
|
15
|
-
for (const k of removed) Reflect.deleteProperty(updated, k);
|
|
16
|
-
}
|
|
17
|
-
return updated;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function applyMutations(body: ComponentNode[], mutations: Mutation[]): ComponentNode[] {
|
|
21
|
-
let result = body;
|
|
22
|
-
for (const m of mutations) {
|
|
23
|
-
result = applyOne(result, m);
|
|
24
|
-
}
|
|
25
|
-
return result;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function applyOne(body: ComponentNode[], mutation: Mutation): ComponentNode[] {
|
|
29
|
-
const segments = mutation[1].split('.').map(Number);
|
|
30
|
-
return updateAtPath(body, segments, 0, mutation);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function updateAtPath(
|
|
34
|
-
nodes: ComponentNode[],
|
|
35
|
-
segments: number[],
|
|
36
|
-
depth: number,
|
|
37
|
-
mutation: Mutation
|
|
38
|
-
): ComponentNode[] {
|
|
39
|
-
const idx = segments[depth] ?? 0;
|
|
40
|
-
const isLeaf = depth === segments.length - 1;
|
|
41
|
-
|
|
42
|
-
if (isLeaf) {
|
|
43
|
-
switch (mutation[0]) {
|
|
44
|
-
case MUT.CREATE: {
|
|
45
|
-
const result = [...nodes];
|
|
46
|
-
if (idx >= result.length) {
|
|
47
|
-
result.push(mutation[2]);
|
|
48
|
-
} else {
|
|
49
|
-
result.splice(idx, 0, mutation[2]);
|
|
50
|
-
}
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
case MUT.REPLACE: {
|
|
54
|
-
const result = [...nodes];
|
|
55
|
-
result[idx] = mutation[2];
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
case MUT.UPDATE: {
|
|
59
|
-
const target = nodes[idx];
|
|
60
|
-
if (!target) return nodes;
|
|
61
|
-
const result = [...nodes];
|
|
62
|
-
result[idx] = applyChanges(target, mutation[2], mutation[3]);
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
case MUT.REMOVE: {
|
|
66
|
-
return nodes.filter((_, i) => i !== idx);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const node = nodes[idx];
|
|
72
|
-
if (!node || !hasChildren(node)) return nodes;
|
|
73
|
-
|
|
74
|
-
const updatedChildren = updateAtPath(node.children, segments, depth + 1, mutation);
|
|
75
|
-
|
|
76
|
-
const result = [...nodes];
|
|
77
|
-
result[idx] = { ...node, children: updatedChildren } as ComponentNode;
|
|
78
|
-
return result;
|
|
79
|
-
}
|