# Was ist ein Widget? Source: https://www.arc-rider.com/docs/intro/was-sind-widgets # Was ist ein Widget in React? Ein arcWidget ist eine konfigurierbare Komponente für eine Geschäftsanwendung: zum Beispiel eine Tabelle, ein Kalender oder ein Kanban-Board. Du übergibst die Darstellung als `data` und verbindest Ereignisse über Callback-Props mit deiner Anwendung. ## Ein vollständiges Beispiel ```tsx import { ArcWidgetBadge, ArcWidgetIcon, ArcWidgetKpiBar, ArcWidgetProgressBar, ArcWidgetTable, type ArcWidgetTableData, type ArcWidgetTableRow, } from '@arcrider/arcwidgets-react'; import { useMemo, useState, type CSSProperties } from 'react'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; type Theme = 'light' | 'dark'; type DbStatus = 'Active' | 'Idle' | 'Locked'; type DbRow = { id: string; schema: string; name: string; type: 'Table' | 'View'; rows: number; size: string; status: DbStatus; }; const STATUS_LOOK: Record< DbStatus, { fontColor: string; backgroundColor: string } > = { Active: { fontColor: '#15803D', backgroundColor: '#DCFCE7' }, Idle: { fontColor: '#52525B', backgroundColor: '#F4F4F5' }, Locked: { fontColor: '#991B1B', backgroundColor: '#FFE4E6' }, }; const STATUS_LOOK_DARK: Record< DbStatus, { fontColor: string; backgroundColor: string } > = { Active: { fontColor: '#86EFAC', backgroundColor: '#14532D' }, Idle: { fontColor: '#A1A1AA', backgroundColor: '#27272A' }, Locked: { fontColor: '#FDA4AF', backgroundColor: '#4C0519' }, }; const SCHEMA_THEME = { public: { light: { bg: '#E8F1FF', ink: '#2563EB', track: '#BFDBFE' }, dark: { bg: '#152033', ink: '#93C5FD', track: '#1E3A5F' }, }, auth: { light: { bg: '#FFF4E8', ink: '#C2410C', track: '#FED7AA' }, dark: { bg: '#24180E', ink: '#FDBA74', track: '#4A2E12' }, }, storage: { light: { bg: '#E8F8EF', ink: '#15803D', track: '#BBF7D0' }, dark: { bg: '#122018', ink: '#86EFAC', track: '#14532D' }, }, } as const; type SchemaName = keyof typeof SCHEMA_THEME; const SCHEMAS = Object.keys(SCHEMA_THEME) as SchemaName[]; const STATUSES: DbStatus[] = ['Active', 'Idle', 'Locked']; const demoRows: DbRow[] = [ { id: 'tbl-users', schema: 'public', name: 'users', type: 'Table', rows: 1204, size: '4.2 MB', status: 'Active', }, { id: 'tbl-orders', schema: 'public', name: 'orders', type: 'Table', rows: 43012, size: '128.0 MB', status: 'Active', }, { id: 'tbl-products', schema: 'public', name: 'products', type: 'Table', rows: 890, size: '1.1 MB', status: 'Idle', }, { id: 'vw-revenue', schema: 'public', name: 'monthly_revenue', type: 'View', rows: 0, size: '0.0 MB', status: 'Idle', }, { id: 'tbl-sessions', schema: 'auth', name: 'sessions', type: 'Table', rows: 52, size: '0.4 MB', status: 'Active', }, { id: 'tbl-audit', schema: 'auth', name: 'audit_log', type: 'Table', rows: 12301, size: '45.0 MB', status: 'Locked', }, { id: 'tbl-identities', schema: 'auth', name: 'identities', type: 'Table', rows: 104, size: '0.8 MB', status: 'Active', }, { id: 'tbl-objects', schema: 'storage', name: 'objects', type: 'Table', rows: 4019, size: '12.5 MB', status: 'Idle', }, { id: 'tbl-buckets', schema: 'storage', name: 'buckets', type: 'Table', rows: 4, size: '0.1 MB', status: 'Idle', }, ]; const busyRows: DbRow[] = [ ...demoRows, ...Array.from({ length: 24 }, (_, index): DbRow => ({ id: `busy-${index + 1}`, schema: SCHEMAS[index % SCHEMAS.length], name: `extra_${String(index + 1).padStart(2, '0')}`, type: index % 5 === 0 ? 'View' : 'Table', rows: 20 + ((index * 37) % 9000), size: `${(0.1 + (index % 9) * 0.4).toFixed(1)} MB`, status: STATUSES[index % STATUSES.length], })), ]; const rows = pack === 'empty' ? [] : pack === 'busy' ? busyRows : demoRows; const connections = pack === 'empty' ? 0 : pack === 'busy' ? 12 : 4; const statusLabel: Record = { Active: 'Active', Idle: 'Idle', Locked: 'Locked', }; const typeLabel = { Table: 'Table', View: 'View', }; function parseMb(size: string): number { const value = Number.parseFloat(size); if (Number.isNaN(value)) return 0; return size.includes('GB') ? value * 1024 : value; } function formatMb(mb: number): string { if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`; return `${mb.toFixed(1)} MB`; } function groupBySchema(list: DbRow[]) { const groups = new Map(); for (const row of list) { const next = groups.get(row.schema) ?? []; next.push(row); groups.set(row.schema, next); } return [...groups.entries()]; } function StatusBadge({ status, theme }: { status: DbStatus; theme: Theme }) { const look = (theme === 'dark' ? STATUS_LOOK_DARK : STATUS_LOOK)[status]; return ( ); } function leafRow( row: DbRow, selectedIds: Set, toggleSelection: (id: string) => void, theme: Theme, accent: string, ): ArcWidgetTableRow { return { id: row.id, clickable: true, rowHeight: '36px', rowPaddingY: '4px', rowColor: selectedIds.has(row.id) ? theme === 'dark' ? '#27272A' : '#F4F4F5' : '', columns: [ { width: 'fraction', clickable: true, value: (
toggleSelection(row.id)} onClick={(event) => event.stopPropagation()} style={{ '--check-color': accent } as CSSProperties} /> {row.name}
), sort: { value: row.name }, }, { value: ( {typeLabel[row.type]} ), width: '100px', }, { value: , width: '100px', }, { value: ( {row.rows.toLocaleString('en-US')} ), width: '80px', alignX: 'right', sort: { value: row.rows }, }, { value: {row.size}, width: '110px', alignX: 'right', }, ], }; } function buildTableData( list: DbRow[], selectedIds: Set, toggleSelection: (id: string) => void, theme: Theme, ): ArcWidgetTableData { const dark = theme === 'dark'; const line = dark ? '#2A3A4E' : '#E4E4E7'; return { height: '360px', collapsible: { enabled: true, expandColumn: 0, indentColumn: 0, indentSize: '32px', } as ArcWidgetTableData['collapsible'] & { indentSize: string }, rowHoverAction: { hoverActive: true, backgroundColor: dark ? '#16202A' : '#FAFAFA', }, scrollBar: { showScrollBar: true, height: '6px', backgroundColor: dark ? '#1A2430' : '#F4F4F5', handle: { backgroundColor: dark ? '#6B8499' : '#A1A1AA', borderRadius: '6px', }, }, header: { showHeader: true, height: '32px', backgroundColor: dark ? '#121820' : '#FAFAFA', fontColor: dark ? '#9AADC0' : '#71717A', fontSize: '11px', columns: [ { title: 'Name', width: 'fraction' }, { title: 'Type', width: '100px' }, { title: 'Status', width: '100px' }, { title: 'Rows', width: '80px', alignX: 'right' }, { title: 'Size', width: '110px', alignX: 'right' }, ], }, styles: { backgroundColor: dark ? '#0B1016' : '#FFFFFF', fontColor: dark ? '#9AADC0' : '#52525B', borderRadius: '8px', borders: { color: line, width: '1px', outer: { color: line, width: '1px' }, columns: false, rows: { color: line, width: '1px' }, }, }, table: groupBySchema(list).map(([schema, children]) => { const look = SCHEMA_THEME[schema as SchemaName]?.[theme] ?? { bg: dark ? '#16202A' : '#F4F4F5', ink: dark ? '#E4E4E7' : '#3F3F46', track: dark ? '#1A2430' : '#E4E4E7', }; const totalRows = children.reduce((sum, child) => sum + child.rows, 0); const totalMb = children.reduce( (sum, child) => sum + parseMb(child.size), 0, ); const active = children.filter( (child) => child.status === 'Active', ).length; return { id: `schema-${schema}`, clickable: true, rowHeight: '36px', rowPaddingY: '4px', rowColor: look.bg, columns: [ { width: 'fraction', fontColor: look.ink, value: ( {schema} ), sort: { value: schema }, }, { fontColor: look.ink, value: ( {`${children.length} objects`} ), width: '100px', }, { width: '100px', value: ( ), }, { fontColor: look.ink, value: ( {totalRows.toLocaleString('en-US')} ), width: '80px', alignX: 'right', sort: { value: totalRows }, }, { width: '110px', alignX: 'right', fontColor: look.ink, value: ( {formatMb(totalMb)} ), }, ], items: children.map((child) => leafRow(child, selectedIds, toggleSelection, theme, look.ink), ), }; }), footer: { showFooter: true, showActionButton: false, height: '58px', styles: { height: '58px', minHeight: '58px' }, backgroundColor: dark ? '#121820' : '#FAFAFA', leftSideContent: ( {selectedIds.size === 0 ? 'No objects selected' : `${selectedIds.size} object${selectedIds.size === 1 ? '' : 's'} selected`} ), rightSideContent: (
), }, emptyTable: { title: 'No data', value: 'The query returned 0 rows.', }, }; } export default function Example() { const [selectedIds, setSelectedIds] = useState>(new Set()); const [theme, setTheme] = useState('dark'); const toggleSelection = (id: string) => { setSelectedIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const tableData = useMemo( () => buildTableData(rows, selectedIds, toggleSelection, theme), [selectedIds, theme], ); const totalRows = rows.reduce((sum, row) => sum + row.rows, 0); const totalMb = rows.reduce((sum, row) => sum + parseMb(row.size), 0); const dark = theme === 'dark'; const kpiValue = dark ? '#F4F4F5' : '#18181B'; const kpiBorder = dark ? '#2A3A4E' : '#E4E4E7'; const kpiBg = dark ? '#121820' : '#FAFAFA'; const kpiCard = ( card: number, title: string, value: string, color = kpiValue, ) => ({ card, title, value, settings: { fontSizeLabel: '10px', fontSizeValue: '18px', colorValue: color, borderColor: kpiBorder, backgroundColor: kpiBg, }, }); return (
); } ``` Kennzahlen und Tabelle kommen aus denselben React-Daten. Theme und Auswahl bleiben lokaler State. Für Datenzugriff, Speicherung und Navigation nutzt du deine eigene API und die Komponenten deiner Anwendung. ## Die Bausteine - `data` enthält die widgetspezifische Konfiguration. - `id` identifiziert optional die Instanz. - Callback-Props wie `onCellClick` melden Interaktionen. - Inhaltsslots erlauben eigene React-Elemente und weitere Widgets. [React einrichten](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components) · [Komponenten kombinieren](/docs/react/patterns/nested-widget-descriptors). # Was ist ein Widget? Ein **arcWidget** ist eine fertige Business-Oberfläche — Tabelle, Kalender, Kanban, Layout — die du über ein deklaratives **Datenobjekt** konfigurierst. Dieses plattformübergreifende Modell lässt dich beschreiben, *was* das Widget zeigen und tun soll, während die Engine sich um Architektur, Styling und Event-Routing kümmert. Typische Bausteine: `uniqueId`, `data`, Slots, `actions`, `styles`. In **Ninox** setzt du das Widget in ein Formelfeld. Einmal **Global Code** pro Team, dann der **Anwendungscode** dort, wo die Oberfläche erscheinen soll — ohne eigene App zu bauen. 🔧 Anpassbar im Formelfeld ✨ HTML/CSS-UI statt Ninox-Standardformulare 🚀 In wenigen Minuten eingebunden Mehr zur Einrichtung: [Widget-Integration in Ninox](/docs/intro/aufbau-in-nx-db). Beispiel: Aufgaben in einer Liste abhaken, ohne jeden Datensatz zu öffnen — [Custom Table](/docs/widgets/custom-table). ![](https://framerusercontent.com/images/wU0ZV7W9mfUZgbvDBO8vdyPuH4c.png) # Was ist ein Widget? Ein **arcWidget** ist eine fertige Business-Oberfläche — Tabelle, Kalender, Kanban, Layout — die du über ein deklaratives **Datenobjekt** konfigurierst. Dieses plattformübergreifende Modell lässt dich beschreiben, *was* das Widget zeigen und tun soll, während die Engine sich um Architektur, Styling und Event-Routing kümmert. Typische Bausteine: `uniqueId`, `data`, Slots, `actions`, `styles`. In **Vue** gilt derselbe deklarative `data`-Vertrag. Der Widget-Core sorgt für die Performance; das öffentliche Paket `@arcrider/arcwidgets-vue` übernimmt die Vue-Integration via Props und Emits. Einrichtung: [Vue einrichten](/docs/intro/setup-vue). Widget-Beispiel: [Custom Table](/docs/widgets/custom-table). --- # Widgets und Mini-Widgets Source: https://www.arc-rider.com/docs/intro/widgets-und-mini-widgets # Widgets und Mini-Widgets in React Das React-Paket enthält acht Premium-Widgets für größere Oberflächen und 14 Mini-Widgets für Eingabe, Darstellung und Layout. Beide Gruppen werden aus demselben Paket importiert. ## Premium-Widgets Table, Kanban, drei Kalenderansichten, Drawing, Upload und Slideshow decken Listen, Planung, Zeichnungen, Dateiübernahme und Präsentation ab. Für produktive Nutzung gelten die Bedingungen der Projektlizenz. ## Mini-Widgets Button, Badge, Input, Select, Signature, Icon, Text, Progress Bar, Layout, Numpad, Slider, Steps, KPI Bar und DevTools sind kleinere Bausteine. Du kannst sie eigenständig oder in den Inhaltsslots größerer Widgets verwenden. ## Widgets zusammensetzen ```tsx import { ArcWidgetBadge, ArcWidgetInput, ArcWidgetLayout, } from '@arcrider/arcwidgets-react'; const row = (number: string, title: string) => ({ value: ( ), }, { width: 'fraction', value: ( ), }, ], }} /> ), }); export default function Example() { return ( ); } ``` In React lassen sich unterstützte Slots direkt mit JSX befüllen. So bleiben die Callbacks verschachtelter Komponenten an deinem State. Die Slideshow ist eine Ausnahme: Ihre Slides verwenden aktuell String-Inhalte. [Komponentenübersicht mit allen Referenzen](/docs/react/intro/react-components) · [JSX und Deskriptoren](/docs/react/patterns/nested-widget-descriptors) · [Lizenzierung](/docs/react/faq/react-paket-und-lizenzierung). # Widgets und Mini-Widgets **Widgets** sind eigenständige Business-Oberflächen. **Mini-Widgets** sind kleinere Bausteine — allein oder in einem größeren Widget. ## Widgets Größerer Umfang, eigene Logik, oft der Kern einer Ansicht: Tabelle, Kalender, Kanban, Layout. - mehr Parameter und Zustände - eigener Use Case (Liste, Planung, Board) - meist die Fläche, in der Mini-Widgets landen Beispiel: [Custom Table](/docs/widgets/custom-table) — Zeilen, Filter, Zellen mit Mini-Widgets. In Ninox heißt der Einstieg `arcCustomTable({ … })` im Formelfeld. ![](https://framerusercontent.com/images/OUceOtboxPQac3AJ3N3DEu9yas.png) ## Mini-Widgets Schmaler Fokus: Button, Badge, Select, Fortschrittsbalken. Wenig Konfiguration, oft in Tabellen-Zellen oder Layout-Blöcken. Beispiele: - [Select](/docs/mini-widgets/select) — Dropdown mit Styling - [Progress Bar](/docs/mini-widgets/progress-bar) — Fortschritt in einer Zelle oder einem Formular In Ninox: `arcCustomSelect`, `arcCustomProgressBar`, `arcCustomButton`, … ![](https://framerusercontent.com/images/qSmv08CT1x4gwgcOSt9jEYdtJs.png) ## Widgets in Widgets Mini-Widgets sitzen in Slots größerer Widgets — Zellen, Layout-Blöcke, Karten. Auch ein volles Widget kann in einem anderen landen, zum Beispiel [Custom Layout](/docs/widgets/custom-layout) in einer [Custom Table](/docs/widgets/custom-table)-Zelle (Text + Badge + Fortschritt). Wie die verschachtelte Schreibweise aussieht: [Nested Widget Descriptors](/docs/patterns/nested-widget-descriptors). # Widgets und Mini-Widgets **Widgets** sind eigenständige Business-Oberflächen. **Mini-Widgets** sind kleinere Bausteine — allein oder in einem größeren Widget. ## Widgets Größerer Umfang, eigene Logik, oft der Kern einer Ansicht: Tabelle, Kalender, Kanban, Layout. - mehr Parameter und Zustände - eigener Use Case (Liste, Planung, Board) - meist die Fläche, in der Mini-Widgets landen Beispiel: [Custom Table](/docs/widgets/custom-table) — Zeilen, Filter, Zellen mit Mini-Widgets. In Vue heißt die Komponente `ArcWidgetTable`. ![](https://framerusercontent.com/images/OUceOtboxPQac3AJ3N3DEu9yas.png) ## Mini-Widgets Schmaler Fokus: Button, Badge, Select, Fortschrittsbalken. Wenig Konfiguration, oft in Tabellen-Zellen oder Layout-Blöcken. Beispiele: - [Select](/docs/mini-widgets/select) — Dropdown mit Styling - [Progress Bar](/docs/mini-widgets/progress-bar) — Fortschritt in einer Zelle oder einem Formular In Vue dieselben Namen wie in React, als Komponenten oder als Nested Descriptor in einem Slot. ![](https://framerusercontent.com/images/qSmv08CT1x4gwgcOSt9jEYdtJs.png) ## Widgets in Widgets Mini-Widgets sitzen in Slots größerer Widgets — Zellen, Layout-Blöcke, Karten. Auch ein volles Widget kann in einem anderen landen, zum Beispiel [Custom Layout](/docs/widgets/custom-layout) in einer [Custom Table](/docs/widgets/custom-table)-Zelle (Text + Badge + Fortschritt). Wie die verschachtelte Schreibweise aussieht: [Nested Widget Descriptors](/docs/patterns/nested-widget-descriptors). --- # React einrichten Source: https://www.arc-rider.com/docs/intro/setup-react # React einrichten arcWidgets liefert 24 konfigurierbare Komponenten für Geschäftsanwendungen. Du beschreibst die Anzeige in `data` und verbindest Interaktionen über Callback-Props mit deinem State und deiner API. ## Installation ```bash npm install --save-exact @arcrider/arcwidgets-react@0.1.0-alpha.6 ``` Diese Dokumentation gehört zu `0.1.0-alpha.6`. Das Paket benötigt React und React DOM 18 oder 19 und läuft im Browser. Die Styles sind enthalten; ein separater CSS-Import ist nicht nötig. Die Version ist eine Alpha: Prüfe deine verwendeten Workflows vor einem Update. ## Erstes Widget Dieses vollständige Beispiel kann als `App.tsx` in einer React-Browser-App verwendet werden: ```tsx import { ArcWidgetBadge, ArcWidgetKanban } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const startLanes = [ { id: 'open', title: 'Open', items: [ { id: 'brief', title: 'Prepare quote', tag: 'Today', color: '#d97706', }, ], }, { id: 'done', title: 'Done', items: [] as { id: string; title: string; tag: string; color: string }[], }, ]; export default function Example() { const [lanes, setLanes] = useState(startLanes); const [selected, setSelected] = useState('Drag or click a card'); return ( <> ({ ...lane, items: lane.items.map((item) => ({ id: item.id, title: item.title, subtitle: ( ), clickable: true, draggable: true, borderPosition: 'left', borderColor: item.color, borderWidth: '4px', })), })), }} onItemClick={(_event, { item_index }) => setSelected(`Task ${item_index + 1}`) } onItemDrop={async (payload) => { const next = lanes.map((lane) => ({ ...lane, items: [...lane.items], })); const [item] = next[payload.source_swimlane_index].items.splice( payload.item_index_in_section, 1, ); next[payload.target_swimlane_index].items.splice( payload.target_index_in_section, 0, item, ); setLanes(next); setSelected('Card moved'); }} />

{selected}

); } ``` ## Daten und Events `data` enthält die widgetspezifische Konfiguration. Importiere bei Bedarf den zugehörigen Datentyp aus demselben Paket, etwa `ArcWidgetKanbanData`. `id` auf der Komponente ist optional und identifiziert die Widget-Instanz. Callbacks liegen auf der Komponente. Beim Kanban meldet `onItemClick` ein Browser-Event und die Position der Karte. Für mehrere Spalten oder Gruppen nutzt du alle passenden Indizes des Payloads. Der allgemeine Fallback ist `onAction(type, ...args)`; ein konkreter Callback hat Vorrang. Datenzugriff, Speicherung, Navigation und Fehlerbehandlung liegen in deiner Anwendung. Unterstützte Inhaltsslots erlauben JSX, sodass du eigene Komponenten und weitere Widgets kombinieren kannst. ## Vollständige Beispiele und KI-Kontext Das Paket enthält die Quellen aller Widget- und Anwendungsbeispiele unter `examples/widgets/`. Jede Datei exportiert eine vollständige React-Komponente. Der Markdown-Link einer React-Seite liefert denselben React-Inhalt. Für KI-Assistenten gibt es den [React-Index](/docs/react/llms.txt) und den [vollständigen React-Export](/docs/react/llms-full.txt). ## Weiter - [Alle 24 Komponenten](/docs/react/intro/react-components) - [State und Events](/docs/react/patterns/react-state-events) - [JSX und verschachtelte Widgets](/docs/react/patterns/nested-widget-descriptors) - [Dashboard-Vorlage](/docs/react/templates/react-starter) - [Server Rendering und Fehlerbehebung](/docs/react/faq/react-troubleshooting) - [Paket und Lizenzierung](/docs/react/faq/react-paket-und-lizenzierung) --- # Vue einrichten Source: https://www.arc-rider.com/docs/intro/setup-vue # Vue einrichten Arc Rider Widgets bieten einen Vue 3 Support als direktes npm-Paket. Die deklarative Widget-Konfiguration (`data`-Objekt) wird als Prop an die Komponente übergeben, während Events sauber als Emits zurückkommen. ## Paket ```bash npm install @arcrider/arcwidgets-vue ``` Das öffentliche Vue-Paket exportiert dieselben Widgets und folgt der standardisierten Arc Rider API. ## Erstes Widget ```vue ``` Stabile IDs (`id` auf der Komponente und `uniqueListId` / Zeilen-`id` im Objekt) sind Pflicht — sonst triggert die Engine einen vollen Re-Render. ## Weiter - [Custom Table](/docs/widgets/custom-table) — Parameter-Referenz und Beispiele - [Custom Layout](/docs/widgets/custom-layout) - [Button](/docs/mini-widgets/buttons) --- # Widget-Integration in deine Datenbank Source: https://www.arc-rider.com/docs/intro/aufbau-in-nx-db # Widget-Integration in deine Ninox-Datenbank Damit ein arcWidget in deiner Datenbank funktioniert, sind zwei Elemente wichtig: 1. Ein **Global Code** – die technische Basis des Widgets (einmalig pro Ninox-Team) 2. Ein **Anwendungscode** – deine individuelle Konfiguration im Ninox-Funktionsfeld --- ## 1. Was ist der Global Code / Widget Code? Der **Global Code** bildet das technische Fundament für deine Widgets. Er enthält die Kernfunktionen, die globale Steuerung und die Programmlogik, die die Widgets zum Leben erweckt. Du musst diesen Code **nur einmal pro Ninox-Team** in den globalen Funktionen hinterlegen. So gehst du vor: 1. Öffne deine Ninox-Datenbank. 2. Klicke im Admin-Modus auf die **drei Punkte** (Zahnrad) neben dem Datenbanknamen. 3. Wähle **„Optionen“ > „Globale Funktionen“**. 4. Kopiere den **Build-Code aus deinem Universe-Projekt** und füge ihn ganz unten ein. Der Global Code **ist** dieser Build: ein Paket mit allen Widgets des Projekts, einmal pro Ninox-Team. 💡 **Wichtig:** Ohne diesen Global Code können die Formelfelder das Widget nicht laden. Was ein Build ist und wie Updates laufen: [Ninox Builds einbauen und aktualisieren](/docs/faq/wie-man-builds-einbaut-und-updatet). --- ## 2. Was ist der Anwendungscode? Der **Anwendungscode** ist deine Konfiguration, mit der du das Widget **in deiner Datenbank anzeigst und steuerst**. Diesen Code fügst du in ein normales **Ninox-Funktionsfeld** (Formelfeld) ein – genau dort, wo die Oberfläche erscheinen soll. Im Anwendungscode bestimmst du: - Welche Tabellen-Daten das Widget laden soll. - Welche Filter, Farben, Abstände oder Optionen gelten. - Wie das Widget auf Klicks und Änderungen reagiert (Event-Routing). Du steuerst also über einfache Formel-Parameter, wie sich das Widget im konkreten Kontext verhält, während die Engine im Hintergrund das Zeichnen und die Performance übernimmt. --- # React-Komponenten Source: https://www.arc-rider.com/docs/intro/react-components # React-Komponenten Diese Komponenten sind in [@arcrider/arcwidgets-react auf npm](https://www.npmjs.com/package/@arcrider/arcwidgets-react), Version `0.1.0-alpha.5`, enthalten. Importiere sie direkt aus dem Paket. Styles und TypeScript-Typen werden mitgeliefert. Neu hier? Beginne mit [Installation und erstem Widget](/docs/react/intro/setup-react). ## Business-Widgets - `ArcWidgetTable` — [Table](/docs/react/widgets/custom-table) - `ArcWidgetKanban` — [Kanban](/docs/react/widgets/custom-kanban) - `ArcWidgetCalendarWeek` — [Calendar Week](/docs/react/widgets/custom-calendar-week) - `ArcWidgetCalendarGrid` — [Calendar Grid](/docs/react/widgets/custom-calendar-grid) - `ArcWidgetCalendarTimeline` — [Calendar Timeline](/docs/react/widgets/custom-calendar-timeline) - `ArcWidgetDrawing` — [Drawing](/docs/react/widgets/custom-drawing) - `ArcWidgetUpload` — [Upload](/docs/react/widgets/custom-upload) - `ArcWidgetSlideshow` — [Slideshow](/docs/react/widgets/custom-slideshow) ## Mini-Widgets - `ArcWidgetBadge` — [Badge](/docs/react/mini-widgets/badges) - `ArcWidgetButton` — [Button](/docs/react/mini-widgets/buttons) - `ArcWidgetDevTools` — [DevTools](/docs/react/widgets/custom-dev-tools) - `ArcWidgetGrid` — [Grid](/docs/react/widgets/custom-grid) - `ArcWidgetIcon` — [Icon](/docs/react/mini-widgets/custom-icon) - `ArcWidgetImage` — [Image](/docs/react/mini-widgets/images) - `ArcWidgetInput` — [Input](/docs/react/mini-widgets/input) - `ArcWidgetKpiBar` — [KPI Bar](/docs/react/widgets/custom-kpi-bar) - `ArcWidgetLayout` — [Layout](/docs/react/widgets/custom-layout) - `ArcWidgetNumpad` — [Numpad](/docs/react/widgets/custom-numpad) - `ArcWidgetProgressBar` — [Progress Bar](/docs/react/mini-widgets/progress-bar) - `ArcWidgetSelect` — [Select](/docs/react/mini-widgets/select) - `ArcWidgetSignature` — [Signature](/docs/react/mini-widgets/signature) - `ArcWidgetSlider` — [Slider](/docs/react/widgets/custom-slider) - `ArcWidgetSteps` — [Steps](/docs/react/widgets/custom-steps) - `ArcWidgetText` — [Text](/docs/react/mini-widgets/text) ## Konfiguration und Events Jedes Widget hat ein eigenes `data`-Format und einen passenden TypeScript-Typ. Eine Tabelle verwendet `table`, ein Kanban-Board `swimlanes`. Verwende die mit deiner Paketversion ausgelieferten Typen zur Prüfung deiner Konfiguration. Callback-Props verbinden Interaktionen mit deiner Anwendung. Beim Kanban sind das beispielsweise `onItemClick` und `onItemDrop`. Der allgemeine Fallback lautet `onAction(type, ...args)`; das erste Argument ist der Event-Name, kein Action-Objekt. Die React-Referenzen enthalten vollständige Beispiele, Parameter, Callback-Signaturen und bekannte Grenzen. Datenzugriff und Speichern implementierst du mit deinem React-State und deiner API. Quellen aller Beispiele liegen im Paket unter `examples/widgets/`. ## Lizenz und Updates Mini-Widgets sind im Rahmen der Nutzungsbedingungen auch produktiv kostenlos. Premium-Widgets kannst du für Entwicklung und Evaluation nutzen; für den produktiven Einsatz benötigst du eine Projektlizenz. Details: [React-Paket und Lizenzierung](/docs/react/faq/react-paket-und-lizenzierung). Der Release ist eine Alpha. Prüfe bei Updates die Typen, Callback-Namen und deine eigenen Workflows. --- # Das Universe Portal Source: https://www.arc-rider.com/docs/intro/user-cockpit # Das Universe Portal Das **Universe Portal** ist die Schaltzentrale für deine Arc-Rider-Projekte. Anmeldung: [universe.arc-rider.com](https://www.arc-rider.com/). Im Portal verwaltest du: - **Projekte:** Getrennte Projekte für Kunden oder eigene Apps — Lizenz, Build und Umgebungen bleiben sauber getrennt. - **Widgets & Builds:** Katalog zusammenstellen. Universe bündelt das in **einen** Code-Build für dein Ninox-Team. - **Lizenzen:** Free (Mini-Widgets) oder Ultimate (alle Premium-Widgets, einmalig). - **Umgebungen:** Private-Cloud-URL, On-Premise-URL oder Public-Cloud-**Team-ID**. Nur hinterlegte Produktions-Umgebungen laufen ohne Lizenzhinweis. ## Wo finde ich den Widget-Code? In der Projekt-Detailansicht liegen die Builds. Code kopieren oder als Datei laden, dann in Ninox einfügen. Weiter: [Widget-Integration in deine Datenbank](/docs/intro/aufbau-in-nx-db). Im Portal verwaltest du: - **Projekte:** Eines pro React-App — Lizenz und Produktions-Domains bleiben getrennt. - **Lizenzen:** Free für Mini-Widgets oder eine Ultimate-Projektlizenz für die produktive Nutzung der Premium-Widgets. Details stehen unter [Preise](/docs/react/faq/wie-sich-preise-zusammensetzen). - **Produktions-Domains:** Die Domain-Zuordnung deines Projekts. Die technische Aktivierung des npm-Pakets ist im aktuellen Alpha-Stand ein zusätzlicher Schritt und wird nicht automatisch aus dem Portal übernommen. Prüfe auch öffentlich erreichbare Test-Domains. Die Komponenten, Styles und Typen installierst du über das React-npm-Paket. Lokale Entwicklung auf `localhost` oder `127.0.0.1` zeigt keinen Lizenzhinweis. Weiter: [React einrichten](/docs/react/intro/setup-react) · [Paket und technische Aktivierung](/docs/react/faq/react-paket-und-lizenzierung). Im Portal verwaltest du: - **Projekte:** Eines pro Vue-App — Lizenz und Produktions-Domains bleiben getrennt. - **Lizenzen:** Free oder Ultimate, wie bei React. - **Produktions-Domains:** Nur hinterlegte Live-Domains laufen ohne Lizenzhinweis. Die Widgets kommen über `npm install @arcrider/arcwidgets-vue`. Weiter: [Vue einrichten](/docs/intro/setup-vue). --- # ADK Setup Guide Source: https://www.arc-rider.com/docs/intro/adk-setup-guide # ADK Setup Guide ## Dein AI-Editor kennt ab jetzt deine Ninox-Datenbank – Setup in 10 Minuten Du baust Ninox-Oberflächen mit Arc Rider Widgets und wünschst dir, dass dir jemand beim Code schreiben hilft? Jemand, der deine Datenbank kennt, die richtige Syntax beherrscht und weiß, wie die Widgets funktionieren? Genau das macht das Arc Rider Developer Kit (ADK). Es verbindet einen AI-Editor mit deiner Ninox-Datenbank und unserer Widget-Dokumentation. Du beschreibst auf Deutsch, was du willst – die AI schreibt den Code. Keine Programmierkenntnisse nötig. Kein Terminal-Wissen vorausgesetzt. In diesem Guide gehen wir jeden Schritt gemeinsam durch. Der Weg hier ist Cursor. Dieselben Befehle gelten für Claude Code. ![ADK Setup Guide: Projektordner und fertiger Widget-Code in Cursor](/docs-assets/adk/01-folder.png) ## Was du am Ende hast Nach diesem Guide hast du auf deinem Computer einen Projektordner mit allem, was die AI braucht: ```text mein-ninox-projekt/ ├── .cursor/rules/ ← Regeln für die AI (NinoxScript-Syntax, Widget-Properties) ├── docs/ ← Widget-Dokumentation (Referenz für die AI) ├── ninox/ ← Dein Datenbankschema (Tabellen, Felder, Relationen) ├── spaces/ ← Hier landen deine Widget-Konfigurationen (.arc-Dateien) └── package.json ``` Die AI liest diese Dateien automatisch im Hintergrund. Du beschreibst, was du bauen willst, und die AI nutzt den Kontext. ## Voraussetzungen Bevor du loslegst, brauchst du: - Ninox-Account mit aktiven Arc Rider Widgets – [Widgets bestellen](/widgets) - [Node.js](https://nodejs.org) (Version 18+) – einmal installieren, danach läuft es im Hintergrund. Nimm die LTS-Version. ![Node.js, Ninox und Arc Rider](/docs-assets/adk/02-nodejs.png) ## Schritt 1: Cursor installieren [Cursor](https://cursor.com) ist ein Code-Editor mit eingebauter AI. Du beschreibst, was du willst – die AI schreibt den Code. - Die Dateien bleiben auf deinem Rechner. - Liegt dein Ninox-Schema lokal, kennt die AI deine Tabellen und Felder. - Die kostenlose Version reicht für den Einstieg. So installierst du Cursor: 1. Gehe zu [cursor.com](https://cursor.com). 2. Lade die Version für dein Betriebssystem herunter. 3. Installiere das Programm. 4. Erstelle einen kostenlosen Account. ![Cursor herunterladen](/docs-assets/adk/03-cursor.png) ## Schritt 2: Projektordner anlegen Erstelle einen neuen, leeren Ordner, zum Beispiel `mein-ninox-projekt`. Auf dem Mac eignet sich der Ordner `Sites`. Öffne den Ordner in Cursor: **File → Open Folder**, oder ziehe den Ordner ins Fenster. ![Projektordner in Cursor öffnen](/docs-assets/adk/04-open-folder.png) ## Schritt 3: ADK installieren Öffne das Terminal in Cursor (**Terminal → New Terminal**, oder `Ctrl` + `` ` ``). ```bash npm install @arcrider/developer-kit npx arc-rider-init ``` `npm install` lädt das ADK. Darin stecken die Widget-Dokumentation, die AI-Regeln und das Ninox-Werkzeug für dein Datenbankschema. Ein zweites Paket brauchst du nicht. `arc-rider-init` legt danach die Projektstruktur an: - `.cursor/rules/` – AI-Regeln für NinoxScript und Widget-Properties - `docs/` – die Widget-Referenz - `ninox/` – hier landet gleich dein Datenbankschema - `spaces/` – hier landen deine Widget-Konfigurationen, zum Beispiel `spaces/dashboard/main.arc` Danach siehst du die neuen Ordner in der Sidebar. ![ADK-Installation im Terminal und die neuen Ordner in der Sidebar](/docs-assets/adk/05-install.png) ## Schritt 4: Ninox-Zugangsdaten eintragen ### API-Key erstellen 1. Öffne deine Ninox-Instanz im Browser. 2. Klicke oben rechts auf dein Profil-Icon. 3. Gehe zu **Einstellungen → API Keys**. 4. Erstelle einen neuen API-Key und kopiere ihn. API-Keys geben vollen Zugriff auf deine Ninox-Daten. Teile sie nicht öffentlich. Die `config.yaml` bleibt lokal und wird nicht ins Git committed. ![API-Key in den Ninox-Einstellungen anlegen](/docs-assets/adk/06-api-key.png) ### Domain, Workspace-ID und Datenbank-ID Die IDs stehen in der Browser-URL, wenn du die Datenbank öffnest. **Public Cloud** (`app.ninox.com`): - Beispiel: `https://app.ninox.com/#/teams/Xk7mNpQr2sT4wYz/database/a9b3c7d2e1f/` - Workspace-ID: der Teil nach `teams/` - Datenbank-ID: der Teil nach `database/` - Domain in der Config: immer `https://api.ninox.com` (nicht `app.ninox.com`) **Private Cloud:** - Beispiel: `https://meinefirma.ninoxdb.com/#/teams/abc123/database/xyz789/` - Domain: deine API-URL, zum Beispiel `https://meinefirma.ninoxdb.com` - Workspace-ID und Datenbank-ID wie oben aus der URL ### Konfigurationsdatei `arc-rider-init` hat `ninox/config.yaml` schon angelegt. Trage deine Daten unter `environments.dev` ein: ```yaml environments: dev: domain: https://api.ninox.com apiKey: dein-api-key workspaceId: DEINE_WORKSPACE_ID databases: - id: "DEINE_DATENBANK_ID" name: "Mein Projekt" ``` In der Browser-URL siehst du `app.ninox.com`. Das ist nur die Oberfläche. Für die API nimmst du `https://api.ninox.com`. ![Zugangsdaten in ninox/config.yaml](/docs-assets/adk/07-config.png) ## Schritt 5: Datenbankschema laden ```bash npm run ninox:download ``` Bei mehreren Datenbanken in der Config erscheint eine Auswahl. Bei einer Datenbank wird sie automatisch genommen. Optional: `--db=DEINE_DB_ID`. Danach liegt unter `ninox/src/Objects/` für jede Tabelle eine Datei mit Feldern, Typen und Relationen. Die AI kennt ab dann die echten Namen aus deiner Datenbank. ![Heruntergeladenes Ninox-Schema in Cursor](/docs-assets/adk/08-schema.png) ## Dein erster Prompt Öffne den Agent in Cursor (`Cmd+L` auf dem Mac, `Ctrl+L` unter Windows) und probiere zum Beispiel: ```text Erstelle einen neuen Space "dashboard" und baue darin ein Dashboard-Layout für die Tabelle "Aufträge". Oben eine KPI-Bar mit: Anzahl offener Aufträge, Gesamtbetrag, Durchschnittsbetrag. Darunter eine Tabelle mit den Spalten: Bezeichnung, Kunde, Betrag, Status als Badge. ``` Die AI legt `spaces/dashboard/main.arc` an. Jedes Feature bekommt einen eigenen Space. `shared/` ist für Farben, Schrift und Bausteine, die mehrere Spaces nutzen. Die AI liest die Regeln, dein Schema und die Widget-Docs und schreibt den Code nach `spaces/`. ![Erster Prompt und generierter Widget-Code](/docs-assets/adk/09-prompt.png) ## Code in Ninox einfügen 1. Öffne `spaces/dashboard/main.arc`. 2. Kopiere den gesamten Code. 3. Füge ihn in Ninox in ein Funktionsfeld ein, dort wo das Widget erscheinen soll. ![Fertiges Widget in Ninox](/docs-assets/adk/10-ninox.png) Wer das Terminal mag, kann den Code auch direkt schieben: Mapping in `ninox/ninox-mapping.yaml`, danach `npm run ninox:push`. `ninox:upload` kann die Datenbankstruktur verändern. Für den normalen Widget-Workflow brauchst du das nicht. Es reichen `ninox:download` (lesen) und `ninox:push` (Code ins Formelfeld). ## Tipps für bessere Prompts Nenne Tabellen und Felder: ```text Nutze die Tabelle "Kontakte" und zeige die Felder "Name", "E-Mail", "Telefon" und "Status". ``` Beschreibe das Design und ändere in kleinen Schritten nach: ```text Füge der Tabelle eine Pagination mit 20 Einträgen pro Seite hinzu. Die Spalte "Betrag" soll als formatierte Zahl mit € angezeigt werden. ``` ## Häufige Fragen **Muss ich programmieren können?** Nein. Du beschreibst auf Deutsch, was du willst. Die AI schreibt den Code. Du fügst ihn in Ninox ein. **Was ist npm?** npm kommt mit Node.js. Es lädt Pakete herunter. Du brauchst es für die Einrichtung. Danach arbeitest du in Cursor. **Brauche ich die kostenpflichtige Cursor-Version?** Nein. Die kostenlose Version reicht für den Einstieg. **Was, wenn sich mein Schema ändert?** Führe `npm run ninox:download` erneut aus. Die AI kennt danach die neuen Felder. **Geht das ohne Arc Rider Widgets?** Nein. Ohne die Widgets fehlt der Global Code in Ninox, auf dem die Konfigurationen aufbauen. Du brauchst ein aktives Widget-Abo. Bestellen kannst du im [Widget Store](/widgets). ## Zusammenfassung | Schritt | Was passiert | Dauer | | --- | --- | --- | | 1. Cursor installieren | AI-Editor herunterladen | 2 Min | | 2. Ordner anlegen | Neuen Projektordner erstellen | 30 Sek | | 3. ADK einrichten | Paket installieren und Struktur anlegen | 2 Min | | 4. Zugangsdaten | API-Key, Domain, Workspace-ID | 3 Min | | 5. Schema laden | Datenbankstruktur herunterladen | 1 Min | | Erster Prompt | AI schreibt den Widget-Code | Sofort | ```bash npm install @arcrider/developer-kit npx arc-rider-init npm run ninox:download ``` Dein AI-Editor kennt jetzt deine Datenbank, deine Widgets und die richtige Syntax. --- # Component ProgressList Source: https://www.arc-rider.com/docs/intro/component-progresslist # Component ProgressList ```javascript arcComponentProgressList({ items: [{ title: "Lagername", valueTotal: 2000, valueProgress: 100, valueText: 100 + " / " + 2000 }, { title: "Lagername 2", valueTotal: 5000, valueProgress: 2000, valueText: 2000 + " / " + 5000 }] }) ``` ```javascript arcComponentProgressList({ items: list_Lager.[{ title: Lagername, valueTotal: 'Lagerkapazität', valueProgress: sum_lagerbestand, valueText: sum_lagerbestand + " / " + 'Lagerkapazität' }] }); ``` --- # Custom Calendar (Week) Source: https://www.arc-rider.com/docs/widgets/custom-calendar-week # CalendarWeek in React Termine in einer Wochenansicht mit Stundenraster anzeigen. Das Beispiel hält Auswahl und Termine in der Anwendung. Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.5`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components). ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetCalendarWeek } from '@arcrider/arcwidgets-react'; import type { ArcWidgetCalendarWeekCreatePayload, ArcWidgetCalendarWeekDailyEntryDragSavePayload, ArcWidgetCalendarWeekDragSourcePayload, ArcWidgetCalendarWeekEntryDragSavePayload, ArcWidgetCalendarWeekTimeEntry, } from '@arcrider/arcwidgets-react'; const DAY = 86400000; const monday = new Date(2027, 0, 4).getTime(); const day = (index: number) => monday + index * DAY; const at = (hours: number, minutes = 0) => (hours * 60 + minutes) * 60000; const initialEntries: ArcWidgetCalendarWeekTimeEntry[] = [ { title: 'Team Offsite', subtitle: '2 days', dateFrom: day(0), dateTo: day(1), draggable: true, }, { title: 'Public Holiday', subtitle: 'All-day', dateFrom: day(2), dateTo: day(2), draggable: true, }, { title: 'Team Meeting', subtitle: '09:00 - 10:30', dateFrom: day(0), dateTo: day(0), timeFrom: at(9), timeTo: at(10, 30), draggable: true, }, { title: 'Code Review', subtitle: '10:00 - 11:00', dateFrom: day(1), dateTo: day(1), timeFrom: at(10), timeTo: at(11), draggable: true, }, { title: 'Workshop', subtitle: '13:30 - 17:00', dateFrom: day(1), dateTo: day(1), timeFrom: at(13, 30), timeTo: at(17), insetBorder: { stroke: 'dotted' }, draggable: true, }, { title: 'Sprint Planning', subtitle: '09:00 - 12:00', dateFrom: day(4), dateTo: day(4), timeFrom: at(9), timeTo: at(12), draggable: true, }, ]; export default function Example() { // Only the visible week, hours and entries are set; look and behavior are widget defaults. // Entries without times span whole days: one day (Public Holiday) or several (Team Offsite). // Inset line: every timed entry shows a vertical line on its left edge (all-day entries do not). // It takes the entry's `styles.color`, or #18181b. `insetBorder` on an entry changes that entry: // `stroke` ('solid', 'dashed' or 'dotted'), `width`, `color`, `position` ('left' or 'right'), // `enabled: false` hides it. Put `insetBorder` on `data` to apply it to all entries // (see the Minimalistic example); fields set on an entry win over it. // // Drag and save: `draggable: true` lets an entry be moved. The calendar only reports the drop. // Write `endData` into the entry at `source.entry_index`, then save it to your API. // If saving fails, put `startData` back. Without this step the entry jumps back on the next update. // // Create: `createNew` turns on drawing a new range over empty slots (or over the all-day row). // `onCreate` reports it; your app validates it, gives it an id and saves it. Without `createNew` // dragging over empty slots does nothing. const [entries, setEntries] = useState(initialEntries); const persist = ( source: ArcWidgetCalendarWeekDragSourcePayload, endData: ArcWidgetCalendarWeekEntryDragSavePayload | ArcWidgetCalendarWeekDailyEntryDragSavePayload, ) => { setEntries((current) => current.map((entry, index) => (index === source.entry_index ? { ...entry, ...endData } : entry)), ); }; const create = (values: ArcWidgetCalendarWeekCreatePayload) => { const allDay = values.timeFrom === undefined; setEntries((current) => [ ...current, { id: `new-${current.length + 1}`, title: 'New entry', subtitle: allDay ? 'All-day' : undefined, ...values, draggable: true, }, ]); }; return ( persist(source, endData)} onDailyEntryDragSave={(_entryIndex, _startData, endData, source) => persist(source, endData)} /> ); } ``` ## Features ausprobieren ### Tagesansichten, Terminbearbeitung und JSX Ein gemeinsamer State steuert die Zahl der Tage und den Terminbeginn. Drag persistiert über `onEntryDragSave` mit **`endData`**. ```tsx import { useState } from 'react'; import { ArcWidgetCalendarWeek } from '@arcrider/arcwidgets-react'; import type { ArcWidgetCalendarWeekCreatePayload, ArcWidgetCalendarWeekDailyEntryDragSavePayload, ArcWidgetCalendarWeekDragSourcePayload, ArcWidgetCalendarWeekEntryDragSavePayload, ArcWidgetCalendarWeekTimeEntry, } from '@arcrider/arcwidgets-react'; const DAY = 86400000; const monday = new Date(2027, 0, 4).getTime(); const day = (index: number) => monday + index * DAY; const at = (hours: number, minutes = 0) => (hours * 60 + minutes) * 60000; const initialEntries: ArcWidgetCalendarWeekTimeEntry[] = [ { title: 'Team Offsite', subtitle: '2 days', dateFrom: day(0), dateTo: day(1), draggable: true, }, { title: 'Public Holiday', subtitle: 'All-day', dateFrom: day(2), dateTo: day(2), draggable: true, }, { title: 'Team Meeting', subtitle: '09:00 - 10:30', dateFrom: day(0), dateTo: day(0), timeFrom: at(9), timeTo: at(10, 30), draggable: true, }, { title: 'Code Review', subtitle: '10:00 - 11:00', dateFrom: day(1), dateTo: day(1), timeFrom: at(10), timeTo: at(11), draggable: true, }, { title: 'Workshop', subtitle: '13:30 - 17:00', dateFrom: day(1), dateTo: day(1), timeFrom: at(13, 30), timeTo: at(17), insetBorder: { stroke: 'dotted' }, draggable: true, }, { title: 'Sprint Planning', subtitle: '09:00 - 12:00', dateFrom: day(4), dateTo: day(4), timeFrom: at(9), timeTo: at(12), draggable: true, }, ]; export default function Example() { // Only the visible week, hours and entries are set; look and behavior are widget defaults. // Entries without times span whole days: one day (Public Holiday) or several (Team Offsite). // Inset line: every timed entry shows a vertical line on its left edge (all-day entries do not). // It takes the entry's `styles.color`, or #18181b. `insetBorder` on an entry changes that entry: // `stroke` ('solid', 'dashed' or 'dotted'), `width`, `color`, `position` ('left' or 'right'), // `enabled: false` hides it. Put `insetBorder` on `data` to apply it to all entries // (see the Minimalistic example); fields set on an entry win over it. // // Drag and save: `draggable: true` lets an entry be moved. The calendar only reports the drop. // Write `endData` into the entry at `source.entry_index`, then save it to your API. // If saving fails, put `startData` back. Without this step the entry jumps back on the next update. // // Create: `createNew` turns on drawing a new range over empty slots (or over the all-day row). // `onCreate` reports it; your app validates it, gives it an id and saves it. Without `createNew` // dragging over empty slots does nothing. const [entries, setEntries] = useState(initialEntries); const persist = ( source: ArcWidgetCalendarWeekDragSourcePayload, endData: ArcWidgetCalendarWeekEntryDragSavePayload | ArcWidgetCalendarWeekDailyEntryDragSavePayload, ) => { setEntries((current) => current.map((entry, index) => (index === source.entry_index ? { ...entry, ...endData } : entry)), ); }; const create = (values: ArcWidgetCalendarWeekCreatePayload) => { const allDay = values.timeFrom === undefined; setEntries((current) => [ ...current, { id: `new-${current.length + 1}`, title: 'New entry', subtitle: allDay ? 'All-day' : undefined, ...values, draggable: true, }, ]); }; return ( persist(source, endData)} onDailyEntryDragSave={(_entryIndex, _startData, endData, source) => persist(source, endData)} /> ); } ``` ### Segmente der Inset-Linie Teile die Inset-Linie eines Eintrags in `segments`, zum Beispiel eine Farbe pro Person. Ein Segment mit Zeit (`timeFrom` + `timeTo` oder `offset` + `duration`) sitzt dort, wo die Zeit liegt; überlappende Segmente werden nebeneinander gezeichnet. Ohne Zeiten teilen sich die Segmente die Linie nach `size`. `label` erscheint als Tooltip. ```tsx import { ArcWidgetCalendarWeek } from '@arcrider/arcwidgets-react'; import type { ArcWidgetCalendarWeekTimeEntry } from '@arcrider/arcwidgets-react'; const DAY = 86400000; const monday = new Date(2027, 0, 4).getTime(); const day = (index: number) => monday + index * DAY; const at = (hours: number, minutes = 0) => (hours * 60 + minutes) * 60000; const minutes = (count: number) => count * 60000; const people = { anna: { label: 'Anna', color: '#DC2626' }, ben: { label: 'Ben', color: '#2563EB' }, clara: { label: 'Clara', color: '#16A34A' }, }; // The inset line of an entry can be cut into `segments`, for example one per person. // Each segment has its own `color` and a `label` that appears as a tooltip. const entries: ArcWidgetCalendarWeekTimeEntry[] = [ { // Segments with clock times sit where their time is. Where two overlap (10:00 - 10:30) // they are drawn side by side. Time with no segment (11:30 - 12:00) stays empty: nobody is assigned. title: 'Team Meeting', subtitle: 'Anna and Ben', dateFrom: day(0), dateTo: day(0), timeFrom: at(9), timeTo: at(12), insetBorder: { segments: [ { ...people.anna, timeFrom: at(9), timeTo: at(10, 30) }, { ...people.ben, timeFrom: at(10), timeTo: at(11, 30) }, ], }, }, { // `offset` counts from the start of the entry, so these segments move with it. title: 'Customer Workshop', subtitle: 'Handover between two people', dateFrom: day(1), dateTo: day(1), timeFrom: at(13), timeTo: at(16), insetBorder: { segments: [ { ...people.clara, offset: 0, duration: minutes(90) }, { ...people.ben, offset: minutes(90), duration: minutes(90) }, ], }, }, { // Without times the segments share the line: 'fraction' (or 'auto') splits what is left equally, // '25%' or '24px' are fixed. `stroke` styles all segments that do not set their own. title: 'Planning', subtitle: 'Three people, one share each', dateFrom: day(2), dateTo: day(2), timeFrom: at(9), timeTo: at(11), insetBorder: { stroke: 'dotted', segments: [ { ...people.anna }, { ...people.ben }, { ...people.clara, stroke: 'solid', size: '25%' }, ], }, }, { title: 'Site Visit', subtitle: 'Default line', dateFrom: day(3), dateTo: day(3), timeFrom: at(10), timeTo: at(12), }, ]; export default function Example() { return ( ); } ``` ## Komponenten-Props ### id Optionale DOM-ID. Im Normalfall nicht nötig; ohne Angabe wird sie automatisch erzeugt. ### data Konfiguration vom Typ `ArcWidgetCalendarWeekData`. Übergib Änderungen als neue Objekte; verwalte fachliche Daten und Auswahl in React-State. ### onAction Optionaler Fallback `(type: string, ...args: unknown[]) => unknown`. Der konkrete Callback hat Vorrang. Details: [State und Events](/docs/react/patterns/react-state-events). ### onEntryClick 0-basierter Index des geklickten Stücks. Feuert nur bei Einträgen mit `clickable: true`. Segmente und Splits werden zu einzelnen Stücken aufgefaltet; der Index entspricht `data.timeEntries` nur bei Einträgen ohne `segments`. ```typescript onEntryClick?: (entryIndex: number) => void; ``` ### onDayClick Gewählter Tag als Epoch-Millisekunden um 00:00 UTC. Feuert beim Klick auf den Tageskopf mit `dateSettings.day.clickable: true`. ```typescript onDayClick?: (dateMillis: number) => void; ``` ### onEntryDragSave Drag-Ende eines Stücks mit Uhrzeit: `startData` (vorher) und `endData` (Drop-Ziel) — persistiere `endData`. `source` verortet das Stück in deinen `timeEntries` (`entry_index`, `segment_index`, `split_index`). Bewegt sich eine Segmentgruppe gemeinsam, feuert der Callback einmal pro bewegtem Stück. ```typescript onEntryDragSave?: ( entryIndex: number, startData: ArcWidgetCalendarWeekEntryDragSavePayload, endData: ArcWidgetCalendarWeekEntryDragSavePayload, source: ArcWidgetCalendarWeekDragSourcePayload ) => void; ``` ### onDailyEntryDragSave Drag-Ende eines ganztägigen Termins: `startData` und `endData` nur mit `dateFrom` / `dateTo` — persistiere `endData`. ```typescript onDailyEntryDragSave?: ( entryIndex: number, startData: ArcWidgetCalendarWeekDailyEntryDragSavePayload, endData: ArcWidgetCalendarWeekDailyEntryDragSavePayload, source: ArcWidgetCalendarWeekDragSourcePayload ) => void; ``` ### onCreate Neuer Bereich, über leere Slots gezogen, oder ein platziertes `createNew.item`. Benötigt `createNew`. Bei einem ganztägigen Bereich fehlen `timeFrom` / `timeTo`. Validieren und Speichern übernimmt die Anwendung. ```typescript onCreate?: (values: ArcWidgetCalendarWeekCreatePayload) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### width Breite des Widgets, z. B. `"100%"`. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### styles Rahmen, Rasterlinien, Kopfzeilenfläche und Texttöne (`borders`, `borderColor`, `borderSize`, `borderRadius`, `backgroundColor`, `headerBackgroundColor`, `fontColor`, `mutedColor`, `subtleColor`). Typ: `ArcWidgetCalendarWeekStyles`. ### weekdaySettings ISO-Kalenderwoche und Jahr, wenn `dateSettings.startDate` fehlt, z. B. `{ week: 1, year: 2027 }`. Der Bereich beginnt dann um 00:00 UTC am ersten Tag dieser Woche. Typ: `ArcWidgetCalendarWeekdaySettings`. ### weekStart Erster Wochentag für `weekdaySettings` und die Standardwoche: `1` = Montag … `7` = Sonntag. Standard: `1`. Typ: `number`. ### dateSettings Sichtbarer Bereich, Tageskopf und Markierungen. `startDate` (Epoch-Millisekunden) legt den ersten sichtbaren Tag fest und hat Vorrang vor `weekdaySettings`; Termindaten werden relativ dazu platziert. `endDate` (exklusiv) oder `duration` (Tage, Standard `7`) legt die Länge fest. Samstag und Sonntag sind standardmäßig leicht grau getönt; eine Markierung für diese Tage ersetzt die Farbe, `backgroundColor: 'transparent'` entfernt sie. `weekends: false` blendet Samstag und Sonntag von Anfang an aus (im Einstellungsmenü lassen sie sich wieder einblenden). Typ: `ArcWidgetCalendarWeekDateSettings`. ### timeZoneBalance Stundenversatz, der auf die Stundenlabels und Uhrzeit-Labels der Termine addiert wird, z. B. `-1`; die Position der Termine bleibt gleich. Standard: `0`. Typ: `number`. ### timeSettings Sichtbare Stunden des Tages: `timeStart` und `duration` in Millisekunden (`8 * 3600000` = 08:00). Standard: der ganze Tag. Typ: `ArcWidgetCalendarWeekTimeSettings`. ### currentTime `true` zeigt eine Linie für die aktuelle Uhrzeit; ein Farbstring wie `"#f95757"` setzt zusätzlich ihre Farbe. Standard: `false`. Typ: `boolean | string`. ### dragStep Rasterschritt beim Ziehen und Erstellen in Millisekunden. Standard: `300000` (5 Minuten). Typ: `number`. ### lang Locale für Wochentags- und Monatsnamen sowie die Toolbar. Standard: `"de"`. Typ: `string`. ### timeSlotWidth Mindestbreite einer Tagesspalte, z. B. `"200px"`. Typ: `string`. ### timeSlotHeight Mindesthöhe einer Stundenzeile. Standard: `"50px"`. Ist der Kalender höher, als alle Stunden bei dieser Höhe brauchen, werden die Zeilen gleichmäßig gestreckt; unter der letzten Stunde bleibt keine leere Fläche. Ist er niedriger, scrollt er. Typ: `string`. ### allDayHeight Höhe des Bereichs für ganztägige Termine, z. B. `"120px"`. Standard: passt sich den Einträgen an. Typ: `string`. ### createNew Aktiviert das Erstellen von Terminen; der neue Bereich kommt in `onCreate` an. Ohne `item` zieht man einen Bereich über leere Slots. Mit `item` folgt ein vorgefertigter Termin dem Zeiger und wird per Klick platziert. Typ: `ArcWidgetCalendarWeekCreateNew`. ### segmentSettings Drag-Verhalten segmentierter Einträge: `dayJump`, `lockGroups`, `edgeMagnetism` und die Segment-`toolbar`. Typ: `ArcWidgetCalendarWeekSegmentSettings`. ### insetBorder Vertikale Linie am linken Rand jedes Eintrags mit Uhrzeit; ganztägige Einträge zeigen sie nicht. Standardmäßig aktiv. Mit `{ enabled: false }` blendest du sie überall aus, mit `stroke`, `width`, `color` und `position` gestaltest du alle Linien um. Ein `insetBorder` an einem einzelnen Eintrag oder Segment hat feldweise Vorrang. Mit `segments` teilst du die Linie in Stücke, z. B. eines pro Person, auf Wunsch nach Zeit platziert (siehe unten). Typ: `ArcWidgetCalendarWeekInsetBorder`. ### timeEntries Termine mit Datumswerten, Uhrzeiten und Inhalten. Beide Zeiten weglassen für einen Ganztagsbalken. Typ: `ArcWidgetCalendarWeekTimeEntry[]`. ## Verhalten und Grenzen - Für JSX in Terminen verwende `customLayout`. Die Standardfelder `title`, `subtitle` und `value` werden als Text oder HTML verarbeitet. - Datumswerte sind Epoch-Millisekunden in derselben Zeitbasis wie `dateSettings.startDate`, z. B. `new Date(2027, 0, 4).getTime()` für beide; Uhrzeiten sind Millisekunden seit Tagesbeginn (`9 * 3600000` = 09:00). Siehe [Datum und Zeit](/docs/react/patterns/react-dates). - `clickable: true` aktiviert die Auswahl. Ein Callback-Index ist keine Datensatz-ID; löse ihn über deine aktuelle Eintragsliste auf. - Drag: `draggable: true` auf dem Eintrag. Callback `onEntryDragSave(entryIndex, startData, endData, source)` / `onDailyEntryDragSave` — persistiere **`endData`** an `source.entry_index` / `source.segment_index`. `startData` ist der Stand vor dem Zug. Ohne `draggable: true` lässt sich der Eintrag nicht verschieben. - Ablegen speichern, Schritt für Schritt: (1) `endData` in den Eintrag an `source.entry_index` in deinem `timeEntries`-State schreiben, (2) im Backend speichern, (3) bei einem Fehler `startData` zurücksetzen. Der Kalender zeichnet aus den `timeEntries`, die du übergibst; ein nicht aktualisierter Eintrag springt beim nächsten Update an seine alte Position zurück. Vollständiges Beispiel: `calendar_week-00-defaults.tsx`. - Der sichtbare Uhrzeitbereich wird über `timeSettings.timeStart` und `timeSettings.duration` in Millisekunden konfiguriert. - `onCreate` liefert die Werte der Auswahl. Deine Anwendung validiert sie, vergibt eine ID und übernimmt den neuen Termin. Unterstützte `ArcWidgetSlot`-Felder erlauben einzelne JSX-Elemente, Text oder Widget-Deskriptoren. Reiche fremden Text als JSX-Text weiter; HTML-Strings werden als HTML interpretiert. [Inhalte kombinieren](/docs/react/patterns/nested-widget-descriptors). ## Datentypen Diese widgetbezogenen Typen werden aus dem Paket exportiert. Gemeinsame Typen wie `ArcWidgetSlot` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Die Konfigurationsobjekte sind geschlossen: Ein falsch geschriebener oder nicht unterstützter Schlüssel wird als Typfehler gemeldet. ```typescript export type ArcWidgetCalendarWeekBorderLine = | boolean | { enabled?: boolean; color?: string; width?: string; }; export interface ArcWidgetCalendarWeekBorders { color?: string; width?: string; outer?: ArcWidgetCalendarWeekBorderLine; columns?: ArcWidgetCalendarWeekBorderLine; rows?: ArcWidgetCalendarWeekBorderLine; } export interface ArcWidgetCalendarWeekStyles { borders?: ArcWidgetCalendarWeekBorders; borderColor?: string; borderSize?: string; borderRadius?: string; backgroundColor?: string; headerBackgroundColor?: string; fontColor?: string; mutedColor?: string; subtleColor?: string; } export interface ArcWidgetCalendarWeekHeaderText { label?: string; fontSize?: string; fontColor?: string; fontWeight?: string | number; alignX?: 'left' | 'center' | 'right'; } export interface ArcWidgetCalendarWeekHeader { backgroundColor?: string; title?: ArcWidgetCalendarWeekHeaderText; subtitle?: ArcWidgetCalendarWeekHeaderText; } export interface ArcWidgetCalendarWeekHighlightStyles { backgroundColor?: string; borderColor?: string; borderSize?: string; borders?: Pick; } export interface ArcWidgetCalendarWeekHighlight { date?: number | string; weekday?: number; color?: string; styles?: ArcWidgetCalendarWeekHighlightStyles; title?: ArcWidgetCalendarWeekHeaderText; subtitle?: ArcWidgetCalendarWeekHeaderText; } export interface ArcWidgetCalendarWeekDay { clickable?: boolean; } export interface ArcWidgetCalendarWeekDateSettings { startDate?: number; endDate?: number; duration?: number; weekends?: boolean; header?: ArcWidgetCalendarWeekHeader; day?: ArcWidgetCalendarWeekDay; highlights?: ArcWidgetCalendarWeekHighlight[]; } export interface ArcWidgetCalendarWeekdaySettings { week?: number; year?: number; } export interface ArcWidgetCalendarWeekTimeSettings { timeStart?: number; duration?: number; } export interface ArcWidgetCalendarWeekDayJump { threshold?: number; startTime?: number; } export interface ArcWidgetCalendarWeekSegmentSettings { dayJump?: ArcWidgetCalendarWeekDayJump; lockGroups?: boolean; edgeMagnetism?: boolean; toolbar?: boolean; } export interface ArcWidgetCalendarWeekInsetSegment { color?: string; stroke?: 'solid' | 'dashed' | 'dotted'; label?: string; size?: 'fraction' | 'auto' | (string & {}); timeFrom?: number; timeTo?: number; offset?: number; duration?: number; } export interface ArcWidgetCalendarWeekInsetBorder { enabled?: boolean; position?: 'left' | 'right'; width?: string; stroke?: 'solid' | 'dashed' | 'dotted'; color?: string; align?: 'start' | 'center' | 'end'; segments?: ArcWidgetCalendarWeekInsetSegment[]; } export type ArcWidgetCalendarWeekEntryStyles = ArcWidgetCssProperties; export interface ArcWidgetCalendarWeekSplit { title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; styles?: ArcWidgetCalendarWeekEntryStyles; insetBorder?: ArcWidgetCalendarWeekInsetBorder; timeFrom?: number; timeTo?: number; offset?: number; duration?: number; } export interface ArcWidgetCalendarWeekSegment { title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; styles?: ArcWidgetCalendarWeekEntryStyles; insetBorder?: ArcWidgetCalendarWeekInsetBorder; dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; linked?: boolean; segmentSettings?: Pick; splits?: ArcWidgetCalendarWeekSplit[]; clickable?: boolean; draggable?: boolean; } export interface ArcWidgetCalendarWeekTimeEntry { id?: string | number; title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; styles?: ArcWidgetCalendarWeekEntryStyles; insetBorder?: ArcWidgetCalendarWeekInsetBorder; clickable?: boolean; draggable?: boolean; segments?: ArcWidgetCalendarWeekSegment[]; segmentSettings?: Pick; } export interface ArcWidgetCalendarWeekPlaceOffset { days?: number; time?: number; } export interface ArcWidgetCalendarWeekPlaceSegment { title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; styles?: ArcWidgetCalendarWeekEntryStyles; insetBorder?: ArcWidgetCalendarWeekInsetBorder; linked?: boolean; duration?: number; offset?: ArcWidgetCalendarWeekPlaceOffset; dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; splits?: ArcWidgetCalendarWeekSplit[]; } export interface ArcWidgetCalendarWeekPlaceItem extends ArcWidgetCalendarWeekPlaceSegment { active?: boolean; segments?: ArcWidgetCalendarWeekPlaceSegment[]; } export interface ArcWidgetCalendarWeekCreateNew { item?: ArcWidgetCalendarWeekPlaceItem | null; } export interface ArcWidgetCalendarWeekData { width?: string; height?: string; lang?: string; weekStart?: number; weekdaySettings?: ArcWidgetCalendarWeekdaySettings; dateSettings?: ArcWidgetCalendarWeekDateSettings; timeSettings?: ArcWidgetCalendarWeekTimeSettings; timeZoneBalance?: number; currentTime?: boolean | string; dragStep?: number; timeSlotWidth?: string; timeSlotHeight?: string; allDayHeight?: string; styles?: ArcWidgetCalendarWeekStyles; createNew?: ArcWidgetCalendarWeekCreateNew; segmentSettings?: ArcWidgetCalendarWeekSegmentSettings; insetBorder?: ArcWidgetCalendarWeekInsetBorder; timeEntries?: ArcWidgetCalendarWeekTimeEntry[]; } export interface ArcWidgetCalendarWeekEntryDragSavePayload { dateFrom: number; dateTo: number; timeFrom: number; timeTo: number; } export interface ArcWidgetCalendarWeekDailyEntryDragSavePayload { dateFrom: number; dateTo: number; } export interface ArcWidgetCalendarWeekDragSourcePayload { entry_index: number; segment_index: number | null; split_index: number | null; } export interface ArcWidgetCalendarWeekCreatePayload { dateFrom: number; dateTo: number; timeFrom?: number; timeTo?: number; } export interface ArcWidgetCalendarWeekProps extends ArcReactWidgetProps { onEntryClick?: (entryIndex: number) => void; onDayClick?: (dateMillis: number) => void; onEntryDragSave?: ( entryIndex: number, startData: ArcWidgetCalendarWeekEntryDragSavePayload, endData: ArcWidgetCalendarWeekEntryDragSavePayload, source: ArcWidgetCalendarWeekDragSourcePayload ) => void; onDailyEntryDragSave?: ( entryIndex: number, startData: ArcWidgetCalendarWeekDailyEntryDragSavePayload, endData: ArcWidgetCalendarWeekDailyEntryDragSavePayload, source: ArcWidgetCalendarWeekDragSourcePayload ) => void; onCreate?: (values: ArcWidgetCalendarWeekCreatePayload) => void; } ``` ## Einsatzbeispiele - [Termine gestalten und bearbeiten](/docs/react/examples/individuelles-design-in-der-calendarweek) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Calendar (Week) ## AI Defaults (read first) - **uniqueId**: Required, `"calendar-" + Nr`. - **height**: Use `"100%"` when embedded in a layout. - **timeZoneBalance**: Set `-1` for German timezone in `dateSettings`. - **dateFrom/dateTo**: Must be actual date field values, not strings. - **dragAction**: Requires `recordId: Nr` and field IDs via `fieldId(Nr, "Feldname")`. Optional `changeFieldValues` for extra field updates after a successful drag (cache/trigger fields). - **createNew**: Needs `tableId` and field IDs via `fieldId(Nr, "Feldname")`. Optional `changeFieldValues` for extra fields on create. Optional `dataBag` after a successful create (same as Button create; `##newRecordId##`). Optional `item` for click-to-place (duration, clock, `offset: { days, time }`, `splits`, `linked`). `linked: false` + clock = satellite (does not pin the duration chain). Shift moves the whole ghost; release snaps satellites back. Create writes the same four date/time fields — overall span of the chain. Toggle with `item.active`. Empty selection: omit `item` (never `else ""`). Same-title pause: `splits`. Named leftover pieces (Teil 1 / Teil 2): three `segments`, not `splits`. - **Persistence**: Drag/create go through the **Action Performer** (`type: "update"` / `type: "create"`). Ninox config (`dragAction` / `createNew`) is unchanged. React hosts: handle these via `onAction` — **no** global `database` stub. - **timeSettings**: Omit for full 0–24h range. Set `from`/`to` in hours (integers) to restrict. - **lang**: Set `lang: clientLang()` for localized weekdays/months. # Custom Calendar (Week) Mit dem Widget **Custom Calendar Week** bringst du eine leistungsstarke Kalenderansicht direkt in deine Ninox-Datenbank. Du kannst es nahtlos in beliebige Tabellen integrieren, flexibel gestalten und dynamisch mit deinen eigenen Daten befüllen. Ob Projektplanung, Ressourcenmanagement oder Terminübersicht – das Widget passt sich deinen Anforderungen an. 💡 **Was du mit Custom Calendar machen kannst:** - **Individuelles Design:** Passe Farben, Icons und Layout an deinen Stil oder dein Branding an. - **Flexible Einträge:** Gestalte die Darstellung deiner Termine, Aufgaben oder Ereignisse exakt so, wie du sie brauchst – mit beliebigen Feldern, Icons oder Tags. - **Mehrtägige & ganztägige Events:** Plane übersichtlich und erkenne längere Zeiträume auf einen Blick. - **Drag & Drop:** Verschiebe Termine direkt im Kalender – intuitiv und ohne Umwege. - **Dynamische Filterung:** Zeige nur relevante Einträge an – per Ninox-Skript steuerbar. - **Hervorhebung besonderer Tage:** Markiere Feiertage, Urlaub, Abwesenheiten oder individuelle Events. - **Anpassbare Zeitachse:** Zeige nur die Stunden, die für deinen Anwendungsfall wichtig sind – z. B. 8–18 Uhr statt 0–24. - **Integration aus mehreren Tabellen:** Verknüpfe Daten aus verschiedenen Quellen – ideal für verteilte Teams oder Projekte. - **Offline-fähig:** Auch ohne Internet bleibt dein Kalender nutzbar. 📅 **Custom Calendar Week** macht deine Terminplanung in Ninox übersichtlicher, interaktiver und deutlich effizienter – perfekt für alle, die mehr aus ihrer Datenbank herausholen wollen. ## Anwendungscode ```javascript let current := this; arcCustomCalendarWeek({ uniqueId: "cal-39", embedded: false, weekdaySettings: { week: 12, year:2024 }, weekStart: "", dateSettings: { startDate: date(2025, 2, 2), endDate: "", duration: 7, highlights: [{ date: date(2025, 2, 7), color: "#F1B6CE" }, { weekday: 6, color: "rgba(255,0,0,0.1)" }, { weekday: 7, color: "rgba(255,0,0,0.1)" }] }, timeZoneBalance: "", timeSettings: { timeStart: time(6, 30), duration: time(15, 0) }, currentTime: "rgba(0,0,255,0.5)", lang: clientLang(), timeSlotWidth: "250px", timeSlotHeight: "80px", allDayHeight: "", styles: { backgroundColor: "rgba(0,0,255,0.01)" }, createNew: { tableId: "B", dateFrom: "A", timeFrom: "B", dateTo: "C", timeTo: "D", changeFieldValues: [{ fieldId: fieldId(Nr, "CacheTrigger"), value: now() }] }, timeEntries: (select 'Time Entries').[{ title: Kalendertitel, subtitle: Description, value: "", customLayout: "", dateFrom: 'Date From', dateTo: 'Date To', timeFrom: 'Time From', timeTo: 'Time To', styles: { backgroundColor: color(Person), color: "#fff", borderRadius: "8px", padding: "5px" }, insetBorder: { enabled: true, stroke: "dotted" }, dragAction: { recordId: Nr, dateFrom: "A", timeFrom: "B", dateTo: "C", timeTo: "D", changeFieldValues: [{ fieldId: fieldId(Nr, "CacheTrigger"), value: now() }] }, clickAction: { type: "popup", recordId: Nr }, segments: [{ title: "Anfahrt", timeFrom: SegVon, timeTo: SegBis, dateFrom: 'Seg Date From', dateTo: 'Seg Date To', linked: false, dragAction: { recordId: Nr, dateFrom: "E", timeFrom: "F", dateTo: "G", timeTo: "H" } }] }] }) ``` ## Allgemeine Settings ### weekdaySettings– gezielte Steuerung der Kalenderwoche Mit dem Parameter `**weekdaySettings**` kannst du genau festlegen, **welche Kalenderwoche aus welchem Jahr** im Kalender angezeigt werden soll. In diesem Beispiel wird die **12. Kalenderwoche des Jahres 2025** angezeigt. Statt feste Werte einzutragen, kannst du auch auf Ninox-Felder verweisen, um die Daten zu dynamisieren. ⚠️ **Wichtig: **Wenn sowohl `dateSettings` als auch `weekdaySettings` gesetzt sind, **haben **`dateSettings`** Vorrang** und überschreiben die Angaben aus `weekdaySettings`. ### dateSettings– Start, Dauer & Hervorhebung in der Wochenansicht Mit der Parametergruppe `**dateSettings**` steuerst du, **wann dein Kalender beginnt, wie lange er angezeigt wird und welche Tage besonders hervorgehoben werden sollen**. **Einzelne Parameter in dateSettings:** - `**startDate**` *(Pflichtfeld)* Definiert das Startdatum des Kalenders. Gib hier ein Datumsfeld an. → Kann dynamisch über Ninox-Felder gesteuert werden. - `**endDate**` *(optional)* Gibt das **Enddatum** des Kalenders an. Gib hier ein Datumsfeld an. → Hat Vorrang vor `duration`. - `**duration**` *(optional)* Bestimmt die **Anzahl der anzuzeigenden Tage** (Standard: 7). → Wird nur berücksichtigt, wenn `endDate` nicht gesetzt ist. **Logik der Anzeige-Dauer:** - Nur `startDate` gesetzt → zeigt **7 Tage** ab Startdatum - `startDate` + `duration` → zeigt die definierte **Anzahl an Tagen** - `startDate` + `endDate` → `**duration**`** wird ignoriert** - `**highlights**` *(optional)* Erlaubt das **farbliche Hervorheben bestimmter Tage** – ideal für Urlaube, Feiertage oder andere besondere Anlässe. → Du kannst sowohl **konkrete Daten** als auch **Wochentage** markieren (Details siehe unten). - `**header**` *(optional)* Steuert die Darstellung der Tagesköpfe (Wochentag, Datum). Ohne Angabe werden Standard-Labels verwendet. ```javascript header: { backgroundColor: "#F8FAFC", title: { label: "##weekDayShort##. ##day##.", fontSize: "15px", fontColor: "#000", alignX: "center" }, subtitle: { label: "##weekdayName##", fontSize: "12px", fontColor: "#666" } } ``` **Platzhalter für label:** `##date##`, `##weekdayName##`, `##weekDayLong##`, `##weekDayShort##`, `##day##`, `##month##`, `##monthLong##`, `##monthShort##`, `##year##` 📌 **Hinweis:** `subtitle` ist optional. Wenn du nur `title` angibst (ohne `subtitle`), wird die zweite Zeile nicht angezeigt – der Titel erhält den vollen Platz. - `**day**` *(optional)* Steuert Aktionen beim Klick auf einen Tag (z. B. Datum auswählen, View wechseln). ```javascript day: { actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "helper_selectedDate"), value: "##clickedDate##" }] } ``` **Platzhalter:** `##clickedDate##` wird durch den Millisekunden-Timestamp des geklickten Datums ersetzt (z. B. `1731369600000`). ```javascript dateSettings: { startDate: "", endDate: "", duration: 14, highlights: [{ date: date(2024, 10, 31), color: 'Color Highlights' }, { weekday: 6, color: "rgba(255,0,0,0.1)" }, { weekday: 7, color: "rgba(255,0,0,0.1)" }] } ``` ### weekStart – Starttag der Kalenderwoche festlegen Mit dem Parameter `**weekStart**` bestimmst du, **an welchem Wochentag dein Kalender beginnen soll**. 📌 **Typ:** Zahl (0–6) 📅 **Werte:** - `0` = Sonntag *(z. B. in den USA üblich)* - `1` = Montag *(Standard in Mitteleuropa)* - `2` = Dienstag - ... - `6` = Samstag 💡 **Tipp: **Für die meisten Nutzer im deutschsprachigen Raum ist `1` (Montag) die passende Wahl. ```javascript weekStart: 1, ``` ### timeZoneBalance – manuelle Zeitzonenkorrektur für präzise Anzeige **Typ:** Zahl (in Stunden) Der Parameter `**timeZoneBalance**` hilft dir dabei, **Zeitzonenunterschiede auszugleichen**, die durch die Darstellung in Ninox entstehen können. Hintergrund**: **Ninox verwendet intern eine andere Zeitzonen-Logik, was zu einer Verschiebung der Zeiten im Kalender führen kann. Empfehlung für Deutschland: Setze den Wert auf `-1`, um die Anzeige an die mitteleuropäische Zeit (MEZ/MESZ) anzupassen. ```javascript timeZoneBalance: -1, timeZoneBalance: "" // default: Zeiten werden wie in Ninox gespeichert angezeigt, die Nutzer-Zeitzone wird nicht beachtet. ``` ### timeSettings – individuelle Steuerung der Zeitachse Mit der Parametergruppe `**timeSettings**` kannst du festlegen, **welcher Zeitraum pro Tag im Kalender angezeigt werden soll**. So lässt sich die Kalenderansicht besser an deinen Anwendungsfall anpassen – z. B. für typische Bürozeiten oder Schichtmodelle. ```javascript timeSettings: { timeStart: time(6, 30), // Zeitfeld oder Milisekunden duration: time(6, 30), // Duration in Milisekunden angeben. hier: 8 Stunden }, ``` In diesem Beispiel startet der Kalender-Tag um **06:00 Uhr** und endet um **22:00 Uhr**. 🧠 **Wichtig zu wissen:** - Es werden **nur volle Stunden** unterstützt. - Gibt man z. B. `6.5` (für 06:30 Uhr) an, **rundet das System auf 06:00 Uhr** ab. - Der Standardbereich ohne eigene Konfiguration ist **0–24 Uhr** (also ein kompletter Tag). 💡 **Tipp: **Weniger sichtbare Stunden = kompaktere Darstellung = bessere Übersicht bei vielen Einträgen. ### currentTime – farbliche Markierung der aktuellen Uhrzeit Der Parameter `**currentTime**` steuert die Darstellung eines schmalen Balkens, der dir **die aktuelle Uhrzeit im Kalender anzeigt** – ähnlich wie in bekannten Kalender-Apps. 🎨 Funktion: Mit diesem Parameter legst du die Farbe des Balkens fest, der die aktuelle Position im Tagesverlauf markiert. ```javascript currentTime: "rgba(0,0,255,0.5)", // RGB Codes currentTime: "#f95757" // HEX Codes ``` ### lang Diese Funktion wird standardmäßig eingestellt und muss nicht angepasst werden. ```javascript lang: clientLang(), ``` ### timeSlotWidth – Breite der einzelnen Wochentage Mit dem Parameter `**timeSlotWidth**` legst du fest, **wie breit die einzelnen Tages-Spalten im Wochenkalender dargestellt werden**.
`timeSlotWidth:  "250px" ,`
→ Jeder Wochentag wird mit **250 Pixeln Breite** angezeigt. 📌 **Hinweis:** - Der Standardwert liegt bei ca. **100 px**, kann aber je nach Layout-Anforderungen angepasst werden. - Eine höhere Zahl sorgt für **mehr Platz pro Tag**, besonders hilfreich bei vielen Einträgen. ### timeSlotHeight – Höhe der Stundenzeilen festlegen Mit dem Parameter `**timeSlotHeight**` bestimmst du, **wie hoch jede einzelne Stunde im Kalender mindestens dargestellt wird**.
`timeSlotHeight: "80px",`
→ Jede Stunde erhält mindestens eine **Höhe von 80 Pixeln** in der vertikalen Zeitachse. 📌 **Hinweis:** - Der Standardwert liegt bei **50 px**. - Ein höherer Wert sorgt für **mehr Raum pro Stunde.** - Ist der Kalender höher als die Stunden bei dieser Mindesthöhe brauchen, werden die Zeilen **automatisch gestreckt** und füllen die Höhe aus. ### allDayHeight – Höhe der Zeile für Ganztagseinträge Mit dem Parameter `allDayHeight` legst du fest, wie hoch die obere Zeile für Ganztagseinträge dargestellt wird. → Die Zeile für Ganztagseinträge wird mit 60 Pixeln Höhe angezeigt. 📌 Hinweis: - Standardmäßig liegt die Höhe bei ca. 40 px. - Ein höherer Wert schafft mehr Platz für mehrere Ganztagseinträge oder größere Textdarstellungen. ### styles – individuelles Styling für deinen Kalender Mit `**styles**` kannst du das **Aussehen deines Kalenders anpassen** – hier kannst du aktuell die Hintergrundfarbe deines Kalenders individualisieren.
	styles: {
            backgroundColor: "rgba(0,0,255,0.01)"
        },
### createNew – neue Termine durch Ziehen erstellen Mit dem Parameter `**createNew**` aktivierst du die Möglichkeit, **neue Termine direkt durch Aufziehen eines Zeitbereichs im Kalender zu erstellen**. 🖱️ **So funktioniert’s:** - Du klickst in einen freien Bereich des Kalenders, **ziehst mit gedrückter Maustaste**, und lässt wieder los. - Dabei wird automatisch ein neuer Termin erstellt – inklusive **Startzeit und Dauer**. 📌 **Hinweis:** - Alle Feldbezüge (`dateFrom`, `dateTo`, `timeFrom`, `timeTo`) müssen mit den **Field IDs** der entsprechenden Felder aus deiner **Kalendereintrags-Tabelle (**`**tableId**`**)** übereinstimmen. - Du kannst die Field IDs ganz einfach mit der Ninox-Funktion `fieldId()` herausfinden und direkt in die Konfiguration übernehmen. **💡 **Tipp: Ideal für eine schnelle und intuitive Terminplanung – z. B. bei Schichten, Buchungen oder Raumvergabe. Optional kannst du mit **`changeFieldValues`** Zusatzfelder im neuen Datensatz setzen – z. B. Cache- oder Trigger-Felder: ```javascript createNew: { tableId: "B", dateFrom: fieldId(Nr, "Datum von"), timeFrom: fieldId(Nr, "Von"), dateTo: fieldId(Nr, "Datum bis"), timeTo: fieldId(Nr, "Bis"), changeFieldValues: [{ fieldId: fieldId(Nr, "CacheTrigger"), value: now() }] } ``` Optional **`dataBag`**: nach erfolgreichem Anlegen denselben JSON-State-Topf patchen wie bei Button-Create. Läuft nur, wenn der Datensatz wirklich angelegt wurde. `##newRecordId##` wird durch die ID des neuen Termins ersetzt. Typisch: Auswahl leeren, damit der Ghost verschwindet. ```javascript createNew: { tableId: "B", dateFrom: fieldId(Nr, "Datum von"), timeFrom: fieldId(Nr, "Von"), dateTo: fieldId(Nr, "Datum bis"), timeTo: fieldId(Nr, "Bis"), item: if Auswahl then { /* Ghost */ }, dataBag: { recordId: Nr, fieldId: fieldId(Nr, "helper_uiState"), base: helper_uiState, patch: { auswahlId: null } } } ``` `null` im Patch löscht den Key. Die neue Termin-ID mit ablegen: `patch: { auswahlId: null, lastTerminId: "##newRecordId##" }`. Syntax: [`data-bag-actions`](../patterns/data-bag-actions.md). 📌 **Persistenz / React:** Create läuft über `{ type: "create", tableId, changeFieldValues, setNewRecordId?, dataBag?, popup?, fullscreen? }`. React: dieselbe Action über `onAction` verarbeiten. ### createNew.item – vorgebauten Termin per Klick setzen Mit **`createNew.item`** legst du einen **fertigen Termin** in den Kalender, statt einen Zeitraum aufzuziehen. Typischer Fall: Auswahl in einer Seitenleiste (Typ, Dauer, Phasen) schreibt ins Formelfeld — der Kalender zeigt einen **Ghost**, der der Maus folgt. Ein Klick setzt ihn. Ohne `item` bleibt das heutige Aufziehen. Mit `item` (und gesetzter `duration` oder Segment-Dauern) ist Aufziehen aus — außer **`active`** ist `false`. Den Platzieren-Modus schaltest du **außerhalb** des Kalenders: ein Ja/Nein-Feld, eine Checkbox in der Seitenleiste oder die Auswahl selbst. ```javascript createNew: { tableId: "B", dateFrom: fieldId(first(Termine), "Datum von"), timeFrom: fieldId(first(Termine), "Von"), dateTo: fieldId(first(Termine), "Datum bis"), timeTo: fieldId(first(Termine), "Bis"), changeFieldValues: if Auswahl then [{ fieldId: fieldId(first(Termine), "Typ"), value: number(Auswahl) }] else [{ fieldId: fieldId(first(Termine), "CacheTrigger"), value: now() }], item: if Auswahl then { active: Platzieren, title: Auswahl.Name, styles: { backgroundColor: color(Auswahl), color: "#fff", borderRadius: "8px" }, segments: [{ title: "Anfahrt", linked: false, offset: { days: -1 }, timeFrom: time(16, 0), timeTo: time(17, 0) }, { title: "Kernarbeit", timeFrom: time(11, 0), timeTo: time(14, 0), splits: [{ title: "Pause", timeFrom: time(12, 30), timeTo: time(13, 0) }] }, { title: "Abfahrt", linked: false, offset: { days: 1 }, timeFrom: time(8, 0), timeTo: time(9, 0) }] } } ``` **Felder in `item`:** - **`active`** — optional. `true` oder weggelassen: Ghost an, Klick setzt. `false` / leer: Ghost aus, Aufziehen wieder an. Bindet an ein Feld außerhalb des Widgets (z. B. `Platzieren`). - **`duration`** — relative Länge. Reine Duration-Phasen hängen sich an die vorherige Follow-Phase (erste: an die Maus). - **`timeFrom` / `timeTo`** — Uhr auf dem Anker-Tag (Tag unter der Maus). Pinnt **diese** Phase. Mit `linked: false` bleibt sie Satellit — der Follow-Strang folgt weiter der Maus. Ohne `linked: false` hängt die Kette an dieser Uhr. - **`linked`** — wie bei gespeicherten Segmenten. `false` = Satellit (Anfahrt/Abfahrt). Standard `true`. - **`offset`** — `{ days, time }`, beide optional und signed. `days: -1` Vortag, `days: 1` Folgetag. `time: -time(1, 0)` eine Stunde früher (Maus / vorige Phase). Kombinierbar: `{ days: -1, time: -time(1, 0) }`. Mit fester Uhr wählt `days` den Kalendertag; `time` schiebt die Uhr mit. `days: 0` hält die Phase auf dem Anker-Tag, wenn die Phase davor auf einem anderen Tag lag. Nicht verwechseln mit dem flachen `offset: time(…)` in **`splits`**. - **`dateFrom` / `dateTo`** — echte Datumswerte (wie bei `timeEntries`), wenn die Phase fest auf einem Kalendertag liegt. - **`splits`** — eine Ebene Unterbrechungen in dieser Phase (typisch Pause). Beide Kern-Stücke behalten den Phasen-Titel. Siehe unten. - **`title` / `subtitle` / `value` / `customLayout` / `styles` / `insetBorder`** — wie ein `timeEntry`. Auflösung pro Phase, erste passende Regel: festes Datum + Uhr → Uhr (`timeFrom`/`timeTo`, optional `offset.days` / `offset.time`) → `offset` + `duration` → nur `duration`. `linked: false` + Uhr aktualisiert den Follow-Cursor nicht. **Umschalttaste** beim Ghost: ganze Kette inkl. Satelliten mitverschieben. Loslassen: Satelliten zurück auf ihre Uhr. Klick speichert das, was du siehst. Kern unter die Maus, Satellit mit fester Uhr: ```javascript segments: [{ title: "Anfahrt", linked: false, offset: { days: -1 }, timeFrom: time(16, 0), timeTo: time(17, 0) }, { title: "Vorarbeit", offset: { days: 0 }, duration: time(0, 30) }, { title: "Kern", duration: time(3, 0) }, { title: "Abfahrt", offset: { days: 1 }, duration: time(1, 0) }] ``` Ohne feste Uhren (folgt der Maus): ```javascript segments: [{ title: "Anfahrt", offset: { days: -1, time: -time(1, 0) }, duration: time(1, 0) }, { title: "Kernarbeit", offset: { days: 0 }, duration: time(3, 0), splits: [{ title: "Pause", offset: time(1, 30), duration: time(0, 30) }] }, { title: "Abfahrt", offset: { days: 1 }, duration: time(1, 0) }] ``` Sollen die Stücke **anders heißen** (Teil 1 / Pause / Teil 2): drei `segments` mit `duration`, kein `splits`. Der Ghost darf über mehrere Tage gehen. Pro Phase läuft die Uhrzeit mit (`##entrytime##`). Der Ghost nimmt eine **eigene Spur** ein — bestehende Termine zur gleichen Zeit rücken zur Seite, wie bei zwei echten Einträgen. Maus weg: die anderen gehen wieder auf volle Breite. Ausblenden: **Auge** in der Toolbar oder **Esc**. Wieder einblenden nur über das Auge. Die Seitenleisten-Auswahl bleibt. Gespeichert wird **ein** neuer Datensatz auf dieselben vier `createNew`-Felder: Von der ersten Phase bis Bis der letzten. `changeFieldValues` und `dataBag` wie bei Aufziehen. Leere Auswahl: `item` weglassen (kein `else ""`). Dann gilt wieder Aufziehen. ## timeEntries – deine Kalendereinträge im Detail Der `timeEntries`-Block legt fest, welche konkreten Zeiteinträge im Kalender angezeigt werden. Hier bestimmst du, wie jeder einzelne Eintrag dargestellt wird – von Titel über Farbe bis hin zu eigenen Layouts. So kannst du deine Kalenderkarten ganz individuell gestalten und an deinen Anwendungsfall anpassen. Dabei wird automatisch ein neuer Termin erstellt – inklusive Startzeit und Dauer. 📌 **Hinweis:** Für die Darstellung der **Echtzeit-Veränderung** der Termindaten (beim Erstellen oder auch beim Verschieben eines Termins) kann der Platzhalter **"##entrytime##"** überall in `timeEntries` eingesetzt werden, wo die **Terminzeit** angezeigt werden soll, also im title, subtitle, value, aber auch in allen Widgets, die hier eingesetzt werden. Wie zB. wenn ein `customLayout` für das Design des timeEntries genutzt wird oder in Buttons, Inputs usw. Zum Beispiel innerhalb eines Layout-Blocks im Text Widget: ```javascript ... value: arcCustomText({ alignY: "top", fontSize: typo.paragraph3.fontSize, fontWeight: "400", fontColor: appointmentType.colorShade, value: "##entrytime##" }), ... ``` ➡️ Alle Parameter für die timeEntries findest du im **folgenden Abschnitt**. ### title – Titel des Kalendereintrags Mit dem Parameter `title` legst du fest, was in der ersten Zeile des Kalendereintrags angezeigt wird. 🔧 Zwei Optionen: - Eigener Titel: Gib ein Feld oder einen Text an, der als Titel im Eintrag angezeigt werden soll. - Keine Angabe (`"`): Lässt du das Feld leer (`"`), wird stattdessen die Uhrzeit des Termins angezeigt.
title: Kalendertitel, // Ninox Feld
title: "", // Default: Gibt die Uhrzeit des Kalendereintrages an
📌 Hinweis: - Wenn du keinen Titel vergibst und stattdessen die Uhrzeit angezeigt wird, wird diese Uhrzeit bei Änderungen live aktualisiert, z. B. beim Ziehen oder Verlängern eines Eintrags. - **Leerer Wert:** Wenn `title` leer oder nur Leerzeichen enthält, wird die Titel-Zeile ausgeblendet – der Platz geht an `subtitle` oder `##entrytime##`. ### subtitle – Beschreibung unter dem Titel Mit dem Parameter `subtitle` kannst du eine zusätzliche Zeile unter dem Titel im Kalendereintrag anzeigen lassen. Typischerweise wird hier eine kurze Beschreibung oder ein ergänzender Hinweis wie z. B. der Grund des Termins oder ein Status hinterlegt.
subtitle: Description, // Ninox Feld
subtitle: "" // Textfeld
→ Zeigt z. B. den Inhalt des Feldes *„Description“* unter dem Titel im Kalendereintrag an. 📌 **Hinweis:** - Der `subtitle` ist optional und wird **nur angezeigt, wenn ein entsprechender Feldwert vorhanden ist**. - **Leerer Wert:** Wenn `subtitle` leer oder nur Leerzeichen enthält, wird die Subtitle-Zeile ausgeblendet – der Platz geht an den Titel. Titel und Subtitle werden symmetrisch behandelt: Wenn beide leer sind, wird nur `##entrytime##` angezeigt. ### value – Zusatzinformationen oder interaktive Felder Der Parameter `value` steht für die dritte Zeile deines Kalendereintrags. Hier kannst du weitere Informationen anzeigen oder sogar interaktive Elemente wie Eingabefelder oder Auswahllisten integrieren. 🔧 Einsatzmöglichkeiten: - Zusätzliche Informationen wie z. B. Ort, Status, Verantwortlicher - Interaktive Widgets direkt im Kalendereintrag – z. B.:

  • Ein Select-Feld, um den zuständigen Mitarbeiter auszuwählen - Ein Input-Feld für kurze Notizen oder Rückmeldungen

value: "",
value: Kommentar, 
→ Zeigt den Inhalt des Feldes *„Kommentar“* in der dritten Zeile an. 📌 **Hinweis:** - `value` ist **optional**, erweitert aber die Darstellung deiner Einträge um eine zusätzliche Ebene. - In Verbindung mit eingebetteten Widgets entsteht ein **interaktives Kalendererlebnis direkt im Eintrag**. 💡 **Tipp: **Nutze `value`, um wichtige Zusatzinfos sofort sichtbar zu machen – oder um deine Nutzer direkt im Kalender arbeiten zu lassen. ### customLayout – vollständiges individuelles Layout für Kalendereinträge Mit dem Parameter `**customLayout**` kannst du ein **komplett eigenes Layout** für deine Kalendereinträge definieren. Wenn du diesen Parameter nutzt, werden die Standardfelder `**title**`, `**subtitle**` und `**value**` **nicht mehr angezeigt**. Stattdessen kannst du frei gestalten, wie dein Kalendereintrag aussieht – mit beliebigen Widgets.
`customLayout: "",`
💡 Tipp: Nutze `customLayout`, wenn du eine komplett individuelle Darstellung brauchst, z. B. mit eigenen Farben, Icons, Struktur oder logischen Bedingungen. ### dateFrom, dateTo, timeFrom, timeTo – Start- und Endzeit je Eintrag Mit diesen vier Parametern gibst du an, **welche Felder aus deinem Zeiteintrags-Datensatz** für **Datum und Uhrzeit** verwendet werden sollen. ```javascript dateFrom: 'Date From', dateTo: 'Date To', timeFrom: 'Time From', timeTo: 'Time To', ``` ![](https://framerusercontent.com/images/zRF7pUd7VZBvsfI8yuyCarMGYs.png) 📌 Hinweis: - Es müssen echte Datums- und Zeitfelder aus Ninox angegeben werden. - Die Kombination aus Datum + Uhrzeit bestimmt die Position und Länge des Kalendereintrags. ⚠️ Wichtig zu wissen: - Fehlt `dateTo`, wird automatisch der Tag aus `dateFrom` verwendet (und umgekehrt). - Fehlen beide Datumsfelder, wird der Eintrag nicht angezeigt. - Fehlt `timeTo`, wird automatisch `timeFrom + 5 Minuten` verwendet. - Fehlt `timeFrom`, wird `timeTo - 5 Minuten` angenommen. → So ist eine minimale Anzeige trotzdem möglich. 💡 Tipp: Wenn du nur Tages-Einträge ohne genaue Uhrzeiten darstellen willst, kannst du auch nur mit Datumsfeldern arbeiten – der Kalender stellt den Eintrag dann als Ganztagstermin dar. ### styles – individuelles Styling für Kalendereinträge Mit dem Parameter `**styles**` kannst du das **Aussehen einzelner Kalendereinträge** anpassen – z. B. Farben, Innenabstand (Padding) oder abgerundete Ecken (Border-Radius).
styles: {
                    backgroundColor: color(Person),
                    color: "#fff",
                    borderRadius: "8px",
                    padding: ""
                },
📌 **Unterstützte Eigenschaften:** - `backgroundColor`: Hintergrundfarbe der Karte - `color`: Schriftfarbe - `padding`: Innenabstand (z. B. `"6px"`) - `borderRadius`: Abrundung der Ecken (z. B. `"8px"`) 💡 Tipp: Mit `styles` kannst du deine Einträge **visuell voneinander abheben**, etwa nach Status, Typ oder Priorität – auch dynamisch, wenn du Felder deiner Datenquelle als Wert verwendest. ### dragAction – Felder für das Verschieben von Einträgen festlegen Mit dem Parameter `**dragAction**` definierst du, **welche Datums- und Zeitfelder beim Ziehen oder Verschieben eines Eintrags aktualisiert werden sollen**. So wird sichergestellt, dass der neue Zeitpunkt im **Datensatz gespeichert** wird, wenn ein Eintrag im Kalender bewegt wird. ```javascript dragAction: { recordId: Nr, dateFrom: fieldId(Nr, "Datum von"), timeFrom: fieldId(Nr, "Von"), dateTo: fieldId(Nr, "Datum bis"), timeTo: fieldId(Nr, "Bis"), changeFieldValues: [{ fieldId: fieldId(Nr, "CacheTrigger"), value: now() }] } ``` 📌 Hinweis: - Gib hier die korrekten Feld-IDs der einzelnen Felder in der Zeiteintrags-Tabelle an. - Du kannst hier die Ninox Funktion `fieldId()` benutzen. - **changeFieldValues**: Optional. Array von `{ fieldId, value }` – wird **nach** einem erfolgreichen Drag geschrieben (Datum/Zeit haben sich geändert). Typischer Einsatz: Cache- oder Trigger-Felder aktualisieren. Aufbau wie bei `createNew.changeFieldValues` und wie in der Timeline. ⚠️ Wichtig: `recordId` und die Datums-/Zeitfelder sind zwingend erforderlich, wenn du möchtest, dass Einträge per Drag & Drop verschoben oder in ihrer Dauer verändert werden können. 📌 **Persistenz / React:** Beim Speichern nach Drag sendet das Widget Action-Performer-Actions `{ type: "update", recordId, field, value }` (ein Action pro geändertes Feld). In Ninox mappt der Adapter das auf `database.update`. Im React-Adapter landet das in `onAction` — ohne globales `database`. ### clickAction – Einträge per Klick in Ninox öffnen Mit dem Parameter `**clickAction**` ermöglichst du es, dass der Nutzer durch einen Klick auf einen Kalendereintrag direkt den **zugehörigen Datensatz in Ninox öffnen** kann.
clickAction: {
                    type: "popup",
                    recordId: Nr
                }
📌 Bedeutung der Parameter: - `type: "popup"` – Öffnet den Datensatz in einem Popup-Fenster (weitere Typen können ggf. folgen). - `recordId`: Das Feld im Kalendereintrag, das den Primärschlüssel (Record ID) enthält – meist `"Nr"`. ⚠️ Wichtig: - Das angegebene Feld bei `recordId` muss die echte Ninox-Datensatz-ID enthalten. - Wird kein gültiger Wert gefunden, passiert beim Klick nichts. ### insetBorder – vertikale Inset-Linie im Kalendereintrag Mit `**insetBorder**` kannst du eine **dezente vertikale Linie** am linken (oder rechten) Rand jedes Kalendereintrags **mit Uhrzeit** anzeigen lassen – ähnlich dem Stil des Apple Kalenders. Ganztägige Einträge zeigen sie nicht. Das Feature ist **standardmäßig aktiv** und muss nur deaktiviert werden, wenn du es nicht möchtest. ```javascript // On data: applies to all timed entries (hide everywhere with enabled: false) insetBorder: { enabled: false } // On a timeEntry or segment: applies to that entry only insetBorder: { enabled: true, // default: true; false hides the line position: 'left', // 'left' (default) or 'right' width: '2px', // line width, default: 3px stroke: 'dotted', // 'solid' (default), 'dashed' or 'dotted' color: '#ffffff' // default: the entry's styles.color, then #18181b } ``` 📌 **Hinweis:** - Setze `insetBorder` in den Widget-`data`, um alle Einträge mit Uhrzeit auf einmal zu ändern, z. B. `{ enabled: false }`, um die Linie überall auszublenden. - Auf einem `timeEntry` oder Segment gesetzt, gilt es nur dort. Dort gesetzte Felder haben Vorrang vor der Widget-Ebene; nicht gesetzte Felder übernehmen deren Wert. - Setzen Eintrag und eines seiner Segmente beide `insetBorder`, gilt für dieses Segment der Wert des Segments. - Die Linie reagiert automatisch auf den `borderRadius` des Eintrags und wird kürzer, je höher der Radius ist. - Bei zusammenhängenden Segmenten (Kette) wird sie an Übergängen nahtlos fortgesetzt. **Stücke der Linie (`segments`)** Die Linie lässt sich in Stücke teilen, zum Beispiel eine Farbe pro Person. Das ist nicht dasselbe wie die `segments` eines Eintrags (seine Phasen): Hier steht `segments` innerhalb von `insetBorder` und teilt nur die Linie. ```javascript insetBorder: { color: '#18181b', // Standard der Stücke align: 'start', // 'start' (Standard), 'center' oder 'end': wo Stücke ohne Zeit sitzen, wenn sie die Linie nicht füllen segments: [ // Mit Zeit sitzt das Stück dort, wo die Zeit liegt (Uhrzeiten am ersten Tag des Eintrags): { color: '#dc2626', label: 'Anna', timeFrom: 9 * 3600000, timeTo: 10.5 * 3600000 }, // oder relativ zum Start des Eintrags; es wandert mit dem Eintrag: { color: '#2563eb', label: 'Ben', offset: 60 * 60000, duration: 90 * 60000 }, // Ohne Zeit teilen sich die Stücke die Linie nach size: // { color: '#16a34a', label: 'Clara', size: '25%' } // 'fraction' / 'auto' (Standard), '24px', '25%' ] } ``` - `label` erscheint als Tooltip und wird von Screenreadern vorgelesen. - Stücke mit Zeit dürfen sich überlappen: Wo zwei gleichzeitig aktiv sind, werden sie nebeneinander gezeichnet (bis zu drei Spuren) und der Text rückt ein. Zeit, die kein Stück abdeckt, bleibt leer. - Hat irgendein Stück eine Zeit, werden Stücke ohne Zeit nicht gezeichnet. Ohne jede Zeit werden die Stücke nach `size` verteilt; sind die festen Größen länger als die Linie, schrumpfen alle im gleichen Verhältnis. - `offset` zählt ab dem Start des angezeigten Stücks des Eintrags. Bei einem Eintrag, der durch `splits` geteilt ist, zählt jedes Stück ab seinem eigenen Start. - Jedes Stück kann eigene `color` und `stroke` setzen; nicht gesetzte Felder übernehmen die der Linie. ### segments – Termine in Phasen unterteilen Mit dem Parameter `**segments**` kannst du einen Termin in mehrere **zusammengehörige Zeitphasen** aufteilen – z. B. Anfahrt, Vorarbeit, Kernarbeit, Pause, Nacharbeit und Abfahrt. Jedes Segment ist ein eigenständiger Eintrag mit eigener Zeit und eigenem Design, kann aber mit anderen Segmenten **verknüpft** werden, sodass sie beim Verschieben gemeinsam bewegt werden. ```javascript timeEntries: (select Termine).[{ title: Bezeichnung, dateFrom: 'Datum von', dateTo: 'Datum bis', timeFrom: Von, timeTo: Bis, styles: { backgroundColor: "#5b6af0", color: "#fff", borderRadius: "8px" }, dragAction: { recordId: Nr, dateFrom: fieldId(Nr, "Datum von"), timeFrom: fieldId(Nr, "Von"), dateTo: fieldId(Nr, "Datum bis"), timeTo: fieldId(Nr, "Bis") }, segmentSettings: { dayJump: { threshold: time(22, 0), startTime: time(7, 0) } }, segments: [{ title: "Anfahrt", dateFrom: 'Seg Datum von', dateTo: 'Seg Datum bis', timeFrom: SegVon, timeTo: SegBis, styles: { backgroundColor: "#3b4fd0", color: "#fff" }, linked: false, segmentType: "travel", dragAction: { recordId: Nr, dateFrom: fieldId(Nr, "Seg Datum von"), timeFrom: fieldId(Nr, "SegVon"), dateTo: fieldId(Nr, "Seg Datum bis"), timeTo: fieldId(Nr, "SegBis") } }, { title: "Kernarbeit", dateFrom: 'Kern Datum von', dateTo: 'Kern Datum bis', timeFrom: KernVon, timeTo: KernBis, styles: { backgroundColor: "#5b6af0", color: "#fff" }, linked: true }] }] ``` #### Felder je Segment | Parameter | Typ | Beschreibung | |---|---|---| | `title` | Text | Titel des Segments | | `subtitle` | Text | Untertitel (optional) | | `value` | Text/Widget | Zusatzzeile oder eingebettetes Widget | | `dateFrom`, `dateTo` | Datum | Start- und Enddatum | | `timeFrom`, `timeTo` | Zeit | Start- und Endzeit | | `styles` | Objekt | Wie beim `timeEntry` – überschreibt Parent-Styles | | `linked` | Boolean | `true` = Segment wird mit anderen `linked`-Segmenten gemeinsam verschoben (Standard: `true`) | | `segmentType` | Text | Frei wählbarer Typ-Bezeichner (z. B. `"travel"`, `"core"`) | | `insetBorder` | Objekt | Inset-Linie für dieses Segment (überschreibt Entry-Level) | | `dragAction` | Objekt | Felder für Drag & Drop – Aufbau identisch mit `timeEntry.dragAction` | | `segmentSettings` | Objekt | Segment-Einstellungen für dieses einzelne Segment (z. B. `dayJump`) | | `splits` | Array | Eine Ebene Unterbrechungen in dieser Phase (z. B. Pause). Das Widget macht daraus: Teil 1 → Pause → Teil 2. | 💡 **Tipp:** Felder die nicht im Segment gesetzt werden, erben den Wert des übergeordneten `timeEntry`. So reicht es, nur die Unterschiede pro Segment zu definieren. #### splits – Pause in einer Phase `splits` teilt **eine** Phase (meist Kernarbeit), ohne dass du in Ninox „Kern Teil 1 / Kern Teil 2“ anlegen musst. Keine weitere Verschachtelung — nur diese eine Ebene. ```javascript { title: "Kernarbeit", dateFrom: 'Datum von', dateTo: 'Datum bis', timeFrom: time(11, 0), timeTo: time(14, 0), splits: [{ title: "Pause", timeFrom: time(12, 30), timeTo: time(13, 0), styles: { backgroundColor: "#94a3b8" } }] } ``` Oder relativ zum Phasen-Start: `offset: time(1, 30)`, `duration: time(0, 30)`. Mehrere Splits sind erlaubt; sie werden nach Startzeit sortiert und auf den Phasen-Zeitraum begrenzt. Kern-Stücke behalten Look und Titel der Phase, die Pause ihren eigenen. Unterschiedliche Titel für die Kern-Stücke (Teil 1 / Teil 2): **drei `segments`**, kein `splits` — z. B. `duration: time(1, 30)`, Pause, `duration: time(1, 30)`. #### linked – Segmente verknüpfen oder entkoppeln Mit `linked: true` (Standard) wird ein Segment Teil der **verbundenen Kette**. Beim Verschieben im Kalender bewegen sich alle verknüpften Segmente gemeinsam. Mit `linked: false` wird ein Segment **entkoppelt** – z. B. Anfahrt und Abfahrt, die auf einem anderen Tag liegen können und unabhängig verschoben werden sollen. Im **Placement-Ghost** (`createNew.item`) gilt dasselbe: eine Uhr auf einem `linked: false`-Satelliten nagelt den Kern nicht fest. #### Toolbar Unten rechts sitzt eine **kleine Toolbar**. Sie ist **immer sichtbar**, außer `segmentSettings.toolbar` ist `false`. **Schloss** — Segmente gemeinsam oder einzeln verschieben: - **Gesperrt** (Schloss zu): Alle Segmente des `timeEntry` werden gemeinsam verschoben - **Einzeln** (Schloss offen): `linked`-Segmente bilden eine Gruppe, `linked: false`-Segmente können unabhängig verschoben werden Ohne Segmente ist das Schloss inaktiv. Hover zeigt einen kurzen Hinweis. **Auge** — nur bei `createNew.item`: Ghost ein- und ausblenden. **Esc** blendet aus. Wieder einblenden nur über das Auge. Ausgeblendet gilt wieder Aufziehen, ohne die Auswahl in der Seitenleiste zu ändern. 💡 **Shift-Taste:** Während eines Drags oder beim **Placement-Ghost** hältst du **Shift**, um den Lock umzukehren: gespeicherte Satelliten bzw. Ghost-Satelliten wandern mit. Loslassen setzt Ghost-Satelliten auf ihre Uhr zurück. Das Schloss-Icon zeigt den Zustand live. 💡 **Shift + Resize:** Wenn du die Shift-Taste hältst und an einem Segment-Rand ziehst (oben/unten), verschieben sich alle angrenzenden Segmente mit – der **Gesamtblock wird größer**, statt dem Folge-Segment Zeit wegzunehmen. ## segmentSettings – Globale Einstellungen für Segmente Der optionale Parameter `**segmentSettings**` auf Ebene des gesamten Widgets steuert das **Standardverhalten aller Segmente** – z. B. ob Gruppen standardmäßig gesperrt sind oder ob die Toolbar angezeigt wird. ```javascript arcCustomCalendarWeek({ // ... segmentSettings: { lockGroups: true, edgeMagnetism: true, toolbar: true, dayJump: { threshold: time(22, 0), startTime: time(7, 0) } }, timeEntries: [...] }) ``` 📌 **Parameter im Überblick:** | Parameter | Typ | Beschreibung | |---|---|---| | `lockGroups` | Boolean | `true` = Segmente werden standardmäßig gemeinsam verschoben (Standard: `true`) | | `edgeMagnetism` | Boolean | `true` = Beim Resize bleibt die gemeinsame Grenze angrenzender Segmente synchron (Standard: `true`) | | `toolbar` | Boolean | `false` = Toolbar ausblenden. Sonst immer sichtbar (Schloss + Auge bei `createNew.item`) | | `dayJump.threshold` | Zeit | Ab dieser Uhrzeit springt ein Segment beim Drag auf den nächsten Tag | | `dayJump.startTime` | Zeit | Startzeit am neuen Tag nach dem Sprung | ⚠️ **Priorität:** Einstellungen auf `timeEntry`-Ebene (`segmentSettings` innerhalb eines Eintrags) überschreiben die globalen `segmentSettings`. Einstellungen auf Segment-Ebene überschreiben wiederum die Entry-Ebene. --- Mit dem Custom Calendar Week bringst du strukturierte Planung und volle Flexibilität in deine Ninox-Datenbank – individuell anpassbar, intuitiv bedienbar und bereit für deinen Alltag. Happy Widgeting 🥳 --- # Custom Calendar Grid Source: https://www.arc-rider.com/docs/widgets/custom-calendar-grid # CalendarGrid in React Termine in einer Monatsübersicht anzeigen und Tage oder Einträge auswählen. ## AI Defaults (read first) - Kopiere `examples/widgets/calendar_grid/calendar_grid-00-defaults.tsx` aus dem npm-Paket. - Daten sind Epoch-Millisekunden des Tages um 00:00 UTC (`Date.UTC(2027, 0, 4)`), wie im Beispiel. `dateTo` ist **inklusive**. Gleiches `dateFrom` und `dateTo` = ein Tag. ISO-Strings werden nicht akzeptiert. - Timed-Chip: `timeFrom` und `timeTo` als Millisekunden ab Mitternacht (`9 * 3600000` = 09:00). Ganztagsbalken: **beide Zeiten weglassen**. Dieselben Zeiten über mehrere Tage zeichnen einen Balken über diese Tage, bündig mit den Tagen; die Endzeit steht rechts. Bricht er in die nächste Woche um, zeigt die erste Zeile die Startzeit am rechten Rand des Starttags, die letzte Zeile wiederholt den Titel und zeigt die Endzeit. - `dateSettings.month` ist 1–12. Den Monat nicht unter `weekdaySettings` setzen. - `timeZoneBalance` verschiebt nur die Uhrzeit-Labels der Chips um ganze Stunden; `0` lassen, außer die Zeiten liegen in einer anderen Zone. - `onEntryClick`, `onDayClick` und `onEntryDragSave` verdrahten. Einträge sind nur mit `draggable: true` verschiebbar. Dasselbe Flag erlaubt es, den linken oder rechten Rand eines Eintrags zu ziehen, um ihn zu kürzen oder zu verlängern. Persistiere **`endData`** aus `onEntryDragSave(entryIndex, startData, endData)` — nicht `startData` (sonst Snap-back). - `title` ist ein String. JSX gehört in `customLayout`. Chip-Farbe: `styles.backgroundColor` und `styles.color` (`fontColor` fällt auf `color`). Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.5`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components). ## Vollständiges Beispiel ```tsx import { useState } from "react"; import { ArcWidgetCalendarGrid } from "@arcrider/arcwidgets-react"; import type { ArcWidgetCalendarGridDailyEntryDragSavePayload, ArcWidgetCalendarGridEntryDragSavePayload, ArcWidgetCalendarGridTimeEntry, } from "@arcrider/arcwidgets-react"; const day = (date: number) => Date.UTC(2027, 0, date); const at = (hours: number, minutes = 0) => (hours * 60 + minutes) * 60000; const initialEntries: ArcWidgetCalendarGridTimeEntry[] = [ { title: "Team Offsite", dateFrom: day(4), dateTo: day(5), draggable: true, }, { title: "Public Holiday", dateFrom: day(6), dateTo: day(6), draggable: true, }, { title: "Team Meeting", dateFrom: day(4), dateTo: day(4), timeFrom: at(9), timeTo: at(10, 30), draggable: true, }, { title: "Code Review", dateFrom: day(5), dateTo: day(5), timeFrom: at(10), timeTo: at(11), draggable: true, }, { title: "Workshop", subtitle: "Room 2", dateFrom: day(5), dateTo: day(5), timeFrom: at(13, 30), timeTo: at(17), draggable: true, }, { title: "Lunch", dateFrom: day(5), dateTo: day(5), timeFrom: at(12), timeTo: at(13), draggable: true, }, { title: "Retro", dateFrom: day(5), dateTo: day(5), timeFrom: at(17), timeTo: at(18), draggable: true, }, { title: "Sprint Planning", dateFrom: day(8), dateTo: day(8), timeFrom: at(9), timeTo: at(12), draggable: true, }, // Timed, several days in one week. The bar lines up with Thursday–Saturday. // 15:00 is written at the right edge. { title: "Conference", dateFrom: day(7), dateTo: day(9), timeFrom: at(11), timeTo: at(15), draggable: true, }, // Continues onto the next week row: that row repeats the title, and 11:00 // is written at the right edge of the last piece only. { title: "Client Workshop", dateFrom: day(8), dateTo: day(11), timeFrom: at(16), timeTo: at(11), draggable: true, }, { title: "Stand-up", dateFrom: day(18), dateTo: day(18), timeFrom: at(9), timeTo: at(9, 30), draggable: true, }, { title: "Design Review", dateFrom: day(20), dateTo: day(20), timeFrom: at(14), timeTo: at(16), draggable: true, }, { title: "All-Hands", dateFrom: day(22), dateTo: day(22), timeFrom: at(10), timeTo: at(11), draggable: true, }, { title: "Office Closed", dateFrom: day(29), dateTo: day(29), draggable: true, }, { title: "Release", subtitle: "v2.1", dateFrom: day(18), dateTo: day(22), timeFrom: at(10), timeTo: at(17), draggable: true, }, { title: "Onboarding", subtitle: "Berlin office", dateFrom: day(25), dateTo: day(28), timeFrom: at(9), timeTo: at(16), draggable: true, }, ]; export default function Example() { // Only the month and the entries are set; look and behavior are widget defaults. // A timed entry on one day is text with a stripe. The start time sits at the right edge. // Entries without times are light gray bars (Public Holiday, Team Offsite), with the same stripe. // A timed entry across days is the same kind of bar, lined up with the days. The end time sits at the right edge; if the bar wraps, its first row shows the start time at the right edge of the start day. // // Drag and save: `draggable: true` lets an entry be moved to another day. Its left and right // edge make it shorter or longer, from one day to several. The calendar only reports the // result. Write `endData` into the entry at `entryIndex`, then save it to your API. // If saving fails, put `startData` back. Without this step the entry jumps back on the next update. // // Day 5 has four timed entries. `maxTimeEntries.amount: 2` shows two and turns the rest into "+ 2 more". const [entries, setEntries] = useState(initialEntries); const persist = ( entryIndex: number, endData: | ArcWidgetCalendarGridEntryDragSavePayload | ArcWidgetCalendarGridDailyEntryDragSavePayload, ) => { setEntries((current) => current.map((entry, index) => index === entryIndex ? { ...entry, ...endData } : entry, ), ); }; return ( persist(entryIndex, endData) } onDailyEntryDragSave={(entryIndex, _startData, endData) => persist(entryIndex, endData) } /> ); } ``` ## Komponenten-Props ### id Optionale DOM-ID. Im Normalfall nicht nötig; ohne Angabe wird sie automatisch erzeugt. ### data Konfiguration vom Typ `ArcWidgetCalendarGridData`. Übergib Änderungen als neue Objekte; verwalte fachliche Daten und Auswahl in React-State. ### onAction Optionaler Fallback `(type: string, ...args: unknown[]) => unknown`. Der konkrete Callback hat Vorrang. Details: [State und Events](/docs/react/patterns/react-state-events). ### onEntryClick 0-basierter Index des geklickten Termins in `data.timeEntries`. Feuert nur bei Einträgen mit `clickable: true`. ```typescript onEntryClick?: (entryIndex: number) => void; ``` ### onDayClick Gewählter Tag als Epoch-Millisekunden um 00:00 UTC. Feuert nur mit `dateSettings.day.clickable: true`. ```typescript onDayClick?: (dateMillis: number) => void; ``` ### onEntryDragSave Drag-Ende eines Termins mit Uhrzeit: `startData` (vorher) und `endData` (Drop-Ziel) — persistiere `endData`. ```typescript onEntryDragSave?: ( entryIndex: number, startData: ArcWidgetCalendarGridEntryDragSavePayload, endData: ArcWidgetCalendarGridEntryDragSavePayload ) => void; ``` ### onDailyEntryDragSave Drag-Ende eines ganztägigen Termins: `startData` und `endData` nur mit `dateFrom` / `dateTo` — persistiere `endData`. ```typescript onDailyEntryDragSave?: ( entryIndex: number, startData: ArcWidgetCalendarGridDailyEntryDragSavePayload, endData: ArcWidgetCalendarGridDailyEntryDragSavePayload ) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### width Breite des Widgets, z. B. `"100%"`. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### timeZoneBalance Stundenversatz, der auf die Uhrzeit-Labels der Termine addiert wird, z. B. `-1`. Standard: `0`. Typ: `number`. ### styles Rahmen, Rasterlinien, Kopfzeilenfläche und Texttöne (`borders`, `borderColor`, `borderSize`, `borderRadius`, `backgroundColor`, `headerBackgroundColor`, `fontColor`, `mutedColor`, `subtleColor`). Typ: `ArcWidgetCalendarGridStyles`. ### dateSettings Sichtbarer Bereich, Kopfzeilen, Tageszellen und Markierungen. Setze `month` (`1`–`12`) und `year` für eine Monatsansicht über sechs Wochen, oder `startDate` mit `endDate` / `duration` (Tage) für einen freien Bereich. Ohne beides wird der aktuelle Monat gezeigt. Typ: `ArcWidgetCalendarGridDateSettings`. ### lang Locale für Wochentags- und Monatsnamen, z. B. `"en"`. Legt auch den ersten Wochentag fest: Sonntag bei `en-US`, `en-CA`, `ja`, `ko`, `zh-CN` und `zh-TW`, sonst Montag. Standard: `"de"`. Typ: `string`. ### timeEntries Termine mit Datumswerten, Uhrzeiten und Inhalten. Beide Zeiten weglassen für einen Ganztagsbalken. Typ: `ArcWidgetCalendarGridTimeEntry[]`. ## Verhalten und Grenzen - Für JSX in Terminen verwende `customLayout`. Die Standardfelder `title`, `subtitle` und `value` werden als Text oder HTML verarbeitet. - Datumswerte sind Epoch-Millisekunden des Tages um 00:00 UTC, z. B. `Date.UTC(2024, 10, 6)`; Uhrzeiten sind Millisekunden seit Tagesbeginn (`9 * 3600000` = 09:00). Beide Zeiten weglassen für einen Ganztagsbalken. Siehe [Datum und Zeit](/docs/react/patterns/react-dates). - `clickable: true` aktiviert die Auswahl. Ein Callback-Index ist keine Datensatz-ID; löse ihn über deine aktuelle Eintragsliste auf. - Drag: `draggable: true` auf dem Eintrag. Callback `onEntryDragSave(entryIndex, startData, endData)` / `onDailyEntryDragSave` — persistiere **`endData`** (`dateFrom`/`dateTo`/`timeFrom`/`timeTo`). `startData` ist der Stand vor dem Zug. Ohne `draggable: true` lässt sich der Eintrag nicht verschieben. Dasselbe Flag legt am echten Anfang und Ende des Eintrags eine Greifzone an: Ziehe daran, um den Eintrag tageweise zu kürzen oder zu verlängern, von einem Tag auf mehrere und zurück, auch über Wochenzeilen hinweg. Nur `dateFrom`/`dateTo` ändern sich, die Uhrzeiten bleiben. Schrumpft ein Eintrag mit Uhrzeit auf einen Tag und würde vor seinem Start enden, rückt `timeTo` knapp hinter `timeFrom`. Das Ergebnis kommt in denselben Callbacks wie beim Verschieben. Auf Touch-Geräten zuerst den Eintrag antippen, dann den Rand ziehen. Escape bricht ab. - `dateSettings.month` ist einsbasiert: Januar ist `1`. Markierungen liegen in `dateSettings.highlights`. Unterstützte `ArcWidgetSlot`-Felder erlauben einzelne JSX-Elemente, Text oder Widget-Deskriptoren. Reiche fremden Text als JSX-Text weiter; HTML-Strings werden als HTML interpretiert. [Inhalte kombinieren](/docs/react/patterns/nested-widget-descriptors). ## Datentypen Diese widgetbezogenen Typen werden aus dem Paket exportiert. Gemeinsame Typen wie `ArcWidgetSlot` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Die Konfigurationsobjekte sind geschlossen: Ein falsch geschriebener oder nicht unterstützter Schlüssel wird als Typfehler gemeldet. ```typescript export type ArcWidgetCalendarGridBorderLine = | boolean | { enabled?: boolean; color?: string; width?: string; }; export interface ArcWidgetCalendarGridBorders { color?: string; width?: string; outer?: ArcWidgetCalendarGridBorderLine; columns?: ArcWidgetCalendarGridBorderLine; rows?: ArcWidgetCalendarGridBorderLine; } export interface ArcWidgetCalendarGridStyles { borders?: ArcWidgetCalendarGridBorders; borderColor?: string; borderSize?: string; borderRadius?: string; backgroundColor?: string; headerBackgroundColor?: string; fontColor?: string; mutedColor?: string; subtleColor?: string; } export interface ArcWidgetCalendarGridHeaderText { label?: string; fontSize?: string; fontColor?: string; fontWeight?: string | number; alignX?: 'left' | 'center' | 'right'; } export interface ArcWidgetCalendarGridHeader { backgroundColor?: string; title?: ArcWidgetCalendarGridHeaderText; subtitle?: ArcWidgetCalendarGridHeaderText; } export interface ArcWidgetCalendarGridWeekdays { label?: string; backgroundColor?: string; alignX?: 'left' | 'center' | 'right'; fontSize?: string; fontColor?: string; fontWeight?: string | number; padding?: string; } export interface ArcWidgetCalendarGridWeekNumber { fontColor?: string; fontWeight?: string | number; fontSize?: string; } export interface ArcWidgetCalendarGridWeek { show?: boolean; number?: ArcWidgetCalendarGridWeekNumber; } export interface ArcWidgetCalendarGridDayNumber { fontSize?: string; fontColor?: string; fontWeight?: string | number; backgroundColor?: string; borderRadius?: string; border?: string; } export interface ArcWidgetCalendarGridMaxTimeEntries { amount?: number; suffix?: string; } export interface ArcWidgetCalendarGridDay { clickable?: boolean; height?: string; alignX?: 'left' | 'center' | 'right'; backgroundColor?: string; hoverBackgroundColor?: string; padding?: string; number?: ArcWidgetCalendarGridDayNumber; maxTimeEntries?: ArcWidgetCalendarGridMaxTimeEntries; } export interface ArcWidgetCalendarGridHighlight { weekday?: number; date?: number; day?: ArcWidgetCalendarGridDay; color?: string; } export interface ArcWidgetCalendarGridDateSettings { month?: number; year?: number; startDate?: number; endDate?: number; duration?: number; header?: ArcWidgetCalendarGridHeader; weekdays?: ArcWidgetCalendarGridWeekdays; week?: ArcWidgetCalendarGridWeek; day?: ArcWidgetCalendarGridDay; highlights?: ArcWidgetCalendarGridHighlight[]; } export type ArcWidgetCalendarGridEntryStyles = ArcWidgetCssProperties & { fontColor?: string; }; export interface ArcWidgetCalendarGridTimeEntry { id?: string | number; title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; styles?: ArcWidgetCalendarGridEntryStyles; borderLeft?: string; clickable?: boolean; draggable?: boolean; } export interface ArcWidgetCalendarGridData { width?: string; height?: string; timeZoneBalance?: number; lang?: string; styles?: ArcWidgetCalendarGridStyles; dateSettings?: ArcWidgetCalendarGridDateSettings; timeEntries?: ArcWidgetCalendarGridTimeEntry[]; } export interface ArcWidgetCalendarGridEntryDragSavePayload { dateFrom: number; dateTo: number; timeFrom: number; timeTo: number; } export interface ArcWidgetCalendarGridDailyEntryDragSavePayload { dateFrom: number; dateTo: number; } export interface ArcWidgetCalendarGridProps extends ArcReactWidgetProps { onEntryClick?: (entryIndex: number) => void; onDayClick?: (dateMillis: number) => void; onEntryDragSave?: ( entryIndex: number, startData: ArcWidgetCalendarGridEntryDragSavePayload, endData: ArcWidgetCalendarGridEntryDragSavePayload ) => void; onDailyEntryDragSave?: ( entryIndex: number, startData: ArcWidgetCalendarGridDailyEntryDragSavePayload, endData: ArcWidgetCalendarGridDailyEntryDragSavePayload ) => void; } ``` ## Einsatzbeispiele - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Calendar Grid ## AI Defaults (read first) - **uniqueId**: Required, `"cal-grid-" + Nr`. - **height**: Use `"100%"` when embedded in a layout. - **timeZoneBalance**: Set `-1` for German timezone. - **dateSettings.month/year**: 1–12 für Monat, Jahr für die Monatsansicht. - **dateFrom/dateTo**: Must be actual date field values in timeEntries. - **dragAction**: Requires `recordId` and field IDs via `fieldId(Nr, "Feldname")`. - **createNew**: Needs `tableId`, field IDs for target fields. - **Persistence**: Drag saves go through the **Action Performer** (`type: "update"`). Ninox `dragAction` config is unchanged. React hosts: handle via `onAction` — **no** global `database` stub. - **day.actions**: Array of actions with `##clickedDate##` placeholder – use instead of clickAction. # Custom Calendar Grid Mit dem Widget **Custom Calendar Grid** bringst du eine Monats-Grid-Kalenderansicht in deine Ninox-Datenbank. Es zeigt einen klassischen Kalendermonat mit Wochenzeilen und Tageszellen – ideal für Monatsübersichten, Urlaubsplanung oder Terminübersicht. 💡 **Was du mit Custom Calendar Grid machen kannst:** - **Monatsansicht:** Klassisches 6-Wochen-Grid mit Wochentagen und Kalenderwochen. - **Zeit- und ganztägige Einträge:** Zeige Termine mit Uhrzeit und mehrtägige Events. - **Drag & Drop:** Verschiebe Termine direkt im Kalender. - **Klick auf Tag:** Führe Aktionen aus mit dem geklickten Datum als Platzhalter. - **Hervorhebung:** Markiere Wochenenden, Feiertage oder besondere Tage. - **Individuelles Styling:** Passe Header, Wochentage und Tageszellen an. ## Anwendungscode ```javascript let current := this; arcCustomCalendarGrid({ uniqueId: "cal-grid-" + Nr, embedded: false, height: "100%", timeZoneBalance: -1, lang: clientLang(), dateSettings: { month: month(today()), year: year(today()), header: { title: { label: "##monthLong## ##year##", fontSize: "18px", fontColor: "#333" } }, weekdays: { label: "##weekDayShort##", fontSize: "13px" }, week: { show: true }, day: { height: "120px", alignX: "left", actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "helper_ui_state_backoffice"), dataBag: { base: helper_ui_state_backoffice, patch: { cal_view: "1", cal_date: "##clickedDate##" } } }] }, highlights: [ { weekday: 6, day: { backgroundColor: "#fafafa" } }, { weekday: 0, day: { backgroundColor: "#fafafa" } } ] }, timeEntries: (select Termine where month(Termin) = month(today()) and year(Termin) = year(today())).[{ title: Bezeichnung, dateFrom: Termin, dateTo: Termin, timeFrom: von, timeTo: bis, styles: { backgroundColor: "#3388FF", color: "#fff", borderRadius: "6px" }, dragAction: { recordId: Nr, dateFrom: "A", timeFrom: "B", dateTo: "C", timeTo: "D" }, clickAction: { type: "popup", recordId: Nr } }] }) ``` ## dateSettings – Zeitraum & Darstellung ### month / year Definiert den anzuzeigenden Monat. `month` ist 1–12 (Januar = 1), `year` ist die Jahreszahl. ```javascript dateSettings: { month: 11, year: 2024 } ``` ### startDate / endDate / duration (optional) Alternativ zu `month`/`year` kannst du einen expliziten Zeitraum definieren: - **startDate**: Startdatum (Millisekunden oder Ninox-Datum) - **endDate**: *(optional)* Enddatum. Hat Vorrang vor `duration`. - **duration**: *(optional)* Anzahl der anzuzeigenden Tage. Wird nur berücksichtigt, wenn `endDate` nicht gesetzt ist. ```javascript dateSettings: { startDate: date(2024, 11, 1), endDate: date(2024, 11, 30) } ``` Oder mit `duration` statt `endDate`: ```javascript dateSettings: { startDate: date(2024, 11, 1), duration: 42 } ``` ### day – Tageszellen & Klick-Aktionen Unter `dateSettings.day` steuerst du das Aussehen der Tageszellen und die Aktionen beim Klick auf einen Tag. #### day.actions – Aktionen beim Klick auf einen Tag Mit `day.actions` definierst du ein **Array von Aktionen**, die beim Klick auf eine Tageszelle ausgeführt werden. Der Platzhalter `##clickedDate##` wird durch den **Millisekunden-Timestamp** des geklickten Datums ersetzt. ```javascript day: { actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "helper_ui_state_backoffice"), dataBag: { base: helper_ui_state_backoffice, patch: { cal_view: "1", cal_date: "##clickedDate##" } } }] } ``` **Platzhalter:** | Platzhalter | Ersetzt durch | |-------------|---------------| | `##clickedDate##` | Millisekunden-Timestamp des geklickten Datums (z. B. `1731369600000`) | **Action-Typen:** Wie bei `arcCustomInput` und anderen Widgets – `update`, `popup`, `openUrl`, etc. Siehe `action_performer` für alle unterstützten Typen. **Wichtig:** Der Platzhalter wird rekursiv in allen String-Werten der Action ersetzt – auch in verschachtelten Objekten (z. B. in `value` bei JSON-State-Updates). #### Weitere day-Eigenschaften - `height`: Mindesthöhe der Tageszelle (z. B. `"120px"`) - `alignX`: `"left"`, `"center"`, `"right"` für die Ausrichtung der Tagesnummer - `backgroundColor`, `hoverBackgroundColor`: Hintergrundfarben - `padding`: Innenabstand - `number`: Styling für die Tagesnummer (fontSize, fontColor, containerSize, etc.) - `maxTimeEntries`: Begrenzt die sichtbaren Einträge pro Tag (siehe unten) #### maxTimeEntries – Begrenzung sichtbarer Einträge pro Tag Wenn ein Tag mehr Einträge hat als `amount`, werden nur die ersten angezeigt. Ein Hinweis „+ X weitere“ erscheint; Klick darauf löst dieselbe Aktion aus wie ein Klick auf die Tageszelle (`day.actions`). ```javascript maxTimeEntries: { amount: 5, suffix: "weitere" } ``` | Eigenschaft | Bedeutung | |-------------|-----------| | `amount` | Maximale Anzahl sichtbarer Einträge. Ab dem (amount+1). Eintrag wird gekürzt. | | `suffix` | Text für „+ X suffix“ (z. B. `"weitere"` → „+ 3 weitere“). Standard: `"more"`. | ### week – Kalenderwochennummer Mit `week.show` zeigst du die ISO-Kalenderwoche am ersten Tag jeder Woche an: ```javascript week: { show: true, number: { fontSize: "12px", fontColor: "#999", fontWeight: "normal" } } ``` ### header – Monats-/Jahres-Header - `backgroundColor`: Hintergrundfarbe - `title`: `label`, `fontSize`, `fontColor`, `fontWeight`, `alignX` - `subtitle`: *(optional)* Zweite Zeile mit `label`, `fontSize`, `fontColor`, `fontWeight`, `alignX` Platzhalter für `label`: `##monthLong##`, `##year##`, `##weekDayShort##` etc. Siehe `custom-calendar-week` für alle Platzhalter. ### weekdays – Wochentagszeile - `label`: Platzhalter (z. B. `"##weekDayShort##"`) - `backgroundColor`, `fontSize`, `fontColor`, `fontWeight`, `padding`, `alignX` ### highlights – Hervorhebung von Tagen Hervorhebung nach Wochentag oder konkretem Datum: ```javascript highlights: [ { weekday: 6, day: { backgroundColor: "#fafafa" } }, { weekday: 0, day: { backgroundColor: "#fafafa" } }, { date: 1731369600000, day: { number: { containerSize: "32px", backgroundColor: "#4970ff", borderRadius: "50%", fontColor: "#fff" } } } ] ``` - `weekday`: 0–6 (Sonntag = 0, Samstag = 6) - `date`: Millisekunden-Timestamp für ein bestimmtes Datum - `day`: Styling für die Tageszelle (backgroundColor, number, padding, etc.) ## styles – Gesamtkalender-Styling Über `styles` kannst du den **äußersten** Kalender-Container gestalten. Alle Einstellungen (z. B. `borderRadius`, `border`, `borderColor`, `backgroundColor`) werden direkt auf den äußersten Container angewendet. Standard-Border-Radius ist `8px`. Wenn `borderRadius` gesetzt ist, erhält der Container automatisch `overflow: hidden`, damit abgerundete Ecken sauber dargestellt werden. ```javascript styles: { borderRadius: "8px", borderColor: "#ddd", backgroundColor: "#fff" } ``` ## timeEntries – Kalendereinträge Jeder Eintrag benötigt: - `dateFrom`, `dateTo`: Datum (Millisekunden) - `timeFrom`, `timeTo`: Optional – Uhrzeit in Millisekunden (0 = Mitternacht). Ohne Zeit = ganztägiger Eintrag - `title`, `subtitle`, `value`: Anzeige (oder `customLayout` für eigene HTML-Struktur) - `styles`: CSS-ähnliche Styles (backgroundColor, color, borderRadius, padding) - `dragAction`: Für Drag & Drop; `recordId`, `dateFrom`, `timeFrom`, `dateTo`, `timeTo` als Feld-IDs. Persistenz über Action Performer (`type: "update"`); React: `onAction`, kein globales `database`. - `clickAction`: Für Klick auf den Eintrag (z. B. `{ type: "popup", recordId: Nr }`) **customLayout:** HTML-String für individuelle Darstellung statt `title`/`subtitle`/`value`. Bei Ganztagseinträgen ersetzt der Platzhalter `##entrytime##` die Zeitspanne (z. B. „00:00 - 23:59“ bei eintägigen Events). ## createNew – Erstellen neuer Einträge Definiert, wie neue Einträge erstellt werden (z. B. beim Klick auf einen leeren Tag): ```javascript createNew: { tableId: "B", dateFrom: "A", timeFrom: "B", dateTo: "C", timeTo: "D" } ``` --- # Custom Calendar Timeline Source: https://www.arc-rider.com/docs/widgets/custom-calendar-timeline # CalendarTimeline in React Zeiträume pro Ressource oder Objekt auf einer gemeinsamen Zeitachse anzeigen. Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.5`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components). ## Vollständiges Beispiel ## Features ausprobieren ### Zeitraum, Zoom, Unterressourcen und Gestaltung Nutze die Zoom-Tasten im Widget, ändere den Zeitraum und blende die Unterressource ein. Die Balken verwenden JSX; die Klick-Callbacks zeigen die tatsächlichen Zeilenindizes. ## Komponenten-Props ### id Optionale DOM-ID. Im Normalfall nicht nötig; ohne Angabe wird sie automatisch erzeugt. ### data Konfiguration vom Typ `ArcWidgetCalendarTimelineData`. Übergib Änderungen als neue Objekte; verwalte fachliche Daten und Auswahl in React-State. ### onAction Optionaler Fallback `(type: string, ...args: unknown[]) => unknown`. Der konkrete Callback hat Vorrang. Details: [State und Events](/docs/react/patterns/react-state-events). ### onEntryClick Ressourcenzeile und Termin des geklickten Balkens. `rowIndex` zählt die Zeilen der aufgefalteten `items`-Liste (Eltern vor ihren Kindern); `entryIndex` ist 0-basiert in den `timeEntries` dieser Zeile. ```typescript onEntryClick?: (rowIndex: number, entryIndex: number) => void; ``` ### onEntryDragSave Drag-Ende: `startData` (vorher) und `endData` (Drop-Ziel) — persistiere `endData`; die Zielzeile ist `rowChange.row_index`. Im Tagesmodus kommen `dateFrom` / `dateTo`, im Zeitmodus `timeFrom` / `timeTo`. ```typescript onEntryDragSave?: ( rowIndex: number, entryIndex: number, startData: ArcWidgetCalendarTimelineEntryDragSavePayload, rowChange: ArcWidgetCalendarTimelineRowChangePayload, endData: ArcWidgetCalendarTimelineEntryDragSavePayload ) => void; ``` ### onLabelClick Ressourcenzeile und 0-basierte Beschriftungsspalte der geklickten Zelle. Feuert nur bei Zellen mit `clickable: true`. ```typescript onLabelClick?: (rowIndex: number, columnIndex: number) => void; ``` ### onGhostClick Ressourcenzeile und Spaltenbeginn des geklickten Plus-Buttons: Epoch-Millisekunden im Tagesmodus, Millisekunden ab Mitternacht im Zeitmodus. ```typescript onGhostClick?: (rowIndex: number, columnValue: number) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### mode Spaltentyp: `"day"` zeigt Tage, `"time"` die Stunden eines Tages. Standard: `"time"`, wenn `timeSettings` gesetzt ist, sonst `"day"`. Typ: `'day' | 'time'`. ### width Breite des Widgets, z. B. `"100%"`. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. `"auto"` wächst mit den Zeilen; Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### minHeight / maxHeight Grenzen für `height: "auto"`, z. B. `"600px"`; über `maxHeight` scrollt die Timeline. Typ: `string`. ### timeZoneBalance Stundenversatz, der auf die Uhrzeit-Labels der Balken addiert wird, z. B. `-1`. Standard: `0`. Typ: `number`. ### weekStart Erster Tag der Standardwoche, wenn `dateSettings.startDate` fehlt: `1` = Montag … `7` = Sonntag. Standard: `1`. Typ: `number`. ### dateSettings Sichtbarer Bereich, Kopfzeilen und Markierungen. `startDate` (Epoch-Millisekunden) ist der erste sichtbare Tag; Termindaten werden relativ dazu platziert. `endDate` (exklusiv) oder `duration` (Tage, Standard `7`) legt die Länge fest. Typ: `ArcWidgetCalendarTimelineDateSettings`. ### timeSettings Sichtbare Stunden im Zeitmodus, in **Stunden**: `startTime` (Standard `0`), `amount` pro Spalte (Standard `1`) und `duration` (Standard `12`). Typ: `ArcWidgetCalendarTimelineTimeSettings`. ### columns Linke Beschriftungsspalten der Ressourcen. Ohne Angabe wird eine Spalte mit `labelWidth` gezeigt. Typ: `ArcWidgetCalendarTimelineColumn[]`. ### labelWidth Breite der Beschriftungsspalten ohne eigenes `width`. Standard: `"120px"`. Typ: `string`. ### items Ressourcenzeilen mit Beschriftungszellen, `timeEntries`, Markierungen, Plus-Button (`ghost`) und Unterzeilen (`items`). Typ: `ArcWidgetCalendarTimelineItem[]`. ### maxItemDepth Maximale Verschachtelungstiefe von `items`; `0` zeigt nur die oberste Ebene. Standardmäßig ohne Grenze. Typ: `number`. ### collapsible Macht Elternzeilen über ihre erste Beschriftungszelle einklappbar. Standard: aus. Typ: `ArcWidgetCalendarTimelineCollapsible`. ### dayWidth Mindestbreite einer Tagesspalte, z. B. `"120px"`. Typ: `string`. ### columnWidth Mindestbreite einer Tages- oder Stundenspalte; hat Vorrang vor `dayWidth`. Typ: `string`. ### rowHeight Mindesthöhe einer Ressourcenzeile. Standard: `"80px"`. Typ: `string`. ### currentTime `true` zeigt eine Linie für die aktuelle Uhrzeit; ein Farbstring setzt zusätzlich ihre Farbe. Standard: `false`. Typ: `boolean | string`. ### zoomSettings Zoom der Tagesbreite: Grenzen, Schrittweite, Speichern und die Plus-/Minus-Buttons. Zoom ist standardmäßig an. Typ: `ArcWidgetCalendarTimelineZoomSettings`. ### lang Locale für Datumsbeschriftungen. Standard: `"de"`. Typ: `string`. ### styles Rahmen, Rasterlinien, Flächen, Texttöne und Tagesstreifen. Typ: `ArcWidgetCalendarTimelineStyles`. ## Verhalten und Grenzen - Für JSX in Terminen verwende `customLayout` oder `tooltip.value`. Die Standardfelder `title`, `subtitle` und `value` werden als Text oder HTML verarbeitet. Beschriftungszellen und der Kopf-Titel akzeptieren HTML-Strings oder Widget-Deskriptoren, kein JSX. - Datumswerte sind Epoch-Millisekunden in derselben Zeitbasis wie `dateSettings.startDate`; Uhrzeiten sind Millisekunden seit Tagesbeginn (`9 * 3600000` = 09:00). Siehe [Datum und Zeit](/docs/react/patterns/react-dates). - `clickable: true` aktiviert die Auswahl. Ein Callback-Index ist keine Datensatz-ID; löse ihn über deine aktuelle Eintragsliste auf. - Drag: `draggable: true` auf dem Eintrag. Callback `onEntryDragSave(rowIndex, entryIndex, startData, rowChange, endData)` — persistiere **`endData`** und die Zielzeile aus `rowChange.row_index`. Ohne `draggable: true` lässt sich der Eintrag nicht verschieben. - `items` enthält Ressourcenzeilen, darin liegen die `timeEntries`. `columns` definiert die linken Beschriftungsspalten. - `onEntryClick(rowIndex, entryIndex)` verwendet zwei Indizes. `dayWidth` und `zoomSettings` steuern die Skalierung. Unterstützte `ArcWidgetSlot`-Felder erlauben einzelne JSX-Elemente, Text oder Widget-Deskriptoren. Reiche fremden Text als JSX-Text weiter; HTML-Strings werden als HTML interpretiert. [Inhalte kombinieren](/docs/react/patterns/nested-widget-descriptors). ## Datentypen Diese widgetbezogenen Typen werden aus dem Paket exportiert. Gemeinsame Typen wie `ArcWidgetSlot`, `ArcWidgetCssStyles`, `ArcNestedWidgetDescriptor` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Die Konfigurationsobjekte sind geschlossen: Ein falsch geschriebener oder nicht unterstützter Schlüssel wird als Typfehler gemeldet. ```typescript export type ArcWidgetCalendarTimelineBorderLine = | boolean | { enabled?: boolean; color?: string; width?: string; }; export interface ArcWidgetCalendarTimelineBorders { color?: string; width?: string; outer?: ArcWidgetCalendarTimelineBorderLine; columns?: ArcWidgetCalendarTimelineBorderLine; rows?: ArcWidgetCalendarTimelineBorderLine; } export interface ArcWidgetCalendarTimelineDayStripes { enabled?: boolean; odd?: string; even?: string; } export interface ArcWidgetCalendarTimelineStyles { borders?: ArcWidgetCalendarTimelineBorders; borderColor?: string; borderSize?: string; borderRadius?: string; backgroundColor?: string; headerBackgroundColor?: string; groupBackgroundColor?: string; rowBackgroundColor?: string; fontColor?: string; mutedColor?: string; subtleColor?: string; fontSize?: string; dayStripes?: boolean | ArcWidgetCalendarTimelineDayStripes; } export interface ArcWidgetCalendarTimelineTitleSlot { value?: string | ArcNestedWidgetDescriptor; customLayout?: string | ArcNestedWidgetDescriptor; alignX?: 'left' | 'center' | 'right'; fontSize?: string; fontColor?: string; fontWeight?: string | number; backgroundColor?: string; } export interface ArcWidgetCalendarTimelineLabelBreakpoint { below: number; label: string; } export interface ArcWidgetCalendarTimelineDaysHeader { visible?: boolean; label?: string; labelBreakpoints?: ArcWidgetCalendarTimelineLabelBreakpoint[]; hideBelow?: number; compactRanges?: boolean; monthCompactBelow?: number; backgroundColor?: string; fontColor?: string; fontSize?: string; fontWeight?: string | number; alignX?: 'left' | 'center' | 'right'; sticky?: 'left'; stickyPadding?: number | string; } export interface ArcWidgetCalendarTimelineGroupHighlight { month?: number; week?: number; color?: string; } export interface ArcWidgetCalendarTimelineGroupHeader { visible?: boolean; label?: string; hideBelow?: number; backgroundColor?: string; fontColor?: string; fontSize?: string; fontWeight?: string | number; alignX?: 'left' | 'center' | 'right'; sticky?: 'left'; stickyPadding?: number | string; highlights?: ArcWidgetCalendarTimelineGroupHighlight[]; } export interface ArcWidgetCalendarTimelineHeader { backgroundColor?: string; fontColor?: string; title?: string | ArcNestedWidgetDescriptor | ArcWidgetCalendarTimelineTitleSlot; days?: ArcWidgetCalendarTimelineDaysHeader; months?: ArcWidgetCalendarTimelineGroupHeader; weeks?: ArcWidgetCalendarTimelineGroupHeader; } export interface ArcWidgetCalendarTimelineHighlightStyles { backgroundColor?: string; borderColor?: string; borderSize?: string; borders?: Pick; } export interface ArcWidgetCalendarTimelineHighlight { date?: number | string; weekday?: number; dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; hour?: number; color?: string; styles?: ArcWidgetCalendarTimelineHighlightStyles; } export interface ArcWidgetCalendarTimelineDateHighlight extends ArcWidgetCalendarTimelineHighlight { applyToRows?: boolean; title?: { label?: string; }; } export interface ArcWidgetCalendarTimelineDateSettings { startDate?: number; endDate?: number; duration?: number; header?: ArcWidgetCalendarTimelineHeader; highlights?: ArcWidgetCalendarTimelineDateHighlight[]; } export interface ArcWidgetCalendarTimelineTimeSettings { startTime?: number; amount?: number; duration?: number; } export interface ArcWidgetCalendarTimelineColumn { title?: string; subtitle?: string; width?: string; alignX?: 'left' | 'center' | 'right'; alignY?: 'top' | 'center' | 'bottom'; fontSize?: string; fontWeight?: string | number; backgroundColor?: string; fontColor?: string; } export interface ArcWidgetCalendarTimelineItemColumn { title?: string; subtitle?: string; customLayout?: string | ArcNestedWidgetDescriptor; clickable?: boolean; alignX?: 'left' | 'center' | 'right'; alignY?: 'top' | 'center' | 'bottom'; fontSize?: string; fontWeight?: string | number; } export interface ArcWidgetCalendarTimelineItemStyles { backgroundColor?: string; fontSize?: string; fontWeight?: string | number; } export interface ArcWidgetCalendarTimelineGhost { title?: string; clickable?: boolean; position?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft' | 'timeEntry'; styles?: ArcWidgetCssStyles; } export interface ArcWidgetCalendarTimelineEntryLabel { value?: string | number; width?: 'fraction' | 'auto' | (string & {}); } export interface ArcWidgetCalendarTimelineTooltipStyles { backgroundColor?: string; color?: string; borderRadius?: string; borderSize?: string; borderColor?: string; padding?: string; fontSize?: string; boxShadow?: string; } export interface ArcWidgetCalendarTimelineTooltip { value?: ArcWidgetSlot; position?: 'top' | 'bottom' | 'left' | 'right' | 'auto'; width?: string; maxWidth?: string; maxHeight?: string; styles?: ArcWidgetCalendarTimelineTooltipStyles; } export type ArcWidgetCalendarTimelineEntryStyles = ArcWidgetCssProperties; export interface ArcWidgetCalendarTimelineTimeEntry { id?: string | number; title?: string | ArcWidgetCalendarTimelineEntryLabel; subtitle?: string | ArcWidgetCalendarTimelineEntryLabel; direction?: 'vertical' | 'horizontal'; value?: string; customLayout?: ArcWidgetSlot; dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; styles?: ArcWidgetCalendarTimelineEntryStyles; clickable?: boolean; draggable?: boolean; tooltip?: ArcWidgetCalendarTimelineTooltip; } export interface ArcWidgetCalendarTimelineItem { id?: string | number; title?: string; subtitle?: string; styles?: ArcWidgetCalendarTimelineItemStyles; fontSize?: string; fontWeight?: string | number; columns?: ArcWidgetCalendarTimelineItemColumn[]; timeEntries?: ArcWidgetCalendarTimelineTimeEntry[]; highlights?: ArcWidgetCalendarTimelineHighlight[]; ghost?: ArcWidgetCalendarTimelineGhost; items?: ArcWidgetCalendarTimelineItem[]; } export interface ArcWidgetCalendarTimelineCollapsible { enabled?: boolean; iconCollapse?: string; iconExpand?: string; iconPosition?: 'left' | 'right'; } export interface ArcWidgetCalendarTimelineZoomControls { enabled?: boolean; position?: 'bottomRight'; } export interface ArcWidgetCalendarTimelineZoomSettings { enabled?: boolean; defaultDayWidth?: number | string; minDayWidth?: number | string; maxDayWidth?: number | string; step?: number | string; persist?: boolean; controls?: ArcWidgetCalendarTimelineZoomControls; } export interface ArcWidgetCalendarTimelineData { mode?: 'day' | 'time'; width?: string; height?: string; minHeight?: string; maxHeight?: string; lang?: string; timeZoneBalance?: number; weekStart?: number; dateSettings?: ArcWidgetCalendarTimelineDateSettings; timeSettings?: ArcWidgetCalendarTimelineTimeSettings; columns?: ArcWidgetCalendarTimelineColumn[]; labelWidth?: string; items?: ArcWidgetCalendarTimelineItem[]; maxItemDepth?: number; collapsible?: ArcWidgetCalendarTimelineCollapsible; dayWidth?: string; columnWidth?: string; rowHeight?: string; currentTime?: boolean | string; zoomSettings?: ArcWidgetCalendarTimelineZoomSettings; styles?: ArcWidgetCalendarTimelineStyles; } export interface ArcWidgetCalendarTimelineEntryDragSavePayload { dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; } export interface ArcWidgetCalendarTimelineRowChangePayload { start_row_index: number; row_index: number; } export interface ArcWidgetCalendarTimelineProps extends ArcReactWidgetProps { onEntryClick?: (rowIndex: number, entryIndex: number) => void; onEntryDragSave?: ( rowIndex: number, entryIndex: number, startData: ArcWidgetCalendarTimelineEntryDragSavePayload, rowChange: ArcWidgetCalendarTimelineRowChangePayload, endData: ArcWidgetCalendarTimelineEntryDragSavePayload ) => void; onLabelClick?: (rowIndex: number, columnIndex: number) => void; onGhostClick?: (rowIndex: number, columnValue: number) => void; } ``` ## Einsatzbeispiele - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Calendar Timeline ## AI Defaults (read first) - **uniqueId**: Required, `"timeline-" + Nr`. - **height**: Use `"700px"` or `"100%"` when embedded; `"auto"` lets the widget grow with content (Ninox form scrolls). - **timeZoneBalance**: Set `-1` for German timezone. - **header**: `dateSettings.header.days` (Tages-Labels). Optional `header.months` / `header.weeks`. **`header.title`**: Titel/Layout oben links über den Spaltennamen (Text, HTML oder Widget) — nicht mehr die Tageszeile. Reiner Text ohne Extra-Typo: **16px / Schriftstärke 400**. Alte Configs mit `header.title.label` (`##day##` …) ohne `value`/`customLayout` gelten weiter als Tageszeile. - **columns**: Label-Spalten links (Titel im Header, optional `width`); mindestens eine Spalte. Header-Farbe default = `dateSettings.header.days` (`backgroundColor` / `fontColor`); pro Spalte überschreibbar. - **items**: Ressourcen-Zeilen; jedes Item braucht `id` (eindeutig pro Zeile) und `timeEntries`. Verschachtelung über `items` (max. eine Ebene unterhalb des Parents). - **dateFrom/dateTo**: Actual date values in timeEntries, not strings. - **dayWidth**: Default `"120px"`, increase for detailed timelines. Startbreite, wenn kein gespeicherter Zoom existiert; überschreibbar via `zoomSettings.defaultDayWidth`. - **zoomSettings**: Optional. Zoom ist standardmäßig aktiv (`enabled: false` zum Abschalten). `defaultDayWidth`, `minDayWidth`, `maxDayWidth`, `step`, `persist` (LocalStorage pro `uniqueId`), `controls.enabled` für +/- unten rechts. - **dragAction**: `recordId` + optional `resourceField` für vertikal; nur **gleiche Verschachtelungstiefe** (`_depth`, z. B. Mitarbeiter ↔ Mitarbeiter auch **über Teams hinweg**). Optional `enabled: false` schaltet Drag für diesen Balken aus. Optional `changeFieldValues` für Zusatzfelder nach erfolgreichem Drag (z. B. Cache-/Trigger-Felder). - **Uhrzeit mode**: Activated by `timeSettings`, uses `columnWidth` instead of `dayWidth`. `timeFrom`/`timeTo` in milliseconds. - **styles.borders**: Gleicher Vertrag wie Table (`outer` / `columns` / `rows`). Default: Rahmen an, Zeilenlinien an, **Spaltenlinien aus**. `outer: false` für bündig im Layout. Dark Mode über `backgroundColor`, `headerBackgroundColor`, `fontColor`, `borderColor`. - **styles.dayStripes**: Default **an** — Tagesspalten abwechselnd Weiß / sehr helles Grau. `false` schaltet aus; `{ odd, even }` setzt eigene Farben. Wochenend-Highlights liegen darüber. - **Zellen-Hintergrund**: `dateSettings.highlights[]` mit optional `applyToRows: true`; optional `items[].highlights` pro Zeile (`date`, `dateFrom`/`dateTo` bzw. `timeFrom`/`timeTo`). - **Ghost (Neueinträge)**: Optional **pro Zeile** `items[].ghost` mit **`title`**, **`actions`**, optional **`position`** (Default **`bottomRight`**: kleiner Plus-Button unten rechts in der Spalte), optional **`styles`**. Klick führt `ghost.actions` aus. **Positionen außer `timeEntry`**: Plus erscheint erst bei **Maus über der Spalte** (Spalte wird per Zeilen-`mousemove` erkannt); **Touch**: Ghost bleibt sichtbar. **`timeEntry`**: siehe Abschnitt ghost (Idle nur in leeren Spalten). Details im Abschnitt **ghost**. - **tooltip** *(pro Balken)*: Optional. Hover-Tooltip mit `value` (Text **oder** Widget-Deskriptor `{ widget, uid, data }`), optional `width` / `maxWidth` / `maxHeight` / `position` / `styles`. Portaliert an `document.body` (kein Clipping). Details im Abschnitt **tooltip**. Mit dem Widget **Custom Calendar Timeline** bringst du eine ressourcenbasierte Kalenderansicht in deine Ninox-Datenbank. Ressourcen (z. B. Mitarbeiter, Räume, Fahrzeuge) werden als Zeilen dargestellt, die Tage oder Uhrzeiten als Spalten. Termine erscheinen als horizontale Balken – ideal für Einsatzplanung, Kapazitätsübersicht oder Ressourcenplanung. 💡 **Was du mit Custom Calendar Timeline machen kannst:** - **Ressourcen als Zeilen:** Jede Zeile steht für eine Ressource (Person, Raum, Team, Fahrzeug). - **Termine als Balken:** Einträge werden horizontal über die Tage gespannt – mit oder ohne Uhrzeit. - **Uhrzeitmodus:** Zeige statt Tagen Uhrzeitspalten an – ideal für Flottendiagramme und Tageseinsatzplanung. - **Hierarchische Einträge:** `items` mit Unter-`items` (eine Ebene), flache Zeilen mit Einrückung – Datenmodell wie Gantt. - **Drag & Drop:** Verschiebe Termine horizontal (Datum/Uhrzeit ändern) oder vertikal (Ressource/Zeile wechseln). - **Mehrtägige Einträge:** Zeige Projekte, Urlaub oder längere Blöcke über mehrere Tage. - **Hervorhebung:** Markiere Wochenenden, bestimmte Stunden oder besondere Tage farblich. - **Individuelles Styling:** Passe Farben, Breiten und Höhen an dein Design an. 📅 **Custom Calendar Timeline** eignet sich besonders für Einsatzplanung, Schichtpläne, Kapazitätsübersichten und **Flottendiagramme** – überall dort, wo du Ressourcen und Termine im Zeitverlauf im Blick behalten willst. ## Anwendungscode ```javascript let current := this; arcCustomCalendarTimeline({ uniqueId: "timeline-1", timeZoneBalance: 0, lang: "de", dateSettings: { startDate: today(), duration: 30, highlights: [ { weekday: 6, color: "rgba(59,130,246,0.06)" }, { weekday: 0, color: "rgba(59,130,246,0.06)" } ], header: { days: { label: "##weekDayShort## ##day##.", fontSize: "12px", fontColor: "#52525b", fontWeight: "600", backgroundColor: "#fafafa" } } }, dayWidth: "120px", rowHeight: "auto", currentTime: "rgba(59,130,246,0.5)", height: "700px", styles: { backgroundColor: "#ffffff" }, columns: [ { title: "Name", width: "200px" }, { title: "Abteilung", width: "130px" } ], items: (select Mitarbeiter).[{ id: Nr, title: Name, subtitle: Abteilung, columns: [ { title: text(Name) }, { title: text(Abteilung) } ], timeEntries: (select Termine where Bearbeiter = Nr).[{ title: Bezeichnung, subtitle: Kunde, dateFrom: Datum_von, dateTo: Datum_bis, timeFrom: Zeit_von, timeTo: Zeit_bis, styles: { backgroundColor: text(Farbe), color: "#fff", borderRadius: "6px", padding: "4px 8px" }, dragAction: { recordId: Nr, dateFrom: fieldId(Nr, "Datum_von"), dateTo: fieldId(Nr, "Datum_bis"), resourceField: fieldId(Nr, "Bearbeiter"), changeFieldValues: [{ fieldId: fieldId(Nr, "CacheTrigger"), value: now() }] }, actions: [{ type: "popup", recordId: Nr }] }] }] }) ``` ## Allgemeine Settings ### uniqueId Eindeutige Kennung des Widgets. Wichtig, wenn mehrere Timeline-Widgets auf einer Seite verwendet werden. ```javascript uniqueId: "timeline-1", ``` ### timeZoneBalance Zeitzonenkorrektur in Stunden. Für Deutschland oft `-1`, um die Anzeige an MEZ/MESZ anzupassen. ```javascript timeZoneBalance: 0, timeZoneBalance: -1, ``` ### lang Sprache für Datumsformatierung (z. B. Wochentagsnamen). ```javascript lang: "de", lang: clientLang(), ``` ### height Höhe des Widget-Containers. - `"700px"` oder `"100%"` — feste Höhe mit internem Scroll (Standard in Ninox-Embeds). - `"auto"` — die Timeline wächst mit dem Inhalt; das Ninox-Formular scrollt vertikal (kein innerer Vertikal-Scroll). Horizontal scrollt die Timeline weiterhin innerhalb des Widgets. - Mit `maxHeight` kann die automatische Höhe begrenzt werden; dann scrollt die Timeline intern vertikal. ```javascript height: "700px", height: "100%", height: "auto", maxHeight: "600px", minHeight: "200px", ``` ### styles Rahmen und Gitterlinien leben in **`styles`** — zusammen mit Hintergrund, Radius und Linienfarbe. Gleicher Vertrag wie Custom Table. Default: abgerundeter Außenrahmen, Zeilenlinien, **keine vertikalen Spaltenlinien** — wie Table. Jedes Linienfeld ist `true` / `false` **oder** `{ color, width }`. | Prop | Default | Bedeutung | |---|---|---| | `styles.backgroundColor` | — | Fläche inkl. Label-Spalten links | | `styles.headerBackgroundColor` | = `backgroundColor` | Name/Abteilung + Ecken | | `styles.groupBackgroundColor` | = Header | Monats-/KW-Zeile | | `styles.rowBackgroundColor` | = `backgroundColor` | Tagesraster-Zeilen | | `styles.fontColor` | — | Text links und Fallback für Header | | `styles.mutedColor` / `subtleColor` | = `fontColor` | Sekundärtext / Untertitel | | `styles.borderColor` | `#e4e4e7` | Farbe für alle angeschalteten Linien | | `styles.borderRadius` | `12px` | Eckenradius (nur wenn `outer` an) | | `styles.borders.outer` | `true` | Außenrahmen. `false` = bündig im Layout | | `styles.borders.columns` | `false` | Vertikale Linien im Tagesgitter und zwischen Label-Spalten. Die Kante Labels → Raster bleibt an. | | `styles.borders.rows` | `true` | Horizontale Ressourcenlinien | | `styles.dayStripes` | `true` | Tagesspalten abwechselnd Weiß / sehr helles Grau. `false` = aus. `{ odd, even }` = eigene Farben. | ```javascript styles: { backgroundColor: "#ffffff", borderRadius: "12px", borderColor: "#e4e4e7", borders: { outer: true, columns: false, rows: true } } // In einem Layout / Card: styles: { borders: { outer: false } } // Tagesgitter wieder an: styles: { borders: { columns: true } } // Tagesspalten-Zebra aus: styles: { dayStripes: false } // Dunkler Hintergrund: styles: { backgroundColor: "#18181b", borderColor: "#3f3f46", borders: { outer: true, rows: { color: "#27272a" } } } ``` ## dateSettings – Zeitraum & Darstellung ### startDate Startdatum der Timeline. Ab diesem Tag werden die Spalten angezeigt. ```javascript startDate: today(), startDate: date(2025, 2, 1), ``` ### duration Anzahl der angezeigten Tage. Wird ignoriert, wenn `endDate` gesetzt ist. ```javascript duration: 30, duration: 14, ``` ### endDate *(Optional)* Enddatum der Timeline. Hat Vorrang vor `duration`. ```javascript dateSettings: { startDate: date(2025, 2, 1), endDate: date(2025, 2, 28) } ``` ### highlights Farbliche Hervorhebung bestimmter Tage – z. B. Wochenenden oder Feiertage. ```javascript highlights: [ { weekday: 6, color: "rgba(59,130,246,0.06)" }, { weekday: 0, color: "rgba(59,130,246,0.06)" }, { date: date(2025, 12, 24), color: "rgba(239,68,68,0.1)" } ], ``` - `weekday`: 0 = Sonntag, 6 = Samstag - `date`: Konkretes Datum - `dateFrom` / `dateTo`: Datumsbereich (optional statt einzelnem `date`) - `color`: CSS-Farbe (HEX oder rgba) - **`applyToRows`** *(optional)*: `true` – dieselbe Hervorhebung wird **in der Zeilenfläche** (hinter den Termin-Balken) für **alle** Ressourcen-Zeilen gezeichnet, nicht nur in der Kopfzeile. Ohne dieses Flag bleibt das Verhalten wie bisher (nur Header). **Im Uhrzeitmodus** zusätzlich: - `hour`: Stunde (0–23) für Hervorhebung bestimmter Stunden-Spalten - `timeFrom` / `timeTo`: Zeitbereich in Millisekunden (wie bei `timeEntries`), z. B. Mittagspause ```javascript highlights: [ { hour: 12, color: "rgba(239,68,68,0.1)" } ] ``` ```javascript highlights: [ { weekday: 6, color: "rgba(59,130,246,0.06)", applyToRows: true }, { date: date(2025, 12, 24), color: "rgba(239,68,68,0.1)", applyToRows: true }, { timeFrom: time(12, 0), timeTo: time(13, 0), color: "rgba(156,163,175,0.15)", applyToRows: true } ], ``` ### highlights pro Zeile (`items[].highlights`) Neben den globalen `dateSettings.highlights` kann jede Ressourcen-Zeile ein eigenes Array **`highlights`** haben – z. B. Urlaub oder Krankenstand nur für einen Mitarbeiter. Die gleichen Felder wie oben gelten; **`applyToRows` ist hier nicht nötig** (gilt immer nur für diese Zeile). ```javascript items: (select Mitarbeiter).[{ id: Nr, title: Name, highlights: [ { dateFrom: Urlaub_von, dateTo: Urlaub_bis, color: "rgba(34,197,94,0.15)" }, { date: Krank_Tag, color: "rgba(239,68,68,0.12)" } ], timeEntries: [...] }] ``` **Uhrzeitmodus:** z. B. Sperr- oder Wartungsfenster mit `timeFrom` / `timeTo`. ### header Der `header`-Block steuert alle Kopfzeilen der Timeline. Er besteht aus drei optionalen Unterblöcken: `months`, `weeks` und `days`. Alle drei haben dieselbe Struktur und können unabhängig voneinander aktiviert werden. Die Reihenfolge im Widget ist immer: **Monate → Kalenderwochen → Tage** (von oben nach unten). ```javascript header: { months: { visible: true, label: "##monthLong## ##year##", fontSize: "11px", fontColor: "#555", fontWeight: "600", backgroundColor: "#ececec", highlights: [ { month: 5, color: "rgba(59,130,246,0.12)" } ] }, weeks: { visible: true, label: "KW ##week##", fontSize: "11px", fontColor: "#555", backgroundColor: "#f2f2f2", highlights: [ { week: 18, color: "rgba(239,68,68,0.1)" } ] }, days: { visible: true, label: "##weekDayShort## ##day##.", fontSize: "12px", fontColor: "#333", fontWeight: "600", backgroundColor: "#f9f9f9" } } ``` `header.title` sitzt **oben links** über den Spaltennamen (Name, Abteilung), in einer Fläche so hoch wie Monat- + KW-Zeile. Spaltennamen stehen in **derselben Zeile wie die Tage**, mit Linie nach oben. Reiner Text ohne `fontSize` / `fontWeight` / Custom-Layout: **16px, Schriftstärke 400**. ```javascript header: { title: { value: "Teamplanung", alignX: "left" } } ``` | Property | Beschreibung | |---|---| | `value` | Text oder HTML. Alternativ ein Widget-Deskriptor `{ widget, uid, data }`. | | `customLayout` | HTML oder Widget-Deskriptor (wie in Label-Zellen). Hat Vorrang vor `value`. | | `alignX` | `"left"` \| `"center"` \| `"right"` | | `fontSize` / `fontColor` / `fontWeight` / `backgroundColor` | Typo und Fläche des Slots | Ohne Monat/KW gibt es den Slot nicht (keine extra Zeile). **Legacy:** Nur `header.title` mit `label` (`##day##` …) und ohne `value`/`customLayout` steuert weiter die **Tageszeile**, wenn `header.days` fehlt. Alle drei Zeilen-Blöcke (`months` / `weeks` / `days`) sind optional. Wird ein Block weggelassen, ist er nicht aktiv. **Gemeinsame Properties (je Unterblock):** | Property | Beschreibung | |---|---| | `visible` | `false` blendet die Zeile aus (Standard: `true`) | | `label` | Anzeigetext mit Platzhaltern (s. u.) | | `alignX` | `"left"` \| `"center"` \| `"right"` – horizontale Ausrichtung in der Zelle bzw. Gruppe. Standard: **`"left"`** (Monat/KW-Gruppen und Tage). | | `fontSize` | Schriftgröße, z. B. `"12px"` | | `fontColor` | Schriftfarbe | | `fontWeight` | Schriftstärke, z. B. `"600"` | | `backgroundColor` | Hintergrund der gesamten Zeile bzw. Monats-/KW-Gruppe. **`days`**: färbt u. a. die Tages-Headerzeile; bei **`days.sticky: "left"`** hat der **Beschriftungs-`span` kein eigenes** undurchsichtiges `backgroundColor`, damit **`dateSettings.highlights`** (Farb-Overlay pro Tages-**`div`**) hinter dem Text sichtbar bleibt. | | `highlights` | Hervorhebungen pro Monats- bzw. KW-Gruppe (s. u.) – **nur** für `months` / `weeks` | | `sticky` | Optional für **`months`**, **`weeks`** und **`days`**: `sticky: "left"` hält die Bezeichnung bei horizontalem Scrollen sichtbar (CSS `position: sticky`); der horizontale Abstand setzt an den linken sichtbaren Rand des Zeitbereichs, berechnet als **Summe aller linken `columns`-Breiten** + `stickyPadding`. Ohne `sticky` steuert nur `alignX` die Ausrichtung. | | `stickyPadding` | Zusätzlicher Abstand in px (Zahl) oder z. B. `"12px"`, addiert auf die Labelspalten-Breite für `sticky: "left"`. Wenn weggelassen, wird **+8** px addiert. | | `hideBelow` | Optional. **`days`**: Spaltenbreite in px — darunter verschwindet der Tages-Text; die Zeile bleibt als Trenner. **Default:** pro Tag eine vertikale Linie (`data-arc-day-ticks`). Mit **`compactRanges: true`** stattdessen KW- bzw. Monats-Ranges (`01.09. - 07.09.`). Default-Schwelle: 36. **`weeks`/`months`**: minimale **Gruppenbreite** in px, unter der der Label-Text ausgeblendet wird — die Zeile bleibt. Defaults: `weeks` 44, `months` 72. | | `compactRanges` | Optional, nur **`days`**. `true` = beim Rauszoomen Tage zu KW-/Monats-Ranges zusammenfassen. Default: aus (Tick-Linien). | | `monthCompactBelow` | Optional, nur **`days`**, nur mit `compactRanges`. Spaltenbreite in px — darunter wechselt die Range von KW auf Monat. Default: 14. | | `labelBreakpoints` | Optional, nur **`days`**: Array `{ below, label }` — bei mittlerer Spaltenbreite kürzeres Tages-Label (bevor der Text ganz verschwindet). | **Hinweis:** `dateSettings.highlights` färben pro Tag die **Tages-Headerzelle** (`div`, optional mit `applyToRows` auch die Zeilenfläche). In Kombination mit `header.days.sticky: "left"` bitte `backgroundColor`-Hinweis in der Tabelle beachten. **Platzhalter für `days.label`:** - `##weekDayShort##` – Kurzer Wochentag (Mo, Di, …) - `##weekDayLong##` – Langer Wochentag (Montag, Dienstag, …) - `##day##` – Tag (1–31) - `##monthShort##` – Kurzer Monat (Jan, Feb, …) - `##monthLong##` – Langer Monat (Januar, Februar, …) - `##year##` – Jahr **Platzhalter für `weeks.label`:** - `##week##` – Kalenderwochennummer (01–53, zweistellig mit führender Null) - `##year##` – Jahr **Platzhalter für `months.label`:** - `##monthLong##` – Langer Monatsname (Januar, Februar, …) - `##monthShort##` – Kurzer Monatsname (Jan, Feb, …) - `##year##` – Jahr **`highlights` pro Unterblock:** ```javascript months: { highlights: [ { month: 5, color: "rgba(59,130,246,0.12)" } // Mai hervorheben (1 = Januar) ] }, weeks: { highlights: [ { week: 18, color: "rgba(239,68,68,0.1)" } // KW 18 hervorheben ] } ``` - `months.highlights`: `{ month: N }` – N = 1 (Januar) bis 12 (Dezember) - `weeks.highlights`: `{ week: N }` – N = ISO-Kalenderwochennummer ## Layout-Parameter ### dayWidth Breite einer Tages-Spalte (Startwert). Wenn Zoom aktiv ist und der Nutzer noch keine Zoomstufe gespeichert hat, gilt `zoomSettings.defaultDayWidth` — sonst dieser Wert. ```javascript dayWidth: "120px", dayWidth: "90px", ``` ### zoomSettings Steuert interaktiven Zoom und die initiale Spaltenbreite. Die gewählte Zoomstufe wird pro `uniqueId` im LocalStorage gespeichert (zusammen mit Scroll-Position), sofern `persist` nicht `false` ist. ```javascript zoomSettings: { enabled: true, defaultDayWidth: 48, minDayWidth: 16, maxDayWidth: 200, step: 8, persist: true, controls: { enabled: true, position: "bottomRight" } } ``` - **enabled**: `false` deaktiviert Zoom komplett — nur statisches `dayWidth`, keine Controls. - **defaultDayWidth**: Initiale Breite in px, wenn kein gespeicherter Wert existiert. - **minDayWidth** / **maxDayWidth**: Unter- und Obergrenze der Spaltenbreite. - **step**: px-Schritt pro Klick auf +/−. - **persist**: Zoomstufe in LocalStorage merken (Standard: `true`). - **controls**: Floating-Buttons unten rechts im Widget. Mausrad/Pinch-Zoom ist in v1 nicht enthalten (geplant). ### rowHeight Höhe einer Ressourcen-Zeile. `"auto"` passt sich dem Inhalt an. ```javascript rowHeight: "auto", rowHeight: "36px", ``` ### labelWidth Standard-Breite für **eine** Label-Spalte, wenn `columns` fehlt oder wenn in `columns[i]` kein `width` gesetzt ist. Standard: `"120px"`. ```javascript labelWidth: "150px", ``` ### currentTime Farbe des Balkens, der die aktuelle Uhrzeit markiert. Leer oder `false` blendet ihn aus. ```javascript currentTime: "rgba(59,130,246,0.5)", currentTime: "", ``` ## items – Ressourcen und Termine `items` ist ein Array von Ressourcen-Zeilen. Jede Zeile hat `timeEntries` (Balken). Optional: `items` (Kinder, eine Ebene) und `columns` (Zelltexte pro Label-Spalte). ### Struktur einer Zeile ```javascript { id: Nr, title: Name, subtitle: Abteilung, fontSize: "13px", fontWeight: "normal", columns: [ { title: text(Name) }, { title: text(Abteilung) } ], styles: {}, highlights: [], timeEntries: [...], items: [...] } ``` - **id** *(Pflicht)*: Eindeutige ID (z. B. Record-Nr). Wird für Drag & Drop (`resourceField`) und **collapsible** benötigt. - **title** / **subtitle**: Fallback für die **erste** Label-Spalte, wenn `columns` fehlt oder die erste Zelle leer ist. - **fontSize** / **fontWeight** *(optional, Item-Ebene)*: Standard-Typo für alle Label-Zellen dieser Zeile, sofern weder Top-Level-Spalte noch `columns[i]` etwas setzen. Reihenfolge der Priorität (höher zuerst): **`columns[i]`** → **Item** `fontSize`/`fontWeight` → **`styles.fontSize`/`styles.fontWeight`**. - **columns** *(pro Zeile)*: Ein Objekt pro Label-Spalte (gleiche Anzahl wie Top-Level `columns`): `title`, `subtitle`, optional **`customLayout`** (HTML für diese Zelle), `actions`, optional `alignX`, `alignY`, `fontSize`, `fontWeight`. - **styles**: Optional (z. B. Zeilen-Hintergrund; `fontSize`/`fontWeight` hier wirken als letzte Typo-Fallbacks). - **highlights** *(optional)*: Zellen-Hintergrund für diese Zeile (Abwesenheiten, Sperrzeiten); siehe Abschnitt **highlights pro Zeile**. - **timeEntries**: Termine/Balken in dieser Zeile. **Horizontaler Abstand:** fest **4px** zur Spaltenkante (wie das Zeilen-Padding oben/unten); bei **mehrspaltigen** Balken nur am **äußeren** Anfang und Ende, nicht zwischen den Tagen (nicht über `timeEntries[].styles` änderbar). - **items**: Optional. Unterzeilen (eine Ebene). ### Struktur eines timeEntry (Termin/Balken) ```javascript { title: Bezeichnung, subtitle: Kunde, dateFrom: Datum_von, dateTo: Datum_bis, timeFrom: Zeit_von, timeTo: Zeit_bis, styles: {}, dragAction: {}, actions: [], tooltip: {}, customLayout: "", value: "" } ``` - **title**: Text im Balken, oder Objekt `{ value: …, width: … }` (siehe unten). Bei Zeitangaben kann `##entrytime##` verwendet werden. - **subtitle**: Optional. Zweiter Textblock im Balken (String oder Objekt wie bei **title**). - **direction** *(optional)*: `"vertical"` oder `"horizontal"` – Titel und Subtitle **untereinander** oder **nebeneinander**. - **Default bei Plain-Strings** (ohne Objekt-Form): `"vertical"` — Subtitle bleibt sichtbar, auch wenn der Titel lang ist. - **Default bei Objekt-Form** (`{ value, width }`): `"horizontal"` — sinnvoll für Breite-Paarung (z. B. Titel + Uhrzeit nebeneinander). - Explizit setzen überschreibt den Default immer. - **dateFrom**, **dateTo**: Start- und Enddatum (Pflicht). - **timeFrom**, **timeTo**: Optional. Uhrzeit in Millisekunden (0 = Mitternacht). Ohne Angabe wird der Eintrag als ganztägig dargestellt. - **styles**: CSS-ähnliche Eigenschaften (backgroundColor, color, borderRadius, padding). - **dragAction**: Konfiguration für Drag & Drop. - **actions**: Array von Aktionen beim Klick auf den Balken (wie bei anderen Widgets). Das frühere einzelne Feld **`clickAction`** wird weiterhin unterstützt und wie ein Eintrag in `actions` ausgeführt. - **tooltip**: Optional. Hover-Overlay mit zusätzlichen Infos (siehe unten). - **customLayout**: Optional. Eigenes Layout statt title/subtitle/value. - **value**: Optional. Zusätzlicher HTML-Inhalt im Balken (nach dem Titel/Subtitle-Block). **Objekt-Form für `title` / `subtitle`:** `{ value: TextOderHtml, width: "fraction" | "auto" | CSS-Länge }` - **`value`**: Anzuzeigender Text oder HTML (wie bisher); `##entrytime##` möglich. - **`width`** *(optional, Default `"fraction"`)**: - **`"fraction"`** – nutzt den **verbleibenden** Platz in der Zeile; langer Text wird mit Ellipsis gekürzt. - **`"auto"`** – **Inhaltsbreite**; schrumpft nicht zugunsten des Partners (z. B. für Uhrzeiten, die voll sichtbar bleiben sollen). Sehr langer Text kann am Balkenrand trotzdem mit Ellipsis enden. - **CSS-Länge** (z. B. `"72px"`, `"4.5rem"`) – feste Basisbreite (`flex-basis`) für diesen Block. Sind **title** und **subtitle** nur **Strings** (ohne Objekt) und ist **direction** nicht gesetzt, stehen sie **untereinander**. Nutzt mindestens ein Feld die Objekt-Form (ohne `direction`), stehen sie **nebeneinander** (Flex + `width`). Mit **`direction: "horizontal"`** bzw. **`"vertical"`** erzwingst du die Ausrichtung. ```javascript title: { value: text(Bezeichnung), width: "fraction" }, subtitle: { value: "##entrytime##", width: "auto" }, ``` ### tooltip – Hover-Info am Balken Optionaler Hover-Tooltip pro `timeEntry`. Der Overlay hängt an `document.body` (Pfeil + Schatten), wird nicht vom Balken abgeschnitten und bleibt beim Hovern stabil offen — auch wenn Ninox im Hintergrund neu rendert. Mit der Maus in den Tooltip fahren ist möglich (interaktive Widgets/Buttons). ```javascript tooltip: { value: "Kunde: Meier GmbH" + newline() + "Status: geplant", width: "280px", maxWidth: "360px", maxHeight: "300px", position: "auto", styles: { backgroundColor: "#1f2937", color: "#fff" }, trigger: "hover" } ``` - **value** *(Pflicht)*: Inhalt — **Text/HTML-String** **oder** Next-Gen-Deskriptor `{ widget: "arc-widget-layout", uid: "tt-" + Nr, data: { … } }`. Volle Registry-ID (`arc-widget-…`), Config unter **`data`**, eigene `uid`. Nested Widgets inkl. Actions funktionieren. - **width** / **maxWidth** / **maxHeight**: Optional. CSS-Längen; bei `maxHeight` scrollt der Inhalt intern. - **position**: `"top"` | `"bottom"` | `"left"` | `"right"` | `"auto"` (Default `"auto"` — bevorzugt oben, kippt bei Platzmangel). - **styles**: Optional. u. a. `backgroundColor`, `color`, `borderRadius`, `padding`, `boxShadow`. Für eine durchgehende Kontur um Box **und** Pfeil: `borderWidth` + `borderColor` (SVG-`stroke` auf der Bubble-Shape). - **trigger**: v1 nur `"hover"` (reserviert für spätere `"click"`). Technisch: der Tooltip ist eine **SVG-Bubble** (ein Path für abgerundetes Rechteck + integrierten Pfeil), portaliert an `document.body`. Fill und Stroke gelten für die ganze Form — kein separates CSS-Dreieck. Klick auf den Balken führt weiterhin `actions` aus. Ohne `tooltip` ändert sich nichts. #### Beispiele **1) Dark + Border (Text)** ```javascript tooltip: { value: "Kunde: Meier GmbH" + newline() + "Status: geplant" + newline() + "Notiz: Parkplatz hinter dem Gebäude", width: "280px", position: "auto", styles: { backgroundColor: "#1f2937", color: "#fff", borderWidth: "1px", borderColor: "rgba(255,255,255,0.28)" } } ``` **2) Light Soft (hell + Border)** ```javascript tooltip: { value: html("Wartung Anlage Nord
Dauer: 3 Tage
Ersatzteil: Filterkit #442"), maxWidth: "300px", position: "bottom", styles: { backgroundColor: "#ffffff", color: "#1f2937", borderWidth: "1px", borderColor: "rgba(15,23,42,0.14)", borderRadius: "10px", boxShadow: "0 10px 28px rgba(15, 23, 42, 0.12)" } } ``` **3) Light + Widget (vertikal, Button volle Breite)** ```javascript tooltip: { value: { widget: "arc-widget-layout", uid: "tt-" + Nr, data: { direction: "vertical", gap: "8px", width: "100%", blocks: [ { width: "100%", value: html("
Termin-Details
") }, { width: "100%", value: html("
Kunde: Schmidt AG
Thema: Angebot Q3
") }, { width: "100%", value: { widget: "arc-widget-button", uid: "tt-btn-" + Nr, data: { title: "Öffnen", width: "100%", height: "30px", fontSize: "12px", backgroundColor: "#3b82f6", fontColor: "#fff", borderRadius: "6px", actions: [{ type: "popup", recordId: Nr }] } } } ] } }, width: "240px", position: "top", styles: { backgroundColor: "#ffffff", color: "#0f172a", borderWidth: "1px", borderColor: "rgba(15,23,42,0.16)", borderRadius: "12px", padding: "12px" } } ``` **4) Position links / rechts** ```javascript tooltip: { value: "Zusatzinfo neben dem Balken", width: "240px", position: "right", // oder "left" styles: { backgroundColor: "#ffffff", color: "#0f172a", borderWidth: "1px", borderColor: "rgba(15,23,42,0.16)" } } ``` Tipp: Am Balken oft `direction: "vertical"` setzen, damit Titel und Subtitle untereinander stehen und der Subtitle sichtbar bleibt. ### styles – Styling der Termin-Balken ```javascript styles: { backgroundColor: "#3b82f6", color: "#fff", borderRadius: "6px", padding: "4px 8px" }, ``` ### dragAction – Verschieben von Terminen Definiert, welche Felder beim Ziehen aktualisiert werden. ```javascript dragAction: { recordId: Nr, dateFrom: fieldId(Nr, "Datum_von"), dateTo: fieldId(Nr, "Datum_bis"), resourceField: fieldId(Nr, "Bearbeiter"), enabled: true, changeFieldValues: [{ fieldId: fieldId(Nr, "CacheTrigger"), value: now() }] }, ``` - **recordId**: Record-ID des Termins (meist `Nr`). - **dateFrom**, **dateTo**: Feld-IDs für Start- und Enddatum. Werden beim horizontalen Verschieben aktualisiert. - **resourceField**: Optional. Feld-ID für die Ressource (z. B. Bearbeiter). Wird beim vertikalen Verschieben (Zeilenwechsel) mit der `id` der Ziel-Zeile aktualisiert – **nur wenn** Start- und Zielzeile dieselbe **Tiefe** in der `items`-Hierarchie haben (`_depth` gleich). Erforderlich z. B., damit kein Termin von einer Team-Zeile auf eine Mitarbeiter-Zeile (oder umgekehrt) gezogen wird; **Mitarbeiter unter Team A → Mitarbeiter unter Team B** ist erlaubt. - **enabled**: Optional. `false`: kein Drag für diesen Balken (z. B. Parent-Summen). Wenn weggelassen, ist Drag aktiv, sobald `dragAction` gesetzt ist. - **changeFieldValues**: Optional. Array von `{ fieldId, value }` – wird **nach** einem erfolgreichen Drag geschrieben (Datum/Zeit und/oder Ressource haben sich geändert). Typischer Einsatz: Cache- oder Trigger-Felder aktualisieren. Aufbau wie bei Create-Actions (`ghost.actions`). ⚠️ **Hinweis:** Ohne `dragAction` ist Drag & Drop deaktiviert. Vertikales Ziehen zwischen **verschiedenen Tiefen** (z. B. Team-Zeile ↔ Mitarbeiter-Zeile) ist nicht möglich; innerhalb derselben Tiefe (z. B. nur Mitarbeiter-Zeilen) auch **über verschiedene Parents** (Teams). ### actions – Klick auf Termin ```javascript actions: [{ type: "popup", recordId: Nr }], ``` - **type**: `"popup"` öffnet den Datensatz in einem Popup. - **recordId**: Record-ID des angeklickten Termins. Mehrere Einträge im Array werden nacheinander ausgeführt. **Legacy:** Ein einzelnes **`clickAction`**-Objekt (ohne Array) funktioniert weiterhin. ### actions – Klick auf Label-Zelle In **`items[].columns[i]`** kann optional **`actions`** gesetzt werden. Ist das Array nicht leer, löst ein Klick auf die Zelle dieselben Ninox-Aktionen aus. **Hinweis:** In der **ersten** Label-Spalte hat bei **eingeklappten Gruppen** (`collapsible`) der Klick Vorrang zum Auf-/Zuklappen; dort werden keine zusätzlichen `actions` auf der Zelle ausgeführt. ```javascript items: (select Fahrzeuge).[{ id: Nr, columns: [ { title: text(Bezeichnung), subtitle: "Fahrzeug" }, { title: "⌂", actions: [{ type: "popup", recordId: Nr }] } ], timeEntries: [...] }] ``` `columns` gehört zur jeweiligen Zeile unter **`items`**. Ein Klick auf die zweite Spalte (hier **⌂**) öffnet den Datensatz der Zeile (`Nr`). Beliebig viele Einträge im `actions`-Array werden nacheinander ausgeführt. ### ghost – Zeilen-Balken für Neueinträge (Wrapper) Auf **Ebene jedes Eintrags in `items[]`** (nicht in `columns[]`) kann optional ein Objekt **`ghost`** gesetzt werden – **unabhängig pro Zeile** (Ressource). Eine Zeile ohne `ghost` hat keinen Anlege-Streifen. **Felder:** - **`title`** – optional, wird als **Browser-Tooltip** (`title`-Attribut) auf dem Plus-Button genutzt (z. B. „Termin anlegen“). - **`position`** – optional, steuert Anzeige und Layout des Ghost-**Buttons** pro Spalte: - **`bottomRight`** (Default, kann weggelassen werden) – kleiner quadratischer Button (**28×28 px**), **absolut** in der jeweiligen Spalte unten rechts; nimmt **keine** volle Spaltenbreite ein. Der Button sitzt **außerhalb des CSS-Grids** der Zeile (absolut zur Zeile), damit **timeEntries** in derselben Grid-Zeile bleiben wie ohne Ghost. **Stacking:** der Button liegt knapp **über** normalen Balken (`z-index` **11**), **unter** Balken-Hover (**10**) und **aktivem/Drag**-Balken (**100**). - **`bottomLeft`**, **`topRight`**, **`topLeft`** – gleicher Button, andere Ecke der Spalte. - **`timeEntry`** – **klassisches Verhalten**: volle Spaltenbreite in der **ersten Grid-Zeile** (mit **4px** horizontalem Abstand wie die Termin-Balken), schiebt sichtbare Termin-Balken in die nächste Zeile (`align-content: start` auf der Zeile). - **`actions`** – Pflicht, mindestens eine Ninox-Aktion; typischerweise **`type: "create"`** mit vorbefüllten Feldern. Ein Klick auf den **Plus-Button** führt diese Aktionen mit dem **Wert dieser Spalte** aus (siehe Platzhalter). - **`styles`** – optional, Objekt mit **CSS-Eigenschaften in camelCase** (wie bei **`timeEntries[].styles`**), z. B. `backgroundColor`, **`fontColor`** (Textfarbe inkl. Plus-Icon über `currentColor`), `fontSize`, `fontWeight`, `borderRadius`, **`paddingX`** / **`paddingY`** (werden zu `padding` zusammengesetzt) oder **`padding`** (Shorthand), `border`, `boxShadow`. Legacy-**`color`** wird wie **`fontColor`** auf die Schriftfarbe gemappt; ist **`fontColor`** gesetzt, hat es Vorrang. Werden auf den Button gelegt; **`gridColumn` / `gridRow`** setzt das Widget für die Rasterplatzierung (überschreibt ggf. eigene Werte in `styles`). **Darstellung (Default `bottomRight` u. a.):** Pro Spalte ein **absolut positionierter, klick-durchlässiger Bereich** über der Zeile (kein Grid-Item – verhindert Verschiebung der Balken). Der **Plus-Button** ist **weiß**, mit **leichtem Schatten**, Ecke per `position`. **Spalten-Hover (Maus in der Spalte):** Button wird sichtbar mit **halbtransparentem weißem Hintergrund** und **`backdrop-filter`-Weichzeichnung** (Frosted-Glass). **Direkt über dem Button:** undurchsichtiges **Weiß**, kein Blur – volle Lesbarkeit. Über **`styles`** kannst du z. B. `backgroundColor` setzen (überschreibt die Standardfarben). **Darstellung (`timeEntry`):** Wie ein schmaler **Balken** über die Spaltenbreite in Zeile 1 (mit **4px** Rand links/rechts wie die Termin-Balken); **timeEntries** der Zeile beginnen darunter. **Sichtbarkeit / Hover:** - **Alle Positionen außer `timeEntry`:** Der Plus-Button ist **standardmäßig unsichtbar** und erscheint erst, wenn die **Maus irgendwo in dieser Tag-/Zeitspalte** ist (auch über einem Balken); die Spalte wird anhand der Mausposition auf der **Zeile** ermittelt. Auf **Touch-Geräten** (kein präzises Hover) bleiben die Ghost-Buttons **sichtbar**. - **`timeEntry`:** Für jede Spalte wird getrennt geprüft, ob ein **sichtbarer** Termin-Balken in die Spalte reicht. **Ohne** Balken: bei **`@media (hover: hover)`** ist der Ghost-Balken zunächst per Opacity verborgen, bis die Maus über die **Ghost-Zelle** fährt. **Mit** Balken in der Spalte: der Ghost-Balken bleibt sichtbar. Auf **Touch** bleiben Ghost-Elemente sichtbar. **Platzhalter** (in beliebigen String-Werten innerhalb der Action, rekursiv): - **`##columnValue##`** – Millisekundenwert der **Spalte unter dem Klick**: im **Tag-Modus** Tagesbeginn (`startDate` + Spaltenindex), im **Uhrzeit-Modus** Beginn des Zeitslots (`startTime` + Index × `amount`). - **`##clickedDate##`** – Alias mit gleicher Semantik (Kompatibilität zu Calendar Grid / Week). ```javascript items: (select Mitarbeiter).[{ id: Nr, ghost: { position: "bottomRight", title: "Termin anlegen", styles: { backgroundColor: "rgba(0, 0, 0, 0.08)", fontColor: "#333333", borderRadius: "6px", paddingY: "4px", paddingX: "8px" }, actions: [{ type: "create", tableId: "B", popup: true, changeFieldValues: [ { fieldId: fieldId(Nr, "Datum"), value: "##columnValue##" }, { fieldId: fieldId(Nr, "Bearbeiter"), value: number(Nr) } ] }] }, timeEntries: [...] }] ``` Klicks auf **Termin-Balken** (timeEntries) lösen weiterhin nur die **`actions`** des jeweiligen Balkens aus, nicht `ghost.actions`. **Beispiel – Uhrzeitmodus:** `##columnValue##` ist der **Beginn des geklickten Zeitslots** in Millisekunden (wie bei `timeFrom` in den Balken). ```javascript arcCustomCalendarTimeline({ uniqueId: "timeline-fleet-neu", mode: "time", timeSettings: { startTime: 6, duration: 12, amount: 0.25 }, columnWidth: "60px", rowHeight: "36px", height: "700px", dateSettings: { header: { title: { label: "##hour##:##minute##", fontSize: "11px", fontColor: "#333" } } }, columns: [ { title: "Fahrzeug", width: "160px" }, { title: "Info", width: "80px" } ], items: (select Fahrzeuge).[{ id: Nr, ghost: { title: "Neue Fahrt", actions: [{ type: "create", tableId: "B", popup: true, changeFieldValues: [ { fieldId: fieldId(Nr, "Startzeit"), value: "##columnValue##" }, { fieldId: fieldId(Nr, "Fahrzeug"), value: number(Nr) } ] }] }, timeEntries: [...] }] }) ``` Alternativ kannst du **`##clickedDate##`** statt `##columnValue##` verwenden – beide werden gleich aufgelöst. ## weekStart Starttag der Woche (0 = Sonntag, 1 = Montag, …). Beeinflusst die Sortierung der Tage. ```javascript weekStart: 1, ``` ## timeSettings – Uhrzeitmodus (Time Mode) Wenn du statt Tagen **Uhrzeiten** als Spalten anzeigen möchtest, kannst du den Uhrzeitmodus aktivieren. Das ist ideal für **Flottendiagramme**, **Schichtpläne** oder **Tageseinsatzplanung**, bei denen die X-Achse einen einzelnen Tag darstellt. Der Uhrzeitmodus wird automatisch aktiviert, sobald `timeSettings` gesetzt ist – oder explizit mit `mode: "time"`. ```javascript arcCustomCalendarTimeline({ uniqueId: "fleet-timeline", mode: "time", timeSettings: { startTime: 6, duration: 12, amount: 0.25 }, columnWidth: "60px", rowHeight: "36px", height: "700px", dateSettings: { header: { title: { label: "##hour##:##minute##", fontSize: "11px", fontColor: "#333" } } }, columns: [ { title: "Fahrzeug / Tag", width: "160px" }, { title: "Info", width: "80px" } ], items: (select Fahrzeuge).[{ id: Nr, title: Bezeichnung, fontSize: "12px", fontWeight: "600", columns: [ { title: text(Bezeichnung), subtitle: "Fahrzeug" }, { title: "⌂" } ], items: (select Einsatztage where Fahrzeug = Nr).[{ id: Nr, title: Wochentag, fontSize: "11px", columns: [ { title: text(Wochentag) }, { title: "" } ], timeEntries: (select Fahrten where Einsatztag = Nr).[{ title: Route, timeFrom: Startzeit, timeTo: Endzeit, styles: { backgroundColor: text(Farbe), color: "#fff" } }] }] }] }) ``` ### startTime Ab welcher Stunde die Zeitleiste beginnt (z. B. `6` = 06:00 Uhr). ```javascript startTime: 6, startTime: 8, ``` ### duration Wie viele Stunden angezeigt werden (z. B. `12` = 12 Stunden ab `startTime`). ```javascript duration: 12, duration: 8, ``` ### amount Spaltenintervall in Stunden. Bestimmt die Feinheit der Zeiteinteilung. ```javascript amount: 1, amount: 0.5, amount: 0.25, ``` - `1` = Stundenspalten (08:00, 09:00, …) - `0.5` = 30-Minuten-Spalten (08:00, 08:30, 09:00, …) - `0.25` = 15-Minuten-Spalten (08:00, 08:15, 08:30, …) ### Header-Platzhalter im Uhrzeitmodus Im Uhrzeitmodus stehen zusätzliche Platzhalter für `dateSettings.header.days.label` zur Verfügung: - `##hour##` – Stunde mit führender Null (06, 07, …) - `##minute##` – Minute mit führender Null (00, 15, 30, …) - `##time##` – Vollständige Uhrzeit (06:00, 08:15, …) ### columnWidth Breite einer Zeitspalte (Alias für `dayWidth` im Uhrzeitmodus). ```javascript columnWidth: "60px", columnWidth: "80px", ``` ### timeEntries im Uhrzeitmodus Im Uhrzeitmodus verwenden `timeEntries` statt `dateFrom`/`dateTo` die Felder `timeFrom`/`timeTo` (in Millisekunden ab Mitternacht): ```javascript timeEntries: [{ title: "RVS GZ-NU-KE", timeFrom: 27000000, timeTo: 41400000, styles: { backgroundColor: "#0066cc" } }] ``` 💡 Umrechnung: `Stunden × 3600000` (z. B. 07:30 = 7.5 × 3600000 = 27000000) ### Drag & Drop im Uhrzeitmodus Im Uhrzeitmodus werden beim Drag & Drop `timeFrom`/`timeTo` statt `dateFrom`/`dateTo` aktualisiert: ```javascript dragAction: { recordId: Nr, timeFrom: fieldId(Nr, "Startzeit"), timeTo: fieldId(Nr, "Endzeit"), resourceField: fieldId(Nr, "Einsatztag") } ``` ## columns (Top-Level) Definiert die **linken Label-Spalten** (neben dem Zeitbereich). Pro Eintrag im Array: `title` (erscheint in der Zeile mit den Tageslabels), optional `subtitle`, optional `width` (CSS-Breite, z. B. `"200px"`). Fehlt `width`, gilt `labelWidth` (Standard `120px`). Optional: **`alignX`**: `"left"` \| `"center"` \| `"right"` (Standard `left`), **`alignY`**: `"top"` \| `"center"` \| `"bottom"` (Standard `center`) – Ausrichtung von Header und Zellinhalt in dieser Spalte. **`fontSize`** und **`fontWeight`** (CSS-Werte, z. B. `"13px"`, `"600"` oder `"bold"`) gelten für Header und Zellen dieser Spalte, sofern weder **`items[].columns[i]`** noch **Item-**`fontSize`/`fontWeight` gesetzt sind. Pro Zelle kann in `items[].columns[i]` jeweils `alignX`, `alignY`, `fontSize`, `fontWeight` überschrieben werden. **Kopfzeile der Label-Spalten** (Name, Abteilung): Standardfarbe kommt von **`dateSettings.header.days`** (`backgroundColor`, `fontColor`). Pro Spalte überschreibbar mit `columns[n].backgroundColor` und `columns[n].fontColor`. Ohne `columns` oder leeres Array: **eine** Spalte mit `labelWidth`. ## items – Datenzeilen und Verschachtelung Alle Ressourcen werden einheitlich **`items`** genannt. Flache Liste: `items: [{ id, title, subtitle, timeEntries, … }]`. **Verschachtelung (eine Ebene):** Ein Item kann `items: [...]` enthalten. Parent und Kinder werden als **eigene Zeilen** untereinander gerendert; die erste Label-Spalte ist bei Kindern **eingerückt**. Tiefer als eine Ebene ist in der Timeline aktuell nicht vorgesehen (Datenstruktur kann später erweitert werden). **`items[].columns`** (pro Zeile): Array mit **einem Eintrag pro Label-Spalte** (gleiche Länge wie Top-Level `columns`). Felder pro Zelle: `title`, `subtitle`, optional `customLayout`, optional `actions`, optional `alignX` / `alignY`, optional `fontSize` / `fontWeight` (höchste Priorität für Typo). Danach greifen **Item-**`fontSize`/`fontWeight`, dann Top-Level-Spalte, dann **`styles`**. Fehlende Einträge → leere Zelle. **Fallback:** Fehlt `columns` oder die erste Zelle hat keinen Inhalt, werden `title`/`subtitle` des Items **nur für die erste Spalte** verwendet. **Parent mit Unterzeilen:** Die Parent-Zeile hat `timeEntries` wie jede andere Zeile (z. B. Summen-Balken). Kinder stehen in `items` und haben eigene `columns`-Werte. ```javascript columns: [ { title: "Fahrzeug / Tag", width: "160px" }, { title: "Info", width: "80px" } ], items: [{ id: "v1", title: "A-MS 419", subtitle: "GPS", columns: [ { title: "A-MS 419", subtitle: "GPS" }, { title: "⌂", subtitle: "Fzg." } ], timeEntries: [{ title: "Tagesblock", timeFrom: ... }], items: [{ id: "v1-mo", title: "Mo", columns: [{ title: "Mo" }, { title: "" }], timeEntries: [...] }] }] ``` ## collapsible – Gruppen ein- und ausklappen Wenn du hierarchische Einträge (`items` mit Unter-`items`) nutzt, kannst du die **Parent-Zeile** (z. B. Team- oder Fahrzeugname) per Widget-Prop **ein- und ausklappen**. Der Zustand wird **pro `uniqueId` im Browser** persistiert (`localStorage`). ### Konfiguration ```javascript arcCustomCalendarTimeline({ uniqueId: "timeline-teams-" + Nr, collapsible: { enabled: true, iconPosition: "left", iconCollapse: "", iconExpand: "" }, columns: [ { title: "Team / Person", width: "200px" }, { title: "Termine", width: "120px" } ], items: [ /* Parent mit items */ ] }) ``` | Property | Bedeutung | |----------|-----------| | `enabled` | `true`: Toggle-Icon in der **ersten Label-Spalte** der Parent-Zeile; Klick klappt alle Unterzeilen ein/aus. | | `iconPosition` | *(Optional)* `"left"` oder `"right"` – Icon vor/neben dem Text (Standard: `left`). | | `iconCollapse` | *(Optional)* HTML/SVG für **aufgeklappt** (Standard: Chevron unten). | | `iconExpand` | *(Optional)* HTML/SVG für **eingeklappt** (Standard: Chevron rechts). | - Gilt für Zeilen mit `items` (Kinder). **`id` des Parent-Items** muss eindeutig sein (Klappzustand). - `styles` am Parent/Kind wirken auf die gesamte Label-Zeile (Hintergrund der Zellen). --- Mit dem Custom Calendar Timeline bringst du ressourcenbasierte Planung und volle Flexibilität in deine Ninox-Datenbank – ideal für Einsatzplanung, Kapazitätsübersicht, Ressourcenmanagement und Flottendiagramme. Happy Widgeting 🥳 --- # Custom Dev Tools Source: https://www.arc-rider.com/docs/widgets/custom-dev-tools # DevTools in React Ein frei positionierbares Diagnosepanel anzeigen. Es sammelt vorhandene Editor-Fehlermarkierungen und zeigt eigene Debug-Werte; es ist keine React Error Boundary. Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.5`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components). ## Vollständiges Beispiel ## Features ausprobieren ### Debug-Werte und Panel-Lebenszyklus Ändere den State und blende das Panel ein und aus. Es bleibt innerhalb der Live-Vorschau und verändert die Docs-Oberfläche nicht. ## Komponenten-Props ### id Optionale DOM-ID. Im Normalfall nicht nötig; ohne Angabe wird sie automatisch erzeugt. ### data Konfiguration vom Typ `ArcWidgetDevToolsData`. Übergib Änderungen als neue Objekte; verwalte fachliche Daten und Auswahl in React-State. ### onAction Optionaler Fallback `(type: string, ...args: unknown[]) => unknown`. Der konkrete Callback hat Vorrang. Details: [State und Events](/docs/react/patterns/react-state-events). ### onFooterClick Ein Footer-Button mit `clickable: true` wurde angeklickt. Liefert seinen nullbasierten Index in `footerButtons`. ```typescript onFooterClick?: (index: number) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### errorPanel Das Panel: `visible` blendet es ein (Standard `false`), `maxErrors` begrenzt die Liste (Standard `50`), `debug` ergänzt eigene Key/Value-Zeilen (Objekte erscheinen als JSON), `footerButtons` ergänzt Buttons mit `title` und `clickable`. Typ: `ArcWidgetDevToolsErrorPanel`. ## Verhalten und Grenzen - Das Panel wird am Dokumentkörper angehängt. Fehler-Scanning benötigt einen passenden CodeMirror-Editor im Dokument. Ohne Editor zeigt es keine Anwendungsfehler an; `debug` zeigt die von dir übergebenen Werte. Zum Schließen entfernt das Beispiel die Komponente aus dem React-Baum. - Nur als `clickable` markierte Footer-Buttons lösen `onFooterClick` aus. Button-Titel und Debug-Werte werden als reiner Text angezeigt. - Die Position des Panels wird nach dem Verschieben je Instanz gespeichert. Zeige nur Diagnosedaten, die für die jeweiligen Nutzer bestimmt sind. ## Datentypen Diese widgetbezogenen Typen werden aus dem Paket exportiert. Gemeinsame Typen wie `ArcWidgetSlot`, `ArcWidgetStyles` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Die Konfigurationsobjekte sind geschlossen: Ein falsch geschriebener oder nicht unterstützter Schlüssel wird als Typfehler gemeldet. ```typescript export interface ArcWidgetDevToolsFooterButton { title?: string; clickable?: boolean; } export interface ArcWidgetDevToolsDebug { [key: string]: unknown; } export interface ArcWidgetDevToolsErrorPanel { visible?: boolean; maxErrors?: number; debug?: ArcWidgetDevToolsDebug; footerButtons?: ArcWidgetDevToolsFooterButton[]; } export interface ArcWidgetDevToolsData { errorPanel?: ArcWidgetDevToolsErrorPanel; } export interface ArcWidgetDevToolsProps extends ArcReactWidgetProps { onFooterClick?: (index: number) => void; } ``` ## Einsatzbeispiele - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Dev Tools ## AI Defaults (read first) - **uniqueId**: Required, e.g. `"dev-tools-" + Nr`. - **errorPanel.visible**: Only show when `isAdminMode()` – Dev-Tools sind für Entwickler gedacht. - **errorPanel.footerButtons**: Nutze `type: "update"` für Feld-Updates, `type: "customJS"` nur für spezielle UI-Aktionen. Core emits `footer_click` (index); Ninox runs `actions`. React: `onFooterClick(index)` / `onAction`. Set `clickable: true` on React hosts. # Custom Dev Tools Dev-Helper-Widget für Ninox mit erweiterbarer Sektionen-Struktur. Erste Sektion: **Error Panel** mit CodeMirror-Fehlerliste, Debug-Ausgabe und optionalen Footer-Buttons. ## Anwendungscode ```javascript if isAdminMode() then arcCustomDevTools({ uniqueId: "dev-tools-" + Nr, errorPanel: { visible: true, maxErrors: 50, debug: { activeTab: activeTab, filter: filterStatus, selectedRecord: selectedRecord }, footerButtons: [ { title: "State zurücksetzen", actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "helper_mainContentState"), value: "" }] } ] } }) end ``` ## Parameter | Parameter | Typ | Beschreibung | |----------|-----|--------------| | `uniqueId` | string | Erforderlich, eindeutige ID | | `errorPanel` | object | Konfiguration des Error-Panels | | `errorPanel.visible` | boolean | Error-Panel ein/aus | | `errorPanel.maxErrors` | number | Max. Anzahl angezeigter Fehler (Default: 50) | | `errorPanel.debug` | object | Debug-Variablen (key: value), z.B. `{ activeTab: activeTab, filter: filterStatus }` | | `errorPanel.footerButtons` | array | Custom Buttons im Footer, siehe unten | ### errorPanel.footerButtons Array von Button-Objekten mit: | Eigenschaft | Typ | Beschreibung | |-------------|-----|--------------| | `title` | string | Button-Label | | `actions` | array | Ninox-Actions wie bei arcCustomButton: `[{ type: "update", recordId: Nr, field: ..., value: ... }]` | ```javascript footerButtons: [ { title: "Editor fokussieren", actions: [{ type: "customJS", value: "document.querySelector('.cm-editor')?.focus()" }] }, { title: "State zurücksetzen", actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "helper_mainContentState"), value: "" }] } ] ``` ## Verhalten - **Editor geschlossen:** Zeigt „Editor nicht geöffnet“. Kein MutationObserver, nur leichtes Polling (1 s). - **Editor geöffnet:** Sammelt Fehler aus dem CodeMirror-Gutter und aktualisiert die Liste. - **Scan-Button:** Scrollt den Editor einmal durch und sammelt alle Fehler (auch außerhalb des sichtbaren Bereichs). Behebt das Virtualisierungs-Problem von CodeMirror. - **Klick auf Fehler:** Scrollt zur Zeile im Editor. Zwei-Phasen-Ansatz: Erst scrollt das Panel den Editor zum Zielbereich, dann wird die Zeile nach dem Render gesucht und zentriert. - **Position:** Panel wird unten rechts angezeigt. Position kann per Drag verschoben werden und wird in localStorage gespeichert. ## Erweiterbarkeit Später können weitere Sektionen ergänzt werden, z.B. `aiHelper: { visible: true }`. --- # Custom Drawing Source: https://www.arc-rider.com/docs/widgets/custom-drawing # Drawing in React Formen und Zeichnungen auf einer Zeichenfläche bearbeiten. Die Anwendung vergibt IDs, speichert Änderungen und verarbeitet Exporte. Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.5`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components). ## Vollständiges Beispiel ```tsx import { ArcWidgetDrawing } from '@arcrider/arcwidgets-react'; export default function Example() { // Only required fields are set; everything else is the widget default. return ( ); } ``` ## Features ausprobieren ### Freihand zeichnen und Formen übernehmen Zeichne eigene Formen. Erstellen und Bearbeiten aktualisieren den React-State. ### Markierungen speichern und wiederherstellen Wähle eine Markierung aus und teste die lokale Speicherung. ## Komponenten-Props ### id Optionale DOM-ID. Im Normalfall nicht nötig; ohne Angabe wird sie automatisch erzeugt. ### data Konfiguration vom Typ `ArcWidgetDrawingData`. Übergib Änderungen als neue Objekte; verwalte fachliche Daten und Auswahl in React-State. ### onAction Optionaler Fallback `(type: string, ...args: unknown[]) => unknown`. Der konkrete Callback hat Vorrang. Details: [State und Events](/docs/react/patterns/react-state-events). ### onShapeCreate Eine Form wurde gezeichnet oder ein Sticker platziert. Das zweite Argument ist ein Completion-Objekt: Setze dessen `promise` auf ein Promise mit der neuen ID. ```typescript onShapeCreate?: (payload: ArcWidgetDrawingShapeCreatePayload, completion: ArcWidgetDrawingShapeCreateCompletion) => void; ``` ### onShapeUpdate Eine gespeicherte Form wurde mit dem Verschieben-Werkzeug gezogen oder angeklickt und losgelassen. Liefert `shape_id`, die neue Geometrie und mit `dataBag` den ganzen Topf. ```typescript onShapeUpdate?: (payload: ArcWidgetDrawingShapeUpdatePayload) => void; ``` ### onShapeActivate Eine Form mit `clickable: true` wurde angeklickt. Liefert ihre `shape_id`. ```typescript onShapeActivate?: (payload: ArcWidgetDrawingShapeActivatePayload) => void; ``` ### onExport Ein Export mit Ziel `field` ist fertig. Liefert das Format und die Datei als Data-URL. ```typescript onExport?: (payload: ArcWidgetDrawingExportPayload) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen in verschachtelten Objekten sind Zahlen in Pixeln; Widget-Größen sind CSS-Strings. ### artboard Pflicht. Zeichen- und Exportbereich in Pixeln. Typ: `ArcWidgetDrawingArtboard` (`{ width: number; height: number }`). ### shapes Pflicht. Formen mit stabiler `id` und Geometrie in `shapeDataValue` (Objekt oder JSON-String). Für eine leere Fläche `[]` übergeben. Optional je Form: `movable`, `scalable`, `clickable`, `stickerUID`, `label`, `placeholders`, `export`, `tooltip`, `sidebarItem`. Typ: `ArcWidgetDrawingShape[]`. ### height Höhe des Widgets, z. B. `"700px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### width Breite des Widgets, z. B. `"100%"`. Typ: `string`. ### canvas Verschiebbare Arbeitsfläche um das Artboard in Pixeln: `width` / `height` oder `offsetX` / `offsetY` als zusätzlicher Rand je Seite. Ohne Angabe lässt sich die Ansicht etwa eine Viewport-Größe über das Artboard hinaus bewegen. Typ: `ArcWidgetDrawingCanvas`. ### zooming Start-Zoom (`default`, 1), Grenzen (`min` 0,1, `max` unbegrenzt), Schrittweite der Buttons (`step`, 0,1) und Geschwindigkeit für Strg + Mausrad (`panSpeed`, 0,002). Typ: `ArcWidgetDrawingZooming`. ### drawingSettings Linie für neue Freihandlinien, Rechtecke, Kreise und Polygone: `strokeColor` (Standard `black`) und `strokeWidth` in Bildschirmpixeln (Standard `1`). `polygon.fillColor` setzt die Füllung neuer Polygone (Standard: keine Füllung). `text` enthält die Standardwerte für neuen Text: `color` (Standard `black`), `fontSize` (Standard `16`), `fontWeight` (Standard `normal`) und `fontFamily` (Standard `Arial, Helvetica, sans-serif`). Typ: `ArcWidgetDrawingSettings`. ### tools `useMoveTool: true` lässt Formen nur mit dem Verschieben-Werkzeug greifen; in den anderen Werkzeugen erzeugt ein Klick auf eine Form neuen Inhalt. Typ: `ArcWidgetDrawingTools`. ### create `enabled: false` deaktiviert Zeichenwerkzeuge und Sticker. Verschieben der Ansicht, Zoomen, Verschieben von Formen und Klicks funktionieren weiter. Standard: `true`. Typ: `ArcWidgetDrawingCreate`. ### stickerTypes Sticker zum Platzieren: `uid`, `image`, `width`, `height` sowie optional `icon`, `label`, `originX` / `originY`, `default`, `labelPlaceholder`, `defaultLabel`, `defaultPlaceholders`. Typ: `ArcWidgetDrawingStickerType[]`. ### toolbarSettings Reihenfolge und Gruppierung der Toolbar-Buttons in `top` und `bottom`. Einträge sind Werkzeug-IDs (`"move"`, `"draw"`, `"rect"`, `"circle"`, `"polygon"`, `"text"`, `"image"`, `"export"`), `"stickers"`, `{ tool, icon }`, `{ sticker, icon }` oder in `top` eine Gruppe `{ group, label, icon, items }`. Nicht aufgeführte Buttons werden nicht angezeigt. Typ: `ArcWidgetDrawingToolbarSettings`. ### exportSettings Export-Button und Formate. `allowedTypes` ist Pflicht und listet `"jpg"`, `"png"`, `"svg"` oder Einstellungen je Format. `target: "field"` übergibt die Datei an `onExport`; `"file"` (Standard) startet den Download. Typ: `ArcWidgetDrawingExportSettings`. ### dataBag Liest die Geometrie aus einem `key` eines JSON-Topfs in `shapeDataValue`, damit deine übrigen Keys erhalten bleiben. `onShapeUpdate` liefert den ganzen Topf als `bag`. Typ: `ArcWidgetDrawingDataBag`. ### rightSideContent Seitenleiste rechts neben der Zeichenfläche mit `header`, `content` (`showShapeLayers` oder `value`) und `footer`. Ohne Angabe ausgeblendet; `show: false` blendet sie aus. Typ: `ArcWidgetDrawingSidebar`. ## Verhalten und Grenzen - Jede Form braucht eine stabile `id` und `shapeDataValue` als Objekt oder serialisierten JSON-String; Formen ohne `shapeDataValue` werden nicht gezeichnet. - Beim Erstellen erhältst du einen zweiten Parameter: Setze `completion.promise` auf ein Promise mit der neuen ID. Übernimm dieselbe Form mit dieser ID in deinen State. - `onShapeUpdate` liefert `shape_id` und die neue Geometrie in `shape_data_value`. Ersetze die passende Form unveränderlich in deinem State. - Für `onExport` ist `exportSettings.allowedTypes: [{ format: "png", target: "field" }]` nötig. `target: "field"` übergibt das Bild als Data-URL an `onExport`; `target: "file"` startet den Browser-Download. Unterstützte `ArcWidgetSlot`-Felder erlauben einzelne JSX-Elemente, Text oder Widget-Deskriptoren. Reiche fremden Text als JSX-Text weiter; HTML-Strings werden als HTML interpretiert. [Inhalte kombinieren](/docs/react/patterns/nested-widget-descriptors). ## Datentypen Diese widgetbezogenen Typen werden aus dem Paket exportiert. Gemeinsame Typen wie `ArcWidgetSlot`, `ArcWidgetStyles` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Die Konfigurationsobjekte sind geschlossen: Ein falsch geschriebener oder nicht unterstützter Schlüssel wird als Typfehler gemeldet. ```typescript export type ArcWidgetDrawingShapeType = 'rect' | 'circle' | 'polyline' | 'polygon' | 'text' | 'image' | 'sticker'; export interface ArcWidgetDrawingShapeData { type: ArcWidgetDrawingShapeType; x?: number; y?: number; width?: number; height?: number; cx?: number; cy?: number; r?: number; points?: string; href?: string; stickerUID?: string; stroke?: string; strokeWidth?: number; fill?: string; text?: string; fontSize?: number; fontWeight?: string | number; fontFamily?: string; [key: string]: unknown; } export type ArcWidgetDrawingTooltipStyles = ArcWidgetCssProperties; export interface ArcWidgetDrawingTooltip { content?: ArcWidgetSlot; width?: number | string; height?: number | string; alignX?: 'left' | 'center' | 'right'; alignY?: 'top' | 'center' | 'bottom'; offsetX?: number; offsetY?: number; styles?: ArcWidgetDrawingTooltipStyles; } export interface ArcWidgetDrawingSidebarItem { header?: ArcWidgetSlot; content?: ArcWidgetSlot; hide?: boolean; } export interface ArcWidgetDrawingShape { id: string | number; shapeDataValue?: ArcWidgetDrawingShapeData | string; movable?: boolean; scalable?: boolean; clickable?: boolean; stickerUID?: string; label?: string | number; placeholders?: Record; export?: boolean; tooltip?: ArcWidgetDrawingTooltip; sidebarItem?: ArcWidgetDrawingSidebarItem; } export type ArcWidgetDrawingIcon = string | ArcNestedWidgetDescriptor; export interface ArcWidgetDrawingStickerLabel { value: string; visible?: boolean; fontColor?: string; fontSize?: number | string; maxWidth?: number | string; } export interface ArcWidgetDrawingStickerType { uid: string; image: string; icon?: ArcWidgetDrawingIcon; label?: string | ArcWidgetDrawingStickerLabel; width: number; height: number; originX?: number; originY?: number; default?: boolean; labelPlaceholder?: string; defaultLabel?: string | number; defaultPlaceholders?: Record; } export type ArcWidgetDrawingTool = 'move' | 'draw' | 'rect' | 'circle' | 'polygon' | 'text' | 'image' | 'export'; export type ArcWidgetDrawingToolbarItem = | ArcWidgetDrawingTool | 'stickers' | { tool: ArcWidgetDrawingTool; icon?: ArcWidgetDrawingIcon; } | { sticker: string; icon?: ArcWidgetDrawingIcon; }; export interface ArcWidgetDrawingToolbarGroup { group: string; label?: string; icon?: ArcWidgetDrawingIcon; items: ArcWidgetDrawingToolbarItem[]; } export interface ArcWidgetDrawingToolbarSettings { top?: (ArcWidgetDrawingToolbarItem | ArcWidgetDrawingToolbarGroup)[]; bottom?: ArcWidgetDrawingToolbarItem[]; } export type ArcWidgetDrawingExportFormat = 'jpg' | 'png' | 'svg' | 'pdf'; export type ArcWidgetDrawingExportTarget = 'file' | 'field'; export interface ArcWidgetDrawingExportType { format: ArcWidgetDrawingExportFormat; target?: ArcWidgetDrawingExportTarget; mergeFile?: boolean; apiKey?: string; autoExport?: boolean; autoExportDelay?: number; } export interface ArcWidgetDrawingExportSettings { allowedTypes: (ArcWidgetDrawingExportFormat | ArcWidgetDrawingExportType)[]; target?: ArcWidgetDrawingExportTarget; apiKey?: string; mergeFile?: string; iconColor?: string; } export interface ArcWidgetDrawingArtboard { width: number; height: number; } export interface ArcWidgetDrawingCanvas { width?: number; height?: number; offsetX?: number; offsetY?: number; } export interface ArcWidgetDrawingZooming { default?: number; min?: number; max?: number; step?: number; panSpeed?: number; } export interface ArcWidgetDrawingPolygonSettings { fillColor?: string; } export interface ArcWidgetDrawingTextSettings { color?: string; fontSize?: number; fontWeight?: string | number; fontFamily?: string; } export interface ArcWidgetDrawingSettings { strokeColor?: string; strokeWidth?: number; polygon?: ArcWidgetDrawingPolygonSettings; text?: ArcWidgetDrawingTextSettings; } export interface ArcWidgetDrawingTools { useMoveTool?: boolean; } export interface ArcWidgetDrawingCreate { enabled?: boolean; } export interface ArcWidgetDrawingDataBag { key: string; encoding?: 'raw' | 'urlEncoded'; } export interface ArcWidgetDrawingSidebarContent { showShapeLayers?: boolean; value?: ArcWidgetSlot; } export interface ArcWidgetDrawingSidebar { show?: boolean; width?: string; header?: ArcWidgetSlot; content?: ArcWidgetDrawingSidebarContent; footer?: ArcWidgetSlot; } export interface ArcWidgetDrawingData { artboard: ArcWidgetDrawingArtboard; shapes: ArcWidgetDrawingShape[]; height?: string; width?: string; canvas?: ArcWidgetDrawingCanvas; zooming?: ArcWidgetDrawingZooming; drawingSettings?: ArcWidgetDrawingSettings; tools?: ArcWidgetDrawingTools; create?: ArcWidgetDrawingCreate; stickerTypes?: ArcWidgetDrawingStickerType[]; toolbarSettings?: ArcWidgetDrawingToolbarSettings; exportSettings?: ArcWidgetDrawingExportSettings; dataBag?: ArcWidgetDrawingDataBag; rightSideContent?: ArcWidgetDrawingSidebar; } export interface ArcWidgetDrawingShapeCreatePayload { shape_data_value: ArcWidgetDrawingShapeData; sticker_uid: string | null; } export interface ArcWidgetDrawingShapeCreateCompletion { promise?: Promise; } export interface ArcWidgetDrawingShapeUpdatePayload { shape_id: string | number; shape_data_value: ArcWidgetDrawingShapeData; bag?: Record; } export interface ArcWidgetDrawingShapeActivatePayload { shape_id: string | number; } export interface ArcWidgetDrawingExportPayload { format: ArcWidgetDrawingExportFormat; base64: string; } export interface ArcWidgetDrawingProps extends ArcReactWidgetProps { onShapeCreate?: (payload: ArcWidgetDrawingShapeCreatePayload, completion: ArcWidgetDrawingShapeCreateCompletion) => void; onShapeUpdate?: (payload: ArcWidgetDrawingShapeUpdatePayload) => void; onShapeActivate?: (payload: ArcWidgetDrawingShapeActivatePayload) => void; onExport?: (payload: ArcWidgetDrawingExportPayload) => void; } ``` ## Einsatzbeispiele - [Ein Bild annotieren und exportieren](/docs/react/examples/custom-drawing-zeichnen-im-bild-teil1) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Drawing ## AI Defaults (read first) - **uniqueId**: Required, `"drawing-" + Nr`. - **tableId**: Required – the shapes table (`tableId("Shapes")`). - **fieldId**: The text field storing shape data (JSON), use `fieldId(Nr, "Feldname")`. - **height**: Typically `"800px"` or `"100%"`. - **canvas/artboard**: `canvas: { width: 4000, height: 4000 }`, `artboard: { width: 1000, height: 1000 }`. - **shapes**: Each shape needs `shapeId: Nr` and `shapeDataValue` (from the shapes table text field). - **embedded**: Default `false`, set `true` when nested. - **Persistence**: Core emits `shape_create` / `shape_update` / `shape_activate` / `export`. Ninox handlers build Action Performer `create`/`update` and run shape `actions`. React: `onShapeCreate`, `onShapeUpdate`, `onShapeActivate`, `onExport` (or `onAction`). # Custom Drawing Das **Custom Drawing Widget** ermöglicht es, beliebige Bilder (z. B. Grundrisse, Produktfotos, Körper-Silhouetten) hochzuladen und mit interaktiven **Pins, Sticker oder Freihandzeichnungen** zu versehen. Jede Markierung kann mit einer **Notiz**, **Kategorie** oder **zusätzlichen Information** verknüpft werden. Ideal für Anwendungen in Bau, Mode, Medizin, Logistik u. v. m. ## Anwendungscode ```javascript arcCustomDrawing({ uniqueId: "titel " + Nr, embedded: false, height: "800px", tableId: tableId("Shapes"), fieldId: fieldId("Shapes", "helper_shapeDataValue"), changeFieldValues: [{ fieldId: fieldId("Tabelle", "Feld Name"), value: "Wert" }], exportSettings: { allowedTypes: [{ format: "jpg" }, { format: "png" }, { format: "svg" }], target: "file", recordId: Nr, fieldId: fieldId("Tabelle", "Feld für den Export"), }, canvas: { width: 4000, height: 4000, offsetX: 0, offsetY: 0 }, zooming: { step: 0.25, min: 0.01, max: 4 }, drawingSettings: { strokeWidth: 1, strokeColor: "#ffffff" }, artboard: { width: 1000, height: 1000, }, stickerTypes:[{ uid: uid, image: "stickerBase64", icon: base64, label: Name, width: 100, height: 100, originX: 0.5, originY: 0.89, default: false }], shapes: (Shapes).[{ shapeId: Nr, movable: true, shapeDataValue: helper_shapeDataValue, stickerUID: "", sidebarItem: { header: "Titel des Eintrags", content: "Beschreibung des Eintrags", } }], rightSideContent: { show: true, header: "Header", footer: "Footer" } }) ``` ### 🔸 `embedded` Gibt an, ob das Widget sich in eine bestehende Maske (z. B. Ninox-Formular) einfügt. ```javascript embedded: "" // default: false embedded: true, ``` 👀 **Tipp:** Bei `true` ist keine feste Höhe nötig – das Widget passt sich automatisch dem Elterncontainer an. ### 🔸 `height` Mit dem Parameter `height` gibst du die **Gesamthöhe** des Drawing-Tools in Pixel an. Er bestimmt, wie viel vertikaler Platz das Widget im eingebetteten Kontext (z. B. in einem Formular oder auf einer Seite) einnimmt. **Empfehlung: **Das Widget sollte für eine bessere UI idealer Weise in Custom Layout eingebunden werden. Und die Höhe im Layout Block bestimmt werden. ### 🔸 `tableId` & `fieldId` Diese beiden Parameter definieren, **wo die Formdaten (Shapes)** gespeichert oder geladen werden. - `**tableId**` gibt die Tabelle an, in der die Shapes verwaltet werden. - `**fieldId**` verweist auf das konkrete Feld innerhalb dieser Tabelle, das die Geometriedaten (`shapeDataValue`) des jeweiligen Shapes enthält. Achtung: Hier muss ein Ninox-Textfeld verwendet werden. Du kannst hiermit eigene Formen, Marker oder Objekte dynamisch speichern – z. B. Rechtecke, Sticker, Kreise, Bilder oder Linien. ### 🔸 `dataBag` Optional. Steuert, wie Shape-Daten in einem **JSON-Topf** (ein Textfeld mit mehreren Keys) gelesen und geschrieben werden – ohne andere Keys zu überschreiben. ```javascript dataBag: { key: "pin" // Shape liest/schreibt nur diesen Key im Topf // encoding: "raw" // Default — Plain-JSON; nur bei Legacy: "urlEncoded" } ``` **Lesen:** Das Feld `shapeDataValue` pro Shape enthält den gesamten Topf als Plain-JSON. Das Widget extrahiert `key` als Geometrie. In Formeln: `parseJSON(helper_data)` — ohne `urlDecode`. **Schreiben:** Beim Verschieben wird nur `key` aktualisiert; andere Keys (`placedAt`, `meta`, …) bleiben erhalten. **Beispiel: Wartungsplan mit Prüfpunkten** (z. B. Gebäudeinspektion): ```json { "geo": { "type": "sticker", "stickerUID": "pin_ok", "x": 420, "y": 180, "width": 40, "height": 40 }, "placedAt": "2026-06-13", "inspectionPointId": "123" } ``` Ohne `dataBag` bleibt das bisherige Verhalten: das gesamte Feld ist `shapeDataValue` (raw JSON). > **Hinweis:** Das ist die **Widget-Config** (`key` + `encoding`). Für **Actions** (`update` mit `patch` in State-Bags) gilt dasselbe `dataBag`-Konzept plattformweit — siehe [`data-bag-actions`](../patterns/data-bag-actions.md). ### 🔸 `changeFieldValues` Erlaubt es, beim **Anlegen eines neuen Shapes** zusätzliche Felder oder Verknüpfungen zu befüllen – z. B. Kategorie, Ersteller o. ä. Pro Eintrag: `{ fieldId, value }` — `fieldId` ist die Ninox-Feld-ID (z. B. via `fieldId("Tabelle", "Feldname")`). **Sticker-spezifisch:** Zusätzlich kann jedes Element in `stickerTypes` ein eigenes `changeFieldValues`-Array haben. Beim Platzieren eines Stickers werden zuerst die **globalen** `changeFieldValues` am Drawing gesetzt, danach die des **aktiven Sticker-Typs** — bei gleichem `fieldId` gewinnt der Sticker. ```javascript changeFieldValues: [{ fieldId: fieldId("Pruefpunkte", "Objekt zuordnen"), value: number(obj.Nr) }], stickerTypes: [{ uid: "pin_ok", changeFieldValues: [{ fieldId: fieldId("Pruefpunkte", "Kategorie"), value: "OK" }] }] ``` **Laufende Nummern:** Werte wie Positionsnummern oder fortlaufende IDs **nicht** über `changeFieldValues` setzen — der Wert stammt vom letzten Render und ist bei mehreren schnellen Creates falsch. Stattdessen **After-change-Trigger** auf dem Parent-Link-Feld; im Trigger **`myNr := number(Nr)` vor** dem Filter `number(Nr) != myNr`, sonst filtert man sich selbst weg. ### 🔸 `create` Steuert das **Anlegen neuer Shapes** (Pins, Linien, Rechtecke, Kreise, Polygone, Text, Bilder) und bündelt optional die Create-Konfiguration. Empfohlen ab dieser Version; die Root-Props `tableId`, `fieldId`, `changeFieldValues` und `setNewRecordId` bleiben **weiterhin gültig** (Legacy). ```javascript create: { enabled: if lockCreate then false else true end, tableId: tableId("Shapes"), fieldId: fieldId("Shapes", "helper_shapeDataValue"), changeFieldValues: [{ fieldId: fieldId("Pruefpunkte", "Objekt"), value: number(obj.Nr) }], setNewRecordId: [{ recordId: Nr, fieldId: fieldId("Objekt", "Letzter Shape") }] } ``` | Feld | Beschreibung | |------|--------------| | `enabled` | `false` = kein neues Shape (Toolbar ausgegraut, Pan/Klick/`actions` bleiben). Default: `true`. | | `tableId` | Zieltabelle für neue Shapes (überschreibt Root-`tableId`, falls gesetzt). | | `fieldId` | Textfeld für Geometrie am **neuen** Datensatz (überschreibt Root-`fieldId`). | | `changeFieldValues` | Zusatzfelder beim Create (überschreibt Root-`changeFieldValues`). | | `setNewRecordId` | Nach Create: neuen Datensatz in Parent verlinken (`[{ recordId, fieldId }]`). | **Hinweise:** - `dataBag` bleibt am Widget-Root (Lesen + Schreiben bestehender Shapes). - `stickerTypes[].changeFieldValues` bleibt sticker-spezifisch und ergänzt die globalen Werte. - Nur `create.enabled: false` reicht zum Sperren — `tableId`/`fieldId` am Root können für bestehende Shapes weiter gesetzt bleiben. ### 🔸 `setNewRecordId` (Legacy am Root) Optional am Widget-Root oder innerhalb von `create`. Nach dem Anlegen eines Shapes wird der neue Datensatz in ein Referenzfeld geschrieben — z. B. um den Parent-Datensatz zu verknüpfen. ```javascript setNewRecordId: [{ recordId: Nr, fieldId: fieldId("Objekt", "Letzter Shape") }] ``` ### 🔸 `exportSettings` Mit diesem Block konfigurierst du die **Exportfunktion des Widgets**. Du kannst festlegen, in welchen Formaten der Nutzer exportieren darf (z. B. PNG) und wohin die Datei gespeichert werden soll.
Feld Beschreibung
`allowedTypes` Liste der erlaubten Exportformate. Du kannst `"jpg"`, `"png"` und `"svg"` angeben.
`target` Legt fest, wohin die Datei exportiert werden soll. `"file"` bedeutet, die Datei wird heruntergeladen. `"field"` bedeutet, die Datei wird in ein Datei-Feld in Ninox hochgeladen.
`recordId` Gibt den Datensatz an, in den exportiert werden soll (nur bei `target: "field"` erforderlich).
`fieldId` Definiert das genaue Feld im Datensatz, in das die exportierte Datei geschrieben wird.
#### 🔄 Auto-Export Mit `autoExport` kannst du festlegen, dass nach jeder Shape-Änderung automatisch ein Export erfolgt – z. B. um ein Live-Vorschaubild zu speichern. Die Properties `autoExport`, `autoExportDelay`, `target`, `recordId` und `fieldId` können sowohl global als auch **pro Format** in `allowedTypes` gesetzt werden. Format-spezifische Einstellungen überschreiben die globalen. **Beispiel mit Auto-Export:** ```javascript exportSettings: { allowedTypes: [{ format: "jpg", target: "field", recordId: Nr, fieldId: fieldId("Tabelle", "Vorschaubild"), autoExport: true, autoExportDelay: 500 // Verzögerung in ms (default: 500) }, { format: \"png\", target: "file", // Manueller Download autoExport: false }] } ``` | Feld | Beschreibung | |------|--------------| | `autoExport` | `true` oder `false` – aktiviert automatischen Export nach jeder Shape-Änderung | | `autoExportDelay` | Verzögerung in Millisekunden bevor der Export startet (default: 500). Dient als Debounce bei schnellen Änderungen. | ### 🔸 `canvas` Mit dem `canvas`-Block definierst du die **technische Zeichenfläche**, also den kompletten Bereich, auf dem Shapes gezeichnet und verschoben werden können. Dieser Bereich kann deutlich größer sein als der sichtbare Ausschnitt (`artboard`) und bestimmt, wie weit sich Nutzer im Zeichenbereich bewegen können (z. B. beim Verschieben oder Zoomen). #### 🔧 Struktur: - `width`: Die gesamte Breite der Zeichenfläche in Pixeln. - `height`: Die gesamte Höhe der Zeichenfläche in Pixeln. - `offsetX`: Abstand zum linken und rechten Rand – kann genutzt werden, um Inhalte zentriert darzustellen. - `offsetY`: Abstand zum oberen und unteren Rand – ideal für zusätzliche Arbeitsfläche oder Druckränder. Tipp: Du kannst durchaus auch dynamische Werte aus Ninox Feldern oderBerechnungen hier einsetzen. Beispielsweise haben wir Workflows entwickelt, in denen wir die Größe des canvas abhängig von der Größe des Hintergrundshapes gemacht haben. ### 🔸 `zooming` Der `zooming`-Block steuert, **wie weit in das Zeichenfeld hinein- oder herausgezoomt werden kann** – etwa per Mausrad oder Touch-Gesten. Damit definierst du den Zoombereich und wie fein abgestuft gezoomt wird. #### 🔧 Struktur: - `step`: Gibt an, wie groß ein Zoom-Schritt ist, z. B. bei Klick auf die Plus/Minus-Buttons oder beim Scrollen mit gedrückter Strg-Taste. Ein kleiner Wert (z. B. `0.1`) bedeutet feinere Zoomschritte. - `min`: Der **kleinste erlaubte Zoomfaktor** (z. B. `0.1` = 10 %). Damit legst du fest, wie weit man maximal herauszoomen darf. - `max`: Der **größte erlaubte Zoomfaktor** (z. B. `4` = 400 %). Damit begrenzt du das Hereinzoomen auf eine bestimmte Detailstufe. 💡 **Hinweise:** - Der initiale Zoom wird automatisch so gesetzt, dass das Artboard gut in das Widget passt. - Zoomaktionen wirken sich auf die Darstellung der Shapes aus, nicht auf ihre tatsächliche Größe im `canvas`. - Wird kein `zooming`-Block gesetzt, gelten Standardwerte (Zoom zwischen ca. 10 % und 400 %). ### 🔸 `drawingSettings` Mit dem `drawingSettings`-Block legst du die **Standard-Eigenschaften für neue Zeichenobjekte** fest – z. B. Linienfarbe und -stärke. Diese Werte gelten für alle neu gezeichneten Formen wie Linien, Rechtecke, Kreise, Polygone oder Text, solange der Nutzer keine individuellen Einstellungen verändert. #### 🔧 Struktur: - `strokeWidth`: Bestimmt die Linienbreite in Pixeln. Ein Wert von `1` ergibt eine feine Linie, `2` oder `3` wirken etwas kräftiger. - `strokeColor`: Definiert die Farbe der Linien im Hex-Format, z. B. `#ffffff` für Weiß oder `#ffaa21` für Orange. - `polygon.fillColor`: Füllfarbe neuer Polygone, z. B. `rgba(0, 120, 255, 0.2)`. Ohne Angabe haben Polygone keine Füllung. - `text.color`: Textfarbe für neuen Text (Standard `black`). - `text.fontSize`: Schriftgröße neuer Texte in Canvas-Pixeln (Standard `16`). - `text.fontWeight`: Schriftstärke, z. B. `bold` oder `600` (Standard `normal`). - `text.fontFamily`: CSS-Schriftfamilie (Standard `Arial, Helvetica, sans-serif`). 💡 **Hinweis:** Du kannst `strokeColor` und `strokeWidth` auch **aus Ninox-Feldern dynamisch übergeben**, um die Zeichenoptionen z. B. **vom Nutzer auswählbar zu machen**. So kannst du pro Datensatz individuelle Zeichenstile ermöglichen. ### 🔸 `toolbarSettings` Optional. Steuert **Reihenfolge, Sichtbarkeit und Gruppierung** der Toolbar-Buttons. Ohne `toolbarSettings` bleibt das Standard-Layout unverändert (Move, Zeichenwerkzeuge, Stickers-Untermenü, Export). #### Struktur - `top`: Buttons in der oberen Toolbar-Leiste (von oben auf Desktop / links auf Mobile). - `bottom`: Buttons in der unteren Leiste (Export etc.). #### Einträge in `top` / `bottom` | Form | Bedeutung | |------|-----------| | `"move"` | Tool mit Standard-Icon (Kurzform) | | `{ tool: "draw", icon: "..." }` | Tool mit eigenem Icon (URL, Base64 oder SVG) | | `"stickers"` | Alle `stickerTypes` **direkt** in der Toolbar (Ebene 1) | | `{ sticker: "pin_rot" }` | Nur dieser Sticker direkt in der Toolbar | | `{ group: "zeichnen", icon: "...", items: ["draw", "rect", "circle"] }` | Gruppe mit Unterpanel (wie bisher bei Stickers/Export) | #### Polygon- und Text-Werkzeug Beide Werkzeuge sind nicht in der Standard-Toolbar. Mit `"polygon"` und `"text"` in `toolbarSettings` blendest du sie ein. - `"polygon"`: Jede Ecke per Klick setzen. Beenden mit Klick auf die erste Ecke (ab drei Ecken), Doppelklick oder Enter; Esc bricht ab, Rücktaste entfernt die letzte Ecke. - `"text"`: Dorthin klicken, wo der Text beginnen soll, tippen und mit Enter bestätigen (Umschalt+Enter = neue Zeile). Esc bricht ab. Doppelklick auf einen vorhandenen Text bearbeitet ihn. **Weglassen = nicht anzeigen.** Es gibt kein separates `enabled`-Flag — was nicht im Layout steht, erscheint nicht. #### Beispiel: Prüfpunkt-Pins, nur Move + ausgewählte Sticker ```javascript toolbarSettings: { top: [ "move", { sticker: "pin_rot" }, { sticker: "pin_gruen" } ], bottom: ["export"] } ``` #### Beispiel: Zeichenwerkzeuge gruppieren ```javascript toolbarSettings: { top: [ "move", { group: "zeichnen", label: "Zeichnen", icon: "data:image/svg+xml,...", items: ["draw", "rect", "circle"] }, "image", "stickers" ], bottom: ["export"] } ``` ### 🔸 `artboard` Der `artboard`-Block definiert den **sichtbaren Ausschnitt deiner Zeichenfläche** – also das, was der Nutzer beim Öffnen des Widgets zunächst sieht. Es handelt sich sozusagen um das „Papier“, auf dem gezeichnet wird – während der `canvas`-Bereich das „gesamte Atelier“ darstellt. #### 🔧 Struktur: - `width`: Breite des sichtbaren Zeichenbereichs in Pixeln. - `height`: Höhe des sichtbaren Zeichenbereichs in Pixeln. Dieser Bereich wird automatisch zentriert und skaliert dargestellt, sodass er vollständig im Widget sichtbar ist (abhängig von Zoom- und Darstellungsparametern). 💡 **Hinweise:** - Das `artboard` ist rein visuell – alle Shapes können sich auch **außerhalb dieses Bereichs** befinden, solange sie innerhalb des `canvas` liegen. - Wenn du ein bestimmtes Seitenverhältnis möchtest (z. B. A4 oder Quadrat), kannst du die `width` und `height` entsprechend setzen. - In Kombination mit `zooming` und `canvas` kannst du gezielt steuern, wie viel Platz Nutzer:innen für ihre Zeichnung haben – und was beim Start sofort sichtbar ist. Tipp: Du kannst durchaus auch dynamische Werte aus Ninox Feldern oderBerechnungen hier einsetzen. Beispielsweise haben wir Workflows entwickelt, in denen wir die Größe des artboards abhängig von der Größe des Hintergrundshapes gemacht haben. Dadurch scrollt der User nicht so viel hin und her. ### 🔸 `stickerTypes` Der `stickerTypes`-Block erlaubt es dir, **individuelle Sticker** bereitzustellen, die Nutzer im Zeichenbereich platzieren können – z. B. Icons, Symbole, Marker oder Piktogramme. Jeder Sticker ist ein Objekt mit Eigenschaften wie Bildquelle, Größe und Positionierungsverhalten. Ideal für Use Cases wie: - 🔧 Technische Markierungen auf Fotos oder Plänen - 📍 Standortpins auf Karten oder Grundrissen - 👕 Qualitätskontrollen in der Modebranche - 🏥 Medizinische Annotationen auf Körpersilhouetten #### 🔧 Struktur je Sticker: - `uid`: Eindeutige ID des Stickers (z. B. `"pin_rot"`). - `image`: Base64-Daten-URL oder URL zur Grafik — wird beim Platzieren auf dem Canvas verwendet. - `icon` (optional): Vorschaubild **nur** für den Toolbar-Button (wenn abweichend vom Stickerbild). Erlaubt sind: - Bild (SVG-Markup, PNG/JPG/Data-URL, HTTP-URL) — wie `image` - `arcCustomIcon`-Widget-Objekt: `{ widget: "arcCustomIcon", data: { iconSet: "phosphor", name: "map-pin", color: "#1565c0" } }` - `label`: Bezeichnung des Stickers. Zwei Formen möglich: - **String** (Legacy): nur Tooltip beim Hovern über dem Toolbar-Button, kein sichtbarer Text. - **Objekt** (neu): sichtbarer Text unter dem Icon im Toolbar-Button — `{ value: "K", visible: true, fontColor: "#1565c0", fontSize: 10, maxWidth: 50 }`. `visible: false` verhält sich wie der String-Fall (nur Tooltip). Zu langer Text wird bei `maxWidth` mit `…` abgeschnitten. - `width`: Breite des Stickers in Pixeln. - `height`: Höhe des Stickers in Pixeln. - `originX`: Horizontaler Ankerpunkt (0 = links, 0.5 = Mitte, 1 = rechts). - `originY`: Vertikaler Ankerpunkt (0 = oben, 0.5 = Mitte, 1 = unten). - `default` (optional): Wenn `true`, wird dieser Sticker vorausgewählt. - `labelPlaceholder` (optional): Platzhalter-String im SVG (`image`), der durch `label` am Shape ersetzt wird — Konvention: `##LABEL##` (wie bei anderen arcWidgets: `##key##`). - `defaultLabel` (optional): Fallback für `label` direkt beim Platzieren, bis Ninox die Formel neu liefert. - `defaultPlaceholders` (optional): Fallback-Objekt für `shape.placeholders` beim Platzieren (gleiche Keys wie im SVG). - `changeFieldValues` (optional): Zusätzliche Felder beim **Anlegen** eines Datensatzes für diesen Sticker-Typ — merged mit globalen `changeFieldValues` (Sticker überschreibt bei gleichem `fieldId`). #### Dynamische SVG-Platzhalter (Sticker + Shape) Für Marker mit Text, Status-Dot und Icons kann das **SVG-Template** Platzhalter enthalten. Das Widget ersetzt sie beim Rendern und im Export: | Quelle | Ersetzung | |--------|-----------| | `stickerType.labelPlaceholder` + `shape.label` | z. B. `##LABEL##` → `"K15"` | | `shape.placeholders` (Objekt) | Jeder Key `foo` ersetzt `##foo##` im SVG | | `stickerType.defaultLabel` / `defaultPlaceholders` | Sofort beim Platzieren, bis Ninox reagiert | **Data-URL-Hinweis:** Wenn das SVG per `encodeURIComponent` in `data:image/svg+xml,…` eingebettet ist, ersetzt das Widget Platzhalter auch in URL-kodierter Form (z. B. `%23%23LABEL%23%23` → `K15`, `%23ff0000` für `#ff0000`). **Warum `##key##`?** Wie bei arcCustomInput (`##inputValue##`) und arcCustomCalendarWeek (`##clickedDate##`). Anders als `{{key}}` kollidiert `##key##` nicht mit der Ninox-Syntax `{variable}` in Text-Blöcken. Beispiel `shape.placeholders` für Status-Icons via `display` (alle Varianten im SVG, Ninox schaltet Sichtbarkeit): ```javascript placeholders: { statusColor: "#4caf50", showCheck: "block", showWarn: "none", showError: "none" } ``` Beispiel `defaultPlaceholders` am `stickerType` (Fallback bis Ninox reagiert): ```javascript defaultLabel: "", defaultPlaceholders: { statusColor: "#9e9e9e", showCheck: "none", showWarn: "none", showError: "none" } ``` Neue Platzhalter erfordern **keine Widget-Änderung** — nur `##key##` ins SVG und passenden Key in `shape.placeholders`. 💡 **Hinweise:** - Die Sticker erscheinen im linken Menü des Widgets unter „Stickers“. - Beim Platzieren eines Stickers wird seine Position automatisch anhand von `originX` / `originY` berechnet. So kannst du z. B. Marker exakt mit ihrer Spitze am Klickpunkt ausrichten. - Du kannst beliebig viele Sticker definieren – z. B. farbige Pins, Icons für Mängel, Emojis, Status-Symbole etc. - Die Sticker-Grafiken sollten idealerweise als **SVG** oder als **PNG mit transparentem Hintergrund** vorbereitet sein. **Tipp**: Erstell dir eine Ninox-Tabelle mit Stickern. Lade deine SVG oder PNG Datei in ein Bildfeld. Nutze die Ninox Funktion `loadFileAsBase64URL(Image)`in einem Funktionsfeld, um das base64 für deine Stickerliste im Drawing zu erhalten. Natürlich kannst du in deiner Stickerliste auch gleich Breite, Höhe, Name … definieren. ### 🔸 `shapes` Der `shapes`-Block definiert die Liste der **Zeichenobjekten**, die du im Widget hinterlegst. Jedes Objekt im Array entspricht einem gespeicherten Eintrag in deiner `tableId` und enthält die nötigen Informationen zur Darstellung, Position und Interaktion. Wichtig: Um Drawing korrekt nutzen zu können, ist es essentiell eine Tabelle Shapes aufzusetzen und darin mindestens ein Textfeld `helper_shapeDataValue` anzulegen. #### 🔧 Struktur je Shape: - `shapeId`: Die eindeutige ID des Shapes (meist die `Nr` des Datensatzes). - `movable`: Gibt an, ob das Objekt im Zeichenbereich verschoben werden darf (`true` oder `false`). `false` sperrt nur das Verschieben — Klick, Hover und `actions` funktionieren weiterhin. Im **Sticker-Modus**: Shapes mit `actions` fangen Klicks ab (z. B. Detail in Sidebar); reine Hintergrund-Shapes ohne `actions` lassen Pin-Platzierung durch. - `shapeDataValue`: Die gespeicherten Formdaten (z. B. Koordinaten, Größe, Farbe, Typ etc.). Mit `dataBag.key` kann hier der gesamte JSON-Topf stehen; das Widget nutzt dann nur den Key-Wert als Geometrie. Unterstützte `type`-Werte: `rect`, `circle`, `polyline`, `polygon` (geschlossene Fläche mit `points` und optional `fill`), `text` (`x`, `y`, `text`, optional `fontSize`, `fontWeight`, `fontFamily`; `fill` ist die Textfarbe), `image` und `sticker`. - `label` (optional): Dynamischer Text für Sticker mit `labelPlaceholder` im `stickerType`. - `placeholders` (optional): Objekt mit zusätzlichen SVG-Ersetzungen — jeder Key ersetzt `##key##` im Sticker-SVG (z. B. `statusColor`, `showCheck`). - `scalable` (optional): Bei Stickern standardmäßig `false` — für pixel-stabile Marker auf Plänen setzen. - `actions`: Optional – Liste von Aktionen, die bei **Klick** (nicht beim Draggen) ausgeführt werden. Gleiches Format wie bei anderen arcWidgets (`update`, `popup`, `openRecord`, …). Unterstützt plain `value` oder `dataBag`/`patch` für partielle Updates in einen State-Topf: ```javascript actions: [{ type: "update", recordId: stateRecordId, fieldId: stateField, dataBag: { base: helper_mainContentState, patch: { selectedShape: Nr } } }] ``` Typischer Use-Case: Shape-ID in einen gemeinsamen UI-State schreiben → Sidebar (`rightSideContent`) zeigt das Detail. Ausführlich: [`data-bag-actions`](../patterns/data-bag-actions.md). - `stickerUID`: Wenn du einen anderen Sticker, als den initial erstellten, anzeigen möchtest (z. B. weil sich ein Status geändert hat), musst du hier die UID des entsprechenden Stickers angeben. - `sidebarItem`: Optional – zeigt das Shape zusätzlich in der rechten Seitenleiste an, mit:

  • `header`: Titel (z. B. „Mangel #3“) - `content`: Beschreibung oder weitere Infos - `hide`: `true` oder `false` – blendet das Item in der Sidebar aus, ohne es zu löschen (z. B. für gefilterte Ansichten)

### Rezept: Wartungsplan mit Prüfpunkten Typisches Setup für Gebäudeinspektion, Anlagenwartung oder ähnliche Plan-basierte Marker — ein **nicht verschiebbarer Plan-Hintergrund** plus **verschiebbare Prüfpunkt-Pins** mit Persistenz in einer Kind-Tabelle. #### Bausteine 1. **Plan-Hintergrund** — ein Shape mit `type: "image"`, volle Artboard-Größe, `movable: false`, `hidden: true` (nur visuell, nicht in Sidebar). Bild aus Upload via `loadFileAsBase64URL(PlanBild)`. 2. **Prüfpunkt-Shapes** — aus `(select Pruefpunkte where Objekt = obj.Nr)` mit `movable`, `placeholders`, `actions` (Selection in State schreiben). 3. **`dataBag: { key: "geo" }`** am Drawing — Geometrie in JSON-Topf, Metadaten (`placedAt`, `inspectionPointId`, …) bleiben erhalten. 4. **`canvas` / `artboard`** — aus Upload-Metadaten (Breite/Höhe des Plans). 5. **`toolbarSettings`** — nur Move + die benötigten Sticker inline (siehe Beispiel oben). 6. **Verschieben sperren** — Boolean im State (`lockShapeMove`), pro Shape `movable: if lockShapeMove then false else true end`. Toggle per Button mit `dataBag`-Patch. Siehe State-Management-Rule (`07-state-management.mdc`). #### Skizze (Ninox) ```javascript let lockShapeMove := if item(stateParsed, "lockShapeMove") = false then false else true end; let planBg := [{ shapeId: "plan-background", movable: false, shapeDataValue: { type: "image", href: loadFileAsBase64URL(obj.PlanBild), x: 0, y: 0, width: planWidth, height: planHeight } }]; let pinShapes := (select Pruefpunkte where number(Objekt) = number(obj.Nr)).[{ shapeId: text(Nr), movable: if lockShapeMove then false else true end, scalable: false, label: text(Positionsnummer), placeholders: { statusColor: statusFarbe, showCheck: "block", showWarn: "none", showError: "none" }, shapeDataValue: helper_shapeDataValue, actions: [{ type: "update", recordId: Nr, fieldId: stateField, dataBag: { base: helper_mainContentState, patch: { selectedShape: text(Nr) } } }] }]; arcCustomDrawing({ dataBag: { key: "geo" }, tableId: "Pruefpunkte", fieldId: fieldId(Nr, "helper_shapeDataValue"), shapes: array(planBg, pinShapes), toolbarSettings: { top: ["move", { sticker: "pin_ok" }, { sticker: "pin_warn" }], bottom: [] }, rightSideContent: { show: if isMobile then false else true end } }) ``` **Upload:** Zwei After-change-Trigger (Web = Server, App = Browser) auf dem Upload-Feld — `importFile` → Zieldatei + JSON-Metadaten (Breite/Höhe) → Trigger-Felder leeren. **Sidebar auf Mobile:** `rightSideContent.show: false` auf Mobile; stattdessen Bottom-Sheet per Layout-Block mit `actions` + `dataBag`-Patch (siehe UI-Architecture-Rule `06-ui-architecture.mdc`). ### 🔸 `rightSideContent` Der Block `rightSideContent` steuert, **ob eine rechte Seitenleiste im Widget angezeigt wird** und was dort dargestellt werden soll. Diese Sidebar ist besonders nützlich, um eine Liste der gezeichneten Elemente anzuzeigen oder zusätzliche Informationen bereitzustellen. #### 🔧 Struktur: - `show`: `true` oder `false` – aktiviert oder deaktiviert die rechte Sidebar. - `header`: (optional) Widgets oder Textinhalt für den oberen Bereich der Sidebar. - `footer`: (optional) Widgets oder Textinhalt für den unteren Bereich der Sidebar. - `content`: (optional) Individuelle Konfiguration des mittleren Inhalts. Wenn `show: true` gesetzt ist und die Shapes `sidebarItem`-Einträge enthalten (siehe `shapes`), wird automatisch eine **interaktive Liste aller Shapes** angezeigt – mit Titel, Beschreibung, Hover-Effekt und Auswahlverhalten.
🔍 `content`-Block im Detail:
- `showShapeLayers`: Wenn `true`, wird eine automatische Liste aller vorhandenen Shapes (mit `sidebarItem`) angezeigt – inklusive Titel, Beschreibung, Hover- & Klickverhalten. Ideal z. B. für Mängellisten, Prüfpunkte oder Annotationen. - `customLayout`: Optionaler benutzerdefinierter Inhalt, der im mittleren Bereich angezeigt werden kann. Hier kannst du z. B. HTML, Ninox-Formelergebnisse oder vorbereitete Layout-Komponenten einbinden. Wenn `showShapeLayers` aktiv ist, wird die Shape-Liste angezeigt. Wenn du stattdessen ein eigenes Layout anzeigen willst, kannst du `customLayout` nutzen. 💡 **Hinweise:** - Die Sidebar funktioniert besonders gut, wenn du Shapes mit `sidebarItem`-Infos versiehst. - Der Bereich kann je nach Use Case rein informativ, interaktiv oder visuell angepasst werden. - Du kannst `customLayout` auch dynamisch aus einem Ninox-Feld übernehmen – z. B. über eine Textformel oder HTML-Komponente. - Wenn du beides kombinieren möchtest (Layer-Liste + eigene Inhalte), kannst du `customLayout` über dem Footer einfügen und `showShapeLayers` auf `true` lassen. ### 🔧 Kein Entwickler? Kein Problem. Du hast keine Zeit, dich mit Code und Konfiguration zu beschäftigen? Wir vom **arcRider-Team** bieten dir einen **Installations- und Einrichtungssupport** für das `arcCustomDrawing`-Widget an – maßgeschneidert auf deinen Anwendungsfall. ➡️ **Schreib uns einfach an **[**office@arc-rider.com**](mailto:office@arc-rider.com). Wir helfen dir, in wenigen Stunden loszulegen – ohne technisches Kopfzerbrechen. # Custom Drawing ## AI Defaults (read first) - **uniqueId**: Required, `"drawing-" + Nr`. - **tableId**: Required – the shapes table (`tableId("Shapes")`). - **fieldId**: The text field storing shape data (JSON), use `fieldId(Nr, "Feldname")`. - **height**: Typically `"800px"` or `"100%"`. - **canvas/artboard**: `canvas: { width: 4000, height: 4000 }`, `artboard: { width: 1000, height: 1000 }`. - **shapes**: Each shape needs `shapeId: Nr` and `shapeDataValue` (from the shapes table text field). - **embedded**: Default `false`, set `true` when nested. - **Persistence**: Core emits `shape_create` / `shape_update` / `shape_activate` / `export`. Ninox handlers build Action Performer `create`/`update` and run shape `actions`. React: `onShapeCreate`, `onShapeUpdate`, `onShapeActivate`, `onExport` (or `onAction`). # Custom Drawing Das **Custom Drawing Widget** ermöglicht es, beliebige Bilder (z. B. Grundrisse, Produktfotos, Körper-Silhouetten) hochzuladen und mit interaktiven **Pins, Sticker oder Freihandzeichnungen** zu versehen. Jede Markierung kann mit einer **Notiz**, **Kategorie** oder **zusätzlichen Information** verknüpft werden. Ideal für Anwendungen in Bau, Mode, Medizin, Logistik u. v. m. ## Anwendungscode ```javascript arcCustomDrawing({ uniqueId: "titel " + Nr, embedded: false, height: "800px", tableId: tableId("Shapes"), fieldId: fieldId("Shapes", "helper_shapeDataValue"), changeFieldValues: [{ fieldId: fieldId("Tabelle", "Feld Name"), value: "Wert" }], exportSettings: { allowedTypes: [{ format: "jpg" }, { format: "png" }, { format: "svg" }], target: "file", recordId: Nr, fieldId: fieldId("Tabelle", "Feld für den Export"), }, canvas: { width: 4000, height: 4000, offsetX: 0, offsetY: 0 }, zooming: { step: 0.25, min: 0.01, max: 4 }, drawingSettings: { strokeWidth: 1, strokeColor: "#ffffff" }, artboard: { width: 1000, height: 1000, }, stickerTypes:[{ uid: uid, image: "stickerBase64", icon: base64, label: Name, width: 100, height: 100, originX: 0.5, originY: 0.89, default: false }], shapes: (Shapes).[{ shapeId: Nr, movable: true, shapeDataValue: helper_shapeDataValue, stickerUID: "", sidebarItem: { header: "Titel des Eintrags", content: "Beschreibung des Eintrags", } }], rightSideContent: { show: true, header: "Header", footer: "Footer" } }) ``` ### 🔸 `embedded` Gibt an, ob das Widget sich in eine bestehende Maske (z. B. Ninox-Formular) einfügt. ```javascript embedded: "" // default: false embedded: true, ``` 👀 **Tipp:** Bei `true` ist keine feste Höhe nötig – das Widget passt sich automatisch dem Elterncontainer an. ### 🔸 `height` Mit dem Parameter `height` gibst du die **Gesamthöhe** des Drawing-Tools in Pixel an. Er bestimmt, wie viel vertikaler Platz das Widget im eingebetteten Kontext (z. B. in einem Formular oder auf einer Seite) einnimmt. **Empfehlung: **Das Widget sollte für eine bessere UI idealer Weise in Custom Layout eingebunden werden. Und die Höhe im Layout Block bestimmt werden. ### 🔸 `tableId` & `fieldId` Diese beiden Parameter definieren, **wo die Formdaten (Shapes)** gespeichert oder geladen werden. - `**tableId**` gibt die Tabelle an, in der die Shapes verwaltet werden. - `**fieldId**` verweist auf das konkrete Feld innerhalb dieser Tabelle, das die Geometriedaten (`shapeDataValue`) des jeweiligen Shapes enthält. Achtung: Hier muss ein Ninox-Textfeld verwendet werden. Du kannst hiermit eigene Formen, Marker oder Objekte dynamisch speichern – z. B. Rechtecke, Sticker, Kreise, Bilder oder Linien. ### 🔸 `dataBag` Optional. Steuert, wie Shape-Daten in einem **JSON-Topf** (ein Textfeld mit mehreren Keys) gelesen und geschrieben werden – ohne andere Keys zu überschreiben. ```javascript dataBag: { key: "pin" // Shape liest/schreibt nur diesen Key im Topf // encoding: "raw" // Default — Plain-JSON; nur bei Legacy: "urlEncoded" } ``` **Lesen:** Das Feld `shapeDataValue` pro Shape enthält den gesamten Topf als Plain-JSON. Das Widget extrahiert `key` als Geometrie. In Formeln: `parseJSON(helper_data)` — ohne `urlDecode`. **Schreiben:** Beim Verschieben wird nur `key` aktualisiert; andere Keys (`placedAt`, `meta`, …) bleiben erhalten. **Beispiel: Wartungsplan mit Prüfpunkten** (z. B. Gebäudeinspektion): ```json { "geo": { "type": "sticker", "stickerUID": "pin_ok", "x": 420, "y": 180, "width": 40, "height": 40 }, "placedAt": "2026-06-13", "inspectionPointId": "123" } ``` Ohne `dataBag` bleibt das bisherige Verhalten: das gesamte Feld ist `shapeDataValue` (raw JSON). > **Hinweis:** Das ist die **Widget-Config** (`key` + `encoding`). Für **Actions** (`update` mit `patch` in State-Bags) gilt dasselbe `dataBag`-Konzept plattformweit — siehe [`data-bag-actions`](../patterns/data-bag-actions.md). ### 🔸 `changeFieldValues` Erlaubt es, beim **Anlegen eines neuen Shapes** zusätzliche Felder oder Verknüpfungen zu befüllen – z. B. Kategorie, Ersteller o. ä. Pro Eintrag: `{ fieldId, value }` — `fieldId` ist die Ninox-Feld-ID (z. B. via `fieldId("Tabelle", "Feldname")`). **Sticker-spezifisch:** Zusätzlich kann jedes Element in `stickerTypes` ein eigenes `changeFieldValues`-Array haben. Beim Platzieren eines Stickers werden zuerst die **globalen** `changeFieldValues` am Drawing gesetzt, danach die des **aktiven Sticker-Typs** — bei gleichem `fieldId` gewinnt der Sticker. ```javascript changeFieldValues: [{ fieldId: fieldId("Pruefpunkte", "Objekt zuordnen"), value: number(obj.Nr) }], stickerTypes: [{ uid: "pin_ok", changeFieldValues: [{ fieldId: fieldId("Pruefpunkte", "Kategorie"), value: "OK" }] }] ``` **Laufende Nummern:** Werte wie Positionsnummern oder fortlaufende IDs **nicht** über `changeFieldValues` setzen — der Wert stammt vom letzten Render und ist bei mehreren schnellen Creates falsch. Stattdessen **After-change-Trigger** auf dem Parent-Link-Feld; im Trigger **`myNr := number(Nr)` vor** dem Filter `number(Nr) != myNr`, sonst filtert man sich selbst weg. ### 🔸 `create` Steuert das **Anlegen neuer Shapes** (Pins, Linien, Rechtecke, Kreise, Polygone, Text, Bilder) und bündelt optional die Create-Konfiguration. Empfohlen ab dieser Version; die Root-Props `tableId`, `fieldId`, `changeFieldValues` und `setNewRecordId` bleiben **weiterhin gültig** (Legacy). ```javascript create: { enabled: if lockCreate then false else true end, tableId: tableId("Shapes"), fieldId: fieldId("Shapes", "helper_shapeDataValue"), changeFieldValues: [{ fieldId: fieldId("Pruefpunkte", "Objekt"), value: number(obj.Nr) }], setNewRecordId: [{ recordId: Nr, fieldId: fieldId("Objekt", "Letzter Shape") }] } ``` | Feld | Beschreibung | |------|--------------| | `enabled` | `false` = kein neues Shape (Toolbar ausgegraut, Pan/Klick/`actions` bleiben). Default: `true`. | | `tableId` | Zieltabelle für neue Shapes (überschreibt Root-`tableId`, falls gesetzt). | | `fieldId` | Textfeld für Geometrie am **neuen** Datensatz (überschreibt Root-`fieldId`). | | `changeFieldValues` | Zusatzfelder beim Create (überschreibt Root-`changeFieldValues`). | | `setNewRecordId` | Nach Create: neuen Datensatz in Parent verlinken (`[{ recordId, fieldId }]`). | **Hinweise:** - `dataBag` bleibt am Widget-Root (Lesen + Schreiben bestehender Shapes). - `stickerTypes[].changeFieldValues` bleibt sticker-spezifisch und ergänzt die globalen Werte. - Nur `create.enabled: false` reicht zum Sperren — `tableId`/`fieldId` am Root können für bestehende Shapes weiter gesetzt bleiben. ### 🔸 `setNewRecordId` (Legacy am Root) Optional am Widget-Root oder innerhalb von `create`. Nach dem Anlegen eines Shapes wird der neue Datensatz in ein Referenzfeld geschrieben — z. B. um den Parent-Datensatz zu verknüpfen. ```javascript setNewRecordId: [{ recordId: Nr, fieldId: fieldId("Objekt", "Letzter Shape") }] ``` ### 🔸 `exportSettings` Mit diesem Block konfigurierst du die **Exportfunktion des Widgets**. Du kannst festlegen, in welchen Formaten der Nutzer exportieren darf (z. B. PNG) und wohin die Datei gespeichert werden soll.
Feld Beschreibung
`allowedTypes` Liste der erlaubten Exportformate. Du kannst `"jpg"`, `"png"` und `"svg"` angeben.
`target` Legt fest, wohin die Datei exportiert werden soll. `"file"` bedeutet, die Datei wird heruntergeladen. `"field"` bedeutet, die Datei wird in ein Datei-Feld in Ninox hochgeladen.
`recordId` Gibt den Datensatz an, in den exportiert werden soll (nur bei `target: "field"` erforderlich).
`fieldId` Definiert das genaue Feld im Datensatz, in das die exportierte Datei geschrieben wird.
#### 🔄 Auto-Export Mit `autoExport` kannst du festlegen, dass nach jeder Shape-Änderung automatisch ein Export erfolgt – z. B. um ein Live-Vorschaubild zu speichern. Die Properties `autoExport`, `autoExportDelay`, `target`, `recordId` und `fieldId` können sowohl global als auch **pro Format** in `allowedTypes` gesetzt werden. Format-spezifische Einstellungen überschreiben die globalen. **Beispiel mit Auto-Export:** ```javascript exportSettings: { allowedTypes: [{ format: "jpg", target: "field", recordId: Nr, fieldId: fieldId("Tabelle", "Vorschaubild"), autoExport: true, autoExportDelay: 500 // Verzögerung in ms (default: 500) }, { format: \"png\", target: "file", // Manueller Download autoExport: false }] } ``` | Feld | Beschreibung | |------|--------------| | `autoExport` | `true` oder `false` – aktiviert automatischen Export nach jeder Shape-Änderung | | `autoExportDelay` | Verzögerung in Millisekunden bevor der Export startet (default: 500). Dient als Debounce bei schnellen Änderungen. | ### 🔸 `canvas` Mit dem `canvas`-Block definierst du die **technische Zeichenfläche**, also den kompletten Bereich, auf dem Shapes gezeichnet und verschoben werden können. Dieser Bereich kann deutlich größer sein als der sichtbare Ausschnitt (`artboard`) und bestimmt, wie weit sich Nutzer im Zeichenbereich bewegen können (z. B. beim Verschieben oder Zoomen). #### 🔧 Struktur: - `width`: Die gesamte Breite der Zeichenfläche in Pixeln. - `height`: Die gesamte Höhe der Zeichenfläche in Pixeln. - `offsetX`: Abstand zum linken und rechten Rand – kann genutzt werden, um Inhalte zentriert darzustellen. - `offsetY`: Abstand zum oberen und unteren Rand – ideal für zusätzliche Arbeitsfläche oder Druckränder. Tipp: Du kannst durchaus auch dynamische Werte aus Ninox Feldern oderBerechnungen hier einsetzen. Beispielsweise haben wir Workflows entwickelt, in denen wir die Größe des canvas abhängig von der Größe des Hintergrundshapes gemacht haben. ### 🔸 `zooming` Der `zooming`-Block steuert, **wie weit in das Zeichenfeld hinein- oder herausgezoomt werden kann** – etwa per Mausrad oder Touch-Gesten. Damit definierst du den Zoombereich und wie fein abgestuft gezoomt wird. #### 🔧 Struktur: - `step`: Gibt an, wie groß ein Zoom-Schritt ist, z. B. bei Klick auf die Plus/Minus-Buttons oder beim Scrollen mit gedrückter Strg-Taste. Ein kleiner Wert (z. B. `0.1`) bedeutet feinere Zoomschritte. - `min`: Der **kleinste erlaubte Zoomfaktor** (z. B. `0.1` = 10 %). Damit legst du fest, wie weit man maximal herauszoomen darf. - `max`: Der **größte erlaubte Zoomfaktor** (z. B. `4` = 400 %). Damit begrenzt du das Hereinzoomen auf eine bestimmte Detailstufe. 💡 **Hinweise:** - Der initiale Zoom wird automatisch so gesetzt, dass das Artboard gut in das Widget passt. - Zoomaktionen wirken sich auf die Darstellung der Shapes aus, nicht auf ihre tatsächliche Größe im `canvas`. - Wird kein `zooming`-Block gesetzt, gelten Standardwerte (Zoom zwischen ca. 10 % und 400 %). ### 🔸 `drawingSettings` Mit dem `drawingSettings`-Block legst du die **Standard-Eigenschaften für neue Zeichenobjekte** fest – z. B. Linienfarbe und -stärke. Diese Werte gelten für alle neu gezeichneten Formen wie Linien, Rechtecke, Kreise, Polygone oder Text, solange der Nutzer keine individuellen Einstellungen verändert. #### 🔧 Struktur: - `strokeWidth`: Bestimmt die Linienbreite in Pixeln. Ein Wert von `1` ergibt eine feine Linie, `2` oder `3` wirken etwas kräftiger. - `strokeColor`: Definiert die Farbe der Linien im Hex-Format, z. B. `#ffffff` für Weiß oder `#ffaa21` für Orange. - `polygon.fillColor`: Füllfarbe neuer Polygone, z. B. `rgba(0, 120, 255, 0.2)`. Ohne Angabe haben Polygone keine Füllung. - `text.color`: Textfarbe für neuen Text (Standard `black`). - `text.fontSize`: Schriftgröße neuer Texte in Canvas-Pixeln (Standard `16`). - `text.fontWeight`: Schriftstärke, z. B. `bold` oder `600` (Standard `normal`). - `text.fontFamily`: CSS-Schriftfamilie (Standard `Arial, Helvetica, sans-serif`). 💡 **Hinweis:** Du kannst `strokeColor` und `strokeWidth` auch **aus Ninox-Feldern dynamisch übergeben**, um die Zeichenoptionen z. B. **vom Nutzer auswählbar zu machen**. So kannst du pro Datensatz individuelle Zeichenstile ermöglichen. ### 🔸 `toolbarSettings` Optional. Steuert **Reihenfolge, Sichtbarkeit und Gruppierung** der Toolbar-Buttons. Ohne `toolbarSettings` bleibt das Standard-Layout unverändert (Move, Zeichenwerkzeuge, Stickers-Untermenü, Export). #### Struktur - `top`: Buttons in der oberen Toolbar-Leiste (von oben auf Desktop / links auf Mobile). - `bottom`: Buttons in der unteren Leiste (Export etc.). #### Einträge in `top` / `bottom` | Form | Bedeutung | |------|-----------| | `"move"` | Tool mit Standard-Icon (Kurzform) | | `{ tool: "draw", icon: "..." }` | Tool mit eigenem Icon (URL, Base64 oder SVG) | | `"stickers"` | Alle `stickerTypes` **direkt** in der Toolbar (Ebene 1) | | `{ sticker: "pin_rot" }` | Nur dieser Sticker direkt in der Toolbar | | `{ group: "zeichnen", icon: "...", items: ["draw", "rect", "circle"] }` | Gruppe mit Unterpanel (wie bisher bei Stickers/Export) | #### Polygon- und Text-Werkzeug Beide Werkzeuge sind nicht in der Standard-Toolbar. Mit `"polygon"` und `"text"` in `toolbarSettings` blendest du sie ein. - `"polygon"`: Jede Ecke per Klick setzen. Beenden mit Klick auf die erste Ecke (ab drei Ecken), Doppelklick oder Enter; Esc bricht ab, Rücktaste entfernt die letzte Ecke. - `"text"`: Dorthin klicken, wo der Text beginnen soll, tippen und mit Enter bestätigen (Umschalt+Enter = neue Zeile). Esc bricht ab. Doppelklick auf einen vorhandenen Text bearbeitet ihn. **Weglassen = nicht anzeigen.** Es gibt kein separates `enabled`-Flag — was nicht im Layout steht, erscheint nicht. #### Beispiel: Prüfpunkt-Pins, nur Move + ausgewählte Sticker ```javascript toolbarSettings: { top: [ "move", { sticker: "pin_rot" }, { sticker: "pin_gruen" } ], bottom: ["export"] } ``` #### Beispiel: Zeichenwerkzeuge gruppieren ```javascript toolbarSettings: { top: [ "move", { group: "zeichnen", label: "Zeichnen", icon: "data:image/svg+xml,...", items: ["draw", "rect", "circle"] }, "image", "stickers" ], bottom: ["export"] } ``` ### 🔸 `artboard` Der `artboard`-Block definiert den **sichtbaren Ausschnitt deiner Zeichenfläche** – also das, was der Nutzer beim Öffnen des Widgets zunächst sieht. Es handelt sich sozusagen um das „Papier“, auf dem gezeichnet wird – während der `canvas`-Bereich das „gesamte Atelier“ darstellt. #### 🔧 Struktur: - `width`: Breite des sichtbaren Zeichenbereichs in Pixeln. - `height`: Höhe des sichtbaren Zeichenbereichs in Pixeln. Dieser Bereich wird automatisch zentriert und skaliert dargestellt, sodass er vollständig im Widget sichtbar ist (abhängig von Zoom- und Darstellungsparametern). 💡 **Hinweise:** - Das `artboard` ist rein visuell – alle Shapes können sich auch **außerhalb dieses Bereichs** befinden, solange sie innerhalb des `canvas` liegen. - Wenn du ein bestimmtes Seitenverhältnis möchtest (z. B. A4 oder Quadrat), kannst du die `width` und `height` entsprechend setzen. - In Kombination mit `zooming` und `canvas` kannst du gezielt steuern, wie viel Platz Nutzer:innen für ihre Zeichnung haben – und was beim Start sofort sichtbar ist. Tipp: Du kannst durchaus auch dynamische Werte aus Ninox Feldern oderBerechnungen hier einsetzen. Beispielsweise haben wir Workflows entwickelt, in denen wir die Größe des artboards abhängig von der Größe des Hintergrundshapes gemacht haben. Dadurch scrollt der User nicht so viel hin und her. ### 🔸 `stickerTypes` Der `stickerTypes`-Block erlaubt es dir, **individuelle Sticker** bereitzustellen, die Nutzer im Zeichenbereich platzieren können – z. B. Icons, Symbole, Marker oder Piktogramme. Jeder Sticker ist ein Objekt mit Eigenschaften wie Bildquelle, Größe und Positionierungsverhalten. Ideal für Use Cases wie: - 🔧 Technische Markierungen auf Fotos oder Plänen - 📍 Standortpins auf Karten oder Grundrissen - 👕 Qualitätskontrollen in der Modebranche - 🏥 Medizinische Annotationen auf Körpersilhouetten #### 🔧 Struktur je Sticker: - `uid`: Eindeutige ID des Stickers (z. B. `"pin_rot"`). - `image`: Base64-Daten-URL oder URL zur Grafik — wird beim Platzieren auf dem Canvas verwendet. - `icon` (optional): Vorschaubild **nur** für den Toolbar-Button (wenn abweichend vom Stickerbild). Erlaubt sind: - Bild (SVG-Markup, PNG/JPG/Data-URL, HTTP-URL) — wie `image` - `arcCustomIcon`-Widget-Objekt: `{ widget: "arcCustomIcon", data: { iconSet: "phosphor", name: "map-pin", color: "#1565c0" } }` - `label`: Bezeichnung des Stickers. Zwei Formen möglich: - **String** (Legacy): nur Tooltip beim Hovern über dem Toolbar-Button, kein sichtbarer Text. - **Objekt** (neu): sichtbarer Text unter dem Icon im Toolbar-Button — `{ value: "K", visible: true, fontColor: "#1565c0", fontSize: 10, maxWidth: 50 }`. `visible: false` verhält sich wie der String-Fall (nur Tooltip). Zu langer Text wird bei `maxWidth` mit `…` abgeschnitten. - `width`: Breite des Stickers in Pixeln. - `height`: Höhe des Stickers in Pixeln. - `originX`: Horizontaler Ankerpunkt (0 = links, 0.5 = Mitte, 1 = rechts). - `originY`: Vertikaler Ankerpunkt (0 = oben, 0.5 = Mitte, 1 = unten). - `default` (optional): Wenn `true`, wird dieser Sticker vorausgewählt. - `labelPlaceholder` (optional): Platzhalter-String im SVG (`image`), der durch `label` am Shape ersetzt wird — Konvention: `##LABEL##` (wie bei anderen arcWidgets: `##key##`). - `defaultLabel` (optional): Fallback für `label` direkt beim Platzieren, bis Ninox die Formel neu liefert. - `defaultPlaceholders` (optional): Fallback-Objekt für `shape.placeholders` beim Platzieren (gleiche Keys wie im SVG). - `changeFieldValues` (optional): Zusätzliche Felder beim **Anlegen** eines Datensatzes für diesen Sticker-Typ — merged mit globalen `changeFieldValues` (Sticker überschreibt bei gleichem `fieldId`). #### Dynamische SVG-Platzhalter (Sticker + Shape) Für Marker mit Text, Status-Dot und Icons kann das **SVG-Template** Platzhalter enthalten. Das Widget ersetzt sie beim Rendern und im Export: | Quelle | Ersetzung | |--------|-----------| | `stickerType.labelPlaceholder` + `shape.label` | z. B. `##LABEL##` → `"K15"` | | `shape.placeholders` (Objekt) | Jeder Key `foo` ersetzt `##foo##` im SVG | | `stickerType.defaultLabel` / `defaultPlaceholders` | Sofort beim Platzieren, bis Ninox reagiert | **Data-URL-Hinweis:** Wenn das SVG per `encodeURIComponent` in `data:image/svg+xml,…` eingebettet ist, ersetzt das Widget Platzhalter auch in URL-kodierter Form (z. B. `%23%23LABEL%23%23` → `K15`, `%23ff0000` für `#ff0000`). **Warum `##key##`?** Wie bei arcCustomInput (`##inputValue##`) und arcCustomCalendarWeek (`##clickedDate##`). Anders als `{{key}}` kollidiert `##key##` nicht mit der Ninox-Syntax `{variable}` in Text-Blöcken. Beispiel `shape.placeholders` für Status-Icons via `display` (alle Varianten im SVG, Ninox schaltet Sichtbarkeit): ```javascript placeholders: { statusColor: "#4caf50", showCheck: "block", showWarn: "none", showError: "none" } ``` Beispiel `defaultPlaceholders` am `stickerType` (Fallback bis Ninox reagiert): ```javascript defaultLabel: "", defaultPlaceholders: { statusColor: "#9e9e9e", showCheck: "none", showWarn: "none", showError: "none" } ``` Neue Platzhalter erfordern **keine Widget-Änderung** — nur `##key##` ins SVG und passenden Key in `shape.placeholders`. 💡 **Hinweise:** - Die Sticker erscheinen im linken Menü des Widgets unter „Stickers“. - Beim Platzieren eines Stickers wird seine Position automatisch anhand von `originX` / `originY` berechnet. So kannst du z. B. Marker exakt mit ihrer Spitze am Klickpunkt ausrichten. - Du kannst beliebig viele Sticker definieren – z. B. farbige Pins, Icons für Mängel, Emojis, Status-Symbole etc. - Die Sticker-Grafiken sollten idealerweise als **SVG** oder als **PNG mit transparentem Hintergrund** vorbereitet sein. **Tipp**: Erstell dir eine Ninox-Tabelle mit Stickern. Lade deine SVG oder PNG Datei in ein Bildfeld. Nutze die Ninox Funktion `loadFileAsBase64URL(Image)`in einem Funktionsfeld, um das base64 für deine Stickerliste im Drawing zu erhalten. Natürlich kannst du in deiner Stickerliste auch gleich Breite, Höhe, Name … definieren. ### 🔸 `shapes` Der `shapes`-Block definiert die Liste der **Zeichenobjekten**, die du im Widget hinterlegst. Jedes Objekt im Array entspricht einem gespeicherten Eintrag in deiner `tableId` und enthält die nötigen Informationen zur Darstellung, Position und Interaktion. Wichtig: Um Drawing korrekt nutzen zu können, ist es essentiell eine Tabelle Shapes aufzusetzen und darin mindestens ein Textfeld `helper_shapeDataValue` anzulegen. #### 🔧 Struktur je Shape: - `shapeId`: Die eindeutige ID des Shapes (meist die `Nr` des Datensatzes). - `movable`: Gibt an, ob das Objekt im Zeichenbereich verschoben werden darf (`true` oder `false`). `false` sperrt nur das Verschieben — Klick, Hover und `actions` funktionieren weiterhin. Im **Sticker-Modus**: Shapes mit `actions` fangen Klicks ab (z. B. Detail in Sidebar); reine Hintergrund-Shapes ohne `actions` lassen Pin-Platzierung durch. - `shapeDataValue`: Die gespeicherten Formdaten (z. B. Koordinaten, Größe, Farbe, Typ etc.). Mit `dataBag.key` kann hier der gesamte JSON-Topf stehen; das Widget nutzt dann nur den Key-Wert als Geometrie. Unterstützte `type`-Werte: `rect`, `circle`, `polyline`, `polygon` (geschlossene Fläche mit `points` und optional `fill`), `text` (`x`, `y`, `text`, optional `fontSize`, `fontWeight`, `fontFamily`; `fill` ist die Textfarbe), `image` und `sticker`. - `label` (optional): Dynamischer Text für Sticker mit `labelPlaceholder` im `stickerType`. - `placeholders` (optional): Objekt mit zusätzlichen SVG-Ersetzungen — jeder Key ersetzt `##key##` im Sticker-SVG (z. B. `statusColor`, `showCheck`). - `scalable` (optional): Bei Stickern standardmäßig `false` — für pixel-stabile Marker auf Plänen setzen. - `actions`: Optional – Liste von Aktionen, die bei **Klick** (nicht beim Draggen) ausgeführt werden. Gleiches Format wie bei anderen arcWidgets (`update`, `popup`, `openRecord`, …). Unterstützt plain `value` oder `dataBag`/`patch` für partielle Updates in einen State-Topf: ```javascript actions: [{ type: "update", recordId: stateRecordId, fieldId: stateField, dataBag: { base: helper_mainContentState, patch: { selectedShape: Nr } } }] ``` Typischer Use-Case: Shape-ID in einen gemeinsamen UI-State schreiben → Sidebar (`rightSideContent`) zeigt das Detail. Ausführlich: [`data-bag-actions`](../patterns/data-bag-actions.md). - `stickerUID`: Wenn du einen anderen Sticker, als den initial erstellten, anzeigen möchtest (z. B. weil sich ein Status geändert hat), musst du hier die UID des entsprechenden Stickers angeben. - `sidebarItem`: Optional – zeigt das Shape zusätzlich in der rechten Seitenleiste an, mit:

  • `header`: Titel (z. B. „Mangel #3“) - `content`: Beschreibung oder weitere Infos - `hide`: `true` oder `false` – blendet das Item in der Sidebar aus, ohne es zu löschen (z. B. für gefilterte Ansichten)

### Rezept: Wartungsplan mit Prüfpunkten Typisches Setup für Gebäudeinspektion, Anlagenwartung oder ähnliche Plan-basierte Marker — ein **nicht verschiebbarer Plan-Hintergrund** plus **verschiebbare Prüfpunkt-Pins** mit Persistenz in einer Kind-Tabelle. #### Bausteine 1. **Plan-Hintergrund** — ein Shape mit `type: "image"`, volle Artboard-Größe, `movable: false`, `hidden: true` (nur visuell, nicht in Sidebar). Bild aus Upload via `loadFileAsBase64URL(PlanBild)`. 2. **Prüfpunkt-Shapes** — aus `(select Pruefpunkte where Objekt = obj.Nr)` mit `movable`, `placeholders`, `actions` (Selection in State schreiben). 3. **`dataBag: { key: "geo" }`** am Drawing — Geometrie in JSON-Topf, Metadaten (`placedAt`, `inspectionPointId`, …) bleiben erhalten. 4. **`canvas` / `artboard`** — aus Upload-Metadaten (Breite/Höhe des Plans). 5. **`toolbarSettings`** — nur Move + die benötigten Sticker inline (siehe Beispiel oben). 6. **Verschieben sperren** — Boolean im State (`lockShapeMove`), pro Shape `movable: if lockShapeMove then false else true end`. Toggle per Button mit `dataBag`-Patch. Siehe State-Management-Rule (`07-state-management.mdc`). ### 🔸 `rightSideContent` Der Block `rightSideContent` steuert, **ob eine rechte Seitenleiste im Widget angezeigt wird** und was dort dargestellt werden soll. Diese Sidebar ist besonders nützlich, um eine Liste der gezeichneten Elemente anzuzeigen oder zusätzliche Informationen bereitzustellen. #### 🔧 Struktur: - `show`: `true` oder `false` – aktiviert oder deaktiviert die rechte Sidebar. - `header`: (optional) Widgets oder Textinhalt für den oberen Bereich der Sidebar. - `footer`: (optional) Widgets oder Textinhalt für den unteren Bereich der Sidebar. - `content`: (optional) Individuelle Konfiguration des mittleren Inhalts. Wenn `show: true` gesetzt ist und die Shapes `sidebarItem`-Einträge enthalten (siehe `shapes`), wird automatisch eine **interaktive Liste aller Shapes** angezeigt – mit Titel, Beschreibung, Hover-Effekt und Auswahlverhalten.
🔍 `content`-Block im Detail:
- `showShapeLayers`: Wenn `true`, wird eine automatische Liste aller vorhandenen Shapes (mit `sidebarItem`) angezeigt – inklusive Titel, Beschreibung, Hover- & Klickverhalten. Ideal z. B. für Mängellisten, Prüfpunkte oder Annotationen. - `customLayout`: Optionaler benutzerdefinierter Inhalt, der im mittleren Bereich angezeigt werden kann. Hier kannst du z. B. HTML, Ninox-Formelergebnisse oder vorbereitete Layout-Komponenten einbinden. Wenn `showShapeLayers` aktiv ist, wird die Shape-Liste angezeigt. Wenn du stattdessen ein eigenes Layout anzeigen willst, kannst du `customLayout` nutzen. 💡 **Hinweise:** - Die Sidebar funktioniert besonders gut, wenn du Shapes mit `sidebarItem`-Infos versiehst. - Der Bereich kann je nach Use Case rein informativ, interaktiv oder visuell angepasst werden. - Du kannst `customLayout` auch dynamisch aus einem Ninox-Feld übernehmen – z. B. über eine Textformel oder HTML-Komponente. - Wenn du beides kombinieren möchtest (Layer-Liste + eigene Inhalte), kannst du `customLayout` über dem Footer einfügen und `showShapeLayers` auf `true` lassen. ### 🔧 Kein Entwickler? Kein Problem. Du hast keine Zeit, dich mit Code und Konfiguration zu beschäftigen? Wir vom **arcRider-Team** bieten dir einen **Installations- und Einrichtungssupport** für das `arcCustomDrawing`-Widget an – maßgeschneidert auf deinen Anwendungsfall. ➡️ **Schreib uns einfach an **[**office@arc-rider.com**](mailto:office@arc-rider.com). Wir helfen dir, in wenigen Stunden loszulegen – ohne technisches Kopfzerbrechen. --- # Custom Gantt Source: https://www.arc-rider.com/docs/widgets/gantt # Custom Gantt ## AI Defaults (read first) - **Function name**: `arcGantt` (NOT `arcCustomGantt`). - **uniqueId**: Required, `"gantt-" + Nr`. - **ganttSettings**: `height`, `embedded`, `showScrollbar` live here (NOT at root level). - **height**: `ganttSettings: { height: "100%" }` when embedded in dashboard. - **years**: Array of integers, e.g. `[2025, 2026]`. - **items**: Each item needs `recordId: Nr` and `timeEntries` array. - **timeEntries**: Each needs `recordId: Nr`, `startDate`, `endDate` (date field values). - **Hierarchy**: Nest `items` arrays inside parent items for tree structure. - **sidebarSettings**: `width: "400px"` for the left sidebar. # Custom Gantt ![](https://framerusercontent.com/images/dcMx5Gbm9HXvf5ETGtWzoMCLes.png) Das arcGantt Widget ist ein interaktives Tool zur grafischen Planung und Verwaltung von beispielsweise Projekten, Aufgaben und Zeiträumen. Es ermöglicht eine klare Balkendarstellung von Zeitverläufen, unterstützt hierarchische Strukturen und bietet eine dynamische Steuerung per Klick. Typische Einsatzbereiche: ✔ Projektmanagement – Aufgaben & Deadlines verwalten ✔ Ressourcenplanung – Verfügbarkeiten im Blick behalten ✔ Einsatzplanung – Teams & Termine effizient organisieren Das Widget lässt sich flexibel in deiner Ninox Datenbank integrieren und auf deinen Anwendungsfall anpassen. In den folgenden Abschnitten erfährst du, wie du das Widget konfigurierst. 🚀 ## Anwendungscode ```javascript let data := { uniqueId: "Projekt Gantt", ganttSettings: { height: "800px", embedded: false, showScrollbar: true, currentZoomLevel: 500, nativeCollapse: true, }, dateSettings: { highlights: [{ title: "Feiertag", date: date(2024, 1, 1), backgroundColor: "", color: "#fff" }, { title: "Heute", date: today(), backgroundColor: "#e9595c10", color: "#fff" }, { title: "Samstag", weekday: 6, backgroundColor: "", color: "#fff" }, { title: "Sonntag", weekday: 7, backgroundColor: "", color: "#fff" }] }, timeSettings: { amount: 1, startTime: 6, duration: 12 }, createModeSettings: { show: false, amount: 2 }, barSettings: { height: "40px", borderRadius: "10px" }, sidebarSettings: { width: "400px", title: "Einsatzplanung", clickActions: [{ title: "Heute", date: today(), position: "" }, { title: "Q1", date: date(2024, 1, 1), position: "" }, { title: "Q2", date: date(2024, 4, 1), position: "" }, { title: "Q3", date: date(2024, 7, 1), position: "" }, { title: "Q4", date: date(2024, 10, 1), position: "" }, { title: "Christmas", date: date(2024, 12, 24), position: "" }] }, years: [2025,2026,2027], items: (select Projekte where Aktiv=true).[{ recordId: Nr, title: Name, backgroundColor: "", height: "100px", barSettings: { height: "20px", borderRadius: "50px", borderColor: "", backgroundColor: "" }, clickAction:true, timeEntries: [{ title: Name, recordId: Nr, startDate: min(Aufgaben.Startdatum), endDate: max(Aufgaben.Enddatum), clickAction: true, barSettings: { height: "20px", borderRadius: "50px", borderColor: "", backgroundColor: "" }, }], items: Aufgaben.[{ recordId: Nr, title: "Aufgaben", backgroundColor: "", height: "100px", barSettings: { height: "20px", borderRadius: "4px", borderColor: "", backgroundColor: "" }, timeEntries: [{ recordId: Nr, title: Aufgabe.Bezeichnung, startDate: Startdatum, endDate: Enddatum, startTime: Startzeit, endTime: Endzeit, clickAction: true, barSettings: { height: "20px", borderRadius: "50px", borderColor: "", backgroundColor: "" }, }] }] }] }; arcCustomGantt(data) ``` ## Einzelne Parameter ### `**uniqueId**` `uniqueID` ist die individuelle Bezeichnung deines Gantts. Achte darauf, dass du hier einen einzigartigen Titel vergibst. Das ist wichtig, falls du mehrere Gantts auf einer Seite darstellen möchtest und sich keine Style-Einstellungen überschreiben. ### `**ganttSettings**` Block Der Block `ganttSettings` enthält allgemeine Anzeigeeinstellungen für das Gantt-Diagramm – also wie das Gantt selbst dargestellt wird, unabhängig von seinen Inhalten. #### `**height**` im `**ganttSettings**` Block `height` bestimmt die Höhe des Gantt-Diagramms in Pixeln. Beispiel: `"800px"` #### `**embedded**` im `**ganttSettings**` Block Mit dem Parameter `embedded` legst du fest, ob dein Gantt-Diagramm **eigenständig** angezeigt oder **in ein anderes Widget eingebettet** wird. #### `**showScrollbar**` im `**ganttSettings**` Block Mit `showScrollbar` bestimmst du, ob unterhalb des Gantt-Diagramms eine **horizontale Scrollbar** angezeigt wird. Das ist besonders hilfreich für User ohne Touchpad oder Maus mit horizontalem Scrollrad – sie können dann bequem per Klick auf der Scrollbar im Zeitraster nach rechts oder links navigieren. #### `**currentZoomLevel**` im `**ganttSettings**` Block ![](https://framerusercontent.com/images/Mif9IVD9lV8C9GqZzriUa1cHjPA.png) Mit `currentZoomLevel` legst du die **Zoomstufe** deines Gantt-Diagramms fest. Der Wert wird als **Prozentzahl** angegeben. - Ein höherer Wert (z. B. `500`) bedeutet eine stärkere Vergrößerung – die einzelnen Zeitblöcke im Grid werden größer dargestellt. - Ein niedrigerer Wert (z. B. `100`) zeigt mehr Zeitraum auf einmal, aber in kleinerem Maßstab. So kannst du steuern, wie detailliert oder wie übersichtlich dein Gantt beim Laden angezeigt wird. #### `**nativeCollapse**` im `**ganttSettings**` Block Mit `nativeCollapse` steuerst du, ob die **standardmäßigen Auf- und Zuklapp-Icons** für hierarchische Gantt-Einträge automatisch angezeigt werden. - `true` → Die Gantt-Komponente zeigt automatisch die bekannten Pfeil-Icons zum Ein- und Ausklappen von Gruppen oder Unterpunkten. - `false` → Du übernimmst selbst die Kontrolle über die Darstellung und Funktion – z. B. mit eigenen Icons und Klick-Logiken. Das ist besonders nützlich, wenn du ein individuelles Verhalten oder Design für die Aufklappfunktion brauchst. Wenn du `nativeCollapse` auf `false` setzt, kannst du eine **eigene Logik zum Ein- und Ausklappen** deiner Gantt-Einträge umsetzen. Dazu steuerst du die Anzeige der untergeordneten Items mithilfe einer `**if**`**-Bedingung**, die auf ein **Hilfsfeld** wie z. B. `showChildren` zugreift: Diese Methode ist nicht nur flexibel, sondern auch **performancefreundlich**: Unterelemente werden nur geladen und angezeigt, wenn sie tatsächlich sichtbar sein sollen – das reduziert Ladezeiten und sorgt für flüssigere Interaktionen im Gantt. ### `**dateSettings**` Block Mit dem `dateSettings`-Block kannst du **bestimmte Tage oder Daten im Gantt-Grid visuell hervorheben** – z. B. Feiertage, Projekt-Deadlines oder andere wichtige Zeitpunkte. Jedes Highlight kannst du individuell gestalten, z. B. mit eigener Hintergrundfarbe und Linienfabre der Zeilen. ### `**timeSettings**` Block Der `timeSettings` Block ist ein besonders geeignetes Feature, wenn du nicht 24 Stunden eines Tages darstellen möchtest, sondern nur eine bestimmte Stundenanzahl je Tag. Das kann sinnvoll sein, wenn du zum Beispiel Mitarbeitereinsätze einplanen möchtest und dir dabei nur die wirklichen Arbeitsstunden des Tages anzeigen lässt. Zum Beispiel 08:00 Uhr bis 18:00 Uhr. ### `**amount**` in `**timeSettings**` Mit dem Parameter `amount` legst du fest, in welche Zeiteinheiten ein Tag im Gantt-Diagramm unterteilt wird. Standardmäßig ist eine Stunde (also `1`) eingestellt – das bedeutet, jede Spalte im Gantt steht für eine Stunde. Wenn du feinere Einteilungen möchtest, kannst du zum Beispiel `0.5` angeben, um halbstündliche Intervalle anzuzeigen. Auch kleinere Werte wie `0.25` (15 Minuten) sind möglich – je nachdem, wie detailliert du deine Zeitplanung darstellen möchtest. 💡Hinweis: Achte darauf, dass du Dezimalzahlen mit einem Punkt, und nicht einem Komma trennst. ### `**startTime**` in `**timeSettings**` Mit dem Parameter `startTime` bestimmst du, ab welcher Uhrzeit der Tag im Gantt-Diagramm beginnen soll. Standardmäßig würde der gesamte 24-Stunden-Tag angezeigt. Wenn du jedoch nur einen bestimmten Zeitraum darstellen möchtest – zum Beispiel einen typischen Arbeitstag ab 8 Uhr – dann gibst du einfach `8` an. Das Gantt-Diagramm startet dann bei Stunde 8 (also 08:00 Uhr) und zeigt keine früheren Zeiten an. ### `**duration**` in `**timeSettings**` Mit dem Parameter `duration` legst du fest, wie viele Stunden im Gantt-Diagramm ab der `startTime` angezeigt werden. Das ist besonders hilfreich, wenn du nur einen bestimmten Zeitraum – z. B. einen klassischen Arbeitstag – abbilden möchtest. Wenn du zum Beispiel `startTime: 8` und `duration: 10` angibst, wird der Zeitraum von 08:00 Uhr bis 18:00 Uhr dargestellt. ### `**createModeSettings**` Block Mit dem `createMode`-Block legst du fest, ob User direkt im Gantt-Diagramm per Hover einen neuen Eintrag erstellen dürfen. Innerhalb dieses Blocks definierst du auch den Parameter `amount`, der bestimmt, wie viele Stunden ein neu erstellter Eintrag standardmäßig umfassen soll. Beispiel: Wenn `amount: 2` gesetzt ist, wird bei einem Klick auf das Gantt automatisch ein Termin mit einer Dauer von 2 Stunden erstellt. ### `**barSettings**` Block Die `barSettings` können auf jeder Ebene deiner hierarchischen Gantt-Items individuell festgelegt werden. Wenn eine untergeordnete Ebene (z. B. ein Task) keine eigenen Einstellungen hat, übernimmt sie automatisch die `barSettings` der übergeordneten Ebene. So musst du nicht jede Einstellung mehrfach definieren und behältst trotzdem maximale Flexibilität. ### `**sidebarSettings**` Block Die `sidebarSettings` bestimmen das Aussehen und Verhalten der linken Seitenleiste im Gantt-Diagramm. - Über den Parameter `title` legst du die Überschrift des Gantt-Diagramms fest. - Mit `customText` kannst du diesen Titel zusätzlich stylen – z. B. Schriftgröße, Farbe oder Formatierung. - `position` bestimmt die Position des Sichtfensters, das beim Springen zu einem spezifischen Datum angezeigt wird. Das Datum kann also linksbündig oder mittig im Sichtfenster angezeigt werden. Außerdem kannst du `clickActions` definieren: Damit bestimmst du, was passieren soll, wenn ein Nutzer auf einen Eintrag in der Sidebar klickt. Ein typisches Beispiel: Per Quicklink springt der Gantt-View automatisch zu einem bestimmten Datum, das du vordefiniert hast. ### `**years**` Mit dem Parameter `years` legst du fest, welche Jahreszahlen im Gantt-Diagramm angezeigt werden sollen. Du kannst entweder ein einzelnes Jahr oder eine ganze Spanne definieren – zum Beispiel: Es ist auch möglich, die Anzeige dynamisch zu steuern – etwa über ein Ninox-Feld. So kann der User per Auswahlfeld selbst bestimmen, welche Jahre im Gantt dargestellt werden sollen. ### `**items**` Abschnitt Der Abschnitt `items` definiert die konkreten Inhalte deines Gantt-Diagramms. Dabei legst du fest: - **Was in der linken Sidebar** (also den Zeilenüberschriften) angezeigt wird - **Und was im Content-Bereich des Gantts** (also dem Zeitraster) für jede Zeile dargestellt wird Typischerweise arbeitest du hier mit einer `**for**`**-Schleife**, um dynamisch auf Werte bzw. Datensätze aus einer Ninox-Tabelle zuzugreifen. So kannst du dein Gantt automatisch mit den relevanten Daten füllen – etwa so: ### `**items**` in der Sidebar Im oberen Bereich des `items`-Blocks definierst du, **wie die einzelnen Zeilen (Rows) in der linken Sidebar des Gantt-Diagramms aussehen sollen**. Du kannst hier z. B. Titel, Farben, Höhe, Bar-Styling und Klickverhalten festlegen. - `recordId`: Verknüpft das Sidebar-Item mit einem konkreten Datensatz. Wird unter anderem für die `clickAction` benötigt. - `title`: Der Text, der in der linken Spalte angezeigt wird – z. B. der Projektname. Hier kannst du auch ganz eigene Designs mithilfe anderer Widgets, wie Custom Layout, custom text oder Selects gestalten. - `clickAction`: Ermöglicht, dass ein Klick auf das Sidebar-Item zum zugehörigen Record führt. - `barSettings`: Hier kannst du das Aussehen des zugehörigen Zeitbalkens im Grid-Bereich steuern – unabhängig vom Stil der Sidebar. ### `**timeEntries**` – Zeiteinträge im Content-Bereich Im `timeEntries`-Block definierst du, **wie die einzelnen Zeiteinträge im Zeitraster (Content-Bereich) des Gantt-Diagramms dargestellt werden**. Ein `timeEntry` ist dabei **ein Balken auf der Zeitachse** – also ein Zeitraum mit Start- und Enddatum, der z. B. ein Projekt oder eine Aufgabe visualisiert. #### Wichtige Eigenschaften: - **Mehrere Zeiteinträge pro Zeile möglich** Du kannst beliebig viele `timeEntries` pro Item definieren – z. B. wenn ein Projekt mehrere Zeitabschnitte hat. - `**title**` Der Text, der im Balken angezeigt wird – kann auch durch Widgets ersetzt oder dynamisch gefüllt werden. - `**recordId**` Verlinkt den Eintrag mit einem Datensatz. In Verbindung mit `clickAction` wird so ein Klick auf den Balken zur Detailansicht weitergeleitet. - `**startDate**`** / **`**endDate**` Bestimmen die Position und Breite des Balkens auf der Zeitachse. - `**barSettings**` Für jedes `timeEntry` kannst du eigene Styles definieren – z. B. eine andere Farbe für unerledigte Aufgaben (`backgroundColor: "#ef1d2e"`). Bei Fragen zur Konfiguration oder bei individuellen Anwendungsfällen hilft dir das Entwicklerteam gerne weiter. Schau auch in die Beispiele oder hole dir Inspiration in der Discord Community. Happy Widgeting 🥳 --- # Custom Grid Source: https://www.arc-rider.com/docs/widgets/custom-grid # Grid in React Inhalte gleichzeitig in Zeilen und Spalten anordnen: Karten, KPI-Kacheln und Dashboards. Ohne Spaltenanzahl füllt das Grid die Zeile mit so vielen Spalten, wie die Breite zulässt, und passt sich so ohne Breakpoints an den Bildschirm an. Für eine Richtung (eine Zeile oder eine Spalte) und für Seiten-Rahmen nimmst du Arc Layout; das Grid nutzt du, wenn der Inhalt beide Dimensionen braucht. Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.7`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components). ## AI Defaults (read first) - Ohne `columns` füllt das Grid die Zeile mit Spalten von mindestens `columnWidth` (Standard `200px`). Mit `columns` legst du eine feste Anzahl fest. - `gap` ist der Abstand zwischen Zeilen und Spalten. Standard: `10px`. - `columnSpan` und `rowSpan` an einem Block machen breite oder hohe Kacheln. Bei festem `columns` wird `columnSpan` auf diese Anzahl begrenzt. - Das Grid ist standardmäßig `height: 'auto'` und so hoch wie sein Inhalt. Eine definierte `height` nur setzen, wenn es einen Rahmen ausfüllen soll. - Das gemeinsame Aussehen aller Kacheln einmal in `blockDefaults` festlegen und Umrandungen mit `border` zeichnen, nicht mit `styles`. - Widgets gehören in `blocks[].value`. Keine Wrapper-`div`-Elemente für das Grid selbst; die Spuren zeichnet das Grid. - Nur Blöcke mit `clickable: true` reagieren auf Klicks. Sie rufen `onBlockClick` mit ihrem Index auf. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetBadge, ArcWidgetGrid, ArcWidgetText, } from '@arcrider/arcwidgets-react'; export default function Example() { const [selected, setSelected] = useState('none'); const ids = ['revenue', 'orders', 'chart', 'inbox']; // `columnSpan` and `rowSpan` make a block wider or taller than one cell. // The chart block takes two columns and two rows; the neighbours flow around it. return ( setSelected(ids[index] ?? 'none')} data={{ columns: 4, gap: '12px', paddingX: '16px', paddingY: '16px', backgroundColor: '#F4F4F5', blockDefaults: { clickable: true, paddingX: '16px', paddingY: '16px', backgroundColor: '#FFFFFF', border: { color: '#D4D4D8', radius: '12px' }, }, blocks: [ { blockId: 'revenue', value: , }, { blockId: 'orders', value: , }, { blockId: 'chart', columnSpan: 2, rowSpan: 2, alignX: 'center', alignY: 'center', value: 'Chart area (2 x 2)', }, { blockId: 'inbox', columnSpan: 2, value: ( ), }, ], }} /> ); } ``` ## Features ausprobieren ### Responsive Spalten ohne Breakpoints Nur Blöcke sind gesetzt. Ändere die Fensterbreite: Die Karten brechen in so viele Spalten um, wie passen. ```tsx import { ArcWidgetGrid } from '@arcrider/arcwidgets-react'; export default function Example() { // Only blocks are set. The Grid fits as many columns of at least 200px as the // width allows, so resize the window and the cards wrap by themselves. return ( ); } ``` ### Feste Spalten, Abstand und Kachel-Look `columns` legt die Anzahl fest; die Spalten teilen sich die Breite. `blockDefaults` hält das gemeinsame Aussehen aller Kacheln, jeder Block setzt nur noch seinen Inhalt. ```tsx import { ArcWidgetGrid, ArcWidgetLayout, ArcWidgetText, } from '@arcrider/arcwidgets-react'; const tile = (title: string, amount: string) => ({ value: ( ), }, { value: ( ), }, ], }} /> ), }); export default function Example() { // `columns` fixes the column count; the columns share the width and shrink on narrow screens. // `blockDefaults` is the look every tile shares, so each block only sets its content. return ( ); } ``` ## Komponenten-Props ### id Optionale DOM-id. Im Normalfall nicht nötig; wird automatisch erzeugt, wenn sie fehlt. ### data Konfiguration vom Typ `ArcWidgetGridData`. Änderungen als neue Objekte übergeben; Geschäftsdaten und Auswahl im React-State halten. ### onAction Optionaler Fallback `(type: string, ...args: unknown[]) => unknown`. Der spezifische Callback hat Vorrang. Details: [State und Events](/docs/react/patterns/react-state-events). ### onBlockClick Wird beim Klick auf einen `clickable` Block aufgerufen. `blockIndex` ist der nullbasierte Index in `data.blocks`. ```typescript onBlockClick?: (blockIndex: number) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben. ### columns Anzahl der Spalten, z. B. `3`. Weglassen (oder `'auto-fill'`) füllt die Zeile mit so vielen Spalten, wie die Breite zulässt. `'auto-fit'` richtet sich ebenfalls nach der Breite, streckt die Zellen aber, wenn es wenige Blöcke gibt. Standard: `'auto-fill'`. Typ: `number | 'auto-fill' | 'auto-fit' | (string & {})`. ### columnWidth Mindestbreite einer Spalte, z. B. `"280px"`. Die Spalten wachsen gleichmäßig, bis die Zeile gefüllt ist. Standard: `200px`. Bei festem `columns` ohne `columnWidth` teilen sich die Spalten die Breite und werden auf schmalen Bildschirmen beliebig schmal. Typ: `string`. ### gap Abstand zwischen den Blöcken, für Zeilen und Spalten gleich. Standard: `10px`. Typ: `string`. ### width Breite des Grids als CSS. Standard: `100%`. Typ: `string`. ### height Höhe des Grids als CSS. Eine definierte Höhe, wenn das Grid einen Rahmen ausfüllt, `'auto'` für inhaltsgroße Grids. Standard: `auto`. Typ: `string`. ### paddingX Innenabstand links und rechts. Typ: `string`. ### paddingY Innenabstand oben und unten. Typ: `string`. ### backgroundColor Hintergrundfarbe des Grids. Typ: `string`. ### border Linie und Eckenradius des Grid-Containers: `color`, `width`, `style`, `radius` und `sides`. Die Linie wird gezeichnet, sobald `color`, `width`, `style` oder `sides` gesetzt ist; `radius` allein rundet nur die Ecken. Blöcke nehmen dasselbe `border`-Objekt. Typ: `ArcWidgetGridBorder`. ### blockDefaults Werte, die jeder Block übernimmt, solange er keinen eigenen setzt, z. B. `{ paddingX: '16px', backgroundColor: '#FFFFFF', border: { radius: '12px' } }`. Ein am Block gesetzter Wert ersetzt den Standard vollständig, auch bei `border`. `value` und `blockId` kommen nie aus den Defaults. Typ: `Omit`. ### styles Rohe CSS-Deklarationen, die zuletzt auf den Grid-Container angewendet werden, z. B. `'padding: 8px; color: #333'`. Typ: `string`. ### blocks Zellen in Lesereihenfolge. Jeder Block hat eigene Größe, Spannweiten, Ausrichtung (`alignX`, `alignY`), Farben, `styles` und Inhalt in `value`; siehe `ArcWidgetGridBlock`. Typ: `ArcWidgetGridBlock[]`. ## Verhalten und Grenzen - Zeilen sind so hoch wie ihr höchster Block. `rowSpan` lässt einen Block mehrere Zeilen belegen, `columnSpan` mehrere Spalten. Nachfolgende Blöcke fließen um Blöcke mit Spannweite herum. - Bei festem `columns` wird ein `columnSpan` über der Spaltenanzahl begrenzt, damit keine zusätzlichen Spalten entstehen. Im auto-fill-Modus gibt es keine feste Anzahl, dort gilt die Spannweite wie angegeben. - Jeder Block ist ein horizontaler Flex-Container. Positioniere seinen Inhalt mit `blocks[].alignX` (`left`, `center`, `right`; Standard `left`) und `blocks[].alignY` (`top`, `center`, `bottom`, `stretch`; Standard `center`). `text-align` in `blocks[].styles` kann den Inhalt nicht zentrieren. - `blocks[].width` und `blocks[].height` bestimmen die Größe des Inhalts innerhalb seiner Rasterfläche. `'fraction'` füllt die Fläche. - `blocks[].value` unterstützt JSX. Events verschachtelter Komponenten direkt an deren Callback-Props binden. - Grids lassen sich verschachteln. Jedes Grid wendet nur seine eigenen Einstellungen an, nie die des umgebenden Grids. - Für Block-Klicks muss `clickable: true` am Block gesetzt sein. Das Grid selbst hat keinen Klick-Callback. ## Datentypen Die folgenden Widget-Typen werden vom Paket exportiert. Gemeinsame Typen wie `ArcWidgetSlot` und `ArcReactWidgetProps` stehen ebenfalls zur Verfügung. Konfigurationsobjekte sind geschlossen, ein falsch geschriebener oder nicht unterstützter Schlüssel wird als Typfehler gemeldet. ```typescript export interface ArcWidgetGridBorder { color?: string; width?: string; style?: 'solid' | 'dashed' | 'dotted' | 'double'; radius?: string; sides?: Array<'top' | 'right' | 'bottom' | 'left'>; } export interface ArcWidgetGridBlock { value?: ArcWidgetSlot; columnSpan?: number; rowSpan?: number; width?: string; height?: string; alignX?: 'left' | 'center' | 'right'; alignY?: 'top' | 'center' | 'bottom' | 'stretch'; paddingX?: string; paddingY?: string; backgroundColor?: string; border?: ArcWidgetGridBorder; color?: string; backgroundImage?: string; backgroundSize?: string; backgroundPosition?: string; lineHeight?: string; styles?: string; clickable?: boolean; blockId?: string; } export interface ArcWidgetGridData { columns?: number | 'auto-fill' | 'auto-fit' | (string & {}); columnWidth?: string; gap?: string; width?: string; height?: string; paddingX?: string; paddingY?: string; backgroundColor?: string; border?: ArcWidgetGridBorder; styles?: string; blockDefaults?: Omit; blocks?: ArcWidgetGridBlock[]; } export interface ArcWidgetGridProps extends ArcReactWidgetProps { onBlockClick?: (blockIndex: number) => void; } ``` ## Einsatzbeispiele - [Dashboard mit CSS Grid](/docs/react/examples/dashboardansicht-mit-grid) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Grid ## AI Defaults (read first) - **uniqueId**: Required, `"grid-" + Nr`. - **columns**: String value (e.g. `"3"`), NOT a number. Default: auto-fill. - **columnWidth**: Default `"200px"`, controls minimum column width in auto-fill mode. - **gap**: Default `"10px"`, typically set to `"10px"` or `"16px"`. - **height**: Use `"auto"` or `"100%"` depending on context. - **blocks**: Each block can have `width`, `height`, `alignX`, `alignY`, `columnSpan`, `rowSpan`. - **embedded**: Set `true` when inside another widget. # Custom Grid Das Custom Grid ermöglicht die Erstellung von flexiblen und leistungsstarken Layouts durch die Definition eines zweidimensionalen Rasters. Durch die Festlegung von Spalten und Zeilen hast du eine präzise Kontrolle über die Anordnung von Inhalten, was besonders nützlich für die Schaffung komplexer Layoutstrukturen und die Gestaltung responsiver Designs ist. Der grundlegende Unterschied zwischen dem Grid und Layout besteht darin, dass das Layout in einer Dimension konzipiert wurde – entweder einer Zeile *oder* einer Spalte. Grid wurde für ein zweidimensionales Layout konzipiert – Zeilen und Spalten gleichzeitig. Beispiel-Bild eines Rasters mit der Integration von [Cards ](/documentation/cards)und [Layouts](/documentation/custom-layout): ![](https://framerusercontent.com/images/2YBdVENarVNdTsZmsI9uEVrHA.png) ## Anwendungscode manuell Bei einem manuellen Grid kannst du die Blöcke einzeln definieren und die Anzahl der Blöcke & deren Inhalte beliebig selbst bestimmen. ```javascript arcCustomGrid({ uniqueId: "", width: "100%", height: "100%", columns: "2", columnWidth: "400px", gap: "10px", paddingY: "20px", paddingX: "20px", blocks: [{ blockId: "", width: "", height: "", columnSpan: 1, rowSpan: 1, alignX: "center", alignY: "center", paddingY: "", paddingX: "", backgroundColor: "", lineHeight: "", styles: "", value: "" },{ blockId: "", width: "", height: "", columnSpan: 1, rowSpan: 1, alignX: "center", alignY: "center", paddingY: "", paddingX: "", backgroundColor: "", lineHeight: "", styles: "", value: "" }] }) ``` ## Anwendungscode dynamisch Bei einem dynamischen Grid, speist du die einzelnen Block-Elemente aus einer dynamischen Liste in das Grid ein. Im Anwendungscode kannst du vor deinen block-Elementen deine Liste referenzieren. Hier im Beispiel aus einer Aufgaben-Tabelle. ```javascript let aufgabe := (select Aufgaben); arcCustomGrid({ uniqueId: "", width: "100%", height: "100%", columns: "2", columnWidth: "400px", gap: "10px", paddingY: "20px", paddingX: "20px", blocks: aufgabe.[{ blockId: "", width: "", height: "", columnSpan: 1, rowSpan: 1, alignX: "center", alignY: "center", paddingY: "", paddingX: "", backgroundColor: "", lineHeight: "", styles: "", value: "" }] }) ``` ## Allgemeine Settings Die oberen Parameter geben die allgemeinen Settings deines Grids an. ### uniqueId *uniqueId* ist die individuelle Bezeichnung deines Grids. Achte darauf, dass du hier einen einzigartigen Titel vergibst. Das ist wichtig, falls du mehrere Grids auf einer Seite / in einer Tabelle darstellen möchtest und diese sich nicht gegenseitig überschreiben. ```javascript uniqueId: "Grid Aufgaben"+Nr, ``` ### width *width* gibt die Breite deines Grids an. ```javascript width: "100%", // Prozent-Werte. Hier: volle Breite. width: "100px", // Pixel-Werte width: "" // default: 100% ``` ### height *height* gibt die Höhe des Grids an. ```javascript height: "auto", // Automatisch an den Inhalt angepasste Höhe. height: "300px", // Pixel-Werte height: "", // default: auto ``` ### columns *columns* definiert wie viele Spalten (also wie viele Blöcke je Zeile) in deinem Grid angezeigt werden. ```javascript columns: "2", // Zahlen-Wert columns: "", // default: auto-fill ``` ### columnWidth *columnWidth* definiert wie breit deine einzelnen Spalten, die du unter columns angibst, sein sollen. ```javascript columnWidth: "400px", // Pixel-Wert columnWidth: "", // default: 200px ``` ### gap *gap* gibt den Abstand zwischen den einzelnen Blöcken (nicht nur Spalten!) an. ```javascript gap: "10px", // Pixel-Werte gap: "", // default: 10px ``` ### paddingX *paddingX gibt den Abstand zwischen dem Inhalt (hier: Blöcke) und dem Rand jeweils links und rechts an. * ```javascript paddingX: "20px", // Pixel-Werte paddingX: "", // default: 0px ``` ### paddingY *paddingY gibt den Abstand zwischen dem Inhalt (hier: Blöcke) und dem Rand jeweils oben und unten an. * ```javascript paddingY: "20px", // Pixel-Werte paddingY: "", // default: 0px ``` ### border *border* zeichnet Linie und Ecken des Grids: `color`, `width`, `style`, `radius` und `sides`. Ohne `color`, `width`, `style` oder `sides` rundet `radius` nur die Ecken. Dasselbe Objekt funktioniert an jedem Block. ```javascript border: { color: "#D4D4D8", radius: "12px" }, // Rahmen mit runden Ecken border: { sides: ["bottom"] }, // nur Linie unten ``` ### blockDefaults *blockDefaults* sind Werte, die jeder Block übernimmt, solange er keinen eigenen setzt. Ein am Block gesetzter Wert ersetzt den Standard vollständig. `value`, `blockId` und `clickAction` kommen nie aus den Defaults. ```javascript blockDefaults: { paddingX: "16px", backgroundColor: "#fff", border: { color: "#D4D4D8", radius: "12px" } }, ``` ## Block Settings In den Block Settings gibst du an, was in deinen einzelnen Blöcken des Grids angezeigt werden soll. Wie oben aufgezeigt, kannst du die Blöcke entweder manuell oder dynamisch definieren. ### blockId Die blockId kannst du nutzen, um deine Blöcke besser unterscheiden zu können. Das macht vor allem bei dem manuellen Grid Sinn. Die blockId wirkt sich aber auf keine anderen Parameter aus. ```javascript blockId: "", ``` ### width *width* gibt die Breite deines Grid-Blocks an. ```javascript width: "100%", // Prozent-Werte. Hier: volle Breite. width: "fraction", // Mit Fraction ordnet sich das Element unter und nimmt nur der übrig gebliebenen Platz ein. width: "100px", // Pixel-Werte width: "" // default: 100% ``` ### height *height* gibt die Höhe des Grid-Blocks an. ```javascript height: "auto", // Automatisch an den Inhalt angepasste Höhe. height: "fraction", // Mit Fraction ordnet sich das Element unter und nimmt nur der übrig gebliebenen Platz ein. height: "300px", // Pixel-Werte height: "", // default: auto ``` ### border *border* zeichnet Linie und Ecken des Blocks, siehe die Einstellung gleichen Namens beim Grid. ```javascript border: { color: "#D4D4D8", radius: "12px" }, ``` ### columnSpan *columnSpan* gibt an, über wie viele Spalten sich der Block erstreckt. Bei festem *columns* wird der Wert auf die Spaltenanzahl begrenzt. ```javascript columnSpan: 2, // Block belegt zwei Spalten columnSpan: "", // default: 1 ``` ### rowSpan *rowSpan* gibt an, über wie viele Zeilen sich der Block erstreckt. Die folgenden Blöcke fließen um ihn herum. ```javascript rowSpan: 2, // Block belegt zwei Zeilen rowSpan: "", // default: 1 ``` ### alignX *alignX* gibt an, wie deine Inhalte auf der X-Achse angeordnet werden. ```javascript alignX: "center", // zentriert // default: center alignX: "left", // linksbündig alignX: "right", // rechtsbündig alignX: "between", // teilt blocks auf gesamtbreite auf alignX: "", // default: left ``` ### alignY *alignY* gibt an, wie deine Inhalte auf der Y-Achse angeordnet werden. ```javascript alignY: "center", // zentriert // default: center alignY: "top", // oben angeordnet alignY: "bottom", // unten angeordnet alignY: "between", // teilt blocks auf gesamtbreite auf alignY: "", // default: center ``` ### paddingY *paddingY gibt den Abstand zwischen dem Inhalt deines einzelnenBlocks und dem Rand jeweils links und rechts an. * ```javascript paddingY: "20px", // Pixel-Werte paddingY: "", // default: 0px ``` ### paddingX *paddingX gibt den Abstand zwischen dem Inhalt deines einzelnenBlocks und dem Rand jeweils links und rechts an. * ```javascript paddingX: "20px", // Pixel-Werte paddingX: "", // default: 0px ``` ### backgroundColor *backgroundColor gibt die Hintergrundfarbe deines einzelnen Blocks an. * ```javascript backgroundColor: "#000", // HEX-Wert backgroundColor: "", // default: Keine ``` ### lineHeight *lineHeight* gibt den Zeilenabstand in den Texten deines Blocks an. ```javascript lineHeight: "normal", // Standardhöhe je nach Schriftgröße lineHeight: "1.6", // Zahlenwerte getrennt durch Punkte lineHeight: "10px", // Pixel-Werte lineHeight: "", // default: normal ``` ### styles Bei ***styles*** kannst du dem Grid-Block eigene CSS styles hinzufügen. ```javascript styles: "", // z.B. font-weight: 700; ``` ### value In deinem *value *definierst du nun, was in den einzelnen Blöcken angezeigt wird. Beispielsweise kannst du hier nun andere Widgets, wie die [Custom Card](/documentation/cards) einfügen. * * ```javascript value: "" ``` ## Aktionen ***clickAction*** beschreibt deine Aktionen in den jeweiligen Blocks. ```javascript clickAction: { recordId: Nr, type: "update", field: "", value: "", } ``` ### Unterschiedliche Typen der Aktionen ***type*** sind die jeweiligen Funktionen der Aktion. ```javascript // Folgende Actions benötigen die zwei Paramter: "recordId" und "type" "popup" "delete" "openRecord" "fullscreen" // Folgender Actions benötigt die 4 Parmater: "recordId", "type", "field", "value" "update" // Folgende Actions benötigen nur den Parameter "type" in der clickAction. "closeFullScreen" "closeRecord" "closeAllRecords" ``` --- # Custom Kanban Source: https://www.arc-rider.com/docs/widgets/custom-kanban # Kanban in React Aufgaben oder Vorgänge als Karten in Statusspalten darstellen. Auswahl und gespeicherter Board-Zustand gehören deiner React-Anwendung. Referenz für `@arcrider/arcwidgets-react@0.1.0-alpha.5`. [Installation](/docs/react/intro/setup-react) · [Alle Komponenten](/docs/react/intro/react-components). ## Vollständiges Beispiel Gesetzt sind nur Spaltentitel und Karten; alles andere ist Standard: dunkle Köpfe mit kleiner fetter Schrift, graue Spalten mit derselben Rundung wie der Button und ein kleiner Abstand zwischen Kopf und Spalte. ```tsx import { ArcWidgetKanban, ArcWidgetLayout } from '@arcrider/arcwidgets-react'; export default function Example() { // Only required fields are set on the board; everything else is the widget default. // The Layout gives the board a white surface. return ( ), }, ], }} /> ); } ``` ## Features ausprobieren Jedes Beispiel setzt nur die Werte, die die Variante ausmachen. ### Kopf ohne Farbe `backgroundColorHeader: 'transparent'` lässt nur die Beschriftung über der grauen Spalte stehen. Setze eine dunkle `fontColor`; die Zähler-Bubble folgt der Schriftfarbe, und der Rahmen beim Ziehen bleibt dunkel. ```tsx import { ArcWidgetKanban, ArcWidgetLayout, type ArcWidgetKanbanSwimlane, } from '@arcrider/arcwidgets-react'; // A transparent header leaves only the label above the gray lane. // Set `fontColor` to a dark color; the count bubble follows it. const plainHeader: ArcWidgetKanbanSwimlane = { backgroundColorHeader: 'transparent', fontColor: '#18181B', }; const lanes: ArcWidgetKanbanSwimlane[] = [ { title: 'To do', items: [ { id: 'brief', title: 'Write brief', subtitle: 'Due Friday' }, { id: 'venue', title: 'Book venue', subtitle: 'Due next week' }, ], }, { title: 'In progress', items: [{ id: 'poster', title: 'Design poster', subtitle: 'Draft 2' }], }, { title: 'Done', items: [{ id: 'kickoff', title: 'Kickoff call', subtitle: 'Completed' }], }, ]; export default function Example() { return ( ({ ...plainHeader, ...lane, amount: lane.items?.length, })), }} /> ), }, ], }} /> ); } ``` ### Kopffarben Eine Kopffarbe pro Status. Die Kopffarbe umrandet auch die Spalte, wenn eine Karte darübergezogen wird; ein heller Kopf braucht eine dunkle `fontColor`. ```tsx import { ArcWidgetKanban, ArcWidgetLayout, type ArcWidgetKanbanSwimlane, } from '@arcrider/arcwidgets-react'; // One header color per status. The header color also outlines the lane while a card is dragged over it. // A light header needs a dark `fontColor`. const lanes: ArcWidgetKanbanSwimlane[] = [ { title: 'Backlog', backgroundColorHeader: '#E4E4E7', fontColor: '#18181B', items: [{ id: 'survey', title: 'Customer survey', subtitle: 'Not planned yet' }], }, { title: 'In progress', backgroundColorHeader: '#2563EB', items: [ { id: 'poster', title: 'Design poster', subtitle: 'Draft 2' }, { id: 'venue', title: 'Book venue', subtitle: 'Waiting for offer' }, ], }, { title: 'Review', backgroundColorHeader: '#D97706', items: [{ id: 'brief', title: 'Write brief', subtitle: 'With Anna' }], }, { title: 'Done', backgroundColorHeader: '#16A34A', backgroundColorBody: '#F0FDF4', items: [{ id: 'kickoff', title: 'Kickoff call', subtitle: 'Completed' }], }, ]; export default function Example() { return ( ({ ...lane, amount: lane.items?.length })), }} /> ), }, ], }} /> ); } ``` ### Slots im Kopf `slotLeft` steht vor dem Titel, `slotRight` hinter der Zähler-Bubble. Beide nehmen beliebige Inhalte, z. B. einen Status-Punkt oder einen Icon-Button; Klicks in einem Slot bleiben bei dessen Inhalt. ```tsx import { useState } from 'react'; import { ArcWidgetButton, ArcWidgetKanban, ArcWidgetLayout, type ArcWidgetKanbanItem, } from '@arcrider/arcwidgets-react'; import { CircleIcon, PlusIcon } from '@phosphor-icons/react'; const lanes = [ { id: 'todo', title: 'To do', color: '#A1A1AA' }, { id: 'doing', title: 'In progress', color: '#3B82F6' }, { id: 'done', title: 'Done', color: '#22C55E' }, ]; const initialCards: Record = { todo: [{ id: 'brief', title: 'Write brief', subtitle: 'Due Friday' }], doing: [{ id: 'poster', title: 'Design poster', subtitle: 'Draft 2' }], done: [{ id: 'kickoff', title: 'Kickoff call', subtitle: 'Completed' }], }; export default function Example() { const [cards, setCards] = useState(initialCards); const addCard = (laneId: string) => setCards((current) => { const count = current[laneId].length + 1; return { ...current, [laneId]: [...current[laneId], { id: `${laneId}-${count}`, title: `New card ${count}` }], }; }); return ( ({ title: lane.title, amount: cards[lane.id].length, // `slotLeft` sits before the title, `slotRight` after the count bubble. slotLeft: , // The default button matches the dark header and lightens on hover. slotRight: ( , paddingX: '0.4em', paddingY: '0.4em' }} onClick={() => addCard(lane.id)} /> ), items: cards[lane.id], })), }} /> ), }, ], }} /> ); } ``` ### Karten `title`, `subtitle` und `value` nehmen Text oder ein beliebiges Widget, hier ein Badge für die Priorität. `borderPosition: 'left'` macht aus dem Kartenrand einen Akzentstreifen. ```tsx import { ArcWidgetBadge, ArcWidgetKanban, ArcWidgetLayout, type ArcWidgetKanbanItem, } from '@arcrider/arcwidgets-react'; const priorities: Record = { high: { label: 'High', color: '#DC2626', stroke: 'solid' }, normal: { label: 'Normal', color: '#2563EB', stroke: 'solid' }, low: { label: 'Low', color: '#64748B', stroke: 'dashed' }, }; type Task = { id: string; title: string; owner: string; due: string; priority: keyof typeof priorities }; const tasks: Record = { 'To do': [ { id: 'T-12', title: 'Replace workshop lights', owner: 'Facilities', due: 'Mon', priority: 'high' }, { id: 'T-14', title: 'Order office supplies', owner: 'Office', due: 'Fri', priority: 'low' }, ], 'In progress': [ { id: 'T-09', title: 'Renew design software', owner: 'Design', due: 'Wed', priority: 'normal' }, ], Done: [{ id: 'T-03', title: 'Service ventilation', owner: 'Operations', due: 'Done', priority: 'normal' }], }; // `title`, `subtitle` and `value` take text or any widget; `borderPosition: 'left'` turns the border into an accent stripe. // `borderStroke` makes the stripe solid (default), dashed or dotted. const toCard = (task: Task): ArcWidgetKanbanItem => { const priority = priorities[task.priority]; return { id: task.id, title: task.title, subtitle: `${task.id} · ${task.owner} · ${task.due}`, value: , borderColor: priority.color, borderWidth: '3px', borderPosition: 'left', borderStroke: priority.stroke, borderRadius: '8px', }; }; export default function Example() { return ( ({ title, amount: list.length, items: list.map(toCard), })), }} /> ), }, ], }} /> ); } ``` ### Gruppen `sections` gruppieren die Karten einer Spalte. Gib jeder Gruppe eine stabile `id`, damit ihr Auf- oder Zuklappzustand gemerkt wird. ```tsx import { ArcWidgetKanban, ArcWidgetLayout } from '@arcrider/arcwidgets-react'; const lanes = [ { id: 'todo', title: 'To do' }, { id: 'doing', title: 'In progress' }, ]; const groups = [ { id: 'urgent', title: 'Urgent' }, { id: 'routine', title: 'Routine' }, ]; const tasks = [ { id: 'T-12', title: 'Replace workshop lights', lane: 'todo', group: 'urgent' }, { id: 'T-14', title: 'Order office supplies', lane: 'todo', group: 'routine' }, { id: 'T-15', title: 'Replace reception chair', lane: 'todo', group: 'routine' }, { id: 'T-09', title: 'Inspect ventilation fault', lane: 'doing', group: 'urgent' }, { id: 'T-10', title: 'Renew design software', lane: 'doing', group: 'routine' }, ]; export default function Example() { return ( { const laneTasks = tasks.filter((task) => task.lane === lane.id); return { title: lane.title, amount: laneTasks.length, // `sections` replace `items`. A stable `id` keeps each group's open or collapsed state. sections: groups.map((group) => ({ id: `${lane.id}-${group.id}`, title: group.title, items: laneTasks .filter((task) => task.group === group.id) .map((task) => ({ id: task.id, title: task.title, subtitle: task.id })), })), }; }), }} /> ), }, ], }} /> ); } ``` ### Leere Spalten und Einklappen `empty` zeigt einen Platzhalter in Spalten ohne Karten; ein eigenes `empty` an der Spalte hat Vorrang. `collapsible: true` fügt jedem Kopf einen Umschalter hinzu. ```tsx import { ArcWidgetKanban, ArcWidgetLayout } from '@arcrider/arcwidgets-react'; export default function Example() { return ( ), }, ], }} /> ); } ``` ### Einklapp-Icons Mit `iconExpand` und `iconCollapse` setzt du eigene Icons für den Umschalter; ohne sie ist er ein Chevron, das sich dreht. Jeder Inhalt geht, z. B. ein Icon oder ein Button. ```tsx import { ArcWidgetKanban, ArcWidgetLayout } from '@arcrider/arcwidgets-react'; import { MinusIcon, PlusIcon } from '@phosphor-icons/react'; export default function Example() { return ( , iconCollapse: , }, swimlanes: [ { title: 'To do', amount: 2, items: [ { id: 'brief', title: 'Write brief', subtitle: 'Due Friday' }, { id: 'venue', title: 'Book venue', subtitle: 'Due next week' }, ], }, { title: 'In progress', amount: 1, items: [{ id: 'poster', title: 'Design poster', subtitle: 'Draft 2' }], }, { title: 'Done', amount: 1, items: [{ id: 'kickoff', title: 'Kickoff call', subtitle: 'Completed' }], }, ], }} /> ), }, ], }} /> ); } ``` ### Akzentlinien `insetBorder` zeichnet eine Linie mit runden Enden innerhalb der Karte. Teile sie in `segments`, um zu zeigen, wer oder was beteiligt ist, zum Beispiel eine Farbe pro Person. Jedes Segment hat eine eigene `color` und ein `label`, das als Tooltip erscheint. `size` legt die Länge entlang der Linie fest: `'fraction'` (oder `'auto'`) teilt den Rest gleichmäßig, `'24px'` oder `'25%'` sind fest. Feste Segmente, die die Linie nicht füllen, sitzen am Anfang; mit `align` verschiebst du sie. Jede Karte zeigt standardmäßig eine schwarze Linie am linken Rand. Mit `insetBorder: false` blendest du sie aus; setzt du `insetBorder` am Board (`data`), gestaltest du die Linie aller Karten um, zum Beispiel `{ color: '#A1A1AA' }` auf einem dunklen Board; Felder der Karte selbst haben Vorrang. `position` setzt die Linie nach `right`, `top` oder `bottom`. ```tsx import { ArcWidgetKanban, ArcWidgetLayout, type ArcWidgetKanbanInsetBorder, type ArcWidgetKanbanItem, } from '@arcrider/arcwidgets-react'; const people = { anna: { name: 'Anna', color: '#DC2626' }, ben: { name: 'Ben', color: '#2563EB' }, clara: { name: 'Clara', color: '#16A34A' }, }; type Person = keyof typeof people; type Owner = { person: Person; size?: string }; type Task = { id: string; title: string; owners: Owner[]; note?: string; side?: ArcWidgetKanbanInsetBorder['position'] }; const tasks: Record = { 'To do': [ { id: 'T-12', title: 'Replace workshop lights', owners: [{ person: 'anna' }] }, { id: 'T-14', title: 'Order office supplies', owners: [{ person: 'anna' }, { person: 'ben' }, { person: 'clara' }] }, ], 'In progress': [ { id: 'T-09', title: 'Renew design software', owners: [{ person: 'ben', size: '66%' }, { person: 'clara' }], note: 'Ben has the larger share' }, ], Done: [{ id: 'T-03', title: 'Service ventilation', owners: [{ person: 'clara' }], note: 'Line at the bottom', side: 'bottom' }], }; // `insetBorder` draws a line with round ends inside the card. Cut it into `segments` to show who is // involved: each segment has its own `color` and a `label` that appears as a tooltip. // `size` sets the length along the line: 'fraction' (or 'auto') shares what is left equally, // '24px' or '25%' are fixed. Every card has a black line on the left by default; without `segments` it stays one piece. // `position` moves the line to 'right', 'top' or 'bottom'. const toCard = (task: Task): ArcWidgetKanbanItem => ({ id: task.id, title: task.title, subtitle: task.note ?? task.id, insetBorder: { position: task.side, segments: task.owners.map(({ person, size }) => ({ color: people[person].color, label: people[person].name, size, })), }, }); export default function Example() { return ( ({ title, amount: list.length, items: list.map(toCard), })), }} /> ), }, ], }} /> ); } ``` ### Reichhaltige Karten im breiten Board Ein Layout im `value` der Karte setzt sie frei zusammen: oben ein Badge und ein Icon, darunter Text, unten Team-Porträts und Icon-Buttons. Ein Block mit `width: 'fraction'` nimmt die freie Breite und schiebt die folgenden Blöcke an den rechten Rand. Das Board hat `styles.minWidth`: Spalten teilen sich die Breite gleichmäßig, und sobald sie schmaler als dieser Wert würden, scrollt das Board seitlich (`scrollSettings.scrollX` einschalten). Eine Spalte selbst scrollt nie seitlich. Beide Scrollbars sind standardmäßig ausgeblendet; `scrollBar` zeigt und gestaltet sie wie bei der Tabelle. Die horizontale Leiste sitzt mit etwas Abstand unter den Spalten, der Track hat keine Farbe, solange du keine setzt. ```tsx import type { ReactNode } from 'react'; import { ArcWidgetBadge, ArcWidgetButton, ArcWidgetImage, ArcWidgetKanban, ArcWidgetLayout, ArcWidgetText, type ArcWidgetKanbanItem, } from '@arcrider/arcwidgets-react'; import { ChatCircleIcon, FlagIcon, PaperclipIcon } from '@phosphor-icons/react'; // Generated square portraits keep the example offline. const portrait = (color: string) => `data:image/svg+xml,${encodeURIComponent( "" + `` + "" + "" + '', )}`; const people: Record = { am: { name: 'Alex Morgan', color: '#6366F1' }, sl: { name: 'Sam Lee', color: '#0EA5E9' }, rd: { name: 'Robin Diaz', color: '#F59E0B' }, kn: { name: 'Kim Novak', color: '#10B981' }, }; const priorities = { high: { label: 'High', color: '#DC2626' }, normal: { label: 'Normal', color: '#2563EB' }, low: { label: 'Low', color: '#64748B' }, }; type Task = { id: string; title: string; note: string; priority: keyof typeof priorities; team: string[] }; const board: Record = { Backlog: [ { id: 'T-21', title: 'Plan the autumn campaign', note: 'Channels, budget and the calendar.', priority: 'low', team: ['am'] }, { id: 'T-22', title: 'Collect customer quotes', note: 'Three short quotes for the landing page.', priority: 'normal', team: ['sl', 'rd'] }, ], Design: [ { id: 'T-17', title: 'Poster series', note: 'Three formats, print and screen.', priority: 'high', team: ['am', 'sl', 'kn'] }, ], Build: [ { id: 'T-12', title: 'Newsletter template', note: 'Header, two columns and a footer with legal links.', priority: 'normal', team: ['rd', 'kn'] }, { id: 'T-14', title: 'Booking form', note: 'Validation and a confirmation mail.', priority: 'high', team: ['sl'] }, ], Review: [{ id: 'T-09', title: 'Price list 2027', note: 'Waiting for sign-off.', priority: 'normal', team: ['am', 'kn'] }], Testing: [{ id: 'T-06', title: 'Checkout on mobile', note: 'iOS and Android, three browsers.', priority: 'high', team: ['rd'] }], Done: [{ id: 'T-03', title: 'Kickoff call', note: 'Notes shared with everyone.', priority: 'low', team: ['am', 'sl', 'rd', 'kn'] }], }; const iconButton = (icon: ReactNode, onClick: () => void) => ( ); // The whole card is one Layout in `value`: three rows, each a horizontal Layout. // A block with `width: 'fraction'` takes the free width and pushes the blocks after it to the right edge. const toCard = (task: Task): ArcWidgetKanbanItem => { const priority = priorities[task.priority]; return { id: task.id, value: ( }, { width: 'fraction', value: ( ), }, { width: 'auto', value: , }, ], }} /> ), }, { value: ( ), }, { value: ( ({ width: 'auto', // `title` names the person on hover. value: ( ), })), }} /> ), }, { width: 'fraction', value: '' }, { width: 'auto', value: iconButton(, () => undefined) }, { width: 'auto', value: iconButton(, () => undefined) }, ], }} /> ), }, ], }} /> ), }; }; export default function Example() { // Six lanes of at least 300px: when the board is wider they share the width equally, // when it is narrower the board scrolls sideways. A lane never gets a scrollbar of its own sideways. return (