Merge branch 'dev'
This commit is contained in:
commit
4139b829a1
|
|
@ -3,12 +3,23 @@ import { css, html } from "./tags";
|
|||
|
||||
const styles = css`
|
||||
:host {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
background: var(--rs-bg-surface, #fff);
|
||||
color: var(--rs-text-primary, #1e293b);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
min-width: 350px;
|
||||
min-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.note-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
@ -22,12 +33,14 @@ const styles = css`
|
|||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: move;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.note-title {
|
||||
|
|
@ -37,7 +50,9 @@ const styles = css`
|
|||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
width: 150px;
|
||||
min-width: 60px;
|
||||
max-width: 200px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.note-title::placeholder {
|
||||
|
|
@ -66,7 +81,9 @@ const styles = css`
|
|||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 36px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
|
|
@ -76,6 +93,7 @@ const styles = css`
|
|||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
|
|
@ -135,6 +153,7 @@ const styles = css`
|
|||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.editor {
|
||||
|
|
@ -147,6 +166,8 @@ const styles = css`
|
|||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
background: var(--rs-bg-surface-sunken, #0f172a);
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview {
|
||||
|
|
@ -156,6 +177,7 @@ const styles = css`
|
|||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
display: none;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preview.visible {
|
||||
|
|
@ -241,9 +263,10 @@ const styles = css`
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
border-top: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.word-count {
|
||||
|
|
@ -264,6 +287,77 @@ const styles = css`
|
|||
.save-status.unsaved {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
/* ── Semantic zoom levels ── */
|
||||
|
||||
/* Icon-only mode: zoomed far out */
|
||||
.zoom-icon {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.zoom-icon-emoji {
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.zoom-icon-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--rs-text-primary, #e2e8f0);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Summary mode: medium zoom */
|
||||
.zoom-summary {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.zoom-summary-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(135deg, #7c3aed, #8b5cf6);
|
||||
color: white;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zoom-summary-body {
|
||||
flex: 1;
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--rs-text-secondary, #94a3b8);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Zoom state classes applied by JS */
|
||||
:host(.zoom-level-icon) .note-wrapper { display: none; }
|
||||
:host(.zoom-level-icon) .zoom-icon { display: flex; }
|
||||
:host(.zoom-level-icon) .zoom-summary { display: none; }
|
||||
|
||||
:host(.zoom-level-summary) .note-wrapper { display: none; }
|
||||
:host(.zoom-level-summary) .zoom-icon { display: none; }
|
||||
:host(.zoom-level-summary) .zoom-summary { display: flex; }
|
||||
|
||||
:host(.zoom-level-full) .note-wrapper { display: flex; }
|
||||
:host(.zoom-level-full) .zoom-icon { display: none; }
|
||||
:host(.zoom-level-full) .zoom-summary { display: none; }
|
||||
`;
|
||||
|
||||
declare global {
|
||||
|
|
@ -292,12 +386,17 @@ export class FolkObsNote extends FolkShape {
|
|||
#mode: "edit" | "preview" | "split" = "edit";
|
||||
#isDirty = false;
|
||||
#lastSaved: Date | null = null;
|
||||
#zoomLevel: "icon" | "summary" | "full" = "full";
|
||||
#zoomRaf = 0;
|
||||
|
||||
#editor: HTMLTextAreaElement | null = null;
|
||||
#preview: HTMLElement | null = null;
|
||||
#titleInput: HTMLInputElement | null = null;
|
||||
#wordCountEl: HTMLElement | null = null;
|
||||
#saveStatusEl: HTMLElement | null = null;
|
||||
#iconTitleEl: HTMLElement | null = null;
|
||||
#summaryTitleEl: HTMLElement | null = null;
|
||||
#summaryBodyEl: HTMLElement | null = null;
|
||||
|
||||
get content() {
|
||||
return this.#content;
|
||||
|
|
@ -308,6 +407,7 @@ export class FolkObsNote extends FolkShape {
|
|||
if (this.#editor) this.#editor.value = value;
|
||||
this.#updatePreview();
|
||||
this.#updateWordCount();
|
||||
this.#updateZoomContent();
|
||||
}
|
||||
|
||||
get title() {
|
||||
|
|
@ -317,12 +417,14 @@ export class FolkObsNote extends FolkShape {
|
|||
set title(value: string) {
|
||||
this.#title = value;
|
||||
if (this.#titleInput) this.#titleInput.value = value;
|
||||
this.#updateZoomContent();
|
||||
}
|
||||
|
||||
override createRenderRoot() {
|
||||
const root = super.createRenderRoot();
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "note-wrapper";
|
||||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
|
|
@ -367,11 +469,25 @@ export class FolkObsNote extends FolkShape {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Replace the container div (slot's parent) with our wrapper
|
||||
// Semantic zoom: icon-only view
|
||||
const iconView = document.createElement("div");
|
||||
iconView.className = "zoom-icon";
|
||||
iconView.innerHTML = `<span class="zoom-icon-emoji">📝</span><span class="zoom-icon-title"></span>`;
|
||||
|
||||
// Semantic zoom: summary view
|
||||
const summaryView = document.createElement("div");
|
||||
summaryView.className = "zoom-summary";
|
||||
summaryView.innerHTML = `<div class="zoom-summary-header"><span>📝</span><span class="zoom-summary-title"></span></div><div class="zoom-summary-body"></div>`;
|
||||
|
||||
// Replace the container div (slot's parent) with our wrapper + zoom views
|
||||
const slot = root.querySelector("slot");
|
||||
const containerDiv = slot?.parentElement as HTMLElement;
|
||||
if (containerDiv) {
|
||||
containerDiv.replaceWith(wrapper);
|
||||
const frag = document.createDocumentFragment();
|
||||
frag.appendChild(wrapper);
|
||||
frag.appendChild(iconView);
|
||||
frag.appendChild(summaryView);
|
||||
containerDiv.replaceWith(frag);
|
||||
}
|
||||
|
||||
this.#editor = wrapper.querySelector(".editor");
|
||||
|
|
@ -379,6 +495,9 @@ export class FolkObsNote extends FolkShape {
|
|||
this.#titleInput = wrapper.querySelector(".note-title");
|
||||
this.#wordCountEl = wrapper.querySelector(".word-count");
|
||||
this.#saveStatusEl = wrapper.querySelector(".save-status");
|
||||
this.#iconTitleEl = iconView.querySelector(".zoom-icon-title");
|
||||
this.#summaryTitleEl = summaryView.querySelector(".zoom-summary-title");
|
||||
this.#summaryBodyEl = summaryView.querySelector(".zoom-summary-body");
|
||||
const saveBtn = wrapper.querySelector(".save-btn") as HTMLButtonElement;
|
||||
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||||
const toolbarBtns = wrapper.querySelectorAll(".toolbar-btn[data-action]");
|
||||
|
|
@ -391,6 +510,7 @@ export class FolkObsNote extends FolkShape {
|
|||
this.#updatePreview();
|
||||
this.#updateWordCount();
|
||||
this.#updateSaveStatus();
|
||||
this.#updateZoomContent();
|
||||
this.dispatchEvent(new CustomEvent("content-change", { detail: { content: this.#content } }));
|
||||
});
|
||||
|
||||
|
|
@ -399,6 +519,7 @@ export class FolkObsNote extends FolkShape {
|
|||
this.#title = this.#titleInput?.value || "Untitled";
|
||||
this.#isDirty = true;
|
||||
this.#updateSaveStatus();
|
||||
this.#updateZoomContent();
|
||||
this.dispatchEvent(new CustomEvent("title-change", { detail: { title: this.#title } }));
|
||||
});
|
||||
|
||||
|
|
@ -436,8 +557,10 @@ export class FolkObsNote extends FolkShape {
|
|||
this.dispatchEvent(new CustomEvent("close"));
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
this.#editor?.addEventListener("keydown", (e) => {
|
||||
// Keyboard shortcuts — stop all propagation from editor/title to prevent
|
||||
// canvas handlers (delete, arrow-move, space-pan) from firing
|
||||
const stopKeys = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
this.#save();
|
||||
|
|
@ -450,11 +573,51 @@ export class FolkObsNote extends FolkShape {
|
|||
e.preventDefault();
|
||||
this.#applyFormatting("italic");
|
||||
}
|
||||
});
|
||||
};
|
||||
this.#editor?.addEventListener("keydown", stopKeys);
|
||||
this.#titleInput?.addEventListener("keydown", (e) => e.stopPropagation());
|
||||
|
||||
// Semantic zoom: poll --canvas-scale CSS custom property
|
||||
this.classList.add("zoom-level-full");
|
||||
this.#updateZoomContent();
|
||||
const checkZoom = () => {
|
||||
const scaleStr = getComputedStyle(this).getPropertyValue("--canvas-scale");
|
||||
const s = parseFloat(scaleStr) || 1;
|
||||
let level: "icon" | "summary" | "full";
|
||||
if (s < 0.3) level = "icon";
|
||||
else if (s < 0.7) level = "summary";
|
||||
else level = "full";
|
||||
if (level !== this.#zoomLevel) {
|
||||
this.classList.remove(`zoom-level-${this.#zoomLevel}`);
|
||||
this.#zoomLevel = level;
|
||||
this.classList.add(`zoom-level-${level}`);
|
||||
this.#updateZoomContent();
|
||||
}
|
||||
this.#zoomRaf = requestAnimationFrame(checkZoom);
|
||||
};
|
||||
this.#zoomRaf = requestAnimationFrame(checkZoom);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.#zoomRaf) cancelAnimationFrame(this.#zoomRaf);
|
||||
}
|
||||
|
||||
#updateZoomContent() {
|
||||
if (this.#iconTitleEl) {
|
||||
this.#iconTitleEl.textContent = this.#title || "Untitled";
|
||||
}
|
||||
if (this.#summaryTitleEl) {
|
||||
this.#summaryTitleEl.textContent = this.#title || "Untitled";
|
||||
}
|
||||
if (this.#summaryBodyEl) {
|
||||
// Show first ~200 chars of content as plain text summary
|
||||
const plain = this.#content.replace(/[#*`>\[\]()-]/g, "").trim();
|
||||
this.#summaryBodyEl.textContent = plain.length > 200 ? plain.slice(0, 200) + "..." : plain || "Empty note";
|
||||
}
|
||||
}
|
||||
|
||||
#setMode(mode: "edit" | "preview" | "split", buttons: NodeListOf<Element>) {
|
||||
this.#mode = mode;
|
||||
|
||||
|
|
|
|||
|
|
@ -6305,6 +6305,8 @@
|
|||
function updateCanvasTransform() {
|
||||
// Transform only the content layer — canvas viewport stays fixed
|
||||
canvasContent.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
||||
// Expose scale as CSS custom property for semantic zoom in shapes
|
||||
canvasContent.style.setProperty("--canvas-scale", String(scale));
|
||||
// Adjust grid/dot pattern to track pan/zoom (skip for blank)
|
||||
const bgStyle = document.documentElement.getAttribute('data-canvas-bg') || 'grid';
|
||||
if (bgStyle !== 'blank') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue