Formatted, fixed some ttp stuff
This commit is contained in:
parent
c0102df54f
commit
d77271e4b7
27 changed files with 1089 additions and 371 deletions
|
|
@ -11,15 +11,15 @@ export const context = React.createContext<contextType | undefined>(undefined);
|
|||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function Provider(
|
||||
props: { children: React.ReactNode },
|
||||
) {
|
||||
export default function Provider(props: { children: React.ReactNode }) {
|
||||
const { get_shared_secret } = useCrypto();
|
||||
const { get } = useUser();
|
||||
const { load } = useStorage();
|
||||
const { send } = useSocket();
|
||||
|
||||
const [liveMessagesState, setLiveMessagesState] = React.useState<RawMessages>([]);
|
||||
const [liveMessagesState, setLiveMessagesState] = React.useState<RawMessages>(
|
||||
[],
|
||||
);
|
||||
const [currentSharedSecret, setCurrentSharedSecret] = React.useState("");
|
||||
|
||||
const locationSearch = useRouterState({
|
||||
|
|
@ -104,7 +104,14 @@ export default function Provider(
|
|||
sharedSecret: () => currentSharedSecret,
|
||||
userId: () => userIdValue,
|
||||
}),
|
||||
[addLiveMessage, clearLiveMessages, currentSharedSecret, customGetMessages, liveMessagesState, userIdValue],
|
||||
[
|
||||
addLiveMessage,
|
||||
clearLiveMessages,
|
||||
currentSharedSecret,
|
||||
customGetMessages,
|
||||
liveMessagesState,
|
||||
userIdValue,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -129,4 +136,4 @@ export function useChat(): contextType {
|
|||
throw new Error("useChat must be used within a ChatProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,11 @@ export default function Screen() {
|
|||
}, [lastLiveMessageCount, liveMessages]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!scrollRef.current || !prependAnchor || messagesQuery.isFetchingNextPage) {
|
||||
if (
|
||||
!scrollRef.current ||
|
||||
!prependAnchor ||
|
||||
messagesQuery.isFetchingNextPage
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +195,10 @@ export default function Screen() {
|
|||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
<Message message={message} notEncrypted={message.not_encrypted} />
|
||||
<Message
|
||||
message={message}
|
||||
notEncrypted={message.not_encrypted}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -200,4 +207,4 @@ export default function Screen() {
|
|||
<InputComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,4 +100,4 @@ export function useCrypto(): contextType {
|
|||
throw new Error("useCrypto must be used within a CryptoProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,7 @@ import {
|
|||
historyKeymap,
|
||||
indentWithTab,
|
||||
} from "@codemirror/commands";
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { collectInlineRanges, ensureMarkdownStyles } from "./markdown";
|
||||
|
||||
|
|
|
|||
|
|
@ -476,19 +476,22 @@ export function renderBlocks(blocks: MarkdownBlock[]): React.ReactElement {
|
|||
if (block.type === "list") {
|
||||
const Tag = block.ordered ? "ol" : "ul";
|
||||
return (
|
||||
<Tag key={blockIndex} className={block.ordered ? "tm-md-ol" : "tm-md-ul"}>
|
||||
<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
|
||||
className="tm-md-checkbox"
|
||||
/>
|
||||
) : null}
|
||||
<span>{renderInline(parseInlineNodes(item.text))}</span>
|
||||
</li>
|
||||
{item.checked !== null ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
disabled
|
||||
className="tm-md-checkbox"
|
||||
/>
|
||||
) : null}
|
||||
<span>{renderInline(parseInlineNodes(item.text))}</span>
|
||||
</li>
|
||||
))}
|
||||
</Tag>
|
||||
);
|
||||
|
|
@ -501,7 +504,9 @@ export function renderBlocks(blocks: MarkdownBlock[]): React.ReactElement {
|
|||
<thead>
|
||||
<tr>
|
||||
{block.headers.map((header, headerIndex) => (
|
||||
<th key={headerIndex}>{renderInline(parseInlineNodes(header))}</th>
|
||||
<th key={headerIndex}>
|
||||
{renderInline(parseInlineNodes(header))}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -509,9 +514,11 @@ export function renderBlocks(blocks: MarkdownBlock[]): React.ReactElement {
|
|||
{block.rows.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={cellIndex}>{renderInline(parseInlineNodes(cell))}</td>
|
||||
<td key={cellIndex}>
|
||||
{renderInline(parseInlineNodes(cell))}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ export type TextProps = {
|
|||
export default function Text(props: TextProps) {
|
||||
ensureMarkdownStyles();
|
||||
|
||||
const blocks = React.useMemo(() => parseMarkdownBlocks(props.value), [props.value]);
|
||||
const blocks = React.useMemo(
|
||||
() => parseMarkdownBlocks(props.value),
|
||||
[props.value],
|
||||
);
|
||||
|
||||
return <div className="tm-md-root">{renderBlocks(blocks)}</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ export default function StorageProvider(props: { children: React.ReactNode }) {
|
|||
}, [storage]);
|
||||
|
||||
const loadIO = React.useCallback(
|
||||
async <K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]> => {
|
||||
async <K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
): Promise<StorageSchema[K]> => {
|
||||
let stored: StorageSchema[K] | undefined;
|
||||
|
||||
try {
|
||||
|
|
@ -75,7 +77,9 @@ export default function StorageProvider(props: { children: React.ReactNode }) {
|
|||
|
||||
const value = React.useMemo<StorageContextValue>(
|
||||
() => ({
|
||||
async load<K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]> {
|
||||
async load<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
): Promise<StorageSchema[K]> {
|
||||
const current = storageRef.current[key];
|
||||
|
||||
if (
|
||||
|
|
@ -124,7 +128,11 @@ export default function StorageProvider(props: { children: React.ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
return <StorageContext.Provider value={value}>{props.children}</StorageContext.Provider>;
|
||||
return (
|
||||
<StorageContext.Provider value={value}>
|
||||
{props.children}
|
||||
</StorageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useStorage(): StorageContextValue {
|
||||
|
|
@ -133,4 +141,4 @@ export function useStorage(): StorageContextValue {
|
|||
throw new Error("useStorage must be used within a StorageProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,7 +271,9 @@ export default function Provider(props: { children: React.ReactNode }) {
|
|||
if (isStopSendingError(connectError)) {
|
||||
clearReconnectTimer();
|
||||
setError("Connection closed");
|
||||
setErrorDescription("Connection closed");
|
||||
setErrorDescription(
|
||||
"The connection was forcefully closed by the Omikron.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -366,7 +368,9 @@ export default function Provider(props: { children: React.ReactNode }) {
|
|||
|
||||
if (isStopSendingError(identificationError)) {
|
||||
setError("Connection closed");
|
||||
setErrorDescription("Connection closed");
|
||||
setErrorDescription(
|
||||
"The connection was forcefully closed by the Omikron.",
|
||||
);
|
||||
setIdentified(false);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,24 @@ type ActiveConnection = {
|
|||
closeNotified: boolean;
|
||||
};
|
||||
|
||||
class RecoverableMessageDecodeError extends Error {
|
||||
readonly messageId: number;
|
||||
|
||||
readonly messageType: string;
|
||||
|
||||
readonly cause: unknown;
|
||||
|
||||
constructor(messageId: number, messageType: string, cause: unknown) {
|
||||
super(
|
||||
`Failed to decode message payload for "${messageType}" (id=${messageId}): ${formatUnknownError(cause)}`,
|
||||
);
|
||||
this.name = "RecoverableMessageDecodeError";
|
||||
this.messageId = messageId;
|
||||
this.messageType = messageType;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
const COMMUNICATION_TYPES = [
|
||||
"error",
|
||||
"error_protocol",
|
||||
|
|
@ -573,6 +591,33 @@ export function createTransportClient<T extends SchemaMap>(
|
|||
}
|
||||
};
|
||||
|
||||
const handleRecoverableDecodeFailure = (
|
||||
error: RecoverableMessageDecodeError,
|
||||
) => {
|
||||
log(1, "Socket", "yellow", "Recoverable message decode failure", {
|
||||
id: error.messageId,
|
||||
type: error.messageType,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
if (error.messageId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingRequest = pending.get(error.messageId);
|
||||
if (!pendingRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(pendingRequest.timeoutId);
|
||||
pending.delete(error.messageId);
|
||||
pendingRequest.reject(
|
||||
new Error(
|
||||
`Failed to decode response for "${pendingRequest.requestType}": ${formatUnknownError(error.cause)}`,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const startIncomingLoop = (connection: ActiveConnection) => {
|
||||
connection.streamReader =
|
||||
connection.transport.incomingUnidirectionalStreams.getReader();
|
||||
|
|
@ -585,7 +630,18 @@ export function createTransportClient<T extends SchemaMap>(
|
|||
break;
|
||||
}
|
||||
|
||||
const frame = await readFrame(result.value);
|
||||
let frame: TypedMessage | null;
|
||||
try {
|
||||
frame = await readFrame(result.value);
|
||||
} catch (error) {
|
||||
if (error instanceof RecoverableMessageDecodeError) {
|
||||
handleRecoverableDecodeFailure(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (frame === null) {
|
||||
try {
|
||||
connection.transport.close({
|
||||
|
|
@ -780,10 +836,12 @@ export function createTransportClient<T extends SchemaMap>(
|
|||
});
|
||||
|
||||
if (!expectsResponse) {
|
||||
return writeMessage(connection.transport, messageBytes).catch((error) => {
|
||||
closeFromStopSending(connection, error);
|
||||
throw error;
|
||||
});
|
||||
return writeMessage(connection.transport, messageBytes).catch(
|
||||
(error) => {
|
||||
closeFromStopSending(connection, error);
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise<TypedMessage>((resolve, reject) => {
|
||||
|
|
@ -862,6 +920,22 @@ function normalizeName(value: string) {
|
|||
return value.toLowerCase().replaceAll("_", "");
|
||||
}
|
||||
|
||||
function formatUnknownError(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
}
|
||||
|
||||
function isStopSendingError(error: unknown) {
|
||||
if (typeof error === "string") {
|
||||
return error.includes("STOP_SENDING");
|
||||
|
|
@ -1084,12 +1158,24 @@ function decodeCommunicationMessage(frame: Uint8Array): TypedMessage {
|
|||
throw new Error("Communication header exceeds payload length");
|
||||
}
|
||||
|
||||
const messageType = COMMUNICATION_TYPES[typeIndex] ?? "error_protocol";
|
||||
|
||||
const dataLength = payloadLength - consumedHeaderBytes;
|
||||
const dataReader = new ByteReader(reader.readBytes(dataLength));
|
||||
const decodedData = decodeContainerPayload(dataReader);
|
||||
let decodedData: Record<string, unknown>;
|
||||
|
||||
try {
|
||||
decodedData = decodeContainerPayload(dataReader);
|
||||
} catch (error) {
|
||||
throw new RecoverableMessageDecodeError(id, messageType, error);
|
||||
}
|
||||
|
||||
if (!dataReader.isAtEnd()) {
|
||||
throw new Error("Trailing bytes found after communication data payload");
|
||||
throw new RecoverableMessageDecodeError(
|
||||
id,
|
||||
messageType,
|
||||
new Error("Trailing bytes found after communication data payload"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!reader.isAtEnd()) {
|
||||
|
|
@ -1098,7 +1184,7 @@ function decodeCommunicationMessage(frame: Uint8Array): TypedMessage {
|
|||
|
||||
return {
|
||||
id,
|
||||
type: COMMUNICATION_TYPES[typeIndex] ?? "error_protocol",
|
||||
type: messageType,
|
||||
data: decodedData,
|
||||
};
|
||||
}
|
||||
|
|
@ -1427,7 +1513,7 @@ function decodeContainerPayload(reader: ByteReader) {
|
|||
: reader.readBytes(payloadLength);
|
||||
|
||||
const expectedKind = getExpectedKind(key);
|
||||
if (!isMarkerCompatibleWithKind(marker, expectedKind)) {
|
||||
if (!isMarkerCompatibleWithKey(marker, expectedKind, key)) {
|
||||
throw new Error(
|
||||
`Unexpected marker 0x${marker.toString(16)} for data type "${key}"`,
|
||||
);
|
||||
|
|
@ -1477,6 +1563,21 @@ function isMarkerCompatibleWithKind(marker: number, kind: DataKind) {
|
|||
}
|
||||
}
|
||||
|
||||
function isMarkerCompatibleWithKey(
|
||||
marker: number,
|
||||
kind: DataKind,
|
||||
key: string,
|
||||
) {
|
||||
if (
|
||||
SCALAR_NUMBER_ARRAY_DATA_TYPES.has(key) &&
|
||||
marker === DATA_VALUE_KIND_NUMBER
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isMarkerCompatibleWithKind(marker, kind);
|
||||
}
|
||||
|
||||
function normalizeOutgoingValue(type: string, value: unknown) {
|
||||
if (SCALAR_NUMBER_ARRAY_DATA_TYPES.has(type) && typeof value === "number") {
|
||||
return [value];
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import * as React from "react"
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
|
||||
import * as React from "react";
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
||||
|
||||
import { cn } from "../lib/utils"
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: AvatarPrimitive.Root.Props & {
|
||||
size?: "default" | "sm" | "lg"
|
||||
size?: "default" | "sm" | "lg";
|
||||
}) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
|
|
@ -16,11 +16,11 @@ function Avatar({
|
|||
data-size={size}
|
||||
className={cn(
|
||||
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||
|
|
@ -29,11 +29,11 @@ function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
|||
data-slot="avatar-image"
|
||||
className={cn(
|
||||
"aspect-square size-full rounded-full object-cover",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
|
|
@ -45,11 +45,11 @@ function AvatarFallback({
|
|||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
|
|
@ -61,11 +61,11 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
|||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -74,11 +74,11 @@ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarGroupCount({
|
||||
|
|
@ -90,11 +90,11 @@ function AvatarGroupCount({
|
|||
data-slot="avatar-group-count"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs/relaxed text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -104,4 +104,4 @@ export {
|
|||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
AvatarBadge,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "../lib/utils"
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
|
|
@ -35,8 +35,8 @@ const buttonVariants = cva(
|
|||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
|
|
@ -50,7 +50,7 @@ function Button({
|
|||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../lib/utils"
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
function Card({
|
||||
className,
|
||||
|
|
@ -13,11 +13,11 @@ function Card({
|
|||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -26,11 +26,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -40,7 +40,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|||
className={cn("text-sm font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -50,7 +50,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|||
className={cn("text-xs/relaxed text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -59,11 +59,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -73,7 +73,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -82,11 +82,11 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-lg px-4 group-data-[size=sm]/card:px-3 [.border-t]:pt-4 group-data-[size=sm]/card:[.border-t]:pt-3",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -97,4 +97,4 @@ export {
|
|||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
||||
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
||||
|
||||
import { cn } from "../lib/utils"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { cn } from "../lib/utils";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
||||
return (
|
||||
|
|
@ -9,7 +9,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -17,11 +17,10 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
||||
>
|
||||
<CheckIcon
|
||||
/>
|
||||
<CheckIcon />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import * as React from "react"
|
||||
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
|
||||
import * as React from "react";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";
|
||||
|
||||
import { cn } from "../lib/utils"
|
||||
import { ChevronRightIcon, CheckIcon } from "lucide-react"
|
||||
import { cn } from "../lib/utils";
|
||||
import { ChevronRightIcon, CheckIcon } from "lucide-react";
|
||||
|
||||
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
|
|
@ -24,7 +24,7 @@ function ContextMenuTrigger({
|
|||
className={cn("select-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
|
|
@ -50,18 +50,21 @@ function ContextMenuContent({
|
|||
>
|
||||
<ContextMenuPrimitive.Popup
|
||||
data-slot="context-menu-content"
|
||||
className={cn("z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!", className )}
|
||||
className={cn(
|
||||
"z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Positioner>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
|
|
@ -69,7 +72,7 @@ function ContextMenuLabel({
|
|||
inset,
|
||||
...props
|
||||
}: ContextMenuPrimitive.GroupLabel.Props & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.GroupLabel
|
||||
|
|
@ -77,11 +80,11 @@ function ContextMenuLabel({
|
|||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-xs text-muted-foreground data-inset:pl-7.5",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
|
|
@ -90,8 +93,8 @@ function ContextMenuItem({
|
|||
variant = "default",
|
||||
...props
|
||||
}: ContextMenuPrimitive.Item.Props & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
|
|
@ -100,17 +103,17 @@ function ContextMenuItem({
|
|||
data-variant={variant}
|
||||
className={cn(
|
||||
"group/context-menu-item relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 data-[variant=destructive]:*:[svg]:text-destructive",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
|
|
@ -119,7 +122,7 @@ function ContextMenuSubTrigger({
|
|||
children,
|
||||
...props
|
||||
}: ContextMenuPrimitive.SubmenuTrigger.Props & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubmenuTrigger
|
||||
|
|
@ -127,14 +130,14 @@ function ContextMenuSubTrigger({
|
|||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubmenuTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
|
|
@ -147,7 +150,7 @@ function ContextMenuSubContent({
|
|||
side="right"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
|
|
@ -157,7 +160,7 @@ function ContextMenuCheckboxItem({
|
|||
inset,
|
||||
...props
|
||||
}: ContextMenuPrimitive.CheckboxItem.Props & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
|
|
@ -165,20 +168,19 @@ function ContextMenuCheckboxItem({
|
|||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||
<ContextMenuPrimitive.CheckboxItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
<CheckIcon />
|
||||
</ContextMenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
|
|
@ -189,7 +191,7 @@ function ContextMenuRadioGroup({
|
|||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
|
|
@ -198,7 +200,7 @@ function ContextMenuRadioItem({
|
|||
inset,
|
||||
...props
|
||||
}: ContextMenuPrimitive.RadioItem.Props & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
|
|
@ -206,19 +208,18 @@ function ContextMenuRadioItem({
|
|||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||
<ContextMenuPrimitive.RadioItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
<CheckIcon />
|
||||
</ContextMenuPrimitive.RadioItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
|
|
@ -231,7 +232,7 @@ function ContextMenuSeparator({
|
|||
className={cn("-mx-1 my-1 h-px bg-border/50", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
|
|
@ -243,11 +244,11 @@ function ContextMenuShortcut({
|
|||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-[0.625rem] tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -266,4 +267,4 @@ export {
|
|||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react"
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||
import * as React from "react";
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input";
|
||||
|
||||
import { cn } from "../lib/utils"
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
|
|
@ -10,11 +10,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||
data-slot="input"
|
||||
className={cn(
|
||||
"h-7 w-full min-w-0 rounded-md border border-input bg-input/20 px-2 py-0.5 text-sm transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs/relaxed file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../lib/utils"
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
return (
|
||||
|
|
@ -8,11 +8,11 @@ function Label({ className, ...props }: React.ComponentProps<"label">) {
|
|||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-xs/relaxed 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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
TriangleAlertIcon,
|
||||
OctagonXIcon,
|
||||
Loader2Icon,
|
||||
} from "lucide-react";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: (
|
||||
<CircleCheckIcon className="size-4" />
|
||||
),
|
||||
info: (
|
||||
<InfoIcon className="size-4" />
|
||||
),
|
||||
warning: (
|
||||
<TriangleAlertIcon className="size-4" />
|
||||
),
|
||||
error: (
|
||||
<OctagonXIcon className="size-4" />
|
||||
),
|
||||
loading: (
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
),
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
|
|
@ -41,7 +37,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster }
|
||||
export { Toaster };
|
||||
|
|
|
|||
|
|
@ -6,124 +6,124 @@
|
|||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.148 0.004 228.8);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.148 0.004 228.8);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.148 0.004 228.8);
|
||||
--primary: oklch(0.511 0.096 186.391);
|
||||
--primary-foreground: oklch(0.984 0.014 180.72);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.963 0.002 197.1);
|
||||
--muted-foreground: oklch(0.56 0.021 213.5);
|
||||
--accent: oklch(0.963 0.002 197.1);
|
||||
--accent-foreground: oklch(0.218 0.008 223.9);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.925 0.005 214.3);
|
||||
--input: oklch(0.925 0.005 214.3);
|
||||
--ring: oklch(0.723 0.014 214.4);
|
||||
--chart-1: oklch(0.855 0.138 181.071);
|
||||
--chart-2: oklch(0.704 0.14 182.503);
|
||||
--chart-3: oklch(0.6 0.118 184.704);
|
||||
--chart-4: oklch(0.511 0.096 186.391);
|
||||
--chart-5: oklch(0.437 0.078 188.216);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.987 0.002 197.1);
|
||||
--sidebar-foreground: oklch(0.148 0.004 228.8);
|
||||
--sidebar-primary: oklch(0.6 0.118 184.704);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.014 180.72);
|
||||
--sidebar-accent: oklch(0.963 0.002 197.1);
|
||||
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
|
||||
--sidebar-border: oklch(0.925 0.005 214.3);
|
||||
--sidebar-ring: oklch(0.723 0.014 214.4);
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.148 0.004 228.8);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.148 0.004 228.8);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.148 0.004 228.8);
|
||||
--primary: oklch(0.511 0.096 186.391);
|
||||
--primary-foreground: oklch(0.984 0.014 180.72);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.963 0.002 197.1);
|
||||
--muted-foreground: oklch(0.56 0.021 213.5);
|
||||
--accent: oklch(0.963 0.002 197.1);
|
||||
--accent-foreground: oklch(0.218 0.008 223.9);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.925 0.005 214.3);
|
||||
--input: oklch(0.925 0.005 214.3);
|
||||
--ring: oklch(0.723 0.014 214.4);
|
||||
--chart-1: oklch(0.855 0.138 181.071);
|
||||
--chart-2: oklch(0.704 0.14 182.503);
|
||||
--chart-3: oklch(0.6 0.118 184.704);
|
||||
--chart-4: oklch(0.511 0.096 186.391);
|
||||
--chart-5: oklch(0.437 0.078 188.216);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.987 0.002 197.1);
|
||||
--sidebar-foreground: oklch(0.148 0.004 228.8);
|
||||
--sidebar-primary: oklch(0.6 0.118 184.704);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.014 180.72);
|
||||
--sidebar-accent: oklch(0.963 0.002 197.1);
|
||||
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
|
||||
--sidebar-border: oklch(0.925 0.005 214.3);
|
||||
--sidebar-ring: oklch(0.723 0.014 214.4);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.148 0.004 228.8);
|
||||
--foreground: oklch(0.987 0.002 197.1);
|
||||
--card: oklch(0.218 0.008 223.9);
|
||||
--card-foreground: oklch(0.987 0.002 197.1);
|
||||
--popover: oklch(0.218 0.008 223.9);
|
||||
--popover-foreground: oklch(0.987 0.002 197.1);
|
||||
--primary: oklch(0.437 0.078 188.216);
|
||||
--primary-foreground: oklch(0.984 0.014 180.72);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.275 0.011 216.9);
|
||||
--muted-foreground: oklch(0.723 0.014 214.4);
|
||||
--accent: oklch(0.275 0.011 216.9);
|
||||
--accent-foreground: oklch(0.987 0.002 197.1);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.56 0.021 213.5);
|
||||
--chart-1: oklch(0.855 0.138 181.071);
|
||||
--chart-2: oklch(0.704 0.14 182.503);
|
||||
--chart-3: oklch(0.6 0.118 184.704);
|
||||
--chart-4: oklch(0.511 0.096 186.391);
|
||||
--chart-5: oklch(0.437 0.078 188.216);
|
||||
--sidebar: oklch(0.218 0.008 223.9);
|
||||
--sidebar-foreground: oklch(0.987 0.002 197.1);
|
||||
--sidebar-primary: oklch(0.704 0.14 182.503);
|
||||
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
|
||||
--sidebar-accent: oklch(0.275 0.011 216.9);
|
||||
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.56 0.021 213.5);
|
||||
--background: oklch(0.148 0.004 228.8);
|
||||
--foreground: oklch(0.987 0.002 197.1);
|
||||
--card: oklch(0.218 0.008 223.9);
|
||||
--card-foreground: oklch(0.987 0.002 197.1);
|
||||
--popover: oklch(0.218 0.008 223.9);
|
||||
--popover-foreground: oklch(0.987 0.002 197.1);
|
||||
--primary: oklch(0.437 0.078 188.216);
|
||||
--primary-foreground: oklch(0.984 0.014 180.72);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.275 0.011 216.9);
|
||||
--muted-foreground: oklch(0.723 0.014 214.4);
|
||||
--accent: oklch(0.275 0.011 216.9);
|
||||
--accent-foreground: oklch(0.987 0.002 197.1);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.56 0.021 213.5);
|
||||
--chart-1: oklch(0.855 0.138 181.071);
|
||||
--chart-2: oklch(0.704 0.14 182.503);
|
||||
--chart-3: oklch(0.6 0.118 184.704);
|
||||
--chart-4: oklch(0.511 0.096 186.391);
|
||||
--chart-5: oklch(0.437 0.078 188.216);
|
||||
--sidebar: oklch(0.218 0.008 223.9);
|
||||
--sidebar-foreground: oklch(0.987 0.002 197.1);
|
||||
--sidebar-primary: oklch(0.704 0.14 182.503);
|
||||
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
|
||||
--sidebar-accent: oklch(0.275 0.011 216.9);
|
||||
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.56 0.021 213.5);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'Inter Variable', sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,79 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system"
|
||||
type ResolvedTheme = "dark" | "light"
|
||||
type Theme = "dark" | "light" | "system";
|
||||
type ResolvedTheme = "dark" | "light";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode
|
||||
defaultTheme?: Theme
|
||||
storageKey?: string
|
||||
disableTransitionOnChange?: boolean
|
||||
}
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
disableTransitionOnChange?: boolean;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
|
||||
const THEME_VALUES: Theme[] = ["dark", "light", "system"]
|
||||
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)";
|
||||
const THEME_VALUES: Theme[] = ["dark", "light", "system"];
|
||||
|
||||
const ThemeProviderContext = React.createContext<
|
||||
ThemeProviderState | undefined
|
||||
>(undefined)
|
||||
>(undefined);
|
||||
|
||||
function isTheme(value: string | null): value is Theme {
|
||||
if (value === null) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
return THEME_VALUES.includes(value as Theme)
|
||||
return THEME_VALUES.includes(value as Theme);
|
||||
}
|
||||
|
||||
function getSystemTheme(): ResolvedTheme {
|
||||
if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
|
||||
return "dark"
|
||||
return "dark";
|
||||
}
|
||||
|
||||
return "light"
|
||||
return "light";
|
||||
}
|
||||
|
||||
function disableTransitionsTemporarily() {
|
||||
const style = document.createElement("style")
|
||||
const style = document.createElement("style");
|
||||
style.appendChild(
|
||||
document.createTextNode(
|
||||
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
|
||||
)
|
||||
)
|
||||
document.head.appendChild(style)
|
||||
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}",
|
||||
),
|
||||
);
|
||||
document.head.appendChild(style);
|
||||
|
||||
return () => {
|
||||
window.getComputedStyle(document.body)
|
||||
window.getComputedStyle(document.body);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
style.remove()
|
||||
})
|
||||
})
|
||||
}
|
||||
style.remove();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function isEditableTarget(target: EventTarget | null) {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.isContentEditable) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
const editableParent = target.closest(
|
||||
"input, textarea, select, [contenteditable='true']"
|
||||
)
|
||||
"input, textarea, select, [contenteditable='true']",
|
||||
);
|
||||
if (editableParent) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
|
|
@ -84,76 +84,76 @@ export function ThemeProvider({
|
|||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setThemeState] = React.useState<Theme>(() => {
|
||||
const storedTheme = localStorage.getItem(storageKey)
|
||||
const storedTheme = localStorage.getItem(storageKey);
|
||||
if (isTheme(storedTheme)) {
|
||||
return storedTheme
|
||||
return storedTheme;
|
||||
}
|
||||
|
||||
return defaultTheme
|
||||
})
|
||||
return defaultTheme;
|
||||
});
|
||||
|
||||
const setTheme = React.useCallback(
|
||||
(nextTheme: Theme) => {
|
||||
localStorage.setItem(storageKey, nextTheme)
|
||||
setThemeState(nextTheme)
|
||||
localStorage.setItem(storageKey, nextTheme);
|
||||
setThemeState(nextTheme);
|
||||
},
|
||||
[storageKey]
|
||||
)
|
||||
[storageKey],
|
||||
);
|
||||
|
||||
const applyTheme = React.useCallback(
|
||||
(nextTheme: Theme) => {
|
||||
const root = document.documentElement
|
||||
const root = document.documentElement;
|
||||
const resolvedTheme =
|
||||
nextTheme === "system" ? getSystemTheme() : nextTheme
|
||||
nextTheme === "system" ? getSystemTheme() : nextTheme;
|
||||
const restoreTransitions = disableTransitionOnChange
|
||||
? disableTransitionsTemporarily()
|
||||
: null
|
||||
: null;
|
||||
|
||||
root.classList.remove("light", "dark")
|
||||
root.classList.add(resolvedTheme)
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(resolvedTheme);
|
||||
|
||||
if (restoreTransitions) {
|
||||
restoreTransitions()
|
||||
restoreTransitions();
|
||||
}
|
||||
},
|
||||
[disableTransitionOnChange]
|
||||
)
|
||||
[disableTransitionOnChange],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
applyTheme(theme)
|
||||
applyTheme(theme);
|
||||
|
||||
if (theme !== "system") {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
|
||||
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY);
|
||||
const handleChange = () => {
|
||||
applyTheme("system")
|
||||
}
|
||||
applyTheme("system");
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange)
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange)
|
||||
}
|
||||
}, [theme, applyTheme])
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, [theme, applyTheme]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.repeat) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.metaKey || event.ctrlKey || event.altKey) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditableTarget(event.target)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() !== "d") {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
setThemeState((currentTheme) => {
|
||||
|
|
@ -164,66 +164,66 @@ export function ThemeProvider({
|
|||
? "dark"
|
||||
: getSystemTheme() === "dark"
|
||||
? "light"
|
||||
: "dark"
|
||||
: "dark";
|
||||
|
||||
localStorage.setItem(storageKey, nextTheme)
|
||||
return nextTheme
|
||||
})
|
||||
}
|
||||
localStorage.setItem(storageKey, nextTheme);
|
||||
return nextTheme;
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [storageKey])
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [storageKey]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.storageArea !== localStorage) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== storageKey) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTheme(event.newValue)) {
|
||||
setThemeState(event.newValue)
|
||||
return
|
||||
setThemeState(event.newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
setThemeState(defaultTheme)
|
||||
}
|
||||
setThemeState(defaultTheme);
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handleStorageChange)
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorageChange)
|
||||
}
|
||||
}, [defaultTheme, storageKey])
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
};
|
||||
}, [defaultTheme, storageKey]);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
setTheme,
|
||||
}),
|
||||
[theme, setTheme]
|
||||
)
|
||||
[theme, setTheme],
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = React.useContext(ThemeProviderContext)
|
||||
const context = React.useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue