diff --git a/README.md b/README.md
index b4d19de7..5d84258a 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,11 @@
[](https://worxpace-playground.vercel.app)
+### Important
+
+**This project is no longer maintaining.**
+I'm currently working on the Notion design system, see my new repo [`Notion Kit`](https://github.com/steeeee0223/notion-kit).
+
### Apps
- [Steeeee WorXpace](https://worxpace.steeeee0223.vercel.app/)
diff --git a/packages/database-ui/eslint.config.js b/packages/database-ui/eslint.config.js
new file mode 100644
index 00000000..972aac39
--- /dev/null
+++ b/packages/database-ui/eslint.config.js
@@ -0,0 +1,11 @@
+import baseConfig from "@swy/eslint-config/base";
+import reactConfig from "@swy/eslint-config/react";
+
+/** @type {import('typescript-eslint').Config} */
+export default [
+ {
+ ignores: ["dist/**"],
+ },
+ ...baseConfig,
+ ...reactConfig,
+];
diff --git a/packages/database-ui/index.html b/packages/database-ui/index.html
new file mode 100644
index 00000000..e4b78eae
--- /dev/null
+++ b/packages/database-ui/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
diff --git a/packages/database-ui/package.json b/packages/database-ui/package.json
new file mode 100644
index 00000000..fabcba7c
--- /dev/null
+++ b/packages/database-ui/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@swy/database-ui",
+ "version": "1.4.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "tsc -b && vite build",
+ "clean": "git clean -xdf .turbo node_modules",
+ "dev": "vite --port 5174",
+ "format": "prettier --check . --ignore-path ../../.gitignore",
+ "lint": "eslint",
+ "preview": "vite preview",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@swy/ui": "workspace:*",
+ "@tanstack/react-table": "catalog:ui",
+ "lucide-react": "catalog:ui",
+ "react": "catalog:react18",
+ "react-dom": "catalog:react18",
+ "usehooks-ts": "^2.9.1",
+ "uuid": "catalog:uuid",
+ "zod": "catalog:"
+ },
+ "devDependencies": {
+ "@swy/eslint-config": "workspace:*",
+ "@swy/prettier-config": "workspace:*",
+ "@swy/tailwind-config": "workspace:*",
+ "@swy/tsconfig": "workspace:*",
+ "@types/node": "catalog:node22",
+ "@types/react": "catalog:react18",
+ "@types/react-dom": "catalog:react18",
+ "@types/uuid": "catalog:uuid",
+ "eslint": "catalog:",
+ "globals": "^15.12.0",
+ "prettier": "catalog:",
+ "sonner": "catalog:ui",
+ "tailwindcss": "catalog:",
+ "typescript": "catalog:",
+ "vite": "^6.0.1"
+ },
+ "prettier": "@swy/prettier-config"
+}
diff --git a/packages/database-ui/postcss.config.cjs b/packages/database-ui/postcss.config.cjs
new file mode 100644
index 00000000..ee5f90b3
--- /dev/null
+++ b/packages/database-ui/postcss.config.cjs
@@ -0,0 +1,5 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ },
+};
diff --git a/packages/database-ui/public/vite.svg b/packages/database-ui/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/packages/database-ui/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/database-ui/src/App.tsx b/packages/database-ui/src/App.tsx
new file mode 100644
index 00000000..3b49abef
--- /dev/null
+++ b/packages/database-ui/src/App.tsx
@@ -0,0 +1,27 @@
+import "./notion.css";
+
+import { Database } from "./database";
+import { ThemeProvider, ThemeToggle } from "./theme";
+
+function App() {
+ return (
+
+
+
+ {/* Notion Page Content */}
+
+ {/* Wrapper for Database View */}
+ {/* width, left, padding-x will change when resizing */}
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/packages/database-ui/src/database/button-group.tsx b/packages/database-ui/src/database/button-group.tsx
new file mode 100644
index 00000000..3bd26c1a
--- /dev/null
+++ b/packages/database-ui/src/database/button-group.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import {
+ ArrowUpDown,
+ ChevronDown,
+ Ellipsis,
+ ListFilter,
+ Maximize2,
+ Search,
+ Zap,
+} from "lucide-react";
+
+import { cn } from "@swy/ui/lib";
+import { Button } from "@swy/ui/shadcn";
+
+const styles = {
+ icon: "flex-shrink-0 size-4 text-primary/45 dark:text-primary/45",
+};
+
+export const ButtonGroup: React.FC<{ className?: string }> = ({
+ className,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/database-ui/src/database/constant.ts b/packages/database-ui/src/database/constant.ts
new file mode 100644
index 00000000..140d02aa
--- /dev/null
+++ b/packages/database-ui/src/database/constant.ts
@@ -0,0 +1 @@
+export const paddingX = 96;
diff --git a/packages/database-ui/src/database/database.tsx b/packages/database-ui/src/database/database.tsx
new file mode 100644
index 00000000..163ed51d
--- /dev/null
+++ b/packages/database-ui/src/database/database.tsx
@@ -0,0 +1,56 @@
+import { DatabaseIcon, Ellipsis, Plus } from "lucide-react";
+
+import {
+ Button,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@swy/ui/shadcn";
+
+import { TableView } from "../table-view";
+import { ButtonGroup } from "./button-group";
+import { ViewWrapper } from "./view-wrapper";
+
+export const Database = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/database-ui/src/database/index.ts b/packages/database-ui/src/database/index.ts
new file mode 100644
index 00000000..c30cd664
--- /dev/null
+++ b/packages/database-ui/src/database/index.ts
@@ -0,0 +1 @@
+export * from "./database";
diff --git a/packages/database-ui/src/database/view-wrapper.tsx b/packages/database-ui/src/database/view-wrapper.tsx
new file mode 100644
index 00000000..5bf60e50
--- /dev/null
+++ b/packages/database-ui/src/database/view-wrapper.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+
+import { HintProvider, ModalProvider } from "@swy/ui/shared";
+
+import { paddingX } from "./constant";
+
+export const ViewWrapper: React.FC = ({
+ children,
+}) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/database-ui/src/examples/README.md b/packages/database-ui/src/examples/README.md
new file mode 100644
index 00000000..c7d12211
--- /dev/null
+++ b/packages/database-ui/src/examples/README.md
@@ -0,0 +1,15 @@
+# Examples Usage
+
+1. Popover with Triggers
+
+ ```tsx
+
+ ```
+
+2. Popover with Modals
+
+ ```tsx
+
+
+
+ ```
diff --git a/packages/database-ui/src/examples/cell-action-provider.tsx b/packages/database-ui/src/examples/cell-action-provider.tsx
new file mode 100644
index 00000000..2d2284de
--- /dev/null
+++ b/packages/database-ui/src/examples/cell-action-provider.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import React, { useRef } from "react";
+
+import { Input, Popover, PopoverContent } from "@swy/ui/shadcn";
+import { useModal } from "@swy/ui/shared";
+
+import { CellType } from "../table-view";
+
+import "./view.css";
+
+type CellActionPopoverProps = CellType & {
+ position: {
+ top: number;
+ left: number;
+ };
+};
+
+export const CellActionPopover: React.FC = ({
+ position,
+ ...props
+}) => {
+ const { isOpen, setClose } = useModal();
+ const containerRef = useRef(null);
+
+ return (
+
+ {/* TODO this is a workaround solution */}
+ {/* See https://github.com/radix-ui/primitives/issues/2908 */}
+
+ {renderContent({ containerRef, ...props })}
+
+
+ );
+};
+
+type RenderContentProps = CellType & {
+ containerRef: React.RefObject;
+};
+
+function renderContent({ containerRef, ...data }: RenderContentProps) {
+ switch (data.type) {
+ case "title":
+ case "text":
+ return (
+ e.stopPropagation()}
+ className="flex max-h-[773px] min-h-[34px] w-[240px] flex-col overflow-visible backdrop-filter-none"
+ >
+
+ {/* */}
+
+ );
+ default:
+ return null;
+ }
+}
diff --git a/packages/database-ui/src/examples/popover-with-modals.tsx b/packages/database-ui/src/examples/popover-with-modals.tsx
new file mode 100644
index 00000000..4e835561
--- /dev/null
+++ b/packages/database-ui/src/examples/popover-with-modals.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+
+import { Button } from "@swy/ui/shadcn";
+import { useModal } from "@swy/ui/shared";
+
+import "./view.css";
+
+import { CellActionPopover } from "./cell-action-provider";
+
+const randomName: Record = {
+ "1": "alpha 1!!",
+ "2": "BRAVO",
+ "3": "chris for 3...",
+};
+
+export function PopoverDemo2() {
+ const { setOpen } = useModal();
+ const onClick = (e: React.MouseEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+ console.log("button position", e.currentTarget.title, rect);
+ setOpen(
+ ,
+ );
+ };
+ return (
+
+
+ {Array.from("abcd").map((id) => (
+
+ {Array.from("123").map((id) => (
+
+ ))}
+
+ ))}
+
+
+ );
+}
diff --git a/packages/database-ui/src/examples/popover-with-triggers.tsx b/packages/database-ui/src/examples/popover-with-triggers.tsx
new file mode 100644
index 00000000..854d0a87
--- /dev/null
+++ b/packages/database-ui/src/examples/popover-with-triggers.tsx
@@ -0,0 +1,60 @@
+import React, { useRef, useState } from "react";
+
+import {
+ Button,
+ Input,
+ Label,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@swy/ui/shadcn";
+
+const randomName: Record = {
+ "1": "alpha 1!!",
+ "2": "BRAVO",
+ "3": "chris for 3...",
+};
+
+export function PopoverDemo() {
+ const containerRef = useRef(null);
+ const [open, setOpen] = useState(false);
+ const [position, setPosition] = useState({ height: 0 });
+ const onOpenChange = (open: boolean) => {
+ if (!open) setOpen(false);
+ };
+ const onClick = (e: React.MouseEvent) => {
+ setOpen(true);
+ const rect = e.currentTarget.getBoundingClientRect();
+ setPosition({ height: rect.height });
+ console.log("button position", e.currentTarget.title, rect);
+ };
+ return (
+
+
+
+ {Array.from("abcd").map((id) => (
+
+ {Array.from("123").map((id) => (
+
+
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/database-ui/src/globals.css b/packages/database-ui/src/globals.css
new file mode 100644
index 00000000..bb8c63ee
--- /dev/null
+++ b/packages/database-ui/src/globals.css
@@ -0,0 +1,102 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ /* text color */
+ --primary: 55 53 47;
+ /* background color */
+ --bg-main: 255 255 255;
+ --bg-input: 242 241 238;
+ --bg-sidebar: 247 247 245;
+ --bg-modal: 255 255 255;
+ --bg-popover: 255 255 255;
+ --bg-tooltip: 15 15 15;
+ /* border color */
+ --border-cell: 233 233 231;
+
+ /* Geneated by Shadcn */
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ /* --popover: 0 0% 100%; */
+ /* --popover-foreground: 222.2 84% 4.9%; */
+
+ /* --primary: 222.2 47.4% 11.2%; */
+ /* --primary-foreground: 210 40% 98%; */
+
+ /* --secondary: 210 40% 96.1%; */
+ /* --secondary-foreground: 222.2 47.4% 11.2%; */
+
+ /* --muted: 210 40% 96.1%; */
+ /* --muted-foreground: 215.4 16.3% 46.9%; */
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ /* --border: 214.3 31.8% 91.4%; */
+ /* --input: 214.3 31.8% 91.4%; */
+ /* --ring: 222.2 84% 4.9%; */
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ /* text color */
+ --primary: 255 255 255;
+ /* background color */
+ --bg-main: 25 25 25;
+ --bg-input: 255 255 255;
+ --bg-sidebar: 32 32 32;
+ --bg-modal: 32 32 32;
+ --bg-popover: 37 37 37;
+ --bg-tooltip: 47 47 47;
+ /* border color */
+ --border-cell: 47 47 47;
+
+ /* Geneated by Shadcn */
+ --background: 0 0% 12.5%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ /* --popover: 222.2 84% 4.9%; */
+ /* --popover-foreground: 210 40% 98%; */
+
+ /* --primary: 210 40% 98%; */
+ /* --primary-foreground: 222.2 47.4% 11.2%; */
+
+ /* --secondary: 217.2 32.6% 17.5%; */
+ /* --secondary-foreground: 210 40% 98%; */
+
+ /* --muted: 217.2 32.6% 17.5%; */
+ /* --muted-foreground: 215 20.2% 65.1%; */
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ /* --border: 217.2 32.6% 17.5%; */
+ /* --input: 217.2 32.6% 17.5%; */
+ /* --ring: 212.7 26.8% 83.9%; */
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-main text-primary dark:text-primary/80;
+ }
+}
diff --git a/packages/database-ui/src/lib/use-reducer-atom.ts b/packages/database-ui/src/lib/use-reducer-atom.ts
new file mode 100644
index 00000000..2d9f5f46
--- /dev/null
+++ b/packages/database-ui/src/lib/use-reducer-atom.ts
@@ -0,0 +1,15 @@
+// import { useCallback } from "react";
+// import { useAtom } from "jotai";
+// import type { PrimitiveAtom } from "jotai";
+
+// export function useReducerAtom(
+// anAtom: PrimitiveAtom,
+// reducer: (v: Value, a: Action) => Value,
+// ) {
+// const [state, setState] = useAtom(anAtom);
+// const dispatch = useCallback(
+// (action: Action) => setState((prev) => reducer(prev, action)),
+// [setState, reducer],
+// );
+// return [state, dispatch] as const;
+// }
diff --git a/packages/database-ui/src/main.tsx b/packages/database-ui/src/main.tsx
new file mode 100644
index 00000000..d8d5ae6a
--- /dev/null
+++ b/packages/database-ui/src/main.tsx
@@ -0,0 +1,12 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+
+import "./globals.css";
+
+import App from "./App";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/packages/database-ui/src/notion.css b/packages/database-ui/src/notion.css
new file mode 100644
index 00000000..74c6be42
--- /dev/null
+++ b/packages/database-ui/src/notion.css
@@ -0,0 +1,9 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer components {
+ .notion-page-content {
+ @apply relative block min-h-0 w-full overflow-visible;
+ }
+}
diff --git a/packages/database-ui/src/table-view/README.md b/packages/database-ui/src/table-view/README.md
new file mode 100644
index 00000000..fdd667d2
--- /dev/null
+++ b/packages/database-ui/src/table-view/README.md
@@ -0,0 +1 @@
+- rgb(233, 233, 231) -> #E9E9E7
diff --git a/packages/database-ui/src/table-view/__mock__.tsx b/packages/database-ui/src/table-view/__mock__.tsx
new file mode 100644
index 00000000..5e3976d6
--- /dev/null
+++ b/packages/database-ui/src/table-view/__mock__.tsx
@@ -0,0 +1,65 @@
+import type { DatabaseProperty, RowDataType } from "./types";
+
+export const cols: DatabaseProperty[] = [
+ {
+ id: "prop-1",
+ type: "title",
+ name: "Name",
+ width: "216px",
+ },
+ {
+ id: "prop-2",
+ type: "text",
+ name: "Desc.",
+ width: "100px",
+ },
+ {
+ id: "prop-3",
+ type: "checkbox",
+ name: "Done",
+ width: "90px",
+ },
+];
+
+export const mockData: RowDataType[] = [
+ {
+ id: "15f35e0f-492c-804c-9534-d615e3925074",
+ properties: {
+ "prop-1": {
+ id: "prop-1-1",
+ type: "title",
+ value: "page 1",
+ },
+ "prop-2": {
+ id: "prop-1-2",
+ type: "text",
+ value: "desc1",
+ },
+ "prop-3": {
+ id: "prop-1-3",
+ type: "checkbox",
+ checked: true,
+ },
+ },
+ },
+ {
+ id: "15f35e0f-492c-809e-b647-f72038f14c5f",
+ properties: {
+ "prop-1": {
+ id: "prop-2-1",
+ type: "title",
+ value: "page 2",
+ },
+ "prop-2": {
+ id: "prop-2-2",
+ type: "text",
+ value: "desc2",
+ },
+ "prop-3": {
+ id: "prop-2-3",
+ type: "checkbox",
+ checked: false,
+ },
+ },
+ },
+];
diff --git a/packages/database-ui/src/table-view/cells/cell-trigger.tsx b/packages/database-ui/src/table-view/cells/cell-trigger.tsx
new file mode 100644
index 00000000..110e5c8f
--- /dev/null
+++ b/packages/database-ui/src/table-view/cells/cell-trigger.tsx
@@ -0,0 +1,31 @@
+import React, { forwardRef } from "react";
+
+import "../view.css";
+
+import { cn } from "@swy/ui/lib";
+
+interface CellTriggerProps extends React.PropsWithChildren {
+ className?: string;
+ wrapped?: boolean;
+ onPointerDown?: React.PointerEventHandler;
+}
+
+export const CellTrigger = forwardRef(
+ ({ className, wrapped, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+
+CellTrigger.displayName = "CellTrigger";
diff --git a/packages/database-ui/src/table-view/cells/checkbox-cell.tsx b/packages/database-ui/src/table-view/cells/checkbox-cell.tsx
new file mode 100644
index 00000000..acec13fc
--- /dev/null
+++ b/packages/database-ui/src/table-view/cells/checkbox-cell.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import React from "react";
+
+import "../view.css";
+
+import { cn } from "@swy/ui/lib";
+
+import * as Icon from "../icons";
+import { CellTrigger } from "./cell-trigger";
+
+interface CheckboxCellProps {
+ checked: boolean;
+ wrapped?: boolean;
+ onChange?: (check: boolean) => void;
+}
+
+export const CheckboxCell: React.FC = ({
+ checked,
+ wrapped,
+ onChange,
+}) => {
+ return (
+ onChange?.(!checked)}
+ >
+
+
+
+ {checked ? (
+
+ ) : (
+
+ )}
+
+
{
+ e.stopPropagation();
+ onChange?.(e.target.checked);
+ }}
+ className="absolute left-0 top-0 size-4 cursor-pointer opacity-0"
+ />
+
+
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/cells/index.ts b/packages/database-ui/src/table-view/cells/index.ts
new file mode 100644
index 00000000..5fd91abf
--- /dev/null
+++ b/packages/database-ui/src/table-view/cells/index.ts
@@ -0,0 +1,3 @@
+export * from "./checkbox-cell";
+export * from "./text-cell";
+export * from "./title-cell";
diff --git a/packages/database-ui/src/table-view/cells/text-cell.tsx b/packages/database-ui/src/table-view/cells/text-cell.tsx
new file mode 100644
index 00000000..88d76e3d
--- /dev/null
+++ b/packages/database-ui/src/table-view/cells/text-cell.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import React from "react";
+
+import "../view.css";
+
+import { useCopyToClipboard } from "usehooks-ts";
+
+import { cn } from "@swy/ui/lib";
+import { Hint } from "@swy/ui/shared";
+
+import * as Icon from "../icons";
+import { CellTrigger } from "./cell-trigger";
+import { TextInputPopover } from "./text-input-popover";
+import { useTriggerPosition } from "./use-trigger-position";
+
+interface TextCellProps {
+ value: string;
+ wrap?: boolean;
+ onChange?: (value: string) => void;
+}
+
+export const TextCell: React.FC = ({
+ value,
+ wrap,
+ onChange,
+}) => {
+ const { ref, position } = useTriggerPosition();
+ const [, copy] = useCopyToClipboard();
+
+ return (
+
+
+
+
+
+ {
+ e.stopPropagation();
+ void copy(value);
+ }}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+
+
+
+
+
+
+ {value}
+
+
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/cells/text-input-popover.tsx b/packages/database-ui/src/table-view/cells/text-input-popover.tsx
new file mode 100644
index 00000000..1fe4a3e4
--- /dev/null
+++ b/packages/database-ui/src/table-view/cells/text-input-popover.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import React, { useState } from "react";
+
+import { Input, Popover, PopoverContent, PopoverTrigger } from "@swy/ui/shadcn";
+
+import "../view.css";
+
+type TextInputPopoverProps = React.PropsWithChildren<{
+ position: { top?: number; left?: number };
+ value: string;
+ onChange?: (value: string) => void;
+}>;
+
+export const TextInputPopover: React.FC = ({
+ position,
+ value: initialValue,
+ onChange,
+ children,
+}) => {
+ const [value, setValue] = useState(initialValue);
+
+ return (
+
+ {children}
+ onChange?.(value)}
+ >
+ {
+ e.preventDefault();
+ setValue(e.target.value);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") onChange?.(value);
+ }}
+ className="word-break max-h-[771px] min-h-8 whitespace-pre-wrap border-none bg-transparent caret-primary"
+ />
+ {/* */}
+
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/cells/title-cell.tsx b/packages/database-ui/src/table-view/cells/title-cell.tsx
new file mode 100644
index 00000000..3fe7aa55
--- /dev/null
+++ b/packages/database-ui/src/table-view/cells/title-cell.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import React from "react";
+
+import { Hint } from "@swy/ui/shared";
+
+import "../view.css";
+
+import { cn } from "@swy/ui/lib";
+
+import * as Icon from "../icons";
+import { CellTrigger } from "./cell-trigger";
+import { TextInputPopover } from "./text-input-popover";
+import { useTriggerPosition } from "./use-trigger-position";
+
+interface TitleCellProps {
+ value: string;
+ wrapped?: boolean;
+ onChange?: (value: string) => void;
+}
+
+export const TitleCell: React.FC = ({
+ value,
+ wrapped,
+ onChange,
+}) => {
+ const { ref, position, width } = useTriggerPosition();
+ return (
+
+
+
+
+
+ e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+ 110 && "mr-1.5",
+ )}
+ />
+ {width > 110 && <>Open>}
+
+
+
+
+
+ {value}
+
+
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/cells/use-trigger-position.ts b/packages/database-ui/src/table-view/cells/use-trigger-position.ts
new file mode 100644
index 00000000..ed838847
--- /dev/null
+++ b/packages/database-ui/src/table-view/cells/use-trigger-position.ts
@@ -0,0 +1,24 @@
+"use client";
+
+import { useLayoutEffect, useRef, useState } from "react";
+
+interface Position {
+ top: number;
+ left: number;
+}
+
+export const useTriggerPosition = () => {
+ const ref = useRef(null);
+ const [position, setPosition] = useState({ top: 0, left: 0 });
+ const [width, setWidth] = useState(0);
+
+ useLayoutEffect(() => {
+ const rect = ref.current?.getBoundingClientRect();
+ if (rect) {
+ setPosition({ top: rect.height, left: rect.left });
+ setWidth(rect.width);
+ }
+ }, []);
+
+ return { ref, position, width };
+};
diff --git a/packages/database-ui/src/table-view/common/index.ts b/packages/database-ui/src/table-view/common/index.ts
new file mode 100644
index 00000000..97aea51a
--- /dev/null
+++ b/packages/database-ui/src/table-view/common/index.ts
@@ -0,0 +1,2 @@
+export * from "./menu";
+export * from "./prop-meta";
diff --git a/packages/database-ui/src/table-view/common/menu.tsx b/packages/database-ui/src/table-view/common/menu.tsx
new file mode 100644
index 00000000..c1c4e154
--- /dev/null
+++ b/packages/database-ui/src/table-view/common/menu.tsx
@@ -0,0 +1,98 @@
+import React, { forwardRef } from "react";
+
+import { cn } from "@swy/ui/lib";
+import {
+ Button,
+ groupVariants,
+ menuItemVariants,
+ PopoverClose,
+ type MenuItemVariants,
+} from "@swy/ui/shadcn";
+
+import * as Icon from "../icons";
+
+interface MenuHeaderProps {
+ title: string;
+ onBack?: () => void;
+}
+
+export const MenuHeader: React.FC = ({ title, onBack }) => {
+ return (
+
+
+
+ {title}
+
+
+
+
+
+ );
+};
+
+export const MenuGroup: React.FC = ({ children }) => (
+
+ {children}
+
+);
+
+interface MenuGroupHeaderProps {
+ title: string;
+ action?: string | null;
+ onActionClick?: () => void;
+}
+
+export const MenuGroupHeader: React.FC = ({
+ title,
+ action,
+ onActionClick,
+}) => {
+ return (
+
+
{title}
+ {action && (
+
+
+
+ )}
+
+ );
+};
+
+type MenuItemProps = React.HTMLAttributes &
+ MenuItemVariants & {
+ disabled?: boolean;
+ };
+
+export const MenuItem = forwardRef(
+ ({ children, variant, disabled, className, ...props }, ref) => (
+
+ {children}
+
+ ),
+);
+
+MenuItem.displayName = "MenuItem";
diff --git a/packages/database-ui/src/table-view/common/prop-meta.tsx b/packages/database-ui/src/table-view/common/prop-meta.tsx
new file mode 100644
index 00000000..6fa354ef
--- /dev/null
+++ b/packages/database-ui/src/table-view/common/prop-meta.tsx
@@ -0,0 +1,111 @@
+"use client";
+
+import { useLayoutEffect, useState } from "react";
+
+import { Input } from "@swy/ui/shadcn";
+import { Hint, IconBlock, IconMenu } from "@swy/ui/shared";
+
+import { DefaultIcon } from "../default-icon";
+import { useInputField } from "../hooks";
+import * as Icon from "../icons";
+import { UpdateColumnPayload } from "../table-contexts";
+import { DatabaseProperty } from "../types";
+
+interface PropMetaProps {
+ property: Pick;
+ validateName: (value: string) => boolean;
+ onUpdate: (data: Omit) => void;
+ onKeyDownUpdate: () => void;
+}
+
+export const PropMeta: React.FC = ({
+ property,
+ validateName,
+ onUpdate,
+ onKeyDownUpdate,
+}) => {
+ const [showDesc, setShowDesc] = useState(false);
+ const toggleDesc = () => setShowDesc((prev) => !prev);
+
+ const nameField = useInputField({
+ id: "name",
+ initialValue: property.name,
+ validate: validateName,
+ onUpdate: (name) => onUpdate({ name }),
+ onKeyDownUpdate,
+ });
+ const descField = useInputField({
+ id: "description",
+ initialValue: property.description ?? "",
+ onUpdate: (description) => onUpdate({ description }),
+ onKeyDownUpdate,
+ });
+ /** Icon */
+ const uploadIcon = (file: File) => {
+ // TODO impl. this
+ onUpdate({ icon: { type: "file", url: URL.createObjectURL(file) } });
+ };
+
+ useLayoutEffect(() => {
+ if (showDesc) descField.ref.current?.focus();
+ }, [descField.ref, showDesc]);
+
+ return (
+ <>
+
+
+
+ onUpdate({ icon })}
+ onRemove={() => onUpdate({ icon: null })}
+ onUpload={uploadIcon}
+ >
+ {property.icon ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {nameField.error && (
+
+ A property named Select already exists in this database.
+
+ )}
+
+ {showDesc && (
+
+
+
+ )}
+ >
+ );
+};
diff --git a/packages/database-ui/src/table-view/default-icon.tsx b/packages/database-ui/src/table-view/default-icon.tsx
new file mode 100644
index 00000000..68fda399
--- /dev/null
+++ b/packages/database-ui/src/table-view/default-icon.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+
+import { cn } from "@swy/ui/lib";
+
+import * as Icon from "./icons";
+import { PropertyType } from "./types";
+
+interface DefaultIconProps {
+ type: PropertyType;
+ className?: string;
+}
+
+export const DefaultIcon: React.FC = ({
+ type,
+ className,
+}) => {
+ const iconClassName = cn("block size-4 shrink-0", className);
+ switch (type) {
+ case "title":
+ return ;
+ case "text":
+ return ;
+ case "checkbox":
+ return ;
+ case "select":
+ default:
+ return ;
+ }
+};
diff --git a/packages/database-ui/src/table-view/hooks/index.ts b/packages/database-ui/src/table-view/hooks/index.ts
new file mode 100644
index 00000000..b4335405
--- /dev/null
+++ b/packages/database-ui/src/table-view/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./use-input-field";
diff --git a/packages/database-ui/src/table-view/hooks/use-input-field.ts b/packages/database-ui/src/table-view/hooks/use-input-field.ts
new file mode 100644
index 00000000..ffd1c46c
--- /dev/null
+++ b/packages/database-ui/src/table-view/hooks/use-input-field.ts
@@ -0,0 +1,58 @@
+import React, { useLayoutEffect, useRef, useState } from "react";
+
+interface UseInputFieldOptions {
+ id: string;
+ initialValue: string;
+ validate?: (value: string) => boolean;
+ onUpdate?: (value: string) => void;
+ onKeyDownUpdate?: () => void;
+}
+
+interface UseInputFieldResults {
+ error: boolean;
+ props: Pick<
+ React.InputHTMLAttributes,
+ "id" | "value" | "onError" | "onChange" | "onBlur" | "onKeyDown"
+ >;
+ ref: React.RefObject;
+}
+
+export const useInputField = ({
+ id,
+ initialValue,
+ validate = (_value) => true,
+ onUpdate,
+ onKeyDownUpdate,
+}: UseInputFieldOptions): UseInputFieldResults => {
+ const ref = useRef(null);
+ const [value, setValue] = useState(initialValue);
+ const [error, setError] = useState(false);
+
+ useLayoutEffect(() => {
+ ref.current?.focus();
+ }, []);
+
+ return {
+ error,
+ ref,
+ props: {
+ id,
+ value,
+ onChange: (e) => {
+ e.preventDefault();
+ setValue(e.target.value);
+ const isValid = validate(e.target.value);
+ setError(!isValid);
+ },
+ onBlur: () => {
+ if (error || value === initialValue) return;
+ onUpdate?.(value);
+ },
+ onKeyDown: (e) => {
+ if (e.key !== "Enter" || value === initialValue) return;
+ if (!error) onUpdate?.(value);
+ onKeyDownUpdate?.();
+ },
+ },
+ };
+};
diff --git a/packages/database-ui/src/table-view/icons.tsx b/packages/database-ui/src/table-view/icons.tsx
new file mode 100644
index 00000000..93bcdc34
--- /dev/null
+++ b/packages/database-ui/src/table-view/icons.tsx
@@ -0,0 +1,230 @@
+interface IconProps {
+ className?: string;
+}
+
+export const DragHandle = (props: IconProps) => (
+
+);
+
+export const TypesTitle = (props: IconProps) => (
+
+);
+
+export const TypesText = (props: IconProps) => (
+
+);
+
+export const TypesCheckbox = (props: IconProps) => (
+
+);
+
+export const Plus = (props: IconProps) => (
+
+);
+
+export const Dots = (props: IconProps) => (
+
+);
+
+export const RoundedSquareCheckbox = (props: IconProps) => (
+
+);
+
+export const RoundedCheck = (props: IconProps) => (
+
+);
+
+export const PeekModeSide = (props: IconProps) => (
+
+);
+
+export const Copy = (props: IconProps) => (
+
+);
+
+export const ArrowLeftThick = (props: IconProps) => (
+
+);
+
+export const Close = (props: IconProps) => (
+
+);
+
+export const InfoFilled = (props: IconProps) => (
+
+);
+
+export const Info = (props: IconProps) => (
+
+);
+
+export const Help = (props: IconProps) => (
+
+);
+
+export const ChevronRight = (props: IconProps) => (
+
+);
+
+export const Undo = (props: IconProps) => (
+
+);
+
+export const Eye = (props: IconProps) => (
+
+);
+
+export const EyeHide = (props: IconProps) => (
+
+);
+
+export const EyeHideInversePadded = (props: IconProps) => (
+
+);
+
+export const Duplicate = (props: IconProps) => (
+
+);
+export const Trash = (props: IconProps) => (
+
+);
+
+export const Options = (props: IconProps) => (
+
+);
+
+export const Pin = (props: IconProps) => (
+
+);
+
+export const PinStrikeThrough = (props: IconProps) => (
+
+);
+
+export const LockedFilled = (props: IconProps) => (
+
+);
diff --git a/packages/database-ui/src/table-view/index.ts b/packages/database-ui/src/table-view/index.ts
new file mode 100644
index 00000000..34870859
--- /dev/null
+++ b/packages/database-ui/src/table-view/index.ts
@@ -0,0 +1,2 @@
+export * from "./types";
+export * from "./view";
diff --git a/packages/database-ui/src/table-view/menus/deleted-props-menu.tsx b/packages/database-ui/src/table-view/menus/deleted-props-menu.tsx
new file mode 100644
index 00000000..b58f923a
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/deleted-props-menu.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import { useMemo } from "react";
+
+import { Button } from "@swy/ui/shadcn";
+import { IconBlock } from "@swy/ui/shared";
+
+import { MenuGroup, MenuHeader, MenuItem } from "../common";
+import { DefaultIcon } from "../default-icon";
+import * as Icon from "../icons";
+import { useTableActions, useTableViewCtx } from "../table-contexts";
+import type { DatabaseProperty } from "../types";
+import { useMenuControl } from "./menu-control-context";
+import { PropsMenu } from "./props-menu";
+
+export const DeletedPropsMenu = () => {
+ const { properties } = useTableViewCtx();
+ const { updateColumn, deleteColumn } = useTableActions();
+ const { openPopover } = useMenuControl();
+
+ const openPropsMenu = () => openPopover(, { x: -12, y: -12 });
+
+ const deletedProps = useMemo(
+ () => Object.values(properties).filter((prop) => prop.isDeleted),
+ [properties],
+ );
+
+ return (
+ <>
+
+
+ {deletedProps.map((prop) => (
+ updateColumn(prop.id, { isDeleted: false })}
+ onDelete={() => deleteColumn(prop.id)}
+ />
+ ))}
+
+ >
+ );
+};
+
+interface PropertyItemProps {
+ property: DatabaseProperty;
+ onRestore: () => void;
+ onDelete: () => void;
+}
+
+const PropertyItem: React.FC = ({
+ property,
+ onRestore,
+ onDelete,
+}) => {
+ const { name, icon, type } = property;
+
+ return (
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/menus/edit-prop-menu.tsx b/packages/database-ui/src/table-view/menus/edit-prop-menu.tsx
new file mode 100644
index 00000000..fb0878e6
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/edit-prop-menu.tsx
@@ -0,0 +1,161 @@
+"use client";
+
+import React from "react";
+
+import { Separator, Switch } from "@swy/ui/shadcn";
+
+import "../view.css";
+
+import { Hint } from "@swy/ui/shared";
+
+import { MenuGroup, MenuHeader, MenuItem, PropMeta } from "../common";
+import { DefaultIcon } from "../default-icon";
+import * as Icon from "../icons";
+import { useTableActions, useTableViewCtx } from "../table-contexts";
+import { useMenuControl } from "./menu-control-context";
+import { PropsMenu } from "./props-menu";
+import { TypesMenu } from "./types-menu";
+import { propertyTypes } from "./types-menu-options";
+
+interface EditPropMenuProps {
+ propId: string;
+}
+
+/**
+ * @summary The property editing menu
+ *
+ * 1. ✅ Type selection
+ * 2. 🚧 Type config
+ * 3. ✅ Wrap in view
+ * 4. ✅ Hide in view
+ * 5. ✅ Duplicate property
+ * 6. ✅ Delete property
+ */
+export const EditPropMenu: React.FC = ({ propId }) => {
+ const { properties, isPropertyUnique } = useTableViewCtx();
+ const { updateColumn, duplicateColumn } = useTableActions();
+ const { openPopover, closePopover } = useMenuControl();
+
+ const property = properties[propId]!;
+
+ const openPropsMenu = () => openPopover(, { x: -12, y: -12 });
+
+ // 1. Type selection
+ const openTypesMenu = () =>
+ openPopover(, { x: -12, y: -12 });
+ // 3. Wrap in view
+ const wrapProp = () =>
+ updateColumn(property.id, { wrapped: !property.wrapped });
+ // 4. Hide in view
+ const hideProp = () => {
+ updateColumn(property.id, { hidden: true });
+ closePopover();
+ };
+ // 5. Duplicate property
+ const duplicateProp = () => {
+ duplicateColumn(property.id);
+ closePopover();
+ };
+ // 6. Delete property
+ const deleteProp = () => {
+ updateColumn(property.id, { isDeleted: true });
+ closePopover();
+ };
+
+ return (
+ <>
+
+ updateColumn(property.id, data)}
+ onKeyDownUpdate={closePopover}
+ />
+
+ {property.type === "title" ? (
+
+
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ {property.type !== "title" && (
+ <>
+
+
+
+ >
+ )}
+
+ >
+ );
+};
diff --git a/packages/database-ui/src/table-view/menus/index.ts b/packages/database-ui/src/table-view/menus/index.ts
new file mode 100644
index 00000000..dad203b4
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/index.ts
@@ -0,0 +1,6 @@
+export * from "./edit-prop-menu";
+export * from "./menu-control-context";
+export * from "./menu-control-provider";
+export * from "./types-menu";
+export * from "./prop-menu";
+export * from "./props-menu";
diff --git a/packages/database-ui/src/table-view/menus/menu-control-context.ts b/packages/database-ui/src/table-view/menus/menu-control-context.ts
new file mode 100644
index 00000000..eaea8169
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/menu-control-context.ts
@@ -0,0 +1,27 @@
+"use client";
+
+import { createContext, useContext } from "react";
+
+export interface MenuControlInterface {
+ openPopover: (
+ popover: React.ReactNode,
+ style: {
+ x?: number;
+ y?: number;
+ className?: string;
+ },
+ ) => void;
+ closePopover: () => void;
+}
+
+export const MenuControlContext = createContext(
+ null,
+);
+
+export const useMenuControl = () => {
+ const context = useContext(MenuControlContext);
+ if (!context) {
+ throw new Error("useMenuControl must be used within a MenuControlProvider");
+ }
+ return context;
+};
diff --git a/packages/database-ui/src/table-view/menus/menu-control-provider.tsx b/packages/database-ui/src/table-view/menus/menu-control-provider.tsx
new file mode 100644
index 00000000..3e92ed14
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/menu-control-provider.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import React, { useMemo, useRef, useState } from "react";
+
+import { cn } from "@swy/ui/lib";
+import { Popover, PopoverContent, PopoverTrigger } from "@swy/ui/shadcn";
+
+import {
+ MenuControlContext,
+ MenuControlInterface,
+} from "./menu-control-context";
+
+type MenuControlProviderProps = React.PropsWithChildren;
+
+export const MenuControlProvider: React.FC = ({
+ children,
+}) => {
+ const containerRef = useRef(null);
+
+ const [open, setOpen] = useState(false);
+ const [popover, setPopover] = useState(null);
+ const [position, setPosition] = useState({ x: 0, y: 0 });
+
+ const contextValue = useMemo(
+ () => ({
+ openPopover: (popover, { x, y, className }) => {
+ if (!popover) return false;
+ setPopover(
+
+ {popover}
+ ,
+ );
+ setPosition({ x: x ?? 0, y: y ?? 0 });
+ setOpen(true);
+ },
+ closePopover: () => {
+ setPopover(null);
+ setOpen(false);
+ },
+ }),
+ [],
+ );
+
+ return (
+
+
+ {children}
+
+
+ {popover}
+
+
+
+ );
+};
+
+const getTriggerPosition = (
+ containerRef: React.RefObject,
+ position: { x: number; y: number },
+) => {
+ const rect = containerRef.current?.getBoundingClientRect();
+ const style: React.CSSProperties = {};
+ if (position.x >= 0) {
+ style.left = position.x - (rect?.x ?? 0);
+ } else {
+ style.right = -position.x;
+ }
+ if (position.y >= 0) {
+ style.top = position.y - (rect?.y ?? 0);
+ } else {
+ style.bottom = -position.y;
+ }
+ return style;
+};
diff --git a/packages/database-ui/src/table-view/menus/prop-menu.tsx b/packages/database-ui/src/table-view/menus/prop-menu.tsx
new file mode 100644
index 00000000..671e5114
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/prop-menu.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import React from "react";
+
+import { Separator, Switch } from "@swy/ui/shadcn";
+
+import "../view.css";
+
+import { MenuGroup, MenuItem, PropMeta } from "../common";
+import * as Icon from "../icons";
+import { useTableActions, useTableViewCtx } from "../table-contexts";
+import { EditPropMenu } from "./edit-prop-menu";
+import { useMenuControl } from "./menu-control-context";
+
+interface PropMenuProps {
+ propId: string;
+ rect?: DOMRect;
+}
+
+/**
+ * @summary The definition of the property
+ *
+ * 1. ✅ Edit property: opens `EditPropMenu`
+ * 2. 🚧 Sorting
+ * 3. 🚧 Filter
+ * 4. ✅ Hide in view
+ * 5. ✅ Freeze up to column
+ * 6. ✅ Duplicate property
+ * 7. ✅ Delete property
+ * 8. ✅ Wrap column
+ */
+export const PropMenu: React.FC = ({ propId, rect }) => {
+ const { table, properties, isPropertyUnique, canFreezeProperty } =
+ useTableViewCtx();
+ const { updateColumn, duplicateColumn, freezeColumns } = useTableActions();
+ const { openPopover, closePopover } = useMenuControl();
+
+ const property = properties[propId]!;
+
+ // 1. Edit property
+ const openEditPropMenu = () => {
+ openPopover(, {
+ x: rect?.x,
+ y: rect?.bottom,
+ });
+ };
+ // 4. Hide in view
+ const hideProp = () => {
+ updateColumn(property.id, { hidden: true });
+ closePopover();
+ };
+ // 5. Pin columns
+ const canFreeze = canFreezeProperty(property.id);
+ const canUnfreeze = table.getColumn(property.id)?.getIsLastColumn("left");
+ const pinColumns = () => {
+ freezeColumns(canUnfreeze ? null : property.id);
+ closePopover();
+ };
+ // 6. Duplicate property
+ const duplicateProp = () => {
+ duplicateColumn(property.id);
+ closePopover();
+ };
+ // 7. Delete property
+ const deleteProp = () => {
+ updateColumn(property.id, { isDeleted: true });
+ closePopover();
+ };
+ // 8. Wrap in view
+ const wrapProp = () =>
+ updateColumn(property.id, { wrapped: !property.wrapped });
+
+ return (
+ <>
+ updateColumn(property.id, data)}
+ onKeyDownUpdate={closePopover}
+ />
+
+
+
+
+
+ {property.type !== "title" && (
+
+ )}
+
+ {property.type !== "title" && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+ >
+ );
+};
diff --git a/packages/database-ui/src/table-view/menus/props-menu.tsx b/packages/database-ui/src/table-view/menus/props-menu.tsx
new file mode 100644
index 00000000..fcd98b89
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/props-menu.tsx
@@ -0,0 +1,243 @@
+import { useLayoutEffect, useMemo, useRef } from "react";
+import { closestCenter, DndContext } from "@dnd-kit/core";
+import {
+ restrictToParentElement,
+ restrictToVerticalAxis,
+} from "@dnd-kit/modifiers";
+import {
+ SortableContext,
+ useSortable,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+
+import { useFilter } from "@swy/ui/hooks";
+import { Button, Input, Separator } from "@swy/ui/shadcn";
+import { IconBlock } from "@swy/ui/shared";
+
+import { MenuGroup, MenuGroupHeader, MenuHeader, MenuItem } from "../common";
+import { DefaultIcon } from "../default-icon";
+import * as Icon from "../icons";
+import { useTableActions, useTableViewCtx } from "../table-contexts";
+import type { DatabaseProperty } from "../types";
+import { DeletedPropsMenu } from "./deleted-props-menu";
+import { EditPropMenu } from "./edit-prop-menu";
+import { useMenuControl } from "./menu-control-context";
+import { TypesMenu } from "./types-menu";
+
+/**
+ * @summary The menu of all properties
+ */
+export const PropsMenu = () => {
+ const { table, properties } = useTableViewCtx();
+ const { reorderColumns, updateColumn, toggleAllColumns } = useTableActions();
+ const { openPopover } = useMenuControl();
+
+ const { columnOrder, columnVisibility } = table.getState();
+ const noShownProps = (() => {
+ const count = Object.values(columnVisibility).reduce(
+ (acc, shown) => {
+ const num = Number(shown) as 0 | 1;
+ acc[num]++;
+ return acc;
+ },
+ { [1]: 0, [0]: 0 },
+ );
+ return count[1] === 1;
+ })();
+ // Search
+ const inputRef = useRef(null);
+ const [props, deletedCount] = useMemo(() => {
+ const props: DatabaseProperty[] = [];
+ let deletedCount = 0;
+ columnOrder.forEach((propId) => {
+ const prop = properties[propId];
+ if (prop && !prop.isDeleted) {
+ props.push(prop);
+ } else {
+ deletedCount++;
+ }
+ });
+ return [props, deletedCount];
+ }, [properties, columnOrder]);
+ const { search, results, updateSearch } = useFilter(
+ props,
+ (prop, v) => prop.name.toLowerCase().includes(v),
+ { default: "empty" },
+ );
+ // Menu actions
+ const openTypesMenu = () =>
+ openPopover(, { x: -12, y: -12 });
+ const openEditPropMenu = (propId: string) =>
+ openPopover(, { x: -12, y: -12 });
+ const toggleVisibility = (propId: string, hidden: boolean) =>
+ updateColumn(propId, { hidden });
+ const openDeletedPropsMenu = () =>
+ openPopover(, { x: -12, y: -12 });
+
+ useLayoutEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+
+ return (
+ <>
+
+
+ updateSearch(e.target.value)}
+ onCancel={() => updateSearch("")}
+ placeholder="Search for a property..."
+ />
+
+
+ toggleAllColumns(!noShownProps)}
+ />
+
+
+
+ {search.length === 0
+ ? props.map((prop) => (
+ openEditPropMenu(prop.id)}
+ onVisibilityChange={(hidden) =>
+ toggleVisibility(prop.id, hidden)
+ }
+ />
+ ))
+ : (results ?? []).map((prop) => (
+ openEditPropMenu(prop.id)}
+ onVisibilityChange={(hidden) =>
+ toggleVisibility(prop.id, hidden)
+ }
+ />
+ ))}
+
+
+
+
+
+ {deletedCount > 0 && (
+
+ )}
+
+
+
+
+
+
+ >
+ );
+};
+
+interface PropertyItemProps {
+ draggable?: boolean;
+ property: DatabaseProperty;
+ onClick: () => void;
+ onVisibilityChange: (hidden: boolean) => void;
+}
+
+const PropertyItem: React.FC = ({
+ draggable,
+ property,
+ onClick,
+ onVisibilityChange,
+}) => {
+ const { id, name, icon, type, hidden } = property;
+
+ /** DND */
+ const {
+ attributes,
+ isDragging,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ } = useSortable({ id });
+
+ const style: React.CSSProperties = {
+ opacity: isDragging ? 0.8 : 1,
+ zIndex: isDragging ? 10 : 0,
+ transform: CSS.Translate.toString(transform), // translate instead of transform to avoid squishing
+ transition, // Warning: it is somehow laggy
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/menus/types-menu-options.tsx b/packages/database-ui/src/table-view/menus/types-menu-options.tsx
new file mode 100644
index 00000000..81fde473
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/types-menu-options.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+
+import { DefaultIcon } from "../default-icon";
+import type { PropertyType } from "../types";
+
+interface MenuOption {
+ type: PropertyType;
+ title: string;
+ description: string;
+ icon: React.ReactNode;
+}
+
+export const propertyTypes: Partial> = {
+ title: {
+ type: "title",
+ title: "Title",
+ description: "",
+ icon: (
+
+ ),
+ },
+ text: {
+ type: "text",
+ title: "Text",
+ description:
+ "Add text that can be formatted. Great for summaries, notes, or descriptions.",
+ icon: ,
+ },
+ checkbox: {
+ type: "checkbox",
+ title: "Checkbox",
+ description:
+ "Use a checkbox to indicate whether a condition is true or false. Useful for lightweight task tracking.",
+ icon: (
+
+ ),
+ },
+} as const;
+
+export const propOptions = Object.values(propertyTypes);
diff --git a/packages/database-ui/src/table-view/menus/types-menu.tsx b/packages/database-ui/src/table-view/menus/types-menu.tsx
new file mode 100644
index 00000000..bae17a57
--- /dev/null
+++ b/packages/database-ui/src/table-view/menus/types-menu.tsx
@@ -0,0 +1,142 @@
+"use client";
+
+import React from "react";
+import { v4 } from "uuid";
+
+import { useFilter } from "@swy/ui/hooks";
+import { cn } from "@swy/ui/lib";
+import {
+ Command,
+ CommandGroup,
+ CommandItem,
+ CommandList,
+ Input,
+} from "@swy/ui/shadcn";
+import { Hint } from "@swy/ui/shared";
+
+import { MenuHeader } from "../common";
+import { DefaultIcon } from "../default-icon";
+import { useTableActions, useTableViewCtx } from "../table-contexts";
+import type { PropertyType } from "../types";
+import { getUniqueName } from "../utils";
+import { EditPropMenu } from "./edit-prop-menu";
+import { useMenuControl } from "./menu-control-context";
+import { propOptions } from "./types-menu-options";
+
+interface TypesMenuProps {
+ /**
+ * @prop {propId}: if null, will create a new column;
+ * otherwise will update a column by given `propId`
+ */
+ propId: string | null;
+}
+
+export const TypesMenu: React.FC = ({ propId }) => {
+ const { properties } = useTableViewCtx();
+ const { addColumn, updateColumnType } = useTableActions();
+ const { openPopover } = useMenuControl();
+
+ const property = propId ? properties[propId]! : null;
+
+ const { search, results, updateSearch } = useFilter(propOptions, (prop, v) =>
+ prop.title.toLowerCase().includes(v),
+ );
+ const select = (type: PropertyType, name: string) => {
+ let colId = propId;
+ if (colId === null) {
+ colId = v4();
+ const uniqueName = getUniqueName(
+ name,
+ Object.values(properties).map((p) => p.name),
+ );
+ addColumn({ id: colId, type, name: uniqueName });
+ } else {
+ updateColumnType(colId, type);
+ }
+
+ openPopover(, { x: -12, y: -12 });
+ };
+
+ return (
+ <>
+
+
+
+ updateSearch(e.target.value)}
+ placeholder={
+ propId ? "Search for property type" : "Search or add new property"
+ }
+ />
+
+
+ {results && results.length > 0 && (
+
+ {results.map(({ type, title, description, icon }) => (
+
+ select(type, title)}
+ disabled={type === "title"}
+ >
+ {icon}
+ {title}
+ {property?.type === type && (
+
+ )}
+
+
+ ))}
+
+ )}
+ {!propId && search.length > 0 && (
+
+ select("text", search)}
+ >
+
+ {search}
+
+
+ )}
+
+
+ >
+ );
+};
diff --git a/packages/database-ui/src/table-view/table-body.tsx b/packages/database-ui/src/table-view/table-body.tsx
new file mode 100644
index 00000000..a1ed5b6b
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-body.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import type { Table } from "@tanstack/react-table";
+
+import { TableRow } from "./table-row";
+import type { RowDataType } from "./types";
+
+interface TableBodyProps {
+ table: Table;
+}
+
+/**
+ * un-memoized normal table body component - see memoized version below
+ */
+export const TableBody: React.FC = ({ table }) => {
+ return table
+ .getRowModel()
+ .rows.map((row) => );
+};
+
+/**
+ * special memoized wrapper for our table body that we will use during column resizing
+ */
+export const MemoizedTableBody = React.memo(
+ TableBody,
+ (prev, next) => prev.table.options.data === next.table.options.data,
+);
diff --git a/packages/database-ui/src/table-view/table-contexts/index.ts b/packages/database-ui/src/table-view/table-contexts/index.ts
new file mode 100644
index 00000000..50104102
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-contexts/index.ts
@@ -0,0 +1,4 @@
+export * from "./table-view-context";
+export * from "./table-view-provider";
+export * from "./table-reducer";
+export type * from "./types";
diff --git a/packages/database-ui/src/table-view/table-contexts/table-reducer.tsx b/packages/database-ui/src/table-view/table-contexts/table-reducer.tsx
new file mode 100644
index 00000000..cf594b8d
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-contexts/table-reducer.tsx
@@ -0,0 +1,199 @@
+import type { Updater } from "@tanstack/react-table";
+import { v4 } from "uuid";
+
+import type {
+ CellDataType,
+ DatabaseProperty,
+ PropertyType,
+ RowDataType,
+} from "../types";
+import {
+ getDefaultCell,
+ getUniqueName,
+ transferPropertyValues,
+} from "../utils";
+import type { AddColumnPayload, UpdateColumnPayload } from "./types";
+
+export interface TableViewAtom {
+ /**
+ * @field property definitions
+ * key: property (column) id
+ */
+ properties: Record;
+ /**
+ * @field column freezing up to the given `index` in `propertiesOrder`
+ * returns -1 if no column is freezing
+ */
+ freezedIndex: number;
+ /**
+ * @field array of ordered property (column) ids
+ * @note freezed columns: `propertiesOrder.slice(0, freezedIndex + 1)`
+ */
+ propertiesOrder: string[];
+ data: RowDataType[];
+}
+
+export type TableViewAction =
+ | { type: "add:col"; payload: AddColumnPayload }
+ | { type: "update:col"; payload: { id: string; data: UpdateColumnPayload } }
+ | { type: "update:col:type"; payload: { id: string; type: PropertyType } }
+ | { type: "update:col:visibility"; payload: { hidden: boolean } }
+ | { type: "reorder:col"; updater: Updater }
+ | { type: "freeze:col"; payload: { id: string | null } }
+ | { type: "delete:col" | "duplicate:col"; payload: { id: string } }
+ | {
+ type: "update:cell";
+ payload: { rowId: string; colId: string; data: CellDataType };
+ }
+ | { type: "reset" };
+
+export const tableViewReducer = (
+ v: TableViewAtom,
+ a: TableViewAction,
+): TableViewAtom => {
+ switch (a.type) {
+ case "add:col": {
+ const { id: colId, type, name } = a.payload;
+ return {
+ properties: {
+ ...v.properties,
+ [colId]: { id: colId, type, name, icon: null },
+ },
+ propertiesOrder: [...v.propertiesOrder, colId],
+ freezedIndex: v.freezedIndex,
+ data: v.data.map(({ id, properties }) => ({
+ id,
+ properties: { ...properties, [colId]: getDefaultCell(type) },
+ })),
+ };
+ }
+ case "update:col": {
+ const prop = v.properties[a.payload.id];
+ if (!prop) return v;
+ return {
+ ...v,
+ properties: {
+ ...v.properties,
+ [a.payload.id]: { ...prop, ...a.payload.data },
+ },
+ };
+ }
+ case "update:col:type": {
+ const prop = v.properties[a.payload.id];
+ if (!prop) return v;
+ return {
+ ...v,
+ properties: {
+ ...v.properties,
+ [a.payload.id]: { ...prop, type: a.payload.type },
+ },
+ data: v.data.map((row) => {
+ if (row.properties[a.payload.id] === undefined) return row;
+ return {
+ ...row,
+ properties: {
+ ...row.properties,
+ [a.payload.id]: transferPropertyValues(
+ row.properties[a.payload.id]!,
+ a.payload.type,
+ ),
+ },
+ };
+ }),
+ };
+ }
+ case "update:col:visibility": {
+ const properties = { ...v.properties };
+ v.propertiesOrder.forEach((propId) => {
+ const property = properties[propId]!;
+ property.hidden = property.type === "title" ? false : a.payload.hidden;
+ });
+ return { ...v, properties };
+ }
+ case "reorder:col": {
+ const propertiesOrder = Array.isArray(a.updater)
+ ? a.updater
+ : a.updater(v.propertiesOrder);
+ return { ...v, propertiesOrder };
+ }
+ case "duplicate:col": {
+ const src = v.properties[a.payload.id];
+ const idx = v.propertiesOrder.findIndex(
+ (colId) => colId === a.payload.id,
+ );
+ if (!src || idx < 0) return v;
+ const prop = {
+ ...src,
+ id: v4(),
+ name: getUniqueName(
+ src.name,
+ Object.values(v.properties).map((col) => col.name),
+ ),
+ };
+ return {
+ properties: { ...v.properties, [prop.id]: prop },
+ propertiesOrder: [
+ ...v.propertiesOrder.slice(0, idx + 1),
+ prop.id,
+ ...v.propertiesOrder.slice(idx + 1),
+ ],
+ freezedIndex: v.freezedIndex + Number(idx <= v.freezedIndex),
+ data: v.data.map(({ id, properties }) => ({
+ id,
+ properties: { ...properties, [prop.id]: getDefaultCell(src.type) },
+ })),
+ };
+ }
+ case "freeze:col": {
+ const freezedIndex =
+ a.payload.id !== null
+ ? v.propertiesOrder.findIndex((colId) => colId === a.payload.id)
+ : -1;
+ return { ...v, freezedIndex };
+ }
+ case "delete:col": {
+ const idx = v.propertiesOrder.findIndex(
+ (colId) => colId === a.payload.id,
+ );
+ if (idx < 0) return v;
+
+ return {
+ get properties() {
+ const { [a.payload.id]: _, ...properties } = v.properties;
+ return properties;
+ },
+ propertiesOrder: v.propertiesOrder.filter(
+ (colId) => colId !== a.payload.id,
+ ),
+ freezedIndex: v.freezedIndex - Number(idx <= v.freezedIndex),
+ data: v.data.map((row) => {
+ const { [a.payload.id]: _, ...rest } = row.properties;
+ return { ...row, properties: rest };
+ }),
+ };
+ }
+ case "update:cell":
+ return {
+ ...v,
+ data: v.data.map((row) => {
+ if (row.id !== a.payload.rowId) return row;
+ return {
+ ...row,
+ properties: {
+ ...row.properties,
+ [a.payload.colId]: a.payload.data,
+ },
+ };
+ }),
+ };
+ case "reset":
+ return {
+ properties: {},
+ propertiesOrder: [],
+ freezedIndex: -1,
+ data: [],
+ };
+ default:
+ return v;
+ }
+};
diff --git a/packages/database-ui/src/table-view/table-contexts/table-view-context.ts b/packages/database-ui/src/table-view/table-contexts/table-view-context.ts
new file mode 100644
index 00000000..0b2e1738
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-contexts/table-view-context.ts
@@ -0,0 +1,58 @@
+"use client";
+
+import React, { createContext, useContext } from "react";
+import type {
+ DragEndEvent,
+ SensorDescriptor,
+ SensorOptions,
+} from "@dnd-kit/core";
+import type { Table } from "@tanstack/react-table";
+
+import type { DatabaseProperty, PropertyType, RowDataType } from "../types";
+import type { TableViewAction } from "./table-reducer";
+import type { AddColumnPayload, UpdateColumnPayload } from "./types";
+
+export interface TableViewCtx {
+ table: Table;
+ properties: Record;
+ data: RowDataType[];
+ columnSizeVars: Record;
+ isPropertyUnique: (name: string) => boolean;
+ canFreezeProperty: (id: string) => boolean;
+ /** DND */
+ columnSensors: SensorDescriptor[];
+}
+
+export const TableViewContext = createContext(null);
+
+export const useTableViewCtx = () => {
+ const ctx = useContext(TableViewContext);
+ if (!ctx)
+ throw new Error(
+ "`useTableViewCtx` must be used within `TableViewProvider`",
+ );
+ return ctx;
+};
+
+export interface TableActions {
+ dispatch: React.Dispatch;
+ addColumn: (data: AddColumnPayload) => void;
+ updateColumn: (id: string, data: UpdateColumnPayload) => void;
+ toggleAllColumns: (hidden: boolean) => void;
+ updateColumnType: (id: string, type: PropertyType) => void;
+ reorderColumns: (e: DragEndEvent) => void;
+ duplicateColumn: (id: string) => void;
+ freezeColumns: (id: string | null) => void;
+ deleteColumn: (id: string) => void;
+}
+
+export const TableActionsContext = createContext(null);
+
+export const useTableActions = () => {
+ const ctx = useContext(TableActionsContext);
+ if (!ctx)
+ throw new Error(
+ "`useTableActions` must be used within `TableActionsProvider`",
+ );
+ return ctx;
+};
diff --git a/packages/database-ui/src/table-view/table-contexts/table-view-provider.tsx b/packages/database-ui/src/table-view/table-contexts/table-view-provider.tsx
new file mode 100644
index 00000000..73fcd8d3
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-contexts/table-view-provider.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import React, { useMemo } from "react";
+import { arrayMove } from "@dnd-kit/sortable";
+
+import type { DatabaseProperty, RowDataType } from "../types";
+import {
+ TableActionsContext,
+ TableViewContext,
+ type TableActions,
+ type TableViewCtx,
+} from "./table-view-context";
+import { useTableView } from "./use-table-view";
+
+interface TableViewProviderProps extends React.PropsWithChildren {
+ initialData: {
+ properties: DatabaseProperty[];
+ data: RowDataType[];
+ };
+}
+
+export const TableViewProvider: React.FC = ({
+ children,
+ initialData,
+}) => {
+ const { dispatch, ...ctx } = useTableView(initialData);
+
+ const tableViewCtx = useMemo(
+ () => ({
+ ...ctx,
+ getColumn: (id: string) => ctx.properties[id] ?? null,
+ isPropertyUnique: (name: string) =>
+ Object.values(ctx.properties).every((p) => p.name !== name),
+ canFreezeProperty: (id: string) =>
+ ctx.table.getState().columnOrder.at(-1) !== id,
+ }),
+ [ctx],
+ );
+
+ const actions = useMemo(
+ () => ({
+ dispatch,
+ addColumn: (payload) => dispatch({ type: "add:col", payload }),
+ updateColumn: (id, data) =>
+ dispatch({ type: "update:col", payload: { id, data } }),
+ updateColumnType: (id, type) =>
+ dispatch({ type: "update:col:type", payload: { id, type } }),
+ toggleAllColumns: (hidden) =>
+ dispatch({ type: "update:col:visibility", payload: { hidden } }),
+ reorderColumns: (e) => {
+ // reorder columns after drag & drop
+ const { active, over } = e;
+ if (!over || active.id === over.id) return;
+ dispatch({
+ type: "reorder:col",
+ updater: (prev) => {
+ const oldIndex = prev.indexOf(active.id as string);
+ const newIndex = prev.indexOf(over.id as string);
+ return arrayMove(prev, oldIndex, newIndex); //this is just a splice util
+ },
+ });
+ },
+ duplicateColumn: (id) =>
+ dispatch({ type: "duplicate:col", payload: { id } }),
+ freezeColumns: (id) => dispatch({ type: "freeze:col", payload: { id } }),
+ deleteColumn: (id) => dispatch({ type: "delete:col", payload: { id } }),
+ }),
+ [dispatch],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/table-contexts/types.ts b/packages/database-ui/src/table-view/table-contexts/types.ts
new file mode 100644
index 00000000..854f0f7e
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-contexts/types.ts
@@ -0,0 +1,16 @@
+import { DatabaseProperty } from "../types";
+
+export type AddColumnPayload = Pick;
+
+export type UpdateColumnPayload = Partial<
+ Pick<
+ DatabaseProperty,
+ | "name"
+ | "icon"
+ | "description"
+ | "width"
+ | "wrapped"
+ | "hidden"
+ | "isDeleted"
+ >
+>;
diff --git a/packages/database-ui/src/table-view/table-contexts/use-table-view.tsx b/packages/database-ui/src/table-view/table-contexts/use-table-view.tsx
new file mode 100644
index 00000000..194c8529
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-contexts/use-table-view.tsx
@@ -0,0 +1,175 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+"use client";
+
+import { useMemo, useReducer } from "react";
+import {
+ KeyboardSensor,
+ MouseSensor,
+ TouchSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core";
+import {
+ ColumnPinningState,
+ getCoreRowModel,
+ useReactTable,
+ VisibilityState,
+ type ColumnDef,
+} from "@tanstack/react-table";
+
+import { TableHeaderCell } from "../table-header-cells";
+import { TableRowCell } from "../table-row-cells";
+import type { DatabaseProperty, RowDataType } from "../types";
+import { TableViewAtom, tableViewReducer } from "./table-reducer";
+
+export const useTableView = (initial: {
+ properties: DatabaseProperty[];
+ data: RowDataType[];
+}) => {
+ const [{ properties, propertiesOrder, freezedIndex, data }, dispatch] =
+ useReducer(tableViewReducer, {
+ ...initial.properties.reduce<
+ Omit
+ >(
+ (acc, col) => ({
+ properties: { ...acc.properties, [col.id]: col },
+ propertiesOrder: [...acc.propertiesOrder, col.id],
+ }),
+ { properties: {}, propertiesOrder: [] },
+ ),
+ freezedIndex: -1,
+ data: initial.data,
+ });
+
+ const columns = useMemo(
+ () =>
+ Object.values(properties).map>(
+ ({ id, ...property }) => ({
+ id,
+ accessorKey: property.name,
+ minSize: property.type === "checkbox" ? 32 : 100,
+ header: ({ header }) => (
+
+ dispatch({
+ type: "update:col",
+ payload: {
+ id,
+ data: { width: `${header.column.getSize()}px` },
+ },
+ }),
+ onTouchStart: header.getResizeHandler(),
+ onTouchEnd: () =>
+ dispatch({
+ type: "update:col",
+ payload: {
+ id,
+ data: { width: `${header.column.getSize()}px` },
+ },
+ }),
+ }}
+ />
+ ),
+ cell: ({ row, column }) => {
+ const cell = row.original.properties[id];
+ if (!cell) return null;
+ return (
+
+ dispatch({
+ type: "update:cell",
+ payload: {
+ rowId: row.original.id,
+ colId: id,
+ data: { id: cell.id, ...data },
+ },
+ })
+ }
+ />
+ );
+ },
+ }),
+ ),
+ [properties],
+ );
+
+ const columnVisibility = useMemo(
+ () =>
+ Object.values(properties).reduce(
+ (acc, col) => ({ ...acc, [col.id]: !col.hidden && !col.isDeleted }),
+ {},
+ ),
+ [properties],
+ );
+
+ const columnPinning = useMemo(
+ () => ({ left: propertiesOrder.slice(0, freezedIndex + 1) }),
+ [propertiesOrder, freezedIndex],
+ );
+
+ const table = useReactTable({
+ columns,
+ data,
+ defaultColumn: {
+ size: 200,
+ minSize: 100,
+ maxSize: Number.MAX_SAFE_INTEGER,
+ },
+ columnResizeMode: "onChange",
+ getCoreRowModel: getCoreRowModel(),
+ state: {
+ columnOrder: propertiesOrder,
+ columnVisibility,
+ columnPinning,
+ },
+ });
+
+ /**
+ * Instead of calling `column.getSize()` on every render for every header
+ * and especially every data cell (very expensive),
+ * we will calculate all column sizes at once at the root table level in a useMemo
+ * and pass the column sizes down as CSS variables to the element.
+ */
+ const columnSizeVars = useMemo(() => {
+ return table.getFlatHeaders().reduce>(
+ (sizes, header) => ({
+ ...sizes,
+ [`--header-${header.id}-size`]: header.getSize(),
+ [`--col-${header.column.id}-size`]: header.column.getSize(),
+ }),
+ {},
+ );
+ }, [
+ table.getFlatHeaders(),
+ table.getState().columnSizingInfo,
+ table.getState().columnSizing,
+ ]);
+
+ /** DND */
+ const columnSensors = useSensors(
+ useSensor(MouseSensor, {}),
+ useSensor(TouchSensor, {}),
+ useSensor(KeyboardSensor, {}),
+ );
+
+ return {
+ table,
+ columnSizeVars,
+ data,
+ properties,
+ dispatch,
+ /** DND */
+ columnSensors,
+ };
+};
diff --git a/packages/database-ui/src/table-view/table-header-cells.tsx b/packages/database-ui/src/table-view/table-header-cells.tsx
new file mode 100644
index 00000000..ee426b2b
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-header-cells.tsx
@@ -0,0 +1,193 @@
+"use client";
+
+import React, { forwardRef, useRef } from "react";
+import {
+ DraggableAttributes,
+ DraggableSyntheticListeners,
+} from "@dnd-kit/core";
+import { useSortable } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+
+import { cn } from "@swy/ui/lib";
+import { Hint, IconBlock } from "@swy/ui/shared";
+
+import { DefaultIcon } from "./default-icon";
+import * as Icon from "./icons";
+import { PropMenu, useMenuControl } from "./menus";
+import { useTableViewCtx } from "./table-contexts";
+
+interface TableHeaderCellProps {
+ id: string;
+ width: string;
+ isResizing?: boolean;
+ resizeHandle: {
+ /**
+ * @prop onMouseDown: resize for desktop
+ */
+ onMouseDown?: React.MouseEventHandler;
+ onMouseUp?: React.MouseEventHandler;
+ /**
+ * @prop onTouchStart: resize for mobile
+ */
+ onTouchStart?: React.TouchEventHandler;
+ onTouchEnd?: React.TouchEventHandler;
+ };
+}
+
+/**
+ * Table Header Cell
+ *
+ * @requires SortableContext
+ */
+export const TableHeaderCell: React.FC = ({
+ id,
+ width,
+ isResizing,
+ resizeHandle,
+}) => {
+ const { properties } = useTableViewCtx();
+ const { openPopover } = useMenuControl();
+
+ const property = properties[id]!;
+
+ const cellRef = useRef(null);
+ const openPropMenu = () => {
+ const rect = cellRef.current?.getBoundingClientRect();
+ openPopover(, {
+ x: rect?.x,
+ y: rect?.bottom,
+ className: "h-full max-h-[70vh] w-[220px]",
+ });
+ };
+
+ /** DND */
+ const {
+ attributes,
+ isDragging,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ } = useSortable({ id });
+
+ const style: React.CSSProperties = {
+ width,
+ opacity: isDragging ? 0.8 : 1,
+ zIndex: isDragging ? 10 : 0,
+ transform: CSS.Translate.toString(transform), // translate instead of transform to avoid squishing
+ transition, // Warning: it is somehow laggy
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {property.icon ? (
+
+ ) : (
+
+ )}
+
+ {/* Use this to sort columns */}
+
+
+
{property.name}
+ {property.description && (
+
+
+
+ )}
+
+
+
+
+ {/* Resize handle */}
+
+
+
+ );
+};
+
+interface DragHandleProps {
+ attributes: DraggableAttributes;
+ listeners: DraggableSyntheticListeners;
+}
+
+const DragHandle: React.FC = ({ attributes, listeners }) => {
+ return (
+
+ );
+};
+
+interface ActionCellProps {
+ icon: React.ReactNode;
+ onClick?: () => void;
+}
+
+export const ActionCell = forwardRef(
+ ({ icon, onClick }, ref) => {
+ return (
+
+ );
+ },
+);
+
+ActionCell.displayName = "ActionCell";
diff --git a/packages/database-ui/src/table-view/table-header-row.tsx b/packages/database-ui/src/table-view/table-header-row.tsx
new file mode 100644
index 00000000..84050ad1
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-header-row.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import React, { useRef } from "react";
+
+import "./view.css";
+
+import {
+ horizontalListSortingStrategy,
+ SortableContext,
+} from "@dnd-kit/sortable";
+import {
+ flexRender,
+ type ColumnOrderState,
+ type Header,
+} from "@tanstack/react-table";
+
+import { cn } from "@swy/ui/lib";
+
+import * as Icon from "./icons";
+import { PropsMenu, TypesMenu, useMenuControl } from "./menus";
+import { ActionCell } from "./table-header-cells";
+import type { RowDataType } from "./types";
+
+interface TableHeaderRowProps {
+ leftPinnedHeaders: Header[];
+ headers: Header[];
+ columnOrder: ColumnOrderState;
+}
+
+export const TableHeaderRow: React.FC = ({
+ leftPinnedHeaders,
+ headers,
+ columnOrder,
+}) => {
+ const { openPopover } = useMenuControl();
+
+ const isLeftPinned = leftPinnedHeaders.length > 0;
+
+ const plusButtonRef = useRef(null);
+ const openTypesMenu = () => {
+ const rect = plusButtonRef.current?.getBoundingClientRect();
+ openPopover(, {
+ x: rect?.left,
+ y: rect ? rect.top + rect.height : 0,
+ });
+ };
+ const openPropsMenu = () => openPopover(, { x: -12, y: -12 });
+
+ return (
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/table-row-cells.tsx b/packages/database-ui/src/table-view/table-row-cells.tsx
new file mode 100644
index 00000000..f539539c
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-row-cells.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import React, { useState } from "react";
+
+import "./view.css";
+
+import { CheckboxCell, TextCell, TitleCell } from "./cells";
+import { CellType } from "./types";
+
+enum CellMode {
+ Normal = "normal",
+ Edit = "edit",
+ Select = "select",
+}
+
+interface DataCellProps {
+ data: CellType;
+ wrapped?: boolean;
+ onChange?: (data: CellType) => void;
+}
+
+interface TableRowCellProps extends DataCellProps {
+ rowId: number;
+ colId: number;
+ width?: string;
+}
+
+export const TableRowCell: React.FC = ({
+ data,
+ rowId,
+ colId,
+ width,
+ wrapped,
+ onChange,
+}) => {
+ const [mode] = useState(CellMode.Normal);
+
+ return (
+
+
+
+
+ {mode === CellMode.Select && (
+
+ )}
+
+ );
+};
+
+const DataCell: React.FC = ({ data, wrapped, onChange }) => {
+ switch (data.type) {
+ case "title":
+ return (
+ onChange?.({ type: "title", value })}
+ />
+ );
+ case "text":
+ return (
+ onChange?.({ type: "text", value })}
+ />
+ );
+ case "checkbox":
+ return (
+ onChange?.({ type: "checkbox", checked })}
+ />
+ );
+ default:
+ return null;
+ }
+};
diff --git a/packages/database-ui/src/table-view/table-row.tsx b/packages/database-ui/src/table-view/table-row.tsx
new file mode 100644
index 00000000..b85afa2c
--- /dev/null
+++ b/packages/database-ui/src/table-view/table-row.tsx
@@ -0,0 +1,62 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import React from "react";
+import { flexRender, Row } from "@tanstack/react-table";
+
+import type { RowDataType } from "./types";
+
+interface TableRowProps {
+ row: Row;
+}
+
+export const TableRow: React.FC = ({ row }) => {
+ return (
+
+
+
+ {/* Left pinned columns */}
+
+ {/* pinned: wrap another div (is this needed?) */}
+ {/* div: flex opacity-100 transition-duration: 200ms; transition-timing-function: ease; transition-property: opacity; */}
+ {/* Hover checkbox */}
+
+ {/* TODO: pinned columns in the wrapped div */}
+ {row.getLeftVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ {/* Center columns */}
+ {row.getCenterVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/packages/database-ui/src/table-view/types.ts b/packages/database-ui/src/table-view/types.ts
new file mode 100644
index 00000000..f16f9022
--- /dev/null
+++ b/packages/database-ui/src/table-view/types.ts
@@ -0,0 +1,42 @@
+import type { IconInfo } from "@swy/ui/shared";
+
+export interface Option {
+ id: string;
+ name: string;
+ color: string;
+}
+
+export type CellType =
+ | { type: "title" | "text"; value: string }
+ | { type: "checkbox"; checked: boolean }
+ | { type: "select"; select: Option | null };
+export type PropertyType = CellType["type"];
+
+export type CellDataType = {
+ id: string; // cell id
+} & CellType;
+
+export interface RowDataType {
+ id: string; // row id (page id)
+ /**
+ * @param key: column id
+ * @param value: cell data
+ */
+ properties: Record;
+}
+
+export type HeaderCellType =
+ | { type: "title" | "text" | "checkbox" }
+ | { type: "select"; options: Option[] };
+
+export interface DatabaseProperty {
+ id: string;
+ type: PropertyType;
+ name: string;
+ icon?: IconInfo | null;
+ width?: string;
+ description?: string;
+ wrapped?: boolean;
+ hidden?: boolean;
+ isDeleted?: boolean;
+}
diff --git a/packages/database-ui/src/table-view/utils.ts b/packages/database-ui/src/table-view/utils.ts
new file mode 100644
index 00000000..ef3b317d
--- /dev/null
+++ b/packages/database-ui/src/table-view/utils.ts
@@ -0,0 +1,70 @@
+import { v4 } from "uuid";
+
+import type { CellDataType, CellType, Option, PropertyType } from "./types";
+
+export function getDefaultCell(type: PropertyType): CellDataType {
+ switch (type) {
+ case "checkbox":
+ return { type, id: v4(), checked: false };
+ case "select":
+ return { type, id: v4(), select: null };
+ default:
+ return { type, id: v4(), value: "" };
+ }
+}
+
+export function getUniqueName(name: string, names: string[]) {
+ const namesSet = new Set(names);
+ let uniqueName = name;
+ let suffix = 1;
+
+ while (namesSet.has(uniqueName)) {
+ uniqueName = `${name} ${suffix}`;
+ suffix++;
+ }
+ return uniqueName;
+}
+
+export function transferPropertyValues(
+ src: CellDataType,
+ dest: PropertyType,
+): CellDataType {
+ switch (dest) {
+ case "title":
+ case "text":
+ return { type: dest, id: src.id, value: toTextValue(src) };
+ case "checkbox":
+ return { type: dest, id: src.id, checked: toCheckboxValue(src) };
+ case "select":
+ return { type: dest, id: src.id, select: toSelectValue(src) };
+ }
+}
+
+function toTextValue(src: CellType): string {
+ switch (src.type) {
+ case "text":
+ return src.value;
+ case "select":
+ return src.select?.name ?? "";
+ default:
+ return "";
+ }
+}
+
+function toCheckboxValue(src: CellType): boolean {
+ switch (src.type) {
+ case "checkbox":
+ return src.checked;
+ default:
+ return false;
+ }
+}
+
+function toSelectValue(src: CellType): Option | null {
+ switch (src.type) {
+ case "select":
+ return src.select;
+ default:
+ return null;
+ }
+}
diff --git a/packages/database-ui/src/table-view/view.css b/packages/database-ui/src/table-view/view.css
new file mode 100644
index 00000000..e2b1362a
--- /dev/null
+++ b/packages/database-ui/src/table-view/view.css
@@ -0,0 +1,72 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ /* undefined */
+ --sticky-horizontal-offset: 0px;
+}
+
+@layer utilities {
+ .cursor-webkit-grab {
+ cursor: -webkit-grab;
+ }
+ .word-break {
+ word-break: break-word;
+ }
+ /* used in specific component */
+ .shadow-header-row {
+ box-shadow:
+ white -3px 0px 0px,
+ rgb(233, 233, 231) 0px -1px 0px inset;
+ }
+ .shadow-header-row-dark {
+ box-shadow:
+ rgb(25, 25, 25) -3px 0px 0px,
+ rgb(47, 47, 47) 0px -1px 0px inset;
+ }
+ /* made by myself */
+ .shadow-header-sticky {
+ box-shadow:
+ white -3px 0px 0px,
+ rgb(233, 233, 231) -1px -1px 0px inset;
+ }
+ /* made by myself */
+ .shadow-header-sticky-dark {
+ box-shadow:
+ rgb(25, 25, 25) -3px 0px 0px,
+ rgb(47, 47, 47) -1px -1px 0px inset;
+ }
+ .shadow-cell {
+ box-shadow:
+ rgba(35, 131, 226, 0.57) 0px 0px 0px 2px inset,
+ rgba(35, 131, 226, 0.35) 0px 0px 0px 1px inset;
+ }
+ .cell-open {
+ font-family:
+ ui-sans-serif,
+ -apple-system,
+ BlinkMacSystemFont,
+ Segoe UI Variable Display,
+ Segoe UI,
+ Helvetica,
+ Apple Color Emoji,
+ Arial,
+ sans-serif,
+ Segoe UI Emoji,
+ Segoe UI Symbol;
+ box-shadow:
+ rgba(15, 15, 15, 0.1) 0px 0px 0px 1px,
+ rgba(15, 15, 15, 0.1) 0px 2px 4px;
+ }
+ .title-cell-bg-img {
+ background-image: linear-gradient(
+ right,
+ rgba(55, 53, 47, 0.16) 0%,
+ rgba(55, 53, 47, 0.16) 100%
+ );
+ background-repeat: repeat-x;
+ background-position: 0px 100%;
+ background-size: 100% 1px;
+ }
+}
diff --git a/packages/database-ui/src/table-view/view.tsx b/packages/database-ui/src/table-view/view.tsx
new file mode 100644
index 00000000..3bb903a5
--- /dev/null
+++ b/packages/database-ui/src/table-view/view.tsx
@@ -0,0 +1,178 @@
+"use client";
+
+import React from "react";
+import { closestCenter, DndContext } from "@dnd-kit/core";
+import {
+ restrictToHorizontalAxis,
+ restrictToParentElement,
+} from "@dnd-kit/modifiers";
+
+import { paddingX } from "../database/constant";
+import { cols, mockData } from "./__mock__";
+import * as Icon from "./icons";
+import { MenuControlProvider } from "./menus";
+import { MemoizedTableBody, TableBody } from "./table-body";
+import {
+ TableViewProvider,
+ useTableActions,
+ useTableViewCtx,
+} from "./table-contexts";
+import { TableHeaderRow } from "./table-header-row";
+import type { DatabaseProperty, RowDataType } from "./types";
+
+interface TableViewProps {
+ properties?: DatabaseProperty[];
+ data?: RowDataType[];
+}
+
+export const TableView: React.FC = ({
+ properties = cols,
+ data = mockData,
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+const TableViewContent = () => {
+ const { table, columnSizeVars, columnSensors } = useTableViewCtx();
+ const { reorderColumns } = useTableActions();
+
+ const leftPinnedHeaders = table.getLeftLeafHeaders();
+ const headers = table.getCenterLeafHeaders();
+
+ return (
+
+
+
+
+ {/* Header row */}
+
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ ))}
+
+
+
+
+
+
+ {/* Table body */}
+
+ {/* Drag and Fill handle */}
+
+
+ {/* The blue circle */}
+ {/*
*/}
+
+
+ {/* ??? */}
+
+ {/* Rows */}
+ {table.getState().columnSizingInfo.isResizingColumn ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ New page
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/database-ui/src/theme/index.ts b/packages/database-ui/src/theme/index.ts
new file mode 100644
index 00000000..bb2b82b2
--- /dev/null
+++ b/packages/database-ui/src/theme/index.ts
@@ -0,0 +1,2 @@
+export * from "./theme-provider";
+export * from "./theme-toggle";
diff --git a/packages/database-ui/src/theme/theme-provider.tsx b/packages/database-ui/src/theme/theme-provider.tsx
new file mode 100644
index 00000000..0fc1b2f5
--- /dev/null
+++ b/packages/database-ui/src/theme/theme-provider.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import { createContext, useContext, useEffect, useMemo, useState } from "react";
+
+type Theme = "dark" | "light" | "system";
+
+interface ThemeProviderProps {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+}
+
+interface ThemeProviderState {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+}
+
+const initialState: ThemeProviderState = {
+ theme: "system",
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "vite-ui-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (localStorage.getItem(storageKey) ?? defaultTheme) as Theme,
+ );
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove("light", "dark");
+
+ if (theme === "system") {
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = useMemo(
+ () => ({
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ }),
+ [theme, storageKey],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (context === undefined)
+ throw new Error("useTheme must be used within a ThemeProvider");
+
+ return context;
+};
diff --git a/packages/database-ui/src/theme/theme-toggle.tsx b/packages/database-ui/src/theme/theme-toggle.tsx
new file mode 100644
index 00000000..b38dfc19
--- /dev/null
+++ b/packages/database-ui/src/theme/theme-toggle.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { Moon, Sun } from "lucide-react";
+
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@swy/ui/shadcn";
+
+import { useTheme } from "./theme-provider";
+
+export function ThemeToggle() {
+ const { setTheme } = useTheme();
+
+ return (
+
+
+
+
+
+ setTheme("light")}>
+ Light
+
+ setTheme("dark")}>
+ Dark
+
+ setTheme("system")}>
+ System
+
+
+
+ );
+}
diff --git a/packages/database-ui/src/vite-env.d.ts b/packages/database-ui/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/packages/database-ui/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/packages/database-ui/tailwind.config.ts b/packages/database-ui/tailwind.config.ts
new file mode 100644
index 00000000..173f63c1
--- /dev/null
+++ b/packages/database-ui/tailwind.config.ts
@@ -0,0 +1,20 @@
+import type { Config } from "tailwindcss";
+import { fontFamily } from "tailwindcss/defaultTheme";
+
+import baseConfig from "@swy/tailwind-config";
+
+const config = {
+ darkMode: ["class"],
+ content: [...baseConfig.content, "../ui/src/**/*.{ts,tsx}"],
+ presets: [baseConfig],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ["var(--font-geist-sans)", ...fontFamily.sans],
+ mono: ["var(--font-geist-mono)", ...fontFamily.mono],
+ },
+ },
+ },
+} satisfies Config;
+
+export default config;
diff --git a/packages/database-ui/tsconfig.json b/packages/database-ui/tsconfig.json
new file mode 100644
index 00000000..ca9d92b2
--- /dev/null
+++ b/packages/database-ui/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "@swy/tsconfig/internal-package.json",
+ "compilerOptions": {
+ "module": "esnext",
+ "lib": ["ES2022", "dom", "dom.iterable"],
+ "baseUrl": ".",
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e9974cd3..c37e8f16 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -589,6 +589,88 @@ importers:
specifier: 'catalog:'
version: 5.6.3
+ packages/database-ui:
+ dependencies:
+ '@dnd-kit/core':
+ specifier: ^6.3.1
+ version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@dnd-kit/modifiers':
+ specifier: ^9.0.0
+ version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
+ '@dnd-kit/sortable':
+ specifier: ^10.0.0
+ version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
+ '@swy/ui':
+ specifier: workspace:*
+ version: link:../ui
+ '@tanstack/react-table':
+ specifier: catalog:ui
+ version: 8.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ lucide-react:
+ specifier: catalog:ui
+ version: 0.456.0(react@18.3.1)
+ react:
+ specifier: catalog:react18
+ version: 18.3.1
+ react-dom:
+ specifier: catalog:react18
+ version: 18.3.1(react@18.3.1)
+ usehooks-ts:
+ specifier: ^2.9.1
+ version: 2.16.0(react@18.3.1)
+ uuid:
+ specifier: catalog:uuid
+ version: 10.0.0
+ zod:
+ specifier: 'catalog:'
+ version: 3.23.8
+ devDependencies:
+ '@swy/eslint-config':
+ specifier: workspace:*
+ version: link:../../tooling/eslint
+ '@swy/prettier-config':
+ specifier: workspace:*
+ version: link:../../tooling/prettier
+ '@swy/tailwind-config':
+ specifier: workspace:*
+ version: link:../../tooling/tailwind
+ '@swy/tsconfig':
+ specifier: workspace:*
+ version: link:../../tooling/typescript
+ '@types/node':
+ specifier: catalog:node22
+ version: 22.13.10
+ '@types/react':
+ specifier: catalog:react18
+ version: 18.3.11
+ '@types/react-dom':
+ specifier: catalog:react18
+ version: 18.3.1
+ '@types/uuid':
+ specifier: catalog:uuid
+ version: 10.0.0
+ eslint:
+ specifier: 'catalog:'
+ version: 9.13.0(jiti@2.4.0)
+ globals:
+ specifier: ^15.12.0
+ version: 15.15.0
+ prettier:
+ specifier: 'catalog:'
+ version: 3.3.3
+ sonner:
+ specifier: catalog:ui
+ version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ tailwindcss:
+ specifier: 'catalog:'
+ version: 3.4.17(ts-node@10.9.2(@types/node@22.13.10)(typescript@5.6.3))
+ typescript:
+ specifier: 'catalog:'
+ version: 5.6.3
+ vite:
+ specifier: ^6.0.1
+ version: 6.0.3(@types/node@22.13.10)(jiti@2.4.0)(terser@5.36.0)(yaml@2.6.0)
+
packages/db:
dependencies:
'@vercel/postgres':
@@ -2112,6 +2194,34 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
+ '@dnd-kit/accessibility@3.1.1':
+ resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@dnd-kit/core@6.3.1':
+ resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@dnd-kit/modifiers@9.0.0':
+ resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==}
+ peerDependencies:
+ '@dnd-kit/core': ^6.3.0
+ react: '>=16.8.0'
+
+ '@dnd-kit/sortable@10.0.0':
+ resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
+ peerDependencies:
+ '@dnd-kit/core': ^6.3.0
+ react: '>=16.8.0'
+
+ '@dnd-kit/utilities@3.2.2':
+ resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
+ peerDependencies:
+ react: '>=16.8.0'
+
'@drizzle-team/brocli@0.10.1':
resolution: {integrity: sha512-AHy0vjc+n/4w/8Mif+w86qpppHuF3AyXbcWW+R/W7GNA3F5/p2nuhlkCJaTXSLZheB4l1rtHzOfr9A7NwoR/Zg==}
@@ -7543,6 +7653,10 @@ packages:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
+ globals@15.15.0:
+ resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
+ engines: {node: '>=18'}
+
globalthis@1.0.4:
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
engines: {node: '>= 0.4'}
@@ -13007,6 +13121,38 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
+ '@dnd-kit/accessibility@3.1.1(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ tslib: 2.8.0
+
+ '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@dnd-kit/accessibility': 3.1.1(react@18.3.1)
+ '@dnd-kit/utilities': 3.2.2(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ tslib: 2.8.0
+
+ '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@dnd-kit/utilities': 3.2.2(react@18.3.1)
+ react: 18.3.1
+ tslib: 2.8.0
+
+ '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@dnd-kit/utilities': 3.2.2(react@18.3.1)
+ react: 18.3.1
+ tslib: 2.8.0
+
+ '@dnd-kit/utilities@3.2.2(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ tslib: 2.8.0
+
'@drizzle-team/brocli@0.10.1': {}
'@edgestore/react@0.1.7(next@14.2.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.23.8)':
@@ -18869,6 +19015,8 @@ snapshots:
globals@14.0.0: {}
+ globals@15.15.0: {}
+
globalthis@1.0.4:
dependencies:
define-properties: 1.2.1