mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
Compare commits
170 Commits
shadcn/pac
...
shadcn@4.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2baa86081d | ||
|
|
980f288149 | ||
|
|
07900769d9 | ||
|
|
360e8a19c3 | ||
|
|
e2fa0101e3 | ||
|
|
55ea86f252 | ||
|
|
f584f05489 | ||
|
|
a06ba18dcc | ||
|
|
f3e16e7db7 | ||
|
|
64afddefd9 | ||
|
|
c873713992 | ||
|
|
3751fdfa4c | ||
|
|
c824d6b78d | ||
|
|
df1752dfe0 | ||
|
|
e826e543f2 | ||
|
|
f7eecafb45 | ||
|
|
6e6cf9ee96 | ||
|
|
5b628e23e3 | ||
|
|
4a4dc8eb0f | ||
|
|
a33becad35 | ||
|
|
d60e8b6ce3 | ||
|
|
072c27fcd5 | ||
|
|
194dcc4571 | ||
|
|
51e3cfaf32 | ||
|
|
c8ab3801ec | ||
|
|
731e6dd8a2 | ||
|
|
d7066f4a2d | ||
|
|
5274de83d6 | ||
|
|
7e4dac7f31 | ||
|
|
28122dba18 | ||
|
|
93cde61946 | ||
|
|
c2dc06a99c | ||
|
|
c9930b7fda | ||
|
|
d1149454a8 | ||
|
|
36139f6200 | ||
|
|
15ac1be92b | ||
|
|
8ca30ed32c | ||
|
|
e2605bc7c2 | ||
|
|
b8608d0976 | ||
|
|
fc1ca40af4 | ||
|
|
f454f6e4d1 | ||
|
|
8cc7073aec | ||
|
|
031387a471 | ||
|
|
dd3567c39d | ||
|
|
ad2b8891a5 | ||
|
|
f6e18c65cf | ||
|
|
1c4a53a37a | ||
|
|
bc2db187aa | ||
|
|
92b4927a80 | ||
|
|
3cbabe012e | ||
|
|
1137b24a97 | ||
|
|
bb251e2ab6 | ||
|
|
28b3e5f360 | ||
|
|
309d95017f | ||
|
|
eb42ae25fd | ||
|
|
3977fb9ace | ||
|
|
7865621397 | ||
|
|
b07070cd07 | ||
|
|
ad68a44717 | ||
|
|
56161142f1 | ||
|
|
c2e1a5793f | ||
|
|
ea6086cbcc | ||
|
|
68a69d81f7 | ||
|
|
55fd4dc71b | ||
|
|
6dea65ebcb | ||
|
|
ba10089b8d | ||
|
|
8a814f926b | ||
|
|
c236d0c009 | ||
|
|
fd0e0c369b | ||
|
|
07d14abde1 | ||
|
|
8dd51c49f8 | ||
|
|
c20e0cc596 | ||
|
|
0126502236 | ||
|
|
94074e4bb2 | ||
|
|
eb6e783fb3 | ||
|
|
f785bfab44 | ||
|
|
cc20c8a794 | ||
|
|
05948dce8e | ||
|
|
5d23df4e35 | ||
|
|
abbdd32953 | ||
|
|
3f14ffa632 | ||
|
|
5927f6de80 | ||
|
|
39eb34104b | ||
|
|
7cbc7e8d53 | ||
|
|
d0ac558ce2 | ||
|
|
bc0c46a93c | ||
|
|
a64575d8a4 | ||
|
|
5d0cd7819b | ||
|
|
13478b26b6 | ||
|
|
aee8a71679 | ||
|
|
4507f1c794 | ||
|
|
81cd2266aa | ||
|
|
cf756b1b55 | ||
|
|
5e61f9c4a4 | ||
|
|
c4def9305f | ||
|
|
e456fed9d3 | ||
|
|
b95cd29508 | ||
|
|
11cbc32840 | ||
|
|
01539fb4d7 | ||
|
|
e47ee89dcf | ||
|
|
2f5c32c0b1 | ||
|
|
fbfe9f34bb | ||
|
|
d55e059fda | ||
|
|
9c572ab778 | ||
|
|
91403eeb63 | ||
|
|
3411d53856 | ||
|
|
efa2b38d07 | ||
|
|
d00605c5fb | ||
|
|
4bdeea4c63 | ||
|
|
f632f5d798 | ||
|
|
7d6d489f83 | ||
|
|
e8b1be1f22 | ||
|
|
d987955893 | ||
|
|
7b5435ac0b | ||
|
|
f289497e35 | ||
|
|
0d266984e6 | ||
|
|
cf92d4f8f2 | ||
|
|
b7cfc364ac | ||
|
|
de385d04fc | ||
|
|
b9f78c8a35 | ||
|
|
97b9e7b0ae | ||
|
|
e4b25981bf | ||
|
|
1017410468 | ||
|
|
fa71bb8624 | ||
|
|
d99839ec2a | ||
|
|
70b6bfd687 | ||
|
|
541c08f112 | ||
|
|
420433ae6f | ||
|
|
a7d77e0cf7 | ||
|
|
7ec2acc87d | ||
|
|
eeb5d22fe5 | ||
|
|
a757e80242 | ||
|
|
84d1d476b1 | ||
|
|
a52a606fb5 | ||
|
|
6ba39bb720 | ||
|
|
dd4b5c287c | ||
|
|
aa534e5875 | ||
|
|
2be9640c88 | ||
|
|
56567ae21a | ||
|
|
429e258322 | ||
|
|
2f57100061 | ||
|
|
fc62d5781d | ||
|
|
d86c5e5939 | ||
|
|
8006dd1c93 | ||
|
|
1dcbb4c88a | ||
|
|
4f4ffde4aa | ||
|
|
6d7a0ed93b | ||
|
|
b909b0363f | ||
|
|
a6fa6893eb | ||
|
|
561586bd98 | ||
|
|
7ddb30aade | ||
|
|
024425d45a | ||
|
|
4bdaf48f9b | ||
|
|
e9546e87ff | ||
|
|
0b34d581f9 | ||
|
|
5c2ed5e90e | ||
|
|
e9443ccd4a | ||
|
|
1fe0fe65e8 | ||
|
|
6823bad998 | ||
|
|
398e6c3406 | ||
|
|
710cc27de7 | ||
|
|
08212a478d | ||
|
|
50dc9b506b | ||
|
|
6b5aa16668 | ||
|
|
706806a207 | ||
|
|
8a7502d7fa | ||
|
|
945298ed2d | ||
|
|
f9b216af77 | ||
|
|
6525227036 | ||
|
|
b57e192965 |
@@ -9,5 +9,6 @@
|
||||
"WebFetch(domain:github.com)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
},
|
||||
"outputStyle": "Explanatory"
|
||||
}
|
||||
|
||||
41
.cursor-plugin/plugin.json
Normal file
41
.cursor-plugin/plugin.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"displayName": "shadcn/ui",
|
||||
"version": "1.0.0",
|
||||
"description": "UI component and design system framework. Search registries, install components as source code, and audit your project.",
|
||||
"author": {
|
||||
"name": "shadcn"
|
||||
},
|
||||
"homepage": "https://ui.shadcn.com",
|
||||
"repository": "https://github.com/shadcn-ui/ui",
|
||||
"license": "MIT",
|
||||
"logo": "skills/shadcn/assets/shadcn.png",
|
||||
"keywords": [
|
||||
"shadcn",
|
||||
"shadcn-ui",
|
||||
"ui",
|
||||
"components",
|
||||
"tailwind",
|
||||
"tailwindcss",
|
||||
"radix",
|
||||
"react",
|
||||
"design-system",
|
||||
"registry",
|
||||
"mcp"
|
||||
],
|
||||
"category": "developer-tools",
|
||||
"tags": [
|
||||
"ui",
|
||||
"components",
|
||||
"design-system",
|
||||
"react",
|
||||
"tailwind"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": ["shadcn@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
6
.github/workflows/code-check.yml
vendored
6
.github/workflows/code-check.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
|
||||
9
.github/workflows/prerelease-comment.yml
vendored
9
.github/workflows/prerelease-comment.yml
vendored
@@ -3,7 +3,7 @@ name: Write Beta Release comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Release - Beta"]
|
||||
workflows: ["Release"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
@@ -11,12 +11,13 @@ jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
name: Write comment to the PR
|
||||
steps:
|
||||
- name: "Comment on PR"
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -53,7 +54,7 @@ jobs:
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
64
.github/workflows/prerelease.yml
vendored
64
.github/workflows/prerelease.yml
vendored
@@ -1,64 +0,0 @@
|
||||
# Adapted from create-t3-app.
|
||||
|
||||
name: Release - Beta
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
|
||||
name: Build & Publish a beta release to NPM
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-beta.js
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: pnpm pub:beta
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: packages/shadcn
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/shadcn/dist/index.js
|
||||
86
.github/workflows/release.yml
vendored
86
.github/workflows/release.yml
vendored
@@ -2,31 +2,88 @@
|
||||
|
||||
name: Release
|
||||
|
||||
run-name: ${{ github.event_name == 'pull_request' && format('Release Beta - PR {0}', github.event.number) || 'Release Stable' }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'shadcn-ui' }}
|
||||
name: Create a PR for release workflow
|
||||
prerelease:
|
||||
if: ${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && contains(github.event.pull_request.labels.*.name, '🚀 autorelease') }}
|
||||
name: Publish Beta to NPM
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-beta.js
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: pnpm pub:beta
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: packages/shadcn
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/shadcn/dist/index.js
|
||||
|
||||
release:
|
||||
if: ${{ github.event_name == 'push' && github.repository_owner == 'shadcn-ui' }}
|
||||
name: Create Version PR or Publish Stable Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.33.4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
@@ -47,10 +104,19 @@ jobs:
|
||||
- name: Build the package
|
||||
run: pnpm shadcn:build
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
git_tag_gpgsign: true
|
||||
|
||||
- name: Create Version PR or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
setupGitUser: false
|
||||
commit: "chore(release): version packages"
|
||||
title: "chore(release): version packages"
|
||||
version: node .github/changeset-version.js
|
||||
|
||||
75
.github/workflows/signed-commits.yml
vendored
Normal file
75
.github/workflows/signed-commits.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Signed commits
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
signed-commits:
|
||||
if: github.repository_owner == 'shadcn-ui'
|
||||
runs-on: ubuntu-latest
|
||||
name: Signed commits
|
||||
steps:
|
||||
- name: Check PR commits
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const body = "Can you sign the commits please? See https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits. Thank you."
|
||||
|
||||
const { owner, repo } = context.repo
|
||||
const pullNumber = context.payload.pull_request.number
|
||||
|
||||
const commits = await github.paginate(github.rest.pulls.listCommits, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
const unsignedCommits = commits.filter((commit) => {
|
||||
return commit.commit.verification?.reason === "unsigned"
|
||||
})
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
const existingComments = comments.filter((comment) => {
|
||||
return comment.user.type === "Bot" && comment.body.trim() === body
|
||||
})
|
||||
|
||||
if (unsignedCommits.length > 0) {
|
||||
core.info(`Found ${unsignedCommits.length} unsigned commits.`)
|
||||
|
||||
if (existingComments.length === 0) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
core.info("All commits are signed.")
|
||||
|
||||
for (const comment of existingComments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
})
|
||||
}
|
||||
314
.github/workflows/templates.yml
vendored
Normal file
314
.github/workflows/templates.yml
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
name: Templates
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
paths:
|
||||
- ".github/workflows/templates.yml"
|
||||
- "apps/v4/registry/**"
|
||||
- "package.json"
|
||||
- "packages/shadcn/src/commands/add.ts"
|
||||
- "packages/shadcn/src/commands/init.ts"
|
||||
- "packages/shadcn/src/templates/**"
|
||||
- "packages/shadcn/src/utils/create-project.ts"
|
||||
- "packages/shadcn/src/utils/get-monorepo-info.ts"
|
||||
- "packages/shadcn/src/utils/get-package-manager.ts"
|
||||
- "pnpm-lock.yaml"
|
||||
- "templates/**"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ matrix.package-manager == 'pnpm' && format('pnpm {0}', matrix.pnpm-version) || matrix.package-manager }} ${{ matrix.template }}
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template: [next, vite, astro, start, react-router]
|
||||
package-manager: [pnpm, bun, npm, yarn]
|
||||
pnpm-version: [10.33.4, 11]
|
||||
exclude:
|
||||
- package-manager: bun
|
||||
pnpm-version: 11
|
||||
- package-manager: npm
|
||||
pnpm-version: 11
|
||||
- package-manager: yarn
|
||||
pnpm-version: 11
|
||||
env:
|
||||
NEXT_PUBLIC_APP_URL: http://localhost:4000
|
||||
NEXT_PUBLIC_V0_URL: https://v0.dev
|
||||
REGISTRY_URL: http://localhost:4000/r
|
||||
ROOT_PNPM_VERSION: 10.33.4
|
||||
TEMPLATE_PNPM_VERSION: ${{ matrix.pnpm-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: ${{ env.ROOT_PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install Yarn
|
||||
if: matrix.package-manager == 'yarn'
|
||||
run: |
|
||||
corepack enable
|
||||
COREPACK_ENABLE_PROJECT_SPEC=0 corepack prepare yarn@4.12.0 --activate
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
pnpm --filter=shadcn build
|
||||
pnpm --filter=v4 registry:build
|
||||
|
||||
- name: Validate templates
|
||||
env:
|
||||
TEMPLATE: ${{ matrix.template }}
|
||||
TEMPLATE_PACKAGE_MANAGER: ${{ matrix.package-manager }}
|
||||
SHADCN_TEMPLATE_DIR: ${{ github.workspace }}/templates
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
root_pnpm="$(command -v pnpm)"
|
||||
validation_script="$RUNNER_TEMP/validate-templates.sh"
|
||||
|
||||
cat > "$validation_script" <<'BASH'
|
||||
set -euo pipefail
|
||||
|
||||
bin_dir="$RUNNER_TEMP/template-pnpm-bin"
|
||||
mkdir -p "$bin_dir"
|
||||
|
||||
cat > "$bin_dir/pnpm" <<'PNPM'
|
||||
#!/usr/bin/env bash
|
||||
exec npx -y "pnpm@${TEMPLATE_PNPM_VERSION}" "$@"
|
||||
PNPM
|
||||
chmod +x "$bin_dir/pnpm"
|
||||
export PATH="$bin_dir:$PATH"
|
||||
|
||||
echo "Using template pnpm $(pnpm --version)"
|
||||
|
||||
cli="$GITHUB_WORKSPACE/packages/shadcn/dist/index.js"
|
||||
template_root="$RUNNER_TEMP/generated-template-${TEMPLATE_PACKAGE_MANAGER}-${TEMPLATE}"
|
||||
rm -rf "$template_root"
|
||||
mkdir -p "$template_root"
|
||||
|
||||
modes=(app monorepo)
|
||||
|
||||
has_script() {
|
||||
node -e "const pkg = require('./package.json'); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1"
|
||||
}
|
||||
|
||||
run_script_if_present() {
|
||||
local script="$1"
|
||||
if has_script "$script"; then
|
||||
pnpm run "$script"
|
||||
else
|
||||
echo "No $script script found; skipping."
|
||||
fi
|
||||
}
|
||||
|
||||
validate_non_pnpm_project() {
|
||||
local package_manager="$1"
|
||||
local project_path="$2"
|
||||
local check_workspace_protocol="$3"
|
||||
local is_monorepo="$4"
|
||||
|
||||
cd "$project_path"
|
||||
test ! -f pnpm-workspace.yaml
|
||||
test ! -f pnpm-lock.yaml
|
||||
|
||||
EXPECTED_PACKAGE_MANAGER="$package_manager" \
|
||||
CHECK_WORKSPACE_PROTOCOL="$check_workspace_protocol" \
|
||||
IS_MONOREPO="$is_monorepo" \
|
||||
node <<'NODE'
|
||||
const fs = require("node:fs")
|
||||
const path = require("node:path")
|
||||
|
||||
const expectedPackageManager = process.env.EXPECTED_PACKAGE_MANAGER
|
||||
const checkWorkspaceProtocol =
|
||||
process.env.CHECK_WORKSPACE_PROTOCOL === "true"
|
||||
const isMonorepo = process.env.IS_MONOREPO === "true"
|
||||
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
|
||||
|
||||
if (isMonorepo) {
|
||||
const workspaces = pkg.workspaces ?? []
|
||||
|
||||
if (!Array.isArray(workspaces)) {
|
||||
throw new Error("Expected package.json workspaces to be an array.")
|
||||
}
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
throw new Error("Expected package.json workspaces to have entries.")
|
||||
}
|
||||
|
||||
for (const workspace of ["sharp", "unrs-resolver", "esbuild"]) {
|
||||
if (workspaces.includes(workspace)) {
|
||||
throw new Error(`Unexpected workspace entry: ${workspace}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!pkg.packageManager?.startsWith(`${expectedPackageManager}@`)) {
|
||||
throw new Error(
|
||||
`Expected packageManager to use ${expectedPackageManager}, got ${pkg.packageManager}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (pkg.workspaces !== undefined) {
|
||||
throw new Error("Did not expect package.json workspaces for app template.")
|
||||
}
|
||||
|
||||
if (pkg.packageManager !== undefined) {
|
||||
throw new Error(
|
||||
`Did not expect packageManager for app template, got ${pkg.packageManager}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (checkWorkspaceProtocol) {
|
||||
const packageJsonFiles = []
|
||||
function collectPackageJsonFiles(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === "node_modules") {
|
||||
continue
|
||||
}
|
||||
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
collectPackageJsonFiles(fullPath)
|
||||
} else if (entry.name === "package.json") {
|
||||
packageJsonFiles.push(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectPackageJsonFiles(process.cwd())
|
||||
|
||||
for (const file of packageJsonFiles) {
|
||||
const json = fs.readFileSync(file, "utf8")
|
||||
if (json.includes("workspace:")) {
|
||||
throw new Error(`Unexpected workspace: protocol in ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
for mode in "${modes[@]}"; do
|
||||
project="test-${TEMPLATE}-${mode}-${TEMPLATE_PACKAGE_MANAGER}"
|
||||
project_path="$template_root/$project"
|
||||
|
||||
echo "::group::${TEMPLATE} ${mode} ${TEMPLATE_PACKAGE_MANAGER}"
|
||||
args=(
|
||||
init
|
||||
--defaults
|
||||
--name "$project"
|
||||
--template "$TEMPLATE"
|
||||
--cwd "$template_root"
|
||||
--silent
|
||||
)
|
||||
|
||||
if [ "$mode" = "monorepo" ]; then
|
||||
args+=(--monorepo)
|
||||
is_monorepo="true"
|
||||
else
|
||||
args+=(--no-monorepo)
|
||||
is_monorepo="false"
|
||||
fi
|
||||
|
||||
case "$TEMPLATE_PACKAGE_MANAGER" in
|
||||
pnpm)
|
||||
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
|
||||
REGISTRY_URL="$REGISTRY_URL" \
|
||||
npm_config_user_agent="pnpm/${TEMPLATE_PNPM_VERSION}" \
|
||||
node "$cli" "${args[@]}"
|
||||
|
||||
cd "$project_path"
|
||||
pnpm install --frozen-lockfile
|
||||
run_script_if_present typecheck
|
||||
run_script_if_present build
|
||||
;;
|
||||
bun)
|
||||
(
|
||||
cd "$template_root"
|
||||
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
|
||||
REGISTRY_URL="$REGISTRY_URL" \
|
||||
npm_config_user_agent="bun/$(bun --version)" \
|
||||
bunx --bun --package "$GITHUB_WORKSPACE/packages/shadcn" \
|
||||
shadcn "${args[@]}"
|
||||
)
|
||||
validate_non_pnpm_project \
|
||||
"bun" \
|
||||
"$project_path" \
|
||||
"false" \
|
||||
"$is_monorepo"
|
||||
;;
|
||||
npm)
|
||||
(
|
||||
cd "$template_root"
|
||||
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
|
||||
REGISTRY_URL="$REGISTRY_URL" \
|
||||
npm_config_user_agent="npm/$(npm --version)" \
|
||||
npx --yes --package "$GITHUB_WORKSPACE/packages/shadcn" \
|
||||
shadcn "${args[@]}"
|
||||
)
|
||||
validate_non_pnpm_project \
|
||||
"npm" \
|
||||
"$project_path" \
|
||||
"true" \
|
||||
"$is_monorepo"
|
||||
;;
|
||||
yarn)
|
||||
(
|
||||
cd "$template_root"
|
||||
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
|
||||
REGISTRY_URL="$REGISTRY_URL" \
|
||||
COREPACK_ENABLE_PROJECT_SPEC=0 \
|
||||
npm_config_user_agent="yarn/$(COREPACK_ENABLE_PROJECT_SPEC=0 yarn --version)" \
|
||||
yarn dlx --package "$GITHUB_WORKSPACE/packages/shadcn" \
|
||||
shadcn "${args[@]}"
|
||||
)
|
||||
validate_non_pnpm_project \
|
||||
"yarn" \
|
||||
"$project_path" \
|
||||
"false" \
|
||||
"$is_monorepo"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "::endgroup::"
|
||||
done
|
||||
BASH
|
||||
|
||||
"$root_pnpm" exec start-server-and-test \
|
||||
"$root_pnpm v4:dev" \
|
||||
http://localhost:4000 \
|
||||
"bash $validation_script"
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -39,6 +39,9 @@ jobs:
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
|
||||
59
.github/workflows/validate-registries.yml
vendored
59
.github/workflows/validate-registries.yml
vendored
@@ -3,54 +3,14 @@ name: Validate Registries
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
|
||||
jobs:
|
||||
check-registry-sync:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
name: check-registry-sync
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check changed files
|
||||
id: changed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only)
|
||||
|
||||
DIRECTORY_CHANGED=false
|
||||
REGISTRIES_CHANGED=false
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "^apps/v4/registry/directory.json$"; then
|
||||
DIRECTORY_CHANGED=true
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "^apps/v4/public/r/registries.json$"; then
|
||||
REGISTRIES_CHANGED=true
|
||||
fi
|
||||
|
||||
echo "directory_changed=$DIRECTORY_CHANGED" >> $GITHUB_OUTPUT
|
||||
echo "registries_changed=$REGISTRIES_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Flag missing registries.json update
|
||||
if: steps.changed.outputs.directory_changed == 'true' && steps.changed.outputs.registries_changed == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "registries: invalid"
|
||||
gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "can you run \`pnpm registry:build\` and commit the json files please?"
|
||||
exit 1
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm validate:registries
|
||||
@@ -73,25 +33,20 @@ jobs:
|
||||
node <<'EOF'
|
||||
const fs = require("node:fs")
|
||||
|
||||
const files = [
|
||||
"apps/v4/public/r/registries.json",
|
||||
"apps/v4/registry/directory.json",
|
||||
]
|
||||
const file = "apps/v4/registry/directory.json"
|
||||
const reservedNamespaces = new Set(
|
||||
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
|
||||
)
|
||||
|
||||
function readNames(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
|
||||
function readNames() {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8")).map(
|
||||
(entry) => entry.name
|
||||
)
|
||||
}
|
||||
|
||||
const violations = files.flatMap((filePath) => {
|
||||
return readNames(filePath)
|
||||
.filter((name) => reservedNamespaces.has(name))
|
||||
.map((name) => `${filePath}: ${name}`)
|
||||
})
|
||||
const violations = readNames()
|
||||
.filter((name) => reservedNamespaces.has(name))
|
||||
.map((name) => `${file}: ${name}`)
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Reserved registry namespaces are not allowed:")
|
||||
@@ -108,7 +63,7 @@ jobs:
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -43,5 +43,6 @@ tsconfig.tsbuildinfo
|
||||
|
||||
.notes
|
||||
.playwright-mcp
|
||||
.playwright-cli
|
||||
shadcn-workspace
|
||||
.codex-artifacts
|
||||
|
||||
94
apps/v4/app/(app)/(root)/cards/account-access.tsx
Normal file
94
apps/v4/app/(app)/(root)/cards/account-access.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
ArrowRight01Icon,
|
||||
SquareLock02Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
|
||||
import { Input } from "@/styles/base-rhea/ui/input"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-rhea/ui/item"
|
||||
|
||||
export function AccountAccess() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Access</CardTitle>
|
||||
<CardDescription>
|
||||
Update your credentials or re-authenticate.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email-address">Email Address</FieldLabel>
|
||||
<Input
|
||||
id="email-address"
|
||||
type="email"
|
||||
placeholder="artist@studio.inc"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel htmlFor="current-password">
|
||||
Current Password
|
||||
</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-medium tracking-wider text-muted-foreground uppercase hover:text-foreground"
|
||||
>
|
||||
Forgot?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
id="current-password"
|
||||
type="password"
|
||||
placeholder="••••••••••••••••••••••••"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Button className="w-full">
|
||||
<HugeiconsIcon icon={SquareLock02Icon} strokeWidth={2} />
|
||||
Update Security
|
||||
</Button>
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<HugeiconsIcon
|
||||
icon={AlertCircleIcon}
|
||||
className="text-destructive"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Danger Zone</ItemTitle>
|
||||
<ItemDescription className="line-clamp-1">
|
||||
Archive account and remove catalog
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-4"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Item>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
46
apps/v4/app/(app)/(root)/cards/analytics-card.tsx
Normal file
46
apps/v4/app/(app)/(root)/cards/analytics-card.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Badge } from "@/styles/base-rhea/ui/badge"
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
|
||||
const areaPath = "M0 52L18 40L36 46L54 70L72 50L100 49V86H0Z"
|
||||
const strokePath = "M0 52L18 40L36 46L54 70L72 50L100 49"
|
||||
|
||||
export function AnalyticsCard() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
418.2K Visitors <Badge>+10%</Badge>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
View Analytics
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<svg
|
||||
viewBox="0 0 100 86"
|
||||
preserveAspectRatio="none"
|
||||
className="aspect-[1/0.35] w-full text-chart-1"
|
||||
role="img"
|
||||
aria-label="Visitor trend"
|
||||
>
|
||||
<path d={areaPath} fill="currentColor" opacity="0.28" />
|
||||
<path
|
||||
d={strokePath}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
75
apps/v4/app/(app)/(root)/cards/claimable-balance.tsx
Normal file
75
apps/v4/app/(app)/(root)/cards/claimable-balance.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Badge } from "@/styles/base-rhea/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Item, ItemContent } from "@/styles/base-rhea/ui/item"
|
||||
import { Separator } from "@/styles/base-rhea/ui/separator"
|
||||
|
||||
const netRoyalties = 1248.75
|
||||
const processingFee = 37.46
|
||||
const totalClaimable = netRoyalties - processingFee
|
||||
|
||||
const formatCurrency = (amount: number) =>
|
||||
amount.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
|
||||
export function ClaimableBalance() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>Claimable Balance</CardDescription>
|
||||
<CardTitle className="text-4xl tabular-nums">
|
||||
${formatCurrency(totalClaimable)}
|
||||
</CardTitle>
|
||||
<Badge variant="outline">
|
||||
<span className="size-2 rounded-full bg-yellow-500" />
|
||||
Pending Setup
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col justify-end">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Net Royalties
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">
|
||||
${formatCurrency(netRoyalties)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Processing Fee
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">
|
||||
-${formatCurrency(processingFee)}
|
||||
</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Total Ready to Claim
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
${formatCurrency(totalClaimable)} USD
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardDescription>
|
||||
Once your bank is connected, balances over $10.00 are automatically
|
||||
eligible for monthly distribution on the 15th of each month.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
88
apps/v4/app/(app)/(root)/cards/contribution-history.tsx
Normal file
88
apps/v4/app/(app)/(root)/cards/contribution-history.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Badge } from "@/styles/base-rhea/ui/badge"
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Item, ItemContent, ItemDescription } from "@/styles/base-rhea/ui/item"
|
||||
|
||||
const chartData = [
|
||||
{ month: "Dec", amount: 800 },
|
||||
{ month: "Jan", amount: 1100 },
|
||||
{ month: "Feb", amount: 900 },
|
||||
{ month: "Mar", amount: 1300 },
|
||||
{ month: "Apr", amount: 750 },
|
||||
{ month: "May", amount: 1400 },
|
||||
]
|
||||
|
||||
export function ContributionHistory() {
|
||||
const maxAmount = Math.max(...chartData.map((item) => item.amount))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contribution History</CardTitle>
|
||||
<CardDescription>Last 6 months of activity</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
className="flex h-[200px] w-full items-end gap-3"
|
||||
role="img"
|
||||
aria-label="Last 6 months of contribution activity"
|
||||
>
|
||||
{chartData.map((item) => (
|
||||
<div
|
||||
key={item.month}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-2"
|
||||
>
|
||||
<div
|
||||
className="min-h-2 rounded-t-md bg-chart-2"
|
||||
style={{ height: `${(item.amount / maxAmount) * 100}%` }}
|
||||
/>
|
||||
<span className="text-center text-xs text-muted-foreground">
|
||||
{item.month}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Upcoming
|
||||
</ItemDescription>
|
||||
<span className="cn-font-heading text-base font-semibold">
|
||||
May 2024
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">Scheduled</span>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item
|
||||
variant="muted"
|
||||
className="hidden flex-col items-stretch xl:flex"
|
||||
>
|
||||
<ItemContent className="gap-1">
|
||||
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Savings Plan
|
||||
</ItemDescription>
|
||||
<span className="cn-font-heading text-base font-semibold">
|
||||
Accelerated
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">Recurring</span>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View Full Report</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
116
apps/v4/app/(app)/(root)/cards/dividend-income.tsx
Normal file
116
apps/v4/app/(app)/(root)/cards/dividend-income.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Cancel01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-rhea/ui/item"
|
||||
|
||||
const HOLDINGS = [
|
||||
{
|
||||
name: "Vanguard",
|
||||
shares: "450 Shares",
|
||||
amount: "$1,842.10",
|
||||
data: [
|
||||
{ q: "Q1", value: 380 },
|
||||
{ q: "Q2", value: 420 },
|
||||
{ q: "Q3", value: 390 },
|
||||
{ q: "Q4", value: 652 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "S&P 500 VOO",
|
||||
shares: "112 Shares",
|
||||
amount: "$928.40",
|
||||
data: [
|
||||
{ q: "Q1", value: 180 },
|
||||
{ q: "Q2", value: 210 },
|
||||
{ q: "Q3", value: 320 },
|
||||
{ q: "Q4", value: 218 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Apple AAPL",
|
||||
shares: "85 Shares",
|
||||
amount: "$340.00",
|
||||
data: [
|
||||
{ q: "Q1", value: 60 },
|
||||
{ q: "Q2", value: 70 },
|
||||
{ q: "Q3", value: 120 },
|
||||
{ q: "Q4", value: 90 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Realty Income",
|
||||
shares: "320 Shares",
|
||||
amount: "$1,139.50",
|
||||
data: [
|
||||
{ q: "Q1", value: 240 },
|
||||
{ q: "Q2", value: 260 },
|
||||
{ q: "Q3", value: 280 },
|
||||
{ q: "Q4", value: 360 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function DividendIncome() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Q2 Dividend Income</CardTitle>
|
||||
<CardDescription>
|
||||
Quarterly dividend payouts across your portfolio holdings.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="bg-muted"
|
||||
aria-label="Dismiss dividend income"
|
||||
>
|
||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
{HOLDINGS.map((holding) => (
|
||||
<Item key={holding.name} role="listitem" variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{holding.name}</ItemTitle>
|
||||
<ItemDescription>{holding.shares}</ItemDescription>
|
||||
</ItemContent>
|
||||
<div
|
||||
className="hidden h-8 w-24 items-end gap-1 md:flex"
|
||||
role="img"
|
||||
aria-label={`${holding.name} quarterly dividends`}
|
||||
>
|
||||
{holding.data.map((item) => (
|
||||
<div
|
||||
key={item.q}
|
||||
className="min-h-1 flex-1 rounded-t-sm bg-chart-2"
|
||||
style={{
|
||||
height: `${(item.value / Math.max(...holding.data.map((point) => point.value))) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
37
apps/v4/app/(app)/(root)/cards/empty-distribute-track.tsx
Normal file
37
apps/v4/app/(app)/(root)/cards/empty-distribute-track.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Add01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/base-rhea/ui/empty"
|
||||
|
||||
export function EmptyDistributeTrack() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyMedia variant="icon">
|
||||
<HugeiconsIcon icon={Add01Icon} strokeWidth={2} />
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Distribute Track</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Upload your first master to start reaching listeners on Spotify,
|
||||
Apple Music, and more.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Create Release</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
117
apps/v4/app/(app)/(root)/cards/index.tsx
Normal file
117
apps/v4/app/(app)/(root)/cards/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { AccountAccess } from "./account-access"
|
||||
import { AnalyticsCard } from "./analytics-card"
|
||||
import { ClaimableBalance } from "./claimable-balance"
|
||||
import { ContributionHistory } from "./contribution-history"
|
||||
import { DividendIncome } from "./dividend-income"
|
||||
import { EmptyDistributeTrack } from "./empty-distribute-track"
|
||||
import { NewMilestone } from "./new-milestone"
|
||||
import { NotificationSettings } from "./notification-settings"
|
||||
import { Payments } from "./payments"
|
||||
import { PayoutThreshold } from "./payout-threshold"
|
||||
import { PowerUsage } from "./power-usage"
|
||||
import { QrConnect } from "./qr-connect"
|
||||
import { SavingsTargets } from "./savings-targets"
|
||||
import { SidebarNav } from "./sidebar-nav"
|
||||
import { AccountAccess as SkeletonAccountAccess } from "./skeleton/account-access"
|
||||
import { AnalyticsCard as SkeletonAnalyticsCard } from "./skeleton/analytics-card"
|
||||
import { ClaimableBalance as SkeletonClaimableBalance } from "./skeleton/claimable-balance"
|
||||
import { ContributionHistory as SkeletonContributionHistory } from "./skeleton/contribution-history"
|
||||
import { DividendIncome as SkeletonDividendIncome } from "./skeleton/dividend-income"
|
||||
import { EmptyDistributeTrack as SkeletonEmptyDistributeTrack } from "./skeleton/empty-distribute-track"
|
||||
import { NewMilestone as SkeletonNewMilestone } from "./skeleton/new-milestone"
|
||||
import { NotificationSettings as SkeletonNotificationSettings } from "./skeleton/notification-settings"
|
||||
import { Payments as SkeletonPayments } from "./skeleton/payments"
|
||||
import { PayoutThreshold as SkeletonPayoutThreshold } from "./skeleton/payout-threshold"
|
||||
import { PowerUsage as SkeletonPowerUsage } from "./skeleton/power-usage"
|
||||
import { QrConnect as SkeletonQrConnect } from "./skeleton/qr-connect"
|
||||
import { SavingsTargets as SkeletonSavingsTargets } from "./skeleton/savings-targets"
|
||||
import { TransferFunds as SkeletonTransferFunds } from "./skeleton/transfer-funds"
|
||||
import { UIElements as SkeletonUIElements } from "./skeleton/ui-elements"
|
||||
import { TransferFunds } from "./transfer-funds"
|
||||
import { UIElements } from "./ui-elements"
|
||||
|
||||
function CardsSkeletonRails() {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 top-12 z-10 hidden min-[2200px]:block [&_[data-slot=skeleton]:nth-child(even)]:hidden"
|
||||
>
|
||||
<div className="absolute top-0 left-[calc(50%-950px-var(--rail-width)-var(--gap))] grid w-(--rail-width) grid-cols-[repeat(2,var(--rail-column))] gap-(--gap) opacity-50 [--rail-column:20rem] [--rail-width:calc(var(--rail-column)*2+var(--gap))]">
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SkeletonContributionHistory />
|
||||
<SkeletonClaimableBalance />
|
||||
<SkeletonDividendIncome />
|
||||
<SkeletonPayoutThreshold />
|
||||
</div>
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SkeletonUIElements />
|
||||
<SkeletonSavingsTargets />
|
||||
<SkeletonNewMilestone />
|
||||
<SkeletonPayoutThreshold />
|
||||
<SkeletonAccountAccess />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 right-[calc(50%-950px-var(--rail-width)-var(--gap))] grid w-(--rail-width) grid-cols-[repeat(2,var(--rail-column))] gap-(--gap) opacity-50 [--rail-column:20rem] [--rail-width:calc(var(--rail-column)*2+var(--gap))]">
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SkeletonNewMilestone />
|
||||
<SkeletonPayoutThreshold />
|
||||
<SkeletonAccountAccess />
|
||||
<SkeletonQrConnect />
|
||||
<SkeletonTransferFunds />
|
||||
<SkeletonPayments />
|
||||
<SkeletonEmptyDistributeTrack />
|
||||
</div>
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SkeletonQrConnect />
|
||||
<SkeletonTransferFunds />
|
||||
<SkeletonPayments />
|
||||
<SkeletonEmptyDistributeTrack />
|
||||
<SkeletonAnalyticsCard />
|
||||
<SkeletonNotificationSettings />
|
||||
<SkeletonPowerUsage />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardsDemo() {
|
||||
return (
|
||||
<div
|
||||
data-slot="demo"
|
||||
className="theme-neutral relative flex w-full max-w-none flex-col gap-(--gap) overflow-hidden bg-muted p-12 pb-0! [--gap:--spacing(8)] 3xl:[--gap:--spacing(8)] min-[1900px]:p-12 min-[1900px]:[--gap:--spacing(10)]! lg:p-6 lg:[--gap:--spacing(6)] dark:bg-background"
|
||||
>
|
||||
<CardsSkeletonRails />
|
||||
<div className="relative z-10 mx-auto grid gap-(--gap) **:data-[slot=card]:w-full min-[1400px]:grid-cols-4! min-[1900px]:grid-cols-5! md:max-w-3xl md:grid-cols-2 lg:max-w-none lg:grid-cols-3 xl:max-w-[1600px] 2xl:max-w-[1900px]">
|
||||
<div className="flex flex-col items-start gap-(--gap)">
|
||||
<UIElements />
|
||||
<SidebarNav />
|
||||
<SavingsTargets />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) lg:flex">
|
||||
<ContributionHistory />
|
||||
<ClaimableBalance />
|
||||
<DividendIncome />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) 3xl:flex!">
|
||||
<NewMilestone />
|
||||
<PayoutThreshold />
|
||||
<AccountAccess />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) md:flex">
|
||||
<QrConnect />
|
||||
<TransferFunds />
|
||||
<Payments />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) min-[1400px]:flex">
|
||||
<EmptyDistributeTrack />
|
||||
<AnalyticsCard />
|
||||
<NotificationSettings />
|
||||
<PowerUsage />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-x-0 top-0 z-1 h-120 bg-linear-to-b from-background via-muted to-transparent dark:hidden" />
|
||||
<div className="absolute inset-x-0 bottom-0 z-20 h-48 bg-linear-to-t from-background via-muted/80 to-transparent lg:h-80 xl:h-64 dark:via-background/80" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
apps/v4/app/(app)/(root)/cards/new-milestone.tsx
Normal file
52
apps/v4/app/(app)/(root)/cards/new-milestone.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
|
||||
import { Input } from "@/styles/base-rhea/ui/input"
|
||||
|
||||
export function NewMilestone() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Set a new milestone</CardTitle>
|
||||
<CardDescription>
|
||||
Define your financial target and we'll help you pace your
|
||||
savings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="goal-name">Goal Name</FieldLabel>
|
||||
<Input
|
||||
id="goal-name"
|
||||
placeholder="e.g. New Car, Home Downpayment"
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="target-amount">Target Amount</FieldLabel>
|
||||
<Input id="target-amount" defaultValue="$15,000" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="target-date">Target Date</FieldLabel>
|
||||
<Input id="target-date" defaultValue="Dec 2025" />
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Button className="w-full">Create Goal</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Cancel
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
76
apps/v4/app/(app)/(root)/cards/notification-settings.tsx
Normal file
76
apps/v4/app/(app)/(root)/cards/notification-settings.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Checkbox } from "@/styles/base-rhea/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/styles/base-rhea/ui/field"
|
||||
|
||||
const NOTIFICATIONS = [
|
||||
{
|
||||
id: "transactions",
|
||||
label: "Transaction alerts",
|
||||
description: "Deposits, withdrawals, and transfers.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
label: "Security alerts",
|
||||
description: "Login attempts and account changes.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "goals",
|
||||
label: "Goal milestones",
|
||||
description: "Updates at 25%, 50%, 75%, and 100%.",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "market",
|
||||
label: "Market updates",
|
||||
description: "Daily portfolio summary and price alerts.",
|
||||
defaultChecked: false,
|
||||
},
|
||||
]
|
||||
|
||||
export function NotificationSettings() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notifications</CardTitle>
|
||||
<CardDescription>
|
||||
Choose which email and push alerts you want to receive.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
{NOTIFICATIONS.map((n) => (
|
||||
<Field key={n.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={`notify-${n.id}`}
|
||||
defaultChecked={n.defaultChecked}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={`notify-${n.id}`}>{n.label}</FieldLabel>
|
||||
<FieldDescription>{n.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Save Preferences</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
139
apps/v4/app/(app)/(root)/cards/payments.tsx
Normal file
139
apps/v4/app/(app)/(root)/cards/payments.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
ArrowRight01Icon,
|
||||
Calendar03Icon,
|
||||
MoreHorizontalCircle01Icon,
|
||||
RefreshIcon,
|
||||
Settings01Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/styles/base-rhea/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-rhea/ui/dropdown-menu"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-rhea/ui/item"
|
||||
|
||||
export function Payments() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-3">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
aria-label="Account options"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={MoreHorizontalCircle01Icon}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span className="sr-only">Account options</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Statements</DropdownMenuItem>
|
||||
<DropdownMenuItem>Documents</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Payments</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
<div role="listitem" className="w-full">
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<HugeiconsIcon icon={Settings01Icon} strokeWidth={2} />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Change transfer limit</ItemTitle>
|
||||
<ItemDescription>
|
||||
Adjust how much you can send from your balance.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Item>
|
||||
</div>
|
||||
<div role="listitem" className="w-full">
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<HugeiconsIcon icon={Calendar03Icon} strokeWidth={2} />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Scheduled transfers</ItemTitle>
|
||||
<ItemDescription>
|
||||
Set up a transfer to send at a later date.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Item>
|
||||
</div>
|
||||
<div role="listitem" className="w-full">
|
||||
<Item variant="muted" render={<a href="#" />}>
|
||||
<ItemMedia variant="icon">
|
||||
<HugeiconsIcon icon={RefreshIcon} strokeWidth={2} />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Recurring card payments</ItemTitle>
|
||||
<ItemDescription>
|
||||
Manage your repeated card transactions.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Item>
|
||||
</div>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
112
apps/v4/app/(app)/(root)/cards/payout-threshold.tsx
Normal file
112
apps/v4/app/(app)/(root)/cards/payout-threshold.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Cancel01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/styles/base-rhea/ui/field"
|
||||
import { Progress } from "@/styles/base-rhea/ui/progress"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/base-rhea/ui/select"
|
||||
import { Textarea } from "@/styles/base-rhea/ui/textarea"
|
||||
|
||||
const CURRENCIES = [
|
||||
{ label: "USD — United States Dollar", value: "usd" },
|
||||
{ label: "EUR — Euro", value: "eur" },
|
||||
{ label: "GBP — British Pound", value: "gbp" },
|
||||
{ label: "JPY — Japanese Yen", value: "jpy" },
|
||||
]
|
||||
|
||||
export function PayoutThreshold() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Payout Threshold</CardTitle>
|
||||
<CardDescription>
|
||||
Set the minimum balance required before a payout is triggered.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="bg-muted"
|
||||
aria-label="Dismiss payout threshold"
|
||||
>
|
||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="preferred-currency">
|
||||
Preferred Currency
|
||||
</FieldLabel>
|
||||
<Select items={CURRENCIES} defaultValue="usd">
|
||||
<SelectTrigger id="preferred-currency" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{CURRENCIES.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<FieldLabel id="min-payout-label">
|
||||
Minimum Payout Amount
|
||||
</FieldLabel>
|
||||
<span className="text-2xl font-semibold tabular-nums">
|
||||
$2500.00
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={25}
|
||||
aria-labelledby="min-payout-label"
|
||||
aria-valuetext="$2,500 of $10,000"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldDescription>$50 (MIN)</FieldDescription>
|
||||
<FieldDescription>$10,000 (MAX)</FieldDescription>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="payout-notes">Notes</FieldLabel>
|
||||
<Textarea
|
||||
id="payout-notes"
|
||||
placeholder="Add any notes for this payout configuration..."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Save Threshold</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
67
apps/v4/app/(app)/(root)/cards/power-usage.tsx
Normal file
67
apps/v4/app/(app)/(root)/cards/power-usage.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Separator } from "@/styles/base-rhea/ui/separator"
|
||||
|
||||
const chartData = [
|
||||
{ hour: "6a", usage: 1.2 },
|
||||
{ hour: "8a", usage: 2.8 },
|
||||
{ hour: "10a", usage: 3.1 },
|
||||
{ hour: "12p", usage: 2.4 },
|
||||
{ hour: "2p", usage: 3.4 },
|
||||
{ hour: "4p", usage: 2.9 },
|
||||
{ hour: "6p", usage: 3.8 },
|
||||
{ hour: "8p", usage: 3.2 },
|
||||
]
|
||||
|
||||
export function PowerUsage() {
|
||||
const maxUsage = Math.max(...chartData.map((item) => item.usage))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Power Usage</CardTitle>
|
||||
<CardDescription>Whole Home</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div
|
||||
className="flex h-[140px] w-full items-end gap-2"
|
||||
role="img"
|
||||
aria-label="Power usage by hour"
|
||||
>
|
||||
{chartData.map((item) => (
|
||||
<div
|
||||
key={item.hour}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-1.5"
|
||||
>
|
||||
<div
|
||||
className="min-h-2 rounded-t bg-chart-2"
|
||||
style={{ height: `${(item.usage / maxUsage) * 100}%` }}
|
||||
/>
|
||||
<span className="text-center text-xs text-muted-foreground">
|
||||
{item.hour}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Currently Using
|
||||
</span>
|
||||
<span className="text-lg font-semibold tabular-nums">3.4 kW</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-muted-foreground">Solar Gen</span>
|
||||
<span className="text-lg font-semibold tabular-nums">+1.2 kW</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
64
apps/v4/app/(app)/(root)/cards/qr-connect.tsx
Normal file
64
apps/v4/app/(app)/(root)/cards/qr-connect.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
|
||||
const qrCells = [
|
||||
"111111100101101111111",
|
||||
"100000101001001000001",
|
||||
"101110101111101011101",
|
||||
"101110100100001011101",
|
||||
"101110101010101011101",
|
||||
"100000100111001000001",
|
||||
"111111101010101111111",
|
||||
"000000001101000000000",
|
||||
"101011111001111010110",
|
||||
"010100001110010101001",
|
||||
"111010111011101111010",
|
||||
"001101000101000010101",
|
||||
"110111101111010111011",
|
||||
"000000001001010001010",
|
||||
"111111101101111101001",
|
||||
"100000100010001001111",
|
||||
"101110101011101110100",
|
||||
"101110100110100010011",
|
||||
"101110101000111101110",
|
||||
"100000101101000011001",
|
||||
"111111101011101101111",
|
||||
]
|
||||
|
||||
export function QrConnect() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex justify-center pt-6">
|
||||
<div className="rounded-xl border bg-white p-4">
|
||||
<svg
|
||||
viewBox="0 0 21 21"
|
||||
className="size-40 text-black"
|
||||
role="img"
|
||||
aria-label="Connect device QR code"
|
||||
shapeRendering="crispEdges"
|
||||
>
|
||||
<rect width="21" height="21" fill="white" />
|
||||
{qrCells.map((row, y) =>
|
||||
[...row].map((cell, x) =>
|
||||
cell === "1" ? (
|
||||
<rect key={`${x}-${y}`} x={x} y={y} width="1" height="1" />
|
||||
) : null
|
||||
)
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Scan to connect your mobile device</CardTitle>
|
||||
<CardDescription className="text-balance">
|
||||
Open the Ledger mobile app and scan this code to link your device.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
81
apps/v4/app/(app)/(root)/cards/savings-targets.tsx
Normal file
81
apps/v4/app/(app)/(root)/cards/savings-targets.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
} from "@/styles/base-rhea/ui/item"
|
||||
import { Progress } from "@/styles/base-rhea/ui/progress"
|
||||
|
||||
export function SavingsTargets() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Savings Targets</CardTitle>
|
||||
<CardDescription>
|
||||
Active milestones for 2024 across your portfolio. Monitor how close
|
||||
you are to each savings goal.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup className="gap-3">
|
||||
<Item
|
||||
role="listitem"
|
||||
variant="muted"
|
||||
className="flex-col items-stretch"
|
||||
>
|
||||
<ItemContent className="gap-3">
|
||||
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Retirement
|
||||
</ItemDescription>
|
||||
<span className="text-3xl font-semibold tabular-nums">
|
||||
$420,000
|
||||
</span>
|
||||
<Progress value={65} aria-label="Retirement savings progress" />
|
||||
</ItemContent>
|
||||
<ItemFooter>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
65% achieved
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$273,000</span>
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
<Item
|
||||
role="listitem"
|
||||
variant="muted"
|
||||
className="flex-col items-stretch"
|
||||
>
|
||||
<ItemContent className="gap-3">
|
||||
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Real Estate
|
||||
</ItemDescription>
|
||||
<span className="text-3xl font-semibold tabular-nums">
|
||||
$85,000
|
||||
</span>
|
||||
<Progress value={32} aria-label="Real estate savings progress" />
|
||||
</ItemContent>
|
||||
<ItemFooter>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
32% achieved
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$27,200</span>
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardDescription className="text-center">
|
||||
You have not met your targets for this year.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
218
apps/v4/app/(app)/(root)/cards/sidebar-nav.tsx
Normal file
218
apps/v4/app/(app)/(root)/cards/sidebar-nav.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ActivityIcon,
|
||||
Analytics01Icon,
|
||||
AnalyticsUpIcon,
|
||||
ArrowDataTransferHorizontalIcon,
|
||||
BankIcon,
|
||||
BookOpen02Icon,
|
||||
Calendar03Icon,
|
||||
ChartBarLineIcon,
|
||||
CreditCardIcon,
|
||||
File02Icon,
|
||||
Globe02Icon,
|
||||
HelpCircleIcon,
|
||||
Message01Icon,
|
||||
Notification03Icon,
|
||||
PaintBoardIcon,
|
||||
PieChartIcon,
|
||||
ShieldIcon,
|
||||
Target02Icon,
|
||||
UserIcon,
|
||||
Wallet01Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Card } from "@/styles/base-rhea/ui/card"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
} from "@/styles/base-rhea/ui/sidebar"
|
||||
|
||||
function SidebarSection({
|
||||
label,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Card className={cn("w-full overflow-hidden rounded-3xl py-0", className)}>
|
||||
<SidebarProvider className="min-h-0">
|
||||
<Sidebar collapsible="none" className="w-full bg-transparent">
|
||||
<SidebarContent className="gap-0 overflow-hidden">
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{label}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="gap-1">{children}</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarNav() {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-2 gap-4 xl:gap-6">
|
||||
<SidebarSection
|
||||
label="Overview"
|
||||
className="xl:col-start-1 xl:row-start-2"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton isActive>
|
||||
<HugeiconsIcon icon={Analytics01Icon} strokeWidth={2} />
|
||||
Analytics
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowDataTransferHorizontalIcon}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
Transactions
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={AnalyticsUpIcon} strokeWidth={2} />
|
||||
Investments
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={BankIcon} strokeWidth={2} />
|
||||
Accounts
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={PieChartIcon} strokeWidth={2} />
|
||||
Spending
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarSection>
|
||||
|
||||
<SidebarSection
|
||||
label="Planning"
|
||||
className="xl:col-start-1 xl:row-start-1"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={File02Icon} strokeWidth={2} />
|
||||
Documents
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Wallet01Icon} strokeWidth={2} />
|
||||
Budget
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={ChartBarLineIcon} strokeWidth={2} />
|
||||
Reports
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Target02Icon} strokeWidth={2} />
|
||||
Goals
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Calendar03Icon} strokeWidth={2} />
|
||||
Calendar
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarSection>
|
||||
|
||||
<SidebarSection
|
||||
label="Support"
|
||||
className="flex xl:col-start-2 xl:row-start-1"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={HelpCircleIcon} strokeWidth={2} />
|
||||
Help Center
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={BookOpen02Icon} strokeWidth={2} />
|
||||
Docs
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Message01Icon} strokeWidth={2} />
|
||||
Contact Us
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={ActivityIcon} strokeWidth={2} />
|
||||
Status
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Globe02Icon} strokeWidth={2} />
|
||||
Community
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarSection>
|
||||
|
||||
<SidebarSection
|
||||
label="Account"
|
||||
className="flex xl:col-start-2 xl:row-start-2"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={UserIcon} strokeWidth={2} />
|
||||
Profile
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton isActive>
|
||||
<HugeiconsIcon icon={CreditCardIcon} strokeWidth={2} />
|
||||
Billing
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={Notification03Icon} strokeWidth={2} />
|
||||
Notifications
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={ShieldIcon} strokeWidth={2} />
|
||||
Security
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<HugeiconsIcon icon={PaintBoardIcon} strokeWidth={2} />
|
||||
Appearance
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
apps/v4/app/(app)/(root)/cards/skeleton/account-access.tsx
Normal file
35
apps/v4/app/(app)/(root)/cards/skeleton/account-access.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function AccountAccess() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-36 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-3 w-32 rounded-md" />
|
||||
<Skeleton className="h-3 w-12 rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
<Skeleton className="h-14 w-full rounded-xl" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
apps/v4/app/(app)/(root)/cards/skeleton/analytics-card.tsx
Normal file
17
apps/v4/app/(app)/(root)/cards/skeleton/analytics-card.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardAction, CardHeader } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function AnalyticsCard() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-24 rounded-md" />
|
||||
<Skeleton className="h-4 w-40 rounded-md" />
|
||||
<CardAction>
|
||||
<Skeleton className="h-7 w-28 rounded-lg" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<Skeleton className="mx-6 mb-6 aspect-[1/0.35] w-auto rounded-lg" />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function ClaimableBalance() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-3">
|
||||
<Skeleton className="h-4 w-36 rounded-md" />
|
||||
<Skeleton className="h-12 w-56 rounded-lg" />
|
||||
<Skeleton className="h-6 w-32 rounded-full" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col justify-end">
|
||||
<div className="flex flex-col gap-3 rounded-xl bg-muted p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-20 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-32 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-16 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-36 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Skeleton className="h-3 w-full rounded-md" />
|
||||
<Skeleton className="h-3 w-11/12 rounded-md" />
|
||||
<Skeleton className="h-3 w-3/4 rounded-md" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const bars = [60, 80, 65, 95, 50, 100]
|
||||
|
||||
export function ContributionHistory() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-44 rounded-md" />
|
||||
<Skeleton className="h-4 w-52 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-[200px] w-full items-end gap-3">
|
||||
{bars.map((height, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-2"
|
||||
>
|
||||
<Skeleton
|
||||
className="w-full rounded-t-md rounded-b-none"
|
||||
style={{ height: `${height}%` }}
|
||||
/>
|
||||
<Skeleton className="mx-auto h-3 w-6 rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<div className="flex flex-col gap-2 rounded-xl bg-muted p-4">
|
||||
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-5 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-2 rounded-xl bg-muted p-4 xl:flex">
|
||||
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-5 w-32 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-28 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
49
apps/v4/app/(app)/(root)/cards/skeleton/dividend-income.tsx
Normal file
49
apps/v4/app/(app)/(root)/cards/skeleton/dividend-income.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const rows = [0, 1, 2, 3]
|
||||
const miniBars = [40, 60, 80, 50]
|
||||
|
||||
export function DividendIncome() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-48 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
<CardAction>
|
||||
<Skeleton className="size-8 rounded-md" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row}
|
||||
className="flex items-center gap-3 rounded-xl bg-muted p-3"
|
||||
>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<div className="hidden h-8 w-24 items-end gap-1 md:flex">
|
||||
{miniBars.map((h, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
className="flex-1 rounded-t-sm rounded-b-none bg-muted-foreground/15"
|
||||
style={{ height: `${h}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="hidden h-4 w-16 rounded-md bg-muted-foreground/15 md:block" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function EmptyDistributeTrack() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center gap-4 p-4">
|
||||
<Skeleton className="size-12 rounded-xl" />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Skeleton className="h-5 w-40 rounded-md" />
|
||||
<Skeleton className="h-3 w-64 rounded-md" />
|
||||
<Skeleton className="h-3 w-48 rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-32 rounded-lg" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
56
apps/v4/app/(app)/(root)/cards/skeleton/index.tsx
Normal file
56
apps/v4/app/(app)/(root)/cards/skeleton/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { AccountAccess } from "./account-access"
|
||||
import { AnalyticsCard } from "./analytics-card"
|
||||
import { ClaimableBalance } from "./claimable-balance"
|
||||
import { ContributionHistory } from "./contribution-history"
|
||||
import { DividendIncome } from "./dividend-income"
|
||||
import { EmptyDistributeTrack } from "./empty-distribute-track"
|
||||
import { NewMilestone } from "./new-milestone"
|
||||
import { NotificationSettings } from "./notification-settings"
|
||||
import { Payments } from "./payments"
|
||||
import { PayoutThreshold } from "./payout-threshold"
|
||||
import { PowerUsage } from "./power-usage"
|
||||
import { QrConnect } from "./qr-connect"
|
||||
import { SavingsTargets } from "./savings-targets"
|
||||
import { SidebarNav } from "./sidebar-nav"
|
||||
import { TransferFunds } from "./transfer-funds"
|
||||
import { UIElements } from "./ui-elements"
|
||||
|
||||
export function CardsSkeletonDemo() {
|
||||
return (
|
||||
<div
|
||||
data-slot="demo"
|
||||
className="theme-neutral relative flex w-full max-w-none flex-col gap-(--gap) bg-muted p-12 pb-0! [--gap:--spacing(8)] 3xl:[--gap:--spacing(8)] min-[1900px]:[--gap:--spacing(10)]! lg:p-8 lg:[--gap:--spacing(6)] xl:p-12 dark:bg-muted/30"
|
||||
>
|
||||
<div className="relative z-10 mx-auto grid gap-(--gap) **:data-[slot=card]:w-full min-[1900px]:grid-cols-5! md:max-w-3xl md:grid-cols-2 lg:max-w-none lg:grid-cols-3 xl:max-w-[1600px] xl:grid-cols-4 2xl:max-w-[1900px]">
|
||||
<div className="flex flex-col items-start gap-(--gap)">
|
||||
<UIElements />
|
||||
<SidebarNav />
|
||||
<SavingsTargets />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) lg:flex">
|
||||
<ContributionHistory />
|
||||
<ClaimableBalance />
|
||||
<DividendIncome />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) 3xl:flex!">
|
||||
<NewMilestone />
|
||||
<PayoutThreshold />
|
||||
<AccountAccess />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) md:flex">
|
||||
<QrConnect />
|
||||
<TransferFunds />
|
||||
<Payments />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) xl:flex">
|
||||
<EmptyDistributeTrack />
|
||||
<AnalyticsCard />
|
||||
<NotificationSettings />
|
||||
<PowerUsage />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-x-0 top-0 z-1 h-80 bg-linear-to-b from-background via-muted to-transparent dark:via-muted/30" />
|
||||
<div className="absolute inset-x-0 bottom-0 z-20 h-80 bg-linear-to-t from-background via-muted to-transparent dark:via-muted/30" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
apps/v4/app/(app)/(root)/cards/skeleton/new-milestone.tsx
Normal file
38
apps/v4/app/(app)/(root)/cards/skeleton/new-milestone.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function NewMilestone() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-44 rounded-md" />
|
||||
<Skeleton className="h-4 w-72 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const rows = [0, 1, 2, 3]
|
||||
|
||||
export function NotificationSettings() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-32 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{rows.map((row) => (
|
||||
<div key={row} className="flex items-start gap-3">
|
||||
<Skeleton className="size-4 rounded-sm" />
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-40 rounded-md" />
|
||||
<Skeleton className="h-3 w-56 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
37
apps/v4/app/(app)/(root)/cards/skeleton/payments.tsx
Normal file
37
apps/v4/app/(app)/(root)/cards/skeleton/payments.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const rows = [0, 1, 2]
|
||||
|
||||
export function Payments() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-12 rounded-md" />
|
||||
<Skeleton className="size-1.5 rounded-full" />
|
||||
<Skeleton className="size-7 rounded-md" />
|
||||
<Skeleton className="size-1.5 rounded-full" />
|
||||
<Skeleton className="h-4 w-20 rounded-md" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row}
|
||||
className="flex items-center gap-3 rounded-xl bg-muted p-3"
|
||||
>
|
||||
<Skeleton className="size-9 rounded-lg bg-muted-foreground/15" />
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-40 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-56 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<Skeleton className="size-4 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
43
apps/v4/app/(app)/(root)/cards/skeleton/payout-threshold.tsx
Normal file
43
apps/v4/app/(app)/(root)/cards/skeleton/payout-threshold.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function PayoutThreshold() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-44 rounded-md" />
|
||||
<Skeleton className="h-4 w-72 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-32 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<Skeleton className="h-3 w-40 rounded-md" />
|
||||
<Skeleton className="h-7 w-24 rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-3 w-16 rounded-md" />
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-16 rounded-md" />
|
||||
<Skeleton className="h-[100px] w-full rounded-lg" />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
54
apps/v4/app/(app)/(root)/cards/skeleton/power-usage.tsx
Normal file
54
apps/v4/app/(app)/(root)/cards/skeleton/power-usage.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const bars = [30, 70, 80, 60, 90, 75, 100, 85]
|
||||
|
||||
export function PowerUsage() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-32 rounded-md" />
|
||||
<Skeleton className="h-4 w-24 rounded-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex h-[140px] w-full items-end gap-2">
|
||||
{bars.map((height, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-1.5"
|
||||
>
|
||||
<Skeleton
|
||||
className="w-full rounded-t rounded-b-none"
|
||||
style={{ height: `${height}%` }}
|
||||
/>
|
||||
<Skeleton className="mx-auto h-3 w-5 rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-px w-full rounded-none" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Skeleton className="h-3 w-28 rounded-md" />
|
||||
<Skeleton className="h-5 w-20 rounded-md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
<Skeleton className="h-5 w-24 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2">
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Skeleton className="h-2 flex-1 rounded-full" />
|
||||
<Skeleton className="h-3 w-10 rounded-md" />
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
apps/v4/app/(app)/(root)/cards/skeleton/qr-connect.tsx
Normal file
17
apps/v4/app/(app)/(root)/cards/skeleton/qr-connect.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function QrConnect() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex justify-center pt-6">
|
||||
<Skeleton className="size-44 rounded-xl" />
|
||||
</CardContent>
|
||||
<CardHeader className="items-center gap-2 text-center">
|
||||
<Skeleton className="h-5 w-56 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
<Skeleton className="h-4 w-48 rounded-md" />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
44
apps/v4/app/(app)/(root)/cards/skeleton/savings-targets.tsx
Normal file
44
apps/v4/app/(app)/(root)/cards/skeleton/savings-targets.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const rows = [0, 1]
|
||||
|
||||
export function SavingsTargets() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-36 rounded-md" />
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Skeleton className="h-4 w-full max-w-64 rounded-md" />
|
||||
<Skeleton className="h-4 w-48 rounded-md" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row}
|
||||
className="flex flex-col gap-3 rounded-xl bg-muted p-4"
|
||||
>
|
||||
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-8 w-36 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-2 w-full rounded-full bg-muted-foreground/15" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<Skeleton className="h-3 w-56 rounded-md" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
39
apps/v4/app/(app)/(root)/cards/skeleton/sidebar-nav.tsx
Normal file
39
apps/v4/app/(app)/(root)/cards/skeleton/sidebar-nav.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Card } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
const groupA = [0, 1, 2, 3, 4]
|
||||
const groupB = [0, 1, 2, 3, 4]
|
||||
|
||||
function NavSkeleton({ groups }: { groups: number[][] }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 p-2">
|
||||
{groups.map((items, gi) => (
|
||||
<div key={gi} className="flex flex-col gap-1 px-2 py-1.5">
|
||||
<Skeleton className="mb-1 h-3 w-20 rounded-md" />
|
||||
{items.map((item) => (
|
||||
<div key={item} className="flex items-center gap-2 px-2 py-2">
|
||||
<Skeleton className="size-4 rounded-md" />
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
{gi < groups.length - 1 && (
|
||||
<Skeleton className="my-1 h-px w-full rounded-none" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarNav() {
|
||||
return (
|
||||
<div className="grid w-full items-start gap-4 xl:grid-cols-2 xl:gap-6">
|
||||
<Card className="w-full overflow-hidden rounded-3xl py-0">
|
||||
<NavSkeleton groups={[groupA, groupB]} />
|
||||
</Card>
|
||||
<Card className="hidden w-full overflow-hidden rounded-3xl py-0 xl:flex">
|
||||
<NavSkeleton groups={[groupA, groupB]} />
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
apps/v4/app/(app)/(root)/cards/skeleton/transfer-funds.tsx
Normal file
55
apps/v4/app/(app)/(root)/cards/skeleton/transfer-funds.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function TransferFunds() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="gap-2">
|
||||
<Skeleton className="h-5 w-36 rounded-md" />
|
||||
<Skeleton className="h-4 w-64 rounded-md" />
|
||||
<CardAction>
|
||||
<Skeleton className="size-8 rounded-md" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-32 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 rounded-xl bg-muted p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-12 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
|
||||
<Skeleton className="h-4 w-20 rounded-md bg-muted-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
45
apps/v4/app/(app)/(root)/cards/skeleton/ui-elements.tsx
Normal file
45
apps/v4/app/(app)/(root)/cards/skeleton/ui-elements.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
|
||||
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
|
||||
|
||||
export function UIElements() {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<Skeleton className="h-8 w-full rounded-2xl" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-9 w-20 rounded-lg" />
|
||||
<Skeleton className="h-9 w-24 rounded-lg" />
|
||||
<Skeleton className="h-9 w-20 rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Skeleton className="h-9 w-full rounded-lg" />
|
||||
<Skeleton className="h-20 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-5 w-12 rounded-full" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
<Skeleton className="hidden h-5 w-14 rounded-full 4xl:block" />
|
||||
</div>
|
||||
<div className="ml-auto flex gap-3">
|
||||
<Skeleton className="size-4 rounded-full" />
|
||||
<Skeleton className="size-4 rounded-full" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Skeleton className="size-4 rounded-sm" />
|
||||
<Skeleton className="hidden size-4 rounded-sm 4xl:block" />
|
||||
</div>
|
||||
<Skeleton className="ml-auto h-5 w-9 rounded-full 4xl:hidden" />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Skeleton className="h-9 w-24 rounded-lg" />
|
||||
<div className="flex">
|
||||
<Skeleton className="h-9 w-28 rounded-l-lg rounded-r-none" />
|
||||
<Skeleton className="ml-px h-9 w-9 rounded-l-none rounded-r-lg" />
|
||||
</div>
|
||||
<Skeleton className="ml-auto hidden h-5 w-9 rounded-full 4xl:block" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
139
apps/v4/app/(app)/(root)/cards/transfer-funds.tsx
Normal file
139
apps/v4/app/(app)/(root)/cards/transfer-funds.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Cancel01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-rhea/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/styles/base-rhea/ui/input-group"
|
||||
import { Item, ItemContent } from "@/styles/base-rhea/ui/item"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/base-rhea/ui/select"
|
||||
import { Separator } from "@/styles/base-rhea/ui/separator"
|
||||
|
||||
const FROM_ACCOUNTS = [
|
||||
{ label: "Main Checking (··8402) — $12,450.00", value: "checking" },
|
||||
{ label: "Business (··7731) — $8,920.00", value: "business" },
|
||||
]
|
||||
|
||||
const TO_ACCOUNTS = [
|
||||
{ label: "High Yield Savings (··1192) — $42,100.00", value: "savings" },
|
||||
{ label: "Investment (··3349) — $18,200.00", value: "investment" },
|
||||
]
|
||||
|
||||
export function TransferFunds() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transfer Funds</CardTitle>
|
||||
<CardDescription>
|
||||
Move money between your connected accounts.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="bg-muted"
|
||||
aria-label="Dismiss transfer funds"
|
||||
>
|
||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="transfer-amount">
|
||||
Amount to Transfer
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>$</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="transfer-amount" defaultValue="1,200.00" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="from-account">From Account</FieldLabel>
|
||||
<Select items={FROM_ACCOUNTS} defaultValue="checking">
|
||||
<SelectTrigger id="from-account" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{FROM_ACCOUNTS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="to-account">To Account</FieldLabel>
|
||||
<Select items={TO_ACCOUNTS} defaultValue="savings">
|
||||
<SelectTrigger id="to-account" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{TO_ACCOUNTS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Estimated arrival
|
||||
</span>
|
||||
<span className="text-sm font-medium">Today, Apr 14</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Transaction fee
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Total amount</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
$1,200.00
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Confirm Transfer</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
176
apps/v4/app/(app)/(root)/cards/ui-elements.tsx
Normal file
176
apps/v4/app/(app)/(root)/cards/ui-elements.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
ArrowRight02Icon,
|
||||
ArrowUp01Icon,
|
||||
Search01Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/styles/base-rhea/ui/alert-dialog"
|
||||
import { Badge } from "@/styles/base-rhea/ui/badge"
|
||||
import { Button } from "@/styles/base-rhea/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-rhea/ui/button-group"
|
||||
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
|
||||
import { Checkbox } from "@/styles/base-rhea/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-rhea/ui/dropdown-menu"
|
||||
import { Field, FieldGroup } from "@/styles/base-rhea/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/styles/base-rhea/ui/input-group"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/base-rhea/ui/radio-group"
|
||||
import { Switch } from "@/styles/base-rhea/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/styles/base-rhea/ui/tabs"
|
||||
import { Textarea } from "@/styles/base-rhea/ui/textarea"
|
||||
|
||||
export function UIElements() {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex gap-2">
|
||||
<Button>
|
||||
Button{" "}
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight02Icon}
|
||||
strokeWidth={2}
|
||||
data-icon="inline-end"
|
||||
/>
|
||||
</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
</div>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>
|
||||
<HugeiconsIcon icon={Search01Icon} strokeWidth={2} />
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field className="flex-1">
|
||||
<Textarea placeholder="Message" className="resize-none" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Badge>Badge</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline" className="hidden 4xl:flex">
|
||||
Outline
|
||||
</Badge>
|
||||
</div>
|
||||
<RadioGroup
|
||||
defaultValue="apple"
|
||||
className="ml-auto flex w-fit gap-3"
|
||||
aria-label="Fruit preference"
|
||||
>
|
||||
<RadioGroupItem value="apple" aria-label="Apple" />
|
||||
<RadioGroupItem value="banana" aria-label="Banana" />
|
||||
</RadioGroup>
|
||||
<div className="flex gap-3">
|
||||
<Checkbox defaultChecked aria-label="Enable email alerts" />
|
||||
<Checkbox
|
||||
className="hidden 4xl:flex"
|
||||
aria-label="Enable push alerts"
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
defaultChecked
|
||||
className="flex 4xl:hidden"
|
||||
aria-label="Enable compact notifications"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||
<span className="hidden md:flex style-sera:md:hidden">
|
||||
Alert Dialog
|
||||
</span>
|
||||
<span className="flex md:hidden style-sera:md:flex">Dialog</span>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Do you want to allow the USB accessory to connect to this
|
||||
device and your data?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Don't allow</AlertDialogCancel>
|
||||
<AlertDialogAction>Allow</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ButtonGroup className="ml-auto">
|
||||
<Button variant="outline">
|
||||
<span className="style-sera:hidden">Button Group</span>
|
||||
<span className="hidden style-sera:block">Group</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Open quick actions"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HugeiconsIcon icon={ArrowUp01Icon} strokeWidth={2} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="top" className="w-40">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Quick Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Mute Conversation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Mark as Read</DropdownMenuItem>
|
||||
<DropdownMenuItem>Block User</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<Switch
|
||||
defaultChecked
|
||||
className="hidden 4xl:flex"
|
||||
aria-label="Enable advanced setting"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Input } from "@/styles/radix-nova/ui/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/radix-nova/ui/radio-group"
|
||||
import { Switch } from "@/styles/radix-nova/ui/switch"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
|
||||
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
|
||||
setGpuCount((prevCount) =>
|
||||
Math.max(1, Math.min(99, prevCount + adjustment))
|
||||
)
|
||||
}, [])
|
||||
|
||||
const handleGpuInputChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!isNaN(value) && value >= 1 && value <= 99) {
|
||||
setGpuCount(value)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Compute Environment</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster. This is the
|
||||
default.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads. (Coming
|
||||
soon)
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
|
||||
<FieldDescription>You can add more later.</FieldDescription>
|
||||
</FieldContent>
|
||||
<ButtonGroup>
|
||||
<Input
|
||||
id="number-of-gpus-f6l"
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 w-14! font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Decrement"
|
||||
onClick={() => handleGpuAdjustment(-1)}
|
||||
disabled={gpuCount <= 1}
|
||||
>
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Increment"
|
||||
onClick={() => handleGpuAdjustment(1)}
|
||||
disabled={gpuCount >= 99}
|
||||
>
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup className="hidden sm:flex">
|
||||
<Button variant="outline" size="icon-sm" aria-label="Go Back">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Archive
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Report
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Snooze
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon-sm" aria-label="More Options">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<MailCheckIcon />
|
||||
Mark as Read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ArchiveIcon />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<ClockIcon />
|
||||
Snooze
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CalendarPlusIcon />
|
||||
Add to Calendar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListFilterIcon />
|
||||
Add to List
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<TagIcon />
|
||||
Label As...
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={label}
|
||||
onValueChange={setLabel}
|
||||
>
|
||||
<DropdownMenuRadioItem value="personal">
|
||||
Personal
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="work">
|
||||
Work
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="other">
|
||||
Other
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<Trash2Icon />
|
||||
Trash
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
return (
|
||||
<ButtonGroup className="[--radius:9999rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon" aria-label="Add">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="flex-1">
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
placeholder={
|
||||
voiceEnabled ? "Record and send audio..." : "Send a message..."
|
||||
}
|
||||
disabled={voiceEnabled}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
onClick={() => setVoiceEnabled(!voiceEnabled)}
|
||||
data-active={voiceEnabled}
|
||||
className="data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
|
||||
aria-pressed={voiceEnabled}
|
||||
size="icon-xs"
|
||||
aria-label="Voice Mode"
|
||||
>
|
||||
<AudioLinesIcon />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Voice Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Previous">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Next">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
import { Separator } from "@/styles/radix-nova/ui/separator"
|
||||
import { Textarea } from "@/styles/radix-nova/ui/textarea"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
<BotIcon /> Copilot
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="icon-sm" aria-label="Open Popover">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="gap-0 rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
|
||||
<Textarea
|
||||
placeholder="Describe your task in natural language."
|
||||
className="mb-4 resize-none"
|
||||
/>
|
||||
<p className="font-medium">Start a new task with Copilot</p>
|
||||
<p className="text-muted-foreground">
|
||||
Describe your task in natural language. Copilot will work in the
|
||||
background and open a pull request for your review.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/styles/radix-nova/ui/avatar"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/radix-nova/ui/empty"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty className="flex-none border py-10">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<AvatarGroup className="grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Invite your team to collaborate on this project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button size="sm">
|
||||
<PlusIcon />
|
||||
Invite Members
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
|
||||
export function EmptyInputGroup() {
|
||||
return (
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching for
|
||||
what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<EmptyDescription>
|
||||
Need help? <a href="#">Contact support</a>
|
||||
</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
<FieldLabel htmlFor="checkbox-demo">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="checkbox-demo" defaultChecked />
|
||||
<FieldLabel htmlFor="checkbox-demo" className="line-clamp-1">
|
||||
I agree to the terms and conditions
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
|
||||
export function FieldChoiceCard() {
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLabel htmlFor="compute-environment-p8w">
|
||||
Compute Environment
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Input } from "@/styles/radix-nova/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/radix-nova/ui/select"
|
||||
import { Textarea } from "@/styles/radix-nova/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-xl border p-6">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Payment Method</FieldLegend>
|
||||
<FieldDescription>
|
||||
All transactions are secure and encrypted
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
|
||||
Name on Card
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-name-43j"
|
||||
placeholder="John Doe"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
|
||||
Card Number
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="checkout-7j9-card-number-uw1"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
required
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your 16-digit number.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field className="col-span-1">
|
||||
<FieldLabel htmlFor="checkout-7j9-cvv">CVV</FieldLabel>
|
||||
<Input id="checkout-7j9-cvv" placeholder="123" required />
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-month-ts6">
|
||||
Month
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-month-ts6">
|
||||
<SelectValue placeholder="MM" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="01">01</SelectItem>
|
||||
<SelectItem value="02">02</SelectItem>
|
||||
<SelectItem value="03">03</SelectItem>
|
||||
<SelectItem value="04">04</SelectItem>
|
||||
<SelectItem value="05">05</SelectItem>
|
||||
<SelectItem value="06">06</SelectItem>
|
||||
<SelectItem value="07">07</SelectItem>
|
||||
<SelectItem value="08">08</SelectItem>
|
||||
<SelectItem value="09">09</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="11">11</SelectItem>
|
||||
<SelectItem value="12">12</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
|
||||
Year
|
||||
</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="checkout-7j9-exp-year-f59">
|
||||
<SelectValue placeholder="YYYY" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="2024">2024</SelectItem>
|
||||
<SelectItem value="2025">2025</SelectItem>
|
||||
<SelectItem value="2026">2026</SelectItem>
|
||||
<SelectItem value="2027">2027</SelectItem>
|
||||
<SelectItem value="2028">2028</SelectItem>
|
||||
<SelectItem value="2029">2029</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLegend>Billing Address</FieldLegend>
|
||||
<FieldDescription>
|
||||
The billing address associated with your payment method
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="checkout-7j9-same-as-shipping-wgm"
|
||||
defaultChecked
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor="checkout-7j9-same-as-shipping-wgm"
|
||||
className="font-normal"
|
||||
>
|
||||
Same as shipping address
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="checkout-7j9-optional-comments">
|
||||
Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="checkout-7j9-optional-comments"
|
||||
placeholder="Add any additional comments"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Cancel
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Card, CardContent } from "@/styles/radix-nova/ui/card"
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "Social Media",
|
||||
value: "social-media",
|
||||
},
|
||||
|
||||
{
|
||||
label: "Search Engine",
|
||||
value: "search-engine",
|
||||
},
|
||||
{
|
||||
label: "Referral",
|
||||
value: "referral",
|
||||
},
|
||||
{
|
||||
label: "Other",
|
||||
value: "other",
|
||||
},
|
||||
]
|
||||
|
||||
export function FieldHear() {
|
||||
return (
|
||||
<Card className="py-4 shadow-none">
|
||||
<CardContent className="px-4">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend>How did you hear about us?</FieldLegend>
|
||||
<FieldDescription className="line-clamp-1">
|
||||
Select the option that best describes how you heard about us.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
|
||||
{options.map((option) => (
|
||||
<FieldLabel
|
||||
htmlFor={option.value}
|
||||
key={option.value}
|
||||
className="w-fit!"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="gap-1.5 overflow-hidden px-3! py-1.5! transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:px-2!"
|
||||
>
|
||||
<Checkbox
|
||||
value={option.value}
|
||||
id={option.value}
|
||||
defaultChecked={option.value === "social-media"}
|
||||
className="-ml-6 -translate-x-1 rounded-full transition-all duration-100 ease-linear data-[state=checked]:ml-0 data-[state=checked]:translate-x-0"
|
||||
/>
|
||||
<FieldTitle>{option.label}</FieldTitle>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Slider } from "@/styles/radix-nova/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<Field>
|
||||
<FieldTitle>Price Range</FieldTitle>
|
||||
<FieldDescription>
|
||||
Set your budget range ($
|
||||
<span className="font-medium tabular-nums">{value[0]}</span> -{" "}
|
||||
<span className="font-medium tabular-nums">{value[1]}</span>).
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
max={1000}
|
||||
min={0}
|
||||
step={10}
|
||||
className="mt-2 w-full"
|
||||
aria-label="Price Range"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { FieldSeparator } from "@/styles/radix-nova/ui/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
import { ButtonGroupInputGroup } from "./button-group-input-group"
|
||||
import { ButtonGroupNested } from "./button-group-nested"
|
||||
import { ButtonGroupPopover } from "./button-group-popover"
|
||||
import { EmptyAvatarGroup } from "./empty-avatar-group"
|
||||
import { FieldCheckbox } from "./field-checkbox"
|
||||
import { FieldDemo } from "./field-demo"
|
||||
import { FieldHear } from "./field-hear"
|
||||
import { FieldSlider } from "./field-slider"
|
||||
import { InputGroupButtonExample } from "./input-group-button"
|
||||
import { InputGroupDemo } from "./input-group-demo"
|
||||
import { ItemDemo } from "./item-demo"
|
||||
import { NotionPromptForm } from "./notion-prompt-form"
|
||||
import { SpinnerBadge } from "./spinner-badge"
|
||||
import { SpinnerEmpty } from "./spinner-empty"
|
||||
|
||||
export function RootComponents() {
|
||||
return (
|
||||
<div className="mx-auto grid gap-8 py-1 theme-container md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<FieldDemo />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<EmptyAvatarGroup />
|
||||
<SpinnerBadge />
|
||||
<ButtonGroupInputGroup />
|
||||
<FieldSlider />
|
||||
<InputGroupDemo />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<InputGroupButtonExample />
|
||||
<ItemDemo />
|
||||
<FieldSeparator className="my-4">Appearance Settings</FieldSeparator>
|
||||
<AppearanceSettings />
|
||||
</div>
|
||||
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
|
||||
<NotionPromptForm />
|
||||
<ButtonGroupDemo />
|
||||
<FieldCheckbox />
|
||||
<div className="flex justify-between gap-4">
|
||||
<ButtonGroupNested />
|
||||
<ButtonGroupPopover />
|
||||
</div>
|
||||
<FieldHear />
|
||||
<SpinnerEmpty />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import { Label } from "@/styles/radix-nova/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<Label htmlFor="input-secure-19" className="sr-only">
|
||||
Input Secure
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-19" className="pl-0.5!" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton
|
||||
variant="secondary"
|
||||
size="icon-xs"
|
||||
aria-label="Info"
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
alignOffset={10}
|
||||
className="flex flex-col gap-1 rounded-xl text-sm"
|
||||
>
|
||||
<p className="font-medium">Your connection is not secure.</p>
|
||||
<p>You should not enter any sensitive information on this site.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="pl-1! text-muted-foreground">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
onClick={() => setIsFavorite(!isFavorite)}
|
||||
size="icon-xs"
|
||||
aria-label="Favorite"
|
||||
>
|
||||
<IconStar
|
||||
data-favorite={isFavorite}
|
||||
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import { Separator } from "@/styles/radix-nova/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
<div className="grid w-full max-w-sm gap-6">
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Search..." />
|
||||
<InputGroupAddon>
|
||||
<Search />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="example.com" className="pl-1!" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
aria-label="Info"
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is content in a tooltip.</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
aria-label="Add"
|
||||
>
|
||||
<IconPlus />
|
||||
</InputGroupButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost">Auto</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuItem>Auto</DropdownMenuItem>
|
||||
<DropdownMenuItem>Agent</DropdownMenuItem>
|
||||
<DropdownMenuItem>Manual</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">52% used</InputGroupText>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||
<IconCheck className="size-3 text-background" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
IconBrandJavascript,
|
||||
IconCopy,
|
||||
IconCornerDownLeft,
|
||||
IconRefresh,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
|
||||
export function InputGroupTextareaExample() {
|
||||
return (
|
||||
<div className="grid w-full max-w-md gap-4">
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-code-32"
|
||||
placeholder="console.log('Hello, world!');"
|
||||
className="min-h-[180px]"
|
||||
/>
|
||||
<InputGroupAddon align="block-end" className="border-t">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupButton size="sm" className="ml-auto" variant="default">
|
||||
Run <IconCornerDownLeft />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-start" className="border-b">
|
||||
<InputGroupText className="font-mono font-medium">
|
||||
<IconBrandJavascript />
|
||||
script.js
|
||||
</InputGroupText>
|
||||
<InputGroupButton className="ml-auto">
|
||||
<IconRefresh />
|
||||
</InputGroupButton>
|
||||
<InputGroupButton variant="ghost">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemAvatar() {
|
||||
return (
|
||||
<div className="flex w-full max-w-lg flex-col gap-6">
|
||||
<Item variant="outline" className="hidden">
|
||||
<ItemMedia>
|
||||
<Avatar className="size-10">
|
||||
<AvatarImage src="https://github.com/maxleiter.png" />
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Max Leiter</ItemTitle>
|
||||
<ItemDescription>Last seen 5 months ago</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
aria-label="Invite"
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background *:data-[slot=avatar]:grayscale">
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>No Team Members</ItemTitle>
|
||||
<ItemDescription>Invite your team to collaborate.</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm" variant="outline">
|
||||
Invite
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/styles/radix-nova/ui/item"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
<div className="flex w-full max-w-md flex-col gap-6">
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Two-factor authentication</ItemTitle>
|
||||
<ItemDescription className="text-pretty xl:hidden 2xl:block">
|
||||
Verify via email or phone number.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Enable</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline" size="sm" asChild>
|
||||
<a href="#">
|
||||
<ItemMedia>
|
||||
<BadgeCheckIcon className="size-5" />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Your profile has been verified.</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,453 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/styles/radix-nova/ui/avatar"
|
||||
import { Badge } from "@/styles/radix-nova/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/styles/radix-nova/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
import { Switch } from "@/styles/radix-nova/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
{
|
||||
type: "page",
|
||||
title: "Meeting Notes",
|
||||
image: "📝",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Project Dashboard",
|
||||
image: "📊",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Ideas & Brainstorming",
|
||||
image: "💡",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Calendar & Events",
|
||||
image: "📅",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Documentation",
|
||||
image: "📚",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Goals & Objectives",
|
||||
image: "🎯",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Budget Planning",
|
||||
image: "💰",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Team Directory",
|
||||
image: "👥",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Technical Specs",
|
||||
image: "🔧",
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
title: "Analytics Report",
|
||||
image: "📈",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "shadcn",
|
||||
image: "https://github.com/shadcn.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "maxleiter",
|
||||
image: "https://github.com/maxleiter.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "evilrabbit",
|
||||
image: "https://github.com/evilrabbit.png",
|
||||
workspace: "Workspace",
|
||||
},
|
||||
],
|
||||
models: [
|
||||
{
|
||||
name: "Auto",
|
||||
},
|
||||
{
|
||||
name: "Agent Mode",
|
||||
badge: "Beta",
|
||||
},
|
||||
{
|
||||
name: "Plan Mode",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function MentionableIcon({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof SAMPLE_DATA.mentionable)[0]
|
||||
}) {
|
||||
return item.type === "page" ? (
|
||||
<span className="flex size-4 items-center justify-center">
|
||||
{item.image}
|
||||
</span>
|
||||
) : (
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>{item.title[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotionPromptForm() {
|
||||
const [mentions, setMentions] = useState<string[]>([])
|
||||
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [selectedModel, setSelectedModel] = useState<
|
||||
(typeof SAMPLE_DATA.models)[0]
|
||||
>(SAMPLE_DATA.models[0])
|
||||
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
return SAMPLE_DATA.mentionable.reduce(
|
||||
(acc, item) => {
|
||||
const isAvailable = !mentions.includes(item.title)
|
||||
|
||||
if (isAvailable) {
|
||||
if (!acc[item.type]) {
|
||||
acc[item.type] = []
|
||||
}
|
||||
acc[item.type].push(item)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, typeof SAMPLE_DATA.mentionable>
|
||||
)
|
||||
}, [mentions])
|
||||
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup className="rounded-xl">
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start" className="pt-3">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onFocusCapture={(e) => e.stopPropagation()}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No pages found</CommandEmpty>
|
||||
{Object.entries(grouped).map(([type, items]) => (
|
||||
<CommandGroup
|
||||
key={type}
|
||||
heading={type === "page" ? "Pages" : "Users"}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.title}
|
||||
value={item.title}
|
||||
onSelect={(currentValue) => {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
)
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full pl-2!"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
<IconX />
|
||||
</InputGroupButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton
|
||||
size="icon-sm"
|
||||
className="rounded-full"
|
||||
aria-label="Attach file"
|
||||
>
|
||||
<IconPaperclip />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach file</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
{selectedModel.name}
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="min-w-48"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={model.name}
|
||||
checked={model.name === selectedModel.name}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedModel(model)
|
||||
}
|
||||
}}
|
||||
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
|
||||
>
|
||||
{model.name}
|
||||
{model.badge && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
|
||||
>
|
||||
{model.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton size="sm" className="rounded-full">
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="end" className="w-72">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="web-search">
|
||||
<IconWorld /> Web Search{" "}
|
||||
<Switch
|
||||
id="web-search"
|
||||
className="ml-auto"
|
||||
defaultChecked
|
||||
/>
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<label htmlFor="apps">
|
||||
<IconApps /> Apps and Integrations
|
||||
<Switch id="apps" className="ml-auto" defaultChecked />
|
||||
</label>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleDashedPlus /> All Sources I can access
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src="https://github.com/shadcn.png" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
shadcn
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-72 p-0 [--radius:1rem]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Find or use knowledge in..."
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No knowledge found</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{SAMPLE_DATA.mentionable
|
||||
.filter((item) => item.type === "user")
|
||||
.map((user) => (
|
||||
<CommandItem
|
||||
key={user.title}
|
||||
value={user.title}
|
||||
onSelect={() => {
|
||||
// Handle user selection here
|
||||
console.log("Selected user:", user.title)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback>
|
||||
{user.title[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.title}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
- {user.workspace}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem>
|
||||
<IconBook /> Help Center
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupButton
|
||||
aria-label="Send"
|
||||
className="ml-auto rounded-full"
|
||||
variant="default"
|
||||
size="icon-sm"
|
||||
>
|
||||
<IconArrowUp />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Badge } from "@/styles/radix-nova/ui/badge"
|
||||
import { Spinner } from "@/styles/radix-nova/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Syncing
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Updating
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Loading
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/radix-nova/ui/empty"
|
||||
import { Spinner } from "@/styles/radix-nova/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
<Empty className="w-full border md:p-6">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Processing your request</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Please wait while we process your request. Do not refresh the page.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
import { type Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { IconArrowRight } from "@tabler/icons-react"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import { ExamplesNav } from "@/components/examples-nav"
|
||||
import {
|
||||
PageActions,
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header"
|
||||
import { PageNav } from "@/components/page-nav"
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
|
||||
import { RootComponents } from "./components"
|
||||
import { CardsDemo } from "./cards"
|
||||
|
||||
const title = "The Foundation for your Design System"
|
||||
const description =
|
||||
@@ -50,21 +48,20 @@ export const metadata: Metadata = {
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<PageHeader>
|
||||
<PageHeader className="md:**:[.container]:pb-8 lg:**:[.container]:pb-12">
|
||||
<Announcement />
|
||||
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm" className="h-[31px] rounded-lg">
|
||||
<Link href="/create">New Project</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost" className="rounded-lg">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
<Button asChild className="h-[31px] rounded-lg">
|
||||
<Link href="/create?preset=b27GcrRo">
|
||||
Build Your Own <IconArrowRight data-icon="inline-end" />
|
||||
</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<div className="container-wrapper flex-1 pb-6">
|
||||
<div className="container overflow-hidden">
|
||||
<div className="container-wrapper flex-1 pb-6 md:px-0">
|
||||
<div className="container overflow-hidden md:px-0 lg:max-w-none">
|
||||
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-light.png"
|
||||
@@ -83,8 +80,8 @@ export default function IndexPage() {
|
||||
priority
|
||||
/>
|
||||
</section>
|
||||
<section className="hidden theme-container md:block">
|
||||
<RootComponents />
|
||||
<section className="hidden md:block">
|
||||
<CardsDemo />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
"use client"
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/styles/base-sera/ui/input-group"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from "@/styles/base-sera/ui/pagination"
|
||||
import { Progress, ProgressValue } from "@/styles/base-sera/ui/progress"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/base-sera/ui/table"
|
||||
|
||||
const ARTICLE_ROWS = [
|
||||
{
|
||||
title: "The Future of Sustainable Architecture",
|
||||
wordProgress: "1.4k / 2.6k words",
|
||||
author: "Elena Rostova",
|
||||
issue: "Summer 2024",
|
||||
status: "in-revision",
|
||||
statusLabel: "In revision",
|
||||
progress: 45,
|
||||
},
|
||||
{
|
||||
title: "Brutalism's Second Act",
|
||||
wordProgress: "2.1k / 2.5k words",
|
||||
author: "Marcus Chen",
|
||||
issue: "Summer 2024",
|
||||
status: "final-edit",
|
||||
statusLabel: "Final edit",
|
||||
progress: 90,
|
||||
},
|
||||
{
|
||||
title: "The Typography of Public Spaces",
|
||||
wordProgress: "0.5k / 1.5k words",
|
||||
author: "Sarah Jenkins",
|
||||
issue: "Autumn 2024",
|
||||
status: "drafting",
|
||||
statusLabel: "Drafting",
|
||||
progress: 20,
|
||||
},
|
||||
{
|
||||
title: "Rethinking Urban Canopies",
|
||||
wordProgress: "1.8k / 1.8k words",
|
||||
author: "David O'Connor",
|
||||
issue: "Summer 2024",
|
||||
status: "published",
|
||||
statusLabel: "Published",
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
title: "Light, Glass, and the Modern Museum",
|
||||
wordProgress: "1.2k / 2.0k words",
|
||||
author: "Amara Osei",
|
||||
issue: "Autumn 2024",
|
||||
status: "in-revision",
|
||||
statusLabel: "In revision",
|
||||
progress: 55,
|
||||
},
|
||||
{
|
||||
title: "Concrete Utopias: Housing in the 21st Century",
|
||||
wordProgress: "3.0k / 3.0k words",
|
||||
author: "Tomás Herrera",
|
||||
issue: "Summer 2024",
|
||||
status: "published",
|
||||
statusLabel: "Published",
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
title: "Designing for Silence",
|
||||
wordProgress: "0.8k / 2.2k words",
|
||||
author: "Ingrid Solberg",
|
||||
issue: "Winter 2024",
|
||||
status: "drafting",
|
||||
statusLabel: "Drafting",
|
||||
progress: 30,
|
||||
},
|
||||
{
|
||||
title: "The Invisible Infrastructure of Cities",
|
||||
wordProgress: "2.4k / 2.8k words",
|
||||
author: "James Whitfield",
|
||||
issue: "Autumn 2024",
|
||||
status: "final-edit",
|
||||
statusLabel: "Final edit",
|
||||
progress: 85,
|
||||
},
|
||||
] as const
|
||||
|
||||
const STATUS_BADGE_VARIANT = {
|
||||
"in-revision": "outline",
|
||||
"final-edit": "default",
|
||||
drafting: "ghost",
|
||||
published: "secondary",
|
||||
} as const
|
||||
|
||||
const STATUS_DOT_CLASSNAME = {
|
||||
"in-revision": "bg-amber-600/80",
|
||||
"final-edit": "bg-foreground/90",
|
||||
drafting: "bg-muted-foreground/60",
|
||||
published: "bg-emerald-600/80",
|
||||
}
|
||||
|
||||
export function ArticleDirectory() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput type="search" placeholder="Search articles..." />
|
||||
</InputGroup>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead className="w-[170px]">Author</TableHead>
|
||||
<TableHead className="w-[150px]">Issue</TableHead>
|
||||
<TableHead className="w-[180px]">Status</TableHead>
|
||||
<TableHead className="w-[140px]">Progress</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ARTICLE_ROWS.map((row) => (
|
||||
<TableRow key={row.title}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-heading text-xl tracking-tight text-foreground">
|
||||
{row.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{row.wordProgress}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{row.author}</TableCell>
|
||||
<TableCell>{row.issue}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={STATUS_BADGE_VARIANT[row.status]}>
|
||||
<span
|
||||
className={cn(
|
||||
"size-1.5 rounded-full",
|
||||
STATUS_DOT_CLASSNAME[row.status]
|
||||
)}
|
||||
/>
|
||||
{row.statusLabel}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Progress
|
||||
value={row.progress}
|
||||
aria-label={`${row.progress}% complete`}
|
||||
className="flex flex-row-reverse items-center **:data-[slot=progress-track]:w-16"
|
||||
>
|
||||
<ProgressValue>
|
||||
{(formattedValue) => `${formattedValue}`}
|
||||
</ProgressValue>
|
||||
</Progress>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
size="icon-sm"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<ChevronLeftIcon className="cn-rtl-flip" />
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
{[1, 2, 3].map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink href="#" size="icon-sm" isActive={page === 1}>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#" size="icon-sm" aria-label="Next page">
|
||||
<ChevronRightIcon className="cn-rtl-flip" />
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ArrowLeftIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList className="justify-center md:justify-start">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
href="#"
|
||||
className="inline-flex items-center gap-1.5"
|
||||
>
|
||||
<ArrowLeftIcon className="size-3" />
|
||||
Editorial Dashboard
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Article Directory
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<Button>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
New Article
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
16
apps/v4/app/(app)/(styles)/sera/article-directory/index.tsx
Normal file
16
apps/v4/app/(app)/(styles)/sera/article-directory/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { ArticleDirectory as ArticleDirectoryList } from "./components/article-directory"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function ArticleDirectory() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container py-(--gap)">
|
||||
<ArticleDirectoryList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { MoveRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
Progress,
|
||||
ProgressLabel,
|
||||
ProgressValue,
|
||||
} from "@/styles/base-sera/ui/progress"
|
||||
|
||||
const DEMOGRAPHIC_DATA = [
|
||||
{ age: "18 - 24", percentage: 22 },
|
||||
{ age: "25 - 34", percentage: 64 },
|
||||
{ age: "35 - 44", percentage: 12 },
|
||||
{ age: "45+", percentage: 5 },
|
||||
]
|
||||
|
||||
export function Demographics({ ...props }: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Demographics</CardTitle>
|
||||
<CardDescription>Reader Profile</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-10">
|
||||
{DEMOGRAPHIC_DATA.map((item) => (
|
||||
<Progress
|
||||
key={item.age}
|
||||
value={item.percentage}
|
||||
aria-label={item.age}
|
||||
>
|
||||
<ProgressLabel>{item.age}</ProgressLabel>
|
||||
<ProgressValue>
|
||||
{(formattedValue) => `${formattedValue}`}
|
||||
</ProgressValue>
|
||||
</Progress>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="link" className="w-full">
|
||||
View all source <MoveRightIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
|
||||
type Metric = {
|
||||
label: string
|
||||
value: string
|
||||
comparison: string
|
||||
change: string
|
||||
trend: "up" | "down"
|
||||
}
|
||||
|
||||
const METRIC_CARDS: Metric[] = [
|
||||
{
|
||||
label: "Total visitors",
|
||||
value: "248.5k",
|
||||
comparison: "12.4%",
|
||||
change: "vs last period",
|
||||
trend: "up",
|
||||
},
|
||||
{
|
||||
label: "Unique readers",
|
||||
value: "182.1k",
|
||||
comparison: "8.7%",
|
||||
change: "vs last period",
|
||||
trend: "up",
|
||||
},
|
||||
{
|
||||
label: "Avg. time on page",
|
||||
value: "3m 42s",
|
||||
comparison: "1.2%",
|
||||
change: "vs last period",
|
||||
trend: "down",
|
||||
},
|
||||
{
|
||||
label: "Bounce rate",
|
||||
value: "42.8%",
|
||||
comparison: "3.5%",
|
||||
change: "vs last period",
|
||||
trend: "down",
|
||||
},
|
||||
]
|
||||
|
||||
export function MetricsGrid() {
|
||||
return (
|
||||
<>
|
||||
{METRIC_CARDS.map((metric) => (
|
||||
<MetricCard
|
||||
key={metric.label}
|
||||
metric={metric}
|
||||
className="col-span-full md:col-span-6 lg:col-span-3"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
metric,
|
||||
className,
|
||||
}: {
|
||||
metric: Metric
|
||||
className: string
|
||||
}) {
|
||||
return (
|
||||
<Card className={cn("gap-0", className)}>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<CardDescription className="text-xs uppercase">
|
||||
{metric.label}
|
||||
</CardDescription>
|
||||
<CardTitle className="text-5xl tracking-tight lowercase">
|
||||
{metric.value}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{metric.trend === "up" ? (
|
||||
<TrendingUpIcon className="inline-block size-2.5 text-muted-foreground" />
|
||||
) : (
|
||||
<TrendingDownIcon className="inline-block size-2.5 text-muted-foreground" />
|
||||
)}{" "}
|
||||
<span className="text-foreground">{metric.comparison}</span>{" "}
|
||||
<span>{metric.change}</span>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon, DownloadIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
|
||||
const EXPORT_DATE_OPTIONS = [
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: "last-7-days",
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: "last-30-days",
|
||||
},
|
||||
{
|
||||
label: "This month",
|
||||
value: "this-month",
|
||||
},
|
||||
{
|
||||
label: "Last month",
|
||||
value: "last-month",
|
||||
},
|
||||
]
|
||||
|
||||
export function PreviewHeader() {
|
||||
const [selectedDateRange, setSelectedDateRange] =
|
||||
React.useState("last-30-days")
|
||||
|
||||
const selectedDateRangeLabel = React.useMemo(() => {
|
||||
const selectedOption = EXPORT_DATE_OPTIONS.find(
|
||||
(option) => option.value === selectedDateRange
|
||||
)
|
||||
|
||||
if (!selectedOption) {
|
||||
return "Last 30 days"
|
||||
}
|
||||
|
||||
return selectedOption.label
|
||||
}, [selectedDateRange])
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Audience Analytics
|
||||
</h1>
|
||||
<div className="line-clamp-1 text-sm font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Editorial Performance Dashboard
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-background hover:bg-background/80 data-popup-open:bg-background"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{selectedDateRangeLabel}{" "}
|
||||
<ChevronDownIcon data-icon="inline-end" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuRadioGroup
|
||||
value={selectedDateRange}
|
||||
onValueChange={setSelectedDateRange}
|
||||
>
|
||||
{EXPORT_DATE_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button>
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
<span className="lg:hidden">Export</span>
|
||||
<span className="hidden lg:inline">Export Report</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ArrowDownIcon, MoreHorizontalIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
import { Spinner } from "@/styles/base-sera/ui/spinner"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/base-sera/ui/table"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/styles/base-sera/ui/toggle-group"
|
||||
|
||||
type EditorialMetric = "views" | "time" | "shares"
|
||||
|
||||
type EditorialRow = {
|
||||
rank: number
|
||||
title: string
|
||||
author: string
|
||||
published: string
|
||||
pageviews: string
|
||||
avgTime: string
|
||||
}
|
||||
|
||||
const METRIC_LABEL: Record<EditorialMetric, string> = {
|
||||
views: "VIEWS",
|
||||
time: "TIME",
|
||||
shares: "SHARES",
|
||||
}
|
||||
|
||||
const EDITORIAL_ROWS: EditorialRow[] = [
|
||||
{
|
||||
rank: 1,
|
||||
title: "The New Vanguard of Minimalist Architecture",
|
||||
author: "Elena Rostova",
|
||||
published: "Oct 12",
|
||||
pageviews: "45.2k",
|
||||
avgTime: "04:15",
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
title: "Autumn Sartorial Code: Deconstructed Classics",
|
||||
author: "Julian Vance",
|
||||
published: "Oct 05",
|
||||
pageviews: "38.9k",
|
||||
avgTime: "03:42",
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
title: "Interview: Director Sofia Coppola on The Aesthetics of Isolation",
|
||||
author: "Marcus Trent",
|
||||
published: "Sep 28",
|
||||
pageviews: "31.4k",
|
||||
avgTime: "06:20",
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
title: "Sourcing Ceramics from Kyoto's Oldest Kilns",
|
||||
author: "Sarah Lin",
|
||||
published: "Oct 18",
|
||||
pageviews: "22.1k",
|
||||
avgTime: "02:55",
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
title: "Field Notes from Copenhagen Design Week",
|
||||
author: "Noah Bennett",
|
||||
published: "Oct 21",
|
||||
pageviews: "19.7k",
|
||||
avgTime: "03:18",
|
||||
},
|
||||
{
|
||||
rank: 6,
|
||||
title: "A Studio Visit with Milan's Most Elusive Lighting Designer",
|
||||
author: "Claire Duval",
|
||||
published: "Oct 09",
|
||||
pageviews: "17.4k",
|
||||
avgTime: "04:02",
|
||||
},
|
||||
{
|
||||
rank: 7,
|
||||
title: "Collecting the New Avant-Garde in Contemporary Furniture",
|
||||
author: "Tommy Rhodes",
|
||||
published: "Sep 30",
|
||||
pageviews: "15.9k",
|
||||
avgTime: "03:36",
|
||||
},
|
||||
{
|
||||
rank: 8,
|
||||
title: "Inside Lisbon's Quiet Culinary Renaissance",
|
||||
author: "Amara Iqbal",
|
||||
published: "Oct 14",
|
||||
pageviews: "14.2k",
|
||||
avgTime: "05:08",
|
||||
},
|
||||
{
|
||||
rank: 9,
|
||||
title: "Why Slow Interiors Are Defining the Next Luxury Wave",
|
||||
author: "Henry Vale",
|
||||
published: "Oct 03",
|
||||
pageviews: "12.7k",
|
||||
avgTime: "03:11",
|
||||
},
|
||||
{
|
||||
rank: 10,
|
||||
title: "The Return of Print: Independent Magazine Covers to Watch",
|
||||
author: "Mina Okafor",
|
||||
published: "Sep 26",
|
||||
pageviews: "11.3k",
|
||||
avgTime: "02:49",
|
||||
},
|
||||
]
|
||||
|
||||
type TopEditorialProps = React.ComponentProps<typeof Card> & {
|
||||
selectedMetric?: EditorialMetric
|
||||
}
|
||||
|
||||
export function TopEditorial({
|
||||
selectedMetric = "views",
|
||||
...props
|
||||
}: TopEditorialProps) {
|
||||
const [visibleCount, setVisibleCount] = React.useState(5)
|
||||
const [isLoadingMore, setIsLoadingMore] = React.useState(false)
|
||||
const hasMoreRows = visibleCount < EDITORIAL_ROWS.length
|
||||
const visibleRows = EDITORIAL_ROWS.slice(0, visibleCount)
|
||||
|
||||
const handleLoadMore = React.useCallback(() => {
|
||||
if (!hasMoreRows || isLoadingMore) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoadingMore(true)
|
||||
|
||||
window.setTimeout(() => {
|
||||
setVisibleCount(EDITORIAL_ROWS.length)
|
||||
setIsLoadingMore(false)
|
||||
}, 2000)
|
||||
}, [hasMoreRows, isLoadingMore])
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-(--gap) sm:flex-row">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<CardTitle className="text-2xl">Top Editorials</CardTitle>
|
||||
<CardDescription>Ranked by engagement</CardDescription>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
aria-label="Top editorials metric selector"
|
||||
value={[selectedMetric]}
|
||||
variant="outline"
|
||||
className="w-full sm:ml-auto sm:w-fit"
|
||||
>
|
||||
{(["views", "time", "shares"] as const).map((metric) => {
|
||||
return (
|
||||
<ToggleGroupItem key={metric} value={metric} className="flex-1">
|
||||
{METRIC_LABEL[metric]}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
})}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 **:data-[slot=table-container]:no-scrollbar **:data-[slot=table-container]:overflow-y-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Published</TableHead>
|
||||
<TableHead>Page Views</TableHead>
|
||||
<TableHead>Read Time</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{visibleRows.map((row) => (
|
||||
<TableRow key={row.rank}>
|
||||
<TableCell className="translate-y-1 align-text-top">
|
||||
{row.rank}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-heading text-xl tracking-tight text-foreground">
|
||||
{row.title}
|
||||
</p>
|
||||
<p className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
||||
By {row.author}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{row.published}</TableCell>
|
||||
<TableCell>{row.pageviews}</TableCell>
|
||||
<TableCell>{row.avgTime}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
aria-label={`Open actions for ${row.title}`}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Publish</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
{hasMoreRows ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleLoadMore}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
Load more content{" "}
|
||||
{isLoadingMore ? (
|
||||
<Spinner data-icon="inline-end" />
|
||||
) : (
|
||||
<ArrowDownIcon data-icon="inline-end" />
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
|
||||
const TrafficOverviewContent = dynamic(
|
||||
() => import("./traffic-overview").then((mod) => mod.TrafficOverview),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <TrafficOverviewFallback />,
|
||||
}
|
||||
)
|
||||
|
||||
export function TrafficOverviewDeferred({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<TrafficOverviewContent {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TrafficOverviewFallback() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Traffic Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Traffic for the last 30 days has increased by 12.4% compared to the
|
||||
previous period.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="flex h-82 w-full flex-col justify-end gap-6 overflow-hidden bg-muted/40 p-5"
|
||||
>
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUpIcon } from "lucide-react"
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceDot,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/styles/base-sera/ui/chart"
|
||||
|
||||
const TRAFFIC_OVERVIEW_DATA = [
|
||||
{ date: "2025-10-01", views: 2600, unique: 1600 },
|
||||
{ date: "2025-10-04", views: 4500, unique: 3000 },
|
||||
{ date: "2025-10-08", views: 3500, unique: 2500 },
|
||||
{ date: "2025-10-10", views: 6400, unique: 4500 },
|
||||
{ date: "2025-10-13", views: 5400, unique: 4000 },
|
||||
{ date: "2025-10-15", views: 8300, unique: 6500 },
|
||||
{ date: "2025-10-17", views: 7400, unique: 6000 },
|
||||
{ date: "2025-10-18", views: 9240, unique: 7105 },
|
||||
{ date: "2025-10-22", views: 7700, unique: 6400 },
|
||||
{ date: "2025-10-26", views: 8800, unique: 7000 },
|
||||
{ date: "2025-10-29", views: 9800, unique: 8400 },
|
||||
]
|
||||
|
||||
const TRAFFIC_CHART_CONFIG = {
|
||||
views: {
|
||||
label: "Views",
|
||||
theme: {
|
||||
light: "var(--chart-5)",
|
||||
dark: "var(--chart-1)",
|
||||
},
|
||||
},
|
||||
unique: {
|
||||
label: "Unique",
|
||||
theme: {
|
||||
light: "var(--chart-1)",
|
||||
dark: "var(--chart-2)",
|
||||
},
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const X_AXIS_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
|
||||
function formatYAxisTick(value: number) {
|
||||
if (value === 0) {
|
||||
return "0"
|
||||
}
|
||||
|
||||
if (value % 1000 === 0) {
|
||||
return `${value / 1000}k`
|
||||
}
|
||||
|
||||
return `${value / 1000}k`
|
||||
}
|
||||
|
||||
function formatXAxisTick(value: string) {
|
||||
const date = new Date(`${value}T00:00:00Z`)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
|
||||
return X_AXIS_DATE_FORMATTER.format(date)
|
||||
}
|
||||
|
||||
export function TrafficOverview({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Traffic Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Traffic for the last 30 days has increased by 12.4% compared to the
|
||||
previous period.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={TRAFFIC_CHART_CONFIG} className="h-82 w-full">
|
||||
<LineChart data={TRAFFIC_OVERVIEW_DATA}>
|
||||
<CartesianGrid
|
||||
vertical={false}
|
||||
strokeDasharray="3 6"
|
||||
stroke="var(--border)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
interval="preserveStartEnd"
|
||||
tickMargin={10}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={44}
|
||||
domain={[0, 10000]}
|
||||
ticks={[0, 2500, 5000, 7500, 10000]}
|
||||
tickFormatter={formatYAxisTick}
|
||||
hide
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
type="linear"
|
||||
dataKey="views"
|
||||
stroke="var(--color-views)"
|
||||
strokeWidth={2.2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3.5, fill: "var(--color-views)" }}
|
||||
/>
|
||||
<Line
|
||||
type="linear"
|
||||
dataKey="unique"
|
||||
stroke="var(--color-unique)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="4 6"
|
||||
dot={false}
|
||||
activeDot={false}
|
||||
/>
|
||||
<ReferenceDot
|
||||
x="2025-10-18"
|
||||
y={9240}
|
||||
r={2.5}
|
||||
fill="var(--color-views)"
|
||||
stroke="var(--color-views)"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
22
apps/v4/app/(app)/(styles)/sera/audience-analytics/index.tsx
Normal file
22
apps/v4/app/(app)/(styles)/sera/audience-analytics/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { Demographics } from "./components/demographics"
|
||||
import { MetricsGrid } from "./components/metrics-grid"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
import { TopEditorial } from "./components/top-editorial"
|
||||
import { TrafficOverviewDeferred } from "./components/traffic-overview-deferred"
|
||||
|
||||
export function AudienceAnalytics() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container grid grid-cols-12 gap-(--gap) py-(--gap)">
|
||||
<MetricsGrid />
|
||||
<TrafficOverviewDeferred className="col-span-full md:col-span-6 lg:col-span-8" />
|
||||
<Demographics className="col-span-full md:col-span-6 lg:col-span-4" />
|
||||
<TopEditorial className="col-span-full" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
apps/v4/app/(app)/(styles)/sera/components/image-preview.tsx
Normal file
46
apps/v4/app/(app)/(styles)/sera/components/image-preview.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import Image from "next/image"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function ImagePreview() {
|
||||
return (
|
||||
<div className="mt-8 flex flex-col overflow-hidden md:hidden">
|
||||
<ImagePreviewItem name="sera-01" />
|
||||
<ImagePreviewItem name="sera-03" />
|
||||
<ImagePreviewItem name="sera-02" />
|
||||
<ImagePreviewItem name="sera-06" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ImagePreviewItem({
|
||||
name,
|
||||
className,
|
||||
}: {
|
||||
name: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"theme-taupe overflow-hidden bg-muted px-4 py-2 first:pt-4 last:pb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={`/images/${name}-light.png`}
|
||||
alt={name}
|
||||
width={1440}
|
||||
height={900}
|
||||
className="dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src={`/images/${name}-dark.png`}
|
||||
alt={name}
|
||||
width={1440}
|
||||
height={900}
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
148
apps/v4/app/(app)/(styles)/sera/components/lazy-preview.tsx
Normal file
148
apps/v4/app/(app)/(styles)/sera/components/lazy-preview.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
type LazyPreviewName =
|
||||
| "articleDirectory"
|
||||
| "emptyState"
|
||||
| "editArticle"
|
||||
| "mediaLibrary"
|
||||
| "mediaLibraryTable"
|
||||
|
||||
const PREVIEW_MIN_HEIGHTS: Record<LazyPreviewName, number> = {
|
||||
articleDirectory: 760,
|
||||
emptyState: 560,
|
||||
editArticle: 980,
|
||||
mediaLibrary: 880,
|
||||
mediaLibraryTable: 980,
|
||||
}
|
||||
|
||||
const ArticleDirectoryPreview = dynamic(
|
||||
() => import("../article-directory").then((mod) => mod.ArticleDirectory),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.articleDirectory} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const EmptyStatePreview = dynamic(
|
||||
() => import("../empty-state").then((mod) => mod.EmptyState),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.emptyState} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const EditArticlePreview = dynamic(
|
||||
() => import("../edit-article").then((mod) => mod.EditArticle),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.editArticle} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const MediaLibraryPreview = dynamic(
|
||||
() => import("../media-library").then((mod) => mod.MediaLibrary),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.mediaLibrary} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const MediaLibraryTablePreview = dynamic(
|
||||
() => import("../media-library-table").then((mod) => mod.MediaLibraryTable),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.mediaLibraryTable} />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const PREVIEW_COMPONENTS: Record<LazyPreviewName, React.ComponentType> = {
|
||||
articleDirectory: ArticleDirectoryPreview,
|
||||
emptyState: EmptyStatePreview,
|
||||
editArticle: EditArticlePreview,
|
||||
mediaLibrary: MediaLibraryPreview,
|
||||
mediaLibraryTable: MediaLibraryTablePreview,
|
||||
}
|
||||
|
||||
export function LazyPreview({ name }: { name: LazyPreviewName }) {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
const [shouldRender, setShouldRender] = React.useState(false)
|
||||
const PreviewComponent = PREVIEW_COMPONENTS[name]
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldRender) {
|
||||
return
|
||||
}
|
||||
|
||||
const container = containerRef.current
|
||||
|
||||
if (!container || !("IntersectionObserver" in window)) {
|
||||
setShouldRender(true)
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (!entries.some((entry) => entry.isIntersecting)) {
|
||||
return
|
||||
}
|
||||
|
||||
setShouldRender(true)
|
||||
observer.disconnect()
|
||||
},
|
||||
{
|
||||
rootMargin: "800px 0px",
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(container)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [shouldRender])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{shouldRender ? (
|
||||
<PreviewComponent />
|
||||
) : (
|
||||
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS[name]} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewPlaceholder({ minHeight }: { minHeight: number }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="preview theme-taupe @container/preview w-full flex-1 bg-muted p-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:p-6 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)]"
|
||||
style={{ minHeight }}
|
||||
>
|
||||
<div className="container flex flex-col gap-(--gap) py-(--gap)">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="h-5 w-44 bg-background/80" />
|
||||
<div className="h-3 w-56 max-w-full bg-background/60" />
|
||||
</div>
|
||||
<div className="hidden h-8 w-28 bg-background/70 sm:block" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-(--gap) md:grid-cols-3">
|
||||
<div className="min-h-48 bg-background/70 md:col-span-2" />
|
||||
<div className="min-h-48 bg-background/70" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ label: "Taupe", value: "theme-taupe" },
|
||||
{ label: "Neutral", value: "theme-neutral" },
|
||||
{ label: "Stone", value: "theme-stone" },
|
||||
{ label: "Zinc", value: "theme-zinc" },
|
||||
{ label: "Mauve", value: "theme-mauve" },
|
||||
{ label: "Olive", value: "theme-olive" },
|
||||
{ label: "Mist", value: "theme-mist" },
|
||||
] as const
|
||||
|
||||
const DEFAULT_THEME = "theme-taupe"
|
||||
|
||||
function applyThemeToPreviews(theme: string) {
|
||||
const previewElements = document.querySelectorAll<HTMLElement>(".preview")
|
||||
|
||||
previewElements.forEach((element) => {
|
||||
THEME_OPTIONS.forEach((option) => {
|
||||
element.classList.remove(option.value)
|
||||
})
|
||||
|
||||
element.classList.add(theme)
|
||||
})
|
||||
}
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const [theme, setTheme] = React.useState<string>(DEFAULT_THEME)
|
||||
|
||||
React.useEffect(() => {
|
||||
applyThemeToPreviews(theme)
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<div className="fixed inset-x-0 bottom-8 z-50 flex justify-center px-4">
|
||||
<div className="w-full max-w-[60vw] rounded-full border-0 bg-neutral-950/50 p-1.5 shadow-xl backdrop-blur-xl sm:max-w-fit">
|
||||
<div className="no-scrollbar flex snap-x snap-mandatory items-center overflow-x-auto">
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<button
|
||||
data-active={theme === option.value}
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTheme(option.value)
|
||||
}}
|
||||
className="shrink-0 snap-center rounded-full px-3 py-1.5 text-sm font-medium text-neutral-300 outline-hidden transition-colors select-none hover:text-neutral-100 data-active:bg-neutral-500 data-active:text-neutral-100"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
AlignCenterIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
BoldIcon,
|
||||
ChevronDownIcon,
|
||||
Code2Icon,
|
||||
Heading1Icon,
|
||||
Heading2Icon,
|
||||
Heading3Icon,
|
||||
ImageIcon,
|
||||
ItalicIcon,
|
||||
LinkIcon,
|
||||
ListIcon,
|
||||
ListOrderedIcon,
|
||||
RedoIcon,
|
||||
StrikethroughIcon,
|
||||
TypeIcon,
|
||||
UnderlineIcon,
|
||||
UndoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
} from "@/styles/base-sera/ui/button-group"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/styles/base-sera/ui/field"
|
||||
import { Input } from "@/styles/base-sera/ui/input"
|
||||
import {
|
||||
Progress,
|
||||
ProgressLabel,
|
||||
ProgressValue,
|
||||
} from "@/styles/base-sera/ui/progress"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/base-sera/ui/select"
|
||||
import { Textarea } from "@/styles/base-sera/ui/textarea"
|
||||
|
||||
type Milestone = {
|
||||
name: string
|
||||
complete: boolean
|
||||
note?: string
|
||||
}
|
||||
|
||||
const MILESTONES: Milestone[] = [
|
||||
{
|
||||
name: "Outline & Commissioning",
|
||||
complete: true,
|
||||
},
|
||||
{
|
||||
name: "First Draft Submitted",
|
||||
complete: true,
|
||||
},
|
||||
{
|
||||
name: "Review & Revisions",
|
||||
complete: false,
|
||||
note: "Waiting on editor",
|
||||
},
|
||||
{
|
||||
name: "Final Copy Edit",
|
||||
complete: false,
|
||||
},
|
||||
{
|
||||
name: "Art Direction & Layout",
|
||||
complete: false,
|
||||
},
|
||||
]
|
||||
|
||||
const ISSUES = [
|
||||
{ label: "Spring Issue 2024", value: "spring-2024" },
|
||||
{ label: "Summer Issue 2024", value: "summer-2024" },
|
||||
{ label: "Autumn Issue 2024", value: "autumn-2024" },
|
||||
{ label: "Winter Issue 2024", value: "winter-2024" },
|
||||
]
|
||||
|
||||
export function EditorWorkspace() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 items-start gap-6 xl:grid-cols-[minmax(0,1fr)_300px]">
|
||||
<section className="flex flex-col border border-border/70 bg-background">
|
||||
<div className="flex border-b p-2">
|
||||
<ButtonGroup>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant="ghost" size="sm">
|
||||
Normal Text
|
||||
<ChevronDownIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<TypeIcon />
|
||||
Normal Text
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Heading1Icon />
|
||||
Heading 1
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Heading2Icon />
|
||||
Heading 2
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Heading3Icon />
|
||||
Heading 3
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListIcon />
|
||||
Bullet List
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ListOrderedIcon />
|
||||
Numbered List
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ButtonGroupSeparator className="mx-2 data-vertical:h-4 data-vertical:self-center" />
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Bold">
|
||||
<BoldIcon />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Italic">
|
||||
<ItalicIcon />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Underline">
|
||||
<UnderlineIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Strikethrough"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<StrikethroughIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Code"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<Code2Icon />
|
||||
</Button>
|
||||
<ButtonGroupSeparator className="mx-2 hidden md:flex data-vertical:h-4 data-vertical:self-center" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Align Left"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<AlignLeftIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Align Center"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<AlignCenterIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Align Right"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<AlignRightIcon />
|
||||
</Button>
|
||||
<ButtonGroupSeparator className="mx-2 hidden md:flex data-vertical:h-4 data-vertical:self-center" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Link"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<LinkIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Image"
|
||||
className="hidden md:flex"
|
||||
>
|
||||
<ImageIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="ml-auto">
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Undo">
|
||||
<UndoIcon />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Redo">
|
||||
<RedoIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-2xl flex-1 flex-col gap-8 px-10 py-10 leading-loose md:px-14 lg:py-18">
|
||||
<h1 className="font-heading text-4xl leading-12 font-medium tracking-wide uppercase">
|
||||
The Future of Sustainable Architecture
|
||||
</h1>
|
||||
<p>
|
||||
As cities continue to expand at an unprecedented rate, the
|
||||
architectural paradigm is shifting from mere expansion to
|
||||
sustainable integration. The concrete jungles of the 20th century
|
||||
are making way for structures that breathe, adapt, and give back to
|
||||
their environments.
|
||||
</p>
|
||||
<p>
|
||||
Historically, urban development has been a zero-sum game with
|
||||
nature.
|
||||
</p>
|
||||
<h2 className="font-heading text-2xl tracking-wide uppercase">
|
||||
The Living Building Challenge
|
||||
</h2>
|
||||
<p>
|
||||
Sterling's latest project in downtown Seattle is a testament to
|
||||
this new philosophy. "We are no longer designing static
|
||||
structures," Sterling explained during a recent site visit.
|
||||
"We are engineering localized ecosystems."
|
||||
</p>
|
||||
<p>
|
||||
The building features a facade of responsive biomaterials that
|
||||
adjust their porosity based on humidity and temperature,
|
||||
significantly reducing the need for artificial climate control.
|
||||
Rainwater is not merely channeled away but captured, filtered
|
||||
through a series of integrated rooftop wetlands, and reused within
|
||||
the building's greywater system.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This shift requires more than just innovative materials; it demands
|
||||
a fundamental change in how we value space. Check with engineering
|
||||
team for specific stats.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<aside className="grid grid-cols-12 gap-(--gap) xl:flex xl:flex-col">
|
||||
<Card className="col-span-full md:col-span-6 lg:col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Article Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel>Issue</FieldLabel>
|
||||
<Select items={ISSUES} defaultValue="summer-2024">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{ISSUES.map((issue) => (
|
||||
<SelectItem key={issue.value} value={issue.value}>
|
||||
{issue.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Author</FieldLabel>
|
||||
<Input defaultValue="Elena Rostova" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-full md:col-span-6 lg:col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Publication Flow</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Required Milestones</FieldLegend>
|
||||
<Field>
|
||||
{MILESTONES.map((milestone) => (
|
||||
<Field key={milestone.name} orientation="horizontal">
|
||||
<Checkbox
|
||||
defaultChecked={milestone.complete}
|
||||
name={milestone.name}
|
||||
id={milestone.name}
|
||||
/>
|
||||
<FieldLabel htmlFor={milestone.name}>
|
||||
{milestone.name}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel>Add note for editor</FieldLabel>
|
||||
<Textarea placeholder="This article needs to be revised for clarity and accuracy." />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-full lg:col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Word Count</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Progress value={70}>
|
||||
<ProgressLabel>1,402 / 2,000 words</ProgressLabel>
|
||||
<ProgressValue />
|
||||
</Progress>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
|
||||
<ArrowLeftIcon className="size-3.5" />
|
||||
Back to articles
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
EDIT ARTICLE
|
||||
</h1>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 md:gap-4">
|
||||
<Badge title="2 minutes ago">Autosaved</Badge>
|
||||
<ButtonGroup className="gap-2 md:gap-4">
|
||||
<Button variant="link">
|
||||
Preview
|
||||
<ExternalLinkIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
<Button>Submit Draft</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
16
apps/v4/app/(app)/(styles)/sera/edit-article/index.tsx
Normal file
16
apps/v4/app/(app)/(styles)/sera/edit-article/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { EditorWorkspace } from "./components/editor-workspace"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function EditArticle() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container py-(--gap)">
|
||||
<EditorWorkspace />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { FileTextIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { Card, CardContent } from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/base-sera/ui/empty"
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
type Stage = {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
dotClassName: string
|
||||
}
|
||||
|
||||
const STAGES: Stage[] = [
|
||||
{
|
||||
id: "drafting",
|
||||
label: "Drafting",
|
||||
description:
|
||||
"Start the writing process. Articles here are works in progress, visible only to editors and authors.",
|
||||
dotClassName: "bg-amber-600",
|
||||
},
|
||||
{
|
||||
id: "in-revision",
|
||||
label: "In Revision",
|
||||
description:
|
||||
"Content undergoing editorial review. Track changes and word counts as pieces take shape.",
|
||||
dotClassName: "bg-orange-700",
|
||||
},
|
||||
{
|
||||
id: "final-edit",
|
||||
label: "Final Edit",
|
||||
description:
|
||||
"The final polish before publication. Ensure all styling and factual checks are complete.",
|
||||
dotClassName: "bg-foreground",
|
||||
},
|
||||
]
|
||||
|
||||
export function EmptyDirectory() {
|
||||
return (
|
||||
<Card className="py-24">
|
||||
<CardContent className="flex flex-col items-center gap-10">
|
||||
<Empty className="min-h-96">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia
|
||||
variant="icon"
|
||||
className="size-14 rounded-full bg-muted/70 text-muted-foreground"
|
||||
>
|
||||
<FileTextIcon className="size-5" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle className="font-heading text-2xl tracking-normal normal-case">
|
||||
A Blank Canvas
|
||||
</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Your editorial directory is currently empty. Start building your
|
||||
publication's next issue by drafting the first piece.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
Create first article
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Separator className="max-w-2xl" />
|
||||
<div className="grid w-full max-w-2xl grid-cols-1 gap-8 sm:grid-cols-3">
|
||||
{STAGES.map((stage) => (
|
||||
<div key={stage.id} className="flex flex-col gap-2">
|
||||
<Badge>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`size-1.5 rounded-full ${stage.dotClassName}`}
|
||||
/>
|
||||
|
||||
{stage.label}
|
||||
</Badge>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{stage.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ArrowLeftIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
|
||||
<ArrowLeftIcon className="size-3.5" />
|
||||
Editorial Dashboard
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Article Directory
|
||||
</h1>
|
||||
</div>
|
||||
<Button className="sm:ml-auto">
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
New Article
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
16
apps/v4/app/(app)/(styles)/sera/empty-state/index.tsx
Normal file
16
apps/v4/app/(app)/(styles)/sera/empty-state/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { EmptyDirectory } from "./components/empty-directory"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function EmptyState() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container py-(--gap)">
|
||||
<EmptyDirectory />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
MoreVerticalIcon,
|
||||
SearchIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/styles/base-sera/ui/input-group"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/styles/base-sera/ui/pagination"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/base-sera/ui/table"
|
||||
|
||||
import { ASSETS, type AssetType } from "../../media-library/data"
|
||||
|
||||
function AssetTypeIcon({
|
||||
type,
|
||||
className,
|
||||
}: {
|
||||
type: AssetType
|
||||
className?: string
|
||||
}) {
|
||||
if (type === "MP4") {
|
||||
return <VideoIcon className={className} />
|
||||
}
|
||||
|
||||
if (type === "PDF") {
|
||||
return <FileTextIcon className={className} />
|
||||
}
|
||||
|
||||
return <ImageIcon className={className} />
|
||||
}
|
||||
|
||||
export function AssetTable() {
|
||||
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(
|
||||
new Set(["1"])
|
||||
)
|
||||
|
||||
const toggleSelection = React.useCallback((id: string) => {
|
||||
setSelectedIds((previous) => {
|
||||
const next = new Set(previous)
|
||||
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search files, tags, or metadata..." />
|
||||
</InputGroup>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0 py-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10 pl-6" aria-label="Select" />
|
||||
<TableHead className="w-20" aria-label="Preview" />
|
||||
<TableHead>Filename</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Dimensions</TableHead>
|
||||
<TableHead>Size</TableHead>
|
||||
<TableHead>Uploaded By</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="w-10 pr-6" aria-label="Actions" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ASSETS.map((asset) => {
|
||||
const isSelected = selectedIds.has(asset.id)
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={asset.id}
|
||||
data-state={isSelected ? "selected" : undefined}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleSelection(asset.id)}
|
||||
>
|
||||
<TableCell className="pl-6">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
aria-label={`Select ${asset.name}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onCheckedChange={() => toggleSelection(asset.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="relative flex aspect-4/3 w-16 items-center justify-center bg-muted/60 ring-1 ring-border/70 ring-inset">
|
||||
{asset.duration ? (
|
||||
<span className="absolute right-1 bottom-1 bg-foreground/90 px-1 text-[0.5rem] font-semibold tracking-wider text-background">
|
||||
{asset.duration}
|
||||
</span>
|
||||
) : null}
|
||||
<AssetTypeIcon
|
||||
type={asset.type}
|
||||
className="size-4 text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm font-medium text-foreground">
|
||||
{asset.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border px-2 py-0.5 text-[0.625rem]"
|
||||
>
|
||||
{asset.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{asset.dimensions}</TableCell>
|
||||
<TableCell className="text-sm">{asset.size}</TableCell>
|
||||
<TableCell>{asset.uploadedBy}</TableCell>
|
||||
<TableCell className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
||||
{asset.date}
|
||||
</TableCell>
|
||||
<TableCell className="pr-6 text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
aria-label={`Open actions for ${asset.name}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Preview</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center py-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious href="#" text="" />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#" isActive>
|
||||
1
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">2</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">3</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext href="#" text="" />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { addDays, format } from "date-fns"
|
||||
import { CalendarIcon, FilterIcon, XIcon } from "lucide-react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { Calendar } from "@/styles/base-sera/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/styles/base-sera/ui/combobox"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/styles/base-sera/ui/field"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/base-sera/ui/popover"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/base-sera/ui/radio-group"
|
||||
import { Slider } from "@/styles/base-sera/ui/slider"
|
||||
|
||||
const FILE_TYPES = [
|
||||
{
|
||||
id: "images",
|
||||
label: "Images (JPEG, PNG, WEBP)",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "video",
|
||||
label: "Video (MP4, MOV)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
label: "Documents (PDF)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "audio",
|
||||
label: "Audio (MP3, WAV)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
]
|
||||
|
||||
const DATE_OPTIONS = [
|
||||
{ value: "any", label: "Any time" },
|
||||
{ value: "24h", label: "Past 24 hours" },
|
||||
{ value: "week", label: "Past week" },
|
||||
{ value: "month", label: "Past month" },
|
||||
]
|
||||
|
||||
const TAGS = [
|
||||
"architecture",
|
||||
"brutalism",
|
||||
"ceramics",
|
||||
"design-week",
|
||||
"editorial",
|
||||
"exterior",
|
||||
"film",
|
||||
"food",
|
||||
"furniture",
|
||||
"interior",
|
||||
"kyoto",
|
||||
"minimalism",
|
||||
"print",
|
||||
"sustainability",
|
||||
"summer-issue",
|
||||
"video",
|
||||
] as const
|
||||
|
||||
export function FilterLibrary() {
|
||||
const tagAnchor = useComboboxAnchor()
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
to: addDays(
|
||||
new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
21
|
||||
),
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Filter Library</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>File Type</FieldLegend>
|
||||
<Field>
|
||||
{FILE_TYPES.map((type) => (
|
||||
<Field key={type.id} orientation="horizontal">
|
||||
<Checkbox id={type.id} defaultChecked={type.defaultChecked} />
|
||||
<FieldLabel htmlFor={type.id}>{type.label}</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>Date Uploaded</FieldLegend>
|
||||
<RadioGroup defaultValue="any">
|
||||
{DATE_OPTIONS.map((option) => (
|
||||
<Field key={option.value} orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value={option.value}
|
||||
id={`date-${option.value}`}
|
||||
/>
|
||||
<FieldLabel htmlFor={`date-${option.value}`}>
|
||||
{option.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="custom-range">Custom Range</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
id="custom-range"
|
||||
className="w-full"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CalendarIcon data-icon="inline-start" />
|
||||
{dateRange?.from ? (
|
||||
dateRange.to ? (
|
||||
<>
|
||||
{format(dateRange.from, "LLL dd, y")} –{" "}
|
||||
{format(dateRange.to, "LLL dd, y")}
|
||||
</>
|
||||
) : (
|
||||
format(dateRange.from, "LLL dd, y")
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date range</span>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>File Size</FieldLegend>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
<span>0 MB</span>
|
||||
<span>500+ MB</span>
|
||||
</div>
|
||||
<Slider defaultValue={[0, 60]} max={100} step={1} />
|
||||
<div className="flex items-center justify-between text-xs font-medium">
|
||||
<span>Min: 0 MB</span>
|
||||
<span>Max: 300 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>Tags</FieldLegend>
|
||||
<Field>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={TAGS}
|
||||
defaultValue={["architecture", "brutalism"]}
|
||||
>
|
||||
<ComboboxChips ref={tagAnchor}>
|
||||
<ComboboxValue>
|
||||
{(values) => (
|
||||
<React.Fragment>
|
||||
{values.map((value: string) => (
|
||||
<ComboboxChip key={value}>{value}</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput placeholder="Filter by tag..." />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={tagAnchor}>
|
||||
<ComboboxEmpty>No tags found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-2 border-t">
|
||||
<Button className="w-full">Apply Filters</Button>
|
||||
<Button variant="ghost" className="w-full">
|
||||
Reset
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ArrowLeftIcon, SlidersHorizontalIcon, UploadIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
|
||||
<ArrowLeftIcon className="size-3.5" />
|
||||
Asset management
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Media Library
|
||||
</h1>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-background hover:bg-background/80"
|
||||
>
|
||||
<SlidersHorizontalIcon data-icon="inline-start" />
|
||||
Filters
|
||||
</Button>
|
||||
<Button>
|
||||
<UploadIcon data-icon="inline-start" />
|
||||
Upload Assets
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { AssetTable } from "./components/asset-table"
|
||||
import { FilterLibrary } from "./components/filter-library"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function MediaLibraryTable() {
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container grid grid-cols-1 items-start gap-(--gap) py-(--gap) xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<AssetTable />
|
||||
<FilterLibrary />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
DownloadIcon,
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
PlusIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-sera/ui/item"
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { type Asset, type AssetType } from "../data"
|
||||
|
||||
const TYPE_LABEL: Record<AssetType, string> = {
|
||||
JPEG: "Image / JPEG",
|
||||
PNG: "Image / PNG",
|
||||
WEBP: "Image / WEBP",
|
||||
MP4: "Video / MP4",
|
||||
PDF: "Document / PDF",
|
||||
}
|
||||
|
||||
export function AssetDetails({ asset }: { asset: Asset }) {
|
||||
return (
|
||||
<Card className="gap-0">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex aspect-5/4 items-center justify-center bg-muted/60 text-muted-foreground/60 ring-1 ring-border/70 ring-inset">
|
||||
{asset.type === "MP4" ? (
|
||||
<VideoIcon className="size-8" />
|
||||
) : asset.type === "PDF" ? (
|
||||
<FileTextIcon className="size-8" />
|
||||
) : (
|
||||
<ImageIcon className="size-8" />
|
||||
)}
|
||||
</div>
|
||||
<h2 className="line-clamp-2 font-heading text-xl tracking-wide">
|
||||
{asset.name}
|
||||
</h2>
|
||||
<Separator />
|
||||
<dl className="flex flex-col gap-5 text-sm">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Asset Type
|
||||
</dt>
|
||||
<dd>{TYPE_LABEL[asset.type]}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Dimensions
|
||||
</dt>
|
||||
<dd>{asset.dimensions}</dd>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
File Size
|
||||
</dt>
|
||||
<dd>{asset.size}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Tags
|
||||
</h3>
|
||||
<Button variant="ghost" size="icon-xs" aria-label="Add tag">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
||||
{asset.tags.map((tag) => (
|
||||
<Badge key={tag}>{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Used In
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{asset.usedIn.map((usage) => (
|
||||
<Item key={usage.title} variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>{usage.title}</ItemTitle>
|
||||
<ItemDescription>{usage.role}</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="mt-6 border-t pt-6">
|
||||
<Button className="w-full">
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
Download
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
CheckIcon,
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
SearchIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/styles/base-sera/ui/input-group"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/styles/base-sera/ui/pagination"
|
||||
|
||||
import { ASSETS, type Asset, type AssetType } from "../data"
|
||||
|
||||
function AssetTypeIcon({
|
||||
type,
|
||||
className,
|
||||
}: {
|
||||
type: AssetType
|
||||
className?: string
|
||||
}) {
|
||||
if (type === "MP4") {
|
||||
return <VideoIcon className={className} />
|
||||
}
|
||||
|
||||
if (type === "PDF") {
|
||||
return <FileTextIcon className={className} />
|
||||
}
|
||||
|
||||
return <ImageIcon className={className} />
|
||||
}
|
||||
|
||||
export function AssetGrid({
|
||||
selectedId,
|
||||
onSelect,
|
||||
}: {
|
||||
selectedId: string
|
||||
onSelect: (id: string) => void
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search files, tags, or metadata..." />
|
||||
</InputGroup>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{ASSETS.map((asset) => (
|
||||
<AssetGridItem
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
selected={asset.id === selectedId}
|
||||
onSelect={() => onSelect(asset.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious href="#" />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#" isActive>
|
||||
1
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">2</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">3</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext href="#" />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function AssetGridItem({
|
||||
asset,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
asset: Asset
|
||||
selected: boolean
|
||||
onSelect: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSelect}
|
||||
aria-pressed={selected}
|
||||
className="group flex flex-col gap-2.5 text-left outline-none focus-visible:[&>div:first-child]:ring-2 focus-visible:[&>div:first-child]:ring-ring"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex aspect-4/3 items-center justify-center bg-muted/60 ring-1 ring-border/70 transition-shadow ring-inset group-hover:ring-foreground/40",
|
||||
selected && "ring-2 ring-foreground group-hover:ring-foreground"
|
||||
)}
|
||||
>
|
||||
{selected ? (
|
||||
<div className="absolute top-2 left-2 flex size-5 items-center justify-center bg-foreground text-background">
|
||||
<CheckIcon className="size-3" />
|
||||
</div>
|
||||
) : null}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="absolute top-2 right-2 border bg-background px-2 py-1 text-[0.625rem]"
|
||||
>
|
||||
{asset.type}
|
||||
</Badge>
|
||||
{asset.duration ? (
|
||||
<Badge className="absolute bottom-2 left-2 bg-foreground px-2 py-1 text-background">
|
||||
{asset.duration}
|
||||
</Badge>
|
||||
) : null}
|
||||
<AssetTypeIcon
|
||||
type={asset.type}
|
||||
className="size-7 text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 px-0.5">
|
||||
<p className="line-clamp-1 text-sm font-medium">{asset.name}</p>
|
||||
<p className="text-[0.625rem] font-semibold tracking-wider text-muted-foreground uppercase">
|
||||
{asset.date} · {asset.size}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ArrowLeftIcon, SlidersHorizontalIcon, UploadIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
|
||||
<ArrowLeftIcon className="size-3.5" />
|
||||
Asset management
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Media Library
|
||||
</h1>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-background hover:bg-background/80"
|
||||
>
|
||||
<SlidersHorizontalIcon data-icon="inline-start" />
|
||||
Filters
|
||||
</Button>
|
||||
<Button>
|
||||
<UploadIcon data-icon="inline-start" />
|
||||
Upload Assets
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
188
apps/v4/app/(app)/(styles)/sera/media-library/data.ts
Normal file
188
apps/v4/app/(app)/(styles)/sera/media-library/data.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
export type AssetType = "JPEG" | "PNG" | "WEBP" | "MP4" | "PDF"
|
||||
|
||||
export type Asset = {
|
||||
id: string
|
||||
name: string
|
||||
date: string
|
||||
size: string
|
||||
type: AssetType
|
||||
dimensions: string
|
||||
duration?: string
|
||||
uploadedBy: string
|
||||
uploadedByInitials: string
|
||||
uploadedOn: string
|
||||
tags: string[]
|
||||
usedIn: { title: string; role: string }[]
|
||||
}
|
||||
|
||||
export const ASSETS: Asset[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "brutalism-facade-01.jpg",
|
||||
date: "Oct 24",
|
||||
size: "4.2 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "4000 × 3000",
|
||||
uploadedBy: "Marcus Chen",
|
||||
uploadedByInitials: "MC",
|
||||
uploadedOn: "Oct 24, 2024",
|
||||
tags: ["architecture", "brutalism", "exterior", "summer-issue"],
|
||||
usedIn: [
|
||||
{ title: "Brutalism's Second Act", role: "Cover Image" },
|
||||
{ title: "Autumn Sartorial Code", role: "Inline Gallery" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "brutalism-interior-raw.jpg",
|
||||
date: "Oct 24",
|
||||
size: "3.8 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "3800 × 2850",
|
||||
uploadedBy: "Marcus Chen",
|
||||
uploadedByInitials: "MC",
|
||||
uploadedOn: "Oct 24, 2024",
|
||||
tags: ["architecture", "brutalism", "interior"],
|
||||
usedIn: [{ title: "Brutalism's Second Act", role: "Inline Gallery" }],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "seattle-living-building-diagram.png",
|
||||
date: "Oct 22",
|
||||
size: "1.1 MB",
|
||||
type: "PNG",
|
||||
dimensions: "2000 × 1500",
|
||||
uploadedBy: "Sarah Jenkins",
|
||||
uploadedByInitials: "SJ",
|
||||
uploadedOn: "Oct 22, 2024",
|
||||
tags: ["diagram", "sustainability", "seattle"],
|
||||
usedIn: [
|
||||
{ title: "The Future of Sustainable Architecture", role: "Diagram" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "interview-sofia-coppola-clip1.mp4",
|
||||
date: "Oct 18",
|
||||
size: "45.0 MB",
|
||||
type: "MP4",
|
||||
dimensions: "1920 × 1080",
|
||||
duration: "0:45",
|
||||
uploadedBy: "Emma Ross",
|
||||
uploadedByInitials: "ER",
|
||||
uploadedOn: "Oct 18, 2024",
|
||||
tags: ["video", "interview", "film"],
|
||||
usedIn: [{ title: "The Aesthetics of Isolation", role: "Featured Video" }],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "kyoto-kilns-pottery-detail.jpg",
|
||||
date: "Oct 15",
|
||||
size: "5.6 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "4500 × 3000",
|
||||
uploadedBy: "Marcus Chen",
|
||||
uploadedByInitials: "MC",
|
||||
uploadedOn: "Oct 15, 2024",
|
||||
tags: ["ceramics", "kyoto", "craft"],
|
||||
usedIn: [{ title: "Kyoto's Oldest Kilns", role: "Hero Image" }],
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "copenhagen-design-week-street.jpg",
|
||||
date: "Oct 12",
|
||||
size: "3.2 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "3600 × 2400",
|
||||
uploadedBy: "Noah Bennett",
|
||||
uploadedByInitials: "NB",
|
||||
uploadedOn: "Oct 12, 2024",
|
||||
tags: ["copenhagen", "design-week", "street"],
|
||||
usedIn: [{ title: "Field Notes from Copenhagen", role: "Inline Gallery" }],
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "minimalist-chair-render.webp",
|
||||
date: "Oct 10",
|
||||
size: "0.8 MB",
|
||||
type: "WEBP",
|
||||
dimensions: "2400 × 1600",
|
||||
uploadedBy: "Claire Duval",
|
||||
uploadedByInitials: "CD",
|
||||
uploadedOn: "Oct 10, 2024",
|
||||
tags: ["furniture", "minimalism", "render"],
|
||||
usedIn: [{ title: "The New Vanguard", role: "Product Shot" }],
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
name: "autumn-issue-style-guide.pdf",
|
||||
date: "Oct 05",
|
||||
size: "12.4 MB",
|
||||
type: "PDF",
|
||||
dimensions: "N/A",
|
||||
uploadedBy: "Emma Ross",
|
||||
uploadedByInitials: "ER",
|
||||
uploadedOn: "Oct 05, 2024",
|
||||
tags: ["guidelines", "internal", "autumn"],
|
||||
usedIn: [{ title: "Autumn Issue 2024", role: "Reference" }],
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
name: "milan-lighting-studio-visit.jpg",
|
||||
date: "Oct 09",
|
||||
size: "6.1 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "5200 × 3466",
|
||||
uploadedBy: "Claire Duval",
|
||||
uploadedByInitials: "CD",
|
||||
uploadedOn: "Oct 09, 2024",
|
||||
tags: ["milan", "lighting", "studio"],
|
||||
usedIn: [
|
||||
{ title: "Milan's Most Elusive Lighting Designer", role: "Hero Image" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
name: "lisbon-culinary-scene-raw.webp",
|
||||
date: "Oct 14",
|
||||
size: "2.4 MB",
|
||||
type: "WEBP",
|
||||
dimensions: "3000 × 2000",
|
||||
uploadedBy: "Amara Iqbal",
|
||||
uploadedByInitials: "AI",
|
||||
uploadedOn: "Oct 14, 2024",
|
||||
tags: ["lisbon", "food", "editorial"],
|
||||
usedIn: [
|
||||
{ title: "Lisbon's Quiet Culinary Renaissance", role: "Inline Gallery" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "11",
|
||||
name: "print-magazine-covers-mo...",
|
||||
date: "Sep 26",
|
||||
size: "8.9 MB",
|
||||
type: "PNG",
|
||||
dimensions: "3200 × 2400",
|
||||
uploadedBy: "Mina Okafor",
|
||||
uploadedByInitials: "MO",
|
||||
uploadedOn: "Sep 26, 2024",
|
||||
tags: ["print", "magazine", "covers"],
|
||||
usedIn: [{ title: "The Return of Print", role: "Cover Image" }],
|
||||
},
|
||||
{
|
||||
id: "12",
|
||||
name: "avant-garde-furniture-trailer.mp4",
|
||||
date: "Sep 30",
|
||||
size: "78.2 MB",
|
||||
type: "MP4",
|
||||
dimensions: "3840 × 2160",
|
||||
duration: "1:12",
|
||||
uploadedBy: "Tommy Rhodes",
|
||||
uploadedByInitials: "TR",
|
||||
uploadedOn: "Sep 30, 2024",
|
||||
tags: ["video", "furniture", "trailer"],
|
||||
usedIn: [
|
||||
{ title: "Collecting the New Avant-Garde", role: "Featured Video" },
|
||||
],
|
||||
},
|
||||
]
|
||||
30
apps/v4/app/(app)/(styles)/sera/media-library/index.tsx
Normal file
30
apps/v4/app/(app)/(styles)/sera/media-library/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { AssetDetails } from "./components/asset-details"
|
||||
import { AssetGrid } from "./components/asset-grid"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
import { ASSETS } from "./data"
|
||||
|
||||
export function MediaLibrary() {
|
||||
const [selectedId, setSelectedId] = React.useState<string>(ASSETS[0].id)
|
||||
|
||||
const selectedAsset = React.useMemo(
|
||||
() => ASSETS.find((asset) => asset.id === selectedId) ?? ASSETS[0],
|
||||
[selectedId]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container grid grid-cols-1 items-start gap-(--gap) py-(--gap) xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<AssetGrid selectedId={selectedId} onSelect={setSelectedId} />
|
||||
<AssetDetails asset={selectedAsset} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
apps/v4/app/(app)/(styles)/sera/opengraph-image.jpg
Normal file
BIN
apps/v4/app/(app)/(styles)/sera/opengraph-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
72
apps/v4/app/(app)/(styles)/sera/page.tsx
Normal file
72
apps/v4/app/(app)/(styles)/sera/page.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { type Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
import {
|
||||
PageActions,
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header"
|
||||
import { Button } from "@/styles/radix-sera/ui/button"
|
||||
|
||||
import { AudienceAnalytics } from "./audience-analytics"
|
||||
import { LazyPreview } from "./components/lazy-preview"
|
||||
|
||||
import "./style.css"
|
||||
|
||||
import { ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { ImagePreview } from "./components/image-preview"
|
||||
|
||||
const title = "Introducing Sera"
|
||||
const description =
|
||||
"Minimal. Editorial. Typographic. Underline Controls and Uppercase Headings. Shaped by Print Design Principles."
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
},
|
||||
}
|
||||
|
||||
export default function SeraPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader>
|
||||
<PageHeaderHeading className="font-(family-name:--font-playfair-display) text-[2.875rem] tracking-tight!">
|
||||
{title}
|
||||
</PageHeaderHeading>
|
||||
<PageHeaderDescription className="max-w-2xl text-pretty md:text-balance">
|
||||
{description}
|
||||
</PageHeaderDescription>
|
||||
<PageActions className="**:[.container]:justify-start">
|
||||
<Button asChild size="sm">
|
||||
<Link href="/create?preset=b4xFeBLg4O">
|
||||
Open in shadcn/create
|
||||
<ArrowRightIcon data-icon="inline-end" />
|
||||
</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<ImagePreview />
|
||||
<div className="container-wrapper hidden flex-1 flex-col section-soft px-0 md:flex md:px-2 md:py-12">
|
||||
<div className="container flex flex-1 flex-col gap-10 px-0 3xl:max-w-[2000px] md:px-6">
|
||||
<AudienceAnalytics />
|
||||
<LazyPreview name="articleDirectory" />
|
||||
<LazyPreview name="emptyState" />
|
||||
<LazyPreview name="editArticle" />
|
||||
<LazyPreview name="mediaLibrary" />
|
||||
<LazyPreview name="mediaLibraryTable" />
|
||||
</div>
|
||||
</div>
|
||||
{/* <ThemeSwitcher /> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
495
apps/v4/app/(app)/(styles)/sera/style.css
Normal file
495
apps/v4/app/(app)/(styles)/sera/style.css
Normal file
@@ -0,0 +1,495 @@
|
||||
@layer base {
|
||||
.preview {
|
||||
--font-sans: var(--font-noto-sans);
|
||||
--font-heading: var(--font-playfair-display);
|
||||
contain-intrinsic-size: auto 900px;
|
||||
content-visibility: auto;
|
||||
}
|
||||
|
||||
.theme-taupe {
|
||||
--radius: 0;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.147 0.004 49.3);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.147 0.004 49.3);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.147 0.004 49.3);
|
||||
--primary: oklch(0.214 0.009 43.1);
|
||||
--primary-foreground: oklch(0.986 0.002 67.8);
|
||||
--secondary: oklch(0.96 0.002 17.2);
|
||||
--secondary-foreground: oklch(0.214 0.009 43.1);
|
||||
--muted: oklch(0.96 0.002 17.2);
|
||||
--muted-foreground: oklch(0.547 0.021 43.1);
|
||||
--accent: oklch(0.96 0.002 17.2);
|
||||
--accent-foreground: oklch(0.214 0.009 43.1);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0.005 34.3);
|
||||
--input: oklch(0.922 0.005 34.3);
|
||||
--ring: oklch(0.714 0.014 41.2);
|
||||
--chart-1: oklch(0.868 0.007 39.5);
|
||||
--chart-2: oklch(0.547 0.021 43.1);
|
||||
--chart-3: oklch(0.438 0.017 39.3);
|
||||
--chart-4: oklch(0.367 0.016 35.7);
|
||||
--chart-5: oklch(0.268 0.011 36.5);
|
||||
--sidebar: oklch(0.986 0.002 67.8);
|
||||
--sidebar-foreground: oklch(0.147 0.004 49.3);
|
||||
--sidebar-primary: oklch(0.214 0.009 43.1);
|
||||
--sidebar-primary-foreground: oklch(0.986 0.002 67.8);
|
||||
--sidebar-accent: oklch(0.96 0.002 17.2);
|
||||
--sidebar-accent-foreground: oklch(0.214 0.009 43.1);
|
||||
--sidebar-border: oklch(0.922 0.005 34.3);
|
||||
--sidebar-ring: oklch(0.714 0.014 41.2);
|
||||
|
||||
.dark & {
|
||||
--background: oklch(0.147 0.004 49.3);
|
||||
--foreground: oklch(0.986 0.002 67.8);
|
||||
--card: oklch(0.214 0.009 43.1);
|
||||
--card-foreground: oklch(0.986 0.002 67.8);
|
||||
--popover: oklch(0.214 0.009 43.1);
|
||||
--popover-foreground: oklch(0.986 0.002 67.8);
|
||||
--primary: oklch(0.922 0.005 34.3);
|
||||
--primary-foreground: oklch(0.214 0.009 43.1);
|
||||
--secondary: oklch(0.268 0.011 36.5);
|
||||
--secondary-foreground: oklch(0.986 0.002 67.8);
|
||||
--muted: oklch(0.268 0.011 36.5);
|
||||
--muted-foreground: oklch(0.714 0.014 41.2);
|
||||
--accent: oklch(0.268 0.011 36.5);
|
||||
--accent-foreground: oklch(0.986 0.002 67.8);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.547 0.021 43.1);
|
||||
--chart-1: oklch(0.868 0.007 39.5);
|
||||
--chart-2: oklch(0.547 0.021 43.1);
|
||||
--chart-3: oklch(0.438 0.017 39.3);
|
||||
--chart-4: oklch(0.367 0.016 35.7);
|
||||
--chart-5: oklch(0.268 0.011 36.5);
|
||||
--sidebar: oklch(0.214 0.009 43.1);
|
||||
--sidebar-foreground: oklch(0.986 0.002 67.8);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.986 0.002 67.8);
|
||||
--sidebar-accent: oklch(0.268 0.011 36.5);
|
||||
--sidebar-accent-foreground: oklch(0.986 0.002 67.8);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.547 0.021 43.1);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-neutral {
|
||||
--radius: 0;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
|
||||
.dark & {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-stone {
|
||||
--radius: 0;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.147 0.004 49.25);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.147 0.004 49.25);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.147 0.004 49.25);
|
||||
--primary: oklch(0.216 0.006 56.043);
|
||||
--primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--secondary: oklch(0.97 0.001 106.424);
|
||||
--secondary-foreground: oklch(0.216 0.006 56.043);
|
||||
--muted: oklch(0.97 0.001 106.424);
|
||||
--muted-foreground: oklch(0.553 0.013 58.071);
|
||||
--accent: oklch(0.97 0.001 106.424);
|
||||
--accent-foreground: oklch(0.216 0.006 56.043);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.923 0.003 48.717);
|
||||
--input: oklch(0.923 0.003 48.717);
|
||||
--ring: oklch(0.709 0.01 56.259);
|
||||
--chart-1: oklch(0.869 0.005 56.366);
|
||||
--chart-2: oklch(0.553 0.013 58.071);
|
||||
--chart-3: oklch(0.444 0.011 73.639);
|
||||
--chart-4: oklch(0.374 0.01 67.558);
|
||||
--chart-5: oklch(0.268 0.007 34.298);
|
||||
--sidebar: oklch(0.985 0.001 106.423);
|
||||
--sidebar-foreground: oklch(0.147 0.004 49.25);
|
||||
--sidebar-primary: oklch(0.216 0.006 56.043);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-accent: oklch(0.97 0.001 106.424);
|
||||
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
|
||||
--sidebar-border: oklch(0.923 0.003 48.717);
|
||||
--sidebar-ring: oklch(0.709 0.01 56.259);
|
||||
|
||||
.dark & {
|
||||
--background: oklch(0.147 0.004 49.25);
|
||||
--foreground: oklch(0.985 0.001 106.423);
|
||||
--card: oklch(0.216 0.006 56.043);
|
||||
--card-foreground: oklch(0.985 0.001 106.423);
|
||||
--popover: oklch(0.216 0.006 56.043);
|
||||
--popover-foreground: oklch(0.985 0.001 106.423);
|
||||
--primary: oklch(0.923 0.003 48.717);
|
||||
--primary-foreground: oklch(0.216 0.006 56.043);
|
||||
--secondary: oklch(0.268 0.007 34.298);
|
||||
--secondary-foreground: oklch(0.985 0.001 106.423);
|
||||
--muted: oklch(0.268 0.007 34.298);
|
||||
--muted-foreground: oklch(0.709 0.01 56.259);
|
||||
--accent: oklch(0.268 0.007 34.298);
|
||||
--accent-foreground: oklch(0.985 0.001 106.423);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.553 0.013 58.071);
|
||||
--chart-1: oklch(0.869 0.005 56.366);
|
||||
--chart-2: oklch(0.553 0.013 58.071);
|
||||
--chart-3: oklch(0.444 0.011 73.639);
|
||||
--chart-4: oklch(0.374 0.01 67.558);
|
||||
--chart-5: oklch(0.268 0.007 34.298);
|
||||
--sidebar: oklch(0.216 0.006 56.043);
|
||||
--sidebar-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-accent: oklch(0.268 0.007 34.298);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.553 0.013 58.071);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-zinc {
|
||||
--radius: 0;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.871 0.006 286.286);
|
||||
--chart-2: oklch(0.552 0.016 285.938);
|
||||
--chart-3: oklch(0.442 0.017 285.786);
|
||||
--chart-4: oklch(0.37 0.013 285.805);
|
||||
--chart-5: oklch(0.274 0.006 286.033);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
|
||||
.dark & {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.871 0.006 286.286);
|
||||
--chart-2: oklch(0.552 0.016 285.938);
|
||||
--chart-3: oklch(0.442 0.017 285.786);
|
||||
--chart-4: oklch(0.37 0.013 285.805);
|
||||
--chart-5: oklch(0.274 0.006 286.033);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-mauve {
|
||||
--radius: 0;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0.008 326);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0.008 326);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0.008 326);
|
||||
--primary: oklch(0.212 0.019 322.12);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.96 0.003 325.6);
|
||||
--secondary-foreground: oklch(0.212 0.019 322.12);
|
||||
--muted: oklch(0.96 0.003 325.6);
|
||||
--muted-foreground: oklch(0.542 0.034 322.5);
|
||||
--accent: oklch(0.96 0.003 325.6);
|
||||
--accent-foreground: oklch(0.212 0.019 322.12);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0.005 325.62);
|
||||
--input: oklch(0.922 0.005 325.62);
|
||||
--ring: oklch(0.711 0.019 323.02);
|
||||
--chart-1: oklch(0.865 0.012 325.68);
|
||||
--chart-2: oklch(0.542 0.034 322.5);
|
||||
--chart-3: oklch(0.435 0.029 321.78);
|
||||
--chart-4: oklch(0.364 0.029 323.89);
|
||||
--chart-5: oklch(0.263 0.024 320.12);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0.008 326);
|
||||
--sidebar-primary: oklch(0.212 0.019 322.12);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.96 0.003 325.6);
|
||||
--sidebar-accent-foreground: oklch(0.212 0.019 322.12);
|
||||
--sidebar-border: oklch(0.922 0.005 325.62);
|
||||
--sidebar-ring: oklch(0.711 0.019 323.02);
|
||||
|
||||
.dark & {
|
||||
--background: oklch(0.145 0.008 326);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.212 0.019 322.12);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.212 0.019 322.12);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0.005 325.62);
|
||||
--primary-foreground: oklch(0.212 0.019 322.12);
|
||||
--secondary: oklch(0.263 0.024 320.12);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.263 0.024 320.12);
|
||||
--muted-foreground: oklch(0.711 0.019 323.02);
|
||||
--accent: oklch(0.263 0.024 320.12);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.542 0.034 322.5);
|
||||
--chart-1: oklch(0.865 0.012 325.68);
|
||||
--chart-2: oklch(0.542 0.034 322.5);
|
||||
--chart-3: oklch(0.435 0.029 321.78);
|
||||
--chart-4: oklch(0.364 0.029 323.89);
|
||||
--chart-5: oklch(0.263 0.024 320.12);
|
||||
--sidebar: oklch(0.212 0.019 322.12);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.263 0.024 320.12);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.542 0.034 322.5);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-olive {
|
||||
--radius: 0;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.153 0.006 107.1);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.153 0.006 107.1);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.153 0.006 107.1);
|
||||
--primary: oklch(0.228 0.013 107.4);
|
||||
--primary-foreground: oklch(0.988 0.003 106.5);
|
||||
--secondary: oklch(0.966 0.005 106.5);
|
||||
--secondary-foreground: oklch(0.228 0.013 107.4);
|
||||
--muted: oklch(0.966 0.005 106.5);
|
||||
--muted-foreground: oklch(0.58 0.031 107.3);
|
||||
--accent: oklch(0.966 0.005 106.5);
|
||||
--accent-foreground: oklch(0.228 0.013 107.4);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.93 0.007 106.5);
|
||||
--input: oklch(0.93 0.007 106.5);
|
||||
--ring: oklch(0.737 0.021 106.9);
|
||||
--chart-1: oklch(0.88 0.011 106.6);
|
||||
--chart-2: oklch(0.58 0.031 107.3);
|
||||
--chart-3: oklch(0.466 0.025 107.3);
|
||||
--chart-4: oklch(0.394 0.023 107.4);
|
||||
--chart-5: oklch(0.286 0.016 107.4);
|
||||
--sidebar: oklch(0.988 0.003 106.5);
|
||||
--sidebar-foreground: oklch(0.153 0.006 107.1);
|
||||
--sidebar-primary: oklch(0.228 0.013 107.4);
|
||||
--sidebar-primary-foreground: oklch(0.988 0.003 106.5);
|
||||
--sidebar-accent: oklch(0.966 0.005 106.5);
|
||||
--sidebar-accent-foreground: oklch(0.228 0.013 107.4);
|
||||
--sidebar-border: oklch(0.93 0.007 106.5);
|
||||
--sidebar-ring: oklch(0.737 0.021 106.9);
|
||||
|
||||
.dark & {
|
||||
--background: oklch(0.153 0.006 107.1);
|
||||
--foreground: oklch(0.988 0.003 106.5);
|
||||
--card: oklch(0.228 0.013 107.4);
|
||||
--card-foreground: oklch(0.988 0.003 106.5);
|
||||
--popover: oklch(0.228 0.013 107.4);
|
||||
--popover-foreground: oklch(0.988 0.003 106.5);
|
||||
--primary: oklch(0.93 0.007 106.5);
|
||||
--primary-foreground: oklch(0.228 0.013 107.4);
|
||||
--secondary: oklch(0.286 0.016 107.4);
|
||||
--secondary-foreground: oklch(0.988 0.003 106.5);
|
||||
--muted: oklch(0.286 0.016 107.4);
|
||||
--muted-foreground: oklch(0.737 0.021 106.9);
|
||||
--accent: oklch(0.286 0.016 107.4);
|
||||
--accent-foreground: oklch(0.988 0.003 106.5);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.58 0.031 107.3);
|
||||
--chart-1: oklch(0.88 0.011 106.6);
|
||||
--chart-2: oklch(0.58 0.031 107.3);
|
||||
--chart-3: oklch(0.466 0.025 107.3);
|
||||
--chart-4: oklch(0.394 0.023 107.4);
|
||||
--chart-5: oklch(0.286 0.016 107.4);
|
||||
--sidebar: oklch(0.228 0.013 107.4);
|
||||
--sidebar-foreground: oklch(0.988 0.003 106.5);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.988 0.003 106.5);
|
||||
--sidebar-accent: oklch(0.286 0.016 107.4);
|
||||
--sidebar-accent-foreground: oklch(0.988 0.003 106.5);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.58 0.031 107.3);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-mist {
|
||||
--radius: 0;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.148 0.004 228.8);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.148 0.004 228.8);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.148 0.004 228.8);
|
||||
--primary: oklch(0.218 0.008 223.9);
|
||||
--primary-foreground: oklch(0.987 0.002 197.1);
|
||||
--secondary: oklch(0.963 0.002 197.1);
|
||||
--secondary-foreground: oklch(0.218 0.008 223.9);
|
||||
--muted: oklch(0.963 0.002 197.1);
|
||||
--muted-foreground: oklch(0.56 0.021 213.5);
|
||||
--accent: oklch(0.963 0.002 197.1);
|
||||
--accent-foreground: oklch(0.218 0.008 223.9);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.925 0.005 214.3);
|
||||
--input: oklch(0.925 0.005 214.3);
|
||||
--ring: oklch(0.723 0.014 214.4);
|
||||
--chart-1: oklch(0.872 0.007 219.6);
|
||||
--chart-2: oklch(0.56 0.021 213.5);
|
||||
--chart-3: oklch(0.45 0.017 213.2);
|
||||
--chart-4: oklch(0.378 0.015 216);
|
||||
--chart-5: oklch(0.275 0.011 216.9);
|
||||
--sidebar: oklch(0.987 0.002 197.1);
|
||||
--sidebar-foreground: oklch(0.148 0.004 228.8);
|
||||
--sidebar-primary: oklch(0.218 0.008 223.9);
|
||||
--sidebar-primary-foreground: oklch(0.987 0.002 197.1);
|
||||
--sidebar-accent: oklch(0.963 0.002 197.1);
|
||||
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
|
||||
--sidebar-border: oklch(0.925 0.005 214.3);
|
||||
--sidebar-ring: oklch(0.723 0.014 214.4);
|
||||
|
||||
.dark & {
|
||||
--background: oklch(0.148 0.004 228.8);
|
||||
--foreground: oklch(0.987 0.002 197.1);
|
||||
--card: oklch(0.218 0.008 223.9);
|
||||
--card-foreground: oklch(0.987 0.002 197.1);
|
||||
--popover: oklch(0.218 0.008 223.9);
|
||||
--popover-foreground: oklch(0.987 0.002 197.1);
|
||||
--primary: oklch(0.925 0.005 214.3);
|
||||
--primary-foreground: oklch(0.218 0.008 223.9);
|
||||
--secondary: oklch(0.275 0.011 216.9);
|
||||
--secondary-foreground: oklch(0.987 0.002 197.1);
|
||||
--muted: oklch(0.275 0.011 216.9);
|
||||
--muted-foreground: oklch(0.723 0.014 214.4);
|
||||
--accent: oklch(0.275 0.011 216.9);
|
||||
--accent-foreground: oklch(0.987 0.002 197.1);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.56 0.021 213.5);
|
||||
--chart-1: oklch(0.872 0.007 219.6);
|
||||
--chart-2: oklch(0.56 0.021 213.5);
|
||||
--chart-3: oklch(0.45 0.017 213.2);
|
||||
--chart-4: oklch(0.378 0.015 216);
|
||||
--chart-5: oklch(0.275 0.011 216.9);
|
||||
--sidebar: oklch(0.218 0.008 223.9);
|
||||
--sidebar-foreground: oklch(0.987 0.002 197.1);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.987 0.002 197.1);
|
||||
--sidebar-accent: oklch(0.275 0.011 216.9);
|
||||
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.56 0.021 213.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@utility font-heading {
|
||||
font-family: var(--font-serif);
|
||||
}
|
||||
BIN
apps/v4/app/(app)/(styles)/sera/twitter-image.jpg
Normal file
BIN
apps/v4/app/(app)/(styles)/sera/twitter-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -10,6 +10,7 @@ import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||
const presetCode = usePresetCode()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
const label = hasCopied ? "Copied" : `--preset ${presetCode}`
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
@@ -32,12 +33,13 @@ export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
title={label}
|
||||
className={cn(
|
||||
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span>{hasCopied ? "Copied" : `--preset ${presetCode}`}</span>
|
||||
<span className="block min-w-0 truncate">{label}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user