5 Commits

Author SHA1 Message Date
be4c98b543 fix: show template name instead of ID in filter list and card badges
All checks were successful
Lint & Test / test (push) Successful in 1m7s
Collapsed filter cards in the modal showed raw template IDs (e.g.
pp_cb72e227) instead of resolving select options to their labels.
Card filter chain badges now include the referenced template name.
2026-03-25 23:56:40 +03:00
dca2d212b1 fix: clip graph node title and subtitle to prevent overflow
Long entity names overflowed past the icon area on graph cards.
Added SVG clipPath to constrain text within the node bounds.
2026-03-25 23:56:30 +03:00
53986f8d95 fix: replace emoji with SVG icons on weather and daylight cards
Weather card used  and 🌡 emoji, daylight card used 🕐 and .
Replaced with ICON_FAST_FORWARD, ICON_THERMOMETER, and ICON_CLOCK.
Added thermometer icon path.
2026-03-25 23:56:21 +03:00
a4a9f6f77f fix: send gradient_id instead of palette in effect transient preview
All checks were successful
Lint & Test / test (push) Successful in 1m19s
The preview config was sending `palette` which defaults to "fire" on
the server, ignoring the user's selected gradient. Also removed the
dead fallback notification block and stale custom_palette check.
2026-03-25 23:43:33 +03:00
9fcfdb8570 ci: use sparse checkout for release notes in release workflow
Only fetch RELEASE_NOTES.md instead of full repo checkout, and
simplify the file detection to a direct path check.
2026-03-25 23:43:31 +03:00
8 changed files with 42 additions and 14 deletions

View File

@@ -12,8 +12,11 @@ jobs:
outputs:
release_id: ${{ steps.create.outputs.release_id }}
steps:
- name: Checkout
- name: Fetch RELEASE_NOTES.md only
uses: actions/checkout@v4
with:
sparse-checkout: RELEASE_NOTES.md
sparse-checkout-cone-mode: false
- name: Create Gitea release
id: create
@@ -33,11 +36,9 @@ jobs:
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
DOCKER_IMAGE="${SERVER_HOST}/${REPO}"
# Scan for RELEASE_NOTES.md (check repo root first, then recursively)
NOTES_FILE=$(find . -maxdepth 3 -name "RELEASE_NOTES.md" -type f | head -1)
if [ -n "$NOTES_FILE" ]; then
export RELEASE_NOTES=$(cat "$NOTES_FILE")
echo "Found release notes: $NOTES_FILE"
if [ -f RELEASE_NOTES.md ]; then
export RELEASE_NOTES=$(cat RELEASE_NOTES.md)
echo "Found RELEASE_NOTES.md"
else
export RELEASE_NOTES=""
echo "No RELEASE_NOTES.md found"

View File

