mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Add QML Material lib, demo email UI, and QML refactor
Add a large set of QML components (qml/Material, qml/MetaBuilder, qml/dbal) and a QmlComponents symlink for local development; migrate many frontends/qt6 files into qml/qt6. Replace the email client bootloader with a self-contained demo UI using FakeMUI primitives (MailboxLayout, ThreadList, EmailHeader, ComposeWindow), demo data, handlers, and new folder-navigation styles in globals.css. Update several QML component APIs to new signal/handler names (e.g. selectAllChanged→selectAllToggled, pageChanged→pageRequested, *Changed→*Edited) to standardize events. Add find_config_files() to frontends/qt6/generate_cmake.py to include config JS/JSON in QML/files and resources. Also add /frontends/qt6/_build to .gitignore.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -118,3 +118,4 @@ bun.lockb
|
||||
# frontends/pastebin/public/pyodide/ # kept in fat repo
|
||||
.vscode/settings.json
|
||||
.act-env
|
||||
/frontends/qt6/_build
|
||||
|
||||
1
QmlComponents
Symbolic link
1
QmlComponents
Symbolic link
@@ -0,0 +1 @@
|
||||
/Users/rmac/Documents/GitHub/metabuilder/qml
|
||||
@@ -1,212 +1,338 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BASE_PATH } from './lib/app-config'
|
||||
import { useAppDispatch } from '@metabuilder/redux-core'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import {
|
||||
MailboxLayout,
|
||||
FolderNavigation,
|
||||
type FolderNavigationItem,
|
||||
ThreadList,
|
||||
EmailHeader,
|
||||
ComposeWindow,
|
||||
} from '@metabuilder/fakemui/email'
|
||||
import {
|
||||
// Layout
|
||||
Box,
|
||||
// Surfaces
|
||||
Card,
|
||||
// Feedback
|
||||
Spinner, Alert,
|
||||
// Data Display
|
||||
Typography,
|
||||
IconButton,
|
||||
Button,
|
||||
} from '@metabuilder/fakemui'
|
||||
|
||||
/**
|
||||
* Email Client Main Page
|
||||
*
|
||||
* This is the bootloader page that loads the email_client package from packages/email_client/.
|
||||
* It handles:
|
||||
* - Loading package metadata and page configuration
|
||||
* - Initializing Redux state for email accounts, folders, and messages
|
||||
* - Rendering declarative UI from package page-config
|
||||
* - Wiring up email hooks and Redux state
|
||||
*/
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Demo data — replace with useMessages/useMailboxes hooks when IMAP backend is ready
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface PageConfig {
|
||||
type: string
|
||||
props?: Record<string, unknown>
|
||||
children?: unknown
|
||||
}
|
||||
const DEMO_FOLDERS: FolderNavigationItem[] = [
|
||||
{ id: 'inbox', label: 'Inbox', icon: '📥', unreadCount: 3, isActive: true },
|
||||
{ id: 'starred', label: 'Starred', icon: '⭐' },
|
||||
{ id: 'sent', label: 'Sent', icon: '📤' },
|
||||
{ id: 'drafts', label: 'Drafts', icon: '📝', unreadCount: 1 },
|
||||
{ id: 'spam', label: 'Spam', icon: '⚠️' },
|
||||
{ id: 'trash', label: 'Trash', icon: '🗑️' },
|
||||
]
|
||||
|
||||
interface PackageMetadata {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
pageConfig?: PageConfig
|
||||
}
|
||||
const now = Date.now()
|
||||
const hour = 3600000
|
||||
const day = 86400000
|
||||
|
||||
async function loadEmailClientPackage(): Promise<PackageMetadata> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_PATH}/api/v1/packages/email_client/metadata`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load email_client package: ${response.statusText}`)
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Error loading email_client package:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const DEMO_EMAILS = [
|
||||
{
|
||||
id: '1',
|
||||
testId: '1',
|
||||
from: 'Alice Chen',
|
||||
to: ['you@metabuilder.io'],
|
||||
subject: 'Sprint planning for next week',
|
||||
preview: 'Hey team, I\'ve put together the backlog items we need to discuss in our sprint planning session...',
|
||||
receivedAt: now - 2 * hour,
|
||||
isRead: false,
|
||||
isStarred: true,
|
||||
body: 'Hey team,\n\nI\'ve put together the backlog items we need to discuss in our sprint planning session on Monday. Please review the board before the meeting.\n\nKey items:\n- DBAL multi-adapter support\n- Email client backend integration\n- Workflow engine performance improvements\n\nLet me know if you have anything to add.\n\nBest,\nAlice',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
testId: '2',
|
||||
from: 'GitHub',
|
||||
to: ['you@metabuilder.io'],
|
||||
subject: '[metabuilder] PR #847: Fix loadFromDirectory tenantId auto-add',
|
||||
preview: 'mergify[bot] merged pull request #847. loadFromDirectory() was missing the tenantId field...',
|
||||
receivedAt: now - 5 * hour,
|
||||
isRead: false,
|
||||
isStarred: false,
|
||||
body: 'mergify[bot] merged pull request #847.\n\nloadFromDirectory() was missing the tenantId field auto-add logic that loadFromFile() already had, causing "Unknown field: tenantId" on all entities using the shorthand convention.\n\nFiles changed: 2\nAdditions: 51\nDeletions: 22',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
testId: '3',
|
||||
from: 'Bob Martinez',
|
||||
to: ['you@metabuilder.io'],
|
||||
cc: ['alice@metabuilder.io'],
|
||||
subject: 'Re: Database adapter benchmarks',
|
||||
preview: 'The PostgreSQL adapter is showing 3x throughput improvement with the new connection pool settings...',
|
||||
receivedAt: now - 1 * day,
|
||||
isRead: false,
|
||||
isStarred: false,
|
||||
body: 'The PostgreSQL adapter is showing 3x throughput improvement with the new connection pool settings. Here are the results:\n\n- SQLite: 12,400 ops/sec (dev baseline)\n- PostgreSQL: 8,900 ops/sec → 26,700 ops/sec\n- MySQL: 7,200 ops/sec (unchanged)\n\nI\'ll push the config changes to the deployment branch.\n\nBob',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
testId: '4',
|
||||
from: 'Docker Hub',
|
||||
to: ['you@metabuilder.io'],
|
||||
subject: 'Build succeeded: metabuilder/dbal:latest',
|
||||
preview: 'Your automated build for metabuilder/dbal has completed successfully. Image size: 487MB...',
|
||||
receivedAt: now - 1 * day - 3 * hour,
|
||||
isRead: true,
|
||||
isStarred: false,
|
||||
body: 'Your automated build for metabuilder/dbal has completed successfully.\n\nImage: metabuilder/dbal:latest\nSize: 487MB\nBuild time: 4m 23s\nLayers: 12',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
testId: '5',
|
||||
from: 'Carol Wu',
|
||||
to: ['you@metabuilder.io'],
|
||||
subject: 'FakeMUI component audit results',
|
||||
preview: 'Finished the component audit — 167 components across 11 categories. All tests passing...',
|
||||
receivedAt: now - 2 * day,
|
||||
isRead: true,
|
||||
isStarred: true,
|
||||
body: 'Finished the component audit — 167 components across 11 categories. All tests passing.\n\nBreakdown:\n- Core React: 145 components\n- Email: 22 components\n- Icons: 421\n- SCSS modules: 78\n\nThe QML ports are at 104 components. Python bindings cover 15 of the most-used ones.\n\nCarol',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
testId: '6',
|
||||
from: 'Sentry',
|
||||
to: ['you@metabuilder.io'],
|
||||
subject: 'New issue: DBAL fetch failed: 422 Unprocessable Entity',
|
||||
preview: 'A new error was detected in codegen-frontend. fetchFromDBAL threw an error for unknown entity...',
|
||||
receivedAt: now - 3 * day,
|
||||
isRead: true,
|
||||
isStarred: false,
|
||||
body: 'A new error was detected in codegen-frontend.\n\nError: DBAL fetch failed: 422 Unprocessable Entity\nFile: src/store/middleware/dbalSync.ts:131\nOccurrences: 47\nUsers affected: 3\n\nStack trace:\nfetchFromDBAL @ dbalSync.ts:131\nloadSettings @ layout.tsx:33',
|
||||
},
|
||||
]
|
||||
|
||||
async function loadPageConfig(packageId: string): Promise<PageConfig> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_PATH}/api/v1/packages/${packageId}/page-config`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load page config: ${response.statusText}`)
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Error loading page config:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic component renderer
|
||||
* Renders declarative component definitions from JSON
|
||||
*/
|
||||
function RenderComponent({ component }: { component: PageConfig }): React.JSX.Element {
|
||||
const { type, props = {}, children } = component
|
||||
|
||||
// Map component types to FakeMUI components
|
||||
const componentMap: Record<string, React.ElementType> = {
|
||||
// Layout
|
||||
'Box': Box,
|
||||
|
||||
// Surfaces
|
||||
'Card': Card,
|
||||
'Paper': Card,
|
||||
|
||||
// Data Display
|
||||
'Typography': Typography,
|
||||
|
||||
// Feedback
|
||||
'Spinner': Spinner,
|
||||
'Loader': Spinner,
|
||||
'Alert': Alert,
|
||||
|
||||
// Fallback
|
||||
'Fragment': React.Fragment
|
||||
}
|
||||
|
||||
const Component = componentMap[type] || Box
|
||||
const componentProps = props as Record<string, unknown>
|
||||
|
||||
return (
|
||||
<Component {...componentProps}>
|
||||
{children && Array.isArray(children) ? (
|
||||
children.map((child, idx) => (
|
||||
typeof child === 'string' ? (
|
||||
<span key={idx}>{child}</span>
|
||||
) : (
|
||||
<RenderComponent key={idx} component={child as PageConfig} />
|
||||
)
|
||||
))
|
||||
) : children ? (
|
||||
<RenderComponent component={children as PageConfig} />
|
||||
) : null}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Email Client App
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function EmailClientContent() {
|
||||
const dispatch = useAppDispatch()
|
||||
const [packageMetadata, setPackageMetadata] = useState<PackageMetadata | null>(null)
|
||||
const [pageConfig, setPageConfig] = useState<PageConfig | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [activeFolder, setActiveFolder] = useState('inbox')
|
||||
const [selectedEmailId, setSelectedEmailId] = useState<string | null>(null)
|
||||
const [emails, setEmails] = useState(DEMO_EMAILS)
|
||||
const [showCompose, setShowCompose] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// Load package metadata and page config on mount
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const folders = DEMO_FOLDERS.map(f => ({
|
||||
...f,
|
||||
isActive: f.id === activeFolder,
|
||||
}))
|
||||
|
||||
// Load package metadata
|
||||
const metadata = await loadEmailClientPackage()
|
||||
setPackageMetadata(metadata)
|
||||
const selectedEmail = emails.find(e => e.id === selectedEmailId) || null
|
||||
|
||||
// Load page configuration
|
||||
const config = await loadPageConfig('email_client')
|
||||
setPageConfig(config)
|
||||
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Unknown error loading email client')
|
||||
setError(error)
|
||||
console.error('Failed to load email client:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
const filteredEmails = emails.filter(e => {
|
||||
if (activeFolder === 'starred') return e.isStarred
|
||||
if (activeFolder === 'sent') return false
|
||||
if (activeFolder === 'drafts') return false
|
||||
if (activeFolder === 'spam') return false
|
||||
if (activeFolder === 'trash') return false
|
||||
// inbox
|
||||
if (searchQuery) {
|
||||
const q = searchQuery.toLowerCase()
|
||||
return (
|
||||
e.from.toLowerCase().includes(q) ||
|
||||
e.subject.toLowerCase().includes(q) ||
|
||||
e.preview.toLowerCase().includes(q)
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
load()
|
||||
}, [dispatch])
|
||||
const handleSelectEmail = useCallback((emailId: string) => {
|
||||
setSelectedEmailId(emailId)
|
||||
// Mark as read
|
||||
setEmails(prev => prev.map(e => e.id === emailId ? { ...e, isRead: true } : e))
|
||||
}, [])
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '100vh',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
<span>Loading Email Client...</span>
|
||||
const handleToggleRead = useCallback((emailId: string, read: boolean) => {
|
||||
setEmails(prev => prev.map(e => e.id === emailId ? { ...e, isRead: read } : e))
|
||||
}, [])
|
||||
|
||||
const handleToggleStar = useCallback((emailId: string, starred: boolean) => {
|
||||
setEmails(prev => prev.map(e => e.id === emailId ? { ...e, isStarred: starred } : e))
|
||||
}, [])
|
||||
|
||||
const handleSend = useCallback((data: { to: string[]; cc?: string[]; bcc?: string[]; subject: string; body: string }) => {
|
||||
const newEmail = {
|
||||
id: String(Date.now()),
|
||||
from: 'You',
|
||||
to: data.to,
|
||||
subject: data.subject,
|
||||
preview: data.body.slice(0, 100),
|
||||
receivedAt: Date.now(),
|
||||
isRead: true,
|
||||
isStarred: false,
|
||||
body: data.body,
|
||||
}
|
||||
setEmails(prev => [newEmail, ...prev])
|
||||
setShowCompose(false)
|
||||
}, [])
|
||||
|
||||
const handleNavigateFolder = useCallback((folderId: string) => {
|
||||
setActiveFolder(folderId)
|
||||
setSelectedEmailId(null)
|
||||
}, [])
|
||||
|
||||
// ── Sidebar ──────────────────────────────────────────────────────────────
|
||||
const sidebar = (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Box sx={{ padding: '16px' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setShowCompose(true)}
|
||||
sx={{ width: '100%', borderRadius: '24px', padding: '12px 24px', fontSize: '0.9375rem', fontWeight: 500, textTransform: 'none', boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
|
||||
>
|
||||
✏️ Compose
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<Box sx={{ padding: 3 }}>
|
||||
<Alert severity="error">
|
||||
<strong>Failed to load Email Client</strong>
|
||||
<p>{error.message}</p>
|
||||
</Alert>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// No package metadata
|
||||
if (!packageMetadata) {
|
||||
return (
|
||||
<Box sx={{ padding: 3 }}>
|
||||
<Alert severity="warning">
|
||||
<strong>Email Client Package Not Found</strong>
|
||||
<p>The email_client package could not be loaded. Please ensure it is installed and configured.</p>
|
||||
</Alert>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Render default layout if no page config
|
||||
if (!pageConfig) {
|
||||
return (
|
||||
<Box sx={{ padding: 3 }}>
|
||||
<h1>{packageMetadata.name}</h1>
|
||||
<p>{packageMetadata.description}</p>
|
||||
<p>Version: {packageMetadata.version}</p>
|
||||
<Alert severity="info">
|
||||
Page configuration is loading or not available. Please check back soon.
|
||||
</Alert>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Render declarative component from page config
|
||||
return (
|
||||
<Box component="main" sx={{ minHeight: '100vh' }}>
|
||||
<RenderComponent component={pageConfig} />
|
||||
<FolderNavigation items={folders} onNavigate={handleNavigateFolder} />
|
||||
</Box>
|
||||
)
|
||||
|
||||
// ── Header ───────────────────────────────────────────────────────────────
|
||||
const header = (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '12px', width: '100%', padding: '0 8px' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '24px' }}>📧</span>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '1.125rem' }}>
|
||||
MetaMail
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, maxWidth: '680px', margin: '0 auto' }}>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search mail"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 16px',
|
||||
borderRadius: '24px',
|
||||
border: 'none',
|
||||
backgroundColor: '#f1f3f4',
|
||||
fontSize: '0.9375rem',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<IconButton aria-label="Settings" title="Settings">
|
||||
<span style={{ fontSize: '20px' }}>⚙️</span>
|
||||
</IconButton>
|
||||
<Box sx={{
|
||||
width: '32px', height: '32px', borderRadius: '50%',
|
||||
backgroundColor: '#1976d2', color: 'white',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: '0.875rem', fontWeight: 600, cursor: 'pointer',
|
||||
}}>
|
||||
U
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
// ── Main (thread list) ───────────────────────────────────────────────────
|
||||
const main = (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ padding: '8px 16px', borderBottom: '1px solid #e0e0e0', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2" sx={{ color: '#5f6368', fontWeight: 500, textTransform: 'capitalize' }}>
|
||||
{activeFolder} {filteredEmails.length > 0 && `(${filteredEmails.length})`}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#5f6368' }}>
|
||||
{filteredEmails.filter(e => !e.isRead).length} unread
|
||||
</Typography>
|
||||
</Box>
|
||||
{filteredEmails.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', flex: 1, color: '#5f6368', gap: '8px' }}>
|
||||
<span style={{ fontSize: '48px', opacity: 0.5 }}>
|
||||
{activeFolder === 'starred' ? '⭐' : activeFolder === 'trash' ? '🗑️' : '📭'}
|
||||
</span>
|
||||
<Typography variant="body1">
|
||||
{activeFolder === 'inbox' && searchQuery ? 'No results found' : `No messages in ${activeFolder}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<ThreadList
|
||||
emails={filteredEmails}
|
||||
selectedEmailId={selectedEmailId || undefined}
|
||||
onSelectEmail={handleSelectEmail}
|
||||
onToggleRead={handleToggleRead}
|
||||
onToggleStar={handleToggleStar}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
||||
// ── Detail (selected email) ──────────────────────────────────────────────
|
||||
const detail = selectedEmail ? (
|
||||
<Box sx={{ height: '100%', overflow: 'auto', padding: '16px' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
|
||||
<button
|
||||
onClick={() => setSelectedEmailId(null)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.25rem', padding: '4px' }}
|
||||
aria-label="Back to list"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<Box sx={{ display: 'flex', gap: '4px' }}>
|
||||
<IconButton aria-label="Archive" title="Archive">
|
||||
<span style={{ fontSize: '18px' }}>📥</span>
|
||||
</IconButton>
|
||||
<IconButton aria-label="Delete" title="Delete">
|
||||
<span style={{ fontSize: '18px' }}>🗑️</span>
|
||||
</IconButton>
|
||||
<IconButton aria-label="Reply" title="Reply">
|
||||
<span style={{ fontSize: '18px' }}>↩️</span>
|
||||
</IconButton>
|
||||
<IconButton aria-label="Forward" title="Forward">
|
||||
<span style={{ fontSize: '18px' }}>↪️</span>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<EmailHeader
|
||||
from={selectedEmail.from}
|
||||
to={selectedEmail.to || ['you@metabuilder.io']}
|
||||
cc={selectedEmail.cc}
|
||||
subject={selectedEmail.subject}
|
||||
receivedAt={selectedEmail.receivedAt}
|
||||
isStarred={selectedEmail.isStarred}
|
||||
onToggleStar={(starred) => handleToggleStar(selectedEmail.id, starred)}
|
||||
/>
|
||||
<Box sx={{ marginTop: '24px', fontSize: '0.9375rem', lineHeight: 1.7, whiteSpace: 'pre-wrap', color: '#3c4043' }}>
|
||||
{selectedEmail.body}
|
||||
</Box>
|
||||
<Box sx={{ marginTop: '32px', borderTop: '1px solid #e0e0e0', paddingTop: '16px' }}>
|
||||
<Button variant="outlined" onClick={() => setShowCompose(true)} sx={{ borderRadius: '20px', textTransform: 'none' }}>
|
||||
↩️ Reply
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={() => setShowCompose(true)} sx={{ borderRadius: '20px', textTransform: 'none', marginLeft: '8px' }}>
|
||||
↪️ Forward
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<MailboxLayout
|
||||
header={header}
|
||||
sidebar={sidebar}
|
||||
main={main}
|
||||
detail={detail}
|
||||
/>
|
||||
{showCompose && (
|
||||
<ComposeWindow
|
||||
onSend={handleSend}
|
||||
onClose={() => setShowCompose(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -360,6 +360,62 @@ th {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Folder Navigation */
|
||||
.folder-navigation {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.folder-nav-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.folder-nav-item {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 0 24px 24px 0 !important;
|
||||
text-align: left !important;
|
||||
justify-content: flex-start !important;
|
||||
font-size: 0.875rem !important;
|
||||
font-weight: 500 !important;
|
||||
text-transform: none !important;
|
||||
color: #3c4043 !important;
|
||||
min-height: 32px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.folder-nav-item:hover {
|
||||
background-color: #e8eaed !important;
|
||||
}
|
||||
|
||||
.folder-nav-item .folder-icon {
|
||||
font-size: 1rem;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.folder-nav-item .folder-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.folder-nav-item .unread-count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #3c4043;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Active folder */
|
||||
.folder-nav-item[class*="primary"] {
|
||||
background-color: #d3e3fd !important;
|
||||
color: #001d35 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.compose-window {
|
||||
|
||||
@@ -2,7 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
import MetaBuilder 1.0
|
||||
import "qmllib/MetaBuilder"
|
||||
import "config/GodPanelConfig.js" as GodPanelConfig
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -100,6 +100,19 @@ def find_audio_assets(root_dir: Path) -> list[str]:
|
||||
return [str(f.relative_to(root_dir)) for f in files if f.is_file()]
|
||||
|
||||
|
||||
def find_config_files(root_dir: Path) -> dict[str, list[str]]:
|
||||
"""Find config/ files: JS goes into QML_FILES, JSON into RESOURCES."""
|
||||
config_dir = root_dir / "config"
|
||||
result = {"qml": [], "resources": []}
|
||||
if not config_dir.exists():
|
||||
return result
|
||||
for f in sorted(config_dir.rglob("*.js")):
|
||||
result["qml"].append(str(f.relative_to(root_dir)))
|
||||
for f in sorted(config_dir.rglob("*.json")):
|
||||
result["resources"].append(str(f.relative_to(root_dir)))
|
||||
return result
|
||||
|
||||
|
||||
def find_cpp_sources(root_dir: Path) -> dict[str, list[str]]:
|
||||
"""Find all *.cpp and *.h files in src/."""
|
||||
src_dir = root_dir / "src"
|
||||
|
||||
@@ -37,7 +37,7 @@ CCard {
|
||||
signal editClicked(int index, var record)
|
||||
signal deleteClicked(int index, var record)
|
||||
signal pageRequested(int newPage)
|
||||
signal selectAllChanged(bool checked)
|
||||
signal selectAllToggled(bool checked)
|
||||
signal rowSelectionChanged(var selectedRows)
|
||||
|
||||
Layout.fillWidth: true
|
||||
@@ -59,7 +59,7 @@ CCard {
|
||||
}
|
||||
root.selectedRows = newSel;
|
||||
root.rowSelectionChanged(newSel);
|
||||
root.selectAllChanged(checked);
|
||||
root.selectAllToggled(checked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ CCard {
|
||||
page: root.page
|
||||
pageSize: root.pageSize
|
||||
totalFiltered: root.totalFiltered
|
||||
onPageChanged: function(newPage) { root.pageRequested(newPage) }
|
||||
onPageRequested: function(newPage) { root.pageRequested(newPage) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ CCard {
|
||||
property var encryptionOptions: ["None", "TLS", "SSL"]
|
||||
property string connectionStatus: "untested"
|
||||
|
||||
signal hostChanged(string value)
|
||||
signal portChanged(string value)
|
||||
signal usernameChanged(string value)
|
||||
signal passwordChanged(string value)
|
||||
signal encryptionChanged(int index)
|
||||
signal hostEdited(string value)
|
||||
signal portEdited(string value)
|
||||
signal usernameEdited(string value)
|
||||
signal passwordEdited(string value)
|
||||
signal encryptionEdited(int index)
|
||||
signal testRequested()
|
||||
|
||||
ColumnLayout {
|
||||
@@ -36,7 +36,7 @@ CCard {
|
||||
label: "Host"
|
||||
placeholderText: "smtp.example.com"
|
||||
text: root.host
|
||||
onTextChanged: root.hostChanged(text)
|
||||
onTextChanged: root.hostEdited(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
@@ -44,7 +44,7 @@ CCard {
|
||||
label: "Port"
|
||||
placeholderText: "587"
|
||||
text: root.port
|
||||
onTextChanged: root.portChanged(text)
|
||||
onTextChanged: root.portEdited(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
@@ -52,7 +52,7 @@ CCard {
|
||||
label: "Username"
|
||||
placeholderText: "user@example.com"
|
||||
text: root.username
|
||||
onTextChanged: root.usernameChanged(text)
|
||||
onTextChanged: root.usernameEdited(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
@@ -61,7 +61,7 @@ CCard {
|
||||
placeholderText: "Enter password"
|
||||
echoMode: TextInput.Password
|
||||
text: root.password
|
||||
onTextChanged: root.passwordChanged(text)
|
||||
onTextChanged: root.passwordEdited(text)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -80,7 +80,7 @@ CCard {
|
||||
text: modelData
|
||||
variant: root.encryptionIndex === index ? "primary" : "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.encryptionChanged(index)
|
||||
onClicked: root.encryptionEdited(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ Rectangle {
|
||||
property int pageSize: 5
|
||||
property int totalFiltered: 0
|
||||
|
||||
signal pageChanged(int page)
|
||||
signal pageRequested(int newPage)
|
||||
|
||||
readonly property int _totalPages: Math.max(1, Math.ceil(totalFiltered / pageSize))
|
||||
|
||||
@@ -55,7 +55,7 @@ Rectangle {
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
enabled: root.page > 0
|
||||
onClicked: root.pageChanged(root.page - 1)
|
||||
onClicked: root.pageRequested(root.page - 1)
|
||||
}
|
||||
|
||||
CText {
|
||||
@@ -69,7 +69,7 @@ Rectangle {
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
enabled: root.page < root._totalPages - 1
|
||||
onClicked: root.pageChanged(root.page + 1)
|
||||
onClicked: root.pageRequested(root.page + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ CCard {
|
||||
property string customSuccess: "#000000"
|
||||
property string customInfo: "#000000"
|
||||
|
||||
signal colorChanged(string token, string value)
|
||||
signal tokenColorEdited(string token, string value)
|
||||
|
||||
// Inline color field component
|
||||
component ColorField: RowLayout {
|
||||
@@ -88,57 +88,57 @@ CCard {
|
||||
ColorField {
|
||||
label: "Primary"
|
||||
colorValue: customPrimary
|
||||
onColorEdited: function(val) { colorChanged("primary", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("primary", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Background"
|
||||
colorValue: customBackground
|
||||
onColorEdited: function(val) { colorChanged("background", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("background", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Surface"
|
||||
colorValue: customSurface
|
||||
onColorEdited: function(val) { colorChanged("surface", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("surface", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Paper"
|
||||
colorValue: customPaper
|
||||
onColorEdited: function(val) { colorChanged("paper", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("paper", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Text"
|
||||
colorValue: customText
|
||||
onColorEdited: function(val) { colorChanged("text", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("text", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Text Secondary"
|
||||
colorValue: customTextSecondary
|
||||
onColorEdited: function(val) { colorChanged("textSecondary", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("textSecondary", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Border"
|
||||
colorValue: customBorder
|
||||
onColorEdited: function(val) { colorChanged("border", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("border", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Error"
|
||||
colorValue: customError
|
||||
onColorEdited: function(val) { colorChanged("error", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("error", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Warning"
|
||||
colorValue: customWarning
|
||||
onColorEdited: function(val) { colorChanged("warning", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("warning", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Success"
|
||||
colorValue: customSuccess
|
||||
onColorEdited: function(val) { colorChanged("success", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("success", val) }
|
||||
}
|
||||
ColorField {
|
||||
label: "Info"
|
||||
colorValue: customInfo
|
||||
onColorEdited: function(val) { colorChanged("info", val) }
|
||||
onColorEdited: function(val) { tokenColorEdited("info", val) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ CCard {
|
||||
property string fontFamily: "Inter"
|
||||
property int baseFontSize: 14
|
||||
|
||||
signal fontFamilyChanged(string family)
|
||||
signal baseFontSizeChanged(int size)
|
||||
signal fontFamilyEdited(string family)
|
||||
signal baseFontSizeEdited(int size)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
@@ -36,7 +36,7 @@ CCard {
|
||||
label: "Font Family"
|
||||
placeholderText: "e.g., Inter, Roboto, system-ui"
|
||||
text: fontFamily
|
||||
onTextChanged: fontFamilyChanged(text)
|
||||
onTextChanged: fontFamilyEdited(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ CCard {
|
||||
to: 24
|
||||
stepSize: 1
|
||||
value: baseFontSize
|
||||
onValueChanged: baseFontSizeChanged(value)
|
||||
onValueChanged: baseFontSizeEdited(value)
|
||||
|
||||
background: Rectangle {
|
||||
x: parent.leftPadding
|
||||
|
||||
68
qml/Material/MaterialAccordion.qml
Normal file
68
qml/Material/MaterialAccordion.qml
Normal file
@@ -0,0 +1,68 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: accordion
|
||||
property string headerText: ""
|
||||
property string summaryText: ""
|
||||
property bool expanded: false
|
||||
signal toggled(bool expanded)
|
||||
|
||||
width: parent ? parent.width : 360
|
||||
color: MaterialPalette.surface
|
||||
radius: 12
|
||||
border.color: MaterialPalette.outline
|
||||
border.width: 1
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
Text {
|
||||
text: headerText
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: MaterialPalette.onSurface
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Button {
|
||||
text: accordion.expanded ? "−" : "+"
|
||||
font.pixelSize: 16
|
||||
width: 36
|
||||
height: 36
|
||||
background: Rectangle {
|
||||
radius: 18
|
||||
color: MaterialPalette.primaryContainer
|
||||
}
|
||||
onClicked: {
|
||||
accordion.expanded = !accordion.expanded
|
||||
accordion.toggled(accordion.expanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: summaryText
|
||||
font.pixelSize: 14
|
||||
color: MaterialPalette.onSurface
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
visible: accordion.expanded
|
||||
asynchronous: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: accordion.expanded ? implicitHeight : 0
|
||||
}
|
||||
}
|
||||
|
||||
default property alias content: contentLoader.sourceComponent
|
||||
}
|
||||
75
qml/Material/MaterialAlert.qml
Normal file
75
qml/Material/MaterialAlert.qml
Normal file
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: alert
|
||||
property alias title: titleText.text
|
||||
property alias message: messageText.text
|
||||
property string severity: "info"
|
||||
property bool dismissible: false
|
||||
signal dismissed()
|
||||
radius: 12
|
||||
color: severity === "success" ? MaterialPalette.primaryContainer :
|
||||
severity === "error" ? MaterialPalette.error :
|
||||
severity === "warning" ? MaterialPalette.secondaryContainer :
|
||||
MaterialPalette.surfaceVariant
|
||||
border.color: MaterialPalette.outline
|
||||
border.width: 1
|
||||
padding: 18
|
||||
implicitHeight: content.implicitHeight + 12
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: severity === "success" ? MaterialPalette.primary :
|
||||
severity === "error" ? MaterialPalette.error :
|
||||
severity === "warning" ? MaterialPalette.secondary : MaterialPalette.primary
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: severity === "success" ? "✓" : severity === "error" ? "!" : severity === "warning" ? "!" : "ℹ"
|
||||
color: "#fff"
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
Text {
|
||||
id: titleText
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: MaterialPalette.onSurface
|
||||
}
|
||||
Text {
|
||||
id: messageText
|
||||
font.pixelSize: 14
|
||||
color: MaterialPalette.onSurface
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: dismissible
|
||||
text: "Close"
|
||||
font.pixelSize: 12
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
onClicked: alert.dismissed()
|
||||
}
|
||||
}
|
||||
}
|
||||
24
qml/Material/MaterialAppBar.qml
Normal file
24
qml/Material/MaterialAppBar.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: appBar
|
||||
property bool elevated: true
|
||||
property alias content: layout.data
|
||||
property real appBarHeight: 64
|
||||
height: appBarHeight
|
||||
width: parent ? parent.width : 640
|
||||
color: MaterialPalette.surface
|
||||
border.color: MaterialPalette.outline
|
||||
border.width: 1
|
||||
radius: 0
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 16
|
||||
}
|
||||
}
|
||||
35
qml/Material/MaterialAvatar.qml
Normal file
35
qml/Material/MaterialAvatar.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: avatar
|
||||
property url source: ""
|
||||
property string initials: ""
|
||||
property color backgroundColor: MaterialPalette.secondaryContainer
|
||||
radius: width / 2
|
||||
color: backgroundColor
|
||||
border.color: MaterialPalette.outline
|
||||
border.width: 1
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
source: avatar.source
|
||||
visible: source.length > 0
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
clip: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: source.length === 0
|
||||
text: avatar.initials
|
||||
color: MaterialPalette.onSurface
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
44
qml/Material/MaterialBadge.qml
Normal file
44
qml/Material/MaterialBadge.qml
Normal file
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: badge
|
||||
property string text: ""
|
||||
property url iconSource: ""
|
||||
property bool accent: false
|
||||
property bool outlined: false
|
||||
property bool dense: false
|
||||
|
||||
height: dense ? 24 : 28
|
||||
radius: height / 2
|
||||
implicitWidth: label.width + (iconSource.length > 0 ? 32 : 20)
|
||||
color: outlined ? "transparent" : (accent ? MaterialPalette.secondaryContainer : MaterialPalette.surfaceVariant)
|
||||
border.color: outlined ? MaterialPalette.secondary : "transparent"
|
||||
border.width: outlined ? 1 : 0
|
||||
|
||||
RowLayout {
|
||||
id: wrapper
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
spacing: iconSource.length > 0 ? 6 : 0
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
Image {
|
||||
source: iconSource
|
||||
visible: iconSource.length > 0
|
||||
width: 16
|
||||
height: 16
|
||||
fillMode: Image.PreserveAspectFit
|
||||
opacity: 0.85
|
||||
}
|
||||
|
||||
Text {
|
||||
id: label
|
||||
text: badge.text
|
||||
font.pixelSize: dense ? 12 : 14
|
||||
color: accent ? MaterialPalette.secondary : MaterialPalette.onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
17
qml/Material/MaterialBox.qml
Normal file
17
qml/Material/MaterialBox.qml
Normal file
@@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: box
|
||||
property alias content: layout.data
|
||||
color: "transparent"
|
||||
radius: 10
|
||||
border.width: 0
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 10
|
||||
}
|
||||
}
|
||||
87
qml/Material/MaterialButton.qml
Normal file
87
qml/Material/MaterialButton.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property alias text: label.text
|
||||
property bool outlined: false
|
||||
property bool elevated: true
|
||||
property string iconPath: ""
|
||||
property color fillColor: outlined ? "transparent" : MaterialPalette.primary
|
||||
property color borderColor: outlined ? MaterialPalette.outline : "transparent"
|
||||
property color textColor: outlined ? MaterialPalette.primary : MaterialPalette.onPrimary
|
||||
property color rippleColor: outlined ? MaterialPalette.primary : MaterialPalette.onPrimary
|
||||
property bool disabled: false
|
||||
property real cornerRadius: 12
|
||||
signal clicked()
|
||||
|
||||
implicitHeight: 48
|
||||
implicitWidth: contentRow.implicitWidth + 32
|
||||
radius: cornerRadius
|
||||
color: disabled ? MaterialPalette.surfaceVariant : fillColor
|
||||
border.color: disabled ? MaterialPalette.surfaceVariant : borderColor
|
||||
border.width: outlined ? 1 : 0
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
id: ripple
|
||||
anchors.fill: parent
|
||||
color: rippleColor
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: iconPath.length > 0 ? 8 : 0
|
||||
|
||||
Image {
|
||||
source: iconPath
|
||||
visible: iconPath.length > 0
|
||||
width: 20
|
||||
height: 20
|
||||
fillMode: Image.PreserveAspectFit
|
||||
opacity: disabled ? 0.5 : 1
|
||||
}
|
||||
|
||||
Text {
|
||||
id: label
|
||||
text: root.text
|
||||
font.pixelSize: 15
|
||||
font.bold: true
|
||||
color: disabled ? MaterialPalette.surface : textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: rippleFade
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
from: 0.3
|
||||
to: 0
|
||||
duration: 360
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !disabled
|
||||
hoverEnabled: true
|
||||
onClicked: root.clicked()
|
||||
onPressed: {
|
||||
root.opacity = 0.85
|
||||
ripple.opacity = 0.35
|
||||
rippleFade.running = false
|
||||
rippleFade.start()
|
||||
}
|
||||
onReleased: root.opacity = 1
|
||||
}
|
||||
}
|
||||
24
qml/Material/MaterialCard.qml
Normal file
24
qml/Material/MaterialCard.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
property alias contentItem: contentLoader.sourceComponent
|
||||
property real cornerRadius: 16
|
||||
property color surfaceColor: MaterialPalette.surface
|
||||
property real elevation: MaterialPalette.elevationLow
|
||||
|
||||
width: parent ? parent.width : 320
|
||||
radius: cornerRadius
|
||||
color: surfaceColor
|
||||
border.color: MaterialPalette.outline
|
||||
border.width: 1
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
}
|
||||
}
|
||||
34
qml/Material/MaterialCheckbox.qml
Normal file
34
qml/Material/MaterialCheckbox.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
CheckBox {
|
||||
id: checkbox
|
||||
property alias label: labelText.text
|
||||
property color checkColor: MaterialPalette.primary
|
||||
indicator: Rectangle {
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 4
|
||||
border.color: checkbox.checked ? checkColor : MaterialPalette.outline
|
||||
border.width: 1
|
||||
color: checkbox.checked ? checkColor : MaterialPalette.surfaceVariant
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: checkbox.checked ? "✓" : ""
|
||||
font.pixelSize: 14
|
||||
color: "#fff"
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
text: ""
|
||||
anchors.left: indicator.right
|
||||
anchors.leftMargin: 8
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: MaterialPalette.onSurface
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
27
qml/Material/MaterialChip.qml
Normal file
27
qml/Material/MaterialChip.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: chip
|
||||
property string text: ""
|
||||
property color fillColor: MaterialPalette.surfaceVariant
|
||||
property color textColor: MaterialPalette.onSurface
|
||||
property bool outlined: false
|
||||
|
||||
radius: 999
|
||||
height: 32
|
||||
implicitWidth: label.width + 32
|
||||
color: outlined ? "transparent" : fillColor
|
||||
border.color: outlined ? MaterialPalette.outline : "transparent"
|
||||
border.width: outlined ? 1 : 0
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: 14
|
||||
text: chip.text
|
||||
color: textColor
|
||||
}
|
||||
}
|
||||
18
qml/Material/MaterialCircularProgress.qml
Normal file
18
qml/Material/MaterialCircularProgress.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
BusyIndicator {
|
||||
id: indicator
|
||||
running: true
|
||||
width: 48
|
||||
height: 48
|
||||
anchors.centerIn: parent
|
||||
busyIndicatorStyle: BusyIndicatorStyle {
|
||||
indicator {
|
||||
color: MaterialPalette.primary
|
||||
width: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
19
qml/Material/MaterialCollapse.qml
Normal file
19
qml/Material/MaterialCollapse.qml
Normal file
@@ -0,0 +1,19 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: collapse
|
||||
property bool expanded: false
|
||||
property Component contentComponent
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
sourceComponent: expanded ? contentComponent : null
|
||||
}
|
||||
|
||||
height: expanded ? loader.implicitHeight : 0
|
||||
Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.OutQuad } }
|
||||
}
|
||||
18
qml/Material/MaterialContainer.qml
Normal file
18
qml/Material/MaterialContainer.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: container
|
||||
color: "transparent"
|
||||
radius: 12
|
||||
border.width: 0
|
||||
width: parent ? parent.width : 640
|
||||
property real maxWidth: 1040
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
default property alias content: data
|
||||
}
|
||||
}
|
||||
64
qml/Material/MaterialDialog.qml
Normal file
64
qml/Material/MaterialDialog.qml
Normal file
@@ -0,0 +1,64 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Dialog {
|
||||
id: dialog
|
||||
modal: true
|
||||
focus: true
|
||||
property alias title: titleText.text
|
||||
property alias description: descriptionText.text
|
||||
signal actionTriggered(string id)
|
||||
|
||||
contentItem: Rectangle {
|
||||
color: MaterialPalette.surface
|
||||
radius: 16
|
||||
border.color: MaterialPalette.outline
|
||||
border.width: 1
|
||||
width: 480
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 24
|
||||
spacing: 12
|
||||
default property alias actionItems: actionsRow.data
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
Text {
|
||||
id: titleText
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
color: MaterialPalette.onSurface
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Button {
|
||||
text: "Close"
|
||||
onClicked: dialog.close()
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: descriptionText
|
||||
font.pixelSize: 14
|
||||
color: MaterialPalette.onSurface
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: actionsRow
|
||||
spacing: 10
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
qml/Material/MaterialDivider.qml
Normal file
11
qml/Material/MaterialDivider.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: divider
|
||||
height: 1
|
||||
width: parent ? parent.width : 100
|
||||
color: MaterialPalette.outline
|
||||
radius: 0.5
|
||||
}
|
||||
14
qml/Material/MaterialDividerProps.qml
Normal file
14
qml/Material/MaterialDividerProps.qml
Normal file
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Item {
|
||||
id: dividerProps
|
||||
property real thickness: 1
|
||||
Rectangle {
|
||||
id: divider
|
||||
width: parent ? parent.width : 100
|
||||
height: thickness
|
||||
color: MaterialPalette.outline
|
||||
}
|
||||
}
|
||||
12
qml/Material/MaterialGrid.qml
Normal file
12
qml/Material/MaterialGrid.qml
Normal file
@@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
GridLayout {
|
||||
id: grid
|
||||
property int columns: 2
|
||||
property real spacing: 12
|
||||
columnSpacing: spacing
|
||||
rowSpacing: spacing
|
||||
anchors.fill: parent
|
||||
default property alias content: data
|
||||
}
|
||||
42
qml/Material/MaterialIconButton.qml
Normal file
42
qml/Material/MaterialIconButton.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: iconButton
|
||||
property url iconSource: ""
|
||||
property bool disabled: false
|
||||
property string tooltip: ""
|
||||
signal clicked()
|
||||
|
||||
width: 48
|
||||
height: 48
|
||||
radius: width / 2
|
||||
color: iconButton.hovered && !disabled ? MaterialPalette.surfaceVariant : MaterialPalette.surface
|
||||
border.color: MaterialPalette.outline
|
||||
border.width: 1
|
||||
|
||||
property bool hovered: false
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
source: iconSource
|
||||
width: 20
|
||||
height: 20
|
||||
opacity: disabled ? 0.4 : 1
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: !disabled
|
||||
onClicked: iconButton.clicked()
|
||||
onPressed: iconButton.scale = 0.95
|
||||
onReleased: iconButton.scale = 1
|
||||
onEntered: iconButton.hovered = true
|
||||
onExited: iconButton.hovered = false
|
||||
}
|
||||
}
|
||||
23
qml/Material/MaterialLinearProgress.qml
Normal file
23
qml/Material/MaterialLinearProgress.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: progress
|
||||
property real value: 0
|
||||
property real minValue: 0
|
||||
property real maxValue: 1
|
||||
implicitHeight: 6
|
||||
width: parent ? parent.width : 160
|
||||
radius: 3
|
||||
color: MaterialPalette.surfaceVariant
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
y: (parent.height - height) / 2
|
||||
height: parent.height
|
||||
width: ((progress.value - progress.minValue) / Math.max(0.0001, progress.maxValue - progress.minValue)) * parent.width
|
||||
radius: 3
|
||||
color: MaterialPalette.primary
|
||||
}
|
||||
}
|
||||
25
qml/Material/MaterialLink.qml
Normal file
25
qml/Material/MaterialLink.qml
Normal file
@@ -0,0 +1,25 @@
|
||||
import QtQuick
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Text {
|
||||
id: link
|
||||
property alias url: mouseArea.url
|
||||
property string href: ""
|
||||
property color hoverColor: MaterialPalette.primary
|
||||
color: MaterialPalette.primary
|
||||
font.pixelSize: 14
|
||||
font.bold: false
|
||||
textDecoration: "underline"
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: link.color = hoverColor
|
||||
onExited: link.color = MaterialPalette.primary
|
||||
onClicked: {
|
||||
if (href.length > 0) Qt.openUrlExternally(href)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
qml/Material/MaterialMenu.qml
Normal file
17
qml/Material/MaterialMenu.qml
Normal file
@@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Menu {
|
||||
id: menu
|
||||
background: Rectangle {
|
||||
color: MaterialPalette.surface
|
||||
border.color: MaterialPalette.outline
|
||||
radius: 12
|
||||
}
|
||||
contentItem: Column {
|
||||
spacing: 6
|
||||
anchors.margins: 8
|
||||
}
|
||||
}
|
||||
6
qml/Material/MaterialMenuItem.qml
Normal file
6
qml/Material/MaterialMenuItem.qml
Normal file
@@ -0,0 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
MenuItem {
|
||||
id: menuItem
|
||||
}
|
||||
10
qml/Material/MaterialMenuProps.qml
Normal file
10
qml/Material/MaterialMenuProps.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Menu {
|
||||
id: menuProps
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
property alias currentAction: menuProps.activeAction
|
||||
}
|
||||
25
qml/Material/MaterialPalette.qml
Normal file
25
qml/Material/MaterialPalette.qml
Normal file
@@ -0,0 +1,25 @@
|
||||
import QtQuick
|
||||
|
||||
pragma Singleton
|
||||
|
||||
QtObject {
|
||||
|
||||
property color primary: "#6750A4"
|
||||
property color primaryContainer: "#EADDFF"
|
||||
property color secondary: "#625B71"
|
||||
property color secondaryContainer: "#E8DEF8"
|
||||
property color background: "#121212"
|
||||
property color surface: "#1E1B2D"
|
||||
property color surfaceVariant: "#302B3E"
|
||||
property color onPrimary: "#ffffff"
|
||||
property color onSecondary: "#ffffff"
|
||||
property color onSurface: "#E6E1FF"
|
||||
property color outline: "#494458"
|
||||
|
||||
property color focus: "#BB86FC"
|
||||
property color error: "#CF6679"
|
||||
|
||||
property real elevationHigh: 18
|
||||
property real elevationMedium: 12
|
||||
property real elevationLow: 6
|
||||
}
|
||||
22
qml/Material/MaterialPaper.qml
Normal file
22
qml/Material/MaterialPaper.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialSurface.qml" as MaterialSurface
|
||||
|
||||
MaterialSurface {
|
||||
id: paper
|
||||
property Component body
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
|
||||
Loader {
|
||||
id: paperLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: body
|
||||
}
|
||||
}
|
||||
}
|
||||
20
qml/Material/MaterialPopover.qml
Normal file
20
qml/Material/MaterialPopover.qml
Normal file
@@ -0,0 +1,20 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Popup {
|
||||
id: popover
|
||||
modal: false
|
||||
focus: true
|
||||
background: Rectangle {
|
||||
color: MaterialPalette.surface
|
||||
border.color: MaterialPalette.outline
|
||||
radius: 12
|
||||
}
|
||||
contentItem: Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
}
|
||||
}
|
||||
8
qml/Material/MaterialPopoverProps.qml
Normal file
8
qml/Material/MaterialPopoverProps.qml
Normal file
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Popup {
|
||||
id: popoverProps
|
||||
property bool arrowVisible: true
|
||||
property bool hasShadow: true
|
||||
}
|
||||
36
qml/Material/MaterialSkeleton.qml
Normal file
36
qml/Material/MaterialSkeleton.qml
Normal file
@@ -0,0 +1,36 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: skeleton
|
||||
property bool animated: true
|
||||
property color baseColor: MaterialPalette.surfaceVariant
|
||||
property color highlightColor: MaterialPalette.surface
|
||||
radius: 8
|
||||
color: baseColor
|
||||
implicitWidth: 120
|
||||
implicitHeight: 20
|
||||
|
||||
Rectangle {
|
||||
id: shimmer
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Qt.rgba(highlightColor.r, highlightColor.g, highlightColor.b, 0) }
|
||||
GradientStop { position: 0.5; color: highlightColor }
|
||||
GradientStop { position: 1.0; color: Qt.rgba(highlightColor.r, highlightColor.g, highlightColor.b, 0) }
|
||||
}
|
||||
opacity: animated ? 1 : 0
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 1100
|
||||
from: -width
|
||||
to: width
|
||||
loops: Animation.Infinite
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
qml/Material/MaterialSnackbar.qml
Normal file
44
qml/Material/MaterialSnackbar.qml
Normal file
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: snackbar
|
||||
property string message: ""
|
||||
property string actionText: ""
|
||||
property bool open: false
|
||||
signal actionTriggered()
|
||||
implicitHeight: 56
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
width: parent ? Math.min(parent.width * 0.8, 520) : 520
|
||||
radius: 12
|
||||
color: MaterialPalette.surfaceVariant
|
||||
border.color: MaterialPalette.outline
|
||||
border.width: 1
|
||||
opacity: open ? 1 : 0
|
||||
y: open ? (parent ? parent.height - implicitHeight - 32 : 0) : (parent ? parent.height : implicitHeight)
|
||||
Behavior on y { NumberAnimation { duration: 300; easing.type: Easing.OutQuad } }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: message
|
||||
color: MaterialPalette.onSurface
|
||||
font.pixelSize: 15
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: actionText.length > 0
|
||||
text: actionText
|
||||
font.pixelSize: 14
|
||||
background: Rectangle { color: "transparent" }
|
||||
onClicked: snackbar.actionTriggered()
|
||||
}
|
||||
}
|
||||
}
|
||||
26
qml/Material/MaterialSurface.qml
Normal file
26
qml/Material/MaterialSurface.qml
Normal file
@@ -0,0 +1,26 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Rectangle {
|
||||
id: surface
|
||||
property Component contentComponent: null
|
||||
property real elevation: MaterialPalette.elevationLow
|
||||
property color surfaceColor: MaterialPalette.surface
|
||||
property bool outlined: false
|
||||
|
||||
radius: 18
|
||||
color: surfaceColor
|
||||
border.color: outlined ? MaterialPalette.outline : "transparent"
|
||||
border.width: outlined ? 1 : 0
|
||||
width: parent ? parent.width : 320
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
sourceComponent: contentComponent
|
||||
}
|
||||
}
|
||||
31
qml/Material/MaterialSwitch.qml
Normal file
31
qml/Material/MaterialSwitch.qml
Normal file
@@ -0,0 +1,31 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Switch {
|
||||
id: switchControl
|
||||
property alias label: labelText.text
|
||||
property color thumbOnColor: MaterialPalette.primary
|
||||
property color trackOnColor: MaterialPalette.primaryContainer
|
||||
property color trackOffColor: MaterialPalette.surfaceVariant
|
||||
|
||||
indicator: Rectangle {
|
||||
radius: height / 2
|
||||
color: switchControl.checked ? thumbOnColor : MaterialPalette.surface
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: height / 2
|
||||
color: switchControl.checked ? trackOnColor : trackOffColor
|
||||
}
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.right
|
||||
anchors.leftMargin: 10
|
||||
color: MaterialPalette.onSurface
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
24
qml/Material/MaterialTextField.qml
Normal file
24
qml/Material/MaterialTextField.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
TextField {
|
||||
id: field
|
||||
property color baseColor: MaterialPalette.surfaceVariant
|
||||
property color focusColor: MaterialPalette.focus
|
||||
property color caretColor: MaterialPalette.onSurface
|
||||
|
||||
implicitHeight: 48
|
||||
font.pixelSize: 15
|
||||
background: Rectangle {
|
||||
radius: 12
|
||||
border.width: 1
|
||||
border.color: field.activeFocus ? focusColor : MaterialPalette.outline
|
||||
color: baseColor
|
||||
}
|
||||
color: MaterialPalette.onSurface
|
||||
cursorVisible: true
|
||||
cursorColor: caretColor
|
||||
padding: 14
|
||||
}
|
||||
11
qml/Material/MaterialToolbar.qml
Normal file
11
qml/Material/MaterialToolbar.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
RowLayout {
|
||||
id: toolbar
|
||||
spacing: 12
|
||||
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
|
||||
default property alias content: toolbar.data
|
||||
}
|
||||
18
qml/Material/MaterialTypography.qml
Normal file
18
qml/Material/MaterialTypography.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
|
||||
import "MaterialPalette.qml" as MaterialPalette
|
||||
|
||||
Text {
|
||||
id: typ
|
||||
property alias content: text
|
||||
property string variant: "body1"
|
||||
|
||||
color: MaterialPalette.onSurface
|
||||
font.pixelSize: variant === "h1" ? 32 :
|
||||
variant === "h2" ? 28 :
|
||||
variant === "h3" ? 24 :
|
||||
variant === "h4" ? 20 :
|
||||
variant === "button" ? 16 :
|
||||
14
|
||||
font.bold: variant === "h1" || variant === "h2" || variant === "h3"
|
||||
}
|
||||
35
qml/Material/qmldir
Normal file
35
qml/Material/qmldir
Normal file
@@ -0,0 +1,35 @@
|
||||
module Material
|
||||
singleton MaterialPalette 1.0 MaterialPalette.qml
|
||||
MaterialButton 1.0 MaterialButton.qml
|
||||
MaterialCard 1.0 MaterialCard.qml
|
||||
MaterialTextField 1.0 MaterialTextField.qml
|
||||
MaterialChip 1.0 MaterialChip.qml
|
||||
MaterialSurface 1.0 MaterialSurface.qml
|
||||
MaterialDivider 1.0 MaterialDivider.qml
|
||||
MaterialBadge 1.0 MaterialBadge.qml
|
||||
MaterialAlert 1.0 MaterialAlert.qml
|
||||
MaterialDialog 1.0 MaterialDialog.qml
|
||||
MaterialSnackbar 1.0 MaterialSnackbar.qml
|
||||
MaterialIconButton 1.0 MaterialIconButton.qml
|
||||
MaterialCircularProgress 1.0 MaterialCircularProgress.qml
|
||||
MaterialLinearProgress 1.0 MaterialLinearProgress.qml
|
||||
MaterialSkeleton 1.0 MaterialSkeleton.qml
|
||||
MaterialSwitch 1.0 MaterialSwitch.qml
|
||||
MaterialAppBar 1.0 MaterialAppBar.qml
|
||||
MaterialToolbar 1.0 MaterialToolbar.qml
|
||||
MaterialAvatar 1.0 MaterialAvatar.qml
|
||||
MaterialLink 1.0 MaterialLink.qml
|
||||
MaterialTypography 1.0 MaterialTypography.qml
|
||||
MaterialPaper 1.0 MaterialPaper.qml
|
||||
MaterialBox 1.0 MaterialBox.qml
|
||||
MaterialAccordion 1.0 MaterialAccordion.qml
|
||||
MaterialCollapse 1.0 MaterialCollapse.qml
|
||||
MaterialCheckbox 1.0 MaterialCheckbox.qml
|
||||
MaterialGrid 1.0 MaterialGrid.qml
|
||||
MaterialMenu 1.0 MaterialMenu.qml
|
||||
MaterialMenuItem 1.0 MaterialMenuItem.qml
|
||||
MaterialMenuProps 1.0 MaterialMenuProps.qml
|
||||
MaterialPopover 1.0 MaterialPopover.qml
|
||||
MaterialPopoverProps 1.0 MaterialPopoverProps.qml
|
||||
MaterialDividerProps 1.0 MaterialDividerProps.qml
|
||||
MaterialContainer 1.0 MaterialContainer.qml
|
||||
34
qml/MetaBuilder/CActivityList.qml
Normal file
34
qml/MetaBuilder/CActivityList.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
|
||||
property var activities: []
|
||||
property bool isDark: false
|
||||
|
||||
variant: "filled"
|
||||
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
variant: "h4"
|
||||
text: "Recent Activity"
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 8 }
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
Item { Layout.preferredHeight: 8 }
|
||||
|
||||
Repeater {
|
||||
model: root.activities
|
||||
delegate: CListItem {
|
||||
Layout.fillWidth: true
|
||||
title: modelData.action
|
||||
subtitle: modelData.detail + " \u00b7 " + modelData.time
|
||||
}
|
||||
}
|
||||
}
|
||||
53
qml/MetaBuilder/CAdapterPatternSelector.qml
Normal file
53
qml/MetaBuilder/CAdapterPatternSelector.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
property int selectedPattern: 0
|
||||
property var patterns: ["read-through", "write-through", "cache-aside", "dual-write"]
|
||||
|
||||
property var descriptions: [
|
||||
"Read-through: Reads check cache first. On miss, the cache fetches from the primary DB, stores the result, and returns it. Best for read-heavy workloads.",
|
||||
"Write-through: Every write goes to both cache and primary DB synchronously. Guarantees consistency at the cost of write latency.",
|
||||
"Cache-aside: Application manages cache explicitly. Reads check cache, fetch from DB on miss and populate cache. Writes go directly to DB and invalidate cache.",
|
||||
"Dual-write: Writes are sent to two backends simultaneously (e.g., primary DB + search index). Requires conflict resolution strategy."
|
||||
]
|
||||
|
||||
signal patternChanged(int index)
|
||||
|
||||
CText { variant: "subtitle1"; text: "Multi-Adapter Pattern" }
|
||||
CText { variant: "body2"; text: "Select how the primary, cache, and search adapters coordinate data flow." }
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: root.patterns
|
||||
delegate: CButton {
|
||||
text: modelData
|
||||
variant: index === root.selectedPattern ? "primary" : "ghost"
|
||||
onClicked: root.patternChanged(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CPaper {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: patternDesc.implicitHeight + 24
|
||||
|
||||
CText {
|
||||
id: patternDesc
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
variant: "body2"
|
||||
wrapMode: Text.Wrap
|
||||
text: root.descriptions[root.selectedPattern]
|
||||
}
|
||||
}
|
||||
}
|
||||
88
qml/MetaBuilder/CAddRouteDialog.qml
Normal file
88
qml/MetaBuilder/CAddRouteDialog.qml
Normal file
@@ -0,0 +1,88 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CDialog {
|
||||
id: root
|
||||
|
||||
property string newPath: ""
|
||||
property string newTitle: ""
|
||||
property int newLevel: 1
|
||||
property string newLayout: "default"
|
||||
property var layoutOptions: ["default", "sidebar", "dashboard", "blank"]
|
||||
property var levelOptions: [1, 2, 3, 4, 5]
|
||||
|
||||
signal addRoute(string path, string title, int level, string layout)
|
||||
|
||||
title: "Add New Route"
|
||||
|
||||
function reset() {
|
||||
newPath = ""
|
||||
newTitle = ""
|
||||
newLevel = 1
|
||||
newLayout = "default"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
width: 320
|
||||
|
||||
CText { variant: "caption"; text: "Path" }
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "/new-page"
|
||||
text: root.newPath
|
||||
onTextChanged: root.newPath = text
|
||||
}
|
||||
|
||||
CText { variant: "caption"; text: "Page Title" }
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "New Page"
|
||||
text: root.newTitle
|
||||
onTextChanged: root.newTitle = text
|
||||
}
|
||||
|
||||
CText { variant: "caption"; text: "Required Level" }
|
||||
CSelect {
|
||||
Layout.fillWidth: true
|
||||
model: root.levelOptions
|
||||
currentIndex: root.newLevel - 1
|
||||
onCurrentIndexChanged: root.newLevel = currentIndex + 1
|
||||
}
|
||||
|
||||
CText { variant: "caption"; text: "Layout Type" }
|
||||
CSelect {
|
||||
Layout.fillWidth: true
|
||||
model: root.layoutOptions
|
||||
currentIndex: root.layoutOptions.indexOf(root.newLayout)
|
||||
onCurrentIndexChanged: root.newLayout = root.layoutOptions[currentIndex]
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CButton {
|
||||
text: "Cancel"
|
||||
variant: "ghost"
|
||||
onClicked: {
|
||||
root.reset()
|
||||
root.visible = false
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
CButton {
|
||||
text: "Add Route"
|
||||
variant: "primary"
|
||||
enabled: root.newPath.length > 0 && root.newTitle.length > 0
|
||||
onClicked: {
|
||||
root.addRoute(root.newPath, root.newTitle, root.newLevel, root.newLayout)
|
||||
root.reset()
|
||||
root.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
qml/MetaBuilder/CAdminStatsBar.qml
Normal file
60
qml/MetaBuilder/CAdminStatsBar.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CAdminStatsBar.qml - Stats bar showing entity counts with colored accents
|
||||
*
|
||||
* Usage:
|
||||
* CAdminStatsBar {
|
||||
* stats: [
|
||||
* { label: "Total Users", value: 8, accent: "#4CAF50" },
|
||||
* { label: "Active Sessions", value: 6, accent: "#2196F3" }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var stats: [] // Array of { label, value, accent }
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 88
|
||||
color: Theme.surface
|
||||
radius: 0
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
Repeater {
|
||||
model: root.stats
|
||||
|
||||
delegate: CCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
Layout.fillHeight: true
|
||||
color: modelData.accent
|
||||
radius: 2
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
CText { variant: "caption"; text: modelData.label; color: Theme.textSecondary }
|
||||
CText { variant: "h3"; text: String(modelData.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
qml/MetaBuilder/CBackendDetailPanel.qml
Normal file
149
qml/MetaBuilder/CBackendDetailPanel.qml
Normal file
@@ -0,0 +1,149 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
property var backend: ({ name: "", key: "", status: "disconnected", description: "", connectionString: "", records: 0, sizeKb: 0, lastBackup: "Never" })
|
||||
property bool isActive: false
|
||||
property int testingIndex: -1
|
||||
property int backendIndex: -1
|
||||
property var testResult: undefined
|
||||
|
||||
signal testConnectionRequested()
|
||||
signal setActiveRequested()
|
||||
|
||||
function formatSize(kb) {
|
||||
if (kb < 1024) return kb + " KB"
|
||||
return (kb / 1024).toFixed(1) + " MB"
|
||||
}
|
||||
|
||||
Flickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
contentHeight: detailColumn.implicitHeight
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
id: detailColumn
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CText { variant: "h4"; text: root.backend.name }
|
||||
CStatusBadge {
|
||||
status: root.backend.status === "connected" ? "success" : (root.backend.status === "error" ? "error" : "warning")
|
||||
text: root.backend.status
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
CBadge {
|
||||
text: root.isActive ? "ACTIVE" : "INACTIVE"
|
||||
accent: root.isActive
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: "body1"
|
||||
text: root.backend.description
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CText { variant: "subtitle1"; text: "Connection" }
|
||||
|
||||
CTextField {
|
||||
label: "Connection String"
|
||||
text: root.backend.connectionString
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
CButton {
|
||||
text: root.testingIndex === root.backendIndex ? "Testing..." : "Test Connection"
|
||||
variant: "primary"
|
||||
enabled: root.testingIndex === -1
|
||||
onClicked: root.testConnectionRequested()
|
||||
}
|
||||
|
||||
CButton {
|
||||
text: root.isActive ? "Active Backend" : "Set as Active"
|
||||
variant: root.isActive ? "ghost" : "primary"
|
||||
enabled: !root.isActive && root.backend.status === "connected"
|
||||
onClicked: root.setActiveRequested()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Loader {
|
||||
active: root.testResult !== undefined
|
||||
sourceComponent: CStatusBadge {
|
||||
status: root.testResult === "success" ? "success" : "error"
|
||||
text: root.testResult === "success" ? "Connection OK" : "Connection Failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CText { variant: "subtitle1"; text: "Storage Statistics" }
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CPaper {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 60
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 2
|
||||
CText { variant: "caption"; text: "Records" }
|
||||
CText { variant: "h4"; text: root.backend.records.toLocaleString() }
|
||||
}
|
||||
}
|
||||
|
||||
CPaper {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 60
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 2
|
||||
CText { variant: "caption"; text: "Size" }
|
||||
CText { variant: "h4"; text: root.formatSize(root.backend.sizeKb) }
|
||||
}
|
||||
}
|
||||
|
||||
CPaper {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 60
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 2
|
||||
CText { variant: "caption"; text: "Last Backup" }
|
||||
CText { variant: "body2"; text: root.backend.lastBackup }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
qml/MetaBuilder/CBackendListSidebar.qml
Normal file
50
qml/MetaBuilder/CBackendListSidebar.qml
Normal file
@@ -0,0 +1,50 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
Layout.preferredWidth: 300
|
||||
Layout.fillHeight: true
|
||||
|
||||
property var backends: []
|
||||
property int selectedIndex: 0
|
||||
|
||||
signal backendSelected(int index)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 8
|
||||
|
||||
CText { variant: "subtitle1"; text: "Backends (" + backends.length + ")" }
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: root.backends
|
||||
spacing: 4
|
||||
clip: true
|
||||
|
||||
delegate: CListItem {
|
||||
width: parent ? parent.width : 268
|
||||
title: modelData.name
|
||||
subtitle: modelData.key
|
||||
selected: index === root.selectedIndex
|
||||
leadingIcon: modelData.status === "connected" ? "check_circle" : (modelData.status === "error" ? "error" : "radio_button_unchecked")
|
||||
|
||||
onClicked: root.backendSelected(index)
|
||||
|
||||
CStatusBadge {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
status: modelData.status === "connected" ? "success" : (modelData.status === "error" ? "error" : "warning")
|
||||
text: modelData.status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
qml/MetaBuilder/CCanvasGrid.qml
Normal file
28
qml/MetaBuilder/CCanvasGrid.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
|
||||
Canvas {
|
||||
id: root
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
var gridSize = 50
|
||||
ctx.strokeStyle = Qt.rgba(0.5, 0.5, 0.5, 0.1)
|
||||
ctx.lineWidth = 1
|
||||
|
||||
for (var x = 0; x < width; x += gridSize) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, 0)
|
||||
ctx.lineTo(x, height)
|
||||
ctx.stroke()
|
||||
}
|
||||
for (var y = 0; y < height; y += gridSize) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, y)
|
||||
ctx.lineTo(width, y)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
}
|
||||
43
qml/MetaBuilder/CCanvasZoomOverlay.qml
Normal file
43
qml/MetaBuilder/CCanvasZoomOverlay.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real zoom: 1.0
|
||||
|
||||
signal zoomIn()
|
||||
signal zoomOut()
|
||||
|
||||
width: 120
|
||||
height: 36
|
||||
radius: 18
|
||||
color: Qt.rgba(Theme.paper.r || 0.1, Theme.paper.g || 0.1, Theme.paper.b || 0.1, 0.9)
|
||||
border.color: Theme.border
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
CButton {
|
||||
text: "-"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.zoomOut()
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: Math.round(root.zoom * 100) + "%"
|
||||
}
|
||||
|
||||
CButton {
|
||||
text: "+"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.zoomIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
83
qml/MetaBuilder/CCommentCard.qml
Normal file
83
qml/MetaBuilder/CCommentCard.qml
Normal file
@@ -0,0 +1,83 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
|
||||
required property var comment
|
||||
property string currentUser: ""
|
||||
property bool isDark: false
|
||||
|
||||
signal liked()
|
||||
signal deleted()
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Comment header: avatar, user, time
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CAvatar {
|
||||
initials: root.comment.initials
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
FlexRow {
|
||||
spacing: 8
|
||||
CText {
|
||||
variant: "subtitle1"
|
||||
text: root.comment.username
|
||||
}
|
||||
CChip {
|
||||
text: root.comment.username === root.currentUser ? "You" : ""
|
||||
chipColor: Theme.primary
|
||||
visible: root.comment.username === root.currentUser
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: root.comment.timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comment body
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
variant: "body1"
|
||||
text: root.comment.body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// Actions row
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
CButton {
|
||||
text: root.comment.liked ? "Liked (" + root.comment.likes + ")" : "Like (" + root.comment.likes + ")"
|
||||
variant: root.comment.liked ? "primary" : "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.liked()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
CButton {
|
||||
text: "Delete"
|
||||
variant: "danger"
|
||||
size: "sm"
|
||||
visible: root.comment.canDelete || false
|
||||
onClicked: root.deleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
42
qml/MetaBuilder/CCommentInput.qml
Normal file
42
qml/MetaBuilder/CCommentInput.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
|
||||
property bool isDark: false
|
||||
property bool loading: false
|
||||
property string commentText: ""
|
||||
|
||||
signal submit(string text)
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
CText { variant: "subtitle1"; text: "Post a Comment" }
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Your comment"
|
||||
placeholderText: "Write your thoughts..."
|
||||
text: root.commentText
|
||||
onTextChanged: root.commentText = text
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Item { Layout.fillWidth: true }
|
||||
CButton {
|
||||
text: root.loading ? "Posting..." : "Post Comment"
|
||||
variant: "primary"
|
||||
size: "sm"
|
||||
enabled: root.commentText.trim().length > 0 && !root.loading
|
||||
onClicked: {
|
||||
root.submit(root.commentText.trim())
|
||||
root.commentText = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
150
qml/MetaBuilder/CComponentPropertiesPanel.qml
Normal file
150
qml/MetaBuilder/CComponentPropertiesPanel.qml
Normal file
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 14
|
||||
|
||||
property var node: null
|
||||
property int childCount: 0
|
||||
|
||||
signal nameChanged(string name)
|
||||
signal typeChanged(string type)
|
||||
signal visibleChanged(bool visible)
|
||||
signal addProp()
|
||||
signal removeProp(int index)
|
||||
|
||||
// Name field
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: "NAME" }
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
text: root.node ? root.node.name : ""
|
||||
onTextChanged: {
|
||||
if (root.node && text !== root.node.name)
|
||||
root.nameChanged(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type selector
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: "TYPE" }
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: ["container", "layout", "widget", "atom"]
|
||||
delegate: CButton {
|
||||
text: modelData
|
||||
size: "sm"
|
||||
variant: (root.node && root.node.type === modelData) ? "primary" : "ghost"
|
||||
onClicked: root.typeChanged(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visible toggle
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
CText { variant: "body2"; text: "Visible" }
|
||||
Item { Layout.fillWidth: true }
|
||||
CSwitch {
|
||||
checked: root.node ? root.node.visible : false
|
||||
onCheckedChanged: {
|
||||
if (root.node && checked !== root.node.visible)
|
||||
root.visibleChanged(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// Info row
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 16
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
CText { variant: "caption"; text: "DEPTH" }
|
||||
CText { variant: "body1"; text: root.node ? root.node.depth.toString() : "0" }
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
CText { variant: "caption"; text: "CHILDREN" }
|
||||
CText { variant: "body1"; text: root.childCount.toString() }
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
CText { variant: "caption"; text: "NODE ID" }
|
||||
CText { variant: "body1"; text: root.node ? root.node.nodeId.toString() : "-" }
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// Custom props header
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
CText { variant: "h4"; text: "Custom Properties" }
|
||||
Item { Layout.fillWidth: true }
|
||||
CButton {
|
||||
text: "Add Prop"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.addProp()
|
||||
}
|
||||
}
|
||||
|
||||
// Props list
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 6
|
||||
model: root.node ? root.node.props : []
|
||||
|
||||
delegate: CPaper {
|
||||
width: parent ? parent.width : 300
|
||||
height: 44
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
|
||||
CText {
|
||||
variant: "body2"
|
||||
text: modelData.key
|
||||
Layout.preferredWidth: 120
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: "body1"
|
||||
text: modelData.value
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
CButton {
|
||||
text: "\u00D7"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.removeProp(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
qml/MetaBuilder/CComponentTreeRow.qml
Normal file
74
qml/MetaBuilder/CComponentTreeRow.qml
Normal file
@@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var node
|
||||
property bool isSelected: false
|
||||
property int childCount: 0
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: parent ? parent.width : 400
|
||||
height: 40
|
||||
radius: 6
|
||||
color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
: mouseArea.containsMouse ? Theme.surface : "transparent"
|
||||
|
||||
function typeBadgeColor(nodeType) {
|
||||
switch (nodeType) {
|
||||
case "container": return "#5C6BC0"
|
||||
case "layout": return "#26A69A"
|
||||
case "widget": return "#FFA726"
|
||||
case "atom": return "#EF5350"
|
||||
default: return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12 + (root.node ? root.node.depth * 24 : 0)
|
||||
anchors.rightMargin: 12
|
||||
spacing: 8
|
||||
|
||||
CText {
|
||||
visible: root.node && root.node.depth > 0
|
||||
variant: "caption"
|
||||
text: "\u251C\u2500"
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 8; height: 8; radius: 4
|
||||
color: root.node ? typeBadgeColor(root.node.type) : "#9e9e9e"
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: root.isSelected ? "body1" : "body2"
|
||||
text: root.node ? root.node.name : ""
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: root.node && root.node.visible ? "\uD83D\uDC41" : "\u2014"
|
||||
opacity: root.node && root.node.visible ? 0.6 : 0.3
|
||||
}
|
||||
|
||||
CBadge {
|
||||
visible: root.childCount > 0
|
||||
text: root.childCount.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
28
qml/MetaBuilder/CComponentTypeLegend.qml
Normal file
28
qml/MetaBuilder/CComponentTypeLegend.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
FlexRow {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ label: "container", color: "#5C6BC0" },
|
||||
{ label: "layout", color: "#26A69A" },
|
||||
{ label: "widget", color: "#FFA726" },
|
||||
{ label: "atom", color: "#EF5350" }
|
||||
]
|
||||
delegate: Row {
|
||||
spacing: 4
|
||||
Rectangle {
|
||||
width: 10; height: 10; radius: 2
|
||||
color: modelData.color
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
CText { variant: "caption"; text: modelData.label }
|
||||
}
|
||||
}
|
||||
}
|
||||
56
qml/MetaBuilder/CConfigStatCard.qml
Normal file
56
qml/MetaBuilder/CConfigStatCard.qml
Normal file
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
property color accent: "#6366F1"
|
||||
property bool isDark: false
|
||||
|
||||
// MD3 tonal surfaces
|
||||
readonly property color surfaceContainer: isDark ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0.31, 0.31, 0.44, 0.06)
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
|
||||
implicitHeight: 64
|
||||
radius: 12
|
||||
color: surfaceContainer
|
||||
border.width: 1
|
||||
border.color: outlineVariant
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: 32
|
||||
radius: 2
|
||||
color: root.accent
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
CText {
|
||||
text: root.value
|
||||
font.pixelSize: 20
|
||||
font.weight: Font.Bold
|
||||
font.family: "monospace"
|
||||
color: root.accent
|
||||
}
|
||||
CText {
|
||||
text: root.label
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
color: root.onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
qml/MetaBuilder/CConnectionLayer.qml
Normal file
124
qml/MetaBuilder/CConnectionLayer.qml
Normal file
@@ -0,0 +1,124 @@
|
||||
import QtQuick
|
||||
import QmlComponents 1.0
|
||||
|
||||
Canvas {
|
||||
id: root
|
||||
|
||||
property var nodes: []
|
||||
property var connections: ({})
|
||||
property bool drawingConnection: false
|
||||
property string connSourceNode: ""
|
||||
property bool connSourceIsOutput: true
|
||||
property real connDragX: 0
|
||||
property real connDragY: 0
|
||||
property bool isDark: false
|
||||
|
||||
function groupColor(nodeType) {
|
||||
var prefix = nodeType ? nodeType.split(".")[0] : ""
|
||||
switch (prefix) {
|
||||
case "metabuilder": return Theme.success
|
||||
case "logic": return Theme.warning
|
||||
case "transform":
|
||||
case "packagerepo": return "#FF9800"
|
||||
case "sdl":
|
||||
case "graphics": return "#2196F3"
|
||||
case "integration": return "#9C27B0"
|
||||
case "io": return "#00BCD4"
|
||||
default: return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
function findNodeById(id) {
|
||||
if (!nodes) return null
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === id) return nodes[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
ctx.lineWidth = 2.5
|
||||
|
||||
if (!connections || !nodes) return
|
||||
|
||||
var nodeW = 180
|
||||
var headerH = 32
|
||||
var portSpacing = 24
|
||||
var portOffset = 8
|
||||
|
||||
// Draw established connections
|
||||
for (var srcId in connections) {
|
||||
var srcNode = findNodeById(srcId)
|
||||
if (!srcNode) continue
|
||||
|
||||
var srcConns = connections[srcId]
|
||||
for (var outName in srcConns) {
|
||||
for (var outIdx in srcConns[outName]) {
|
||||
var targets = srcConns[outName][outIdx]
|
||||
var outIndex = parseInt(outIdx)
|
||||
var srcX = srcNode.position[0] + nodeW
|
||||
var srcY = srcNode.position[1] + headerH + portOffset + outIndex * portSpacing + 6
|
||||
|
||||
for (var t = 0; t < targets.length; t++) {
|
||||
var target = targets[t]
|
||||
var dstNode = findNodeById(target.node)
|
||||
if (!dstNode) continue
|
||||
|
||||
var inIndex = target.index || 0
|
||||
var dstX = dstNode.position[0]
|
||||
var dstY = dstNode.position[1] + headerH + portOffset + inIndex * portSpacing + 6
|
||||
|
||||
// Draw Bezier
|
||||
var cpOffset = Math.max(80, Math.abs(dstX - srcX) * 0.4)
|
||||
ctx.strokeStyle = groupColor(srcNode.type)
|
||||
ctx.globalAlpha = 0.8
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(srcX, srcY)
|
||||
ctx.bezierCurveTo(srcX + cpOffset, srcY, dstX - cpOffset, dstY, dstX, dstY)
|
||||
ctx.stroke()
|
||||
|
||||
// Arrow at destination
|
||||
ctx.globalAlpha = 1.0
|
||||
ctx.fillStyle = groupColor(srcNode.type)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(dstX, dstY)
|
||||
ctx.lineTo(dstX - 8, dstY - 4)
|
||||
ctx.lineTo(dstX - 8, dstY + 4)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw connection being dragged
|
||||
if (drawingConnection && connSourceNode) {
|
||||
var dragSrc = findNodeById(connSourceNode)
|
||||
if (dragSrc) {
|
||||
var sx, sy
|
||||
if (connSourceIsOutput) {
|
||||
sx = dragSrc.position[0] + nodeW
|
||||
sy = dragSrc.position[1] + headerH + portOffset + 6
|
||||
} else {
|
||||
sx = dragSrc.position[0]
|
||||
sy = dragSrc.position[1] + headerH + portOffset + 6
|
||||
}
|
||||
var dx = connDragX
|
||||
var dy = connDragY
|
||||
var cp = Math.max(60, Math.abs(dx - sx) * 0.4)
|
||||
|
||||
ctx.strokeStyle = Theme.primary
|
||||
ctx.globalAlpha = 0.6
|
||||
ctx.setLineDash([6, 4])
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(sx, sy)
|
||||
ctx.bezierCurveTo(sx + cp, sy, dx - cp, dy, dx, dy)
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
qml/MetaBuilder/CConnectionTest.qml
Normal file
135
qml/MetaBuilder/CConnectionTest.qml
Normal file
@@ -0,0 +1,135 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
import "../dbal"
|
||||
|
||||
/**
|
||||
* CConnectionTest - DBAL connection test panel with URL input,
|
||||
* test button, and status indicator.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 12
|
||||
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
// ── Internal DBAL provider ──────────────────────────────────
|
||||
DBALProvider { id: dbal }
|
||||
|
||||
// ── Connection state ────────────────────────────────────────
|
||||
property string dbalUrl: dbal.baseUrl
|
||||
property string mediaServiceUrl: "http://localhost:9090"
|
||||
property string dbalConnectionStatus: dbal.connected ? "connected" : "disconnected"
|
||||
property string mediaConnectionStatus: "unknown"
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────
|
||||
function connectionStatusColor(status) {
|
||||
switch (status) {
|
||||
case "connected": return "success"
|
||||
case "disconnected": return "error"
|
||||
case "testing": return "warning"
|
||||
default: return "info"
|
||||
}
|
||||
}
|
||||
|
||||
function connectionStatusLabel(status) {
|
||||
switch (status) {
|
||||
case "connected": return "Connected"
|
||||
case "disconnected": return "Disconnected"
|
||||
case "testing": return "Testing..."
|
||||
default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
function testDBALConnection() {
|
||||
dbalConnectionStatus = "testing"
|
||||
dbal.baseUrl = dbalUrl
|
||||
dbal.ping(function(success, error) {
|
||||
dbalConnectionStatus = success ? "connected" : "disconnected"
|
||||
})
|
||||
}
|
||||
|
||||
function testMediaConnection() {
|
||||
mediaConnectionStatus = "testing"
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
mediaConnectionStatus = (xhr.status >= 200 && xhr.status < 300)
|
||||
? "connected" : "disconnected"
|
||||
}
|
||||
}
|
||||
xhr.open("GET", mediaServiceUrl + "/health")
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
// ── DBAL Server ─────────────────────────────────────────────
|
||||
CText { variant: "subtitle2"; text: "DBAL Server"; Layout.topMargin: 4 }
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "DBAL URL"
|
||||
placeholderText: "http://localhost:8080"
|
||||
text: root.dbalUrl
|
||||
onTextChanged: root.dbalUrl = text
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
CButton {
|
||||
text: dbalConnectionStatus === "testing" ? "Testing..." : "Test Connection"
|
||||
variant: "default"
|
||||
size: "sm"
|
||||
enabled: dbalConnectionStatus !== "testing"
|
||||
onClicked: testDBALConnection()
|
||||
}
|
||||
|
||||
CStatusBadge {
|
||||
status: connectionStatusColor(dbalConnectionStatus)
|
||||
text: connectionStatusLabel(dbalConnectionStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// ── Media Service ───────────────────────────────────────────
|
||||
CText { variant: "subtitle2"; text: "Media Service" }
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Media Service URL"
|
||||
placeholderText: "http://localhost:9090"
|
||||
text: root.mediaServiceUrl
|
||||
onTextChanged: root.mediaServiceUrl = text
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
CButton {
|
||||
text: mediaConnectionStatus === "testing" ? "Testing..." : "Test Connection"
|
||||
variant: "default"
|
||||
size: "sm"
|
||||
enabled: mediaConnectionStatus !== "testing"
|
||||
onClicked: testMediaConnection()
|
||||
}
|
||||
|
||||
CStatusBadge {
|
||||
status: connectionStatusColor(mediaConnectionStatus)
|
||||
text: connectionStatusLabel(mediaConnectionStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
193
qml/MetaBuilder/CDataTable.qml
Normal file
193
qml/MetaBuilder/CDataTable.qml
Normal file
@@ -0,0 +1,193 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CDataTable.qml - Generic data table with checkboxes, pagination, search, actions
|
||||
*
|
||||
* Usage:
|
||||
* CDataTable {
|
||||
* headers: ["ID", "Username", "Email", "Role", "Status", "Created"]
|
||||
* fields: ["id", "username", "email", "role", "status", "created"]
|
||||
* rows: [{ id: "USR-001", username: "admin", ... }]
|
||||
* totalFiltered: 24
|
||||
* page: 0
|
||||
* pageSize: 5
|
||||
* onRowClicked: function(index) { ... }
|
||||
* onEditClicked: function(index, record) { ... }
|
||||
* onDeleteClicked: function(index, record) { ... }
|
||||
* }
|
||||
*/
|
||||
CCard {
|
||||
id: root
|
||||
|
||||
property var headers: [] // Column header labels
|
||||
property var fields: [] // Field keys matching headers
|
||||
property var rows: [] // Array of record objects (current page)
|
||||
property int totalFiltered: 0 // Total filtered count (for pagination text)
|
||||
property int page: 0 // Current page index
|
||||
property int pageSize: 5
|
||||
property int selectedRow: -1
|
||||
property var selectedRows: ({})
|
||||
property bool selectAll: false
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
signal rowClicked(int index)
|
||||
signal editClicked(int index, var record)
|
||||
signal deleteClicked(int index, var record)
|
||||
signal pageRequested(int newPage)
|
||||
signal selectAllToggled(bool checked)
|
||||
signal rowSelectionChanged(var selectedRows)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
// ── Column headers ──────────────────────────────────────
|
||||
CTableHeader {
|
||||
headers: root.headers
|
||||
selectAll: root.selectAll
|
||||
onSelectAllToggled: function(checked) {
|
||||
root.selectAll = checked;
|
||||
var newSel = {};
|
||||
for (var i = 0; i < root.rows.length; i++) {
|
||||
newSel[i] = checked;
|
||||
}
|
||||
root.selectedRows = newSel;
|
||||
root.rowSelectionChanged(newSel);
|
||||
root.selectAllToggled(checked);
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// ── Data rows ───────────────────────────────────────────
|
||||
ListView {
|
||||
id: tableView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: root.rows
|
||||
clip: true
|
||||
spacing: 0
|
||||
|
||||
delegate: Rectangle {
|
||||
id: rowDelegate
|
||||
width: tableView.width
|
||||
height: 48
|
||||
property var rowData: modelData
|
||||
property int rowIndex: index
|
||||
color: {
|
||||
if (root.selectedRow === rowIndex) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
if (root.selectedRows[rowIndex]) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
|
||||
return rowIndex % 2 === 0 ? "transparent" : Theme.surfaceVariant;
|
||||
}
|
||||
radius: 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.rowClicked(rowDelegate.rowIndex)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 0
|
||||
|
||||
CheckBox {
|
||||
Layout.preferredWidth: 36
|
||||
checked: root.selectedRows[rowDelegate.rowIndex] || false
|
||||
onCheckedChanged: {
|
||||
var newSel = Object.assign({}, root.selectedRows);
|
||||
newSel[rowDelegate.rowIndex] = checked;
|
||||
root.selectedRows = newSel;
|
||||
root.rowSelectionChanged(newSel);
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.fields
|
||||
delegate: Item {
|
||||
Layout.fillWidth: index > 0
|
||||
Layout.preferredWidth: index === 0 ? 80 : -1
|
||||
implicitHeight: 48
|
||||
|
||||
CText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
variant: "body2"
|
||||
text: {
|
||||
var key = modelData;
|
||||
var rec = rowDelegate.rowData;
|
||||
return rec ? (String(rec[key] || "")) : "";
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.preferredWidth: 110
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: 4
|
||||
CButton {
|
||||
text: "Edit"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.editClicked(rowDelegate.rowIndex, rowDelegate.rowData)
|
||||
}
|
||||
CButton {
|
||||
text: "Del"
|
||||
variant: "danger"
|
||||
size: "sm"
|
||||
onClicked: root.deleteClicked(rowDelegate.rowIndex, rowDelegate.rowData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ─────────────────────────────────────────
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: root.totalFiltered === 0
|
||||
visible: root.totalFiltered === 0
|
||||
Layout.preferredHeight: visible ? 120 : 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
variant: "h4"
|
||||
text: "No records found"
|
||||
color: Theme.textSecondary
|
||||
}
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
variant: "caption"
|
||||
text: "Try adjusting your search or filter criteria."
|
||||
color: Theme.textMuted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// ── Pagination footer ───────────────────────────────────
|
||||
CTablePagination {
|
||||
page: root.page
|
||||
pageSize: root.pageSize
|
||||
totalFiltered: root.totalFiltered
|
||||
onPageRequested: function(newPage) { root.pageRequested(newPage) }
|
||||
}
|
||||
}
|
||||
}
|
||||
67
qml/MetaBuilder/CDatabaseStatsRow.qml
Normal file
67
qml/MetaBuilder/CDatabaseStatsRow.qml
Normal file
@@ -0,0 +1,67 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
FlexRow {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
property string totalRecords: "0"
|
||||
property string totalSize: "0 KB"
|
||||
property string activeBackend: ""
|
||||
property string adapterPattern: ""
|
||||
|
||||
CCard {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 72
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: "Total Records" }
|
||||
CText { variant: "h4"; text: root.totalRecords }
|
||||
}
|
||||
}
|
||||
|
||||
CCard {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 72
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: "Total Size" }
|
||||
CText { variant: "h4"; text: root.totalSize }
|
||||
}
|
||||
}
|
||||
|
||||
CCard {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 72
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: "Active Backend" }
|
||||
CText { variant: "h4"; text: root.activeBackend }
|
||||
}
|
||||
}
|
||||
|
||||
CCard {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 72
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: "Adapter Pattern" }
|
||||
CText { variant: "h4"; text: root.adapterPattern }
|
||||
}
|
||||
}
|
||||
}
|
||||
55
qml/MetaBuilder/CDeleteConfirmDialog.qml
Normal file
55
qml/MetaBuilder/CDeleteConfirmDialog.qml
Normal file
@@ -0,0 +1,55 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CDialog {
|
||||
id: root
|
||||
|
||||
property string itemName: ""
|
||||
property string description: "This action cannot be undone."
|
||||
|
||||
signal confirmed()
|
||||
|
||||
title: "Confirm Delete"
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
width: 320
|
||||
|
||||
CAlert {
|
||||
Layout.fillWidth: true
|
||||
severity: "warning"
|
||||
text: itemName.length > 0
|
||||
? "Are you sure you want to delete \"" + itemName + "\"?"
|
||||
: "No item selected."
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: "body2"
|
||||
text: root.description
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CButton {
|
||||
text: "Cancel"
|
||||
variant: "ghost"
|
||||
onClicked: root.visible = false
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
CButton {
|
||||
text: "Delete"
|
||||
variant: "danger"
|
||||
onClicked: {
|
||||
root.confirmed()
|
||||
root.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
qml/MetaBuilder/CDropdownMenu.qml
Normal file
127
qml/MetaBuilder/CDropdownMenu.qml
Normal file
@@ -0,0 +1,127 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CDropdownMenu.qml - Reusable dropdown menu panel with header, items, and footer
|
||||
*
|
||||
* Usage:
|
||||
* CDropdownMenu {
|
||||
* visible: false
|
||||
* width: 200
|
||||
* isDark: Theme.mode === "dark"
|
||||
* menuItems: [
|
||||
* { label: "Profile", icon: "P", action: "profile" },
|
||||
* { label: "Settings", icon: "S", action: "settings" }
|
||||
* ]
|
||||
* onItemClicked: function(action) { ... }
|
||||
*
|
||||
* headerContent: RowLayout { ... } // optional
|
||||
* footerContent: Rectangle { ... } // optional
|
||||
* }
|
||||
*/
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
property var menuItems: []
|
||||
|
||||
property alias headerContent: headerLoader.sourceComponent
|
||||
property alias footerContent: footerLoader.sourceComponent
|
||||
|
||||
signal itemClicked(string action)
|
||||
|
||||
radius: 12
|
||||
color: Theme.paper
|
||||
border.color: isDark ? Qt.rgba(1,1,1,0.1) : Qt.rgba(0,0,0,0.1)
|
||||
border.width: 1
|
||||
z: 100
|
||||
|
||||
height: menuCol.implicitHeight + 16
|
||||
width: 200
|
||||
|
||||
function close() {
|
||||
root.visible = false;
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: menuCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 8
|
||||
spacing: 2
|
||||
|
||||
// Optional header
|
||||
Loader {
|
||||
id: headerLoader
|
||||
Layout.fillWidth: true
|
||||
active: sourceComponent !== null
|
||||
}
|
||||
|
||||
// Divider after header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
height: 1
|
||||
color: isDark ? Qt.rgba(1,1,1,0.06) : Qt.rgba(0,0,0,0.06)
|
||||
visible: headerLoader.active
|
||||
}
|
||||
|
||||
// Menu items
|
||||
Repeater {
|
||||
model: root.menuItems
|
||||
delegate: Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 36
|
||||
radius: 8
|
||||
color: itemMA.containsMouse ? (isDark ? Qt.rgba(1,1,1,0.06) : Qt.rgba(0,0,0,0.04)) : "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
spacing: 10
|
||||
CText {
|
||||
text: modelData.icon
|
||||
font.pixelSize: 14
|
||||
color: modelData.color || Theme.textSecondary
|
||||
}
|
||||
CText {
|
||||
text: modelData.label
|
||||
font.pixelSize: 13
|
||||
color: modelData.color || Theme.text
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMA
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.itemClicked(modelData.action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Divider before footer
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
height: 1
|
||||
color: isDark ? Qt.rgba(1,1,1,0.06) : Qt.rgba(0,0,0,0.06)
|
||||
visible: footerLoader.active
|
||||
}
|
||||
|
||||
// Optional footer
|
||||
Loader {
|
||||
id: footerLoader
|
||||
Layout.fillWidth: true
|
||||
active: sourceComponent !== null
|
||||
}
|
||||
}
|
||||
}
|
||||
110
qml/MetaBuilder/CEntityForm.qml
Normal file
110
qml/MetaBuilder/CEntityForm.qml
Normal file
@@ -0,0 +1,110 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CEntityForm.qml - Create/edit entity form dialog
|
||||
*
|
||||
* Usage:
|
||||
* CEntityForm {
|
||||
* visible: createDialogOpen
|
||||
* entity: "User"
|
||||
* fields: [{ field: "username", label: "Username" }, ...]
|
||||
* isEdit: false
|
||||
* editId: ""
|
||||
* onSave: function(data) { addRecord(data) }
|
||||
* onCancel: createDialogOpen = false
|
||||
* }
|
||||
*/
|
||||
CDialog {
|
||||
id: root
|
||||
|
||||
property string entity: "" // Entity type name (e.g. "User")
|
||||
property var fields: [] // Array of { field, label, value? }
|
||||
property bool isEdit: false
|
||||
property string editId: "" // ID of record being edited (display only)
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
signal save(var data)
|
||||
signal cancel()
|
||||
|
||||
title: (isEdit ? "Edit " : "Create ") + entity + (isEdit && editId ? " - " + editId : "")
|
||||
|
||||
// Internal form data store
|
||||
property var _formData: ({})
|
||||
|
||||
function _setField(key, val) {
|
||||
var d = Object.assign({}, _formData);
|
||||
d[key] = val;
|
||||
_formData = d;
|
||||
}
|
||||
|
||||
function _resetForm() {
|
||||
var d = {};
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
d[fields[i].field] = fields[i].value || "";
|
||||
}
|
||||
_formData = d;
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) _resetForm();
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
width: 400
|
||||
spacing: 12
|
||||
|
||||
// Show ID in edit mode
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
active: root.isEdit && root.editId.length > 0
|
||||
sourceComponent: CText {
|
||||
variant: "caption"
|
||||
text: "ID: " + root.editId
|
||||
color: Theme.textSecondary
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.fields
|
||||
|
||||
delegate: CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: modelData.label
|
||||
text: modelData.value || ""
|
||||
placeholderText: "Enter " + modelData.label.toLowerCase() + "..."
|
||||
onTextChanged: root._setField(modelData.field, text)
|
||||
}
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
spacing: 8
|
||||
Item { Layout.fillWidth: true }
|
||||
CButton {
|
||||
text: "Cancel"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.cancel()
|
||||
}
|
||||
CButton {
|
||||
text: root.isEdit ? "Save" : "Create"
|
||||
variant: "primary"
|
||||
size: "sm"
|
||||
onClicked: {
|
||||
var data = Object.assign({}, root._formData);
|
||||
// Fill in any fields not yet touched
|
||||
for (var i = 0; i < root.fields.length; i++) {
|
||||
var f = root.fields[i];
|
||||
if (data[f.field] === undefined || data[f.field] === "") {
|
||||
data[f.field] = f.value || "";
|
||||
}
|
||||
}
|
||||
root.save(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
qml/MetaBuilder/CEntitySidebar.qml
Normal file
58
qml/MetaBuilder/CEntitySidebar.qml
Normal file
@@ -0,0 +1,58 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CEntitySidebar.qml - Left sidebar listing entity types with record counts
|
||||
*
|
||||
* Usage:
|
||||
* CEntitySidebar {
|
||||
* entities: ["User", "Session", "Workflow"]
|
||||
* entityIcons: ({ "User": "\u{1F464}", "Session": "\u{1F513}" })
|
||||
* entityCounts: ({ "User": 8, "Session": 6 })
|
||||
* selectedEntity: "User"
|
||||
* onEntitySelected: function(name) { selectedEntity = name }
|
||||
* }
|
||||
*/
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var entities: [] // Array of entity name strings
|
||||
property string selectedEntity: "" // Currently selected entity
|
||||
property var entityIcons: ({}) // Map of entity name -> icon emoji
|
||||
property var entityCounts: ({}) // Map of entity name -> record count
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
signal entitySelected(string name)
|
||||
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
color: Theme.surface
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
CText { variant: "h4"; text: "Entities" }
|
||||
CText { variant: "caption"; text: "God Panel Level 3"; color: Theme.textSecondary }
|
||||
|
||||
CDivider { Layout.fillWidth: true; Layout.topMargin: 8; Layout.bottomMargin: 4 }
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: root.entities
|
||||
spacing: 2
|
||||
clip: true
|
||||
delegate: CListItem {
|
||||
width: parent ? parent.width : 200
|
||||
title: modelData
|
||||
subtitle: (root.entityCounts[modelData] || 0) + " records"
|
||||
leadingIcon: root.entityIcons[modelData] || ""
|
||||
selected: root.selectedEntity === modelData
|
||||
onClicked: root.entitySelected(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
qml/MetaBuilder/CGodPanelHeader.qml
Normal file
101
qml/MetaBuilder/CGodPanelHeader.qml
Normal file
@@ -0,0 +1,101 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var configCounts: ({})
|
||||
property bool isDark: false
|
||||
|
||||
signal navigateLevel(int level)
|
||||
|
||||
// MD3 tonal surfaces
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
readonly property color onSurface: Theme.text
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
|
||||
implicitHeight: headerCol.implicitHeight + 40
|
||||
radius: 16
|
||||
clip: true
|
||||
color: surfaceContainerHigh
|
||||
border.width: 1
|
||||
border.color: outlineVariant
|
||||
|
||||
// Subtle gradient wash
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: root.isDark ? Qt.rgba(0.39, 0.4, 0.95, 0.04) : Qt.rgba(0.30, 0.40, 0.90, 0.08) }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: headerCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 20
|
||||
spacing: 14
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CText {
|
||||
text: "God Panel"
|
||||
font.pixelSize: 28
|
||||
font.weight: Font.Bold
|
||||
font.letterSpacing: -0.5
|
||||
color: root.onSurface
|
||||
}
|
||||
|
||||
CBadge {
|
||||
text: "Level 4"
|
||||
variant: "primary"
|
||||
}
|
||||
|
||||
CStatusBadge { status: "success"; text: "Active" }
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
CButton {
|
||||
text: "Level 1"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.navigateLevel(1)
|
||||
}
|
||||
CButton {
|
||||
text: "Level 2"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.navigateLevel(2)
|
||||
}
|
||||
CButton {
|
||||
text: "Level 3"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.navigateLevel(3)
|
||||
}
|
||||
}
|
||||
|
||||
// Config summary chips
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
CChip { text: root.configCounts.schemas + " Schemas"; variant: "primary" }
|
||||
CChip { text: root.configCounts.workflows + " Workflows"; variant: "primary" }
|
||||
CChip { text: root.configCounts.luaScripts + " Lua Scripts"; variant: "primary" }
|
||||
CChip { text: root.configCounts.packages + " Packages"; variant: "primary" }
|
||||
CChip { text: root.configCounts.pages + " Pages"; variant: "primary" }
|
||||
CChip { text: root.configCounts.components + " Components"; variant: "primary" }
|
||||
CChip { text: root.configCounts.users + " Users"; variant: "primary" }
|
||||
CChip { text: root.configCounts.dbBackends + " DB Backends"; variant: "primary" }
|
||||
}
|
||||
}
|
||||
}
|
||||
51
qml/MetaBuilder/CGodUserCard.qml
Normal file
51
qml/MetaBuilder/CGodUserCard.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
|
||||
required property var user
|
||||
property bool isDark: false
|
||||
|
||||
signal demote()
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 16
|
||||
|
||||
CAvatar { initials: root.user.initials }
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
Layout.fillWidth: true
|
||||
|
||||
FlexRow {
|
||||
spacing: 8
|
||||
CText { variant: "subtitle1"; text: root.user.username }
|
||||
CBadge { text: "L" + root.user.level; badgeColor: Theme.primary }
|
||||
CBadge { text: root.user.role; badgeColor: Theme.secondary }
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
spacing: 8
|
||||
CText { variant: "caption"; text: "Tenant: " + root.user.tenant; color: Theme.textSecondary }
|
||||
}
|
||||
}
|
||||
|
||||
CStatusBadge {
|
||||
status: root.user.status === "online" ? "success" : root.user.status === "away" ? "warning" : "error"
|
||||
text: root.user.status
|
||||
}
|
||||
|
||||
CButton {
|
||||
text: "Manage"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.demote()
|
||||
}
|
||||
}
|
||||
}
|
||||
115
qml/MetaBuilder/CHeroSection.qml
Normal file
115
qml/MetaBuilder/CHeroSection.qml
Normal file
@@ -0,0 +1,115 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string platformVersion: "0.9.1"
|
||||
property bool isDark: false
|
||||
|
||||
signal getStarted()
|
||||
signal openStorybook()
|
||||
signal openPackages()
|
||||
|
||||
// MD3 tonal surfaces
|
||||
readonly property color accentBlue: "#6366F1"
|
||||
readonly property color primaryContainer: isDark
|
||||
? Qt.rgba(accentBlue.r, accentBlue.g, accentBlue.b, 0.15)
|
||||
: Qt.rgba(accentBlue.r, accentBlue.g, accentBlue.b, 0.12)
|
||||
readonly property color onSurface: Theme.text
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
|
||||
color: "transparent"
|
||||
implicitHeight: 400
|
||||
|
||||
// Blue gradient wash
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: isDark ? Qt.rgba(0.39, 0.4, 0.95, 0.06) : Qt.rgba(0.30, 0.40, 0.90, 0.12) }
|
||||
GradientStop { position: 0.7; color: isDark ? "transparent" : Qt.rgba(0.35, 0.45, 0.88, 0.04) }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: 16
|
||||
width: Math.min(parent.width - 80, 720)
|
||||
spacing: 16
|
||||
|
||||
// Version pill
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
width: vLabel.implicitWidth + 24
|
||||
height: 28
|
||||
radius: 14
|
||||
color: primaryContainer
|
||||
|
||||
CText {
|
||||
id: vLabel
|
||||
anchors.centerIn: parent
|
||||
text: "v" + platformVersion
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
color: accentBlue
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
text: "MetaBuilder"
|
||||
font.pixelSize: 52
|
||||
font.weight: Font.Black
|
||||
font.letterSpacing: -2
|
||||
color: onSurface
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
CText {
|
||||
text: "The universal platform for building data-driven applications."
|
||||
font.pixelSize: 17
|
||||
color: onSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
CText {
|
||||
text: "95% JSON config \u00B7 5% infrastructure \u00B7 Desktop + Web + CLI"
|
||||
font.pixelSize: 13
|
||||
font.family: "monospace"
|
||||
color: onSurfaceVariant
|
||||
opacity: isDark ? 0.4 : 0.55
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 20
|
||||
spacing: 12
|
||||
|
||||
CButton {
|
||||
text: "Get Started"
|
||||
variant: "primary"
|
||||
size: "lg"
|
||||
onClicked: root.getStarted()
|
||||
}
|
||||
CButton {
|
||||
text: "Storybook"
|
||||
variant: "ghost"
|
||||
size: "lg"
|
||||
onClicked: root.openStorybook()
|
||||
}
|
||||
CButton {
|
||||
text: "Packages"
|
||||
variant: "ghost"
|
||||
size: "lg"
|
||||
onClicked: root.openPackages()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
qml/MetaBuilder/CLanguageSelector.qml
Normal file
45
qml/MetaBuilder/CLanguageSelector.qml
Normal file
@@ -0,0 +1,45 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
// Language selector pill (e.g. "EN")
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string currentLanguage: "EN"
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: langText.implicitWidth + 20
|
||||
height: 28
|
||||
|
||||
// ── MD3 palette ──
|
||||
readonly property color surfaceContainer: isDark ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0.31, 0.31, 0.44, 0.06)
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 14
|
||||
color: surfaceContainer
|
||||
border.color: outlineVariant
|
||||
border.width: 1
|
||||
|
||||
CText {
|
||||
id: langText
|
||||
anchors.centerIn: parent
|
||||
text: root.currentLanguage
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Bold
|
||||
font.family: "monospace"
|
||||
color: Theme.textSecondary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
115
qml/MetaBuilder/CLevelCard.qml
Normal file
115
qml/MetaBuilder/CLevelCard.qml
Normal file
@@ -0,0 +1,115 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int level: 1
|
||||
property string name: ""
|
||||
property color accent: "#94A3B8"
|
||||
property string desc: ""
|
||||
property var tags: []
|
||||
property bool locked: false
|
||||
property bool isDark: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
// MD3 tonal surfaces
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
readonly property color surfaceContainerHighest: isDark ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(0.31, 0.31, 0.44, 0.14)
|
||||
readonly property color onSurface: Theme.text
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
|
||||
radius: 16
|
||||
clip: true
|
||||
color: lvlMA.containsMouse ? surfaceContainerHighest : surfaceContainerHigh
|
||||
border.color: lvlMA.containsMouse ? accent : outlineVariant
|
||||
border.width: 1
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 200 } }
|
||||
|
||||
MouseArea {
|
||||
id: lvlMA
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 28; height: 28; radius: 8
|
||||
color: Qt.rgba(accent.r, accent.g, accent.b, isDark ? 0.2 : 0.15)
|
||||
|
||||
CText {
|
||||
anchors.centerIn: parent
|
||||
text: root.level.toString()
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Bold
|
||||
color: accent
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
text: root.name
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.DemiBold
|
||||
color: onSurface
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
CText {
|
||||
visible: root.locked
|
||||
text: "\uD83D\uDD12"
|
||||
font.pixelSize: 14
|
||||
opacity: isDark ? 0.3 : 0.4
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
text: root.desc
|
||||
font.pixelSize: 13
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
color: onSurfaceVariant
|
||||
lineHeight: 1.4
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.tags
|
||||
Rectangle {
|
||||
width: tText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 8
|
||||
color: Qt.rgba(accent.r, accent.g, accent.b, isDark ? 0.12 : 0.10)
|
||||
|
||||
CText {
|
||||
id: tText
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
color: accent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
qml/MetaBuilder/CLevelReferenceCard.qml
Normal file
94
qml/MetaBuilder/CLevelReferenceCard.qml
Normal file
@@ -0,0 +1,94 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string levelName: ""
|
||||
property string role: ""
|
||||
property string description: ""
|
||||
property color accent: "#6366F1"
|
||||
property int levelNumber: 1
|
||||
property bool isDark: false
|
||||
|
||||
// MD3 tonal surfaces
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
readonly property color onSurface: Theme.text
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
|
||||
implicitHeight: lvlCardCol.implicitHeight + 32
|
||||
radius: 16
|
||||
clip: true
|
||||
color: surfaceContainerHigh
|
||||
border.width: 1
|
||||
border.color: outlineVariant
|
||||
|
||||
// Colored accent bar on the left
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 4
|
||||
color: root.accent
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: lvlCardCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: 20
|
||||
anchors.rightMargin: 20
|
||||
anchors.topMargin: 16
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
// Level number badge
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 8
|
||||
color: Qt.rgba(root.accent.r, root.accent.g, root.accent.b, root.isDark ? 0.2 : 0.15)
|
||||
|
||||
CText {
|
||||
anchors.centerIn: parent
|
||||
text: root.levelNumber.toString()
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Bold
|
||||
color: root.accent
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
text: root.levelName
|
||||
font.pixelSize: 15
|
||||
font.weight: Font.DemiBold
|
||||
color: root.onSurface
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
CChip {
|
||||
text: root.role
|
||||
chipColor: root.accent
|
||||
variant: "filter"
|
||||
selected: true
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
text: root.description
|
||||
font.pixelSize: 13
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
color: root.onSurfaceVariant
|
||||
lineHeight: 1.4
|
||||
}
|
||||
}
|
||||
}
|
||||
66
qml/MetaBuilder/CLoginForm.qml
Normal file
66
qml/MetaBuilder/CLoginForm.qml
Normal file
@@ -0,0 +1,66 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool isDark: false
|
||||
property bool loading: false
|
||||
property string errorMessage: ""
|
||||
|
||||
// Expose fields so parent can set them programmatically
|
||||
property alias username: usernameField.text
|
||||
property alias password: passwordField.text
|
||||
|
||||
signal login(string username, string password)
|
||||
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
|
||||
implicitHeight: formCol.implicitHeight + 48
|
||||
radius: 16
|
||||
color: surfaceContainerHigh
|
||||
border.color: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: formCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 24
|
||||
spacing: 16
|
||||
|
||||
CTextField {
|
||||
id: usernameField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Username"
|
||||
enabled: !root.loading
|
||||
}
|
||||
|
||||
CTextField {
|
||||
id: passwordField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Password"
|
||||
echoMode: TextInput.Password
|
||||
enabled: !root.loading
|
||||
onAccepted: root.login(usernameField.text, passwordField.text)
|
||||
}
|
||||
|
||||
CAlert {
|
||||
Layout.fillWidth: true
|
||||
visible: root.errorMessage.length > 0
|
||||
severity: "error"
|
||||
text: root.errorMessage
|
||||
}
|
||||
|
||||
CButton {
|
||||
Layout.fillWidth: true
|
||||
text: root.loading ? "Signing in..." : "Sign In"
|
||||
variant: "primary"
|
||||
enabled: !root.loading
|
||||
onClicked: root.login(usernameField.text, passwordField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
qml/MetaBuilder/CModActionCard.qml
Normal file
55
qml/MetaBuilder/CModActionCard.qml
Normal file
@@ -0,0 +1,55 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var action
|
||||
property bool isDark: false
|
||||
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 56
|
||||
radius: 12
|
||||
color: surfaceContainerHigh
|
||||
border.color: outlineVariant
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: 28
|
||||
radius: 2
|
||||
color: root.action.action === "Deleted" ? "#F43F5E" :
|
||||
root.action.action === "Warned" ? "#F59E0B" :
|
||||
root.action.action === "Muted" ? "#EF4444" :
|
||||
"#22C55E"
|
||||
}
|
||||
|
||||
CText {
|
||||
text: root.action.action
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
CText {
|
||||
text: root.action.target
|
||||
font.pixelSize: 13
|
||||
font.family: "monospace"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
CText {
|
||||
text: root.action.time
|
||||
font.pixelSize: 11
|
||||
color: root.onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
51
qml/MetaBuilder/CModStatsRow.qml
Normal file
51
qml/MetaBuilder/CModStatsRow.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property var stats
|
||||
property bool isDark: false
|
||||
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Repeater {
|
||||
model: root.stats
|
||||
delegate: Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 72
|
||||
radius: 12
|
||||
color: root.surfaceContainerHigh
|
||||
border.color: root.outlineVariant
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
CText {
|
||||
text: modelData.value
|
||||
font.pixelSize: 22
|
||||
font.weight: Font.Bold
|
||||
font.family: "monospace"
|
||||
color: modelData.color
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
CText {
|
||||
text: modelData.label
|
||||
font.pixelSize: 9
|
||||
font.family: "monospace"
|
||||
font.letterSpacing: 1.5
|
||||
color: root.onSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
qml/MetaBuilder/CNavBar.qml
Normal file
42
qml/MetaBuilder/CNavBar.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
// Centered level navigation buttons for the app bar
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string currentView: "frontpage"
|
||||
property int currentLevel: 1
|
||||
property var levels: [
|
||||
{ label: "Home", level: 1, view: "frontpage" },
|
||||
{ label: "User", level: 2, view: "dashboard" },
|
||||
{ label: "Mod", level: 3, view: "moderator" },
|
||||
{ label: "Admin", level: 4, view: "admin" },
|
||||
{ label: "God", level: 5, view: "god-panel" },
|
||||
{ label: "Super", level: 6, view: "supergod" }
|
||||
]
|
||||
|
||||
signal navigate(string view)
|
||||
|
||||
implicitWidth: navRow.implicitWidth
|
||||
implicitHeight: navRow.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: navRow
|
||||
anchors.fill: parent
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.levels
|
||||
delegate: CButton {
|
||||
visible: modelData.level <= root.currentLevel
|
||||
text: modelData.label
|
||||
variant: root.currentView === modelData.view ? "default" : "text"
|
||||
size: "sm"
|
||||
onClicked: root.navigate(modelData.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
qml/MetaBuilder/CNodePalette.qml
Normal file
153
qml/MetaBuilder/CNodePalette.qml
Normal file
@@ -0,0 +1,153 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string searchText: ""
|
||||
property string selectedGroup: ""
|
||||
property bool isDark: false
|
||||
|
||||
signal nodeDoubleClicked(string nodeType)
|
||||
|
||||
spacing: 8
|
||||
|
||||
function groupColor(nodeType) {
|
||||
var prefix = nodeType ? nodeType.split(".")[0] : ""
|
||||
switch (prefix) {
|
||||
case "metabuilder": return Theme.success
|
||||
case "logic": return Theme.warning
|
||||
case "transform":
|
||||
case "packagerepo": return "#FF9800"
|
||||
case "sdl":
|
||||
case "graphics": return "#2196F3"
|
||||
case "integration": return "#9C27B0"
|
||||
case "io": return "#00BCD4"
|
||||
default: return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
CText { variant: "h4"; text: "Node Palette" }
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: NodeRegistry.nodeCount + " types"
|
||||
}
|
||||
}
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Search nodes..."
|
||||
text: root.searchText
|
||||
onTextChanged: root.searchText = text
|
||||
}
|
||||
|
||||
// Group filter chips
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
CChip {
|
||||
text: "All"
|
||||
selected: root.selectedGroup === ""
|
||||
chipColor: Theme.primary
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.selectedGroup = ""
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: NodeRegistry.groups
|
||||
CChip {
|
||||
text: modelData
|
||||
selected: root.selectedGroup === modelData
|
||||
chipColor: root.groupColor(modelData + ".x")
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.selectedGroup = (root.selectedGroup === modelData) ? "" : modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Node type list
|
||||
ListView {
|
||||
id: paletteList
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 2
|
||||
|
||||
model: {
|
||||
var nodes = root.searchText
|
||||
? NodeRegistry.searchNodes(root.searchText)
|
||||
: (root.selectedGroup ? NodeRegistry.nodesByGroup(root.selectedGroup) : NodeRegistry.nodeTypes)
|
||||
return nodes
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: paletteList.width
|
||||
height: 40
|
||||
radius: 4
|
||||
color: paletteMA.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
border.color: paletteMA.containsMouse ? Theme.border : "transparent"
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
width: 6
|
||||
height: 24
|
||||
radius: 3
|
||||
color: root.groupColor(modelData.name || "")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
CText {
|
||||
variant: "body2"
|
||||
text: modelData.displayName || modelData.name || ""
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: modelData.group || ""
|
||||
font.pixelSize: 9
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: paletteMA
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onDoubleClicked: {
|
||||
root.nodeDoubleClicked(modelData.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Drag to canvas
|
||||
Drag.active: paletteDragHandler.active
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
Drag.mimeData: ({ "text/node-type": modelData.name || "" })
|
||||
|
||||
DragHandler {
|
||||
id: paletteDragHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
238
qml/MetaBuilder/CNodePropertiesPanel.qml
Normal file
238
qml/MetaBuilder/CNodePropertiesPanel.qml
Normal file
@@ -0,0 +1,238 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var node: null
|
||||
property bool isDark: false
|
||||
property var workflowVariables: ({})
|
||||
|
||||
signal nameChanged(string name)
|
||||
signal parameterChanged(string key, string value)
|
||||
signal deleteRequested()
|
||||
signal closed()
|
||||
|
||||
color: Theme.paper
|
||||
border.color: Theme.border
|
||||
border.width: node ? 1 : 0
|
||||
clip: true
|
||||
visible: node !== null
|
||||
|
||||
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
|
||||
function groupColor(nodeType) {
|
||||
var prefix = nodeType ? nodeType.split(".")[0] : ""
|
||||
switch (prefix) {
|
||||
case "metabuilder": return Theme.success
|
||||
case "logic": return Theme.warning
|
||||
case "transform":
|
||||
case "packagerepo": return "#FF9800"
|
||||
case "sdl":
|
||||
case "graphics": return "#2196F3"
|
||||
case "integration": return "#9C27B0"
|
||||
case "io": return "#00BCD4"
|
||||
default: return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
visible: root.node !== null
|
||||
|
||||
// Header
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
CText { variant: "h4"; text: "Node Properties" }
|
||||
Item { Layout.fillWidth: true }
|
||||
CButton {
|
||||
text: "Delete"
|
||||
variant: "danger"
|
||||
size: "sm"
|
||||
onClicked: root.deleteRequested()
|
||||
}
|
||||
CButton {
|
||||
text: "X"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.closed()
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// Type badge
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
CText { variant: "body2"; text: "Type" }
|
||||
CChip {
|
||||
text: root.node ? root.node.type : ""
|
||||
chipColor: root.node ? groupColor(root.node.type) : Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
// Name field
|
||||
CText { variant: "body2"; text: "Name" }
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
text: root.node ? root.node.name : ""
|
||||
onTextChanged: {
|
||||
if (root.node && text !== root.node.name) {
|
||||
root.nameChanged(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Position display
|
||||
CText { variant: "body2"; text: "Position" }
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: root.node ? "x: " + Math.round(root.node.position[0]) + " y: " + Math.round(root.node.position[1]) : ""
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// Parameters
|
||||
CText { variant: "body2"; text: "Parameters"; font.bold: true }
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(contentHeight, 200)
|
||||
clip: true
|
||||
spacing: 8
|
||||
|
||||
model: {
|
||||
if (!root.node) return []
|
||||
var regEntry = NodeRegistry.nodeType(root.node.type)
|
||||
return regEntry ? (regEntry.properties || []) : []
|
||||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: parent ? parent.width : 250
|
||||
spacing: 4
|
||||
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: modelData.displayName || modelData.name
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: {
|
||||
if (modelData.options && modelData.options.length > 0) return selectComp
|
||||
return textFieldComp
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textFieldComp
|
||||
CTextField {
|
||||
text: root.node && root.node.parameters
|
||||
? (root.node.parameters[modelData.name] || modelData.default || "") : ""
|
||||
placeholderText: modelData.description || ""
|
||||
onTextChanged: {
|
||||
if (root.node) {
|
||||
root.parameterChanged(modelData.name, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: selectComp
|
||||
CSelect {
|
||||
model: {
|
||||
var opts = modelData.options || []
|
||||
var labels = []
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
labels.push(opts[i].name || opts[i].value || "")
|
||||
}
|
||||
return labels
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
// Inputs/Outputs display
|
||||
CText { variant: "body2"; text: "Ports"; font.bold: true }
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: "Inputs" }
|
||||
Repeater {
|
||||
model: root.node ? (root.node.inputs || []) : []
|
||||
CChip {
|
||||
text: modelData.displayName || modelData.name
|
||||
chipColor: Theme.primary
|
||||
}
|
||||
}
|
||||
CText {
|
||||
visible: !root.node || !root.node.inputs || root.node.inputs.length === 0
|
||||
variant: "caption"
|
||||
text: "None"
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: "Outputs" }
|
||||
Repeater {
|
||||
model: root.node ? (root.node.outputs || []) : []
|
||||
CChip {
|
||||
text: modelData.displayName || modelData.name
|
||||
chipColor: Theme.success
|
||||
}
|
||||
}
|
||||
CText {
|
||||
visible: !root.node || !root.node.outputs || root.node.outputs.length === 0
|
||||
variant: "caption"
|
||||
text: "None"
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workflow Variables section
|
||||
CDivider { Layout.fillWidth: true; visible: Object.keys(root.workflowVariables).length > 0 }
|
||||
|
||||
CText {
|
||||
variant: "body2"
|
||||
text: "Workflow Variables"
|
||||
font.bold: true
|
||||
visible: Object.keys(root.workflowVariables).length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Object.keys(root.workflowVariables)
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
CText { variant: "caption"; text: modelData + ":" }
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: {
|
||||
var v = root.workflowVariables[modelData]
|
||||
return v ? (v.defaultValue !== undefined ? String(v.defaultValue) : "") : ""
|
||||
}
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
53
qml/MetaBuilder/CNotificationBell.qml
Normal file
53
qml/MetaBuilder/CNotificationBell.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
// Bell icon with red notification dot
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool hasNotifications: true
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: 32
|
||||
height: 32
|
||||
|
||||
// ── MD3 palette ──
|
||||
readonly property color surfaceContainer: isDark ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0.31, 0.31, 0.44, 0.06)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 16
|
||||
color: bellMA.containsMouse ? surfaceContainer : "transparent"
|
||||
|
||||
CText {
|
||||
anchors.centerIn: parent
|
||||
text: "\uD83D\uDD14"
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
// Notification dot
|
||||
Rectangle {
|
||||
visible: root.hasNotifications
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 2
|
||||
anchors.rightMargin: 4
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: "#F43F5E"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: bellMA
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
37
qml/MetaBuilder/CNotificationEmptyState.qml
Normal file
37
qml/MetaBuilder/CNotificationEmptyState.qml
Normal file
@@ -0,0 +1,37 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
|
||||
property string filterLabel: "All"
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 40
|
||||
spacing: 16
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
CText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
variant: "h2"
|
||||
text: "\u{1F514}"
|
||||
}
|
||||
|
||||
CText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
variant: "h4"
|
||||
text: filterLabel === "All" ? "No notifications" : "No " + filterLabel.toLowerCase() + " notifications"
|
||||
}
|
||||
|
||||
CText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
variant: "body2"
|
||||
text: "When there are new notifications, they will appear here."
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
150
qml/MetaBuilder/CNotificationItem.qml
Normal file
150
qml/MetaBuilder/CNotificationItem.qml
Normal file
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
height: notifContent.implicitHeight + 24
|
||||
radius: 6
|
||||
|
||||
property var notification
|
||||
property bool isRead: notification ? notification.read : true
|
||||
|
||||
signal markRead()
|
||||
signal dismiss()
|
||||
|
||||
color: isRead ? "transparent" : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.04)
|
||||
|
||||
function typeIcon(type) {
|
||||
switch (type) {
|
||||
case "system": return "\u2699"
|
||||
case "alert": return "\u26A0"
|
||||
case "warning": return "\u26A0"
|
||||
case "info": return "\u2139"
|
||||
default: return "\u2709"
|
||||
}
|
||||
}
|
||||
|
||||
function typeColor(type) {
|
||||
switch (type) {
|
||||
case "system": return "#2196f3"
|
||||
case "alert": return "#f44336"
|
||||
case "warning": return "#ff9800"
|
||||
case "info": return "#4caf50"
|
||||
default: return "#9e9e9e"
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimestamp(ts) {
|
||||
if (ts.indexOf("2026-03-18") === 0) return "Today " + ts.substring(11)
|
||||
if (ts.indexOf("2026-03-17") === 0) return "Yesterday " + ts.substring(11)
|
||||
return ts
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: parent.height - 8
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 2
|
||||
color: root.notification ? typeColor(root.notification.type) : "#9e9e9e"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: notifContent
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 12
|
||||
anchors.topMargin: 12
|
||||
anchors.bottomMargin: 12
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
color: root.notification ? Qt.rgba(typeColor(root.notification.type).r, typeColor(root.notification.type).g, typeColor(root.notification.type).b, 0.15) : "transparent"
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
CText {
|
||||
anchors.centerIn: parent
|
||||
text: root.notification ? typeIcon(root.notification.type) : ""
|
||||
variant: "body1"
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
CText {
|
||||
variant: root.isRead ? "body1" : "subtitle1"
|
||||
text: root.notification ? root.notification.title : ""
|
||||
font.bold: !root.isRead
|
||||
}
|
||||
|
||||
CBadge {
|
||||
text: root.notification ? root.notification.type : ""
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: root.notification ? formatTimestamp(root.notification.timestamp) : ""
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
variant: "body2"
|
||||
text: root.notification ? root.notification.message : ""
|
||||
opacity: root.isRead ? 0.6 : 0.85
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: 4
|
||||
|
||||
CButton {
|
||||
visible: !root.isRead
|
||||
text: "Read"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.markRead()
|
||||
}
|
||||
|
||||
CButton {
|
||||
text: "Dismiss"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CDivider {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: root.markRead()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
54
qml/MetaBuilder/CNotificationToggles.qml
Normal file
54
qml/MetaBuilder/CNotificationToggles.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CNotificationToggles - Data-driven notification preference toggles.
|
||||
*
|
||||
* model: JSON array of { id, title, description }
|
||||
* values: Object mapping id → bool (e.g. { emailNotifications: true })
|
||||
* Emits toggled(id, value) when any switch changes.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
property var model: []
|
||||
property var values: ({})
|
||||
signal toggled(string id, bool value)
|
||||
|
||||
Repeater {
|
||||
model: root.model
|
||||
|
||||
delegate: ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
CDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: index > 0
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: index === 0 ? 4 : 0
|
||||
spacing: 12
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
CText { variant: "subtitle2"; text: modelData.title }
|
||||
CText { variant: "caption"; text: modelData.description; opacity: 0.6 }
|
||||
}
|
||||
|
||||
CSwitch {
|
||||
checked: root.values[modelData.id] || false
|
||||
onToggled: function(value) {
|
||||
root.toggled(modelData.id, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
qml/MetaBuilder/CProfileForm.qml
Normal file
82
qml/MetaBuilder/CProfileForm.qml
Normal file
@@ -0,0 +1,82 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CProfileForm - Editable profile fields (display name, email, bio).
|
||||
*
|
||||
* Expects profile: { displayName, email, bio }.
|
||||
* Emits save(data) with the edited field values.
|
||||
*/
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
variant: "filled"
|
||||
|
||||
property var profile: ({ displayName: "", email: "", bio: "" })
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
signal save(var data)
|
||||
|
||||
// ── Local editable copies ───────────────────────────────────
|
||||
property string editDisplayName: profile.displayName || ""
|
||||
property string editEmail: profile.email || ""
|
||||
property string editBio: profile.bio || ""
|
||||
|
||||
onProfileChanged: {
|
||||
editDisplayName = (profile && profile.displayName) ? profile.displayName : ""
|
||||
editEmail = (profile && profile.email) ? profile.email : ""
|
||||
editBio = (profile && profile.bio) ? profile.bio : ""
|
||||
}
|
||||
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
variant: "h4"
|
||||
text: "Edit Profile"
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 8 }
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
Item { Layout.preferredHeight: 14 }
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Display Name"
|
||||
placeholderText: "Enter display name"
|
||||
text: root.editDisplayName
|
||||
onTextChanged: root.editDisplayName = text
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 14 }
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Email"
|
||||
placeholderText: "Enter email address"
|
||||
text: root.editEmail
|
||||
onTextChanged: root.editEmail = text
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 14 }
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Bio"
|
||||
placeholderText: "Tell us about yourself..."
|
||||
text: root.editBio
|
||||
onTextChanged: root.editBio = text
|
||||
}
|
||||
|
||||
function getData() {
|
||||
return {
|
||||
displayName: editDisplayName,
|
||||
email: editEmail,
|
||||
bio: editBio
|
||||
}
|
||||
}
|
||||
}
|
||||
75
qml/MetaBuilder/CProfileHeader.qml
Normal file
75
qml/MetaBuilder/CProfileHeader.qml
Normal file
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CProfileHeader - User profile header with large avatar, name,
|
||||
* role, level badge, and member-since caption.
|
||||
*/
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
variant: "filled"
|
||||
|
||||
property string username: ""
|
||||
property int level: 1
|
||||
property string role: ""
|
||||
property string email: ""
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
property string memberSince: "January 15, 2026"
|
||||
|
||||
readonly property color onSurfaceVariant: isDark
|
||||
? Theme.textSecondary : Theme.textSecondary
|
||||
|
||||
function userInitials() {
|
||||
var name = username
|
||||
if (!name || name.length === 0) return "??"
|
||||
var parts = name.split(" ")
|
||||
if (parts.length >= 2)
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase()
|
||||
return name.substring(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 16
|
||||
|
||||
CAvatar {
|
||||
initials: root.userInitials()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
variant: "h3"
|
||||
text: root.username
|
||||
}
|
||||
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
variant: "body2"
|
||||
text: root.email
|
||||
color: root.onSurfaceVariant
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
spacing: 8
|
||||
CBadge { text: root.role; badgeColor: Theme.primary }
|
||||
CBadge { text: "Level " + root.level; badgeColor: Theme.info }
|
||||
}
|
||||
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
variant: "caption"
|
||||
text: "Member since " + root.memberSince
|
||||
color: root.onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
qml/MetaBuilder/CQuickActions.qml
Normal file
37
qml/MetaBuilder/CQuickActions.qml
Normal file
@@ -0,0 +1,37 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
|
||||
property var actions: []
|
||||
property bool isDark: false
|
||||
|
||||
signal actionClicked(string view)
|
||||
|
||||
variant: "filled"
|
||||
|
||||
CText {
|
||||
Layout.fillWidth: true
|
||||
variant: "h4"
|
||||
text: "Quick Actions"
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 12 }
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Repeater {
|
||||
model: root.actions
|
||||
delegate: CButton {
|
||||
text: modelData.label
|
||||
variant: modelData.variant || "default"
|
||||
onClicked: root.actionClicked(modelData.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
qml/MetaBuilder/CQuickLoginCard.qml
Normal file
85
qml/MetaBuilder/CQuickLoginCard.qml
Normal file
@@ -0,0 +1,85 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string username: ""
|
||||
property string password: ""
|
||||
property string label: ""
|
||||
property int level: 1
|
||||
property color accent: "#6366F1"
|
||||
property bool isDark: false
|
||||
|
||||
signal login()
|
||||
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
readonly property color surfaceContainerHighest: isDark ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(0.31, 0.31, 0.44, 0.14)
|
||||
readonly property color onSurface: Theme.text
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
|
||||
implicitHeight: 60
|
||||
radius: 16
|
||||
color: cMA.containsMouse ? surfaceContainerHighest : surfaceContainerHigh
|
||||
border.color: cMA.containsMouse ? accent : outlineVariant
|
||||
border.width: 1
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
MouseArea {
|
||||
id: cMA
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.login()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 32; height: 32; radius: 10
|
||||
color: Qt.rgba(accent.r, accent.g, accent.b, isDark ? 0.2 : 0.15)
|
||||
|
||||
CText {
|
||||
anchors.centerIn: parent
|
||||
text: root.level.toString()
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Bold
|
||||
color: accent
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 1
|
||||
|
||||
CText {
|
||||
text: root.label
|
||||
font.pixelSize: 14
|
||||
font.weight: Font.DemiBold
|
||||
color: onSurface
|
||||
}
|
||||
CText {
|
||||
text: root.username + " / " + root.password
|
||||
font.pixelSize: 11
|
||||
font.family: "monospace"
|
||||
color: onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
text: "\u2192"
|
||||
font.pixelSize: 18
|
||||
color: onSurfaceVariant
|
||||
opacity: cMA.containsMouse ? 1.0 : 0.3
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
70
qml/MetaBuilder/CReportCard.qml
Normal file
70
qml/MetaBuilder/CReportCard.qml
Normal file
@@ -0,0 +1,70 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var report
|
||||
property bool isDark: false
|
||||
|
||||
signal dismiss()
|
||||
signal takeAction()
|
||||
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 72
|
||||
radius: 12
|
||||
color: surfaceContainerHigh
|
||||
border.color: outlineVariant
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
// Type indicator bar
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: 36
|
||||
radius: 2
|
||||
color: root.report.type === "spam" ? "#F43F5E" :
|
||||
root.report.type === "abuse" ? "#EF4444" :
|
||||
"#F59E0B"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
CText {
|
||||
text: root.report.content
|
||||
font.pixelSize: 13
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
CText {
|
||||
text: root.report.type + " \u00B7 " + root.report.user + " \u00B7 " + root.report.reported
|
||||
font.pixelSize: 11
|
||||
color: root.onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
CButton {
|
||||
text: "Dismiss"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.dismiss()
|
||||
}
|
||||
CButton {
|
||||
text: "Action"
|
||||
variant: "primary"
|
||||
size: "sm"
|
||||
onClicked: root.takeAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
117
qml/MetaBuilder/CRouteEditPanel.qml
Normal file
117
qml/MetaBuilder/CRouteEditPanel.qml
Normal file
@@ -0,0 +1,117 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
|
||||
property var route: null
|
||||
property var layoutOptions: ["default", "sidebar", "dashboard", "blank"]
|
||||
property var levelOptions: [1, 2, 3, 4, 5]
|
||||
|
||||
signal fieldChanged(string field, var value)
|
||||
signal deleteRequested()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 14
|
||||
|
||||
CText { variant: "h4"; text: "Edit Route" }
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CText { variant: "caption"; text: "Path" }
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
text: root.route ? root.route.path : ""
|
||||
onTextChanged: {
|
||||
if (root.route && text !== root.route.path)
|
||||
root.fieldChanged("path", text)
|
||||
}
|
||||
}
|
||||
|
||||
CText { variant: "caption"; text: "Page Title" }
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
text: root.route ? root.route.title : ""
|
||||
onTextChanged: {
|
||||
if (root.route && text !== root.route.title)
|
||||
root.fieldChanged("title", text)
|
||||
}
|
||||
}
|
||||
|
||||
CText { variant: "caption"; text: "Required Level (1-5)" }
|
||||
CSelect {
|
||||
Layout.fillWidth: true
|
||||
model: levelOptions
|
||||
currentIndex: root.route ? root.route.level - 1 : 0
|
||||
onCurrentIndexChanged: {
|
||||
if (root.route) {
|
||||
var newLvl = currentIndex + 1
|
||||
if (newLvl !== root.route.level)
|
||||
root.fieldChanged("level", newLvl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CText { variant: "caption"; text: "Layout Type" }
|
||||
CSelect {
|
||||
Layout.fillWidth: true
|
||||
model: layoutOptions
|
||||
currentIndex: root.route ? layoutOptions.indexOf(root.route.layout) : 0
|
||||
onCurrentIndexChanged: {
|
||||
if (root.route) {
|
||||
var newLayoutVal = layoutOptions[currentIndex]
|
||||
if (newLayoutVal !== root.route.layout)
|
||||
root.fieldChanged("layout", newLayoutVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CText { variant: "body2"; text: "Enabled" }
|
||||
Item { Layout.fillWidth: true }
|
||||
CSwitch {
|
||||
checked: root.route ? root.route.enabled : false
|
||||
onCheckedChanged: {
|
||||
if (root.route && checked !== root.route.enabled)
|
||||
root.fieldChanged("enabled", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CText { variant: "caption"; text: "Permission Rules" }
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
text: root.route ? root.route.permissions : ""
|
||||
onTextChanged: {
|
||||
if (root.route && text !== root.route.permissions)
|
||||
root.fieldChanged("permissions", text)
|
||||
}
|
||||
}
|
||||
|
||||
CAlert {
|
||||
Layout.fillWidth: true
|
||||
severity: root.route && root.route.level >= 4 ? "warning" : "info"
|
||||
text: root.route && root.route.level >= 4
|
||||
? "High privilege route (level " + root.route.level + ")"
|
||||
: "Standard access route"
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
CButton {
|
||||
text: "Delete Route"
|
||||
variant: "danger"
|
||||
size: "sm"
|
||||
Layout.fillWidth: true
|
||||
onClicked: root.deleteRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
50
qml/MetaBuilder/CRouteTableHeader.qml
Normal file
50
qml/MetaBuilder/CRouteTableHeader.qml
Normal file
@@ -0,0 +1,50 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
height: 40
|
||||
color: Theme.surface
|
||||
radius: 4
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 0
|
||||
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: "ORDER"
|
||||
Layout.preferredWidth: 60
|
||||
}
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: "PATH"
|
||||
Layout.preferredWidth: 120
|
||||
}
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: "TITLE"
|
||||
Layout.preferredWidth: 120
|
||||
}
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: "LEVEL"
|
||||
Layout.preferredWidth: 60
|
||||
}
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: "LAYOUT"
|
||||
Layout.preferredWidth: 100
|
||||
}
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: "STATUS"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
109
qml/MetaBuilder/CRouteTableRow.qml
Normal file
109
qml/MetaBuilder/CRouteTableRow.qml
Normal file
@@ -0,0 +1,109 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var routeData
|
||||
property bool isSelected: false
|
||||
property int routeIndex: -1
|
||||
property int routeCount: 0
|
||||
|
||||
signal clicked()
|
||||
signal moveUp()
|
||||
signal moveDown()
|
||||
|
||||
width: parent ? parent.width : 400
|
||||
height: 48
|
||||
color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : (hoverHandler.hovered ? Theme.surface : "transparent")
|
||||
radius: 4
|
||||
|
||||
function levelColor(level) {
|
||||
if (level <= 1) return "#4caf50"
|
||||
if (level === 2) return "#8bc34a"
|
||||
if (level === 3) return "#ff9800"
|
||||
if (level === 4) return "#f44336"
|
||||
return "#9c27b0"
|
||||
}
|
||||
|
||||
HoverHandler { id: hoverHandler }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.preferredWidth: 60
|
||||
spacing: 2
|
||||
|
||||
CButton {
|
||||
text: "\u25B2"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
enabled: routeIndex > 0
|
||||
onClicked: root.moveUp()
|
||||
}
|
||||
CButton {
|
||||
text: "\u25BC"
|
||||
variant: "ghost"
|
||||
size: "sm"
|
||||
enabled: routeIndex < routeCount - 1
|
||||
onClicked: root.moveDown()
|
||||
}
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: "body2"
|
||||
text: routeData ? routeData.path : ""
|
||||
Layout.preferredWidth: 120
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
CText {
|
||||
variant: "body2"
|
||||
text: routeData ? routeData.title : ""
|
||||
Layout.preferredWidth: 120
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 60
|
||||
height: 24
|
||||
width: 32
|
||||
radius: 12
|
||||
color: routeData ? levelColor(routeData.level) : "#9e9e9e"
|
||||
|
||||
CText {
|
||||
anchors.centerIn: parent
|
||||
variant: "caption"
|
||||
text: routeData ? routeData.level.toString() : ""
|
||||
color: "#ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
CChip {
|
||||
text: routeData ? routeData.layout : ""
|
||||
Layout.preferredWidth: 100
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 10
|
||||
width: 10
|
||||
radius: 5
|
||||
color: routeData && routeData.enabled ? "#4caf50" : "#9e9e9e"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 10
|
||||
Layout.preferredHeight: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
52
qml/MetaBuilder/CServiceStatus.qml
Normal file
52
qml/MetaBuilder/CServiceStatus.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string name: ""
|
||||
property string status: "offline" // "online" | "standby" | "offline"
|
||||
property bool isDark: false
|
||||
|
||||
readonly property color accentBlue: "#6366F1"
|
||||
readonly property color accentAmber: "#F59E0B"
|
||||
readonly property color accentRose: "#F43F5E"
|
||||
readonly property color statusColor: status === "online" ? accentBlue
|
||||
: status === "standby" ? accentAmber : accentRose
|
||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
||||
readonly property color onSurface: Theme.text
|
||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
||||
|
||||
implicitHeight: 56
|
||||
radius: 12
|
||||
color: surfaceContainerHigh
|
||||
border.color: outlineVariant
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 8; height: 8; radius: 4
|
||||
color: statusColor
|
||||
}
|
||||
|
||||
CText {
|
||||
text: root.name
|
||||
font.pixelSize: 13
|
||||
color: onSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
CText {
|
||||
text: root.status
|
||||
font.pixelSize: 11
|
||||
font.family: "monospace"
|
||||
color: statusColor
|
||||
}
|
||||
}
|
||||
}
|
||||
37
qml/MetaBuilder/CSettingsSection.qml
Normal file
37
qml/MetaBuilder/CSettingsSection.qml
Normal file
@@ -0,0 +1,37 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
/**
|
||||
* CSettingsSection - Generic settings section with title and content slot.
|
||||
*
|
||||
* Usage:
|
||||
* CSettingsSection {
|
||||
* title: "Appearance"
|
||||
* isDark: Theme.mode === "dark"
|
||||
* CText { text: "Hello" }
|
||||
* }
|
||||
*/
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
|
||||
property string title: ""
|
||||
property bool isDark: Theme.mode === "dark"
|
||||
|
||||
default property alias content: contentColumn.data
|
||||
|
||||
CText {
|
||||
variant: "h4"
|
||||
text: root.title
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
}
|
||||
}
|
||||
95
qml/MetaBuilder/CSidebar.qml
Normal file
95
qml/MetaBuilder/CSidebar.qml
Normal file
@@ -0,0 +1,95 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
// Left sidebar with navigation header, static items, dynamic package items, Settings
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string currentView: "frontpage"
|
||||
property int currentLevel: 1
|
||||
property bool loggedIn: false
|
||||
|
||||
// PackageLoader helper — caller must provide this function
|
||||
// to convert a package object to a view name
|
||||
property var packageViewName: function(pkg) {
|
||||
return pkg.navLabel ? pkg.navLabel.toLowerCase().replace(/ /g, "-") : pkg.packageId
|
||||
}
|
||||
|
||||
signal navigate(string view)
|
||||
|
||||
visible: loggedIn
|
||||
color: Theme.paper
|
||||
border.color: Theme.border
|
||||
border.width: 1
|
||||
|
||||
implicitWidth: 220
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
CText {
|
||||
variant: "subtitle2"
|
||||
text: "Navigation"
|
||||
Layout.bottomMargin: 8
|
||||
}
|
||||
|
||||
// Static core nav items
|
||||
Repeater {
|
||||
model: {
|
||||
var items = [
|
||||
{ label: "Dashboard", view: "dashboard", icon: "~", level: 2 },
|
||||
{ label: "Profile", view: "profile", icon: "P", level: 2 },
|
||||
{ label: "Comments", view: "comments", icon: "C", level: 2 },
|
||||
{ label: "Mod Tools", view: "moderator", icon: "M", level: 3 },
|
||||
{ label: "Admin Panel", view: "admin", icon: "A", level: 4 },
|
||||
{ label: "God Panel", view: "god-panel", icon: "G", level: 5 },
|
||||
{ label: "Super God", view: "supergod", icon: "S", level: 6 }
|
||||
]
|
||||
return items.filter(function(item) { return item.level <= root.currentLevel })
|
||||
}
|
||||
|
||||
delegate: CListItem {
|
||||
Layout.fillWidth: true
|
||||
title: modelData.label
|
||||
leadingIcon: modelData.icon
|
||||
selected: root.currentView === modelData.view
|
||||
onClicked: root.navigate(modelData.view)
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic package nav items (from PackageLoader)
|
||||
Repeater {
|
||||
model: {
|
||||
var navPkgs = PackageLoader.navigablePackages()
|
||||
return navPkgs.filter(function(pkg) {
|
||||
var lvl = pkg.level ? pkg.level : 2
|
||||
return lvl <= root.currentLevel
|
||||
})
|
||||
}
|
||||
|
||||
delegate: CListItem {
|
||||
Layout.fillWidth: true
|
||||
title: modelData.navLabel ? modelData.navLabel : modelData.name
|
||||
leadingIcon: modelData.icon ? modelData.icon : modelData.name.charAt(0)
|
||||
selected: root.currentView === root.packageViewName(modelData)
|
||||
onClicked: root.navigate(root.packageViewName(modelData))
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CListItem {
|
||||
Layout.fillWidth: true
|
||||
title: "Settings"
|
||||
leadingIcon: "S"
|
||||
selected: root.currentView === "settings"
|
||||
onClicked: root.navigate("settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
57
qml/MetaBuilder/CSmtpSenderForm.qml
Normal file
57
qml/MetaBuilder/CSmtpSenderForm.qml
Normal file
@@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
property string fromName: ""
|
||||
property string fromEmail: ""
|
||||
|
||||
signal fromNameChanged(string value)
|
||||
signal fromEmailChanged(string value)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
CText { variant: "h4"; text: "Sender" }
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "From Name"
|
||||
placeholderText: "MetaBuilder"
|
||||
text: root.fromName
|
||||
onTextChanged: root.fromNameChanged(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "From Email"
|
||||
placeholderText: "noreply@example.com"
|
||||
text: root.fromEmail
|
||||
onTextChanged: root.fromEmailChanged(text)
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CText { variant: "caption"; text: "Preview" }
|
||||
|
||||
CPaper {
|
||||
Layout.fillWidth: true
|
||||
|
||||
CText {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
variant: "body2"
|
||||
text: root.fromName + " <" + root.fromEmail + ">"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
qml/MetaBuilder/CSmtpServerForm.qml
Normal file
110
qml/MetaBuilder/CSmtpServerForm.qml
Normal file
@@ -0,0 +1,110 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
|
||||
property string host: ""
|
||||
property string port: ""
|
||||
property string username: ""
|
||||
property string password: ""
|
||||
property int encryptionIndex: 1
|
||||
property var encryptionOptions: ["None", "TLS", "SSL"]
|
||||
property string connectionStatus: "untested"
|
||||
|
||||
signal hostEdited(string value)
|
||||
signal portEdited(string value)
|
||||
signal usernameEdited(string value)
|
||||
signal passwordEdited(string value)
|
||||
signal encryptionEdited(int index)
|
||||
signal testRequested()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
CText { variant: "h4"; text: "SMTP Server" }
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Host"
|
||||
placeholderText: "smtp.example.com"
|
||||
text: root.host
|
||||
onTextChanged: root.hostEdited(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Port"
|
||||
placeholderText: "587"
|
||||
text: root.port
|
||||
onTextChanged: root.portChanged(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Username"
|
||||
placeholderText: "user@example.com"
|
||||
text: root.username
|
||||
onTextChanged: root.usernameChanged(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Password"
|
||||
placeholderText: "Enter password"
|
||||
echoMode: TextInput.Password
|
||||
text: root.password
|
||||
onTextChanged: root.passwordChanged(text)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
CText { variant: "caption"; text: "Encryption" }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: root.encryptionOptions
|
||||
delegate: CButton {
|
||||
text: modelData
|
||||
variant: root.encryptionIndex === index ? "primary" : "ghost"
|
||||
size: "sm"
|
||||
onClicked: root.encryptionChanged(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
CButton {
|
||||
text: root.connectionStatus === "testing" ? "Testing..." : "Test Connection"
|
||||
variant: "primary"
|
||||
size: "sm"
|
||||
enabled: root.connectionStatus !== "testing"
|
||||
onClicked: root.testRequested()
|
||||
}
|
||||
|
||||
CStatusBadge {
|
||||
visible: root.connectionStatus === "success" || root.connectionStatus === "failed"
|
||||
status: root.connectionStatus === "success" ? "success" : "error"
|
||||
text: root.connectionStatus === "success" ? "OK" : "Fail"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
qml/MetaBuilder/CSmtpTemplateEditor.qml
Normal file
119
qml/MetaBuilder/CSmtpTemplateEditor.qml
Normal file
@@ -0,0 +1,119 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
property bool hasSelection: false
|
||||
property string templateName: ""
|
||||
property string templateSubject: ""
|
||||
property string templateBody: ""
|
||||
|
||||
signal nameChanged(string value)
|
||||
signal subjectChanged(string value)
|
||||
signal bodyChanged(string value)
|
||||
signal saveRequested()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
CText { variant: "h4"; text: "Template Editor" }
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
CButton {
|
||||
visible: root.hasSelection
|
||||
text: "Save Template"
|
||||
variant: "primary"
|
||||
size: "sm"
|
||||
onClicked: root.saveRequested()
|
||||
}
|
||||
}
|
||||
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
CText {
|
||||
visible: !root.hasSelection
|
||||
variant: "body2"
|
||||
text: "Select a template from the list to edit."
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 40
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: root.hasSelection
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 12
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Template Name"
|
||||
placeholderText: "e.g. Welcome Email"
|
||||
text: root.templateName
|
||||
onTextChanged: root.nameChanged(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Subject Template"
|
||||
placeholderText: "e.g. Welcome to {{app_name}}"
|
||||
text: root.templateSubject
|
||||
onTextChanged: root.subjectChanged(text)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 4
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
CText { variant: "caption"; text: "Body Template" }
|
||||
Item { Layout.fillWidth: true }
|
||||
CText {
|
||||
variant: "caption"
|
||||
text: "Supports {{variable}} placeholders"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 180
|
||||
color: Theme.surface
|
||||
border.color: Theme.border
|
||||
border.width: 1
|
||||
radius: 4
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
|
||||
TextArea {
|
||||
text: root.templateBody
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.text
|
||||
font.pixelSize: 13
|
||||
font.family: "monospace"
|
||||
background: null
|
||||
onTextChanged: root.bodyChanged(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
qml/MetaBuilder/CSmtpTemplateList.qml
Normal file
42
qml/MetaBuilder/CSmtpTemplateList.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
Layout.preferredWidth: 280
|
||||
Layout.fillHeight: true
|
||||
|
||||
property var templates: []
|
||||
property int selectedIndex: -1
|
||||
|
||||
signal templateSelected(int index)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 8
|
||||
|
||||
CText { variant: "h4"; text: "Templates" }
|
||||
CText { variant: "caption"; text: root.templates.length + " templates" }
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 200
|
||||
model: root.templates
|
||||
spacing: 2
|
||||
clip: true
|
||||
|
||||
delegate: CListItem {
|
||||
width: parent ? parent.width : 248
|
||||
title: modelData.name
|
||||
subtitle: modelData.id
|
||||
selected: root.selectedIndex === index
|
||||
onClicked: root.templateSelected(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
qml/MetaBuilder/CSmtpTestEmailForm.qml
Normal file
98
qml/MetaBuilder/CSmtpTestEmailForm.qml
Normal file
@@ -0,0 +1,98 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QmlComponents 1.0
|
||||
|
||||
CCard {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
|
||||
property string recipient: ""
|
||||
property string subject: "MetaBuilder SMTP Test"
|
||||
property string body: "This is a test email from MetaBuilder."
|
||||
property bool sending: false
|
||||
|
||||
signal recipientChanged(string value)
|
||||
signal subjectChanged(string value)
|
||||
signal bodyChanged(string value)
|
||||
signal sendRequested()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
CText { variant: "h4"; text: "Send Test Email" }
|
||||
CDivider { Layout.fillWidth: true }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Recipient"
|
||||
placeholderText: "test@example.com"
|
||||
text: root.recipient
|
||||
onTextChanged: root.recipientChanged(text)
|
||||
}
|
||||
|
||||
CTextField {
|
||||
Layout.fillWidth: true
|
||||
label: "Subject"
|
||||
placeholderText: "Test subject"
|
||||
text: root.subject
|
||||
onTextChanged: root.subjectChanged(text)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
CText { variant: "caption"; text: "Body" }
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 100
|
||||
color: Theme.surface
|
||||
border.color: Theme.border
|
||||
border.width: 1
|
||||
radius: 4
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
|
||||
TextArea {
|
||||
text: root.body
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.text
|
||||
font.pixelSize: 13
|
||||
background: null
|
||||
onTextChanged: root.bodyChanged(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlexRow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
CButton {
|
||||
text: root.sending ? "Sending..." : "Send Test Email"
|
||||
variant: "primary"
|
||||
size: "sm"
|
||||
enabled: !root.sending && root.recipient.length > 0
|
||||
onClicked: root.sendRequested()
|
||||
}
|
||||
|
||||
CText {
|
||||
visible: root.recipient.length === 0
|
||||
variant: "caption"
|
||||
text: "Enter a recipient to enable sending"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user