feat: add all application source code

- apps/api: FastAPI backend with Dockerfile
- apps/web: Next.js frontend with Dockerfile
- apps/sensor: Signal collection agent
- packages: shared packages

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-22 18:57:44 +08:00
parent a840bf975b
commit 196d269b92
245 changed files with 42207 additions and 6 deletions

View File

@@ -0,0 +1,118 @@
'use client'
/**
* BorderBeam - Magic UI 流光邊框特效
* ===================================
* 用於 CRITICAL 風險等級的審核卡片
*
* Features:
* - 沿邊框流動的光束動畫
* - 可配置顏色、速度、寬度
* - 支援暫停/啟動
*/
import { cn } from '@/lib/utils'
import { type CSSProperties, type HTMLAttributes } from 'react'
// =============================================================================
// Types
// =============================================================================
export interface BorderBeamProps extends HTMLAttributes<HTMLDivElement> {
/** 動畫持續時間 (秒) */
duration?: number
/** 邊框寬度 (px) */
borderWidth?: number
/** 光束顏色 - 起始色 */
colorFrom?: string
/** 光束顏色 - 結束色 */
colorTo?: string
/** 動畫延遲 (秒) */
delay?: number
/** 光束尺寸 (px) */
size?: number
/** 是否暫停動畫 */
paused?: boolean
}
// =============================================================================
// Component
// =============================================================================
export function BorderBeam({
className,
duration = 6,
borderWidth = 2,
colorFrom = '#FF3300',
colorTo = '#FF6B35',
delay = 0,
size = 200,
paused = false,
...props
}: BorderBeamProps) {
return (
<div
className={cn(
'pointer-events-none absolute inset-0 rounded-xl overflow-hidden',
'[mask-composite:intersect] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]',
className
)}
style={
{
'--duration': `${duration}s`,
'--delay': `${delay}s`,
'--color-from': colorFrom,
'--color-to': colorTo,
'--size': `${size}px`,
padding: `${borderWidth}px`,
} as CSSProperties
}
{...props}
>
<div
className={cn(
'absolute inset-0',
'bg-[conic-gradient(from_calc(var(--start,_0)*1turn),transparent_0%,var(--color-from)_10%,var(--color-to)_20%,transparent_30%)]',
'[animation:border-beam_var(--duration)_linear_infinite]',
paused && '[animation-play-state:paused]'
)}
style={{
width: `calc(100% + var(--size))`,
height: `calc(100% + var(--size))`,
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
animationDelay: `var(--delay)`,
}}
/>
</div>
)
}
/**
* CriticalGlow - CRITICAL 風險等級的脈動光暈
*/
export function CriticalGlow({
className,
intensity = 'medium',
...props
}: HTMLAttributes<HTMLDivElement> & { intensity?: 'low' | 'medium' | 'high' }) {
const intensityStyles = {
low: 'opacity-20',
medium: 'opacity-40',
high: 'opacity-60',
}
return (
<div
className={cn(
'absolute inset-0 rounded-xl pointer-events-none',
'bg-gradient-to-br from-status-critical/20 via-transparent to-status-critical/10',
intensityStyles[intensity],
'animate-pulse',
className
)}
{...props}
/>
)
}

View File

