Merge branch 'master' into chore/remove-jwt-secret-from-db

This commit is contained in:
Etienne Stalmans authored and GitHub committed 2025-02-21 06:55:14 +01:00
commit 2e76677703
3283 files changed
+250667 -164830

No files matched your search

+11 -1
View File
@@ -1,10 +1,20 @@
/packages/shared-data/pricing.ts @roryw10 @kevcodez
/packages/shared-data/plans.ts @roryw10 @kevcodez
/packages/common/telemetry-constants.ts @4L3k51 @loong @pamelachia
/apps/studio/ @supabase/Dashboard
# temporary until we've ironed out all the problems with sync workflow
/apps/docs/content/troubleshooting/ @charislam
/apps/www/ @supabase/marketing
/apps/www/public/images/blog @supabase/marketing
/apps/www/lib/redirects.js
/docker/ @supabase/cli
/docker/ @supabase/dev-workflows
/apps/studio/next.config.js @supabase/security
/apps/studio/components/interfaces/Billing/Payment @supabase/security
/apps/studio/components/interfaces/Organization/BillingSettings/ @supabase/security
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
/apps/studio/pages/new/index.tsx @supabase/security
+8 -4
View File
@@ -33,14 +33,18 @@ jobs:
with:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Type check
run: npm run typecheck
run: pnpm run typecheck
- name: Run tests
run: npm run test
run: pnpm run test
+28 -1
View File
@@ -42,4 +42,31 @@ jobs:
run: |
echo "Applying triage label for new issue"
gh issue edit "$NUMBER" --add-label "$LABELS"
spam-detection:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Check GitHub Issue for spam
env:
POST_URL: ${{ secrets.POST_URL }}
BEARER_TOKEN: ${{ secrets.BEARER_TOKEN }}
NUMBER: ${{ github.event.issue.number }}
run: |
RESPONSE=$(curl -s -X POST "$POST_URL" \
-H "Authorization: Bearer $BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"issue_id": $NUMBER}')
echo "spam_response=$RESPONSE" >> $GITHUB_OUTPUT
- name: Use spam detector output to label issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABEL: flagged
run: |
IS_SPAM=$(echo "$SPAM_RESPONSE" | jq -r '.spam')
if [ "$IS_SPAM" == "true" ]; then
echo "Applying flagged label for new issue suspected of spam"
gh issue edit "$NUMBER" --add-label "$LABEL"
fi
+9 -4
View File
@@ -18,20 +18,25 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'autofix') }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: apps
ref: ${{ github.head_ref }}
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install required dependencies
run: npm ci
run: pnpm i
- name: Run Prettier in fix mode
run: npm run format
run: pnpm run format
- name: Commit changes and push to existing branch
run: |
@@ -39,7 +44,7 @@ jobs:
git config --global user.email 'github-tidy-bot@supabase.com'
if [[ `git status --porcelain` ]]; then
echo "[bot] Changes detected, committing."
echo "[bot] Running in non-squash mode."
git commit -am "ci: Autofix updates from GitHub workflow"
git push
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
exclude: |
*.css
**/package.json
**/package-lock.json
**/pnpm-lock.yaml
./.git/*
*.ipynb
./i18n/README.*.md
-63
View File
@@ -1,63 +0,0 @@
name: Config (Preview)
on:
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- master
paths:
- "supabase/**"
workflow_dispatch:
jobs:
wait:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.check.outputs.conclusion }}
steps:
- uses: fountainhead/action-wait-for-check@v1.2.0
id: check
with:
checkName: Supabase
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
apply:
needs:
- wait
if: ${{ needs.wait.outputs.status == 'success' }}
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
TF_VAR_linked_project: xguihxuzqibwxjnimxev
TF_VAR_git_branch: ${{ github.head_ref }}
TF_CLI_ARGS_apply: -target=supabase_settings.preview
defaults:
run:
working-directory: supabase/remotes
outputs:
db_user: ${{ steps.branch.outputs.user }}
db_pass: ${{ steps.branch.outputs.password }}
db_host: ${{ steps.branch.outputs.host }}
db_port: ${{ steps.branch.outputs.port }}
jwt_secret: ${{ steps.branch.outputs.jwt_secret }}
ref: ${{ steps.branch.outputs.id }}
status: ${{ steps.branch.outputs.status }}
version: ${{ steps.branch.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- run: terraform init
- run: terraform apply -auto-approve -no-color
- id: branch
run: |
terraform output -json branch_database \
| jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" \
>> "$GITHUB_OUTPUT"
-28
View File
@@ -1,28 +0,0 @@
name: Config (Production)
on:
push:
branches:
- master
paths:
- "supabase/remotes/**"
workflow_dispatch:
jobs:
apply:
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
TF_VAR_linked_project: xguihxuzqibwxjnimxev
defaults:
run:
working-directory: supabase/remotes
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- run: terraform init
- run: terraform apply -auto-approve -no-color
+8 -3
View File
@@ -26,20 +26,25 @@ jobs:
sparse-checkout: |
apps/docs
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Download dependencies
run: npm ci
run: pnpm i
- name: Update last-changed dates
working-directory: ./apps/docs
if: ${{ !inputs.reset }}
run: npm run last-changed
run: pnpm run last-changed
- name: Reset last-changed dates
working-directory: ./apps/docs
if: ${{ inputs.reset }}
run: npm run last-changed:reset
run: pnpm run last-changed:reset
+87
View File
@@ -0,0 +1,87 @@
name: '[Docs] Lint v2'
on:
pull_request:
paths:
- '.github/workflows/docs-lint-v2.yml'
- 'supa-mdx-lint.config.toml'
- 'apps/docs/content/**'
push:
branches:
- master
workflow_dispatch:
env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
permissions:
contents: write
pull-requests: write
jobs:
supa-mdx-lint:
name: supa-mdx-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
sparse-checkout: |
supa-mdx-lint.config.toml
supa-mdx-lint
apps/docs/content
- name: cache cargo
id: cache-cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: 0be447eba738c3fd56698eff8ad01fa56c2391d7
- name: install linter
if: steps.cache-cargo.outputs.cache-hit != 'true'
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev 0be447eba738c3fd56698eff8ad01fa56c2391d7
- name: install reviewdog
uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.0
with:
reviewdog_version: v0.20.2
- name: run linter on PR
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o pipefail
git diff --name-only origin/$BASE_REF HEAD \
| { grep -E "^apps/docs/content/" || test $? = 1; } \
| xargs -r supa-mdx-lint --format rdf \
| reviewdog -f=rdjsonl -reporter=github-pr-review
- name: run linter on push or workflow dispatch
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
supa-mdx-lint apps/docs/content || {
echo "Linter failed, attempting to fix errors..."
git config --global user.name 'github-docs-bot'
git config --global user.email 'github-docs-bot@supabase.com'
BRANCH_NAME="bot/docs-lint-fixes"
EXISTING_BRANCH=$(git ls-remote --heads origin $BRANCH_NAME)
if [ -n "$EXISTING_BRANCH" ]; then
git checkout $BRANCH_NAME
else
git checkout -b $BRANCH_NAME
fi
supa-mdx-lint apps/docs/content --fix || FIX_FAILED=1
git add .
git commit -m '[bot] fix lint errors' || true
git push origin $BRANCH_NAME
if [ -z "$EXISTING_BRANCH" ]; then
gh pr create --title '[bot] fix lint errors' --body 'This PR fixes lint errors in the documentation.' --head $BRANCH_NAME
fi
if [ "${FIX_FAILED:-0}" -eq 1 ]; then
echo "Fix did not correct all errors."
exit 1
fi
}
+29 -24
View File
@@ -11,31 +11,36 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: master
sparse-checkout: |
apps/docs
- uses: actions/checkout@v4
with:
ref: master
sparse-checkout: |
apps/docs
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install deps
run: npm ci
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Change to apps/docs/spec directory and run make command
working-directory: apps/docs/spec
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
- name: Install deps
run: pnpm i
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'feat: update mgmt api docs'
title: 'feat: update mgmt api docs'
body: 'This PR updates mgmt api docs automatically.'
branch: 'gha/auto-update-mgmt-api-docs'
base: 'master'
- name: Change to apps/docs/spec directory and run make command
working-directory: apps/docs/spec
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'feat: update mgmt api docs'
title: 'feat: update mgmt api docs'
body: 'This PR updates mgmt api docs automatically.'
branch: 'gha/auto-update-mgmt-api-docs'
base: 'master'
@@ -0,0 +1,56 @@
name: '[Docs] Sync troubleshooting guides to GitHub Discussions'
on:
push:
branches:
- master
paths:
- 'apps/docs/content/troubleshooting/**'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-troubleshooting:
runs-on: ubuntu-latest
env:
SEARCH_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
SEARCH_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
SEARCH_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
apps/docs
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Run Troubleshooting script
run: pnpm run -F docs troubleshooting:sync
- name: Create Pull Request
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with:
commit-message: '[bot] sync troubleshooting guides to db'
title: '[bot] sync troubleshooting guides to db'
author: 'github-docs-sync-bot <github-docs-sync-bot@supabase.com>'
branch: 'bot/docs-sync-troubleshooting'
branch-suffix: 'random'
+8 -3
View File
@@ -22,14 +22,19 @@ jobs:
apps/docs
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run tests
run: npm --prefix="apps/docs" run test:smoke
run: pnpm --prefix="apps/docs" run test:smoke
+8 -3
View File
@@ -23,14 +23,19 @@ jobs:
examples
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run tests
run: npm run test:docs
run: pnpm run test:docs
+31
View File
@@ -0,0 +1,31 @@
name: Comment on external PRs
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
comment-on-external-pr:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const commentBody = `
Thanks for contributing to Supabase! ❤️ Our team will review your PR.
A few tips for a smoother review process:
- If you have a local version of the repo, run \`pnpm run format\` to make sure formatting checks pass.
- Once we've reviewed your PR, please don't trivially merge master (don't click \`Update branch\` if there are no merge conflicts to be fixed). This invalidates any pre-merge checks we've run.
`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
+11 -5
View File
@@ -11,15 +11,21 @@ jobs:
name: runner / vale
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
sparse-checkout: |
.vale.ini
vale
apps/docs/content
- name: Get changed files
id: changed-files
env:
BASE_REF: ${{ github.base_ref }}
SHA: ${{ github.sha }}
run: |
git fetch origin $BASE_REF
echo "CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/$BASE_REF $SHA | grep '^apps/docs/content/' | tr '\n' ',' | sed 's/,$//')" >> $GITHUB_OUTPUT
- uses: errata-ai/vale-action@reviewdog
with:
files: apps/docs/content
# [Charis] Temporary because we still have to go through giant backlog of existing errors
# Don't want people to worry about this (just want them looking at the specific PR comments)
# fail_on_error: true
files: '${{ steps.changed-files.outputs.CHANGED_FILES }}'
separator: ','
+25 -13
View File
@@ -18,17 +18,21 @@ jobs:
uses: actions/checkout@v4
with:
sparse-checkout: apps
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
cache: 'pnpm'
- name: Download dependencies
run: |
npm ci
pnpm i
- name: Run prettier
run: |-
npm run test:prettier
pnpm run test:prettier
# i18n is not a node package, so we handle that one separately
format-i18n:
@@ -39,19 +43,21 @@ jobs:
with:
sparse-checkout: |
i18n
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
cache: 'pnpm'
- name: Download dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
pnpm i
- name: Run prettier
run: |-
npx prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
pnpm exec prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
format-sql:
runs-on: ubuntu-latest
@@ -62,13 +68,19 @@ jobs:
sparse-checkout: |
apps/docs/pages
apps/docs/content
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Download dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
pnpm i
- name: Run prettier
run: |-
# Check mdx files which contain sql code blocks
grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs npx prettier -c
grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs pnpm exec prettier -c
+8 -3
View File
@@ -38,13 +38,18 @@ jobs:
apps/www/.env.local.example
supabase
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Download dependencies
run: npm ci
run: pnpm i
# Need the miscellaneous use API, which is available publicly (by design) in www
- name: Copy environment variables
@@ -53,9 +58,9 @@ jobs:
- name: Update embeddings
working-directory: ./apps/docs
if: ${{ !inputs.refresh }}
run: npm run embeddings
run: pnpm run embeddings
- name: Refresh embeddings
working-directory: ./apps/docs
if: ${{ inputs.refresh }}
run: npm run embeddings:refresh
run: pnpm run embeddings:refresh
@@ -0,0 +1,25 @@
name: Self-Host Tests Smoke
on:
pull_request:
branches: ['master']
paths:
- 'docker/**/*'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
docker/
- name: Run docker-compose up
# Ensure all services can be started and healthy with default config
run: cd docker && cp .env.example .env && docker compose up --wait
@@ -1,15 +1,17 @@
name: Playwright Tests
name: Studio E2E Tests
on:
push:
branches: [master]
paths:
- 'apps/studio/**'
- 'package-lock.json'
- 'tests/studio-tests/**'
- 'pnpm-lock.yaml'
pull_request:
branches: [master]
paths:
- 'apps/studio/**'
- 'package-lock.json'
- 'tests/studio-tests/**'
- 'pnpm-lock.yaml'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
@@ -23,23 +25,29 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- uses: supabase/setup-cli@v1
with:
version: latest
- name: Install dependencies
run: npm ci
run: pnpm i
- name: Install Playwright Browsers
run: npx playwright install --with-deps
run: pnpm -C tests/studio-tests exec playwright install --with-deps
- name: Run Playwright tests
run: npm run test:playwright
run: pnpm test:e2e:studio-local
# mark the action as succeeded even if the tests failed. This is temporarily until we make the tests more stable.
# continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-tests/playwright-report/
path: tests/studio-tests/playwright-report/
retention-days: 7
@@ -8,12 +8,12 @@ on:
branches: [master, studio]
paths:
- 'apps/studio/**'
- 'package-lock.json'
- 'pnpm-lock.yaml'
pull_request:
branches: [master, studio]
paths:
- 'apps/studio/**'
- 'package-lock.json'
- 'pnpm-lock.yaml'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
@@ -31,17 +31,21 @@ jobs:
sparse-checkout: |
apps/studio
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
working-directory: ./
- name: Run Tests
env:
# Default is 2 GB, increase to have less frequent OOM errors
NODE_OPTIONS: '--max_old_space_size=3072'
run: npm run test:studio
run: pnpm run test:studio
working-directory: ./
+9 -4
View File
@@ -19,17 +19,22 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run TypeScript type check
run: npx turbo run typecheck
run: pnpm exec turbo run typecheck
- name: Run Lint
run: npx turbo run lint
run: pnpm exec turbo run lint
+8 -3
View File
@@ -21,14 +21,19 @@ jobs:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run tests
run: npm run test:ui-patterns
run: pnpm run test:ui-patterns
+8 -3
View File
@@ -21,14 +21,19 @@ jobs:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run tests
run: npm run test:ui
run: pnpm run test:ui
+2 -1
View File
@@ -1 +1,2 @@
engine-strict=true
engine-strict=true
update-notifier=false
+5 -3
View File
@@ -7,12 +7,14 @@ apps/**/out
# prettier-plugin-sql-cst only supports sqlite syntax
**/supabase/migrations/*.sql
apps/www/schema.sql
apps/www/public/images/*
apps/docs/**/generated/*
apps/docs/examples
examples/slack-clone/nextjs-slack-clone/full-schema.sql
# ignore files with custom js formatting
apps/docs/pages/guides/auth/*.mdx
apps/docs/pages/guides/integrations/*.mdx
apps/studio/public
apps/**/.turbo
apps/docs/CONTRIBUTING.md
apps/design-system/__registry__
apps/design-system/__registry__
packages/icons/__registry__
packages/icons/src/icons/*.ts
-1
View File
@@ -6,7 +6,6 @@
"printWidth": 100,
"endOfLine": "lf",
"sqlKeywordCase": "lower",
"pluginSearchDirs": false,
"plugins": ["prettier-plugin-sql-cst"],
"overrides": [
{
+8 -11
View File
@@ -38,7 +38,8 @@ You will need to install and configure the following dependencies on your machin
- [Git](http://git-scm.com/)
- [Node.js v20.x (LTS)](http://nodejs.org)
- [npm](https://www.npmjs.com/) version 10.x.x or higher
- [pnpm](https://pnpm.io/) version 9.x.x or higher
- [make](https://www.gnu.org/software/make/) or the equivalent to `build-essentials` for your OS
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
## Local development
@@ -69,7 +70,7 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
1. Install the dependencies in the root of the repo.
```sh
npm install # install dependencies
pnpm install # install dependencies
```
2. Copy the example `.env.local.example` to `.env.local`
@@ -80,7 +81,7 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
3. After that you can run the apps simultaneously with the following.
```sh
npm run dev # start all the applications
pnpm dev # start all the applications
```
Then visit, and edit, any of the following sites:
@@ -96,7 +97,7 @@ Then visit, and edit, any of the following sites:
You can run any of the sites individually by using the scope name. For example:
```sh
npm run dev:www
pnpm dev:www
```
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`
@@ -114,17 +115,13 @@ The monorepo has a set of shared components under `/packages`:
#### Installing packages
Installing a package with NPM workspaces requires you to add the `-w` flag to tell NPM which workspace you want to install into. Do not install dependencies in their local folder, install them from the route using the `-w` flag.
The format is: `npm install <package name> -w=<workspace to install in>`.
Installing a package in a specific workspace requires you to move to the workspace and then run the install command.
For example:
- `npm install react -w common`: installs into `./packages/common`
- `npm install react -w www`: installs into `./apps/www`
- `npm install react -w studio`: installs into `./apps/studio`
1. `cd apps/studio`: move to the `studio` workspace
2. `pnpm add react`: installs `react` into `studio` workspace
You do not need to install `devDependencies` in each workspace. These can all be installed in the root package.
---
+2 -2
View File
@@ -52,10 +52,10 @@ You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and
- [Postgres](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API.
- [GoTrue](https://github.com/supabase/gotrue) is a JWT based API for managing users and issuing JWT tokens.
- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions.
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API.
- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc.
- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway.
+158 -4
View File
@@ -654,6 +654,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"dialog-centered-off": {
name: "dialog-centered-off",
type: "components:example",
registryDependencies: ["dialog","button"],
component: React.lazy(() => import("@/registry/default/example/dialog-centered-off")),
source: "",
files: ["registry/default/example/dialog-centered-off.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"drawer-demo": {
name: "drawer-demo",
type: "components:example",
@@ -1787,6 +1798,72 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"inner-side-menu-demo": {
name: "inner-side-menu-demo",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/inner-side-menu-demo")),
source: "",
files: ["registry/default/example/inner-side-menu-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"inner-side-menu-static-titles": {
name: "inner-side-menu-static-titles",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/inner-side-menu-static-titles")),
source: "",
files: ["registry/default/example/inner-side-menu-static-titles.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"inner-side-menu-loading": {
name: "inner-side-menu-loading",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/inner-side-menu-loading")),
source: "",
files: ["registry/default/example/inner-side-menu-loading.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"inner-side-menu-multiple-sections": {
name: "inner-side-menu-multiple-sections",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/inner-side-menu-multiple-sections")),
source: "",
files: ["registry/default/example/inner-side-menu-multiple-sections.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"inner-side-menu-empty": {
name: "inner-side-menu-empty",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/inner-side-menu-empty")),
source: "",
files: ["registry/default/example/inner-side-menu-empty.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"inner-side-menu-with-search": {
name: "inner-side-menu-with-search",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/inner-side-menu-with-search")),
source: "",
files: ["registry/default/example/inner-side-menu-with-search.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-demo": {
name: "multi-select-demo",
type: "components:example",
@@ -1798,13 +1875,79 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"multi-select-form": {
name: "multi-select-form",
"multi-select-disabled": {
name: "multi-select-disabled",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-form")),
component: React.lazy(() => import("@/registry/default/example/multi-select-disabled")),
source: "",
files: ["registry/default/example/multi-select-form.tsx"],
files: ["registry/default/example/multi-select-disabled.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-badge-limit-wrap": {
name: "multi-select-badge-limit-wrap",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-badge-limit-wrap")),
source: "",
files: ["registry/default/example/multi-select-badge-limit-wrap.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-badge-limit": {
name: "multi-select-badge-limit",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-badge-limit")),
source: "",
files: ["registry/default/example/multi-select-badge-limit.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-combobox": {
name: "multi-select-combobox",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-combobox")),
source: "",
files: ["registry/default/example/multi-select-combobox.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-combobox-creatable": {
name: "multi-select-combobox-creatable",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-combobox-creatable")),
source: "",
files: ["registry/default/example/multi-select-combobox-creatable.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-deletable-badge": {
name: "multi-select-deletable-badge",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-deletable-badge")),
source: "",
files: ["registry/default/example/multi-select-deletable-badge.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-inline-search-input": {
name: "multi-select-inline-search-input",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-inline-search-input")),
source: "",
files: ["registry/default/example/multi-select-inline-search-input.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
@@ -2029,6 +2172,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"logs-bar-chart": {
name: "logs-bar-chart",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/logs-bar-chart")),
source: "",
files: ["registry/default/example/logs-bar-chart.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"chart-area-axes": {
name: "chart-area-axes",
type: "components:block",
+2 -3
View File
@@ -1,17 +1,16 @@
'use client'
import * as React from 'react'
import { Provider as JotaiProvider } from 'jotai'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from 'next-themes/dist/types'
import { TooltipProvider_Shadcn_ } from 'ui'
import { TooltipProvider } from 'ui'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<JotaiProvider>
<NextThemesProvider {...props}>
<TooltipProvider_Shadcn_ delayDuration={0}>{children}</TooltipProvider_Shadcn_>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</NextThemesProvider>
</JotaiProvider>
)
+15
View File
@@ -102,6 +102,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/info-tooltip',
items: [],
},
{
title: 'Inner Side Menu',
href: '/docs/fragments/inner-side-menu',
items: [],
},
{
title: 'Form Item Layout',
href: '/docs/fragments/form-item-layout',
@@ -112,6 +117,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/multi-select',
items: [],
},
{
title: 'Logs Bar Chart',
href: '/docs/fragments/logs-bar-chart',
items: [],
},
],
},
{
@@ -339,6 +349,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/slider',
items: [],
},
{
title: 'Sidebar',
href: '/docs/components/sidebar',
items: [],
},
{
title: 'Sonner',
href: '/docs/components/sonner',
@@ -4,6 +4,14 @@ description: Latest updates and announcements.
toc: false
---
## 18th Oct 2024 - Update multi-select
Improved Multi Select with combobox, badge limit, and more examples.
- [Multi Select](/design-system/components/multi-select)
[PR](https://github.com/supabase/supabase/pull/29695)
## 12th June 2024 - Button styles updated
- [`<Button />`](/design-system/docs/components/button) styles updated
@@ -13,44 +13,6 @@ source:
<ComponentPreview name="dialog-demo" peekCode wide />
## Installation
<Tabs defaultValue="cli">
<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
```bash
npx shadcn-ui@latest add dialog
```
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Install the following dependencies:</Step>
```bash
npm install @radix-ui/react-dialog
```
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="dialog" />
<Step>Update the import paths to match your project setup.</Step>
</Steps>
</TabsContent>
</Tabs>
## Usage
```tsx
@@ -85,6 +47,23 @@ import {
<ComponentPreview name="dialog-close-button" />
### Centered behaviour
You can control whether the dialog is centered by passing `centered={false}` to the `DialogContent` component.
```tsx {3}
<Dialog>
<ContextMenuTrigger>Click here</ContextMenuTrigger>
<DialogContent centered={false}>
{/*
* Content in here
*/}
</DialogContent>
</Dialog>
```
<ComponentPreview name="dialog-centered-off" />
## Notes
To activate the `Dialog` component from within a `Context Menu` or `Dropdown Menu`, you must encase the `Context Menu` or
@@ -93,7 +72,7 @@ To activate the `Dialog` component from within a `Context Menu` or `Dropdown Men
```tsx {14-25}
<Dialog>
<ContextMenu>
<ContextMenuTrigger>Right click</ContextMenuTrigger>
<ContextMenuTrigger>Dialog not centered</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuItem>Download</ContextMenuItem>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,142 @@
---
title: Inner Side Menu
description: InnerSideMenu is a component that provides a collapsible side menu with multiple sections.
component: true
---
<ComponentPreview name="inner-side-menu-demo" peekCode wide />
## Examples
{/* ### Multiple sections */}
{/* <ComponentPreview name="inner-side-menu-multiple-sections" peekCode /> */}
### Collapsible menus
Inner Side Menu uses the `<InnerSideMenuCollapsible />` component to wrap the menu items.
This is a wrapper component around the `<Collapsible />` component.
```jsx {10-12,16-17}
import {
InnerSideMenuCollapsible,
InnerSideMenuCollapsibleTrigger,
InnerSideMenuCollapsibleContent,
InnerSideMenuItem,
} from 'ui-patterns/InnerSideMenu'
function app() {
return (
<InnerSideMenuCollapsible key={category} defaultOpen>
<InnerSideMenuCollapsibleTrigger title={category} />
<InnerSideMenuCollapsibleContent>
<InnerSideMenuItem href="/dashboard">Dashboard</InnerSideMenuItem>
<InnerSideMenuItem href="/team">Team</InnerSideMenuItem>
<InnerSideMenuItem href="/settings">Settings</InnerSideMenuItem>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
)
}
```
### Static titles
If you don't need the Collapsible features, you can instead use `<InnerSideBarTitle />`.
This doesn't require the use of the `<InnerSideMenuCollapsible />` parent component wrapped around it.
You can wrap the `<InnerSideBarItem />` conponents however you like. Below we've put a `<div/ >` around them.
```jsx {1}
<InnerSideBarTitle>Projects</InnerSideBarTitle>
<div className="mt-2">
<InnerSideMenuItem href="/dashboard">Dashboard</InnerSideMenuItem>
<InnerSideMenuItem href="/team">Team</InnerSideMenuItem>
<InnerSideMenuItem href="/settings">Settings</InnerSideMenuItem>
</div>
```
<ComponentPreview name="inner-side-menu-static-titles" />
### Loading state
Use `<InnerSideMenuItemLoading />` to show a loading state for menu items.
This item is a wrapper around the `<Skeleton />` component, and also includes some y-padding so the items don't layout shift when shifting to loaded state and to look visually separated in a list.
<ComponentPreview name="inner-side-menu-loading" />
### With search
Use the following components to add a search input to the menu.
```jsx
<InnerSideBarFilters>
<InnerSideBarFilterSearchInput
name="search-input"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
aria-labelledby="Search items"
/>
</InnerSideBarFilters>
```
There is also a Filter Dropdown component that can be used to sort the menu items.
This can be used inside `<InnerSideBarFilterSearchInput />`.
```jsx {9-16}
<InnerSideBarFilters>
<InnerSideBarFilterSearchInput
name="search-input"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
aria-labelledby="Search items"
>
<InnerSideBarFilterSortDropdown value={sort} onValueChange={(value) => setSort(value)}>
<InnerSideBarFilterSortDropdownItem value="alphabetical">
Sort Alphabetically
</InnerSideBarFilterSortDropdownItem>
<InnerSideBarFilterSortDropdownItem value="reverse">
Sort Reverse Alphabetically
</InnerSideBarFilterSortDropdownItem>
</InnerSideBarFilterSortDropdown>
</InnerSideBarFilterSearchInput>
</InnerSideBarFilters>
```
<ComponentPreview name="inner-side-menu-with-search" />
### Empty list state
Use `<InnerSideMenuEmptyState />` to show an empty state.
Use the `actions` prop to add any actions like a `<Button />`.
```jsx {4-7}
<InnerSideBarEmptyPanel
title="No functions found"
description="Create your first serverless function to get started."
actions={
<Button type="default" onClick={createAction}>
Create Function
</Button>
}
/>
```
You can also use the prop `illustration` to pass in a custom illustration, it accets any React Node.
```jsx {4-7}
<InnerSideBarEmptyPanel
title="No functions found"
description="Create your first serverless function to get started."
illustration={
<figure>
<svg>../</svg>
</figure>
}
/>
```
<ComponentPreview name="inner-side-menu-empty" />
@@ -0,0 +1,21 @@
---
title: Logs Bar Chart
description: A stacked bar chart that displays logs errors and successes.
fragment: true
---
<ComponentPreview name="logs-bar-chart" peekCode wide />
## Usage
This component is used to display the bar chart in logs pages.
It requires that the `data` prop is an array of objects with the following shape:
```ts
type LogsBarChartDatum = {
timestamp: string
ok_count: number
error_count: number
}
```
@@ -12,31 +12,69 @@ fragment: true
```tsx
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
MultiSelectorContent,
MultiSelectorItem,
} from 'ui-patterns/multi-select'
```
```tsx
<MultiSelector values={value} onValuesChange={setValue}>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select items" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="1">Item 1</MultiSelectorItem>
<MultiSelectorItem value="2">Item 2</MultiSelectorItem>
<MultiSelectorItem value="3">Item 3</MultiSelectorItem>
</MultiSelectorList>
<MultiSelector
open={open}
onOpenChange={setOpen}
selected={selected}
onSelectedChange={onSelectedChange}
>
<MultiSelectorTrigger className="w-72" label="Fruits" />
<MultiSelectorContent sameWidthAsTrigger>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorContent>
</MultiSelector>
```
## Examples
### Form
### Disabled
<ComponentPreview name="multi-select-form" />
<ComponentPreview name="multi-select-disabled" />
### Badge Combo Box
Use `MultiSelectorInput`to add a search input.
<ComponentPreview name="multi-select-combobox" />
### Badge Combo Box with creatable selector
creatable: `boolean`
`creatable` prop on the `MultiSelectorList` component can be used to add a new option if no items match
<ComponentPreview name="multi-select-combobox-creatable" />
### Badge Limit
badgeLimit: `number` | `"wrap"`.
`badgeLimit` prop on the `MultiSelectorTrigger` component can be used to limit the number of badges displayed.
<ComponentPreview name="multi-select-badge-limit" />
### Badge Limit="wrap"
`badgeLimit` prop can also be "wrap" to wrap the badges to the next line.
<ComponentPreview name="multi-select-badge-limit-wrap" />
### Deletable Badge
Add the `deletableBadge` prop on the `MultiSelectorTrigger` component to enable badge removal directly from the badge.
<ComponentPreview name="multi-select-deletable-badge" />
### Inline search input
Use `mode="inline-combobox"` on the `MultiSelectorTrigger` component to add an inline search input.
<ComponentPreview name="multi-select-inline-search-input" />
@@ -186,9 +186,3 @@ text-border-control
| `500` |
| `DEFAULT` |
| `600` |
## Storybook
The current colors are also auto documented in Storybook
[Storybook Colors](https://ui-storybook-pre-release.vercel.app/)
+12 -11
View File
@@ -4,25 +4,26 @@
"private": true,
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --port 3003",
"build": "npm run content:build && npm run build:registry && next build",
"build": "pnpm run content:build && pnpm run build:registry && next build",
"build:registry": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache",
"start": "next start",
"lint": "next lint",
"content:build": "contentlayer2 build",
"clean": "rimraf node_modules .next .turbo",
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@hookform/resolvers": "^3.1.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
"contentlayer2": "0.4.6",
"eslint-config-supabase": "*",
"fs": "^0.0.1-security",
"eslint-config-supabase": "workspace:*",
"icons": "workspace:*",
"jotai": "^2.8.0",
"lodash.template": "4.5.0",
"lucide-react": "*",
"markdown-wasm": "^1.2.0",
"next": "^14.2.10",
"next": "catalog:",
"next-contentlayer2": "0.4.6",
"next-themes": "^0.3.0",
"react": "^18.2.0",
@@ -33,32 +34,32 @@
"react-wrap-balancer": "^1.1.0",
"recharts": "^2.12.7",
"rehype-autolink-headings": "^7.1.0",
"rehype-pretty-code": "^0.6.0",
"rehype-pretty-code": "^0.9.0",
"rehype-slug": "^6.0.0",
"remark": "^14.0.3",
"remark-code-import": "^1.2.0",
"remark-gfm": "^4.0.0",
"sonner": "^1.5.0",
"ts-morph": "^22.0.0",
"ui": "*",
"ui-patterns": "*",
"ui": "workspace:*",
"ui-patterns": "workspace:*",
"unist-util-visit": "^5.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@shikijs/compat": "^1.1.7",
"@types/lodash.template": "4.5.0",
"@types/node": "^20",
"@types/react": "^18.2.65",
"@types/react-dom": "^18.2.22",
"autoprefixer": "^10.0.1",
"config": "*",
"config": "workspace:^",
"mdast-util-toc": "^6.1.1",
"postcss": "^8",
"rimraf": "^4.1.3",
"shiki": "^1.1.7",
"tailwindcss": "^3.3.0",
"tsconfig": "*",
"tsconfig": "workspace:*",
"tsx": "^4.7.0",
"typescript": "~5.5.0",
"unist-builder": "3.0.0"
}
@@ -3,7 +3,7 @@
import * as React from 'react'
import { Calendar, MoreHorizontal, Tags, Trash, User } from 'lucide-react'
import { Button, CommandList_Shadcn_ } from 'ui'
import { Button, CommandList_Shadcn_, ScrollArea } from 'ui'
import {
Command_Shadcn_,
CommandEmpty_Shadcn_,
@@ -75,23 +75,25 @@ export default function ComboboxDropdownMenu() {
<DropdownMenuSubContent className="p-0">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Filter label..." autoFocus={true} />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No label found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{labels.map((label) => (
<CommandItem_Shadcn_
key={label}
value={label}
onSelect={(value) => {
setLabel(value)
setOpen(false)
}}
>
{label}
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
<ScrollArea className="h-20">
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No label found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{labels.map((label) => (
<CommandItem_Shadcn_
key={label}
value={label}
onSelect={(value) => {
setLabel(value)
setOpen(false)
}}
>
{label}
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</ScrollArea>
</Command_Shadcn_>
</DropdownMenuSubContent>
</DropdownMenuSub>
@@ -0,0 +1,46 @@
import { Button, DialogSection, DialogSectionSeparator } from 'ui'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from 'ui'
import { Input_Shadcn_ } from 'ui'
import { Label_Shadcn_ } from 'ui'
export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button type="default">Edit Profile</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]" centered={false}>
<DialogHeader padding={'small'}>
<DialogTitle>This dialog is not centered.</DialogTitle>
<DialogDescription>This dialog is not centered.</DialogDescription>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="space-y-4" padding={'small'}>
<div>
<Label_Shadcn_ htmlFor="name" className="text-right">
Name
</Label_Shadcn_>
<Input_Shadcn_ id="name" defaultValue="Pedro Duarte" className="col-span-3" />
</div>
<div>
<Label_Shadcn_ htmlFor="username" className="text-right">
Username
</Label_Shadcn_>
<Input_Shadcn_ id="username" defaultValue="@peduarte" className="col-span-3" />
</div>
</DialogSection>
<DialogFooter padding={'small'}>
<Button htmlType="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,45 @@
import React from 'react'
import { Card } from 'ui'
import {
InnerSideMenuCollapsible,
InnerSideMenuCollapsibleTrigger,
InnerSideMenuCollapsibleContent,
InnerSideMenuItem,
InnerSideMenuSeparator,
} from 'ui-patterns/InnerSideMenu'
export default function InnerSideMenuDemo() {
return (
<Card className="min-w-60 bg-dash-sidebar py-4 flex flex-col gap-6 h-full">
<InnerSideMenuCollapsible defaultOpen>
<InnerSideMenuCollapsibleTrigger title="Projects">Hello</InnerSideMenuCollapsibleTrigger>
<InnerSideMenuCollapsibleContent className="mt-2">
<InnerSideMenuItem href="/">Dashboard</InnerSideMenuItem>
<InnerSideMenuItem href="/">Team</InnerSideMenuItem>
<InnerSideMenuItem href="/">Settings</InnerSideMenuItem>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
<InnerSideMenuSeparator />
<InnerSideMenuCollapsible defaultOpen>
<InnerSideMenuCollapsibleTrigger title="Projects">Hello</InnerSideMenuCollapsibleTrigger>
<InnerSideMenuCollapsibleContent className="mt-2">
<InnerSideMenuItem href="/" title="Dashboard">
Dashboard
</InnerSideMenuItem>
<InnerSideMenuItem href="/">Team</InnerSideMenuItem>
<InnerSideMenuItem href="/">Settings</InnerSideMenuItem>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
<InnerSideMenuCollapsible defaultOpen>
<InnerSideMenuCollapsibleTrigger title="Projects">Hello</InnerSideMenuCollapsibleTrigger>
<InnerSideMenuCollapsibleContent className="mt-2">
<InnerSideMenuItem href="/" title="Dashboard">
Dashboard
</InnerSideMenuItem>
<InnerSideMenuItem href="/">Team</InnerSideMenuItem>
<InnerSideMenuItem href="/">Settings</InnerSideMenuItem>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
</Card>
)
}
@@ -0,0 +1,74 @@
import React from 'react'
import {
InnerSideMenuCollapsible,
InnerSideMenuCollapsibleTrigger,
InnerSideMenuCollapsibleContent,
InnerSideBarShimmeringLoaders,
InnerSideBarEmptyPanel,
InnerSideMenuItem,
InnerSideMenuSeparator,
} from 'ui-patterns/InnerSideMenu'
import { Button, Card } from 'ui'
import { Heart, Pointer } from 'lucide-react'
export default function InnerSideMenuEmpty() {
const [isLoading, setIsLoading] = React.useState(true)
const [hasItems, setHasItems] = React.useState(false)
React.useEffect(() => {
// Simulate loading
const timer = setTimeout(() => {
setIsLoading(false)
}, 2000)
return () => clearTimeout(timer)
}, [])
return (
<Card className="w-64 py-4 flex flex-col gap-4 bg-dash-sidebar">
<InnerSideMenuCollapsible defaultOpen>
<InnerSideMenuCollapsibleTrigger title="Functions" />
<InnerSideMenuCollapsibleContent className="pt-2">
<InnerSideBarEmptyPanel
className="mx-2"
title="No functions found"
description="Create your first serverless function to get started."
illustration={<div className="text-4xl">🚀</div>}
actions={
<Button type="default" onClick={() => setHasItems(true)}>
Create Function
</Button>
}
/>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
<InnerSideMenuSeparator />
<InnerSideMenuCollapsible defaultOpen>
<InnerSideMenuCollapsibleTrigger title="Functions" />
<InnerSideMenuCollapsibleContent className="pt-2">
<InnerSideBarEmptyPanel
className="mx-2"
title="No functions found"
description="Create your first serverless function to get started."
illustration={
<figure className="relative mb-3">
<div className="h-6 w-6 bg-surface-100 border rounded-md flex items-center justify-center">
<Heart className="text-light" size={13} />
</div>
<Pointer
className="absolute -right-[6px] -bottom-2 text-lighter"
strokeWidth={1.5}
size={16}
/>
</figure>
}
actions={
<Button type="default" onClick={() => setHasItems(true)}>
Create Function
</Button>
}
/>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
</Card>
)
}
@@ -0,0 +1,32 @@
import { Card } from 'ui'
import {
InnerSideMenuCollapsible,
InnerSideMenuCollapsibleContent,
InnerSideMenuCollapsibleTrigger,
InnerSideMenuItemLoading,
InnerSideMenuSeparator,
} from 'ui-patterns/InnerSideMenu'
export default function InnerSideMenuLoading() {
return (
<Card className="w-64 py-4 flex flex-col gap-4 bg-dash-sidebar">
<InnerSideMenuCollapsible defaultOpen>
<InnerSideMenuCollapsibleTrigger title="Functions" />
<InnerSideMenuCollapsibleContent className="pt-2">
<InnerSideMenuItemLoading />
<InnerSideMenuItemLoading />
<InnerSideMenuItemLoading />
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
<InnerSideMenuSeparator />
<InnerSideMenuCollapsible defaultOpen>
<InnerSideMenuCollapsibleTrigger title="Functions" />
<InnerSideMenuCollapsibleContent className="pt-2">
<InnerSideMenuItemLoading />
<InnerSideMenuItemLoading />
<InnerSideMenuItemLoading />
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
</Card>
)
}
@@ -0,0 +1,32 @@
import React from 'react'
import {
InnerSideMenuCollapsible,
InnerSideMenuCollapsibleTrigger,
InnerSideMenuCollapsibleContent,
InnerSideMenuItem,
InnerSideMenuSeparator,
} from 'ui-patterns/InnerSideMenu'
export default function InnerSideMenuMultipleSections() {
return (
<div className="w-64">
<InnerSideMenuCollapsible>
<InnerSideMenuCollapsibleTrigger title="Development" />
<InnerSideMenuCollapsibleContent>
<InnerSideMenuItem href="#" isActive>
API
</InnerSideMenuItem>
<InnerSideMenuItem href="#">Database</InnerSideMenuItem>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
<InnerSideMenuSeparator />
<InnerSideMenuCollapsible>
<InnerSideMenuCollapsibleTrigger title="Analytics" />
<InnerSideMenuCollapsibleContent>
<InnerSideMenuItem href="#">Reports</InnerSideMenuItem>
<InnerSideMenuItem href="#">Usage</InnerSideMenuItem>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
</div>
)
}
@@ -0,0 +1,40 @@
import React from 'react'
import { Card } from 'ui'
import {
InnerSideMenuCollapsible,
InnerSideMenuCollapsibleTrigger,
InnerSideMenuCollapsibleContent,
InnerSideMenuItem,
InnerSideMenuSeparator,
InnerSideBarTitle,
} from 'ui-patterns/InnerSideMenu'
export default function InnerSideMenuBasic() {
return (
<Card className="min-w-60 bg-dash-sidebar py-4 flex flex-col gap-6 h-full">
<div className="px-2">
<InnerSideBarTitle className="mb-2">Projects</InnerSideBarTitle>
<InnerSideMenuItem href="/">Dashboard</InnerSideMenuItem>
<InnerSideMenuItem href="/">Team</InnerSideMenuItem>
<InnerSideMenuItem href="/">Settings</InnerSideMenuItem>
</div>
<InnerSideMenuSeparator />
<div className="px-2">
<InnerSideBarTitle className="mb-2">Projects</InnerSideBarTitle>
<InnerSideMenuItem href="/" title="Dashboard">
Dashboard
</InnerSideMenuItem>
<InnerSideMenuItem href="/">Team</InnerSideMenuItem>
<InnerSideMenuItem href="/">Settings</InnerSideMenuItem>
</div>
<div className="px-2">
<InnerSideBarTitle className="mb-2">Projects</InnerSideBarTitle>
<InnerSideMenuItem href="/" title="Dashboard">
Dashboard
</InnerSideMenuItem>
<InnerSideMenuItem href="/">Team</InnerSideMenuItem>
<InnerSideMenuItem href="/">Settings</InnerSideMenuItem>
</div>
</Card>
)
}
@@ -0,0 +1,92 @@
import React, { useState, useMemo } from 'react'
import { Card } from 'ui'
import {
InnerSideMenuCollapsible,
InnerSideMenuCollapsibleTrigger,
InnerSideMenuCollapsibleContent,
InnerSideMenuItem,
InnerSideBarFilters,
InnerSideBarFilterSearchInput,
InnerSideBarFilterSortDropdown,
InnerSideBarFilterSortDropdownItem,
InnerSideBarEmptyPanel,
} from 'ui-patterns/InnerSideMenu'
export default function InnerSideMenuWithSearch() {
const [sort, setSort] = useState('alphabetical')
const [searchTerm, setSearchTerm] = useState('')
const foodCategories = {
Fruits: ['Apple', 'Banana', 'Orange', 'Strawberry'],
Vegetables: ['Carrot', 'Broccoli', 'Spinach', 'Tomato'],
Grains: ['Rice', 'Wheat', 'Oats', 'Quinoa'],
Proteins: ['Chicken', 'Beef', 'Fish', 'Tofu'],
}
const filteredAndSortedCategories = useMemo(() => {
return Object.entries(foodCategories).reduce(
(acc, [category, items]) => {
let filteredItems = items.filter((item) =>
item.toLowerCase().includes(searchTerm.toLowerCase())
)
// Sort the filtered items
filteredItems.sort((a, b) => {
if (sort === 'alphabetical') {
return a.localeCompare(b)
} else {
return b.localeCompare(a)
}
})
acc[category] = filteredItems
return acc
},
{} as Record<string, string[]>
)
}, [foodCategories, searchTerm, sort])
return (
<Card className="w-64 py-4 flex flex-col gap-4 bg-dash-sidebar">
<InnerSideBarFilters>
<InnerSideBarFilterSearchInput
name="search-input"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
aria-labelledby="Search items"
>
<InnerSideBarFilterSortDropdown value={sort} onValueChange={(value) => setSort(value)}>
<InnerSideBarFilterSortDropdownItem value="alphabetical">
Sort Alphabetically
</InnerSideBarFilterSortDropdownItem>
<InnerSideBarFilterSortDropdownItem value="reverse">
Sort Reverse Alphabetically
</InnerSideBarFilterSortDropdownItem>
</InnerSideBarFilterSortDropdown>
</InnerSideBarFilterSearchInput>
</InnerSideBarFilters>
{Object.entries(filteredAndSortedCategories).map(([category, items]) => (
<InnerSideMenuCollapsible key={category} defaultOpen>
<InnerSideMenuCollapsibleTrigger title={category} />
<InnerSideMenuCollapsibleContent className="pt-2">
{items.length > 0 ? (
items.map((item) => (
<InnerSideMenuItem key={item} href="#">
{item}
</InnerSideMenuItem>
))
) : (
<InnerSideBarEmptyPanel
className="mx-2"
title={`No ${category.toLowerCase()} found`}
description={`Your search did not return any results in ${category}`}
/>
)}
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
))}
</Card>
)
}
@@ -0,0 +1,21 @@
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
export default function LogsBarChartDemo() {
const data = Array.from({ length: 100 }, (_, i) => {
const date = new Date()
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
return {
timestamp: date.toISOString(),
ok_count: Math.floor(Math.random() * 100), // Random value 0-99
error_count: Math.floor(Math.random() * 50), // Random value 0-50
warning_count: Math.floor(Math.random() * 50), // Random value 0-50
}
}).reverse()
return (
<div className="w-full">
<LogsBarChart data={data} />
</div>
)
}
@@ -0,0 +1,45 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>([
'Apple',
'Banana',
'Cherry',
'Date',
'Elderberrie',
])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger
className="w-72"
label="Select fruits"
persistLabel
badgeLimit="wrap"
showIcon={false}
deletableBadge={false}
/>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
@@ -0,0 +1,48 @@
import { Minus, Plus } from 'lucide-react'
import { useState } from 'react'
import { Button } from 'ui'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>(['Apple', 'Banana', 'Cherry'])
const [limit, setLimit] = useState(2)
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2">
<Button size="tiny" type="default" onClick={() => setLimit(limit - 1)} disabled={limit < 1}>
<Minus size={12} />
</Button>
<span className="text-sm text-foreground/90 peer-checked:line-through font-semibold hover:cursor-pointer">
Limit: {limit}
</span>
<Button size="tiny" type="default" onClick={() => setLimit(limit + 1)}>
<Plus size={12} />
</Button>
</div>
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger className="w-72" label="Select fruits" badgeLimit={limit} />
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
</div>
)
}
@@ -0,0 +1,34 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>([])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger className="w-72" label="Select fruits" badgeLimit="wrap" />
<MultiSelectorContent>
<MultiSelectorInput placeholder="Search fruits" showResetIcon />
<MultiSelectorList creatable>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
@@ -0,0 +1,34 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>([])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger className="w-72" label="Select fruits" badgeLimit="wrap" />
<MultiSelectorContent>
<MultiSelectorInput placeholder="Search fruits" showResetIcon />
<MultiSelectorList>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
@@ -0,0 +1,37 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>(['Apple', 'Banana'])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger
className="w-72"
label="Select fruits"
deletableBadge
badgeLimit="wrap"
showIcon={false}
/>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
@@ -2,25 +2,42 @@ import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [value, setValue] = useState<string[]>([])
const [selectedValues, setSelectedValues] = useState<string[]>([])
const fruits = [
{ value: 'Apple', isDisabled: false },
{ value: 'Banana', isDisabled: false },
{ value: 'Cherry', isDisabled: false },
{ value: 'Date', isDisabled: false },
{ value: 'Elderberrie', isDisabled: false },
{ value: 'Fig', isDisabled: false },
{ value: 'Grape', isDisabled: false },
{ value: 'Kiwi', isDisabled: true },
{ value: 'Mango', isDisabled: false },
{ value: 'Strawberry', isDisabled: false },
]
return (
<MultiSelector values={value} onValuesChange={setValue} size="small">
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select items" />
</MultiSelectorTrigger>
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger
className="w-72"
label="Select fruits"
badgeLimit="wrap"
showIcon={false}
/>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="1">Item 1</MultiSelectorItem>
<MultiSelectorItem value="2">Item 2</MultiSelectorItem>
<MultiSelectorItem value="3">Item 3</MultiSelectorItem>
{fruits.map(({ value, isDisabled }) => (
<MultiSelectorItem key={value} value={value} disabled={isDisabled}>
{value}
</MultiSelectorItem>
))}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
@@ -0,0 +1,18 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>([])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues} disabled={true}>
<MultiSelectorTrigger className="w-72" label="Select fruits" />
</MultiSelector>
)
}
@@ -1,105 +0,0 @@
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import Image from 'next/image'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import z from 'zod'
import {
Button,
Form_Shadcn_,
FormDescription_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormLabel_Shadcn_,
FormMessage_Shadcn_,
} from 'ui'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
const form = z.object({
value: z.array(z.string()).nonempty('Please select at least one person'),
})
type Form = z.infer<typeof form>
const users = [
{
name: 'ThePrimeagen',
picture: 'https://pbs.twimg.com/profile_images/1759330620160049152/2i_wkOoK_400x400.jpg',
},
{
name: 'Shadcn',
picture: 'https://pbs.twimg.com/profile_images/1593304942210478080/TUYae5z7_400x400.jpg',
},
{
name: 'Theo',
picture: 'https://pbs.twimg.com/profile_images/1605762947686375425/lsoGWWty_400x400.jpg',
},
]
const MultiSelectZod = () => {
const multiForm = useForm<Form>({
resolver: zodResolver(form),
defaultValues: form.parse({ value: [users[0].name] }),
})
const onSubmit = (data: Form) => {
toast('Form submitted ', { description: JSON.stringify(data, null, 2) })
}
return (
<Form_Shadcn_ {...multiForm}>
<form onSubmit={multiForm.handleSubmit(onSubmit)} className="space-y-3 grid gap-3 w-full">
<FormField_Shadcn_
control={multiForm.control}
name="value"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormLabel_Shadcn_>Invite people</FormLabel_Shadcn_>
<MultiSelector onValuesChange={field.onChange} values={field.value} size={'small'}>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select people to invite" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
{users.map((user) => (
<MultiSelectorItem key={user.name} value={user.name}>
<div className="flex items-center space-x-2">
<Image
src={user.picture}
alt={user.name}
width={32}
height={32}
className="w-8 h-8 rounded-full"
/>
<span>{user.name}</span>
</div>
</MultiSelectorItem>
))}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
<FormDescription_Shadcn_>
Select people to invite to this event
</FormDescription_Shadcn_>
<FormMessage_Shadcn_ />
</FormItem_Shadcn_>
)}
/>
<Button type="secondary" size="medium" htmlType="submit">
Submit
</Button>
</form>
</Form_Shadcn_>
)
}
export default MultiSelectZod
@@ -0,0 +1,39 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>(['Apple'])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger
mode="inline-combobox"
className="w-72"
label="Select fruits"
deletableBadge
badgeLimit="wrap"
showIcon={false}
/>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
@@ -1,22 +1,16 @@
import { Button } from 'ui'
import {
Tooltip_Shadcn_,
TooltipContent_Shadcn_,
TooltipProvider_Shadcn_,
TooltipTrigger_Shadcn_,
} from 'ui'
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
export default function TooltipDemo() {
return (
<TooltipProvider_Shadcn_>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button type="outline">Hover</Button>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_>
</TooltipTrigger>
<TooltipContent>
<p>Add to library</p>
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
</TooltipProvider_Shadcn_>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
@@ -97,7 +97,6 @@ export default function TreeViewDemo() {
name={element.name}
isEditing={element.metadata?.isEditing === true}
onEditSubmit={(value) => {
console.log('value', value)
let updatedTreeData = { ...treeData }
const findNode = (node: any) => {
if (node.id === element.id) {
+73 -2
View File
@@ -431,6 +431,12 @@ export const examples: Registry = [
registryDependencies: ['dialog', 'button'],
files: ['example/dialog-close-button.tsx'],
},
{
name: 'dialog-centered-off',
type: 'components:example',
registryDependencies: ['dialog', 'button'],
files: ['example/dialog-centered-off.tsx'],
},
{
name: 'drawer-demo',
type: 'components:example',
@@ -1030,15 +1036,75 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/info-tooltip-demo.tsx'],
},
{
name: 'inner-side-menu-demo',
type: 'components:example',
files: ['example/inner-side-menu-demo.tsx'],
},
{
name: 'inner-side-menu-static-titles',
type: 'components:example',
files: ['example/inner-side-menu-static-titles.tsx'],
},
{
name: 'inner-side-menu-loading',
type: 'components:example',
files: ['example/inner-side-menu-loading.tsx'],
},
{
name: 'inner-side-menu-multiple-sections',
type: 'components:example',
files: ['example/inner-side-menu-multiple-sections.tsx'],
},
{
name: 'inner-side-menu-empty',
type: 'components:example',
files: ['example/inner-side-menu-empty.tsx'],
},
{
name: 'inner-side-menu-with-search',
type: 'components:example',
files: ['example/inner-side-menu-with-search.tsx'],
},
{
name: 'multi-select-demo',
type: 'components:example',
files: ['example/multi-select-demo.tsx'],
},
{
name: 'multi-select-form',
name: 'multi-select-disabled',
type: 'components:example',
files: ['example/multi-select-form.tsx'],
files: ['example/multi-select-disabled.tsx'],
},
{
name: 'multi-select-badge-limit-wrap',
type: 'components:example',
files: ['example/multi-select-badge-limit-wrap.tsx'],
},
{
name: 'multi-select-badge-limit',
type: 'components:example',
files: ['example/multi-select-badge-limit.tsx'],
},
{
name: 'multi-select-combobox',
type: 'components:example',
files: ['example/multi-select-combobox.tsx'],
},
{
name: 'multi-select-combobox-creatable',
type: 'components:example',
files: ['example/multi-select-combobox-creatable.tsx'],
},
{
name: 'multi-select-deletable-badge',
type: 'components:example',
files: ['example/multi-select-deletable-badge.tsx'],
},
{
name: 'multi-select-inline-search-input',
type: 'components:example',
files: ['example/multi-select-inline-search-input.tsx'],
},
{
name: 'tree-view-demo',
@@ -1140,4 +1206,9 @@ export const examples: Registry = [
type: 'docs:example',
files: ['example/expanding-textarea-demo.tsx'],
},
{
name: 'logs-bar-chart',
type: 'components:example',
files: ['example/logs-bar-chart.tsx'],
},
]
+1
View File
@@ -9,6 +9,7 @@ module.exports = config({
//
'./../../packages/ui/src/**/*.{tsx,ts,js}',
'./../../packages/ui-patterns/**/*.{tsx,ts,js}',
'!./../../packages/ui-patterns/node_modules/**/*', // ignore node_modules rule
],
theme: {
extend: {
+6
View File
@@ -0,0 +1,6 @@
# IMPORTANT: This file is checked into version control.
# Only publicly available environment variables should be listed here.
# Do not add secrets here. Use .env.local instead.
NEXT_PUBLIC_MISC_URL="https://obuldanrptloktxcffvn.supabase.co"
NEXT_PUBLIC_MISC_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTg2MTQ2ODUsImV4cCI6MjAzNDE5MDY4NX0.NFt49g6DFkc1X5khCzN5p01iAVo2TMxlx88cY1V0E2M"
+3
View File
@@ -26,3 +26,6 @@ yarn-error.log*
# Sitemap
public/sitemap.xml
# Copied examples folder
/examples/
+1 -1
View File
@@ -77,7 +77,7 @@ You can usually identify a federated or reference doc because it uses a Next.js
Example spec file import:
```js
import specFile from '~/spec/transforms/analytics_v0_openapi_deparsed.json' assert { type: 'json' }
import specFile from '~/spec/transforms/analytics_v0_openapi_deparsed.json' with { type: 'json' }
```
Example repo definition:
+1 -1
View File
@@ -14,7 +14,7 @@ For a complete run-down on how all of our tools work together, see the main DEVE
## Local setup
[supabase.com/docs](https://supabase.com/docs) is a Next.JS site. You can get setup by following the same steps for all of our other Next.JS projects:
[supabase.com/docs](https://supabase.com/docs) is a Next.js site. You can get setup by following the same steps for all of our other Next.js projects:
1. Follow the steps outlined in the Local Development section of the main [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md)
2. If you work at Supabase, run `dev:secrets:pull` to pull down the internal environment variables. If you're a community member, create a `.env` file and add this line to it: `NEXT_PUBLIC_IS_PLATFORM=false`
+1 -1
View File
@@ -31,7 +31,7 @@ We use documentation specifications which can be used to generate human-readable
- CLISpec (custom to Supabase): for CLI commands and usage.
The benefit of using custom specifications is that we can generate many other types from a strict schema (eg, HTML and manpages).
It also means that we can switch to any documentation system we want. On this site we use Next.JS, but on Supabase's official website, we use a custom React site and expose only a subset of the available API for each tool.
It also means that we can switch to any documentation system we want. On this site we use Next.js, but on Supabase's official website, we use a custom React site and expose only a subset of the available API for each tool.
## Contributing
+1 -1
View File
@@ -6,7 +6,7 @@ import OpenAI from 'openai'
export const runtime = 'edge'
/* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that
overlap with the OpenAI API regions.
Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list
Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories
*/
+4 -2
View File
@@ -167,7 +167,7 @@ const PI = 3.14
```
````
Of, you can use the `<$CodeSample />` component to include code samples from a source code file.
Or, you can use the `<$CodeSample />` component to include code samples from a source code file.
If the file is within the `supabase/supabase` repo's `examples` directory:
@@ -183,7 +183,7 @@ meta="display/path.js"
/>
```
If the file is within some other GitHub repo (note that the repo must be public):
If the file is within some other GitHub repo:
```mdx
<$CodeSample
@@ -197,6 +197,8 @@ meta="display/path.js"
/>
```
The repo must be public, the org must be on the allow list, and the commit must be an immutable SHA (not a mutable tag or branch name).
### Icons
The following icons are available. They can be styled with [Tailwind](https://tailwindcss.com/) classes:
+1 -1
View File
@@ -12,7 +12,7 @@ export default async function ContributingPage() {
const content = await readFile(contentFile, 'utf-8')
return (
<SidebarSkeleton>
<SidebarSkeleton hideSideNav>
<div className="px-8 py-16">
<article className="prose mx-auto">
<MDXProviderGuides>
@@ -1,19 +0,0 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
type Params = { slug: string[] }
const GuidePage = async ({ params }: { params: Params }) => {
const data = await getGuidesMarkdown(params)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams()
const generateMetadata = genGuideMeta(getGuidesMarkdown)
export default GuidePage
export { generateStaticParams, generateMetadata }
@@ -1,16 +0,0 @@
'use client'
import { usePathname } from 'next/navigation'
import { type PropsWithChildren } from 'react'
import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils'
import Layout from '~/layouts/guides'
const GuidesLayout = ({ children }: PropsWithChildren) => {
const pathname = usePathname()
const menuId = getMenuId(pathname)
return <Layout menuId={menuId}>{children}</Layout>
}
export default GuidesLayout
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const AiGuidePage = async ({ params }: { params: Params }) => {
const slug = ['ai', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('ai')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['ai', ...(params.slug ?? [])])
)
export default AiGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function AiLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -1,15 +1,17 @@
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import type { SerializeOptions } from 'next-mdx-remote/dist/types'
import { notFound } from 'next/navigation'
import { relative } from 'path'
import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
export const dynamicParams = false
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'vecs'
@@ -84,7 +86,8 @@ const getContent = async ({ slug }: Params) => {
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
const content = await response.text()
let content = await response.text()
content = removeRedundantH1(content)
return {
pathname: `/guides/ai/python/${slug}` satisfies `/${string}`,
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const ApiGuidePage = async ({ params }: { params: Params }) => {
const slug = ['api', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('api')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['api', ...(params.slug ?? [])])
)
export default ApiGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function ApiLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const AuthGuidePage = async ({ params }: { params: Params }) => {
const slug = ['auth', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('auth')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['auth', ...(params.slug ?? [])])
)
export default AuthGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function AuthLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const CronGuidePage = async ({ params }: { params: Params }) => {
const slug = ['cron', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('cron')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['cron', ...(params.slug ?? [])])
)
export default CronGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function CronLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const DatabaseGuidePage = async ({ params }: { params: Params }) => {
const slug = ['database', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('database')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['database', ...(params.slug ?? [])])
)
export default DatabaseGuidePage
export { generateStaticParams, generateMetadata }
@@ -1,14 +1,15 @@
import matter from 'gray-matter'
import { type Heading } from 'mdast'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { toMarkdown } from 'mdast-util-to-markdown'
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import { readFile } from 'node:fs/promises'
import { join, relative } from 'node:path'
import rehypeSlug from 'rehype-slug'
import emoji from 'remark-emoji'
import { genGuideMeta, genGuidesStaticParams } from '~/features/docs/GuidesMdx.utils'
import {
genGuideMeta,
genGuidesStaticParams,
removeRedundantH1,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs'
@@ -16,7 +17,8 @@ import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLin
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import remarkGfm from 'remark-gfm'
export const dynamicParams = false
// We fetch these docs at build time from an external repo
const org = 'supabase'
@@ -184,22 +186,7 @@ const getContent = async (params: Params) => {
const rawContent = await response.text()
const { content: contentWithoutFrontmatter } = matter(rawContent)
// This is the more robust way of doing it, but problems with the rewritten
// Markdown and handling of tables this way, so saving it for later.
//
// const mdxTree = fromMarkdown(contentWithoutFrontmatter)
// const maybeH1 = mdxTree.children[0]
// if (maybeH1 && maybeH1.type === 'heading' && (maybeH1 as Heading).depth === 1) {
// mdxTree.children.shift()
// }
// content = toMarkdown(mdxTree)
content = contentWithoutFrontmatter
if (meta.title) {
const h1Regex = new RegExp(`(?:^|\n)# ${meta.title}\n+`)
content = content.replace(h1Regex, '')
}
content = removeRedundantH1(contentWithoutFrontmatter)
}
return {
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function DatabaseLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const DeploymentGuidePage = async ({ params }: { params: Params }) => {
const slug = ['deployment', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('deployment')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['deployment', ...(params.slug ?? [])])
)
export default DeploymentGuidePage
export { generateStaticParams, generateMetadata }
@@ -3,7 +3,7 @@ import { notFound } from 'next/navigation'
import { relative } from 'node:path'
import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
@@ -11,6 +11,8 @@ import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
export const dynamicParams = false
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'setup-cli'
@@ -83,7 +85,7 @@ const getContent = async ({ slug }: Params) => {
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
const content = await response.text()
const content = removeRedundantH1(await response.text())
return {
pathname: `/guides/cli/github-action/${slug}` satisfies `/${string}`,
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function DeploymentLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -3,7 +3,7 @@ import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import { notFound } from 'next/navigation'
import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { isValidGuideFrontmatter } from '~/lib/docs'
@@ -18,6 +18,8 @@ import {
terraformDocsRepo,
} from '../terraformConstants'
export const dynamicParams = false
// Each external docs page is mapped to a local page
const pageMap = [
{
@@ -119,7 +121,11 @@ const getContent = async ({ slug }: Params) => {
let rawContent = await response.text()
// Strip out HTML comments
rawContent = rawContent.replace(/<!--.*?-->/, '')
const { content, data } = matter(rawContent)
let { content, data } = matter(rawContent)
// Remove the title from the content so it isn't duplicated in the final display
content = removeRedundantH1(content)
Object.assign(meta, data)
if (!isValidGuideFrontmatter(meta)) {
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const FunctionsGuidePage = async ({ params }: { params: Params }) => {
const slug = ['functions', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('functions')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['functions', ...(params.slug ?? [])])
)
export default FunctionsGuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function FunctionsLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const GettingStartedGuidePage = async ({ params }: { params: Params }) => {
const slug = ['getting-started', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('getting-started')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['getting-started', ...(params.slug ?? [])])
)
export default GettingStartedGuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,98 @@
import matter from 'gray-matter'
import type { Code, Heading, Root } from 'mdast'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { toMarkdown } from 'mdast-util-to-markdown'
import { readdir, readFile, stat } from 'node:fs/promises'
import { basename, extname, join } from 'node:path'
import { cache } from 'react'
import { visit, EXIT } from 'unist-util-visit'
import { EXAMPLES_DIRECTORY } from '~/lib/docs'
const PROMPTS_DIRECTORY = join(EXAMPLES_DIRECTORY, 'prompts')
function parseMarkdown(markdown: string) {
const { content: withoutFrontmatter } = matter(markdown)
const mdast = fromMarkdown(withoutFrontmatter)
let heading = ''
visit(mdast, 'heading', (node: Heading) => {
if (node.depth === 1) {
if ('value' in node.children[0]) {
heading = node.children[0].value
}
return EXIT
}
})
const codeBlock: Code = {
type: 'code',
lang: 'markdown',
value: markdown,
}
const root: Root = {
type: 'root',
children: [codeBlock],
}
const content = toMarkdown(root)
return { heading, content }
}
async function getAiPromptsImpl() {
const directoryContents = await readdir(PROMPTS_DIRECTORY)
const prompts = directoryContents
.filter(async (file) => {
if (extname(file) !== '.md') {
return false
}
const fileStats = await stat(join(PROMPTS_DIRECTORY, file))
const isFile = fileStats.isFile()
return isFile
})
.map(async (filename) => {
const rawContent = await readFile(join(PROMPTS_DIRECTORY, filename), 'utf-8')
const { heading, content } = parseMarkdown(rawContent)
return {
filename: basename(filename, '.md'),
heading,
content,
}
})
return (await Promise.all(prompts)).sort((a, b) => b.filename.localeCompare(a.filename))
}
export const getAiPrompts = cache(getAiPromptsImpl)
async function getAiPromptImpl(prompt: string) {
const filePath = join(PROMPTS_DIRECTORY, `${prompt}.md`)
try {
const rawContent = await readFile(filePath, 'utf-8')
const { heading, content } = parseMarkdown(rawContent)
return { heading, content }
} catch (err) {
console.error('Failed to fetch prompt from repo: %o', err)
}
}
export const getAiPrompt = cache(getAiPromptImpl)
export async function generateAiPromptMetadata({ params: { slug } }: { params: { slug: string } }) {
const prompt = await getAiPrompt(slug)
return {
title: `AI Prompt: ${prompt.heading} | Supabase Docs`,
}
}
export async function generateAiPromptsStaticParams() {
const prompts = await getAiPrompts()
return prompts.map((prompt) => {
return {
slug: prompt.filename,
}
})
}
@@ -0,0 +1,26 @@
import Link from 'next/link'
import { GlassPanel } from 'ui-patterns'
import { getAiPrompts } from './AiPrompts.utils'
export async function AiPromptsIndex() {
const prompts = await getAiPrompts()
return (
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-6 not-prose">
{prompts.map((prompt) => (
<Link
key={prompt.filename}
href={`/guides/getting-started/ai-prompts/${prompt.filename}`}
passHref
>
<GlassPanel
key={prompt.filename}
title={prompt.heading ?? prompt.filename.replaceAll('-', ' ')}
/>
</Link>
))}
</div>
)
}
@@ -0,0 +1,34 @@
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import {
generateAiPromptMetadata,
generateAiPromptsStaticParams,
getAiPrompt,
} from './AiPrompts.utils'
export const dynamicParams = false
export default async function AiPromptsPage({ params: { slug } }: { params: { slug: string } }) {
let { heading, content } = await getAiPrompt(slug)
content = `
## How to use
Copy the prompt to a file in your repo.
Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use \`#<filename>\`, in Cursor, use \`@Files\`, and in Zed, use \`/file\`.
## Prompt
${content}
`.trim()
return (
<GuideTemplate
meta={{ title: `AI Prompt: ${heading}` }}
content={content}
editLink={newEditLink(`supabase/supabase/blob/master/examples/prompts/${slug}.md`)}
/>
)
}
export const generateMetadata = generateAiPromptMetadata
export const generateStaticParams = generateAiPromptsStaticParams
@@ -0,0 +1,20 @@
import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
import Layout from '~/layouts/guides'
import { getAiPrompts } from '../getting-started/ai-prompts/[slug]/AiPrompts.utils'
export default async function GettingStartedLayout({ children }: { children: React.ReactNode }) {
const additionalNavItems = { prompts: await getPrompts() }
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
async function getPrompts() {
const prompts = await getAiPrompts()
return prompts.map(
(prompt) =>
({
name: prompt.heading,
url: `/guides/getting-started/ai-prompts/${prompt.filename}`,
}) as Partial<NavMenuSection>
)
}
@@ -5,12 +5,14 @@ import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { fetchRevalidatePerDay_TEMP_TESTING } from '~/features/helpers.fetch'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
export const dynamicParams = false
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'pg_graphql'
@@ -133,7 +135,7 @@ const getContent = async ({ slug }: Params) => {
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
const response = await fetchRevalidatePerDay(
const response = await fetchRevalidatePerDay_TEMP_TESTING(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function GraphQlLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const IntegrationsGuidePage = async ({ params }: { params: Params }) => {
const slug = ['integrations', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('integrations')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['integrations', ...(params.slug ?? [])])
)
export default IntegrationsGuidePage
export { generateStaticParams, generateMetadata }
Loaded 100 of 3283 files, more files were not shown because too many files have changed in this diff. Show more