diff --git a/.gitignore b/.gitignore index 570f93a73..06100ab14 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,4 @@ bun.lockb # frontends/pastebin/public/pyodide/ # kept in fat repo .vscode/settings.json .act-env +/frontends/qt6/_build diff --git a/QmlComponents b/QmlComponents new file mode 120000 index 000000000..fafaa673a --- /dev/null +++ b/QmlComponents @@ -0,0 +1 @@ +/Users/rmac/Documents/GitHub/metabuilder/qml \ No newline at end of file diff --git a/frontends/emailclient/app/EmailClientContent.tsx b/frontends/emailclient/app/EmailClientContent.tsx index 37d7c5eaf..151ecc91e 100644 --- a/frontends/emailclient/app/EmailClientContent.tsx +++ b/frontends/emailclient/app/EmailClientContent.tsx @@ -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 - 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 { - 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 { - 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 = { - // 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 - - return ( - - {children && Array.isArray(children) ? ( - children.map((child, idx) => ( - typeof child === 'string' ? ( - {child} - ) : ( - - ) - )) - ) : children ? ( - - ) : null} - - ) -} +// ───────────────────────────────────────────────────────────────────────────── +// Email Client App +// ───────────────────────────────────────────────────────────────────────────── export default function EmailClientContent() { - const dispatch = useAppDispatch() - const [packageMetadata, setPackageMetadata] = useState(null) - const [pageConfig, setPageConfig] = useState(null) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) + const [activeFolder, setActiveFolder] = useState('inbox') + const [selectedEmailId, setSelectedEmailId] = useState(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 ( - - - Loading Email Client... + 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 = ( + + + - ) - } - - // Error state - if (error) { - return ( - - - Failed to load Email Client -

{error.message}

-
-
- ) - } - - // No package metadata - if (!packageMetadata) { - return ( - - - Email Client Package Not Found -

The email_client package could not be loaded. Please ensure it is installed and configured.

-
-
- ) - } - - // Render default layout if no page config - if (!pageConfig) { - return ( - -

{packageMetadata.name}

-

{packageMetadata.description}

-

Version: {packageMetadata.version}

