feat: add bookmark view modes (card/tile/cover) and enhance button block with visual config modal - Added three view modes for bookmark blocks: card (default with side image), tile (compact horizontal), and cover (large top image) - Implemented view mode submenu in block context menu with visual indicators for active mode - Enhanced bookmark favicon handling with fallback logic and error state tracking - Refactored button block with inline visual editor replacing text inputs - Created Button
129 lines
3.9 KiB
TypeScript
129 lines
3.9 KiB
TypeScript
import { Component, Input, Output, EventEmitter, signal } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { Block, ButtonProps } from '../../../core/models/block.model';
|
|
import { ButtonConfigModalComponent } from './button-config-modal.component';
|
|
|
|
@Component({
|
|
selector: 'app-button-block',
|
|
standalone: true,
|
|
imports: [CommonModule, ButtonConfigModalComponent],
|
|
template: `
|
|
<div class="relative group inline-block">
|
|
<!-- Rendered Button -->
|
|
<a
|
|
[href]="props.url"
|
|
[target]="props.openInNewTab ? '_blank' : '_self'"
|
|
class="inline-flex items-center justify-center font-medium transition-all select-none no-underline cursor-pointer"
|
|
[ngClass]="getButtonClasses()"
|
|
[style.background-color]="getBgColor()"
|
|
[style.color]="getTextColor()"
|
|
(click)="onClick($event)"
|
|
>
|
|
{{ props.label || 'Button' }}
|
|
</a>
|
|
|
|
<!-- Edit Trigger (visible on hover/focus if not configuring) -->
|
|
<button
|
|
*ngIf="!showConfig()"
|
|
class="absolute -top-2 -right-2 bg-surface2 border border-app rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity shadow-sm z-10"
|
|
title="Configure button"
|
|
(click)="openConfig($event)"
|
|
>
|
|
<svg class="w-3 h-3 text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Config Modal -->
|
|
@if (showConfig()) {
|
|
<app-button-config-modal
|
|
[props]="props"
|
|
[blockId]="block.id"
|
|
(saveProps)="onSaveConfig($event)"
|
|
(cancel)="closeConfig()"
|
|
/>
|
|
}
|
|
</div>
|
|
`
|
|
})
|
|
export class ButtonBlockComponent {
|
|
@Input({ required: true }) block!: Block<ButtonProps>;
|
|
@Output() update = new EventEmitter<ButtonProps>();
|
|
|
|
showConfig = signal(false);
|
|
|
|
ngOnInit() {
|
|
// If new button (empty label), open config immediately
|
|
if (!this.props.label) {
|
|
this.showConfig.set(true);
|
|
}
|
|
}
|
|
|
|
get props(): ButtonProps {
|
|
return this.block.props;
|
|
}
|
|
|
|
getButtonClasses(): string {
|
|
const classes: string[] = [];
|
|
|
|
// Size
|
|
switch (this.props.size) {
|
|
case 'small': classes.push('px-3 py-1.5 text-xs'); break;
|
|
case 'large': classes.push('px-6 py-3 text-base'); break;
|
|
default: classes.push('px-4 py-2 text-sm'); break; // medium
|
|
}
|
|
|
|
// Shape
|
|
if (this.props.shape === 'pill') {
|
|
classes.push('rounded-full');
|
|
} else if (this.props.shape === 'rounded') {
|
|
classes.push('rounded-lg');
|
|
} else {
|
|
classes.push('rounded-md'); // default/square
|
|
}
|
|
|
|
// Variant / Style
|
|
if (this.props.variant === '3d') {
|
|
classes.push('border-b-4 border-black/20 active:border-b-0 active:translate-y-1');
|
|
} else if (this.props.variant === 'shadow') {
|
|
classes.push('shadow-lg hover:shadow-xl hover:-translate-y-0.5');
|
|
} else if (this.props.variant === 'outline') {
|
|
classes.push('border-2');
|
|
}
|
|
|
|
return classes.join(' ');
|
|
}
|
|
|
|
getBgColor(): string {
|
|
if (this.props.variant === 'outline') return 'transparent';
|
|
return this.props.backgroundColor || '#3b82f6';
|
|
}
|
|
|
|
getTextColor(): string {
|
|
if (this.props.variant === 'outline') {
|
|
return this.props.backgroundColor || '#3b82f6';
|
|
}
|
|
// Simple contrast check could be added here, assuming white for now for colored buttons
|
|
return '#ffffff';
|
|
}
|
|
|
|
onClick(event: MouseEvent) {
|
|
// Prevent navigation in editor
|
|
event.preventDefault();
|
|
}
|
|
|
|
openConfig(event: MouseEvent) {
|
|
event.stopPropagation();
|
|
this.showConfig.set(true);
|
|
}
|
|
|
|
closeConfig() {
|
|
this.showConfig.set(false);
|
|
}
|
|
|
|
onSaveConfig(newProps: ButtonProps) {
|
|
this.update.emit(newProps);
|
|
this.showConfig.set(false);
|
|
}
|
|
}
|