Compare commits
7 Commits
b6562b1de4
...
5e519be049
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e519be049 | ||
|
|
e698605e62 | ||
| 9ea2731c9a | |||
| ffc94c23fa | |||
| b0efc0e363 | |||
| 9eff8f5e75 | |||
|
|
112dae8d6f |
@ -3,9 +3,17 @@ name: CI — Common Platform
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'packages/**'
|
||||||
|
- 'services/**'
|
||||||
|
- 'dashboards/**'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- 'package.json'
|
||||||
|
- 'tsconfig.base.json'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ci-common-plat-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -13,63 +21,63 @@ jobs:
|
|||||||
name: Build, Test & Typecheck
|
name: Build, Test & Typecheck
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: /Users/sd9235/code/mygh/learning_ai_common_plat
|
|
||||||
steps:
|
steps:
|
||||||
- name: Reset and pull latest
|
- name: Pull latest
|
||||||
run: git reset --hard HEAD && git clean -fd && git pull --ff-only origin main || true
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
git fetch origin main
|
||||||
|
git checkout main
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build all packages
|
- name: Build all packages
|
||||||
run: pnpm -r --filter './packages/**' build
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
pnpm build
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: pnpm lint
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: pnpm typecheck
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: pnpm test
|
|
||||||
|
|
||||||
token-drift:
|
|
||||||
name: Check design token drift
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: /Users/sd9235/code/mygh/learning_ai_common_plat
|
|
||||||
steps:
|
|
||||||
- name: Reset and pull latest
|
|
||||||
run: git reset --hard HEAD && git clean -fd && git pull --ff-only origin main || true
|
|
||||||
|
|
||||||
- name: Regenerate tokens
|
|
||||||
run: npx tsx packages/design-tokens/scripts/generate.ts
|
|
||||||
|
|
||||||
- name: Check for drift
|
|
||||||
run: |
|
run: |
|
||||||
if git diff --exit-code packages/design-tokens/generated/; then
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
echo "✅ No token drift detected"
|
pnpm test
|
||||||
else
|
|
||||||
echo "❌ Token drift detected — run 'npx tsx packages/design-tokens/scripts/generate.ts' and commit"
|
|
||||||
git diff packages/design-tokens/generated/
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
publish-packages:
|
publish-packages:
|
||||||
name: Publish @bytelyst/* to Gitea npm registry
|
name: Publish @bytelyst/* to Gitea npm registry
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build-and-test]
|
needs: [build-and-test]
|
||||||
timeout-minutes: 10
|
timeout-minutes: 15
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: /Users/sd9235/code/mygh/learning_ai_common_plat
|
|
||||||
steps:
|
steps:
|
||||||
- name: Reset and pull latest
|
- name: Pull latest
|
||||||
run: git reset --hard HEAD && git clean -fd && git pull --ff-only origin main || true
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
git fetch origin main
|
||||||
|
git checkout main
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build all packages
|
- name: Build all packages
|
||||||
run: pnpm -r --filter './packages/**' build
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
pnpm build
|
||||||
|
|
||||||
- name: Publish outdated packages to Gitea registry
|
- name: Publish outdated packages to Gitea registry
|
||||||
run: bash ./scripts/gitea/publish-outdated-packages.sh --skip-build
|
run: |
|
||||||
|
cd /opt/bytelyst/learning_ai_common_plat
|
||||||
|
bash ./scripts/gitea/publish-outdated-packages.sh --skip-build
|
||||||
|
|||||||
3
.npmrc
3
.npmrc
@ -1,5 +1,2 @@
|
|||||||
@bytelyst:registry=http://${GITEA_NPM_HOST:-localhost}:3300/api/packages/ByteLyst/npm/
|
|
||||||
//localhost:3300/api/packages/ByteLyst/npm/:_authToken=${GITEA_NPM_TOKEN}
|
|
||||||
strict-ssl=false
|
|
||||||
link-workspace-packages=true
|
link-workspace-packages=true
|
||||||
prefer-workspace-packages=true
|
prefer-workspace-packages=true
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
"docker:clean": "./scripts/docker-clean.sh",
|
"docker:clean": "./scripts/docker-clean.sh",
|
||||||
"dns:godaddy:bytelyst": "./scripts/godaddy-sync-bytelyst-dns.sh",
|
"dns:godaddy:bytelyst": "./scripts/godaddy-sync-bytelyst-dns.sh",
|
||||||
"prototype:self-test": "./scripts/prototype-self-test.sh",
|
"prototype:self-test": "./scripts/prototype-self-test.sh",
|
||||||
"release": "./scripts/gitea/release-packages.sh",
|
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -16,8 +16,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,5 @@
|
|||||||
"happy-dom": "^18.0.1",
|
"happy-dom": "^18.0.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,5 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"jose": ">=5.0.0",
|
"jose": ">=5.0.0",
|
||||||
"bcryptjs": ">=2.4.0"
|
"bcryptjs": ">=2.4.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,8 +26,5 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
]
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,5 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
]
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,5 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
]
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,5 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bytelyst/storage": "workspace:*"
|
"@bytelyst/storage": "workspace:*"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,8 +41,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@azure/identity": "^4.13.0",
|
"@azure/identity": "^4.13.0",
|
||||||
"@azure/keyvault-secrets": "^4.10.0"
|
"@azure/keyvault-secrets": "^4.10.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,5 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@azure/cosmos": ">=4.0.0"
|
"@azure/cosmos": ">=4.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,5 @@
|
|||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^3.0.5"
|
"vitest": "^3.0.5"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,5 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,5 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,8 +31,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,36 @@
|
|||||||
--ml-focus-ring: rgba(90,140,255,0.45);
|
--ml-focus-ring: rgba(90,140,255,0.45);
|
||||||
--ml-overlay-scrim: rgba(5,8,18,0.72);
|
--ml-overlay-scrim: rgba(5,8,18,0.72);
|
||||||
|
|
||||||
|
--bl-bg-canvas: var(--ml-bg-canvas);
|
||||||
|
--bl-bg-elevated: var(--ml-bg-elevated);
|
||||||
|
--bl-surface-card: var(--ml-surface-card);
|
||||||
|
--bl-surface-muted: var(--ml-surface-muted);
|
||||||
|
--bl-surface-highlight: color-mix(in oklab, var(--ml-surface-muted) 82%, white);
|
||||||
|
--bl-surface-overlay: color-mix(in oklab, var(--ml-bg-canvas) 88%, transparent);
|
||||||
|
--bl-input: color-mix(in oklab, var(--ml-surface-muted) 76%, var(--ml-bg-canvas));
|
||||||
|
--bl-border: var(--ml-border-default);
|
||||||
|
--bl-border-strong: var(--ml-border-strong);
|
||||||
|
--bl-border-subtle: color-mix(in oklab, var(--ml-border-default) 62%, transparent);
|
||||||
|
--bl-text-primary: var(--ml-text-primary);
|
||||||
|
--bl-text-secondary: var(--ml-text-secondary);
|
||||||
|
--bl-text-tertiary: var(--ml-text-tertiary);
|
||||||
|
--bl-text-quiet: color-mix(in oklab, var(--ml-text-secondary) 78%, var(--ml-bg-canvas));
|
||||||
|
--bl-accent: var(--ml-accent-primary);
|
||||||
|
--bl-accent-foreground: var(--ml-bg-canvas);
|
||||||
|
--bl-accent-muted: color-mix(in oklab, var(--ml-accent-primary) 16%, transparent);
|
||||||
|
--bl-info: var(--ml-accent-primary);
|
||||||
|
--bl-info-muted: color-mix(in oklab, var(--ml-accent-primary) 14%, transparent);
|
||||||
|
--bl-success: var(--ml-success);
|
||||||
|
--bl-success-muted: color-mix(in oklab, var(--ml-success) 14%, transparent);
|
||||||
|
--bl-warning: var(--ml-warning);
|
||||||
|
--bl-warning-muted: color-mix(in oklab, var(--ml-warning) 14%, transparent);
|
||||||
|
--bl-danger: var(--ml-danger);
|
||||||
|
--bl-danger-muted: color-mix(in oklab, var(--ml-danger) 14%, transparent);
|
||||||
|
--bl-danger-foreground: var(--ml-bg-canvas);
|
||||||
|
--bl-focus-ring: var(--ml-focus-ring);
|
||||||
|
--bl-focus-ring-muted: color-mix(in oklab, var(--ml-accent-primary) 18%, transparent);
|
||||||
|
--bl-overlay-scrim: var(--ml-overlay-scrim);
|
||||||
|
|
||||||
--ml-font-display: "Space Grotesk", "SF Pro Display", sans-serif;
|
--ml-font-display: "Space Grotesk", "SF Pro Display", sans-serif;
|
||||||
--ml-font-body: "DM Sans", "SF Pro Text", sans-serif;
|
--ml-font-body: "DM Sans", "SF Pro Text", sans-serif;
|
||||||
--ml-font-mono: "IBM Plex Mono", "SF Mono", monospace;
|
--ml-font-mono: "IBM Plex Mono", "SF Mono", monospace;
|
||||||
@ -50,10 +80,18 @@
|
|||||||
--ml-radius-lg: 20px;
|
--ml-radius-lg: 20px;
|
||||||
--ml-radius-xl: 24px;
|
--ml-radius-xl: 24px;
|
||||||
--ml-radius-pill: 999px;
|
--ml-radius-pill: 999px;
|
||||||
|
--bl-radius-control: var(--ml-radius-xs);
|
||||||
|
--bl-radius-surface: var(--ml-radius-sm);
|
||||||
|
--bl-radius-card: var(--ml-radius-md);
|
||||||
|
--bl-radius-panel: var(--ml-radius-lg);
|
||||||
|
--bl-radius-pill: var(--ml-radius-pill);
|
||||||
|
|
||||||
--ml-elevation-sm: 0 4px 12px rgba(0,0,0,0.12);
|
--ml-elevation-sm: 0 4px 12px rgba(0,0,0,0.12);
|
||||||
--ml-elevation-md: 0 12px 28px rgba(0,0,0,0.18);
|
--ml-elevation-md: 0 12px 28px rgba(0,0,0,0.18);
|
||||||
--ml-elevation-lg: 0 20px 48px rgba(0,0,0,0.24);
|
--ml-elevation-lg: 0 20px 48px rgba(0,0,0,0.24);
|
||||||
|
--bl-shadow-sm: var(--ml-elevation-sm);
|
||||||
|
--bl-shadow-md: var(--ml-elevation-md);
|
||||||
|
--bl-shadow-lg: var(--ml-elevation-lg);
|
||||||
|
|
||||||
--ml-motion-fast: 140ms;
|
--ml-motion-fast: 140ms;
|
||||||
--ml-motion-base: 220ms;
|
--ml-motion-base: 220ms;
|
||||||
|
|||||||
@ -37,8 +37,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsx": "^4.0.0"
|
"tsx": "^4.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,49 @@ function generateCSS(): string {
|
|||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
|
// ByteLyst semantic aliases. These are the shared UI contract and map the
|
||||||
|
// historical MindLyst token names into a product-neutral design system.
|
||||||
|
lines.push(' --bl-bg-canvas: var(--ml-bg-canvas);');
|
||||||
|
lines.push(' --bl-bg-elevated: var(--ml-bg-elevated);');
|
||||||
|
lines.push(' --bl-surface-card: var(--ml-surface-card);');
|
||||||
|
lines.push(' --bl-surface-muted: var(--ml-surface-muted);');
|
||||||
|
lines.push(' --bl-surface-highlight: color-mix(in oklab, var(--ml-surface-muted) 82%, white);');
|
||||||
|
lines.push(' --bl-surface-overlay: color-mix(in oklab, var(--ml-bg-canvas) 88%, transparent);');
|
||||||
|
lines.push(
|
||||||
|
' --bl-input: color-mix(in oklab, var(--ml-surface-muted) 76%, var(--ml-bg-canvas));'
|
||||||
|
);
|
||||||
|
lines.push(' --bl-border: var(--ml-border-default);');
|
||||||
|
lines.push(' --bl-border-strong: var(--ml-border-strong);');
|
||||||
|
lines.push(
|
||||||
|
' --bl-border-subtle: color-mix(in oklab, var(--ml-border-default) 62%, transparent);'
|
||||||
|
);
|
||||||
|
lines.push(' --bl-text-primary: var(--ml-text-primary);');
|
||||||
|
lines.push(' --bl-text-secondary: var(--ml-text-secondary);');
|
||||||
|
lines.push(' --bl-text-tertiary: var(--ml-text-tertiary);');
|
||||||
|
lines.push(
|
||||||
|
' --bl-text-quiet: color-mix(in oklab, var(--ml-text-secondary) 78%, var(--ml-bg-canvas));'
|
||||||
|
);
|
||||||
|
lines.push(' --bl-accent: var(--ml-accent-primary);');
|
||||||
|
lines.push(' --bl-accent-foreground: var(--ml-bg-canvas);');
|
||||||
|
lines.push(
|
||||||
|
' --bl-accent-muted: color-mix(in oklab, var(--ml-accent-primary) 16%, transparent);'
|
||||||
|
);
|
||||||
|
lines.push(' --bl-info: var(--ml-accent-primary);');
|
||||||
|
lines.push(' --bl-info-muted: color-mix(in oklab, var(--ml-accent-primary) 14%, transparent);');
|
||||||
|
lines.push(' --bl-success: var(--ml-success);');
|
||||||
|
lines.push(' --bl-success-muted: color-mix(in oklab, var(--ml-success) 14%, transparent);');
|
||||||
|
lines.push(' --bl-warning: var(--ml-warning);');
|
||||||
|
lines.push(' --bl-warning-muted: color-mix(in oklab, var(--ml-warning) 14%, transparent);');
|
||||||
|
lines.push(' --bl-danger: var(--ml-danger);');
|
||||||
|
lines.push(' --bl-danger-muted: color-mix(in oklab, var(--ml-danger) 14%, transparent);');
|
||||||
|
lines.push(' --bl-danger-foreground: var(--ml-bg-canvas);');
|
||||||
|
lines.push(' --bl-focus-ring: var(--ml-focus-ring);');
|
||||||
|
lines.push(
|
||||||
|
' --bl-focus-ring-muted: color-mix(in oklab, var(--ml-accent-primary) 18%, transparent);'
|
||||||
|
);
|
||||||
|
lines.push(' --bl-overlay-scrim: var(--ml-overlay-scrim);');
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
for (const [key, value] of Object.entries(tokens.typography.fontFamily)) {
|
for (const [key, value] of Object.entries(tokens.typography.fontFamily)) {
|
||||||
// Swap single quotes → double quotes for CSS
|
// Swap single quotes → double quotes for CSS
|
||||||
@ -99,6 +142,11 @@ function generateCSS(): string {
|
|||||||
for (const [key, value] of Object.entries(tokens.radius)) {
|
for (const [key, value] of Object.entries(tokens.radius)) {
|
||||||
lines.push(` --ml-radius-${key}: ${value}px;`);
|
lines.push(` --ml-radius-${key}: ${value}px;`);
|
||||||
}
|
}
|
||||||
|
lines.push(' --bl-radius-control: var(--ml-radius-xs);');
|
||||||
|
lines.push(' --bl-radius-surface: var(--ml-radius-sm);');
|
||||||
|
lines.push(' --bl-radius-card: var(--ml-radius-md);');
|
||||||
|
lines.push(' --bl-radius-panel: var(--ml-radius-lg);');
|
||||||
|
lines.push(' --bl-radius-pill: var(--ml-radius-pill);');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
// Elevation (--ml-elevation-* to match existing)
|
// Elevation (--ml-elevation-* to match existing)
|
||||||
@ -106,6 +154,9 @@ function generateCSS(): string {
|
|||||||
if (key === 'none') continue;
|
if (key === 'none') continue;
|
||||||
lines.push(` --ml-elevation-${key}: ${value};`);
|
lines.push(` --ml-elevation-${key}: ${value};`);
|
||||||
}
|
}
|
||||||
|
lines.push(' --bl-shadow-sm: var(--ml-elevation-sm);');
|
||||||
|
lines.push(' --bl-shadow-md: var(--ml-elevation-md);');
|
||||||
|
lines.push(' --bl-shadow-lg: var(--ml-elevation-lg);');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
// Motion
|
// Motion
|
||||||
|
|||||||
@ -28,8 +28,5 @@
|
|||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,8 +26,5 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.0.0"
|
"zod": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,5 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@bytelyst/api-client": "workspace:*"
|
"@bytelyst/api-client": "workspace:*"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,5 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
]
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,8 +38,5 @@
|
|||||||
"@fastify/swagger": "^9.7.0",
|
"@fastify/swagger": "^9.7.0",
|
||||||
"@fastify/swagger-ui": "^5.2.5",
|
"@fastify/swagger-ui": "^5.2.5",
|
||||||
"fastify-metrics": "^10.6.0"
|
"fastify-metrics": "^10.6.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,8 +27,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,8 +36,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"zod": "^3.24.0"
|
"zod": "^3.24.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"typescript": "^5.7.0"
|
"typescript": "^5.7.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"typescript": "^5.7.0"
|
"typescript": "^5.7.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,8 +24,5 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.0.0"
|
"zod": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.12.0",
|
"@types/node": "^22.12.0",
|
||||||
"vitest": "^3.0.5"
|
"vitest": "^3.0.5"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,8 +30,5 @@
|
|||||||
"happy-dom": "^18.0.1",
|
"happy-dom": "^18.0.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,8 +59,5 @@
|
|||||||
"expo",
|
"expo",
|
||||||
"mobile"
|
"mobile"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"fake-indexeddb": "^6.0.0"
|
"fake-indexeddb": "^6.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,8 +26,5 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,8 +27,5 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@bytelyst/api-client": "workspace:*"
|
"@bytelyst/api-client": "workspace:*"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,5 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run --pool forks"
|
"test": "vitest run --pool forks"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,8 +30,5 @@
|
|||||||
"fastify": {
|
"fastify": {
|
||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,54 @@
|
|||||||
"types": "./dist/components/Button.d.ts",
|
"types": "./dist/components/Button.d.ts",
|
||||||
"import": "./dist/components/Button.js"
|
"import": "./dist/components/Button.js"
|
||||||
},
|
},
|
||||||
|
"./page-header": {
|
||||||
|
"types": "./dist/components/PageHeader.d.ts",
|
||||||
|
"import": "./dist/components/PageHeader.js"
|
||||||
|
},
|
||||||
|
"./section": {
|
||||||
|
"types": "./dist/components/Section.d.ts",
|
||||||
|
"import": "./dist/components/Section.js"
|
||||||
|
},
|
||||||
|
"./toolbar": {
|
||||||
|
"types": "./dist/components/Toolbar.d.ts",
|
||||||
|
"import": "./dist/components/Toolbar.js"
|
||||||
|
},
|
||||||
|
"./filter-bar": {
|
||||||
|
"types": "./dist/components/FilterBar.d.ts",
|
||||||
|
"import": "./dist/components/FilterBar.js"
|
||||||
|
},
|
||||||
|
"./form-section": {
|
||||||
|
"types": "./dist/components/FormSection.d.ts",
|
||||||
|
"import": "./dist/components/FormSection.js"
|
||||||
|
},
|
||||||
|
"./field-grid": {
|
||||||
|
"types": "./dist/components/FieldGrid.d.ts",
|
||||||
|
"import": "./dist/components/FieldGrid.js"
|
||||||
|
},
|
||||||
|
"./alert-banner": {
|
||||||
|
"types": "./dist/components/AlertBanner.d.ts",
|
||||||
|
"import": "./dist/components/AlertBanner.js"
|
||||||
|
},
|
||||||
|
"./skeleton": {
|
||||||
|
"types": "./dist/components/Skeleton.d.ts",
|
||||||
|
"import": "./dist/components/Skeleton.js"
|
||||||
|
},
|
||||||
|
"./entity-card": {
|
||||||
|
"types": "./dist/components/EntityCard.d.ts",
|
||||||
|
"import": "./dist/components/EntityCard.js"
|
||||||
|
},
|
||||||
|
"./metric-card": {
|
||||||
|
"types": "./dist/components/MetricCard.d.ts",
|
||||||
|
"import": "./dist/components/MetricCard.js"
|
||||||
|
},
|
||||||
|
"./action-menu": {
|
||||||
|
"types": "./dist/components/ActionMenu.d.ts",
|
||||||
|
"import": "./dist/components/ActionMenu.js"
|
||||||
|
},
|
||||||
|
"./drawer": {
|
||||||
|
"types": "./dist/components/Drawer.d.ts",
|
||||||
|
"import": "./dist/components/Drawer.js"
|
||||||
|
},
|
||||||
"./app-shell": {
|
"./app-shell": {
|
||||||
"types": "./dist/components/AppShell.d.ts",
|
"types": "./dist/components/AppShell.d.ts",
|
||||||
"import": "./dist/components/AppShell.js"
|
"import": "./dist/components/AppShell.js"
|
||||||
@ -52,6 +100,10 @@
|
|||||||
"types": "./dist/components/Input.d.ts",
|
"types": "./dist/components/Input.d.ts",
|
||||||
"import": "./dist/components/Input.js"
|
"import": "./dist/components/Input.js"
|
||||||
},
|
},
|
||||||
|
"./field": {
|
||||||
|
"types": "./dist/components/Field.d.ts",
|
||||||
|
"import": "./dist/components/Field.js"
|
||||||
|
},
|
||||||
"./textarea": {
|
"./textarea": {
|
||||||
"types": "./dist/components/Textarea.d.ts",
|
"types": "./dist/components/Textarea.d.ts",
|
||||||
"import": "./dist/components/Textarea.js"
|
"import": "./dist/components/Textarea.js"
|
||||||
|
|||||||
50
packages/ui/src/components/ActionMenu.tsx
Normal file
50
packages/ui/src/components/ActionMenu.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { MoreHorizontal } from 'lucide-react';
|
||||||
|
import { Button } from './Button.js';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from './DropdownMenu.js';
|
||||||
|
|
||||||
|
export interface ActionMenuItem {
|
||||||
|
id: string;
|
||||||
|
label: React.ReactNode;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
destructive?: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionMenuProps {
|
||||||
|
label?: string;
|
||||||
|
items: ActionMenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActionMenu({ label = 'Open actions', items }: ActionMenuProps) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button type="button" variant="ghost" size="sm" aria-label={label}>
|
||||||
|
<MoreHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{items.map(item => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={item.id}
|
||||||
|
disabled={item.disabled}
|
||||||
|
className={item.destructive ? 'text-[var(--bl-danger)]' : undefined}
|
||||||
|
onSelect={item.onSelect}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
{item.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
packages/ui/src/components/AlertBanner.tsx
Normal file
55
packages/ui/src/components/AlertBanner.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { AlertCircle, CheckCircle2, Info, TriangleAlert } from 'lucide-react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export type AlertBannerTone = 'info' | 'success' | 'warning' | 'error' | 'neutral';
|
||||||
|
|
||||||
|
export interface AlertBannerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
||||||
|
tone?: AlertBannerTone;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toneClass: Record<AlertBannerTone, string> = {
|
||||||
|
info: 'border-[var(--bl-info-border,var(--bl-border))] bg-[var(--bl-info-muted,var(--bl-surface-muted))] text-[var(--bl-info,var(--bl-accent))]',
|
||||||
|
success:
|
||||||
|
'border-[var(--bl-success-border,var(--bl-border))] bg-[var(--bl-success-muted,var(--bl-surface-muted))] text-[var(--bl-success)]',
|
||||||
|
warning:
|
||||||
|
'border-[var(--bl-warning-border,var(--bl-border))] bg-[var(--bl-warning-muted,var(--bl-surface-muted))] text-[var(--bl-warning)]',
|
||||||
|
error:
|
||||||
|
'border-[var(--bl-danger-border,var(--bl-border))] bg-[var(--bl-danger-muted,var(--bl-surface-muted))] text-[var(--bl-danger)]',
|
||||||
|
neutral: 'border-[var(--bl-border)] bg-[var(--bl-surface-muted)] text-[var(--bl-text-secondary)]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconByTone: Record<AlertBannerTone, React.ReactNode> = {
|
||||||
|
info: <Info className="h-4 w-4" aria-hidden="true" />,
|
||||||
|
success: <CheckCircle2 className="h-4 w-4" aria-hidden="true" />,
|
||||||
|
warning: <TriangleAlert className="h-4 w-4" aria-hidden="true" />,
|
||||||
|
error: <AlertCircle className="h-4 w-4" aria-hidden="true" />,
|
||||||
|
neutral: <Info className="h-4 w-4" aria-hidden="true" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AlertBanner({
|
||||||
|
tone = 'info',
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: AlertBannerProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('flex gap-3 rounded-xl border px-4 py-3 text-sm', toneClass[tone], className)}
|
||||||
|
role={tone === 'error' || tone === 'warning' ? 'alert' : 'status'}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 shrink-0">{icon ?? iconByTone[tone]}</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
{title && (
|
||||||
|
<div className="font-semibold leading-5 text-[var(--bl-text-primary)]">{title}</div>
|
||||||
|
)}
|
||||||
|
<div className="leading-6 text-[var(--bl-text-secondary)]">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -18,28 +18,28 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
const Comp = asChild ? Slot : 'button';
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
|
||||||
const baseStyles =
|
const baseStyles =
|
||||||
'inline-flex items-center justify-center font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50';
|
'inline-flex shrink-0 items-center justify-center whitespace-nowrap rounded-lg font-semibold tracking-normal transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bl-focus-ring,var(--bl-accent,#5A8CFF))] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bl-bg-canvas,#0b0f17)] disabled:pointer-events-none disabled:opacity-50';
|
||||||
|
|
||||||
const variants: Record<string, string> = {
|
const variants: Record<string, string> = {
|
||||||
primary:
|
primary:
|
||||||
'bg-[var(--bl-accent,#5A8CFF)] text-[var(--bl-accent-foreground,var(--bl-bg-canvas,#0b0f17))] hover:opacity-90',
|
'border border-transparent bg-[var(--bl-accent,#5A8CFF)] text-[var(--bl-accent-foreground,var(--bl-bg-canvas,#0b0f17))] shadow-sm shadow-black/10 hover:brightness-105 active:brightness-95',
|
||||||
secondary:
|
secondary:
|
||||||
'bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)] border border-[var(--bl-border,#2a2a4a)] hover:bg-[var(--bl-surface-muted,#252540)]',
|
'border border-[var(--bl-border,#2a2a4a)] bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)] shadow-sm shadow-black/5 hover:border-[var(--bl-border-strong,var(--bl-border,#2a2a4a))] hover:bg-[var(--bl-surface-highlight,var(--bl-surface-muted,#252540))]',
|
||||||
ghost:
|
ghost:
|
||||||
'text-[var(--bl-text-secondary,#a0a0b0)] hover:bg-[var(--bl-surface-muted,#252540)] hover:text-[var(--bl-text-primary,#fff)]',
|
'border border-transparent text-[var(--bl-text-secondary,#a0a0b0)] hover:bg-[var(--bl-surface-muted,#252540)] hover:text-[var(--bl-text-primary,#fff)]',
|
||||||
destructive:
|
destructive:
|
||||||
'bg-[var(--bl-danger)] text-[var(--bl-danger-foreground,var(--bl-bg-canvas,#0b0f17))] hover:opacity-90',
|
'border border-transparent bg-[var(--bl-danger)] text-[var(--bl-danger-foreground,#fff)] shadow-sm shadow-black/10 hover:brightness-105 active:brightness-95',
|
||||||
outline:
|
outline:
|
||||||
'border border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-muted,#252540)]',
|
'border border-[var(--bl-border,#2a2a4a)] bg-transparent text-[var(--bl-text-primary,#fff)] hover:border-[var(--bl-accent,#5A8CFF)] hover:bg-[var(--bl-accent-muted,var(--bl-surface-muted,#252540))]',
|
||||||
subtle:
|
subtle:
|
||||||
'bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-card,#1a1a2e)]',
|
'border border-transparent bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-highlight,var(--bl-surface-card,#1a1a2e))]',
|
||||||
link: 'h-auto rounded-none p-0 text-[var(--bl-accent,#5A8CFF)] underline-offset-4 hover:underline',
|
link: 'h-auto rounded-md border border-transparent p-0 text-[var(--bl-accent,#5A8CFF)] underline-offset-4 hover:underline',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizes: Record<string, string> = {
|
const sizes: Record<string, string> = {
|
||||||
sm: 'h-8 px-3 text-xs gap-1.5',
|
sm: 'h-8 px-3 text-xs gap-1.5',
|
||||||
md: 'h-10 px-4 text-sm gap-2',
|
md: 'h-10 px-4 text-sm gap-2',
|
||||||
lg: 'h-12 px-6 text-base gap-2.5',
|
lg: 'h-11 px-5 text-sm gap-2.5',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -9,8 +9,8 @@ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
|
|
||||||
const paddings: Record<string, string> = {
|
const paddings: Record<string, string> = {
|
||||||
none: '',
|
none: '',
|
||||||
sm: 'p-3',
|
sm: 'p-4',
|
||||||
md: 'p-4',
|
md: 'p-5',
|
||||||
lg: 'p-6',
|
lg: 'p-6',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,9 +23,11 @@ export function Card({
|
|||||||
...props
|
...props
|
||||||
}: CardProps) {
|
}: CardProps) {
|
||||||
const variants: Record<NonNullable<CardProps['variant']>, string> = {
|
const variants: Record<NonNullable<CardProps['variant']>, string> = {
|
||||||
default: 'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)]',
|
default:
|
||||||
|
'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)] shadow-sm shadow-black/[0.04]',
|
||||||
muted: 'bg-[var(--bl-surface-muted,#252540)] border-[var(--bl-border,#2a2a4a)]',
|
muted: 'bg-[var(--bl-surface-muted,#252540)] border-[var(--bl-border,#2a2a4a)]',
|
||||||
elevated: 'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] shadow-sm',
|
elevated:
|
||||||
|
'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] shadow-lg shadow-black/10',
|
||||||
outline: 'bg-transparent border-[var(--bl-border,#2a2a4a)]',
|
outline: 'bg-transparent border-[var(--bl-border,#2a2a4a)]',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,7 +36,8 @@ export function Card({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-xl border',
|
'rounded-xl border',
|
||||||
variants[variant],
|
variants[variant],
|
||||||
hover && 'transition-colors hover:border-[var(--bl-accent,#5A8CFF)]/40',
|
hover &&
|
||||||
|
'transition duration-150 hover:-translate-y-0.5 hover:border-[var(--bl-accent,#5A8CFF)] hover:shadow-lg hover:shadow-black/10',
|
||||||
paddings[padding],
|
paddings[padding],
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@ -49,7 +52,7 @@ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|||||||
|
|
||||||
export function CardHeader({ className, children, ...props }: CardHeaderProps) {
|
export function CardHeader({ className, children, ...props }: CardHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('mb-3', className)} {...props}>
|
<div className={clsx('mb-4 flex flex-col gap-1', className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -60,7 +63,10 @@ export type CardTitleProps = React.ComponentPropsWithoutRef<'h3'>;
|
|||||||
export function CardTitle({ className, children, ...props }: CardTitleProps) {
|
export function CardTitle({ className, children, ...props }: CardTitleProps) {
|
||||||
return (
|
return (
|
||||||
<h3
|
<h3
|
||||||
className={clsx('text-lg font-semibold text-[var(--bl-text-primary,#fff)]', className)}
|
className={clsx(
|
||||||
|
'm-0 text-base font-semibold leading-6 text-[var(--bl-text-primary,#fff)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -72,7 +78,10 @@ export type CardDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
|
|||||||
|
|
||||||
export function CardDescription({ className, children, ...props }: CardDescriptionProps) {
|
export function CardDescription({ className, children, ...props }: CardDescriptionProps) {
|
||||||
return (
|
return (
|
||||||
<p className={clsx('text-sm text-[var(--bl-text-secondary,#a0a0b0)]', className)} {...props}>
|
<p
|
||||||
|
className={clsx('m-0 text-sm leading-6 text-[var(--bl-text-secondary,#a0a0b0)]', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export type DataTableProps = React.TableHTMLAttributes<HTMLElement>;
|
|||||||
|
|
||||||
export function DataTable({ className, children, ...props }: DataTableProps) {
|
export function DataTable({ className, children, ...props }: DataTableProps) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full overflow-x-auto rounded-lg border border-[var(--bl-border)]">
|
<div className="w-full overflow-x-auto rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] shadow-sm shadow-black/[0.04]">
|
||||||
<table
|
<table
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full border-collapse text-left text-sm text-[var(--bl-text-primary)]',
|
'w-full border-collapse text-left text-sm text-[var(--bl-text-primary)]',
|
||||||
@ -22,7 +22,7 @@ export function DataTable({ className, children, ...props }: DataTableProps) {
|
|||||||
export type DataTableHeaderProps = React.HTMLAttributes<HTMLElement>;
|
export type DataTableHeaderProps = React.HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
export function DataTableHeader({ className, ...props }: DataTableHeaderProps) {
|
export function DataTableHeader({ className, ...props }: DataTableHeaderProps) {
|
||||||
return <thead className={clsx('bg-[var(--bl-surface-muted)]', className)} {...props} />;
|
return <thead className={clsx('bg-[var(--bl-surface-muted)]/80', className)} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataTableBodyProps = React.HTMLAttributes<HTMLElement>;
|
export type DataTableBodyProps = React.HTMLAttributes<HTMLElement>;
|
||||||
@ -36,7 +36,7 @@ export type DataTableRowProps = React.HTMLAttributes<HTMLElement>;
|
|||||||
export function DataTableRow({ className, ...props }: DataTableRowProps) {
|
export function DataTableRow({ className, ...props }: DataTableRowProps) {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
className={clsx('transition-colors hover:bg-[var(--bl-surface-muted)]', className)}
|
className={clsx('transition-colors hover:bg-[var(--bl-surface-muted)]/70', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -47,7 +47,10 @@ export type DataTableHeadProps = React.ThHTMLAttributes<HTMLElement>;
|
|||||||
export function DataTableHead({ className, ...props }: DataTableHeadProps) {
|
export function DataTableHead({ className, ...props }: DataTableHeadProps) {
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
className={clsx('px-3 py-2 text-xs font-medium text-[var(--bl-text-secondary)]', className)}
|
className={clsx(
|
||||||
|
'px-4 py-3 text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -56,5 +59,5 @@ export function DataTableHead({ className, ...props }: DataTableHeadProps) {
|
|||||||
export type DataTableCellProps = React.TdHTMLAttributes<HTMLElement>;
|
export type DataTableCellProps = React.TdHTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
export function DataTableCell({ className, ...props }: DataTableCellProps) {
|
export function DataTableCell({ className, ...props }: DataTableCellProps) {
|
||||||
return <td className={clsx('px-3 py-2 align-middle', className)} {...props} />;
|
return <td className={clsx('px-4 py-3 align-middle', className)} {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
56
packages/ui/src/components/Drawer.tsx
Normal file
56
packages/ui/src/components/Drawer.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { Button } from './Button.js';
|
||||||
|
|
||||||
|
export interface DrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
title: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
side?: 'right' | 'left';
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Drawer({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
side = 'right',
|
||||||
|
children,
|
||||||
|
}: DrawerProps) {
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 z-[9998] bg-black/50 backdrop-blur-sm" />
|
||||||
|
<Dialog.Content
|
||||||
|
className={clsx(
|
||||||
|
'fixed top-0 z-[9999] flex h-dvh w-full max-w-xl flex-col border-[var(--bl-border)] bg-[var(--bl-bg-elevated)] text-[var(--bl-text-primary)] shadow-xl focus:outline-none',
|
||||||
|
side === 'right' ? 'right-0 border-l' : 'left-0 border-r'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4 border-b border-[var(--bl-border)] p-5">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Dialog.Title className="m-0 text-lg font-semibold leading-7">{title}</Dialog.Title>
|
||||||
|
{description && (
|
||||||
|
<Dialog.Description className="mt-1 text-sm leading-6 text-[var(--bl-text-secondary)]">
|
||||||
|
{description}
|
||||||
|
</Dialog.Description>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<Button type="button" variant="ghost" size="sm" aria-label="Close drawer">
|
||||||
|
<X className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto p-5">{children}</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -23,16 +23,16 @@ export function EmptyState({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex flex-col items-center justify-center py-16 px-4 text-center',
|
'flex flex-col items-center justify-center rounded-xl border border-dashed border-[var(--bl-border)] bg-[var(--bl-surface-muted)]/35 px-6 py-14 text-center',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mb-4 text-[var(--bl-text-tertiary,#555)]">
|
<div className="mb-4 rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] p-3 text-[var(--bl-text-tertiary,#555)] shadow-sm shadow-black/[0.04]">
|
||||||
{icon ?? <Inbox className="h-12 w-12" />}
|
{icon ?? <Inbox className="h-12 w-12" />}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-[var(--bl-text-primary,#fff)]">{title}</h3>
|
<h3 className="m-0 text-base font-semibold text-[var(--bl-text-primary,#fff)]">{title}</h3>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="mt-2 max-w-sm text-sm text-[var(--bl-text-secondary,#a0a0b0)]">
|
<p className="mt-2 max-w-sm text-sm leading-6 text-[var(--bl-text-secondary,#a0a0b0)]">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
63
packages/ui/src/components/EntityCard.tsx
Normal file
63
packages/ui/src/components/EntityCard.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export interface EntityCardProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
|
||||||
|
title: React.ReactNode;
|
||||||
|
subtitle?: React.ReactNode;
|
||||||
|
eyebrow?: React.ReactNode;
|
||||||
|
status?: React.ReactNode;
|
||||||
|
metadata?: React.ReactNode;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
footer?: React.ReactNode;
|
||||||
|
selected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EntityCard({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
eyebrow,
|
||||||
|
status,
|
||||||
|
metadata,
|
||||||
|
actions,
|
||||||
|
footer,
|
||||||
|
selected,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: EntityCardProps) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={clsx(
|
||||||
|
'grid gap-4 rounded-xl border bg-[var(--bl-surface-card)] p-5 shadow-sm shadow-black/[0.03]',
|
||||||
|
selected
|
||||||
|
? 'border-[var(--bl-accent)] ring-2 ring-[var(--bl-focus-ring-muted)]'
|
||||||
|
: 'border-[var(--bl-border)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
{eyebrow && (
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary)]">
|
||||||
|
{eyebrow}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
|
<h3 className="m-0 min-w-0 text-base font-semibold leading-6 text-[var(--bl-text-primary)]">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="mt-1 text-sm leading-6 text-[var(--bl-text-secondary)]">{subtitle}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
|
||||||
|
</div>
|
||||||
|
{metadata && <div className="flex min-w-0 flex-wrap gap-2">{metadata}</div>}
|
||||||
|
{children}
|
||||||
|
{footer && <div className="border-t border-[var(--bl-border)] pt-3">{footer}</div>}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
packages/ui/src/components/Field.tsx
Normal file
91
packages/ui/src/components/Field.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { Label } from './Label.js';
|
||||||
|
|
||||||
|
export interface FieldProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
orientation?: 'vertical' | 'horizontal';
|
||||||
|
invalid?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Field({ orientation = 'vertical', invalid, className, ...props }: FieldProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-invalid={invalid ? 'true' : undefined}
|
||||||
|
data-orientation={orientation}
|
||||||
|
className={clsx(
|
||||||
|
'grid gap-2 text-[var(--bl-text-primary)]',
|
||||||
|
orientation === 'horizontal' && 'items-start sm:grid-cols-[minmax(11rem,16rem)_1fr]',
|
||||||
|
invalid && 'text-[var(--bl-danger)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
role="group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldGroupProps = React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export function FieldGroup({ className, ...props }: FieldGroupProps) {
|
||||||
|
return <div className={clsx('grid gap-5', className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldContentProps = React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export function FieldContent({ className, ...props }: FieldContentProps) {
|
||||||
|
return <div className={clsx('grid gap-1.5', className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldLabelProps extends React.ComponentPropsWithoutRef<typeof Label> {}
|
||||||
|
|
||||||
|
export function FieldLabel({ className, ...props }: FieldLabelProps) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={clsx(
|
||||||
|
'text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldTitleProps = React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export function FieldTitle({ className, ...props }: FieldTitleProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('text-sm font-semibold leading-5 text-[var(--bl-text-primary)]', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
|
||||||
|
|
||||||
|
export function FieldDescription({ className, ...props }: FieldDescriptionProps) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={clsx('m-0 text-sm leading-6 text-[var(--bl-text-secondary)]', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldErrorProps extends React.ComponentPropsWithoutRef<'p'> {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldError({ className, children, ...props }: FieldErrorProps) {
|
||||||
|
if (!children) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={clsx('m-0 text-sm font-medium leading-5 text-[var(--bl-danger)]', className)}
|
||||||
|
role="alert"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
packages/ui/src/components/FieldGrid.tsx
Normal file
16
packages/ui/src/components/FieldGrid.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export interface FieldGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
columns?: 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnClass: Record<NonNullable<FieldGridProps['columns']>, string> = {
|
||||||
|
1: 'grid-cols-1',
|
||||||
|
2: 'grid-cols-1 md:grid-cols-2',
|
||||||
|
3: 'grid-cols-1 md:grid-cols-2 xl:grid-cols-3',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FieldGrid({ columns = 2, className, ...props }: FieldGridProps) {
|
||||||
|
return <div className={clsx('grid min-w-0 gap-4', columnClass[columns], className)} {...props} />;
|
||||||
|
}
|
||||||
72
packages/ui/src/components/FilterBar.tsx
Normal file
72
packages/ui/src/components/FilterBar.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Search, X } from 'lucide-react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { Button } from './Button.js';
|
||||||
|
import { Input } from './Input.js';
|
||||||
|
|
||||||
|
export interface FilterBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
searchLabel?: string;
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
searchValue?: string;
|
||||||
|
onSearchChange?: (value: string) => void;
|
||||||
|
filters?: React.ReactNode;
|
||||||
|
chips?: React.ReactNode;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
onReset?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilterBar({
|
||||||
|
searchLabel = 'Search',
|
||||||
|
searchPlaceholder = 'Search...',
|
||||||
|
searchValue,
|
||||||
|
onSearchChange,
|
||||||
|
filters,
|
||||||
|
chips,
|
||||||
|
actions,
|
||||||
|
onReset,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: FilterBarProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'grid gap-3 rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] p-3 shadow-sm shadow-black/[0.03]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 flex-col gap-2 lg:flex-row lg:items-center">
|
||||||
|
{onSearchChange && (
|
||||||
|
<div className="relative min-w-0 flex-1">
|
||||||
|
<Search
|
||||||
|
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--bl-text-tertiary,var(--bl-text-secondary))]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
aria-label={searchLabel}
|
||||||
|
className="pl-9"
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
value={searchValue ?? ''}
|
||||||
|
onChange={event =>
|
||||||
|
onSearchChange((event.currentTarget as unknown as { value: string }).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{filters && <div className="flex min-w-0 flex-wrap items-center gap-2">{filters}</div>}
|
||||||
|
{(actions || onReset) && (
|
||||||
|
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
||||||
|
{onReset && (
|
||||||
|
<Button type="button" variant="subtle" size="sm" onClick={onReset}>
|
||||||
|
<X className="h-4 w-4" aria-hidden="true" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{chips && <div className="flex min-w-0 flex-wrap gap-2">{chips}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
packages/ui/src/components/FormSection.tsx
Normal file
44
packages/ui/src/components/FormSection.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { AlertBanner } from './AlertBanner.js';
|
||||||
|
|
||||||
|
export interface FormSectionProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
|
||||||
|
title: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
error?: React.ReactNode;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormSection({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
error,
|
||||||
|
actions,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: FormSectionProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={clsx(
|
||||||
|
'grid gap-4 rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] p-5 shadow-sm shadow-black/[0.03]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="m-0 text-base font-semibold leading-6 text-[var(--bl-text-primary)]">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-1 text-sm leading-6 text-[var(--bl-text-secondary)]">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{actions && <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>}
|
||||||
|
</div>
|
||||||
|
{error && <AlertBanner tone="error">{error}</AlertBanner>}
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -19,20 +19,20 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
const sizes: Record<NonNullable<InputProps['controlSize']>, string> = {
|
const sizes: Record<NonNullable<InputProps['controlSize']>, string> = {
|
||||||
sm: 'h-8 px-2.5 text-xs',
|
sm: 'h-8 px-2.5 text-xs',
|
||||||
md: 'h-10 px-3 text-sm',
|
md: 'h-10 px-3 text-sm',
|
||||||
lg: 'h-12 px-4 text-base',
|
lg: 'h-11 px-4 text-sm',
|
||||||
};
|
};
|
||||||
const variants: Record<NonNullable<InputProps['variant']>, string> = {
|
const variants: Record<NonNullable<InputProps['variant']>, string> = {
|
||||||
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]',
|
surface: 'bg-[var(--bl-input,var(--bl-surface-card,#1a1a2e))]',
|
||||||
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
||||||
ghost: 'bg-transparent',
|
ghost: 'bg-transparent',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="grid gap-1.5">
|
||||||
{label && (
|
{label && (
|
||||||
<label
|
<label
|
||||||
htmlFor={inputId}
|
htmlFor={inputId}
|
||||||
className="block text-sm font-medium text-[var(--bl-text-secondary,#a0a0b0)]"
|
className="block text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary,#a0a0b0)]"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
@ -41,12 +41,13 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={inputId}
|
id={inputId}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full rounded-md border outline-none transition-colors',
|
'w-full rounded-lg border shadow-sm shadow-black/[0.03] outline-none transition duration-150',
|
||||||
variants[variant],
|
variants[variant],
|
||||||
sizes[controlSize],
|
sizes[controlSize],
|
||||||
'text-[var(--bl-text-primary,#fff)]',
|
'text-[var(--bl-text-primary,#fff)]',
|
||||||
'placeholder:text-[var(--bl-text-tertiary,#555)]',
|
'placeholder:text-[var(--bl-text-tertiary,#555)]',
|
||||||
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0',
|
'disabled:cursor-not-allowed disabled:opacity-60',
|
||||||
|
'focus:border-[var(--bl-focus-ring,var(--bl-accent,#5A8CFF))] focus:ring-2 focus:ring-[var(--bl-focus-ring-muted,var(--bl-accent-muted,rgba(90,140,255,0.2)))] focus:ring-offset-0',
|
||||||
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
48
packages/ui/src/components/MetricCard.tsx
Normal file
48
packages/ui/src/components/MetricCard.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export interface MetricCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
label: React.ReactNode;
|
||||||
|
value: React.ReactNode;
|
||||||
|
helper?: React.ReactNode;
|
||||||
|
trend?: React.ReactNode;
|
||||||
|
tone?: 'neutral' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
const toneClass: Record<NonNullable<MetricCardProps['tone']>, string> = {
|
||||||
|
neutral: 'text-[var(--bl-text-secondary)]',
|
||||||
|
success: 'text-[var(--bl-success)]',
|
||||||
|
warning: 'text-[var(--bl-warning)]',
|
||||||
|
danger: 'text-[var(--bl-danger)]',
|
||||||
|
info: 'text-[var(--bl-info,var(--bl-accent))]',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MetricCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
helper,
|
||||||
|
trend,
|
||||||
|
tone = 'neutral',
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: MetricCardProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] p-4 shadow-sm shadow-black/[0.03]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary)]">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-2xl font-semibold leading-8 text-[var(--bl-text-primary)]">
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
{(helper || trend) && (
|
||||||
|
<div className={clsx('mt-2 text-sm leading-5', toneClass[tone])}>{trend ?? helper}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
476
packages/ui/src/components/OperationalPreview.stories.tsx
Normal file
476
packages/ui/src/components/OperationalPreview.stories.tsx
Normal file
@ -0,0 +1,476 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Activity, Copy, MoreHorizontal, Plus, RefreshCw, ShieldCheck, Trash2 } from 'lucide-react';
|
||||||
|
import { ActionMenu } from './ActionMenu.js';
|
||||||
|
import { AlertBanner } from './AlertBanner.js';
|
||||||
|
import { Badge } from './Badge.js';
|
||||||
|
import { Button } from './Button.js';
|
||||||
|
import {
|
||||||
|
DataTable,
|
||||||
|
DataTableBody,
|
||||||
|
DataTableCell,
|
||||||
|
DataTableHead,
|
||||||
|
DataTableHeader,
|
||||||
|
DataTableRow,
|
||||||
|
} from './DataTable.js';
|
||||||
|
import { EmptyState } from './EmptyState.js';
|
||||||
|
import { EntityCard } from './EntityCard.js';
|
||||||
|
import { FieldGrid } from './FieldGrid.js';
|
||||||
|
import { FilterBar } from './FilterBar.js';
|
||||||
|
import { FormSection } from './FormSection.js';
|
||||||
|
import { Input } from './Input.js';
|
||||||
|
import { MetricCard } from './MetricCard.js';
|
||||||
|
import { PageHeader } from './PageHeader.js';
|
||||||
|
import { Panel, PanelBody, PanelDescription, PanelHeader, PanelTitle } from './Panel.js';
|
||||||
|
import { Section } from './Section.js';
|
||||||
|
import { Select } from './Select.js';
|
||||||
|
import { Skeleton, TableSkeleton } from './Skeleton.js';
|
||||||
|
import { StatusBadge } from './StatusBadge.js';
|
||||||
|
import { Textarea } from './Textarea.js';
|
||||||
|
import { Toolbar } from './Toolbar.js';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Examples/Operational Preview',
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj;
|
||||||
|
|
||||||
|
export const LaunchReadyConsole: Story = {
|
||||||
|
render: () => (
|
||||||
|
<main className="min-h-screen bg-[var(--bl-bg-canvas)] p-6 text-[var(--bl-text-primary)]">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6">
|
||||||
|
<PageHeader
|
||||||
|
eyebrow="ByteLyst platform preview"
|
||||||
|
title="Operational trading console"
|
||||||
|
description="A realistic composition for validating shared tokens, primitives, density, states, and responsive behavior before product teams adopt the system."
|
||||||
|
metadata={
|
||||||
|
<>
|
||||||
|
<Badge variant="success" dot>
|
||||||
|
Local packages
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="info" dot>
|
||||||
|
Strict audit clean
|
||||||
|
</Badge>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button variant="secondary" size="sm">
|
||||||
|
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<Button size="sm">
|
||||||
|
<Plus className="h-4 w-4" aria-hidden="true" />
|
||||||
|
New setup
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AlertBanner tone="warning" title="Production bar">
|
||||||
|
Every route should look deliberate in populated, loading, empty, error, disabled, and
|
||||||
|
destructive-action states.
|
||||||
|
</AlertBanner>
|
||||||
|
|
||||||
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<MetricCard label="Portfolio value" value="$128,420" trend="+4.8% today" tone="success" />
|
||||||
|
<MetricCard label="Open setups" value="18" trend="3 need review" tone="warning" />
|
||||||
|
<MetricCard
|
||||||
|
label="Execution state"
|
||||||
|
value="Paper"
|
||||||
|
trend="Live trading locked"
|
||||||
|
tone="info"
|
||||||
|
/>
|
||||||
|
<MetricCard label="Risk budget" value="62%" trend="Within limits" tone="success" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<FilterBar
|
||||||
|
searchValue="BTC"
|
||||||
|
onSearchChange={() => undefined}
|
||||||
|
searchPlaceholder="Search symbol, setup, or owner..."
|
||||||
|
chips={
|
||||||
|
<>
|
||||||
|
<Badge variant="accent">Crypto</Badge>
|
||||||
|
<Badge variant="neutral">Active only</Badge>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
filters={
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
aria-label="Status"
|
||||||
|
value="active"
|
||||||
|
options={[
|
||||||
|
{ value: 'active', label: 'Active' },
|
||||||
|
{ value: 'waiting', label: 'Waiting' },
|
||||||
|
{ value: 'closed', label: 'Closed' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
aria-label="Strategy"
|
||||||
|
value="all"
|
||||||
|
options={[
|
||||||
|
{ value: 'all', label: 'All strategies' },
|
||||||
|
{ value: 'simple', label: 'Simple auto' },
|
||||||
|
{ value: 'manual', label: 'Manual' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<Button variant="subtle" size="sm">
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
onReset={() => undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid gap-6 xl:grid-cols-[1fr_360px]">
|
||||||
|
<Section
|
||||||
|
title="Active setups"
|
||||||
|
description="Dense data surfaces should scan cleanly and recover gracefully from loading, empty, and error states."
|
||||||
|
actions={
|
||||||
|
<Toolbar>
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
All
|
||||||
|
</Button>
|
||||||
|
<Button variant="subtle" size="sm">
|
||||||
|
Needs attention
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
Closed
|
||||||
|
</Button>
|
||||||
|
</Toolbar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Panel>
|
||||||
|
<PanelHeader>
|
||||||
|
<div>
|
||||||
|
<PanelTitle>Runtime order state</PanelTitle>
|
||||||
|
<PanelDescription>Responsive table with compact row actions.</PanelDescription>
|
||||||
|
</div>
|
||||||
|
<StatusBadge tone="success" dot>
|
||||||
|
Synced
|
||||||
|
</StatusBadge>
|
||||||
|
</PanelHeader>
|
||||||
|
<PanelBody>
|
||||||
|
<DataTable>
|
||||||
|
<DataTableHeader>
|
||||||
|
<DataTableRow>
|
||||||
|
<DataTableHead>Symbol</DataTableHead>
|
||||||
|
<DataTableHead>State</DataTableHead>
|
||||||
|
<DataTableHead>Budget</DataTableHead>
|
||||||
|
<DataTableHead>P/L</DataTableHead>
|
||||||
|
<DataTableHead>Action</DataTableHead>
|
||||||
|
</DataTableRow>
|
||||||
|
</DataTableHeader>
|
||||||
|
<DataTableBody>
|
||||||
|
{[
|
||||||
|
['BTC/USD', 'Waiting', '$120.00', '+2.4%'],
|
||||||
|
['ETH/USD', 'Armed', '$90.00', '-0.7%'],
|
||||||
|
['AAPL', 'Review', '$250.00', '+0.8%'],
|
||||||
|
].map(([symbol, state, budget, pnl]) => (
|
||||||
|
<DataTableRow key={symbol}>
|
||||||
|
<DataTableCell className="font-semibold">{symbol}</DataTableCell>
|
||||||
|
<DataTableCell>
|
||||||
|
<StatusBadge tone={state === 'Review' ? 'warning' : 'info'} dot>
|
||||||
|
{state}
|
||||||
|
</StatusBadge>
|
||||||
|
</DataTableCell>
|
||||||
|
<DataTableCell>{budget}</DataTableCell>
|
||||||
|
<DataTableCell>{pnl}</DataTableCell>
|
||||||
|
<DataTableCell>
|
||||||
|
<ActionMenu
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
id: 'copy',
|
||||||
|
label: 'Clone setup',
|
||||||
|
icon: <Copy className="h-4 w-4" />,
|
||||||
|
onSelect: () => undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'archive',
|
||||||
|
label: 'Archive',
|
||||||
|
icon: <MoreHorizontal className="h-4 w-4" />,
|
||||||
|
onSelect: () => undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
icon: <Trash2 className="h-4 w-4" />,
|
||||||
|
destructive: true,
|
||||||
|
onSelect: () => undefined,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataTableCell>
|
||||||
|
</DataTableRow>
|
||||||
|
))}
|
||||||
|
</DataTableBody>
|
||||||
|
</DataTable>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<EntityCard
|
||||||
|
eyebrow="Strategy"
|
||||||
|
title="Breakout confirmation"
|
||||||
|
subtitle="BTC/USD paper setup with managed profit exit."
|
||||||
|
status={<StatusBadge tone="success">Ready</StatusBadge>}
|
||||||
|
metadata={
|
||||||
|
<>
|
||||||
|
<Badge>Budget $120</Badge>
|
||||||
|
<Badge variant="info">Short term</Badge>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<Button variant="secondary" size="sm">
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<span className="text-sm text-[var(--bl-text-secondary)]">
|
||||||
|
Updated 4 minutes ago
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<EmptyState
|
||||||
|
title="No critical alerts"
|
||||||
|
description="This compact empty state keeps low-volume surfaces feeling complete."
|
||||||
|
icon={<ShieldCheck className="h-10 w-10" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<aside className="grid gap-6">
|
||||||
|
<FormSection
|
||||||
|
title="Plan builder"
|
||||||
|
description="Forms use consistent spacing, labels, hints, and review sections."
|
||||||
|
>
|
||||||
|
<FieldGrid columns={1}>
|
||||||
|
<Input label="Symbol" value="BTC/USD" readOnly />
|
||||||
|
<Select
|
||||||
|
label="Setup type"
|
||||||
|
value="dip"
|
||||||
|
options={[
|
||||||
|
{ value: 'dip', label: 'Buy the dip' },
|
||||||
|
{ value: 'breakout', label: 'Breakout' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Textarea label="Notes" placeholder="Optional context" />
|
||||||
|
</FieldGrid>
|
||||||
|
<Button>Save setup</Button>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<PanelHeader>
|
||||||
|
<div>
|
||||||
|
<PanelTitle>Loading shape</PanelTitle>
|
||||||
|
<PanelDescription>Skeletons should match final layout.</PanelDescription>
|
||||||
|
</div>
|
||||||
|
</PanelHeader>
|
||||||
|
<PanelBody>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Skeleton shape="circle" />
|
||||||
|
<div className="grid flex-1 gap-2">
|
||||||
|
<Skeleton shape="text" />
|
||||||
|
<Skeleton shape="text" className="w-2/3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TableSkeleton rows={3} />
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<PanelHeader>
|
||||||
|
<div>
|
||||||
|
<PanelTitle>System health</PanelTitle>
|
||||||
|
<PanelDescription>State should be visible without reading logs.</PanelDescription>
|
||||||
|
</div>
|
||||||
|
<Activity className="h-5 w-5 text-[var(--bl-success)]" aria-hidden="true" />
|
||||||
|
</PanelHeader>
|
||||||
|
<PanelBody>
|
||||||
|
<AlertBanner tone="success">
|
||||||
|
All shared primitives render from common platform.
|
||||||
|
</AlertBanner>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LaunchStateMatrix: Story = {
|
||||||
|
render: () => (
|
||||||
|
<main className="min-h-screen bg-[var(--bl-bg-canvas)] p-6 text-[var(--bl-text-primary)]">
|
||||||
|
<div className="mx-auto grid max-w-6xl gap-6">
|
||||||
|
<PageHeader
|
||||||
|
eyebrow="Launch readiness"
|
||||||
|
title="State matrix"
|
||||||
|
description="Shared product states for checking loading, empty, warning, error, disabled, confirmation, and dense data behavior before a route ships."
|
||||||
|
metadata={
|
||||||
|
<>
|
||||||
|
<Badge variant="info" dot>
|
||||||
|
Reusable
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="success" dot>
|
||||||
|
Accessible states
|
||||||
|
</Badge>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<Panel>
|
||||||
|
<PanelHeader>
|
||||||
|
<div>
|
||||||
|
<PanelTitle>Loading</PanelTitle>
|
||||||
|
<PanelDescription>Skeletons match the final rhythm and density.</PanelDescription>
|
||||||
|
</div>
|
||||||
|
<StatusBadge tone="info" dot>
|
||||||
|
Fetching
|
||||||
|
</StatusBadge>
|
||||||
|
</PanelHeader>
|
||||||
|
<PanelBody>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Skeleton shape="circle" />
|
||||||
|
<div className="grid flex-1 gap-2">
|
||||||
|
<Skeleton shape="text" className="w-2/3" />
|
||||||
|
<Skeleton shape="text" className="w-1/2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TableSkeleton rows={4} />
|
||||||
|
</div>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<PanelHeader>
|
||||||
|
<div>
|
||||||
|
<PanelTitle>Empty</PanelTitle>
|
||||||
|
<PanelDescription>Empty states provide a clear next action.</PanelDescription>
|
||||||
|
</div>
|
||||||
|
</PanelHeader>
|
||||||
|
<PanelBody>
|
||||||
|
<EmptyState
|
||||||
|
title="No setups need review"
|
||||||
|
description="When work is complete, the surface should still feel intentional."
|
||||||
|
icon={<ShieldCheck className="h-10 w-10" />}
|
||||||
|
action={
|
||||||
|
<Button size="sm" variant="secondary">
|
||||||
|
Create setup
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<PanelHeader>
|
||||||
|
<div>
|
||||||
|
<PanelTitle>Alert stack</PanelTitle>
|
||||||
|
<PanelDescription>Warnings and errors use consistent semantics.</PanelDescription>
|
||||||
|
</div>
|
||||||
|
</PanelHeader>
|
||||||
|
<PanelBody>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<AlertBanner tone="warning" title="Data freshness degraded">
|
||||||
|
Market data is delayed. Show the user what changed and whether action is safe.
|
||||||
|
</AlertBanner>
|
||||||
|
<AlertBanner tone="error" title="Execution blocked">
|
||||||
|
Destructive or risky actions require a clear reason and recovery path.
|
||||||
|
</AlertBanner>
|
||||||
|
</div>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<PanelHeader>
|
||||||
|
<div>
|
||||||
|
<PanelTitle>Forms and disabled states</PanelTitle>
|
||||||
|
<PanelDescription>Controls remain readable when unavailable.</PanelDescription>
|
||||||
|
</div>
|
||||||
|
<Badge variant="neutral">Read-only</Badge>
|
||||||
|
</PanelHeader>
|
||||||
|
<PanelBody>
|
||||||
|
<FormSection
|
||||||
|
title="Risk limits"
|
||||||
|
description="Disabled controls explain state nearby."
|
||||||
|
>
|
||||||
|
<FieldGrid columns={2}>
|
||||||
|
<Input label="Max daily loss" value="$250" readOnly />
|
||||||
|
<Select
|
||||||
|
label="Execution mode"
|
||||||
|
value="paper"
|
||||||
|
disabled
|
||||||
|
options={[
|
||||||
|
{ value: 'paper', label: 'Paper trading' },
|
||||||
|
{ value: 'live', label: 'Live trading' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FieldGrid>
|
||||||
|
<AlertBanner tone="info">
|
||||||
|
Live execution is locked until admin approval is complete.
|
||||||
|
</AlertBanner>
|
||||||
|
</FormSection>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel className="lg:col-span-2">
|
||||||
|
<PanelHeader>
|
||||||
|
<div>
|
||||||
|
<PanelTitle>Destructive confirmation context</PanelTitle>
|
||||||
|
<PanelDescription>
|
||||||
|
Danger actions should be visually distinct and isolated.
|
||||||
|
</PanelDescription>
|
||||||
|
</div>
|
||||||
|
<Button variant="destructive" size="sm">
|
||||||
|
<Trash2 className="h-4 w-4" aria-hidden="true" />
|
||||||
|
Delete setup
|
||||||
|
</Button>
|
||||||
|
</PanelHeader>
|
||||||
|
<PanelBody>
|
||||||
|
<DataTable>
|
||||||
|
<DataTableHeader>
|
||||||
|
<DataTableRow>
|
||||||
|
<DataTableHead>Resource</DataTableHead>
|
||||||
|
<DataTableHead>State</DataTableHead>
|
||||||
|
<DataTableHead>Owner</DataTableHead>
|
||||||
|
<DataTableHead>Impact</DataTableHead>
|
||||||
|
</DataTableRow>
|
||||||
|
</DataTableHeader>
|
||||||
|
<DataTableBody>
|
||||||
|
<DataTableRow>
|
||||||
|
<DataTableCell className="font-semibold">BTC managed setup</DataTableCell>
|
||||||
|
<DataTableCell>
|
||||||
|
<StatusBadge tone="warning" dot>
|
||||||
|
Armed
|
||||||
|
</StatusBadge>
|
||||||
|
</DataTableCell>
|
||||||
|
<DataTableCell>Trading operations</DataTableCell>
|
||||||
|
<DataTableCell>Open automation will be cancelled</DataTableCell>
|
||||||
|
</DataTableRow>
|
||||||
|
<DataTableRow>
|
||||||
|
<DataTableCell className="font-semibold">Audit trail</DataTableCell>
|
||||||
|
<DataTableCell>
|
||||||
|
<StatusBadge tone="success" dot>
|
||||||
|
Retained
|
||||||
|
</StatusBadge>
|
||||||
|
</DataTableCell>
|
||||||
|
<DataTableCell>Platform</DataTableCell>
|
||||||
|
<DataTableCell>Historical events remain available</DataTableCell>
|
||||||
|
</DataTableRow>
|
||||||
|
</DataTableBody>
|
||||||
|
</DataTable>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
),
|
||||||
|
};
|
||||||
57
packages/ui/src/components/PageHeader.tsx
Normal file
57
packages/ui/src/components/PageHeader.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export interface PageHeaderProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
|
||||||
|
eyebrow?: React.ReactNode;
|
||||||
|
title: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
metadata?: React.ReactNode;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageHeader({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actions,
|
||||||
|
metadata,
|
||||||
|
compact,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PageHeaderProps) {
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={clsx(
|
||||||
|
'flex min-w-0 flex-col gap-4 border-b border-[var(--bl-border)]',
|
||||||
|
compact ? 'pb-4' : 'pb-6',
|
||||||
|
'sm:flex-row sm:items-end sm:justify-between',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
{eyebrow && (
|
||||||
|
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary)]">
|
||||||
|
{eyebrow}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h1
|
||||||
|
className={clsx(
|
||||||
|
'm-0 text-[var(--bl-text-primary)]',
|
||||||
|
compact ? 'text-2xl font-semibold leading-8' : 'text-3xl font-semibold leading-10'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-[var(--bl-text-secondary)]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{metadata && <div className="mt-3 flex flex-wrap gap-2">{metadata}</div>}
|
||||||
|
</div>
|
||||||
|
{actions && <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ export interface PanelProps extends React.HTMLAttributes<HTMLElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const panelPadding: Record<NonNullable<PanelProps['density']>, string> = {
|
const panelPadding: Record<NonNullable<PanelProps['density']>, string> = {
|
||||||
compact: 'p-3',
|
compact: 'p-4',
|
||||||
normal: 'p-5',
|
normal: 'p-5',
|
||||||
spacious: 'p-6',
|
spacious: 'p-6',
|
||||||
};
|
};
|
||||||
@ -22,7 +22,7 @@ export function Panel({
|
|||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-lg border bg-[var(--bl-surface-card)] border-[var(--bl-border)] shadow-sm',
|
'rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] shadow-sm shadow-black/[0.04]',
|
||||||
panelPadding[density],
|
panelPadding[density],
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@ -37,7 +37,13 @@ export type PanelHeaderProps = React.HTMLAttributes<HTMLDivElement>;
|
|||||||
|
|
||||||
export function PanelHeader({ className, children, ...props }: PanelHeaderProps) {
|
export function PanelHeader({ className, children, ...props }: PanelHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex items-center justify-between gap-3', className)} {...props}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -47,7 +53,7 @@ export type PanelBodyProps = React.HTMLAttributes<HTMLDivElement>;
|
|||||||
|
|
||||||
export function PanelBody({ className, children, ...props }: PanelBodyProps) {
|
export function PanelBody({ className, children, ...props }: PanelBodyProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('grid gap-3', className)} {...props}>
|
<div className={clsx('grid gap-4', className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -58,7 +64,10 @@ export type PanelTitleProps = React.ComponentPropsWithoutRef<'h2'>;
|
|||||||
export function PanelTitle({ className, children, ...props }: PanelTitleProps) {
|
export function PanelTitle({ className, children, ...props }: PanelTitleProps) {
|
||||||
return (
|
return (
|
||||||
<h2
|
<h2
|
||||||
className={clsx('m-0 text-base font-semibold text-[var(--bl-text-primary)]', className)}
|
className={clsx(
|
||||||
|
'm-0 text-base font-semibold leading-6 text-[var(--bl-text-primary)]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -70,7 +79,10 @@ export type PanelDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
|
|||||||
|
|
||||||
export function PanelDescription({ className, children, ...props }: PanelDescriptionProps) {
|
export function PanelDescription({ className, children, ...props }: PanelDescriptionProps) {
|
||||||
return (
|
return (
|
||||||
<p className={clsx('m-0 text-sm text-[var(--bl-text-secondary)]', className)} {...props}>
|
<p
|
||||||
|
className={clsx('m-0 text-sm leading-6 text-[var(--bl-text-secondary)]', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
48
packages/ui/src/components/Section.tsx
Normal file
48
packages/ui/src/components/Section.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export interface SectionProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
|
||||||
|
title?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
density?: 'compact' | 'normal' | 'spacious';
|
||||||
|
}
|
||||||
|
|
||||||
|
const densityClass: Record<NonNullable<SectionProps['density']>, string> = {
|
||||||
|
compact: 'gap-3',
|
||||||
|
normal: 'gap-4',
|
||||||
|
spacious: 'gap-6',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Section({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actions,
|
||||||
|
density = 'normal',
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SectionProps) {
|
||||||
|
return (
|
||||||
|
<section className={clsx('grid min-w-0', densityClass[density], className)} {...props}>
|
||||||
|
{(title || description || actions) && (
|
||||||
|
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div className="min-w-0">
|
||||||
|
{title && (
|
||||||
|
<h2 className="m-0 text-base font-semibold leading-6 text-[var(--bl-text-primary)]">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<p className="mt-1 max-w-3xl text-sm leading-6 text-[var(--bl-text-secondary)]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{actions && <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -31,20 +31,20 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|||||||
const sizes: Record<NonNullable<SelectProps['controlSize']>, string> = {
|
const sizes: Record<NonNullable<SelectProps['controlSize']>, string> = {
|
||||||
sm: 'h-8 px-2.5 pr-8 text-xs',
|
sm: 'h-8 px-2.5 pr-8 text-xs',
|
||||||
md: 'h-10 px-3 pr-8 text-sm',
|
md: 'h-10 px-3 pr-8 text-sm',
|
||||||
lg: 'h-12 px-4 pr-9 text-base',
|
lg: 'h-11 px-4 pr-9 text-sm',
|
||||||
};
|
};
|
||||||
const variants: Record<NonNullable<SelectProps['variant']>, string> = {
|
const variants: Record<NonNullable<SelectProps['variant']>, string> = {
|
||||||
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]',
|
surface: 'bg-[var(--bl-input,var(--bl-surface-card,#1a1a2e))]',
|
||||||
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
||||||
ghost: 'bg-transparent',
|
ghost: 'bg-transparent',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="grid gap-1.5">
|
||||||
{label && (
|
{label && (
|
||||||
<label
|
<label
|
||||||
htmlFor={selectId}
|
htmlFor={selectId}
|
||||||
className="block text-sm font-medium text-[var(--bl-text-secondary,#a0a0b0)]"
|
className="block text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary,#a0a0b0)]"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
@ -54,11 +54,12 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={selectId}
|
id={selectId}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full appearance-none rounded-md border outline-none transition-colors',
|
'w-full appearance-none rounded-lg border shadow-sm shadow-black/[0.03] outline-none transition duration-150',
|
||||||
variants[variant],
|
variants[variant],
|
||||||
sizes[controlSize],
|
sizes[controlSize],
|
||||||
'text-[var(--bl-text-primary,#fff)]',
|
'text-[var(--bl-text-primary,#fff)]',
|
||||||
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0',
|
'disabled:cursor-not-allowed disabled:opacity-60',
|
||||||
|
'focus:border-[var(--bl-focus-ring,var(--bl-accent,#5A8CFF))] focus:ring-2 focus:ring-[var(--bl-focus-ring-muted,var(--bl-accent-muted,rgba(90,140,255,0.2)))] focus:ring-offset-0',
|
||||||
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@ -78,7 +79,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--bl-text-tertiary,#555)]"
|
className="pointer-events-none absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--bl-text-tertiary,#555)]"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
32
packages/ui/src/components/Skeleton.tsx
Normal file
32
packages/ui/src/components/Skeleton.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
shape?: 'block' | 'text' | 'circle';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Skeleton({ shape = 'block', className, ...props }: SkeletonProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className={clsx(
|
||||||
|
'animate-pulse bg-[var(--bl-surface-muted)]',
|
||||||
|
shape === 'text' && 'h-4 rounded-full',
|
||||||
|
shape === 'block' && 'min-h-20 rounded-xl',
|
||||||
|
shape === 'circle' && 'h-10 w-10 rounded-full',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{Array.from({ length: rows }).map((_, index) => (
|
||||||
|
<Skeleton key={index} className="h-12 min-h-0" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -17,17 +17,19 @@ export function StatCard({ label, value, trend, trendValue, icon, className }: S
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-xl border p-5',
|
'rounded-xl border p-5 shadow-sm shadow-black/[0.04]',
|
||||||
'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)]',
|
'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-[var(--bl-text-secondary,#a0a0b0)] mb-1">
|
<p className="mb-1 text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary,#a0a0b0)]">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-2xl font-bold text-[var(--bl-text-primary,#fff)]">{value}</p>
|
<p className="text-2xl font-semibold tracking-tight text-[var(--bl-text-primary,#fff)]">
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{icon && (
|
{icon && (
|
||||||
<div className="rounded-lg p-2 bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-secondary,#a0a0b0)]">
|
<div className="rounded-lg p-2 bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-secondary,#a0a0b0)]">
|
||||||
|
|||||||
@ -28,11 +28,11 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="grid gap-1.5">
|
||||||
{label && (
|
{label && (
|
||||||
<label
|
<label
|
||||||
htmlFor={textareaId}
|
htmlFor={textareaId}
|
||||||
className="block text-sm font-medium text-[var(--bl-text-secondary,#a0a0b0)]"
|
className="block text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary,#a0a0b0)]"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
@ -41,12 +41,13 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={textareaId}
|
id={textareaId}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full resize-y rounded-md border outline-none transition-colors',
|
'w-full resize-y rounded-lg border shadow-sm shadow-black/[0.03] outline-none transition duration-150',
|
||||||
variants[variant],
|
variants[variant],
|
||||||
sizes[controlSize],
|
sizes[controlSize],
|
||||||
'text-[var(--bl-text-primary,#fff)]',
|
'text-[var(--bl-text-primary,#fff)]',
|
||||||
'placeholder:text-[var(--bl-text-tertiary,#555)]',
|
'placeholder:text-[var(--bl-text-tertiary,#555)]',
|
||||||
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0',
|
'disabled:cursor-not-allowed disabled:opacity-60',
|
||||||
|
'focus:border-[var(--bl-focus-ring,var(--bl-accent,#5A8CFF))] focus:ring-2 focus:ring-[var(--bl-focus-ring-muted,var(--bl-accent-muted,rgba(90,140,255,0.2)))] focus:ring-offset-0',
|
||||||
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
25
packages/ui/src/components/Toolbar.tsx
Normal file
25
packages/ui/src/components/Toolbar.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
export interface ToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
align?: 'start' | 'between' | 'end';
|
||||||
|
}
|
||||||
|
|
||||||
|
const alignClass: Record<NonNullable<ToolbarProps['align']>, string> = {
|
||||||
|
start: 'justify-start',
|
||||||
|
between: 'justify-between',
|
||||||
|
end: 'justify-end',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Toolbar({ align = 'start', className, ...props }: ToolbarProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex min-w-0 flex-wrap items-center gap-2 rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] p-2 shadow-sm shadow-black/[0.03]',
|
||||||
|
alignClass[align],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,20 @@
|
|||||||
export { Button, type ButtonProps } from './components/Button.js';
|
export { Button, type ButtonProps } from './components/Button.js';
|
||||||
|
export { PageHeader, type PageHeaderProps } from './components/PageHeader.js';
|
||||||
|
export { Section, type SectionProps } from './components/Section.js';
|
||||||
|
export { Toolbar, type ToolbarProps } from './components/Toolbar.js';
|
||||||
|
export { FilterBar, type FilterBarProps } from './components/FilterBar.js';
|
||||||
|
export { FormSection, type FormSectionProps } from './components/FormSection.js';
|
||||||
|
export { FieldGrid, type FieldGridProps } from './components/FieldGrid.js';
|
||||||
|
export {
|
||||||
|
AlertBanner,
|
||||||
|
type AlertBannerProps,
|
||||||
|
type AlertBannerTone,
|
||||||
|
} from './components/AlertBanner.js';
|
||||||
|
export { Skeleton, TableSkeleton, type SkeletonProps } from './components/Skeleton.js';
|
||||||
|
export { EntityCard, type EntityCardProps } from './components/EntityCard.js';
|
||||||
|
export { MetricCard, type MetricCardProps } from './components/MetricCard.js';
|
||||||
|
export { ActionMenu, type ActionMenuItem, type ActionMenuProps } from './components/ActionMenu.js';
|
||||||
|
export { Drawer, type DrawerProps } from './components/Drawer.js';
|
||||||
export {
|
export {
|
||||||
AppShell,
|
AppShell,
|
||||||
AppShellMain,
|
AppShellMain,
|
||||||
@ -39,6 +55,22 @@ export {
|
|||||||
} from './components/StatusBadge.js';
|
} from './components/StatusBadge.js';
|
||||||
export { EmptyState, type EmptyStateProps } from './components/EmptyState.js';
|
export { EmptyState, type EmptyStateProps } from './components/EmptyState.js';
|
||||||
export { Input, type InputProps } from './components/Input.js';
|
export { Input, type InputProps } from './components/Input.js';
|
||||||
|
export {
|
||||||
|
Field,
|
||||||
|
FieldContent,
|
||||||
|
FieldDescription,
|
||||||
|
FieldError,
|
||||||
|
FieldGroup,
|
||||||
|
FieldLabel,
|
||||||
|
FieldTitle,
|
||||||
|
type FieldContentProps,
|
||||||
|
type FieldDescriptionProps,
|
||||||
|
type FieldErrorProps,
|
||||||
|
type FieldGroupProps,
|
||||||
|
type FieldLabelProps,
|
||||||
|
type FieldProps,
|
||||||
|
type FieldTitleProps,
|
||||||
|
} from './components/Field.js';
|
||||||
export { Textarea, type TextareaProps } from './components/Textarea.js';
|
export { Textarea, type TextareaProps } from './components/Textarea.js';
|
||||||
export { Card, CardHeader, CardTitle, CardDescription, type CardProps } from './components/Card.js';
|
export { Card, CardHeader, CardTitle, CardDescription, type CardProps } from './components/Card.js';
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -29,8 +29,5 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,5 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"registry": "https://gitea.bytelyst.com/api/packages/ByteLyst/npm/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ else
|
|||||||
IS_CORP=false
|
IS_CORP=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REGISTRY_URL="${GITEA_NPM_REGISTRY_URL:-${GITEA_BASE}/api/packages/ByteLyst/npm/}"
|
REGISTRY_URL="${GITEA_NPM_REGISTRY_URL:-${GITEA_BASE}/api/packages/bytelyst/npm/}"
|
||||||
TOKEN="${GITEA_NPM_TOKEN:-}"
|
TOKEN="${GITEA_NPM_TOKEN:-}"
|
||||||
WORK_DIR="${TMPDIR:-/tmp}/bytelyst-publish-$$"
|
WORK_DIR="${TMPDIR:-/tmp}/bytelyst-publish-$$"
|
||||||
DRY_RUN=false
|
DRY_RUN=false
|
||||||
|
|||||||
@ -46,7 +46,7 @@ else
|
|||||||
IS_CORP=false
|
IS_CORP=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REGISTRY_URL="${GITEA_NPM_REGISTRY_URL:-${GITEA_BASE}/api/packages/ByteLyst/npm/}"
|
REGISTRY_URL="${GITEA_NPM_REGISTRY_URL:-${GITEA_BASE}/api/packages/bytelyst/npm/}"
|
||||||
AUTH_TARGET="${REGISTRY_URL#http://}"
|
AUTH_TARGET="${REGISTRY_URL#http://}"
|
||||||
AUTH_TARGET="${AUTH_TARGET#https://}"
|
AUTH_TARGET="${AUTH_TARGET#https://}"
|
||||||
TOKEN="${GITEA_NPM_TOKEN:-}"
|
TOKEN="${GITEA_NPM_TOKEN:-}"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user