diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 0a3a28e..ab31660 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -4,13 +4,14 @@ on: push: tags: - "desktop-v*" + workflow_dispatch: permissions: contents: write jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout repository @@ -21,13 +22,21 @@ jobs: with: node-version: "lts/*" + - name: Inject version from tag + working-directory: desktop-app + run: | + VERSION="${GITHUB_REF_NAME#desktop-v}" + jq --arg v "$VERSION" '.version = $v' neutralino.config.json > tmp.json \ + && mv tmp.json neutralino.config.json + echo "::notice::Building version $VERSION" + - name: Setup Neutralinojs binaries working-directory: desktop-app run: npm run setup - name: Build all binaries (embedded + portable) working-directory: desktop-app - run: npm run build:all + run: npm run build - name: Stage release assets working-directory: desktop-app @@ -63,6 +72,7 @@ jobs: --exclude='desktop-app/bin' \ --exclude='desktop-app/node_modules' \ --exclude='desktop-app/output' \ + --exclude="desktop-app/$STAGING" \ --exclude='.git' \ desktop-app/ cd desktop-app @@ -74,6 +84,6 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - name: "Markdown Viewer Desktop ${{ github.ref_name }}" + name: "${{ github.ref_name }}" generate_release_notes: true files: desktop-app/release-assets/* diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index b489d2f..a6d8f22 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -2,9 +2,13 @@ name: Build and Push Docker Image on: push: - branches: [ main ] + branches: [main] + paths-ignore: + - "desktop-app/**" pull_request: - branches: [ main ] + branches: [main] + paths-ignore: + - "desktop-app/**" env: REGISTRY: ghcr.io @@ -18,37 +22,36 @@ jobs: packages: write steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=sha,prefix={{branch}}- - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/desktop-app/.dockerignore b/desktop-app/.dockerignore index 28f2c2a..6be7bc7 100644 --- a/desktop-app/.dockerignore +++ b/desktop-app/.dockerignore @@ -1,5 +1,6 @@ # Build-generated resources resources/js/script.js +resources/js/neutralino* resources/styles.css resources/assets/ resources/index.html diff --git a/desktop-app/.gitignore b/desktop-app/.gitignore index 4c77b9d..9fc6d7b 100644 --- a/desktop-app/.gitignore +++ b/desktop-app/.gitignore @@ -5,17 +5,16 @@ node_modules/ .lite_workspace.lua # Neutralinojs binaries and builds -/bin -/dist - -# Neutralinojs client (minified) -neutralino.js +bin/ +dist/ # Build-generated resources (copied from root by prepare.js) -/resources/js/script.js -/resources/styles.css -/resources/assets/ -/resources/index.html +resources/js/script.js +resources/js/neutralino* +resources/styles.css +resources/assets/ +resources/index.html + # Neutralinojs related files .storage @@ -25,4 +24,4 @@ neutralino.js .tmp # Docker build output -/output \ No newline at end of file +output/ \ No newline at end of file diff --git a/desktop-app/Dockerfile b/desktop-app/Dockerfile index 9c5f12d..1f613b1 100644 --- a/desktop-app/Dockerfile +++ b/desktop-app/Dockerfile @@ -13,7 +13,7 @@ COPY . . WORKDIR /app/desktop-app # Setup (download binaries + prepare resources) and build all variants -RUN npm run build:all +RUN npm run build # Final stage: Export the dist artifacts FROM alpine:latest diff --git a/desktop-app/README.md b/desktop-app/README.md index 210d881..a925842 100644 --- a/desktop-app/README.md +++ b/desktop-app/README.md @@ -11,9 +11,8 @@ Neutralinojs platform binaries are managed by `setup-binaries.js`, which downloa Desktop-only files (not generated): - `resources/js/main.js` — Neutralinojs lifecycle, tray menu, window events -- `resources/js/neutralino.js` — Neutralinojs client library - `neutralino.config.json` — App configuration -- `setup-binaries.js` — Idempotent binary setup (downloads on first use) +- `setup-binaries.js` — Idempotent binary setup (downloads on first use or updates if `cli.binaryVersion` changes) ## Development @@ -45,7 +44,7 @@ For more information, see the [Neutralinojs documentation](https://neutralino.js ### Building the app -**Default** — Single-file executables with embedded resources: +**Default** — Single-file executables with embedded resources + release ZIP bundle with separate `resources.neu` file: ```bash npm run build @@ -57,10 +56,10 @@ npm run build npm run build:portable ``` -**Both** — Build embedded + portable in one step: +**Embedded** — Single-file executables with embedded resources: ```bash -npm run build:all +npm run build:embedded ``` Build output is placed in `dist/`. @@ -79,7 +78,31 @@ Build artifacts will be output to `desktop-app/output/`. ## Releases -Prebuilt binaries are automatically built and published as GitHub Releases when a tag matching `desktop-v*` is pushed (e.g., `desktop-v1.0.0`). See [`.github/workflows/desktop-build.yml`](../.github/workflows/desktop-build.yml). +Prebuilt binaries are automatically built and published as GitHub Releases when a tag matching `desktop-v*` is pushed (e.g., `desktop-v2026.2.0`). See [`.github/workflows/desktop-build.yml`](../.github/workflows/desktop-build.yml). + +### Versioning + +The Git tag is the **single source of truth** for the release version, using CalVer (Calendar Versioning) format `desktop-vYYYY.M.P`; + +- `YYYY` = Year +- `M` = Month +- `P` = Patch (Defaults to 0, bumped if new release occurs same month) + +The CI workflow extracts the version from the tag (e.g., `desktop-v2026.2.0` → `2026.2.0`) and injects it into `neutralino.config.json` at build time. `package.json` carries a placeholder version (`0.0.0-dev`) since this is *not* an npm package. + +To create a release, you can use the utility script `tag.sh` to calculate the next [lightweight tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging): + +```bash +./tag.sh # Calculates the next tag based on the current date, latest tag, and commit SHA +``` + +or run the following commands, replacing `` with the desired version (e.g., `2026.2.1`): + +```bash +git tag desktop-v && git push origin desktop-v +``` + +### Release assets Each release includes: diff --git a/desktop-app/neutralino.config.json b/desktop-app/neutralino.config.json index be975d8..a2c7fca 100644 --- a/desktop-app/neutralino.config.json +++ b/desktop-app/neutralino.config.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json", - "applicationId": "js.neutralino.sample", - "version": "1.0.0", + "applicationId": "js.markdownviewer.desktop", + "version": "2026.2.0", "defaultMode": "window", "port": 0, "documentRoot": "/resources/", diff --git a/desktop-app/package.json b/desktop-app/package.json index b02da6d..8634f03 100644 --- a/desktop-app/package.json +++ b/desktop-app/package.json @@ -1,17 +1,29 @@ { "name": "markdown-viewer-desktop", - "version": "1.0.0", + "author": "ramezio", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/ramezio/markdown-viewer-fork.git" + }, + "contributors": [ + "ramezio", + "JBroeren", + "ThisIs-Developer" + ], + "version": "0.0.0-dev", "private": true, - "description": "Neutralinojs desktop port of Markdown Viewer", + "description": "Neutralinojs desktop port of Markdown Viewer (https://github.com/ThisIs-Developer/markdown-viewer)", "scripts": { "setup": "node setup-binaries.js", "postsetup": "node prepare.js", + "clean": "rm -rf bin dist node_modules .tmp .neutralinojs.log resources/js/script.js resources/styles.css resources/assets resources/index.html resources/js/neutralino.js resources/js/neutralino.d.ts", "predev": "npm run setup", "dev": "npx -y @neutralinojs/neu@11.7.0 run", "prebuild": "npm run setup", - "build": "npx -y @neutralinojs/neu@11.7.0 build --embed-resources", - "build:portable": "npx -y @neutralinojs/neu@11.7.0 build --release", - "build:all": "npm run build && npm run build:portable" + "build": "npx -y @neutralinojs/neu@11.7.0 build --embed-resources --release", + "build:portable": "npm run setup && npx -y @neutralinojs/neu@11.7.0 build --release", + "build:embedded": "npm run setup && npx -y @neutralinojs/neu@11.7.0 build --embed-resources" }, "dependencies": {} } diff --git a/desktop-app/resources/assets/Black and Beige Simple Coming Soon Banner.png b/desktop-app/resources/assets/Black and Beige Simple Coming Soon Banner.png deleted file mode 100644 index 8ec1fc1..0000000 Binary files a/desktop-app/resources/assets/Black and Beige Simple Coming Soon Banner.png and /dev/null differ diff --git a/desktop-app/resources/assets/code.png b/desktop-app/resources/assets/code.png deleted file mode 100644 index 9473cb1..0000000 Binary files a/desktop-app/resources/assets/code.png and /dev/null differ diff --git a/desktop-app/resources/assets/github.png b/desktop-app/resources/assets/github.png deleted file mode 100644 index 0c2ee50..0000000 Binary files a/desktop-app/resources/assets/github.png and /dev/null differ diff --git a/desktop-app/resources/assets/icon.jpg b/desktop-app/resources/assets/icon.jpg deleted file mode 100644 index cdb8b4a..0000000 Binary files a/desktop-app/resources/assets/icon.jpg and /dev/null differ diff --git a/desktop-app/resources/assets/live-peview.gif b/desktop-app/resources/assets/live-peview.gif deleted file mode 100644 index 56edb86..0000000 Binary files a/desktop-app/resources/assets/live-peview.gif and /dev/null differ diff --git a/desktop-app/resources/assets/mathexp.png b/desktop-app/resources/assets/mathexp.png deleted file mode 100644 index 4731f6f..0000000 Binary files a/desktop-app/resources/assets/mathexp.png and /dev/null differ diff --git a/desktop-app/resources/assets/mermaid.png b/desktop-app/resources/assets/mermaid.png deleted file mode 100644 index 16323a4..0000000 Binary files a/desktop-app/resources/assets/mermaid.png and /dev/null differ diff --git a/desktop-app/resources/assets/table.png b/desktop-app/resources/assets/table.png deleted file mode 100644 index e0cca14..0000000 Binary files a/desktop-app/resources/assets/table.png and /dev/null differ diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html deleted file mode 100644 index 8b21ccc..0000000 --- a/desktop-app/resources/index.html +++ /dev/null @@ -1,234 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Markdown Viewer - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-
- -
-

Markdown Viewer

- - - -
-
- 0 Min Read -
-
- 0 Words -
-
- 0 Chars -
-
-
- - -
- - - -
- - -
- - - - - - - - - -
- - -
- - -
-
-
Menu
- -
- - -
- - - -
- -
-
- 0 Min Read -
-
- 0 Words -
-
- 0 Chars -
-
- -
- - - - - - - - - - - - - -
-
- -
-
-
-
- -
- -

Drop your Markdown file here or click to browse

-
- -
-
- -
- - -
-
-
-
-
- - - - - - - \ No newline at end of file diff --git a/desktop-app/resources/js/main.js b/desktop-app/resources/js/main.js index 6cec292..7e11bc7 100644 --- a/desktop-app/resources/js/main.js +++ b/desktop-app/resources/js/main.js @@ -1,36 +1,3 @@ -// This is just a sample app. You can structure your Neutralinojs app code as you wish. -// This example app is written with vanilla JavaScript and HTML. -// Feel free to use any frontend framework you like :) -// See more details: https://neutralino.js.org/docs/how-to/use-a-frontend-library - -/* - Function to display information about the Neutralino app. - This function updates the content of the 'info' element in the HTML - with details regarding the running Neutralino application, including - its ID, port, operating system, and version information. -*/ -function showInfo() { - return ` - ${NL_APPID} is running on port ${NL_PORT} inside ${NL_OS} -

- server: v${NL_VERSION} . client: v${NL_CVERSION} - `; -} - -/* - Function to open the official Neutralino documentation in the default web browser. -*/ -function openDocs() { - Neutralino.os.open("https://neutralino.js.org/docs"); -} - -/* - Function to open a tutorial video on Neutralino's official YouTube channel in the default web browser. -*/ -function openTutorial() { - Neutralino.os.open("https://www.youtube.com/c/CodeZri"); -} - /* Function to set up a system tray menu with options specific to the window mode. This function checks if the application is running in window mode, and if so, @@ -45,7 +12,7 @@ function setTray() { // Define tray menu items let tray = { - icon: "/resources/icons/trayIcon.png", + icon: "/resources/assets/icon.jpg", menuItems: [ { id: "VERSION", text: "Get version" }, { id: "SEP", text: "-" }, @@ -68,7 +35,7 @@ function onTrayMenuItemClicked(event) { // Display version information Neutralino.os.showMessageBox( "Version information", - `Neutralinojs server: v${NL_VERSION} | Neutralinojs client: v${NL_CVERSION}`, + `Neutralinojs server: v${NL_VERSION}\nNeutralinojs client: v${NL_CVERSION}\nOS Name: ${NL_OS}\nArchitecture: ${NL_ARCH}\nApplication ID: ${NL_APPID}\nApplication Version: ${NL_APPVERSION}\nPort: ${NL_PORT}\nMode: ${NL_MODE}\nNeutralinojs server: v${NL_VERSION}\nNeutralinojs client: v${NL_CVERSION}\nCurrent working directory: ${NL_CWD}\nApplication path: ${NL_PATH}\nApplication data path: ${NL_DATAPATH}\nCommand-line arguments: ${NL_ARGS}\nProcess ID: ${NL_PID}\nResource mode: ${NL_RESMODE}\nExtensions enabled: ${NL_EXTENABLED}\nFramework binary's release commit hash: ${NL_COMMIT}\nClient library's release commit hash: ${NL_CCOMMIT}\nCustom method identifiers: ${NL_CMETHODS}\nInitial window state was loaded from the saved configuration: ${NL_WSAVSTLOADED}\nUser System Locale: ${NL_LOCALE}\nData passed during the framework binary compilation via the NEU_COMPILATION_DATA definition in the BuildZri configuration file: ${NL_COMPDATA}`, ); break; case "QUIT": diff --git a/desktop-app/resources/js/neutralino.d.ts b/desktop-app/resources/js/neutralino.d.ts deleted file mode 100644 index 09d69e6..0000000 --- a/desktop-app/resources/js/neutralino.d.ts +++ /dev/null @@ -1,531 +0,0 @@ -export declare enum LoggerType { - WARNING = "WARNING", - ERROR = "ERROR", - INFO = "INFO" -} -export declare enum Icon { - WARNING = "WARNING", - ERROR = "ERROR", - INFO = "INFO", - QUESTION = "QUESTION" -} -export declare enum MessageBoxChoice { - OK = "OK", - OK_CANCEL = "OK_CANCEL", - YES_NO = "YES_NO", - YES_NO_CANCEL = "YES_NO_CANCEL", - RETRY_CANCEL = "RETRY_CANCEL", - ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE" -} -export declare enum ClipboardFormat { - unknown = "unknown", - text = "text", - image = "image" -} -export declare enum Mode { - window = "window", - browser = "browser", - cloud = "cloud", - chrome = "chrome" -} -export declare enum OperatingSystem { - Linux = "Linux", - Windows = "Windows", - Darwin = "Darwin", - FreeBSD = "FreeBSD", - Unknown = "Unknown" -} -export declare enum Architecture { - x64 = "x64", - arm = "arm", - itanium = "itanium", - ia32 = "ia32", - unknown = "unknown" -} -export interface DirectoryEntry { - entry: string; - path: string; - type: string; -} -export interface FileReaderOptions { - pos: number; - size: number; -} -export interface DirectoryReaderOptions { - recursive: boolean; -} -export interface OpenedFile { - id: number; - eof: boolean; - pos: number; - lastRead: number; -} -export interface Stats { - size: number; - isFile: boolean; - isDirectory: boolean; - createdAt: number; - modifiedAt: number; -} -export interface Watcher { - id: number; - path: string; -} -export interface CopyOptions { - recursive: boolean; - overwrite: boolean; - skip: boolean; -} -export interface PathParts { - rootName: string; - rootDirectory: string; - rootPath: string; - relativePath: string; - parentPath: string; - filename: string; - stem: string; - extension: string; -} -interface Permissions$1 { - all: boolean; - ownerAll: boolean; - ownerRead: boolean; - ownerWrite: boolean; - ownerExec: boolean; - groupAll: boolean; - groupRead: boolean; - groupWrite: boolean; - groupExec: boolean; - othersAll: boolean; - othersRead: boolean; - othersWrite: boolean; - othersExec: boolean; -} -export type PermissionsMode = "ADD" | "REPLACE" | "REMOVE"; -declare function createDirectory(path: string): Promise; -declare function remove(path: string): Promise; -declare function writeFile(path: string, data: string): Promise; -declare function appendFile(path: string, data: string): Promise; -declare function writeBinaryFile(path: string, data: ArrayBuffer): Promise; -declare function appendBinaryFile(path: string, data: ArrayBuffer): Promise; -declare function readFile(path: string, options?: FileReaderOptions): Promise; -declare function readBinaryFile(path: string, options?: FileReaderOptions): Promise; -declare function openFile(path: string): Promise; -declare function createWatcher(path: string): Promise; -declare function removeWatcher(id: number): Promise; -declare function getWatchers(): Promise; -declare function updateOpenedFile(id: number, event: string, data?: any): Promise; -declare function getOpenedFileInfo(id: number): Promise; -declare function readDirectory(path: string, options?: DirectoryReaderOptions): Promise; -declare function copy(source: string, destination: string, options?: CopyOptions): Promise; -declare function move(source: string, destination: string): Promise; -declare function getStats(path: string): Promise; -declare function getAbsolutePath(path: string): Promise; -declare function getRelativePath(path: string, base?: string): Promise; -declare function getPathParts(path: string): Promise; -declare function getPermissions(path: string): Promise; -declare function setPermissions(path: string, permissions: Permissions$1, mode: PermissionsMode): Promise; -declare function getJoinedPath(...paths: string[]): Promise; -declare function getNormalizedPath(path: string): Promise; -declare function getUnnormalizedPath(path: string): Promise; -export interface ExecCommandOptions { - stdIn?: string; - background?: boolean; - cwd?: string; -} -export interface ExecCommandResult { - pid: number; - stdOut: string; - stdErr: string; - exitCode: number; -} -export interface SpawnedProcess { - id: number; - pid: number; -} -export interface SpawnedProcessOptions { - cwd?: string; - envs?: Record; -} -export interface Envs { - [key: string]: string; -} -export interface OpenDialogOptions { - multiSelections?: boolean; - filters?: Filter[]; - defaultPath?: string; -} -export interface FolderDialogOptions { - defaultPath?: string; -} -export interface SaveDialogOptions { - forceOverwrite?: boolean; - filters?: Filter[]; - defaultPath?: string; -} -export interface Filter { - name: string; - extensions: string[]; -} -export interface TrayOptions { - icon: string; - menuItems: TrayMenuItem[]; -} -export interface TrayMenuItem { - id?: string; - text: string; - isDisabled?: boolean; - isChecked?: boolean; -} -export type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp"; -declare function execCommand(command: string, options?: ExecCommandOptions): Promise; -declare function spawnProcess(command: string, options?: SpawnedProcessOptions): Promise; -declare function updateSpawnedProcess(id: number, event: string, data?: any): Promise; -declare function getSpawnedProcesses(): Promise; -declare function getEnv(key: string): Promise; -declare function getEnvs(): Promise; -declare function showOpenDialog(title?: string, options?: OpenDialogOptions): Promise; -declare function showFolderDialog(title?: string, options?: FolderDialogOptions): Promise; -declare function showSaveDialog(title?: string, options?: SaveDialogOptions): Promise; -declare function showNotification(title: string, content: string, icon?: Icon): Promise; -declare function showMessageBox(title: string, content: string, choice?: MessageBoxChoice, icon?: Icon): Promise; -declare function setTray(options: TrayOptions): Promise; -declare function open$1(url: string): Promise; -declare function getPath(name: KnownPath): Promise; -export interface MemoryInfo { - physical: { - total: number; - available: number; - }; - virtual: { - total: number; - available: number; - }; -} -export interface KernelInfo { - variant: string; - version: string; -} -export interface OSInfo { - name: string; - description: string; - version: string; -} -export interface CPUInfo { - vendor: string; - model: string; - frequency: number; - architecture: string; - logicalThreads: number; - physicalCores: number; - physicalUnits: number; -} -export interface Display { - id: number; - resolution: Resolution; - dpi: number; - bpp: number; - refreshRate: number; -} -export interface Resolution { - width: number; - height: number; -} -export interface MousePosition { - x: number; - y: number; -} -declare function getMemoryInfo(): Promise; -declare function getArch(): Promise; -declare function getKernelInfo(): Promise; -declare function getOSInfo(): Promise; -declare function getCPUInfo(): Promise; -declare function getDisplays(): Promise; -declare function getMousePosition(): Promise; -declare function setData(key: string, data: string | null): Promise; -declare function getData(key: string): Promise; -declare function removeData(key: string): Promise; -declare function getKeys(): Promise; -declare function clear(): Promise; -declare function log(message: string, type?: LoggerType): Promise; -export interface OpenActionOptions { - url: string; -} -export interface RestartOptions { - args: string; -} -declare function exit(code?: number): Promise; -declare function killProcess(): Promise; -declare function restartProcess(options?: RestartOptions): Promise; -declare function getConfig(): Promise; -declare function broadcast(event: string, data?: any): Promise; -declare function readProcessInput(readAll?: boolean): Promise; -declare function writeProcessOutput(data: string): Promise; -declare function writeProcessError(data: string): Promise; -export interface WindowOptions extends WindowSizeOptions, WindowPosOptions { - title?: string; - icon?: string; - fullScreen?: boolean; - alwaysOnTop?: boolean; - enableInspector?: boolean; - borderless?: boolean; - maximize?: boolean; - hidden?: boolean; - maximizable?: boolean; - useSavedState?: boolean; - exitProcessOnClose?: boolean; - extendUserAgentWith?: string; - injectGlobals?: boolean; - injectClientLibrary?: boolean; - injectScript?: string; - processArgs?: string; -} -export interface WindowSizeOptions { - width?: number; - height?: number; - minWidth?: number; - minHeight?: number; - maxWidth?: number; - maxHeight?: number; - resizable?: boolean; -} -export interface WindowPosOptions { - x?: number; - y?: number; - center?: boolean; -} -export interface WindowMenu extends Array { -} -export interface WindowMenuItem { - id?: string; - text: string; - action?: string; - shortcut?: string; - isDisabled?: boolean; - isChecked?: boolean; - menuItems?: WindowMenuItem[]; -} -declare function setTitle(title: string): Promise; -declare function getTitle(): Promise; -declare function maximize(): Promise; -declare function unmaximize(): Promise; -declare function isMaximized(): Promise; -declare function minimize(): Promise; -declare function unminimize(): Promise; -declare function isMinimized(): Promise; -declare function setFullScreen(): Promise; -declare function exitFullScreen(): Promise; -declare function isFullScreen(): Promise; -declare function show(): Promise; -declare function hide(): Promise; -declare function isVisible(): Promise; -declare function focus$1(): Promise; -declare function setIcon(icon: string): Promise; -declare function move$1(x: number, y: number): Promise; -declare function center(): Promise; -declare function beginDrag(screenX?: number, screenY?: number): Promise; -declare function setDraggableRegion(DOMElementOrId: string | HTMLElement, options?: { - exclude?: Array; -}): Promise<{ - success: true; - message: string; - exclusions: { - add(elements: Array): void; - remove(elements: Array): void; - removeAll(): void; - }; -}>; -declare function unsetDraggableRegion(DOMElementOrId: string | HTMLElement): Promise<{ - success: true; - message: string; -}>; -declare function setSize(options: WindowSizeOptions): Promise; -declare function getSize(): Promise; -declare function getPosition(): Promise; -declare function setAlwaysOnTop(onTop: boolean): Promise; -declare function setBorderless(borderless: boolean): Promise; -declare function create(url: string, options?: WindowOptions): Promise; -declare function snapshot(path: string): Promise; -declare function setMainMenu(options: WindowMenu): Promise; -declare function print$1(): Promise; -interface Response$1 { - success: boolean; - message: string; -} -export type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp"; -declare function on(event: string, handler: (ev: CustomEvent) => void): Promise; -declare function off(event: string, handler: (ev: CustomEvent) => void): Promise; -declare function dispatch(event: string, data?: any): Promise; -declare function broadcast$1(event: string, data?: any): Promise; -export interface ExtensionStats { - loaded: string[]; - connected: string[]; -} -declare function dispatch$1(extensionId: string, event: string, data?: any): Promise; -declare function broadcast$2(event: string, data?: any): Promise; -declare function getStats$1(): Promise; -export interface Manifest { - applicationId: string; - version: string; - resourcesURL: string; -} -declare function checkForUpdates(url: string): Promise; -declare function install(): Promise; -export interface ClipboardImage { - width: number; - height: number; - bpp: number; - bpr: number; - redMask: number; - greenMask: number; - blueMask: number; - redShift: number; - greenShift: number; - blueShift: number; - data: ArrayBuffer; -} -declare function getFormat(): Promise; -declare function readText(): Promise; -declare function readImage(format?: string): Promise; -declare function writeText(data: string): Promise; -declare function writeImage(image: ClipboardImage): Promise; -declare function readHTML(): Promise; -declare function writeHTML(data: string): Promise; -declare function clear$1(): Promise; -interface Stats$1 { - size: number; - isFile: boolean; - isDirectory: boolean; -} -declare function getFiles(): Promise; -declare function getStats$2(path: string): Promise; -declare function extractFile(path: string, destination: string): Promise; -declare function extractDirectory(path: string, destination: string): Promise; -declare function readFile$1(path: string): Promise; -declare function readBinaryFile$1(path: string): Promise; -declare function mount(path: string, target: string): Promise; -declare function unmount(path: string): Promise; -declare function getMounts(): Promise>; -declare function getMethods(): Promise; -export interface InitOptions { - exportCustomMethods?: boolean; -} -export declare function init(options?: InitOptions): void; -export type ErrorCode = "NE_FS_DIRCRER" | "NE_FS_RMDIRER" | "NE_FS_FILRDER" | "NE_FS_FILWRER" | "NE_FS_FILRMER" | "NE_FS_NOPATHE" | "NE_FS_COPYFER" | "NE_FS_MOVEFER" | "NE_OS_INVMSGA" | "NE_OS_INVKNPT" | "NE_ST_INVSTKY" | "NE_ST_STKEYWE" | "NE_RT_INVTOKN" | "NE_RT_NATPRME" | "NE_RT_APIPRME" | "NE_RT_NATRTER" | "NE_RT_NATNTIM" | "NE_CL_NSEROFF" | "NE_EX_EXTNOTC" | "NE_UP_CUPDMER" | "NE_UP_CUPDERR" | "NE_UP_UPDNOUF" | "NE_UP_UPDINER"; -interface Error$1 { - code: ErrorCode; - message: string; -} -declare global { - interface Window { - /** Mode of the application: window, browser, cloud, or chrome */ - NL_MODE: Mode; - /** Application port */ - NL_PORT: number; - /** Command-line arguments */ - NL_ARGS: string[]; - /** Basic authentication token */ - NL_TOKEN: string; - /** Neutralinojs client version */ - NL_CVERSION: string; - /** Application identifier */ - NL_APPID: string; - /** Application version */ - NL_APPVERSION: string; - /** Application path */ - NL_PATH: string; - /** Application data path */ - NL_DATAPATH: string; - /** Returns true if extensions are enabled */ - NL_EXTENABLED: boolean; - /** Returns true if the client library is injected */ - NL_GINJECTED: boolean; - /** Returns true if globals are injected */ - NL_CINJECTED: boolean; - /** Operating system name: Linux, Windows, Darwin, FreeBSD, or Uknown */ - NL_OS: OperatingSystem; - /** CPU architecture: x64, arm, itanium, ia32, or unknown */ - NL_ARCH: Architecture; - /** Neutralinojs server version */ - NL_VERSION: string; - /** Current working directory */ - NL_CWD: string; - /** Identifier of the current process */ - NL_PID: string; - /** Source of application resources: bundle or directory */ - NL_RESMODE: string; - /** Release commit of the client library */ - NL_CCOMMIT: string; - /** An array of custom methods */ - NL_CMETHODS: string[]; - } - /** Neutralino global object for custom methods **/ - const Neutralino: any; -} - -declare namespace custom { - export { getMethods }; -} -declare namespace filesystem { - export { appendBinaryFile, appendFile, copy, createDirectory, createWatcher, getAbsolutePath, getJoinedPath, getNormalizedPath, getOpenedFileInfo, getPathParts, getPermissions, getRelativePath, getStats, getUnnormalizedPath, getWatchers, move, openFile, readBinaryFile, readDirectory, readFile, remove, removeWatcher, setPermissions, updateOpenedFile, writeBinaryFile, writeFile }; -} -declare namespace os { - export { execCommand, getEnv, getEnvs, getPath, getSpawnedProcesses, open$1 as open, setTray, showFolderDialog, showMessageBox, showNotification, showOpenDialog, showSaveDialog, spawnProcess, updateSpawnedProcess }; -} -declare namespace computer { - export { getArch, getCPUInfo, getDisplays, getKernelInfo, getMemoryInfo, getMousePosition, getOSInfo }; -} -declare namespace storage { - export { clear, getData, getKeys, removeData, setData }; -} -declare namespace debug { - export { log }; -} -declare namespace app { - export { broadcast, exit, getConfig, killProcess, readProcessInput, restartProcess, writeProcessError, writeProcessOutput }; -} -declare namespace window$1 { - export { beginDrag, center, create, exitFullScreen, focus$1 as focus, getPosition, getSize, getTitle, hide, isFullScreen, isMaximized, isMinimized, isVisible, maximize, minimize, move$1 as move, print$1 as print, setAlwaysOnTop, setBorderless, setDraggableRegion, setFullScreen, setIcon, setMainMenu, setSize, setTitle, show, snapshot, unmaximize, unminimize, unsetDraggableRegion }; -} -declare namespace events { - export { broadcast$1 as broadcast, dispatch, off, on }; -} -declare namespace extensions { - export { broadcast$2 as broadcast, dispatch$1 as dispatch, getStats$1 as getStats }; -} -declare namespace updater { - export { checkForUpdates, install }; -} -declare namespace clipboard { - export { clear$1 as clear, getFormat, readHTML, readImage, readText, writeHTML, writeImage, writeText }; -} -declare namespace resources { - export { extractDirectory, extractFile, getFiles, getStats$2 as getStats, readBinaryFile$1 as readBinaryFile, readFile$1 as readFile }; -} -declare namespace server { - export { getMounts, mount, unmount }; -} - -export { - Error$1 as Error, - Permissions$1 as Permissions, - Response$1 as Response, - app, - clipboard, - computer, - custom, - debug, - events, - extensions, - filesystem, - os, - resources, - server, - storage, - updater, - window$1 as window, -}; - -export as namespace Neutralino; - -export {}; diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js deleted file mode 100644 index dc6d741..0000000 --- a/desktop-app/resources/js/script.js +++ /dev/null @@ -1,1594 +0,0 @@ -document.addEventListener("DOMContentLoaded", function () { - let markdownRenderTimeout = null; - const RENDER_DELAY = 100; - let syncScrollingEnabled = true; - let isEditorScrolling = false; - let isPreviewScrolling = false; - let scrollSyncTimeout = null; - const SCROLL_SYNC_DELAY = 10; - - // View Mode State - Story 1.1 - let currentViewMode = 'split'; // 'editor', 'split', or 'preview' - - const markdownEditor = document.getElementById("markdown-editor"); - const markdownPreview = document.getElementById("markdown-preview"); - const themeToggle = document.getElementById("theme-toggle"); - const importButton = document.getElementById("import-button"); - const fileInput = document.getElementById("file-input"); - const exportMd = document.getElementById("export-md"); - const exportHtml = document.getElementById("export-html"); - const exportPdf = document.getElementById("export-pdf"); - const copyMarkdownButton = document.getElementById("copy-markdown-button"); - const dropzone = document.getElementById("dropzone"); - const closeDropzoneBtn = document.getElementById("close-dropzone"); - const toggleSyncButton = document.getElementById("toggle-sync"); - const editorPane = document.getElementById("markdown-editor"); - const previewPane = document.querySelector(".preview-pane"); - const readingTimeElement = document.getElementById("reading-time"); - const wordCountElement = document.getElementById("word-count"); - const charCountElement = document.getElementById("char-count"); - - // View Mode Elements - Story 1.1 - const contentContainer = document.querySelector(".content-container"); - const viewModeButtons = document.querySelectorAll(".view-mode-btn"); - - // Mobile View Mode Elements - Story 1.4 - const mobileViewModeButtons = document.querySelectorAll(".mobile-view-mode-btn"); - - // Resize Divider Elements - Story 1.3 - const resizeDivider = document.querySelector(".resize-divider"); - const editorPaneElement = document.querySelector(".editor-pane"); - const previewPaneElement = document.querySelector(".preview-pane"); - let isResizing = false; - let editorWidthPercent = 50; // Default 50% - const MIN_PANE_PERCENT = 20; // Minimum 20% width - - const mobileMenuToggle = document.getElementById("mobile-menu-toggle"); - const mobileMenuPanel = document.getElementById("mobile-menu-panel"); - const mobileMenuOverlay = document.getElementById("mobile-menu-overlay"); - const mobileCloseMenu = document.getElementById("close-mobile-menu"); - const mobileReadingTime = document.getElementById("mobile-reading-time"); - const mobileWordCount = document.getElementById("mobile-word-count"); - const mobileCharCount = document.getElementById("mobile-char-count"); - const mobileToggleSync = document.getElementById("mobile-toggle-sync"); - const mobileImportBtn = document.getElementById("mobile-import-button"); - const mobileExportMd = document.getElementById("mobile-export-md"); - const mobileExportHtml = document.getElementById("mobile-export-html"); - const mobileExportPdf = document.getElementById("mobile-export-pdf"); - const mobileCopyMarkdown = document.getElementById("mobile-copy-markdown"); - const mobileThemeToggle = document.getElementById("mobile-theme-toggle"); - - // Check dark mode preference first for proper initialization - const prefersDarkMode = - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches; - - document.documentElement.setAttribute( - "data-theme", - prefersDarkMode ? "dark" : "light" - ); - - themeToggle.innerHTML = prefersDarkMode - ? '' - : ''; - - const initMermaid = () => { - const currentTheme = document.documentElement.getAttribute("data-theme"); - const mermaidTheme = currentTheme === "dark" ? "dark" : "default"; - - mermaid.initialize({ - startOnLoad: false, - theme: mermaidTheme, - securityLevel: 'loose', - flowchart: { useMaxWidth: true, htmlLabels: true }, - fontSize: 16 - }); - }; - - try { - initMermaid(); - } catch (e) { - console.warn("Mermaid initialization failed:", e); - } - - const markedOptions = { - gfm: true, - breaks: false, - pedantic: false, - sanitize: false, - smartypants: false, - xhtml: false, - headerIds: true, - mangle: false, - }; - - const renderer = new marked.Renderer(); - renderer.code = function (code, language) { - if (language === 'mermaid') { - const uniqueId = 'mermaid-diagram-' + Math.random().toString(36).substr(2, 9); - return `
${code}
`; - } - - const validLanguage = hljs.getLanguage(language) ? language : "plaintext"; - const highlightedCode = hljs.highlight(code, { - language: validLanguage, - }).value; - return `
${highlightedCode}
`; - }; - - marked.setOptions({ - ...markedOptions, - renderer: renderer, - highlight: function (code, language) { - if (language === 'mermaid') return code; - const validLanguage = hljs.getLanguage(language) ? language : "plaintext"; - return hljs.highlight(code, { language: validLanguage }).value; - }, - }); - - const sampleMarkdown = `# Welcome to Markdown Viewer - -## ✨ Key Features -- **Live Preview** with GitHub styling -- **Smart Import/Export** (MD, HTML, PDF) -- **Mermaid Diagrams** for visual documentation -- **LaTeX Math Support** for scientific notation -- **Emoji Support** 😄 👍 🎉 - -## 💻 Code with Syntax Highlighting -\`\`\`javascript - function renderMarkdown() { - const markdown = markdownEditor.value; - const html = marked.parse(markdown); - const sanitizedHtml = DOMPurify.sanitize(html); - markdownPreview.innerHTML = sanitizedHtml; - - // Apply syntax highlighting to code blocks - markdownPreview.querySelectorAll('pre code').forEach((block) => { - hljs.highlightElement(block); - }); - } -\`\`\` - -## 🧮 Mathematical Expressions -Write complex formulas with LaTeX syntax: - -Inline equation: $$E = mc^2$$ - -Display equations: -$$\\frac{\\partial f}{\\partial x} = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}$$ - -$$\\sum_{i=1}^{n} i^2 = \\frac{n(n+1)(2n+1)}{6}$$ - -## 📊 Mermaid Diagrams -Create powerful visualizations directly in markdown: - -\`\`\`mermaid -flowchart LR - A[Start] --> B{Is it working?} - B -->|Yes| C[Great!] - B -->|No| D[Debug] - C --> E[Deploy] - D --> B -\`\`\` - -### Sequence Diagram Example -\`\`\`mermaid -sequenceDiagram - User->>Editor: Type markdown - Editor->>Preview: Render content - User->>Editor: Make changes - Editor->>Preview: Update rendering - User->>Export: Save as PDF -\`\`\` - -## 📋 Task Management -- [x] Create responsive layout -- [x] Implement live preview with GitHub styling -- [x] Add syntax highlighting for code blocks -- [x] Support math expressions with LaTeX -- [x] Enable mermaid diagrams - -## 🆚 Feature Comparison - -| Feature | Markdown Viewer (Ours) | Other Markdown Editors | -|:-------------------------|:----------------------:|:-----------------------:| -| Live Preview | ✅ GitHub-Styled | ✅ | -| Sync Scrolling | ✅ Two-way | 🔄 Partial/None | -| Mermaid Support | ✅ | ❌/Limited | -| LaTeX Math Rendering | ✅ | ❌/Limited | - -### 📝 Multi-row Headers Support - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Document TypeSupport
Markdown Viewer (Ours)Other Markdown Editors
Technical DocsFull + DiagramsLimited/Basic
Research NotesFull + MathPartial
Developer GuidesFull + Export OptionsBasic
- -## 📝 Text Formatting Examples - -### Text Formatting - -Text can be formatted in various ways for ~~strikethrough~~, **bold**, *italic*, or ***bold italic***. - -For highlighting important information, use highlighted text or add underlines where appropriate. - -### Superscript and Subscript - -Chemical formulas: H2O, CO2 -Mathematical notation: x2, e - -### Keyboard Keys - -Press Ctrl + B for bold text. - -### Abbreviations - -GUI -API - -### Text Alignment - -
-Centered text for headings or important notices -
- -
-Right-aligned text (for dates, signatures, etc.) -
- -### **Lists** - -Create bullet points: -* Item 1 -* Item 2 - * Nested item - * Nested further - -### **Links and Images** - -Add a [link](https://github.com/ThisIs-Developer/Markdown-Viewer) to important resources. - -Embed an image: -![Markdown Logo](https://example.com/logo.png) - -### **Blockquotes** - -Quote someone famous: -> "The best way to predict the future is to invent it." - Alan Kay - ---- - -## 🛡️ Security Note - -This is a fully client-side application. Your content never leaves your browser and stays secure on your device.`; - - markdownEditor.value = sampleMarkdown; - - function renderMarkdown() { - try { - const markdown = markdownEditor.value; - const html = marked.parse(markdown); - const sanitizedHtml = DOMPurify.sanitize(html, { - ADD_TAGS: ['mjx-container'], - ADD_ATTR: ['id', 'class', 'style'] - }); - markdownPreview.innerHTML = sanitizedHtml; - - markdownPreview.querySelectorAll("pre code").forEach((block) => { - try { - if (!block.classList.contains('mermaid')) { - hljs.highlightElement(block); - } - } catch (e) { - console.warn("Syntax highlighting failed for a code block:", e); - } - }); - - processEmojis(markdownPreview); - - // Reinitialize mermaid with current theme before rendering diagrams - initMermaid(); - - try { - mermaid.init(undefined, markdownPreview.querySelectorAll('.mermaid')); - } catch (e) { - console.warn("Mermaid rendering failed:", e); - } - - if (window.MathJax) { - try { - MathJax.typesetPromise([markdownPreview]).catch((err) => { - console.warn('MathJax typesetting failed:', err); - }); - } catch (e) { - console.warn("MathJax rendering failed:", e); - } - } - - updateDocumentStats(); - } catch (e) { - console.error("Markdown rendering failed:", e); - markdownPreview.innerHTML = `
- Error rendering markdown: ${e.message} -
-
${markdownEditor.value}
`; - } - } - - function importMarkdownFile(file) { - const reader = new FileReader(); - reader.onload = function(e) { - markdownEditor.value = e.target.result; - renderMarkdown(); - dropzone.style.display = "none"; - }; - reader.readAsText(file); - } - - function processEmojis(element) { - const walker = document.createTreeWalker( - element, - NodeFilter.SHOW_TEXT, - null, - false - ); - - const textNodes = []; - let node; - while ((node = walker.nextNode())) { - let parent = node.parentNode; - let isInCode = false; - while (parent && parent !== element) { - if (parent.tagName === 'PRE' || parent.tagName === 'CODE') { - isInCode = true; - break; - } - parent = parent.parentNode; - } - - if (!isInCode && node.nodeValue.includes(':')) { - textNodes.push(node); - } - } - - textNodes.forEach(textNode => { - const text = textNode.nodeValue; - const emojiRegex = /:([\w+-]+):/g; - - let match; - let lastIndex = 0; - let result = ''; - let hasEmoji = false; - - while ((match = emojiRegex.exec(text)) !== null) { - const shortcode = match[1]; - const emoji = joypixels.shortnameToUnicode(`:${shortcode}:`); - - if (emoji !== `:${shortcode}:`) { // If conversion was successful - hasEmoji = true; - result += text.substring(lastIndex, match.index) + emoji; - lastIndex = emojiRegex.lastIndex; - } else { - result += text.substring(lastIndex, emojiRegex.lastIndex); - lastIndex = emojiRegex.lastIndex; - } - } - - if (hasEmoji) { - result += text.substring(lastIndex); - const span = document.createElement('span'); - span.innerHTML = result; - textNode.parentNode.replaceChild(span, textNode); - } - }); - } - - function debouncedRender() { - clearTimeout(markdownRenderTimeout); - markdownRenderTimeout = setTimeout(renderMarkdown, RENDER_DELAY); - } - - function updateDocumentStats() { - const text = markdownEditor.value; - - const charCount = text.length; - charCountElement.textContent = charCount.toLocaleString(); - - const wordCount = text.trim() === "" ? 0 : text.trim().split(/\s+/).length; - wordCountElement.textContent = wordCount.toLocaleString(); - - const readingTimeMinutes = Math.ceil(wordCount / 200); - readingTimeElement.textContent = readingTimeMinutes; - } - - function syncEditorToPreview() { - if (!syncScrollingEnabled || isPreviewScrolling) return; - - isEditorScrolling = true; - clearTimeout(scrollSyncTimeout); - - scrollSyncTimeout = setTimeout(() => { - const editorScrollRatio = - editorPane.scrollTop / - (editorPane.scrollHeight - editorPane.clientHeight); - const previewScrollPosition = - (previewPane.scrollHeight - previewPane.clientHeight) * - editorScrollRatio; - - if (!isNaN(previewScrollPosition) && isFinite(previewScrollPosition)) { - previewPane.scrollTop = previewScrollPosition; - } - - setTimeout(() => { - isEditorScrolling = false; - }, 50); - }, SCROLL_SYNC_DELAY); - } - - function syncPreviewToEditor() { - if (!syncScrollingEnabled || isEditorScrolling) return; - - isPreviewScrolling = true; - clearTimeout(scrollSyncTimeout); - - scrollSyncTimeout = setTimeout(() => { - const previewScrollRatio = - previewPane.scrollTop / - (previewPane.scrollHeight - previewPane.clientHeight); - const editorScrollPosition = - (editorPane.scrollHeight - editorPane.clientHeight) * - previewScrollRatio; - - if (!isNaN(editorScrollPosition) && isFinite(editorScrollPosition)) { - editorPane.scrollTop = editorScrollPosition; - } - - setTimeout(() => { - isPreviewScrolling = false; - }, 50); - }, SCROLL_SYNC_DELAY); - } - - function toggleSyncScrolling() { - syncScrollingEnabled = !syncScrollingEnabled; - if (syncScrollingEnabled) { - toggleSyncButton.innerHTML = ' Sync Off'; - toggleSyncButton.classList.add("sync-disabled"); - toggleSyncButton.classList.remove("sync-enabled"); - toggleSyncButton.classList.add("border-primary"); - } else { - toggleSyncButton.innerHTML = ' Sync On'; - toggleSyncButton.classList.add("sync-enabled"); - toggleSyncButton.classList.remove("sync-disabled"); - toggleSyncButton.classList.remove("border-primary"); - } - } - - // View Mode Functions - Story 1.1 & 1.2 - function setViewMode(mode) { - if (mode === currentViewMode) return; - - const previousMode = currentViewMode; - currentViewMode = mode; - - // Update content container class - contentContainer.classList.remove('view-editor-only', 'view-preview-only', 'view-split'); - contentContainer.classList.add('view-' + (mode === 'editor' ? 'editor-only' : mode === 'preview' ? 'preview-only' : 'split')); - - // Update button active states (desktop) - viewModeButtons.forEach(btn => { - const btnMode = btn.getAttribute('data-mode'); - if (btnMode === mode) { - btn.classList.add('active'); - btn.setAttribute('aria-pressed', 'true'); - } else { - btn.classList.remove('active'); - btn.setAttribute('aria-pressed', 'false'); - } - }); - - // Story 1.4: Update mobile button active states - mobileViewModeButtons.forEach(btn => { - const btnMode = btn.getAttribute('data-mode'); - if (btnMode === mode) { - btn.classList.add('active'); - btn.setAttribute('aria-pressed', 'true'); - } else { - btn.classList.remove('active'); - btn.setAttribute('aria-pressed', 'false'); - } - }); - - // Story 1.2: Show/hide sync toggle based on view mode - updateSyncToggleVisibility(mode); - - // Story 1.3: Handle pane widths when switching modes - if (mode === 'split') { - // Restore preserved pane widths when entering split mode - applyPaneWidths(); - } else if (previousMode === 'split') { - // Reset pane widths when leaving split mode - resetPaneWidths(); - } - - // Re-render markdown when switching to a view that includes preview - if (mode === 'split' || mode === 'preview') { - renderMarkdown(); - } - } - - // Story 1.2: Update sync toggle visibility - function updateSyncToggleVisibility(mode) { - const isSplitView = mode === 'split'; - - // Desktop sync toggle - if (toggleSyncButton) { - toggleSyncButton.style.display = isSplitView ? '' : 'none'; - toggleSyncButton.setAttribute('aria-hidden', !isSplitView); - } - - // Mobile sync toggle - if (mobileToggleSync) { - mobileToggleSync.style.display = isSplitView ? '' : 'none'; - mobileToggleSync.setAttribute('aria-hidden', !isSplitView); - } - } - - // Story 1.3: Resize Divider Functions - function initResizer() { - if (!resizeDivider) return; - - resizeDivider.addEventListener('mousedown', startResize); - document.addEventListener('mousemove', handleResize); - document.addEventListener('mouseup', stopResize); - - // Touch support for tablets (though disabled via CSS, keeping for future) - resizeDivider.addEventListener('touchstart', startResizeTouch); - document.addEventListener('touchmove', handleResizeTouch); - document.addEventListener('touchend', stopResize); - } - - function startResize(e) { - if (currentViewMode !== 'split') return; - e.preventDefault(); - isResizing = true; - resizeDivider.classList.add('dragging'); - document.body.classList.add('resizing'); - } - - function startResizeTouch(e) { - if (currentViewMode !== 'split') return; - e.preventDefault(); - isResizing = true; - resizeDivider.classList.add('dragging'); - document.body.classList.add('resizing'); - } - - function handleResize(e) { - if (!isResizing) return; - - const containerRect = contentContainer.getBoundingClientRect(); - const containerWidth = containerRect.width; - const mouseX = e.clientX - containerRect.left; - - // Calculate percentage - let newEditorPercent = (mouseX / containerWidth) * 100; - - // Enforce minimum pane widths - newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent)); - - editorWidthPercent = newEditorPercent; - applyPaneWidths(); - } - - function handleResizeTouch(e) { - if (!isResizing || !e.touches[0]) return; - - const containerRect = contentContainer.getBoundingClientRect(); - const containerWidth = containerRect.width; - const touchX = e.touches[0].clientX - containerRect.left; - - let newEditorPercent = (touchX / containerWidth) * 100; - newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent)); - - editorWidthPercent = newEditorPercent; - applyPaneWidths(); - } - - function stopResize() { - if (!isResizing) return; - isResizing = false; - resizeDivider.classList.remove('dragging'); - document.body.classList.remove('resizing'); - } - - function applyPaneWidths() { - if (currentViewMode !== 'split') return; - - const previewPercent = 100 - editorWidthPercent; - editorPaneElement.style.flex = `0 0 calc(${editorWidthPercent}% - 4px)`; - previewPaneElement.style.flex = `0 0 calc(${previewPercent}% - 4px)`; - } - - function resetPaneWidths() { - editorPaneElement.style.flex = ''; - previewPaneElement.style.flex = ''; - } - - function openMobileMenu() { - mobileMenuPanel.classList.add("active"); - mobileMenuOverlay.classList.add("active"); - } - function closeMobileMenu() { - mobileMenuPanel.classList.remove("active"); - mobileMenuOverlay.classList.remove("active"); - } - mobileMenuToggle.addEventListener("click", openMobileMenu); - mobileCloseMenu.addEventListener("click", closeMobileMenu); - mobileMenuOverlay.addEventListener("click", closeMobileMenu); - - function updateMobileStats() { - mobileCharCount.textContent = charCountElement.textContent; - mobileWordCount.textContent = wordCountElement.textContent; - mobileReadingTime.textContent = readingTimeElement.textContent; - } - - const origUpdateStats = updateDocumentStats; - updateDocumentStats = function() { - origUpdateStats(); - updateMobileStats(); - }; - - mobileToggleSync.addEventListener("click", () => { - toggleSyncScrolling(); - if (syncScrollingEnabled) { - mobileToggleSync.innerHTML = ' Sync Off'; - mobileToggleSync.classList.add("sync-disabled"); - mobileToggleSync.classList.remove("sync-enabled"); - mobileToggleSync.classList.add("border-primary"); - } else { - mobileToggleSync.innerHTML = ' Sync On'; - mobileToggleSync.classList.add("sync-enabled"); - mobileToggleSync.classList.remove("sync-disabled"); - mobileToggleSync.classList.remove("border-primary"); - } - }); - mobileImportBtn.addEventListener("click", () => fileInput.click()); - mobileExportMd.addEventListener("click", () => exportMd.click()); - mobileExportHtml.addEventListener("click", () => exportHtml.click()); - mobileExportPdf.addEventListener("click", () => exportPdf.click()); - mobileCopyMarkdown.addEventListener("click", () => copyMarkdownButton.click()); - mobileThemeToggle.addEventListener("click", () => { - themeToggle.click(); - mobileThemeToggle.innerHTML = themeToggle.innerHTML + " Toggle Dark Mode"; - }); - - renderMarkdown(); - updateMobileStats(); - - // Initialize view mode - Story 1.1 - contentContainer.classList.add('view-split'); - - // Initialize resizer - Story 1.3 - initResizer(); - - // View Mode Button Event Listeners - Story 1.1 - viewModeButtons.forEach(btn => { - btn.addEventListener('click', function() { - const mode = this.getAttribute('data-mode'); - setViewMode(mode); - }); - }); - - // Story 1.4: Mobile View Mode Button Event Listeners - mobileViewModeButtons.forEach(btn => { - btn.addEventListener('click', function() { - const mode = this.getAttribute('data-mode'); - setViewMode(mode); - closeMobileMenu(); - }); - }); - - markdownEditor.addEventListener("input", debouncedRender); - - // Tab key handler to insert indentation instead of moving focus - markdownEditor.addEventListener("keydown", function(e) { - if (e.key === 'Tab') { - e.preventDefault(); - - const start = this.selectionStart; - const end = this.selectionEnd; - const value = this.value; - - // Insert 2 spaces - const indent = ' '; // 2 spaces - - // Update textarea value - this.value = value.substring(0, start) + indent + value.substring(end); - - // Update cursor position - this.selectionStart = this.selectionEnd = start + indent.length; - - // Trigger input event to update preview - this.dispatchEvent(new Event('input')); - } - }); - - editorPane.addEventListener("scroll", syncEditorToPreview); - previewPane.addEventListener("scroll", syncPreviewToEditor); - toggleSyncButton.addEventListener("click", toggleSyncScrolling); - themeToggle.addEventListener("click", function () { - const theme = - document.documentElement.getAttribute("data-theme") === "dark" - ? "light" - : "dark"; - document.documentElement.setAttribute("data-theme", theme); - - if (theme === "dark") { - themeToggle.innerHTML = ''; - } else { - themeToggle.innerHTML = ''; - } - - renderMarkdown(); - }); - - importButton.addEventListener("click", function () { - fileInput.click(); - }); - - fileInput.addEventListener("change", function (e) { - const file = e.target.files[0]; - if (file) { - importMarkdownFile(file); - } - this.value = ""; - }); - - exportMd.addEventListener("click", function () { - try { - const blob = new Blob([markdownEditor.value], { - type: "text/markdown;charset=utf-8", - }); - saveAs(blob, "document.md"); - } catch (e) { - console.error("Export failed:", e); - alert("Export failed: " + e.message); - } - }); - - exportHtml.addEventListener("click", function () { - try { - const markdown = markdownEditor.value; - const html = marked.parse(markdown); - const sanitizedHtml = DOMPurify.sanitize(html, { - ADD_TAGS: ['mjx-container'], - ADD_ATTR: ['id', 'class', 'style'] - }); - const isDarkTheme = - document.documentElement.getAttribute("data-theme") === "dark"; - const cssTheme = isDarkTheme - ? "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown-dark.min.css" - : "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css"; - const fullHtml = ` - - - - - Markdown Export - - - - - -
- ${sanitizedHtml} -
- -`; - const blob = new Blob([fullHtml], { type: "text/html;charset=utf-8" }); - saveAs(blob, "document.html"); - } catch (e) { - console.error("HTML export failed:", e); - alert("HTML export failed: " + e.message); - } - }); - - // ============================================ - // Page-Break Detection Functions (Story 1.1) - // ============================================ - - // Page configuration constants for A4 PDF export - const PAGE_CONFIG = { - a4Width: 210, // mm - a4Height: 297, // mm - margin: 15, // mm each side - contentWidth: 180, // 210 - 30 (margins) - contentHeight: 267, // 297 - 30 (margins) - windowWidth: 1000, // html2canvas config - scale: 2 // html2canvas scale factor - }; - - /** - * Task 1: Identifies all graphic elements that may need page-break handling - * @param {HTMLElement} container - The container element to search within - * @returns {Array} Array of {element, type} objects - */ - function identifyGraphicElements(container) { - const graphics = []; - - // Query for images - container.querySelectorAll('img').forEach(el => { - graphics.push({ element: el, type: 'img' }); - }); - - // Query for SVGs (Mermaid diagrams) - container.querySelectorAll('svg').forEach(el => { - graphics.push({ element: el, type: 'svg' }); - }); - - // Query for pre elements (code blocks) - container.querySelectorAll('pre').forEach(el => { - graphics.push({ element: el, type: 'pre' }); - }); - - // Query for tables - container.querySelectorAll('table').forEach(el => { - graphics.push({ element: el, type: 'table' }); - }); - - return graphics; - } - - /** - * Task 2: Calculates element positions relative to the container - * @param {Array} elements - Array of {element, type} objects - * @param {HTMLElement} container - The container element - * @returns {Array} Array with position data added - */ - function calculateElementPositions(elements, container) { - const containerRect = container.getBoundingClientRect(); - - return elements.map(item => { - const rect = item.element.getBoundingClientRect(); - const top = rect.top - containerRect.top; - const height = rect.height; - const bottom = top + height; - - return { - element: item.element, - type: item.type, - top: top, - height: height, - bottom: bottom - }; - }); - } - - /** - * Task 3: Calculates page boundary positions - * @param {number} totalHeight - Total height of content in pixels - * @param {number} elementWidth - Actual width of the rendered element in pixels - * @param {Object} pageConfig - Page configuration object - * @returns {Array} Array of y-coordinates where pages end - */ - function calculatePageBoundaries(totalHeight, elementWidth, pageConfig) { - // Calculate pixel height per page based on the element's actual width - // This must match how PDF pagination will split the canvas - // The aspect ratio of content area determines page height relative to width - const aspectRatio = pageConfig.contentHeight / pageConfig.contentWidth; - const pageHeightPx = elementWidth * aspectRatio; - - const boundaries = []; - let y = pageHeightPx; - - while (y < totalHeight) { - boundaries.push(y); - y += pageHeightPx; - } - - return { boundaries, pageHeightPx }; - } - - /** - * Task 4: Detects which elements would be split across page boundaries - * @param {Array} elements - Array of elements with position data - * @param {Array} pageBoundaries - Array of page break y-coordinates - * @returns {Array} Array of split elements with additional split info - */ - function detectSplitElements(elements, pageBoundaries) { - // Handle edge case: empty elements array - if (!elements || elements.length === 0) { - return []; - } - - // Handle edge case: no page boundaries (single page) - if (!pageBoundaries || pageBoundaries.length === 0) { - return []; - } - - const splitElements = []; - - for (const item of elements) { - // Find which page the element starts on - let startPage = 0; - for (let i = 0; i < pageBoundaries.length; i++) { - if (item.top >= pageBoundaries[i]) { - startPage = i + 1; - } else { - break; - } - } - - // Find which page the element ends on - let endPage = 0; - for (let i = 0; i < pageBoundaries.length; i++) { - if (item.bottom > pageBoundaries[i]) { - endPage = i + 1; - } else { - break; - } - } - - // Element is split if it spans multiple pages - if (endPage > startPage) { - // Calculate overflow amount (how much crosses into next page) - const boundaryY = pageBoundaries[startPage] || pageBoundaries[0]; - const overflowAmount = item.bottom - boundaryY; - - splitElements.push({ - element: item.element, - type: item.type, - top: item.top, - height: item.height, - splitPageIndex: startPage, - overflowAmount: overflowAmount - }); - } - } - - return splitElements; - } - - /** - * Task 5: Main entry point for analyzing graphics for page breaks - * @param {HTMLElement} tempElement - The rendered content container - * @returns {Object} Analysis result with totalElements, splitElements, pageCount - */ - function analyzeGraphicsForPageBreaks(tempElement) { - try { - // Step 1: Identify all graphic elements - const graphics = identifyGraphicElements(tempElement); - console.log('Step 1 - Graphics found:', graphics.length, graphics.map(g => g.type)); - - // Step 2: Calculate positions for each element - const elementsWithPositions = calculateElementPositions(graphics, tempElement); - console.log('Step 2 - Element positions:', elementsWithPositions.map(e => ({ - type: e.type, - top: Math.round(e.top), - height: Math.round(e.height), - bottom: Math.round(e.bottom) - }))); - - // Step 3: Calculate page boundaries using the element's ACTUAL width - const totalHeight = tempElement.scrollHeight; - const elementWidth = tempElement.offsetWidth; - const { boundaries: pageBoundaries, pageHeightPx } = calculatePageBoundaries( - totalHeight, - elementWidth, - PAGE_CONFIG - ); - - console.log('Step 3 - Page boundaries:', { - elementWidth, - totalHeight, - pageHeightPx: Math.round(pageHeightPx), - boundaries: pageBoundaries.map(b => Math.round(b)) - }); - - // Step 4: Detect split elements - const splitElements = detectSplitElements(elementsWithPositions, pageBoundaries); - console.log('Step 4 - Split elements detected:', splitElements.length); - - // Calculate page count - const pageCount = pageBoundaries.length + 1; - - return { - totalElements: graphics.length, - splitElements: splitElements, - pageCount: pageCount, - pageBoundaries: pageBoundaries, - pageHeightPx: pageHeightPx - }; - } catch (error) { - console.error('Page-break analysis failed:', error); - return { - totalElements: 0, - splitElements: [], - pageCount: 1, - pageBoundaries: [], - pageHeightPx: 0 - }; - } - } - - // ============================================ - // End Page-Break Detection Functions - // ============================================ - - // ============================================ - // Page-Break Insertion Functions (Story 1.2) - // ============================================ - - // Threshold for whitespace optimization (30% of page height) - const PAGE_BREAK_THRESHOLD = 0.3; - - /** - * Task 3: Categorizes split elements by whether they fit on a single page - * @param {Array} splitElements - Array of split elements from detection - * @param {number} pageHeightPx - Page height in pixels - * @returns {Object} { fittingElements, oversizedElements } - */ - function categorizeBySize(splitElements, pageHeightPx) { - const fittingElements = []; - const oversizedElements = []; - - for (const item of splitElements) { - if (item.height <= pageHeightPx) { - fittingElements.push(item); - } else { - oversizedElements.push(item); - } - } - - return { fittingElements, oversizedElements }; - } - - /** - * Task 1: Inserts page breaks by adjusting margins for fitting elements - * @param {Array} fittingElements - Elements that fit on a single page - * @param {number} pageHeightPx - Page height in pixels - */ - function insertPageBreaks(fittingElements, pageHeightPx) { - for (const item of fittingElements) { - // Calculate where the current page ends - const currentPageBottom = (item.splitPageIndex + 1) * pageHeightPx; - - // Calculate remaining space on current page - const remainingSpace = currentPageBottom - item.top; - const remainingRatio = remainingSpace / pageHeightPx; - - console.log('Processing split element:', { - type: item.type, - top: Math.round(item.top), - height: Math.round(item.height), - splitPageIndex: item.splitPageIndex, - currentPageBottom: Math.round(currentPageBottom), - remainingSpace: Math.round(remainingSpace), - remainingRatio: remainingRatio.toFixed(2) - }); - - // Task 4: Whitespace optimization - // If remaining space is more than threshold and element almost fits, skip - // (Will be handled by Story 1.3 scaling instead) - if (remainingRatio > PAGE_BREAK_THRESHOLD) { - const scaledHeight = item.height * 0.9; // 90% scale - if (scaledHeight <= remainingSpace) { - console.log(' -> Skipping (can fit with 90% scaling)'); - continue; - } - } - - // Calculate margin needed to push element to next page - const marginNeeded = currentPageBottom - item.top + 5; // 5px buffer - - console.log(' -> Applying marginTop:', marginNeeded, 'px'); - - // Determine which element to apply margin to - // For SVG elements (Mermaid diagrams), apply to parent container for proper layout - let targetElement = item.element; - if (item.type === 'svg' && item.element.parentElement) { - targetElement = item.element.parentElement; - console.log(' -> Using parent element:', targetElement.tagName, targetElement.className); - } - - // Apply margin to push element to next page - const currentMargin = parseFloat(targetElement.style.marginTop) || 0; - targetElement.style.marginTop = `${currentMargin + marginNeeded}px`; - - console.log(' -> Element after margin:', targetElement.tagName, 'marginTop =', targetElement.style.marginTop); - } - } - - /** - * Task 2: Applies page breaks with cascading adjustment handling - * @param {HTMLElement} tempElement - The rendered content container - * @param {Object} pageConfig - Page configuration object (unused, kept for API compatibility) - * @param {number} maxIterations - Maximum iterations to prevent infinite loops - * @returns {Object} Final analysis result - */ - function applyPageBreaksWithCascade(tempElement, pageConfig, maxIterations = 10) { - let iteration = 0; - let analysis; - let previousSplitCount = -1; - - do { - // Re-analyze after each adjustment - analysis = analyzeGraphicsForPageBreaks(tempElement); - - // Use pageHeightPx from analysis (calculated from actual element width) - const pageHeightPx = analysis.pageHeightPx; - - // Categorize elements by size - const { fittingElements, oversizedElements } = categorizeBySize( - analysis.splitElements, - pageHeightPx - ); - - // Store oversized elements for Story 1.3 - analysis.oversizedElements = oversizedElements; - - // If no fitting elements need adjustment, we're done - if (fittingElements.length === 0) { - break; - } - - // Check if we're making progress (prevent infinite loops) - if (fittingElements.length === previousSplitCount) { - console.warn('Page-break adjustment not making progress, stopping'); - break; - } - previousSplitCount = fittingElements.length; - - // Apply page breaks to fitting elements - insertPageBreaks(fittingElements, pageHeightPx); - iteration++; - - } while (iteration < maxIterations); - - if (iteration >= maxIterations) { - console.warn('Page-break stabilization reached max iterations:', maxIterations); - } - - console.log('Page-break cascade complete:', { - iterations: iteration, - finalSplitCount: analysis.splitElements.length, - oversizedCount: analysis.oversizedElements ? analysis.oversizedElements.length : 0 - }); - - return analysis; - } - - // ============================================ - // End Page-Break Insertion Functions - // ============================================ - - // ============================================ - // Oversized Graphics Scaling Functions (Story 1.3) - // ============================================ - - // Minimum scale factor to maintain readability (50%) - const MIN_SCALE_FACTOR = 0.5; - - /** - * Task 1 & 2: Calculates scale factor with minimum enforcement - * @param {number} elementHeight - Original height of element in pixels - * @param {number} availableHeight - Available page height in pixels - * @param {number} buffer - Small buffer to prevent edge overflow - * @returns {Object} { scaleFactor, wasClampedToMin } - */ - function calculateScaleFactor(elementHeight, availableHeight, buffer = 5) { - const targetHeight = availableHeight - buffer; - let scaleFactor = targetHeight / elementHeight; - let wasClampedToMin = false; - - // Enforce minimum scale for readability - if (scaleFactor < MIN_SCALE_FACTOR) { - console.warn( - `Warning: Large graphic requires ${(scaleFactor * 100).toFixed(0)}% scaling. ` + - `Clamping to minimum ${MIN_SCALE_FACTOR * 100}%. Content may be cut off.` - ); - scaleFactor = MIN_SCALE_FACTOR; - wasClampedToMin = true; - } - - return { scaleFactor, wasClampedToMin }; - } - - /** - * Task 3: Applies CSS transform scaling to an element - * @param {HTMLElement} element - The element to scale - * @param {number} scaleFactor - Scale factor (0.5 = 50%) - * @param {string} elementType - Type of element (svg, pre, img, table) - */ - function applyGraphicScaling(element, scaleFactor, elementType) { - // Get original dimensions before transform - const originalHeight = element.offsetHeight; - - // Task 4: Handle SVG elements (Mermaid diagrams) - if (elementType === 'svg') { - // Remove max-width constraint that may interfere - element.style.maxWidth = 'none'; - } - - // Apply CSS transform - element.style.transform = `scale(${scaleFactor})`; - element.style.transformOrigin = 'top left'; - - // Calculate margin adjustment to collapse visual space - const scaledHeight = originalHeight * scaleFactor; - const marginAdjustment = originalHeight - scaledHeight; - - // Apply negative margin to pull subsequent content up - element.style.marginBottom = `-${marginAdjustment}px`; - } - - /** - * Task 6: Handles all oversized elements by applying appropriate scaling - * @param {Array} oversizedElements - Array of oversized element data - * @param {number} pageHeightPx - Page height in pixels - */ - function handleOversizedElements(oversizedElements, pageHeightPx) { - if (!oversizedElements || oversizedElements.length === 0) { - return; - } - - let scaledCount = 0; - let clampedCount = 0; - - for (const item of oversizedElements) { - // Calculate required scale factor - const { scaleFactor, wasClampedToMin } = calculateScaleFactor( - item.height, - pageHeightPx - ); - - // Apply scaling to the element - applyGraphicScaling(item.element, scaleFactor, item.type); - - scaledCount++; - if (wasClampedToMin) { - clampedCount++; - } - } - - console.log('Oversized graphics scaling complete:', { - totalScaled: scaledCount, - clampedToMinimum: clampedCount - }); - } - - // ============================================ - // End Oversized Graphics Scaling Functions - // ============================================ - - exportPdf.addEventListener("click", async function () { - try { - const originalText = exportPdf.innerHTML; - exportPdf.innerHTML = ' Generating...'; - exportPdf.disabled = true; - - const progressContainer = document.createElement('div'); - progressContainer.style.position = 'fixed'; - progressContainer.style.top = '50%'; - progressContainer.style.left = '50%'; - progressContainer.style.transform = 'translate(-50%, -50%)'; - progressContainer.style.padding = '15px 20px'; - progressContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; - progressContainer.style.color = 'white'; - progressContainer.style.borderRadius = '5px'; - progressContainer.style.zIndex = '9999'; - progressContainer.style.textAlign = 'center'; - - const statusText = document.createElement('div'); - statusText.textContent = 'Generating PDF...'; - progressContainer.appendChild(statusText); - document.body.appendChild(progressContainer); - - const markdown = markdownEditor.value; - const html = marked.parse(markdown); - const sanitizedHtml = DOMPurify.sanitize(html, { - ADD_TAGS: ['mjx-container', 'svg', 'path', 'g', 'marker', 'defs', 'pattern', 'clipPath'], - ADD_ATTR: ['id', 'class', 'style', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start'] - }); - - const tempElement = document.createElement("div"); - tempElement.className = "markdown-body pdf-export"; - tempElement.innerHTML = sanitizedHtml; - tempElement.style.padding = "20px"; - tempElement.style.width = "210mm"; - tempElement.style.margin = "0 auto"; - tempElement.style.fontSize = "14px"; - tempElement.style.position = "fixed"; - tempElement.style.left = "-9999px"; - tempElement.style.top = "0"; - - const currentTheme = document.documentElement.getAttribute("data-theme"); - tempElement.style.backgroundColor = currentTheme === "dark" ? "#0d1117" : "#ffffff"; - tempElement.style.color = currentTheme === "dark" ? "#c9d1d9" : "#24292e"; - - document.body.appendChild(tempElement); - - await new Promise(resolve => setTimeout(resolve, 200)); - - try { - await mermaid.run({ - nodes: tempElement.querySelectorAll('.mermaid'), - suppressErrors: true - }); - } catch (mermaidError) { - console.warn("Mermaid rendering issue:", mermaidError); - } - - if (window.MathJax) { - try { - await MathJax.typesetPromise([tempElement]); - } catch (mathJaxError) { - console.warn("MathJax rendering issue:", mathJaxError); - } - - // Hide MathJax assistive elements that cause duplicate text in PDF - // These are screen reader elements that html2canvas captures as visible - // Use multiple CSS properties to ensure html2canvas doesn't render them - const assistiveElements = tempElement.querySelectorAll('mjx-assistive-mml'); - assistiveElements.forEach(el => { - el.style.display = 'none'; - el.style.visibility = 'hidden'; - el.style.position = 'absolute'; - el.style.width = '0'; - el.style.height = '0'; - el.style.overflow = 'hidden'; - el.remove(); // Remove entirely from DOM - }); - - // Also hide any MathJax script elements that might contain source - const mathScripts = tempElement.querySelectorAll('script[type*="math"], script[type*="tex"]'); - mathScripts.forEach(el => el.remove()); - } - - await new Promise(resolve => setTimeout(resolve, 500)); - - // Analyze and apply page-breaks for graphics (Story 1.1 + 1.2) - const pageBreakAnalysis = applyPageBreaksWithCascade(tempElement, PAGE_CONFIG); - - // Scale oversized graphics that can't fit on a single page (Story 1.3) - if (pageBreakAnalysis.oversizedElements && pageBreakAnalysis.pageHeightPx) { - handleOversizedElements(pageBreakAnalysis.oversizedElements, pageBreakAnalysis.pageHeightPx); - } - - const pdfOptions = { - orientation: 'portrait', - unit: 'mm', - format: 'a4', - compress: true, - hotfixes: ["px_scaling"] - }; - - const pdf = new jspdf.jsPDF(pdfOptions); - const pageWidth = pdf.internal.pageSize.getWidth(); - const pageHeight = pdf.internal.pageSize.getHeight(); - const margin = 15; - const contentWidth = pageWidth - (margin * 2); - - const canvas = await html2canvas(tempElement, { - scale: 2, - useCORS: true, - allowTaint: true, - logging: false, - windowWidth: 1000, - windowHeight: tempElement.scrollHeight - }); - - const scaleFactor = canvas.width / contentWidth; - const imgHeight = canvas.height / scaleFactor; - const pagesCount = Math.ceil(imgHeight / (pageHeight - margin * 2)); - - for (let page = 0; page < pagesCount; page++) { - if (page > 0) pdf.addPage(); - - const sourceY = page * (pageHeight - margin * 2) * scaleFactor; - const sourceHeight = Math.min(canvas.height - sourceY, (pageHeight - margin * 2) * scaleFactor); - const destHeight = sourceHeight / scaleFactor; - - const pageCanvas = document.createElement('canvas'); - pageCanvas.width = canvas.width; - pageCanvas.height = sourceHeight; - - const ctx = pageCanvas.getContext('2d'); - ctx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight); - - const imgData = pageCanvas.toDataURL('image/png'); - pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight); - } - - pdf.save("document.pdf"); - - statusText.textContent = 'Download successful!'; - setTimeout(() => { - document.body.removeChild(progressContainer); - }, 1500); - - document.body.removeChild(tempElement); - exportPdf.innerHTML = originalText; - exportPdf.disabled = false; - - } catch (error) { - console.error("PDF export failed:", error); - alert("PDF export failed: " + error.message); - exportPdf.innerHTML = ' Export'; - exportPdf.disabled = false; - - const progressContainer = document.querySelector('div[style*="Preparing PDF"]'); - if (progressContainer) { - document.body.removeChild(progressContainer); - } - } - }); - - copyMarkdownButton.addEventListener("click", function () { - try { - const markdownText = markdownEditor.value; - copyToClipboard(markdownText); - } catch (e) { - console.error("Copy failed:", e); - alert("Failed to copy Markdown: " + e.message); - } - }); - - async function copyToClipboard(text) { - try { - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(text); - showCopiedMessage(); - } else { - const textArea = document.createElement("textarea"); - textArea.value = text; - textArea.style.position = "fixed"; - textArea.style.opacity = "0"; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - const successful = document.execCommand("copy"); - document.body.removeChild(textArea); - if (successful) { - showCopiedMessage(); - } else { - throw new Error("Copy command was unsuccessful"); - } - } - } catch (err) { - console.error("Copy failed:", err); - alert("Failed to copy HTML: " + err.message); - } - } - - function showCopiedMessage() { - const originalText = copyMarkdownButton.innerHTML; - copyMarkdownButton.innerHTML = ' Copied!'; - - setTimeout(() => { - copyMarkdownButton.innerHTML = originalText; - }, 2000); - } - - const dropEvents = ["dragenter", "dragover", "dragleave", "drop"]; - - dropEvents.forEach((eventName) => { - dropzone.addEventListener(eventName, preventDefaults, false); - document.body.addEventListener(eventName, preventDefaults, false); - }); - - function preventDefaults(e) { - e.preventDefault(); - e.stopPropagation(); - } - - ["dragenter", "dragover"].forEach((eventName) => { - dropzone.addEventListener(eventName, highlight, false); - }); - - ["dragleave", "drop"].forEach((eventName) => { - dropzone.addEventListener(eventName, unhighlight, false); - }); - - function highlight() { - dropzone.classList.add("active"); - } - - function unhighlight() { - dropzone.classList.remove("active"); - } - - dropzone.addEventListener("drop", handleDrop, false); - dropzone.addEventListener("click", function (e) { - if (e.target !== closeDropzoneBtn && !closeDropzoneBtn.contains(e.target)) { - fileInput.click(); - } - }); - closeDropzoneBtn.addEventListener("click", function(e) { - e.stopPropagation(); - dropzone.style.display = "none"; - }); - - function handleDrop(e) { - const dt = e.dataTransfer; - const files = dt.files; - if (files.length) { - const file = files[0]; - const isMarkdownFile = - file.type === "text/markdown" || - file.name.endsWith(".md") || - file.name.endsWith(".markdown"); - if (isMarkdownFile) { - importMarkdownFile(file); - } else { - alert("Please upload a Markdown file (.md or .markdown)"); - } - } - } - - document.addEventListener("keydown", function (e) { - if ((e.ctrlKey || e.metaKey) && e.key === "s") { - e.preventDefault(); - exportMd.click(); - } - if ((e.ctrlKey || e.metaKey) && e.key === "c") { - e.preventDefault(); - copyMarkdownButton.click(); - } - // Story 1.2: Only allow sync toggle shortcut when in split view - if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "S") { - e.preventDefault(); - if (currentViewMode === 'split') { - toggleSyncScrolling(); - } - } - }); -}); \ No newline at end of file diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css deleted file mode 100644 index 492b1ef..0000000 --- a/desktop-app/resources/styles.css +++ /dev/null @@ -1,954 +0,0 @@ -:root { - --bg-color: #ffffff; - --editor-bg: #f6f8fa; - --preview-bg: #ffffff; /* Preview background for light mode */ - --text-color: #24292e; - --preview-text-color: #24292e; /* Text color for preview in light mode */ - --border-color: #e1e4e8; - --header-bg: #f6f8fa; - --button-bg: #f6f8fa; - --button-hover: #e1e4e8; - --button-active: #d1d5da; - --dropzone-bg: rgba(255, 255, 255, 0.8); - --scrollbar-thumb: #c1c1c1; - --scrollbar-track: #f1f1f1; - --accent-color: #0366d6; - --table-bg: #ffffff; /* Table background for light mode */ - --code-bg: #f6f8fa; /* Code block background for light mode */ -} - -[data-theme="dark"] { - --bg-color: #0d1117; - --editor-bg: #161b22; - --preview-bg: #0d1117; /* Preview background for dark mode */ - --text-color: #c9d1d9; - --preview-text-color: #c9d1d9; /* Text color for preview in dark mode */ - --border-color: #30363d; - --header-bg: #161b22; - --button-bg: #21262d; - --button-hover: #30363d; - --button-active: #3b434b; - --dropzone-bg: rgba(13, 17, 23, 0.8); - --scrollbar-thumb: #484f58; - --scrollbar-track: #21262d; - --accent-color: #58a6ff; - --table-bg: #161b22; /* Table background for dark mode */ - --code-bg: #161b22; /* Code block background for dark mode */ -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - background-color: var(--bg-color); - color: var(--text-color); - transition: background-color 0.3s ease, color 0.3s ease; - min-height: 100vh; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; -} - -.app-header { - background-color: var(--header-bg); - border-bottom: 1px solid var(--border-color); - padding: 1rem; - transition: background-color 0.3s ease; - position: sticky; - top: 0; - z-index: 100; -} - -.app-container { - height: 100vh; - display: flex; - flex-direction: column; -} - -.content-container { - display: flex; - flex: 1; - overflow: hidden; -} - -.editor-pane, .preview-pane { - flex: 1; - padding: 20px; - overflow-y: auto; - position: relative; - transition: background-color 0.3s ease; -} - -.editor-pane { - background-color: var(--editor-bg); - border-right: 1px solid var(--border-color); - padding-right: 0px; -} - -.preview-pane { - background-color: var(--preview-bg); /* Using the new variable for preview background */ -} - -/* Custom scrollbar */ -.editor-pane::-webkit-scrollbar, -.preview-pane::-webkit-scrollbar, -#markdown-editor::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -.editor-pane::-webkit-scrollbar-track, -.preview-pane::-webkit-scrollbar-track, -#markdown-editor::-webkit-scrollbar-track { - background: var(--scrollbar-track); -} - -.editor-pane::-webkit-scrollbar-thumb, -.preview-pane::-webkit-scrollbar-thumb, -#markdown-editor::-webkit-scrollbar-thumb { - background: var(--scrollbar-thumb); - border-radius: 4px; -} - -.editor-pane::-webkit-scrollbar-thumb:hover, -.preview-pane::-webkit-scrollbar-thumb:hover, -#markdown-editor::-webkit-scrollbar-thumb:hover { - background: var(--button-active); -} - -#markdown-editor { - width: 100%; - height: 100%; - border: none; - background-color: var(--editor-bg); - color: var(--text-color); - resize: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; - font-size: 14px; - line-height: 1.5; - padding: 10px; - transition: background-color 0.3s ease, color 0.3s ease; - overflow-y: auto; -} - -#markdown-editor:focus { - outline: none; -} - -.preview-pane { - padding: 20px; -} - -.markdown-body { - padding: 20px; - width: 100%; - background-color: var(--preview-bg); /* Ensuring the markdown content matches preview background */ - color: var(--preview-text-color); /* Using specific text color for preview content */ -} - -/* Style tables in light mode */ -.markdown-body table { - background-color: var(--table-bg); - border-color: var(--border-color); -} - -.markdown-body table tr { - background-color: var(--table-bg); - border-top: 1px solid var(--border-color); -} - -.markdown-body table tr:nth-child(2n) { - background-color: var(--bg-color); -} - -/* Style code blocks in light mode */ -.markdown-body pre { - background-color: var(--code-bg); - border-radius: 6px; -} - -.markdown-body code { - background-color: var(--code-bg); - border-radius: 3px; - padding: 0.2em 0.4em; -} - -.toolbar { - display: flex; - gap: 8px; -} - -.tool-button { - background-color: var(--button-bg); - border: 1px solid var(--border-color); - color: var(--text-color); - border-radius: 6px; - padding: 6px 12px; - font-size: 14px; - cursor: pointer; - display: flex; - align-items: center; - gap: 4px; - transition: all 0.2s ease; -} - -.tool-button:hover { - background-color: var(--button-hover); -} - -.tool-button:active { - background-color: var(--button-active); -} - -.tool-button i { - font-size: 16px; -} - -.file-input { - display: none; -} - -.dropzone { - border: 2px dashed var(--border-color); - border-radius: 6px; - padding: 20px; - text-align: center; - margin-bottom: 20px; - cursor: pointer; - transition: all 0.3s ease; - background-color: var(--dropzone-bg); -} - -.dropzone.active { - border-color: var(--accent-color); - background-color: rgba(var(--accent-color), 0.05); -} - -.dropzone p { - transition: transform 0.2s ease; -} - -.dropzone:hover p { - transform: scale(1.02); -} - -/* Dropdown improvements */ -.dropdown-menu { - background-color: var(--bg-color); - border-color: var(--border-color); -} - -.dropdown-item { - color: var(--text-color); -} - -.dropdown-item:hover, .dropdown-item:focus { - background-color: var(--button-hover); - color: var(--text-color); -} - -/* Responsive design for mobile */ -@media (max-width: 1080px) { - .content-container { - flex-direction: column; - } - - .editor-pane, .preview-pane { - flex: none; - height: 50%; - border-right: none; - } - - .editor-pane { - border-bottom: 1px solid var(--border-color); - } - - .toolbar { - flex-wrap: wrap; - justify-content: center; - gap: 1rem; - } -} - -/* Loading indicators */ -.loading { - opacity: 0.6; - pointer-events: none; -} - -/* Focus outline for accessibility */ -button:focus, -a:focus { - outline: 2px solid var(--accent-color); - outline-offset: 2px; -} - -/* Animation for copied message */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -/* Tooltip styles */ -.tooltip { - position: absolute; - background: var(--button-bg); - border: 1px solid var(--border-color); - padding: 5px 8px; - border-radius: 4px; - font-size: 12px; - z-index: 1000; - animation: fadeIn 0.2s ease; -} - -/* Styles for GitHub markdown preview light mode */ -.markdown-body { - color-scheme: light; - --color-prettylights-syntax-comment: #6a737d; - --color-prettylights-syntax-constant: #005cc5; - --color-prettylights-syntax-entity: #6f42c1; - --color-prettylights-syntax-storage-modifier-import: #24292e; - --color-prettylights-syntax-entity-tag: #22863a; - --color-prettylights-syntax-keyword: #d73a49; - --color-prettylights-syntax-string: #032f62; - --color-prettylights-syntax-variable: #e36209; - --color-prettylights-syntax-brackethighlighter-unmatched: #b31d28; - --color-prettylights-syntax-invalid-illegal-text: #fafbfc; - --color-prettylights-syntax-invalid-illegal-bg: #b31d28; - --color-prettylights-syntax-carriage-return-text: #fafbfc; - --color-prettylights-syntax-carriage-return-bg: #d73a49; - --color-prettylights-syntax-string-regexp: #22863a; - --color-prettylights-syntax-markup-list: #735c0f; - --color-prettylights-syntax-markup-heading: #005cc5; - --color-prettylights-syntax-markup-italic: #24292e; - --color-prettylights-syntax-markup-bold: #24292e; - --color-prettylights-syntax-markup-deleted-text: #b31d28; - --color-prettylights-syntax-markup-deleted-bg: #ffeef0; - --color-prettylights-syntax-markup-inserted-text: #22863a; - --color-prettylights-syntax-markup-inserted-bg: #f0fff4; - --color-prettylights-syntax-markup-changed-text: #e36209; - --color-prettylights-syntax-markup-changed-bg: #ffebda; - --color-prettylights-syntax-markup-ignored-text: #f6f8fa; - --color-prettylights-syntax-markup-ignored-bg: #005cc5; - --color-prettylights-syntax-meta-diff-range: #6f42c1; - --color-prettylights-syntax-brackethighlighter-angle: #586069; - --color-prettylights-syntax-sublimelinter-gutter-mark: #e1e4e8; - --color-prettylights-syntax-constant-other-reference-link: #032f62; - --color-fg-default: #24292e; - --color-fg-muted: #586069; - --color-fg-subtle: #6a737d; - --color-canvas-default: #ffffff; - --color-canvas-subtle: #f6f8fa; - --color-border-default: #e1e4e8; - --color-border-muted: #eaecef; - --color-neutral-muted: rgba(175,184,193,0.2); - --color-accent-fg: #0366d6; - --color-accent-emphasis: #0366d6; - --color-attention-subtle: #fff5b1; - --color-danger-fg: #d73a49; -} - -/* Styles for GitHub markdown preview dark mode */ -[data-theme="dark"] .markdown-body { - color-scheme: dark; - --color-prettylights-syntax-comment: #8b949e; - --color-prettylights-syntax-constant: #79c0ff; - --color-prettylights-syntax-entity: #d2a8ff; - --color-prettylights-syntax-storage-modifier-import: #c9d1d9; - --color-prettylights-syntax-entity-tag: #7ee787; - --color-prettylights-syntax-keyword: #ff7b72; - --color-prettylights-syntax-string: #a5d6ff; - --color-prettylights-syntax-variable: #ffa657; - --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; - --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; - --color-prettylights-syntax-invalid-illegal-bg: #8e1519; - --color-prettylights-syntax-carriage-return-text: #f0f6fc; - --color-prettylights-syntax-carriage-return-bg: #b62324; - --color-prettylights-syntax-string-regexp: #7ee787; - --color-prettylights-syntax-markup-list: #f2cc60; - --color-prettylights-syntax-markup-heading: #1f6feb; - --color-prettylights-syntax-markup-italic: #c9d1d9; - --color-prettylights-syntax-markup-bold: #c9d1d9; - --color-prettylights-syntax-markup-deleted-text: #ffdcd7; - --color-prettylights-syntax-markup-deleted-bg: #67060c; - --color-prettylights-syntax-markup-inserted-text: #aff5b4; - --color-prettylights-syntax-markup-inserted-bg: #033a16; - --color-prettylights-syntax-markup-changed-text: #ffdfb6; - --color-prettylights-syntax-markup-changed-bg: #5a1e02; - --color-prettylights-syntax-markup-ignored-text: #c9d1d9; - --color-prettylights-syntax-markup-ignored-bg: #1158c7; - --color-prettylights-syntax-meta-diff-range: #d2a8ff; - --color-prettylights-syntax-brackethighlighter-angle: #8b949e; - --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; - --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; - --color-fg-default: #c9d1d9; - --color-fg-muted: #8b949e; - --color-fg-subtle: #484f58; - --color-canvas-default: #0d1117; - --color-canvas-subtle: #161b22; - --color-border-default: #30363d; - --color-border-muted: #21262d; - --color-neutral-muted: rgba(110,118,129,0.4); - --color-accent-fg: #58a6ff; - --color-accent-emphasis: #1f6feb; - --color-attention-subtle: rgba(187,128,9,0.15); - --color-danger-fg: #f85149; -} - -/* Override specific styles for dark mode tables and code */ -[data-theme="dark"] .markdown-body table tr { - background-color: var(--table-bg); -} - -[data-theme="dark"] .markdown-body table tr:nth-child(2n) { - background-color: #1c2128; /* Slightly lighter than base dark background */ -} - -[data-theme="dark"] .markdown-body pre { - background-color: var(--code-bg); -} - -[data-theme="dark"] .markdown-body code { - background-color: var(--code-bg); -} - -[data-theme="dark"] .hljs { - color: #e8eaed; -} - -.stats-container { - font-size: 0.9rem; - color: var(--text-color); -} - -.stat-item { - align-items: center; -} - -.stat-item i { - font-size: 1rem; - opacity: 0.8; -} - -.dropzone { - border: 2px dashed var(--border-color); - border-radius: 6px; - padding: 20px; - text-align: center; - margin-bottom: 10px; - cursor: pointer; - transition: all 0.3s ease; - background-color: var(--dropzone-bg); - position: relative; -} - -.dropzone.active { - border-color: var(--accent-color); - background-color: rgba(var(--accent-color), 0.05); -} - -.dropzone p { - transition: transform 0.2s ease; -} - -.dropzone:hover { - border: var(--accent-color) 2px dashed; -} - -.dropzone:hover p { - transform: scale(1.02); -} - -.close-btn { - position: absolute; - top: 5px; - right: 5px; - background: none; - border: none; - color: var(--text-color); - font-size: 1rem; - cursor: pointer; - padding: 5px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - width: 28px; - height: 28px; - opacity: 0.6; - transition: all 0.2s ease; - background-color: var(--button-bg); - border: 1px solid var(--border-color); -} - -.close-btn:hover { - opacity: 1; - background-color: var(--color-danger-fg); -} - -.editor-pane { - overflow: hidden; -} - -/* Mobile Menu Styles */ -.mobile-menu { - display: none; - position: relative; - z-index: 1001; -} - -@media (max-width: 1080px) { - .mobile-menu { - display: block; - } -} - -/* slide‑in panel */ -.mobile-menu-panel { - position: fixed; - top: 0; - right: -300px; - width: 280px; - height: 100vh; - background-color: var(--bg-color); - box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2); - transition: right 0.3s ease; - overflow-y: auto; - padding: 1rem; - display: flex; - flex-direction: column; - z-index: 1002; -} - -.mobile-menu-panel.active { - right: 0; -} - -/* translucent overlay behind panel */ -.mobile-menu-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100vh; - background-color: rgba(0, 0, 0, 0.5); - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; - z-index: 1000; -} - -.mobile-menu-overlay.active { - display: block; - opacity: 1; - visibility: visible; -} - -/* header inside mobile menu */ -.mobile-menu-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.mobile-menu-header h5 { - margin: 0; - font-size: 1.25rem; - color: var(--text-color); -} - -/* stats section in mobile menu */ -.mobile-stats-container { - border-bottom: 1px solid var(--border-color); - padding-bottom: 0.75rem; - margin-bottom: 1rem; -} - -.mobile-stats-container .stat-item { - font-size: 0.9rem; - color: var(--text-color); - display: flex; - align-items: center; -} - -.mobile-stats-container .stat-item i { - margin-right: 0.5em; - opacity: 0.8; -} - -/* menu buttons list */ -.mobile-menu-items { - display: flex; - flex-direction: column; - gap: 0.5rem; - flex-grow: 1; -} - -/* each menu item */ -.mobile-menu-item { - background-color: var(--button-bg); - border: 1px solid var(--border-color); - color: var(--text-color); - border-radius: 6px; - padding: 0.6rem 1rem; - font-size: 1rem; - text-align: left; - display: flex; - align-items: center; - gap: 0.5rem; - transition: background-color 0.2s ease; - cursor: pointer; -} - -.mobile-menu-item:hover { - background-color: var(--button-hover); -} - -.mobile-menu-item:active { - background-color: var(--button-active); -} - -/* close button override */ -#close-mobile-menu.tool-button { - padding: 0.25rem 0.5rem; - font-size: 1rem; -} - -/* ensure dropzone doesn’t cover menu */ -.mobile-menu-panel .dropzone { - margin-bottom: 0; -} - -/* hide desktop-only stats and toolbar on mobile */ -@media (max-width: 767px) { - .stats-container.d-none.d-md-flex, - .toolbar.d-none.d-md-flex { - display: none !important; - } -} - -.github-link { - color: var(--text-color); - text-decoration: none; - display: flex; - align-items: center; - justify-content: center; - transition: transform 0.2s ease, color 0.2s ease; - margin-right: 2rem; -} - -.github-link:hover { - color: var(--accent-color); - transform: scale(1.1); -} - -.github-link i { - font-size: 1.5rem; -} - -/* ======================================== - VIEW MODE CONTROLS - Story 1.1 - ======================================== */ - -/* Header layout for three sections */ -.header-container { - position: relative; -} - -.header-left { - flex: 1; - justify-content: flex-start; -} - -.header-right { - flex: 1; - justify-content: flex-end; -} - -/* View Mode Button Group */ -.view-mode-group { - display: flex; - gap: 0; - position: absolute; - left: 50%; - transform: translateX(-50%); -} - -.view-mode-btn { - background-color: var(--button-bg); - border: 1px solid var(--border-color); - color: var(--text-color); - padding: 6px 12px; - font-size: 14px; - cursor: pointer; - display: flex; - align-items: center; - gap: 4px; - transition: all 0.2s ease; -} - -.view-mode-btn:first-child { - border-radius: 6px 0 0 6px; -} - -.view-mode-btn:last-child { - border-radius: 0 6px 6px 0; -} - -.view-mode-btn:not(:last-child) { - border-right: none; -} - -.view-mode-btn:hover { - background-color: var(--button-hover); -} - -.view-mode-btn.active { - background-color: var(--button-bg); - border-color: var(--accent-color); - color: var(--accent-color); - border-width: 2px; - padding: 5px 11px; /* Adjust for thicker border */ -} - -.view-mode-btn.active:not(:last-child) { - border-right: 2px solid var(--accent-color); -} - -.view-mode-btn i { - font-size: 16px; -} - -/* Pane View States */ -.content-container.view-editor-only .preview-pane { - display: none; -} - -.content-container.view-editor-only .editor-pane { - flex: 1; - border-right: none; -} - -.content-container.view-preview-only .editor-pane { - display: none; -} - -.content-container.view-preview-only .preview-pane { - flex: 1; -} - -.content-container.view-split .editor-pane, -.content-container.view-split .preview-pane { - flex: 1; -} - -/* Responsive adjustments for view mode buttons */ -@media (max-width: 1079px) { - .view-mode-group { - position: static; - transform: none; - } -} - -@media (max-width: 767px) { - .view-mode-group { - display: none; - } -} - -/* ======================================== - RESIZE DIVIDER - Story 1.3 - ======================================== */ - -.resize-divider { - width: 8px; - background-color: transparent; - cursor: col-resize; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - position: relative; - z-index: 10; - transition: background-color 0.2s ease; -} - -.resize-divider:hover { - background-color: var(--button-hover); -} - -.resize-divider.dragging { - background-color: var(--accent-color); -} - -.resize-divider-handle { - width: 2px; - height: 40px; - background-color: var(--border-color); - border-radius: 2px; - transition: background-color 0.2s ease, width 0.2s ease; -} - -.resize-divider:hover .resize-divider-handle, -.resize-divider.dragging .resize-divider-handle { - background-color: var(--accent-color); - width: 3px; -} - -/* Hide divider in single-pane modes */ -.content-container.view-editor-only .resize-divider, -.content-container.view-preview-only .resize-divider { - display: none; -} - -/* Hide divider on tablet and mobile (no drag resize) */ -@media (max-width: 1079px) { - .resize-divider { - display: none; - } -} - -/* Prevent text selection during drag */ -.resizing { - user-select: none; - cursor: col-resize !important; -} - -.resizing * { - cursor: col-resize !important; -} - -/* ======================================== - MOBILE VIEW MODE CONTROLS - Story 1.4 - ======================================== */ - -.mobile-view-mode-group { - display: flex; - gap: 0; - border-bottom: 1px solid var(--border-color); - padding-bottom: 0.75rem; -} - -.mobile-view-mode-btn { - flex: 1; - background-color: var(--button-bg); - border: 1px solid var(--border-color); - color: var(--text-color); - padding: 8px 12px; - font-size: 14px; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 4px; - transition: all 0.2s ease; -} - -.mobile-view-mode-btn:first-child { - border-radius: 6px 0 0 6px; -} - -.mobile-view-mode-btn:last-child { - border-radius: 0 6px 6px 0; -} - -.mobile-view-mode-btn:not(:last-child) { - border-right: none; -} - -.mobile-view-mode-btn:hover, -.mobile-view-mode-btn:active { - background-color: var(--button-hover); -} - -.mobile-view-mode-btn.active { - background-color: var(--button-bg); - border-color: var(--accent-color); - color: var(--accent-color); - border-width: 2px; - padding: 7px 11px; -} - -.mobile-view-mode-btn.active:not(:last-child) { - border-right: 2px solid var(--accent-color); -} - -.mobile-view-mode-btn i { - font-size: 18px; -} - -.mobile-view-mode-btn span { - font-size: 12px; -} - -/* ======================================== - RESPONSIVE VIEW MODE FIXES - Story 1.5 - ======================================== */ - -/* On tablet/mobile, ensure single-pane modes take full height */ -@media (max-width: 1079px) { - .content-container.view-editor-only .editor-pane, - .content-container.view-preview-only .preview-pane { - height: 100%; - } - - .content-container.view-split .editor-pane, - .content-container.view-split .preview-pane { - height: 50%; - } -} - -/* ======================================== - PDF EXPORT TABLE FIX - Rowspan/Colspan - ======================================== */ - -/* Fix for html2canvas not properly rendering rowspan/colspan cells. - Apply backgrounds to cells instead of rows to prevent row backgrounds - from painting over rowspan cells during canvas capture. */ -.pdf-export table tr { - background-color: transparent !important; -} - -.pdf-export table th, -.pdf-export table td { - background-color: var(--table-bg, #ffffff); - position: relative; -} - -.pdf-export table tr:nth-child(2n) th, -.pdf-export table tr:nth-child(2n) td { - background-color: var(--bg-color, #f6f8fa); -} - -/* Ensure rowspan cells render correctly */ -.pdf-export table th[rowspan], -.pdf-export table td[rowspan] { - vertical-align: middle; - background-color: var(--table-bg, #ffffff) !important; -} - -/* Ensure colspan cells render correctly */ -.pdf-export table th[colspan], -.pdf-export table td[colspan] { - text-align: center; -} - -/* Dark mode PDF export table fix */ -[data-theme="dark"] .pdf-export table th, -[data-theme="dark"] .pdf-export table td { - background-color: var(--table-bg, #161b22); -} - -[data-theme="dark"] .pdf-export table tr:nth-child(2n) th, -[data-theme="dark"] .pdf-export table tr:nth-child(2n) td { - background-color: #1c2128; -} - -[data-theme="dark"] .pdf-export table th[rowspan], -[data-theme="dark"] .pdf-export table td[rowspan] { - background-color: var(--table-bg, #161b22) !important; -} \ No newline at end of file diff --git a/desktop-app/tag.sh b/desktop-app/tag.sh new file mode 100644 index 0000000..c8ea173 --- /dev/null +++ b/desktop-app/tag.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -euo pipefail + +echo "" +echo "@tag.sh - Utility script to calculate the next tag for the desktop app" +echo "---" + +DEFAULT_VERSION="$(date +"%Y.%-m.0")" +DEFAULT_TAG_NAME="desktop-v$DEFAULT_VERSION" + +# Get the latest tag for the current branch and prune deleted tags +TAG_NAME=$(git fetch --tags --prune --prune-tags && git tag -l --contains HEAD | tail -n1) + +# If no tag is found, create one using CalVer (Calendar Versioning) +if [ -z "$TAG_NAME" ]; then + echo "[WARNING] No tag found, creating one using CalVer (Calendar Versioning)" + # Use CalVer (Calendar Versioning) + # format: YYYY.M.P + # YYYY = Year, M = Month, P = Patch (Defaults to 0 if not specified) + # Example: 2026.2.0 + TAG_NAME="$DEFAULT_TAG_NAME" + +else # If a tag is found, determine the next tag + # Remove "desktop-v" prefix + TAG_NAME=${TAG_NAME#desktop-v} + + # Check if not from current month or year + if [ "$(echo "$TAG_NAME" | awk -F. '{print $2}')" != "$(date +"%-m")" ] || [ "$(echo "$TAG_NAME" | awk -F. '{print $1}')" != "$(date +"%Y")" ]; then + # Reset patch to 0 and set YYYY.M to current date + TAG_NAME="$DEFAULT_VERSION" + else + # Same month & year => only increment the patch number + TAG_NAME=$(echo "$TAG_NAME" | awk -F. '{$NF = $NF + 1; OFS="."; print}') + fi + # Add "desktop-v" prefix back + TAG_NAME="desktop-v$TAG_NAME" +fi + +# Get the current short commit-hash +COMMIT_HASH=$(git show -s --format=%h) + +# Print the tag and commit-hash +echo "TAG "$'\t'""$'\t '" | COMMIT" +echo "----------------- | --------" +echo "$TAG_NAME | $COMMIT_HASH" +echo "" +echo "To create and push the tag, run:" +echo "git tag \"$TAG_NAME\" && git push origin \"$TAG_NAME\""