@broberg/seti-client 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 +55 -0
- package/dist/chunk-VWTGZF3D.js +177 -0
- package/dist/chunk-VWTGZF3D.js.map +1 -0
- package/dist/client-BIxdDJYz.d.cts +89 -0
- package/dist/client-BIxdDJYz.d.ts +89 -0
- package/dist/index.cjs +195 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/preact.cjs +333 -0
- package/dist/preact.cjs.map +1 -0
- package/dist/preact.d.cts +30 -0
- package/dist/preact.d.ts +30 -0
- package/dist/preact.js +157 -0
- package/dist/preact.js.map +1 -0
- package/package.json +59 -0
package/dist/preact.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { SetiClient, FrameAccumulator } from './chunk-VWTGZF3D.js';
|
|
2
|
+
export { SetiClient } from './chunk-VWTGZF3D.js';
|
|
3
|
+
import { useMemo, useState, useRef, useEffect } from 'preact/hooks';
|
|
4
|
+
import { jsxs, jsx } from 'preact/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var NAV_KEYS = [
|
|
7
|
+
{ key: "Escape", label: "Esc", title: "Escape" },
|
|
8
|
+
{ key: "Up", label: "\u2191", title: "Pil op" },
|
|
9
|
+
{ key: "Down", label: "\u2193", title: "Pil ned" },
|
|
10
|
+
{ key: "Left", label: "\u2190", title: "Pil venstre" },
|
|
11
|
+
{ key: "Right", label: "\u2192", title: "Pil h\xF8jre" },
|
|
12
|
+
{ key: "Enter", label: "\u23CE", title: "Enter" }
|
|
13
|
+
];
|
|
14
|
+
var STYLE_ID = "broberg-seti-chat-style";
|
|
15
|
+
var CSS = `
|
|
16
|
+
.seti-chat{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--seti-bg,#0b0e14);color:var(--seti-fg,#d7dce5);border:1px solid var(--seti-edge,#1e2430);border-radius:var(--seti-radius,12px);overflow:hidden}
|
|
17
|
+
.seti-chat__header{display:flex;align-items:center;gap:.55rem;padding:.6rem .9rem;background:var(--seti-panel,#11151f);border-bottom:1px solid var(--seti-edge,#1e2430);font-size:.82rem}
|
|
18
|
+
.seti-chat__dot{width:9px;height:9px;border-radius:50%;background:var(--seti-dim,#8a93a6);transition:background .2s;flex:none}
|
|
19
|
+
.seti-chat__dot.is-on{background:var(--seti-accent,#34d399);animation:seti-pulse 2s infinite}
|
|
20
|
+
.seti-chat__dot.is-bad{background:var(--seti-bad,#f87171)}
|
|
21
|
+
@keyframes seti-pulse{0%{box-shadow:0 0 0 0 rgba(52,211,153,.5)}70%{box-shadow:0 0 0 7px rgba(52,211,153,0)}100%{box-shadow:0 0 0 0 rgba(52,211,153,0)}}
|
|
22
|
+
.seti-chat__meta{color:var(--seti-dim,#8a93a6);font-family:var(--seti-mono,ui-monospace,Menlo,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
23
|
+
.seti-chat__screen{flex:1;margin:0;padding:.9rem 1rem;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:var(--seti-mono,ui-monospace,Menlo,monospace);font-size:12.5px;line-height:1.45;-webkit-overflow-scrolling:touch}
|
|
24
|
+
.seti-chat__screen.is-empty{color:var(--seti-dim,#8a93a6)}
|
|
25
|
+
.seti-chat__navkeys{display:flex;gap:.4rem;padding:.45rem .65rem .2rem;background:var(--seti-panel,#11151f);flex-wrap:wrap;border-top:1px solid var(--seti-edge,#1e2430)}
|
|
26
|
+
.seti-chat__navkeys button{padding:.45rem .7rem;background:var(--seti-edge,#1e2430);color:var(--seti-fg,#d7dce5);font-size:13px;min-width:2.6rem;min-height:2.2rem;font-weight:600;border:0;border-radius:8px;cursor:pointer;transition:transform .06s,background .15s,opacity .15s}
|
|
27
|
+
.seti-chat__navkeys button:hover{filter:brightness(1.25)}
|
|
28
|
+
.seti-chat__navkeys button:active{transform:translateY(1px) scale(.98)}
|
|
29
|
+
.seti-chat__navkeys button:disabled{opacity:.5;cursor:not-allowed}
|
|
30
|
+
.seti-chat__form{display:flex;gap:.5rem;padding:.6rem;background:var(--seti-panel,#11151f);border-top:1px solid var(--seti-edge,#1e2430)}
|
|
31
|
+
.seti-chat__input{flex:1;min-width:0;background:var(--seti-bg,#0b0e14);border:1px solid var(--seti-edge,#1e2430);color:var(--seti-fg,#d7dce5);border-radius:10px;padding:.65rem .85rem;font-family:var(--seti-mono,ui-monospace,Menlo,monospace);font-size:16px;outline:none}
|
|
32
|
+
.seti-chat__input:focus{border-color:var(--seti-accent,#34d399);box-shadow:0 0 0 3px rgba(52,211,153,.15)}
|
|
33
|
+
.seti-chat__send{background:var(--seti-accent,#34d399);color:#07120d;border:0;border-radius:10px;padding:.65rem 1.05rem;font-weight:650;cursor:pointer;transition:transform .06s,background .15s,opacity .15s}
|
|
34
|
+
.seti-chat__send:hover{filter:brightness(1.08)}
|
|
35
|
+
.seti-chat__send:active{transform:translateY(1px) scale(.99)}
|
|
36
|
+
.seti-chat__send:disabled{opacity:.5;cursor:not-allowed}
|
|
37
|
+
.seti-chat__send.is-sending{background:var(--seti-warn,#fbbf24);color:#2a1d00}
|
|
38
|
+
`;
|
|
39
|
+
function ensureStyle() {
|
|
40
|
+
if (typeof document === "undefined") return;
|
|
41
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
42
|
+
const el = document.createElement("style");
|
|
43
|
+
el.id = STYLE_ID;
|
|
44
|
+
el.textContent = CSS;
|
|
45
|
+
document.head.appendChild(el);
|
|
46
|
+
}
|
|
47
|
+
function SetiChat(props) {
|
|
48
|
+
const client = useMemo(
|
|
49
|
+
() => props.client ?? new SetiClient({ baseUrl: props.baseUrl ?? "/api/seti" }),
|
|
50
|
+
[props.client, props.baseUrl]
|
|
51
|
+
);
|
|
52
|
+
const [text, setText] = useState("");
|
|
53
|
+
const [sending, setSending] = useState(false);
|
|
54
|
+
const [screenText, setScreenText] = useState("");
|
|
55
|
+
const [edgeOn, setEdgeOn] = useState(null);
|
|
56
|
+
const [streamState, setStreamState] = useState("connecting");
|
|
57
|
+
const [notice, setNotice] = useState(null);
|
|
58
|
+
const screenRef = useRef(null);
|
|
59
|
+
useEffect(ensureStyle, []);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const acc = new FrameAccumulator();
|
|
62
|
+
setScreenText("");
|
|
63
|
+
setNotice(null);
|
|
64
|
+
const atBottom = () => {
|
|
65
|
+
const s = screenRef.current;
|
|
66
|
+
return !s || s.scrollHeight - s.scrollTop - s.clientHeight < 40;
|
|
67
|
+
};
|
|
68
|
+
const handle = client.openStream(props.edge, props.session, {
|
|
69
|
+
onHello: (h) => setEdgeOn(h.edgeConnected),
|
|
70
|
+
onPing: (p) => setEdgeOn(p.edgeConnected),
|
|
71
|
+
onStateChange: setStreamState,
|
|
72
|
+
onFrame: (content) => {
|
|
73
|
+
const stick = atBottom();
|
|
74
|
+
acc.feed(content);
|
|
75
|
+
setScreenText(acc.text);
|
|
76
|
+
if (stick) {
|
|
77
|
+
requestAnimationFrame(() => {
|
|
78
|
+
const s = screenRef.current;
|
|
79
|
+
if (s) s.scrollTop = s.scrollHeight;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return () => handle.close();
|
|
85
|
+
}, [client, props.edge, props.session]);
|
|
86
|
+
const meta = streamState === "open" ? edgeOn === false ? `${props.edge} \xB7 edge offline` : `${props.edge} \xB7 ${props.session}` : streamState === "closed" ? "lukket" : "forbinder\u2026";
|
|
87
|
+
const dotClass = "seti-chat__dot" + (streamState === "open" && edgeOn ? " is-on" : edgeOn === false ? " is-bad" : "");
|
|
88
|
+
const submit = async (ev) => {
|
|
89
|
+
ev.preventDefault();
|
|
90
|
+
if (!text.trim() || sending) return;
|
|
91
|
+
setSending(true);
|
|
92
|
+
setNotice(null);
|
|
93
|
+
const res = await client.sendText(props.edge, props.session, text);
|
|
94
|
+
if (res.ok) {
|
|
95
|
+
setText("");
|
|
96
|
+
} else {
|
|
97
|
+
setNotice("Ikke leveret \u2014 din tekst er bevaret");
|
|
98
|
+
}
|
|
99
|
+
setSending(false);
|
|
100
|
+
};
|
|
101
|
+
const pressKey = async (key) => {
|
|
102
|
+
await client.sendKey(props.edge, props.session, key);
|
|
103
|
+
};
|
|
104
|
+
return /* @__PURE__ */ jsxs("div", { class: "seti-chat" + (props.class ? ` ${props.class}` : ""), "data-testid": "seti-chat-root", children: [
|
|
105
|
+
/* @__PURE__ */ jsxs("div", { class: "seti-chat__header", "data-testid": "seti-chat-header", children: [
|
|
106
|
+
/* @__PURE__ */ jsx("span", { class: dotClass, "data-testid": "seti-chat-status-dot" }),
|
|
107
|
+
/* @__PURE__ */ jsx("span", { class: "seti-chat__meta", "data-testid": "seti-chat-meta", children: notice ?? meta })
|
|
108
|
+
] }),
|
|
109
|
+
/* @__PURE__ */ jsx(
|
|
110
|
+
"pre",
|
|
111
|
+
{
|
|
112
|
+
ref: screenRef,
|
|
113
|
+
class: "seti-chat__screen" + (screenText ? "" : " is-empty"),
|
|
114
|
+
"data-testid": "seti-chat-screen",
|
|
115
|
+
children: screenText || "Venter p\xE5 den f\xF8rste frame fra edgen\u2026"
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
/* @__PURE__ */ jsx("div", { class: "seti-chat__navkeys", "data-testid": "seti-chat-navkeys", children: NAV_KEYS.map((k) => /* @__PURE__ */ jsx(
|
|
119
|
+
"button",
|
|
120
|
+
{
|
|
121
|
+
type: "button",
|
|
122
|
+
title: k.title,
|
|
123
|
+
"data-testid": `seti-chat-key-${k.key.toLowerCase()}`,
|
|
124
|
+
onClick: () => void pressKey(k.key),
|
|
125
|
+
children: k.label
|
|
126
|
+
},
|
|
127
|
+
k.key
|
|
128
|
+
)) }),
|
|
129
|
+
/* @__PURE__ */ jsxs("form", { class: "seti-chat__form", "data-testid": "seti-chat-form", onSubmit: submit, children: [
|
|
130
|
+
/* @__PURE__ */ jsx(
|
|
131
|
+
"input",
|
|
132
|
+
{
|
|
133
|
+
class: "seti-chat__input",
|
|
134
|
+
"data-testid": "seti-chat-input",
|
|
135
|
+
value: text,
|
|
136
|
+
placeholder: props.placeholder ?? "Skriv til sessionen\u2026",
|
|
137
|
+
autocomplete: "off",
|
|
138
|
+
onInput: (e) => setText(e.target.value)
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
/* @__PURE__ */ jsx(
|
|
142
|
+
"button",
|
|
143
|
+
{
|
|
144
|
+
type: "submit",
|
|
145
|
+
class: "seti-chat__send" + (sending ? " is-sending" : ""),
|
|
146
|
+
"data-testid": "seti-chat-send",
|
|
147
|
+
disabled: sending,
|
|
148
|
+
children: sending ? "\u2026" : "Send"
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
] })
|
|
152
|
+
] });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { SetiChat };
|
|
156
|
+
//# sourceMappingURL=preact.js.map
|
|
157
|
+
//# sourceMappingURL=preact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/preact.tsx"],"names":[],"mappings":"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","file":"preact.js","sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport { SetiClient } from \"./client\";\nimport { FrameAccumulator } from \"./frame-accumulator\";\nimport type { SetiKey, SetiStreamState } from \"./types\";\n\n/**\n * <SetiChat> — the complete mobile-first SET/SETI live chat surface:\n * status header, accumulated screen (FrameAccumulator), nav-keys bar\n * (Esc/↑/↓/←/→/⏎) and a text input with delivery feedback (text is preserved\n * when delivery fails).\n *\n * Self-contained styles, themeable via CSS vars (set them on a parent):\n * --seti-bg, --seti-panel, --seti-edge, --seti-fg, --seti-dim,\n * --seti-accent, --seti-warn, --seti-bad, --seti-mono, --seti-radius\n *\n * Every interactive element carries data-testid=\"seti-chat-*\".\n */\nexport interface SetiChatProps {\n /** The host app's proxy mount, e.g. \"/api/seti\". Ignored if `client` is given. */\n baseUrl?: string;\n client?: SetiClient;\n edge: string;\n session: string;\n /** Extra class on the root (sizing/layout belongs to the host). */\n class?: string;\n /** Placeholder for the text input. Default: \"Skriv til sessionen…\" */\n placeholder?: string;\n}\n\nconst NAV_KEYS: Array<{ key: SetiKey; label: string; title: string }> = [\n { key: \"Escape\", label: \"Esc\", title: \"Escape\" },\n { key: \"Up\", label: \"↑\", title: \"Pil op\" },\n { key: \"Down\", label: \"↓\", title: \"Pil ned\" },\n { key: \"Left\", label: \"←\", title: \"Pil venstre\" },\n { key: \"Right\", label: \"→\", title: \"Pil højre\" },\n { key: \"Enter\", label: \"⏎\", title: \"Enter\" },\n];\n\nconst STYLE_ID = \"broberg-seti-chat-style\";\nconst CSS = `\n.seti-chat{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--seti-bg,#0b0e14);color:var(--seti-fg,#d7dce5);border:1px solid var(--seti-edge,#1e2430);border-radius:var(--seti-radius,12px);overflow:hidden}\n.seti-chat__header{display:flex;align-items:center;gap:.55rem;padding:.6rem .9rem;background:var(--seti-panel,#11151f);border-bottom:1px solid var(--seti-edge,#1e2430);font-size:.82rem}\n.seti-chat__dot{width:9px;height:9px;border-radius:50%;background:var(--seti-dim,#8a93a6);transition:background .2s;flex:none}\n.seti-chat__dot.is-on{background:var(--seti-accent,#34d399);animation:seti-pulse 2s infinite}\n.seti-chat__dot.is-bad{background:var(--seti-bad,#f87171)}\n@keyframes seti-pulse{0%{box-shadow:0 0 0 0 rgba(52,211,153,.5)}70%{box-shadow:0 0 0 7px rgba(52,211,153,0)}100%{box-shadow:0 0 0 0 rgba(52,211,153,0)}}\n.seti-chat__meta{color:var(--seti-dim,#8a93a6);font-family:var(--seti-mono,ui-monospace,Menlo,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.seti-chat__screen{flex:1;margin:0;padding:.9rem 1rem;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:var(--seti-mono,ui-monospace,Menlo,monospace);font-size:12.5px;line-height:1.45;-webkit-overflow-scrolling:touch}\n.seti-chat__screen.is-empty{color:var(--seti-dim,#8a93a6)}\n.seti-chat__navkeys{display:flex;gap:.4rem;padding:.45rem .65rem .2rem;background:var(--seti-panel,#11151f);flex-wrap:wrap;border-top:1px solid var(--seti-edge,#1e2430)}\n.seti-chat__navkeys button{padding:.45rem .7rem;background:var(--seti-edge,#1e2430);color:var(--seti-fg,#d7dce5);font-size:13px;min-width:2.6rem;min-height:2.2rem;font-weight:600;border:0;border-radius:8px;cursor:pointer;transition:transform .06s,background .15s,opacity .15s}\n.seti-chat__navkeys button:hover{filter:brightness(1.25)}\n.seti-chat__navkeys button:active{transform:translateY(1px) scale(.98)}\n.seti-chat__navkeys button:disabled{opacity:.5;cursor:not-allowed}\n.seti-chat__form{display:flex;gap:.5rem;padding:.6rem;background:var(--seti-panel,#11151f);border-top:1px solid var(--seti-edge,#1e2430)}\n.seti-chat__input{flex:1;min-width:0;background:var(--seti-bg,#0b0e14);border:1px solid var(--seti-edge,#1e2430);color:var(--seti-fg,#d7dce5);border-radius:10px;padding:.65rem .85rem;font-family:var(--seti-mono,ui-monospace,Menlo,monospace);font-size:16px;outline:none}\n.seti-chat__input:focus{border-color:var(--seti-accent,#34d399);box-shadow:0 0 0 3px rgba(52,211,153,.15)}\n.seti-chat__send{background:var(--seti-accent,#34d399);color:#07120d;border:0;border-radius:10px;padding:.65rem 1.05rem;font-weight:650;cursor:pointer;transition:transform .06s,background .15s,opacity .15s}\n.seti-chat__send:hover{filter:brightness(1.08)}\n.seti-chat__send:active{transform:translateY(1px) scale(.99)}\n.seti-chat__send:disabled{opacity:.5;cursor:not-allowed}\n.seti-chat__send.is-sending{background:var(--seti-warn,#fbbf24);color:#2a1d00}\n`;\n\nfunction ensureStyle(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = CSS;\n document.head.appendChild(el);\n}\n\nexport function SetiChat(props: SetiChatProps) {\n const client = useMemo(\n () => props.client ?? new SetiClient({ baseUrl: props.baseUrl ?? \"/api/seti\" }),\n [props.client, props.baseUrl],\n );\n const [text, setText] = useState(\"\");\n const [sending, setSending] = useState(false);\n const [screenText, setScreenText] = useState(\"\");\n const [edgeOn, setEdgeOn] = useState<boolean | null>(null);\n const [streamState, setStreamState] = useState<SetiStreamState>(\"connecting\");\n const [notice, setNotice] = useState<string | null>(null);\n const screenRef = useRef<HTMLPreElement>(null);\n\n useEffect(ensureStyle, []);\n\n useEffect(() => {\n const acc = new FrameAccumulator();\n setScreenText(\"\");\n setNotice(null);\n const atBottom = (): boolean => {\n const s = screenRef.current;\n return !s || s.scrollHeight - s.scrollTop - s.clientHeight < 40;\n };\n const handle = client.openStream(props.edge, props.session, {\n onHello: (h) => setEdgeOn(h.edgeConnected),\n onPing: (p) => setEdgeOn(p.edgeConnected),\n onStateChange: setStreamState,\n onFrame: (content) => {\n const stick = atBottom();\n acc.feed(content);\n setScreenText(acc.text);\n if (stick) {\n requestAnimationFrame(() => {\n const s = screenRef.current;\n if (s) s.scrollTop = s.scrollHeight;\n });\n }\n },\n });\n return () => handle.close();\n }, [client, props.edge, props.session]);\n\n const meta =\n streamState === \"open\"\n ? edgeOn === false\n ? `${props.edge} · edge offline`\n : `${props.edge} · ${props.session}`\n : streamState === \"closed\"\n ? \"lukket\"\n : \"forbinder…\";\n const dotClass =\n \"seti-chat__dot\" + (streamState === \"open\" && edgeOn ? \" is-on\" : edgeOn === false ? \" is-bad\" : \"\");\n\n const submit = async (ev: Event) => {\n ev.preventDefault();\n if (!text.trim() || sending) return;\n setSending(true);\n setNotice(null);\n const res = await client.sendText(props.edge, props.session, text);\n if (res.ok) {\n setText(\"\"); // only clear when actually delivered — text survives failures\n } else {\n setNotice(\"Ikke leveret — din tekst er bevaret\");\n }\n setSending(false);\n };\n\n const pressKey = async (key: SetiKey) => {\n await client.sendKey(props.edge, props.session, key);\n };\n\n return (\n <div class={\"seti-chat\" + (props.class ? ` ${props.class}` : \"\")} data-testid=\"seti-chat-root\">\n <div class=\"seti-chat__header\" data-testid=\"seti-chat-header\">\n <span class={dotClass} data-testid=\"seti-chat-status-dot\" />\n <span class=\"seti-chat__meta\" data-testid=\"seti-chat-meta\">\n {notice ?? meta}\n </span>\n </div>\n <pre\n ref={screenRef}\n class={\"seti-chat__screen\" + (screenText ? \"\" : \" is-empty\")}\n data-testid=\"seti-chat-screen\"\n >\n {screenText || \"Venter på den første frame fra edgen…\"}\n </pre>\n <div class=\"seti-chat__navkeys\" data-testid=\"seti-chat-navkeys\">\n {NAV_KEYS.map((k) => (\n <button\n key={k.key}\n type=\"button\"\n title={k.title}\n data-testid={`seti-chat-key-${k.key.toLowerCase()}`}\n onClick={() => void pressKey(k.key)}\n >\n {k.label}\n </button>\n ))}\n </div>\n <form class=\"seti-chat__form\" data-testid=\"seti-chat-form\" onSubmit={submit}>\n <input\n class=\"seti-chat__input\"\n data-testid=\"seti-chat-input\"\n value={text}\n placeholder={props.placeholder ?? \"Skriv til sessionen…\"}\n autocomplete=\"off\"\n onInput={(e) => setText((e.target as HTMLInputElement).value)}\n />\n <button\n type=\"submit\"\n class={\"seti-chat__send\" + (sending ? \" is-sending\" : \"\")}\n data-testid=\"seti-chat-send\"\n disabled={sending}\n >\n {sending ? \"…\" : \"Send\"}\n </button>\n </form>\n </div>\n );\n}\n\nexport { SetiClient } from \"./client\";\nexport type { SetiKey } from \"./types\";\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@broberg/seti-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed client + frame-merge engine + Preact <SetiChat> component for buddycloud.cc SET/SETI live streaming chat (consumed through a host-app proxy from @broberg/seti-server).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": ["dist", "README.md"],
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./preact": {
|
|
19
|
+
"types": "./dist/preact.d.ts",
|
|
20
|
+
"import": "./dist/preact.js",
|
|
21
|
+
"require": "./dist/preact.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"preact": "^10.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"preact": { "optional": true }
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"preact": "^10.24.3",
|
|
37
|
+
"tsup": "^8.3.0",
|
|
38
|
+
"typescript": "^5.6.0",
|
|
39
|
+
"vitest": "^2.1.0"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"seti",
|
|
43
|
+
"set",
|
|
44
|
+
"streaming",
|
|
45
|
+
"terminal",
|
|
46
|
+
"tmux",
|
|
47
|
+
"sse",
|
|
48
|
+
"chat",
|
|
49
|
+
"preact",
|
|
50
|
+
"buddycloud",
|
|
51
|
+
"broberg"
|
|
52
|
+
],
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/broberg-ai/components",
|
|
56
|
+
"directory": "packages/seti-client"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": { "access": "public" }
|
|
59
|
+
}
|