diff --git a/.github/workflows/studio-docker-build.yml b/.github/workflows/studio-docker-build.yml index dcd3380572663..312745399bb0d 100644 --- a/.github/workflows/studio-docker-build.yml +++ b/.github/workflows/studio-docker-build.yml @@ -21,6 +21,10 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false + # On push events paths-filter diffs against github.event.before with + # local git; without enough history it falls back to a git fetch that + # fails unauthenticated (persist-credentials is false). + fetch-depth: 50 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: filter with: diff --git a/.github/workflows/studio-e2e-test.yml b/.github/workflows/studio-e2e-test.yml index 67da2f67312e2..9b7ba85b54921 100644 --- a/.github/workflows/studio-e2e-test.yml +++ b/.github/workflows/studio-e2e-test.yml @@ -38,6 +38,10 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false + # On push events paths-filter diffs against github.event.before with + # local git; without enough history it falls back to a git fetch that + # fails unauthenticated (persist-credentials is false). + fetch-depth: 50 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: filter with: diff --git a/.github/workflows/studio-unit-tests.yml b/.github/workflows/studio-unit-tests.yml index 65e65dabcf412..597cf60938676 100644 --- a/.github/workflows/studio-unit-tests.yml +++ b/.github/workflows/studio-unit-tests.yml @@ -36,6 +36,10 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false + # On push events paths-filter diffs against github.event.before with + # local git; without enough history it falls back to a git fetch that + # fails unauthenticated (persist-credentials is false). + fetch-depth: 50 sparse-checkout: | apps/studio packages diff --git a/apps/design-system/content/docs/components/radio-group-card.mdx b/apps/design-system/content/docs/components/radio-group-card.mdx index 26adb02c51493..f30c838bdf98a 100644 --- a/apps/design-system/content/docs/components/radio-group-card.mdx +++ b/apps/design-system/content/docs/components/radio-group-card.mdx @@ -18,10 +18,10 @@ import { RadioGroupCard, RadioGroupCardItem } from 'ui' ``` ```tsx - - - - + + + + ``` diff --git a/apps/design-system/content/docs/components/radio-group-stacked.mdx b/apps/design-system/content/docs/components/radio-group-stacked.mdx index ff3c80616bf2e..a10fe095155dc 100644 --- a/apps/design-system/content/docs/components/radio-group-stacked.mdx +++ b/apps/design-system/content/docs/components/radio-group-stacked.mdx @@ -18,7 +18,7 @@ import { RadioGroupStacked, RadioGroupStackedItem } from 'ui' ``` ```tsx - + diff --git a/apps/design-system/content/docs/components/radio-group.mdx b/apps/design-system/content/docs/components/radio-group.mdx index 8bd7e79ac525a..f507c098d6036 100644 --- a/apps/design-system/content/docs/components/radio-group.mdx +++ b/apps/design-system/content/docs/components/radio-group.mdx @@ -58,7 +58,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' ``` ```tsx - +
diff --git a/apps/design-system/registry/default/example/radio-group-card-demo.tsx b/apps/design-system/registry/default/example/radio-group-card-demo.tsx index 85e885d6e286a..52cd0d3953c87 100644 --- a/apps/design-system/registry/default/example/radio-group-card-demo.tsx +++ b/apps/design-system/registry/default/example/radio-group-card-demo.tsx @@ -2,10 +2,10 @@ import { RadioGroupCard, RadioGroupCardItem } from 'ui' export default function RadioGroupDemo() { return ( - - - - + + + + ) } diff --git a/apps/design-system/registry/default/example/radio-group-card-form.tsx b/apps/design-system/registry/default/example/radio-group-card-form.tsx index c873f87056fc3..3717507e31ac2 100644 --- a/apps/design-system/registry/default/example/radio-group-card-form.tsx +++ b/apps/design-system/registry/default/example/radio-group-card-form.tsx @@ -53,21 +53,9 @@ export default function RadioGroupForm() { defaultValue={field.value} className="flex flex-wrap gap-3" > - - - - - - - - - - - - - - - + + + diff --git a/apps/design-system/registry/default/example/radio-group-card-with-children.tsx b/apps/design-system/registry/default/example/radio-group-card-with-children.tsx index bb33298a6103d..e4f1be820b17b 100644 --- a/apps/design-system/registry/default/example/radio-group-card-with-children.tsx +++ b/apps/design-system/registry/default/example/radio-group-card-with-children.tsx @@ -10,7 +10,7 @@ export default function RadioGroupDemo() { ] as const return ( - + {singleThemes.map((theme) => ( diff --git a/apps/design-system/registry/default/example/radio-group-demo.tsx b/apps/design-system/registry/default/example/radio-group-demo.tsx index 69086855cace4..677d047905ec5 100644 --- a/apps/design-system/registry/default/example/radio-group-demo.tsx +++ b/apps/design-system/registry/default/example/radio-group-demo.tsx @@ -2,7 +2,7 @@ import { Label, RadioGroup, RadioGroupItem } from 'ui' export default function RadioGroupDemo() { return ( - +
diff --git a/apps/design-system/registry/default/example/radio-group-form.tsx b/apps/design-system/registry/default/example/radio-group-form.tsx index edd035db328b9..8893a529bdb82 100644 --- a/apps/design-system/registry/default/example/radio-group-form.tsx +++ b/apps/design-system/registry/default/example/radio-group-form.tsx @@ -52,24 +52,24 @@ export default function RadioGroupForm() { defaultValue={field.value} className="flex flex-col space-y-1" > - - - - - All new messages - - - - - - Direct messages and mentions - - - - - - Nothing - +
+ + + All new messages + +
+
+ + + Direct messages and mentions + +
+
+ + + Nothing + +
diff --git a/apps/design-system/registry/default/example/radio-group-stacked-demo.tsx b/apps/design-system/registry/default/example/radio-group-stacked-demo.tsx index b8de098b5fa46..3e833b03244b2 100644 --- a/apps/design-system/registry/default/example/radio-group-stacked-demo.tsx +++ b/apps/design-system/registry/default/example/radio-group-stacked-demo.tsx @@ -2,7 +2,7 @@ import { RadioGroupStacked, RadioGroupStackedItem } from 'ui' export default function RadioGroupDemo() { return ( - + Notify me about... - - - - - - - - - - - - - - - + + + diff --git a/apps/docs/package.json b/apps/docs/package.json index a016e79a1b0ff..a394f747f7573 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -22,7 +22,6 @@ "codegen:references:new": "pnpm run codegen:references:new:ensure && tsx scripts/build-reference-content.ts", "codegen:references:legacy": "tsx features/docs/Reference.generated.script.ts", "codegen:references": "pnpm codegen:references:legacy && pnpm codegen:references:new", - "codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --cache --write \"content/**/*.mdx\"", "dev": "run-p --race dev:next dev:watch:troubleshooting", "dev:next": "next dev --port 3001", "dev:secrets:pull": "AWS_PROFILE=supa-dev node ../../scripts/getSecrets.js -n local/docs", diff --git a/apps/kb/.gitignore b/apps/kb/.gitignore new file mode 100644 index 0000000000000..016b59ea143dc --- /dev/null +++ b/apps/kb/.gitignore @@ -0,0 +1,24 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/apps/kb/AGENTS.md b/apps/kb/AGENTS.md new file mode 100644 index 0000000000000..d2093805665ce --- /dev/null +++ b/apps/kb/AGENTS.md @@ -0,0 +1,36 @@ +## Development + +When starting the dev server, run: + +``` +# From the root directory of the project +pnpm dev:kb + +# From the apps/kb directory +pnpm dev +``` + +## Build + +To run a full production build, run: + +``` +# From the root directory of the project +pnpm build:kb + +# From the apps/kb directory +pnpm build +``` + +## Documentation + +Full documentation: https://docs.astro.build + +Consult these guides before working on related tasks: + +- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/) +- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/) +- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/) +- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/) +- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/) +- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/) diff --git a/apps/kb/CLAUDE.md b/apps/kb/CLAUDE.md new file mode 120000 index 0000000000000..47dc3e3d863cf --- /dev/null +++ b/apps/kb/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/apps/kb/README.md b/apps/kb/README.md new file mode 100644 index 0000000000000..414a13a66195f --- /dev/null +++ b/apps/kb/README.md @@ -0,0 +1,46 @@ +# Astro Starter Kit: Basics + +```sh +npm create astro@latest -- --template basics +``` + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +/ +├── public/ +│ └── favicon.svg +├── src +│   ├── assets +│   │   └── astro.svg +│   ├── components +│   │   └── Welcome.astro +│   ├── layouts +│   │   └── Layout.astro +│   └── pages +│   └── index.astro +└── package.json +``` + +To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/apps/kb/astro.config.mjs b/apps/kb/astro.config.mjs new file mode 100644 index 0000000000000..23662b95a674e --- /dev/null +++ b/apps/kb/astro.config.mjs @@ -0,0 +1,47 @@ +// @ts-check +import { createRequire } from 'node:module' +import path from 'node:path' + +import react from '@astrojs/react' +import tailwindcss from '@tailwindcss/vite' +import { defineConfig } from 'astro/config' + +// Absolute dir of lodash-es, for the SSR-only lodash alias below (same fix +// apps/studio/vite.config.ts uses). `packages/ui`'s clipboard util does +// `import { noop } from 'lodash'` — under Vite's dev-mode SSR module runner, +// that CJS import evaluates as pure ESM (no module/exports/require), so the +// named import binds to undefined. lodash-es is real ESM and the same +// version, so swapping it in for SSR only (client bundles are untouched) +// fixes it without touching the shared `ui` package. +const lodashEsDir = path.dirname(createRequire(import.meta.url).resolve('lodash-es/package.json')) + +// Not typed as `import('vite').Plugin` — kb's own `vite` (from the pnpm +// catalog) and Astro's internal vite dependency resolve to distinct type +// instances, and comparing them blows TS's structural-comparison stack depth. +const ssrLodashEs = { + name: 'kb-ssr-lodash-es', + enforce: 'pre', + /** + * @param {string} source + * @param {string | undefined} _importer + * @param {{ ssr?: boolean } | undefined} options + */ + resolveId(source, _importer, options) { + if (!options?.ssr) return + if (source === 'lodash') return path.join(lodashEsDir, 'lodash.js') + const subpath = source.match(/^lodash\/(.+?)(\.js)?$/) + if (subpath) return path.join(lodashEsDir, `${subpath[1]}.js`) + }, +} + +// https://astro.build/config +export default defineConfig({ + base: '/kb', + integrations: [react()], + vite: { + ssr: { + noExternal: ['lodash'], + }, + plugins: [tailwindcss(), ssrLodashEs], + }, +}) diff --git a/apps/kb/package.json b/apps/kb/package.json new file mode 100644 index 0000000000000..109857da3d7f4 --- /dev/null +++ b/apps/kb/package.json @@ -0,0 +1,32 @@ +{ + "name": "kb", + "type": "module", + "version": "0.0.1", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "astro dev --port 3008", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/react": "^6.0.4", + "astro": "^7.2.6", + "react": "catalog:", + "react-dom": "catalog:", + "ui": "workspace:*" + }, + "devDependencies": { + "@tailwindcss/vite": "4.2.4", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "config": "workspace:*", + "lodash-es": "catalog:", + "tailwindcss": "catalog:" + }, + "allowScripts": { + "esbuild": true + } +} diff --git a/apps/kb/public/favicon/apple-icon-114x114.png b/apps/kb/public/favicon/apple-icon-114x114.png new file mode 100644 index 0000000000000..396cee8516afe Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-114x114.png differ diff --git a/apps/kb/public/favicon/apple-icon-120x120.png b/apps/kb/public/favicon/apple-icon-120x120.png new file mode 100644 index 0000000000000..7e4e727910e0c Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-120x120.png differ diff --git a/apps/kb/public/favicon/apple-icon-144x144.png b/apps/kb/public/favicon/apple-icon-144x144.png new file mode 100644 index 0000000000000..144190d197673 Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-144x144.png differ diff --git a/apps/kb/public/favicon/apple-icon-152x152.png b/apps/kb/public/favicon/apple-icon-152x152.png new file mode 100644 index 0000000000000..cb261b8bba09f Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-152x152.png differ diff --git a/apps/kb/public/favicon/apple-icon-57x57.png b/apps/kb/public/favicon/apple-icon-57x57.png new file mode 100644 index 0000000000000..278a7a87bee75 Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-57x57.png differ diff --git a/apps/kb/public/favicon/apple-icon-60x60.png b/apps/kb/public/favicon/apple-icon-60x60.png new file mode 100644 index 0000000000000..a693570eea352 Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-60x60.png differ diff --git a/apps/kb/public/favicon/apple-icon-72x72.png b/apps/kb/public/favicon/apple-icon-72x72.png new file mode 100644 index 0000000000000..10d2bbaaa8699 Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-72x72.png differ diff --git a/apps/kb/public/favicon/apple-icon-76x76.png b/apps/kb/public/favicon/apple-icon-76x76.png new file mode 100644 index 0000000000000..241bd6a24525f Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-76x76.png differ diff --git a/apps/kb/public/favicon/favicon-128.png b/apps/kb/public/favicon/favicon-128.png new file mode 100644 index 0000000000000..4fab8e34978e7 Binary files /dev/null and b/apps/kb/public/favicon/favicon-128.png differ diff --git a/apps/kb/public/favicon/favicon-16x16.png b/apps/kb/public/favicon/favicon-16x16.png new file mode 100644 index 0000000000000..7d05a72096bbb Binary files /dev/null and b/apps/kb/public/favicon/favicon-16x16.png differ diff --git a/apps/kb/public/favicon/favicon-180x180.png b/apps/kb/public/favicon/favicon-180x180.png new file mode 100644 index 0000000000000..3cf7f1132c8f3 Binary files /dev/null and b/apps/kb/public/favicon/favicon-180x180.png differ diff --git a/apps/kb/public/favicon/favicon-196x196.png b/apps/kb/public/favicon/favicon-196x196.png new file mode 100644 index 0000000000000..1e0258526ae9c Binary files /dev/null and b/apps/kb/public/favicon/favicon-196x196.png differ diff --git a/apps/kb/public/favicon/favicon-32x32.png b/apps/kb/public/favicon/favicon-32x32.png new file mode 100644 index 0000000000000..7eb8cabea60b5 Binary files /dev/null and b/apps/kb/public/favicon/favicon-32x32.png differ diff --git a/apps/kb/public/favicon/favicon-48x48.png b/apps/kb/public/favicon/favicon-48x48.png new file mode 100644 index 0000000000000..40377a51b54a7 Binary files /dev/null and b/apps/kb/public/favicon/favicon-48x48.png differ diff --git a/apps/kb/public/favicon/favicon-96x96.png b/apps/kb/public/favicon/favicon-96x96.png new file mode 100644 index 0000000000000..a8d96ffed730e Binary files /dev/null and b/apps/kb/public/favicon/favicon-96x96.png differ diff --git a/apps/kb/public/favicon/favicon.ico b/apps/kb/public/favicon/favicon.ico new file mode 100644 index 0000000000000..343ad561d7124 Binary files /dev/null and b/apps/kb/public/favicon/favicon.ico differ diff --git a/apps/kb/public/supabase-dark.svg b/apps/kb/public/supabase-dark.svg new file mode 100644 index 0000000000000..9e96fab4ddc8e --- /dev/null +++ b/apps/kb/public/supabase-dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/kb/public/supabase-light.svg b/apps/kb/public/supabase-light.svg new file mode 100644 index 0000000000000..60cbc71dc7c74 --- /dev/null +++ b/apps/kb/public/supabase-light.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/kb/src/assets/astro.svg b/apps/kb/src/assets/astro.svg new file mode 100644 index 0000000000000..8cf8fb0c7da6f --- /dev/null +++ b/apps/kb/src/assets/astro.svg @@ -0,0 +1 @@ + diff --git a/apps/kb/src/assets/background.svg b/apps/kb/src/assets/background.svg new file mode 100644 index 0000000000000..4b2be0ac0e47a --- /dev/null +++ b/apps/kb/src/assets/background.svg @@ -0,0 +1 @@ + diff --git a/apps/kb/src/components/Header.tsx b/apps/kb/src/components/Header.tsx new file mode 100644 index 0000000000000..5f7c344da070b --- /dev/null +++ b/apps/kb/src/components/Header.tsx @@ -0,0 +1,20 @@ +import { Logo } from './Logo' +import { Nav } from './Nav' +import { ThemeToggle } from './ThemeToggle' + +// Bare-bones port of apps/docs' TopNavBar — logo, nav, and theme toggle, each +// split into their own component (Logo.tsx, Nav.tsx, ThemeToggle.tsx). No +// search, no user menu, no dashboard link — kb doesn't have any of that yet. +function Header() { + return ( +
+
+ +
+ +
+ ) +} + +export { Header } diff --git a/apps/kb/src/components/Hero.tsx b/apps/kb/src/components/Hero.tsx new file mode 100644 index 0000000000000..8da35007604ed --- /dev/null +++ b/apps/kb/src/components/Hero.tsx @@ -0,0 +1,17 @@ +// Ported from apps/docs/components/HomePageCover.tsx — just the title + intro +// copy (rebranded for kb), without the animated logo or the CLI/AI setup +// prompt panel (docs-specific, feature-flagged, and not something kb has yet). +function Hero() { + return ( +
+
+

Supabase Knowledge Base

+

+ In-depth guides, tutorials, and explainers for best practices for Supabase databases. +

+
+
+ ) +} + +export { Hero } diff --git a/apps/kb/src/components/Logo.tsx b/apps/kb/src/components/Logo.tsx new file mode 100644 index 0000000000000..4e9a308302b5f --- /dev/null +++ b/apps/kb/src/components/Logo.tsx @@ -0,0 +1,31 @@ +// Same wordmark swap apps/docs' HeaderLogo uses (light/dark SVG pair from +// apps/docs/public), plus the "KB" mono label docs uses "DOCS" for. +// +// The SVGs live in public/, which Astro's `base` config doesn't rewrite on +// its own, so the base is prepended by hand — see +// https://docs.astro.build/en/reference/configuration-reference/#base +function Logo() { + const base = import.meta.env.BASE_URL + + return ( + + Supabase logo + Supabase logo + KB + + ) +} + +export { Logo } diff --git a/apps/kb/src/components/Nav.tsx b/apps/kb/src/components/Nav.tsx new file mode 100644 index 0000000000000..268a5953a1e87 --- /dev/null +++ b/apps/kb/src/components/Nav.tsx @@ -0,0 +1,78 @@ +import { + cn, + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, +} from 'ui' + +/** + * Hard-codding links in here for now until we have actual content. Might be worth putting these arrays in their on data file too. + */ +const topics = [ + { label: 'Troubleshooting', href: '#' }, + { label: 'Migrations', href: '#' }, + { label: 'Comparisons', href: '#' }, +] + +const resources = [ + { label: 'Status', href: 'https://status.supabase.com' }, + { label: 'Changelog', href: 'https://supabase.com/changelog' }, + { label: 'Docs', href: 'https://supabase.com/docs' }, +] + +const menus = [ + { label: 'Topics', items: topics }, + { label: 'Resources', items: resources }, +] + +const triggerClass = + 'h-(--header-height) p-2 border-transparent font-normal rounded-none text-foreground-light hover:text-foreground data-open:text-foreground! border-0 focus-ring focus-visible:text-foreground h-full focus-visible:rounded-sm shadow-none!' +// docs gates this at `md:absolute` (its own base component class) because its +// nav is hidden entirely below `lg` in favor of a separate mobile menu. kb +// doesn't have that split — the nav is always visible — so `absolute` is +// unconditional here; without it, an open menu pushes its siblings around +// below the `md` breakpoint instead of overlaying them. +const contentClass = + 'absolute top-[calc(100%+4px)]! min-w-56 max-h-[calc(100vh-4rem)] border-y w-screen md:w-64 overflow-hidden overflow-y-auto rounded-none md:rounded-md md:border border-overlay bg-overlay text-foreground-light shadow-md duration-0!' +const itemClass = + 'w-full flex h-8 items-center text-foreground-light text-sm hover:text-foreground select-none rounded-md p-2 leading-none no-underline focus-ring focus-visible:text-foreground' + +function Nav() { + return ( +
+ + + {menus.map((menu) => ( + + + {menu.label} + + +
+ {menu.items.map((item) => ( + + + {item.label} + + + ))} +
+
+
+ ))} +
+
+
+ ) +} + +export { Nav } diff --git a/apps/kb/src/components/ThemeToggle.tsx b/apps/kb/src/components/ThemeToggle.tsx new file mode 100644 index 0000000000000..a09b8163bdac6 --- /dev/null +++ b/apps/kb/src/components/ThemeToggle.tsx @@ -0,0 +1,65 @@ +// Same sun/moon icon apps/docs' mobile menu ThemeToggle uses (ui-patterns/ThemeToggle), +// but a plain binary light/dark switch — no system option, no dropdown, no next-themes. +function toggleTheme() { + const isDark = document.documentElement.getAttribute('data-theme') !== 'light' + const next = isDark ? 'light' : 'dark' + document.documentElement.setAttribute('data-theme', next) + try { + localStorage.setItem('theme', next) + } catch { + // localStorage unavailable (e.g. private browsing) — theme just won't persist. + } +} + +// Theme is read/written as the `data-theme` attribute on (matching +// next-themes' default `attribute="data-theme"`, which is what the shared +// theme CSS and Tailwind's `dark:` variant key off — see +// packages/config/css/variants.css). The no-flash script in Layout.astro sets +// the initial attribute before paint; this just flips it on click. The icons +// themselves don't need React state — which one is visible is driven purely +// by the `dark:` CSS variant, so there's no hydration-mismatch risk to guard +// against here. +function ThemeToggle() { + return ( + + ) +} + +export { ThemeToggle } diff --git a/apps/kb/src/env.d.ts b/apps/kb/src/env.d.ts new file mode 100644 index 0000000000000..f964fe0cffd88 --- /dev/null +++ b/apps/kb/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/kb/src/layouts/Layout.astro b/apps/kb/src/layouts/Layout.astro new file mode 100644 index 0000000000000..fc04f9e14e599 --- /dev/null +++ b/apps/kb/src/layouts/Layout.astro @@ -0,0 +1,88 @@ +--- +import { Header } from '../components/Header' +import '../styles/globals.css' +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Supabase Knowledge Base + + +
+ + + + + diff --git a/apps/kb/src/pages/index.astro b/apps/kb/src/pages/index.astro new file mode 100644 index 0000000000000..d860bb804ea9a --- /dev/null +++ b/apps/kb/src/pages/index.astro @@ -0,0 +1,8 @@ +--- +import { Hero } from '../components/Hero' +import Layout from '../layouts/Layout.astro' +--- + + + + diff --git a/apps/kb/src/styles/globals.css b/apps/kb/src/styles/globals.css new file mode 100644 index 0000000000000..bd5c11ac0689f --- /dev/null +++ b/apps/kb/src/styles/globals.css @@ -0,0 +1,74 @@ +@import 'config/tailwind.config.css'; + +@source '../pages/**/*.astro'; +@source '../components/**/*.{astro,tsx}'; +@source '../layouts/**/*.astro'; +@source './../../../../packages/ui/src/**/*.{tsx,ts,js}'; +@source './../../../../packages/ui-patterns/src/**/*.{tsx,ts,js}'; + +/* apps/docs loads Inter/Manrope via next/font, which injects these two + * variables as inline styles on . kb loads the same fonts via the + * tags in Layout.astro instead, so they're declared here as plain + * custom properties rather than next/font output. */ +:root { + --font-inter: 'Inter', system-ui, sans-serif; + --font-manrope: 'Manrope', system-ui, sans-serif; +} + +/* Same @theme block as apps/docs/styles/globals.css. */ +@theme inline { + --font-sans: + var(--font-inter), Inter, Helvetica Neue, Helvetica, ui-sans-serif, system-ui, sans-serif; + --font-heading: var(--font-manrope, var(--font-sans)); + --font-mono: 'Source Code Pro', ui-monospace, Menlo, monospace; +} + +/* Same type scale as apps/docs/styles/globals.css — without this, utilities + * like `text-sm` resolve to Tailwind's default 0.875rem instead of docs' + * 0.8125rem, so text using the same class names still renders at different + * sizes than docs. */ +@theme { + --text-sm: 0.8125rem; + --text-base: 0.9375rem; + --text-lg: 1rem; + --text-xl: 1.125rem; + --text-2xl: 1.375rem; + --text-3xl: 1.75rem; + --text-4xl: 2.125rem; + --text-5xl: 2.875rem; + --text-6xl: 3.625rem; + --text-7xl: 4.375rem; + --text-8xl: 5.875rem; + --text-9xl: 7.875rem; + --font-weight-normal: 450; +} + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Same rule as apps/docs/styles/globals.css: headings use font-heading + * (Manrope) by default instead of the body's font-sans (Inter). */ +h1:not(.font-mono), +h2:not(.font-mono), +h3:not(.font-mono), +h4:not(.font-mono), +h5:not(.font-mono), +h6:not(.font-mono), +.h1:not(.font-mono), +.h2:not(.font-mono), +.h3:not(.font-mono), +.h4:not(.font-mono), +.h5:not(.font-mono), +.h6:not(.font-mono) { + @apply font-heading font-semibold; +} + +*, +::after, +::before, +::backdrop, +::file-selector-button { + border-color: var(--border-default, currentColor); +} diff --git a/apps/kb/tsconfig.json b/apps/kb/tsconfig.json new file mode 100644 index 0000000000000..2424dae7d70c6 --- /dev/null +++ b/apps/kb/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react" + }, + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/apps/kb/turbo.jsonc b/apps/kb/turbo.jsonc new file mode 100644 index 0000000000000..53b4a6210ff52 --- /dev/null +++ b/apps/kb/turbo.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "./../../node_modules/turbo/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$"], + "outputs": ["dist/**"], + }, + }, +} diff --git a/apps/kb/vercel.json b/apps/kb/vercel.json new file mode 100644 index 0000000000000..8df7e53755bba --- /dev/null +++ b/apps/kb/vercel.json @@ -0,0 +1,3 @@ +{ + "buildCommand": "pnpm build" +} diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx index 7f8b36d6ac56e..01d3f17f8d447 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx @@ -193,7 +193,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) { key={compute.identifier} value={compute.identifier} className={cn( - 'relative text-sm text-left flex flex-col gap-0 px-0 py-3 [&_label]:w-full group w-full h-[110px]', + 'relative text-sm text-left flex flex-col gap-0 px-0 py-3 group w-full h-[110px]', lockedOption && 'opacity-50' )} disabled={disabled || lockedOption} diff --git a/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx b/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx index 4ad4e4159ce01..06007bd406a42 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx @@ -458,6 +458,7 @@ export const ExplorerNotebookTab = () => { key={cell._id} cell={cell} onEdit={persistNotebookTab} + onPrettifyQuery={() => queryCellRefs.current.get(cell._id)?.prettify()} ref={(instance) => { if (instance) queryCellRefs.current.set(cell._id, instance) else queryCellRefs.current.delete(cell._id) diff --git a/apps/studio/components/interfaces/Explorer/ExplorerQueryTab.tsx b/apps/studio/components/interfaces/Explorer/ExplorerQueryTab.tsx index 5cc0ad4a3bd54..ca6e043997e4f 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerQueryTab.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerQueryTab.tsx @@ -1,8 +1,9 @@ +import { type Hotkey } from '@tanstack/react-hotkeys' import { useDebounce } from '@uidotdev/usehooks' import { LOCAL_STORAGE_KEYS, useParams } from 'common' -import { Check, Keyboard, Loader2, MoreVertical, Save, SquareCode } from 'lucide-react' +import { AlignLeft, Check, Keyboard, Loader2, MoreVertical, Save, SquareCode } from 'lucide-react' import { useRouter } from 'next/router' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { Button, @@ -14,16 +15,18 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, + KeyboardShortcut, } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ExplorerToolbarAction } from './ExplorerToolbar' import { useCreateNotebook } from './hooks' -import { QueryEditor, type ExplorerQueryModel } from './QueryEditor' +import { QueryEditor, type ExplorerQueryModel, type QueryEditorHandle } from './QueryEditor' import { type QueryDisplay, type QueryResult } from './types' import { createQueryCellSkeleton } from './utils' import { getNotebook } from '@/data/content/notebooks/notebook-query' @@ -33,6 +36,8 @@ import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { explorerQueryState, useExplorerQueryStateSnapshot } from '@/state/explorer-query' import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state' import { useControlledRoleImpersonationState } from '@/state/role-impersonation-state' +import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut' +import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry' import { createTabId, TabsStateContext } from '@/state/tabs' /** Query-tab lifecycle adapter around the shared QueryEditor. */ @@ -50,6 +55,12 @@ export const ExplorerQueryTab = () => { true ) + const queryEditorRef = useRef(null) + + const hotkeySequnece: Hotkey | undefined = + SHORTCUT_DEFINITIONS[SHORTCUT_IDS.SQL_EDITOR_FORMAT].sequence[0] + const formatKeys = hotkeySequnece ? hotkeyToKeys(hotkeySequnece) : undefined + const [restoredQueryKey, setRestoredQueryKey] = useState() const [showQuery, setShowQuery] = useState(true) const [search, setSearch] = useState('') @@ -166,6 +177,7 @@ export const ExplorerQueryTab = () => { return ( { } /> - + setIsIntellisenseEnabled(!isIntellisenseEnabled)} @@ -266,6 +278,17 @@ export const ExplorerQueryTab = () => {
{isIntellisenseEnabled && } + + queryEditorRef.current?.prettify()} + > + + + Prettify SQL + + {formatKeys && } + diff --git a/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx b/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx index ce1d946780317..ce464262573ff 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx @@ -138,7 +138,7 @@ export type ExplorerToolbarActionProps = Omit< React.ComponentPropsWithRef, 'size' | 'variant' > & { - tooltip?: string + tooltip?: React.ReactNode } /** diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx index 8f2183c1a5516..cdbff562c8fd1 100644 --- a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx @@ -1,7 +1,10 @@ +import { AlignLeft } from 'lucide-react' import { forwardRef, useState } from 'react' +import { KeyboardShortcut } from 'ui' import { type Snapshot } from 'valtio' import { AddCellDropdown } from '../AddCellDropdown' +import { ExplorerToolbarAction } from '../ExplorerToolbar' import { MoveCellDropdownContent } from '../MoveCellDropdownContent' import { QueryEditor, type QueryEditorHandle } from '../QueryEditor' import { type QueryDisplay, type QueryResult } from '../types' @@ -22,15 +25,22 @@ import { import { type QuerySourceBinding } from '@/data/query-sources/query-source-registry' import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state' import { useLocalRoleImpersonationState } from '@/state/role-impersonation-state' +import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut' +import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry' + +const PRETTIFY_SHORTCUT_KEYS = hotkeyToKeys( + SHORTCUT_DEFINITIONS[SHORTCUT_IDS.SQL_EDITOR_FORMAT].sequence[0] +) interface QueryCellProps { cell: Snapshot onEdit?: () => void + onPrettifyQuery?: () => void } /** Notebook adapter around the shared QueryEditor. */ export const QueryCell = forwardRef(function QueryCell( - { cell, onEdit }, + { cell, onEdit, onPrettifyQuery }, ref ) { const snap = useNotebooksStateSnapshot() @@ -124,6 +134,18 @@ export const QueryCell = forwardRef(function onResultChange={setResult} onRowLimitChange={handleRowLimitChange} onDisplayChange={handleDisplayChange} + toolbarActions={ + } + tooltip={ +
+ Prettify SQL + +
+ } + onClick={onPrettifyQuery} + /> + } /> ) diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx index 96fd8e4a7c983..11cfe9b4f2c22 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx @@ -12,7 +12,7 @@ import { useState, type ReactNode, } from 'react' -import { Button, cn } from 'ui' +import { Button, cn, KeyboardShortcut } from 'ui' import { resolveLogTimeRange } from '../../QuerySources/LogTimeRange.utils' import { @@ -113,6 +113,8 @@ export type QueryEditorHandle = { run: (force?: boolean) => Promise /** The editor's live text buffer, ahead of any blur-triggered commit to the store. */ getSql: () => string + /** Formats the editor's SQL in place and commits the result, same as the SQL Editor's Prettify SQL action. */ + prettify: () => Promise } type QueryEditorProps = { @@ -350,9 +352,19 @@ export const QueryEditor = forwardRef(funct const Shell = variant === 'viewport' ? ExplorerQueryViewport : ExplorerQuery + const handlePrettify = async () => { + if (pendingProposalRef.current) return + + const editor = editorInstanceRef.current + if (!editor) return + await editor.getAction('editor.action.formatDocument')?.run() + onSqlCommitRef.current?.(editor.getValue()) + } + useImperativeHandle(ref, () => ({ run: (force = false) => handleRunQuery({ shouldForce: force }), getSql: () => sqlRef.current, + prettify: handlePrettify, })) useEffect(() => { @@ -409,7 +421,12 @@ export const QueryEditor = forwardRef(funct } loading={isExecuting} - tooltip={hasSelection ? 'Run selected query' : 'Run query'} + tooltip={ +
+ {hasSelection ? 'Run selected query' : 'Run query'} + +
+ } disabled={ isBusy || pendingProposal !== null || isRunDisabled || sql.trim().length === 0 } @@ -494,6 +511,16 @@ export const QueryEditor = forwardRef(funct if (selectionParts) setPromptState({ isOpen: true, ...selectionParts }) }, }) + + editor.addAction({ + id: 'prettify-query', + label: 'Prettify SQL', + keybindings: [monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF], + contextMenuGroupId: 'operation', + run: () => { + handlePrettify() + }, + }) }} /> diff --git a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx index 56c288a0ef6e1..0fdbf8eab0ffc 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx @@ -502,16 +502,8 @@ export function LogDrainDestinationSheetForm({ onValueChange={field.onChange} value={field.value} > - - - - - - - - - - + +
diff --git a/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx b/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx index ef41eea090df5..e8b3721d06ed3 100644 --- a/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx @@ -63,7 +63,7 @@ export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => { } description="Recommended for production workloads" - className="[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>label]:flex [&>div>div>label]:items-center [&>div>div>label]:gap-x-2" + className="[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>div]:flex [&>div>div>div]:items-center [&>div>div>div]:gap-x-2" /> @@ -82,7 +82,7 @@ export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => { } description="Not recommended for production workloads" className={cn( - '[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>label]:flex [&>div>div>label]:items-center [&>div>div>label]:gap-x-2', + '[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>div]:flex [&>div>div>div]:items-center [&>div>div>div]:gap-x-2', form.getValues('useOrioleDb') ? 'rounded-b-none!' : '' )} disabled={disableOrioleProjectCreation} diff --git a/knip.jsonc b/knip.jsonc index c1240def9d703..1f537e739faeb 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -72,6 +72,7 @@ // read are documentation of the valid values, not dead code. Scoped to // this file so `enumMembers` keeps working everywhere else. "data/database/constraints-query.ts": ["enumMembers"], + "hooks/misc/useTrackExperimentExposure.ts": ["files"], }, // `vercel` is a globally installed CLI used by the `deploy:staging` script "ignoreBinaries": ["vercel"], diff --git a/package.json b/package.json index 0ca9b3f9aa1fd..aa441729e0087 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "build:studio:docker:tanstack": "docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local-tanstack --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --build-arg STUDIO_FRAMEWORK=tanstack --no-cache", "build:design-system": "turbo run build --filter=design-system", "build:docs": "turbo run build --filter=docs", + "build:kb": "turbo run build --filter=kb", "clean": "turbo run clean --parallel && rimraf -G .turbo/cache && pnpm purge", "dev": "turbo run dev --parallel", "dev:studio": "turbo run dev --filter=studio --parallel", "dev:studio-local": "pnpm setup:cli && NODE_ENV=test MODE=test pnpm --prefix ./apps/studio dev", "dev:docs": "turbo run dev --filter=docs --parallel", + "dev:kb": "turbo run dev --filter=kb --parallel", "dev:www": "turbo run dev --filter=www --parallel", "dev:design-system": "turbo run dev --filter=design-system --parallel", "lint": "turbo run lint", diff --git a/packages/ui/src/components/radio-group-card.tsx b/packages/ui/src/components/radio-group-card.tsx index b51a2a5667bc8..dc62ce535d7a6 100644 --- a/packages/ui/src/components/radio-group-card.tsx +++ b/packages/ui/src/components/radio-group-card.tsx @@ -29,10 +29,14 @@ interface RadioGroupCardItemProps { const RadioGroupCardItem = React.forwardRef< React.ElementRef, RadioGroupCardItemProps & React.ComponentPropsWithoutRef ->(({ image, label, showIndicator = true, ...props }, ref) => { +>(({ id: idProp, children, className, image, label, showIndicator = true, ...props }, ref) => { + const generatedId = React.useId() + const id = idProp || generatedId return ( - {props.children} -