@hyperspaceng/neural-web-ui 0.60.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/CHANGELOG.md +329 -0
- package/README.md +601 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package.json +25 -0
- package/example/src/app.css +1 -0
- package/example/src/custom-messages.ts +99 -0
- package/example/src/main.ts +421 -0
- package/example/tsconfig.json +23 -0
- package/example/vite.config.ts +6 -0
- package/package.json +51 -0
- package/scripts/count-prompt-tokens.ts +88 -0
- package/src/ChatPanel.ts +209 -0
- package/src/app.css +68 -0
- package/src/components/AgentInterface.ts +401 -0
- package/src/components/AttachmentTile.ts +107 -0
- package/src/components/ConsoleBlock.ts +72 -0
- package/src/components/CustomProviderCard.ts +100 -0
- package/src/components/ExpandableSection.ts +46 -0
- package/src/components/Input.ts +113 -0
- package/src/components/MessageEditor.ts +402 -0
- package/src/components/MessageList.ts +98 -0
- package/src/components/Messages.ts +383 -0
- package/src/components/ProviderKeyInput.ts +153 -0
- package/src/components/SandboxedIframe.ts +626 -0
- package/src/components/StreamingMessageContainer.ts +103 -0
- package/src/components/ThinkingBlock.ts +43 -0
- package/src/components/message-renderer-registry.ts +28 -0
- package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
- package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/src/dialogs/AttachmentOverlay.ts +636 -0
- package/src/dialogs/CustomProviderDialog.ts +274 -0
- package/src/dialogs/ModelSelector.ts +367 -0
- package/src/dialogs/PersistentStorageDialog.ts +144 -0
- package/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/src/dialogs/SessionListDialog.ts +150 -0
- package/src/dialogs/SettingsDialog.ts +218 -0
- package/src/index.ts +120 -0
- package/src/prompts/prompts.ts +282 -0
- package/src/storage/app-storage.ts +60 -0
- package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/src/storage/store.ts +33 -0
- package/src/storage/stores/custom-providers-store.ts +62 -0
- package/src/storage/stores/provider-keys-store.ts +33 -0
- package/src/storage/stores/sessions-store.ts +136 -0
- package/src/storage/stores/settings-store.ts +34 -0
- package/src/storage/types.ts +206 -0
- package/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/src/tools/artifacts/Console.ts +93 -0
- package/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/src/tools/artifacts/GenericArtifact.ts +117 -0
- package/src/tools/artifacts/HtmlArtifact.ts +195 -0
- package/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +82 -0
- package/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/src/tools/artifacts/SvgArtifact.ts +78 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts-tool-renderer.ts +310 -0
- package/src/tools/artifacts/artifacts.ts +713 -0
- package/src/tools/artifacts/index.ts +7 -0
- package/src/tools/extract-document.ts +275 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/javascript-repl.ts +293 -0
- package/src/tools/renderer-registry.ts +130 -0
- package/src/tools/renderers/BashRenderer.ts +52 -0
- package/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/src/tools/renderers/DefaultRenderer.ts +103 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/src/tools/types.ts +15 -0
- package/src/utils/attachment-utils.ts +472 -0
- package/src/utils/auth-token.ts +22 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/i18n.ts +653 -0
- package/src/utils/model-discovery.ts +277 -0
- package/src/utils/proxy-utils.ts +139 -0
- package/src/utils/test-sessions.ts +2357 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { icon } from "@mariozechner/mini-lit/dist/icons.js";
|
|
2
|
+
import { LitElement } from "lit";
|
|
3
|
+
import { customElement, property } from "lit/decorators.js";
|
|
4
|
+
import { html } from "lit/html.js";
|
|
5
|
+
import { FileSpreadsheet, FileText, X } from "lucide";
|
|
6
|
+
import { AttachmentOverlay } from "../dialogs/AttachmentOverlay.js";
|
|
7
|
+
import type { Attachment } from "../utils/attachment-utils.js";
|
|
8
|
+
import { i18n } from "../utils/i18n.js";
|
|
9
|
+
|
|
10
|
+
@customElement("attachment-tile")
|
|
11
|
+
export class AttachmentTile extends LitElement {
|
|
12
|
+
@property({ type: Object }) attachment!: Attachment;
|
|
13
|
+
@property({ type: Boolean }) showDelete = false;
|
|
14
|
+
@property() onDelete?: () => void;
|
|
15
|
+
|
|
16
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override connectedCallback(): void {
|
|
21
|
+
super.connectedCallback();
|
|
22
|
+
this.style.display = "block";
|
|
23
|
+
this.classList.add("max-h-16");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private handleClick = () => {
|
|
27
|
+
AttachmentOverlay.open(this.attachment);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
override render() {
|
|
31
|
+
const hasPreview = !!this.attachment.preview;
|
|
32
|
+
const isImage = this.attachment.type === "image";
|
|
33
|
+
const isPdf = this.attachment.mimeType === "application/pdf";
|
|
34
|
+
const isExcel =
|
|
35
|
+
this.attachment.mimeType?.includes("spreadsheetml") ||
|
|
36
|
+
this.attachment.fileName.toLowerCase().endsWith(".xlsx") ||
|
|
37
|
+
this.attachment.fileName.toLowerCase().endsWith(".xls");
|
|
38
|
+
|
|
39
|
+
// Choose the appropriate icon
|
|
40
|
+
const getDocumentIcon = () => {
|
|
41
|
+
if (isExcel) return icon(FileSpreadsheet, "md");
|
|
42
|
+
return icon(FileText, "md");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return html`
|
|
46
|
+
<div class="relative group inline-block">
|
|
47
|
+
${
|
|
48
|
+
hasPreview
|
|
49
|
+
? html`
|
|
50
|
+
<div class="relative">
|
|
51
|
+
<img
|
|
52
|
+
src="data:${isImage ? this.attachment.mimeType : "image/png"};base64,${this.attachment.preview}"
|
|
53
|
+
class="w-16 h-16 object-cover rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity"
|
|
54
|
+
alt="${this.attachment.fileName}"
|
|
55
|
+
title="${this.attachment.fileName}"
|
|
56
|
+
@click=${this.handleClick}
|
|
57
|
+
/>
|
|
58
|
+
${
|
|
59
|
+
isPdf
|
|
60
|
+
? html`
|
|
61
|
+
<!-- PDF badge overlay -->
|
|
62
|
+
<div class="absolute bottom-0 left-0 right-0 bg-background/90 px-1 py-0.5 rounded-b-lg">
|
|
63
|
+
<div class="text-[10px] text-muted-foreground text-center font-medium">${i18n("PDF")}</div>
|
|
64
|
+
</div>
|
|
65
|
+
`
|
|
66
|
+
: ""
|
|
67
|
+
}
|
|
68
|
+
</div>
|
|
69
|
+
`
|
|
70
|
+
: html`
|
|
71
|
+
<!-- Fallback: document icon + filename -->
|
|
72
|
+
<div
|
|
73
|
+
class="w-16 h-16 rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity bg-muted text-muted-foreground flex flex-col items-center justify-center p-2"
|
|
74
|
+
@click=${this.handleClick}
|
|
75
|
+
title="${this.attachment.fileName}"
|
|
76
|
+
>
|
|
77
|
+
${getDocumentIcon()}
|
|
78
|
+
<div class="text-[10px] text-center truncate w-full">
|
|
79
|
+
${
|
|
80
|
+
this.attachment.fileName.length > 10
|
|
81
|
+
? `${this.attachment.fileName.substring(0, 8)}...`
|
|
82
|
+
: this.attachment.fileName
|
|
83
|
+
}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
`
|
|
87
|
+
}
|
|
88
|
+
${
|
|
89
|
+
this.showDelete
|
|
90
|
+
? html`
|
|
91
|
+
<button
|
|
92
|
+
@click=${(e: Event) => {
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
this.onDelete?.();
|
|
95
|
+
}}
|
|
96
|
+
class="absolute -top-1 -right-1 w-5 h-5 bg-background hover:bg-muted text-muted-foreground hover:text-foreground rounded-full flex items-center justify-center opacity-100 hover:opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100 transition-opacity border border-input shadow-sm"
|
|
97
|
+
title="${i18n("Remove")}"
|
|
98
|
+
>
|
|
99
|
+
${icon(X, "xs")}
|
|
100
|
+
</button>
|
|
101
|
+
`
|
|
102
|
+
: ""
|
|
103
|
+
}
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { icon } from "@mariozechner/mini-lit";
|
|
2
|
+
import { LitElement } from "lit";
|
|
3
|
+
import { property, state } from "lit/decorators.js";
|
|
4
|
+
import { html } from "lit/html.js";
|
|
5
|
+
import { Check, Copy } from "lucide";
|
|
6
|
+
import { i18n } from "../utils/i18n.js";
|
|
7
|
+
|
|
8
|
+
export class ConsoleBlock extends LitElement {
|
|
9
|
+
@property() content: string = "";
|
|
10
|
+
@property() variant: "default" | "error" = "default";
|
|
11
|
+
@state() private copied = false;
|
|
12
|
+
|
|
13
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override connectedCallback(): void {
|
|
18
|
+
super.connectedCallback();
|
|
19
|
+
this.style.display = "block";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async copy() {
|
|
23
|
+
try {
|
|
24
|
+
await navigator.clipboard.writeText(this.content || "");
|
|
25
|
+
this.copied = true;
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
this.copied = false;
|
|
28
|
+
}, 1500);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error("Copy failed", e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override updated() {
|
|
35
|
+
// Auto-scroll to bottom on content changes
|
|
36
|
+
const container = this.querySelector(".console-scroll") as HTMLElement | null;
|
|
37
|
+
if (container) {
|
|
38
|
+
container.scrollTop = container.scrollHeight;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override render() {
|
|
43
|
+
const isError = this.variant === "error";
|
|
44
|
+
const textClass = isError ? "text-destructive" : "text-foreground";
|
|
45
|
+
|
|
46
|
+
return html`
|
|
47
|
+
<div class="border border-border rounded-lg overflow-hidden">
|
|
48
|
+
<div class="flex items-center justify-between px-3 py-1.5 bg-muted border-b border-border">
|
|
49
|
+
<span class="text-xs text-muted-foreground font-mono">${i18n("console")}</span>
|
|
50
|
+
<button
|
|
51
|
+
@click=${() => this.copy()}
|
|
52
|
+
class="flex items-center gap-1 px-2 py-0.5 text-xs rounded hover:bg-accent text-muted-foreground hover:text-accent-foreground transition-colors"
|
|
53
|
+
title="${i18n("Copy output")}"
|
|
54
|
+
>
|
|
55
|
+
${this.copied ? icon(Check, "sm") : icon(Copy, "sm")}
|
|
56
|
+
${this.copied ? html`<span>${i18n("Copied!")}</span>` : ""}
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="console-scroll overflow-auto max-h-64">
|
|
60
|
+
<pre class="!bg-background !border-0 !rounded-none m-0 p-3 text-xs ${textClass} font-mono whitespace-pre-wrap">
|
|
61
|
+
${this.content || ""}</pre
|
|
62
|
+
>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Register custom element
|
|
70
|
+
if (!customElements.get("console-block")) {
|
|
71
|
+
customElements.define("console-block", ConsoleBlock);
|
|
72
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { i18n } from "@mariozechner/mini-lit";
|
|
2
|
+
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
|
|
3
|
+
import { html, LitElement, type TemplateResult } from "lit";
|
|
4
|
+
import { customElement, property } from "lit/decorators.js";
|
|
5
|
+
import type { CustomProvider } from "../storage/stores/custom-providers-store.js";
|
|
6
|
+
|
|
7
|
+
@customElement("custom-provider-card")
|
|
8
|
+
export class CustomProviderCard extends LitElement {
|
|
9
|
+
@property({ type: Object }) provider!: CustomProvider;
|
|
10
|
+
@property({ type: Boolean }) isAutoDiscovery = false;
|
|
11
|
+
@property({ type: Object }) status?: { modelCount: number; status: "connected" | "disconnected" | "checking" };
|
|
12
|
+
@property() onRefresh?: (provider: CustomProvider) => void;
|
|
13
|
+
@property() onEdit?: (provider: CustomProvider) => void;
|
|
14
|
+
@property() onDelete?: (provider: CustomProvider) => void;
|
|
15
|
+
|
|
16
|
+
protected createRenderRoot() {
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private renderStatus(): TemplateResult {
|
|
21
|
+
if (!this.isAutoDiscovery) {
|
|
22
|
+
return html`
|
|
23
|
+
<div class="text-xs text-muted-foreground mt-1">
|
|
24
|
+
${i18n("Models")}: ${this.provider.models?.length || 0}
|
|
25
|
+
</div>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!this.status) return html``;
|
|
30
|
+
|
|
31
|
+
const statusIcon =
|
|
32
|
+
this.status.status === "connected"
|
|
33
|
+
? html`<span class="text-green-500">●</span>`
|
|
34
|
+
: this.status.status === "checking"
|
|
35
|
+
? html`<span class="text-yellow-500">●</span>`
|
|
36
|
+
: html`<span class="text-red-500">●</span>`;
|
|
37
|
+
|
|
38
|
+
const statusText =
|
|
39
|
+
this.status.status === "connected"
|
|
40
|
+
? `${this.status.modelCount} ${i18n("models")}`
|
|
41
|
+
: this.status.status === "checking"
|
|
42
|
+
? i18n("Checking...")
|
|
43
|
+
: i18n("Disconnected");
|
|
44
|
+
|
|
45
|
+
return html`
|
|
46
|
+
<div class="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
|
47
|
+
${statusIcon} ${statusText}
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render(): TemplateResult {
|
|
53
|
+
return html`
|
|
54
|
+
<div class="border border-border rounded-lg p-4 space-y-2">
|
|
55
|
+
<div class="flex items-center justify-between">
|
|
56
|
+
<div class="flex-1">
|
|
57
|
+
<div class="font-medium text-sm text-foreground">${this.provider.name}</div>
|
|
58
|
+
<div class="text-xs text-muted-foreground mt-1">
|
|
59
|
+
<span class="capitalize">${this.provider.type}</span>
|
|
60
|
+
${this.provider.baseUrl ? html` • ${this.provider.baseUrl}` : ""}
|
|
61
|
+
</div>
|
|
62
|
+
${this.renderStatus()}
|
|
63
|
+
</div>
|
|
64
|
+
<div class="flex gap-2">
|
|
65
|
+
${
|
|
66
|
+
this.isAutoDiscovery && this.onRefresh
|
|
67
|
+
? Button({
|
|
68
|
+
onClick: () => this.onRefresh?.(this.provider),
|
|
69
|
+
variant: "ghost",
|
|
70
|
+
size: "sm",
|
|
71
|
+
children: i18n("Refresh"),
|
|
72
|
+
})
|
|
73
|
+
: ""
|
|
74
|
+
}
|
|
75
|
+
${
|
|
76
|
+
this.onEdit
|
|
77
|
+
? Button({
|
|
78
|
+
onClick: () => this.onEdit?.(this.provider),
|
|
79
|
+
variant: "ghost",
|
|
80
|
+
size: "sm",
|
|
81
|
+
children: i18n("Edit"),
|
|
82
|
+
})
|
|
83
|
+
: ""
|
|
84
|
+
}
|
|
85
|
+
${
|
|
86
|
+
this.onDelete
|
|
87
|
+
? Button({
|
|
88
|
+
onClick: () => this.onDelete?.(this.provider),
|
|
89
|
+
variant: "ghost",
|
|
90
|
+
size: "sm",
|
|
91
|
+
children: i18n("Delete"),
|
|
92
|
+
})
|
|
93
|
+
: ""
|
|
94
|
+
}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { icon } from "@mariozechner/mini-lit";
|
|
2
|
+
import { html, LitElement, type TemplateResult } from "lit";
|
|
3
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
4
|
+
import { ChevronDown, ChevronRight } from "lucide";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reusable expandable section component for tool renderers.
|
|
8
|
+
* Captures children in connectedCallback and re-renders them in the details area.
|
|
9
|
+
*/
|
|
10
|
+
@customElement("expandable-section")
|
|
11
|
+
export class ExpandableSection extends LitElement {
|
|
12
|
+
@property() summary!: string;
|
|
13
|
+
@property({ type: Boolean }) defaultExpanded = false;
|
|
14
|
+
@state() private expanded = false;
|
|
15
|
+
private capturedChildren: Node[] = [];
|
|
16
|
+
|
|
17
|
+
protected createRenderRoot() {
|
|
18
|
+
return this; // light DOM
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override connectedCallback() {
|
|
22
|
+
super.connectedCallback();
|
|
23
|
+
// Capture children before first render
|
|
24
|
+
this.capturedChildren = Array.from(this.childNodes);
|
|
25
|
+
// Clear children (we'll re-insert them in render)
|
|
26
|
+
this.innerHTML = "";
|
|
27
|
+
this.expanded = this.defaultExpanded;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override render(): TemplateResult {
|
|
31
|
+
return html`
|
|
32
|
+
<div>
|
|
33
|
+
<button
|
|
34
|
+
@click=${() => {
|
|
35
|
+
this.expanded = !this.expanded;
|
|
36
|
+
}}
|
|
37
|
+
class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full text-left"
|
|
38
|
+
>
|
|
39
|
+
${icon(this.expanded ? ChevronDown : ChevronRight, "sm")}
|
|
40
|
+
<span>${this.summary}</span>
|
|
41
|
+
</button>
|
|
42
|
+
${this.expanded ? html`<div class="mt-2">${this.capturedChildren}</div>` : ""}
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type BaseComponentProps, fc } from "@mariozechner/mini-lit/dist/mini.js";
|
|
2
|
+
import { html } from "lit";
|
|
3
|
+
import { type Ref, ref } from "lit/directives/ref.js";
|
|
4
|
+
import { i18n } from "../utils/i18n.js";
|
|
5
|
+
|
|
6
|
+
export type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "search";
|
|
7
|
+
export type InputSize = "sm" | "md" | "lg";
|
|
8
|
+
|
|
9
|
+
export interface InputProps extends BaseComponentProps {
|
|
10
|
+
type?: InputType;
|
|
11
|
+
size?: InputSize;
|
|
12
|
+
value?: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
name?: string;
|
|
19
|
+
autocomplete?: string;
|
|
20
|
+
min?: number;
|
|
21
|
+
max?: number;
|
|
22
|
+
step?: number;
|
|
23
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
24
|
+
onInput?: (e: Event) => void;
|
|
25
|
+
onChange?: (e: Event) => void;
|
|
26
|
+
onKeyDown?: (e: KeyboardEvent) => void;
|
|
27
|
+
onKeyUp?: (e: KeyboardEvent) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Input = fc<InputProps>(
|
|
31
|
+
({
|
|
32
|
+
type = "text",
|
|
33
|
+
size = "md",
|
|
34
|
+
value = "",
|
|
35
|
+
placeholder = "",
|
|
36
|
+
label = "",
|
|
37
|
+
error = "",
|
|
38
|
+
disabled = false,
|
|
39
|
+
required = false,
|
|
40
|
+
name = "",
|
|
41
|
+
autocomplete = "",
|
|
42
|
+
min,
|
|
43
|
+
max,
|
|
44
|
+
step,
|
|
45
|
+
inputRef,
|
|
46
|
+
onInput,
|
|
47
|
+
onChange,
|
|
48
|
+
onKeyDown,
|
|
49
|
+
onKeyUp,
|
|
50
|
+
className = "",
|
|
51
|
+
}) => {
|
|
52
|
+
const sizeClasses = {
|
|
53
|
+
sm: "h-8 px-3 py-1 text-sm",
|
|
54
|
+
md: "h-9 px-3 py-1 text-sm md:text-sm",
|
|
55
|
+
lg: "h-10 px-4 py-1 text-base",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const baseClasses =
|
|
59
|
+
"flex w-full min-w-0 rounded-md border bg-transparent text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium";
|
|
60
|
+
const interactionClasses =
|
|
61
|
+
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground";
|
|
62
|
+
const focusClasses = "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]";
|
|
63
|
+
const darkClasses = "dark:bg-input/30";
|
|
64
|
+
const stateClasses = error
|
|
65
|
+
? "border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40"
|
|
66
|
+
: "border-input";
|
|
67
|
+
const disabledClasses = "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50";
|
|
68
|
+
|
|
69
|
+
const handleInput = (e: Event) => {
|
|
70
|
+
onInput?.(e);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleChange = (e: Event) => {
|
|
74
|
+
onChange?.(e);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return html`
|
|
78
|
+
<div class="flex flex-col gap-1.5 ${className}">
|
|
79
|
+
${
|
|
80
|
+
label
|
|
81
|
+
? html`
|
|
82
|
+
<label class="text-sm font-medium text-foreground">
|
|
83
|
+
${label} ${required ? html`<span class="text-destructive">${i18n("*")}</span>` : ""}
|
|
84
|
+
</label>
|
|
85
|
+
`
|
|
86
|
+
: ""
|
|
87
|
+
}
|
|
88
|
+
<input
|
|
89
|
+
type="${type}"
|
|
90
|
+
class="${baseClasses} ${
|
|
91
|
+
sizeClasses[size]
|
|
92
|
+
} ${interactionClasses} ${focusClasses} ${darkClasses} ${stateClasses} ${disabledClasses}"
|
|
93
|
+
.value=${value}
|
|
94
|
+
placeholder="${placeholder}"
|
|
95
|
+
?disabled=${disabled}
|
|
96
|
+
?required=${required}
|
|
97
|
+
?aria-invalid=${!!error}
|
|
98
|
+
name="${name}"
|
|
99
|
+
autocomplete="${autocomplete}"
|
|
100
|
+
min="${min ?? ""}"
|
|
101
|
+
max="${max ?? ""}"
|
|
102
|
+
step="${step ?? ""}"
|
|
103
|
+
@input=${handleInput}
|
|
104
|
+
@change=${handleChange}
|
|
105
|
+
@keydown=${onKeyDown}
|
|
106
|
+
@keyup=${onKeyUp}
|
|
107
|
+
${inputRef ? ref(inputRef) : ""}
|
|
108
|
+
/>
|
|
109
|
+
${error ? html`<span class="text-sm text-destructive">${error}</span>` : ""}
|
|
110
|
+
</div>
|
|
111
|
+
`;
|
|
112
|
+
},
|
|
113
|
+
);
|