Initial commit

This commit is contained in:
Alois 2026-03-10 20:44:25 +01:00
commit 2f5e10140c
133 changed files with 10429 additions and 0 deletions

View file

@ -0,0 +1,33 @@
{
"name": "@tensamin/chat",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./context": "./src/context.tsx",
"./screen": "./src/screen.tsx",
"./behaviour-conversation": "./src/behaviour/conversation.ts",
"./behaviour-community": "./src/behaviour/community.ts"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@solidjs/router": "^0.15.4",
"@tanstack/solid-query": "^5.90.23",
"@tanstack/solid-virtual": "^3.13.21",
"@tensamin/core-crypto": "workspace:*",
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git",
"@tensamin/core-storage": "workspace:*",
"@tensamin/core-user": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/ui": "workspace:*",
"lucide-solid": "^0.564.0",
"solid-js": "^1.9.11",
"solid-sonner": "^0.2.8",
"zod": "^4.3.6"
}
}

View file

@ -0,0 +1,3 @@
export function getMessages() {
return 0;
}

View file

@ -0,0 +1,22 @@
import { useSocket } from "@tensamin/ttp/context";
import type { RawMessages } from "../values";
export async function getMessages(
amount: number,
offset: number,
user_id: number,
): Promise<RawMessages> {
const { send } = useSocket();
const messages = await send("messages_get", {
amount: amount,
offset: offset,
user_id: user_id,
});
if (messages.type.startsWith("error")) {
throw new Error(messages.type);
}
return messages.data.messages;
}

View file

@ -0,0 +1,103 @@
import Input from "@tensamin/markdown/input";
import { Card, CardHeader } from "@tensamin/ui/card";
import { useStorage } from "@tensamin/core-storage/context";
import { createEffect, createSignal } from "solid-js";
import { Button } from "@tensamin/ui/button";
import { Plus, Laugh, Clapperboard } from "lucide-solid";
import { useChat } from "../context";
import { useSocket } from "@tensamin/ttp/context";
import { useCrypto } from "@tensamin/core-crypto/context";
import { log, toast } from "@tensamin/shared/log";
export default function InputComponent() {
const [value, setValue] = createSignal("");
const [invertEnterBehavior, setInvertEnterBehavior] = createSignal(false);
const { encrypt } = useCrypto();
const { send } = useSocket();
const { addLiveMessage, sharedSecret, userId } = useChat();
const { load } = useStorage();
createEffect(() => {
void load("chat_invert_enter_behaviour").then((shouldInvert) => {
setInvertEnterBehavior(shouldInvert);
});
});
async function handleSubmit() {
if (value().trim() === "") return;
const time = Date.now();
const currentValue = value();
const currentUserId = userId();
addLiveMessage({
height: 0,
not_encrypted: true,
timestamp: time,
content: currentValue,
sent_by_self: true,
});
const encryptedContext = await encrypt(sharedSecret(), currentValue);
send("message_send", {
height: 0,
content: encryptedContext,
receiver_id: currentUserId,
send_time: time,
}).catch((e) => {
log(0, "Chat", "red", "Failed to send message", e, {
content: currentValue,
encryptedContext,
receiver_id: currentUserId,
send_time: time,
});
toast("error", "Failed to send message");
});
setValue("");
}
return (
<Card class="rounded-none rounded-t-xl border border-input/50 border-b-0">
<CardHeader class="p-0 flex flex-col">
<Input
placeholder="Send a message..."
value={value()}
setValue={setValue}
onSubmit={handleSubmit}
invertEnterBehavior={invertEnterBehavior()}
/>
<div class="w-full flex justify-between gap-2 p-2 pt-0">
<div class="flex gap-2">
<Button
class="w-9 h-9 p-0 border border-ring/10"
variant="secondary"
>
<Plus size={20} />
</Button>
<div class="w-auto flex">
<p>Files list</p>
</div>
</div>
<div class="flex gap-2">
<Button
class="w-9 h-9 p-0 border border-ring/10"
variant="secondary"
>
<Laugh size={20} />
</Button>
<Button
class="w-9 h-9 p-0 border border-ring/10"
variant="secondary"
>
<Clapperboard size={20} />
</Button>
</div>
</div>
</CardHeader>
</Card>
);
}

View file

@ -0,0 +1,80 @@
import { useCrypto } from "@tensamin/core-crypto/context";
import { createEffect, createSignal, onCleanup, Show } from "solid-js";
import { useChat } from "../context";
import type { RawMessage } from "../values";
import { log } from "@tensamin/shared/log";
import Text from "@tensamin/markdown/text";
export default function Message(props: {
message: RawMessage;
notEncrypted?: boolean;
}) {
const { sharedSecret } = useChat();
const { decrypt } = useCrypto();
const message = () => props.message;
const [decodedContent, setDecodedContent] = createSignal("");
const [isReady, setIsReady] = createSignal(false);
createEffect(() => {
if (props.notEncrypted) {
setDecodedContent(message().content);
setIsReady(true);
return;
}
const content = message().content;
const secret = sharedSecret();
let active = true;
if (!secret) {
setIsReady(false);
return;
}
setIsReady(false);
decrypt(secret, content)
.then((value) => {
if (!active) {
return;
}
setDecodedContent(value);
setIsReady(true);
})
.catch((e) => {
if (!active) {
return;
}
log(0, "Chat", "red", "Failed to decrypt message", e, {
content,
secret,
});
setDecodedContent("Failed to decrypt");
setIsReady(true);
});
onCleanup(() => {
active = false;
});
});
return (
<div class="w-full flex justify-start">
<div
class={`animate-in fade-in duration-200 max-w-[80%] rounded-xl px-2 py-1 whitespace-pre-wrap wrap-break-word ${
message().sent_by_self
? "bg-primary text-primary-foreground"
: "bg-muted"
}`}
>
<Show when={isReady()}>
<Text value={decodedContent()} />
</Show>
</div>
</div>
);
}

View file

@ -0,0 +1,108 @@
import {
createContext,
createEffect,
createSignal,
useContext,
} from "solid-js";
import type { ParentProps } from "solid-js";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import type { RawMessage, RawMessages } from "./values";
import { useSearchParams } from "@solidjs/router";
import { useCrypto } from "@tensamin/core-crypto/context";
import { useUser } from "@tensamin/core-user/context";
import { useStorage } from "@tensamin/core-storage/context";
export const context = createContext<contextType>();
const queryClient = new QueryClient();
export default function Provider(
props: ParentProps & {
getMessages: (
amount: number,
offset: number,
user_id: number,
) => Promise<RawMessages>;
},
) {
const { get_shared_secret } = useCrypto();
const { get } = useUser();
const { load } = useStorage();
const [liveMessages, setLiveMessages] = createSignal<RawMessages>([]);
const [currentSharedSecret, setCurrentSharedSecret] = createSignal("");
const [searchParams] = useSearchParams();
const userId = () => Number(searchParams.id ?? 0);
createEffect(() => {
const recipientId = userId();
if (!recipientId) {
setCurrentSharedSecret("");
return;
}
get(recipientId).then(async (recipientData) => {
const ownId = await load("user_id");
const privateKey = await load("private_key");
const ownData = await get(ownId);
const sharedSecret = await get_shared_secret(
privateKey,
ownData.public_key,
recipientData.public_key,
);
setCurrentSharedSecret(sharedSecret);
});
});
async function customGetMessages(amount: number, offset: number) {
const messages = await props.getMessages(amount, offset, userId());
const sorted = [...messages].sort((a, b) => a.timestamp - b.timestamp);
return sorted;
}
function addLiveMessage(message: RawMessage) {
setLiveMessages((prev) => [...prev, message]);
}
function clearLiveMessages() {
setLiveMessages([]);
}
return (
<QueryClientProvider client={queryClient}>
<context.Provider
value={{
getMessages: customGetMessages,
liveMessages,
addLiveMessage,
clearLiveMessages,
sharedSecret: currentSharedSecret,
userId,
}}
>
{props.children}
</context.Provider>
</QueryClientProvider>
);
}
type contextType = {
getMessages: (amount: number, offset: number) => Promise<RawMessages>;
liveMessages: () => RawMessages;
addLiveMessage: (message: RawMessage) => void;
clearLiveMessages: () => void;
sharedSecret: () => string;
userId: () => number;
};
export function useChat(): contextType {
const ctx = useContext(context);
if (!ctx) {
throw new Error("useChat must be used within a ChatProvider");
}
return ctx;
}

View file

@ -0,0 +1,204 @@
import { createVirtualizer } from "@tanstack/solid-virtual";
import { useInfiniteQuery } from "@tanstack/solid-query";
import { useChat } from "./context";
import { createEffect, createMemo, createSignal, For } from "solid-js";
import InputComponent from "./components/input";
import Message from "./components/message";
const PAGE_SIZE = 50;
export default function Screen() {
const { getMessages, liveMessages, clearLiveMessages, userId } = useChat();
// eslint-disable-next-line no-unassigned-vars
let scrollRef!: HTMLDivElement;
const [hasScrolledToBottomInitially, setHasScrolledToBottomInitially] =
createSignal(false);
const [lastLiveMessageCount, setLastLiveMessageCount] = createSignal(0);
const [prependAnchor, setPrependAnchor] = createSignal<{
totalSize: number;
scrollTop: number;
} | null>(null);
const messagesQuery = useInfiniteQuery(() => ({
queryKey: ["chat-messages", String(userId())],
initialPageParam: 0,
queryFn: ({ pageParam }) => getMessages(PAGE_SIZE, pageParam),
getNextPageParam: (lastPage, allPages) => {
if (lastPage.length < PAGE_SIZE) {
return undefined;
}
return allPages.length * PAGE_SIZE;
},
}));
const historicalMessages = createMemo(() => {
const pages = messagesQuery.data?.pages ?? [];
return [...pages].reverse().flat();
});
createEffect(() => {
userId();
clearLiveMessages();
setHasScrolledToBottomInitially(false);
setLastLiveMessageCount(0);
setPrependAnchor(null);
});
const messages = createMemo(() => [
...historicalMessages(),
...liveMessages(),
]);
async function onScroll() {
if (!scrollRef) {
return;
}
if (scrollRef.scrollTop > 96) {
return;
}
if (messagesQuery.isFetchingNextPage || !messagesQuery.hasNextPage) {
return;
}
setPrependAnchor({
totalSize: virtualizer.getTotalSize(),
scrollTop: scrollRef.scrollTop,
});
await messagesQuery.fetchNextPage();
}
const virtualizer = createVirtualizer({
get count() {
return messages().length;
},
getScrollElement: () => scrollRef,
estimateSize: (index) => {
const message = messages()[index];
if (!message) {
return 56;
}
let decodedContent = message.content;
try {
decodedContent = atob(message.content);
} catch {
/* noop */
}
const lineCount = decodedContent.split("\n").length;
const wrappedLineCount = Math.ceil(decodedContent.length / 42);
const fileCount = message.files?.length ?? 0;
return 28 + Math.max(lineCount, wrappedLineCount) * 20 + fileCount * 20;
},
overscan: 6,
});
createEffect(() => {
const currentMessages = messages();
if (
!scrollRef ||
hasScrolledToBottomInitially() ||
currentMessages.length === 0
) {
return;
}
queueMicrotask(() => {
if (!scrollRef) {
return;
}
scrollRef.scrollTop = scrollRef.scrollHeight;
setHasScrolledToBottomInitially(true);
});
});
createEffect(() => {
const count = liveMessages().length;
if (!scrollRef) {
return;
}
if (count > lastLiveMessageCount()) {
queueMicrotask(() => {
if (!scrollRef) {
return;
}
scrollRef.scrollTop = scrollRef.scrollHeight;
});
}
setLastLiveMessageCount(count);
});
createEffect(() => {
const anchor = prependAnchor();
if (!scrollRef || !anchor || messagesQuery.isFetchingNextPage) {
return;
}
queueMicrotask(() => {
if (!scrollRef) {
return;
}
const delta = virtualizer.getTotalSize() - anchor.totalSize;
scrollRef.scrollTop = anchor.scrollTop + delta;
setPrependAnchor(null);
});
});
return (
<div class="w-full h-full flex flex-col overflow-hidden px-2">
<div
ref={scrollRef}
id="chat_container"
class={`flex-1 max-h-[calc(100vh-151px)] overflow-y-auto px-2.5`}
onScroll={onScroll}
>
<div
class="relative w-full"
style={{
height: `${virtualizer.getTotalSize()}px`,
}}
>
<For each={virtualizer.getVirtualItems()}>
{(virtualRow) => (
<div
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
padding: "4px 0",
}}
>
<Message
message={messages()[virtualRow.index]}
notEncrypted={messages()[virtualRow.index].not_encrypted}
/>
</div>
)}
</For>
</div>
</div>
<InputComponent />
</div>
);
}

View file

@ -0,0 +1,8 @@
import { z } from "zod";
import { socket } from "@tensamin/shared/data";
export type RawMessages = z.infer<
typeof socket.messages_get.response
>["messages"];
export type RawMessage = RawMessages[number];

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View file

@ -0,0 +1,21 @@
{
"name": "@tensamin/core-crypto",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./context": "./src/context.tsx",
"./worker": "./src/worker.ts"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@noble/curves": "^2.0.1",
"@tensamin/ui": "workspace:*",
"comlink": "^4.4.2",
"solid-js": "^1.9.11"
}
}

View file

@ -0,0 +1,107 @@
import {
createContext,
onMount,
onCleanup,
createSignal,
Show,
useContext,
} from "solid-js";
import type { ParentProps } from "solid-js";
import * as Comlink from "comlink";
import Loading from "@tensamin/ui/screens/loading";
export const context = createContext<contextType>();
export default function Provider(props: ParentProps) {
let apiRef: ApiRef | null = null;
const [isWorkerReady, setIsWorkerReady] = createSignal(false);
const { encrypt, decrypt, get_shared_secret } = createCryptoActions(
() => apiRef,
);
onMount(() => {
const worker = new Worker(new URL("./worker.ts", import.meta.url), {
type: "module",
});
apiRef = Comlink.wrap(worker);
setIsWorkerReady(true);
onCleanup(() => {
apiRef = null;
worker.terminate();
});
});
return (
<Show when={isWorkerReady()} fallback={<Loading progress={10} />}>
<context.Provider value={{ encrypt, decrypt, get_shared_secret }}>
{props.children}
</context.Provider>
</Show>
);
}
type contextType = {
decrypt: (secret: string, data: string) => Promise<string>;
encrypt: (secret: string, data: string) => Promise<string>;
get_shared_secret: (
ownPrivateKey: string,
ownPublicKey: string,
otherPublicKey: string,
) => Promise<string>;
};
type ApiRef = {
encrypt: (secret: string, message: string) => Promise<string>;
decrypt: (secret: string, encryptedMessage: string) => Promise<string>;
get_shared_secret: (
own_private_key: string,
own_public_key: string,
other_public_key: string,
) => Promise<string>;
};
export function createCryptoActions(
getApiRef: () => ApiRef | null,
): contextType {
const encrypt = async (secret: string, message: string): Promise<string> => {
const apiRef = getApiRef();
if (!apiRef) throw "API not initialized";
return await apiRef.encrypt(secret, message);
};
const decrypt = async (
secret: string,
encryptedMessage: string,
): Promise<string> => {
const apiRef = getApiRef();
if (!apiRef) throw "API not initialized";
return await apiRef.decrypt(secret, encryptedMessage);
};
const get_shared_secret = async (
own_private_key: string,
own_public_key: string,
other_public_key: string,
): Promise<string> => {
const apiRef = getApiRef();
if (!apiRef) throw "API not initialized";
return await apiRef.get_shared_secret(
own_private_key,
own_public_key,
other_public_key,
);
};
return { encrypt, decrypt, get_shared_secret };
}
export function useCrypto(): contextType {
const ctx = useContext(context);
if (!ctx) {
throw new Error("useCrypto must be used within a CryptoProvider");
}
return ctx;
}

View file

