diff --git a/packages/tok-ui/src/components/ActionHud.ts b/packages/tok-ui/src/components/ActionHud.ts index 05d8fd3..9c6f270 100644 --- a/packages/tok-ui/src/components/ActionHud.ts +++ b/packages/tok-ui/src/components/ActionHud.ts @@ -21,6 +21,16 @@ export class ActionHud { private currentStyle = 'gemara'; private currentFont = ''; + // Drag state + private isDragging = false; + private dragStartX = 0; + private dragStartY = 0; + private initialLeft = 0; + private initialTop = 0; + private userHasMoved = false; + private customLeft: number | null = null; + private customTop: number | null = null; + constructor(callbacks: ActionHudCallbacks) { this.callbacks = callbacks; this.element = document.createElement('div'); @@ -44,6 +54,11 @@ export class ActionHud { this.callbacks.onDismiss(); } }); + + // Window drag event listeners + window.addEventListener('pointermove', (e) => this.onPointerMove(e)); + window.addEventListener('pointerup', () => this.onPointerUp()); + window.addEventListener('pointercancel', () => this.onPointerUp()); } public showAt(x: number, y: number, initialValues?: { @@ -64,18 +79,34 @@ export class ActionHud { this.isBold = initialValues.font_weight >= 700; } if (initialValues?.align) this.currentAlign = initialValues.align; - if (initialValues?.style) this.currentStyle = initialValues.style; if (initialValues?.font) this.currentFont = initialValues.font; this.render(); - // Position HUD centered above (x, y), kept fully inside the window this.element.style.display = 'flex'; - const width = this.element.offsetWidth || 360; - const maxLeft = Math.max(10, window.innerWidth - width - 10); - this.element.style.left = `${Math.min(maxLeft, Math.max(10, x - width / 2))}px`; - this.element.style.top = `${Math.max(50, y - 48)}px`; + const width = this.element.offsetWidth || 380; + const height = this.element.offsetHeight || 42; + + if (this.userHasMoved && this.customLeft !== null && this.customTop !== null) { + // Keep at the user's preferred location so it doesn't obstruct their view + const maxLeft = Math.max(10, window.innerWidth - width - 10); + const maxTop = Math.max(45, window.innerHeight - height - 10); + this.element.style.left = `${Math.min(maxLeft, Math.max(10, this.customLeft))}px`; + this.element.style.top = `${Math.min(maxTop, Math.max(45, this.customTop))}px`; + } else { + // Position smartly: if selection is high up, put HUD below it; otherwise put it well above with clearance + const maxLeft = Math.max(10, window.innerWidth - width - 10); + const targetLeft = Math.min(maxLeft, Math.max(10, x - width / 2)); + let targetTop = y - 56; + if (targetTop < 50) { + // Place below the click so it never disappears off-screen or covers text + targetTop = y + 32; + } + this.element.style.left = `${targetLeft}px`; + this.element.style.top = `${targetTop}px`; + } + this.isVisible = true; } @@ -88,9 +119,55 @@ export class ActionHud { return this.isVisible; } + private onPointerDown(e: PointerEvent): void { + if (e.button !== 0) return; + this.isDragging = true; + this.dragStartX = e.clientX; + this.dragStartY = e.clientY; + this.initialLeft = this.element.offsetLeft; + this.initialTop = this.element.offsetTop; + this.element.style.cursor = 'grabbing'; + e.preventDefault(); + } + + private onPointerMove(e: PointerEvent): void { + if (!this.isDragging) return; + const dx = e.clientX - this.dragStartX; + const dy = e.clientY - this.dragStartY; + const width = this.element.offsetWidth || 380; + const height = this.element.offsetHeight || 42; + + const newLeft = Math.min(window.innerWidth - width - 5, Math.max(5, this.initialLeft + dx)); + const newTop = Math.min(window.innerHeight - height - 5, Math.max(40, this.initialTop + dy)); + + this.element.style.left = `${newLeft}px`; + this.element.style.top = `${newTop}px`; + this.userHasMoved = true; + this.customLeft = newLeft; + this.customTop = newTop; + } + + private onPointerUp(): void { + if (this.isDragging) { + this.isDragging = false; + this.element.style.cursor = 'default'; + } + } + private render(): void { this.element.innerHTML = ''; + // 0. Draggable handle + const dragHandle = el('div', 'tok-hud-drag-handle', { + title: 'גרור להזזת הסרגל לכל מקום במסך', + style: 'cursor:grab;display:flex;align-items:center;justify-content:center;padding:0 4px;color:var(--tok-text-muted);user-select:none;height:100%;' + }); + dragHandle.appendChild(icon('layers', 14)); + dragHandle.addEventListener('pointerdown', (e) => this.onPointerDown(e)); + this.element.appendChild(dragHandle); + + this.element.appendChild(this.createDivider()); + // 1. Font family const fontSelect = selectField(t('hudFont'), getAvailableFonts().map((f) => ({ value: f.family, label: f.label })), this.currentFont || DEFAULT_DOCUMENT_FONT, (value) => { this.currentFont = value; @@ -103,9 +180,9 @@ export class ActionHud { // 2. Font size stepper const sizeInput = el('input', 'tok-input tok-input-number', { type: 'text', 'aria-label': t('inspFontSize') }); sizeInput.value = `${this.currentSize}pt`; - sizeInput.style.width = '52px'; + sizeInput.style.width = '48px'; sizeInput.style.textAlign = 'center'; - sizeInput.style.padding = '0 4px'; + sizeInput.style.padding = '0 2px'; const step = (delta: number) => { this.currentSize = Math.max(6, Math.min(150, this.currentSize + delta)); sizeInput.value = `${this.currentSize}pt`; @@ -113,7 +190,6 @@ export class ActionHud { }; sizeInput.addEventListener('change', () => { const parsed = parseInt(sizeInput.value, 10); - // Same 6–150pt range the −/+ steppers enforce. if (!isNaN(parsed) && parsed >= 6 && parsed <= 150) { this.currentSize = parsed; this.callbacks.onSizeChange(this.currentSize); @@ -136,13 +212,15 @@ export class ActionHud { }); this.element.appendChild(boldBtn); - // 4. Alignment (cycles right → justify → center) + // 4. Alignment button (Icon only - prevents text overflow!) const alignIcon = () => (this.currentAlign === 'justify' ? 'alignJustify' : this.currentAlign === 'center' ? 'alignCenter' : 'alignRight'); const alignLabel = () => t(this.currentAlign === 'justify' ? 'hudAlignJustify' : this.currentAlign === 'center' ? 'hudAlignCenter' : 'hudAlignRight'); - const alignBtn = el('button', 'tok-hud-btn', { type: 'button', title: t('hudAlignTitle') }); + const alignBtn = el('button', 'tok-hud-btn', { type: 'button' }); const paintAlign = () => { - alignBtn.replaceChildren(icon(alignIcon(), 15), el('span', undefined, undefined, alignLabel())); - alignBtn.setAttribute('aria-label', `${t('hudAlignTitle')}: ${alignLabel()}`); + alignBtn.replaceChildren(icon(alignIcon(), 15)); + const lbl = alignLabel(); + alignBtn.title = `${t('hudAlignTitle')}: ${lbl}`; + alignBtn.setAttribute('aria-label', `${t('hudAlignTitle')}: ${lbl}`); }; paintAlign(); alignBtn.addEventListener('click', () => { @@ -165,6 +243,15 @@ export class ActionHud { this.currentStyle = value; this.callbacks.onStyleChange(value); }, { title: t('hudQuickStyle') })); + + this.element.appendChild(this.createDivider()); + + // 6. Close button + const closeBtn = iconButton('close', 'סגירת סרגל צף (Esc)', () => { + this.hide(); + this.callbacks.onDismiss(); + }, { size: 14, className: 'tok-hud-btn', attrs: { style: 'color:var(--tok-text-muted);' } }); + this.element.appendChild(closeBtn); } private createDivider(): HTMLElement { diff --git a/packages/tok-ui/src/components/ImportTemplateModal.ts b/packages/tok-ui/src/components/ImportTemplateModal.ts index 4d40785..2a240b4 100644 --- a/packages/tok-ui/src/components/ImportTemplateModal.ts +++ b/packages/tok-ui/src/components/ImportTemplateModal.ts @@ -111,36 +111,37 @@ export class ImportTemplateModal { dialog.appendChild(head); // Body: template grid - const body = el('div', 'tok-modal-body', { style: 'padding:20px 24px;display:flex;flex-direction:column;gap:10px;max-height:420px;overflow-y:auto;' }); + const body = el('div', 'tok-modal-body', { style: 'padding:16px 22px;display:flex;flex-direction:column;gap:8px;max-height:56vh;overflow-y:auto;' }); for (const opt of TEMPLATE_OPTIONS) { const card = el('button', 'tok-template-option-btn', { type: 'button', - style: 'display:flex;align-items:center;justify-content:space-between;gap:14px;padding:12px 16px;border-radius:8px;border:1px solid var(--tok-border-strong);background:var(--tok-surface-elevated, #1E293B);color:var(--tok-text-primary);cursor:pointer;text-align:start;transition:all 0.15s ease;' + style: 'display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-radius:10px;border:1px solid var(--tok-border-strong);background:var(--tok-bg-surface-2, #1E293B);color:var(--tok-text-primary);cursor:pointer;text-align:start;transition:all 0.15s ease;flex-shrink:0;' }); - const info = el('div', undefined, { style: 'display:flex;flex-direction:column;gap:3px;flex:1;' }); + const info = el('div', undefined, { style: 'display:flex;flex-direction:column;gap:3px;flex:1;min-width:0;' }); const optTitleRow = el('div', undefined, { style: 'display:flex;align-items:center;gap:8px;' }); - optTitleRow.appendChild(el('strong', undefined, { style: 'font-size:14px;' }, t(opt.titleKey))); + optTitleRow.appendChild(el('strong', undefined, { style: 'font-size:14px;font-weight:700;' }, t(opt.titleKey))); if (opt.badge) { optTitleRow.appendChild(el('span', undefined, { style: `font-size:11px;font-weight:600;padding:2px 8px;border-radius:12px;background:${opt.badgeColor}22;color:${opt.badgeColor};` }, opt.badge)); } info.appendChild(optTitleRow); - info.appendChild(el('span', undefined, { style: 'font-size:12px;color:var(--tok-text-muted);' }, t(opt.descKey))); + info.appendChild(el('span', undefined, { style: 'font-size:12px;color:var(--tok-text-secondary);line-height:1.4;' }, t(opt.descKey))); card.appendChild(info); - const arrow = el('span', undefined, { style: 'opacity:0.6;font-size:16px;' }, '←'); + const arrow = el('span', 'tok-flip-rtl', { style: 'opacity:0.6;font-size:16px;flex:none;' }); + arrow.appendChild(icon('arrowForward', 16)); card.appendChild(arrow); card.addEventListener('mouseenter', () => { - card.style.borderColor = 'var(--tok-accent, #3B82F6)'; - card.style.background = 'rgba(59, 130, 246, 0.1)'; + card.style.borderColor = 'var(--tok-accent-primary, #3B82F6)'; + card.style.background = 'var(--tok-accent-soft, rgba(59, 130, 246, 0.1))'; }); card.addEventListener('mouseleave', () => { card.style.borderColor = 'var(--tok-border-strong)'; - card.style.background = 'var(--tok-surface-elevated, #1E293B)'; + card.style.background = 'var(--tok-bg-surface-2, #1E293B)'; }); card.addEventListener('click', () => { diff --git a/packages/tok-ui/src/components/SpreadCanvas.ts b/packages/tok-ui/src/components/SpreadCanvas.ts index cd3bc4a..b6ddcfb 100644 --- a/packages/tok-ui/src/components/SpreadCanvas.ts +++ b/packages/tok-ui/src/components/SpreadCanvas.ts @@ -299,19 +299,69 @@ export class SpreadCanvas { this.zoomHud = el('div', 'tok-canvas-zoom-hud', { role: 'toolbar', 'aria-label': 'בקרת תקריב וזום', - style: 'position:absolute;bottom:20px;inset-inline-end:24px;z-index:25;display:flex;align-items:center;gap:4px;padding:4px 6px;background:var(--tok-bg-surface-1);border:1px solid var(--tok-border-strong);border-radius:24px;box-shadow:0 4px 18px rgba(0,0,0,0.18);backdrop-filter:blur(8px);' + style: 'position:absolute;bottom:20px;inset-inline-end:24px;z-index:25;display:flex;align-items:center;gap:3px;padding:3px 6px;background:var(--tok-bg-surface-1);border:1px solid var(--tok-border-strong);border-radius:24px;box-shadow:0 4px 18px rgba(0,0,0,0.18);backdrop-filter:blur(8px);user-select:none;' }); + // Drag handle for Zoom HUD + const zoomDrag = el('div', 'tok-zoom-drag-handle', { + title: 'גרור להזזת סרגל הזום', + style: 'cursor:grab;display:flex;align-items:center;justify-content:center;padding:0 3px;color:var(--tok-text-muted);' + }); + zoomDrag.appendChild(icon('layers', 12)); + + let isZoomDragging = false; + let zStartX = 0; + let zStartY = 0; + let zInitLeft = 0; + let zInitTop = 0; + + zoomDrag.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; + isZoomDragging = true; + zStartX = e.clientX; + zStartY = e.clientY; + const rect = this.zoomHud.getBoundingClientRect(); + const parentRect = this.element.getBoundingClientRect(); + zInitLeft = rect.left - parentRect.left; + zInitTop = rect.top - parentRect.top; + this.zoomHud.style.insetInlineEnd = 'auto'; + this.zoomHud.style.bottom = 'auto'; + this.zoomHud.style.left = `${zInitLeft}px`; + this.zoomHud.style.top = `${zInitTop}px`; + zoomDrag.style.cursor = 'grabbing'; + e.preventDefault(); + }); + + window.addEventListener('pointermove', (e) => { + if (!isZoomDragging) return; + const dx = e.clientX - zStartX; + const dy = e.clientY - zStartY; + const parentRect = this.element.getBoundingClientRect(); + const newL = Math.max(10, Math.min(parentRect.width - 150, zInitLeft + dx)); + const newT = Math.max(10, Math.min(parentRect.height - 45, zInitTop + dy)); + this.zoomHud.style.left = `${newL}px`; + this.zoomHud.style.top = `${newT}px`; + }); + + window.addEventListener('pointerup', () => { + if (isZoomDragging) { + isZoomDragging = false; + zoomDrag.style.cursor = 'grab'; + } + }); + + this.zoomHud.appendChild(zoomDrag); + const zoomOut = iconButton('minus', 'הקטנת תצוגה (Ctrl+-)', () => this.applyUserZoom(this.zoomPercent - 10), { size: 13, - attrs: { style: 'width:26px;height:26px;border-radius:50%;' } + attrs: { style: 'width:24px;height:24px;border-radius:50%;' } }); this.zoomHud.appendChild(zoomOut); this.zoomDisplayBtn = el('button', 'tok-btn tok-btn-ghost tok-btn-xs', { type: 'button', title: 'איפוס ל-100% (Ctrl+0)', - style: 'font-size:11px;font-weight:600;min-width:44px;padding:0 4px;height:26px;' + style: 'font-size:11px;font-weight:600;min-width:40px;padding:0 3px;height:24px;' }, `${this.zoomPercent}%`) as HTMLButtonElement; this.zoomDisplayBtn.addEventListener('click', () => { if (this.zoomPercent === 100) this.fitToWindow(); @@ -321,13 +371,13 @@ export class SpreadCanvas { const zoomIn = iconButton('plus', 'הגדלת תצוגה (Ctrl++)', () => this.applyUserZoom(this.zoomPercent + 10), { size: 13, - attrs: { style: 'width:26px;height:26px;border-radius:50%;' } + attrs: { style: 'width:24px;height:24px;border-radius:50%;' } }); this.zoomHud.appendChild(zoomIn); const fitBtn = button('התאם', { className: 'tok-btn tok-btn-ghost tok-btn-xs', - attrs: { title: 'התאמה מלאה לחלון', style: 'font-size:11px;padding:0 6px;height:26px;' }, + attrs: { title: 'התאמה מלאה לחלון', style: 'font-size:11px;padding:0 5px;height:24px;' }, onClick: () => this.fitToWindow() }); this.zoomHud.appendChild(fitBtn);