diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..bba2e99 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + parserOptions: { + ecmaVersion: "latest" + }, + ignorePatterns: ['packages/config-eslint', 'packages/config-typescript'] +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f906a12 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @theopenlane/blacksmiths \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..6a1b23e --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,21 @@ +# Add 'bug' label to any PR where the head branch name starts with `bug` or has a `bug` section in the name +bug: + - head-branch: ["^bug", "bug"] +# Add 'enhancement' label to any PR where the head branch name starts with `enhancement` or has a `enhancement` section in the name +enhancement: + - head-branch: ["^enhancement", "enhancement", "^feature", "feature", "^enhance", "enhance", "^feat", "feat"] +# Add 'breaking-change' label to any PR where the head branch name starts with `breaking-change` or has a `breaking-change` section in the name +breaking-change: + - head-branch: ["^breaking-change", "breaking-change"] +ci: + - changed-files: + - any-glob-to-any-file: .github/** + - any-glob-to-any-file: .buildkite/** +local-development: + - changed-files: + - any-glob-to-any-file: scripts/** + - any-glob-to-any-file: Taskfile.yaml + - any-glob-to-any-file: docker/** +cli: + - changed-files: + - any-glob-to-any-file: cmd/** diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..37df9bc --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,24 @@ +changelog: + exclude: + labels: + - ignore-for-release + authors: [] + categories: + - title: Breaking Changes 🛠 + labels: + - Semver-Major + - breaking-change + - title: New Features 🎉 + labels: + - Semver-Minor + - enhancement + - feature + - title: Bug Fixes 🐛 + labels: + - bug + - title: 👒 Dependencies + labels: + - dependencies + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/labeler.yaml b/.github/workflows/labeler.yaml new file mode 100644 index 0000000..fc43cb1 --- /dev/null +++ b/.github/workflows/labeler.yaml @@ -0,0 +1,13 @@ +name: "Pull Request Labeler" +on: + - pull_request_target +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + sync-labels: true diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml new file mode 100644 index 0000000..c1ec02e --- /dev/null +++ b/.github/workflows/releaser.yml @@ -0,0 +1,127 @@ +name: Release +on: + workflow_dispatch: + release: + types: [created] +permissions: + contents: write +jobs: + ldflags_args: + runs-on: ubuntu-latest + outputs: + commit-date: ${{ steps.ldflags.outputs.commit-date }} + commit: ${{ steps.ldflags.outputs.commit }} + version: ${{ steps.ldflags.outputs.version }} + tree-state: ${{ steps.ldflags.outputs.tree-state }} + steps: + - id: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: ldflags + run: | + echo "commit=$GITHUB_SHA" >> $GITHUB_OUTPUT + echo "commit-date=$(git log --date=iso8601-strict -1 --pretty=%ct)" >> $GITHUB_OUTPUT + echo "version=$(git describe --tags --always --dirty | cut -c2-)" >> $GITHUB_OUTPUT + echo "tree-state=$(if git diff --quiet; then echo "clean"; else echo "dirty"; fi)" >> $GITHUB_OUTPUT + release: + name: Build and release + needs: + - ldflags_args + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + permissions: + contents: write # To add assets to a release. + id-token: write # To do keyless signing with cosign + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: true + - name: Install Syft + uses: anchore/sbom-action/download-syft@ab9d16d4b419c9d1a02df5213fa0ebe965ca5a57 # v0.17.1 + - name: Install Cosign + uses: sigstore/cosign-installer@v3.6.0 + - name: Run GoReleaser + id: run-goreleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + VERSION: ${{ needs.ldflags_args.outputs.version }} + COMMIT: ${{ needs.ldflags_args.outputs.commit }} + COMMIT_DATE: ${{ needs.ldflags_args.outputs.commit-date }} + TREE_STATE: ${{ needs.ldflags_args.outputs.tree-state }} + - name: Generate subject + id: hash + env: + ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" + run: | + set -euo pipefail + hashes=$(echo $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0) + if test "$hashes" = ""; then # goreleaser < v1.13.0 + checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path') + hashes=$(cat $checksum_file | base64 -w0) + fi + echo "hashes=$hashes" >> $GITHUB_OUTPUT + provenance: + name: Generate provenance (SLSA3) + needs: + - release + permissions: + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + contents: write # To add assets to a release. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 + with: + base64-subjects: "${{ needs.release.outputs.hashes }}" + upload-assets: true # upload to a new release + verification: + name: Verify provenance of assets (SLSA3) + needs: + - release + - provenance + runs-on: ubuntu-latest + permissions: read-all + steps: + - name: Install the SLSA verifier + uses: slsa-framework/slsa-verifier/actions/installer@v2.6.0 + - name: Download assets + env: + GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CHECKSUMS: "${{ needs.release.outputs.hashes }}" + ATT_FILE_NAME: "${{ needs.provenance.outputs.provenance-name }}" + run: | + set -euo pipefail + checksums=$(echo "$CHECKSUMS" | base64 -d) + while read -r line; do + fn=$(echo $line | cut -d ' ' -f2) + echo "Downloading $fn" + gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$fn" + done <<<"$checksums" + gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$ATT_FILE_NAME" + - name: Verify assets + env: + CHECKSUMS: "${{ needs.release.outputs.hashes }}" + PROVENANCE: "${{ needs.provenance.outputs.provenance-name }}" + run: |- + set -euo pipefail + checksums=$(echo "$CHECKSUMS" | base64 -d) + while read -r line; do + fn=$(echo $line | cut -d ' ' -f2) + echo "Verifying SLSA provenance for $fn" + slsa-verifier verify-artifact --provenance-path "$PROVENANCE" \ + --source-uri "github.com/$GITHUB_REPOSITORY" \ + --source-tag "$GITHUB_REF_NAME" \ + "$fn" + done <<<"$checksums" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7138a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +build +.swc/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +# ui +dist/ + +# vscode +.vscode/ + diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ded82e2 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers = true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..58d6355 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +default_stages: [pre-commit] +fail_fast: true +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + exclude: jsonschema/api-docs.md + - id: detect-private-key + - repo: https://github.com/google/yamlfmt + rev: v0.13.0 + hooks: + - id: yamlfmt + - repo: https://github.com/crate-ci/typos + rev: v1.24.1 + hooks: + - id: typos diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..a7ea9f7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "semi": false +} \ No newline at end of file diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..81ea939 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,20 @@ +[files] +extend-exclude = ["go.mod","go.sum"] +ignore-hidden = true +ignore-files = true +ignore-dot = true +ignore-vcs = true +ignore-global = true +ignore-parent = true + +[default] +binary = false +check-filename = true +check-file = true +unicode = true +ignore-hex = true +identifier-leading-digits = false +locale = "en" +extend-ignore-identifiers-re = [] +extend-ignore-words-re = ["(?i)requestor","(?i)indentity","(?i)encrypter","(?i)seeked","(?i)generater"] +extend-ignore-re = ["#\\s*spellchecker:off\\s*\\n.*\\n\\s*#\\s*spellchecker:on"] \ No newline at end of file diff --git a/.yamlfmt b/.yamlfmt new file mode 100644 index 0000000..f6cfc8b --- /dev/null +++ b/.yamlfmt @@ -0,0 +1,4 @@ +exclude: + - config/ +formatter: + retain_line_breaks: true \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9..a3d69de 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright [2024] [The Open Lane, Inc.] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 946b431..2e16b38 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # openlane-ui -the openlane ui + +Where the openlane UI will be housed - stay tuned! diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..65036cb --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,64 @@ +version: '3' + +env: + ENV: config + +dotenv: ['.env', '{{.ENV}}/.env'] + +includes: + codegen: + taskfile: ./packages/codegen/Taskfile.yaml + dir: ./packages/codegen + +tasks: + build: + desc: build all apps and package + cmd: bun run build + + install: + desc: install the dependencies listed in package.json + cmds: + - bun install + + dev: + desc: develop all apps + cmds: + - task: install + - bun dev + + dev:docs: + desc: develop docs + cmds: + - task: install + - bun dev --filter={apps/docs} + + dev:operator: + desc: develop operator + cmds: + - task: install + - bun dev --filter={apps/operator} + + dev:web: + desc: develop web + cmds: + - task: install + - bun dev --filter={apps/web} + + dev:storybook: + desc: develop storybook + cmds: + - task: install + - bun dev --filter={apps/storybook} + + build:web: + desc: build web + cmds: + - task: install + - bun run build --filter={apps/web} + + precommit-full: + desc: Lint the project against all files + cmds: + - pre-commit install && pre-commit install-hooks + - pre-commit autoupdate + - pre-commit run --show-diff-on-failure --color=always --all-files diff --git a/apps/console/.eslintrc.js b/apps/console/.eslintrc.js new file mode 100644 index 0000000..e9c84f5 --- /dev/null +++ b/apps/console/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["@repo/eslint-config/next.js"], +}; diff --git a/apps/console/README.md b/apps/console/README.md new file mode 100644 index 0000000..d364535 --- /dev/null +++ b/apps/console/README.md @@ -0,0 +1,28 @@ +## Getting Started + +First, run the development server: + +```bash +yarn dev +``` + +Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3001/api/hello](http://localhost:3001/api/hello). + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/console/next-auth.d.ts b/apps/console/next-auth.d.ts new file mode 100644 index 0000000..53f2852 --- /dev/null +++ b/apps/console/next-auth.d.ts @@ -0,0 +1,41 @@ +import { DefaultUser } from 'next-auth'; +import { JwtPayload } from 'jsonwebtoken'; + +/** + * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context + */ +declare module 'next-auth' { + interface Session { + user: DefaultUser & { + accessToken: string; + refreshToken: string; + organization: string; + + }; + } + interface User extends DefaultUser { + accessToken: string; + refreshToken: string; + session: string; + } + interface Profile extends DefaultProfile { + display_name: string; + first_name: string; + last_name: string; + } +} + +declare module "@auth/core/jwt" { + interface JWT { + user: { + accessToken: string; + refreshToken: string; + } + } +} + +declare module "@jsonwebtoken" { + interface JwtPayload extends DefaultJwtPayload { + org?: string; + } +} \ No newline at end of file diff --git a/apps/console/next-env.d.ts b/apps/console/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/apps/console/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/console/next.config.js b/apps/console/next.config.js new file mode 100644 index 0000000..dccb0a4 --- /dev/null +++ b/apps/console/next.config.js @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + reactStrictMode: true, + transpilePackages: ['@repo/dally', '@repo/ui', '@repo/codegen'], + experimental: { + missingSuspenseWithCSRBailout: false, + }, +} diff --git a/apps/console/package.json b/apps/console/package.json new file mode 100644 index 0000000..b1d28ce --- /dev/null +++ b/apps/console/package.json @@ -0,0 +1,59 @@ +{ + "name": "operator", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev -p 3001", + "build": "next build", + "start": "next start -p 3001", + "lint": "next lint" + }, + "dependencies": { + "@jsonforms/react": "^3.2.1", + "@jsonforms/vanilla-renderers": "^3.2.1", + "@nextui-org/react": "^2.2.10", + "@novu/node": "^0.24.1", + "@novu/notification-center": "^0.24.1", + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-popover": "^1.0.7", + "@repo/codegen": "workspace:*", + "@repo/dally": "workspace:*", + "@repo/ui": "workspace:*", + "@simplewebauthn/browser": "^10.0.0", + "@simplewebauthn/server": "^10.0.0", + "@types/jsonwebtoken": "^9.0.6", + "@uidotdev/usehooks": "^2.4.1", + "graphql-request": "latest", + "graphql-tag": "^2.12.6", + "jwt-decode": "^4.0.0", + "lucide-react": "^0.424.0", + "next": "latest", + "next-auth": "beta", + "next-themes": "^0.3.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-dropzone": "^14.2.3", + "react-easy-crop": "^5.0.7", + "swr": "latest", + "urql": "^4.0.7", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@headlessui/react": "latest", + "@next/eslint-plugin-next": "latest", + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@repo/tailwind-config": "workspace:*", + "@types/eslint": "latest", + "@types/node": "latest", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "autoprefixer": "latest", + "clsx": "latest", + "eslint": "latest", + "postcss": "latest", + "tailwindcss": "latest", + "tailwind-variants": "latest", + "typescript": "latest" + } +} \ No newline at end of file diff --git a/apps/console/postcss.config.js b/apps/console/postcss.config.js new file mode 100644 index 0000000..9e24cdf --- /dev/null +++ b/apps/console/postcss.config.js @@ -0,0 +1,9 @@ +// If you want to use other PostCSS plugins, see the following: +// https://tailwindcss.com/docs/using-with-preprocessors + +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/console/tailwind.config.ts b/apps/console/tailwind.config.ts new file mode 100644 index 0000000..f62d5a4 --- /dev/null +++ b/apps/console/tailwind.config.ts @@ -0,0 +1,29 @@ +import type { Config } from 'tailwindcss' +import sharedConfig from '@repo/tailwind-config' + +const config: Pick< + Config, + 'darkMode' | 'content' | 'presets' | 'prefix' | 'theme' +> = { + darkMode: 'class', + content: ['./src/app/**/*.tsx', './src/components/**/*.tsx'], + presets: [sharedConfig], + theme: { + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + }, +} + +export default config diff --git a/apps/console/tsconfig.json b/apps/console/tsconfig.json new file mode 100644 index 0000000..5f5fbc5 --- /dev/null +++ b/apps/console/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "@repo/typescript-config/nextjs.json", + "compilerOptions": { + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + }, + "strictNullChecks": true + }, + "include": [ + "next-env.d.ts", + "next-auth.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "postcss.config.js", + "../../packages/config-typescript/typings.d.ts" + ], + "exclude": ["node_modules", "dist", "build", "coverage", ".next", ".turbo"] +} diff --git a/apps/storybook/.eslintrc.js b/apps/storybook/.eslintrc.js new file mode 100644 index 0000000..1651e4c --- /dev/null +++ b/apps/storybook/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + root: true, + extends: [ + "@repo/eslint-config", + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "plugin:storybook/recommended", + ], + plugins: ["react-refresh"], + rules: { + "react-refresh/only-export-components": "warn", + "tailwindcss/no-custom-classname": "off", + "tailwindcss/classnames-order": "off", + }, + settings: { + tailwindcss: { + callees: ["cn", "cva"], + config: "tailwind.config.ts", + }, + }, +} diff --git a/apps/storybook/.gitignore b/apps/storybook/.gitignore new file mode 100644 index 0000000..395f879 --- /dev/null +++ b/apps/storybook/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +storybook-static \ No newline at end of file diff --git a/apps/storybook/tailwind.config.ts b/apps/storybook/tailwind.config.ts new file mode 100644 index 0000000..3eaf0c4 --- /dev/null +++ b/apps/storybook/tailwind.config.ts @@ -0,0 +1,12 @@ +import type { Config } from "tailwindcss" +import sharedConfig from "@repo/tailwind-config" + +const config = { + content: [ + "./src/stories/**/*.{ts,tsx}", + "../../packages/ui/components/**/*.{ts,tsx}", + ], + presets: [sharedConfig], +} satisfies Config + +export default config diff --git a/apps/storybook/tsconfig.json b/apps/storybook/tsconfig.json new file mode 100644 index 0000000..33d9d83 --- /dev/null +++ b/apps/storybook/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@repo/typescript-config/nextjs.json", + "compilerOptions": { + "strictNullChecks": true, + "declaration": false, + "declarationMap": false, + "plugins": [] + }, + "include": [ + "src", + "**/*.ts", + "**/*.tsx", + "vite.config.ts", + ], + "exclude": [ + "node_modules" + ] +} diff --git a/config/.env-example b/config/.env-example new file mode 100644 index 0000000..e80c2ed --- /dev/null +++ b/config/.env-example @@ -0,0 +1,26 @@ +AUTH_SECRET: $(openssl rand -base64 32) + +API_REST_URL: http://localhost:17608 +NEXT_PUBLIC_OPENLANE_URL: http://localhost:17608 +SESSION_COOKIE_NAME: temporary-cookie +SESSION_COOKIE_EXPIRATION_MINUTES: 60 +NOVU_API_KEY: novu-api-key +NEXT_PUBLIC_NOVU_APP_IDENTIFIER: novu-app-identifier + +# Registered auth providers +AUTH_GITHUB_ID: fakeid +AUTH_GITHUB_SECRET: fakesecret +AUTH_GOOGLE_ID: fakeid.apps.googleusercontent.com +AUTH_GOOGLE_SECRET: fakesecret + +# Sanity +NEXT_PUBLIC_SANITY_DATASET: fakedataset +NEXT_PUBLIC_SANITY_PROJECT_ID: fakeid +SANITY_API_DATASET: fakedataset +SANITY_API_PROJECT_ID: fakeprojectid +SANITY_API_READ_TOKEN: fakereadtoken +SANITY_API_WRITE_TOKEN: fakewritetoken +SANITY_STUDIO_DATASET: fakedataset +SANITY_STUDIO_PROJECT_ID: fakeprojectid +OPENLANE_API_WRITE_TOKEN: faketoken + diff --git a/package.json b/package.json new file mode 100644 index 0000000..653b915 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "openlane-ui", + "version": "1.0.0", + "description": "A monorepo for openlane UI apps and supporting libraries", + "repository": "git@github.com:theopenlane/openlane-ui.git", + "author": "openlane ", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "turbo build", + "dev": "turbo dev --parallel", + "type-check": "turbo type-check", + "clean": "rm -rf node_modules/** && rm -rf ./apps/operator/.next && rm -rf ./apps/docs/.next && rm -rf ./apps/web/.next && turbo clean", + "lint": "turbo lint", + "format": "prettier --write \"**/*.{ts,tsx,md}\"" + }, + "devDependencies": { + "dotenv-cli": "latest", + "eslint": "latest", + "prettier": "latest", + "prettier-plugin-tailwindcss": "latest", + "turbo": "1.13" + }, + "engines": { + "node": ">=18" + }, + "packageManager": "bun@1.1.3", + "workspaces": [ + "apps/*", + "packages/*" + ], + "trustedDependencies": [ + "core-js" + ], + "resolutions": { + "react": "18.3.1" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "dependencies": { + "@react-pdf/renderer": "^3.4.4" + } +} \ No newline at end of file diff --git a/packages/codegen/README.md b/packages/codegen/README.md new file mode 100644 index 0000000..7866afc --- /dev/null +++ b/packages/codegen/README.md @@ -0,0 +1,7 @@ +# GraphQL code generator + +[@graphql-codegen](https://the-guild.dev/graphql/codegen) is what generated everything in this directory, using the configuration `graphql-codegen.yml`. Comments were placed in the file during evaluation of the tool but you may want to lookup the respective definitions so you understand what controls what. + +To get up and running you should be able to do a `npm install` in this directory which will setup all the necessary dependencies related to the plugins via the `package.json`, and running the generator can be completed via `npm run codegen` in this directory. If there are any errors with the configuration or your output it will update you within the terminal. + +More to come! \ No newline at end of file diff --git a/packages/codegen/Taskfile.yaml b/packages/codegen/Taskfile.yaml new file mode 100644 index 0000000..2dd46c0 --- /dev/null +++ b/packages/codegen/Taskfile.yaml @@ -0,0 +1,12 @@ +version: '3' + +tasks: + codegen: + desc: run generate + cmds: + - task: generate + - task: clean + + generate: + desc: run generate + cmd: bun run codegen diff --git a/packages/codegen/graphql-codegen.yml b/packages/codegen/graphql-codegen.yml new file mode 100644 index 0000000..3ee13b7 --- /dev/null +++ b/packages/codegen/graphql-codegen.yml @@ -0,0 +1,49 @@ +config: + strict: true + useTypeImports: true + exposeDocument: true # For each generate query hook adds document field with a corresponding GraphQL query. Useful for queryClient.fetchQuery + exposeQueryKeys: true # For each generate query hook adds getKey(variables: QueryVariables) function. Useful for cache updates. If addInfiniteQuery is true, it will also add a getKey function to each infinite query + exposeMutationKeys: true # For each generate mutation hook adds getKey() function. Useful for call outside of functional component + exposeFetcher: true # For each generate query hook adds fetcher field with a corresponding GraphQL query using the fetcher. It is useful for queryClient.fetchQuery and queryClient.prefetchQuery + maybeValue: T | null + declarationKind: interface + preResolveTypes: true + onlyOperationTypes: true + flattenGeneratedTypes: true + namingConvention: + enumValues: keep + scalars: + DateTime: string + Date: string + Decimal: number + UUID: string + ID: string + JSON: any + Upload: File + schema: yup +overwrite: true +schema: https://raw.githubusercontent.com/theopenlane/core/main/schema.graphql +documents: './query/*.graphql' +generates: + ./src/introspectionschema.json: + plugins: + - introspection + config: + ./src/schema.ts: + # preset: near-operation-file + # presetConfig: + # baseTypesPath: 'types.ts' + plugins: + - typescript # This plugin generates the base TypeScript types, based on your GraphQL schema + - typescript-operations # This plugin generates TypeScript types based on your GraphQLSchema and your GraphQL operations and fragments. It generates types for your GraphQL documents: Query, Mutation, Subscription and Fragment + # - typescript-resolvers # This plugin generates TypeScript signature for resolve functions of your GraphQL API. You can use this plugin to generate simple resolvers signature based on your GraphQL types, or you can change its behavior be providing custom model types (mappers) + # - typescript-graphql-request + # - plugin-typescript-swr + # - typescript-react-query # This plugin generates React-Query Hooks with TypeScript typings + - typescript-urql # This plugin generates urql (https://github.com/FormidableLabs/urql) components and HOC with TypeScript typings + # - typescript-react-apollo + # - typed-document-node # These plugins generate a ready-to-use TypedDocumentNode (a combination of pre-compiled DocumentNode and the TypeScript signature it represents) + # - typescript-mock-data + # - typescript-validation-schema # A plugin for GraphQL Codegen to generate form validation schema (such as yup, zod) based on your GraphQL schema for use in a client application + - add: + content: '/* eslint-disable */' diff --git a/packages/codegen/package.json b/packages/codegen/package.json new file mode 100644 index 0000000..5008826 --- /dev/null +++ b/packages/codegen/package.json @@ -0,0 +1,32 @@ +{ + "name": "@repo/codegen", + "version": "0.0.0", + "dependencies": { + "@graphql-tools/schema": "^10.0.0", + "graphql": "^16.8.1", + "graphql-codegen-plugin-typescript-swr": "^0.8.5", + "graphql-codegen-typescript-validation-schema": "^0.16.0", + "graphql-request": "^7.0.0", + "typescript": "^5.2.2" + }, + "devDependencies": { + "@graphql-codegen/add": "^5.0.0", + "@graphql-codegen/cli": "5.0.2", + "@graphql-codegen/introspection": "^4.0.0", + "@graphql-codegen/near-operation-file-preset": "^3.0.0", + "@graphql-codegen/relay-operation-optimizer": "^1.11.2", + "@graphql-codegen/typed-document-node": "^5.0.1", + "@graphql-codegen/typescript": "4.0.9", + "@graphql-codegen/typescript-document-nodes": "4.0.9", + "@graphql-codegen/typescript-graphql-request": "^6.2.0", + "@graphql-codegen/typescript-operations": "^4.0.1", + "@graphql-codegen/typescript-react-apollo": "^4.1.0", + "@graphql-codegen/typescript-react-query": "^6.0.0", + "@graphql-codegen/typescript-resolvers": "4.2.1", + "@graphql-codegen/typescript-urql": "^4.0.0", + "@graphql-codegen/urql-introspection": "^3.0.0" + }, + "scripts": { + "codegen": "graphql-codegen --config graphql-codegen.yml" + } +} \ No newline at end of file diff --git a/packages/codegen/query/documentdata.graphql b/packages/codegen/query/documentdata.graphql new file mode 100644 index 0000000..abdec0c --- /dev/null +++ b/packages/codegen/query/documentdata.graphql @@ -0,0 +1,33 @@ +query GetDocumentData($documentDataId: ID!) { + documentData(id: $documentDataId) { + id + templateID + data + } +} + +mutation CreateDocumentData($input: CreateDocumentDataInput!) { + createDocumentData(input: $input) { + documentData { + id + templateID + data + } + } +} + +mutation UpdateDocumentData($updateDocumentDataId: ID!, $input: UpdateDocumentDataInput!) { + updateDocumentData(id: $updateDocumentDataId, input: $input) { + documentData { + id + templateID + data + } + } +} + +mutation DeleteDocumentData($deleteDocumentDataId: ID!) { + deleteDocumentData(id: $deleteDocumentDataId) { + deletedID + } +} \ No newline at end of file diff --git a/packages/codegen/query/group.graphql b/packages/codegen/query/group.graphql new file mode 100644 index 0000000..24d7567 --- /dev/null +++ b/packages/codegen/query/group.graphql @@ -0,0 +1,20 @@ +query GetAllGroups { + groups { + edges { + node { + id + name + description + displayName + logoURL + setting { + visibility + joinPolicy + syncToSlack + syncToGithub + tags + } + } + } + } +} diff --git a/packages/codegen/query/organsation.graphql b/packages/codegen/query/organsation.graphql new file mode 100644 index 0000000..0ea5638 --- /dev/null +++ b/packages/codegen/query/organsation.graphql @@ -0,0 +1,135 @@ +query GetAllOrganizations { + organizations { + edges { + node { + id + name + displayName + avatarRemoteURL + description + personalOrg + parent { + id + name + } + children { + edges { + node { + id + name + displayName + description + } + } + } + members { + id + role + user { + id + firstName + lastName + } + } + setting { + id + createdAt + updatedAt + createdBy + updatedBy + domains + billingContact + billingEmail + billingPhone + billingAddress + taxIdentifier + tags + geoLocation + } + createdAt + updatedAt + } + } + } +} + +query GetOrganizationNameByID($organizationId: ID!) { + organization(id: $organizationId) { + name + displayName + } +} + +query GetOrganizationMembers($organizationId: ID!) { + organization(id: $organizationId) { + members { + id + createdAt + role + user { + id + firstName + lastName + authProvider + avatarRemoteURL + email + role + createdAt + } + } + } +} + +query GetInvites { + invites { + edges { + node { + id + recipient + status + createdAt + expires + role + } + } + } +} + +mutation CreateOrganization($input: CreateOrganizationInput!) { + createOrganization(input: $input) { + organization { + id + } + } +} + +mutation UpdateOrganization( + $updateOrganizationId: ID! + $input: UpdateOrganizationInput! +) { + updateOrganization(id: $updateOrganizationId, input: $input) { + organization { + id + } + } +} + +mutation CreateBulkInvite($input: [CreateInviteInput!]) { + createBulkInvite(input: $input) { + invites { + id + } + } +} + +mutation DeleteOrganizationInvite($deleteInviteId: ID!) { + deleteInvite(id: $deleteInviteId) { + deletedID + } +} + +mutation DeleteOrganization($deleteOrganizationId: ID!) { + deleteOrganization(id: $deleteOrganizationId) { + deletedID + } +} diff --git a/packages/codegen/query/subscribe.graphql b/packages/codegen/query/subscribe.graphql new file mode 100644 index 0000000..971e2c2 --- /dev/null +++ b/packages/codegen/query/subscribe.graphql @@ -0,0 +1,7 @@ +mutation CreateSubscriber($input: CreateSubscriberInput!) { + createSubscriber(input: $input) { + subscriber { + email + } + } +} \ No newline at end of file diff --git a/packages/codegen/query/template.graphql b/packages/codegen/query/template.graphql new file mode 100644 index 0000000..d357e9c --- /dev/null +++ b/packages/codegen/query/template.graphql @@ -0,0 +1,72 @@ +mutation CreateTemplate($input: CreateTemplateInput!) { + createTemplate(input: $input) { + template { + id + name + templateType + description + jsonconfig + uischema + owner { + id + } + } + } +} + +mutation UpdateTemplate($updateTemplateId: ID!, $input: UpdateTemplateInput!) { + updateTemplate(id: $updateTemplateId, input: $input) { + template { + id + name + templateType + description + jsonconfig + uischema + owner { + id + } + } + } +} + +query GetAllTemplates { + templates { + edges { + node { + id + name + templateType + description + jsonconfig + uischema + } + } + } +} + +query FilterTemplates($where: TemplateWhereInput) { + templates(where: $where) { + edges { + node { + id + name + templateType + description + jsonconfig + uischema + } + } + } +} + +query GetTemplate($getTemplateId: ID!) { + template(id: $getTemplateId) { + id + templateType + name + description + jsonconfig + uischema + } +} \ No newline at end of file diff --git a/packages/codegen/query/tokens.graphql b/packages/codegen/query/tokens.graphql new file mode 100644 index 0000000..37b0c7a --- /dev/null +++ b/packages/codegen/query/tokens.graphql @@ -0,0 +1,26 @@ +mutation CreatePersonalAccessToken($input: CreatePersonalAccessTokenInput!) { + createPersonalAccessToken(input: $input) { + personalAccessToken { + token + } + } +} + +query GetPersonalAccessTokens { + personalAccessTokens { + edges { + node { + id + name + description + expiresAt + } + } + } +} + +mutation DeletePersonalAccessToken($deletePersonalAccessTokenId: ID!) { + deletePersonalAccessToken(id: $deletePersonalAccessTokenId) { + deletedID + } +} diff --git a/packages/codegen/query/user.graphql b/packages/codegen/query/user.graphql new file mode 100644 index 0000000..d1af8de --- /dev/null +++ b/packages/codegen/query/user.graphql @@ -0,0 +1,22 @@ +query GetUserProfile($userId: ID!) { + user(id: $userId) { + id + firstName + lastName + displayName + email + avatarRemoteURL + setting { + status + tags + } + } +} + +mutation updateUserName($updateUserId: ID!, $input: UpdateUserInput!) { + updateUser(id: $updateUserId, input: $input) { + user { + id + } + } +} diff --git a/packages/codegen/src/.gitkeep b/packages/codegen/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/config-eslint/README.md b/packages/config-eslint/README.md new file mode 100644 index 0000000..8b42d90 --- /dev/null +++ b/packages/config-eslint/README.md @@ -0,0 +1,3 @@ +# `@turbo/eslint-config` + +Collection of internal eslint configurations. diff --git a/packages/config-eslint/library.js b/packages/config-eslint/library.js new file mode 100644 index 0000000..770b4b5 --- /dev/null +++ b/packages/config-eslint/library.js @@ -0,0 +1,40 @@ +const { resolve } = require("node:path"); + +const project = resolve(process.cwd(), "tsconfig.json"); + +module.exports = { + extends: [ + "@vercel/style-guide/eslint/node", + "@vercel/style-guide/eslint/typescript", + ].map(require.resolve), + plugins: [ + "eslint-plugin-tsdoc", + ], + parserOptions: { + project, + }, + globals: { + React: true, + JSX: true, + }, + settings: { + "import/resolver": { + typescript: { + project, + }, + node: { + extensions: [".mjs", ".js", ".jsx", ".ts", ".tsx"], + }, + }, + }, + ignorePatterns: ["node_modules/", "dist/"], + rules: { + "import/no-default-export": 0, + "no-console": "warn", + "no-unsafe-unassignment": 0, + "tsdoc/syntax": "warn", + "@typescript-eslint/no-unsafe-argument": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-floating-promises": 0, + }, +}; diff --git a/packages/config-eslint/next.js b/packages/config-eslint/next.js new file mode 100644 index 0000000..177ba9f --- /dev/null +++ b/packages/config-eslint/next.js @@ -0,0 +1,58 @@ +const { resolve } = require('node:path') + +const project = resolve(process.cwd(), 'tsconfig.json') + +module.exports = { + extends: [ + '@vercel/style-guide/eslint/node', + '@vercel/style-guide/eslint/typescript', + '@vercel/style-guide/eslint/browser', + '@vercel/style-guide/eslint/react', + '@vercel/style-guide/eslint/next', + 'plugin:@typescript-eslint/recommended', + 'eslint-config-turbo', + ].map(require.resolve), + plugins: ['eslint-plugin-tsdoc', 'import'], + parserOptions: { + project, + }, + globals: { + React: true, + JSX: true, + }, + settings: { + 'import/resolver': { + typescript: { + project, + }, + node: { + extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx'], + }, + }, + }, + ignorePatterns: ['node_modules/', 'dist/', '.next/', '.turbo/', 'coverage/'], + // add rules configurations here + rules: { + 'import/no-default-export': 0, + 'no-console': 1, + 'no-unsafe-unassignment': 0, + 'tsdoc/syntax': 0, + 'react/function-component-definition': [ + 'off', + { + namedComponents: 'arrow-function', + unnamedComponents: 'arrow-function', + }, + ], + '@typescript-eslint/no-unsafe-assignment': 0, + '@typescript-eslint/no-unsafe-unassignment': 0, + '@typescript-eslint/no-unsafe-member-access': 0, + '@typescript-eslint/no-unsafe-return': 0, + '@typescript-eslint/no-unsafe-call': 0, + '@typescript-eslint/no-unsafe-argument': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-floating-promises': 0, + '@typescript-eslint/explicit-function-return-type': 0, + '@typescript-eslint/restrict-template-expressions': 0, + }, +} diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json new file mode 100644 index 0000000..be145cd --- /dev/null +++ b/packages/config-eslint/package.json @@ -0,0 +1,10 @@ +{ + "name": "@repo/eslint-config", + "version": "0.0.0", + "private": true, + "devDependencies": { + "@vercel/style-guide": "latest", + "eslint-config-turbo": "latest", + "eslint-plugin-tsdoc": "latest" + } +} \ No newline at end of file diff --git a/packages/config-eslint/react.js b/packages/config-eslint/react.js new file mode 100644 index 0000000..0ac0985 --- /dev/null +++ b/packages/config-eslint/react.js @@ -0,0 +1,52 @@ +const { resolve } = require("node:path"); + +const project = resolve(process.cwd(), "tsconfig.json"); + +module.exports = { + extends: [ + "@vercel/style-guide/eslint/typescript", + "@vercel/style-guide/eslint/browser", + "@vercel/style-guide/eslint/react", + ].map(require.resolve), + plugins: [ + "eslint-plugin-tsdoc", 'import', + ], + parserOptions: { + project, + }, + globals: { + JSX: true, + }, + settings: { + "import/resolver": { + typescript: { + project, + }, + node: { + extensions: [".mjs", ".js", ".jsx", ".ts", ".tsx"], + }, + }, + }, + ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js", "**/*.css"], + // add rules configurations here + rules: { + "import/no-default-export": 0, + "no-console": 1, + "@typescript-eslint/no-unsafe-assignment": 0, + "@typescript-eslint/no-unsafe-unassignment": 0, + "@typescript-eslint/no-unsafe-member-access": 0, + "@typescript-eslint/no-unsafe-return": 0, + "@typescript-eslint/no-unsafe-call": 0, + "no-alert": 1, + "tsdoc/syntax": 0, + "@typescript-eslint/no-confusing-void-expression": 0, + "react/function-component-definition": ["off", { + namedComponents: "arrow-function", + unnamedComponents: "arrow-function", + }], + "@typescript-eslint/no-unsafe-argument": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-floating-promises": 0, + "@typescript-eslint/explicit-function-return-type": 0 + }, +}; diff --git a/packages/config-typescript/base.json b/packages/config-typescript/base.json new file mode 100644 index 0000000..eb9dc29 --- /dev/null +++ b/packages/config-typescript/base.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "jsx": "react", + "inlineSources": false, + "isolatedModules": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": true + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/packages/config-typescript/nextjs.json b/packages/config-typescript/nextjs.json new file mode 100644 index 0000000..40ac104 --- /dev/null +++ b/packages/config-typescript/nextjs.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Next.js", + "extends": "./base.json", + "compilerOptions": { + "plugins": [ + { + "name": "next" + } + ], + "allowJs": true, + "declaration": false, + "declarationMap": false, + "incremental": true, + "jsx": "preserve", + "lib": [ + "DOM", + "DOM.Iterable", + "esnext" + ], + "module": "esnext", + "resolveJsonModule": true, + "strict": true, + "target": "es5", + "downlevelIteration": true, + }, + "include": [ + "src", + "next-env.d.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/packages/config-typescript/package.json b/packages/config-typescript/package.json new file mode 100644 index 0000000..3efea07 --- /dev/null +++ b/packages/config-typescript/package.json @@ -0,0 +1,9 @@ +{ + "name": "@repo/typescript-config", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/config-typescript/react-library.json b/packages/config-typescript/react-library.json new file mode 100644 index 0000000..512282e --- /dev/null +++ b/packages/config-typescript/react-library.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "React Library", + "extends": "./base.json", + "compilerOptions": { + "lib": [ + "ES2015", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "target": "ES6", + "jsx": "react-jsx", + "noEmit": true, + "downlevelIteration": true + } +} \ No newline at end of file diff --git a/packages/dally/.eslintrc.js b/packages/dally/.eslintrc.js new file mode 100644 index 0000000..9a0d767 --- /dev/null +++ b/packages/dally/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["@repo/eslint-config/library.js"], +}; diff --git a/packages/dally/package.json b/packages/dally/package.json new file mode 100644 index 0000000..a2565a0 --- /dev/null +++ b/packages/dally/package.json @@ -0,0 +1,32 @@ +{ + "name": "@repo/dally", + "version": "0.0.0", + "private": true, + "sideEffects": [ + "**/*.js" + ], + "scripts": { + "build": "tsc", + "lint": "eslint src/", + "dev": "tsc --watch", + "type-check": "tsc --noEmit" + }, + "exports": { + "./auth": "./src/index.ts", + "./user": "./src/lib/user.ts" + }, + "peerDependencies": { + "react": "latest" + }, + "devDependencies": { + "swr": "latest", + "dotenv": "latest", + "@repo/eslint-config": "workspace:*", + "@types/node": "latest", + "@types/eslint": "latest", + "eslint": "latest", + "react": "latest", + "tsup": "latest", + "typescript": "latest" + } +} \ No newline at end of file diff --git a/packages/dally/src/index.ts b/packages/dally/src/index.ts new file mode 100644 index 0000000..72d8ee1 --- /dev/null +++ b/packages/dally/src/index.ts @@ -0,0 +1,5 @@ +export const restUrl = process.env.API_REST_URL! +export const openlaneAPIUrl = process.env.NEXT_PUBLIC_OPENLANE_URL! +export const openlaneGQLUrl = `${process.env.NEXT_PUBLIC_OPENLANE_URL!}/query` +export const sessionCookieName = process.env.SESSION_COOKIE_NAME +export const sessionCookieExpiration = process.env.SESSION_COOKIE_EXPIRATION_MINUTES \ No newline at end of file diff --git a/packages/dally/src/lib/user.ts b/packages/dally/src/lib/user.ts new file mode 100644 index 0000000..6bad10a --- /dev/null +++ b/packages/dally/src/lib/user.ts @@ -0,0 +1,46 @@ +import { restUrl } from '../index.ts' + +export interface LoginUser { + username: string + password: string +} + +export interface RegisterUser { + username: string + password: string + confirmedPassword?: string +} + +export async function registerUser(arg: RegisterUser) { + const fData = await fetch(`${restUrl}/v1/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(arg), + }) + + if (fData.ok) { + return await fData.json() + } + + if (!fData.ok) { + return { + message: await fData.text(), + } + } +} + +export async function verifyUser(token: string) { + const fData = await fetch(`${restUrl}/v1/verify?${token}`) + + if (fData.ok) { + return await fData.json() + } + + if (!fData.ok) { + return { + message: await fData.text(), + } + } +} diff --git a/packages/dally/tsconfig.json b/packages/dally/tsconfig.json new file mode 100644 index 0000000..940bd69 --- /dev/null +++ b/packages/dally/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@repo/typescript-config/base.json", + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/tailwind-config/.eslintrc.js b/packages/tailwind-config/.eslintrc.js new file mode 100644 index 0000000..848ace8 --- /dev/null +++ b/packages/tailwind-config/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@repo/eslint-config/react.js'], +} diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json new file mode 100644 index 0000000..befdbd4 --- /dev/null +++ b/packages/tailwind-config/package.json @@ -0,0 +1,15 @@ +{ + "name": "@repo/tailwind-config", + "version": "0.0.0", + "private": true, + "exports": { + ".": "./tailwind.config.ts" + }, + "devDependencies": { + "autoprefixer": "latest", + "postcss-import": "latest", + "tailwindcss": "latest", + "@tailwindcss/forms": "latest", + "tailwind-variants": "latest" + } +} diff --git a/packages/tailwind-config/postcss.config.js b/packages/tailwind-config/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/packages/tailwind-config/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/packages/tailwind-config/tailwind.config.ts b/packages/tailwind-config/tailwind.config.ts new file mode 100644 index 0000000..25d12b0 --- /dev/null +++ b/packages/tailwind-config/tailwind.config.ts @@ -0,0 +1,111 @@ +import type { Config } from 'tailwindcss' +import forms from '@tailwindcss/forms' + +export const config: Omit = { + plugins: [forms], + theme: { + extend: { + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + boxShadow: { + auth: '0px 8.671px 17.343px -8.671px rgba(0, 0, 0, 0.10)', + popover: '0px 8.671px 17.343px -8.671px rgba(0, 0, 0, 0.10)', + }, + letterSpacing: { + tighter: '-0.01rem', + heading: '-0.038rem', + }, + colors: { + blackberry: { + 900: '#312847', + 800: '#443A5B', + 700: '#62587A', + 600: '#72688C', + 500: '#9C94B0', + 400: '#BBB5C6', + 300: '#D5D1DC', + 200: '#DFDCE5', + 100: '#ECEBEF', + 50: '#F5F5F7', + }, + 'mulled-wine': { + 900: '#564663', + }, + peat: { + 900: '#383238', + 800: '#4C464C', + 700: '#625B62', + 600: '#7A727A', + 500: '#908990', + 400: '#A5A1A5', + 300: '#CBC7CB', + 200: '#EAE8EA', + 100: '#F3F3F3', + 50: '#F9FAFB', + }, + tangerine: { + 900: '#F08C00', + }, + sunglow: { + 900: '#F27A67', + 800: '#F39182', + 700: '#F4A79A', + 600: '#FABBB1', + 500: '#FAC7BE', + 400: '#F7D3CD', + 300: '#F8DFDB', + 200: '#F3E6E4', + 100: '#F4EDEB', + 50: '#F9F7F6', + }, + 'winter-sky': { + 900: '#E1EBEA', + 800: '#EEF3F2', + 700: '#F7FAFA', + }, + 'util-green': { + 900: '#014737', + 800: '#03543F', + 700: '#046C4E', + 600: '#057A55', + 500: '#0E9F6E', + 400: '#31C48D', + 300: '#84E1BC', + 200: '#BCF0DA', + 100: '#DEF7EC', + 50: '#F3FAF7', + }, + 'util-red': { + 900: '#771D1D', + 800: '#9B1C1C', + 700: '#C81E1E', + 600: '#E02424', + 500: '#F05252', + 400: '#F98080', + 300: '#F8B4B4', + 200: '#FBD5D5', + 100: '#FDE8E8', + 50: '#FDF2F2', + }, + }, + fontFamily: { + sans: ['var(--font-ftRegola)'], + mono: ['var(--font-karelia)'], + serif: ['var(--font-FAMAime)'], + }, + }, + }, +} +export default config diff --git a/packages/tailwind-config/tsconfig.json b/packages/tailwind-config/tsconfig.json new file mode 100644 index 0000000..940bd69 --- /dev/null +++ b/packages/tailwind-config/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@repo/typescript-config/base.json", + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/ui/.eslintrc.js b/packages/ui/.eslintrc.js new file mode 100644 index 0000000..3bc4de4 --- /dev/null +++ b/packages/ui/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["@repo/eslint-config/react.js"], +}; diff --git a/packages/ui/components.json b/packages/ui/components.json new file mode 100644 index 0000000..3544ce2 --- /dev/null +++ b/packages/ui/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/globals.css", + "baseColor": "slate", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "src", + "utils": "lib/utils" + } +} diff --git a/packages/ui/lib/utils.ts b/packages/ui/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/packages/ui/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..51bae76 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,109 @@ +{ + "name": "@repo/ui", + "version": "0.0.0", + "private": true, + "sideEffects": [ + "**/*.css" + ], + "exports": { + "./styles.css": "./dist/index.css", + "./alerts": "./src/alerts.tsx", + "./alert-dialog": "./src/alert-dialog/alert-dialog.tsx", + "./avatar": "./src/avatar/avatar.tsx", + "./button": "./src/button/button.tsx", + "./calendar": "./src/calendar/calendar.tsx", + "./card": "./src/card.tsx", + "./checkbox": "./src/checkbox/checkbox.tsx", + "./code": "./src/code.tsx", + "./data-table": "./src/data-table/data-table.tsx", + "./dialog": "./src/dialog/dialog.tsx", + "./drawer": "./src/drawer.tsx", + "./dropdown-menu": "./src/dropdown-menu/dropdown-menu.tsx", + "./form": "./src/form/form.tsx", + "./global-search": "./src/global-search.tsx", + "./grid": "./src/grid/grid.tsx", + "./kbd": "./src/kbd/kbd.tsx", + "./logo": "./src/logo/logo.tsx", + "./input": "./src/input/input.tsx", + "./info": "./src/info/info.tsx", + "./label": "./src/label/label.tsx", + "./page-heading": "./src/page-heading/page-heading.tsx", + "./panel": "./src/panel/panel.tsx", + "./password-input": "./src/password-input/password-input.tsx", + "./popover": "./src/popover/popover.tsx", + "./text-input": "./src/text-input.tsx", + "./tag-input": "./src/tag-input/tag-input.tsx", + "./toaster": "./src/toast/toaster.tsx", + "./toast": "./src/toast/toast.tsx", + "./use-toast": "./src/toast/use-toast.ts", + "./simple-form": "./src/simple-form.tsx", + "./select": "./src/select/select.tsx", + "./separator": "./src/separator/separator.tsx", + "./slider": "./src/slider/slider.tsx", + "./switch": "./src/switch/switch.tsx", + "./tabs": "./src/tabs/tabs.tsx", + "./tag": "./src/tag/tag.tsx", + "./message-box": "./src/message-box.tsx", + "./icons/google": "./src/icons/google.tsx", + "./icons/github": "./src/icons/github.tsx", + "./icons/chevron-down": "./src/icons/chevron-down.tsx", + "./lib/utils": "./lib/utils.ts" + }, + "scripts": { + "build": "tailwindcss -i ./src/styles.css -o dist/index.css", + "lint": "eslint src/", + "dev": "tailwindcss -i ./src/styles.css -o ./dist/index.css --watch", + "type-check": "tsc --noEmit" + }, + "peerDependencies": { + "react": "latest" + }, + "dependencies": { + "@hookform/resolvers": "^3.5.0", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-avatar": "latest", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "latest", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "latest", + "@radix-ui/react-toast": "^1.2.1", + "@storybook/react": "latest", + "@tanstack/react-table": "^8.17.3", + "class-variance-authority": "^0.7.0", + "date-fns": "^3.6.0", + "emblor": "^1.4.0", + "lucide-react": "^0.424.0", + "react": "18.3.1", + "react-day-picker": "8.10.1", + "react-dom": "18.3.1", + "react-hook-form": "^7.51.5", + "tailwind-merge": "^2.2.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" + }, + "devDependencies": { + "@headlessui/react": "latest", + "@repo/eslint-config": "workspace:*", + "@repo/tailwind-config": "workspace:*", + "@tailwindui/react": "latest", + "@tailwindcss/forms": "latest", + "@types/node": "latest", + "@types/eslint": "latest", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "autoprefixer": "latest", + "clsx": "latest", + "eslint": "latest", + "postcss": "latest", + "react": "latest", + "tailwindcss": "latest", + "tailwind-variants": "latest", + "tsup": "latest", + "typescript": "latest" + } +} \ No newline at end of file diff --git a/packages/ui/postcss.config.ts b/packages/ui/postcss.config.ts new file mode 100644 index 0000000..393a10f --- /dev/null +++ b/packages/ui/postcss.config.ts @@ -0,0 +1,8 @@ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + +export default config diff --git a/packages/ui/src/alert-dialog/alert-dialog.stories.tsx b/packages/ui/src/alert-dialog/alert-dialog.stories.tsx new file mode 100644 index 0000000..a18bd4f --- /dev/null +++ b/packages/ui/src/alert-dialog/alert-dialog.stories.tsx @@ -0,0 +1,91 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogAction, + AlertDialogCancel, +} from './alert-dialog' +import { Button } from '../button/button' + +const meta: Meta = { + title: 'UI/AlertDialog', + component: AlertDialog, + parameters: { + docs: { + description: { + component: + 'An alert dialog that interrupts the user with important information and requires a response. https://ui.shadcn.com/docs/components/alert-dialog', + }, + }, + backgrounds: { default: 'white' }, + }, + render: () => { + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + + + + + + + + + + ) + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Example: Story = {} + +export const ConfirmationAlertDialog: Story = { + render: () => { + return ( + + + + + + + Confirm Your Action + + Please confirm that you want to proceed with this action. This + action is irreversible. + + + + + + + + + + + + + ) + }, +} diff --git a/packages/ui/src/alert-dialog/alert-dialog.styles.tsx b/packages/ui/src/alert-dialog/alert-dialog.styles.tsx new file mode 100644 index 0000000..34dc0cc --- /dev/null +++ b/packages/ui/src/alert-dialog/alert-dialog.styles.tsx @@ -0,0 +1,16 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +export const alertDialogStyles = tv({ + slots: { + overlay: + 'text-left fixed inset-0 z-50 bg-blackberry-900/70 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', + content: + 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', + header: 'flex flex-col space-y-2 text-left sm:text-left', + footer: 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', + title: 'text-lg text-blackberry-800 font-semibold', + description: 'text-sm text-blackberry-800', + }, +}) + +export type AlertDialogVariants = VariantProps diff --git a/packages/ui/src/alert-dialog/alert-dialog.tsx b/packages/ui/src/alert-dialog/alert-dialog.tsx new file mode 100644 index 0000000..657d98f --- /dev/null +++ b/packages/ui/src/alert-dialog/alert-dialog.tsx @@ -0,0 +1,118 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import { cn } from '../../lib/utils' +import { alertDialogStyles } from './alert-dialog.styles' +import { buttonStyles } from '../button/button' + +const AlertDialog = AlertDialogPrimitive.Root +const AlertDialogTrigger = AlertDialogPrimitive.Trigger +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const { overlay, content, header, footer, title, description } = + alertDialogStyles() + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/packages/ui/src/alerts.tsx b/packages/ui/src/alerts.tsx new file mode 100644 index 0000000..7d0a1e4 --- /dev/null +++ b/packages/ui/src/alerts.tsx @@ -0,0 +1,39 @@ +'use client' + +import { signOut } from 'next-auth/react' +import { Menu, Transition } from '@headlessui/react' +import { Fragment } from 'react' +import Link from 'next/link' +import Image from 'next/image' +import bellIcon from './assets/bell.svg' + +export const Alerts = () => { + return ( + + + Open alert menu + View notifications + alerts icons + + +
+
    +
  • +

    No new alerts 🎉

    +
  • +
