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,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card Component
|
|
3
|
+
*
|
|
4
|
+
* A card container with optional title and elevation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
@customElement('fs-card')
|
|
11
|
+
export class FreesailCard extends LitElement {
|
|
12
|
+
static override styles = css`
|
|
13
|
+
:host {
|
|
14
|
+
display: block;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.card {
|
|
18
|
+
background: white;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.card.elevated {
|
|
24
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.card-header {
|
|
28
|
+
border-bottom: 1px solid #e5e7eb;
|
|
29
|
+
padding: 16px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.card-title {
|
|
33
|
+
font-size: 1.125rem;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
margin: 0;
|
|
36
|
+
color: #111827;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.card-subtitle {
|
|
40
|
+
font-size: 0.875rem;
|
|
41
|
+
color: #6b7280;
|
|
42
|
+
margin: 4px 0 0 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.card-content {
|
|
46
|
+
padding: var(--card-padding, 16px);
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
@property({ type: String }) title = '';
|
|
51
|
+
@property({ type: String }) subtitle = '';
|
|
52
|
+
@property({ type: String }) padding = '16px';
|
|
53
|
+
@property({ type: Boolean }) elevated = true;
|
|
54
|
+
|
|
55
|
+
override render() {
|
|
56
|
+
const cardClasses = `card ${this.elevated ? 'elevated' : ''}`;
|
|
57
|
+
|
|
58
|
+
return html`
|
|
59
|
+
<div class=${cardClasses} style="--card-padding: ${this.padding}">
|
|
60
|
+
${this.title || this.subtitle ? html`
|
|
61
|
+
<div class="card-header">
|
|
62
|
+
${this.title ? html`<h3 class="card-title">${this.title}</h3>` : ''}
|
|
63
|
+
${this.subtitle ? html`<p class="card-subtitle">${this.subtitle}</p>` : ''}
|
|
64
|
+
</div>
|
|
65
|
+
` : ''}
|
|
66
|
+
<div class="card-content">
|
|
67
|
+
<slot></slot>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
declare global {
|
|
75
|
+
interface HTMLElementTagNameMap {
|
|
76
|
+
'fs-card': FreesailCard;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkbox Component
|
|
3
|
+
*
|
|
4
|
+
* A checkbox input with label support.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
@customElement('fs-checkbox')
|
|
11
|
+
export class FreesailCheckbox extends LitElement {
|
|
12
|
+
static override styles = css`
|
|
13
|
+
:host {
|
|
14
|
+
display: inline-block;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.checkbox-wrapper {
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 8px;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.checkbox-wrapper.disabled {
|
|
25
|
+
cursor: not-allowed;
|
|
26
|
+
opacity: 0.5;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
input[type="checkbox"] {
|
|
30
|
+
width: 18px;
|
|
31
|
+
height: 18px;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
accent-color: #2563eb;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
input[type="checkbox"]:disabled {
|
|
37
|
+
cursor: not-allowed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
label {
|
|
41
|
+
font-size: 0.875rem;
|
|
42
|
+
color: #374151;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.disabled label {
|
|
47
|
+
cursor: not-allowed;
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
@property({ type: Boolean }) checked = false;
|
|
52
|
+
@property({ type: String }) label = '';
|
|
53
|
+
@property({ type: Boolean }) disabled = false;
|
|
54
|
+
@property({ type: String }) bindPath = '';
|
|
55
|
+
|
|
56
|
+
private handleChange(e: Event) {
|
|
57
|
+
if (this.disabled) return;
|
|
58
|
+
|
|
59
|
+
const input = e.target as HTMLInputElement;
|
|
60
|
+
this.checked = input.checked;
|
|
61
|
+
|
|
62
|
+
this.dispatchEvent(new CustomEvent('fs-change', {
|
|
63
|
+
bubbles: true,
|
|
64
|
+
composed: true,
|
|
65
|
+
detail: {
|
|
66
|
+
value: this.checked,
|
|
67
|
+
bindPath: this.bindPath,
|
|
68
|
+
componentId: this.id,
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override render() {
|
|
74
|
+
const wrapperClasses = `checkbox-wrapper ${this.disabled ? 'disabled' : ''}`;
|
|
75
|
+
|
|
76
|
+
return html`
|
|
77
|
+
<div class=${wrapperClasses}>
|
|
78
|
+
<input
|
|
79
|
+
type="checkbox"
|
|
80
|
+
.checked=${this.checked}
|
|
81
|
+
?disabled=${this.disabled}
|
|
82
|
+
@change=${this.handleChange}
|
|
83
|
+
/>
|
|
84
|
+
${this.label ? html`<label>${this.label}</label>` : ''}
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
declare global {
|
|
91
|
+
interface HTMLElementTagNameMap {
|
|
92
|
+
'fs-checkbox': FreesailCheckbox;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column Component
|
|
3
|
+
*
|
|
4
|
+
* A vertical flex container that stacks children vertically.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
@customElement('fs-column')
|
|
11
|
+
export class FreesailColumn extends LitElement {
|
|
12
|
+
static override styles = css`
|
|
13
|
+
:host {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
@property({ type: String }) gap = '8px';
|
|
21
|
+
@property({ type: String }) align = 'stretch';
|
|
22
|
+
@property({ type: String }) justify = 'start';
|
|
23
|
+
@property({ type: String }) padding = '0';
|
|
24
|
+
|
|
25
|
+
private getAlignItems(): string {
|
|
26
|
+
const map: Record<string, string> = {
|
|
27
|
+
'start': 'flex-start',
|
|
28
|
+
'center': 'center',
|
|
29
|
+
'end': 'flex-end',
|
|
30
|
+
'stretch': 'stretch',
|
|
31
|
+
};
|
|
32
|
+
return map[this.align] || 'stretch';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private getJustifyContent(): string {
|
|
36
|
+
const map: Record<string, string> = {
|
|
37
|
+
'start': 'flex-start',
|
|
38
|
+
'center': 'center',
|
|
39
|
+
'end': 'flex-end',
|
|
40
|
+
'between': 'space-between',
|
|
41
|
+
'around': 'space-around',
|
|
42
|
+
};
|
|
43
|
+
return map[this.justify] || 'flex-start';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override render() {
|
|
47
|
+
const style = `
|
|
48
|
+
gap: ${this.gap};
|
|
49
|
+
align-items: ${this.getAlignItems()};
|
|
50
|
+
justify-content: ${this.getJustifyContent()};
|
|
51
|
+
padding: ${this.padding};
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
return html`
|
|
55
|
+
<div style=${style}>
|
|
56
|
+
<slot></slot>
|
|
57
|
+
</div>
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
declare global {
|
|
63
|
+
interface HTMLElementTagNameMap {
|
|
64
|
+
'fs-column': FreesailColumn;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Divider Component
|
|
3
|
+
*
|
|
4
|
+
* A horizontal or vertical divider line.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
type Orientation = 'horizontal' | 'vertical';
|
|
11
|
+
|
|
12
|
+
@customElement('fs-divider')
|
|
13
|
+
export class FreesailDivider extends LitElement {
|
|
14
|
+
static override styles = css`
|
|
15
|
+
:host {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.divider {
|
|
20
|
+
background-color: var(--divider-color, #e0e0e0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.divider.horizontal {
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: var(--divider-thickness, 1px);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.divider.vertical {
|
|
29
|
+
height: 100%;
|
|
30
|
+
width: var(--divider-thickness, 1px);
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
@property({ type: String }) orientation: Orientation = 'horizontal';
|
|
35
|
+
@property({ type: String }) thickness = '1px';
|
|
36
|
+
@property({ type: String }) color = '#e0e0e0';
|
|
37
|
+
|
|
38
|
+
override render() {
|
|
39
|
+
const style = `
|
|
40
|
+
--divider-thickness: ${this.thickness};
|
|
41
|
+
--divider-color: ${this.color};
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
return html`
|
|
45
|
+
<div
|
|
46
|
+
class="divider ${this.orientation}"
|
|
47
|
+
style=${style}
|
|
48
|
+
role="separator"
|
|
49
|
+
aria-orientation=${this.orientation}
|
|
50
|
+
></div>
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
declare global {
|
|
56
|
+
interface HTMLElementTagNameMap {
|
|
57
|
+
'fs-divider': FreesailDivider;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Component
|
|
3
|
+
*
|
|
4
|
+
* An image display component with fit options.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
type ObjectFit = 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
|
11
|
+
|
|
12
|
+
@customElement('fs-image')
|
|
13
|
+
export class FreesailImage extends LitElement {
|
|
14
|
+
static override styles = css`
|
|
15
|
+
:host {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
img {
|
|
20
|
+
display: block;
|
|
21
|
+
max-width: 100%;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
@property({ type: String }) src = '';
|
|
26
|
+
@property({ type: String }) alt = '';
|
|
27
|
+
@property({ type: String }) width = '';
|
|
28
|
+
@property({ type: String }) height = '';
|
|
29
|
+
@property({ type: String }) fit: ObjectFit = 'cover';
|
|
30
|
+
@property({ type: String }) borderRadius = '0';
|
|
31
|
+
|
|
32
|
+
override render() {
|
|
33
|
+
const style = `
|
|
34
|
+
width: ${this.width || 'auto'};
|
|
35
|
+
height: ${this.height || 'auto'};
|
|
36
|
+
object-fit: ${this.fit};
|
|
37
|
+
border-radius: ${this.borderRadius};
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
return html`
|
|
41
|
+
<img
|
|
42
|
+
src=${this.src}
|
|
43
|
+
alt=${this.alt}
|
|
44
|
+
style=${style}
|
|
45
|
+
/>
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare global {
|
|
51
|
+
interface HTMLElementTagNameMap {
|
|
52
|
+
'fs-image': FreesailImage;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Component
|
|
3
|
+
*
|
|
4
|
+
* A text input field with label and validation support.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
type InputType = 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
11
|
+
|
|
12
|
+
@customElement('fs-input')
|
|
13
|
+
export class FreesailInput extends LitElement {
|
|
14
|
+
static override styles = css`
|
|
15
|
+
:host {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.input-wrapper {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 4px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
label {
|
|
26
|
+
font-size: 0.875rem;
|
|
27
|
+
font-weight: 500;
|
|
28
|
+
color: #374151;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
input {
|
|
32
|
+
font-family: inherit;
|
|
33
|
+
font-size: 1rem;
|
|
34
|
+
padding: 10px 12px;
|
|
35
|
+
border: 1px solid #d1d5db;
|
|
36
|
+
border-radius: 6px;
|
|
37
|
+
outline: none;
|
|
38
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
input:focus {
|
|
42
|
+
border-color: #2563eb;
|
|
43
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
input:disabled {
|
|
47
|
+
background: #f3f4f6;
|
|
48
|
+
cursor: not-allowed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
input.has-error {
|
|
52
|
+
border-color: #dc2626;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input.has-error:focus {
|
|
56
|
+
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.error-message {
|
|
60
|
+
font-size: 0.75rem;
|
|
61
|
+
color: #dc2626;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.required-mark {
|
|
65
|
+
color: #dc2626;
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
@property({ type: String }) value = '';
|
|
70
|
+
@property({ type: String }) placeholder = '';
|
|
71
|
+
@property({ type: String }) label = '';
|
|
72
|
+
@property({ type: String }) type: InputType = 'text';
|
|
73
|
+
@property({ type: Boolean }) disabled = false;
|
|
74
|
+
@property({ type: Boolean }) required = false;
|
|
75
|
+
@property({ type: String }) error = '';
|
|
76
|
+
@property({ type: String }) bindPath = '';
|
|
77
|
+
|
|
78
|
+
private handleInput(e: Event) {
|
|
79
|
+
const input = e.target as HTMLInputElement;
|
|
80
|
+
this.value = input.value;
|
|
81
|
+
|
|
82
|
+
this.dispatchEvent(new CustomEvent('fs-change', {
|
|
83
|
+
bubbles: true,
|
|
84
|
+
composed: true,
|
|
85
|
+
detail: {
|
|
86
|
+
value: this.value,
|
|
87
|
+
bindPath: this.bindPath,
|
|
88
|
+
componentId: this.id,
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override render() {
|
|
94
|
+
return html`
|
|
95
|
+
<div class="input-wrapper">
|
|
96
|
+
${this.label ? html`
|
|
97
|
+
<label>
|
|
98
|
+
${this.label}
|
|
99
|
+
${this.required ? html`<span class="required-mark">*</span>` : ''}
|
|
100
|
+
</label>
|
|
101
|
+
` : ''}
|
|
102
|
+
|
|
103
|
+
<input
|
|
104
|
+
type=${this.type}
|
|
105
|
+
.value=${this.value}
|
|
106
|
+
placeholder=${this.placeholder}
|
|
107
|
+
?disabled=${this.disabled}
|
|
108
|
+
?required=${this.required}
|
|
109
|
+
class=${this.error ? 'has-error' : ''}
|
|
110
|
+
@input=${this.handleInput}
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
${this.error ? html`
|
|
114
|
+
<span class="error-message">${this.error}</span>
|
|
115
|
+
` : ''}
|
|
116
|
+
</div>
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
declare global {
|
|
122
|
+
interface HTMLElementTagNameMap {
|
|
123
|
+
'fs-input': FreesailInput;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Component
|
|
3
|
+
*
|
|
4
|
+
* A progress bar indicator.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
@customElement('fs-progress')
|
|
11
|
+
export class FreesailProgress extends LitElement {
|
|
12
|
+
static override styles = css`
|
|
13
|
+
:host {
|
|
14
|
+
display: block;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.progress-wrapper {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 4px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.progress-track {
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 8px;
|
|
26
|
+
background: #e5e7eb;
|
|
27
|
+
border-radius: 4px;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.progress-bar {
|
|
32
|
+
height: 100%;
|
|
33
|
+
background: var(--progress-color, #2563eb);
|
|
34
|
+
border-radius: 4px;
|
|
35
|
+
transition: width 0.3s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.progress-label {
|
|
39
|
+
font-size: 0.75rem;
|
|
40
|
+
color: #6b7280;
|
|
41
|
+
text-align: right;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
@property({ type: Number }) value = 0;
|
|
46
|
+
@property({ type: Number }) max = 100;
|
|
47
|
+
@property({ type: Boolean }) showLabel = false;
|
|
48
|
+
@property({ type: String }) color = '#2563eb';
|
|
49
|
+
|
|
50
|
+
private get percentage(): number {
|
|
51
|
+
return Math.min(100, Math.max(0, (this.value / this.max) * 100));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override render() {
|
|
55
|
+
return html`
|
|
56
|
+
<div class="progress-wrapper">
|
|
57
|
+
<div class="progress-track">
|
|
58
|
+
<div
|
|
59
|
+
class="progress-bar"
|
|
60
|
+
style="width: ${this.percentage}%; --progress-color: ${this.color}"
|
|
61
|
+
role="progressbar"
|
|
62
|
+
aria-valuenow=${this.value}
|
|
63
|
+
aria-valuemin="0"
|
|
64
|
+
aria-valuemax=${this.max}
|
|
65
|
+
></div>
|
|
66
|
+
</div>
|
|
67
|
+
${this.showLabel ? html`
|
|
68
|
+
<span class="progress-label">${Math.round(this.percentage)}%</span>
|
|
69
|
+
` : ''}
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
declare global {
|
|
76
|
+
interface HTMLElementTagNameMap {
|
|
77
|
+
'fs-progress': FreesailProgress;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row Component
|
|
3
|
+
*
|
|
4
|
+
* A horizontal flex container that arranges children horizontally.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LitElement, html, css } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
|
|
10
|
+
@customElement('fs-row')
|
|
11
|
+
export class FreesailRow extends LitElement {
|
|
12
|
+
static override styles = css`
|
|
13
|
+
:host {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: row;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
@property({ type: String }) gap = '8px';
|
|
21
|
+
@property({ type: String }) align = 'center';
|
|
22
|
+
@property({ type: String }) justify = 'start';
|
|
23
|
+
@property({ type: Boolean }) wrap = false;
|
|
24
|
+
@property({ type: String }) padding = '0';
|
|
25
|
+
|
|
26
|
+
private getAlignItems(): string {
|
|
27
|
+
const map: Record<string, string> = {
|
|
28
|
+
'start': 'flex-start',
|
|
29
|
+
'center': 'center',
|
|
30
|
+
'end': 'flex-end',
|
|
31
|
+
'stretch': 'stretch',
|
|
32
|
+
};
|
|
33
|
+
return map[this.align] || 'center';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private getJustifyContent(): string {
|
|
37
|
+
const map: Record<string, string> = {
|
|
38
|
+
'start': 'flex-start',
|
|
39
|
+
'center': 'center',
|
|
40
|
+
'end': 'flex-end',
|
|
41
|
+
'between': 'space-between',
|
|
42
|
+
'around': 'space-around',
|
|
43
|
+
};
|
|
44
|
+
return map[this.justify] || 'flex-start';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override render() {
|
|
48
|
+
const style = `
|
|
49
|
+
gap: ${this.gap};
|
|
50
|
+
align-items: ${this.getAlignItems()};
|
|
51
|
+
justify-content: ${this.getJustifyContent()};
|
|
52
|
+
flex-wrap: ${this.wrap ? 'wrap' : 'nowrap'};
|
|
53
|
+
padding: ${this.padding};
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
return html`
|
|
57
|
+
<div style=${style}>
|
|
58
|
+
<slot></slot>
|
|
59
|
+
</div>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare global {
|
|
65
|
+
interface HTMLElementTagNameMap {
|
|
66
|
+
'fs-row': FreesailRow;
|
|
67
|
+
}
|
|
68
|
+
}
|