- - Page configuration is loading or not available. Please check back soon. - -
- ) - } - - // Render declarative component from page config - return ( - - + ) + + // ── Header ─────────────────────────────────────────────────────────────── + const header = ( + + + 📧 + + MetaMail + + + + setSearchQuery(e.target.value)} + style={{ + width: '100%', + padding: '10px 16px', + borderRadius: '24px', + border: 'none', + backgroundColor: '#f1f3f4', + fontSize: '0.9375rem', + outline: 'none', + }} + /> + + + + ⚙️ + + + U + + + + ) + + // ── Main (thread list) ─────────────────────────────────────────────────── + const main = ( + + + + {activeFolder} {filteredEmails.length > 0 && `(${filteredEmails.length})`} + + + {filteredEmails.filter(e => !e.isRead).length} unread + + + {filteredEmails.length === 0 ? ( + + + {activeFolder === 'starred' ? '⭐' : activeFolder === 'trash' ? '🗑️' : '📭'} + + + {activeFolder === 'inbox' && searchQuery ? 'No results found' : `No messages in ${activeFolder}`} + + + ) : ( + + )} + + ) + + // ── Detail (selected email) ────────────────────────────────────────────── + const detail = selectedEmail ? ( + + + + + + 📥 + + + 🗑️ + + + ↩️ + + + ↪️ + + + + handleToggleStar(selectedEmail.id, starred)} + /> + + {selectedEmail.body} + + + + + + + ) : undefined + + return ( + <> + + {showCompose && ( + setShowCompose(false)} + /> + )} + + ) } diff --git a/frontends/emailclient/app/globals.css b/frontends/emailclient/app/globals.css index 92a7e7800..187d52632 100644 --- a/frontends/emailclient/app/globals.css +++ b/frontends/emailclient/app/globals.css @@ -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 { diff --git a/frontends/qt6/GodPanel.qml b/frontends/qt6/GodPanel.qml index 365905149..2b2e024ce 100644 --- a/frontends/qt6/GodPanel.qml +++ b/frontends/qt6/GodPanel.qml @@ -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 { diff --git a/frontends/qt6/generate_cmake.py b/frontends/qt6/generate_cmake.py index 9674606fe..acf8a3a41 100755 --- a/frontends/qt6/generate_cmake.py +++ b/frontends/qt6/generate_cmake.py @@ -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" diff --git a/frontends/qt6/qmllib/MetaBuilder/CDataTable.qml b/frontends/qt6/qmllib/MetaBuilder/CDataTable.qml index 4359f9702..6b477a6a8 100644 --- a/frontends/qt6/qmllib/MetaBuilder/CDataTable.qml +++ b/frontends/qt6/qmllib/MetaBuilder/CDataTable.qml @@ -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) } } } } diff --git a/frontends/qt6/qmllib/MetaBuilder/CSmtpServerForm.qml b/frontends/qt6/qmllib/MetaBuilder/CSmtpServerForm.qml index b076acf05..9600781ed 100644 --- a/frontends/qt6/qmllib/MetaBuilder/CSmtpServerForm.qml +++ b/frontends/qt6/qmllib/MetaBuilder/CSmtpServerForm.qml @@ -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) } } } diff --git a/frontends/qt6/qmllib/MetaBuilder/CTablePagination.qml b/frontends/qt6/qmllib/MetaBuilder/CTablePagination.qml index cd24eb07d..b3696072c 100644 --- a/frontends/qt6/qmllib/MetaBuilder/CTablePagination.qml +++ b/frontends/qt6/qmllib/MetaBuilder/CTablePagination.qml @@ -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) } } } diff --git a/frontends/qt6/qmllib/MetaBuilder/ThemeColorTokens.qml b/frontends/qt6/qmllib/MetaBuilder/ThemeColorTokens.qml index ac2c8310f..0befac693 100644 --- a/frontends/qt6/qmllib/MetaBuilder/ThemeColorTokens.qml +++ b/frontends/qt6/qmllib/MetaBuilder/ThemeColorTokens.qml @@ -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) } } } } diff --git a/frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml b/frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml index a8c1249c3..50bb0ed69 100644 --- a/frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml +++ b/frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml @@ -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 diff --git a/qml/Material/MaterialAccordion.qml b/qml/Material/MaterialAccordion.qml new file mode 100644 index 000000000..e90d70c21 --- /dev/null +++ b/qml/Material/MaterialAccordion.qml @@ -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 +} diff --git a/qml/Material/MaterialAlert.qml b/qml/Material/MaterialAlert.qml new file mode 100644 index 000000000..5477c0351 --- /dev/null +++ b/qml/Material/MaterialAlert.qml @@ -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() + } + } +} diff --git a/qml/Material/MaterialAppBar.qml b/qml/Material/MaterialAppBar.qml new file mode 100644 index 000000000..bbac06e8e --- /dev/null +++ b/qml/Material/MaterialAppBar.qml @@ -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 + } +} diff --git a/qml/Material/MaterialAvatar.qml b/qml/Material/MaterialAvatar.qml new file mode 100644 index 000000000..649b79700 --- /dev/null +++ b/qml/Material/MaterialAvatar.qml @@ -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 + } +} diff --git a/qml/Material/MaterialBadge.qml b/qml/Material/MaterialBadge.qml new file mode 100644 index 000000000..a1b50def5 --- /dev/null +++ b/qml/Material/MaterialBadge.qml @@ -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 + } + } +} diff --git a/qml/Material/MaterialBox.qml b/qml/Material/MaterialBox.qml new file mode 100644 index 000000000..6d609c19d --- /dev/null +++ b/qml/Material/MaterialBox.qml @@ -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 + } +} diff --git a/qml/Material/MaterialButton.qml b/qml/Material/MaterialButton.qml new file mode 100644 index 000000000..9c78e90d8 --- /dev/null +++ b/qml/Material/MaterialButton.qml @@ -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 + } +} diff --git a/qml/Material/MaterialCard.qml b/qml/Material/MaterialCard.qml new file mode 100644 index 000000000..4cfc0c069 --- /dev/null +++ b/qml/Material/MaterialCard.qml @@ -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 + } +} diff --git a/qml/Material/MaterialCheckbox.qml b/qml/Material/MaterialCheckbox.qml new file mode 100644 index 000000000..5504bae19 --- /dev/null +++ b/qml/Material/MaterialCheckbox.qml @@ -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 + } +} diff --git a/qml/Material/MaterialChip.qml b/qml/Material/MaterialChip.qml new file mode 100644 index 000000000..ec0ecb400 --- /dev/null +++ b/qml/Material/MaterialChip.qml @@ -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 + } +} diff --git a/qml/Material/MaterialCircularProgress.qml b/qml/Material/MaterialCircularProgress.qml new file mode 100644 index 000000000..74400b3a3 --- /dev/null +++ b/qml/Material/MaterialCircularProgress.qml @@ -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 + } + } +} diff --git a/qml/Material/MaterialCollapse.qml b/qml/Material/MaterialCollapse.qml new file mode 100644 index 000000000..0f4876e65 --- /dev/null +++ b/qml/Material/MaterialCollapse.qml @@ -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 } } +} diff --git a/qml/Material/MaterialContainer.qml b/qml/Material/MaterialContainer.qml new file mode 100644 index 000000000..ac9370e59 --- /dev/null +++ b/qml/Material/MaterialContainer.qml @@ -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 + } +} diff --git a/qml/Material/MaterialDialog.qml b/qml/Material/MaterialDialog.qml new file mode 100644 index 000000000..2ffc14b9e --- /dev/null +++ b/qml/Material/MaterialDialog.qml @@ -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 + } + } + } +} diff --git a/qml/Material/MaterialDivider.qml b/qml/Material/MaterialDivider.qml new file mode 100644 index 000000000..05e714ef9 --- /dev/null +++ b/qml/Material/MaterialDivider.qml @@ -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 +} diff --git a/qml/Material/MaterialDividerProps.qml b/qml/Material/MaterialDividerProps.qml new file mode 100644 index 000000000..ec7219fe8 --- /dev/null +++ b/qml/Material/MaterialDividerProps.qml @@ -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 + } +} diff --git a/qml/Material/MaterialGrid.qml b/qml/Material/MaterialGrid.qml new file mode 100644 index 000000000..fb0aa55dc --- /dev/null +++ b/qml/Material/MaterialGrid.qml @@ -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 +} diff --git a/qml/Material/MaterialIconButton.qml b/qml/Material/MaterialIconButton.qml new file mode 100644 index 000000000..67b437e0f --- /dev/null +++ b/qml/Material/MaterialIconButton.qml @@ -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 + } +} diff --git a/qml/Material/MaterialLinearProgress.qml b/qml/Material/MaterialLinearProgress.qml new file mode 100644 index 000000000..b0cf43a04 --- /dev/null +++ b/qml/Material/MaterialLinearProgress.qml @@ -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 + } +} diff --git a/qml/Material/MaterialLink.qml b/qml/Material/MaterialLink.qml new file mode 100644 index 000000000..03cdbfc4b --- /dev/null +++ b/qml/Material/MaterialLink.qml @@ -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) + } + } +} diff --git a/qml/Material/MaterialMenu.qml b/qml/Material/MaterialMenu.qml new file mode 100644 index 000000000..a3a3b7c63 --- /dev/null +++ b/qml/Material/MaterialMenu.qml @@ -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 + } +} diff --git a/qml/Material/MaterialMenuItem.qml b/qml/Material/MaterialMenuItem.qml new file mode 100644 index 000000000..426a57b63 --- /dev/null +++ b/qml/Material/MaterialMenuItem.qml @@ -0,0 +1,6 @@ +import QtQuick +import QtQuick.Controls + +MenuItem { + id: menuItem +} diff --git a/qml/Material/MaterialMenuProps.qml b/qml/Material/MaterialMenuProps.qml new file mode 100644 index 000000000..32116ed2c --- /dev/null +++ b/qml/Material/MaterialMenuProps.qml @@ -0,0 +1,10 @@ +import QtQuick +import QtQuick.Controls + +Menu { + id: menuProps + background: Rectangle { + color: "transparent" + } + property alias currentAction: menuProps.activeAction +} diff --git a/qml/Material/MaterialPalette.qml b/qml/Material/MaterialPalette.qml new file mode 100644 index 000000000..d947a7125 --- /dev/null +++ b/qml/Material/MaterialPalette.qml @@ -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 +} diff --git a/qml/Material/MaterialPaper.qml b/qml/Material/MaterialPaper.qml new file mode 100644 index 000000000..0207a2c86 --- /dev/null +++ b/qml/Material/MaterialPaper.qml @@ -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 + } + } +} diff --git a/qml/Material/MaterialPopover.qml b/qml/Material/MaterialPopover.qml new file mode 100644 index 000000000..f5390005c --- /dev/null +++ b/qml/Material/MaterialPopover.qml @@ -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 + } +} diff --git a/qml/Material/MaterialPopoverProps.qml b/qml/Material/MaterialPopoverProps.qml new file mode 100644 index 000000000..58466ab30 --- /dev/null +++ b/qml/Material/MaterialPopoverProps.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtQuick.Controls + +Popup { + id: popoverProps + property bool arrowVisible: true + property bool hasShadow: true +} diff --git a/qml/Material/MaterialSkeleton.qml b/qml/Material/MaterialSkeleton.qml new file mode 100644 index 000000000..df1714e0d --- /dev/null +++ b/qml/Material/MaterialSkeleton.qml @@ -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 + } + } + } +} diff --git a/qml/Material/MaterialSnackbar.qml b/qml/Material/MaterialSnackbar.qml new file mode 100644 index 000000000..a8e16f5fd --- /dev/null +++ b/qml/Material/MaterialSnackbar.qml @@ -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() + } + } +} diff --git a/qml/Material/MaterialSurface.qml b/qml/Material/MaterialSurface.qml new file mode 100644 index 000000000..d43b0dd48 --- /dev/null +++ b/qml/Material/MaterialSurface.qml @@ -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 + } +} diff --git a/qml/Material/MaterialSwitch.qml b/qml/Material/MaterialSwitch.qml new file mode 100644 index 000000000..6c110b2a2 --- /dev/null +++ b/qml/Material/MaterialSwitch.qml @@ -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 + } +} diff --git a/qml/Material/MaterialTextField.qml b/qml/Material/MaterialTextField.qml new file mode 100644 index 000000000..dea55d950 --- /dev/null +++ b/qml/Material/MaterialTextField.qml @@ -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 +} diff --git a/qml/Material/MaterialToolbar.qml b/qml/Material/MaterialToolbar.qml new file mode 100644 index 000000000..bc4aee697 --- /dev/null +++ b/qml/Material/MaterialToolbar.qml @@ -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 +} diff --git a/qml/Material/MaterialTypography.qml b/qml/Material/MaterialTypography.qml new file mode 100644 index 000000000..11a1a3f22 --- /dev/null +++ b/qml/Material/MaterialTypography.qml @@ -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" +} diff --git a/qml/Material/qmldir b/qml/Material/qmldir new file mode 100644 index 000000000..55f059835 --- /dev/null +++ b/qml/Material/qmldir @@ -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 diff --git a/qml/MetaBuilder/CActivityList.qml b/qml/MetaBuilder/CActivityList.qml new file mode 100644 index 000000000..37e338c68 --- /dev/null +++ b/qml/MetaBuilder/CActivityList.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CAdapterPatternSelector.qml b/qml/MetaBuilder/CAdapterPatternSelector.qml new file mode 100644 index 000000000..498fe3a65 --- /dev/null +++ b/qml/MetaBuilder/CAdapterPatternSelector.qml @@ -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] + } + } +} diff --git a/qml/MetaBuilder/CAddRouteDialog.qml b/qml/MetaBuilder/CAddRouteDialog.qml new file mode 100644 index 000000000..fd2159d16 --- /dev/null +++ b/qml/MetaBuilder/CAddRouteDialog.qml @@ -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 + } + } + } + } +} diff --git a/qml/MetaBuilder/CAdminStatsBar.qml b/qml/MetaBuilder/CAdminStatsBar.qml new file mode 100644 index 000000000..2e7e1e96f --- /dev/null +++ b/qml/MetaBuilder/CAdminStatsBar.qml @@ -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) } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/CBackendDetailPanel.qml b/qml/MetaBuilder/CBackendDetailPanel.qml new file mode 100644 index 000000000..70c72a839 --- /dev/null +++ b/qml/MetaBuilder/CBackendDetailPanel.qml @@ -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 } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/CBackendListSidebar.qml b/qml/MetaBuilder/CBackendListSidebar.qml new file mode 100644 index 000000000..d5c4c9a48 --- /dev/null +++ b/qml/MetaBuilder/CBackendListSidebar.qml @@ -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 + } + } + } + } +} diff --git a/qml/MetaBuilder/CCanvasGrid.qml b/qml/MetaBuilder/CCanvasGrid.qml new file mode 100644 index 000000000..ea6f6cc24 --- /dev/null +++ b/qml/MetaBuilder/CCanvasGrid.qml @@ -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() +} diff --git a/qml/MetaBuilder/CCanvasZoomOverlay.qml b/qml/MetaBuilder/CCanvasZoomOverlay.qml new file mode 100644 index 000000000..8bf951bee --- /dev/null +++ b/qml/MetaBuilder/CCanvasZoomOverlay.qml @@ -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() + } + } +} diff --git a/qml/MetaBuilder/CCommentCard.qml b/qml/MetaBuilder/CCommentCard.qml new file mode 100644 index 000000000..9d095c4cf --- /dev/null +++ b/qml/MetaBuilder/CCommentCard.qml @@ -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() + } + } +} diff --git a/qml/MetaBuilder/CCommentInput.qml b/qml/MetaBuilder/CCommentInput.qml new file mode 100644 index 000000000..51e2f3d84 --- /dev/null +++ b/qml/MetaBuilder/CCommentInput.qml @@ -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 = "" + } + } + } +} diff --git a/qml/MetaBuilder/CComponentPropertiesPanel.qml b/qml/MetaBuilder/CComponentPropertiesPanel.qml new file mode 100644 index 000000000..85be157e3 --- /dev/null +++ b/qml/MetaBuilder/CComponentPropertiesPanel.qml @@ -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) + } + } + } + } +} diff --git a/qml/MetaBuilder/CComponentTreeRow.qml b/qml/MetaBuilder/CComponentTreeRow.qml new file mode 100644 index 000000000..a0a7ca622 --- /dev/null +++ b/qml/MetaBuilder/CComponentTreeRow.qml @@ -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() + } + } +} diff --git a/qml/MetaBuilder/CComponentTypeLegend.qml b/qml/MetaBuilder/CComponentTypeLegend.qml new file mode 100644 index 000000000..e233cf1d5 --- /dev/null +++ b/qml/MetaBuilder/CComponentTypeLegend.qml @@ -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 } + } + } +} diff --git a/qml/MetaBuilder/CConfigStatCard.qml b/qml/MetaBuilder/CConfigStatCard.qml new file mode 100644 index 000000000..532942534 --- /dev/null +++ b/qml/MetaBuilder/CConfigStatCard.qml @@ -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 + } + } + } +} diff --git a/qml/MetaBuilder/CConnectionLayer.qml b/qml/MetaBuilder/CConnectionLayer.qml new file mode 100644 index 000000000..72e8d6c09 --- /dev/null +++ b/qml/MetaBuilder/CConnectionLayer.qml @@ -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([]) + } + } + } +} diff --git a/qml/MetaBuilder/CConnectionTest.qml b/qml/MetaBuilder/CConnectionTest.qml new file mode 100644 index 000000000..f9e889b6e --- /dev/null +++ b/qml/MetaBuilder/CConnectionTest.qml @@ -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) + } + } + } +} diff --git a/qml/MetaBuilder/CDataTable.qml b/qml/MetaBuilder/CDataTable.qml new file mode 100644 index 000000000..6b477a6a8 --- /dev/null +++ b/qml/MetaBuilder/CDataTable.qml @@ -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) } + } + } +} diff --git a/qml/MetaBuilder/CDatabaseStatsRow.qml b/qml/MetaBuilder/CDatabaseStatsRow.qml new file mode 100644 index 000000000..8e8f1aeb4 --- /dev/null +++ b/qml/MetaBuilder/CDatabaseStatsRow.qml @@ -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 } + } + } +} diff --git a/qml/MetaBuilder/CDeleteConfirmDialog.qml b/qml/MetaBuilder/CDeleteConfirmDialog.qml new file mode 100644 index 000000000..7ef667fe5 --- /dev/null +++ b/qml/MetaBuilder/CDeleteConfirmDialog.qml @@ -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 + } + } + } + } +} diff --git a/qml/MetaBuilder/CDropdownMenu.qml b/qml/MetaBuilder/CDropdownMenu.qml new file mode 100644 index 000000000..1fed8bb04 --- /dev/null +++ b/qml/MetaBuilder/CDropdownMenu.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CEntityForm.qml b/qml/MetaBuilder/CEntityForm.qml new file mode 100644 index 000000000..339f6b02c --- /dev/null +++ b/qml/MetaBuilder/CEntityForm.qml @@ -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); + } + } + } + } +} diff --git a/qml/MetaBuilder/CEntitySidebar.qml b/qml/MetaBuilder/CEntitySidebar.qml new file mode 100644 index 000000000..50320ed96 --- /dev/null +++ b/qml/MetaBuilder/CEntitySidebar.qml @@ -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) + } + } + } +} diff --git a/qml/MetaBuilder/CGodPanelHeader.qml b/qml/MetaBuilder/CGodPanelHeader.qml new file mode 100644 index 000000000..0b4051576 --- /dev/null +++ b/qml/MetaBuilder/CGodPanelHeader.qml @@ -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" } + } + } +} diff --git a/qml/MetaBuilder/CGodUserCard.qml b/qml/MetaBuilder/CGodUserCard.qml new file mode 100644 index 000000000..e604fcae0 --- /dev/null +++ b/qml/MetaBuilder/CGodUserCard.qml @@ -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() + } + } +} diff --git a/qml/MetaBuilder/CHeroSection.qml b/qml/MetaBuilder/CHeroSection.qml new file mode 100644 index 000000000..c7ecfc894 --- /dev/null +++ b/qml/MetaBuilder/CHeroSection.qml @@ -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() + } + } + } +} diff --git a/qml/MetaBuilder/CLanguageSelector.qml b/qml/MetaBuilder/CLanguageSelector.qml new file mode 100644 index 000000000..b8d1e81c6 --- /dev/null +++ b/qml/MetaBuilder/CLanguageSelector.qml @@ -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() + } + } +} diff --git a/qml/MetaBuilder/CLevelCard.qml b/qml/MetaBuilder/CLevelCard.qml new file mode 100644 index 000000000..acf2cc917 --- /dev/null +++ b/qml/MetaBuilder/CLevelCard.qml @@ -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 + } + } + } + } + } +} diff --git a/qml/MetaBuilder/CLevelReferenceCard.qml b/qml/MetaBuilder/CLevelReferenceCard.qml new file mode 100644 index 000000000..2a48de0c8 --- /dev/null +++ b/qml/MetaBuilder/CLevelReferenceCard.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CLoginForm.qml b/qml/MetaBuilder/CLoginForm.qml new file mode 100644 index 000000000..bfbf0288e --- /dev/null +++ b/qml/MetaBuilder/CLoginForm.qml @@ -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) + } + } +} diff --git a/qml/MetaBuilder/CModActionCard.qml b/qml/MetaBuilder/CModActionCard.qml new file mode 100644 index 000000000..669b0a95d --- /dev/null +++ b/qml/MetaBuilder/CModActionCard.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CModStatsRow.qml b/qml/MetaBuilder/CModStatsRow.qml new file mode 100644 index 000000000..1c13dfe38 --- /dev/null +++ b/qml/MetaBuilder/CModStatsRow.qml @@ -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 + } + } + } + } +} diff --git a/qml/MetaBuilder/CNavBar.qml b/qml/MetaBuilder/CNavBar.qml new file mode 100644 index 000000000..541d9b7cc --- /dev/null +++ b/qml/MetaBuilder/CNavBar.qml @@ -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) + } + } + } +} diff --git a/qml/MetaBuilder/CNodePalette.qml b/qml/MetaBuilder/CNodePalette.qml new file mode 100644 index 000000000..714b8a449 --- /dev/null +++ b/qml/MetaBuilder/CNodePalette.qml @@ -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 + } + } + } +} diff --git a/qml/MetaBuilder/CNodePropertiesPanel.qml b/qml/MetaBuilder/CNodePropertiesPanel.qml new file mode 100644 index 000000000..b50593bd2 --- /dev/null +++ b/qml/MetaBuilder/CNodePropertiesPanel.qml @@ -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 } + } +} diff --git a/qml/MetaBuilder/CNotificationBell.qml b/qml/MetaBuilder/CNotificationBell.qml new file mode 100644 index 000000000..a304698e1 --- /dev/null +++ b/qml/MetaBuilder/CNotificationBell.qml @@ -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() + } + } +} diff --git a/qml/MetaBuilder/CNotificationEmptyState.qml b/qml/MetaBuilder/CNotificationEmptyState.qml new file mode 100644 index 000000000..3e7ca8bf6 --- /dev/null +++ b/qml/MetaBuilder/CNotificationEmptyState.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CNotificationItem.qml b/qml/MetaBuilder/CNotificationItem.qml new file mode 100644 index 000000000..d9c138b5d --- /dev/null +++ b/qml/MetaBuilder/CNotificationItem.qml @@ -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 + } +} diff --git a/qml/MetaBuilder/CNotificationToggles.qml b/qml/MetaBuilder/CNotificationToggles.qml new file mode 100644 index 000000000..b3ab7d8a4 --- /dev/null +++ b/qml/MetaBuilder/CNotificationToggles.qml @@ -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) + } + } + } + } + } +} diff --git a/qml/MetaBuilder/CProfileForm.qml b/qml/MetaBuilder/CProfileForm.qml new file mode 100644 index 000000000..2921c18cf --- /dev/null +++ b/qml/MetaBuilder/CProfileForm.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CProfileHeader.qml b/qml/MetaBuilder/CProfileHeader.qml new file mode 100644 index 000000000..c5d6bf142 --- /dev/null +++ b/qml/MetaBuilder/CProfileHeader.qml @@ -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 + } + } + } +} diff --git a/qml/MetaBuilder/CQuickActions.qml b/qml/MetaBuilder/CQuickActions.qml new file mode 100644 index 000000000..b9783c752 --- /dev/null +++ b/qml/MetaBuilder/CQuickActions.qml @@ -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) + } + } + } +} diff --git a/qml/MetaBuilder/CQuickLoginCard.qml b/qml/MetaBuilder/CQuickLoginCard.qml new file mode 100644 index 000000000..02c61253b --- /dev/null +++ b/qml/MetaBuilder/CQuickLoginCard.qml @@ -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 } } + } + } +} diff --git a/qml/MetaBuilder/CReportCard.qml b/qml/MetaBuilder/CReportCard.qml new file mode 100644 index 000000000..f88ef2a9d --- /dev/null +++ b/qml/MetaBuilder/CReportCard.qml @@ -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() + } + } +} diff --git a/qml/MetaBuilder/CRouteEditPanel.qml b/qml/MetaBuilder/CRouteEditPanel.qml new file mode 100644 index 000000000..596d0b71d --- /dev/null +++ b/qml/MetaBuilder/CRouteEditPanel.qml @@ -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() + } + } +} diff --git a/qml/MetaBuilder/CRouteTableHeader.qml b/qml/MetaBuilder/CRouteTableHeader.qml new file mode 100644 index 000000000..fd919f729 --- /dev/null +++ b/qml/MetaBuilder/CRouteTableHeader.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CRouteTableRow.qml b/qml/MetaBuilder/CRouteTableRow.qml new file mode 100644 index 000000000..9448351db --- /dev/null +++ b/qml/MetaBuilder/CRouteTableRow.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CServiceStatus.qml b/qml/MetaBuilder/CServiceStatus.qml new file mode 100644 index 000000000..f93893a20 --- /dev/null +++ b/qml/MetaBuilder/CServiceStatus.qml @@ -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 + } + } +} diff --git a/qml/MetaBuilder/CSettingsSection.qml b/qml/MetaBuilder/CSettingsSection.qml new file mode 100644 index 000000000..bf51c747b --- /dev/null +++ b/qml/MetaBuilder/CSettingsSection.qml @@ -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 + } +} diff --git a/qml/MetaBuilder/CSidebar.qml b/qml/MetaBuilder/CSidebar.qml new file mode 100644 index 000000000..94f71e32d --- /dev/null +++ b/qml/MetaBuilder/CSidebar.qml @@ -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") + } + } +} diff --git a/qml/MetaBuilder/CSmtpSenderForm.qml b/qml/MetaBuilder/CSmtpSenderForm.qml new file mode 100644 index 000000000..925d3f389 --- /dev/null +++ b/qml/MetaBuilder/CSmtpSenderForm.qml @@ -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 + ">" + } + } + } +} diff --git a/qml/MetaBuilder/CSmtpServerForm.qml b/qml/MetaBuilder/CSmtpServerForm.qml new file mode 100644 index 000000000..70cce6f8e --- /dev/null +++ b/qml/MetaBuilder/CSmtpServerForm.qml @@ -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" + } + } + } +} diff --git a/qml/MetaBuilder/CSmtpTemplateEditor.qml b/qml/MetaBuilder/CSmtpTemplateEditor.qml new file mode 100644 index 000000000..2d8a7f8b5 --- /dev/null +++ b/qml/MetaBuilder/CSmtpTemplateEditor.qml @@ -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) + } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/CSmtpTemplateList.qml b/qml/MetaBuilder/CSmtpTemplateList.qml new file mode 100644 index 000000000..9039e7182 --- /dev/null +++ b/qml/MetaBuilder/CSmtpTemplateList.qml @@ -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) + } + } + } +} diff --git a/qml/MetaBuilder/CSmtpTestEmailForm.qml b/qml/MetaBuilder/CSmtpTestEmailForm.qml new file mode 100644 index 000000000..d268ffc78 --- /dev/null +++ b/qml/MetaBuilder/CSmtpTestEmailForm.qml @@ -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" + } + } + } +} diff --git a/qml/MetaBuilder/CStatCard.qml b/qml/MetaBuilder/CStatCard.qml new file mode 100644 index 000000000..bdb353a7b --- /dev/null +++ b/qml/MetaBuilder/CStatCard.qml @@ -0,0 +1,39 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property string label: "" + property string value: "" + property string status: "info" + property bool isDark: false + + readonly property color onSurfaceVariant: Theme.textSecondary + + variant: "outlined" + + CText { + Layout.fillWidth: true + variant: "caption" + text: root.label + color: root.onSurfaceVariant + } + + Item { Layout.preferredHeight: 4 } + + CText { + Layout.fillWidth: true + variant: "h3" + text: root.value + } + + Item { Layout.preferredHeight: 8 } + + CStatusBadge { + status: root.status + text: root.status === "success" ? "Online" : "Active" + } +} diff --git a/qml/MetaBuilder/CStatsStrip.qml b/qml/MetaBuilder/CStatsStrip.qml new file mode 100644 index 000000000..11a4026a3 --- /dev/null +++ b/qml/MetaBuilder/CStatsStrip.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: root + + property var stats: ({ users: "0", packages: "0", workflows: "0", backends: "0" }) + property color accentColor: "#6366F1" + property bool isDark: false + + 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: 88 + color: isDark ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0.31, 0.31, 0.44, 0.06) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 60 + anchors.rightMargin: 60 + spacing: 0 + + Repeater { + model: [ + { label: "USERS", value: stats.users }, + { label: "PACKAGES", value: stats.packages }, + { label: "WORKFLOWS", value: stats.workflows }, + { label: "BACKENDS", value: stats.backends } + ] + delegate: Item { + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + anchors.centerIn: parent + spacing: 10 + + CText { + text: modelData.value + font.pixelSize: 24 + font.weight: Font.Bold + font.family: "monospace" + color: root.accentColor + } + CText { + text: modelData.label + font.pixelSize: 10 + font.family: "monospace" + font.letterSpacing: 2 + color: onSurfaceVariant + } + } + + Rectangle { + visible: index < 3 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 1 + height: 32 + color: outlineVariant + } + } + } + } +} diff --git a/qml/MetaBuilder/CSystemMetricCard.qml b/qml/MetaBuilder/CSystemMetricCard.qml new file mode 100644 index 000000000..5210604eb --- /dev/null +++ b/qml/MetaBuilder/CSystemMetricCard.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + required property string label + required property real value + property string unit: "%" + property bool isDark: false + + Layout.fillWidth: true + + CText { + variant: "caption" + text: root.label + color: Theme.textSecondary + Layout.fillWidth: true + } + + CText { + variant: "h3" + text: Math.round(root.value) + root.unit + Layout.fillWidth: true + } + + Rectangle { + Layout.fillWidth: true + height: 6 + radius: 3 + color: Theme.surfaceVariant + + Rectangle { + width: parent.width * (root.value / 100) + height: parent.height + radius: 3 + color: root.value > 80 ? Theme.error : root.value > 60 ? Theme.warning : Theme.primary + } + } +} diff --git a/qml/MetaBuilder/CTableHeader.qml b/qml/MetaBuilder/CTableHeader.qml new file mode 100644 index 000000000..0ed8779ef --- /dev/null +++ b/qml/MetaBuilder/CTableHeader.qml @@ -0,0 +1,60 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +/** + * CTableHeader.qml - Column header row with select-all checkbox + * + * Usage: + * CTableHeader { + * headers: ["ID", "Username", "Email"] + * selectAll: false + * onSelectAllToggled: function(checked) { ... } + * } + */ +Rectangle { + id: root + + property var headers: [] + property bool selectAll: false + + signal selectAllToggled(bool checked) + + Layout.fillWidth: true + height: 44 + color: Theme.surfaceVariant + radius: 0 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 0 + + CheckBox { + Layout.preferredWidth: 36 + checked: root.selectAll + onCheckedChanged: { + root.selectAllToggled(checked); + } + } + + Repeater { + model: root.headers + delegate: CText { + Layout.fillWidth: index > 0 + Layout.preferredWidth: index === 0 ? 80 : -1 + variant: "subtitle2" + text: modelData + } + } + + CText { + Layout.preferredWidth: 110 + variant: "subtitle2" + text: "Actions" + horizontalAlignment: Text.AlignRight + } + } +} diff --git a/qml/MetaBuilder/CTablePagination.qml b/qml/MetaBuilder/CTablePagination.qml new file mode 100644 index 000000000..b3696072c --- /dev/null +++ b/qml/MetaBuilder/CTablePagination.qml @@ -0,0 +1,75 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +/** + * CTablePagination.qml - Pagination controls with page info and prev/next buttons + * + * Usage: + * CTablePagination { + * page: 0 + * pageSize: 5 + * totalFiltered: 24 + * onPageChanged: function(newPage) { ... } + * } + */ +Rectangle { + id: root + + property int page: 0 + property int pageSize: 5 + property int totalFiltered: 0 + + signal pageRequested(int newPage) + + readonly property int _totalPages: Math.max(1, Math.ceil(totalFiltered / pageSize)) + + Layout.fillWidth: true + height: 48 + color: Theme.surfaceVariant + radius: 0 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + spacing: 8 + + CText { + variant: "caption" + text: { + var total = root.totalFiltered; + if (total === 0) return "0 records"; + var start = root.page * root.pageSize + 1; + var end = Math.min(start + root.pageSize - 1, total); + return start + "-" + end + " of " + total + " records"; + } + color: Theme.textSecondary + } + + Item { Layout.fillWidth: true } + + CButton { + text: "Previous" + variant: "ghost" + size: "sm" + enabled: root.page > 0 + onClicked: root.pageRequested(root.page - 1) + } + + CText { + variant: "caption" + text: "Page " + (root.page + 1) + " of " + root._totalPages + color: Theme.textSecondary + } + + CButton { + text: "Next" + variant: "ghost" + size: "sm" + enabled: root.page < root._totalPages - 1 + onClicked: root.pageRequested(root.page + 1) + } + } +} diff --git a/qml/MetaBuilder/CTechCard.qml b/qml/MetaBuilder/CTechCard.qml new file mode 100644 index 000000000..32cb7bb08 --- /dev/null +++ b/qml/MetaBuilder/CTechCard.qml @@ -0,0 +1,51 @@ +import QtQuick +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: root + + property string name: "" + property string desc: "" + property color accent: "#6366F1" + 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 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: 72 + radius: 12 + color: surfaceContainerHigh + border.color: outlineVariant + border.width: 1 + + RowLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 12 + + Rectangle { + width: 4; height: 36; radius: 2 + color: root.accent + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + + CText { + text: root.name + font.pixelSize: 14 + font.weight: Font.DemiBold + color: onSurface + } + CText { + text: root.desc + font.pixelSize: 12 + color: onSurfaceVariant + } + } + } +} diff --git a/qml/MetaBuilder/CTenantCard.qml b/qml/MetaBuilder/CTenantCard.qml new file mode 100644 index 000000000..82262848a --- /dev/null +++ b/qml/MetaBuilder/CTenantCard.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + required property var tenant + property bool isDark: false + + signal edit() + signal remove() + + Layout.fillWidth: true + + FlexRow { + Layout.fillWidth: true + spacing: 10 + + CText { variant: "h4"; text: root.tenant.name } + CStatusBadge { + status: root.tenant.status === "active" ? "success" : "warning" + text: root.tenant.status + } + Item { Layout.fillWidth: true } + CBadge { text: "Tenant"; badgeColor: Theme.primary } + } + + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 24 + + ColumnLayout { + spacing: 4 + CText { variant: "caption"; text: "Owner"; color: Theme.textSecondary } + CText { variant: "body1"; text: root.tenant.owner } + } + ColumnLayout { + spacing: 4 + CText { variant: "caption"; text: "Homepage"; color: Theme.textSecondary } + CText { variant: "body1"; text: root.tenant.homepage } + } + ColumnLayout { + spacing: 4 + CText { variant: "caption"; text: "Created"; color: Theme.textSecondary } + CText { variant: "body1"; text: root.tenant.created } + } + Item { Layout.fillWidth: true } + CButton { + text: "Configure" + variant: "ghost" + size: "sm" + onClicked: root.edit() + } + } +} diff --git a/qml/MetaBuilder/CThemePicker.qml b/qml/MetaBuilder/CThemePicker.qml new file mode 100644 index 000000000..f3ea7eee0 --- /dev/null +++ b/qml/MetaBuilder/CThemePicker.qml @@ -0,0 +1,49 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +/** + * CThemePicker - Theme selection grid showing all available themes. + * + * Reads from Theme.themeKeys and Theme.themes singleton. + * Emits themeSelected(name) when user picks a theme. + */ +ColumnLayout { + id: root + spacing: 8 + + property string currentTheme: Theme.current + property bool isDark: Theme.mode === "dark" + + signal themeSelected(string name) + + CText { + variant: "subtitle2" + text: "Theme" + Layout.topMargin: 4 + } + + Flow { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: Theme.themeKeys + + delegate: CButton { + property string themeId: modelData + property var themeObj: Theme.themes[themeId] || {} + + text: themeObj.name || themeId + variant: root.currentTheme === themeId ? "primary" : "default" + size: "sm" + + onClicked: { + root.currentTheme = themeId + root.themeSelected(themeId) + } + } + } + } +} diff --git a/qml/MetaBuilder/CTransferCard.qml b/qml/MetaBuilder/CTransferCard.qml new file mode 100644 index 000000000..5eb41fa78 --- /dev/null +++ b/qml/MetaBuilder/CTransferCard.qml @@ -0,0 +1,52 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + required property var transfer + property bool isDark: false + + signal approve() + signal deny() + + Layout.fillWidth: true + + FlexRow { + Layout.fillWidth: true + spacing: 10 + + CText { variant: "subtitle1"; text: root.transfer.from + " -> " + root.transfer.to } + CStatusBadge { status: "warning"; text: "Pending" } + Item { Layout.fillWidth: true } + CText { variant: "caption"; text: "Expires: " + root.transfer.expiry; color: Theme.textSecondary } + } + + CText { + variant: "body2" + text: root.transfer.reason + wrapMode: Text.Wrap + Layout.fillWidth: true + } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + Item { Layout.fillWidth: true } + CButton { + text: "Approve" + variant: "primary" + size: "sm" + onClicked: root.approve() + } + CButton { + text: "Deny" + variant: "danger" + size: "sm" + onClicked: root.deny() + } + } +} diff --git a/qml/MetaBuilder/CUserMenu.qml b/qml/MetaBuilder/CUserMenu.qml new file mode 100644 index 000000000..476806b26 --- /dev/null +++ b/qml/MetaBuilder/CUserMenu.qml @@ -0,0 +1,143 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +// User avatar circle with dropdown menu (Profile, Settings, Sign out) +Item { + id: root + + property string username: "" + property int level: 1 + property string role: "public" + property bool isDark: Theme.mode === "dark" + + signal navigateTo(string view) + signal signOut() + + width: 32 + height: 32 + + // ── Avatar circle ── + Rectangle { + id: avatarCircle + anchors.fill: parent + radius: 16 + color: avatarMA.containsMouse + ? Qt.rgba(0.39, 0.4, 0.95, isDark ? 0.25 : 0.2) + : Qt.rgba(0.39, 0.4, 0.95, isDark ? 0.15 : 0.12) + + Behavior on color { ColorAnimation { duration: 150 } } + + CText { + anchors.centerIn: parent + text: username ? username.charAt(0).toUpperCase() : "?" + font.pixelSize: 14 + font.weight: Font.Bold + color: "#6366F1" + } + + MouseArea { + id: avatarMA + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: dropdownMenu.visible = !dropdownMenu.visible + } + + // ── Dropdown menu ── + CDropdownMenu { + id: dropdownMenu + visible: false + anchors.top: parent.bottom + anchors.right: parent.right + anchors.topMargin: 8 + isDark: root.isDark + + menuItems: [ + { label: "Profile", icon: "P", action: "profile" }, + { label: "Settings", icon: "S", action: "settings" } + ] + + onItemClicked: function(action) { + root.navigateTo(action); + dropdownMenu.close(); + } + + headerContent: Component { + RowLayout { + Layout.fillWidth: true + Layout.margins: 8 + spacing: 10 + + Rectangle { + width: 36 + height: 36 + radius: 18 + color: Qt.rgba(0.39, 0.4, 0.95, root.isDark ? 0.2 : 0.15) + + CText { + anchors.centerIn: parent + text: root.username ? root.username.charAt(0).toUpperCase() : "?" + font.pixelSize: 16 + font.weight: Font.Bold + color: "#6366F1" + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 1 + CText { + text: root.username + font.pixelSize: 14 + font.weight: Font.DemiBold + } + CText { + text: "L" + root.level + " \u00B7 " + root.role + font.pixelSize: 11 + font.family: "monospace" + color: Theme.textSecondary + } + } + } + } + + footerContent: Component { + Rectangle { + Layout.fillWidth: true + height: 36 + radius: 8 + color: logoutMA.containsMouse ? Qt.rgba(0.96, 0.25, 0.37, 0.08) : "transparent" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + spacing: 10 + CText { + text: "\u2192" + font.pixelSize: 14 + color: "#F43F5E" + } + CText { + text: "Sign out" + font.pixelSize: 13 + color: "#F43F5E" + } + } + + MouseArea { + id: logoutMA + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + dropdownMenu.close() + root.signOut() + } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/CWelcomeCard.qml b/qml/MetaBuilder/CWelcomeCard.qml new file mode 100644 index 000000000..a92391cf4 --- /dev/null +++ b/qml/MetaBuilder/CWelcomeCard.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: root + + property string username: "" + property int level: 1 + property string role: "" + property bool isDark: false + property bool loading: false + + signal refresh() + + 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 onSurfaceVariant: Theme.textSecondary + + implicitHeight: welcomeRow.implicitHeight + 40 + radius: 16 + color: surfaceContainerHigh + border.color: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08) + border.width: 1 + + RowLayout { + id: welcomeRow + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + // User avatar + Rectangle { + width: 56 + height: 56 + radius: 28 + color: Qt.rgba(0.39, 0.4, 0.95, isDark ? 0.2 : 0.15) + + CText { + anchors.centerIn: parent + text: root.username ? root.username.charAt(0).toUpperCase() : "?" + font.pixelSize: 24 + font.weight: Font.Bold + color: "#6366F1" + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + CText { + text: "Welcome back, " + root.username + font.pixelSize: 22 + font.weight: Font.Bold + color: root.onSurface + } + CText { + text: "Level " + root.level + " \u00b7 " + root.role + font.pixelSize: 13 + color: root.onSurfaceVariant + } + } + + CButton { + text: root.loading ? "Refreshing..." : "Refresh" + variant: "ghost" + size: "sm" + enabled: !root.loading + onClicked: root.refresh() + } + } +} diff --git a/qml/MetaBuilder/CWorkflowCanvas.qml b/qml/MetaBuilder/CWorkflowCanvas.qml new file mode 100644 index 000000000..a07912f80 --- /dev/null +++ b/qml/MetaBuilder/CWorkflowCanvas.qml @@ -0,0 +1,182 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: root + + property var nodes: [] + property var connections: ({}) + property real zoom: 1.0 + property string selectedNodeId: "" + property bool drawingConnection: false + property string connSourceNode: "" + property string connSourcePort: "" + property bool connSourceIsOutput: true + property real connDragX: 0 + property real connDragY: 0 + + signal nodeSelected(string id) + signal nodeMoved(string id, real x, real y) + signal connectionCreated(string srcId, string srcPort, string dstId, string dstPort) + signal nodeDropped(string type, real x, real y) + signal zoomChanged(real zoom) + signal canvasClicked() + signal connectionDragStarted(string nodeId, string portName, bool isOutput, real portX, real portY) + signal connectionDragUpdated(real x, real y) + signal connectionDragFinished() + signal connectionCompleted(string nodeId, string portName) + + color: Theme.background + clip: true + + function requestPaint() { + connectionLayer.requestPaint() + } + + 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 + } + } + + // Drop area for palette drag + DropArea { + anchors.fill: parent + keys: ["text/node-type"] + onDropped: function(drop) { + var nodeType = drop.getDataAsString("text/node-type") + if (nodeType) { + var localPos = mapToItem(canvasContent, drop.x, drop.y) + root.nodeDropped(nodeType, localPos.x, localPos.y) + } + } + } + + Flickable { + id: canvas + anchors.fill: parent + contentWidth: 5000 + contentHeight: 5000 + clip: true + boundsBehavior: Flickable.StopAtBounds + + Component.onCompleted: { + contentX = 1500 + contentY = 1500 + } + + function centerX() { return contentX + width / 2 } + function centerY() { return contentY + height / 2 } + + Item { + id: canvasContent + width: canvas.contentWidth + height: canvas.contentHeight + + transform: Scale { + origin.x: 0 + origin.y: 0 + xScale: root.zoom + yScale: root.zoom + } + + CCanvasGrid { + anchors.fill: parent + } + + CConnectionLayer { + id: connectionLayer + anchors.fill: parent + z: 1 + nodes: root.nodes + connections: root.connections + drawingConnection: root.drawingConnection + connSourceNode: root.connSourceNode + connSourceIsOutput: root.connSourceIsOutput + connDragX: root.connDragX + connDragY: root.connDragY + } + + Repeater { + id: nodeRepeater + model: root.nodes.length + z: 2 + + delegate: CWorkflowNodeDelegate { + nodeData: root.nodes[index] + isSelected: root.selectedNodeId === nodeData.id + groupColorValue: groupColor(nodeData.type) + drawingConnection: root.drawingConnection + connSourceIsOutput: root.connSourceIsOutput + canvasContentItem: canvasContent + + onNodeSelected: function(id) { root.nodeSelected(id) } + onNodeMoved: function(id, x, y) { root.nodeMoved(id, x, y) } + onConnectionDragStarted: function(nodeId, portName, isOutput, portX, portY) { + root.connectionDragStarted(nodeId, portName, isOutput, portX, portY) + } + onConnectionCompleted: function(nodeId, portName) { + root.connectionCompleted(nodeId, portName) + } + onPaintRequested: connectionLayer.requestPaint() + } + } + + MouseArea { + anchors.fill: parent + z: 0 + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + hoverEnabled: true + + onPositionChanged: function(mouse) { + if (root.drawingConnection) { + root.connectionDragUpdated(mouse.x, mouse.y) + } + } + + onReleased: function(mouse) { + if (root.drawingConnection) { + root.connectionDragFinished() + } + } + + onClicked: function(mouse) { + root.canvasClicked() + } + + onWheel: function(wheel) { + var zoomDelta = wheel.angleDelta.y > 0 ? 0.1 : -0.1 + root.zoomChanged(root.zoom + zoomDelta) + } + } + } + } + + CCanvasZoomOverlay { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 12 + zoom: root.zoom + onZoomIn: root.zoomChanged(root.zoom + 0.1) + onZoomOut: root.zoomChanged(root.zoom - 0.1) + } + + CText { + anchors.centerIn: parent + visible: root.nodes.length === 0 + text: "Empty canvas — drag nodes from the palette or double-click a node type" + variant: "body1" + opacity: 0.5 + } +} diff --git a/qml/MetaBuilder/CWorkflowNodeDelegate.qml b/qml/MetaBuilder/CWorkflowNodeDelegate.qml new file mode 100644 index 000000000..76993141a --- /dev/null +++ b/qml/MetaBuilder/CWorkflowNodeDelegate.qml @@ -0,0 +1,184 @@ +import QtQuick +import QtQuick.Controls +import QmlComponents 1.0 + +Rectangle { + id: nodeRect + + property var nodeData: ({}) + property bool isSelected: false + property string groupColorValue: Theme.primary + property bool drawingConnection: false + property bool connSourceIsOutput: true + + signal nodeSelected(string id) + signal nodeMoved(string id, real x, real y) + signal connectionDragStarted(string nodeId, string portName, bool isOutput, real portX, real portY) + signal connectionCompleted(string nodeId, string portName) + signal paintRequested() + + // Provide access to canvasContent for port coordinate mapping + property Item canvasContentItem: null + + readonly property int portRadius: 6 + readonly property int headerHeight: 32 + readonly property int portSpacing: 24 + readonly property int nodeWidth: 180 + readonly property int inputCount: nodeData.inputs ? nodeData.inputs.length : 0 + readonly property int outputCount: nodeData.outputs ? nodeData.outputs.length : 0 + readonly property int bodyPorts: Math.max(inputCount, outputCount) + + x: nodeData.position ? nodeData.position[0] : 0 + y: nodeData.position ? nodeData.position[1] : 0 + width: nodeWidth + height: headerHeight + Math.max(1, bodyPorts) * portSpacing + 16 + radius: 8 + color: isSelected ? Qt.lighter(Theme.paper, 1.1) : Theme.paper + border.color: isSelected ? groupColorValue : Theme.border + border.width: isSelected ? 2 : 1 + z: isSelected ? 10 : 2 + + // Header + Rectangle { + id: nodeHeader + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: nodeRect.headerHeight + radius: 8 + color: nodeRect.groupColorValue + + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: parent.radius + color: parent.color + } + + CText { + anchors.centerIn: parent + text: nodeRect.nodeData.name || nodeRect.nodeData.type || "" + color: "#FFFFFF" + variant: "body2" + font.bold: true + elide: Text.ElideRight + width: parent.width - 16 + horizontalAlignment: Text.AlignHCenter + } + } + + // Type label below header + CText { + anchors.top: nodeHeader.bottom + anchors.topMargin: 2 + anchors.horizontalCenter: parent.horizontalCenter + text: nodeRect.nodeData.type || "" + variant: "caption" + font.pixelSize: 9 + color: Theme.textSecondary || Theme.text + opacity: 0.6 + } + + // Input ports + Column { + anchors.left: parent.left + anchors.leftMargin: -portRadius + anchors.top: nodeHeader.bottom + anchors.topMargin: 8 + spacing: portSpacing - portRadius * 2 + + Repeater { + model: nodeRect.nodeData.inputs || [] + Item { + width: nodeRect.portRadius * 2 + height: nodeRect.portRadius * 2 + + Rectangle { + id: inPort + width: nodeRect.portRadius * 2 + height: nodeRect.portRadius * 2 + radius: nodeRect.portRadius + color: Theme.primary + border.color: "#FFFFFF" + border.width: 1.5 + + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.CrossCursor + hoverEnabled: true + + onPressed: { + if (nodeRect.drawingConnection && nodeRect.connSourceIsOutput) { + nodeRect.connectionCompleted(nodeRect.nodeData.id, modelData.name) + } + } + } + } + } + } + } + + // Output ports + Column { + anchors.right: parent.right + anchors.rightMargin: -portRadius + anchors.top: nodeHeader.bottom + anchors.topMargin: 8 + spacing: portSpacing - portRadius * 2 + + Repeater { + model: nodeRect.nodeData.outputs || [] + Item { + width: nodeRect.portRadius * 2 + height: nodeRect.portRadius * 2 + + Rectangle { + id: outPort + width: nodeRect.portRadius * 2 + height: nodeRect.portRadius * 2 + radius: nodeRect.portRadius + color: Theme.success + border.color: "#FFFFFF" + border.width: 1.5 + + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.CrossCursor + hoverEnabled: true + + onPressed: { + if (nodeRect.canvasContentItem) { + var globalPos = outPort.mapToItem(nodeRect.canvasContentItem, nodeRect.portRadius, nodeRect.portRadius) + nodeRect.connectionDragStarted(nodeRect.nodeData.id, modelData.name, true, globalPos.x, globalPos.y) + } + } + } + } + } + } + } + + // Drag handler for moving the node + DragHandler { + id: nodeDrag + target: nodeRect + onActiveChanged: { + if (!active) { + nodeRect.nodeMoved(nodeRect.nodeData.id, nodeRect.x, nodeRect.y) + } + } + onCentroidChanged: { + nodeRect.paintRequested() + } + } + + // Click to select + TapHandler { + onTapped: { + nodeRect.nodeSelected(nodeRect.nodeData.id) + } + } +} diff --git a/qml/MetaBuilder/CWorkflowSidebar.qml b/qml/MetaBuilder/CWorkflowSidebar.qml new file mode 100644 index 000000000..665a2f96c --- /dev/null +++ b/qml/MetaBuilder/CWorkflowSidebar.qml @@ -0,0 +1,80 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: root + + property var workflows: [] + property int selectedWorkflowIndex: -1 + + signal workflowSelected(int index) + signal nodeDoubleClicked(string nodeType, real canvasCenterX, real canvasCenterY) + + // Pass canvas dimensions so parent can compute center + property real canvasWidth: 800 + property real canvasHeight: 600 + + Layout.preferredWidth: 260 + Layout.fillHeight: true + color: Theme.paper + border.color: Theme.border + border.width: 1 + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Workflow List Section + ColumnLayout { + Layout.fillWidth: true + Layout.preferredHeight: 200 + Layout.margins: 12 + spacing: 4 + + CText { variant: "h4"; text: "Workflows" } + CText { variant: "caption"; text: workflows.length + " registered" } + + CDivider { Layout.fillWidth: true; Layout.topMargin: 4; Layout.bottomMargin: 4 } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: workflows.length + spacing: 2 + clip: true + delegate: CListItem { + width: parent ? parent.width : 200 + title: workflows[index].name + subtitle: (workflows[index].nodes ? workflows[index].nodes.length : 0) + " nodes" + selected: root.selectedWorkflowIndex === index + onClicked: root.workflowSelected(index) + + CBadge { + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + text: workflows[index].active ? "ON" : "OFF" + accent: workflows[index].active + } + } + } + } + + CDivider { Layout.fillWidth: true } + + // Node Palette Section + CNodePalette { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 12 + + onNodeDoubleClicked: function(nodeType) { + var cx = root.canvasWidth / 2 + var cy = root.canvasHeight / 2 + root.nodeDoubleClicked(nodeType, cx, cy) + } + } + } +} diff --git a/qml/MetaBuilder/CWorkflowTestPanel.qml b/qml/MetaBuilder/CWorkflowTestPanel.qml new file mode 100644 index 000000000..6f90afdbd --- /dev/null +++ b/qml/MetaBuilder/CWorkflowTestPanel.qml @@ -0,0 +1,157 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: root + + property bool panelVisible: false + property string executionStatus: "" + property string testInput: '{"userId": "u-42", "email": "demo@example.com"}' + property string testOutput: "" + property bool canExecute: true + property string workflowName: "" + property var workflowNodes: [] + + signal toggleVisibility() + + function execute() { + if (!canExecute) return + executionStatus = "running" + testOutput = "Executing workflow " + workflowName + "..." + panelVisible = true + executionTimer.start() + } + + Timer { + id: executionTimer + interval: 1800 + onTriggered: { + var nodes = root.workflowNodes || [] + var lines = [] + lines.push("[" + Qt.formatTime(new Date(), "HH:mm:ss") + "] Workflow: " + root.workflowName) + lines.push("[" + Qt.formatTime(new Date(), "HH:mm:ss") + "] Nodes: " + nodes.length) + lines.push("") + for (var i = 0; i < nodes.length; i++) { + var n = nodes[i] + lines.push(" [" + (i + 1) + "/" + nodes.length + "] " + n.type + "::" + n.name + " ... OK") + } + lines.push("") + lines.push("[RESULT] Workflow completed successfully.") + root.executionStatus = "success" + root.testOutput = lines.join("\n") + } + } + + Layout.fillWidth: true + Layout.preferredHeight: panelVisible ? 220 : 36 + color: Theme.paper + border.color: Theme.border + border.width: 1 + clip: true + + Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { + variant: "body2" + text: "Test Execution" + font.bold: true + } + + Rectangle { + width: 10; height: 10; radius: 5 + visible: executionStatus !== "" + color: { + if (executionStatus === "running") return Theme.warning + if (executionStatus === "success") return Theme.success + return Theme.error + } + } + CText { + variant: "caption" + visible: executionStatus !== "" + text: { + if (executionStatus === "running") return "Running..." + if (executionStatus === "success") return "Passed" + return "Failed" + } + } + + Item { Layout.fillWidth: true } + + CButton { + text: panelVisible ? "Hide" : "Show" + variant: "ghost" + size: "sm" + onClicked: root.toggleVisibility() + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 12 + visible: panelVisible + + ColumnLayout { + Layout.preferredWidth: 300 + Layout.fillHeight: true + spacing: 6 + + CText { variant: "caption"; text: "Test Input (JSON)" } + CTextField { + Layout.fillWidth: true + Layout.fillHeight: true + text: root.testInput + onTextChanged: root.testInput = text + } + CButton { + text: executionStatus === "running" ? "Executing..." : "Execute" + variant: "primary" + enabled: executionStatus !== "running" && canExecute + onClicked: root.execute() + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 6 + + CText { variant: "caption"; text: "Output Log" } + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Theme.surface + radius: 4 + border.color: Theme.border + border.width: 1 + + ScrollView { + anchors.fill: parent + anchors.margins: 8 + + Text { + width: parent.width + text: root.testOutput + color: Theme.text + font.family: "monospace" + font.pixelSize: 11 + wrapMode: Text.WrapAnywhere + } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/CWorkflowToolbar.qml b/qml/MetaBuilder/CWorkflowToolbar.qml new file mode 100644 index 000000000..e7c345ab8 --- /dev/null +++ b/qml/MetaBuilder/CWorkflowToolbar.qml @@ -0,0 +1,99 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: root + + property var workflow: null + property bool useLiveData: false + property real zoom: 1.0 + property string executionStatus: "" + property int nodeCount: 0 + property var tags: [] + + signal newWorkflow() + signal runTest() + signal toggleActive(bool active) + signal resetZoom() + + implicitHeight: 56 + color: Theme.paper + border.color: Theme.border + border.width: 1 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + spacing: 12 + + CText { + variant: "h3" + text: workflow ? workflow.name : "No Workflow" + } + + CBadge { + visible: workflow !== null + text: workflow && workflow.active ? "Active" : "Inactive" + accent: workflow ? workflow.active : false + } + + CBadge { + text: useLiveData ? "Live" : "Mock" + color: useLiveData ? Theme.success : Theme.warning + } + + Repeater { + model: tags.length > 3 ? 3 : tags.length + CChip { + text: tags[index] ? tags[index].name : "" + chipColor: Theme.border + } + } + + CBadge { + visible: nodeCount > 0 + text: nodeCount + " nodes" + color: Theme.border + } + + Item { Layout.fillWidth: true } + + CText { + variant: "caption" + text: Math.round(zoom * 100) + "%" + } + + CButton { + text: "Fit" + variant: "ghost" + size: "sm" + onClicked: root.resetZoom() + } + + CSwitch { + visible: workflow !== null + checked: workflow ? workflow.active : false + onCheckedChanged: { + if (workflow && workflow.active !== checked) { + root.toggleActive(checked) + } + } + } + + CButton { + text: "New" + variant: "ghost" + onClicked: root.newWorkflow() + } + + CButton { + text: executionStatus === "running" ? "Running..." : "Run Test" + variant: "primary" + enabled: executionStatus !== "running" && workflow !== null + onClicked: root.runTest() + } + } +} diff --git a/qml/MetaBuilder/ContactForm.qml b/qml/MetaBuilder/ContactForm.qml new file mode 100644 index 000000000..16d6376be --- /dev/null +++ b/qml/MetaBuilder/ContactForm.qml @@ -0,0 +1,64 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Rectangle { + id: contact + radius: 14 + color: "#0b1121" + border.color: "#1d2a42" + border.width: 1 + padding: 22 + + signal submitRequested(string name, string company, string email) + + ColumnLayout { + anchors.fill: parent + spacing: 16 + + Text { + text: "Start a project" + font.pixelSize: 22 + color: "#ffffff" + } + + Text { + text: "Share your stack vision and MetaBuilder will map it to seeds, workflows, and runtime automation." + font.pixelSize: 16 + color: "#aeb8cf" + wrapMode: Text.Wrap + } + + RowLayout { + spacing: 10 + + TextField { + id: nameInput + placeholderText: "Your name" + Layout.fillWidth: true + } + + TextField { + id: companyInput + placeholderText: "Company" + Layout.fillWidth: true + } + + TextField { + id: emailInput + placeholderText: "Email" + Layout.fillWidth: true + } + } + + Button { + text: "Schedule a call" + font.pixelSize: 16 + background: Rectangle { + radius: 10 + color: "#5a7dff" + } + onClicked: contact.submitRequested(nameInput.text, companyInput.text, emailInput.text) + } + } +} diff --git a/qml/MetaBuilder/CssClassPreview.qml b/qml/MetaBuilder/CssClassPreview.qml new file mode 100644 index 000000000..84a2c9754 --- /dev/null +++ b/qml/MetaBuilder/CssClassPreview.qml @@ -0,0 +1,95 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property var selectedClass: null + + function resolveColor(properties, name, fallback) { + for (var i = 0; i < properties.length; i++) { + if (properties[i].prop === name) + return properties[i].value + } + return fallback + } + + function resolveRadius(properties) { + for (var i = 0; i < properties.length; i++) { + if (properties[i].prop === "border-radius") + return parseInt(properties[i].value) || 0 + } + return 0 + } + + function resolveOpacity(properties) { + for (var i = 0; i < properties.length; i++) { + if (properties[i].prop === "opacity") + return parseFloat(properties[i].value) || 1.0 + } + return 1.0 + } + + function resolveFontSize(properties) { + for (var i = 0; i < properties.length; i++) { + if (properties[i].prop === "font-size") + return parseInt(properties[i].value) || 14 + } + return 14 + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 10 + + CText { variant: "h4"; text: "Preview" } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Theme.surface + radius: 6 + border.color: Theme.border + border.width: 1 + + Grid { + anchors.fill: parent + rows: Math.ceil(height / 12) + columns: Math.ceil(width / 12) + clip: true + Repeater { + model: Math.ceil(parent.parent.width / 12) * Math.ceil(parent.parent.height / 12) + Rectangle { + width: 12; height: 12 + color: (Math.floor(index / Math.ceil(parent.parent.width / 12)) + index) % 2 === 0 + ? "#2a2a2a" : "#333333" + } + } + } + + Rectangle { + anchors.centerIn: parent + width: Math.min(parent.width - 40, 280) + height: Math.min(parent.height - 20, 72) + radius: root.selectedClass ? resolveRadius(root.selectedClass.properties) : 0 + opacity: root.selectedClass ? resolveOpacity(root.selectedClass.properties) : 1.0 + color: root.selectedClass + ? resolveColor(root.selectedClass.properties, "background-color", Theme.surface) + : Theme.surface + + CText { + anchors.centerIn: parent + variant: "body1" + text: root.selectedClass ? "." + root.selectedClass.name : "" + color: root.selectedClass + ? resolveColor(root.selectedClass.properties, "color", Theme.text) + : Theme.text + font.pixelSize: root.selectedClass ? resolveFontSize(root.selectedClass.properties) : 14 + } + } + } + } +} diff --git a/qml/MetaBuilder/CssClassSidebar.qml b/qml/MetaBuilder/CssClassSidebar.qml new file mode 100644 index 000000000..c265d961f --- /dev/null +++ b/qml/MetaBuilder/CssClassSidebar.qml @@ -0,0 +1,68 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property var cssClasses: [] + property int selectedIndex: 0 + + signal itemClicked(int index) + + function swatchColor(properties) { + for (var i = 0; i < properties.length; i++) { + if (properties[i].prop === "background-color") + return properties[i].value + } + for (var j = 0; j < properties.length; j++) { + if (properties[j].prop === "color") + return properties[j].value + } + return Theme.surface + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 10 + + CText { variant: "h4"; text: "Classes" } + + ListView { + id: classListView + Layout.fillWidth: true + Layout.fillHeight: true + model: root.cssClasses + spacing: 4 + clip: true + + delegate: CListItem { + width: classListView.width + title: modelData.name + subtitle: modelData.properties.length + " properties" + selected: index === root.selectedIndex + + Row { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + Rectangle { + width: 16; height: 16 + radius: 3 + color: swatchColor(modelData.properties) + border.color: Theme.border + border.width: 1 + } + + CBadge { text: modelData.usageCount.toString() } + } + + onClicked: root.itemClicked(index) + } + } + } +} diff --git a/qml/MetaBuilder/CssPropertyEditor.qml b/qml/MetaBuilder/CssPropertyEditor.qml new file mode 100644 index 000000000..a4182224d --- /dev/null +++ b/qml/MetaBuilder/CssPropertyEditor.qml @@ -0,0 +1,209 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property var selectedClass: null + property var propertySuggestions: [] + + signal classNameChanged(string name) + signal deleteClassClicked() + signal addPropertyClicked() + signal removePropertyClicked(int propIndex) + signal propertyNameChanged(int propIndex, string name) + signal propertyValueChanged(int propIndex, string value) + + property bool showSuggestions: false + property int editingPropertyIndex: -1 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + + CTextField { + Layout.preferredWidth: 260 + label: "Class Name" + placeholderText: ".class-name" + text: root.selectedClass ? root.selectedClass.name : "" + onTextChanged: { + if (root.selectedClass && text !== root.selectedClass.name) + root.classNameChanged(text) + } + } + + Item { Layout.fillWidth: true } + + CButton { + text: "Delete Class" + variant: "danger" + size: "sm" + onClicked: root.deleteClassClicked() + } + } + + CDivider { Layout.fillWidth: true } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "h4"; text: "Properties" } + Item { Layout.fillWidth: true } + CButton { + text: "Add Property" + variant: "primary" + size: "sm" + onClicked: root.addPropertyClicked() + } + } + + ListView { + id: propertyListView + Layout.fillWidth: true + Layout.fillHeight: true + model: root.selectedClass ? root.selectedClass.properties : [] + spacing: 6 + clip: true + + delegate: Item { + width: propertyListView.width + height: 48 + + RowLayout { + anchors.fill: parent + spacing: 8 + + Item { + Layout.preferredWidth: 200 + Layout.fillHeight: true + + CTextField { + id: propNameField + anchors.fill: parent + label: index === 0 ? "Property" : "" + placeholderText: "property-name" + text: modelData.prop + onTextChanged: { + if (text !== modelData.prop) + root.propertyNameChanged(index, text) + } + onActiveFocusChanged: { + if (activeFocus) { + root.editingPropertyIndex = index + root.showSuggestions = true + } else { + suggestHideTimer.start() + } + } + } + + Rectangle { + visible: root.showSuggestions && root.editingPropertyIndex === index + anchors.top: propNameField.bottom + anchors.left: propNameField.left + width: propNameField.width + height: Math.min(suggestCol.implicitHeight + 8, 180) + z: 100 + color: Theme.paper + border.color: Theme.border + border.width: 1 + radius: 6 + clip: true + + Flickable { + anchors.fill: parent + anchors.margins: 4 + contentHeight: suggestCol.implicitHeight + clip: true + + Column { + id: suggestCol + width: parent.width + spacing: 2 + + Repeater { + model: root.propertySuggestions + + Rectangle { + width: suggestCol.width + height: 26 + radius: 4 + color: suggestMa.containsMouse ? Theme.surface : "transparent" + + CText { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 8 + variant: "body2" + text: modelData + } + + MouseArea { + id: suggestMa + anchors.fill: parent + hoverEnabled: true + onClicked: { + root.propertyNameChanged(root.editingPropertyIndex, modelData) + root.showSuggestions = false + } + } + } + } + } + } + } + } + + CTextField { + Layout.fillWidth: true + Layout.fillHeight: true + label: index === 0 ? "Value" : "" + placeholderText: "#000000" + text: modelData.value + onTextChanged: { + if (text !== modelData.value) + root.propertyValueChanged(index, text) + } + } + + Rectangle { + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignVCenter + radius: 4 + border.color: Theme.border + border.width: 1 + color: { + var v = modelData.value + if (v.charAt(0) === "#" || v.indexOf("rgb") === 0) + return v + return "transparent" + } + visible: modelData.prop === "color" || modelData.prop === "background-color" + } + + CButton { + text: "x" + variant: "ghost" + size: "sm" + onClicked: root.removePropertyClicked(index) + } + } + } + } + } + + Timer { + id: suggestHideTimer + interval: 200 + onTriggered: root.showSuggestions = false + } +} diff --git a/qml/MetaBuilder/DropdownGeneralForm.qml b/qml/MetaBuilder/DropdownGeneralForm.qml new file mode 100644 index 000000000..6cb59d278 --- /dev/null +++ b/qml/MetaBuilder/DropdownGeneralForm.qml @@ -0,0 +1,80 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +ColumnLayout { + id: root + spacing: 16 + + property var dropdown: null + signal fieldChanged(string field, var value) + + CText { variant: "body2"; text: "General"; font.bold: true } + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + CTextField { + label: "Name" + placeholderText: "dropdown_name" + text: root.dropdown ? root.dropdown.name : "" + Layout.fillWidth: true + onTextChanged: { + if (root.dropdown && text !== root.dropdown.name) + root.fieldChanged("name", text) + } + } + + CTextField { + label: "Description" + placeholderText: "What is this dropdown for?" + text: root.dropdown ? root.dropdown.description : "" + Layout.fillWidth: true + onTextChanged: { + if (root.dropdown && text !== root.dropdown.description) + root.fieldChanged("description", text) + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 24 + + RowLayout { + spacing: 8 + Switch { + checked: root.dropdown ? root.dropdown.allowCustom : false + onCheckedChanged: { + if (root.dropdown && checked !== root.dropdown.allowCustom) + root.fieldChanged("allowCustom", checked) + } + } + CText { variant: "body2"; text: "Allow custom values" } + } + + RowLayout { + spacing: 8 + Switch { + checked: root.dropdown ? root.dropdown.required : false + onCheckedChanged: { + if (root.dropdown && checked !== root.dropdown.required) + root.fieldChanged("required", checked) + } + } + CText { variant: "body2"; text: "Required" } + } + + Item { Layout.fillWidth: true } + + CBadge { + text: (root.dropdown && root.dropdown.required ? "Required" : "Optional") + accent: root.dropdown ? root.dropdown.required : false + } + CBadge { + text: (root.dropdown && root.dropdown.allowCustom ? "Custom allowed" : "Fixed options") + } + } +} diff --git a/qml/MetaBuilder/DropdownOptionsEditor.qml b/qml/MetaBuilder/DropdownOptionsEditor.qml new file mode 100644 index 000000000..d77fd592a --- /dev/null +++ b/qml/MetaBuilder/DropdownOptionsEditor.qml @@ -0,0 +1,101 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +ColumnLayout { + id: root + spacing: 16 + + property var dropdown: null + signal addOptionClicked() + signal removeOptionClicked(int optIndex) + signal moveOptionClicked(int optIndex, int direction) + signal optionFieldChanged(int optIndex, string field, string value) + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "body2"; text: "Options"; font.bold: true } + CBadge { text: root.dropdown ? root.dropdown.options.length + " items" : "0 items" } + Item { Layout.fillWidth: true } + CButton { + text: "+ Add Option" + variant: "primary" + size: "sm" + onClicked: root.addOptionClicked() + } + } + + Repeater { + model: root.dropdown ? root.dropdown.options : [] + + CPaper { + Layout.fillWidth: true + implicitHeight: optionRow.implicitHeight + 24 + + RowLayout { + id: optionRow + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + CText { + variant: "caption" + text: (index + 1) + "." + Layout.preferredWidth: 24 + color: Theme.text + opacity: 0.5 + } + + CTextField { + label: "Label" + placeholderText: "Display label" + text: modelData.label + Layout.fillWidth: true + onTextChanged: { + if (text !== modelData.label) + root.optionFieldChanged(index, "label", text) + } + } + + CTextField { + label: "Value" + placeholderText: "stored_value" + text: modelData.value + Layout.fillWidth: true + onTextChanged: { + if (text !== modelData.value) + root.optionFieldChanged(index, "value", text) + } + } + + ColumnLayout { + spacing: 2 + CButton { + text: "\u25B2" + variant: "ghost" + size: "sm" + enabled: index > 0 + onClicked: root.moveOptionClicked(index, -1) + } + CButton { + text: "\u25BC" + variant: "ghost" + size: "sm" + enabled: root.dropdown ? index < root.dropdown.options.length - 1 : false + onClicked: root.moveOptionClicked(index, 1) + } + } + + CButton { + text: "X" + variant: "danger" + size: "sm" + onClicked: root.removeOptionClicked(index) + } + } + } + } +} diff --git a/qml/MetaBuilder/DropdownPreview.qml b/qml/MetaBuilder/DropdownPreview.qml new file mode 100644 index 000000000..37af9f1cc --- /dev/null +++ b/qml/MetaBuilder/DropdownPreview.qml @@ -0,0 +1,153 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CPaper { + id: root + + property var dropdown: null + implicitHeight: previewColumn.implicitHeight + 32 + + ColumnLayout { + id: previewColumn + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + CText { + variant: "caption" + text: "This is how the dropdown will render in forms:" + color: Theme.text + opacity: 0.6 + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + CText { + variant: "body2" + text: (root.dropdown ? root.dropdown.name : "") + (root.dropdown && root.dropdown.required ? " *" : "") + font.bold: true + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + color: Theme.surface + border.color: Theme.border + border.width: 1 + radius: 4 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 8 + + CText { + variant: "body1" + text: root.dropdown && root.dropdown.options.length > 0 + ? root.dropdown.options[0].label + : "No options" + Layout.fillWidth: true + color: Theme.text + } + CText { + variant: "body2" + text: "\u25BE" + color: Theme.text + opacity: 0.5 + } + } + } + + CText { + variant: "caption" + text: root.dropdown ? root.dropdown.description : "" + color: Theme.text + opacity: 0.5 + } + } + + CDivider { Layout.fillWidth: true } + + CText { + variant: "caption" + text: "Expanded view:" + color: Theme.text + opacity: 0.6 + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: previewOptionsList.implicitHeight + 8 + color: Theme.surface + border.color: Theme.border + border.width: 1 + radius: 4 + + ColumnLayout { + id: previewOptionsList + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 4 + spacing: 0 + + Repeater { + model: root.dropdown ? root.dropdown.options : [] + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 + color: optMouse.containsMouse ? Theme.primary : "transparent" + opacity: optMouse.containsMouse ? 0.12 : 1.0 + radius: 2 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 8 + + CText { + variant: "body1" + text: modelData.label + Layout.fillWidth: true + } + CText { + variant: "caption" + text: modelData.value + color: Theme.text + opacity: 0.4 + } + } + + MouseArea { + id: optMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + } + } + } + } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CBadge { text: root.dropdown ? root.dropdown.options.length + " options" : "0 options" } + CBadge { + text: root.dropdown && root.dropdown.required ? "Required" : "Optional" + accent: root.dropdown ? root.dropdown.required : false + } + CBadge { + text: root.dropdown && root.dropdown.allowCustom ? "Custom values OK" : "Fixed options only" + } + } + } +} diff --git a/qml/MetaBuilder/DropdownSidebar.qml b/qml/MetaBuilder/DropdownSidebar.qml new file mode 100644 index 000000000..070f5bb87 --- /dev/null +++ b/qml/MetaBuilder/DropdownSidebar.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property var dropdowns: [] + property int selectedIndex: -1 + signal itemClicked(int index) + signal addClicked() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + CText { variant: "h4"; text: "Dropdowns" } + Item { Layout.fillWidth: true } + CButton { + text: "+ Add" + variant: "primary" + size: "sm" + onClicked: root.addClicked() + } + } + + CDivider { Layout.fillWidth: true } + + ListView { + id: dropdownList + Layout.fillWidth: true + Layout.fillHeight: true + model: root.dropdowns + spacing: 4 + clip: true + + delegate: CListItem { + width: dropdownList.width + title: modelData.name + subtitle: modelData.description + selected: index === root.selectedIndex + onClicked: root.itemClicked(index) + + CBadge { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.options.length + "" + } + } + } + } +} diff --git a/qml/MetaBuilder/FeatureCard.qml b/qml/MetaBuilder/FeatureCard.qml new file mode 100644 index 000000000..a20f5c2ce --- /dev/null +++ b/qml/MetaBuilder/FeatureCard.qml @@ -0,0 +1,33 @@ +import QtQuick +import QtQuick.Layouts + +Rectangle { + id: card + property string title: "" + property string description: "" + + radius: 10 + color: "#11152b" + border.color: "#1f2b45" + border.width: 1 + padding: 14 + + ColumnLayout { + anchors.fill: parent + spacing: 6 + + Text { + text: card.title + font.pixelSize: 16 + color: "#f5f8ff" + wrapMode: Text.Wrap + } + + Text { + text: card.description + font.pixelSize: 13 + color: "#aeb8cf" + wrapMode: Text.Wrap + } + } +} diff --git a/qml/MetaBuilder/HeroSection.qml b/qml/MetaBuilder/HeroSection.qml new file mode 100644 index 000000000..b9e9608df --- /dev/null +++ b/qml/MetaBuilder/HeroSection.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Rectangle { + id: hero + property string headline: "Build entire stacks visually, from public sites to secure admin panels." + property string subhead: "MetaBuilder layers marketing, observability, and runtime tooling into a single declarative canvas." + signal primaryAction() + signal secondaryAction() + + radius: 16 + color: "#11172d" + border.color: "#25315b" + border.width: 1 + padding: 32 + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + + ColumnLayout { + anchors.fill: parent + spacing: 18 + + Text { + text: hero.headline + font.pixelSize: 36 + font.bold: true + color: "#ffffff" + wrapMode: Text.Wrap + } + + Text { + text: hero.subhead + font.pixelSize: 18 + color: "#b1bfd7" + wrapMode: Text.Wrap + } + + RowLayout { + spacing: 12 + + Button { + text: "Explore levels" + font.pixelSize: 15 + onClicked: hero.primaryAction() + background: Rectangle { + radius: 12 + color: "#5a7dff" + border.color: "#4b6ef9" + border.width: 1 + } + } + + Button { + text: "View live demo" + font.pixelSize: 15 + onClicked: hero.secondaryAction() + background: Rectangle { + radius: 12 + color: "#11162b" + border.color: "#5a7dff" + border.width: 1 + } + } + } + } +} diff --git a/qml/MetaBuilder/LuaCodeEditor.qml b/qml/MetaBuilder/LuaCodeEditor.qml new file mode 100644 index 000000000..8812740a6 --- /dev/null +++ b/qml/MetaBuilder/LuaCodeEditor.qml @@ -0,0 +1,115 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: codeEditorRoot + Layout.fillWidth: true + Layout.fillHeight: true + color: "#1e1e2e" + border.color: Theme.border + border.width: 1 + + property alias code: codeEditor.text + property string scriptName: "" + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Line number gutter + code area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "transparent" + + ScrollView { + anchors.fill: parent + clip: true + + TextArea { + id: codeEditor + font.family: "Consolas, 'Courier New', monospace" + font.pixelSize: 13 + color: "#cdd6f4" + selectionColor: "#45475a" + selectedTextColor: "#cdd6f4" + wrapMode: TextEdit.NoWrap + tabStopDistance: 28 + padding: 16 + leftPadding: 56 + + background: Rectangle { + color: "transparent" + + // Line numbers column + Column { + x: 4 + y: codeEditor.topPadding + width: 44 + + Repeater { + model: codeEditor.text.split("\n").length + delegate: Text { + width: 40 + height: codeEditor.font.pixelSize * 1.4 + horizontalAlignment: Text.AlignRight + text: (index + 1).toString() + font.family: codeEditor.font.family + font.pixelSize: codeEditor.font.pixelSize + color: "#585b70" + } + } + } + + // Gutter separator + Rectangle { + x: 48 + y: 0 + width: 1 + height: parent.height + color: "#313244" + } + } + } + } + } + + // Editor status bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 26 + color: "#181825" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 16 + + Text { + text: "Lua 5.4" + font.pixelSize: 11 + color: "#a6adc8" + } + Text { + text: "UTF-8" + font.pixelSize: 11 + color: "#a6adc8" + } + Text { + text: codeEditor.text.split("\n").length + " lines" + font.pixelSize: 11 + color: "#a6adc8" + } + Item { Layout.fillWidth: true } + Text { + text: "MetaBuilder Lua Runtime" + font.pixelSize: 11 + color: "#585b70" + } + } + } + } +} diff --git a/qml/MetaBuilder/LuaOutputPanel.qml b/qml/MetaBuilder/LuaOutputPanel.qml new file mode 100644 index 000000000..c00762610 --- /dev/null +++ b/qml/MetaBuilder/LuaOutputPanel.qml @@ -0,0 +1,181 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: outputPanel + Layout.fillWidth: true + Layout.preferredHeight: 220 + color: Theme.paper + border.color: Theme.border + border.width: 1 + + property var params: [] + property string testOutput: "" + property string securityScanResult: "" + property string scriptName: "" + + signal executeTest(var args) + signal clearOutput() + signal paramValueChanged(int index, string value) + + RowLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 14 + + // Test inputs + ColumnLayout { + Layout.preferredWidth: 280 + Layout.fillHeight: true + spacing: 8 + + CText { variant: "h4"; text: "Test Parameters" } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + ColumnLayout { + width: parent.width + spacing: 6 + + Repeater { + model: params.length + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + CText { + variant: "caption" + text: params[index].name + " (" + params[index].type + ")" + } + CTextField { + Layout.fillWidth: true + placeholderText: "Enter " + params[index].name + "..." + text: params[index].value || "" + onTextChanged: paramValueChanged(index, text) + } + } + } + } + } + + CButton { + text: "Execute Test" + variant: "primary" + Layout.fillWidth: true + onClicked: { + var args = []; + for (var i = 0; i < params.length; i++) { + args.push(params[i].name + " = " + JSON.stringify(params[i].value || "")); + } + executeTest(args) + } + } + } + + // Separator + Rectangle { + Layout.preferredWidth: 1 + Layout.fillHeight: true + color: Theme.border + } + + // Test output + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 8 + + FlexRow { + Layout.fillWidth: true + CText { variant: "h4"; text: "Output" } + Item { Layout.fillWidth: true } + CButton { + text: "Clear" + variant: "ghost" + onClicked: clearOutput() + } + } + + // Output area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "#1e1e2e" + radius: 4 + border.color: "#313244" + border.width: 1 + + ScrollView { + anchors.fill: parent + anchors.margins: 8 + clip: true + + TextArea { + readOnly: true + text: { + var output = ""; + if (testOutput) output += testOutput; + if (securityScanResult) { + if (output) output += "\n\n"; + output += "--- Security Scan ---\n" + securityScanResult; + } + if (!output) output = "No output yet. Run a test or security scan."; + return output; + } + font.family: "Consolas, 'Courier New', monospace" + font.pixelSize: 12 + color: { + if (securityScanResult && securityScanResult.indexOf("WARN") !== -1) + return "#f9e2af"; + if (testOutput && testOutput.indexOf("SUCCESS") !== -1) + return "#a6e3a1"; + return "#a6adc8"; + } + wrapMode: TextEdit.Wrap + background: Rectangle { color: "transparent" } + } + } + } + } + + // Separator + Rectangle { + Layout.preferredWidth: 1 + Layout.fillHeight: true + color: Theme.border + } + + // Security scan results + ColumnLayout { + Layout.preferredWidth: 200 + Layout.fillHeight: true + spacing: 8 + + CText { variant: "h4"; text: "Security" } + + CAlert { + Layout.fillWidth: true + severity: securityScanResult + ? (securityScanResult.indexOf("WARN") !== -1 ? "warning" : "success") + : "info" + text: securityScanResult + ? (securityScanResult.indexOf("WARN") !== -1 ? "Advisories found" : "All checks passed") + : "Not scanned yet" + } + + CText { variant: "caption"; text: "SCAN CHECKS" } + CText { variant: "body2"; text: "os.execute() calls" } + CText { variant: "body2"; text: "Raw SQL injection" } + CText { variant: "body2"; text: "File system access" } + CText { variant: "body2"; text: "Global pollution" } + CText { variant: "body2"; text: "Unsafe concat" } + + Item { Layout.fillHeight: true } + } + } +} diff --git a/qml/MetaBuilder/LuaPropertiesPanel.qml b/qml/MetaBuilder/LuaPropertiesPanel.qml new file mode 100644 index 000000000..24fa18651 --- /dev/null +++ b/qml/MetaBuilder/LuaPropertiesPanel.qml @@ -0,0 +1,111 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: propsPanel + Layout.preferredWidth: 260 + Layout.fillHeight: true + color: Theme.paper + border.color: Theme.border + border.width: 1 + + property string scriptName: "" + property string scriptDescription: "" + property string returnType: "" + property var params: [] + property string currentCode: "" + + signal nameChanged(string name) + signal descriptionChanged(string desc) + signal returnTypeChanged(string rt) + signal paramAdded() + + ScrollView { + anchors.fill: parent + clip: true + + ColumnLayout { + width: parent.width + spacing: 12 + + Item { Layout.preferredHeight: 4 } + + ColumnLayout { + Layout.leftMargin: 14 + Layout.rightMargin: 14 + spacing: 12 + + CText { variant: "h4"; text: "Properties" } + + CDivider { Layout.fillWidth: true } + + CText { variant: "caption"; text: "SCRIPT NAME" } + CTextField { + Layout.fillWidth: true + text: scriptName + onTextChanged: nameChanged(text) + } + + CText { variant: "caption"; text: "DESCRIPTION" } + CTextField { + Layout.fillWidth: true + text: scriptDescription + onTextChanged: descriptionChanged(text) + } + + CText { variant: "caption"; text: "RETURN TYPE" } + CTextField { + Layout.fillWidth: true + text: returnType + onTextChanged: returnTypeChanged(text) + } + + CDivider { Layout.fillWidth: true } + + FlexRow { + Layout.fillWidth: true + CText { variant: "h4"; text: "Parameters" } + Item { Layout.fillWidth: true } + CBadge { text: params.length.toString() } + } + + // Parameter list + Repeater { + model: params.length + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + FlexRow { + Layout.fillWidth: true + spacing: 6 + CChip { text: params[index].type } + CText { variant: "body2"; text: params[index].name } + } + + CDivider { Layout.fillWidth: true } + } + } + + CButton { + text: "Add Parameter" + variant: "ghost" + Layout.fillWidth: true + onClicked: paramAdded() + } + + CDivider { Layout.fillWidth: true } + + CText { variant: "h4"; text: "Info" } + CText { variant: "caption"; text: "LINES OF CODE" } + CText { variant: "body2"; text: currentCode.split("\n").length.toString() } + CText { variant: "caption"; text: "SIZE" } + CText { variant: "body2"; text: (currentCode.length / 1024).toFixed(1) + " KB" } + } + + Item { Layout.preferredHeight: 8 } + } + } +} diff --git a/qml/MetaBuilder/LuaScriptSidebar.qml b/qml/MetaBuilder/LuaScriptSidebar.qml new file mode 100644 index 000000000..d2fee1adb --- /dev/null +++ b/qml/MetaBuilder/LuaScriptSidebar.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: sidebar + Layout.preferredWidth: 220 + Layout.fillHeight: true + color: Theme.paper + border.color: Theme.border + border.width: 1 + + property var scripts: [] + property var snippets: [] + property int selectedScriptIndex: 0 + property bool showSnippets: false + + signal scriptSelected(int index) + signal snippetInserted(string code) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 4 + + CText { variant: "h4"; text: "Lua Scripts" } + CText { variant: "caption"; text: scripts.length + " scripts loaded" } + + CDivider { Layout.fillWidth: true; Layout.topMargin: 8; Layout.bottomMargin: 4 } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: scripts + spacing: 2 + clip: true + delegate: CListItem { + width: parent ? parent.width : 200 + title: modelData.name + subtitle: modelData.returnType + " | " + modelData.params.length + " param" + (modelData.params.length !== 1 ? "s" : "") + selected: selectedScriptIndex === index + onClicked: scriptSelected(index) + } + } + + CDivider { Layout.fillWidth: true; Layout.topMargin: 4; Layout.bottomMargin: 4 } + + // Snippet library toggle + CButton { + text: showSnippets ? "Hide Snippets" : "Snippet Library" + variant: "ghost" + Layout.fillWidth: true + onClicked: showSnippets = !showSnippets + } + + // Snippet library + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + visible: showSnippets + + Repeater { + model: snippets + delegate: CListItem { + width: parent ? parent.width : 200 + title: modelData.label + subtitle: "Insert snippet" + onClicked: snippetInserted(modelData.code) + } + } + } + } +} diff --git a/qml/MetaBuilder/MediaJobForm.qml b/qml/MetaBuilder/MediaJobForm.qml new file mode 100644 index 000000000..1ecbf6636 --- /dev/null +++ b/qml/MetaBuilder/MediaJobForm.qml @@ -0,0 +1,97 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: jobForm + Layout.fillWidth: true + + property int jobTypeIndex: 0 + property var jobTypes: ["video", "audio", "document", "image"] + property string jobInputPath: "" + property string jobOutputPath: "" + property int jobPriorityIndex: 2 + property var jobPriorities: ["urgent", "high", "normal", "low"] + + signal submitRequested() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + CText { variant: "h4"; text: "Submit Job" } + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + // Type selector + ColumnLayout { + Layout.preferredWidth: 200 + spacing: 4 + + CText { variant: "caption"; text: "Type" } + + RowLayout { + spacing: 4 + + Repeater { + model: jobTypes + delegate: CButton { + text: modelData + variant: jobTypeIndex === index ? "primary" : "ghost" + size: "sm" + onClicked: jobTypeIndex = index + } + } + } + } + + CTextField { + Layout.fillWidth: true + label: "Input Path" + placeholderText: "/media/input/video.mp4" + text: jobInputPath + onTextChanged: jobInputPath = text + } + + CTextField { + Layout.fillWidth: true + label: "Output Path" + placeholderText: "/media/output/video.webm" + text: jobOutputPath + onTextChanged: jobOutputPath = text + } + } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + // Priority selector + CText { variant: "caption"; text: "Priority:" } + + Repeater { + model: jobPriorities + delegate: CButton { + text: modelData + variant: jobPriorityIndex === index ? "primary" : "ghost" + size: "sm" + onClicked: jobPriorityIndex = index + } + } + + Item { Layout.fillWidth: true } + + CButton { + text: "Submit Job" + variant: "primary" + enabled: jobInputPath.length > 0 && jobOutputPath.length > 0 + onClicked: submitRequested() + } + } + } +} diff --git a/qml/MetaBuilder/MediaJobTable.qml b/qml/MetaBuilder/MediaJobTable.qml new file mode 100644 index 000000000..49b196813 --- /dev/null +++ b/qml/MetaBuilder/MediaJobTable.qml @@ -0,0 +1,146 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: jobTable + Layout.fillWidth: true + + property var jobs: [] + + signal cancelRequested(string jobId) + + function jobStatusColor(status) { + switch (status) { + case "completed": return "success" + case "processing": return "warning" + case "queued": return "info" + case "failed": return "error" + default: return "info" + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + + CText { variant: "h4"; text: "Active Jobs" } + CText { variant: "caption"; text: jobs.length + " total"; color: Theme.textSecondary } + } + + CDivider { Layout.fillWidth: true } + + // Table header + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "caption"; text: "ID"; Layout.preferredWidth: 100 } + CText { variant: "caption"; text: "Type"; Layout.preferredWidth: 80 } + CText { variant: "caption"; text: "Status"; Layout.preferredWidth: 100 } + CText { variant: "caption"; text: "Progress"; Layout.fillWidth: true } + CText { variant: "caption"; text: "Created"; Layout.preferredWidth: 160 } + CText { variant: "caption"; text: ""; Layout.preferredWidth: 70 } + } + + CDivider { Layout.fillWidth: true } + + // Job rows + Repeater { + model: jobs + + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { + variant: "body2" + text: modelData.id + font.family: "monospace" + Layout.preferredWidth: 100 + } + + CBadge { + text: modelData.type + Layout.preferredWidth: 80 + } + + CStatusBadge { + status: jobStatusColor(modelData.status) + text: modelData.status + Layout.preferredWidth: 100 + } + + // Progress bar area + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 20 + color: "transparent" + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: 6 + radius: 3 + color: Theme.border + + Rectangle { + width: parent.width * (modelData.progress / 100) + height: parent.height + radius: 3 + color: modelData.status === "failed" ? Theme.error + : modelData.status === "completed" ? Theme.success + : Theme.primary + } + } + + CText { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + variant: "caption" + text: modelData.progress + "%" + } + } + + CText { + variant: "caption" + text: modelData.created + Layout.preferredWidth: 160 + color: Theme.textSecondary + } + + CButton { + text: "Cancel" + variant: "danger" + size: "sm" + enabled: modelData.status === "queued" || modelData.status === "processing" + visible: modelData.status !== "completed" && modelData.status !== "failed" + Layout.preferredWidth: 70 + onClicked: cancelRequested(modelData.id) + } + + // Placeholder for completed/failed jobs + Item { + visible: modelData.status === "completed" || modelData.status === "failed" + Layout.preferredWidth: 70 + } + } + + CDivider { + Layout.fillWidth: true + visible: index < jobs.length - 1 + } + } + } + } +} diff --git a/qml/MetaBuilder/MediaPluginsTab.qml b/qml/MetaBuilder/MediaPluginsTab.qml new file mode 100644 index 000000000..223476f86 --- /dev/null +++ b/qml/MetaBuilder/MediaPluginsTab.qml @@ -0,0 +1,119 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: pluginsTab + color: "transparent" + + property var plugins: [] + + signal reloadAll() + signal reloadPlugin(string pluginName) + + ScrollView { + anchors.fill: parent + clip: true + + ColumnLayout { + width: parent.width + spacing: 16 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + + CText { variant: "h3"; text: "Installed Plugins" } + CText { variant: "caption"; text: plugins.length + " plugins"; color: Theme.textSecondary } + + Item { Layout.fillWidth: true } + + CButton { + text: "Reload All (Dev)" + variant: "ghost" + size: "sm" + onClicked: reloadAll() + } + } + + CDivider { Layout.fillWidth: true } + + // Plugin grid (2 columns) + GridLayout { + Layout.fillWidth: true + columns: 2 + columnSpacing: 16 + rowSpacing: 16 + + Repeater { + model: plugins + + delegate: CCard { + Layout.fillWidth: true + variant: "outlined" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 10 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "subtitle1"; text: modelData.name } + + Item { Layout.fillWidth: true } + + CStatusBadge { + status: modelData.status === "active" ? "success" : "warning" + text: modelData.status + } + } + + CText { + variant: "caption" + text: "v" + modelData.version + color: Theme.textSecondary + } + + CDivider { Layout.fillWidth: true } + + CText { variant: "caption"; text: "Capabilities" } + + Flow { + Layout.fillWidth: true + spacing: 6 + + Repeater { + model: modelData.capabilities + + delegate: CChip { + text: modelData + } + } + } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + Item { Layout.fillWidth: true } + + CButton { + text: "Reload" + variant: "ghost" + size: "sm" + onClicked: reloadPlugin(plugins[index].name.toLowerCase()) + } + } + } + } + } + } + + Item { Layout.preferredHeight: 8 } + } + } +} diff --git a/qml/MetaBuilder/MediaRadioTab.qml b/qml/MetaBuilder/MediaRadioTab.qml new file mode 100644 index 000000000..4e607bdca --- /dev/null +++ b/qml/MetaBuilder/MediaRadioTab.qml @@ -0,0 +1,190 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: radioTab + color: "transparent" + + property int selectedRadioIndex: 0 + property var radioChannels: [] + + signal toggleStream(int index) + + RowLayout { + anchors.fill: parent + spacing: 16 + + // Channel List + CCard { + Layout.preferredWidth: 320 + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "h4"; text: "Radio Channels" } + CText { variant: "caption"; text: radioChannels.length + " channels"; color: Theme.textSecondary } + } + + CDivider { Layout.fillWidth: true } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: radioChannels + spacing: 4 + clip: true + + delegate: CListItem { + width: parent ? parent.width : 288 + title: modelData.name + subtitle: modelData.status === "live" + ? modelData.listeners + " listeners" + : "Offline" + selected: index === selectedRadioIndex + onClicked: selectedRadioIndex = index + } + } + } + } + + // Channel Detail + CCard { + Layout.fillWidth: true + Layout.fillHeight: true + + Flickable { + anchors.fill: parent + anchors.margins: 16 + contentHeight: radioDetailCol.implicitHeight + clip: true + + ColumnLayout { + id: radioDetailCol + width: parent.width + spacing: 16 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + + CText { variant: "h3"; text: radioChannels[selectedRadioIndex].name } + CStatusBadge { + status: radioChannels[selectedRadioIndex].status === "live" ? "success" : "error" + text: radioChannels[selectedRadioIndex].status === "live" ? "Live" : "Offline" + } + + Item { Layout.fillWidth: true } + + CButton { + text: radioChannels[selectedRadioIndex].status === "live" ? "Stop Stream" : "Start Stream" + variant: radioChannels[selectedRadioIndex].status === "live" ? "danger" : "primary" + onClicked: toggleStream(selectedRadioIndex) + } + } + + CDivider { Layout.fillWidth: true } + + // Stats row + 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: "Listeners" } + CText { variant: "h4"; text: radioChannels[selectedRadioIndex].listeners.toString() } + } + } + + CPaper { + Layout.fillWidth: true + implicitHeight: 60 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 2 + CText { variant: "caption"; text: "Bitrate" } + CText { variant: "h4"; text: radioChannels[selectedRadioIndex].bitrate } + } + } + + CPaper { + Layout.fillWidth: true + implicitHeight: 60 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 2 + CText { variant: "caption"; text: "Now Playing" } + CText { + variant: "body2" + text: radioChannels[selectedRadioIndex].currentTrack + elide: Text.ElideRight + Layout.fillWidth: true + } + } + } + } + + CDivider { Layout.fillWidth: true } + + // Playlist + CText { variant: "subtitle1"; text: "Playlist" } + CText { + variant: "caption" + text: radioChannels[selectedRadioIndex].playlist.length + " tracks" + color: Theme.textSecondary + } + + Repeater { + model: radioChannels[selectedRadioIndex].playlist + + delegate: FlexRow { + Layout.fillWidth: true + spacing: 12 + + CText { + variant: "caption" + text: (index + 1).toString().padStart(2, " ") + "." + font.family: "monospace" + color: Theme.textSecondary + } + + CText { + variant: "body2" + text: modelData + Layout.fillWidth: true + } + + CStatusBadge { + visible: modelData === radioChannels[selectedRadioIndex].currentTrack + && radioChannels[selectedRadioIndex].status === "live" + status: "success" + text: "Playing" + } + } + } + + Item { Layout.preferredHeight: 8 } + } + } + } + } +} diff --git a/qml/MetaBuilder/MediaTvTab.qml b/qml/MetaBuilder/MediaTvTab.qml new file mode 100644 index 000000000..c9bec5579 --- /dev/null +++ b/qml/MetaBuilder/MediaTvTab.qml @@ -0,0 +1,235 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: tvTab + color: "transparent" + + property int selectedTvIndex: 0 + property var tvChannels: [] + + signal toggleBroadcast(int index) + + function resolutionColor(res) { + switch (res) { + case "1080p": return Theme.success + case "720p": return Theme.warning + case "480p": return Theme.error + default: return Theme.textSecondary + } + } + + RowLayout { + anchors.fill: parent + spacing: 16 + + // Channel List + CCard { + Layout.preferredWidth: 320 + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "h4"; text: "TV Channels" } + CText { variant: "caption"; text: tvChannels.length + " channels"; color: Theme.textSecondary } + } + + CDivider { Layout.fillWidth: true } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: tvChannels + spacing: 4 + clip: true + + delegate: CListItem { + width: parent ? parent.width : 288 + title: modelData.name + subtitle: modelData.status === "broadcasting" + ? modelData.viewers + " viewers" + : "Offline" + selected: index === selectedTvIndex + onClicked: selectedTvIndex = index + } + } + } + } + + // Channel Detail + CCard { + Layout.fillWidth: true + Layout.fillHeight: true + + Flickable { + anchors.fill: parent + anchors.margins: 16 + contentHeight: tvDetailCol.implicitHeight + clip: true + + ColumnLayout { + id: tvDetailCol + width: parent.width + spacing: 16 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + + CText { variant: "h3"; text: tvChannels[selectedTvIndex].name } + CStatusBadge { + status: tvChannels[selectedTvIndex].status === "broadcasting" ? "success" : "error" + text: tvChannels[selectedTvIndex].status === "broadcasting" ? "Broadcasting" : "Offline" + } + + // Resolution badge + Rectangle { + width: resLabel.implicitWidth + 16 + height: 24 + radius: 4 + color: resolutionColor(tvChannels[selectedTvIndex].resolution) + opacity: 0.15 + + CText { + id: resLabel + anchors.centerIn: parent + variant: "caption" + text: tvChannels[selectedTvIndex].resolution + color: resolutionColor(tvChannels[selectedTvIndex].resolution) + font.bold: true + } + } + + Item { Layout.fillWidth: true } + + CButton { + text: tvChannels[selectedTvIndex].status === "broadcasting" ? "Stop Broadcast" : "Start Broadcast" + variant: tvChannels[selectedTvIndex].status === "broadcasting" ? "danger" : "primary" + onClicked: toggleBroadcast(selectedTvIndex) + } + } + + CDivider { Layout.fillWidth: true } + + // Stats row + 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: "Viewers" } + CText { variant: "h4"; text: tvChannels[selectedTvIndex].viewers.toString() } + } + } + + CPaper { + Layout.fillWidth: true + implicitHeight: 60 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 2 + CText { variant: "caption"; text: "Resolution" } + CText { variant: "h4"; text: tvChannels[selectedTvIndex].resolution } + } + } + + CPaper { + Layout.fillWidth: true + implicitHeight: 60 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 2 + CText { variant: "caption"; text: "Uptime" } + CText { variant: "h4"; text: tvChannels[selectedTvIndex].uptime } + } + } + } + + CDivider { Layout.fillWidth: true } + + // Schedule + CText { variant: "subtitle1"; text: "Schedule" } + CText { + variant: "caption" + text: tvChannels[selectedTvIndex].schedule.length + " programs" + color: Theme.textSecondary + } + + // Schedule table header + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "caption"; text: "Time"; Layout.preferredWidth: 80 } + CText { variant: "caption"; text: "Program"; Layout.fillWidth: true } + CText { variant: "caption"; text: "Duration"; Layout.preferredWidth: 80 } + } + + CDivider { Layout.fillWidth: true } + + Repeater { + model: tvChannels[selectedTvIndex].schedule + + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CText { + variant: "body2" + text: modelData.time + font.family: "monospace" + font.bold: true + Layout.preferredWidth: 80 + } + + CText { + variant: "body2" + text: modelData.program + Layout.fillWidth: true + } + + CText { + variant: "caption" + text: modelData.duration + color: Theme.textSecondary + Layout.preferredWidth: 80 + } + } + + CDivider { + Layout.fillWidth: true + visible: index < tvChannels[selectedTvIndex].schedule.length - 1 + } + } + } + + Item { Layout.preferredHeight: 8 } + } + } + } + } +} diff --git a/qml/MetaBuilder/NavBar.qml b/qml/MetaBuilder/NavBar.qml new file mode 100644 index 000000000..6889901ea --- /dev/null +++ b/qml/MetaBuilder/NavBar.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Rectangle { + id: navBar + property alias title: titleText.text + property var actions: ["Home", "Docs", "Login"] + signal actionTriggered(string action) + + height: 64 + width: parent ? parent.width : 1280 + color: "#050613" + border.color: "#1e2b4a" + + RowLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 24 + + Text { + id: titleText + text: "MetaBuilder" + color: "#f8fbff" + font.pixelSize: 20 + font.bold: true + } + + Item { Layout.fillWidth: true } + + Repeater { + model: actions + delegate: Button { + text: modelData + font.pixelSize: 14 + onClicked: navBar.actionTriggered(modelData) + } + } + } +} diff --git a/qml/MetaBuilder/SchemaFieldEditor.qml b/qml/MetaBuilder/SchemaFieldEditor.qml new file mode 100644 index 000000000..4fd1d0000 --- /dev/null +++ b/qml/MetaBuilder/SchemaFieldEditor.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property var field: null + property var fieldTypes: [] + + signal fieldUpdated(string key, var value) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 14 + + CText { variant: "subtitle1"; text: "Field Editor" } + CDivider { Layout.fillWidth: true } + + CTextField { + label: "Field Name" + placeholderText: "e.g. username" + text: root.field ? root.field.name : "" + Layout.fillWidth: true + onTextChanged: { + if (root.field && text !== root.field.name) + root.fieldUpdated("name", text) + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + CText { variant: "caption"; text: "Type" } + CSelect { + model: root.fieldTypes + currentIndex: { + if (!root.field) return 0 + var idx = root.fieldTypes.indexOf(root.field.type) + return idx >= 0 ? idx : 0 + } + Layout.fillWidth: true + onCurrentIndexChanged: { + if (root.field && root.fieldTypes[currentIndex] !== root.field.type) + root.fieldUpdated("type", root.fieldTypes[currentIndex]) + } + } + } + + CSwitch { + text: "Required" + checked: root.field ? root.field.required : false + onCheckedChanged: { + if (root.field && checked !== root.field.required) + root.fieldUpdated("required", checked) + } + } + + CTextField { + label: "Default Value" + placeholderText: "e.g. uuid()" + text: root.field ? root.field.defaultValue : "" + Layout.fillWidth: true + onTextChanged: { + if (root.field && text !== root.field.defaultValue) + root.fieldUpdated("defaultValue", text) + } + } + + CTextField { + label: "Description" + placeholderText: "Field description" + text: root.field ? root.field.description : "" + Layout.fillWidth: true + onTextChanged: { + if (root.field && text !== root.field.description) + root.fieldUpdated("description", text) + } + } + + Item { Layout.fillHeight: true } + + CAlert { + severity: "info" + text: "Editing: " + (root.field ? root.field.name : "") + Layout.fillWidth: true + } + } +} diff --git a/qml/MetaBuilder/SchemaFieldsTable.qml b/qml/MetaBuilder/SchemaFieldsTable.qml new file mode 100644 index 000000000..a86488396 --- /dev/null +++ b/qml/MetaBuilder/SchemaFieldsTable.qml @@ -0,0 +1,129 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property var schema: null + property var fields: [] + property int selectedFieldIndex: -1 + + signal fieldClicked(int index) + signal addFieldClicked() + signal removeFieldClicked() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + + CText { + variant: "h4" + text: root.schema ? root.schema.name + " Fields" : "No Schema Selected" + } + Item { Layout.fillWidth: true } + CButton { + text: "Add Field" + variant: "primary" + size: "sm" + onClicked: root.addFieldClicked() + visible: root.schema !== null + } + CButton { + text: "Remove Field" + variant: "danger" + size: "sm" + enabled: root.selectedFieldIndex >= 0 + visible: root.schema !== null + onClicked: root.removeFieldClicked() + } + } + + Rectangle { + Layout.fillWidth: true + height: 36 + color: Theme.surface + radius: 4 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 8 + + CText { variant: "caption"; text: "NAME"; Layout.preferredWidth: 140; font.bold: true } + CText { variant: "caption"; text: "TYPE"; Layout.preferredWidth: 100; font.bold: true } + CText { variant: "caption"; text: "REQUIRED"; Layout.preferredWidth: 80; font.bold: true } + CText { variant: "caption"; text: "DEFAULT"; Layout.fillWidth: true; font.bold: true } + } + } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: root.fields + spacing: 2 + clip: true + + delegate: Rectangle { + width: parent ? parent.width : 400 + height: 40 + radius: 4 + color: index === root.selectedFieldIndex + ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + : (fieldMouse.containsMouse ? Theme.surface : "transparent") + + MouseArea { + id: fieldMouse + anchors.fill: parent + hoverEnabled: true + onClicked: root.fieldClicked(index) + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 8 + + CText { + variant: "body2" + text: modelData.name + Layout.preferredWidth: 140 + font.bold: index === root.selectedFieldIndex + } + CChip { + text: modelData.type + Layout.preferredWidth: 100 + } + CText { + variant: "body2" + text: modelData.required ? "Yes" : "No" + color: modelData.required ? Theme.primary : Theme.border + Layout.preferredWidth: 80 + } + CText { + variant: "caption" + text: modelData.defaultValue || "-" + Layout.fillWidth: true + elide: Text.ElideRight + } + } + } + } + + CDivider { Layout.fillWidth: true } + + CText { + variant: "caption" + text: root.schema ? root.schema.description : "" + color: Theme.border + } + } +} diff --git a/qml/MetaBuilder/SchemaSidebar.qml b/qml/MetaBuilder/SchemaSidebar.qml new file mode 100644 index 000000000..d89aa7906 --- /dev/null +++ b/qml/MetaBuilder/SchemaSidebar.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property var schemas: [] + property int selectedIndex: 0 + + signal itemClicked(int index) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 10 + + CText { variant: "subtitle1"; text: "Schemas" } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: root.schemas + spacing: 4 + clip: true + delegate: CListItem { + width: parent ? parent.width : 200 + title: modelData.name + subtitle: modelData.fields.length + " fields" + selected: index === root.selectedIndex + leadingIcon: "schema" + onClicked: root.itemClicked(index) + } + } + + CDivider { Layout.fillWidth: true } + + CText { + variant: "caption" + text: root.schemas.length + " schemas total" + color: Theme.border + } + } +} diff --git a/qml/MetaBuilder/StatusCard.qml b/qml/MetaBuilder/StatusCard.qml new file mode 100644 index 000000000..d109f7b82 --- /dev/null +++ b/qml/MetaBuilder/StatusCard.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Layouts + +Rectangle { + id: statusCard + property string label: "" + property string value: "" + + radius: 12 + color: "#0f1324" + border.color: "#1f2b46" + border.width: 1 + height: 60 + padding: 16 + + RowLayout { + anchors.fill: parent + spacing: 12 + + Text { + text: statusCard.label + color: "#d9e1ff" + font.pixelSize: 17 + horizontalAlignment: Text.AlignLeft + } + + Item { Layout.fillWidth: true } + + Text { + text: statusCard.value + font.pixelSize: 16 + color: statusCard.value === "healthy" ? "#39d98a" : "#facc15" + horizontalAlignment: Text.AlignRight + } + } +} diff --git a/qml/MetaBuilder/ThemeColorTokens.qml b/qml/MetaBuilder/ThemeColorTokens.qml new file mode 100644 index 000000000..ac2c8310f --- /dev/null +++ b/qml/MetaBuilder/ThemeColorTokens.qml @@ -0,0 +1,145 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: colorTokens + Layout.fillWidth: true + + property string customPrimary: "#000000" + property string customBackground: "#000000" + property string customSurface: "#000000" + property string customPaper: "#000000" + property string customText: "#000000" + property string customTextSecondary: "#000000" + property string customBorder: "#000000" + property string customError: "#000000" + property string customWarning: "#000000" + property string customSuccess: "#000000" + property string customInfo: "#000000" + + signal colorChanged(string token, string value) + + // Inline color field component + component ColorField: RowLayout { + Layout.fillWidth: true + spacing: 10 + + property string label: "" + property string colorValue: "#000000" + signal colorEdited(string val) + + Rectangle { + width: 32 + height: 32 + radius: 6 + color: colorValue + border.width: 1 + border.color: Theme.border + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: 5 + color: "transparent" + border.width: 1 + border.color: Qt.darker(colorValue, 1.4) + z: -1 + } + } + + CTextField { + Layout.fillWidth: true + label: parent.label + placeholderText: "#RRGGBB" + text: colorValue + onTextChanged: { + if (/^#[0-9a-fA-F]{6}$/.test(text)) { + colorEdited(text) + } + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + CText { variant: "h4"; text: "Color Tokens" } + Item { Layout.fillWidth: true } + CChip { text: "11 tokens" } + } + + CText { variant: "caption"; text: "Click any swatch to fine-tune. Values must be valid hex colors (#RRGGBB)." } + + CDivider { Layout.fillWidth: true } + + GridLayout { + Layout.fillWidth: true + columns: 2 + rowSpacing: 12 + columnSpacing: 16 + + ColorField { + label: "Primary" + colorValue: customPrimary + onColorEdited: function(val) { colorChanged("primary", val) } + } + ColorField { + label: "Background" + colorValue: customBackground + onColorEdited: function(val) { colorChanged("background", val) } + } + ColorField { + label: "Surface" + colorValue: customSurface + onColorEdited: function(val) { colorChanged("surface", val) } + } + ColorField { + label: "Paper" + colorValue: customPaper + onColorEdited: function(val) { colorChanged("paper", val) } + } + ColorField { + label: "Text" + colorValue: customText + onColorEdited: function(val) { colorChanged("text", val) } + } + ColorField { + label: "Text Secondary" + colorValue: customTextSecondary + onColorEdited: function(val) { colorChanged("textSecondary", val) } + } + ColorField { + label: "Border" + colorValue: customBorder + onColorEdited: function(val) { colorChanged("border", val) } + } + ColorField { + label: "Error" + colorValue: customError + onColorEdited: function(val) { colorChanged("error", val) } + } + ColorField { + label: "Warning" + colorValue: customWarning + onColorEdited: function(val) { colorChanged("warning", val) } + } + ColorField { + label: "Success" + colorValue: customSuccess + onColorEdited: function(val) { colorChanged("success", val) } + } + ColorField { + label: "Info" + colorValue: customInfo + onColorEdited: function(val) { colorChanged("info", val) } + } + } + } +} diff --git a/qml/MetaBuilder/ThemeLivePreview.qml b/qml/MetaBuilder/ThemeLivePreview.qml new file mode 100644 index 000000000..bc2ecb5a0 --- /dev/null +++ b/qml/MetaBuilder/ThemeLivePreview.qml @@ -0,0 +1,256 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: livePreview + Layout.fillWidth: true + + property string customPrimary: "#000000" + property string customBackground: "#000000" + property string customSurface: "#000000" + property string customPaper: "#000000" + property string customText: "#000000" + property string customTextSecondary: "#000000" + property string customBorder: "#000000" + property string customError: "#000000" + property string customWarning: "#000000" + property string customSuccess: "#000000" + property string customInfo: "#000000" + property string fontFamily: "Inter" + property int baseFontSize: 14 + property int radiusSmall: 4 + property int radiusMedium: 8 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + FlexRow { + Layout.fillWidth: true + spacing: 12 + CText { variant: "h4"; text: "Live Preview" } + Item { Layout.fillWidth: true } + CBadge { text: "Interactive" } + } + + CText { variant: "caption"; text: "A sample UI rendered with your current theme configuration" } + + CDivider { Layout.fillWidth: true } + + // Preview container + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 340 + radius: radiusMedium + color: customBackground + border.width: 1 + border.color: customBorder + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 14 + + // Preview header bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + radius: radiusSmall + color: customSurface + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 14 + anchors.rightMargin: 14 + spacing: 12 + + Text { + text: "MetaBuilder" + font.pixelSize: baseFontSize + 2 + font.weight: Font.Bold + font.family: fontFamily + color: customText + } + + Item { Layout.fillWidth: true } + + Repeater { + model: ["Dashboard", "Settings", "Help"] + Text { + text: modelData + font.pixelSize: baseFontSize - 1 + font.family: fontFamily + color: customTextSecondary + } + } + } + } + + // Preview content area + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 12 + + // Preview card 1 - Status + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: radiusMedium + color: customPaper + border.width: 1 + border.color: customBorder + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 8 + + Text { + text: "Status" + font.pixelSize: baseFontSize + font.weight: Font.Bold + font.family: fontFamily + color: customText + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: customBorder + } + + Repeater { + model: [ + { label: "DBAL", col: customSuccess }, + { label: "Auth", col: customSuccess }, + { label: "Storage", col: customWarning } + ] + + RowLayout { + spacing: 8 + Rectangle { + width: 8; height: 8; radius: 4 + color: modelData.col + } + Text { + text: modelData.label + font.pixelSize: baseFontSize - 2 + font.family: fontFamily + color: customTextSecondary + } + } + } + + Item { Layout.fillHeight: true } + + Rectangle { + Layout.fillWidth: true + height: 30 + radius: radiusSmall + color: customPrimary + + Text { + anchors.centerIn: parent + text: "View Details" + font.pixelSize: baseFontSize - 2 + font.family: fontFamily + color: "#ffffff" + } + } + } + } + + // Preview card 2 - Activity + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: radiusMedium + color: customPaper + border.width: 1 + border.color: customBorder + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 8 + + Text { + text: "Activity" + font.pixelSize: baseFontSize + font.weight: Font.Bold + font.family: fontFamily + color: customText + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: customBorder + } + + Repeater { + model: [ + { msg: "User signed in", t: "2m ago" }, + { msg: "Package installed", t: "5m ago" }, + { msg: "Schema updated", t: "1h ago" } + ] + + ColumnLayout { + spacing: 2 + Text { + text: modelData.msg + font.pixelSize: baseFontSize - 2 + font.family: fontFamily + color: customText + } + Text { + text: modelData.t + font.pixelSize: baseFontSize - 4 + font.family: fontFamily + color: customTextSecondary + } + } + } + + Item { Layout.fillHeight: true } + + Rectangle { + Layout.fillWidth: true + height: 24 + radius: radiusSmall + color: Qt.alpha(customError, 0.15) + + Text { + anchors.centerIn: parent + text: "1 alert" + font.pixelSize: baseFontSize - 4 + font.family: fontFamily + color: customError + } + } + + Rectangle { + Layout.fillWidth: true + height: 24 + radius: radiusSmall + color: Qt.alpha(customInfo, 0.15) + + Text { + anchors.centerIn: parent + text: "3 notifications" + font.pixelSize: baseFontSize - 4 + font.family: fontFamily + color: customInfo + } + } + } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/ThemePresetGrid.qml b/qml/MetaBuilder/ThemePresetGrid.qml new file mode 100644 index 000000000..b466a78b5 --- /dev/null +++ b/qml/MetaBuilder/ThemePresetGrid.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: presetGrid + Layout.fillWidth: true + + property string selectedTheme: "dark" + property int radiusMedium: 8 + property var themeDefinitions: [] + + signal themeSelected(string name) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + CText { variant: "h4"; text: "Theme Presets" } + CText { variant: "caption"; text: "Select a base theme to start from" } + + GridLayout { + Layout.fillWidth: true + columns: 3 + rowSpacing: 12 + columnSpacing: 12 + + Repeater { + model: themeDefinitions + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 90 + radius: radiusMedium + color: modelData.surface + border.width: selectedTheme === modelData.name ? 2 : 1 + border.color: selectedTheme === modelData.name ? Theme.primary : Theme.border + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: themeSelected(modelData.name) + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + + // Mini color swatch row + RowLayout { + spacing: 4 + + Repeater { + model: [modelData.bg, modelData.primary, modelData.text, modelData.surface] + + Rectangle { + width: 16 + height: 16 + radius: 3 + color: modelData + border.width: 1 + border.color: Qt.darker(modelData, 1.3) + } + } + } + + Item { Layout.fillHeight: true } + + Text { + text: themeDefinitions[index].label + font.pixelSize: 12 + font.weight: selectedTheme === themeDefinitions[index].name ? Font.Bold : Font.Normal + color: themeDefinitions[index].text + } + + // Selection indicator + Rectangle { + width: 8 + height: 8 + radius: 4 + color: selectedTheme === themeDefinitions[index].name ? Theme.primary : "transparent" + } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/ThemeSpacingRadius.qml b/qml/MetaBuilder/ThemeSpacingRadius.qml new file mode 100644 index 000000000..a7daf2c5a --- /dev/null +++ b/qml/MetaBuilder/ThemeSpacingRadius.qml @@ -0,0 +1,182 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +ColumnLayout { + id: spacingRadiusRoot + Layout.fillWidth: true + spacing: 20 + + property int baseSpacing: 8 + property int radiusSmall: 4 + property int radiusMedium: 8 + property int radiusLarge: 16 + + signal baseSpacingChanged(int value) + signal radiusSmallChanged(int value) + signal radiusMediumChanged(int value) + signal radiusLargeChanged(int value) + + // Spacing section + CCard { + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + CText { variant: "h4"; text: "Spacing" } + CText { variant: "caption"; text: "Base spacing unit used as a multiplier across the layout system" } + + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + CTextField { + Layout.preferredWidth: 120 + label: "Base Spacing (px)" + placeholderText: "8" + text: baseSpacing.toString() + onTextChanged: { + var val = parseInt(text) + if (!isNaN(val) && val > 0 && val <= 32) { + baseSpacingChanged(val) + } + } + } + + // Spacing preview + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + CText { variant: "caption"; text: "Preview: spacing scale" } + + RowLayout { + spacing: 8 + + Repeater { + model: [1, 2, 3, 4, 6] + + ColumnLayout { + spacing: 4 + Rectangle { + width: baseSpacing * modelData + height: baseSpacing * modelData + radius: 3 + color: Theme.primary + opacity: 0.3 + (index * 0.15) + } + Text { + text: (baseSpacing * modelData) + "px" + font.pixelSize: 10 + color: Theme.textSecondary + horizontalAlignment: Text.AlignHCenter + } + } + } + } + } + } + } + } + + // Border Radius section + CCard { + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + CText { variant: "h4"; text: "Border Radius" } + CText { variant: "caption"; text: "Control corner rounding for small, medium, and large elements" } + + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + CTextField { + Layout.preferredWidth: 100 + label: "Small (px)" + placeholderText: "4" + text: radiusSmall.toString() + onTextChanged: { + var val = parseInt(text) + if (!isNaN(val) && val >= 0) { + radiusSmallChanged(val) + } + } + } + + CTextField { + Layout.preferredWidth: 100 + label: "Medium (px)" + placeholderText: "8" + text: radiusMedium.toString() + onTextChanged: { + var val = parseInt(text) + if (!isNaN(val) && val >= 0) { + radiusMediumChanged(val) + } + } + } + + CTextField { + Layout.preferredWidth: 100 + label: "Large (px)" + placeholderText: "16" + text: radiusLarge.toString() + onTextChanged: { + var val = parseInt(text) + if (!isNaN(val) && val >= 0) { + radiusLargeChanged(val) + } + } + } + + Item { Layout.fillWidth: true } + + // Radius preview + RowLayout { + spacing: 16 + + Repeater { + model: [ + { label: "Sm", r: radiusSmall }, + { label: "Md", r: radiusMedium }, + { label: "Lg", r: radiusLarge } + ] + + ColumnLayout { + spacing: 4 + + Rectangle { + width: 48 + height: 48 + radius: modelData.r + color: "transparent" + border.width: 2 + border.color: Theme.primary + } + + Text { + text: modelData.label + " (" + modelData.r + "px)" + font.pixelSize: 10 + color: Theme.textSecondary + horizontalAlignment: Text.AlignHCenter + } + } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/ThemeTypography.qml b/qml/MetaBuilder/ThemeTypography.qml new file mode 100644 index 000000000..a8c1249c3 --- /dev/null +++ b/qml/MetaBuilder/ThemeTypography.qml @@ -0,0 +1,94 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: typographyCard + Layout.fillWidth: true + + property string fontFamily: "Inter" + property int baseFontSize: 14 + + signal fontFamilyChanged(string family) + signal baseFontSizeChanged(int size) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 16 + + CText { variant: "h4"; text: "Typography" } + CText { variant: "caption"; text: "Configure font family and base size for the entire interface" } + + CDivider { Layout.fillWidth: true } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + ColumnLayout { + Layout.fillWidth: true + spacing: 8 + + CTextField { + Layout.fillWidth: true + label: "Font Family" + placeholderText: "e.g., Inter, Roboto, system-ui" + text: fontFamily + onTextChanged: fontFamilyChanged(text) + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 8 + + CText { variant: "body2"; text: "Base Font Size: " + baseFontSize + "px" } + + Slider { + Layout.fillWidth: true + from: 10 + to: 24 + stepSize: 1 + value: baseFontSize + onValueChanged: baseFontSizeChanged(value) + + background: Rectangle { + x: parent.leftPadding + y: parent.topPadding + parent.availableHeight / 2 - height / 2 + width: parent.availableWidth + height: 4 + radius: 2 + color: Theme.border + + Rectangle { + width: parent.parent.visualPosition * parent.width + height: parent.height + radius: 2 + color: Theme.primary + } + } + + handle: Rectangle { + x: parent.leftPadding + parent.visualPosition * (parent.availableWidth - width) + y: parent.topPadding + parent.availableHeight / 2 - height / 2 + width: 18 + height: 18 + radius: 9 + color: Theme.primary + border.width: 2 + border.color: Theme.background + } + } + + RowLayout { + spacing: 4 + CText { variant: "caption"; text: "10px" } + Item { Layout.fillWidth: true } + CText { variant: "caption"; text: "24px" } + } + } + } + } +} diff --git a/qml/MetaBuilder/UserFormDialog.qml b/qml/MetaBuilder/UserFormDialog.qml new file mode 100644 index 000000000..4ed2a1846 --- /dev/null +++ b/qml/MetaBuilder/UserFormDialog.qml @@ -0,0 +1,110 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CDialog { + id: root + + property bool isEdit: false + property string formUsername: "" + property string formEmail: "" + property string formPassword: "" + property string formRole: "user" + property bool formActive: true + property var roles: ["user", "admin", "god", "supergod"] + + signal accepted() + signal cancelled() + + title: isEdit ? "Edit User" : "Create User" + + ColumnLayout { + spacing: 14 + width: 380 + + CTextField { + Layout.fillWidth: true + label: "Username" + placeholderText: "Enter username" + text: root.formUsername + onTextChanged: root.formUsername = text + } + + CTextField { + Layout.fillWidth: true + label: "Email" + placeholderText: "Enter email address" + text: root.formEmail + onTextChanged: root.formEmail = text + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + CTextField { + Layout.fillWidth: true + label: "Password" + placeholderText: root.isEdit ? "Leave blank to keep current" : "Enter password" + text: root.formPassword + echoMode: TextInput.Password + onTextChanged: root.formPassword = text + } + + CBadge { text: "SHA-512 hashed"; badgeColor: "#607d8b" } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 6 + CText { variant: "caption"; text: "Role" } + + FlexRow { + Layout.fillWidth: true + spacing: 6 + + Repeater { + model: root.roles + CChip { + text: modelData + selected: root.formRole === modelData + onClicked: root.formRole = modelData + } + } + } + } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + CText { variant: "body2"; text: "Active" } + CSwitch { + checked: root.formActive + onCheckedChanged: root.formActive = checked + } + } + + CDivider { Layout.fillWidth: true } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + Item { Layout.fillWidth: true } + CButton { + text: "Cancel" + variant: "ghost" + onClicked: root.cancelled() + } + CButton { + text: root.isEdit ? "Save Changes" : "Create" + variant: "primary" + enabled: root.isEdit + ? root.formUsername !== "" && root.formEmail !== "" + : root.formUsername !== "" && root.formEmail !== "" && root.formPassword !== "" + onClicked: root.accepted() + } + } + } +} diff --git a/qml/MetaBuilder/UserSearchFilter.qml b/qml/MetaBuilder/UserSearchFilter.qml new file mode 100644 index 000000000..f16e28e91 --- /dev/null +++ b/qml/MetaBuilder/UserSearchFilter.qml @@ -0,0 +1,62 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property string searchText: "" + property string activeRoleFilter: "all" + + signal searchChanged(string text) + signal roleFilterChanged(string role) + + implicitHeight: filterCol.implicitHeight + 32 + + ColumnLayout { + id: filterCol + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + CTextField { + Layout.fillWidth: true + label: "Search" + placeholderText: "Filter by username, email, or role..." + text: root.searchText + onTextChanged: root.searchChanged(text) + } + + FlexRow { + Layout.fillWidth: true + spacing: 8 + + CChip { + text: "All" + selected: root.activeRoleFilter === "all" + onClicked: root.roleFilterChanged("all") + } + CChip { + text: "User" + selected: root.activeRoleFilter === "user" + onClicked: root.roleFilterChanged("user") + } + CChip { + text: "Admin" + selected: root.activeRoleFilter === "admin" + onClicked: root.roleFilterChanged("admin") + } + CChip { + text: "God" + selected: root.activeRoleFilter === "god" + onClicked: root.roleFilterChanged("god") + } + CChip { + text: "SuperGod" + selected: root.activeRoleFilter === "supergod" + onClicked: root.roleFilterChanged("supergod") + } + } + } +} diff --git a/qml/MetaBuilder/UserStatsBar.qml b/qml/MetaBuilder/UserStatsBar.qml new file mode 100644 index 000000000..1134932de --- /dev/null +++ b/qml/MetaBuilder/UserStatsBar.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +FlexRow { + id: root + spacing: 12 + + property int totalUsers: 0 + property int adminCount: 0 + property int godCount: 0 + property int superGodCount: 0 + + CCard { + Layout.fillWidth: true + implicitHeight: col1.implicitHeight + 32 + + ColumnLayout { + id: col1 + anchors.fill: parent + anchors.margins: 16 + spacing: 4 + CText { variant: "caption"; text: "Total Users" } + CText { variant: "h4"; text: String(root.totalUsers) } + } + } + + CCard { + Layout.fillWidth: true + implicitHeight: col2.implicitHeight + 32 + + ColumnLayout { + id: col2 + anchors.fill: parent + anchors.margins: 16 + spacing: 4 + CText { variant: "caption"; text: "Admins" } + CText { variant: "h4"; text: String(root.adminCount) } + } + } + + CCard { + Layout.fillWidth: true + implicitHeight: col3.implicitHeight + 32 + + ColumnLayout { + id: col3 + anchors.fill: parent + anchors.margins: 16 + spacing: 4 + CText { variant: "caption"; text: "Gods" } + CText { variant: "h4"; text: String(root.godCount) } + } + } + + CCard { + Layout.fillWidth: true + implicitHeight: col4.implicitHeight + 32 + + ColumnLayout { + id: col4 + anchors.fill: parent + anchors.margins: 16 + spacing: 4 + CText { variant: "caption"; text: "SuperGods" } + CText { variant: "h4"; text: String(root.superGodCount) } + } + } +} diff --git a/qml/MetaBuilder/UserTable.qml b/qml/MetaBuilder/UserTable.qml new file mode 100644 index 000000000..f50761e95 --- /dev/null +++ b/qml/MetaBuilder/UserTable.qml @@ -0,0 +1,160 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +CCard { + id: root + + property var users: [] + property var allUsers: [] + + signal editClicked(int uid) + signal deleteClicked(int uid) + + function roleColor(role) { + if (role === "supergod") return "#e040fb" + if (role === "god") return "#ff9800" + if (role === "admin") return "#2196f3" + return "#4caf50" + } + + function statusString(s) { return s === "active" ? "success" : "warning" } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 0 + + Rectangle { + Layout.fillWidth: true + height: 40 + color: Theme.surface + radius: 4 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 8 + + CText { variant: "caption"; text: "AVATAR"; Layout.preferredWidth: 56 } + CText { variant: "caption"; text: "USERNAME"; Layout.preferredWidth: 120 } + CText { variant: "caption"; text: "EMAIL"; Layout.fillWidth: true } + CText { variant: "caption"; text: "ROLE"; Layout.preferredWidth: 100 } + CText { variant: "caption"; text: "LEVEL"; Layout.preferredWidth: 50 } + CText { variant: "caption"; text: "STATUS"; Layout.preferredWidth: 90 } + CText { variant: "caption"; text: "CREATED"; Layout.preferredWidth: 100 } + CText { variant: "caption"; text: "ACTIONS"; Layout.preferredWidth: 140 } + } + } + + CDivider { Layout.fillWidth: true } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: root.users + clip: true + spacing: 0 + + delegate: Rectangle { + width: parent ? parent.width : 600 + height: 56 + color: index % 2 === 0 ? "transparent" : Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 0.3) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 8 + + Item { + Layout.preferredWidth: 56 + Layout.preferredHeight: 40 + + CAvatar { + anchors.centerIn: parent + initials: modelData.username.substring(0, 2).toUpperCase() + } + } + + CText { + variant: "body1" + text: modelData.username + Layout.preferredWidth: 120 + elide: Text.ElideRight + } + + CText { + variant: "body2" + text: modelData.email + Layout.fillWidth: true + elide: Text.ElideRight + } + + Item { + Layout.preferredWidth: 100 + Layout.preferredHeight: 40 + + CBadge { + anchors.verticalCenter: parent.verticalCenter + text: modelData.role + badgeColor: roleColor(modelData.role) + } + } + + CText { + variant: "body2" + text: "L" + modelData.level + Layout.preferredWidth: 50 + } + + Item { + Layout.preferredWidth: 90 + Layout.preferredHeight: 40 + + CStatusBadge { + anchors.verticalCenter: parent.verticalCenter + status: statusString(modelData.status) + text: modelData.status + } + } + + CText { + variant: "caption" + text: modelData.created + Layout.preferredWidth: 100 + } + + FlexRow { + Layout.preferredWidth: 140 + spacing: 6 + + CButton { + text: "Edit" + variant: "ghost" + size: "sm" + onClicked: root.editClicked(modelData.uid) + } + CButton { + text: "Delete" + variant: "danger" + size: "sm" + onClicked: root.deleteClicked(modelData.uid) + } + } + } + } + } + + CText { + visible: root.users.length === 0 + Layout.fillWidth: true + Layout.topMargin: 24 + variant: "body2" + text: "No users match the current filter." + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/qml/MetaBuilder/WorkflowNode.qml b/qml/MetaBuilder/WorkflowNode.qml new file mode 100644 index 000000000..84d307732 --- /dev/null +++ b/qml/MetaBuilder/WorkflowNode.qml @@ -0,0 +1,214 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 + +Rectangle { + id: nodeRoot + + property string nodeId: "" + property string nodeName: "Node" + property string nodeType: "action" + property string displayName: "" + property var nodeInputs: [] + property var nodeOutputs: [] + property var parameters: ({}) + property real nodeX: 0 + property real nodeY: 0 + property bool selected: false + property real zoom: 1.0 + + // Port geometry constants + readonly property int portRadius: 6 + readonly property int portSpacing: 24 + readonly property int headerHeight: 32 + readonly property int minWidth: 180 + readonly property int minHeight: headerHeight + Math.max(nodeInputs.length, nodeOutputs.length) * portSpacing + 16 + + signal moved(string id, real newX, real newY) + signal clicked(string id) + signal doubleClicked(string id) + signal portPressed(string nodeId, string portName, string portType, bool isOutput, real globalX, real globalY) + signal portReleased(string nodeId, string portName, string portType, bool isOutput, real globalX, real globalY) + + x: nodeX + y: nodeY + width: minWidth + height: minHeight + radius: 8 + color: selected ? Qt.lighter(Theme.paper, 1.1) : Theme.paper + border.color: selected ? groupColor() : Theme.border + border.width: selected ? 2 : 1 + + layer.enabled: true + layer.effect: null + + function groupColor() { + switch (nodeType.split(".")[0]) { + 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 + } + } + + // Header bar + Rectangle { + id: header + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: headerHeight + radius: 8 + color: groupColor() + + // Square off bottom corners + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: parent.radius + color: parent.color + } + + CText { + anchors.centerIn: parent + text: displayName || nodeName + color: "#FFFFFF" + variant: "body2" + font.bold: true + elide: Text.ElideRight + width: parent.width - 16 + horizontalAlignment: Text.AlignHCenter + } + } + + // Drag handler on the header + DragHandler { + id: dragHandler + target: nodeRoot + onActiveChanged: { + if (!active) { + nodeRoot.moved(nodeId, nodeRoot.x, nodeRoot.y) + } + } + } + + // Click handler + TapHandler { + onTapped: nodeRoot.clicked(nodeId) + onDoubleTapped: nodeRoot.doubleClicked(nodeId) + } + + // Input ports (left side) + Column { + anchors.left: parent.left + anchors.leftMargin: -portRadius + anchors.top: header.bottom + anchors.topMargin: 8 + spacing: portSpacing - portRadius * 2 + + Repeater { + model: nodeInputs + delegate: Item { + width: portRadius * 2 + 60 + height: portRadius * 2 + + Rectangle { + id: inputPort + width: portRadius * 2 + height: portRadius * 2 + radius: portRadius + color: Theme.primary + border.color: Theme.background + border.width: 1 + + MouseArea { + anchors.fill: parent + anchors.margins: -4 + hoverEnabled: true + cursorShape: Qt.CrossCursor + + onPressed: function(mouse) { + var global = inputPort.mapToItem(null, portRadius, portRadius) + nodeRoot.portPressed(nodeId, modelData.name, modelData.type, false, global.x, global.y) + } + onReleased: function(mouse) { + var global = inputPort.mapToItem(null, portRadius, portRadius) + nodeRoot.portReleased(nodeId, modelData.name, modelData.type, false, global.x, global.y) + } + } + } + + CText { + anchors.left: inputPort.right + anchors.leftMargin: 4 + anchors.verticalCenter: inputPort.verticalCenter + text: modelData.displayName || modelData.name || "in" + variant: "caption" + font.pixelSize: 10 + } + } + } + } + + // Output ports (right side) + Column { + anchors.right: parent.right + anchors.rightMargin: -portRadius + anchors.top: header.bottom + anchors.topMargin: 8 + spacing: portSpacing - portRadius * 2 + + Repeater { + model: nodeOutputs + delegate: Item { + width: portRadius * 2 + 60 + height: portRadius * 2 + layoutDirection: Qt.RightToLeft + + CText { + anchors.right: outputPort.left + anchors.rightMargin: 4 + anchors.verticalCenter: outputPort.verticalCenter + text: modelData.displayName || modelData.name || "out" + variant: "caption" + font.pixelSize: 10 + horizontalAlignment: Text.AlignRight + } + + Rectangle { + id: outputPort + anchors.right: parent.right + width: portRadius * 2 + height: portRadius * 2 + radius: portRadius + color: Theme.success + border.color: Theme.background + border.width: 1 + + MouseArea { + anchors.fill: parent + anchors.margins: -4 + hoverEnabled: true + cursorShape: Qt.CrossCursor + + onPressed: function(mouse) { + var global = outputPort.mapToItem(null, portRadius, portRadius) + nodeRoot.portPressed(nodeId, modelData.name, modelData.type, true, global.x, global.y) + } + onReleased: function(mouse) { + var global = outputPort.mapToItem(null, portRadius, portRadius) + nodeRoot.portReleased(nodeId, modelData.name, modelData.type, true, global.x, global.y) + } + } + } + } + } + } +} diff --git a/qml/MetaBuilder/qmldir b/qml/MetaBuilder/qmldir new file mode 100644 index 000000000..0d308b237 --- /dev/null +++ b/qml/MetaBuilder/qmldir @@ -0,0 +1,105 @@ +module MetaBuilder +NavBar 1.0 NavBar.qml +HeroSection 1.0 HeroSection.qml +FeatureCard 1.0 FeatureCard.qml +StatusCard 1.0 StatusCard.qml +ContactForm 1.0 ContactForm.qml +WorkflowNode 1.0 WorkflowNode.qml +CTenantCard 1.0 CTenantCard.qml +CGodUserCard 1.0 CGodUserCard.qml +CTransferCard 1.0 CTransferCard.qml +CSystemMetricCard 1.0 CSystemMetricCard.qml +CUserMenu 1.0 CUserMenu.qml +CNotificationBell 1.0 CNotificationBell.qml +CLanguageSelector 1.0 CLanguageSelector.qml +CNavBar 1.0 CNavBar.qml +CSidebar 1.0 CSidebar.qml +CHeroSection 1.0 CHeroSection.qml +CStatsStrip 1.0 CStatsStrip.qml +CLevelCard 1.0 CLevelCard.qml +CTechCard 1.0 CTechCard.qml +CServiceStatus 1.0 CServiceStatus.qml +CQuickLoginCard 1.0 CQuickLoginCard.qml +CAdminStatsBar 1.0 CAdminStatsBar.qml +CEntitySidebar 1.0 CEntitySidebar.qml +CDataTable 1.0 CDataTable.qml +CTableHeader 1.0 CTableHeader.qml +CTablePagination 1.0 CTablePagination.qml +CDropdownMenu 1.0 CDropdownMenu.qml +CEntityForm 1.0 CEntityForm.qml +CGodPanelHeader 1.0 CGodPanelHeader.qml +CLevelReferenceCard 1.0 CLevelReferenceCard.qml +CConfigStatCard 1.0 CConfigStatCard.qml +CWelcomeCard 1.0 CWelcomeCard.qml +CStatCard 1.0 CStatCard.qml +CActivityList 1.0 CActivityList.qml +CQuickActions 1.0 CQuickActions.qml +CLoginForm 1.0 CLoginForm.qml +CSettingsSection 1.0 CSettingsSection.qml +CThemePicker 1.0 CThemePicker.qml +CConnectionTest 1.0 CConnectionTest.qml +CProfileHeader 1.0 CProfileHeader.qml +CProfileForm 1.0 CProfileForm.qml +CCommentCard 1.0 CCommentCard.qml +CCommentInput 1.0 CCommentInput.qml +CReportCard 1.0 CReportCard.qml +CModActionCard 1.0 CModActionCard.qml +CModStatsRow 1.0 CModStatsRow.qml +CWorkflowToolbar 1.0 CWorkflowToolbar.qml +CNodePalette 1.0 CNodePalette.qml +CNodePropertiesPanel 1.0 CNodePropertiesPanel.qml +CConnectionLayer 1.0 CConnectionLayer.qml +CWorkflowCanvas 1.0 CWorkflowCanvas.qml +DropdownSidebar 1.0 DropdownSidebar.qml +DropdownGeneralForm 1.0 DropdownGeneralForm.qml +DropdownOptionsEditor 1.0 DropdownOptionsEditor.qml +DropdownPreview 1.0 DropdownPreview.qml +UserStatsBar 1.0 UserStatsBar.qml +UserSearchFilter 1.0 UserSearchFilter.qml +UserTable 1.0 UserTable.qml +UserFormDialog 1.0 UserFormDialog.qml +CssClassSidebar 1.0 CssClassSidebar.qml +CssPropertyEditor 1.0 CssPropertyEditor.qml +CssClassPreview 1.0 CssClassPreview.qml +SchemaSidebar 1.0 SchemaSidebar.qml +SchemaFieldsTable 1.0 SchemaFieldsTable.qml +SchemaFieldEditor 1.0 SchemaFieldEditor.qml +MediaJobForm 1.0 MediaJobForm.qml +MediaJobTable 1.0 MediaJobTable.qml +MediaRadioTab 1.0 MediaRadioTab.qml +MediaTvTab 1.0 MediaTvTab.qml +MediaPluginsTab 1.0 MediaPluginsTab.qml +LuaScriptSidebar 1.0 LuaScriptSidebar.qml +LuaCodeEditor 1.0 LuaCodeEditor.qml +LuaPropertiesPanel 1.0 LuaPropertiesPanel.qml +LuaOutputPanel 1.0 LuaOutputPanel.qml +ThemePresetGrid 1.0 ThemePresetGrid.qml +ThemeColorTokens 1.0 ThemeColorTokens.qml +ThemeTypography 1.0 ThemeTypography.qml +ThemeSpacingRadius 1.0 ThemeSpacingRadius.qml +ThemeLivePreview 1.0 ThemeLivePreview.qml +CRouteTableHeader 1.0 CRouteTableHeader.qml +CRouteTableRow 1.0 CRouteTableRow.qml +CRouteEditPanel 1.0 CRouteEditPanel.qml +CAddRouteDialog 1.0 CAddRouteDialog.qml +CDeleteConfirmDialog 1.0 CDeleteConfirmDialog.qml +CDatabaseStatsRow 1.0 CDatabaseStatsRow.qml +CBackendListSidebar 1.0 CBackendListSidebar.qml +CBackendDetailPanel 1.0 CBackendDetailPanel.qml +CAdapterPatternSelector 1.0 CAdapterPatternSelector.qml +CComponentTypeLegend 1.0 CComponentTypeLegend.qml +CComponentTreeRow 1.0 CComponentTreeRow.qml +CComponentPropertiesPanel 1.0 CComponentPropertiesPanel.qml +CNotificationItem 1.0 CNotificationItem.qml +CNotificationEmptyState 1.0 CNotificationEmptyState.qml +CSmtpServerForm 1.0 CSmtpServerForm.qml +CSmtpSenderForm 1.0 CSmtpSenderForm.qml +CSmtpTestEmailForm 1.0 CSmtpTestEmailForm.qml +CSmtpTemplateList 1.0 CSmtpTemplateList.qml +CSmtpTemplateEditor 1.0 CSmtpTemplateEditor.qml +CCanvasGrid 1.0 CCanvasGrid.qml +CCanvasZoomOverlay 1.0 CCanvasZoomOverlay.qml +CNotificationToggles 1.0 CNotificationToggles.qml +CWorkflowNodeDelegate 1.0 CWorkflowNodeDelegate.qml +CWorkflowTestPanel 1.0 CWorkflowTestPanel.qml +CWorkflowSidebar 1.0 CWorkflowSidebar.qml diff --git a/qml/dbal/DBALProvider.qml b/qml/dbal/DBALProvider.qml new file mode 100644 index 000000000..5b230c3a6 --- /dev/null +++ b/qml/dbal/DBALProvider.qml @@ -0,0 +1,165 @@ +import QtQuick + +/** + * QML DBAL Client Component + * + * Provides database operations for QML UI components. + * Wraps the C++ DBALClient for easy QML integration. + * + * Example: + * ```qml + * import "../qmllib/dbal" + * + * DBALProvider { + * id: dbal + * baseUrl: "http://localhost:3001/api/dbal" + * tenantId: "default" + * + * onConnectedChanged: { + * if (connected) { + * loadUsers() + * } + * } + * } + * + * function loadUsers() { + * dbal.list("User", { take: 20 }, function(result) { + * userModel.clear() + * for (var i = 0; i < result.items.length; i++) { + * userModel.append(result.items[i]) + * } + * }) + * } + * ``` + */ +Item { + id: root + + // Configuration — DBAL REST: /api/v1/{tenant}/{package}/{entity}[/{id}] + property string baseUrl: "http://localhost:8080" + property string tenantId: "default" + property string packageId: "core" + property string authToken: "" + + // State + property bool connected: false + property bool loading: false + property string lastError: "" + + // Signals + signal errorOccurred(string message) + signal operationCompleted(string operation, var result) + + // Internal HTTP client (simplified - would use XMLHttpRequest in real impl) + QtObject { + id: internal + + function request(method, endpoint, body, callback) { + root.loading = true + root.lastError = "" + + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + root.loading = false + + if (xhr.status >= 200 && xhr.status < 300) { + try { + var result = JSON.parse(xhr.responseText) + if (callback) callback(result, null) + root.operationCompleted(endpoint, result) + } catch (e) { + var err = "Failed to parse response: " + e.message + root.lastError = err + root.errorOccurred(err) + if (callback) callback(null, err) + } + } else { + var error = xhr.statusText || "Request failed" + root.lastError = error + root.errorOccurred(error) + if (callback) callback(null, error) + } + } + } + + var url = root.baseUrl + endpoint + xhr.open(method, url) + xhr.setRequestHeader("Content-Type", "application/json") + xhr.setRequestHeader("X-Tenant-ID", root.tenantId) + + if (root.authToken) { + xhr.setRequestHeader("Authorization", "Bearer " + root.authToken) + } + + if (body) { + xhr.send(JSON.stringify(body)) + } else { + xhr.send() + } + } + } + + // REST path helpers + function entityPath(entity) { + return "/api/v1/" + tenantId + "/" + packageId + "/" + entity.toLowerCase() + } + + function entityPathWithId(entity, id) { + return entityPath(entity) + "/" + id + } + + // Public API — DBAL REST: /api/v1/{tenant}/{package}/{entity}[/{id}] + + function create(entity, data, callback) { + internal.request("POST", entityPath(entity), data, callback) + } + + function read(entity, id, callback) { + internal.request("GET", entityPathWithId(entity, id), null, callback) + } + + function update(entity, id, data, callback) { + internal.request("PUT", entityPathWithId(entity, id), data, callback) + } + + function remove(entity, id, callback) { + internal.request("DELETE", entityPathWithId(entity, id), null, callback) + } + + function list(entity, options, callback) { + var path = entityPath(entity) + var queryParts = [] + if (options.take !== undefined) queryParts.push("take=" + options.take) + if (options.skip !== undefined) queryParts.push("skip=" + options.skip) + if (options.orderBy !== undefined) queryParts.push("orderBy=" + options.orderBy) + if (queryParts.length > 0) path += "?" + queryParts.join("&") + + internal.request("GET", path, null, callback) + } + + function findFirst(entity, filter, callback) { + var path = entityPath(entity) + "?take=1" + for (var key in filter) { + path += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(filter[key]) + } + internal.request("GET", path, null, callback) + } + + function execute(operation, params, callback) { + var path = "/api/v1/" + tenantId + "/" + operation + internal.request("POST", path, params, callback) + } + + function ping(callback) { + internal.request("GET", "/health", null, function(result, error) { + root.connected = !error + if (callback) callback(!error, error) + }) + } + + // Auto-ping on component ready + Component.onCompleted: { + ping() + } +} diff --git a/qml/dbal/qmldir b/qml/dbal/qmldir new file mode 100644 index 000000000..86edd98f4 --- /dev/null +++ b/qml/dbal/qmldir @@ -0,0 +1,3 @@ +module dbal + +DBALProvider 1.0 DBALProvider.qml diff --git a/frontends/qt6/AdminCrud.js b/qml/qt6/AdminCrud.js similarity index 100% rename from frontends/qt6/AdminCrud.js rename to qml/qt6/AdminCrud.js diff --git a/frontends/qt6/AdminView.qml b/qml/qt6/AdminView.qml similarity index 100% rename from frontends/qt6/AdminView.qml rename to qml/qt6/AdminView.qml diff --git a/frontends/qt6/App.qml b/qml/qt6/App.qml similarity index 100% rename from frontends/qt6/App.qml rename to qml/qt6/App.qml diff --git a/frontends/qt6/CommentsView.qml b/qml/qt6/CommentsView.qml similarity index 100% rename from frontends/qt6/CommentsView.qml rename to qml/qt6/CommentsView.qml diff --git a/frontends/qt6/ComponentHierarchyEditor.qml b/qml/qt6/ComponentHierarchyEditor.qml similarity index 100% rename from frontends/qt6/ComponentHierarchyEditor.qml rename to qml/qt6/ComponentHierarchyEditor.qml diff --git a/frontends/qt6/CssClassManager.qml b/qml/qt6/CssClassManager.qml similarity index 100% rename from frontends/qt6/CssClassManager.qml rename to qml/qt6/CssClassManager.qml diff --git a/frontends/qt6/DashboardView.qml b/qml/qt6/DashboardView.qml similarity index 100% rename from frontends/qt6/DashboardView.qml rename to qml/qt6/DashboardView.qml diff --git a/frontends/qt6/DatabaseManager.qml b/qml/qt6/DatabaseManager.qml similarity index 100% rename from frontends/qt6/DatabaseManager.qml rename to qml/qt6/DatabaseManager.qml diff --git a/frontends/qt6/DropdownConfigManager.qml b/qml/qt6/DropdownConfigManager.qml similarity index 100% rename from frontends/qt6/DropdownConfigManager.qml rename to qml/qt6/DropdownConfigManager.qml diff --git a/frontends/qt6/FrontPage.qml b/qml/qt6/FrontPage.qml similarity index 100% rename from frontends/qt6/FrontPage.qml rename to qml/qt6/FrontPage.qml diff --git a/qml/qt6/GodPanel.qml b/qml/qt6/GodPanel.qml new file mode 100644 index 000000000..365905149 --- /dev/null +++ b/qml/qt6/GodPanel.qml @@ -0,0 +1,227 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 +import MetaBuilder 1.0 +import "config/GodPanelConfig.js" as GodPanelConfig + +Rectangle { + id: godPanel + color: Theme.background + + property int currentTab: 0 + + // Config summary counts (would come from DBAL in production) + property var configCounts: ({ + schemas: 39, workflows: 12, luaScripts: 8, packages: 62, + pages: 27, components: 152, users: 3, snippets: 5, + cssClasses: 44, dropdowns: 16, dbBackends: 14 + }) + + // ── Data loaded from JSON config ── + property var tabModel: [] + property var levelData: [] + property var configStatData: [] + property var tabSources: [] + + // ── MD3-inspired palette ── + readonly property bool isDark: Theme.mode === "dark" + readonly property color accentBlue: "#6366F1" + readonly property color accentCyan: "#06B6D4" + readonly property color accentViolet: "#8B5CF6" + readonly property color accentAmber: "#F59E0B" + readonly property color accentRose: "#F43F5E" + + // 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 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 outline: Theme.border + readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08) + 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) + + // Level accent colors for guide cards + readonly property var levelAccents: [ + "#94A3B8", accentBlue, accentCyan, accentViolet, accentRose + ] + + Component.onCompleted: { + var tabs = GodPanelConfig.loadTabs() + tabModel = tabs + + // Extract source paths for the Loader-based tabs + var sources = [] + for (var i = 0; i < tabs.length; i++) + sources.push(tabs[i].source || "") + tabSources = sources + + levelData = GodPanelConfig.loadLevels() + + var rawStats = GodPanelConfig.loadConfigStats() + var palette = { accentBlue: accentBlue, accentCyan: accentCyan, + accentViolet: accentViolet, accentAmber: accentAmber, + accentRose: accentRose } + configStatData = GodPanelConfig.resolveConfigStats(rawStats, configCounts, palette) + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 24 + spacing: 20 + + // ═══════════════════════════════════════════════════ + // HEADER + // ═══════════════════════════════════════════════════ + CGodPanelHeader { + Layout.fillWidth: true + configCounts: godPanel.configCounts + isDark: godPanel.isDark + onNavigateLevel: function(level) { + if (level === 1) appWindow.currentView = "frontpage" + else if (level === 2) appWindow.currentView = "dashboard" + else if (level === 3) appWindow.currentView = "admin" + } + } + + // ═══════════════════════════════════════════════════ + // TAB BAR + // ═══════════════════════════════════════════════════ + CTabBar { + id: tabBar + Layout.fillWidth: true + currentIndex: currentTab + onCurrentIndexChanged: currentTab = currentIndex + tabs: tabModel + } + + // ═══════════════════════════════════════════════════ + // TAB CONTENT + // ═══════════════════════════════════════════════════ + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: currentTab + + // ── 0 - Guide ── + Rectangle { + color: "transparent" + ScrollView { + anchors.fill: parent + clip: true + contentWidth: availableWidth + + ColumnLayout { + width: parent.width + spacing: 20 + + // Intro section + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: guideIntroCol.implicitHeight + 48 + radius: 16; color: surfaceContainerHigh + border.width: 1; border.color: outlineVariant + + ColumnLayout { + id: guideIntroCol + anchors { left: parent.left; right: parent.right; top: parent.top; margins: 24 } + spacing: 16 + CText { text: "Builder Quick Reference"; font.pixelSize: 22; font.weight: Font.Bold; color: onSurface; Layout.fillWidth: true } + CText { text: "MetaBuilder uses a 5-level permission and interface system. Each level unlocks progressively more powerful tools."; font.pixelSize: 14; wrapMode: Text.Wrap; Layout.fillWidth: true; color: onSurfaceVariant; lineHeight: 1.5 } + } + } + + // Level cards + CText { text: "Access Levels"; font.pixelSize: 18; font.weight: Font.DemiBold; color: onSurface; Layout.fillWidth: true; Layout.topMargin: 4 } + Repeater { + model: levelData + delegate: CLevelReferenceCard { + Layout.fillWidth: true + levelName: modelData.level; role: modelData.role; description: modelData.desc + accent: levelAccents[modelData.accentIndex]; levelNumber: modelData.accentIndex + 1 + isDark: godPanel.isDark + } + } + + // Config summary section + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: configSummaryCol.implicitHeight + 48 + Layout.topMargin: 8; radius: 16; color: surfaceContainerHigh + border.width: 1; border.color: outlineVariant + + ColumnLayout { + id: configSummaryCol + anchors { left: parent.left; right: parent.right; top: parent.top; margins: 24 } + spacing: 16 + CText { text: "Current Configuration"; font.pixelSize: 16; font.weight: Font.DemiBold; color: onSurface; Layout.fillWidth: true } + GridLayout { + Layout.fillWidth: true + columns: Math.max(2, Math.min(4, Math.floor((parent.width + 12) / 180))) + columnSpacing: 12; rowSpacing: 12 + Repeater { + model: configStatData + delegate: CConfigStatCard { + Layout.fillWidth: true + label: modelData.label; value: modelData.value; accent: modelData.accent + isDark: godPanel.isDark + } + } + } + } + } + + CAlert { Layout.fillWidth: true; severity: "info"; text: "Philosophy: 95% JSON config, 5% TypeScript/C++ infrastructure. Entities, workflows, pages, and business logic are all declarative." } + Item { Layout.preferredHeight: 16 } + } + } + } + + // ── Tabs 1-12: data-driven Loader tabs ── + Repeater { + model: 12 + delegate: Rectangle { + color: "transparent" + Loader { + anchors.fill: parent + source: (tabSources.length > index + 1) ? tabSources[index + 1] : "" + } + } + } + + // ── 13 - Settings (Theme + SMTP side by side) ── + Rectangle { + color: "transparent" + ColumnLayout { + anchors.fill: parent; spacing: 20 + CText { text: "System Settings"; font.pixelSize: 22; font.weight: Font.Bold; color: onSurface; Layout.fillWidth: true } + CText { text: "Theme customization and SMTP configuration for outbound email."; font.pixelSize: 14; color: onSurfaceVariant; Layout.fillWidth: true } + RowLayout { + Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16 + Rectangle { + Layout.fillWidth: true; Layout.fillHeight: true; radius: 16 + color: surfaceContainerHigh; border.width: 1; border.color: outlineVariant + ColumnLayout { + anchors.fill: parent; anchors.margins: 20; spacing: 12 + RowLayout { Layout.fillWidth: true; spacing: 10; CText { text: "Theme Editor"; font.pixelSize: 16; font.weight: Font.DemiBold; color: onSurface } CChip { text: "Visual"; variant: "info" } } + Rectangle { Layout.fillWidth: true; height: 1; color: outlineVariant } + Loader { Layout.fillWidth: true; Layout.fillHeight: true; source: "ThemeEditor.qml" } + } + } + Rectangle { + Layout.fillWidth: true; Layout.fillHeight: true; radius: 16 + color: surfaceContainerHigh; border.width: 1; border.color: outlineVariant + ColumnLayout { + anchors.fill: parent; anchors.margins: 20; spacing: 12 + RowLayout { Layout.fillWidth: true; spacing: 10; CText { text: "SMTP Configuration"; font.pixelSize: 16; font.weight: Font.DemiBold; color: onSurface } CChip { text: "Email"; variant: "primary" } } + Rectangle { Layout.fillWidth: true; height: 1; color: outlineVariant } + Loader { Layout.fillWidth: true; Layout.fillHeight: true; source: "SMTPConfigEditor.qml" } + } + } + } + } + } + } + } +} diff --git a/frontends/qt6/LoginView.qml b/qml/qt6/LoginView.qml similarity index 100% rename from frontends/qt6/LoginView.qml rename to qml/qt6/LoginView.qml diff --git a/frontends/qt6/LuaEditor.qml b/qml/qt6/LuaEditor.qml similarity index 100% rename from frontends/qt6/LuaEditor.qml rename to qml/qt6/LuaEditor.qml diff --git a/frontends/qt6/MaterialLanding.qml b/qml/qt6/MaterialLanding.qml similarity index 100% rename from frontends/qt6/MaterialLanding.qml rename to qml/qt6/MaterialLanding.qml diff --git a/frontends/qt6/MediaServicePanel.qml b/qml/qt6/MediaServicePanel.qml similarity index 100% rename from frontends/qt6/MediaServicePanel.qml rename to qml/qt6/MediaServicePanel.qml diff --git a/frontends/qt6/ModPlayerPanel.qml b/qml/qt6/ModPlayerPanel.qml similarity index 100% rename from frontends/qt6/ModPlayerPanel.qml rename to qml/qt6/ModPlayerPanel.qml diff --git a/frontends/qt6/ModeratorView.qml b/qml/qt6/ModeratorView.qml similarity index 100% rename from frontends/qt6/ModeratorView.qml rename to qml/qt6/ModeratorView.qml diff --git a/frontends/qt6/NotificationsPanel.qml b/qml/qt6/NotificationsPanel.qml similarity index 100% rename from frontends/qt6/NotificationsPanel.qml rename to qml/qt6/NotificationsPanel.qml diff --git a/frontends/qt6/PackageManager.qml b/qml/qt6/PackageManager.qml similarity index 100% rename from frontends/qt6/PackageManager.qml rename to qml/qt6/PackageManager.qml diff --git a/frontends/qt6/PackageViewLoader.qml b/qml/qt6/PackageViewLoader.qml similarity index 100% rename from frontends/qt6/PackageViewLoader.qml rename to qml/qt6/PackageViewLoader.qml diff --git a/frontends/qt6/PageRoutesManager.qml b/qml/qt6/PageRoutesManager.qml similarity index 100% rename from frontends/qt6/PageRoutesManager.qml rename to qml/qt6/PageRoutesManager.qml diff --git a/frontends/qt6/ProfileView.qml b/qml/qt6/ProfileView.qml similarity index 100% rename from frontends/qt6/ProfileView.qml rename to qml/qt6/ProfileView.qml diff --git a/frontends/qt6/SMTPConfigEditor.qml b/qml/qt6/SMTPConfigEditor.qml similarity index 100% rename from frontends/qt6/SMTPConfigEditor.qml rename to qml/qt6/SMTPConfigEditor.qml diff --git a/frontends/qt6/SchemaEditor.qml b/qml/qt6/SchemaEditor.qml similarity index 100% rename from frontends/qt6/SchemaEditor.qml rename to qml/qt6/SchemaEditor.qml diff --git a/qml/qt6/SettingsView.qml b/qml/qt6/SettingsView.qml new file mode 100644 index 000000000..935e44323 --- /dev/null +++ b/qml/qt6/SettingsView.qml @@ -0,0 +1,183 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QmlComponents 1.0 +import "qmllib/dbal" +import "qmllib/MetaBuilder" + +Rectangle { + id: root + color: "transparent" + + DBALProvider { id: dbal } + property bool useLiveData: dbal.connected + + // ── JSON config ────────────────────────────────────────────── + function loadJson(path) { + var xhr = new XMLHttpRequest() + xhr.open("GET", Qt.resolvedUrl(path), false); xhr.send() + return xhr.status === 200 ? JSON.parse(xhr.responseText) : null + } + property var notificationConfig: loadJson("config/settings-notifications.json") || [] + property var aboutConfig: loadJson("config/settings-about.json") || [] + property var fontSizeConfig: loadJson("config/settings-font-sizes.json") || [] + + // ── State ──────────────────────────────────────────────────── + property string displayName: appWindow.currentUser + property string userEmail: appWindow.currentUser + "@metabuilder.io" + property bool profileSaved: false + property string selectedTheme: appWindow.currentTheme + property string fontSize: "medium" + property var notifValues: ({ emailNotifications: true, desktopNotifications: true, soundAlerts: false }) + + function userInitials() { + var n = appWindow.currentUser + if (!n || n.length === 0) return "??" + var p = n.split(" ") + return p.length >= 2 ? (p[0][0] + p[1][0]).toUpperCase() : n.substring(0, 2).toUpperCase() + } + + // ── DBAL persistence ───────────────────────────────────────── + function saveProfile() { + var onSaved = function() { profileSaved = true; profileSavedTimer.restart() } + if (useLiveData) { + dbal.update("user", appWindow.currentUser, + { displayName: displayName, email: userEmail }, + function(r, e) { if (!e) onSaved() }) + } else { onSaved(); console.log("[Settings] Profile saved (offline):", displayName, userEmail) } + } + + function savePreferences() { + if (!useLiveData) return + dbal.update("user", appWindow.currentUser, { + preferences: { theme: selectedTheme, fontSize: fontSize, + emailNotifications: notifValues.emailNotifications, + desktopNotifications: notifValues.desktopNotifications, + soundAlerts: notifValues.soundAlerts } + }, function(r, e) { if (!e) console.log("[Settings] Preferences saved to DBAL") }) + } + + function loadPreferences() { + if (!useLiveData) return + dbal.findFirst("user", { username: appWindow.currentUser }, function(result, error) { + if (error || !result) return + var items = result.items || []; if (items.length === 0) return + var u = items[0] + if (u.displayName) displayName = u.displayName + if (u.email) userEmail = u.email + if (u.preferences) { + if (u.preferences.theme) selectedTheme = u.preferences.theme + if (u.preferences.fontSize) fontSize = u.preferences.fontSize + var nv = JSON.parse(JSON.stringify(notifValues)) + if (u.preferences.emailNotifications !== undefined) nv.emailNotifications = u.preferences.emailNotifications + if (u.preferences.desktopNotifications !== undefined) nv.desktopNotifications = u.preferences.desktopNotifications + if (u.preferences.soundAlerts !== undefined) nv.soundAlerts = u.preferences.soundAlerts + notifValues = nv + } + }) + } + + Component.onCompleted: { if (useLiveData) loadPreferences() } + onUseLiveDataChanged: { if (useLiveData) loadPreferences() } + Timer { id: profileSavedTimer; interval: 3000; onTriggered: profileSaved = false } + + // ── UI ─────────────────────────────────────────────────────── + ScrollView { + anchors.fill: parent; anchors.margins: 24; clip: true + ColumnLayout { + width: parent.width; spacing: 20 + + CText { variant: "h3"; text: "Settings" } + + // Profile + CSettingsSection { + title: "Profile" + FlexRow { + Layout.fillWidth: true; Layout.topMargin: 4; spacing: 16 + CAvatar { size: "lg"; initials: userInitials() } + ColumnLayout { + Layout.fillWidth: true; spacing: 4 + CText { variant: "subtitle1"; text: appWindow.currentUser; font.bold: true } + CText { variant: "body2"; text: appWindow.currentRole + " \u00b7 Level " + appWindow.currentLevel; opacity: 0.7 } + } + } + CTextField { Layout.fillWidth: true; Layout.topMargin: 8; label: "Display Name"; placeholderText: "Enter display name"; text: displayName; onTextChanged: displayName = text } + CTextField { Layout.fillWidth: true; label: "Email"; placeholderText: "Enter email address"; text: userEmail; onTextChanged: userEmail = text } + FlexRow { + Layout.fillWidth: true; spacing: 12 + Item { Layout.fillWidth: true } + CAlert { visible: profileSaved; severity: "success"; text: "Profile saved successfully" } + CButton { text: "Save Profile"; variant: "primary"; onClicked: saveProfile() } + } + } + + // Appearance + CSettingsSection { + title: "Appearance" + CThemePicker { + Layout.fillWidth: true; currentTheme: root.selectedTheme + onThemeSelected: function(name) { + root.selectedTheme = name; appWindow.currentTheme = name + if (typeof Theme.setTheme === "function") Theme.setTheme(name) + savePreferences() + } + } + CText { variant: "subtitle2"; text: "Font Size"; Layout.topMargin: 8 } + FlexRow { + Layout.fillWidth: true; spacing: 8 + Repeater { + model: fontSizeConfig + delegate: CButton { + text: modelData.label; variant: fontSize === modelData.id ? "primary" : "default"; size: "sm" + onClicked: { fontSize = modelData.id; savePreferences() } + } + } + } + } + + // Notifications + CSettingsSection { + title: "Notifications" + CNotificationToggles { + Layout.fillWidth: true; model: notificationConfig; values: notifValues + onToggled: function(id, value) { + var nv = JSON.parse(JSON.stringify(notifValues)); nv[id] = value; notifValues = nv; savePreferences() + } + } + } + + // Connection + CSettingsSection { + title: "Connection" + CConnectionTest { Layout.fillWidth: true } + } + + // About + CSettingsSection { + title: "About" + Repeater { + model: aboutConfig + delegate: FlexRow { + Layout.fillWidth: true; spacing: 12 + CText { variant: "body2"; text: modelData.label; opacity: 0.6; Layout.preferredWidth: 120 } + CText { variant: "body1"; text: modelData.value } + } + } + FlexRow { + Layout.fillWidth: true; spacing: 12 + CText { variant: "body2"; text: "Platform"; opacity: 0.6; Layout.preferredWidth: 120 } + CText { variant: "body1"; text: Qt.platform.os } + } + CDivider { Layout.fillWidth: true } + FlexRow { + Layout.fillWidth: true; spacing: 12 + CButton { text: "View Documentation"; variant: "default"; size: "sm"; onClicked: Qt.openUrlExternally("https://github.com/nicholasgriffintn/metabuilder") } + CButton { text: "Report Issue"; variant: "ghost"; size: "sm"; onClicked: Qt.openUrlExternally("https://github.com/nicholasgriffintn/metabuilder/issues") } + } + } + + CText { variant: "caption"; text: useLiveData ? "Connected to DBAL \u2014 preferences synced" : "Offline \u2014 preferences stored locally"; opacity: 0.4 } + Item { Layout.preferredHeight: 20 } + } + } +} diff --git a/frontends/qt6/Storybook.qml b/qml/qt6/Storybook.qml similarity index 100% rename from frontends/qt6/Storybook.qml rename to qml/qt6/Storybook.qml diff --git a/frontends/qt6/SuperGodPanel.qml b/qml/qt6/SuperGodPanel.qml similarity index 100% rename from frontends/qt6/SuperGodPanel.qml rename to qml/qt6/SuperGodPanel.qml diff --git a/frontends/qt6/ThemeEditor.qml b/qml/qt6/ThemeEditor.qml similarity index 100% rename from frontends/qt6/ThemeEditor.qml rename to qml/qt6/ThemeEditor.qml diff --git a/frontends/qt6/UserManagement.qml b/qml/qt6/UserManagement.qml similarity index 100% rename from frontends/qt6/UserManagement.qml rename to qml/qt6/UserManagement.qml diff --git a/frontends/qt6/WorkflowEditor.qml b/qml/qt6/WorkflowEditor.qml similarity index 100% rename from frontends/qt6/WorkflowEditor.qml rename to qml/qt6/WorkflowEditor.qml