@ -0,0 +1,364 @@
import * as Comlink from "comlink";
type Base64URLString = string;
type JWK = {
kty: string;
crv: string;
x?: string;
d?: string;
};
const textEncoder = new TextEncoder();
const crypto = globalThis.crypto;
export async function encrypt(
password: string,
input: string,
): Promise<string> {
const sharedSecret = new Uint8Array(
password.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)),
);
const hkdfKey = await crypto.subtle.importKey(
"raw",
sharedSecret,
"HKDF",
false,
["deriveBits"],
);
const okm = await crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array([]),
info: textEncoder.encode("x448-aes-gcm-no-overhead"),
},
hkdfKey,
44 * 8,
);
const okmBytes = new Uint8Array(okm);
const keyBytes = okmBytes.slice(0, 32);
const nonce = okmBytes.slice(32, 44);
const aesKey = await crypto.subtle.importKey(
"raw",
keyBytes,
{ name: "AES-GCM" },
false,
["encrypt"],
);
const encryptedBuffer = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce },
aesKey,
textEncoder.encode(input),
);
return btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer)));
}
export async function decrypt(
password: string,
input: Base64URLString | string,
): Promise<string> {
const sharedSecret = new Uint8Array(
password.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)),
);
const ciphertext = Uint8Array.from(atob(input), (c) => c.charCodeAt(0));
const hkdfKey = await crypto.subtle.importKey(
"raw",
sharedSecret,
"HKDF",
false,
["deriveBits"],
);
const okm = await crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array([]),
info: textEncoder.encode("x448-aes-gcm-no-overhead"),
},
hkdfKey,
44 * 8,
);
const okmBytes = new Uint8Array(okm);
const keyBytes = okmBytes.slice(0, 32);
const nonce = okmBytes.slice(32, 44);
const aesKey = await crypto.subtle.importKey(
"raw",
keyBytes,
{ name: "AES-GCM" },
false,
["decrypt"],
);
const decryptedBuffer = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: nonce,
},
aesKey,
ciphertext,
);
return new TextDecoder().decode(decryptedBuffer);
}
export async function get_shared_secret(
own_private_key: string,
own_public_key: string,
other_public_key: string,
): Promise<string> {
const other_jwk: JWK = { kty: "OKP", crv: "X448", x: other_public_key };
const own_jwk: JWK = {
kty: "OKP",
crv: "X448",
x: own_public_key,
d: own_private_key,
};
const bytesToHex = (u8: Uint8Array): string =>
Array.from(u8, (b) => b.toString(16).padStart(2, "0")).join("");
const b64ToBytes = (s: Base64URLString): Uint8Array => {
const bin = atob(s);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
};
const b64uToBytes = (s: Base64URLString): Uint8Array => {
const b64 =
s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
return b64ToBytes(b64);
};
const bytesToB64u = (u8: Uint8Array): string => {
const b64 = btoa(String.fromCharCode(...u8));
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
};
const decodeBase64Auto = (s: string): Uint8Array =>
/[-_]/.test(s) ? b64uToBytes(s) : b64ToBytes(s);
const readTLV = (view: Uint8Array, off: number) => {
const tag = view[off++];
if (off >= view.length) throw new Error("DER: truncated");
let len = view[off++];
if (len & 0x80) {
const n = len & 0x7f;
if (n === 0) throw new Error("DER: indefinite length not supported");
if (off + n > view.length) throw new Error("DER: truncated length");
len = 0;
for (let i = 0; i < n; i++) len = (len << 8) | view[off++];
}
const start = off;
const end = off + len;
if (end > view.length) throw new Error("DER: content truncated");
return { tag, len, start, end };
};
const ensureOidX448 = (view: Uint8Array, start: number): boolean => {
const oid = readTLV(view, start);
if (oid.tag !== 0x06) return false;
const len = oid.end - oid.start;
if (len !== 3) return false;
return (
view[oid.start] === 0x2b &&
view[oid.start + 1] === 0x65 &&
view[oid.start + 2] === 0x6f
);
};
const extractRawX448FromSPKI = (spkiBytes: Uint8Array): Uint8Array => {
const view = spkiBytes;
const outer = readTLV(view, 0);
if (outer.tag !== 0x30) throw new Error("SPKI: expected SEQUENCE");
const alg = readTLV(view, outer.start);
if (alg.tag !== 0x30) throw new Error("SPKI: expected AlgorithmIdentifier");
if (!ensureOidX448(view, alg.start)) throw new Error("SPKI: not X448");
const bitstr = readTLV(view, alg.end);
if (bitstr.tag !== 0x03) throw new Error("SPKI: expected BIT STRING");
const unusedBits = view[bitstr.start];
if (unusedBits !== 0x00) throw new Error("SPKI: unexpected unused bits");
const raw = view.subarray(bitstr.start + 1, bitstr.end);
if (raw.length !== 56)
throw new Error("SPKI: X448 public key must be 56 bytes");
return raw;
};
const extractRawX448FromPKCS8 = (pkcs8Bytes: Uint8Array): Uint8Array => {
const view = pkcs8Bytes;
const outer = readTLV(view, 0);
if (outer.tag !== 0x30) throw new Error("PKCS8: expected SEQUENCE");
let off = outer.start;
const version = readTLV(view, off);
if (version.tag !== 0x02)
throw new Error("PKCS8: expected version INTEGER");
off = version.end;
const alg = readTLV(view, off);
if (alg.tag !== 0x30)
throw new Error("PKCS8: expected AlgorithmIdentifier");
if (!ensureOidX448(view, alg.start)) throw new Error("PKCS8: not X448");
off = alg.end;
const priv = readTLV(view, off);
if (priv.tag !== 0x04)
throw new Error("PKCS8: expected privateKey OCTET STRING");
let raw = view.subarray(priv.start, priv.end);
// Some encoders nest another OCTET STRING inside
if (raw[0] === 0x04) {
const inner = readTLV(raw, 0);
if (inner.tag === 0x04) {
raw = raw.subarray(inner.start, inner.end);
}
}
if (raw.length !== 56)
throw new Error("PKCS8: X448 private key must be 56 bytes");
return raw;
};
const normalizeOkpX448Jwk = (jwk: JWK, label: string): JWK => {
if (!jwk || jwk.kty !== "OKP" || jwk.crv !== "X448") {
throw new Error(`${label}: expected OKP JWK with crv "X448"`);
}
const out = { ...jwk };
if (out.x) {
const xBytes = decodeBase64Auto(out.x);
let rawX: Uint8Array;
try {
rawX = extractRawX448FromSPKI(xBytes);
} catch {
if (xBytes.length !== 56) {
throw new Error(
`${label}: "x" is not a valid X448 SPKI or raw 56-byte key`,
);
}
rawX = xBytes;
}
out.x = bytesToB64u(rawX);
}
if (out.d) {
const dBytes = decodeBase64Auto(out.d);
let rawD: Uint8Array;
try {
rawD = extractRawX448FromPKCS8(dBytes);
} catch {
if (dBytes.length !== 56) {
throw new Error(
`${label}: "d" is not a valid X448 PKCS#8 or raw 56-byte key`,
);
}
rawD = dBytes;
}
out.d = bytesToB64u(rawD);
}
return out;
};
const getSubtle = () => globalThis.crypto?.subtle;
{
/*
const hkdfAesGcmFromShared = async (
sharedSecret: BufferSource,
infoStr: string
): Promise<CryptoKey> => {
const subtle = getSubtle();
if (!subtle) throw new Error("WebCrypto subtle not available");
const info = textEncoder.encode(infoStr);
const baseKey = await subtle.importKey(
"raw",
sharedSecret,
"HKDF",
false,
["deriveKey"]
);
return await subtle.deriveKey(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(0),
info,
},
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
};
*/
}
const myJwk: JWK = normalizeOkpX448Jwk(own_jwk, "own_jwk");
const peerJwk: JWK = normalizeOkpX448Jwk(other_jwk, "other_jwk");
const subtle = getSubtle();
//const infoStr = `ECDH-X448-AES-GCM-v1|my=${myJwk.x}|peer=${peerJwk.x}`;
if (subtle) {
const algorithms = [{ name: "ECDH", namedCurve: "X448" }, { name: "X448" }];
for (const algorithm of algorithms) {
try {
const [myPriv, peerPub] = await Promise.all([
subtle.importKey("jwk", myJwk, algorithm, false, ["deriveBits"]),
subtle.importKey("jwk", peerJwk, algorithm, false, []),
]);
const sharedBits = await subtle.deriveBits(
{ name: algorithm.name, public: peerPub },
myPriv,
448,
);
const sharedSecret = new Uint8Array(sharedBits);
//const aeadKey = await hkdfAesGcmFromShared(sharedSecret, infoStr);
return bytesToHex(sharedSecret);
} catch {
// Browser doesn't support this algorithm, try next or fall through to software fallback
}
}
}
const { d: dMyB64u } = myJwk;
//const { x: xMyB64u, d: dMyB64u } = myJwk;
const { x: xPeerB64u } = peerJwk;
if (!dMyB64u || !xPeerB64u) {
return "Failed to get shared secret due to missing keys";
}
const [dRaw, xRawPeer] = [b64uToBytes(dMyB64u), b64uToBytes(xPeerB64u)];
if (dRaw.length !== 56 || xRawPeer.length !== 56) {
return "Failed to get shared secret due to invalid key lengths";
}
const { x448 } = await import("@noble/curves/ed448.js");
const sharedSecret = new Uint8Array(x448.getSharedSecret(dRaw, xRawPeer));
//const aeadKey = await hkdfAesGcmFromShared(sharedSecret, infoStr);
return bytesToHex(sharedSecret);
}
Comlink.expose({
encrypt,
decrypt,
get_shared_secret,
});

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View file

@ -0,0 +1,23 @@
{
"name": "@tensamin/markdown",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./text": "./src/text.tsx",
"./input": "./src/input.tsx"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@codemirror/commands": "^6.10.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.16",
"@tensamin/ui": "workspace:*",
"solid-js": "^1.9.11"
}
}

View file

@ -0,0 +1,364 @@
import { markdown } from "@codemirror/lang-markdown";
import {
EditorState,
type Extension,
type Range,
type SelectionRange,
} from "@codemirror/state";
import {
Decoration,
EditorView,
keymap,
placeholder,
ViewPlugin,
type DecorationSet,
type ViewUpdate,
} from "@codemirror/view";
import {
defaultKeymap,
history,
historyKeymap,
indentWithTab,
} from "@codemirror/commands";
import {
createEffect,
createMemo,
onCleanup,
onMount,
type Setter,
} from "solid-js";
import { collectInlineRanges, ensureMarkdownStyles } from "./markdown";
export type InputProps = {
ref?: HTMLDivElement;
placeholder?: string;
value: string;
setValue: Setter<string>;
onSubmit?: () => void;
invertEnterBehavior?: boolean;
};
type TokenRange = {
from: number;
to: number;
};
const hiddenTokenDecoration = Decoration.mark({ class: "tm-md-hidden-token" });
const strongDecoration = Decoration.mark({ class: "tm-md-strong" });
const emDecoration = Decoration.mark({ class: "tm-md-em" });
const delDecoration = Decoration.mark({ class: "tm-md-del" });
const codeDecoration = Decoration.mark({ class: "tm-md-code" });
const linkDecoration = Decoration.mark({ class: "tm-md-link" });
const codeLineDecoration = Decoration.line({ class: "tm-md-code-line" });
/**
* Builds markdown styling decorations every time the document or cursor selection changes.
* Token delimiters are hidden unless the cursor is currently intersecting that token range.
*/
const markdownDecorations = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.selectionSet || update.viewportChanged) {
this.decorations = buildDecorations(update.view);
}
}
},
{
decorations: (instance: { decorations: DecorationSet }) =>
instance.decorations,
},
);
export default function Input(props: InputProps) {
ensureMarkdownStyles();
// eslint-disable-next-line no-unassigned-vars
let element: HTMLDivElement | undefined;
let view: EditorView | undefined;
let ignoreSync = false;
const externalValue = createMemo(() => props.value);
onMount(() => {
if (!element) return;
const state = EditorState.create({
doc: props.value,
extensions: createEditorExtensions(
(value) => {
ignoreSync = true;
props.setValue(value);
},
() => props.placeholder,
() => Boolean(props.invertEnterBehavior),
() => props.onSubmit?.(),
),
});
view = new EditorView({
state,
parent: element,
});
});
createEffect(() => {
const editor = view;
if (!editor) return;
const next = externalValue();
const current = editor.state.doc.toString();
if (ignoreSync) {
ignoreSync = false;
return;
}
if (next === current) return;
editor.dispatch({
changes: {
from: 0,
to: current.length,
insert: next,
},
});
});
onCleanup(() => {
view?.destroy();
view = undefined;
});
return <div ref={element} class="tm-md-root" />;
}
function createEditorExtensions(
onChange: (value: string) => void,
getPlaceholder: () => string | undefined,
getInvertEnterBehavior: () => boolean,
onSubmit: () => void,
): Extension[] {
const customEnterKeymap = keymap.of([
{
key: "Shift-Enter",
run: () => {
if (!getInvertEnterBehavior()) {
return false;
}
onSubmit();
return true;
},
},
{
key: "Enter",
run: () => {
if (getInvertEnterBehavior()) {
return false;
}
onSubmit();
return true;
},
},
]);
return [
history(),
markdown(),
customEnterKeymap,
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
EditorView.lineWrapping,
placeholder(getPlaceholder() ?? ""),
EditorView.updateListener.of((update: ViewUpdate) => {
if (!update.docChanged) return;
onChange(update.state.doc.toString());
}),
EditorView.theme({
"&": {
fontSize: "1rem",
},
"&.cm-editor": {
width: "100%",
},
}),
EditorView.editorAttributes.of({
class: "tm-md-editor",
spellcheck: "true",
"aria-label": "Markdown input",
}),
markdownDecorations,
];
}
function buildDecorations(view: EditorView): DecorationSet {
const builder: Range<Decoration>[] = [];
const selections = view.state.selection.ranges.map(
(range: SelectionRange) => ({
from: range.from,
to: range.to,
}),
);
let codeFenceOpen = false;
for (
let lineNumber = 1;
lineNumber <= view.state.doc.lines;
lineNumber += 1
) {
const line = view.state.doc.line(lineNumber);
const text = line.text;
const lineFrom = line.from;
const trimmed = text.trim();
const fence = text.match(/^```\s*([^`]*)$/);
if (fence) {
const ticksStart = lineFrom + text.indexOf("```");
const ticksEnd = ticksStart + 3;
addHiddenToken(builder, selections, { from: ticksStart, to: ticksEnd });
if (trimmed.length > 3) {
addHiddenToken(builder, selections, {
from: ticksEnd,
to: line.to,
});
}
codeFenceOpen = !codeFenceOpen;
continue;
}
if (codeFenceOpen) {
builder.push(codeLineDecoration.range(lineFrom));
continue;
}
const heading = text.match(/^(#{1,6})\s+/);
if (heading) {
const markerLength = heading[0].length;
addHiddenToken(builder, selections, {
from: lineFrom,
to: lineFrom + markerLength,
});
const level = heading[1].length;
const headingClass = Decoration.mark({
class: `tm-md-heading tm-md-h${String(level)}`,
});
const contentFrom = lineFrom + markerLength;
if (contentFrom < line.to) {
builder.push(headingClass.range(contentFrom, line.to));
}
}
const quote = text.match(/^>\s?/);
if (quote) {
addHiddenToken(builder, selections, {
from: lineFrom,
to: lineFrom + quote[0].length,
});
}
const unordered = text.match(/^(\s*)([-+*])\s+(?:\[( |x|X)\]\s+)?/);
if (unordered) {
const markerStart = lineFrom + unordered[1].length;
const markerEnd = markerStart + unordered[2].length + 1;
addHiddenToken(builder, selections, { from: markerStart, to: markerEnd });
const checkbox = unordered[0].match(/\[( |x|X)\]\s+$/);
if (checkbox) {
const checkboxStart = lineFrom + unordered[0].lastIndexOf("[");
addHiddenToken(builder, selections, {
from: checkboxStart,
to: checkboxStart + checkbox[0].length,
});
}
}
const ordered = text.match(/^(\s*)(\d+\.)\s+/);
if (ordered) {
const markerStart = lineFrom + ordered[1].length;
addHiddenToken(builder, selections, {
from: markerStart,
to: markerStart + ordered[2].length + 1,
});
}
if (/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(trimmed)) {
if (lineFrom < line.to) {
builder.push(
Decoration.mark({ class: "tm-md-hr" }).range(lineFrom, line.to),
);
}
continue;
}
const tableSeparator = /^\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?$/.test(
text,
);
if (tableSeparator) {
if (lineFrom < line.to) {
builder.push(
Decoration.mark({ class: "tm-md-del" }).range(lineFrom, line.to),
);
}
continue;
}
const { styleRanges, tokenRanges } = collectInlineRanges(text, lineFrom);
for (const range of styleRanges) {
if (range.from >= range.to) continue;
if (range.className === "tm-md-strong") {
builder.push(strongDecoration.range(range.from, range.to));
} else if (range.className === "tm-md-em") {
builder.push(emDecoration.range(range.from, range.to));
} else if (range.className === "tm-md-del") {
builder.push(delDecoration.range(range.from, range.to));
} else if (range.className === "tm-md-code") {
builder.push(codeDecoration.range(range.from, range.to));
} else if (range.className === "tm-md-link") {
builder.push(linkDecoration.range(range.from, range.to));
}
}
for (const token of tokenRanges) {
addHiddenToken(builder, selections, token);
}
}
return Decoration.set(builder, true);
}
/**
* Keeps markdown syntax visible only when user selection intersects the token.
* This preserves cursor predictability and cross-token selection while still hiding syntax during reading.
*/
function addHiddenToken(
builder: Range<Decoration>[],
selections: ReadonlyArray<{ from: number; to: number }>,
token: TokenRange,
): void {
if (token.from >= token.to) return;
const overlapsSelection = selections.some((selection) => {
const selectionFrom = Math.min(selection.from, selection.to);
const selectionTo = Math.max(selection.from, selection.to);
if (selectionFrom === selectionTo) {
return selectionFrom >= token.from && selectionFrom <= token.to;
}
return selectionFrom < token.to && selectionTo > token.from;
});
if (overlapsSelection) return;
builder.push(hiddenTokenDecoration.range(token.from, token.to));
}

View file

