diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/.dockerignore b/PROJECTS/advanced/ai-threat-detection/frontend/.dockerignore new file mode 100644 index 00000000..a0256ec7 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/.dockerignore @@ -0,0 +1,15 @@ +node_modules +build +dist +.git +.gitignore +*.md +.env* +.vscode +.idea +*.log +npm-debug.log* +pnpm-debug.log* +.DS_Store +coverage +.nyc_output diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/.gitignore b/PROJECTS/advanced/ai-threat-detection/frontend/.gitignore new file mode 100644 index 00000000..61cb0c2e --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.vite + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/.stylelintignore b/PROJECTS/advanced/ai-threat-detection/frontend/.stylelintignore new file mode 100644 index 00000000..37da03e1 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/.stylelintignore @@ -0,0 +1,22 @@ +# ©AngelaMos | 2025 +# .stylelintignore + +# Dependencies +node_modules/ + +# Production builds +dist/ +build/ +out/ + +# JS/TS files +**/*.js +**/*.jsx +**/*.ts +**/*.tsx + +# Generated files +*.min.css + +# Error system styles - ignore from linting +src/core/app/_toastStyles.scss diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/biome.json b/PROJECTS/advanced/ai-threat-detection/frontend/biome.json new file mode 100644 index 00000000..7f29029a --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/biome.json @@ -0,0 +1,94 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 82, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "semicolons": "asNeeded", + "trailingCommas": "es5", + "arrowParentheses": "always" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noExcessiveCognitiveComplexity": { + "level": "error", + "options": { "maxAllowedComplexity": 25 } + }, + "noForEach": "off", + "useLiteralKeys": "off" + }, + "correctness": { + "noUnusedVariables": "error", + "noUnusedImports": "error", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error", + "noUndeclaredVariables": "error" + }, + "style": { + "useImportType": "error", + "useConst": "error", + "useTemplate": "error", + "useSelfClosingElements": "error", + "useFragmentSyntax": "error", + "noNonNullAssertion": "error", + "useConsistentArrayType": { + "level": "error", + "options": { "syntax": "shorthand" } + }, + "useNamingConvention": "off" + }, + "suspicious": { + "noExplicitAny": "error", + "noDebugger": "error", + "noConsole": "warn", + "noArrayIndexKey": "warn", + "noAssignInExpressions": "error", + "noDoubleEquals": "error", + "noRedeclare": "error", + "noVar": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "error" + }, + "a11y": { + "useAltText": "error", + "useAnchorContent": "error", + "useKeyWithClickEvents": "error", + "noStaticElementInteractions": "error", + "useButtonType": "error", + "useValidAnchor": "error" + } + } + }, + "overrides": [ + { + "includes": ["src/main.tsx"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off" + } + } + } + } + ] +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/index.html b/PROJECTS/advanced/ai-threat-detection/frontend/index.html new file mode 100644 index 00000000..9d7b000a --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + AngelusVigil + + + + +
+ + + diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/package.json b/PROJECTS/advanced/ai-threat-detection/frontend/package.json new file mode 100644 index 00000000..ddddc732 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "frontend-template", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "typecheck": "tsc -b", + "lint:scss": "stylelint '**/*.scss'", + "lint:scss:fix": "stylelint '**/*.scss' --fix" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.20", + "axios": "^1.13.4", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-error-boundary": "^6.1.0", + "react-icon": "^1.0.0", + "react-icons": "^5.5.0", + "react-router-dom": "^7.13.0", + "sonner": "^2.0.7", + "zod": "^4.3.6", + "zustand": "^5.0.10" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.13", + "@tanstack/react-query-devtools": "^5.91.2", + "@types/node": "^25.1.0", + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "sass": "^1.97.3", + "stylelint": "^17.0.0", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard-scss": "^17.0.0", + "typescript": "~5.9.3", + "vite": "npm:rolldown-vite@7.3.1", + "vite-tsconfig-paths": "^6.0.5" + }, + "pnpm": { + "overrides": { + "vite": "npm:rolldown-vite@7.2.5" + } + } +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/android-chrome-192x192.png b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/android-chrome-192x192.png new file mode 100644 index 00000000..587a3c29 Binary files /dev/null and b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/android-chrome-192x192.png differ diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/android-chrome-512x512.png b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/android-chrome-512x512.png new file mode 100644 index 00000000..d4b40f14 Binary files /dev/null and b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/android-chrome-512x512.png differ diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/apple-touch-icon.png b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/apple-touch-icon.png new file mode 100644 index 00000000..910b753c Binary files /dev/null and b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/apple-touch-icon.png differ diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon-16x16.png b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon-16x16.png new file mode 100644 index 00000000..eb19c0dd Binary files /dev/null and b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon-16x16.png differ diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon-32x32.png b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon-32x32.png new file mode 100644 index 00000000..fc1293cc Binary files /dev/null and b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon-32x32.png differ diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon.ico b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon.ico new file mode 100644 index 00000000..ee279807 Binary files /dev/null and b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/favicon.ico differ diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/site.webmanifest b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/site.webmanifest new file mode 100644 index 00000000..1dd91123 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/public/assets/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/App.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/App.tsx new file mode 100644 index 00000000..efbd5388 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/App.tsx @@ -0,0 +1,35 @@ +// =================== +// © AngelaMos | 2026 +// App.tsx +// =================== + +import { QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { RouterProvider } from 'react-router-dom' +import { Toaster } from 'sonner' + +import { queryClient } from '@/core/api' +import { router } from '@/core/app/routers' + +export default function App(): React.ReactElement { + return ( + +
+ + +
+ +
+ ) +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts new file mode 100644 index 00000000..6221af9a --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts @@ -0,0 +1,4 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/index.ts new file mode 100644 index 00000000..8eb1f51a --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/index.ts @@ -0,0 +1,7 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +export * from './hooks' +export * from './types' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/index.ts new file mode 100644 index 00000000..6221af9a --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/index.ts @@ -0,0 +1,4 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx new file mode 100644 index 00000000..9f7eeadd --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx @@ -0,0 +1,4 @@ +/** + * ©AngelaMos | 2026 + * index.tsx + */ diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/config.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/config.ts new file mode 100644 index 00000000..f608e046 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/config.ts @@ -0,0 +1,78 @@ +// =================== +// © AngelaMos | 2026 +// config.ts +// =================== + +export const API_ENDPOINTS = { + HEALTH: '/health', + READY: '/ready', + THREATS: { + LIST: '/threats', + BY_ID: (id: string) => `/threats/${id}`, + }, + STATS: '/stats', + MODELS: { + STATUS: '/models/status', + RETRAIN: '/models/retrain', + }, +} as const + +export const WS_ENDPOINTS = { + ALERTS: '/ws/alerts', +} as const + +export const QUERY_KEYS = { + THREATS: { + ALL: ['threats'] as const, + LIST: (params: Record) => + [...QUERY_KEYS.THREATS.ALL, 'list', params] as const, + BY_ID: (id: string) => + [...QUERY_KEYS.THREATS.ALL, 'detail', id] as const, + }, + STATS: { + ALL: ['stats'] as const, + BY_RANGE: (range: string) => + [...QUERY_KEYS.STATS.ALL, range] as const, + }, + MODELS: { + ALL: ['models'] as const, + STATUS: () => [...QUERY_KEYS.MODELS.ALL, 'status'] as const, + }, +} as const + +export const ROUTES = { + DASHBOARD: '/', + THREATS: '/threats', + MODELS: '/models', +} as const + +export const STORAGE_KEYS = { + UI: 'ui-storage', +} as const + +export const QUERY_CONFIG = { + STALE_TIME: { + STANDARD: 1000 * 60 * 5, + FREQUENT: 1000 * 30, + STATIC: Infinity, + }, + GC_TIME: { + DEFAULT: 1000 * 60 * 30, + LONG: 1000 * 60 * 60, + }, + RETRY: { + DEFAULT: 3, + NONE: 0, + }, +} as const + +export const PAGINATION = { + DEFAULT_LIMIT: 50, + MAX_LIMIT: 100, +} as const + +export const ALERTS = { + MAX_ITEMS: 50, + RECONNECT_BASE_MS: 1000, + RECONNECT_MAX_MS: 30000, +} as const diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/api.config.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/api.config.ts new file mode 100644 index 00000000..0901c75c --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/api.config.ts @@ -0,0 +1,24 @@ +// =================== +// © AngelaMos | 2026 +// api.config.ts +// =================== + +import axios, { type AxiosError, type AxiosInstance } from 'axios' +import { transformAxiosError } from './errors' + +const getBaseURL = (): string => { + return import.meta.env.VITE_API_URL ?? '/api' +} + +export const apiClient: AxiosInstance = axios.create({ + baseURL: getBaseURL(), + timeout: 15000, + headers: { 'Content-Type': 'application/json' }, +}) + +apiClient.interceptors.response.use( + (response) => response, + (error: AxiosError): Promise => { + return Promise.reject(transformAxiosError(error)) + }, +) diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/errors.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/errors.ts new file mode 100644 index 00000000..786c90d4 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/errors.ts @@ -0,0 +1,100 @@ +// =================== +// © AngelaMos | 2026 +// errors.ts +// =================== + +import type { AxiosError } from 'axios' + +export const ApiErrorCode = { + NETWORK_ERROR: 'NETWORK_ERROR', + VALIDATION_ERROR: 'VALIDATION_ERROR', + NOT_FOUND: 'NOT_FOUND', + CONFLICT: 'CONFLICT', + RATE_LIMITED: 'RATE_LIMITED', + SERVER_ERROR: 'SERVER_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR', +} as const + +export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode] + +export class ApiError extends Error { + readonly code: ApiErrorCode + readonly statusCode: number + readonly details?: Record + + constructor( + message: string, + code: ApiErrorCode, + statusCode: number, + details?: Record, + ) { + super(message) + this.name = 'ApiError' + this.code = code + this.statusCode = statusCode + this.details = details + } + + getUserMessage(): string { + const messages: Record = { + [ApiErrorCode.NETWORK_ERROR]: + 'Unable to connect to the server. Check your connection.', + [ApiErrorCode.VALIDATION_ERROR]: 'Invalid request parameters.', + [ApiErrorCode.NOT_FOUND]: 'The requested resource was not found.', + [ApiErrorCode.CONFLICT]: + 'This operation conflicts with an existing resource.', + [ApiErrorCode.RATE_LIMITED]: + 'Too many requests. Please wait a moment and try again.', + [ApiErrorCode.SERVER_ERROR]: + 'Something went wrong on the server. Please try again later.', + [ApiErrorCode.UNKNOWN_ERROR]: + 'An unexpected error occurred. Please try again.', + } + return messages[this.code] + } +} + +interface ApiErrorResponse { + detail?: string | { msg: string; type: string }[] + message?: string +} + +export function transformAxiosError(error: AxiosError): ApiError { + if (!error.response) { + return new ApiError('Network error', ApiErrorCode.NETWORK_ERROR, 0) + } + + const { status } = error.response + const data = error.response.data as ApiErrorResponse | undefined + let message = 'An error occurred' + const details: Record | undefined = undefined + + if (data?.detail) { + if (typeof data.detail === 'string') { + message = data.detail + } + } else if (data?.message) { + message = data.message + } + + const codeMap: Record = { + 400: ApiErrorCode.VALIDATION_ERROR, + 404: ApiErrorCode.NOT_FOUND, + 409: ApiErrorCode.CONFLICT, + 429: ApiErrorCode.RATE_LIMITED, + 500: ApiErrorCode.SERVER_ERROR, + 502: ApiErrorCode.SERVER_ERROR, + 503: ApiErrorCode.SERVER_ERROR, + 504: ApiErrorCode.SERVER_ERROR, + } + + const code = codeMap[status] || ApiErrorCode.UNKNOWN_ERROR + + return new ApiError(message, code, status, details) +} + +declare module '@tanstack/react-query' { + interface Register { + defaultError: ApiError + } +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/index.ts new file mode 100644 index 00000000..f6dc3639 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/index.ts @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +export * from './api.config' +export * from './errors' +export * from './query.config' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/query.config.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/query.config.ts new file mode 100644 index 00000000..c2559e62 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/query.config.ts @@ -0,0 +1,98 @@ +// =================== +// © AngelaMos | 2026 +// query.config.ts +// =================== + +import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import { QUERY_CONFIG } from '@/config' +import { ApiError, ApiErrorCode } from './errors' + +const NO_RETRY_ERROR_CODES: readonly ApiErrorCode[] = [ + ApiErrorCode.NOT_FOUND, + ApiErrorCode.VALIDATION_ERROR, +] as const + +const shouldRetryQuery = (failureCount: number, error: Error): boolean => { + if (error instanceof ApiError) { + if (NO_RETRY_ERROR_CODES.includes(error.code)) { + return false + } + } + return failureCount < QUERY_CONFIG.RETRY.DEFAULT +} + +const calculateRetryDelay = (attemptIndex: number): number => { + const baseDelay = 1000 + const maxDelay = 30000 + return Math.min(baseDelay * 2 ** attemptIndex, maxDelay) +} + +const handleQueryCacheError = ( + error: Error, + query: { state: { data: unknown } }, +): void => { + if (query.state.data !== undefined) { + const message = + error instanceof ApiError + ? error.getUserMessage() + : 'Background update failed' + toast.error(message) + } +} + +const handleMutationCacheError = ( + error: Error, + _variables: unknown, + _context: unknown, + mutation: { options: { onError?: unknown } }, +): void => { + if (mutation.options.onError === undefined) { + const message = + error instanceof ApiError ? error.getUserMessage() : 'Operation failed' + toast.error(message) + } +} + +export const QUERY_STRATEGIES = { + standard: { + staleTime: QUERY_CONFIG.STALE_TIME.STANDARD, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + }, + frequent: { + staleTime: QUERY_CONFIG.STALE_TIME.FREQUENT, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + refetchInterval: QUERY_CONFIG.STALE_TIME.FREQUENT, + }, + static: { + staleTime: QUERY_CONFIG.STALE_TIME.STATIC, + gcTime: QUERY_CONFIG.GC_TIME.LONG, + refetchOnMount: false, + refetchOnWindowFocus: false, + }, +} as const + +export type QueryStrategy = keyof typeof QUERY_STRATEGIES + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: QUERY_CONFIG.STALE_TIME.STANDARD, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + retry: shouldRetryQuery, + retryDelay: calculateRetryDelay, + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchOnReconnect: true, + }, + mutations: { + retry: QUERY_CONFIG.RETRY.NONE, + }, + }, + queryCache: new QueryCache({ + onError: handleQueryCacheError, + }), + mutationCache: new MutationCache({ + onError: handleMutationCacheError, + }), +}) diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/routers.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/routers.tsx new file mode 100644 index 00000000..2cee809e --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/routers.tsx @@ -0,0 +1,34 @@ +// =================== +// © AngelaMos | 2026 +// routers.tsx +// =================== + +import { Navigate, createBrowserRouter, type RouteObject } from 'react-router-dom' +import { ROUTES } from '@/config' +import { Shell } from './shell' + +const routes: RouteObject[] = [ + { + element: , + children: [ + { + path: ROUTES.DASHBOARD, + lazy: () => import('@/pages/dashboard'), + }, + { + path: ROUTES.THREATS, + lazy: () => import('@/pages/threats'), + }, + { + path: ROUTES.MODELS, + lazy: () => import('@/pages/models'), + }, + ], + }, + { + path: '*', + element: , + }, +] + +export const router = createBrowserRouter(routes) diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss new file mode 100644 index 00000000..53789f3c --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss @@ -0,0 +1,331 @@ +// =================== +// © AngelaMos | 2026 +// shell.module.scss +// =================== + +@use '@/styles' as *; + +$sidebar-width: 240px; +$sidebar-collapsed-width: 64px; +$header-height: 56px; + +.shell { + display: flex; + min-height: 100vh; + min-height: 100dvh; +} + +.sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: $sidebar-width; + background: $bg-surface-100; + border-right: 1px solid $border-default; + display: flex; + flex-direction: column; + z-index: $z-fixed; + @include transition-fast; + + &.collapsed { + width: $sidebar-collapsed-width; + } + + @include breakpoint-down('sm') { + transform: translateX(-100%); + + &.open { + transform: translateX(0); + } + + &.collapsed { + width: $sidebar-width; + } + } +} + +.sidebarHeader { + height: $header-height; + padding: 0 $space-3; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid $border-default; + + .sidebar.collapsed & { + justify-content: center; + padding: 0; + } +} + +.logo { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-default; + @include transition-fast; + + .sidebar.collapsed & { + display: none; + } +} + +.nav { + flex: 1; + padding: $space-3; + display: flex; + flex-direction: column; + gap: $space-1; +} + +.navItem { + display: flex; + align-items: center; + gap: $space-3; + padding: $space-2 $space-3; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-light; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &.active { + background: $bg-selection; + color: $text-default; + } + + .sidebar.collapsed & { + justify-content: center; + } +} + +.navIcon { + width: 17px; + height: 17px; + flex-shrink: 0; +} + +.navLabel { + @include transition-fast; + + .sidebar.collapsed & { + display: none; + } +} + +.adminItem { + margin-top: auto; + border-top: 1px solid $border-default; + padding-top: $space-3; +} + +.collapseBtn { + width: 45px; + height: 45px; + border-radius: $radius-md; + color: $text-light; + @include flex-center; + @include transition-fast; + + svg { + width: 23.5px; + height: 23.5px; + } + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + @include breakpoint-down('sm') { + display: none; + } +} + +.sidebarFooter { + padding: $space-3; + border-top: 1px solid $border-default; +} + +.logoutBtn { + width: 100%; + display: flex; + align-items: center; + gap: $space-3; + padding: $space-3; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + } + + .sidebar.collapsed & { + justify-content: center; + + .logoutText { + display: none; + } + } +} + +.logoutIcon { + width: 18px; + height: 18px; + flex-shrink: 0; +} + +.logoutText { + font-weight: $font-weight-medium; + @include transition-fast; +} + +.overlay { + position: fixed; + inset: 0; + background: rgb(0, 0, 0, 50%); + z-index: calc($z-fixed - 1); + display: none; + border: none; + padding: 0; + cursor: pointer; + + @include breakpoint-down('sm') { + display: block; + } +} + +.main { + flex: 1; + display: flex; + flex-direction: column; + margin-left: $sidebar-width; + min-width: 0; + @include transition-fast; + + &.collapsed { + margin-left: $sidebar-collapsed-width; + } + + @include breakpoint-down('sm') { + margin-left: 0; + + &.collapsed { + margin-left: 0; + } + } +} + +.header { + position: sticky; + top: 0; + height: $header-height; + background: $bg-surface-100; + border-bottom: 1px solid $border-default; + z-index: $z-sticky; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 $space-4; +} + +.headerLeft { + display: flex; + align-items: center; + gap: $space-3; +} + +.menuBtn { + display: none; + width: 36px; + height: 36px; + border-radius: $radius-md; + color: $text-light; + align-items: center; + justify-content: center; + @include transition-fast; + + svg { + width: 20px; + height: 20px; + } + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + @media (width <= 479px) { + display: flex; + } +} + +.pageTitle { + font-size: $font-size-base; + font-weight: $font-weight-medium; + color: $text-default; + margin-left: 7px; +} + +.headerRight { + display: flex; + align-items: center; + gap: $space-3; +} + +.avatar { + width: 32px; + height: 32px; + border-radius: $radius-full; + background: $bg-surface-300; + color: $text-light; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + @include flex-center; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(1.2); + } +} + +.content { + flex: 1; + overflow-y: auto; +} + +.loading { + @include flex-center; + height: 100%; + color: $text-muted; +} + +.error { + @include flex-column-center; + height: 100%; + gap: $space-4; + padding: $space-6; + color: $error-default; + + h2 { + font-size: $font-size-xl; + font-weight: $font-weight-semibold; + } + + pre { + font-family: $font-mono; + font-size: $font-size-sm; + padding: $space-4; + background: $bg-surface-200; + border-radius: $radius-lg; + overflow-x: auto; + max-width: 100%; + } +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.tsx new file mode 100644 index 00000000..7c564fb1 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.tsx @@ -0,0 +1,118 @@ +// =================== +// © AngelaMos | 2026 +// shell.tsx +// =================== + +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { LuChevronLeft, LuChevronRight, LuCpu, LuLayoutDashboard, LuMenu, LuShield } from 'react-icons/lu' +import { NavLink, Outlet, useLocation } from 'react-router-dom' +import { ROUTES } from '@/config' +import { useUIStore } from '@/core/lib' +import styles from './shell.module.scss' + +const NAV_ITEMS = [ + { path: ROUTES.DASHBOARD, label: 'Dashboard', icon: LuLayoutDashboard }, + { path: ROUTES.THREATS, label: 'Threats', icon: LuShield }, + { path: ROUTES.MODELS, label: 'Models', icon: LuCpu }, +] + +function ShellErrorFallback({ error }: { error: Error }): React.ReactElement { + return ( +
+

