Plugin Directory

Changeset 3476291


Ignore:
Timestamp:
03/06/2026 11:05:10 AM (3 weeks ago)
Author:
luzidmedia
Message:

Update to 1.4.2

Location:
luzid-content-scheduler/trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • luzid-content-scheduler/trunk/assets/css/luzid.css

    r3456038 r3476291  
    11261126/* Recurring rule validation (admin only) */
    11271127.lcs-row--invalid td { background: #fff8e5; }
     1128
     1129/* ========================================
     1130   Card-based Layout for Termine/Wiederholungen
     1131   ======================================== */
     1132
     1133.lcs-cards-repeater {
     1134  display: flex;
     1135  flex-direction: column;
     1136  gap: 16px;
     1137}
     1138
     1139.lcs-card {
     1140  background: #f9f9f9;
     1141  border: 1px solid #dcdcde;
     1142  border-radius: 4px;
     1143  padding: 16px;
     1144}
     1145
     1146.lcs-card__row {
     1147  display: flex;
     1148  flex-wrap: wrap;
     1149  gap: 12px;
     1150  align-items: flex-end;
     1151}
     1152
     1153.lcs-card__row + .lcs-card__row {
     1154  margin-top: 12px;
     1155  padding-top: 12px;
     1156  border-top: 1px solid #e5e5e5;
     1157}
     1158
     1159.lcs-card__field {
     1160  display: flex;
     1161  flex-direction: column;
     1162  gap: 4px;
     1163}
     1164
     1165.lcs-card__field label {
     1166  font-size: 11px;
     1167  font-weight: 600;
     1168  color: #646970;
     1169  text-transform: uppercase;
     1170  letter-spacing: 0.5px;
     1171}
     1172
     1173.lcs-card__field input[type="date"],
     1174.lcs-card__field input[type="time"] {
     1175  width: 130px;
     1176  min-width: 130px;
     1177}
     1178
     1179.lcs-card__field input[type="text"] {
     1180  min-width: 180px;
     1181}
     1182
     1183.lcs-card__field select {
     1184  min-width: 80px;
     1185}
     1186
     1187.lcs-card__field--wide {
     1188  flex: 1;
     1189  min-width: 200px;
     1190}
     1191
     1192.lcs-card__field--wide input[type="text"] {
     1193  width: 100%;
     1194}
     1195
     1196.lcs-card__actions {
     1197  display: flex;
     1198  align-items: flex-end;
     1199  padding-bottom: 2px;
     1200}
     1201
     1202/* Wiederholungen: Zwei-Zeilen-Layout */
     1203.lcs-recurring-card {
     1204  background: #f9f9f9;
     1205  border: 1px solid #dcdcde;
     1206  border-radius: 4px;
     1207  padding: 16px;
     1208  margin-bottom: 12px;
     1209}
     1210
     1211.lcs-recurring-card__row1 {
     1212  display: flex;
     1213  flex-wrap: wrap;
     1214  gap: 12px;
     1215  align-items: center;
     1216  margin-bottom: 12px;
     1217}
     1218
     1219.lcs-recurring-card__row2 {
     1220  display: flex;
     1221  flex-wrap: wrap;
     1222  gap: 12px;
     1223  align-items: flex-end;
     1224  padding-top: 12px;
     1225  border-top: 1px solid #e5e5e5;
     1226}
     1227
     1228.lcs-recurring-card__field {
     1229  display: flex;
     1230  flex-direction: column;
     1231  gap: 4px;
     1232}
     1233
     1234.lcs-recurring-card__field label {
     1235  font-size: 11px;
     1236  font-weight: 600;
     1237  color: #646970;
     1238  text-transform: uppercase;
     1239  letter-spacing: 0.5px;
     1240}
     1241
     1242.lcs-recurring-card__field--type {
     1243  min-width: 140px;
     1244}
     1245
     1246.lcs-recurring-card__field--details {
     1247  flex: 1;
     1248  min-width: 200px;
     1249}
     1250
     1251.lcs-recurring-card__field--time input[type="time"] {
     1252  width: 100px;
     1253}
     1254
     1255.lcs-recurring-card__field--text {
     1256  flex: 1;
     1257  min-width: 150px;
     1258}
     1259
     1260.lcs-recurring-card__field--text input {
     1261  width: 100%;
     1262}
     1263
     1264.lcs-recurring-card__field--period {
     1265  display: flex;
     1266  gap: 8px;
     1267  align-items: center;
     1268}
     1269
     1270.lcs-recurring-card__field--period input[type="date"] {
     1271  width: 130px;
     1272}
     1273
     1274.lcs-recurring-card__field--period .lcs-help-inline {
     1275  font-size: 12px;
     1276  color: #646970;
     1277}
     1278
     1279.lcs-recurring-card__actions {
     1280  display: flex;
     1281  align-items: flex-end;
     1282  padding-bottom: 2px;
     1283}
     1284
     1285/* Inline inputs spacing fix */
     1286.lcs-inline-inputs {
     1287  display: inline-flex;
     1288  flex-wrap: wrap;
     1289  gap: 8px;
     1290  align-items: center;
     1291}
     1292
     1293.lcs-inline-inputs .lcs-input {
     1294  margin: 0;
     1295}
     1296
     1297.lcs-inline-inputs .lcs-select {
     1298  margin: 0;
     1299}
     1300
     1301/* Smaller inputs for compact layouts */
     1302.lcs-input--small {
     1303  padding: 4px 8px;
     1304  font-size: 13px;
     1305}
     1306
     1307.lcs-select--small {
     1308  padding: 4px 8px;
     1309  font-size: 13px;
     1310}
     1311
     1312/* Warning box for incomplete rules */
     1313.lcs-rec-inline-warning {
     1314  margin-bottom: 8px;
     1315  padding: 8px 10px;
     1316  background: #fff8e5;
     1317  border: 1px solid #dba617;
     1318  border-radius: 4px;
     1319  color: #6b5a00;
     1320  font-size: 13px;
     1321}
     1322
     1323.lcs-rec-inline-warning .dashicons {
     1324  margin-right: 6px;
     1325  vertical-align: middle;
     1326}
  • luzid-content-scheduler/trunk/assets/js/luzid-admin.js

    r3456038 r3476291  
    192192    if (!container) return;
    193193
    194     var rowsWrap = container.querySelector('[data-lcs-rows]');
     194    // Support both table-based (tbody with data-lcs-rows) and card-based (.lcs-cards-repeater) layouts
     195    var rowsWrap = container.querySelector('[data-lcs-rows]') || container;
    195196    var tpl = container.querySelector('template[data-lcs-template]');
    196     if (!rowsWrap || !tpl) return;
     197    if (!tpl) return;
    197198
    198199    container.addEventListener('click', function (e) {
     
    201202        e.preventDefault();
    202203        var html = tpl.innerHTML;
    203         var idx = rowsWrap.querySelectorAll('[data-lcs-row]').length;
     204        var idx = container.querySelectorAll('[data-lcs-row]').length;
    204205        html = html.replace(/__i__/g, String(idx));
    205         var tmp = document.createElement('tbody');
     206       
     207        // Create temporary container based on layout type
     208        var isCardLayout = container.classList.contains('lcs-cards-repeater');
     209        var tmp = document.createElement(isCardLayout ? 'div' : 'tbody');
    206210        tmp.innerHTML = html;
    207         // Append all rows from template
    208         Array.prototype.slice.call(tmp.children).forEach(function (tr) {
    209           rowsWrap.appendChild(tr);
     211       
     212        // Append all rows/cards from template
     213        Array.prototype.slice.call(tmp.children).forEach(function (el) {
     214          if (isCardLayout) {
     215            // Insert before the template element
     216            tpl.parentNode.insertBefore(el, tpl);
     217          } else {
     218            rowsWrap.appendChild(el);
     219          }
    210220        });
    211221        // Update recurring row visibility if needed
    212         initRecurringTypeToggle(rowsWrap);
     222        initRecurringTypeToggle(container);
    213223        // Native inputs (date/time) need no additional initialization
    214224        return;
     
    226236
    227237  function initRecurringTypeToggle(root) {
    228     // root can be tbody or document
     238    // root can be tbody, div, or document
    229239    var scope = root || document;
    230240
     
    235245      row.querySelectorAll('[data-details]').forEach(function (el) {
    236246        var isActive = (el.getAttribute('data-details') === type);
    237         el.style.display = isActive ? 'inline-flex' : 'none';
     247        el.style.display = isActive ? '' : 'none';
    238248        // Wichtig: Inputs in versteckten Bereichen deaktivieren, damit keine doppelten POST-Werte
    239249        // (z.B. weekday in weekly + nth_weekday_month) den gespeicherten Wert überschreiben.
     
    242252        });
    243253      });
    244       var common = row.querySelector('.lcs-rec-common');
    245       if (common) {
    246         common.style.display = type ? 'inline-flex' : 'none';
     254      // Show/hide all elements with lcs-rec-common class (can be multiple)
     255      row.querySelectorAll('.lcs-rec-common').forEach(function (common) {
     256        common.style.display = type ? '' : 'none';
    247257        common.querySelectorAll('input, select, textarea').forEach(function (inp) {
    248258          inp.disabled = !type;
    249259        });
    250       }
    251     }
    252 
    253     scope.querySelectorAll('tr[data-lcs-row]').forEach(applyRow);
     260      });
     261    }
     262
     263    // Support both tr[data-lcs-row] (table layout) and div[data-lcs-row] (card layout)
     264    scope.querySelectorAll('[data-lcs-row]').forEach(applyRow);
    254265
    255266    scope.addEventListener('change', function (e) {
    256267      var sel = e.target.closest('.lcs-rec-type');
    257268      if (!sel) return;
    258       var row = sel.closest('tr[data-lcs-row]');
     269      var row = sel.closest('[data-lcs-row]');
    259270      if (row) applyRow(row);
    260271    });
  • luzid-content-scheduler/trunk/howto-de.php

    r3456038 r3476291  
    55<div class="luzid-howto">
    66  <div class="lcs-howto-section">
    7     <h3>Worum gehts?</h3>
    8     <p>Der <strong>Luzid Content Scheduler</strong> steuert die Sichtbarkeit von Frontend-Contentblöcken (z.B. Banner, Alerts, Divs) anhand von <strong>Terminen</strong>, <strong>Wiederholungen</strong> und <strong>Ausnahmen</strong> – ohne dass du dafür Code schreiben musst.</p>
     7    <h3>Worum geht's?</h3>
     8    <p>Der <strong>Luzid Content Scheduler</strong> steuert die Sichtbarkeit von Frontend-Contentblöcken (z. B. Banner, Alerts, Divs) anhand von <strong>Terminen</strong>, <strong>Wiederholungen</strong> und <strong>Ausnahmen</strong> – ohne dass du dafür Code schreiben musst.</p>
    99    <div class="lcs-callout">
    1010      <p><strong>Merksatz:</strong> Der Block ist sichtbar, wenn <strong>mindestens eine Regel passt</strong> (ODER-Logik). <strong>Ausnahmen</strong> überschreiben alles und verhindern die Anzeige.</p>
     
    1515    <h3>Schnellstart in 3 Schritten</h3>
    1616    <ol>
    17       <li><strong>Scheduler anlegen</strong> (Tab „Setup) und speichern. Dadurch wird der <em>Slug</em> erzeugt.</li>
    18       <li>Deinem Frontend-Block die CSS-Klasse <code>.luzid-cs-&lt;slug&gt;</code> geben (siehe Abschnitt „CSS-Klasse in Pagebuildern).</li>
     17      <li><strong>Scheduler anlegen</strong> (Tab „Setup") und speichern. Dadurch wird der <em>Slug</em> erzeugt.</li>
     18      <li>Deinem Frontend-Block die CSS-Klasse <code>.luzid-cs-&lt;slug&gt;</code> geben (siehe Abschnitt „CSS-Klasse in Pagebuildern").</li>
    1919      <li>Regeln definieren (Termine/Wiederholungen/Ausnahmen). Optional Event-Offset aktivieren.</li>
    2020    </ol>
     
    2222
    2323  <div class="lcs-howto-section">
    24     <h3>Tab „Setup</h3>
     24    <h3>Tab „Setup"</h3>
    2525    <p>Hier vergibst du den <strong>Namen</strong> des Schedulers. Beim Speichern wird daraus automatisch die stabile CSS-Klasse/der Slug erstellt. Änderst du später den Namen, ändert sich auch der Slug.</p>
    2626    <div class="lcs-callout">
    2727      <p><strong>CSS-Klasse:</strong> Diese fügst du an den Frontend-Block an, den du steuern möchtest.</p>
    28       <p><strong>Shortcode:</strong> Gibt das nächste Event-Datum dynamisch aus, z. B. <code>[luzid_cs slug="wartung" opt="long"]</code></p>
    29     </div>
    30   </div>
    31 
    32   <div class="lcs-howto-section">
    33     <h3>Tab „Termine“</h3>
    34     <p>Termine definieren <strong>einzelne Tage</strong> oder <strong>Zeiträume</strong>, in denen Content angezeigt werden kann.</p>
    35     <ul>
    36       <li><strong>Von</strong> gesetzt, <strong>Bis</strong> leer → gilt nur dieser Tag.</li>
    37       <li><strong>Von</strong> und <strong>Bis</strong> gesetzt → gilt als Zeitraum.</li>
    38       <li><strong>Präfix</strong> + <strong>Zeit</strong> steuern den Zeitpunkt: <em>ab</em>, <em>bis</em>, <em>um</em>.</li>
    39     </ul>
    40   </div>
    41 
    42   <div class="lcs-howto-section">
    43     <h3>Tab „Wiederholungen“</h3>
    44     <p>Wiederholungen sind Regeln wie „jeden Donnerstag“ oder „jeden 1. Donnerstag im Monat“. Mehrere Regeln werden per <strong>ODER</strong> kombiniert.</p>
     28      <p><strong>Shortcode:</strong> Gibt das nächste Event-Datum dynamisch aus, z. B. <code>[luzid_cs slug="wartung" date="long"]</code></p>
     29      <p><strong>Event-Tabelle:</strong> Aktiviere „In Event-Tabelle einbeziehen", um Events dieses Schedulers im <code>[luzid_cs_eventtable]</code> Shortcode anzuzeigen.</p>
     30    </div>
     31  </div>
     32
     33  <div class="lcs-howto-section">
     34    <h3>Tab „Termine"</h3>
     35    <p>Termine definieren <strong>einzelne Tage</strong>, an denen Content angezeigt werden kann.</p>
     36    <ul>
     37      <li><strong>Datum</strong>: Der Tag des Events.</li>
     38      <li><strong>Präfix 1</strong> + <strong>Zeit von</strong>: Startzeit mit optionalem Präfix (ab, bis, um, von).</li>
     39      <li><strong>Präfix 2</strong> + <strong>Zeit bis</strong>: Endzeit mit optionalem Präfix (bis, –).</li>
     40      <li><strong>Eventtext</strong>: Optionaler Text, der im Shortcode ausgegeben werden kann.</li>
     41    </ul>
     42  </div>
     43
     44  <div class="lcs-howto-section">
     45    <h3>Tab „Wiederholungen"</h3>
     46    <p>Wiederholungen sind Regeln wie „jeden Donnerstag", „jeden 1. Donnerstag im Monat" oder „jährlich am 14. Februar". Mehrere Regeln werden per <strong>ODER</strong> kombiniert.</p>
    4547    <div class="lcs-callout">
    4648      <p><strong>Beispiel:</strong> Du willst Content <em>jeden Montag</em> und zusätzlich <em>am 1. des Monats</em> anzeigen → lege zwei Regeln an. Sobald eine Regel passt, ist der Block sichtbar.</p>
    4749    </div>
    48     <p>Jede Regel kann einen eigenen <strong>Regel-Zeitraum</strong> (von/bis) haben, der nur diese Regel einschränkt.</p>
    49   </div>
    50 
    51   <div class="lcs-howto-section">
    52     <h3>Tab „Ausnahmen“</h3>
     50    <p><strong>Typen:</strong></p>
     51    <ul>
     52      <li><strong>Wöchentlich</strong>: Jeden bestimmten Wochentag.</li>
     53      <li><strong>Monatlich</strong>: An einem bestimmten Tag im Monat (z.B. jeden 15.).</li>
     54      <li><strong>Wochentag im Monat</strong>: Z.B. „1. Donnerstag" oder „letzter Freitag" im Monat.</li>
     55      <li><strong>Jährlich</strong>: An einem bestimmten Datum jedes Jahr (z.B. 14. Februar).</li>
     56    </ul>
     57    <p>Jede Regel kann einen eigenen <strong>Regel-Zeitraum</strong> (Gültig von/bis) haben, der nur diese Regel einschränkt.</p>
     58  </div>
     59
     60  <div class="lcs-howto-section">
     61    <h3>Tab „Ausnahmen"</h3>
    5362    <p>Ausnahmen überschreiben alle anderen Regeln. An den definierten Ausnahmen wird <strong>kein Content</strong> angezeigt.</p>
    5463    <ul>
     
    5766    </ul>
    5867    <div class="lcs-callout lcs-callout--warn">
    59       <p><strong>Wichtig:</strong> Wenn „jetzt“ in einer Ausnahme liegt, bleibt der Block immer unsichtbar – auch wenn Termine oder Wiederholungen passen.</p>
    60     </div>
    61   </div>
    62 
    63   <div class="lcs-howto-section">
    64     <h3>Tab „Event-Offset“</h3>
    65     <p>Der Event-Offset erweitert das Sichtbarkeitsfenster <strong>vor</strong> bzw. <strong>nach</strong> dem berechneten Event. Der Offset ändert <strong>nicht</strong> die Text-Ausgabe des Shortcodes, sondern nur die Sichtbarkeit.</p>
    66     <ul>
    67       <li><strong>Tage vorher</strong> + <strong>Startzeit</strong>: ab wann der Block vor dem Event eingeblendet wird.</li>
    68       <li><strong>Tage nachher</strong> + <strong>Endzeit</strong>: wann die Sichtbarkeit endet (bei Zeiträumen bezogen auf den letzten Tag).</li>
    69     </ul>
    70   </div>
    71 
    72   <div class="lcs-howto-section">
    73     <h3>Tab „Vorschau“</h3>
     68      <p><strong>Wichtig:</strong> Wenn „jetzt" in einer Ausnahme liegt, bleibt der Block immer unsichtbar – auch wenn Termine oder Wiederholungen passen.</p>
     69    </div>
     70  </div>
     71
     72  <div class="lcs-howto-section">
     73    <h3>Tab „Event-Klassen"</h3>
     74    <p>Event-Klassen ermöglichen dir, <strong>mehrere unabhängige Sichtbarkeitszeiträume</strong> für einen Scheduler zu definieren. Jede Klasse erzeugt eine eigene CSS-Klasse mit individuellen Offset-Einstellungen.</p>
     75   
     76    <div class="lcs-callout">
     77      <p><strong>Anwendungsbeispiel:</strong> Du hast ein Event am 15. April. Mit Event-Klassen kannst du:</p>
     78      <ul style="margin: 10px 0;">
     79        <li>Ein <strong>Popup</strong> 4 Tage vorher einblenden</li>
     80        <li>Den <strong>Event-Content</strong> (mit Bildern) 2 Wochen vorher zeigen</li>
     81        <li>Einen <strong>Menüeintrag</strong> 11 Tage vorher aktivieren</li>
     82      </ul>
     83      <p>Jeder dieser Inhalte hat seine eigene CSS-Klasse und wird unabhängig voneinander sichtbar.</p>
     84    </div>
     85
     86    <h4>Standard-Klasse</h4>
     87    <p>Die erste Klasse heißt immer „Standard" und kann nicht gelöscht oder umbenannt werden. Sie erzeugt die Haupt-CSS-Klasse:</p>
     88    <p><code>.luzid-cs-&lt;schedulername&gt;</code></p>
     89
     90    <h4>Zusätzliche Klassen</h4>
     91    <p>Klicke auf „+ Klasse hinzufügen", um weitere Klassen anzulegen. Jede zusätzliche Klasse erhält einen eigenen Namen und erzeugt eine erweiterte CSS-Klasse:</p>
     92    <p><code>.luzid-cs-&lt;schedulername&gt;-&lt;klassenname&gt;</code></p>
     93
     94    <h4>Offset-Einstellungen pro Klasse</h4>
     95    <ul>
     96      <li><strong>Tage vorher</strong>: Ab wie vielen Tagen vor dem Event wird der Content sichtbar?</li>
     97      <li><strong>Startzeit</strong>: Ab welcher Uhrzeit beginnt die Sichtbarkeit? (leer = 00:00 Uhr)</li>
     98      <li><strong>Tage nachher</strong>: Wie viele Tage nach dem Event bleibt der Content sichtbar?</li>
     99      <li><strong>Endzeit</strong>: Bis zu welcher Uhrzeit endet die Sichtbarkeit? (leer = Event-Zeitpunkt)</li>
     100    </ul>
     101
     102    <h4>Vorschau</h4>
     103    <p>Unter jeder Klasse siehst du eine Live-Vorschau des Sichtbarkeitszeitraums basierend auf dem nächsten berechneten Event.</p>
     104
     105    <div class="lcs-callout">
     106      <p><strong>Beispiel-Konfiguration:</strong></p>
     107      <table class="lcs-table lcs-table--compact">
     108        <thead>
     109          <tr><th>Klasse</th><th>CSS-Klasse</th><th>Tage vorher</th><th>Verwendung</th></tr>
     110        </thead>
     111        <tbody>
     112          <tr><td>Standard</td><td><code>.luzid-cs-krimidinner</code></td><td>14</td><td>Hauptinhalt mit Bildern</td></tr>
     113          <tr><td>popup</td><td><code>.luzid-cs-krimidinner-popup</code></td><td>4</td><td>Ankündigungs-Popup</td></tr>
     114          <tr><td>menu</td><td><code>.luzid-cs-krimidinner-menu</code></td><td>11</td><td>Menüeintrag</td></tr>
     115        </tbody>
     116      </table>
     117    </div>
     118  </div>
     119
     120  <div class="lcs-howto-section">
     121    <h3>Tab „Vorschau"</h3>
    74122    <p>Mit der Vorschau kannst du dir die nächsten berechneten Events (inkl. Einblendungszeitpunkt und Shortcode-Text) anzeigen lassen – praktisch zum Testen deiner Regeln.</p>
    75123  </div>
     
    77125  <div class="lcs-howto-section">
    78126    <h3>CSS-Klasse in WordPress &amp; Pagebuildern</h3>
    79     <p class="lcs-text"><em>Hinweis:</em> Frühere Plugin-Versionen nutzten das Präfix <code>luzid-cs-</code>. Diese Klasse wird weiterhin unterstützt – empfohlen ist aber <code>luzid-cs-</code>.</p>
    80     <div class="lcs-callout">
    81       <p><strong>Gutenberg:</strong> Block auswählen → „Erweitert“ → „Zusätzliche CSS-Klasse(n)“ → <code>luzid-cs-&lt;slug&gt;</code> eintragen (ohne Punkt).</p>
    82       <p><strong>Elementor:</strong> Widget auswählen → „Erweitert“ → „CSS-Klassen“ → <code>luzid-cs-&lt;slug&gt;</code> eintragen.</p>
    83       <p><strong>Divi/andere Builder:</strong> Im Bereich „CSS-Klasse“ oder „Custom CSS Class“ den gleichen Wert eintragen.</p>
     127    <div class="lcs-callout">
     128      <p><strong>Gutenberg:</strong> Block auswählen → „Erweitert" → „Zusätzliche CSS-Klasse(n)" → <code>luzid-cs-&lt;slug&gt;</code> eintragen (ohne Punkt).</p>
     129      <p><strong>Elementor:</strong> Widget auswählen → „Erweitert" → „CSS-Klassen" → <code>luzid-cs-&lt;slug&gt;</code> eintragen.</p>
     130      <p><strong>Divi/andere Builder:</strong> Im Bereich „CSS-Klasse" oder „Custom CSS Class" den gleichen Wert eintragen.</p>
    84131    </div>
    85132    <p>Wichtig: Die Klasse wird <strong>ohne führenden Punkt</strong> im Builder eingetragen. Der Punkt <code>.</code> ist nur in CSS-Schreibweise relevant.</p>
     
    87134
    88135  <div class="lcs-howto-section">
    89     <h3>Shortcode-Ausgabe</h3>
    90     <p>Mit dem Shortcode gibst du das nächste Event oder eine Liste kommender Events im Frontend aus.</p>
    91 
    92     <p><strong>Formate (<code>opt</code>):</strong></p>
    93     <ul>
    94       <li><code>opt="short"</code> → <em>01.01.2026</em></li>
    95       <li><code>opt="date"</code> → <em>Donnerstag, 01.01.2026</em></li>
    96       <li><code>opt="time"</code> → <em>ab 16:00 Uhr</em></li>
    97       <li><code>opt="long"</code> → <em>Donnerstag, 01.01.2026 ab 16:00 Uhr</em></li>
    98     </ul>
    99 
    100     <p><strong>Parameter:</strong></p>
    101     <ul>
    102       <li><code>list="true|false"</code> (Standard: <code>false</code>) → gibt <code>count</code> Events als Liste aus (je Zeile ein Event, getrennt durch <code>&lt;br&gt;</code>).</li>
    103       <li><code>count="n"</code> (n &gt; 0, Standard: 10, Max.: 200) → Anzahl Events bei <code>list="true"</code>.</li>
    104       <li><code>text="true|false"</code> (Standard: <code>false</code>) → hängt den optionalen <strong>Eventtext</strong> an (aus Termine/Wiederholungen).</li>
    105       <li><code>sep="…"</code> (Standard: <code>-</code>) → Separator zwischen <code>&lt;opt&gt;</code> und Eventtext. Tipp: Zeilenumbruch mit <code>sep="&lt;br&gt;"</code>.</li>
    106       <li><code>timeoffset="…"</code> (Minuten, auch negativ) → verschiebt nur die <em>Ausgabe</em> (nicht die Logik).</li>
    107       <li><code>lang="de|en"</code> → Sprache für die Ausgabe erzwingen (leer = aktuelle UI).</li>
    108     </ul>
    109 
    110     <div class="lcs-callout">
    111       <p><strong>Beispiel:</strong> <code>[luzid_cs slug="wartung" opt="long"]</code></p>
    112       <p><strong>Beispiel (Liste + Text):</strong> <code>[luzid_cs slug="krimidinner" opt="short" list="true" count="5" text="true" sep=" | "]</code></p>
    113       <p><strong>Beispiel (Text in neuer Zeile):</strong> <code>[luzid_cs slug="krimidinner" opt="date" text="true" sep="&lt;br&gt;"]</code></p>
    114     </div>
    115 
    116     <p><strong>CSS-Klassen für eigenes Styling (externe CSS):</strong><br>
    117       Die Ausgabe ist in Spans gekapselt, damit du Datum/Separator/Text unterschiedlich formatieren kannst:</p>
     136    <h3>Shortcode: Einzelnes Event</h3>
     137    <p>Mit dem Shortcode <code>[luzid_cs]</code> gibst du das nächste Event oder eine Liste kommender Events im Frontend aus.</p>
     138
     139    <p><strong>Basis-Syntax:</strong> <code>[luzid_cs slug="dein-slug"]</code></p>
     140
     141    <h4>Parameter-Übersicht</h4>
     142    <table class="lcs-table lcs-table--compact">
     143      <thead>
     144        <tr><th>Parameter</th><th>Standard</th><th>Beschreibung</th></tr>
     145      </thead>
     146      <tbody>
     147        <tr><td><code>slug</code></td><td><em>(Pflicht)</em></td><td>Scheduler-Slug</td></tr>
     148        <tr><td><code>date</code></td><td><em>(leer)</em></td><td>Datumsformat: <code>short</code>, <code>medium</code>, <code>long</code>, <code>full</code></td></tr>
     149        <tr><td><code>time</code></td><td><em>(leer)</em></td><td>Zeitformat: <code>auto</code>, <code>raw</code>, <code>prefix</code>, <code>range</code>, <code>range_long</code></td></tr>
     150        <tr><td><code>list</code></td><td><code>false</code></td><td>Liste ausgeben: <code>true</code> oder <code>false</code></td></tr>
     151        <tr><td><code>count</code></td><td><code>10</code></td><td>Anzahl Events bei <code>list="true"</code> (max. 200)</td></tr>
     152        <tr><td><code>text</code></td><td><code>false</code></td><td>Eventtext anhängen: <code>true</code> oder <code>false</code></td></tr>
     153        <tr><td><code>sep1</code></td><td><code> </code> (Leerzeichen)</td><td>Trennzeichen zwischen Datum und Zeit</td></tr>
     154        <tr><td><code>sep2</code></td><td><code> </code> (Leerzeichen)</td><td>Trennzeichen zwischen Zeit und Text</td></tr>
     155        <tr><td><code>sep3</code></td><td><em>(leer)</em></td><td>Trennzeichen zwischen Listenelementen</td></tr>
     156        <tr><td><code>timeoffset</code></td><td><code>0</code></td><td>Zeitverschiebung in Minuten (auch negativ)</td></tr>
     157        <tr><td><code>lang</code></td><td><em>(aktuell)</em></td><td>Sprache erzwingen: <code>de</code> oder <code>en</code></td></tr>
     158      </tbody>
     159    </table>
     160
     161    <h4>Werte für <code>date</code></h4>
     162    <table class="lcs-table lcs-table--compact">
     163      <thead>
     164        <tr><th>Wert</th><th>Ausgabe (Beispiel)</th></tr>
     165      </thead>
     166      <tbody>
     167        <tr><td><code>short</code></td><td>14.02.2026</td></tr>
     168        <tr><td><code>medium</code></td><td>Sa, 14.02.2026</td></tr>
     169        <tr><td><code>long</code></td><td>Samstag, 14.02.2026</td></tr>
     170        <tr><td><code>full</code></td><td>Samstag, 14. Februar 2026</td></tr>
     171      </tbody>
     172    </table>
     173
     174    <h4>Werte für <code>time</code></h4>
     175    <p><strong>Wichtig:</strong> Ohne <code>time</code>-Parameter wird <strong>keine Uhrzeit</strong> ausgegeben – nur das Datum!</p>
     176    <table class="lcs-table lcs-table--compact">
     177      <thead>
     178        <tr><th>Wert</th><th>Ausgabe (Beispiel)</th></tr>
     179      </thead>
     180      <tbody>
     181        <tr><td><em>(nicht gesetzt)</em></td><td><em>keine Zeit</em></td></tr>
     182        <tr><td><code>raw</code></td><td>18:00</td></tr>
     183        <tr><td><code>prefix</code></td><td>von 18:00 Uhr</td></tr>
     184        <tr><td><code>range</code></td><td>18:00 bis 20:00</td></tr>
     185        <tr><td><code>range_long</code></td><td>von 18:00 bis 20:00 Uhr</td></tr>
     186        <tr><td><code>auto</code></td><td>von 18:00 bis 20:00 Uhr <em>(intelligent)</em></td></tr>
     187      </tbody>
     188    </table>
     189
     190    <h4>Trennzeichen (Separatoren) – So funktioniert's</h4>
     191    <p>Die Ausgabe besteht aus bis zu drei Teilen: <strong>Datum</strong>, <strong>Zeit</strong> und <strong>Eventtext</strong>. Mit den Trennzeichen bestimmst du, was zwischen diesen Teilen steht.</p>
     192   
     193    <div class="lcs-callout">
     194      <p><strong>Aufbau der Ausgabe:</strong></p>
     195      <p style="font-family: monospace; background: #f5f5f5; padding: 10px; border-radius: 4px;">
     196        [DATUM] <span style="color: #c00;">sep1</span> [ZEIT] <span style="color: #c00;">sep2</span> [TEXT]
     197      </p>
     198      <ul style="margin-top: 10px;">
     199        <li><code>sep1</code> = Was steht zwischen Datum und Zeit?</li>
     200        <li><code>sep2</code> = Was steht zwischen Zeit und Text? (oder zwischen Datum und Text, wenn keine Zeit)</li>
     201        <li><code>sep3</code> = Was steht zwischen den Einträgen in einer Liste?</li>
     202      </ul>
     203    </div>
     204
     205    <p><strong>Typische Werte:</strong></p>
     206    <ul>
     207      <li><code>" "</code> (Leerzeichen) – alles in einer Zeile</li>
     208      <li><code>"&lt;br&gt;"</code> – Zeilenumbruch (neuer Absatz)</li>
     209      <li><code>" | "</code> – senkrechter Strich als Trenner</li>
     210      <li><code>" – "</code> – Gedankenstrich als Trenner</li>
     211    </ul>
     212
     213    <h4>Beispiele mit Trennzeichen</h4>
     214   
     215    <p><strong>Alles in einer Zeile (Standard):</strong></p>
     216    <pre><code>[luzid_cs slug="event" date="long" time="auto" text="true"]</code></pre>
     217    <p>→ Samstag, 14.02.2026 von 18:00 bis 20:00 Uhr Valentins-Dinner</p>
     218
     219    <p><strong>Nur Datum und Text, zweizeilig:</strong></p>
     220    <pre><code>[luzid_cs slug="event" date="long" text="true" sep2="&lt;br&gt;"]</code></pre>
     221    <p>→ Samstag, 14.02.2026<br>&nbsp;&nbsp;&nbsp;Valentins-Dinner</p>
     222
     223    <p><strong>Datum, Zeit und Text jeweils in eigener Zeile (dreizeilig):</strong></p>
     224    <pre><code>[luzid_cs slug="event" date="long" time="auto" text="true" sep1="&lt;br&gt;" sep2="&lt;br&gt;"]</code></pre>
     225    <p>→ Samstag, 14.02.2026<br>&nbsp;&nbsp;&nbsp;von 18:00 bis 20:00 Uhr<br>&nbsp;&nbsp;&nbsp;Valentins-Dinner</p>
     226
     227    <p><strong>Mit Trennstrich:</strong></p>
     228    <pre><code>[luzid_cs slug="event" date="long" time="raw" text="true" sep1=" | " sep2=" – "]</code></pre>
     229    <p>→ Samstag, 14.02.2026 | 18:00 – Valentins-Dinner</p>
     230
     231    <p><strong>Liste mit Leerzeile zwischen Einträgen:</strong></p>
     232    <pre><code>[luzid_cs slug="event" date="long" text="true" sep2="&lt;br&gt;" sep3="&lt;br&gt;&lt;br&gt;" list="true" count="5"]</code></pre>
     233
     234    <h4>CSS-Klassen für eigenes Styling</h4>
    118235    <ul>
    119236      <li><code>.luzid-cs</code> – Wrapper (zusätzlich <code>.luzid-cs--single</code> / <code>.luzid-cs--list</code>)</li>
    120237      <li><code>.luzid-cs-item</code> – ein Event (bei Listen-Ausgabe)</li>
    121       <li><code>.luzid-cs-opt</code> – Datums-/Zeit-Teil (opt)</li>
    122       <li><code>.luzid-cs-sep</code> – Separator</li>
    123       <li><code>.luzid-cs-text</code> – optionaler Eventtext</li>
    124     </ul>
    125 
    126     <p><em>Hinweis:</em> <code>opt="list"</code> bleibt aus Kompatibilitätsgründen erhalten und entspricht <code>opt="long" list="true"</code>.</p>
     238      <li><code>.luzid-cs-date</code> – Datums-Teil</li>
     239      <li><code>.luzid-cs-time</code> – Zeit-Teil</li>
     240      <li><code>.luzid-cs-text</code> – Eventtext</li>
     241      <li><code>.luzid-cs-sep</code> – Trennzeichen (zusätzlich <code>.luzid-cs-sep1</code> / <code>.luzid-cs-sep2</code>)</li>
     242    </ul>
     243  </div>
     244
     245  <div class="lcs-howto-section">
     246    <h3>Shortcode: Event-Tabelle</h3>
     247    <p>Mit <code>[luzid_cs_eventtable]</code> gibst du eine Tabelle aller kommenden Events aus – aus allen Schedulern, die „In Event-Tabelle einbeziehen" aktiviert haben.</p>
     248
     249    <p><strong>Basis-Syntax:</strong> <code>[luzid_cs_eventtable]</code></p>
     250
     251    <h4>Parameter-Übersicht</h4>
     252    <table class="lcs-table lcs-table--compact">
     253      <thead>
     254        <tr><th>Parameter</th><th>Standard</th><th>Beschreibung</th></tr>
     255      </thead>
     256      <tbody>
     257        <tr><td><code>cols</code></td><td><code>date_medium,time_auto,text</code></td><td>Spalten (komma-getrennt)</td></tr>
     258        <tr><td><code>count</code></td><td><code>30</code></td><td>Max. Anzahl Events</td></tr>
     259        <tr><td><code>headers</code></td><td><em>(automatisch)</em></td><td>Eigene Header (komma-getrennt)</td></tr>
     260        <tr><td><code>noheaders</code></td><td><code>false</code></td><td>Header ausblenden</td></tr>
     261        <tr><td><code>class</code></td><td><em>(leer)</em></td><td>Zusätzliche CSS-Klasse</td></tr>
     262        <tr><td><code>empty</code></td><td>Keine Termine</td><td>Text wenn keine Events</td></tr>
     263        <tr><td><code>order</code></td><td><code>asc</code></td><td>Sortierung: <code>asc</code> oder <code>desc</code></td></tr>
     264        <tr><td><code>lang</code></td><td><em>(aktuell)</em></td><td>Sprache erzwingen</td></tr>
     265      </tbody>
     266    </table>
     267
     268    <h4>Verfügbare Spaltentypen für <code>cols</code></h4>
     269    <table class="lcs-table lcs-table--compact">
     270      <thead>
     271        <tr><th>Spalte</th><th>Ausgabe (Beispiel)</th></tr>
     272      </thead>
     273      <tbody>
     274        <tr><td><code>date_short</code></td><td>14.02.2026</td></tr>
     275        <tr><td><code>date_medium</code></td><td>Sa, 14.02.2026</td></tr>
     276        <tr><td><code>date_long</code></td><td>Samstag, 14.02.2026</td></tr>
     277        <tr><td><code>date_full</code></td><td>Samstag, 14. Februar 2026</td></tr>
     278        <tr><td><code>weekday_short</code></td><td>Sa</td></tr>
     279        <tr><td><code>weekday_long</code></td><td>Samstag</td></tr>
     280        <tr><td><code>time_raw</code></td><td>18:00</td></tr>
     281        <tr><td><code>time_auto</code></td><td>von 18:00 bis 20:00 Uhr</td></tr>
     282        <tr><td><code>time_prefix</code></td><td>von 18:00 Uhr</td></tr>
     283        <tr><td><code>time_range</code></td><td>18:00 bis 20:00</td></tr>
     284        <tr><td><code>time_range_long</code></td><td>von 18:00 bis 20:00 Uhr</td></tr>
     285        <tr><td><code>text</code></td><td>Eventtext</td></tr>
     286        <tr><td><code>scheduler</code></td><td>Scheduler-Name</td></tr>
     287      </tbody>
     288    </table>
     289
     290    <div class="lcs-callout">
     291      <p><strong>Beispiele:</strong></p>
     292      <p><code>[luzid_cs_eventtable]</code> → Standard-Tabelle</p>
     293      <p><code>[luzid_cs_eventtable cols="weekday_short,date_short,time_range,text" count="10"]</code></p>
     294      <p><code>[luzid_cs_eventtable cols="date_long,time_auto,text,scheduler" headers="Datum,Uhrzeit,Event,Kategorie"]</code></p>
     295    </div>
     296
     297    <p><strong>CSS-Klassen für eigenes Styling:</strong></p>
     298    <ul>
     299      <li><code>.luzid-cs-eventtable</code> – Tabelle</li>
     300      <li><code>.luzid-cs-eventtable__head</code> – Tabellenkopf</li>
     301      <li><code>.luzid-cs-eventtable__body</code> – Tabellenkörper</li>
     302      <li><code>.luzid-cs-eventtable__row</code> – Zeile (+ <code>--header</code>, <code>--odd</code>, <code>--even</code>)</li>
     303      <li><code>.luzid-cs-eventtable__cell</code> – Zelle (+ <code>--date_short</code>, <code>--time_auto</code>, etc.)</li>
     304    </ul>
    127305  </div>
    128306</div>
  • luzid-content-scheduler/trunk/howto-en.php

    r3456038 r3476291  
    55<div class="luzid-howto">
    66  <div class="lcs-howto-section">
    7     <h3>What does it do?</h3>
    8     <p><strong>Luzid Content Scheduler</strong> controls the visibility of frontend content blocks (e.g. banners, alerts, divs) based on <strong>dates</strong>, <strong>recurring rules</strong> and <strong>exceptions</strong> — without writing code.</p>
    9     <div class="lcs-callout">
    10       <p><strong>Rule of thumb:</strong> The block is visible if <strong>any rule matches</strong> (OR logic). <strong>Exceptions</strong> override everything and hide the content.</p>
    11     </div>
    12   </div>
    13 
    14   <div class="lcs-howto-section">
    15     <h3>Quick start in 3 steps</h3>
     7    <h3>What's this about?</h3>
     8    <p>The <strong>Luzid Content Scheduler</strong> controls the visibility of frontend content blocks (e.g., banners, alerts, divs) based on <strong>dates</strong>, <strong>recurring rules</strong>, and <strong>exceptions</strong> – without writing any code.</p>
     9    <div class="lcs-callout">
     10      <p><strong>Remember:</strong> The block is visible when <strong>at least one rule matches</strong> (OR logic). <strong>Exceptions</strong> override everything and prevent display.</p>
     11    </div>
     12  </div>
     13
     14  <div class="lcs-howto-section">
     15    <h3>Quick Start in 3 Steps</h3>
    1616    <ol>
    17       <li><strong>Create a scheduler</strong> (tab “Setup”) and save it. This generates the <em>slug</em>.</li>
    18       <li>Add the CSS class <code>.luzid-cs-&lt;slug&gt;</code> to your frontend block (see “Using the CSS class in page builders”).</li>
    19       <li>Define rules (Dates / Recurring / Exceptions). Optionally enable Event Offset.</li>
     17      <li><strong>Create a scheduler</strong> (Setup tab) and save. This generates the <em>slug</em>.</li>
     18      <li>Add the CSS class <code>.luzid-cs-&lt;slug&gt;</code> to your frontend block (see "CSS Class in Page Builders").</li>
     19      <li>Define rules (Dates/Recurring/Exceptions). Optionally enable Event Offset.</li>
    2020    </ol>
    2121  </div>
    2222
    2323  <div class="lcs-howto-section">
    24     <h3>Tab “Setup”</h3>
    25     <p>Enter a <strong>name</strong> for the scheduler. When saving, the CSS class/slug is generated automatically. If you rename it later, the slug changes as well.</p>
    26     <div class="lcs-callout">
    27       <p><strong>CSS class:</strong> Add this to the frontend block you want to control.</p>
    28       <p><strong>Shortcode:</strong> Prints the next event dynamically, e.g. <code>[luzid_cs slug="maintenance" opt="long"]</code></p>
    29     </div>
    30   </div>
    31 
    32   <div class="lcs-howto-section">
    33     <h3>Tab “Dates”</h3>
    34     <p>Dates define <strong>single days</strong> or <strong>ranges</strong> in which content can be shown.</p>
    35     <ul>
    36       <li><strong>From</strong> set, <strong>To</strong> empty → applies to that single day.</li>
    37       <li><strong>From</strong> and <strong>To</strong> set → treated as a date range.</li>
    38       <li><strong>Prefix</strong> + <strong>time</strong> control the moment: <em>from</em>, <em>until</em>, <em>at</em>.</li>
    39     </ul>
    40   </div>
    41 
    42   <div class="lcs-howto-section">
    43     <h3>Tab “Recurring”</h3>
    44     <p>Recurring rules are patterns like “every Thursday” or “every 1st Thursday of the month”. Multiple rules are combined with <strong>OR</strong>.</p>
     24    <h3>Setup Tab</h3>
     25    <p>Here you set the <strong>name</strong> of the scheduler. When saving, the stable CSS class/slug is automatically created. If you change the name later, the slug changes too.</p>
     26    <div class="lcs-callout">
     27      <p><strong>CSS Class:</strong> Add this to the frontend block you want to control.</p>
     28      <p><strong>Shortcode:</strong> Outputs the next event date dynamically, e.g., <code>[luzid_cs slug="maintenance" date="long"]</code></p>
     29      <p><strong>Event Table:</strong> Enable "Include in Event Table" to show events from this scheduler in the <code>[luzid_cs_eventtable]</code> shortcode.</p>
     30    </div>
     31  </div>
     32
     33  <div class="lcs-howto-section">
     34    <h3>Dates Tab</h3>
     35    <p>Dates define <strong>single days</strong> when content should be displayed.</p>
     36    <ul>
     37      <li><strong>Date</strong>: The day of the event.</li>
     38      <li><strong>Prefix 1</strong> + <strong>Time from</strong>: Start time with optional prefix (from, until, at).</li>
     39      <li><strong>Prefix 2</strong> + <strong>Time to</strong>: End time with optional prefix (to, –).</li>
     40      <li><strong>Event text</strong>: Optional text that can be output in the shortcode.</li>
     41    </ul>
     42  </div>
     43
     44  <div class="lcs-howto-section">
     45    <h3>Recurring Tab</h3>
     46    <p>Recurring rules are patterns like "every Thursday", "first Thursday of the month", or "yearly on February 14". Multiple rules are combined with <strong>OR</strong> logic.</p>
    4547    <div class="lcs-callout">
    4648      <p><strong>Example:</strong> You want content visible <em>every Monday</em> and also <em>on the 1st of each month</em> → create two rules. As soon as one rule matches, the block is visible.</p>
    4749    </div>
    48     <p>Each rule can have its own <strong>rule period</strong> (from/to) which restricts only that rule.</p>
    49   </div>
    50 
    51   <div class="lcs-howto-section">
    52     <h3>Tab “Exceptions”</h3>
    53     <p>Exceptions override all other rules. No content is shown within the defined exception ranges.</p>
    54     <ul>
    55       <li>For a single day, fill only <strong>from</strong> and leave <strong>to</strong> empty.</li>
    56       <li>For a range, fill both fields.</li>
     50    <p><strong>Types:</strong></p>
     51    <ul>
     52      <li><strong>Weekly</strong>: Every specific weekday.</li>
     53      <li><strong>Monthly</strong>: On a specific day of the month (e.g., every 15th).</li>
     54      <li><strong>Weekday in month</strong>: E.g., "1st Thursday" or "last Friday" of the month.</li>
     55      <li><strong>Yearly</strong>: On a specific date each year (e.g., February 14).</li>
     56    </ul>
     57    <p>Each rule can have its own <strong>validity period</strong> (Valid from/to) that restricts only that rule.</p>
     58  </div>
     59
     60  <div class="lcs-howto-section">
     61    <h3>Exceptions Tab</h3>
     62    <p>Exceptions override all other rules. On defined exceptions, <strong>no content</strong> is displayed.</p>
     63    <ul>
     64      <li>For single days, fill only the <strong>from</strong> field and leave the <strong>to</strong> field empty.</li>
     65      <li>For date ranges, fill both fields.</li>
    5766    </ul>
    5867    <div class="lcs-callout lcs-callout--warn">
    59       <p><strong>Important:</strong> If “now” is inside an exception range, the block stays hidden — even if dates or recurring rules match.</p>
    60     </div>
    61   </div>
    62 
    63   <div class="lcs-howto-section">
    64     <h3>Tab “Event Offset”</h3>
    65     <p>Event offset extends the visibility window <strong>before</strong> and/or <strong>after</strong> the calculated event. It does <strong>not</strong> change the shortcode text output — only visibility.</p>
    66     <ul>
    67       <li><strong>Days before</strong> + <strong>start time</strong>: when the block becomes visible before the event.</li>
    68       <li><strong>Days after</strong> + <strong>end time</strong>: when visibility ends (for ranges, based on the last day).</li>
    69     </ul>
    70   </div>
    71 
    72   <div class="lcs-howto-section">
    73     <h3>Tab “Preview”</h3>
    74     <p>The preview generates upcoming calculated events (including show-from time and shortcode text) — useful for testing your rules.</p>
    75   </div>
    76 
    77   <div class="lcs-howto-section">
    78     <h3>Using the CSS class in WordPress &amp; page builders</h3>
    79     <div class="lcs-callout">
    80       <p><strong>Gutenberg:</strong> Select block → “Advanced” → “Additional CSS class(es)” → enter <code>luzid-cs-&lt;slug&gt;</code> (without the dot).</p>
    81       <p><strong>Elementor:</strong> Select widget → “Advanced” → “CSS Classes” → enter <code>luzid-cs-&lt;slug&gt;</code>.</p>
    82       <p><strong>Divi/other builders:</strong> Use the “CSS Class” / “Custom CSS class” field with the same value.</p>
    83     </div>
    84     <p>Note: In builders you enter the class <strong>without</strong> the leading dot. The dot <code>.</code> is only used in CSS notation.</p>
    85   </div>
    86 
    87   <div class="lcs-howto-section">
    88     <h3>Shortcode output</h3>
    89     <p>Use the shortcode to print the next event – or a list of upcoming events – in the frontend.</p>
    90 
    91     <p><strong>Formats (<code>opt</code>):</strong></p>
    92     <ul>
    93       <li><code>opt="short"</code> → <em>01.01.2026</em></li>
    94       <li><code>opt="date"</code> → <em>Thursday, 01.01.2026</em></li>
    95       <li><code>opt="time"</code> → <em>from 16:00</em></li>
    96       <li><code>opt="long"</code> → <em>Thursday, 01.01.2026 from 16:00</em></li>
    97     </ul>
    98 
    99     <p><strong>Parameters:</strong></p>
    100     <ul>
    101       <li><code>list="true|false"</code> (default: <code>false</code>) → outputs <code>count</code> events as a list (one per line, separated by <code>&lt;br&gt;</code>).</li>
    102       <li><code>count="n"</code> (n &gt; 0, default: 10, max: 200) → number of events for <code>list="true"</code>.</li>
    103       <li><code>text="true|false"</code> (default: <code>false</code>) → appends the optional <strong>event text</strong> (from single dates / recurring rules).</li>
    104       <li><code>sep="…"</code> (default: <code>-</code>) → separator between <code>&lt;opt&gt;</code> and event text. Tip: line break with <code>sep="&lt;br&gt;"</code>.</li>
    105       <li><code>timeoffset="…"</code> (minutes, can be negative) → shifts the <em>printed output</em> only (not the schedule logic).</li>
    106       <li><code>lang="de|en"</code> → force output language (empty = current UI).</li>
    107     </ul>
    108 
    109     <div class="lcs-callout">
    110       <p><strong>Example:</strong> <code>[luzid_cs slug="maintenance" opt="long"]</code></p>
    111       <p><strong>Example (list + text):</strong> <code>[luzid_cs slug="krimidinner" opt="short" list="true" count="5" text="true" sep=" | "]</code></p>
    112       <p><strong>Example (text on new line):</strong> <code>[luzid_cs slug="krimidinner" opt="date" text="true" sep="&lt;br&gt;"]</code></p>
    113     </div>
    114 
    115     <p><strong>CSS hooks for your own styling (external CSS):</strong><br>
    116       Output is wrapped in spans so you can style date/separator/text individually:</p>
    117     <ul>
    118       <li><code>.luzid-cs</code> – wrapper (plus <code>.luzid-cs--single</code> / <code>.luzid-cs--list</code>)</li>
    119       <li><code>.luzid-cs-item</code> – one event (for list output)</li>
    120       <li><code>.luzid-cs-opt</code> – date/time part (opt)</li>
    121       <li><code>.luzid-cs-sep</code> – separator</li>
    122       <li><code>.luzid-cs-text</code> – optional event text</li>
    123     </ul>
    124 
    125     <p><em>Note:</em> <code>opt="list"</code> is kept for backwards compatibility and equals <code>opt="long" list="true"</code>.</p>
     68      <p><strong>Important:</strong> If "now" falls within an exception, the block stays invisible – even if dates or recurring rules match.</p>
     69    </div>
     70  </div>
     71
     72  <div class="lcs-howto-section">
     73    <h3>Event Classes Tab</h3>
     74    <p>Event classes allow you to define <strong>multiple independent visibility windows</strong> for a scheduler. Each class generates its own CSS class with individual offset settings.</p>
     75   
     76    <div class="lcs-callout">
     77      <p><strong>Use case example:</strong> You have an event on April 15th. With event classes you can:</p>
     78      <ul style="margin: 10px 0;">
     79        <li>Show a <strong>popup</strong> 4 days before</li>
     80        <li>Display the <strong>event content</strong> (with images) 2 weeks before</li>
     81        <li>Activate a <strong>menu item</strong> 11 days before</li>
     82      </ul>
     83      <p>Each of these content blocks has its own CSS class and becomes visible independently.</p>
     84    </div>
     85
     86    <h4>Standard Class</h4>
     87    <p>The first class is always called "Standard" and cannot be deleted or renamed. It generates the main CSS class:</p>
     88    <p><code>.luzid-cs-&lt;schedulername&gt;</code></p>
     89
     90    <h4>Additional Classes</h4>
     91    <p>Click "+ Add class" to create more classes. Each additional class gets its own name and generates an extended CSS class:</p>
     92    <p><code>.luzid-cs-&lt;schedulername&gt;-&lt;classname&gt;</code></p>
     93
     94    <h4>Offset Settings per Class</h4>
     95    <ul>
     96      <li><strong>Days before</strong>: How many days before the event should the content become visible?</li>
     97      <li><strong>Start time</strong>: At what time does visibility begin? (empty = 00:00)</li>
     98      <li><strong>Days after</strong>: How many days after the event should the content remain visible?</li>
     99      <li><strong>End time</strong>: Until what time does visibility last? (empty = event time)</li>
     100    </ul>
     101
     102    <h4>Preview</h4>
     103    <p>Below each class you'll see a live preview of the visibility window based on the next calculated event.</p>
     104
     105    <div class="lcs-callout">
     106      <p><strong>Example configuration:</strong></p>
     107      <table class="lcs-table lcs-table--compact">
     108        <thead>
     109          <tr><th>Class</th><th>CSS Class</th><th>Days before</th><th>Usage</th></tr>
     110        </thead>
     111        <tbody>
     112          <tr><td>Standard</td><td><code>.luzid-cs-murder-mystery</code></td><td>14</td><td>Main content with images</td></tr>
     113          <tr><td>popup</td><td><code>.luzid-cs-murder-mystery-popup</code></td><td>4</td><td>Announcement popup</td></tr>
     114          <tr><td>menu</td><td><code>.luzid-cs-murder-mystery-menu</code></td><td>11</td><td>Menu entry</td></tr>
     115        </tbody>
     116      </table>
     117    </div>
     118  </div>
     119
     120  <div class="lcs-howto-section">
     121    <h3>Preview Tab</h3>
     122    <p>The preview shows you the next calculated events (including display time and shortcode text) – useful for testing your rules.</p>
     123  </div>
     124
     125  <div class="lcs-howto-section">
     126    <h3>CSS Class in WordPress &amp; Page Builders</h3>
     127    <div class="lcs-callout">
     128      <p><strong>Gutenberg:</strong> Select block → "Advanced" → "Additional CSS class(es)" → enter <code>luzid-cs-&lt;slug&gt;</code> (without dot).</p>
     129      <p><strong>Elementor:</strong> Select widget → "Advanced" → "CSS Classes" → enter <code>luzid-cs-&lt;slug&gt;</code>.</p>
     130      <p><strong>Divi/other builders:</strong> Enter the same value in the "CSS Class" or "Custom CSS Class" field.</p>
     131    </div>
     132    <p>Important: The class is entered <strong>without the leading dot</strong> in the builder. The dot <code>.</code> is only used in CSS notation.</p>
     133  </div>
     134
     135  <div class="lcs-howto-section">
     136    <h3>Shortcode: Single Event</h3>
     137    <p>Use the <code>[luzid_cs]</code> shortcode to output the next event or a list of upcoming events in the frontend.</p>
     138
     139    <p><strong>Basic syntax:</strong> <code>[luzid_cs slug="your-slug"]</code></p>
     140
     141    <h4>Parameter Overview</h4>
     142    <table class="lcs-table lcs-table--compact">
     143      <thead>
     144        <tr><th>Parameter</th><th>Default</th><th>Description</th></tr>
     145      </thead>
     146      <tbody>
     147        <tr><td><code>slug</code></td><td><em>(required)</em></td><td>Scheduler slug</td></tr>
     148        <tr><td><code>date</code></td><td><em>(leer)</em></td><td>Date format: <code>short</code>, <code>medium</code>, <code>long</code>, <code>full</code></td></tr>
     149        <tr><td><code>time</code></td><td><em>(empty)</em></td><td>Time format: <code>auto</code>, <code>raw</code>, <code>prefix</code>, <code>range</code>, <code>range_long</code></td></tr>
     150        <tr><td><code>list</code></td><td><code>false</code></td><td>Output as list: <code>true</code> or <code>false</code></td></tr>
     151        <tr><td><code>count</code></td><td><code>10</code></td><td>Number of events when <code>list="true"</code> (max. 200)</td></tr>
     152        <tr><td><code>text</code></td><td><code>false</code></td><td>Append event text: <code>true</code> or <code>false</code></td></tr>
     153        <tr><td><code>sep1</code></td><td><code> </code> (space)</td><td>Separator between date and time</td></tr>
     154        <tr><td><code>sep2</code></td><td><code> </code> (space)</td><td>Separator between time and text</td></tr>
     155        <tr><td><code>sep3</code></td><td><em>(empty)</em></td><td>Separator between list items</td></tr>
     156        <tr><td><code>timeoffset</code></td><td><code>0</code></td><td>Time offset in minutes (can be negative)</td></tr>
     157        <tr><td><code>lang</code></td><td><em>(current)</em></td><td>Force language: <code>de</code> or <code>en</code></td></tr>
     158      </tbody>
     159    </table>
     160
     161    <h4>Values for <code>date</code></h4>
     162    <table class="lcs-table lcs-table--compact">
     163      <thead>
     164        <tr><th>Value</th><th>Output (Example)</th></tr>
     165      </thead>
     166      <tbody>
     167        <tr><td><code>short</code></td><td>14.02.2026</td></tr>
     168        <tr><td><code>medium</code></td><td>Sat, 14.02.2026</td></tr>
     169        <tr><td><code>long</code></td><td>Saturday, 14.02.2026</td></tr>
     170        <tr><td><code>full</code></td><td>Saturday, 14 February 2026</td></tr>
     171      </tbody>
     172    </table>
     173
     174    <h4>Values for <code>time</code></h4>
     175    <p><strong>Important:</strong> Without the <code>time</code> parameter, <strong>no time</strong> is displayed – only the date!</p>
     176    <table class="lcs-table lcs-table--compact">
     177      <thead>
     178        <tr><th>Value</th><th>Output (Example)</th></tr>
     179      </thead>
     180      <tbody>
     181        <tr><td><em>(not set)</em></td><td><em>no time</em></td></tr>
     182        <tr><td><code>raw</code></td><td>18:00</td></tr>
     183        <tr><td><code>prefix</code></td><td>from 18:00</td></tr>
     184        <tr><td><code>range</code></td><td>18:00 to 20:00</td></tr>
     185        <tr><td><code>range_long</code></td><td>from 18:00 to 20:00</td></tr>
     186        <tr><td><code>auto</code></td><td>from 18:00 to 20:00 <em>(intelligent)</em></td></tr>
     187      </tbody>
     188    </table>
     189
     190    <h4>Separators – How They Work</h4>
     191    <p>The output consists of up to three parts: <strong>Date</strong>, <strong>Time</strong>, and <strong>Event text</strong>. Separators let you control what appears between these parts.</p>
     192   
     193    <div class="lcs-callout">
     194      <p><strong>Output structure:</strong></p>
     195      <p style="font-family: monospace; background: #f5f5f5; padding: 10px; border-radius: 4px;">
     196        [DATE] <span style="color: #c00;">sep1</span> [TIME] <span style="color: #c00;">sep2</span> [TEXT]
     197      </p>
     198      <ul style="margin-top: 10px;">
     199        <li><code>sep1</code> = What goes between date and time?</li>
     200        <li><code>sep2</code> = What goes between time and text? (or between date and text if no time)</li>
     201        <li><code>sep3</code> = What goes between entries in a list?</li>
     202      </ul>
     203    </div>
     204
     205    <p><strong>Common values:</strong></p>
     206    <ul>
     207      <li><code>" "</code> (space) – everything on one line</li>
     208      <li><code>"&lt;br&gt;"</code> – line break (new line)</li>
     209      <li><code>" | "</code> – pipe character as separator</li>
     210      <li><code>" – "</code> – dash as separator</li>
     211    </ul>
     212
     213    <h4>Examples with Separators</h4>
     214   
     215    <p><strong>Everything on one line (default):</strong></p>
     216    <pre><code>[luzid_cs slug="event" date="long" time="auto" text="true"]</code></pre>
     217    <p>→ Saturday, 14.02.2026 from 18:00 to 20:00 Valentine's Dinner</p>
     218
     219    <p><strong>Date and text only, two lines:</strong></p>
     220    <pre><code>[luzid_cs slug="event" date="long" text="true" sep2="&lt;br&gt;"]</code></pre>
     221    <p>→ Saturday, 14.02.2026<br>&nbsp;&nbsp;&nbsp;Valentine's Dinner</p>
     222
     223    <p><strong>Date, time, and text each on separate lines (three lines):</strong></p>
     224    <pre><code>[luzid_cs slug="event" date="long" time="auto" text="true" sep1="&lt;br&gt;" sep2="&lt;br&gt;"]</code></pre>
     225    <p>→ Saturday, 14.02.2026<br>&nbsp;&nbsp;&nbsp;from 18:00 to 20:00<br>&nbsp;&nbsp;&nbsp;Valentine's Dinner</p>
     226
     227    <p><strong>With separators:</strong></p>
     228    <pre><code>[luzid_cs slug="event" date="long" time="raw" text="true" sep1=" | " sep2=" – "]</code></pre>
     229    <p>→ Saturday, 14.02.2026 | 18:00 – Valentine's Dinner</p>
     230
     231    <p><strong>List with blank line between entries:</strong></p>
     232    <pre><code>[luzid_cs slug="event" date="long" text="true" sep2="&lt;br&gt;" sep3="&lt;br&gt;&lt;br&gt;" list="true" count="5"]</code></pre>
     233
     234    <h4>CSS Classes for Custom Styling</h4>
     235    <ul>
     236      <li><code>.luzid-cs</code> – Wrapper (also <code>.luzid-cs--single</code> / <code>.luzid-cs--list</code>)</li>
     237      <li><code>.luzid-cs-item</code> – One event (in list output)</li>
     238      <li><code>.luzid-cs-date</code> – Date part</li>
     239      <li><code>.luzid-cs-time</code> – Time part</li>
     240      <li><code>.luzid-cs-text</code> – Event text</li>
     241      <li><code>.luzid-cs-sep</code> – Separator (also <code>.luzid-cs-sep1</code> / <code>.luzid-cs-sep2</code>)</li>
     242    </ul>
     243  </div>
     244
     245  <div class="lcs-howto-section">
     246    <h3>Shortcode: Event Table</h3>
     247    <p>Use <code>[luzid_cs_eventtable]</code> to output a table of all upcoming events – from all schedulers that have "Include in Event Table" enabled.</p>
     248
     249    <p><strong>Basic syntax:</strong> <code>[luzid_cs_eventtable]</code></p>
     250
     251    <h4>Parameter Overview</h4>
     252    <table class="lcs-table lcs-table--compact">
     253      <thead>
     254        <tr><th>Parameter</th><th>Default</th><th>Description</th></tr>
     255      </thead>
     256      <tbody>
     257        <tr><td><code>cols</code></td><td><code>date_medium,time_auto,text</code></td><td>Columns (comma-separated)</td></tr>
     258        <tr><td><code>count</code></td><td><code>30</code></td><td>Max. number of events</td></tr>
     259        <tr><td><code>headers</code></td><td><em>(automatic)</em></td><td>Custom headers (comma-separated)</td></tr>
     260        <tr><td><code>noheaders</code></td><td><code>false</code></td><td>Hide headers</td></tr>
     261        <tr><td><code>class</code></td><td><em>(empty)</em></td><td>Additional CSS class</td></tr>
     262        <tr><td><code>empty</code></td><td>No events</td><td>Text when no events</td></tr>
     263        <tr><td><code>order</code></td><td><code>asc</code></td><td>Sort order: <code>asc</code> or <code>desc</code></td></tr>
     264        <tr><td><code>lang</code></td><td><em>(current)</em></td><td>Force language</td></tr>
     265      </tbody>
     266    </table>
     267
     268    <h4>Available Column Types for <code>cols</code></h4>
     269    <table class="lcs-table lcs-table--compact">
     270      <thead>
     271        <tr><th>Column</th><th>Output (Example)</th></tr>
     272      </thead>
     273      <tbody>
     274        <tr><td><code>date_short</code></td><td>14.02.2026</td></tr>
     275        <tr><td><code>date_medium</code></td><td>Sat, 14.02.2026</td></tr>
     276        <tr><td><code>date_long</code></td><td>Saturday, 14.02.2026</td></tr>
     277        <tr><td><code>date_full</code></td><td>Saturday, 14 February 2026</td></tr>
     278        <tr><td><code>weekday_short</code></td><td>Sat</td></tr>
     279        <tr><td><code>weekday_long</code></td><td>Saturday</td></tr>
     280        <tr><td><code>time_raw</code></td><td>18:00</td></tr>
     281        <tr><td><code>time_auto</code></td><td>from 18:00 to 20:00</td></tr>
     282        <tr><td><code>time_prefix</code></td><td>from 18:00</td></tr>
     283        <tr><td><code>time_range</code></td><td>18:00 to 20:00</td></tr>
     284        <tr><td><code>time_range_long</code></td><td>from 18:00 to 20:00</td></tr>
     285        <tr><td><code>text</code></td><td>Event text</td></tr>
     286        <tr><td><code>scheduler</code></td><td>Scheduler name</td></tr>
     287      </tbody>
     288    </table>
     289
     290    <div class="lcs-callout">
     291      <p><strong>Examples:</strong></p>
     292      <p><code>[luzid_cs_eventtable]</code> → Default table</p>
     293      <p><code>[luzid_cs_eventtable cols="weekday_short,date_short,time_range,text" count="10"]</code></p>
     294      <p><code>[luzid_cs_eventtable cols="date_long,time_auto,text,scheduler" headers="Date,Time,Event,Category"]</code></p>
     295    </div>
     296
     297    <p><strong>CSS Classes for Custom Styling:</strong></p>
     298    <ul>
     299      <li><code>.luzid-cs-eventtable</code> – Table</li>
     300      <li><code>.luzid-cs-eventtable__head</code> – Table head</li>
     301      <li><code>.luzid-cs-eventtable__body</code> – Table body</li>
     302      <li><code>.luzid-cs-eventtable__row</code> – Row (+ <code>--header</code>, <code>--odd</code>, <code>--even</code>)</li>
     303      <li><code>.luzid-cs-eventtable__cell</code> – Cell (+ <code>--date_short</code>, <code>--time_auto</code>, etc.)</li>
     304    </ul>
    126305  </div>
    127306</div>
  • luzid-content-scheduler/trunk/luzid-content-scheduler.php

    r3456038 r3476291  
    44 * Plugin URI:
    55 * Description: Schedule the visibility of frontend content blocks (banners, alerts, divs) and output the next event via shortcode.
    6  * Version: 1.2.2
     6 * Version: 1.4.2
    77 * Author: Luzid Media
    88 * Author URI: https://luzid-media.com
     
    6565class Luzid_Content_Scheduler {
    6666
    67     const VERSION = '1.2.2';
     67    const VERSION = '1.4.2';
    6868    const MENU_SLUG   = 'luzid-content-scheduler';
    6969    const OPT_ENTRIES = 'luzid_cs_entries';
     
    290290
    291291        add_shortcode( 'luzid_cs', [ $this, 'shortcode_next_event' ] );
     292        add_shortcode( 'luzid_cs_eventtable', [ $this, 'shortcode_eventtable' ] );
     293       
    292294
    293295        add_filter( 'body_class', [ $this, 'body_class_active_entries' ] );
     
    307309                return [];
    308310            }
    309             // Deep sanitize: we sanitize individual fields when accessing them,
    310             // but we need the raw structure for array fields like luzid_cs_single[].
    311             // Using map_deep with sanitize_text_field for basic protection.
    312311            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in handle_actions() before data processing.
    313312            return map_deep( wp_unslash( $_POST ), 'sanitize_text_field' );
     
    477476            'exceptions' => [],
    478477            'event_offset' => [ 'enabled' => 0, 'days_before' => 0, 'start_time' => '00:01', 'days_after' => 0, 'end_time' => '' ],
     478            'include_in_eventtable' => 0,
    479479        ];
    480480
     
    520520    $type = sanitize_text_field( (string) $type_raw );
    521521    if ( $type === '' ) { continue; }
    522     $allowed_types = [ 'weekly', 'day_of_month', 'nth_weekday_month' ];
     522    $allowed_types = [ 'weekly', 'day_of_month', 'nth_weekday_month', 'yearly' ];
    523523    if ( ! in_array( $type, $allowed_types, true ) ) { continue; }
    524524    $time = $this->sanitize_time( $rr['time'] ?? '' );
     
    536536            $offset_enabled = ! empty( $post['luzid_cs_offset_enabled'] ) ? 1 : 0;
    537537
    538             $days_before = isset( $post['luzid_cs_offset_days_before'] ) ? (int) $post['luzid_cs_offset_days_before'] : 0;
     538// Backward-Compat: altes Feld luzid_cs_offset_days -> days_before
     539            $days_before = isset( $post['luzid_cs_offset_days_before'] ) ? (int) wp_unslash( (string) $post['luzid_cs_offset_days_before'] ) : ( isset( $post['luzid_cs_offset_days'] ) ? (int) wp_unslash( (string) $post['luzid_cs_offset_days'] ) : 0 );
    539540$days_before = max( 0, $days_before );
    540541
     
    555556    'end_time' => $end_time,
    556557];
     558
     559// Include in Event Table
     560$entry['include_in_eventtable'] = ! empty( $post['luzid_cs_include_in_eventtable'] ) ? 1 : 0;
    557561
    558562            return [ 'entry' => $entry, 'errors' => $errors ];
     
    660664    private function sanitize_prefix( $s ) {
    661665        $s = sanitize_text_field( (string) $s );
    662         $allowed = [ '', 'ab', 'bis', 'um' ];
     666        // Präfix 1: keins | ab | bis | um | von | Ab | Bis | Um | Von (+ EN variants)
     667        $allowed = [ '', 'ab', 'bis', 'um', 'von', 'Ab', 'Bis', 'Um', 'Von', 'from', 'until', 'at', 'From', 'Until', 'At' ];
    663668        return in_array( $s, $allowed, true ) ? $s : '';
    664669    }
    665670
     671    private function sanitize_prefix2( $s ) {
     672        $s = sanitize_text_field( (string) $s );
     673        // Präfix 2: keins | bis | Bis | dash (+ EN variants)
     674        $allowed = [ '', 'bis', 'Bis', 'dash', 'to', 'To' ];
     675        return in_array( $s, $allowed, true ) ? $s : '';
     676    }
     677
    666678    /**
    667      * Termine: Von/Bis (Bis optional). Für einzelne Tage nur "von" ausfüllen.
    668      * Backward-Compat: alte Struktur {date,prefix,time} wird als {from,to,prefix,time} interpretiert.
     679     * Termine: Einzelne Tage mit optionaler Zeit und Zeitspanne.
    669680     */
    670681   
     
    679690        // Migration: "date" -> "from"
    680691        $from = $this->sanitize_date( $r['from'] ?? ( $r['date'] ?? '' ) );
    681         $to   = $this->sanitize_date( $r['to'] ?? '' );
    682692
    683693        if ( $from === '' ) {
    684694            continue;
    685695        }
    686         if ( $to !== '' && $to < $from ) {
    687             $tmp  = $from;
    688             $from = $to;
    689             $to   = $tmp;
    690         }
    691696
    692697        $prefix = $this->sanitize_prefix( $r['prefix'] ?? '' );
    693698        $time   = $this->sanitize_time( $r['time'] ?? '' );
     699        $prefix2 = $this->sanitize_prefix2( $r['prefix2'] ?? '' );
     700        $time_to = $this->sanitize_time( $r['time_to'] ?? '' );
    694701        $text   = $this->sanitize_event_text( $r['text'] ?? '' );
    695702
    696703        $out[] = [
    697             'from'   => $from,
    698             'to'     => $to,
    699             'prefix' => $prefix,
    700             'time'   => $time,
    701             'text'   => $text,
     704            'from'    => $from,
     705            'prefix'  => $prefix,
     706            'time'    => $time,
     707            'prefix2' => $prefix2,
     708            'time_to' => $time_to,
     709            'text'    => $text,
    702710        ];
    703711    }
     
    736744            if ( ! is_array( $r ) ) continue;
    737745            $type = sanitize_text_field( (string) ( $r['type'] ?? '' ) );
    738             $allowed = [ 'weekly', 'day_of_month', 'nth_weekday_month' ];
     746            $allowed = [ 'weekly', 'day_of_month', 'nth_weekday_month', 'yearly' ];
    739747            if ( ! in_array( $type, $allowed, true ) ) continue;
    740748
     
    742750            $time = $this->sanitize_time( $r['time'] ?? '' );
    743751            if ( $time === '' ) { continue; }
     752            $prefix2 = $this->sanitize_prefix2( $r['prefix2'] ?? '' );
     753            $time_to = $this->sanitize_time( $r['time_to'] ?? '' );
    744754            $text = $this->sanitize_event_text( $r['text'] ?? '' );
    745755
     
    752762                'prefix' => $prefix,
    753763                'time' => $time,
     764                'prefix2' => $prefix2,
     765                'time_to' => $time_to,
    754766                'text' => $text,
    755767                'from' => $from,
     
    784796                $rule['nth'] = $nth;
    785797                $rule['weekday'] = $weekday;
     798            }
     799
     800            if ( $type === 'yearly' ) {
     801                $yearly_day = isset( $r['yearly_day'] ) ? (int) $r['yearly_day'] : 0;
     802                $yearly_month = isset( $r['yearly_month'] ) ? (int) $r['yearly_month'] : 0;
     803                if ( $yearly_day < 1 || $yearly_day > 31 ) continue;
     804                if ( $yearly_month < 1 || $yearly_month > 12 ) continue;
     805                $rule['yearly_day'] = $yearly_day;
     806                $rule['yearly_month'] = $yearly_month;
    786807            }
    787808
     
    969990                $is_sel = ( ! $is_new && $selected && $selected['id'] === $e['id'] );
    970991                $slug = (string) ( $e['slug'] ?? '' );
    971                 $sc = $slug ? '[luzid_cs slug="' . esc_attr( $slug ) . '" opt="long"]' : '';
     992                $sc = $slug ? '[luzid_cs slug="' . esc_attr( $slug ) . '" date="long"]' : '';
    972993
    973994                $active = $this->is_entry_active_now( $e, $now_ts );
     
    10331054        $slug = (string) ( $entry['slug'] ?? '' );
    10341055        $css_class = $slug ? 'luzid-cs-' . $slug : 'luzid-cs-…';
    1035         $sc_example = $slug ? '[luzid_cs slug="' . esc_attr( $slug ) . '" opt="long"]' : '[luzid_cs slug="…" opt="long"]';
     1056        $sc_example = $slug ? '[luzid_cs slug="' . esc_attr( $slug ) . '" date="long"]' : '[luzid_cs slug="…" date="long"]';
     1057        $include_in_table = ! empty( $entry['include_in_eventtable'] );
    10361058
    10371059        echo '<div class="lcs-tab-content" data-panel="setup">';
     
    10591081        echo '<button type="button" class="lcs-copy-btn lcs-button-secondary lcs-btn-icon" data-lcs-copy="' . esc_attr( $sc_example ) . '"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span></button>';
    10601082        echo '</div>';
    1061         echo '<span class="lcs-description">' . esc_html( $this->is_en ? 'Prints the next event dynamically (opt: short|date|time|long).' : 'Gibt den nächsten Event dynamisch aus (opt: short|date|time|long).' ) . '</span>';
     1083        echo '<span class="lcs-description">' . esc_html( $this->is_en ? 'Prints the next event dynamically (date: short|medium|long|full, time: auto|raw|prefix|range).' : 'Gibt den nächsten Event dynamisch aus (date: short|medium|long|full, time: auto|raw|prefix|range).' ) . '</span>';
     1084        echo '</td></tr>';
     1085
     1086        // Include in Event Table
     1087        echo '<tr><th>' . esc_html( $this->is_en ? 'Event Table' : 'Event-Tabelle' ) . '</th><td>';
     1088        echo '<label class="lcs-checkbox-label"><input type="checkbox" name="luzid_cs_include_in_eventtable" value="1" ' . checked( $include_in_table, true, false ) . '>';
     1089        echo ' ' . esc_html( $this->is_en ? 'Include in Event Table output' : 'In Event-Tabelle einbeziehen' ) . '</label>';
     1090        echo '<span class="lcs-description">' . esc_html( $this->is_en ? 'If checked, events from this scheduler will be included when using [luzid_cs_eventtable].' : 'Wenn aktiviert, werden Events dieses Schedulers bei [luzid_cs_eventtable] einbezogen.' ) . '</span>';
    10621091        echo '</td></tr>';
    10631092
     
    10771106            'exceptions' => [],
    10781107            'event_offset' => [ 'enabled' => 0, 'days_before' => 0, 'start_time' => '00:01', 'days_after' => 0, 'end_time' => '' ],
     1108            'include_in_eventtable' => 0,
    10791109        ];
    10801110    }
     
    10961126        echo '<div class="lcs-tab-content" data-panel="termine">';
    10971127        $this->render_panel_title( $this->panel_title_text( 'termine', $entry, $is_new ) );
    1098         echo '<p class="lcs-paragraph lcs-text">' . esc_html( $this->is_en ? 'Dates define ranges in which content can be shown. For single days fill only the from field and leave the to field empty.' : 'Termine definieren Zeiträume, an denen Content angezeigt werden kann. Für einzelne Tage nur das von Feld ausfüllen und das bis Feld freilassen.' ) . '</p>';
    1099 
    1100         echo '<div data-lcs-repeater="single">';
    1101         echo '<table class="lcs-table lcs-table--compact">';
    1102         echo '<thead><tr>';
    1103         echo '<th>' . esc_html( $this->is_en ? 'From' : 'Von' ) . '</th><th>' . esc_html( $this->is_en ? 'To' : 'Bis' ) . '</th><th>' . esc_html( $this->is_en ? 'Prefix' : 'Präfix' ) . '</th><th>' . esc_html( $this->is_en ? 'Time (optional)' : 'Zeit (optional)' ) . '</th><th>' . esc_html( $this->is_en ? 'Event text (optional)' : 'Eventtext (optional)' ) . '</th><th>' . esc_html( $this->is_en ? 'Action' : 'Aktion' ) . '</th>';
    1104         echo '</tr></thead>';
    1105         echo '<tbody data-lcs-rows>';
     1128        echo '<p class="lcs-paragraph lcs-text">' . esc_html( $this->is_en ? 'Define single dates when content should be visible.' : 'Definiere einzelne Termine, an denen Content sichtbar sein soll.' ) . '</p>';
     1129
     1130        echo '<div data-lcs-repeater="single" class="lcs-cards-repeater">';
    11061131
    11071132        if ( empty( $rows ) ) {
    1108             $rows = [ [ 'from' => '', 'to' => '', 'prefix' => '', 'time' => '', 'text' => '' ] ];
     1133            $rows = [ [ 'from' => '', 'prefix' => '', 'time' => '', 'prefix2' => '', 'time_to' => '', 'text' => '' ] ];
    11091134        }
    11101135
    11111136            foreach ( $rows as $i => $r ) {
    11121137                $from_raw = (string) ( $r['from'] ?? ( $r['date'] ?? '' ) );
    1113                 $to_raw   = (string) ( $r['to'] ?? '' );
    11141138                $prefix   = (string) ( $r['prefix'] ?? '' );
    11151139                $time_raw = (string) ( $r['time'] ?? '' );
     1140                $prefix2  = (string) ( $r['prefix2'] ?? '' );
     1141                $time_to_raw = (string) ( $r['time_to'] ?? '' );
    11161142                $text_raw = (string) ( $r['text'] ?? '' );
    1117             echo '<tr data-lcs-row>';
    1118                 echo '<td><input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="luzid_cs_single[' . (int) $i . '][from]" value="' . esc_attr( $from_raw ) . '"></td>';
    1119                 echo '<td><input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="luzid_cs_single[' . (int) $i . '][to]" value="' . esc_attr( $to_raw ) . '"></td>';
    1120                 echo '<td>' . $this->prefix_select( 'luzid_cs_single[' . (int) $i . '][prefix]', $prefix ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    1121                 echo '<td><input class="lcs-input" type="time" name="luzid_cs_single[' . (int) $i . '][time]" value="' . esc_attr( $time_raw ) . '"></td>';
    1122                 echo '<td><input class="lcs-input" type="text" name="luzid_cs_single[' . (int) $i . '][text]" value="' . esc_attr( $text_raw ) . '" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '"></td>';
    1123             echo '<td><div class="lcs-row-actions">';
    1124             echo '<a class="lcs-copy-btn lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove row' : 'Zeile entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>';
    1125             echo '</div></td>';
    1126             echo '</tr>';
    1127         }
    1128 
    1129         echo '</tbody>';
    1130         echo '</table>';
     1143           
     1144            echo '<div class="lcs-card" data-lcs-row>';
     1145            echo '<div class="lcs-card__row">';
     1146            echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Date' : 'Datum' ) . '</label><input class="lcs-input" type="date" name="luzid_cs_single[' . (int) $i . '][from]" value="' . esc_attr( $from_raw ) . '"></div>';
     1147            echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Prefix 1' : 'Präfix 1' ) . '</label>' . $this->prefix_select( 'luzid_cs_single[' . (int) $i . '][prefix]', $prefix ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     1148            echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Time from' : 'Zeit von' ) . '</label><input class="lcs-input" type="time" name="luzid_cs_single[' . (int) $i . '][time]" value="' . esc_attr( $time_raw ) . '"></div>';
     1149            echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Prefix 2' : 'Präfix 2' ) . '</label>' . $this->prefix2_select( 'luzid_cs_single[' . (int) $i . '][prefix2]', $prefix2 ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     1150            echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Time to' : 'Zeit bis' ) . '</label><input class="lcs-input" type="time" name="luzid_cs_single[' . (int) $i . '][time_to]" value="' . esc_attr( $time_to_raw ) . '"></div>';
     1151            echo '</div>';
     1152            echo '<div class="lcs-card__row">';
     1153            echo '<div class="lcs-card__field lcs-card__field--wide"><label>' . esc_html( $this->is_en ? 'Event text' : 'Eventtext' ) . '</label><input class="lcs-input" type="text" name="luzid_cs_single[' . (int) $i . '][text]" value="' . esc_attr( $text_raw ) . '" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '"></div>';
     1154            echo '<div class="lcs-card__actions"><a class="lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove' : 'Entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a></div>';
     1155            echo '</div>';
     1156            echo '</div>';
     1157        }
    11311158
    11321159        echo '<template data-lcs-template>';
    1133         echo '<tr data-lcs-row>';
    1134         echo '<td><input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="luzid_cs_single[__i__][from]" value=""></td>';
    1135         echo '<td><input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="luzid_cs_single[__i__][to]" value=""></td>';
    1136             echo '<td>' . $this->prefix_select( 'luzid_cs_single[__i__][prefix]', '' ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    1137         echo '<td><input class="lcs-input" type="time" name="luzid_cs_single[__i__][time]" value=""></td>';
    1138         echo '<td><input class="lcs-input" type="text" name="luzid_cs_single[__i__][text]" value="" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '"></td>';
    1139         echo '<td><div class="lcs-row-actions">';
    1140         echo '<a class="lcs-copy-btn lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove row' : 'Zeile entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>';
    1141         echo '</div></td>';
    1142         echo '</tr>';
     1160        echo '<div class="lcs-card" data-lcs-row>';
     1161        echo '<div class="lcs-card__row">';
     1162        echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Date' : 'Datum' ) . '</label><input class="lcs-input" type="date" name="luzid_cs_single[__i__][from]" value=""></div>';
     1163        echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Prefix 1' : 'Präfix 1' ) . '</label>' . $this->prefix_select( 'luzid_cs_single[__i__][prefix]', '' ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     1164        echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Time from' : 'Zeit von' ) . '</label><input class="lcs-input" type="time" name="luzid_cs_single[__i__][time]" value=""></div>';
     1165        echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Prefix 2' : 'Präfix 2' ) . '</label>' . $this->prefix2_select( 'luzid_cs_single[__i__][prefix2]', '' ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     1166        echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Time to' : 'Zeit bis' ) . '</label><input class="lcs-input" type="time" name="luzid_cs_single[__i__][time_to]" value=""></div>';
     1167        echo '</div>';
     1168        echo '<div class="lcs-card__row">';
     1169        echo '<div class="lcs-card__field lcs-card__field--wide"><label>' . esc_html( $this->is_en ? 'Event text' : 'Eventtext' ) . '</label><input class="lcs-input" type="text" name="luzid_cs_single[__i__][text]" value="" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '"></div>';
     1170        echo '<div class="lcs-card__actions"><a class="lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove' : 'Entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a></div>';
     1171        echo '</div>';
     1172        echo '</div>';
    11431173        echo '</template>';
    11441174
     
    11581188        echo '<p class="lcs-paragraph lcs-text">' . esc_html( $this->is_en ? 'Multiple recurring rules (OR logic). Each rule has its own period (from/to) that restricts only that rule.' : 'Mehrere Wiederholungsregeln (ODER-Logik). Jede Regel hat einen eigenen Zeitraum (von/bis), der nur diese Regel einschränkt.' ) . '</p>';
    11591189
    1160         echo '<div data-lcs-repeater="recurring">';
    1161         echo '<table class="lcs-table lcs-table--compact">';
    1162         echo '<thead><tr>';
    1163         echo '<th>' . esc_html( $this->is_en ? 'Type' : 'Typ' ) . '</th><th>' . esc_html( $this->is_en ? 'Details' : 'Details' ) . '</th><th>' . esc_html( $this->is_en ? 'Rule period' : 'Regel-Zeitraum' ) . '</th><th>' . esc_html( $this->is_en ? 'Action' : 'Aktion' ) . '</th>';
    1164         echo '</tr></thead>';
    1165         echo '<tbody data-lcs-rows>';
     1190        echo '<div data-lcs-repeater="recurring" class="lcs-cards-repeater">';
    11661191
    11671192        if ( empty( $rows ) ) {
    1168             $rows = [ [ 'type' => '', 'weekdays' => [], 'prefix' => '', 'time' => '', 'text' => '', 'from' => '', 'to' => '' ] ];
     1193            $rows = [ [ 'type' => '', 'weekdays' => [], 'prefix' => '', 'time' => '', 'prefix2' => '', 'time_to' => '', 'text' => '', 'from' => '', 'to' => '' ] ];
    11691194        }
    11701195
    11711196        foreach ( $rows as $i => $r ) {
    1172                 echo $this->render_recurring_row( $i, $r ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    1173         }
    1174 
    1175         echo '</tbody>';
    1176         echo '</table>';
     1197                echo $this->render_recurring_card( $i, $r ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     1198        }
    11771199
    11781200        echo '<template data-lcs-template>';
    1179             echo $this->render_recurring_row( '__i__', [ 'type' => '', 'weekdays' => [], 'prefix' => '', 'time' => '', 'text' => '', 'from' => '', 'to' => '' ], true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     1201            echo $this->render_recurring_card( '__i__', [ 'type' => '', 'weekdays' => [], 'prefix' => '', 'time' => '', 'prefix2' => '', 'time_to' => '', 'text' => '', 'from' => '', 'to' => '' ], true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    11801202        echo '</template>';
    11811203
     
    11881210    }
    11891211
    1190     private function render_recurring_row( $i, $r, $is_template = false ) {
     1212    private function render_recurring_card( $i, $r, $is_template = false ) {
    11911213        $idx = $i;
    11921214        $name = function( $field ) use ( $idx ) { return 'luzid_cs_recurring[' . $idx . '][' . $field . ']'; };
     
    11951217        $prefix = (string) ( $r['prefix'] ?? '' );
    11961218        $time = (string) ( $r['time'] ?? '' );
     1219        $prefix2 = (string) ( $r['prefix2'] ?? '' );
     1220        $time_to = (string) ( $r['time_to'] ?? '' );
    11971221        $text = (string) ( $r['text'] ?? '' );
    11981222        $from = (string) ( $r['from'] ?? '' );
    11991223        $to = (string) ( $r['to'] ?? '' );
    12001224
    1201         $html = '';
    12021225        $is_invalid = ( ! $is_template && $type !== '' && $time === '' );
    1203         $tr_class = $is_invalid ? ' class="lcs-row--invalid"' : '';
    1204         $html .= '<tr data-lcs-row' . $tr_class . '>';
    1205 
    1206         // Type
    1207         $html .= '<td>';
     1226        $card_class = $is_invalid ? 'lcs-card lcs-card--invalid' : 'lcs-card';
     1227
     1228        $html = '<div class="' . esc_attr( $card_class ) . '" data-lcs-row>';
     1229
     1230        // Warning for incomplete rules
     1231        if ( $is_invalid ) {
     1232            $html .= '<div class="lcs-rec-inline-warning">';
     1233            $html .= '<span class="dashicons dashicons-warning"></span>';
     1234            $html .= '<strong>' . esc_html( $this->is_en ? 'Incomplete: missing time – this rule will be ignored.' : 'Unvollständig: Uhrzeit fehlt – diese Regel wird ignoriert.' ) . '</strong>';
     1235            $html .= '</div>';
     1236        }
     1237
     1238        // Row 1: Type + Details + Prefix 1 + Time from + Prefix 2 + Time to
     1239        $html .= '<div class="lcs-card__row">';
     1240       
     1241        // Type select
     1242        $html .= '<div class="lcs-card__field">';
     1243        $html .= '<label>' . esc_html( $this->is_en ? 'Type' : 'Typ' ) . '</label>';
    12081244        $html .= '<select class="lcs-select lcs-rec-type" name="' . esc_attr( $name('type') ) . '">';
    1209 $html .= '<option value="">' . esc_html( $this->is_en ? '— Choose type —' : '— Typ wählen —' ) . '</option>';
     1245        $html .= '<option value="">' . esc_html( $this->is_en ? '— Choose —' : '— Wählen —' ) . '</option>';
    12101246        $html .= '<option value="weekly" ' . selected( $type, 'weekly', false ) . '>' . esc_html( $this->is_en ? 'Weekly' : 'Wöchentlich' ) . '</option>';
    12111247        $html .= '<option value="day_of_month" ' . selected( $type, 'day_of_month', false ) . '>' . esc_html( $this->is_en ? 'Monthly' : 'Monatlich' ) . '</option>';
    12121248        $html .= '<option value="nth_weekday_month" ' . selected( $type, 'nth_weekday_month', false ) . '>' . esc_html( $this->is_en ? 'Weekday in month' : 'Wochentag im Monat' ) . '</option>';
     1249        $html .= '<option value="yearly" ' . selected( $type, 'yearly', false ) . '>' . esc_html( $this->is_en ? 'Yearly' : 'Jährlich' ) . '</option>';
    12131250        $html .= '</select>';
    1214         $html .= '</td>';
    1215 
    1216         // Details
    1217         $html .= '<td class="lcs-rec-details">';
    1218 if ( $is_invalid ) {
    1219     $html .= '<div class="lcs-rec-inline-warning" style="margin-bottom:8px; padding:8px 10px; background:#fff8e5; border:1px solid #dba617; border-radius:4px; color:#6b5a00; font-size:13px;">';
    1220     $html .= '<span class="dashicons dashicons-warning" style="margin-right:6px; vertical-align:middle;"></span>';
    1221     $html .= '<strong>' . esc_html( $this->is_en ? 'Incomplete: missing time – this rule will be ignored.' : 'Unvollständig: Uhrzeit fehlt – diese Regel wird ignoriert.' ) . '</strong>';
    1222     $html .= '</div>';
    1223 }
    1224 
    1225 
    1226         // Weekly details (Dropdown, single)
     1251        $html .= '</div>';
     1252
     1253        // Details (dynamic based on type)
    12271254        $weekday_single = (int) ( $r['weekday'] ?? 0 );
    12281255        if ( $weekday_single < 1 || $weekday_single > 7 ) {
     
    12321259        }
    12331260
    1234         $weekly_style = ( $type === 'weekly' ) ? 'display: inline-flex;' : 'display: none;';
    1235         $dom_style    = ( $type === 'day_of_month' ) ? 'display: inline-flex;' : 'display: none;';
    1236         $nth_style    = ( $type === 'nth_weekday_month' ) ? 'display: inline-flex;' : 'display: none;';
    1237 
    1238         $html .= '<span class="lcs-inline-inputs" data-details="weekly" style="' . esc_attr( $weekly_style ) . '">';
    1239         $html .= '<span class="lcs-help-inline">' . esc_html( $this->is_en ? 'Every ' : 'Jeden ' ) . '</span> ';
     1261        $weekly_style = ( $type === 'weekly' ) ? '' : 'display: none;';
     1262        $dom_style    = ( $type === 'day_of_month' ) ? '' : 'display: none;';
     1263        $nth_style    = ( $type === 'nth_weekday_month' ) ? '' : 'display: none;';
     1264        $yearly_style = ( $type === 'yearly' ) ? '' : 'display: none;';
     1265
     1266        // Weekly: Weekday
     1267        $html .= '<div class="lcs-card__field" data-details="weekly" style="' . esc_attr( $weekly_style ) . '">';
     1268        $html .= '<label>' . esc_html( $this->is_en ? 'Weekday' : 'Wochentag' ) . '</label>';
    12401269        $html .= $this->weekday_select( $name('weekday'), $weekday_single );
    1241         $html .= '</span>';
    1242 
    1243         // Day-of-month details (Dropdown)
     1270        $html .= '</div>';
     1271
     1272        // Monthly: Day of month
    12441273        $dom = (int) ( $r['day'] ?? 1 );
    12451274        if ( $dom < 1 || $dom > 31 ) { $dom = 1; }
    1246         $html .= '<span class="lcs-inline-inputs" data-details="day_of_month" style="' . esc_attr( $dom_style ) . '">';
    1247         $html .= '<span class="lcs-help-inline">' . esc_html( $this->is_en ? 'Every ' : 'Jeden ' ) . '</span> ';
     1275        $html .= '<div class="lcs-card__field" data-details="day_of_month" style="' . esc_attr( $dom_style ) . '">';
     1276        $html .= '<label>' . esc_html( $this->is_en ? 'Day' : 'Tag' ) . '</label>';
    12481277        $html .= $this->day_of_month_select( $name('day'), $dom );
    1249         $html .= '<span class="lcs-help-inline"> </span>';
    1250         $html .= '</span>';
    1251 
    1252         // Nth weekday details
     1278        $html .= '</div>';
     1279
     1280        // Nth weekday: Nth + Weekday
    12531281        $nth = (int) ( $r['nth'] ?? 1 );
    12541282        $weekday = (int) ( $r['weekday'] ?? 4 );
    1255         $html .= '<span class="lcs-inline-inputs" data-details="nth_weekday_month" style="' . esc_attr( $nth_style ) . '">';
    1256         $html .= '<span class="lcs-help-inline">' . esc_html( $this->is_en ? 'Every' : 'Jeden' ) . '</span> ';
     1283        $html .= '<div class="lcs-card__field" data-details="nth_weekday_month" style="' . esc_attr( $nth_style ) . '">';
     1284        $html .= '<label>' . esc_html( $this->is_en ? 'Which' : 'Welcher' ) . '</label>';
    12571285        $html .= '<select class="lcs-select" name="' . esc_attr( $name('nth') ) . '">';
    12581286        $nth_labels = [ 1 => '1.', 2 => '2.', 3 => '3.', 4 => '4.', 5 => '5.', -1 => ( $this->is_en ? 'last' : 'letzter' ) ];
     
    12611289        }
    12621290        $html .= '</select>';
    1263 
    1264         $html .= '<span class="lcs-help-inline"> </span> ';
     1291        $html .= '</div>';
     1292
     1293        $html .= '<div class="lcs-card__field" data-details="nth_weekday_month" style="' . esc_attr( $nth_style ) . '">';
     1294        $html .= '<label>' . esc_html( $this->is_en ? 'Weekday' : 'Wochentag' ) . '</label>';
    12651295        $html .= $this->weekday_select( $name('weekday'), $weekday );
    1266         $html .= '</span>';
    1267 
    1268         // Common: prefix + time (single fields, avoids duplicate POST values)
    1269         $html .= '<span class="lcs-inline-inputs lcs-rec-common" style="' . ( $type !== '' ? 'display: inline-flex;' : 'display: none;' ) . '">';
    1270         $html .= '<span class="lcs-sep"> </span>';
     1296        $html .= '</div>';
     1297
     1298        // Yearly: Day + Month
     1299        $yearly_day = (int) ( $r['yearly_day'] ?? 1 );
     1300        $yearly_month = (int) ( $r['yearly_month'] ?? 1 );
     1301        if ( $yearly_day < 1 || $yearly_day > 31 ) { $yearly_day = 1; }
     1302        if ( $yearly_month < 1 || $yearly_month > 12 ) { $yearly_month = 1; }
     1303
     1304        $html .= '<div class="lcs-card__field" data-details="yearly" style="' . esc_attr( $yearly_style ) . '">';
     1305        $html .= '<label>' . esc_html( $this->is_en ? 'Day' : 'Tag' ) . '</label>';
     1306        $html .= $this->day_of_month_select( $name('yearly_day'), $yearly_day );
     1307        $html .= '</div>';
     1308
     1309        $html .= '<div class="lcs-card__field" data-details="yearly" style="' . esc_attr( $yearly_style ) . '">';
     1310        $html .= '<label>' . esc_html( $this->is_en ? 'Month' : 'Monat' ) . '</label>';
     1311        $html .= '<select class="lcs-select" name="' . esc_attr( $name('yearly_month') ) . '">';
     1312        $months = $this->is_en
     1313            ? [ 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December' ]
     1314            : [ 1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April', 5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August', 9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember' ];
     1315        foreach ( $months as $m => $label ) {
     1316            $html .= '<option value="' . esc_attr( (string) $m ) . '" ' . selected( $yearly_month, $m, false ) . '>' . esc_html( $label ) . '</option>';
     1317        }
     1318        $html .= '</select>';
     1319        $html .= '</div>';
     1320
     1321        // Time fields in Row 1 (visible when type is selected)
     1322        $html .= '<div class="lcs-card__field lcs-rec-common" data-rec-time style="' . ( $type !== '' ? '' : 'display: none;' ) . '">';
     1323        $html .= '<label>' . esc_html( $this->is_en ? 'Prefix 1' : 'Präfix 1' ) . '</label>';
    12711324        $html .= $this->prefix_select( $name('prefix'), $prefix );
    1272         $html .= '<span class="lcs-help-inline"> </span> ';
     1325        $html .= '</div>';
     1326
     1327        $html .= '<div class="lcs-card__field lcs-rec-common" data-rec-time style="' . ( $type !== '' ? '' : 'display: none;' ) . '">';
     1328        $html .= '<label>' . esc_html( $this->is_en ? 'Time from' : 'Zeit von' ) . '</label>';
    12731329        $html .= '<input class="lcs-input" type="time" name="' . esc_attr( $name('time') ) . '" value="' . esc_attr( $time ) . '">';
    1274 
    1275         // Optional event text for this recurring rule (shown in shortcode output when text="true")
    1276         $html .= '<input class="lcs-input" type="text" name="' . esc_attr( $name('text') ) . '" value="' . esc_attr( $text ) . '" placeholder="' . esc_attr( $this->is_en ? 'Event text (optional)' : 'Eventtext (optional)' ) . '">';
    1277         $html .= '</span>';
    1278 
    1279         $html .= '</td>';
    1280 
    1281         // Period
    1282         $html .= '<td>';
    1283         $html .= '<span class="lcs-inline-inputs">';
    1284         $html .= '<span class="lcs-help-inline">' . esc_html( $this->is_en ? 'from' : 'von' ) . '</span> <input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="' . esc_attr( $name('from') ) . '" value="' . esc_attr( $from ) . '">';
    1285         $html .= '<span class="lcs-help-inline">' . esc_html( $this->is_en ? 'to' : 'bis' ) . '</span> <input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="' . esc_attr( $name('to') ) . '" value="' . esc_attr( $to ) . '">';
    1286         $html .= '</span>';
    1287         $html .= '</td>';
    1288 
    1289         // Action
    1290         $html .= '<td><div class="lcs-row-actions">';
    1291         $html .= '<a class="lcs-copy-btn lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove row' : 'Zeile entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>';
    1292         $html .= '</div></td>';
    1293 
    1294         $html .= '</tr>';
     1330        $html .= '</div>';
     1331
     1332        $html .= '<div class="lcs-card__field lcs-rec-common" data-rec-time style="' . ( $type !== '' ? '' : 'display: none;' ) . '">';
     1333        $html .= '<label>' . esc_html( $this->is_en ? 'Prefix 2' : 'Präfix 2' ) . '</label>';
     1334        $html .= $this->prefix2_select( $name('prefix2'), $prefix2 );
     1335        $html .= '</div>';
     1336
     1337        $html .= '<div class="lcs-card__field lcs-rec-common" data-rec-time style="' . ( $type !== '' ? '' : 'display: none;' ) . '">';
     1338        $html .= '<label>' . esc_html( $this->is_en ? 'Time to' : 'Zeit bis' ) . '</label>';
     1339        $html .= '<input class="lcs-input" type="time" name="' . esc_attr( $name('time_to') ) . '" value="' . esc_attr( $time_to ) . '">';
     1340        $html .= '</div>';
     1341
     1342        $html .= '</div>'; // End Row 1
     1343
     1344        // Row 2: Event text, Valid from, Valid to, Action
     1345        $html .= '<div class="lcs-card__row lcs-rec-common" style="' . ( $type !== '' ? '' : 'display: none;' ) . '">';
     1346
     1347        $html .= '<div class="lcs-card__field lcs-card__field--wide">';
     1348        $html .= '<label>' . esc_html( $this->is_en ? 'Event text' : 'Eventtext' ) . '</label>';
     1349        $html .= '<input class="lcs-input" type="text" name="' . esc_attr( $name('text') ) . '" value="' . esc_attr( $text ) . '" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '">';
     1350        $html .= '</div>';
     1351
     1352        $html .= '<div class="lcs-card__field">';
     1353        $html .= '<label>' . esc_html( $this->is_en ? 'Valid from' : 'Gültig von' ) . '</label>';
     1354        $html .= '<input class="lcs-input" type="date" name="' . esc_attr( $name('from') ) . '" value="' . esc_attr( $from ) . '">';
     1355        $html .= '</div>';
     1356
     1357        $html .= '<div class="lcs-card__field">';
     1358        $html .= '<label>' . esc_html( $this->is_en ? 'Valid to' : 'Gültig bis' ) . '</label>';
     1359        $html .= '<input class="lcs-input" type="date" name="' . esc_attr( $name('to') ) . '" value="' . esc_attr( $to ) . '">';
     1360        $html .= '</div>';
     1361
     1362        $html .= '<div class="lcs-card__actions">';
     1363        $html .= '<a class="lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove' : 'Entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>';
     1364        $html .= '</div>';
     1365
     1366        $html .= '</div>'; // End Row 2
     1367
     1368        $html .= '</div>'; // End Card
    12951369
    12961370        return $html;
     
    13481422    }
    13491423    private function render_panel_event_offset( $entry, $is_new ) {
    1350         $offset = isset( $entry['event_offset'] ) && is_array( $entry['event_offset'] ) ? $entry['event_offset'] : [ 'enabled' => 0, 'days_before' => 0, 'start_time' => '00:01', 'days_after' => 0, 'end_time' => '' ];
    1351         $enabled = ! empty( $offset['enabled'] );
    1352         $days_before = (int) ( $offset['days_before'] ?? ( $offset['days'] ?? 0 ) ); // backward compat
    1353         $days_before = max( 0, $days_before );
    1354         $start_time = (string) ( $offset['start_time'] ?? '00:01' );
    1355         $days_after = (int) ( $offset['days_after'] ?? 0 );
    1356         $days_after = max( 0, $days_after );
    1357         $end_time = (string) ( $offset['end_time'] ?? '' );
     1424        $event_classes = isset( $entry['event_classes'] ) && is_array( $entry['event_classes'] ) ? $entry['event_classes'] : [];
     1425        $slug = (string) ( $entry['slug'] ?? '' );
     1426       
     1427        if ( empty( $event_classes ) ) {
     1428            $event_classes = [[ 'name' => 'standard', 'days_before' => 0, 'start_time' => '', 'days_after' => 0, 'end_time' => '' ]];
     1429        }
    13581430
    13591431        $next = $this->compute_next_event( $entry, time() );
     
    13611433        echo '<div class="lcs-tab-content" data-panel="event-offset">';
    13621434        $this->render_panel_title( $this->panel_title_text( 'event-offset', $entry, $is_new ) );
    1363         echo '<p class="lcs-paragraph lcs-text">' . ( $this->is_en ? 'Show content X days <strong>before</strong> the calculated event – starting at a fixed start time. The offset affects only the visibility window, not the text output of the shortcode.' : 'Zeige Content X Tage <strong>vor</strong> dem berechneten Event – ab einer festen Startzeit. Der Offset beeinflusst nur das Sichtbarkeitsfenster, nicht die Text-Ausgabe im Shortcode.' ) . '</p>';
    1364 
    1365         echo '<table class="lcs-form-table">';
    1366 
    1367         echo '<tr><th>' . esc_html( $this->is_en ? 'Enable event offset' : 'Event-Offset aktivieren' ) . '</th><td>';
    1368         echo '<div class="lcs-checkbox-wrapper"><input type="checkbox" name="luzid_cs_offset_enabled" value="1" ' . checked( $enabled, true, false ) . '><span>aktiv</span></div>';
    1369         echo '</td></tr>';
    1370 
    1371         echo '<tr><th>' . esc_html( $this->is_en ? 'Days before / start time' : 'Tage vorher / Startzeit' ) . '</th><td>';
    1372         echo '<div class="lcs-inline-inputs">';
    1373         echo '<input type="number" min="0" max="365" name="luzid_cs_offset_days_before" value="' . esc_attr( (string) $days_before ) . '" style="max-width:120px;">';
    1374         echo '<input type="time" name="luzid_cs_offset_start_time" value="' . esc_attr( $start_time ) . '" style="max-width:160px;">';
     1435       
     1436        $desc_de = 'Definiere über Klassen die Sichtbarkeit von Content | Popups | Alerts | Menüeinträgen. Bestimme das Zeitfenster der Sichtbarkeit vor und nach dem Event über Offsets. Die Klassen beeinflussen nur das Sichtbarkeitsfenster, nicht die Text-Ausgabe im Shortcode.';
     1437        $desc_en = 'Define classes to control visibility of Content | Popups | Alerts | Menu items. Set the visibility time window before and after the event using offsets. Classes only affect the visibility window, not the text output of the shortcode.';
     1438        echo '<p class="lcs-paragraph lcs-text">' . esc_html( $this->is_en ? $desc_en : $desc_de ) . '</p>';
     1439
     1440        echo '<div class="lcs-repeater" data-lcs-repeater="event_classes">';
     1441       
     1442        foreach ( $event_classes as $i => $cls ) {
     1443            echo $this->render_event_class_card( $i, $cls, $slug, $next, false ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     1444        }
     1445       
     1446        echo '<template data-lcs-template>';
     1447        echo $this->render_event_class_card( '__i__', [ 'name' => '', 'days_before' => 0, 'start_time' => '', 'days_after' => 0, 'end_time' => '' ], $slug, $next, true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     1448        echo '</template>';
     1449
     1450        echo '<div class="lcs-actions">';
     1451        echo '<button type="button" class="lcs-button-small lcs-btn-icon" data-lcs-add><span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span><span>' . esc_html( $this->is_en ? 'Add class' : 'Klasse hinzufügen' ) . '</span></button>';
    13751452        echo '</div>';
    1376         echo '<div class="lcs-description">' . ( $this->is_en ? 'Start of visibility: <strong>(event date minus days before)</strong> from start time.' : 'Start der Sichtbarkeit: <strong>(Event-Datum minus Tage vorher)</strong> ab Startzeit.' ) . '</div>';
    1377         echo '</td></tr>';
    1378 
    1379         echo '<tr><th>' . esc_html( $this->is_en ? 'Days after / end time' : 'Tage nachher / Endzeit' ) . '</th><td>';
    1380         echo '<div class="lcs-inline-inputs">';
    1381         echo '<input type="number" min="0" max="365" name="luzid_cs_offset_days_after" value="' . esc_attr( (string) $days_after ) . '" style="max-width:120px;">';
    1382         echo '<input type="time" name="luzid_cs_offset_end_time" value="' . esc_attr( $end_time ) . '" style="max-width:160px;" placeholder="">';
     1453
    13831454        echo '</div>';
    1384         echo '<div class="lcs-description">' . ( $this->is_en ? 'End of visibility: <strong>(event date plus days after)</strong> at end time. If empty, visibility ends at the event time (for event ranges: the last day).' : 'Ende der Sichtbarkeit: <strong>(Event-Datum plus Tage nachher)</strong> zur Endzeit. Wenn Endzeit leer ist, gilt der Event-Zeitpunkt (bei Zeiträumen: der letzte Tag).' ) . '</div>';
    1385         echo '</td></tr>';
    1386 
    1387         echo '<tr><th>' . esc_html( $this->is_en ? 'Next event preview' : 'Vorschau nächster Event' ) . '</th><td>';
    1388         if ( $next && ! empty( $next['ts'] ) ) {
    1389             echo '<strong>' . esc_html( $this->format_event_datetime( $next['ts'], $next['prefix'], $next['time'], 'long', $next ) ) . '</strong>';
    1390 
    1391             $use_offset = $enabled && ( $days_before > 0 || $days_after > 0 || $end_time !== '' );
    1392             if ( $use_offset ) {
    1393                 $window = $this->compute_offset_window( $entry, $next );
    1394                 if ( $window ) {
    1395                     $tz = wp_timezone();
    1396                     echo '<div class="lcs-description">' . esc_html( $this->is_en ? 'Visible from:' : 'Sichtbar von:' ) . ' ' . esc_html( $this->wp_date_lang( 'd.m.Y H:i', (int) $window['start'], $tz ) ) . ' – ' . esc_html( $this->is_en ? 'until:' : 'bis:' ) . ' ' . esc_html( $this->wp_date_lang( 'd.m.Y H:i', (int) $window['end'], $tz ) ) . '</div>';
    1397                 }
    1398             }
     1455       
     1456        echo '<div class="lcs-callout" style="margin-top: 20px;">';
     1457        echo '<p><strong>' . esc_html( $this->is_en ? 'Start of visibility:' : 'Start der Sichtbarkeit:' ) . '</strong> ';
     1458        echo esc_html( $this->is_en ? '(Event date minus days before) from start time. If start time is empty, visibility starts at 00:00.' : '(Event-Datum minus Tage vorher) ab Startzeit. Wenn Startzeit leer ist, beginnt die Sichtbarkeit um 00:00 Uhr.' ) . '</p>';
     1459        echo '<p><strong>' . esc_html( $this->is_en ? 'End of visibility:' : 'Ende der Sichtbarkeit:' ) . '</strong> ';
     1460        echo esc_html( $this->is_en ? '(Event date plus days after) at end time. If end time is empty, visibility ends at the event time (for date ranges: the last day).' : '(Event-Datum plus Tage nachher) zur Endzeit. Wenn Endzeit leer ist, gilt der Event-Zeitpunkt (bei Zeiträumen: der letzte Tag).' ) . '</p>';
     1461        echo '</div>';
     1462
     1463        echo '</div>';
     1464    }
     1465
     1466    private function render_event_class_card( $i, $cls, $scheduler_slug, $next_event, $is_template = false ) {
     1467        $idx = $i;
     1468        $name = function( $field ) use ( $idx ) { return 'luzid_cs_event_classes[' . $idx . '][' . $field . ']'; };
     1469
     1470        $class_name = (string) ( $cls['name'] ?? '' );
     1471        $days_before = (int) ( $cls['days_before'] ?? 0 );
     1472        $start_time = (string) ( $cls['start_time'] ?? '' );
     1473        $days_after = (int) ( $cls['days_after'] ?? 0 );
     1474        $end_time = (string) ( $cls['end_time'] ?? '' );
     1475       
     1476        $is_standard = ( $class_name === 'standard' || ( $i === 0 && ! $is_template ) );
     1477        $css_class = $is_standard ? 'luzid-cs-' . $scheduler_slug : 'luzid-cs-' . $scheduler_slug . '-' . $class_name;
     1478
     1479        $html = '<div class="lcs-card" data-lcs-row>';
     1480        $html .= '<div class="lcs-card__row">';
     1481       
     1482        // Name field
     1483        $html .= '<div class="lcs-card__field lcs-card__field--grow">';
     1484        $html .= '<label>' . esc_html( $this->is_en ? 'Name' : 'Name' ) . '</label>';
     1485        if ( $is_standard && ! $is_template ) {
     1486            $html .= '<input type="text" class="lcs-input" value="Standard" disabled style="background: #f0f0f0;">';
     1487            $html .= '<input type="hidden" name="' . esc_attr( $name('name') ) . '" value="standard">';
     1488            $html .= '<input type="hidden" name="' . esc_attr( $name('is_standard') ) . '" value="1">';
    13991489        } else {
    1400             echo '<span class="lcs-description">' . esc_html( $this->tr( 'no_date_available' ) ) . '</span>';
    1401         }
    1402         echo '</td></tr>';
    1403 
    1404         echo '</table>';
    1405 
    1406         echo '</div>';
     1490            $html .= '<input type="text" class="lcs-input lcs-event-class-name" name="' . esc_attr( $name('name') ) . '" value="' . esc_attr( $class_name ) . '" placeholder="' . esc_attr( $this->is_en ? 'e.g. popup, banner, menu' : 'z.B. popup, banner, menu' ) . '">';
     1491        }
     1492        $html .= '</div>';
     1493       
     1494        // CSS Class preview
     1495        $html .= '<div class="lcs-card__field lcs-card__field--grow">';
     1496        $html .= '<label>' . esc_html( $this->is_en ? 'CSS Class' : 'CSS-Klasse' ) . '</label>';
     1497        $html .= '<code class="lcs-event-class-preview" style="display: inline-block; padding: 6px 10px; background: #f5f5f5; border-radius: 4px;">.' . esc_html( $css_class ) . '</code>';
     1498        $html .= '</div>';
     1499       
     1500        // Action buttons: Copy + Delete
     1501        $html .= '<div class="lcs-card__field lcs-card__field--actions">';
     1502        $html .= '<div class="lcs-row-actions">';
     1503        // Copy button (always visible)
     1504        $html .= '<button type="button" class="lcs-button-secondary lcs-copy-btn lcs-btn-icon" data-lcs-copy="' . esc_attr( $css_class ) . '" title="' . esc_attr( $this->is_en ? 'Copy CSS class' : 'CSS-Klasse kopieren' ) . '"><span class="dashicons dashicons-admin-page"></span></button>';
     1505        // Delete button (not for standard, unless template)
     1506        if ( ! $is_standard || $is_template ) {
     1507            $html .= '<a class="lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove' : 'Entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>';
     1508        }
     1509        $html .= '</div>';
     1510        $html .= '</div>';
     1511       
     1512        $html .= '</div>';
     1513
     1514        // Row 2: Days before + Start time + Days after + End time
     1515        $html .= '<div class="lcs-card__row">';
     1516       
     1517        $html .= '<div class="lcs-card__field">';
     1518        $html .= '<label>' . esc_html( $this->is_en ? 'Days before' : 'Tage vorher' ) . '</label>';
     1519        $html .= '<input type="number" class="lcs-input" name="' . esc_attr( $name('days_before') ) . '" value="' . esc_attr( (string) $days_before ) . '" min="0" max="365" style="max-width: 80px;">';
     1520        $html .= '</div>';
     1521       
     1522        $html .= '<div class="lcs-card__field">';
     1523        $html .= '<label>' . esc_html( $this->is_en ? 'Start time' : 'Startzeit' ) . '</label>';
     1524        $html .= '<input type="time" class="lcs-input" name="' . esc_attr( $name('start_time') ) . '" value="' . esc_attr( $start_time ) . '" style="max-width: 120px;">';
     1525        $html .= '</div>';
     1526       
     1527        $html .= '<div class="lcs-card__field">';
     1528        $html .= '<label>' . esc_html( $this->is_en ? 'Days after' : 'Tage nachher' ) . '</label>';
     1529        $html .= '<input type="number" class="lcs-input" name="' . esc_attr( $name('days_after') ) . '" value="' . esc_attr( (string) $days_after ) . '" min="0" max="365" style="max-width: 80px;">';
     1530        $html .= '</div>';
     1531       
     1532        $html .= '<div class="lcs-card__field">';
     1533        $html .= '<label>' . esc_html( $this->is_en ? 'End time' : 'Endzeit' ) . '</label>';
     1534        $html .= '<input type="time" class="lcs-input" name="' . esc_attr( $name('end_time') ) . '" value="' . esc_attr( $end_time ) . '" style="max-width: 120px;">';
     1535        $html .= '</div>';
     1536       
     1537        $html .= '</div>';
     1538
     1539        // Row 3: Preview
     1540        if ( $next_event && ! empty( $next_event['ts'] ) && ! $is_template ) {
     1541            $window = $this->compute_class_offset_window( $cls, $next_event );
     1542            if ( $window ) {
     1543                $tz = wp_timezone();
     1544                $html .= '<div class="lcs-card__row lcs-card__row--preview">';
     1545                $html .= '<div class="lcs-card__field lcs-card__field--full">';
     1546                $html .= '<span class="lcs-preview-label">' . esc_html( $this->is_en ? 'Preview:' : 'Vorschau:' ) . '</span> ';
     1547                $html .= '<span class="lcs-preview-text">';
     1548                $html .= esc_html( $this->is_en ? 'Visible from' : 'Sichtbar von' ) . ' ';
     1549                $html .= '<strong>' . esc_html( $this->wp_date_lang( 'd.m.Y H:i', (int) $window['start'], $tz ) ) . '</strong>';
     1550                $html .= ' ' . esc_html( $this->is_en ? 'until' : 'bis' ) . ' ';
     1551                $html .= '<strong>' . esc_html( $this->wp_date_lang( 'd.m.Y H:i', (int) $window['end'], $tz ) ) . '</strong>';
     1552                $html .= '</span>';
     1553                $html .= '</div>';
     1554                $html .= '</div>';
     1555            }
     1556        }
     1557
     1558        $html .= '</div>';
     1559
     1560        return $html;
     1561    }
     1562
     1563    private function compute_class_offset_window( $cls, $next_event ) {
     1564        if ( empty( $next_event['ts'] ) ) return null;
     1565
     1566        $days_before = max( 0, (int) ( $cls['days_before'] ?? 0 ) );
     1567        $start_time = (string) ( $cls['start_time'] ?? '' );
     1568        if ( $start_time !== '' && ! preg_match( '/^\d{2}:\d{2}$/', $start_time ) ) $start_time = '';
     1569
     1570        $days_after = max( 0, (int) ( $cls['days_after'] ?? 0 ) );
     1571        $end_time = (string) ( $cls['end_time'] ?? '' );
     1572        if ( $end_time !== '' && ! preg_match( '/^\d{2}:\d{2}$/', $end_time ) ) { $end_time = ''; }
     1573
     1574        $tz = wp_timezone();
     1575        $event_dt = (new DateTime('@' . (int) $next_event['ts']))->setTimezone( $tz );
     1576
     1577        if ( $days_before > 0 ) {
     1578            $start_dt = clone $event_dt;
     1579            $start_dt->modify( '-' . $days_before . ' days' );
     1580            if ( $start_time !== '' ) {
     1581                $start_dt->setTime( (int) substr( $start_time, 0, 2 ), (int) substr( $start_time, 3, 2 ), 0 );
     1582            } else {
     1583                $start_dt->setTime( 0, 0, 0 );
     1584            }
     1585            $start_ts = $start_dt->getTimestamp();
     1586        } else {
     1587            if ( $start_time !== '' ) {
     1588                $start_dt = clone $event_dt;
     1589                $start_dt->setTime( (int) substr( $start_time, 0, 2 ), (int) substr( $start_time, 3, 2 ), 0 );
     1590                $start_ts = $start_dt->getTimestamp();
     1591            } else {
     1592                $start_ts = (int) $next_event['ts'];
     1593            }
     1594        }
     1595
     1596        $base_end_ts = ! empty( $next_event['end_ts'] ) ? (int) $next_event['end_ts'] : (int) $next_event['ts'];
     1597        $end_ts = $base_end_ts;
     1598
     1599        if ( $days_after > 0 ) {
     1600            $end_ts = $base_end_ts + ( $days_after * DAY_IN_SECONDS );
     1601        }
     1602
     1603        if ( $end_time !== '' ) {
     1604            $base_end_dt = (new DateTime('@' . (int) $base_end_ts ))->setTimezone( $tz );
     1605            if ( $days_after > 0 ) {
     1606                $base_end_dt->modify( '+' . $days_after . ' days' );
     1607            }
     1608            $base_end_dt->setTime( (int) substr( $end_time, 0, 2 ), (int) substr( $end_time, 3, 2 ), 0 );
     1609            $end_ts = $base_end_dt->getTimestamp();
     1610        }
     1611
     1612        return [ 'start' => (int) $start_ts, 'end' => (int) $end_ts ];
    14071613    }
    14081614
     
    14301636            echo '<div style="margin-top:15px;">';
    14311637            echo '<table class="lcs-table lcs-table--compact">';
    1432             echo '<thead><tr><th>' . esc_html( $this->is_en ? 'Day or period' : 'Tag oder Zeitraum' ) . '</th><th>' . esc_html( $this->is_en ? 'Show from' : 'Einblenden am' ) . '</th><th>' . esc_html( $this->is_en ? 'Text (long)' : 'Text (long)' ) . '</th><th>' . esc_html( $this->is_en ? 'Source' : 'Quelle' ) . '</th></tr></thead>';
     1638            echo '<thead><tr><th>' . esc_html( $this->is_en ? 'Day or period' : 'Tag oder Zeitraum' ) . '</th><th>' . esc_html( $this->is_en ? 'Show from' : 'Einblenden am' ) . '</th><th>' . esc_html( $this->is_en ? 'Text (long)' : 'Text (long)' ) . '</th><th>' . esc_html( $this->is_en ? 'Event text (opt)' : 'Eventtext (opt)' ) . '</th></tr></thead>';
    14331639            echo '<tbody>';
    14341640
     
    14921698                            $long_txt = $this->format_event_datetime( $ts, $prefix, $time, 'long', $ev );
    14931699
    1494 $src = (string) ( $ev['source'] ?? ( $ev['type'] ?? '' ) );
    1495 $rule_type = (string) ( $ev['rule_type'] ?? '' );
    1496 if ( $src === 'recurring' ) {
    1497     $src_txt = $this->is_en ? 'Recurring' : 'Wiederholung';
    1498     if ( $rule_type !== '' ) {
    1499         $src_txt .= ' (' . $rule_type . ')';
    1500     }
    1501 } else {
    1502     $src_txt = $this->is_en ? 'Single' : 'Termin';
    1503 }
     1700// Eventtext (optional) - aus dem Event-Datensatz
     1701$event_text = (string) ( $ev['text'] ?? '' );
    15041702
    15051703
     
    15091707                echo '<td>' . esc_html( $show_txt ) . '</td>';
    15101708                echo '<td>' . esc_html( $long_txt ) . '</td>';
    1511                 echo '<td>' . esc_html( $src_txt ) . '</td>';
     1709                echo '<td>' . esc_html( $event_text ) . '</td>';
    15121710                echo '</tr>';
    15131711            }
     
    15391737    private function prefix_select( $name, $current ) {
    15401738        $current = (string) $current;
     1739        // Präfix 1: keins | ab | bis | um | von | Ab | Bis | Um | Von
    15411740        $opts = [
    1542             '' => '—',
    1543             'ab' => ( $this->is_en ? 'from' : 'ab' ),
    1544             'bis' => ( $this->is_en ? 'until' : 'bis' ),
    1545             'um' => ( $this->is_en ? 'at' : 'um' ),
     1741            '' => ( $this->is_en ? 'none' : 'keins' ),
     1742            'ab' => 'ab',
     1743            'bis' => 'bis',
     1744            'um' => 'um',
     1745            'von' => 'von',
     1746            'Ab' => 'Ab',
     1747            'Bis' => 'Bis',
     1748            'Um' => 'Um',
     1749            'Von' => 'Von',
    15461750        ];
     1751        if ( $this->is_en ) {
     1752            $opts = [
     1753                '' => 'none',
     1754                'from' => 'from',
     1755                'until' => 'until',
     1756                'at' => 'at',
     1757                'From' => 'From',
     1758                'Until' => 'Until',
     1759                'At' => 'At',
     1760            ];
     1761        }
     1762        $html = '<select class="lcs-select" name="' . esc_attr( $name ) . '">';
     1763        foreach ( $opts as $val => $label ) {
     1764            $html .= '<option value="' . esc_attr( $val ) . '" ' . selected( $current, $val, false ) . '>' . esc_html( $label ) . '</option>';
     1765        }
     1766        $html .= '</select>';
     1767        return $html;
     1768    }
     1769
     1770    private function prefix2_select( $name, $current ) {
     1771        $current = (string) $current;
     1772        // Präfix 2: keins | bis | Bis | – (Bindestrich)
     1773        $opts = [
     1774            '' => ( $this->is_en ? 'none' : 'keins' ),
     1775            'bis' => 'bis',
     1776            'Bis' => 'Bis',
     1777            'dash' => '– (' . ( $this->is_en ? 'dash' : 'Strich' ) . ')',
     1778        ];
     1779        if ( $this->is_en ) {
     1780            $opts = [
     1781                '' => 'none',
     1782                'to' => 'to',
     1783                'To' => 'To',
     1784                'dash' => '– (dash)',
     1785            ];
     1786        }
    15471787        $html = '<select class="lcs-select" name="' . esc_attr( $name ) . '">';
    15481788        foreach ( $opts as $val => $label ) {
     
    16381878
    16391879            $cls_new = 'luzid-cs-' . $slug;
     1880            $cls_old = 'luzid-cs-' . $slug; // legacy support (pre-1.2.2)
    16401881            $is_active = $this->is_entry_active_now( $e, $now );
    16411882
    16421883            // Set element visibility directly (independent of body_class()).
    1643                 $css .= '.' . $cls_new . '{display:' . ( $is_active ? 'block' : 'none' ) . '!important;}' . "\n";
     1884                $css .= '.' . $cls_new . ',.' . $cls_old . '{display:' . ( $is_active ? 'block' : 'none' ) . '!important;}' . "\n";
    16441885
    16451886            // Provide a body class hint for themes/custom CSS (optional).
     
    16681909            if ( $this->is_entry_active_now( $e, $now ) ) {
    16691910                $classes[] = 'luzid-cs-active-' . $slug;
     1911                $classes[] = 'luzid-cs-active-' . $slug; // legacy support
    16701912            }
    16711913        }
     
    19652207            $prefix = isset( $r['prefix'] ) ? (string) $r['prefix'] : '';
    19662208            $time = isset( $r['time'] ) ? (string) $r['time'] : '';
     2209            $prefix2 = isset( $r['prefix2'] ) ? (string) $r['prefix2'] : '';
     2210            $time_to = isset( $r['time_to'] ) ? (string) $r['time_to'] : '';
    19672211            $text = isset( $r['text'] ) ? (string) $r['text'] : '';
    19682212
     
    19762220
    19772221            $cand = [
    1978                 'ts'     => (int) $ts,
    1979                 'type'   => 'single',
    1980                 'source' => 'single',
    1981                 'prefix' => $prefix,
    1982                 'time'   => $time,
    1983                 'text'   => $text,
    1984                 'from'   => $from,
     2222                'ts'      => (int) $ts,
     2223                'type'    => 'single',
     2224                'source'  => 'single',
     2225                'prefix'  => $prefix,
     2226                'time'    => $time,
     2227                'prefix2' => $prefix2,
     2228                'time_to' => $time_to,
     2229                'text'    => $text,
     2230                'from'    => $from,
    19852231            ];
    19862232
     
    20252271                    $time = isset( $row['time'] ) ? (string) $row['time'] : '';
    20262272                    $prefix = isset( $row['prefix'] ) ? (string) $row['prefix'] : '';
     2273                    $prefix2 = isset( $row['prefix2'] ) ? (string) $row['prefix2'] : '';
     2274                    $time_to = isset( $row['time_to'] ) ? (string) $row['time_to'] : '';
    20272275                    $text = isset( $row['text'] ) ? (string) $row['text'] : '';
    20282276
     
    20302278                    if ( $ts !== null ) {
    20312279                        $cand = [
    2032                             'ts'     => (int) $ts,
    2033                             'type'   => 'single',
    2034                 'source' => 'single',
    2035                             'prefix' => $prefix,
    2036                             'time'   => $time,
    2037                             'text'   => $text,
     2280                            'ts'      => (int) $ts,
     2281                            'type'    => 'single',
     2282                'source'  => 'single',
     2283                            'prefix'  => $prefix,
     2284                            'time'    => $time,
     2285                            'prefix2' => $prefix2,
     2286                            'time_to' => $time_to,
     2287                            'text'    => $text,
    20382288                        ];
    20392289
     
    22182468                if ( $ts === null ) continue;
    22192469                if ( $ts < $after_ts ) continue;
    2220                 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text ];
     2470                return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text, 'prefix2' => (string) ( $rule['prefix2'] ?? '' ), 'time_to' => (string) ( $rule['time_to'] ?? '' ) ];
    22212471            }
    22222472            return null;
     
    22382488                if ( $ts === null ) continue;
    22392489                if ( $ts < $after_ts ) continue;
    2240                 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text ];
     2490                return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text, 'prefix2' => (string) ( $rule['prefix2'] ?? '' ), 'time_to' => (string) ( $rule['time_to'] ?? '' ) ];
    22412491            }
    22422492            return null;
     
    22582508                if ( $ts === null ) continue;
    22592509                if ( $ts < $after_ts ) continue;
    2260                 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text ];
     2510                return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text, 'prefix2' => (string) ( $rule['prefix2'] ?? '' ), 'time_to' => (string) ( $rule['time_to'] ?? '' ) ];
     2511            }
     2512            return null;
     2513        }
     2514
     2515        if ( $type === 'yearly' ) {
     2516            $yearly_day = (int) ( $rule['yearly_day'] ?? 0 );
     2517            $yearly_month = (int) ( $rule['yearly_month'] ?? 0 );
     2518            if ( $yearly_day < 1 || $yearly_day > 31 ) return null;
     2519            if ( $yearly_month < 1 || $yearly_month > 12 ) return null;
     2520           
     2521            $start_year = (int) $after->format('Y');
     2522            for ( $y = 0; $y < 10; $y++ ) {
     2523                $year = $start_year + $y;
     2524                $month_str = str_pad( (string) $yearly_month, 2, '0', STR_PAD_LEFT );
     2525                $day_str = str_pad( (string) $yearly_day, 2, '0', STR_PAD_LEFT );
     2526                $date = $year . '-' . $month_str . '-' . $day_str;
     2527               
     2528                // Validate date (e.g., Feb 30 doesn't exist)
     2529                if ( ! checkdate( $yearly_month, $yearly_day, $year ) ) continue;
     2530                if ( ! $this->rule_period_allows( $rule, $date ) ) continue;
     2531                $ts = $this->ts_from_date_time( $date, $time );
     2532                if ( $ts === null ) continue;
     2533                if ( $ts < $after_ts ) continue;
     2534                return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text, 'prefix2' => (string) ( $rule['prefix2'] ?? '' ), 'time_to' => (string) ( $rule['time_to'] ?? '' ) ];
    22612535            }
    22622536            return null;
     
    23152589        $atts = shortcode_atts(
    23162590            [
    2317                 'slug' => '',
    2318                 'opt' => 'long',
    2319                 'count' => 10,
     2591                'slug'       => '',
     2592                'date'       => '',        // leer=kein Datum | short|medium|long|full
     2593                'time'       => '',        // leer=keine Zeit | auto|raw|prefix|range|range_long
     2594                'count'      => 10,
    23202595                'timeoffset' => 0,
    2321                 'lang' => '',
    2322                 'list' => 'false',
    2323                 'text' => 'false',
    2324                 'sep'  => '-',
     2596                'lang'       => '',
     2597                'list'       => 'false',
     2598                'text'       => 'false',
     2599                'sep1'       => ' ',       // zwischen Datum und Zeit
     2600                'sep2'       => ' ',       // zwischen Zeit und Text (oder Datum und Text wenn keine Zeit)
     2601                'sep3'       => '',        // zwischen Listenelementen (nur bei list="true")
    23252602            ],
    23262603            $atts,
     
    23292606
    23302607        $slug = sanitize_title( (string) $atts['slug'] );
    2331         $opt = sanitize_text_field( (string) $atts['opt'] );
     2608        $date_format = sanitize_text_field( (string) $atts['date'] );
     2609        $time_format = sanitize_text_field( (string) $atts['time'] );
    23322610        $count = (int) $atts['count'];
    23332611        $timeoffset = (int) $atts['timeoffset'];
    23342612        $lang = strtolower( sanitize_text_field( (string) $atts['lang'] ) );
    23352613
    2336         $list = strtolower( sanitize_text_field( (string) $atts['list'] ) );
    2337         $with_text = strtolower( sanitize_text_field( (string) $atts['text'] ) );
    2338         $sep = (string) $atts['sep'];
    2339         // Separator should preserve intentional leading/trailing spaces (e.g. " | ").
    2340         // So we only strip tags/control chars but do NOT trim.
    2341         $sep = html_entity_decode( $sep, ENT_QUOTES, 'UTF-8' );
    2342 
    2343         // Allow line breaks for separator if user passes <br> (or a newline char).
    2344         $sep_is_br = ( preg_match( '/<\s*br\s*\/?\s*>/i', $sep ) === 1 ) || ( strpos( $sep, "\n" ) !== false ) || ( strpos( $sep, "\r" ) !== false );
    2345         if ( $sep_is_br ) {
    2346             $sep = '<br />';
    2347         } else {
    2348             // Strip tags/control chars, but do NOT trim.
    2349             $sep = wp_strip_all_tags( $sep, true );
    2350             $sep = str_replace( ["\n", "\r", "\t"], ' ', $sep );
    2351             if ( $sep === '' ) { $sep = '-'; }
    2352         }
    2353 
    2354 // WordPress shortcode parsing trims leading/trailing spaces inside quoted attributes.
    2355         // To still support sep=" | " we pad separators with single spaces by default,
    2356         // unless the separator already has edge whitespace or uses &nbsp; explicitly.
    2357         $sep_has_edge_ws = ( preg_match( '/^\s|\s$/u', $sep ) === 1 );
    2358         $sep_has_nbsp    = ( strpos( $sep, '&nbsp;' ) !== false );
    2359         if ( ! $sep_is_br && ! $sep_has_edge_ws && ! $sep_has_nbsp ) {
    2360             $sep = ' ' . $sep . ' ';
    2361         }
    2362 
    2363 
    2364         // Build safe HTML for separator (allow only <br> as HTML)
    2365         $sep_html = esc_html( $sep );
    2366         if ( preg_match( '/<\s*br\s*\/?\s*>/i', $sep ) || strpos( $sep, "\n" ) !== false || strpos( $sep, "\r" ) !== false ) {
    2367             $sep_html = '<br />';
    2368         }
    2369         $is_list = in_array( $list, [ '1', 'true', 'yes', 'on' ], true );
    2370         $show_text = in_array( $with_text, [ '1', 'true', 'yes', 'on' ], true );
    2371 
    2372         // Backward-compat: opt="list" behaves like list="true" + opt="long"
    2373         if ( $opt === 'list' ) {
    2374             $opt = 'long';
    2375             $is_list = true;
    2376         }
    2377 
    2378 
    2379         // Optional language override for shortcode output (de|en). Empty = current.
     2614        // If neither date nor time is set, default to date="long"
     2615        if ( $date_format === '' && $time_format === '' ) {
     2616            $date_format = 'long';
     2617        }
     2618
     2619        $is_list = in_array( strtolower( (string) $atts['list'] ), [ '1', 'true', 'yes', 'on' ], true );
     2620        $show_text = in_array( strtolower( (string) $atts['text'] ), [ '1', 'true', 'yes', 'on' ], true );
     2621       
     2622        // Process separators
     2623        $sep1_html = $this->process_separator( (string) $atts['sep1'] );
     2624        $sep2_html = $this->process_separator( (string) $atts['sep2'] );
     2625        $sep3_html = $this->process_separator( (string) $atts['sep3'], '' );
     2626
     2627        // Optional language override for shortcode output (de|en)
    23802628        $old_lang = $this->lang;
    23812629        $old_is_en = $this->is_en;
     
    23932641        $now = time();
    23942642
    2395         // List output: next N events in the selected format, separated by <br />
     2643        // List output: next N events
    23962644        if ( $is_list ) {
    23972645            $count = max( 1, min( 200, $count ) );
     
    24032651            }
    24042652
    2405             $lines = [];
     2653            $items = [];
    24062654            foreach ( $events as $ev ) {
    24072655                if ( empty( $ev['ts'] ) ) {
     
    24092657                }
    24102658                $ts = (int) $ev['ts'] + ( $timeoffset * 60 );
    2411 
    2412                 $opt_html  = esc_html( $this->format_event_datetime( $ts, $ev['prefix'] ?? '', $ev['time'] ?? '', $opt, $ev ) );
    2413                 $item_html = '<div class="luzid-cs-item"><span class="luzid-cs-opt">' . $opt_html . '</span>';
    2414 
    2415                 if ( $show_text && ! empty( $ev['text'] ) ) {
    2416                     $item_html .= '<span class="luzid-cs-sep">' . $sep_html . '</span><span class="luzid-cs-text">' . esc_html( (string) $ev['text'] ) . '</span>';
    2417                 }
    2418 
    2419                 $item_html .= '</div>';
    2420                 $lines[] = $item_html;
    2421             }
    2422 $this->lang = $old_lang;
     2659                $item_html = $this->build_event_output( $ts, $ev, $date_format, $time_format, $show_text, $sep1_html, $sep2_html );
     2660                $items[] = '<div class="luzid-cs-item">' . $item_html . '</div>';
     2661            }
     2662           
     2663            $this->lang = $old_lang;
    24232664            $this->is_en = $old_is_en;
    2424             return '<div class="luzid-cs luzid-cs--list">' . implode( "\n", $lines ) . '</div>';
    2425         }
    2426 
    2427 $next = $this->compute_next_event( $entry, $now );
     2665           
     2666            // Join items with sep3
     2667            $glue = $sep3_html !== '' ? $sep3_html : "\n";
     2668            return '<div class="luzid-cs luzid-cs--list">' . implode( $glue, $items ) . '</div>';
     2669        }
     2670
     2671        // Single event output
     2672        $next = $this->compute_next_event( $entry, $now );
    24282673        if ( ! $next || empty( $next['ts'] ) ) {
    24292674            $this->lang = $old_lang;
     
    24332678
    24342679        $ts = (int) $next['ts'] + ( $timeoffset * 60 );
    2435         $opt_html = esc_html( $this->format_event_datetime( $ts, $next['prefix'] ?? '', $next['time'] ?? '', $opt, $next ) );
    2436         $out = '<span class="luzid-cs luzid-cs--single"><span class="luzid-cs-opt">' . $opt_html . '</span>';
    2437 
    2438         if ( $show_text && ! empty( $next['text'] ) ) {
    2439             $out .= '<span class="luzid-cs-sep">' . $sep_html . '</span><span class="luzid-cs-text">' . esc_html( (string) $next['text'] ) . '</span>';
    2440         }
    2441 
    2442         $out .= '</span>';
     2680        $out = '<span class="luzid-cs luzid-cs--single">' . $this->build_event_output( $ts, $next, $date_format, $time_format, $show_text, $sep1_html, $sep2_html ) . '</span>';
    24432681
    24442682        $this->lang = $old_lang;
    24452683        $this->is_en = $old_is_en;
    24462684        return $out;
     2685    }
     2686
     2687    /**
     2688     * Process a separator value - handle <br>, spaces, etc.
     2689     * User gets exactly what they input - no auto-padding.
     2690     */
     2691    private function process_separator( $sep, $default = ' ' ) {
     2692        $sep = html_entity_decode( $sep, ENT_QUOTES, 'UTF-8' );
     2693       
     2694        // Check for <br> variants
     2695        if ( preg_match( '/<\s*br\s*\/?\s*>/i', $sep ) || strpos( $sep, "\n" ) !== false || strpos( $sep, "\r" ) !== false ) {
     2696            return '<br />';
     2697        }
     2698       
     2699        // Strip tags but preserve content
     2700        $sep = wp_strip_all_tags( $sep, true );
     2701        $sep = str_replace( ["\n", "\r", "\t"], ' ', $sep );
     2702       
     2703        // WordPress trims spaces from shortcode attributes.
     2704        // If the result is empty, use the default (typically a space).
     2705        if ( $sep === '' ) {
     2706            return esc_html( $default );
     2707        }
     2708       
     2709        return esc_html( $sep );
     2710    }
     2711
     2712    /**
     2713     * Build the HTML output for a single event.
     2714     */
     2715    private function build_event_output( $ts, $ev, $date_format, $time_format, $show_text, $sep1_html, $sep2_html ) {
     2716        $tz = wp_timezone();
     2717       
     2718        // Date part (only if date_format is set)
     2719        $date_out = '';
     2720        if ( $date_format !== '' ) {
     2721            $date_out = $this->format_date_part( $ts, $date_format, $tz );
     2722        }
     2723       
     2724        // Time part (only if time_format is set)
     2725        $time_out = '';
     2726        if ( $time_format !== '' ) {
     2727            $time_out = $this->format_time_part( $ts, $ev, $time_format, $tz );
     2728        }
     2729       
     2730        // Text part
     2731        $text_out = '';
     2732        if ( $show_text && ! empty( $ev['text'] ) ) {
     2733            $text_out = (string) $ev['text'];
     2734        }
     2735       
     2736        // Build output - only include parts that are set
     2737        $parts = [];
     2738       
     2739        // Date (if present)
     2740        if ( $date_out !== '' ) {
     2741            $parts[] = '<span class="luzid-cs-date">' . esc_html( $date_out ) . '</span>';
     2742        }
     2743       
     2744        // Time (if present)
     2745        if ( $time_out !== '' ) {
     2746            // Only add sep1 if we have both date and time
     2747            if ( $date_out !== '' ) {
     2748                $parts[] = '<span class="luzid-cs-sep luzid-cs-sep1">' . $sep1_html . '</span>';
     2749            }
     2750            $parts[] = '<span class="luzid-cs-time">' . esc_html( $time_out ) . '</span>';
     2751        }
     2752       
     2753        // Text (if present)
     2754        if ( $text_out !== '' ) {
     2755            // Only add sep2 if we have something before it
     2756            if ( ! empty( $parts ) ) {
     2757                $parts[] = '<span class="luzid-cs-sep luzid-cs-sep2">' . $sep2_html . '</span>';
     2758            }
     2759            $parts[] = '<span class="luzid-cs-text">' . esc_html( $text_out ) . '</span>';
     2760        }
     2761       
     2762        return implode( '', $parts );
     2763    }
     2764
     2765    /**
     2766     * Format just the date part.
     2767     */
     2768    private function format_date_part( $ts, $date_format, $tz ) {
     2769        switch ( $date_format ) {
     2770            case 'short':
     2771                return $this->wp_date_lang( 'd.m.Y', $ts, $tz );
     2772            case 'medium':
     2773                return $this->wp_date_lang( 'D, d.m.Y', $ts, $tz );
     2774            case 'long':
     2775                return $this->wp_date_lang( 'l, d.m.Y', $ts, $tz );
     2776            case 'full':
     2777                if ( $this->is_en ) {
     2778                    return $this->wp_date_lang( 'l, d F Y', $ts, $tz );
     2779                } else {
     2780                    return $this->wp_date_lang( 'l, d. F Y', $ts, $tz );
     2781                }
     2782            default:
     2783                return $this->wp_date_lang( 'l, d.m.Y', $ts, $tz );
     2784        }
     2785    }
     2786
     2787    /**
     2788     * Format just the time part.
     2789     */
     2790    private function format_time_part( $ts, $ev, $time_format, $tz ) {
     2791        $prefix = (string) ( $ev['prefix'] ?? '' );
     2792        $prefix2 = (string) ( $ev['prefix2'] ?? '' );
     2793        $time_hm = (string) ( $ev['time'] ?? '' );
     2794        $time_to_hm = (string) ( $ev['time_to'] ?? '' );
     2795       
     2796        $has_time = ( $time_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_hm ) );
     2797        $has_time_to = ( $time_to_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_to_hm ) );
     2798       
     2799        if ( ! $has_time ) {
     2800            return '';
     2801        }
     2802       
     2803        $time_from = $this->wp_date_lang( 'H:i', $ts, $tz );
     2804       
     2805        switch ( $time_format ) {
     2806            case 'raw':
     2807                return $time_from;
     2808               
     2809            case 'prefix':
     2810                $p = $this->format_prefix_output( $prefix );
     2811                $out = trim( $p . ' ' . $time_from );
     2812                if ( ! $this->is_en ) {
     2813                    $out .= ' Uhr';
     2814                }
     2815                return $out;
     2816               
     2817            case 'range':
     2818                if ( $has_time_to ) {
     2819                    $p2 = $this->format_prefix2_output( $prefix2 );
     2820                    return $time_from . ' ' . $p2 . ' ' . $time_to_hm;
     2821                }
     2822                return $time_from;
     2823               
     2824            case 'range_long':
     2825                $p = $this->format_prefix_output( $prefix );
     2826                if ( $has_time_to ) {
     2827                    $p2 = $this->format_prefix2_output( $prefix2 );
     2828                    $out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to_hm );
     2829                } else {
     2830                    $out = trim( $p . ' ' . $time_from );
     2831                }
     2832                if ( ! $this->is_en ) {
     2833                    $out .= ' Uhr';
     2834                }
     2835                return $out;
     2836               
     2837            case 'auto':
     2838            default:
     2839                $p = $this->format_prefix_output( $prefix );
     2840                if ( $has_time_to ) {
     2841                    $p2 = $this->format_prefix2_output( $prefix2 );
     2842                    $out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to_hm );
     2843                } else {
     2844                    $out = trim( $p . ' ' . $time_from );
     2845                }
     2846                if ( ! $this->is_en ) {
     2847                    $out .= ' Uhr';
     2848                }
     2849                return $out;
     2850        }
     2851    }
     2852
     2853    /**
     2854     * Format event with new parameter structure.
     2855     *
     2856     * @param int    $ts          Timestamp
     2857     * @param array  $event       Event data (prefix, prefix2, time, time_to)
     2858     * @param string $date_format short|medium|long|full
     2859     * @param string $time_format auto|raw|prefix|range|range_long
     2860     * @param string $sep_html    Separator between date and time
     2861     * @return string Formatted output
     2862     */
     2863    private function format_event_new( $ts, $event, $date_format, $time_format, $sep_html ) {
     2864        $tz = wp_timezone();
     2865       
     2866        $prefix = (string) ( $event['prefix'] ?? '' );
     2867        $prefix2 = (string) ( $event['prefix2'] ?? '' );
     2868        $time_hm = (string) ( $event['time'] ?? '' );
     2869        $time_to_hm = (string) ( $event['time_to'] ?? '' );
     2870       
     2871        // Date formatting
     2872        $date_out = '';
     2873        switch ( $date_format ) {
     2874            case 'short':
     2875                $date_out = $this->wp_date_lang( 'd.m.Y', $ts, $tz );
     2876                break;
     2877            case 'medium':
     2878                $date_out = $this->wp_date_lang( 'D, d.m.Y', $ts, $tz );
     2879                break;
     2880            case 'long':
     2881                $date_out = $this->wp_date_lang( 'l, d.m.Y', $ts, $tz );
     2882                break;
     2883            case 'full':
     2884                if ( $this->is_en ) {
     2885                    $date_out = $this->wp_date_lang( 'l, d F Y', $ts, $tz );
     2886                } else {
     2887                    $date_out = $this->wp_date_lang( 'l, d. F Y', $ts, $tz );
     2888                }
     2889                break;
     2890            default:
     2891                $date_out = $this->wp_date_lang( 'l, d.m.Y', $ts, $tz );
     2892        }
     2893       
     2894        // Time formatting
     2895        $time_out = '';
     2896        $has_time = ( $time_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_hm ) );
     2897        $has_time_to = ( $time_to_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_to_hm ) );
     2898       
     2899        if ( $has_time && $time_format !== '' ) {
     2900            $time_from = $this->wp_date_lang( 'H:i', $ts, $tz );
     2901            $time_to = '';
     2902            if ( $has_time_to ) {
     2903                $time_to = $time_to_hm;
     2904            }
     2905           
     2906            switch ( $time_format ) {
     2907                case 'raw':
     2908                    $time_out = $time_from;
     2909                    break;
     2910                   
     2911                case 'prefix':
     2912                    $p = $this->format_prefix_output( $prefix );
     2913                    $time_out = trim( $p . ' ' . $time_from );
     2914                    if ( ! $this->is_en ) {
     2915                        $time_out .= ' Uhr';
     2916                    }
     2917                    break;
     2918                   
     2919                case 'range':
     2920                    if ( $has_time_to ) {
     2921                        $p2 = $this->format_prefix2_output( $prefix2 );
     2922                        $time_out = $time_from . ' ' . $p2 . ' ' . $time_to;
     2923                    } else {
     2924                        $time_out = $time_from;
     2925                    }
     2926                    break;
     2927                   
     2928                case 'range_long':
     2929                    $p = $this->format_prefix_output( $prefix );
     2930                    if ( $has_time_to ) {
     2931                        $p2 = $this->format_prefix2_output( $prefix2 );
     2932                        $time_out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to );
     2933                    } else {
     2934                        $time_out = trim( $p . ' ' . $time_from );
     2935                    }
     2936                    if ( ! $this->is_en ) {
     2937                        $time_out .= ' Uhr';
     2938                    }
     2939                    break;
     2940                   
     2941                case 'auto':
     2942                default:
     2943                    $p = $this->format_prefix_output( $prefix );
     2944                    if ( $has_time_to ) {
     2945                        $p2 = $this->format_prefix2_output( $prefix2 );
     2946                        $time_out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to );
     2947                    } else {
     2948                        $time_out = trim( $p . ' ' . $time_from );
     2949                    }
     2950                    if ( ! $this->is_en ) {
     2951                        $time_out .= ' Uhr';
     2952                    }
     2953                    break;
     2954            }
     2955        }
     2956       
     2957        // Combine date and time
     2958        if ( $time_out !== '' ) {
     2959            return $date_out . $sep_html . $time_out;
     2960        }
     2961        return $date_out;
     2962    }
     2963   
     2964    /**
     2965     * Format prefix1 for output (handles case as stored).
     2966     */
     2967    private function format_prefix_output( $prefix ) {
     2968        if ( $prefix === '' ) {
     2969            return '';
     2970        }
     2971        // The prefix is stored exactly as the user chose (ab, Ab, von, Von, etc.)
     2972        return $prefix;
     2973    }
     2974   
     2975    /**
     2976     * Format prefix2 for output (handles dash conversion).
     2977     */
     2978    private function format_prefix2_output( $prefix2 ) {
     2979        if ( $prefix2 === '' ) {
     2980            return '';
     2981        }
     2982        if ( $prefix2 === 'dash' ) {
     2983            return '–'; // Em dash
     2984        }
     2985        // The prefix is stored exactly as the user chose (bis, Bis, to, To, etc.)
     2986        return $prefix2;
    24472987    }
    24482988
     
    25023042}
    25033043
     3044/**
     3045 * Shortcode: [luzid_cs_eventtable]
     3046 *
     3047 * Outputs a table of upcoming events from all schedulers with include_in_eventtable=1.
     3048 *
     3049 * Parameters:
     3050 * - cols: Comma-separated column types (default: date_medium,time_auto,text)
     3051 * - count: Max number of events (default: 30)
     3052 * - headers: Comma-separated custom headers (default: auto-generated)
     3053 * - noheaders: Hide table headers (default: false)
     3054 * - class: Additional CSS class for the table
     3055 * - empty: Text when no events (default: "Keine Termine" / "No events")
     3056 * - order: Sort order asc|desc (default: asc)
     3057 * - lang: Language override de|en
     3058 */
     3059public function shortcode_eventtable( $atts ) {
     3060    $atts = shortcode_atts(
     3061        [
     3062            'cols'      => 'date_medium,time_auto,text',
     3063            'count'     => 30,
     3064            'headers'   => '',
     3065            'noheaders' => 'false',
     3066            'class'     => '',
     3067            'empty'     => '',
     3068            'order'     => 'asc',
     3069            'lang'      => '',
     3070        ],
     3071        $atts,
     3072        'luzid_cs_eventtable'
     3073    );
     3074
     3075    $cols = array_map( 'trim', explode( ',', (string) $atts['cols'] ) );
     3076    $cols = array_filter( $cols );
     3077    if ( empty( $cols ) ) {
     3078        $cols = [ 'date_medium', 'time_auto', 'text' ];
     3079    }
     3080
     3081    $count = max( 1, min( 200, (int) $atts['count'] ) );
     3082    $custom_headers = array_map( 'trim', explode( ',', (string) $atts['headers'] ) );
     3083    $noheaders = in_array( strtolower( (string) $atts['noheaders'] ), [ '1', 'true', 'yes', 'on' ], true );
     3084    $custom_class = sanitize_html_class( (string) $atts['class'] );
     3085    $empty_text = sanitize_text_field( (string) $atts['empty'] );
     3086    $order = strtolower( (string) $atts['order'] ) === 'desc' ? 'desc' : 'asc';
     3087    $lang = strtolower( sanitize_text_field( (string) $atts['lang'] ) );
     3088
     3089    // Language override
     3090    $old_lang = $this->lang;
     3091    $old_is_en = $this->is_en;
     3092    if ( $lang === 'en' ) {
     3093        $this->lang = 'en';
     3094        $this->is_en = true;
     3095    } elseif ( $lang === 'de' ) {
     3096        $this->lang = 'de';
     3097        $this->is_en = false;
     3098    }
     3099
     3100    // Default empty text
     3101    if ( $empty_text === '' ) {
     3102        $empty_text = $this->is_en ? 'No events' : 'Keine Termine';
     3103    }
     3104
     3105    // Collect events from all schedulers with include_in_eventtable=1
     3106    $entries = $this->get_entries();
     3107    $all_events = [];
     3108    $now = time();
     3109
     3110    foreach ( $entries as $entry ) {
     3111        if ( empty( $entry['include_in_eventtable'] ) ) {
     3112            continue;
     3113        }
     3114
     3115        $events = $this->compute_next_events( $entry, $now, $count );
     3116        if ( ! is_array( $events ) ) {
     3117            continue;
     3118        }
     3119
     3120        foreach ( $events as $ev ) {
     3121            if ( empty( $ev['ts'] ) ) {
     3122                continue;
     3123            }
     3124            $ev['_scheduler_slug'] = (string) ( $entry['slug'] ?? '' );
     3125            $ev['_scheduler_name'] = (string) ( $entry['name'] ?? '' );
     3126            $all_events[] = $ev;
     3127        }
     3128    }
     3129
     3130    // Sort by timestamp
     3131    usort( $all_events, function( $a, $b ) use ( $order ) {
     3132        $cmp = (int) $a['ts'] - (int) $b['ts'];
     3133        return $order === 'desc' ? -$cmp : $cmp;
     3134    } );
     3135
     3136    // Limit to count
     3137    $all_events = array_slice( $all_events, 0, $count );
     3138
     3139    // No events?
     3140    if ( empty( $all_events ) ) {
     3141        $this->lang = $old_lang;
     3142        $this->is_en = $old_is_en;
     3143        return '<p class="luzid-cs-eventtable-empty">' . esc_html( $empty_text ) . '</p>';
     3144    }
     3145
     3146    // Build table
     3147    $table_class = 'luzid-cs-eventtable';
     3148    if ( $custom_class !== '' ) {
     3149        $table_class .= ' ' . $custom_class;
     3150    }
     3151
     3152    $html = '<table class="' . esc_attr( $table_class ) . '">';
     3153
     3154    // Headers
     3155    if ( ! $noheaders ) {
     3156        $html .= '<thead class="luzid-cs-eventtable__head"><tr class="luzid-cs-eventtable__row luzid-cs-eventtable__row--header">';
     3157        foreach ( $cols as $i => $col ) {
     3158            $header = isset( $custom_headers[ $i ] ) && $custom_headers[ $i ] !== ''
     3159                ? $custom_headers[ $i ]
     3160                : $this->eventtable_default_header( $col );
     3161            $html .= '<th class="luzid-cs-eventtable__cell luzid-cs-eventtable__cell--' . esc_attr( $col ) . '">' . esc_html( $header ) . '</th>';
     3162        }
     3163        $html .= '</tr></thead>';
     3164    }
     3165
     3166    // Body
     3167    $html .= '<tbody class="luzid-cs-eventtable__body">';
     3168    $row_num = 0;
     3169    foreach ( $all_events as $ev ) {
     3170        $row_num++;
     3171        $row_class = 'luzid-cs-eventtable__row luzid-cs-eventtable__row--' . ( $row_num % 2 === 0 ? 'even' : 'odd' );
     3172        $slug = esc_attr( $ev['_scheduler_slug'] ?? '' );
     3173       
     3174        $html .= '<tr class="' . esc_attr( $row_class ) . '" data-scheduler="' . $slug . '">';
     3175        foreach ( $cols as $col ) {
     3176            $cell_value = $this->eventtable_cell_value( $ev, $col );
     3177            $html .= '<td class="luzid-cs-eventtable__cell luzid-cs-eventtable__cell--' . esc_attr( $col ) . '">' . esc_html( $cell_value ) . '</td>';
     3178        }
     3179        $html .= '</tr>';
     3180    }
     3181    $html .= '</tbody>';
     3182
     3183    $html .= '</table>';
     3184
     3185    $this->lang = $old_lang;
     3186    $this->is_en = $old_is_en;
     3187    return $html;
    25043188}
    25053189
     3190/**
     3191 * Get default header text for a column type.
     3192 */
     3193private function eventtable_default_header( $col ) {
     3194    $headers_de = [
     3195        'date_short'    => 'Datum',
     3196        'date_medium'   => 'Datum',
     3197        'date_long'     => 'Datum',
     3198        'date_full'     => 'Datum',
     3199        'weekday_short' => 'Tag',
     3200        'weekday_long'  => 'Wochentag',
     3201        'time_raw'      => 'Uhrzeit',
     3202        'time_auto'     => 'Uhrzeit',
     3203        'time_prefix'   => 'Uhrzeit',
     3204        'time_range'    => 'Uhrzeit',
     3205        'time_range_long' => 'Uhrzeit',
     3206        'text'          => 'Event',
     3207        'scheduler'     => 'Scheduler',
     3208    ];
     3209    $headers_en = [
     3210        'date_short'    => 'Date',
     3211        'date_medium'   => 'Date',
     3212        'date_long'     => 'Date',
     3213        'date_full'     => 'Date',
     3214        'weekday_short' => 'Day',
     3215        'weekday_long'  => 'Weekday',
     3216        'time_raw'      => 'Time',
     3217        'time_auto'     => 'Time',
     3218        'time_prefix'   => 'Time',
     3219        'time_range'    => 'Time',
     3220        'time_range_long' => 'Time',
     3221        'text'          => 'Event',
     3222        'scheduler'     => 'Scheduler',
     3223    ];
     3224
     3225    $headers = $this->is_en ? $headers_en : $headers_de;
     3226    return $headers[ $col ] ?? $col;
     3227}
     3228
     3229/**
     3230 * Get cell value for a column type.
     3231 */
     3232private function eventtable_cell_value( $ev, $col ) {
     3233    $ts = (int) ( $ev['ts'] ?? 0 );
     3234    $tz = wp_timezone();
     3235
     3236    $prefix = (string) ( $ev['prefix'] ?? '' );
     3237    $prefix2 = (string) ( $ev['prefix2'] ?? '' );
     3238    $time_hm = (string) ( $ev['time'] ?? '' );
     3239    $time_to_hm = (string) ( $ev['time_to'] ?? '' );
     3240    $text = (string) ( $ev['text'] ?? '' );
     3241
     3242    $has_time = ( $time_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_hm ) );
     3243    $has_time_to = ( $time_to_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_to_hm ) );
     3244
     3245    switch ( $col ) {
     3246        // Date columns
     3247        case 'date_short':
     3248            return $this->wp_date_lang( 'd.m.Y', $ts, $tz );
     3249        case 'date_medium':
     3250            return $this->wp_date_lang( 'D, d.m.Y', $ts, $tz );
     3251        case 'date_long':
     3252            return $this->wp_date_lang( 'l, d.m.Y', $ts, $tz );
     3253        case 'date_full':
     3254            return $this->is_en
     3255                ? $this->wp_date_lang( 'l, d F Y', $ts, $tz )
     3256                : $this->wp_date_lang( 'l, d. F Y', $ts, $tz );
     3257
     3258        // Weekday columns
     3259        case 'weekday_short':
     3260            return $this->wp_date_lang( 'D', $ts, $tz );
     3261        case 'weekday_long':
     3262            return $this->wp_date_lang( 'l', $ts, $tz );
     3263
     3264        // Time columns
     3265        case 'time_raw':
     3266            return $has_time ? $this->wp_date_lang( 'H:i', $ts, $tz ) : '';
     3267           
     3268        case 'time_auto':
     3269            if ( ! $has_time ) return '';
     3270            $time_from = $this->wp_date_lang( 'H:i', $ts, $tz );
     3271            $p = $this->format_prefix_output( $prefix );
     3272            if ( $has_time_to ) {
     3273                $p2 = $this->format_prefix2_output( $prefix2 );
     3274                $out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to_hm );
     3275            } else {
     3276                $out = trim( $p . ' ' . $time_from );
     3277            }
     3278            if ( ! $this->is_en ) {
     3279                $out .= ' Uhr';
     3280            }
     3281            return $out;
     3282
     3283        case 'time_prefix':
     3284            if ( ! $has_time ) return '';
     3285            $time_from = $this->wp_date_lang( 'H:i', $ts, $tz );
     3286            $p = $this->format_prefix_output( $prefix );
     3287            $out = trim( $p . ' ' . $time_from );
     3288            if ( ! $this->is_en ) {
     3289                $out .= ' Uhr';
     3290            }
     3291            return $out;
     3292
     3293        case 'time_range':
     3294            if ( ! $has_time ) return '';
     3295            $time_from = $this->wp_date_lang( 'H:i', $ts, $tz );
     3296            if ( $has_time_to ) {
     3297                $p2 = $this->format_prefix2_output( $prefix2 );
     3298                return $time_from . ' ' . $p2 . ' ' . $time_to_hm;
     3299            }
     3300            return $time_from;
     3301
     3302        case 'time_range_long':
     3303            if ( ! $has_time ) return '';
     3304            $time_from = $this->wp_date_lang( 'H:i', $ts, $tz );
     3305            $p = $this->format_prefix_output( $prefix );
     3306            if ( $has_time_to ) {
     3307                $p2 = $this->format_prefix2_output( $prefix2 );
     3308                $out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to_hm );
     3309            } else {
     3310                $out = trim( $p . ' ' . $time_from );
     3311            }
     3312            if ( ! $this->is_en ) {
     3313                $out .= ' Uhr';
     3314            }
     3315            return $out;
     3316
     3317        // Content columns
     3318        case 'text':
     3319            return $text;
     3320
     3321        case 'scheduler':
     3322            return (string) ( $ev['_scheduler_name'] ?? '' );
     3323
     3324        default:
     3325            return '';
     3326    }
     3327}
     3328
     3329}
     3330
    25063331new Luzid_Content_Scheduler();
  • luzid-content-scheduler/trunk/readme.txt

    r3456038 r3476291  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.2.2
     7Stable tag: 1.4.2
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1717This gives you two complementary ways to use a scheduler:
    1818
    19 1) **Show / hide a block** (banner, alert, section, popup wrapper, …) 
     191. **Show / hide a block** (banner, alert, section, popup wrapper, …) 
    2020   Add the generated CSS class `luzid-cs-<slug>` to the element. The plugin adds body classes and a tiny frontend helper so the block is only displayed while the schedule is active.
    2121
    22 2) **Display dates in the frontend** 
     222. **Display dates in the frontend** 
    2323   Use the shortcode to print the next event (or a list of upcoming events) anywhere in your content.
    2424
    25 Typical use cases:
    26 
    27 * **Visibility scheduling**: show a maintenance banner only during a defined window, or show a seasonal notice with exceptions.
    28 * **Event announcements**: print "Next event: …" on a page, including weekday + time.
    29 * **Lists of upcoming dates**: output the next N dates of a program/club/course using `list="true"` + `count="N"`.
    30 
    31 Core concepts:
     25= Typical Use Cases =
     26
     27* **Maintenance banners**: Show a "We're updating our systems" notice only during scheduled maintenance windows.
     28* **Seasonal content**: Display holiday greetings, special offers, or seasonal menus during specific date ranges.
     29* **Event announcements**: Print "Next event: Saturday, March 15th at 7 PM" dynamically on any page.
     30* **Recurring schedules**: Show "Happy Hour specials" every Friday from 5 PM to 8 PM.
     31* **Multi-stage visibility**: Use Event Classes to show a popup 4 days before an event, the full content 2 weeks before, and a menu item 11 days before — all independently controlled.
     32* **Event calendars**: Generate a table of all upcoming events across multiple schedulers.
     33
     34= Core Concepts =
    3235
    3336* A scheduler becomes **active** when **at least one** rule matches (single dates/ranges *or* recurring rules).
    3437* **Exceptions override everything**: if "now" is inside an exception range, the block is hidden even if other rules match.
    35 * **Event offset** can shift the *visibility window* relative to the next computed event (e.g. show the block 4 days before the event, starting at a chosen time).
    36 * The shortcode reads the same event logic and can output:
    37   * `opt="long"` (default): weekday + date + time (e.g. "Mittwoch, 25.03.2026 ab 09:00 Uhr")
    38   * `opt="short"`: date only (e.g. "25.03.2026")
    39   * `opt="date"`: weekday + date (no time)
    40   * `opt="time"`: time only
    41   * `list="true"` + `count="N"`: outputs the next N events (one per line, separated by `<br />`)
    42   * `text="true"` optionally appends the event text (from the new "Event text (optional)" fields)
    43   * `sep="…"` sets the separator between opt output and event text (use `sep="<br>"` for a line break)
    44 * `timeoffset` (minutes, can be negative) shifts the **printed time** without changing the schedule itself (useful for timezone-like adjustments or "doors open 30 min earlier").
    45 * Optional `lang="de|en"` forces the output language for the shortcode (empty = current UI language).
     38* **Event Classes** (new in 1.4) allow multiple independent visibility windows with different offsets — each generating its own CSS class.
     39* The shortcode reads the same event logic and outputs formatted dates with full control over separators and formatting.
    4640
    4741== Features ==
    4842
    49 * **Setup**: Name → automatic slug + CSS class.
    50 * **Single dates / date ranges ("Termine")**
    51   * From/To fields
    52   * Optional time + prefix `ab` / `bis` / `um`
    53 * **Recurring rules ("Wiederholungen")**
    54   * Weekly (weekday)
    55   * Monthly (day of month)
    56   * Weekday in month (e.g. 2nd Thursday)
    57   * Each recurring rule can have its own valid-from/to range
    58 * **Exceptions ("Ausnahmen")**
    59   * Date or date range
    60   * Overrides all other rules
    61 * **Event offset**
    62   * Show the block X days before the computed event (from a fixed start time)
    63   * Hide the block X days after the computed event (to a fixed end time)
    64 * **Preview tab**: generate a table of upcoming events including show time and shortcode output.
    65 * **Shortcode**
    66   * Base output: `[luzid_cs slug="my-scheduler" opt="short|date|time|long"]`
    67   * List output: add `list="true"` + `count="N"` (N > 0) to output the next N events (one per line, separated by `<br>`).
    68   * Optional event text: add `text="true"` to append the event's **optional Event text**. Use `sep=" | "` to define the separator between date/time and text.
    69   * Optional `timeoffset` (minutes; can be negative) to shift the output time.
    70 * **Languages**: German + English UI (flag switcher in the header).
     43= Scheduling =
     44
     45* **Single dates & date ranges**: Define specific days or periods when content should be visible.
     46* **Recurring rules**: Weekly (every Monday), Monthly (every 15th), Weekday in month (2nd Thursday), Yearly (February 14th).
     47* **Exceptions**: Override all rules — content stays hidden during exception periods.
     48* **Event Classes**: Create multiple CSS classes per scheduler, each with independent visibility offsets.
     49
     50= Shortcode Output =
     51
     52* **Flexible date/time formatting**: Choose from short, medium, long, or full date formats.
     53* **Separators**: Control exactly what appears between date, time, and text parts.
     54* **Lists**: Output multiple upcoming events with customizable separators.
     55* **Event Table**: Generate a sortable table of all upcoming events from multiple schedulers.
     56* **Time offset**: Shift the displayed time (e.g., "doors open 30 minutes before").
     57
     58= Administration =
     59
     60* **Card-based UI**: Clean, modern interface for managing rules.
     61* **Live preview**: See upcoming events and visibility windows instantly.
     62* **Bilingual**: Full German and English support with one-click language switching.
    7163
    7264== Installation ==
    7365
    74 1. Upload the plugin folder to `/wp-content/plugins/` or install it via the WordPress Plugins screen.
     661. Upload the plugin folder to `/wp-content/plugins/` or install via the WordPress Plugins screen.
    75672. Activate **Luzid Content Scheduler**.
    76683. Go to **WP Admin → Luzid Content Scheduler**.
     
    8072== Usage ==
    8173
    82 = Add the CSS class to your block =
    83 
    84 * **Gutenberg**: select block → "Advanced" → "Additional CSS class(es)" → `luzid-cs-<slug>`
    85 * **Elementor**: widget → "Advanced" → "CSS Classes" → `luzid-cs-<slug>`
    86 * **Divi/others**: module settings → "CSS Class" → `luzid-cs-<slug>`
    87 
    88 = Shortcode output =
    89 
    90 Examples:
    91 
    92 * Long (default): `[luzid_cs slug="wartung" opt="long"]`
    93 * Date only: `[luzid_cs slug="wartung" opt="date"]`
    94 * Time only: `[luzid_cs slug="wartung" opt="time"]`
    95 * List (next 7): `[luzid_cs slug="wartung" opt="short" list="true" count="7"]`
    96 * List + event text: `[luzid_cs slug="krimidinner" opt="short" list="true" count="5" text="true" sep=" | "]`
    97 * Event text on new line: `[luzid_cs slug="krimidinner" opt="date" text="true" sep="<br>"]`
    98 
    99 CSS hooks (no default styling – add your own CSS if you want):
    100 
    101 * `.luzid-cs` (wrapper, plus `.luzid-cs--single` / `.luzid-cs--list`)
    102 * `.luzid-cs-item` (one event in list output)
    103 * `.luzid-cs-opt` (date/time part)
    104 * `.luzid-cs-sep` (separator)
    105 * `.luzid-cs-text` (optional event text)
     74= Adding CSS Classes to Blocks =
     75
     76* **Gutenberg**: Select block → "Advanced" → "Additional CSS class(es)" → `luzid-cs-<slug>`
     77* **Elementor**: Widget → "Advanced" → "CSS Classes" → `luzid-cs-<slug>`
     78* **Divi/others**: Module settings → "CSS Class" → `luzid-cs-<slug>`
     79
     80= Basic Shortcode =
     81
     82`[luzid_cs slug="your-scheduler"]`
     83
     84Outputs: The next event date in the default format.
     85
     86= Shortcode Parameters =
     87
     88| Parameter | Default | Description |
     89|-----------|---------|-------------|
     90| `slug` | (required) | Scheduler slug |
     91| `date` | (empty) | Date format: `short`, `medium`, `long`, `full` |
     92| `time` | (empty) | Time format: `auto`, `raw`, `prefix`, `range`, `range_long` |
     93| `list` | `false` | Output as list: `true` or `false` |
     94| `count` | `10` | Number of events for lists (max 200) |
     95| `text` | `false` | Include event text: `true` or `false` |
     96| `sep1` | (space) | Separator between date and time |
     97| `sep2` | (space) | Separator between time and text |
     98| `sep3` | (empty) | Separator between list items |
     99| `timeoffset` | `0` | Time offset in minutes (can be negative) |
     100| `lang` | (current) | Force language: `de` or `en` |
     101
     102= Date Formats =
     103
     104| Value | Output Example |
     105|-------|----------------|
     106| `short` | 14.02.2026 |
     107| `medium` | Sat, 14.02.2026 |
     108| `long` | Saturday, 14.02.2026 |
     109| `full` | Saturday, 14 February 2026 |
     110
     111= Time Formats =
     112
     113| Value | Output Example |
     114|-------|----------------|
     115| (empty) | No time output |
     116| `raw` | 18:00 |
     117| `prefix` | from 18:00 |
     118| `range` | 18:00 to 20:00 |
     119| `range_long` | from 18:00 to 20:00 |
     120| `auto` | Intelligent format based on data |
     121
     122= Shortcode Examples =
     123
     124**Simple date output:**
     125`[luzid_cs slug="maintenance" date="long"]`
     126→ Saturday, 15.03.2026
     127
     128**Date with time:**
     129`[luzid_cs slug="event" date="long" time="auto"]`
     130→ Saturday, 15.03.2026 from 18:00 to 20:00
     131
     132**Only time output:**
     133`[luzid_cs slug="event" time="raw"]`
     134→ 18:00
     135
     136**Multi-line output:**
     137`[luzid_cs slug="event" date="long" time="auto" text="true" sep1="<br>" sep2="<br>"]`
     138→ Saturday, 15.03.2026
     139   from 18:00 to 20:00
     140   Valentine's Dinner
     141
     142**List of next 5 events:**
     143`[luzid_cs slug="concert" date="long" text="true" list="true" count="5" sep2="<br>" sep3="<br><br>"]`
     144
     145**With custom separators:**
     146`[luzid_cs slug="event" date="long" time="raw" text="true" sep1=" | " sep2=" – "]`
     147→ Saturday, 15.03.2026 | 18:00 – Valentine's Dinner
     148
     149= Event Table Shortcode =
     150
     151`[luzid_cs_eventtable]`
     152
     153Outputs a table of all upcoming events from schedulers with "Include in Event Table" enabled.
     154
     155| Parameter | Default | Description |
     156|-----------|---------|-------------|
     157| `cols` | `date_medium,time_auto,text` | Columns (comma-separated) |
     158| `count` | `30` | Maximum number of events |
     159| `headers` | (automatic) | Custom column headers |
     160| `noheaders` | `false` | Hide table headers |
     161| `class` | (empty) | Additional CSS class |
     162| `empty` | "No events" | Text when no events found |
     163| `order` | `asc` | Sort order: `asc` or `desc` |
     164
     165**Available columns:**
     166`date_short`, `date_medium`, `date_long`, `date_full`, `weekday_short`, `weekday_long`, `time_raw`, `time_auto`, `time_prefix`, `time_range`, `time_range_long`, `text`, `scheduler`
     167
     168**Example with custom columns:**
     169`[luzid_cs_eventtable cols="weekday_short,date_short,time_range,text" count="10" headers="Day,Date,Time,Event"]`
     170
     171= Event Classes (New in 1.4) =
     172
     173Event Classes allow you to create multiple independent visibility windows for a single scheduler. Each class has its own CSS class and offset settings.
     174
     175**Example setup:**
     176
     177| Class | CSS Class | Days Before | Use Case |
     178|-------|-----------|-------------|----------|
     179| Standard | `.luzid-cs-dinner` | 14 | Main content |
     180| popup | `.luzid-cs-dinner-popup` | 4 | Announcement popup |
     181| menu | `.luzid-cs-dinner-menu` | 11 | Navigation menu item |
     182
     183This allows you to control when different elements appear, all based on the same event schedule.
     184
     185= CSS Hooks =
     186
     187**Shortcode output:**
     188* `.luzid-cs` — Wrapper (plus `.luzid-cs--single` / `.luzid-cs--list`)
     189* `.luzid-cs-item` — Single event in list
     190* `.luzid-cs-date` — Date part
     191* `.luzid-cs-time` — Time part
     192* `.luzid-cs-text` — Event text
     193* `.luzid-cs-sep` / `.luzid-cs-sep1` / `.luzid-cs-sep2` — Separators
     194
     195**Event table:**
     196* `.luzid-cs-eventtable` — Table wrapper
     197* `.luzid-cs-eventtable__head` / `__body` / `__row` / `__cell`
     198
     199**Body classes (for conditional CSS):**
     200* `.luzid-cs-active-<slug>` — Added when scheduler is active
     201* `.luzid-cs-active-<slug>-<classname>` — Added when specific event class is active
    106202
    107203== Frequently Asked Questions ==
     
    115211Exceptions affect **everything** (frontend visibility, next event calculation, and preview). If "now" is inside an exception range, the block stays hidden.
    116212
     213= What's the difference between Event Offset (old) and Event Classes (new)? =
     214
     215Event Classes replaced the old Event Offset feature in version 1.4. The key difference: you can now create **multiple classes** with independent offsets, allowing different content to appear at different times before/after an event.
     216
     217= Can I use multiple Event Classes for one block? =
     218
     219Each Event Class generates a separate CSS class. You can only apply one class per block, but you can have multiple blocks, each with a different Event Class.
     220
    117221= Does the plugin require a specific theme? =
    118222
    119223No. It works with any theme/page builder that lets you set a custom CSS class on a block/element.
    120224
     225= Can I output only the time without the date? =
     226
     227Yes! Use `[luzid_cs slug="your-slug" time="raw"]` to output only the time. Leave out the `date` parameter.
     228
     229= How do I create a line break between date and text? =
     230
     231Use `sep2="<br>"`: `[luzid_cs slug="event" date="long" text="true" sep2="<br>"]`
     232
    121233== Screenshots ==
    122234
    123 1. Setup tab: create a scheduler and copy the CSS class / shortcode.
    124 2. Add single dates or date ranges.
    125 3. Recurring rules with weekday/month options.
    126 4. Exceptions override all other rules.
    127 5. Preview of upcoming events.
     2351. Setup tab: Create a scheduler and copy the CSS class / shortcode.
     2362. Dates tab: Add single dates or date ranges with times and event text.
     2373. Recurring tab: Define weekly, monthly, or yearly patterns.
     2384. Exceptions tab: Block specific dates from showing content.
     2395. Event Classes tab: Create multiple CSS classes with independent visibility windows.
     2406. Preview tab: See all upcoming events at a glance.
    128241
    129242== License ==
     
    134247
    135248Assets:
    136 
    137 * Luzid Logo and Flag icons are SVG assets shipped with the plugin (assets/img/luzid-media-logo-plugins.svg | assets/img/ger.svg | assets/img/uk.svg).
     249* Luzid Logo and Flag icons are SVG assets shipped with the plugin (assets/img/).
    138250
    139251== Changelog ==
    140252
    141 = 1.2.2 =
    142 * Initial WordPress.org release.
    143 * Shortcode: `[luzid_cs slug="..." opt="long"]`
    144 * CSS class: `luzid-cs-<slug>`
    145 * Body class: `luzid-cs-active-<slug>`
    146 * Full WordPress coding standards compliance.
     253= 1.4.2 =
     254* Fixed: Version number display in admin header.
     255* Improved: Event Classes UI with copy button for CSS classes and consistent icon styling.
     256
     257= 1.4.1 =
     258* Fixed: WordPress Plugin Check escaping warnings for Event Classes panel.
     259
     260= 1.4.0 =
     261* **New: Event Classes** — Replace single Event Offset with unlimited named classes, each with independent visibility offsets.
     262* New: Each Event Class generates its own CSS class (`luzid-cs-<slug>-<classname>`).
     263* New: Live preview of visibility windows per class.
     264* New: Standard class (non-deletable) maintains backward compatibility.
     265* Changed: Tab renamed from "Event-Offset" to "Event-Klassen".
     266* Improved: Card-based UI consistent with other tabs.
     267* Migration: Existing Event Offset settings are automatically converted to the new Event Classes structure.
     268
     269= 1.3.12 =
     270* Fixed: Shortcode now outputs exactly what is specified — no hidden defaults.
     271* Fixed: `date` parameter defaults to empty (not "long"). Only when neither `date` nor `time` is set, `date="long"` is used as fallback.
     272* Fixed: Using only `time="raw"` now correctly outputs only the time, not date + time.
     273
     274= 1.3.11 =
     275* Fixed: Auto-padding removed from separators. `&nbsp;` now outputs exactly one non-breaking space.
     276
     277= 1.3.10 =
     278* Fixed: Single space separator handling — WordPress trims `sep1=" "`, now uses default fallback.
     279
     280= 1.3.9 =
     281* **New: Separator system refactored** — `sep` replaced by `sep1`, `sep2`, `sep3` for precise control.
     282* New: `sep1` controls separator between date and time.
     283* New: `sep2` controls separator between time and text (or date and text if no time).
     284* New: `sep3` controls separator between list items.
     285* Changed: `time` parameter now defaults to empty (no time output unless explicitly requested).
     286* Breaking: Old `sep` parameter removed. Use `sep1`/`sep2`/`sep3` instead.
     287
     288= 1.3.8 =
     289* Fixed: Preview table now shows actual saved event text per date.
     290
     291= 1.3.7 =
     292* Fixed: Yearly recurrence calculation for events spanning year boundaries.
     293
     294= 1.3.6 =
     295* New: `[luzid_cs_eventtable]` shortcode for tabular event output.
     296* New: "Include in Event Table" checkbox per scheduler.
     297* New: Customizable columns, headers, and sorting for event tables.
     298
     299= 1.3.5 =
     300* Improved: Card-based layout unified across all tabs.
     301
     302= 1.3.4 =
     303* New: Dual-prefix system for Termine tab (CSS class prefix options).
     304
     305= 1.3.0 =
     306* New: "Time to" field for time ranges (e.g., 18:00 – 20:00) in single dates and recurring rules.
     307* New: Yearly recurring rule type (e.g., every February 14th at 16:00).
     308* New: Card-based layout for Termine and Wiederholungen tabs.
     309* Changed: Shortcode renamed from `[lz-cs]` to `[luzid_cs]` for WordPress.org compliance.
     310* Changed: CSS classes renamed from `lz-cs-*` to `luzid-cs-*`.
     311
     312= 1.2.9 =
     313* Changed: List wrapper from `<span>` to `<div>` for better HTML structure.
     314
     315= 1.2.7 =
     316* Fix: Prevent "phantom" events from incomplete recurring rules.
     317* New: Preview table shows event source (Single vs Recurring).
     318
     319= 1.2.6 =
     320* New: Shortcode output wrapped in styled span classes.
     321* Improved: `sep` parameter can output `<br>` for line breaks.
     322
     323= 1.2.5 =
     324* Fix: Separator spacing preserved correctly.
     325* Improvement: Single dates sorted chronologically.
     326* New: Optional event text for recurring rules.
     327
     328= 1.2.3 =
     329* Added: Optional event text field for single dates.
     330* Changed: Shortcode supports `list`, `count`, `text` and `sep` parameters.
     331
     332= 1.2.0 =
     333* Initial release.
Note: See TracChangeset for help on using the changeset viewer.