@@ -138,6 +138,10 @@ export class FilterListManager {
if (filterDef && !isExpanded) {
summary = filterDef.options_schema.map(opt => {
const val = fi.options[opt.key] !== undefined ? fi.options[opt.key] : opt.default;
if (opt.type === 'select' && Array.isArray(opt.choices)) {
const choice = opt.choices.find(c => c.value === val);
if (choice) return choice.label;
}
return val;
}).join(', ');
}

View File

@@ -336,10 +336,17 @@ function renderNode(node: GraphNode, callbacks: NodeCallbacks): SVGElement {
g.appendChild(dot);
}
// Clip path for title/subtitle text (prevent overflow past icon area)
const clipId = `clip-text-${id.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
const clipPath = svgEl('clipPath', { id: clipId });
clipPath.appendChild(svgEl('rect', { x: 14, y: 0, width: width - 48, height }));
g.appendChild(clipPath);
// Title (shift left edge for icon to have room)
const title = svgEl('text', {
class: 'graph-node-title',
x: 16, y: 24,
'clip-path': `url(#${clipId})`,
});
title.textContent = name;
g.appendChild(title);
@@ -349,6 +356,7 @@ function renderNode(node: GraphNode, callbacks: NodeCallbacks): SVGElement {
const sub = svgEl('text', {
class: 'graph-node-subtitle',
x: 16, y: 42,
'clip-path': `url(#${clipId})`,
});
sub.textContent = subtype.replace(/_/g, ' ');
g.appendChild(sub);

View File

@@ -82,4 +82,5 @@ export const trash2 = '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c
export const listChecks = '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/>';
export const circleOff = '<path d="m2 2 20 20"/><path d="M8.35 2.69A10 10 0 0 1 21.3 15.65"/><path d="M19.08 19.08A10 10 0 1 1 4.92 4.92"/>';
export const externalLink = '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>';
export const thermometer = '<path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"/>';
export const xIcon = '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>';

View File

@@ -176,6 +176,7 @@ export const ICON_UNDO = _svg(P.undo2);
export const ICON_SCENE = _svg(P.sparkles);
export const ICON_CAPTURE = _svg(P.camera);
export const ICON_BELL = _svg(P.bellRing);
export const ICON_THERMOMETER = _svg(P.thermometer);
export const ICON_CPU = _svg(P.cpu);
export const ICON_KEYBOARD = _svg(P.keyboard);
export const ICON_MOUSE = _svg(P.mouse);

View File

@@ -38,9 +38,8 @@ function _collectPreviewConfig() {
if (colors.length < 2) return null;
config = { source_type: 'color_cycle', colors };
} else if (sourceType === 'effect') {
config = { source_type: 'effect', effect_type: (document.getElementById('css-editor-effect-type') as HTMLInputElement).value, palette: (document.getElementById('css-editor-effect-palette') as HTMLInputElement).value, intensity: parseFloat((document.getElementById('css-editor-effect-intensity') as HTMLInputElement).value), scale: parseFloat((document.getElementById('css-editor-effect-scale') as HTMLInputElement).value), mirror: (document.getElementById('css-editor-effect-mirror') as HTMLInputElement).checked };
config = { source_type: 'effect', effect_type: (document.getElementById('css-editor-effect-type') as HTMLInputElement).value, gradient_id: (document.getElementById('css-editor-effect-palette') as HTMLInputElement).value, intensity: parseFloat((document.getElementById('css-editor-effect-intensity') as HTMLInputElement).value), scale: parseFloat((document.getElementById('css-editor-effect-scale') as HTMLInputElement).value), mirror: (document.getElementById('css-editor-effect-mirror') as HTMLInputElement).checked };
if (['meteor', 'comet', 'bouncing_ball'].includes(config.effect_type)) { const hex = (document.getElementById('css-editor-effect-color') as HTMLInputElement).value; config.color = [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)]; }
if (config.palette === 'custom') { const cpText = (document.getElementById('css-editor-effect-custom-palette') as HTMLTextAreaElement)?.value?.trim(); if (cpText) { try { config.custom_palette = JSON.parse(cpText); } catch {} } }
} else if (sourceType === 'daylight') {
config = { source_type: 'daylight', speed: parseFloat((document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value), use_real_time: (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked, latitude: parseFloat((document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value), longitude: parseFloat((document.getElementById('css-editor-daylight-longitude') as HTMLInputElement).value) };
} else if (sourceType === 'candlelight') {

View File

@@ -13,7 +13,7 @@ import {
ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC,
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL, ICON_TEST,
ICON_AUTOMATION,
ICON_AUTOMATION, ICON_FAST_FORWARD, ICON_THERMOMETER,
} from '../core/icons.ts';
import * as P from '../core/icon-paths.ts';
import { wrapCard } from '../core/card-colors.ts';
@@ -1067,7 +1067,7 @@ const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
const useRealTime = source.use_real_time;
const speedVal = (source.speed ?? 1.0).toFixed(1);
return `
<span class="stream-card-prop">${useRealTime ? '🕐 ' + t('color_strip.daylight.real_time') : ' ' + speedVal + 'x'}</span>
<span class="stream-card-prop">${useRealTime ? ICON_CLOCK + ' ' + t('color_strip.daylight.real_time') : ICON_FAST_FORWARD + ' ' + speedVal + 'x'}</span>
${clockBadge}
`;
},
@@ -1092,8 +1092,8 @@ const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
: '';
return `
<span class="stream-card-prop${wsLink}" title="${t('color_strip.weather.source')}">${ICON_LINK_SOURCE} ${escapeHtml(wsName)}</span>
<span class="stream-card-prop"> ${speedVal}x</span>
<span class="stream-card-prop">🌡 ${tempInfl}</span>
<span class="stream-card-prop">${ICON_FAST_FORWARD} ${speedVal}x</span>
<span class="stream-card-prop">${ICON_THERMOMETER} ${tempInfl}</span>
${clockBadge}
`;
},

View File

@@ -409,7 +409,14 @@ function renderPictureSourcesList(streams: any) {
const renderPPTemplateCard = (tmpl: any) => {
let filterChainHtml = '';
if (tmpl.filters && tmpl.filters.length > 0) {
const filterNames = tmpl.filters.map(fi => `<span class="filter-chain-item">${escapeHtml(_getFilterName(fi.filter_id))}</span>`);
const filterNames = tmpl.filters.map(fi => {
let label = _getFilterName(fi.filter_id);
if (fi.filter_id === 'filter_template' && fi.options?.template_id) {
const ref = _cachedPPTemplates.find(p => p.id === fi.options.template_id);
if (ref) label += `: ${ref.name}`;
}
return `<span class="filter-chain-item">${escapeHtml(label)}</span>`;
});
filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">→</span>')}</div>`;
}
return wrapCard({
@@ -435,7 +442,14 @@ function renderPictureSourcesList(streams: any) {
const renderCSPTCard = (tmpl: any) => {
let filterChainHtml = '';
if (tmpl.filters && tmpl.filters.length > 0) {
const filterNames = tmpl.filters.map(fi => `<span class="filter-chain-item">${escapeHtml(_getStripFilterName(fi.filter_id))}</span>`);
const filterNames = tmpl.filters.map(fi => {
let label = _getStripFilterName(fi.filter_id);
if (fi.filter_id === 'css_filter_template' && fi.options?.template_id) {
const ref = _cachedCSPTemplates.find(p => p.id === fi.options.template_id);
if (ref) label += `: ${ref.name}`;
}
return `<span class="filter-chain-item">${escapeHtml(label)}</span>`;
});
filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">\u2192</span>')}</div>`;
}
return wrapCard({