@brika/ui-kit 0.3.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 +3 -0
- package/package.json +32 -0
- package/src/__tests__/define-brick.test.ts +125 -0
- package/src/__tests__/mutations.test.ts +211 -0
- package/src/__tests__/nodes.test.ts +1595 -0
- package/src/colors.ts +99 -0
- package/src/define-brick.ts +92 -0
- package/src/descriptors.ts +28 -0
- package/src/index.ts +154 -0
- package/src/jsx-dev-runtime.ts +3 -0
- package/src/jsx-runtime.ts +60 -0
- package/src/mutations.ts +79 -0
- package/src/nodes/_shared.ts +129 -0
- package/src/nodes/avatar.ts +36 -0
- package/src/nodes/badge.ts +29 -0
- package/src/nodes/box.ts +69 -0
- package/src/nodes/button.ts +44 -0
- package/src/nodes/callout.ts +23 -0
- package/src/nodes/chart.ts +43 -0
- package/src/nodes/checkbox.ts +27 -0
- package/src/nodes/code-block.ts +27 -0
- package/src/nodes/column.ts +37 -0
- package/src/nodes/divider.ts +25 -0
- package/src/nodes/grid.ts +44 -0
- package/src/nodes/icon.ts +28 -0
- package/src/nodes/image.ts +29 -0
- package/src/nodes/index.ts +54 -0
- package/src/nodes/key-value.ts +31 -0
- package/src/nodes/link.ts +25 -0
- package/src/nodes/markdown.ts +16 -0
- package/src/nodes/progress.ts +28 -0
- package/src/nodes/row.ts +37 -0
- package/src/nodes/section.ts +42 -0
- package/src/nodes/select.ts +44 -0
- package/src/nodes/skeleton.ts +23 -0
- package/src/nodes/slider.ts +40 -0
- package/src/nodes/spacer.ts +17 -0
- package/src/nodes/stat-value.ts +26 -0
- package/src/nodes/status.ts +20 -0
- package/src/nodes/table.ts +35 -0
- package/src/nodes/tabs.ts +52 -0
- package/src/nodes/text-input.ts +53 -0
- package/src/nodes/text.ts +66 -0
- package/src/nodes/toggle.ts +32 -0
- package/src/nodes/video.ts +24 -0
package/src/colors.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic color tokens for theme-aware brick styling.
|
|
3
|
+
*
|
|
4
|
+
* Tokens resolve to CSS custom-property references (`var(--color-…)`) that the
|
|
5
|
+
* browser evaluates at render time, so bricks automatically adapt to
|
|
6
|
+
* the active light / dark theme without any extra logic.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { colors } from '@brika/ui-kit'
|
|
11
|
+
*
|
|
12
|
+
* <Text color={colors.mutedForeground}>Secondary text</Text>
|
|
13
|
+
* <Box background={colors.card}>Card surface</Box>
|
|
14
|
+
*
|
|
15
|
+
* // Or use shorthand token names — renderers resolve them automatically:
|
|
16
|
+
* <Text color="muted">Secondary text</Text>
|
|
17
|
+
* <Box background="card">Card surface</Box>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Token types
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** Shorthand token names accepted by `color` props (text / icon context). */
|
|
26
|
+
export type ColorToken =
|
|
27
|
+
| 'foreground'
|
|
28
|
+
| 'muted'
|
|
29
|
+
| 'primary'
|
|
30
|
+
| 'secondary'
|
|
31
|
+
| 'accent'
|
|
32
|
+
| 'card'
|
|
33
|
+
| 'destructive'
|
|
34
|
+
| 'success'
|
|
35
|
+
| 'warning'
|
|
36
|
+
| 'info'
|
|
37
|
+
| 'border';
|
|
38
|
+
|
|
39
|
+
/** Shorthand token names accepted by `background` props (surface context). */
|
|
40
|
+
export type BackgroundToken =
|
|
41
|
+
| 'background'
|
|
42
|
+
| 'card'
|
|
43
|
+
| 'muted'
|
|
44
|
+
| 'primary'
|
|
45
|
+
| 'secondary'
|
|
46
|
+
| 'accent'
|
|
47
|
+
| 'destructive';
|
|
48
|
+
|
|
49
|
+
/** Value for a `color` prop — token name or any CSS color string. */
|
|
50
|
+
export type ColorValue = ColorToken | (string & Record<never, never>);
|
|
51
|
+
|
|
52
|
+
/** Value for a `background` prop — token name or any CSS color string. */
|
|
53
|
+
export type BackgroundValue = BackgroundToken | (string & Record<never, never>);
|
|
54
|
+
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
// Runtime color object (var() CSS references)
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export const colors = {
|
|
60
|
+
// Surface
|
|
61
|
+
background: 'var(--color-background)',
|
|
62
|
+
foreground: 'var(--color-foreground)',
|
|
63
|
+
card: 'var(--color-card)',
|
|
64
|
+
cardForeground: 'var(--color-card-foreground)',
|
|
65
|
+
|
|
66
|
+
// Interactive
|
|
67
|
+
primary: 'var(--color-primary)',
|
|
68
|
+
primaryForeground: 'var(--color-primary-foreground)',
|
|
69
|
+
secondary: 'var(--color-secondary)',
|
|
70
|
+
secondaryForeground: 'var(--color-secondary-foreground)',
|
|
71
|
+
muted: 'var(--color-muted)',
|
|
72
|
+
mutedForeground: 'var(--color-muted-foreground)',
|
|
73
|
+
accent: 'var(--color-accent)',
|
|
74
|
+
accentForeground: 'var(--color-accent-foreground)',
|
|
75
|
+
|
|
76
|
+
// Feedback
|
|
77
|
+
destructive: 'var(--color-destructive)',
|
|
78
|
+
destructiveForeground: 'var(--color-destructive-foreground)',
|
|
79
|
+
success: 'var(--color-success)',
|
|
80
|
+
successForeground: 'var(--color-success-foreground)',
|
|
81
|
+
warning: 'var(--color-warning)',
|
|
82
|
+
warningForeground: 'var(--color-warning-foreground)',
|
|
83
|
+
info: 'var(--color-info)',
|
|
84
|
+
infoForeground: 'var(--color-info-foreground)',
|
|
85
|
+
|
|
86
|
+
// UI elements
|
|
87
|
+
border: 'var(--color-border)',
|
|
88
|
+
ring: 'var(--color-ring)',
|
|
89
|
+
|
|
90
|
+
// Data visualization
|
|
91
|
+
data1: 'var(--color-data-1)',
|
|
92
|
+
data2: 'var(--color-data-2)',
|
|
93
|
+
data3: 'var(--color-data-3)',
|
|
94
|
+
data4: 'var(--color-data-4)',
|
|
95
|
+
data5: 'var(--color-data-5)',
|
|
96
|
+
data6: 'var(--color-data-6)',
|
|
97
|
+
data7: 'var(--color-data-7)',
|
|
98
|
+
data8: 'var(--color-data-8)',
|
|
99
|
+
} as const;
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ComponentNode } from './nodes';
|
|
2
|
+
|
|
3
|
+
export interface ActionNode {
|
|
4
|
+
id: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const MUT = { CREATE: 0, REPLACE: 1, UPDATE: 2, REMOVE: 3 } as const;
|
|
10
|
+
|
|
11
|
+
export type Mutation =
|
|
12
|
+
| [op: 0, path: string, node: ComponentNode]
|
|
13
|
+
| [op: 1, path: string, node: ComponentNode]
|
|
14
|
+
| [op: 2, path: string, changes: Record<string, unknown>, removed?: string[]]
|
|
15
|
+
| [op: 3, path: string];
|
|
16
|
+
|
|
17
|
+
export interface BrickDescriptor {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
icon?: string;
|
|
22
|
+
color?: string;
|
|
23
|
+
size: 'sm' | 'md' | 'lg' | 'xl';
|
|
24
|
+
body: ComponentNode[];
|
|
25
|
+
actions?: ActionNode[];
|
|
26
|
+
category?: string;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @brika/ui-kit
|
|
3
|
+
*
|
|
4
|
+
* Descriptor types + component functions for plugin UI bricks.
|
|
5
|
+
* Zero React, zero DOM — consumed by both SDK (plugin-side) and UI app.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Node Types
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
// Shared
|
|
14
|
+
ActionHandler,
|
|
15
|
+
AvatarNode,
|
|
16
|
+
BadgeNode,
|
|
17
|
+
BaseNode,
|
|
18
|
+
BoxNode,
|
|
19
|
+
// Input
|
|
20
|
+
ButtonNode,
|
|
21
|
+
// Feedback
|
|
22
|
+
CalloutNode,
|
|
23
|
+
ChartDataPoint,
|
|
24
|
+
ChartNode,
|
|
25
|
+
ChartSeries,
|
|
26
|
+
CheckboxNode,
|
|
27
|
+
CodeBlockNode,
|
|
28
|
+
ColumnNode,
|
|
29
|
+
ComponentNode,
|
|
30
|
+
DividerNode,
|
|
31
|
+
FlexLayoutProps,
|
|
32
|
+
GridNode,
|
|
33
|
+
// I18n + Intl
|
|
34
|
+
I18nRef,
|
|
35
|
+
IconNode,
|
|
36
|
+
ImageNode,
|
|
37
|
+
IntlRef,
|
|
38
|
+
KeyValueItem,
|
|
39
|
+
KeyValueNode,
|
|
40
|
+
LinkNode,
|
|
41
|
+
MarkdownNode,
|
|
42
|
+
NodeTypeMap,
|
|
43
|
+
ProgressNode,
|
|
44
|
+
// Layout
|
|
45
|
+
RowNode,
|
|
46
|
+
SectionNode,
|
|
47
|
+
SelectNode,
|
|
48
|
+
SelectOption,
|
|
49
|
+
SkeletonNode,
|
|
50
|
+
SliderNode,
|
|
51
|
+
SpacerNode,
|
|
52
|
+
StatusNode,
|
|
53
|
+
StatValueNode,
|
|
54
|
+
TabItem,
|
|
55
|
+
TableColumn,
|
|
56
|
+
TableNode,
|
|
57
|
+
TabsNode,
|
|
58
|
+
TextContent,
|
|
59
|
+
TextInputNode,
|
|
60
|
+
// Data display
|
|
61
|
+
TextNode,
|
|
62
|
+
ToggleNode,
|
|
63
|
+
VideoNode,
|
|
64
|
+
} from './nodes';
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// Component Builders (PascalCase — used with custom jsx-runtime)
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
Avatar,
|
|
72
|
+
Badge,
|
|
73
|
+
Box,
|
|
74
|
+
// Input
|
|
75
|
+
Button,
|
|
76
|
+
// Feedback
|
|
77
|
+
Callout,
|
|
78
|
+
Chart,
|
|
79
|
+
Checkbox,
|
|
80
|
+
CodeBlock,
|
|
81
|
+
Column,
|
|
82
|
+
Divider,
|
|
83
|
+
Grid,
|
|
84
|
+
Icon,
|
|
85
|
+
Image,
|
|
86
|
+
KeyValue,
|
|
87
|
+
Link,
|
|
88
|
+
Markdown,
|
|
89
|
+
Progress,
|
|
90
|
+
// Layout
|
|
91
|
+
Row,
|
|
92
|
+
Section,
|
|
93
|
+
Select,
|
|
94
|
+
Skeleton,
|
|
95
|
+
Slider,
|
|
96
|
+
Spacer,
|
|
97
|
+
Stat,
|
|
98
|
+
Status,
|
|
99
|
+
Table,
|
|
100
|
+
Tabs,
|
|
101
|
+
// Data display
|
|
102
|
+
Text,
|
|
103
|
+
TextInput,
|
|
104
|
+
Toggle,
|
|
105
|
+
Video,
|
|
106
|
+
} from './nodes';
|
|
107
|
+
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
// Descriptors
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export type { ActionNode, BrickDescriptor, Mutation } from './descriptors';
|
|
113
|
+
export { MUT } from './descriptors';
|
|
114
|
+
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
// defineBrick
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export type {
|
|
120
|
+
BrickActionHandler,
|
|
121
|
+
BrickComponent,
|
|
122
|
+
BrickFamily,
|
|
123
|
+
BrickInstanceContext,
|
|
124
|
+
BrickTypeSpec,
|
|
125
|
+
CompiledBrickType,
|
|
126
|
+
} from './define-brick';
|
|
127
|
+
|
|
128
|
+
export { defineBrick } from './define-brick';
|
|
129
|
+
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// Mutation Applicator (shared between Hub and UI)
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export { applyMutations } from './mutations';
|
|
135
|
+
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
// Theme-aware color tokens
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export type { BackgroundToken, BackgroundValue, ColorToken, ColorValue } from './colors';
|
|
141
|
+
export { colors } from './colors';
|
|
142
|
+
|
|
143
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
144
|
+
// Auto-action registration (internal — used by SDK render pipeline)
|
|
145
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
_setActionRegistrar,
|
|
149
|
+
i18nRef,
|
|
150
|
+
intlRef,
|
|
151
|
+
isI18nRef,
|
|
152
|
+
isIntlRef,
|
|
153
|
+
resolveIntlRef,
|
|
154
|
+
} from './nodes';
|
|
@@ -0,0 +1,60 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Base fields shared by all component nodes */
|
|
2
|
+
export interface BaseNode {
|
|
3
|
+
type: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// ── Marker type guard ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function isMarker<T>(brand: string) {
|
|
9
|
+
return (v: unknown): v is T =>
|
|
10
|
+
v != null && typeof v === 'object' && (v as Record<string, unknown>)[brand] === true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ── I18n Reference ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export const i18nRef = (ns: string, key: string, params?: Record<string, string | number>) => ({
|
|
16
|
+
__i18n: true as const,
|
|
17
|
+
ns,
|
|
18
|
+
key,
|
|
19
|
+
params,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type I18nRef = Readonly<ReturnType<typeof i18nRef>>;
|
|
23
|
+
export const isI18nRef = isMarker<I18nRef>('__i18n');
|
|
24
|
+
|
|
25
|
+
// ── Intl Reference ───────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export const intlRef = {
|
|
28
|
+
dateTime: (value: number, options?: Intl.DateTimeFormatOptions) => ({
|
|
29
|
+
__intl: true as const,
|
|
30
|
+
type: 'dateTime' as const,
|
|
31
|
+
value,
|
|
32
|
+
options,
|
|
33
|
+
}),
|
|
34
|
+
number: (value: number, options?: Intl.NumberFormatOptions) => ({
|
|
35
|
+
__intl: true as const,
|
|
36
|
+
type: 'number' as const,
|
|
37
|
+
value,
|
|
38
|
+
options,
|
|
39
|
+
}),
|
|
40
|
+
relativeTime: (value: number, unit: Intl.RelativeTimeFormatUnit) => ({
|
|
41
|
+
__intl: true as const,
|
|
42
|
+
type: 'relativeTime' as const,
|
|
43
|
+
value,
|
|
44
|
+
unit,
|
|
45
|
+
}),
|
|
46
|
+
list: (value: string[], options?: Intl.ListFormatOptions) => ({
|
|
47
|
+
__intl: true as const,
|
|
48
|
+
type: 'list' as const,
|
|
49
|
+
value,
|
|
50
|
+
options,
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type IntlRef = {
|
|
55
|
+
[K in keyof typeof intlRef]: Readonly<ReturnType<(typeof intlRef)[K]>>;
|
|
56
|
+
}[keyof typeof intlRef];
|
|
57
|
+
export const isIntlRef = isMarker<IntlRef>('__intl');
|
|
58
|
+
|
|
59
|
+
export function resolveIntlRef(ref: IntlRef, locale?: string): string {
|
|
60
|
+
if (!locale) return ref.type === 'list' ? ref.value.join(', ') : String(ref.value);
|
|
61
|
+
switch (ref.type) {
|
|
62
|
+
case 'dateTime':
|
|
63
|
+
return new Intl.DateTimeFormat(locale, ref.options).format(ref.value);
|
|
64
|
+
case 'number':
|
|
65
|
+
return new Intl.NumberFormat(locale, ref.options).format(ref.value);
|
|
66
|
+
case 'relativeTime':
|
|
67
|
+
return new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(ref.value, ref.unit);
|
|
68
|
+
case 'list':
|
|
69
|
+
return new Intl.ListFormat(
|
|
70
|
+
locale,
|
|
71
|
+
ref.options ?? { style: 'long', type: 'conjunction' }
|
|
72
|
+
).format(ref.value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type TextContent = string | I18nRef | IntlRef;
|
|
77
|
+
|
|
78
|
+
// ── Auto-action registration ─────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export type ActionHandler = (payload?: Record<string, unknown>) => void;
|
|
81
|
+
|
|
82
|
+
let _registrar: ((handler: ActionHandler) => string) | null = null;
|
|
83
|
+
let _fallbackIdx = 0;
|
|
84
|
+
|
|
85
|
+
export function _setActionRegistrar(fn: ((handler: ActionHandler) => string) | null): void {
|
|
86
|
+
_registrar = fn;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resolveAction(handler: ActionHandler): string {
|
|
90
|
+
if (_registrar) return _registrar(handler);
|
|
91
|
+
return `__action_${_fallbackIdx++}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Layout + Children ────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
export interface FlexLayoutProps {
|
|
97
|
+
gap?: 'sm' | 'md' | 'lg';
|
|
98
|
+
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
99
|
+
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
|
|
100
|
+
wrap?: boolean;
|
|
101
|
+
grow?: boolean;
|
|
102
|
+
width?: string;
|
|
103
|
+
height?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface NodeTypeMap {}
|
|
107
|
+
export type ComponentNode = NodeTypeMap[keyof NodeTypeMap];
|
|
108
|
+
export type Child = ComponentNode | I18nRef | IntlRef | ComponentNode[] | false | null | undefined;
|
|
109
|
+
|
|
110
|
+
export function normalizeChildren(children: Child | Child[]): ComponentNode[] {
|
|
111
|
+
if (!children) return [];
|
|
112
|
+
const items = Array.isArray(children) ? children.flat() : [children];
|
|
113
|
+
const result: ComponentNode[] = [];
|
|
114
|
+
for (const c of items) {
|
|
115
|
+
if (c == null || c === false) continue;
|
|
116
|
+
if (isI18nRef(c)) {
|
|
117
|
+
result.push({
|
|
118
|
+
type: 'text',
|
|
119
|
+
content: c.key,
|
|
120
|
+
i18n: { ns: c.ns, key: c.key, params: c.params },
|
|
121
|
+
} as ComponentNode);
|
|
122
|
+
} else if (isIntlRef(c)) {
|
|
123
|
+
result.push({ type: 'text', content: resolveIntlRef(c), intl: c } as ComponentNode);
|
|
124
|
+
} else {
|
|
125
|
+
result.push(c);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ActionHandler, type BaseNode, resolveAction } from './_shared';
|
|
2
|
+
|
|
3
|
+
export interface AvatarNode extends BaseNode {
|
|
4
|
+
type: 'avatar';
|
|
5
|
+
/** Image URL */
|
|
6
|
+
src?: string;
|
|
7
|
+
/** Fallback text (typically initials, e.g. "MS") */
|
|
8
|
+
fallback?: string;
|
|
9
|
+
/** Lucide icon name shown inside the avatar (overrides fallback text) */
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** Background color for icon/fallback mode */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Accessible label */
|
|
14
|
+
alt?: string;
|
|
15
|
+
/** Display size */
|
|
16
|
+
size?: 'sm' | 'md' | 'lg';
|
|
17
|
+
/** Shape */
|
|
18
|
+
shape?: 'circle' | 'square';
|
|
19
|
+
/** Status indicator */
|
|
20
|
+
status?: 'online' | 'offline' | 'busy' | 'away';
|
|
21
|
+
/** Action dispatched when clicked */
|
|
22
|
+
onPress?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Avatar(
|
|
26
|
+
props: Omit<AvatarNode, 'type' | 'onPress'> & { onPress?: ActionHandler }
|
|
27
|
+
): AvatarNode {
|
|
28
|
+
const { onPress, ...rest } = props;
|
|
29
|
+
return { type: 'avatar', ...rest, onPress: onPress ? resolveAction(onPress) : undefined };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare module './_shared' {
|
|
33
|
+
interface NodeTypeMap {
|
|
34
|
+
avatar: AvatarNode;
|
|
35
|
+
}
|
|
36
|
+
}
|