feat(qt6): package metadata updates, QmlComponents library enhancements, PackageLoader improvements

- Update 22 package metadata.json files with consistent schema
- Enhance PackageLoader with file watching and navigable packages
- Update QmlComponents library (atoms, core, data-display, feedback, form, layout, surfaces)
- Improve watchtower PackageView and MaterialAccordion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 03:04:31 +00:00
parent e6a2a50ae1
commit 7c5d0f1012
74 changed files with 508 additions and 129 deletions

View File

@@ -64,6 +64,36 @@ ApplicationWindow {
currentView = "frontpage"
}
// ── Static view registry (fixed indices 08) ──
readonly property var staticViews: [
"frontpage", "login", "dashboard", "profile",
"admin", "god-panel", "supergod", "settings", "comments"
]
// ── Dynamic view index computation ──
function viewIndex(view) {
// Check static views first (indices 08)
var staticIdx = staticViews.indexOf(view)
if (staticIdx >= 0)
return staticIdx
// Check dynamic package views (indices 9+)
var navPkgs = PackageLoader.navigablePackages()
for (var i = 0; i < navPkgs.length; i++) {
var pkg = navPkgs[i]
var viewName = pkg.navLabel ? pkg.navLabel.toLowerCase().replace(/ /g, "-") : pkg.packageId
if (viewName === view || pkg.packageId === view)
return staticViews.length + i
}
return 0
}
// Convert packageId to view name for navigation
function packageViewName(pkg) {
return pkg.navLabel ? pkg.navLabel.toLowerCase().replace(/ /g, "-") : pkg.packageId
}
// ── App bar ──
header: CAppBar {
height: 56
@@ -205,22 +235,15 @@ ApplicationWindow {
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: "Forum", view: "forum", icon: "F", level: 2 },
{ label: "Gallery", view: "gallery", icon: "G", level: 2 },
{ label: "Guestbook", view: "guestbook", icon: "B", level: 2 },
{ label: "Blog", view: "blog", icon: "W", level: 2 },
{ label: "Comments", view: "comments", icon: "C", level: 2 },
{ label: "Admin Panel", view: "admin", icon: "A", level: 3 },
{ label: "Analytics", view: "analytics", icon: "A", level: 3 },
{ label: "Watchtower", view: "watchtower", icon: "W", level: 3 },
{ label: "God Panel", view: "god-panel", icon: "G", level: 4 },
{ label: "Packages", view: "packages", icon: "P", level: 4 },
{ label: "Storybook", view: "storybook", icon: "S", level: 4 },
{ label: "Super God", view: "supergod", icon: "S", level: 5 }
]
return items.filter(function(item) { return item.level <= currentLevel })
@@ -235,6 +258,25 @@ ApplicationWindow {
}
}
// 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 <= currentLevel
})
}
delegate: CListItem {
Layout.fillWidth: true
title: modelData.navLabel ? modelData.navLabel : modelData.name
leadingIcon: modelData.icon ? modelData.icon : modelData.name.charAt(0)
selected: currentView === packageViewName(modelData)
onClicked: currentView = packageViewName(modelData)
}
}
Item { Layout.fillHeight: true }
CDivider { Layout.fillWidth: true }
@@ -259,38 +301,28 @@ ApplicationWindow {
anchors.fill: parent
currentIndex: viewIndex(currentView)
// Static views (indices 08)
FrontPage {} // 0: Public landing
LoginView {} // 1: Login
DashboardView {} // 2: Dashboard
ProfileView {} // 3: Profile
PackageViewLoader { packageId: "forum" } // 4
PackageViewLoader { packageId: "gallery" } // 5
PackageViewLoader { packageId: "guestbook" } // 6
PackageViewLoader { packageId: "blog" } // 7
AdminView {} // 8: Admin
PackageViewLoader { packageId: "analytics" } // 9
PackageViewLoader { packageId: "watchtower" } // 10
GodPanel {} // 11: God Panel (13-tab builder)
PackageManager {} // 12: Package Manager
Storybook {} // 13: Storybook
SuperGodPanel {} // 14: Super God Panel
SettingsView {} // 15: Settings
CommentsView {} // 16: Comments
AdminView {} // 4: Admin
GodPanel {} // 5: God Panel (13-tab builder)
SuperGodPanel {} // 6: Super God Panel
SettingsView {} // 7: Settings
CommentsView {} // 8: Comments
// Dynamic package views (indices 9+)
Repeater {
model: PackageLoader.navigablePackages()
delegate: PackageViewLoader {
packageId: modelData.packageId
}
}
}
}
}
function viewIndex(view) {
var views = [
"frontpage", "login", "dashboard", "profile", "forum",
"gallery", "guestbook", "blog", "admin", "analytics",
"watchtower", "god-panel", "packages", "storybook",
"supergod", "settings", "comments"
]
var idx = views.indexOf(view)
return idx >= 0 ? idx : 0
}
// ── Window state persistence ──
Settings {
id: windowSettings

View File

@@ -6,33 +6,18 @@ import QmlComponents 1.0
Rectangle {
color: Theme.background
property var repositories: [
{ name: "Official", url: "https://repo.metabuilder.dev", description: "Curated MetaBuilder toolkit", status: "online" },
{ name: "Community", url: "https://community.metabuilder.dev", description: "Community-contributed adapters", status: "online" },
{ name: "Local", url: "file://packages/local", description: "Local drafts", status: "offline" }
]
property var packages: [
{ id: "material_ui", name: "Material UI Kit", repo: "Official", version: "2.1.0", description: "Shared Material components for Qt.", installed: true, size: "4.1 MB" },
{ id: "db_connector", name: "Qt DB Connector", repo: "Community", version: "1.4.2", description: "Live DBAL observability widgets.", installed: false, size: "2.7 MB" },
{ id: "prisma_console", name: "Prisma Console", repo: "Official", version: "0.9.0", description: "Prisma schema preview and migrations view.", installed: false, size: "3.2 MB" },
{ id: "storybook_themes", name: "Storybook Themes", repo: "Local", version: "1.0.3", description: "Additional Storybook scenes + theming", installed: true, size: "1.8 MB" },
{ id: "telemetry", name: "Telemetry Metrics", repo: "Community", version: "0.4.1", description: "CPU/RAM/Latency dashboards for daemons.", installed: false, size: "5.6 MB" }
]
property int selectedRepoIndex: 0
property string searchText: ""
function togglePackage(name, install) {
packages = packages.map(function(pkg) {
return pkg.name === name ? Object.assign({}, pkg, { installed: install }) : pkg
})
}
property string selectedPackageId: ""
function filteredPackages() {
var currentRepo = repositories[selectedRepoIndex].name
return packages.filter(function(pkg) {
return pkg.repo === currentRepo && pkg.name.toLowerCase().indexOf(searchText.toLowerCase()) !== -1
var allPkgs = PackageLoader.packages
if (searchText === "")
return allPkgs
var lower = searchText.toLowerCase()
return allPkgs.filter(function(pkg) {
return pkg.name.toLowerCase().indexOf(lower) !== -1
|| pkg.packageId.toLowerCase().indexOf(lower) !== -1
|| (pkg.description && pkg.description.toLowerCase().indexOf(lower) !== -1)
})
}
@@ -46,8 +31,8 @@ Rectangle {
spacing: 12
CText { variant: "h3"; text: "Package Manager" }
CStatusBadge {
status: repositories[selectedRepoIndex].status === "online" ? "success" : "warning"
text: repositories[selectedRepoIndex].status
status: "success"
text: PackageLoader.packageCount + " packages"
}
}
@@ -56,7 +41,7 @@ Rectangle {
Layout.fillHeight: true
spacing: 16
// Repo sidebar
// Package details sidebar
CCard {
Layout.preferredWidth: 320
Layout.fillHeight: true
@@ -66,25 +51,90 @@ Rectangle {
anchors.margins: 16
spacing: 12
CText { variant: "subtitle1"; text: "Repositories" }
CText { variant: "subtitle1"; text: "Package Details" }
ListView {
// Show details for selected package
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
model: repositories
spacing: 6
delegate: CListItem {
width: parent ? parent.width : 280
title: modelData.name
subtitle: modelData.description
selected: index === selectedRepoIndex
onClicked: selectedRepoIndex = index
color: "transparent"
visible: selectedPackageId !== ""
ColumnLayout {
anchors.fill: parent
spacing: 8
CText {
variant: "h5"
text: {
var pkg = PackageLoader.getPackage(selectedPackageId)
return pkg ? pkg.name : ""
}
}
CText {
variant: "body2"
wrapMode: Text.Wrap
Layout.fillWidth: true
text: {
var pkg = PackageLoader.getPackage(selectedPackageId)
return pkg ? pkg.description : ""
}
}
CDivider { Layout.fillWidth: true }
CText { variant: "caption"; text: "Version" }
CText {
variant: "body2"
text: {
var pkg = PackageLoader.getPackage(selectedPackageId)
return pkg ? pkg.version : ""
}
}
CText { variant: "caption"; text: "Category" }
CBadge {
text: {
var pkg = PackageLoader.getPackage(selectedPackageId)
return pkg && pkg.category ? pkg.category : "—"
}
}
CText { variant: "caption"; text: "Dependencies" }
CText {
variant: "body2"
wrapMode: Text.Wrap
Layout.fillWidth: true
text: {
var deps = PackageLoader.resolveDependencies(selectedPackageId)
return deps.length > 0 ? deps.join(", ") : "None"
}
}
Item { Layout.fillHeight: true }
}
}
// Placeholder when nothing selected
CText {
visible: selectedPackageId === ""
variant: "body2"
text: "Select a package to view details"
Layout.fillWidth: true
wrapMode: Text.Wrap
}
Item { Layout.fillHeight: true }
CDivider { Layout.fillWidth: true }
CButton { text: "Add repository"; variant: "primary"; Layout.fillWidth: true }
CButton { text: "Refresh metadata"; variant: "ghost"; Layout.fillWidth: true }
CButton {
text: "Rescan packages"
variant: "ghost"
Layout.fillWidth: true
onClicked: PackageLoader.scan()
}
}
}
@@ -107,7 +157,10 @@ Rectangle {
onTextChanged: searchText = text
Layout.fillWidth: true
}
CButton { text: "Install from archive"; variant: "ghost" }
CText {
variant: "caption"
text: filteredPackages().length + " / " + PackageLoader.packageCount
}
}
ListView {
@@ -115,6 +168,7 @@ Rectangle {
Layout.fillHeight: true
model: filteredPackages()
spacing: 8
clip: true
delegate: CCard {
width: parent ? parent.width : 400
variant: "outlined"
@@ -124,12 +178,38 @@ Rectangle {
anchors.margins: 16
spacing: 16
// Package icon badge
Rectangle {
width: 40
height: 40
radius: 8
color: modelData.installed ? Theme.primary : Theme.border
Layout.alignment: Qt.AlignVCenter
CText {
anchors.centerIn: parent
text: modelData.icon ? modelData.icon : modelData.name.charAt(0)
variant: "subtitle1"
color: modelData.installed ? "#ffffff" : Theme.textPrimary
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
CText { variant: "subtitle1"; text: modelData.name }
CText { variant: "body2"; text: modelData.description; wrapMode: Text.Wrap }
CText { variant: "caption"; text: "v" + modelData.version + " \u00b7 " + modelData.size }
FlexRow {
spacing: 8
CBadge { text: "v" + modelData.version }
CBadge {
text: modelData.category ? modelData.category : "—"
visible: modelData.category !== undefined
}
CBadge {
text: "Level " + (modelData.level ? modelData.level : 2)
}
}
}
ColumnLayout {
@@ -139,18 +219,20 @@ Rectangle {
variant: modelData.installed ? "default" : "primary"
enabled: !modelData.installed
size: "sm"
onClicked: {
if (PackageRegistry.loadPackage(modelData.id)) {
togglePackage(modelData.name, true)
}
}
onClicked: PackageLoader.install(modelData.packageId)
}
CButton {
text: "Uninstall"
variant: "danger"
size: "sm"
enabled: modelData.installed
onClicked: togglePackage(modelData.name, false)
onClicked: PackageLoader.uninstall(modelData.packageId)
}
CButton {
text: "Details"
variant: "ghost"
size: "sm"
onClicked: selectedPackageId = modelData.packageId
}
}
}

View File

@@ -9,11 +9,11 @@ Rectangle {
property string packageId: ""
// Try to load the package's QML view from disk
// Resolve view URL via PackageLoader (disk → QRC fallback)
Loader {
id: viewLoader
anchors.fill: parent
source: resolvePackageView()
source: packageId !== "" ? PackageLoader.qmlPathUrl(packageId) : ""
onStatusChanged: {
if (status === Loader.Error) {
console.warn("PackageViewLoader: failed to load", packageId, source)
@@ -21,6 +21,18 @@ Rectangle {
}
}
// Hot-reload: react to QML file changes on disk
Connections {
target: PackageLoader
function onPackageUpdated(id) {
if (id === packageId) {
var url = PackageLoader.qmlPathUrl(packageId)
viewLoader.source = ""
viewLoader.source = url
}
}
}
// Fallback when view can't be loaded
Rectangle {
anchors.fill: parent
@@ -40,7 +52,7 @@ Rectangle {
CText {
variant: "body1"
text: {
var meta = PackageRegistry.metadata(packageId)
var meta = PackageLoader.getPackage(packageId)
return meta && meta.description ? meta.description : "Package view for " + packageId
}
Layout.alignment: Qt.AlignHCenter
@@ -56,26 +68,15 @@ Rectangle {
variant: "primary"
Layout.alignment: Qt.AlignHCenter
onClicked: {
if (PackageRegistry.loadPackage(packageId)) {
console.log("Loaded package:", packageId)
}
PackageLoader.install(packageId)
console.log("Installed package:", packageId)
}
}
}
}
function resolvePackageView() {
// Try to find the PackageView.qml from package directories
var paths = [
"packages/" + packageId + "/PackageView.qml",
"../packages/" + packageId + "/PackageView.qml"
]
// Return first candidate; QML Loader handles missing gracefully
return paths[0]
}
function formatTitle(id) {
return id.split("-").map(function(w) {
return id.split("_").map(function(w) {
return w.charAt(0).toUpperCase() + w.slice(1)
}).join(" ")
}

View File

@@ -3,5 +3,10 @@
"name": "Analytics Studio",
"version": "1.0.0",
"description": "Realtime dashboards for usage, telemetry, and forum health.",
"dependencies": ["god_panel", "blog"]
"dependencies": ["god_panel", "blog"],
"level": 3,
"category": "admin",
"icon": "A",
"navLabel": "Analytics",
"showInNav": true
}

View File

@@ -3,5 +3,10 @@
"name": "Blog",
"version": "1.0.0",
"description": "Storytelling hub with author profiles and read-later tagging.",
"dependencies": ["profile_page"]
"dependencies": ["profile_page"],
"level": 2,
"category": "social",
"icon": "W",
"navLabel": "Blog",
"showInNav": true
}

View File

@@ -3,5 +3,10 @@
"name": "Breakout",
"version": "1.0.0",
"description": "Brick-breaking arcade experience that shares assets with the Retro Arcade package.",
"dependencies": ["retro_games", "gallery"]
"dependencies": ["retro_games", "gallery"],
"level": 2,
"category": "games",
"icon": "B",
"navLabel": "Breakout",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Connection Hub",
"version": "1.0.0",
"description": "Social graph with events, groups, and shared media.",
"dependencies": ["profile_page", "gallery"]
"dependencies": ["profile_page", "gallery"],
"level": 2,
"category": "social",
"icon": "C",
"navLabel": "Connection Hub",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Escape Room",
"version": "1.0.0",
"description": "Puzzle-driven escape room within the Qt UI for team-building.",
"dependencies": ["retro_games", "microthread"]
"dependencies": ["retro_games", "microthread"],
"level": 2,
"category": "games",
"icon": "E",
"navLabel": "Escape Room",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Community Forum",
"version": "1.0.0",
"description": "Threaded discussions with tagging and moderation tools.",
"dependencies": ["profile_page", "guestbook"]
"dependencies": ["profile_page", "guestbook"],
"level": 2,
"category": "social",
"icon": "F",
"navLabel": "Forum",
"showInNav": true
}

View File

@@ -3,5 +3,10 @@
"name": "Frontpage Experience",
"version": "1.0.0",
"description": "Public landing page that stitches the hero, features, and CTA across all Qt experiences.",
"dependencies": ["login", "gallery", "blog"]
"dependencies": ["login", "gallery", "blog"],
"level": 1,
"category": "core",
"icon": "H",
"navLabel": "Frontpage",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Gallery",
"version": "1.0.0",
"description": "Media grid with lightbox, filters, and curated collections.",
"dependencies": ["frontpage"]
"dependencies": ["frontpage"],
"level": 2,
"category": "media",
"icon": "G",
"navLabel": "Gallery",
"showInNav": true
}

View File

@@ -3,5 +3,10 @@
"name": "God Panel",
"version": "1.0.0",
"description": "Power user console with server toggles, credentials, and runtime health.",
"dependencies": ["login", "user_settings"]
"dependencies": ["login", "user_settings"],
"level": 4,
"category": "admin",
"icon": "G",
"navLabel": "God Panel",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Guestbook",
"version": "1.0.0",
"description": "Simple contributions feed for visitors to leave messages.",
"dependencies": []
"dependencies": [],
"level": 2,
"category": "social",
"icon": "B",
"navLabel": "Guestbook",
"showInNav": true
}

View File

@@ -3,5 +3,10 @@
"name": "Login Shell",
"version": "1.0.0",
"description": "Authentication gate with credential helpers and shortcuts to God-level panels.",
"dependencies": []
"dependencies": [],
"level": 1,
"category": "core",
"icon": "L",
"navLabel": "Login",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Marketplace",
"version": "1.0.0",
"description": "Curated extensions and paid components with purchase-to-install workflow.",
"dependencies": ["frontpage", "storybook"]
"dependencies": ["frontpage", "storybook"],
"level": 2,
"category": "commerce",
"icon": "M",
"navLabel": "Marketplace",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "MicroThread",
"version": "1.0.0",
"description": "Rapid-fire posting feed, like a micro-blogging stream.",
"dependencies": ["login", "profile_page"]
"dependencies": ["login", "profile_page"],
"level": 2,
"category": "social",
"icon": "T",
"navLabel": "MicroThread",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Music Player",
"version": "1.0.0",
"description": "Ambient audio player with playlists, scrobbling, and visualizers.",
"dependencies": ["gallery", "analytics"]
"dependencies": ["gallery", "analytics"],
"level": 2,
"category": "media",
"icon": "M",
"navLabel": "Music Player",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Package Manager",
"version": "1.0.0",
"description": "Ubuntu Store style manager that hosts repositories and installer UI.",
"dependencies": ["storybook", "frontpage", "analytics"]
"dependencies": ["storybook", "frontpage", "analytics"],
"level": 4,
"category": "dev",
"icon": "P",
"navLabel": "Packages",
"showInNav": true
}

View File

@@ -3,5 +3,10 @@
"name": "Profile Page",
"version": "1.0.0",
"description": "User profile, activity feed, and quick links to social mini-apps.",
"dependencies": ["login", "user_settings"]
"dependencies": ["login", "user_settings"],
"level": 2,
"category": "core",
"icon": "P",
"navLabel": "Profile",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Retro Arcade",
"version": "1.0.0",
"description": "Pixel-perfect retro games to keep visitors entertained inside the app.",
"dependencies": ["gallery"]
"dependencies": ["gallery"],
"level": 2,
"category": "games",
"icon": "R",
"navLabel": "Retro Arcade",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Snake Game",
"version": "1.0.0",
"description": "Classic snake action that can be embedded into any launcher view.",
"dependencies": ["retro_games"]
"dependencies": ["retro_games"],
"level": 2,
"category": "games",
"icon": "S",
"navLabel": "Snake Game",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "Storybook Showcase",
"version": "1.0.0",
"description": "Interactive catalog of all Material components, used for QA and design feedback.",
"dependencies": ["login", "frontpage"]
"dependencies": ["login", "frontpage"],
"level": 4,
"category": "dev",
"icon": "S",
"navLabel": "Storybook",
"showInNav": true
}

View File

@@ -3,5 +3,10 @@
"name": "SuperGod Panel",
"version": "1.0.0",
"description": "Overlord console that orchestrates daemons, migrations, and meta workflows.",
"dependencies": ["god_panel", "retro_games"]
"dependencies": ["god_panel", "retro_games"],
"level": 5,
"category": "admin",
"icon": "S",
"navLabel": "Super God",
"showInNav": false
}

View File

@@ -3,5 +3,10 @@
"name": "User Settings",
"version": "1.0.0",
"description": "Personalization controls for notifications, themes, and privacy knobs.",
"dependencies": ["login"]
"dependencies": ["login"],
"level": 2,
"category": "core",
"icon": "U",
"navLabel": "Settings",
"showInNav": false
}

View File

@@ -36,7 +36,7 @@ Rectangle {
ListElement { severity: "info"; message: "System startup complete — all services initialized"; timestamp: "07:40:00" }
ListElement { severity: "error"; message: "Failed health check on Postgres (timeout), auto-retried OK"; timestamp: "07:38:45" }
ListElement { severity: "warning"; message: "Memory usage spike to 78% during backup (normalized)"; timestamp: "06:30:10" }
]
}
Timer {
id: refreshTimer

View File

@@ -3,5 +3,10 @@
"name": "Watchtower",
"version": "1.0.0",
"description": "Mission control for logging, alerts, and daemon orchestration.",
"dependencies": ["analytics", "god_panel"]
"dependencies": ["analytics", "god_panel"],
"level": 3,
"category": "admin",
"icon": "W",
"navLabel": "Watchtower",
"showInNav": true
}

View File

@@ -66,4 +66,3 @@ Rectangle {
default property alias content: contentLoader.sourceComponent
}
}

View File

@@ -5,6 +5,7 @@
#include <QJsonDocument>
#include <QJsonArray>
#include <QDebug>
#include <algorithm>
PackageLoader::PackageLoader(QObject *parent)
: QObject(parent)
@@ -72,6 +73,10 @@ void PackageLoader::setWatching(bool enabled)
const QString metaPath = pkgDir + QStringLiteral("/metadata.json");
if (QFile::exists(metaPath))
m_watcher->addPath(metaPath);
const QString qmlViewPath = pkgDir + QStringLiteral("/PackageView.qml");
if (QFile::exists(qmlViewPath))
m_watcher->addPath(qmlViewPath);
}
} else {
delete m_watcher;
@@ -101,6 +106,7 @@ void PackageLoader::scan()
loadPackage(root.absoluteFilePath(entry));
}
loadInstallState();
emit packagesChanged();
}
@@ -157,6 +163,7 @@ void PackageLoader::install(const QString &packageId)
return;
}
m_installed[packageId] = true;
saveInstallState();
emit packageInstalled(packageId);
emit packagesChanged();
}
@@ -166,6 +173,7 @@ void PackageLoader::uninstall(const QString &packageId)
if (!m_installed.contains(packageId))
return;
m_installed.remove(packageId);
saveInstallState();
emit packageUninstalled(packageId);
emit packagesChanged();
}
@@ -227,6 +235,94 @@ QString PackageLoader::qmlPath(const QString &packageId) const
return dir + QStringLiteral("/PackageView.qml");
}
QUrl PackageLoader::qmlPathUrl(const QString &packageId) const
{
if (!m_packages.contains(packageId))
return {};
// Try disk path first (dev mode)
const QString diskPath = qmlPath(packageId);
if (QFile::exists(diskPath))
return QUrl::fromLocalFile(diskPath);
// Fall back to QRC for bundled builds
const QString qrcPath = QStringLiteral("qrc:/packages/") + packageId + QStringLiteral("/PackageView.qml");
return QUrl(qrcPath);
}
QVariantList PackageLoader::navigablePackages() const
{
QVariantList result;
for (auto it = m_packages.constBegin(); it != m_packages.constEnd(); ++it) {
const QJsonObject &meta = it.value();
if (meta.value(QStringLiteral("showInNav")).toBool(false)) {
QVariantMap entry = meta.toVariantMap();
entry[QStringLiteral("installed")] = m_installed.value(it.key(), false);
result.append(entry);
}
}
// Sort by level (ascending) then by name (alphabetical)
std::sort(result.begin(), result.end(), [](const QVariant &a, const QVariant &b) {
const QVariantMap ma = a.toMap();
const QVariantMap mb = b.toMap();
const int levelA = ma.value(QStringLiteral("level"), 2).toInt();
const int levelB = mb.value(QStringLiteral("level"), 2).toInt();
if (levelA != levelB)
return levelA < levelB;
return ma.value(QStringLiteral("name")).toString() < mb.value(QStringLiteral("name")).toString();
});
return result;
}
// ---------------------------------------------------------------------------
// Persistent install state
// ---------------------------------------------------------------------------
void PackageLoader::loadInstallState()
{
const QString path = m_packagesDir + QStringLiteral("/installed.json");
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return;
QJsonParseError parseErr;
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseErr);
file.close();
if (parseErr.error != QJsonParseError::NoError) {
qWarning() << "PackageLoader: failed to parse installed.json:" << parseErr.errorString();
return;
}
m_installed.clear();
const QJsonObject obj = doc.object();
for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) {
if (it.value().toBool())
m_installed[it.key()] = true;
}
}
void PackageLoader::saveInstallState() const
{
const QString path = m_packagesDir + QStringLiteral("/installed.json");
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "PackageLoader: failed to write installed.json:" << path;
return;
}
QJsonObject obj;
for (auto it = m_installed.constBegin(); it != m_installed.constEnd(); ++it) {
if (it.value())
obj[it.key()] = true;
}
file.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
file.close();
}
// ---------------------------------------------------------------------------
// File system watcher slots
// ---------------------------------------------------------------------------
@@ -262,18 +358,25 @@ void PackageLoader::onFileChanged(const QString &path)
{
emit fileChanged(path);
// Re-read the metadata.json that was modified
QFileInfo fi(path);
const QString pkgDir = fi.absolutePath();
loadPackage(pkgDir);
const bool isQmlFile = path.endsWith(QStringLiteral(".qml"));
if (!isQmlFile) {
// Re-read the metadata.json that was modified
loadPackage(pkgDir);
}
// Emit packageUpdated for both metadata and QML file changes
for (auto it = m_packages.constBegin(); it != m_packages.constEnd(); ++it) {
if (it.value().value(QStringLiteral("_dir")).toString() == pkgDir) {
emit packageUpdated(it.key());
break;
}
}
emit packagesChanged();
if (!isQmlFile)
emit packagesChanged();
// QFileSystemWatcher may drop the watch after a file change re-add
if (m_watcher && QFile::exists(path) && !m_watcher->files().contains(path))

View File

@@ -8,6 +8,7 @@
#include <QStringList>
#include <QFileSystemWatcher>
#include <QMap>
#include <QUrl>
#include <QVariantList>
class PackageLoader : public QObject
@@ -38,6 +39,8 @@ public slots:
QVariantMap getPackage(const QString &packageId) const;
QStringList resolveDependencies(const QString &packageId) const;
QString qmlPath(const QString &packageId) const;
QUrl qmlPathUrl(const QString &packageId) const;
QVariantList navigablePackages() const;
signals:
void packagesChanged();
@@ -56,6 +59,8 @@ private:
void loadPackage(const QString &dir);
bool validateMetadata(const QJsonObject &metadata) const;
QStringList resolveDepChain(const QString &packageId, QStringList &visited) const;
void loadInstallState();
void saveInstallState() const;
QString m_packagesDir;
bool m_watching;

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CBlockquote.qml - Blockquote (mirrors _blockquote.scss)

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CCodeBlock.qml - Code block display (mirrors _code-block.scss)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CCodeInline.qml - Inline code (mirrors _code-inline.scss)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CHighlight.qml - Text highlighting (mirrors _highlight.scss)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CMarkdown.qml - Markdown text display (mirrors _markdown.scss)

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import QmlComponents 1.0
/**
* CPanel.qml - Panel component (mirrors _panel.scss)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CProse.qml - Long-form text container (mirrors _prose.scss)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CSection.qml - Content section (mirrors _section.scss)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CText.qml - Styled text component (mirrors SCSS text utilities)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CTitle.qml - Title text (mirrors _title.scss)

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CButton.qml - Styled button component (mirrors _button.scss)

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QmlComponents 1.0
/**
* CCard.qml - Card container component (mirrors _card.scss)

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CChip.qml - Chip/tag component (mirrors _chip.scss)
@@ -15,6 +16,7 @@ Rectangle {
property string size: "sm" // sm, md
property bool clickable: false
property bool closable: false
property bool checked: false
signal clicked()
signal closeClicked()

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CFab.qml - Floating action button

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CIcon.qml - Icon container (mirrors _icon.scss)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QmlComponents 1.0
Item {
id: control

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CAvatar.qml - Circular avatar (mirrors _avatar.scss)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CBadge.qml - Notification badge (mirrors _badge.scss)
@@ -11,6 +12,7 @@ Rectangle {
property string variant: "primary" // primary, success, warning, error
property int count: 0 // Number to display (0 = dot only)
property bool dot: false // Show as dot without number
property string text: "" // Direct text label (overrides count)
// Size mapping
readonly property var _sizes: ({
@@ -34,6 +36,7 @@ Rectangle {
readonly property color _textColor: variant === "warning" ? "#000000" : "#ffffff"
// Sizing
readonly property bool _hasText: text !== ""
width: dot ? _sizeConfig.height : Math.max(_sizeConfig.minWidth, label.implicitWidth + _sizeConfig.padding * 2)
height: _sizeConfig.height
radius: height / 2
@@ -43,10 +46,10 @@ Rectangle {
Text {
id: label
anchors.centerIn: parent
text: root.count > 99 ? "99+" : root.count.toString()
text: root._hasText ? root.text : (root.count > 99 ? "99+" : root.count.toString())
color: root._textColor
font.pixelSize: root._sizeConfig.fontSize
font.weight: Font.DemiBold
visible: !root.dot && root.count > 0
visible: !root.dot && (root._hasText || root.count > 0)
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CDivider.qml - Divider/separator component (mirrors _divider.scss)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CStatBadge.qml - Statistic badge (mirrors _stat-badge.scss)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CTable.qml - Data table (mirrors _table.scss)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CAlert.qml - Alert/notification component (mirrors _alert.scss)

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CDialog.qml - Modal dialog (mirrors _dialog.scss)
@@ -12,7 +13,7 @@ Popup {
property string title: ""
property string size: "md" // sm, md, lg, xl
property bool showClose: true
property alias contentItem: contentArea.data
property alias dialogContent: contentArea.data
property alias footerItem: footerArea.data
// Size mapping

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CErrorState.qml - Error state display (mirrors _error-state.scss)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CProgress.qml - Progress indicator (mirrors _progress.scss)

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import QmlComponents 1.0
/**
* CSnackbar.qml - Snackbar/toast notification (mirrors _snackbar.scss)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CSpinner.qml - Loading spinner (mirrors _spinner.scss)

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CAutocomplete.qml - simple autocomplete input with popup suggestions

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QmlComponents 1.0
/**
* CCheckbox.qml - styled checkbox wrapper

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CFormGroup.qml - Form field container (mirrors _form.scss)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QmlComponents 1.0
Text {
id: label

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CLabel.qml - Form label (mirrors _label.scss)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CRadio.qml - radio control

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CRating.qml - simple star rating control

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
/**
* CSelect.qml - simple select (ComboBox wrapper)
@@ -7,7 +8,5 @@ import QtQuick.Controls
ComboBox {
id: root
model: []
property alias currentIndex: root.currentIndex
property alias currentText: root.currentText
Layout.preferredWidth: 200
}

View File

@@ -3,5 +3,4 @@ import QtQuick.Controls
Switch {
id: sw
property alias checked: sw.checked
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CBox.qml - Generic container component (mirrors common SCSS patterns)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* CContainer.qml - Responsive container with max-width (mirrors CSS container)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CGrid.qml - Responsive grid layout (mirrors _grid.scss)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* FlexCol.qml - Vertical flex container (mirrors SCSS .flex, .flex-col utilities)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* FlexRow.qml - Horizontal flex container (mirrors SCSS .flex, .flex-row utilities)

View File

@@ -1,4 +1,5 @@
import QtQuick
import QmlComponents 1.0
/**
* Spacer.qml - Flexible spacer utility (mirrors SCSS spacing utilities)

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QmlComponents 1.0
/**
* CAccordionItem.qml - Single accordion item

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ToolBar {
id: appbar

View File

@@ -112,6 +112,10 @@ CUseMediaQuery 1.0 components/utils/CUseMediaQuery.qml
CPopover 1.0 components/utils/CPopover.qml
# Theming Components
singleton Theme 1.0 components/theming/Theme.qml
singleton StyleVariables 1.0 components/theming/StyleVariables.qml
singleton StyleMixins 1.0 components/theming/StyleMixins.qml
singleton Responsive 1.0 components/theming/Responsive.qml
CThemeProvider 1.0 components/theming/CThemeProvider.qml
CStyled 1.0 components/theming/CStyled.qml