@@ -0,0 +1,204 @@
'use client'
/**
* DotMatrixBg - 工業風點陣背景
* ============================
* Nothing.tech 明亮工業風設計
*
* Features:
* - CSS Pattern 實作 (高效能)
* - 可調整點陣密度與顏色
* - 支援漸層遮罩
* - 固定背景或跟隨滾動
*
* Usage:
* <DotMatrixBg />
* <main>...content...</main>
*/
import { forwardRef, type HTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
// =============================================================================
// Types
// =============================================================================
export interface DotMatrixBgProps extends HTMLAttributes<HTMLDivElement> {
/** 點陣密度 (間距 px) */
spacing?: number
/** 點的大小 (px) */
dotSize?: number
/** 點的顏色 (CSS color) */
dotColor?: string
/** 背景顏色 */
bgColor?: string
/** 是否固定位置 (fixed) */
fixed?: boolean
/** 是否添加漸層遮罩 */
fade?: 'none' | 'bottom' | 'edges'
/** 透明度 (0-1) */
opacity?: number
}
// =============================================================================
// Component
// =============================================================================
export const DotMatrixBg = forwardRef<HTMLDivElement, DotMatrixBgProps>(
(
{
spacing = 24,
dotSize = 1,
dotColor = 'rgba(0, 0, 0, 0.08)',
bgColor = '#FAFAFA',
fixed = true,
fade = 'none',
opacity = 1,
className,
style,
...props
},
ref
) => {
// CSS Pattern for dot matrix
const dotPattern = `radial-gradient(circle, ${dotColor} ${dotSize}px, transparent ${dotSize}px)`
// Fade gradients
const fadeStyles: Record<typeof fade, string> = {
none: '',
bottom: 'mask-image: linear-gradient(to bottom, black 70%, transparent 100%)',
edges: 'mask-image: radial-gradient(ellipse at center, black 50%, transparent 100%)',
}
return (
<div
ref={ref}
aria-hidden="true"
className={cn(
// Positioning
fixed ? 'fixed' : 'absolute',
'inset-0 -z-10',
// Pointer events
'pointer-events-none',
// Custom
className
)}
style={{
backgroundColor: bgColor,
backgroundImage: dotPattern,
backgroundSize: `${spacing}px ${spacing}px`,
backgroundPosition: '0 0',
opacity,
...(fade !== 'none' && {
WebkitMaskImage:
fade === 'bottom'
? 'linear-gradient(to bottom, black 70%, transparent 100%)'
: 'radial-gradient(ellipse at center, black 50%, transparent 100%)',
maskImage:
fade === 'bottom'
? 'linear-gradient(to bottom, black 70%, transparent 100%)'
: 'radial-gradient(ellipse at center, black 50%, transparent 100%)',
}),
...style,
}}
{...props}
/>
)
}
)
DotMatrixBg.displayName = 'DotMatrixBg'
// =============================================================================
// Preset Variants
// =============================================================================
/** 淺色點陣 (預設) */
export function DotMatrixLight(props: Omit<DotMatrixBgProps, 'dotColor' | 'bgColor'>) {
return (
<DotMatrixBg
dotColor="rgba(0, 0, 0, 0.06)"
bgColor="#FAFAFA"
{...props}
/>
)
}
/** 極淺點陣 (更subtle) */
export function DotMatrixSubtle(props: Omit<DotMatrixBgProps, 'dotColor' | 'bgColor'>) {
return (
<DotMatrixBg
dotColor="rgba(0, 0, 0, 0.03)"
bgColor="#FFFFFF"
spacing={20}
{...props}
/>
)
}
/** 密集點陣 (更工業風) */
export function DotMatrixDense(props: Omit<DotMatrixBgProps, 'spacing'>) {
return (
<DotMatrixBg
spacing={16}
dotSize={1}
{...props}
/>
)
}
// =============================================================================
// Grid Lines Background (替代選項)
// =============================================================================
export interface GridLinesBgProps extends HTMLAttributes<HTMLDivElement> {
/** 網格大小 (px) */
gridSize?: number
/** 線條顏色 */
lineColor?: string
/** 背景顏色 */
bgColor?: string
/** 是否固定 */
fixed?: boolean
}
export const GridLinesBg = forwardRef<HTMLDivElement, GridLinesBgProps>(
(
{
gridSize = 40,
lineColor = 'rgba(0, 0, 0, 0.04)',
bgColor = '#FAFAFA',
fixed = true,
className,
style,
...props
},
ref
) => {
const gridPattern = `
linear-gradient(to right, ${lineColor} 1px, transparent 1px),
linear-gradient(to bottom, ${lineColor} 1px, transparent 1px)
`
return (
<div
ref={ref}
aria-hidden="true"
className={cn(
fixed ? 'fixed' : 'absolute',
'inset-0 -z-10 pointer-events-none',
className
)}
style={{
backgroundColor: bgColor,
backgroundImage: gridPattern,
backgroundSize: `${gridSize}px ${gridSize}px`,
...style,
}}
{...props}
/>
)
}
)
GridLinesBg.displayName = 'GridLinesBg'

View File

@@ -0,0 +1,224 @@
'use client'
/**
* GlassCard - 純白透光玻璃卡片
* ============================
* Nothing.tech 明亮工業風設計
*
* Features:
* - backdrop-blur 毛玻璃效果
* - 極淡灰色邊框 (border-black/5)
* - Hover 浮動效果
* - 支援多種尺寸與變體
*
* WCAG 2.1 AA: 背景對比度 4.5:1+
*/
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
import { cn } from '@/lib/utils'
// =============================================================================
// Types
// =============================================================================
export interface GlassCardProps extends HTMLAttributes<HTMLDivElement> {
/** 子元素 */
children: ReactNode
/** 變體樣式 */
variant?: 'default' | 'elevated' | 'copilot' | 'subtle'
/** 內距尺寸 */
padding?: 'none' | 'sm' | 'md' | 'lg'
/** 是否啟用 hover 效果 */
hoverable?: boolean
/** 是否可點擊 (啟用 cursor-pointer) */
clickable?: boolean
/** 無障礙標籤 */
'aria-label'?: string
}
// =============================================================================
// Styles
// =============================================================================
const variantStyles = {
default: [
// 白色毛玻璃
'bg-white/70',
'backdrop-blur-[16px]',
'-webkit-backdrop-blur-[16px]',
// 極淡邊框
'border border-black/[0.05]',
// 陰影
'shadow-[0_4px_24px_rgba(0,0,0,0.06)]',
// 圓角
'rounded-xl',
],
elevated: [
'bg-white/80',
'backdrop-blur-[20px]',
'-webkit-backdrop-blur-[20px]',
'border border-black/[0.08]',
'shadow-[0_8px_32px_rgba(0,0,0,0.10)]',
'rounded-xl',
],
copilot: [
// AI Copilot 面板 - 帶紫色調
'bg-nothing-cream/90',
'backdrop-blur-[20px]',
'-webkit-backdrop-blur-[20px]',
'border border-status-thinking/20',
'shadow-[0_4px_24px_rgba(139,92,246,0.1)]',
'rounded-xl',
],
subtle: [
'bg-white/50',
'backdrop-blur-[12px]',
'-webkit-backdrop-blur-[12px]',
'border border-black/[0.03]',
'shadow-[0_2px_12px_rgba(0,0,0,0.04)]',
'rounded-lg',
],
}
const paddingStyles = {
none: '',
sm: 'p-3',
md: 'p-5',
lg: 'p-6',
}
const hoverStyles = [
'transition-all duration-300 ease-out',
'hover:bg-white/80',
'hover:shadow-[0_8px_32px_rgba(0,0,0,0.10)]',
'hover:border-black/[0.08]',
'hover:-translate-y-0.5',
]
// =============================================================================
// Component
// =============================================================================
export const GlassCard = forwardRef<HTMLDivElement, GlassCardProps>(
(
{
children,
variant = 'default',
padding = 'md',
hoverable = false,
clickable = false,
className,
...props
},
ref
) => {
return (
<div
ref={ref}
className={cn(
// Base styles
variantStyles[variant],
paddingStyles[padding],
// Hover effect
hoverable && hoverStyles,
// Clickable
clickable && 'cursor-pointer',
// Custom classes
className
)}
role={clickable ? 'button' : undefined}
tabIndex={clickable ? 0 : undefined}
{...props}
>
{children}
</div>
)
}
)
GlassCard.displayName = 'GlassCard'
// =============================================================================
// Sub-components
// =============================================================================
export interface GlassCardHeaderProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode
}
export function GlassCardHeader({
children,
className,
...props
}: GlassCardHeaderProps) {
return (
<div
className={cn('flex items-center justify-between mb-4', className)}
{...props}
>
{children}
</div>
)
}
export interface GlassCardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
children: ReactNode
as?: 'h1' | 'h2' | 'h3' | 'h4'
}
export function GlassCardTitle({
children,
as: Tag = 'h3',
className,
...props
}: GlassCardTitleProps) {
return (
<Tag
className={cn(
'font-heading text-lg font-semibold text-nothing-gray-900',
className
)}
{...props}
>
{children}
</Tag>
)
}
export interface GlassCardContentProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode
}
export function GlassCardContent({
children,
className,
...props
}: GlassCardContentProps) {
return (
<div className={cn('flex-1', className)} {...props}>
{children}
</div>
)
}
export interface GlassCardFooterProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode
}
export function GlassCardFooter({
children,
className,
...props
}: GlassCardFooterProps) {
return (
<div
className={cn(
'mt-4 pt-3 border-t border-nothing-gray-200',
className
)}
{...props}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,36 @@
/**
* AWOOOI UI Component Library
* ===========================
* Nothing.tech 明亮工業風原子組件
*
* CPO-101: 原子組件庫
*/
// Glass Card
export {
GlassCard,
GlassCardHeader,
GlassCardTitle,
GlassCardContent,
GlassCardFooter,
type GlassCardProps,
} from './glass-card'
// Status Orb
export {
StatusOrb,
StatusOrbWithRing,
StatusBadge,
type StatusOrbProps,
type StatusType,
} from './status-orb'
// Dot Matrix Background
export {
DotMatrixBg,
DotMatrixLight,
DotMatrixSubtle,
DotMatrixDense,
GridLinesBg,
type DotMatrixBgProps,
} from './dot-matrix-bg'

View File

@@ -0,0 +1,253 @@
'use client'
/**
* StatusOrb - 狀態呼吸燈
* ======================
* Nothing.tech 明亮工業風設計
*
* Features:
* - 10px 預設尺寸 (可調整)
* - 4 種狀態: healthy (綠), warning (橘), critical (紅), thinking (紫)
* - 平滑 pulse 動畫
* - WCAG 2.1 AA 色彩對比度
*
* Accessibility:
* - 使用 aria-label 描述狀態
* - 色彩 + 動畫雙重指示 (不僅依賴顏色)
*/
import { forwardRef, type HTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
// =============================================================================
// Types
// =============================================================================
export type StatusType = 'healthy' | 'warning' | 'critical' | 'thinking' | 'idle' | 'syncing'
export interface StatusOrbProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
/** 狀態類型 */
status: StatusType
/** 尺寸 (px) */
size?: 'xs' | 'sm' | 'md' | 'lg'
/** 是否顯示脈衝動畫 */
pulse?: boolean
/** 是否顯示外發光 */
glow?: boolean
/** 無障礙標籤 (i18n) */
'aria-label'?: string
}
// =============================================================================
// Status Configuration
// =============================================================================
interface StatusConfig {
/** 背景色 */
bgColor: string
/** 發光色 */
glowColor: string
/** 是否預設動畫 */
defaultPulse: boolean
/** 無障礙標籤 (預設) */
label: string
}
const statusConfig: Record<StatusType, StatusConfig> = {
healthy: {
bgColor: 'bg-status-healthy', // #22C55E
glowColor: 'shadow-glow-green',
defaultPulse: false,
label: 'Healthy',
},
warning: {
bgColor: 'bg-status-warning', // #F59E0B
glowColor: 'shadow-[0_0_12px_rgba(245,158,11,0.5)]',
defaultPulse: true,
label: 'Warning',
},
critical: {
bgColor: 'bg-status-critical', // #FF3300
glowColor: 'shadow-glow-red',
defaultPulse: true,
label: 'Critical',
},
thinking: {
bgColor: 'bg-status-thinking', // #8B5CF6
glowColor: 'shadow-glow-purple',
defaultPulse: true,
label: 'Thinking',
},
idle: {
bgColor: 'bg-status-idle', // #A3A3A3
glowColor: '',
defaultPulse: false,
label: 'Idle',
},
syncing: {
bgColor: 'bg-status-syncing', // #3B82F6
glowColor: 'shadow-glow-blue',
defaultPulse: true,
label: 'Syncing',
},
}
// =============================================================================
// Size Configuration
// =============================================================================
const sizeConfig = {
xs: 'w-2 h-2', // 8px
sm: 'w-2.5 h-2.5', // 10px (default per spec)
md: 'w-3 h-3', // 12px
lg: 'w-4 h-4', // 16px
}
// =============================================================================
// Component
// =============================================================================
export const StatusOrb = forwardRef<HTMLSpanElement, StatusOrbProps>(
(
{
status,
size = 'sm',
pulse,
glow = false,
className,
'aria-label': ariaLabel,
...props
},
ref
) => {
const config = statusConfig[status]
const shouldPulse = pulse ?? config.defaultPulse
const label = ariaLabel || config.label
return (
<span
ref={ref}
role="status"
aria-label={label}
className={cn(
// Base
'inline-block rounded-full',
// Size
sizeConfig[size],
// Color
config.bgColor,
// Glow effect
glow && config.glowColor,
// Pulse animation
shouldPulse && 'animate-pulse',
// Transition
'transition-all duration-300',
// Custom
className
)}
{...props}
/>
)
}
)
StatusOrb.displayName = 'StatusOrb'
// =============================================================================
// Variant with Ring (更明顯的指示器)
// =============================================================================
export interface StatusOrbWithRingProps extends StatusOrbProps {
/** 是否顯示外環 */
showRing?: boolean
}
export const StatusOrbWithRing = forwardRef<HTMLSpanElement, StatusOrbWithRingProps>(
({ showRing = true, size = 'sm', status, className, ...props }, ref) => {
const config = statusConfig[status]
const shouldPulse = props.pulse ?? config.defaultPulse
// Ring sizes
const ringSizeConfig = {
xs: 'w-4 h-4',
sm: 'w-5 h-5',
md: 'w-6 h-6',
lg: 'w-8 h-8',
}
if (!showRing) {
return <StatusOrb ref={ref} status={status} size={size} className={className} {...props} />
}
return (
<span
ref={ref}
className={cn(
'relative inline-flex items-center justify-center',
ringSizeConfig[size],
className
)}
>
{/* Outer ring (animated) */}
{shouldPulse && (
<span
className={cn(
'absolute inset-0 rounded-full opacity-30 animate-ping',
config.bgColor
)}
/>
)}
{/* Inner orb */}
<StatusOrb status={status} size={size} pulse={false} {...props} />
</span>
)
}
)
StatusOrbWithRing.displayName = 'StatusOrbWithRing'
// =============================================================================
// Status Badge (狀態 + 文字標籤)
// =============================================================================
export interface StatusBadgeProps extends StatusOrbProps {
/** 顯示文字 (i18n) */
label?: string
/** 標籤位置 */
labelPosition?: 'right' | 'left'
}
export const StatusBadge = forwardRef<HTMLSpanElement, StatusBadgeProps>(
(
{
status,
label,
labelPosition = 'right',
size = 'sm',
className,
...props
},
ref
) => {
const config = statusConfig[status]
const displayLabel = label || config.label
return (
<span
ref={ref}
className={cn(
'inline-flex items-center gap-2',
labelPosition === 'left' && 'flex-row-reverse',
className
)}
>
<StatusOrb status={status} size={size} {...props} />
<span className="text-xs font-mono text-nothing-gray-600">
{displayLabel}
</span>
</span>
)
}
)
StatusBadge.displayName = 'StatusBadge'

View File

@@ -0,0 +1,191 @@
'use client'
/**
* AWOOOI Toast - 全局通知提示
* ============================
* Nothing.tech 設計語言
*
* 用法:
* import { toast, ToastContainer } from '@/components/ui/toast'
* toast.success('操作成功')
* toast.error('操作失敗')
* toast.info('K8s 指令發送中...')
*/
import { useState, useEffect, useCallback, createContext, useContext } from 'react'
import { cn } from '@/lib/utils'
import { CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react'
// =============================================================================
// Types
// =============================================================================
type ToastType = 'success' | 'error' | 'info' | 'loading'
interface ToastItem {
id: string
type: ToastType
message: string
duration?: number
}
interface ToastContextType {
toasts: ToastItem[]
addToast: (type: ToastType, message: string, duration?: number) => string
removeToast: (id: string) => void
}
// =============================================================================
// Context
// =============================================================================
const ToastContext = createContext<ToastContextType | null>(null)
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<ToastItem[]>([])
const addToast = useCallback((type: ToastType, message: string, duration = 4000) => {
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
setToasts((prev) => [...prev, { id, type, message, duration }])
if (duration > 0 && type !== 'loading') {
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id))
}, duration)
}
return id
}, [])
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id))
}, [])
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
{children}
<ToastContainer />
</ToastContext.Provider>
)
}
export function useToast() {
const context = useContext(ToastContext)
if (!context) {
throw new Error('useToast must be used within ToastProvider')
}
return context
}
// =============================================================================
// Toast Container
// =============================================================================
function ToastContainer() {
const context = useContext(ToastContext)
if (!context) return null
const { toasts, removeToast } = context
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onClose={() => removeToast(toast.id)} />
))}
</div>
)
}
// =============================================================================
// Toast Item
// =============================================================================
const TOAST_CONFIG: Record<ToastType, { icon: typeof CheckCircle2; bg: string; border: string; text: string }> = {
success: {
icon: CheckCircle2,
bg: 'bg-status-healthy/10',
border: 'border-status-healthy/30',
text: 'text-status-healthy',
},
error: {
icon: XCircle,
bg: 'bg-status-critical/10',
border: 'border-status-critical/30',
text: 'text-status-critical',
},
info: {
icon: AlertCircle,
bg: 'bg-status-thinking/10',
border: 'border-status-thinking/30',
text: 'text-status-thinking',
},
loading: {
icon: Loader2,
bg: 'bg-nothing-gray-100',
border: 'border-nothing-gray-300',
text: 'text-nothing-gray-700',
},
}
function ToastItem({ toast, onClose }: { toast: ToastItem; onClose: () => void }) {
const config = TOAST_CONFIG[toast.type]
const Icon = config.icon
return (
<div
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-xl border shadow-lg backdrop-blur-md',
'animate-slide-in-up min-w-[280px] max-w-[400px]',
config.bg,
config.border
)}
onClick={onClose}
>
<Icon
className={cn(
'w-5 h-5 flex-shrink-0',
config.text,
toast.type === 'loading' && 'animate-spin'
)}
/>
<p className={cn('font-mono text-sm', config.text)}>{toast.message}</p>
</div>
)
}
// =============================================================================
// Global Toast API (for use outside React components)
// =============================================================================
let globalAddToast: ((type: ToastType, message: string, duration?: number) => string) | null = null
let globalRemoveToast: ((id: string) => void) | null = null
export function setGlobalToast(
addFn: (type: ToastType, message: string, duration?: number) => string,
removeFn: (id: string) => void
) {
globalAddToast = addFn
globalRemoveToast = removeFn
}
export const toast = {
success: (message: string, duration?: number) => globalAddToast?.('success', message, duration),
error: (message: string, duration?: number) => globalAddToast?.('error', message, duration),
info: (message: string, duration?: number) => globalAddToast?.('info', message, duration),
loading: (message: string) => globalAddToast?.('loading', message, 0),
dismiss: (id: string) => globalRemoveToast?.(id),
}
// =============================================================================
// Toast Initializer (放在 layout 中)
// =============================================================================
export function ToastInitializer() {
const { addToast, removeToast } = useToast()
useEffect(() => {
setGlobalToast(addToast, removeToast)
}, [addToast, removeToast])
return null
}