@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.
@@ -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
- });
@@ -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
- }
@@ -1,3 +0,0 @@
1
- // Dev mode re-export — Bun resolves jsx-dev-runtime in development
2
- // Dev mode uses jsxDEV instead of jsx/jsxs
3
- export { Fragment, type JSX, jsx as jsxDEV } from './jsx-runtime';
@@ -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
- }