diff --git a/apps/playground/eslint.config.js b/apps/playground/eslint.config.js
new file mode 100644
index 00000000..fe3ef4a9
--- /dev/null
+++ b/apps/playground/eslint.config.js
@@ -0,0 +1,14 @@
+import baseConfig, { restrictEnvAccess } from "@swy/eslint-config/base";
+import nextjsConfig from "@swy/eslint-config/nextjs";
+import reactConfig from "@swy/eslint-config/react";
+
+/** @type {import('typescript-eslint').Config} */
+export default [
+ {
+ ignores: [".next/**"],
+ },
+ ...baseConfig,
+ ...reactConfig,
+ ...nextjsConfig,
+ ...restrictEnvAccess,
+];
diff --git a/apps/playground/next-env.d.ts b/apps/playground/next-env.d.ts
new file mode 100644
index 00000000..40c3d680
--- /dev/null
+++ b/apps/playground/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/apps/playground/next.config.js b/apps/playground/next.config.js
new file mode 100644
index 00000000..3f4065ce
--- /dev/null
+++ b/apps/playground/next.config.js
@@ -0,0 +1,57 @@
+import { fileURLToPath } from "url";
+import { createJiti } from "jiti";
+
+// Import env files to validate at build time. Use jiti so we can load .ts files in here.
+await createJiti(fileURLToPath(import.meta.url))("./src/env");
+
+/** @type {import("next").NextConfig} */
+const config = {
+ reactStrictMode: true,
+
+ /** Enables hot reloading for local packages without a build step */
+ transpilePackages: [
+ "@swy/liveblocks",
+ "@swy/notion",
+ "@swy/ui",
+ "@swy/validators",
+ "lucide-react",
+ ],
+ experimental: {
+ optimizePackageImports: ["lucide-react"],
+ },
+
+ /** We already do linting and typechecking as separate tasks in CI */
+ eslint: { ignoreDuringBuilds: true },
+ typescript: { ignoreBuildErrors: true },
+ /** */
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "img.clerk.com",
+ },
+ {
+ protocol: "https",
+ hostname: "images.unsplash.com",
+ },
+ {
+ protocol: "https",
+ hostname: "files.edgestore.dev",
+ },
+ {
+ protocol: "https",
+ hostname: "www.notion.so",
+ },
+ {
+ protocol: "https",
+ hostname: "upload.wikimedia.org",
+ },
+ {
+ protocol: "https",
+ hostname: "avatars.githubusercontent.com",
+ },
+ ],
+ },
+};
+
+export default config;
diff --git a/apps/playground/package.json b/apps/playground/package.json
new file mode 100644
index 00000000..180d0e13
--- /dev/null
+++ b/apps/playground/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "@swy/playground",
+ "version": "1.4.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "pnpm with-env next build",
+ "clean": "git clean -xdf .next .turbo node_modules",
+ "dev": "pnpm with-env next dev -p 3002",
+ "format": "prettier --check . --ignore-path ../../.gitignore",
+ "lint": "eslint",
+ "start": "pnpm with-env next start",
+ "typecheck": "tsc --noEmit",
+ "with-env": "dotenv -e ../../.env --"
+ },
+ "dependencies": {
+ "@swy/i18n": "workspace:*",
+ "@swy/liveblocks": "workspace:*",
+ "@swy/notion": "workspace:*",
+ "@swy/ui": "workspace:*",
+ "@swy/validators": "workspace:*",
+ "@t3-oss/env-nextjs": "catalog:env",
+ "lucide-react": "catalog:ui",
+ "next": "catalog:next14",
+ "react": "catalog:react18",
+ "react-dom": "catalog:react18",
+ "react-textarea-autosize": "^8.5.3",
+ "sonner": "catalog:ui",
+ "swr": "^2.2.5",
+ "yjs": "^13.6.15",
+ "zod": "catalog:"
+ },
+ "devDependencies": {
+ "@swy/eslint-config": "workspace:*",
+ "@swy/prettier-config": "workspace:*",
+ "@swy/tailwind-config": "workspace:*",
+ "@swy/tsconfig": "workspace:*",
+ "@types/node": "catalog:node20",
+ "@types/react": "catalog:react18",
+ "@types/react-dom": "catalog:react18",
+ "dotenv-cli": "catalog:env",
+ "eslint": "catalog:",
+ "jiti": "^2.4.0",
+ "prettier": "catalog:",
+ "tailwindcss": "catalog:",
+ "typescript": "catalog:"
+ },
+ "prettier": "@swy/prettier-config"
+}
diff --git a/apps/playground/postcss.config.cjs b/apps/playground/postcss.config.cjs
new file mode 100644
index 00000000..ee5f90b3
--- /dev/null
+++ b/apps/playground/postcss.config.cjs
@@ -0,0 +1,5 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ },
+};
diff --git a/apps/playground/public/documents-dark.png b/apps/playground/public/documents-dark.png
new file mode 100644
index 00000000..db6193c0
Binary files /dev/null and b/apps/playground/public/documents-dark.png differ
diff --git a/apps/playground/public/documents.png b/apps/playground/public/documents.png
new file mode 100644
index 00000000..4e4518e1
Binary files /dev/null and b/apps/playground/public/documents.png differ
diff --git a/apps/playground/public/empty-dark.png b/apps/playground/public/empty-dark.png
new file mode 100644
index 00000000..6349df5c
Binary files /dev/null and b/apps/playground/public/empty-dark.png differ
diff --git a/apps/playground/public/empty.png b/apps/playground/public/empty.png
new file mode 100644
index 00000000..3875ded0
Binary files /dev/null and b/apps/playground/public/empty.png differ
diff --git a/apps/playground/public/error-dark.png b/apps/playground/public/error-dark.png
new file mode 100644
index 00000000..134a35ff
Binary files /dev/null and b/apps/playground/public/error-dark.png differ
diff --git a/apps/playground/public/error.png b/apps/playground/public/error.png
new file mode 100644
index 00000000..50f4495f
Binary files /dev/null and b/apps/playground/public/error.png differ
diff --git a/apps/playground/public/favicon.ico b/apps/playground/public/favicon.ico
new file mode 100644
index 00000000..f0058b40
Binary files /dev/null and b/apps/playground/public/favicon.ico differ
diff --git a/apps/playground/public/next.svg b/apps/playground/public/next.svg
new file mode 100644
index 00000000..5174b28c
--- /dev/null
+++ b/apps/playground/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/playground/public/notion-dark.svg b/apps/playground/public/notion-dark.svg
new file mode 100644
index 00000000..b3fa4afc
--- /dev/null
+++ b/apps/playground/public/notion-dark.svg
@@ -0,0 +1,4 @@
+
diff --git a/apps/playground/public/notion.svg b/apps/playground/public/notion.svg
new file mode 100644
index 00000000..bf6442f7
--- /dev/null
+++ b/apps/playground/public/notion.svg
@@ -0,0 +1,4 @@
+
diff --git a/apps/playground/public/reading-dark.png b/apps/playground/public/reading-dark.png
new file mode 100644
index 00000000..c3871589
Binary files /dev/null and b/apps/playground/public/reading-dark.png differ
diff --git a/apps/playground/public/reading.png b/apps/playground/public/reading.png
new file mode 100644
index 00000000..826b7e9a
Binary files /dev/null and b/apps/playground/public/reading.png differ
diff --git a/apps/playground/public/t3-icon.svg b/apps/playground/public/t3-icon.svg
new file mode 100644
index 00000000..e377165f
--- /dev/null
+++ b/apps/playground/public/t3-icon.svg
@@ -0,0 +1,13 @@
+
diff --git a/apps/playground/public/vercel.svg b/apps/playground/public/vercel.svg
new file mode 100644
index 00000000..d2f84222
--- /dev/null
+++ b/apps/playground/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/playground/src/app/(marketing)/_components/footer.tsx b/apps/playground/src/app/(marketing)/_components/footer.tsx
new file mode 100644
index 00000000..a7b2101d
--- /dev/null
+++ b/apps/playground/src/app/(marketing)/_components/footer.tsx
@@ -0,0 +1,19 @@
+import { Button } from "@swy/ui/shadcn";
+
+import Logo from "./logo";
+
+export default function Footer() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/playground/src/app/(marketing)/_components/heading.tsx b/apps/playground/src/app/(marketing)/_components/heading.tsx
new file mode 100644
index 00000000..9a99175d
--- /dev/null
+++ b/apps/playground/src/app/(marketing)/_components/heading.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import Link from "next/link";
+import { ArrowRight } from "lucide-react";
+
+import { Button } from "@swy/ui/shadcn";
+
+const Heading = () => {
+ return (
+
+
+ Your Ideas , Documents, & Plans. Unified. Welcome to{" "}
+ Steeeee WorXpace
+
+
+ Steeeee WorXpace is the connected workspace where
+ better, faster work happens.
+
+
+
+
+
+ );
+};
+
+export default Heading;
diff --git a/apps/playground/src/app/(marketing)/_components/heroes.tsx b/apps/playground/src/app/(marketing)/_components/heroes.tsx
new file mode 100644
index 00000000..2d918374
--- /dev/null
+++ b/apps/playground/src/app/(marketing)/_components/heroes.tsx
@@ -0,0 +1,38 @@
+import Image from "next/image";
+
+export default function Heroes() {
+ return (
+
+ );
+}
diff --git a/apps/playground/src/app/(marketing)/_components/index.ts b/apps/playground/src/app/(marketing)/_components/index.ts
new file mode 100644
index 00000000..57bfa331
--- /dev/null
+++ b/apps/playground/src/app/(marketing)/_components/index.ts
@@ -0,0 +1,4 @@
+export { default as Heading } from "./heading";
+export { default as Heroes } from "./heroes";
+export { default as Footer } from "./footer";
+export { default as Navbar } from "./navbar";
diff --git a/apps/playground/src/app/(marketing)/_components/logo.tsx b/apps/playground/src/app/(marketing)/_components/logo.tsx
new file mode 100644
index 00000000..e2cd9d0c
--- /dev/null
+++ b/apps/playground/src/app/(marketing)/_components/logo.tsx
@@ -0,0 +1,31 @@
+import { Poppins } from "next/font/google";
+import Image from "next/image";
+
+import { cn } from "@swy/ui/lib";
+
+const font = Poppins({
+ subsets: ["latin"],
+ weight: ["400", "600"],
+});
+
+export default function Logo() {
+ return (
+
+ );
+}
diff --git a/apps/playground/src/app/(marketing)/_components/navbar.tsx b/apps/playground/src/app/(marketing)/_components/navbar.tsx
new file mode 100644
index 00000000..cbab21dc
--- /dev/null
+++ b/apps/playground/src/app/(marketing)/_components/navbar.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { useScrollTop } from "@swy/ui/hooks";
+import { cn } from "@swy/ui/lib";
+import { ThemeToggle } from "@swy/ui/shadcn";
+
+import Logo from "./logo";
+
+export default function Navbar() {
+ const scrolled = useScrollTop();
+
+ return (
+
+ );
+}
diff --git a/apps/playground/src/app/(marketing)/layout.tsx b/apps/playground/src/app/(marketing)/layout.tsx
new file mode 100644
index 00000000..e65a7f49
--- /dev/null
+++ b/apps/playground/src/app/(marketing)/layout.tsx
@@ -0,0 +1,12 @@
+import { PropsWithChildren } from "react";
+
+import { Navbar } from "./_components";
+
+export default function MarketingLayout({ children }: PropsWithChildren) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/apps/playground/src/app/(marketing)/page.tsx b/apps/playground/src/app/(marketing)/page.tsx
new file mode 100644
index 00000000..878fae34
--- /dev/null
+++ b/apps/playground/src/app/(marketing)/page.tsx
@@ -0,0 +1,15 @@
+import { Footer, Heading, Heroes } from "./_components";
+
+const Marketing = () => {
+ return (
+
+ );
+};
+
+export default Marketing;
diff --git a/apps/playground/src/app/(platform)/(auth)/sign-in/_components/sign-in-button.tsx b/apps/playground/src/app/(platform)/(auth)/sign-in/_components/sign-in-button.tsx
new file mode 100644
index 00000000..e0c02b66
--- /dev/null
+++ b/apps/playground/src/app/(platform)/(auth)/sign-in/_components/sign-in-button.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import Image from "next/image";
+
+import { Button } from "@swy/ui/shadcn";
+
+interface SignInButtonProps {
+ name: string;
+ avatarUrl: string;
+}
+
+export const SignInButton: React.FC = ({
+ name,
+ avatarUrl,
+}) => {
+ return (
+
+ );
+};
diff --git a/apps/playground/src/app/(platform)/(auth)/sign-in/page.tsx b/apps/playground/src/app/(platform)/(auth)/sign-in/page.tsx
new file mode 100644
index 00000000..cc7cbd71
--- /dev/null
+++ b/apps/playground/src/app/(platform)/(auth)/sign-in/page.tsx
@@ -0,0 +1,25 @@
+import { IconBlock } from "@swy/ui/shared";
+
+import { accounts } from "~/db/accounts";
+import { SignInButton } from "./_components/sign-in-button";
+
+export default function Page() {
+ return (
+
+
+
+
+ Sign in with ...
+
+
+ {Object.values(accounts).map(({ name, avatarUrl }) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/playground/src/app/(platform)/layout.tsx b/apps/playground/src/app/(platform)/layout.tsx
new file mode 100644
index 00000000..12c23dc3
--- /dev/null
+++ b/apps/playground/src/app/(platform)/layout.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import React, { useEffect } from "react";
+
+import { mockDB } from "~/db";
+import { useMockDB } from "~/hooks";
+
+export default function Layout({ children }: React.PropsWithChildren) {
+ const { update } = useMockDB();
+
+ useEffect(() => {
+ update(mockDB);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+ {children}
+ );
+}
diff --git a/apps/playground/src/app/globals.css b/apps/playground/src/app/globals.css
new file mode 100644
index 00000000..caa1c9ff
--- /dev/null
+++ b/apps/playground/src/app/globals.css
@@ -0,0 +1,98 @@
+@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;
+
+ /* 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;
+
+ /* 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/apps/playground/src/app/layout.tsx b/apps/playground/src/app/layout.tsx
new file mode 100644
index 00000000..7b4e5958
--- /dev/null
+++ b/apps/playground/src/app/layout.tsx
@@ -0,0 +1,63 @@
+import type { Metadata, Viewport } from "next";
+import { Inter } from "next/font/google";
+
+import { ThemeProvider } from "@swy/ui/shadcn";
+
+import { env } from "~/env";
+
+import "~/app/globals.css";
+
+const inter = Inter({ subsets: ["latin", "latin-ext"] });
+
+export const metadata: Metadata = {
+ metadataBase: new URL(
+ env.VERCEL_ENV === "production"
+ ? "https://playground.steeeee0223.vercel.app"
+ : `http://localhost:${env.PORT}`,
+ ),
+ title: "Steeeee WorXpace",
+ description: "The all-in-1 workspace where better, faster work happens",
+ icons: {
+ icon: [
+ {
+ media: "(prefers-color-scheme: light)",
+ url: "/notion.svg",
+ href: "/notion.svg",
+ },
+ {
+ media: "(prefers-color-scheme: dark)",
+ url: "/notion-dark.svg",
+ href: "/notion-dark.svg",
+ },
+ ],
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "white" },
+ { media: "(prefers-color-scheme: dark)", color: "black" },
+ ],
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/apps/playground/src/components/index.ts b/apps/playground/src/components/index.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/playground/src/db/accounts.ts b/apps/playground/src/db/accounts.ts
new file mode 100644
index 00000000..93c279fe
--- /dev/null
+++ b/apps/playground/src/db/accounts.ts
@@ -0,0 +1,47 @@
+import { AccountModel } from "./types";
+
+export enum _USER {
+ U1 = "a97f4e50-0b72-44f4-a95a-aba319534af5",
+ U2 = "22bd2a9e-bbee-4f3f-9c78-6a430a2c29b2",
+ U3 = "83e2f249-f02c-4971-b8fc-cda5cf71dd77",
+}
+
+export const accounts: Record = {
+ [_USER.U1]: {
+ id: _USER.U1,
+ name: "Steve Yu",
+ clerkId: "user_2",
+ email: "steve-yu@example.com",
+ avatarUrl:
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2d/Go_gopher_favicon.svg/1200px-Go_gopher_favicon.svg.png",
+ preferredName: "Steve",
+ updatedAt: Date.UTC(2024, 1, 1),
+ },
+ [_USER.U2]: {
+ id: _USER.U2,
+ name: "Chien Pong",
+ clerkId: "user_3",
+ email: "pong123@example.com",
+ avatarUrl: "https://avatars.githubusercontent.com/u/91364382",
+ preferredName: "Pong",
+ updatedAt: Date.UTC(2024, 1, 1),
+ },
+ [_USER.U3]: {
+ id: _USER.U3,
+ name: "Chia Ming",
+ clerkId: "user_4",
+ email: "chiaming@example.com",
+ avatarUrl: "https://avatars.githubusercontent.com/u/15240773",
+ preferredName: "Mark",
+ updatedAt: Date.UTC(2024, 1, 1),
+ },
+ // "45d7e133-d354-47c2-881b-441b7f95a327": {
+ // id: "45d7e133-d354-47c2-881b-441b7f95a327",
+ // name: "John Wick",
+ // clerkId: "user_1",
+ // email: "john-wick@example.com",
+ // avatarUrl: "",
+ // preferredName: "John Wick",
+ // updatedAt: Date.UTC(2024, 1, 1),
+ // },
+};
diff --git a/apps/playground/src/db/index.ts b/apps/playground/src/db/index.ts
new file mode 100644
index 00000000..3bbf07b2
--- /dev/null
+++ b/apps/playground/src/db/index.ts
@@ -0,0 +1,16 @@
+import { accounts } from "./accounts";
+import { memberships } from "./memberships";
+import type { AccountModel, MembershipModel, WorkspaceModel } from "./types";
+import { workspaces } from "./workspaces";
+
+export interface MockDB {
+ accounts: Record;
+ workspaces: Record;
+ memberships: MembershipModel[];
+}
+
+export const mockDB: MockDB = {
+ accounts,
+ workspaces,
+ memberships,
+};
diff --git a/apps/playground/src/db/memberships.ts b/apps/playground/src/db/memberships.ts
new file mode 100644
index 00000000..c7a9b6a7
--- /dev/null
+++ b/apps/playground/src/db/memberships.ts
@@ -0,0 +1,71 @@
+import { Role } from "@swy/validators";
+
+import { _USER } from "./accounts";
+import type { MembershipModel } from "./types";
+import { _WORKSPACE } from "./workspaces";
+
+export const memberships: MembershipModel[] = [
+ {
+ id: "mem-1",
+ role: Role.OWNER,
+ accountId: _USER.U1,
+ workspaceId: _WORKSPACE.W1,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+ {
+ id: "mem-2",
+ role: Role.MEMBER,
+ accountId: _USER.U2,
+ workspaceId: _WORKSPACE.W1,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+ {
+ id: "mem-3",
+ role: Role.GUEST,
+ accountId: _USER.U3,
+ workspaceId: _WORKSPACE.W1,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+ {
+ id: "mem-4",
+ role: Role.OWNER,
+ accountId: _USER.U3,
+ workspaceId: _WORKSPACE.W2,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+ {
+ id: "mem-5",
+ role: Role.MEMBER,
+ accountId: _USER.U1,
+ workspaceId: _WORKSPACE.W2,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+ {
+ id: "mem-6",
+ role: Role.OWNER,
+ accountId: _USER.U1,
+ workspaceId: _WORKSPACE.W3,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+ {
+ id: "mem-7",
+ role: Role.OWNER,
+ accountId: _USER.U2,
+ workspaceId: _WORKSPACE.W4,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+ {
+ id: "mem-8",
+ role: Role.OWNER,
+ accountId: _USER.U2,
+ workspaceId: _WORKSPACE.W5,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+ {
+ id: "mem-9",
+ role: Role.OWNER,
+ accountId: _USER.U3,
+ workspaceId: _WORKSPACE.W5,
+ joinedAt: Date.UTC(2024, 1, 1),
+ },
+];
diff --git a/apps/playground/src/db/types.ts b/apps/playground/src/db/types.ts
new file mode 100644
index 00000000..48acf6eb
--- /dev/null
+++ b/apps/playground/src/db/types.ts
@@ -0,0 +1,32 @@
+import type { IconInfo } from "@swy/ui/shared";
+import { Plan, Role } from "@swy/validators";
+
+export interface AccountModel {
+ id: string; // default uuid
+ name: string;
+ email: string;
+ avatarUrl: string;
+ clerkId: string;
+ preferredName: string;
+ hasPassword?: boolean;
+ updatedAt: number; // ts in 'ms'
+}
+
+export interface WorkspaceModel {
+ id: string; // (uuid)
+ name: string;
+ icon?: IconInfo | null;
+ domain: string;
+ plan: Plan;
+ inviteToken: string; // (uuid) default: same as `id`
+ createdBy: string; // (uuid) account.id
+ lastEditedAt: number; // ts in 'ms'
+}
+
+export interface MembershipModel {
+ id: string; // (uuid)
+ role: Role;
+ accountId: string; // (uuid) account.id
+ workspaceId: string; // (uuid) workspace.id
+ joinedAt: number; // ts in 'ms'
+}
diff --git a/apps/playground/src/db/workspaces.ts b/apps/playground/src/db/workspaces.ts
new file mode 100644
index 00000000..1e9d5115
--- /dev/null
+++ b/apps/playground/src/db/workspaces.ts
@@ -0,0 +1,65 @@
+import { Plan } from "@swy/validators";
+
+import { _USER } from "./accounts";
+import type { WorkspaceModel } from "./types";
+
+export enum _WORKSPACE {
+ W1 = "f12d4c5b-2d3b-4d2b-aef3-8f7319c5d481",
+ W2 = "b3c2a6e8-9231-4a9b-a89e-7d36d3f35ec2",
+ W3 = "a2c4e8d7-4b7e-45d9-bb3d-1f9e6fbe7d4f",
+ W4 = "c1d7e9b6-7a3d-4b2e-a8b3-9f7e6c5d3a2f",
+ W5 = "d9b3f6a2-5c4e-7b2d-8a3f-1f9e6c7d4b2e",
+}
+
+export const workspaces: Record = {
+ [_WORKSPACE.W1]: {
+ id: _WORKSPACE.W1,
+ name: "Alpha Workspace",
+ icon: { type: "emoji", emoji: "🚀" },
+ domain: "alphaworkspace.com",
+ plan: Plan.FREE,
+ inviteToken: _WORKSPACE.W1,
+ createdBy: _USER.U1,
+ lastEditedAt: Date.UTC(2024, 1, 31),
+ },
+ [_WORKSPACE.W2]: {
+ id: _WORKSPACE.W2,
+ name: "Beta Labs",
+ icon: { type: "lucide", name: "goal", color: "#D44C47" },
+ domain: "betalabs.org",
+ plan: Plan.EDUCATION,
+ inviteToken: _WORKSPACE.W2,
+ createdBy: _USER.U3,
+ lastEditedAt: Date.UTC(2024, 2, 26),
+ },
+ [_WORKSPACE.W3]: {
+ id: _WORKSPACE.W3,
+ name: "Gamma Studio",
+ icon: { type: "emoji", emoji: "🎨" },
+ domain: "gammastudio.net",
+ plan: Plan.PLUS,
+ inviteToken: _WORKSPACE.W3,
+ createdBy: _USER.U1,
+ lastEditedAt: Date.UTC(2024, 3, 14),
+ },
+ [_WORKSPACE.W4]: {
+ id: _WORKSPACE.W4,
+ name: "Delta Group",
+ icon: { type: "text", text: "D" },
+ domain: "deltagroup.com",
+ plan: Plan.BUSINESS,
+ inviteToken: _WORKSPACE.W4,
+ createdBy: _USER.U2,
+ lastEditedAt: Date.UTC(2024, 4, 29),
+ },
+ [_WORKSPACE.W5]: {
+ id: _WORKSPACE.W5,
+ name: "Epsilon Enterprises",
+ icon: null,
+ domain: "epsilonent.com",
+ plan: Plan.ENTERPRISE,
+ inviteToken: _WORKSPACE.W5,
+ createdBy: _USER.U2,
+ lastEditedAt: Date.UTC(2024, 7, 17),
+ },
+};
diff --git a/apps/playground/src/env.ts b/apps/playground/src/env.ts
new file mode 100644
index 00000000..7639a296
--- /dev/null
+++ b/apps/playground/src/env.ts
@@ -0,0 +1,44 @@
+import { createEnv } from "@t3-oss/env-nextjs";
+import { vercel } from "@t3-oss/env-nextjs/presets";
+import { z } from "zod";
+
+export const env = createEnv({
+ extends: [vercel()],
+ shared: {
+ NODE_ENV: z
+ .enum(["development", "production", "test"])
+ .default("development"),
+ PORT: z.coerce.number().default(3002),
+ },
+ /**
+ * Specify your server-side environment variables schema here.
+ * This way you can ensure the app isn't built with invalid env vars.
+ */
+ server: {
+ // LIVEBLOCKS
+ LIVEBLOCKS_SECRET_KEY: z.string(),
+ },
+ /**
+ * Specify your client-side environment variables schema here.
+ * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
+ */
+ client: {
+ // UNSPLASH
+ NEXT_PUBLIC_UNSPLASH_ACCESS_KEY: z.string(),
+ },
+ /**
+ * Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
+ */
+ runtimeEnv: {
+ NODE_ENV: process.env.NODE_ENV,
+ // NEXT
+ PORT: process.env.PORT,
+ // LIVEBLOCKS
+ LIVEBLOCKS_SECRET_KEY: process.env.LIVEBLOCKS_SECRET_KEY,
+ // UNSPLASH
+ NEXT_PUBLIC_UNSPLASH_ACCESS_KEY:
+ process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY,
+ },
+ skipValidation:
+ !!process.env.CI || process.env.npm_lifecycle_event === "lint",
+});
diff --git a/apps/playground/src/hooks/index.ts b/apps/playground/src/hooks/index.ts
new file mode 100644
index 00000000..2183ad53
--- /dev/null
+++ b/apps/playground/src/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./useMockDB";
diff --git a/apps/playground/src/hooks/useMockDB.ts b/apps/playground/src/hooks/useMockDB.ts
new file mode 100644
index 00000000..2bf080db
--- /dev/null
+++ b/apps/playground/src/hooks/useMockDB.ts
@@ -0,0 +1,21 @@
+"use client";
+
+import { useLocalStorage } from "usehooks-ts";
+
+import type { MockDB } from "~/db";
+
+const initial: MockDB = {
+ accounts: {},
+ workspaces: {},
+ memberships: [],
+};
+
+export const useMockDB = () => {
+ const [value, update] = useLocalStorage("mock:db", initial);
+
+ return {
+ ...value,
+ update,
+ reset: () => update(initial),
+ };
+};
diff --git a/apps/playground/src/liveblocks.config.ts b/apps/playground/src/liveblocks.config.ts
new file mode 100644
index 00000000..185ca73b
--- /dev/null
+++ b/apps/playground/src/liveblocks.config.ts
@@ -0,0 +1,50 @@
+// Define Liveblocks types for your application
+// https://liveblocks.io/docs/api-reference/liveblocks-react#Typing-your-data
+declare global {
+ interface Liveblocks {
+ // Each user's Presence, for useMyPresence, useOthers, etc.
+ Presence: {
+ // Example, real-time cursor coordinates
+ cursor: { x: number; y: number } | null;
+ };
+
+ // The Storage tree for the room, for useMutation, useStorage, etc.
+ Storage: {
+ // Example, a conflict-free list
+ // animals: LiveList;
+ };
+
+ // Custom user info set when authenticating with a secret key
+ UserMeta: {
+ id: string;
+ info: {
+ // Example properties, for useSelf, useUser, useOthers, etc.
+ name: string;
+ email: string;
+ avatarUrl: string;
+ };
+ };
+
+ // Custom events, for useBroadcastEvent, useEventListener
+ RoomEvent: {};
+ // Example has two events, using a union
+ // | { type: "PLAY" }
+ // | { type: "REACTION"; emoji: "🔥" };
+
+ // Custom metadata set on threads, for useThreads, useCreateThread, etc.
+ ThreadMetadata: {
+ // Example, attaching coordinates to a thread
+ // x: number;
+ // y: number;
+ };
+
+ // Custom room info set with resolveRoomsInfo, for useRoomInfo
+ RoomInfo: {
+ // Example, rooms with a title and url
+ // title: string;
+ // url: string;
+ };
+ }
+}
+
+export {};
diff --git a/apps/playground/tailwind.config.ts b/apps/playground/tailwind.config.ts
new file mode 100644
index 00000000..2e814470
--- /dev/null
+++ b/apps/playground/tailwind.config.ts
@@ -0,0 +1,17 @@
+import type { Config } from "tailwindcss";
+
+import baseConfig from "@swy/tailwind-config";
+
+export default {
+ // We need to append the path to the UI package to the content array so that
+ // those classes are included correctly.
+ content: [
+ ...baseConfig.content,
+ "../../packages/notion/src/**/*.{ts,tsx}",
+ "../../packages/ui/src/**/*.{ts,tsx}",
+ ],
+ presets: [baseConfig],
+ theme: {
+ extend: {},
+ },
+} satisfies Config;
diff --git a/apps/playground/tsconfig.json b/apps/playground/tsconfig.json
new file mode 100644
index 00000000..0739dc1d
--- /dev/null
+++ b/apps/playground/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "@swy/tsconfig/base.json",
+ "compilerOptions": {
+ "module": "esnext",
+ "lib": ["ES2022", "dom", "dom.iterable"],
+ "jsx": "preserve",
+ "baseUrl": ".",
+ "types": ["../../packages/i18n/src/types/i18next.d.ts"],
+ "paths": { "~/*": ["./src/*"] },
+ "plugins": [{ "name": "next" }]
+ },
+ "include": [".", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/apps/storybook/src/stories/shared/icon-menu.stories.tsx b/apps/storybook/src/stories/shared/icon-menu.stories.tsx
index b2bb7ccc..559a66fe 100644
--- a/apps/storybook/src/stories/shared/icon-menu.stories.tsx
+++ b/apps/storybook/src/stories/shared/icon-menu.stories.tsx
@@ -12,14 +12,11 @@ export default meta;
type Story = StoryObj;
-const defaultIcon: IconInfo = { type: "text", text: "S" }
+const defaultIcon: IconInfo = { type: "text", text: "S" };
const Template: Story["render"] = () => {
const [icon, setIcon] = useState(defaultIcon);
return (
- setIcon(defaultIcon)}
- >
+ setIcon(defaultIcon)}>
);
diff --git a/packages/ui/src/components/shared/icon-menu/emoji-picker/emoji-picker-content.tsx b/packages/ui/src/components/shared/icon-menu/emoji-picker/emoji-picker-content.tsx
index fdd2a634..26c00bc9 100644
--- a/packages/ui/src/components/shared/icon-menu/emoji-picker/emoji-picker-content.tsx
+++ b/packages/ui/src/components/shared/icon-menu/emoji-picker/emoji-picker-content.tsx
@@ -116,7 +116,7 @@ export function EmojiPickerContent({
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9ab43041..b4027ee8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -185,6 +185,94 @@ importers:
specifier: 'catalog:'
version: 5.6.3
+ apps/playground:
+ dependencies:
+ '@swy/i18n':
+ specifier: workspace:*
+ version: link:../../packages/i18n
+ '@swy/liveblocks':
+ specifier: workspace:*
+ version: link:../../packages/liveblocks
+ '@swy/notion':
+ specifier: workspace:*
+ version: link:../../packages/notion
+ '@swy/ui':
+ specifier: workspace:*
+ version: link:../../packages/ui
+ '@swy/validators':
+ specifier: workspace:*
+ version: link:../../packages/validators
+ '@t3-oss/env-nextjs':
+ specifier: catalog:env
+ version: 0.11.1(typescript@5.6.3)(zod@3.23.8)
+ lucide-react:
+ specifier: catalog:ui
+ version: 0.456.0(react@18.3.1)
+ next:
+ specifier: catalog:next14
+ version: 14.2.15(@babel/core@7.25.8)(react-dom@18.3.1(react@18.3.1))(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)
+ react-textarea-autosize:
+ specifier: ^8.5.3
+ version: 8.5.4(@types/react@18.3.11)(react@18.3.1)
+ sonner:
+ specifier: catalog:ui
+ version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ swr:
+ specifier: ^2.2.5
+ version: 2.2.5(react@18.3.1)
+ yjs:
+ specifier: ^13.6.15
+ version: 13.6.20
+ 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:node20
+ version: 20.16.13
+ '@types/react':
+ specifier: catalog:react18
+ version: 18.3.11
+ '@types/react-dom':
+ specifier: catalog:react18
+ version: 18.3.1
+ dotenv-cli:
+ specifier: catalog:env
+ version: 7.4.2
+ eslint:
+ specifier: 'catalog:'
+ version: 9.13.0(jiti@2.4.0)
+ jiti:
+ specifier: ^2.4.0
+ version: 2.4.0
+ prettier:
+ specifier: 'catalog:'
+ version: 3.3.3
+ tailwindcss:
+ specifier: 'catalog:'
+ version: 3.4.14(ts-node@10.9.2(@types/node@20.16.13)(typescript@5.6.3))
+ typescript:
+ specifier: 'catalog:'
+ version: 5.6.3
+
apps/storybook:
dependencies:
'@swy/liveblocks':