Loading editor/qml/DocumentTree.qml +1 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ ColumnLayout { property var treeData: null function reload() { selectedUuid = "" var json = localApi.getDocTree() var resp = JSON.parse(json) if (resp.status === "success") Loading editor/qml/HtmlEditorField.qml +264 −31 Original line number Diff line number Diff line Loading @@ -8,26 +8,234 @@ ColumnLayout { signal textChangedByUser(string newText) property bool rawMode: false property string _storedHtml: "" property bool _switching: false onRawModeChanged: { _switching = true if (rawMode) { /* RichText → Raw: extract and clean Qt artifacts */ _storedHtml = stripQtArtifacts(editorArea.text) editorArea.textFormat = TextEdit.PlainText editorArea.text = _storedHtml } else { /* Raw → RichText: take raw text as HTML source */ _storedHtml = editorArea.text editorArea.textFormat = TextEdit.RichText editorArea.text = _storedHtml } _switching = false } function stripQtArtifacts(html) { var s = html /* Remove DOCTYPE, html/head/body wrappers */ s = s.replace(/<!DOCTYPE[^>]*>/gi, "") s = s.replace(/<\/?html[^>]*>/gi, "") s = s.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, "") s = s.replace(/<\/?body[^>]*>/gi, "") /* Remove Qt paragraph style artifacts */ s = s.replace(/\s*style\s*=\s*"\s*-qt-[^"]*"/gi, "") /* Remove empty style attributes left over */ s = s.replace(/\s+style\s*=\s*"[\s;]*"/gi, "") /* Remove font-family / font-size from inline styles (Qt injects system font) */ s = s.replace(/\s*font-family\s*:\s*'[^']*'\s*;?/gi, "") s = s.replace(/\s*font-size\s*:\s*[^;'"]+;?/gi, "") s = s.replace(/\s*font-weight\s*:\s*[^;'"]+;?/gi, "") s = s.replace(/\s*font-style\s*:\s*[^;'"]+;?/gi, "") /* Clean up remaining empty style attributes after font removal */ s = s.replace(/\s+style\s*=\s*"[\s;]*"/gi, "") /* Remove <span> tags with no remaining attributes */ s = s.replace(/<span\s*>([\s\S]*?)<\/span>/gi, "$1") /* Remove <font> wrapper tags */ s = s.replace(/<\/?font[^>]*>/gi, "") /* Collapse multiple blank lines */ s = s.replace(/\n{3,}/g, "\n\n") return s.trim() } RowLayout { Layout.fillWidth: true spacing: 2 ToolButton { text: "<b>B</b>"; onClicked: insertText("<b>", "</b>"); font.bold: true; visible: !rawMode } ToolButton { text: "<i>I</i>"; onClicked: insertText("<i>", "</i>"); font.italic: true; visible: !rawMode } ToolButton { text: "<u>U</u>"; onClicked: insertText("<u>", "</u>"); font.underline: true; visible: !rawMode } ToolButton { text: "<b>B</b>"; onClicked: wrapSelection("<b>", "</b>"); font.bold: true; visible: !rawMode } ToolButton { text: "<i>I</i>"; onClicked: wrapSelection("<i>", "</i>"); font.italic: true; visible: !rawMode } ToolButton { text: "<u>U</u>"; onClicked: wrapSelection("<u>", "</u>"); font.underline: true; visible: !rawMode } ToolSeparator { visible: !rawMode } ToolButton { text: "H1"; onClicked: insertText("<h1>", "</h1>"); visible: !rawMode } ToolButton { text: "H2"; onClicked: insertText("<h2>", "</h2>"); visible: !rawMode } ToolButton { text: "H3"; onClicked: insertText("<h3>", "</h3>"); visible: !rawMode } ToolButton { text: "H1"; onClicked: wrapSelection("<h1>", "</h1>"); visible: !rawMode } ToolButton { text: "H2"; onClicked: wrapSelection("<h2>", "</h2>"); visible: !rawMode } ToolButton { text: "H3"; onClicked: wrapSelection("<h3>", "</h3>"); visible: !rawMode } ToolSeparator { visible: !rawMode } ToolButton { text: "p"; onClicked: insertText("<p>", "</p>"); visible: !rawMode } ToolButton { text: "br"; onClicked: insertText("<br/>", ""); visible: !rawMode } ToolButton { text: "Link"; onClicked: insertText("<a href=\"#\">", "</a>"); visible: !rawMode } ToolButton { text: "p"; onClicked: wrapSelection("<p>", "</p>"); visible: !rawMode } ToolButton { text: "br"; onClicked: wrapSelection("<br/>", ""); visible: !rawMode } ToolButton { text: "Link"; onClicked: wrapSelection("<a href=\"#\">", "</a>"); visible: !rawMode } ToolSeparator { visible: !rawMode } /* Text color */ ToolButton { id: fgColorBtn visible: !rawMode contentItem: Row { spacing: 2 Text { text: "A"; font.pixelSize: 13; font.bold: true; color: Theme.textPrimary } Rectangle { width: 14; height: 4; anchors.bottom: parent.bottom; color: fgColorSwatch.selectedColor; radius: 1 } } ToolTip.text: qsTr("Text Color") ToolTip.visible: hovered onClicked: fgColorPopup.open() property string selectedColor: "" Popup { id: fgColorPopup width: colorGrid.width + 16 height: colorGrid.height + clearFgBtn.height + 24 padding: 8 GridLayout { id: colorGrid columns: 6 columnSpacing: 4 rowSpacing: 4 Repeater { model: [ "#000000", "#ffffff", "#e63946", "#f4a261", "#2a9d8f", "#264653", "#e76f51", "#f1c453", "#06d6a0", "#118ab2", "#073b4c", "#6c757d", "#d62828", "#f77f00", "#3ad43a", "#4361ee", "#7209b7", "#ff006e", "var(--blogi-text)", "var(--blogi-accent)", "var(--blogi-text-muted)", "var(--blogi-error)", "var(--blogi-success-text)", "var(--blogi-nav-active)" ] Rectangle { width: 24; height: 24; radius: 3 color: modelData.startsWith("var(") ? "transparent" : modelData border.color: Theme.border; border.width: 1 /* Show label for CSS variables */ Text { visible: modelData.startsWith("var(") anchors.centerIn: parent text: modelData.replace("var(--blogi-", "").replace(")", "") font.pixelSize: 5 color: Theme.textPrimary } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { fgColorBtn.selectedColor = modelData wrapSelection("<span style=\"color:" + modelData + "\">", "</span>") fgColorPopup.close() } } } } } Button { id: clearFgBtn anchors.top: colorGrid.bottom anchors.topMargin: 4 text: qsTr("Custom...") font.pixelSize: 11 onClicked: { fgColorPopup.close() fgCustomPicker.initialColor = fgColorBtn.selectedColor || "#000000" fgCustomPicker.open() } } } } /* Background color */ ToolButton { id: bgColorBtn visible: !rawMode contentItem: Row { spacing: 2 Rectangle { width: 14; height: 14; color: bgColorSwatch.selectedColor || Theme.bgTertiary; radius: 2; border.color: Theme.border } } ToolTip.text: qsTr("Background Color") ToolTip.visible: hovered onClicked: bgColorPopup.open() property string selectedColor: "" Popup { id: bgColorPopup width: bgColorGrid.width + 16 height: bgColorGrid.height + clearBgBtn.height + 24 padding: 8 GridLayout { id: bgColorGrid columns: 6 columnSpacing: 4 rowSpacing: 4 Repeater { model: [ "#000000", "#ffffff", "#fce4ec", "#fff3e0", "#e8f5e9", "#e3f2fd", "#f3e5f5", "#fff8e1", "#e0f7fa", "#fbe9e7", "#f1f8e9", "#ede7f6", "var(--blogi-bg)", "var(--blogi-surface)", "var(--blogi-bg-alt)", "var(--blogi-success-bg)", "var(--blogi-error-bg)", "var(--blogi-accent)" ] Rectangle { width: 24; height: 24; radius: 3 color: modelData.startsWith("var(") ? "transparent" : modelData border.color: Theme.border; border.width: 1 Text { visible: modelData.startsWith("var(") anchors.centerIn: parent text: modelData.replace("var(--blogi-", "").replace(")", "") font.pixelSize: 5 color: Theme.textPrimary } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { bgColorBtn.selectedColor = modelData wrapSelection("<span style=\"background:" + modelData + "\">", "</span>") bgColorPopup.close() } } } } } Button { id: clearBgBtn anchors.top: bgColorGrid.bottom anchors.topMargin: 4 text: qsTr("Custom...") font.pixelSize: 11 onClicked: { bgColorPopup.close() bgCustomPicker.initialColor = bgColorBtn.selectedColor || "#ffffff" bgCustomPicker.open() } } } } Item { Layout.fillWidth: true } Loading @@ -39,6 +247,26 @@ ColumnLayout { } } /* Hidden color swatch references for the buttons */ QtObject { id: fgColorSwatch; property string selectedColor: fgColorBtn.selectedColor || Theme.textPrimary } QtObject { id: bgColorSwatch; property string selectedColor: bgColorBtn.selectedColor } /* Custom RGBA pickers */ RgbaColorPicker { id: fgCustomPicker onColorSelected: function(colorStr) { fgColorBtn.selectedColor = colorStr wrapSelection("<span style=\"color:" + colorStr + "\">", "</span>") } } RgbaColorPicker { id: bgCustomPicker onColorSelected: function(colorStr) { bgColorBtn.selectedColor = colorStr wrapSelection("<span style=\"background:" + colorStr + "\">", "</span>") } } ScrollView { Layout.fillWidth: true Layout.minimumHeight: 150 Loading @@ -47,30 +275,35 @@ ColumnLayout { TextArea { id: editorArea text: root.text textFormat: root.rawMode ? TextEdit.PlainText : TextEdit.RichText textFormat: TextEdit.RichText font.pixelSize: 13 color: Theme.textPrimary wrapMode: TextEdit.Wrap background: Rectangle { color: Theme.bgTertiary; border.color: Theme.border; radius: Theme.radius } onTextChanged: { if (editorArea.activeFocus) { if (editorArea.activeFocus && !root._switching) { if (root.rawMode) root.textChangedByUser(editorArea.text) else root.textChangedByUser(stripQtArtifacts(editorArea.text)) } } } } function insertText(prefix, suffix) { var start = editorArea.selectionStart; var end = editorArea.selectionEnd; var currentText = editorArea.getText(0, editorArea.length); var selectedText = currentText.substring(start, end); var newText = currentText.substring(0, start) + prefix + selectedText + suffix + currentText.substring(end); function wrapSelection(prefix, suffix) { var start = editorArea.selectionStart var end = editorArea.selectionEnd var currentText = editorArea.getText(0, editorArea.length) var selectedText = currentText.substring(start, end) var newText = currentText.substring(0, start) + prefix + selectedText + suffix + currentText.substring(end) editorArea.text = newText; editorArea.cursorPosition = start + prefix.length + selectedText.length; _switching = true editorArea.text = newText editorArea.cursorPosition = start + prefix.length + selectedText.length _switching = false root.textChangedByUser(editorArea.text); root.textChangedByUser(stripQtArtifacts(editorArea.text)) } } editor/qml/Main.qml +11 −4 Original line number Diff line number Diff line Loading @@ -52,6 +52,12 @@ ApplicationWindow { Item { Layout.fillWidth: true } Row { spacing: 4 ComboBox { id: themeSelect model: ["Dark", "Light"] implicitWidth: 90 onCurrentIndexChanged: previewFrame.darkPreview = (currentIndex === 0) } Button { text: "Desktop" highlighted: !previewFrame.mobileMode Loading @@ -72,13 +78,14 @@ ApplicationWindow { /* ---- Sidebar ---- */ ScrollView { SplitView.preferredWidth: 280 SplitView.minimumWidth: 200 SplitView.maximumWidth: 500 id: leftSidebar SplitView.preferredWidth: 350 SplitView.minimumWidth: 250 SplitView.maximumWidth: 1024 clip: true ColumnLayout { width: parent.width width: leftSidebar.availableWidth spacing: 0 /* Widgets (collapsed by default) */ Loading editor/qml/PreviewPane.qml +50 −20 Original line number Diff line number Diff line Loading @@ -6,11 +6,13 @@ Item { id: previewPane property bool mobileMode: false property bool darkPreview: true property bool useServerPreview: false property string serverHtml: "" property bool engineReady: false onMobileModeChanged: reload() onDarkPreviewChanged: reload() function reload() { if (!engineReady) return Loading @@ -20,26 +22,54 @@ Item { } else { var html = localApi.renderPreview() var baseUrl = surl.length > 0 ? surl : "about:blank" var cssVars if (darkPreview) { cssVars = ":root {" + " --blogi-bg: #232627;" + " --blogi-surface: #31363b;" + " --blogi-surface-a: rgba(49,54,59,0.7);" + " --blogi-text: #eeeeee;" + " --blogi-text-muted: #aaaaaa;" + " --blogi-accent: #3ad43a;" + " --blogi-accent-dark: #2ea82e;" + " --blogi-border: #3a3f44;" + " --blogi-bg-alt: #2a2e32;" + " --blogi-error: #F36947;" + " --blogi-error-bg: #3d1f1a;" + " --blogi-error-text: #ff9a8a;" + " --blogi-success-bg: #1a3d1f;" + " --blogi-success-text: #8aff9a;" + " --blogi-nav-active: #3daee9;" + " --blogi-nav-hover: #232629;" + " --blogi-nav-border: #222529;" + " color-scheme: dark;" + "}" } else { cssVars = ":root {" + " --blogi-bg: rgba(235,237,240,1);" + " --blogi-surface: rgba(245,246,248,1);" + " --blogi-surface-a: rgba(245,246,248,0.88);" + " --blogi-text: rgba(44,62,80,1);" + " --blogi-text-muted: rgba(127,140,141,1);" + " --blogi-accent: rgba(39,148,94,1);" + " --blogi-accent-dark: rgba(30,122,75,1);" + " --blogi-border: rgba(220,225,230,1);" + " --blogi-bg-alt: rgba(232,236,240,1);" + " --blogi-error: rgba(231,76,60,1);" + " --blogi-error-bg: rgba(253,236,234,1);" + " --blogi-error-text: rgba(192,57,43,1);" + " --blogi-success-bg: rgba(234,250,241,1);" + " --blogi-success-text: rgba(39,174,96,1);" + " --blogi-nav-active: rgba(41,128,185,1);" + " --blogi-nav-hover: rgba(232,236,240,1);" + " --blogi-nav-border: rgba(189,195,199,1);" + " color-scheme: light;" + "}" } var bgColor = darkPreview ? "#232627" : "rgba(235,237,240,1)" webView.loadHtml( "<!DOCTYPE html><html><head><meta charset='utf-8'>" + "<style>" + ":root {" + " --blogi-bg: #ffffff;" + " --blogi-surface: #f8f9fa;" + " --blogi-surface-a: #f0f1f3;" + " --blogi-text: #1a1a2e;" + " --blogi-text-muted: #6c757d;" + " --blogi-accent: #4361ee;" + " --blogi-accent-dark: #3a56d4;" + " --blogi-border: #dee2e6;" + " --blogi-bg-alt: #eef0f2;" + " --blogi-error: #e63946;" + " --blogi-success-bg: #d4edda;" + " --blogi-success-text: #155724;" + " --blogi-nav-active: #4361ee;" + " --blogi-nav-hover: #e9ecef;" + " --blogi-nav-border: #dee2e6;" + "}" + "<style>" + cssVars + "html,body{margin:0;padding:8px;background:var(--blogi-bg);color:var(--blogi-text);font-family:sans-serif;}" + "</style>" + "</head><body>" + html + "</body></html>", Loading @@ -50,7 +80,7 @@ Item { Rectangle { anchors.fill: parent color: "#ffffff" color: darkPreview ? "#232627" : "#ffffff" } WebEngineView { Loading @@ -62,7 +92,7 @@ Item { settings.localContentCanAccessRemoteUrls: true settings.javascriptEnabled: true backgroundColor: "#ffffff" backgroundColor: darkPreview ? "#232627" : "#ffffff" onLoadingChanged: function(loadRequest) { if (!previewPane.engineReady && loadRequest.status !== WebEngineView.LoadStartedStatus) { Loading Loading
editor/qml/DocumentTree.qml +1 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ ColumnLayout { property var treeData: null function reload() { selectedUuid = "" var json = localApi.getDocTree() var resp = JSON.parse(json) if (resp.status === "success") Loading
editor/qml/HtmlEditorField.qml +264 −31 Original line number Diff line number Diff line Loading @@ -8,26 +8,234 @@ ColumnLayout { signal textChangedByUser(string newText) property bool rawMode: false property string _storedHtml: "" property bool _switching: false onRawModeChanged: { _switching = true if (rawMode) { /* RichText → Raw: extract and clean Qt artifacts */ _storedHtml = stripQtArtifacts(editorArea.text) editorArea.textFormat = TextEdit.PlainText editorArea.text = _storedHtml } else { /* Raw → RichText: take raw text as HTML source */ _storedHtml = editorArea.text editorArea.textFormat = TextEdit.RichText editorArea.text = _storedHtml } _switching = false } function stripQtArtifacts(html) { var s = html /* Remove DOCTYPE, html/head/body wrappers */ s = s.replace(/<!DOCTYPE[^>]*>/gi, "") s = s.replace(/<\/?html[^>]*>/gi, "") s = s.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, "") s = s.replace(/<\/?body[^>]*>/gi, "") /* Remove Qt paragraph style artifacts */ s = s.replace(/\s*style\s*=\s*"\s*-qt-[^"]*"/gi, "") /* Remove empty style attributes left over */ s = s.replace(/\s+style\s*=\s*"[\s;]*"/gi, "") /* Remove font-family / font-size from inline styles (Qt injects system font) */ s = s.replace(/\s*font-family\s*:\s*'[^']*'\s*;?/gi, "") s = s.replace(/\s*font-size\s*:\s*[^;'"]+;?/gi, "") s = s.replace(/\s*font-weight\s*:\s*[^;'"]+;?/gi, "") s = s.replace(/\s*font-style\s*:\s*[^;'"]+;?/gi, "") /* Clean up remaining empty style attributes after font removal */ s = s.replace(/\s+style\s*=\s*"[\s;]*"/gi, "") /* Remove <span> tags with no remaining attributes */ s = s.replace(/<span\s*>([\s\S]*?)<\/span>/gi, "$1") /* Remove <font> wrapper tags */ s = s.replace(/<\/?font[^>]*>/gi, "") /* Collapse multiple blank lines */ s = s.replace(/\n{3,}/g, "\n\n") return s.trim() } RowLayout { Layout.fillWidth: true spacing: 2 ToolButton { text: "<b>B</b>"; onClicked: insertText("<b>", "</b>"); font.bold: true; visible: !rawMode } ToolButton { text: "<i>I</i>"; onClicked: insertText("<i>", "</i>"); font.italic: true; visible: !rawMode } ToolButton { text: "<u>U</u>"; onClicked: insertText("<u>", "</u>"); font.underline: true; visible: !rawMode } ToolButton { text: "<b>B</b>"; onClicked: wrapSelection("<b>", "</b>"); font.bold: true; visible: !rawMode } ToolButton { text: "<i>I</i>"; onClicked: wrapSelection("<i>", "</i>"); font.italic: true; visible: !rawMode } ToolButton { text: "<u>U</u>"; onClicked: wrapSelection("<u>", "</u>"); font.underline: true; visible: !rawMode } ToolSeparator { visible: !rawMode } ToolButton { text: "H1"; onClicked: insertText("<h1>", "</h1>"); visible: !rawMode } ToolButton { text: "H2"; onClicked: insertText("<h2>", "</h2>"); visible: !rawMode } ToolButton { text: "H3"; onClicked: insertText("<h3>", "</h3>"); visible: !rawMode } ToolButton { text: "H1"; onClicked: wrapSelection("<h1>", "</h1>"); visible: !rawMode } ToolButton { text: "H2"; onClicked: wrapSelection("<h2>", "</h2>"); visible: !rawMode } ToolButton { text: "H3"; onClicked: wrapSelection("<h3>", "</h3>"); visible: !rawMode } ToolSeparator { visible: !rawMode } ToolButton { text: "p"; onClicked: insertText("<p>", "</p>"); visible: !rawMode } ToolButton { text: "br"; onClicked: insertText("<br/>", ""); visible: !rawMode } ToolButton { text: "Link"; onClicked: insertText("<a href=\"#\">", "</a>"); visible: !rawMode } ToolButton { text: "p"; onClicked: wrapSelection("<p>", "</p>"); visible: !rawMode } ToolButton { text: "br"; onClicked: wrapSelection("<br/>", ""); visible: !rawMode } ToolButton { text: "Link"; onClicked: wrapSelection("<a href=\"#\">", "</a>"); visible: !rawMode } ToolSeparator { visible: !rawMode } /* Text color */ ToolButton { id: fgColorBtn visible: !rawMode contentItem: Row { spacing: 2 Text { text: "A"; font.pixelSize: 13; font.bold: true; color: Theme.textPrimary } Rectangle { width: 14; height: 4; anchors.bottom: parent.bottom; color: fgColorSwatch.selectedColor; radius: 1 } } ToolTip.text: qsTr("Text Color") ToolTip.visible: hovered onClicked: fgColorPopup.open() property string selectedColor: "" Popup { id: fgColorPopup width: colorGrid.width + 16 height: colorGrid.height + clearFgBtn.height + 24 padding: 8 GridLayout { id: colorGrid columns: 6 columnSpacing: 4 rowSpacing: 4 Repeater { model: [ "#000000", "#ffffff", "#e63946", "#f4a261", "#2a9d8f", "#264653", "#e76f51", "#f1c453", "#06d6a0", "#118ab2", "#073b4c", "#6c757d", "#d62828", "#f77f00", "#3ad43a", "#4361ee", "#7209b7", "#ff006e", "var(--blogi-text)", "var(--blogi-accent)", "var(--blogi-text-muted)", "var(--blogi-error)", "var(--blogi-success-text)", "var(--blogi-nav-active)" ] Rectangle { width: 24; height: 24; radius: 3 color: modelData.startsWith("var(") ? "transparent" : modelData border.color: Theme.border; border.width: 1 /* Show label for CSS variables */ Text { visible: modelData.startsWith("var(") anchors.centerIn: parent text: modelData.replace("var(--blogi-", "").replace(")", "") font.pixelSize: 5 color: Theme.textPrimary } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { fgColorBtn.selectedColor = modelData wrapSelection("<span style=\"color:" + modelData + "\">", "</span>") fgColorPopup.close() } } } } } Button { id: clearFgBtn anchors.top: colorGrid.bottom anchors.topMargin: 4 text: qsTr("Custom...") font.pixelSize: 11 onClicked: { fgColorPopup.close() fgCustomPicker.initialColor = fgColorBtn.selectedColor || "#000000" fgCustomPicker.open() } } } } /* Background color */ ToolButton { id: bgColorBtn visible: !rawMode contentItem: Row { spacing: 2 Rectangle { width: 14; height: 14; color: bgColorSwatch.selectedColor || Theme.bgTertiary; radius: 2; border.color: Theme.border } } ToolTip.text: qsTr("Background Color") ToolTip.visible: hovered onClicked: bgColorPopup.open() property string selectedColor: "" Popup { id: bgColorPopup width: bgColorGrid.width + 16 height: bgColorGrid.height + clearBgBtn.height + 24 padding: 8 GridLayout { id: bgColorGrid columns: 6 columnSpacing: 4 rowSpacing: 4 Repeater { model: [ "#000000", "#ffffff", "#fce4ec", "#fff3e0", "#e8f5e9", "#e3f2fd", "#f3e5f5", "#fff8e1", "#e0f7fa", "#fbe9e7", "#f1f8e9", "#ede7f6", "var(--blogi-bg)", "var(--blogi-surface)", "var(--blogi-bg-alt)", "var(--blogi-success-bg)", "var(--blogi-error-bg)", "var(--blogi-accent)" ] Rectangle { width: 24; height: 24; radius: 3 color: modelData.startsWith("var(") ? "transparent" : modelData border.color: Theme.border; border.width: 1 Text { visible: modelData.startsWith("var(") anchors.centerIn: parent text: modelData.replace("var(--blogi-", "").replace(")", "") font.pixelSize: 5 color: Theme.textPrimary } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { bgColorBtn.selectedColor = modelData wrapSelection("<span style=\"background:" + modelData + "\">", "</span>") bgColorPopup.close() } } } } } Button { id: clearBgBtn anchors.top: bgColorGrid.bottom anchors.topMargin: 4 text: qsTr("Custom...") font.pixelSize: 11 onClicked: { bgColorPopup.close() bgCustomPicker.initialColor = bgColorBtn.selectedColor || "#ffffff" bgCustomPicker.open() } } } } Item { Layout.fillWidth: true } Loading @@ -39,6 +247,26 @@ ColumnLayout { } } /* Hidden color swatch references for the buttons */ QtObject { id: fgColorSwatch; property string selectedColor: fgColorBtn.selectedColor || Theme.textPrimary } QtObject { id: bgColorSwatch; property string selectedColor: bgColorBtn.selectedColor } /* Custom RGBA pickers */ RgbaColorPicker { id: fgCustomPicker onColorSelected: function(colorStr) { fgColorBtn.selectedColor = colorStr wrapSelection("<span style=\"color:" + colorStr + "\">", "</span>") } } RgbaColorPicker { id: bgCustomPicker onColorSelected: function(colorStr) { bgColorBtn.selectedColor = colorStr wrapSelection("<span style=\"background:" + colorStr + "\">", "</span>") } } ScrollView { Layout.fillWidth: true Layout.minimumHeight: 150 Loading @@ -47,30 +275,35 @@ ColumnLayout { TextArea { id: editorArea text: root.text textFormat: root.rawMode ? TextEdit.PlainText : TextEdit.RichText textFormat: TextEdit.RichText font.pixelSize: 13 color: Theme.textPrimary wrapMode: TextEdit.Wrap background: Rectangle { color: Theme.bgTertiary; border.color: Theme.border; radius: Theme.radius } onTextChanged: { if (editorArea.activeFocus) { if (editorArea.activeFocus && !root._switching) { if (root.rawMode) root.textChangedByUser(editorArea.text) else root.textChangedByUser(stripQtArtifacts(editorArea.text)) } } } } function insertText(prefix, suffix) { var start = editorArea.selectionStart; var end = editorArea.selectionEnd; var currentText = editorArea.getText(0, editorArea.length); var selectedText = currentText.substring(start, end); var newText = currentText.substring(0, start) + prefix + selectedText + suffix + currentText.substring(end); function wrapSelection(prefix, suffix) { var start = editorArea.selectionStart var end = editorArea.selectionEnd var currentText = editorArea.getText(0, editorArea.length) var selectedText = currentText.substring(start, end) var newText = currentText.substring(0, start) + prefix + selectedText + suffix + currentText.substring(end) editorArea.text = newText; editorArea.cursorPosition = start + prefix.length + selectedText.length; _switching = true editorArea.text = newText editorArea.cursorPosition = start + prefix.length + selectedText.length _switching = false root.textChangedByUser(editorArea.text); root.textChangedByUser(stripQtArtifacts(editorArea.text)) } }
editor/qml/Main.qml +11 −4 Original line number Diff line number Diff line Loading @@ -52,6 +52,12 @@ ApplicationWindow { Item { Layout.fillWidth: true } Row { spacing: 4 ComboBox { id: themeSelect model: ["Dark", "Light"] implicitWidth: 90 onCurrentIndexChanged: previewFrame.darkPreview = (currentIndex === 0) } Button { text: "Desktop" highlighted: !previewFrame.mobileMode Loading @@ -72,13 +78,14 @@ ApplicationWindow { /* ---- Sidebar ---- */ ScrollView { SplitView.preferredWidth: 280 SplitView.minimumWidth: 200 SplitView.maximumWidth: 500 id: leftSidebar SplitView.preferredWidth: 350 SplitView.minimumWidth: 250 SplitView.maximumWidth: 1024 clip: true ColumnLayout { width: parent.width width: leftSidebar.availableWidth spacing: 0 /* Widgets (collapsed by default) */ Loading
editor/qml/PreviewPane.qml +50 −20 Original line number Diff line number Diff line Loading @@ -6,11 +6,13 @@ Item { id: previewPane property bool mobileMode: false property bool darkPreview: true property bool useServerPreview: false property string serverHtml: "" property bool engineReady: false onMobileModeChanged: reload() onDarkPreviewChanged: reload() function reload() { if (!engineReady) return Loading @@ -20,26 +22,54 @@ Item { } else { var html = localApi.renderPreview() var baseUrl = surl.length > 0 ? surl : "about:blank" var cssVars if (darkPreview) { cssVars = ":root {" + " --blogi-bg: #232627;" + " --blogi-surface: #31363b;" + " --blogi-surface-a: rgba(49,54,59,0.7);" + " --blogi-text: #eeeeee;" + " --blogi-text-muted: #aaaaaa;" + " --blogi-accent: #3ad43a;" + " --blogi-accent-dark: #2ea82e;" + " --blogi-border: #3a3f44;" + " --blogi-bg-alt: #2a2e32;" + " --blogi-error: #F36947;" + " --blogi-error-bg: #3d1f1a;" + " --blogi-error-text: #ff9a8a;" + " --blogi-success-bg: #1a3d1f;" + " --blogi-success-text: #8aff9a;" + " --blogi-nav-active: #3daee9;" + " --blogi-nav-hover: #232629;" + " --blogi-nav-border: #222529;" + " color-scheme: dark;" + "}" } else { cssVars = ":root {" + " --blogi-bg: rgba(235,237,240,1);" + " --blogi-surface: rgba(245,246,248,1);" + " --blogi-surface-a: rgba(245,246,248,0.88);" + " --blogi-text: rgba(44,62,80,1);" + " --blogi-text-muted: rgba(127,140,141,1);" + " --blogi-accent: rgba(39,148,94,1);" + " --blogi-accent-dark: rgba(30,122,75,1);" + " --blogi-border: rgba(220,225,230,1);" + " --blogi-bg-alt: rgba(232,236,240,1);" + " --blogi-error: rgba(231,76,60,1);" + " --blogi-error-bg: rgba(253,236,234,1);" + " --blogi-error-text: rgba(192,57,43,1);" + " --blogi-success-bg: rgba(234,250,241,1);" + " --blogi-success-text: rgba(39,174,96,1);" + " --blogi-nav-active: rgba(41,128,185,1);" + " --blogi-nav-hover: rgba(232,236,240,1);" + " --blogi-nav-border: rgba(189,195,199,1);" + " color-scheme: light;" + "}" } var bgColor = darkPreview ? "#232627" : "rgba(235,237,240,1)" webView.loadHtml( "<!DOCTYPE html><html><head><meta charset='utf-8'>" + "<style>" + ":root {" + " --blogi-bg: #ffffff;" + " --blogi-surface: #f8f9fa;" + " --blogi-surface-a: #f0f1f3;" + " --blogi-text: #1a1a2e;" + " --blogi-text-muted: #6c757d;" + " --blogi-accent: #4361ee;" + " --blogi-accent-dark: #3a56d4;" + " --blogi-border: #dee2e6;" + " --blogi-bg-alt: #eef0f2;" + " --blogi-error: #e63946;" + " --blogi-success-bg: #d4edda;" + " --blogi-success-text: #155724;" + " --blogi-nav-active: #4361ee;" + " --blogi-nav-hover: #e9ecef;" + " --blogi-nav-border: #dee2e6;" + "}" + "<style>" + cssVars + "html,body{margin:0;padding:8px;background:var(--blogi-bg);color:var(--blogi-text);font-family:sans-serif;}" + "</style>" + "</head><body>" + html + "</body></html>", Loading @@ -50,7 +80,7 @@ Item { Rectangle { anchors.fill: parent color: "#ffffff" color: darkPreview ? "#232627" : "#ffffff" } WebEngineView { Loading @@ -62,7 +92,7 @@ Item { settings.localContentCanAccessRemoteUrls: true settings.javascriptEnabled: true backgroundColor: "#ffffff" backgroundColor: darkPreview ? "#232627" : "#ffffff" onLoadingChanged: function(loadRequest) { if (!previewPane.engineReady && loadRequest.status !== WebEngineView.LoadStartedStatus) { Loading