Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
branches:
- main

concurrency:
group: "ci-${{ github.ref }}"
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2

- name: Install
run: bun install --frozen-lockfile

- name: Typecheck
run: bun run typecheck

- name: Lint
run: bun run lint

- name: Test
run: bun run test
14 changes: 8 additions & 6 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ concurrency:
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 8
timeout-minutes: 12
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: "20.x"

- name: Install
run: bun install --frozen-lockfile

- name: Check (typecheck, lint, test)
run: bun run ci

- name: Build
run: |
bun install --frozen-lockfile
bun run build
cp CNAME dist/
cp .nojekyll dist/
Expand Down
10 changes: 9 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"files": {
"ignoreUnknown": false,
"includes": ["src/**", "*.json", "*.ts", "*.js", "!dist", "!node_modules"],
"includes": [
"src/**",
"*.json",
"*.ts",
"*.js",
"!dist",
"!node_modules",
"!src/**/*.css"
],
"maxSize": 1048576
},
"linter": {
Expand Down
231 changes: 231 additions & 0 deletions docs/project-review.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"build": "NODE_ENV=production rsbuild build",
"preview": "rsbuild preview",
"typecheck": "tsgo --noEmit -p tsconfig.json",
"lint": "tsgo --noEmit -p tsconfig.json && biome check --write ."
"test": "bun test",
"lint": "biome check .",
"lint:fix": "tsgo --noEmit -p tsconfig.json && biome check --write .",
"ci": "bun run typecheck && bun run lint && bun run test"
},
"dependencies": {
"@ant-design/charts": "^2.6.7",
Expand All @@ -21,6 +24,7 @@
"git-url-parse": "^16.1.0",
"hash-wasm": "^4.12.0",
"history": "^5.3.0",
"i18next": "^26.3.4",
"i18next-browser-languagedetector": "^8.2.1",
"json-diff-kit": "^1.0.35",
"react": "^19.2.7",
Expand Down
4 changes: 2 additions & 2 deletions src/components/app-detail-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ function AppDetailTab({
<button
aria-selected={active}
className={cn(
'flex min-h-12 flex-1 items-center justify-center gap-2 rounded-lg border border-slate-200 bg-white px-6 py-3 font-medium text-base text-slate-700 shadow-sm transition-colors hover:border-blue-300 hover:text-blue-600 md:min-w-36 md:flex-none',
'flex min-h-12 flex-1 items-center justify-center gap-2 rounded-lg border border-slate-200 bg-white px-6 py-3 font-medium text-base text-slate-700 shadow-sm transition-colors hover:border-blue-300 hover:text-primary md:min-w-36 md:flex-none',
active
? 'border-blue-600! bg-blue-600! text-white! shadow-none hover:border-blue-600! hover:bg-blue-600! hover:text-white!'
? 'border-primary! bg-primary! text-white! shadow-none hover:border-primary! hover:bg-primary! hover:text-white!'
: undefined,
disabled
? 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-300 shadow-none hover:border-slate-200 hover:bg-slate-50 hover:text-slate-300'
Expand Down
8 changes: 4 additions & 4 deletions src/components/app-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ function AppIconButton({
<button
className={cn(
'flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-xl border-0 bg-transparent transition-colors hover:bg-slate-100',
isActive ? 'bg-blue-600 text-white hover:bg-blue-600' : undefined,
isActive ? 'bg-primary text-white hover:bg-primary' : undefined,
)}
onClick={() => onSelect(app)}
title={`${app.name} · ${formatCheckCount(app, t)}`}
Expand Down Expand Up @@ -381,7 +381,7 @@ function AppDrawerRow({
)}
>
{isActive && (
<span className="absolute top-1/2 left-2 h-2 w-2 -translate-y-1/2 rounded-full bg-blue-600" />
<span className="absolute top-1/2 left-2 h-2 w-2 -translate-y-1/2 rounded-full bg-primary" />
)}
<button
className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 border-0 bg-transparent py-2.5 pr-2 pl-5 text-left text-inherit"
Expand Down Expand Up @@ -419,8 +419,8 @@ function AppDrawerRow({
<button
aria-label={t('app_drawer.open_app_settings', { name: app.name })}
className={cn(
'mr-2 flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-slate-400 opacity-0 transition-all hover:bg-white/80 hover:text-blue-600 hover:opacity-100 focus-visible:opacity-100 group-hover:opacity-100',
isActive ? 'text-blue-600 hover:bg-blue-50' : undefined,
'mr-2 flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-slate-400 opacity-0 transition-all hover:bg-white/80 hover:text-primary hover:opacity-100 focus-visible:opacity-100 group-hover:opacity-100',
isActive ? 'text-primary hover:bg-blue-50' : undefined,
)}
onClick={() => onSettings(app)}
title={t('app_drawer.app_settings')}
Expand Down
8 changes: 4 additions & 4 deletions src/components/app-settings-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DeleteFilled } from '@ant-design/icons';
import { DeleteOutlined } from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
import {
Button,
Expand All @@ -17,6 +17,7 @@ import { rootRouterPath, router } from '@/router';
import { api } from '@/services/api';
import type { App } from '@/types';
import { useUserInfo } from '@/utils/hooks';
import { appKeys } from '@/utils/query-keys';

export interface AppSettingsTarget {
id: number;
Expand Down Expand Up @@ -83,7 +84,7 @@ function AppSettingsModalContent({
const { t } = useTranslation();
const { user } = useUserInfo();
const { data: app, isLoading } = useQuery({
queryKey: ['app', appId],
queryKey: appKeys.detail(appId),
queryFn: () => api.getApp(appId),
});
const appKey = Form.useWatch('appKey', form) as string;
Expand Down Expand Up @@ -162,8 +163,7 @@ function AppSettingsModalContent({
</Form.Item>
<Form.Item label={t('app_settings_modal.delete_app')} layout="vertical">
<Button
type="primary"
icon={<DeleteFilled />}
icon={<DeleteOutlined />}
onClick={() => {
Modal.confirm({
title: t('app_settings_modal.delete_confirm'),
Expand Down
35 changes: 8 additions & 27 deletions src/components/daily-check-quota.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { WarningOutlined } from '@ant-design/icons';
import { Alert, Progress, Tag, Tooltip, Typography } from 'antd';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import { quotas } from '@/constants/quotas';
import {
CHECK_QUOTA_LOW_RATIO,
getCheckQuotaColors,
getCheckQuotaWarningState,
} from '@/utils/check-quota-warning';
import dayjs from '@/utils/dayjs';
import { cn } from '@/utils/helper';
import { useUserInfo } from '@/utils/hooks';

Expand Down Expand Up @@ -106,11 +107,7 @@ export function DailyCheckQuotaUserTrigger({
const { t } = useTranslation();
const quotaState = useDailyCheckQuotaState();
const { user } = quotaState;
const strokeColor = quotaState.isExceeded
? '#ef4444'
: quotaState.isLow
? '#f59e0b'
: '#2563eb';
const quotaColors = getCheckQuotaColors(quotaState.level);
const tierTitle = user ? getTierTitle(user.tier, user.quota?.title, t) : '';
const expireLabel = user?.tierExpiresAt
? t('daily_check_quota.expire_date', {
Expand Down Expand Up @@ -151,14 +148,8 @@ export function DailyCheckQuotaUserTrigger({
showInfo={false}
size="small"
status={quotaState.progressStatus}
strokeColor={strokeColor}
trailColor={
quotaState.isExceeded
? '#fecaca'
: quotaState.isLow
? '#fde68a'
: '#e5e7eb'
}
strokeColor={quotaColors.stroke}
trailColor={quotaColors.trail}
/>
);
const content = compact ? (
Expand Down Expand Up @@ -259,11 +250,7 @@ export default function DailyCheckQuota(_props: DailyCheckQuotaProps) {
: quotaState.isLow
? t('daily_check_quota.status_low')
: t('daily_check_quota.status_healthy');
const progressStrokeColor = quotaState.isExceeded
? '#ef4444'
: quotaState.isLow
? '#f59e0b'
: '#2563eb';
const progressColors = getCheckQuotaColors(quotaState.level);

return (
<div className="space-y-3">
Expand Down Expand Up @@ -343,15 +330,9 @@ export default function DailyCheckQuota(_props: DailyCheckQuotaProps) {
percent={quotaState.percent}
showInfo={false}
status={quotaState.progressStatus}
strokeColor={progressStrokeColor}
strokeColor={progressColors.stroke}
strokeLinecap="round"
trailColor={
quotaState.isExceeded
? '#fecaca'
: quotaState.isLow
? '#fde68a'
: undefined
}
trailColor={quotaState.isWarning ? progressColors.trail : undefined}
/>
</Tooltip>
<div className="mt-2 text-gray-500 text-xs">
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ErrorBoundary } from './error-boundary';
export { default as Footer } from './footer';
export { PageContainer } from './page-container';
4 changes: 2 additions & 2 deletions src/components/main-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ export default MainLayout;

const style: Style = {
header: {
background: '#fff',
background: 'var(--ant-color-bg-container)',
minHeight: 64,
height: 'auto',
lineHeight: 'normal',
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid #e5e7eb',
borderBottom: '1px solid var(--ant-color-border-secondary)',
boxShadow: 'none',
zIndex: 10,
},
Expand Down
53 changes: 53 additions & 0 deletions src/components/page-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Typography } from 'antd';
import type { ReactNode } from 'react';
import { cn } from '@/utils/helper';

const { Title } = Typography;

interface PageContainerProps {
/** 页面主标题,统一使用 level 4。 */
title?: ReactNode;
/** 标题下方的辅助说明文字。 */
description?: ReactNode;
/** 标题行右侧的操作区(搜索框、按钮等)。 */
extra?: ReactNode;
className?: string;
children: ReactNode;
}

/**
* 页面级容器:统一最大宽度、水平居中与标题层级。
* 替代此前散落在各页面、且无 CSS 定义的 `page-section` 幽灵类,
* 并统一 admin/apps 等页面参差不齐的标题层级(level 3/4/5 混用)。
*/
export function PageContainer({
title,
description,
extra,
className,
children,
}: PageContainerProps) {
const hasHeader = title || description || extra;
return (
<div className={cn('page-section', className)}>
{hasHeader && (
<div className="mb-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
{title && (
<Title level={4} className="m-0!">
{title}
</Title>
)}
{description && (
<div className="text-sm text-text-secondary">{description}</div>
)}
</div>
{extra}
</div>
)}
{children}
</div>
);
}

export default PageContainer;
8 changes: 4 additions & 4 deletions src/components/top-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ export default function TopNavigation({
<button
aria-label={t('nav.open_menu')}
className={cn(
'flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border-0 bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-500',
'flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border-0 bg-primary text-white shadow-sm transition-colors hover:bg-primary-hover',
showAuthenticatedChrome && user ? undefined : 'ml-auto',
)}
onClick={() => setMobileMenuOpen(true)}
Expand Down Expand Up @@ -687,9 +687,9 @@ function AppSwitcherContent({
{recentApps.map((app) => (
<button
className={cn(
'inline-flex max-w-[180px] cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-gray-50 px-2 py-1 text-gray-600 text-xs transition-colors hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600',
'inline-flex max-w-[180px] cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-gray-50 px-2 py-1 text-gray-600 text-xs transition-colors hover:border-blue-300 hover:bg-blue-50 hover:text-primary',
app.id === currentAppId
? 'border-blue-200 bg-blue-50 text-blue-600'
? 'border-blue-200 bg-blue-50 text-primary'
: undefined,
)}
key={app.id}
Expand Down Expand Up @@ -833,7 +833,7 @@ function AppRow({
</button>
<button
aria-label={t('nav.open_app_settings', { name: app.name })}
className="mr-2 flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-slate-400 transition-colors hover:bg-white hover:text-blue-600"
className="mr-2 flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-slate-400 transition-colors hover:bg-white hover:text-primary"
onClick={() => onSettings(app)}
title={t('nav.app_settings')}
type="button"
Expand Down
23 changes: 3 additions & 20 deletions src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ declare module 'bun:test' {
type ExpectAssertions = {
toBe(expected: unknown): void;
toBeGreaterThan(expected: number): void;
toBeInstanceOf(expected: unknown): void;
toBeNull(): void;
toContain(expected: unknown): void;
toEqual(expected: unknown): void;
toHaveBeenCalledWith(...args: unknown[]): void;
toHaveBeenCalled(): void;
toHaveBeenCalledTimes(expected: number): void;
toHaveLength(expected: number): void;
toThrow(expected?: unknown): void;
};
Expand All @@ -49,28 +51,9 @@ declare module 'bun:test' {
export function setSystemTime(time: Date | number | null): void;
export const mock: {
module(path: string, factory: () => any): void;
restore(): void;
<T extends (...args: any[]) => any>(
fn?: T,
): T & { mockClear(): void; mockImplementationOnce(fn: T): void };
};
}

type Tier = import('./types').Tier;

type User = import('./types').User;
type AdminUser = import('./types').AdminUser;
type AdminApp = import('./types').AdminApp;
type AdminVersion = import('./types').AdminVersion;
type Quota = import('./types').Quota;
type App = import('./types').App;
type PackageBase = import('./types').PackageBase;
type Package = import('./types').Package;
type Commit = import('./types').Commit;
type Version = import('./types').Version;
type AppDetail = import('./types').AppDetail;
type ContentProps = import('./types').ContentProps;
type VersionConfig = import('./types').VersionConfig;
type BindingType = import('./types').BindingType;
type Binding = import('./types').Binding;
type AuditLog = import('./types').AuditLog;
type ApiToken = import('./types').ApiToken;
Loading
Loading