Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 100 additions & 13 deletions packages/tok-ui/src/components/ActionHud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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?: {
Expand All @@ -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;
}

Expand All @@ -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;
Expand All @@ -103,17 +180,16 @@ 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`;
this.callbacks.onSizeChange(this.currentSize);
};
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);
Expand All @@ -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', () => {
Expand All @@ -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 {
Expand Down
19 changes: 10 additions & 9 deletions packages/tok-ui/src/components/ImportTemplateModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
60 changes: 55 additions & 5 deletions packages/tok-ui/src/components/SpreadCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
Loading