Something went wrong

+
{error.message}
+
+ ) +} + +function ShellLoading(): React.ReactElement { + return
Loading...
+} + +function getPageTitle(pathname: string): string { + const item = NAV_ITEMS.find((i) => i.path === pathname) + return item?.label ?? 'Dashboard' +} + +export function Shell(): React.ReactElement { + const location = useLocation() + const { sidebarOpen, sidebarCollapsed, toggleSidebar, toggleSidebarCollapsed } = + useUIStore() + + const pageTitle = getPageTitle(location.pathname) + + return ( +
+ + + {sidebarOpen && ( + +

{pageTitle}

+
+ + +
+ + }> + + + +
+ + + ) +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/toast.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/toast.module.scss new file mode 100644 index 00000000..29b3f614 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/toast.module.scss @@ -0,0 +1,67 @@ +// =================== +// © AngelaMos | 2026 +// toast.module.scss +// =================== + +@use '@/styles' as *; + +:global { + [data-sonner-toaster] { + --normal-bg: #{$bg-surface-100}; + --normal-border: #{$border-default}; + --normal-text: #{$text-default}; + + --success-bg: #{$bg-surface-100}; + --success-border: #{$border-default}; + --success-text: #{$text-default}; + + --error-bg: #{$bg-surface-100}; + --error-border: #{$error-default}; + --error-text: #{$text-default}; + + --warning-bg: #{$bg-surface-100}; + --warning-border: #{$border-default}; + --warning-text: #{$text-default}; + + --info-bg: #{$bg-surface-100}; + --info-border: #{$border-default}; + --info-text: #{$text-default}; + + font-family: $font-sans; + } + + [data-sonner-toast] { + border-radius: $radius-md; + padding: $space-3 $space-4; + font-size: $font-size-sm; + border: 1px solid $border-default; + background: $bg-surface-100; + color: $text-default; + + [data-title] { + font-weight: $font-weight-medium; + } + + [data-description] { + color: $text-light; + font-size: $font-size-xs; + } + + [data-close-button] { + background: none; + border: none; + padding: 0; + cursor: pointer; + color: $text-muted; + @include transition-fast; + + @include hover { + color: $text-default; + } + } + } + + [data-sonner-toast][data-type='error'] { + border-color: $error-default; + } +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/main.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/main.tsx new file mode 100644 index 00000000..ac319510 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/main.tsx @@ -0,0 +1,15 @@ +// =========================== +// ©AngelaMos | 2026 +// main.tsx +// =========================== + +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './styles.scss' + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss new file mode 100644 index 00000000..ec05e16e --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss @@ -0,0 +1,152 @@ +// =================== +// © AngelaMos | 2026 +// dashboard.module.scss +// =================== + +@use '@/styles' as *; + +.page { + min-height: calc(100vh - 56px); + padding: $space-6; + background-color: $bg-default; +} + +.container { + max-width: 800px; + margin: 0 auto; +} + +.header { + margin-bottom: $space-6; +} + +.title { + font-size: $font-size-2xl; + font-weight: $font-weight-semibold; + color: $text-default; + margin-bottom: $space-2; +} + +.subtitle { + font-size: $font-size-sm; + color: $text-lighter; +} + +.userCard { + display: flex; + align-items: center; + gap: $space-4; + padding: $space-5; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + margin-bottom: $space-8; +} + +.avatar { + width: 56px; + height: 56px; + border-radius: $radius-full; + background: $bg-surface-300; + color: $text-default; + font-size: $font-size-xl; + font-weight: $font-weight-semibold; + @include flex-center; + flex-shrink: 0; +} + +.userInfo { + @include flex-column; + gap: $space-1; + min-width: 0; +} + +.userName { + font-size: $font-size-base; + font-weight: $font-weight-medium; + color: $text-default; + @include truncate; +} + +.userEmail { + font-size: $font-size-sm; + color: $text-light; + @include truncate; +} + +.userRole { + display: inline-flex; + align-self: flex-start; + padding: $space-0-5 $space-2; + background: $bg-surface-200; + border-radius: $radius-full; + font-size: $font-size-xs; + color: $text-lighter; + text-transform: uppercase; + letter-spacing: $tracking-wide; +} + +.section { + margin-bottom: $space-8; +} + +.sectionTitle { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-light; + text-transform: uppercase; + letter-spacing: $tracking-wide; + margin-bottom: $space-4; +} + +.grid { + display: grid; + gap: $space-4; + + @include breakpoint-up('md') { + grid-template-columns: repeat(3, 1fr); + } +} + +.card { + padding: $space-4; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; +} + +.hookName { + display: inline-block; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-default; + background: $bg-surface-200; + padding: $space-1 $space-2; + border-radius: $radius-sm; + margin-bottom: $space-2; +} + +.description { + font-size: $font-size-sm; + color: $text-light; + margin-bottom: $space-2; + line-height: $line-height-relaxed; +} + +.file { + font-size: $font-size-xs; + color: $text-muted; + font-family: $font-mono; +} + +.list { + @include flex-column; + gap: $space-2; + padding-left: $space-5; + list-style: disc; + + li { + font-size: $font-size-sm; + color: $text-light; + } +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx new file mode 100644 index 00000000..4b1fca5d --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx @@ -0,0 +1,10 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +export function Component(): React.ReactElement { + return
Dashboard
+} + +Component.displayName = 'DashboardPage' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx new file mode 100644 index 00000000..9f0c013d --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx @@ -0,0 +1,10 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +export function Component(): React.ReactElement { + return
Models
+} + +Component.displayName = 'ModelsPage' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx new file mode 100644 index 00000000..3dd9c9ac --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx @@ -0,0 +1,10 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +export function Component(): React.ReactElement { + return
Threats
+} + +Component.displayName = 'ThreatsPage' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles.scss new file mode 100644 index 00000000..e06571ba --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles.scss @@ -0,0 +1,29 @@ +// =================== +// © AngelaMos | 2026 +// styles.scss +// =================== + +@forward 'styles/tokens'; +@forward 'styles/fonts'; +@forward 'styles/mixins'; + +@use 'styles/reset'; +@use 'styles/tokens' as *; +@use 'styles/fonts' as *; + +#root { + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; + background: $bg-default; +} + +.app { + flex: 1; + display: flex; + flex-direction: column; + background: $bg-default; + color: $text-default; + font-family: $font-sans; +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_fonts.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_fonts.scss new file mode 100644 index 00000000..10d59dca --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_fonts.scss @@ -0,0 +1,12 @@ +// =================== +// © AngelaMos | 2026 +// _fonts.scss +// =================== + +@use 'tokens' as *; + +$font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, + 'Helvetica Neue', Arial, sans-serif; + +$font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, + 'Liberation Mono', monospace; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_index.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_index.scss new file mode 100644 index 00000000..9d5d028c --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_index.scss @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2026 +// _index.scss +// =================== + +@forward 'tokens'; +@forward 'fonts'; +@forward 'mixins'; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_mixins.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_mixins.scss new file mode 100644 index 00000000..288b4e67 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_mixins.scss @@ -0,0 +1,120 @@ +// =================== +// © AngelaMos | 2026 +// _mixins.scss +// =================== + +@use 'sass:map'; +@use 'sass:list'; +@use 'tokens' as *; + +$breakpoints: ( + 'xs': $breakpoint-xs, + 'sm': $breakpoint-sm, + 'md': $breakpoint-md, + 'lg': $breakpoint-lg, + 'xl': $breakpoint-xl, + '2xl': $breakpoint-2xl, +); + +@mixin breakpoint-up($size) { + @media (min-width: map.get($breakpoints, $size)) { + @content; + } +} + +@mixin breakpoint-down($size) { + @media (width < map.get($breakpoints, $size)) { + @content; + } +} + +@mixin flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +@mixin flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +@mixin flex-column { + display: flex; + flex-direction: column; +} + +@mixin flex-column-center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +@mixin sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +@mixin truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@mixin line-clamp($lines: 2) { + display: -webkit-box; + -webkit-line-clamp: #{$lines}; + -webkit-box-orient: vertical; + overflow: hidden; +} + +@mixin transition-fast { + transition-property: background-color, border-color, color, opacity; + transition-duration: $duration-fast; + transition-timing-function: $ease-out; +} + +@mixin transition-normal { + transition-property: background-color, border-color, color, opacity; + transition-duration: $duration-normal; + transition-timing-function: $ease-out; +} + +@mixin absolute-fill { + position: absolute; + inset: 0; +} + +@mixin absolute-center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +@mixin hover { + @media (hover: hover) and (pointer: fine) { + &:hover { + @content; + } + } +} + +@mixin hide-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_reset.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_reset.scss new file mode 100644 index 00000000..fb9eca5b --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_reset.scss @@ -0,0 +1,198 @@ +// =================== +// © AngelaMos | 2026 +// _reset.scss +// =================== + +@use 'tokens' as *; +@use 'fonts' as *; + +*, +*::before, +*::after { + box-sizing: border-box; +} + +* { + margin: 0; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + +html { + font-size: 16px; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + text-size-adjust: none; + overflow-x: hidden; +} + +@media (prefers-reduced-motion: no-preference) { + html { + interpolate-size: allow-keywords; + scroll-behavior: smooth; + } +} + +body { + min-height: 100vh; + min-height: 100dvh; + line-height: $line-height-normal; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; + background-color: $bg-default; + color: $text-default; + font-family: $font-sans; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: $line-height-tight; + text-wrap: balance; + overflow-wrap: break-word; + font-weight: $font-weight-semibold; +} + +p { + text-wrap: pretty; + overflow-wrap: break-word; +} + +ul, +ol { + list-style: none; +} + +a { + color: inherit; + text-decoration: none; +} + +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; + height: auto; +} + +input, +button, +textarea, +select { + font: inherit; + color: inherit; +} + +input[type='text'], +input[type='email'], +input[type='password'], +input[type='search'], +input[type='number'], +input[type='tel'], +input[type='url'], +textarea, +select { + font-size: $font-size-sm; + appearance: none; +} + +button { + background: none; + border: none; + cursor: pointer; + text-align: inherit; + font-family: inherit; +} + +fieldset { + border: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +textarea:not([rows]) { + min-height: 10em; +} + +:target { + scroll-margin-block: 5ex; +} + +:focus-visible { + outline: 2px solid $border-strong; + outline-offset: 2px; +} + +:focus:not(:focus-visible) { + outline: none; +} + +[hidden] { + display: none !important; +} + +[disabled] { + cursor: not-allowed; + opacity: 0.5; +} + +dialog { + padding: 0; + border: none; + background: transparent; +} + +summary { + cursor: pointer; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +@supports (padding: max(0px)) { + body { + padding-left: max(0px, env(safe-area-inset-left)); + padding-right: max(0px, env(safe-area-inset-right)); + } +} + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: $border-default; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: $border-strong; +} + +::selection { + background-color: $bg-selection; +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss new file mode 100644 index 00000000..f3c63ba5 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss @@ -0,0 +1,168 @@ +// =================== +// © AngelaMos | 2026 +// _tokens.scss +// =================== + +// ============================================================================ +// SPACING (8px base system) +// ============================================================================ +$space-0: 0; +$space-px: 1px; +$space-0-5: 0.125rem; +$space-1: 0.25rem; +$space-1-5: 0.375rem; +$space-2: 0.5rem; +$space-2-5: 0.625rem; +$space-3: 0.75rem; +$space-3-5: 0.875rem; +$space-4: 1rem; +$space-5: 1.25rem; +$space-6: 1.5rem; +$space-7: 1.75rem; +$space-8: 2rem; +$space-9: 2.25rem; +$space-10: 2.5rem; +$space-11: 2.75rem; +$space-12: 3rem; +$space-14: 3.5rem; +$space-16: 4rem; +$space-20: 5rem; +$space-24: 6rem; +$space-28: 7rem; +$space-32: 8rem; + +// ============================================================================ +// TYPOGRAPHY SCALE +// ============================================================================ +$font-size-3xs: 0.625rem; +$font-size-2xs: 0.6875rem; +$font-size-xs: 0.75rem; +$font-size-sm: 0.875rem; +$font-size-base: 1rem; +$font-size-lg: 1.125rem; +$font-size-xl: 1.25rem; +$font-size-2xl: 1.5rem; +$font-size-3xl: 1.875rem; +$font-size-4xl: 2.25rem; +$font-size-5xl: 3rem; + +// ============================================================================ +// FONT WEIGHTS +// ============================================================================ +$font-weight-regular: 400; +$font-weight-medium: 500; +$font-weight-semibold: 600; + +// ============================================================================ +// LINE HEIGHTS +// ============================================================================ +$line-height-none: 1; +$line-height-tight: 1.2; +$line-height-snug: 1.375; +$line-height-normal: 1.5; +$line-height-relaxed: 1.625; + +// ============================================================================ +// LETTER SPACING +// ============================================================================ +$tracking-tighter: -0.05em; +$tracking-tight: -0.025em; +$tracking-normal: 0; +$tracking-wide: 0.025em; +$tracking-wider: 0.05em; + +// ============================================================================ +// COLORS +// ============================================================================ +$white: hsl(0, 0%, 100%); +$black: hsl(0, 0%, 0%); + +// Auth +$bg-page: hsl(0, 0%, 10.5%); +$bg-card: hsl(0, 0%, 6.2%); + +// Home/landing +$bg-landing: hsl(0, 0%, 10.8%); + +$bg-default: hsl(0, 0%, 7.1%); +$bg-alternative: hsl(0, 0%, 5.9%); +$bg-surface-75: hsl(0, 0%, 9%); +$bg-surface-100: hsl(0, 0%, 12.2%); +$bg-surface-200: hsl(0, 0%, 14.1%); +$bg-surface-300: hsl(0, 0%, 16.1%); +$bg-control: hsl(0, 0%, 10%); +$bg-selection: hsl(0, 0%, 19.2%); +$bg-overlay: hsl(0, 0%, 14.1%); +$bg-overlay-hover: hsl(0, 0%, 18%); + +$border-muted: hsl(0, 0%, 11.1%); +$border-default: hsl(0, 0%, 18%); +$border-strong: hsl(0, 0%, 22.4%); +$border-stronger: hsl(0, 0%, 27.1%); +$border-control: hsl(0, 0%, 22.4%); + +$text-default: hsl(0, 0%, 98%); +$text-light: hsl(0, 0%, 70.6%); +$text-lighter: hsl(0, 0%, 53.7%); +$text-muted: hsl(0, 0%, 30.2%); + +$error-default: hsl(0, 72%, 51%); +$error-light: hsl(0, 72%, 65%); + +// ============================================================================ +// BORDER RADIUS +// ============================================================================ +$radius-none: 0; +$radius-xs: 2px; +$radius-sm: 4px; +$radius-md: 6px; +$radius-lg: 8px; +$radius-xl: 12px; +$radius-full: 9999px; + +// ============================================================================ +// Z-INDEX SCALE +// ============================================================================ +$z-hide: -1; +$z-base: 0; +$z-dropdown: 100; +$z-sticky: 200; +$z-fixed: 300; +$z-overlay: 400; +$z-modal: 500; +$z-popover: 600; +$z-tooltip: 700; +$z-toast: 800; +$z-max: 9999; + +// ============================================================================ +// TRANSITIONS +// ============================================================================ +$duration-instant: 0ms; +$duration-fast: 100ms; +$duration-normal: 150ms; +$duration-slow: 200ms; + +$ease-out: cubic-bezier(0, 0, 0.2, 1); +$ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + +// ============================================================================ +// BREAKPOINTS +// ============================================================================ +$breakpoint-xs: 360px; +$breakpoint-sm: 480px; +$breakpoint-md: 768px; +$breakpoint-lg: 1024px; +$breakpoint-xl: 1280px; +$breakpoint-2xl: 1536px; + +// ============================================================================ +// CONTAINER WIDTHS +// ============================================================================ +$container-xs: 20rem; +$container-sm: 24rem; +$container-md: 28rem; +$container-lg: 32rem; +$container-xl: 36rem; +$container-2xl: 42rem; +$container-full: 100%; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/stylelint.config.js b/PROJECTS/advanced/ai-threat-detection/frontend/stylelint.config.js new file mode 100644 index 00000000..56ebd7bc --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/stylelint.config.js @@ -0,0 +1,107 @@ +// ©AngelaMos | 2025 +// stylelint.config.js + +/** @type {import('stylelint').Config} */ +export default { + extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'], + rules: { + 'block-no-empty': true, + 'declaration-no-important': true, + 'color-no-invalid-hex': true, + 'property-no-unknown': true, + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global'], + }, + ], + + 'selector-class-pattern': [ + '^[a-z]([a-z0-9-]+)?(__[a-z0-9]([a-z0-9-]+)?)?(--[a-z0-9]([a-z0-9-]+)?)?$|^[a-z][a-zA-Z0-9]*$', + { + message: + 'Selector should be in BEM format (e.g., .block__element--modifier) or CSS Modules camelCase (e.g., .testButton)', + }, + ], + + 'value-keyword-case': [ + 'lower', + { + camelCaseSvgKeywords: true, + ignoreKeywords: [ + 'BlinkMacSystemFont', + 'SFMono-Regular', + 'Menlo', + 'Monaco', + 'Consolas', + 'Roboto', + 'Arial', + 'Helvetica', + 'Times', + 'Georgia', + 'Verdana', + 'Tahoma', + 'Trebuchet', + 'Impact', + 'Comic', + ], + }, + ], + + 'property-no-vendor-prefix': [ + true, + { + ignoreProperties: ['text-size-adjust', 'appearance', 'backdrop-filter'], + }, + ], + 'value-no-vendor-prefix': true, + 'selector-no-vendor-prefix': true, + + 'property-no-deprecated': [ + true, + { + ignoreProperties: ['clip'], + }, + ], + + 'container-name-pattern': null, + 'layer-name-pattern': null, + + 'scss/at-rule-no-unknown': true, + 'scss/declaration-nested-properties-no-divided-groups': true, + 'scss/dollar-variable-no-missing-interpolation': true, + 'scss/dollar-variable-empty-line-before': null, + + 'declaration-empty-line-before': null, + 'custom-property-empty-line-before': null, + + 'no-descending-specificity': null, + + 'media-feature-name-no-unknown': [ + true, + { + ignoreMediaFeatureNames: ['map'], + }, + ], + + 'color-function-notation': null, + 'hue-degree-notation': null, + }, + ignoreFiles: [ + 'node_modules/**', + 'dist/**', + 'build/**', + '**/*.js', + '**/*.ts', + '**/*.tsx', + ], + overrides: [ + { + files: ['**/styles/_reset.scss', '**/styles/_fonts.scss'], + rules: { + 'declaration-no-important': null, + 'scss/comment-no-empty': null, + }, + }, + ], +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.app.json b/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.app.json new file mode 100644 index 00000000..afe17f60 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.json b/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.node.json b/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.node.json new file mode 100644 index 00000000..a96b3e59 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/vite.config.ts b/PROJECTS/advanced/ai-threat-detection/frontend/vite.config.ts new file mode 100644 index 00000000..a4ad5b31 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/vite.config.ts @@ -0,0 +1,71 @@ +/** + * ©AngelaMos | 2026 + * vite.config.ts + */ + +import path from 'node:path' +import react from '@vitejs/plugin-react' +import { defineConfig, loadEnv } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, path.resolve(__dirname, '..'), '') + const isDev = mode === 'development' + + return { + plugins: [react(), tsconfigPaths()], + + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + + css: { + preprocessorOptions: { + scss: {}, + }, + }, + + server: { + port: 5173, + host: '0.0.0.0', + proxy: { + '/api': { + target: env.VITE_API_TARGET || 'http://localhost:8000', + changeOrigin: true, + rewrite: (p) => p.replace(/^\/api/, ''), + }, + }, + }, + + build: { + target: 'esnext', + cssTarget: 'chrome100', + sourcemap: isDev ? true : 'hidden', + minify: 'oxc', + rollupOptions: { + output: { + manualChunks(id: string): string | undefined { + if (id.includes('node_modules')) { + if (id.includes('react-dom') || id.includes('react-router')) { + return 'vendor-react' + } + if (id.includes('@tanstack/react-query')) { + return 'vendor-query' + } + if (id.includes('zustand')) { + return 'vendor-state' + } + } + return undefined + }, + }, + }, + }, + + preview: { + port: 4173, + }, + } +})