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:
2026-03-19 10:18:09 +00:00
parent 0405cdfa90
commit 786f91ec64
183 changed files with 11910 additions and 222 deletions

1
.gitignore vendored
View File

@@ -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
View File

@@ -0,0 +1 @@
/Users/rmac/Documents/GitHub/metabuilder/qml

View File

@@ -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)}
/>
)}
</>
)
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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) }
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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) }
}
}
}

View File

@@ -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

View 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
}

View 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()
}
}
}

View 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
}
}

View 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
}
}

View 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
}
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}
}

View 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 } }
}

View 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
}
}

View 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
}
}
}
}

View 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
}

View 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
}
}

View 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
}

View 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
}
}

View 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
}
}

View 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)
}
}
}

View 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
}
}

View File

@@ -0,0 +1,6 @@
import QtQuick
import QtQuick.Controls
MenuItem {
id: menuItem
}

View File

@@ -0,0 +1,10 @@
import QtQuick
import QtQuick.Controls
Menu {
id: menuProps
background: Rectangle {
color: "transparent"
}
property alias currentAction: menuProps.activeAction
}

View 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
}

View 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
}
}
}

View 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
}
}

View File

@@ -0,0 +1,8 @@
import QtQuick
import QtQuick.Controls
Popup {
id: popoverProps
property bool arrowVisible: true
property bool hasShadow: true
}

View 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
}
}
}
}

View 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()
}
}
}

View 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
}
}

View 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
}
}

View 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
}

View 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
}

View 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
View 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

View 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
}
}
}

View 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]
}
}
}

View 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
}
}
}
}
}

View 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) }
}
}
}
}
}
}

View 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 }
}
}
}
}
}
}

View 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
}
}
}
}
}

View 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()
}

View 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()
}
}
}

View 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()
}
}
}

View 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 = ""
}
}
}
}

View 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)
}
}
}
}
}

View 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()
}
}
}

View 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 }
}
}
}

View 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
}
}
}
}

View 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([])
}
}
}
}

View 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)
}
}
}
}

View 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) }
}
}
}

View 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 }
}
}
}

View 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
}
}
}
}
}

View 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
}
}
}

View 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);
}
}
}
}
}

View 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)
}
}
}
}

View 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" }
}
}
}

View 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()
}
}
}

View 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()
}
}
}
}

View 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()
}
}
}

View 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
}
}
}
}
}
}

View 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
}
}
}

View 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)
}
}
}

View 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
}
}
}

View 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
}
}
}
}
}

View 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)
}
}
}
}

View 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
}
}
}
}

View 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 }
}
}

View 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()
}
}
}

View 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
}
}
}

View 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
}
}

View 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)
}
}
}
}
}
}

View 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
}
}
}

View 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
}
}
}
}

View 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)
}
}
}
}

View 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 } }
}
}
}

View 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()
}
}
}

View 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()
}
}
}

View 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
}
}
}

View 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
}
}
}

View 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
}
}
}

View 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
}
}

View 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")
}
}
}

View 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 + ">"
}
}
}
}

View 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"
}
}
}
}

View 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)
}
}
}
}
}
}
}

View 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)
}
}
}
}

View 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