diff --git a/text2sql-backend/.gitignore b/text2sql-backend/.gitignore index 7895cd44..c9ba56d7 100644 --- a/text2sql-backend/.gitignore +++ b/text2sql-backend/.gitignore @@ -64,9 +64,9 @@ cover/ local_settings.py # Test dbs -*.sqlite3 -*.sqlite3-journal -*.db +db.sqlite3 +db.sqlite3-journal +test.sqlite3 # Flask stuff: instance/ diff --git a/text2sql-backend/app.py b/text2sql-backend/app.py index 2754e60a..417680ff 100644 --- a/text2sql-backend/app.py +++ b/text2sql-backend/app.py @@ -7,7 +7,7 @@ from dataline.api.connection.router import router as connection_router from dataline.api.settings.router import router as settings_router -from dataline.repositories.base import NotFoundError +from dataline.repositories.base import NotFoundError, NotUniqueError logger = logging.getLogger(__name__) @@ -15,6 +15,8 @@ def handle_exceptions(request: Request, e: Exception) -> JSONResponse: if isinstance(e, NotFoundError): return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={"message": e.message}) + elif isinstance(e, NotUniqueError): + return JSONResponse(status_code=status.HTTP_409_CONFLICT, content={"message": e.message}) logger.exception(e) return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"message": str(e)}) @@ -35,4 +37,6 @@ def __init__(self) -> None: self.include_router(connection_router) # Handle 500s separately to play well with TestClient and allow re-raising in tests + self.add_exception_handler(NotFoundError, handle_exceptions) + self.add_exception_handler(NotUniqueError, handle_exceptions) self.add_exception_handler(Exception, handle_exceptions) diff --git a/text2sql-backend/dataline/api/connection/router.py b/text2sql-backend/dataline/api/connection/router.py index 5734509f..f67564e0 100644 --- a/text2sql-backend/dataline/api/connection/router.py +++ b/text2sql-backend/dataline/api/connection/router.py @@ -15,7 +15,7 @@ GetConnectionOut, TableSchemasOut, ) -from dataline.repositories.base import NotFoundError +from dataline.repositories.base import NotFoundError, NotUniqueError from dataline.utils import get_sqlite_dsn from models import StatusType, SuccessResponse, UpdateConnectionRequest from services import SchemaService @@ -49,7 +49,7 @@ def create_db_connection(dsn: str, name: str, is_sample: bool = False) -> Succes try: existing_connection = db.get_connection_from_dsn(dsn) if existing_connection: - return SuccessResponse(status=StatusType.ok, data=existing_connection) + raise NotUniqueError("Connection already exists.") except NotFoundError: pass @@ -92,9 +92,12 @@ def validate_dsn_format(cls, value: str) -> str: dsn_regex = r"^[\w\+]+:\/\/[\/\w-]+.*$" if not re.match(dsn_regex, value): - raise ValueError( - 'Invalid DSN format. The expected format is "driver://username:password@host:port/database".' - ) + raise ValueError("Invalid DSN format.") + + # Simpler way to connect to postgres even though officially deprecated + # This mirrors psql which is a very common way to connect to postgres + if "postgres://" in value: + value = value.replace("postgres://", "postgresql://") return value @@ -102,7 +105,7 @@ def validate_dsn_format(cls, value: str) -> str: @router.post("/create-sample-db") async def create_sample_db() -> SuccessResponse[ConnectionOut]: name = "DVD Rental (Sample)" - dsn = get_sqlite_dsn(config.sample_postgres_path) + dsn = get_sqlite_dsn(config.sample_dvdrental_path) return create_db_connection(dsn, name, is_sample=True) diff --git a/text2sql-backend/dataline/config.py b/text2sql-backend/dataline/config.py index ccd37735..15264f5b 100644 --- a/text2sql-backend/dataline/config.py +++ b/text2sql-backend/dataline/config.py @@ -10,7 +10,9 @@ class Config(BaseSettings): sqlite_path: str = str(Path(__file__).parent / "db.sqlite3") sqlite_echo: bool = False - sample_postgres_path: str = str(Path(__file__).parent / "samples" / "postgres" / "dvd_rental.sqlite3") + sample_dvdrental_path: str = str(Path(__file__).parent / "samples" / "dvd_rental.sqlite3") + sample_netflix_path: str = str(Path(__file__).parent / "samples" / "netflix.sqlite3") + sample_titanic_path: str = str(Path(__file__).parent / "samples" / "titanic.sqlite3") config = Config() diff --git a/text2sql-backend/dataline/models/conversation/model.py b/text2sql-backend/dataline/models/conversation/model.py index 533d9fc0..d9c8de81 100644 --- a/text2sql-backend/dataline/models/conversation/model.py +++ b/text2sql-backend/dataline/models/conversation/model.py @@ -1,7 +1,7 @@ from uuid import UUID from sqlalchemy import ForeignKey, Integer, String -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy.orm import Mapped, mapped_column from dataline.models.base import DBModel from dataline.models.connection import ConnectionModel diff --git a/text2sql-backend/dataline/samples/postgres/dvd_rental.sqlite3 b/text2sql-backend/dataline/samples/dvd_rental.sqlite3 similarity index 100% rename from text2sql-backend/dataline/samples/postgres/dvd_rental.sqlite3 rename to text2sql-backend/dataline/samples/dvd_rental.sqlite3 diff --git a/text2sql-backend/dataline/samples/netflix.sqlite3 b/text2sql-backend/dataline/samples/netflix.sqlite3 new file mode 100644 index 00000000..559c8609 Binary files /dev/null and b/text2sql-backend/dataline/samples/netflix.sqlite3 differ diff --git a/text2sql-backend/dataline/samples/titanic.sqlite3 b/text2sql-backend/dataline/samples/titanic.sqlite3 new file mode 100644 index 00000000..5278cf0f Binary files /dev/null and b/text2sql-backend/dataline/samples/titanic.sqlite3 differ diff --git a/text2sql-backend/db.py b/text2sql-backend/db.py index 10767e17..b072bd0b 100644 --- a/text2sql-backend/db.py +++ b/text2sql-backend/db.py @@ -26,9 +26,10 @@ @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection: Any, connection_record: Any) -> None: # type: ignore[misc] - cursor = dbapi_connection.cursor() - cursor.execute("PRAGMA foreign_keys=ON") - cursor.close() + if type(dbapi_connection) is sqlite3.Connection: # play well with other DB backends + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() # Old way of using database - this is a single connection, hard to manage transactions diff --git a/text2sql-backend/main.py b/text2sql-backend/main.py index 55c54ed0..9f643d2a 100644 --- a/text2sql-backend/main.py +++ b/text2sql-backend/main.py @@ -15,6 +15,7 @@ from dataline.services.settings import SettingsService from models import ( ConversationWithMessagesWithResults, + Conversation, DataResult, MessageWithResults, Result, @@ -88,6 +89,11 @@ async def delete_conversation(conversation_id: str) -> dict[str, str]: return {"status": "ok"} +@app.get("/conversation/{conversation_id}") +async def get_conversation(conversation_id: str) -> SuccessResponse[Conversation]: + return SuccessResponse(status=StatusType.ok, data=db.get_conversation(conversation_id)) + + class ListMessageOut(BaseModel): messages: list[MessageWithResults] diff --git a/text2sql-backend/tests/api/test_connection.py b/text2sql-backend/tests/api/test_connection.py index 8d30349a..520a0c42 100644 --- a/text2sql-backend/tests/api/test_connection.py +++ b/text2sql-backend/tests/api/test_connection.py @@ -1,5 +1,6 @@ import logging import pathlib +from typing import AsyncGenerator import pytest import pytest_asyncio @@ -22,11 +23,35 @@ async def test_create_sample_db_connection(client: TestClient) -> None: assert data["is_sample"] is True assert data["id"] + # TODO: Remove after sqlalchemy migration + # Manual rollback + client.delete(f"/connection/{data['id']}") + @pytest_asyncio.fixture -async def sample_db(client: TestClient) -> Connection: +async def sample_db(client: TestClient) -> AsyncGenerator[Connection, None]: + response = client.post("/create-sample-db") + assert response.status_code == 200 + connection = Connection(**response.json()["data"]) + + # TODO: Remove after sqlalchemy migration + # Manual rollback + yield connection + client.delete(f"/connection/{str(connection.id)}") + + +@pytest.mark.asyncio +async def test_create_sample_db_connection_twice_409(client: TestClient) -> None: + response = client.post("/create-sample-db") + assert response.status_code == 200 + connection = Connection(**response.json()["data"]) + response = client.post("/create-sample-db") - return Connection(**response.json()["data"]) + assert response.status_code == 409 + + # TODO: Remove after sqlalchemy migration + # Manual rollback + client.delete(f"/connection/{str(connection.id)}") @pytest.mark.asyncio @@ -74,6 +99,10 @@ async def test_connect_db(client: TestClient) -> None: # Delete database after tests pathlib.Path("test.db").unlink(missing_ok=True) + # TODO: Remove after sqlalchemy migration + # Manual rollback + client.delete(f"/connection/{data['id']}") + @pytest.mark.asyncio async def test_get_table_schemas(client: TestClient, sample_db: Connection) -> None: @@ -140,19 +169,6 @@ async def test_update_table_schema_field_description(client: TestClient, example assert field.description == update_in["description"] -@pytest.mark.asyncio -@pytest.mark.skip(reason="Do not want to deal with this now") -async def test_delete_connection(client: TestClient, sample_db: Connection) -> None: - response = client.delete(f"/connection/{str(sample_db.id)}") - - assert response.status_code == 200 - - # Check if the connection was deleted - response = client.get("/connections") - data = response.json()["data"] - assert len(data["connections"]) == 0 - - @pytest.mark.asyncio async def test_update_connection(client: TestClient, sample_db: Connection) -> None: update_in = { @@ -169,3 +185,15 @@ async def test_update_connection(client: TestClient, sample_db: Connection) -> N # Delete database after tests pathlib.Path("new.db").unlink(missing_ok=True) + + +@pytest.mark.asyncio +async def test_delete_connection(client: TestClient, sample_db: Connection) -> None: + response = client.delete(f"/connection/{str(sample_db.id)}") + + assert response.status_code == 200 + + # Check if the connection was deleted + response = client.get("/connections") + data = response.json()["data"] + assert len(data["connections"]) == 0 diff --git a/text2sql-backend/tests/api/test_settings.py b/text2sql-backend/tests/api/test_settings.py index 8b32a607..c843173f 100644 --- a/text2sql-backend/tests/api/test_settings.py +++ b/text2sql-backend/tests/api/test_settings.py @@ -6,7 +6,6 @@ import pytest_asyncio from fastapi.testclient import TestClient -from dataline.repositories.base import NotFoundError logger = logging.getLogger(__name__) @@ -97,9 +96,8 @@ async def test_get_info(client: TestClient, user_info: dict[str, str]) -> None: @pytest.mark.asyncio async def test_get_info_not_found(client: TestClient) -> None: - with pytest.raises(NotFoundError): - response = client.get("/settings/info") - assert response.status_code == 404 + response = client.get("/settings/info") + assert response.status_code == 404 FileTuple = tuple[str, tuple[str, BytesIO, str]] diff --git a/text2sql-backend/tests/conftest.py b/text2sql-backend/tests/conftest.py index 1954fd9c..e0e870c7 100644 --- a/text2sql-backend/tests/conftest.py +++ b/text2sql-backend/tests/conftest.py @@ -32,7 +32,7 @@ async def engine() -> AsyncGenerator[AsyncEngine, None]: pathlib.Path("test.sqlite3").unlink(missing_ok=True) -@pytest_asyncio.fixture +@pytest_asyncio.fixture(scope="function") async def session(engine: AsyncEngine, monkeypatch: pytest.MonkeyPatch) -> AsyncGenerator[AsyncSession, None]: async with AsyncSession(engine) as session, session.begin(): # prevent test from committing anything, only flush diff --git a/text2sql-frontend/src/api.ts b/text2sql-frontend/src/api.ts index d13c9792..c7c75756 100644 --- a/text2sql-frontend/src/api.ts +++ b/text2sql-frontend/src/api.ts @@ -57,6 +57,7 @@ export type ConnectionResult = { database: string; name: string; dialect: string; + is_sample: boolean; }; type ConnectResult = ApiResponse; const createConnection = async ( diff --git a/text2sql-frontend/src/components/Catalyst/alert.tsx b/text2sql-frontend/src/components/Catalyst/alert.tsx index 0fe11f1a..3a3d4e3c 100644 --- a/text2sql-frontend/src/components/Catalyst/alert.tsx +++ b/text2sql-frontend/src/components/Catalyst/alert.tsx @@ -6,32 +6,35 @@ import { Transition as HeadlessTransition, TransitionChild as HeadlessTransitionChild, type DialogProps as HeadlessDialogProps, -} from '@headlessui/react' -import clsx from 'clsx' -import type React from 'react' -import { Fragment } from 'react' -import { Text } from './text' +} from "@headlessui/react"; +import clsx from "clsx"; +import type React from "react"; +import { Fragment } from "react"; +import { Text } from "./text"; const sizes = { - xs: 'sm:max-w-xs', - sm: 'sm:max-w-sm', - md: 'sm:max-w-md', - lg: 'sm:max-w-lg', - xl: 'sm:max-w-xl', - '2xl': 'sm:max-w-2xl', - '3xl': 'sm:max-w-3xl', - '4xl': 'sm:max-w-4xl', - '5xl': 'sm:max-w-5xl', -} + xs: "sm:max-w-xs", + sm: "sm:max-w-sm", + md: "sm:max-w-md", + lg: "sm:max-w-lg", + xl: "sm:max-w-xl", + "2xl": "sm:max-w-2xl", + "3xl": "sm:max-w-3xl", + "4xl": "sm:max-w-4xl", + "5xl": "sm:max-w-5xl", +}; export function Alert({ open, onClose, - size = 'md', + size = "md", className, children, ...props -}: { size?: keyof typeof sizes; children: React.ReactNode } & HeadlessDialogProps) { +}: { + size?: keyof typeof sizes; + children: React.ReactNode; +} & HeadlessDialogProps) { return ( @@ -44,7 +47,7 @@ export function Alert({ leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
- ) + ); } -export function AlertTitle({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { +export function AlertTitle({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { return ( - ) + ); } -export function AlertDescription({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { +export function AlertDescription({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { return ( - ) + ); } -export function AlertBody({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { - return
+export function AlertBody({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return
; } -export function AlertActions({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { +export function AlertActions({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { return (
- ) + ); } diff --git a/text2sql-frontend/src/components/Catalyst/badge.tsx b/text2sql-frontend/src/components/Catalyst/badge.tsx index fc26845b..21bc7cf1 100644 --- a/text2sql-frontend/src/components/Catalyst/badge.tsx +++ b/text2sql-frontend/src/components/Catalyst/badge.tsx @@ -1,70 +1,84 @@ -import { Button as HeadlessButton, type ButtonProps as HeadlessButtonProps } from '@headlessui/react' -import clsx from 'clsx' -import React from 'react' -import { TouchTarget } from './button' -import { Link } from './link' +import { + Button as HeadlessButton, + type ButtonProps as HeadlessButtonProps, +} from "@headlessui/react"; +import clsx from "clsx"; +import React from "react"; +import { TouchTarget } from "./button"; +import { Link } from "./link"; const colors = { - red: 'bg-red-500/15 text-red-700 group-data-[hover]:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-[hover]:bg-red-500/20', + red: "bg-red-500/15 text-red-700 group-data-[hover]:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-[hover]:bg-red-500/20", orange: - 'bg-orange-500/15 text-orange-700 group-data-[hover]:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-[hover]:bg-orange-500/20', + "bg-orange-500/15 text-orange-700 group-data-[hover]:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-[hover]:bg-orange-500/20", amber: - 'bg-amber-400/20 text-amber-700 group-data-[hover]:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-[hover]:bg-amber-400/15', + "bg-amber-400/20 text-amber-700 group-data-[hover]:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-[hover]:bg-amber-400/15", yellow: - 'bg-yellow-400/20 text-yellow-700 group-data-[hover]:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-[hover]:bg-yellow-400/15', - lime: 'bg-lime-400/20 text-lime-700 group-data-[hover]:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-[hover]:bg-lime-400/15', + "bg-yellow-400/20 text-yellow-700 group-data-[hover]:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-[hover]:bg-yellow-400/15", + lime: "bg-lime-400/20 text-lime-700 group-data-[hover]:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-[hover]:bg-lime-400/15", green: - 'bg-green-500/15 text-green-700 group-data-[hover]:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-[hover]:bg-green-500/20', + "bg-green-500/15 text-green-700 group-data-[hover]:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-[hover]:bg-green-500/20", emerald: - 'bg-emerald-500/15 text-emerald-700 group-data-[hover]:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-[hover]:bg-emerald-500/20', - teal: 'bg-teal-500/15 text-teal-700 group-data-[hover]:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-[hover]:bg-teal-500/20', - cyan: 'bg-cyan-400/20 text-cyan-700 group-data-[hover]:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-[hover]:bg-cyan-400/15', - sky: 'bg-sky-500/15 text-sky-700 group-data-[hover]:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-[hover]:bg-sky-500/20', - blue: 'bg-blue-500/15 text-blue-700 group-data-[hover]:bg-blue-500/25 dark:text-blue-400 dark:group-data-[hover]:bg-blue-500/25', + "bg-emerald-500/15 text-emerald-700 group-data-[hover]:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-[hover]:bg-emerald-500/20", + teal: "bg-teal-500/15 text-teal-700 group-data-[hover]:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-[hover]:bg-teal-500/20", + cyan: "bg-cyan-400/20 text-cyan-700 group-data-[hover]:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-[hover]:bg-cyan-400/15", + sky: "bg-sky-500/15 text-sky-700 group-data-[hover]:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-[hover]:bg-sky-500/20", + blue: "bg-blue-500/15 text-blue-700 group-data-[hover]:bg-blue-500/25 dark:text-blue-400 dark:group-data-[hover]:bg-blue-500/25", indigo: - 'bg-indigo-500/15 text-indigo-700 group-data-[hover]:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-[hover]:bg-indigo-500/20', + "bg-indigo-500/15 text-indigo-700 group-data-[hover]:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-[hover]:bg-indigo-500/20", violet: - 'bg-violet-500/15 text-violet-700 group-data-[hover]:bg-violet-500/25 dark:text-violet-400 dark:group-data-[hover]:bg-violet-500/20', + "bg-violet-500/15 text-violet-700 group-data-[hover]:bg-violet-500/25 dark:text-violet-400 dark:group-data-[hover]:bg-violet-500/20", purple: - 'bg-purple-500/15 text-purple-700 group-data-[hover]:bg-purple-500/25 dark:text-purple-400 dark:group-data-[hover]:bg-purple-500/20', + "bg-purple-500/15 text-purple-700 group-data-[hover]:bg-purple-500/25 dark:text-purple-400 dark:group-data-[hover]:bg-purple-500/20", fuchsia: - 'bg-fuchsia-400/15 text-fuchsia-700 group-data-[hover]:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-[hover]:bg-fuchsia-400/20', - pink: 'bg-pink-400/15 text-pink-700 group-data-[hover]:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-[hover]:bg-pink-400/20', - rose: 'bg-rose-400/15 text-rose-700 group-data-[hover]:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-[hover]:bg-rose-400/20', - zinc: 'bg-zinc-600/10 text-zinc-700 group-data-[hover]:bg-zinc-600/20 dark:bg-white/5 dark:text-zinc-400 dark:group-data-[hover]:bg-white/10', -} + "bg-fuchsia-400/15 text-fuchsia-700 group-data-[hover]:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-[hover]:bg-fuchsia-400/20", + pink: "bg-pink-400/15 text-pink-700 group-data-[hover]:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-[hover]:bg-pink-400/20", + rose: "bg-rose-400/15 text-rose-700 group-data-[hover]:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-[hover]:bg-rose-400/20", + zinc: "bg-gray-600/10 text-gray-700 group-data-[hover]:bg-gray-600/20 dark:bg-white/5 dark:text-gray-400 dark:group-data-[hover]:bg-white/10", +}; -type BadgeProps = { color?: keyof typeof colors } +type BadgeProps = { color?: keyof typeof colors }; -export function Badge({ color = 'zinc', className, ...props }: BadgeProps & React.ComponentPropsWithoutRef<'span'>) { +export function Badge({ + color = "zinc", + className, + ...props +}: BadgeProps & React.ComponentPropsWithoutRef<"span">) { return ( - ) + ); } export const BadgeButton = React.forwardRef(function BadgeButton( { - color = 'zinc', + color = "zinc", className, children, ...props - }: BadgeProps & { children: React.ReactNode } & (HeadlessButtonProps | React.ComponentPropsWithoutRef), + }: BadgeProps & { children: React.ReactNode } & ( + | HeadlessButtonProps + | React.ComponentPropsWithoutRef + ), ref: React.ForwardedRef ) { let classes = clsx( className, - 'group relative inline-flex rounded-md focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500' - ) + "group relative inline-flex rounded-md focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500" + ); - return 'href' in props ? ( - }> + return "href" in props ? ( + } + > {children} @@ -75,5 +89,5 @@ export const BadgeButton = React.forwardRef(function BadgeButton( {children} - ) -}) + ); +}); diff --git a/text2sql-frontend/src/components/Catalyst/button.tsx b/text2sql-frontend/src/components/Catalyst/button.tsx index ed7874f0..636635a5 100644 --- a/text2sql-frontend/src/components/Catalyst/button.tsx +++ b/text2sql-frontend/src/components/Catalyst/button.tsx @@ -1,186 +1,192 @@ -import { Button as HeadlessButton, type ButtonProps as HeadlessButtonProps } from '@headlessui/react' -import { clsx } from 'clsx' -import React from 'react' -import { Link } from './link' +import { + Button as HeadlessButton, + type ButtonProps as HeadlessButtonProps, +} from "@headlessui/react"; +import { clsx } from "clsx"; +import React from "react"; +import { Link } from "./link"; const styles = { base: [ // Base - 'relative isolate inline-flex items-center justify-center gap-x-2 rounded-lg border text-base/6 font-semibold', + "relative isolate inline-flex items-center justify-center gap-x-2 rounded-lg border text-base/6 font-semibold", // Sizing - 'px-[calc(theme(spacing[3.5])-1px)] py-[calc(theme(spacing[2.5])-1px)] sm:px-[calc(theme(spacing.3)-1px)] sm:py-[calc(theme(spacing[1.5])-1px)] sm:text-sm/6', + "px-[calc(theme(spacing[3.5])-1px)] py-[calc(theme(spacing[2.5])-1px)] sm:px-[calc(theme(spacing.3)-1px)] sm:py-[calc(theme(spacing[1.5])-1px)] sm:text-sm/6", // Focus - 'focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500', + "focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500", // Disabled - 'data-[disabled]:opacity-50', + "data-[disabled]:opacity-50", // Icon - '[&>[data-slot=icon]]:-mx-0.5 [&>[data-slot=icon]]:my-0.5 [&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:shrink-0 [&>[data-slot=icon]]:text-[--btn-icon] [&>[data-slot=icon]]:sm:my-1 [&>[data-slot=icon]]:sm:size-4 forced-colors:[--btn-icon:ButtonText] forced-colors:data-[hover]:[--btn-icon:ButtonText]', + "[&>[data-slot=icon]]:-mx-0.5 [&>[data-slot=icon]]:my-0.5 [&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:shrink-0 [&>[data-slot=icon]]:text-[--btn-icon] [&>[data-slot=icon]]:sm:my-1 [&>[data-slot=icon]]:sm:size-4 forced-colors:[--btn-icon:ButtonText] forced-colors:data-[hover]:[--btn-icon:ButtonText]", ], solid: [ // Optical border, implemented as the button background to avoid corner artifacts - 'border-transparent bg-[--btn-border]', + "border-transparent bg-[--btn-border]", // Dark mode: border is rendered on `after` so background is set to button background - 'dark:bg-[--btn-bg]', + "dark:bg-[--btn-bg]", // Button background, implemented as foreground layer to stack on top of pseudo-border layer - 'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-[--btn-bg]', + "before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-[--btn-bg]", // Drop shadow, applied to the inset `before` layer so it blends with the border - 'before:shadow', + "before:shadow", // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo - 'dark:before:hidden', + "dark:before:hidden", // Dark mode: Subtle white outline is applied using a border - 'dark:border-white/5', + "dark:border-white/5", // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow - 'after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]', + "after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]", // Inner highlight shadow - 'after:shadow-[shadow:inset_0_1px_theme(colors.white/15%)]', + "after:shadow-[shadow:inset_0_1px_theme(colors.white/15%)]", // White overlay on hover - 'after:data-[active]:bg-[--btn-hover-overlay] after:data-[hover]:bg-[--btn-hover-overlay]', + "after:data-[active]:bg-[--btn-hover-overlay] after:data-[hover]:bg-[--btn-hover-overlay]", // Dark mode: `after` layer expands to cover entire button - 'dark:after:-inset-px dark:after:rounded-lg', + "dark:after:-inset-px dark:after:rounded-lg", // Disabled - 'before:data-[disabled]:shadow-none after:data-[disabled]:shadow-none', + "before:data-[disabled]:shadow-none after:data-[disabled]:shadow-none", ], outline: [ // Base - 'border-zinc-950/10 text-zinc-950 data-[active]:bg-zinc-950/[2.5%] data-[hover]:bg-zinc-950/[2.5%]', + "border-gray-950/10 text-gray-950 data-[active]:bg-gray-950/[2.5%] data-[hover]:bg-gray-950/[2.5%]", // Dark mode - 'dark:border-white/15 dark:text-white dark:[--btn-bg:transparent] dark:data-[active]:bg-white/5 dark:data-[hover]:bg-white/5', + "dark:border-white/15 dark:text-white dark:[--btn-bg:transparent] dark:data-[active]:bg-white/5 dark:data-[hover]:bg-white/5", // Icon - '[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]', + "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]", ], plain: [ // Base - 'border-transparent text-zinc-950 data-[active]:bg-zinc-950/5 data-[hover]:bg-zinc-950/5', + "border-transparent text-gray-950 data-[active]:bg-gray-950/5 data-[hover]:bg-gray-950/5", // Dark mode - 'dark:text-white dark:data-[active]:bg-white/10 dark:data-[hover]:bg-white/10', + "dark:text-white dark:data-[active]:bg-white/10 dark:data-[hover]:bg-white/10", // Icon - '[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]', + "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]", ], colors: { - 'dark/zinc': [ - 'text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]', - 'dark:text-white dark:[--btn-bg:theme(colors.zinc.600)] dark:[--btn-hover-overlay:theme(colors.white/5%)]', - '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]', + "dark/zinc": [ + "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]", + "dark:text-white dark:[--btn-bg:theme(colors.zinc.600)] dark:[--btn-hover-overlay:theme(colors.white/5%)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]", ], light: [ - 'text-zinc-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]', - 'dark:text-white dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]', - '[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]', + "text-gray-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]", + "dark:text-white dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]", + "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]", ], - 'dark/white': [ - 'text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]', - 'dark:text-zinc-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]', - '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]', + "dark/white": [ + "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]", + "dark:text-gray-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]", ], dark: [ - 'text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]', - 'dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]', - '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]', + "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]", + "dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]", ], white: [ - 'text-zinc-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]', - 'dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]', - '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.500)] data-[hover]:[--btn-icon:theme(colors.zinc.500)]', + "text-gray-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]", + "dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.500)] data-[hover]:[--btn-icon:theme(colors.zinc.500)]", ], zinc: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.zinc.600)] [--btn-border:theme(colors.zinc.700/90%)]', - 'dark:[--btn-hover-overlay:theme(colors.white/5%)]', - '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.zinc.600)] [--btn-border:theme(colors.zinc.700/90%)]", + "dark:[--btn-hover-overlay:theme(colors.white/5%)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]", ], indigo: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.indigo.500)] [--btn-border:theme(colors.indigo.600/90%)]', - '[--btn-icon:theme(colors.indigo.300)] data-[active]:[--btn-icon:theme(colors.indigo.200)] data-[hover]:[--btn-icon:theme(colors.indigo.200)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.indigo.500)] [--btn-border:theme(colors.indigo.600/90%)]", + "[--btn-icon:theme(colors.indigo.300)] data-[active]:[--btn-icon:theme(colors.indigo.200)] data-[hover]:[--btn-icon:theme(colors.indigo.200)]", ], cyan: [ - 'text-cyan-950 [--btn-bg:theme(colors.cyan.300)] [--btn-border:theme(colors.cyan.400/80%)] [--btn-hover-overlay:theme(colors.white/25%)]', - '[--btn-icon:theme(colors.cyan.500)]', + "text-cyan-950 [--btn-bg:theme(colors.cyan.300)] [--btn-border:theme(colors.cyan.400/80%)] [--btn-hover-overlay:theme(colors.white/25%)]", + "[--btn-icon:theme(colors.cyan.500)]", ], red: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.red.600)] [--btn-border:theme(colors.red.700/90%)]', - '[--btn-icon:theme(colors.red.300)] data-[active]:[--btn-icon:theme(colors.red.200)] data-[hover]:[--btn-icon:theme(colors.red.200)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.red.600)] [--btn-border:theme(colors.red.700/90%)]", + "[--btn-icon:theme(colors.red.300)] data-[active]:[--btn-icon:theme(colors.red.200)] data-[hover]:[--btn-icon:theme(colors.red.200)]", ], orange: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.orange.500)] [--btn-border:theme(colors.orange.600/90%)]', - '[--btn-icon:theme(colors.orange.300)] data-[active]:[--btn-icon:theme(colors.orange.200)] data-[hover]:[--btn-icon:theme(colors.orange.200)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.orange.500)] [--btn-border:theme(colors.orange.600/90%)]", + "[--btn-icon:theme(colors.orange.300)] data-[active]:[--btn-icon:theme(colors.orange.200)] data-[hover]:[--btn-icon:theme(colors.orange.200)]", ], amber: [ - 'text-amber-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.amber.400)] [--btn-border:theme(colors.amber.500/80%)]', - '[--btn-icon:theme(colors.amber.600)]', + "text-amber-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.amber.400)] [--btn-border:theme(colors.amber.500/80%)]", + "[--btn-icon:theme(colors.amber.600)]", ], yellow: [ - 'text-yellow-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.yellow.300)] [--btn-border:theme(colors.yellow.400/80%)]', - '[--btn-icon:theme(colors.yellow.600)] data-[active]:[--btn-icon:theme(colors.yellow.700)] data-[hover]:[--btn-icon:theme(colors.yellow.700)]', + "text-yellow-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.yellow.300)] [--btn-border:theme(colors.yellow.400/80%)]", + "[--btn-icon:theme(colors.yellow.600)] data-[active]:[--btn-icon:theme(colors.yellow.700)] data-[hover]:[--btn-icon:theme(colors.yellow.700)]", ], lime: [ - 'text-lime-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.lime.300)] [--btn-border:theme(colors.lime.400/80%)]', - '[--btn-icon:theme(colors.lime.600)] data-[active]:[--btn-icon:theme(colors.lime.700)] data-[hover]:[--btn-icon:theme(colors.lime.700)]', + "text-lime-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.lime.300)] [--btn-border:theme(colors.lime.400/80%)]", + "[--btn-icon:theme(colors.lime.600)] data-[active]:[--btn-icon:theme(colors.lime.700)] data-[hover]:[--btn-icon:theme(colors.lime.700)]", ], green: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.green.600)] [--btn-border:theme(colors.green.700/90%)]', - '[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.green.600)] [--btn-border:theme(colors.green.700/90%)]", + "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]", ], emerald: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.emerald.600)] [--btn-border:theme(colors.emerald.700/90%)]', - '[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.emerald.600)] [--btn-border:theme(colors.emerald.700/90%)]", + "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]", ], teal: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.teal.600)] [--btn-border:theme(colors.teal.700/90%)]', - '[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.teal.600)] [--btn-border:theme(colors.teal.700/90%)]", + "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]", ], sky: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.sky.500)] [--btn-border:theme(colors.sky.600/80%)]', - '[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.sky.500)] [--btn-border:theme(colors.sky.600/80%)]", + "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]", ], blue: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.blue.600)] [--btn-border:theme(colors.blue.700/90%)]', - '[--btn-icon:theme(colors.blue.400)] data-[active]:[--btn-icon:theme(colors.blue.300)] data-[hover]:[--btn-icon:theme(colors.blue.300)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.blue.600)] [--btn-border:theme(colors.blue.700/90%)]", + "[--btn-icon:theme(colors.blue.400)] data-[active]:[--btn-icon:theme(colors.blue.300)] data-[hover]:[--btn-icon:theme(colors.blue.300)]", ], violet: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.violet.500)] [--btn-border:theme(colors.violet.600/90%)]', - '[--btn-icon:theme(colors.violet.300)] data-[active]:[--btn-icon:theme(colors.violet.200)] data-[hover]:[--btn-icon:theme(colors.violet.200)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.violet.500)] [--btn-border:theme(colors.violet.600/90%)]", + "[--btn-icon:theme(colors.violet.300)] data-[active]:[--btn-icon:theme(colors.violet.200)] data-[hover]:[--btn-icon:theme(colors.violet.200)]", ], purple: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.purple.500)] [--btn-border:theme(colors.purple.600/90%)]', - '[--btn-icon:theme(colors.purple.300)] data-[active]:[--btn-icon:theme(colors.purple.200)] data-[hover]:[--btn-icon:theme(colors.purple.200)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.purple.500)] [--btn-border:theme(colors.purple.600/90%)]", + "[--btn-icon:theme(colors.purple.300)] data-[active]:[--btn-icon:theme(colors.purple.200)] data-[hover]:[--btn-icon:theme(colors.purple.200)]", ], fuchsia: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.fuchsia.500)] [--btn-border:theme(colors.fuchsia.600/90%)]', - '[--btn-icon:theme(colors.fuchsia.300)] data-[active]:[--btn-icon:theme(colors.fuchsia.200)] data-[hover]:[--btn-icon:theme(colors.fuchsia.200)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.fuchsia.500)] [--btn-border:theme(colors.fuchsia.600/90%)]", + "[--btn-icon:theme(colors.fuchsia.300)] data-[active]:[--btn-icon:theme(colors.fuchsia.200)] data-[hover]:[--btn-icon:theme(colors.fuchsia.200)]", ], pink: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.pink.500)] [--btn-border:theme(colors.pink.600/90%)]', - '[--btn-icon:theme(colors.pink.300)] data-[active]:[--btn-icon:theme(colors.pink.200)] data-[hover]:[--btn-icon:theme(colors.pink.200)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.pink.500)] [--btn-border:theme(colors.pink.600/90%)]", + "[--btn-icon:theme(colors.pink.300)] data-[active]:[--btn-icon:theme(colors.pink.200)] data-[hover]:[--btn-icon:theme(colors.pink.200)]", ], rose: [ - 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.rose.500)] [--btn-border:theme(colors.rose.600/90%)]', - '[--btn-icon:theme(colors.rose.300)] data-[active]:[--btn-icon:theme(colors.rose.200)] data-[hover]:[--btn-icon:theme(colors.rose.200)]', + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.rose.500)] [--btn-border:theme(colors.rose.600/90%)]", + "[--btn-icon:theme(colors.rose.300)] data-[active]:[--btn-icon:theme(colors.rose.200)] data-[hover]:[--btn-icon:theme(colors.rose.200)]", ], }, -} +}; type ButtonProps = ( | { color?: keyof typeof styles.colors; outline?: never; plain?: never } | { color?: never; outline: true; plain?: never } | { color?: never; outline?: never; plain: true } -) & { children: React.ReactNode } & (HeadlessButtonProps | React.ComponentPropsWithoutRef) +) & { children: React.ReactNode } & ( + | HeadlessButtonProps + | React.ComponentPropsWithoutRef + ); export const Button = React.forwardRef(function Button( { color, outline, plain, className, children, ...props }: ButtonProps, @@ -189,19 +195,31 @@ export const Button = React.forwardRef(function Button( let classes = clsx( className, styles.base, - outline ? styles.outline : plain ? styles.plain : clsx(styles.solid, styles.colors[color ?? 'dark/zinc']) - ) - - return 'href' in props ? ( - }> + outline + ? styles.outline + : plain + ? styles.plain + : clsx(styles.solid, styles.colors[color ?? "dark/zinc"]) + ); + + return "href" in props ? ( + } + > {children} ) : ( - + {children} - ) -}) + ); +}); /* Expand the hit area to at least 44×44px on touch devices */ export function TouchTarget({ children }: { children: React.ReactNode }) { @@ -213,5 +231,5 @@ export function TouchTarget({ children }: { children: React.ReactNode }) { aria-hidden="true" /> - ) + ); } diff --git a/text2sql-frontend/src/components/Catalyst/checkbox.tsx b/text2sql-frontend/src/components/Catalyst/checkbox.tsx index 71122608..720f2f02 100644 --- a/text2sql-frontend/src/components/Catalyst/checkbox.tsx +++ b/text2sql-frontend/src/components/Catalyst/checkbox.tsx @@ -3,11 +3,14 @@ import { Field as HeadlessField, type CheckboxProps as HeadlessCheckboxProps, type FieldProps as HeadlessFieldProps, -} from '@headlessui/react' -import { clsx } from 'clsx' -import type React from 'react' +} from "@headlessui/react"; +import { clsx } from "clsx"; +import type React from "react"; -export function CheckboxGroup({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { +export function CheckboxGroup({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { return (
- ) + ); } export function CheckboxField({ className, ...props }: HeadlessFieldProps) { @@ -34,116 +37,116 @@ export function CheckboxField({ className, ...props }: HeadlessFieldProps) { className, // Base layout - 'grid grid-cols-[1.125rem_1fr] items-center gap-x-4 gap-y-1 sm:grid-cols-[1rem_1fr]', + "grid grid-cols-[1.125rem_1fr] items-center gap-x-4 gap-y-1 sm:grid-cols-[1rem_1fr]", // Control layout - '[&>[data-slot=control]]:col-start-1 [&>[data-slot=control]]:row-start-1 [&>[data-slot=control]]:justify-self-center', + "[&>[data-slot=control]]:col-start-1 [&>[data-slot=control]]:row-start-1 [&>[data-slot=control]]:justify-self-center", // Label layout - '[&>[data-slot=label]]:col-start-2 [&>[data-slot=label]]:row-start-1 [&>[data-slot=label]]:justify-self-start', + "[&>[data-slot=label]]:col-start-2 [&>[data-slot=label]]:row-start-1 [&>[data-slot=label]]:justify-self-start", // Description layout - '[&>[data-slot=description]]:col-start-2 [&>[data-slot=description]]:row-start-2', + "[&>[data-slot=description]]:col-start-2 [&>[data-slot=description]]:row-start-2", // With description - '[&_[data-slot=label]]:has-[[data-slot=description]]:font-medium' + "[&_[data-slot=label]]:has-[[data-slot=description]]:font-medium" )} /> - ) + ); } const base = [ // Basic layout - 'relative isolate flex size-[1.125rem] items-center justify-center rounded-[0.3125rem] sm:size-4', + "relative isolate flex size-[1.125rem] items-center justify-center rounded-[0.3125rem] sm:size-4", // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode - 'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(0.3125rem-1px)] before:bg-white before:shadow', + "before:absolute before:inset-0 before:-z-10 before:rounded-[calc(0.3125rem-1px)] before:bg-white before:shadow", // Background color when checked - 'before:group-data-[checked]:bg-[--checkbox-checked-bg]', + "before:group-data-[checked]:bg-[--checkbox-checked-bg]", // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo - 'dark:before:hidden', + "dark:before:hidden", // Background color applied to control in dark mode - 'dark:bg-white/5 dark:group-data-[checked]:bg-[--checkbox-checked-bg]', + "dark:bg-white/5 dark:group-data-[checked]:bg-[--checkbox-checked-bg]", // Border - 'border border-zinc-950/15 group-data-[checked]:border-transparent group-data-[checked]:group-data-[hover]:border-transparent group-data-[hover]:border-zinc-950/30 group-data-[checked]:bg-[--checkbox-checked-border]', - 'dark:border-white/15 dark:group-data-[checked]:border-white/5 dark:group-data-[checked]:group-data-[hover]:border-white/5 dark:group-data-[hover]:border-white/30', + "border border-gray-950/15 group-data-[checked]:border-transparent group-data-[checked]:group-data-[hover]:border-transparent group-data-[hover]:border-gray-950/30 group-data-[checked]:bg-[--checkbox-checked-border]", + "dark:border-white/15 dark:group-data-[checked]:border-white/5 dark:group-data-[checked]:group-data-[hover]:border-white/5 dark:group-data-[hover]:border-white/30", // Inner highlight shadow - 'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_theme(colors.white/15%)]', - 'dark:after:-inset-px dark:after:hidden dark:after:rounded-[0.3125rem] dark:group-data-[checked]:after:block', + "after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_theme(colors.white/15%)]", + "dark:after:-inset-px dark:after:hidden dark:after:rounded-[0.3125rem] dark:group-data-[checked]:after:block", // Focus ring - 'group-data-[focus]:outline group-data-[focus]:outline-2 group-data-[focus]:outline-offset-2 group-data-[focus]:outline-blue-500', + "group-data-[focus]:outline group-data-[focus]:outline-2 group-data-[focus]:outline-offset-2 group-data-[focus]:outline-blue-500", // Disabled state - 'group-data-[disabled]:opacity-50', - 'group-data-[disabled]:border-zinc-950/25 group-data-[disabled]:bg-zinc-950/5 group-data-[disabled]:[--checkbox-check:theme(colors.zinc.950/50%)] group-data-[disabled]:before:bg-transparent', - 'dark:group-data-[disabled]:border-white/20 dark:group-data-[disabled]:bg-white/[2.5%] dark:group-data-[disabled]:[--checkbox-check:theme(colors.white/50%)] dark:group-data-[disabled]:group-data-[checked]:after:hidden', + "group-data-[disabled]:opacity-50", + "group-data-[disabled]:border-gray-950/25 group-data-[disabled]:bg-gray-950/5 group-data-[disabled]:[--checkbox-check:theme(colors.zinc.950/50%)] group-data-[disabled]:before:bg-transparent", + "dark:group-data-[disabled]:border-white/20 dark:group-data-[disabled]:bg-white/[2.5%] dark:group-data-[disabled]:[--checkbox-check:theme(colors.white/50%)] dark:group-data-[disabled]:group-data-[checked]:after:hidden", // Forced colors mode - 'forced-colors:[--checkbox-check:HighlightText] forced-colors:[--checkbox-checked-bg:Highlight] forced-colors:group-data-[disabled]:[--checkbox-check:Highlight]', - 'dark:forced-colors:[--checkbox-check:HighlightText] dark:forced-colors:[--checkbox-checked-bg:Highlight] dark:forced-colors:group-data-[disabled]:[--checkbox-check:Highlight]', -] + "forced-colors:[--checkbox-check:HighlightText] forced-colors:[--checkbox-checked-bg:Highlight] forced-colors:group-data-[disabled]:[--checkbox-check:Highlight]", + "dark:forced-colors:[--checkbox-check:HighlightText] dark:forced-colors:[--checkbox-checked-bg:Highlight] dark:forced-colors:group-data-[disabled]:[--checkbox-check:Highlight]", +]; const colors = { - 'dark/zinc': [ - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]', - 'dark:[--checkbox-checked-bg:theme(colors.zinc.600)]', + "dark/zinc": [ + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]", + "dark:[--checkbox-checked-bg:theme(colors.zinc.600)]", ], - 'dark/white': [ - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]', - 'dark:[--checkbox-check:theme(colors.zinc.900)] dark:[--checkbox-checked-bg:theme(colors.white)] dark:[--checkbox-checked-border:theme(colors.zinc.950/15%)]', + "dark/white": [ + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]", + "dark:[--checkbox-check:theme(colors.zinc.900)] dark:[--checkbox-checked-bg:theme(colors.white)] dark:[--checkbox-checked-border:theme(colors.zinc.950/15%)]", ], white: - '[--checkbox-check:theme(colors.zinc.900)] [--checkbox-checked-bg:theme(colors.white)] [--checkbox-checked-border:theme(colors.zinc.950/15%)]', - dark: '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]', - zinc: '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.600)] [--checkbox-checked-border:theme(colors.zinc.700/90%)]', - red: '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.red.600)] [--checkbox-checked-border:theme(colors.red.700/90%)]', + "[--checkbox-check:theme(colors.zinc.900)] [--checkbox-checked-bg:theme(colors.white)] [--checkbox-checked-border:theme(colors.zinc.950/15%)]", + dark: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]", + zinc: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.600)] [--checkbox-checked-border:theme(colors.zinc.700/90%)]", + red: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.red.600)] [--checkbox-checked-border:theme(colors.red.700/90%)]", orange: - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.orange.500)] [--checkbox-checked-border:theme(colors.orange.600/90%)]', + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.orange.500)] [--checkbox-checked-border:theme(colors.orange.600/90%)]", amber: - '[--checkbox-check:theme(colors.amber.950)] [--checkbox-checked-bg:theme(colors.amber.400)] [--checkbox-checked-border:theme(colors.amber.500/80%)]', + "[--checkbox-check:theme(colors.amber.950)] [--checkbox-checked-bg:theme(colors.amber.400)] [--checkbox-checked-border:theme(colors.amber.500/80%)]", yellow: - '[--checkbox-check:theme(colors.yellow.950)] [--checkbox-checked-bg:theme(colors.yellow.300)] [--checkbox-checked-border:theme(colors.yellow.400/80%)]', - lime: '[--checkbox-check:theme(colors.lime.950)] [--checkbox-checked-bg:theme(colors.lime.300)] [--checkbox-checked-border:theme(colors.lime.400/80%)]', + "[--checkbox-check:theme(colors.yellow.950)] [--checkbox-checked-bg:theme(colors.yellow.300)] [--checkbox-checked-border:theme(colors.yellow.400/80%)]", + lime: "[--checkbox-check:theme(colors.lime.950)] [--checkbox-checked-bg:theme(colors.lime.300)] [--checkbox-checked-border:theme(colors.lime.400/80%)]", green: - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.green.600)] [--checkbox-checked-border:theme(colors.green.700/90%)]', + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.green.600)] [--checkbox-checked-border:theme(colors.green.700/90%)]", emerald: - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.emerald.600)] [--checkbox-checked-border:theme(colors.emerald.700/90%)]', - teal: '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.teal.600)] [--checkbox-checked-border:theme(colors.teal.700/90%)]', - cyan: '[--checkbox-check:theme(colors.cyan.950)] [--checkbox-checked-bg:theme(colors.cyan.300)] [--checkbox-checked-border:theme(colors.cyan.400/80%)]', - sky: '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.sky.500)] [--checkbox-checked-border:theme(colors.sky.600/80%)]', - blue: '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.blue.600)] [--checkbox-checked-border:theme(colors.blue.700/90%)]', + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.emerald.600)] [--checkbox-checked-border:theme(colors.emerald.700/90%)]", + teal: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.teal.600)] [--checkbox-checked-border:theme(colors.teal.700/90%)]", + cyan: "[--checkbox-check:theme(colors.cyan.950)] [--checkbox-checked-bg:theme(colors.cyan.300)] [--checkbox-checked-border:theme(colors.cyan.400/80%)]", + sky: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.sky.500)] [--checkbox-checked-border:theme(colors.sky.600/80%)]", + blue: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.blue.600)] [--checkbox-checked-border:theme(colors.blue.700/90%)]", indigo: - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.indigo.500)] [--checkbox-checked-border:theme(colors.indigo.600/90%)]', + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.indigo.500)] [--checkbox-checked-border:theme(colors.indigo.600/90%)]", violet: - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.violet.500)] [--checkbox-checked-border:theme(colors.violet.600/90%)]', + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.violet.500)] [--checkbox-checked-border:theme(colors.violet.600/90%)]", purple: - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.purple.500)] [--checkbox-checked-border:theme(colors.purple.600/90%)]', + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.purple.500)] [--checkbox-checked-border:theme(colors.purple.600/90%)]", fuchsia: - '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.fuchsia.500)] [--checkbox-checked-border:theme(colors.fuchsia.600/90%)]', - pink: '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.pink.500)] [--checkbox-checked-border:theme(colors.pink.600/90%)]', - rose: '[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.rose.500)] [--checkbox-checked-border:theme(colors.rose.600/90%)]', -} + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.fuchsia.500)] [--checkbox-checked-border:theme(colors.fuchsia.600/90%)]", + pink: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.pink.500)] [--checkbox-checked-border:theme(colors.pink.600/90%)]", + rose: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.rose.500)] [--checkbox-checked-border:theme(colors.rose.600/90%)]", +}; -type Color = keyof typeof colors +type Color = keyof typeof colors; export function Checkbox({ - color = 'dark/zinc', + color = "dark/zinc", className, ...props }: { - color?: Color - className?: string + color?: Color; + className?: string; } & HeadlessCheckboxProps) { return ( @@ -171,5 +174,5 @@ export function Checkbox({ - ) + ); } diff --git a/text2sql-frontend/src/components/Catalyst/dialog.tsx b/text2sql-frontend/src/components/Catalyst/dialog.tsx index 2cb652d0..fec4c8dd 100644 --- a/text2sql-frontend/src/components/Catalyst/dialog.tsx +++ b/text2sql-frontend/src/components/Catalyst/dialog.tsx @@ -6,32 +6,35 @@ import { Transition as HeadlessTransition, TransitionChild as HeadlessTransitionChild, type DialogProps as HeadlessDialogProps, -} from '@headlessui/react' -import clsx from 'clsx' -import type React from 'react' -import { Fragment } from 'react' -import { Text } from './text' +} from "@headlessui/react"; +import clsx from "clsx"; +import type React from "react"; +import { Fragment } from "react"; +import { Text } from "./text"; const sizes = { - xs: 'sm:max-w-xs', - sm: 'sm:max-w-sm', - md: 'sm:max-w-md', - lg: 'sm:max-w-lg', - xl: 'sm:max-w-xl', - '2xl': 'sm:max-w-2xl', - '3xl': 'sm:max-w-3xl', - '4xl': 'sm:max-w-4xl', - '5xl': 'sm:max-w-5xl', -} + xs: "sm:max-w-xs", + sm: "sm:max-w-sm", + md: "sm:max-w-md", + lg: "sm:max-w-lg", + xl: "sm:max-w-xl", + "2xl": "sm:max-w-2xl", + "3xl": "sm:max-w-3xl", + "4xl": "sm:max-w-4xl", + "5xl": "sm:max-w-5xl", +}; export function Dialog({ open, onClose, - size = 'lg', + size = "lg", className, children, ...props -}: { size?: keyof typeof sizes; children: React.ReactNode } & HeadlessDialogProps) { +}: { + size?: keyof typeof sizes; + children: React.ReactNode; +} & HeadlessDialogProps) { return ( @@ -44,7 +47,7 @@ export function Dialog({ leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
- ) + ); } -export function DialogTitle({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { +export function DialogTitle({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { return ( - ) + ); } -export function DialogDescription({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { - return +export function DialogDescription({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return ( + + ); } -export function DialogBody({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { - return
+export function DialogBody({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return
; } -export function DialogActions({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { +export function DialogActions({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { return (
- ) + ); } diff --git a/text2sql-frontend/src/components/Catalyst/dropdown.tsx b/text2sql-frontend/src/components/Catalyst/dropdown.tsx index f6d74877..c279e076 100644 --- a/text2sql-frontend/src/components/Catalyst/dropdown.tsx +++ b/text2sql-frontend/src/components/Catalyst/dropdown.tsx @@ -1,4 +1,4 @@ -'use client' +"use client"; import { Description as HeadlessDescription, @@ -19,185 +19,222 @@ import { type MenuProps as HeadlessMenuProps, type MenuSectionProps as HeadlessMenuSectionProps, type MenuSeparatorProps as HeadlessMenuSeparatorProps, -} from '@headlessui/react' -import clsx from 'clsx' -import type React from 'react' -import { Fragment } from 'react' -import { Button } from './button' -import { Link } from './link' +} from "@headlessui/react"; +import clsx from "clsx"; +import type React from "react"; +import { Fragment } from "react"; +import { Button } from "./button"; +import { Link } from "./link"; export function Dropdown(props: HeadlessMenuProps) { - return + return ; } export function DropdownButton( props: React.ComponentProps> ) { - return + return ; } export function DropdownMenu({ - anchor = 'bottom', + anchor = "bottom", ...props -}: { anchor?: NonNullable['to'] } & Omit) { +}: { anchor?: NonNullable["to"] } & Omit< + HeadlessMenuItemsProps, + "anchor" +>) { return ( - + - ) + ); } -export function DropdownItem(props: { href?: string } & HeadlessMenuItemProps<'button'>) { +export function DropdownItem( + props: { href?: string } & HeadlessMenuItemProps<"button"> +) { return ( [data-slot=icon]]:data-[focus]:text-[HighlightText]', + "forced-color-adjust-none forced-colors:data-[focus]:bg-[Highlight] forced-colors:data-[focus]:text-[HighlightText] forced-colors:[&>[data-slot=icon]]:data-[focus]:text-[HighlightText]", // Use subgrid when available but fallback to an explicit grid layout if not - 'col-span-full grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] items-center supports-[grid-template-columns:subgrid]:grid-cols-subgrid', + "col-span-full grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] items-center supports-[grid-template-columns:subgrid]:grid-cols-subgrid", // Icon - '[&>[data-slot=icon]]:col-start-1 [&>[data-slot=icon]]:row-start-1 [&>[data-slot=icon]]:mr-2.5 [&>[data-slot=icon]]:size-5 sm:[&>[data-slot=icon]]:mr-2 [&>[data-slot=icon]]:sm:size-4', - '[&>[data-slot=icon]]:text-zinc-500 [&>[data-slot=icon]]:data-[focus]:text-white [&>[data-slot=icon]]:dark:text-zinc-500 [&>[data-slot=icon]]:data-[focus]:dark:text-white' + "[&>[data-slot=icon]]:col-start-1 [&>[data-slot=icon]]:row-start-1 [&>[data-slot=icon]]:mr-2.5 [&>[data-slot=icon]]:size-5 sm:[&>[data-slot=icon]]:mr-2 [&>[data-slot=icon]]:sm:size-4", + "[&>[data-slot=icon]]:text-gray-500 [&>[data-slot=icon]]:data-[focus]:text-white [&>[data-slot=icon]]:dark:text-gray-500 [&>[data-slot=icon]]:data-[focus]:dark:text-white" )} /> - ) + ); } -export function DropdownHeader({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { - return
+export function DropdownHeader({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return ( +
+ ); } -export function DropdownSection({ className, ...props }: HeadlessMenuSectionProps) { +export function DropdownSection({ + className, + ...props +}: HeadlessMenuSectionProps) { return ( - ) + ); } -export function DropdownHeading({ className, ...props }: HeadlessMenuHeadingProps) { +export function DropdownHeading({ + className, + ...props +}: HeadlessMenuHeadingProps) { return ( - ) + ); } -export function DropdownSeparator({ className, ...props }: HeadlessMenuSeparatorProps) { +export function DropdownSeparator({ + className, + ...props +}: HeadlessMenuSeparatorProps) { return ( - ) + ); } export function DropdownLabel({ className, ...props }: HeadlessLabelProps) { return ( - - ) + + ); } -export function DropdownDescription({ className, ...props }: HeadlessDescriptionProps) { +export function DropdownDescription({ + className, + ...props +}: HeadlessDescriptionProps) { return ( - ) + ); } export function DropdownShortcut({ className, keys, ...props -}: { keys: string | string[] } & HeadlessDescriptionProps<'kbd'>) { +}: { keys: string | string[] } & HeadlessDescriptionProps<"kbd">) { return ( - {(Array.isArray(keys) ? keys : keys.split('')).map((char, index) => ( + {(Array.isArray(keys) ? keys : keys.split("")).map((char, index) => ( 0 && char.length > 1 && 'pl-1', + index > 0 && char.length > 1 && "pl-1", ])} > {char} ))} - ) + ); } diff --git a/text2sql-frontend/src/components/Catalyst/fieldset.tsx b/text2sql-frontend/src/components/Catalyst/fieldset.tsx index 4cc07b50..dbcde517 100644 --- a/text2sql-frontend/src/components/Catalyst/fieldset.tsx +++ b/text2sql-frontend/src/components/Catalyst/fieldset.tsx @@ -9,17 +9,23 @@ import { type FieldsetProps as HeadlessFieldsetProps, type LabelProps as HeadlessLabelProps, type LegendProps as HeadlessLegendProps, -} from '@headlessui/react' -import clsx from 'clsx' -import type React from 'react' +} from "@headlessui/react"; +import clsx from "clsx"; +import type React from "react"; -export function Fieldset({ className, ...props }: { disabled?: boolean } & HeadlessFieldsetProps) { +export function Fieldset({ + className, + ...props +}: { disabled?: boolean } & HeadlessFieldsetProps) { return ( *+[data-slot=control]]:mt-6 [&>[data-slot=text]]:mt-1')} + className={clsx( + className, + "[&>*+[data-slot=control]]:mt-6 [&>[data-slot=text]]:mt-1" + )} /> - ) + ); } export function Legend({ ...props }: HeadlessLegendProps) { @@ -29,14 +35,23 @@ export function Legend({ ...props }: HeadlessLegendProps) { data-slot="legend" className={clsx( props.className, - 'text-base/6 font-semibold text-zinc-950 data-[disabled]:opacity-50 sm:text-sm/6 dark:text-white' + "text-base/6 font-semibold text-gray-950 data-[disabled]:opacity-50 sm:text-sm/6 dark:text-white" )} /> - ) + ); } -export function FieldGroup({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { - return
+export function FieldGroup({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return ( +
+ ); } export function Field({ className, ...props }: HeadlessFieldProps) { @@ -44,29 +59,32 @@ export function Field({ className, ...props }: HeadlessFieldProps) { [data-slot=label]+[data-slot=control]]:mt-3', - '[&>[data-slot=label]+[data-slot=description]]:mt-1', - '[&>[data-slot=description]+[data-slot=control]]:mt-3', - '[&>[data-slot=control]+[data-slot=description]]:mt-3', - '[&>[data-slot=control]+[data-slot=error]]:mt-3', - '[&>[data-slot=label]]:font-medium' + "[&>[data-slot=label]+[data-slot=control]]:mt-3", + "[&>[data-slot=label]+[data-slot=description]]:mt-1", + "[&>[data-slot=description]+[data-slot=control]]:mt-3", + "[&>[data-slot=control]+[data-slot=description]]:mt-3", + "[&>[data-slot=control]+[data-slot=error]]:mt-3", + "[&>[data-slot=label]]:font-medium" )} {...props} /> - ) + ); } -export function Label({ className, ...props }: { className?: string } & HeadlessLabelProps) { +export function Label({ + className, + ...props +}: { className?: string } & HeadlessLabelProps) { return ( - ) + ); } export function Description({ @@ -80,10 +98,10 @@ export function Description({ data-slot="description" className={clsx( className, - 'text-base/6 text-zinc-500 data-[disabled]:opacity-50 sm:text-sm/6 dark:text-zinc-400' + "text-base/6 text-gray-500 data-[disabled]:opacity-50 sm:text-sm/6 dark:text-gray-400" )} /> - ) + ); } export function ErrorMessage({ @@ -95,7 +113,10 @@ export function ErrorMessage({ - ) + ); } diff --git a/text2sql-frontend/src/components/Catalyst/input.tsx b/text2sql-frontend/src/components/Catalyst/input.tsx index 2568dbd2..a86bf758 100644 --- a/text2sql-frontend/src/components/Catalyst/input.tsx +++ b/text2sql-frontend/src/components/Catalyst/input.tsx @@ -1,13 +1,26 @@ -import { Input as HeadlessInput, type InputProps as HeadlessInputProps } from '@headlessui/react' -import { clsx } from 'clsx' -import { forwardRef } from 'react' +import { + Input as HeadlessInput, + type InputProps as HeadlessInputProps, +} from "@headlessui/react"; +import { clsx } from "clsx"; +import { forwardRef } from "react"; -const dateTypes = ['date', 'datetime-local', 'month', 'time', 'week'] -type DateType = (typeof dateTypes)[number] +const dateTypes = ["date", "datetime-local", "month", "time", "week"]; +type DateType = (typeof dateTypes)[number]; export const Input = forwardRef< HTMLInputElement, - { type?: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | DateType } & HeadlessInputProps + { + type?: + | "email" + | "number" + | "password" + | "search" + | "tel" + | "text" + | "url" + | DateType; + } & HeadlessInputProps >(function Input({ className, ...props }, ref) { return ( - ) -}) + ); +}); diff --git a/text2sql-frontend/src/components/Catalyst/listbox.tsx b/text2sql-frontend/src/components/Catalyst/listbox.tsx index 6bc6006f..1f3b4bef 100644 --- a/text2sql-frontend/src/components/Catalyst/listbox.tsx +++ b/text2sql-frontend/src/components/Catalyst/listbox.tsx @@ -1,4 +1,4 @@ -'use client' +"use client"; import { Listbox as HeadlessListbox, @@ -9,24 +9,24 @@ import { Transition as HeadlessTransition, type ListboxOptionProps as HeadlessListboxOptionProps, type ListboxProps as HeadlessListboxProps, -} from '@headlessui/react' -import clsx from 'clsx' -import { Fragment } from 'react' +} from "@headlessui/react"; +import clsx from "clsx"; +import { Fragment } from "react"; export function Listbox({ className, placeholder, autoFocus, - 'aria-label': ariaLabel, + "aria-label": ariaLabel, children: options, ...props }: { - className?: string - placeholder?: React.ReactNode - autoFocus?: boolean - 'aria-label'?: string - children?: React.ReactNode -} & Omit, 'multiple'>) { + className?: string; + placeholder?: React.ReactNode; + autoFocus?: boolean; + "aria-label"?: string; + children?: React.ReactNode; +} & Omit, "multiple">) { return ( ({ className, // Basic layout - 'group relative block w-full', + "group relative block w-full", // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode - 'before:absolute before:inset-px before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-white before:shadow', + "before:absolute before:inset-px before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-white before:shadow", // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo - 'dark:before:hidden', + "dark:before:hidden", // Hide default focus styles - 'focus:outline-none', + "focus:outline-none", // Focus ring - 'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-inset after:ring-transparent sm:after:data-[focus]:ring-2 sm:after:data-[focus]:ring-blue-500', + "after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-inset after:ring-transparent sm:after:data-[focus]:ring-2 sm:after:data-[focus]:ring-blue-500", // Disabled state - 'data-[disabled]:opacity-50 before:data-[disabled]:bg-zinc-950/5 before:data-[disabled]:shadow-none', + "data-[disabled]:opacity-50 before:data-[disabled]:bg-gray-950/5 before:data-[disabled]:shadow-none", ])} > {placeholder}} + placeholder={ + placeholder && ( + + {placeholder} + + ) + } className={clsx([ // Basic layout - 'relative block w-full appearance-none rounded-lg py-[calc(theme(spacing[2.5])-1px)] sm:py-[calc(theme(spacing[1.5])-1px)]', + "relative block w-full appearance-none rounded-lg py-[calc(theme(spacing[2.5])-1px)] sm:py-[calc(theme(spacing[1.5])-1px)]", // Set minimum height for when no value is selected - 'min-h-11 sm:min-h-9', + "min-h-11 sm:min-h-9", // Horizontal padding - 'pl-[calc(theme(spacing[3.5])-1px)] pr-[calc(theme(spacing.7)-1px)] sm:pl-[calc(theme(spacing.3)-1px)]', + "pl-[calc(theme(spacing[3.5])-1px)] pr-[calc(theme(spacing.7)-1px)] sm:pl-[calc(theme(spacing.3)-1px)]", // Typography - 'text-left text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]', + "text-left text-base/6 text-gray-950 placeholder:text-gray-500 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]", // Border - 'border border-zinc-950/10 group-data-[active]:border-zinc-950/20 group-data-[hover]:border-zinc-950/20 dark:border-white/10 dark:group-data-[active]:border-white/20 dark:group-data-[hover]:border-white/20', + "border border-gray-950/10 group-data-[active]:border-gray-950/20 group-data-[hover]:border-gray-950/20 dark:border-white/10 dark:group-data-[active]:border-white/20 dark:group-data-[hover]:border-white/20", // Background color - 'bg-transparent dark:bg-white/5', + "bg-transparent dark:bg-white/5", // Invalid state - 'group-data-[invalid]:border-red-500 group-data-[invalid]:group-data-[hover]:border-red-500 group-data-[invalid]:dark:border-red-600 group-data-[invalid]:data-[hover]:dark:border-red-600', + "group-data-[invalid]:border-red-500 group-data-[invalid]:group-data-[hover]:border-red-500 group-data-[invalid]:dark:border-red-600 group-data-[invalid]:data-[hover]:dark:border-red-600", // Disabled state - 'group-data-[disabled]:border-zinc-950/20 group-data-[disabled]:opacity-100 group-data-[disabled]:dark:border-white/15 group-data-[disabled]:dark:bg-white/[2.5%] dark:data-[hover]:group-data-[disabled]:border-white/15', + "group-data-[disabled]:border-gray-950/20 group-data-[disabled]:opacity-100 group-data-[disabled]:dark:border-white/15 group-data-[disabled]:dark:bg-white/[2.5%] dark:data-[hover]:group-data-[disabled]:border-white/15", ])} /> @@ -106,77 +122,79 @@ export function Listbox({ {options} - ) + ); } export function ListboxOption({ children, className, ...props -}: { children?: React.ReactNode } & HeadlessListboxOptionProps<'div', T>) { +}: { children?: React.ReactNode } & HeadlessListboxOptionProps<"div", T>) { const sharedClasses = clsx( // Base - 'flex min-w-0 items-center', + "flex min-w-0 items-center", // Icons - '[&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:shrink-0 [&>[data-slot=icon]]:text-zinc-500 [&>[data-slot=icon]]:group-data-[focus]/option:text-white sm:[&>[data-slot=icon]]:size-4 forced-colors:[&>[data-slot=icon]]:text-[CanvasText] forced-colors:[&>[data-slot=icon]]:group-data-[focus]/option:text-[Canvas]', + "[&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:shrink-0 [&>[data-slot=icon]]:text-gray-500 [&>[data-slot=icon]]:group-data-[focus]/option:text-white sm:[&>[data-slot=icon]]:size-4 forced-colors:[&>[data-slot=icon]]:text-[CanvasText] forced-colors:[&>[data-slot=icon]]:group-data-[focus]/option:text-[Canvas]", // Avatars - '[&>[data-slot=avatar]]:size-6 sm:[&>[data-slot=avatar]]:size-5' - ) + "[&>[data-slot=avatar]]:size-6 sm:[&>[data-slot=avatar]]:size-5" + ); return ( {({ selectedOption }) => { if (selectedOption) { - return
{children}
+ return ( +
{children}
+ ); } return (
({ fill="none" aria-hidden="true" > - + - {children} + + {children} +
- ) + ); }}
- ) + ); } -export function ListboxLabel({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) { - return +export function ListboxLabel({ + className, + ...props +}: React.ComponentPropsWithoutRef<"span">) { + return ( + + ); } -export function ListboxDescription({ className, children, ...props }: React.ComponentPropsWithoutRef<'span'>) { +export function ListboxDescription({ + className, + children, + ...props +}: React.ComponentPropsWithoutRef<"span">) { return ( {children} - ) + ); } diff --git a/text2sql-frontend/src/components/Catalyst/pagination.tsx b/text2sql-frontend/src/components/Catalyst/pagination.tsx index 6a1b649d..e9440d19 100644 --- a/text2sql-frontend/src/components/Catalyst/pagination.tsx +++ b/text2sql-frontend/src/components/Catalyst/pagination.tsx @@ -1,26 +1,42 @@ -import clsx from 'clsx' -import type React from 'react' -import { Button } from './button' +import clsx from "clsx"; +import type React from "react"; +import { Button } from "./button"; export function Pagination({ - 'aria-label': ariaLabel = 'Page navigation', + "aria-label": ariaLabel = "Page navigation", className, ...props -}: React.ComponentPropsWithoutRef<'nav'>) { - return