Merge branch 'dev'
This commit is contained in:
commit
4139b829a1
|
|
@ -3,12 +3,23 @@ import { css, html } from "./tags";
|
||||||
|
|
||||||
const styles = css`
|
const styles = css`
|
||||||
:host {
|
:host {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column;
|
||||||
background: var(--rs-bg-surface, #fff);
|
background: var(--rs-bg-surface, #fff);
|
||||||
color: var(--rs-text-primary, #1e293b);
|
color: var(--rs-text-primary, #1e293b);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|
@ -22,12 +33,14 @@ const styles = css`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-title {
|
.note-title {
|
||||||
|
|
@ -37,7 +50,9 @@ const styles = css`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
outline: none;
|
outline: none;
|
||||||
width: 150px;
|
min-width: 60px;
|
||||||
|
max-width: 200px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-title::placeholder {
|
.note-title::placeholder {
|
||||||
|
|
@ -66,7 +81,9 @@ const styles = css`
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100% - 36px);
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|
@ -76,6 +93,7 @@ const styles = css`
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-bottom: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
|
border-bottom: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-btn {
|
.toolbar-btn {
|
||||||
|
|
@ -135,6 +153,7 @@ const styles = css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
|
|
@ -147,6 +166,8 @@ const styles = css`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
background: var(--rs-bg-surface-sunken, #0f172a);
|
background: var(--rs-bg-surface-sunken, #0f172a);
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
|
|
@ -156,6 +177,7 @@ const styles = css`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
display: none;
|
display: none;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview.visible {
|
.preview.visible {
|
||||||
|
|
@ -241,9 +263,10 @@ const styles = css`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-top: 1px solid #e2e8f0;
|
border-top: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.word-count {
|
.word-count {
|
||||||
|
|
@ -264,6 +287,77 @@ const styles = css`
|
||||||
.save-status.unsaved {
|
.save-status.unsaved {
|
||||||
color: #f59e0b;
|
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 {
|
declare global {
|
||||||
|
|
@ -292,12 +386,17 @@ export class FolkObsNote extends FolkShape {
|
||||||
#mode: "edit" | "preview" | "split" = "edit";
|
#mode: "edit" | "preview" | "split" = "edit";
|
||||||
#isDirty = false;
|
#isDirty = false;
|
||||||
#lastSaved: Date | null = null;
|
#lastSaved: Date | null = null;
|
||||||
|
#zoomLevel: "icon" | "summary" | "full" = "full";
|
||||||
|
#zoomRaf = 0;
|
||||||
|
|
||||||
#editor: HTMLTextAreaElement | null = null;
|
#editor: HTMLTextAreaElement | null = null;
|
||||||
#preview: HTMLElement | null = null;
|
#preview: HTMLElement | null = null;
|
||||||
#titleInput: HTMLInputElement | null = null;
|
#titleInput: HTMLInputElement | null = null;
|
||||||
#wordCountEl: HTMLElement | null = null;
|
#wordCountEl: HTMLElement | null = null;
|
||||||
#saveStatusEl: HTMLElement | null = null;
|
#saveStatusEl: HTMLElement | null = null;
|
||||||
|
#iconTitleEl: HTMLElement | null = null;
|
||||||
|
#summaryTitleEl: HTMLElement | null = null;
|
||||||
|
#summaryBodyEl: HTMLElement | null = null;
|
||||||
|
|
||||||
get content() {
|
get content() {
|
||||||
return this.#content;
|
return this.#content;
|
||||||
|
|
@ -308,6 +407,7 @@ export class FolkObsNote extends FolkShape {
|
||||||
if (this.#editor) this.#editor.value = value;
|
if (this.#editor) this.#editor.value = value;
|
||||||
this.#updatePreview();
|
this.#updatePreview();
|
||||||
this.#updateWordCount();
|
this.#updateWordCount();
|
||||||
|
this.#updateZoomContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
|
|
@ -317,12 +417,14 @@ export class FolkObsNote extends FolkShape {
|
||||||
set title(value: string) {
|
set title(value: string) {
|
||||||
this.#title = value;
|
this.#title = value;
|
||||||
if (this.#titleInput) this.#titleInput.value = value;
|
if (this.#titleInput) this.#titleInput.value = value;
|
||||||
|
this.#updateZoomContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
override createRenderRoot() {
|
override createRenderRoot() {
|
||||||
const root = super.createRenderRoot();
|
const root = super.createRenderRoot();
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "note-wrapper";
|
||||||
wrapper.innerHTML = html`
|
wrapper.innerHTML = html`
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="header-title">
|
<span class="header-title">
|
||||||
|
|
@ -367,11 +469,25 @@ export class FolkObsNote extends FolkShape {
|
||||||
</div>
|
</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 slot = root.querySelector("slot");
|
||||||
const containerDiv = slot?.parentElement as HTMLElement;
|
const containerDiv = slot?.parentElement as HTMLElement;
|
||||||
if (containerDiv) {
|
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");
|
this.#editor = wrapper.querySelector(".editor");
|
||||||
|
|
@ -379,6 +495,9 @@ export class FolkObsNote extends FolkShape {
|
||||||
this.#titleInput = wrapper.querySelector(".note-title");
|
this.#titleInput = wrapper.querySelector(".note-title");
|
||||||
this.#wordCountEl = wrapper.querySelector(".word-count");
|
this.#wordCountEl = wrapper.querySelector(".word-count");
|
||||||
this.#saveStatusEl = wrapper.querySelector(".save-status");
|
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 saveBtn = wrapper.querySelector(".save-btn") as HTMLButtonElement;
|
||||||
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||||||
const toolbarBtns = wrapper.querySelectorAll(".toolbar-btn[data-action]");
|
const toolbarBtns = wrapper.querySelectorAll(".toolbar-btn[data-action]");
|
||||||
|
|
@ -391,6 +510,7 @@ export class FolkObsNote extends FolkShape {
|
||||||
this.#updatePreview();
|
this.#updatePreview();
|
||||||
this.#updateWordCount();
|
this.#updateWordCount();
|
||||||
this.#updateSaveStatus();
|
this.#updateSaveStatus();
|
||||||
|
this.#updateZoomContent();
|
||||||
this.dispatchEvent(new CustomEvent("content-change", { detail: { content: this.#content } }));
|
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.#title = this.#titleInput?.value || "Untitled";
|
||||||
this.#isDirty = true;
|
this.#isDirty = true;
|
||||||
this.#updateSaveStatus();
|
this.#updateSaveStatus();
|
||||||
|
this.#updateZoomContent();
|
||||||
this.dispatchEvent(new CustomEvent("title-change", { detail: { title: this.#title } }));
|
this.dispatchEvent(new CustomEvent("title-change", { detail: { title: this.#title } }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -436,8 +557,10 @@ export class FolkObsNote extends FolkShape {
|
||||||
this.dispatchEvent(new CustomEvent("close"));
|
this.dispatchEvent(new CustomEvent("close"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts — stop all propagation from editor/title to prevent
|
||||||
this.#editor?.addEventListener("keydown", (e) => {
|
// canvas handlers (delete, arrow-move, space-pan) from firing
|
||||||
|
const stopKeys = (e: KeyboardEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.#save();
|
this.#save();
|
||||||
|
|
@ -450,11 +573,51 @@ export class FolkObsNote extends FolkShape {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.#applyFormatting("italic");
|
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;
|
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>) {
|
#setMode(mode: "edit" | "preview" | "split", buttons: NodeListOf<Element>) {
|
||||||
this.#mode = mode;
|
this.#mode = mode;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6305,6 +6305,8 @@
|
||||||
function updateCanvasTransform() {
|
function updateCanvasTransform() {
|
||||||
// Transform only the content layer — canvas viewport stays fixed
|
// Transform only the content layer — canvas viewport stays fixed
|
||||||
canvasContent.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
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)
|
// Adjust grid/dot pattern to track pan/zoom (skip for blank)
|
||||||
const bgStyle = document.documentElement.getAttribute('data-canvas-bg') || 'grid';
|
const bgStyle = document.documentElement.getAttribute('data-canvas-bg') || 'grid';
|
||||||
if (bgStyle !== 'blank') {
|
if (bgStyle !== 'blank') {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue