diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a4d7781..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.nojekyll b/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/index.html b/index.html index 7054ae7..7fa8b01 100644 --- a/index.html +++ b/index.html @@ -1,65 +1,225 @@ - + - - - - Lean Canvas - + + + LeanCanvas Editor | Local-First Workspace + + + + + - - -
- -
- - -
-

Lean Canvas

-
- - -
-
-

Problem

-
-
-
-

Solution

-
-
-
-

Unique Value Proposition

-
-
-
-

Unfair Advantage

-
+ +
+
+ LeanCanvas
-
-

Customer Segment

-
+
+ + +
+ + +
+
+
-
-

Key Metrics

-
-
-
-

Channels

-
-
-
-

Cost Structure

-
-
-
-

Revenue Streams

-
+
+ +
+
+
+
+

Lean Canvas

+

Tip: touch/click content to edit inline. Hover list items to remove them.

+
+
+ schedule Last edit: -- +
+
+ +
+
+
+
+

Problem

+ report_problem +
+
    +
    +
    +

    Existing Alternatives

    +
    +
    +
    + +
    +
    +
    +

    Solution

    + lightbulb +
    +

    +
    +

    Observation:

    +

    +
    +
    +
    +

    Key Metrics

    +
      +
      +
      + +
      +
      +
      star
      +
      +

      Unique Value Proposition

      +
      +

      +
      +

      High-Level Concept

      +

      +
      +
      +
      + +
      +
      +
      +

      Unfair Advantage

      + workspace_premium +
      +
        +
        +
        +

        Channels

        +
        +
        +
        + +
        +
        +
        +

        Customer Segments

        + person_search +
        +
        +
        +
        +

        Early Adopters

        +
        +
        +
        +
        + +
        +
        +
        + payments +

        Cost Structure

        +
        +
        +
        +
        +
        + monetization_on +

        Revenue Streams

        +
        +
        +
        +
        -
        - - - + + +
        + diff --git a/script.js b/script.js index 2376a2e..8ac1d4d 100644 --- a/script.js +++ b/script.js @@ -1,166 +1,709 @@ -console.log("script.js geladen."); - -document.addEventListener("DOMContentLoaded", () => { - console.log("DOMContentLoaded-Ereignis empfangen."); - - // PDF Export Button initialisieren - const exportBtn = document.getElementById("exportPDF"); - if (exportBtn) { - console.log("PDF Export Button gefunden."); - exportBtn.addEventListener("click", () => { - console.log("PDF Export Button geklickt. Öffne window.print()."); - window.print(); - }); - } else { - console.warn("PDF Export Button nicht gefunden."); +const STORAGE_KEY = "mdcanvas_state_v2"; + +const initialState = { + title: "Lean Canvas", + problem: [], + existingAlternatives: [], + solution: "", + observation: "Users prefer conversational UX over dashboards.", + keyMetrics: [], + uniqueValueProposition: "", + highLevelConcept: "Personal digital intelligent butler", + unfairAdvantage: [], + channels: [], + customerSegments: [], + earlyAdopters: [], + costStructure: [], + revenueStreams: [], + todo: [], +}; + +const headerMap = { + "lean canvas": "title", + problem: "problem", + "existing alternatives": "existingAlternatives", + solution: "solution", + observation: "observation", + "key metrics": "keyMetrics", + "unique value proposition": "uniqueValueProposition", + "high-level concept": "highLevelConcept", + "high level concept": "highLevelConcept", + "unfair advantage": "unfairAdvantage", + channels: "channels", + "customer segment": "customerSegments", + "customer segments": "customerSegments", + "early adopters": "earlyAdopters", + "cost structure": "costStructure", + "revenue streams": "revenueStreams", + todo: "todo", +}; + +const sectionTypes = { + title: "text", + problem: "list", + existingAlternatives: "list", + solution: "text", + observation: "text", + keyMetrics: "list", + uniqueValueProposition: "text", + highLevelConcept: "text", + unfairAdvantage: "list", + channels: "list", + customerSegments: "list", + earlyAdopters: "list", + costStructure: "list", + revenueStreams: "list", + todo: "list", +}; + +let state = structuredClone(initialState); +let lastEditAt = null; + +document.addEventListener("DOMContentLoaded", async () => { + bindActions(); + await loadInitialState(); + render(); + setupInlineTextEditing(); + setInterval(updateLastEditLabel, 30000); +}); + +function bindActions() { + const importBtn = document.getElementById("importBtn"); + const saveBtn = document.getElementById("saveBtn"); + const saveMenuWrap = document.getElementById("saveMenuWrap"); + const saveMenu = document.getElementById("saveMenu"); + const saveMarkdownBtn = document.getElementById("saveMarkdownBtn"); + const savePdfBtn = document.getElementById("savePdfBtn"); + const createBtn = document.getElementById("createBtn"); + const importInput = document.getElementById("importFileInput"); + + importBtn?.addEventListener("click", () => importInput?.click()); + + importInput?.addEventListener("change", async (event) => { + const file = event.target.files?.[0]; + if (!file) { + return; + } + + const content = await file.text(); + const imported = parseMarkdown(content); + state = mergeWithDefaults(imported); + markEdited("Markdown imported"); + render(); + importInput.value = ""; + }); + + saveBtn?.addEventListener("click", (event) => { + event.stopPropagation(); + toggleSaveMenu(); + }); + + saveMarkdownBtn?.addEventListener("click", () => { + const markdown = stateToMarkdown(state); + downloadFile("canvas.md", markdown); + persistState(); + hideSaveMenu(); + showToast("Canvas saved as canvas.md"); + }); + + savePdfBtn?.addEventListener("click", () => { + hideSaveMenu(); + saveAsPdf(); + }); + + document.addEventListener("click", (event) => { + if (!saveMenuWrap?.contains(event.target)) { + hideSaveMenu(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + hideSaveMenu(); + } + }); + + createBtn?.addEventListener("click", () => { + const confirmed = window.confirm( + "Create a new canvas? Current content will be replaced.", + ); + if (!confirmed) { + return; + } + + state = structuredClone(initialState); + markEdited("New canvas created"); + render(); + }); + + function toggleSaveMenu() { + if (!saveMenu) { + return; + } + saveMenu.classList.toggle("hidden"); } - // Markdown laden und parsen - console.log("Starte Fetch für canvas.md..."); - fetch('canvas.md') - .then(response => { - console.log("Fetch-Antwort empfangen."); - if (!response.ok) { - throw new Error("Fehler beim Laden der Markdown-Datei."); + function hideSaveMenu() { + saveMenu?.classList.add("hidden"); + } +} + +function saveAsPdf() { + document.body.classList.add("print-canvas-mode"); + window.addEventListener( + "afterprint", + () => { + document.body.classList.remove("print-canvas-mode"); + }, + { once: true }, + ); + window.print(); +} + +function setupInlineTextEditing() { + const bindings = [ + { id: "canvasTitle", key: "title" }, + { id: "solutionText", key: "solution" }, + { id: "observationText", key: "observation" }, + { id: "uvpText", key: "uniqueValueProposition" }, + { id: "highLevelConceptText", key: "highLevelConcept" }, + ]; + + bindings.forEach(({ id, key }) => { + const element = document.getElementById(id); + if (!element || element.dataset.inlineBound === "true") { + return; + } + + element.contentEditable = "true"; + element.spellcheck = true; + element.classList.add("editable-text"); + element.title = "Edit directly"; + + element.addEventListener("blur", () => { + const value = element.innerText.replace(/\s+/g, " ").trim(); + if ((state[key] || "") === value) { + return; } - return response.text(); - }) - .then(mdText => { - console.log("Markdown-Inhalt geladen:"); - console.log(mdText); - parseMarkdown(mdText); - }) - .catch(error => { - console.error("Fehler beim Laden/parsen der Markdown-Datei:", error); + + state[key] = value; + markEdited("Text updated"); + render(); }); -}); + + element.dataset.inlineBound = "true"; + }); +} + +async function loadInitialState() { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + const parsed = JSON.parse(saved); + state = mergeWithDefaults(parsed); + lastEditAt = Date.now(); + return; + } catch { + localStorage.removeItem(STORAGE_KEY); + } + } + + try { + const response = await fetch("canvas.md"); + if (!response.ok) { + throw new Error("Unable to read canvas.md"); + } + + const text = await response.text(); + state = mergeWithDefaults(parseMarkdown(text)); + lastEditAt = Date.now(); + persistState(); + } catch { + state = structuredClone(initialState); + lastEditAt = Date.now(); + } +} + +function mergeWithDefaults(data) { + return { + ...structuredClone(initialState), + ...data, + problem: normalizeList(data.problem), + existingAlternatives: normalizeList(data.existingAlternatives), + keyMetrics: normalizeList(data.keyMetrics), + unfairAdvantage: normalizeList(data.unfairAdvantage), + channels: normalizeList(data.channels), + customerSegments: normalizeList(data.customerSegments), + earlyAdopters: normalizeList(data.earlyAdopters), + costStructure: normalizeList(data.costStructure), + revenueStreams: normalizeList(data.revenueStreams), + todo: normalizeList(data.todo), + }; +} function parseMarkdown(mdText) { - console.log("Starte Parsing der Markdown-Datei..."); - const lines = mdText.split('\n'); - let currentSection = null; - const sectionContent = {}; - - lines.forEach((line, index) => { - line = line.trim(); - console.log(`Zeile ${index + 1}: "${line}"`); - if (line.startsWith("# ")) { - // Neue Überschrift gefunden - const header = line.replace("# ", "").trim(); - console.log(`Überschrift erkannt: "${header}"`); - if (sectionMapping.hasOwnProperty(header)) { - currentSection = sectionMapping[header]; - console.log(`Mapping gefunden. currentSection gesetzt auf: "${currentSection}"`); - sectionContent[currentSection] = (currentSection === "lean-canvas") ? "" : []; - } else { - console.log(`Kein Mapping für Überschrift: "${header}". Setze currentSection auf null.`); - currentSection = null; - } - } else if (currentSection) { - if (line !== "") { - if (currentSection === "lean-canvas") { - sectionContent[currentSection] += line + " "; - console.log(`Anhängen an lean-canvas: "${line}"`); - } else if (/^[-*]\s+/.test(line)) { - const item = line.replace(/^[-*]\s+/, "").trim(); - sectionContent[currentSection].push(item); - console.log(`Listeneintrag erkannt in Section "${currentSection}": "${item}"`); - } else { - sectionContent[currentSection] += line + " "; - console.log(`Anhängen an Section "${currentSection}" (kein Listenpunkt): "${line}"`); - } + const parsed = {}; + const lines = mdText.split(/\r?\n/); + let currentKey = null; + + for (const rawLine of lines) { + const line = rawLine.trim(); + + if (!line) { + continue; + } + + const headingMatch = line.match(/^#{1,2}\s+(.+)$/); + if (headingMatch) { + const heading = normalizeHeader(headingMatch[1]); + currentKey = headerMap[heading] || null; + + if (!currentKey) { + continue; } + + parsed[currentKey] = sectionTypes[currentKey] === "list" ? [] : ""; + continue; } - }); - console.log("Parsing abgeschlossen. Ergebnis:", sectionContent); - - // Inhalte in die entsprechenden Container einfügen - for (const section in sectionContent) { - if (section === "lean-canvas") { - const leanHeader = document.querySelector("#lean-canvas-header h1"); - const leanText = sectionContent[section].trim(); - const newHeaderText = leanText ? `Lean Canvas - ${leanText}` : "Lean Canvas"; - leanHeader.textContent = newHeaderText; - document.title = newHeaderText; - console.log(`Lean Canvas Header gesetzt: "${newHeaderText}"`); - } else if (section === "todo") { - // Verarbeitung der Todo-Sektion erfolgt später + if (!currentKey) { continue; - } else { - const container = document.querySelector(`#${section} .content`); - if (container) { - if (Array.isArray(sectionContent[section]) && sectionContent[section].length) { - const ul = document.createElement("ul"); - sectionContent[section].forEach(item => { - const li = document.createElement("li"); - li.textContent = item; - ul.appendChild(li); - console.log(`Erstelle
      • in Section "${section}": "${item}"`); - }); - container.innerHTML = ""; // Vorherigen Inhalt löschen - container.appendChild(ul); - console.log(`Liste in Container "#${section} .content" eingefügt.`); - } else { - container.textContent = sectionContent[section].trim(); - console.log(`Fließtext in Container "#${section} .content" gesetzt: "${sectionContent[section].trim()}"`); - } - } else { - console.warn(`Kein Container gefunden für Section "${section}"`); + } + + if (sectionTypes[currentKey] === "list") { + const bulletMatch = line.match(/^[-*]\s+(.+)$/); + const value = bulletMatch ? bulletMatch[1].trim() : line; + if (value) { + parsed[currentKey].push(value); } + } else { + parsed[currentKey] = `${parsed[currentKey]} ${line}`.trim(); } } - // Todo-Bereich erstellen, falls die Markdown-Sektion vorhanden ist - if (sectionContent.hasOwnProperty("todo") && sectionContent["todo"]) { - const todoData = sectionContent["todo"]; - console.log("Todo-Sektion gefunden:", todoData); - const todoContainer = document.createElement("div"); - todoContainer.id = "todo-container"; - const todoHeader = document.createElement("h2"); - todoHeader.textContent = "Todo"; - todoHeader.addEventListener("click", () => { - const currentDisplay = todoContent.style.display; - todoContent.style.display = (currentDisplay === "none") ? "" : "none"; - console.log(`Todo-Bereich umgeschaltet. Neuer Display-Status: "${todoContent.style.display}"`); - }); - const todoContent = document.createElement("div"); - todoContent.classList.add("todo-content"); - if (Array.isArray(todoData) && todoData.length) { - const ul = document.createElement("ul"); - todoData.forEach(item => { - const li = document.createElement("li"); - li.textContent = item; - ul.appendChild(li); - console.log(`Erstelle
      • im Todo-Bereich: "${item}"`); - }); - todoContent.appendChild(ul); - } else { - todoContent.textContent = todoData.trim(); - console.log(`Setze Fließtext im Todo-Bereich: "${todoData.trim()}"`); - } - todoContent.style.display = ""; - todoContainer.appendChild(todoHeader); - todoContainer.appendChild(todoContent); - const canvas = document.querySelector(".canvas"); - canvas.parentNode.insertBefore(todoContainer, canvas.nextSibling); - console.log("Todo-Bereich wurde unterhalb des Canvas eingefügt."); + return parsed; +} + +function normalizeHeader(header) { + return header + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, "") + .replace(/\s+/g, " ") + .trim(); +} + +function normalizeList(list) { + if (!Array.isArray(list)) { + return []; + } + + return list.map((item) => String(item).trim()).filter(Boolean); +} + +function render() { + const canvasTitle = document.getElementById("canvasTitle"); + if (canvasTitle) { + canvasTitle.innerHTML = formatInlineMarkdown(state.title || "Lean Canvas"); + } + document.title = `${state.title || "Lean Canvas"} | Local-First Workspace`; + + renderDotList("problemList", state.problem); + renderTagList( + "existingAlternativesTags", + state.existingAlternatives, + "px-2 py-1 bg-secondary-fixed-dim text-on-secondary-fixed text-[11px] rounded-sm font-medium", + ); + renderText("solutionText", state.solution); + renderText("observationText", state.observation); + renderIconList("keyMetricsList", state.keyMetrics, "query_stats"); + renderText("uvpText", state.uniqueValueProposition); + renderText("highLevelConceptText", state.highLevelConcept); + renderCheckList("unfairAdvantageList", state.unfairAdvantage); + renderChannels("channelsList", state.channels); + renderCustomerCards("customerSegmentsCards", state.customerSegments); + renderTagList( + "earlyAdoptersTags", + state.earlyAdopters, + "text-xs font-medium px-2 py-1 bg-secondary-container text-on-secondary-container rounded-sm w-max", + ); + renderCostStructure("costStructureList", state.costStructure); + renderRevenueCards("revenueStreamsCards", state.revenueStreams); + + persistState(); + updateLastEditLabel(); +} + +function renderText(elementId, value) { + const element = document.getElementById(elementId); + if (element) { + element.innerHTML = formatInlineMarkdown(value || ""); + } +} + +function renderDotList(elementId, items) { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + element.innerHTML = ""; + for (const [index, item] of items.entries()) { + const li = document.createElement("li"); + li.className = "group relative flex items-start gap-2 pr-8"; + li.innerHTML = `${formatInlineMarkdown(item)}`; + element.appendChild(li); + } + + appendAddButton(element, "problem"); +} + +function renderIconList(elementId, items, icon) { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + element.innerHTML = ""; + const key = elementId === "keyMetricsList" ? "keyMetrics" : "channels"; + for (const [index, item] of items.entries()) { + const li = document.createElement("li"); + li.className = "group relative flex items-start gap-2 pr-8"; + li.innerHTML = `${icon}${formatInlineMarkdown(item)}`; + element.appendChild(li); + } + + appendAddButton(element, key); +} + +function renderCheckList(elementId, items) { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + element.innerHTML = ""; + for (const [index, item] of items.entries()) { + const li = document.createElement("li"); + li.className = "group relative flex items-start gap-2 pr-8"; + li.innerHTML = `check_circle${formatInlineMarkdown(item)}`; + element.appendChild(li); + } + + appendAddButton(element, "unfairAdvantage"); +} + +function renderTagList(elementId, items, className) { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + element.innerHTML = ""; + const key = + elementId === "existingAlternativesTags" + ? "existingAlternatives" + : "earlyAdopters"; + for (const [index, item] of items.entries()) { + const wrapper = document.createElement("div"); + wrapper.className = + "group relative inline-flex items-start pr-6 max-w-full"; + wrapper.innerHTML = `${formatInlineMarkdown(item)}`; + element.appendChild(wrapper); + } + + appendAddButton(element, key); +} + +function renderChannels(elementId, items) { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + element.innerHTML = ""; + items.forEach((item, index) => { + const row = document.createElement("div"); + row.className = "group relative pr-8"; + const textClass = "text-[11px] text-on-surface-variant"; + row.innerHTML = `

        ${formatInlineMarkdown(item)}

        `; + element.appendChild(row); + }); + + appendAddButton(element, "channels"); +} + +function renderCustomerCards(elementId, items) { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + element.innerHTML = ""; + for (const [index, item] of items.entries()) { + const wrapper = document.createElement("div"); + wrapper.className = "group relative pr-8"; + wrapper.innerHTML = `
        ${formatInlineMarkdown(item)}
        `; + element.appendChild(wrapper); + } + + appendAddButton(element, "customerSegments"); +} + +function renderCostStructure(elementId, items) { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + element.innerHTML = ""; + for (const [index, item] of items.entries()) { + const div = document.createElement("div"); + div.className = + "group relative flex items-start gap-3 pr-8 pb-2 border-b border-surface-container"; + div.innerHTML = `${formatInlineMarkdown(item)}`; + element.appendChild(div); + } + + appendAddButton(element, "costStructure"); +} + +function renderRevenueCards(elementId, items) { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + const tones = [ + "bg-primary-container/30 text-on-primary-container", + "bg-secondary-container/30 text-on-secondary-container", + "bg-tertiary-container/30 text-on-tertiary-container", + ]; + + element.innerHTML = ""; + items.forEach((item, index) => { + const wrapper = document.createElement("div"); + wrapper.className = "group relative flex-1 min-w-[140px] pr-8"; + wrapper.innerHTML = `

        ${formatInlineMarkdown(item)}

        `; + element.appendChild(wrapper); + }); + + appendAddButton(element, "revenueStreams"); +} + +function appendAddButton(container, key) { + const addButton = document.createElement("button"); + addButton.className = "list-add-btn"; + addButton.type = "button"; + addButton.dataset.key = key; + addButton.textContent = "+ Add"; + addButton.addEventListener("click", () => { + state[key] = [...(state[key] || []), "New item"]; + markEdited("Item added"); + render(); + }); + container.appendChild(addButton); +} + +document.addEventListener("click", (event) => { + const removeBtn = event.target.closest(".list-item-remove"); + if (removeBtn) { + const key = removeBtn.dataset.key; + const index = Number(removeBtn.dataset.index); + if (!key || Number.isNaN(index)) { + return; + } + + state[key] = (state[key] || []).filter( + (_, itemIndex) => itemIndex !== index, + ); + markEdited("Item removed"); + render(); + return; + } + + const textElement = event.target.closest(".list-item-text"); + if (!textElement || textElement.isContentEditable) { + return; + } + + textElement.contentEditable = "true"; + textElement.classList.add("is-editing-inline"); + placeCaretAtEnd(textElement); +}); + +document.addEventListener( + "blur", + (event) => { + const textElement = event.target.closest(".list-item-text"); + if (!textElement || !textElement.isContentEditable) { + return; + } + + const key = textElement.dataset.key; + const index = Number(textElement.dataset.index); + if (!key || Number.isNaN(index)) { + return; + } + + const value = textElement.innerText.replace(/\s+/g, " ").trim(); + textElement.contentEditable = "false"; + textElement.classList.remove("is-editing-inline"); + + if (!value) { + state[key] = (state[key] || []).filter( + (_, itemIndex) => itemIndex !== index, + ); + markEdited("Empty item removed"); + render(); + return; + } + + if (state[key]?.[index] === value) { + return; + } + + state[key][index] = value; + markEdited("Item updated"); + render(); + }, + true, +); + +document.addEventListener("keydown", (event) => { + const textElement = event.target.closest(".list-item-text"); + if (!textElement || !textElement.isContentEditable) { + return; + } + + if (event.key === "Enter") { + event.preventDefault(); + textElement.blur(); + } +}); + +function placeCaretAtEnd(element) { + element.focus(); + const selection = window.getSelection(); + if (!selection) { + return; + } + + const range = document.createRange(); + range.selectNodeContents(element); + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); +} + +function stateToMarkdown(current) { + const blocks = [ + ["Lean Canvas", current.title], + ["Problem", current.problem], + ["Existing Alternatives", current.existingAlternatives], + ["Solution", current.solution], + ["Observation", current.observation], + ["Key Metrics", current.keyMetrics], + ["Unique Value Proposition", current.uniqueValueProposition], + ["High-Level Concept", current.highLevelConcept], + ["Unfair Advantage", current.unfairAdvantage], + ["Channels", current.channels], + ["Customer Segments", current.customerSegments], + ["Early Adopters", current.earlyAdopters], + ["Cost Structure", current.costStructure], + ["Revenue Streams", current.revenueStreams], + ["Todo", current.todo], + ]; + + return ( + blocks + .map(([header, content]) => { + if (Array.isArray(content)) { + const list = content.length + ? content.map((item) => `- ${item}`).join("\n") + : "-"; + return `# ${header}\n${list}`; + } + return `# ${header}\n${content || ""}`; + }) + .join("\n\n") + .trim() + "\n" + ); +} + +function persistState() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); +} + +function markEdited(message) { + lastEditAt = Date.now(); + showToast(message); +} + +function updateLastEditLabel() { + const label = document.getElementById("lastEditLabel"); + if (!label) { + return; + } + + if (!lastEditAt) { + label.textContent = "Last edit: --"; + return; + } + + const minutes = Math.floor((Date.now() - lastEditAt) / 60000); + if (minutes <= 0) { + label.textContent = "Last edit: just now"; + } else if (minutes === 1) { + label.textContent = "Last edit: 1 min ago"; } else { - console.log("Keine Todo-Sektion gefunden."); - } -} - -const sectionMapping = { - "Lean Canvas": "lean-canvas", - "Problem": "problem", - "Solution": "solution", - "Key Metrics": "key-metrics", - "Unique Value Proposition": "unique-value-proposition", - "Unfair Advantage": "unfair-advantage", - "Channels": "channels", - "Customer Segment": "customer-segment", - "Cost Structure": "cost-structure", - "Revenue Streams": "revenue-streams", - "Todo": "todo" -}; + label.textContent = `Last edit: ${minutes} mins ago`; + } +} + +function showToast(message) { + const toast = document.getElementById("toast"); + if (!toast) { + return; + } + + toast.textContent = message; + toast.classList.add("opacity-100"); + toast.classList.remove("opacity-0"); + window.setTimeout(() => { + toast.classList.add("opacity-0"); + toast.classList.remove("opacity-100"); + }, 1600); +} +function downloadFile(filename, text) { + const blob = new Blob([text], { type: "text/markdown;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); +} + +function escapeHtml(value) { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function formatInlineMarkdown(value) { + const safe = escapeHtml(String(value || "")); + + return safe + .replace(/\*\*\*([^*]+?)\*\*\*/g, "$1") + .replace(/___([^_]+?)___/g, "$1") + .replace(/\*\*([^*]+?)\*\*/g, "$1") + .replace(/__([^_]+?)__/g, "$1") + .replace(/\*([^*]+?)\*/g, "$1") + .replace(/_([^_]+?)_/g, "$1"); +} diff --git a/style.css b/style.css index 579f8af..68ccc0f 100644 --- a/style.css +++ b/style.css @@ -1,128 +1,329 @@ -/* Grundstyles */ -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 20px; - background: #f5f5f5; +.material-symbols-outlined { + font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24; } -h1, h2 { - margin-top: 0; +body { + font-family: "Inter", sans-serif; } -/* Export Button */ -#export-container { - text-align: right; - margin-bottom: 10px; +.pb-safe { + padding-bottom: env(safe-area-inset-bottom); } -#exportPDF { - padding: 8px 16px; - font-size: 14px; - cursor: pointer; +.section-edit-trigger { + transition: color 0.2s ease, opacity 0.2s ease; } -/* Header für Lean Canvas */ -#lean-canvas-header { - margin-bottom: 20px; - text-align: center; +.section-edit-trigger:hover { + opacity: 0.85; } -/* Grid-Layout für den Lean Canvas */ -.canvas { - display: grid; - grid-template-columns: repeat(5, 1fr); - grid-template-rows: auto auto auto; - grid-gap: 10px; - grid-template-areas: - "problem solution unique-value-proposition unfair-advantage customer-segment" - "problem key-metrics unique-value-proposition channels customer-segment" - "cost-structure cost-structure cost-structure revenue-streams revenue-streams"; - background: #fff; - padding: 10px; - border: 2px solid #333; +.section-edit-trigger::after { + content: " ✎"; + font-size: 10px; + opacity: 0.6; + vertical-align: middle; } -/* Positionierung der einzelnen Bereiche */ -#problem { - grid-area: problem; +#toast { + will-change: opacity; } -#solution { - grid-area: solution; +section, +section * { + box-sizing: border-box; } -#unique-value-proposition { - grid-area: unique-value-proposition; +#costStructureList, +#revenueStreamsCards, +#channelsList, +#keyMetricsList, +#existingAlternativesTags, +#earlyAdoptersTags { + overflow-wrap: anywhere; } -#unfair-advantage { - grid-area: unfair-advantage; +.editable-text { + cursor: text; + border-radius: 6px; + padding: 2px 4px; + margin: -2px -4px; + outline: none; } -#customer-segment { - grid-area: customer-segment; +.editable-text:focus { + background: rgba(66, 96, 134, 0.08); + box-shadow: 0 0 0 2px rgba(66, 96, 134, 0.2); } -#key-metrics { - grid-area: key-metrics; +.list-item-text { + cursor: text; + outline: none; } -#channels { - grid-area: channels; +.list-item-text.is-editing-inline { + background: rgba(66, 96, 134, 0.08); + border-radius: 4px; + box-shadow: 0 0 0 2px rgba(66, 96, 134, 0.2); } -#cost-structure { - grid-area: cost-structure; +.list-item-remove { + position: absolute; + right: 0; + top: 0; + width: 22px; + height: 22px; + border-radius: 999px; + border: 1px solid #e2e8f0; + background: #ffffff; + color: #9f403d; + font-size: 14px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; } -#revenue-streams { - grid-area: revenue-streams; +.group:hover > .list-item-remove, +.group:focus-within > .list-item-remove { + opacity: 1; + pointer-events: auto; } -/* Stil für die Inhalte */ -.canvas-item { - border: 1px solid #ccc; - padding: 10px; - background: #fafafa; - overflow: auto; +.list-add-btn { + margin-top: 8px; + padding: 2px 8px; + border-radius: 999px; + border: 1px dashed #81b5f6; + color: #355379; + font-size: 11px; + font-weight: 600; + background: rgba(211, 228, 255, 0.35); } -.content ul { - padding-left: 20px; - margin: 0; +.list-add-btn:hover { + background: rgba(211, 228, 255, 0.55); } -/* Todo Bereich */ -#todo-container { - margin-top: 20px; - border: 1px solid #ccc; - padding: 10px; - background: #fafafa; -} -#todo-container h2 { - margin: 0 0 10px 0; - cursor: pointer; -} +@media print { + @page { + size: A4 landscape; + margin: 7mm; + } -/* Print Styles: DIN A4 im Querformat */ -@page { - size: A4 landscape; - margin: 10mm; -} + html, + body { + font-size: 11pt !important; + color: #1f2937 !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } -@media print { body { - background: none; - padding: 0; + background: #ffffff !important; + } + + body > header, + nav, + #toast, + #saveMenu, + .flex.items-center.gap-1:has(#lastEditLabel), + .list-item-remove, + .list-add-btn { + display: none !important; + } + + main { + padding: 0 !important; + } + + .max-w-7xl { + max-width: 100% !important; + } + + .mb-8, + .mt-4, + .gap-4, + .p-6, + .p-5, + .p-4 { + margin-top: 0 !important; + margin-bottom: 0 !important; + gap: 2.5mm !important; + padding: 2.5mm !important; + } + + #canvasTitle + p { + display: none !important; + } + + section, + li, + .group, + #revenueStreamsCards > div, + #costStructureList > div, + #canvasBottomGrid { + break-inside: avoid; + page-break-inside: avoid; + } + + #canvasMainGrid { + display: grid !important; + grid-template-columns: repeat(5, minmax(0, 1fr)) !important; + grid-template-rows: auto auto !important; + gap: 2.5mm !important; + padding: 2.5mm !important; + } + + #canvasMainGrid > :nth-child(1) { + grid-column: 1; + grid-row: 1 / span 2; + } + + #canvasMainGrid > :nth-child(2) { + grid-column: 2; + grid-row: 1 / span 2; + } + + #canvasMainGrid > :nth-child(3) { + grid-column: 3; + grid-row: 1 / span 2; + } + + #canvasMainGrid > :nth-child(4) { + grid-column: 4; + grid-row: 1 / span 2; + } + + #canvasMainGrid > :nth-child(5) { + grid-column: 5; + grid-row: 1 / span 2; + } + + #canvasBottomGrid { + display: grid !important; + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + gap: 2.5mm !important; + margin-top: 2.5mm !important; + } + + .min-h-screen { + min-height: auto !important; + } + + .h-40, + .overflow-y-auto { + height: auto !important; + max-height: none !important; + overflow: visible !important; + } + + #problemList, + #keyMetricsList, + #unfairAdvantageList, + #channelsList, + #customerSegmentsCards, + #earlyAdoptersTags, + #existingAlternativesTags, + #costStructureList, + #revenueStreamsCards { + gap: 1.2mm !important; } - #export-container, header { - display: none; + + .shadow-sm, + .shadow-xl, + .hover\:shadow-md { + box-shadow: none !important; + } + + section { + background: #ffffff !important; + color: #1f2937 !important; + border: 1px solid #cbd5e1 !important; + border-left-width: 3px !important; + } + + .bg-primary, + .bg-primary-container, + .text-on-primary, + .text-on-primary, + .bg-primary-container\/30, + .bg-secondary-container\/30, + .bg-tertiary-container\/30, + .bg-surface-container, + .bg-surface-container-low, + .bg-surface-container-lowest, + .bg-surface-container-low\/50 { + background: #ffffff !important; + color: #1f2937 !important; + } + + h3[data-key="uniqueValueProposition"], + h4[data-key="highLevelConcept"] { + color: #1f2937 !important; + opacity: 1 !important; + } + + #canvasTitle { + font-size: 17pt !important; + line-height: 1.1 !important; + margin-bottom: 1mm !important; } - .canvas { - border: none; - padding: 0; - margin: 0; + + #lastEditLabel { + font-size: 9pt !important; + } + + .text-\[10px\], + .text-\[11px\], + .text-xs, + .text-sm { + font-size: 7.6pt !important; + line-height: 1.22 !important; + } + + .text-lg, + .text-base { + font-size: 8.4pt !important; + line-height: 1.22 !important; + } + + .text-3xl { + font-size: 17pt !important; + line-height: 1.1 !important; + } + + p, + li, + span, + h3, + h4 { + orphans: 2; + widows: 2; + } + + .-rotate-1 { + transform: none !important; + } + + #canvasMainGrid, + #canvasBottomGrid { + page-break-before: auto !important; + page-break-after: auto !important; + } + + .section-edit-trigger::after { + content: ""; + } + + .editable-text, + .list-item-text, + .list-item-text.is-editing-inline { + background: transparent !important; + box-shadow: none !important; } } diff --git a/todo.md b/todo.md deleted file mode 100644 index 6d66a34..0000000 --- a/todo.md +++ /dev/null @@ -1,5 +0,0 @@ -# Todo -- show icons on each chapter. -- make a icon-toggle-swith: on/off with a gear-symbol. How to make it persistant? -- like vim, mode-line comments: mdc: icon:on -- check next.md