Moved to ReactJS
This commit is contained in:
parent
c8ef2b248f
commit
1d0ebeb2b7
100 changed files with 1909 additions and 6056 deletions
|
|
@ -15,9 +15,9 @@
|
|||
"build": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.15.4",
|
||||
"@tanstack/solid-query": "^5.90.23",
|
||||
"@tanstack/solid-virtual": "^3.13.21",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@tanstack/react-virtual": "^3.0.0",
|
||||
"@tensamin/core-crypto": "workspace:*",
|
||||
"@tensamin/ttp": "workspace:*",
|
||||
"@tensamin/core-storage": "workspace:*",
|
||||
|
|
@ -25,9 +25,9 @@
|
|||
"@tensamin/markdown": "workspace:*",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"lucide-solid": "^0.564.0",
|
||||
"solid-js": "^1.9.11",
|
||||
"solid-sonner": "^0.2.8",
|
||||
"lucide-react": "^0.564.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useSocket } from "@tensamin/ttp/context";
|
||||
import type { BoundSendFn } from "@tensamin/ttp/core";
|
||||
import type { Socket } from "@tensamin/shared/data";
|
||||
import type { RawMessages } from "../values";
|
||||
|
||||
export async function getMessages(
|
||||
send: BoundSendFn<Socket>,
|
||||
amount: number,
|
||||
offset: number,
|
||||
user_id: number,
|
||||
): Promise<RawMessages> {
|
||||
const { send } = useSocket();
|
||||
|
||||
const messages = await send("messages_get", {
|
||||
amount: amount,
|
||||
offset: offset,
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
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 * as React from "react";
|
||||
import { Button } from "@tensamin/ui/button";
|
||||
|
||||
import { Plus, Laugh, Clapperboard } from "lucide-solid";
|
||||
import { Plus, Laugh, Clapperboard } from "lucide-react";
|
||||
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 [value, setValue] = React.useState("");
|
||||
const [invertEnterBehavior, setInvertEnterBehavior] = React.useState(false);
|
||||
|
||||
const { encrypt } = useCrypto();
|
||||
const { send } = useSocket();
|
||||
const { addLiveMessage, sharedSecret, userId } = useChat();
|
||||
const { load } = useStorage();
|
||||
|
||||
createEffect(() => {
|
||||
React.useEffect(() => {
|
||||
void load("chat_invert_enter_behaviour").then((shouldInvert) => {
|
||||
setInvertEnterBehavior(shouldInvert);
|
||||
});
|
||||
});
|
||||
}, [load]);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (value().trim() === "") return;
|
||||
if (value.trim() === "") return;
|
||||
|
||||
const time = Date.now();
|
||||
const currentValue = value();
|
||||
const currentValue = value;
|
||||
const currentUserId = userId();
|
||||
|
||||
addLiveMessage({
|
||||
|
|
@ -61,36 +61,36 @@ export default function InputComponent() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Card class="rounded-none rounded-t-xl border border-input/50 border-b-0">
|
||||
<CardHeader class="p-0 flex flex-col">
|
||||
<Card className="rounded-none rounded-t-xl border border-input/50 border-b-0">
|
||||
<CardHeader className="p-0 flex flex-col">
|
||||
<Input
|
||||
placeholder="Send a message..."
|
||||
value={value()}
|
||||
value={value}
|
||||
setValue={setValue}
|
||||
onSubmit={handleSubmit}
|
||||
invertEnterBehavior={invertEnterBehavior()}
|
||||
invertEnterBehavior={invertEnterBehavior}
|
||||
/>
|
||||
<div class="w-full flex justify-between gap-2 p-2 pt-0">
|
||||
<div class="flex gap-2">
|
||||
<div className="w-full flex justify-between gap-2 p-2 pt-0">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
class="w-9 h-9 p-0 border border-ring/10"
|
||||
className="w-9 h-9 p-0 border border-ring/10"
|
||||
variant="secondary"
|
||||
>
|
||||
<Plus size={20} />
|
||||
</Button>
|
||||
<div class="w-auto flex">
|
||||
<div className="w-auto flex">
|
||||
<p>Files list</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
class="w-9 h-9 p-0 border border-ring/10"
|
||||
className="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"
|
||||
className="w-9 h-9 p-0 border border-ring/10"
|
||||
variant="secondary"
|
||||
>
|
||||
<Clapperboard size={20} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCrypto } from "@tensamin/core-crypto/context";
|
||||
import { createEffect, createSignal, onCleanup, Show } from "solid-js";
|
||||
import * as React from "react";
|
||||
import { useChat } from "../context";
|
||||
import type { RawMessage } from "../values";
|
||||
import { log } from "@tensamin/shared/log";
|
||||
|
|
@ -12,18 +12,17 @@ export default function Message(props: {
|
|||
const { sharedSecret } = useChat();
|
||||
const { decrypt } = useCrypto();
|
||||
|
||||
const message = () => props.message;
|
||||
const [decodedContent, setDecodedContent] = createSignal("");
|
||||
const [isReady, setIsReady] = createSignal(false);
|
||||
const [decodedContent, setDecodedContent] = React.useState("");
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
|
||||
createEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (props.notEncrypted) {
|
||||
setDecodedContent(message().content);
|
||||
setDecodedContent(props.message.content);
|
||||
setIsReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = message().content;
|
||||
const content = props.message.content;
|
||||
const secret = sharedSecret();
|
||||
let active = true;
|
||||
|
||||
|
|
@ -57,23 +56,21 @@ export default function Message(props: {
|
|||
setIsReady(true);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
return () => {
|
||||
active = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
}, [decrypt, props.message.content, props.notEncrypted, sharedSecret]);
|
||||
|
||||
return (
|
||||
<div class="w-full flex justify-start">
|
||||
<div className="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
|
||||
className={`animate-in fade-in duration-200 max-w-[80%] rounded-xl px-2 py-1 whitespace-pre-wrap wrap-break-word ${
|
||||
props.message.sent_by_self
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted"
|
||||
}`}
|
||||
>
|
||||
<Show when={isReady()}>
|
||||
<Text value={decodedContent()} />
|
||||
</Show>
|
||||
{isReady ? <Text value={decodedContent} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,49 +1,47 @@
|
|||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
createSignal,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import type { ParentProps } from "solid-js";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
import * as React from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useRouterState } from "@tanstack/react-router";
|
||||
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";
|
||||
import { useSocket } from "@tensamin/ttp/context";
|
||||
|
||||
export const context = createContext<contextType>();
|
||||
export const context = React.createContext<contextType | undefined>(undefined);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function Provider(
|
||||
props: ParentProps & {
|
||||
getMessages: (
|
||||
amount: number,
|
||||
offset: number,
|
||||
user_id: number,
|
||||
) => Promise<RawMessages>;
|
||||
},
|
||||
props: { children: React.ReactNode },
|
||||
) {
|
||||
const { get_shared_secret } = useCrypto();
|
||||
const { get } = useUser();
|
||||
const { load } = useStorage();
|
||||
const { send } = useSocket();
|
||||
|
||||
const [liveMessages, setLiveMessages] = createSignal<RawMessages>([]);
|
||||
const [currentSharedSecret, setCurrentSharedSecret] = createSignal("");
|
||||
const [liveMessagesState, setLiveMessagesState] = React.useState<RawMessages>([]);
|
||||
const [currentSharedSecret, setCurrentSharedSecret] = React.useState("");
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const userId = () => Number(searchParams.id ?? 0);
|
||||
const locationSearch = useRouterState({
|
||||
select: (state) => state.location.search,
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const recipientId = userId();
|
||||
const userIdValue = React.useMemo(() => {
|
||||
const rawId = (locationSearch as unknown as { id?: unknown })?.id;
|
||||
return Number(rawId ?? 0);
|
||||
}, [locationSearch]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const recipientId = userIdValue;
|
||||
|
||||
if (!recipientId) {
|
||||
setCurrentSharedSecret("");
|
||||
return;
|
||||
}
|
||||
|
||||
get(recipientId).then(async (recipientData) => {
|
||||
let active = true;
|
||||
|
||||
void get(recipientId).then(async (recipientData) => {
|
||||
const ownId = await load("user_id");
|
||||
const privateKey = await load("private_key");
|
||||
const ownData = await get(ownId);
|
||||
|
|
@ -53,39 +51,58 @@ export default function Provider(
|
|||
recipientData.public_key,
|
||||
);
|
||||
|
||||
setCurrentSharedSecret(sharedSecret);
|
||||
if (active) {
|
||||
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 () => {
|
||||
active = false;
|
||||
};
|
||||
}, [get, get_shared_secret, load, userIdValue]);
|
||||
|
||||
return sorted;
|
||||
}
|
||||
const customGetMessages = React.useCallback(
|
||||
async (amount: number, offset: number) => {
|
||||
const messages = await send("messages_get", {
|
||||
amount,
|
||||
offset,
|
||||
user_id: userIdValue,
|
||||
});
|
||||
|
||||
function addLiveMessage(message: RawMessage) {
|
||||
setLiveMessages((prev) => [...prev, message]);
|
||||
}
|
||||
if (messages.type.startsWith("error")) {
|
||||
throw new Error(messages.type);
|
||||
}
|
||||
|
||||
function clearLiveMessages() {
|
||||
setLiveMessages([]);
|
||||
}
|
||||
const rawMessages = messages.data.messages;
|
||||
const sorted = [...rawMessages].sort((a, b) => a.timestamp - b.timestamp);
|
||||
return sorted;
|
||||
},
|
||||
[send, userIdValue],
|
||||
);
|
||||
|
||||
const addLiveMessage = React.useCallback((message: RawMessage) => {
|
||||
setLiveMessagesState((prev) => [...prev, message]);
|
||||
}, []);
|
||||
|
||||
const clearLiveMessages = React.useCallback(() => {
|
||||
setLiveMessagesState([]);
|
||||
}, []);
|
||||
|
||||
const value = React.useMemo<contextType>(
|
||||
() => ({
|
||||
getMessages: customGetMessages,
|
||||
liveMessages: () => liveMessagesState,
|
||||
addLiveMessage,
|
||||
clearLiveMessages,
|
||||
sharedSecret: () => currentSharedSecret,
|
||||
userId: () => userIdValue,
|
||||
}),
|
||||
[addLiveMessage, clearLiveMessages, currentSharedSecret, customGetMessages, liveMessagesState, userIdValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<context.Provider
|
||||
value={{
|
||||
getMessages: customGetMessages,
|
||||
liveMessages,
|
||||
addLiveMessage,
|
||||
clearLiveMessages,
|
||||
sharedSecret: currentSharedSecret,
|
||||
userId,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</context.Provider>
|
||||
<context.Provider value={value}>{props.children}</context.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -100,9 +117,9 @@ type contextType = {
|
|||
};
|
||||
|
||||
export function useChat(): contextType {
|
||||
const ctx = useContext(context);
|
||||
const ctx = React.useContext(context);
|
||||
if (!ctx) {
|
||||
throw new Error("useChat must be used within a ChatProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { createVirtualizer } from "@tanstack/solid-virtual";
|
||||
import { useInfiniteQuery } from "@tanstack/solid-query";
|
||||
import * as React from "react";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useChat } from "./context";
|
||||
import { createEffect, createMemo, createSignal, For } from "solid-js";
|
||||
|
||||
import InputComponent from "./components/input";
|
||||
import Message from "./components/message";
|
||||
|
|
@ -11,21 +11,22 @@ 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 scrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [hasScrolledToBottomInitially, setHasScrolledToBottomInitially] =
|
||||
createSignal(false);
|
||||
const [lastLiveMessageCount, setLastLiveMessageCount] = createSignal(0);
|
||||
const [prependAnchor, setPrependAnchor] = createSignal<{
|
||||
React.useState(false);
|
||||
const [lastLiveMessageCount, setLastLiveMessageCount] = React.useState(0);
|
||||
const [prependAnchor, setPrependAnchor] = React.useState<{
|
||||
totalSize: number;
|
||||
scrollTop: number;
|
||||
} | null>(null);
|
||||
|
||||
const messagesQuery = useInfiniteQuery(() => ({
|
||||
queryKey: ["chat-messages", String(userId())],
|
||||
const chatUserId = userId();
|
||||
|
||||
const messagesQuery = useInfiniteQuery({
|
||||
queryKey: ["chat-messages", String(chatUserId)],
|
||||
initialPageParam: 0,
|
||||
queryFn: ({ pageParam }) => getMessages(PAGE_SIZE, pageParam),
|
||||
queryFn: ({ pageParam }) => getMessages(PAGE_SIZE, Number(pageParam)),
|
||||
getNextPageParam: (lastPage, allPages) => {
|
||||
if (lastPage.length < PAGE_SIZE) {
|
||||
return undefined;
|
||||
|
|
@ -33,54 +34,30 @@ export default function Screen() {
|
|||
|
||||
return allPages.length * PAGE_SIZE;
|
||||
},
|
||||
}));
|
||||
|
||||
const historicalMessages = createMemo(() => {
|
||||
const pages = messagesQuery.data?.pages ?? [];
|
||||
return [...pages].reverse().flat();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
userId();
|
||||
const historicalMessages = React.useMemo(() => {
|
||||
const pages = messagesQuery.data?.pages ?? [];
|
||||
return [...pages].reverse().flat();
|
||||
}, [messagesQuery.data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
clearLiveMessages();
|
||||
setHasScrolledToBottomInitially(false);
|
||||
setLastLiveMessageCount(0);
|
||||
setPrependAnchor(null);
|
||||
});
|
||||
}, [chatUserId, clearLiveMessages]);
|
||||
|
||||
const messages = createMemo(() => [
|
||||
...historicalMessages(),
|
||||
...liveMessages(),
|
||||
]);
|
||||
const messages = React.useMemo(
|
||||
() => [...historicalMessages, ...liveMessages()],
|
||||
[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,
|
||||
const virtualizer = useVirtualizer({
|
||||
count: messages.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: (index) => {
|
||||
const message = messages()[index];
|
||||
const message = messages[index];
|
||||
|
||||
if (!message) {
|
||||
return 56;
|
||||
|
|
@ -102,82 +79,107 @@ export default function Screen() {
|
|||
overscan: 6,
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const currentMessages = messages();
|
||||
const onScroll = React.useCallback(async () => {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollRef.current.scrollTop > 96) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messagesQuery.isFetchingNextPage || !messagesQuery.hasNextPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPrependAnchor({
|
||||
totalSize: virtualizer.getTotalSize(),
|
||||
scrollTop: scrollRef.current.scrollTop,
|
||||
});
|
||||
|
||||
await messagesQuery.fetchNextPage();
|
||||
}, [messagesQuery, virtualizer]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!scrollRef ||
|
||||
hasScrolledToBottomInitially() ||
|
||||
currentMessages.length === 0
|
||||
!scrollRef.current ||
|
||||
hasScrolledToBottomInitially ||
|
||||
messages.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (!scrollRef) {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollRef.scrollTop = scrollRef.scrollHeight;
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
setHasScrolledToBottomInitially(true);
|
||||
});
|
||||
});
|
||||
}, [hasScrolledToBottomInitially, messages]);
|
||||
|
||||
createEffect(() => {
|
||||
React.useEffect(() => {
|
||||
const count = liveMessages().length;
|
||||
|
||||
if (!scrollRef) {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count > lastLiveMessageCount()) {
|
||||
if (count > lastLiveMessageCount) {
|
||||
queueMicrotask(() => {
|
||||
if (!scrollRef) {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollRef.scrollTop = scrollRef.scrollHeight;
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
setLastLiveMessageCount(count);
|
||||
});
|
||||
}, [lastLiveMessageCount, liveMessages]);
|
||||
|
||||
createEffect(() => {
|
||||
const anchor = prependAnchor();
|
||||
|
||||
if (!scrollRef || !anchor || messagesQuery.isFetchingNextPage) {
|
||||
React.useEffect(() => {
|
||||
if (!scrollRef.current || !prependAnchor || messagesQuery.isFetchingNextPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (!scrollRef) {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = virtualizer.getTotalSize() - anchor.totalSize;
|
||||
scrollRef.scrollTop = anchor.scrollTop + delta;
|
||||
const delta = virtualizer.getTotalSize() - prependAnchor.totalSize;
|
||||
scrollRef.current.scrollTop = prependAnchor.scrollTop + delta;
|
||||
setPrependAnchor(null);
|
||||
});
|
||||
});
|
||||
}, [messagesQuery.isFetchingNextPage, prependAnchor, virtualizer]);
|
||||
|
||||
return (
|
||||
<div class="w-full h-full flex flex-col overflow-hidden px-2">
|
||||
<div className="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}
|
||||
className="flex-1 max-h-[calc(100vh-151px)] overflow-y-auto px-2.5"
|
||||
onScroll={() => {
|
||||
void onScroll();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="relative w-full"
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
<For each={virtualizer.getVirtualItems()}>
|
||||
{(virtualRow) => (
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const message = messages[virtualRow.index];
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
|
|
@ -189,16 +191,13 @@ export default function Screen() {
|
|||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
<Message
|
||||
message={messages()[virtualRow.index]}
|
||||
notEncrypted={messages()[virtualRow.index].not_encrypted}
|
||||
/>
|
||||
<Message message={message} notEncrypted={message.not_encrypted} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<InputComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"@noble/curves": "^2.0.1",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"comlink": "^4.4.2",
|
||||
"solid-js": "^1.9.11"
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,41 @@
|
|||
import {
|
||||
createContext,
|
||||
onMount,
|
||||
onCleanup,
|
||||
createSignal,
|
||||
Show,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import type { ParentProps } from "solid-js";
|
||||
|
||||
import * as React from "react";
|
||||
import * as Comlink from "comlink";
|
||||
import Loading from "@tensamin/ui/screens/loading";
|
||||
|
||||
export const context = createContext<contextType>();
|
||||
export const context = React.createContext<contextType | undefined>(undefined);
|
||||
|
||||
export default function Provider(props: ParentProps) {
|
||||
let apiRef: ApiRef | null = null;
|
||||
const [isWorkerReady, setIsWorkerReady] = createSignal(false);
|
||||
export default function Provider(props: { children: React.ReactNode }) {
|
||||
const apiRef = React.useRef<ApiRef | null>(null);
|
||||
const [isWorkerReady, setIsWorkerReady] = React.useState(false);
|
||||
|
||||
const { encrypt, decrypt, get_shared_secret } = createCryptoActions(
|
||||
() => apiRef,
|
||||
const { encrypt, decrypt, get_shared_secret } = React.useMemo(
|
||||
() => createCryptoActions(() => apiRef.current),
|
||||
[],
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
React.useEffect(() => {
|
||||
const worker = new Worker(new URL("./worker.ts", import.meta.url), {
|
||||
type: "module",
|
||||
});
|
||||
apiRef = Comlink.wrap(worker);
|
||||
|
||||
apiRef.current = Comlink.wrap(worker);
|
||||
setIsWorkerReady(true);
|
||||
|
||||
onCleanup(() => {
|
||||
apiRef = null;
|
||||
return () => {
|
||||
apiRef.current = null;
|
||||
worker.terminate();
|
||||
});
|
||||
});
|
||||
setIsWorkerReady(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isWorkerReady) {
|
||||
return <Loading progress={10} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={isWorkerReady()} fallback={<Loading progress={10} />}>
|
||||
<context.Provider value={{ encrypt, decrypt, get_shared_secret }}>
|
||||
{props.children}
|
||||
</context.Provider>
|
||||
</Show>
|
||||
<context.Provider value={{ encrypt, decrypt, get_shared_secret }}>
|
||||
{props.children}
|
||||
</context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +64,7 @@ export function createCryptoActions(
|
|||
): contextType {
|
||||
const encrypt = async (secret: string, message: string): Promise<string> => {
|
||||
const apiRef = getApiRef();
|
||||
if (!apiRef) throw "API not initialized";
|
||||
if (!apiRef) throw new Error("API not initialized");
|
||||
return await apiRef.encrypt(secret, message);
|
||||
};
|
||||
|
||||
|
|
@ -77,7 +73,7 @@ export function createCryptoActions(
|
|||
encryptedMessage: string,
|
||||
): Promise<string> => {
|
||||
const apiRef = getApiRef();
|
||||
if (!apiRef) throw "API not initialized";
|
||||
if (!apiRef) throw new Error("API not initialized");
|
||||
return await apiRef.decrypt(secret, encryptedMessage);
|
||||
};
|
||||
|
||||
|
|
@ -87,7 +83,7 @@ export function createCryptoActions(
|
|||
other_public_key: string,
|
||||
): Promise<string> => {
|
||||
const apiRef = getApiRef();
|
||||
if (!apiRef) throw "API not initialized";
|
||||
if (!apiRef) throw new Error("API not initialized");
|
||||
return await apiRef.get_shared_secret(
|
||||
own_private_key,
|
||||
own_public_key,
|
||||
|
|
@ -99,9 +95,9 @@ export function createCryptoActions(
|
|||
}
|
||||
|
||||
export function useCrypto(): contextType {
|
||||
const ctx = useContext(context);
|
||||
const ctx = React.useContext(context);
|
||||
if (!ctx) {
|
||||
throw new Error("useCrypto must be used within a CryptoProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"solid-js": "^1.9.11"
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,9 @@ import {
|
|||
indentWithTab,
|
||||
} from "@codemirror/commands";
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
onCleanup,
|
||||
onMount,
|
||||
type Setter,
|
||||
} from "solid-js";
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
import { collectInlineRanges, ensureMarkdownStyles } from "./markdown";
|
||||
|
||||
|
|
@ -34,7 +31,7 @@ export type InputProps = {
|
|||
ref?: HTMLDivElement;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
setValue: Setter<string>;
|
||||
setValue: (value: string) => void;
|
||||
onSubmit?: () => void;
|
||||
invertEnterBehavior?: boolean;
|
||||
};
|
||||
|
|
@ -79,21 +76,18 @@ const markdownDecorations = ViewPlugin.fromClass(
|
|||
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 elementRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewRef = useRef<EditorView | undefined>(undefined);
|
||||
const ignoreSyncRef = useRef(false);
|
||||
|
||||
const externalValue = createMemo(() => props.value);
|
||||
|
||||
onMount(() => {
|
||||
if (!element) return;
|
||||
useEffect(() => {
|
||||
if (!elementRef.current) return;
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: props.value,
|
||||
extensions: createEditorExtensions(
|
||||
(value) => {
|
||||
ignoreSync = true;
|
||||
ignoreSyncRef.current = true;
|
||||
props.setValue(value);
|
||||
},
|
||||
() => props.placeholder,
|
||||
|
|
@ -102,21 +96,28 @@ export default function Input(props: InputProps) {
|
|||
),
|
||||
});
|
||||
|
||||
view = new EditorView({
|
||||
viewRef.current = new EditorView({
|
||||
state,
|
||||
parent: element,
|
||||
parent: elementRef.current,
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const editor = view;
|
||||
return () => {
|
||||
viewRef.current?.destroy();
|
||||
viewRef.current = undefined;
|
||||
};
|
||||
// Run once to initialize/destroy the editor instance.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const editor = viewRef.current;
|
||||
if (!editor) return;
|
||||
|
||||
const next = externalValue();
|
||||
const next = props.value;
|
||||
const current = editor.state.doc.toString();
|
||||
|
||||
if (ignoreSync) {
|
||||
ignoreSync = false;
|
||||
if (ignoreSyncRef.current) {
|
||||
ignoreSyncRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -129,14 +130,9 @@ export default function Input(props: InputProps) {
|
|||
insert: next,
|
||||
},
|
||||
});
|
||||
});
|
||||
}, [props.value]);
|
||||
|
||||
onCleanup(() => {
|
||||
view?.destroy();
|
||||
view = undefined;
|
||||
});
|
||||
|
||||
return <div ref={element} class="tm-md-root" />;
|
||||
return <div ref={elementRef} className="tm-md-root" />;
|
||||
}
|
||||
|
||||
function createEditorExtensions(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { For, Index, type JSX } from "solid-js";
|
||||
import * as React from "react";
|
||||
|
||||
type InlineNode =
|
||||
| { type: "text"; value: string }
|
||||
|
|
@ -345,31 +345,53 @@ export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] {
|
|||
return blocks;
|
||||
}
|
||||
|
||||
export function renderInline(nodes: InlineNode[]): JSX.Element[] {
|
||||
return nodes.map((node) => {
|
||||
export function renderInline(nodes: InlineNode[]): React.ReactNode[] {
|
||||
return nodes.map((node, index) => {
|
||||
if (node.type === "text") {
|
||||
return node.value;
|
||||
}
|
||||
|
||||
if (node.type === "strong") {
|
||||
return <strong class="tm-md-strong">{node.value}</strong>;
|
||||
return (
|
||||
<strong key={index} className="tm-md-strong">
|
||||
{node.value}
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === "em") {
|
||||
return <em class="tm-md-em">{node.value}</em>;
|
||||
return (
|
||||
<em key={index} className="tm-md-em">
|
||||
{node.value}
|
||||
</em>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === "del") {
|
||||
return <del class="tm-md-del">{node.value}</del>;
|
||||
return (
|
||||
<del key={index} className="tm-md-del">
|
||||
{node.value}
|
||||
</del>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === "code") {
|
||||
return <code class="tm-md-code">{node.value}</code>;
|
||||
return (
|
||||
<code key={index} className="tm-md-code">
|
||||
{node.value}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === "link") {
|
||||
return (
|
||||
<a class="tm-md-link" href={node.href} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
key={index}
|
||||
className="tm-md-link"
|
||||
href={node.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{node.label}
|
||||
</a>
|
||||
);
|
||||
|
|
@ -377,7 +399,8 @@ export function renderInline(nodes: InlineNode[]): JSX.Element[] {
|
|||
|
||||
return (
|
||||
<img
|
||||
class="tm-md-image"
|
||||
key={index}
|
||||
className="tm-md-image"
|
||||
src={node.src}
|
||||
alt={node.alt}
|
||||
loading="lazy"
|
||||
|
|
@ -387,44 +410,44 @@ export function renderInline(nodes: InlineNode[]): JSX.Element[] {
|
|||
});
|
||||
}
|
||||
|
||||
export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
|
||||
export function renderBlocks(blocks: MarkdownBlock[]): React.ReactElement {
|
||||
return (
|
||||
<For each={blocks}>
|
||||
{(block) => {
|
||||
<>
|
||||
{blocks.map((block, blockIndex) => {
|
||||
if (block.type === "heading") {
|
||||
const className = `tm-md-heading tm-md-h${String(block.level)}`;
|
||||
if (block.level === 1)
|
||||
return (
|
||||
<h1 class={className}>
|
||||
<h1 key={blockIndex} className={className}>
|
||||
{renderInline(parseInlineNodes(block.text))}
|
||||
</h1>
|
||||
);
|
||||
if (block.level === 2)
|
||||
return (
|
||||
<h2 class={className}>
|
||||
<h2 key={blockIndex} className={className}>
|
||||
{renderInline(parseInlineNodes(block.text))}
|
||||
</h2>
|
||||
);
|
||||
if (block.level === 3)
|
||||
return (
|
||||
<h3 class={className}>
|
||||
<h3 key={blockIndex} className={className}>
|
||||
{renderInline(parseInlineNodes(block.text))}
|
||||
</h3>
|
||||
);
|
||||
if (block.level === 4)
|
||||
return (
|
||||
<h4 class={className}>
|
||||
<h4 key={blockIndex} className={className}>
|
||||
{renderInline(parseInlineNodes(block.text))}
|
||||
</h4>
|
||||
);
|
||||
if (block.level === 5)
|
||||
return (
|
||||
<h5 class={className}>
|
||||
<h5 key={blockIndex} className={className}>
|
||||
{renderInline(parseInlineNodes(block.text))}
|
||||
</h5>
|
||||
);
|
||||
return (
|
||||
<h6 class={className}>
|
||||
<h6 key={blockIndex} className={className}>
|
||||
{renderInline(parseInlineNodes(block.text))}
|
||||
</h6>
|
||||
);
|
||||
|
|
@ -432,18 +455,18 @@ export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
|
|||
|
||||
if (block.type === "blockquote") {
|
||||
return (
|
||||
<blockquote class="tm-md-blockquote">
|
||||
<For each={block.text.split("\n")}>
|
||||
{(line) => <p>{renderInline(parseInlineNodes(line))}</p>}
|
||||
</For>
|
||||
<blockquote key={blockIndex} className="tm-md-blockquote">
|
||||
{block.text.split("\n").map((line, lineIndex) => (
|
||||
<p key={lineIndex}>{renderInline(parseInlineNodes(line))}</p>
|
||||
))}
|
||||
</blockquote>
|
||||
);
|
||||
}
|
||||
|
||||
if (block.type === "code") {
|
||||
return (
|
||||
<pre class="tm-md-pre">
|
||||
<code class="tm-md-codeblock" data-language={block.language}>
|
||||
<pre key={blockIndex} className="tm-md-pre">
|
||||
<code className="tm-md-codeblock" data-language={block.language}>
|
||||
{block.code}
|
||||
</code>
|
||||
</pre>
|
||||
|
|
@ -453,51 +476,43 @@ export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
|
|||
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">
|
||||
<Tag key={blockIndex} className={block.ordered ? "tm-md-ol" : "tm-md-ul"}>
|
||||
{block.items.map((item, itemIndex) => (
|
||||
<li key={itemIndex} className="tm-md-li">
|
||||
{item.checked !== null ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
disabled
|
||||
class="tm-md-checkbox"
|
||||
className="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">
|
||||
<div key={blockIndex} className="tm-md-table-wrap">
|
||||
<table className="tm-md-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<For each={block.headers}>
|
||||
{(header) => (
|
||||
<th>{renderInline(parseInlineNodes(header))}</th>
|
||||
)}
|
||||
</For>
|
||||
{block.headers.map((header, headerIndex) => (
|
||||
<th key={headerIndex}>{renderInline(parseInlineNodes(header))}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={block.rows}>
|
||||
{(row) => (
|
||||
<tr>
|
||||
<For each={row}>
|
||||
{(cell) => (
|
||||
<td>{renderInline(parseInlineNodes(cell))}</td>
|
||||
)}
|
||||
</For>
|
||||
{block.rows.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={cellIndex}>{renderInline(parseInlineNodes(cell))}</td>
|
||||
))}
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -505,23 +520,21 @@ export function renderBlocks(blocks: MarkdownBlock[]): JSX.Element {
|
|||
}
|
||||
|
||||
if (block.type === "hr") {
|
||||
return <hr class="tm-md-hr" />;
|
||||
return <hr key={blockIndex} className="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 key={blockIndex} className="tm-md-p">
|
||||
{block.text.split("\n").map((line, lineIndex) => (
|
||||
<React.Fragment key={lineIndex}>
|
||||
{lineIndex > 0 ? <br /> : null}
|
||||
{renderInline(parseInlineNodes(line))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createMemo } from "solid-js";
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
ensureMarkdownStyles,
|
||||
|
|
@ -13,7 +13,7 @@ export type TextProps = {
|
|||
export default function Text(props: TextProps) {
|
||||
ensureMarkdownStyles();
|
||||
|
||||
const blocks = createMemo(() => parseMarkdownBlocks(props.value));
|
||||
const blocks = React.useMemo(() => parseMarkdownBlocks(props.value), [props.value]);
|
||||
|
||||
return <div class="tm-md-root">{renderBlocks(blocks())}</div>;
|
||||
return <div className="tm-md-root">{renderBlocks(blocks)}</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"dependencies": {
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"solid-js": "^1.9.11"
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
import {
|
||||
createContext,
|
||||
createSignal,
|
||||
Show,
|
||||
useContext,
|
||||
type ParentProps,
|
||||
} from "solid-js";
|
||||
import * as React from "react";
|
||||
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 {
|
||||
|
|
@ -23,109 +16,121 @@ interface StorageContextValue {
|
|||
clear: () => Promise<void>;
|
||||
}
|
||||
|
||||
const StorageContext = createContext<StorageContextValue>();
|
||||
const StorageContext = React.createContext<StorageContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const isIndexedDBSupported = typeof indexedDB !== "undefined";
|
||||
|
||||
export default function StorageProvider(props: ParentProps) {
|
||||
const [storage, setStorage] = createStore<StorageSchema>(defaults);
|
||||
export default function StorageProvider(props: { children: React.ReactNode }) {
|
||||
const [storage, setStorage] = React.useState<StorageSchema>(defaults);
|
||||
const storageRef = React.useRef(storage);
|
||||
|
||||
const [error, setError] = createSignal<string>("");
|
||||
const [errorDescription, setErrorDescription] = createSignal<string>("");
|
||||
const [error, setError] = React.useState("");
|
||||
const [errorDescription, setErrorDescription] = React.useState("");
|
||||
|
||||
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];
|
||||
}
|
||||
React.useEffect(() => {
|
||||
storageRef.current = storage;
|
||||
}, [storage]);
|
||||
|
||||
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 loadIO = React.useCallback(
|
||||
async <K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]> => {
|
||||
let stored: StorageSchema[K] | undefined;
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
return storage[key];
|
||||
},
|
||||
if (stored !== undefined) {
|
||||
setStorage((prev) => ({ ...prev, [key]: stored }));
|
||||
return stored;
|
||||
}
|
||||
|
||||
async save<K extends keyof StorageSchema>(
|
||||
return defaults[key];
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const saveIO = React.useCallback(
|
||||
async <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>
|
||||
): Promise<void> => {
|
||||
if (JSON.stringify(value) === JSON.stringify(defaults[key])) {
|
||||
await deleteEntry(key);
|
||||
setStorage((prev) => ({ ...prev, [key]: defaults[key] }));
|
||||
} else {
|
||||
await setEntry(key, value);
|
||||
setStorage((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
>
|
||||
<ErrorScreen error={error()} description={errorDescription()} />
|
||||
</Show>
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const value = React.useMemo<StorageContextValue>(
|
||||
() => ({
|
||||
async load<K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]> {
|
||||
const current = storageRef.current[key];
|
||||
|
||||
if (
|
||||
current === undefined ||
|
||||
JSON.stringify(current) === JSON.stringify(defaults[key])
|
||||
) {
|
||||
const loadedValue = await loadIO(key);
|
||||
setStorage((prev) => ({ ...prev, [key]: loadedValue }));
|
||||
return loadedValue;
|
||||
}
|
||||
|
||||
return current;
|
||||
},
|
||||
|
||||
async save<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
nextValue: StorageSchema[K],
|
||||
): Promise<void> {
|
||||
await saveIO(key, nextValue);
|
||||
},
|
||||
|
||||
async clear() {
|
||||
const keys = Object.keys(defaults) as (keyof StorageSchema)[];
|
||||
await Promise.all(keys.map((key) => deleteEntry(key)));
|
||||
setStorage(defaults);
|
||||
},
|
||||
}),
|
||||
[loadIO, saveIO],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// @ts-expect-error development utility
|
||||
window.save = value.save;
|
||||
}, [value]);
|
||||
|
||||
if (error !== "" && errorDescription !== "") {
|
||||
return <ErrorScreen error={error} description={errorDescription} />;
|
||||
}
|
||||
|
||||
if (!isIndexedDBSupported) {
|
||||
return (
|
||||
<ErrorScreen
|
||||
error="Unsupported Browser"
|
||||
description="Your browser does not support IndexedDB, which is required for this application to function."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <StorageContext.Provider value={value}>{props.children}</StorageContext.Provider>;
|
||||
}
|
||||
|
||||
export function useStorage(): StorageContextValue {
|
||||
const context = useContext(StorageContext);
|
||||
const context = React.useContext(StorageContext);
|
||||
if (!context) {
|
||||
throw new Error("useStorage must be used within a StorageProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@
|
|||
"build": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.15.4",
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@tensamin/core-crypto": "workspace:*",
|
||||
"@tensamin/core-storage": "workspace:*",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"solid-js": "^1.9.10",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { log } from "@tensamin/shared/log";
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
Show,
|
||||
untrack,
|
||||
useContext,
|
||||
type ParentProps,
|
||||
} from "solid-js";
|
||||
|
||||
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
|
||||
import {
|
||||
PING_INTERVAL,
|
||||
|
|
@ -21,17 +11,8 @@ import {
|
|||
socket as schemas,
|
||||
type Socket as Schemas,
|
||||
} from "@tensamin/shared/data";
|
||||
import { useStorage } from "@tensamin/core-storage/context";
|
||||
import { useCrypto } from "@tensamin/core-crypto/context";
|
||||
import Loading from "@tensamin/ui/screens/loading";
|
||||
import ErrorScreen from "@tensamin/ui/screens/error";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
|
||||
type OmikronData = {
|
||||
id: number;
|
||||
public_key: string;
|
||||
ip_address: string;
|
||||
};
|
||||
|
||||
type ContextType = {
|
||||
send: BoundSendFn<Schemas>;
|
||||
|
|
@ -40,273 +21,217 @@ type ContextType = {
|
|||
iotaPing: () => number;
|
||||
};
|
||||
|
||||
const socketContext = createContext<ContextType>();
|
||||
const socketContext = React.createContext<ContextType | undefined>(undefined);
|
||||
|
||||
export default function Provider(props: ParentProps) {
|
||||
const [omikron, setOmikron] = createSignal<OmikronData | null>(null);
|
||||
const [readyState, setReadyState] = createSignal<number>(READY_STATE.CLOSED);
|
||||
const [identified, setIdentified] = createSignal<boolean>(false);
|
||||
export default function Provider(props: { children: React.ReactNode }) {
|
||||
const [readyState, setReadyState] = React.useState<number>(READY_STATE.CLOSED);
|
||||
const [connected, setConnected] = React.useState<boolean>(false);
|
||||
|
||||
const [ownPing, setOwnPing] = createSignal<number>(0);
|
||||
const [iotaPing, setIotaPing] = createSignal<number>(0);
|
||||
const [ownPing, setOwnPing] = React.useState<number>(0);
|
||||
const [iotaPing, setIotaPing] = React.useState<number>(0);
|
||||
|
||||
const [error, setError] = createSignal<string>("");
|
||||
const [errorDescription, setErrorDescription] = createSignal<string>("");
|
||||
const [error, setError] = React.useState("");
|
||||
const [errorDescription, setErrorDescription] = React.useState("");
|
||||
|
||||
const { load } = useStorage();
|
||||
const { get_shared_secret, decrypt } = useCrypto();
|
||||
const clientRef = React.useRef<ReturnType<
|
||||
typeof createTransportClient<Schemas>
|
||||
> | null>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const send = React.useCallback<BoundSendFn<Schemas>>(
|
||||
((
|
||||
type: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: { id?: number; noResponse?: boolean },
|
||||
) => {
|
||||
const client = clientRef.current;
|
||||
|
||||
let client: ReturnType<typeof createTransportClient<Schemas>> | null = null;
|
||||
|
||||
// Load Omikron
|
||||
createEffect(() => {
|
||||
if (omikron()) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const userId = await load("user_id");
|
||||
|
||||
// Redirect to login
|
||||
if (userId === 0) {
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
"https://omega.tensamin.net/api/get/omikron/" + String(userId),
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
const data = await res.json();
|
||||
setOmikron(data);
|
||||
} catch (e) {
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
setError("Failed to load Omikron data");
|
||||
setErrorDescription(
|
||||
"An error occurred while fetching the Omikron server data. Please try again later.",
|
||||
);
|
||||
log(0, "Socket", "red", "Failed to fetch Omikron data", e);
|
||||
if (!client) {
|
||||
return Promise.reject(new Error("Socket is not connected"));
|
||||
}
|
||||
})();
|
||||
|
||||
onCleanup(() => controller.abort());
|
||||
});
|
||||
if (options?.noResponse) {
|
||||
return client.send(type as keyof Schemas & string, data as never, {
|
||||
...options,
|
||||
noResponse: true,
|
||||
});
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (identified()) {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const originalNow = Date.now();
|
||||
return client.send(type as keyof Schemas & string, data as never, {
|
||||
...options,
|
||||
noResponse: false,
|
||||
});
|
||||
}) as BoundSendFn<Schemas>,
|
||||
[],
|
||||
);
|
||||
|
||||
const data = await send("ping", {
|
||||
last_ping: originalNow,
|
||||
});
|
||||
|
||||
const travelTime = Date.now() - originalNow;
|
||||
|
||||
setOwnPing(travelTime);
|
||||
setIotaPing(data.data.ping_iota);
|
||||
} catch (error) {
|
||||
log(1, "Socket", "yellow", "Ping failed", error);
|
||||
}
|
||||
}, PING_INTERVAL);
|
||||
|
||||
onCleanup(() => clearInterval(interval));
|
||||
React.useEffect(() => {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Create connection
|
||||
createEffect(() => {
|
||||
if (!omikron()) return;
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const originalNow = Date.now();
|
||||
|
||||
const data = await send("ping", {
|
||||
last_ping: originalNow,
|
||||
});
|
||||
|
||||
const travelTime = Date.now() - originalNow;
|
||||
setOwnPing(travelTime);
|
||||
|
||||
const remotePing = data.data.ping_iota;
|
||||
if (typeof remotePing === "number") {
|
||||
setIotaPing(remotePing);
|
||||
}
|
||||
} catch (intervalError) {
|
||||
log(1, "Socket", "yellow", "Ping failed", intervalError);
|
||||
}
|
||||
}, PING_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [connected, send]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let attempts = 0;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let reconnectScheduled = false;
|
||||
let disposed = false;
|
||||
|
||||
const clearReconnectTimer = () => {
|
||||
if (!reconnectTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
reconnectScheduled = false;
|
||||
};
|
||||
|
||||
const scheduleReconnect = (reason?: unknown) => {
|
||||
if (disposed || reconnectScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempts >= RETRY_COUNT) {
|
||||
setError("Connection Failed");
|
||||
setErrorDescription(
|
||||
"Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.",
|
||||
);
|
||||
log(0, "Socket", "red", "Reconnection attempts exhausted", reason);
|
||||
return;
|
||||
}
|
||||
|
||||
attempts += 1;
|
||||
reconnectScheduled = true;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectScheduled = false;
|
||||
reconnectTimer = null;
|
||||
void connect();
|
||||
}, RETRY_INTERVAL);
|
||||
};
|
||||
|
||||
const transportClient = createTransportClient(schemas, {
|
||||
url: TRANSPORT_URL,
|
||||
onReadyStateChange: setReadyState,
|
||||
onReadyStateChange: (state) => {
|
||||
setReadyState(state);
|
||||
|
||||
if (state === READY_STATE.OPEN) {
|
||||
attempts = 0;
|
||||
clearReconnectTimer();
|
||||
setConnected(true);
|
||||
setError("");
|
||||
setErrorDescription("");
|
||||
log(1, "Socket", "green", "Connected");
|
||||
return;
|
||||
}
|
||||
|
||||
setConnected(false);
|
||||
},
|
||||
onClose: ({ error: closeError, intentional }) => {
|
||||
setIdentified(false);
|
||||
setConnected(false);
|
||||
|
||||
if (disposed || intentional) {
|
||||
return;
|
||||
}
|
||||
|
||||
log(0, "Socket", "red", "Disconnected", closeError);
|
||||
|
||||
if (attempts < RETRY_COUNT) {
|
||||
attempts += 1;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
void connect();
|
||||
}, RETRY_INTERVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
setError("Connection Failed");
|
||||
setErrorDescription(
|
||||
"Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.",
|
||||
);
|
||||
log(0, "Socket", "red", "Reconnection attempts exhausted", closeError);
|
||||
scheduleReconnect(closeError);
|
||||
},
|
||||
});
|
||||
|
||||
client = transportClient;
|
||||
|
||||
async function identify(activeClient: typeof transportClient) {
|
||||
const userId = await load("user_id");
|
||||
const privateKey = await load("private_key");
|
||||
const currentOmikron = untrack(() => omikron());
|
||||
|
||||
activeClient
|
||||
.send("identification", { user_id: userId })
|
||||
.then(async (data) => {
|
||||
const ownUserData = await activeClient.send("get_user_data", {
|
||||
user_id: userId,
|
||||
});
|
||||
|
||||
if (!currentOmikron?.public_key) {
|
||||
setError("Omikron data missing");
|
||||
setErrorDescription(
|
||||
"Omikron server data is missing. Please try again later.",
|
||||
);
|
||||
log(0, "Socket", "red", "Omikron public key missing");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sharedSecret = await get_shared_secret(
|
||||
privateKey,
|
||||
ownUserData.data.public_key,
|
||||
currentOmikron.public_key,
|
||||
);
|
||||
|
||||
const solvedChallenge = await decrypt(
|
||||
sharedSecret,
|
||||
data.data.challenge,
|
||||
);
|
||||
|
||||
activeClient
|
||||
.send("challenge_response", {
|
||||
challenge: btoa(solvedChallenge),
|
||||
})
|
||||
.then(() => {
|
||||
if (client !== activeClient || disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
log(1, "Socket", "green", "Identification successful");
|
||||
setIdentified(true);
|
||||
setError("");
|
||||
setErrorDescription("");
|
||||
})
|
||||
.catch((error) => {
|
||||
log(0, "Socket", "red", "Challenge failed", error);
|
||||
setError("Challenge Failed");
|
||||
setErrorDescription(
|
||||
"Failed to respond to the server's challenge. Please try again later.",
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
setError("Validation Failed");
|
||||
setErrorDescription(
|
||||
"Failed to validate the server's identity. Please try again later.",
|
||||
);
|
||||
log(0, "Socket", "red", "Server identity validation failed", err);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
log(0, "Socket", "red", "Identification failed", e);
|
||||
setError("Identification Failed");
|
||||
setErrorDescription(
|
||||
"Failed to identify with the server. Please try again later.",
|
||||
);
|
||||
});
|
||||
}
|
||||
clientRef.current = transportClient;
|
||||
|
||||
async function connect() {
|
||||
if (disposed) return;
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await transportClient.connect(TRANSPORT_URL);
|
||||
attempts = 0;
|
||||
await identify(transportClient);
|
||||
} catch (error) {
|
||||
if (!disposed) {
|
||||
log(0, "Socket", "red", "Connection attempt failed", error);
|
||||
} catch (connectError) {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
log(0, "Socket", "red", "Connection attempt failed", connectError);
|
||||
scheduleReconnect(connectError);
|
||||
}
|
||||
}
|
||||
|
||||
void connect();
|
||||
|
||||
onCleanup(() => {
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
if (client === transportClient) {
|
||||
client = null;
|
||||
clearReconnectTimer();
|
||||
|
||||
if (clientRef.current === transportClient) {
|
||||
clientRef.current = null;
|
||||
}
|
||||
|
||||
void transportClient.close("context-dispose");
|
||||
setReadyState(READY_STATE.CLOSED);
|
||||
setIdentified(false);
|
||||
});
|
||||
});
|
||||
setConnected(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Create Send Function
|
||||
const send: BoundSendFn<Schemas> = ((
|
||||
type: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: { id?: number; noResponse?: boolean },
|
||||
) => {
|
||||
if (!client) {
|
||||
return Promise.reject(new Error("Socket is not connected"));
|
||||
}
|
||||
|
||||
if (options?.noResponse) {
|
||||
return client.send(type as keyof Schemas & string, data as never, {
|
||||
...options,
|
||||
noResponse: true,
|
||||
});
|
||||
}
|
||||
return client.send(type as keyof Schemas & string, data as never, {
|
||||
...options,
|
||||
noResponse: false,
|
||||
});
|
||||
}) as BoundSendFn<Schemas>;
|
||||
|
||||
const progress = () => {
|
||||
if (!omikron()) return 40;
|
||||
if (readyState() !== READY_STATE.OPEN) return 70;
|
||||
if (!identified()) return 90;
|
||||
const progress = React.useMemo(() => {
|
||||
if (readyState === READY_STATE.CONNECTING) return 70;
|
||||
if (!connected) return 90;
|
||||
return 100;
|
||||
};
|
||||
}, [connected, readyState]);
|
||||
|
||||
const contextValue = React.useMemo<ContextType>(
|
||||
() => ({
|
||||
send,
|
||||
readyState: () => readyState,
|
||||
ownPing: () => ownPing,
|
||||
iotaPing: () => iotaPing,
|
||||
}),
|
||||
[iotaPing, ownPing, readyState, send],
|
||||
);
|
||||
|
||||
if (error !== "" && errorDescription !== "") {
|
||||
return <ErrorScreen error={error} description={errorDescription} />;
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
return <Loading progress={progress} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={error() === "" && errorDescription() === ""}
|
||||
fallback={
|
||||
<ErrorScreen error={error()} description={errorDescription()} />
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={omikron() && identified()}
|
||||
fallback={<Loading progress={progress()} />}
|
||||
>
|
||||
<socketContext.Provider value={{ send, readyState, ownPing, iotaPing }}>
|
||||
{props.children}
|
||||
</socketContext.Provider>
|
||||
</Show>
|
||||
</Show>
|
||||
<socketContext.Provider value={contextValue}>
|
||||
{props.children}
|
||||
</socketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSocket(): ContextType {
|
||||
const context = useContext(socketContext);
|
||||
if (!context)
|
||||
const context = React.useContext(socketContext);
|
||||
if (!context) {
|
||||
throw new Error("useSocket must be used within a SocketProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,14 @@ const APPLICATION_CLOSE_CODE = 0;
|
|||
const APPLICATION_CLOSE_REASON = "epsilon-close";
|
||||
const MAX_REQUEST_ID = 0xffff_fffe;
|
||||
|
||||
const DATA_VALUE_KIND_BOOL_TRUE = 0x01;
|
||||
const DATA_VALUE_KIND_BOOL_FALSE = 0x02;
|
||||
const DATA_VALUE_KIND_NUMBER = 0x03;
|
||||
const DATA_VALUE_KIND_STRING = 0x04;
|
||||
const DATA_VALUE_KIND_ARRAY = 0x05;
|
||||
const DATA_VALUE_KIND_CONTAINER = 0x06;
|
||||
const DATA_VALUE_KIND_NULL = 0x07;
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
|
|
@ -966,34 +974,40 @@ function encodeCommunicationMessage(
|
|||
message: TypedMessage<Record<string, unknown>>,
|
||||
) {
|
||||
const typeIndex = parseCommunicationType(message.type);
|
||||
const dataBuffer = encodeValueForKind("container", message.data, "payload");
|
||||
const hasId = message.id !== 0;
|
||||
const totalLength = 2 + (hasId ? 4 : 0) + 4 + dataBuffer.byteLength;
|
||||
const buffer = new Uint8Array(totalLength);
|
||||
const dataBuffer = encodeContainerPayload(message.data, "payload");
|
||||
const payloadLength = 2 + (hasId ? 4 : 0) + dataBuffer.byteLength;
|
||||
const buffer = new Uint8Array(4 + payloadLength);
|
||||
|
||||
buffer[0] = typeIndex;
|
||||
buffer[1] = hasId ? 0b0010_0000 : 0;
|
||||
writeU32(buffer, 0, payloadLength);
|
||||
buffer[4] = typeIndex;
|
||||
buffer[5] = hasId ? 0b0000_0100 : 0;
|
||||
|
||||
let offset = 2;
|
||||
let offset = 6;
|
||||
if (hasId) {
|
||||
writeU32(buffer, offset, message.id);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
writeU32(buffer, offset, dataBuffer.byteLength);
|
||||
offset += 4;
|
||||
buffer.set(dataBuffer, offset);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function decodeCommunicationMessage(payload: Uint8Array): TypedMessage {
|
||||
const reader = new ByteReader(payload);
|
||||
function decodeCommunicationMessage(frame: Uint8Array): TypedMessage {
|
||||
const reader = new ByteReader(frame);
|
||||
const payloadLength = reader.readU32();
|
||||
if (payloadLength !== frame.byteLength - 4) {
|
||||
throw new Error(
|
||||
`Communication payload length mismatch: expected ${payloadLength}, received ${frame.byteLength - 4}`,
|
||||
);
|
||||
}
|
||||
|
||||
const typeIndex = reader.readU8();
|
||||
const flags = reader.readU8();
|
||||
const hasSender = (flags & 0b1000_0000) !== 0;
|
||||
const hasReceiver = (flags & 0b0100_0000) !== 0;
|
||||
const hasId = (flags & 0b0010_0000) !== 0;
|
||||
const hasSender = (flags & 0b0000_0001) !== 0;
|
||||
const hasReceiver = (flags & 0b0000_0010) !== 0;
|
||||
const hasId = (flags & 0b0000_0100) !== 0;
|
||||
|
||||
const id = hasId ? reader.readU32() : 0;
|
||||
if (hasSender) {
|
||||
|
|
@ -1003,11 +1017,18 @@ function decodeCommunicationMessage(payload: Uint8Array): TypedMessage {
|
|||
reader.readU48();
|
||||
}
|
||||
|
||||
const dataLength = reader.readU32();
|
||||
const dataBytes = reader.readBytes(dataLength);
|
||||
const decodedData = decodeValue(new ByteReader(dataBytes));
|
||||
if (!isPlainObject(decodedData)) {
|
||||
throw new Error("Protocol payload container was not decoded as an object");
|
||||
const consumedHeaderBytes =
|
||||
2 + (hasId ? 4 : 0) + (hasSender ? 6 : 0) + (hasReceiver ? 6 : 0);
|
||||
if (consumedHeaderBytes > payloadLength) {
|
||||
throw new Error("Communication header exceeds payload length");
|
||||
}
|
||||
|
||||
const dataLength = payloadLength - consumedHeaderBytes;
|
||||
const dataReader = new ByteReader(reader.readBytes(dataLength));
|
||||
const decodedData = decodeContainerPayload(dataReader);
|
||||
|
||||
if (!dataReader.isAtEnd()) {
|
||||
throw new Error("Trailing bytes found after communication data payload");
|
||||
}
|
||||
|
||||
if (!reader.isAtEnd()) {
|
||||
|
|
@ -1051,13 +1072,21 @@ function getExpectedKind(type: string) {
|
|||
return kind;
|
||||
}
|
||||
|
||||
function encodeValueForKind(
|
||||
type EncodedDataValue = {
|
||||
kind: number;
|
||||
payload: Uint8Array;
|
||||
};
|
||||
|
||||
function encodeDataValueForKind(
|
||||
kind: DataKind,
|
||||
value: unknown,
|
||||
path: string,
|
||||
): Uint8Array {
|
||||
): EncodedDataValue {
|
||||
if (typeof kind === "object") {
|
||||
return encodeArrayValue(kind.array, value, path);
|
||||
return {
|
||||
kind: DATA_VALUE_KIND_ARRAY,
|
||||
payload: encodeArrayPayload(kind.array, value, path),
|
||||
};
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
|
|
@ -1066,35 +1095,50 @@ function encodeValueForKind(
|
|||
throw new Error(`Expected boolean at "${path}"`);
|
||||
}
|
||||
|
||||
return Uint8Array.of(0x03, value ? 1 : 0);
|
||||
return {
|
||||
kind: value ? DATA_VALUE_KIND_BOOL_TRUE : DATA_VALUE_KIND_BOOL_FALSE,
|
||||
payload: new Uint8Array(0),
|
||||
};
|
||||
|
||||
case "number":
|
||||
return encodeNumberValue(value, path);
|
||||
return {
|
||||
kind: DATA_VALUE_KIND_NUMBER,
|
||||
payload: encodeNumberPayload(value, path),
|
||||
};
|
||||
|
||||
case "string":
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`Expected string at "${path}"`);
|
||||
}
|
||||
|
||||
return encodeStringValue(value);
|
||||
return {
|
||||
kind: DATA_VALUE_KIND_STRING,
|
||||
payload: textEncoder.encode(value),
|
||||
};
|
||||
|
||||
case "container":
|
||||
if (!isPlainObject(value)) {
|
||||
throw new Error(`Expected object at "${path}"`);
|
||||
}
|
||||
|
||||
return encodeContainerValue(value, path);
|
||||
return {
|
||||
kind: DATA_VALUE_KIND_CONTAINER,
|
||||
payload: encodeContainerPayload(value, path),
|
||||
};
|
||||
|
||||
case "null":
|
||||
if (value !== null && value !== undefined) {
|
||||
throw new Error(`Expected null at "${path}"`);
|
||||
}
|
||||
|
||||
return Uint8Array.of(0x06);
|
||||
return {
|
||||
kind: DATA_VALUE_KIND_NULL,
|
||||
payload: new Uint8Array(0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function encodeNumberValue(value: unknown, path: string) {
|
||||
function encodeNumberPayload(value: unknown, path: string) {
|
||||
if (
|
||||
typeof value !== "number" ||
|
||||
!Number.isFinite(value) ||
|
||||
|
|
@ -1103,22 +1147,12 @@ function encodeNumberValue(value: unknown, path: string) {
|
|||
throw new Error(`Expected safe integer at "${path}"`);
|
||||
}
|
||||
|
||||
const buffer = new Uint8Array(9);
|
||||
buffer[0] = 0x01;
|
||||
writeI64(buffer, 1, value);
|
||||
const buffer = new Uint8Array(8);
|
||||
writeI64(buffer, 0, value);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function encodeStringValue(value: string) {
|
||||
const bytes = textEncoder.encode(value);
|
||||
const buffer = new Uint8Array(5 + bytes.byteLength);
|
||||
buffer[0] = 0x02;
|
||||
writeU32(buffer, 1, bytes.byteLength);
|
||||
buffer.set(bytes, 5);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function encodeArrayValue(
|
||||
function encodeArrayPayload(
|
||||
innerKind: PrimitiveDataKind | "container" | "null",
|
||||
value: unknown,
|
||||
path: string,
|
||||
|
|
@ -1132,28 +1166,44 @@ function encodeArrayValue(
|
|||
}
|
||||
|
||||
const encodedItems = value.map((entry, index) =>
|
||||
encodeValueForKind(innerKind, entry, `${path}[${index}]`),
|
||||
encodeDataValueForKind(innerKind, entry, `${path}[${index}]`),
|
||||
);
|
||||
|
||||
const byteLength = encodedItems.reduce(
|
||||
(sum, item) => sum + item.byteLength,
|
||||
0,
|
||||
);
|
||||
const buffer = new Uint8Array(4 + byteLength);
|
||||
buffer[0] = 0x04;
|
||||
buffer[1] = value.length === 0 ? 0x00 : kindToMarker(innerKind);
|
||||
writeU16(buffer, 2, value.length);
|
||||
let totalLength = 2;
|
||||
for (const encodedItem of encodedItems) {
|
||||
totalLength += 1;
|
||||
|
||||
let offset = 4;
|
||||
if (!isBoolKindMarker(encodedItem.kind)) {
|
||||
if (encodedItem.payload.byteLength > 0xffff) {
|
||||
throw new Error(`Array item at "${path}" is too large for protocol encoding`);
|
||||
}
|
||||
|
||||
totalLength += 2 + encodedItem.payload.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = new Uint8Array(totalLength);
|
||||
writeU16(buffer, 0, value.length);
|
||||
|
||||
let offset = 2;
|
||||
for (const item of encodedItems) {
|
||||
buffer.set(item, offset);
|
||||
offset += item.byteLength;
|
||||
buffer[offset] = item.kind;
|
||||
offset += 1;
|
||||
|
||||
if (isBoolKindMarker(item.kind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
writeU16(buffer, offset, item.payload.byteLength);
|
||||
offset += 2;
|
||||
buffer.set(item.payload, offset);
|
||||
offset += item.payload.byteLength;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function encodeContainerValue(value: Record<string, unknown>, path: string) {
|
||||
function encodeContainerPayload(value: Record<string, unknown>, path: string) {
|
||||
const normalizedEntries = new Map<
|
||||
string,
|
||||
{ index: number; value: unknown }
|
||||
|
|
@ -1171,82 +1221,103 @@ function encodeContainerValue(value: Record<string, unknown>, path: string) {
|
|||
([, left], [, right]) => left.index - right.index,
|
||||
);
|
||||
|
||||
const encodedEntries: Uint8Array[] = [];
|
||||
let totalLength = 3;
|
||||
if (entries.length > 0xffff) {
|
||||
throw new Error(`Container at "${path}" has too many entries for protocol encoding`);
|
||||
}
|
||||
|
||||
const encodedEntries: Array<{ keyIndex: number; value: EncodedDataValue }> = [];
|
||||
let totalLength = 2;
|
||||
|
||||
for (const [name, entry] of entries) {
|
||||
const expectedKind = getExpectedKind(name);
|
||||
const pathForEntry = `${path}.${name}`;
|
||||
|
||||
if (expectedKind === "bool") {
|
||||
if (typeof entry.value !== "boolean") {
|
||||
throw new Error(`Expected boolean at "${pathForEntry}"`);
|
||||
}
|
||||
|
||||
const encoded = Uint8Array.of(entry.index, entry.value ? 1 : 0);
|
||||
encodedEntries.push(encoded);
|
||||
totalLength += encoded.byteLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
const encodedValue = encodeValueForKind(
|
||||
const encodedValue = encodeDataValueForKind(
|
||||
expectedKind,
|
||||
entry.value,
|
||||
pathForEntry,
|
||||
);
|
||||
const body = encodedValue.subarray(1);
|
||||
const encoded = new Uint8Array(5 + body.byteLength);
|
||||
encoded[0] = entry.index;
|
||||
writeU32(encoded, 1, body.byteLength);
|
||||
encoded.set(body, 5);
|
||||
encodedEntries.push(encoded);
|
||||
totalLength += encoded.byteLength;
|
||||
|
||||
if (isBoolKindMarker(encodedValue.kind)) {
|
||||
totalLength += 2;
|
||||
} else {
|
||||
if (encodedValue.payload.byteLength > 0xffff) {
|
||||
throw new Error(`Container entry "${pathForEntry}" is too large for protocol encoding`);
|
||||
}
|
||||
|
||||
totalLength += 4 + encodedValue.payload.byteLength;
|
||||
}
|
||||
|
||||
encodedEntries.push({
|
||||
keyIndex: entry.index,
|
||||
value: encodedValue,
|
||||
});
|
||||
}
|
||||
|
||||
const buffer = new Uint8Array(totalLength);
|
||||
buffer[0] = 0x05;
|
||||
writeU16(buffer, 1, entries.length);
|
||||
writeU16(buffer, 0, entries.length);
|
||||
|
||||
let offset = 3;
|
||||
for (const encoded of encodedEntries) {
|
||||
buffer.set(encoded, offset);
|
||||
offset += encoded.byteLength;
|
||||
let offset = 2;
|
||||
for (const entry of encodedEntries) {
|
||||
buffer[offset] = entry.value.kind;
|
||||
offset += 1;
|
||||
|
||||
if (isBoolKindMarker(entry.value.kind)) {
|
||||
buffer[offset] = entry.keyIndex;
|
||||
offset += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
writeU16(buffer, offset, entry.value.payload.byteLength);
|
||||
offset += 2;
|
||||
buffer[offset] = entry.keyIndex;
|
||||
offset += 1;
|
||||
buffer.set(entry.value.payload, offset);
|
||||
offset += entry.value.payload.byteLength;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function decodeValue(reader: ByteReader): unknown {
|
||||
const marker = reader.readU8();
|
||||
function decodeValuePayload(marker: number, payload: Uint8Array): unknown {
|
||||
const reader = new ByteReader(payload);
|
||||
|
||||
switch (marker) {
|
||||
case 0x01:
|
||||
return reader.readI64();
|
||||
|
||||
case 0x02: {
|
||||
const length = reader.readU32();
|
||||
return textDecoder.decode(reader.readBytes(length));
|
||||
}
|
||||
|
||||
case 0x03:
|
||||
return reader.readU8() !== 0;
|
||||
|
||||
case 0x04: {
|
||||
reader.readU8();
|
||||
const length = reader.readU16();
|
||||
const values: unknown[] = [];
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
values.push(decodeValue(reader));
|
||||
case DATA_VALUE_KIND_BOOL_TRUE:
|
||||
if (!reader.isAtEnd()) {
|
||||
throw new Error("Unexpected payload for boolean true value");
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
return true;
|
||||
|
||||
case 0x05:
|
||||
return decodeContainer(reader);
|
||||
case DATA_VALUE_KIND_BOOL_FALSE:
|
||||
if (!reader.isAtEnd()) {
|
||||
throw new Error("Unexpected payload for boolean false value");
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case DATA_VALUE_KIND_NUMBER:
|
||||
if (payload.byteLength !== 8) {
|
||||
throw new Error(`Invalid number payload length ${payload.byteLength}`);
|
||||
}
|
||||
|
||||
return reader.readI64();
|
||||
|
||||
case DATA_VALUE_KIND_STRING:
|
||||
return textDecoder.decode(payload);
|
||||
|
||||
case DATA_VALUE_KIND_ARRAY:
|
||||
return decodeArrayPayload(reader);
|
||||
|
||||
case DATA_VALUE_KIND_CONTAINER:
|
||||
return decodeContainerPayload(reader);
|
||||
|
||||
case DATA_VALUE_KIND_NULL:
|
||||
if (!reader.isAtEnd()) {
|
||||
throw new Error("Unexpected payload for null value");
|
||||
}
|
||||
|
||||
case 0x06:
|
||||
return null;
|
||||
|
||||
default:
|
||||
|
|
@ -1254,39 +1325,86 @@ function decodeValue(reader: ByteReader): unknown {
|
|||
}
|
||||
}
|
||||
|
||||
function decodeContainer(reader: ByteReader) {
|
||||
const length = reader.readU16();
|
||||
const value: Record<string, unknown> = {};
|
||||
function decodeArrayPayload(reader: ByteReader) {
|
||||
const itemCount = reader.readU16();
|
||||
const values: unknown[] = [];
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const keyIndex = reader.readU8();
|
||||
const key = DATA_TYPES[keyIndex];
|
||||
if (!key) {
|
||||
throw new Error(`Unknown data type index ${keyIndex}`);
|
||||
}
|
||||
for (let index = 0; index < itemCount; index += 1) {
|
||||
const marker = reader.readU8();
|
||||
|
||||
const expectedKind = getExpectedKind(key);
|
||||
|
||||
if (expectedKind === "bool") {
|
||||
value[key] = normalizeIncomingValue(key, reader.readU8() !== 0);
|
||||
if (isBoolKindMarker(marker)) {
|
||||
values.push(marker === DATA_VALUE_KIND_BOOL_TRUE);
|
||||
continue;
|
||||
}
|
||||
|
||||
const encodedLength = reader.readU32();
|
||||
const encodedValue = reader.readBytes(encodedLength);
|
||||
const fullValue = new Uint8Array(1 + encodedValue.byteLength);
|
||||
fullValue[0] = kindToMarker(expectedKind);
|
||||
fullValue.set(encodedValue, 1);
|
||||
const payloadLength = reader.readU16();
|
||||
const payload = reader.readBytes(payloadLength);
|
||||
values.push(decodeValuePayload(marker, payload));
|
||||
}
|
||||
|
||||
value[key] = normalizeIncomingValue(
|
||||
key,
|
||||
decodeValue(new ByteReader(fullValue)),
|
||||
);
|
||||
return values;
|
||||
}
|
||||
|
||||
function decodeContainerPayload(reader: ByteReader) {
|
||||
const entryCount = reader.readU16();
|
||||
const value: Record<string, unknown> = {};
|
||||
|
||||
for (let index = 0; index < entryCount; index += 1) {
|
||||
const marker = reader.readU8();
|
||||
const payloadLength = isBoolKindMarker(marker) ? 0 : reader.readU16();
|
||||
const keyIndex = reader.readU8();
|
||||
const key = getDataTypeNameByIndex(keyIndex);
|
||||
const payload = isBoolKindMarker(marker)
|
||||
? new Uint8Array(0)
|
||||
: reader.readBytes(payloadLength);
|
||||
|
||||
const expectedKind = getExpectedKind(key);
|
||||
if (!isMarkerCompatibleWithKind(marker, expectedKind)) {
|
||||
throw new Error(
|
||||
`Unexpected marker 0x${marker.toString(16)} for data type "${key}"`,
|
||||
);
|
||||
}
|
||||
|
||||
value[key] = normalizeIncomingValue(key, decodeValuePayload(marker, payload));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getDataTypeNameByIndex(index: number) {
|
||||
const key = DATA_TYPES[index];
|
||||
if (!key) {
|
||||
throw new Error(`Unknown data type index ${index}`);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
function isBoolKindMarker(marker: number) {
|
||||
return (
|
||||
marker === DATA_VALUE_KIND_BOOL_TRUE || marker === DATA_VALUE_KIND_BOOL_FALSE
|
||||
);
|
||||
}
|
||||
|
||||
function isMarkerCompatibleWithKind(marker: number, kind: DataKind) {
|
||||
if (typeof kind === "object") {
|
||||
return marker === DATA_VALUE_KIND_ARRAY;
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case "bool":
|
||||
return isBoolKindMarker(marker);
|
||||
case "number":
|
||||
return marker === DATA_VALUE_KIND_NUMBER;
|
||||
case "string":
|
||||
return marker === DATA_VALUE_KIND_STRING;
|
||||
case "container":
|
||||
return marker === DATA_VALUE_KIND_CONTAINER;
|
||||
case "null":
|
||||
return marker === DATA_VALUE_KIND_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOutgoingValue(type: string, value: unknown) {
|
||||
if (SCALAR_NUMBER_ARRAY_DATA_TYPES.has(type) && typeof value === "number") {
|
||||
return [value];
|
||||
|
|
@ -1308,25 +1426,6 @@ function normalizeIncomingValue(type: string, value: unknown) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function kindToMarker(kind: DataKind) {
|
||||
if (typeof kind === "object") {
|
||||
return 0x04;
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case "number":
|
||||
return 0x01;
|
||||
case "string":
|
||||
return 0x02;
|
||||
case "bool":
|
||||
return 0x03;
|
||||
case "container":
|
||||
return 0x05;
|
||||
case "null":
|
||||
return 0x06;
|
||||
}
|
||||
}
|
||||
|
||||
function writeU16(buffer: Uint8Array, offset: number, value: number) {
|
||||
new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).setUint16(
|
||||
offset,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
"dependencies": {
|
||||
"@tensamin/ttp": "workspace:*",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"solid-js": "^1.9.11",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { createContext, useContext, type ParentProps } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import * as React from "react";
|
||||
import { useSocket } from "@tensamin/ttp/context";
|
||||
|
||||
import { socket as schemas } from "@tensamin/shared/data";
|
||||
|
|
@ -12,15 +11,15 @@ interface contextValue {
|
|||
get(userId: number): Promise<User>;
|
||||
}
|
||||
|
||||
const UserContext = createContext<contextValue>();
|
||||
const UserContext = React.createContext<contextValue | undefined>(undefined);
|
||||
|
||||
export default function UserProvider(props: ParentProps) {
|
||||
const [storage, setStorage] = createStore<Record<number, User>>({});
|
||||
export default function UserProvider(props: { children: React.ReactNode }) {
|
||||
const storageRef = React.useRef<Record<number, User>>({});
|
||||
|
||||
const { send } = useSocket();
|
||||
|
||||
async function get(userId: number): Promise<User> {
|
||||
if (storage[userId] === undefined) {
|
||||
if (storageRef.current[userId] === undefined) {
|
||||
try {
|
||||
const userData = await send("get_user_data", { user_id: userId });
|
||||
|
||||
|
|
@ -30,12 +29,12 @@ export default function UserProvider(props: ParentProps) {
|
|||
: undefined;
|
||||
// Temp end
|
||||
|
||||
setStorage(userId, userData.data);
|
||||
storageRef.current[userId] = userData.data;
|
||||
} catch {
|
||||
setStorage(userId, failedUser);
|
||||
storageRef.current[userId] = failedUser;
|
||||
}
|
||||
}
|
||||
return storage[userId];
|
||||
return storageRef.current[userId];
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -46,7 +45,7 @@ export default function UserProvider(props: ParentProps) {
|
|||
}
|
||||
|
||||
export function useUser(): contextValue {
|
||||
const context = useContext(UserContext);
|
||||
const context = React.useContext(UserContext);
|
||||
if (!context) {
|
||||
throw new Error("useUser must be used within a UserProvider");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,25 @@
|
|||
import { createEffect, createSignal, type JSX } from "solid-js";
|
||||
import * as React from "react";
|
||||
import { useUser, type User } from "./context";
|
||||
|
||||
export default function Wrapper(props: {
|
||||
userId: number;
|
||||
component: (user: User) => JSX.Element;
|
||||
component: (user: User) => React.ReactNode;
|
||||
}) {
|
||||
const { get } = useUser();
|
||||
const [user, setUser] = createSignal<User | null>(null);
|
||||
const [user, setUser] = React.useState<User | null>(null);
|
||||
|
||||
createEffect(() => {
|
||||
get(props.userId).then(setUser);
|
||||
});
|
||||
React.useEffect(() => {
|
||||
let active = true;
|
||||
get(props.userId).then((value) => {
|
||||
if (active) {
|
||||
setUser(value);
|
||||
}
|
||||
});
|
||||
|
||||
return <>{user() ? props.component(user() as User) : null}</>;
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [get, props.userId]);
|
||||
|
||||
return <>{user ? props.component(user) : null}</>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
|
|
|
|||
|
|
@ -13,10 +13,8 @@
|
|||
},
|
||||
"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",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { createContext, useContext } from "solid-js";
|
||||
import type { ParentProps } from "solid-js";
|
||||
import * as React from "react";
|
||||
|
||||
export const context = createContext<contextType>();
|
||||
export const context = React.createContext<contextType | undefined>(undefined);
|
||||
|
||||
export default function Provider(props: ParentProps) {
|
||||
export default function Provider(props: { children: React.ReactNode }) {
|
||||
// live_messages
|
||||
|
||||
return (
|
||||
|
|
@ -18,7 +17,7 @@ type contextType = {
|
|||
};
|
||||
|
||||
export function useNotifications(): contextType {
|
||||
const ctx = useContext(context);
|
||||
const ctx = React.useContext(context);
|
||||
if (!ctx) {
|
||||
throw new Error("useNotifications must be used within a ChatProvider");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@
|
|||
"build": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-solid": "^0.564.0",
|
||||
"solid-js": "^1.9.11",
|
||||
"solid-sonner": "^0.2.8",
|
||||
"lucide-react": "^0.564.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"sonner": "^1.0.0",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { toast as sonnerToast } from "solid-sonner";
|
||||
import { Ban, Check, Info, TriangleAlert } from "lucide-solid";
|
||||
import { toast as sonnerToast } from "sonner";
|
||||
import { Ban, Check, Info, TriangleAlert } from "lucide-react";
|
||||
|
||||
export function log(
|
||||
logLevel: number,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
|
|
|
|||
|
|
@ -14,19 +14,13 @@
|
|||
"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",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"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",
|
||||
"lucide-react": "^0.564.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"sonner": "^1.0.0",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { ExternalLink } from "lucide-solid";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
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"
|
||||
className="flex gap-1.5 items-center justify-center text-primary border-b hover:border-primary border-transparent"
|
||||
>
|
||||
<ExternalLink size={17} /> {props.label}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ 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">
|
||||
<div className="bg-background w-full h-screen flex flex-col justify-center items-center">
|
||||
<p className="text-3xl font-bold">{props.error}</p>
|
||||
<p className="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" />
|
||||
|
|
|
|||
|
|
@ -1,42 +1,53 @@
|
|||
import { createSignal, createEffect, on } from "solid-js";
|
||||
import * as React from "react";
|
||||
|
||||
const DELAY = 250;
|
||||
|
||||
export default function Screen(props: { progress: number }) {
|
||||
const [displayProgress, setDisplayProgress] = createSignal(0);
|
||||
const [displayProgress, setDisplayProgress] = React.useState(0);
|
||||
const displayProgressRef = React.useRef(0);
|
||||
|
||||
// Animation
|
||||
createEffect(
|
||||
on(
|
||||
() => props.progress,
|
||||
(target) => {
|
||||
const start = displayProgress();
|
||||
const delta = target - start;
|
||||
if (delta === 0) return;
|
||||
React.useEffect(() => {
|
||||
displayProgressRef.current = displayProgress;
|
||||
}, [displayProgress]);
|
||||
|
||||
const duration = DELAY;
|
||||
const startTime = performance.now();
|
||||
React.useEffect(() => {
|
||||
const target = props.progress;
|
||||
const start = displayProgressRef.current;
|
||||
const delta = target - start;
|
||||
|
||||
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);
|
||||
}
|
||||
if (delta === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
),
|
||||
);
|
||||
const duration = DELAY;
|
||||
const startTime = performance.now();
|
||||
let frameId = 0;
|
||||
|
||||
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) {
|
||||
frameId = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
frameId = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frameId);
|
||||
};
|
||||
}, [props.progress]);
|
||||
|
||||
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 className="bg-background w-full h-screen flex flex-col justify-center items-center">
|
||||
<div className="w-64 h-1.5 bg-secondary rounded-full overflow-hidden relative">
|
||||
<div
|
||||
class="h-full bg-primary absolute left-0 top-0"
|
||||
className="h-full bg-primary absolute left-0 top-0"
|
||||
style={{
|
||||
width: `${displayProgress()}%`,
|
||||
width: `${displayProgress}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
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} />
|
||||
);
|
||||
};
|
||||
|
|
@ -1,10 +1,20 @@
|
|||
import { Avatar as ArkAvatar } from "@ark-ui/solid/avatar";
|
||||
import * as React from "react";
|
||||
|
||||
export default function Avatar(props: { img?: string; fallback: string }) {
|
||||
const [imageFailed, setImageFailed] = React.useState(false);
|
||||
|
||||
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>
|
||||
<div className="m-0 bg-card rounded-full border border-input/75 size-9 aspect-square flex items-center justify-center select-none overflow-hidden">
|
||||
{props.img && !imageFailed ? (
|
||||
<img
|
||||
className="rounded-full w-full h-full object-cover"
|
||||
src={props.img}
|
||||
alt={props.fallback}
|
||||
onError={() => setImageFailed(true)}
|
||||
/>
|
||||
) : (
|
||||
<span>{props.fallback}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
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";
|
||||
import * as React from "react";
|
||||
|
||||
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",
|
||||
|
|
@ -37,30 +33,21 @@ export const buttonVariants = cva(
|
|||
},
|
||||
);
|
||||
|
||||
type buttonProps<T extends ValidComponent = "button"> = ButtonRootProps<T> &
|
||||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
class?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Button = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, buttonProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as buttonProps, [
|
||||
"class",
|
||||
"variant",
|
||||
"size",
|
||||
]);
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
size: local.size,
|
||||
variant: local.variant,
|
||||
}),
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Button.displayName = "Button";
|
||||
|
|
|
|||
|
|
@ -1,60 +1,35 @@
|
|||
import { cn } from "../libs/cn";
|
||||
import type { ComponentProps, ParentComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
import * as React from "react";
|
||||
|
||||
export const Card = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
export const Card = ({ className, ...props }: React.ComponentProps<"div">) => (
|
||||
<div
|
||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const CardHeader = ({ className, ...props }: React.ComponentProps<"div">) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
);
|
||||
|
||||
export const CardHeader = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
export const CardTitle = ({ className, ...props }: React.ComponentProps<"h1">) => (
|
||||
<h1
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div class={cn("flex flex-col space-y-1.5 p-6", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
export const CardDescription = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"h3">) => (
|
||||
<h3 className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
);
|
||||
|
||||
export const CardTitle: ParentComponent<ComponentProps<"h1">> = (props) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
export const CardContent = ({ className, ...props }: React.ComponentProps<"div">) => (
|
||||
<div className={cn("p-6 pt-0", className)} {...props} />
|
||||
);
|
||||
|
||||
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} />
|
||||
);
|
||||
};
|
||||
export const CardFooter = ({ className, ...props }: React.ComponentProps<"div">) => (
|
||||
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,272 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,55 +1,104 @@
|
|||
import * as React from "react";
|
||||
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 CheckboxContextValue = {
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type checkboxControlProps<T extends ValidComponent = "div"> = VoidProps<
|
||||
CheckboxControlProps<T> & { class?: string }
|
||||
>;
|
||||
const CheckboxContext = React.createContext<CheckboxContextValue | null>(null);
|
||||
|
||||
export const CheckboxControl = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, checkboxControlProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as checkboxControlProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
export type CheckboxProps = {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Checkbox({
|
||||
checked,
|
||||
onChange,
|
||||
disabled,
|
||||
className,
|
||||
children,
|
||||
}: CheckboxProps) {
|
||||
return (
|
||||
<CheckboxContext.Provider value={{ checked, disabled }}>
|
||||
<label className={cn("inline-flex items-center", className)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.currentTarget.checked)}
|
||||
/>
|
||||
{children}
|
||||
</label>
|
||||
</CheckboxContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckboxControl({
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
}): React.ReactElement {
|
||||
const context = React.useContext(CheckboxContext);
|
||||
|
||||
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>
|
||||
</>
|
||||
<span
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 rounded-sm border border-primary shadow transition-shadow peer-focus-visible:ring-[1.5px] peer-focus-visible:ring-ring peer-disabled:cursor-not-allowed",
|
||||
context?.checked && "bg-primary text-primary-foreground",
|
||||
context?.disabled && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{context?.checked ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/>
|
||||
</svg>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function CheckboxLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
className={cn("text-sm leading-none font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckboxErrorMessage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"p">) {
|
||||
return <p className={cn("text-sm text-destructive", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CheckboxDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,327 +1,182 @@
|
|||
import * as React from "react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
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 ContextMenu = ContextMenuPrimitive.Root;
|
||||
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;
|
||||
export const ContextMenuSubTrigger = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>;
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={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",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
));
|
||||
|
||||
export const ContextMenuSubTrigger = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuSubTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuSubTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
"inset",
|
||||
]);
|
||||
ContextMenuSubTrigger.displayName = "ContextMenuSubTrigger";
|
||||
|
||||
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,
|
||||
export const ContextMenuSubContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...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}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
</ContextMenuPrimitive.Portal>
|
||||
));
|
||||
|
||||
type contextMenuCheckboxItemProps<T extends ValidComponent = "div"> =
|
||||
ParentProps<
|
||||
ContextMenuCheckboxItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
ContextMenuSubContent.displayName = "ContextMenuSubContent";
|
||||
|
||||
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,
|
||||
export const ContextMenuContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...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>
|
||||
);
|
||||
};
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
));
|
||||
|
||||
type contextMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
ContextMenuRadioItemProps<T> & {
|
||||
class?: string;
|
||||
ContextMenuContent.displayName = "ContextMenuContent";
|
||||
|
||||
export const ContextMenuItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>;
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={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",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const ContextMenuRadioItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuRadioItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuRadioItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
ContextMenuItem.displayName = "ContextMenuItem";
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
export const ContextMenuCheckboxItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={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",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
|
||||
type contextMenuItemLabelProps<T extends ValidComponent = "div"> =
|
||||
ContextMenuItemLabelProps<T> & {
|
||||
class?: string;
|
||||
ContextMenuCheckboxItem.displayName = "ContextMenuCheckboxItem";
|
||||
|
||||
export const ContextMenuRadioItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={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",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
));
|
||||
|
||||
ContextMenuRadioItem.displayName = "ContextMenuRadioItem";
|
||||
|
||||
export const ContextMenuItemLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
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;
|
||||
}
|
||||
>;
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const ContextMenuSeparator = <T extends ValidComponent = "hr">(
|
||||
props: PolymorphicProps<T, contextMenuSeparatorProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuSeparatorProps, [
|
||||
"class",
|
||||
]);
|
||||
ContextMenuItemLabel.displayName = "ContextMenuItemLabel";
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
class={cn("-mx-1 my-1 h-px bg-border", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const ContextMenuGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const ContextMenuShortcut = (props: ComponentProps<"span">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
ContextMenuGroupLabel.displayName = "ContextMenuGroupLabel";
|
||||
|
||||
return (
|
||||
<span
|
||||
class={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const ContextMenuSeparator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
ContextMenuSeparator.displayName = "ContextMenuSeparator";
|
||||
|
||||
export const ContextMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,320 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,27 +1,30 @@
|
|||
import { splitProps, type JSX } from "solid-js";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../libs/cn";
|
||||
|
||||
interface InputProps extends JSX.HTMLAttributes<HTMLInputElement> {
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
function Input(props: InputProps) {
|
||||
const [local, others] = splitProps(props, ["class", "type"]);
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={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",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
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"]);
|
||||
import * as React from "react";
|
||||
|
||||
const Label = React.forwardRef<
|
||||
HTMLLabelElement,
|
||||
React.LabelHTMLAttributes<HTMLLabelElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<label
|
||||
ref={ref}
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
className={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,
|
||||
className,
|
||||
)}
|
||||
{...others}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
Label.displayName = "Label";
|
||||
|
||||
export { Label };
|
||||
|
|
|
|||
|
|
@ -1,356 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,21 +1,5 @@
|
|||
import { Toaster as Sonner } from "solid-sonner";
|
||||
import { Toaster as Sonner } from "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}
|
||||
/>
|
||||
);
|
||||
return <Sonner className="toaster group" {...props} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,8 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
|
|
@ -13,5 +12,18 @@
|
|||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": [
|
||||
"src/link.tsx",
|
||||
"src/libs/cn.ts",
|
||||
"src/screens/error.tsx",
|
||||
"src/screens/loading.tsx",
|
||||
"src/ui/avatar.tsx",
|
||||
"src/ui/button.tsx",
|
||||
"src/ui/card.tsx",
|
||||
"src/ui/checkbox.tsx",
|
||||
"src/ui/context-menu.tsx",
|
||||
"src/ui/input.tsx",
|
||||
"src/ui/label.tsx",
|
||||
"src/ui/sonner.tsx"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,23 +18,17 @@
|
|||
"@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",
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@tanstack/react-virtual": "^3.0.0",
|
||||
"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",
|
||||
"lucide-react": "^0.564.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"sonner": "^1.0.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwind-scrollbar-hide": "^4.0.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
|
|
@ -43,14 +37,14 @@
|
|||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"@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"
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ 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">
|
||||
<Card className="animate-in fade-in duration-300 rounded-2xl">
|
||||
<CardHeader className="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]">
|
||||
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
|
||||
<p>{props.user.display}</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,45 @@
|
|||
import { Button } from "@tensamin/ui/button";
|
||||
import { House } from "lucide-solid";
|
||||
import { useNavigate, useSearchParams } from "@solidjs/router";
|
||||
import { createEffect, createSignal } from "solid-js";
|
||||
import { House } from "lucide-react";
|
||||
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
import * as React from "react";
|
||||
import { useUser, type User } from "@tensamin/core-user/context";
|
||||
|
||||
export default function Navbar() {
|
||||
const navigate = useNavigate();
|
||||
const { get } = useUser();
|
||||
const search = useRouterState({ select: (state) => state.location.search });
|
||||
|
||||
const [user, setUser] = createSignal<User | null>(null);
|
||||
const [user, setUser] = React.useState<User | null>(null);
|
||||
|
||||
createEffect(() => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const id = Number(searchParams.id);
|
||||
const currentId = React.useMemo(
|
||||
() => Number((search as Record<string, unknown>).id ?? Number.NaN),
|
||||
[search],
|
||||
);
|
||||
|
||||
if (isNaN(id)) {
|
||||
React.useEffect(() => {
|
||||
if (Number.isNaN(currentId)) {
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
|
||||
get(id)
|
||||
get(currentId)
|
||||
.then(setUser)
|
||||
.catch(() => setUser(null));
|
||||
});
|
||||
}, [currentId, get]);
|
||||
|
||||
return (
|
||||
<div class="w-full h-13.5 flex items-center justify-center">
|
||||
<div className="w-full h-13.5 flex items-center justify-center">
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
void navigate({ to: "/" });
|
||||
}}
|
||||
class="w-9 h-9 aspect-square p-0 rounded-lg"
|
||||
className="w-9 h-9 aspect-square p-0 rounded-lg"
|
||||
variant="outline"
|
||||
>
|
||||
<House size={18} />
|
||||
</Button>
|
||||
<p class="font-medium pl-3 text-md">{user()?.display}</p>
|
||||
<div class="w-full" />
|
||||
<p className="font-medium pl-3 text-md">{user?.display}</p>
|
||||
<div className="w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import { Input } from "@tensamin/ui/input";
|
|||
import { Label } from "@tensamin/ui/label";
|
||||
import { useStorage } from "@tensamin/core-storage/context";
|
||||
import { log, toast } from "@tensamin/shared/log";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Upload } from "lucide-solid";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Upload } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
const fetchedUser = z.object({
|
||||
|
|
@ -27,25 +28,22 @@ const formSchema = z.object({
|
|||
});
|
||||
|
||||
export default function Form() {
|
||||
let uploadRef: HTMLInputElement | undefined = undefined;
|
||||
const setUploadRef = (el: HTMLInputElement) => {
|
||||
uploadRef = el;
|
||||
};
|
||||
const uploadRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const { save } = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div class="flex gap-5">
|
||||
<Card class="w-75 h-80">
|
||||
<div className="flex gap-5">
|
||||
<Card className="w-75 h-80">
|
||||
<CardHeader>
|
||||
<CardTitle>Use .tu file</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="h-full flex items-center justify-center">
|
||||
<CardContent className="h-full flex items-center justify-center">
|
||||
<div
|
||||
onClick={() => uploadRef?.click()}
|
||||
class="cursor-pointer w-60 aspect-square mb-17 bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-dotted border-input/75 border-3 flex items-center justify-center rounded-lg"
|
||||
onClick={() => uploadRef.current?.click()}
|
||||
className="cursor-pointer w-60 aspect-square mb-17 bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-dotted border-input/75 border-3 flex items-center justify-center rounded-lg"
|
||||
>
|
||||
<Upload class="text-input/75" size={34} />
|
||||
<Upload className="text-input/75" size={34} />
|
||||
</div>
|
||||
<input
|
||||
onChange={async (e) => {
|
||||
|
|
@ -62,7 +60,7 @@ export default function Form() {
|
|||
save("user_id", userId);
|
||||
save("private_key", privateKey);
|
||||
|
||||
navigate("/");
|
||||
void navigate({ to: "/" });
|
||||
} else {
|
||||
throw new Error("No file selected");
|
||||
}
|
||||
|
|
@ -72,18 +70,18 @@ export default function Form() {
|
|||
}
|
||||
}}
|
||||
type="file"
|
||||
ref={setUploadRef}
|
||||
class="hidden"
|
||||
ref={uploadRef}
|
||||
className="hidden"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="w-75 h-auto">
|
||||
<Card className="w-75 h-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Use credentials</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
class="flex flex-col gap-5 h-full"
|
||||
className="flex flex-col gap-5 h-full"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
|
@ -111,7 +109,7 @@ export default function Form() {
|
|||
save("user_id", user.data.user_id);
|
||||
save("private_key", inputParse.data.private_key);
|
||||
|
||||
navigate("/");
|
||||
void navigate({ to: "/" });
|
||||
} else {
|
||||
log(0, "Login", "red", "Invalid response from server");
|
||||
toast("error", "Invalid response from server");
|
||||
|
|
@ -123,13 +121,13 @@ export default function Form() {
|
|||
});
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label for="username">Username</Label>
|
||||
<Input type="text" id="username" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input type="text" id="username" name="username" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label for="private_key">Private Key</Label>
|
||||
<Input type="password" id="private_key" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="private_key">Private Key</Label>
|
||||
<Input type="password" id="private_key" name="private_key" />
|
||||
</div>
|
||||
<Button type="submit">Login</Button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { useStorage } from "@tensamin/core-storage/context";
|
||||
import Wrapper from "@tensamin/core-user/wrapper";
|
||||
import { createEffect, createSignal } from "solid-js";
|
||||
import * as React from "react";
|
||||
import Basic from "./modals/basic";
|
||||
import List from "@/features/conversation/list/body";
|
||||
|
||||
export default function Sidebar() {
|
||||
const [userId, setUserId] = createSignal(0);
|
||||
const [userId, setUserId] = React.useState(0);
|
||||
const { load } = useStorage();
|
||||
|
||||
createEffect(() => {
|
||||
React.useEffect(() => {
|
||||
load("user_id").then(setUserId);
|
||||
});
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<div class="w-75 h-full flex flex-col gap-3 p-2">
|
||||
{userId() !== 0 && (
|
||||
<div className="w-75 h-full flex flex-col gap-3 p-2">
|
||||
{userId !== 0 && (
|
||||
<Wrapper
|
||||
userId={userId()}
|
||||
userId={userId}
|
||||
component={(user) => <Basic user={user} />}
|
||||
/>
|
||||
)}
|
||||
<div class="h-full">
|
||||
<div className="h-full">
|
||||
<List />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
useContext,
|
||||
type ParentProps,
|
||||
} from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import * as React from "react";
|
||||
import { useSocket } from "@tensamin/ttp/context";
|
||||
|
||||
import { toast } from "@tensamin/shared/log";
|
||||
|
|
@ -18,44 +12,64 @@ interface contextValue {
|
|||
communities: Community[];
|
||||
}
|
||||
|
||||
const ConversationContext = createContext<contextValue>();
|
||||
const ConversationContext = React.createContext<contextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export default function ConversationProvider(props: ParentProps) {
|
||||
const [conversations, setConversations] = createStore<Conversation[]>([]);
|
||||
const [communities, setCommunities] = createStore<Community[]>([]);
|
||||
export default function ConversationProvider(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [conversations, setConversations] = React.useState<Conversation[]>([]);
|
||||
const [communities, setCommunities] = React.useState<Community[]>([]);
|
||||
|
||||
const { send } = useSocket();
|
||||
|
||||
createEffect(() => {
|
||||
React.useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
send("get_chats", {})
|
||||
.then((data) => {
|
||||
setConversations(data.data.user_ids);
|
||||
if (active) {
|
||||
setConversations(data.data.user_ids);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast("error", "Failed to load conversations");
|
||||
});
|
||||
|
||||
send("get_communities", {})
|
||||
.then((data) => {
|
||||
setCommunities(data.data.communities);
|
||||
if (active) {
|
||||
setCommunities(data.data.communities);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast("error", "Failed to load communities");
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [send]);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({ conversations, communities }),
|
||||
[communities, conversations],
|
||||
);
|
||||
|
||||
return (
|
||||
<ConversationContext.Provider value={{ conversations, communities }}>
|
||||
<ConversationContext.Provider value={value}>
|
||||
{props.children}
|
||||
</ConversationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useConversation(): contextValue {
|
||||
const context = useContext(ConversationContext);
|
||||
const context = React.useContext(ConversationContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useConversation must be used within a ConversationProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +1,64 @@
|
|||
import { createSignal, For, Show } from "solid-js";
|
||||
import * as React from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useConversation } from "../context";
|
||||
import { createVirtualizer } from "@tanstack/solid-virtual";
|
||||
|
||||
import Switch from "./switch";
|
||||
import ConversationModal from "../modal/conversation";
|
||||
import CommunityModal from "../modal/community";
|
||||
|
||||
export default function List() {
|
||||
const [category, setCategory] = createSignal<"conversations" | "communities">(
|
||||
"conversations",
|
||||
);
|
||||
const [category, setCategory] = React.useState<
|
||||
"conversations" | "communities"
|
||||
>("conversations");
|
||||
|
||||
const { conversations, communities } = useConversation();
|
||||
|
||||
// eslint-disable-next-line no-unassigned-vars
|
||||
let scrollRef!: HTMLDivElement;
|
||||
const scrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const virtualizer = createVirtualizer({
|
||||
get count() {
|
||||
return category() === "conversations"
|
||||
? conversations.length
|
||||
: communities.length;
|
||||
},
|
||||
const virtualizer = useVirtualizer({
|
||||
count: category === "conversations" ? conversations.length : communities.length,
|
||||
estimateSize: () => 80,
|
||||
getScrollElement: () => scrollRef,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
<Switch category={category()} setCategory={setCategory} />
|
||||
<div
|
||||
ref={scrollRef}
|
||||
id="conversation-list"
|
||||
class="overflow-y-auto flex-1"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Switch category={category} setCategory={setCategory} />
|
||||
<div ref={scrollRef} id="conversation-list" className="overflow-y-auto flex-1">
|
||||
<div
|
||||
class="relative w-full flex flex-col gap-2"
|
||||
className="relative w-full flex flex-col gap-2"
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
<For each={virtualizer.getVirtualItems()}>
|
||||
{(virtualItem) => {
|
||||
return (
|
||||
<Show
|
||||
when={category() === "conversations"}
|
||||
fallback={
|
||||
<CommunityModal
|
||||
community={communities[virtualItem.index]}
|
||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const itemKey = `${category}-${virtualItem.index}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={itemKey}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
{category === "conversations" ? (
|
||||
conversations[virtualItem.index] ? (
|
||||
<ConversationModal
|
||||
userId={conversations[virtualItem.index].user_id}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ConversationModal
|
||||
userId={conversations[virtualItem.index].user_id}
|
||||
/>
|
||||
</Show>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
) : null
|
||||
) : communities[virtualItem.index] ? (
|
||||
<CommunityModal community={communities[virtualItem.index]} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { createEffect, createSignal, onMount } from "solid-js";
|
||||
import * as React from "react";
|
||||
|
||||
type Category = "conversations" | "communities";
|
||||
|
||||
|
|
@ -6,24 +6,27 @@ export default function Switch(props: {
|
|||
category: Category;
|
||||
setCategory: (category: Category) => void;
|
||||
}) {
|
||||
let conversationsRef: HTMLButtonElement | undefined;
|
||||
let communitiesRef: HTMLButtonElement | undefined;
|
||||
const conversationsRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const communitiesRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const [indicator, setIndicator] = createSignal({ left: 0, width: 0 });
|
||||
const [indicator, setIndicator] = React.useState({ left: 0, width: 0 });
|
||||
|
||||
function updateIndicator() {
|
||||
const updateIndicator = React.useCallback(() => {
|
||||
const active =
|
||||
props.category === "conversations" ? conversationsRef : communitiesRef;
|
||||
props.category === "conversations"
|
||||
? conversationsRef.current
|
||||
: communitiesRef.current;
|
||||
if (!active) return;
|
||||
|
||||
setIndicator({
|
||||
left: active.offsetLeft,
|
||||
width: active.offsetWidth,
|
||||
});
|
||||
}
|
||||
}, [props.category]);
|
||||
|
||||
onMount(updateIndicator);
|
||||
createEffect(updateIndicator);
|
||||
React.useLayoutEffect(() => {
|
||||
updateIndicator();
|
||||
}, [updateIndicator]);
|
||||
|
||||
function toggleCategory() {
|
||||
props.setCategory(
|
||||
|
|
@ -34,20 +37,20 @@ export default function Switch(props: {
|
|||
return (
|
||||
<div
|
||||
role="tablist"
|
||||
class="border relative inline-flex rounded-full bg-card p-1 select-none"
|
||||
className="border relative inline-flex rounded-full bg-card p-1 select-none"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 bottom-1 rounded-full bg-input shadow-sm transition-all duration-300 ease-in-out"
|
||||
className="absolute top-1 bottom-1 rounded-full bg-input shadow-sm transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
left: `${indicator().left}px`,
|
||||
width: `${indicator().width}px`,
|
||||
left: `${indicator.left}px`,
|
||||
width: `${indicator.width}px`,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
ref={(el) => (conversationsRef = el)}
|
||||
ref={conversationsRef}
|
||||
role="tab"
|
||||
aria-selected={props.category === "conversations"}
|
||||
class={`relative z-10 cursor-pointer px-2.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-300 ${
|
||||
className={`relative z-10 cursor-pointer px-2.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-300 ${
|
||||
props.category === "conversations"
|
||||
? "text-foreground"
|
||||
: "text-ring/50 hover:text-ring"
|
||||
|
|
@ -57,10 +60,10 @@ export default function Switch(props: {
|
|||
Conversations
|
||||
</button>
|
||||
<button
|
||||
ref={(el) => (communitiesRef = el)}
|
||||
ref={communitiesRef}
|
||||
role="tab"
|
||||
aria-selected={props.category === "communities"}
|
||||
class={`relative z-10 cursor-pointer px-2.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-300 ${
|
||||
className={`relative z-10 cursor-pointer px-2.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-300 ${
|
||||
props.category === "communities"
|
||||
? "text-foreground"
|
||||
: "text-ring/50 hover:text-ring"
|
||||
|
|
@ -71,4 +74,4 @@ export default function Switch(props: {
|
|||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import {
|
|||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@tensamin/ui/context-menu";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export default function ConversationModal(props: { userId: number }) {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -16,9 +16,9 @@ export default function ConversationModal(props: { userId: number }) {
|
|||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<div
|
||||
class="select-none cursor-pointer"
|
||||
className="select-none cursor-pointer"
|
||||
onClick={() => {
|
||||
navigate("/chat?id=" + props.userId);
|
||||
void navigate({ to: "/chat", search: { id: props.userId } });
|
||||
}}
|
||||
>
|
||||
<Wrapper
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import * as React from "react";
|
||||
import { useStorage } from "@tensamin/core-storage/context";
|
||||
import { Button } from "@tensamin/ui/button";
|
||||
import {
|
||||
|
|
@ -5,7 +6,6 @@ import {
|
|||
CheckboxLabel,
|
||||
CheckboxControl,
|
||||
} from "@tensamin/ui/checkbox";
|
||||
import { createEffect, createSignal, type JSX, Show } from "solid-js";
|
||||
import { z } from "zod";
|
||||
|
||||
import ErrorScreen from "@tensamin/ui/screens/error";
|
||||
|
|
@ -14,20 +14,34 @@ import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
|
|||
import { log } from "@tensamin/shared/log";
|
||||
import Link from "@tensamin/ui/link";
|
||||
|
||||
export default function Screen(props: { children: JSX.Element }) {
|
||||
export default function Screen(props: { children: React.ReactNode }) {
|
||||
const { load, save } = useStorage();
|
||||
|
||||
const [error, setError] = createSignal("");
|
||||
const [errorDescription, setErrorDescription] = createSignal("");
|
||||
const [error, setError] = React.useState("");
|
||||
const [errorDescription, setErrorDescription] = React.useState("");
|
||||
|
||||
const [remoteDocs, setRemoteDocs] =
|
||||
createSignal<z.infer<typeof legalDocsSchema>>();
|
||||
const [remoteDocs, setRemoteDocs] = React.useState<
|
||||
z.infer<typeof legalDocsSchema> | undefined
|
||||
>(undefined);
|
||||
|
||||
const [loading, setLoading] = createSignal(true);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
|
||||
const [PPandToSDone, setPPandToSDone] = React.useState(false);
|
||||
const [acceptedPP, acceptPP] = React.useState(false);
|
||||
const [acceptedTOS, acceptTOS] = React.useState(false);
|
||||
|
||||
const [doneWithAnalytics, setDoneWithAnalytics] = React.useState(false);
|
||||
const [crashReports, setCrashReports] = React.useState(false);
|
||||
const [usageData, setUsageData] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
void load("user_id").then(async (id) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
load("user_id").then(async (id) => {
|
||||
// For non-logged in users
|
||||
if (id === 0) {
|
||||
setPPandToSDone(true);
|
||||
setDoneWithAnalytics(true);
|
||||
|
|
@ -38,42 +52,63 @@ export default function Screen(props: { children: JSX.Element }) {
|
|||
const current = await fetch("https://legal.tensamin.net/api/current")
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
if (!active) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setError("Failed to load legal documents");
|
||||
setErrorDescription(
|
||||
"An error occurred while fetching the legal documents from the server. Please try again later.",
|
||||
);
|
||||
log(0, "Legal", "red", "Failed to fetch legal documents", err);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const safeCurrent = legalDocsSchema.safeParse(current);
|
||||
setRemoteDocs(safeCurrent.data);
|
||||
if (!active || current === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const safeCurrent = legalDocsSchema.safeParse(current);
|
||||
if (!safeCurrent.success) {
|
||||
setError("Failed to load legal documents");
|
||||
setErrorDescription(
|
||||
"The legal documents data received from the server is invalid. Please try again later.",
|
||||
);
|
||||
log(
|
||||
0,
|
||||
"Legal",
|
||||
"red",
|
||||
"Invalid legal documents data",
|
||||
safeCurrent.error,
|
||||
);
|
||||
log(0, "Legal", "red", "Invalid legal documents data", safeCurrent.error);
|
||||
return;
|
||||
}
|
||||
|
||||
setRemoteDocs(safeCurrent.data);
|
||||
|
||||
const localDocs = await load("legal_docs");
|
||||
|
||||
await Promise.all([
|
||||
load("ppandtos_done").then(setPPandToSDone),
|
||||
load("accepted_privacy_policy").then(acceptPP),
|
||||
load("accepted_terms_of_service").then(acceptTOS),
|
||||
load("analytics_done").then(setDoneWithAnalytics),
|
||||
load("analytics_crash_reports").then(setCrashReports),
|
||||
load("analytics_usage_data").then(setUsageData),
|
||||
const [
|
||||
loadedPPAndTOS,
|
||||
loadedAcceptedPP,
|
||||
loadedAcceptedTOS,
|
||||
loadedAnalyticsDone,
|
||||
loadedCrashReports,
|
||||
loadedUsageData,
|
||||
] = await Promise.all([
|
||||
load("ppandtos_done"),
|
||||
load("accepted_privacy_policy"),
|
||||
load("accepted_terms_of_service"),
|
||||
load("analytics_done"),
|
||||
load("analytics_crash_reports"),
|
||||
load("analytics_usage_data"),
|
||||
]);
|
||||
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPPandToSDone(loadedPPAndTOS);
|
||||
acceptPP(loadedAcceptedPP);
|
||||
acceptTOS(loadedAcceptedTOS);
|
||||
setDoneWithAnalytics(loadedAnalyticsDone);
|
||||
setCrashReports(loadedCrashReports);
|
||||
setUsageData(loadedUsageData);
|
||||
|
||||
if (localDocs.pp.hash !== safeCurrent.data.pp.hash) {
|
||||
acceptPP(false);
|
||||
setPPandToSDone(false);
|
||||
|
|
@ -86,132 +121,122 @@ export default function Screen(props: { children: JSX.Element }) {
|
|||
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
|
||||
const [PPandToSDone, setPPandToSDone] = createSignal(false);
|
||||
const [acceptedPP, acceptPP] = createSignal(false);
|
||||
const [acceptedTOS, acceptTOS] = createSignal(false);
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
const [doneWithAnalytics, setDoneWithAnalytics] = createSignal(false);
|
||||
const [crashReports, setCrashReports] = createSignal(false);
|
||||
const [usageData, setUsageData] = createSignal(false);
|
||||
if (error !== "" && errorDescription !== "") {
|
||||
return <ErrorScreen error={error} description={errorDescription} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (PPandToSDone && doneWithAnalytics) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={error() !== "" && errorDescription() !== ""}
|
||||
fallback={
|
||||
<Show when={!loading()} fallback={null}>
|
||||
<Show
|
||||
when={!PPandToSDone() || !doneWithAnalytics()}
|
||||
fallback={props.children}
|
||||
>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<div class="h-full flex flex-col gap-15 p-10 md:p-40 w-full lg:w-2/3">
|
||||
{!PPandToSDone() ? (
|
||||
<>
|
||||
<h1 class="text-3xl md:text-4xl font-bold">
|
||||
Privacy Policy & ToS
|
||||
<p class="text-muted-foreground text-[20px] font-normal pt-3">
|
||||
{remoteDocs()?.pp.version} / {remoteDocs()?.tos.version}
|
||||
</p>
|
||||
</h1>
|
||||
<div class="w-full h-full flex flex-col items-center justify-center gap-5">
|
||||
<div class="justify-start items-start flex flex-col gap-2">
|
||||
<BigCheckbox
|
||||
checked={acceptedPP()}
|
||||
onChange={acceptPP}
|
||||
label="I agree to the Privacy Policy"
|
||||
/>
|
||||
<BigCheckbox
|
||||
checked={acceptedTOS()}
|
||||
onChange={acceptTOS}
|
||||
label="I agree to the Terms of Service"
|
||||
/>
|
||||
<div class="w-full border-t-2" />
|
||||
<Link
|
||||
label="Privacy Policy"
|
||||
link={`https://legal.tensamin.net/pp/${remoteDocs()?.pp.version}`}
|
||||
/>
|
||||
<Link
|
||||
label="Terms of Service"
|
||||
link={`https://legal.tensamin.net/tos/${remoteDocs()?.tos.version}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContinueButton
|
||||
disabled={!acceptedPP() || !acceptedTOS()}
|
||||
onClick={() => {
|
||||
const currentDocs = remoteDocs()!;
|
||||
|
||||
save("accepted_privacy_policy", true).then(() =>
|
||||
save("accepted_terms_of_service", true).then(() =>
|
||||
save("ppandtos_done", true).then(() =>
|
||||
save("legal_docs", currentDocs).then(() =>
|
||||
setPPandToSDone(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : !doneWithAnalytics() ? (
|
||||
<>
|
||||
<h1 class="text-3xl md:text-4xl font-bold">Analytics</h1>
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<div class="justify-start flex flex-col gap-2">
|
||||
<BigCheckbox
|
||||
checked={crashReports()}
|
||||
onChange={setCrashReports}
|
||||
label="Send anonymous crash reports"
|
||||
/>
|
||||
<BigCheckbox
|
||||
checked={usageData()}
|
||||
onChange={setUsageData}
|
||||
label="Send anonymous usage data"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContinueButton
|
||||
onClick={() => {
|
||||
const currentCrashReports = crashReports();
|
||||
const currentUsageData = usageData();
|
||||
|
||||
save(
|
||||
"analytics_crash_reports",
|
||||
currentCrashReports,
|
||||
).then(() => {
|
||||
setCrashReports(currentCrashReports);
|
||||
});
|
||||
save("analytics_usage_data", currentUsageData).then(
|
||||
() => {
|
||||
setUsageData(currentUsageData);
|
||||
},
|
||||
);
|
||||
save("analytics_done", true).then(() => {
|
||||
setDoneWithAnalytics(true);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="h-full flex flex-col gap-15 p-10 md:p-40 w-full lg:w-2/3">
|
||||
{!PPandToSDone ? (
|
||||
<>
|
||||
<h1 className="text-3xl md:text-4xl font-bold">
|
||||
Privacy Policy & ToS
|
||||
<p className="text-muted-foreground text-[20px] font-normal pt-3">
|
||||
{remoteDocs?.pp.version} / {remoteDocs?.tos.version}
|
||||
</p>
|
||||
</h1>
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-5">
|
||||
<div className="justify-start items-start flex flex-col gap-2">
|
||||
<BigCheckbox
|
||||
checked={acceptedPP}
|
||||
onChange={acceptPP}
|
||||
label="I agree to the Privacy Policy"
|
||||
/>
|
||||
<BigCheckbox
|
||||
checked={acceptedTOS}
|
||||
onChange={acceptTOS}
|
||||
label="I agree to the Terms of Service"
|
||||
/>
|
||||
<div className="w-full border-t-2" />
|
||||
<Link
|
||||
label="Privacy Policy"
|
||||
link={`https://legal.tensamin.net/pp/${remoteDocs?.pp.version}`}
|
||||
/>
|
||||
<Link
|
||||
label="Terms of Service"
|
||||
link={`https://legal.tensamin.net/tos/${remoteDocs?.tos.version}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<ErrorScreen error={error()} description={errorDescription()} />
|
||||
</Show>
|
||||
<ContinueButton
|
||||
disabled={!acceptedPP || !acceptedTOS}
|
||||
onClick={() => {
|
||||
const currentDocs = remoteDocs;
|
||||
if (!currentDocs) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
await save("accepted_privacy_policy", true);
|
||||
await save("accepted_terms_of_service", true);
|
||||
await save("ppandtos_done", true);
|
||||
await save("legal_docs", currentDocs);
|
||||
setPPandToSDone(true);
|
||||
})();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-3xl md:text-4xl font-bold">Analytics</h1>
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<div className="justify-start flex flex-col gap-2">
|
||||
<BigCheckbox
|
||||
checked={crashReports}
|
||||
onChange={setCrashReports}
|
||||
label="Send anonymous crash reports"
|
||||
/>
|
||||
<BigCheckbox
|
||||
checked={usageData}
|
||||
onChange={setUsageData}
|
||||
label="Send anonymous usage data"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContinueButton
|
||||
onClick={() => {
|
||||
const currentCrashReports = crashReports;
|
||||
const currentUsageData = usageData;
|
||||
|
||||
void save("analytics_crash_reports", currentCrashReports).then(() => {
|
||||
setCrashReports(currentCrashReports);
|
||||
});
|
||||
void save("analytics_usage_data", currentUsageData).then(() => {
|
||||
setUsageData(currentUsageData);
|
||||
});
|
||||
void save("analytics_done", true).then(() => {
|
||||
setDoneWithAnalytics(true);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContinueButton(props: { onClick: () => void; disabled?: boolean }) {
|
||||
return (
|
||||
<div class="w-full flex justify-end">
|
||||
<div className="w-full flex justify-end">
|
||||
<Button
|
||||
size="lg"
|
||||
class="text-lg w-full md:w-auto"
|
||||
className="text-lg w-full md:w-auto"
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
|
|
@ -230,10 +255,10 @@ export function BigCheckbox(props: {
|
|||
<Checkbox
|
||||
checked={props.checked}
|
||||
onChange={props.onChange}
|
||||
class="flex items-center space-x-2"
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<CheckboxControl class="size-5.5 rounded-md flex items-center justify-center" />
|
||||
<CheckboxLabel class="text-lg">{props.label}</CheckboxLabel>
|
||||
<CheckboxControl className="size-5.5 rounded-md flex items-center justify-center" />
|
||||
<CheckboxLabel className="text-lg">{props.label}</CheckboxLabel>
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,30 @@
|
|||
import { createEffect } from "solid-js";
|
||||
import { render } from "solid-js/web";
|
||||
import { Route, Router } from "@solidjs/router";
|
||||
import * as React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import {
|
||||
Outlet,
|
||||
RouterProvider,
|
||||
createRootRoute,
|
||||
createRoute,
|
||||
createRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import "./index.css";
|
||||
|
||||
import NotFound from "@/routes/404";
|
||||
|
||||
// Layouts
|
||||
import RootLayout from "./routes/layout";
|
||||
import AppLayout from "./routes/app/layout";
|
||||
|
||||
// Pages
|
||||
import Home from "@/routes/app/home";
|
||||
import ChatScreen from "@tensamin/chat/screen";
|
||||
import ChatContext from "@tensamin/chat/context";
|
||||
import Login from "@/routes/screens/login";
|
||||
import Signup from "@/routes/screens/signup";
|
||||
|
||||
import { getMessages } from "@tensamin/chat/behaviour-conversation";
|
||||
const wrapper = document.getElementById("root");
|
||||
|
||||
// Render
|
||||
const wrapper = document.getElementById("root")!;
|
||||
if (!wrapper) {
|
||||
throw new Error("Missing root element");
|
||||
}
|
||||
|
||||
// @ts-expect-error Declare global function
|
||||
window.setLogLevelToMax = () => {
|
||||
|
|
@ -27,67 +32,101 @@ window.setLogLevelToMax = () => {
|
|||
location.reload();
|
||||
};
|
||||
|
||||
render(
|
||||
() => (
|
||||
<Router
|
||||
root={(props) => {
|
||||
// Theme Detection
|
||||
createEffect(() => {
|
||||
try {
|
||||
const mediaQuery = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
);
|
||||
function RootShell() {
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
// Initial check
|
||||
document.documentElement.classList.toggle(
|
||||
"dark",
|
||||
mediaQuery.matches,
|
||||
);
|
||||
document.documentElement.classList.toggle("dark", mediaQuery.matches);
|
||||
|
||||
// Listen to changes
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
document.documentElement.classList.toggle("dark", e.matches);
|
||||
};
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
document.documentElement.classList.toggle("dark", event.matches);
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
// Cleanup
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
} catch {
|
||||
/* theme detection not supported */
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
};
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div class="w-screen h-screen overflow-hidden">{props.children}</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Route path="/" component={RootLayout}>
|
||||
{/* App */}
|
||||
<Route path="/" component={AppLayout}>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/chat" component={Chat} />
|
||||
</Route>
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden">
|
||||
<RootLayout>
|
||||
<Outlet />
|
||||
</RootLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Screens */}
|
||||
<Route path="/">
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/signup" component={Signup} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* 404 */}
|
||||
<Route path="*" component={NotFound} />
|
||||
</Router>
|
||||
),
|
||||
wrapper,
|
||||
);
|
||||
function AppShell() {
|
||||
return (
|
||||
<AppLayout>
|
||||
<Outlet />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function Chat() {
|
||||
return (
|
||||
<ChatContext getMessages={getMessages}>
|
||||
<ChatContext>
|
||||
<ChatScreen />
|
||||
</ChatContext>
|
||||
);
|
||||
}
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
component: RootShell,
|
||||
});
|
||||
|
||||
const appRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
id: "app",
|
||||
component: AppShell,
|
||||
});
|
||||
|
||||
const homeRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: "/",
|
||||
component: Home,
|
||||
});
|
||||
|
||||
const chatRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: "chat",
|
||||
component: Chat,
|
||||
});
|
||||
|
||||
const loginRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "login",
|
||||
component: Login,
|
||||
});
|
||||
|
||||
const signupRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "signup",
|
||||
component: Signup,
|
||||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
appRoute.addChildren([homeRoute, chatRoute]),
|
||||
loginRoute,
|
||||
signupRoute,
|
||||
]);
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
defaultNotFoundComponent: NotFound,
|
||||
});
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
createRoot(wrapper).render(<RouterProvider router={router} />);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
export default function Page() {
|
||||
return (
|
||||
<div class="w-full h-full flex items-center justify-center text-4xl font-bold">
|
||||
<div className="w-full h-full flex items-center justify-center text-4xl font-bold">
|
||||
Page not found (404)
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import Socket from "@tensamin/ttp/context";
|
||||
import User from "@tensamin/core-user/context";
|
||||
import type { RouteSectionProps } from "@solidjs/router";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import Conversation from "@/features/conversation/context";
|
||||
import Navbar from "@/components/navbar";
|
||||
|
||||
export default function Layout(props: RouteSectionProps) {
|
||||
export default function Layout(props: { children: ReactNode }) {
|
||||
return (
|
||||
<Socket>
|
||||
<User>
|
||||
<Conversation>
|
||||
<div class="w-full h-full flex bg-sidebar">
|
||||
<div className="w-full h-full flex bg-sidebar">
|
||||
<Sidebar />
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<Navbar />
|
||||
<div class="bg-background h-full w-full rounded-tl-3xl border-t border-l">
|
||||
<div className="bg-background h-full w-full rounded-tl-3xl border-t border-l">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { RouteSectionProps } from "@solidjs/router";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import Storage from "@tensamin/core-storage/context";
|
||||
import Crypto from "@tensamin/core-crypto/context";
|
||||
|
|
@ -7,7 +7,7 @@ import LegalWrapper from "@/features/legal/screen";
|
|||
|
||||
import { Toaster } from "@tensamin/ui/sonner";
|
||||
|
||||
export default function Layout(props: RouteSectionProps) {
|
||||
export default function Layout(props: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import Form from "@/components/screens/login/form";
|
||||
import { Button } from "@tensamin/ui/button";
|
||||
import { A } from "@solidjs/router";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div class="w-full h-full flex flex-col gap-10 items-center justify-center">
|
||||
<div className="w-full h-full flex flex-col gap-10 items-center justify-center">
|
||||
<Form />
|
||||
<div class="flex">
|
||||
<A href="/signup">
|
||||
<div className="flex">
|
||||
<Link to="/signup">
|
||||
<Button variant="outline">Sign up</Button>
|
||||
</A>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@
|
|||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { defineConfig } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import lucidePreprocess from "vite-plugin-lucide-preprocess";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid(), tsconfigPaths(), tailwindcss(), lucidePreprocess()],
|
||||
plugins: [react(), tsconfigPaths(), tailwindcss()],
|
||||
worker: {
|
||||
format: "es",
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue