feat: add gradient entity modal and fix color picker clipping

Add full gradient editor modal with name, description, visual stop
editor, tags, and dirty checking. Gradient editor now supports ID
prefix to avoid DOM conflicts between CSS editor and standalone modal.

Fix color picker popover clipped by template-card overflow:hidden.
Fix gradient canvas not sizing correctly in standalone modal.
This commit is contained in:
2026-03-24 13:58:51 +03:00
parent 227b82f522
commit c0d0d839dc
14 changed files with 465 additions and 161 deletions

View File

@@ -54,6 +54,12 @@ let _gradientStops: GradientStop[] = [];
let _gradientSelectedIdx: number = -1;
let _gradientDragging: GradientDragState | null = null;
let _gradientOnChange: (() => void) | null = null;
let _idPrefix: string = '';
/** Set an ID prefix for DOM elements (e.g. 'ge-' to find 'ge-gradient-canvas'). */
export function gradientSetIdPrefix(prefix: string): void { _idPrefix = prefix; }
function _el(id: string): HTMLElement | null { return document.getElementById(_idPrefix + id); }
/** Set a callback that fires whenever stops change. */
export function gradientSetOnChange(fn: (() => void) | null): void { _gradientOnChange = fn; }
@@ -215,7 +221,7 @@ export function gradientRenderAll(): void {
}
function _gradientRenderCanvas(): void {
const canvas = document.getElementById('gradient-canvas') as HTMLCanvasElement | null;
const canvas = _el('gradient-canvas') as HTMLCanvasElement | null;
if (!canvas) return;
// Sync canvas pixel width to its CSS display width
@@ -241,7 +247,7 @@ function _gradientRenderCanvas(): void {
}
function _gradientRenderMarkers(): void {
const track = document.getElementById('gradient-markers-track');
const track = _el('gradient-markers-track');
if (!track) return;
track.innerHTML = '';
@@ -276,7 +282,7 @@ function _gradientSelectStop(idx: number): void {
}
function _gradientRenderStopList(): void {
const list = document.getElementById('gradient-stops-list');
const list = _el('gradient-stops-list');
if (!list) return;
list.innerHTML = '';
@@ -305,7 +311,7 @@ function _gradientRenderStopList(): void {
row.addEventListener('mousedown', () => _gradientSelectStop(idx));
// Position
const posInput = row.querySelector('.gradient-stop-pos');
const posInput = row.querySelector('.gradient-stop-pos')!;
posInput.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
const val = Math.min(1, Math.max(0, parseFloat(target.value) || 0));
@@ -316,7 +322,7 @@ function _gradientRenderStopList(): void {
posInput.addEventListener('focus', () => _gradientSelectStop(idx));
// Left color
row.querySelector('.gradient-stop-color').addEventListener('input', (e) => {
row.querySelector('.gradient-stop-color')!.addEventListener('input', (e) => {
const val = (e.target as HTMLInputElement).value;
_gradientStops[idx].color = hexToRgbArray(val);
const markers = document.querySelectorAll('.gradient-marker');
@@ -325,7 +331,7 @@ function _gradientRenderStopList(): void {
});
// Bidirectional toggle
row.querySelector('.gradient-stop-bidir-btn').addEventListener('click', (e) => {
row.querySelector('.gradient-stop-bidir-btn')!.addEventListener('click', (e) => {
e.stopPropagation();
_gradientStops[idx].colorRight = _gradientStops[idx].colorRight
? null
@@ -335,13 +341,13 @@ function _gradientRenderStopList(): void {
});
// Right color
row.querySelector('.gradient-stop-color-right').addEventListener('input', (e) => {
row.querySelector('.gradient-stop-color-right')!.addEventListener('input', (e) => {
_gradientStops[idx].colorRight = hexToRgbArray((e.target as HTMLInputElement).value);
_gradientRenderCanvas();
});
// Remove
row.querySelector('.btn-danger').addEventListener('click', (e) => {
row.querySelector('.btn-danger')!.addEventListener('click', (e) => {
e.stopPropagation();
if (_gradientStops.length > 2) {
_gradientStops.splice(idx, 1);
@@ -382,7 +388,7 @@ export function gradientAddStop(position?: number): void {
/* ── Drag ─────────────────────────────────────────────────────── */
function _gradientStartDrag(e: MouseEvent, idx: number): void {
const track = document.getElementById('gradient-markers-track');
const track = _el('gradient-markers-track');
if (!track) return;
_gradientDragging = { idx, trackRect: track.getBoundingClientRect() };
@@ -442,7 +448,7 @@ export function deleteCustomGradientPreset(name: string): void {
/* ── Track click → add stop ───────────────────────────────────── */
function _gradientSetupTrackClick(): void {
const track = document.getElementById('gradient-markers-track');
const track = _el('gradient-markers-track');
if (!track || (track as any)._gradientClickBound) return;
(track as any)._gradientClickBound = true;