diff --git a/docs/styleguide/docs/portal_sync_flow.md b/docs/styleguide/docs/portal_sync_flow.md index e0a3f54..952e999 100644 --- a/docs/styleguide/docs/portal_sync_flow.md +++ b/docs/styleguide/docs/portal_sync_flow.md @@ -7,6 +7,7 @@ Der Styleguide bleibt in diesem Repository die Source of Truth. Der Sync verteil - den deploy-relevanten CSS-Upstream versioniert als `public/assets/styleguide.upstream.css` - die vollstaendige Styleguide-Dokumentation gespiegelt nach `docs/styleguide` - die portal-spezifische Fertig-CSS-Datei als `public/assets/styles.css` +- die konsumierbare Large-Table-Runtime als `public/scripts/large-table-column-resize.js` Zielrepos: @@ -31,8 +32,10 @@ Beispiel: - `public/assets/styleguide.upstream.css` aktualisiert - `public/assets/styleguide.upstream.meta.json` aktualisiert (Version, Commit, Zeitstempel) -- `docs/styleguide` gespiegelt (mit `--delete`, ohne `.git`, `.codex`, `AGENTS.md`, `scripts/`) +- `docs/styleguide` gespiegelt (mit `--delete`, ohne `.git`, `.codex`, `AGENTS.md` und interne `scripts/`) +- `public/scripts/large-table-column-resize.js` aus der Styleguide-Source-of-Truth kopiert - `public/assets/styles.css` aktualisiert +- `public/scripts/large-table-column-resize.js` aktualisiert; lokale Portal-Kopien sind nicht zulässig - Optional: automatischer Commit + Push im Portalrepo ## Standardprozess je Release @@ -42,3 +45,18 @@ Beispiel: 3. Styleguide commit + push 4. Sync-Skript ausfuehren 5. Beide Portalrepos Smoke-Testen + +## Large-Table-Runtime + +Die Runtime liegt ausschließlich im Styleguide unter `scripts/large-table-column-resize.js`. +Der Sync kopiert nur diese explizit freigegebene Runtime nach `public/scripts/`; andere +Styleguide-Scripts wie Prüf- und Sync-Shellscripts werden nicht in `docs/styleguide/scripts/` +gespiegelt. Dadurch bleibt der Styleguide die Source of Truth und ein erneuter Sync +überschreibt die Portaldatei kontrolliert. + +Für den Smoke-Test in beiden Portalen ist zu prüfen: + +- `public/scripts/large-table-column-resize.js` ist vorhanden und unter `/scripts/large-table-column-resize.js` ladbar. +- Die Large-Table-Seite lädt die Runtime ohne lokale Inline- oder Duplikatimplementierung. +- Nach einer Spaltenbreitenänderung wird der Eintrag unter `sg:large-table:column-widths:` gespeichert. +- Ein erneuter Sync ersetzt die Datei aus der Styleguide-Quelle. diff --git a/public/assets/styleguide.upstream.meta.json b/public/assets/styleguide.upstream.meta.json index 6436eb1..9a0c4d9 100644 --- a/public/assets/styleguide.upstream.meta.json +++ b/public/assets/styleguide.upstream.meta.json @@ -1,7 +1,7 @@ { "styleguideVersion": "2026.05.18.2", - "styleguideCommit": "5134b61", - "syncedAtUtc": "2026-09-27T06:20:05Z", + "styleguideCommit": "09966ce", + "syncedAtUtc": "2026-09-27T06:25:30Z", "sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide", "mirroredDocsPath": "docs/styleguide" } diff --git a/public/scripts/large-table-column-resize.js b/public/scripts/large-table-column-resize.js new file mode 100644 index 0000000..194e7a8 --- /dev/null +++ b/public/scripts/large-table-column-resize.js @@ -0,0 +1,242 @@ +(function initLargeTableColumnResizeModule() { + const STORAGE_PREFIX = 'sg:large-table:column-widths:'; + const STORAGE_VERSION = 1; + const DEFAULT_MIN_WIDTH = 128; + const KEYBOARD_STEP = 8; + const KEYBOARD_LARGE_STEP = 40; + let activeResize = null; + + const getTables = (root) => root.querySelectorAll('[data-component="large-table"][data-column-resize="true"]'); + + const getColumnHeaders = (table) => Array.from( + table.querySelectorAll('[data-component-part="large-table-header-row"] [role="columnheader"]'), + ); + + const getMinWidth = (table) => { + const configured = table.dataset.columnMinWidth; + const value = configured || getComputedStyle(table).getPropertyValue('--large-table-column-min-width').trim(); + if (!value) { + return DEFAULT_MIN_WIDTH; + } + + const probe = document.createElement('span'); + probe.style.cssText = `position:absolute;width:${value};visibility:hidden;`; + table.appendChild(probe); + const width = probe.getBoundingClientRect().width; + probe.remove(); + return width > 0 ? width : DEFAULT_MIN_WIDTH; + }; + + const parseDefaultWidth = (rawValue, tableWidth) => { + if (!rawValue) { + return null; + } + const value = Number.parseFloat(rawValue); + if (!Number.isFinite(value) || value <= 0) { + return null; + } + if (rawValue.trim().endsWith('%')) { + return tableWidth * value / 100; + } + if (rawValue.trim().endsWith('fr')) { + return value; + } + return value; + }; + + const normalizeWidths = (widths, tableWidth, minWidth) => { + const safeWidths = widths.map((width) => Math.max(minWidth, width)); + const total = safeWidths.reduce((sum, width) => sum + width, 0); + if (total <= 0) { + return Array(widths.length).fill(Math.max(minWidth, tableWidth / widths.length)); + } + if (widths.some((width) => width !== width)) { + return Array(widths.length).fill(Math.max(minWidth, tableWidth / widths.length)); + } + return safeWidths.map((width) => width * tableWidth / total); + }; + + const getDefaultWidths = (table, headers, minWidth) => { + const tableWidth = Math.max(table.clientWidth, minWidth * headers.length); + const configured = headers.map((header) => parseDefaultWidth(header.dataset.defaultWidth, tableWidth)); + if (configured.some((width) => width === null)) { + return Array(headers.length).fill(Math.max(minWidth, tableWidth / headers.length)); + } + return normalizeWidths(configured, tableWidth, minWidth); + }; + + const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`; + + const readStoredWidths = (table, count, minWidth) => { + const key = getStorageKey(table); + if (!table.dataset.columnResizeKey) { + return null; + } + try { + if (!window.localStorage) { + return null; + } + const stored = JSON.parse(window.localStorage.getItem(key)); + if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.widths) || stored.widths.length !== count) { + return null; + } + if (stored.widths.some((width) => !Number.isFinite(width) || width < minWidth)) { + return null; + } + return stored.widths; + } catch (error) { + return null; + } + }; + + const writeStoredWidths = (table, widths) => { + if (!table.dataset.columnResizeKey) { + return; + } + try { + if (!window.localStorage) { + return; + } + window.localStorage.setItem(getStorageKey(table), JSON.stringify({ + version: STORAGE_VERSION, + widths: widths.map((width) => Math.round(width)), + })); + } catch (error) { + // Storage can be unavailable in privacy-restricted browser contexts. + } + }; + + const updateHandleState = (table, widths, minWidth) => { + const handles = table.querySelectorAll('[data-large-table-resize-handle]'); + handles.forEach((handle) => { + const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10); + const current = widths[index]; + const maximum = Math.max(minWidth, widths[index] + widths[index + 1] - minWidth); + if (table.id) { + handle.setAttribute('aria-controls', table.id); + } + handle.setAttribute('aria-valuemin', String(Math.round(minWidth))); + handle.setAttribute('aria-valuemax', String(Math.round(maximum))); + handle.setAttribute('aria-valuenow', String(Math.round(current))); + handle.setAttribute('aria-valuetext', `${Math.round(current)} Pixel`); + }); + }; + + const applyWidths = (table, widths, minWidth) => { + const safeWidths = widths.map((width) => Math.max(minWidth, width)); + const total = safeWidths.reduce((sum, width) => sum + width, 0); + table.style.setProperty('--large-table-grid-template-columns', safeWidths.map((width) => `${width}px`).join(' ')); + table.style.setProperty('--large-table-grid-min-width', `${Math.ceil(total)}px`); + table._largeTableColumnWidths = safeWidths; + updateHandleState(table, safeWidths, minWidth); + }; + + const resetWidths = (table) => { + const headers = getColumnHeaders(table); + const minWidth = getMinWidth(table); + const widths = getDefaultWidths(table, headers, minWidth); + try { + window.localStorage.removeItem(getStorageKey(table)); + } catch (error) { + // Storage can be unavailable in privacy-restricted browser contexts. + } + applyWidths(table, widths, minWidth); + }; + + const resizeColumn = (table, index, delta, persist) => { + const widths = [...table._largeTableColumnWidths]; + const minWidth = getMinWidth(table); + const nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta)); + const appliedDelta = nextLeft - widths[index]; + widths[index] = nextLeft; + widths[index + 1] -= appliedDelta; + applyWidths(table, widths, minWidth); + if (persist) { + writeStoredWidths(table, widths); + } + }; + + const initTable = (table) => { + const headers = getColumnHeaders(table); + if (headers.length < 2) { + return; + } + const minWidth = getMinWidth(table); + const stored = readStoredWidths(table, headers.length, minWidth); + applyWidths(table, stored || getDefaultWidths(table, headers, minWidth), minWidth); + + if (table.dataset.largeTableResizeInitialized === 'true') { + return; + } + table.dataset.largeTableResizeInitialized = 'true'; + + table.addEventListener('pointerdown', (event) => { + const handle = event.target.closest('[data-large-table-resize-handle]'); + if (!handle || !table.contains(handle)) { + return; + } + event.preventDefault(); + activeResize = { + table, + handle, + index: Number.parseInt(handle.dataset.largeTableResizeHandle, 10), + startX: event.clientX, + startWidths: [...table._largeTableColumnWidths], + }; + handle.dataset.largeTableResizeActive = 'true'; + table.dataset.columnResizeState = 'resizing'; + handle.setPointerCapture?.(event.pointerId); + }); + + table.addEventListener('pointermove', (event) => { + if (!activeResize || activeResize.table !== table) { + return; + } + applyWidths(table, activeResize.startWidths, getMinWidth(table)); + resizeColumn(table, activeResize.index, event.clientX - activeResize.startX, false); + }); + + table.addEventListener('pointerup', () => { + if (!activeResize || activeResize.table !== table) { + return; + } + writeStoredWidths(table, table._largeTableColumnWidths); + activeResize.handle.dataset.largeTableResizeActive = 'false'; + table.dataset.columnResizeState = 'idle'; + activeResize = null; + }); + + table.addEventListener('keydown', (event) => { + const handle = event.target.closest('[data-large-table-resize-handle]'); + if (!handle || !table.contains(handle)) { + return; + } + const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10); + const step = event.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP; + if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { + event.preventDefault(); + resizeColumn(table, index, event.key === 'ArrowRight' ? step : -step, true); + } else if (event.key === 'Home' || event.key === 'End') { + event.preventDefault(); + const minWidth = getMinWidth(table); + const current = table._largeTableColumnWidths[index]; + const target = event.key === 'Home' ? minWidth : current + table._largeTableColumnWidths[index + 1] - minWidth; + resizeColumn(table, index, target - current, true); + } + }); + + table.addEventListener('click', (event) => { + const reset = event.target.closest('[data-large-table-reset-widths]'); + if (reset && table.contains(reset)) { + resetWidths(table); + } + }); + }; + + window.sgInitLargeTableColumnResize = (options = {}) => { + const root = options.root || document; + getTables(root).forEach(initTable); + }; + + document.addEventListener('DOMContentLoaded', () => window.sgInitLargeTableColumnResize(), { once: true }); +})();