@ -0,0 +1,623 @@
import { For, Index, type JSX } from "solid-js";
type InlineNode =
| { type: "text"; value: string }
| { type: "strong"; value: string }
| { type: "em"; value: string }
| { type: "del"; value: string }
| { type: "code"; value: string }
| { type: "link"; label: string; href: string }
| { type: "image"; alt: string; src: string };
type InlineDecorationRange = {
from: number;
to: number;
className: string;
};
type InlineTokenRange = {
from: number;
to: number;
};
type ParagraphBlock = {
type: "paragraph";
text: string;
};
type HeadingBlock = {
type: "heading";
level: number;
text: string;
};
type HrBlock = {
type: "hr";
};
type BlockQuoteBlock = {
type: "blockquote";
text: string;
};
type CodeBlock = {
type: "code";
language: string;
code: string;
};
type ListItem = {
text: string;
checked: boolean | null;
};
type ListBlock = {
type: "list";
ordered: boolean;
items: ListItem[];
};
type TableBlock = {
type: "table";
headers: string[];
rows: string[][];
};
type MarkdownBlock =
| ParagraphBlock
| HeadingBlock
| HrBlock
| BlockQuoteBlock
| CodeBlock
| ListBlock
| TableBlock;
const INLINE_TOKEN_REGEX =
/!\[([^\]]*)\]\(([^)\s]+(?:\s+"[^"]*")?)\)|\[([^\]]+)\]\(([^)\s]+(?:\s+"[^"]*")?)\)|`([^`\n]+)`|~~([^~\n]+)~~|\*\*([^*\n]+)\*\*|__([^_\n]+)__|\*([^*\n]+)\*|_([^_\n]+)_/g;
export function parseInlineNodes(input: string): InlineNode[] {
const nodes: InlineNode[] = [];
let cursor = 0;
let match = INLINE_TOKEN_REGEX.exec(input);
while (match) {
const index = match.index;
const raw = match[0];
if (index > cursor) {
nodes.push({ type: "text", value: input.slice(cursor, index) });
}
if (match[1] !== undefined && match[2] !== undefined) {
nodes.push({ type: "image", alt: match[1], src: normalizeUrl(match[2]) });
} else if (match[3] !== undefined && match[4] !== undefined) {
nodes.push({
type: "link",
label: match[3],
href: normalizeUrl(match[4]),
});
} else if (match[5] !== undefined) {
nodes.push({ type: "code", value: match[5] });
} else if (match[6] !== undefined) {
nodes.push({ type: "del", value: match[6] });
} else if (match[7] !== undefined || match[8] !== undefined) {
nodes.push({ type: "strong", value: match[7] ?? match[8] ?? "" });
} else if (match[9] !== undefined || match[10] !== undefined) {
nodes.push({ type: "em", value: match[9] ?? match[10] ?? "" });
} else {
nodes.push({ type: "text", value: raw });
}
cursor = index + raw.length;
match = INLINE_TOKEN_REGEX.exec(input);
}
if (cursor < input.length) {
nodes.push({ type: "text", value: input.slice(cursor) });
}
INLINE_TOKEN_REGEX.lastIndex = 0;
return nodes;
}
export function collectInlineRanges(
input: string,
offset = 0,
): {
styleRanges: InlineDecorationRange[];
tokenRanges: InlineTokenRange[];
} {
const styleRanges: InlineDecorationRange[] = [];
const tokenRanges: InlineTokenRange[] = [];
let match = INLINE_TOKEN_REGEX.exec(input);
while (match) {
const raw = match[0];
const start = offset + match.index;
const end = start + raw.length;
if (match[1] !== undefined && match[2] !== undefined) {
const openLength = 2;
const closeLength = raw.endsWith(")") ? 1 : 0;
const imageEnd = start + openLength + match[1].length;
tokenRanges.push({ from: start, to: start + openLength });
tokenRanges.push({ from: imageEnd, to: imageEnd + 1 });
const srcStart = imageEnd + 1;
const srcEnd = end - closeLength;
tokenRanges.push({ from: srcStart, to: srcStart + 1 });
tokenRanges.push({ from: srcEnd, to: srcEnd + closeLength });
} else if (match[3] !== undefined && match[4] !== undefined) {
const label = match[3];
const labelStart = start + 1;
const labelEnd = labelStart + label.length;
tokenRanges.push({ from: start, to: start + 1 });
tokenRanges.push({ from: labelEnd, to: labelEnd + 1 });
tokenRanges.push({ from: labelEnd + 1, to: labelEnd + 2 });
tokenRanges.push({ from: end - 1, to: end });
styleRanges.push({
from: labelStart,
to: labelEnd,
className: "tm-md-link",
});
} else if (match[5] !== undefined) {
const codeStart = start + 1;
const codeEnd = end - 1;
tokenRanges.push({ from: start, to: start + 1 });
tokenRanges.push({ from: end - 1, to: end });
styleRanges.push({
from: codeStart,
to: codeEnd,
className: "tm-md-code",
});
} else if (match[6] !== undefined) {
const contentStart = start + 2;
const contentEnd = end - 2;
tokenRanges.push({ from: start, to: start + 2 });
tokenRanges.push({ from: end - 2, to: end });
styleRanges.push({
from: contentStart,
to: contentEnd,
className: "tm-md-del",
});
} else if (match[7] !== undefined || match[8] !== undefined) {
const contentStart = start + 2;
const contentEnd = end - 2;
tokenRanges.push({ from: start, to: start + 2 });
tokenRanges.push({ from: end - 2, to: end });
styleRanges.push({
from: contentStart,
to: contentEnd,
className: "tm-md-strong",
});
} else if (match[9] !== undefined || match[10] !== undefined) {
const contentStart = start + 1;
const contentEnd = end - 1;
tokenRanges.push({ from: start, to: start + 1 });
tokenRanges.push({ from: end - 1, to: end });
styleRanges.push({
from: contentStart,
to: contentEnd,
className: "tm-md-em",
});
}
match = INLINE_TOKEN_REGEX.exec(input);
}
INLINE_TOKEN_REGEX.lastIndex = 0;
return { styleRanges, tokenRanges };
}
export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] {
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
const blocks: MarkdownBlock[] = [];
let index = 0;
while (index < lines.length) {
const line = lines[index];
if (!line.trim()) {
index += 1;
continue;
}
const codeFence = line.match(/^```\s*([^`]*)$/);
if (codeFence) {
const language = (codeFence[1] ?? "").trim();
const codeLines: string[] = [];
index += 1;
while (index < lines.length && !/^```\s*$/.test(lines[index])) {
codeLines.push(lines[index]);
index += 1;
}
if (index < lines.length) {
index += 1;
}
blocks.push({ type: "code", language, code: codeLines.join("\n") });
continue;
}
if (/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(line.trim())) {
blocks.push({ type: "hr" });
index += 1;
continue;
}
const heading = line.match(/^(#{1,6})\s+(.+)$/);
if (heading) {
blocks.push({
type: "heading",
level: heading[1].length,
text: heading[2],
});
index += 1;
continue;
}
const quote = line.match(/^>\s?(.*)$/);
if (quote) {
const quoteLines: string[] = [quote[1]];
index += 1;
while (index < lines.length) {
const next = lines[index].match(/^>\s?(.*)$/);
if (!next) break;
quoteLines.push(next[1]);
index += 1;
}
blocks.push({ type: "blockquote", text: quoteLines.join("\n") });
continue;
}
const tableCandidate = readTable(lines, index);
if (tableCandidate) {
blocks.push(tableCandidate.block);
index = tableCandidate.nextIndex;
continue;
}
const unordered = line.match(/^\s*[-*+]\s+(.*)$/);
const ordered = line.match(/^\s*\d+\.\s+(.*)$/);
if (unordered || ordered) {
const orderedList = Boolean(ordered);
const items: ListItem[] = [];
while (index < lines.length) {
const current = lines[index];
const match = orderedList
? current.match(/^\s*\d+\.\s+(.*)$/)
: current.match(/^\s*[-*+]\s+(.*)$/);
if (!match) break;
const task = match[1].match(/^\[( |x|X)\]\s+(.*)$/);
if (task) {
items.push({
text: task[2],
checked: task[1].toLowerCase() === "x",
});
} else {
items.push({ text: match[1], checked: null });
}
index += 1;
}
blocks.push({ type: "list", ordered: orderedList, items });
continue;
}
const paragraphLines = [line];
index += 1;
while (
index < lines.length &&
lines[index].trim() &&
!/^(#{1,6})\s+/.test(lines[index]) &&
!/^```\s*/.test(lines[index]) &&
!/^>\s?/.test(lines[index]) &&
!/^\s*[-*+]\s+/.test(lines[index]) &&
!/^\s*\d+\.\s+/.test(lines[index]) &&
!/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/.test(lines[index].trim())
) {
paragraphLines.push(lines[index]);
index += 1;
}
blocks.push({ type: "paragraph", text: paragraphLines.join("\n") });
}
return blocks;
}
export function renderInline(nodes: InlineNode[]): JSX.Element[] {
return nodes.map((node) => {
if (node.type === "text") {
return node.value;
}
if (node.type === "strong") {
return <strong class="tm-md-strong">{node.value}</strong>;
}
if (node.type === "em") {
return <em class="tm-md-em">{node.value}</em>;
}
if (node.type === "del") {
return <del class="tm-md-del">{node.value}</del>;
}
if (node.type === "code") {
return <code class="tm-md-code">{node.value}</code>;
}
if (node.type === "link") {
return (
<a class="tm-md-link" href={node.href} target="_blank" rel="noreferrer">
{node.label}
</a>
);
}
return (
<img
class="tm-md-image"
src={node.src}
alt={node.alt}
loading="lazy"
decoding="async"
/>
);
});
}
export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
return (
<For each={blocks}>
{(block) => {
if (block.type === "heading") {
const className = `tm-md-heading tm-md-h${String(block.level)}`;
if (block.level === 1)
return (
<h1 class={className}>
{renderInline(parseInlineNodes(block.text))}
</h1>
);
if (block.level === 2)
return (
<h2 class={className}>
{renderInline(parseInlineNodes(block.text))}
</h2>
);
if (block.level === 3)
return (
<h3 class={className}>
{renderInline(parseInlineNodes(block.text))}
</h3>
);
if (block.level === 4)
return (
<h4 class={className}>
{renderInline(parseInlineNodes(block.text))}
</h4>
);
if (block.level === 5)
return (
<h5 class={className}>
{renderInline(parseInlineNodes(block.text))}
</h5>
);
return (
<h6 class={className}>
{renderInline(parseInlineNodes(block.text))}
</h6>
);
}
if (block.type === "blockquote") {
return (
<blockquote class="tm-md-blockquote">
<For each={block.text.split("\n")}>
{(line) => <p>{renderInline(parseInlineNodes(line))}</p>}
</For>
</blockquote>
);
}
if (block.type === "code") {
return (
<pre class="tm-md-pre">
<code class="tm-md-codeblock" data-language={block.language}>
{block.code}
</code>
</pre>
);
}
if (block.type === "list") {
const Tag = block.ordered ? "ol" : "ul";
return (
<Tag class={block.ordered ? "tm-md-ol" : "tm-md-ul"}>
<For each={block.items}>
{(item) => (
<li class="tm-md-li">
{item.checked !== null ? (
<input
type="checkbox"
checked={item.checked}
disabled
class="tm-md-checkbox"
/>
) : null}
<span>{renderInline(parseInlineNodes(item.text))}</span>
</li>
)}
</For>
</Tag>
);
}
if (block.type === "table") {
return (
<div class="tm-md-table-wrap">
<table class="tm-md-table">
<thead>
<tr>
<For each={block.headers}>
{(header) => (
<th>{renderInline(parseInlineNodes(header))}</th>
)}
</For>
</tr>
</thead>
<tbody>
<For each={block.rows}>
{(row) => (
<tr>
<For each={row}>
{(cell) => (
<td>{renderInline(parseInlineNodes(cell))}</td>
)}
</For>
</tr>
)}
</For>
</tbody>
</table>
</div>
);
}
if (block.type === "hr") {
return <hr class="tm-md-hr" />;
}
return (
<p class="tm-md-p">
<Index each={block.text.split("\n")}>
{(line, idx) => (
<>
{idx > 0 ? <br /> : null}
{renderInline(parseInlineNodes(line()))}
</>
)}
</Index>
</p>
);
}}
</For>
);
}
function normalizeUrl(input: string): string {
const value = input.trim();
if (/^(https?:|mailto:|tel:|\/)/i.test(value)) {
return value;
}
return "#";
}
function splitTableRow(row: string): string[] {
const cleaned = row.trim().replace(/^\|/, "").replace(/\|$/, "");
return cleaned.split("|").map((cell) => cell.trim());
}
function readTable(
lines: string[],
index: number,
): { block: TableBlock; nextIndex: number } | null {
const header = lines[index] ?? "";
const separator = lines[index + 1] ?? "";
if (!header.includes("|") || !separator.includes("|")) {
return null;
}
const separatorCells = splitTableRow(separator);
const isSeparator = separatorCells.every((cell) => /^:?-{3,}:?$/.test(cell));
if (!isSeparator) {
return null;
}
const headers = splitTableRow(header);
const rows: string[][] = [];
let cursor = index + 2;
while (cursor < lines.length && lines[cursor].includes("|")) {
rows.push(splitTableRow(lines[cursor]));
cursor += 1;
}
return {
block: { type: "table", headers, rows },
nextIndex: cursor,
};
}
export const markdownStyles = `
.tm-md-root { color: var(--foreground); line-height: 1.55; font-size: 0.95rem; }
.tm-md-root * { box-sizing: border-box; }
.tm-md-heading { margin: 0.2rem 0 0.35rem; font-weight: 700; line-height: 1.25; }
.tm-md-h1 { font-size: 1.65rem; }
.tm-md-h2 { font-size: 1.45rem; }
.tm-md-h3 { font-size: 1.25rem; }
.tm-md-h4 { font-size: 1.1rem; }
.tm-md-h5 { font-size: 1rem; }
.tm-md-h6 { font-size: 0.95rem; opacity: 0.9; }
.tm-md-p { margin: 0.25rem 0; }
.tm-md-blockquote { margin: 0.45rem 0; padding-left: 0.75rem; border-left: 2px solid var(--border); opacity: 0.95; }
.tm-md-blockquote p { margin: 0.2rem 0; }
.tm-md-pre { margin: 0.45rem 0; padding: 0.65rem 0.75rem; border: 1px solid var(--border); border-radius: 0.5rem; background: var(--muted); overflow-x: auto; }
.tm-md-code, .tm-md-codeblock { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.tm-md-code { padding: 0.08rem 0.32rem; border-radius: 0.28rem; background: var(--muted); }
.tm-md-strong { font-weight: 700; }
.tm-md-em { font-style: italic; }
.tm-md-del { text-decoration: line-through; }
.tm-md-link { color: var(--primary); text-decoration: underline; text-underline-offset: 0.14rem; }
.tm-md-image { display: block; max-width: 100%; border-radius: 0.4rem; margin: 0.5rem 0; }
.tm-md-ul, .tm-md-ol { margin: 0.3rem 0 0.35rem 1.2rem; padding: 0; }
.tm-md-li { margin: 0.2rem 0; }
.tm-md-checkbox { margin-right: 0.5rem; vertical-align: middle; }
.tm-md-table-wrap { overflow-x: auto; margin: 0.45rem 0; }
.tm-md-table { border-collapse: collapse; width: 100%; min-width: 16rem; }
.tm-md-table th, .tm-md-table td { border: 1px solid var(--border); padding: 0.4rem 0.5rem; text-align: left; }
.tm-md-table th { background: var(--muted); font-weight: 600; }
.tm-md-hr { border: 0; border-top: 1px solid var(--border); margin: 0.55rem 0; }
.cm-editor.tm-md-editor { border: 1px solid var(--border); border-radius: 0.65rem; background: var(--background); }
.cm-editor.tm-md-editor.cm-focused { outline: 2px solid var(--ring); outline-offset: 1px; }
.cm-editor.tm-md-editor .cm-scroller { font-family: inherit; line-height: 1.55; max-height: 30vh; overflow-y: auto; overflow-x: hidden; }
.cm-editor.tm-md-editor .cm-content { padding: 0.7rem 0.85rem; min-height: 2.75rem; }
.cm-editor.tm-md-editor .cm-line { padding: 0 1px; }
.cm-editor.tm-md-editor .tm-md-hidden-token { color: transparent; opacity: 0; font-size: inherit; }
.cm-editor.tm-md-editor .tm-md-code-line { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; background: var(--muted); border-radius: 0.3rem; }
`;
export function ensureMarkdownStyles(): void {
if (typeof document === "undefined") return;
const styleId = "tensamin-markdown-styles";
if (document.getElementById(styleId)) return;
const style = document.createElement("style");
style.id = styleId;
style.textContent = markdownStyles;
document.head.appendChild(style);
}

View file

@ -0,0 +1,19 @@
import { createMemo } from "solid-js";
import {
ensureMarkdownStyles,
parseMarkdownBlocks,
renderBlocks,
} from "./markdown";
export type TextProps = {
value: string;
};
export default function Text(props: TextProps) {
ensureMarkdownStyles();
const blocks = createMemo(() => parseMarkdownBlocks(props.value));
return <div class="tm-md-root">{renderBlocks(blocks())}</div>;
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View file

@ -0,0 +1,20 @@
{
"name": "@tensamin/core-storage",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./context": "./src/context.tsx",
"./indexed-db": "./src/indexed-db.ts"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@tensamin/shared": "workspace:*",
"@tensamin/ui": "workspace:*",
"solid-js": "^1.9.11"
}
}

View file

@ -0,0 +1,131 @@
import {
createContext,
createSignal,
Show,
useContext,
type ParentProps,
} from "solid-js";
import {
type Storage as StorageSchema,
storageDefaults as defaults,
} from "@tensamin/shared/data";
import { getEntry, setEntry, deleteEntry } from "./indexed-db";
import ErrorScreen from "@tensamin/ui/screens/error";
import { createStore } from "solid-js/store";
import { log } from "@tensamin/shared/log";
interface StorageContextValue {
load<K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]>;
save<K extends keyof StorageSchema>(
key: K,
value: StorageSchema[K],
): Promise<void>;
clear: () => Promise<void>;
}
const StorageContext = createContext<StorageContextValue>();
const isIndexedDBSupported = typeof indexedDB !== "undefined";
export default function StorageProvider(props: ParentProps) {
const [storage, setStorage] = createStore<StorageSchema>(defaults);
const [error, setError] = createSignal<string>("");
const [errorDescription, setErrorDescription] = createSignal<string>("");
async function loadIO<K extends keyof StorageSchema>(
key: K,
): Promise<StorageSchema[K]> {
let stored;
try {
stored = await getEntry(key);
} catch (err) {
setError("Failed to load data");
setErrorDescription(
"An error occurred while loading data from IndexedDB. Please try again.",
);
log(0, "Storage", "red", err);
}
if (stored !== undefined) {
setStorage(key, stored);
return stored;
}
return defaults[key];
}
async function saveIO<K extends keyof StorageSchema>(
key: K,
value: StorageSchema[K],
): Promise<void> {
if (JSON.stringify(value) === JSON.stringify(defaults[key])) {
await deleteEntry(key);
setStorage(key, defaults[key]);
} else {
await setEntry(key, value);
setStorage(key, value);
}
}
const value: StorageContextValue = {
async load<K extends keyof StorageSchema>(
key: K,
): Promise<StorageSchema[K]> {
if (
storage[key] === undefined ||
JSON.stringify(storage[key]) === JSON.stringify(defaults[key])
) {
const value = await loadIO(key);
setStorage(key, value);
}
return storage[key];
},
async save<K extends keyof StorageSchema>(
key: K,
value: StorageSchema[K],
): Promise<void> {
await saveIO(key, value);
setStorage(key, value);
},
async clear() {
const keys = Object.keys(defaults) as (keyof StorageSchema)[];
await Promise.all(keys.map((key) => deleteEntry(key)));
},
};
// @ts-expect-error development utility
window.save = value.save;
return (
<Show
when={error() !== "" && errorDescription() !== ""}
fallback={
<Show
when={isIndexedDBSupported}
fallback={
<ErrorScreen
error="Unsupported Browser"
description="Your browser does not support IndexedDB, which is required for this application to function."
/>
}
>
<StorageContext.Provider value={value}>
{props.children}
</StorageContext.Provider>
</Show>
}
>
<ErrorScreen error={error()} description={errorDescription()} />
</Show>
);
}
export function useStorage(): StorageContextValue {
const context = useContext(StorageContext);
if (!context) {
throw new Error("useStorage must be used within a StorageProvider");
}
return context;
}

View file

@ -0,0 +1,71 @@
import type { Storage as StorageSchema } from "@tensamin/shared/data";
const DB_NAME = "tensamin";
const DB_VERSION = 1;
const STORE_NAME = "storage";
let dbPromise: Promise<IDBDatabase> | null = null;
function openDB(): Promise<IDBDatabase> {
if (dbPromise) return dbPromise;
dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
return dbPromise;
}
export async function getEntry<K extends keyof StorageSchema>(
key: K,
): Promise<StorageSchema[K] | undefined> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readonly");
const store = tx.objectStore(STORE_NAME);
const request = store.get(key as string);
request.onsuccess = () =>
resolve(request.result as StorageSchema[K] | undefined);
request.onerror = () => reject(request.error);
});
}
export async function setEntry<K extends keyof StorageSchema>(
key: K,
value: StorageSchema[K],
): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readwrite");
const store = tx.objectStore(STORE_NAME);
const request = store.put(value, key as string);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
export async function deleteEntry<K extends keyof StorageSchema>(
key: K,
): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, "readwrite");
const store = tx.objectStore(STORE_NAME);
const request = store.delete(key as string);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View file

@ -0,0 +1,22 @@
{
"name": "@tensamin/core-user",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./context": "./src/context.tsx",
"./wrapper": "./src/wrapper.tsx",
"./values": "./src/values.ts"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git",
"@tensamin/shared": "workspace:*",
"solid-js": "^1.9.11",
"zod": "^4.3.6"
}
}

View file

@ -0,0 +1,54 @@
import { createContext, useContext, type ParentProps } from "solid-js";
import { createStore } from "solid-js/store";
import { useSocket } from "@tensamin/ttp/context";
import { socket as schemas } from "@tensamin/shared/data";
import type z from "zod";
import { failedUser } from "./values";
export type User = z.infer<typeof schemas.get_user_data.response>;
interface contextValue {
get(userId: number): Promise<User>;
}
const UserContext = createContext<contextValue>();
export default function UserProvider(props: ParentProps) {
const [storage, setStorage] = createStore<Record<number, User>>({});
const { send } = useSocket();
async function get(userId: number): Promise<User> {
if (storage[userId] === undefined) {
try {
const userData = await send("get_user_data", { user_id: userId });
// Temp, add base64 stuff
userData.data.avatar = userData.data.avatar
? `data:image/png;base64,${userData.data.avatar}`
: undefined;
// Temp end
setStorage(userId, userData.data);
} catch {
setStorage(userId, failedUser);
}
}
return storage[userId];
}
return (
<UserContext.Provider value={{ get }}>
{props.children}
</UserContext.Provider>
);
}
export function useUser(): contextValue {
const context = useContext(UserContext);
if (!context) {
throw new Error("useUser must be used within a UserProvider");
}
return context;
}

View file

@ -0,0 +1,13 @@
import type { User } from "./context";
export const failedUser: User = {
user_id: 0,
display: "Failed",
iota_id: 0,
omikron_connections: [],
online_status: "user_offline",
public_key: "",
sub_end: 0,
sub_level: 0,
username: "failed",
};

View file

@ -0,0 +1,16 @@
import { createEffect, createSignal, type JSX } from "solid-js";
import { useUser, type User } from "./context";
export default function Wrapper(props: {
userId: number;
component: (user: User) => JSX.Element;
}) {
const { get } = useUser();
const [user, setUser] = createSignal<User | null>(null);
createEffect(() => {
get(props.userId).then(setUser);
});
return <>{user() ? props.component(user() as User) : null}</>;
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

1
packages/desktop/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build

View file

@ -0,0 +1,37 @@
import type { ElectrobunConfig } from "electrobun";
import { version } from "@tensamin/shared/package.json";
export default {
app: {
name: "tensamin",
identifier: "client.tensamin.net",
version: version,
},
build: {
views: {
mainview: {
entrypoint: "src/mainview/index.ts",
},
},
copy: {
"../web/dist/": "views/mainview/",
},
mac: {
defaultRenderer: "cef",
bundleCEF: true,
},
linux: {
defaultRenderer: "cef",
bundleCEF: true,
chromiumFlags: {
"enable-features": "UseOzonePlatform",
"ozone-platform": "wayland",
},
},
win: {
defaultRenderer: "cef",
bundleCEF: true,
},
},
} satisfies ElectrobunConfig;

24
packages/desktop/llms.txt Normal file
View file

@ -0,0 +1,24 @@
# Electrobun Project
This is an Electrobun desktop application.
IMPORTANT: Electrobun is NOT Electron. Do not use Electron APIs or patterns.
## Documentation
Full API reference: https://blackboard.sh/electrobun/llms.txt
Getting started: https://blackboard.sh/electrobun/docs/
## Quick Reference
Import patterns:
- Main process (Bun): `import { BrowserWindow } from "electrobun/bun"`
- Browser context: `import { Electroview } from "electrobun/view"`
Use `views://` URLs to load bundled assets (e.g., `url: "views://mainview/index.html"`).
Views must be configured in `electrobun.config.ts` to be built and copied into the bundle.
## About
Electrobun is built by Blackboard (https://blackboard.sh), an innovation lab building
tools and funding teams that define the next generation of technology.

View file

@ -0,0 +1,19 @@
{
"name": "electrobun-hello-world",
"version": "1.0.0",
"description": "A simple Electrobun app showcasing core features",
"scripts": {
"format": "bunx prettier --write .",
"lint": "echo desktop lint skipped",
"build": "echo desktop build skipped",
"start": "electrobun dev",
"dev": "electrobun dev --watch"
},
"dependencies": {
"electrobun": "1.15.1",
"@tensamin/shared": "workspace:*"
},
"devDependencies": {
"@types/bun": "latest"
}
}

View file

@ -0,0 +1,41 @@
{pkgs ? import <nixpkgs> {}}: let
libs = with pkgs; [
nspr
nss
libxcb
libxkbcommon
wayland
libdecor
dbus
atk
glib
gtk3
cups
libx11
libxcomposite
libxdamage
libxext
libxfixes
libxrandr
libgbm
expat
cairo
pango
alsa-lib
webkitgtk_4_1
libsoup_3
libayatana-appindicator
gdk-pixbuf
];
in
pkgs.mkShell {
buildInputs = [pkgs.bun] ++ libs;
shellHook = ''
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libs}:$LD_LIBRARY_PATH
export GDK_BACKEND=wayland
export OZONE_PLATFORM=wayland
export XDG_SESSION_TYPE=wayland
'';
}

View file

@ -0,0 +1,18 @@
import { BrowserWindow } from "electrobun/bun";
// Create the main application window
const mainWindow = new BrowserWindow({
title: "Hello Electrobun!",
url: "views://mainview/index.html",
renderer: "cef",
frame: {
width: 800,
height: 800,
x: 200,
y: 200,
},
});
mainWindow.url = "views://mainview/index.html";
console.log("Hello Electrobun app started!");

View file

@ -0,0 +1,126 @@
* {
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
h1 {
color: white;
font-size: 3rem;
text-align: center;
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
color: rgba(255, 255, 255, 0.9);
font-size: 1.25rem;
text-align: center;
margin-top: 0;
margin-bottom: 40px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.welcome-section {
background: white;
border-radius: 12px;
padding: 30px;
margin: 30px 0;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
line-height: 1.6;
}
h2 {
color: #2563eb;
margin-top: 30px;
margin-bottom: 15px;
}
ul {
margin: 20px 0;
padding-left: 20px;
}
li {
margin: 8px 0;
}
.links {
display: flex;
gap: 15px;
margin: 25px 0;
flex-wrap: wrap;
}
.doc-link {
display: inline-block;
background: #2563eb;
color: white;
text-decoration: none;
padding: 12px 20px;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2);
}
.doc-link:hover {
background: #1d4ed8;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(37, 99, 235, 0.3);
}
code {
background: #f1f5f9;
color: #475569;
padding: 2px 6px;
border-radius: 4px;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 0.9em;
}
.footer {
text-align: center;
color: rgba(255, 255, 255, 0.8);
margin-top: 40px;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
backdrop-filter: blur(10px);
}
.footer p {
margin: 8px 0;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.welcome-section {
background: #1f2937;
color: #f3f4f6;
}
h2 {
color: #60a5fa;
}
code {
background: #374151;
color: #d1d5db;
}
}

View file

@ -0,0 +1,64 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello Electrobun!</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div class="container">
<h1>Hello Electrobun! 🎉</h1>
<p class="subtitle">A fast, cross-platform desktop app framework</p>
<div class="welcome-section">
<p>
Welcome to your first Electrobun app! This framework combines the
power of Bun with native desktop capabilities.
</p>
<h2>What is Electrobun?</h2>
<ul>
<li>
<strong>Fast:</strong> Built on Bun's lightning-fast JavaScript
runtime
</li>
<li>
<strong>Native:</strong> Access to system APIs like menus, trays,
and file dialogs
</li>
<li>
<strong>Cross-platform:</strong> Works on macOS, Windows, and Linux
</li>
<li>
<strong>Web-based UI:</strong> Use familiar HTML, CSS, and
JavaScript for your interface
</li>
</ul>
<h2>Get Started</h2>
<p>
Ready to build something amazing? Check out the documentation and
examples:
</p>
<div class="links">
<a href="https://electrobun.dev/" class="doc-link"> 📚 Electrobun </a>
<a href="https://github.com/blackboardsh/electrobun" class="doc-link">
🐙 GitHub Repository
</a>
<a href="https://electrobun.dev/docs/apis/bun/" class="doc-link">
💡 Api Docs
</a>
</div>
</div>
<div class="footer">
<p>
Edit <code>src/bun/index.ts</code> and <code>src/mainview/</code> to
customize your app
</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1 @@
console.log("Hello Electrobun view loaded!");

View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": true
},
"include": ["src/**/*", "./electrobun.config.ts"],
"exclude": ["node_modules", "dist", "build", "../../package/dist"]
}

View file

@ -0,0 +1,22 @@
{
"name": "@tensamin/notifications",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./context": "./src/context.tsx"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git",
"@solidjs/router": "^0.15.4",
"lucide-solid": "^0.564.0",
"solid-js": "^1.9.11",
"solid-sonner": "^0.2.8",
"zod": "^4.3.6"
}
}

View file

@ -0,0 +1,26 @@
import { createContext, useContext } from "solid-js";
import type { ParentProps } from "solid-js";
export const context = createContext<contextType>();
export default function Provider(props: ParentProps) {
// live_messages
return (
<context.Provider value={{ test: () => {} }}>
{props.children}
</context.Provider>
);
}
type contextType = {
test: () => void;
};
export function useNotifications(): contextType {
const ctx = useContext(context);
if (!ctx) {
throw new Error("useNotifications must be used within a ChatProvider");
}
return ctx;
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View file

@ -0,0 +1,23 @@
{
"name": "@tensamin/shared",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./data": "./src/data.ts",
"./log": "./src/log.tsx",
"./features/legal/schema": "./src/features/legal/schema.ts",
"./features/conversation/schema": "./src/features/conversation/schema.ts"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"lucide-solid": "^0.564.0",
"solid-js": "^1.9.11",
"solid-sonner": "^0.2.8",
"zod": "^4.3.6"
}
}

164
packages/shared/src/data.ts Normal file
View file

@ -0,0 +1,164 @@
import { z } from "zod";
import { legalDocsSchema } from "./features/legal/schema";
import { community, conversation } from "./features/conversation/schema";
const fileFromMessage = z.object({
name: z.string(),
id: z.uuidv4(),
type: z.enum(["image", "image_top_right", "file"]),
});
const message = z.object({
height: z.number(),
not_encrypted: z.boolean().optional(),
sent_by_self: z.boolean(),
timestamp: z.number(),
content: z.base64(),
files: z.array(fileFromMessage).optional(),
tint: z.string().length(7).startsWith("#").optional(),
avatar: z.boolean().optional(),
display: z.boolean().optional(),
});
// Socket
export const socket = {
identification: {
request: z.object({
user_id: z.number(),
}),
response: z.object({
challenge: z.string(),
public_key: z.base64(),
}),
},
get_user_data: {
request: z.object({
user_id: z.number(),
}),
response: z.object({
about: z.string().max(255).optional(),
avatar: z.string().optional(),
display: z.string().max(15),
iota_id: z.number(),
omikron_connections: z.array(z.number()),
omikron_id: z.number().optional(),
online_status: z.enum([
"user_offline",
"user_online",
"user_dnd",
"user_idle",
"user_wc",
"user_borked",
"iota_offline",
"iota_online",
"iota_borked",
]),
public_key: z.base64(),
status: z.string().max(15).optional(),
sub_end: z.number(),
sub_level: z.number(),
user_id: z.number(),
username: z.string().max(15),
}),
},
challenge_response: {
request: z.object({
challenge: z.base64(),
}),
response: z.object({}),
},
ping: {
request: z.object({
last_ping: z.number(),
}),
response: z.object({
ping_iota: z.number(),
}),
},
get_chats: {
request: z.object({}),
response: z.object({
user_ids: z.array(conversation),
}),
},
get_communities: {
request: z.object({}),
response: z.object({
communities: z.array(community),
}),
},
live_message: {
request: z.object({}),
response: z.object({
sender_id: z.number(),
send_time: z.number(),
message,
}),
},
messages_get: {
request: z.object({
user_id: z.number(),
amount: z.number(),
offset: z.number(),
}),
response: z.object({
messages: z.array(message),
}),
},
message_send: {
request: z.object({
height: z.number(),
content: z.base64(),
receiver_id: z.number(),
send_time: z.number(),
files: z.array(fileFromMessage).optional(),
}),
response: z.object({}),
},
} satisfies Record<string, { request: z.ZodType; response: z.ZodType }>;
export type Socket = typeof socket;
// Storage
export interface Storage {
user_id: number;
private_key: string;
ppandtos_done: boolean;
accepted_terms_of_service: boolean;
accepted_privacy_policy: boolean;
analytics_crash_reports: boolean;
analytics_usage_data: boolean;
analytics_done: boolean;
legal_docs: z.infer<typeof legalDocsSchema>;
chat_invert_enter_behaviour: boolean;
}
export const storageDefaults: Storage = {
user_id: 0,
private_key: "",
ppandtos_done: false,
accepted_terms_of_service: false,
accepted_privacy_policy: false,
analytics_crash_reports: true,
analytics_usage_data: true,
analytics_done: false,
legal_docs: {
eula: {
version: "0.0",
hash: "000000000000",
unix: 0,
},
tos: {
version: "0.0",
hash: "000000000000",
unix: 0,
},
pp: {
version: "0.0",
hash: "000000000000",
unix: 0,
},
},
chat_invert_enter_behaviour: false,
};

View file

@ -0,0 +1,16 @@
import { z } from "zod";
export const conversation = z.object({
user_id: z.number(),
calls: z.array(z.uuidv4()).optional(),
last_message_at: z.number(),
});
export const community = z.object({
community_address: z.string(),
community_title: z.string(),
position: z.string(),
});
export type Conversation = z.infer<typeof conversation>;
export type Community = z.infer<typeof community>;

View file

@ -0,0 +1,13 @@
import { z } from "zod";
const legalDocSchema = z.object({
version: z.string().regex(/^\d+\.\d+$/),
hash: z.string().regex(/^[a-f0-9]{12}$/),
unix: z.number().int().positive(),
});
export const legalDocsSchema = z.object({
eula: legalDocSchema,
tos: legalDocSchema,
pp: legalDocSchema,
});

View file

@ -0,0 +1,52 @@
import { toast as sonnerToast } from "solid-sonner";
import { Ban, Check, Info, TriangleAlert } from "lucide-solid";
export function log(
logLevel: number,
logger: string,
color: "red" | "green" | "yellow" | "purple" | "blue",
...args: unknown[]
) {
const currentLogLevel = Number(localStorage.getItem("log_level") || 0);
if (logLevel > currentLogLevel) return;
const colorCodes: Record<typeof color, string> = {
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
purple: "\x1b[35m",
blue: "\x1b[34m",
};
const resetCode = "\x1b[0m";
console.log(`${colorCodes[color]}[${logger}]${resetCode}`, ...args);
}
const size = 20;
export function toast(
type: "error" | "info" | "warn" | "success",
message: string,
) {
switch (type) {
case "error":
sonnerToast(message, {
icon: <Ban size={size} />,
});
break;
case "info":
sonnerToast(message, {
icon: <Info size={size} />,
});
break;
case "warn":
sonnerToast(message, {
icon: <TriangleAlert size={size} />,
});
break;
case "success":
sonnerToast(message, {
icon: <Check size={size} />,
});
break;
}
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

32
packages/ui/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "@tensamin/ui",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./*": "./src/ui/*.tsx",
"./screens/*": "./src/screens/*.tsx",
"./link": "./src/link.tsx"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@ark-ui/solid": "^5.34.1",
"@corvu/drawer": "^0.2.4",
"@corvu/otp-field": "^0.1.4",
"@corvu/resizable": "^0.2.5",
"@kobalte/core": "^0.13.11",
"@tanstack/solid-virtual": "^3.13.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk-solid": "^1.1.2",
"embla-carousel-solid": "^8.6.0",
"lucide-solid": "^0.564.0",
"solid-js": "^1.9.11",
"solid-sonner": "^0.2.8",
"tailwind-merge": "^3.5.0"
}
}

View file

@ -0,0 +1,5 @@
import type { ClassValue } from "clsx";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));

13
packages/ui/src/link.tsx Normal file
View file

@ -0,0 +1,13 @@
import { ExternalLink } from "lucide-solid";
export default function Link(props: { link: string; label: string }) {
return (
<a
target="_blank"
href={props.link}
class="flex gap-1.5 items-center justify-center text-primary border-b hover:border-primary border-transparent"
>
<ExternalLink size={17} /> {props.label}
</a>
);
}

View file

@ -0,0 +1,13 @@
import Link from "../link";
export default function Screen(props: { error: string; description: string }) {
return (
<div class="bg-background w-full h-screen flex flex-col justify-center items-center">
<p class="text-3xl font-bold">{props.error}</p>
<p class="pt-4 text-lg w-1/2 text-center text-muted-foreground pb-8">
{props.description}
</p>
<Link label="status.tensamin.net" link="https://status.tensamin.net" />
</div>
);
}

View file

@ -0,0 +1,45 @@
import { createSignal, createEffect, on } from "solid-js";
const DELAY = 250;
export default function Screen(props: { progress: number }) {
const [displayProgress, setDisplayProgress] = createSignal(0);
// Animation
createEffect(
on(
() => props.progress,
(target) => {
const start = displayProgress();
const delta = target - start;
if (delta === 0) return;
const duration = DELAY;
const startTime = performance.now();
function animate(now: number) {
const elapsed = now - startTime;
const t = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - t, 3);
setDisplayProgress(start + delta * eased);
if (t < 1) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
},
),
);
return (
<div class="bg-background w-full h-screen flex flex-col justify-center items-center">
<div class="w-64 h-1.5 bg-secondary rounded-full overflow-hidden relative">
<div
class="h-full bg-primary absolute left-0 top-0"
style={{
width: `${displayProgress()}%`,
}}
/>
</div>
</div>
);
}

View file

@ -0,0 +1,97 @@
import { cn } from "../libs/cn";
import type {
AccordionContentProps,
AccordionItemProps,
AccordionTriggerProps,
} from "@kobalte/core/accordion";
import { Accordion as AccordionPrimitive } from "@kobalte/core/accordion";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import { type ParentProps, type ValidComponent, splitProps } from "solid-js";
export const Accordion = AccordionPrimitive;
type accordionItemProps<T extends ValidComponent = "div"> =
AccordionItemProps<T> & {
class?: string;
};
export const AccordionItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, accordionItemProps<T>>,
) => {
const [local, rest] = splitProps(props as accordionItemProps, ["class"]);
return (
<AccordionPrimitive.Item class={cn("border-b", local.class)} {...rest} />
);
};
type accordionTriggerProps<T extends ValidComponent = "button"> = ParentProps<
AccordionTriggerProps<T> & {
class?: string;
}
>;
export const AccordionTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, accordionTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as accordionTriggerProps, [
"class",
"children",
]);
return (
<AccordionPrimitive.Header class="flex" as="div">
<AccordionPrimitive.Trigger
class={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-shadow hover:underline focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring [&[data-expanded]>svg]:rotate-180",
local.class,
)}
{...rest}
>
{local.children}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4 text-muted-foreground transition-transform duration-200"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m6 9l6 6l6-6"
/>
<title>Arrow</title>
</svg>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
};
type accordionContentProps<T extends ValidComponent = "div"> = ParentProps<
AccordionContentProps<T> & {
class?: string;
}
>;
export const AccordionContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, accordionContentProps<T>>,
) => {
const [local, rest] = splitProps(props as accordionContentProps, [
"class",
"children",
]);
return (
<AccordionPrimitive.Content
class={cn(
"animate-accordion-up overflow-hidden text-sm data-[expanded]:animate-accordion-down",
local.class,
)}
{...rest}
>
<div class="pb-4 pt-0">{local.children}</div>
</AccordionPrimitive.Content>
);
};

View file

@ -0,0 +1,152 @@
import { cn } from "../libs/cn";
import type {
AlertDialogCloseButtonProps,
AlertDialogContentProps,
AlertDialogDescriptionProps,
AlertDialogTitleProps,
} from "@kobalte/core/alert-dialog";
import { AlertDialog as AlertDialogPrimitive } from "@kobalte/core/alert-dialog";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
import { buttonVariants } from "./button";
export const AlertDialog = AlertDialogPrimitive;
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
type alertDialogContentProps<T extends ValidComponent = "div"> = ParentProps<
AlertDialogContentProps<T> & {
class?: string;
}
>;
export const AlertDialogContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, alertDialogContentProps<T>>,
) => {
const [local, rest] = splitProps(props as alertDialogContentProps, [
"class",
"children",
]);
return (
<AlertDialogPrimitive.Portal>
<AlertDialogPrimitive.Overlay
class={cn(
"fixed inset-0 z-50 bg-background/80 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
)}
/>
<AlertDialogPrimitive.Content
class={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg data-[closed]:duration-200 data-[expanded]:duration-200 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95 data-[closed]:slide-out-to-left-1/2 data-[closed]:slide-out-to-top-[48%] data-[expanded]:slide-in-from-left-1/2 data-[expanded]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
local.class,
)}
{...rest}
>
{local.children}
</AlertDialogPrimitive.Content>
</AlertDialogPrimitive.Portal>
);
};
export const AlertDialogHeader = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"flex flex-col space-y-2 text-center sm:text-left",
local.class,
)}
{...rest}
/>
);
};
export const AlertDialogFooter = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
local.class,
)}
{...rest}
/>
);
};
type alertDialogTitleProps<T extends ValidComponent = "h2"> =
AlertDialogTitleProps<T> & {
class?: string;
};
export const AlertDialogTitle = <T extends ValidComponent = "h2">(
props: PolymorphicProps<T, alertDialogTitleProps<T>>,
) => {
const [local, rest] = splitProps(props as alertDialogTitleProps, ["class"]);
return (
<AlertDialogPrimitive.Title
class={cn("text-lg font-semibold", local.class)}
{...rest}
/>
);
};
type alertDialogDescriptionProps<T extends ValidComponent = "p"> =
AlertDialogDescriptionProps<T> & {
class?: string;
};
export const AlertDialogDescription = <T extends ValidComponent = "p">(
props: PolymorphicProps<T, alertDialogDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as alertDialogDescriptionProps, [
"class",
]);
return (
<AlertDialogPrimitive.Description
class={cn("text-sm text-muted-foreground", local.class)}
{...rest}
/>
);
};
type alertDialogCloseProps<T extends ValidComponent = "button"> =
AlertDialogCloseButtonProps<T> & {
class?: string;
};
export const AlertDialogClose = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, alertDialogCloseProps<T>>,
) => {
const [local, rest] = splitProps(props as alertDialogCloseProps, ["class"]);
return (
<AlertDialogPrimitive.CloseButton
class={cn(
buttonVariants({
variant: "outline",
}),
"mt-2 md:mt-0",
local.class,
)}
{...rest}
/>
);
};
export const AlertDialogAction = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, alertDialogCloseProps<T>>,
) => {
const [local, rest] = splitProps(props as alertDialogCloseProps, ["class"]);
return (
<AlertDialogPrimitive.CloseButton
class={cn(buttonVariants(), local.class)}
{...rest}
/>
);
};

View file

@ -0,0 +1,66 @@
import { cn } from "../libs/cn";
import type { AlertRootProps } from "@kobalte/core/alert";
import { Alert as AlertPrimitive } from "@kobalte/core/alert";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { ComponentProps, ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
type alertProps<T extends ValidComponent = "div"> = AlertRootProps<T> &
VariantProps<typeof alertVariants> & {
class?: string;
};
export const Alert = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, alertProps<T>>,
) => {
const [local, rest] = splitProps(props as alertProps, ["class", "variant"]);
return (
<AlertPrimitive
class={cn(
alertVariants({
variant: props.variant,
}),
local.class,
)}
{...rest}
/>
);
};
export const AlertTitle = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn("font-medium leading-5 tracking-tight", local.class)}
{...rest}
/>
);
};
export const AlertDescription = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div class={cn("text-sm [&_p]:leading-relaxed", local.class)} {...rest} />
);
};

View file

@ -0,0 +1,10 @@
import { Avatar as ArkAvatar } from "@ark-ui/solid/avatar";
export default function Avatar(props: { img?: string; fallback: string }) {
return (
<ArkAvatar.Root class="m-0 bg-card rounded-full border border-input/75 size-9 aspect-square flex items-center justify-center select-none">
<ArkAvatar.Fallback>{props.fallback}</ArkAvatar.Fallback>
<ArkAvatar.Image class="rounded-full" src={props.img} />
</ArkAvatar.Root>
);
}

View file

@ -0,0 +1,42 @@
import { cn } from "../libs/cn";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import { type ComponentProps, splitProps } from "solid-js";
export const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export const Badge = (
props: ComponentProps<"div"> & VariantProps<typeof badgeVariants>,
) => {
const [local, rest] = splitProps(props, ["class", "variant"]);
return (
<div
class={cn(
badgeVariants({
variant: local.variant,
}),
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,66 @@
import { cn } from "../libs/cn";
import type { ButtonRootProps } from "@kobalte/core/button";
import { Button as ButtonPrimitive } from "@kobalte/core/button";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-[color,background-color,box-shadow] focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border bg-card shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
type buttonProps<T extends ValidComponent = "button"> = ButtonRootProps<T> &
VariantProps<typeof buttonVariants> & {
class?: string;
};
export const Button = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, buttonProps<T>>,
) => {
const [local, rest] = splitProps(props as buttonProps, [
"class",
"variant",
"size",
]);
return (
<ButtonPrimitive
class={cn(
buttonVariants({
size: local.size,
variant: local.variant,
}),
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,60 @@
import { cn } from "../libs/cn";
import type { ComponentProps, ParentComponent } from "solid-js";
import { splitProps } from "solid-js";
export const Card = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"rounded-xl border bg-card text-card-foreground shadow",
local.class,
)}
{...rest}
/>
);
};
export const CardHeader = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div class={cn("flex flex-col space-y-1.5 p-6", local.class)} {...rest} />
);
};
export const CardTitle: ParentComponent<ComponentProps<"h1">> = (props) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<h1
class={cn("font-semibold leading-none tracking-tight", local.class)}
{...rest}
/>
);
};
export const CardDescription: ParentComponent<ComponentProps<"h3">> = (
props,
) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<h3 class={cn("text-sm text-muted-foreground", local.class)} {...rest} />
);
};
export const CardContent = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return <div class={cn("p-6 pt-0", local.class)} {...rest} />;
};
export const CardFooter = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div class={cn("flex items-center p-6 pt-0", local.class)} {...rest} />
);
};

View file

@ -0,0 +1,272 @@
import { cn } from "../libs/cn";
import type { CreateEmblaCarouselType } from "embla-carousel-solid";
import createEmblaCarousel from "embla-carousel-solid";
import type {
Accessor,
ComponentProps,
ParentProps,
VoidProps,
} from "solid-js";
import {
createContext,
createEffect,
createMemo,
createSignal,
mergeProps,
onCleanup,
splitProps,
useContext,
} from "solid-js";
import { Button } from "./button";
export type CarouselApi = CreateEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof createEmblaCarousel>;
type CarouselOptions = NonNullable<UseCarouselParameters[0]>;
type CarouselPlugin = NonNullable<UseCarouselParameters[1]>;
type CarouselProps = {
opts?: ReturnType<CarouselOptions>;
plugins?: ReturnType<CarouselPlugin>;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof createEmblaCarousel>[0];
api: ReturnType<typeof createEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: Accessor<boolean>;
canScrollNext: Accessor<boolean>;
} & CarouselProps;
const CarouselContext = createContext<Accessor<CarouselContextProps> | null>(
null,
);
const useCarousel = () => {
const context = useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context();
};
export const Carousel = (props: ComponentProps<"div"> & CarouselProps) => {
const merge = mergeProps<
ParentProps<ComponentProps<"div"> & CarouselProps>[]
>({ orientation: "horizontal" }, props);
const [local, rest] = splitProps(merge, [
"orientation",
"opts",
"setApi",
"plugins",
"class",
"children",
]);
const [carouselRef, api] = createEmblaCarousel(
() => ({
...local.opts,
axis: local.orientation === "horizontal" ? "x" : "y",
}),
() => (local.plugins === undefined ? [] : local.plugins),
);
const [canScrollPrev, setCanScrollPrev] = createSignal(false);
const [canScrollNext, setCanScrollNext] = createSignal(false);
const onSelect = (api: NonNullable<ReturnType<CarouselApi>>) => {
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
};
const scrollPrev = () => api()?.scrollPrev();
const scrollNext = () => api()?.scrollNext();
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
};
createEffect(() => {
if (!api() || !local.setApi) return;
local.setApi(api);
});
createEffect(() => {
const _api = api();
if (_api === undefined) return;
onSelect(_api);
_api.on("reInit", onSelect);
_api.on("select", onSelect);
onCleanup(() => {
_api.off("select", onSelect);
});
});
const value = createMemo(
() =>
({
carouselRef,
api,
opts: local.opts,
orientation:
local.orientation ||
(local.opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}) satisfies CarouselContextProps,
);
return (
<CarouselContext.Provider value={value}>
<div
onKeyDown={handleKeyDown}
class={cn("relative", local.class)}
role="region"
aria-roledescription="carousel"
{...rest}
>
{local.children}
</div>
</CarouselContext.Provider>
);
};
export const CarouselContent = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} class="overflow-hidden">
<div
class={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
local.class,
)}
{...rest}
/>
</div>
);
};
export const CarouselItem = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
class={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
local.class,
)}
{...rest}
/>
);
};
export const CarouselPrevious = (
props: VoidProps<ComponentProps<typeof Button>>,
) => {
const merge = mergeProps<VoidProps<ComponentProps<typeof Button>[]>>(
{ variant: "outline", size: "icon" },
props,
);
const [local, rest] = splitProps(merge, ["class", "variant", "size"]);
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
variant={local.variant}
size={local.size}
class={cn(
"absolute h-8 w-8 touch-manipulation rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
local.class,
)}
disabled={!canScrollPrev()}
onClick={scrollPrev}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="size-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 12h14M5 12l6 6m-6-6l6-6"
/>
<title>Previous slide</title>
</svg>
</Button>
);
};
export const CarouselNext = (
props: VoidProps<ComponentProps<typeof Button>>,
) => {
const merge = mergeProps<VoidProps<ComponentProps<typeof Button>[]>>(
{ variant: "outline", size: "icon" },
props,
);
const [local, rest] = splitProps(merge, ["class", "variant", "size"]);
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
variant={local.variant}
size={local.size}
class={cn(
"absolute h-8 w-8 touch-manipulation rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
local.class,
)}
disabled={!canScrollNext()}
onClick={scrollNext}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="size-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 12h14m-4 4l4-4m-4-4l4 4"
/>
<title>Next slide</title>
</svg>
</Button>
);
};

View file

@ -0,0 +1,55 @@
import { cn } from "../libs/cn";
import type { CheckboxControlProps } from "@kobalte/core/checkbox";
import { Checkbox as CheckboxPrimitive } from "@kobalte/core/checkbox";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
export const CheckboxLabel = CheckboxPrimitive.Label;
export const Checkbox = CheckboxPrimitive;
export const CheckboxErrorMessage = CheckboxPrimitive.ErrorMessage;
export const CheckboxDescription = CheckboxPrimitive.Description;
type checkboxControlProps<T extends ValidComponent = "div"> = VoidProps<
CheckboxControlProps<T> & { class?: string }
>;
export const CheckboxControl = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, checkboxControlProps<T>>,
) => {
const [local, rest] = splitProps(props as checkboxControlProps, [
"class",
"children",
]);
return (
<>
<CheckboxPrimitive.Input class="[&:focus-visible+div]:outline-none [&:focus-visible+div]:ring-[1.5px] [&:focus-visible+div]:ring-ring [&:focus-visible+div]:ring-offset-2 [&:focus-visible+div]:ring-offset-background" />
<CheckboxPrimitive.Control
class={cn(
"h-4 w-4 shrink-0 rounded-sm border border-primary shadow transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring data-[disabled]:cursor-not-allowed data-[checked]:bg-primary data-[checked]:text-primary-foreground data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<CheckboxPrimitive.Indicator class="flex items-center justify-center text-current">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m5 12l5 5L20 7"
/>
<title>Checkbox</title>
</svg>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Control>
</>
);
};

View file

@ -0,0 +1,31 @@
import { cn } from "../libs/cn";
import type { CollapsibleContentProps } from "@kobalte/core/collapsible";
import { Collapsible as CollapsiblePrimitive } from "@kobalte/core/collapsible";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const Collapsible = CollapsiblePrimitive;
export const CollapsibleTrigger = CollapsiblePrimitive.Trigger;
type collapsibleContentProps<T extends ValidComponent = "div"> =
CollapsibleContentProps<T> & {
class?: string;
};
export const CollapsibleContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, collapsibleContentProps<T>>,
) => {
const [local, rest] = splitProps(props as collapsibleContentProps, ["class"]);
return (
<CollapsiblePrimitive.Content
class={cn(
"animate-collapsible-up data-[expanded]:animate-collapsible-down",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,156 @@
import { cn } from "../libs/cn";
import type {
ComboboxContentProps,
ComboboxInputProps,
ComboboxItemProps,
ComboboxTriggerProps,
} from "@kobalte/core/combobox";
import { Combobox as ComboboxPrimitive } from "@kobalte/core/combobox";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ParentProps, ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
export const Combobox = ComboboxPrimitive;
export const ComboboxDescription = ComboboxPrimitive.Description;
export const ComboboxErrorMessage = ComboboxPrimitive.ErrorMessage;
export const ComboboxItemDescription = ComboboxPrimitive.ItemDescription;
export const ComboboxHiddenSelect = ComboboxPrimitive.HiddenSelect;
type comboboxInputProps<T extends ValidComponent = "input"> = VoidProps<
ComboboxInputProps<T> & {
class?: string;
}
>;
export const ComboboxInput = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, comboboxInputProps<T>>,
) => {
const [local, rest] = splitProps(props as comboboxInputProps, ["class"]);
return (
<ComboboxPrimitive.Input
class={cn(
"h-full bg-transparent text-sm placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
local.class,
)}
{...rest}
/>
);
};
type comboboxTriggerProps<T extends ValidComponent = "button"> = ParentProps<
ComboboxTriggerProps<T> & {
class?: string;
}
>;
export const ComboboxTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, comboboxTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as comboboxTriggerProps, [
"class",
"children",
]);
return (
<ComboboxPrimitive.Control>
<ComboboxPrimitive.Trigger
class={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input px-3 shadow-sm",
local.class,
)}
{...rest}
>
{local.children}
<ComboboxPrimitive.Icon class="flex h-3.5 w-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4 opacity-50"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m8 9l4-4l4 4m0 6l-4 4l-4-4"
/>
<title>Arrow</title>
</svg>
</ComboboxPrimitive.Icon>
</ComboboxPrimitive.Trigger>
</ComboboxPrimitive.Control>
);
};
type comboboxContentProps<T extends ValidComponent = "div"> =
ComboboxContentProps<T> & {
class?: string;
};
export const ComboboxContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, comboboxContentProps<T>>,
) => {
const [local, rest] = splitProps(props as comboboxContentProps, ["class"]);
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Content
class={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95 origin-[--kb-combobox-content-transform-origin]",
local.class,
)}
{...rest}
>
<ComboboxPrimitive.Listbox class="p-1" />
</ComboboxPrimitive.Content>
</ComboboxPrimitive.Portal>
);
};
type comboboxItemProps<T extends ValidComponent = "li"> = ParentProps<
ComboboxItemProps<T> & {
class?: string;
}
>;
export const ComboboxItem = <T extends ValidComponent = "li">(
props: PolymorphicProps<T, comboboxItemProps<T>>,
) => {
const [local, rest] = splitProps(props as comboboxItemProps, [
"class",
"children",
]);
return (
<ComboboxPrimitive.Item
class={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<ComboboxPrimitive.ItemIndicator class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m5 12l5 5L20 7"
/>
<title>Checked</title>
</svg>
</ComboboxPrimitive.ItemIndicator>
<ComboboxPrimitive.ItemLabel>
{local.children}
</ComboboxPrimitive.ItemLabel>
</ComboboxPrimitive.Item>
);
};

View file

@ -0,0 +1,151 @@
import { cn } from "../libs/cn";
import type {
CommandDialogProps,
CommandEmptyProps,
CommandGroupProps,
CommandInputProps,
CommandItemProps,
CommandListProps,
CommandRootProps,
} from "cmdk-solid";
import { Command as CommandPrimitive } from "cmdk-solid";
import type { ComponentProps, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
import { Dialog, DialogContent } from "./dialog";
export const Command = (props: CommandRootProps) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<CommandPrimitive
class={cn(
"flex size-full flex-col overflow-hidden bg-popover text-popover-foreground",
local.class,
)}
{...rest}
/>
);
};
export const CommandList = (props: CommandListProps) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<CommandPrimitive.List
class={cn(
"max-h-[300px] overflow-y-auto overflow-x-hidden p-1",
local.class,
)}
{...rest}
/>
);
};
export const CommandInput = (props: VoidProps<CommandInputProps>) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div class="flex items-center border-b px-3" cmdk-input-wrapper="">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="mr-2 h-4 w-4 shrink-0 opacity-50"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0m18 11l-6-6"
/>
<title>Search</title>
</svg>
<CommandPrimitive.Input
class={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
local.class,
)}
{...rest}
/>
</div>
);
};
export const CommandItem = (props: CommandItemProps) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<CommandPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground",
local.class,
)}
{...rest}
/>
);
};
export const CommandShortcut = (props: ComponentProps<"span">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<span
class={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
local.class,
)}
{...rest}
/>
);
};
export const CommandDialog = (props: CommandDialogProps) => {
const [local, rest] = splitProps(props, ["children"]);
return (
<Dialog {...rest}>
<DialogContent class="overflow-hidden p-0">
<Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5">
{local.children}
</Command>
</DialogContent>
</Dialog>
);
};
export const CommandEmpty = (props: CommandEmptyProps) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<CommandPrimitive.Empty
class={cn("py-6 text-center text-sm", local.class)}
{...rest}
/>
);
};
export const CommandGroup = (props: CommandGroupProps) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<CommandPrimitive.Group
class={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
local.class,
)}
{...rest}
/>
);
};
export const CommandSeparator = (props: CommandEmptyProps) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<CommandPrimitive.Separator
class={cn("-mx-1 h-px bg-border", local.class)}
{...rest}
/>
);
};

View file

@ -0,0 +1,327 @@
import { cn } from "../libs/cn";
import type {
ContextMenuCheckboxItemProps,
ContextMenuContentProps,
ContextMenuGroupLabelProps,
ContextMenuItemLabelProps,
ContextMenuItemProps,
ContextMenuRadioItemProps,
ContextMenuSeparatorProps,
ContextMenuSubContentProps,
ContextMenuSubTriggerProps,
} from "@kobalte/core/context-menu";
import { ContextMenu as ContextMenuPrimitive } from "@kobalte/core/context-menu";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
ComponentProps,
ParentProps,
ValidComponent,
VoidProps,
} from "solid-js";
import { splitProps } from "solid-js";
export const ContextMenu = ContextMenuPrimitive;
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
export const ContextMenuGroup = ContextMenuPrimitive.Group;
export const ContextMenuSub = ContextMenuPrimitive.Sub;
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
type contextMenuSubTriggerProps<T extends ValidComponent = "div"> = ParentProps<
ContextMenuSubTriggerProps<T> & {
class?: string;
inset?: boolean;
}
>;
export const ContextMenuSubTrigger = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, contextMenuSubTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuSubTriggerProps, [
"class",
"children",
"inset",
]);
return (
<ContextMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[expanded]:bg-accent data-[expanded]:text-accent-foreground",
local.inset && "pl-8",
local.class,
)}
{...rest}
>
{local.children}
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-auto h-4 w-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 6l6 6l-6 6"
/>
<title>Arrow</title>
</svg>
</ContextMenuPrimitive.SubTrigger>
);
};
type contextMenuSubContentProps<T extends ValidComponent = "div"> =
ContextMenuSubContentProps<T> & {
class?: string;
};
export const ContextMenuSubContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, contextMenuSubContentProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuSubContentProps, [
"class",
]);
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.SubContent
class={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
/>
</ContextMenuPrimitive.Portal>
);
};
type contextMenuContentProps<T extends ValidComponent = "div"> =
ContextMenuContentProps<T> & {
class?: string;
};
export const ContextMenuContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, contextMenuContentProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuContentProps, ["class"]);
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
class={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
/>
</ContextMenuPrimitive.Portal>
);
};
type contextMenuItemProps<T extends ValidComponent = "div"> =
ContextMenuItemProps<T> & {
class?: string;
inset?: boolean;
};
export const ContextMenuItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, contextMenuItemProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuItemProps, [
"class",
"inset",
]);
return (
<ContextMenuPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.inset && "pl-8",
local.class,
)}
{...rest}
/>
);
};
type contextMenuCheckboxItemProps<T extends ValidComponent = "div"> =
ParentProps<
ContextMenuCheckboxItemProps<T> & {
class?: string;
}
>;
export const ContextMenuCheckboxItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, contextMenuCheckboxItemProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuCheckboxItemProps, [
"class",
"children",
]);
return (
<ContextMenuPrimitive.CheckboxItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<ContextMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-3.5 w-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m5 12l5 5L20 7"
/>
<title>Checkbox</title>
</svg>
</ContextMenuPrimitive.ItemIndicator>
{local.children}
</ContextMenuPrimitive.CheckboxItem>
);
};
type contextMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
ContextMenuRadioItemProps<T> & {
class?: string;
}
>;
export const ContextMenuRadioItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, contextMenuRadioItemProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuRadioItemProps, [
"class",
"children",
]);
return (
<ContextMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<ContextMenuPrimitive.ItemIndicator class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-2 w-2"
>
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
/>
</g>
<title>Radio</title>
</svg>
</ContextMenuPrimitive.ItemIndicator>
{local.children}
</ContextMenuPrimitive.RadioItem>
);
};
type contextMenuItemLabelProps<T extends ValidComponent = "div"> =
ContextMenuItemLabelProps<T> & {
class?: string;
inset?: boolean;
};
export const ContextMenuItemLabel = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, contextMenuItemLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuItemLabelProps, [
"class",
"inset",
]);
return (
<ContextMenuPrimitive.ItemLabel
class={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
local.inset && "pl-8",
local.class,
)}
{...rest}
/>
);
};
type contextMenuGroupLabelProps<T extends ValidComponent = "span"> =
ContextMenuGroupLabelProps<T> & {
class?: string;
inset?: boolean;
};
export const ContextMenuGroupLabel = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, contextMenuGroupLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuGroupLabelProps, [
"class",
"inset",
]);
return (
<ContextMenuPrimitive.GroupLabel
as="div"
class={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
local.inset && "pl-8",
local.class,
)}
{...rest}
/>
);
};
type contextMenuSeparatorProps<T extends ValidComponent = "hr"> = VoidProps<
ContextMenuSeparatorProps<T> & {
class?: string;
}
>;
export const ContextMenuSeparator = <T extends ValidComponent = "hr">(
props: PolymorphicProps<T, contextMenuSeparatorProps<T>>,
) => {
const [local, rest] = splitProps(props as contextMenuSeparatorProps, [
"class",
]);
return (
<ContextMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-border", local.class)}
{...rest}
/>
);
};
export const ContextMenuShortcut = (props: ComponentProps<"span">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<span
class={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,128 @@
import { cn } from "../libs/cn";
import type {
DialogContentProps,
DialogDescriptionProps,
DialogTitleProps,
} from "@kobalte/core/dialog";
import { Dialog as DialogPrimitive } from "@kobalte/core/dialog";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const Dialog = DialogPrimitive;
export const DialogTrigger = DialogPrimitive.Trigger;
type dialogContentProps<T extends ValidComponent = "div"> = ParentProps<
DialogContentProps<T> & {
class?: string;
}
>;
export const DialogContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dialogContentProps<T>>,
) => {
const [local, rest] = splitProps(props as dialogContentProps, [
"class",
"children",
]);
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
class={cn(
"fixed inset-0 z-50 bg-background/80 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
)}
{...rest}
/>
<DialogPrimitive.Content
class={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg data-[closed]:duration-200 data-[expanded]:duration-200 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95 data-[closed]:slide-out-to-left-1/2 data-[closed]:slide-out-to-top-[48%] data-[expanded]:slide-in-from-left-1/2 data-[expanded]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
local.class,
)}
{...rest}
>
{local.children}
<DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-[opacity,box-shadow] hover:opacity-100 focus:outline-none focus:ring-[1.5px] focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12"
/>
<title>Close</title>
</svg>
</DialogPrimitive.CloseButton>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
};
type dialogTitleProps<T extends ValidComponent = "h2"> = DialogTitleProps<T> & {
class?: string;
};
export const DialogTitle = <T extends ValidComponent = "h2">(
props: PolymorphicProps<T, dialogTitleProps<T>>,
) => {
const [local, rest] = splitProps(props as dialogTitleProps, ["class"]);
return (
<DialogPrimitive.Title
class={cn("text-lg font-semibold text-foreground", local.class)}
{...rest}
/>
);
};
type dialogDescriptionProps<T extends ValidComponent = "p"> =
DialogDescriptionProps<T> & {
class?: string;
};
export const DialogDescription = <T extends ValidComponent = "p">(
props: PolymorphicProps<T, dialogDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as dialogDescriptionProps, ["class"]);
return (
<DialogPrimitive.Description
class={cn("text-sm text-muted-foreground", local.class)}
{...rest}
/>
);
};
export const DialogHeader = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"flex flex-col space-y-2 text-center sm:text-left",
local.class,
)}
{...rest}
/>
);
};
export const DialogFooter = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,107 @@
import { cn } from "../libs/cn";
import type {
ContentProps,
DescriptionProps,
DynamicProps,
LabelProps,
} from "@corvu/drawer";
import DrawerPrimitive from "@corvu/drawer";
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const Drawer = DrawerPrimitive;
export const DrawerTrigger = DrawerPrimitive.Trigger;
export const DrawerClose = DrawerPrimitive.Close;
type drawerContentProps<T extends ValidComponent = "div"> = ParentProps<
ContentProps<T> & {
class?: string;
}
>;
export const DrawerContent = <T extends ValidComponent = "div">(
props: DynamicProps<T, drawerContentProps<T>>,
) => {
const [local, rest] = splitProps(props as drawerContentProps, [
"class",
"children",
]);
const ctx = DrawerPrimitive.useContext();
return (
<DrawerPrimitive.Portal>
<DrawerPrimitive.Overlay
class="fixed inset-0 z-50 data-[transitioning]:transition-colors data-[transitioning]:duration-200"
style={{
"background-color": `hsl(var(--background) / ${0.8 * ctx.openPercentage()})`,
}}
/>
<DrawerPrimitive.Content
class={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-xl border bg-background after:absolute after:inset-x-0 after:top-full after:h-[50%] after:bg-inherit data-[transitioning]:transition-transform data-[transitioning]:duration-200 md:select-none",
local.class,
)}
{...rest}
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{local.children}
</DrawerPrimitive.Content>
</DrawerPrimitive.Portal>
);
};
export const DrawerHeader = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn("grid gap-1.5 p-4 text-center sm:text-left", local.class)}
{...rest}
/>
);
};
export const DrawerFooter = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div class={cn("mt-auto flex flex-col gap-2 p-4", local.class)} {...rest} />
);
};
type DrawerLabelProps = LabelProps & {
class?: string;
};
export const DrawerLabel = <T extends ValidComponent = "h2">(
props: DynamicProps<T, DrawerLabelProps>,
) => {
const [local, rest] = splitProps(props as DrawerLabelProps, ["class"]);
return (
<DrawerPrimitive.Label
class={cn(
"text-lg font-semibold leading-none tracking-tight",
local.class,
)}
{...rest}
/>
);
};
type DrawerDescriptionProps = DescriptionProps & {
class?: string;
};
export const DrawerDescription = <T extends ValidComponent = "p">(
props: DynamicProps<T, DrawerDescriptionProps>,
) => {
const [local, rest] = splitProps(props as DrawerDescriptionProps, ["class"]);
return (
<DrawerPrimitive.Description
class={cn("text-sm text-muted-foreground", local.class)}
{...rest}
/>
);
};

View file

@ -0,0 +1,320 @@
import { cn } from "../libs/cn";
import type {
DropdownMenuCheckboxItemProps,
DropdownMenuContentProps,
DropdownMenuGroupLabelProps,
DropdownMenuItemLabelProps,
DropdownMenuItemProps,
DropdownMenuRadioItemProps,
DropdownMenuRootProps,
DropdownMenuSeparatorProps,
DropdownMenuSubTriggerProps,
} from "@kobalte/core/dropdown-menu";
import { DropdownMenu as DropdownMenuPrimitive } from "@kobalte/core/dropdown-menu";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
import { mergeProps, splitProps } from "solid-js";
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
export const DropdownMenu = (props: DropdownMenuRootProps) => {
const merge = mergeProps<DropdownMenuRootProps[]>(
{
gutter: 4,
flip: false,
},
props,
);
return <DropdownMenuPrimitive {...merge} />;
};
type dropdownMenuContentProps<T extends ValidComponent = "div"> =
DropdownMenuContentProps<T> & {
class?: string;
};
export const DropdownMenuContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuContentProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuContentProps, [
"class",
]);
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
class={cn(
"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
/>
</DropdownMenuPrimitive.Portal>
);
};
type dropdownMenuItemProps<T extends ValidComponent = "div"> =
DropdownMenuItemProps<T> & {
class?: string;
inset?: boolean;
};
export const DropdownMenuItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuItemProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuItemProps, [
"class",
"inset",
]);
return (
<DropdownMenuPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.inset && "pl-8",
local.class,
)}
{...rest}
/>
);
};
type dropdownMenuGroupLabelProps<T extends ValidComponent = "span"> =
DropdownMenuGroupLabelProps<T> & {
class?: string;
};
export const DropdownMenuGroupLabel = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [
"class",
]);
return (
<DropdownMenuPrimitive.GroupLabel
as="div"
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)}
{...rest}
/>
);
};
type dropdownMenuItemLabelProps<T extends ValidComponent = "div"> =
DropdownMenuItemLabelProps<T> & {
class?: string;
};
export const DropdownMenuItemLabel = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [
"class",
]);
return (
<DropdownMenuPrimitive.ItemLabel
as="div"
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)}
{...rest}
/>
);
};
type dropdownMenuSeparatorProps<T extends ValidComponent = "hr"> =
DropdownMenuSeparatorProps<T> & {
class?: string;
};
export const DropdownMenuSeparator = <T extends ValidComponent = "hr">(
props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [
"class",
]);
return (
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", local.class)}
{...rest}
/>
);
};
export const DropdownMenuShortcut = (props: ComponentProps<"span">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<span
class={cn("ml-auto text-xs tracking-widest opacity-60", local.class)}
{...rest}
/>
);
};
type dropdownMenuSubTriggerProps<T extends ValidComponent = "div"> =
ParentProps<
DropdownMenuSubTriggerProps<T> & {
class?: string;
}
>;
export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [
"class",
"children",
]);
return (
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[expanded]:bg-accent",
local.class,
)}
{...rest}
>
{local.children}
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
class="ml-auto h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 6l6 6l-6 6"
/>
<title>Arrow</title>
</svg>
</DropdownMenuPrimitive.SubTrigger>
);
};
type dropdownMenuSubContentProps<T extends ValidComponent = "div"> =
DropdownMenuSubTriggerProps<T> & {
class?: string;
};
export const DropdownMenuSubContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [
"class",
]);
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.SubContent
class={cn(
"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
/>
</DropdownMenuPrimitive.Portal>
);
};
type dropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> =
ParentProps<
DropdownMenuCheckboxItemProps<T> & {
class?: string;
}
>;
export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [
"class",
"children",
]);
return (
<DropdownMenuPrimitive.CheckboxItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m5 12l5 5L20 7"
/>
<title>Checkbox</title>
</svg>
</DropdownMenuPrimitive.ItemIndicator>
{props.children}
</DropdownMenuPrimitive.CheckboxItem>
);
};
type dropdownMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
DropdownMenuRadioItemProps<T> & {
class?: string;
}
>;
export const DropdownMenuRadioItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>,
) => {
const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [
"class",
"children",
]);
return (
<DropdownMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-2 w-2"
>
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
/>
</g>
<title>Radio</title>
</svg>
</DropdownMenuPrimitive.ItemIndicator>
{props.children}
</DropdownMenuPrimitive.RadioItem>
);
};

View file

@ -0,0 +1,32 @@
import { cn } from "../libs/cn";
import type { HoverCardContentProps } from "@kobalte/core/hover-card";
import { HoverCard as HoverCardPrimitive } from "@kobalte/core/hover-card";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const HoverCard = HoverCardPrimitive;
export const HoverCardTrigger = HoverCardPrimitive.Trigger;
type hoverCardContentProps<T extends ValidComponent = "div"> =
HoverCardContentProps<T> & {
class?: string;
};
export const HoverCardContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, hoverCardContentProps<T>>,
) => {
const [local, rest] = splitProps(props as hoverCardContentProps, ["class"]);
return (
<HoverCardPrimitive.Portal>
<HoverCardPrimitive.Content
class={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
/>
</HoverCardPrimitive.Portal>
);
};

View file

@ -0,0 +1,68 @@
import { cn } from "../libs/cn";
import type {
ImageFallbackProps,
ImageImgProps,
ImageRootProps,
} from "@kobalte/core/image";
import { Image as ImagePrimitive } from "@kobalte/core/image";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
type imageRootProps<T extends ValidComponent = "span"> = ImageRootProps<T> & {
class?: string;
};
export const ImageRoot = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, imageRootProps<T>>,
) => {
const [local, rest] = splitProps(props as imageRootProps, ["class"]);
return (
<ImagePrimitive
class={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
local.class,
)}
{...rest}
/>
);
};
type imageProps<T extends ValidComponent = "img"> = ImageImgProps<T> & {
class?: string;
};
export const Image = <T extends ValidComponent = "img">(
props: PolymorphicProps<T, imageProps<T>>,
) => {
const [local, rest] = splitProps(props as imageProps, ["class"]);
return (
<ImagePrimitive.Img
class={cn("aspect-square h-full w-full", local.class)}
{...rest}
/>
);
};
type imageFallbackProps<T extends ValidComponent = "span"> =
ImageFallbackProps<T> & {
class?: string;
};
export const ImageFallback = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, imageFallbackProps<T>>,
) => {
const [local, rest] = splitProps(props as imageFallbackProps, ["class"]);
return (
<ImagePrimitive.Fallback
class={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,27 @@
import { splitProps, type JSX } from "solid-js";
import { cn } from "../libs/cn";
interface InputProps extends JSX.HTMLAttributes<HTMLInputElement> {
type?: string;
}
function Input(props: InputProps) {
const [local, others] = splitProps(props, ["class", "type"]);
return (
<input
type={local.type}
data-slot="input"
class={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base 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 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-input focus-visible:dark:bg-input/50 transition-all duration-200 ease-in-out",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
local.class,
)}
{...others}
/>
);
}
export { Input };

View file

@ -0,0 +1,20 @@
import { cn } from "../libs/cn";
import type { Component, JSX } from "solid-js";
import { splitProps } from "solid-js";
const Label: Component<JSX.LabelHTMLAttributes<HTMLLabelElement>> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<label
data-slot="label"
class={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 transition-all duration-200 ease-in-out",
local.class,
)}
{...others}
/>
);
};
export { Label };

View file

@ -0,0 +1,356 @@
import { cn } from "../libs/cn";
import type {
MenubarCheckboxItemProps,
MenubarContentProps,
MenubarItemLabelProps,
MenubarItemProps,
MenubarMenuProps,
MenubarRadioItemProps,
MenubarRootProps,
MenubarSeparatorProps,
MenubarSubContentProps,
MenubarSubTriggerProps,
MenubarTriggerProps,
} from "@kobalte/core/menubar";
import { Menubar as MenubarPrimitive } from "@kobalte/core/menubar";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
import { mergeProps, splitProps } from "solid-js";
export const MenubarSub = MenubarPrimitive.Sub;
export const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
type menubarProps<T extends ValidComponent = "div"> = MenubarRootProps<T> & {
class?: string;
};
export const Menubar = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, menubarProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarProps, ["class"]);
return (
<MenubarPrimitive
class={cn(
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
local.class,
)}
{...rest}
/>
);
};
export const MenubarMenu = (props: MenubarMenuProps) => {
const merge = mergeProps<MenubarMenuProps[]>(
{
gutter: 8,
shift: -4,
flip: false,
},
props,
);
return <MenubarPrimitive.Menu {...merge} />;
};
type menubarTriggerProps<T extends ValidComponent = "button"> =
MenubarTriggerProps<T> & {
class?: string;
};
export const MenubarTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, menubarTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarTriggerProps, ["class"]);
return (
<MenubarPrimitive.Trigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[expanded]:bg-accent data-[expanded]:text-accent-foreground",
local.class,
)}
{...rest}
/>
);
};
type menubarSubTriggerProps<T extends ValidComponent = "button"> = ParentProps<
MenubarSubTriggerProps<T> & {
class?: string;
inset?: boolean;
}
>;
export const MenubarSubTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, menubarSubTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarSubTriggerProps, [
"class",
"children",
"inset",
]);
return (
<MenubarPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[expanded]:bg-accent data-[expanded]:text-accent-foreground",
local.inset && "pl-8",
local.class,
)}
{...rest}
>
{local.children}
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
class="ml-auto h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 6l6 6l-6 6"
/>
<title>Arrow</title>
</svg>
</MenubarPrimitive.SubTrigger>
);
};
type menubarSubContentProps<T extends ValidComponent = "div"> = ParentProps<
MenubarSubContentProps<T> & {
class?: string;
}
>;
export const MenubarSubContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, menubarSubContentProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarSubContentProps, [
"class",
"children",
]);
return (
<MenubarPrimitive.Portal>
<MenubarPrimitive.SubContent
class={cn(
"z-50 min-w-[8rem] origin-[--kb-menu-content-transform-origin] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
>
{local.children}
</MenubarPrimitive.SubContent>
</MenubarPrimitive.Portal>
);
};
type menubarContentProps<T extends ValidComponent = "div"> = ParentProps<
MenubarContentProps<T> & {
class?: string;
}
>;
export const MenubarContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, menubarContentProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarContentProps, [
"class",
"children",
]);
return (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
class={cn(
"z-50 min-w-[12rem] origin-[--kb-menu-content-transform-origin] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none data-[expanded]:animate-in data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
>
{local.children}
</MenubarPrimitive.Content>
</MenubarPrimitive.Portal>
);
};
type menubarItemProps<T extends ValidComponent = "div"> =
MenubarItemProps<T> & {
class?: string;
inset?: boolean;
};
export const MenubarItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, menubarItemProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarItemProps, [
"class",
"inset",
]);
return (
<MenubarPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.inset && "pl-8",
local.class,
)}
{...rest}
/>
);
};
type menubarItemLabelProps<T extends ValidComponent = "div"> =
MenubarItemLabelProps<T> & {
class?: string;
inset?: boolean;
};
export const MenubarItemLabel = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, menubarItemLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarItemLabelProps, [
"class",
"inset",
]);
return (
<MenubarPrimitive.ItemLabel
class={cn(
"px-2 py-1.5 text-sm font-semibold",
local.inset && "pl-8",
local.class,
)}
{...rest}
/>
);
};
type menubarSeparatorProps<T extends ValidComponent = "hr"> =
MenubarSeparatorProps<T> & {
class?: string;
};
export const MenubarSeparator = <T extends ValidComponent = "hr">(
props: PolymorphicProps<T, menubarSeparatorProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarSeparatorProps, ["class"]);
return (
<MenubarPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", local.class)}
{...rest}
/>
);
};
type menubarCheckboxItemProps<T extends ValidComponent = "div"> = ParentProps<
MenubarCheckboxItemProps<T> & {
class?: string;
}
>;
export const MenubarCheckboxItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, menubarCheckboxItemProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarCheckboxItemProps, [
"class",
"children",
]);
return (
<MenubarPrimitive.CheckboxItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<MenubarPrimitive.ItemIndicator class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m5 12l5 5L20 7"
/>
<title>Checkbox</title>
</svg>
</MenubarPrimitive.ItemIndicator>
{local.children}
</MenubarPrimitive.CheckboxItem>
);
};
type menubarRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
MenubarRadioItemProps<T> & {
class?: string;
}
>;
export const MenubarRadioItem = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, menubarRadioItemProps<T>>,
) => {
const [local, rest] = splitProps(props as menubarRadioItemProps, [
"class",
"children",
]);
return (
<MenubarPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<MenubarPrimitive.ItemIndicator class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-2 w-2"
>
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
/>
</g>
<title>Radio</title>
</svg>
</MenubarPrimitive.ItemIndicator>
{local.children}
</MenubarPrimitive.RadioItem>
);
};
export const MenubarShortcut = (props: ComponentProps<"span">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<span
class={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,169 @@
import { cn } from "../libs/cn";
import type {
NavigationMenuContentProps,
NavigationMenuRootProps,
NavigationMenuTriggerProps,
} from "@kobalte/core/navigation-menu";
import { NavigationMenu as NavigationMenuPrimitive } from "@kobalte/core/navigation-menu";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import {
type ParentProps,
Show,
type ValidComponent,
mergeProps,
splitProps,
} from "solid-js";
export const NavigationMenuItem = NavigationMenuPrimitive.Menu;
export const NavigationMenuLink = NavigationMenuPrimitive.Item;
export const NavigationMenuItemLabel = NavigationMenuPrimitive.ItemLabel;
export const NavigationMenuDescription =
NavigationMenuPrimitive.ItemDescription;
export const NavigationMenuItemIndicator =
NavigationMenuPrimitive.ItemIndicator;
export const NavigationMenuSub = NavigationMenuPrimitive.Sub;
export const NavigationMenuSubTrigger = NavigationMenuPrimitive.SubTrigger;
export const NavigationMenuSubContent = NavigationMenuPrimitive.SubContent;
export const NavigationMenuRadioGroup = NavigationMenuPrimitive.RadioGroup;
export const NavigationMenuRadioItem = NavigationMenuPrimitive.RadioItem;
export const NavigationMenuCheckboxItem = NavigationMenuPrimitive.CheckboxItem;
export const NavigationMenuSeparator = NavigationMenuPrimitive.Separator;
type withArrow = {
withArrow?: boolean;
};
type navigationMenuProps<T extends ValidComponent = "ul"> = ParentProps<
NavigationMenuRootProps<T> &
withArrow & {
class?: string;
}
>;
export const NavigationMenu = <T extends ValidComponent = "ul">(
props: PolymorphicProps<T, navigationMenuProps<T>>,
) => {
const merge = mergeProps<navigationMenuProps<T>[]>(
{
get gutter() {
return props.withArrow ? props.gutter : 6;
},
withArrow: false,
flip: false,
},
props,
);
const [local, rest] = splitProps(merge as navigationMenuProps, [
"class",
"children",
"withArrow",
]);
return (
<NavigationMenuPrimitive
class={cn("flex w-max items-center justify-center gap-x-1", local.class)}
{...rest}
>
{local.children}
<NavigationMenuPrimitive.Viewport
class={cn(
"pointer-events-none z-50 overflow-x-clip overflow-y-visible rounded-md border bg-popover text-popover-foreground shadow",
"h-[--kb-navigation-menu-viewport-height] w-[--kb-navigation-menu-viewport-width] transition-[width,height] duration-300",
"origin-[--kb-menu-content-transform-origin]",
"data-[expanded]:duration-300 data-[expanded]:animate-in data-[expanded]:fade-in data-[expanded]:zoom-in-95",
"data-[closed]:duration-300 data-[closed]:animate-out data-[closed]:fade-out data-[closed]:zoom-out-95",
)}
>
<Show when={local.withArrow}>
<NavigationMenuPrimitive.Arrow class="transition-transform duration-300" />
</Show>
</NavigationMenuPrimitive.Viewport>
</NavigationMenuPrimitive>
);
};
type navigationMenuTriggerProps<T extends ValidComponent = "button"> =
ParentProps<
NavigationMenuTriggerProps<T> &
withArrow & {
class?: string;
}
>;
export const NavigationMenuTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, navigationMenuTriggerProps<T>>,
) => {
const merge = mergeProps<navigationMenuTriggerProps<T>[]>(
{
get withArrow() {
return props.as === undefined ? true : props.withArrow;
},
},
props,
);
const [local, rest] = splitProps(merge as navigationMenuTriggerProps, [
"class",
"children",
"withArrow",
]);
return (
<NavigationMenuPrimitive.Trigger
class={cn(
"inline-flex w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium outline-none transition-colors duration-300 hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50",
local.class,
)}
{...rest}
>
{local.children}
<Show when={local.withArrow}>
<NavigationMenuPrimitive.Icon
class="ml-1 size-3 transition-transform duration-300 data-[expanded]:rotate-180"
as="svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m6 9l6 6l6-6"
/>
</NavigationMenuPrimitive.Icon>
</Show>
</NavigationMenuPrimitive.Trigger>
);
};
type navigationMenuContentProps<T extends ValidComponent = "ul"> = ParentProps<
NavigationMenuContentProps<T> & {
class?: string;
}
>;
export const NavigationMenuContent = <T extends ValidComponent = "ul">(
props: PolymorphicProps<T, navigationMenuContentProps<T>>,
) => {
const [local, rest] = splitProps(props as navigationMenuContentProps, [
"class",
"children",
]);
return (
<NavigationMenuPrimitive.Portal>
<NavigationMenuPrimitive.Content
class={cn(
"absolute left-0 top-0 p-4 outline-none",
"data-[motion^=from-]:duration-300 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52",
"data-[motion^=to-]:duration-300 data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52",
local.class,
)}
{...rest}
>
{local.children}
</NavigationMenuPrimitive.Content>
</NavigationMenuPrimitive.Portal>
);
};

View file

@ -0,0 +1,214 @@
import { cn } from "../libs/cn";
import type {
NumberFieldDecrementTriggerProps,
NumberFieldDescriptionProps,
NumberFieldErrorMessageProps,
NumberFieldIncrementTriggerProps,
NumberFieldInputProps,
NumberFieldLabelProps,
NumberFieldRootProps,
} from "@kobalte/core/number-field";
import { NumberField as NumberFieldPrimitive } from "@kobalte/core/number-field";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ComponentProps, ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
import { textfieldLabel } from "./textfield";
export const NumberFieldHiddenInput = NumberFieldPrimitive.HiddenInput;
type numberFieldLabelProps<T extends ValidComponent = "div"> =
NumberFieldLabelProps<T> & {
class?: string;
};
export const NumberFieldLabel = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, numberFieldLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldLabelProps, ["class"]);
return (
<NumberFieldPrimitive.Label
class={cn(textfieldLabel({ label: true }), local.class)}
{...rest}
/>
);
};
type numberFieldDescriptionProps<T extends ValidComponent = "div"> =
NumberFieldDescriptionProps<T> & {
class?: string;
};
export const NumberFieldDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, numberFieldDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldDescriptionProps, [
"class",
]);
return (
<NumberFieldPrimitive.Description
class={cn(
textfieldLabel({ description: true, label: false }),
local.class,
)}
{...rest}
/>
);
};
type numberFieldErrorMessageProps<T extends ValidComponent = "div"> =
NumberFieldErrorMessageProps<T> & {
class?: string;
};
export const NumberFieldErrorMessage = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, numberFieldErrorMessageProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldErrorMessageProps, [
"class",
]);
return (
<NumberFieldPrimitive.ErrorMessage
class={cn(textfieldLabel({ error: true }), local.class)}
{...rest}
/>
);
};
type numberFieldProps<T extends ValidComponent = "div"> =
NumberFieldRootProps<T> & {
class?: string;
};
export const NumberField = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, numberFieldProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldProps, ["class"]);
return (
<NumberFieldPrimitive class={cn("grid gap-1.5", local.class)} {...rest} />
);
};
export const NumberFieldGroup = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"relative rounded-md transition-shadow focus-within:outline-none focus-within:ring-[1.5px] focus-within:ring-ring",
local.class,
)}
{...rest}
/>
);
};
type numberFieldInputProps<T extends ValidComponent = "input"> =
NumberFieldInputProps<T> & {
class?: string;
};
export const NumberFieldInput = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, VoidProps<numberFieldInputProps<T>>>,
) => {
const [local, rest] = splitProps(props as numberFieldInputProps, ["class"]);
return (
<NumberFieldPrimitive.Input
class={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-10 py-1 text-center text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
local.class,
)}
{...rest}
/>
);
};
type numberFieldDecrementTriggerProps<T extends ValidComponent = "button"> =
VoidProps<
NumberFieldDecrementTriggerProps<T> & {
class?: string;
}
>;
export const NumberFieldDecrementTrigger = <
T extends ValidComponent = "button",
>(
props: PolymorphicProps<T, VoidProps<numberFieldDecrementTriggerProps<T>>>,
) => {
const [local, rest] = splitProps(props as numberFieldDecrementTriggerProps, [
"class",
]);
return (
<NumberFieldPrimitive.DecrementTrigger
class={cn(
"absolute left-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20",
local.class,
)}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 12h14"
/>
<title>Decreasing number</title>
</svg>
</NumberFieldPrimitive.DecrementTrigger>
);
};
type numberFieldIncrementTriggerProps<T extends ValidComponent = "button"> =
VoidProps<
NumberFieldIncrementTriggerProps<T> & {
class?: string;
}
>;
export const NumberFieldIncrementTrigger = <
T extends ValidComponent = "button",
>(
props: PolymorphicProps<T, numberFieldIncrementTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as numberFieldIncrementTriggerProps, [
"class",
]);
return (
<NumberFieldPrimitive.IncrementTrigger
class={cn(
"absolute right-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20",
local.class,
)}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m-7-7h14"
/>
<title>Increase number</title>
</svg>
</NumberFieldPrimitive.IncrementTrigger>
);
};

View file

@ -0,0 +1,83 @@
import { cn } from "../libs/cn";
import type { DynamicProps, RootProps } from "@corvu/otp-field";
import OTPFieldPrimitive from "@corvu/otp-field";
import type { ComponentProps, ValidComponent } from "solid-js";
import { Show, splitProps } from "solid-js";
export const OTPFieldInput = OTPFieldPrimitive.Input;
type OTPFieldProps<T extends ValidComponent = "div"> = RootProps<T> & {
class?: string;
};
export const OTPField = <T extends ValidComponent = "div">(
props: DynamicProps<T, OTPFieldProps<T>>,
) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<OTPFieldPrimitive
class={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
local.class,
)}
{...rest}
/>
);
};
export const OTPFieldGroup = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return <div class={cn("flex items-center", local.class)} {...rest} />;
};
export const OTPFieldSeparator = (props: ComponentProps<"div">) => {
return (
// biome-ignore lint/a11y/useAriaPropsForRole: []
<div role="separator" {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 15 15"
>
<title>Separator</title>
<path
fill="currentColor"
fill-rule="evenodd"
d="M5 7.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5"
clip-rule="evenodd"
/>
</svg>
</div>
);
};
export const OTPFieldSlot = (
props: ComponentProps<"div"> & { index: number },
) => {
const [local, rest] = splitProps(props, ["class", "index"]);
const context = OTPFieldPrimitive.useContext();
const char = () => context.value()[local.index];
const hasFakeCaret = () =>
context.value().length === local.index && context.isInserting();
const isActive = () => context.activeSlots().includes(local.index);
return (
<div
class={cn(
"relative flex size-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-shadow first:rounded-l-md first:border-l last:rounded-r-md",
isActive() && "z-10 ring-[1.5px] ring-ring",
local.class,
)}
{...rest}
>
{char()}
<Show when={hasFakeCaret()}>
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
<div class="h-4 w-px animate-caret-blink bg-foreground" />
</div>
</Show>
</div>
);
};

View file

@ -0,0 +1,194 @@
import { cn } from "../libs/cn";
import type {
PaginationEllipsisProps,
PaginationItemProps,
PaginationPreviousProps,
PaginationRootProps,
} from "@kobalte/core/pagination";
import { Pagination as PaginationPrimitive } from "@kobalte/core/pagination";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { VariantProps } from "class-variance-authority";
import type { ValidComponent, VoidProps } from "solid-js";
import { mergeProps, splitProps } from "solid-js";
import { buttonVariants } from "./button";
export const PaginationItems = PaginationPrimitive.Items;
type paginationProps<T extends ValidComponent = "nav"> =
PaginationRootProps<T> & {
class?: string;
};
export const Pagination = <T extends ValidComponent = "nav">(
props: PolymorphicProps<T, paginationProps<T>>,
) => {
const [local, rest] = splitProps(props as paginationProps, ["class"]);
return (
<PaginationPrimitive
class={cn(
"mx-auto flex w-full justify-center [&>ul]:flex [&>ul]:flex-row [&>ul]:items-center [&>ul]:gap-1",
local.class,
)}
{...rest}
/>
);
};
type paginationItemProps<T extends ValidComponent = "button"> =
PaginationItemProps<T> &
Pick<VariantProps<typeof buttonVariants>, "size"> & {
class?: string;
};
export const PaginationItem = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, paginationItemProps<T>>,
) => {
// @ts-expect-error - required `page`
const merge = mergeProps<paginationItemProps[]>({ size: "icon" }, props);
const [local, rest] = splitProps(merge as paginationItemProps, [
"class",
"size",
]);
return (
<PaginationPrimitive.Item
class={cn(
buttonVariants({
variant: "ghost",
size: local.size,
}),
"aria-[current=page]:border aria-[current=page]:border-input aria-[current=page]:bg-background aria-[current=page]:shadow-sm aria-[current=page]:hover:bg-accent aria-[current=page]:hover:text-accent-foreground",
local.class,
)}
{...rest}
/>
);
};
type paginationEllipsisProps<T extends ValidComponent = "div"> = VoidProps<
PaginationEllipsisProps<T> & {
class?: string;
}
>;
export const PaginationEllipsis = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, paginationEllipsisProps<T>>,
) => {
const [local, rest] = splitProps(props as paginationEllipsisProps, ["class"]);
return (
<PaginationPrimitive.Ellipsis
class={cn("flex h-9 w-9 items-center justify-center", local.class)}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
/>
<title>More pages</title>
</svg>
</PaginationPrimitive.Ellipsis>
);
};
type paginationPreviousProps<T extends ValidComponent = "button"> =
PaginationPreviousProps<T> &
Pick<VariantProps<typeof buttonVariants>, "size"> & {
class?: string;
};
export const PaginationPrevious = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, paginationPreviousProps<T>>,
) => {
const merge = mergeProps<paginationPreviousProps<T>[]>(
{ size: "icon" },
props,
);
const [local, rest] = splitProps(merge as paginationPreviousProps, [
"class",
"size",
]);
return (
<PaginationPrimitive.Previous
class={cn(
buttonVariants({
variant: "ghost",
size: local.size,
}),
"aria-[current=page]:border aria-[current=page]:border-input aria-[current=page]:bg-background aria-[current=page]:shadow-sm aria-[current=page]:hover:bg-accent aria-[current=page]:hover:text-accent-foreground",
local.class,
)}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m15 6l-6 6l6 6"
/>
<title>Previous page</title>
</svg>
</PaginationPrimitive.Previous>
);
};
type paginationNextProps<T extends ValidComponent = "button"> =
paginationPreviousProps<T>;
export const PaginationNext = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, paginationNextProps<T>>,
) => {
const merge = mergeProps<paginationNextProps<T>[]>({ size: "icon" }, props);
const [local, rest] = splitProps(merge as paginationNextProps, [
"class",
"size",
]);
return (
<PaginationPrimitive.Next
class={cn(
buttonVariants({
variant: "ghost",
size: local.size,
}),
"aria-[current=page]:border aria-[current=page]:border-input aria-[current=page]:bg-background aria-[current=page]:shadow-sm aria-[current=page]:hover:bg-accent aria-[current=page]:hover:text-accent-foreground",
local.class,
)}
{...rest}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 6l6 6l-6 6"
/>
<title>Next page</title>
</svg>
</PaginationPrimitive.Next>
);
};

View file

@ -0,0 +1,71 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
PopoverContentProps,
PopoverRootProps,
} from "@kobalte/core/popover";
import { Popover as PopoverPrimitive } from "@kobalte/core/popover";
import type { ParentProps, ValidComponent } from "solid-js";
import { mergeProps, splitProps } from "solid-js";
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverTitle = PopoverPrimitive.Title;
export const PopoverDescription = PopoverPrimitive.Description;
export const Popover = (props: PopoverRootProps) => {
const merge = mergeProps<PopoverRootProps[]>(
{
gutter: 4,
flip: false,
},
props,
);
return <PopoverPrimitive {...merge} />;
};
type popoverContentProps<T extends ValidComponent = "div"> = ParentProps<
PopoverContentProps<T> & {
class?: string;
}
>;
export const PopoverContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, popoverContentProps<T>>,
) => {
const [local, rest] = splitProps(props as popoverContentProps, [
"class",
"children",
]);
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
class={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
>
{local.children}
<PopoverPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-[opacity,box-shadow] hover:opacity-100 focus:outline-none focus:ring-[1.5px] focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12"
/>
<title>Close</title>
</svg>
</PopoverPrimitive.CloseButton>
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
);
};

View file

@ -0,0 +1,36 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ProgressRootProps } from "@kobalte/core/progress";
import { Progress as ProgressPrimitive } from "@kobalte/core/progress";
import type { ParentProps, ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const ProgressLabel = ProgressPrimitive.Label;
export const ProgressValueLabel = ProgressPrimitive.ValueLabel;
type progressProps<T extends ValidComponent = "div"> = ParentProps<
ProgressRootProps<T> & {
class?: string;
}
>;
export const Progress = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, progressProps<T>>,
) => {
const [local, rest] = splitProps(props as progressProps, [
"class",
"children",
]);
return (
<ProgressPrimitive
class={cn("flex w-full flex-col gap-2", local.class)}
{...rest}
>
{local.children}
<ProgressPrimitive.Track class="h-2 overflow-hidden rounded-full bg-primary/20">
<ProgressPrimitive.Fill class="h-full w-[--kb-progress-fill-width] bg-primary transition-all duration-500 ease-linear data-[progress=complete]:bg-primary" />
</ProgressPrimitive.Track>
</ProgressPrimitive>
);
};

View file

@ -0,0 +1,39 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { RadioGroupItemControlProps } from "@kobalte/core/radio-group";
import { RadioGroup as RadioGroupPrimitive } from "@kobalte/core/radio-group";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
export const RadioGroupDescription = RadioGroupPrimitive.Description;
export const RadioGroupErrorMessage = RadioGroupPrimitive.ErrorMessage;
export const RadioGroupItemDescription = RadioGroupPrimitive.ItemDescription;
export const RadioGroupItemInput = RadioGroupPrimitive.ItemInput;
export const RadioGroupItemLabel = RadioGroupPrimitive.ItemLabel;
export const RadioGroupLabel = RadioGroupPrimitive.Label;
export const RadioGroup = RadioGroupPrimitive;
export const RadioGroupItem = RadioGroupPrimitive.Item;
type radioGroupItemControlProps<T extends ValidComponent = "div"> = VoidProps<
RadioGroupItemControlProps<T> & { class?: string }
>;
export const RadioGroupItemControl = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, radioGroupItemControlProps<T>>,
) => {
const [local, rest] = splitProps(props as radioGroupItemControlProps, [
"class",
]);
return (
<RadioGroupPrimitive.ItemControl
class={cn(
"flex aspect-square h-4 w-4 items-center justify-center rounded-full border border-primary text-primary shadow transition-shadow focus:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:bg-foreground",
local.class,
)}
{...rest}
>
<RadioGroupPrimitive.ItemIndicator class="h-2 w-2 rounded-full data-[checked]:bg-background" />
</RadioGroupPrimitive.ItemControl>
);
};

View file

@ -0,0 +1,63 @@
import { cn } from "../libs/cn";
import type { DynamicProps, HandleProps, RootProps } from "@corvu/resizable";
import ResizablePrimitive from "@corvu/resizable";
import type { ValidComponent, VoidProps } from "solid-js";
import { Show, splitProps } from "solid-js";
export const ResizablePanel = ResizablePrimitive.Panel;
type resizableProps<T extends ValidComponent = "div"> = RootProps<T> & {
class?: string;
};
export const Resizable = <T extends ValidComponent = "div">(
props: DynamicProps<T, resizableProps<T>>,
) => {
const [local, rest] = splitProps(props as resizableProps, ["class"]);
return <ResizablePrimitive class={cn("size-full", local.class)} {...rest} />;
};
type resizableHandleProps<T extends ValidComponent = "button"> = VoidProps<
HandleProps<T> & {
class?: string;
withHandle?: boolean;
}
>;
export const ResizableHandle = <T extends ValidComponent = "button">(
props: DynamicProps<T, resizableHandleProps<T>>,
) => {
const [local, rest] = splitProps(props as resizableHandleProps, [
"class",
"withHandle",
]);
return (
<ResizablePrimitive.Handle
class={cn(
"flex w-px items-center justify-center bg-border transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring focus-visible:ring-offset-1 data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full",
local.class,
)}
{...rest}
>
<Show when={local.withHandle}>
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-2.5 w-2.5"
viewBox="0 0 15 15"
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M5.5 4.625a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25m4 0a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25M10.625 7.5a1.125 1.125 0 1 1-2.25 0a1.125 1.125 0 0 1 2.25 0M5.5 8.625a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25m5.125 2.875a1.125 1.125 0 1 1-2.25 0a1.125 1.125 0 0 1 2.25 0M5.5 12.625a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25"
clip-rule="evenodd"
/>
<title>Resizable handle</title>
</svg>
</div>
</Show>
</ResizablePrimitive.Handle>
);
};

View file

@ -0,0 +1,127 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
SelectContentProps,
SelectItemProps,
SelectTriggerProps,
} from "@kobalte/core/select";
import { Select as SelectPrimitive } from "@kobalte/core/select";
import type { ParentProps, ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const Select = SelectPrimitive;
export const SelectValue = SelectPrimitive.Value;
export const SelectDescription = SelectPrimitive.Description;
export const SelectErrorMessage = SelectPrimitive.ErrorMessage;
export const SelectItemDescription = SelectPrimitive.ItemDescription;
export const SelectHiddenSelect = SelectPrimitive.HiddenSelect;
export const SelectSection = SelectPrimitive.Section;
type selectTriggerProps<T extends ValidComponent = "button"> = ParentProps<
SelectTriggerProps<T> & { class?: string }
>;
export const SelectTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, selectTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as selectTriggerProps, [
"class",
"children",
]);
return (
<SelectPrimitive.Trigger
class={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background transition-shadow placeholder:text-muted-foreground focus:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
local.class,
)}
{...rest}
>
{local.children}
<SelectPrimitive.Icon
as="svg"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
class="flex size-4 items-center justify-center opacity-50"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m8 9l4-4l4 4m0 6l-4 4l-4-4"
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
};
type selectContentProps<T extends ValidComponent = "div"> =
SelectContentProps<T> & {
class?: string;
};
export const SelectContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, selectContentProps<T>>,
) => {
const [local, rest] = splitProps(props as selectContentProps, ["class"]);
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
class={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
>
<SelectPrimitive.Listbox class="p-1 focus-visible:outline-none" />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
};
type selectItemProps<T extends ValidComponent = "li"> = ParentProps<
SelectItemProps<T> & { class?: string }
>;
export const SelectItem = <T extends ValidComponent = "li">(
props: PolymorphicProps<T, selectItemProps<T>>,
) => {
const [local, rest] = splitProps(props as selectItemProps, [
"class",
"children",
]);
return (
<SelectPrimitive.Item
class={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
<SelectPrimitive.ItemIndicator class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m5 12l5 5L20 7"
/>
<title>Checked</title>
</svg>
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>
</SelectPrimitive.Item>
);
};

View file

@ -0,0 +1,26 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { SeparatorRootProps } from "@kobalte/core/separator";
import { Separator as SeparatorPrimitive } from "@kobalte/core/separator";
import type { ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
type separatorProps<T extends ValidComponent = "hr"> = SeparatorRootProps<T> & {
class?: string;
};
export const Separator = <T extends ValidComponent = "hr">(
props: PolymorphicProps<T, separatorProps<T>>,
) => {
const [local, rest] = splitProps(props as separatorProps, ["class"]);
return (
<SeparatorPrimitive
class={cn(
"shrink-0 bg-border data-[orientation=horizontal]:h-[1px] data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-[1px]",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,148 @@
import { cn } from "../libs/cn";
import type {
DialogContentProps,
DialogDescriptionProps,
DialogTitleProps,
} from "@kobalte/core/dialog";
import { Dialog as DialogPrimitive } from "@kobalte/core/dialog";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
import { mergeProps, splitProps } from "solid-js";
export const Sheet = DialogPrimitive;
export const SheetTrigger = DialogPrimitive.Trigger;
export const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[expanded]:animate-in data-[closed]:animate-out data-[expanded]:duration-200 data-[closed]:duration-200",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[closed]:slide-out-to-top data-[expanded]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[closed]:slide-out-to-bottom data-[expanded]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[closed]:slide-out-to-left data-[expanded]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[closed]:slide-out-to-right data-[expanded]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
type sheetContentProps<T extends ValidComponent = "div"> = ParentProps<
DialogContentProps<T> &
VariantProps<typeof sheetVariants> & {
class?: string;
}
>;
export const SheetContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, sheetContentProps<T>>,
) => {
const merge = mergeProps<sheetContentProps<T>[]>({ side: "right" }, props);
const [local, rest] = splitProps(merge as sheetContentProps, [
"class",
"children",
"side",
]);
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
class={cn(
"fixed inset-0 z-50 bg-background/80 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
)}
/>
<DialogPrimitive.Content
class={sheetVariants({ side: local.side, class: local.class })}
{...rest}
>
{local.children}
<DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-[opacity,box-shadow] hover:opacity-100 focus:outline-none focus:ring-[1.5px] focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12"
/>
<title>Close</title>
</svg>
</DialogPrimitive.CloseButton>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
};
type sheetTitleProps<T extends ValidComponent = "h2"> = DialogTitleProps<T> & {
class?: string;
};
export const SheetTitle = <T extends ValidComponent = "h2">(
props: PolymorphicProps<T, sheetTitleProps<T>>,
) => {
const [local, rest] = splitProps(props as sheetTitleProps, ["class"]);
return (
<DialogPrimitive.Title
class={cn("text-lg font-semibold text-foreground", local.class)}
{...rest}
/>
);
};
type sheetDescriptionProps<T extends ValidComponent = "p"> =
DialogDescriptionProps<T> & {
class?: string;
};
export const SheetDescription = <T extends ValidComponent = "p">(
props: PolymorphicProps<T, sheetDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as sheetDescriptionProps, ["class"]);
return (
<DialogPrimitive.Description
class={cn("text-sm text-muted-foreground", local.class)}
{...rest}
/>
);
};
export const SheetHeader = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"flex flex-col space-y-2 text-center sm:text-left",
local.class,
)}
{...rest}
/>
);
};
export const SheetFooter = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,13 @@
import { cn } from "../libs/cn";
import { type ComponentProps, splitProps } from "solid-js";
export const Skeleton = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div
class={cn("animate-pulse rounded-md bg-primary/10", local.class)}
{...rest}
/>
);
};

View file

@ -0,0 +1,21 @@
import { Toaster as Sonner } from "solid-sonner";
export const Toaster = (props: Parameters<typeof Sonner>[0]) => {
return (
<Sonner
class="toaster group"
toastOptions={{
classes: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};

View file

@ -0,0 +1,62 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
SwitchControlProps,
SwitchThumbProps,
} from "@kobalte/core/switch";
import { Switch as SwitchPrimitive } from "@kobalte/core/switch";
import type { ParentProps, ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
export const SwitchLabel = SwitchPrimitive.Label;
export const Switch = SwitchPrimitive;
export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage;
export const SwitchDescription = SwitchPrimitive.Description;
type switchControlProps<T extends ValidComponent = "input"> = ParentProps<
SwitchControlProps<T> & { class?: string }
>;
export const SwitchControl = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, switchControlProps<T>>,
) => {
const [local, rest] = splitProps(props as switchControlProps, [
"class",
"children",
]);
return (
<>
<SwitchPrimitive.Input class="[&:focus-visible+div]:outline-none [&:focus-visible+div]:ring-[1.5px] [&:focus-visible+div]:ring-ring [&:focus-visible+div]:ring-offset-2 [&:focus-visible+div]:ring-offset-background" />
<SwitchPrimitive.Control
class={cn(
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-[color,background-color,box-shadow] data-[disabled]:cursor-not-allowed data-[checked]:bg-primary data-[disabled]:opacity-50",
local.class,
)}
{...rest}
>
{local.children}
</SwitchPrimitive.Control>
</>
);
};
type switchThumbProps<T extends ValidComponent = "div"> = VoidProps<
SwitchThumbProps<T> & { class?: string }
>;
export const SwitchThumb = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, switchThumbProps<T>>,
) => {
const [local, rest] = splitProps(props as switchThumbProps, ["class"]);
return (
<SwitchPrimitive.Thumb
class={cn(
"pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg ring-0 transition-transform data-[checked]:translate-x-4",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,93 @@
import { cn } from "../libs/cn";
import { type ComponentProps, splitProps } from "solid-js";
export const Table = (props: ComponentProps<"table">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<div class="w-full overflow-auto">
<table
class={cn("w-full caption-bottom text-sm", local.class)}
{...rest}
/>
</div>
);
};
export const TableHeader = (props: ComponentProps<"thead">) => {
const [local, rest] = splitProps(props, ["class"]);
return <thead class={cn("[&_tr]:border-b", local.class)} {...rest} />;
};
export const TableBody = (props: ComponentProps<"tbody">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<tbody class={cn("[&_tr:last-child]:border-0", local.class)} {...rest} />
);
};
export const TableFooter = (props: ComponentProps<"tfoot">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<tbody
class={cn("bg-primary font-medium text-primary-foreground", local.class)}
{...rest}
/>
);
};
export const TableRow = (props: ComponentProps<"tr">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<tr
class={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
local.class,
)}
{...rest}
/>
);
};
export const TableHead = (props: ComponentProps<"th">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<th
class={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
local.class,
)}
{...rest}
/>
);
};
export const TableCell = (props: ComponentProps<"td">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<td
class={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
local.class,
)}
{...rest}
/>
);
};
export const TableCaption = (props: ComponentProps<"caption">) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<caption
class={cn("mt-4 text-sm text-muted-foreground", local.class)}
{...rest}
/>
);
};

133
packages/ui/src/ui/tabs.tsx Normal file
View file

@ -0,0 +1,133 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
TabsContentProps,
TabsIndicatorProps,
TabsListProps,
TabsRootProps,
TabsTriggerProps,
} from "@kobalte/core/tabs";
import { Tabs as TabsPrimitive } from "@kobalte/core/tabs";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
type tabsProps<T extends ValidComponent = "div"> = TabsRootProps<T> & {
class?: string;
};
export const Tabs = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, tabsProps<T>>,
) => {
const [local, rest] = splitProps(props as tabsProps, ["class"]);
return (
<TabsPrimitive
class={cn("w-full data-[orientation=vertical]:flex", local.class)}
{...rest}
/>
);
};
type tabsListProps<T extends ValidComponent = "div"> = TabsListProps<T> & {
class?: string;
};
export const TabsList = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, tabsListProps<T>>,
) => {
const [local, rest] = splitProps(props as tabsListProps, ["class"]);
return (
<TabsPrimitive.List
class={cn(
"relative flex w-full rounded-lg bg-muted p-1 text-muted-foreground data-[orientation=vertical]:flex-col data-[orientation=horizontal]:items-center data-[orientation=vertical]:items-stretch",
local.class,
)}
{...rest}
/>
);
};
type tabsContentProps<T extends ValidComponent = "div"> =
TabsContentProps<T> & {
class?: string;
};
export const TabsContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, tabsContentProps<T>>,
) => {
const [local, rest] = splitProps(props as tabsContentProps, ["class"]);
return (
<TabsPrimitive.Content
class={cn(
"transition-shadow duration-200 focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[orientation=horizontal]:mt-2 data-[orientation=vertical]:ml-2",
local.class,
)}
{...rest}
/>
);
};
type tabsTriggerProps<T extends ValidComponent = "button"> =
TabsTriggerProps<T> & {
class?: string;
};
export const TabsTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, tabsTriggerProps<T>>,
) => {
const [local, rest] = splitProps(props as tabsTriggerProps, ["class"]);
return (
<TabsPrimitive.Trigger
class={cn(
"peer relative z-10 inline-flex h-7 w-full items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium outline-none transition-colors disabled:pointer-events-none disabled:opacity-50 data-[selected]:text-foreground",
local.class,
)}
{...rest}
/>
);
};
const tabsIndicatorVariants = cva(
"absolute transition-all duration-200 outline-none",
{
variants: {
variant: {
block:
"data-[orientation=horizontal]:bottom-1 data-[orientation=horizontal]:left-0 data-[orientation=vertical]:right-1 data-[orientation=vertical]:top-0 data-[orientation=horizontal]:h-[calc(100%-0.5rem)] data-[orientation=vertical]:w-[calc(100%-0.5rem)] bg-background shadow rounded-md peer-focus-visible:ring-[1.5px] peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-background peer-focus-visible:outline-none",
underline:
"data-[orientation=horizontal]:-bottom-[1px] data-[orientation=horizontal]:left-0 data-[orientation=vertical]:-right-[1px] data-[orientation=vertical]:top-0 data-[orientation=horizontal]:h-[2px] data-[orientation=vertical]:w-[2px] bg-primary",
},
},
defaultVariants: {
variant: "block",
},
},
);
type tabsIndicatorProps<T extends ValidComponent = "div"> = VoidProps<
TabsIndicatorProps<T> &
VariantProps<typeof tabsIndicatorVariants> & {
class?: string;
}
>;
export const TabsIndicator = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, tabsIndicatorProps<T>>,
) => {
const [local, rest] = splitProps(props as tabsIndicatorProps, [
"class",
"variant",
]);
return (
<TabsPrimitive.Indicator
class={cn(tabsIndicatorVariants({ variant: local.variant }), local.class)}
{...rest}
/>
);
};

View file

@ -0,0 +1,28 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { TextFieldTextAreaProps } from "@kobalte/core/text-field";
import { TextArea as TextFieldPrimitive } from "@kobalte/core/text-field";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps<
TextFieldTextAreaProps<T> & {
class?: string;
}
>;
export const TextArea = <T extends ValidComponent = "textarea">(
props: PolymorphicProps<T, textAreaProps<T>>,
) => {
const [local, rest] = splitProps(props as textAreaProps, ["class"]);
return (
<TextFieldPrimitive
class={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-shadow placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,129 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
TextFieldDescriptionProps,
TextFieldErrorMessageProps,
TextFieldInputProps,
TextFieldLabelProps,
TextFieldRootProps,
} from "@kobalte/core/text-field";
import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field";
import { cva } from "class-variance-authority";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
type textFieldProps<T extends ValidComponent = "div"> =
TextFieldRootProps<T> & {
class?: string;
};
export const TextFieldRoot = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldProps, ["class"]);
return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />;
};
export const textfieldLabel = cva(
"text-sm data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70 font-medium",
{
variants: {
label: {
true: "data-[invalid]:text-destructive",
},
error: {
true: "text-destructive text-xs",
},
description: {
true: "font-normal text-muted-foreground",
},
},
defaultVariants: {
label: true,
},
},
);
type textFieldLabelProps<T extends ValidComponent = "label"> =
TextFieldLabelProps<T> & {
class?: string;
};
export const TextFieldLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, textFieldLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldLabelProps, ["class"]);
return (
<TextFieldPrimitive.Label
class={cn(textfieldLabel(), local.class)}
{...rest}
/>
);
};
type textFieldErrorMessageProps<T extends ValidComponent = "div"> =
TextFieldErrorMessageProps<T> & {
class?: string;
};
export const TextFieldErrorMessage = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldErrorMessageProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldErrorMessageProps, [
"class",
]);
return (
<TextFieldPrimitive.ErrorMessage
class={cn(textfieldLabel({ error: true }), local.class)}
{...rest}
/>
);
};
type textFieldDescriptionProps<T extends ValidComponent = "div"> =
TextFieldDescriptionProps<T> & {
class?: string;
};
export const TextFieldDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldDescriptionProps, [
"class",
]);
return (
<TextFieldPrimitive.Description
class={cn(
textfieldLabel({ description: true, label: false }),
local.class,
)}
{...rest}
/>
);
};
type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps<
TextFieldInputProps<T> & {
class?: string;
}
>;
export const TextField = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, textFieldInputProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldInputProps, ["class"]);
return (
<TextFieldPrimitive.Input
class={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-shadow file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,168 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
ToastDescriptionProps,
ToastListProps,
ToastRegionProps,
ToastRootProps,
ToastTitleProps,
} from "@kobalte/core/toast";
import { Toast as ToastPrimitive } from "@kobalte/core/toast";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type {
ComponentProps,
ValidComponent,
VoidComponent,
VoidProps,
} from "solid-js";
import { mergeProps, splitProps } from "solid-js";
import { Portal } from "solid-js/web";
export const toastVariants = cva(
"group pointer-events-auto relative flex flex-col gap-3 w-full items-center justify-between overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-y-0 data-[swipe=end]:translate-y-[var(--kb-toast-swipe-end-y)] data-[swipe=move]:translate-y-[--kb-toast-swipe-move-y] data-[swipe=move]:transition-none data-[opened]:animate-in data-[closed]:animate-out data-[swipe=end]:animate-out data-[closed]:fade-out-80 data-[closed]:slide-out-to-top-full data-[closed]:sm:slide-out-to-bottom-full data-[opened]:slide-in-from-top-full data-[opened]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
type toastProps<T extends ValidComponent = "li"> = ToastRootProps<T> &
VariantProps<typeof toastVariants> & {
class?: string;
};
export const Toast = <T extends ValidComponent = "li">(
props: PolymorphicProps<T, toastProps<T>>,
) => {
const [local, rest] = splitProps(props as toastProps, ["class", "variant"]);
return (
<ToastPrimitive
class={cn(toastVariants({ variant: local.variant }), local.class)}
{...rest}
/>
);
};
type toastTitleProps<T extends ValidComponent = "div"> = ToastTitleProps<T> & {
class?: string;
};
export const ToastTitle = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, toastTitleProps<T>>,
) => {
const [local, rest] = splitProps(props as toastTitleProps, ["class"]);
return (
<ToastPrimitive.Title
class={cn("text-sm font-semibold [&+div]:text-xs", local.class)}
{...rest}
/>
);
};
type toastDescriptionProps<T extends ValidComponent = "div"> =
ToastDescriptionProps<T> & {
class?: string;
};
export const ToastDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, toastDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as toastDescriptionProps, ["class"]);
return (
<ToastPrimitive.Description
class={cn("text-sm opacity-90", local.class)}
{...rest}
/>
);
};
type toastRegionProps<T extends ValidComponent = "div"> =
ToastRegionProps<T> & {
class?: string;
};
export const ToastRegion = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, toastRegionProps<T>>,
) => {
const merge = mergeProps<toastRegionProps[]>(
{
swipeDirection: "down",
},
props,
);
return (
<Portal>
<ToastPrimitive.Region {...merge} />
</Portal>
);
};
type toastListProps<T extends ValidComponent = "ol"> = VoidProps<
ToastListProps<T> & {
class?: string;
}
>;
export const ToastList = <T extends ValidComponent = "ol">(
props: PolymorphicProps<T, toastListProps<T>>,
) => {
const [local, rest] = splitProps(props as toastListProps, ["class"]);
return (
<ToastPrimitive.List
class={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse gap-2 p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
local.class,
)}
{...rest}
/>
);
};
export const ToastContent = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class", "children"]);
return (
<div class={cn("flex w-full flex-col", local.class)} {...rest}>
<div>{local.children}</div>
<ToastPrimitive.CloseButton class="absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12"
/>
<title>Close</title>
</svg>
</ToastPrimitive.CloseButton>
</div>
);
};
export const ToastProgress: VoidComponent = () => {
return (
<ToastPrimitive.ProgressTrack class="h-1 w-full overflow-hidden rounded-xl bg-primary/20 group-[.destructive]:bg-background/20">
<ToastPrimitive.ProgressFill class="h-full w-[--kb-toast-progress-fill-width] bg-primary transition-all duration-150 ease-linear group-[.destructive]:bg-destructive-foreground" />
</ToastPrimitive.ProgressTrack>
);
};

View file

@ -0,0 +1,85 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
ToggleGroupItemProps,
ToggleGroupRootProps,
} from "@kobalte/core/toggle-group";
import { ToggleGroup as ToggleGroupPrimitive } from "@kobalte/core/toggle-group";
import type { VariantProps } from "class-variance-authority";
import type { Accessor, ParentProps, ValidComponent } from "solid-js";
import { createContext, createMemo, splitProps, useContext } from "solid-js";
import { toggleVariants } from "./toggle";
const ToggleGroupContext =
createContext<Accessor<VariantProps<typeof toggleVariants>>>();
const useToggleGroup = () => {
const context = useContext(ToggleGroupContext);
if (!context) {
throw new Error(
"`useToggleGroup`: must be used within a `ToggleGroup` component",
);
}
return context;
};
type toggleGroupProps<T extends ValidComponent = "div"> = ParentProps<
ToggleGroupRootProps<T> &
VariantProps<typeof toggleVariants> & {
class?: string;
}
>;
export const ToggleGroup = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, toggleGroupProps<T>>,
) => {
const [local, rest] = splitProps(props as toggleGroupProps, [
"class",
"children",
"size",
"variant",
]);
const value = createMemo<VariantProps<typeof toggleVariants>>(() => ({
size: local.size,
variant: local.variant,
}));
return (
<ToggleGroupPrimitive
class={cn("flex items-center justify-center gap-1", local.class)}
{...rest}
>
<ToggleGroupContext.Provider value={value}>
{local.children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive>
);
};
type toggleGroupItemProps<T extends ValidComponent = "button"> =
ToggleGroupItemProps<T> & {
class?: string;
};
export const ToggleGroupItem = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, toggleGroupItemProps<T>>,
) => {
const [local, rest] = splitProps(props as toggleGroupItemProps, ["class"]);
const context = useToggleGroup();
return (
<ToggleGroupPrimitive.Item
class={cn(
toggleVariants({
variant: context().variant,
size: context().size,
}),
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,56 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { ToggleButtonRootProps } from "@kobalte/core/toggle-button";
import { ToggleButton as ToggleButtonPrimitive } from "@kobalte/core/toggle-button";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { ValidComponent } from "solid-js";
import { splitProps } from "solid-js";
export const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-[box-shadow,color,background-color] hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-accent data-[pressed]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-3",
sm: "h-8 px-2",
lg: "h-10 px-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
type toggleButtonProps<T extends ValidComponent = "button"> =
ToggleButtonRootProps<T> &
VariantProps<typeof toggleVariants> & {
class?: string;
};
export const ToggleButton = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, toggleButtonProps<T>>,
) => {
const [local, rest] = splitProps(props as toggleButtonProps, [
"class",
"variant",
"size",
]);
return (
<ToggleButtonPrimitive
class={cn(
toggleVariants({ variant: local.variant, size: local.size }),
local.class,
)}
{...rest}
/>
);
};

View file

@ -0,0 +1,45 @@
import { cn } from "../libs/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type {
TooltipContentProps,
TooltipRootProps,
} from "@kobalte/core/tooltip";
import { Tooltip as TooltipPrimitive } from "@kobalte/core/tooltip";
import { type ValidComponent, mergeProps, splitProps } from "solid-js";
export const TooltipTrigger = TooltipPrimitive.Trigger;
export const Tooltip = (props: TooltipRootProps) => {
const merge = mergeProps<TooltipRootProps[]>(
{
gutter: 4,
flip: false,
},
props,
);
return <TooltipPrimitive {...merge} />;
};
type tooltipContentProps<T extends ValidComponent = "div"> =
TooltipContentProps<T> & {
class?: string;
};
export const TooltipContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, tooltipContentProps<T>>,
) => {
const [local, rest] = splitProps(props as tooltipContentProps, ["class"]);
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
class={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
local.class,
)}
{...rest}
/>
</TooltipPrimitive.Portal>
);
};

17
packages/ui/tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

24
packages/web/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

20
packages/web/index.html Normal file
View file

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tensamin</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

56
packages/web/package.json Normal file
View file

@ -0,0 +1,56 @@
{
"name": "@tensamin/web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"dev": "vite --port 3000",
"build": "tsc -b && vite build",
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
},
"dependencies": {
"@tensamin/chat": "workspace:*",
"@tensamin/core-crypto": "workspace:*",
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git",
"@tensamin/core-storage": "workspace:*",
"@tensamin/core-user": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/ui": "workspace:*",
"@ark-ui/solid": "^5.34.1",
"@corvu/drawer": "^0.2.4",
"@corvu/otp-field": "^0.1.4",
"@corvu/resizable": "^0.2.5",
"@kobalte/core": "^0.13.11",
"@noble/curves": "^2.0.1",
"@solidjs/router": "^0.15.4",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/solid-virtual": "^3.13.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk-solid": "^1.1.2",
"comlink": "^4.4.2",
"embla-carousel-solid": "^8.6.0",
"lucide-solid": "^0.564.0",
"solid-js": "^1.9.11",
"solid-sonner": "^0.2.8",
"tailwind-merge": "^3.5.0",
"tailwind-scrollbar-hide": "^4.0.0",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^6.1.1",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"eslint": "^10.0.3",
"eslint-plugin-solid": "^0.14.5",
"globals": "^17.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.57.0",
"vite": "^7.3.1",
"vite-plugin-lucide-preprocess": "^1.4.8",
"vite-plugin-solid": "^2.11.10"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,20 @@
import type { User } from "@tensamin/core-user/context";
import Avatar from "@tensamin/ui/avatar";
import { reduceDisplay } from "./utils";
import { Card, CardHeader } from "@tensamin/ui/card";
export default function Basic(props: { user: User }) {
return (
<Card class="animate-in fade-in duration-300 rounded-2xl">
<CardHeader class="flex flex-row gap-2.5 items-center justify-start p-2">
<Avatar
img={props.user.avatar}
fallback={reduceDisplay(props.user.display)}
/>
<div class="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
<p>{props.user.display}</p>
</div>
</CardHeader>
</Card>
);
}

Some files were not shown because too many files have changed in this diff Show more