mirror: import production source 3/3
This commit is contained in:
parent
17e3db9970
commit
2fdd51f921
40
Frontend/src/app/routes/learner-routes.tsx
Normal file
40
Frontend/src/app/routes/learner-routes.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import type { RouteObject } from 'react-router'
|
||||
|
||||
import { LearnerLayout } from '../layouts/LearnerLayout'
|
||||
import { LearnerDashboardPage } from '../pages/LearnerDashboardPage'
|
||||
import { LearnerLiveCoursesPage } from '../pages/LearnerLiveCoursesPage'
|
||||
import { LearnerCourseDetailPage } from '../pages/LearnerCourseDetailPage'
|
||||
import { LearnerScormCoursesPage } from '../pages/LearnerScormCoursesPage'
|
||||
import { LearnerScormLaunchPage } from '../pages/LearnerScormLaunchPage'
|
||||
import { LearnerLiveSessionsPage } from '../pages/LearnerLiveSessionsPage'
|
||||
import { LearnerLiveSessionPage } from '../pages/LearnerLiveSessionPage'
|
||||
import { LearnerCertificatesPage } from '../pages/LearnerCertificatesPage'
|
||||
import { LearnerCommunityPage } from '../pages/LearnerCommunityPage'
|
||||
import { FeatureGuardPage } from '../pages/FeatureGuardPage'
|
||||
import { LearnerProfilePage } from '../pages/LearnerProfilePage'
|
||||
import { LearnerNotificationsPage } from '../pages/LearnerNotificationsPage'
|
||||
|
||||
function LearnerBillingNotEnabledPage() {
|
||||
return <FeatureGuardPage title="Learner Billing" summary="Self-service billing is not available in the learner portal yet." />
|
||||
}
|
||||
|
||||
export const learnerRoutes: RouteObject[] = [
|
||||
{
|
||||
path: '/learn',
|
||||
Component: LearnerLayout,
|
||||
children: [
|
||||
{ index: true, Component: LearnerDashboardPage },
|
||||
{ path: 'courses', Component: LearnerLiveCoursesPage },
|
||||
{ path: 'courses/:slug', Component: LearnerCourseDetailPage },
|
||||
{ path: 'scorm', Component: LearnerScormCoursesPage },
|
||||
{ path: 'scorm/:id', Component: LearnerScormLaunchPage },
|
||||
{ path: 'live', Component: LearnerLiveSessionsPage },
|
||||
{ path: 'live/:id/sessions/:sessionId', Component: LearnerLiveSessionPage },
|
||||
{ path: 'certificates', Component: LearnerCertificatesPage },
|
||||
{ path: 'community', Component: LearnerCommunityPage },
|
||||
{ path: 'billing', Component: LearnerBillingNotEnabledPage },
|
||||
{ path: 'profile', Component: LearnerProfilePage },
|
||||
{ path: 'notifications', Component: LearnerNotificationsPage },
|
||||
],
|
||||
},
|
||||
]
|
||||
9
Frontend/src/app/routes/legacy-routes.tsx
Normal file
9
Frontend/src/app/routes/legacy-routes.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import type { RouteObject } from 'react-router'
|
||||
|
||||
import { LearnerLiveCoursesPage } from '../pages/LearnerLiveCoursesPage'
|
||||
import { LearnerLiveSessionPage } from '../pages/LearnerLiveSessionPage'
|
||||
|
||||
export const legacyRoutes: RouteObject[] = [
|
||||
{ path: '/learner/live-courses', Component: LearnerLiveCoursesPage },
|
||||
{ path: '/learner/live-courses/:id/sessions/:sessionId', Component: LearnerLiveSessionPage },
|
||||
]
|
||||
37
Frontend/src/app/routes/public-routes.tsx
Normal file
37
Frontend/src/app/routes/public-routes.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Navigate, type RouteObject } from 'react-router'
|
||||
|
||||
import { currentUser } from '../auth/keycloak'
|
||||
import { BlogIndexPage } from '../pages/BlogIndexPage'
|
||||
import { BlogPostPage } from '../pages/BlogPostPage'
|
||||
import { FormsBuilderPage } from '../pages/FormsBuilderPage'
|
||||
import { HomePage } from '../pages/HomePage'
|
||||
import { LiveCourseDetailPage } from '../pages/LiveCourseDetailPage'
|
||||
import { LiveCoursesIndexPage } from '../pages/LiveCoursesIndexPage'
|
||||
import { PublicFormPage } from '../pages/PublicFormPage'
|
||||
import { ScormPage } from '../pages/ScormPage'
|
||||
import { PlatformAdminPage } from '../pages/PlatformAdminPage'
|
||||
import { PublicCmsPage } from '../pages/PublicCmsPage'
|
||||
|
||||
function RootRoleRedirect() {
|
||||
const roles = (currentUser().roles ?? []).map(role => role.trim().toLowerCase())
|
||||
if (roles.includes('platform_admin') || roles.includes('platform_owner')) return <Navigate to="/platform-admin" replace />
|
||||
if (roles.includes('admin') || roles.includes('instructor')) return <Navigate to="/admin" replace />
|
||||
if (roles.includes('learner')) return <Navigate to="/learn" replace />
|
||||
return <Navigate to="/home" replace />
|
||||
}
|
||||
|
||||
export const publicRoutes: RouteObject[] = [
|
||||
{ path: '/', Component: RootRoleRedirect },
|
||||
{ path: '/home', Component: HomePage },
|
||||
{ path: '/forms', Component: FormsBuilderPage },
|
||||
{ path: '/forms/:id', Component: PublicFormPage },
|
||||
{ path: '/pages/:slug', Component: PublicCmsPage },
|
||||
{ path: '/scorm', Component: ScormPage },
|
||||
{ path: '/blog', Component: BlogIndexPage },
|
||||
{ path: '/blog/category/:categorySlug', Component: BlogIndexPage },
|
||||
{ path: '/blog/tag/:tagSlug', Component: BlogIndexPage },
|
||||
{ path: '/blog/:slug', Component: BlogPostPage },
|
||||
{ path: '/live-courses', Component: LiveCoursesIndexPage },
|
||||
{ path: '/live-courses/:slug', Component: LiveCourseDetailPage },
|
||||
{ path: '/platform-admin', Component: PlatformAdminPage },
|
||||
]
|
||||
222
Frontend/src/app/sitemap-menu.ts
Normal file
222
Frontend/src/app/sitemap-menu.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import { apiRequest, listResource } from './api/client'
|
||||
|
||||
export type SitemapTargetType = 'page' | 'post' | 'course'
|
||||
|
||||
export type SitemapMenuItem = {
|
||||
id: string
|
||||
label: string
|
||||
targetType: SitemapTargetType
|
||||
targetId: string
|
||||
to: string
|
||||
children: SitemapMenuItem[]
|
||||
}
|
||||
|
||||
export type SitemapMenu = {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
items: SitemapMenuItem[]
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
export type SitemapSourceOption = {
|
||||
id: string
|
||||
label: string
|
||||
slug?: string
|
||||
}
|
||||
|
||||
export type SitemapSources = {
|
||||
pages: SitemapSourceOption[]
|
||||
posts: SitemapSourceOption[]
|
||||
courses: SitemapSourceOption[]
|
||||
}
|
||||
|
||||
function asString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
function asNumber(value: unknown): number | undefined {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||
return undefined
|
||||
}
|
||||
|
||||
function normalizeItem(raw: unknown): SitemapMenuItem {
|
||||
const source = (raw ?? {}) as Record<string, unknown>
|
||||
const children = Array.isArray(source.children) ? source.children.map(normalizeItem) : []
|
||||
|
||||
const targetTypeRaw = asString(source.targetType)
|
||||
const targetType: SitemapTargetType = targetTypeRaw === 'post' || targetTypeRaw === 'course' ? targetTypeRaw : 'page'
|
||||
|
||||
return {
|
||||
id: asString(source.id),
|
||||
label: asString(source.label),
|
||||
targetType,
|
||||
targetId: asString(source.targetId),
|
||||
to: asString(source.to),
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSitemapMenu(raw: unknown): SitemapMenu {
|
||||
const source = (raw ?? {}) as Record<string, unknown>
|
||||
const items = Array.isArray(source.items) ? source.items.map(normalizeItem) : []
|
||||
|
||||
return {
|
||||
id: asString(source.id),
|
||||
name: asString(source.name),
|
||||
description: asString(source.description) || undefined,
|
||||
items,
|
||||
createdAt: asNumber(source.createdAt),
|
||||
updatedAt: asNumber(source.updatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSitemapMenus(): Promise<SitemapMenu[]> {
|
||||
const records = await listResource<unknown>('sitemap-menus')
|
||||
return records.map(normalizeSitemapMenu).filter(menu => menu.id && menu.name)
|
||||
}
|
||||
|
||||
export async function createSitemapMenu(payload: Pick<SitemapMenu, 'name' | 'description' | 'items'>): Promise<SitemapMenu> {
|
||||
const created = await apiRequest<unknown>('/api/sitemap-menus/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
return normalizeSitemapMenu(created)
|
||||
}
|
||||
|
||||
export async function updateSitemapMenu(id: string, payload: Pick<SitemapMenu, 'name' | 'description' | 'items'>): Promise<SitemapMenu> {
|
||||
const updated = await apiRequest<unknown>(`/api/sitemap-menus/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
return normalizeSitemapMenu(updated)
|
||||
}
|
||||
|
||||
export async function removeSitemapMenu(id: string): Promise<void> {
|
||||
await apiRequest<void>(`/api/sitemap-menus/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export function buildPathForSource(targetType: SitemapTargetType, option: SitemapSourceOption): string {
|
||||
if (targetType === 'page') {
|
||||
return '/admin/builder'
|
||||
}
|
||||
if (targetType === 'post') {
|
||||
return option.slug ? `/blog/${option.slug}` : '/blog'
|
||||
}
|
||||
return option.slug ? `/live-courses/${option.slug}` : '/live-courses'
|
||||
}
|
||||
|
||||
export async function fetchSitemapSources(): Promise<SitemapSources> {
|
||||
const [pagesRaw, postsRaw, coursesRaw] = await Promise.all([
|
||||
listResource<unknown>('pages'),
|
||||
apiRequest<unknown[]>('/api/blog/posts'),
|
||||
listResource<unknown>('live-courses'),
|
||||
])
|
||||
|
||||
const pages = pagesRaw
|
||||
.map(raw => {
|
||||
const source = (raw ?? {}) as Record<string, unknown>
|
||||
return {
|
||||
id: asString(source.id),
|
||||
label: asString(source.name) || asString(source.slug) || asString(source.id),
|
||||
slug: asString(source.slug) || undefined,
|
||||
}
|
||||
})
|
||||
.filter(item => item.id && item.label)
|
||||
|
||||
const posts = postsRaw
|
||||
.map(raw => {
|
||||
const source = (raw ?? {}) as Record<string, unknown>
|
||||
return {
|
||||
id: asString(source.id),
|
||||
label: asString(source.title) || asString(source.slug) || asString(source.id),
|
||||
slug: asString(source.slug) || undefined,
|
||||
}
|
||||
})
|
||||
.filter(item => item.id && item.label)
|
||||
|
||||
const courses = coursesRaw
|
||||
.map(raw => {
|
||||
const source = (raw ?? {}) as Record<string, unknown>
|
||||
return {
|
||||
id: asString(source.id),
|
||||
label: asString(source.title) || asString(source.slug) || asString(source.id),
|
||||
slug: asString(source.slug) || undefined,
|
||||
}
|
||||
})
|
||||
.filter(item => item.id && item.label)
|
||||
|
||||
return { pages, posts, courses }
|
||||
}
|
||||
|
||||
export type MenuParentOption = {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export function flattenParentOptions(items: SitemapMenuItem[], depth = 0): MenuParentOption[] {
|
||||
const options: MenuParentOption[] = []
|
||||
for (const item of items) {
|
||||
options.push({ id: item.id, label: `${' '.repeat(depth * 2)}${item.label}`.trimStart() })
|
||||
options.push(...flattenParentOptions(item.children, depth + 1))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
export function addMenuItem(items: SitemapMenuItem[], parentId: string | null, newItem: SitemapMenuItem): SitemapMenuItem[] {
|
||||
if (!parentId) {
|
||||
return [...items, newItem]
|
||||
}
|
||||
|
||||
return items.map(item => {
|
||||
if (item.id === parentId) {
|
||||
return { ...item, children: [...item.children, newItem] }
|
||||
}
|
||||
return { ...item, children: addMenuItem(item.children, parentId, newItem) }
|
||||
})
|
||||
}
|
||||
|
||||
export function removeMenuItem(items: SitemapMenuItem[], itemId: string): SitemapMenuItem[] {
|
||||
return items
|
||||
.filter(item => item.id !== itemId)
|
||||
.map(item => ({ ...item, children: removeMenuItem(item.children, itemId) }))
|
||||
}
|
||||
|
||||
function moveWithinSiblings(items: SitemapMenuItem[], itemId: string, direction: 'up' | 'down'): SitemapMenuItem[] {
|
||||
const index = items.findIndex(item => item.id === itemId)
|
||||
if (index === -1) {
|
||||
return items.map(item => ({ ...item, children: moveWithinSiblings(item.children, itemId, direction) }))
|
||||
}
|
||||
|
||||
const target = direction === 'up' ? index - 1 : index + 1
|
||||
if (target < 0 || target >= items.length) {
|
||||
return items
|
||||
}
|
||||
|
||||
const next = [...items]
|
||||
const [entry] = next.splice(index, 1)
|
||||
next.splice(target, 0, entry)
|
||||
return next
|
||||
}
|
||||
|
||||
export function moveMenuItem(items: SitemapMenuItem[], itemId: string, direction: 'up' | 'down'): SitemapMenuItem[] {
|
||||
return moveWithinSiblings(items, itemId, direction)
|
||||
}
|
||||
|
||||
export function updateMenuItemLabel(items: SitemapMenuItem[], itemId: string, label: string): SitemapMenuItem[] {
|
||||
return items.map(item => {
|
||||
if (item.id === itemId) {
|
||||
return { ...item, label }
|
||||
}
|
||||
return { ...item, children: updateMenuItemLabel(item.children, itemId, label) }
|
||||
})
|
||||
}
|
||||
|
||||
export function toHomeNavItems(items: SitemapMenuItem[]): Array<{ id: string; label: string; to: string }> {
|
||||
return items
|
||||
.map(item => ({ id: item.id, label: item.label, to: item.to }))
|
||||
.filter(item => item.id && item.label && item.to.startsWith('/'))
|
||||
}
|
||||
81
Frontend/src/app/types.ts
Normal file
81
Frontend/src/app/types.ts
Normal file
@ -0,0 +1,81 @@
|
||||
export type ThemeConfig = {
|
||||
background: string
|
||||
foreground: string
|
||||
primary: string
|
||||
primaryForeground: string
|
||||
muted: string
|
||||
mutedForeground: string
|
||||
border: string
|
||||
accent: string
|
||||
radius: number
|
||||
fontFamily: string
|
||||
}
|
||||
|
||||
export type CanvasBlock = {
|
||||
instanceId: string
|
||||
blockId: string
|
||||
content: Record<string, string>
|
||||
styles: Record<string, string>
|
||||
slots?: CanvasBlock[][]
|
||||
}
|
||||
|
||||
export type CustomBlockDef = {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export type PageOpenGraph = {
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string
|
||||
url?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export type PageRobots = {
|
||||
index?: boolean
|
||||
follow?: boolean
|
||||
noarchive?: boolean
|
||||
nosnippet?: boolean
|
||||
}
|
||||
|
||||
export type PageSEO = {
|
||||
title?: string
|
||||
description?: string
|
||||
canonicalUrl?: string
|
||||
openGraph?: PageOpenGraph
|
||||
robots?: PageRobots
|
||||
}
|
||||
|
||||
export type Page = {
|
||||
id: string
|
||||
name: string
|
||||
slug?: string
|
||||
status?: 'draft' | 'published'
|
||||
publishedAt?: number
|
||||
seo?: PageSEO
|
||||
blocks: CanvasBlock[]
|
||||
theme: ThemeConfig
|
||||
customBlocks: CustomBlockDef[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export type DataSource = {
|
||||
id: string
|
||||
name: string
|
||||
type: 'rest' | 'static'
|
||||
url: string
|
||||
method: 'GET' | 'POST'
|
||||
headers: string
|
||||
body: string
|
||||
staticData: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export type DataCacheEntry = {
|
||||
data: Record<string, unknown>[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
19
Frontend/src/main.tsx
Normal file
19
Frontend/src/main.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './app/App.tsx'
|
||||
import './styles/index.css'
|
||||
import { initKeycloak } from './app/auth/keycloak'
|
||||
|
||||
async function bootstrap() {
|
||||
try {
|
||||
await initKeycloak()
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
} catch (error) {
|
||||
const root = document.getElementById('root')
|
||||
if (root) {
|
||||
root.innerHTML = '<div style="padding:24px;font-family:system-ui,sans-serif">Failed to initialize authentication. Please refresh and try again.</div>'
|
||||
}
|
||||
console.error('keycloak init failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
void bootstrap()
|
||||
0
Frontend/src/styles/fonts.css
Normal file
0
Frontend/src/styles/fonts.css
Normal file
0
Frontend/src/styles/globals.css
Normal file
0
Frontend/src/styles/globals.css
Normal file
3
Frontend/src/styles/index.css
Normal file
3
Frontend/src/styles/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import './fonts.css';
|
||||
@import './tailwind.css';
|
||||
@import './theme.css';
|
||||
4
Frontend/src/styles/tailwind.css
Normal file
4
Frontend/src/styles/tailwind.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import 'tailwindcss' source(none);
|
||||
@source '../**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
@import 'tw-animate-css';
|
||||
275
Frontend/src/styles/theme.css
Normal file
275
Frontend/src/styles/theme.css
Normal file
@ -0,0 +1,275 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--font-size: 16px;
|
||||
--background: #ffffff;
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: #ffffff;
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: #030213;
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.95 0.0058 264.53);
|
||||
--secondary-foreground: #030213;
|
||||
--muted: #ececf0;
|
||||
--muted-foreground: #717182;
|
||||
--accent: #e9ebef;
|
||||
--accent-foreground: #030213;
|
||||
--destructive: #d4183d;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: rgba(0, 0, 0, 0.1);
|
||||
--input: transparent;
|
||||
--input-background: #f3f3f5;
|
||||
--switch-background: #cbced4;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-normal: 400;
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: #030213;
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-normal: 400;
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-input-background: var(--input-background);
|
||||
--color-switch-background: var(--switch-background);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--muted-foreground) 55%, transparent) transparent;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar,
|
||||
*::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track,
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb,
|
||||
*::-webkit-scrollbar-thumb {
|
||||
border-radius: 9999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: color-mix(in srgb, var(--muted-foreground) 42%, transparent);
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb:hover,
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: color-mix(in srgb, var(--foreground) 52%, transparent);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default typography styles for HTML elements (h1-h4, p, label, button, input).
|
||||
* These are in @layer base, so Tailwind utility classes (like text-sm, text-lg) automatically override them.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
|
||||
outline: 2px solid #4f46e5;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.responsive-table {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responsive-table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.responsive-table tbody {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.responsive-table tr {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(228 228 231);
|
||||
border-radius: 0.875rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.responsive-table td {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(6.5rem, 36%) minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0.55rem 0.75rem;
|
||||
}
|
||||
|
||||
.responsive-table td::before {
|
||||
content: attr(data-label);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: rgb(113 113 122);
|
||||
}
|
||||
|
||||
.responsive-table td[data-primary='true'] {
|
||||
display: block;
|
||||
border-bottom: 1px solid rgb(244 244 245);
|
||||
padding-top: 0.8rem;
|
||||
padding-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.responsive-table td[data-primary='true']::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.responsive-table td > :where(button, a) {
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
41
Frontend/vite.config.ts
Normal file
41
Frontend/vite.config.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import path from 'path'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
|
||||
function figmaAssetResolver() {
|
||||
return {
|
||||
name: 'figma-asset-resolver',
|
||||
resolveId(id) {
|
||||
if (id.startsWith('figma:asset/')) {
|
||||
const filename = id.replace('figma:asset/', '')
|
||||
return path.resolve(__dirname, 'src/assets', filename)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
figmaAssetResolver(),
|
||||
// The React and Tailwind plugins are both required for Make, even if
|
||||
// Tailwind is not being actively used – do not remove them
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
// Alias @ to the src directory
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
|
||||
// File types to support raw imports. Never add .css, .tsx, or .ts files to this.
|
||||
assetsInclude: ['**/*.svg', '**/*.csv'],
|
||||
test: {
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
exclude: ['e2e/**', 'node_modules/**'],
|
||||
},
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user