+
+
+
+ ) +} + +export default Alerts diff --git a/packages/ui/src/assets/arrow-right-dark.svg b/packages/ui/src/assets/arrow-right-dark.svg new file mode 100644 index 0000000..4e60764 --- /dev/null +++ b/packages/ui/src/assets/arrow-right-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/arrow-right.svg b/packages/ui/src/assets/arrow-right.svg new file mode 100644 index 0000000..9752172 --- /dev/null +++ b/packages/ui/src/assets/arrow-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/bell-dark.svg b/packages/ui/src/assets/bell-dark.svg new file mode 100644 index 0000000..f9d83b2 --- /dev/null +++ b/packages/ui/src/assets/bell-dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/bell.svg b/packages/ui/src/assets/bell.svg new file mode 100644 index 0000000..8dae7ef --- /dev/null +++ b/packages/ui/src/assets/bell.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/chevron-left.svg b/packages/ui/src/assets/chevron-left.svg new file mode 100644 index 0000000..b2fe94c --- /dev/null +++ b/packages/ui/src/assets/chevron-left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/chevron-right.svg b/packages/ui/src/assets/chevron-right.svg new file mode 100644 index 0000000..4322138 --- /dev/null +++ b/packages/ui/src/assets/chevron-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/horizontal-bars.svg b/packages/ui/src/assets/horizontal-bars.svg new file mode 100644 index 0000000..516ee32 --- /dev/null +++ b/packages/ui/src/assets/horizontal-bars.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/icon_logo.svg b/packages/ui/src/assets/icon_logo.svg new file mode 100644 index 0000000..5670f97 --- /dev/null +++ b/packages/ui/src/assets/icon_logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/packages/ui/src/assets/search-outline-white.svg b/packages/ui/src/assets/search-outline-white.svg new file mode 100644 index 0000000..e715347 --- /dev/null +++ b/packages/ui/src/assets/search-outline-white.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/search-outline.svg b/packages/ui/src/assets/search-outline.svg new file mode 100644 index 0000000..0e44ca9 --- /dev/null +++ b/packages/ui/src/assets/search-outline.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/ui/src/avatar/avatar.stories.tsx b/packages/ui/src/avatar/avatar.stories.tsx new file mode 100644 index 0000000..fcbe86d --- /dev/null +++ b/packages/ui/src/avatar/avatar.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Avatar, AvatarFallback, AvatarImage } from './avatar' +const meta: Meta = { + title: 'UI/Avatar', + component: Avatar, + parameters: { + docs: { + description: { + component: + 'An image element with a fallback for representing the user: https://ui.shadcn.com/docs/components/avatar', + }, + }, + backgrounds: { default: 'white' }, + }, + render: ({ children, ...args }) => { + return ( + + + DT + + ) + }, +} satisfies Meta + +export default meta +meta.args = { + src: 'Email', + type: 'email', +} +type Story = StoryObj + +export const AvatarWithImage: Story = {} + +export const AvatarNoImage: Story = { + render: () => { + return ( + + DT + + ) + }, +} diff --git a/packages/ui/src/avatar/avatar.styles.tsx b/packages/ui/src/avatar/avatar.styles.tsx new file mode 100644 index 0000000..88a3e8e --- /dev/null +++ b/packages/ui/src/avatar/avatar.styles.tsx @@ -0,0 +1,26 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +export const avatarStyles = tv({ + slots: { + avatarImageWrap: + 'relative flex h-11 w-11 shrink-0 overflow-hidden border-none rounded-md p-0 bg-blackberry-700', + avatarImage: 'aspect-square h-full w-full', + avatarFallBack: + 'uppercase flex h-full w-full items-center justify-center rounded-md bg-sunglow-900 text-white', + }, + variants: { + size: { + medium: { + avatarImageWrap: 'h-[34px] w-[34px]', + }, + large: { + avatarImageWrap: 'h-14 w-14', + }, + 'extra-large': { + avatarImageWrap: 'h-[72px] w-[72px]', + }, + }, + }, +}) + +export type AvatarVariants = VariantProps diff --git a/packages/ui/src/avatar/avatar.tsx b/packages/ui/src/avatar/avatar.tsx new file mode 100644 index 0000000..2548863 --- /dev/null +++ b/packages/ui/src/avatar/avatar.tsx @@ -0,0 +1,54 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import { cn } from '../../lib/utils' +import { avatarStyles, AvatarVariants } from './avatar.styles' + +const { avatarImage, avatarFallBack } = avatarStyles() + +interface AvatarProps + extends React.ComponentPropsWithoutRef { + variant?: AvatarVariants['size'] +} + +const Avatar = React.forwardRef< + React.ElementRef, + AvatarProps +>(({ className, variant, ...props }, ref) => { + const styles = avatarStyles({ size: variant }) + return ( + + ) +}) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/packages/ui/src/button/button.stories.tsx b/packages/ui/src/button/button.stories.tsx new file mode 100644 index 0000000..b6f78c7 --- /dev/null +++ b/packages/ui/src/button/button.stories.tsx @@ -0,0 +1,148 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { ArrowRight, ArrowUpRight, InfoIcon } from 'lucide-react' +import { GoogleIcon } from '../icons/google' +import { Button, buttonStyles } from './button' + +type ButtonVariants = keyof typeof buttonStyles.variants.variant +type ButtonSizes = keyof typeof buttonStyles.variants.size +const variants = Object.keys(buttonStyles.variants.variant) as ButtonVariants[] +const sizes = Object.keys(buttonStyles.variants.size) as ButtonSizes[] + +const meta: Meta = { + title: 'UI/Button', + component: Button, + parameters: { + docs: { + description: { + component: + 'Displays a button or a component that looks like a button: https://ui.shadcn.com/docs/components/button', + }, + }, + }, + argTypes: { + variant: { + description: 'Defines the theme of the button', + table: { + type: { summary: variants.join('|') }, + defaultValue: { summary: 'light' }, + }, + control: 'select', + options: variants, + }, + size: { + description: 'Defines the sizing of the button', + table: { + type: { summary: sizes.join('|') }, + defaultValue: { summary: 'lg' }, + }, + control: 'select', + options: sizes, + }, + children: { + description: 'Button text', + control: 'text', + defaultValue: 'Button', + }, + }, + render: ({ children, ...args }) => { + return + }, +} satisfies Meta + +export default meta +meta.args = { + children: 'Button Text', +} +type Story = StoryObj + +export const Sunglow: Story = { + args: { + variant: 'sunglow', + }, +} + +export const Blackberry: Story = { + args: { + variant: 'blackberry', + }, +} + +export const Outline: Story = { + args: { + variant: 'outline', + }, +} + +export const Medium: Story = { + args: { + variant: 'blackberry', + size: 'md', + icon: , + iconAnimated: true, + }, +} + +export const SmallSunglow: Story = { + args: { + variant: 'sunglow', + size: 'sm', + icon: , + }, +} + +export const SmallBlackberry: Story = { + args: { + variant: 'blackberry', + size: 'sm', + icon: , + }, +} + +export const SmallWhite: Story = { + parameters: { + backgrounds: { default: 'dark' }, + }, + args: { + variant: 'white', + size: 'sm', + icon: , + }, +} + +export const WithIconAndHoverAnimation: Story = { + name: 'With icon and hover animation', + args: { + icon: , + iconAnimated: true, + }, +} + +export const IconPositionLeft: Story = { + name: 'With static icon positioned on the left', + args: { + icon: , + iconPosition: 'left', + }, +} + +export const BrandIcon: Story = { + args: { + icon: , + iconPosition: 'left', + variant: 'outline', + }, +} + +export const Loading: Story = { + name: 'In a loading state', + args: { + loading: true, + }, +} + +export const Success: Story = { + name: 'In a success state', + args: { + variant: 'success', + }, +} diff --git a/packages/ui/src/button/button.styles.tsx b/packages/ui/src/button/button.styles.tsx new file mode 100644 index 0000000..d9230df --- /dev/null +++ b/packages/ui/src/button/button.styles.tsx @@ -0,0 +1,90 @@ +import { type ReactNode, type ButtonHTMLAttributes } from 'react' +import { tv, type VariantProps } from 'tailwind-variants' + +export const buttonStyles = tv({ + slots: { + base: 'relative group font-sans font-semibold text-white inline-flex items-center gap-2 justify-center whitespace-nowrap rounded-md leading-none text-sm transition-all duration-500 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-blackberry-300 disabled:pointer-events-none disabled:opacity-50', + iconOuter: 'relative h-4 w-4 overflow-hidden', + iconInner: 'absolute transition-all duration-500', + loadingWrapper: + 'absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', + loadingIcon: 'animate-spin !h-6 !w-6', + loadingText: 'opacity-0', + childWrapper: '', + }, + variants: { + variant: { + sunglow: '!bg-sunglow-900 hover:!opacity-90', + blackberry: '!bg-blackberry-900 hover:!opacity-90', + outline: + 'border-blackberry-800 text-blackberry-800 border hover:!opacity-90', + outlineLight: + 'border-blackberry-500 text-blackberry-800 border hover:!opacity-90', + redOutline: + 'border-util-red-500 text-util-red-500 border bg-white hover:!opacity-90 dark:border-util-red-500 dark:text-util-red-500', + white: {}, + success: 'flex-row-reverse !bg-util-green-500 hover:!opacity-90', + }, + iconPosition: { + left: 'flex-row-reverse', + }, + iconAnimated: { + true: { + iconInner: 'group-hover:-translate-y-4', + }, + }, + size: { + sm: 'h-auto rounded-none p-0 !bg-transparent font-sans text-lg', + md: 'h-12 rounded-md text-base px-5', + lg: 'h-16 px-8 text-lg', + }, + full: { + true: { + base: 'flex w-full', + }, + }, + childFull: { + true: { + childWrapper: 'flex w-full', + }, + }, + }, + compoundVariants: [ + { + variant: 'sunglow', + size: 'sm', + class: 'text-sunglow-900', + }, + { + variant: 'blackberry', + size: 'sm', + class: 'text-blackberry-900', + }, + { + variant: 'white', + size: 'sm', + class: 'text-white', + }, + { + variant: 'success', + size: 'sm', + class: 'text-white', + }, + ], + defaultVariants: { + variant: 'sunglow', + size: 'md', + }, +}) + +//TODO: Important is needed here for backgrounds due to https://github.com/tailwindlabs/tailwindcss/issues/12734 + +export type ButtonVariants = VariantProps + +export interface ButtonProps + extends ButtonHTMLAttributes, + ButtonVariants { + asChild?: boolean + icon?: ReactNode + loading?: boolean +} diff --git a/packages/ui/src/button/button.tsx b/packages/ui/src/button/button.tsx new file mode 100644 index 0000000..09ddcb3 --- /dev/null +++ b/packages/ui/src/button/button.tsx @@ -0,0 +1,77 @@ +import { Slot } from '@radix-ui/react-slot' +import { forwardRef } from 'react' +import { buttonStyles, type ButtonProps } from './button.styles' +import { CheckIcon, LoaderCircle } from 'lucide-react' +import { cn } from '../../lib/utils' + +const Button = forwardRef( + ( + { + asChild = false, + className, + icon, + loading, + iconAnimated, + iconPosition, + variant, + full, + childFull, + ...rest + }, + ref, + ) => { + const Comp = asChild ? Slot : 'button' + const { + base, + iconOuter, + iconInner, + loadingIcon, + loadingWrapper, + loadingText, + childWrapper, + } = buttonStyles({ + iconAnimated, + iconPosition, + variant, + full, + childFull, + ...rest, + }) + + return ( + + + {rest.children} + + {icon ? ( +
+
+ {icon} + {icon} +
+
+ ) : null} + {variant === 'success' ? ( +
+
+ +
+
+ ) : null} + {loading && ( +
+ +
+ )} +
+ ) + }, +) + +Button.displayName = 'Button' + +export { Button, buttonStyles } diff --git a/packages/ui/src/calendar/calendar.stories.tsx b/packages/ui/src/calendar/calendar.stories.tsx new file mode 100644 index 0000000..a981abd --- /dev/null +++ b/packages/ui/src/calendar/calendar.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Calendar } from './calendar' +import { CalendarProps } from './calendar' + +const meta: Meta = { + title: 'UI/Calendar', + component: Calendar, + parameters: { + docs: { + description: { + component: 'Calendar component with various customization options', + }, + }, + backgrounds: { default: 'white' }, + }, + render: (args: any) => { + return + }, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + showOutsideDays: true, + }, +} + +export const CustomClassNames: Story = { + args: { + showOutsideDays: true, + classNames: { + nav_button: 'custom-nav-button', + day_selected: 'custom-day-selected', + }, + }, +} + +export const WithoutOutsideDays: Story = { + args: { + showOutsideDays: false, + }, +} diff --git a/packages/ui/src/calendar/calendar.styles.tsx b/packages/ui/src/calendar/calendar.styles.tsx new file mode 100644 index 0000000..a63e571 --- /dev/null +++ b/packages/ui/src/calendar/calendar.styles.tsx @@ -0,0 +1,43 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +export const calendarStyles = tv({ + slots: { + root: 'p-3', + months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0', + month: 'space-y-4', + caption: 'flex justify-center pt-1 relative items-center', + caption_label: 'text-sm font-medium', + nav: 'space-x-1 flex items-center', + nav_button: 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100', + nav_button_previous: 'absolute left-1', + nav_button_next: 'absolute right-1', + table: 'w-full border-collapse space-y-1', + head_row: 'flex', + head_cell: + 'text-slate-500 rounded-md w-9 font-normal text-[0.8rem] dark:text-slate-400', + row: 'flex w-full mt-2', + cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-blackberry-400/50 [&:has([aria-selected])]:bg-blackberry-400 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20 ', + day: 'h-9 w-9 p-0 font-normal aria-selected:opacity-100', + day_range_end: 'day-range-end', + day_selected: + 'rounded-sm bg-blackberry-600 text-slate-50 hover:bg-blackberry-600 hover:text-slate-50', + day_today: + 'rounded-sm bg-winter-sky-900 text-blackberry-800 dark:bg-slate-800 dark:text-slate-50', + day_outside: + 'day-outside text-slate-500 opacity-50 aria-selected:bg-slate-100/50 aria-selected:text-slate-500 aria-selected:opacity-30 dark:text-slate-400 dark:aria-selected:bg-slate-800/50 dark:aria-selected:text-slate-400', + day_disabled: 'text-slate-500 opacity-50 dark:text-slate-400', + day_range_middle: + 'aria-selected:bg-slate-100 aria-selected:text-blackberry-800 dark:aria-selected:bg-slate-800 dark:aria-selected:text-slate-50', + day_hidden: 'invisible', + }, + variants: { + style: { + default: {}, + }, + }, + defaultVariants: { + style: 'default', + }, +}) + +export type CalendarVariants = VariantProps diff --git a/packages/ui/src/calendar/calendar.tsx b/packages/ui/src/calendar/calendar.tsx new file mode 100644 index 0000000..337ed2e --- /dev/null +++ b/packages/ui/src/calendar/calendar.tsx @@ -0,0 +1,70 @@ +'use client' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { DayPicker, DayPickerProps } from 'react-day-picker' + +import { calendarStyles, CalendarVariants } from './calendar.styles' +import { cn } from '../../lib/utils' +import { buttonStyles } from '../button/button' + +export type CalendarProps = DayPickerProps & + CalendarVariants & { + classNames?: Partial<(typeof calendarStyles)['slots']> + } + +const components = { + IconLeft: () => , + IconRight: () => , +} + +function Calendar({ + className, + classNames: customClassNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + const styles = calendarStyles() + + return ( + + ) +} +Calendar.displayName = 'Calendar' + +export { Calendar } diff --git a/packages/ui/src/card.tsx b/packages/ui/src/card.tsx new file mode 100644 index 0000000..71d6a0c --- /dev/null +++ b/packages/ui/src/card.tsx @@ -0,0 +1,27 @@ +import React from 'react' + +export const Card = ({ + className, + title, + children, + href, +}: { + className?: string + title: string + children: React.ReactNode + href: string +}): JSX.Element => { + return ( + +

+ {title} -> +

+

{children}

+
+ ) +} diff --git a/packages/ui/src/checkbox/checkbox.stories.tsx b/packages/ui/src/checkbox/checkbox.stories.tsx new file mode 100644 index 0000000..7055bc1 --- /dev/null +++ b/packages/ui/src/checkbox/checkbox.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Checkbox } from './checkbox' + +const meta: Meta = { + title: 'Forms/Checkbox', + component: Checkbox, + parameters: { + docs: { + description: { + component: + 'A control that allows the user to toggle between checked and not checked.', + }, + }, + backgrounds: { default: 'white' }, + }, + render: (args) => , +} satisfies Meta + +export default meta +type Story = StoryObj + +export const SingleCheckbox: Story = { + args: { + 'aria-label': 'Single Checkbox', + }, +} + +export const MultipleCheckboxesWithLabels: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ), +} + +export const DisabledCheckboxes: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ), +} diff --git a/packages/ui/src/checkbox/checkbox.styles.tsx b/packages/ui/src/checkbox/checkbox.styles.tsx new file mode 100644 index 0000000..ccb2b64 --- /dev/null +++ b/packages/ui/src/checkbox/checkbox.styles.tsx @@ -0,0 +1,11 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +export const checkboxStyles = tv({ + slots: { + root: 'peer h-5 w-5 shrink-0 rounded-sm border border-blackberry-400 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:text-sunglow-900 ', + indicator: 'flex items-center justify-center text-current', + checkIcon: 'h-3 w-3', + }, +}) + +export type CheckboxVariants = VariantProps diff --git a/packages/ui/src/checkbox/checkbox.tsx b/packages/ui/src/checkbox/checkbox.tsx new file mode 100644 index 0000000..f546cab --- /dev/null +++ b/packages/ui/src/checkbox/checkbox.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" +import { cn } from '../../lib/utils' +import { checkboxStyles } from './checkbox.styles' + +const { + root, + indicator, + checkIcon, +} = checkboxStyles() + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/packages/ui/src/code.tsx b/packages/ui/src/code.tsx new file mode 100644 index 0000000..6a6be1e --- /dev/null +++ b/packages/ui/src/code.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +export const Code = ({ + children, + className, +}: { + children: React.ReactNode + className?: string +}): JSX.Element => { + return {children} +} diff --git a/packages/ui/src/data-table/data-table.stories.tsx b/packages/ui/src/data-table/data-table.stories.tsx new file mode 100644 index 0000000..12b82f0 --- /dev/null +++ b/packages/ui/src/data-table/data-table.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { DataTable } from './data-table' +import { columns, mockData } from './mocks/data-table.mock' + +const meta: Meta = { + title: 'UI/DataTable', + component: DataTable, + parameters: { + docs: { + description: { + component: + 'A data table component', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( +
+ +
+ ), +} diff --git a/packages/ui/src/data-table/data-table.tsx b/packages/ui/src/data-table/data-table.tsx new file mode 100644 index 0000000..6b372aa --- /dev/null +++ b/packages/ui/src/data-table/data-table.tsx @@ -0,0 +1,195 @@ +'use client' + +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table' + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '../table/table' +import { Button } from '../button/button' +import { useState } from 'react' +import { Input } from '../input/input' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '../dropdown-menu/dropdown-menu' +import { EyeIcon } from 'lucide-react' + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] + showFilter?: boolean + showVisibility?: boolean + noResultsText?: string +} + +export function DataTable({ + columns, + data, + showFilter = false, + showVisibility = false, + noResultsText = 'No results', +}: DataTableProps) { + const [sorting, setSorting] = useState([]) + const [columnFilters, setColumnFilters] = useState([]) + const [columnVisibility, setColumnVisibility] = useState({}) + const [rowSelection, setRowSelection] = useState({}) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + defaultColumn: { + size: 0, + }, + }) + + return ( +
+ {(showFilter || showVisibility) && ( +
+ {showFilter && ( + + table.getColumn('name')?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + )} + {showVisibility && ( + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ) + })} + + + )} +
+ )} + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const columnWidth = + header.getSize() === 20 ? 'auto' : `${header.getSize()}px` + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + {noResultsText} + + + )} + + {/* + + + + + + + */} +
+
+ ) +} diff --git a/packages/ui/src/data-table/mocks/data-table.mock.tsx b/packages/ui/src/data-table/mocks/data-table.mock.tsx new file mode 100644 index 0000000..54e9bfe --- /dev/null +++ b/packages/ui/src/data-table/mocks/data-table.mock.tsx @@ -0,0 +1,297 @@ +'use client' + +import { ColumnDef } from '@tanstack/react-table' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '../../dropdown-menu/dropdown-menu' +import { + ArrowUpDown, + ClipboardCopyIcon, + CreditCardIcon, + MoreHorizontal, + User, +} from 'lucide-react' +import { Tag } from '../../tag/tag' +import { Checkbox } from '../../checkbox/checkbox' +import { Avatar, AvatarFallback, AvatarImage } from '../../avatar/avatar' + +export type Payment = { + id: string + amount: number + status: 'pending' | 'processing' | 'success' | 'failed' + email: string + name: { + firstname: string + lastname: string + } +} + +export const columns: ColumnDef[] = [ + { + id: 'select', + size: 100, + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + id: 'name', + accessorKey: 'name.lastname', + cell: ({ row }) => { + return ( +
+
+ + + +
+
+ {`${row.original.name.lastname}, ${row.original.name.firstname.charAt(0)}`} +
+ {row.original.email} +
+
+ ) + }, + header: ({ column }) => { + return ( +
column.toggleSorting(column.getIsSorted() === 'asc')} + > + Name + +
+ ) + }, + }, + { + accessorKey: 'status', + header: 'Status', + cell: ({ row }) => {row.getValue('status')}, + }, + { + accessorKey: 'amount', + header: () =>
Amount
, + cell: ({ row }) => { + const amount = parseFloat(row.getValue('amount')) + const formatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(amount) + + return
{formatted}
+ }, + }, + { + id: 'actions', + cell: ({ row }) => { + const payment = row.original + return ( + + + + + + navigator.clipboard.writeText(payment.id)} + > + Copy payment ID + + + View customer + + + View payment details + + + + ) + }, + }, +] + +export const mockData: Payment[] = [ + { + id: '728ed52f', + amount: 100, + status: 'pending', + email: 'm@example.com', + name: { firstname: 'John', lastname: 'Doe' }, + }, + { + id: '728ed53f', + amount: 200, + status: 'failed', + email: 'n@example.com', + name: { firstname: 'Jane', lastname: 'Smith' }, + }, + { + id: '728ed54f', + amount: 300, + status: 'failed', + email: 'o@example.com', + name: { firstname: 'Michael', lastname: 'Johnson' }, + }, + { + id: '728ed55f', + amount: 150, + status: 'success', + email: 'p@example.com', + name: { firstname: 'Chris', lastname: 'Lee' }, + }, + { + id: '728ed56f', + amount: 250, + status: 'pending', + email: 'q@example.com', + name: { firstname: 'Patricia', lastname: 'Brown' }, + }, + { + id: '728ed57f', + amount: 350, + status: 'success', + email: 'r@example.com', + name: { firstname: 'Linda', lastname: 'Davis' }, + }, + { + id: '728ed58f', + amount: 450, + status: 'failed', + email: 's@example.com', + name: { firstname: 'Robert', lastname: 'Miller' }, + }, + { + id: '728ed59f', + amount: 550, + status: 'pending', + email: 't@example.com', + name: { firstname: 'Jennifer', lastname: 'Wilson' }, + }, + { + id: '728ed60f', + amount: 150, + status: 'success', + email: 'u@example.com', + name: { firstname: 'James', lastname: 'Moore' }, + }, + { + id: '728ed61f', + amount: 250, + status: 'failed', + email: 'v@example.com', + name: { firstname: 'Barbara', lastname: 'Taylor' }, + }, + { + id: '728ed62f', + amount: 350, + status: 'pending', + email: 'w@example.com', + name: { firstname: 'David', lastname: 'Anderson' }, + }, + { + id: '728ed63f', + amount: 450, + status: 'success', + email: 'x@example.com', + name: { firstname: 'Susan', lastname: 'Thomas' }, + }, + { + id: '728ed64f', + amount: 550, + status: 'failed', + email: 'y@example.com', + name: { firstname: 'Charles', lastname: 'Jackson' }, + }, + { + id: '728ed65f', + amount: 150, + status: 'pending', + email: 'z@example.com', + name: { firstname: 'Elizabeth', lastname: 'White' }, + }, + { + id: '728ed66f', + amount: 250, + status: 'success', + email: 'a@example.com', + name: { firstname: 'Paul', lastname: 'Harris' }, + }, + { + id: '728ed67f', + amount: 350, + status: 'failed', + email: 'b@example.com', + name: { firstname: 'Nancy', lastname: 'Martin' }, + }, + { + id: '728ed68f', + amount: 450, + status: 'pending', + email: 'c@example.com', + name: { firstname: 'Mark', lastname: 'Thompson' }, + }, + { + id: '728ed69f', + amount: 550, + status: 'success', + email: 'd@example.com', + name: { firstname: 'Donna', lastname: 'Garcia' }, + }, + { + id: '728ed70f', + amount: 150, + status: 'failed', + email: 'e@example.com', + name: { firstname: 'George', lastname: 'Martinez' }, + }, + { + id: '728ed71f', + amount: 250, + status: 'pending', + email: 'f@example.com', + name: { firstname: 'Sandra', lastname: 'Rodriguez' }, + }, + { + id: '728ed72f', + amount: 350, + status: 'success', + email: 'g@example.com', + name: { firstname: 'Kenneth', lastname: 'Lewis' }, + }, + { + id: '728ed73f', + amount: 450, + status: 'failed', + email: 'h@example.com', + name: { firstname: 'Carol', lastname: 'Walker' }, + }, + { + id: '728ed74f', + amount: 550, + status: 'pending', + email: 'i@example.com', + name: { firstname: 'Steven', lastname: 'Hall' }, + }, +] diff --git a/packages/ui/src/dialog/dialog.stories.tsx b/packages/ui/src/dialog/dialog.stories.tsx new file mode 100644 index 0000000..408931b --- /dev/null +++ b/packages/ui/src/dialog/dialog.stories.tsx @@ -0,0 +1,85 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, + DialogClose, +} from './dialog' +import { Button } from '../button/button' + +const meta: Meta = { + title: 'UI/Dialog', + component: Dialog, + parameters: { + docs: { + description: { + component: + 'A modal dialog that focuses the user’s attention and blocks interaction with the rest of the application. https://ui.shadcn.com/docs/components/dialog', + }, + }, + backgrounds: { default: 'white' }, + }, + render: () => { + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. Your workspace and associated records will be deleted. + + + + + + + + + + + ) + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Example: Story = {} + +export const ConfirmationDialog: Story = { + render: () => { + return ( + + + + + + + Confirm Your Action + + Please confirm that you want to proceed with this action. This + action is irreversible. + + + + + + + + + + + ) + }, +} diff --git a/packages/ui/src/dialog/dialog.styles.tsx b/packages/ui/src/dialog/dialog.styles.tsx new file mode 100644 index 0000000..34bff60 --- /dev/null +++ b/packages/ui/src/dialog/dialog.styles.tsx @@ -0,0 +1,19 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +export const dialogStyles = tv({ + slots: { + overlay: + 'text-left fixed inset-0 z-50 bg-blackberry-900/70 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', + content: + 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-slate-800 dark:bg-slate-950', + close: + 'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 data-[state=open]:text-slate-500 dark:ring-offset-slate-950 dark:focus:ring-slate-300 dark:data-[state=open]:bg-slate-800 dark:data-[state=open]:text-slate-400', + closeIcon: 'h-4 w-4', + header: 'flex flex-col space-y-1.5 sm:text-left', + footer: 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', + title: 'text-lg font-semibold leading-none tracking-tight', + description: 'text-blackberry-800 dark:text-blackberry-800 max-w-[315px]', + }, +}) + +export type DialogVariants = VariantProps diff --git a/packages/ui/src/dialog/dialog.tsx b/packages/ui/src/dialog/dialog.tsx new file mode 100644 index 0000000..4612b7a --- /dev/null +++ b/packages/ui/src/dialog/dialog.tsx @@ -0,0 +1,113 @@ +'use client' + +import * as React from 'react' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { X } from 'lucide-react' + +import { cn } from '../../lib/utils' +import { dialogStyles } from './dialog.styles' + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const { + overlay, + content, + header, + footer, + title, + description, + close, + closeIcon, +} = dialogStyles() + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = 'DialogHeader' + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = 'DialogFooter' + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/packages/ui/src/drawer.tsx b/packages/ui/src/drawer.tsx new file mode 100644 index 0000000..7b8fbb7 --- /dev/null +++ b/packages/ui/src/drawer.tsx @@ -0,0 +1,388 @@ +'use client' + +import { Fragment, useEffect, useState } from 'react' +import Image from 'next/image' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { Dialog, Transition, Disclosure } from '@headlessui/react' +import { clsx } from 'clsx' +import chevronRight from './assets/chevron-right.svg' +import bars from './assets/horizonal-bars.svg' +import arrowRight from './assets/arrow-right.svg' + +export const Drawer = ({ routes, currentPath }: any) => { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const [tabletMenuOpen, setTabletMenuOpen] = useState(false) + const [isMobile, setIsMobile] = useState(false) + const pathname = usePathname() + + useEffect(() => { + if (typeof window !== 'undefined') { + const handleResize = () => { + /** + * Window is going into mobile mode + */ + if (window.innerWidth < 1024) { + setIsMobile(true) + /** + * If user has tablet menu open when playing with window + * size or rotating device, retain the open menu state + */ + if (tabletMenuOpen) { + setMobileMenuOpen(true) + setTabletMenuOpen(false) + } + } + /** + * Window is going into tablet mode + */ + if (window.innerWidth >= 1024) { + setIsMobile(false) + /** + * If user has mobile menu open when playing with window + * size or rotating device, retain the open menu state + */ + if (mobileMenuOpen) { + setMobileMenuOpen(false) + setTabletMenuOpen(true) + } + } + } + + window.addEventListener('resize', handleResize) + handleResize() + + return () => window.removeEventListener('resize', handleResize) + } + }, [mobileMenuOpen, tabletMenuOpen]) + + routes.map((route: any) => { + route.current = false + + if (route.children) { + route.current = pathname.includes(route.href) + + route.children.map((childRoute: any) => { + childRoute.current = false + childRoute.current = pathname === childRoute.href + }) + } + + if (route.detail) { + route.current = pathname.includes(route.href) + } + + if (!route.children || !route.detail) { + route.current = route.href === pathname + } + }) + + return ( + <> + + setMobileMenuOpen(!mobileMenuOpen)} + > + +
+ + +
+ + +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+ + + + ) +} + +export default Drawer diff --git a/packages/ui/src/dropdown-menu/dropdown-menu.stories.tsx b/packages/ui/src/dropdown-menu/dropdown-menu.stories.tsx new file mode 100644 index 0000000..11e92cf --- /dev/null +++ b/packages/ui/src/dropdown-menu/dropdown-menu.stories.tsx @@ -0,0 +1,138 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from './dropdown-menu' +import { Switch } from '../switch/switch' +import { Button } from '../button/button' + +const meta: Meta = { + title: 'UI/DropdownMenu', + component: DropdownMenu, + parameters: { + docs: { + description: { + component: + 'Displays a menu to the user — such as a set of actions or functions — triggered by a button. https://ui.shadcn.com/docs/components/dropdown-menu', + }, + }, + backgrounds: { default: 'white' }, + }, + render: () => { + return ( + + + + + + My Account + + + + Profile + ⇧⌘P + + + Billing + ⌘B + + + Settings + ⌘S + + + Keyboard shortcuts + ⌘K + + + + + Team + + Invite users + + + Email + Message + + More... + + + + + New Team + ⌘+T + + + + GitHub + Support + API + + + Log out + ⇧⌘Q + + + + ) + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Example: Story = {} + +export const UserMenu: Story = { + render: () => { + return ( + + + + + + +
+

+ Chris Berridge +
+ chris_b@lloydsbank.com +

+

+ + User Settings + +

+
+
+ +
+

Dark mode

+ +
+ + + + +
+
+ ) + }, +} diff --git a/packages/ui/src/dropdown-menu/dropdown-menu.styles.tsx b/packages/ui/src/dropdown-menu/dropdown-menu.styles.tsx new file mode 100644 index 0000000..6ca3059 --- /dev/null +++ b/packages/ui/src/dropdown-menu/dropdown-menu.styles.tsx @@ -0,0 +1,46 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +export const dropdownMenuStyles = tv({ + slots: { + subTrigger: + 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent', + subTriggerChevron: 'ml-auto h-4 w-4', + icon: 'h-4 w-4"', + separator: '-mx-8 my-1 h-px bg-winter-sky-800 dark:bg-peat-800', + subContent: + 'z-50 min-w-72 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + menuContent: + 'font-sans z-50 min-w-72 px-6 py-6 overflow-hidden rounded-md border border-winter-sky-800 bg-white text-popover-foreground shadow-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-peat-900', + menuItem: + 'relative flex cursor-default select-none items-center gap-3 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + menuCheckboxItem: + 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + menuCheckboxItemSpan: + 'absolute left-2 flex h-3.5 w-3.5 items-center justify-center', + menuRadioItem: + 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + menuRadioItemSpan: + 'absolute left-2 flex h-3.5 w-3.5 items-center justify-center', + menuLabel: 'px-2 py-1.5 text-sm font-semibold', + menuShortcut: 'ml-auto text-xs tracking-widest opacity-60', + }, + variants: { + inset: { + true: { + subTrigger: 'pl-8', + menuItem: 'pl-8', + menuLabel: 'pl-8', + }, + }, + spacing: { + md: { + separator: 'my-4', + }, + lg: { + separator: 'my-8', + }, + }, + }, +}) + +export type DropdownMenuVariants = VariantProps diff --git a/packages/ui/src/dropdown-menu/dropdown-menu.tsx b/packages/ui/src/dropdown-menu/dropdown-menu.tsx new file mode 100644 index 0000000..22e6ef5 --- /dev/null +++ b/packages/ui/src/dropdown-menu/dropdown-menu.tsx @@ -0,0 +1,190 @@ +'use client' + +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from '@radix-ui/react-icons' +import { cn } from '../../lib/utils' +import { + dropdownMenuStyles, + type DropdownMenuVariants, +} from './dropdown-menu.styles' + +const DropdownMenu = DropdownMenuPrimitive.Root +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger +const DropdownMenuGroup = DropdownMenuPrimitive.Group +const DropdownMenuPortal = DropdownMenuPrimitive.Portal +const DropdownMenuSub = DropdownMenuPrimitive.Sub +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const { + separator, + subTrigger, + subTriggerChevron, + subContent, + menuContent, + menuItem, + menuCheckboxItem, + menuCheckboxItemSpan, + icon, + menuRadioItem, + menuRadioItemSpan, + menuLabel, + menuShortcut, +} = dropdownMenuStyles() + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + DropdownMenuVariants +>(({ className, spacing, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/packages/ui/src/form/form.styles.tsx b/packages/ui/src/form/form.styles.tsx new file mode 100644 index 0000000..0d85fb7 --- /dev/null +++ b/packages/ui/src/form/form.styles.tsx @@ -0,0 +1,14 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +export const formStyles = tv({ + slots: { + formItem: 'space-y-2', + formLabelError: '', + formDescription: 'text-sm text-slate-500 dark:text-slate-400', + formMessageIcon: 'text-util-red-400', + formMessage: + 'text-util-red-500 rounded p-[14px] bg-util-red-100 flex items-center gap-4 !mt-5', + }, +}) + +export type FormVariants = VariantProps diff --git a/packages/ui/src/form/form.tsx b/packages/ui/src/form/form.tsx new file mode 100644 index 0000000..512392b --- /dev/null +++ b/packages/ui/src/form/form.tsx @@ -0,0 +1,202 @@ +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from 'react-hook-form' + +import { cn } from '../../lib/utils' +import { Label } from '../label/label' +import { formStyles } from './form.styles' +import { InfoIcon } from 'lucide-react' + +const { + formItem, + formLabelError, + formDescription, + formMessage, + formMessageIcon, +} = formStyles() + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error('useFormField should be used within ') + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = 'FormItem' + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +