freesail 0.0.1 → 0.1.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 +190 -5
- package/docs/A2UX_Protocol.md +183 -0
- package/docs/Agents.md +218 -0
- package/docs/Architecture.md +285 -0
- package/docs/CatalogReference.md +377 -0
- package/docs/GettingStarted.md +230 -0
- package/examples/demo/package.json +21 -0
- package/examples/demo/public/index.html +381 -0
- package/examples/demo/server.js +253 -0
- package/package.json +38 -5
- package/packages/core/package.json +48 -0
- package/packages/core/src/functions.ts +403 -0
- package/packages/core/src/index.ts +214 -0
- package/packages/core/src/parser.ts +270 -0
- package/packages/core/src/protocol.ts +254 -0
- package/packages/core/src/store.ts +452 -0
- package/packages/core/src/transport.ts +439 -0
- package/packages/core/src/types.ts +209 -0
- package/packages/core/tsconfig.json +10 -0
- package/packages/lit-ui/package.json +44 -0
- package/packages/lit-ui/src/catalogs/standard/catalog.json +405 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Badge.ts +96 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Button.ts +147 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Card.ts +78 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Checkbox.ts +94 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Column.ts +66 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Divider.ts +59 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Image.ts +54 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Input.ts +125 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Progress.ts +79 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Row.ts +68 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Select.ts +110 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Spacer.ts +37 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Spinner.ts +76 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Text.ts +86 -0
- package/packages/lit-ui/src/catalogs/standard/elements/index.ts +18 -0
- package/packages/lit-ui/src/catalogs/standard/index.ts +17 -0
- package/packages/lit-ui/src/index.ts +84 -0
- package/packages/lit-ui/src/renderer.ts +211 -0
- package/packages/lit-ui/src/types.ts +49 -0
- package/packages/lit-ui/src/utils/define-props.ts +157 -0
- package/packages/lit-ui/src/utils/index.ts +2 -0
- package/packages/lit-ui/src/utils/registry.ts +139 -0
- package/packages/lit-ui/tsconfig.json +11 -0
- package/packages/server/package.json +61 -0
- package/packages/server/src/adapters/index.ts +5 -0
- package/packages/server/src/adapters/langchain.ts +175 -0
- package/packages/server/src/adapters/openai.ts +209 -0
- package/packages/server/src/catalog-loader.ts +311 -0
- package/packages/server/src/index.ts +142 -0
- package/packages/server/src/stream.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/tsconfig.base.json +23 -0
- package/index.js +0 -3
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select Component
|
|
3
|
+
*
|
|
4
|
+
* A dropdown select with options.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
interface SelectOption {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@customElement('fs-select')
|
|
16
|
+
export class FreesailSelect extends LitElement {
|
|
17
|
+
static override styles = css`
|
|
18
|
+
:host {
|
|
19
|
+
display: block;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.select-wrapper {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: 4px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
label {
|
|
29
|
+
font-size: 0.875rem;
|
|
30
|
+
font-weight: 500;
|
|
31
|
+
color: #374151;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
select {
|
|
35
|
+
font-family: inherit;
|
|
36
|
+
font-size: 1rem;
|
|
37
|
+
padding: 10px 12px;
|
|
38
|
+
border: 1px solid #d1d5db;
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
outline: none;
|
|
41
|
+
background: white;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
select:focus {
|
|
47
|
+
border-color: #2563eb;
|
|
48
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
select:disabled {
|
|
52
|
+
background: #f3f4f6;
|
|
53
|
+
cursor: not-allowed;
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
@property({ type: String }) value = '';
|
|
58
|
+
@property({ type: Array }) options: SelectOption[] = [];
|
|
59
|
+
@property({ type: String }) placeholder = 'Select an option';
|
|
60
|
+
@property({ type: String }) label = '';
|
|
61
|
+
@property({ type: Boolean }) disabled = false;
|
|
62
|
+
@property({ type: String }) bindPath = '';
|
|
63
|
+
|
|
64
|
+
private handleChange(e: Event) {
|
|
65
|
+
const select = e.target as HTMLSelectElement;
|
|
66
|
+
this.value = select.value;
|
|
67
|
+
|
|
68
|
+
this.dispatchEvent(new CustomEvent('fs-change', {
|
|
69
|
+
bubbles: true,
|
|
70
|
+
composed: true,
|
|
71
|
+
detail: {
|
|
72
|
+
value: this.value,
|
|
73
|
+
bindPath: this.bindPath,
|
|
74
|
+
componentId: this.id,
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override render() {
|
|
80
|
+
return html`
|
|
81
|
+
<div class="select-wrapper">
|
|
82
|
+
${this.label ? html`<label>${this.label}</label>` : ''}
|
|
83
|
+
|
|
84
|
+
<select
|
|
85
|
+
.value=${this.value}
|
|
86
|
+
?disabled=${this.disabled}
|
|
87
|
+
@change=${this.handleChange}
|
|
88
|
+
>
|
|
89
|
+
<option value="" disabled ?selected=${!this.value}>
|
|
90
|
+
${this.placeholder}
|
|
91
|
+
</option>
|
|
92
|
+
${this.options.map(opt => html`
|
|
93
|
+
<option
|
|
94
|
+
value=${opt.value}
|
|
95
|
+
?selected=${opt.value === this.value}
|
|
96
|
+
>
|
|
97
|
+
${opt.label}
|
|
98
|
+
</option>
|
|
99
|
+
`)}
|
|
100
|
+
</select>
|
|
101
|
+
</div>
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
declare global {
|
|
107
|
+
interface HTMLElementTagNameMap {
|
|
108
|
+
'fs-select': FreesailSelect;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spacer Component
|
|
3
|
+
*
|
|
4
|
+
* A flexible space component for layout.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
@customElement('fs-spacer')
|
|
11
|
+
export class FreesailSpacer extends LitElement {
|
|
12
|
+
static override styles = css`
|
|
13
|
+
:host {
|
|
14
|
+
display: block;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.spacer.flex {
|
|
18
|
+
flex: 1;
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
@property({ type: String }) size = 'flex';
|
|
23
|
+
|
|
24
|
+
override render() {
|
|
25
|
+
if (this.size === 'flex') {
|
|
26
|
+
return html`<div class="spacer flex"></div>`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return html`<div class="spacer" style="height: ${this.size}; width: ${this.size}"></div>`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare global {
|
|
34
|
+
interface HTMLElementTagNameMap {
|
|
35
|
+
'fs-spacer': FreesailSpacer;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spinner Component
|
|
3
|
+
*
|
|
4
|
+
* A loading spinner indicator.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
type SpinnerSize = 'small' | 'medium' | 'large';
|
|
11
|
+
|
|
12
|
+
@customElement('fs-spinner')
|
|
13
|
+
export class FreesailSpinner extends LitElement {
|
|
14
|
+
static override styles = css`
|
|
15
|
+
:host {
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.spinner {
|
|
22
|
+
border-radius: 50%;
|
|
23
|
+
border-style: solid;
|
|
24
|
+
border-color: currentColor;
|
|
25
|
+
border-top-color: transparent;
|
|
26
|
+
animation: spin 0.8s linear infinite;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.size-small {
|
|
30
|
+
width: 16px;
|
|
31
|
+
height: 16px;
|
|
32
|
+
border-width: 2px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.size-medium {
|
|
36
|
+
width: 24px;
|
|
37
|
+
height: 24px;
|
|
38
|
+
border-width: 3px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.size-large {
|
|
42
|
+
width: 36px;
|
|
43
|
+
height: 36px;
|
|
44
|
+
border-width: 4px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@keyframes spin {
|
|
48
|
+
to {
|
|
49
|
+
transform: rotate(360deg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
@property({ type: String }) size: SpinnerSize = 'medium';
|
|
55
|
+
@property({ type: String }) color = 'currentColor';
|
|
56
|
+
|
|
57
|
+
override render() {
|
|
58
|
+
const classes = `spinner size-${this.size}`;
|
|
59
|
+
const style = `color: ${this.color}`;
|
|
60
|
+
|
|
61
|
+
return html`
|
|
62
|
+
<div
|
|
63
|
+
class=${classes}
|
|
64
|
+
style=${style}
|
|
65
|
+
role="status"
|
|
66
|
+
aria-label="Loading"
|
|
67
|
+
></div>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declare global {
|
|
73
|
+
interface HTMLElementTagNameMap {
|
|
74
|
+
'fs-spinner': FreesailSpinner;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Component
|
|
3
|
+
*
|
|
4
|
+
* A text display component with variant styles.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
type TextVariant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body' | 'caption' | 'overline';
|
|
11
|
+
type FontWeight = 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
|
|
12
|
+
|
|
13
|
+
@customElement('fs-text')
|
|
14
|
+
export class FreesailText extends LitElement {
|
|
15
|
+
static override styles = css`
|
|
16
|
+
:host {
|
|
17
|
+
display: block;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.text {
|
|
21
|
+
margin: 0;
|
|
22
|
+
font-family: inherit;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.h1 { font-size: 2.5rem; line-height: 1.2; }
|
|
26
|
+
.h2 { font-size: 2rem; line-height: 1.25; }
|
|
27
|
+
.h3 { font-size: 1.75rem; line-height: 1.3; }
|
|
28
|
+
.h4 { font-size: 1.5rem; line-height: 1.35; }
|
|
29
|
+
.h5 { font-size: 1.25rem; line-height: 1.4; }
|
|
30
|
+
.h6 { font-size: 1rem; line-height: 1.45; }
|
|
31
|
+
.body { font-size: 1rem; line-height: 1.5; }
|
|
32
|
+
.caption { font-size: 0.875rem; line-height: 1.4; }
|
|
33
|
+
.overline {
|
|
34
|
+
font-size: 0.75rem;
|
|
35
|
+
line-height: 1.4;
|
|
36
|
+
text-transform: uppercase;
|
|
37
|
+
letter-spacing: 0.1em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.weight-light { font-weight: 300; }
|
|
41
|
+
.weight-normal { font-weight: 400; }
|
|
42
|
+
.weight-medium { font-weight: 500; }
|
|
43
|
+
.weight-semibold { font-weight: 600; }
|
|
44
|
+
.weight-bold { font-weight: 700; }
|
|
45
|
+
|
|
46
|
+
.align-left { text-align: left; }
|
|
47
|
+
.align-center { text-align: center; }
|
|
48
|
+
.align-right { text-align: right; }
|
|
49
|
+
.align-justify { text-align: justify; }
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
@property({ type: String }) text = '';
|
|
53
|
+
@property({ type: String }) variant: TextVariant = 'body';
|
|
54
|
+
@property({ type: String }) color = 'inherit';
|
|
55
|
+
@property({ type: String }) weight: FontWeight = 'normal';
|
|
56
|
+
@property({ type: String }) align = 'left';
|
|
57
|
+
|
|
58
|
+
override render() {
|
|
59
|
+
const classes = `text ${this.variant} weight-${this.weight} align-${this.align}`;
|
|
60
|
+
const style = `color: ${this.color}`;
|
|
61
|
+
|
|
62
|
+
// Use appropriate semantic element
|
|
63
|
+
switch (this.variant) {
|
|
64
|
+
case 'h1':
|
|
65
|
+
return html`<h1 class=${classes} style=${style}>${this.text}</h1>`;
|
|
66
|
+
case 'h2':
|
|
67
|
+
return html`<h2 class=${classes} style=${style}>${this.text}</h2>`;
|
|
68
|
+
case 'h3':
|
|
69
|
+
return html`<h3 class=${classes} style=${style}>${this.text}</h3>`;
|
|
70
|
+
case 'h4':
|
|
71
|
+
return html`<h4 class=${classes} style=${style}>${this.text}</h4>`;
|
|
72
|
+
case 'h5':
|
|
73
|
+
return html`<h5 class=${classes} style=${style}>${this.text}</h5>`;
|
|
74
|
+
case 'h6':
|
|
75
|
+
return html`<h6 class=${classes} style=${style}>${this.text}</h6>`;
|
|
76
|
+
default:
|
|
77
|
+
return html`<p class=${classes} style=${style}>${this.text}</p>`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declare global {
|
|
83
|
+
interface HTMLElementTagNameMap {
|
|
84
|
+
'fs-text': FreesailText;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Catalog Elements Index
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { FreesailColumn } from './Column.js';
|
|
6
|
+
export { FreesailRow } from './Row.js';
|
|
7
|
+
export { FreesailText } from './Text.js';
|
|
8
|
+
export { FreesailButton } from './Button.js';
|
|
9
|
+
export { FreesailInput } from './Input.js';
|
|
10
|
+
export { FreesailCard } from './Card.js';
|
|
11
|
+
export { FreesailImage } from './Image.js';
|
|
12
|
+
export { FreesailDivider } from './Divider.js';
|
|
13
|
+
export { FreesailSpacer } from './Spacer.js';
|
|
14
|
+
export { FreesailBadge } from './Badge.js';
|
|
15
|
+
export { FreesailCheckbox } from './Checkbox.js';
|
|
16
|
+
export { FreesailSelect } from './Select.js';
|
|
17
|
+
export { FreesailSpinner } from './Spinner.js';
|
|
18
|
+
export { FreesailProgress } from './Progress.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Catalog
|
|
3
|
+
*
|
|
4
|
+
* Exports the standard A2UI catalog with all components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import catalog from './catalog.json';
|
|
8
|
+
import type { CatalogDefinition } from '../../types.js';
|
|
9
|
+
|
|
10
|
+
// Import all elements to register them
|
|
11
|
+
export * from './elements/index.js';
|
|
12
|
+
|
|
13
|
+
// Export catalog definition
|
|
14
|
+
export const standardCatalog = catalog as unknown as CatalogDefinition;
|
|
15
|
+
|
|
16
|
+
// Export catalog ID for convenience
|
|
17
|
+
export const STANDARD_CATALOG_ID = 'standard_catalog_v1';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freesail Lit UI
|
|
3
|
+
*
|
|
4
|
+
* Web Components library for A2UX Protocol rendering.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type { CatalogDefinition, ComponentDefinition, PropertyDefinition, RendererConfig } from './types.js';
|
|
9
|
+
|
|
10
|
+
// Utils
|
|
11
|
+
export { defineProps, getDefaults, validateProps } from './utils/define-props.js';
|
|
12
|
+
export { ComponentRegistry, registry } from './utils/registry.js';
|
|
13
|
+
|
|
14
|
+
// Renderer
|
|
15
|
+
export { SurfaceRenderer, createSurfaceRenderer } from './renderer.js';
|
|
16
|
+
export type { SurfaceRendererOptions } from './renderer.js';
|
|
17
|
+
|
|
18
|
+
// Standard Catalog
|
|
19
|
+
export {
|
|
20
|
+
standardCatalog,
|
|
21
|
+
STANDARD_CATALOG_ID,
|
|
22
|
+
FreesailColumn,
|
|
23
|
+
FreesailRow,
|
|
24
|
+
FreesailText,
|
|
25
|
+
FreesailButton,
|
|
26
|
+
FreesailInput,
|
|
27
|
+
FreesailCard,
|
|
28
|
+
FreesailImage,
|
|
29
|
+
FreesailDivider,
|
|
30
|
+
FreesailSpacer,
|
|
31
|
+
FreesailBadge,
|
|
32
|
+
FreesailCheckbox,
|
|
33
|
+
FreesailSelect,
|
|
34
|
+
FreesailSpinner,
|
|
35
|
+
FreesailProgress,
|
|
36
|
+
} from './catalogs/standard/index.js';
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Setup Helper
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
import { registry } from './utils/registry.js';
|
|
43
|
+
import { standardCatalog, STANDARD_CATALOG_ID } from './catalogs/standard/index.js';
|
|
44
|
+
import type { LitElement } from 'lit';
|
|
45
|
+
|
|
46
|
+
// Import element classes for registration
|
|
47
|
+
import { FreesailColumn } from './catalogs/standard/elements/Column.js';
|
|
48
|
+
import { FreesailRow } from './catalogs/standard/elements/Row.js';
|
|
49
|
+
import { FreesailText } from './catalogs/standard/elements/Text.js';
|
|
50
|
+
import { FreesailButton } from './catalogs/standard/elements/Button.js';
|
|
51
|
+
import { FreesailInput } from './catalogs/standard/elements/Input.js';
|
|
52
|
+
import { FreesailCard } from './catalogs/standard/elements/Card.js';
|
|
53
|
+
import { FreesailImage } from './catalogs/standard/elements/Image.js';
|
|
54
|
+
import { FreesailDivider } from './catalogs/standard/elements/Divider.js';
|
|
55
|
+
import { FreesailSpacer } from './catalogs/standard/elements/Spacer.js';
|
|
56
|
+
import { FreesailBadge } from './catalogs/standard/elements/Badge.js';
|
|
57
|
+
import { FreesailCheckbox } from './catalogs/standard/elements/Checkbox.js';
|
|
58
|
+
import { FreesailSelect } from './catalogs/standard/elements/Select.js';
|
|
59
|
+
import { FreesailSpinner } from './catalogs/standard/elements/Spinner.js';
|
|
60
|
+
import { FreesailProgress } from './catalogs/standard/elements/Progress.js';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register the standard catalog with all components
|
|
64
|
+
*/
|
|
65
|
+
export function registerStandardCatalog(): void {
|
|
66
|
+
const components = new Map<string, typeof LitElement>([
|
|
67
|
+
['Column', FreesailColumn],
|
|
68
|
+
['Row', FreesailRow],
|
|
69
|
+
['Text', FreesailText],
|
|
70
|
+
['Button', FreesailButton],
|
|
71
|
+
['Input', FreesailInput],
|
|
72
|
+
['Card', FreesailCard],
|
|
73
|
+
['Image', FreesailImage],
|
|
74
|
+
['Divider', FreesailDivider],
|
|
75
|
+
['Spacer', FreesailSpacer],
|
|
76
|
+
['Badge', FreesailBadge],
|
|
77
|
+
['Checkbox', FreesailCheckbox],
|
|
78
|
+
['Select', FreesailSelect],
|
|
79
|
+
['Spinner', FreesailSpinner],
|
|
80
|
+
['Progress', FreesailProgress],
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
registry.registerCatalog(STANDARD_CATALOG_ID, standardCatalog, components, 'fs');
|
|
84
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface Renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders A2UX surfaces to the DOM using registered Web Components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html, render, TemplateResult } from 'lit';
|
|
8
|
+
import type { A2UXComponent, SurfaceStore, Surface } from '@freesail/core';
|
|
9
|
+
import { registry } from './utils/registry.js';
|
|
10
|
+
|
|
11
|
+
export interface SurfaceRendererOptions {
|
|
12
|
+
/** Target container element or selector */
|
|
13
|
+
container: HTMLElement | string;
|
|
14
|
+
/** Surface ID to render */
|
|
15
|
+
surfaceId: string;
|
|
16
|
+
/** Store instance */
|
|
17
|
+
store: SurfaceStore;
|
|
18
|
+
/** Enable debug mode */
|
|
19
|
+
debug?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* SurfaceRenderer dynamically renders A2UX surfaces to the DOM.
|
|
24
|
+
* It subscribes to store changes and re-renders automatically.
|
|
25
|
+
*/
|
|
26
|
+
export class SurfaceRenderer {
|
|
27
|
+
private container: HTMLElement;
|
|
28
|
+
private surfaceId: string;
|
|
29
|
+
private store: SurfaceStore;
|
|
30
|
+
private debug: boolean;
|
|
31
|
+
private unsubscribe: (() => void) | null = null;
|
|
32
|
+
|
|
33
|
+
constructor(options: SurfaceRendererOptions) {
|
|
34
|
+
// Resolve container
|
|
35
|
+
if (typeof options.container === 'string') {
|
|
36
|
+
const el = document.querySelector(options.container);
|
|
37
|
+
if (!el) {
|
|
38
|
+
throw new Error(`Container not found: ${options.container}`);
|
|
39
|
+
}
|
|
40
|
+
this.container = el as HTMLElement;
|
|
41
|
+
} else {
|
|
42
|
+
this.container = options.container;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.surfaceId = options.surfaceId;
|
|
46
|
+
this.store = options.store;
|
|
47
|
+
this.debug = options.debug ?? false;
|
|
48
|
+
|
|
49
|
+
// Subscribe to surface changes
|
|
50
|
+
this.unsubscribe = this.store.subscribeSurface(this.surfaceId, () => {
|
|
51
|
+
this.render();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Initial render
|
|
55
|
+
this.render();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Render the surface to the container
|
|
60
|
+
*/
|
|
61
|
+
render(): void {
|
|
62
|
+
const surface = this.store.getSurface(this.surfaceId);
|
|
63
|
+
|
|
64
|
+
if (!surface) {
|
|
65
|
+
this.log(`Surface not found: ${this.surfaceId}`);
|
|
66
|
+
render(html``, this.container);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rootComponent = this.store.getRootComponent(this.surfaceId);
|
|
71
|
+
|
|
72
|
+
if (!rootComponent) {
|
|
73
|
+
this.log(`No root component for surface: ${this.surfaceId}`);
|
|
74
|
+
render(html``, this.container);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const template = this.renderComponent(rootComponent, surface);
|
|
79
|
+
render(template, this.container);
|
|
80
|
+
|
|
81
|
+
this.log(`Rendered surface: ${this.surfaceId}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Render a single component and its children
|
|
86
|
+
*/
|
|
87
|
+
private renderComponent(component: A2UXComponent, surface: Surface): TemplateResult {
|
|
88
|
+
const tagName = registry.getTagNameForCatalog(surface.catalogId, component.component);
|
|
89
|
+
|
|
90
|
+
if (!tagName) {
|
|
91
|
+
this.log(`Component not registered: ${component.component}`);
|
|
92
|
+
return html`<!-- Unknown component: ${component.component} -->`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build children templates
|
|
96
|
+
let childTemplates: TemplateResult[] = [];
|
|
97
|
+
|
|
98
|
+
if (component.children && Array.isArray(component.children)) {
|
|
99
|
+
childTemplates = component.children.map(childId => {
|
|
100
|
+
const child = this.store.getComponent(this.surfaceId, childId as string);
|
|
101
|
+
if (child) {
|
|
102
|
+
return this.renderComponent(child, surface);
|
|
103
|
+
}
|
|
104
|
+
return html`<!-- Missing child: ${childId} -->`;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Extract component properties (exclude id, component, children)
|
|
109
|
+
const { id: _id, component: _component, children: _children, ...props } = component;
|
|
110
|
+
|
|
111
|
+
// Apply data model bindings
|
|
112
|
+
this.applyDataBindings(props, surface);
|
|
113
|
+
|
|
114
|
+
// Create element dynamically
|
|
115
|
+
return this.createDynamicElement(tagName, props, childTemplates, component.id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply data model bindings to properties
|
|
120
|
+
*/
|
|
121
|
+
private applyDataBindings(props: Record<string, unknown>, surface: Surface): void {
|
|
122
|
+
for (const [key, value] of Object.entries(props)) {
|
|
123
|
+
if (typeof value === 'string' && value.startsWith('$data:')) {
|
|
124
|
+
const path = value.slice(6); // Remove '$data:' prefix
|
|
125
|
+
props[key] = this.store.getDataValue(surface.surfaceId, path);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a dynamic element with properties
|
|
132
|
+
*/
|
|
133
|
+
private createDynamicElement(
|
|
134
|
+
tagName: string,
|
|
135
|
+
props: Record<string, unknown>,
|
|
136
|
+
children: TemplateResult[],
|
|
137
|
+
id: string
|
|
138
|
+
): TemplateResult {
|
|
139
|
+
// Convert props to attribute format for Lit
|
|
140
|
+
const propEntries = Object.entries(props);
|
|
141
|
+
|
|
142
|
+
// Build props string for logging
|
|
143
|
+
const propsStr = propEntries
|
|
144
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
145
|
+
.join(' ');
|
|
146
|
+
|
|
147
|
+
this.log(`Creating <${tagName} ${propsStr}>`);
|
|
148
|
+
|
|
149
|
+
// Use unsafeStatic for dynamic tag names requires lit-html/static
|
|
150
|
+
// For simplicity, we'll use document.createElement approach
|
|
151
|
+
const el = document.createElement(tagName);
|
|
152
|
+
el.id = id;
|
|
153
|
+
|
|
154
|
+
// Set properties
|
|
155
|
+
for (const [key, value] of propEntries) {
|
|
156
|
+
if (value !== undefined && value !== null) {
|
|
157
|
+
(el as unknown as Record<string, unknown>)[key] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Render children into slot
|
|
162
|
+
if (children.length > 0) {
|
|
163
|
+
const childContainer = document.createElement('div');
|
|
164
|
+
render(html`${children}`, childContainer);
|
|
165
|
+
while (childContainer.firstChild) {
|
|
166
|
+
el.appendChild(childContainer.firstChild);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Return as template
|
|
171
|
+
return html`${el}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Update a specific component without full re-render
|
|
176
|
+
*/
|
|
177
|
+
updateComponent(_componentId: string): void {
|
|
178
|
+
// For now, just do a full re-render
|
|
179
|
+
// Future optimization: targeted DOM updates
|
|
180
|
+
this.render();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Destroy the renderer and cleanup
|
|
185
|
+
*/
|
|
186
|
+
destroy(): void {
|
|
187
|
+
if (this.unsubscribe) {
|
|
188
|
+
this.unsubscribe();
|
|
189
|
+
this.unsubscribe = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Clear container
|
|
193
|
+
this.container.innerHTML = '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Debug logging
|
|
198
|
+
*/
|
|
199
|
+
private log(message: string): void {
|
|
200
|
+
if (this.debug) {
|
|
201
|
+
console.log(`[SurfaceRenderer:${this.surfaceId}] ${message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a surface renderer instance
|
|
208
|
+
*/
|
|
209
|
+
export function createSurfaceRenderer(options: SurfaceRendererOptions): SurfaceRenderer {
|
|
210
|
+
return new SurfaceRenderer(options);
|
|
211
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freesail Lit UI Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Property definition in a catalog
|
|
7
|
+
*/
|
|
8
|
+
export interface PropertyDefinition {
|
|
9
|
+
type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
|
|
10
|
+
description?: string;
|
|
11
|
+
default?: unknown;
|
|
12
|
+
enum?: string[];
|
|
13
|
+
reflect?: boolean;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Component definition in a catalog
|
|
19
|
+
*/
|
|
20
|
+
export interface ComponentDefinition {
|
|
21
|
+
description?: string;
|
|
22
|
+
properties?: Record<string, PropertyDefinition>;
|
|
23
|
+
required?: string[];
|
|
24
|
+
slots?: Record<string, { description?: string }>;
|
|
25
|
+
events?: Record<string, { description?: string }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Catalog definition structure
|
|
30
|
+
*/
|
|
31
|
+
export interface CatalogDefinition {
|
|
32
|
+
id: string;
|
|
33
|
+
version: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
components: Record<string, ComponentDefinition>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Surface renderer configuration
|
|
41
|
+
*/
|
|
42
|
+
export interface RendererConfig {
|
|
43
|
+
/** Target container element */
|
|
44
|
+
container: HTMLElement;
|
|
45
|
+
/** Catalog ID to use */
|
|
46
|
+
catalogId: string;
|
|
47
|
+
/** Enable debug mode */
|
|
48
|
+
debug?: boolean;
|
|
49
|
+
}
|