# 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, ArcWidgetTable, type ArcWidgetTableRow, } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const statusTone = { open: { color: '#d97706', label: t('Offen', 'Open', 'Abierto') }, work: { color: '#0f766e', label: t('In Arbeit', 'In progress', 'En curso') }, done: { color: '#15803d', label: t('Erledigt', 'Done', 'Hecho') }, wait: { color: '#be123c', label: t('Wartet', 'Waiting', 'En espera') }, }; type Status = keyof typeof statusTone; type Order = { id: string; title: string; amount: number; status: Status; items?: Order[]; }; const demoOrders: Order[] = [ { id: 'a', title: t('Auftrag Nordhafen', 'North Harbor order', 'Pedido Puerto Norte'), amount: 240, status: 'work', items: [ { id: 'a-1', title: t('Vorbereitung', 'Preparation', 'Preparación'), amount: 80, status: 'done', }, { id: 'a-2', title: t('Umsetzung', 'Delivery', 'Entrega'), amount: 160, status: 'work', }, ], }, { id: 'b', title: t('Auftrag Westpark', 'Westpark order', 'Pedido Westpark'), amount: 80, status: 'open', }, { id: 'c', title: t('Auftrag Innenstadt', 'Downtown order', 'Pedido centro'), amount: 420, status: 'wait', }, { id: 'd', title: t('Auftrag Werkstatt', 'Workshop order', 'Pedido taller'), amount: 150, status: 'done', }, ]; const SITES = [ 'Nordhafen', 'Westpark', 'Innenstadt', 'Werkstatt', 'Messe', 'Logistik', 'Campus', 'Hafen Süd', ]; const STATUSES: Status[] = ['open', 'work', 'done', 'wait']; const busyOrders: Order[] = Array.from({ length: 800 }, (_, index) => ({ id: `busy-${index + 1}`, title: `${t('Auftrag', 'Order', 'Pedido')} ${SITES[index % SITES.length]} ${String(index + 1).padStart(3, '0')}`, amount: 50 + ((index * 47) % 9950), status: STATUSES[index % STATUSES.length], })); const orders = pack === 'empty' ? [] : pack === 'busy' ? busyOrders : demoOrders; export default function Example() { const [selected, setSelected] = useState( t('Keine Auswahl', 'Nothing selected', 'Sin selección'), ); const statusMark = (status: Status) => pack === 'busy' ? ( {statusTone[status].label} ) : ( ); const row = (order: Order): ArcWidgetTableRow => ({ id: order.id, clickable: true, columns: [ { value: order.title, width: 'fraction' }, { value: statusMark(order.status), width: '130px', }, { value: order.amount, width: '90px', sort: { value: order.amount }, }, ], items: (order.items ?? []).map(row), }); return ( <> {t('Keine Aufträge', 'No orders', 'Sin pedidos')}

{t( 'Leerer Zustand — über Daten auf Voll oder Demo wechseln.', 'Empty state — switch Data to Full or Demo.', 'Estado vacío — cambia Datos a Completo o Demo.', )}

), }, }} onCellClick={(_event, { row_index }) => { setSelected( row_index == null ? t('Keine Auswahl', 'Nothing selected', 'Sin selección') : `${t('Zeile', 'Row', 'Fila')} ${row_index + 1}`, ); }} />

{selected}

); } ``` Die Liste entsteht aus React-Daten. Ein Klick aktualisiert den lokalen 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, ArcWidgetButton, ArcWidgetLayout, ArcWidgetText, } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [count, setCount] = useState(pack === 'busy' ? 12 : 0); const badges = pack === 'empty' ? [] : [ { value: t('Offen', 'Open', 'Abierto'), color: '#d97706' }, { value: t('Live', 'Live', 'En vivo'), color: '#15803d' }, ...(pack === 'busy' ? [ { value: t('Priorität', 'Priority', 'Prioridad'), color: '#be123c', }, { value: t('Messe', 'Fair', 'Feria'), color: '#7c3aed' }, ] : []), ]; return ( ), }, ...(badges.length ? [ { height: 'auto' as const, value: ( ({ width: 'auto', height: 'auto', value: ( ), })), }} /> ), }, ] : []), { height: 'fraction', value: ( setCount((value) => value + 1)} /> ), }, ], }} /> ); } ``` 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 22 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.5 ``` Diese Dokumentation gehört zu `0.1.0-alpha.5`. 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 lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const startLanes = [ { id: 'open', title: t('Offen', 'Open', 'Abierto'), items: [ { id: 'brief', title: t('Angebot vorbereiten', 'Prepare quote', 'Preparar oferta'), tag: t('Heute', 'Today', 'Hoy'), color: '#d97706', }, ], }, { id: 'done', title: t('Erledigt', 'Done', 'Hecho'), items: [] as { id: string; title: string; tag: string; color: string }[], }, ]; export default function Example() { const [lanes, setLanes] = useState(startLanes); const [selected, setSelected] = useState( t( 'Karte ziehen oder anklicken', 'Drag or click a card', 'Arrastra o pulsa una tarjeta', ), ); 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(`${t('Aufgabe', 'Task', 'Tarea')} ${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(t('Karte verschoben', 'Card moved', 'Tarjeta movida')); }} />

{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/docs/`. 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 22 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) - `ArcWidgetIcon` — [Icon](/docs/react/mini-widgets/custom-icon) - `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/docs/`. ## 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 { ArcWidgetCalendarWeek } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const locale = lang === 'en' ? 'en-US' : lang === 'es' ? 'es-ES' : 'de-DE'; const day = (offset: number) => new Date(2026, 8, 14 + offset).getTime(); const hour = (value: number) => value * 3600000; const chrome = { backgroundColor: '#ffffff', fontColor: '#18181b', mutedColor: '#52525b', borders: { color: '#e4e4e7', outer: true, columns: true, rows: true }, }; const look = (backgroundColor: string, color: string) => ({ backgroundColor, color, fontColor: color, }); const teal = '#115e59'; const tealFill = '#ccfbf1'; const travelFill = '#99f6e4'; const pauseFill = '#f4f4f5'; const pauseInk = '#52525b'; type Entry = { id: string; title: string; dateFrom: number; dateTo: number; timeFrom: number; timeTo: number; clickable: true; draggable: true; styles: ReturnType; insetBorder: { enabled: true; color: string }; segments?: Array<{ title: string; linked?: boolean; dateFrom: number; dateTo: number; timeFrom: number; timeTo: number; styles: ReturnType; insetBorder: { enabled: true; color: string }; splits?: Array<{ title: string; timeFrom: number; timeTo: number; styles: ReturnType; }>; }>; }; const event = ( id: string, title: string, offset: number, from: number, to: number, backgroundColor: string, color: string, ): Entry => ({ id, title, dateFrom: day(offset), dateTo: day(offset), timeFrom: hour(from), timeTo: hour(to), clickable: true, draggable: true, styles: look(backgroundColor, color), insetBorder: { enabled: true, color }, }); const siteVisit = (): Entry => ({ id: 'visit', title: t('Kundentermin', 'Site visit', 'Visita'), dateFrom: day(3), dateTo: day(3), timeFrom: hour(11), timeTo: hour(14), clickable: true, draggable: true, styles: look(tealFill, teal), insetBorder: { enabled: true, color: teal }, segments: [ { title: t('Anfahrt', 'Travel there', 'Ida'), linked: false, dateFrom: day(2), dateTo: day(2), timeFrom: hour(16), timeTo: hour(17), styles: look(travelFill, teal), insetBorder: { enabled: true, color: teal }, }, { title: t('Hauptarbeit', 'On site', 'Trabajo principal'), linked: true, dateFrom: day(3), dateTo: day(3), timeFrom: hour(11), timeTo: hour(14), styles: look(tealFill, teal), insetBorder: { enabled: true, color: teal }, splits: [ { title: t('Pause', 'Break', 'Pausa'), timeFrom: hour(12) + 30 * 60 * 1000, timeTo: hour(13), styles: look(pauseFill, pauseInk), }, ], }, { title: t('Abfahrt', 'Travel back', 'Vuelta'), linked: false, dateFrom: day(4), dateTo: day(4), timeFrom: hour(8), timeTo: hour(9), styles: look(travelFill, teal), insetBorder: { enabled: true, color: teal }, }, ], }); const demoEntries: Entry[] = [ event( 'plan', t('Planung', 'Planning', 'Planificación'), 0, 9, 10, '#d1fae5', '#065f46', ), event( 'workshop', t('Workshop', 'Workshop', 'Taller'), 1, 10, 12, '#fef3c7', '#92400e', ), event( 'review', t('Review', 'Review', 'Revisión'), 2, 14, 15, '#ffe4e6', '#9f1239', ), siteVisit(), ]; const busyEntries: Entry[] = [ ...demoEntries, event( 'stand', t('Standabnahme', 'Booth check', 'Revisión stand'), 4, 10, 12, '#ede9fe', '#5b21b6', ), event('retro', t('Retro', 'Retro', 'Retro'), 4, 15, 16, '#e0f2fe', '#075985'), ]; export default function Example() { const [entries, setEntries] = useState( pack === 'empty' ? [] : pack === 'busy' ? busyEntries : demoEntries, ); const [selected, setSelected] = useState( t( 'Termin ziehen oder anklicken', 'Drag or click an event', 'Arrastra o pulsa un evento', ), ); return ( <> setSelected( t('Termin ausgewählt', 'Event selected', 'Evento seleccionado'), ) } onDayClick={(date) => setSelected(new Date(date).toLocaleDateString(locale)) } onEntryDragSave={(index, start) => { setEntries((previous) => { if (index >= previous.length || previous[index].segments) return previous; return previous.map((entry, i) => i === index ? { ...entry, dateFrom: Number(start.dateFrom ?? entry.dateFrom), dateTo: Number(start.dateTo ?? entry.dateTo), timeFrom: Number(start.timeFrom ?? entry.timeFrom), timeTo: Number(start.timeTo ?? entry.timeTo), } : entry, ); }); setSelected(t('Termin verschoben', 'Event moved', 'Evento movido')); }} />

{selected}

); } ``` ## Features ausprobieren ### Tagesansichten, Terminbearbeitung und JSX Ein gemeinsamer State steuert die Zahl der Tage und den Terminbeginn. Die Bearbeitung nutzt bewusst den Regler, da Drag-Callbacks noch keine vollständigen Zielzeiten liefern. ```tsx import { useState } from 'react'; import { ArcWidgetCalendarWeek, ArcWidgetBadge, } from '@arcrider/arcwidgets-react'; export default function Example() { const [days, setDays] = useState(7); const [hour, setHour] = useState(10); const [custom, setCustom] = useState(true); const [selected, setSelected] = useState('Keine Auswahl'); return ( <> Projektbesprechung ) : undefined, dateFrom: new Date(2026, 8, 14).getTime(), dateTo: new Date(2026, 8, 14).getTime(), timeFrom: hour * 3600000, timeTo: (hour + 1) * 3600000, clickable: true, draggable: false, }, ], }} onEntryClick={() => setSelected(`Termin um ${hour}:00`)} onDayClick={(date) => setSelected(new Date(date).toLocaleDateString('de-DE')) } />

{selected}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### 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 Index des Termins, bei Timeline zusätzlich der Ressourcenzeile. ```typescript onEntryClick?: (entry_index: number) => void; ``` ### onDayClick Datum des gewählten Tages als Millisekunden-Zeitstempel. ```typescript onDayClick?: (date_millis: number) => void; ``` ### onEntryDragSave Drag-Ende mit ursprünglichen Daten. Keine vollständigen neuen Zielzeiten; siehe Hinweise zur Persistenz. ```typescript onEntryDragSave?: (entry_index: number, start_data: ArcWidgetCalendarWeekDragStart) => void; ``` ### onDailyEntryDragSave Drag-Ende eines ganztägigen Termins mit ursprünglichen Daten. ```typescript onDailyEntryDragSave?: (entry_index: number, start_data: ArcWidgetCalendarWeekDragStart) => void; ``` ### onCreate Neue Datum-/Zeitauswahl. Validieren und Speichern übernimmt die Anwendung. ```typescript onCreate?: (values: { dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; [key: string]: unknown; }) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### styles Gemeinsame Farben, Abstände und Rahmen; die angebotenen Felder sind widgetspezifisch. Typ: `ArcWidgetCalendarWeekStyles | string`. ### weekdaySettings Auswahl einer Kalenderwoche und des Jahres. Typ: `{ week?: number; year?: number; [key: string]: unknown }`. ### weekStart Alternativer Wochenbeginn. Typ: `unknown`. ### dateSettings Datumsbereich, Kopfzeilen und Markierungen. Typ: `ArcWidgetCalendarDateSettings`. ### timeZoneBalance Korrektur in Stunden; die Beispiele verwenden `0` und lokal erzeugte Tageswerte. Typ: `number | string`. ### timeSettings Zeitbereich und Raster; Einheiten sind Millisekunden. Typ: `ArcWidgetCalendarTimeSettings`. ### currentTime Darstellung der aktuellen Zeit. Typ: `string`. ### lang Sprache der Kalenderbeschriftungen. Typ: `string`. ### timeSlotWidth Breite einer Tagesspalte. Typ: `string`. ### timeSlotHeight Höhe eines Zeitrasters. Typ: `string`. ### allDayHeight Höhe des Bereichs für ganztägige Termine. Typ: `string`. ### createNew Auswahl zum Erstellen neuer Termine. Speichern erfolgt über `onCreate`. Typ: `ArcWidgetCalendarCreateNew`. ### timeEntries Termine mit Datumswerten, Uhrzeiten und Inhalten. Typ: `ArcWidgetCalendarTimeEntry[]`. ## Verhalten und Grenzen - Für JSX in Terminen verwende `customLayout`. Die Standardfelder `title`, `subtitle` und `value` werden als Text oder HTML verarbeitet. - Datumswerte sind Zeitstempel in Millisekunden; Uhrzeiten sind Millisekunden seit Tagesbeginn. 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. - Die derzeitigen Drag-Callbacks enthalten den ursprünglichen Stand (`start_data`), jedoch keine vollständigen neuen Zielzeiten. Das Beispiel setzt deshalb `draggable: false`. Bearbeite Termine über React-State, bis ein vollständiger Persistenzvertrag für Drag & Drop vorliegt. - Der sichtbare Uhrzeitbereich wird über `timeSettings.timeStart` und `timeSettings.duration` in Millisekunden konfiguriert. Die historischen Felder `from` und `to` haben hier keine Wirkung. - `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`, `ArcWidgetStyles` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export type ArcWidgetCalendarWeekBorderLine = boolean | { color?: string; width?: string; borderColor?: string; borderWidth?: string; borderSize?: string; enabled?: boolean; }; export interface ArcWidgetCalendarWeekBorders { color?: string; width?: string; outer?: ArcWidgetCalendarWeekBorderLine; columns?: ArcWidgetCalendarWeekBorderLine; rows?: ArcWidgetCalendarWeekBorderLine; } export interface ArcWidgetCalendarWeekStyles extends ArcWidgetStyles { borders?: ArcWidgetCalendarWeekBorders; headerBackgroundColor?: string; mutedColor?: string; } export interface ArcWidgetCalendarHighlight { date?: unknown; weekday?: number; color?: string; styles?: ArcWidgetCalendarWeekStyles | string; [key: string]: unknown; } export interface ArcWidgetCalendarWeekDay { clickable?: boolean; [key: string]: unknown; } export interface ArcWidgetCalendarDateSettings { startDate?: unknown; endDate?: unknown; duration?: number; highlights?: ArcWidgetCalendarHighlight[]; day?: ArcWidgetCalendarWeekDay; header?: { days?: unknown; months?: unknown; weeks?: unknown; [key: string]: unknown; }; month?: number; year?: number; [key: string]: unknown; } export interface ArcWidgetCalendarTimeSettings { from?: number; to?: number; timeStart?: number; duration?: number; [key: string]: unknown; } export interface ArcWidgetCalendarPlaceOffset { days?: number; time?: number; } export interface ArcWidgetCalendarPlaceSplit { title?: string; subtitle?: string; value?: string; styles?: ArcWidgetStyles | string; timeFrom?: unknown; timeTo?: unknown; offset?: number; duration?: number; [key: string]: unknown; } export interface ArcWidgetCalendarPlaceSegment { duration?: number; linked?: boolean; offset?: ArcWidgetCalendarPlaceOffset; title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; styles?: ArcWidgetStyles | string; insetBorder?: unknown; dateFrom?: unknown; dateTo?: unknown; timeFrom?: unknown; timeTo?: unknown; splits?: ArcWidgetCalendarPlaceSplit[]; [key: string]: unknown; } export interface ArcWidgetCalendarPlaceItem { active?: boolean; offset?: ArcWidgetCalendarPlaceOffset; splits?: ArcWidgetCalendarPlaceSplit[]; duration?: number; title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; styles?: ArcWidgetStyles | string; insetBorder?: unknown; segments?: ArcWidgetCalendarPlaceSegment[]; [key: string]: unknown; } export interface ArcWidgetCalendarCreateNew { item?: ArcWidgetCalendarPlaceItem | null; [key: string]: unknown; } export interface ArcWidgetCalendarWeekDragStart { dateFrom?: unknown; dateTo?: unknown; timeFrom?: unknown; timeTo?: unknown; } export interface ArcWidgetCalendarTimeEntry { id?: string | number; title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; dateFrom?: unknown; dateTo?: unknown; timeFrom?: unknown; timeTo?: unknown; styles?: ArcWidgetStyles | string; clickable?: boolean; draggable?: boolean; segments?: ArcWidgetCalendarPlaceSegment[]; tooltip?: { value?: ArcWidgetSlot; width?: string; maxWidth?: string; maxHeight?: string; position?: string; styles?: ArcWidgetStyles | string; [key: string]: unknown; }; [key: string]: unknown; } export interface ArcWidgetCalendarWeekData extends Omit { height?: string; styles?: ArcWidgetCalendarWeekStyles | string; weekdaySettings?: { week?: number; year?: number; [key: string]: unknown }; weekStart?: unknown; dateSettings?: ArcWidgetCalendarDateSettings; timeZoneBalance?: number | string; timeSettings?: ArcWidgetCalendarTimeSettings; currentTime?: string; lang?: string; timeSlotWidth?: string; timeSlotHeight?: string; allDayHeight?: string; createNew?: ArcWidgetCalendarCreateNew; timeEntries?: ArcWidgetCalendarTimeEntry[]; } export interface ArcWidgetCalendarWeekProps extends ArcReactWidgetProps { onEntryClick?: (entry_index: number) => void; onDayClick?: (date_millis: number) => void; onEntryDragSave?: (entry_index: number, start_data: ArcWidgetCalendarWeekDragStart) => void; onDailyEntryDragSave?: (entry_index: number, start_data: ArcWidgetCalendarWeekDragStart) => void; onCreate?: (values: { dateFrom?: number; dateTo?: number; timeFrom?: number; timeTo?: number; [key: string]: unknown; }) => 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 dargestellt wird**.
`timeSlotHeight: "80px",`
→ Jede Stunde erhält eine **Höhe von 50 Pixeln** in der vertikalen Zeitachse. 📌 **Hinweis:** - Der Standardwert liegt bei etwa **40 px**. - Ein höherer Wert sorgt für **mehr Raum pro Stunde.** ### 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 anzeigen lassen – ähnlich dem Stil des Apple Kalenders. Das Feature ist **standardmäßig aktiv** und muss nur deaktiviert werden, wenn du es nicht möchtest. ```javascript insetBorder: { enabled: true, // Standard: true – false blendet die Linie aus position: "left", // "left" (Standard) oder "right" width: "2px", // Breite der Linie stroke: "dotted", // "solid", "dashed" oder "dotted" color: "#ffffff" // Farbe (Standard: Schriftfarbe des Eintrags) } ``` 📌 **Hinweis:** - 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. - Du kannst `insetBorder` sowohl auf Ebene des `timeEntry` als auch pro Segment definieren – das Segment-Level überschreibt das Entry-Level. ### 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. 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 { ArcWidgetCalendarGrid } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const locale = lang === 'en' ? 'en-US' : lang === 'es' ? 'es-ES' : 'de-DE'; const chrome = { backgroundColor: '#ffffff', fontColor: '#18181b', mutedColor: '#52525b', subtleColor: '#71717a', borders: { color: '#e4e4e7', outer: true, columns: true, rows: true }, }; const look = (backgroundColor: string, color: string) => ({ backgroundColor, color, fontColor: color, }); type Entry = { id: string; title: string; dateFrom: number; dateTo: number; timeFrom: number; timeTo: number; clickable: true; draggable: true; styles: ReturnType; }; const event = ( id: string, title: string, day: number, backgroundColor: string, color: string, ): Entry => ({ id, title, dateFrom: new Date(2026, 8, day).getTime(), dateTo: new Date(2026, 8, day).getTime(), timeFrom: 9 * 3600000, timeTo: 11 * 3600000, clickable: true, draggable: true, styles: look(backgroundColor, color), }); const demoEntries: Entry[] = [ event( 'travel-out', t('Anfahrt', 'Travel there', 'Ida'), 16, '#99f6e4', '#115e59', ), event( 'visit', t('Hauptarbeit', 'On site', 'Trabajo principal'), 17, '#ccfbf1', '#115e59', ), event( 'travel-back', t('Abfahrt', 'Travel back', 'Vuelta'), 18, '#99f6e4', '#115e59', ), event( 'review', t('Projekt-Review', 'Project review', 'Revisión'), 21, '#ede9fe', '#5b21b6', ), event( 'ship', t('Lieferung', 'Delivery', 'Entrega'), 24, '#fef3c7', '#92400e', ), ]; const busyEntries: Entry[] = [ ...demoEntries, event('kickoff', t('Kickoff', 'Kickoff', 'Kickoff'), 3, '#d1fae5', '#065f46'), event('fair', t('Messe', 'Fair', 'Feria'), 8, '#ffe4e6', '#9f1239'), event('retro', t('Retro', 'Retro', 'Retro'), 28, '#e0f2fe', '#075985'), ]; export default function Example() { const [month, setMonth] = useState(9); const [entries, setEntries] = useState( pack === 'empty' ? [] : pack === 'busy' ? busyEntries : demoEntries, ); const [selected, setSelected] = useState( t( 'Termin ziehen oder anklicken', 'Drag or click an event', 'Arrastra o pulsa un evento', ), ); return ( <> setSelected(new Date(date).toLocaleDateString(locale)) } onEntryClick={(index) => setSelected(entries[index]?.title ?? selected)} onEntryDragSave={(index, start) => { setEntries((previous) => previous.map((entry, i) => i === index ? { ...entry, dateFrom: Number(start.dateFrom ?? entry.dateFrom), dateTo: Number(start.dateTo ?? entry.dateTo), } : entry, ), ); setSelected(t('Termin verschoben', 'Event moved', 'Evento movido')); }} />

{selected}

); } ``` ## Features ausprobieren ### Monatsnavigation, Tagesauswahl und JSX Wechsle auch über den Jahreswechsel und wähle einen Tag aus. Der Beispieltermin bleibt am 17. September 2026; andere Monate sind bewusst leer. ```tsx import { useState } from 'react'; import { ArcWidgetCalendarGrid, ArcWidgetBadge, } from '@arcrider/arcwidgets-react'; export default function Example() { const [offset, setOffset] = useState(0); const [custom, setCustom] = useState(false); const [selected, setSelected] = useState('Keine Auswahl'); const month = new Date(2026, 8 + offset, 1); return ( <> ) : undefined, dateFrom: new Date(2026, 8, 17).getTime(), dateTo: new Date(2026, 8, 17).getTime(), timeFrom: 10 * 3600000, timeTo: 11 * 3600000, clickable: true, draggable: false, }, ], }} onDayClick={(date) => setSelected(new Date(date).toLocaleDateString('de-DE')) } onEntryClick={() => setSelected('Projekt-Review')} />

Auswahl: {selected}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### 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 Index des Termins, bei Timeline zusätzlich der Ressourcenzeile. ```typescript onEntryClick?: (entry_index: number) => void; ``` ### onDayClick Datum des gewählten Tages als Millisekunden-Zeitstempel. ```typescript onDayClick?: (date_millis: number) => void; ``` ### onEntryDragSave Drag-Ende mit ursprünglichen Daten. Keine vollständigen neuen Zielzeiten; siehe Hinweise zur Persistenz. ```typescript onEntryDragSave?: (entry_index: number, start_data: ArcWidgetCalendarGridDragStart) => void; ``` ### onDailyEntryDragSave Drag-Ende eines ganztägigen Termins mit ursprünglichen Daten. ```typescript onDailyEntryDragSave?: (entry_index: number, start_data: ArcWidgetCalendarGridDragStart) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### timeZoneBalance Korrektur in Stunden; die Beispiele verwenden `0` und lokal erzeugte Tageswerte. Typ: `number | string`. ### styles Gemeinsame Farben, Abstände und Rahmen; die angebotenen Felder sind widgetspezifisch. Typ: `ArcWidgetCalendarGridStyles | string`. ### dateSettings Datumsbereich, Kopfzeilen und Markierungen. Typ und verschachtelte Felder stehen in `ArcWidgetCalendarGridData` unter Datentypen. ### timeSettings Zeitbereich und Raster; Einheiten sind Millisekunden. Typ: `Record`. ### lang Sprache der Kalenderbeschriftungen. Typ: `string`. ### timeEntries Termine mit Datumswerten, Uhrzeiten und Inhalten. Typ und verschachtelte Felder stehen in `ArcWidgetCalendarGridData` unter Datentypen. ## Verhalten und Grenzen - Für JSX in Terminen verwende `customLayout`. Die Standardfelder `title`, `subtitle` und `value` werden als Text oder HTML verarbeitet. - Datumswerte sind Zeitstempel in Millisekunden; Uhrzeiten sind Millisekunden seit Tagesbeginn. 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. - Die derzeitigen Drag-Callbacks enthalten den ursprünglichen Stand (`start_data`), jedoch keine vollständigen neuen Zielzeiten. Das Beispiel setzt deshalb `draggable: false`. Bearbeite Termine über React-State, bis ein vollständiger Persistenzvertrag für Drag & Drop vorliegt. - `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`, `ArcWidgetStyles` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetCalendarGridDay { clickable?: boolean; height?: string; alignX?: string; backgroundColor?: string; hoverBackgroundColor?: string; padding?: string; [key: string]: unknown; } export interface ArcWidgetCalendarGridDragStart { dateFrom?: unknown; dateTo?: unknown; timeFrom?: unknown; timeTo?: unknown; } export type ArcWidgetCalendarGridBorderLine = boolean | { color?: string; width?: string; borderColor?: string; borderWidth?: string; borderSize?: string; enabled?: boolean; }; export interface ArcWidgetCalendarGridBorders { color?: string; width?: string; outer?: ArcWidgetCalendarGridBorderLine; columns?: ArcWidgetCalendarGridBorderLine; rows?: ArcWidgetCalendarGridBorderLine; } export interface ArcWidgetCalendarGridStyles extends ArcWidgetStyles { borders?: ArcWidgetCalendarGridBorders; headerBackgroundColor?: string; mutedColor?: string; subtleColor?: string; } export interface ArcWidgetCalendarGridData extends Omit { height?: string; timeZoneBalance?: number | string; styles?: ArcWidgetCalendarGridStyles | string; dateSettings?: { month?: number; year?: number; day?: ArcWidgetCalendarGridDay; highlights?: Array<{ date?: unknown; color?: string; [key: string]: unknown }>; [key: string]: unknown; }; timeSettings?: Record; lang?: string; timeEntries?: Array<{ id?: string | number; title?: string; subtitle?: string; value?: string; customLayout?: ArcWidgetSlot; dateFrom?: unknown; dateTo?: unknown; timeFrom?: unknown; timeTo?: unknown; styles?: ArcWidgetStyles | string; clickable?: boolean; draggable?: boolean; [key: string]: unknown; }>; } export interface ArcWidgetCalendarGridProps extends ArcReactWidgetProps { onEntryClick?: (entry_index: number) => void; onDayClick?: (date_millis: number) => void; onEntryDragSave?: (entry_index: number, start_data: ArcWidgetCalendarGridDragStart) => void; onDailyEntryDragSave?: (entry_index: number, start_data: ArcWidgetCalendarGridDragStart) => 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 ```tsx import { ArcWidgetCalendarTimeline } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const day = (value: number) => new Date(2026, 8, value).getTime(); const chrome = { backgroundColor: '#ffffff', fontColor: '#18181b', mutedColor: '#52525b', subtleColor: '#71717a', headerBackgroundColor: '#fafafa', groupBackgroundColor: '#f4f4f5', rowBackgroundColor: '#ffffff', borders: { outer: true, rows: true, columns: false, color: '#e4e4e7' }, dayStripes: { odd: '#ffffff', even: '#f4f4f5' }, }; const look = (backgroundColor: string, color: string) => ({ backgroundColor, color, fontColor: color, }); type Bar = { id: string; title: string; dateFrom: number; dateTo: number; clickable: true; draggable: true; styles: ReturnType; }; type Row = { id: string; title: string; timeEntries: Bar[]; }; const bar = ( id: string, title: string, from: number, to: number, backgroundColor: string, color: string, ): Bar => ({ id, title, dateFrom: day(from), dateTo: day(to), clickable: true, draggable: true, styles: look(backgroundColor, color), }); const demoRows: Row[] = [ { id: 'team-a', title: t('Team Montage', 'Install team', 'Equipo montaje'), timeEntries: [ bar( 'job-a', t('Installation', 'Installation', 'Instalación'), 16, 18, '#d1fae5', '#065f46', ), ], }, { id: 'team-b', title: t('Kundentermin', 'Site visit', 'Visita'), timeEntries: [ bar( 'job-b-out', t('Anfahrt', 'Travel there', 'Ida'), 16, 16, '#99f6e4', '#115e59', ), bar( 'job-b', t('Hauptarbeit', 'On site', 'Trabajo principal'), 17, 17, '#ccfbf1', '#115e59', ), bar( 'job-b-back', t('Abfahrt', 'Travel back', 'Vuelta'), 18, 18, '#99f6e4', '#115e59', ), ], }, ]; const busyRows: Row[] = [ ...demoRows, { id: 'team-c', title: t('Team Messe', 'Fair team', 'Equipo feria'), timeEntries: [ bar( 'job-c', t('Aufbau', 'Setup', 'Montaje'), 15, 17, '#ede9fe', '#5b21b6', ), bar( 'job-d', t('Abbau', 'Teardown', 'Desmontaje'), 22, 24, '#ffe4e6', '#9f1239', ), ], }, ]; const emptyRows: Row[] = demoRows.map((row) => ({ ...row, timeEntries: [] })); export default function Example() { const [rows, setRows] = useState( pack === 'empty' ? emptyRows : pack === 'busy' ? busyRows : demoRows, ); const [selected, setSelected] = useState( t( 'Balken ziehen oder zoomen', 'Drag bars or zoom days', 'Arrastra barras o haz zoom', ), ); return ( <> ({ id: row.id, columns: [{ title: row.title, clickable: true }], timeEntries: row.timeEntries, })), }} onEntryClick={(rowIndex, entryIndex) => setSelected( rows[rowIndex]?.timeEntries[entryIndex]?.title ?? selected, ) } onLabelClick={(rowIndex) => setSelected(rows[rowIndex]?.title ?? selected) } onEntryDragSave={(rowIndex, entryIndex, start, rowChange) => { setRows((previous) => { const next = previous.map((row) => ({ ...row, timeEntries: [...row.timeEntries], })); const [item] = next[rowIndex].timeEntries.splice(entryIndex, 1); next[rowChange.row_index ?? rowIndex].timeEntries.push({ ...item, dateFrom: Number(start.dateFrom ?? item.dateFrom), dateTo: Number(start.dateTo ?? item.dateTo), }); return next; }); setSelected(t('Balken verschoben', 'Bar moved', 'Barra movida')); }} />

{selected}

); } ``` ## 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. ```tsx import { useState } from 'react'; import { ArcWidgetCalendarTimeline, ArcWidgetBadge, } from '@arcrider/arcwidgets-react'; export default function Example() { const [days, setDays] = useState(14); const [nested, setNested] = useState(true); const [stripes, setStripes] = useState(true); const [selected, setSelected] = useState('Ressource oder Balken anklicken'); const entry = (id: string, title: string, from: number, to: number) => ({ id, title, dateFrom: new Date(2026, 8, from).getTime(), dateTo: new Date(2026, 8, to).getTime(), clickable: true, draggable: false, customLayout: ( ), }); return ( <> setSelected(`Balken ${index + 1}, Ressourcenzeile ${row + 1}`) } onLabelClick={(row) => setSelected(`Ressourcenzeile ${row + 1}`)} />

{selected}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### 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 Index des Termins, bei Timeline zusätzlich der Ressourcenzeile. ```typescript onEntryClick?: (row_index: number, entry_index: number) => void; ``` ### onEntryDragSave Drag-Ende mit ursprünglichen Daten. Keine vollständigen neuen Zielzeiten; siehe Hinweise zur Persistenz. ```typescript onEntryDragSave?: ( row_index: number, entry_index: number, start_data: ArcWidgetCalendarTimelineDragStart, row_change: ArcWidgetCalendarTimelineRowChange ) => void; ``` ### onLabelClick Indizes der geklickten Ressourcenzeile und Beschriftungsspalte. ```typescript onLabelClick?: (row_index: number, column_index: number) => void; ``` ### onGhostClick Position des angeklickten freien Bereichs. ```typescript onGhostClick?: (row_index: number, column_value: number) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### timeZoneBalance Korrektur in Stunden; die Beispiele verwenden `0` und lokal erzeugte Tageswerte. Typ: `number | string`. ### dateSettings Datumsbereich, Kopfzeilen und Markierungen. Typ und verschachtelte Felder stehen in `ArcWidgetCalendarTimelineData` unter Datentypen. ### columns Linke Beschriftungsspalten der Ressourcen. Typ: `ArcWidgetCalendarTimelineColumn[]`. ### items Einträge der Auswahl oder der Darstellung; siehe den zugehörigen Datentyp. Typ: `ArcWidgetCalendarTimelineItem[]`. ### dayWidth Breite eines Tages auf der Zeitachse. Typ: `string`. ### zoomSettings Zoomgrenzen und Bedienelemente. Typ und verschachtelte Felder stehen in `ArcWidgetCalendarTimelineData` unter Datentypen. ### timeSettings Zeitbereich und Raster; Einheiten sind Millisekunden. Typ: `Record`. ### columnWidth Breite des linken Spaltenbereichs. Typ: `string`. ### lang Sprache der Kalenderbeschriftungen. Typ: `string`. ### styles Gemeinsame Farben, Abstände und Rahmen; die angebotenen Felder sind widgetspezifisch. Typ: `ArcWidgetCalendarTimelineStyles | string`. ### borders Historische Rahmenkonfiguration. Bevorzuge `styles.borders`, sofern vom Widget angeboten. Typ: `ArcWidgetCalendarTimelineBorders`. ### borderRadius Abrundung der Ecken. Typ: `string`. ## Verhalten und Grenzen - Für JSX in Terminen verwende `customLayout`. Die Standardfelder `title`, `subtitle` und `value` werden als Text oder HTML verarbeitet. - Datumswerte sind Zeitstempel in Millisekunden; Uhrzeiten sind Millisekunden seit Tagesbeginn. 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. - Die derzeitigen Drag-Callbacks enthalten den ursprünglichen Stand (`start_data`), jedoch keine vollständigen neuen Zielzeiten. Das Beispiel setzt deshalb `draggable: false`. Bearbeite Termine über React-State, bis ein vollständiger Persistenzvertrag für Drag & Drop vorliegt. - `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`, `ArcWidgetStyles` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export type ArcWidgetCalendarTimelineBorderLine = boolean | { color?: string; width?: string; borderColor?: string; borderWidth?: string; borderSize?: string; enabled?: boolean; }; export interface ArcWidgetCalendarTimelineBorders { color?: string; width?: string; outer?: ArcWidgetCalendarTimelineBorderLine; columns?: ArcWidgetCalendarTimelineBorderLine; rows?: ArcWidgetCalendarTimelineBorderLine; } export interface ArcWidgetCalendarTimelineStyles extends ArcWidgetStyles { borders?: ArcWidgetCalendarTimelineBorders; headerBackgroundColor?: string; groupBackgroundColor?: string; rowBackgroundColor?: string; mutedColor?: string; subtleColor?: string; dayStripes?: boolean | { enabled?: boolean; odd?: string; even?: string; oddColor?: string; evenColor?: string; }; } export interface ArcWidgetCalendarTimelineColumn { title?: ArcWidgetSlot; width?: string; backgroundColor?: string; fontColor?: string; fontSize?: string; fontWeight?: string | number; [key: string]: unknown; } export interface ArcWidgetCalendarTimelineItemColumn { title?: ArcWidgetSlot; subtitle?: ArcWidgetSlot; clickable?: boolean; alignX?: string; alignY?: string; fontSize?: string; fontWeight?: string | number; [key: string]: unknown; } export interface ArcWidgetCalendarTimelineGhost { title?: ArcWidgetSlot; clickable?: boolean; position?: 'bottomRight' | 'timeEntry' | string; styles?: ArcWidgetStyles | string; [key: string]: unknown; } export interface ArcWidgetCalendarTimelineEntryLabel { value?: string | number; width?: string | number; } export interface ArcWidgetCalendarTimelineTimeEntry { id?: string | number; title?: string | ArcWidgetCalendarTimelineEntryLabel; subtitle?: string | ArcWidgetCalendarTimelineEntryLabel; value?: string; customLayout?: ArcWidgetSlot; dateFrom?: unknown; dateTo?: unknown; timeFrom?: unknown; timeTo?: unknown; styles?: ArcWidgetStyles | string; clickable?: boolean; draggable?: boolean; tooltip?: { value?: ArcWidgetSlot; width?: string; maxWidth?: string; maxHeight?: string; position?: string; styles?: ArcWidgetStyles | string; [key: string]: unknown; }; [key: string]: unknown; } export interface ArcWidgetCalendarTimelineItem { id: string | number; title?: ArcWidgetSlot; subtitle?: ArcWidgetSlot; columns?: ArcWidgetCalendarTimelineItemColumn[]; timeEntries?: ArcWidgetCalendarTimelineTimeEntry[]; items?: ArcWidgetCalendarTimelineItem[]; highlights?: Array<{ date?: unknown; dateFrom?: unknown; dateTo?: unknown; timeFrom?: unknown; timeTo?: unknown; color?: string; [key: string]: unknown; }>; ghost?: ArcWidgetCalendarTimelineGhost; [key: string]: unknown; } export interface ArcWidgetCalendarTimelineDragStart { dateFrom?: unknown; dateTo?: unknown; timeFrom?: unknown; timeTo?: unknown; } export interface ArcWidgetCalendarTimelineRowChange { start_row_index: number; row_index: number; } export interface ArcWidgetCalendarTimelineData extends Omit { height?: string; timeZoneBalance?: number | string; dateSettings?: { header?: { title?: ArcWidgetSlot | { value?: ArcWidgetSlot; customLayout?: unknown; alignX?: string; fontSize?: string; fontColor?: string; fontWeight?: string | number; backgroundColor?: string; [key: string]: unknown; }; days?: { compactRanges?: boolean; monthCompactBelow?: number; [key: string]: unknown; }; months?: unknown; weeks?: unknown; alignX?: string; sticky?: string; stickyPadding?: string; [key: string]: unknown; }; highlights?: Array<{ date?: unknown; weekday?: number; color?: string; applyToRows?: boolean; styles?: ArcWidgetCalendarTimelineStyles | string; [key: string]: unknown; }>; [key: string]: unknown; }; columns?: ArcWidgetCalendarTimelineColumn[]; items?: ArcWidgetCalendarTimelineItem[]; dayWidth?: string; zoomSettings?: { enabled?: boolean; defaultDayWidth?: string; minDayWidth?: string; maxDayWidth?: string; step?: number | string; persist?: boolean; controls?: { enabled?: boolean; [key: string]: unknown }; [key: string]: unknown; }; timeSettings?: Record; columnWidth?: string; lang?: string; styles?: ArcWidgetCalendarTimelineStyles | string; borders?: ArcWidgetCalendarTimelineBorders; borderRadius?: string; } export interface ArcWidgetCalendarTimelineProps extends ArcReactWidgetProps { onEntryClick?: (row_index: number, entry_index: number) => void; onEntryDragSave?: ( row_index: number, entry_index: number, start_data: ArcWidgetCalendarTimelineDragStart, row_change: ArcWidgetCalendarTimelineRowChange ) => void; onLabelClick?: (row_index: number, column_index: number) => void; onGhostClick?: (row_index: number, column_value: 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 ```tsx import { ArcWidgetDevTools } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [enabled, setEnabled] = useState(true); return ( <> {enabled && ( setEnabled(false)} /> )} ); } ``` ## 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. ```tsx import { useState } from 'react'; import { ArcWidgetDevTools, ArcWidgetButton } from '@arcrider/arcwidgets-react'; export default function Example() { const [visible, setVisible] = useState(true); const [count, setCount] = useState(0); return ( <> setCount((n) => n + 1)} /> setVisible(false)} />

Aktueller Zähler: {count}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### 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 Nullbasierter Index des Footer-Buttons. ```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 Meldungstexte und optionale Footer-Buttons. 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. - Konfiguriere `errorPanel` mit Meldungen und optionalen Footer-Buttons. Nur als `clickable` markierte Buttons lösen `onFooterClick` aus. - Zeige nur Diagnosedaten, die für die jeweiligen Nutzer bestimmt sind. Schließen wird im Beispiel durch React-State umgesetzt. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetDevToolsErrorPanel { visible?: boolean; maxErrors?: number; debug?: Record; footerButtons?: Array<{ title?: string; clickable?: boolean; [key: string]: unknown }>; [key: string]: unknown; } export interface ArcWidgetDevToolsData extends ArcWidgetBaseData { 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'; import { useState } from 'react'; import type { ArcWidgetDrawingShape } from '@arcrider/arcwidgets-react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const demoShapes: ArcWidgetDrawingShape[] = [ { id: 'mark-1', clickable: true, shapeDataValue: { type: 'circle', cx: 140, cy: 120, r: 36, stroke: '#059669', strokeWidth: 3, fill: 'rgba(16,185,129,0.18)', }, }, { id: 'mark-2', clickable: true, shapeDataValue: { type: 'rect', x: 240, y: 80, width: 170, height: 100, rx: 12, stroke: '#d97706', strokeWidth: 3, fill: 'rgba(245,158,11,0.2)', }, }, ]; const busyShapes: ArcWidgetDrawingShape[] = [ ...demoShapes, { id: 'mark-3', clickable: true, shapeDataValue: { type: 'circle', cx: 460, cy: 250, r: 48, stroke: '#e11d48', strokeWidth: 3, fill: 'rgba(244,63,94,0.16)', }, }, { id: 'mark-4', clickable: true, shapeDataValue: { type: 'rect', x: 80, y: 230, width: 120, height: 70, rx: 10, stroke: '#7c3aed', strokeWidth: 3, fill: 'rgba(139,92,246,0.16)', }, }, ]; export default function Example() { const [shapes, setShapes] = useState( pack === 'empty' ? [] : pack === 'busy' ? busyShapes : demoShapes, ); const [selected, setSelected] = useState( t( 'Form ziehen oder skalieren', 'Drag or resize a shape', 'Arrastra o escala una forma', ), ); return ( <> setSelected(String(shape_id))} onShapeCreate={(payload, completion) => { const id = crypto.randomUUID(); setShapes((previous) => [ ...previous, { id, shapeDataValue: payload.shape_data_value }, ]); completion.promise = Promise.resolve(id); }} onShapeUpdate={(payload) => setShapes((previous) => previous.map((shape) => shape.id === payload.shape_id ? { ...shape, shapeDataValue: payload.shape_data_value } : shape, ), ) } />

{t('Formen', 'Shapes', 'Formas')}: {shapes.length} ·{' '} {t('Auswahl', 'Selection', 'Selección')}: {selected}

); } ``` ## Features ausprobieren ### Freihand zeichnen und Formen übernehmen Zeichne eigene Formen. Erstellen und Bearbeiten aktualisieren den React-State. ```tsx import { useState } from 'react'; import { ArcWidgetUpload, ArcWidgetDrawing } from '@arcrider/arcwidgets-react'; import type { ArcWidgetDrawingShape } from '@arcrider/arcwidgets-react'; export default function Example() { const [shapes, setShapes] = useState([]); const [exportUrl, setExportUrl] = useState(''); return ( <> setShapes([ { id: 'background', shapeDataValue: { type: 'image', href: file.src, x: 0, y: 0, width: file.width ?? 600, height: file.height ?? 400, }, }, ]) } /> { const id = crypto.randomUUID(); setShapes((s) => [ ...s, { id, shapeDataValue: p.shape_data_value, }, ]); completion.promise = Promise.resolve(id); }} onShapeUpdate={(p) => setShapes((s) => s.map((v) => v.id === p.shape_id ? { ...v, shapeDataValue: p.shape_data_value, } : v, ), ) } onExport={(p) => setExportUrl(p.base64)} /> {exportUrl && ( PNG herunterladen )} ); } ``` ### Markierungen speichern und wiederherstellen Wähle eine Markierung aus und teste die lokale Speicherung. ```tsx import { useState } from 'react'; import { ArcWidgetDrawing, ArcWidgetButton } from '@arcrider/arcwidgets-react'; import type { ArcWidgetDrawingShape } from '@arcrider/arcwidgets-react'; const initial: ArcWidgetDrawingShape[] = [ { id: 'pin-1', title: 'Prüfpunkt 1', clickable: true, shapeDataValue: { type: 'circle', cx: 150, cy: 120, r: 12, fill: '#34705b', stroke: '#34705b', }, }, ]; export default function Example() { const [shapes, setShapes] = useState(initial); const [status, setStatus] = useState('Bereit'); return ( <> setStatus(`Ausgewählt: ${p.shape_id}`)} onShapeUpdate={(p) => setShapes((s) => s.map((v) => v.id === p.shape_id ? { ...v, shapeDataValue: p.shape_data_value, } : v, ), ) } /> { try { localStorage.setItem('arc-demo-pins', JSON.stringify(shapes)); setStatus('Lokal gespeichert'); } catch { setStatus('Speichern fehlgeschlagen'); } }} />

{status}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### 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 Neue Form und ein veränderliches Completion-Objekt. Setze dessen `promise` auf die vergebene ID. ```typescript onShapeCreate?: (payload: ArcWidgetDrawingCreatePayload, completion: { promise?: Promise }) => void; ``` ### onShapeUpdate Geänderte Form und ihre ID. ```typescript onShapeUpdate?: (payload: { shape_id: string | number; shape_data_value: Record; bag?: unknown }) => void; ``` ### onShapeActivate ID der aktivierten Form. ```typescript onShapeActivate?: (payload: { shape_id: string | number }) => void; ``` ### onExport Exportformat und Bild als Data-URL. ```typescript onExport?: (payload: { format: string; base64: string }) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### canvas Arbeitsfläche in Pixeln. Typ: `{ width?: number; height?: number; [key: string]: unknown }`. ### artboard Zeichen- und Exportbereich in Pixeln. Typ: `{ width?: number; height?: number; [key: string]: unknown }`. ### shapes Formen mit stabiler `id` und Geometrie in `shapeDataValue`. Typ: `ArcWidgetDrawingShape[]`. ### exportSettings Exportformate und Ausgabeziel. Für den Callback derzeit `target: "field"` wählen. Typ: `Record`. ### create Erstellen neuer Formen mit `enabled` erlauben oder verhindern. Typ: `{ enabled?: boolean }`. ### tools Verfügbare Zeichenwerkzeuge. Typ: `Record[]`. ### stickerTypes Verfügbare Sticker. Typ: `Record[]`. ### drawingSettings Einstellungen für das Zeichnen. Typ: `Record`. ### zooming Grenzen und Schrittweite des Zooms. Typ: `{ min?: number; max?: number; step?: number }`. ## Verhalten und Grenzen - Jede Form braucht eine stabile `id` und `shapeDataValue` als Objekt oder serialisierten JSON-String. Das React-Widget normalisiert `id` für den Zeichenkern. - 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 Form. Ersetze die passende Form unveränderlich in deinem State. - Für `onExport` ist aktuell `exportSettings.allowedTypes: [{ format: "png", target: "field" }]` nötig. Der historische Wert `field` wählt den Data-URL-Callback; es wird kein Datenbankfeld geschrieben. `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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetDrawingShape { id: string | number; shapeDataValue?: string | Record; shapeId?: string | number; [key: string]: unknown; } export interface ArcWidgetDrawingData extends ArcWidgetBaseData { height?: string; canvas?: { width?: number; height?: number; [key: string]: unknown }; artboard?: { width?: number; height?: number; [key: string]: unknown }; shapes?: ArcWidgetDrawingShape[]; exportSettings?: Record; create?: { enabled?: boolean }; tools?: Record[]; stickerTypes?: Record[]; drawingSettings?: Record; zooming?: { min?: number; max?: number; step?: number }; } export interface ArcWidgetDrawingCreatePayload { shape_data_value: Record; sticker_uid?: string | null; } export interface ArcWidgetDrawingProps extends ArcReactWidgetProps { onShapeCreate?: (payload: ArcWidgetDrawingCreatePayload, completion: { promise?: Promise }) => void; onShapeUpdate?: (payload: { shape_id: string | number; shape_data_value: Record; bag?: unknown }) => void; onShapeActivate?: (payload: { shape_id: string | number }) => void; onExport?: (payload: { format: string; base64: string }) => void; } ``` ## Einsatzbeispiele - [Ein Bild annotieren und exportieren](/docs/react/examples/custom-drawing-zeichnen-im-bild-teil1) - [Markierungen auf einem Plan](/docs/react/examples/custom-drawing-teil-2-mangel-verortung-mit-pins) - [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 oder PDF's (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" }, { format: "pdf", mergeFile: false }], target: "file", recordId: Nr, fieldId: fieldId("Tabelle", "Feld für den Export"), mergeFile: "Feld mit der Originaldatei", apiKey: "API_KEY", }, 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, 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, PDF), wohin die Datei gespeichert werden soll und ob bei PDF ein bestehendes Dokument zusammengeführt wird. 💡 **Hinweis**: Konvertierung in eine vektorisierte PDF fallen Kosten an und es ist eine Verbindung zum Internet notwendig (funktioniert nicht im Offline Modus). Genaue Kosten erfragst du bitte bei dem arcRider Support.
Feld Beschreibung
`allowedTypes` Liste der erlaubten Exportformate. Du kannst `"jpg"`, `"png"`, `"svg"` und `"pdf"` angeben. Beim Format `"pdf"` kannst du zusätzlich `mergeFile: false` oder ein Feldname setzen, um eine bestehende PDF zu ergänzen.
`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.
`mergeFile` Nur für PDF-Exporte relevant. Wenn angegeben, wird die exportierte Zeichnung mit der PDF aus diesem Feld zusammengeführt.
`apiKey` Wird nur für den PDF-Export mit Zusammenführung benötigt. Hier wird ein API-Schlüssel von Arc Rider verwendet, um das PDF-Rendering serverseitig durchzuführen.
#### 🔄 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: "pdf", 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 oder Kreise, 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. 💡 **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) | **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. - `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 oder PDF's (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" }, { format: "pdf", mergeFile: false }], target: "file", recordId: Nr, fieldId: fieldId("Tabelle", "Feld für den Export"), mergeFile: "Feld mit der Originaldatei", apiKey: "API_KEY", }, 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, 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, PDF), wohin die Datei gespeichert werden soll und ob bei PDF ein bestehendes Dokument zusammengeführt wird. 💡 **Hinweis**: Konvertierung in eine vektorisierte PDF fallen Kosten an und es ist eine Verbindung zum Internet notwendig (funktioniert nicht im Offline Modus). Genaue Kosten erfragst du bitte bei dem arcRider Support.
Feld Beschreibung
`allowedTypes` Liste der erlaubten Exportformate. Du kannst `"jpg"`, `"png"`, `"svg"` und `"pdf"` angeben. Beim Format `"pdf"` kannst du zusätzlich `mergeFile: false` oder ein Feldname setzen, um eine bestehende PDF zu ergänzen.
`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.
`mergeFile` Nur für PDF-Exporte relevant. Wenn angegeben, wird die exportierte Zeichnung mit der PDF aus diesem Feld zusammengeführt.
`apiKey` Wird nur für den PDF-Export mit Zusammenführung benötigt. Hier wird ein API-Schlüssel von Arc Rider verwendet, um das PDF-Rendering serverseitig durchzuführen.
#### 🔄 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: "pdf", 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 oder Kreise, 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. 💡 **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) | **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. - `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 # 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 `"2px"`, typically set to `"10px"` or `"16px"`. - **height**: Use `"auto"` or `"100%"` depending on context. - **blocks**: Each block can have `width`, `height`, `alignX`, `alignY`. - **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: "", alignX: "center", alignY: "center", paddingY: "", paddingX: "", backgroundColor: "", lineHeight: "", styles: "", value: "" },{ blockId: "", width: "", height: "", 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: "", 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: "", // dafault: 2px ``` ### 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 ``` ## 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 ``` ### 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 ```tsx import { ArcWidgetBadge, ArcWidgetIcon, ArcWidgetKanban, ArcWidgetLayout, ArcWidgetText, } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const LANES = { neu: { id: 'neu', title: t('Neu', 'New', 'Nuevo'), borderColor: '#F59E0B', }, offer: { id: 'offer', title: t('Angebot verschickt', 'Offer sent', 'Oferta enviada'), borderColor: '#8B5CF6', }, won: { id: 'won', title: t('Gewonnen', 'Won', 'Ganado'), borderColor: '#22C55E', }, lost: { id: 'lost', title: t('Verloren', 'Lost', 'Perdido'), borderColor: '#E04F5F', }, } as const; type LaneId = keyof typeof LANES; type Deal = { id: string; title: string; company: string; value: string; chance: number; overdue?: boolean; }; const deal = ( id: string, title: string, company: string, value: string, chance: number, overdue = false, ): Deal => ({ id, title, company, value, chance, overdue }); const demoDeals: Record = { neu: [ deal( '1', t('Website-Relaunch', 'Website relaunch', 'Rediseño web'), 'Müller GmbH', '18.400,00 €', 20, true, ), deal( '2', t('Erstgespräch', 'First call', 'Primera llamada'), 'Stahlwerk West', '6.200,00 €', 10, ), deal( '7', t('Shop-Anbindung', 'Shop integration', 'Integración tienda'), 'Nordlicht KG', '9.600,00 €', 15, ), deal( '8', t('App-Redesign', 'App redesign', 'Rediseño app'), 'Bauer AG', '24.100,00 €', 25, ), ], offer: [ deal( '3', t('CRM-Einführung', 'CRM rollout', 'Implantación CRM'), 'Globex Inc.', '42.000,00 €', 60, ), deal( '4', t('Servicevertrag', 'Service contract', 'Contrato servicio'), 'Acme Corp', '12.800,00 €', 45, ), deal( '9', t('Schulungspaket', 'Training package', 'Paquete formación'), 'Wayne Ent.', '16.400,00 €', 55, ), ], won: [ deal( '5', t('Lizenzpaket', 'Licence pack', 'Paquete licencias'), 'Umbrella Co.', '96.500,00 €', 100, ), deal( '10', t('Wartung 24/7', 'Maintenance 24/7', 'Mantenimiento 24/7'), 'Stark Ind.', '54.000,00 €', 100, ), deal( '11', t('Schnittstellen', 'Integration', 'Integración'), 'Soylent Ltd.', '31.200,00 €', 100, ), ], lost: [ deal( '6', t('Pilotprojekt', 'Pilot project', 'Proyecto piloto'), 'Initech', '8.900,00 €', 0, ), deal( '12', t('Workshop-Tag', 'Workshop day', 'Día de taller'), 'Pied Piper', '4.800,00 €', 0, ), ], }; const extraDeals: Record = { neu: [ deal( '13', t('Newsletter-Relaunch', 'Newsletter relaunch', 'Rediseño newsletter'), 'Hooli', '7.400,00 €', 18, ), deal( '14', t('Messestand', 'Fair booth', 'Stand feria'), 'InGen', '11.200,00 €', 22, ), ], offer: [ deal( '15', t('Cloud-Migration', 'Cloud migration', 'Migración cloud'), 'Cyberdyne', '38.000,00 €', 50, ), deal( '16', t('Support-Plus', 'Support plus', 'Soporte plus'), 'Massive Dyn.', '9.100,00 €', 40, ), ], won: [ deal( '17', t('Jahreslizenz', 'Annual licence', 'Licencia anual'), 'Pied Piper', '21.000,00 €', 100, ), ], lost: [ deal( '18', t('PoC intern', 'Internal PoC', 'PoC interno'), 'Hooli', '3.200,00 €', 0, ), ], }; const busyDeals: Record = { neu: [...demoDeals.neu, ...extraDeals.neu], offer: [...demoDeals.offer, ...extraDeals.offer], won: [...demoDeals.won, ...extraDeals.won], lost: [...demoDeals.lost, ...extraDeals.lost], }; const emptyDeals: Record = { neu: [], offer: [], won: [], lost: [], }; const deals = pack === 'empty' ? emptyDeals : pack === 'busy' ? busyDeals : demoDeals; const itemView = (item: Deal, borderColor: string) => ({ id: item.id, clickable: true, draggable: true, paddingX: '12px', paddingY: '10px', backgroundColor: '#ffffff', borderPosition: 'left', borderColor: item.overdue ? '#DC2626' : borderColor, borderWidth: '3px', title: ( {item.title} ), subtitle: ( {item.company}
{item.value}
), value: ( {item.chance}% ), }); const startLanes = (Object.keys(LANES) as LaneId[]).map((id) => ({ ...LANES[id], items: deals[id], })); const PLUS_BLOCK = 4; function LaneHeader({ title, count, dot, onAdd, }: { title: string; count: number; dot: string; onAdd: () => void; }) { return ( ), }, { width: 'auto', height: 'auto', value: ( ), }, { width: 'auto', height: 'auto', value: ( ), }, { width: 'fraction', height: '1px', value: '' }, { width: 'auto', height: 'auto', clickable: true, value: ( ), }, ], }} onBlockClick={(block_index) => { if (block_index === PLUS_BLOCK) onAdd(); }} /> ); } export default function Example() { const [lanes, setLanes] = useState(startLanes); const [selected, setSelected] = useState( t( 'Karte ziehen oder anklicken', 'Drag or click a card', 'Arrastra o pulsa una tarjeta', ), ); const addDeal = (laneId: LaneId) => { setLanes((current) => current.map((lane) => lane.id === laneId ? { ...lane, items: [ ...lane.items, deal( `${laneId}-${lane.items.length + 1}-${Date.now()}`, t('Neuer Deal', 'New deal', 'Nuevo trato'), t('Ohne Kontakt', 'No contact', 'Sin contacto'), '—', laneId === 'won' ? 100 : laneId === 'lost' ? 0 : 15, ), ], } : lane, ), ); setSelected( `${t('Deal angelegt in', 'Deal added to', 'Trato añadido a')} ${LANES[laneId].title}`, ); }; return ( <> ({ id: lane.id, title: ( addDeal(lane.id)} /> ), minWidth: '300px', fontColor: '#111827', backgroundColorHeader: '#FFFFFF', backgroundColorBody: '#F4F5F7', showAmount: false, empty: true, items: lane.items.map((item) => itemView(item, lane.borderColor)), })), }} onItemClick={(_event, { item_index }) => setSelected(`${t('Deal', 'Deal', 'Trato')} ${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(t('Karte verschoben', 'Card moved', 'Tarjeta movida')); }} />

{selected}

); } ``` ## Features ausprobieren ### Gruppen, JSX-Karten und leere Spalten Schalte die Gruppierung um und klicke auf eine Karte. Das ergänzende Drag-Beispiel darunter zeigt die Speicherung mit Fehlerbehandlung. ```tsx import { useState } from 'react'; import { ArcWidgetKanban, ArcWidgetBadge } from '@arcrider/arcwidgets-react'; export default function Example() { const [grouped, setGrouped] = useState(true); const [selected, setSelected] = useState('Keine Auswahl'); const task = (id: string, title: string) => ({ id, title, subtitle: ( ), clickable: true, draggable: false, }); return ( <> setSelected( `Spalte ${p.swimlane_index + 1} · Gruppe ${p.section_index + 1} · Karte ${p.item_index_in_section + 1}`, ) } />

{selected}

); } ``` ### Drag & Drop, Speichern und Rollback Verschiebe die Karte und aktiviere anschließend den simulierten Speicherfehler. ```tsx import { useState } from 'react'; import { ArcWidgetKanban } from '@arcrider/arcwidgets-react'; const initial = [ { id: 'open', title: 'Offen', items: [ { id: 'a', title: 'Angebot prüfen', clickable: true, draggable: true, }, ], }, { id: 'done', title: 'Erledigt', items: [] as initialItem[], }, ]; type initialItem = { id: string; title: string; clickable: boolean; draggable: boolean; }; export default function Example() { const [lanes, setLanes] = useState(initial); const [fail, setFail] = useState(false); const [status, setStatus] = useState('Karte verschieben'); return ( <> { const next = lanes.map((lane) => ({ ...lane, items: [...lane.items], })); const [item] = next[p.source_swimlane_index].items.splice( p.item_index_in_section, 1, ); next[p.target_swimlane_index].items.splice( p.target_index_in_section, 0, item, ); setStatus('Speichert …'); try { if (fail) throw new Error('Simulierter Speicherfehler'); localStorage.setItem('arc-demo-board', JSON.stringify(next)); setLanes(next); setStatus('Lokal gespeichert'); } catch (error) { setStatus('Speichern fehlgeschlagen; Karte zurückgesetzt.'); throw error; } }} />

{status}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetKanbanData`. Ü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). ### onItemClick Browser-Event und Position der Karte. ```typescript onItemClick?: (event: Event, payload: ArcWidgetKanbanItemClickPayload) => void; ``` ### onItemDrop Neue Position einer verschobenen Karte. Ein zurückgegebenes Speicher-Promise hält den Vorgang bis zum Abschluss offen; Ablehnung setzt den Wechsel zurück. ```typescript onItemDrop?: (payload: ArcWidgetKanbanItemDropPayload) => void | Promise; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### empty Anzeige für leere Spalten; `true` oder eine eigene Beschriftung und Farben. Typ: `boolean | string | ArcWidgetKanbanEmpty`. ### swimlanes Spalten mit flachen `items` oder benannten `sections`. Typ: `ArcWidgetKanbanSwimlane[]`. ### scrollSettings Scrollverhalten des Inhaltsbereichs. Typ und verschachtelte Felder stehen in `ArcWidgetKanbanData` unter Datentypen. ### showScrollbar Sichtbarkeit des Scrollbalkens. Typ: `boolean`. ### collapsible Aufklappen bzw. Einklappen; das konkrete Format steht im Typ. Typ: `boolean`. ## Verhalten und Grenzen - Eine Spalte enthält entweder flache `items` oder benannte `sections` mit eigenen `items`. Gruppen brauchen eine stabile `id`. - `onItemClick` liefert zuerst das Browser-Event, dann Indizes für Spalte, Gruppe und Karte. Diese Indizes beziehen sich auf die übergebene Datenstruktur. - Aktiviere `draggable` nur mit einem passenden `onItemDrop`. Gib dessen Speicher-Promise zurück; ein abgelehntes Promise bricht den optimistischen Wechsel ab. Aktualisiere nach erfolgreichem Speichern deinen State. - Die leere Spaltenanzeige aktivierst du mit `empty: true` oder einer eigenen Konfiguration. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetKanbanItemClickPayload { swimlane_index: number; section_index: number; item_index: number; item_index_in_section: number; } export interface ArcWidgetKanbanItemDropPayload { source_swimlane_index: number; target_swimlane_index: number; item_index: number; source_section_index: number; target_section_index: number; item_index_in_section: number; target_index_in_section: number; } export interface ArcWidgetKanbanItem { id?: string | number; title?: ArcWidgetSlot; subtitle?: ArcWidgetSlot; value?: ArcWidgetSlot; clickable?: boolean; draggable?: boolean; paddingX?: string; paddingY?: string; borderPosition?: string; borderColor?: string; borderWidth?: string; } export interface ArcWidgetKanbanSection { id: string; title?: ArcWidgetSlot; items?: ArcWidgetKanbanItem[]; hideWhenEmpty?: boolean; collapsed?: boolean; [key: string]: unknown; } export interface ArcWidgetKanbanEmpty { enabled?: boolean; value?: ArcWidgetSlot; backgroundColor?: string; fontColor?: string; fontWeight?: string | number; } export interface ArcWidgetKanbanSwimlane { id?: string | number; title?: ArcWidgetSlot; minWidth?: string; empty?: boolean | string | ArcWidgetKanbanEmpty; items?: ArcWidgetKanbanItem[]; sections?: ArcWidgetKanbanSection[]; scrollY?: { scrollbar?: { enabled?: boolean }; [key: string]: unknown }; [key: string]: unknown; } export interface ArcWidgetKanbanData extends Omit { height?: string; empty?: boolean | string | ArcWidgetKanbanEmpty; swimlanes?: ArcWidgetKanbanSwimlane[]; scrollSettings?: { scrollX?: { scrollbar?: { enabled?: boolean }; [key: string]: unknown } | boolean; scrollY?: { scrollbar?: { enabled?: boolean }; [key: string]: unknown } | boolean; [key: string]: unknown; }; showScrollbar?: boolean; collapsible?: boolean; } export type ArcWidgetKanbanProps = ArcReactWidgetProps & { onItemClick?: (event: Event, payload: ArcWidgetKanbanItemClickPayload) => void; onItemDrop?: (payload: ArcWidgetKanbanItemDropPayload) => void | Promise; }; ``` ## Einsatzbeispiele - [Kanban speichern und Fehler behandeln](/docs/react/examples/react-kanban-persistenz) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Kanban # Custom Kanban ## AI Defaults (read first) - **uniqueId**: Required, `"kanban-" + Nr`. - **height**: Use `"100%"` when embedded in a dashboard layout. - **Swimlane minWidth**: Set `minWidth: "300px"` for readable swimlanes. - **recordId**: Use raw `Nr` in swimlanes, NEVER `number(Nr)`. Stays on the formula; core never reads it. Drop handler looks up `ui.data.swimlanes[target_swimlane_index].recordId`. - **fieldId**: Use `fieldId(Nr, "Feldname")`, NOT the display name as string. Drop persistence is Ninox-handler only — core does not read `fieldId`. - **items mapping**: Use `.[{ itemId: Nr, ... }]` with `Nr` (not `number(Nr)`). `itemId` stays on the formula. Core emits swimlane/section/item **indices**; the handler reads `itemId` / `fieldId` from `ui.data.swimlanes[i]`. - **sections** (optional pro Swimlane): `[{ id: "…", title: "…", items: [ … ], hideWhenEmpty: false, collapsed: false }]`. Ohne `sections` oder leer → flaches `items` wie bisher (eine implizite Gruppe, kein Gruppenkopf). `id` ist Pflicht sobald Gruppen genutzt werden (State). - **scrollSettings**: Root: `scrollX` / `scrollY` mit `scrollbar: { enabled: boolean }` fuer das gesamte Kanban (ersetzt/ergaenzt `showScrollbar` fuer X). Pro Swimlane: `scrollY.scrollbar.enabled` fuer vertikales Scrollen in der Spalte. - **dragAction**: Needs `fieldId` (field ID) for the status field being updated on drag. Adapter-only. - **Persistence**: Card drag saves in the **Ninox handler** (`type: "update"` from `item.fieldId` + target swimlane `recordId`). React hosts: `onItemDrop` / `onAction('item_drop')` — **no** global `database` stub. - **clickable**: Core gated nur darüber. Ninox: `clickAction: false` → nicht klickbar; sonst Default-Popup über `itemId`. - **Filter values**: Extract filter variables BEFORE the `(select ...)` query, never inline. - **Sections with different data**: Use `for section in sections do...end`, NOT a static array. - **empty**: Off by default. Opt-in pill: `empty: { enabled: true, value: "Keine Einträge", backgroundColor: "#fff", fontColor: "#888", fontWeight: 400 }` on the board or a swimlane. # Custom Kanban ![](https://framerusercontent.com/images/mHlFDpfOkhxieybe820lNfJs.png) Das Kanban stellt Daten einer Tabelle (Kanban Karten) in einem bestimmten Status (in den Swimlanes) dar. Dafür legst du wie gewöhnlich eine Variable ***data*** an, die du dann in der globalen Funktion ***arcCustomKanban*** ausgibst. Es gibt zwei Möglichkeiten ein Kanban anzulegen: - **dynamisch** (Swimlanes beziehen sich auf eine Liste/Tabelle/for-Schleife) - **manuell** (Swimlanes werden manuell definiert) ## Dynamischer Anwendungscode ```javascript let current := this; let listFirmen := (select Firmen); let listStates := (select Status_Leads); let data := { uniqueId: "Kanban individuell", height: "600px", collapsible: true, showScrollbar: true, swimlanes: listStates.[{ title: Titel, recordId: number(Nr), backgroundColorHeader: text(color(Farbe)), backgroundColorBody: "", fontColor: "", minWidth: "", amount: let id := Nr; cnt(listFirmen[number(Status) = number(id)]), items: let id := Nr; listFirmen[number(Status) = number(id)].[{ itemId: Nr, fieldId: "D", clickAction: false, dragAction: true, paddingX:", paddingY: "", borderPosition: "", borderColor: "", borderWidth: "", title: Name, subtitle: "Kunden-Nr.:" + Nr, value: arcCustomProgressBar({ uniqueId: "progress" + Nr, title: "", width: "100%", fontSize: "", fontColor: "", backgroundColor: "", progressColor: "", valueTotal: stateTotal, valueProgress: stateCurrent, valueText: stateCurrent + "/" + stateTotal }) }] }] }; arcCustomKanban(data) ``` ### Variablen einstellen ```javascript let current := this; // Definiert diesen Record let listFirmen := (select Firmen); // Definiert die Tabelle für die darzustellenden Inhalte let listStates := (select Status_Leads); // Definiert die Liste für die Swimlanes ``` ### Allgemeine Einstellungen in *data* ***uniqueId*** ist die individuelle Bezeichnung deines Kanbans. Achte darauf, dass du hier einen einzigartigen Titel vergibst. Das ist wichtig, falls du mehrere Kanbans auf einer Seite darstellen möchtest und deine Style-Einstellungen sich nicht überschreiben. ```javascript uniqueId: "Kanban individuell", ``` ***height*** defineirt die Höhe deines Kanbans. ```javascript height: "600px", // Pixel-Werte height: "auto", // Passt die Höhe an die Inhalte an. height: "100%", // Passt sich der Höhe des Funktionsfeldes an. height: "", // Fallback-Wert: 100% ``` Mit ***collapsible*** kannst du einstellen, dass der User die Swimlanes über einen Klick auf das Einklapp-Icon (caret-up) ein- oder aufklappen kann. ```javascript collapsible: true, // Aktiviert die Funktion collapsible: "", // Fallback-Wert = "false" ``` Mit ***showScrollbar*** kann die horizontale Scrollbar für das gesamte Kanban eingeblendet werden. Dies hilft vor allem Usern ohne Trackpad oder Tablet. ```javascript showScrollbar: true, // Scrollbar wird eingeblendet showScrollbar: false, // keine Anzeige der Scrollbar showScrollbar: "", // default: false ``` ### Swimlane Einstellungen in *data* Der Parameter swimlane definiert die Darstellung deiner einzelnen Spalten deines Kanbans. Diese werden innerhalb der eckigen und geschwungenen Klammer definiert. ```javascript swimlanes: listStates.[{ }] ``` ***title*** definiert den Titel deiner jeweiligen Swimlane. Dieser kann in diesem Beispiel der Titel aus der Tabelle listStates sein - ist also dynamisch. ```javascript title: Titel, // Ninox-Feld im Textformat. title: "Hans" // Text in ", wenn das Kanban manuell angelegt ist title: "" // Kein Fallback hinterlegt. ``` ***recordId*** definiert den Datensatz aus der dynamischen Liste, die du vorher definiert hast. Hier: listStates. ```javascript recordId: number(Nr), // Nr des jeweiligen Records aus der Liste ``` ***backgroundColorHeader*** definiert die Hintergrund-Farbe der Kopfzeile jeder Swimlane. ```javascript backgroundColorHeader: text(color(Farbe)), // Ninox Feld , das Farbe referenziert (HEX Wert ausgibt) backgroundColorHeader: "#000", // HEX Wert in " backgroundColorHeader: " ,// Fallback-Wert: #8594c1 ``` ***backgroundColorBody ***definiert die Hintergrund-Farbe Swimlane-Körpers (Body). ```javascript backgroundColorBody: text(color(Farbe)), // Ninox Feld , das Farbe referenziert (HEX Wert ausgibt) backgroundColorBody: "#000", // HEX Wert in " backgroundColorBody: "", // Fallback-Wert: #f4f7ff ``` ***fontColor*** definiert die Schriftfarbe für den Header. ```javascript fontColor: "#000", // Hex-Wert in " fontColor: "", // Fallback-Wert: #fff ``` ***minWidth*** gibt die minimale Breite der Swimlanes an. Das verhindert, dass die Swimlanes bei kleineren Bildschirmen immer kleiner gedrückt werden. Somit werden deine Inhalte immer gut lesbar dargestellt, da dann nach rechts gescrollt werden kann. ```javascript minWidth: "300px", // Pixel-Wert in " minWidth: "", // Fallback-Wert: 150px ``` ***empty*** blendet in leeren Spalten eine Pill ein. **Default: aus.** Am Board für alle Spalten, oder pro Swimlane (überschreibt das Board). `true` / `"true"` ist der alte Kurz-Schalter. ```javascript empty: { enabled: true, value: "Keine Einträge", backgroundColor: "#fff", fontColor: "#888888", fontWeight: 400 } empty: true empty: "" // Default: keine Pill ``` ***amount*** definiert, welche Anzahl in dem Swimlane Header angezeigt wird. Zahl, HTML-String oder verschachteltes Widget — wie `title`. Leer / weggelassen blendet die Bubble aus. Eine **Zahl** aktualisiert sich sofort beim Verschieben einer Karte. **HTML oder ein Widget** bleibt unverändert (kein automatischer Zähler) und kommt erst mit dem nächsten Ninox-Reload neu. ```javascript amount: let id := Nr; cnt(list[number(Status) = number(id)]), amount: "" + cnt(list[number(Status) = number(id)]) + "", amount: "" // Kein Fallback-Wert. Die Amount-Bubble wird in diesem Fall ausgeblendet. ``` ### Item Einstellungen (Cards) innerhalb von *swimlanes* Der Parameter items definiert die Darstellung deiner Karten innerhalb der Swimlane. Diese werden innerhalb der eckigen und geschwungenen Klammer definiert. ```javascript items: let id := Nr; listFirmen[number(Status) = number(id)].[{ }] ``` ***itemId*** definiert den Record der item-Liste. hier: listFirmen. ```javascript itemId: Nr, // Record-Id ``` ***fieldId*** definiert das Feld, das geändert werden soll, beim Wechsel der Swimlane. In den meisten Fällen ist das der Status. ```javascript fieldId: "D", // Field ID findest du mit dem arcFieldFinder heraus ``` ***clickAction*** steuert den Klick auf die Karte. Legacy-kompatibel: - `false` → kein Klick - `true` / weglassen / `""` → öffnet den Record per `popupRecord` (`itemId`) - Objekt (oder Array) → eine Action wie bei Calendar/Layout (z. B. `{ type: "popup", recordId: Nr }`) ```javascript clickAction: false, // Deaktiviert die Klick-Aktion clickAction: true, // Fallback: popupRecord(itemId) clickAction: { type: "popup", recordId: Nr }, clickAction: { type: "update", recordId: helperRecordId, fieldId: "A", value: Nr }, ``` ***actions*** (Array) — bevorzugter Weg für mehrere Aktionen beim Klick. Hat Vorrang vor `clickAction` (außer `clickAction: false`). Gleiche Action-Typen wie Calendar/Layout (`popup`, `update`, `openRecord`, `create`, …). ```javascript actions: [{ type: "popup", recordId: Nr }, { type: "update", recordId: helperRecordId, fieldId: "A", value: Nr }], ``` ***dragAction*** definiert, ob der User die Karte per Drag and Drop verschieben kann. ```javascript dragAction: false, // Deaktiviert die Drag & Drop Funktion. dragAction: "", // Fallback-Wert: true ``` ***paddingX*** definiert den Abstand von der Border zum Inhalt rechts und links innerhalb der Karte. ```javascript paddingX: "10px" // Pixel-Wert in " paddingX:", // Fallback-Wert: 12px ``` ***paddingY*** definiert den Abstand von der Border zum Inhalt oben und unten innerhalb der Karte. ```javascript paddingY: "10px" // Pixel-Wert in " paddingY: "", // Fallback-Wert: 12px ``` ***borderPosition*** definiert ob du in der Karte eine einseitige border darstellen möchtest. Diese könnte durch de Farbe ein weiteres Attribut / Kategorie deiner Inhalte darstellen (Zum Beispiel Aktiv / Inaktiv) ```javascript borderPosition: "left", // Border auf der linken Seite borderPosition: "right", // Border auf der rechten Seite borderPosition: "top", // Border auf der oberen Seite borderPosition: "bottom", // Border auf der unteren Seite borderPosition: "", // Fallback-Wert: keine border ``` ***borderColor*** definiert die Farbe deiner Border. ```javascript borderColor: if aktiv=true then "#c5f2b3" else "#ccc"// HEX-Wert in " borderColor: "", // Fallback-Wert: ``` ```javascript borderWidth: "1px", // Pixel-Wert in " borderWidth: "", // Fallback-Wert: ``` ***title*** gibt den Obersten Titel in der Karte aus und ist fett gedruckt. ```javascript title: Name, // Ninox-feld in Text-Format title: "" // Kein Fallback ``` ```javascript subtitle: "Kunden-Nr.:" + Nr, // Text und/oder Ninox Felder in Textformat subtitle: arcCustomLayout({ uniqueId: "sub-" + Nr, embedded: true, ... }), // gleiches Slot wie value subtitle: "", // Kein Fallback ``` Mit ***value*** kannst du deine Karte so individuell gestalten, wie du möchtest. Hier kannst du mit Schrift arbeiten oder auch jedes andere Widget integrieren. Zum Beispiel kannst du mit [arcCustomLayout](/documentation/custom-layout) komplexere Karten aufsetzen oder einfach eine Progressbar mit [arcCustomProgressBar](/documentation/progress-bar) einsetzen. ```javascript value: "Hier steht Text" // Texte oder Werte aus Ninox-Feldern value: arcCustomProgressBar({ uniqueId: "progress" + Nr, title: "", width: "100%", fontSize: "", fontColor: "", backgroundColor: "", progressColor: "", valueTotal: stateTotal, valueProgress: stateCurrent, valueText: stateCurrent + "/" + stateTotal }) // Andere Widgets value: "", // Kein Fallback ``` --- # Custom KPI Bar Source: https://www.arc-rider.com/docs/widgets/custom-kpi-bar # KpiBar in React Mehrere Kennzahlen mit Beschriftung und optionaler Klickaktion nebeneinander 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 ```tsx import { ArcWidgetKpiBar } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const demoMetrics = [ { title: t('Offene Aufträge', 'Open orders', 'Pedidos abiertos'), value: 12, color: '#d97706', }, { title: t('In Arbeit', 'In progress', 'En curso'), value: 7, color: '#0f766e', }, { title: t('Abgeschlossen', 'Completed', 'Completados'), value: 38, color: '#15803d', }, ]; const busyMetrics = [ ...demoMetrics, { title: t('Wartet', 'Waiting', 'En espera'), value: 4, color: '#be123c' }, { title: t('Heute fällig', 'Due today', 'Vence hoy'), value: 3, color: '#7c3aed', }, ]; const metrics = pack === 'empty' ? [] : pack === 'busy' ? busyMetrics : demoMetrics; export default function Example() { const [selected, setSelected] = useState(t('Alle', 'All', 'Todos')); return ( <> ({ card: index + 1, title: metric.title, value: metric.value, clickable: true, settings: { colorValue: metric.color, borderColor: metric.color, }, })), }} onClick={(_event, index) => setSelected(metrics[index]?.title ?? selected) } />

{t('Auswahl', 'Selection', 'Selección')}: {selected}

); } ``` ## Features ausprobieren ### Abgeleitete Kennzahlen und Klick-Filter Die Kennzahlen werden gemeinsam aus einem State berechnet. Klicke auf eine Karte, um ihren Index im Callback zu prüfen. ```tsx import { useState } from 'react'; import { ArcWidgetKpiBar } from '@arcrider/arcwidgets-react'; export default function Example() { const [completed, setCompleted] = useState(7); const [selected, setSelected] = useState('Keine'); const cards = [ { title: 'Gesamt', value: 12 }, { title: 'Erledigt', value: completed }, { title: 'Offen', value: 12 - completed }, ]; return ( <> ({ ...c, card: i + 1, clickable: true })), }} onClick={(_event, index) => setSelected(cards[index].title)} />

Ausgewählte Kennzahl: {selected}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetKpiBarData`. Ü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). ### onClick Meldet den Klick. Beachte die widgetspezifischen Argumente unten. ```typescript onClick?: (event: unknown, cardIndex: number) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### settings Gemeinsame Darstellung der Einträge. Typ: `ArcWidgetKpiBarSettings`. ### cards Kennzahlenkarten mit Beschriftung, Wert und optionaler Klickbarkeit. Typ: `ArcWidgetKpiBarCard[]`. ## Verhalten und Grenzen - `cards` enthält Werte und Beschriftungen, `settings` die gemeinsame Darstellung. - Aktiviere `clickable` pro Karte. `onClick(event, cardIndex)` kann einen Filter ändern oder in deiner Anwendung navigieren. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetKpiBarSettings { height?: string; colorLabel?: string; fontSizeLabel?: string; colorValue?: string; fontSizeValue?: string; backgroundColor?: string; borderColor?: string; [key: string]: unknown; } export interface ArcWidgetKpiBarCard { card: number; title?: ArcWidgetSlot; value?: ArcWidgetSlot; subtitle?: ArcWidgetSlot; settings?: ArcWidgetKpiBarSettings; clickable?: boolean; } export interface ArcWidgetKpiBarData extends Omit { settings?: ArcWidgetKpiBarSettings; cards?: ArcWidgetKpiBarCard[]; } export interface ArcWidgetKpiBarProps extends ArcReactWidgetProps { onClick?: (event: unknown, cardIndex: 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 KPI Bar ## AI Defaults (read first) - **Function name**: `arcKPIBar` (NOT `arcCustomKpiBar`). - **uniqueId**: Required, `"kpi-" + Nr`. - **cards**: Array of `{ card: 1, title: "...", value: ... }` – `card` is eine Nummer (1, 2, 3…). - **Klick (Ninox):** `actions: [...]` auf der Karte — der Adapter setzt intern `clickable: true`. Unterstützte Action-Typen wie bei arcCustomButton. - **Klick (React):** `clickable: true` auf der Karte; `onClick(event, cardIndex)` am Host (`cardIndex` = 0-basierter Index in `cards`) — **kein** `actions` in `data`. - **Large numbers**: Use `shortNumbers(value)` for auto-formatting (1.2K, 3.5M). - **height**: Set in `settings: { height: "250px" }`. - **Null handling**: Do NOT guard values with `else "" end`, omit the else. # Custom KPI Bar ![](https://framerusercontent.com/images/8Ouhx7SKhEFXPG22FsEV5i0g.png) Die Custom KPI Bar besteht aus einem Anwendungscode, den du in einem Ninox Formel-Feld auf deiner Wunschoberfläche in der Datenbank einfügst. Dieser besteht aus einem allgemeinen Part (settings), in dem du Grundeinstellungen vornimmst und einem Detail-Bereich (cards), in dem du individuell bestimmst, welche Werte, Farben oder Aktionen du anwendest. ## Gesamter Anwendungscode Hier siehst du einen beispielhaften Anwendungscode, der deine KPI-Bar definiert. Mit *data* definierst du die Inhalte, die du dann mit der Funktion *arcKPIBar()* ausgeben lässt. ```javascript let data := { uniqueId: "KPI Bar 2", settings: { height: "", colorLabel: "", fontSizeLabel: "15", colorValue: "", fontSizeValue: "40px", backgroundColor: "", borderColor: "" }, cards: [{ card: 1, settings: { colorLabel: "#8e99c4", fontSizeLabel: "15", colorValue: "#8e99c4", fontSizeValue: "40px", backgroundColor: "#49547e", borderColor: "#646f9b" }, title: "Gesamtumsatz", value: "€" + shortNumbers(cnt((select Projekte).Umsatz)), subtitle: "€" + shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" }] }; arcKPIBar(data) ``` ## Allgemeine Settings In den allgemeinen Settings nimmst du die Grundeinstellungen deiner KPI Karte vor. Diese **wirkt sich auf alle Karten aus**. Folgend werden dir die einzelnen Parameter erläutert: *uniqueId* vergibt einen eigenen Namen für deine KPI Bar. Es ist wichtig, dass diese Bezeichnung einzigartig ist. Hast du beispielsweise mehrere KPI-Bars auf einem Datensatz und gibst keinen einzigartigen Namen an, dann überschreiben sich die Werte gegenseitig. Versuche also möglichst individuell zu Bezeichnen. ```javascript uniqueId: "KPI Bar 1", ``` *height* bestimmt die Höhe deiner KPI Bar. Hier kannst du folgende Werte angeben: ```javascript height: "250px" // Pixel Werte ``` *colorLabel *gibt die Farbe der Schrift an. Hier kannst du folgende Werte angeben: ```javascript colorLabel: "FFFFFF" // HEX Code für deinen Farbwert. ``` *fontSizeLabel* bestimmt die Größe deiner Schrift über und unter dem KPI-Wert. Diese gibst du mit folgenden Werten an: ```javascript fontSizeLabel: "12px" // Pixel Werte ``` *colorValue *gibt die Farbe deines KPI Wertes an. Hier kannst du folgende Werte angeben: ```javascript colorValue: "FFFFFF" // HEX Code für deinen Farbwert. ``` *fontSizeValue* bestimmt die Größe deines KPI-Wertes. Diese gibst du mit folgenden Werten an: ```javascript fontSizeValue: "35px" // Pixel Werte ``` *backgroundColor* bestimmt die Farbe des Hintergrundes deiner Karte. Hier kannst du folgende Werte angeben: ```javascript backgroundColor: "FFFFFF" // HEX Code für deinen Farbwert. ``` *borderColor* bestimmt die Farbe der Umrandung der Karte. Hier kannst du folgende Werte angeben: ```javascript borderColor: "FFFFFF" // HEX Code für deinen Farbwert. ``` ## Standard-Werte als Fallback 💡 **Hinweis: **Alle Werte der Parameter werden in Anführungszeichen angegeben. Trägst du nichts in die Anführungszeichen ein, so greift das System auf ein Fallback zurück. Das heißt es gibt voreingestellte Standard-Werte, sodass immer etwas ausgegeben wird. Hier siehst du die Karte mit den Fallback-Werten: ![](https://framerusercontent.com/images/vgJSRSruJ02ARYRMMMm6yshxB0A.png) ## Detail-Bereich Im Detail-Bereich definierst du alle Styles und Werte der einzelnen KPI-Karten. Das heißt: Jede Karte kann individuell gestaltet werden. Die Styles überschreiben in dem Fall die Werte aus den Allgemeinen Settings. Folgend siehst du einen Code für eine beispielhafte KPI Card. ```javascript {card: 1, settings: { colorLabel: "#F9244B", fontSizeLabel: "15", colorValue: "#F9244B", fontSizeValue: "40px", backgroundColor: "#FCD9DF", borderColor: "#EABDC5" }, title: "Gesamtumsatz", value: "€" + shortNumbers(cnt((select Projekte).Umsatz)), subtitle: "€" + shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" } ``` *card* ist die individuelle Nummer deiner Karte und sorgt für Übersichtlichkeit und Datenstruktur. Diese gibst du wie folgt an: ```javascript card: 1 // Nummer deiner Karte als reine Zahl ``` *settings* sind die gleichen Einstellungswerte, wie in den allgemeinen Settings. Diese kannst du hier je Karte überschreiben und individuell gestalten. ```javascript settings: { colorLabel: "#F9244B", fontSizeLabel: "15", colorValue: "#F9244B", fontSizeValue: "40px", backgroundColor: "#FCD9DF", borderColor: "#EABDC5" } ``` *title* ist die Überschrift über deinem KPI-Wert. ```javascript title: "Gesamtumsatz" // Reiner, statischer Text als Überschrift title: if Status=abgeschlossen then "Gesamtumsatz" else "Erwarteter Umsatz" // Du kannst alle normalen Ninox Funktionen verwenden, um dir deinen Titel ausgeben zu lassen. ``` *value* gibt deinen Wert der KPI Karte aus. ```javascript value: "5.000.000€" // Reiner, statischer Text als Wert value: "€" + shortNumbers(cnt((select Projekte).Umsatz)) // Greife auf Ninox Felder zurück und arbeite mit üblichen Ninox Funktionen / -Schreibweisen. ``` *subtitle* gibt den Untertitel unter deinem KPI Wert an. ```javascript subtitle: "in diesem Monat" // Reiner, statischer Text als Unterschrift subtitle: "€" + shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" // Greife auf Ninox Felder zurück und arbeite mit üblichen Ninox Funktionen / -Schreibweisen. ``` *actions* (nur Ninox) definiert Aktionen, die beim Klick auf die KPI-Karte ausgeführt werden. Der Adapter setzt daraus `clickable: true` für das Widget-Core — die Karte wird klickbar (Cursor: pointer, Hover-Effekt). Unterstützte Action-Typen: `update`, `popup`, `openRecord`, `fullscreen`, `openUrl`, `create`, `delete` (analog zu arcCustomButton). *clickable* (React / andere Hosts) markiert die Karte als klickbar. Setze `clickable: true` in `data` und reagiere im Host auf `onClick(event, cardIndex)` — `cardIndex` ist der **0-basierte** Index in `cards`. In React **kein** `actions` in `data` verwenden. ```javascript actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "helper_activeTab"), value: "umsatz" }] actions: [{ type: "popup", recordId: Nr }] ``` ## Vervielfältigung deiner KPI Karten Du kannst selbst bestimmen, wie viele KPI Karten du dir anzeigen lassen möchtest. Per Copy Paste kannst du deine Karten einfach in dem Code vervielfältigen. Trenne die einzelnen Cards durch ein Komma (,).
cards: [{
    card: 1,
    title: "Gesamtumsatz",
    value: "€" + shortNumbers(cnt((select Projekte).Umsatz)),
    subtitle: "€" +
    shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
    " in diesem Monat"
}, {
    card: 2,
    title: "Abgeschlossen gesamt",
    value: shortNumbers(cnt(select Projekte where Abgeschlossen != null)),
    subtitle: shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) +
    " in diesem Monat"
}, {
    card: 3,
    title: "Conversion Rate",
    value: round(cnt(select Projekte where Abgeschlossen != null) / cnt(select Projekte) * 100, 2) +
    "%",
    subtitle: round(cnt(select Projekte where Abgeschlossen != null and month(Abgeschlossen) = month(today())) /
    cnt(select Projekte where month(Abgeschlossen) = month(today())) *
    100, 2) + "%" + " in diesem Monat"
}]
    
## Parameter In dem Detail-Bereich deiner KPI-Karten kannst du verschiedenste Parameter definieren. Dabei kannst du selbst entscheiden, welche Parameter du definieren möchtest. Lässt du Parameter einfach weg, werden diese nicht ausgegeben. Hier siehst du alle Parameter, die du bei der KPI Bar verwenden kannst: ```javascript card: "", settings: { colorLabel: "", fontSizeLabel: "", colorValue: "", fontSizeValue: "", backgroundColor: "", borderColor: "" }, title: "", value: "", subtitle: "", actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Feld"), value: "..." }] ``` ## Funktionen Du kannst deine Parameter mit allen Ninox Funktionen & Schreibweisen definieren. Wir haben dir aber auch einige nützliche arcRider Funktionen erstellt, die dir deine Werte noch übersichtlicher machen. *shortNumbers() *kürzt lange Zahlen ab 4 Stellen ab und setzt den Zahlennamen (Tsd.,Mio.,Mrd.,Bill.) dahinter. ```javascript shortNumbers("2000") // Gibt den abgekürzten Wert der statischen Zahl aus (2 Tsd.) shortNumbers(cnt((select Projekte).Umsatz)) // Gibt den abgekürzten Wert der dynamischen Zahl aus (z.B. 35 Mio.) ``` ## Beispiele Folgend findest du einige Praxisbeispiele, die dir den unterschiedlichen Einsatz der KPI Bar veranschaulichen. ### KPI Card Fallback ![](https://framerusercontent.com/images/vgJSRSruJ02ARYRMMMm6yshxB0A.png) ```javascript let data := { uniqueId: "KPI Bar 1", settings: { height: "", colorLabel: "", fontSizeLabel: "15", colorValue: "", fontSizeValue: "40px", backgroundColor: "", borderColor: "" }, cards: [{ card: 1, title: "Gesamtumsatz", value: "€" + shortNumbers(cnt((select Projekte).Umsatz)), subtitle: "€" + shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" }, { card: 2, title: "Abgeschlossen gesamt", value: shortNumbers(cnt(select Projekte where Abgeschlossen != null)), subtitle: shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" }, { card: 3, title: "Conversion Rate", value: round(cnt(select Projekte where Abgeschlossen != null) / cnt(select Projekte) * 100, 2) + "%", subtitle: round(cnt(select Projekte where Abgeschlossen != null and month(Abgeschlossen) = month(today())) / cnt(select Projekte where month(Abgeschlossen) = month(today())) * 100, 2) + "%" + " in diesem Monat" }] }; arcKPIBar(data) ``` ### KPI Bar individuell ![](https://framerusercontent.com/images/al3UmQQbGCgJyxMk6N7cRKGowRw.png) ```javascript let data := { uniqueId: "KPI Bar 2", settings: { height: "", colorLabel: "", fontSizeLabel: "15", colorValue: "", fontSizeValue: "40px", backgroundColor: "", borderColor: "" }, cards: [{ card: 1, settings: { colorLabel: "#F9244B", fontSizeLabel: "15", colorValue: "#F9244B", fontSizeValue: "40px", backgroundColor: "#FCD9DF", borderColor: "#EABDC5" }, title: "Gesamtumsatz", value: "€" + shortNumbers(cnt((select Projekte).Umsatz)), subtitle: "€" + shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" }, { card: 2, settings: { colorLabel: "#000", fontSizeLabel: "15", colorValue: "#1f96bf", fontSizeValue: "40px", backgroundColor: "#FDE8A5", borderColor: "#D9C78C" }, title: "Abgeschlossen gesamt", value: shortNumbers(cnt(select Projekte where Abgeschlossen != null)), subtitle: shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" }, { card: 3, settings: { colorLabel: "#CAECCD", fontSizeLabel: "15", colorValue: "#CAECCD", fontSizeValue: "40px", backgroundColor: "#56C275", borderColor: "#9DCDA1" }, title: "Conversion Rate", value: round(cnt(select Projekte where Abgeschlossen != null) / cnt(select Projekte) * 100, 2) + "%", subtitle: cnt(select Projekte where Abgeschlossen != null and month(Abgeschlossen) = month(today())) / cnt(select Projekte where month(Abgeschlossen) = month(today())) * 100 + "%" + " in diesem Monat" }] }; arcKPIBar(data) ``` ### KPI Bar Darkmode ![](https://framerusercontent.com/images/8Ouhx7SKhEFXPG22FsEV5i0g.png) ```javascript let data := { uniqueId: "KPI Bar 3", settings: { height: "", colorLabel: "", fontSizeLabel: "15", colorValue: "", fontSizeValue: "40px", backgroundColor: "", borderColor: "" }, cards: [{ card: 1, settings: { colorLabel: "#8e99c4", fontSizeLabel: "15", colorValue: "#8e99c4", fontSizeValue: "40px", backgroundColor: "#49547e", borderColor: "#646f9b" }, title: "Gesamtumsatz", value: "€" + shortNumbers(cnt((select Projekte).Umsatz)), subtitle: "€" + shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" }, { card: 2, settings: { colorLabel: "#8e99c4", fontSizeLabel: "15", colorValue: "#8e99c4", fontSizeValue: "40px", backgroundColor: "#49547e", borderColor: "#646f9b" }, title: "Abgeschlossen gesamt", value: shortNumbers(cnt(select Projekte where Abgeschlossen != null)), subtitle: shortNumbers(cnt((select Projekte where month(Abgeschlossen) = month(today())).Umsatz)) + " in diesem Monat" }, { card: 3, settings: { colorLabel: "#8e99c4", fontSizeLabel: "15", colorValue: "#8e99c4", fontSizeValue: "40px", backgroundColor: "#49547e", borderColor: "#646f9b" }, title: "Conversion Rate", value: round(cnt(select Projekte where Abgeschlossen != null) / cnt(select Projekte) * 100, 2) + "%", subtitle: cnt(select Projekte where Abgeschlossen != null and month(Abgeschlossen) = month(today())) / cnt(select Projekte where month(Abgeschlossen) = month(today())) * 100 + "%" + " in diesem Monat" }] }; arcKPIBar(data) ``` **Weiterführende Links: ** [> NX Custom KPI Bar ](https://www.arc-rider.de/custom-kpi-bar) --- # Custom Layout Source: https://www.arc-rider.com/docs/widgets/custom-layout # Layout in React Widgets horizontal oder vertikal anordnen. Für responsive Raster kannst du das Layout mit CSS Grid oder Flexbox deiner Anwendung kombinieren. 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 { ArcWidgetBadge, ArcWidgetButton, ArcWidgetLayout, ArcWidgetText, } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [count, setCount] = useState(pack === 'busy' ? 12 : 0); const badges = pack === 'empty' ? [] : [ { value: t('Offen', 'Open', 'Abierto'), color: '#d97706' }, { value: t('Live', 'Live', 'En vivo'), color: '#15803d' }, ...(pack === 'busy' ? [ { value: t('Priorität', 'Priority', 'Prioridad'), color: '#be123c', }, { value: t('Messe', 'Fair', 'Feria'), color: '#7c3aed' }, ] : []), ]; return ( ), }, ...(badges.length ? [ { height: 'auto' as const, value: ( ({ width: 'auto', height: 'auto', value: ( ), })), }} /> ), }, ] : []), { height: 'fraction', value: ( setCount((value) => value + 1)} /> ), }, ], }} /> ); } ``` ## Features ausprobieren ### Richtung, Abstände und verschachtelte Callbacks Wechsle die Anordnung, ohne den Anwendungs-State zurückzusetzen. Der verschachtelte Button aktualisiert das Badge im anderen Block. ```tsx import { useState } from 'react'; import { ArcWidgetLayout, ArcWidgetButton, ArcWidgetBadge, } from '@arcrider/arcwidgets-react'; export default function Example() { const [horizontal, setHorizontal] = useState(true); const [gap, setGap] = useState(12); const [count, setCount] = useState(0); return ( <> ), }, { width: 'fraction', height: 'fraction', value: ( setCount((c) => c + 1)} /> ), }, ], }} /> ); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetLayoutData`. Ü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). ### onClick Meldet den Klick. Beachte die widgetspezifischen Argumente unten. ```typescript onClick?: () => void; ``` ### onBlockClick Nullbasierter Index in `data.blocks`. ```typescript onBlockClick?: (block_index: number) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### embedded Passt das Widget in einen umgebenden Inhaltsbereich ein. Typ: `boolean`. ### direction Anordnung horizontal oder vertikal. Typ: `'horizontal' | 'vertical' | string`. ### alignX Horizontale Ausrichtung. Typ: `'left' | 'center' | 'right' | string`. ### alignY Vertikale Ausrichtung. Typ: `'top' | 'center' | 'bottom' | string`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### gap Abstand zwischen den Inhalten. Typ: `string`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### paddingX Innerer Abstand links und rechts. Typ: `string`. ### paddingY Innerer Abstand oben und unten. Typ: `string`. ### scrollSettings Scrollverhalten des Inhaltsbereichs. Typ: `ArcWidgetLayoutScrollSettings`. ### clickable Aktiviert den Klick-Callback des Widgets. Typ: `boolean`. ### blocks Layoutbereiche mit Größe, Ausrichtung und Inhalt in `value`. Typ: `ArcWidgetLayoutBlock[]`. ## Verhalten und Grenzen - Setze `direction` ausdrücklich. `fraction` verteilt verbleibenden Platz auf der jeweiligen Achse; eine Höhe von `100%` benötigt einen begrenzten Elterncontainer. - `blocks[].value` unterstützt JSX. Binde Events verschachtelter Komponenten direkt an deren Callback-Props. - Für Layout- oder Blockklicks muss am betreffenden Objekt `clickable: true` gesetzt sein. `onBlockClick` erhält den Index in `blocks`. - Für Vollbild und Seitenlayout verwende den Container deiner React-App. Die historischen Felder `fullscreen`, `fullscreenMode`, `page` und `showAdminTools` sind keine React-Routing-API. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetLayoutBlock { width?: string; height?: string; lineHeight?: string; alignX?: 'left' | 'center' | 'right' | string; alignY?: 'top' | 'center' | 'bottom' | string; color?: string; styles?: ArcWidgetStyles | string; value?: ArcWidgetSlot; clickable?: boolean; [key: string]: unknown; } export interface ArcWidgetLayoutScrollSettings { scrollY?: boolean; scrollX?: boolean; [key: string]: unknown; } export interface ArcWidgetLayoutData extends Omit { embedded?: boolean; direction?: 'horizontal' | 'vertical' | string; alignX?: 'left' | 'center' | 'right' | string; alignY?: 'top' | 'center' | 'bottom' | string; width?: string; height?: string; gap?: string; backgroundColor?: string; paddingX?: string; paddingY?: string; fullscreen?: boolean; fullscreenMode?: string; page?: boolean; showAdminTools?: boolean; scrollSettings?: ArcWidgetLayoutScrollSettings; clickable?: boolean; blocks?: ArcWidgetLayoutBlock[]; } export interface ArcWidgetLayoutProps extends ArcReactWidgetProps { onClick?: () => void; onBlockClick?: (block_index: 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 Layout ## AI Defaults (read first) - **Dashboard wrappers**: MUST have `fullscreen: true`, `fullscreenMode: if isAdminMode() then "" else "full" end`, `height: "100%"`. - **embedded**: `false` for standalone/dashboard layouts, `true` when nested inside another widget. - **direction**: Always set explicitly (`"horizontal"` or `"vertical"`). - **Block widths**: At least one block should use `width: "fraction"` to fill remaining space. - **Scrollable areas**: Use `height: "fraction"` + `scrollSettings: { scrollY: true }` for content below fixed headers. - **uniqueId**: Required, no special characters. - **Sticky headers**: Need `overflow-x: hidden` on parent (NOT `overflow: hidden`), background-color, and z-index. - **Two for-loops**: NEVER place two for-loops side by side in blocks – each needs its own wrapper layout. - **Nested widgets (Next-Gen)**: In `blocks[].value`, use `{ widget: "arc-widget-…", uid: "…", data: { … } }` — not `arcCustomXxx({ … })`. Requires current `bundle.js`; legacy Ninox-formula widgets do not support this. See `docs/patterns/nested-widget-descriptors.md`. # Custom Layout ![](https://framerusercontent.com/images/aFEbrJb3sdVWKcqtanFF6Fd1L1I.png) Mit dem Layout-Widget gestaltest du Oberflächen ohne Tabellenraster — flexibler Container (wie Flexbox): Ausrichtung, Abstände, Farben, verschachtelte Widgets. In Ninox heißt der Einstieg `arcCustomLayout({ ... })` im Formelfeld. Ob du einfache Abschnitte nebeneinander anordnen willst, eine Info-Card mit grafischem Hintergrund bauen möchtest oder interaktive Elemente auf einer Seite strukturieren willst – `arcCustomLayout` ist dein Werkzeug dafür. **Dabei steuerst du:** - Ausrichtung (horizontal / vertikal) - Abstände, Farben, Hintergrundbilder - Flexibles Verhalten bei Bildschirmgrößen - Individuelle Stile je Block - Interaktionen (z. B. Klickaktionen auf Bereiche) Im Gegensatz zum `arcCustomGrid`, das für zweidimensionale Strukturen (Zeilen *und* Spalten) gedacht ist, arbeitet das Layout bewusst **eindimensional**: Du bestimmst, ob die Inhalte **horizontal (nebeneinander)** oder **vertikal (untereinander)** dargestellt werden – inklusive Ausrichtung, Abstände, Farben und responsiven Anpassungen. **🎯 Was du damit machen kannst:** - **Karten-ähnliche Layouts** mit Titel, Beschreibung und Buttons nebeneinander - **Hero-Bereiche** mit großem Textblock + Bild nebeneinander (horizontal) - **Vertikale Formblöcke** mit gezieltem Abstand und Hintergrundfarbe - **Interaktive Blöcke** mit Klickfunktionen (Popup, Update, Löschen, etc.) - **Kombination mehrerer Datenquellen** in einem dynamischen Layout ## Anwendungscode ### Schneller Einstieg – Simple Basis Layout Ein kompaktes Beispiel, mit dem du direkt starten kannst. Ideal, wenn du ein paar Inhalte nebeneinander (`direction: "horizontal"`) oder untereinander (`direction: "vertical"`) anordnen willst – zum Beispiel Textbausteine, KPIs oder Buttons. Die wichtigsten Parameter wie Ausrichtung, Breite und Abstand sind bereits gesetzt und lassen sich leicht anpassen. ✅ Schnell einsetzbar ✅ Flexibel anpassbar ✅ Perfekt zum Kopieren und Loslegen ```javascript arcCustomLayout({ uniqueId: "Beispiel "+ Nr, embedded: true, direction: "horizontal", alignX: "left", alignY: "center", width: "100%", height: "auto", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false, }, blocks: [{ width: "fraction", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: "" }, { width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", value: "", }] }) ``` ### Full Layout Basis – Breites Layout im Datensatz-Container Dieses Setup ist für Layouts gedacht, die **nicht eingebettet** sind und sich **über den gesamten Inhaltsbereich eines Datensatzes oder einer Seite erstrecken**. Besonders geeignet für **Dashboards, Übersichtsseiten oder Startbildschirme**, die innerhalb von Ninox aufgeräumt und fokussiert wirken sollen. ✅ Volle Breite innerhalb des Tabs oder Datensatzes ✅ Kein Einbetten – das Layout steht für sich ✅ Ideal für größere Komponenten, Panels oder Übersichtsseiten ### Code mit allen Parametern ```javascript arcCustomLayout({ uniqueId: Nr, embedded: true, fullscreen: false, // Gibst du hier "true" an, wird das Layout in dem ganzen Datensatz eingebunden fullscreenMode: "full", page: false, // Gibst du an, wenn du den fullscreen auf einer Tabelle einstellen möchtest. showAdminTools: false, hideHeaderIcons: true, direction: "vertical", alignX: "center", alignY: "center", width: "", height: "", gap: "5px", backgroundColor: "#ccc", paddingY: "10px", paddingX: "5px", blocks: [{ width: "", height: "auto", lineHeight: "1.6", alignX: "left", value: "", color: "#000", styles: "", // Hier kannst du eigene CSS Styles hinterlegen. clickAction: { recordId: Nr, type: "popup" } }, { width: "", height: "auto", lineHeight: "", alignX: "left", value: "", color: "", // Fallback: "#000" clickAction: { type: "openFullscreen", recordId: raw(record(Firmen,1).Nr) }, }] }) ``` **💡Hinweis:** Du kannst auch mehrere Layouts ineinander verschachteln. Somit kannst du komplexe Interfaces aufbauen und Ninox noch übersichtlicher gestalten. ## Allgemeine Settings Die oberen Parameter geben die allgemeinen Settings deines gesamten Layouts an. Das ist sozusagen der äußerste Container deines Layouts. ### uniqueId ***uniqueId*** ist die individuelle Bezeichnung deines Layouts. Achte darauf, dass du hier einen einzigartigen Titel vergibst. Das ist wichtig, falls du mehrere Layouts auf einer Seite / in einer Tabelle darstellen möchtest und diese sich nicht gegenseitig überschreiben. ```javascript uniqueId: "Layout container", ``` ### embedded ***embedded*** gibt an, ob dein Layout in einem anderen Widget integriert werden soll. ```javascript embedded: true, // Ja, es wird in einem anderen Widget integriert embedded: false, // Nein, es ist nicht in anderen Widgets integriert embedded: false, // default: false ``` ### fullscreen ***fullscreen*** bestimmt, ob dein Layout über den gesamten Bildschirm (Datensatz / Page) angezeigt werden soll. ```javascript fullscreen: true, fullscreen: false, // default: false ``` ### ninoxVersion ***ninoxVersion*** gibt die Ninox-Web-Version an, in der du dich befindest (z. B. `"3.13"`). Für einwandfreies Fullscreen-Verhalten bitte setzen; Werte ab `3.13` gelten auch für neuere Versionen (z. B. `3.15`). ```javascript ninoxVersion: "3.13", ``` ### page ***page*** definiert, ob du das Layout auf einer Page oder in einer Tabelle einbindest. `true` = Page, `false` = Tabelle / Record. Früher wirkte `fullscreenMode: "full"` nur auf Pages; mit `page: false` greift Fullscreen auch auf Records einer Tabelle. ```javascript page: true, // Page page: false, // Tabelle / Record ``` ### fullscreenMode ***fullscreenMode***: `"full"` blendet Ninox-Header und Tabs aus. Zusammen mit `fullscreen: true` und `ninoxVersion` nutzen. ```javascript fullscreenMode: "full", fullscreenMode: "", // default: kein Full-Chrome-Hide ``` ### showAdminTools ***showAdminTools*** steuert, ob der Admin-Button (Ninox-Schlüssel) für Admins sichtbar bleibt. ```javascript showAdminTools: true, showAdminTools: false, // default: false in vielen Dashboard-Beispielen ``` ### scrollSettings ***scrollSettings*** steuert, ob das Layout vertikal (`scrollY`) oder horizontal (`scrollX`) scrollbar ist. Das Layout merkt sich die Scroll-Position — nach Triggern, die Ninox kurz neu laden, bleibt die letzte Position erhalten. #### scrollY ```javascript scrollSettings: { scrollY: true, scrollX: false, } ``` #### scrollX ```javascript scrollSettings: { scrollY: false, scrollX: true, } ``` ### direction ***direction*** gibt an, wie deine weiteren Inhalte (blocks) angeordnet werden sollen. ```javascript direction: "vertical", // Alle Inhalte werden untereinander angeordnet. direction: "horizontal", // Alle Inhalte werden nebeneinander angeordnet. ``` ### 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 ``` ### 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 Gesamthöhe auf ``` ### width ***width*** gibt die Weite an. ```javascript width: "100%", // Prozent-Werte. Hier: volle Breite. width: "100px", // Pixel-Werte ``` ### height ***height*** gibt die Höhe an. ```javascript height: "auto", // Automatisch an den Inhalt angepasste Höhe. height: "300px", // Pixel-Werte ``` ### gap Mit ***gap*** bestimmst du, wie viel Abstand zwischen deinen ***blocks*** sein soll. ![](https://framerusercontent.com/images/Vkymdh6PJgSU5aZU3b6Bun2Uk4s.png) ```javascript gap: "5px", // Pixel-Werte ``` ### actions (Klick auf das gesamte Layout) Mit ***actions*** auf Widget-Ebene (nicht innerhalb eines Blocks) machst du das **gesamte Layout klickbar** — nützlich z. B. für einen Card-Wrapper oder einen Mobile-Dimmer, ohne dass du dafür einen eigenen Block brauchst. ```javascript actions: [{ recordId: Nr, type: "popup" }] ``` Mehrere Einträge werden nacheinander ausgeführt, `dataBag` funktioniert wie bei jeder anderen Action auf `update`/`create`-Typ: ```javascript actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "helper_mainContentState"), dataBag: { base: helper_mainContentState, patch: { sidebarOpen: "" } } }] ``` **Wichtig:** Hat ein einzelner Block selbst `actions` oder `clickAction`, wird dessen Klick **nicht** zusätzlich an die Widget-`actions` weitergegeben (kein Doppel-Feuern) — nur Klicks außerhalb eines konfigurierten Blocks lösen die Widget-`actions` aus. Ausführlich zu Block-Actions: [`actions Block`](#actions-block-empfohlen) weiter unten. ### loading (Widget-Ebene) Mit **`loading`** steuerst du, ob beim Ausführen der Widget-`actions` ein Spinner über dem Zielbereich erscheint. Ohne `loading` (oder mit `show: false`) läuft die Action wie bisher — nur ohne visuelles Feedback. ```javascript loading: { show: true, target: "layout", // Default: "layout" | { blockId: "sidebar" } indicator: { type: "spinner", color: "#4970ff", width: "32px", height: "32px" }, dim: true, // halbtransparenter Hintergrund über dem Zielbereich minDuration: 300, // Mindest-Anzeigedauer in ms hideOnFinish: true // Default: true } ``` **`target`** auf Widget-Ebene: | Wert | Bedeutung | |---|---| | `"layout"` (Default) | Spinner über dem gesamten Layout | | `{ blockId: "sidebar" }` | Spinner nur über dem Block mit dieser `blockId` | **`indicator.type`:** `dots` | `spinner` | `pulse` — wie beim Button. #### overlay (Fullscreen-Modal) Mit **`loading.overlay`** erscheint statt des lokalen Block-/Layout-Overlays ein **Fullscreen-Modal** über der gesamten Ansicht — identisch zum Button (`arcModalWindows`). `target` und `dim` werden in diesem Modus ignoriert. | `overlay` | Verhalten | |-----------|-----------| | `true` | Nur Backdrop + Spinner | | `{ title, message }` | Feste Meldung in der Dialog-Karte | | `{ sequences: [...] }` | Dynamische Meldungen (Zeit +/oder Ninox-Booleans) | ```javascript loading: { show: true, overlay: { title: { value: "Wird gespeichert…", styles: "font-size: 18px; font-weight: 700;" }, message: "Bitte einen Moment warten.", indicator: { type: "spinner", color: "#4970ff", width: "32px", height: "32px" } }, minDuration: 300, hideOnFinish: true } ``` Vollständige Overlay-API (`width`, `height`, `backgroundColor`, `sequences`, `when`, `priority`): siehe [Button Loading — overlay](../mini_widgets/buttons.md#overlay-zusätzlich). ## Settings im Layout Block Die Parameter innerhalb eines Blocks bestimmen Werte die zu einem jeweiligen Layoutblock gehören. Du kannst auch weitere Blöcke hinzufügen (Komma getrennt). ```javascript blocks: [{ // Beginn Block 1 blockId: "sidebar", // optional — stabile ID für Loading-Targeting width: "", height: "auto", lineHeight: "1.6", // default: normal alignX: "left", color: "#000", // Schriftfarbe von Text im Layoutblock backgroundColor: "#333", // Hintergrundfarbe von Text im Layoutblock styles: "", // Hier kannst du eigene CSS Styles hinterlegen. value: "", // Hier gibst du den Wert an, der im Block ausgegeben werden soll. clickAction: { recordId: Nr, type: "popup" } }, { // Beginn Block 2 width: "", height: "auto", lineHeight: "1.6", // default: normal alignX: "left", color: "#000", backgroundColor: "#ccc", styles: "", // Hier kannst du eigene CSS Styles hinterlegen. value: "", clickAction: { recordId: Nr, type: "popup" } } }] ``` ### width ***width ***und*** height*** geben die Breite und Höhe des Layoutblocks an. ```javascript width: "auto", // Automatisch an den Inhalt angepasste Weite. // default: width: "300px", // Pixel-Werte oder Prozenz-Werte height: "auto", // Automatisch an den Inhalt angepasste Höhe. // default: height: "300px", // Pixel-Werte oder Prozenz-Werte ``` ### lineHeight Mit*** lineHeight ***bestimmst du den Abstand zwischen den Zeilen im Layout. ```javascript lineHeight: "1.6", // default: normal ``` ### alignX **alignX:** gibt an, wie deine Inhalte auf der X-Achse innerhalb des Layoutblocks angeordnet werden. ```javascript alignX: "center", // zentriert // default: center alignX: "left", // linksbündig alignX: "right", // rechtsbündig alignX: "between", // teilt Blocks auf Gesamtbreite auf ``` ### color & backgroundColor Mit ***color***** und *****backgroundColor***** **bestimmst du die Schriftfarbe und Hintergrundfarbe des Textes innerhalb deines Layoutblocks. ```javascript color: "#33AAFF", // default: #000 backgroundColor: "#ccc", // default: no default color ``` ### styles Bei ***styles*** kannst du dem Layoutblock eigene CSS styles hinzufügen. ```javascript styles: "", // z.B. font-weight: 700; ``` ### value ***value*** ist der Wert, der innerhalb des Layoutblocks ausgegeben wird. Hier kannst du z.B. Text, Ninox-Felder oder customWidgets angeben. ```javascript value: "Überschrift", // einfacher Text value: Titel, // Ninox-Feld value: arcCustomButton({...}) // Top-Level oder Legacy: Funktionsaufruf value: "", // default: Gibst du keinen Wert an, wird nichts ausgegeben ;) ``` #### Verschachtelte Widgets (Next-Gen, Object-Notation) Wenn ein Block **ein anderes arcWidget** enthält und beide aus dem aktuellen **`bundle.js`** kommen, kannst du statt `arcCustomButton({ … })` ein **Descriptor-Objekt** setzen: ```javascript value: { widget: "arc-widget-button", uniqueId: "block-btn-" + Nr, // uid: "block-btn-" + Nr ist gleichwertig data: { title: "Speichern", actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Status"), value: "done" }] } } ``` - **`widget`**: interne ID (`arc-widget-layout`, `arc-widget-button`, …) — nicht der Funktionsname - **`uniqueId`** oder **`uid`**: beide akzeptiert — top-level, **nicht** in `data` - **`data`**: dieselben Parameter wie im Funktionsaufruf **Wichtig:** Funktioniert nur mit **Next-Gen-Widgets** im Bundle. Alte **Legacy-Ninox-Formeln** (noch nicht als JS-Widget deployed) verstehen diese Objekte nicht. Ausführlich: [`nested-widget-descriptors`](../patterns/nested-widget-descriptors.md) #### actions Block (empfohlen) ***actions*** beschreibt deine Aktionen in den jeweiligen Blocks als Array. Mehrere Einträge werden nacheinander ausgeführt — z. B. erst State leeren, dann Record öffnen. ```javascript actions: [{ recordId: Nr, type: "update", fieldId: fieldId(Nr, "helper_uiState"), dataBag: { base: helper_uiState, patch: { activeTab: "details" } } }, { recordId: Nr, type: "popup" }] ``` ***openTable*** öffnet eine Tabellenansicht in Ninox: ```javascript actions: [{ type: "openTable", tableId: "A" }] ``` ***openUrl*** öffnet eine externe URL: ```javascript actions: [{ type: "openUrl", value: "https://www.arc-rider.com" }] ``` **Action-Typen** (wie bei Button und anderen Widgets): Wiederholtes Trigger-`update` mit `polling` + Formel-Boolean `active`: siehe [`action-polling`](../patterns/action-polling.md). ```javascript // Folgende Actions benötigen die zwei Parameter: "recordId" und "type" "popup" "delete" "openRecord" "fullscreen" // Folgende Action benötigt die 4 Parameter: "recordId", "type", "field", "value" "update" // Folgende Action benötigt 2 Parameter: "type", "value" "openUrl" // Folgende Action benötigt 2 Parameter: "type", "tableId" "openTable" // Folgende Actions benötigen nur den Parameter "type" "closeFullScreen" "closeRecord" "closeAllRecords" ``` Partielle State-Updates per `dataBag` auf `update`-Actions: [`data-bag-actions`](../patterns/data-bag-actions.md). #### loading Block Mit **`loading`** auf Block-Ebene steuerst du den Spinner beim Ausführen der Block-`actions`. Das Ziel kann der Block selbst, ein anderer Block oder das gesamte Layout sein. ```javascript blockId: "save-panel", actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Status"), value: "done" }], loading: { show: true, target: "block", // Default: "block" | "layout" | { blockId: "other" } indicator: { type: "spinner", color: "#4970ff" }, dim: true, minDuration: 300, hideOnFinish: true } ``` **`target`** auf Block-Ebene: | Wert | Bedeutung | |---|---| | `"block"` (Default) | Spinner über dem geklickten Block | | `"layout"` | Spinner über dem gesamten Layout | | `{ blockId: "sidebar" }` | Spinner über einem anderen Block (per `blockId`) | **`blockId`** ist optional, aber nötig wenn du per `{ blockId: "…" }` ein anderes Ziel ansprechen willst. Die ID muss auf der Seite eindeutig sein — `{ blockId: "…" }` sucht zuerst im selben Layout, dann seitenweit (funktioniert also auch, wenn der Ziel-Block in einem übergeordneten Layout liegt, das dieses Layout per `blocks[].value: { widget: "arc-widget-layout", … }` verschachtelt). **`loading.overlay`** funktioniert auch auf Block-Ebene — dann erscheint das Fullscreen-Modal statt des lokalen Overlays (siehe [overlay (Widget-Ebene)](#overlay-fullscreen-modal) oben). Während eine Action läuft, sind erneute Klicks auf denselben Zielbereich blockiert (Re-Entry-Schutz). #### clickAction Block (Legacy) Das frühere einzelne Feld **`clickAction`** wird weiterhin unterstützt und wie ein Eintrag in `actions` ausgeführt. Für neue Konfigurationen **`actions`** verwenden. ```javascript clickAction: { recordId: Nr, type: "update", field: "", value: "" } ``` ```javascript clickAction: { type: "openTable", tableId: "A" } ``` ```javascript clickAction: { type: "openUrl", value: "https://www.arc-rider.com" } ``` #### Fullscreen Mode (Ninox) ***fullscreenMode*** blendet bei `fullscreen: true` Ninox-Formelemente aus. `showAdminTools` steuert den Ninox-Schlüssel für andere Admins. `hideHeaderIcons` blendet Ninox-Symbole im Header. ```javascript fullscreen: true, fullscreenMode: "full", showAdminTools: true, hideHeaderIcons: true, ``` # Custom Layout ## AI Defaults (read first) - **Dashboard wrappers**: MUST have `fullscreen: true`, `fullscreenMode: if isAdminMode() then "" else "full" end`, `height: "100%"`. - **embedded**: `false` for standalone/dashboard layouts, `true` when nested inside another widget. - **direction**: Always set explicitly (`"horizontal"` or `"vertical"`). - **Block widths**: At least one block should use `width: "fraction"` to fill remaining space. - **Scrollable areas**: Use `height: "fraction"` + `scrollSettings: { scrollY: true }` for content below fixed headers. - **uniqueId**: Required, no special characters. - **Sticky headers**: Need `overflow-x: hidden` on parent (NOT `overflow: hidden`), background-color, and z-index. - **Two for-loops**: NEVER place two for-loops side by side in blocks – each needs its own wrapper layout. - **Nested widgets (Next-Gen)**: In `blocks[].value`, use `{ widget: "arc-widget-…", uid: "…", data: { … } }` — not `arcCustomXxx({ … })`. Requires current `bundle.js`; legacy Ninox-formula widgets do not support this. See `docs/patterns/nested-widget-descriptors.md`. # Custom Layout ![](https://framerusercontent.com/images/aFEbrJb3sdVWKcqtanFF6Fd1L1I.png) Mit dem Layout-Widget gestaltest du Oberflächen ohne Tabellenraster — flexibler Container (wie Flexbox): Ausrichtung, Abstände, Farben, verschachtelte Widgets. Ob du einfache Abschnitte nebeneinander anordnen willst, eine Info-Card mit grafischem Hintergrund bauen möchtest oder interaktive Elemente auf einer Seite strukturieren willst – `arcCustomLayout` ist dein Werkzeug dafür. **Dabei steuerst du:** - Ausrichtung (horizontal / vertikal) - Abstände, Farben, Hintergrundbilder - Flexibles Verhalten bei Bildschirmgrößen - Individuelle Stile je Block - Interaktionen (z. B. Klickaktionen auf Bereiche) Im Gegensatz zum `arcCustomGrid`, das für zweidimensionale Strukturen (Zeilen *und* Spalten) gedacht ist, arbeitet das Layout bewusst **eindimensional**: Du bestimmst, ob die Inhalte **horizontal (nebeneinander)** oder **vertikal (untereinander)** dargestellt werden – inklusive Ausrichtung, Abstände, Farben und responsiven Anpassungen. **🎯 Was du damit machen kannst:** - **Karten-ähnliche Layouts** mit Titel, Beschreibung und Buttons nebeneinander - **Hero-Bereiche** mit großem Textblock + Bild nebeneinander (horizontal) - **Vertikale Formblöcke** mit gezieltem Abstand und Hintergrundfarbe - **Interaktive Blöcke** mit Klickfunktionen (Popup, Update, Löschen, etc.) - **Kombination mehrerer Datenquellen** in einem dynamischen Layout ## Anwendungscode ### Schneller Einstieg – Simple Basis Layout Ein kompaktes Beispiel, mit dem du direkt starten kannst. Ideal, wenn du ein paar Inhalte nebeneinander (`direction: "horizontal"`) oder untereinander (`direction: "vertical"`) anordnen willst – zum Beispiel Textbausteine, KPIs oder Buttons. Die wichtigsten Parameter wie Ausrichtung, Breite und Abstand sind bereits gesetzt und lassen sich leicht anpassen. ✅ Schnell einsetzbar ✅ Flexibel anpassbar ✅ Perfekt zum Kopieren und Loslegen ```javascript arcCustomLayout({ uniqueId: "Beispiel "+ Nr, embedded: true, direction: "horizontal", alignX: "left", alignY: "center", width: "100%", height: "auto", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false, }, blocks: [{ width: "fraction", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: "" }, { width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", value: "", }] }) ``` ### Full Layout Basis – Breites Layout im Datensatz-Container Dieses Setup ist für Layouts gedacht, die **nicht eingebettet** sind und sich **über den gesamten Inhaltsbereich eines Datensatzes oder einer Seite erstrecken**. Besonders geeignet für **Dashboards, Übersichtsseiten oder Startbildschirme**, die innerhalb von Ninox aufgeräumt und fokussiert wirken sollen. ✅ Volle Breite innerhalb des Tabs oder Datensatzes ✅ Kein Einbetten – das Layout steht für sich ✅ Ideal für größere Komponenten, Panels oder Übersichtsseiten ### Code mit allen Parametern ```javascript arcCustomLayout({ uniqueId: Nr, embedded: true, fullscreen: false, // Gibst du hier "true" an, wird das Layout in dem ganzen Datensatz eingebunden fullscreenMode: "full", page: false, // Gibst du an, wenn du den fullscreen auf einer Tabelle einstellen möchtest. showAdminTools: false, hideHeaderIcons: true, direction: "vertical", alignX: "center", alignY: "center", width: "", height: "", gap: "5px", backgroundColor: "#ccc", paddingY: "10px", paddingX: "5px", blocks: [{ width: "", height: "auto", lineHeight: "1.6", alignX: "left", value: "", color: "#000", styles: "", // Hier kannst du eigene CSS Styles hinterlegen. clickAction: { recordId: Nr, type: "popup" } }, { width: "", height: "auto", lineHeight: "", alignX: "left", value: "", color: "", // Fallback: "#000" clickAction: { type: "openFullscreen", recordId: raw(record(Firmen,1).Nr) }, }] }) ``` **💡Hinweis:** Du kannst auch mehrere Layouts ineinander verschachteln. Somit kannst du komplexe Interfaces aufbauen und Ninox noch übersichtlicher gestalten. ## Allgemeine Settings Die oberen Parameter geben die allgemeinen Settings deines gesamten Layouts an. Das ist sozusagen der äußerste Container deines Layouts. ### uniqueId ***uniqueId*** ist die individuelle Bezeichnung deines Layouts. Achte darauf, dass du hier einen einzigartigen Titel vergibst. Das ist wichtig, falls du mehrere Layouts auf einer Seite / in einer Tabelle darstellen möchtest und diese sich nicht gegenseitig überschreiben. ```javascript uniqueId: "Layout container", ``` ### embedded ***embedded*** gibt an, ob dein Layout in einem anderen Widget integriert werden soll. ```javascript embedded: true, // Ja, es wird in einem anderen Widget integriert embedded: false, // Nein, es ist nicht in anderen Widgets integriert embedded: false, // default: false ``` ### fullscreen ***fullscreen*** bestimmt, ob dein Layout über den gesamten Bildschirm (Datensatz / Page) angezeigt werden soll. ```javascript fullscreen: true, fullscreen: false, // default: false ``` ### ninoxVersion ***ninoxVersion*** gibt die Ninox-Web-Version an, in der du dich befindest (z. B. `"3.13"`). Für einwandfreies Fullscreen-Verhalten bitte setzen; Werte ab `3.13` gelten auch für neuere Versionen (z. B. `3.15`). ```javascript ninoxVersion: "3.13", ``` ### page ***page*** definiert, ob du das Layout auf einer Page oder in einer Tabelle einbindest. `true` = Page, `false` = Tabelle / Record. Früher wirkte `fullscreenMode: "full"` nur auf Pages; mit `page: false` greift Fullscreen auch auf Records einer Tabelle. ```javascript page: true, // Page page: false, // Tabelle / Record ``` ### fullscreenMode ***fullscreenMode***: `"full"` blendet Ninox-Header und Tabs aus. Zusammen mit `fullscreen: true` und `ninoxVersion` nutzen. ```javascript fullscreenMode: "full", fullscreenMode: "", // default: kein Full-Chrome-Hide ``` ### showAdminTools ***showAdminTools*** steuert, ob der Admin-Button (Ninox-Schlüssel) für Admins sichtbar bleibt. ```javascript showAdminTools: true, showAdminTools: false, // default: false in vielen Dashboard-Beispielen ``` ### scrollSettings ***scrollSettings*** steuert, ob das Layout vertikal (`scrollY`) oder horizontal (`scrollX`) scrollbar ist. Das Layout merkt sich die Scroll-Position — nach Triggern, die Ninox kurz neu laden, bleibt die letzte Position erhalten. #### scrollY ```javascript scrollSettings: { scrollY: true, scrollX: false, } ``` #### scrollX ```javascript scrollSettings: { scrollY: false, scrollX: true, } ``` ### direction ***direction*** gibt an, wie deine weiteren Inhalte (blocks) angeordnet werden sollen. ```javascript direction: "vertical", // Alle Inhalte werden untereinander angeordnet. direction: "horizontal", // Alle Inhalte werden nebeneinander angeordnet. ``` ### 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 ``` ### 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 Gesamthöhe auf ``` ### width ***width*** gibt die Weite an. ```javascript width: "100%", // Prozent-Werte. Hier: volle Breite. width: "100px", // Pixel-Werte ``` ### height ***height*** gibt die Höhe an. ```javascript height: "auto", // Automatisch an den Inhalt angepasste Höhe. height: "300px", // Pixel-Werte ``` ### gap Mit ***gap*** bestimmst du, wie viel Abstand zwischen deinen ***blocks*** sein soll. ![](https://framerusercontent.com/images/Vkymdh6PJgSU5aZU3b6Bun2Uk4s.png) ```javascript gap: "5px", // Pixel-Werte ``` ### actions (Klick auf das gesamte Layout) Mit ***actions*** auf Widget-Ebene (nicht innerhalb eines Blocks) machst du das **gesamte Layout klickbar** — nützlich z. B. für einen Card-Wrapper oder einen Mobile-Dimmer, ohne dass du dafür einen eigenen Block brauchst. ```javascript actions: [{ recordId: Nr, type: "popup" }] ``` Mehrere Einträge werden nacheinander ausgeführt, `dataBag` funktioniert wie bei jeder anderen Action auf `update`/`create`-Typ: ```javascript actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "helper_mainContentState"), dataBag: { base: helper_mainContentState, patch: { sidebarOpen: "" } } }] ``` **Wichtig:** Hat ein einzelner Block selbst `actions` oder `clickAction`, wird dessen Klick **nicht** zusätzlich an die Widget-`actions` weitergegeben (kein Doppel-Feuern) — nur Klicks außerhalb eines konfigurierten Blocks lösen die Widget-`actions` aus. Ausführlich zu Block-Actions: [`actions Block`](#actions-block-empfohlen) weiter unten. ### loading (Widget-Ebene) Mit **`loading`** steuerst du, ob beim Ausführen der Widget-`actions` ein Spinner über dem Zielbereich erscheint. Ohne `loading` (oder mit `show: false`) läuft die Action wie bisher — nur ohne visuelles Feedback. ```javascript loading: { show: true, target: "layout", // Default: "layout" | { blockId: "sidebar" } indicator: { type: "spinner", color: "#4970ff", width: "32px", height: "32px" }, dim: true, // halbtransparenter Hintergrund über dem Zielbereich minDuration: 300, // Mindest-Anzeigedauer in ms hideOnFinish: true // Default: true } ``` **`target`** auf Widget-Ebene: | Wert | Bedeutung | |---|---| | `"layout"` (Default) | Spinner über dem gesamten Layout | | `{ blockId: "sidebar" }` | Spinner nur über dem Block mit dieser `blockId` | **`indicator.type`:** `dots` | `spinner` | `pulse` — wie beim Button. #### overlay (Fullscreen-Modal) Mit **`loading.overlay`** erscheint statt des lokalen Block-/Layout-Overlays ein **Fullscreen-Modal** über der gesamten Ansicht — identisch zum Button (`arcModalWindows`). `target` und `dim` werden in diesem Modus ignoriert. | `overlay` | Verhalten | |-----------|-----------| | `true` | Nur Backdrop + Spinner | | `{ title, message }` | Feste Meldung in der Dialog-Karte | | `{ sequences: [...] }` | Dynamische Meldungen (Zeit +/oder Ninox-Booleans) | ```javascript loading: { show: true, overlay: { title: { value: "Wird gespeichert…", styles: "font-size: 18px; font-weight: 700;" }, message: "Bitte einen Moment warten.", indicator: { type: "spinner", color: "#4970ff", width: "32px", height: "32px" } }, minDuration: 300, hideOnFinish: true } ``` Vollständige Overlay-API (`width`, `height`, `backgroundColor`, `sequences`, `when`, `priority`): siehe [Button Loading — overlay](../mini_widgets/buttons.md#overlay-zusätzlich). ## Settings im Layout Block Die Parameter innerhalb eines Blocks bestimmen Werte die zu einem jeweiligen Layoutblock gehören. Du kannst auch weitere Blöcke hinzufügen (Komma getrennt). ```javascript blocks: [{ // Beginn Block 1 blockId: "sidebar", // optional — stabile ID für Loading-Targeting width: "", height: "auto", lineHeight: "1.6", // default: normal alignX: "left", color: "#000", // Schriftfarbe von Text im Layoutblock backgroundColor: "#333", // Hintergrundfarbe von Text im Layoutblock styles: "", // Hier kannst du eigene CSS Styles hinterlegen. value: "", // Hier gibst du den Wert an, der im Block ausgegeben werden soll. clickAction: { recordId: Nr, type: "popup" } }, { // Beginn Block 2 width: "", height: "auto", lineHeight: "1.6", // default: normal alignX: "left", color: "#000", backgroundColor: "#ccc", styles: "", // Hier kannst du eigene CSS Styles hinterlegen. value: "", clickAction: { recordId: Nr, type: "popup" } } }] ``` ### width ***width ***und*** height*** geben die Breite und Höhe des Layoutblocks an. ```javascript width: "auto", // Automatisch an den Inhalt angepasste Weite. // default: width: "300px", // Pixel-Werte oder Prozenz-Werte height: "auto", // Automatisch an den Inhalt angepasste Höhe. // default: height: "300px", // Pixel-Werte oder Prozenz-Werte ``` ### lineHeight Mit*** lineHeight ***bestimmst du den Abstand zwischen den Zeilen im Layout. ```javascript lineHeight: "1.6", // default: normal ``` ### alignX **alignX:** gibt an, wie deine Inhalte auf der X-Achse innerhalb des Layoutblocks angeordnet werden. ```javascript alignX: "center", // zentriert // default: center alignX: "left", // linksbündig alignX: "right", // rechtsbündig alignX: "between", // teilt Blocks auf Gesamtbreite auf ``` ### color & backgroundColor Mit ***color***** und *****backgroundColor***** **bestimmst du die Schriftfarbe und Hintergrundfarbe des Textes innerhalb deines Layoutblocks. ```javascript color: "#33AAFF", // default: #000 backgroundColor: "#ccc", // default: no default color ``` ### styles Bei ***styles*** kannst du dem Layoutblock eigene CSS styles hinzufügen. ```javascript styles: "", // z.B. font-weight: 700; ``` ### value ***value*** ist der Wert, der innerhalb des Layoutblocks ausgegeben wird. Hier kannst du z.B. Text, Ninox-Felder oder customWidgets angeben. ```javascript value: "Überschrift", // einfacher Text value: Titel, // Ninox-Feld value: arcCustomButton({...}) // Top-Level oder Legacy: Funktionsaufruf value: "", // default: Gibst du keinen Wert an, wird nichts ausgegeben ;) ``` #### Verschachtelte Widgets (Next-Gen, Object-Notation) Wenn ein Block **ein anderes arcWidget** enthält und beide aus dem aktuellen **`bundle.js`** kommen, kannst du statt `arcCustomButton({ … })` ein **Descriptor-Objekt** setzen: ```javascript value: { widget: "arc-widget-button", uniqueId: "block-btn-" + Nr, // uid: "block-btn-" + Nr ist gleichwertig data: { title: "Speichern", actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Status"), value: "done" }] } } ``` - **`widget`**: interne ID (`arc-widget-layout`, `arc-widget-button`, …) — nicht der Funktionsname - **`uniqueId`** oder **`uid`**: beide akzeptiert — top-level, **nicht** in `data` - **`data`**: dieselben Parameter wie im Funktionsaufruf **Wichtig:** Funktioniert nur mit **Next-Gen-Widgets** im Bundle. Alte **Legacy-Ninox-Formeln** (noch nicht als JS-Widget deployed) verstehen diese Objekte nicht. Ausführlich: [`nested-widget-descriptors`](../patterns/nested-widget-descriptors.md) #### actions Block (empfohlen) ***actions*** beschreibt deine Aktionen in den jeweiligen Blocks als Array. Mehrere Einträge werden nacheinander ausgeführt — z. B. erst State leeren, dann Record öffnen. ```javascript actions: [{ recordId: Nr, type: "update", fieldId: fieldId(Nr, "helper_uiState"), dataBag: { base: helper_uiState, patch: { activeTab: "details" } } }, { recordId: Nr, type: "popup" }] ``` ***openTable*** öffnet eine Tabellenansicht in Ninox: ```javascript actions: [{ type: "openTable", tableId: "A" }] ``` ***openUrl*** öffnet eine externe URL: ```javascript actions: [{ type: "openUrl", value: "https://www.arc-rider.com" }] ``` **Action-Typen** (wie bei Button und anderen Widgets): Wiederholtes Trigger-`update` mit `polling` + Formel-Boolean `active`: siehe [`action-polling`](../patterns/action-polling.md). ```javascript // Folgende Actions benötigen die zwei Parameter: "recordId" und "type" "popup" "delete" "openRecord" "fullscreen" // Folgende Action benötigt die 4 Parameter: "recordId", "type", "field", "value" "update" // Folgende Action benötigt 2 Parameter: "type", "value" "openUrl" // Folgende Action benötigt 2 Parameter: "type", "tableId" "openTable" // Folgende Actions benötigen nur den Parameter "type" "closeFullScreen" "closeRecord" "closeAllRecords" ``` Partielle State-Updates per `dataBag` auf `update`-Actions: [`data-bag-actions`](../patterns/data-bag-actions.md). #### loading Block Mit **`loading`** auf Block-Ebene steuerst du den Spinner beim Ausführen der Block-`actions`. Das Ziel kann der Block selbst, ein anderer Block oder das gesamte Layout sein. ```javascript blockId: "save-panel", actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Status"), value: "done" }], loading: { show: true, target: "block", // Default: "block" | "layout" | { blockId: "other" } indicator: { type: "spinner", color: "#4970ff" }, dim: true, minDuration: 300, hideOnFinish: true } ``` **`target`** auf Block-Ebene: | Wert | Bedeutung | |---|---| | `"block"` (Default) | Spinner über dem geklickten Block | | `"layout"` | Spinner über dem gesamten Layout | | `{ blockId: "sidebar" }` | Spinner über einem anderen Block (per `blockId`) | **`blockId`** ist optional, aber nötig wenn du per `{ blockId: "…" }` ein anderes Ziel ansprechen willst. Die ID muss auf der Seite eindeutig sein — `{ blockId: "…" }` sucht zuerst im selben Layout, dann seitenweit (funktioniert also auch, wenn der Ziel-Block in einem übergeordneten Layout liegt, das dieses Layout per `blocks[].value: { widget: "arc-widget-layout", … }` verschachtelt). **`loading.overlay`** funktioniert auch auf Block-Ebene — dann erscheint das Fullscreen-Modal statt des lokalen Overlays (siehe [overlay (Widget-Ebene)](#overlay-fullscreen-modal) oben). Während eine Action läuft, sind erneute Klicks auf denselben Zielbereich blockiert (Re-Entry-Schutz). #### clickAction Block (Legacy) Das frühere einzelne Feld **`clickAction`** wird weiterhin unterstützt und wie ein Eintrag in `actions` ausgeführt. Für neue Konfigurationen **`actions`** verwenden. ```javascript clickAction: { recordId: Nr, type: "update", field: "", value: "" } ``` ```javascript clickAction: { type: "openTable", tableId: "A" } ``` ```javascript clickAction: { type: "openUrl", value: "https://www.arc-rider.com" } ``` ## Vue Das `data`-Objekt wird als Prop an die Vue-Komponente übergeben. ```vue ``` --- # Custom Menu Source: https://www.arc-rider.com/docs/widgets/custom-menu # Custom Menu ## AI Defaults (read first) - **uniqueId**: Required, `"menu-" + Nr`. - **tableId**: Required – the table the menu writes to (e.g. `tableId("Dashboard")`). - **menuItems**: Each item needs `item` (number), `title`, `active` (boolean), `actions`. - **actions**: Use `type: "update"` with `recordId: Nr`, `field: fieldId(Nr, "Feldname")`. - **active state**: Set `active: Feldname = "Wert"` to highlight current selection. - **embedded**: Default `false`, set `true` when nested. # Custom Menu ![](https://framerusercontent.com/images/OTLGI4WPfhC5wvwpr6txpt5tjW4.png) Das Widget `arcCustomMenu` ist ein flexibles Menü-System, mit dem du Navigationselemente, Filter oder Umschaltoptionen direkt auf deiner Oberfläche darstellen kannst. Ideal, um zwischen Ansichten zu wechseln, Datenfelder zu setzen oder Popups auszulösen – ganz ohne komplexes UI-Bauen in Ninox. Das Menü lässt sich optisch komplett an dein Design anpassen: Farben, Schriftgrößen, Icons, aktive Zustände und Hover-Effekte sind frei konfigurierbar. Buttons können zusätzlich Badges tragen und mit verschiedenen Aktionen belegt werden (`update`, `popup`, `delete`). 💡 Das Widget kann **eingebettet** in bestehende Layouts verwendet werden oder als eigene Komponente dargestellt werden – je nach Einsatzszenario. ## 🔧 Struktur des arcCustomMenu-Widgets Das `arcCustomMenu` besteht aus einem zentralen Konfigurationsobjekt mit folgenden Bereichen: - **Allgemeine Einstellungen**

Direkt auf oberster Ebene (`uniqueId`, `embedded`, `hoverActions`) - **Menü-Konfiguration (**`**menu[…]**`**)**

Steuert das Layout der Menüleiste – z. B. Positionierung, Schriftgröße und Farben für normale und aktive Zustände - **Menüpunkte (**`**menu[…].menuItems[…]**`**)**

Einzelne Buttons mit Titel, Icon, Aktivitätszustand und Aktionen (z. B. Feldwert ändern, Popup öffnen) ```javascript let current := this; arcCustomMenu({ uniqueId: "menu " + Nr, embedded: false, borderRadius: "4px", hoverActions: { backgroundColor: "#192e65", fontColor: "#fff", borderColor: "#192e65", iconColor: "#fff", animation: "0.2s" }, menu: [{ tableId: tableId(current), embedded: false, settings: { menuBar: { width: "", position: "", backgroundColor: "", borderColor: "", fontSize: "20px" }, stateNormal: { backgroundColor: "", borderColor: "", fontColor: "" }, stateActive: { backgroundColor: "", borderColor: "", fontColor: "" } } }, { menuItems: [{ item: 1, title: "Table", active: Menu = text("A"), icon: arcCustomIcon({ name: "table" }), actions: [{ recordId: Nr, type: "update", field: "K", value: "A" }] }, { item: 2, title: "Kanban", active: Menu = text("B"), icon: arcCustomIcon({ name: "kanban" }), actions: [{ recordId: Nr, type: "update", field: "K", value: "B" }] }, { item: 3, title: "KPI Bar", active: Menu = text("C"), icon: arcCustomIcon({ name: "copy" }), actions: [{ recordId: Nr, type: "update", field: "K", value: "C" }] }] }] }) ``` ![](https://framerusercontent.com/images/9Pma4rkrUgqREGiOmgEUKOQ5nY.png) ## Allgemeine Parameter-Einstellungen ### uniqueId **Typ:** `text` **Pflichtfeld:** ja Definiert eine eindeutige ID für die jeweilige Menü-Instanz. Diese ID wird intern zur gezielten CSS-Ansprache verwendet und ist notwendig, damit mehrere Menüs auf derselben Seite unabhängig voneinander funktionieren. 💡 **Tipp: **Setze die ID dynamisch, z. B. mit `uniqueId: "menu-" + Nr`, wenn du das Widget mehrfach aufrufst. ### embedded **Typ:** `boolean` **Default:** `false` Legt fest, ob das Widget in ein bestehendes Widget (z. B. [Layout](https://www.arc-rider.de/documentation/custom-layout)) eingebettet wird oder als eigenständiges Widget auf der Ninox Oberfläche integriert wird. 💡 **Hinweis: **Wenn du `embedded: true` setzt, musst du sicherstellen, dass das umgebende Element eine passende Höhe und ggf. ein eigenes Layout mitbringt. Das Widget passt sich dann vollständig diesem Rahmen an. ### hoverActions **Typ:** `object` **Default:** kein Hover-Verhalten Definiert das Verhalten beim Überfahren eines Menüpunktes mit der Maus. Du kannst hier gezielt Farben und Animationen setzen, die beim Hover-Effekt angewendet werden sollen. Alle Felder sind optional – wenn du nichts angibst, bleibt das Standarddesign erhalten.

Feld Typ Beschreibung
`backgroundColor` `text` Hintergrundfarbe beim Hover. Z. B. HEX-Wert: `"#192e65"`
`fontColor` `text` Schriftfarbe beim Hover. Z. B. HEX-Wert: `"#fff"`
`borderColor` `text` Rahmenfarbe beim Hover. Z. B. HEX-Wert: `"#192e65"`
`iconColor` `text` Farbe des Icons beim Hover. Z. B. HEX-Wert:`"#fff"`
`animation` `text` Transition-Zeit, z. B. `"0.2s"` für weiche Effekte
💡 **Tipp: **Hover-Effekte verbessern die Benutzerführung deutlich – sie geben visuelles Feedback und zeigen, dass ein Element interaktiv ist. Schon ein einzelner Wert wie `backgroundColor` reicht aus, um dieses Feedback spürbar zu machen. ### borderRadius **Typ:** `text` **Default:** kein Radius (eckige Kanten) Legt den Eckenradius der Menü-Buttons fest. Mit diesem Wert kannst du die Buttons abgerundet gestalten – von leicht soften Ecken bis zu pillenförmig. Typische Werte: - `"4px"` für leicht abgerundet - `"12px"` für deutliche Rundung - `"50%"` für kreisförmige Buttons 💡 **Tipp: **Ein kleiner `borderRadius` sorgt oft schon für ein moderneres, freundlicheres Erscheinungsbild – vor allem in Kombination mit Hover-Farben. ## Menü-Einstellungen (`menu[…]`) In diesem Block steuerst du das Layout und Design der Menüleiste – also Ausrichtung, Größe und Farben. Er ist Teil des ersten Elements im `menu`-Array: ```javascript menu: [{ tableId: tableId(current), settings: { menuBar: { width: "", position: "", backgroundColor: "", borderColor: "", fontSize: "20px" }, stateNormal: { backgroundColor: "", borderColor: "", fontColor: "" }, stateActive: { backgroundColor: "", borderColor: "", fontColor: "" } } }, { menuItems: [...] }] ``` ### tableId **Typ:** `text` **Pflichtfeld:** ja Definiert die Tabelle, auf die sich Aktionen innerhalb des Menüs beziehen. Auch wenn der `tableId`-Wert aktuell nicht direkt verarbeitet wird, ist er Teil der erwarteten Datenstruktur und sollte immer gesetzt werden. ### settings-Block **Typ:** `object` **Pflichtfeld:** ja Enthält die visuellen Einstellungen für die Menüleiste und die Darstellung der Buttons – im normalen sowie im aktiven Zustand. Der `settings`-Block besteht aus drei Unterobjekten: - `menuBar` → Layout & Ausrichtung der Leiste - `stateNormal` → Design nicht aktiver Buttons - `stateActive` → Design aktiver Buttons 💡 **Tipp: **Wenn du Werte leer lässt, greifen automatisch sinnvolle Standardwerte – so kannst du das Menü auch minimal konfigurieren und es sieht trotzdem gut aus. ```javascript settings: { menuBar: { width: "", // Pixel-Werte o. Prozent-Werte in " position: "", // left, center, right backgroundColor: "", // HEX-Wert in " borderColor: "", // HEX-Wert in " fontSize: "100px" // Pixel-Wert in " }, stateNormal: { backgroundColor: "", // HEX-Wert in " | definiert die Hintergrundfarbe deines Buttons im unangeklickten Modus. borderColor: "", // HEX-Wert in " | definiert die Rahmengrundfarbe deines Buttons im unangeklickten Modus. fontColor: "" // HEX-Wert in " | definiert die Schriftfarbe deines Buttons im unangeklickten Modus. }, stateActive: { backgroundColor: "", // HEX-Wert in " | definiert die Hintergrundfarbe deines Buttons im angeklickten Modus. borderColor: "", // HEX-Wert in " | definiert die Rahmengrundfarbe deines Buttons im angeklickten Modus. fontColor: "" // HEX-Wert in " | definiert die Schriftfarbe deines Buttons im angeklickten Modus. } } } ``` ## menuItems[] Der zweite Teil des `menu`-Arrays enthält die eigentlichen Menüeinträge. Jeder Menüpunkt wird als Objekt in der Liste `menuItems` definiert. Ein Menüeintrag besteht in der Regel aus einem Titel, einem Icon, einem aktiven Zustand und einer oder mehreren auszuführenden Aktionen. ```javascript menuItems: [{ item: 1, field: "menuText", title: "Übersicht", activeState: if menuText = text("U") then "active" else " end, iconName: "info", actions: [{ recordId: Nr, type: "update", field: "B", value: "U" }] } ``` ### item Dieser Wert ist eine laufende Nummer, die intern zur Sortierung oder Identifikation der Menüeinträge verwendet wird. **Typ:** `number` **Pflichtfeld:** ja ```javascript item: 1, // Zahlenwert. Beschrifte hier deine Menüpunkte einfach nach Anzahl. ``` ### title Hier legst du den Text fest, der im Button angezeigt wird. Der Titel ist für die Nutzer sichtbar und sollte die Funktion des Buttons klar benennen. **Typ:** `text` **Pflichtfeld:** ja ```javascript title: "Übersicht", ``` ***activeState*** definiert, wann dein Menüpunkt aktiv sein soll. In diesem Beispiel: Wenn der Text "A" in dem Textfeld menuText steht, dann soll der Button aktiv sein. ```javascript activeState: if menuText = text("A") then "active" else " end, ``` ### icon Mit dem `icon`-Feld kannst du ein [Icon](https://www.arc-rider.de/documentation/custom-icon) zu deinem Button hinzufügen. Die Icons werden typischerweise über `arcCustomIcon({ name: "..." })` definiert. Wenn kein Icon angegeben wird, besteht der Button nur aus Text. **Typ:** `function` **Pflichtfeld:** nein ```javascript icon: "", // Kein Icon wird angezeigt icon: arcCustomIcon({ name: "kanban" }), // Benutze das Widget arcCiustomIcon() ``` ### active Das Feld `active` bestimmt, ob ein Menüpunkt gerade als aktiv markiert ist. Meist handelt es sich dabei um einen Ausdruck, der den aktuellen Zustand vergleicht – z. B. `Menu = "A"` oder `Menu=1`. Aktive Menüpunkte werden visuell hervorgehoben (z. B. mit einer anderen Hintergrundfarbe) und helfen den Nutzern zu erkennen, welcher Bereich gerade ausgewählt ist. **Typ:** `boolean` oder Ausdruck **Pflichtfeld:** ja ## actions In `actions` legst du fest, was beim Klick auf den Button passieren soll. Du kannst hier eine oder mehrere Aktionen definieren. Jeder Aktionseintrag ist ein Objekt mit einem `type` und ggf. zusätzlichen Parametern wie `recordId`, `field` oder `value`. **Typ:** `array` **Pflichtfeld:** ja 💡 **Hinweis: **Jede Aktion wird in der Reihenfolge ausgeführt, in der sie im Array steht. Du kannst mehrere Aktionen kombinieren – z. B. zuerst einen Feldwert setzen und danach ein Popup öffnen. ### Mögliche Aktionen
`type` Beschreibung Aufbau Code
`update` Setzt den Wert eines Feldes im angegebenen Datensatz `recordId`, `field`, `value`
`popup` Öffnet den angegebenen Datensatz als Popup `recordId`
`delete` Löscht den angegebenen Datensatz aus der Datenbank `recordId`
### Aktion: update ```javascript actions: [{ recordId: Nr, type: "update", // update aktualisiert/manipuliert ein Feld. field: "B", // Feld Id des zu manipulierenden Feldes. value: "A" // Wert, der in das Feld geschrieben werden soll. In dem Beispiel: wird "A" in ein Textfeld geschrieben. }] ``` ### Aktion: popup ```javascript actions: [{ recordId: Nr, type: "popup", // popup öffnet den Record }] ``` ### Aktion: delete ```javascript actions: [{ recordId: Nr, type: "delete", // delete löscht den Record }] ``` ## Fazit ✅ Mit `arcCustomMenu` kannst du flexible Menüleisten in deiner Ninox-Oberfläche erstellen – ideal zum Umschalten von Ansichten, Setzen von Feldwerten oder Öffnen von Popups. Das Widget lässt sich vollständig an dein Design anpassen: von Farben und Schriften bis hin zu Hover- und Aktiv-Zuständen. Let’s build smarter interfaces – with arcWidgets. Fragen oder Feedback? → [office@arc-rider.com](mailto:office@arc-rider.com) --- # Custom NumPad Source: https://www.arc-rider.com/docs/widgets/custom-numpad # Numpad in React Zahlen per Bildschirmtastatur erfassen, etwa Mengen oder Beträge. Das Ergebnis bleibt zunächst ein formatierter String. 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 { ArcWidgetNumpad } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [value, setValue] = useState('0'); return ( <>

{t('Übernommen', 'Committed', 'Confirmado')}: {value}

); } ``` ## Features ausprobieren ### Dezimalstellen, Suffix und Tastatureingabe Teste Ganzzahlen und Dezimalwerte. Tastaturereignisse gelten nur innerhalb der geöffneten Live-Vorschau; Enter übernimmt den Wert. ```tsx import { useState } from 'react'; import { ArcWidgetNumpad } from '@arcrider/arcwidgets-react'; export default function Example() { const [value, setValue] = useState('12.5'); const [decimals, setDecimals] = useState(2); const [keyboard, setKeyboard] = useState(false); return ( <>

Übernommen: {value} Stk

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetNumpadData`. Ü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). ### onSubmit Bestätigter Eingabestring. ```typescript onSubmit?: (value: string) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### currentValue Aktueller Anzeigewert. Typ: `string | number`. ### actionTitle Beschriftung der Bestätigung. Typ: `string`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### fontColor Schriftfarbe. Typ: `string`. ### display Darstellung der Zahlenanzeige. Typ: `ArcWidgetNumpadDisplay`. ### suffix Inhalt nach der Beschriftung oder Einheit. Typ: `string`. ### clearOnFirstInput Ersetzt den bisherigen Wert bei der ersten Eingabe. Typ: `boolean`. ### decimalSeparator Dezimaltrennzeichen des Eingabestrings. Typ: `string`. ### maxDecimals Maximale Anzahl Nachkommastellen. Typ: `number`. ### keyboard Bedienung über die physische Tastatur. Typ: `boolean | ArcWidgetNumpadKeyboard`. ## Verhalten und Grenzen - `onSubmit` liefert einen String, etwa `"12,50"`. Normalisiere das konfigurierte Dezimaltrennzeichen vor einer numerischen Berechnung und prüfe das Ergebnis. - `currentValue` befüllt die Anzeige. `clearOnFirstInput` ist hilfreich beim Überschreiben eines vorhandenen Werts. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetNumpadDisplay { fontColor?: string; height?: string; } export interface ArcWidgetNumpadKeyboard { enabled?: boolean; submitOnEnter?: boolean; } export interface ArcWidgetNumpadData extends Omit { currentValue?: string | number; actionTitle?: string; backgroundColor?: string; fontColor?: string; display?: ArcWidgetNumpadDisplay; suffix?: string; clearOnFirstInput?: boolean; decimalSeparator?: string; maxDecimals?: number; keyboard?: boolean | ArcWidgetNumpadKeyboard; } export interface ArcWidgetNumpadProps extends ArcReactWidgetProps { onSubmit?: (value: string) => void; } ``` ## Einsatzbeispiele - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom NumPad ## AI Defaults (read first) - **uniqueId**: Required, `"numpad-" + Nr`. - **embedded**: Default `false`, set `true` when nested in a layout. - **currentValue**: Initial display value (string or number). - **actions**: Required for submit — use `##numpadValue##` placeholder (numeric, dot decimal) like `##inputValue##` on Input. - **confirm / dataBag**: Standard action types — no Numpad-specific API; nest `confirm` or `dataBag` inside `actions`. - **keyboard**: Opt-in (`true` or `{ enabled: true, submitOnEnter: true }`) — physical keyboard when no other field is focused. - **fontColor**: Text color on digit and submit buttons (default `#ffffff`). Display uses `display.fontColor`. - **Optimized for iPad**: Primary use case is tablet input without keyboard popup. # Custom NumPad Mit dem Numpad kannst du deine Oberflächen noch besser gestalten. Besonders für iPads optimiert, bietet das Numpad eine nahtlose Möglichkeit, Zahlen direkt einzugeben – ganz ohne störend aufspringende Tastatur. Der Submit-Button (oder Enter bei aktivierter Tastatur) feuert das zentrale **Actions-System** — inklusive `confirm`, `dataBag` und beliebiger Action-Ketten. **Abgrenzung:** Für ein **Zahlenfeld mit kleinem Popup-Numpad** (analog Datepicker) nutze `arcCustomInput` mit `type: "number"` und `numpad` / `numberFormat` — siehe [Input](/documentation/input). Dieses Widget hier ist das **eigenständige** Inline-/Fullscreen-Numpad. ## Anwendungscode ```javascript arcCustomNumPad({ uniqueId: "numpad-" + Nr, embedded: false, currentValue: text(Menge), actionTitle: "Speichern", backgroundColor: "#007bff", fontColor: "#ffffff", display: { fontColor: "#121B2B", height: "30%" }, suffix: "€", clearOnFirstInput: true, decimalSeparator: ",", maxDecimals: 2, keyboard: true, actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "Menge"), value: "##numpadValue##" }] }) ``` ## Allgemeine Settings ### uniqueId Die `uniqueId` vergibst du individuell und sollte einzigartig sein. Wenn du mehrere Numpads mit unterschiedlichen Settings auf deiner Oberfläche erstellst, überschreiben sich die Styles nicht. ```javascript uniqueId: "numpad-" + Nr, ``` ### embedded Steuert, ob das Widget standalone oder in einem Layout eingebettet ist. ```javascript embedded: false, embedded: true, ``` ### currentValue Anfangswert in der Anzeige — z. B. der aktuelle Feldwert oder leer. ```javascript currentValue: text(Menge), currentValue: "", ``` ### actionTitle Text auf dem Submit-Button unterhalb des Ziffernfelds. Default: `"Submit"`. ```javascript actionTitle: "Speichern", ``` ### backgroundColor Hintergrundfarbe der Ziffern- und Submit-Buttons. Default: `#007bff`. Beim Drücken (Maus oder Tastatur) wird automatisch eine **leicht hellere** Variante derselben Farbe angezeigt. ```javascript backgroundColor: "#007bff", ``` ### fontColor Schriftfarbe auf den Ziffern- und Submit-Buttons. Default: `#ffffff`. ```javascript fontColor: "#ffffff", fontColor: "#121B2B", ``` ### display | Feld | Typ | Beschreibung | |------|-----|--------------| | `fontColor` | text | Schriftfarbe der Anzeige | | `height` | text | Max. Höhe der Anzeige (CSS, z. B. `"30%"`) | ```javascript display: { fontColor: "#121B2B", height: "30%" }, ``` ### suffix Optionales Suffix neben dem Wert (z. B. Währung oder Einheit). ```javascript suffix: "€", ``` ### clearOnFirstInput Wenn `true`, wird bei der **ersten** Zifferneingabe ein vorbelegter `currentValue` gelöscht und ersetzt — statt angehängt. ```javascript clearOnFirstInput: true, ``` ### decimalSeparator Anzeige-Trennzeichen für Dezimalstellen. Default: `","`. Der Platzhalter `##numpadValue##` wird immer als Punkt-Dezimalzahl aufgelöst (Ninox-kompatibel). ```javascript decimalSeparator: ",", decimalSeparator: ".", ``` ### maxDecimals Optionale Begrenzung der Nachkommastellen nach dem Dezimaltrennzeichen. ```javascript maxDecimals: 2, ``` ## actions Submit feuert `actions` mit aufgelöstem `##numpadValue##` — analog zu `##inputValue##` bei `arcCustomInput`. ```javascript actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "Menge"), value: "##numpadValue##" }] ``` Mehrere Actions werden nacheinander ausgeführt. ### confirm Bestätigungsdialog vor dem Speichern — kein Numpad-spezifischer Code nötig: ```javascript actions: [{ type: "confirm", title: "Wert übernehmen?", message: "##numpadValue## speichern?", confirmLabel: "Speichern", cancelLabel: "Abbrechen", actions: [{ type: "update", recordId: Nr, fieldId: fieldId(Nr, "Menge"), value: "##numpadValue##" }] }] ``` ### dataBag Partielle JSON-State-Updates — siehe [`data-bag-actions`](../patterns/data-bag-actions.md): ```javascript let stateField := fieldId(Nr, "helper_mainContentState"); actions: [{ type: "update", recordId: Nr, fieldId: stateField, dataBag: { base: helper_mainContentState, patch: { lastAmount: "##numpadValue##" } } }] ``` ## keyboard Physische Tastatur-Eingabe (opt-in). Default: `false`. ```javascript keyboard: true, ``` ```javascript keyboard: { enabled: true, submitOnEnter: true }, ``` | Taste | Wirkung | |-------|---------| | `0`–`9` | Ziffer anhängen | | `,` oder `.` | Dezimaltrennzeichen (einmal) | | `Backspace` | Letzte Ziffer löschen (⌫-Button) | | `Delete` / `Escape` | Alles löschen (`C`) | | `Enter` | Submit (`actions` feuern) | **Verhalten bei mehreren Numpads:** Der zuletzt geladene oder angeklickte Numpad ist aktiv. **Sicherheitsnetz:** Solange der Fokus in einem anderen Eingabefeld liegt (`input`, `textarea`, `arcCustomInput`, `arcCustomSelect`, …), greift der Numpad **nicht** auf Tastendrücke zu. ## Ziffernfeld Layout (3 Spalten): ``` 1 2 3 4 5 6 7 8 9 C ⌫ 0 , (Dezimal, volle Breite) ``` - **C** — Wert auf `0` zurücksetzen - **⌫** — letzte Ziffer entfernen - **Dezimal** — einmaliges Dezimaltrennzeichen (Label = `decimalSeparator`) --- # Custom Slider Source: https://www.arc-rider.com/docs/widgets/custom-slider # Slider in React Einen Zahlenwert innerhalb eines festen Bereichs verändern, etwa eine Auslastung oder Zielvorgabe. 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 { ArcWidgetProgressBar, ArcWidgetSlider, } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [value, setValue] = useState(42); return ( <>

{t('Übernommen', 'Committed', 'Confirmado')}: {value} %

); } ``` ## Features ausprobieren ### Schrittweite und gemeinsamer State Der Slider steuert den Fortschrittsbalken über denselben State. Ändere die Schrittweite und verschiebe den Regler. ```tsx import { useState } from 'react'; import { ArcWidgetSlider, ArcWidgetProgressBar, } from '@arcrider/arcwidgets-react'; export default function Example() { const [value, setValue] = useState(40); const [step, setStep] = useState(5); return ( <>

Übernommener Wert: {value}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetSliderData`. Ü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). ### onChange Übernimmt den neuen Eingabewert in deinen State. ```typescript onChange?: (value: number) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### value Anzuzeigender bzw. aktuell bearbeiteter Wert. Typ: `number`. ### min Kleinster Wert. Typ: `number`. ### max Größter Wert. Typ: `number`. ### step Schrittweite. Typ: `number`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### track Darstellung der gesamten Schiene. Typ: `ArcWidgetSliderTrackStyle`. ### fill Darstellung des gefüllten Bereichs. Typ: `ArcWidgetSliderFillStyle`. ### thumb Darstellung des Reglers. Typ: `ArcWidgetSliderThumbStyle`. ### valueDisplay Anzeige und Formatierung des Werts. Typ: `ArcWidgetSliderValueDisplay`. ### statusDisplay Beschriftungen für Wertebereiche. Typ: `ArcWidgetSliderStatusDisplay[]`. ## Verhalten und Grenzen - `onChange` liefert eine Zahl. Übernimm sie in deinen State und gib sie als `data.value` zurück. - Setze sinnvolle `min`, `max` und `step`. Änderungen im Widget speichern noch keine Daten. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetSliderTrackStyle { height?: string; backgroundColor?: string; borderRadius?: string; [key: string]: unknown; } export interface ArcWidgetSliderFillStyle { backgroundColor?: string; [key: string]: unknown; } export interface ArcWidgetSliderThumbStyle { width?: string; height?: string; backgroundColor?: string; borderColor?: string; borderWidth?: string; borderRadius?: string; [key: string]: unknown; } export interface ArcWidgetSliderValueDisplay { show?: boolean; color?: string; fontSize?: string; [key: string]: unknown; } export interface ArcWidgetSliderStatusDisplay { label?: string; value?: number; color?: string; track?: ArcWidgetSliderTrackStyle; fill?: ArcWidgetSliderFillStyle; thumb?: ArcWidgetSliderThumbStyle; [key: string]: unknown; } export interface ArcWidgetSliderData extends ArcWidgetBaseData { value?: number; min?: number; max?: number; step?: number; width?: string; height?: string; track?: ArcWidgetSliderTrackStyle; fill?: ArcWidgetSliderFillStyle; thumb?: ArcWidgetSliderThumbStyle; valueDisplay?: ArcWidgetSliderValueDisplay; statusDisplay?: ArcWidgetSliderStatusDisplay[]; } export interface ArcWidgetSliderProps extends ArcReactWidgetProps { onChange?: (value: number) => void; } ``` ## Einsatzbeispiele - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Slider ## AI Defaults (read first) - **Function name**: `arcCustomSlider`. - **Purpose**: Ein Wertregler (0–100 oder eigener Bereich) zum Ziehen per Maus/Touch – **kein** Bild-/Content-Slideshow-Widget. Für Bildergalerien: [`arcCustomSlideshow`](/documentation/custom-slideshow). - **recordId + fieldId**: Nur nötig, wenn der Wert automatisch gespeichert werden soll. Ohne beide Felder wird beim Loslassen **nicht** persistiert. - **Persistence**: Speichern läuft über **Action Performer** (`type: "update"` mit `recordId` / `field` / `value`). React: `onAction` — kein globales `database`. - **value / min / max / step**: Default `0` / `0` / `100` / `1`. - **valueDisplay.show**: Default `true` (Zahlenwert wird angezeigt). `false` blendet die Zahl aus. - **statusDisplay**: Optionales Array für Icons + dynamische Einfärbung je Schwellenwert (z. B. 🔥 ab 80). Leeres Array oder weglassen = keine Icons. # Custom Slider Mit `arcCustomSlider` baust du einen interaktiven Wertregler in Ninox – auf Basis von reinem JavaScript, nicht dem nativen HTML-Range-Input. Der Nutzer zieht den Regler-Knopf (Thumb) per Maus oder Touch, oder klickt direkt auf die Leiste (Track), um den Wert zu setzen. **Das kannst du steuern:** - Wertebereich (`min`, `max`, `step`) - Aussehen von Track, Fill (gefüllter Bereich) und Thumb - Anzeige des aktuellen Zahlenwerts (ein-/ausblenden) - Dynamische Icons + Einfärbung je Schwellenwert (z. B. Status-Ampel) - Automatisches Speichern in ein Ninox-Feld beim Loslassen ## Anwendungscode ```javascript arcCustomSlider({ value: current.Fortschritt, min: 0, max: 100, step: 1, width: "300px", height: "50px", recordId: current.Nr, fieldId: fieldId("Aufgaben", "Fortschritt"), track: { height: "6px", borderRadius: "3px", backgroundColor: "#E9ECF4" }, fill: { backgroundColor: "#4970ff" }, thumb: { width: "20px", height: "20px", backgroundColor: "#ffffff", borderColor: "#ccc", borderWidth: "2px", borderRadius: "50%" }, valueDisplay: { show: true, color: "#515971", fontSize: "14px" }, statusDisplay: [{ label: "🔥", value: 80, color: "#ff4444" }, { label: "✓", value: 50, color: "#4CAF50" }, { label: "⚠", value: 20, color: "#ff9800" }] }) ``` ## Parameter ### value / min / max / step **Typ:** `number` **Default:** `0` / `0` / `100` / `1` Aktueller Wert, Wertebereich und Schrittweite. `value` wird beim Laden einmalig gesetzt – Ninox-Feldbindung übernimmst du selbst (z. B. `value: current.Fortschritt`). ```javascript value: 50, min: 0, max: 100, step: 1, ``` ### width / height **Typ:** `text` Größe des gesamten Slider-Containers. ```javascript width: "300px", // oder "100%" height: "50px", ``` ### recordId / fieldId **Typ:** `text` **Optional, aber beide zusammen nötig zum automatischen Speichern** Beim Loslassen des Reglers (`mouseup`/`touchend`) wird – **nur wenn beide gesetzt sind** – eine Action-Performer-Action `{ type: "update", recordId, field: fieldId, value }` ausgelöst (Ninox mappt das auf `database.update`; React über `onAction`). ```javascript recordId: current.Nr, fieldId: fieldId("Aufgaben", "Fortschritt"), ``` 💡 **Hinweis:** Fehlt eines der beiden Felder, wird **nicht** persistiert – der Wert geht sonst **nicht** verloren im UI. ### Block: track{} **Typ:** `object` **Optional** Styling der Regler-Leiste (Hintergrund der gesamten Spur).
Feld Beschreibung
`height` Höhe des Tracks, z. B. `"6px"`
`backgroundColor` Farbe des Tracks, z. B. `"#E9ECF4"`
`borderRadius` Border-Radius des Tracks, z. B. `"3px"`
### Block: fill{} **Typ:** `object` **Optional** Styling des **gefüllten** Bereichs zwischen Start und aktuellem Wert. ```javascript fill: { backgroundColor: "#4970ff" } ``` ### Block: thumb{} **Typ:** `object` **Optional** Styling des Regler-Knopfs.
Feld Beschreibung
`width` / `height` Größe des Knopfs, z. B. `"20px"`
`backgroundColor` Füllfarbe des Knopfs
`borderColor` / `borderWidth` / `borderRadius` Rahmen des Knopfs, z. B. `borderRadius: "50%"` für rund
### Block: valueDisplay{} **Typ:** `object` **Default:** `{ show: true }` Steuert die Anzeige des aktuellen Zahlenwerts neben/unter dem Regler. ```javascript valueDisplay: { show: true, // false blendet die Zahl komplett aus color: "#515971", fontSize: "14px" } ``` ### statusDisplay **Typ:** `array of objects` **Optional** Zeigt ein Icon/Symbol an, sobald der aktuelle Wert einen Schwellenwert erreicht – zusätzlich kann jeder Status Track/Fill/Thumb umfärben. Die Einträge werden intern nach `value` absteigend sortiert; der **höchste erreichte** Schwellenwert gewinnt.
Feld Beschreibung
`label` Icon oder Text, z. B. `"🔥"`, `"✓"`, `"⚠"`, `"✕"`
`value` Schwellenwert – ab diesem Wert wird der Status aktiv
`color` Farbe des Icon-Texts
`track` / `fill` / `thumb` Optional: gleiche Struktur wie oben, überschreibt das Standard-Styling für diesen Status (mit `!important`)
```javascript statusDisplay: [{ label: "🔥", value: 80, color: "#ff4444", fill: { backgroundColor: "#ff4444" } }, { label: "✓", value: 50, color: "#4CAF50" }] ``` 💡 **Tipp:** Leeres Array (`statusDisplay: []`) oder Weglassen = keine Icons, nur der reine Regler mit Zahlenwert. ## Verwandtes Widget Für Bildergalerien, Content-Karussells oder mehrseitige Text-Slides mit Swipe-Geste verwendest du stattdessen [`arcCustomSlideshow`](/documentation/custom-slideshow) – trotz ähnlichem Namen ein komplett anderes Widget. --- # Custom Slideshow Source: https://www.arc-rider.com/docs/widgets/custom-slideshow # Slideshow in React Text oder eigenes HTML als Folge von Slides anzeigen. Geeignet für einfache Bildgalerien oder wechselnde Hinweise. 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 { ArcWidgetSlideshow } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const demoItems = [ { value: `

01 · ${t('Entdecken', 'Discover', 'Descubrir')}

${t('Anforderungen und Ziele sammeln.', 'Collect requirements and goals.', 'Reúne requisitos y objetivos.')}

`, backgroundColor: '#ecfdf5', }, { value: `

02 · ${t('Gestalten', 'Design', 'Diseñar')}

${t('Ansichten mit Widgets zusammensetzen.', 'Compose views with widgets.', 'Monta vistas con widgets.')}

`, backgroundColor: '#fffbeb', }, { value: `

03 · ${t('Verbinden', 'Connect', 'Conectar')}

${t('State und Callbacks an eure API hängen.', 'Wire state and callbacks to your API.', 'Conecta estado y callbacks a tu API.')}

`, backgroundColor: '#f5f3ff', }, ]; const busyItems = [ ...demoItems, { value: `

04 · ${t('Ausliefern', 'Ship', 'Publicar')}

${t('Lizenz setzen und live gehen.', 'Set the license and go live.', 'Activa la licencia y publica.')}

`, backgroundColor: '#fff1f2', }, ]; const items = pack === 'empty' ? [] : pack === 'busy' ? busyItems : demoItems; export default function Example() { const [index, setIndex] = useState(0); return ( <> setIndex(payload.index)} />

{t('Folie', 'Slide', 'Diapositiva')} {items.length ? index + 1 : 0}

); } ``` ## Features ausprobieren ### Richtung, Schleife und Autoplay Teste Pfeile, Punkte und die automatische Wiedergabe. Die Folien verwenden feste HTML-Strings; JSX ist in Slideshow-Inhalten nicht unterstützt. ```tsx import { useState } from 'react'; import { ArcWidgetSlideshow } from '@arcrider/arcwidgets-react'; export default function Example() { const [vertical, setVertical] = useState(false); const [autoplay, setAutoplay] = useState(false); const [loop, setLoop] = useState(true); const [index, setIndex] = useState(0); return ( <> 01 · Entdecken

Anforderungen und Ziele sammeln.

', backgroundColor: '#f4f4f5', }, { value: '

02 · Gestalten

Ansichten mit Widgets zusammensetzen.

', backgroundColor: '#e4e4e7', }, { value: '

03 · Ausprobieren

State und Callbacks verbinden.

', backgroundColor: '#fafafa', }, ], }} onSlideChange={(p) => setIndex(p.index)} />

Aktuelle Folie: {index + 1}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetSlideshowData`. Ü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). ### onSlideChange Nullbasierter Index des aktiven Slides. ```typescript onSlideChange?: (payload: { index: number }) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### items Einträge der Auswahl oder der Darstellung; siehe den zugehörigen Datentyp. Typ: `ArcWidgetSlideshowItem[]`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### borderRadius Abrundung der Ecken. Typ: `string`. ### borderWidth Rahmenstärke. Typ: `string`. ### borderColor Rahmenfarbe. Typ: `string`. ### direction Anordnung horizontal oder vertikal. Typ: `'horizontal' | 'vertical' | string`. ### loop Nach dem letzten Slide wieder zum ersten wechseln. Typ: `boolean`. ### autoplay Automatischer Wechsel mit `enabled` und `speed` in Millisekunden. Typ: `{ enabled?: boolean; speed?: number; [key: string]: unknown }`. ### pause Unterbrechung der automatischen Wiedergabe bei Interaktion. Typ: `{ hover?: boolean; focus?: boolean; [key: string]: unknown }`. ### transition Animation, Dauer und Verzögerung in Millisekunden. Typ: `{ duration?: number; delay?: number; transition?: string; [key: string]: unknown }`. ### indicators Sichtbarkeit und Darstellung der Slide-Indikatoren. Typ: `{ enabled?: boolean; type?: string; position?: string; [key: string]: unknown }`. ### controls Konfiguration der Navigation. Typ: `{ enabled?: boolean; [key: string]: unknown }`. ### scroll Wechsel durch Scrollen. Typ: `{ enabled?: boolean; speed?: number; [key: string]: unknown }`. ## Verhalten und Grenzen - `items[].value` unterstützt derzeit Strings, einschließlich eigenem HTML. JSX und verschachtelte Widget-Deskriptoren werden in dieser Komponente nicht gerendert. - `autoplay.speed` und `transition.duration` werden in Millisekunden angegeben. `onSlideChange` liefert einen nullbasierten `index`. - Das Galerie-Beispiel verwendet selbst erzeugte SVG-Bilder. HTML mit dynamischen, fremden Inhalten muss vor Übergabe bereinigt werden. ## Datentypen Diese widgetbezogenen Typen werden aus dem Paket exportiert. Gemeinsame Typen wie `ArcWidgetSlot`, `ArcWidgetStyles` und `ArcReactWidgetProps` sind ebenfalls verfügbar. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetSlideshowItem { value: string; label?: string; width?: string; height?: string; backgroundColor?: string; [key: string]: unknown; } export interface ArcWidgetSlideshowData extends ArcWidgetBaseData { items: ArcWidgetSlideshowItem[]; width?: string; height?: string; backgroundColor?: string; borderRadius?: string; borderWidth?: string; borderColor?: string; direction?: 'horizontal' | 'vertical' | string; loop?: boolean; autoplay?: { enabled?: boolean; speed?: number; [key: string]: unknown }; pause?: { hover?: boolean; focus?: boolean; [key: string]: unknown }; transition?: { duration?: number; delay?: number; transition?: string; [key: string]: unknown }; indicators?: { enabled?: boolean; type?: string; position?: string; [key: string]: unknown }; controls?: { enabled?: boolean; [key: string]: unknown }; scroll?: { enabled?: boolean; speed?: number; [key: string]: unknown }; } export interface ArcWidgetSlideshowProps extends ArcReactWidgetProps { onSlideChange?: (payload: { index: number }) => void; } ``` ## Einsatzbeispiele - [Bildergalerie mit Slideshow](/docs/react/examples/image-slider) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Slideshow ## AI Defaults (read first) - **Function name**: `arcCustomSlideshow`. - **items**: Required array – each item needs at least `value` (Text, HTML-String, or ``-HTML). Without `items` the slideshow renders empty. - **width / height**: Default `"100%"` / `"300px"` – set explicitly when not embedded. - **embedded**: Default `false`. Set `true` when nesting inside `arcCustomLayout` etc. – the slideshow then fills `100%`/`100%` of its parent instead of using `width`/`height`. - **direction**: `"horizontal"` (default) or `"vertical"` – controls both the slide track and swipe/drag direction. - **loop**: Default `true` – set `false` to stop at the first/last slide. - **autoplay**: Default `{ enabled: false, speed: 3000 }` – only starts if `enabled: true`. - **Not yet wired up**: `controls.enabled`, `indicators.type`, `indicators.position`, top-level `scroll`, and `pause.scroll` / `pause.click` / `pause.touch` are accepted in `data` but currently have **no effect** – nav arrows and dot indicators are always shown, dots are always at the bottom (horizontal) / right (vertical). Only `pause.hover` and `pause.focus` actually pause autoplay. Don't rely on these for a client build without re-checking the code. - **uniqueId**: Not required by this widget (unlike most others) – the container `id` passed by the adapter is used directly. # Custom Slideshow Mit `arcCustomSlideshow` baust du eine Bildergalerie / Slideshow direkt in Ninox – mit Swipe-Geste (Touch & Maus-Drag), Pfeil-Navigation, Punkt-Indikatoren und optionalem Autoplay. Es eignet sich für Bildergalerien, Produktvorschauen oder einfache Text-Slides innerhalb eines Dashboards oder Layouts. **Das kannst du steuern:** - Ausrichtung (horizontal / vertikal) - Größe, Rahmen, Hintergrundfarbe des Containers - Autoplay (an/aus, Geschwindigkeit) - Loop-Verhalten an den Rändern - Übergangs-Animation (Dauer, Easing) - Inhalt jedes einzelnen Slides (Text, HTML, Bild) ## Anwendungscode ```javascript arcCustomSlideshow({ embedded: false, width: "100%", height: "400px", backgroundColor: "#f5f5f5", borderRadius: "8px", borderWidth: "0px", borderColor: "transparent", direction: "horizontal", loop: true, autoplay: { enabled: true, speed: 4000 }, pause: { hover: true, focus: true }, transition: { duration: 300, transition: "ease-in-out" }, indicators: { enabled: true }, items: [{ value: "", label: "Bild 1" }, { value: "", label: "Bild 2" }, { value: "Ein einfacher Text-Slide", backgroundColor: "#e3f2fd" }] }) ``` ## Parameter ### embedded **Typ:** `boolean` **Default:** `false` Bestimmt, ob die Slideshow **eingebettet** in ein anderes Widget (z. B. `arcCustomLayout`) dargestellt wird oder **eigenständig** auf der Oberfläche steht. - `true` → Slideshow füllt `100%` Breite/Höhe des umgebenden Containers. - `false` → Slideshow verwendet `width` / `height` direkt. ### width / height **Typ:** `text` **Default:** `"100%"` / `"300px"` Breite und Höhe der Slideshow, wenn `embedded: false`. Bei `embedded: true` werden diese Werte ignoriert (immer `100%`/`100%`). ```javascript width: "100%", height: "400px", ``` ### backgroundColor / borderRadius / borderWidth / borderColor **Typ:** `text` **Default:** `"#f5f5f5"` / `"8px"` / `"0px"` / `"transparent"` Styling des Slideshow-Containers (nicht der einzelnen Slides). ```javascript backgroundColor: "#f5f5f5", borderRadius: "8px", borderWidth: "1px", borderColor: "#ddd", ``` ### direction **Typ:** `text` **Default:** `"horizontal"` Legt fest, ob Slides horizontal (nebeneinander, Swipe links/rechts) oder vertikal (untereinander, Swipe hoch/runter) angeordnet sind. Die Pfeil-Buttons und Punkt-Indikatoren passen ihre Position automatisch an. ```javascript direction: "horizontal", // Standard – Pfeile links/rechts direction: "vertical", // Pfeile oben/unten ``` ### loop **Typ:** `boolean` **Default:** `true` - `true` → Nach dem letzten Slide geht es zurück zum ersten (und umgekehrt). - `false` → Am ersten/letzten Slide stoppt die Navigation (kein Wrap-around). ### Block: autoplay{} **Typ:** `object` **Default:** `{ enabled: false, speed: 3000 }` ```javascript autoplay: { enabled: true, speed: 4000 // ms zwischen den Slides } ``` ### Block: pause{} **Typ:** `object` **Default:** `{ hover: true, focus: true, scroll: true, click: true, touch: true }` ⚠️ **Nur `hover` und `focus` sind aktuell implementiert.** `scroll`, `click`, `touch` werden zwar entgegengenommen, pausieren das Autoplay aber (noch) nicht. ```javascript pause: { hover: true, // Autoplay pausiert, solange die Maus über der Slideshow ist focus: true // Autoplay pausiert, solange die Slideshow fokussiert ist (z. B. per Tab-Taste) } ``` ### Block: transition{} **Typ:** `object` **Default:** `{ duration: 300, delay: 0, transition: "ease-in-out" }` Steuert die Animation beim Wechsel zwischen Slides. `delay` wird aktuell nicht ausgewertet. ```javascript transition: { duration: 300, // ms transition: "ease-in-out" // CSS-Timing-Funktion } ``` ### Block: indicators{} **Typ:** `object` **Default:** `{ enabled: true, type: "dots", position: "bottom" }` ⚠️ Nur `enabled` hat aktuell einen Effekt (Punkte ein-/ausblenden). `type` und `position` sind reserviert für spätere Erweiterungen. ```javascript indicators: { enabled: true // false blendet die Punkt-Navigation komplett aus } ``` ### items **Typ:** `array of objects` **Pflichtfeld** Jeder Eintrag beschreibt einen Slide.
Feld Typ Beschreibung
`value` `text` Inhalt des Slides: einfacher Text, HTML-String (`
…
`), oder ein ``-HTML-Fragment (z. B. Ergebnis von `shareFile()`).
`label` `text` Fallback-Text, falls `value` leer ist oder kein String – z. B. `"Slide 1"`.
`width` / `height` `text` Größe des einzelnen Slides (Default: `100%` / `100%`).
`backgroundColor` `text` Individuelle Hintergrundfarbe für diesen Slide.
```javascript items: [{ value: "", label: "Bild 1" }, { value: "Nur Text als Slide", backgroundColor: "#e3f2fd" }] ``` 💡 **Tipp:** Für eine echte Bildergalerie aus einer Ninox-Tabelle baue `items` per `for`-Schleife über deine Bilder-Tabelle auf (z. B. mit `shareFile()` pro Datensatz), statt die Slides einzeln fest zu codieren. ## Verwandtes Widget Für einen klassischen **Wertregler** (z. B. Prozent- oder Zahlenwert per Ziehen einstellen) verwendest du stattdessen [`arcCustomSlider`](/documentation/custom-slider) – das ist ein eigenständiges Widget und hat mit der Bild-/Content-Slideshow nichts zu tun. --- # Custom Steps Source: https://www.arc-rider.com/docs/widgets/custom-steps # Steps in React Den Fortschritt in einem mehrstufigen Formular anzeigen. Navigation und Validierung steuerst du in React. 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 { ArcWidgetSteps } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const demoSteps = [ { title: t('Eingabe', 'Input', 'Entrada'), color: '#d97706' }, { title: t('Prüfung', 'Review', 'Revisión'), color: '#0f766e' }, { title: t('Abschluss', 'Done', 'Cierre'), color: '#15803d' }, ]; const busySteps = [ ...demoSteps.slice(0, 2), { title: t('Freigabe', 'Approval', 'Aprobación'), color: '#7c3aed' }, demoSteps[2], ]; const steps = pack === 'empty' ? [] : pack === 'busy' ? busySteps : demoSteps; export default function Example() { const [step, setStep] = useState(0); return ( <> ({ title: item.title, value: String(index + 1), clickable: true, backgroundColor: index === step ? item.color : '#f4f4f5', fontColor: index === step ? '#ffffff' : '#18181b', })), }} onClick={(_event, index) => setStep(index)} />

{t('Schritt', 'Step', 'Paso')} {steps.length ? step + 1 : 0}

); } ``` ## Features ausprobieren ### Schritte anklicken und Fortschritt übernehmen Die Schritte und der Weiter-Button verändern denselben aktiven Schritt. Alle bereits erreichten Schritte erhalten eine eigene Farbe. ```tsx import { useState } from 'react'; import { ArcWidgetSteps, ArcWidgetButton } from '@arcrider/arcwidgets-react'; export default function Example() { const [step, setStep] = useState(0); const labels = ['Anfrage', 'Planung', 'Umsetzung', 'Abnahme']; return ( <> ({ title, value: String(i + 1), clickable: true, backgroundColor: i <= step ? '#18181b' : '#e4e4e7', fontColor: i <= step ? '#ffffff' : '#52525b', })), }} onClick={(_event, index) => setStep(index)} /> setStep((s) => (s + 1) % 4)} />

Aktuell: {labels[step]}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetStepsData`. Ü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). ### onClick Meldet den Klick. Beachte die widgetspezifischen Argumente unten. ```typescript onClick?: (event: unknown, stepIndex: number) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### settings Gemeinsame Darstellung der Einträge. Typ: `ArcWidgetStepsSettings`. ### steps Schritte mit Titel, Status und optionaler Klickbarkeit. Typ: `ArcWidgetStepsStep[]`. ## Verhalten und Grenzen - Leite Beschriftung und Farben jedes Schritts aus deinem State ab; klickbare Schritte brauchen `clickable: true`. - `onClick(event, stepIndex)` meldet den angeklickten Schritt. Prüfe vor dem Wechsel die Eingaben des aktuellen Schritts. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetStepsSettings { embedded?: boolean; height?: string; colorLabel?: string; fontSizeLabel?: string; colorValue?: string; fontSizeValue?: string; backgroundColor?: string; borderColor?: string; alignX?: 'left' | 'center' | 'right' | string; stepWidth?: string; gapY?: string; stepRadius?: string; connectionLine?: { show?: boolean; height?: string; }; [key: string]: unknown; } export interface ArcWidgetStepsStep { title?: string; value?: string; subtitle?: string; colorLabel?: string; fontSizeLabel?: string; colorValue?: string; fontSizeValue?: string; backgroundColor?: string; width?: string; clickable?: boolean; } export interface ArcWidgetStepsData extends Omit { settings?: ArcWidgetStepsSettings; steps?: ArcWidgetStepsStep[]; } export interface ArcWidgetStepsProps extends ArcReactWidgetProps { onClick?: (event: unknown, stepIndex: number) => void; } ``` ## Einsatzbeispiele - [Status auswählen und speichern](/docs/react/examples/status-als-buttons-ausgeben) - [Ein Formular in Schritten](/docs/react/examples/datenaufnahme-in-schritten) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Steps ## AI Defaults (read first) - **uniqueId**: Required, `"steps-" + Nr`. - **steps**: Array of `{ title: "...", value: "..." }` objects. - **height**: `"100%"` when embedded. Fixed height (e.g. `"80px"`) when standalone. - **embedded**: Lives inside `settings: { embedded: true }`, NOT at root level. - **Klick (Ninox):** `actions: [...]` auf dem Step — der Adapter setzt intern `clickable: true`. `type: "update"` mit `recordId: Nr`, `field: fieldId(Nr, "Feldname")`. - **Klick (React):** `clickable: true` auf dem Step; `onClick(event, stepIndex)` am Host (`stepIndex` = 0-basierter Index in `steps`) — **kein** `actions` in `data`. - **stepWidth**: Default `"80px"`, set wider for longer labels. - **connectionLine**: `{ show: true, height: "3px" }` for visual step connections. # Visualisiere Fortschritte oder Status – mit Stil. Mit dem `arcCustomSteps`-Widget kannst du mehrstufige Prozesse, Status- oder Fortschrittsanzeigen direkt in deinem Ninox-Interface umsetzen – klar strukturiert, visuell ansprechend und vollständig anpassbar. Ob Projektphasen, Prozessstatus oder Ampelsysteme: Die Steps lassen sich flexibel in Breite, Farben, Abständen und Aktionen konfigurieren. Die einzelnen Schritte können dynamisch aus deinen Datensätzen aufgebaut und optional mit Aktionen (z. B. `update`, `delete`, `popup`) versehen werden. So entsteht eine interaktive Fortschrittsanzeige, die nicht nur informiert, sondern auch steuert. In dieser Dokumentation zeigen wir dir: - wie das Widget aufgebaut ist, - welche Parameter du anpassen kannst, - und wie du es Schritt für Schritt in dein System integrierst. 👉 **Scroll weiter für Beispielcode, Parameterübersicht und Anwendungstipps.** ![](https://framerusercontent.com/images/i14A175WOPvdKLDT7G7rK5XQlmE.png) ## Anwendungscode ```javascript let current := this; let list := (select 'Auftrag Steps'); let data := { uniqueId: "Auftragsphase", settings: { embedded: true, height: "150px", fontSizeLabel: "10px", fontSizeValue: "20px", colorValue: "", colorLabel: "#ffffff", borderColor: "transparent", backgroundColor: "transparent", alignX: "center", stepWidth: "80px", gapY: "10px", stepRadius: "8px", connectionLine: { show: true, height: "3px" } }, steps: list.[{ title: title, value: value, subtitle: "Test", colorValue: "", backgroundColor: "", actions: [{ recordId: current.Nr, type: "update", field: "", value: number(Nr) }] }] }; arcCustomSteps(data) ``` ## Parameter Das Widget erhält ein Objekt mit folgenden Hauptbestandteilen: - `uniqueId` – eine eindeutige Kennung für das Widget - `settings` – allgemeine Darstellungseinstellungen für alle Steps - `steps[]` – eine Liste von Einzelschritten mit optionalen Detailwerten Die Werte aus `settings` gelten für alle Steps, können aber pro Step gezielt überschrieben werden (z. B. Farbe oder Breite). ## Settings-Block ### embedded Typ: `boolean` Default: `false` Legt fest, ob das Widget in ein bestehendes Layout eingebettet wird. - Bei `true` wird auf äußere Rahmen, Hintergrundfarben und Standard-Abstände verzichtet. - Ideal, wenn das Widget in einem größeren Komponenten-Container liegt, der selbst das Layout steuert. 💡 **Tipp: **Wenn du `embedded: true` setzt, musst du sicherstellen, dass das umgebende Element eine passende Höhe und ggf. ein eigenes Layout mitbringt. Das Widget passt sich dann vollständig diesem Rahmen an. ### height **Typ:** `text` **Default:** `"100%"` Definiert die Höhe des gesamten Steps-Widgets. Akzeptiert klassische CSS-Werte wie `"150px"`, `"auto"` oder `"100%"`. ```javascript height: "150px", height: "", ``` 💡 **Tipp: **Wenn du das Widget **nicht eingebettet** nutzt (`embedded: false`), solltest du eine feste Höhe setzen – insbesondere bei schmalen Step-Breiten, damit Layout und Scrollverhalten konsistent bleiben. ![](https://framerusercontent.com/images/IprIm5AORs6cO1ATQc2gvMZtdc.png) ### fontsizeLabel **Typ:** `text` **Default:** `"13px"` Bestimmt die Schriftgröße für `title` und `subtitle` innerhalb eines Steps. Dieser Wert wirkt sowohl auf die obere (`title`) als auch untere (`subtitle`) Beschriftung jedes Steps – sofern kein eigener Wert direkt im Step gesetzt ist. ```javascript fontSizeLabel: "10px", // Pixel-Werte ``` 💡 **Tipp: **Für kompakte Layouts empfiehlt sich eine kleinere Schriftgröße wie `"10px"` oder `"11px"` – besonders bei vielen Steps mit schmaler Breite. ### fontsizeValue **Typ:** `text` **Default:** `"35px"` Gibt die Schriftgröße für den zentralen Wert (`value`) eines Steps an – also die Zahl oder Kennung im Kreis. Dieser Wert beeinflusst die Lesbarkeit und visuelle Gewichtung des zentralen Inhalts. Wird häufig für Statuszahlen, Zähler oder Phasen verwendet. ```javascript fontSizeValue: "20px", // Pixel-Werte ``` 💡 **Tipp: **Kleinere Werte wie `"20px"` eignen sich für sehr kompakte Steps, größere Größen wirken besonders bei wenigen, breit angelegten Schritten. ### colorValue **Typ:** `text` **Default:** `"#3E6FFF"` Legt die Textfarbe des zentralen `value`-Elements im Step-Kreis fest. Die Farbe bezieht sich ausschließlich auf den Text (z. B. Zahl, Status oder Kennung) – nicht auf den Hintergrund des Kreises. ```javascript colorValue: "#ddd", // HEX-Werte ``` 💡 **Tipp: **Die Lesbarkeit hängt stark vom Hintergrund ab. Achte auf ausreichend Kontrast zur `backgroundColor`. ### colorLabel **Typ:** `text` **Default:** `"#566eb1"` Definiert die Textfarbe für `title` und `subtitle` innerhalb eines Steps. Dieser Farbwert wird für die obere und untere Beschriftung verwendet – solange der Step selbst keine abweichende Farbe angibt. ```javascript colorLabel: "#ffffff", // HEX-Werte ``` 💡 **Tipp: **Verwende helle Schriftfarben wie `#ffffff` nur bei dunklem Hintergrund – ansonsten leidet die Lesbarkeit. ### borderColor **Typ:** `text` **Default:** `"#e5e5e5"` Definiert die Farbe der Trennlinien zwischen den Steps. Diese Farbe wird als rechte Rahmenlinie jedes Steps gesetzt (außer beim letzten Step). Sie hilft, die Schritte visuell voneinander abzugrenzen. 💡 **Tipp: **Wenn du eine besonders reduzierte oder fließende Darstellung möchtest, setze `borderColor: "transparent"` – so verschwinden die Trennlinien vollständig. ### backgroundColor **Typ:** `text` **Default:** `"#f7f8fc"` Bestimmt die Hintergrundfarbe der Step-Kreise **und** der Verbindungslinien. Diese Farbe wird auf die mittleren Kreise (`value`) sowie die Linien zwischen den Steps angewendet – nicht jedoch auf den äußeren Hintergrund des gesamten Widgets (dieser ist meist transparent oder durch Container geregelt). ```javascript backgroundColor: "transparent", // Transparenter Hintergrund backgroundColor: "#ededed", // HEX-Werte ``` 💡 **Tipp: **Ein heller Hintergrund sorgt für einen klaren, modernen Look. Für Spezialeffekte kannst du auch `"transparent"` oder leicht getönte Farben einsetzen. ### alignX **Typ:** `text` **Werte:** `"left"`, `"center"` (default), `"right"` Steuert die horizontale Ausrichtung der Steps innerhalb des Widgets. ```javascript alignX: "center", // left, center, right algnX: "", // default: center ``` Diese Einstellung beeinflusst, **wo die Steps innerhalb der Leiste ausgerichtet werden**, insbesondere wenn weniger Steps vorhanden sind als Platz zur Verfügung steht. - `"left"` → Steps starten ganz links - `"center"` → Steps zentriert - `"right"` → Steps rechtsbündig 💡 **Hinweis: **Diese Ausrichtung gilt sowohl für das gesamte Widget als auch für die Inhalte in jedem Step (z. B. Titel, Subtitle, Value). ### stepWidth **Typ:** `text` **Beispiel:** `"80px"` **Default:** *(leer)* Legt die Mindestbreite jedes Steps fest. Wird kein Wert angegeben, passen sich die Steps automatisch an die Breite ihres Inhalts oder Containers an. Die Breite betrifft sowohl den sichtbaren Bereich des Steps als auch dessen Klickfläche. ```javascript stepWidth: "80px", // Pixel-Werte ``` 💡 **Tipp: **Verwende feste Breiten bei vielen Steps mit kurzen Texten, um ein gleichmäßiges Layout zu erzielen. Bei variierenden Inhalten lieber leer lassen und auf automatische Skalierung setzen. ### gapY **Typ:** `text` **Beispiel:** `"10px"` **Default:** *(leer)* Definiert den vertikalen Abstand (`gap`) zwischen den Elementen innerhalb eines Steps – also zwischen `title`, `value` und `subtitle`. Dieser Wert wirkt innerhalb des einzelnen Steps, nicht zwischen den Steps. ```javascript gapY: "10px", // Pixel-Werte ``` 💡 **Tipp: **Mit einem kleineren Wert erzeugst du eine kompaktere Darstellung, größere Gaps wirken luftiger und lesbarer – besonders bei größeren Schriftgrößen. ### stepRadius **Typ:** `text` **Beispiel:** `"8px"` **Default:** *(leer)* Bestimmt die Abrundung des Step-Kreises (`value`) über den CSS-Wert `border-radius`. Ein hoher Wert wie `"50px"` erzeugt einen perfekt runden Kreis – niedrigere Werte führen zu weicheren Rechtecken oder Pillenformen. ```javascript stepRadius: "8px", // Pixel-Werte ``` 💡 **Tipp: **Für klassische runde Zähler-Designs setze `"50%"` oder `"999px"` – für eckigere Varianten kannst du mit Werten wie `"4px"` oder `"8px"` experimentieren. ### connectionLine Block In der Setting-Gruppe `connectionLine` bestimmst du Parameter zu der Linie zwischen deinen Steps. Mit dem Parameter show bestimmst du, ob die Linie überhaupt sichtbar sein soll (Mögliche Werte: true / false; default: true). Mit dem Parameter height, definierst du die Pixel-Höhe der Linie (default: 3px). ```javascript connectionLine: { show: true, // true, false height: "3px" // Pixel-Werte } ``` ## steps[]: Einzelne Schritte konfigurieren Jeder Eintrag in `steps` ist ein Objekt, das einen einzelnen Step beschreibt. Neben Titel, Wert und Aktionen kannst du auch viele Designparameter direkt je Step setzen – sie überschreiben dann die allgemeinen Einstellungen aus `settings`. ```javascript { title: "In Bearbeitung", value: "2", subtitle: "laufend", width: "90px", colorLabel: "#ffffff", fontSizeLabel: "11px", colorValue: "#00cc99", fontSizeValue: "28px", backgroundColor: "#222", borderColor: "transparent", actions: [...] } ```
Feld Typ Beschreibung
`title` `text` Titel oberhalb des Kreises
`value` `text`/`number` Zentrale Anzeige im Kreis
`subtitle` `text` Text unterhalb des Kreises
`width` `text` Individuelle Breite des Steps
`colorLabel` `text` Textfarbe für `title` und `subtitle`
`fontSizeLabel` `text` Schriftgröße für `title` und `subtitle`
`colorValue` `text` Textfarbe des zentralen Werts
`fontSizeValue` `text` Schriftgröße des Werts
`backgroundColor` `text` Hintergrundfarbe des Kreises und der Verbindungslinien
`borderColor` `text` Farbe der Step-Trennlinie
`actions[]` `array` Aktionen, die beim Klick ausgelöst werden können (siehe unten)
💡 **Tipp: **Du musst nicht alle Felder angeben. Jeder Step übernimmt automatisch die Werte aus `settings`, wenn keine eigenen Angaben gemacht werden. ## actions[]: Interaktive Schritte (Ninox) Jeder Step kann mit einer oder mehreren Aktionen versehen werden, die beim Klick auf den `value`-Kreis ausgelöst werden. Der Ninox-Adapter setzt daraus `clickable: true` für das Widget-Core. Alle definierten Aktionstypen (`update`, `delete`, `popup` usw.) werden dabei ausgeführt — nicht nur `update` und `delete`. Aktionen werden über ein Array von Objekten definiert: # Custom Steps ## AI Defaults (read first) - **uniqueId**: Required, `"steps-" + Nr`. - **steps**: Array of `{ title: "...", value: "..." }` objects. - **height**: `"100%"` when embedded. Fixed height (e.g. `"80px"`) when standalone. - **embedded**: Lives inside `settings: { embedded: true }`, NOT at root level. - **Klick (Ninox):** `actions: [...]` auf dem Step — der Adapter setzt intern `clickable: true`. `type: "update"` mit `recordId: Nr`, `field: fieldId(Nr, "Feldname")`. - **Klick (React):** `clickable: true` auf dem Step; `onClick(event, stepIndex)` am Host (`stepIndex` = 0-basierter Index in `steps`) — **kein** `actions` in `data`. - **stepWidth**: Default `"80px"`, set wider for longer labels. - **connectionLine**: `{ show: true, height: "3px" }` for visual step connections. # Visualisiere Fortschritte oder Status – mit Stil. Mit dem `arcCustomSteps`-Widget kannst du mehrstufige Prozesse, Status- oder Fortschrittsanzeigen direkt in deinem Ninox-Interface umsetzen – klar strukturiert, visuell ansprechend und vollständig anpassbar. Ob Projektphasen, Prozessstatus oder Ampelsysteme: Die Steps lassen sich flexibel in Breite, Farben, Abständen und Aktionen konfigurieren. Die einzelnen Schritte können dynamisch aus deinen Datensätzen aufgebaut und optional mit Aktionen (z. B. `update`, `delete`, `popup`) versehen werden. So entsteht eine interaktive Fortschrittsanzeige, die nicht nur informiert, sondern auch steuert. In dieser Dokumentation zeigen wir dir: - wie das Widget aufgebaut ist, - welche Parameter du anpassen kannst, - und wie du es Schritt für Schritt in dein System integrierst. 👉 **Scroll weiter für Beispielcode, Parameterübersicht und Anwendungstipps.** ![](https://framerusercontent.com/images/i14A175WOPvdKLDT7G7rK5XQlmE.png) ## Anwendungscode ```javascript let current := this; let list := (select 'Auftrag Steps'); let data := { uniqueId: "Auftragsphase", settings: { embedded: true, height: "150px", fontSizeLabel: "10px", fontSizeValue: "20px", colorValue: "", colorLabel: "#ffffff", borderColor: "transparent", backgroundColor: "transparent", alignX: "center", stepWidth: "80px", gapY: "10px", stepRadius: "8px", connectionLine: { show: true, height: "3px" } }, steps: list.[{ title: title, value: value, subtitle: "Test", colorValue: "", backgroundColor: "", actions: [{ recordId: current.Nr, type: "update", field: "", value: number(Nr) }] }] }; arcCustomSteps(data) ``` ## Parameter Das Widget erhält ein Objekt mit folgenden Hauptbestandteilen: - `uniqueId` – eine eindeutige Kennung für das Widget - `settings` – allgemeine Darstellungseinstellungen für alle Steps - `steps[]` – eine Liste von Einzelschritten mit optionalen Detailwerten Die Werte aus `settings` gelten für alle Steps, können aber pro Step gezielt überschrieben werden (z. B. Farbe oder Breite). ## Settings-Block ### embedded Typ: `boolean` Default: `false` Legt fest, ob das Widget in ein bestehendes Layout eingebettet wird. - Bei `true` wird auf äußere Rahmen, Hintergrundfarben und Standard-Abstände verzichtet. - Ideal, wenn das Widget in einem größeren Komponenten-Container liegt, der selbst das Layout steuert. 💡 **Tipp: **Wenn du `embedded: true` setzt, musst du sicherstellen, dass das umgebende Element eine passende Höhe und ggf. ein eigenes Layout mitbringt. Das Widget passt sich dann vollständig diesem Rahmen an. ### height **Typ:** `text` **Default:** `"100%"` Definiert die Höhe des gesamten Steps-Widgets. Akzeptiert klassische CSS-Werte wie `"150px"`, `"auto"` oder `"100%"`. ```javascript height: "150px", height: "", ``` 💡 **Tipp: **Wenn du das Widget **nicht eingebettet** nutzt (`embedded: false`), solltest du eine feste Höhe setzen – insbesondere bei schmalen Step-Breiten, damit Layout und Scrollverhalten konsistent bleiben. ![](https://framerusercontent.com/images/IprIm5AORs6cO1ATQc2gvMZtdc.png) ### fontsizeLabel **Typ:** `text` **Default:** `"13px"` Bestimmt die Schriftgröße für `title` und `subtitle` innerhalb eines Steps. Dieser Wert wirkt sowohl auf die obere (`title`) als auch untere (`subtitle`) Beschriftung jedes Steps – sofern kein eigener Wert direkt im Step gesetzt ist. ```javascript fontSizeLabel: "10px", // Pixel-Werte ``` 💡 **Tipp: **Für kompakte Layouts empfiehlt sich eine kleinere Schriftgröße wie `"10px"` oder `"11px"` – besonders bei vielen Steps mit schmaler Breite. ### fontsizeValue **Typ:** `text` **Default:** `"35px"` Gibt die Schriftgröße für den zentralen Wert (`value`) eines Steps an – also die Zahl oder Kennung im Kreis. Dieser Wert beeinflusst die Lesbarkeit und visuelle Gewichtung des zentralen Inhalts. Wird häufig für Statuszahlen, Zähler oder Phasen verwendet. ```javascript fontSizeValue: "20px", // Pixel-Werte ``` 💡 **Tipp: **Kleinere Werte wie `"20px"` eignen sich für sehr kompakte Steps, größere Größen wirken besonders bei wenigen, breit angelegten Schritten. ### colorValue **Typ:** `text` **Default:** `"#3E6FFF"` Legt die Textfarbe des zentralen `value`-Elements im Step-Kreis fest. Die Farbe bezieht sich ausschließlich auf den Text (z. B. Zahl, Status oder Kennung) – nicht auf den Hintergrund des Kreises. ```javascript colorValue: "#ddd", // HEX-Werte ``` 💡 **Tipp: **Die Lesbarkeit hängt stark vom Hintergrund ab. Achte auf ausreichend Kontrast zur `backgroundColor`. ### colorLabel **Typ:** `text` **Default:** `"#566eb1"` Definiert die Textfarbe für `title` und `subtitle` innerhalb eines Steps. Dieser Farbwert wird für die obere und untere Beschriftung verwendet – solange der Step selbst keine abweichende Farbe angibt. ```javascript colorLabel: "#ffffff", // HEX-Werte ``` 💡 **Tipp: **Verwende helle Schriftfarben wie `#ffffff` nur bei dunklem Hintergrund – ansonsten leidet die Lesbarkeit. ### borderColor **Typ:** `text` **Default:** `"#e5e5e5"` Definiert die Farbe der Trennlinien zwischen den Steps. Diese Farbe wird als rechte Rahmenlinie jedes Steps gesetzt (außer beim letzten Step). Sie hilft, die Schritte visuell voneinander abzugrenzen. 💡 **Tipp: **Wenn du eine besonders reduzierte oder fließende Darstellung möchtest, setze `borderColor: "transparent"` – so verschwinden die Trennlinien vollständig. ### backgroundColor **Typ:** `text` **Default:** `"#f7f8fc"` Bestimmt die Hintergrundfarbe der Step-Kreise **und** der Verbindungslinien. Diese Farbe wird auf die mittleren Kreise (`value`) sowie die Linien zwischen den Steps angewendet – nicht jedoch auf den äußeren Hintergrund des gesamten Widgets (dieser ist meist transparent oder durch Container geregelt). ```javascript backgroundColor: "transparent", // Transparenter Hintergrund backgroundColor: "#ededed", // HEX-Werte ``` 💡 **Tipp: **Ein heller Hintergrund sorgt für einen klaren, modernen Look. Für Spezialeffekte kannst du auch `"transparent"` oder leicht getönte Farben einsetzen. ### alignX **Typ:** `text` **Werte:** `"left"`, `"center"` (default), `"right"` Steuert die horizontale Ausrichtung der Steps innerhalb des Widgets. ```javascript alignX: "center", // left, center, right algnX: "", // default: center ``` Diese Einstellung beeinflusst, **wo die Steps innerhalb der Leiste ausgerichtet werden**, insbesondere wenn weniger Steps vorhanden sind als Platz zur Verfügung steht. - `"left"` → Steps starten ganz links - `"center"` → Steps zentriert - `"right"` → Steps rechtsbündig 💡 **Hinweis: **Diese Ausrichtung gilt sowohl für das gesamte Widget als auch für die Inhalte in jedem Step (z. B. Titel, Subtitle, Value). ### stepWidth **Typ:** `text` **Beispiel:** `"80px"` **Default:** *(leer)* Legt die Mindestbreite jedes Steps fest. Wird kein Wert angegeben, passen sich die Steps automatisch an die Breite ihres Inhalts oder Containers an. Die Breite betrifft sowohl den sichtbaren Bereich des Steps als auch dessen Klickfläche. ```javascript stepWidth: "80px", // Pixel-Werte ``` 💡 **Tipp: **Verwende feste Breiten bei vielen Steps mit kurzen Texten, um ein gleichmäßiges Layout zu erzielen. Bei variierenden Inhalten lieber leer lassen und auf automatische Skalierung setzen. ### gapY **Typ:** `text` **Beispiel:** `"10px"` **Default:** *(leer)* Definiert den vertikalen Abstand (`gap`) zwischen den Elementen innerhalb eines Steps – also zwischen `title`, `value` und `subtitle`. Dieser Wert wirkt innerhalb des einzelnen Steps, nicht zwischen den Steps. ```javascript gapY: "10px", // Pixel-Werte ``` 💡 **Tipp: **Mit einem kleineren Wert erzeugst du eine kompaktere Darstellung, größere Gaps wirken luftiger und lesbarer – besonders bei größeren Schriftgrößen. ### stepRadius **Typ:** `text` **Beispiel:** `"8px"` **Default:** *(leer)* Bestimmt die Abrundung des Step-Kreises (`value`) über den CSS-Wert `border-radius`. Ein hoher Wert wie `"50px"` erzeugt einen perfekt runden Kreis – niedrigere Werte führen zu weicheren Rechtecken oder Pillenformen. ```javascript stepRadius: "8px", // Pixel-Werte ``` 💡 **Tipp: **Für klassische runde Zähler-Designs setze `"50%"` oder `"999px"` – für eckigere Varianten kannst du mit Werten wie `"4px"` oder `"8px"` experimentieren. ### connectionLine Block In der Setting-Gruppe `connectionLine` bestimmst du Parameter zu der Linie zwischen deinen Steps. Mit dem Parameter show bestimmst du, ob die Linie überhaupt sichtbar sein soll (Mögliche Werte: true / false; default: true). Mit dem Parameter height, definierst du die Pixel-Höhe der Linie (default: 3px). ```javascript connectionLine: { show: true, // true, false height: "3px" // Pixel-Werte } ``` ## steps[]: Einzelne Schritte konfigurieren Jeder Eintrag in `steps` ist ein Objekt, das einen einzelnen Step beschreibt. Neben Titel, Wert und Aktionen kannst du auch viele Designparameter direkt je Step setzen – sie überschreiben dann die allgemeinen Einstellungen aus `settings`. ```javascript { title: "In Bearbeitung", value: "2", subtitle: "laufend", width: "90px", colorLabel: "#ffffff", fontSizeLabel: "11px", colorValue: "#00cc99", fontSizeValue: "28px", backgroundColor: "#222", borderColor: "transparent", actions: [...] } ```
Feld Typ Beschreibung
`title` `text` Titel oberhalb des Kreises
`value` `text`/`number` Zentrale Anzeige im Kreis
`subtitle` `text` Text unterhalb des Kreises
`width` `text` Individuelle Breite des Steps
`colorLabel` `text` Textfarbe für `title` und `subtitle`
`fontSizeLabel` `text` Schriftgröße für `title` und `subtitle`
`colorValue` `text` Textfarbe des zentralen Werts
`fontSizeValue` `text` Schriftgröße des Werts
`backgroundColor` `text` Hintergrundfarbe des Kreises und der Verbindungslinien
`borderColor` `text` Farbe der Step-Trennlinie
`actions[]` `array` Aktionen, die beim Klick ausgelöst werden können (siehe unten)
💡 **Tipp: **Du musst nicht alle Felder angeben. Jeder Step übernimmt automatisch die Werte aus `settings`, wenn keine eigenen Angaben gemacht werden. --- # Custom Table Source: https://www.arc-rider.com/docs/widgets/custom-table # Table in React Listen mit sortierbaren Spalten, Zeilenaktionen und eingebetteten Komponenten. Geeignet für Aufträge, Inventar und Auswahllisten. 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 { ArcWidgetBadge, ArcWidgetTable, type ArcWidgetTableRow, } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const statusTone = { open: { color: '#d97706', label: t('Offen', 'Open', 'Abierto') }, work: { color: '#0f766e', label: t('In Arbeit', 'In progress', 'En curso') }, done: { color: '#15803d', label: t('Erledigt', 'Done', 'Hecho') }, wait: { color: '#be123c', label: t('Wartet', 'Waiting', 'En espera') }, }; type Status = keyof typeof statusTone; type Order = { id: string; title: string; amount: number; status: Status; items?: Order[]; }; const demoOrders: Order[] = [ { id: 'a', title: t('Auftrag Nordhafen', 'North Harbor order', 'Pedido Puerto Norte'), amount: 240, status: 'work', items: [ { id: 'a-1', title: t('Vorbereitung', 'Preparation', 'Preparación'), amount: 80, status: 'done', }, { id: 'a-2', title: t('Umsetzung', 'Delivery', 'Entrega'), amount: 160, status: 'work', }, ], }, { id: 'b', title: t('Auftrag Westpark', 'Westpark order', 'Pedido Westpark'), amount: 80, status: 'open', }, { id: 'c', title: t('Auftrag Innenstadt', 'Downtown order', 'Pedido centro'), amount: 420, status: 'wait', }, { id: 'd', title: t('Auftrag Werkstatt', 'Workshop order', 'Pedido taller'), amount: 150, status: 'done', }, ]; const SITES = [ 'Nordhafen', 'Westpark', 'Innenstadt', 'Werkstatt', 'Messe', 'Logistik', 'Campus', 'Hafen Süd', ]; const STATUSES: Status[] = ['open', 'work', 'done', 'wait']; const busyOrders: Order[] = Array.from({ length: 800 }, (_, index) => ({ id: `busy-${index + 1}`, title: `${t('Auftrag', 'Order', 'Pedido')} ${SITES[index % SITES.length]} ${String(index + 1).padStart(3, '0')}`, amount: 50 + ((index * 47) % 9950), status: STATUSES[index % STATUSES.length], })); const orders = pack === 'empty' ? [] : pack === 'busy' ? busyOrders : demoOrders; export default function Example() { const [selected, setSelected] = useState( t('Keine Auswahl', 'Nothing selected', 'Sin selección'), ); const statusMark = (status: Status) => pack === 'busy' ? ( {statusTone[status].label} ) : ( ); const row = (order: Order): ArcWidgetTableRow => ({ id: order.id, clickable: true, columns: [ { value: order.title, width: 'fraction' }, { value: statusMark(order.status), width: '130px', }, { value: order.amount, width: '90px', sort: { value: order.amount }, }, ], items: (order.items ?? []).map(row), }); return ( <> {t('Keine Aufträge', 'No orders', 'Sin pedidos')}

{t( 'Leerer Zustand — über Daten auf Voll oder Demo wechseln.', 'Empty state — switch Data to Full or Demo.', 'Estado vacío — cambia Datos a Completo o Demo.', )}

), }, }} onCellClick={(_event, { row_index }) => { setSelected( row_index == null ? t('Keine Auswahl', 'Nothing selected', 'Sin selección') : `${t('Zeile', 'Row', 'Fila')} ${row_index + 1}`, ); }} />

{selected}

); } ``` ## Features ausprobieren ### Sortierung, Unterzeilen, JSX-Zellen und leerer Zustand Sortiere über die Spaltenüberschriften, klappe Unterzeilen auf und wechsle zur leeren Liste. Die Statuszellen sind echte Badge-Komponenten. ```tsx import { useState } from 'react'; import { ArcWidgetTable, ArcWidgetBadge } from '@arcrider/arcwidgets-react'; export default function Example() { const [mode, setMode] = useState('nested'); const [sort, setSort] = useState(true); const [selected, setSelected] = useState('Keine Auswahl'); const row = (id: string, title: string, amount: number) => ({ id, clickable: true, columns: [ { value: title, width: 'fraction' }, { value: ( 100 ? 'Priorität' : 'Standard', backgroundColor: '#f4f4f5', fontColor: '#18181b', }} /> ), width: '120px', }, { value: amount, width: '90px', sort: { value: amount } }, ], }); return ( <> {'Keine passenden Aufträge'}

{ 'Wechsle oben zurück zur flachen Liste oder zu den Unterzeilen.' }

), }, }} onCellClick={(_event, p) => setSelected( `Zeile ${p.row_index}, Spalte ${p.col_index}, flacher Index ${p.flat_index}`, ) } />

{selected}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetTableData`. Ü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). ### onCellClick Browser-Event und Zellenposition. Klickbarkeit muss an Zeile oder Zelle aktiviert sein. ```typescript onCellClick?: (event: Event, payload: ArcWidgetTableCellPayload) => void; ``` ### onCellHoverEnter Position der betretenen Zelle; benötigt `hover_enterable`. ```typescript onCellHoverEnter?: (payload: ArcWidgetTableCellPayload) => void; ``` ### onCellHoverLeave Position der verlassenen Zelle; benötigt `hover_leaveable`. ```typescript onCellHoverLeave?: (payload: ArcWidgetTableCellPayload) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### minHeight Untere Höhengrenze. Typ: `string`. ### minWidth Untere Breitengrenze. Typ: `string`. ### maxHeight Obere Höhengrenze. Typ: `string`. ### groupedBy Gruppierungsschlüssel; weglassen für flache Tabellen. Typ: `string`. ### groupsCollapsed Anfangszustand der Gruppen. Typ: `boolean`. ### collapsible Aufklappen bzw. Einklappen; das konkrete Format steht im Typ. Typ: `{ enabled?: boolean; expandColumn?: number; indentColumn?: number }`. ### theme Widgetspezifische Theme-Konfiguration. Für React kannst du Farben direkt in `styles` und den Bereichen setzen. Typ: `ArcWidgetTableTheme | ArcNestedWidgetDescriptor | string`. ### rowHoverAction Darstellung beim Überfahren einer Zeile. Typ: `ArcWidgetTableHoverAction`. ### header Spaltenüberschriften, Breiten und Sortieroptionen. Typ: `ArcWidgetTableHeader`. ### emptyTable `title` ist der einfache Fallback-Text. Wenn `value` gesetzt ist, ersetzt dieser Inhalt den Fallback vollständig. Für Überschrift und Beschreibung zusammen verwende ein JSX-Element in `value`, wie im Live-Beispiel. Darstellung bei leerer Zeilenliste. Typ: `ArcWidgetTableEmpty`. ### scrollBar Darstellung des Scrollbalkens. Typ: `ArcWidgetTableScrollBar`. ### table Datenzeilen mit `columns`; optionale Unterzeilen stehen in `items`. Typ: `ArcWidgetTableRow[]`. ### footer Inhalte und Sichtbarkeit des Tabellenfußes. Typ: `ArcWidgetTableFooter`. ### styles Gemeinsame Farben, Abstände und Rahmen; die angebotenen Felder sind widgetspezifisch. Typ: `ArcWidgetTableStyles | string`. ### borders Historische Rahmenkonfiguration. Bevorzuge `styles.borders`, sofern vom Widget angeboten. Typ: `ArcWidgetTableBorders`. ### borderRadius Abrundung der Ecken. Typ: `string`. ## Verhalten und Grenzen - Jede Spalte braucht eine Breite. Verwende mindestens einmal `width: "fraction"`; Header und Zellen müssen dieselbe Spaltenreihenfolge haben. - Setze `clickable: true` auf der Zeile oder Zelle, damit `onCellClick` ausgelöst wird. `row_index` und `col_index` sind nullbare Indizes; prüfe sie vor dem Zugriff. - Bei JSX-Zellinhalten braucht Sortierung einen skalaren `sort.value`. Verwende für Datumssortierung Millisekunden statt lokalisierter Datumsstrings. - Verschachtelte Zeilen werden über `items` beschrieben; `collapsible` steuert Einrückung und Aufklappen. Flache Beispiele können `row_index` direkt auf die Datenliste abbilden. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetTableSort { enabled?: boolean; type?: 'text' | 'number' | 'date' | string; value?: string | number; } export interface ArcWidgetTableHeaderColumn { title?: ArcWidgetSlot; width?: string; sort?: ArcWidgetTableSort; styles?: ArcWidgetStyles | string; [key: string]: unknown; } export interface ArcWidgetTableHeader { showHeader?: boolean; height?: string; fontColor?: string; backgroundColor?: string; fontSize?: string; styles?: ArcWidgetStyles | string; columns?: ArcWidgetTableHeaderColumn[]; sortIcons?: { color?: string; activeColor?: string; hoverColor?: string; }; [key: string]: unknown; } export interface ArcWidgetTableCellPayload { row_index?: number | null; col_index?: number | null; flat_index?: number | null; group_value?: string | null; } export interface ArcWidgetTableColumn { title?: ArcWidgetSlot; value?: ArcWidgetSlot; color?: string; backgroundColor?: string; width?: string; align?: 'left' | 'center' | 'right' | string; paddingX?: string; paddingY?: string; groupExpandAction?: boolean; groupValue?: ArcWidgetSlot; groupByValue?: ArcWidgetSlot; clickable?: boolean; hover_enterable?: boolean; hover_leaveable?: boolean; popup_button?: { content?: ArcWidgetSlot }; sort?: ArcWidgetTableSort; styles?: ArcWidgetStyles | string; [key: string]: unknown; } export interface ArcWidgetTableRow { id?: string | number; rowColor?: string; rowHeight?: string; rowPaddingY?: string; groupRowColor?: string; groupRowSettings?: { height?: string; [key: string]: unknown }; columns?: ArcWidgetTableColumn[]; items?: ArcWidgetTableRow[]; clickable?: boolean; styles?: ArcWidgetStyles | string; [key: string]: unknown; } export interface ArcWidgetTableEmpty { title?: ArcWidgetSlot; value?: ArcWidgetSlot; backgroundColor?: string; [key: string]: unknown; } export interface ArcWidgetTableTheme { [key: string]: unknown; } export interface ArcWidgetTableFooter { showFooter?: boolean; showActionButton?: boolean; backgroundColor?: string; actionButtonTitle?: ArcWidgetSlot; leftSideContent?: ArcWidgetSlot; rightSideContent?: ArcWidgetSlot; [key: string]: unknown; } export interface ArcWidgetTableScrollBar { showScrollBar?: boolean; height?: string; backgroundColor?: string; handle?: { height?: string; borderRadius?: string; backgroundColor?: string; [key: string]: unknown; }; [key: string]: unknown; } export type ArcWidgetTableBorderLine = boolean | { color?: string; width?: string; borderColor?: string; borderWidth?: string; borderSize?: string; enabled?: boolean; }; export interface ArcWidgetTableBorders { color?: string; width?: string; outer?: ArcWidgetTableBorderLine; columns?: ArcWidgetTableBorderLine; rows?: ArcWidgetTableBorderLine; } export interface ArcWidgetTableStyles extends ArcWidgetStyles { borders?: ArcWidgetTableBorders; } export interface ArcWidgetTableHoverAction { hoverActive?: boolean; backgroundColor?: string; fontColor?: string; [key: string]: unknown; } export interface ArcWidgetTableData extends ArcWidgetBaseData { height?: string; minHeight?: string; minWidth?: string; maxHeight?: string; groupedBy?: string; groupsCollapsed?: boolean; collapsible?: { enabled?: boolean; expandColumn?: number; indentColumn?: number }; theme?: ArcWidgetTableTheme | ArcNestedWidgetDescriptor | string; rowHoverAction?: ArcWidgetTableHoverAction; header?: ArcWidgetTableHeader; emptyTable?: ArcWidgetTableEmpty; scrollBar?: ArcWidgetTableScrollBar; table?: ArcWidgetTableRow[]; footer?: ArcWidgetTableFooter; styles?: ArcWidgetTableStyles | string; borders?: ArcWidgetTableBorders; borderRadius?: string; } export interface ArcWidgetTableProps extends ArcReactWidgetProps { onCellClick?: (event: Event, payload: ArcWidgetTableCellPayload) => void; onCellHoverEnter?: (payload: ArcWidgetTableCellPayload) => void; onCellHoverLeave?: (payload: ArcWidgetTableCellPayload) => void; } ``` ## Einsatzbeispiele - [Auftragsliste mit Suche und Status](/docs/react/examples/custom-table-mit-mini-widgets) - [Text, Zahlen und Datum sortieren](/docs/react/examples/sortierung-innerhalb-einer-table) - [Eigene Prioritätsreihenfolge](/docs/react/examples/customsort-einbinden) - [Tabellendesign und Dark Mode](/docs/react/examples/custom-table-look) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Custom Table ## AI Defaults (read first) - **Column widths**: Every column MUST have explicit `width`. At least one column MUST use `width: "fraction"`. - **Height**: Use `height: "100%"` (not `calc()`). Especially when `embedded: true`. - **uniqueListId**: Required, use `"my-table-" + Nr`. - **tableId**: Required (e.g. from `tableId("Tabellenname")`). - **recordId**: Always raw `Nr`, NEVER `number(Nr)`. - **Empty state**: Use `emptyTable: { title: "...", value: "..." }`, do NOT wrap table in if/else. - **theme.uniqueId**: Must match `uniqueListId`. - **Columns value**: Each column needs `recordId: Nr` inside the mapping. - **Row actions**: Use `actions` array with `type: "popup"` or `type: "update"`, recordId = `Nr`. - **Sort**: Client-side only; `header.columns[n].sort: { enabled: true, type: "text"|"number"|"date" }`; optional `columns[n].sort.value` for sort key when `value` is not plain text. Optional `header.sortIcons: { color, activeColor, hoverColor }` for caret colors (defaults: muted gray / black active / stronger on header hover). - **Hover**: `actions: [{ type: "hover", fontColor, backgroundColor, ... }]` on table root, row, or column; `type: "hover"` is not passed to click handlers. Legacy `rowHoverAction` still supported. - **Borders**: Live under `styles.borders`. Default is a rounded frame + row lines, **no vertical cell lines**. Hide the frame with `styles: { borders: { outer: false } }`. Opt into a grid with `columns: true`. Set `styles.borderColor` for dark backgrounds; a line may be `{ color, width }` instead of a boolean. - **styles hierarchy**: Same `styles` bag on table, header, row, and column — prefer `{ backgroundColor, fontColor, … }` over a CSS string. Table `styles.backgroundColor` is the default surface. A row or column overrides with its own `styles.backgroundColor`. Header uses `header.backgroundColor` / `header.styles`. Do not invent extra props like `rowBackgroundColor`. # Custom Table Das **Custom Table**-Widget ermöglicht dir die Darstellung dynamischer, interaktiver Tabellen direkt auf deiner Ninox-Oberfläche – maßgeschneidert nach deinen Anforderungen. Es eignet sich ideal, um strukturierte Daten übersichtlich darzustellen, individuell zu formatieren und mit Aktionen zu verknüpfen (z. B. für Bearbeiten, Löschen, Öffnen von Datensätzen oder API-Aufrufe). ## Beispiel: ![](https://framerusercontent.com/images/LW07tfdMxAVkiyhTZ9GOJlmHFws.png) ## Gesamter Anwendungscode Im Folgenden siehst du einen beispielhaften Anwendungscode, der die Basis deiner **Custom Table** definiert. Da der Code – je nach Anwendungsfall – sehr umfangreich werden kann, führen wir dich schrittweise **von einer einfachen Basis** zu **komplexeren Varianten**. Der Aufbau gliedert sich in zwei zentrale Bereiche: - `**data**` – definiert den Inhalt und die Struktur deiner Tabelle (Zeilen und Spalten) - `**arcCustomTable()**` – ist die globale Funktion, die das Widget rendert. Ihr übergibst du die `data` ```javascript let current := this; let projectList := Artikel; let data := { uniqueListId: "beispiel" + Nr, tableId: "", height: "500px", minHeight: "", groupedBy: "", groupsCollapsed: false, theme: arcCustomThemeCleanWhite({ uniqueId: "beispiel" }), embedded: false, rowHoverAction: { hoverActive: true, backgroundColor: "", fontColor: "" }, header: { showHeader: true, height: "", fontColor: "", backgroundColor: "", fontSize: "", columns: [{ title: "Prio", width: "" }] }, emptyTable: { title: "Keine Ergebnisse", backgroundColor: "", value: "" }, scrollBar: { showScrollBar: false, height: "", backgroundColor: "", handle: { height: "", borderRadius: "", backgroundColor: "" } }, table: projectList.[{ recordId: Nr, rowColor: "", rowHeight: "auto", rowPaddingY: "10px", groupRowColor: "", groupRowSettings: { height: "", }, columns: [{ field: "", title: "", value: "", color: "", backgroundColor: "", width: "", align: "left", paddingX: "", paddingY: "", groupExpandAction: false, groupValue: arcCustomIcon({ name: "caret-down" }), groupByValue: "", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }] }], footer: { showFooter: false, showActionButton: true, backgroundColor: "", actionButtonTitle: "", leftSideContent: "", rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte" } }; arcCustomTable(data) ``` ### uniqueListId Der Parameter `**uniqueListId**` ist die **individuelle Bezeichnung deiner Tabelle**. Er sorgt dafür, dass deine Tabelle intern eindeutig identifiziert wird – besonders dann, wenn **mehrere Tabellen gleichzeitig auf einer Seite** angezeigt werden. **Warum ist das wichtig?** - Style-Einstellungen (z. B. Farben, Schriftarten, Hover-Effekte) werden gezielt nur auf die Tabelle mit der angegebenen `uniqueListId` angewendet. - Bei mehreren Tabellen auf einer Ansicht kann es sonst zu **Konflikten bei CSS-Klassen oder Zuständen** kommen. **✅ Best Practice:** Verwende **sprechende, eindeutige Namen** – am besten in camelCase oder mit Unterstrichen. ```javascript uniqueListId: "Projektliste Offen", // Text ``` ### tableId Mit `**tableId**` gibst du die **interne ID der Tabelle in Ninox** an, auf die sich das Widget bezieht. Diese ID wird z. B. benötigt, um: - neue Datensätze über den **„+“-Button** im Footer anzulegen (`create`) - bestehende Datensätze zu öffnen oder bearbeiten zu können (`popup`, `openFullscreen`, `update`) - Aktionen korrekt auf Datensätze dieser Tabelle zu beziehen ```javascript tableId: "AB", // Die ID als Textform tableId: tableId("Kontakte"), // Ninox Funktion zum Herausfinden der Table ID deiner darzustellenden Tabelle ``` ### height Mit `**height**` bestimmst du die **Höhe des Tabellen-Widgets** in **Pixeln**. So kannst du die Darstellung flexibel an das Layout deiner Seite anpassen – egal, wie viele Daten enthalten sind. **💡 Werte** - Der Wert wird in Pixeln angegeben – z. B. `"300px"`, `"500px"` oder `"800px"`. - `**"auto"**`: Die Höhe passt sich automatisch an den Inhalt an. Perfekt für dynamische Inhalte – beachte aber mögliche Sprünge im Layout. - In Kombination mit `maxHeight` kannst du die automatische Höhe begrenzen. ```javascript height: "500px", // Pixel Werte ``` ### minWidth Mit `**minWidth**` legst du die **minimale Breite** deiner Tabelle fest – unabhängig davon, wie viele Spalten oder Daten enthalten sind. So stellst du sicher, dass die Tabelle nicht zu schmal dargestellt wird, z. B. bei schmalen Containern, Tabs oder eingebetteten Ansichten. 🔍 **Format** - Der Wert wird als **Text mit Einheit** übergeben, z. B. `"600px"` - Ohne Angabe kann die Tabelle bei zu wenig Inhalt schmal zusammenfallen – was das Layout stören kann. 💡 **Hinweis:** `minWidth` wirkt sich auf die äußere Tabellenhülle aus – nicht auf die Spaltenbreiten im Inneren. Für Spalten gibt es separate Optionen wie `width` und `minWidth` direkt im `columns`-Block. ### groupedBy Mit `**groupedBy**` definierst du, nach welchem **Feld (Spalten-Key bzw. **`**field**`**)** deine Tabelle gruppiert werden soll. Gruppen werden dabei visuell als **separate Zeilen mit eigener Formatierung** dargestellt – die darunterliegenden Einträge lassen sich ein- und ausklappen. 🔍 **Verhalten** - Du gibst hier den **Spaltennamen (**`**field**`**)** an, der in deinem `columns`-Block definiert ist. - Ist das Feld leer (`"`) oder fehlt ganz, **findet keine Gruppierung statt**. - Gruppierung kann auch auf komplexen Werten basieren (z. B. `Typ`, `Kategorie`, `Verantwortlicher` etc.). - Die Gruppen lassen sich dynamisch durch Klick auf die Gruppenzeile **auf- und zuklappen** – dieser Zustand wird **lokal gespeichert**. ```javascript groupedBy: "", // default: keine Gruppierung ausgewählt groupedby: "Status", // Text des Feldtitels, nach dem gruppiert werden soll groupedBy: Gruppierung, // Ninox Feld, das angesprochen wird. In diesem Fall: Ein Auswahlfeld, in dem die Optionen den Titel verschiedener Ninox-felder tragen, nach denen gruppiert werden kann. (z.B. Status, Mitarbeiter, Aufgabe) ``` ### groupsCollapsed Mit `**groupsCollapsed**` legst du fest, ob gruppierte Zeilen **standardmäßig eingeklappt** oder **ausgeklappt** dargestellt werden. Die Gruppenüberschrift (groupRow) ist dabei immer sichtbar – nur die zugehörigen Einträge (valueRows) werden bei Bedarf ein- oder ausgeblendet. **Dieser Parameter wirkt nur, wenn **`**groupedBy**`** gesetzt ist.** `**true**` = Gruppen sind beim Laden **eingeklappt** (kompakte Ansicht). `**false**` = Gruppen sind beim Laden **vollständig geöffnet**. **Der Zustand (ein-/ausgeklappt) wird lokal gespeichert**, d. h. bei erneutem Öffnen merkt sich das Widget den letzten Zustand pro Benutzer. **Standardwert: **`**false**`** (Gruppen sind offen)** – wenn nicht angegeben, zeigt die Tabelle alle Gruppeneinträge vollständig an. ```javascript groupscollapsed: "", // default: false groupsCollapsed: true, ``` ### embedded Mit `**embedded**` aktivierst du den **eingebetteten Modus** der Tabelle. In diesem Modus passt sich die Tabelle optimal an bestehende Layoutstrukturen an – z. B. in einem Container, Tab oder einem flexiblen UI-Bereich. **Standardwert: **`**false**` – die Tabelle nimmt dann ihren eigenen, festen Platz auf der Seite ein. **Wenn **`**embedded: true**`** gesetzt ist, wird das Widget nicht absolut, sondern relativ im Container positioniert** und übernimmt dessen Breite und ggf. auch andere Stilvorgaben. Kombiniert mit Parametern wie `height` und `minWidth` sorgt das für ein sauberes, integriertes Erscheinungsbild. **Besonders nützlich ist **`**embedded**`** in Dashboards, Tab-Ansichten oder Modalfenstern.** ```javascript embedded: false, embedded: true, embedded: "", // default: false ``` ### styles.borders Rahmen und Gitterlinien leben in **`styles`** — zusammen mit Hintergrund, Radius und Linienfarbe. Default: abgerundeter Außenrahmen, feine Zeilenlinien, **keine vertikalen Zelllinien**. Jedes Linienfeld ist `true` / `false` **oder** `{ color, width }`. | Prop | Default | Bedeutung | |---|---|---| | `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 | | `styles.borders.rows` | `true` | Horizontale Linien | | `styles.borders.color` / `width` | — | Gemeinsame Linie, überschreibt `borderColor` / Default-Stärke | ```javascript styles: { borderRadius: "12px", borderColor: "#e4e4e7", borders: { outer: true, columns: false, rows: true } } // In einem Layout / Card: styles: { borders: { outer: false } } // Dunkler Hintergrund — Fläche auf styles, Kopf auf header: styles: { backgroundColor: "#2a2822", fontColor: "#f5f0e6", borderColor: "#3f3c34" }, header: { backgroundColor: "#1c1b16", fontColor: "#f5f0e6" } // Klassisches Gitter: styles: { borders: { columns: true } } ``` Fertige Varianten (Default, Card, Gitter, flush, Dark): [Custom Table Look](/documentation/custom-table-look). ### rowHoverAction Mit dem `**rowHoverAction**`-Block bestimmst du, ob das **Hover-Verhalten auf Tabellenzeilen** aktiv sein soll – und falls ja, **welche Farben** beim Überfahren mit der Maus verwendet werden. - `**hoverActive: true**` aktiviert den Effekt, andernfalls bleibt die Zeile beim Hover unverändert. - `**backgroundColor**` definiert die **Hintergrundfarbe** der Zeile im Hover-Zustand, - `**fontColor**` legt die **Textfarbe** während des Hoverns fest. **Standardwert bei **`**hoverActive**`** ist **`**true**`, falls der Block gesetzt ist. Ohne `rowHoverAction` ist das Verhalten neutral – keine Hover-Effekte. **Tipp: Setze gedeckte Farben ein**, um den Effekt dezent und UX-freundlich zu halten. ```javascript rowHoverAction: { hoverActive: true, // wenn hovern an sein soll, und false, wenn nicht backgroundColor: "#9ca4a9", // default: #f4f6ff fontColor: "#fff" } // default: #000 ``` **Empfehlung:** Für neue Projekte nutze `actions: [{ type: "hover", ... }]` (siehe unten) – `rowHoverAction` bleibt aus Backwards-Compatibility erhalten. ### actions mit type: "hover" Hover-Styling und optionale Hover-Actions über dasselbe `actions`-Array wie `update` / `popup`. Eintrag mit `**type: "hover"**` wird **nicht** an Klick-Actions weitergegeben, sondern nur vom Widget ausgewertet. **Merge-Reihenfolge:** `rowHoverAction` (Legacy) → `actions` auf Tabellen-Root → `row.actions` → `columns[n].actions`. | Sub-Prop | Bedeutung | |---|---| | `enabled` | `false` = keine Hover-Hervorhebung für diese Zeile/Zelle. | | `backgroundColor` | Hintergrundfarbe bei Hover. | | `fontColor` | Textfarbe bei Hover. | | `styles` | Zusätzliches CSS (String) für die Zelle bei gehoverter Zeile. | | `actions` | Optional: bei **mouseenter** an den Action-Handler (z. B. `update`). | | `leaveActions` | Optional: bei **mouseleave**. | **Row-Level:** `actions: [{ type: 'hover', backgroundColor: '#eff6ff', fontColor: '#0f172a' }]` auf dem Zeilenobjekt. **Column-Level:** z. B. `{ type: 'hover', enabled: false }` um eine einzelne Spalte von der Hover-Hervorhebung auszunehmen. **`row.actions`** (ohne `hover`) – zusätzliche `update`/`popup`-Einträge gelten für die **ganze Zeile** (Klick auf eine Zelle führt Row- und Cell-Actions zusammen aus). ### header Mit dem ***header***-Block steuerst du die Darstellung deiner **Tabellenkopfzeile** (Spaltenüberschriften). **Standardverhalten:** Wenn `showHeader` nicht angegeben ist, wird die Header-Zeile **angezeigt**. Ohne weitere Einstellungen gelten Standardwerte für Größe, Farbe und Hintergrund. Tipp: Wenn du `showHeader: false` setzt, sollte die Bedeutung der Daten trotzdem klar erkennbar bleiben (Position, Farbschema der Spalten). #### showHeader Blendet die gesamte Header-Zeile ein oder aus. ```javascript header: { showHeader: false, // default: true } ``` #### height Höhe der Headerzeile (z. B. `"48px"` oder `"auto"`). ```javascript header: { height: "70px", // default: 36px } ``` #### fontColor Schriftfarbe der Spaltenüberschriften. ```javascript header: { fontColor: "#fff", // default: #000 } ``` #### fontSize Schriftgröße der Überschriften. ```javascript header: { fontSize: "17px", // default: 12px } ``` #### backgroundColor Hintergrund der Headerzeile. ```javascript header: { backgroundColor: "#3a4a54", // default: #f8f9fc } ``` #### styles CSS-String für freies Styling des Header-Containers (überschreibt `fontColor`, `backgroundColor` etc.). ```javascript header: { styles: "border-bottom: 1px solid #eee;", } ``` Vollständiges Beispiel: ```javascript header: { showHeader: false, // default: true height: "70px", // default: 36px fontColor: "#fff", // default: #52525b backgroundColor: "#3a4a54", // default: #fafafa fontSize: "17px", // default: 12px } ``` ### sort (Client-Side) Sortierung erfolgt **vollständig im Browser** (kein Ninox-Update, kein Helper-Feld). Der Zustand (aktive Spalte, Richtung) wird **intern** verwaltet und wie Scroll/Gruppen im **localStorage** persistiert. **Header-Spalte** – `sort` aktivieren und Typ setzen: - `**sort.enabled**`: `true` – Spaltenkopf ist klickbar, zeigt Sort-Icons. - `**sort.type**`: `'text'` (Standard), `'number'` oder `'date'`. **Zeilen-Zelle** – optionaler Rohwert für den Vergleich: - `**sort.value**`: Wert, nach dem sortiert wird. Wenn nicht gesetzt, wird `value` der Zelle verwendet (bei Widgets/HTML in `value` ungeeignet – dann immer `sort.value` setzen). **Klick-Zyklus:** erste Klick → aufsteigend, zweiter → absteigend, dritter → Sortierung aus. **Sort-Icons (Outline-Chevrons):** Farben optional über `header.sortIcons` (werden als CSS-Variablen am Header gesetzt): | Prop | Bedeutung | Standard (CSS-Fallback) | |------|-----------|-------------------------| | `color` | Farbe der **inaktiven** Carets | `rgba(0,0,0,0.35)` | | `activeColor` | Farbe des **aktiven** Carets (aktuelle Sortierrichtung) | `#000000` | | `hoverColor` | Farbe beider Carets bei **Hover** über den sortierbaren Header (inaktive Carets; aktiver Caret bleibt `activeColor`) | `rgba(0,0,0,0.55)` | Bei **Gruppierung** (`groupedBy`) wird nur **innerhalb jeder Gruppe** sortiert, nicht über Gruppen hinweg. ```javascript header: { showHeader: true, sortIcons: { color: 'rgba(0,0,0,0.4)', activeColor: '#000000', hoverColor: 'rgba(0,0,0,0.6)' }, columns: [{ title: 'Name', width: 'fraction', sort: { enabled: true, type: 'text' } }, { title: 'Betrag', width: '120px', sort: { enabled: true, type: 'number' } }] }, table: myList.[{ recordId: Nr, columns: [{ field: 'Name', value: Name, width: 'fraction' }, { field: 'Betrag', value: text(Betrag) + ' €', sort: { value: number(Betrag) }, width: '120px' }] }] ``` ### emptyTable Mit dem `**emptyTable**`-Block legst du fest, **was angezeigt werden soll, wenn keine Daten vorhanden sind** – also wenn `data: []` leer ist. Du kannst Text und Gestaltung des leeren Zustands anpassen und so die Nutzererfahrung deutlich verbessern. - `**title**`: Der Haupttext, der angezeigt wird (meist prominent, z. B. „Keine Einträge gefunden“). - `**value**`: Optionaler zusätzlicher Text oder HTML-Elemente im Inneren des leeren Bereichs. - `**backgroundColor**`: Hintergrundfarbe für den leeren Bereich – z. B. `"white"` oder `"#f4f6ff"`. **Standardverhalten:** Ohne `emptyTable` wird ein schlichter Text „Keine Ergebnisse“ angezeigt. 💡 **Pro-Tipp:** Leere Zustände sind keine Fehler – sie sind deine Bühne. Nutze sie für Erklärungen, motivierende Hinweise oder einen klaren Call-to-Action („Jetzt neuen Eintrag erstellen“). So bleiben deine Nutzer nicht im Leeren hängen – im wahrsten Sinne. ```javascript emptyTable: { title: "", // Text der bei einer leeren Tabelle in einem abgerundeten Badge dargestellt wird. backgroundColor: "", // Hintergrundfarbe in HEX angeben value: "" // Hier kannst du eigene Widgets oder komplexe Ansichten einfügen, die den Title überschreiben. }, ``` ![](https://framerusercontent.com/images/bAxu68gh9ycCRLInmZwR463qpeI.png) ### scrollBar Mit dem `**scrollBar**`-Block konfigurierst du die **horizontale Scrollbar** deiner Tabelle. Diese wird angezeigt, wenn deine Tabelle mehr Spalten enthält, als in den verfügbaren Bereich passen. - `**showScrollBar**`: `true` oder `false` – bestimmt, ob die Scrollbar überhaupt angezeigt wird. - `**height**`: Höhe des Scrollbar-Containers (z. B. `"10px"`). - `**backgroundColor**`: Hintergrundfarbe des Containers. - `**handle.height**`: Höhe des Schiebers („Handle“) innerhalb der Leiste. - `**handle.backgroundColor**`: Farbe des Scroll-Handles. - `**handle.borderRadius**`: Abgerundete Ecken des Schiebers (z. B. `"5px"` oder `"50%"`). **Standardverhalten:** Ohne `scrollBar` wird **keine sichtbare Scrollbar angezeigt** – auch wenn Inhalte horizontal überlaufen. 💡 **Pro-Tipp:** Gerade bei vielen Spalten hilft eine dezente, gut sichtbare Scrollbar, um Orientierung zu geben. Nutze weiche Farben und eine runde Handle-Form, damit die UI nicht „technisch“ wirkt – besonders auf Touch-Geräten zahlt sich das aus. ```javascript scrollBar: { showScrollbar: true, // default: false (Scrollbar ist standardmäßig ausgeblendet) height: 40px, // Höhe des Containers. default: 10px backgroundColor: "#3a4a54" // default: #f0f0f0 handle: { height: "20px", // Höhe des Schiebers. default: 90% borderRadius: 20px // default: 20px backgroundColor: "#fff", } // default: #ccc ``` ### theme Mit dem `**theme**`-Parameter kannst du ein **vordefiniertes Design-Template** für deine Tabelle aktivieren. Das Theme beeinflusst das **gesamte visuelle Erscheinungsbild** – von Farben über Schriftarten bis hin zum Layout. Aktuell verfügbar sind: - `**"clean-white"**` - `**"naked"**` **Wichtig:** Damit das Theme korrekt greift, muss die `theme`-uniqueId **die gleiche **`**uniqueListId**`** verwenden wie deine Tabelle**. Nur so überschreibt das Theme gezielt die richtigen Style-Regeln per CSS. **Standardverhalten:** Wird kein `theme` gesetzt, wird das Standarddesign des arcCustomTable-Widgets verwendet (leicht schattiert, kontrastreich). ```javascript theme: arcCustomThemeCleanWhite({ uniqueId: "Projektliste Demo 1" }), ``` ### styles Dieselbe `styles`-Tasche auf jeder Ebene. Bevorzugt ein Objekt mit CSS-ähnlichen Namen (`backgroundColor`, `fontColor`, `paddingX`). Ein CSS-String geht weiter. | Ebene | Prop | Wirkt auf | |---|---|---| | Tabelle | `styles: { backgroundColor, borders, … }` | Fläche + Rahmen. Default für alle Zeilen | | Header | `header: { backgroundColor }` oder `header.styles` | Kopfzeile | | Header-Zelle | `header.columns[n].styles` | Eine Kopfzelle | | Zeile | `table: [{ styles: { backgroundColor } }]` | Alle Zellen dieser Zeile | | Spalte / Zelle | `table: [{ columns: [{ styles: { backgroundColor } }] }]` | Eine Zelle | | Footer | `footer.styles` | Footer | Spalte überschreibt Zeile, Zeile überschreibt Tabelle. `styles` gewinnt gegen `rowColor` / flaches `backgroundColor`. ```javascript styles: { backgroundColor: "#ffffff", borderColor: "#e4e4e7", borders: { outer: true, columns: false, rows: true } }, header: { backgroundColor: "#fafafa" }, table: myList.[{ recordId: Nr, styles: { backgroundColor: "#f4f4f5" }, columns: [{ value: Name, width: "fraction", styles: { fontWeight: "600" } }] }] ``` ### footer Mit dem `**footer**`-Block kannst du einen **unteren Bereich** unterhalb der Tabelle anzeigen lassen – z. B. für Buttons oder Informationen. Er eignet sich ideal für **Aktionsflächen** wie „Neuen Eintrag anlegen“ oder zur Anzeige von Summen, Filtern oder Statusinfos. - `**showFooter**`: `true` oder `false` – aktiviert oder deaktiviert den Footer. - `**showActionButton**`: Zeigt den Standard-Button *„Neuen Datensatz erstellen“* an. - `**actionButtonTitle**`: Individueller Text für den Action-Button. - `**backgroundColor**`: Hintergrundfarbe des Footers. - `**leftSideContent**`: Freier Inhalt (z. B. Text, HTML, Icons) für die linke Seite. - `**rightSideContent**`: Inhalt für die rechte Seite – z. B. Statusanzeigen, Summen oder Infos. **Standardverhalten:** Ohne `footer`-Block wird kein Footer angezeigt. Wird `showFooter: true` gesetzt, aber keine weiteren Optionen, erscheint ein leerer Footerbereich. ```javascript footer: { showFooter: true, showActionButton: true, backgroundColor: "", actionButtonTitle: "Neue Aufgabe hinzufügen", leftSideContent: "", rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben" } ``` So sieht der oben definierte Footer aus: ![](https://framerusercontent.com/images/juhFlR4IcYzPbEQW10jXQF8x2Ic.png) ## table Im **Detail-Bereich **`**table**` definierst du alle **Spalten (columns)** und deren **Darstellung, Inhalte und Verhalten**. Du legst hier fest, **welche Datenfelder angezeigt werden sollen**, wie sie aussehen und was beim Klicken passieren soll. - Jede Spalte kann **individuell formatiert** werden (z. B. Farbe, Ausrichtung, Padding, Aktionen). - Die hier gesetzten Werte **überschreiben** ggf. globale Einstellungen aus dem `settings`-Block (z. B. `fontColor`, `backgroundColor`, `align` etc.). - Du kannst hier auch **fixierte Spalten**, **Gruppierungen**, **Interaktionen** und **Inline-Editing** steuern. ## Zeilen- und Gruppenparameter ```javascript projectList.[{ recordId: Nr, rowColor: "", rowHeight: "auto", rowPaddingY: "10px", groupRowColor: "", groupRowSettings: { height: "", }, ``` - `**recordId**`: Muss die `**Nr**` des Datensatzes sein (also die interne ID in Ninox). *Wichtig für die Zuordnung von Aktionen, z. B. Öffnen, Bearbeiten, Löschen.* - `**rowColor**`: Setzt die **Hintergrundfarbe** der jeweiligen Zeile. *Akzeptiert HEX-Werte wie *`*"#f4f6ff"*`* oder *`*"transparent"*`*.* - `**rowHeight**`: Bestimmt die **Höhe der Zeile**. *Wert kann *`*"auto"*`* sein oder eine feste Pixelangabe wie *`*"60px"*`*.* - `**rowPaddingY**` → Vertikales **Innenabstands-Polster** (top/bottom) innerhalb der Zellen dieser Zeile. Wird auf alle Zellen angewendet, **sofern dort kein individuelles Padding gesetzt ist**. - `**groupRowColor**`: Setzt die Hintergrundfarbe für eine **Gruppen-Zeile** (wenn `groupedBy` aktiv ist). *Du kannst entweder HEX-Werte verwenden oder dynamisch auf Felder zugreifen (z. B. *`*row.farbe*`*).* - `**groupRowSettings.height**` → Legt die **Höhe der Gruppenzeile** fest. Wert z. B. `"40px"` oder `"auto"`. Wirkt nur, wenn die Zeile eine **Gruppenzeile** ist. - `**styles**`: CSS-String für freies Styling – wird auf jede Zelle der Zeile angewendet. Langfristiger Ersatz für `rowColor` und `rowHeight`. 💡 **Pro-Tipp:** Du kannst **dynamisch berechnete Farben** einsetzen, um Status-Feedback direkt sichtbar zu machen – z. B. grün für „alle erledigt“, grau für „in Arbeit“: ```javascript groupRowColor: if cnt(Firma.Projekte) = cnt(Firma.Projekte[Abgeschlossen != null]) then "#F0FFF1" else "#eee" end, ``` ## Nested rows (`items[]`) Hierarchical tables use the **same row shape** as flat `table[]`, plus optional nested children on each row. ### Top-level input (either/or) | Form | Use case | |------|----------| | **`table: [...]`** | Classic — existing projects, flat + nested | | **`items: [...]`** | Greenfield — same row objects, Timeline-style root | Do **not** set both `table` and root `items` at once. If both are present, `table` wins (dev warning). Each row may contain **`items: [...]`** for children (unlimited depth). Collapse/expand is **client-side** (persisted in `localStorage` per `uniqueListId`). ### Example — `table[]` with nested children ```javascript arcCustomTable({ uniqueListId: "tasks-" + Nr, header: { columns: [ { title: " ", width: "40px" }, { title: "Aufgabe", width: "fraction" }, { title: "Status", width: "120px" } ] }, collapsible: { enabled: true, expandColumn: 0, indentColumn: 1, indentSize: "16px" }, table: [{ id: "p1", recordId: Nr, columns: [ { value: "" }, { value: Bezeichnung }, { value: Status } ], items: Unteraufgaben.[{ id: format(number(Nr), "0000"), recordId: Nr, columns: [ /* same column layout */ ], items: /* deeper levels */ }] }] }) ``` ### Example — root `items[]` Same row objects; replace `table:` with `items:` at the root. ### `collapsible` | Property | Meaning | |----------|---------| | `enabled` | `true`: parent rows with children show expand toggle | | `defaultCollapsed` | `true`: all parent rows start collapsed | | `expandColumn` | Column index for toggle (default `0`) | | `indentColumn` | Column index for depth padding (default `1`) | | `indentSize` | Padding per level, e.g. `"16px"` | | `iconSize` | Default caret size when no custom icons — e.g. `"16px"` (default) | | `iconExpand` / `iconCollapse` | Optional nested widget descriptors (e.g. `arcCustomIcon`) — replaces default caret | **Not combinable with `groupedBy`** in the same table config. Client-side sort is disabled when nested rows are present — order comes from Ninox (`table` / `items` structure). ### `span` / `colspan` on cells All rows share the **same number of header columns**. To show a parent row with one wide cell across multiple columns, use **`span`** (alias **`colspan`**) on the cell: ```javascript // Header: 4 columns (Expand | Name | Status | Date) columns: [ { value: "" }, { value: "Team Nord — 3 members", span: 3 } // 1 + 3 = 4 ] // Child row: four normal cells (span defaults to 1) columns: [ { value: "" }, { value: "Anna M." }, { value: badgeWidget }, { value: "14.06.2026" } ] ``` **Invalid:** parent row with 2 `columns` entries and child row with 4 entries **without** `span` — columns will misalign. Optional **`width`** / **`minWidth`** on a cell still override min-width on that cell; grid tracks come from `header.columns`. ## columns Im `columns`-Array definierst du die **einzelnen Spalten deiner Tabelle** – also wie sie aussehen, heißen und was darin angezeigt wird. Jede Spalte wird über ein eigenes Objekt innerhalb von `columns` beschrieben. **Basics, die du einstellen kannst:** - `**field**`: Das Datenfeld, das angezeigt werden soll – muss zu den Werten in `data.table.columns` passen. - `**title**`: Der sichtbare Name der Spalte im Header. - `**width**`: Die Breite der Spalte — fix z. B. `"200px"` / `"10%"`. Für flexible Spalte: `"fraction"`, `"auto"` oder `""` (leer) — gleiche Grid-Logik (`minmax(30px, 1fr)` bzw. gemischt mit fixen Spalten). - `**align**`: Ausrichtung des Inhalts – `"left"`, `"center"` oder `"right"`. - `**backgroundColor**`** / **`**color**`: Hintergrund- und Textfarbe der Spalte. - `**paddingX**`** / **`**paddingY**`: Innenabstände horizontal und vertikal. - `**truncate**`: Ob der Text abgeschnitten und mit „…“ versehen werden soll. - `**fixed**`: `"left"` oder `"right"`, um eine Spalte zu fixieren. - `**styles**`: CSS-String für freies Styling der Zelle (Header-Cell oder Body-Cell). Überschreibt alle anderen Style-Props. - `**actions**`: Aktionen wie `update`, `popup`, `change`, `openFullscreen`, `delete`. ```javascript columns: [{ field: "", title: "", value: "", width: "", align: "", fixed: "", paddingX: "", paddingY: "", groupExpandAction: "", groupValue: "", actions: [{ recordId: "", type: "", field: "", value: "" }] } ``` ### Gruppen ausklappen mit groupExpandAction Mit ***groupExpandAction*** kannst du bestimmen, bei welchen Spalten die Funktion des Ausklappens aktiviert sein soll. Das funktioniert nur, wenn du eine Gruppierung in deiner Tabelle eingestellt hast. Aktivierst du es bei mindestens einer Spalte (mit groupExpandAction:true), ist es bei den anderen Spalten automatisch deaktiviert, sofern du es dort nicht auch mit aktivierst in den Parametern. ```javascript groupExpandAction: true, // Aktiviert das Ausklappen der Tabellenzeilen durch Klick auf die Spalte. groupExpandAction: false, // Deaktiviert das Ausklappen der Tabellenzeilen durch Klick auf die Spalte. groupExpandAction: "", // default: true ``` ### actions Mit dem Parameter `**actions**` kannst du festlegen, **was passieren soll, wenn ein Nutzer auf eine Zelle klickt oder diese bearbeitet**. Aktionen werden auf **Zellebene innerhalb von **`**columns**` (in `data.table`) gesetzt und machen deine Tabelle interaktiv. **💡 Hinweis:** Du kannst **mehrere Aktionen kombinieren**, indem du sie als Array einfügst. Beispiel: ```javascript actions: [{ recordId: Nr, type: "popup" }, { recordId: Nr, type: "change", field: "A" }], ``` #### Aktion: popup Mit `**type: "popup"**` definierst du eine Aktion, die beim Klick auf die Zelle den verknüpften **Datensatz als Popup** öffnet – direkt innerhalb von Ninox. Dadurch können Nutzer Details einsehen oder bearbeiten, ohne die aktuelle Ansicht zu verlassen. - `**recordId**` muss die `Nr` des jeweiligen Datensatzes sein. - Diese Aktion gilt **immer für die ganze Zelle**, nicht nur für den Text. - Du kannst mit `showPopupButton: true` zusätzlich einen Button anzeigen lassen, der den Popup explizit auslöst – nützlich, wenn du Editieren und Öffnen visuell trennen möchtest. ```javascript actions: [{ recordId: Nr, type: "popup", // Text. Muss genau so geschrieben werden. showPopupButton: true, // true oder false zum Ein- oder Ausblenden des Open-Buttons popupButton: arcCustomButton({ uniqueId: "ButtonProjekt" + Nr, icon: "", title: "Open", fontSize: "13px", fontColor: "", iconColor: "", backgroundColor: "", borderColor: "" }) }] ``` Statt die gesamte Zelle klickbar zu machen, kannst du auch einen **individuellen Button innerhalb der Zelle anzeigen lassen** – z. B. mit dem Widget `arcCustomButton`. Dazu verwendest du: - `**showPopupButton: true**` – aktiviert die Schaltfläche. - `**popupButton**` – enthält das gerenderte Button-Element, z. B. per `arcCustomButton()`. #### Aktion: delete Mit `**type: "delete"**` kannst du beim Klick auf eine Zelle den zugehörigen Datensatz **löschen**. Die Aktion wirkt sich – wie bei `popup` – **auf die gesamte Zelle aus**. - `**recordId**` muss auf die `Nr` des Datensatzes zeigen, der gelöscht werden soll. - Es wird kein zusätzlicher Bestätigungsdialog eingebaut – das Löschen erfolgt direkt. 💡 **Hinweis:** Diese Aktion sollte **nur mit Bedacht** und **klar visualisiert** eingesetzt werden – z. B. durch eine spezielle Icon-Spalte oder einen rot markierten Button innerhalb der Zelle. ```javascript actions: [{ recordId: Nr, type: "delete" // Text. Muss genau so geschrieben werden. }] ``` #### Aktion: update Mit `**type: "update"**` kannst du beim Klick auf eine Zelle direkt ein **Feld des verknüpften Datensatzes ändern** – ohne Popup, ohne Edit-Modus. - `**recordId**`: Die `Nr` des Datensatzes, der verändert werden soll. - `**field**`: Das Feld, das aktualisiert wird (als Text). - `**value**`: Der neue Wert, der gesetzt werden soll (Text, Zahl, Bool, etc.). ```javascript actions: [{ recordId: Nr, type: "update", field: "G", // Gibt die genaue Field ID des Feldes an, auf das die Aktion angewendet wird. value: if erledigt=true then null else true end, // Gibt den Wert an, der bei dem referenzierten Feld eingesetzt werden soll. }] ``` 💡 **Pro-Tipp:** Wenn du das **Löschen nicht direkt**, sondern erst **nach einer Sicherheitsabfrage** durchführen möchtest, kannst du einen kleinen Umweg über ein **Hilfsfeld + Trigger** nehmen: - Erzeuge ein **Ja/Nein-Feld** namens z. B. `trigger_delete`. - Füge in der Tabelle eine `update`-Aktion ein, die `trigger_delete` auf `true` setzt. - Im Trigger dieses Feldes nutzt du folgenden Dialog: ```javascript if dialog("Eintrag Löschen", "Soll der Eintrag wirklich gelöscht werden?", ["Ja, löschen!", "Abbrechen"]) = "Ja, löschen!" then delete this end; trigger_delete := false ``` #### Aktion: openFullscreen Mit `**type: "openFullscreen"**` öffnest du den verknüpften Datensatz direkt im **Vollbildmodus** – also so, als würde man ihn klassisch in Ninox vollständig aufrufen. - `**recordId**`: Die `Nr` des Datensatzes, der geöffnet werden soll. **Einsatzszenario:** Wenn du komplexe Datensätze mit vielen Tabs oder Untertabellen hast, ist `openFullscreen` die bessere Wahl gegenüber `popup`. ```javascript actions: [{ recordId: Nr, type: "openFullscreen", // Text. Muss genau so geschrieben werden. }] ``` #### Aktion: openRecord Mit `**type: "**openRecord**"**` öffnest du das Formular des gegebenen Datensatzes zusammen mit der entsprechenden Tabelle. - `**recordId**`: Die `Nr` des Datensatzes, der geöffnet werden soll. ```javascript actions: [{ recordId: Nr, type: "openRecord", showPopupButton: true }] ``` ### groupValue Mit `**groupValue**` definierst du den **Inhalt der Gruppierungszeile** in einer bestimmten Spalte – also das, was in der Zeile angezeigt wird, die z. B. mehrere Datensätze zu einem Projekt, Kunden oder Status bündelt. - Du kannst `groupValue` **in jeder Spalte** verwenden – auch mehrfach pro Gruppe, wenn gewünscht. - Du kannst einfachen **Text**, HTML oder auch **Mini-Widgets** einbauen (z. B. Buttons, Layouts oder Statusanzeigen). - Die Gruppierungszeile wird automatisch angezeigt, wenn `groupedBy` im `settings`-Block gesetzt ist. ```javascript groupValue: Projekte.Bezeichnung // Ninox-Felder & -Schreibweisen möglich groupValue: "Projekt:" + Projekte.Bezeichnung // Text + Ninox-Feld groupValue: arcCustomProgressBar({ uniqueId: "", width: "", fontSize: "", fontColor: "", backgroundColor: "", progressColor: "", valueTotal: "", valueProgress: "", valueText: "" }) // Andere Mini-Widgets ``` ![](https://framerusercontent.com/images/yp7bARVTzbVbdb75ZV2DK0mkA.png) ### groupByValue Der `**groupByValue**`-Parameter erlaubt es, deine Tabelle nach **einem anderen Wert als dem angezeigten **`**value**` zu gruppieren. So kannst du die Anzeige der Zelle unabhängig von der Logik der Gruppierung steuern. - `**value**`: Was der Nutzer in der Zelle sieht (z. B. ein Icon, ein Label, ein HTML-Element). - `**groupByValue**`: Nach diesem Wert wird die Zeile gruppiert. **Fallback:** Wenn `**value**` oder `**groupByValue**` leer ist, wird die Zeile automatisch der Gruppe `**arc-no-value**` zugeordnet. Diese Gruppe wird ganz normal dargestellt, kann aber auch gezielt angesprochen oder ausgeblendet werden. ```javascript groupByValue: Projekte.Nr // groupByValue: Typ // Ninox-Felder groupByValue: "" // default: es wird nach value gruppiert ``` ## Beispiele Folgend findest du einige Praxisbeispiele, die dir den unterschiedlichen Einsatz der Custom Table veranschaulichen. ### Custom Table Simple ![](https://framerusercontent.com/images/pvAEOaUFtbYHMPzHd4v82XWP3nk.png) ```javascript let current := this; let projectList := (select Projekte); let data := { uniqueListId: "Projektliste Offen", tableId: tableId(first(projectList)), height: "auto", groupedBy: "", popupButtonTitle: "Open", table: projectList.[{ recordId: Nr, rowColor: "", groupRowColor: "#eee", columns: [{ field: "Feldname für Feld-ID", title: "Titel", value: "Ninox Wert", width: "10%" }, { field: "Feldname für Feld-ID", title: "Titel", value: "Ninox Wert", width: "15%" }, { field: "Feldname für Feld-ID", title: "Titel", value: "Ninox Wert", width: "10%" }, { field: "Firma.Name", title: "Firma", value: Firma.Name, width: "10%" }, { field: "Bezeichnung", title: "Projekt", value: Bezeichnung, width: "15%", actions: [{ type: "popup", recordId: Nr }, { type: "change", recordId: Nr, field: "A" }] }, { field: "Umsatz", title: "Umsatz", value: text(Umsatz + 1000), width: "20%" }, { field: "Aufgaben", title: "Status Aufgaben", value: "

", width: "100px" }, { field: "Abgeschlossen", title: "Abgeschlossen am", value: text(Abgeschlossen), width: "100px" }] }], footer: { showFooter: false, actionButtonTitle: "", rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte" } }; arcCustomTable(data) ``` ### Custom Table Complex ![](https://framerusercontent.com/images/OUceOtboxPQac3AJ3N3DEu9yas.png) ```javascript let current := this; arcCustomTable(data); let list := do as transaction select Aufgaben end; let filteredList := list[if current.Suche != null then testx(Mitarbeiter.'First Name' + "," + text(Mitarbeiter.'Last Name') + "," + text(Aufgabe) + "," + text(Projekte.Bezeichnung) + "," + ", "(?:" + current.Suche + ")\.*[^]", "gi") else true end and if current.'Erledigte einblenden' = true then true else Erledigt != true end]; let data := { uniqueListId: "Projektliste A", tableId: tableId(first(list)), height: "500px", theme: "", groupedBy: text('Gruppieren nach'), embedded: false, table: filteredList.[{ recordId: Nr, rowColor: "", rowHeight: "60px", groupRowColor: let currentRecord := this; if cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung and Erledigt = true]) = cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung]) then "#dcf2de" else "#eee" end, columns: [{ field: "Erledigt", title: arcCheckBox({ uniqueId: "checkbox all check", value: Erledigt, embedded: true, clickAction: { recordId: Nr, fieldId: "G", value: if cnt(list[Erledigt = true]) != cnt(list) then false else if cnt(list[Erledigt = null]) = cnt(list) then null else if cnt(list[Erledigt = true]) = cnt(list) then true end end end } }), value: arcCheckBox({ uniqueId: "checkbox single check", value: Erledigt, embedded: true, clickAction: { recordId: Nr, fieldId: "G", value: if Erledigt = true then null else true end } }), width: "100px", align: "center", fixed: "left", groupValue: arcCustomIcon({ name: "caret-down" }) }, { field: "Bild", title: "Logo", width: "150px", value:", actions: [{ recordId: Projekte.Nr, type: "popup" }], groupValue: "", }, { field: "Projekt", title: "Projekt", width: "", value: Projekte.Bezeichnung, actions: [{ recordId: Projekte.Nr, type: "popup" }], groupValue: Projekte.Bezeichnung, }, { field: "Aufgaben", title: "Aufgabe", value: Aufgabe, align: "left", width: "200px", actions: [{ recordId: Nr, type: "popup" }, { recordId: Nr, type: "change", field: "A" }], groupValue: if current.'Gruppieren nach' = 3 then html(--- **{ Aufgabe }** ---) end }, { field: "Mitarbeiter", title: "Mitarbeiter", value:", actions: [{ recordId: Mitarbeiter.Nr, type: "popup" }] }, { field: "delete", title: "", width: "40px", align: "center", value: "", actions: [{ recordId: Nr, type: "delete" }] }] }], footer: { showFooter: true, showActionButton: true, actionButtonTitle: "Neue Aufgabe hinzufügen", rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben" } }; arcCustomTable(data) ``` # Custom Table ## AI Defaults (read first) - **Column widths**: Every column MUST have explicit `width`. At least one column MUST use `width: "fraction"`. - **Height**: Use `height: "100%"` (not `calc()`). Especially when `embedded: true`. - **uniqueListId**: Required, use `"my-table-" + Nr`. - **tableId**: Required (e.g. from `tableId("Tabellenname")`). - **recordId**: Always raw `Nr`, NEVER `number(Nr)`. - **Empty state**: Use `emptyTable: { title: "...", value: "..." }`, do NOT wrap table in if/else. - **theme.uniqueId**: Must match `uniqueListId`. - **Columns value**: Each column needs `recordId: Nr` inside the mapping. - **Row actions**: Use `actions` array with `type: "popup"` or `type: "update"`, recordId = `Nr`. - **Sort**: Client-side only; `header.columns[n].sort: { enabled: true, type: "text"|"number"|"date" }`; optional `columns[n].sort.value` for sort key when `value` is not plain text. Optional `header.sortIcons: { color, activeColor, hoverColor }` for caret colors (defaults: muted gray / black active / stronger on header hover). - **Hover**: `actions: [{ type: "hover", fontColor, backgroundColor, ... }]` on table root, row, or column; `type: "hover"` is not passed to click handlers. Legacy `rowHoverAction` still supported. - **Borders**: Live under `styles.borders`. Default is a rounded frame + row lines, **no vertical cell lines**. Hide the frame with `styles: { borders: { outer: false } }`. Opt into a grid with `columns: true`. Set `styles.borderColor` for dark backgrounds; a line may be `{ color, width }` instead of a boolean. - **styles hierarchy**: Same `styles` bag on table, header, row, and column — prefer `{ backgroundColor, fontColor, … }` over a CSS string. Table `styles.backgroundColor` is the default surface. A row or column overrides with its own `styles.backgroundColor`. Header uses `header.backgroundColor` / `header.styles`. Do not invent extra props like `rowBackgroundColor`. # Custom Table Das **Custom Table**-Widget ermöglicht dir die Darstellung dynamischer, interaktiver Tabellen direkt auf deiner Ninox-Oberfläche – maßgeschneidert nach deinen Anforderungen. Es eignet sich ideal, um strukturierte Daten übersichtlich darzustellen, individuell zu formatieren und mit Aktionen zu verknüpfen (z. B. für Bearbeiten, Löschen, Öffnen von Datensätzen oder API-Aufrufe). ## Beispiel: ![](https://framerusercontent.com/images/LW07tfdMxAVkiyhTZ9GOJlmHFws.png) ## Gesamter Anwendungscode Im Folgenden siehst du einen beispielhaften Anwendungscode, der die Basis deiner **Custom Table** definiert. Da der Code – je nach Anwendungsfall – sehr umfangreich werden kann, führen wir dich schrittweise **von einer einfachen Basis** zu **komplexeren Varianten**. Der Aufbau gliedert sich in zwei zentrale Bereiche: - `**data**` – definiert den Inhalt und die Struktur deiner Tabelle (Zeilen und Spalten) - `**arcCustomTable()**` – ist die globale Funktion, die das Widget rendert. Ihr übergibst du die `data` ```javascript let current := this; let projectList := Artikel; let data := { uniqueListId: "beispiel" + Nr, tableId: "", height: "500px", minHeight: "", groupedBy: "", groupsCollapsed: false, theme: arcCustomThemeCleanWhite({ uniqueId: "beispiel" }), embedded: false, rowHoverAction: { hoverActive: true, backgroundColor: "", fontColor: "" }, header: { showHeader: true, height: "", fontColor: "", backgroundColor: "", fontSize: "", columns: [{ title: "Prio", width: "" }] }, emptyTable: { title: "Keine Ergebnisse", backgroundColor: "", value: "" }, scrollBar: { showScrollBar: false, height: "", backgroundColor: "", handle: { height: "", borderRadius: "", backgroundColor: "" } }, table: projectList.[{ recordId: Nr, rowColor: "", rowHeight: "auto", rowPaddingY: "10px", groupRowColor: "", groupRowSettings: { height: "", }, columns: [{ field: "", title: "", value: "", color: "", backgroundColor: "", width: "", align: "left", paddingX: "", paddingY: "", groupExpandAction: false, groupValue: arcCustomIcon({ name: "caret-down" }), groupByValue: "", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }] }], footer: { showFooter: false, showActionButton: true, backgroundColor: "", actionButtonTitle: "", leftSideContent: "", rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte" } }; arcCustomTable(data) ``` ### uniqueListId Der Parameter `**uniqueListId**` ist die **individuelle Bezeichnung deiner Tabelle**. Er sorgt dafür, dass deine Tabelle intern eindeutig identifiziert wird – besonders dann, wenn **mehrere Tabellen gleichzeitig auf einer Seite** angezeigt werden. **Warum ist das wichtig?** - Style-Einstellungen (z. B. Farben, Schriftarten, Hover-Effekte) werden gezielt nur auf die Tabelle mit der angegebenen `uniqueListId` angewendet. - Bei mehreren Tabellen auf einer Ansicht kann es sonst zu **Konflikten bei CSS-Klassen oder Zuständen** kommen. **✅ Best Practice:** Verwende **sprechende, eindeutige Namen** – am besten in camelCase oder mit Unterstrichen. ```javascript uniqueListId: "Projektliste Offen", // Text ``` ### tableId Mit `**tableId**` gibst du die **interne ID der Tabelle in Ninox** an, auf die sich das Widget bezieht. Diese ID wird z. B. benötigt, um: - neue Datensätze über den **„+“-Button** im Footer anzulegen (`create`) - bestehende Datensätze zu öffnen oder bearbeiten zu können (`popup`, `openFullscreen`, `update`) - Aktionen korrekt auf Datensätze dieser Tabelle zu beziehen ```javascript tableId: "AB", // Die ID als Textform tableId: tableId("Kontakte"), // Ninox Funktion zum Herausfinden der Table ID deiner darzustellenden Tabelle ``` ### height Mit `**height**` bestimmst du die **Höhe des Tabellen-Widgets** in **Pixeln**. So kannst du die Darstellung flexibel an das Layout deiner Seite anpassen – egal, wie viele Daten enthalten sind. **💡 Werte** - Der Wert wird in Pixeln angegeben – z. B. `"300px"`, `"500px"` oder `"800px"`. - `**"auto"**`: Die Höhe passt sich automatisch an den Inhalt an. Perfekt für dynamische Inhalte – beachte aber mögliche Sprünge im Layout. - In Kombination mit `maxHeight` kannst du die automatische Höhe begrenzen. ```javascript height: "500px", // Pixel Werte ``` ### minWidth Mit `**minWidth**` legst du die **minimale Breite** deiner Tabelle fest – unabhängig davon, wie viele Spalten oder Daten enthalten sind. So stellst du sicher, dass die Tabelle nicht zu schmal dargestellt wird, z. B. bei schmalen Containern, Tabs oder eingebetteten Ansichten. 🔍 **Format** - Der Wert wird als **Text mit Einheit** übergeben, z. B. `"600px"` - Ohne Angabe kann die Tabelle bei zu wenig Inhalt schmal zusammenfallen – was das Layout stören kann. 💡 **Hinweis:** `minWidth` wirkt sich auf die äußere Tabellenhülle aus – nicht auf die Spaltenbreiten im Inneren. Für Spalten gibt es separate Optionen wie `width` und `minWidth` direkt im `columns`-Block. ### groupedBy Mit `**groupedBy**` definierst du, nach welchem **Feld (Spalten-Key bzw. **`**field**`**)** deine Tabelle gruppiert werden soll. Gruppen werden dabei visuell als **separate Zeilen mit eigener Formatierung** dargestellt – die darunterliegenden Einträge lassen sich ein- und ausklappen. 🔍 **Verhalten** - Du gibst hier den **Spaltennamen (**`**field**`**)** an, der in deinem `columns`-Block definiert ist. - Ist das Feld leer (`"`) oder fehlt ganz, **findet keine Gruppierung statt**. - Gruppierung kann auch auf komplexen Werten basieren (z. B. `Typ`, `Kategorie`, `Verantwortlicher` etc.). - Die Gruppen lassen sich dynamisch durch Klick auf die Gruppenzeile **auf- und zuklappen** – dieser Zustand wird **lokal gespeichert**. ```javascript groupedBy: "", // default: keine Gruppierung ausgewählt groupedby: "Status", // Text des Feldtitels, nach dem gruppiert werden soll groupedBy: Gruppierung, // Ninox Feld, das angesprochen wird. In diesem Fall: Ein Auswahlfeld, in dem die Optionen den Titel verschiedener Ninox-felder tragen, nach denen gruppiert werden kann. (z.B. Status, Mitarbeiter, Aufgabe) ``` ### groupsCollapsed Mit `**groupsCollapsed**` legst du fest, ob gruppierte Zeilen **standardmäßig eingeklappt** oder **ausgeklappt** dargestellt werden. Die Gruppenüberschrift (groupRow) ist dabei immer sichtbar – nur die zugehörigen Einträge (valueRows) werden bei Bedarf ein- oder ausgeblendet. **Dieser Parameter wirkt nur, wenn **`**groupedBy**`** gesetzt ist.** `**true**` = Gruppen sind beim Laden **eingeklappt** (kompakte Ansicht). `**false**` = Gruppen sind beim Laden **vollständig geöffnet**. **Der Zustand (ein-/ausgeklappt) wird lokal gespeichert**, d. h. bei erneutem Öffnen merkt sich das Widget den letzten Zustand pro Benutzer. **Standardwert: **`**false**`** (Gruppen sind offen)** – wenn nicht angegeben, zeigt die Tabelle alle Gruppeneinträge vollständig an. ```javascript groupscollapsed: "", // default: false groupsCollapsed: true, ``` ### embedded Mit `**embedded**` aktivierst du den **eingebetteten Modus** der Tabelle. In diesem Modus passt sich die Tabelle optimal an bestehende Layoutstrukturen an – z. B. in einem Container, Tab oder einem flexiblen UI-Bereich. **Standardwert: **`**false**` – die Tabelle nimmt dann ihren eigenen, festen Platz auf der Seite ein. **Wenn **`**embedded: true**`** gesetzt ist, wird das Widget nicht absolut, sondern relativ im Container positioniert** und übernimmt dessen Breite und ggf. auch andere Stilvorgaben. Kombiniert mit Parametern wie `height` und `minWidth` sorgt das für ein sauberes, integriertes Erscheinungsbild. **Besonders nützlich ist **`**embedded**`** in Dashboards, Tab-Ansichten oder Modalfenstern.** ```javascript embedded: false, embedded: true, embedded: "", // default: false ``` ### styles.borders Rahmen und Gitterlinien leben in **`styles`** — zusammen mit Hintergrund, Radius und Linienfarbe. Default: abgerundeter Außenrahmen, feine Zeilenlinien, **keine vertikalen Zelllinien**. Jedes Linienfeld ist `true` / `false` **oder** `{ color, width }`. | Prop | Default | Bedeutung | |---|---|---| | `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 | | `styles.borders.rows` | `true` | Horizontale Linien | | `styles.borders.color` / `width` | — | Gemeinsame Linie, überschreibt `borderColor` / Default-Stärke | ```javascript styles: { borderRadius: "12px", borderColor: "#e4e4e7", borders: { outer: true, columns: false, rows: true } } // In einem Layout / Card: styles: { borders: { outer: false } } // Dunkler Hintergrund — Fläche auf styles, Kopf auf header: styles: { backgroundColor: "#2a2822", fontColor: "#f5f0e6", borderColor: "#3f3c34" }, header: { backgroundColor: "#1c1b16", fontColor: "#f5f0e6" } // Klassisches Gitter: styles: { borders: { columns: true } } ``` Fertige Varianten (Default, Card, Gitter, flush, Dark): [Custom Table Look](/documentation/custom-table-look). ### rowHoverAction Mit dem `**rowHoverAction**`-Block bestimmst du, ob das **Hover-Verhalten auf Tabellenzeilen** aktiv sein soll – und falls ja, **welche Farben** beim Überfahren mit der Maus verwendet werden. - `**hoverActive: true**` aktiviert den Effekt, andernfalls bleibt die Zeile beim Hover unverändert. - `**backgroundColor**` definiert die **Hintergrundfarbe** der Zeile im Hover-Zustand, - `**fontColor**` legt die **Textfarbe** während des Hoverns fest. **Standardwert bei **`**hoverActive**`** ist **`**true**`, falls der Block gesetzt ist. Ohne `rowHoverAction` ist das Verhalten neutral – keine Hover-Effekte. **Tipp: Setze gedeckte Farben ein**, um den Effekt dezent und UX-freundlich zu halten. ```javascript rowHoverAction: { hoverActive: true, // wenn hovern an sein soll, und false, wenn nicht backgroundColor: "#9ca4a9", // default: #f4f6ff fontColor: "#fff" } // default: #000 ``` **Empfehlung:** Für neue Projekte nutze `actions: [{ type: "hover", ... }]` (siehe unten) – `rowHoverAction` bleibt aus Backwards-Compatibility erhalten. ### actions mit type: "hover" Hover-Styling und optionale Hover-Actions über dasselbe `actions`-Array wie `update` / `popup`. Eintrag mit `**type: "hover"**` wird **nicht** an Klick-Actions weitergegeben, sondern nur vom Widget ausgewertet. **Merge-Reihenfolge:** `rowHoverAction` (Legacy) → `actions` auf Tabellen-Root → `row.actions` → `columns[n].actions`. | Sub-Prop | Bedeutung | |---|---| | `enabled` | `false` = keine Hover-Hervorhebung für diese Zeile/Zelle. | | `backgroundColor` | Hintergrundfarbe bei Hover. | | `fontColor` | Textfarbe bei Hover. | | `styles` | Zusätzliches CSS (String) für die Zelle bei gehoverter Zeile. | | `actions` | Optional: bei **mouseenter** an den Action-Handler (z. B. `update`). | | `leaveActions` | Optional: bei **mouseleave**. | **Row-Level:** `actions: [{ type: 'hover', backgroundColor: '#eff6ff', fontColor: '#0f172a' }]` auf dem Zeilenobjekt. **Column-Level:** z. B. `{ type: 'hover', enabled: false }` um eine einzelne Spalte von der Hover-Hervorhebung auszunehmen. **`row.actions`** (ohne `hover`) – zusätzliche `update`/`popup`-Einträge gelten für die **ganze Zeile** (Klick auf eine Zelle führt Row- und Cell-Actions zusammen aus). ### header Mit dem ***header***-Block steuerst du die Darstellung deiner **Tabellenkopfzeile** (Spaltenüberschriften). **Standardverhalten:** Wenn `showHeader` nicht angegeben ist, wird die Header-Zeile **angezeigt**. Ohne weitere Einstellungen gelten Standardwerte für Größe, Farbe und Hintergrund. Tipp: Wenn du `showHeader: false` setzt, sollte die Bedeutung der Daten trotzdem klar erkennbar bleiben (Position, Farbschema der Spalten). #### showHeader Blendet die gesamte Header-Zeile ein oder aus. ```javascript header: { showHeader: false, // default: true } ``` #### height Höhe der Headerzeile (z. B. `"48px"` oder `"auto"`). ```javascript header: { height: "70px", // default: 36px } ``` #### fontColor Schriftfarbe der Spaltenüberschriften. ```javascript header: { fontColor: "#fff", // default: #000 } ``` #### fontSize Schriftgröße der Überschriften. ```javascript header: { fontSize: "17px", // default: 12px } ``` #### backgroundColor Hintergrund der Headerzeile. ```javascript header: { backgroundColor: "#3a4a54", // default: #f8f9fc } ``` #### styles CSS-String für freies Styling des Header-Containers (überschreibt `fontColor`, `backgroundColor` etc.). ```javascript header: { styles: "border-bottom: 1px solid #eee;", } ``` Vollständiges Beispiel: ```javascript header: { showHeader: false, // default: true height: "70px", // default: 36px fontColor: "#fff", // default: #52525b backgroundColor: "#3a4a54", // default: #fafafa fontSize: "17px", // default: 12px } ``` ### sort (Client-Side) Sortierung erfolgt **vollständig im Browser** (kein Ninox-Update, kein Helper-Feld). Der Zustand (aktive Spalte, Richtung) wird **intern** verwaltet und wie Scroll/Gruppen im **localStorage** persistiert. **Header-Spalte** – `sort` aktivieren und Typ setzen: - `**sort.enabled**`: `true` – Spaltenkopf ist klickbar, zeigt Sort-Icons. - `**sort.type**`: `'text'` (Standard), `'number'` oder `'date'`. **Zeilen-Zelle** – optionaler Rohwert für den Vergleich: - `**sort.value**`: Wert, nach dem sortiert wird. Wenn nicht gesetzt, wird `value` der Zelle verwendet (bei Widgets/HTML in `value` ungeeignet – dann immer `sort.value` setzen). **Klick-Zyklus:** erste Klick → aufsteigend, zweiter → absteigend, dritter → Sortierung aus. **Sort-Icons (Outline-Chevrons):** Farben optional über `header.sortIcons` (werden als CSS-Variablen am Header gesetzt): | Prop | Bedeutung | Standard (CSS-Fallback) | |------|-----------|-------------------------| | `color` | Farbe der **inaktiven** Carets | `rgba(0,0,0,0.35)` | | `activeColor` | Farbe des **aktiven** Carets (aktuelle Sortierrichtung) | `#000000` | | `hoverColor` | Farbe beider Carets bei **Hover** über den sortierbaren Header (inaktive Carets; aktiver Caret bleibt `activeColor`) | `rgba(0,0,0,0.55)` | Bei **Gruppierung** (`groupedBy`) wird nur **innerhalb jeder Gruppe** sortiert, nicht über Gruppen hinweg. ```javascript header: { showHeader: true, sortIcons: { color: 'rgba(0,0,0,0.4)', activeColor: '#000000', hoverColor: 'rgba(0,0,0,0.6)' }, columns: [{ title: 'Name', width: 'fraction', sort: { enabled: true, type: 'text' } }, { title: 'Betrag', width: '120px', sort: { enabled: true, type: 'number' } }] }, table: myList.[{ recordId: Nr, columns: [{ field: 'Name', value: Name, width: 'fraction' }, { field: 'Betrag', value: text(Betrag) + ' €', sort: { value: number(Betrag) }, width: '120px' }] }] ``` ### emptyTable Mit dem `**emptyTable**`-Block legst du fest, **was angezeigt werden soll, wenn keine Daten vorhanden sind** – also wenn `data: []` leer ist. Du kannst Text und Gestaltung des leeren Zustands anpassen und so die Nutzererfahrung deutlich verbessern. - `**title**`: Der Haupttext, der angezeigt wird (meist prominent, z. B. „Keine Einträge gefunden“). - `**value**`: Optionaler zusätzlicher Text oder HTML-Elemente im Inneren des leeren Bereichs. - `**backgroundColor**`: Hintergrundfarbe für den leeren Bereich – z. B. `"white"` oder `"#f4f6ff"`. **Standardverhalten:** Ohne `emptyTable` wird ein schlichter Text „Keine Ergebnisse“ angezeigt. 💡 **Pro-Tipp:** Leere Zustände sind keine Fehler – sie sind deine Bühne. Nutze sie für Erklärungen, motivierende Hinweise oder einen klaren Call-to-Action („Jetzt neuen Eintrag erstellen“). So bleiben deine Nutzer nicht im Leeren hängen – im wahrsten Sinne. ```javascript emptyTable: { title: "", // Text der bei einer leeren Tabelle in einem abgerundeten Badge dargestellt wird. backgroundColor: "", // Hintergrundfarbe in HEX angeben value: "" // Hier kannst du eigene Widgets oder komplexe Ansichten einfügen, die den Title überschreiben. }, ``` ![](https://framerusercontent.com/images/bAxu68gh9ycCRLInmZwR463qpeI.png) ### scrollBar Mit dem `**scrollBar**`-Block konfigurierst du die **horizontale Scrollbar** deiner Tabelle. Diese wird angezeigt, wenn deine Tabelle mehr Spalten enthält, als in den verfügbaren Bereich passen. - `**showScrollBar**`: `true` oder `false` – bestimmt, ob die Scrollbar überhaupt angezeigt wird. - `**height**`: Höhe des Scrollbar-Containers (z. B. `"10px"`). - `**backgroundColor**`: Hintergrundfarbe des Containers. - `**handle.height**`: Höhe des Schiebers („Handle“) innerhalb der Leiste. - `**handle.backgroundColor**`: Farbe des Scroll-Handles. - `**handle.borderRadius**`: Abgerundete Ecken des Schiebers (z. B. `"5px"` oder `"50%"`). **Standardverhalten:** Ohne `scrollBar` wird **keine sichtbare Scrollbar angezeigt** – auch wenn Inhalte horizontal überlaufen. 💡 **Pro-Tipp:** Gerade bei vielen Spalten hilft eine dezente, gut sichtbare Scrollbar, um Orientierung zu geben. Nutze weiche Farben und eine runde Handle-Form, damit die UI nicht „technisch“ wirkt – besonders auf Touch-Geräten zahlt sich das aus. ```javascript scrollBar: { showScrollbar: true, // default: false (Scrollbar ist standardmäßig ausgeblendet) height: 40px, // Höhe des Containers. default: 10px backgroundColor: "#3a4a54" // default: #f0f0f0 handle: { height: "20px", // Höhe des Schiebers. default: 90% borderRadius: 20px // default: 20px backgroundColor: "#fff", } // default: #ccc ``` ### theme Mit dem `**theme**`-Parameter kannst du ein **vordefiniertes Design-Template** für deine Tabelle aktivieren. Das Theme beeinflusst das **gesamte visuelle Erscheinungsbild** – von Farben über Schriftarten bis hin zum Layout. Aktuell verfügbar sind: - `**"clean-white"**` - `**"naked"**` **Wichtig:** Damit das Theme korrekt greift, muss die `theme`-uniqueId **die gleiche **`**uniqueListId**`** verwenden wie deine Tabelle**. Nur so überschreibt das Theme gezielt die richtigen Style-Regeln per CSS. **Standardverhalten:** Wird kein `theme` gesetzt, wird das Standarddesign des arcCustomTable-Widgets verwendet (leicht schattiert, kontrastreich). ```javascript theme: arcCustomThemeCleanWhite({ uniqueId: "Projektliste Demo 1" }), ``` ### styles Dieselbe `styles`-Tasche auf jeder Ebene. Bevorzugt ein Objekt mit CSS-ähnlichen Namen (`backgroundColor`, `fontColor`, `paddingX`). Ein CSS-String geht weiter. | Ebene | Prop | Wirkt auf | |---|---|---| | Tabelle | `styles: { backgroundColor, borders, … }` | Fläche + Rahmen. Default für alle Zeilen | | Header | `header: { backgroundColor }` oder `header.styles` | Kopfzeile | | Header-Zelle | `header.columns[n].styles` | Eine Kopfzelle | | Zeile | `table: [{ styles: { backgroundColor } }]` | Alle Zellen dieser Zeile | | Spalte / Zelle | `table: [{ columns: [{ styles: { backgroundColor } }] }]` | Eine Zelle | | Footer | `footer.styles` | Footer | Spalte überschreibt Zeile, Zeile überschreibt Tabelle. `styles` gewinnt gegen `rowColor` / flaches `backgroundColor`. ```javascript styles: { backgroundColor: "#ffffff", borderColor: "#e4e4e7", borders: { outer: true, columns: false, rows: true } }, header: { backgroundColor: "#fafafa" }, table: myList.[{ recordId: Nr, styles: { backgroundColor: "#f4f4f5" }, columns: [{ value: Name, width: "fraction", styles: { fontWeight: "600" } }] }] ``` ### footer Mit dem `**footer**`-Block kannst du einen **unteren Bereich** unterhalb der Tabelle anzeigen lassen – z. B. für Buttons oder Informationen. Er eignet sich ideal für **Aktionsflächen** wie „Neuen Eintrag anlegen“ oder zur Anzeige von Summen, Filtern oder Statusinfos. - `**showFooter**`: `true` oder `false` – aktiviert oder deaktiviert den Footer. - `**showActionButton**`: Zeigt den Standard-Button *„Neuen Datensatz erstellen“* an. - `**actionButtonTitle**`: Individueller Text für den Action-Button. - `**backgroundColor**`: Hintergrundfarbe des Footers. - `**leftSideContent**`: Freier Inhalt (z. B. Text, HTML, Icons) für die linke Seite. - `**rightSideContent**`: Inhalt für die rechte Seite – z. B. Statusanzeigen, Summen oder Infos. **Standardverhalten:** Ohne `footer`-Block wird kein Footer angezeigt. Wird `showFooter: true` gesetzt, aber keine weiteren Optionen, erscheint ein leerer Footerbereich. ```javascript footer: { showFooter: true, showActionButton: true, backgroundColor: "", actionButtonTitle: "Neue Aufgabe hinzufügen", leftSideContent: "", rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben" } ``` So sieht der oben definierte Footer aus: ![](https://framerusercontent.com/images/juhFlR4IcYzPbEQW10jXQF8x2Ic.png) ## table Im **Detail-Bereich **`**table**` definierst du alle **Spalten (columns)** und deren **Darstellung, Inhalte und Verhalten**. Du legst hier fest, **welche Datenfelder angezeigt werden sollen**, wie sie aussehen und was beim Klicken passieren soll. - Jede Spalte kann **individuell formatiert** werden (z. B. Farbe, Ausrichtung, Padding, Aktionen). - Die hier gesetzten Werte **überschreiben** ggf. globale Einstellungen aus dem `settings`-Block (z. B. `fontColor`, `backgroundColor`, `align` etc.). - Du kannst hier auch **fixierte Spalten**, **Gruppierungen**, **Interaktionen** und **Inline-Editing** steuern. ## Zeilen- und Gruppenparameter ```javascript projectList.[{ recordId: Nr, rowColor: "", rowHeight: "auto", rowPaddingY: "10px", groupRowColor: "", groupRowSettings: { height: "", }, ``` - `**recordId**`: Muss die `**Nr**` des Datensatzes sein (also die interne ID in Ninox). *Wichtig für die Zuordnung von Aktionen, z. B. Öffnen, Bearbeiten, Löschen.* - `**rowColor**`: Setzt die **Hintergrundfarbe** der jeweiligen Zeile. *Akzeptiert HEX-Werte wie *`*"#f4f6ff"*`* oder *`*"transparent"*`*.* - `**rowHeight**`: Bestimmt die **Höhe der Zeile**. *Wert kann *`*"auto"*`* sein oder eine feste Pixelangabe wie *`*"60px"*`*.* - `**rowPaddingY**` → Vertikales **Innenabstands-Polster** (top/bottom) innerhalb der Zellen dieser Zeile. Wird auf alle Zellen angewendet, **sofern dort kein individuelles Padding gesetzt ist**. - `**groupRowColor**`: Setzt die Hintergrundfarbe für eine **Gruppen-Zeile** (wenn `groupedBy` aktiv ist). *Du kannst entweder HEX-Werte verwenden oder dynamisch auf Felder zugreifen (z. B. *`*row.farbe*`*).* - `**groupRowSettings.height**` → Legt die **Höhe der Gruppenzeile** fest. Wert z. B. `"40px"` oder `"auto"`. Wirkt nur, wenn die Zeile eine **Gruppenzeile** ist. - `**styles**`: CSS-String für freies Styling – wird auf jede Zelle der Zeile angewendet. Langfristiger Ersatz für `rowColor` und `rowHeight`. 💡 **Pro-Tipp:** Du kannst **dynamisch berechnete Farben** einsetzen, um Status-Feedback direkt sichtbar zu machen – z. B. grün für „alle erledigt“, grau für „in Arbeit“: ```javascript groupRowColor: if cnt(Firma.Projekte) = cnt(Firma.Projekte[Abgeschlossen != null]) then "#F0FFF1" else "#eee" end, ``` ## Nested rows (`items[]`) Hierarchical tables use the **same row shape** as flat `table[]`, plus optional nested children on each row. ### Top-level input (either/or) | Form | Use case | |------|----------| | **`table: [...]`** | Classic — existing projects, flat + nested | | **`items: [...]`** | Greenfield — same row objects, Timeline-style root | Do **not** set both `table` and root `items` at once. If both are present, `table` wins (dev warning). Each row may contain **`items: [...]`** for children (unlimited depth). Collapse/expand is **client-side** (persisted in `localStorage` per `uniqueListId`). ### Example — `table[]` with nested children ```javascript arcCustomTable({ uniqueListId: "tasks-" + Nr, header: { columns: [ { title: " ", width: "40px" }, { title: "Aufgabe", width: "fraction" }, { title: "Status", width: "120px" } ] }, collapsible: { enabled: true, expandColumn: 0, indentColumn: 1, indentSize: "16px" }, table: [{ id: "p1", recordId: Nr, columns: [ { value: "" }, { value: Bezeichnung }, { value: Status } ], items: Unteraufgaben.[{ id: format(number(Nr), "0000"), recordId: Nr, columns: [ /* same column layout */ ], items: /* deeper levels */ }] }] }) ``` ### Example — root `items[]` Same row objects; replace `table:` with `items:` at the root. ### `collapsible` | Property | Meaning | |----------|---------| | `enabled` | `true`: parent rows with children show expand toggle | | `defaultCollapsed` | `true`: all parent rows start collapsed | | `expandColumn` | Column index for toggle (default `0`) | | `indentColumn` | Column index for depth padding (default `1`) | | `indentSize` | Padding per level, e.g. `"16px"` | | `iconSize` | Default caret size when no custom icons — e.g. `"16px"` (default) | | `iconExpand` / `iconCollapse` | Optional nested widget descriptors (e.g. `arcCustomIcon`) — replaces default caret | **Not combinable with `groupedBy`** in the same table config. Client-side sort is disabled when nested rows are present — order comes from Ninox (`table` / `items` structure). ### `span` / `colspan` on cells All rows share the **same number of header columns**. To show a parent row with one wide cell across multiple columns, use **`span`** (alias **`colspan`**) on the cell: ```javascript // Header: 4 columns (Expand | Name | Status | Date) columns: [ { value: "" }, { value: "Team Nord — 3 members", span: 3 } // 1 + 3 = 4 ] // Child row: four normal cells (span defaults to 1) columns: [ { value: "" }, { value: "Anna M." }, { value: badgeWidget }, { value: "14.06.2026" } ] ``` **Invalid:** parent row with 2 `columns` entries and child row with 4 entries **without** `span` — columns will misalign. Optional **`width`** / **`minWidth`** on a cell still override min-width on that cell; grid tracks come from `header.columns`. ## columns Im `columns`-Array definierst du die **einzelnen Spalten deiner Tabelle** – also wie sie aussehen, heißen und was darin angezeigt wird. Jede Spalte wird über ein eigenes Objekt innerhalb von `columns` beschrieben. **Basics, die du einstellen kannst:** - `**field**`: Das Datenfeld, das angezeigt werden soll – muss zu den Werten in `data.table.columns` passen. - `**title**`: Der sichtbare Name der Spalte im Header. - `**width**`: Die Breite der Spalte — fix z. B. `"200px"` / `"10%"`. Für flexible Spalte: `"fraction"`, `"auto"` oder `""` (leer) — gleiche Grid-Logik (`minmax(30px, 1fr)` bzw. gemischt mit fixen Spalten). - `**align**`: Ausrichtung des Inhalts – `"left"`, `"center"` oder `"right"`. - `**backgroundColor**`** / **`**color**`: Hintergrund- und Textfarbe der Spalte. - `**paddingX**`** / **`**paddingY**`: Innenabstände horizontal und vertikal. - `**truncate**`: Ob der Text abgeschnitten und mit „…“ versehen werden soll. - `**fixed**`: `"left"` oder `"right"`, um eine Spalte zu fixieren. - `**styles**`: CSS-String für freies Styling der Zelle (Header-Cell oder Body-Cell). Überschreibt alle anderen Style-Props. - `**actions**`: Aktionen wie `update`, `popup`, `change`, `openFullscreen`, `delete`. ```javascript columns: [{ field: "", title: "", value: "", width: "", align: "", fixed: "", paddingX: "", paddingY: "", groupExpandAction: "", groupValue: "", actions: [{ recordId: "", type: "", field: "", value: "" }] } ``` ### Gruppen ausklappen mit groupExpandAction Mit ***groupExpandAction*** kannst du bestimmen, bei welchen Spalten die Funktion des Ausklappens aktiviert sein soll. Das funktioniert nur, wenn du eine Gruppierung in deiner Tabelle eingestellt hast. Aktivierst du es bei mindestens einer Spalte (mit groupExpandAction:true), ist es bei den anderen Spalten automatisch deaktiviert, sofern du es dort nicht auch mit aktivierst in den Parametern. ```javascript groupExpandAction: true, // Aktiviert das Ausklappen der Tabellenzeilen durch Klick auf die Spalte. groupExpandAction: false, // Deaktiviert das Ausklappen der Tabellenzeilen durch Klick auf die Spalte. groupExpandAction: "", // default: true ``` ### actions Mit dem Parameter `**actions**` kannst du festlegen, **was passieren soll, wenn ein Nutzer auf eine Zelle klickt oder diese bearbeitet**. Aktionen werden auf **Zellebene innerhalb von **`**columns**` (in `data.table`) gesetzt und machen deine Tabelle interaktiv. **💡 Hinweis:** Du kannst **mehrere Aktionen kombinieren**, indem du sie als Array einfügst. Beispiel: ```javascript actions: [{ recordId: Nr, type: "popup" }, { recordId: Nr, type: "change", field: "A" }], ``` #### Aktion: popup Mit `**type: "popup"**` definierst du eine Aktion, die beim Klick auf die Zelle den verknüpften **Datensatz als Popup** öffnet – direkt innerhalb von Ninox. Dadurch können Nutzer Details einsehen oder bearbeiten, ohne die aktuelle Ansicht zu verlassen. - `**recordId**` muss die `Nr` des jeweiligen Datensatzes sein. - Diese Aktion gilt **immer für die ganze Zelle**, nicht nur für den Text. - Du kannst mit `showPopupButton: true` zusätzlich einen Button anzeigen lassen, der den Popup explizit auslöst – nützlich, wenn du Editieren und Öffnen visuell trennen möchtest. ```javascript actions: [{ recordId: Nr, type: "popup", // Text. Muss genau so geschrieben werden. showPopupButton: true, // true oder false zum Ein- oder Ausblenden des Open-Buttons popupButton: arcCustomButton({ uniqueId: "ButtonProjekt" + Nr, icon: "", title: "Open", fontSize: "13px", fontColor: "", iconColor: "", backgroundColor: "", borderColor: "" }) }] ``` Statt die gesamte Zelle klickbar zu machen, kannst du auch einen **individuellen Button innerhalb der Zelle anzeigen lassen** – z. B. mit dem Widget `arcCustomButton`. Dazu verwendest du: - `**showPopupButton: true**` – aktiviert die Schaltfläche. - `**popupButton**` – enthält das gerenderte Button-Element, z. B. per `arcCustomButton()`. #### Aktion: delete Mit `**type: "delete"**` kannst du beim Klick auf eine Zelle den zugehörigen Datensatz **löschen**. Die Aktion wirkt sich – wie bei `popup` – **auf die gesamte Zelle aus**. - `**recordId**` muss auf die `Nr` des Datensatzes zeigen, der gelöscht werden soll. - Es wird kein zusätzlicher Bestätigungsdialog eingebaut – das Löschen erfolgt direkt. 💡 **Hinweis:** Diese Aktion sollte **nur mit Bedacht** und **klar visualisiert** eingesetzt werden – z. B. durch eine spezielle Icon-Spalte oder einen rot markierten Button innerhalb der Zelle. ```javascript actions: [{ recordId: Nr, type: "delete" // Text. Muss genau so geschrieben werden. }] ``` #### Aktion: update Mit `**type: "update"**` kannst du beim Klick auf eine Zelle direkt ein **Feld des verknüpften Datensatzes ändern** – ohne Popup, ohne Edit-Modus. - `**recordId**`: Die `Nr` des Datensatzes, der verändert werden soll. - `**field**`: Das Feld, das aktualisiert wird (als Text). - `**value**`: Der neue Wert, der gesetzt werden soll (Text, Zahl, Bool, etc.). ```javascript actions: [{ recordId: Nr, type: "update", field: "G", // Gibt die genaue Field ID des Feldes an, auf das die Aktion angewendet wird. value: if erledigt=true then null else true end, // Gibt den Wert an, der bei dem referenzierten Feld eingesetzt werden soll. }] ``` 💡 **Pro-Tipp:** Wenn du das **Löschen nicht direkt**, sondern erst **nach einer Sicherheitsabfrage** durchführen möchtest, kannst du einen kleinen Umweg über ein **Hilfsfeld + Trigger** nehmen: - Erzeuge ein **Ja/Nein-Feld** namens z. B. `trigger_delete`. - Füge in der Tabelle eine `update`-Aktion ein, die `trigger_delete` auf `true` setzt. - Im Trigger dieses Feldes nutzt du folgenden Dialog: ```javascript if dialog("Eintrag Löschen", "Soll der Eintrag wirklich gelöscht werden?", ["Ja, löschen!", "Abbrechen"]) = "Ja, löschen!" then delete this end; trigger_delete := false ``` #### Aktion: openFullscreen Mit `**type: "openFullscreen"**` öffnest du den verknüpften Datensatz direkt im **Vollbildmodus** – also so, als würde man ihn klassisch in Ninox vollständig aufrufen. - `**recordId**`: Die `Nr` des Datensatzes, der geöffnet werden soll. **Einsatzszenario:** Wenn du komplexe Datensätze mit vielen Tabs oder Untertabellen hast, ist `openFullscreen` die bessere Wahl gegenüber `popup`. ```javascript actions: [{ recordId: Nr, type: "openFullscreen", // Text. Muss genau so geschrieben werden. }] ``` #### Aktion: openRecord Mit `**type: "**openRecord**"**` öffnest du das Formular des gegebenen Datensatzes zusammen mit der entsprechenden Tabelle. - `**recordId**`: Die `Nr` des Datensatzes, der geöffnet werden soll. ```javascript actions: [{ recordId: Nr, type: "openRecord", showPopupButton: true }] ``` ### groupValue Mit `**groupValue**` definierst du den **Inhalt der Gruppierungszeile** in einer bestimmten Spalte – also das, was in der Zeile angezeigt wird, die z. B. mehrere Datensätze zu einem Projekt, Kunden oder Status bündelt. - Du kannst `groupValue` **in jeder Spalte** verwenden – auch mehrfach pro Gruppe, wenn gewünscht. - Du kannst einfachen **Text**, HTML oder auch **Mini-Widgets** einbauen (z. B. Buttons, Layouts oder Statusanzeigen). - Die Gruppierungszeile wird automatisch angezeigt, wenn `groupedBy` im `settings`-Block gesetzt ist. ```javascript groupValue: Projekte.Bezeichnung // Ninox-Felder & -Schreibweisen möglich groupValue: "Projekt:" + Projekte.Bezeichnung // Text + Ninox-Feld groupValue: arcCustomProgressBar({ uniqueId: "", width: "", fontSize: "", fontColor: "", backgroundColor: "", progressColor: "", valueTotal: "", valueProgress: "", valueText: "" }) // Andere Mini-Widgets ``` ![](https://framerusercontent.com/images/yp7bARVTzbVbdb75ZV2DK0mkA.png) ### groupByValue Der `**groupByValue**`-Parameter erlaubt es, deine Tabelle nach **einem anderen Wert als dem angezeigten **`**value**` zu gruppieren. So kannst du die Anzeige der Zelle unabhängig von der Logik der Gruppierung steuern. - `**value**`: Was der Nutzer in der Zelle sieht (z. B. ein Icon, ein Label, ein HTML-Element). - `**groupByValue**`: Nach diesem Wert wird die Zeile gruppiert. **Fallback:** Wenn `**value**` oder `**groupByValue**` leer ist, wird die Zeile automatisch der Gruppe `**arc-no-value**` zugeordnet. Diese Gruppe wird ganz normal dargestellt, kann aber auch gezielt angesprochen oder ausgeblendet werden. ```javascript groupByValue: Projekte.Nr // groupByValue: Typ // Ninox-Felder groupByValue: "" // default: es wird nach value gruppiert ``` ## Beispiele Folgend findest du einige Praxisbeispiele, die dir den unterschiedlichen Einsatz der Custom Table veranschaulichen. ### Custom Table Simple ![](https://framerusercontent.com/images/pvAEOaUFtbYHMPzHd4v82XWP3nk.png) ```javascript let current := this; let projectList := (select Projekte); let data := { uniqueListId: "Projektliste Offen", tableId: tableId(first(projectList)), height: "auto", groupedBy: "", popupButtonTitle: "Open", table: projectList.[{ recordId: Nr, rowColor: "", groupRowColor: "#eee", columns: [{ field: "Feldname für Feld-ID", title: "Titel", value: "Ninox Wert", width: "10%" }, { field: "Feldname für Feld-ID", title: "Titel", value: "Ninox Wert", width: "15%" }, { field: "Feldname für Feld-ID", title: "Titel", value: "Ninox Wert", width: "10%" }, { field: "Firma.Name", title: "Firma", value: Firma.Name, width: "10%" }, { field: "Bezeichnung", title: "Projekt", value: Bezeichnung, width: "15%", actions: [{ type: "popup", recordId: Nr }, { type: "change", recordId: Nr, field: "A" }] }, { field: "Umsatz", title: "Umsatz", value: text(Umsatz + 1000), width: "20%" }, { field: "Aufgaben", title: "Status Aufgaben", value: "

", width: "100px" }, { field: "Abgeschlossen", title: "Abgeschlossen am", value: text(Abgeschlossen), width: "100px" }] }], footer: { showFooter: false, actionButtonTitle: "", rightSideContent: "Gesamt: " + cnt(projectList) + " Projekte" } }; arcCustomTable(data) ``` ### Custom Table Complex ![](https://framerusercontent.com/images/OUceOtboxPQac3AJ3N3DEu9yas.png) ```javascript let current := this; arcCustomTable(data); let list := do as transaction select Aufgaben end; let filteredList := list[if current.Suche != null then testx(Mitarbeiter.'First Name' + "," + text(Mitarbeiter.'Last Name') + "," + text(Aufgabe) + "," + text(Projekte.Bezeichnung) + "," + ", "(?:" + current.Suche + ")\.*[^]", "gi") else true end and if current.'Erledigte einblenden' = true then true else Erledigt != true end]; let data := { uniqueListId: "Projektliste A", tableId: tableId(first(list)), height: "500px", theme: "", groupedBy: text('Gruppieren nach'), embedded: false, table: filteredList.[{ recordId: Nr, rowColor: "", rowHeight: "60px", groupRowColor: let currentRecord := this; if cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung and Erledigt = true]) = cnt(filteredList[Projekte.Bezeichnung = currentRecord.Projekte.Bezeichnung]) then "#dcf2de" else "#eee" end, columns: [{ field: "Erledigt", title: arcCheckBox({ uniqueId: "checkbox all check", value: Erledigt, embedded: true, clickAction: { recordId: Nr, fieldId: "G", value: if cnt(list[Erledigt = true]) != cnt(list) then false else if cnt(list[Erledigt = null]) = cnt(list) then null else if cnt(list[Erledigt = true]) = cnt(list) then true end end end } }), value: arcCheckBox({ uniqueId: "checkbox single check", value: Erledigt, embedded: true, clickAction: { recordId: Nr, fieldId: "G", value: if Erledigt = true then null else true end } }), width: "100px", align: "center", fixed: "left", groupValue: arcCustomIcon({ name: "caret-down" }) }, { field: "Bild", title: "Logo", width: "150px", value:", actions: [{ recordId: Projekte.Nr, type: "popup" }], groupValue: "", }, { field: "Projekt", title: "Projekt", width: "", value: Projekte.Bezeichnung, actions: [{ recordId: Projekte.Nr, type: "popup" }], groupValue: Projekte.Bezeichnung, }, { field: "Aufgaben", title: "Aufgabe", value: Aufgabe, align: "left", width: "200px", actions: [{ recordId: Nr, type: "popup" }, { recordId: Nr, type: "change", field: "A" }], groupValue: if current.'Gruppieren nach' = 3 then html(--- **{ Aufgabe }** ---) end }, { field: "Mitarbeiter", title: "Mitarbeiter", value:", actions: [{ recordId: Mitarbeiter.Nr, type: "popup" }] }, { field: "delete", title: "", width: "40px", align: "center", value: "", actions: [{ recordId: Nr, type: "delete" }] }] }], footer: { showFooter: true, showActionButton: true, actionButtonTitle: "Neue Aufgabe hinzufügen", rightSideContent: "Gesamt: " + cnt(filteredList) + " Aufgaben" } }; arcCustomTable(data) ``` ## Vue Das `data`-Objekt wird direkt in die Vue-Komponente als Prop übergeben. ```vue ``` --- # Custom Upload Source: https://www.arc-rider.com/docs/widgets/custom-upload # Upload in React Dateien auswählen, Bilder verkleinern und die Ergebnisse an deine Anwendung übergeben. Das Widget führt selbst keinen Backend-Upload durch. 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 { ArcWidgetUpload } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [preview, setPreview] = useState(''); const [name, setName] = useState(t('Keine Datei', 'No file', 'Sin archivo')); return ( <> { setPreview(file.src); setName(file.name); }} />

{name}

{preview && ( {t('Ausgewählte )} ); } ``` ## Features ausprobieren ### Mehrfachauswahl und Bildkonvertierung Wähle eigene Bilder und vergleiche die Qualitätseinstellungen. Die Dateien werden ausschließlich lokal im Browser verarbeitet. ```tsx import { useState } from 'react'; import { ArcWidgetUpload } from '@arcrider/arcwidgets-react'; export default function Example() { const [quality, setQuality] = useState(80); const [multiple, setMultiple] = useState(true); const [files, setFiles] = useState<{ name: string; src: string }[]>([]); return ( <> setFiles((previous) => [ ...previous, { name: file.name, src: file.src }, ]) } />

{files.length} Bilder lokal verarbeitet. Es erfolgt kein Upload auf einen Server.

{files.map((file, i) => (
{file.name}
{file.name}
))}
); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetUploadData`. Ü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). ### onUpload Eine aufbereitete Datei je Callback. ```typescript onUpload?: (payload: ArcWidgetUploadPayload) => void | Promise; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### image Verkleinerung, Bildformat und Qualität. Typ: `ArcWidgetUploadImage`. ### container Größe und Beschriftung der Upload-Fläche. Typ: `ArcWidgetUploadContainer`. ### multiupload Erlaubt die Auswahl mehrerer Dateien. Typ: `boolean`. ### capture Fordert auf unterstützten Geräten die Kamera an. Typ: `boolean`. ### clipboard Einfügen aus der Zwischenablage. Typ: `boolean | { tooltip?: string | { value?: string; position?: string } }`. ### drag Dateien per Drag & Drop auswählen. Typ: `boolean | ArcWidgetUploadDrag`. ### logs Zusätzliche Vorschau und Größeninformationen. Typ: `boolean`. ### filename Eigener Name für das Ergebnis. Typ: `string`. ### showLoadingPopup Ladeanzeige während der Verarbeitung. Typ: `boolean`. ### convertSettings Zusätzliche Konvertierungsoptionen, etwa die Rückgabe des Originals. Typ: `{ originalFile?: boolean | Record; [key: string]: unknown }`. ## Verhalten und Grenzen - `onUpload` wird pro Datei aufgerufen und erhält `name`, `src` sowie bei Bildern Abmessungen. `src` ist die aufbereitete Data-URL. - `image` steuert maximale Bildgröße, Format und Qualität. `multiupload` erlaubt mehrere Dateien. - Übertrage die Datei im Callback an deine eigene API und behandle Erfolg und Fehler dort. Das Widget liefert keinen fertigen Speicherdienst. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetUploadImage { width?: number; height?: number; format?: 'jpeg' | 'webp' | 'original' | string; quality?: number; } export interface ArcWidgetUploadDrag { enabled?: boolean; label?: string; target?: string; targetId?: string; } export interface ArcWidgetUploadContainer { height?: string; width?: string; label?: string; icon?: string; value?: unknown; } export interface ArcWidgetUploadData extends ArcWidgetBaseData { image?: ArcWidgetUploadImage; container?: ArcWidgetUploadContainer; multiupload?: boolean; capture?: boolean; clipboard?: boolean | { tooltip?: string | { value?: string; position?: string } }; drag?: boolean | ArcWidgetUploadDrag; logs?: boolean; filename?: string; showLoadingPopup?: boolean; convertSettings?: { originalFile?: boolean | Record; [key: string]: unknown }; } export interface ArcWidgetUploadPayload { name: string; src: string; width?: number; height?: number; original?: string | null; } export interface ArcWidgetUploadProps extends ArcReactWidgetProps { onUpload?: (payload: ArcWidgetUploadPayload) => void | Promise; } ``` ## 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 Upload ## AI Defaults (read first) - **uniqueId**: Required, `"upload-" + Nr`. - **image.tableId**: Required – target table for new records. Ninox-handler only — core emits semantic file payload. - **image.fieldId**: The text field for Base64 data. Platform-dependent (Web vs. App may differ). Adapter-only. - **Persistence**: Core emits `upload` with `{ name, src, width?, height?, original? }` where `original` is an optional Base64 string (PDF source when `convertSettings.originalFile` is set). Ninox handler builds `update` / `create` / `popup` from `ui.data.image.*` and resolves `originalFile.fieldId` / `recordId` from `convertSettings`. React: `onUpload(payload)`. - **container**: `height: "500px"`, `width: "100%"`. - **multiupload**: Default `false`, set `true` for multiple files at once. - **capture**: Default `false`, set `true` to open camera directly. - **clipboard**: Optional `clipboard: true` — Einfügen per Strg/Cmd+V oder Klick, wenn Dateien in der Zwischenablage liegen. - **drag**: Default **on** for the empty dropzone (hover + drop). Custom `container.value` stays opt-in via `drag: true` or `{ enabled, label?, target?, targetId? }`. `drag: false` opts out. - **image.format / image.quality**: Optional encode — `format: "jpeg"` | `"webp"` | `"original"`, `quality: 0–1` (default `0.8` when format is set). Offline im Browser; gut für viele Fotos in PDF-Reports. - **logs**: Optional `logs: true` — Encode-Größen + Bildvorschau (Console + Preview unter dem Widget). - **Two Base64 fields**: Some setups need separate fields for Web and App – check your schema. # Custom Upload Mit `arcCustomUpload` kannst du Bilder und Dateien direkt in Ninox hochladen – einzeln oder als Mehrfachauswahl. Dabei wird automatisch für jede Datei ein eigener Datensatz erzeugt, inklusive Base64-Inhalt, Dateiname und optionalen Zusatzinformationen. Das Widget löst eine der größten Einschränkungen in Ninox: den umständlichen Mehrfach-Upload. Statt komplizierter Workarounds öffnet sich nun mit einem Klick der Datei-Explorer oder die Kamera – und das **in jeder beliebigen Oberfläche**: ob in einem Dashboard, über einen Custom Button oder innerhalb eines eigenen Layouts. Die hochgeladenen Bilder landen zunächst als Base64-Text in einem Feld deiner Wahl und können dort: - direkt mit dem Widget [Custom Image](/documentation/images) angezeigt werden, - per Skript in ein Ninox-Bildfeld überführt werden (empfohlen), - oder einfach als Datei abgespeichert werden. Auch auf **mobilen Geräten** ist das Widget ideal: Du kannst wahlweise per Kamera fotografieren oder mehrere Bilder aus der Galerie auswählen und hochladen. Das spart dem Nutzer viele Klicks und vereinfacht die Datenpflege spürbar. ⚠️ **Hinweis:** Auf der **MacOS App** sowie auf der **Android App** funktioniert aktuell leider die Multi-Upload Funktion nicht. Hier sind die Restriktionen von Ninox Seite her eingeschränkt. ## Unterstützte Formate - **Bilder**: JPG, PNG, SVG - **Dokumente**: PDF - **Tabellen & Daten**: CSV, XML, XLS, XLSX **Hinweis:** PNG- und SVG-Dateien werden aktuell automatisch in JPG konvertiert. Für hochwertige Weiterverarbeitung empfehlen wir, die Base64-Daten zeitnah in echte Dateien umzuwandeln und als Dateifeld in Ninox zu speichern. ### HEIC/HEIF (iPhone-Fotos) Moderne iPhones speichern Fotos standardmäßig im **HEIC-Format**. Hier gilt: | Upload-Methode | Funktioniert? | Erklärung | |----------------|---------------|-----------| | **Direkt vom iPhone/iPad** | ✅ Ja | iOS konvertiert automatisch zu JPEG | | **Desktop → Safari (macOS)** | ✅ Ja | Safari unterstützt HEIC nativ | | **Desktop → Chrome/Firefox/Edge** | ❌ Nein | Diese Browser können HEIC nicht dekodieren | 💡 **Tipp für Desktop-Uploads:** Falls Nutzer regelmäßig Fotos vom iPhone auf den Desktop übertragen und dort hochladen möchten, empfehlen wir folgende iPhone-Einstellung: > **Einstellungen → Kamera → Formate → Maximale Kompatibilität** Damit werden Fotos direkt als JPEG gespeichert und sind mit allen Browsern kompatibel. ![](https://framerusercontent.com/images/tfmYk7LEFF3TRtIAdgEuiSIf1hs.png) ## Anwendungscode Hier ein einfaches Beispiel, wie du `arcCustomUpload` in einer Funktion verwenden kannst – zum Beispiel eingebunden in einer [Tabelle](https://www.arc-rider.de/documentation/custom-table) (`embedded:true`) oder eigenständig auf einem Ninox Formular in einem Formelfeld (`embedded:false`): ```javascript let current := this; arcCustomUpload({ uniqueId: "my-Files-" + Nr, embedded: true, capture: false, multiupload: true, container: { icon: "", label: "Files Upload", height: "500px", width: "100%", value: "" }, image: { tableId: "B", fieldId: if ninoxApp() = "web" then fieldId("Bilder", "base64_web") else fieldId("Bilder", "base64_app") end, width: 1500, height: 1500, widthFieldId: "J", heightFieldId: "K" }, filename: { tableId: "B", fieldId: fieldId("Bilder", "Dateiname") }, convertSettings: { type: "" }, changeFieldValues: [{ fieldId: "B", value: number(current.Nr) }, { fieldId: "E", value: 12 }], setNewRecordId: [{ recordId: current.Nr, fieldId: fieldId(current.Nr, "triggerField_saveForBrowser") }] }) ``` ## Parameter ### uniqueId **Typ:** `text` **Pflichtfeld:** ja ***uniqueId*** vergibst du individuell und sollte einzigartig sein. Sinn dahinter ist: Wenn du mehrere Uploads mit unterschiedlichen Settings auf deiner Oberfläche erstellst, verhinderst du das Überschreiben der Styles. **📌 Achtung: **Wenn zwei Upload-Widgets auf derselben Seite die gleiche `uniqueId` verwenden, kann das zu Fehlern führen (z. B. werden Uploads überschrieben oder nicht korrekt geladen). ```javascript uniqueId: "my-Image-" + Nr, ``` ### embedded **Typ:** `boolean` **Default:** `false` Mit dem Parameter `embedded` bestimmst du, ob das Upload-Widget **eingebettet in ein bestehendes Element** (z. B. ein `Custom Layout`, `Custom Button`, etc.) angezeigt werden soll – oder ob es als eigenständiges Element auf der Oberfläche steht. - `true` → Das Widget passt sich dem umgebenden Container an (z. B. bei festen Layout-Rahmen). - `false` → Das Widget wird frei auf der Ninox-Oberfläche dargestellt (z. B. als alleinstehendes Element). 💡 **Hinweis: **Wenn du `embedded: true` verwendest, achte darauf, dass das umgebende Element (z. B. ein Layout-Block) eine definierte Höhe/Breite hat. Das Upload-Feld übernimmt diese Maße automatisch. ### capture **Typ:** `boolean` **Default:** `false` Mit dem Parameter `capture` steuerst du, ob das Widget auf **mobilen Geräten direkt die Kamera** öffnet oder ob der Nutzer eine Datei manuell auswählen kann. - `true` → Auf unterstützten Smartphones öffnet sich beim Klick direkt die Kamera-App. - `false` → Es wird wie gewohnt der Datei-Explorer oder die Fotogalerie geöffnet. 💡 **Hinweis: **Diese Funktion funktioniert nur auf mobilen Geräten (Apple) und in Kombination mit bestimmten Browsern. Auf Desktop wird der Parameter ignoriert. ```javascript capture: "", // Default: false capture: false, ``` ### multiupload **Typ:** `boolean` **Default:** `false` Mit `multiupload` kannst du festlegen, ob **mehrere Dateien gleichzeitig** hochgeladen werden dürfen. - `true` → Der Nutzer kann im Dateidialog mehrere Dateien auswählen (z. B. mehrere Bilder aus der Galerie). - `false` → Es kann nur eine Datei pro Upload-Vorgang hochgeladen werden. 💡 **Tipp: **Besonders praktisch bei Formularen, in denen mehrere Fotos oder Dokumente auf einmal hochgeladen und als einzelne Datensätze gespeichert werden sollen. ### logs **Typ:** `boolean` oder `{ enabled: true }` **Default:** `false` Mit `logs: true` schreibt das Widget **Encode-/Resize-Statistiken** in die Browser-Console (DevTools) und zeigt unter dem Upload eine **Bildvorschau** des Ergebnisses — nützlich zum Kalibrieren von `image.format` und `image.quality`: - Dateiname und MIME - Pixel vorher → nachher - Größe vorher → nachher und Einsparung in % ```javascript logs: true ``` Beispiel-Zeile: ```text [arcCustomUpload:…] IMG_1234.JPG image/jpeg → jpeg q=0.7 | 4032×3024→1200×900 | 3.2 MB → 180 KB (−94%) ``` ### clipboard **Typ:** `boolean` **Default:** `false` Mit `clipboard: true` können Dateien aus der Zwischenablage eingefügt werden — per **Strg+V / Cmd+V** oder per Klick auf die Upload-Fläche, wenn im Finder/Explorer Dateien kopiert wurden. - Es erscheint ein Badge-Hinweis, sobald die Zwischenablage **Dateien** (nicht nur Text) enthält. - Enthält die Zwischenablage nur Text, öffnet ein Klick wie gewohnt den Datei-Dialog. - Mehrere Upload-Widgets auf einer Seite: reagiert nur das fokussierte Widget. ```javascript clipboard: true ``` ### drag **Typ:** `boolean` oder `object` **Default:** aktiv auf der leeren Standard-Dropzone; aus, sobald `container.value` gesetzt ist Die leere Dropzone (Icon + Label, kein `container.value`) nimmt Dateien per **Drag & Drop** an und zeigt Hover- sowie Drop-Feedback. Mit eigenem `container.value` bleibt Drag opt-in. `drag: false` schaltet Drop überall aus. `drag: true` oder ein Objekt schaltet es auch für Custom-Layouts ein: ```javascript drag: true // oder mit Optionen: drag: { enabled: true, label: "Datei hier ablegen", target: "area", // "area" | "first-child" | "layout" targetId: "meine-zeile-id" // optional: externe Drop-Zone per DOM-ID } ``` | `target` | Verhalten | |----------|-----------| | `"area"` | Overlay über der Upload-Fläche | | `"first-child"` | Standard mit `container.value` | | `"layout"` | Erstes `arcCustomLayout` im Widget | Mit `targetId` kann eine **externe Zeile oder Fläche** (z. B. eine Tabellenzeile) als Drop-Zone dienen — die ID wird zuerst im Upload-Label gesucht, sonst im gesamten Dokument. ### showLoadingPopup **Typ:** `boolean` **Default:** `false` Mit `showLoadingPopup` kannst du ein **Loading-Popup während des Uploads** anzeigen lassen. Das ist besonders nützlich bei: - großen Bilddateien - langsamen Internetverbindungen - Multi-Uploads mit vielen Dateien Das Popup blockiert die Oberfläche und informiert den Nutzer, dass der Upload noch läuft. Es schließt sich automatisch, sobald alle Dateien hochgeladen wurden. ```javascript showLoadingPopup: true, ``` 💡 **Tipp:** Kombiniere `showLoadingPopup: true` mit `multiupload: true` für eine bessere UX bei Massen-Uploads. Auf der **Ninox iOS App** erscheint das Popup zuverlässig, sobald der Upload startet (das Widget wartet auf die Anzeige des Modals, bevor Dateien verarbeitet werden). ### Block: container{} **Typ:** `object` **Optional** Der `container`-Block definiert das **Erscheinungsbild und Verhalten des Upload-Feldes**. Du kannst hier z. B. die Größe festlegen oder einen eigenen Text im Upload-Bereich anzeigen lassen. Wenn du keine Werte übergibst, wird ein Standard-Layout mit Upload-Icon und dem Text „Datei auswählen“ angezeigt. ```javascript container: { icon: "", label: "Upload Image", height: "100%", value: "wert" }, ``` **Mögliche Parameter**
Feld Typ Beschreibung
`label` `text` Beschriftung (Text) im Upload-Feld. Wird in der Mitte angezeigt. Wird unter dem Icon angezeigt.
`value` `widget` `text` Mit value kannst du dein ganz eigenes Design des Uploads gestalten. Alles, was du bei value angibst, überschriebt das bestehende Standard-Design. Hier kannst du beispielsweise einen einfachen [Custom Button](/documentation/buttons) verwenden oder ein [eigenes Custom Layout](/documentation/custom-layout) gestalten, das sich deiner UI anpasst. Dieser Parameter ist optional und muss natürlich nicht befüllt werden.
`height` `text` Höhe des Upload-Feldes, z. B. `"80px"` oder `"100%"`.
`width` `text` Breite des Upload-Feldes, z. B. `"100%"`, `"300px"` oder `"auto"`.
`icon` `widget` Mit `[arcCustomIcon](https://www.arc-rider.de/documentation/custom-icon)` kann ein individuelles Icon bestimmt werden.
💡 **Tipp: **Vermeide zu lange Texte im `label`, da das Widget zentriert arbeitet und die Lesbarkeit sonst leidet. Ideal sind 1–2 Wörter oder ein Symbol mit kurzer Erklärung ### Block: image{} **Typ:** `object` **Pflichtfeld** Im `image`-Block definierst du, **wohin das hochgeladene Bild gespeichert werden soll**. Das Bild wird automatisch in ein Base64-Textfeld in der angegebenen Tabelle geschrieben. Optional kannst du auch die Bildbreite und -höhe separat erfassen.
Feld Typ Beschreibung
`tableId` `text` Ziel-Tabelle, in der das Bild als Datensatz gespeichert wird.
`fieldId` `text` Feld-ID des Textfelds, in das der Base64-Bildinhalt geschrieben wird.
`widthFieldId` `text` *(optional)* Feld-ID für die gespeicherte Bildbreite (in Pixeln). Erstelle dafür ein Ninox-Zahlenfeld.
`heightFieldId` `text` *(optional)* Feld-ID für die gespeicherte Bildhöhe (in Pixeln). Erstelle dafür ein Ninox-Zahlenfeld.
`width` / `height` `text` *(optional)* Die width / height innerhalb des image{} Blocks gibt die Weite / Höhe deines Bildes in Pixelbreite an, in der du es speichern möchtest.
`format` `text` *(optional)* Ausgabeformat nach dem Upload (offline im Browser). `"jpeg"` / `"jpg"`, `"webp"` oder `"original"` (Default: weglassen / `"original"` = bisheriges Verhalten, MIME vom Eingang). Bei `"jpeg"`/`"webp"` wird das Bild immer neu encodiert — auch wenn es schon kleiner als `width`/`height` ist. Der Dateiname erhält die passende Extension (`.jpg` / `.webp`).
`quality` `number` *(optional)* Kompressionsqualität von `0` bis `1` (nur bei `format: "jpeg"` oder `"webp"`). Default `0.8`, wenn `format` gesetzt und `quality` fehlt. Gilt **nicht** bei `"original"`.
Die Encode-Schritte laufen **offline direkt im Widget** (Kamera und Galerie gleich). Kein Cloud-Dienst nötig. Hinweis: Bei `format: "jpeg"` geht PNG-Transparenz verloren (bewusst, für Fotos/PDF-Reports). ```javascript image: { tableId: "B", fieldId: if ninoxApp() = "web" then fieldId("Bilder", "base64_web") else fieldId("Bilder", "base64_app") end, width: 1500, height: 1500, widthFieldId: "J", heightFieldId: "K" }, ``` **Beispiel Baustelle / PDF-Reports** (viele Fotos, PDF per E-Mail ca. 5–10 MB halten): Kamera (`capture: true`) und Galerie-Mehrfachupload (`multiupload: true`) nutzen denselben Encode-Pfad. Für Ninox/`importFile` eher **JPEG** als WebP: ```javascript image: { tableId: "B", fieldId: if ninoxApp() = "web" then fieldId("Bilder", "base64_web") else fieldId("Bilder", "base64_app") end, width: 1200, height: 1200, format: "jpeg", quality: 0.7, widthFieldId: "J", heightFieldId: "K" }, ``` Bei sehr vielen Fotos pro Report (60+) `width`/`height` auf `1000` und/oder `quality` auf `0.6` testen. Die finale PDF-Größe hängt auch vom Report-Generator ab — nach dem Setup an einem echten Report kalibrieren. #### Wichtiger Performance Hinweis 💡 **UI/UX-Tipp: **Base64-Daten können sehr groß werden – besonders bei hochauflösenden Bildern. Daher empfehlen wir folgenden Workflow: - Hinterlege einen Trigger bei Änderung in dem Base64-Feld. - Übertrage das Base64-Bild mit `importFile()` in ein echtes Bildfeld. - Leere das Base64-Feld danach, um Speicherplatz zu sparen. Diesen Trigger hinterlegst du: ```javascript Datei := importFile(Nr, base64, Dateiname); shareURL := shareFile(Datei); base64:=null; ``` So bleiben Ladezeiten und Datenbankgröße im Rahmen – und du kannst trotzdem direkt mit den Bilddaten weiterarbeiten. 🧠 **Wichtig: **Ninox behandelt Trigger in der App und im Browser unterschiedlich. Damit dein Workflow **plattformübergreifend** zuverlässig funktioniert, nutze **zwei getrennte base64-Felder** (mit jeweils unterschiedlichen Speicher-Optionen) – eines für Web, eines für die App: - **App** -> Zuweisung pro Datensatz im Browser - **Web** -> Zuweisung pro Datensatz im Server ✅ So stellst du sicher, dass der Upload in **beiden Umgebungen** automatisch weiterverarbeitet wird. ### filename **Typ:** `object` **Optional** Im `filename`-Block kannst du angeben, **in welchem Feld der Dateiname** gespeichert werden soll. Das ist hilfreich, wenn du später nach Dateien filtern oder sie in Listen anzeigen möchtest.
Feld Typ Beschreibung
`tableId` `text` Ziel-Tabelle, in der der Datensatz gespeichert wird
`fieldId` `text` Feld-ID, in das der Dateiname geschrieben wird
💡 **Tipp: **Falls du später mit `importFile()` weiterarbeitest oder die Datei wieder abrufen willst (z. B. in einer Galerie oder Download-Liste), ist der Dateiname oft notwendig – speichere ihn daher am besten immer mit. ### changeFieldValues **Typ:** `array of objects` **Optional** Mit `changeFieldValues` kannst du **zusätzliche Felder im neu erstellten Upload-Datensatz automatisch befüllen** – z. B. mit einem Bezug zur aktuellen Tabelle, einem festen Typ oder Statuswert. Jeder Eintrag enthält:
Feld Typ Beschreibung
`fieldId` `text` Das Feld im neuen Datensatz, das gesetzt werden soll
`value` `any` Der Wert, der in das Feld geschrieben wird
```javascript changeFieldValues: [{ fieldId: "B", value: number(current.Nr) }, { fieldId: "E", value: 10 }] ``` ### setNewRecordId **Typ:** `array of objects` **Optional** Mit `setNewRecordId` kannst du nach dem Upload den **neu erstellten Datensatz (z. B. das Bild)** direkt in einem vorhandenen Datensatz **verlinken** – etwa in einem Referenzfeld des aktuellen Formulars. Das ist besonders nützlich, wenn du: - eine **1:1-Verknüpfung** zu einem Bild oder Dokument brauchst, - das Upload-Element **nicht in der Zieltabelle** eingebettet ist, - oder gezielt Uploads außerhalb der `changeFieldValues` setzen willst.
Feld Typ Beschreibung
`recordId` `text` Der ID-Wert des bestehenden Datensatzes, in dem verlinkt werden soll
`fieldId` `text` Das Referenzfeld, das mit dem neuen Upload-Datensatz gefüllt werden soll
## Extra Konvertierungsservice Die PDF-Konvertierung über `convertSettings` ist **kein Bestandteil des Standard-Uploads**, sondern ein **externer Service**, der über arcRider bereitgestellt wird. Da bei jeder Konvertierung **Serverkosten** anfallen, ist die Nutzung **kostenpflichtig**. Die Abrechnung erfolgt **je Konvertierungsvorgang**. 💬 Für die Nutzung des Dienstes erstellen wir individuelle Preispläne – abhängig vom erwarteten Volumen und den konkreten Anforderungen. Bitte kontaktiere uns, wenn du den PDF-Konvertierungsdienst aktivieren möchtest oder Fragen zu den Konditionen hast. ([office@arc-rider.com](mailto:office@arc-rider.com)) ### convertSettings **Typ:** `object` **Optional – nur bei PDF-Dateien relevant** Wenn du PDF-Dateien hochlädst, kannst du mit `convertSettings` festlegen, dass sie **automatisch in JPG / PNG / SVG-Dateien umgewandelt** werden – z. B. für technische Zeichnungen oder Formular-PDFs, die in Ninox darstellbar sein sollen.
Feld Typ Beschreibung
`fileType` `text` Zielformat der Konvertierung. Aktuell unterstützt: `"jpg"`
`apiKey` `text` Dein persönlicher API-Schlüssel für den PDF-Konvertierungsdienst (Bitte anfragen bei arcRider)
`width` `number` Zielbreite in Pixeln (wenn `dpi` nicht gesetzt ist)
`height` `number` Zielhöhe in Pixeln (wenn `dpi` nicht gesetzt ist)
`dpi` `number` Auflösung der Konvertierung in **dpi (dots per inch)**
`originalFile` `object` Optional: Feld zur Speicherung der originalen PDF-Datei als Base64
## ✅ Fazit Mit `arcCustomUpload` gestaltest du Upload-Prozesse in Ninox intuitiv, performant und flexibel – egal ob auf Desktop oder mobil. Durch gezielte Parametrierung steuerst du Darstellung, Zieltabellen, Automatisierungen und sogar die Konvertierung von PDFs. Wenn du komplexe Uploads in dein System integrieren möchtest oder Hilfe beim Setup brauchst, melde dich gerne bei uns – wir unterstützen dich bei der optimalen Konfiguration. --- # Badges Source: https://www.arc-rider.com/docs/mini-widgets/badges # Badge in React Status oder kurze Kennzeichnungen in Tabellen, Karten und Formularen darstellen. 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 { ArcWidgetBadge } from '@arcrider/arcwidgets-react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { return ( ); } ``` ## Features ausprobieren ### Statusfarben und verschachtelte Icons Wechsle den Status und blende das Icon ein. Beide Werte werden als neue Konfiguration an dasselbe Widget übergeben. ```tsx import { useState } from 'react'; import { ArcWidgetBadge, ArcWidgetIcon } from '@arcrider/arcwidgets-react'; export default function Example() { const [state, setState] = useState(0); const [icon, setIcon] = useState(true); const states = [ { title: 'Offen', color: '#a16207' }, { title: 'In Arbeit', color: '#52525b' }, { title: 'Erledigt', color: '#15803d' }, ]; return ( <> ) : undefined, }} /> ); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetBadgeData`. Ü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). Dieses Widget ist eine Darstellungskomponente und stellt keine eigenen Interaktions-Callbacks bereit. ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### value Anzuzeigender bzw. aktuell bearbeiteter Wert. Typ: `ArcWidgetSlot`. ### icon Optionales Symbol bzw. unterstützter Inhaltsslot. Typ: `ArcWidgetSlot`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### iconPosition Position des Symbols relativ zur Beschriftung. Typ: `'left' | 'right' | 0`. ### iconSize Größe des Symbols. Typ: `string`. ### iconColor Farbe des Symbols. Typ: `string`. ### fontColor Schriftfarbe. Typ: `string`. ### fontSize Schriftgröße als CSS-Wert. Typ: `string`. ### fontWeight Schriftstärke. Typ: `string`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### borderColor Rahmenfarbe. Typ: `string`. ### borderRadius Abrundung der Ecken. Typ: `string`. ### singleColor Basisfarbe für eine abgestimmte Statusdarstellung. Typ: `string`. ### paddingX Innerer Abstand links und rechts. Typ: `string`. ### paddingY Innerer Abstand oben und unten. Typ: `string`. ## Verhalten und Grenzen - `value` unterstützt Text und JSX. Für einheitliche Statusfarben kannst du `singleColor` setzen oder Hintergrund und Schrift getrennt gestalten. - Das Badge besitzt keinen eigenen Klick-Callback. Verwende einen umgebenden Button, wenn der Status interaktiv sein soll. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetBadgeData extends ArcWidgetBaseData { value?: ArcWidgetSlot; icon?: ArcWidgetSlot; width?: string; height?: string; iconPosition?: 'left' | 'right' | 0; iconSize?: string; iconColor?: string; fontColor?: string; fontSize?: string; fontWeight?: string; backgroundColor?: string; borderColor?: string; borderRadius?: string; singleColor?: string; paddingX?: string; paddingY?: string; } ``` ## 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) # Badges ## AI Defaults (read first) - **Function name**: `arcCustomBadge`. - **value**: The badge text. - **fontColor**: Text color (NOT `color` or `textColor`). - **backgroundColor**: Background fill color. - **borderColor**: Set same as `backgroundColor` for borderless look. - **No uniqueId needed** for display-only badges. - **icon** / **value**: May use `arcCustomIcon` or other mini-widgets (not only strings). # Badge ![](https://framerusercontent.com/images/GZ3ZMuhAEaLfjqObVHweqoidP6U.png) Das Badge eignet sich, um besondere Merkmale von Datensätzen optisch hervorzuheben. Es kann losgelöst in der Datenbank verwendet oder in anderen Widgets, wie die [Custom Table](/documentation/custom-table), eingesetzt werden. **icon** und **value** dürfen Mini-Widgets sein (z. B. `icon: arcCustomIcon({ name: "star", iconSize: "14px" })`), nicht nur Text – das Widget rendert sie wie in `arcCustomSelect` oder `arcCustomButton`. ## Anwendungscode Der folgende Anwendungscode enthält alle möglichen Parameter für dieses Mini-Widget Badge. ```javascript arcCustomBadge({ uniqueId: "customBadge", embedded: true, width: "220px", icon: "", iconPosition: "left", fontSize: "", fontWeight: "", borderRadius: "", singleColor: "", fontColor: "", backgroundColor: "", borderColor: "", paddingY: "", paddingX: "", value: "Löschen" }) ``` ## Detaillierte Beschreibung der Parameter ***uniqueId*** vergibst du individuell und sollte einzigartig auf deiner Oberfläche sein. Sinn dahinter ist: Wenn du mehrere Badges mit unterschiedlichen Settings auf deiner Seite erstellst, überschrieben sich die Styles nicht. ```javascript uniqueId: "", ``` ***value*** gibt den Text-Wert in dem Badge aus. ```javascript value: "Badge-Text" // Fixer Text-Wert in " value: Projekt.Status // Ninox-Felder / -Berechnungen ``` ***width*** gibt die Breite deines Badges an. ```javascript width: "500px", // Pixel-Werte für festgelegte Breiten. width: "fit-content", // Passt sich dem Inhalt automatisch an. ``` ***iconPosition*** ist die Position des Icons innerhalb des Badges. ```javascript iconPosition: "left", // Icon erscheint links vom Text. iconPosition: "right", // Icon erscheint rechts vom Text. ``` ***fontSize*** gibt die Schriftgröße des Textes im Badge an. ```javascript fontSize: "14px", //Pixel-Wert ``` ***fontWeight*** bestimmt die Dicke deines Textes. ```javascript fontWeight: "400", // Zahlen-Wert | 400 entspricht regular fontWeight: "700", // Zahlen-Wert | 700 entspricht bold ``` ***borderRadius*** bestimmt die Abrundung der Ecken vom Badge. ```javascript borderRadius: "0.3em", // Die Ecken passen sich proportional zur Größe des Badges an. borderRadius: "4px", // Pixel-Werte für fixe Abrundung. ``` ***embedded*** benutzt du, wenn du den Button ***nicht ***innerhalb eines anderen Widgets einsetzt. Standardmäßig ist dies aber auf true gesetzt. ```javascript embedded: false, // ermöglicht den Standalone Einsatz des Widgets (nicht eingebettet in anderen Widgets) embedded: true, // Fallback ``` ### Eine Farbe mit Abstufungen Gibst du eine ***singleColor*** an, so passen sich alle weiteren Farben (alle Werte mit Color) automatisch an diesem Wert an. Das heißt du brauchst die folgenden Color-Werte dann nicht mehr ausfüllen. ```javascript singleColor: "#1D8C96", // HEX-Code für Farben. ``` ### Individuelle Farbgestaltung ***fontColor*** gibt die Farbe der Schrift an ```javascript fontColor: "#1D8C96", // HEX-Code für Farben. ``` ***backgroundColor*** gibt die Farbe des Hintergrundes an. ```javascript backgroundColor: "#1D8C96", // HEX-Code für Farben. ``` ***borderColor*** gibt die Farbe der Umrandung an. ```javascript borderColor: "#1D8C96", // HEX-Code für Farben. ``` ## Standard-Werte als Fallback 💡 **Hinweis: **Trägst du keine Werte hinter die Parameter ein, so ist in den meisten Fällen ein Fallback-Wert im System hinterlegt. An manchen Stellen, wie Berechnungsfelder (Tatsächliche Zahlenwerte zum Beispiel) macht das keinen Sinn. In Style-Parametern, wie Farben oder Größen hingegen schon. Hier siehst du den Code ohne eingetragene Werte und in den Kommentaren dahinter die Standard-Werte: ```javascript arcCustomBadge({ uniqueId: "", width: "", // Fallback: "fit-content" icon: "", // Fallback: kein Icon wird angezeigt iconPosition: "",// Fallback: kein Icon wird angezeigt fontSize: "", // Fallback: "12px" fontWeight: "", // Fallback: "400" borderRadius: "", // Fallback: "0.3em" singleColor: "", // Fallback: "#3E6FFF" iconColor: "", // Fallback ist die singleColor fontColor: "", // Fallback ist die singleColor backgroundColor: "", // Fallback ist die singleColor borderColor: "", // Fallback ist die singleColor value: "", // Kein Fallback }) ``` --- # Button Source: https://www.arc-rider.com/docs/mini-widgets/buttons # Button in React Eine Aktion auslösen und während eines asynchronen Vorgangs einen Ladezustand 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 ```tsx import { ArcWidgetButton } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [status, setStatus] = useState(t('Bereit', 'Ready', 'Listo')); return ( <> { setStatus(t('Speichert …', 'Saving …', 'Guardando …')); try { localStorage.setItem('arc-demo-save', new Date().toISOString()); setStatus( t('Lokal gespeichert', 'Saved locally', 'Guardado en local'), ); } catch { setStatus( t('Speichern fehlgeschlagen', 'Save failed', 'Error al guardar'), ); } }} />

{status}

); } ``` ## Features ausprobieren ### Asynchron speichern, Ladezustand und Fehler Speichere mehrfach oder simuliere einen Fehler. Der Button wartet auf das Promise; der Zähler wird nur bei Erfolg erhöht. ```tsx import { useState } from 'react'; import { ArcWidgetButton, ArcWidgetIcon } from '@arcrider/arcwidgets-react'; export default function Example() { const [count, setCount] = useState(0); const [fail, setFail] = useState(false); const [status, setStatus] = useState('Bereit'); return ( <> ), backgroundColor: '#18181b', fontColor: '#ffffff', showBadge: true, badgeTitle: String(count), loading: { show: true, minDuration: 600, indicator: { type: 'spinner' }, }, }} onClick={async () => { setStatus('Speichert …'); await new Promise((resolve) => setTimeout(resolve, 800)); if (fail) { setStatus('Speichern fehlgeschlagen. Erneut versuchen.'); return; } setCount((n) => n + 1); setStatus('Gespeichert'); }} />

{status} · Erfolgreich gespeichert: {count}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetButtonData`. Ü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). ### onClick Meldet den Klick. Beachte die widgetspezifischen Argumente unten. ```typescript onClick?: () => void | Promise; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### title Beschriftung; als Slot auch mit JSX möglich. Typ: `ArcWidgetSlot`. ### subtitle Ergänzende Beschriftung. Typ: `ArcWidgetSlot`. ### icon Optionales Symbol bzw. unterstützter Inhaltsslot. Typ: `ArcWidgetSlot`. ### prefix Inhalt vor der Beschriftung. Typ: `ArcWidgetSlot`. ### suffix Inhalt nach der Beschriftung oder Einheit. Typ: `ArcWidgetSlot`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### borderRadius Abrundung der Ecken. Typ: `string`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### fontColor Schriftfarbe. Typ: `string`. ### fontSize Schriftgröße als CSS-Wert. Typ: `string`. ### showBadge Blendet die zusätzliche Kennzeichnung ein. Typ: `boolean`. ### badgeTitle Inhalt der Kennzeichnung. Typ: `ArcWidgetSlot`. ### badgeColor Schriftfarbe der Kennzeichnung. Typ: `string`. ### badgeBackground Hintergrund der Kennzeichnung. Typ: `string`. ### loading Ladezustand während eines Promise-Callbacks, aktiviert durch `show: true`. Typ: `ArcWidgetButtonLoading`. ## Verhalten und Grenzen - `onClick` darf ein Promise zurückgeben. Mit `loading.show: true` bleibt der Ladezustand bis zum Ende dieses Vorgangs aktiv. - Fange Speicherfehler in deinem Callback ab und zeige sie im eigenen State. Ein Ladeindikator ersetzt keine Erfolgs- oder Fehlermeldung. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetButtonLoading { show?: boolean; indicator?: { type?: 'dots' | 'spinner' | 'pulse' | string; color?: string; width?: string; height?: string; [key: string]: unknown; }; hideOnFinish?: boolean; minDuration?: number; position?: string; hideTitle?: boolean; hideIcon?: boolean; hide?: { icon?: boolean | 'auto'; title?: boolean | 'auto'; subtitle?: boolean | 'auto'; badge?: boolean | 'auto'; prefix?: boolean | 'auto'; suffix?: boolean | 'auto'; [key: string]: unknown; }; overlay?: unknown; styles?: ArcWidgetStyles | string | 'hover' | 'default'; [key: string]: unknown; } export interface ArcWidgetButtonData extends Omit { title?: ArcWidgetSlot; subtitle?: ArcWidgetSlot; icon?: ArcWidgetSlot; prefix?: ArcWidgetSlot; suffix?: ArcWidgetSlot; width?: string; height?: string; borderRadius?: string; backgroundColor?: string; fontColor?: string; fontSize?: string; showBadge?: boolean; badgeTitle?: ArcWidgetSlot; badgeColor?: string; badgeBackground?: string; loading?: ArcWidgetButtonLoading; } export interface ArcWidgetButtonProps extends ArcReactWidgetProps { onClick?: () => void | Promise; } ``` ## Einsatzbeispiele - [Status auswählen und speichern](/docs/react/examples/status-als-buttons-ausgeben) - [Ein Formular in Schritten](/docs/react/examples/datenaufnahme-in-schritten) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Button ## AI Defaults (read first) - **uniqueId**: Required, e.g. `"btn-save-" + Nr`. - **title**: Button label (use `""` for icon-only buttons). - **actions**: Array of `{ type: "update", recordId: Nr, field: fieldId(Nr, "Feld"), value: ... }`. Prefer `type: "update"` over `customJS`. - **Icon-only button**: Use `arcCustomButton` with `icon: arcCustomIcon({...})`, NOT a layout wrapper with `clickAction`. - **Nested widgets**: `icon`, `title`, `subtitle`, and badge text (`badgeTitle` with `showBadge: true`) may use `arcCustomIcon`, `arcCustomBadge`, or other mini-widgets – not only strings. - **prefix / suffix**: Mini-widget slots rendered before (`prefix`) and after (`suffix`) the main content. `suffix` is automatically pushed to the right edge when the button has an explicit `width` (e.g. `"100%"`). Both accept any mini-widget (`arcCustomIcon`, `arcCustomBadge`, `arcCustomLayout`, plain text). - **loading**: After click, `{ show: true, indicator: { type, color, width, height }, hideOnFinish, minDuration, position, hideTitle, hideIcon, styles }`; set **`overlay`** (`true`, `{ title, message }`, or `{ sequences: [...] }`) for fullscreen overlay. Types: `dots`, `spinner`, `pulse`. In-button loading keeps **hover colors** by default (`styles` omitted or `"hover"`); use `"default"` or a `styles` object to override. **`position`**: `left` / `right` (inline next to content), **`prefix`** / **`suffix`** (pinned to slot — use `suffix` when replacing a chevron on full-width buttons). Use **`loading.hide`** to control per-slot visibility: `{ icon, title, subtitle, badge, prefix, suffix }` — `true` always hides, `false` never, `"auto"` (icon only) hides when on the same side as the indicator. Legacy `hideIcon` / `hideTitle` still work. - **Badge**: Use flat properties `showBadge`, `badgeTitle`, `badgeColor`, `badgeBackground` – NOT a nested `badge: {}` object. - **width/height**: Set directly, e.g. `width: "30px"`, `height: "30px"` for icon buttons. - **borderRadius**: `"50%"` for round buttons, `"8px"` for rounded rectangles. # Interaktive Buttons ![](https://framerusercontent.com/images/ILV64ahEXL4mvLmCRBdcVHiMc.png) Das Button-Widget ist über ein Config-Objekt steuerbar: Label, Icon, Farben, Loading, Actions. In Ninox heißt der Einstieg `arcCustomButton({ ... })` — solo im Formelfeld oder eingebettet in Table, Layout, Kanban. Zusätzlich kannst du Aussehen und Verhalten des Buttons individuell gestalten: Farben, Schriftgrößen, Icons, Abstände, Ausrichtung und Hover-Effekte lassen sich per Konfigurationsobjekt steuern. Auch ein optionales Badge (z. B. zur Anzeige eines Counters) ist integrierbar. Das Widget eignet sich besonders für: - **interaktive Dashboards** (z. B. Schnellaktionen wie „neuen Auftrag anlegen“), - **Kundencockpits** (z. B. „PDF herunterladen“, „Support kontaktieren“), - **Workflow-Steuerung** (z. B. „Status ändern“, „Nächster Schritt“), - oder jede andere Stelle, an der du Aktionen visuell zugänglich machen willst. Wie bei allen arcWidgets erfolgt die Konfiguration direkt über einen Anwendungscode (JSON-Objekt) Formel-Feld solo, bzw. in einem anderen Widget integriert. ## Gesamter Anwendungscode ![](https://framerusercontent.com/images/hYjjWfJ7Ja9ldr3SBPThwgP3RiM.png) Folgend siehst du einen beispielhaften Anwendungscode für einen Button: ```javascript arcCustomButton({ uniqueId: "Button " + Nr, title: "Neuen Eintrag erstellen", width: "", height: "", alignY: "", alignX: "", paddingX: "", paddingY: "", gap: "5px", icon: arcCustomIcon({ name: "plus-bold", color: "#fff" }), iconPosition: "left", fontSize: "", fontColor: "#fff", backgroundColor: "#0062AA", borderColor: "#0062AA", borderRadius: "5px", showBadge: false, badgeTitle: "", badgeColor: "", badgeBackground: "", badgeBorderColor: "", badgePosition: "", badgeGap: "6px", hoverActions: { fontColor: "", iconColor: "", backgroundColor: "", borderColor: "", animation: "0.25s" }, actions: [{ type: "create", tableId: "", popup: true, changeFieldValues: [{ fieldId: "", value: "" }, { fieldId: "", value: "" }] }] }) ``` ## Erklärung einzelner Parameter Folgend wird dir aufgeschlüsselt, welche Parameter du verwenden kannst und was du jeweils eintragen musst. ### uniqueId (Pflichtfeld) Die `uniqueId` ist eine eindeutige Kennung des Buttons innerhalb der Seite. Sie wird intern für das Styling (CSS-Selektoren) und zur technischen Identifizierung des Buttons verwendet. **Typ:** `text` **Beispiel:** `"create-button-123"` oder `"delete " + Nr` Diese ID sollte: - **pro Komponente eindeutig** sein (z. B. durch Kombination aus Text und Datensatz-ID) - **keine Sonderzeichen oder Leerzeichen** enthalten *(werden intern automatisch durch Bindestriche ersetzt)* 💡** *****Tipp:***** **Wenn du mehrere Buttons auf einer Seite nutzt, z. B. in einer Liste oder Tabelle, empfiehlt sich ein Format wie `"edit-" + Nr` oder `"btn-" + Nr`. ```javascript uniqueId: Nr, uniqueId: "Button 1", // Text in " ``` ### title Der `title` bestimmt den sichtbaren Text innerhalb des Buttons. Er sollte kurz und eindeutig formuliert sein, damit Nutzer sofort erkennen, was der Button auslöst. **Typ:** `text` **Beispiel:** `"Neuen Eintrag erstellen"` oder `"PDF herunterladen"` 💡** *****Tipp:***** **Wenn du einen reinen Icon-Button erstellen möchtest (z. B. nur ein Plus-Symbol), kannst du `title` einfach auf `"` setzen. ```javascript title: "", ``` ### width Der Parameter `width` legt fest, wie breit der Button dargestellt wird. Wenn kein Wert angegeben wird, passt sich der Button automatisch dem Inhalt an. **Typ:** *text* (mit CSS-Einheit) **Beispiel:** `"120px"`, `"100%"` 💡 *Tipp:* Verwende `"100%"`, wenn der Button die gesamte verfügbare Breite des Containers ausfüllen soll – z. B. bei responsiven Layout oder in mobilen Ansichten. ```javascript width: "100%",// Prozent-Werte in " width: "100px",// Pixel-Werte in " ``` ### height Der Parameter `height` legt fest, wie hoch der Button dargestellt wird. Wenn kein Wert angegeben wird oder `"auto"` gesetzt ist, richtet sich die Höhe nach dem Inhalt und dem Padding. **Typ:** *text* (mit CSS-Einheit) **Beispiel:** `"40px"`, `"auto"` 💡 *Tipp:* Eine feste Höhe kann hilfreich sein, wenn du mehrere Buttons nebeneinander gleich hoch darstellen möchtest – z. B. in Toolbars oder Tabellen. ```javascript height: "100%", // Prozent-Werte in " height: "100px", // Pixel-Werte in " height: "", // default: auto ``` ### alignY ***alignY*** steuert die vertikale Ausrichtung des Buttons innerhalb seines Containers. Ohne Wert: zentriert. ```javascript alignY: "top", alignY: "center", // default alignY: "bottom", ``` ### alignX ***alignX*** steuert die horizontale Ausrichtung des Buttons innerhalb seines Containers. Ohne Wert: zentriert. ```javascript alignX: "left", alignX: "center", // default alignX: "right", ``` ### paddingX ***paddingX*** setzt den horizontalen Innenabstand (links/rechts) zum Text bzw. Icon. **Typ:** text (CSS-Einheit, z. B. `px`, `em`, `%`). ```javascript paddingX: "10px", paddingX: "1em", ``` Tipp: Mit `paddingX` kannst du die Buttonbreite optisch beeinflussen — unabhängig von `width`. ### paddingY ***paddingY*** setzt den vertikalen Innenabstand (oben/unten). **Typ:** text (CSS-Einheit). ```javascript paddingY: "6px", paddingY: "0.5em", ``` Tipp: `paddingY` sorgt für angenehme Klickflächen, besonders auf Touch-Geräten. ### gap Der Parameter `gap` legt den Abstand zwischen Text und Icon im Inneren des Buttons fest. Wenn kein Wert gesetzt ist, beträgt der Standardabstand `5px`. **Typ:** *text* (mit CSS-Einheit, z. B. `px`, `em`) **Beispiel:** `"4px"`, `"0.5em"`, `"8px"` 💡 *Tipp:* Wenn du Buttons mit nur Text oder nur Icon verwendest, kannst du `gap` auch weglassen – relevant wird er nur, wenn beide Elemente vorhanden sind. ```javascript gap: "10px", // z.B. Pixel-Werte gap: "", // default: 5px ``` ### prefix Der Parameter `prefix` fügt einen freien Content-Slot **vor** dem Hauptinhalt (Icon + Text) ein. Er akzeptiert alle Mini-Widgets (`arcCustomIcon`, `arcCustomBadge`, `arcCustomLayout`, Plain-Text). **Typ:** *Mini-Widget oder Text* **Beispiel:** ```javascript prefix: arcCustomIcon({ name: "calendar", color: "#888", size: "14px" }), prefix: arcCustomBadge({ value: "Neu", backgroundColor: "#34C759", fontColor: "#fff", borderColor: "#34C759" }), prefix: "", // default: kein Prefix ``` 💡 *Tipp:* Für Buttons mit `width: "100%"` ist `prefix` ideal für ein Icon oder Badge auf der linken Seite, während `suffix` den rechten Rand besetzt. ### suffix Der Parameter `suffix` fügt einen freien Content-Slot **nach** dem Hauptinhalt ein. Er wird automatisch an den **rechten Rand** gedrückt, sobald der Button eine explizite Breite hat (z. B. `width: "100%"`). Er akzeptiert alle Mini-Widgets. **Typ:** *Mini-Widget oder Text* **Beispiel:** ```javascript suffix: arcCustomIcon({ name: "chevron-right", color: "#fff", size: "14px" }), suffix: arcCustomBadge({ value: "Aktuell", backgroundColor: "#34C759", fontColor: "#fff", borderColor: "#34C759" }), suffix: "", // default: kein Suffix ``` 💡 *Tipp:* Klassischer Anwendungsfall — Listenelement-Button mit Text links und Chevron-Icon rechts: ```javascript arcCustomButton({ uniqueId: "period-btn-" + Nr, width: "100%", title: "04.05.2026", suffix: arcCustomIcon({ name: "chevron-right", color: "#fff", size: "14px" }), backgroundColor: "#333", fontColor: "#fff", actions: [...] }) ``` ### icon Der Parameter `icon` ermöglicht es, dem Button ein beliebiges Icon hinzuzufügen. Das Icon wird mit Hilfe des Widgets [arcCustomIcon](/documentation/custom-icon) erzeugt und kann frei gestaltet werden (Name, Farbe, Größe etc.). Über dein [User Cockpit](https://www.arc-rider.de/documentation/user-cockpit) kannst du deine Wunsch Icons auswählen, die du verwenden möchtest. **Typ:** *Widget* (`arcCustomIcon`) **Beispiel:** ```javascript icon: arcCustomIcon({ name: "plus-bold", color: "#fff" }), // arcCustomIcon einsetzen, um Icon einzufügen. icon: "", // default: Kein Icon wird ausgegeben. ``` 💡 *Tipp:* Du kannst den Button auch ohne Text verwenden (`title: "`) und nur über ein Icon darstellen – ideal für kompakte Toolbars oder Symbolleisten. **title**, **subtitle** und **badgeTitle** (mit `showBadge: true`) können ebenfalls Mini-Widgets sein – z. B. `badgeTitle: arcCustomBadge({ value: "3", ... })` – nicht nur Plain-Text. ### iconPosition Der Parameter `iconPosition` legt fest, wo das Icon im Verhältnis zum Text angezeigt wird. Wenn kein Wert gesetzt ist, wird das Icon links vom Text dargestellt. **Typ:** text **Beispiel: ** ```javascript iconPosition: "left", // Icon erscheint links vom Text. iconPosition: "right", // Icon erscheint rechts vom Text. iconPosition: "top", // Icon erscheint oben vom Text. iconPosition: "buttom", // Icon erscheint unten vom Text. iconPosition: "", // default: links ``` ### fontSize Der Parameter `fontSize` legt die Schriftgröße des Button-Texts fest. Wird kein Wert gesetzt, beträgt die Standardgröße `13px`. **Typ:** *text* (mit CSS-Einheit) **Beispiel:** `"14px"` 💡 *Tipp:* Wenn du Buttons in unterschiedlichen Kontexten einsetzt (z. B. in Karten oder Listen), kannst du mit `fontSize` eine visuelle Hierarchie erzeugen – z. B. kleinere Buttons für Nebenaktionen, größere für zentrale Funktionen. ```javascript fontSize: "18px",// Pixel-Werte in " ``` ### fontColor Der Parameter `fontColor` legt die Farbe des Button-Texts fest. Wenn kein Wert gesetzt ist, wird standardmäßig ein dunkles Grau (`#515971`) verwendet. **Typ:** *text* (Hex-Code oder CSS-Farbname) **Beispiel:** `"#ffffff"`, `"black"`, `"rgb(255, 0, 0)"` 💡 *Tipp:* Achte auf ausreichenden Kontrast zur Hintergrundfarbe (`backgroundColor`), um Lesbarkeit und Barrierefreiheit sicherzustellen – besonders bei farbigen Buttons oder dunklen Themes. ```javascript fontColor: "#ffffff", // HEX-Wert in " ``` ### backgroundColor Der Parameter `backgroundColor` definiert die Hintergrundfarbe des Buttons. Wenn kein Wert angegeben wird, verwendet das Widget standardmäßig ein helles Grau (`#F7F8FC`). **Typ:** *text* (Hex-Code oder CSS-Farbname) **Beispiel:** `"#0062AA"`, `"white"`, `"rgba(0, 0, 0, 0.1)"` 💡 *Tipp:* Für eine klare visuelle Rückmeldung bei Aktionen empfiehlt sich ein kräftiger Farbton – ideal in Kombination mit weißer Schrift (`fontColor: "#ffffff"`). ```javascript backgroundColor: "#4970ff", // HEX-Wert in " ``` ### borderColor Der Parameter `borderColor` legt die Rahmenfarbe des Buttons fest. Wenn kein Wert angegeben wird, wird standardmäßig ein dezentes Hellgrau verwendet (`#E9ECF4`). **Typ:** *text* (Hex-Code oder CSS-Farbname) **Beispiel:** `"#0062AA"`, `"transparent"`, `"rgba(0, 0, 0, 0.2)"` 💡 *Tipp:* Wenn du einen vollständig flächigen Button ohne sichtbaren Rahmen gestalten möchtest, kannst du `borderColor` auf `"transparent"` setzen – oder passend zur Hintergrundfarbe. ```javascript borderColor: "#EEEEEE", // HEX-Wert in " ``` ### borderRadius Der Parameter `borderRadius` bestimmt, wie stark die Ecken des Buttons abgerundet sind. Wird kein Wert gesetzt, verwendet das Widget standardmäßig `3px`. **Typ:** *text* (mit CSS-Einheit) **Beispiel:** `"0px"` (rechteckig), `"5px"` (leicht abgerundet), `"50px"` (pillenförmig) 💡 *Tipp:* Für moderne UI-Elemente eignen sich leicht abgerundete Ecken (`4–8px`). ```javascript borderRadius: "50px", // 50px = ganz abgerundet // default Wert sind: 3px ``` ### embedded Der Parameter `embedded` steuert, ob das Widget innerhalb eines anderen Widgets (z. B. in einer Karte oder einem Layout-Container) eingebettet ist. **Typ:** *boolean* (`true` / `false`) **Standardwert:** `false` ```javascript embedded: false, // ermöglicht den Standalone Einsatz des Widgets (nicht eingebettet in anderen Widgets) embedded: true, // Fallback ``` ### Badge Mit dem Badge-System kannst du dem Button eine kleine Zusatzmarkierung hinzufügen – z. B. einen Zähler, ein Label („Neu“, „!“, „3“), oder ein Symbol für besondere Hinweise. Der Badge wird optisch abgesetzt dargestellt und lässt sich farblich sowie in der Position anpassen.
Parameter Typ Beschreibung
`showBadge` *boolean* Legt fest, ob ein Badge angezeigt wird (`true` / `false`). Kann auch per Skript bestimmt werden: `if cnt(Dokumente)>0 then true end `
`badgeTitle` *text* Text oder Zahl im Badge
`badgeColor` *text* Textfarbe im Badge (z. B. `"#ffffff"`)
`badgeBackground` *text* Hintergrundfarbe des Badges (z. B. `"#e9595d"`)
`badgeBorderColor` *text* Rahmenfarbe des Badges (z. B. `"#ffffff"`)
`badgePosition` *text* Zusätzliche Positionierung des Badges *(z. B. *`*"outside"*`* für Darstellung außerhalb des Buttons)*
`badgeGap` *text* Abstand zwischen Button-Inhalt und Badge in der Standard-Position (Standard: `"6px"`). Wird bei `badgePosition: "outside"` nicht angewendet.
### hoverActions Mit `hoverActions` kannst du definieren, wie sich der Button visuell verhält, wenn der Nutzer mit der Maus darüberfährt („Hover-Effekt“). Du kannst damit gezielt Farbe, Rahmen und Animation verändern – sowohl für den Button selbst als auch das Icon. ```javascript hoverActions: { fontColor: "#ffffff", iconColor: "#ffffff", backgroundColor: "#0062AA", borderColor: "#0062AA", animation: "0.1s" }, ``` 💡 *Tipp:* Wenn du gar keine Hover-Effekte brauchst, kannst du `hoverActions` komplett weglassen. Wenn dein Button eine **helle Hintergrundfarbe** hat, empfiehlt sich beim Hover ein **dunklerer Farbton** mit **weißem Text**, um Interaktivität und Kontrast zu erhöhen. So wird die Aktion visuell klar erkennbar und barriereärmer.
Feld Typ Beschreibung Beispielwert
`fontColor` *text* Textfarbe beim Hover `"#ffffff"`
`backgroundColor` *text* Hintergrundfarbe beim Hover `"#004C8A"`
`borderColor` *text* Rahmenfarbe beim Hover `"#003366"`
`iconColor` *text* Farbe des Icons beim Hover (nur bei SVG-Icons relevant) `"#ffffff"`
`animation` *text* CSS-Transition-Zeit für sanfte Effekte `"0.25s"`
### actions ✅ Mit dem Parameter `actions` definierst du, was passieren soll, wenn der Button geklickt wird. Du kannst eine oder mehrere Aktionen hinterlegen – von einfachem URL-Öffnen über das Erstellen neuer Datensätze bis hin zu benutzerdefiniertem JavaScript. **State-Bags:** Für partielle Updates in JSON-Töpfen (`type: "update"`) siehe [`data-bag-actions`](../patterns/data-bag-actions.md) (`dataBag` + `patch` statt gesamtes Feld überschreiben). **Polling:** Wiederholtes `update` (z. B. Trigger-Feld) mit `polling: { intervalMs, maxAttempts, active }` — siehe [`action-polling`](../patterns/action-polling.md). Passt zu Loading/`sequences.when`. **Typ:** *Liste von Objekten* (Array mit mindestens einem Action-Objekt) **Aufbau einer Action: **Jede Action ist ein Objekt mit mindestens dem Feld `type` – je nach Typ sind weitere Felder erforderlich oder optional. #### Unterstützte Aktionstypen
`type` Beschreibung Code-Beispiel
`"openUrl"` Öffnet einen externen Link in einem neuen Tab ```javascript {type: "openUrl", value: "https://example.com" } ```
`"openRecord"` Öffnet einen bestehenden Datensatz im aktuellen Fenster ```javascript { type: "openRecord", recordId: first(select Dashboard).Nr} ```
`"openFullscreen"` Öffnet einen Datensatz im Vollbild-Modus ```javascript { type: "openFullscreen", recordId: Nr, tab: "Details" } ```
`"popup"` Öffnet einen Datensatz als Popup ```javascript { type: "popup", recordId: Nr, tab: "Info" } ```
`"closeRecord"` Schließt den aktuellen Datensatz ```javascript { type: "closeRecord", recordId: Nr } ```
`"closeFullscreen"` Schließt den Vollbildmodus ```javascript { type: "closeFullscreen", recordId: Nr } ```
`"closeAllRecords"` Schließt alle geöffneten Datensätze ```javascript { type: "closeAllRecords" } ```
`"delete"` Löscht den angegebenen Datensatz ```javascript { type: "delete", recordId: Nr } ```
`"update"` Aktualisiert ein Feld in einem Datensatz ```javascript { type: "update", recordId: Auftrag.Nr, field: "A1", value: "Erledigt" } ```
`"create"` Erstellt einen neuen Datensatz mit optionalem Popup ```javascript { type: "create", tableId: "A", popup: true, changeFieldValues: [{ fieldId: "AZ", value: "Neu" }] } ```
`"customJS"` Führt benutzerdefinierten JavaScript-Code aus ```javascript { type: "customJS", value: "alert('Hallo Welt!');" } ```
💡 *Tipp:* Du kannst mehrere Aktionen hintereinander ausführen, indem du sie im Array kombinierst. Die Ausführung erfolgt in der Reihenfolge, in der sie im Array angegeben sind. ### loading Zeigt nach dem Klick einen Ladezustand an – **im Button** oder als **Overlay** (Backdrop über der ganzen App). ```javascript loading: { show: true, hideOnFinish: true, minDuration: 300, indicator: { type: "dots", color: "#fff", width: "60px", height: "12px" }, position: "right", hideTitle: false, hideIcon: false, overlay: { /* siehe unten */ } } ``` | Property | Typ | Default | Beschreibung | |----------|-----|---------|--------------| | `show` | boolean | — | Loading aktivieren | | `hideOnFinish` | boolean | `true` | Nach Ende der Aktionen ausblenden | | `minDuration` | number (ms) | `0` | Mindest-Anzeigedauer (verhindert Flackern) | | `indicator` | object | siehe unten | Erscheinungsbild des Lade-Indikators | | `position` | text | `right` | In-Button only: `left` / `right` (inline next to content), **`prefix`** / **`suffix`** (pinned to slot — `suffix` replaces chevron on full-width buttons; pair with `hide.suffix: true`) | | `hide` | object | — | Slots beim In-Button-Loading ausblenden – siehe Tabelle unten | | `hideTitle` | boolean | `false` | *Legacy* – blendet den gesamten Inhaltsbereich aus. Bevorzuge `hide.title` | | `hideIcon` | boolean | `false` | *Legacy* – blendet Icon aus wenn auf Indicator-Seite. Bevorzuge `hide.icon: "auto"` | | `styles` | text / object | `hover`* | Nur In-Button: welche Farben während Loading gelten (*Default: `hoverActions`, sonst Normal) | | `overlay` | boolean / object | — | gesetzt = Overlay-Modus; fehlt = In-Button | #### hide (In-Button-Loading) Mit `loading.hide` kannst du einzelne Slots des Buttons während des Ladevorgangs ausblenden. | Slot | Typ | Beschreibung | |------|-----|--------------| | `icon` | boolean / `"auto"` | `true` = immer ausblenden · `false` = nie · `"auto"` = nur wenn Icon auf Indicator-Seite liegt (Legacy-Verhalten von `hideIcon`) | | `title` | boolean | Text des Buttons ausblenden | | `subtitle` | boolean | Untertitel ausblenden | | `badge` | boolean | Badge ausblenden | | `prefix` | boolean | Prefix-Slot ausblenden | | `suffix` | boolean | Suffix-Slot ausblenden | ```javascript loading: { show: true, position: "suffix", hide: { suffix: true, }, indicator: { type: "spinner", color: "#fff", width: "18px", height: "18px" } } ``` 💡 *Tipp:* Bei `width: "100%"` mit Chevron rechts: `suffix` + `loading.position: "suffix"` + `hide.suffix: true` — der Spinner sitzt dann exakt an der Stelle des Chevrons, der Text bleibt links. #### styles (In-Button-Loading) Während des Ladens bleibt der Button nicht auf `:hover` (Mausbewegung/`pointer-events`), sondern bekommt **fest eingefrorene** Farben – damit Indikator-Farbe zum sichtbaren Zustand passt. | Wert | Verhalten | |------|-----------| | *(weglassen)* oder `"hover"` | `hoverActions` als feste Button-Farben (wenn gesetzt) | | `"default"` | Normale Button-Farben (`backgroundColor`, `fontColor`, …) | | `{ backgroundColor, fontColor, borderColor, iconColor }` | Eigene Loading-Farben (gleiche Felder wie `hoverActions`) | ```javascript loading: { show: true, styles: "hover", indicator: { type: "dots", color: "#fff" } } ``` #### indicator Gilt für In-Button und als Basis für Overlay. Kann in `overlay.indicator` und pro Eintrag in `sequences[].indicator` überschrieben werden. | Feld | Typ | Default | Beschreibung | |------|-----|---------|--------------| | `type` | text | `dots` | `dots` (drei Punkte), `spinner` (Kreis), `pulse` (pulsierender Punkt) | | `color` | text | `#4970ff` | Farbe | | `width` | text | `60px` (Button) / `60px` (Overlay) | CSS-Breite | | `height` | text | `12px` (Button) / `14px` (Overlay) | CSS-Höhe | #### overlay (zusätzlich) Steuert die **weiße Dialog-Karte** (nicht den Indikator). | Feld | Typ | Default | Beschreibung | |------|-----|---------|--------------| | `width` | text | `300px` | Breite der Overlay-Karte | | `height` | text | `280px` | Höhe der Overlay-Karte | | `backgroundColor` | text | `#ffffff` | Hintergrundfarbe der Overlay-Karte | | `indicator` | object | von `loading.indicator` | Optional anderer Indikator im Overlay | | `title` | text / object | — | Überschrift (Plain-Text oder `{ value, styles }`) | | `message` | text / object | — | Beschreibung (Plain-Text oder `{ value, styles }`) | **`title` und `message`** — wie bei Layout-Blocks: Plain-String oder Objekt mit optionalem CSS (`styles` überschreibt die Default-Klassen `.arc_loading_title` / `.arc_loading_message`): ```javascript overlay: { title: { value: "Wird gespeichert…", styles: "font-size: 18px; font-weight: 700; color: #0f766e;" }, message: { value: "Bitte warten.", styles: "font-size: 13px; color: #64748b;" } } ``` `value` kann auch ein Mini-Widget sein (z. B. `arcCustomText({...})`), nicht nur Text. #### Overlay-Modi | `overlay` | Verhalten | |-----------|-----------| | `true` | Nur Backdrop + Spinner | | `{ title, message }` | Feste Meldung (Dialog wie Bestätigung, ohne Buttons) | | `{ sequences: [...] }` | Dynamische Meldungen (Zeit +/oder Ninox-Booleans) | #### sequences – kombinierte Timeline Jede Sequence kann **Zeit** (`after`), **Bedingung** (`when`) oder **beides** haben: ```javascript overlay: { width: "320px", backgroundColor: "#f8fafc", title: "Einen Moment…", indicator: { type: "spinner", color: "#3e6fff", width: "32px", height: "32px" }, sequences: [ { after: 0, title: "Starte…", priority: 5 }, { after: 800, title: "Verarbeite…", priority: 5 }, { when: helper_uploading, title: "Upload läuft…", priority: 40, indicator: { type: "pulse" } }, { after: 3000, when: helper_stillBusy, message: "Noch einen Moment…", priority: 15 } ] } ``` | Feld (pro Sequence) | Typ | Beschreibung | |------------------|-----|--------------| | `after` | number (ms) | optional; ab Overlay-Start | | `when` | boolean | optional; Ninox-Feld, bei jedem Reload neu | | `priority` | number | optional; höher = wichtiger bei Überlappung | | `title` / `message` | text / object | mindestens eines erforderlich; optional `{ value, styles }` | | `indicator` | object | optional; vollständiges `indicator`-Objekt für diese Sequence (ersetzt/ergänzt `type`, `color`, `width`, `height`) | **Resolver:** Eine Sequence ist aktiv, wenn Zeit- und Boolean-Bedingung erfüllt sind. Bei mehreren aktiven Sequences gewinnt die höchste `priority`. **Ninox-Tipp für `when`:** Hilfsfelder (boolean) per Trigger setzen, damit die Formel während langer Aktionen neu berechnet wird und der Overlay-Text wechselt. ### Fazit Das `arcCustomButton`-Widget ist ein vielseitiges Werkzeug, um Aktionen in deiner Ninox-App sichtbar und interaktiv zu machen – egal ob du einfache Links, komplexe Workflows oder optisch ansprechende Dashboards gestalten möchtest. Dank der flexiblen Gestaltungsmöglichkeiten und des leistungsstarken Aktionssystems lässt sich der Button nahtlos in bestehende Layouts und Komponenten integrieren. 💡 *Pro-Tipp:* In Kombination mit anderen arcWidgets – z. B. `arcCustomText` oder `arcCustomCard` – kannst du komplette UI-Bausteine entwickeln, die sowohl funktional als auch visuell überzeugen. # Button ## AI Defaults (read first) - **uniqueId**: Required, e.g. `"btn-save-" + Nr`. - **title**: Button label (use `""` for icon-only buttons). - **actions**: Array of `{ type: "update", recordId: Nr, field: fieldId(Nr, "Feld"), value: ... }`. Prefer `type: "update"` over `customJS`. - **Icon-only button**: Use `arcCustomButton` with `icon: arcCustomIcon({...})`, NOT a layout wrapper with `clickAction`. - **Nested widgets**: `icon`, `title`, `subtitle`, and badge text (`badgeTitle` with `showBadge: true`) may use `arcCustomIcon`, `arcCustomBadge`, or other mini-widgets – not only strings. - **prefix / suffix**: Mini-widget slots rendered before (`prefix`) and after (`suffix`) the main content. `suffix` is automatically pushed to the right edge when the button has an explicit `width` (e.g. `"100%"`). Both accept any mini-widget (`arcCustomIcon`, `arcCustomBadge`, `arcCustomLayout`, plain text). - **loading**: After click, `{ show: true, indicator: { type, color, width, height }, hideOnFinish, minDuration, position, hideTitle, hideIcon, styles }`; set **`overlay`** (`true`, `{ title, message }`, or `{ sequences: [...] }`) for fullscreen overlay. Types: `dots`, `spinner`, `pulse`. In-button loading keeps **hover colors** by default (`styles` omitted or `"hover"`); use `"default"` or a `styles` object to override. **`position`**: `left` / `right` (inline next to content), **`prefix`** / **`suffix`** (pinned to slot — use `suffix` when replacing a chevron on full-width buttons). Use **`loading.hide`** to control per-slot visibility: `{ icon, title, subtitle, badge, prefix, suffix }` — `true` always hides, `false` never, `"auto"` (icon only) hides when on the same side as the indicator. Legacy `hideIcon` / `hideTitle` still work. - **Badge**: Use flat properties `showBadge`, `badgeTitle`, `badgeColor`, `badgeBackground` – NOT a nested `badge: {}` object. - **width/height**: Set directly, e.g. `width: "30px"`, `height: "30px"` for icon buttons. - **borderRadius**: `"50%"` for round buttons, `"8px"` for rounded rectangles. # Interaktive Buttons ![](https://framerusercontent.com/images/ILV64ahEXL4mvLmCRBdcVHiMc.png) Das Button-Widget ist über ein Config-Objekt steuerbar: Label, Icon, Farben, Loading, Actions. Zusätzlich kannst du Aussehen und Verhalten des Buttons individuell gestalten: Farben, Schriftgrößen, Icons, Abstände, Ausrichtung und Hover-Effekte lassen sich per Konfigurationsobjekt steuern. Auch ein optionales Badge (z. B. zur Anzeige eines Counters) ist integrierbar. Das Widget eignet sich besonders für: - **interaktive Dashboards** (z. B. Schnellaktionen wie „neuen Auftrag anlegen“), - **Kundencockpits** (z. B. „PDF herunterladen“, „Support kontaktieren“), - **Workflow-Steuerung** (z. B. „Status ändern“, „Nächster Schritt“), - oder jede andere Stelle, an der du Aktionen visuell zugänglich machen willst. Wie bei allen arcWidgets erfolgt die Konfiguration direkt über einen Anwendungscode (JSON-Objekt) Formel-Feld solo, bzw. in einem anderen Widget integriert. ## Gesamter Anwendungscode ![](https://framerusercontent.com/images/hYjjWfJ7Ja9ldr3SBPThwgP3RiM.png) Folgend siehst du einen beispielhaften Anwendungscode für einen Button: ```javascript arcCustomButton({ uniqueId: "Button " + Nr, title: "Neuen Eintrag erstellen", width: "", height: "", alignY: "", alignX: "", paddingX: "", paddingY: "", gap: "5px", icon: arcCustomIcon({ name: "plus-bold", color: "#fff" }), iconPosition: "left", fontSize: "", fontColor: "#fff", backgroundColor: "#0062AA", borderColor: "#0062AA", borderRadius: "5px", showBadge: false, badgeTitle: "", badgeColor: "", badgeBackground: "", badgeBorderColor: "", badgePosition: "", badgeGap: "6px", hoverActions: { fontColor: "", iconColor: "", backgroundColor: "", borderColor: "", animation: "0.25s" }, actions: [{ type: "create", tableId: "", popup: true, changeFieldValues: [{ fieldId: "", value: "" }, { fieldId: "", value: "" }] }] }) ``` ## Erklärung einzelner Parameter Folgend wird dir aufgeschlüsselt, welche Parameter du verwenden kannst und was du jeweils eintragen musst. ### uniqueId (Pflichtfeld) Die `uniqueId` ist eine eindeutige Kennung des Buttons innerhalb der Seite. Sie wird intern für das Styling (CSS-Selektoren) und zur technischen Identifizierung des Buttons verwendet. **Typ:** `text` **Beispiel:** `"create-button-123"` oder `"delete " + Nr` Diese ID sollte: - **pro Komponente eindeutig** sein (z. B. durch Kombination aus Text und Datensatz-ID) - **keine Sonderzeichen oder Leerzeichen** enthalten *(werden intern automatisch durch Bindestriche ersetzt)* 💡** *****Tipp:***** **Wenn du mehrere Buttons auf einer Seite nutzt, z. B. in einer Liste oder Tabelle, empfiehlt sich ein Format wie `"edit-" + Nr` oder `"btn-" + Nr`. ```javascript uniqueId: Nr, uniqueId: "Button 1", // Text in " ``` ### title Der `title` bestimmt den sichtbaren Text innerhalb des Buttons. Er sollte kurz und eindeutig formuliert sein, damit Nutzer sofort erkennen, was der Button auslöst. **Typ:** `text` **Beispiel:** `"Neuen Eintrag erstellen"` oder `"PDF herunterladen"` 💡** *****Tipp:***** **Wenn du einen reinen Icon-Button erstellen möchtest (z. B. nur ein Plus-Symbol), kannst du `title` einfach auf `"` setzen. ```javascript title: "", ``` ### width Der Parameter `width` legt fest, wie breit der Button dargestellt wird. Wenn kein Wert angegeben wird, passt sich der Button automatisch dem Inhalt an. **Typ:** *text* (mit CSS-Einheit) **Beispiel:** `"120px"`, `"100%"` 💡 *Tipp:* Verwende `"100%"`, wenn der Button die gesamte verfügbare Breite des Containers ausfüllen soll – z. B. bei responsiven Layout oder in mobilen Ansichten. ```javascript width: "100%",// Prozent-Werte in " width: "100px",// Pixel-Werte in " ``` ### height Der Parameter `height` legt fest, wie hoch der Button dargestellt wird. Wenn kein Wert angegeben wird oder `"auto"` gesetzt ist, richtet sich die Höhe nach dem Inhalt und dem Padding. **Typ:** *text* (mit CSS-Einheit) **Beispiel:** `"40px"`, `"auto"` 💡 *Tipp:* Eine feste Höhe kann hilfreich sein, wenn du mehrere Buttons nebeneinander gleich hoch darstellen möchtest – z. B. in Toolbars oder Tabellen. ```javascript height: "100%", // Prozent-Werte in " height: "100px", // Pixel-Werte in " height: "", // default: auto ``` ### alignY ***alignY*** steuert die vertikale Ausrichtung des Buttons innerhalb seines Containers. Ohne Wert: zentriert. ```javascript alignY: "top", alignY: "center", // default alignY: "bottom", ``` ### alignX ***alignX*** steuert die horizontale Ausrichtung des Buttons innerhalb seines Containers. Ohne Wert: zentriert. ```javascript alignX: "left", alignX: "center", // default alignX: "right", ``` ### paddingX ***paddingX*** setzt den horizontalen Innenabstand (links/rechts) zum Text bzw. Icon. **Typ:** text (CSS-Einheit, z. B. `px`, `em`, `%`). ```javascript paddingX: "10px", paddingX: "1em", ``` Tipp: Mit `paddingX` kannst du die Buttonbreite optisch beeinflussen — unabhängig von `width`. ### paddingY ***paddingY*** setzt den vertikalen Innenabstand (oben/unten). **Typ:** text (CSS-Einheit). ```javascript paddingY: "6px", paddingY: "0.5em", ``` Tipp: `paddingY` sorgt für angenehme Klickflächen, besonders auf Touch-Geräten. ### gap Der Parameter `gap` legt den Abstand zwischen Text und Icon im Inneren des Buttons fest. Wenn kein Wert gesetzt ist, beträgt der Standardabstand `5px`. **Typ:** *text* (mit CSS-Einheit, z. B. `px`, `em`) **Beispiel:** `"4px"`, `"0.5em"`, `"8px"` 💡 *Tipp:* Wenn du Buttons mit nur Text oder nur Icon verwendest, kannst du `gap` auch weglassen – relevant wird er nur, wenn beide Elemente vorhanden sind. ```javascript gap: "10px", // z.B. Pixel-Werte gap: "", // default: 5px ``` ### prefix Der Parameter `prefix` fügt einen freien Content-Slot **vor** dem Hauptinhalt (Icon + Text) ein. Er akzeptiert alle Mini-Widgets (`arcCustomIcon`, `arcCustomBadge`, `arcCustomLayout`, Plain-Text). **Typ:** *Mini-Widget oder Text* **Beispiel:** ```javascript prefix: arcCustomIcon({ name: "calendar", color: "#888", size: "14px" }), prefix: arcCustomBadge({ value: "Neu", backgroundColor: "#34C759", fontColor: "#fff", borderColor: "#34C759" }), prefix: "", // default: kein Prefix ``` 💡 *Tipp:* Für Buttons mit `width: "100%"` ist `prefix` ideal für ein Icon oder Badge auf der linken Seite, während `suffix` den rechten Rand besetzt. ### suffix Der Parameter `suffix` fügt einen freien Content-Slot **nach** dem Hauptinhalt ein. Er wird automatisch an den **rechten Rand** gedrückt, sobald der Button eine explizite Breite hat (z. B. `width: "100%"`). Er akzeptiert alle Mini-Widgets. **Typ:** *Mini-Widget oder Text* **Beispiel:** ```javascript suffix: arcCustomIcon({ name: "chevron-right", color: "#fff", size: "14px" }), suffix: arcCustomBadge({ value: "Aktuell", backgroundColor: "#34C759", fontColor: "#fff", borderColor: "#34C759" }), suffix: "", // default: kein Suffix ``` 💡 *Tipp:* Klassischer Anwendungsfall — Listenelement-Button mit Text links und Chevron-Icon rechts: ```javascript arcCustomButton({ uniqueId: "period-btn-" + Nr, width: "100%", title: "04.05.2026", suffix: arcCustomIcon({ name: "chevron-right", color: "#fff", size: "14px" }), backgroundColor: "#333", fontColor: "#fff", actions: [...] }) ``` ### icon Der Parameter `icon` ermöglicht es, dem Button ein beliebiges Icon hinzuzufügen. Das Icon wird mit Hilfe des Widgets [arcCustomIcon](/documentation/custom-icon) erzeugt und kann frei gestaltet werden (Name, Farbe, Größe etc.). Über dein [User Cockpit](https://www.arc-rider.de/documentation/user-cockpit) kannst du deine Wunsch Icons auswählen, die du verwenden möchtest. **Typ:** *Widget* (`arcCustomIcon`) **Beispiel:** ```javascript icon: arcCustomIcon({ name: "plus-bold", color: "#fff" }), // arcCustomIcon einsetzen, um Icon einzufügen. icon: "", // default: Kein Icon wird ausgegeben. ``` 💡 *Tipp:* Du kannst den Button auch ohne Text verwenden (`title: "`) und nur über ein Icon darstellen – ideal für kompakte Toolbars oder Symbolleisten. **title**, **subtitle** und **badgeTitle** (mit `showBadge: true`) können ebenfalls Mini-Widgets sein – z. B. `badgeTitle: arcCustomBadge({ value: "3", ... })` – nicht nur Plain-Text. ### iconPosition Der Parameter `iconPosition` legt fest, wo das Icon im Verhältnis zum Text angezeigt wird. Wenn kein Wert gesetzt ist, wird das Icon links vom Text dargestellt. **Typ:** text **Beispiel: ** ```javascript iconPosition: "left", // Icon erscheint links vom Text. iconPosition: "right", // Icon erscheint rechts vom Text. iconPosition: "top", // Icon erscheint oben vom Text. iconPosition: "buttom", // Icon erscheint unten vom Text. iconPosition: "", // default: links ``` ### fontSize Der Parameter `fontSize` legt die Schriftgröße des Button-Texts fest. Wird kein Wert gesetzt, beträgt die Standardgröße `13px`. **Typ:** *text* (mit CSS-Einheit) **Beispiel:** `"14px"` 💡 *Tipp:* Wenn du Buttons in unterschiedlichen Kontexten einsetzt (z. B. in Karten oder Listen), kannst du mit `fontSize` eine visuelle Hierarchie erzeugen – z. B. kleinere Buttons für Nebenaktionen, größere für zentrale Funktionen. ```javascript fontSize: "18px",// Pixel-Werte in " ``` ### fontColor Der Parameter `fontColor` legt die Farbe des Button-Texts fest. Wenn kein Wert gesetzt ist, wird standardmäßig ein dunkles Grau (`#515971`) verwendet. **Typ:** *text* (Hex-Code oder CSS-Farbname) **Beispiel:** `"#ffffff"`, `"black"`, `"rgb(255, 0, 0)"` 💡 *Tipp:* Achte auf ausreichenden Kontrast zur Hintergrundfarbe (`backgroundColor`), um Lesbarkeit und Barrierefreiheit sicherzustellen – besonders bei farbigen Buttons oder dunklen Themes. ```javascript fontColor: "#ffffff", // HEX-Wert in " ``` ### backgroundColor Der Parameter `backgroundColor` definiert die Hintergrundfarbe des Buttons. Wenn kein Wert angegeben wird, verwendet das Widget standardmäßig ein helles Grau (`#F7F8FC`). **Typ:** *text* (Hex-Code oder CSS-Farbname) **Beispiel:** `"#0062AA"`, `"white"`, `"rgba(0, 0, 0, 0.1)"` 💡 *Tipp:* Für eine klare visuelle Rückmeldung bei Aktionen empfiehlt sich ein kräftiger Farbton – ideal in Kombination mit weißer Schrift (`fontColor: "#ffffff"`). ```javascript backgroundColor: "#4970ff", // HEX-Wert in " ``` ### borderColor Der Parameter `borderColor` legt die Rahmenfarbe des Buttons fest. Wenn kein Wert angegeben wird, wird standardmäßig ein dezentes Hellgrau verwendet (`#E9ECF4`). **Typ:** *text* (Hex-Code oder CSS-Farbname) **Beispiel:** `"#0062AA"`, `"transparent"`, `"rgba(0, 0, 0, 0.2)"` 💡 *Tipp:* Wenn du einen vollständig flächigen Button ohne sichtbaren Rahmen gestalten möchtest, kannst du `borderColor` auf `"transparent"` setzen – oder passend zur Hintergrundfarbe. ```javascript borderColor: "#EEEEEE", // HEX-Wert in " ``` ### borderRadius Der Parameter `borderRadius` bestimmt, wie stark die Ecken des Buttons abgerundet sind. Wird kein Wert gesetzt, verwendet das Widget standardmäßig `3px`. **Typ:** *text* (mit CSS-Einheit) **Beispiel:** `"0px"` (rechteckig), `"5px"` (leicht abgerundet), `"50px"` (pillenförmig) 💡 *Tipp:* Für moderne UI-Elemente eignen sich leicht abgerundete Ecken (`4–8px`). ```javascript borderRadius: "50px", // 50px = ganz abgerundet // default Wert sind: 3px ``` ### embedded Der Parameter `embedded` steuert, ob das Widget innerhalb eines anderen Widgets (z. B. in einer Karte oder einem Layout-Container) eingebettet ist. **Typ:** *boolean* (`true` / `false`) **Standardwert:** `false` ```javascript embedded: false, // ermöglicht den Standalone Einsatz des Widgets (nicht eingebettet in anderen Widgets) embedded: true, // Fallback ``` ### Badge Mit dem Badge-System kannst du dem Button eine kleine Zusatzmarkierung hinzufügen – z. B. einen Zähler, ein Label („Neu“, „!“, „3“), oder ein Symbol für besondere Hinweise. Der Badge wird optisch abgesetzt dargestellt und lässt sich farblich sowie in der Position anpassen.
Parameter Typ Beschreibung
`showBadge` *boolean* Legt fest, ob ein Badge angezeigt wird (`true` / `false`). Kann auch per Skript bestimmt werden: `if cnt(Dokumente)>0 then true end `
`badgeTitle` *text* Text oder Zahl im Badge
`badgeColor` *text* Textfarbe im Badge (z. B. `"#ffffff"`)
`badgeBackground` *text* Hintergrundfarbe des Badges (z. B. `"#e9595d"`)
`badgeBorderColor` *text* Rahmenfarbe des Badges (z. B. `"#ffffff"`)
`badgePosition` *text* Zusätzliche Positionierung des Badges *(z. B. *`*"outside"*`* für Darstellung außerhalb des Buttons)*
`badgeGap` *text* Abstand zwischen Button-Inhalt und Badge in der Standard-Position (Standard: `"6px"`). Wird bei `badgePosition: "outside"` nicht angewendet.
### hoverActions Mit `hoverActions` kannst du definieren, wie sich der Button visuell verhält, wenn der Nutzer mit der Maus darüberfährt („Hover-Effekt“). Du kannst damit gezielt Farbe, Rahmen und Animation verändern – sowohl für den Button selbst als auch das Icon. ```javascript hoverActions: { fontColor: "#ffffff", iconColor: "#ffffff", backgroundColor: "#0062AA", borderColor: "#0062AA", animation: "0.1s" }, ``` 💡 *Tipp:* Wenn du gar keine Hover-Effekte brauchst, kannst du `hoverActions` komplett weglassen. Wenn dein Button eine **helle Hintergrundfarbe** hat, empfiehlt sich beim Hover ein **dunklerer Farbton** mit **weißem Text**, um Interaktivität und Kontrast zu erhöhen. So wird die Aktion visuell klar erkennbar und barriereärmer.
Feld Typ Beschreibung Beispielwert
`fontColor` *text* Textfarbe beim Hover `"#ffffff"`
`backgroundColor` *text* Hintergrundfarbe beim Hover `"#004C8A"`
`borderColor` *text* Rahmenfarbe beim Hover `"#003366"`
`iconColor` *text* Farbe des Icons beim Hover (nur bei SVG-Icons relevant) `"#ffffff"`
`animation` *text* CSS-Transition-Zeit für sanfte Effekte `"0.25s"`
### actions ✅ Mit dem Parameter `actions` definierst du, was passieren soll, wenn der Button geklickt wird. Du kannst eine oder mehrere Aktionen hinterlegen – von einfachem URL-Öffnen über das Erstellen neuer Datensätze bis hin zu benutzerdefiniertem JavaScript. **State-Bags:** Für partielle Updates in JSON-Töpfen (`type: "update"`) siehe [`data-bag-actions`](../patterns/data-bag-actions.md) (`dataBag` + `patch` statt gesamtes Feld überschreiben). **Polling:** Wiederholtes `update` (z. B. Trigger-Feld) mit `polling: { intervalMs, maxAttempts, active }` — siehe [`action-polling`](../patterns/action-polling.md). Passt zu Loading/`sequences.when`. **Typ:** *Liste von Objekten* (Array mit mindestens einem Action-Objekt) **Aufbau einer Action: **Jede Action ist ein Objekt mit mindestens dem Feld `type` – je nach Typ sind weitere Felder erforderlich oder optional. #### Unterstützte Aktionstypen
`type` Beschreibung Code-Beispiel
`"openUrl"` Öffnet einen externen Link in einem neuen Tab ```javascript {type: "openUrl", value: "https://example.com" } ```
`"openRecord"` Öffnet einen bestehenden Datensatz im aktuellen Fenster ```javascript { type: "openRecord", recordId: first(select Dashboard).Nr} ```
`"openFullscreen"` Öffnet einen Datensatz im Vollbild-Modus ```javascript { type: "openFullscreen", recordId: Nr, tab: "Details" } ```
`"popup"` Öffnet einen Datensatz als Popup ```javascript { type: "popup", recordId: Nr, tab: "Info" } ```
`"closeRecord"` Schließt den aktuellen Datensatz ```javascript { type: "closeRecord", recordId: Nr } ```
`"closeFullscreen"` Schließt den Vollbildmodus ```javascript { type: "closeFullscreen", recordId: Nr } ```
`"closeAllRecords"` Schließt alle geöffneten Datensätze ```javascript { type: "closeAllRecords" } ```
`"delete"` Löscht den angegebenen Datensatz ```javascript { type: "delete", recordId: Nr } ```
`"update"` Aktualisiert ein Feld in einem Datensatz ```javascript { type: "update", recordId: Auftrag.Nr, field: "A1", value: "Erledigt" } ```
`"create"` Erstellt einen neuen Datensatz mit optionalem Popup ```javascript { type: "create", tableId: "A", popup: true, changeFieldValues: [{ fieldId: "AZ", value: "Neu" }] } ```
`"customJS"` Führt benutzerdefinierten JavaScript-Code aus ```javascript { type: "customJS", value: "alert('Hallo Welt!');" } ```
💡 *Tipp:* Du kannst mehrere Aktionen hintereinander ausführen, indem du sie im Array kombinierst. Die Ausführung erfolgt in der Reihenfolge, in der sie im Array angegeben sind. ### loading Zeigt nach dem Klick einen Ladezustand an – **im Button** oder als **Overlay** (Backdrop über der ganzen App). ```javascript loading: { show: true, hideOnFinish: true, minDuration: 300, indicator: { type: "dots", color: "#fff", width: "60px", height: "12px" }, position: "right", hideTitle: false, hideIcon: false, overlay: { /* siehe unten */ } } ``` | Property | Typ | Default | Beschreibung | |----------|-----|---------|--------------| | `show` | boolean | — | Loading aktivieren | | `hideOnFinish` | boolean | `true` | Nach Ende der Aktionen ausblenden | | `minDuration` | number (ms) | `0` | Mindest-Anzeigedauer (verhindert Flackern) | | `indicator` | object | siehe unten | Erscheinungsbild des Lade-Indikators | | `position` | text | `right` | In-Button only: `left` / `right` (inline next to content), **`prefix`** / **`suffix`** (pinned to slot — `suffix` replaces chevron on full-width buttons; pair with `hide.suffix: true`) | | `hide` | object | — | Slots beim In-Button-Loading ausblenden – siehe Tabelle unten | | `hideTitle` | boolean | `false` | *Legacy* – blendet den gesamten Inhaltsbereich aus. Bevorzuge `hide.title` | | `hideIcon` | boolean | `false` | *Legacy* – blendet Icon aus wenn auf Indicator-Seite. Bevorzuge `hide.icon: "auto"` | | `styles` | text / object | `hover`* | Nur In-Button: welche Farben während Loading gelten (*Default: `hoverActions`, sonst Normal) | | `overlay` | boolean / object | — | gesetzt = Overlay-Modus; fehlt = In-Button | #### hide (In-Button-Loading) Mit `loading.hide` kannst du einzelne Slots des Buttons während des Ladevorgangs ausblenden. | Slot | Typ | Beschreibung | |------|-----|--------------| | `icon` | boolean / `"auto"` | `true` = immer ausblenden · `false` = nie · `"auto"` = nur wenn Icon auf Indicator-Seite liegt (Legacy-Verhalten von `hideIcon`) | | `title` | boolean | Text des Buttons ausblenden | | `subtitle` | boolean | Untertitel ausblenden | | `badge` | boolean | Badge ausblenden | | `prefix` | boolean | Prefix-Slot ausblenden | | `suffix` | boolean | Suffix-Slot ausblenden | ```javascript loading: { show: true, position: "suffix", hide: { suffix: true, }, indicator: { type: "spinner", color: "#fff", width: "18px", height: "18px" } } ``` 💡 *Tipp:* Bei `width: "100%"` mit Chevron rechts: `suffix` + `loading.position: "suffix"` + `hide.suffix: true` — der Spinner sitzt dann exakt an der Stelle des Chevrons, der Text bleibt links. #### styles (In-Button-Loading) Während des Ladens bleibt der Button nicht auf `:hover` (Mausbewegung/`pointer-events`), sondern bekommt **fest eingefrorene** Farben – damit Indikator-Farbe zum sichtbaren Zustand passt. | Wert | Verhalten | |------|-----------| | *(weglassen)* oder `"hover"` | `hoverActions` als feste Button-Farben (wenn gesetzt) | | `"default"` | Normale Button-Farben (`backgroundColor`, `fontColor`, …) | | `{ backgroundColor, fontColor, borderColor, iconColor }` | Eigene Loading-Farben (gleiche Felder wie `hoverActions`) | ```javascript loading: { show: true, styles: "hover", indicator: { type: "dots", color: "#fff" } } ``` #### indicator Gilt für In-Button und als Basis für Overlay. Kann in `overlay.indicator` und pro Eintrag in `sequences[].indicator` überschrieben werden. | Feld | Typ | Default | Beschreibung | |------|-----|---------|--------------| | `type` | text | `dots` | `dots` (drei Punkte), `spinner` (Kreis), `pulse` (pulsierender Punkt) | | `color` | text | `#4970ff` | Farbe | | `width` | text | `60px` (Button) / `60px` (Overlay) | CSS-Breite | | `height` | text | `12px` (Button) / `14px` (Overlay) | CSS-Höhe | #### overlay (zusätzlich) Steuert die **weiße Dialog-Karte** (nicht den Indikator). | Feld | Typ | Default | Beschreibung | |------|-----|---------|--------------| | `width` | text | `300px` | Breite der Overlay-Karte | | `height` | text | `280px` | Höhe der Overlay-Karte | | `backgroundColor` | text | `#ffffff` | Hintergrundfarbe der Overlay-Karte | | `indicator` | object | von `loading.indicator` | Optional anderer Indikator im Overlay | | `title` | text / object | — | Überschrift (Plain-Text oder `{ value, styles }`) | | `message` | text / object | — | Beschreibung (Plain-Text oder `{ value, styles }`) | **`title` und `message`** — wie bei Layout-Blocks: Plain-String oder Objekt mit optionalem CSS (`styles` überschreibt die Default-Klassen `.arc_loading_title` / `.arc_loading_message`): ```javascript overlay: { title: { value: "Wird gespeichert…", styles: "font-size: 18px; font-weight: 700; color: #0f766e;" }, message: { value: "Bitte warten.", styles: "font-size: 13px; color: #64748b;" } } ``` `value` kann auch ein Mini-Widget sein (z. B. `arcCustomText({...})`), nicht nur Text. #### Overlay-Modi | `overlay` | Verhalten | |-----------|-----------| | `true` | Nur Backdrop + Spinner | | `{ title, message }` | Feste Meldung (Dialog wie Bestätigung, ohne Buttons) | | `{ sequences: [...] }` | Dynamische Meldungen (Zeit +/oder Ninox-Booleans) | #### sequences – kombinierte Timeline Jede Sequence kann **Zeit** (`after`), **Bedingung** (`when`) oder **beides** haben: ```javascript overlay: { width: "320px", backgroundColor: "#f8fafc", title: "Einen Moment…", indicator: { type: "spinner", color: "#3e6fff", width: "32px", height: "32px" }, sequences: [ { after: 0, title: "Starte…", priority: 5 }, { after: 800, title: "Verarbeite…", priority: 5 }, { when: helper_uploading, title: "Upload läuft…", priority: 40, indicator: { type: "pulse" } }, { after: 3000, when: helper_stillBusy, message: "Noch einen Moment…", priority: 15 } ] } ``` | Feld (pro Sequence) | Typ | Beschreibung | |------------------|-----|--------------| | `after` | number (ms) | optional; ab Overlay-Start | | `when` | boolean | optional; Ninox-Feld, bei jedem Reload neu | | `priority` | number | optional; höher = wichtiger bei Überlappung | | `title` / `message` | text / object | mindestens eines erforderlich; optional `{ value, styles }` | | `indicator` | object | optional; vollständiges `indicator`-Objekt für diese Sequence (ersetzt/ergänzt `type`, `color`, `width`, `height`) | **Resolver:** Eine Sequence ist aktiv, wenn Zeit- und Boolean-Bedingung erfüllt sind. Bei mehreren aktiven Sequences gewinnt die höchste `priority`. ## Vue Das `data`-Objekt wird als Prop an die Vue-Komponente übergeben. ```vue ``` --- # Cards Source: https://www.arc-rider.com/docs/mini-widgets/cards # Cards ## AI Defaults (read first) - **Function name**: `arcCustomCard`. - **uniqueId**: Required when using actions, e.g. `"card-" + Nr`. - **title**, **subtitle**, **value**: Main content properties. - **Use inside arcCustomLayout blocks**: Set `clickAction` on the block, not the card, for navigation. - **No `else "" end`**: Omit else branches for optional content. # Custom Cards Die Custom Cards kannst du entweder einzeln in Formel-Felder verwenden oder in komplexere Oberflächen mit[ Custom Layout ](/documentation/custom-layout)integrieren. Der Vorteil bei dem Layout ist der, dass du die Card innerhalb der Blocks klickbar machen kannst und so auf andere Tabellen, bzw. Datensätze verlinken kannst. ## Anwendungscode ```javascript let data := { uniqueId: "", width: "", height: "", borderRadius: "", icon: arcCustomIcon({ name: "", containerSize: "", borderSize: "", borderColor: "", iconSize: "", borderRadius: "", color: "", backgroundColor: "", }), borderColor: "", borderWidth: "", backgroundColor:", paddingY: "", paddingX: "", title: "", value: "" }; arcCustomCard(data) ``` ## Erklärung einzelner Parameter ***width*** gibt die Breite deiner Card an. ```javascript width: "100%", // 100% füllt die gesamte Breite des Formelfeldes aus. width: "200px", // Pixel-Wert definiert fixe Breite ``` ***height*** definiert die Höhe der Karte. ```javascript height: "100%", // 100% füllt die gesamte Höhe des Formelfeldes aus. height: "200px", // Pixel-Wert definiert fixe Höhe ``` ***borderRadius*** definiert die Abrundungen der Ecken. ```javascript borderRadius: "15px", ``` ***icon*** wird in der Karte oben links dargestellt. Dieses kannst du ausgeben, musst du aber nicht. Ein Icon kannst du mit dem [Mini-Widget customIcon](/documentation/custom-icon) definieren. Hier kannst du nur das Icon ausgeben lassen oder auch mit Hintergrundfarben arbeiten. ```javascript icon: arcCustomIcon({ name: "", containerSize: "", borderSize: "", borderColor: "", iconSize: "", borderRadius: "", color: "", backgroundColor: "" }) ``` ***title*** definiert den Titel und deiner Karte. ```javascript title: "Mitarbeiter", // Text in " title: Mitarbeiter.Name, // Ninox-Refrenz-Feld in Text Form ``` ***value*** definiert den Body deiner Karte. Hier kannst du entweder mit text arbeiten oder aber andere Mini-Widgets verwenden, wie etwa [eine Progress Bar](/documentation/progress-bar). ```javascript value: "Subtitel", // Text in " value: Mitarbeiter.Info // Ninox-Refrenz-Feld in Text Form value: arcCustomProgressBar({ uniqueId: "", width: "", fontSize: "", fontColor: "", backgroundColor: "", progressColor: "", valueTotal: "", valueProgress: "", valueText: "" }), // Andere Widgets oder Mini-Widgets einsetzen ``` ## Standard Werte als Fallback ![](https://framerusercontent.com/images/Qk8oDQFM0sGgNWLRGHjWMzkbw.png) ```javascript let data := { uniqueId: "", width: "", // Fallback: 100% height: "", // Fallback: auto borderRadius: "", // Fallback: 15px icon: "", title: "Titel", value: "Lorem Ipsum is simply dummy text Lorem Ipsum is simply dummy text" }; arcCustomCard(data) ``` --- # Chart Circle Source: https://www.arc-rider.com/docs/mini-widgets/chart-circle # Chart Circle ## AI Defaults (read first) - **Function name**: `arcCustomChartCircle`. - **value**: Percentage (0–100), as number. - **size**: Circle diameter, e.g. `"120px"`. - **strokeWidth**: Ring thickness, e.g. `"10"`. - **color**: Fill color for the progress arc. - **backgroundColor**: Background ring color. - **No uniqueId needed** for display-only charts. # arcCustomChartCircle ![](https://framerusercontent.com/images/n9PTr1k2xJeCKvO0ro0nydihE0s.png) Das Widget `arcCustomChartCircle` ermöglicht die Visualisierung von Kreisdiagrammen (Pie- und Donut-Charts) direkt in deiner Ninox-Datenbank. Ob als Übersicht über Lagerbestände, Verteilungen, Auslastungen oder Projektanteile – dieses Widget bringt deine Daten in ansprechender, interaktiver Form auf die Oberfläche. Dank flexibler Parameter lässt sich das Design des Charts präzise anpassen: Farben, Größen, Legenden, zentrale Beschriftung, Abstand zwischen Segmenten und vieles mehr. Du kannst mehrere Charts gleichzeitig darstellen – mit jeweils eigener Konfiguration, ohne dass sie sich gegenseitig beeinflussen. **Use Cases:** - Lagerkapazitäten & Auslastungen visualisieren - Projektanteile oder Auftragsverteilungen zeigen - Vergleich von Soll-/Ist-Werten in grafischer Form - Dynamische Dashboards mit mehreren Kreisdiagrammen aufbauen Das Widget funktioniert sowohl eingebettet in andere Komponenten als auch als eigenständiges Element. Dank der `uniqueId` bleibt jede Instanz klar unterscheidbar. ## uniqueId Die `uniqueId` ist ein Pflichtfeld und sorgt dafür, dass jedes Diagramm eindeutig auf deiner Oberfläche erkannt wird. Das ist besonders wichtig, wenn du mehrere `arcCustomChartCircle`-Widgets gleichzeitig anzeigst – nur so bleiben Layout und Farben getrennt und überschreiben sich nicht gegenseitig. 💡 Tipp: Verwende eine Kombination aus statischem Text und einem eindeutigen Feld (z. B. `Nr`), um die ID automatisch pro Datensatz zu erzeugen. ```javascript uniqueId: Nr, // Ninox-Feld uniqueId: "Aufgabe Beschreibung", // Text in " ``` ## embedded Der Parameter `embedded` steuert, ob das Widget innerhalb eines anderen Widgets eingebettet ist oder eigenständig auf der Seite steht. In der Regel musst du nichts ändern, denn `embedded` ist standardmäßig auf `true` gesetzt. ```javascript embedded: false // wenn das Widget NICHT eingebettet in andere Widgets ist embedded: true // default ``` ## width Mit `width` bestimmst du die Breite des äußeren Containers des Diagramms. Dieser Container umschließt das Donut- oder Kreisdiagramm sowie ggf. die Legende. Du kannst jede gültige CSS-Weite angeben: - Prozentangaben (`"100%"`) - feste Pixelwerte (`"300px"`) - automatische Größe (`"auto"`) Standardwert: Wenn kein Wert angegeben ist, wird automatisch `"100%"` verwendet. ```javascript width: "auto" // passt sich automatisch an width: "100%" // in px oder Prozent ``` ## height Mit `height` legst du die Höhe des Widget-Containers fest. Dieser Wert beeinflusst nicht nur die Fläche, in der das Diagramm gerendert wird, sondern auch die vertikale Ausrichtung (z. B. der Legende). Du kannst alle gängigen CSS-Höhenwerte verwenden: - `"auto"` → automatische Höhe basierend auf Inhalt (Standard) - `"300px"` → feste Höhe - `"100%"` → gesamte Höhe des Eltern-Elements Standardwert: Ohne Angabe wird `"auto"` gesetzt. ```javascript height: "auto" // passt sich automatisch an height: "200px" // in px oder Prozent ``` ⚠️ Wenn du `height` auf einen fixen Wert setzt, achte darauf, dass dein Chart (und ggf. die Legende) genug Platz zum Anzeigen hat. ## type Mit dem Parameter `type` bestimmst du, ob dein Diagramm als Donut oder als klassisches Kreisdiagramm (Pie Chart) dargestellt wird. Optionen: - `"donut"` → Ringförmige Darstellung mit optionaler Mittelbeschriftung - `"pie"` → Vollflächiges Kreisdiagramm ohne Loch in der Mitte ```javascript type: "donut" // stellt das Chart als Donut dar type: "pie" // stellt das Chart als Pie dar type: "" // default: pie ``` 💡 Hinweis: Nur bei `type: "donut"` werden die Parameter `thickness` und `center` berücksichtigt. Ein `pie`-Chart ignoriert diese Einstellungen und zeigt einen einfachen Kreis. ## size Der Parameter `size` definiert die Gesamtgröße des Charts (Breite und Höhe des SVG-Kreises in einem). Dies wirkt sich direkt auf die Darstellung des Kreises oder Donuts aus – unabhängig vom äußeren `width`-/`height`-Container. - Du kannst alle gängigen CSS-Größen verwenden (z. B. `"160px"` oder `"100%"`), wobei Pixelangaben empfohlen werden. - Standardwert: Wenn kein Wert angegeben ist, wird automatisch `"160px"` verwendet. ```javascript size: "160px" size: "" // default: 160px ``` ## thickness Der Parameter `thickness` definiert die Dicke des Rings bei einem Donut-Chart (`type: "donut"`). Je höher der Wert, desto schmaler das Loch in der Mitte. Dieser Parameter hat nur eine Wirkung bei `type: "donut"` – bei `type: "pie"` wird er ignoriert. - Einheit: in px angeben z. B. `"30px"` oder `"50px"` - Standardverhalten: Wenn kein Wert gesetzt wird, berechnet das Widget die Ringdicke automatisch auf Basis von `size`. ```javascript thickness: "30px" thickness: "" // default: 120px ``` ⚠️ Zu große Werte können den Ring visuell zu schmal oder sogar unsichtbar machen – ideal sind Werte zwischen 20–60 px. ## gap Mit `gap` bestimmst du den **Abstand zwischen den einzelnen Segmenten** im Chart – sowohl bei `pie` als auch bei `donut`. Ein größerer Wert erzeugt mehr visuelle Trennung zwischen den Bereichen. Der Abstand wird **in Pixeln** angegeben, intern aber auf den **Winkel** umgerechnet (abhängig vom Radius). - **Einheit: **Nur Zahlenwerte in px, z. B. `"2px"` oder `"8px"` - **Standardwert: **Wenn kein Wert gesetzt ist, beträgt der Abstand `0px`, also **kein sichtbarer Abstand**. ```javascript gap: "5px" gap: "" // default: 0px ``` 💡 **Tipp:** Ein kleiner Abstand (2–6px) kann helfen, Segmente besser unterscheidbar zu machen – besonders bei ähnlichen Farben. ## legend settings Mit der `legend`-Gruppe kannst du steuern, **ob und wie die Legende angezeigt wird**, und deren **Position, Schriftart, Farben und Abstände** festlegen. Die Legende zeigt kleine Farb-Dots und einen Text (je nach Einstellung `title`, `value` oder `percent`). ```javascript legend: { position: "right", // left, right, bottom, top // default: right label: "title", // Bezeichnung fontSize: "12px", // Schriftgröße fontWeight: "normal", // Schriftstärke fontColor: "#333", // Schriftfarbe gap: "6px" // Abstand zwischen den Legenden Items } ``` **Einzelne Einstellungen:**
Paramter **Beschreibung**
visible Zeigt die Legende an (`true`) oder blendet sie aus (`false`).
position Platzierung: `"left"`, `"right"`, `"top"` oder `"bottom"`
label Was soll in der Legende stehen? Ninox Felder referenzieren oder eigene Textbezeichnungen wählen.
fontSize Schriftgröße der Labels, z. B. `"12px"`
fontWeight Schriftstärke: `"normal"`, `"bold"`, `"500"` usw.
fontColor Farbe des Legendentextes, z. B. `"#333"`
gap Abstand zwischen den Einträgen, z. B. `"6px"`
## center Settings Mit der `center`-Gruppe kannst du einen Titel und Untertitel in der Mitte des Donut-Charts anzeigen lassen. Diese Anzeige ist nur bei `type: "donut"` sichtbar, da bei `pie` kein Platz in der Mitte vorhanden ist. Ideal für Werte wie Gesamtzahl, Prozentsumme, Statusangaben o. ä. **Einzelne Einstellungen:**
Paramter **Beschreibung**
title Haupttext in der Mitte (z. B. „Gesamt“)
subtitle Untertitel direkt darunter (z. B. eine Zahl oder Status)
fontSize Größe beider Texte (gilt für beide), z. B. `"18px"`
fontColor Farbe der Schrift, z. B. `"#444"`
```javascript center: { title: "Gesamt", // title im Center des Widgets subtitle: "2000", // subtitle im Center des Widgets fontSize: "18px", // Schriftgröße im Center des Widgets fontColor: "#444", // Schriffarbe im Center des Widgets }, ``` ## items Die items-Liste enthält alle **einzelnen Segmente deines Kreisdiagramms**. Jedes Item definiert: - **Titel** für die Legende - **Wert**, der die Segmentgröße bestimmt - **Farbe**, in der das Segment dargestellt wird (und der Dot in der Legende) Die items können entweder statisch, einzeln als items definiert werden oder du greifst auf eine dynamische Liste (Tabelle in Ninox) zurück, aus dem sich die Werte, die du angibst gezogen werden. ```javascript items: [{ title: "Belegung", // Beschriftung in der Legende value: sum(Lager.Belegung), // Wert, der die Größe des Segments bestimmt color: "#3e6fff", // Farbe, in der das Segment dargestellt werden soll (sowie der Dot in der Legende) }, { title: "Gesamtkapazität", value: sum (Lager.Frei+Lager.Belegung), color: "#d3d5de", }] ``` ```javascript items: list_Lager.[{ title: Lagername, value: sum_lagerbestand, fontSize: "12px", color: helper_hex_code, } ``` **🧠 Wichtig:** - Alle Werte zusammen ergeben die Gesamtfläche (`value` wird relativ zum Gesamtwert berechnet). - Farben sollten kontrastreich gewählt werden – vor allem, wenn kein `gap` gesetzt ist. - Die Reihenfolge der Items entspricht der Reihenfolge im Chart (beginnend bei 12 Uhr, im Uhrzeigersinn). --- # Check Box Source: https://www.arc-rider.com/docs/mini-widgets/check-box # Check Box ## AI Defaults (read first) - **Function name**: `arcCustomCheckBox`. - **uniqueId**: Required, e.g. `"checkbox-" + Nr`. - **recordId**: Required, use raw `Nr`. - **field**: Required, use `fieldId(Nr, "Feldname")` – must be a boolean/yes-no field. - **value**: Current boolean state from the field. # Check Box Die Check Box kannst du für die optische Darstellung von deinen Ja/Nein Ninox-Feldern nutzen. ![](https://framerusercontent.com/images/XLOC8UfDRKQjhqx744eb1JzUyzE.png) Um die Check Box darstellen zu können, fügst du den Anwendungscode in ein Ninox Formel-Feld ein und passt die Parameter nach deinen Wünschen an. Du kannst dieses Mini-Widget auch in andere Widgets, wie die [Custom Table](/custom-table) integrieren. Dies ist der gesamte Anwendungscode mit allen Parametern, die aktuell für die Check Box zur Verfügung stehen: ```javascript arcCheckBox({ uniqueId: "uniqueName", value: 'Ja / Nein', embedded: true, width: "", height: "", boxSize: "", borderRadius: "", strokeWidth: "", activeSettings: { backgroundColor: "", borderColor: "", color: "" }, inactiveSettings: { backgroundColor: "", borderColor: "", color: "" }, labelSettings: { title: "Erledigt", position: "", fontSize: "", fontColor: "" }, clickAction: { recordId: Nr, fieldId: "", value: if 'Ja / Nein' = true then null else true end } }) ``` ## Erklärung einzelner Parameter Folgend wird dir aufgeschlüsselt, welche Parameter du verwenden kannst und was du jeweils eintragen musst. ### uniqueId Die*** uniqueId*** vergibst du individuell, sie sollte einzigartig sein. Sinn dahinter ist: Wenn du mehrere Check Boxen mit unterschiedlichen Settings auf deiner Oberfläche erstellst, überschreiben sich die Styles nicht. ```javascript uniqueId: Nr, // Ninox-Feld uniqueId: "Checkbox 1", // Text in " ``` ### value Bei dem ***value*** gibst du den Namen deines Ninox-Ja/Nein-Feldes an. ```javascript value: 'Ja / Nein', // Ninox-Ja/Nein-Feld value: Erledigt, // Ninox-Ja/Nein-Feld, welches du Erledigt genannt hast ``` ### embedded ***embedded*** benutzt du, wenn du die Check Box **nicht***** ***innerhalb eines anderen Widgets einsetzt. Standardmäßig ist dies aber auf true gesetzt. ```javascript embedded: false, // ermöglicht den Standalone Einsatz des Widgets (nicht eingebettet in anderen Widgets) embedded: true, // wenn das CheckBox Widget in einem anderen Widget eingesetzt wird embedded: "", // default: false ``` ### width Mit ***width*** bestimmst du die Breite des gesamten Check Box Elements. ```javascript width: "100%", // Prozent-Werte in " width: "100px", // Pixel-Werte in " width: "", // default: auto ``` ### height Mit ***height*** bestimmst du die Höhe des gesamten Check Box Elements. ```javascript height: "100%", // Prozent-Werte in " height: "100px", // Pixel-Werte in " height: "", // default: auto ``` ### boxSize Mit ***boxSize*** bestimmst du, wie groß die Check Box sein soll. ![](https://framerusercontent.com/images/jAD8j5G4Qgu7GtaCjoiDP1bY9M.png) ```javascript boxSize: "40px", // Pixel-Werte in " boxSize: "", // default: 20px ``` ### borderRadius Mit ***borderRadius*** bestimmst du die Abrundung deiner Check Box. Du kannst px Werte und % Werte eintragen. ![](https://framerusercontent.com/images/wfBxaQXtgMGbuMqspjDQqv7ctyY.png) ```javascript borderRadius: "50px", // runde Check Box borderRadius: "3%", // eckige Check Box borderRadius: "", // default: 4px ``` ### strokeWidth Mit ***strokeWidth*** bestimmst du die Linienstärke für die Umrandung der Checkbox sowie für die Check-Linie des Icons. ![](https://framerusercontent.com/images/rj8oX1OsPhULE3w0y0g1IWQc0N4.png) ```javascript strokeWidth: "0.5px", // Pixel-Werte in " strokeWidth: "2px", // Pixel-Werte in " strokeWidth: "", // default: 1px ``` ### **activeSettings** Block Mit dem ***activeSettings Block*** kannst du die backgroundColor, borderColor der Check Box und color der Check-Linie bei der aktiven Checkbox einstellen. ![](https://framerusercontent.com/images/K8ruoQIAIdeaw7M5uBdQO25vPI.png) ```javascript activeSettings: { backgroundColor: "", borderColor: "", color: "" }, activeSettings: { backgroundColor: "", // default: #0066FF borderColor: "", // default: #0066FF color: "" // default: #FFF }, ``` ### **inactiveSettings** Block Mit dem ***inactiveSettings Block*** kannst du die backgroundColor, borderColor der Check Box und color der Check-Linie bei der inaktiven Checkbox einstellen. ![](https://framerusercontent.com/images/523K0a0Mt7UdM3HrTWvPKC9qRA.png) ```javascript inactiveSettings: { backgroundColor: "", borderColor: "", color: "" }, inactiveSettings: { backgroundColor: "", // default: #C8CCD4 borderColor: "", // default: #C8CCD4 color: "" // default: none }, ``` ### labelSettings Block Mit dem ***labelSettings Block*** kannst du deiner Check Box ein Label geben. Du kannst bei ***title ***den Text eingeben, der beim Label ausgegeben werden soll. Du kannst auch Ninox-Felder angeben oder Variablen definieren. Wichtig ist hier, dass Text ausgegeben wird. Du kannst außerdem mit ***position ***bestimmen, ob das Label "left" oder "right" sein soll, mit ***fontSize*** die Schriftgröße und mit ***color*** die Schriftfarbe des Labels einstellen. ![](https://framerusercontent.com/images/glD2dnfE8bprWD18aeK5wPt5nfw.png) ```javascript labelSettings: { title: "Erledigt", // "text" oder Ninox-Feld (wichtig ist dass hier Text ausgegeben wird) position: "right", fontSize: "17px", fontColor: "#0066FF" }, labelSettings: { title: "", // default: Gebt ihr keinen Titel ein, wird kein Label angezeigt. position: "", // default: right fontSize: "", // default: 13px fontColor: "" // default: #262f4d }, ``` ### clickAction Block Bei dem ***clickAction Block*** trägst du relevante Infos für die Click Aktion ein. Bei ***recordId ***gibst du die record ID des Ninox Records an. Bei der ***fieldId ***gibst du die Field ID des Ninox-Feldes an, auf das die Action angewendet wird. Im ***value*** definierst du, was mit dem Ninox-Feld passieren soll, wenn man die Check Box anklickt. ```javascript clickAction: { recordId: Nr, fieldId: "G", value: if 'Ja / Nein' = true then null else true end } ``` 💡 **Hinweis: **Um die Ninox Feld-ID bestimmter Felder herauszufinden verwendest am besten die nützliche [Funktion arcFieldFinder().](https://www.arc-rider.de/documentation/field-finder) ## Beispiel und Vorlage **Beispiel: Check Box in einer Spalte der **[**Custom Table**](/custom-table)**.** ![](https://framerusercontent.com/images/z36FQ2pPqaAf2qyFUZpe2sv44cc.png) **Vorlage: Anwendungs-Code mit dynamischem Label** Du kannst dir folgenden Code kopieren und mit kleinen Anpassungen, hast du dieses fertige Design mit dynamischem Label: ![](https://framerusercontent.com/images/A5ZtSy9EUovXWfWuVKvxO68oU.png) Die Stellen, wo du Werte ändern oder anpassen musst sind mit // markiert. Denk daran, diese Passagen aus deinem fertigen Code zu entfernen. 💡 **Tipp: **Wenn du z.B. kein Label bei deiner Check Box brauchst, kannst du den gesamten labelSettings Block aus deinem Anwendungs-Code löschen.** ** ```javascript arcCheckBox({ uniqueId: "styled_CheckBox", value: 'Ja / Nein Feld', // mit deinem Wert ersetzen width: "", height: "", boxSize: "", embedded: false, // true eingeben, wenn die Checkbox in einem anderen Widget liegt borderRadius: "", activeSettings: { backgroundColor: "#99CCA5", borderColor: "#239B56", color: "#239B56" }, inactiveSettings: { backgroundColor: "#FFBFBE", borderColor: "#FF7977", color: "" }, labelSettings: { title: if 'Ja / Nein Feld' = true then "erledigt" else "offen" end, // 'Ja / Nein Feld' mit deinem Wert ersetzen und ggf. den Text anpassen position: "right", fontSize: "", fontColor: "" }, clickAction: { recordId: Nr, fieldId: "J", // mit deinem Wert ersetzen value: if 'Ja / Nein Feld' = true then null else true end // 'Ja / Nein Feld' mit deinem Wert ersetzen oder ggf. alles mit deiner Variable austauschen } }) ``` --- # Clock Source: https://www.arc-rider.com/docs/mini-widgets/clock # Clock ## AI Defaults (read first) - **Function name**: `arcCustomClock`. - **uniqueId**: Required, e.g. `"clock-" + Nr`. - **Standalone widget**: Typically needs no `recordId` or `field`. # Clock ![](https://framerusercontent.com/images/IvCDlbWt9G420AliTTvz2wZ0JwU.png) ## Anwendungscode ```javascript arcCustomClock({ uniqueId: "", timezone: "Europe/Berlin" }) ``` ### timezone ***timezone*** definiert in welcher Zeitzone deine Zeit angezeigt werden soll. ```javascript timezone: "Europe/Berlin", timezone: "", // Fallback-Wert: ``` --- # Field Finder Source: https://www.arc-rider.com/docs/mini-widgets/field-finder # Field Finder ## AI Defaults (read first) - **Function name**: `arcFieldFinder`. - **Purpose**: Utility widget to discover Ninox field IDs. Not for production UIs. - **Usage**: Place in a formula field to display field IDs for a table. # Field Finder Mit dem Field Finder kannst du schnell und einfach die eindeutige ID eines Ninox-Feldes ermitteln. Diese ID wird in vielen Widgets benötigt, um Aktionen wie z. B. **„update“** gezielt auf ein bestimmtes Feld auszuführen. ## Anwendungscode ```javascript arcFieldFinder({ apiKey: "YOUR_API_KEY", table: "A", field: "editing" }) ``` 💡 **Hinweis**: Wenn du dir im Globalen Code eine Funktion für deinen API-Key baust, dann musst du diesen nicht immer eingeben. :) Dann könnte dein Anwendungscode besipielsweise so aussehen: ```javascript arcFieldFinder({ apiKey: apiKey(), table: tableId(this), field: "editing" }) ``` ## Beispiel Du kannst für die Parameter natürlich auf Ninox-Felder zurückgreifen. So auch in folgendem Beispiel: ```javascript arcFieldFinder({ apiKey: apiKey(), table: table, field: field }) ``` ![](https://framerusercontent.com/images/3TQuxxR7rg5PIDGt4sudFz8auUA.png) Ausgegeben wird dir nun die Field ID "B", die du für deine Aktionen in anderen Widgets angeben kannst. --- # Icon Source: https://www.arc-rider.com/docs/mini-widgets/custom-icon # Icon in React Ein Symbol aus einem unterstützten Icon-Set darstellen, etwa als Statusindikator oder innerhalb eines Buttons. 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 { ArcWidgetIcon } from '@arcrider/arcwidgets-react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { return ( ); } ``` ## Features ausprobieren ### Symbol, Strichstärke und Größe Wechsle zwischen Phosphor-Symbolen und Varianten. Das Beispiel verwendet die reale Icon-Laufzeit inklusive SVG-Laden. ```tsx import { useState } from 'react'; import { ArcWidgetIcon } from '@arcrider/arcwidgets-react'; export default function Example() { const [name, setName] = useState('star'); const [style, setStyle] = useState('regular'); const [size, setSize] = useState(32); return ( <>

{name} · {style} · {size}px

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetIconData`. Ü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). Dieses Widget ist eine Darstellungskomponente und stellt keine eigenen Interaktions-Callbacks bereit. ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### iconSet Name des verwendeten Icon-Sets. Typ: `'phosphor' | 'openmoji' | 'material_symbols' | 'material' | string`. ### name Name des Symbols im gewählten Set. Typ: `string`. ### style Variante innerhalb des Icon-Sets. Typ: `string`. ### iconSize Größe des Symbols. Typ: `string`. ### color Symbol- oder Inhaltsfarbe. Typ: `string`. ### containerSize Größe des umgebenden Symbolbereichs. Typ: `string`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### borderRadius Abrundung der Ecken. Typ: `string`. ### borderSize Rahmenstärke. Typ: `string`. ### borderColor Rahmenfarbe. Typ: `string`. ## Verhalten und Grenzen - Icons werden bei Bedarf geladen und im Browser zwischengespeichert. Phosphor- und Material-SVGs nutzen bei leerem Cache `https://universe.arc-rider.com/cdn/`; diese Verbindung muss erreichbar und in der Content Security Policy deiner App erlaubt sein. - Setze `iconSet` und den dazu passenden `name`. Größe und Farbe gehören in `iconSize` und `color`. - Das Widget ist eine Darstellungskomponente. Interaktion und zugängliche Beschriftung liegen beim umgebenden Steuerelement. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetIconData extends ArcWidgetBaseData { iconSet?: 'phosphor' | 'openmoji' | 'material_symbols' | 'material' | string; name: string; style?: string; iconSize?: string; color?: string; containerSize?: string; backgroundColor?: string; borderRadius?: string; borderSize?: string; borderColor?: string; } ``` ## Einsatzbeispiele - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Icon ## AI Defaults (read first) - **Function name**: `arcCustomIcon`. - **iconSet**: `"phosphor"` (default if unset), `"openmoji"`, `"material_symbols"` (alias `"material"`). - **name**: Glyph name (e.g. `"user"`, `"trash"`). Phosphor legacy: style in name (`"trash-bold"`) still works and overrides explicit **style**. - **style**: Phosphor: `regular` | `bold` | `duotone` | `light` | `thin` | `fill`. Material: `outlined` | `rounded` | `sharp` | `rounded-fill` | … (default Phosphor `regular`, Material `outlined`). - **iconSize**, **color**: Icon size and fill color (e.g. `"24px"`, `"#818599"`). - **containerSize**, **backgroundColor**, **borderRadius**, **borderSize**, **borderColor**: Built-in container – do NOT wrap in `arcCustomLayout` for styling. - **No uniqueId needed** for display-only icons. # Custom Icon Jeder Kunde erhält zum Start seiner Lizenz ein Standard Icon-Set. Solltest du mehr Icons benötigen, kontaktiere uns über [support@arc-rider.com](mailto:support@arc-rider.com) oder in unserem [Discord-Channel.](https://discord.com/invite/Fg67f8qX3K) 💡** Hinweis**: Custom Icon unterstützt [Phosphor-Icons](https://phosphoricons.com/), [Material Symbols](https://fonts.google.com/icons) (`iconSet: "material"` oder `"material_symbols"`) und [OpenMoji](https://openmoji.org/). Nach dem ersten Laden werden Phosphor- und Material-Icons im Browser zwischengespeichert – wiederholte Aufrufe sind schneller und funktionieren offline, sobald das Set einmal vollständig geladen wurde (keine Extra-Konfiguration nötig). Um Icons zu deinem Projekt hinzuzufügen, gehst du über dein User Cockpit zum Bereich Icons und wählst diese einfach an (siehe weiter unten die Erklärung). ![](https://framerusercontent.com/images/BQHL1tv8vsJu3xvmoxcMkFDA.png) ![](https://framerusercontent.com/images/hfUtTFBevtvrRjhCZgPJeyouqY.png) ![](https://framerusercontent.com/images/yHkFmJcBcApLpAZQeVoujOs6c.png) ## Anwendungscode ![](https://framerusercontent.com/images/6vH4hAeFxzFpJt5B4RKfjE4bRZ8.png) ```javascript arcCustomIcon({ iconSet: "phosphor", name: "trash", containerSize: "40px", borderSize: "", borderColor: "", borderRadius: "7px", iconSize: "30px", color: "#FF2222", backgroundColor: "#fcdfde" }) ``` ![](https://framerusercontent.com/images/6F2gYfjUBFLwVTpV4t7gEDcVsMo.png) ```javascript arcCustomIcon({ iconSet: "openmoji", name: "envelope-with-arrow-color", containerSize: "50px", borderSize: "1px", borderColor: "", iconSize: "50px", borderRadius: "50px", color: "#C637E4", backgroundColor: "#FAE6FF" }), ``` iconSet definiert das Iconset. ```javascript iconSet: "phosphor", // Standard-Fallback iconSet: "material", // Alias für material_symbols iconSet: "material_symbols", iconSet: "openmoji", ``` Wenn du **kein** `iconSet` angibst, nutzt das Widget das zuerst vollständig vorinstallierte Set (Reihenfolge: Phosphor → Material → OpenMoji), sonst Phosphor. style (optional) wählt die Variante innerhalb des Sets. **Phosphor** — `regular` (Standard), `bold`, `duotone`, `light`, `thin`, `fill`: ```javascript arcCustomIcon({ iconSet: "phosphor", name: "trash", style: "bold", color: "#c00" }) ``` **Legacy:** Stil im Namen (`name: "trash-bold"`) funktioniert weiterhin und hat Vorrang vor einem gesetzten `style`. **Material Symbols** — `outlined` (Standard), `rounded`, `sharp`, gefüllte Varianten mit Suffix `-fill` (z. B. `rounded-fill`): ```javascript arcCustomIcon({ iconSet: "material", name: "home", style: "rounded-fill", iconSize: "24px" }) ``` name definiert den Glyph-Namen (ohne Stil-Suffix, wenn du `style` separat setzt). Namen findest du im Cockpit bzw. in den jeweiligen Icon-Katalogen. ```javascript name: "trash", // Titel immer in " schreiben ``` containerSize definiert, ob und in welcher Größe du deinen Container/Background des Icons anzeigen lassen möchtest. ```javascript containerSize: "30px", // Pixel-Wert in " angeben containerSize: "" // Kein Fallback-Wert ``` ***borderSize*** definiert, wie dick deine Umrandung sein soll. ```javascript borderSize: "1px", // Pixel-Wert in " angeben borderSize: "", // Kein Fallback-Wert ``` ***borderColor*** gibt die Farbe deiner Border/Umrandung an. ```javascript borderColor: "#000", // HEX Code in " angeben borderColor: "", // Kein Fallback-Wert ``` ***borderRadius*** gibt die Abrundung deiner Border-Ecken an. ```javascript borderRadius: "7px", // Pixel-Wert in " angeben borderRadius: "", // Kein Fallback ``` ***iconSize*** gibt die Größe deines Icons an. ```javascript iconSize: "20px", // Pixel-Wert in " angeben iconSize: "", // Fallback: "15px" ``` ***color*** gibt die Farbe deines Icons an. ```javascript color: "#FF2222", // Hex-Wert in " angeben color: "", // Kein Fallback-Wert ``` ***backgroundColor*** gibt die Hintergrund-Farbe des Containers an. ```javascript backgroundColor: "#fcdfde", // backgroundColor: "", // Kein Fallbackwert ``` ## Icons über dein Cockpit hinzufügen Du kannst die deine Aktuelle Sammlung und die verfügbaren Iconsets anzeigen lassen. Filter für Icon Styles oder Icon Gruppen findest du auf der rechten Seit. Um ein Icon an- oder abzuwählen, klicke es einfach an. Kopiere dann den Widget Code und füge ihn in deinen globalen Code. Schon kannst du sie im Anwedungscode nutzen. ![](https://framerusercontent.com/images/Gd2H3UKMxrORUpsAvOzylXWDAM.png) --- # Images Source: https://www.arc-rider.com/docs/mini-widgets/images # Images ## AI Defaults (read first) - **Function name**: `arcCustomImage`. - **value**: Image URL or Base64 string. - **width**, **height**: Set dimensions directly. - **borderRadius**: E.g. `"8px"` for rounded corners, `"50%"` for circular. - **objectFit**: `"cover"` (crop to fill) or `"contain"` (fit inside). # Custom Image ![](https://framerusercontent.com/images/h6ypxIbc4vHFWhQqTPFfWjn1k.png) Mit Images kannst du Bilder in Tabellen, Kanban oder anderen Widgets laden. Du hast etliche Einstellungen, von Umrandung bis Abrundung über Größeneinstellungen. ```javascript arcCustomImage({ uniqueId: "ProjektBild"+Nr, title: "Logo", width: "80px", height: "80px", backgroundSize: "cover", backgroundColor: "", style: "", // Hier kannst du eigene CSS Styles hinterlegen. radius: "5px", borderColor: "", link: text(Projekt.'Bild URL') // Hier musst du immer einen shareLink deines Bildes hinterlegen. }) ``` #### Allgemeine Settings ### uniqueId Die ***uniqueId*** vergibst du individuell und sollte einzigartig sein. Sinn dahinter ist: Wenn du mehrere Images mit unterschiedlichen Settings auf deiner Oberfläche erstellst, überschreiben sich die Styles nicht. ```javascript uniqueId: "ProjektBild" + Nr // Text in " + Ninox Nr ``` ### title ```javascript title: "Logo", ``` ### width Mit*** width*** kannst du die Breite des Bild Containers einstellen. ```javascript width: "100%", // Prozent-Werte. Hier: volle Breite. width: "100px", // Pixel-Werte ``` ### height Mit*** height*** kannst du die Höhe des Bilde Containers einstellen. ```javascript height: "auto", // Automatisch an den Inhalt angepasste Höhe. height: "300px", // Pixel-Werte ``` ### backgroundSize Mit ***backgroundSize*** definierst du die Größe des Bildes im Container. ```javascript backgroundSize: "cover", // Füllt den Container komplett aus, kann dazu führen, dass das Bild abgeschnitten wird. backgroundSize: "contain", // Das Bild wird komplett in dem Container angezeigt. backgroundSize: "50%", backgroundSize: "auto", // Bild wird in der Originalgröße angezeigt. backgroundSize: "15px 10px", // Gibt Breite und Höhe in Pixel an. ``` ### backgroundColor Mit ***backgroundColor*** kannst du die Farbe des Hintergrundes bestimmen. Vor allem nützlich, wenn kein Bild angezeigt wird oder für Bilder, die nicht auf die Gesamtgröße angepasst wurden. Es ist auch möglich, den Hintergrund transparent zu machen. ```javascript backgroundColor: "#1D8C96", // HEX-Code für Farben backgroundColor: "transparent", // für einen transparenten Hintergrund ``` ### style Bei dem*** style*** Parameter kannst du eigene CSS styles hinzufügen. ```javascript style: "opacity 30%;", // CSS styles ```
--- # Input Source: https://www.arc-rider.com/docs/mini-widgets/input # Input in React Text, Zahlen, Datum oder Uhrzeit erfassen. Das Beispiel filtert über React-State und zeigt die Rückmeldung beim Bestätigen oder Zurücksetzen. 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 { ArcWidgetInput } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [query, setQuery] = useState(''); const [last, setLast] = useState(''); return ( <> setQuery(String(value ?? ''))} onActions={(_value, source) => setLast(source)} />

{t('Suche', 'Search', 'Búsqueda')}: {query || t('Alle', 'All', 'Todos')}{' '} · {t('Letztes Ereignis', 'Last event', 'Último evento')}: {last || '–'}

); } ``` ## Features ausprobieren ### Live-Update, Debounce, Enter und Reset Tippe einen Suchbegriff, ändere die Verzögerung und teste Enter sowie das Zurücksetzen. Unter dem Feld siehst du den übernommenen React-State. ```tsx import { useState } from 'react'; import { ArcWidgetInput } from '@arcrider/arcwidgets-react'; export default function Example() { const [value, setValue] = useState(''); const [delay, setDelay] = useState(300); const [event, setEvent] = useState('Noch kein Ereignis'); return ( <> setValue(String(next ?? ''))} onActions={(next, source) => setEvent(`${source}: ${next}`)} />

React-State: {value || 'leer'} · {event}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetInputData`. Ü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). ### onChange Übernimmt den neuen Eingabewert in deinen State. ```typescript onChange?: (value: string | number | null) => void | Promise; ``` ### onActions Meldet Bestätigung oder Zurücksetzen. Der Wert ist ein String; `source` unterscheidet beide Vorgänge. ```typescript onActions?: (value: string, source: 'submit' | 'reset') => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### title Beschriftung; als Slot auch mit JSX möglich. Typ: `ArcWidgetSlot`. ### placeholder Hinweis bei leerem Wert. Typ: `string`. ### type Eingabetyp. Im Beispiel wird Text verwendet. Typ: `'text' | 'number' | 'date' | 'time' | 'textarea' | string`. ### value Anzuzeigender bzw. aktuell bearbeiteter Wert. Typ: `unknown`. ### disabled Deaktiviert die Eingabe. Typ: `boolean`. ### tempStorage Unterdrückt den Change-Callback; nur für bewusst lokal gehaltene Werte. Typ: `boolean`. ### suffix Inhalt nach der Beschriftung oder Einheit. Typ: `ArcWidgetSlot`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### alignX Horizontale Ausrichtung. Typ: `string`. ### paddingX Innerer Abstand links und rechts. Typ: `string`. ### paddingY Innerer Abstand oben und unten. Typ: `string`. ### fontColor Schriftfarbe. Typ: `string`. ### fontSize Schriftgröße als CSS-Wert. Typ: `string`. ### fontWeight Schriftstärke. Typ: `string | number`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### borderWidth Rahmenstärke. Typ: `string`. ### borderColor Rahmenfarbe. Typ: `string`. ### borderRadius Abrundung der Ecken. Typ: `string`. ### autocomplete Autovervollständigung der Eingabe. Typ: `boolean`. ### editable Bearbeitbarkeit des Inhalts. Typ: `boolean`. ### focusAction Einstellungen beim Fokussieren, etwa `autoFocus`. Typ: `{ autoFocus?: boolean; [key: string]: unknown }`. ### placeholderSettings Text und Darstellung des leeren Felds. Typ: `ArcWidgetInputPlaceholderSettings`. ### labelSettings Beschriftung und ihre Ausrichtung. Typ: `ArcWidgetInputLabelSettings`. ### numberFormat Dezimalstellen und Trennzeichen für Zahlen. Typ: `ArcWidgetInputNumberFormat`. ### numpad Bildschirmtastatur für Zahleneingaben. Typ: `ArcWidgetInputNumpad`. ### reset Konfiguration der Rücksetzfunktion. Typ: `ArcWidgetInputReset`. ### timeSettings Zeitbereich und Raster; Einheiten sind Millisekunden. Typ: `{ format?: string; minuteStep?: number; showNow?: boolean; [key: string]: unknown }`. ### liveUpdate Laufende Übernahme bei Texteingabe, optional mit Verzögerung (`debounce`) und Mindestlänge. Typ: `boolean | { debounce?: number; minLength?: number; actions?: boolean; [key: string]: unknown }`. ## Verhalten und Grenzen - `onChange` übergibt den neuen Wert; `onActions(value, source)` meldet Bestätigung oder Reset mit `source: "submit" | "reset"`. - Für Suche eignet sich `liveUpdate: { debounce: 200, minLength: 0 }`. Ohne Live-Update wird beim Bestätigen oder Verlassen übernommen. - Bei `tempStorage: true` bleibt der Wert lokal im Widget und `onChange` entfällt; nutze das nur bewusst. - Der Zahlen-Numpad bestätigt über Fertig, Blur oder Enter. `liveUpdate` wirkt dort nicht wie bei Text. `reset` muss für einen sichtbaren Reset konfiguriert werden. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetInputReset { title?: ArcWidgetSlot; hide?: boolean; visible?: boolean; value?: unknown; icon?: ArcWidgetSlot; color?: string; backgroundColor?: string; borderRadius?: string; [key: string]: unknown; } export interface ArcWidgetInputNumberFormat { digits?: number; decimalSeparator?: string; thousandSeparator?: string; } export interface ArcWidgetInputNumpad { enabled?: boolean; size?: string; openOnFocus?: boolean; allowNegative?: boolean; maxDecimals?: number; suffix?: string; [key: string]: unknown; } export interface ArcWidgetInputPlaceholderSettings { value?: string; fontColor?: string; backgroundColor?: string; [key: string]: unknown; } export interface ArcWidgetInputLabelSettings { title?: ArcWidgetSlot; fontSize?: string; fontColor?: string; alignX?: string; gap?: string; [key: string]: unknown; } export interface ArcWidgetInputData extends Omit { title?: ArcWidgetSlot; placeholder?: string; type?: 'text' | 'number' | 'date' | 'time' | 'textarea' | string; value?: unknown; disabled?: boolean; tempStorage?: boolean; suffix?: ArcWidgetSlot; width?: string; height?: string; alignX?: string; paddingX?: string; paddingY?: string; fontColor?: string; fontSize?: string; fontWeight?: string | number; backgroundColor?: string; borderWidth?: string; borderColor?: string; borderRadius?: string; autocomplete?: boolean; editable?: boolean; focusAction?: { autoFocus?: boolean; [key: string]: unknown }; placeholderSettings?: ArcWidgetInputPlaceholderSettings; labelSettings?: ArcWidgetInputLabelSettings; numberFormat?: ArcWidgetInputNumberFormat; numpad?: ArcWidgetInputNumpad; reset?: ArcWidgetInputReset; timeSettings?: { format?: string; minuteStep?: number; showNow?: boolean; [key: string]: unknown }; liveUpdate?: boolean | { debounce?: number; minLength?: number; actions?: boolean; [key: string]: unknown }; } export interface ArcWidgetInputProps extends ArcReactWidgetProps { onChange?: (value: string | number | null) => void | Promise; onActions?: (value: string, source: 'submit' | 'reset') => void; } ``` ## Einsatzbeispiele - [Status auswählen und speichern](/docs/react/examples/status-als-buttons-ausgeben) - [Ein Formular in Schritten](/docs/react/examples/datenaufnahme-in-schritten) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Input ## AI Defaults (read first) - **uniqueId**: Required, e.g. `"input-name-" + Nr`. - **recordId**: Required for Ninox binding, use raw `Nr`. Core does not read it — Ninox handler builds the `update` from `ui.data`. - **field**: Required for Ninox, use `fieldId(Nr, "Feldname")` (alias `fieldId`). Adapter-only. - **placeholder**: Optional hint text. - **type**: `"text"` (default), `"number"`, `"date"`, `"time"`, `"textarea"`. - **focusAction**: Set `{ autoFocus: false }` for tablet compatibility. - **Mobile (iPhone/iPad/Android, schmale Viewports)**: Text/textarea use keyboard dock above the on-screen keyboard; `type: "date"` / `"time"` / `"number"` use the custom picker (not native / not system keyboard). Reset supports `arcCustomIcon` in the dock (text) or the shared icon slot (date/time/number). - **numberFormat** (only `type: "number"`): `{ digits, decimalSeparator, thousandSeparator }` — default DE: `digits: 2`, `","` / `"."`. - **numpad** (only `type: "number"`): `{ enabled, size, openOnFocus, allowNegative, maxDecimals, suffix }` — popup numpad (default `enabled: true`). Not the same as the standalone `arcCustomNumPad` widget. - **Persistence**: Core emits semantic `change` (field value) and `actions` (`inputValue` + `source: "submit" | "reset"`). Ninox adapter writes `type: "update"` with `fieldId` and runs formula `actions` / `reset.actions` (with `##inputValue##`). React: `onChange(value)` / `onActions(inputValue, source)`. - **actions**: Optional (Ninox formula). Array of action objects, fired on submit (Enter/blur / numpad Fertig) and on reset. Use `##inputValue##` placeholder to inject the current input value. Resolved in the Ninox handler (not passed through React callbacks). - **reset.actions**: Optional. If set inside `reset: { actions: [...] }`, only these actions fire on reset (not top-level `actions`). Submit still uses `actions`. - **liveUpdate**: Optional. `true` or `{ debounce: 300, minLength: 0, actions: true }`. Fires updates while typing (debounced). On blur, only fires if value changed since last live update. **Not used by the number numpad** (commit on Fertig / outside / Enter only). # Input ![](https://framerusercontent.com/images/39CfOjKuSZ4PvVFwHAh0IZm7Lc.png) `arcCustomInput` ist ein vielseitiges und anpassbares Mini-Widget zur Darstellung und Bearbeitung von Eingabefeldern in deiner Ninox-Datenbank. Es ersetzt das Standard-Eingabefeld durch ein flexibles, visuell anpassbares Interface, das du direkt in **Formel-Feldern oder komplexeren Widgetcodes **einsetzen kannst – z. B. auf Detailansichten, Dashboards oder in Kombination mit anderen Widgets wie `arcCustomTable`. Mit `arcCustomInput` lassen sich Texteingaben, Zahlen, Datums- oder Uhrzeitwerte gezielt steuern, visuell hervorheben oder mit **eigenen Platzhaltern, Labels und Fokus-Verhalten** versehen. Auch mehrzeilige Eingaben (`textarea`) werden unterstützt. ### 🚀 Hauptfunktionen - **Komplett visuell konfigurierbar** (Farben, Schriftgrößen, Rahmen, Ausrichtung usw.) - **Single-Line oder Multiline** (Text vs. Textarea) – automatisch je nach `type` - **Tab-Steuerung zwischen Feldern** (auch zwischen `arcCustomInput` und `arcCustomSelect`) - **Live-Fokus & Loop-Logik**: Automatisches Reinspringen in Felder (direkt tippen ohne das Input anzuklicken) - **Autofokus bei Start oder Klick** - **Read-Only-Ansicht**, wenn `disabled: true` - **Platzhalter-Designs** für visuelles Feedback bei leeren Feldern - **Reset-Button** zum Zurücksetzen des Werts (nur sichtbar, wenn ein Wert vorhanden ist) - **Zahlen-Numpad** bei `type: "number"` (Popup wie Date/Time; Locale über `numberFormat`) - **Actions** bei Submit (Enter/blur / Numpad-Fertig) und Reset – z. B. für Suchfelder, Barcode-Scanner oder Trigger-Logik ## Gesamter Anwendungscode Folgend siehst du einen beispielhaften Anwendungscode für ein Input Field ```javascript arcCustomInput({ uniqueId: "Mein-Input-" + Nr, recordId: Nr, fieldId: "", title: text(Zahl), value: text(Zahl), type: "text", embedded: true, disabled: false, tempStorage: false, suffix: "", width: "", height: "", alignX: "left", paddingY: "", paddingX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "", borderWidth: "", borderColor: "", borderRadius: "", numberFormat: { digits: 2, decimalSeparator: ",", thousandSeparator: "." }, numpad: { enabled: true, size: "default", openOnFocus: false, allowNegative: false, maxDecimals: 2, suffix: "€" }, placeholderSettings: { value: "", fontColor: "", backgroundColor: "" }, reset: { visible: false, value: null, icon: "", color: "", backgroundColor: "", borderRadius: "" }, focusAction: { width: "", showFocusOutline: false, outlineWidth: "", outlineColor: "", loop:", nextField: "" }, actions: [ { type: "update", recordId: Nr, field: fieldId(Nr, "Feld"), value: "##inputValue##" } ], liveUpdate: false, labelSettings: { title: "", fontSize: "", alignX: "", gap: "" } }) ``` ## Erklärung einzelner Parameter Folgend wird dir aufgeschlüsselt, welche Parameter du verwenden kannst und was du jeweils eintragen musst. ### uniqueId `uniqueId` vergibst du individuell und sollte einzigartig sein. Sinn dahinter ist: Wenn du mehrere Inputfelder mit unterschiedlichen Settings auf deiner Seite erstellst, überschrieben sich die Styles nicht. ```javascript uniqueId: Nr, ``` ### recordId `recordId` gibt an in welchem Record die Änderung vorgenommen werden soll. ```javascript recordId: Nr, ``` ### fieldId `fieldId` gibt an, welches Feld beschrieben werden soll. Die Field ID findest du mit dem Mini Widget [***Field Finder***](/documentation/field-finder) heraus. ```javascript fieldId: "", ``` ### value `value` gibt an, welcher Wert in der Oberfläche angezeigt werden soll. Das ist das Feld, dessen Field ID du ausgibst. Zum Beispiel Menge. **Bei `type: "date"`:** Übergib den Wert als **Millisekunden** (Unix-Timestamp). Ninox speichert Datumsfelder intern so – du kannst das Feld direkt übergeben, ohne `text()` oder `format()`: ```javascript value: Startdatum, ``` Das Widget konvertiert intern für Anzeige und speichert bei Änderung wieder Millisekunden. Fallback: DD.MM.YYYY-String wird ebenfalls akzeptiert. ```javascript value: "", ``` ### type `type` definiert den Feldtypen deines Inputs. Den type *number* nutzt du, wenn du ein Ninox Zahlenfeld wiedergeben möchtest und *text*, wenn du ein Ninox Textfeld wiedergeben möchtest. Indem du *textarea* beim type eingibst, wird das Input als mehrzeiliges Feld angezeigt. Die types ***date*** und ***time*** nutzen auf **Desktop** Segment-Eingabe plus Custom-Picker. Auf **Mobil** (iPhone, iPad, Android oder Viewport ≤ 768px) öffnet Tippen auf das Feld den **arcCustom Date-/Time-Picker** – nicht die System-Tastatur. **`type: "number"`** nutzt ein **Numpad-Popup** (analog Date/Time), gesteuert über `numberFormat` und `numpad` (siehe unten). Auf Mobil öffnet Tippen auf das Feld immer das Numpad. Auf Desktop kannst du tippen und optional per Rechner-Icon (oder `numpad.openOnFocus: true`) das Popup öffnen. Für **text** und **textarea** auf Mobil: Beim Fokus wird das Feld in ein **Keyboard-Dock** über der Bildschirm-Tastatur angehoben, damit Eingabe und Reset sichtbar bleiben. ```javascript type: "number" // Zahlen-Feld mit Numpad-Popup (Z. B. Betrag, Menge) type: "text" // Text-Feld (Z. B. für Volltextsuchen) type: "date" // Datums-Feld mit Datepicker type: "time" // Zeit-Feld mit Timepicker type: "textarea" // Mehrzeiliges Input-Feld type: "" // default: text ``` ### numberFormat (nur bei `type: "number"`) Steuert die **Anzeige** und das **Zurückparsen** von Zahlen (Locale). Speichern / `##inputValue##` liefern eine normale Zahl (Punkt-Dezimal intern). ```javascript numberFormat: { digits: 2, // Nachkommastellen in der Anzeige (Default: 2). null = so viele wie nötig decimalSeparator: ",", // Default: "," (DE) thousandSeparator: "." // Default: "." (DE). EN-Beispiel: "." / "," } ``` | Property | Default | Beschreibung | |----------|---------|--------------| | `digits` | `2` | Feste Nachkommastellen nach Commit (z. B. `14,50`). Während des Tippens im Numpad siehst du den Rohwert (`14,5`). `null` = keine Auffüllung. | | `decimalSeparator` | `","` | Dezimaltrenner in der UI | | `thousandSeparator` | `"."` | Tausendertrenner in der UI (z. B. `1.234,56`) | **Englisch (Punkt-Dezimal):** ```javascript numberFormat: { digits: 2, decimalSeparator: ".", thousandSeparator: "," } ``` ### numpad (nur bei `type: "number"`) Steuert das **Numpad-Popup** im Input. Das ist **nicht** das eigenständige Widget [`arcCustomNumPad`](/documentation/custom-numpad) (Fullscreen-/Inline-Numpad mit eigenem Submit) – hier geht es um den Feld-Picker wie beim Datepicker. ```javascript numpad: { enabled: true, // Default: true. false = nur Tippen (Desktop), kein Popup size: "default", // "default" | "mini" openOnFocus: false, // Desktop: true = Klick ins Feld öffnet Numpad (statt Tippen) allowNegative: false, // ±-Taste maxDecimals: 2, // Max. Nachkommastellen beim Tippen (Default: numberFormat.digits) suffix: "€" // Anzeige im Feld + im Numpad (optional; Fallback: top-level suffix) } ``` | Property | Default | Beschreibung | |----------|---------|--------------| | `enabled` | `true` | Numpad an/aus | | `size` | `"default"` | `"mini"` = kompakteres Panel | | `openOnFocus` | `false` | Desktop: Feldklick öffnet Numpad. Mobil: immer Numpad beim Tap | | `allowNegative` | `false` | Vorzeichen umschalten | | `maxDecimals` | `numberFormat.digits` | Begrenzt Nachkommastellen während der Eingabe; `0` = kein Komma | | `suffix` | `""` / top-level `suffix` | z. B. `€`, `Stk` – erscheint in der Feldanzeige (`14,50 €`) und im Numpad-Display | **Verhalten** - Wert wird erst bei **Fertig**, Klick außerhalb oder **Enter** geschrieben (kein `liveUpdate` über das Numpad). - Escape schließt ohne zu speichern. - Das Zahlenfeld bleibt sichtbar; das Numpad dockt am Feld (bevorzugt darüber). - **Reset:** Bei `reset.visible: true` und vorhandenem Wert erscheint **nur das X** (wie bei Date) – kein Rechner + X gleichzeitig. Leer / ohne Reset → Rechner-Icon öffnet das Numpad. **Beispiel: Betrag mit Mini-Numpad** ```javascript arcCustomInput({ uniqueId: "betrag-" + Nr, recordId: Nr, fieldId: fieldId(Nr, "Betrag"), value: Betrag, type: "number", alignX: "right", numberFormat: { digits: 2, decimalSeparator: ",", thousandSeparator: "." }, numpad: { enabled: true, size: "mini", openOnFocus: true, suffix: "€" }, reset: { visible: true }, placeholderSettings: { value: "0,00" }, labelSettings: { title: "Betrag" } }) ``` **Beispiel: Ganzzahl (Menge)** ```javascript arcCustomInput({ uniqueId: "menge-" + Nr, recordId: Nr, fieldId: fieldId(Nr, "Menge"), value: Menge, type: "number", alignX: "right", numberFormat: { digits: 0, decimalSeparator: ",", thousandSeparator: "." }, numpad: { enabled: true, maxDecimals: 0, suffix: "Stk" }, placeholderSettings: { value: "0" } }) ``` ### timeSettings (nur bei `type: "time"`) Bei `type: "time"` steuert `timeSettings` den Custom Time Picker (analog zum Date Picker): ```javascript timeSettings: { format: "24h", // "24h" (default) oder "12h" (AM/PM links) minuteStep: 5, // Minuten-Schritte: 5 (00,05,10...) oder 1 showNow: true, // "Jetzt"-Button (default: true) hours: [ // Optional: Overrides für einzelne Stunden { value: 10, label: "10", backgroundColor: "#e8f4fd", fontColor: "#1a73e8" } ], minutes: [ // Optional: Overrides für einzelne Minuten { value: 30, label: "½", backgroundColor: "#f0f0f0" } ], labels: [{ hours: "Stunden", minutes: "Minuten" }] // Optional: Übersetzung } ``` - **format**: `"24h"` (default) zeigt 00–23, `"12h"` zeigt 01–12 mit AM/PM-Umschalter links. - **minuteStep**: `5` für 5er-Schritte (00, 05, 10 … 55), `1` für volle Minutenauflösung. - **showNow**: `true` zeigt den „Jetzt“-Button. Klick auf Minute schließt den Picker. - **hours** / **minutes**: Arrays mit Overrides. Nur angegebene Einträge überschreiben Defaults (`value`, `label`, `backgroundColor`, `fontColor`). - **labels**: Array mit Objekt für übersetzbare Sektions-Labels. Beispiel: `labels: [{ hours: "Stunden", minutes: "Minuten" }]`. Es wird das erste Element verwendet. Fehlende Keys nutzen Defaults (`"HOURS"`, `"MINUTES"`). ### embedded `embedded` benutzt du, wenn du den Button ***nicht ***innerhalb eines anderen Widgets einsetzt. Standardmäßig ist dies aber auf true gesetzt. ```javascript embedded: false, // ermöglicht den Standalone Einsatz des Widgets (nicht eingebettet in anderen Widgets) embedded: true, // Fallback ``` ### tempStorage Mit `tempStorage`**: true/false **kann der eingegebene Wert übernommen werden, ohne dass ein Ninox Feld dahinter steht. Dieser Anwendungsfall macht nur Sinn, wenn Daten nur temporär eingegeben werden sollen und nicht in Ninox direkt gespeichert werden sollen. Kombiniert mit `actions` kannst du z. B. ein Suchfeld bauen: Der Wert wird nicht gespeichert, aber beim Submit feuern die Actions (z. B. ein Update auf ein Helper-Feld oder eine URL mit `##inputValue##`). ```javascript tempStorage: "true", // der eingegebene Wert wird nur temporär gespeichert tempStorage: "false", // default ``` ### width `width` gibt die Breite deines Inputfeldes an. ```javascript width: "50px", // Pixel Werte in " width: "100%", // Prozent Werte in " ``` ### height `height` gibt die Höhe deines Input-Feldes an. ```javascript height: "50px", // Pixel-Werte in " ``` ### color `color` gibt die Schriftfarbe des Eingabetextes an. ```javascript color: "#000000", // HEX Wert in " ``` ### background `background` gibt die Hintergundfarbe deines Inputfeldes an. 💡 Tipp: Wenn du das Input in ein anderes Widget, wie [Layout](https://www.arc-rider.de/documentation/custom-layout), [Table](/documentation/custom-table), [Card](/documentation/cards) etc. einfügst, kannst du bei Hintergrundfarbe auf `background: "transparent"` einstellen. So wirkt das Input nachtlos und fügt sich optisch perfekt in den Hintergrund ein. ```javascript background: "#EEEEEE", // HEX Wert in " backgrond: "transparent" // keine Hintergrundfarbe ``` ### fontSize `fontSize` gibt die Schriftgröße deiner Input-Texte an. ```javascript fontSize: "12px" // Pixel Werte in " ``` ### fontWeight `fontWeight` gibt die Dicke deiner Schrift an. ```javascript fontWeight: "800", // Zahl in " Default sind 400 ``` ### alignX `alignX` gibt die Ausrichtung deines Textes an. ```javascript alignX: "left", // left orientiert deinen Text linksbündig innerhalb des Input-Feldes alignX: "center", // center orientiert deinen Text zentriert innerhalb des Input-Feldes alignX: "right", // right orientiert deinen Text rechtsbündig innerhalb des Input-Feldes ``` ### paddingY `paddingY`* *gibt den Abstand zwischen dem Inhalt (hier: Inhalt des Inputs) und dem Rand jeweils oben und unten an. Dieser Parameter ist hauptsächlich beim type= "textarea" sinnvoll. ```javascript paddingY: "20px", // Pixel-Werte paddingY: "", // default: 2px ``` ### paddingX `paddingX`* gibt den Abstand zwischen dem Inhalt (hier: Inhalt des Inputs) und dem Rand jeweils links und rechts an. * ```javascript paddingX: "20px", // Pixel-Werte paddingX: "", // default: 8px ``` ### suffix `suffix` definiert eine Nachsilbe in der **Anzeige** (nicht Teil des gespeicherten Werts). Bei `type: "number"` kannst du dasselbe auch unter `numpad.suffix` setzen (`numpad.suffix` hat Vorrang). ```javascript suffix: "€", // Anzeige z. B. 1.234,56 € — gespeichert wird nur die Zahl ``` ### Placeholder Settings Block `placeholderSettings` erlauben es Einstellungen für den placeholder des Input-Felds zu setzen. Es gibt dabei folgende Optionen unter anderem: ***value***: gibt an, was als Platzhalter vor dem Reinschreiben anzeigt werden soll. Zum Beispiel: "0" oder "Suche" oder auch Ninox Formeln & Konditionen (wie z.B. if cnt(Ergebnisse)!= null then "Suche" else " end).: ```javascript placeholderSettings: { value: "Suche", // Wert der ausgegeben werden soll fontColor: "#000", // Schriftfarbe backgroundColor: "" // Hintergrundfarbe }, ``` ### reset Block Der `reset`-Block steuert einen **Zurücksetzen-Button** rechts im Eingabefeld. Der Button wird nur angezeigt, wenn ein Wert im Feld steht – bei leerem Feld verschwindet er automatisch. - `visible` – Wenn `true`, wird der Reset-Button angezeigt (sofern ein Wert vorhanden ist). - `value` – Optional. Der Wert, der beim Klick gesetzt wird. Default: `null`. Bei `type: "date"` als **Millisekunden** übergeben (z. B. `number(date(calYear, calMonth, 1))` für 1. des Monats, `today()` für heute). Dieser Wert wird 1:1 als `##inputValue##` in den Actions eingesetzt. - `icon` – Optional. Eigenes Icon als HTML-String (z. B. via `arcCustomIcon`). Ohne Angabe wird ein Standard-X-Icon verwendet. - `color` – Optional. Farbe des Icons (für Default-Icon) bzw. Text (für später). - `backgroundColor` – Optional. Hintergrundfarbe des Reset-Buttons. - `borderRadius` – Optional. Abrundung der Ecken des Reset-Buttons. - `actions` – Optional. Eigenes Action-Array nur für den Reset. Wenn gesetzt, feuern beim Reset nur diese Actions statt der Top-Level-`actions`. `##inputValue##` wird durch `reset.value` ersetzt. ```javascript reset: { visible: true, value: null, icon: arcCustomIcon({ name: "x", color: "#666666", iconSize: "14px" }), color: "#555555", backgroundColor: "#f0f0f0", borderRadius: "4px", actions: [] } ``` **💡 Hinweise** - Der Reset-Button erscheint nur, wenn das Feld einen Wert enthält. - Bei `type: "date"` / `"time"` / `"number"` (mit Numpad) wird **ein** Icon rechts genutzt: Picker-Icon wenn leer, **X** wenn Wert + `reset.visible: true` (nie zwei Icons übereinander). - Bei `reset.visible: true` wird `reset.value` beim Klick verwendet (z. B. Sprung auf 1. des Monats statt Leeren). - Bei `disabled: true` wird der Reset-Button nicht angezeigt. - Ohne `icon` wird ein Standard-X-Icon (Phosphor) verwendet. ### actions Mit `actions` kannst du ein Array von Aktionen definieren, die beim **Submit** (Enter oder Blur bei Wertänderung) und beim **Reset** ausgeführt werden. Das entspricht dem gleichen Pattern wie bei `arcCustomButton`. **Trigger-Zeitpunkte** - **Submit**: Wenn der Nutzer Enter drückt oder das Feld verlässt und sich der Wert geändert hat. - **Reset**: Wenn der Nutzer auf den Reset-Button klickt (nach dem DB-Update). **Platzhalter `##inputValue##`** In allen String-Properties der Action-Objekte kannst du `##inputValue##` verwenden. Der Platzhalter wird zur Laufzeit durch den aktuellen (formatierten) Input-Wert ersetzt – beim Submit durch den eingegebenen Wert, beim Reset durch `reset.value` (oder leer, wenn `reset.value` nicht gesetzt ist). Bei `type: "date"` ist das Format immer **Millisekunden-String** (Submit und Reset). **Unterstützte Action-Typen** Wie bei `arcCustomButton`: `update`, `popup`, `openRecord`, `delete`, `openUrl`, `customJS`, `create` usw. Für `openUrl` und `customJS` kannst du `action` statt `actionValue` verwenden. **Beispiele** Suchfeld (mit `tempStorage: true` – Wert wird nicht gespeichert, Action feuert trotzdem): ```javascript actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "helper_search"), value: "##inputValue##" }] ``` Barcode-Scanner (Feld updaten und Popup öffnen): ```javascript actions: [ { type: "update", recordId: Nr, field: fieldId(Nr, "Barcode"), value: "##inputValue##" }, { type: "popup", recordId: Nr } ] ``` URL mit Suchbegriff öffnen: ```javascript actions: [{ type: "openUrl", action: "https://example.com/search?q=##inputValue##" }] ``` ### reset.actions Optional. Wenn innerhalb des `reset`-Blocks ein eigenes `actions`-Array definiert wird, feuern beim **Reset** nur diese Actions (nicht die Top-Level-`actions`). Beim **Submit** werden weiterhin die Top-Level-`actions` ausgeführt. Ohne `reset.actions` verhalten sich Submit und Reset wie bisher – beide nutzen `actions`. **Beispiel: Kalender-Datumsfeld mit Reset auf 1. des Monats** ```javascript arcCustomInput({ uniqueId: "cal-date-" + Nr, recordId: Nr, fieldId: fieldId(Nr, "cal_date"), type: "date", value: calDate, reset: { visible: true, value: number(date(calYear, calMonth, 1)), actions: [{ type: "update", recordId: Nr, field: stateField, value: stateReset }] }, actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "cal_date"), value: "##inputValue##" }] }) ``` ### liveUpdate Mit `liveUpdate` werden Werte **während des Tippens** an Ninox gesendet – nicht erst beim Verlassen des Felds (blur). Ideal für Echtzeit-Suchfelder, Live-Filter oder Preview-Szenarien. **Einfache Variante** (Boolean – 300ms Debounce, Actions feuern mit): ```javascript liveUpdate: true ``` **Objekt-Variante** (volle Kontrolle): ```javascript liveUpdate: { debounce: 300, // ms – Verzögerung zwischen Tastendrücken (Default: 300) minLength: 3, // Mindestzeichen bevor gefeuert wird (Default: 0) actions: false // Sollen actions[] auch live feuern? (Default: true) } ``` **Optionen** | Property | Typ | Default | Beschreibung | |----------|-----|---------|-------------| | `debounce` | `number` | `300` | Wartezeit in ms nach dem letzten Tastendruck | | `minLength` | `number` | `0` | Mindestlänge des Eingabewerts bevor ein Update feuert | | `actions` | `boolean` | `true` | Ob die `actions[]`-Array auch bei jedem Live-Update ausgeführt werden soll | **Verhalten** - Beim Blur wird nur noch gefeuert, wenn sich der Wert seit dem letzten Live-Update geändert hat (kein Doppel-Update). - `tempStorage: true` wird respektiert – bei Live-Update wird dann kein DB-Update gemacht, aber Actions feuern trotzdem (wenn `actions: true`). - `##inputValue##`-Platzhalter in Actions funktionieren auch bei Live-Updates. **Beispiel: Live-Suchfeld** ```javascript arcCustomInput({ uniqueId: "search-" + Nr, recordId: Nr, fieldId: fieldId(Nr, "helper_search"), value: text(helper_search), type: "text", embedded: true, placeholderSettings: { value: "Suche..." }, liveUpdate: { debounce: 400, minLength: 2 }, actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "helper_search"), value: "##inputValue##" }] }) ``` ### Focus Action Block In der `focusAction` definierst du, wie dein Inputfeld im aktiven Zustand aussieht. Also bei Klick in das Inputfeld. Hier kann die Breite des Inputfeldes während des Reinklickens angepasst werden. Außerdem kann man Einstellungen für die Outline vornehmen. #### `loop` im Focus Action Block Die Loop-Funktion sorgt dafür, dass der Cursor automatisch in das nächste oder sogar in das gleiche Eingabefeld springt. **Anwendungsfall: Suchfeld mit automatischem Fokus** Ein praktisches Beispiel ist ein Suchfeld, in das der Cursor nach einer Aktion automatisch zurückkehrt. Beispielsweise: - Ein Nutzer gibt einen Suchbegriff ein. - Er wählt ein Produkt aus der Ergebnisliste und legt es in den Warenkorb. - Statt das Suchfeld erneut anklicken zu müssen, springt der Cursor direkt wieder hinein. **Ideal für Barcode-Workflows** Besonders nützlich ist diese Funktion für Barcode-Workflows, bei denen das Eingabefeld nach jeder Bestätigung weiterhin beschreibbar bleiben muss. Wenn mehrere Felder mit der Loop-Option ausgestattet sind, springt der Fokus automatisch in das letzte Feld. Diese Funktion spart Klicks und macht wiederholte Eingaben effizienter. ### Label Settings Block `labelSettings` erlauben es ein Label über dem Input-Feld zu setzen. Es gibt dabei folgende Optionen: ```javascript labelSettings: { title: "Mein Label", // Die Beschriftung des Labels (Text oder Mini-Widget) fontSize: "", // Die Größe des Labels (Default ist 11px) fontColor: "#EEEEEE", // Schriftfarbe des Labels alignX: "", // Die horizontale Anordnung ist über left oder right steuerbar gap: "" // Wert in Pixel (3px) bestimmt den Abstand zum Input-Feld } ``` ## Standard-Werte als Fallback 💡 **Hinweis: **Trägst du keine Werte hinter die Parameter ein, so ist in den meisten Fällen ein Fallback-Wert im System hinterlegt. An manchen Stellen, wie Werte-Felder (Tatsächliche Zahlenwerte zum Beispiel) macht das keinen Sinn. In Style-Parametern, wie Farben oder Größen hingegen schon. Hier siehst du den Code ohne eingetragene Werte und in den Kommentaren dahinter die Standard-Werte: ```javascript arcCustomInput({ uniqueId: Nr, // Kein Fallback recordId: Nr, // Kein Fallback fieldId: "", // Kein Fallback value: "", // Kein Fallback placeholder: "", // Kein Fallback width: "", // Fallback: 100% color: "", // Fallback: #000000 background: "", // Fallback: #FFFFFF fontSize: "" // Fallback: 13px }) ``` --- ## Feature-Idee: Nice Select (Inspiration für Input/Select) 💡 **Geplante Erweiterung:** [Nice Select](https://nerdy.dev/nice-select#try-it) – moderne native Select-Stilisierung mit `appearance: base-select`, Spring-Animationen, Squircle-Ecken, Light/Dark. Patterns übertragbar auf Input-Fokus, Theming, RTL. [CodePen](https://codepen.io/editor/argyleink/pen/019c1f28-bbc2-7bac-ad4a-a7e41d3730f1). --- # Pagination Source: https://www.arc-rider.com/docs/mini-widgets/pagination # Pagination ## AI Defaults (read first) - **Function name**: `arcCustomPagination`. - **uniqueId**: Required, e.g. `"pagination-" + Nr`. - **recordId**: Required, use raw `Nr`. - **field**: Required, the field storing current page number (`fieldId(Nr, "Feldname")`). - **totalPages**: Total number of pages (computed from data count / items per page). - **currentPage**: Current page value from the field. # arcCustomPagination ![](https://framerusercontent.com/images/8Iufr8Km4medM3pbMqUHXK8sr4.png) Mit `arcCustomPagination` lassen sich Inhalte elegant seitenweise anzeigen – besonders bei langen Listen oder Tabellen ist das ein echter Gamechanger in puncto Performance und Übersichtlichkeit. Statt alle Datensätze auf einmal zu laden, werden nur die gerade sichtbaren Einträge angezeigt. Das spart Ladezeit und macht deine Oberfläche deutlich flüssiger. ### Wann sollte man Pagination einsetzen? Pagination empfiehlt sich vor allem dann, wenn: - eine [Liste sehr viele Einträge](/documentation/custom-table) enthält (z. B. Nachrichten, Projekte, Kunden), - das Rendering durch zu viele gleichzeitig geladene Elemente langsam wird, - eine bessere Nutzerführung durch Seitenzahlen gewünscht ist, - du UI/UX optimieren willst, z. B. für mobile Endgeräte. ## Beispiel für paginierte Liste Bevor du deine Liste paginierst, musst du erstmal die Ninox-Liste an sich definieren. Mit dieser Schleife wird nur der entsprechende Abschnitt aus der Gesamtliste geladen – abhängig von der aktuellen Seitenzahl: ## Anwendungscode für das Widget Diesen Anwendungscode fügst du an der Stelle ein, an der du die Pagination anzeigen lassen möchtest. ```javascript arcCustomPagination({ uniqueId: Nr, editable: true, recordId: Nr, fieldId: "S1", // fieldId vom Ninox Feld "Aktuelle Seite" direction: "horizontal", title: current.'Aktuelle Seite', value: current.'Aktuelle Seite', total: ceil(cnt(list) / 30), totalPrefix: " von " }) ``` ## Parameter
Parameter Typ Beschreibung
`uniqueId` Text Eindeutige ID für das Pagination-Element. Pflichtfeld.
`editable` Boolean Ermöglicht manuelle Eingabe der Seitenzahl im Input-Feld.
`recordId` Text ID des aktuellen Datensatzes, auf den das Update erfolgt. (Die Aktion ausgeführt wird)
`fieldId` Text Das Feld, das die aktuelle Seite speichert.
`direction` Text Layout der Pagination: `"horizontal"` (Standard) oder `"vertical"`.
`title` Text Der aktuell angezeigte Wert im Input-Feld.
`value` Zahl Aktuelle Seitenzahl – wichtig für Navigation und Bedingung von Buttons.
`total` Zahl Gesamtzahl an Seiten (z. B. `ceil(cnt(list) / 30)`).
`totalPrefix` Text Kleiner Zusatz vor der Gesamtseitenanzahl, z. B. `" von "`.
`valuePrefix` Text Optionaler String, der beim Update vor die Seitenzahl gesetzt wird.
`buttonFirst` Widget Optionaler Inhalt (Text oder Widget) für den „erste Seite“-Button.
`buttonMinus` Widget Optionaler Inhalt (Text oder Widget) für den „zurück“-Button.
`buttonPlus` Widget Optionaler Inhalt (Text oder Widget) für den „vor“-Button.
`buttonLast` Widget Optionaler Inhalt (Text oder Widget) für den „letzte Seite“-Button.
## Aufbau in einer Custom Table ```javascript let current := this; let projectList := filteredList_Personen; let current := this; let maxEntries := 'Einträge pro Seite'; let currentPage := 'Aktuelle Seite'; let list := filteredList_Personen; let paginatedList := unique(record('Stammdaten Person',0))[!= 0]; for listItem in slice(list.Nr, maxEntries * (currentPage - 1), maxEntries * currentPage) do paginatedList := unique(paginatedList, record('Stammdaten Person',listItem)) end; let data := { uniqueListId: "Personen " + Nr, tableId: tableId(first(projectList)), height: "100%", embedded: true, table: paginatedList.[{ recordId: Nr, groupRowColor: text(designSystem().colors.borderBlue), rowHeight: "auto", rowPaddingY: "5px", columns: [{ field: "Name", title: "Name", align: "left", value: data.name }, { field: "Typ", title: "Typ", width: "120px", align: "left", value: html(data.monteur.badge) }, { field: "Adresse", title: "Adresse", align: "left", alignY: "center", value: data.address.block, } ] }], footer: { showFooter: true, showActionButton: false, leftSideContent:", rightSideContent: arcCustomPagination({ uniqueId: "pagination-person" + Nr, editable: true, recordId: Nr, fieldId: fieldId(current.Nr, "Aktuelle Seite"), direction: "horizontal", title: current.'Aktuelle Seite', value: current.'Aktuelle Seite', total: ceil(cnt(list) / 'Einträge pro Seite'), totalPrefix: " von " }) } }; arcCustomTable(data) ``` ## Anwendungscode Complex ```javascript arcCustomPagination({ uniqueId: Nr, editable: true, recordId: Nr, fieldId: "E2", direction: "horizontal", title: current.'Aktuelle Seite', value: current.'Aktuelle Seite', total: ceil(cnt(list) / maxEntries), totalPrefix: " von ", buttonMinus: arcCustomButton({ uniqueId: "ButtonMinus" + Nr, icon: arcCustomIcon({ name: "caret-left", color: "#555", size: 20 }), fontSize: "18px", fontColor: "", iconColor: "", backgroundColor: "transparent", borderColor: "transparent" }), buttonPlus: arcCustomButton({ uniqueId: "ButtonPlus" + Nr, icon: arcCustomIcon({ name: "caret-right", color: "#555", size: 20 }), fontSize: "18px", fontColor: "", iconColor: "", backgroundColor: "transparent", borderColor: "transparent" }), buttonFirst: arcCustomButton({ uniqueId: "ButtonFirst" + Nr, icon: arcCustomIcon({ name: "caret-double-left", color: "#555", size: 20 }), fontSize: "18px", fontColor: "", iconColor: "", backgroundColor: "transparent", borderColor: "transparent" }), buttonLast: arcCustomButton({ uniqueId: "ButtonLast" + Nr, icon: arcCustomIcon({ name: "caret-double-right", color: "#555", size: 20 }), fontSize: "18px", fontColor: "", iconColor: "", backgroundColor: "transparent", borderColor: "transparent" }) }) ``` **💡 Pro-Tipp: **Wenn du mehrere Listen auf einer Seite anzeigen willst, kannst du mit verschiedenen `uniqueId`-Werten und eigenen `fieldId`-Felder für jede Liste eine eigene Pagination bauen. So bleibt alles unabhängig. ### Fazit `arcCustomPagination` ist perfekt, um große Datenmengen elegant, performant und nutzerfreundlich zu präsentieren. Ein Muss für alle, die mit langen Listen arbeiten – egal ob im CRM, bei Projekten oder Support-Tickets. Let’s paginate smart. 🚀 --- # Progress Bar Source: https://www.arc-rider.com/docs/mini-widgets/progress-bar # ProgressBar in React Einen Teilwert im Verhältnis zu einem Gesamtwert darstellen, etwa erledigte Aufgaben oder erreichte Ziele. 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 { ArcWidgetProgressBar } from '@arcrider/arcwidgets-react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { return ( ); } ``` ## Features ausprobieren ### Zielwerte, Übererfüllung und Farben Ziehe den Wert auch über 100 Prozent. Ohne feste Farbe entscheidet das Widget anhand des Verhältnisses; die Konfiguration bleibt im Quellcode sichtbar. ```tsx import { useState } from 'react'; import { ArcWidgetProgressBar } from '@arcrider/arcwidgets-react'; export default function Example() { const [value, setValue] = useState(65); const [automatic, setAutomatic] = useState(true); return ( <>

{value}% des Ziels erreicht

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetProgressBarData`. Ü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). Dieses Widget ist eine Darstellungskomponente und stellt keine eigenen Interaktions-Callbacks bereit. ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### valueProgress Aktueller Teilwert. Typ: `number | string`. ### valueTotal Gesamtwert; muss größer als null sein. Typ: `number | string`. ### valueText Eigene Beschriftung des Fortschritts. Typ: `string`. ### progressColor Farbe des gefüllten Fortschrittsbereichs. Typ: `string`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### borderRadius Abrundung der Ecken. Typ: `string`. ### fontColor Schriftfarbe. Typ: `string`. ### fontSize Schriftgröße als CSS-Wert. Typ: `string`. ## Verhalten und Grenzen - `valueProgress` und `valueTotal` bilden das Verhältnis. Verwende einen Gesamtwert größer als null. - `valueText` ist eine eigene Beschriftung. Die Berechnung und Formatierung fachlicher Werte erfolgt in React. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetProgressBarData extends ArcWidgetBaseData { valueProgress?: number | string; valueTotal?: number | string; valueText?: string; progressColor?: string; backgroundColor?: string; width?: string; height?: string; borderRadius?: string; fontColor?: string; fontSize?: string; } ``` ## 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) # Progress Bar ## AI Defaults (read first) - **Function name**: `arcCustomProgressBar`. - **valueProgress**: Current progress (numerator), as number. - **valueTotal**: Target / total (denominator), as number. - **progressColor**: Fill color (optional; auto amber / green / black by ratio when omitted). - **backgroundColor**: Track background color. - **height**: Bar height, e.g. `"8px"`. - **valueText**: Optional label beside the bar. - Display-only — no `actions` / persist. # Progress Bar ![](https://framerusercontent.com/images/CvZw4Szz9DlFWKAz2ei35byiyk.png) Die Progress Bar besteht aus einem Anwendungscode, den du in einem Ninox Formel-Feld auf deiner Wunschoberfläche in der Datenbank einfügst. ## Gesamter Anwendungscode Folgend siehst du einen beispielhaften Anwendungscode für eine Progress Bar: ```javascript arcCustomProgressBar({ uniqueId: "", width: "", height: "", borderRadius: "", fontSize: "", fontColor: "", backgroundColor: "", progressColor: "", valueTotal: "", valueProgress: "", valueText: "" }) ``` ***uniqueId*** vergibst du individuell und sollte einzigartig auf deiner Oberfläche sein. Sinn dahinter ist: Wenn du mehrere Progress Bars mit unterschiedlichen Settings auf deiner Seite erstellst, überschrieben sich die Styles nicht. ```javascript uniqueId: "Progress 1", // Text in " ``` ***width*** gibt die Weite deiner Progress Bar an. ```javascript width: "300px", // Pixel-Werte in " width: "100%", // Prozent-Werte in " / Orientiert sich prozentual an der Breite des Formelfeldes. ``` ***height*** gibt die Höhe deiner Progress Bar an. ```javascript height: "300px", // Pixel-Werte in " height: "100%", // Prozent-Werte in " / Orientiert sich prozentual an der Breite des Formelfeldes. ``` ***borderRadius*** gibt die Abrundung der Progressbar-Ecken an. ```javascript borderRadius: "4px", // Pixel-Werte in " borderradius: "", // default: 50% ``` ***fontSize*** gibt die Schriftgröße der Beschriftung von ***valueText ***wieder. ```javascript fontSize: "14px", // Pixel-Werte in " ``` ***fontColor*** gibt die Schriftfarbe der Beschriftung von ***valueText ***wieder. ```javascript fontColor: "#FFFFFF", // HEX Code in " ``` ***backgroundColor*** definiert die Hintergrundfarbe deiner Progress Bar ```javascript backgroundColor: "#EEEEEE", // HEX Code in " ``` ***progressColor*** definiert die Farbe des Fortschrittbalkens. ```javascript progressColor: "#FFBF00", // HEX Coe in " progressColor: if Status= 2 then "#FFBF00" else "#e9ecf4", // Ninox-Formeln, -Schreibweisen & -Konfditionen möglich. Wichtig ist, dass text() bei ausgegeben wird. ``` ***valueTotal*** definiert den Gesamtwert deiner Progress Bar. Sozusagen den Nenner einer Bruchzahl. ```javascript valueTotal: 20, // Zahlenwert valueTotal: Projekt.umsatzZiel, // Referenzierte Ninox-Felder. Auch gehen Ninox-Funktionen & -Schreibweisen. ``` ***valueProgress*** definiert den bislang erreichten Wert. Den Zähler einer Bruchzahl. ```javascript valueProgress: 10, // Zahlenwert valueProgress: Projekt.umsatzAktuell// Referenzierte Ninox-Felder. Auch gehen Ninox-Funktionen & -Schreibweisen. ``` ***valueText*** gibt die Bezeichnung neben deiner Progress Bar aus. Sinnvoll ist es, hier die Werte als Text auszugeben, ```javascript valueText: Projekt.umsatzAktuell + " / " + Projekt.umsatzZiel ``` ## Standard-Werte als Fallback 💡 **Hinweis: **Trägst du keine Werte hinter die Parameter ein, so ist in den meisten Fällen ein Fallback-Wert im System hinterlegt. An manchen Stellen, wie Berechnungsfelder (Tatsächliche Zahlenwerte zum Beispiel) macht das keinen Sinn. In Style-Parametern, wie Farben oder Größen hingegen schon. Hier siehst du den Code ohne eingetragene Werte und in den Kommentaren dahinter die Standard-Werte: ```javascript arcCustomProgressBar({ uniqueId: "", // Kein Fallback width: "", // Fallback: 100% fontSize: "", // Fallback: 12px fontColor: "", // Fallbacke: #000000 (schwarz) backgroundColor: "", // Fallback: #EEEEEE progressColor: "", // Fallback: if data.progressColor = " then switch complete do case true: "#76CD3C" // gelb case false: if number(data.valueTotal) < number(data.valueProgress) then "#000" else "#FFBF00" // grün end default: "#e9ecf4" end else text(data.progressColor) end; valueTotal: "", // Kein Fallback valueProgress: "", // Kein Fallback valueText: "" // Kein Fallback }) ``` --- # Select (Dropdown) Source: https://www.arc-rider.com/docs/mini-widgets/select # Select in React Eine Option aus einer Liste wählen. Wert, sichtbare Beschriftung und aktive Einträge werden aus React-State abgeleitet. 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 { ArcWidgetSelect } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; const demoOptions = [ { value: 'open', title: t('Offen', 'Open', 'Abierto') }, { value: 'work', title: t('In Arbeit', 'In progress', 'En curso') }, { value: 'done', title: t('Erledigt', 'Done', 'Hecho') }, ]; const busyOptions = [ ...demoOptions, { value: 'wait', title: t('Wartet', 'Waiting', 'En espera') }, { value: 'hold', title: t('Pausiert', 'On hold', 'En pausa') }, ]; const options = pack === 'empty' ? [] : pack === 'busy' ? busyOptions : demoOptions; export default function Example() { const [status, setStatus] = useState(null); const all = t('Alle', 'All', 'Todos'); return ( <> option.value === status)?.title ?? all, items: options.map((option) => ({ ...option, active: option.value === status, })), reset: { title: all }, }} onChange={setStatus} />

{t('Status', 'Status', 'Estado')}: {status ?? all}

); } ``` ## Features ausprobieren ### Auswahl, Zurücksetzen und deaktivierter Zustand Wähle einen Status und prüfe das separate Badge mit derselben Auswahl. Teste anschließend die leere und deaktivierte Variante. ```tsx import { useState } from 'react'; import { ArcWidgetSelect, ArcWidgetBadge } from '@arcrider/arcwidgets-react'; export default function Example() { const [selected, setSelected] = useState(null); const [disabled, setDisabled] = useState(false); const [empty, setEmpty] = useState(false); const options = [ { id: 'open', title: 'Offen' }, { id: 'working', title: 'In Arbeit' }, { id: 'done', title: 'Erledigt' }, ]; return ( <> o.id === selected)?.title ?? 'Alle Status', emptyValue: 'Keine Optionen', items: empty ? [] : options.map((o) => ({ value: o.id, title: o.title, active: o.id === selected, })), reset: { title: 'Alle Status' }, }} onChange={setSelected} /> o.id === selected)?.title ?? 'Alle', backgroundColor: '#f4f4f5', fontColor: '#18181b', borderColor: '#e4e4e7', }} />

Auswahl: {selected ?? 'Alle'}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetSelectData`. Ü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). ### onChange Übernimmt den neuen Eingabewert in deinen State. ```typescript onChange?: (value: string | number | null) => void; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### items Einträge der Auswahl oder der Darstellung; siehe den zugehörigen Datentyp. Typ: `ArcWidgetSelectItem[]`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### itemsWidth Breite der Auswahlliste. Typ: `string`. ### itemsHeight Höhe der Auswahlliste. Typ: `string`. ### alignX Horizontale Ausrichtung. Typ: `string`. ### fontColor Schriftfarbe. Typ: `string`. ### fontSize Schriftgröße als CSS-Wert. Typ: `string`. ### fontWeight Schriftstärke. Typ: `string | number`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### borderColor Rahmenfarbe. Typ: `string`. ### borderWidth Rahmenstärke. Typ: `string`. ### borderRadius Abrundung der Ecken. Typ: `string`. ### disabled Deaktiviert die Eingabe. Typ: `boolean`. ### multiselect Hält die Liste zur Mehrfachauswahl offen; der Callback liefert einen Einzelwert. Typ: `boolean`. ### emptyValue Beschriftung bei leerer Auswahl. Typ: `string`. ### clickPosition Positionierung beim Öffnen. Typ: `string`. ### currentValue Aktueller Anzeigewert. Typ: `ArcWidgetSlot`. ### icon Optionales Symbol bzw. unterstützter Inhaltsslot. Typ: `ArcWidgetSlot`. ### placeholder Hinweis bei leerem Wert. Typ: `string`. ### focusAction Einstellungen beim Fokussieren, etwa `autoFocus`. Typ: `{ autoFocus?: boolean; [key: string]: unknown }`. ### reset Konfiguration der Rücksetzfunktion. Typ: `ArcWidgetSelectReset`. ## Verhalten und Grenzen - `items[].value` ist der Callback-Wert. `currentValue` ist die sichtbare Beschriftung; `items[].active` markiert die Auswahl. Halte alle drei aus demselben State aktuell. - `multiselect: true` hält die Liste offen. Der Callback liefert weiterhin den einzelnen angeklickten Wert; Hinzufügen oder Entfernen aus einer Auswahlmenge implementierst du selbst. - Reset liefert `null`. Behandle diesen Fall ausdrücklich. Verwende bei Select in `0.1.0-alpha.5` Text für die Optionsbeschriftungen. Direkte JSX-Elemente führen beim internen Datenvergleich zu einem Fehler; Widget-Deskriptoren werden im ausgelagerten Dropdown noch nicht gerendert. Das Live-Beispiel zeigt ein separates Badge aus demselben Auswahl-State. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetSelectItem { title?: ArcWidgetSlot; active?: boolean; value?: string | number; [key: string]: unknown; } export interface ArcWidgetSelectReset { title?: ArcWidgetSlot; value?: string | number; hide?: boolean; [key: string]: unknown; } export interface ArcWidgetSelectData extends Omit { items?: ArcWidgetSelectItem[]; width?: string; height?: string; itemsWidth?: string; itemsHeight?: string; alignX?: string; fontColor?: string; fontSize?: string; fontWeight?: string | number; backgroundColor?: string; borderColor?: string; borderWidth?: string; borderRadius?: string; disabled?: boolean; multiselect?: boolean; emptyValue?: string; clickPosition?: string; currentValue?: ArcWidgetSlot; icon?: ArcWidgetSlot; placeholder?: string; focusAction?: { autoFocus?: boolean; [key: string]: unknown }; reset?: ArcWidgetSelectReset; } export interface ArcWidgetSelectProps extends ArcReactWidgetProps { onChange?: (value: string | number | null) => void; } ``` ## Einsatzbeispiele - [Status auswählen und speichern](/docs/react/examples/status-als-buttons-ausgeben) - [Ein Formular in Schritten](/docs/react/examples/datenaufnahme-in-schritten) - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Select (Dropdown) ## AI Defaults (read first) - **uniqueId**: Required, e.g. `"select-status-" + Nr`. - **recordId**: Required for Ninox data binding, use raw `Nr`. Core does not read it — Ninox handler builds the `update` from `ui.data`. - **field**: Required for Ninox, use `fieldId(Nr, "Feldname")` (alias `fieldId` also works). Adapter-only. - **items**: Use `(select Table).[{ title: ..., active: ..., value: number(Nr) }]` – `value` MUST be `number(Nr)`. - **focusAction**: Set `{ autoFocus: false }` (important on tablets). - **reset**: `{ title: "Alle", value: "", hide: true }` to add a reset option. - **Persistence**: Selection emits semantic `change` with the item `value`; Ninox adapter writes `type: "update"`. React: `onChange(value)` / `onAction('change', value)`. - **Nested widgets**: `currentValue`, `icon`, and `items[].title` (and reset row `title`) may use `arcCustomIcon`, `arcCustomBadge`, `arcCustomButton`, or other formula widgets – not only plain text. - **Null handling**: Do NOT use `else "" end` for empty states. # Select (Dropdown) Das Widget `arcCustomSelect` ist dein Tool für moderne, stilvolle Dropdowns in Ninox. Es ersetzt das klassische Auswahlfeld durch ein vollständig anpassbares Interface – mit Icons, Hovereffekten, Tastatursteuerung und dynamischen Einträgen. ![](https://framerusercontent.com/images/sIAp1XmRalt9U4EHtgmp9IyYHuw.png) Du verwendest `arcCustomSelect` überall dort, wo du deinen Nutzern eine Auswahlmöglichkeit anbieten möchtest – sei es zur Statusvergabe, zum Filtern, oder zur Bearbeitung von Datenfeldern. Das Widget eignet sich ideal für: - Auswahl von Statuswerten (z. B. „Offen“, „In Bearbeitung“, „Abgeschlossen“) - Schnelles Tagging oder Labeling von Datensätzen - Kompakte Navigation innerhalb von Masken - Filterung in Kombination mit anderen Widgets 📢 Der große Vorteil: Du kannst das Design flexibel anpassen (z. B. Breite, Farben, Schriftgröße), dynamische Item-Listen aus Tabellen befüllen und sogar das Verhalten bei Fokus oder Klick feinsteuern. ## Gesamter Anwendungscode ```javascript arcCustomSelect({ uniqueId: "Status" + Nr, recordId: Nr, field: "", embedded: true, width: "", height: "", alignX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "", borderColor: "", borderWidth: "", borderRadius: "", itemsSettings: { width: "", height: "", borderRadius: "" }, labelSettings: { title: "", fontSize: "", alignX: "", height: "" }, clickPosition: "", placeholder: "", disabled: false, multiselect: false, reset: { title: "", value: "", hide: true }, focusAction: { autoFocus: false, showCurrentValue: false }, currentValue: text(Status), items: let id := Nr; (select Status).[{ title: Titel, active: number(id.Status) = number(Nr), value: number(Nr) }] }) ``` ## Einzelne Parameter ### uniqueId Pflichtfeld: ✅ Mit `uniqueId` wird jede Select-Instanz eindeutig identifizierbar – besonders wichtig, wenn **mehrere Custom Selects** gleichzeitig auf der Seite verwendet werden. Die ID sorgt intern dafür, dass: - **Fokus, Auswahl und Tastaturverhalten** korrekt zugeordnet werden, - keine Überschneidungen zwischen Instanzen auftreten, - der Zustand jeder Select-Box stabil bleibt, auch bei schnellem Wechsel oder dynamischem Nachladen. ```javascript uniqueId: Nr, // Ninox-Feld uniqueId: "Aufgabe Beschreibung", // Text in " ``` ### recordId Pflichtfeld: ✅ Der Parameter `recordId` verknüpft die Select-Komponente mit einem konkreten Datensatz in Ninox – und zwar genau dem, in dem sich das Feld befindet, das du mit dem Select-Widget bespielen möchtest. ```javascript recordId: Nr, // Ninox-Tabellen Id ``` ### field Pflichtfeld: ✅ Der Parameter `field` gibt an, welches Feld im angegebenen Datensatz (`recordId`) vom Select-Widget gelesen und beschrieben werden soll. Dabei handelt es sich nicht um den sichtbaren Feldnamen, sondern um die interne `fieldId` des Feldes in Ninox**.** **🔍 So findest du die fieldId** Die `fieldId` ist eine systeminterne Bezeichnung, z. B. `"K4"`. Du kannst sie auf zwei Arten ermitteln: - Mit der Ninox-Funktion: `fieldId()` - Oder mit dem kostenlosen Mini-Widget: `fieldFinder` ```javascript field: "B1", field: fieldId(Nr, "Status") field: "", // gibst du keine Id an, funktioniert das Select nicht ``` ### embedded Gibt an, ob das Select-Widget **innerhalb eines anderen Widgets eingebettet** ist – z. B. innerhalb einer [Custom Table](/custom-table)-Spalte oder eines [Layout](https://www.arc-rider.de/en/documentation/custom-layout)-Containers – oder ob es **frei in einem Ninox-Formular als eigenständiges Widget** eingebunden wird (z. B. direkt in einem Formelfeld). ```javascript embedded: false, // ermöglicht den Standalone Einsatz des Widgets (nicht eingebettet in anderen Widgets) embedded: true, // wenn Text eingebettet in anderes Widget embedded: "", // default: true ``` ### width Der Parameter `width` steuert die Darstellungsbreite des Select-Widgets. Du kannst entweder eine feste Breite in Pixeln angeben, z. B. `"240px"`, oder eine relative Breite wie `"100%"`, damit sich das Widget flexibel an das umgebende Layout anpasst. **Anwendung** - In frei platzierten Formelfeldern kannst du mit `width` gezielt das Layout beeinflussen. - Wird das Widget eingebettet verwendet (z. B. in einer [CustomTable](https://www.arc-rider.de/en/documentation/custom-table)-Spalte), kannst du die Breite automatisch an das Eltern-Element anpassen, wenn du die `width: 100% `einstellst. 💡 **Empfehlung**: Vermeide sehr kleine Breiten – lange Platzhalter oder Auswahlwerte könnten sonst abgeschnitten dargestellt werden. ```javascript width: "100%", // Prozent-Werte oder "80px" Pixel-Werte width: "auto", // width passt sich an Inhalt an width: "", // default: 100% ``` ### height Mit dem Parameter `height` kannst du die **sichtbare Höhe der Select-Komponente** steuern. Das ist besonders nützlich, wenn du ein einheitliches Layout brauchst oder die Darstellung in Tabellen und verschachtelten Layouts kontrollieren möchtest. ```javascript height: "100%", // Prozent-Werte oder "80px" Pixel-Werte height: "auto", // height passt sich automatisch an Inhalt an height: "", // default: auto ``` ### alignX Mit `alignX` bestimmst du die **horizontale Ausrichtung** des sichtbaren Inhalts im Select-Feld – also zum Beispiel, ob der ausgewählte Wert linksbündig, zentriert oder rechtsbündig dargestellt wird. **Anwendung** - `"left"` ist der Standardwert und eignet sich in den meisten Fällen. - `"center"` kann in sehr schmalen Feldern oder bei rein ikonischen Inhalten sinnvoll sein. - `"right"` wird selten verwendet, kann aber bei Zahlen oder bestimmten Layoutvorgaben hilfreich sein. **💡 Empfehlung**: Verwende `center` oder `right` nur, wenn das restliche Layout es verlangt – bei längeren Texten ist `left` in der Regel besser lesbar. ```javascript alignX: "left", // linksbündig alignX: "center", // zentriert alignX: "right", // rechtsbündig alignX: "", // default: left ``` ### itemsWidth Mit `itemsWidth` legst du die **Breite der Dropdown-Liste** fest – also des Bereichs, in dem die Auswahloptionen angezeigt werden. Wenn du diesen Wert nicht setzt, richtet sich die Breite der Liste automatisch nach der Breite des Select-Feldes. **Anwendung: **Du kannst die Dropdown-Liste bewusst breiter oder schmaler machen als das Select-Feld selbst. Das ist besonders hilfreich, wenn die Optionsinhalte länger sind als die Breite des eigentlichen Felds zulässt. ```javascript itemsWith: "100%", // Prozent-Werte oder "80px" Pixel-Werte itemsWith: "auto", // width passt sich an Inhalt an itemsWith: "", // default: 100% ``` ### itemsHeight Der Parameter `itemsHeight` legt die **Höhe einzelner Einträge in der Dropdown-Liste** fest. Damit kannst du steuern, wie viel Platz jeder Auswahlpunkt einnimmt – unabhängig von der Schriftgröße. ```javascript itemsHeight: "100%", // Prozent-Werte oder "80px" Pixel-Werte itemsHeight: "auto", // height passt sich an itemsHeight: "", // default: auto ``` ### fontColor Mit `fontColor` kannst du die **Schriftfarbe der Auswahlanzeige im Select-Feld** festlegen. Das betrifft sowohl Platzhaltertext als auch den aktuell ausgewählten Wert. **💡 Empfehlung: **Verwende möglichst hohe Kontraste zum Hintergrund – insbesondere bei eingebetteten Varianten in farbigen Layouts. Bei dunklem Hintergrund solltest du eine helle Schriftfarbe wählen, z. B. `"#ffffff"`. ```javascript fontColor: "#EEEEEE", // z.B. HEX-Wert in " fontColor: "", // default: #000 ``` ### fontSize Der Parameter `fontSize` bestimmt die **Größe des Textes innerhalb des Select-Feldes** – also sowohl für Platzhalter als auch für ausgewählte Einträge. ```javascript fontSize: "15px", fontSize: "", // default: 13px ``` ### fontWeight Der Parameter `fontWeight` bestimmt die **Schriftstärke** des Textes im Select-Feld (values & placeholder). Damit kannst du gezielt steuern, ob der Text eher dezent oder betont dargestellt werden soll. **Anwendung: **Du kannst zwischen vordefinierten Werten wie `"normal"` oder `"bold"` wählen oder numerische Gewichtungen wie `400` (normal) oder `600` (halbfett) verwenden. Das ist besonders nützlich, wenn du verschiedene Schriftstärken im Layout systematisch verwendest. ```javascript fontWeight: "700", fontWeight: "", // default: 400 ``` ### backgroundColor Der Parameter `backgroundColor` legt die **Hintergrundfarbe des sichtbaren Select-Feldes** fest – also die Fläche, in der der Platzhalter oder der ausgewählte Wert angezeigt wird. **Anwendung: **Du kannst damit das Widget visuell an deine Oberfläche anpassen, z. B. in dunklen Layouts oder bei farblich abgestimmten Eingabefeldern. Unterstützt werden Farbwerte im HEX-Format, RGB oder Klartextnamen wie `"white"` oder `"transparent"`. **💡 Empfehlung: **Achte auf ausreichenden Kontrast zur Schriftfarbe (`fontColor`), damit der Text gut lesbar bleibt. In dunklen Layouts kann `"transparent"` sinnvoll sein, wenn das Hintergrunddesign durchscheinen soll. ```javascript backgroundColor: "#4970ff", // z.B. HEX-Wert in " backgroundColor: "", // default: #FFFFFF ``` ### borderColor Mit dem Parameter `borderColor` legst du die Farbe des Rahmens rund um das Select-Feld fest. Dadurch kannst du das Widget gezielt an dein Farbschema oder Layout anpassen. **💡 Empfehlung: **Verwende kontrastreiche Rahmenfarben nur gezielt – z. B. um Felder hervorzuheben oder Gruppen optisch zu trennen. Dezente Grautöne wie `"#dddddd"` oder `"#999999"` wirken meist angenehm zurückhaltend. ```javascript borderColor: "#EEEEEE", // z.B. HEX-Wert in " borderColor: "", // default: #e5e5e5 ``` ### borderWidth Mit `borderWidth` bestimmst du die Dicke des Rahmens rund um das Select-Feld. Dadurch kannst du die visuelle Präsenz des Widgets im Layout erhöhen oder bewusst reduzieren. **Anwendung: **Setze dünne Rahmen (`"1px"`) für eine dezente Integration oder dickere (`"2px"` bis `"3px"`) für optische Hervorhebung. `"0px"` entfernt den Rahmen komplett, besonders nützlich bei eingebetteten Designs mit klaren Flächen. ```javascript borderWidth: "#EEEEEE", // z.B. HEX-Wert in " borderWidth: "", // default: 1px ``` ### borderRadius Der Parameter `borderRadius` steuert die **Abrundung der Ecken** des Select-Feldes. Damit kannst du das Erscheinungsbild der Komponente zwischen eckig, leicht abgerundet oder komplett rund gestalten. **🧩 Typische Werte** - `"0px"` → komplett eckig - `"4px"` → leicht abgerundet (Standard) - `"6px"` → modern und weich - `"50%"` → rund (nur sinnvoll bei quadratischer Breite/Höhe) ```javascript borderRadius: "20px", // z.B. Wert in px borderRadius: "", // default: 3px ``` ### itemsSettings Block Mit `itemsSettings` kannst du das Erscheinungsbild des gesamten Dropdown-Menüs steuern – nicht einzelner Listeneinträge, sondern des gesamten Bereichs, in dem alle Optionen angezeigt werden. ```javascript itemsSettings: { width: "", // default: 100% height: "", // default: 200px borderRadius: "" // default: 5px }, Einzelerklärungen: width: "40px", // Wert in px oder aber auch z.B. "auto" width: "", // default: 100% height: "200px", // Wert in px oder aber auch z.B. "auto" (passt sich Inhalt an) height: "", // default: 200px borderRadius: "30px", // Wert in px oder aber auch z.B. "auto" (passt sich Inhalt an) borderRadius: "" // default: 3px ``` ### labelSettings Block Mit `labelSettings` steuerst du die Darstellung eines optionalen **Labels über dem Select-Feld** – also einen kurzen Titel oder Hinweistext wie z. B. „Status“ oder „Kategorie“. Dieser Text erscheint oberhalb des Widgets. - `title` – Der Text, der als Label angezeigt wird (z. B. `"Kategorie"`) — auch als Mini-Widget (z. B. `arcCustomText`) - `fontSize` – Schriftgröße des Labels (z. B. `"11px"`) - `fontColor` – Schriftfarbe des Labels (Hex, z. B. `"#EEEEEE"`) - `alignX` – Horizontale Ausrichtung des Labels (`"left"`, `"center"`, `"right"`) - `height` – Höhe des Label-Containers (z. B. `"15px"`) ```javascript labelSettings: { title: "", fontSize: "", alignX: "", height: "" } Einzelerklärungen: title: "Status", // Wort, in " welches als Label ausgegeben werden soll title: "", // kein Label wird angezeigt fontSize: "30px", // z.B. Pixel Werte fontSize: "", // default: 11px alignX: "right", // Label ist rechtsbündig alignX: "left", // Label ist linksbündig alignX: "", // default: left height: "40px", // z.B. Pixel Werte height: "", // default: 15px ``` **💡 Hinweise** - Wird `title` nicht gesetzt oder leer gelassen, erscheint **kein Label**. - Die Ausrichtung (`alignX`) betrifft nur das Label, nicht das Select-Feld selbst. - `fontSize` und `height` helfen bei der exakten Abstimmung mit anderen Feldern im Layout. ### clickPosition Der Parameter `clickPosition` legt fest, **welcher Teil des Select-Feldes klickbar ist**, um das Dropdown-Menü zu öffnen. **🧩 Mögliche Einstellungen** - `"icon"` – Nur das kleine Pfeilsymbol ganz rechts ist klickbar. - `"field"` – Das gesamte Feld (inklusive Textfläche und Icon) ist klickbar. (Standard) ```javascript clickPosition: "icon", // nur auf icon klickbar (select klappt sich aus) clickPosition: "", // default: klick auf gesamtem select möglich ``` ### placeholder Der Parameter `placeholder` definiert den **Anzeigetext im Select-Feld**, solange noch **kein Wert ausgewählt wurde**. Er dient als Hinweis für den Nutzer, was im Feld erwartet wird oder welche Auswahl getroffen werden kann. **🧩 Typische Anwendungsbeispiele** - `"Bitte auswählen"` - `"Status wählen"` - `"–"` ```javascript placeholder: "select", // eigener Platzhalter placeholder: "", // default: "Auswählen" ``` Wird ein `placeholder` gesetzt, erscheint er **grau und leicht transparent**, bis eine echte Auswahl getroffen wurde. **💡 Hinweise** - Der Text wird **nicht gespeichert**, sondern dient nur zur Anzeige. - Ist bereits ein `currentValue` übergeben, wird der `placeholder` nicht angezeigt. - Halte den Text kurz – z. B. `"select"` oder `"choose"` – besonders in schmalen Feldern. ### disabled Der Parameter `disabled` legt fest, ob das Select-Feld **deaktiviert** ist – also **nicht klickbar** und **nicht bearbeitbar**. **🧩 Verhalten im deaktivierten Zustand** - Das Dropdown lässt sich nicht öffnen. - Der Mauszeiger zeigt Icon "prohibit" (Kreis mit diagonalem Strich) - Der aktuelle Wert bleibt sichtbar, kann aber nicht verändert werden. - Optisch wird das Feld leicht abgeblendet dargestellt. ```javascript disabled: true, // das select kann nich angeklickt werden disabled: if Datum < today() then true else false, // hier können eigene Abhängigkeiten definiert werden, wenn bei einer Bestimmten Bedingung das select nicht klickbar sein soll disabled: "", // default: false (das select ist anklickbar) ``` ### multiselect Der Parameter `multiselect` aktiviert den **Mehrfachauswahl-Modus** im Select-Feld. Damit können **mehrere Werte gleichzeitig** ausgewählt und in das Feld geschrieben werden – statt nur ein einzelner. **🧩 Verhalten bei aktivem **`**multiselect: true**` - Jeder Klick auf einen Eintrag fügt den Wert zur Liste hinzu (anstatt ihn zu ersetzen). - Bereits ausgewählte Werte bleiben erhalten. - Einträge können visuell als „aktiv“ markiert sein. - Die Auswahl bleibt auch nach einem Klick geöffnet, damit mehrere Werte direkt nacheinander gewählt werden können. - Die **Tastatursteuerung** unterscheidet sich leicht:

  • `Enter` bestätigt eine Auswahl, das Menü bleibt offen. - `Tab` wechselt direkt zum nächsten Feld, ohne automatisch zu bestätigen.

```javascript multiselect: true // wenn du das Select als Multi-Select nutzen möchtest multiselect: "" // default: false ``` **⚠️ Wichtig bei der Umsetzung in Ninox** In Ninox kann ein `multiselect` nicht direkt in ein **Mehrfachauswahlfeld** oder **Mehrfach-Referenzfeld** schreiben. Du musst stattdessen ein **Hilfsfeld vom Typ Text** verwenden: - Übergib dieses Hilfsfeld im Parameter `field` (bzw. `fieldId`) an das Select-Widget. - Dort wird der **Text aller ausgewählten Werte (z. B. IDs oder Namen)** gesammelt – meist als kommaseparierte Liste. - Nutze anschließend einen** „Trigger nach Änderung“** auf das Hilfsfeld, um die tatsächliche Mehrfachauswahl zu befüllen. **Trigger-Code:** ```javascript let current := this; if number(addAuftragsart) = 0 then Auftragsart := null else if contains(numbers(current.Auftragsart), number(addAuftragsart)) then let value := unique(for item in numbers(current.Auftragsart) do if number(item) = number(addAuftragsart) then 0 else item end end); Auftragsart := value[!= 0] else Auftragsart := unique(numbers(current.Auftragsart), number(addAuftragsart)) end end; addAuftragsart := null ``` ### emptyValue In dem Parameter ***emptyValue*** bestimmst du, was der Text des zurücksetzen Buttons (erstes Auswahlfeld im geöffneten dropdown/items) sein soll.(seit v1.1.0) (wird überschrieben, wenn im reset Block bei title etwas eingetragen ist)
*!! Vorerst bleibt der bisherige Einstellungs-Parameter *`*emptyValue: "zurücksetzen"*`*noch aktiv, nutzt aber bitte in Zukunft für diese Einstellungen den Parameter *`*title: "zurücksetzen"*`* im reset Block.*
```javascript emptyValue: "empty", // eigener Text emptyValue: "", // default: "(leer)") ``` ### reset Block Der `reset`-Block steuert, ob und wie im Dropdown-Menü eine **„Zurücksetzen“-Option** angezeigt wird, mit der der aktuelle Wert gelöscht oder auf einen definierten Wert zurückgesetzt werden kann. - `title` – Der angezeigte Text für den Zurücksetzen-Eintrag (z. B. `"(leer)"`, `"Zurücksetzen"`). - `value` – Der Wert, der gesetzt wird, wenn der Eintrag angeklickt wird. Wenn nicht gesetzt, wird automatisch `emptyValue` verwendet. - `hide` – Wenn `true`, wird der Reset-Eintrag **nicht angezeigt**, kann aber trotzdem intern genutzt werden. ```javascript reset: { title: "reset", value: 0, hide: true }, Einzelerklärungen: title: "reset", // eigener Text title: "", // default: Wert in Parameter emptyValue (falls emptyValue: " dann ist hier default: "(leer)") value: 0, // eigener Wert (0 = setzt den Wert auf 0 statt null zurück) value: "", // default: null (Ninox-Feld wird null gesetzt) hide: true, // select kann nicht zurückgesetzt werden hide: "", // default: false (zurücksetzen Button wird angezeigt) ``` ### focusAction Settingsblock Mit dem Settingsblock `focusAction` kannst du das Verhalten des Suchfelds im Select steuern. **Parameter:** - `autoFocus` – Bestimmt, ob der Cursor automatisch in das Suchfeld springt. Besonders für Tablets, wo die Tastatur sofort aufspringt und den User irritieren könnte, ist das Abschalten hilfreich. - `showCurrentValue` – Wenn `true`, wird der `currentValue` (z.B. deine selbst gebauten Badges via `arcCustomLayout`) links angezeigt und das Suchfeld daneben. Ideal für Multi-Select mit Tag-Darstellung. ```javascript focusAction: { autoFocus: false, // true or false // default: true showCurrentValue: true // true or false // default: false } ``` **🧩 Typische Anwendung für showCurrentValue:** Wenn du im `currentValue` eigene Badges/Tags mit `arcCustomLayout` renderst und diese beim Öffnen des Selects weiterhin sichtbar bleiben sollen (mit Suchfeld daneben), setze `showCurrentValue: true`. ### currentValue Der Parameter `currentValue` legt fest, **welcher Wert im Select-Feld bei ausgewählten items** sein soll. Er bestimmt also die **Anzeige** des Feldes im geschlossenen Zustand. **🧩 Verhalten je nach Modus** - **Single-Select (**`**multiselect: false**`**)** Der Wert wird **direkt als einzelner Text oder Zahl** übergeben (z. B. `"Offen"` oder `1`). - **Multi-Select (**`**multiselect: true**`**)** Der Wert ist ein **Array von Strings oder Zahlen**, z. B. `[Bluse, Kleidung, Oberteil]` oder `[1, 2]`. Jeder enthaltene Wert wird in der Liste als **„aktiv“** markiert. ```javascript currentValue: text(Status), // Bezeichnung deines Ninox-Feldes innerhalb von text() currentValue: arcCustomBadge({ uniqueId: "customBadge" + Nr, embedded: true, width: "100%", icon: "", iconPosition: "", fontSize: "", fontWeight: "700", borderRadius: "", singleColor: color(Status), fontColor: "", backgroundColor: "", borderColor: "", paddingY: "5px", paddingX: "10px", value: text(Status) }), // individuell gestaltetes customBadge mit der Bezeichnung deines Ninox-Feldes bei value currentValue: "", // default: es wird nicht der ausgewählte Wert angezeigt, sondern der placeholder ``` ### items Block **Pflichtfeld:** ✅ Der `items`-Block definiert die **Auswahlmöglichkeiten**, die im Dropdown angezeigt werden. Jede Option wird als Objekt mit bestimmten Eigenschaften angegeben – z. B. sichtbarer Text, interner Wert und aktiver Zustand. **🔄 **Zwei Aufbauvarianten: Du kannst das `customSelect` auf zwei verschiedene Arten mit Werten befüllen: 1. **Dynamisch aus einer Ninox-Tabelle** Ideal für Listen, die sich ändern können oder mit anderen Feldern verknüpft sind. ```javascript items: let id := Nr; (select Status_Projekte).[{ title: Titel, active: number(id.Status) = number(Nr), value: number(Nr) }] ``` 2. **Manuelle Liste im Code** Geeignet, um z. B. ein einfaches Auswahlfeld aus Ninox direkt nachzubauen, bzw. anzusteuern. (ohne externe Tabelle). ```javascript items: [{ title: "Offen", active: Status = 1, value: number(Nr) }, { title: "In Arbeit", active: Status = 2, value: number(Nr) }, { title: "Erledigt", active: Status = 3, value: number(Nr) }] ``` **🧩 Struktur pro Eintrag** - `title` – Anzeige im Dropdown (Text oder Widget, z. B. `arcCustomIcon({ name: "check", iconSize: "16px" })` neben Label-Text) - `value` – Der zu speichernde Wert - `active` – Wann wird dieser Eintrag als ausgewählt markiert 💡 **`icon`** (am Select-Root) und **`currentValue`** unterstützen ebenfalls eingebettete Mini-Widgets – dieselbe Regel wie bei `items[].title`. ## 🎹 Tastaturverhalten im Vergleich
Taste Single-Select Verhalten Multi-Select Verhalten
**Tab** ✅ Auswahl wird bestätigt➡️ Fokus springt weiter ➡️ Fokus springt weiter🚫 Keine Bestätigung
**Enter** ✅ Auswahl wird bestätigt🔒 Select wird geschlossen ✅ Auswahl wird bestätigt🔓 Select bleibt geöffnet
**Escape** ❌ Select wird geschlossen ❌ Select wird geschlossen
## ✅ Fazit Das `customSelect`-Widget ist eine vielseitige Lösung für individuelle Auswahlfelder in Ninox – von einfachen Einzelauswahlen bis zu dynamischen Multi-Selects mit Hilfslogik. Nutze es, wenn du mehr Kontrolle über Darstellung, Verhalten und Interaktion brauchst als Ninox von Haus aus bietet. Mit klaren Parametern, flexiblen Datenquellen und voller Tastatursteuerung lässt sich das Widget nahtlos in jedes Formular oder Layout integrieren. --- ## Feature-Idee: Nice Select (moderner nativer Select) 💡 **Geplante Erweiterung:** Inspiration von [Nice Select](https://nerdy.dev/nice-select#try-it) – `appearance: base-select`, `::picker()`, Anchor Positioning, Spring-Animationen, sticky Gruppentitel, Squircle-Ecken. [CodePen](https://codepen.io/editor/argyleink/pen/019c1f28-bbc2-7bac-ad4a-a7e41d3730f1). --- # Short Numbers Source: https://www.arc-rider.com/docs/mini-widgets/short-numbers # Short Numbers ## AI Defaults (read first) - **Function name**: `shortNumbers(value)` – helper function, not a widget. - **Usage**: Formats large numbers: `1200` → `"1.2K"`, `3500000` → `"3.5M"`. - **Use in KPI cards**: Always wrap large numeric values with `shortNumbers()`. # Short Numbers ![](https://framerusercontent.com/images/zU4RZnOHOYqQGZD2v82pBREYQns.png) Short Numbers ist eine Funktion, die es ermöglicht, lange Zahlen abzukürzen und so in einem komplexen Interface übersichtlicher darzustellen. ## Funktion Die Funktion besteht aus einem Parameter, in den die darzustellende Zahl kommt. ```javascript shortNumbers(") // Gibt so nichts aus, zeigt aber dass die Funktion einen Parameter verlangt. shortNumbers(463299) // Zahlenwert fix reinschreiben shortNumbers(Projekt.Umsatz) // Ninox-Feld referenzieren. Es muss ein Zahlenfeld sein, bzw. einen Zahlenwert ausgeben. ``` --- # Signature Source: https://www.arc-rider.com/docs/mini-widgets/signature # Signature in React Eine Unterschrift auf einer Zeichenfläche erfassen und als Bild an deine Anwendung übergeben. Das Beispiel speichert lokal im Browser. 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 { ArcWidgetSignature } from '@arcrider/arcwidgets-react'; import { useState } from 'react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { const [value, setValue] = useState( () => localStorage.getItem('arc-demo-signature') ?? '', ); const [status, setStatus] = useState(t('Bereit', 'Ready', 'Listo')); return ( <> { try { localStorage.setItem('arc-demo-signature', url); setValue(url); setStatus( t('Lokal gespeichert', 'Saved locally', 'Guardado en local'), ); } catch { setStatus( t('Speichern fehlgeschlagen', 'Save failed', 'Error al guardar'), ); } }} onClear={() => { localStorage.removeItem('arc-demo-signature'); setValue(''); setStatus(t('Gelöscht', 'Cleared', 'Borrado')); }} />

{status}

); } ``` ## Features ausprobieren ### Zeichnen, Nur-Lesen und Löschen Unterschreibe mit Maus oder Touch und schalte auf Nur-Lesen. Dieses Beispiel speichert ausschließlich im React-State. ```tsx import { useState } from 'react'; import { ArcWidgetSignature } from '@arcrider/arcwidgets-react'; export default function Example() { const [value, setValue] = useState(''); const [readonly, setReadonly] = useState(false); const [width, setWidth] = useState(2); const [status, setStatus] = useState('Noch keine Unterschrift'); return ( <> { setValue(url); setStatus('Unterschrift im React-State gespeichert'); }} onClear={() => { setValue(''); setStatus('Unterschrift gelöscht'); }} />

{status}

); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetSignatureData`. Ü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). ### onSave Die gezeichnete Unterschrift als Bild-Data-URL. ```typescript onSave?: (dataUrl: string) => void | Promise; ``` ### onClear Die Unterschrift wurde geleert. ```typescript onClear?: () => void | Promise; ``` ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### value Anzuzeigender bzw. aktuell bearbeiteter Wert. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### autoSaveDelay Verzögerung vor `onSave` in Millisekunden. Typ: `number`. ### lineColor Farbe der gezeichneten Linie. Typ: `string`. ### lineWidth Stärke der gezeichneten Linie. Typ: `number`. ### backgroundColor Hintergrundfarbe. Typ: `string`. ### placeholder Hinweis bei leerem Wert. Typ: `string`. ### readonly Zeigt den Inhalt ohne Bearbeitung an. Typ: `boolean`. ### clearLabel Beschriftung zum Leeren. Typ: `string`. ### editLabel Beschriftung zum Bearbeiten. Typ: `string`. ## Verhalten und Grenzen - `onSave` liefert eine Bild-Data-URL; `onClear` meldet das Leeren. Halte `data.value` mit dem gespeicherten Stand synchron. - `autoSaveDelay` gibt die Verzögerung nach dem Zeichnen in Millisekunden an. `readonly` verhindert Bearbeitung. - Für dauerhafte oder geräteübergreifende Speicherung ersetze den lokalen Speicher durch deine API. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetSignatureData extends ArcWidgetBaseData { value?: string; height?: string; autoSaveDelay?: number; lineColor?: string; lineWidth?: number; backgroundColor?: string; placeholder?: string; readonly?: boolean; clearLabel?: string; editLabel?: string; } export interface ArcWidgetSignatureProps extends ArcReactWidgetProps { onSave?: (dataUrl: string) => void | Promise; onClear?: () => void | Promise; } ``` ## Einsatzbeispiele - [State und Events](/docs/react/patterns/react-state-events) - [Fehlerbehebung](/docs/react/faq/react-troubleshooting) # Signature (Unterschrift) ## AI Defaults (read first) - **uniqueId**: Pflicht, z. B. `"signature-" + Nr`. - **recordId**: Raw `Nr` des Datensatzes (nicht `number(Nr)`). - **fieldId**: `fieldId(Nr, "Feldname")` – **Textfeld** für die Data-URL (`data:image/png;base64,...`). - **value**: Aktueller Feldinhalt aus der Formel übergeben, z. B. `Unterschrift`, damit Vorschau und Ansicht funktionieren. - **height**: z. B. `"200px"` oder `"100%"` im umgebenden Layout. - **autoSaveDelay**: Millisekunden Debounce nach jedem Strich-Ende (`pointerup`); Standard `600`. `0` = sofort speichern nach Strich. - **Persistence**: Speichern/Löschen über **Action Performer** (`type: "update"`). React: `onAction` — kein globales `database`. # Unterschrift zeichnen Das Widget `arcCustomSignature` zeichnet eine Unterschrift mit **Maus, Touch oder Stift** (Pointer Events) auf einem HTML-Canvas, exportiert sie als **PNG-Data-URL** und speichert sie per Action Performer (`update`) im angegebenen Textfeld. Zwischen den Strichen wird **debounced** gespeichert (kein separater Speichern-Button nötig). ## Anwendungscode ```javascript arcCustomSignature({ uniqueId: "signature-" + Nr, recordId: Nr, fieldId: fieldId(Nr, "Unterschrift"), value: Unterschrift, height: "220px", lineColor: "#000000", lineWidth: 2, backgroundColor: "#FFFFFF", placeholder: "Hier unterschreiben", readonly: false, autoSaveDelay: 600, clearLabel: "Löschen", editLabel: "Neu unterschreiben" }) ``` ## Parameter | Parameter | Beschreibung | |-----------|----------------| | `uniqueId` | Eindeutige ID (HTML-`id`). | | `recordId` | Datensatz, der aktualisiert wird. | | `fieldId` | Ziel-Textfeld (Data-URL-String). | | `value` | Optional: aktueller Wert für Anzeige/Vorschau. | | `height` | Mindesthöhe des Widgets. | | `lineColor` | Strichfarbe (Hex). | | `lineWidth` | Strichbreite in CSS-Pixeln. | | `backgroundColor` | Hintergrund der Zeichenfläche. | | `placeholder` | Hinweistext bei leerer Fläche. | | `readonly` | `true`: nur Anzeige, keine Eingabe. | | `autoSaveDelay` | Debounce in ms nach `pointerup`; `0` = ohne Verzögerung. | | `clearLabel` | Tooltip und `aria-label` für das **Papierkorb-Icon** oben rechts (Zeichenmodus). | | `editLabel` | Tooltip und `aria-label` für das **Refresh-Icon** oben links (Ansichtsmodus mit gespeicherter Signatur). | ## Verhalten - **Ansicht**: Ist `value` eine gültige `data:image/…`-URL, wird die Unterschrift als Bild angezeigt; das **Refresh-Icon** oben links öffnet die Zeichenfläche erneut. - **Speichern**: Nach jedem abgeschlossenen Strich wird nach Ablauf von `autoSaveDelay` still im Hintergrund gespeichert (keine Statuszeile im Widget). - **Löschen**: **Papierkorb-Icon** oben rechts auf der Zeichenfläche – leert die Fläche und setzt das Feld auf `null` (sofort, ohne Debounce). ## NinoxScript-Hinweise - Feld für die Signatur als **Text** anlegen (lange Data-URL). - `fieldId` immer mit `fieldId(Nr, "…")` setzen. - `value: Unterschrift` (Anzeigename) verwenden, damit das Widget den gespeicherten Inhalt kennt. --- # Sort Source: https://www.arc-rider.com/docs/mini-widgets/sorting # Sort ## AI Defaults (read first) - **Function name**: `arcCustomSorting`. - **uniqueId**: Required, e.g. `"sort-" + Nr`. - **recordId**: Required, use raw `Nr`. - **field**: Required, the field storing the current sort value. - **items**: Array of sort options with `title` and `value`. # Sort ![](https://framerusercontent.com/images/dOkxeX2Wt2QzZSAnYgYHLiUL5EY.png) Um deine Daten easy per One-Klick zu sortieren, hilft dir das Sort-Mini-Widget. Dieses Mini Widget ist aktuell einsetzbar in der [Custom Table](/documentation/custom-table). Hier kannst du das Mini-Widget Sort als ***value*** einbinden. ## Gesamter Anwendungscode ```javascript arcCustomSort({ uniqueId: Nr, editable: true, recordId: current.Nr, fieldId: "H9", // Hier trägst du die Field Id von dem Feld ein, dass du triggerst. value: Position, valuePrefix: number(Nr) + ",", size: "", color: "", showIconsAlways: true, iconStart: "", // Fallback: "caret-up" | Hier fügst du das Custom Icon deiner Wahl ein. iconEnd: "", // Fallback: "caret-down" | Hier fügst du das Custom Icon deiner Wahl ein. }) ``` ***editable*** definiert, ob du in das Input-Feld etwas eingeben kannst oder nicht. ```javascript editable: true, editable: false, editable: "", // Fallback: true ``` ***value*** ist der Wert, der als Grundlage für das ***iconStart*** und ***iconEnd*** getriggert wird. ```javascript value: Position, // Hier sprichst du ein Ninox-Feld, wie beispielsweise ein Position-Zahlenfeld, an. value: "", // Kein Fallback-Wert ``` ***showIconAlways*** definiert die Sichtbarkeit deiner Icons, die bei der Sortierung angezeigt werden. ```javascript showIconsAlways: true, // Icons werden immer angezeigt. showIconsAlways: false, // Icons werden nur beim Hovern mit der Maus 🐭 angezeigt. showIconsAlways: "", // Fallback: false ``` ***valuePrefix*** ist für komplexe Eingaben mit Ninox-Hilfsfeldern gedacht. Ein Beispiel findest du in dem [Ninox-Webinar](https://youtu.be/knbcKbWVEUQ?si=I5GRrp3-QpeQrlVn&t=2395). ```javascript valuePrefix: number(Nr) + ",", ``` --- # Text Source: https://www.arc-rider.com/docs/mini-widgets/text # Text in React Text oder React-Inhalte mit einheitlicher Typografie und Ausrichtung 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 ```tsx import { ArcWidgetText } from '@arcrider/arcwidgets-react'; const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de; export default function Example() { return ( ); } ``` ## Features ausprobieren ### Ellipsis, Ausrichtung und Typografie Die feste Breite macht den Unterschied zwischen gekürztem und mehrzeiligem Text sichtbar. ```tsx import { useState } from 'react'; import { ArcWidgetText } from '@arcrider/arcwidgets-react'; export default function Example() { const [truncate, setTruncate] = useState(true); const [size, setSize] = useState(16); const [align, setAlign] = useState('left'); return ( <>
); } ``` ## Komponenten-Props ### id Optionale, stabile ID der Widget-Instanz. Ohne Angabe erzeugt React eine ID. ### data Konfiguration vom Typ `ArcWidgetTextData`. Ü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). Dieses Widget ist eine Darstellungskomponente und stellt keine eigenen Interaktions-Callbacks bereit. ## Konfiguration Die folgenden Felder gehören in `data`. Größen werden als Strings angegeben, Zahlen und Zeitwerte entsprechend dem jeweiligen Typ. ### value Anzuzeigender bzw. aktuell bearbeiteter Wert. Typ: `ArcWidgetSlot`. ### width Breite des Widgets als Größenangabe. Typ: `string`. ### height Höhe des Widgets, z. B. `"320px"`. Prozentwerte benötigen einen Elterncontainer mit festgelegter Höhe. Typ: `string`. ### alignX Horizontale Ausrichtung. Typ: `'left' | 'right' | 'center' | string`. ### alignY Vertikale Ausrichtung. Typ: `'top' | 'bottom' | 'center' | string`. ### fontSize Schriftgröße als CSS-Wert. Typ: `string`. ### fontWeight Schriftstärke. Typ: `string | number`. ### fontColor Schriftfarbe. Typ: `string`. ### lineHeight Zeilenhöhe. Typ: `string`. ### textAlign Textausrichtung. Typ: `'left' | 'right' | 'center' | 'justify' | string`. ### textWrap Umbruchverhalten. Typ: `string`. ### textScroll Scrollverhalten für Text. Typ: `boolean | string`. ### textDirection Schreibrichtung. Typ: `'horizontal' | 'vertical' | string`. ### textDecoration Textdekoration. Typ: `ArcWidgetTextDecoration`. ### truncate Kürzt überlangen Text bei begrenzter Breite. Typ: `boolean`. ## Verhalten und Grenzen - `value` unterstützt JSX. Rich Text aus eigener Quelle ist möglich; für fremde Inhalte ist ein React-Textelement die sichere Wahl. - `truncate` kürzt lange Texte. Eine begrenzte Breite ist dafür Voraussetzung. 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. Indexsignaturen erlauben Erweiterungen; sie sind keine Zusage, dass beliebige Felder im Widget ausgewertet werden. ```typescript export interface ArcWidgetTextDecoration { type?: string; style?: string; color?: string; thickness?: string; } export interface ArcWidgetTextData extends ArcWidgetBaseData { value?: ArcWidgetSlot; width?: string; height?: string; alignX?: 'left' | 'right' | 'center' | string; alignY?: 'top' | 'bottom' | 'center' | string; fontSize?: string; fontWeight?: string | number; fontColor?: string; lineHeight?: string; textAlign?: 'left' | 'right' | 'center' | 'justify' | string; textWrap?: string; textScroll?: boolean | string; textDirection?: 'horizontal' | 'vertical' | string; textDecoration?: ArcWidgetTextDecoration; truncate?: boolean; } ``` ## 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) # Text ## AI Defaults (read first) - **Function name**: `arcCustomText`. - **value**: The text content. Supports NinoxScript expressions. - **Use for type consistency**: When `if/else` mixes text and widget, wrap the text branch in `arcCustomText({value: ...})`. - **fontSize**, **fontColor**, **fontWeight**: Direct properties (not nested). - **truncate**: `true` enables ellipsis when the text exceeds the container width (set an explicit `width`). - **No uniqueId needed** for simple text display. # Text ![](https://framerusercontent.com/images/QPDhVJgrgK68jBZ7OahdkAomXYQ.png) ## Anwendungscode ```javascript arcCustomText({ uniqueId: " + Nr, embedded: true, width: "", height: "", alignY: "", alignX: "", fontSize: "", fontWeight: "", fontColor: "", lineHeight: "", textWrap: "", textScroll: "", textAlign: "", textDirection: "horizontal", textDecoration: { type: "", style: "", color: "", thickness: "" }, truncate: "", value: "" }) ``` ### uniqueId Die*** **uniqueId* vergibst du individuell, sie muss einzigartig sein. Sinn dahinter ist: Wenn du mehrere customText mit unterschiedlichen Settings auf deiner Oberfläche anzeigst, überschreiben sich die Styles nicht. ```javascript uniqueId: Nr, // Ninox-Feld uniqueId: "Aufgabe Beschreibung", // Text in " ``` ### embedded Den Parameter ***embedded*** nutzt du, wenn du der Text ***nicht ***innerhalb eines anderen Widgets liegt. Standardmäßig ist dies aber auf true gesetzt. ```javascript embedded: false, // ermöglicht den Standalone Einsatz des Widgets (nicht eingebettet in anderen Widgets) embedded: true, // wenn Text eingebettet in anderes Widget embedded: "", // default: true ``` ### width Bei ***with*** definierst du, wie breit der Container deines customText sein soll. ```javascript width: "100%", // Prozent-Werte oder "80px" Pixel-Werte width: "auto", // witdh passt sich an Inhalt an width: "", // default: 100% ``` ### height Bei ***height*** definierst du, wie hoch der Container deines customText sein soll. ```javascript height: "100%", // Prozent Werte height: "100px", // Pixel Werte height: "", // default: auto ``` ### alignY Den Parameter ***alignY*** nutzt du, um zu bestimmen, ob der Container des customText auf der Y-Achse oben, unten oder mittig angeordnet sein soll. ```javascript alignY: "bottom", // top, bottom, center alignY: "", // default: top ``` ### alignX Den Parameter ***alignX*** nutzt du, um zu bestimmen, ob der Container des customText auf der X-Achse links, rechts oder mittig angeordnet sein soll. ```javascript alignX: "right", // left, right, center alignX: "", // default: left ``` ### fontSize Mit ***fontSize*** bestimmst du die Schriftgröße deines Textes. ```javascript fontSize: "12px", // Pixel Werte fontSize: "", // default: 12px ``` ### fontWeight Bei*** fontWeight*** kannst du die Dicke deiner Schrift definieren. ```javascript fontWeight: "normal", // CSS-Values (bold, lighter etc.) fontWeight: "800", // Zahl 100-800 in 100er Schritten (400=normal, 800=bold) fontWeight: "", // default: normal/400 ``` ### fontColor Mit*** fontColor*** definierst du die Schriftfarbe deines customText. ```javascript fontColor: "#000000", // HEX Werte fontColor: "rgb(77,166,255)", // RGB Werte fontColor: "", // default: #000 ``` ### lineHeight Mit*** lineHeight ***bestimmst du den Abstand zwischen den Textzeilen. ```javascript lineHeight: "5px", // fixe line-height in px, pt, cm etc. lineHeight: "1.5", // punkt-getrennte Zahl, multipliziert mit font-size lineHeight: "150%", // line-height in Prozent von font-size lineHeight: "", // default: normal ``` ### textWrap Mit textWrap bestimmst du, wie dein Text umgebrochen werden soll. Du kannst gängige CSS Values für white-space nutzen. ```javascript textWrap: "normal", // automatischer Zeilenumbruch, wenn verfügbaren Platz überschritten textWrap: "nowrap", // Text bleibt in einer Zeile, auch wenn verfügbarer Platz überschritten textWrap: "pre", // Text wird genau so angezeigt wie im HTML-Code, mit Leerzeichen und Zeilenumbrüchen textWrap: "pre-wrap", // Text wird umgebrochen, wenn verfügbarer Platz überschritten, behält Leerzeichen und Zeilenumbrüche bei textWrap: "pre-line", // Text wird wie "pre-wrap" behandelt, aber behält normale Zeilenumbrüche bei und entfernt zusätzliche Leerzeichen textWrap: "", // default: normal ``` ### textScroll Mit ***textScroll*** kannst du mit true oder false einstellen, ob der Text, wenn er mehr Platz braucht, als der Container hergibt (bestimmst du mit width und height), scrollbar sein soll oder abgeschnitten sein soll. ```javascript textScroll: true, // Der Text wird scrollbar, wenn er den Container überschreitet. textScroll: false, // Der Text wird abgeschnitten, wenn er den Container überschreitet. textScroll: "", // default: true ``` ### textAlign Bei ***textAlign*** bestimmst du wie der Text angeordnet sein soll, links- oder rechtsbündig, zentriert oder über die Zeile verteilt. ```javascript textAlign: "right", // left, right, center textAlign: "justify", // streckt den Text so, dass jede Linie dieselbe Breite hat (wie zB in Zeitungen) textAlign: "", // left ``` ### textDirection Mit dem Parameter ***textDirection ***kannst du bestimmen, ob der Text horizontal oder vertical ausgerichtet sein soll. ```javascript textDirection: "vertical", // vertical, horizontal textDirection: "", // horizontal ``` ### textDecoration Bei ***textDecorationSettings*** kannst du mit dem Parameter type deinen Text Unter- und Durchstreichen bzw. eine Linie über, und auch sowohl unter als auch über, dem Text erzeugen. Außerdem kannst du diese Linie mit style, color und thickness individuell stylen. ```javascript textDecoration: { type: "", style: "", color: "", thickness: "" } ``` Der Parameter ***type*** bestimmt die Art der Linie bzw. die Position. ```javascript type: "none", // keine Linie type: "overline", // Linie oberhalb des Textes type: "line-throug", // Text wird durchgestrichen type: "underline", // Text wird unterstrichen type: "overline underline", // Erzeugt eine Linie ober- und unterhalb des Textes type: "", // default: keine Linie ``` Der Parameter ***style*** bestimmt den style der Linie. ```javascript style: "solid", // gerade Linie style: "double", // doppelte Linie style: "dotted", // gepunktete Linie style: "dashed", // gestrichelte Linie style: "wavy", // gewellte Linie style: "", // default: solid ``` Der Parameter ***color*** bestimmt die Farbe der Linie. ```javascript color: "#fff", // hex-code Farben color: "rgb(249, 201, 36)", // rgb() Farben color: "", // default: #000 ``` Der Parameter ***thickness*** bestimmt die Dicke der Linie. ```javascript thickness: "7px", // thickness: "", // default: 1px ``` ### truncate Mit ***truncate*** kannst du langen Text mit Ellipsis (`…`) abschneiden, wenn er breiter ist als der Container (typisch mit fester `width`). ```javascript truncate: true, // Text wird einzeilig abgeschnitten mit Ellipsis truncate: false, // kein Truncate (default) truncate: ", // default: false ``` ### value Bei ***value*** setzt du den Wert ein, der als customText ausgegeben werden soll. Das kann entweder geschriebener Text in " sein oder ein Ninox Feld. Fall es kein Ninox-Textfeld ist, denk dran den Wert in *text()* einzubinden. ```javascript value: "Dies ist mein customText.", // manuell eingefügter Text value: Beschreibung, // Ninox Wert value: "", // gibst du keinen value ein, wird nichts ausgegeben ``` #### Tipp für schmale Spalten: Bei der customTable kannst du nun mithilfe des customText die Spalten-Titel vertical darstellen, so sparst du Platz bei Zellen, die zB nur eine Checkbox beinhalten und stellst Elementen, die mehr Raum benötigen, dementsprechend mehr zur Verfügung💡 ![](https://framerusercontent.com/images/IzGhHmFNjffftaps5KTEwHMzwSc.png) --- ## Feature-Idee: Stifthervorhebung (Handwritten Highlight) 💡 **Geplante Erweiterung:** Text mit einem realistischen „Textmarker“-Effekt versehen (wie handgeschrieben markiert). Referenz: [CodePen](https://codepen.io/vii120/pen/KwMBaaY) – Varianten: gelb, grün, pink mit `conic-gradient` + `linear-gradient`. --- # Farben und Themes in React Source: https://www.arc-rider.com/docs/themes/react-theme # Farben und Themes in React Leite die Widget-Farben aus dem Theme deiner React-Anwendung ab. Die verfügbaren Style-Felder unterscheiden sich pro Widget: Table verwendet unter anderem `styles`, `header` und Zellfarben; Button verwendet direkte Farben in `data`. ## Ein gemeinsamer Farbsatz ```tsx import { useState } from 'react'; import { ArcWidgetTable } from '@arcrider/arcwidgets-react'; export default function Example() { const [dark, setDark] = useState(false); return ( <> ); } ``` ## Zusammengesetzte Oberflächen Gib denselben Farbsatz auch an JSX-Inhalte wie Badges, Buttons und Fortschrittsanzeigen weiter. Ein äußerer Dark-Mode-Schalter überschreibt nicht automatisch alle expliziten Widget-Farben. Prüfe Kontrast sowie Hover-, Auswahl- und Leerzustände. Die historische Theme-Sammlung für andere Plattformen ist keine Sammlung importierbarer React-Komponenten. Dieses Muster funktioniert ohne zusätzliche Theme-Exporte. --- # Theme: Clean White Source: https://www.arc-rider.com/docs/themes/custom-table-clean-white # Theme: Clean White # Theme "Clean White" Das Theme Clean White kannst du in dem Widget [Custom Table ](/custom-table)anwenden. Hier gibst du unter dem Parameter "theme" folgenden Anwendungscode ein: ## Anwendungscode ```javascript arcCustomThemeCleanWhite({ uniqueId: "Projektliste Demo 1" }) ``` ## Beispiel Hier siehst du du ein Beispiel-Bild für das Theme Clean White. Die Spalten werden ohne Trennstriche dargestellt. Auch die linke und rechte Border sind ausgelassen. Lediglich die Zeilen werden durch eine feine Border getrennt. ![](https://framerusercontent.com/images/uUWowsTh220sClRN9Ar8zVa5xYE.png) --- # Theme: Naked Source: https://www.arc-rider.com/docs/themes/theme-naked # Theme: Naked # Theme "Naked" ![](https://framerusercontent.com/images/Satd30fzY0art6F5jdYlPS0MCo.png) ## Gestalte deine Tabelle individuell: Das Theme "Naked" im Widget Custom Table Das Theme Naked bietet dir die Möglichkeit, deine Tabelle in einem völlig neuen Design zu präsentieren. Mit diesem Theme kannst du auf klassische Elemente wie Rahmenlinien, Header oder Footer verzichten und so ein minimalistisches Erscheinungsbild schaffen. Perfekt für alle, die ihre Daten auf das Wesentliche reduziert und stilvoll präsentieren möchten. ## Was macht das Theme "Naked" besonders? Die Funktionalität bleibt trotz des minimalistischen Designs vollständig erhalten – Hover-Aktionen oder Click-Actions beispielsweise funktionieren wie gewohnt. Zusätzlich kannst du Zwischenabstände (gap) individuell anpassen, sowohl zwischen den einzelnen Zeilen (rows) als auch den Spalten (columns). Das verleiht deiner Tabelle nicht nur Struktur, sondern sorgt auch für ein aufgeräumtes und modernes Layout. ## So wendest du das Theme an Um das Theme "Naked" in deiner [Custom Table](/custom-table) zu nutzen, fügst du folgenden Anwendungscode hinter dem Paramter `theme` ein:
*Wichtig: Die uniqueId des Themes muss dieselbe sein, wie die uniqueListId der Table, in der das Theme eingesetzt wird !*
## Anwendungscode ```javascript arcCustomThemeNaked({ uniqueId: "Aufgaben Demo 1" + Nr, rowSettings: { gap: "10px", borderRadius: "" }, columnSettings: { gap: "40px" } }), ``` ## Beispiel Im folgenden Beispiel siehst du den Anwendungscode des oben dargestellten Bildes. Dieser umfasst eine Custom Table, die Termine von Mitarbeitern darstellt. Hier sind sowohl Header als auch Footer ausgeblendet. Durch das Theme sind keinerlei Rahmenlinien in der Tabelle zu sehen und die einzelnen Zeilen sind durch ein gap von 10px getrennt. ```javascript let current := this; let list := (select Termine from 0 to 30); let data := { uniqueListId: "Aufgaben Demo 1" + Nr, tableId: tableId(first(list)), height: "620px", width: "100px", groupedBy: "", header: { showHeader: false }, theme: arcCustomThemeNaked({ uniqueId: "Aufgaben Demo 1" + Nr, rowSettings: { gap: "10px" }, columnSettings: { gap: "10px" }, borderRadius: "" }), embedded: false, table: list.[{ recordId: Nr, rowColor: "#ededed", rowHeight: "auto", rowPaddingY: "10px", groupRowColor: "", columns: [{ field: "Kategorie Icon", title: "", width: "35px", color: "", backgroundColor: "", align: "left", paddingX: "", paddingY: "", value: arcCustomIcon({ iconSet: "phosphor", name: "circle-fill", iconSize: "25px", color: record('Kategorie Aufgabe',number(Kategorie)).Farbe }), actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "Feldname für Feld-ID", title: "", color: "", backgroundColor: "", width: "", align: "left", paddingX: "", paddingY: "", value: arcCustomLayout({ comment: "---------------------------------- LAYOUT TITEL UND KATEGORIE ----------------------------------", uniqueId: "Titel und Kategorie " + Nr, embedded: true, direction: "vertical", alignX: "left", alignY: "center", width: "100%", height: "", gap: "2px", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomText({ uniqueId: "Bezeichnung " + Nr, fontSize: "13px", fontWeight: "600", fontColor: "", value: Auftrag.last(Aufgaben.Aufgabe) }) }, { width: "", height: "auto", lineHeight: "", alignX: "left", color: "", value: arcCustomText({ uniqueId: "Bezeichnung " + Nr, fontSize: "13px", fontWeight: "", fontColor: "#9aa3af", value: text(Kategorie) }) }] }), actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "", title: "", width: "", color: "", backgroundColor: "", align: "left", paddingX: "", paddingY: "", value: arcCustomLayout({ comment: "---------------------------------- LAYOUT UHRZEIT & DATUM ----------------------------------", uniqueId: "Datum und Uhrzeit " + Nr, embedded: true, fullscreen: false, ninoxVersion: "", page: true, fullscreenMode: "", showAdminTools: true, hideHeaderIcons: true, direction: "horizontal", alignX: "left", alignY: "center", width: "", height: "", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomIcon({ iconSet: "phosphor", name: "clock", iconSize: "25px", color: "#9aa3af" }) }, { width: "fraction", height: "auto", lineHeight: "", alignX: "left", color: "", value: arcCustomText({ uniqueId: "Bezeichnung " + Nr, fontSize: "13px", fontWeight: "600", fontColor: "#9aa3af", value: 'Datum von' + ", " + Von + " - " + Bis }) }] }), actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "Mitarbeiter", title: "", width: "", color: "", backgroundColor: "", align: "left", paddingX: "", paddingY: "", value: Mitarbeiter.'Bild-und_Name', actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }] }], footer: { showFooter: false } }; arcCustomTable(data) ``` --- # Ninox: Wie baue ich Builds ein und wie laufen Updates? Source: https://www.arc-rider.com/docs/faq/wie-man-builds-einbaut-und-updatet # Ninox: Wie baue ich Builds ein und wie laufen Updates? ## Was ist ein Build? Ein **Build** ist das fertige Code-Paket für dein Ninox-Team. Nicht jedes Widget einzeln — Universe macht aus den Widgets deines Projekts **einen** Textblock. So hängt das zusammen: 1. **Projekt** — dein Arbeitsbereich in Universe. Dort liegen Lizenz und Widgets. 2. **Build** — der daraus erzeugte Code. Den kopierst du einmal in die **globalen Funktionen** von Ninox. 3. **Anwendungscode** — die kurze Formel im Formelfeld, mit der du ein einzelnes Widget auf einer Seite anzeigst. Ohne den Build im Global Code kann Ninox die Widgets nicht laden. Den Anwendungscode brauchst du trotzdem pro Stelle, an der ein Widget erscheinen soll. Universe hält den **Standard-Build** automatisch aktuell: Neue Widgets und neue Widget-Versionen kommen von selbst hinein, auch größere Versionssprünge. Widgets, die du bewusst auf eine Version festgelegt hast, bleiben dabei unverändert. Eigene benannte Builds nutzt du im Expertenmodus, wenn du einen Stand festhalten willst — zum Beispiel vor einem Go-Live. --- ## Schritt 1: Code in Universe kopieren 1. Gehe in dein Universe-Projekt. 2. Hinterlege unter **„URL freischalten“** zuerst deine Ninox-URL oder Team-ID. Erst dann steckt die Lizenz im Code. 3. Klicke unter **„In Ninox einfügen“** bei **„Globalen Code kopieren“** auf **Kopieren**. Du musst den Code dafür nicht ansehen. Eigene Builds, Widget-Versionen und die Code-Vorschau findest du über den Schalter **Expertenmodus** oben neben „Ninox“. ## Schritt 2: In Ninox einsetzen (Erst-Setup) 1. Wechsel in deine Ninox-Datenbank im **Admin-Modus**. 2. Klicke auf die **drei Punkte** neben dem Datenbanknamen oben links. 3. Gehe auf **„Optionen“ > „Globale Funktionen“**. 4. Füge den kopierten Code am Ende des Textfeldes ein und speichere. Zusätzlich einmalig als **Trigger nach Öffnen** (Datenbankoptionen, ganz oben): ```ninox dialog("ArcRider initialization", html(arcRiderWidgetManager()), ["close"]) ``` Damit startet die Widget-Engine, sobald die Datenbank geöffnet wird. In deinem Universe-Projekt kopierst du dieses Script mit **„Start-Code kopieren“**. --- ## Wie funktionieren Updates? Nach dem ersten Einfügen hakst du in deinem Universe-Projekt **„Ja, Code ist eingefügt“** ab. Ab dann weiß Universe, welcher Stand in deinem Ninox läuft. Wenn wir Fehler beheben, neue Widget-Versionen erscheinen oder sich deine URLs ändern, erzeugt Universe einen neuen Code. In deinem Projekt erscheint dann das Banner **„Neues Update verfügbar“** mit einer Liste, welche Widgets sich geändert haben. Größere Versionssprünge sind dort als **Hauptversion** markiert; prüfe danach kurz deine Widgets. So aktualisierst du Ninox sicher: 1. Klicke im Banner auf **„Neuen Code kopieren“**. 2. Öffne die **globalen Funktionen** in Ninox. 3. Suche den alten Arc-Rider-Block. Er beginnt mit einem klaren Header (ASCII-Wortmarke und `Build ID`). 4. Ersetze diesen Block vollständig durch den neuen Code. Achte darauf, dass deine eigenen globalen Funktionen oberhalb oder unterhalb des Arc-Rider-Blocks unberührt bleiben. Zurück in Universe klickst du auf **„Ja, ich habe die neueste Version eingefügt“**. Dann verschwindet das Banner bis zum nächsten Update. Den Start-Code musst du bei Updates nicht neu einfügen. --- # React: Fehlerbehebung Source: https://www.arc-rider.com/docs/faq/react-troubleshooting # React: Fehlerbehebung ## Das Widget bleibt leer Prüfe zuerst, ob du den exportierten Komponentennamen verwendest und ob alle Optionen in `data` liegen. Die [Komponentenübersicht](/docs/react/intro/react-components) zeigt die verfügbaren Exporte. Eine Höhe von `100%` funktioniert nur in einem Elterncontainer mit berechenbarer Höhe. Beginne zum Eingrenzen mit `height: "320px"`. Bei Table braucht jede Spalte eine Breite, mindestens eine `fraction`. ## Ein Klick löst keinen Callback aus Aktiviere die Klickbarkeit an der richtigen Stelle: etwa an der Table-Zeile, einer Kanban-Karte oder einem Step. Prüfe die Callback-Signatur in der Widget-Referenz. Eine ID ist keine Callback-Position; manche Events liefern Indizes, andere Werte. ## Nach dem Speichern springt die Anzeige zurück Übernimm das bestätigte Ergebnis in React-State und leite ein neues `data`-Objekt daraus ab. Das Widget speichert nicht selbst in deiner Datenbank. Beim Kanban muss `onItemDrop` das Speicher-Promise zurückgeben. ## TypeScript erkennt einen Callback oder JSX-Slot nicht Diese Referenzen gehören zu `0.1.0-alpha.5`. Vergleiche deine installierte Version und vermeide gemischte Kopien des Pakets. Die Typen befinden sich im Paket; du benötigst kein zusätzliches Widget-Typenpaket. ## Server Rendering Die Widget-Laufzeit verwendet Browser-APIs. Importiere sie in einer Anwendung mit Server Rendering erst im Browser. Eine reine Client-Markierung reicht nicht in jedem Framework, wenn Module schon auf dem Server ausgewertet werden. Dieses vollständige Beispiel verschiebt den Laufzeit-Import in einen Effekt; der Typimport wird beim Build entfernt: ```tsx import { useEffect, useState } from 'react'; import type { ComponentType } from 'react'; import type { ArcWidgetButtonProps } from '@arcrider/arcwidgets-react'; export default function Example() { const [Button, setButton] = useState | null>(null); const [error, setError] = useState(false); useEffect(() => { let active = true; import('@arcrider/arcwidgets-react') .then((module) => { if (active) setButton(() => module.ArcWidgetButton); }) .catch(() => { if (active) setError(true); }); return () => { active = false; }; }, []); if (error) return

Widget konnte nicht geladen werden.

; return Button ? (

{status}

); } ``` ## So funktioniert es Die Form-ID verbindet eine Markierung mit deinem Datensatz. In einer Produktivanwendung speicherst du die Formen über deine API und validierst geladene Daten. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Custom Drawing Teil 2: Mangel-Verortung mit Pins # Mangel-Verortung mit Pins ![](https://framerusercontent.com/images/XSN9KLdAVIgeizkqag1BjBvXo.png) In [Custom Drawing Teil 1: Zeichnen im Bild](https://www.arc-rider.de/documentation/custom-drawing) hast du gelernt, wie du ein Tool entwickelst, mit dem du Bilder hochladen, darin zeichnen und die bearbeiteten Bilder wieder abspeichern kannst. In diesem **Teil 2** zeigen wir dir, wie du **PDF-Dateien hochladen** und darauf **Pins setzen** kannst – zum Beispiel für die **Mangelverortung**. Das User Interface unterscheidet sich hier insbesondere im rechten Aktionsbereich, wo die Mängel direkt klickbar sind. Zusätzlich können **Pläne versioniert** werden, etwa wenn Änderungen am Plan vorgenommen werden. Das Besondere dabei: **Beim Duplizieren bleiben die Mängel erhalten**, während sich der zugrunde liegende Plan aktualisieren kann. So lassen sich Änderungen effizient nachvollziehen und verwalten. ## Notwendige Widgets - Custom Drawing - Custom Upload - Custom Layout - Input - Icon - Button - Image ## Vorgehensweise Um Custom Drawing für den Use Case "Pin-Verortung auf PDF" bei dir richtig einzusetzen, musst du einige Schritte in deiner Ninox Datenbank beachten.
  • ### Anlegen von Tabellen
  • In [Custom Drawing Teil 1: Zeichnen im Bild](https://www.arc-rider.de/documentation/custom-drawing) hast du folgende Tabellen bereits angelegt: - Projekte

    • Mängel (Untertabelle)
      • Dokumente (Untertabelle)
        • Shapes (Untertabelle)

    **Was neu dazu kommt: **Für die Pin-Verortung legst du nun eine neue Tabelle "Pläne" an. Diese wird mit Projekte verknüpft. Außerdem ist es wichtig, dass Shapes direkt mit Mängel (N:1) verknüpft wird und nicht nur, wie bislang, mit Dokumente. - Projekte

    • Pläne (Untertabelle) - Mängel (Untertabelle)

      • Dokumente (Untertabelle)
        • Shapes (Untertabelle)

  • ### Felder in Shapes anlegen
  • In [Custom Drawing Teil 1: Zeichnen im Bild](https://www.arc-rider.de/documentation/custom-drawing) hast du die Grundlage aller Felder geschaffen. Folgend werden nur Änderungen oder Ergänzungen erläutert. Bitte verknüpfe in der Tabelle Shapes die Tabelle Mängel. (N:1) #### `Mängel` - Bezeichnung: `Mängel` - Typ: Verknüpfung - Zuweisung: Pro Datensatz im Server - Trigger nach Änderung: Keine ![](https://framerusercontent.com/images/Ds2ai9f0JnAY41kcEDmjNMKUaf4.png) #### `trigger_createMangel` - Bezeichnung: `trigger_createMangel` - Typ: Text - Zuweisung: Pro Datensatz im Speicher (Browser) - Trigger nach Änderung: ```javascript let current := this; let newMangel := (create 'Mängel'); newMangel.(Projekte := current.Dokumente.'Pläne'.Projekte); 'Mängel' := newMangel ```
  • ### Felder in Dokumente anlegen
  • In [Custom Drawing Teil 1: Zeichnen im Bild](https://www.arc-rider.de/documentation/custom-drawing) hast du die Grundlage aller Felder geschaffen. Folgend werden nur Änderungen oder Ergänzungen erläutert. #### `Pläne` - Bezeichnung: `Pläne` - Typ: Verknüpfung - Zuweisung: Pro Datensatz im Server - Trigger nach Änderung: Keine ![](https://framerusercontent.com/images/vArz7BA7MbQNGAaCuZamFTp4DY.png) #### `trigger_addBackgroundImage_browser` - Bezeichnung: `trigger_addBackgroundImage_browser` - Typ: Text - Zuweisung: Pro Datensatz im Server - Trigger nach Änderung: **(Neu)** ```javascript let current := this; let recordDuplicatePlan := record(Dokumente,number('Pläne'.helper_duplicateVersion)); helper_base64 := trigger_addBackgroundImage_browser; let shapeImage := text({ type: "image", href: trigger_addBackgroundImage_browser, width: round(current.helper_width), height: round(current.helper_height), x: 0, y: 0 }); if cnt(Shapes[data_shapeData.isBackgroundImage]) = 0 then let newShape := (create Shapes); newShape.( Dokumente := current; helper_shapeDataValue := shapeImage ) end; if recordDuplicatePlan then let duplicatedShapes := recordDuplicatePlan.Shapes; for item in duplicatedShapes[data_shapeData.isBackgroundShape != true] do let newShape := (create Shapes); newShape.( Dokumente := current.Nr; helper_shapeDataValue := item.helper_shapeDataValue; 'Mängel' := item.'Mängel' ) end end; 'Pläne'.(helper_duplicateVersion := null); 'Pläne'.(helper_selectedImageDrawing := number(current.Nr)); trigger_addBackgroundImage_browser := null ``` #### `trigger_addBackgroundImage_app` - Bezeichnung: `trigger_addBackgroundImage_app` - Typ: Text - Zuweisung: Pro Datensatz im Speicher (Browser) - Trigger nach Änderung: **(Neu)** ```javascript let current := this; helper_base64 := trigger_addBackgroundImage_app; let shapeImage := text({ type: "image", href: trigger_addBackgroundImage_app, width: round(current.helper_width), height: round(current.helper_height), x: 0, y: 0 }); if cnt(Shapes[data_shapeData.isBackgroundImage]) = 0 then let newShape := (create Shapes); newShape.( Dokumente := current; helper_shapeDataValue := shapeImage ) end; let recordDuplicatePlan := record(Dokumente,number('Pläne'.helper_duplicateVersion)); if recordDuplicatePlan then let duplicatedShapes := recordDuplicatePlan.Shapes; for item in duplicatedShapes[data_shapeData.isBackgroundShape != true] do let newShape := (create Shapes); newShape.( Dokumente := current.Nr; helper_shapeDataValue := item.helper_shapeDataValue; 'Mängel' := item.'Mängel' ) end end; 'Pläne'.(helper_duplicateVersion := null); 'Pläne'.(helper_selectedImageDrawing := number(current.Nr)); trigger_addBackgroundImage_app := null ``` #### `trigger_deleteImage` - Bezeichnung: `trigger_deleteImage` - Typ: Ja / Nein - Zuweisung: Pro Datensatz im Speicher (Browser) - Trigger nach Änderung: **(Neu)** ```javascript let current := this; if dialog("Plan-Version löschen", "Soll diese Plan-Version wirklich gelöscht werden?", ["Ja, löschen!", "Abbrechen"]) = "Ja, löschen!" then 'Pläne'.(helper_selectedImageDrawing := number(first(current.'Pläne'.Dokumente[Nr != current.Nr]).Nr)); delete this end; trigger_deleteImage := null ```
  • ### Felder in `Pläne` anlegen
  • In der Tabelle legst du dir einen Karteireiter "Helper" an, damit alle Hilfsfelder hier erstellt werden und auf dem ersten Karteireiter dein Custom Drawing Image liegen kann. ![](https://framerusercontent.com/images/ZXqJg1Ln3GN9ietnyxtz1diUxg.png) #### `Bezeichnung` - Bezeichnung: `Bezeichnung` - Typ: Text - Zuweisung: Pro Datensatz im Server - Trigger nach Änderung: Keine #### `helper_base64` - Bezeichnung: `helper_base64` - Typ: Text - Zuweisung: Pro Datensatz im Server - Trigger nach Änderung: Keine #### `helper_selectedImageDrawing` - Bezeichnung: `helper_selectedImageDrawing` - Typ: Zahl - Zuweisung: Pro Datensatz im Server - Trigger nach Änderung: Keine #### `helper_duplicateVersion` - Bezeichnung: `helper_duplicateVersion` - Typ: Text - Zuweisung: Pro Datensatz im Server - Trigger nach Änderung: Keine #### `trigger_cancelUpload` - Bezeichnung: `trigger_cancelUpload` - Typ: Text - Zuweisung: Pro Datensatz im Speicher (Browser) - Trigger nach Änderung: ```javascript if cnt(Dokumente) = 0 then if dialog("Abbrechen", "Soll der gesamte Plan wirklich gelöscht werden?", ["Ja, löschen!", "Abbrechen"]) = "Ja, löschen!" then delete this end else if helper_duplicateVersion != null then if dialog("Duplizieren abbrechen?", "Soll der Vorgang wirklich abgebrochen werden?", ["Ja", "Nein"]) = "Ja" then helper_duplicateVersion := null end end; trigger_cancelUpload := null end ```
  • ### Widgets in Pläne einfügen
  • In der Tabelle Pläne fügst du nun den folgenden Anwendungscode ein. Dieser umfasst die oben beschriebenen Widgets. ```javascript let current := this; let fontColor := "#7a83a2"; let recordSelectedImageDrawing := record(Dokumente,helper_selectedImageDrawing); let drawingSticker := [{ uid: "red_sticker", image: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0icGluIiBkYXRhLW5hbWU9InBpbiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTYwIDgwMCI+CiAgPGcgaWQ9InBpbiI+CiAgICA8cGF0aCBpZD0icGluLTIiIGRhdGEtbmFtZT0icGluIiAgZmlsbD0iIzNlNmZmZiIgZD0iTTU2MCwyODBjMCwxNTQuNjUtMjgwLDUyMC0yODAsNTIwLDAsMC0yODAtMzY1LjM1LTI4MC01MjBTMTI1LjM2LDAsMjgwLDBzMjgwLDEyNS4zNSwyODAsMjgwIi8+CiAgICA8cGF0aCBpZD0icm91bmRfb3V0ZXIiIGRhdGEtbmFtZT0icm91bmQgb3V0ZXIiIGZpbGw9IiNmZmYiIGQ9Ik00NTguMiwzMDUuMzljMC05OC40MS03OS43Ny0xNzguMi0xNzguMi0xNzguMnMtMTc4LjIsNzkuNzktMTc4LjIsMTc4LjIsNzkuNzcsMTc4LjIsMTc4LjIsMTc4LjIsMTc4LjItNzkuNzYsMTc4LjItMTc4LjIiLz4KICAgIDxwYXRoIGlkPSJyb3VuZF9pbm5lciIgZGF0YS1uYW1lPSJyb3VuZCBpbm5lciIgZmlsbD0iI2U5NTk1YyIgZD0iTTQwMS45NiwzMDUuMzljMCw2Ny4yNC01NC43NiwxMjEuOTYtMTIxLjk2LDEyMS45NnMtMTIxLjk2LTU0LjczLTEyMS45Ni0xMjEuOTYsNTQuNzYtMTIxLjk2LDEyMS45Ni0xMjEuOTYsMTIxLjk2LDU0Ljc5LDEyMS45NiwxMjEuOTYiLz4KICA8L2c+Cjwvc3ZnPg==", icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0icGluIiBkYXRhLW5hbWU9InBpbiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTYwIDgwMCI+CiAgPGcgaWQ9InBpbiI+CiAgICA8cGF0aCBpZD0icGluLTIiIGRhdGEtbmFtZT0icGluIiAgZmlsbD0iIzNlNmZmZiIgZD0iTTU2MCwyODBjMCwxNTQuNjUtMjgwLDUyMC0yODAsNTIwLDAsMC0yODAtMzY1LjM1LTI4MC01MjBTMTI1LjM2LDAsMjgwLDBzMjgwLDEyNS4zNSwyODAsMjgwIi8+CiAgICA8cGF0aCBpZD0icm91bmRfb3V0ZXIiIGRhdGEtbmFtZT0icm91bmQgb3V0ZXIiIGZpbGw9IiNmZmYiIGQ9Ik00NTguMiwzMDUuMzljMC05OC40MS03OS43Ny0xNzguMi0xNzguMi0xNzguMnMtMTc4LjIsNzkuNzktMTc4LjIsMTc4LjIsNzkuNzcsMTc4LjIsMTc4LjIsMTc4LjIsMTc4LjItNzkuNzYsMTc4LjItMTc4LjIiLz4KICAgIDxwYXRoIGlkPSJyb3VuZF9pbm5lciIgZGF0YS1uYW1lPSJyb3VuZCBpbm5lciIgZmlsbD0iI2U5NTk1YyIgZD0iTTQwMS45NiwzMDUuMzljMCw2Ny4yNC01NC43NiwxMjEuOTYtMTIxLjk2LDEyMS45NnMtMTIxLjk2LTU0LjczLTEyMS45Ni0xMjEuOTYsNTQuNzYtMTIxLjk2LDEyMS45Ni0xMjEuOTYsMTIxLjk2LDU0Ljc5LDEyMS45NiwxMjEuOTYiLz4KICA8L2c+Cjwvc3ZnPg==", label: "Roter Pin", width: 60, height: 60, originX: 0.5, originY: 0.89, default: true }, { uid: "green_sticker", image: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0icGluIiBkYXRhLW5hbWU9InBpbiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTYwIDgwMCI+CiAgPGcgaWQ9InBpbiI+CiAgICA8cGF0aCBpZD0icGluLTIiIGRhdGEtbmFtZT0icGluIiAgZmlsbD0iIzNlNmZmZiIgZD0iTTU2MCwyODBjMCwxNTQuNjUtMjgwLDUyMC0yODAsNTIwLDAsMC0yODAtMzY1LjM1LTI4MC01MjBTMTI1LjM2LDAsMjgwLDBzMjgwLDEyNS4zNSwyODAsMjgwIi8+CiAgICA8cGF0aCBpZD0icm91bmRfb3V0ZXIiIGRhdGEtbmFtZT0icm91bmQgb3V0ZXIiIGZpbGw9IiNmZmYiIGQ9Ik00NTguMiwzMDUuMzljMC05OC40MS03OS43Ny0xNzguMi0xNzguMi0xNzguMnMtMTc4LjIsNzkuNzktMTc4LjIsMTc4LjIsNzkuNzcsMTc4LjIsMTc4LjIsMTc4LjIsMTc4LjItNzkuNzYsMTc4LjItMTc4LjIiLz4KICAgIDxwYXRoIGlkPSJyb3VuZF9pbm5lciIgZGF0YS1uYW1lPSJyb3VuZCBpbm5lciIgZmlsbD0iIzc1YjY0OSIgZD0iTTQwMS45NiwzMDUuMzljMCw2Ny4yNC01NC43NiwxMjEuOTYtMTIxLjk2LDEyMS45NnMtMTIxLjk2LTU0LjczLTEyMS45Ni0xMjEuOTYsNTQuNzYtMTIxLjk2LDEyMS45Ni0xMjEuOTYsMTIxLjk2LDU0Ljc5LDEyMS45NiwxMjEuOTYiLz4KICA8L2c+Cjwvc3ZnPg==", icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0icGluIiBkYXRhLW5hbWU9InBpbiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTYwIDgwMCI+CiAgPGcgaWQ9InBpbiI+CiAgICA8cGF0aCBpZD0icGluLTIiIGRhdGEtbmFtZT0icGluIiAgZmlsbD0iIzNlNmZmZiIgZD0iTTU2MCwyODBjMCwxNTQuNjUtMjgwLDUyMC0yODAsNTIwLDAsMC0yODAtMzY1LjM1LTI4MC01MjBTMTI1LjM2LDAsMjgwLDBzMjgwLDEyNS4zNSwyODAsMjgwIi8+CiAgICA8cGF0aCBpZD0icm91bmRfb3V0ZXIiIGRhdGEtbmFtZT0icm91bmQgb3V0ZXIiIGZpbGw9IiNmZmYiIGQ9Ik00NTguMiwzMDUuMzljMC05OC40MS03OS43Ny0xNzguMi0xNzguMi0xNzguMnMtMTc4LjIsNzkuNzktMTc4LjIsMTc4LjIsNzkuNzcsMTc4LjIsMTc4LjIsMTc4LjIsMTc4LjItNzkuNzYsMTc4LjItMTc4LjIiLz4KICAgIDxwYXRoIGlkPSJyb3VuZF9pbm5lciIgZGF0YS1uYW1lPSJyb3VuZCBpbm5lciIgZmlsbD0iIzc1YjY0OSIgZD0iTTQwMS45NiwzMDUuMzljMCw2Ny4yNC01NC43NiwxMjEuOTYtMTIxLjk2LDEyMS45NnMtMTIxLjk2LTU0LjczLTEyMS45Ni0xMjEuOTYsNTQuNzYtMTIxLjk2LDEyMS45Ni0xMjEuOTYsMTIxLjk2LDU0Ljc5LDEyMS45NiwxMjEuOTYiLz4KICA8L2c+Cjwvc3ZnPg==", label: "Green Sticker", width: 60, height: 60, originX: 0.5, originY: 0.89 }]; let drawingArea := arcCustomDrawing({ uniqueId: "issue board" + Nr, embedded: true, height: "100%", tableId: tableId("Shapes"), fieldId: fieldId("Shapes", "helper_shapeDataValue"), changeFieldValues: [{ fieldId: fieldId("Shapes", "Dokumente"), value: helper_selectedImageDrawing }, { fieldId: fieldId("Shapes", "trigger_createMangel"), value: "create" }], exportSettings: { allowedTypes: ["jpg", "png", "svg", "pdf"], target: "file", recordId: recordSelectedImageDrawing.Nr, fieldId: fieldId(recordSelectedImageDrawing.Nr, "helper_base64") }, canvas: { width: recordSelectedImageDrawing.helper_width, height: recordSelectedImageDrawing.helper_height, offsetX: 0, offsetY: 0 }, zooming: { step: 0.25, min: 0.3, max: 4 }, drawingSettings: { strokeWidth: 1, strokeColor: "#e9595c" }, artboard: { width: recordSelectedImageDrawing.helper_width, height: recordSelectedImageDrawing.helper_height }, stickerTypes: drawingSticker, shapes: (recordSelectedImageDrawing.Shapes[helper_disable != true] order by data_shapeData.sortId).[{ shapeId: Nr, movable: data_shapeData.movable, shapeDataValue: helper_shapeDataValue, sidebarItem: { header: arcCustomLayout({ uniqueId: "Beispiel " + Nr, embedded: true, fullscreen: false, ninoxVersion: "", page: true, fullscreenMode: "", showAdminTools: true, hideHeaderIcons: true, direction: "horizontal", alignX: "left", alignY: "center", width: "", height: "", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "fraction", height: "auto", lineHeight: "", alignX: "left", color: "#fff", styles: "", value: if data_shapeData.isBackgroundShape != true then "Mangel ID:" + 'Mängel'.Nr else "Plan Hintergrund" end }, if data_shapeData.isBackgroundShape != true then { width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", value: arcCustomButton({ title: "Öffnen", height: "20px", width: "auto", actions: [{ type: "popup", recordId: 'Mängel'.Nr }] }) } end] }), content: if data_shapeData.isBackgroundShape != true then arcCustomButton({ uniqueId: "undo action" + Nr, title: "Mangel löschen", width: "100%", height: "", alignY: "", alignX: "30px", gap: "5px", fontSize: "16px", fontColor: "#fff", backgroundColor: "#e9595c", borderColor: "transparent", borderRadius: "5px", showBadge: false, badgeTitle: "", badgeColor: "", badgeBackground: "", badgeBorderColor: "", badgePosition: "", actions: let id := this; [{ type: "update", recordId: 'Mängel'.Nr, field: fieldId('Mängel'.Nr, "trigger_deleteMangel"), value: true }] }) end } }], rightSideContent: { show: true, header: arcCustomLayout({ uniqueId: "drawing right side header " + Nr, embedded: true, direction: "vertical", alignX: "left", alignY: "top", width: "100%", height: "100%", gap: "10px", backgroundColor: "", paddingY: "20px", paddingX: "20px", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "", height: "auto", lineHeight: "", alignX: "left", color: "#fff", value: "Plan-Version ID " + helper_selectedImageDrawing }] }), content: { showShapeLayers: true, value: "" }, footer: arcCustomLayout({ uniqueId: "drawing right side footer " + Nr, embedded: true, direction: "vertical", alignX: "left", alignY: "center", width: "100%", height: "", gap: "0", backgroundColor: "", paddingY: "0", paddingX: "0", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "100%", height: "fraction", lineHeight: "", alignX: "left", color: "", styles: "border-bottom: 1px solid #262b40;", value: "" }, { width: "100%", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "padding:8px;padding-bottom:0;", value: arcCustomButton({ uniqueId: "undo action" + Nr, title: "Duplizieren", width: "100%", height: "", alignY: "", alignX: "30px", gap: "5px", fontSize: "16px", fontColor: "#fff", backgroundColor: "#272A40", borderColor: "transparent", borderRadius: "5px", showBadge: false, badgeTitle: "", badgeColor: "", badgeBackground: "", badgeBorderColor: "", badgePosition: "", actions: let id := this; [{ type: "update", recordId: current.Nr, field: fieldId(current.Nr, "helper_duplicateVersion"), value: number(helper_selectedImageDrawing) }] }) }, { width: "100%", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "padding:8px", value: arcCustomButton({ uniqueId: "undo action" + Nr, title: "Löschen", width: "100%", height: "", alignY: "", alignX: "30px", gap: "5px", fontSize: "16px", fontColor: "#fff", backgroundColor: "#e9595c", borderColor: "transparent", borderRadius: "5px", showBadge: false, badgeTitle: "", badgeColor: "", badgeBackground: "", badgeBorderColor: "", badgePosition: "", actions: let id := this; [{ type: "update", recordId: recordSelectedImageDrawing.Nr, field: fieldId(recordSelectedImageDrawing.Nr, "trigger_deleteImage"), value: true }] }) }] }) } }); arcCustomLayout({ uniqueId: "drawing " + Nr, embedded: false, fullscreen: true, ninoxVersion: "3.13", page: false, fullscreenMode: if isAdminMode() then " else "full" end, showAdminTools: false, hideHeaderIcons: false, direction: "vertical", alignX: "left", alignY: "top", width: "100%", height: "100%", gap: "0", backgroundColor: "#181d2c", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: true }, blocks: [{ width: "", height: "100px", lineHeight: "", alignX: "left", paddingX: "", styles: "", value: arcCustomLayout({ uniqueId: "layout header " + Nr, embedded: true, direction: "horizontal", alignX: "left", alignY: "center", width: "100%", height: "100%", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: true }, blocks: [{ width: "fraction", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "font-size:24px;color:#7a83a2;text-wrap:nowrap;padding:0 40px;", value: arcCustomInput({ uniqueId: "Plan Titel " + Nr, recordId: Nr, fieldId: fieldId("Pläne", "Bezeichnung"), title: text(Bezeichnung), value: text(Bezeichnung), type: "text", embedded: true, disabled: false, tempStorage: false, suffix: "", width: "", height: "", alignX: "left", paddingY: "", paddingX: "0", fontColor: "#7a83a2", fontSize: "24px", fontWeight: "", backgroundColor: "transparent", borderWidth: "", borderColor: "transparent", borderRadius: "", placeholderSettings: { value: "Bezeichnung des Plans", fontColor: "#7a83a288", backgroundColor: "transparent" }, focusAction: { width: "100%", showFocusOutline: true, outlineWidth: "4px", outlineColor: "#04CD10" }, labelSettings: { title: "", fontSize: "", alignX: "", gap: "" } }) }, { width: "auto", height: "auto", lineHeight: "", alignX: "right", color: "", value: arcCustomButton({ uniqueId: "Button schließen" + Nr, title: "Schließen", width: "", height: "", alignY: "", alignX: "", paddingX: "", paddingY: "", gap: "5px", icon: arcCustomIcon({ name: "x", color: fontColor }), iconPosition: "right", fontSize: "", fontColor: fontColor, backgroundColor: "transparent", borderColor: "transparent", borderRadius: "5px", actions: [{ type: "closeRecord", recordId: current.Nr }] }) }] }) }, { width: "100%", height: "50px", lineHeight: "", alignX: "left", color: "", backgroundColor: "#141925", styles: "border-bottom: 1px solid #262b40;border-top: 1px solid #262b40;", value: arcCustomLayout({ comment: "Tab menu ausblenden", uniqueId: "Beispiel " + Nr, embedded: true, fullscreen: false, ninoxVersion: "", page: true, fullscreenMode: "", showAdminTools: true, hideHeaderIcons: true, direction: "horizontal", alignX: "left", alignY: "bottom", width: "100%", height: "100%", gap: "0", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: Dokumente.[{ width: "auto", height: "40px", lineHeight: "", alignX: "left", color: fontColor, backgroundColor: if current.recordSelectedImageDrawing = number(Nr) then "#272A40" else "#181d2c" end, styles: "font-weight:600; padding:0 20px; border-left: 1px solid #262b40;border-right: 1px solid #262b40;", value: arcCustomLayout({ uniqueId: "Beispiel " + Nr, embedded: true, direction: "horizontal", alignX: "left", alignY: "center", width: "auto", height: "", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "auto", height: "auto", lineHeight: "", alignX: "left", color: fontColor, styles: "", value: "Plan-Version Nr." + Nr }] }), clickAction: { type: "update", recordId: current.Nr, field: fieldId(current.Nr, "helper_selectedImageDrawing"), value: number(Nr) } }] }) }, { width: "100%", height: "100%", lineHeight: "", alignX: "left", color: "", backgroundColor: "#181d2c", value: if helper_duplicateVersion != null or cnt(Dokumente) = 0 then arcCustomLayout({ uniqueId: "Upload und Button " + Nr, embedded: true, fullscreen: false, ninoxVersion: "", page: true, fullscreenMode: "", showAdminTools: true, hideHeaderIcons: true, direction: "vertical", alignX: "left", alignY: "top", width: "", height: "", gap: "5px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ comment: "------------------------------- Custom Upload ------------------------------", width: "", height: "auto", lineHeight: "", alignX: "center", color: "", styles: "", value: arcCustomUpload({ uniqueId: "multi image upload in drawing " + current.Nr, capture: false, multiupload: false, embedded: true, container: { icon: "", label: "Bilder Upload", height: "100%", width: "100%", value: arcCustomLayout({ uniqueId: "Beispiel " + Nr, embedded: true, fullscreen: false, ninoxVersion: "", page: true, fullscreenMode: "", showAdminTools: true, hideHeaderIcons: true, direction: "vertical", alignX: "center", alignY: "center", width: "", height: "100%", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "border-radius:50px;overflow:hidden;", value: html(--- ---) }, { width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "#7a83a2", value: html(--- **Neue Planversion hochladen** ---) }] }) }, filename: { tableId: tableId("Dokumente"), fieldId: fieldId("Dokumente", "Dateiname") }, image: { tableId: tableId("Dokumente"), fieldId: if ninoxApp() = "web" then fieldId("Dokumente", "trigger_addBackgroundImage_browser") else fieldId("Dokumente", "trigger_addBackgroundImage_app") end, width: 2000, height: 1000, widthFieldId: fieldId("Dokumente", "helper_width"), heightFieldId: fieldId("Dokumente", "helper_height") }, convertSettings: { fileType: "jpg", apiKey: "something", width: 9000, height: 9000, dpi: 150 }, changeFieldValues: [{ fieldId: fieldId("Dokumente", "Pläne"), value: number(current.Nr) }] }) }, { comment: "------------------------------- Custom Button Abbrechen ------------------------------", width: "", height: "auto", lineHeight: "", alignX: "center", color: "", value: arcCustomButton({ uniqueId: "Button cancel" + Nr, title: "Abbrechen", width: "", height: "", alignY: "", alignX: "", paddingX: "", paddingY: "", gap: "5px", icon: "", iconPosition: "left", fontSize: "", fontColor: "#181d2c", backgroundColor: "#7a83a2", borderColor: "#7a83a2", borderRadius: "50px", showBadge: false, badgeTitle: "", badgeColor: "", badgeBackground: "", badgeBorderColor: "", badgePosition: "", hoverActions: { fontColor: "", iconColor: "", backgroundColor: "", borderColor: "", animation: "0.25s" }, actions: [{ type: "update", recordId: current.Nr, field: fieldId(current.Nr, "trigger_cancelUpload"), value: "cancel" }] }) }] }) else drawingArea end }] }) ``` **Tadaaaaa 🧙🏼‍♀️ Das Widget Mangel-Verortung mit Pins sollte nun funktionsfähig bei dir laufen. ** --- # Custom Sort in einer Tabelle einbinden Source: https://www.arc-rider.com/docs/examples/customsort-einbinden # Eigene Prioritätsreihenfolge in React Prioritäten als Badge darstellen und nach einem fachlichen Rang sortieren. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { ArcWidgetTable, ArcWidgetBadge } from '@arcrider/arcwidgets-react'; const priorities = [ { id: 'a', title: 'Wartung', priority: 'Normal', rank: 2, }, { id: 'b', title: 'Störung', priority: 'Dringend', rank: 1, }, ]; export default function Example() { return ( ({ id: p.id, columns: [ { value: p.title, width: 'fraction', }, { value: ( ), width: '180px', sort: { value: p.rank, }, }, ], })), }} /> ); } ``` ## So funktioniert es Der numerische Rang in `sort.value` bestimmt die Reihenfolge. Ein zusätzliches Sort-Widget wird dafür nicht benötigt. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Custom Sort in einer Tabelle einbinden # Custom Sort in einer Tabelle einbinden Du willst deine Einträge mit dem Custom Sort direkt innerhalb der Custom Tabelle sortieren? In diesem Beitrag findest du eine detaillierte Anleitung, welche Ninox-Einstellungen du benötigst und den dazugehörigen Anwendungscode. In folgendem Beispiel wird eine Aufgabenliste (Child / Untertabelle) in einem Auftrag (Parent / Obertabelle) als Custom Table eingeblendet. Mit Custom Sort soll die Aufgaben Reihenfolge manuell bestimmt werden. Widgets, die dafür benötigst: - [Custom Sort](/documentation/customsort-einbinden) - [Custom Table](/custom-table) Ninox Einstellungen, die du benötigst: - ein sortId Funktionsfeld in der Aufgabe (child) - Sortierung Zahlenfeld in der Tabelle, in der die Records sortiert werden sollen - sortItems Textfeld im Auftrag (parent) ## Anwendungscode ```javascript let current := this; arcCustomSort({ uniqueId: Nr, editable: false, recordId: current.Nr, fieldId: "", direction: "horizontal", title: Sortierung, value: Sortierung, valuePrefix: number(Nr) + ",", size: "", color: "", showIconsAlways: true, iconStart: "", iconEnd: "" }) ``` #### sortId Funktionsfeld in der Aufgabe (Child) ```javascript let entities := Auftrag.Aufgaben; if Sortierung != null then let sortValue := format(Sortierung, "000") + "-" + (number('Zuletzt geändert') - number(datetime(3000, 1, 1))) + "-" + number(Nr); number(cnt(entities[Sortierung != null and format(Sortierung, "000") + "-" + (number('Zuletzt geändert') - number(datetime(3000, 1, 1))) + "-" + number(Nr) <= sortValue])) else let sortValue := max(entities.Sortierung) + 1 + " + Nr; number(cnt(entities[max(entities.Sortierung) + 1 + " + Nr <= sortValue])) end ``` #### sortItems Text Feld im Auftrag (Parent) Den folgenden Code setzt du in deinem sortItems Textfeld in die Trigger nach Änderung ein. ```javascript let current := this; if sortItems != null then let splitItem := split(sortItems, ","); let itemNr := item(splitItem, 0); let itemPos := item(splitItem, 1); let item := first(Aufgaben[number(Nr) = number(itemNr)]); item.(Sortierung := number(itemPos) + if itemPos > item.Sortierung then 1 else 0 end); for item in Aufgaben order by Sortierung do item.(Sortierung := item.sortId) end; sortItems := null end ``` ### --- # Custom Table Look — Rahmen, Dark, Zeile und Spalte Source: https://www.arc-rider.com/docs/examples/custom-table-look # Tabellendesign und Dark Mode in React Rahmen, Kopfzeile und Zellen aus einem gemeinsamen Farbsatz gestalten. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetTable } from '@arcrider/arcwidgets-react'; export default function Example() { const [dark, setDark] = useState(false); return ( <> ); } ``` ## So funktioniert es Der Schalter wechselt React-State. Leite Farben auch für eingebettete Komponenten aus demselben Theme ab; ein fremdes Theme-Paket wird nicht vorausgesetzt. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Custom Table Look — Rahmen, Dark, Zeile und Spalte Neue Default-Optik der Custom Table: abgerundeter Rahmen, Zeilenlinien, **keine** vertikalen Zelllinien. Alles über dieselbe `styles`-Tasche — auf der Tabelle, der Zeile und der Spalte. Keine Extra-Props für Zeilenflächen. Ersetze `Termine` durch deine Tabelle. Die Felder sind Demo-Namen. Gemeinsamer Rumpf für alle Varianten: ```javascript let current := this; let list := select Termine; let rows := list.[{ recordId: Nr, columns: [{ field: "status", title: "Status", value: Status, width: "100px", recordId: Nr }, { field: "termin", title: "Termin", value: format(Datum, "DD.MM.YYYY"), width: "120px", recordId: Nr }, { field: "patient", title: "Patient", value: Patient, width: "fraction", recordId: Nr }, { field: "geburtsdatum", title: "Geburtsdatum", value: format(Geburtsdatum, "DD.MM.YYYY"), width: "140px", recordId: Nr }] }]; ``` ## Neutraler Default Nichts Extra setzen. Header 36px, Radius 12px, Zeilenlinien an, Spaltenlinien aus. ```javascript let data := { uniqueListId: "termine-look-" + Nr, tableId: tableId("Termine"), height: "400px", embedded: false, header: { showHeader: true, columns: [{ title: "Status", width: "100px" }, { title: "Termin", width: "120px" }, { title: "Patient", width: "fraction" }, { title: "Geburtsdatum", width: "140px" }] }, table: rows, emptyTable: { title: "Keine Termine" } }; arcCustomTable(data) ``` ## In einer Card — ohne Außenrahmen Die Table sitzt in einem Layout. Der Rahmen kommt von der Card, nicht von der Table. ```javascript styles: { borders: { outer: false } } ``` ## Klassisches Gitter Vertikale Linien dazu, wenn die Spalten wie ein Spreadsheet gelesen werden sollen. ```javascript styles: { borders: { columns: true } } ``` ## Flush — keine Linien Bündig, ohne Rahmen und ohne Gitter. Gut in dichten Dashboards. ```javascript styles: { borders: { outer: false, columns: false, rows: false } } ``` ## Dark Eine Fläche für den Tisch (`styles.backgroundColor`), der Kopf über `header.backgroundColor`. Dieselbe Hierarchie wie im hellen Default — nur andere Farben. ```javascript let data := { uniqueListId: "termine-dark-" + Nr, tableId: tableId("Termine"), height: "400px", embedded: false, styles: { backgroundColor: "#2a2822", fontColor: "#f5f0e6", borderRadius: "12px", borderColor: "#3f3c34", borders: { outer: true, columns: false, rows: { color: "#3a372f" } } }, rowHoverAction: { hoverActive: true, backgroundColor: "#35322a", fontColor: "#f5f0e6" }, header: { showHeader: true, backgroundColor: "#1c1b16", fontColor: "#f5f0e6", columns: [{ title: "Status", width: "100px" }, { title: "Termin", width: "120px" }, { title: "Patient", width: "fraction" }, { title: "Geburtsdatum", width: "140px" }] }, table: rows, emptyTable: { title: "Keine Termine" } }; arcCustomTable(data) ``` ## Dark — ohne Außenrahmen Wie Dark, aber bündig in einer dunklen Card. ```javascript styles: { backgroundColor: "#2a2822", fontColor: "#f5f0e6", borderColor: "#3f3c34", borders: { outer: false, columns: false, rows: { color: "#3a372f" } } }, header: { backgroundColor: "#1c1b16", fontColor: "#f5f0e6" } ``` ## Dark — mit Spaltenlinien ```javascript styles: { backgroundColor: "#2a2822", fontColor: "#f5f0e6", borderColor: "#3f3c34", borders: { outer: true, columns: true, rows: { color: "#3a372f" } } }, header: { backgroundColor: "#1c1b16", fontColor: "#f5f0e6" } ``` ## Eine Zeile und eine Spalte Dieselbe `styles`-Tasche eine Ebene tiefer. Spalte gewinnt gegen Zeile, Zeile gegen Tabelle. ```javascript table: list.[{ recordId: Nr, styles: if Status = "Neu" then { backgroundColor: "#f4f4f5" } end, columns: [{ field: "status", title: "Status", value: Status, width: "100px", recordId: Nr }, { field: "termin", title: "Termin", value: format(Datum, "DD.MM.YYYY"), width: "120px", recordId: Nr }, { field: "patient", title: "Patient", value: Patient, width: "fraction", recordId: Nr, styles: if Status = "Neu" then { backgroundColor: "#e4e4e7", fontWeight: "600" } end }, { field: "geburtsdatum", title: "Geburtsdatum", value: format(Geburtsdatum, "DD.MM.YYYY"), width: "140px", recordId: Nr }] }] ``` `rowColor` bleibt als alter Alias. Neu immer `styles.backgroundColor` auf der passenden Ebene. --- # Custom Table mit Mini Widgets Source: https://www.arc-rider.com/docs/examples/custom-table-mit-mini-widgets # Auftragsliste mit Suche und Status in React Eine Liste filtern, Status und Fortschritt als React-Komponenten in Zellen darstellen und einen Auftrag auswählen. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetTable, ArcWidgetInput, ArcWidgetBadge, ArcWidgetProgressBar, } from '@arcrider/arcwidgets-react'; const orders = [ { id: 'a', name: 'Portal', status: 'Offen', done: 3, total: 8, }, { id: 'b', name: 'Website', status: 'Erledigt', done: 6, total: 6, }, ]; export default function Example() { const [query, setQuery] = useState(''); const [selected, setSelected] = useState('Keine Auswahl'); const visible = orders.filter((o) => o.name.toLowerCase().includes(query.toLowerCase()), ); return ( <> setQuery(String(v ?? ''))} /> ({ id: o.id, columns: [ { value: ( ), width: 'fraction', sort: { value: o.name, }, }, { value: ( ), width: '160px', }, { value: ( ), width: '180px', }, ], })), emptyTable: { title: 'Keine Treffer', value: (
    {'Keine Treffer'}

    {'Ändere den Suchbegriff.'}

    ), }, }} />

    Detail: {selected}

    ); } ``` ## So funktioniert es Der Filter erzeugt eine neue Zeilenliste. Die Auswahl verwendet eine stabile Auftrags-ID. Ersetze die Beispieldaten durch Daten aus deiner API. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Custom Table mit Mini Widgets # Mehr Übersicht in Tabellen: Mini Widgets direkt in der Custom Table ![](https://framerusercontent.com/images/Gipfc03JpgpGTPrqL4RJ0ONktE.png) In diesem Beitrag zeigen wir euch ein praxisnahes und komplexes Beispiel für eine **Custom Table**, die eure **Aufgabenverwaltung in Ninox** deutlich übersichtlicher und interaktiver macht. Die Tabelle gruppiert Aufgaben nach Aufträgen und kombiniert leistungsstarke Funktionen mit einer modernen Darstellung. Dank der integrierten **Mini Widgets** – darunter [Checkbox](/documentation/check-box), [Input](/documentation/input), [Progress Bar](/documentation/progress-bar), [Button](/documentation/buttons), [Select](/documentation/select), [Icon](/documentation/custom-icon) und [Pagination](/documentation/pagination) – lässt sich die Tabelle direkt bedienen, ohne die Ansicht verlassen zu müssen. Ideal für alle, die Effizienz und Benutzerfreundlichkeit in ihren Workflows kombinieren möchten. ## Aufgabenverwaltung leicht gemacht Mit unserer Custom Table verwaltet ihr Aufgaben so einfach und effizient wie nie zuvor. Dank der Gruppierung nach Aufträgen behaltet ihr stets den Überblick – und das ist erst der Anfang. ## ✅ Aufgaben abhaken leicht gemacht Per integrierter [Checkbox](/documentation/check-box) markiert ihr erledigte Aufgaben direkt in der Tabelle. Ein visuelles Feedback zeigt euch sofort, was bereits geschafft ist. ## 📝 Beschreibungen direkt editieren In der Spalte „Beschreibung“ findet ihr mehrzeilige [Inputfelder](/documentation/input). Damit könnt ihr Aufgaben direkt bearbeiten – ohne zusätzliche Klicks oder das Öffnen einzelner Datensätze. ## 📅 Termine schnell anpassen Mit dem eingebauten Datumsfeld aktualisiert ihr Fälligkeiten im Handumdrehen. Einfach das neue Datum auswählen – fertig. ## 📊 Fortschritt immer im Blick Die integrierte [ProgressBar](/documentation/progress-bar) zeigt euch auf einen Blick, wie weit ihr mit den Aufgaben seid – farblich visualisiert und jederzeit aktuell. ## ➕ Unteraufgaben direkt hinzufügen Per [Button](/documentation/buttons) erstellt ihr neue Unteraufgaben in einem Ninox-Popup. So lassen sich Aufgaben flexibel verfeinern und strukturieren. ## 🔄 Status einfach umstellen Ein [Dropdown](/documentation/select)-Menü ermöglicht die direkte Änderung des Aufgabenstatus – schnell, intuitiv und ohne Umwege. ## 🗑️ Aufräumen leicht gemacht Ein Klick auf das [Löschen-Icon](/user/account/icons) entfernt überflüssige Aufgaben direkt aus der Tabelle. Schnell, effizient und ohne Extra-Schritte. ## 🎨 Farbige Übersicht Die dynamische groupRowColor-Funktion färbt die Zeilen je nach Fortschritt automatisch ein – für maximale Übersicht und visuelle Klarheit. ### ⚙️ Automationen weiter nutzen Die Widgets greifen nicht in eure Datenstruktur ein – ihr könnt weiterhin alle gewohnten Automationen in Ninox nutzen. Zum Beispiel: Setzt automatisch das Datum auf `today()` bei Abschluss einer Aufgabe – alles über „Trigger nach Änderung“ steuerbar. ## Fazit Mit dieser **Custom Table** bringt ihr eure Aufgabenverwaltung auf ein neues Level. Interaktive Mini Widgets wie Checkbox, Input, Progress Bar, Button, Select, Icon und Pagination machen eure Tabelle zum leistungsstarken UI-Element. 🚀 **Effizienz trifft Übersicht – direkt in Ninox.** ## Anwendungscode des Beispiels ```javascript let current := this; let list := (flteredList_Aufgaben[cnt(Auftrag.Aufgaben) > 5] order by cnt(Auftrag.Aufgaben)); //Ihr könnt euch Listen in extra Formel-Feldern vorfiltern, damit der Code nicht so lang wird let maxEntries := 'Einträge pro Seite'; // Hilfsfeld Zahl let currentPage := 'Aktuelle Seite'; // Hilfsfeld Zahl let paginatedList := unique(record(Aufgaben,0))[!= 0]; for listItem in slice(list.Nr, maxEntries * (currentPage - 1), maxEntries * currentPage) do paginatedList := unique(paginatedList, record(Aufgaben,listItem)) end; let data := { uniqueListId: "Aufgaben Demo 1", tableId: tableId(first(list)), height: "600px", width: "100px", groupedBy: "Aufgabe", groupsCollapsed: false, embedded: false, table: paginatedList.[{ recordId: Nr, rowColor: "", rowHeight: "auto", rowPaddingY: "10px", groupRowColor: let currentRecord := Nr; if cnt(list[Checkliste.Nr = currentRecord.Checkliste.Nr and Erledigt = true]) = cnt(list[Checkliste.Nr = currentRecord.Checkliste.Nr]) then "#dcf2de" else "#eee" end, columns: [{ field: "Erledigt", title: arcCheckBox({ value: if cnt(list[Erledigt = true]) != cnt(list) then false else if cnt(list[Erledigt = null]) = cnt(list) then null else if cnt(list[Erledigt = true]) = cnt(list) then true end end end }), value: arcCheckBox({ value: Erledigt }), width: "40px", align: "center", fixed: "left", actions: [{ recordId: Nr, type: "update", field: "G", value: if Erledigt = true then null else true end, headerAction: true }], groupValue: arcCustomIcon({ name: "caret-circle-double-down-fill" }) }, { field: "Aufgabe", title: "Aufgabe", width: "", value: replace(Aufgabe, ">", "❯"), actions: [{ recordId: Nr, type: "popup", showPopupButton: true }], groupValue: html("**" + Auftrag.Titel + "**"), groupByValue: Auftrag.Nr }, { field: "'Erledigt am'", title: "Beschreibung", value: arcCustomInput({ uniqueId: "Mein-Textarea-" + Nr, recordId: Nr, fieldId: "B", title: Beschreibung, value: Beschreibung, type: "textarea", embedded: true, disabled: false, width: "", height: "auto", minHeight: "40px", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "", borderWidth: "", borderColor: "", borderRadius: "", placeholderSettings: { value: "Beschreibe die Aufgabe..." }, focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, alignX: "left", paddingX: "" }), align: "left", width: "280px" }, { field: "'Erledigt am'", title: "Erledigt am", value: arcCustomInput({ uniqueId: "Mein-Input-" + Nr, recordId: Nr, fieldId: "A1", value: Startdatum, type: "date", embedded: true, disabled: false, width: "", height: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "#fff6d8", borderWidth: "", borderColor: "", borderRadius: "", placeholderSettings: { value: "Datum eintragen", fontColor: "#e9595c", backgroundColor: "#fff7f7" }, focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, alignX: "left", paddingX: "" }), align: "left", width: "180px" }, { field: "Aufgaben", title: "Aufgaben", value: arcCustomProgressBar({ uniqueId: "", width: "", fontSize: "", fontColor: "", backgroundColor: "", progressColor: "", valueTotal: cnt(Unteraufgaben), valueProgress: cnt(Unteraufgaben[Erledigt = true]), valueText: cnt(Unteraufgaben[Erledigt = true]) + "/" + cnt(Unteraufgaben) }), align: "left", width: "150px", groupValue: arcCustomProgressBar({ uniqueId: "Erledigte Aufgaben", title: "", width: "", fontSize: "", fontColor: "", backgroundColor: "#fff", progressColor: "", valueTotal: cnt(Auftrag.Aufgaben), valueProgress: cnt(Auftrag.Aufgaben[Erledigt = true]), valueText: cnt(Auftrag.Aufgaben[Erledigt = true]) + "/" + cnt(Auftrag.Aufgaben) }) }, { field: "", title: "", value: arcCustomButton({ uniqueId: "Unteraufgabe erstellen" + Nr, title: "Unteraufgabe", width: "100%", icon: arcCustomIcon({ name: "plus-bold", color: "#0062AA" }), fontSize: "", fontColor: "#0062AA", iconColor: "#0062AA", backgroundColor: "#0062AA33", borderColor: "#0062AA", borderRadius: "50px", showBadge: false, badgeTitle: cnt(Unteraufgaben), badgeColor: "", badgeBackground: "", badgeBorderColor: "", badgePosition: "", hoverActions: { fontColor: "#ffffff", iconColor: "#ffffff", backgroundColor: "#0062AA", borderColor: "#0062AA", animation: "0.5s" }, actions: [{ type: "create", tableId: "B", popup: true, changeFieldValues: [{ fieldId: "S", value: number(Nr) }] }] }), align: "left", width: "180px" }, { field: "Status", title: "Status", value: arcCustomSelect({ uniqueId: "Status" + Nr, currentValue: text(Status), recordId: Nr, field: "Q", width: "100%", height: "30px", fontSize: "10px", placeholder: "Status wählen", emptyValue: "Zurücksetzen", items: let id := Nr; (select Status_Aufgabe).[{ title: Bezeichnung, active: number(id.Status) = number(Nr), value: number(Nr) }] }), align: "right", width: "150px" }, { field: "delete", title: arcCustomIcon({ name: "trash" }), width: "40px", align: "center", value: arcCustomIcon({ name: "trash" }), actions: [{ recordId: Nr, type: "delete" }] }] }], footer: { showFooter: true, showActionButton: false, actionButtonTitle: "Neue Aufgabe hinzufügen", leftSideContent: arcCustomButton({ uniqueId: "Button popUp" + Nr, title: "Neue Checkliste hinzufügen", width: "", icon: arcCustomIcon({ name: "plus-bold", color: "#0062AA" }), fontSize: "", fontColor: "#0062AA", iconColor: "#0062AA", backgroundColor: "#0062AA33", borderColor: "#0062AA", borderRadius: "50px", showBadge: false, badgeTitle: "", badgeColor: "", badgeBackground: "", badgeBorderColor: "", badgePosition: "", hoverActions: { fontColor: "#ffffff", iconColor: "#ffffff", backgroundColor: "#0062AA", borderColor: "#0062AA", animation: "0.8s" }, actions: [{ type: "create", tableId: "C", popup: true, changeFieldValues: [{ fieldId: "", value: "" }, { fieldId: "", value: "" }] }] }), rightSideContent: arcCustomPagination({ uniqueId: Nr, editable: true, recordId: current.Nr, fieldId: "P", direction: "horizontal", title: current.'Aktuelle Seite', value: current.'Aktuelle Seite', total: ceil(cnt(list) / maxEntries), totalPrefix: " von " }) } }; arcCustomTable(data) ``` --- # Dashboardansicht mit Grid Source: https://www.arc-rider.com/docs/examples/dashboardansicht-mit-grid # Dashboard mit CSS Grid in React Kennzahlen, Fortschritt und eine Tabelle in einem responsiven Dashboard anordnen. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { ArcWidgetKpiBar, ArcWidgetTable, ArcWidgetProgressBar, } from '@arcrider/arcwidgets-react'; export default function Example() { return (

    Kennzahlen

    Fortschritt

    Aktuell

    ); } ``` ## So funktioniert es Das Raster verwendet CSS Grid. Das React-Paket exportiert kein ArcWidgetGrid. Die Widgets lassen sich ebenso in bestehende Layout-Komponenten deiner App einbetten. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Dashboardansicht mit Grid # Dashboards neu gedacht: Grid-Layout in Ninox Mit dem `arcCustomGrid`-Widget lassen sich individuelle Dashboards und Übersichtsseiten in einem flexiblen [Grid](/documentation/custom-grid)-Layout gestalten. Kombiniere beliebige Widgets, Texte oder [Layout](/documentation/custom-layout)-Blöcke und positioniere sie pixelgenau – ob im internen Cockpit oder für externe Kundenansichten. Das folgende Beispiel zeigt ein einfaches Dashboard-Setup mit drei Spalten und mehreren [Cards](/documentation/cards), Texten und [Icons](/documentation/custom-icon): ## Anwendungscode des Beispiels ```javascript let current := this; arcCustomLayout({ uniqueId: Nr, embedded: false, fullscreen: true, direction: "horizontal", alignX: "center", alignY: "center", width: "", height: "700px", gap: "50px", backgroundColor: "#000000", paddingY: "30px", paddingX: "30px", blocks: [{ width: "", height: "100%", lineHeight: "", alignX: "left", color: "", styles: " overflow-y: scroll;", value: arcCustomGrid({ uniqueId: "", width: "100%", height: "100%", columns: "auto-fit", columnWidth: "250px", gap: "10px", paddingY: "20px", paddingX: "20px", styles: "/*grid-template-columns: repeat(auto-fit, 1fr);*/", blocks: [{ blockId: "", width: "", height: "100%", alignX: "center", alignY: "center", paddingY: "", paddingX: "", backgroundColor: "", lineHeight: "", styles: "overflow-y: scroll;", value: arcCustomGrid({ uniqueId: "Dashboard Grid", styles: "", width: "100%", height: "100%", columns: "auto-fill", columnWidth: "200px", gap: "10px", paddingY: "", paddingX: "", blocks: [{ blockId: "--------- GRID BLOCK 1 ----------", alignX: "center", alignY: "right", height: "100%", width: "100%", clickAction: { type: "fullscreen", recordId: current.Nr }, value: arcCustomCard({ uniqueId: "Card 1", backgroundColor: "#fff", width: "100%", height: "100%", borderRadius: "10px", icon: arcCustomIcon({ name: "clipboard-text", containerSize: "50px", borderSize: "1px", borderColor: "", iconSize: "25px", borderRadius: "50px", color: "#FF7403", backgroundColor: "#FEEFC4" }), title: "Aufgaben", value: "Lorem Ipsum is simply dummy text Lorem Ipsum is simply dummy text" }) }, { blockId: "--------- GRID BLOCK 2 ----------", alignX: "center", alignY: "right", height: "100%", width: "100%", value: arcCustomCard({ uniqueId: "Card 2", backgroundColor: "#fff", width: "100%", height: "100%", borderRadius: "10px", icon: arcCustomIcon({ name: "users", containerSize: "50px", borderSize: "1px", borderColor: "", iconSize: "25px", borderRadius: "50px", color: "#C637E4", backgroundColor: "#FAE6FF" }), title: "Mitarbeiter", value: "Lorem Ipsum is simply dummy text Lorem Ipsum is simply dummy text" }) }, { blockId: "--------- GRID BLOCK 3 ----------", alignX: "center", alignY: "right", height: "100%", width: "100%", value: arcCustomCard({ uniqueId: "Card 3", backgroundColor: "#fff", width: "100%", height: "100%", borderRadius: "10px", icon: arcCustomIcon({ name: "folder", containerSize: "50px", borderSize: "1px", borderColor: "", iconSize: "25px", borderRadius: "50px", color: "#42B000", backgroundColor: "#DEFCCB" }), title: "Buchhaltung", value: "Lorem Ipsum is simply dummy text Lorem Ipsum is simply dummy text" }) }, { blockId: "--------- GRID BLOCK 4 ----------", alignX: "center", alignY: "right", height: "100%", width: "100%", value: arcCustomCard({ uniqueId: "Card 4", backgroundColor: "#fff", width: "100%", height: "100%", borderRadius: "10px", icon: arcCustomIcon({ name: "clock", containerSize: "50px", borderSize: "1px", borderColor: "", iconSize: "25px", borderRadius: "50px", color: "#944810", backgroundColor: "#ECD8C8" }), title: "Zeiterfassung", value: "Lorem Ipsum is simply dummy text Lorem Ipsum is simply dummy text" }) }, { blockId: "--------- GRID BLOCK 5 ----------", alignX: "center", alignY: "right", height: "100%", width: "100%", value: arcCustomCard({ uniqueId: "Card 5", backgroundColor: "#fff", width: "100%", height: "100%", borderRadius: "10px", icon: arcCustomIcon({ name: "buildings", containerSize: "50px", borderSize: "1px", borderColor: "", iconSize: "25px", borderRadius: "50px", color: "#E80102", backgroundColor: "#FFEDEC" }), title: "Kunden", value: "Lorem Ipsum is simply dummy text Lorem Ipsum is simply dummy text" }) }, { blockId: "--------- GRID BLOCK 6 ----------", alignX: "center", alignY: "right", height: "100%", width: "100%", value: arcCustomCard({ uniqueId: "Card 6", backgroundColor: "#fff", width: "100%", height: "100%", borderRadius: "10px", icon: arcCustomIcon({ name: "file", containerSize: "50px", borderSize: "1px", borderColor: "", iconSize: "25px", borderRadius: "50px", color: "#028BFF", backgroundColor: "#E1F1FF" }), title: "Dokumente", value: "Lorem Ipsum is simply dummy text Lorem Ipsum is simply dummy text" }) }] }) }, { blockId: "", width: "", height: "100%", alignX: "center", alignY: "center", paddingY: "", paddingX: "", backgroundColor: "", lineHeight: "", styles: "", value: arcCustomCard({ uniqueId: "Card 6", backgroundColor: "#fff", width: "100%", height: "100%", borderRadius: "10px", icon: arcCustomIcon({ name: "kanban", containerSize: "50px", borderSize: "1px", borderColor: "", iconSize: "25px", borderRadius: "50px", color: "#028BFF", backgroundColor: "#E1F1FF" }), title: "Projekte", value: "I'm baby pitchfork williamsburg twee asymmetrical direct trade, fam blue bottle kombucha slow-carb franzen. Poke gochujang pop-up meditation. Waistcoat vinyl vice small batch photo booth migas iPhone pug kale chips jean shorts poutine. Intelligentsia dreamcatcher plaid, prism fashion axe kale chips typewriter marxism wayfarers enamel pin readymade chia. Pok pok mixtape everyday carry cupping, poke slow-carb skateboard." }) }] }) }] }) ``` --- # Datenaufnahme in Schritten Source: https://www.arc-rider.com/docs/examples/datenaufnahme-in-schritten # Ein Formular in Schritten in React Eingabe, Prüfung und Abschluss durch React-State verbinden. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetSteps, ArcWidgetInput, ArcWidgetButton, } from '@arcrider/arcwidgets-react'; export default function Example() { const [step, setStep] = useState(0); const [name, setName] = useState(''); const [error, setError] = useState(''); return ( <> ({ title, value: String(index + 1), backgroundColor: index === step ? '#34705b' : '#e5e7eb', })), }} /> {step === 0 && ( setName(String(v ?? ''))} /> )} {step === 1 &&

    Bitte prüfen: {name}

    } {step === 2 &&

    Erfasst: {name}

    } {step < 2 && ( { if (!name.trim()) { setError('Bitte einen Namen eingeben.'); return; } setError(''); setStep((s) => s + 1); }} /> )} {step > 0 && ( )}

    {error}

    ); } ``` ## So funktioniert es Vor dem nächsten Schritt wird die Pflichtangabe geprüft. Das Steps-Widget zeigt den Zustand; Fachlogik und Navigation bleiben in der Anwendung. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Datenaufnahme in Schritten # Datenaufnahme in Schritten Wenn du im Außendienst tätig bist und schnell sowie unkompliziert Daten erfassen musst, ist eine klare und gut strukturierte Eingabemaske unerlässlich. Besonders bei einer großen Anzahl an zu erfassenden Feldern ist es sinnvoll, die Dateneingabe in mehrere Schritte zu unterteilen. Auf diese Weise wird der Benutzer nicht überfordert und kann schneller sowie geordneter die notwendigen Informationen eingeben. In diesem Beitrag zeigen wir dir, wie du mit einfachen Tricks und nützlichen Widgets eine benutzerfreundliche Oberfläche gestalten kannst, die dir die Datenerfassung erheblich erleichtert. Erfahre, wie du die Effizienz deines Arbeitsalltags steigerst und dabei stets den Überblick behältst. Widget: [ Custom Layout ](/documentation/custom-layout)mit integrierten Mini Widgets: [Input](/documentation/input), [Button](/documentation/buttons), [Text](/documentation/text) und [Icon](/documentation/custom-icon). ## Anwendungscode Teil 1 Layouts & Überschriften ```javascript let current := this; let namenLayout := arcCustomLayout({ uniqueId: "namenLayout " + Nr, embedded: true, fullscreen: false, direction: "vertical", alignX: "center", alignY: "center", width: "490px", height: "100%", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", blocks: [{ width: "", height: "auto", lineHeight: "", alignX: "center", color: "", styles: "", value: arcCustomInput({ uniqueId: "vorname" + Nr, recordId: Nr, fieldId: "G", title: text(Vorname), value: text(Vorname), type: "text", embedded: true, disabled: false, width: "100%", height: "40px", alignX: "left", paddingY: "", paddingX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "#F0F0F0", borderWidth: "", borderColor: "", borderRadius: "", focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, labelSettings: { title: "Vorname", fontSize: "", alignX: "", gap: "10px" } }) }, { width: "", height: "auto", lineHeight: "", alignX: "center", color: "", value: arcCustomInput({ uniqueId: "Nachname" + Nr, recordId: Nr, fieldId: "H", title: text(Nachname), value: text(Nachname), type: "text", embedded: true, disabled: false, width: "100%", height: "40px", alignX: "left", paddingY: "", paddingX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "#F0F0F0", borderWidth: "", borderColor: "", borderRadius: "", focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, labelSettings: { title: "Nachname", fontSize: "", alignX: "", gap: "10px" } }) }] }); let adressLayout := arcCustomLayout({ uniqueId: "adressLayout " + Nr, embedded: true, fullscreen: false, direction: "vertical", alignX: "center", alignY: "center", width: "490px", height: "100%", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", blocks: [{ width: "", height: "auto", lineHeight: "", alignX: "center", color: "", styles: "", value: arcCustomInput({ uniqueId: "Straße u Hausnr" + Nr, recordId: Nr, fieldId: "I", title: text('Straße + Hausnr'), value: text('Straße + Hausnr'), type: "text", embedded: true, disabled: false, width: "100%", height: "40px", alignX: "left", paddingY: "", paddingX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "#F0F0F0", borderWidth: "", borderColor: "", borderRadius: "", focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, labelSettings: { title: "Straße & Hausnummer", fontSize: "", alignX: "", gap: "10px" } }) }, { width: "", height: "auto", lineHeight: "", alignX: "center", color: "", value: arcCustomLayout({ uniqueId: "adressZeile PLZ und Ort " + Nr, embedded: true, fullscreen: false, direction: "horizontal", alignX: "left", alignY: "center", width: "", height: "", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", blocks: [{ width: "200px", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomInput({ uniqueId: "PLZ" + Nr, recordId: Nr, fieldId: "J", title: text(PLZ), value: text(PLZ), type: "text", embedded: true, disabled: false, width: "100%", height: "40px", alignX: "left", paddingY: "", paddingX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "#F0F0F0", borderWidth: "", borderColor: "", borderRadius: "", focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, labelSettings: { title: "PLZ", fontSize: "", alignX: "", gap: "10px" } }) }, { width: "", height: "auto", lineHeight: "", alignX: "left", color: "", value: arcCustomInput({ uniqueId: "Ort" + Nr, recordId: Nr, fieldId: "K", title: text(Ort), value: text(Ort), type: "text", embedded: true, disabled: false, width: "100%", height: "40px", alignX: "left", paddingY: "", paddingX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "#F0F0F0", borderWidth: "", borderColor: "", borderRadius: "", focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, labelSettings: { title: "Ort", fontSize: "", alignX: "", gap: "10px" } }) }] }) }] }); let kontaktLayout := arcCustomLayout({ uniqueId: "kontaktLayout " + Nr, embedded: true, fullscreen: false, direction: "vertical", alignX: "center", alignY: "center", width: "490px", height: "100%", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", blocks: [{ width: "", height: "auto", lineHeight: "", alignX: "center", color: "", styles: "", value: arcCustomInput({ uniqueId: "kontakt" + Nr, recordId: Nr, fieldId: "N", title: text(Telefon), value: text(Telefon), type: "text", embedded: true, disabled: false, width: "100%", height: "40px", alignX: "left", paddingY: "", paddingX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "#F0F0F0", borderWidth: "", borderColor: "", borderRadius: "", focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, labelSettings: { title: "Telefon", fontSize: "", alignX: "", gap: "10px" } }) }, { width: "", height: "auto", lineHeight: "", alignX: "center", color: "", value: arcCustomInput({ uniqueId: "Ansprechpartner" + Nr, recordId: Nr, fieldId: "O", title: text('E-Mail'), value: text('E-Mail'), type: "text", embedded: true, disabled: false, width: "100%", height: "40px", alignX: "left", paddingY: "", paddingX: "", fontColor: "", fontSize: "", fontWeight: "", backgroundColor: "#F0F0F0", borderWidth: "", borderColor: "", borderRadius: "", focusAction: { width: "", showFocusOutline: "", outlineWidth: "", outlineColor: "" }, labelSettings: { title: "E-Mail", fontSize: "", alignX: "", gap: "10px" } }) }] }); arcCustomLayout({ uniqueId: "Content Aufnahme " + Nr, embedded: false, fullscreen: false, direction: "vertical", alignX: "center", alignY: "top", width: "", height: "450px", gap: "", backgroundColor: "", paddingY: "", paddingX: "", blocks: [{ width: "", height: "100px", lineHeight: "", alignX: "center", color: "", value: arcCustomText({ uniqueId: "headline" + Nr, embedded: true, width: "", height: "", alignY: "", alignX: "center", fontSize: "27px", fontWeight: "700", fontColor: "", lineHeight: "", textWrap: "", textScroll: "", textAlign: "", textDirection: "horizontal", textDecoration: { type: "", style: "", color: "", thickness: "" }, value: if Menu = "1" then "Name" else if Menu = "2" then "Adresse" else if Menu = "3" then "Kontakt" end end end }) }, { width: "", height: "fraction", lineHeight: "", alignX: "center", color: "", value: if Menu = "1" then namenLayout else if Menu = "2" then adressLayout else if Menu = "3" then kontaktLayout end end end }, { width: "", height: "100px", lineHeight: "", alignX: "center", color: "", styles: "", value: menuSteps }] }) ``` ## Anwendungscode Teil 2 Menü Buttons ```javascript let current := this; function buttonSubmenu(data : any) do arcCustomButton({ uniqueId: "Button submenu " + data.uniqueId, title: data.title, width: "", height: "", alignY: "", alignX: "", icon: arcCustomIcon({ name: "check-circle-fill", color: if data.completed = true then "#b5db3b" else "#ddd" end, iconSize: "19px", containerSize: "21px", backgroundColor: "#fff", borderSize: "", borderColor: "#fff", borderRadius: "50%" }), fontSize: "", fontColor: if data.active = true then "#fff" else "#777" end, backgroundColor: if data.active = true then "#4970FF" else if data.completed = true then "#f4fcd9" else "#fff" end end, borderColor: if data.active = true then "#4970FF" else "#777" end, borderRadius: "18px" }) end; let menuList := [{ title: "Name", value: "1", showLine: true, completed: Vorname != null and Nachname != null }, { title: "Adresse", value: "2", showLine: true, completed: 'Straße + Hausnr' != null and PLZ != null and Ort != null }, { title: "Kontakt", value: "3", showLine: true, completed: Telefon != null and 'E-Mail' != null }]; arcCustomLayout({ uniqueId: "menuSteps " + Nr, embedded: true, fullscreen: false, direction: "horizontal", alignX: "center", alignY: "center", width: "", height: "", gap: "5px", backgroundColor: "", paddingY: "", paddingX: "", blocks: menuList.[{ width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", clickAction: { type: "update", recordId: current.Nr, field: "M", value: value }, value: buttonSubmenu({ uniqueId: "buttonSubmenu " + value + " " + Nr, title: title, value: value, active: current.Menu = value, completed: completed }) }] }) ``` --- # Image-Slider Source: https://www.arc-rider.com/docs/examples/image-slider # Bildergalerie mit Slideshow in React Zwei selbst erzeugte Bilder anzeigen und den aktiven Slide ausgeben. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetSlideshow } from '@arcrider/arcwidgets-react'; // Statisches, selbst erzeugtes HTML; keine ungeprüften Benutzereingaben einsetzen. const images = ['Grundriss A', 'Grundriss B'].map((label) => { const svg = `${label}`; return { value: `${label}`, }; }); export default function Example() { const [index, setIndex] = useState(0); return ( <> setIndex(p.index)} />

    Bild {index + 1} von {images.length}

    ); } ``` ## So funktioniert es `items[].value` ist hier ein HTML-String mit einem Bild. Slideshow unterstützt aktuell keine JSX-Slots. Fremde Texte oder URLs dürfen nicht ungeprüft in HTML eingesetzt werden. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Image-Slider # 📸 Image-Slider in Ninox ![](https://framerusercontent.com/images/Zrw6W9ePdP2qQQkzRtUz8KLZgA.png) So baust du deinen individuell gestalteten Bildwechsler – ganz ohne externe Galerie-Tools. Du möchtest mehrere Bilder aus einer Tabelle übersichtlich und interaktiv durchblättern? In diesem Beitrag zeigen wir dir Schritt für Schritt, welche Widgets und Ninox-Einstellungen du brauchst – inklusive Beispielcode zur Umsetzung. ## 🧩 Benötigte Widgets - [Custom Layout](/documentation/custom-layout) - [Custom Image](/documentation/images) - [Custom Text](/documentation/text) - [Button](/documentation/buttons) ## 📁 Aufbau deiner Tabelle Du brauchst eine Tabelle mit folgenden Feldern:
    Feldname Typ
    `Bild` Dokumenten-/Bild-Feld
    `shareUrl` URL-Feld
    `sortId` Zahlenfeld (zur Sortierung)
    `Beschreibung` Text-Feld
    `currentImage` Hilfsfeld (Text), z. B. im Layout
    💡** Tipp:** Das Feld `currentImage` dient der Steuerung des aktuell angezeigten Bilds im Slider. ![](https://framerusercontent.com/images/lm9TX63iMrK06COA4FkH9bpn48.png) ## 🛠️ Vorgehensweise Sobald du die oben genannten Voraussetzungen umgesetzt hast, kannst du mit dem eigentlichen Aufbau des Image-Sliders beginnen. ### 1. Code einfügen Der Anwendungscode für den Slider wird in einem **Formelfeld** (z. B. `imageSlider_Layout`) platziert. Dieses Feld sorgt für die visuelle Darstellung der Bilder und die Navigation. ### 2. Wichtige Ninox-Einstellungen In deiner Bilder-Tabelle solltest du auf folgende Punkte achten: - `sortId` (Zahlenfeld) → Dieses Feld muss für jedes Bild mit einer aufsteigenden Nummer befüllt sein (z. B. 1, 2, 3 …). → Es steuert die Reihenfolge, in der die Bilder im Slider angezeigt werden. - `shareUrl` (URL-Feld) → Dieses Feld enthält den direkten Link zur Bilddatei – wichtig für die Anzeige im `Custom Image`-Widget. → Am besten wird dieser Wert automatisch beim Upload eines Bildes gesetzt. **shareUrl automatisch hinterlegen** Um die `shareUrl` automatisch zu befüllen, kannst du im **Bild-Feld** einen **Trigger nach Änderung** hinterlegen. So wird die URL jedes Mal aktualisiert, wenn ein neues Bild hochgeladen wird. **Beispiel-Code für den Trigger nach Änderung:** ```javascript shareURL := shareFile(Bild) ``` ![](https://framerusercontent.com/images/Z1ncYPYpGQ7ARLeLsiDWjtVE.png) ### 3. Hilfsfeld erstellen Lege auf der Tabelle, in der du den Image-Slider darstellen möchtest, ein **Textfeld** an – z. B. `currentImage`. Dieses Feld speichert die aktuell angezeigte Bild-ID und wird über die Navigationsbuttons im Slider verändert.
  • ### Anwendungscode einfügen
  • Erstelle nun ein **Formelfeld**, z. B. `imageSlide_Layout`, und füge dort den Anwendungscode ein. Achte dabei auf folgende Anpassungen: ```javascript let current := this; let nextImage := number(first(Images_Slider[sortId > record(Images_Slider,number(current.currentImage)).sortId] order by sortId).Nr); let previousImage := number(last(Images_Slider[sortId < record(Images_Slider,number(current.currentImage)).sortId] order by sortId).Nr); let firstImage := number(first(Images_Slider order by sortId)); let lastImage := number(last(Images_Slider order by sortId)); arcCustomLayout({ uniqueId: "fullLayout_Image-Slider_" + Nr, embedded: false, fullscreen: false, direction: "horizontal", alignX: "center", alignY: "center", width: "", height: "auto", gap: "", backgroundColor: "", paddingY: "", paddingX: "30px", blocks: [{ width: "", height: "auto", lineHeight: "", alignX: "center", color: "", styles: "", value: arcCustomButton({ uniqueId: "leftButton " + Nr, title: "", width: "", height: "", alignY: "", alignX: "", icon: arcCustomIcon({ name: "caret-left", color: "#9ba3af", iconSize: "80px" }), fontSize: "", fontColor: "", backgroundColor: "transparent", borderColor: "transparent", borderRadius: "", actions: [{ recordId: Nr, type: "update", field: "X", value: if previousImage then previousImage else lastImage end }] }) }, { width: "auto", height: "auto", lineHeight: "", alignX: "center", color: "", value: arcCustomLayout({ uniqueId: "Image und Description " + Nr, embedded: true, fullscreen: false, direction: "vertical", alignX: "center", alignY: "center", width: "", height: "", gap: "20px", backgroundColor: "", paddingY: "", paddingX: "", blocks: [{ width: "", height: "auto", lineHeight: "", alignX: "center", color: "", styles: "", value: arcCustomText({ uniqueId: "Headline " + Nr, embedded: true, width: "", height: "", alignY: "", alignX: "center", fontSize: "20px", fontWeight: "", fontColor: "", lineHeight: "", textWrap: "", textScroll: "", textAlign: "", textDirection: "horizontal", textDecoration: { type: "", style: "", color: "", thickness: "" }, value: record(Images_Slider,number(currentImage)).Bezeichnung }) }, { width: "", height: "auto", lineHeight: "", alignX: "center", color: "", value: arcCustomImage({ uniqueId: "Image " + Nr, title: "", width: "400px", height: "400px", backgroundSize: "cover", backgroundColor: "", style: "", radius: "5px", borderColor: "", link: record(Images_Slider,number(currentImage)).shareURL }) }] }) }, { width: "", height: "100% ", lineHeight: "", alignX: "center", color: "", value: arcCustomButton({ uniqueId: "leftButton " + Nr, title: "", width: "", height: "", alignY: "", alignX: "", icon: arcCustomIcon({ name: "caret-right", color: "#9ba3af", iconSize: "80px" }), fontSize: "", fontColor: "", backgroundColor: "transparent", borderColor: "transparent", borderRadius: "", actions: [{ recordId: Nr, type: "update", field: "X", value: if nextImage then nextImage else firstImage end }] }) }] }) ``` 💡 **Tipp: **`**embedded**`**-Parameter richtig setzen** Je nachdem, wo du den Image-Slider einsetzen möchtest, musst du den Parameter `embedded` korrekt setzen: - `embedded: false` → Wenn du den Slider direkt in einem Ninox-Formularfeld (z. B. als eigenes Layout-Feld) nutzt. → Der Slider verhält sich wie ein eigenständiges Widget. - `embedded: true` → Wenn du den Slider innerhalb eines anderen Widgets oder in ein größeres Custom Layout einbettest. → Der Slider passt sich dann an die umgebende Struktur an. ### 5. Tabellen- & Feldbezüge setzen Gleich zu Beginn des Codes müssen deine Variablen korrekt auf deine **eigene Bilder-Tabelle** und die darin enthaltenen Felder verweisen. ✍️ Was musst du hier anpassen? - `**Images_Slider**` → Ersetze diesen Namen durch den tatsächlichen Namen deiner Ninox-Tabelle mit den Bildern. - `**sortId**` → Ersetze durch den Namen deines Sortierfelds in dieser Tabelle (z. B. `Reihenfolge`). ```javascript let nextImage := number(first(Images_Slider[sortId > record(Images_Slider,number(current.currentImage)).sortId] order by sortId).Nr); let previousImage := number(last(Images_Slider[sortId < record(Images_Slider,number(current.currentImage)).sortId] order by sortId).Nr); let firstImage := number(first(Images_Slider order by sortId)); let lastImage := number(last(Images_Slider order by sortId)); ``` In den `Custom Buttons` des Sliders musst du die **fieldId** des Feldes `currentImage` angeben, damit die Buttons korrekt funktionieren. Achte darauf, die `fieldId` **in beiden Buttons** zu ersetzen: - Button `"caret-left"` - Button `"caret-right"` ```javascript value: arcCustomButton({ uniqueId: "leftButton " + Nr, title: "", width: "", height: "", alignY: "", alignX: "", icon: arcCustomIcon({ name: "caret-left", color: "#9ba3af", iconSize: "80px" }), fontSize: "", fontColor: "", backgroundColor: "transparent", borderColor: "transparent", borderRadius: "", actions: [{ recordId: Nr, type: "update", field: "X", value: if previousImage then previousImage else lastImage end }] }) ``` ### 6. Finaler Schritt: Referenzen anpassen ✅ Setze in den Record-Referenzierungen den Namen deiner Bildertabelle und deines Feldes auf der Zieltabelle ein. ⚠️ Diese Anpassung ist entscheidend, damit die Navigation im Slider korrekt funktioniert. ### 🚀 Und jetzt: Loslegen! Wenn du alle Schritte umgesetzt hast, ist dein individuell konfigurierter **Image-Slider einsatzbereit.** Egal ob Team-Galerie, Portfolio oder Visual-Highlight – du kannst die Bilder jetzt ganz bequem durchblättern. 🏞️✨ --- # Individuelles Design in der CalendarWeek Source: https://www.arc-rider.com/docs/examples/individuelles-design-in-der-calendarweek # Termine gestalten und bearbeiten in React Einen Termin mit JSX-Inhalt anzeigen und seine Startzeit über ein Formular verändern. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetCalendarWeek, ArcWidgetBadge, } from '@arcrider/arcwidgets-react'; export default function Example() { const [hour, setHour] = useState(9); const [message, setMessage] = useState('Termin auswählen'); return ( <> Kundentermin ), dateFrom: new Date(2026, 8, 17).getTime(), dateTo: new Date(2026, 8, 17).getTime(), timeFrom: hour * 3600000, timeTo: (hour + 1) * 3600000, clickable: true, draggable: false, }, ], }} onEntryClick={() => setMessage(`Termin ab ${hour}:00 ausgewählt`)} />

    {message}

    ); } ``` ## So funktioniert es Die Startstunde wird in Millisekunden umgerechnet. Drag & Drop bleibt deaktiviert, da die aktuellen Drag-Callbacks keine vollständigen neuen Zielzeiten liefern. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Individuelles Design in der CalendarWeek # Individuelles Design in der CalendarWeek ![](https://framerusercontent.com/images/8GpUlcwkZdOmzYEMBkbiw4Fy2Y.png) In diesem Beitrag zeigen wir euch Schritt für Schritt, wie ihr mithilfe von **Layouts** und ergänzenden **Widgets** eine individuell gestaltete **Card** für die `customCalendarWeek` aufbauen könnt. 💡 Ihr lernt dabei nicht nur, wie sich Widgets elegant **ineinander verschachteln** lassen, sondern erhaltet auch direkt die passenden **Beispielcodes** aus unserem Setup. Ob als **direkte Vorlage** (Tabellennamen, Felder und IDs müsst ihr natürlich auf euer System anpassen) – oder als **Inspiration für eure eigenen Designs** im typischen arcRiderWidget-Stil: Dieser Guide hilft euch dabei, euren Kalender optisch und funktional aufs nächste Level zu bringen. ## Benötigte Widgets - **Widget: **[*customCalendarWeek*](/documentation/custom-calendar-week) - **Integrierte Mini Widgets: **[*Layout*](/documentation/custom-layout)*, *[*Button*](https://www.arc-rider.de/documentation/buttons)*, *[*Select*](https://www.arc-rider.de/documentation/select)*, *[*Icon*](https://www.arc-rider.de/documentation/custom-icon)*, *[*Image*](/documentation/images)* und *[*Text*](/documentation/text)*.* ## Let’s build together 🚀 Bevor wir loslegen, sorgt bitte dafür, dass eure **Ninox-Datenbank** die nötigen Tabellen und Felder enthält – entweder nutzt ihr bestehende Strukturen oder erstellt sie neu. Für das Layout der Card in diesem Use Case verwenden wir folgende Tabellen:
    Tabellen
    ![](https://framerusercontent.com/images/lP1viIyLdkCNeoESjqzUBAzWc.png) - Kunden - Mitarbeiter

    • Termine - Terminteilnehmer

    • - Custom Calendar

    • Status_Termine - Typ_Termin

    ***Tipp:*** Für die die Dynamische Auswahl können Hilfs/Settings Tabellen erstellt werden, wo Farb- und Iconwerte hinterlegt und dann in den jeweiligen Widgets aufgerufen werden können. So sind die Werte zentral und müssen nicht immer wieder manuell eingegeben werden, wenn man sie auch auf anderen Oberflächen in verschiedenen Widgets einsetzen möchte. ![](https://framerusercontent.com/images/eK0VYGXu2VKBnER4WlJUYOL0tM.png) ## Individuelles Design der Card ### Widgets übersichtlich integrieren Das Layout der calenderCard und die einzelnen zu integrierenden Widgets, werden in der Tabelle *Termine *erstellt, deren Records zum Schluss im customCalendarWeek aufgerufen werden. ![](https://framerusercontent.com/images/BRqVTA1AaUt8cqAWVWANW49wc.png) Hier erstellen wir für jede Zeile der Card ein fx-Feld mit je einem Layout und fx-Felder für Elemente, wie weitere Layouts, Select, Button. Das bietet gute Übersichtlichkeit beim Coden der Layouts, außerdem können verschiedene Designs schneller ausgetauscht/ersetzt/getestet werden. ### Line 1 ![](https://framerusercontent.com/images/44pvIfDknaVhBorSTm8eGTIrGl4.png) **Schritt 1:** Wir beginnen mit den ausgelagerten Elementen für die erste Zeile. #### select_status ![](https://framerusercontent.com/images/AL1lKCPYqxa2RpoLdIEnRi2cZkU.png) *Verwendete Widgets: customSelect + customLayout* Code: ```javascript let current := this; arcCustomSelect({ uniqueId: "Status Termin" + nr, recordId: nr, field: "Y", embedded: true, width: "100%", height: "25px", alignX: "", fontColor: "", backgroundColor: text(record(Status_Termin,number(Status)).Farbcode), borderColor: "none", borderWidth: "0px", borderRadius: "5px", itemsSettings: { width: "100%", height: "auto", borderRadius: "5px" }, clickPosition: "", placeholder: "", disabled: false, reset: { title: "reset", value: "", hide: true }, currentValue: arcCustomLayout({ uniqueId: "status-bezeichnung-current " + nr, embedded: true, direction: "horizontal", alignX: "left", alignY: "center", width: "100%", height: "auto", gap: "10px", backgroundColor: text(record(Status_Termin,number(Status)).Farbcode), paddingY: "1px", paddingX: "1px", styles: "border-radius: 5px;", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "100%", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: text(record(Status_Termin,number(Status)).Bezeichnung) }] }), items: let id := nr; (select Status_Termin).[{ title: arcCustomLayout({ uniqueId: "status-bezeichnung " + Nr, embedded: true, direction: "horizontal", alignX: "left", alignY: "center", width: "100%", height: "", gap: "0px", backgroundColor: Farbcode, paddingY: "3px", paddingX: "5px", styles: "border-radius: 5px;", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "100%", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: Bezeichnung }] }), active: number(id.Status) = number(Nr), value: number(Nr) }] }) ``` #### layout_line1_left ![](https://framerusercontent.com/images/PVJ8DYdrEqoq42ocr4Z7TVR674.png) *Verwendete Widgets: customLayout + customIcon + customText* Code: ```javascript let current := this; arcCustomLayout({ uniqueId: "calenderCard line1 left" + nr, embedded: true, direction: "horizontal", alignX: "center", alignY: "center", width: "100%", height: "100%", gap: "3px", backgroundColor: "", paddingY: "0px", paddingX: "0px", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "auto", height: "auto", alignX: "center", color: "", styles: "", clickAction: { recordId: nr, type: "popup" }, value: arcCustomIcon({ iconSet: "phosphor", name: text(record(Typ_Termin,number(Typ)).iconName), containerSize: "20px", iconSize: "20px", color: text(record(Typ_Termin,number(current.Typ)).colorDark) }) }, { width: "100%", height: "100%", alignX: "left", color: "", clickAction: { recordId: nr, type: "popup" }, value: arcCustomText({ uniqueId: "appointment-title" + nr, embedded: true, fontSize: "12px", fontWeight: "700", fontColor: text(record(Typ_Termin,number(current.Typ)).colorDark), lineHeight: "1.2", textDirection: "horizontal", value: text(Bezeichnung) }) }] }) ``` **Schritt 2: **Dann werden beide Elemente in das Layout für die erste Zeile gesetzt. #### calendarCard_line1 ![](https://framerusercontent.com/images/v6TEsRutvjB6BG1c5Ow8ZCfvLI.png) *Verwendete Widgets: customLayout* Code: ```javascript let current := this; arcCustomLayout({ uniqueId: "calenderCard line1 " + nr, embedded: true, direction: "horizontal", alignX: "center", alignY: "top", width: "100%", height: "100%", gap: "10px", backgroundColor: "", paddingY: "0px", paddingX: "0px", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "fraction", height: "auto", alignX: "center", color: "", value: layout_line1_left }, { width: "35%", height: "auto", alignX: "right", color: "", value: select_status }] }) ``` ### Line 2 ![](https://framerusercontent.com/images/yx2JRb0qA1pCBGY8bOldxpBJ9BE.png) **Schritt 1:** Wir beginnen wieder mit dem ausgelagerten Element für die zweite Zeile. #### button_customer ![](https://framerusercontent.com/images/BdZ0RuNR87kw7lkQHglNBX3YU4w.png) *Verwendete Widgets: customButton + customIcon* Code: ```javascript let current := this; let lilaDark := "#35339E"; let lilaMiddle := "#D2D2F7"; arcCustomButton({ uniqueId: "Button-kunde " + nr, title: text(Kunde.Name), embedded: true, width: "auto", height: "25px", alignY: "", alignX: "", paddingX: "", paddingY: "", gap: "5px", icon: arcCustomIcon({ name: "link", color: lilaDark, containerSize: "20px", borderSize: "", borderColor: "", borderRadius: "", iconSize: "20px" }), iconPosition: "left", fontSize: "12px", fontColor: lilaDark, backgroundColor: lilaMiddle, borderColor: lilaMiddle, borderRadius: "5px", showBadge: false, }) ``` **Schritt 2:** Dann wird das Element in das Layout in die zweite Zeile eingesetzt. #### calendarCard_line2 ![](https://framerusercontent.com/images/Y3WqsaZ1F91rdQrGG5rQi1P3Q.png) *Verwendete Widgets: customLayout + customText* Code: ```javascript let current := this; arcCustomLayout({ uniqueId: "calenderCard line2 " + nr, embedded: true, direction: "horizontal", alignX: "center", alignY: "top", width: "100%", height: "100%", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "50%", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomText({ uniqueId: "appointment-time" + nr, embedded: true, fontSize: "15px", fontWeight: "500", fontColor: text(record(Typ_Termin,number(current.Typ)).colorDark), lineHeight: "1.2", textDirection: "", value: text(Von) + " - " + text(Bis) + " Uhr" }) }, { width: "50%", height: "auto", lineHeight: "", alignX: "right", color: "", value: if Kunde != null then button_customer end, clickAction: { type: "popup", recordId: current.Kunde.Nr } }] }) ``` ### Line 3 ![](https://framerusercontent.com/images/O4s594gvSjPVo8E0fIqyLh6fb8.png) **Schritt 1:** Die dritte Zeile ist in zwei Layouts aufgeteilt, außerdem brauchen wir ein fx-Feld für die Berechnung der auf 3 Teilnehmer begrenzte Anzeige. #### paginatedList_teilnehmer Code: ```javascript let maxEntries := 3; let currentPage := 1; let list := Terminteilnehmer; let paginatedList := unique(record(Terminteilnehmer,0).Mitarbeiter)[!= 0]; for listItem in slice(list.Nr, maxEntries * (currentPage - 1), maxEntries * currentPage) do paginatedList := unique(paginatedList, record(Terminteilnehmer,listItem).Mitarbeiter) end; paginatedList ``` #### layout_buttons ![](https://framerusercontent.com/images/DWYLtB9xfkSKjIkIo0V9mLkPWtw.png) *Verwendete Widgets: customLayout + customButton + customIcon* Code: ```javascript let current := this; arcCustomLayout({ uniqueId: "layout-buttons " + nr, embedded: true, direction: "horizontal", alignX: "right", alignY: "center", width: "100%", height: "auto", gap: "5px", backgroundColor: "", paddingY: "0px", paddingX: "0px", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomButton({ uniqueId: "Button-paperclip " + nr, title: "", width: "25px", height: "25px", alignY: "", alignX: "", paddingX: "", paddingY: "", gap: "5px", icon: arcCustomIcon({ name: "paperclip-fill", color: text(record(Typ_Termin,number(current.Typ)).colorDark), containerSize: "25px", borderSize: "", borderColor: "", borderRadius: "", iconSize: "20px" }), iconPosition: "left", fontSize: "12px", fontColor: "", backgroundColor: "none", borderColor: "none", borderRadius: "5px", showBadge: false, actions: [{ type: "create", tableId: "", popup: true, changeFieldValues: [{ fieldId: "", value: "" }, { fieldId: "", value: "" }] }] }) }, { width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomButton({ uniqueId: "Button-message " + nr, title: "", width: "25px", height: "25px", alignY: "", alignX: "", paddingX: "", paddingY: "", gap: "5px", icon: arcCustomIcon({ name: "chat-circle-dots-fill", color: text(record(Typ_Termin,number(current.Typ)).colorDark), containerSize: "25px", borderSize: "", borderColor: "", borderRadius: "", iconSize: "20px" }), iconPosition: "left", fontSize: "12px", fontColor: "", backgroundColor: "none", borderColor: "none", borderRadius: "5px", showBadge: false, actions: [{ type: "", tableId: "", popup: true, changeFieldValues: [{ fieldId: "", value: "" }, { fieldId: "", value: "" }] }] }) }, { width: "auto", height: "", lineHeight: "", alignX: "left", color: "", styles: "", value: arcCustomButton({ uniqueId: "Button-list " + nr, title: "", width: "25px", height: "25px", alignY: "", alignX: "", paddingX: "", paddingY: "", gap: "5px", icon: arcCustomIcon({ name: "list-checks-fill", color: text(record(Typ_Termin,number(current.Typ)).colorDark), containerSize: "25px", borderSize: "", borderColor: "", borderRadius: "", iconSize: "20px" }), iconPosition: "left", fontSize: "12px", fontColor: "", backgroundColor: "none", borderColor: "none", borderRadius: "5px", showBadge: false, actions: [{ type: "", tableId: "", popup: true, changeFieldValues: [{ fieldId: "", value: "" }, { fieldId: "", value: "" }] }] }) }] }) ``` #### layout_teilnehmer ![](https://framerusercontent.com/images/uC5bZCbfPqD1bXCXINIPqdHsI.png) *Verwendete Widgets: customLayout + customImage* Code: ```javascript let current := this; let colorDark := text(record(Typ_Termin,number(current.Typ)).colorDark); let colorLight := text(record(Typ_Termin,number(current.Typ)).color); arcCustomLayout({ uniqueId: "layout-teilnehmer " + nr, embedded: true, direction: "horizontal", alignX: "left", alignY: "center", width: "100%", height: "", gap: "0px", backgroundColor: "", paddingY: "0px", paddingX: "0px", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: let imageList := paginatedList_teilnehmer.[{ width: "auto", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "margin-right: -10px; border: 2px solid; border-radius: 50px; border-color: " + colorLight + ";", value: arcCustomImage({ uniqueId: "Mitarbeier-image" + Nr, title: "Logo", width: "30px", height: "30px", backgroundSize: "cover", class: "", style: "", radius: "50px", borderColor: "", link: shareURL }), clickAction: { type: "popup", recordId: Nr } }]; let plusBubble := [if cnt(Terminteilnehmer) > 3 then { width: "30px", height: "30px", lineHeight: "", alignX: "center", color: colorDark, styles: "margin-left: 20px; border: 1px solid; border-radius: 50px; border-color: " + colorDark + "; font-size: 12px;", value: "+" + if cnt(Terminteilnehmer) > 3 then text(cnt(Terminteilnehmer) - 3) end } end]; array(imageList, plusBubble) }) ``` **Schritt 2:** Als nächstes werden beide customLayouts in das Layout der dritten Zeile eingesetzt. #### calendarCard_line3 ![](https://framerusercontent.com/images/grCqFlAt3BK6jGFhfciridlacU.png) *Verwendete Widgets: customLayout* Code: ```javascript let current := this; arcCustomLayout({ uniqueId: "calenderCard Line3 " + nr, embedded: true, direction: "horizontal", alignX: "center", alignY: "center", width: "", height: "auto", gap: "10px", backgroundColor: "", paddingY: "", paddingX: "", styles: "", scrollSettings: { scrollY: false, scrollX: false }, blocks: [{ width: "100%", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "", value: layout_teilnehmer }, { width: "100%", height: "auto", lineHeight: "", alignX: "right", color: "", value: layout_buttons }] }) ``` #### Card Design zusammenfügen **Abschließend **können die Zeilen im letzten Layout der calendarCard zusammengefügt werden. Damit die Card bei kürzeren Terminen noch vollständig angeschaut werden kann, denkt dran in den scrollSettings, scrollY: true zu setzen. ![](https://framerusercontent.com/images/RgWIpNKTTKCXdAjbZ10gAJuV4iI.png) *Verwendete Widgets: customLayout* Code: ```javascript let current := this; arcCustomLayout({ uniqueId: "calenderCard " + nr, embedded: true, direction: "vertical", alignX: "center", alignY: "top", width: "100%", height: "100%", gap: "10px", backgroundColor: record(Typ_Termin,number(current.Typ)).color, paddingY: "8px", paddingX: "8px", styles: "border-radius: 14px;", scrollSettings: { scrollY: true, scrollX: false }, blocks: [{ width: "100%", height: "auto", lineHeight: "", alignX: "center", color: "", styles: "", value: calendarCard_line1 }, { width: "100%", height: "100%", lineHeight: "", alignX: "center", color: "", value: calendarCard_line2 }, { width: "100%", height: "auto", lineHeight: "", alignX: "center", color: "", value: calendarCard_line3 }] }) ``` ***Tipp: **Durch die Auslagerung der einzelnen Elemente, könnt ihr super schnell weitere Designs erstellen und andere Ansichten erschaffen. Zum Beispiel die Zeilen tauschen:* ![](https://framerusercontent.com/images/lc1yOCv4jLEwkW4vMezjGhX3k.png) Code: ```javascript let current := this; arcCustomLayout({ uniqueId: "calenderCard version 2" + nr, embedded: true, direction: "vertical", alignX: "center", alignY: "top", width: "100%", height: "100%", gap: "10px", backgroundColor: record(Typ_Termin,number(current.Typ)).color, paddingY: "8px", paddingX: "8px", styles: "border-radius: 14px;", scrollSettings: { scrollY: true, scrollX: false }, blocks: [{ width: "100%", height: "auto", lineHeight: "", alignX: "center", color: "", styles: "", value: calendarCard_line2 }, { width: "100%", height: "100%", lineHeight: "", alignX: "center", color: "", value: calendarCard_line1 }, { width: "100%", height: "auto", lineHeight: "", alignX: "center", color: "", value: calendarCard_line3 }] }) ``` ## Finish Design Die fertige Card kann **Abschließend **im Widget customCalendarWeek eingesetzt werden. Das Layout unserer calendarCard wird bei den timeEntries im Parameter **customLayout** aufgerufen. ```javascript ... timeEntries: (select Termine where forCalendarWeek = true).[{ subtitle: "", title: "", value: "", customLayout: calendarCard, ... ``` ***Wichtig!*** Wenn ihr die Hintergrundfarbe und einen borderRadius im Layout der Card definiert, müsst ihr dran denken, die backgroundColor bei den styles Parameter der timeEntries auf "transparent" sowie padding auf "0px" zu stellen. ```javascript ... styles: { backgroundColor: "transparent", padding: "0px" }, ... ``` ### Vollständiger Anwendungscode der customCalendarWeek: ```javascript let current := this; arcCustomCalendarWeek({ uniqueId: "customCalendar" + Nr, embedded: false, height: "800px", weekdaySettings: { week: week(today()), year: year(today()) }, weekStart: 1, timeZoneBalance: -1, timeSettings: { timeStart: time(6, 30), duration: time(15, 30) }, currentTime: "#f95757", dayHighlights: [{ date: date(2024, 10, 31), color: "" }, { weekday: 6, color: "#f9f9f9" }, { weekday: 7, color: "#f9f9f9" }], lang: clientLang(), timeSlotWidth: "372px", timeSlotHeight: "120px", styles: { backgroundColor: "#cccccc11" }, timeEntries: (select Termine where forCalendarWeek = true).[{ subtitle: "", title: "", value: "", customLayout: calendarCard, dateFrom: 'Datum von', dateTo: 'Datum bis', timeFrom: Von, timeTo: Bis, styles: { backgroundColor: "transparent", padding: "0px" }, dragAction: { recordId: nr, dateFrom: fieldId(nr, "Datum von"), timeFrom: fieldId(nr, "Von"), dateTo: fieldId(nr, "Datum bis"), timeTo: fieldId(nr, "Bis") } }] }) ``` ** ** --- # Kanban speichern und Fehler behandeln Source: https://www.arc-rider.com/docs/examples/react-kanban-persistenz # Kanban speichern und Fehler behandeln Dieses Beispiel verschiebt Karten zwischen zwei flachen Spalten und speichert den bestätigten Stand im Browser. Die Checkbox simuliert einen fehlgeschlagenen Speichervorgang. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetKanban } from '@arcrider/arcwidgets-react'; const initial = [ { id: 'open', title: 'Offen', items: [ { id: 'a', title: 'Angebot prüfen', clickable: true, draggable: true, }, ], }, { id: 'done', title: 'Erledigt', items: [] as initialItem[], }, ]; type initialItem = { id: string; title: string; clickable: boolean; draggable: boolean; }; export default function Example() { const [lanes, setLanes] = useState(initial); const [fail, setFail] = useState(false); const [status, setStatus] = useState('Karte verschieben'); return ( <> { const next = lanes.map((lane) => ({ ...lane, items: [...lane.items], })); const [item] = next[p.source_swimlane_index].items.splice( p.item_index_in_section, 1, ); next[p.target_swimlane_index].items.splice( p.target_index_in_section, 0, item, ); setStatus('Speichert …'); try { if (fail) throw new Error('Simulierter Speicherfehler'); localStorage.setItem('arc-demo-board', JSON.stringify(next)); setLanes(next); setStatus('Lokal gespeichert'); } catch (error) { setStatus('Speichern fehlgeschlagen; Karte zurückgesetzt.'); throw error; } }} />

    {status}

    ); } ``` ## Datenfluss Der Callback kopiert die aktuelle Liste, entfernt die Karte aus der Quellspalte und fügt sie am Ziel ein. Erst nach erfolgreichem Speichern wird React-State aktualisiert. Ein abgelehntes Promise lässt den Adapter die optimistische Verschiebung abbrechen. Das Beispiel verwendet flache Spalten. Bei gruppierten Boards beziehst du zusätzlich `source_section_index` und `target_section_index` ein. Ersetze `localStorage` durch deine API; diese sollte stabile Karten-IDs statt UI-Indizes speichern. [Kanban-Referenz](/docs/react/widgets/custom-kanban) · [State und Events](/docs/react/patterns/react-state-events). --- # Sortierung innerhalb einer Table Source: https://www.arc-rider.com/docs/examples/sortierung-innerhalb-einer-table # Text, Zahlen und Datum sortieren in React Drei Spalten mit unterschiedlichen Sortiertypen konfigurieren. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { ArcWidgetTable } from '@arcrider/arcwidgets-react'; const entries = [ { id: '1', name: 'Beta', amount: 9, day: '2026-09-20', }, { id: '2', name: 'Alpha', amount: 120, day: '2026-09-17', }, ]; export default function Example() { return ( ({ id: e.id, columns: [ { value: e.name, width: 'fraction', }, { value: `${e.amount} €`, width: '140px', sort: { value: e.amount, }, }, { value: new Date(e.day).toLocaleDateString('de-DE'), width: '140px', sort: { value: new Date(e.day).getTime(), }, }, ], })), }} /> ); } ``` ## So funktioniert es `sort.type` beschreibt die Vergleichsart. Datum und Zahlen bekommen rohe Werte in `sort.value`; die sichtbare Beschriftung bleibt frei formatiert. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Sortierung innerhalb einer Table # Sortierung innerhalb einer Table In diesem Gastbeitrag von Fox Concepts zeigt dir Christoph, wie du ganz einfach eine Sortierung in der Custom Table integrierst. Bestimme selbst, in welchen Spalten nach was sortiert werden kann. ## Anwendungscode ```javascript let dash := this; let baseId := "SortableTable_"; let rowHeight := "40px"; let itemHeight := "30px"; let sortBy := Sortierung; let sortDesc := Absteigend; function sortableColumnHeader(columnTitle : text,fieldName : text) do "Adding a button to the header cell, that writes the actual sort request to helper fields"; arcCustomLayout({ uniqueId: baseId + "ColumnHeader_" + columnTitle, embedded: true, fullscreen: false, direction: "horizontal", gap: "1px", blocks: [{ height: "auto", alignX: "left", value: columnTitle }, { width: "20px", height: "auto", alignX: "right", value: arcCustomButton({ uniqueId: baseId + "HeaderButton_" + columnTitle, embedded: true, icon: GIPMaterialSymbolsV2({ weight: 400, filling: 1, icon: "swap_vert", color: "#3381ff" }), iconPosition: "center", gap: "1px", showBadge: false, actions: [{ recordId: dash.Nr, type: "update", field: "A", value: fieldName }, { recordId: dash.Nr, type: "update", field: "B", value: sortBy = fieldName and not sortDesc }] }) }] }) end; function getSortedList(fieldName : text) do "Get the sorted list according sort settings"; let list := if fieldName != null then (select Kontakte) order by get(this, fieldName) else select Kontakte end; if sortDesc then for i in range(cnt(list), 0, -1) do item(list, i - 1) end else list end end; let list := getSortedList(dash.Sortierung); let data := { uniqueListId: baseId, tableId: tableId(first(list)), height: "500px", header: { showHeader: true, height: rowHeight }, maxHeight: "800px", borderColor: "black", minHeight: "500px", groupedBy: "", popupButtonTitle: "Open", scrollBar: { showScrollBar: true, width: "5px", height: "5px", backgroundColor: "", handle: { height: "5px", borderRadius: "", backgroundColor: "" } }, table: list.[{ recordId: Nr, rowHeight: "40px", columns: [{ field: "Vorname", title: sortableColumnHeader("Vorname", "Vorname"), value: Vorname, width: "220px", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "Nachname", title: sortableColumnHeader("Nachname", "Nachname"), value: Nachname, width: "220px", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "Straße", title: sortableColumnHeader("Straße", "Straße"), value: 'Straße', width: "220px", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "PLZ", title: sortableColumnHeader("PLZ", "PLZ"), value: PLZ, width: "220px", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "Ort", title: sortableColumnHeader("Ort", "Ort"), value: Ort, width: "220px", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "Telefon", title: "Telefon", value: Telefon, width: "220px", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }, { field: "E-Mail", title: "E-Mail", value: 'E-Mail', width: "", actions: [{ type: "popup", showPopupButton: false, recordId: Nr }] }] }] }; arcCustomTable(data) ``` --- # Status als Buttons ausgeben Source: https://www.arc-rider.com/docs/examples/status-als-buttons-ausgeben # Status auswählen und speichern in React Drei Statusaktionen mit Ladeanzeige und Rückmeldung verbinden. ## Vorbereitung Installiere das [React-Paket](/docs/react/intro/setup-react). Das Beispiel ist eine vollständige Komponente für eine Browser-App und verwendet eigene Beispieldaten. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetButton, ArcWidgetBadge } from '@arcrider/arcwidgets-react'; export default function Example() { const [status, setStatus] = useState('Offen'); const [error, setError] = useState(''); async function save(next: string) { try { localStorage.setItem('arc-demo-status', next); setStatus(next); setError(''); } catch { setError( 'Status konnte nicht gespeichert werden. Bitte erneut versuchen.', ); } } return ( <>
    {['Offen', 'In Arbeit', 'Erledigt'].map((label) => ( save(label)} /> ))}

    {error || 'Änderungen werden lokal gespeichert.'}

    ); } ``` ## So funktioniert es Das Beispiel speichert lokal. Ersetze `localStorage.setItem` durch deinen API-Aufruf und gib das gesamte Promise aus dem Callback zurück. Ändere den bestätigten Status erst nach erfolgreichem Speichern. ## In deiner Anwendung Halte fachliche Daten in deinem State oder Store. Ersetze lokale Beispieldaten und gegebenenfalls `localStorage` durch deinen Datenzugriff. Behandle Laden, leere Daten und Speicherfehler sichtbar in der Oberfläche. [State und Events](/docs/react/patterns/react-state-events) · [Komponentenübersicht](/docs/react/intro/react-components). # Status als Buttons ausgeben # Status als Buttons ausgeben ![](https://framerusercontent.com/images/Q7g9aCNTJpdFui2qnCsHayINscg.png) Möchtest du einen Status, beispielsweise den Aufgabenstatus, auf einer Oberfläche wie einer[ Custom Table](/documentation/custom-table) oder direkt auf deinem Datensatz anzeigen? Es gibt verschiedene Möglichkeiten, dies zu erreichen. Du kannst entweder ein [Select-Feld ](/documentation/select)verwenden, um den Status auswählbar zu machen, oder eine effizientere Methode nutzen: [Buttons](/documentation/buttons), die direkt angeklickt werden können. Diese Buttons lassen sich in einem [übersichtlichen Layout](/documentation/custom-layout) anordnen. Hier ist die Vorgehensweise: - **Status in einer separaten Hilfstabelle anlegen**

    Erstelle zunächst eine Hilfstabelle, in der die verschiedenen Statuswerte in einem Textfeld definiert werden. - **Status auf der Zieltabelle als dynamisches Feld einblenden (Ninox)**

    Blende die Statuswerte in der Zieltabelle als dynamisches Feld ein, sodass sie flexibel genutzt werden können. - **Den Anwendungscode auf deiner Oberfläche einfügen**

    Füge den erforderlichen Anwendungscode in deine Oberfläche ein. Das kann beispielsweise in einer Custom Table in einer Spalte sein oder aber blank auf deinem Ninox Record. - **Anwendungscode auf deinen Use Case anpassen: Deine eigene Statusliste auswählen**

    Wähle die entsprechende Hilfstabelle, die du bei Schritt 1 angelegt hast, bei Blocks unter „select ….“ aus, um sicherzustellen, dass die richtigen Statuswerte verwendet werden. - **Anwendungscode auf deinen Use Case anpassen: Field bei Actions mit der fieldID deines Statusfeldes austauschen**

    Tausche in den Actions das Feld mit der fieldID deines Statusfeldes, dass du bei Schritt 2 angelegt hast, aus, damit die Änderungen korrekt übernommen werden. Mit diesen Schritten kannst du eine intuitive und benutzerfreundliche Oberfläche gestalten, auf der der Status schnell und einfach angepasst werden kann. ## Anwendungscode ```javascript arcCustomLayout({ uniqueId: "statusTermin " + Nr, direction: "horizontal", alignX: "center", alignY: "center", width: "100%", height: "100%", gap: "10px", backgroundColor: "", paddingY: "10px", paddingX: "10px", blocks: let id := Nr; (select 'Status Termin').[{ align: "center", styles: "", width: "fraction", clickAction: "", value: arcCustomButton({ uniqueId: "Termin_Status_" + id + Nr, title: Bezeichung, width: "100%", icon: "", fontSize: "", fontColor: if id.Status = number(Nr) then "#fff" else "#9ba3af" end, iconColor: "", backgroundColor: if id.Status = number(Nr) then Farbe else "#9ba3af33" end, borderColor: if id.Status = number(Nr) then Farbe else "#9ba3af33" end, borderRadius: "5px", actions: [{ recordId: id, type: "update", field: "Q1", value: if id.Status = number(Nr) then null else number(Nr) end }] }) }] }) ``` --- # Vertriebs-Cockpit Source: https://www.arc-rider.com/docs/examples/react-sales-app # Vertriebs-Cockpit Opportunities vom Erstgespräch bis zum Abschluss begleiten. Diese vollständige Demo verbindet mehrere echte Widgets mit einem gemeinsamen Datenbestand. ## Ansichten und Workflows **Übersicht · Pipeline · Opportunities · Aktivitäten** - Suche und Team-Filter gelten für alle Ansichten. Ein leerer Filter zeigt den echten Leerzustand der Tabelle. - Klicke eine Zeile, Karte oder einen Kalendertermin an und bearbeite den Eintrag im Detailbereich. - Navigiere in den Kalenderansichten wochenweise vor und zurück. - Ändere den Status, den Fortschritt, das Team oder den Termin. Danach bleibt die Änderung beim Wechsel der Ansicht sichtbar. - Ziehe eine Karte zwischen den Spalten. Status, Tabellenzeile, Detailbereich und Kennzahlen verwenden danach den gleichen aktualisierten Eintrag. ## Live-App und vollständiger Code Über **Groß öffnen** steht die App mit voller Breite zur Verfügung. **Code** zeigt exakt die ausgeführte Komponente. Du kannst sie nach der [Installation](/docs/react/intro/setup-react) direkt in eine Browser-App übernehmen. ```tsx import { useMemo, useState } from 'react'; import { ArcWidgetBadge, ArcWidgetButton, ArcWidgetCalendarTimeline, ArcWidgetCalendarWeek, ArcWidgetInput, ArcWidgetKanban, ArcWidgetKpiBar, ArcWidgetProgressBar, ArcWidgetSelect, ArcWidgetSlider, ArcWidgetTable, } from '@arcrider/arcwidgets-react'; // Vollständige Browser-Demo. Alle Ansichten verwenden denselben Datenbestand. const config = { kind: 'sales', title: 'Vertriebs-Cockpit', description: 'Opportunities vom Erstgespräch bis zum Abschluss begleiten.', recordLabel: 'Opportunity', countLabel: 'Opportunities', searchLabel: 'Opportunities oder Kunden suchen …', stages: ['Qualifiziert', 'Angebot', 'Gewonnen'], views: [ { id: 'overview', title: 'Übersicht', }, { id: 'board', title: 'Pipeline', }, { id: 'table', title: 'Opportunities', }, { id: 'calendar', title: 'Aktivitäten', }, ], }; type Item = { id: string; title: string; account: string; owner: string; stage: number; value: number; progress: number; day: number; hour: number; }; const initial: Item[] = [ { id: 'V-301', title: 'Service-Portal', account: 'Kunde Nord', owner: 'Team Wachstum', stage: 1, value: 18500, progress: 65, day: 0, hour: 9, }, { id: 'V-302', title: 'Digitale Auftragserfassung', account: 'Kunde West', owner: 'Team Beratung', stage: 0, value: 12400, progress: 25, day: 1, hour: 10, }, { id: 'V-303', title: 'Ressourcen- und Einsatzplan', account: 'Kunde Süd', owner: 'Team Wachstum', stage: 2, value: 24800, progress: 100, day: 0, hour: 14, }, { id: 'V-304', title: 'Inventarverwaltung', account: 'Kunde Ost', owner: 'Team Beratung', stage: 1, value: 9600, progress: 70, day: 2, hour: 11, }, { id: 'V-305', title: 'Projekt-Dashboard', account: 'Kunde Mitte', owner: 'Team Partnerschaften', stage: 0, value: 14800, progress: 35, day: 3, hour: 10, }, { id: 'V-306', title: 'Kunden-Self-Service', account: 'Kunde Hafen', owner: 'Team Partnerschaften', stage: 1, value: 32000, progress: 80, day: 4, hour: 13, }, { id: 'V-307', title: 'Digitale Abnahme', account: 'Kunde Park', owner: 'Team Beratung', stage: 0, value: 8400, progress: 15, day: 2, hour: 15, }, { id: 'V-308', title: 'Vertriebsübersicht', account: 'Kunde Campus', owner: 'Team Wachstum', stage: 2, value: 11600, progress: 100, day: 1, hour: 14, }, ]; const dayDate = (offset: number) => new Date(2026, 8, 14 + offset).getTime(); const money = (value: number) => value.toLocaleString('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0, }); const tones = ['#a16207', '#52525b', '#15803d']; const pill = (stage: number) => ( ); export default function Example() { const [items, setItems] = useState(initial); const [view, setView] = useState(config.views[0].id); const [period, setPeriod] = useState(0); const [query, setQuery] = useState(''); const [owner, setOwner] = useState(null); const [stage, setStage] = useState(null); const [selectedId, setSelectedId] = useState(initial[0].id); const [message, setMessage] = useState( 'Wähle einen Eintrag aus, um ihn zu bearbeiten.', ); const owners = [...new Set(items.map((item) => item.owner))]; const filtered = useMemo( () => items.filter( (item) => (!query || `${item.title} ${item.account} ${item.owner}` .toLowerCase() .includes(query.toLowerCase())) && (owner == null || item.owner === owner) && (stage == null || item.stage === stage), ), [items, query, owner, stage], ); const selected = filtered.find((item) => item.id === selectedId) ?? filtered[0]; const completed = filtered.filter((item) => item.stage === 2).length; const total = filtered.reduce((sum, item) => sum + item.value, 0); const progress = filtered.length ? Math.round( filtered.reduce((sum, item) => sum + item.progress, 0) / filtered.length, ) : 0; const update = (patch: Partial) => { if (!selected) return; setItems((previous) => previous.map((item) => item.id === selected.id ? { ...item, ...patch } : item, ), ); setMessage( `${selected.title} aktualisiert. Die Änderung ist in allen Ansichten sichtbar.`, ); }; const lanes = config.stages.map((title, index) => ({ id: `stage-${index}`, title, minWidth: '210px', items: filtered .filter((item) => item.stage === index) .map((item) => ({ id: item.id, title: item.title, subtitle: item.account, value: ( ), clickable: true, draggable: true, })), })); const resourceRows = owners.map((team) => ({ id: team, columns: [ { title: team, subtitle: `${filtered.filter((item) => item.owner === team).length} Einsätze`, clickable: true, }, ], timeEntries: filtered .filter((item) => item.owner === team) .map((item) => ({ id: item.id, title: item.title, dateFrom: dayDate(item.day), dateTo: dayDate(item.day + 1), clickable: true, draggable: false, customLayout: (

    {item.title}
    {item.account}
    ), })), })); const timeEntries = filtered.map((item) => ({ id: item.id, title: item.title, dateFrom: dayDate(item.day), dateTo: dayDate(item.day), timeFrom: item.hour * 3600000, timeTo: (item.hour + 1) * 3600000, clickable: true, draggable: false, customLayout: (
    {item.title}
    {item.owner}
    ), })); const table = ( ({ id: item.id, clickable: true, rowColor: selected?.id === item.id ? '#f4f4f5' : '#ffffff', columns: [ { value: (
    {item.title}
    {item.account}
    ), width: 'fraction', sort: { value: item.title }, }, { value: item.owner, width: '105px' }, { value: pill(item.stage), width: '125px' }, { value: config.kind === 'sales' ? ( money(item.value) ) : ( ), width: '110px', sort: { value: config.kind === 'sales' ? item.value : item.progress, }, }, ], })), emptyTable: { title: 'Keine passenden Einträge', value: (
    {'Keine passenden Einträge'}

    {'Setze die Suche oder den Team-Filter zurück.'}

    ), }, }} onCellClick={(_event, payload) => { if (payload.row_index != null && filtered[payload.row_index]) setSelectedId(filtered[payload.row_index].id); }} /> ); return (
    Arc Rider · Example App

    {config.title}

    Interaktive Demo

    {config.description}

    setQuery(String(value ?? ''))} />
    ({ value, title: value, active: owner === value, })), reset: { title: 'Alle Teams' }, }} onChange={setOwner} />
    {view === 'overview' && (
    setStage(index === 2 ? 2 : null)} />
    )}

    {config.views.find((tab) => tab.id === view)?.title}

    {filtered.length} Einträge
    {(view === 'calendar' || view === 'timeline') && (
    {new Date(dayDate(period * 7)).toLocaleDateString('de-DE')} –{' '} {new Date( dayDate(period * 7 + (view === 'timeline' ? 13 : 6)), ).toLocaleDateString('de-DE')}
    )} {(view === 'overview' || view === 'table') && (
    {table}
    )} {view === 'board' && (
    { const item = lanes[p.swimlane_index]?.items[p.item_index_in_section]; if (item) setSelectedId(String(item.id)); }} onItemDrop={async (p) => { const source = lanes[p.source_swimlane_index]?.items[ p.item_index_in_section ]; if (!source) return; const target = lanes[ p.target_swimlane_index ].items.filter((item) => item.id !== source.id); const before = target[p.target_index_in_section]?.id; setItems((previous) => { const item = previous.find( (item) => item.id === source.id, ); if (!item) return previous; const next = previous.filter( (item) => item.id !== source.id, ); const position = before ? next.findIndex((item) => item.id === before) : next.length; next.splice(position < 0 ? next.length : position, 0, { ...item, stage: p.target_swimlane_index, progress: p.target_swimlane_index === 2 ? 100 : item.stage === 2 ? 0 : item.progress, }); return next; }); setSelectedId(String(source.id)); setMessage( `${source.title} nach ${config.stages[p.target_swimlane_index]} verschoben.`, ); }} />
    )} {view === 'timeline' && (
    { const item = resourceRows[row]?.timeEntries[index]; if (item) setSelectedId(String(item.id)); }} onLabelClick={(row) => { setOwner(owners[row]); setMessage(`${owners[row]} ausgewählt.`); }} />
    )} {view === 'calendar' && (
    { if (filtered[index]) setSelectedId(filtered[index].id); }} />
    )}
    {message} · Änderungen bleiben in dieser Demo.
    ); } ``` ## Zusammenspiel der Widgets Die App hält die Einträge in einem React-State. `useMemo` leitet die gefilterte Liste ab; Tabelle, Board und Kalender erhalten daraus ihre jeweilige Konfiguration. Callbacks wählen stabile IDs aus oder verändern den gemeinsamen State. Die Widgets übernehmen Darstellung und Interaktion, die Anwendung besitzt die fachlichen Daten. ## Grenzen der Demo Die Daten sind fiktiv und bleiben im Browser-Speicher der laufenden Komponente. Es gibt keinen Server, keine Anmeldung und keine echte Speicherung. Kalendertermine werden über den Detailbereich verändert; die aktuellen Drag-Callbacks liefern noch keine vollständigen Zielzeiten. Nach einem Neuladen beginnt die App wieder mit den Ausgangsdaten. --- # Projekt-Cockpit Source: https://www.arc-rider.com/docs/examples/react-projects-app # Projekt-Cockpit Aufgaben, Fortschritt und Termine an einem Ort. Diese vollständige Demo verbindet mehrere echte Widgets mit einem gemeinsamen Datenbestand. ## Ansichten und Workflows **Übersicht · Kanban · Aufgaben · Kalender** - Suche und Team-Filter gelten für alle Ansichten. Ein leerer Filter zeigt den echten Leerzustand der Tabelle. - Klicke eine Zeile, Karte oder einen Kalendertermin an und bearbeite den Eintrag im Detailbereich. - Navigiere in den Kalenderansichten wochenweise vor und zurück. - Ändere den Status, den Fortschritt, das Team oder den Termin. Danach bleibt die Änderung beim Wechsel der Ansicht sichtbar. - Ziehe eine Karte zwischen den Spalten. Status, Tabellenzeile, Detailbereich und Kennzahlen verwenden danach den gleichen aktualisierten Eintrag. ## Live-App und vollständiger Code Über **Groß öffnen** steht die App mit voller Breite zur Verfügung. **Code** zeigt exakt die ausgeführte Komponente. Du kannst sie nach der [Installation](/docs/react/intro/setup-react) direkt in eine Browser-App übernehmen. ```tsx import { useMemo, useState } from 'react'; import { ArcWidgetBadge, ArcWidgetButton, ArcWidgetCalendarTimeline, ArcWidgetCalendarWeek, ArcWidgetInput, ArcWidgetKanban, ArcWidgetKpiBar, ArcWidgetProgressBar, ArcWidgetSelect, ArcWidgetSlider, ArcWidgetTable, } from '@arcrider/arcwidgets-react'; // Vollständige Browser-Demo. Alle Ansichten verwenden denselben Datenbestand. const config = { kind: 'projects', title: 'Projekt-Cockpit', description: 'Aufgaben, Fortschritt und Termine an einem Ort.', recordLabel: 'Aufgabe', countLabel: 'Aufgaben', searchLabel: 'Aufgaben oder Projekte suchen …', stages: ['Geplant', 'In Arbeit', 'Erledigt'], views: [ { id: 'overview', title: 'Übersicht', }, { id: 'board', title: 'Kanban', }, { id: 'table', title: 'Aufgaben', }, { id: 'calendar', title: 'Kalender', }, ], }; type Item = { id: string; title: string; account: string; owner: string; stage: number; value: number; progress: number; day: number; hour: number; }; const initial: Item[] = [ { id: 'P-101', title: 'Anforderungen abstimmen', account: 'Kundenportal', owner: 'Konzeption', stage: 1, value: 3200, progress: 60, day: 0, hour: 9, }, { id: 'P-102', title: 'Dashboard gestalten', account: 'Kundenportal', owner: 'Design', stage: 1, value: 4800, progress: 35, day: 1, hour: 10, }, { id: 'P-103', title: 'Datenmodell aufsetzen', account: 'Kundenportal', owner: 'Entwicklung', stage: 0, value: 6000, progress: 10, day: 2, hour: 11, }, { id: 'P-104', title: 'Angebote überarbeiten', account: 'Auftragsmanagement', owner: 'Konzeption', stage: 2, value: 1800, progress: 100, day: 0, hour: 14, }, { id: 'P-105', title: 'Kalender integrieren', account: 'Service-App', owner: 'Entwicklung', stage: 1, value: 3600, progress: 70, day: 3, hour: 10, }, { id: 'P-106', title: 'Nutzertest vorbereiten', account: 'Service-App', owner: 'Design', stage: 0, value: 1600, progress: 0, day: 4, hour: 14, }, { id: 'P-107', title: 'Freigabe einholen', account: 'Auftragsmanagement', owner: 'Konzeption', stage: 0, value: 900, progress: 20, day: 2, hour: 15, }, { id: 'P-108', title: 'Dokumentation erstellen', account: 'Kundenportal', owner: 'Entwicklung', stage: 2, value: 1400, progress: 100, day: 1, hour: 14, }, ]; const dayDate = (offset: number) => new Date(2026, 8, 14 + offset).getTime(); const money = (value: number) => value.toLocaleString('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0, }); const tones = ['#a16207', '#52525b', '#15803d']; const pill = (stage: number) => ( ); export default function Example() { const [items, setItems] = useState(initial); const [view, setView] = useState(config.views[0].id); const [period, setPeriod] = useState(0); const [query, setQuery] = useState(''); const [owner, setOwner] = useState(null); const [stage, setStage] = useState(null); const [selectedId, setSelectedId] = useState(initial[0].id); const [message, setMessage] = useState( 'Wähle einen Eintrag aus, um ihn zu bearbeiten.', ); const owners = [...new Set(items.map((item) => item.owner))]; const filtered = useMemo( () => items.filter( (item) => (!query || `${item.title} ${item.account} ${item.owner}` .toLowerCase() .includes(query.toLowerCase())) && (owner == null || item.owner === owner) && (stage == null || item.stage === stage), ), [items, query, owner, stage], ); const selected = filtered.find((item) => item.id === selectedId) ?? filtered[0]; const completed = filtered.filter((item) => item.stage === 2).length; const total = filtered.reduce((sum, item) => sum + item.value, 0); const progress = filtered.length ? Math.round( filtered.reduce((sum, item) => sum + item.progress, 0) / filtered.length, ) : 0; const update = (patch: Partial) => { if (!selected) return; setItems((previous) => previous.map((item) => item.id === selected.id ? { ...item, ...patch } : item, ), ); setMessage( `${selected.title} aktualisiert. Die Änderung ist in allen Ansichten sichtbar.`, ); }; const lanes = config.stages.map((title, index) => ({ id: `stage-${index}`, title, minWidth: '210px', items: filtered .filter((item) => item.stage === index) .map((item) => ({ id: item.id, title: item.title, subtitle: item.account, value: ( ), clickable: true, draggable: true, })), })); const resourceRows = owners.map((team) => ({ id: team, columns: [ { title: team, subtitle: `${filtered.filter((item) => item.owner === team).length} Einsätze`, clickable: true, }, ], timeEntries: filtered .filter((item) => item.owner === team) .map((item) => ({ id: item.id, title: item.title, dateFrom: dayDate(item.day), dateTo: dayDate(item.day + 1), clickable: true, draggable: false, customLayout: (
    {item.title}
    {item.account}
    ), })), })); const timeEntries = filtered.map((item) => ({ id: item.id, title: item.title, dateFrom: dayDate(item.day), dateTo: dayDate(item.day), timeFrom: item.hour * 3600000, timeTo: (item.hour + 1) * 3600000, clickable: true, draggable: false, customLayout: (
    {item.title}
    {item.owner}
    ), })); const table = ( ({ id: item.id, clickable: true, rowColor: selected?.id === item.id ? '#f4f4f5' : '#ffffff', columns: [ { value: (
    {item.title}
    {item.account}
    ), width: 'fraction', sort: { value: item.title }, }, { value: item.owner, width: '105px' }, { value: pill(item.stage), width: '125px' }, { value: config.kind === 'sales' ? ( money(item.value) ) : ( ), width: '110px', sort: { value: config.kind === 'sales' ? item.value : item.progress, }, }, ], })), emptyTable: { title: 'Keine passenden Einträge', value: (
    {'Keine passenden Einträge'}

    {'Setze die Suche oder den Team-Filter zurück.'}

    ), }, }} onCellClick={(_event, payload) => { if (payload.row_index != null && filtered[payload.row_index]) setSelectedId(filtered[payload.row_index].id); }} /> ); return (
    Arc Rider · Example App

    {config.title}

    Interaktive Demo

    {config.description}

    setQuery(String(value ?? ''))} />
    ({ value, title: value, active: owner === value, })), reset: { title: 'Alle Teams' }, }} onChange={setOwner} />
    {view === 'overview' && (
    setStage(index === 2 ? 2 : null)} />
    )}

    {config.views.find((tab) => tab.id === view)?.title}

    {filtered.length} Einträge
    {(view === 'calendar' || view === 'timeline') && (
    {new Date(dayDate(period * 7)).toLocaleDateString('de-DE')} –{' '} {new Date( dayDate(period * 7 + (view === 'timeline' ? 13 : 6)), ).toLocaleDateString('de-DE')}
    )} {(view === 'overview' || view === 'table') && (
    {table}
    )} {view === 'board' && (
    { const item = lanes[p.swimlane_index]?.items[p.item_index_in_section]; if (item) setSelectedId(String(item.id)); }} onItemDrop={async (p) => { const source = lanes[p.source_swimlane_index]?.items[ p.item_index_in_section ]; if (!source) return; const target = lanes[ p.target_swimlane_index ].items.filter((item) => item.id !== source.id); const before = target[p.target_index_in_section]?.id; setItems((previous) => { const item = previous.find( (item) => item.id === source.id, ); if (!item) return previous; const next = previous.filter( (item) => item.id !== source.id, ); const position = before ? next.findIndex((item) => item.id === before) : next.length; next.splice(position < 0 ? next.length : position, 0, { ...item, stage: p.target_swimlane_index, progress: p.target_swimlane_index === 2 ? 100 : item.stage === 2 ? 0 : item.progress, }); return next; }); setSelectedId(String(source.id)); setMessage( `${source.title} nach ${config.stages[p.target_swimlane_index]} verschoben.`, ); }} />
    )} {view === 'timeline' && (
    { const item = resourceRows[row]?.timeEntries[index]; if (item) setSelectedId(String(item.id)); }} onLabelClick={(row) => { setOwner(owners[row]); setMessage(`${owners[row]} ausgewählt.`); }} />
    )} {view === 'calendar' && (
    { if (filtered[index]) setSelectedId(filtered[index].id); }} />
    )}
    {message} · Änderungen bleiben in dieser Demo.
    ); } ``` ## Zusammenspiel der Widgets Die App hält die Einträge in einem React-State. `useMemo` leitet die gefilterte Liste ab; Tabelle, Board und Kalender erhalten daraus ihre jeweilige Konfiguration. Callbacks wählen stabile IDs aus oder verändern den gemeinsamen State. Die Widgets übernehmen Darstellung und Interaktion, die Anwendung besitzt die fachlichen Daten. ## Grenzen der Demo Die Daten sind fiktiv und bleiben im Browser-Speicher der laufenden Komponente. Es gibt keinen Server, keine Anmeldung und keine echte Speicherung. Kalendertermine werden über den Detailbereich verändert; die aktuellen Drag-Callbacks liefern noch keine vollständigen Zielzeiten. Nach einem Neuladen beginnt die App wieder mit den Ausgangsdaten. --- # Ressourcenplanung Source: https://www.arc-rider.com/docs/examples/react-resources-app # Ressourcenplanung Teams, Einsätze und Verfügbarkeit gemeinsam planen. Diese vollständige Demo verbindet mehrere echte Widgets mit einem gemeinsamen Datenbestand. ## Ansichten und Workflows **Übersicht · Ressourcen · Wochenplan · Einsatzliste** - Suche und Team-Filter gelten für alle Ansichten. Ein leerer Filter zeigt den echten Leerzustand der Tabelle. - Klicke eine Zeile, Karte oder einen Kalendertermin an und bearbeite den Eintrag im Detailbereich. - Navigiere in den Kalenderansichten wochenweise vor und zurück. - Ändere den Status, den Fortschritt, das Team oder den Termin. Danach bleibt die Änderung beim Wechsel der Ansicht sichtbar. - Wähle in der Ressourcenansicht ein Team oder einen Balken aus. Weise den Einsatz im Detailbereich einem anderen Team zu und prüfe seine neue Zeile. ## Live-App und vollständiger Code Über **Groß öffnen** steht die App mit voller Breite zur Verfügung. **Code** zeigt exakt die ausgeführte Komponente. Du kannst sie nach der [Installation](/docs/react/intro/setup-react) direkt in eine Browser-App übernehmen. ```tsx import { useMemo, useState } from 'react'; import { ArcWidgetBadge, ArcWidgetButton, ArcWidgetCalendarTimeline, ArcWidgetCalendarWeek, ArcWidgetInput, ArcWidgetKanban, ArcWidgetKpiBar, ArcWidgetProgressBar, ArcWidgetSelect, ArcWidgetSlider, ArcWidgetTable, } from '@arcrider/arcwidgets-react'; // Vollständige Browser-Demo. Alle Ansichten verwenden denselben Datenbestand. const config = { kind: 'resources', title: 'Ressourcenplanung', description: 'Teams, Einsätze und Verfügbarkeit gemeinsam planen.', recordLabel: 'Einsatz', countLabel: 'Einsätze', searchLabel: 'Einsätze oder Kunden suchen …', stages: ['Geplant', 'Unterwegs', 'Abgeschlossen'], views: [ { id: 'overview', title: 'Übersicht', }, { id: 'timeline', title: 'Ressourcen', }, { id: 'calendar', title: 'Wochenplan', }, { id: 'table', title: 'Einsatzliste', }, ], }; type Item = { id: string; title: string; account: string; owner: string; stage: number; value: number; progress: number; day: number; hour: number; }; const initial: Item[] = [ { id: 'E-201', title: 'Installation Standort Nord', account: 'Kunde Nord', owner: 'Team A', stage: 1, value: 2400, progress: 60, day: 0, hour: 9, }, { id: 'E-202', title: 'Wartung Gebäudetechnik', account: 'Kunde West', owner: 'Team B', stage: 0, value: 1600, progress: 0, day: 1, hour: 10, }, { id: 'E-203', title: 'Abnahme Standort Süd', account: 'Kunde Süd', owner: 'Team C', stage: 2, value: 900, progress: 100, day: 0, hour: 14, }, { id: 'E-204', title: 'Anlage in Betrieb nehmen', account: 'Kunde Ost', owner: 'Team A', stage: 0, value: 3200, progress: 15, day: 3, hour: 9, }, { id: 'E-205', title: 'Sensoren kalibrieren', account: 'Kunde Nord', owner: 'Team B', stage: 1, value: 1200, progress: 50, day: 2, hour: 13, }, { id: 'E-206', title: 'Service und Prüfung', account: 'Kunde West', owner: 'Team C', stage: 0, value: 1800, progress: 0, day: 4, hour: 10, }, { id: 'E-207', title: 'Einweisung durchführen', account: 'Kunde Süd', owner: 'Team A', stage: 0, value: 600, progress: 10, day: 1, hour: 14, }, { id: 'E-208', title: 'Dokumentation übergeben', account: 'Kunde Ost', owner: 'Team C', stage: 2, value: 400, progress: 100, day: 2, hour: 9, }, ]; const dayDate = (offset: number) => new Date(2026, 8, 14 + offset).getTime(); const money = (value: number) => value.toLocaleString('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0, }); const tones = ['#a16207', '#52525b', '#15803d']; const pill = (stage: number) => ( ); export default function Example() { const [items, setItems] = useState(initial); const [view, setView] = useState(config.views[0].id); const [period, setPeriod] = useState(0); const [query, setQuery] = useState(''); const [owner, setOwner] = useState(null); const [stage, setStage] = useState(null); const [selectedId, setSelectedId] = useState(initial[0].id); const [message, setMessage] = useState( 'Wähle einen Eintrag aus, um ihn zu bearbeiten.', ); const owners = [...new Set(items.map((item) => item.owner))]; const filtered = useMemo( () => items.filter( (item) => (!query || `${item.title} ${item.account} ${item.owner}` .toLowerCase() .includes(query.toLowerCase())) && (owner == null || item.owner === owner) && (stage == null || item.stage === stage), ), [items, query, owner, stage], ); const selected = filtered.find((item) => item.id === selectedId) ?? filtered[0]; const completed = filtered.filter((item) => item.stage === 2).length; const total = filtered.reduce((sum, item) => sum + item.value, 0); const progress = filtered.length ? Math.round( filtered.reduce((sum, item) => sum + item.progress, 0) / filtered.length, ) : 0; const update = (patch: Partial) => { if (!selected) return; setItems((previous) => previous.map((item) => item.id === selected.id ? { ...item, ...patch } : item, ), ); setMessage( `${selected.title} aktualisiert. Die Änderung ist in allen Ansichten sichtbar.`, ); }; const lanes = config.stages.map((title, index) => ({ id: `stage-${index}`, title, minWidth: '210px', items: filtered .filter((item) => item.stage === index) .map((item) => ({ id: item.id, title: item.title, subtitle: item.account, value: ( ), clickable: true, draggable: true, })), })); const resourceRows = owners.map((team) => ({ id: team, columns: [ { title: team, subtitle: `${filtered.filter((item) => item.owner === team).length} Einsätze`, clickable: true, }, ], timeEntries: filtered .filter((item) => item.owner === team) .map((item) => ({ id: item.id, title: item.title, dateFrom: dayDate(item.day), dateTo: dayDate(item.day + 1), clickable: true, draggable: false, customLayout: (
    {item.title}
    {item.account}
    ), })), })); const timeEntries = filtered.map((item) => ({ id: item.id, title: item.title, dateFrom: dayDate(item.day), dateTo: dayDate(item.day), timeFrom: item.hour * 3600000, timeTo: (item.hour + 1) * 3600000, clickable: true, draggable: false, customLayout: (
    {item.title}
    {item.owner}
    ), })); const table = ( ({ id: item.id, clickable: true, rowColor: selected?.id === item.id ? '#f4f4f5' : '#ffffff', columns: [ { value: (
    {item.title}
    {item.account}
    ), width: 'fraction', sort: { value: item.title }, }, { value: item.owner, width: '105px' }, { value: pill(item.stage), width: '125px' }, { value: config.kind === 'sales' ? ( money(item.value) ) : ( ), width: '110px', sort: { value: config.kind === 'sales' ? item.value : item.progress, }, }, ], })), emptyTable: { title: 'Keine passenden Einträge', value: (
    {'Keine passenden Einträge'}

    {'Setze die Suche oder den Team-Filter zurück.'}

    ), }, }} onCellClick={(_event, payload) => { if (payload.row_index != null && filtered[payload.row_index]) setSelectedId(filtered[payload.row_index].id); }} /> ); return (
    Arc Rider · Example App

    {config.title}

    Interaktive Demo

    {config.description}

    setQuery(String(value ?? ''))} />
    ({ value, title: value, active: owner === value, })), reset: { title: 'Alle Teams' }, }} onChange={setOwner} />
    {view === 'overview' && (
    setStage(index === 2 ? 2 : null)} />
    )}

    {config.views.find((tab) => tab.id === view)?.title}

    {filtered.length} Einträge
    {(view === 'calendar' || view === 'timeline') && (
    {new Date(dayDate(period * 7)).toLocaleDateString('de-DE')} –{' '} {new Date( dayDate(period * 7 + (view === 'timeline' ? 13 : 6)), ).toLocaleDateString('de-DE')}
    )} {(view === 'overview' || view === 'table') && (
    {table}
    )} {view === 'board' && (
    { const item = lanes[p.swimlane_index]?.items[p.item_index_in_section]; if (item) setSelectedId(String(item.id)); }} onItemDrop={async (p) => { const source = lanes[p.source_swimlane_index]?.items[ p.item_index_in_section ]; if (!source) return; const target = lanes[ p.target_swimlane_index ].items.filter((item) => item.id !== source.id); const before = target[p.target_index_in_section]?.id; setItems((previous) => { const item = previous.find( (item) => item.id === source.id, ); if (!item) return previous; const next = previous.filter( (item) => item.id !== source.id, ); const position = before ? next.findIndex((item) => item.id === before) : next.length; next.splice(position < 0 ? next.length : position, 0, { ...item, stage: p.target_swimlane_index, progress: p.target_swimlane_index === 2 ? 100 : item.stage === 2 ? 0 : item.progress, }); return next; }); setSelectedId(String(source.id)); setMessage( `${source.title} nach ${config.stages[p.target_swimlane_index]} verschoben.`, ); }} />
    )} {view === 'timeline' && (
    { const item = resourceRows[row]?.timeEntries[index]; if (item) setSelectedId(String(item.id)); }} onLabelClick={(row) => { setOwner(owners[row]); setMessage(`${owners[row]} ausgewählt.`); }} />
    )} {view === 'calendar' && (
    { if (filtered[index]) setSelectedId(filtered[index].id); }} />
    )}
    {message} · Änderungen bleiben in dieser Demo.
    ); } ``` ## Zusammenspiel der Widgets Die App hält die Einträge in einem React-State. `useMemo` leitet die gefilterte Liste ab; Tabelle, Board und Kalender erhalten daraus ihre jeweilige Konfiguration. Callbacks wählen stabile IDs aus oder verändern den gemeinsamen State. Die Widgets übernehmen Darstellung und Interaktion, die Anwendung besitzt die fachlichen Daten. ## Grenzen der Demo Die Daten sind fiktiv und bleiben im Browser-Speicher der laufenden Komponente. Es gibt keinen Server, keine Anmeldung und keine echte Speicherung. Kalendertermine werden über den Detailbereich verändert; die aktuellen Drag-Callbacks liefern noch keine vollständigen Zielzeiten. Nach einem Neuladen beginnt die App wieder mit den Ausgangsdaten. --- # Cyber Switch Source: https://www.arc-rider.com/docs/components/component-cyberswitch # Cyber Switch # Cyber Switch Komponente ![](https://framerusercontent.com/images/PaM0ADwAFOsyz6ngHL15g2UCrL0.png) Mit `arcCustomComponentCyberSwitch` erstellst du ganz einfach einen modernen Umschalter zwischen zwei (oder mehr) Zuständen – perfekt für Filter, Views oder einfache Statuswechsel in deiner App. Die Komponente basiert auf `arcCustomLayout` und kombiniert UI-Design und Interaktion in einem vorgefertigten, reaktiven Block. Der Cyber Switch kommt ohne komplizierte Setup-Schritte aus: Du definierst lediglich die Items, die aktiven und inaktiven Styles – und schon ist dein Schalter einsatzbereit. Dabei kannst du das Design vollständig an dein Farbschema anpassen. **💡 Besonders hilfreich:** - Ideal für visuelle Filter wie "Aktiv / Inaktiv", "Heute / Morgen", "Liste / Karte" etc. - Nutzt `clickAction`, um z. B. Felder direkt zu aktualisieren. - Jeder Button passt sich automatisch an das übergeordnete Layout an. Im folgenden Abschnitt findest du den Anwendungscode sowie ein Beispiel zur Anwendung in Ninox. ## Gesamter Anwendungscode Im folgenden leeren Anwendungscode findest du ein Beispiel mit zwei Items, die manuell definiert werden – inklusive individueller `clickAction` für jeden Schalter. ```javascript arcCustomComponentCyberSwitch({ basicSettings: { backgroundColor: "" }, inactiveSettings: { color: "", backgroundColor: "" }, activeSettings: { color: "", backgroundColor: "" }, items: [{ title: "", active: "", clickAction: { recordId: Nr, type: "", field: fieldId(current, "), value: "" } }, { title: "", active: "", clickAction: { recordId: Nr, type: "", field: fieldId(current, "), value: "" } }] }) ``` ## Einzelne Parameter ### basicSettings Block Der `basicSettings`-Block definiert die grundlegenden Design-Eigenschaften der gesamten Schalter-Komponente. Aktuell gibt es nur einen konfigurierbaren Parameter:
    Parameter Beschreibung Default-Wert
    `backgroundColor` Hintergrund der gesamten Komponente `#F8FAFC`
    ```javascript basicSettings: { backgroundColor: "#F8FAFC" }, ``` 💡 **Tipp**: Nutze helle, neutrale Farben wie `#f8fafc` oder `#e5e7eb`, um den aktiven Zustand visuell stärker hervorzuheben. ### inactiveSettings Block Mit `inactiveSettings` legst du das Design für die nicht aktive(n) Option(en) fest – also für alle Items, die **nicht** den aktuellen Status darstellen.
    Parameter Beschreibung Default-Wert
    `color` Textfarbe für inaktive Buttons `#727c90`
    `backgroundColor` Hintergrundfarbe für inaktive Buttons `#F8FAFC`
    ```javascript inactiveSettings: { color: "#727c90", backgroundColor: "#F8FAFC" }, ``` 💡 **Tipp: **Wähle hier dezente Farben, damit der aktive Zustand deutlich hervorsticht. Beliebt ist z. B. eine Kombination aus hellgrauem Hintergrund mit leicht abgedunkelter Schriftfarbe. ### activeSettings Block Mit `activeSettings` gestaltest du das Design für den **aktiven** Button – also für das Item, das aktuell ausgewählt ist. Dadurch wird klar ersichtlich, welcher Zustand gerade aktiv ist.
    Parameter Beschreibung Default-Wert
    `color` Textfarbe für aktiven Button `#ffffff`
    `backgroundColor` Hintergrundfarbe für aktiven Button `#3b82f6`
    ```javascript activeSettings: { color: "#fff", backgroundColor: "#3b82f6" }, ``` ### items Block Im `items`-Block definierst du die einzelnen Schaltflächen (Switches), die innerhalb der CyberSwitch-Komponente angezeigt werden. Jeder Eintrag repräsentiert dabei einen Button mit eigenem Titel, Aktiv-Status und einer zugehörigen Aktion.
    Parameter Beschreibung Beispiel
    `title` Text, der im Button angezeigt wird. ```javascript title: "Menupunkt 1", ```
    `active` Bedingung, wann der Button als aktiv dargestellt wird.
    `clickAction` Aktion, die beim Klick auf den Button ausgeführt wird (z. B. Feld aktualisieren).
    **💡 Hinweise zur Verwendung:** - Du kannst beliebig viele Items definieren – mindestens zwei sind sinnvoll. - Die Items werden automatisch gleichmäßig in der Breite verteilt (`width: "fraction"`). - Es kann immer nur **ein** Item gleichzeitig als „aktiv“ dargestellt werden. Daher sollte dein Ausdruck bei `active` sicherstellen, dass er nur für eines zutrifft. - Der `clickAction` nutzt den gewohnten Aktionsstandard (z. B. `type: "update"`), wie du ihn aus anderen Komponenten kennst. ## Möglichkeiten, den `items`-Block aufzubauen Der `items`-Block kann auf zwei verschiedene Arten aufgebaut werden – je nach Anwendungsfall: ### 1. Dynamischer Aufbau über Hilfstabelle (empfohlen bei vielen Einträgen) Wenn du eine Tabelle (z. B. `Menu`) mit deinen möglichen Schaltereinträgen hast, kannst du diese dynamisch abrufen und in Buttons umwandeln. ```javascript items: (select Menu).[{ title: Title, active: number(current.helper_Menu) = number(Nr), clickAction: { recordId: current.Nr, type: "update", field: fieldId(current, "helper_Menu"), value: number(Nr) } }] ``` ###  2. Manueller Aufbau mit statischer Definition Alternativ kannst du die Buttons auch einzeln manuell definieren. Das bietet dir maximale Kontrolle über Reihenfolge, Texte und Aktionen – z. B. bei einfachen Umschaltern wie "Liste / Karte" oder "Heute / Morgen". ```javascript items: [{ title: "Allgemein", active: helper_Menu = "1", clickAction: { recordId: Nr, type: "update", field: fieldId(current, "helper_Menu"), value: 1 } }, { title: "Kontaktdaten", active: helper_Menu = "2", clickAction: { recordId: Nr, type: "update", field: fieldId(current, "helper_Menu"), value: 2 } }, { title: "Aufträge", active: helper_Menu = "3", clickAction: { recordId: Nr, type: "update", field: fieldId(current, "helper_Menu"), value: 3 } }] ``` ## Benötigte Widgets Der `CyberSwitch` ist eine **kostenlose Component**, die du direkt verwenden kannst – vorausgesetzt, du hast die folgenden **Widgets in deinem Team lizenziert**:
    Widget Beschreibung
    `arcCustomLayout` Grundstruktur zur Darstellung der Buttons
    `arcCustomText` (optional & kostenfrei) Nur nötig, wenn du Texte innerhalb der Switch-Items selbst gestalten willst
    ## 🧩 Fazit Mit dem `arcCustomComponentCyberSwitch` kannst du in wenigen Minuten ein modernes, visuell ansprechendes Umschaltelement in deine Ninox-Oberfläche integrieren – ideal für Filter, View-Switches oder interaktive Dashboards. Dank der Kombination mit `arcCustomLayout` ist das Design flexibel anpassbar und lässt sich nahtlos in bestehende Strukturen einfügen. Ob dynamisch über Hilfstabellen oder manuell mit gezielter Steuerung: Du behältst die volle Kontrolle. --- # Formular aus React-Bausteinen Source: https://www.arc-rider.com/docs/components/react-form # Formular aus React-Bausteinen Ein Formular kombiniert Eingaben, Statusanzeige und Aktionen. Definiere einen gemeinsamen State, führe Validierung beim Bestätigen aus und zeige Fehler neben den Eingaben. ## Vollständiges Beispiel ```tsx import { useState } from 'react'; import { ArcWidgetSteps, ArcWidgetInput, ArcWidgetButton, } from '@arcrider/arcwidgets-react'; export default function Example() { const [step, setStep] = useState(0); const [name, setName] = useState(''); const [error, setError] = useState(''); return ( <> ({ title, value: String(index + 1), backgroundColor: index === step ? '#34705b' : '#e5e7eb', })), }} /> {step === 0 && ( setName(String(v ?? ''))} /> )} {step === 1 &&

    Bitte prüfen: {name}

    } {step === 2 &&

    Erfasst: {name}

    } {step < 2 && ( { if (!name.trim()) { setError('Bitte einen Namen eingeben.'); return; } setError(''); setStep((s) => s + 1); }} /> )} {step > 0 && ( )}

    {error}

    ); } ``` ## Wiederverwenden Ziehe die fachlichen Felder und Speicherfunktion als Props in deine eigene Komponente. Halte die Widgets an diesen State gebunden. Das Beispiel verwendet bewusst auch native Formularelemente: arcWidgets lassen sich mit deinen vorhandenen React-Komponenten kombinieren. Für Auswahlfelder verwende [Select](/docs/react/mini-widgets/select), für freie Eingaben [Input](/docs/react/mini-widgets/input) und für asynchrones Speichern [Button](/docs/react/mini-widgets/buttons). --- # Full Page Classic Source: https://www.arc-rider.com/docs/components/full-page-classic # Full Page Classic --- # KPI Focus Source: https://www.arc-rider.com/docs/components/kpi-focus # KPI Focus # Kompakte KPI-Übersicht für Dashboards ![](https://framerusercontent.com/images/HI2EdFNTyBNJVfddUlOkVk442M.png) Mit `arcComponentKpiFocus()` kannst du wichtige Kennzahlen übersichtlich und ansprechend direkt in deinen Ninox-Dashboards darstellen. Jede KPI-Box zeigt eine Hauptzahl mit Icon und Titel sowie eine zusätzliche Unterzeile (Subtitle) mit weiterführenden Informationen – zum Beispiel Trends, Veränderungen oder Statusmeldungen. Die Component eignet sich ideal für: - Dashboard-Übersichten (z. B. Aufträge, Aufgaben, Termine) - Lager- und Bestandsmanagement - Team- oder Projekt-KPIs - Jede andere Art von Kennzahlen, die schnell und klar erfassbar sein sollen 💡 Besonderheiten: - Frei konfigurierbare Anzahl von KPI-Elementen - Individuelle Icons und Farben pro KPI - Subtitle-Bereich für zusätzliche Werte und kurze Beschreibungen - Ideal kombinierbar mit anderen arcComponents und Widgets für ein konsistentes Dashboard-Design **Vorgefertigte Komponente:** Du musst dich nicht um Layout, Farben oder Icon-Design kümmern – einfach deine eigenen Daten einsetzen und direkt loslegen. Benötigte Widgets: [KPI Bar](https://www.arc-rider.de/documentation/custom-kpi-bar), [Layout](https://www.arc-rider.de/documentation/custom-layout), [Text](https://www.arc-rider.de/documentation/text), [Icon](https://www.arc-rider.de/documentation/custom-icon) ## Anwendungscode ```javascript arcComponentKpiFocus({ uniqueId: " + Nr, items: [{ id: 1, title: "", iconName: "", value: "", subtitle: { color: "", iconName: "", value: "", description: "" } }, { id: 2, title: "", iconName: "", value: "", subtitle: { color: "", iconName: "", value: "", description: "" } }, { id: 3, title: "", iconName: "", value: "", subtitle: { color: "", iconName: "", value: "", description: "" } }] }) ``` ## Parameter ### uniqueId - Typ: `text` - Pflichtfeld: ja Die uniqueId ist eine eindeutige Kennung für die Komponente. Wenn du die Komponente mehrfach auf einer Seite verwendest, solltest du die ID dynamisch erzeugen, zum Beispiel mit `+ Nr`. ### items-Block Der Block `items` enthält alle KPI-Elemente, die innerhalb der Komponente angezeigt werden sollen. Jedes Element im Array repräsentiert eine einzelne KPI-Box mit Titel, Icon, Wert und einem optionalen Subtitle-Bereich. Mit `items` kannst du: - beliebig viele KPIs definieren, - jedem KPI ein eigenes Icon und eine eigene Farbe zuweisen, - zusätzliche Informationen wie Trends oder Statusangaben über den Subtitle hinzufügen.
    Parameter Typ Pflicht Beschreibung Beispiel
    `id` Zahl ✅ Eindeutige ID des KPI-Elements. Wird zur Unterscheidung der einzelnen KPIs genutzt. `1`
    `title` Text ❌ Titel, der oberhalb des Wertes angezeigt wird. `"Lagerbestand"`
    `iconName` Text ✅ Name des Icons aus der integrierten Icon-Bibliothek. `"buildings"`
    `value` Text ✅ Der Hauptwert, der in der KPI-Box angezeigt wird. `text(sum(lager.sum_lagerbestand))`
    `subtitle` Objekt ❌ Zusätzlicher Bereich unterhalb des Hauptwerts mit Detailinformationen. Siehe unten. Siehe `subtitle`-Struktur
    ### subtitle-Block Der Block `subtitle` ist optional und erlaubt es, unterhalb des Hauptwertes zusätzliche Informationen anzuzeigen. Damit kannst du z. B. Trends, Statuswerte oder Detailinformationen zu deiner KPI hervorheben.
    Parameter Typ Pflicht Beschreibung Beispiel
    `color` Text ❌ Legt die Text- und Icon-Farbe für den Subtitle fest. Unterstützt Hex- oder CSS-Farben. `"#3b82f6"`
    `iconName` Text ❌ Icon für den Subtitle. Wird links neben dem Wert angezeigt. Wird aus dem aktiven Icon-Set aus dem User Cockpit gezogen. `"truck"`
    `value` Text ❌ Der zusätzliche Wert, der im Subtitle angezeigt wird. `cnt(lagerbuchungen[Buchungsart = 1])`
    `description` Text ❌ Eine kurze Beschreibung oder Erläuterung für den Wert. `"neue Lieferungen"`
    ## Beispiel ![](https://framerusercontent.com/images/X6ZMPiPuINt3bM4sjgBfTJGxDQ.png) ```javascript arcComponentKpiFocus({ uniqueId: "KPI Focus Component " + Nr, items: [{ id: 1, title: "Lagerbestand", iconName: "buildings", value: text(sum(lager.sum_lagerbestand)), subtitle: { color: "#3b82f6", iconName: "truck", value: cnt(lagerbuchungen[Buchungsart = 1 and week('Erstellt am') = week(today())]), description: "neue Lieferungen" } }, { id: 2, title: "Warenwert", iconName: "coins", value: text(sum(lager.vk_wert_buchungen)), subtitle: { color: "green", iconName: "arrow-up-bold", value: text(sum(lagerbuchungen[Buchungsart = 2 and year('Erstellt am') = year(today())].vk_wert)), description: " dieses Jahr" } }, { id: 3, title: "Buchungen", iconName: "swap", value: text(cnt(lagerbuchungen)), subtitle: { color: "red", iconName: "warehouse", value: "+ " + cnt(lagerbuchungen[Buchungsart = 2]), description: " diese Woche" } }, { id: 4, title: "Mitarbeiter", iconName: "users", value: text(cnt(mitarbeiter)), subtitle: { color: "#3b82f6", iconName: "users", value: cnt(mitarbeiter['Berechtigt zur Lagerbewegung' = true]), description: "Berechtigung zur Lagerbewegung" } }] }) ``` --- # Nested Items (Table) Source: https://www.arc-rider.com/docs/components/hierarchisch-ausklappbare-einträge # Nested Items (Table) # Hierarchisch Ausklappbare Einträge ## Native nested rows (empfohlen) Ab der Next-Gen **arcCustomTable** kannst du Hierarchien **direkt im Widget** abbilden — ohne `arcComponentNestedItems` und ohne `showChildren`-DB-Updates. **Zwei gleichwertige Top-Level-Formen:** - **Classic:** `table: [{ columns, items: [...] }]` - **Neu:** root-`items: [{ columns, items: [...] }]` ```javascript arcCustomTable({ uniqueListId: "tasks-" + Nr, header: { columns: [ /* widths required */ ] }, collapsible: { enabled: true, expandColumn: 0, indentColumn: 1 }, table: oberaufgaben.[{ id: Nr, recordId: Nr, columns: nestedItem_row.columns, items: unteraufgaben.[{ id: Nr, recordId: Nr, columns: nestedItem_row.columns, items: ... }] }] }) ``` - **Collapse** läuft im Browser (persistiert pro `uniqueListId`). - **Einrückung:** `collapsible.indentColumn` + automatische Tiefe — `paddingLeft`-Formelfeld optional. - **Parent mit breiter Zelle:** `span: 3` auf der Zelle statt Width-Summen in Ninox. Vollständige API: [Custom Table — Nested rows](/custom-table#nested-rows-items). --- ## Legacy: `arcComponentNestedItems` (deprecated) Der folgende Abschnitt beschreibt das **ältere Ninox-Pattern** mit `arcComponentNestedItems` und flachem `table[]`. Es funktioniert weiterhin, Migration zum nativen `items[]`-Baum wird empfohlen. In diesem Artikel zeigen wir dir, wie du das Widget arcComponentNestedItems optimal anpasst, um hierarchisch ausklappbare Elemente in deinem Projekt umzusetzen. Egal ob du verschachtelte Menüs, Kategorien oder andere Listenstrukturen benötigst – mit unseren Tipps und Tricks kannst du die Funktionalität des Widgets erweitern und es perfekt auf deine Anforderungen abstimmen. ![](https://framerusercontent.com/images/d1EdEGEHRyhtVcof3w6hZSlbLM.png) # Vorbereitung Überlege dir zunächst, welche Tabellen du für deine hierarchischen Einträge benötigst. Das könnten zum Beispiel Projekte mit zugehörigen Aufgaben sein. In unserem Beispiel arbeiten wir jedoch nur mit Aufgaben und deren Unteraufgaben, die beide in der Tabelle "Aufgaben" erfasst werden. Jede Aufgabe wird dabei entweder als "Oberaufgabe" oder "Unteraufgabe" kategorisiert. Um die Hierarchie optimal darzustellen, sind folgende Felder in der Tabelle erforderlich: - **Ja/Nein-Feld:** `showChildren` – Steuert, ob die Unteraufgaben angezeigt werden sollen. - **Formelfeld:** `paddingLeft` Dieses Feld sorgt für den Abstand auf der linken Seite (eingerückte Unteraufgaben) der Einträge und verwendet folgende Logik: ```javascript let parent := Oberaufgabe; let insetCount := 10; while parent != null do insetCount := insetCount + 10; parent := parent.Oberaufgabe end ; insetCount + "px" ``` - **Formelfeld:** `Level` Bestimmt die Hierarchieebene der Aufgabe. Die Formel dafür lautet: ```javascript let parent := Oberaufgabe; let levelCount := 0; while parent != null do levelCount := levelCount + 1; parent := parent.Oberaufgabe end ; levelCount ``` - **Formelfeld:** `cntUnteraufgaben` Zählt die Anzahl der Unteraufgaben und verwendet diese Formel: ```javascript let children := Unteraufgaben; cnt(children) ``` - **Formelfeld:** `nestedItem_row` Dieses Feld bereitet die Struktur für die Anzeige der hierarchischen Einträge vor. Diese Felder dienen als Hilfsfelder und können in einem eigenen Helper-Tab untergebracht werden, da sie auf der Hauptoberfläche nicht direkt sichtbar sein müssen. # Gestaltung der Zeilen mit `nestedItem_row` Mit dem folgenden Code befüllst du das Formelfeld `nestedItem_row`, wodurch du die Darstellung der einzelnen Zeilen in deiner benutzerdefinierten Tabelle (`[Custom Table](/custom-table)`) steuern kannst. In diesem Beispiel werden die Zeilen übersichtlich und funktional aufgebaut: Sie enthalten ein Symbol zum Ein- und Ausklappen, die Bezeichnung der Aufgabe und ein Feld, das die Anzahl der Unteraufgaben anzeigt. Außerdem sind die Zeilen klickbar, sodass du direkt auf den jeweiligen Datensatz zugreifen kannst. Hier der Code: ```javascript let current := this; { recordId: Nr, rowColor: if Level = 0 then "#87c5f2" else if Level = 1 then "#c5e4f9" else if Level = 2 then "#eff7fc" end end end, rowHeight: "auto", rowPaddingY: "5px", rowHoverAction: { hoverActive: false, backgroundColor: "", fontColor: "" }, groupRowColor: "", groupRowSettings: { height: "30px" }, columns: [{ field: "", title: "", width: "40px", height: "", lineHeight: "", alignX: "left", value: arcCustomIcon({ name: if showChildren then "minus-bold" else "plus-bold" end, containerSize: "25px", borderSize: "", borderColor: "", iconSize: "", borderRadius: "4px", color: if cnt(Unteraufgaben) > 0 then " else "#c5cce2" end, backgroundColor: "#eee" }), color: "", styles: "", actions: if cnt(Unteraufgaben) > 0 then [{ type: "update", recordId: current.Nr, field: fieldId(this, "showChildren"), value: if showChildren = true then null else true end }] end }, { field: "Bezeichnung", title: "Bezeichnung", paddingX: paddingLeft, value: Aufgabe, color: "", backgroundColor: "", width: "200px", align: "left", actions: [{ type: "popup", showPopupButton: false, recordId: current.Nr }] }, { field: "Anzahl Aufgaben", title: "Anzahl Aufgaben", value: if cntUnteraufgaben > 0 then cntUnteraufgaben + " Aufgaben" else "Keine Unteraufgaben" end, color: "", backgroundColor: "", width: "", align: "left", paddingX: paddingLeft, paddingY: "", groupExpandAction: false, groupValue: arcCustomIcon({ name: "caret-down" }), groupByValue: "", actions: [{ type: "popup", showPopupButton: false, recordId: current.Nr }] }] } ``` In dieser Konfiguration: - **Icon zum Ein- und Ausklappen:** Das Icon zeigt abhängig vom `showChildren`-Status entweder ein Minus (für ausgeklappt) oder ein Plus (für eingeklappt) an. Zudem sind die Icons bei Aufgaben ohne Unteraufgaben abgeblendet. - **Bezeichnung der Aufgabe:** Die Bezeichnung der Aufgabe wird entsprechend eingerückt, damit die Hierarchie klar erkennbar ist. - **Anzahl der Unteraufgaben:** Ein Textfeld zeigt die Anzahl der vorhandenen Unteraufgaben oder gibt an, wenn keine vorhanden sind. Dieser Aufbau ermöglicht eine übersichtliche und funktionale Struktur für deine hierarchischen Einträge. ### Einfügen der hierarchischen Tabelle in die Zieloberfläche Nun kannst du auf deiner Zieloberfläche ein Formelfeld in deiner Custom Table erstellen, das die zuvor definierten Zeilen mit `nestedItem_row` wiedergibt. Der folgende Code setzt die hierarchisch ausklappbare Struktur um: ```javascript let list := (select Aufgaben where Oberaufgabe = null); let showLogik := true; let nestedData := { uniqueId: "Verschachtelte Items Aufgaben" + Nr, items: list.[{ recordId: format(number(Nr), "0000"), row: nestedItem_row, items: if showChildren = true then Unteraufgaben.[{ recordId: format(number(Nr), "0000"), row: nestedItem_row, items: if showChildren = true then Unteraufgaben.[{ recordId: format(number(Nr), "0000"), row: nestedItem_row }] end }] end }] }; let tableData := { uniqueListId: "Nested Example" + Nr, tableId: "", height: "700px", embedded: false, rowHoverAction: { hoverActive: false, backgroundColor: "", fontColor: "" }, header: { showHeader: true, height: "", fontColor: "", backgroundColor: "", fontSize: "" }, emptyTable: { title: "Keine Aufgaben gefunden" }, scrollBar: { showScrollBar: false, height: "", backgroundColor: "", handle: { height: "", borderRadius: "", backgroundColor: "" } }, table: sort(arcComponentNestedItems(nestedData).items).row, footer: { showFooter: false, showActionButton: true, actionButtonTitle: "", leftSideContent: "", rightSideContent: "" } }; arcCustomTable(tableData) ``` Damit ist deine Tabelle für hierarchisch ausklappbare Elemente einsatzbereit! --- # Nested Layout Source: https://www.arc-rider.com/docs/components/layout-hierarchisch-ausklappbare-einträge # Nested Layout # Hierarchisch Ausklappbare Einträge In diesem Artikel zeigen wir dir, wie du das Widget arcComponentNestedLayout optimal anpasst, um hierarchisch ausklappbare Elemente in deinem Projekt umzusetzen. Egal ob du verschachtelte Menüs, Kategorien oder andere Listenstrukturen benötigst – mit unseren Tipps und Tricks kannst du die Funktionalität des Widgets erweitern und es perfekt auf deine Anforderungen abstimmen. ![](https://framerusercontent.com/images/Cd81FvuVmPX1GAfIXPM5uDUD8c.png) # Vorbereitung Überlege dir zunächst, welche Tabellen du für deine hierarchischen Einträge benötigst. Das könnten zum Beispiel Projekte mit zugehörigen Aufgaben sein. In unserem Beispiel arbeiten wir jedoch nur mit Aufgaben und deren Unteraufgaben, die beide in der Tabelle "Aufgaben" erfasst werden. Jede Aufgabe wird dabei entweder als "Oberaufgabe" oder "Unteraufgabe" kategorisiert. Um die Hierarchie optimal darzustellen, sind folgende Felder in der Tabelle erforderlich: - **Ja/Nein-Feld:** `showChildren` – Steuert, ob die Unteraufgaben angezeigt werden sollen. - **Formelfeld:** `paddingLeft` Dieses Feld sorgt für den Abstand auf der linken Seite (eingerückte Unteraufgaben) der Einträge und verwendet folgende Logik: ```javascript let parent := Oberaufgabe; let insetCount := 10; while parent != null do insetCount := insetCount + 10; parent := parent.Oberaufgabe end ; insetCount + "px" ``` - **Formelfeld:** `Level` Bestimmt die Hierarchieebene der Aufgabe. Die Formel dafür lautet: ```javascript let parent := Oberaufgabe; let levelCount := 0; while parent != null do levelCount := levelCount + 1; parent := parent.Oberaufgabe end ; levelCount ``` - **Formelfeld:** `cntUnteraufgaben` Zählt die Anzahl der Unteraufgaben und verwendet diese Formel: ```javascript let children := Unteraufgaben; cnt(children) ``` - **Formelfeld:** `nestedItem_row` Dieses Feld bereitet die Struktur für die Anzeige der hierarchischen Einträge vor. Diese Felder dienen als Hilfsfelder und können in einem eigenen Helper-Tab untergebracht werden, da sie auf der Hauptoberfläche nicht direkt sichtbar sein müssen. # Gestaltung der Zeilen mit `nested_row` Mit dem folgenden Code befüllst du das Formelfeld `nested_row`, wodurch du die Darstellung der einzelnen Zeilen in deinem benutzerdefinierten Layout steuern kannst. In diesem Beispiel werden die Zeilen übersichtlich und funktional aufgebaut: Sie enthalten ein Symbol zum Ein- und Ausklappen, die Bezeichnung der Aufgabe und ein Feld, das die Anzahl der Unteraufgaben anzeigt. Außerdem sind die Zeilen klickbar, sodass du direkt auf den jeweiligen Datensatz zugreifen kannst. Hier der Code: ```javascript arcCustomLayout({ uniqueId: Nr, embedded: true, fullscreen: false, direction: "horizontal", alignX: "center", alignY: "center", width: "", height: "", gap: "5px", backgroundColor: "", paddingY: "", paddingX: "", styles: "padding-left:" + paddingLeft + ";", blocks: [{ width: "40px", height: "auto", lineHeight: "", alignX: "left", value: arcCustomIcon({ name: if showChildren then "minus-bold" else "plus-bold" end, containerSize: "25px", borderSize: "", borderColor: "", iconSize: "", borderRadius: "4px", color: if cnt(Unteraufgaben) > 0 then "#ffffff" else "#ffffff33" end, backgroundColor: if cnt(Unteraufgaben) > 0 then "#4b4d4a" else "#4b4d4a" end }), color: "", styles: "", clickAction: if cnt(Unteraufgaben) > 0 then { type: "update", recordId: Nr, field: "T1", value: if showChildren = true then null else true end } end }, { width: "", height: "auto", lineHeight: "", alignX: "left", value: Aufgabe, color: "", clickAction: { type: "popup", recordId: Nr } }] }) ``` In dieser Konfiguration: - **Icon zum Ein- und Ausklappen:** Das Icon zeigt abhängig vom `showChildren`-Status entweder ein Minus (für ausgeklappt) oder ein Plus (für eingeklappt) an. Zudem sind die Icons bei Aufgaben ohne Unteraufgaben abgeblendet. - **Bezeichnung der Aufgabe:** Die Bezeichnung der Aufgabe wird entsprechend eingerückt, damit die Hierarchie klar erkennbar ist. - **Anzahl der Unteraufgaben:** Ein Textfeld zeigt die Anzahl der vorhandenen Unteraufgaben oder gibt an, wenn keine vorhanden sind. Dieser Aufbau ermöglicht eine übersichtliche und funktionale Struktur für deine hierarchischen Einträge. ### Einfügen des hierarchischen Layouts in die Zieloberfläche Nun kannst du auf deiner Zieloberfläche ein Formelfeld in deinem Custom Layout erstellen, das die zuvor definierten Zeilen mit `nested_row` wiedergibt. Der folgende Code setzt die hierarchisch ausklappbare Struktur um: ```javascript let list := (select Aufgaben); let data := { uniqueId: "Verschachtelte Items", items: list[Oberaufgabe = null and Aufgabe != null].[{ recordId: Nr, title: nested_row, backgroundColor: "", items: if showChildren = true then let id := Nr; Unteraufgaben.[{ recordId: Nr, title: nested_row, startDate: Startdatum, backgroundColor: "", items: if showChildren = true then Unteraufgaben.[{ recordId: Nr, backgroundColor: "", title: nested_row, items: if showChildren = true then Unteraufgaben.[{ recordId: Nr, backgroundColor: "", title: nested_row }] end }] end }] end }] }; --- Hier betten wir die arcComponentNestedLayout() nochmal in ein Layout für das bessere Handling ---; arcCustomLayout({ uniqueId: "nested layout tours" + Nr, embedded: false, fullscreen: false, direction: "vertical", alignX: "center", alignY: "top", width: "", styles: "", height: "500px", gap: "5px", backgroundColor: "#e6f0fe", paddingY: "20px", paddingX: "20px", scrollSettings: { scrollY: true }, blocks: [{ width: "100%", height: "auto", lineHeight: "", alignX: "left", color: "", styles: "flex-direction: column;", value: arcComponentNestedLayout(data) }] }) ``` Damit ist dein Layout für hierarchisch ausklappbare Elemente einsatzbereit! --- # Switch Card Source: https://www.arc-rider.com/docs/components/component-switchcard # Switch Card # Component: SwitchCard ![](https://framerusercontent.com/images/7rZ98bqBrLDlaOefknMiI3aD7c.png) Die `arcComponentSwitchCard` ist eine vielseitige UI-Komponente, die dynamisch zwischen verschiedenen Inhalten umschalten kann – etwa zwischen Lagerbeständen, Kapazitäten oder anderen Visualisierungen. Sie kombiniert eine visuell ansprechende **Karte** mit einem eingebetteten **CyberSwitch**, der zwischen den definierten Ansichten wechselt. Typischerweise besteht sie aus: - einem **Titel** und **Untertitel** zur Einordnung des Inhalts, - einem **interaktiven Umschalter** (`arcCustomComponentCyberSwitch`) mit beliebig vielen Einträgen, - und einem **dynamischen Content-Bereich**, der je nach Auswahl angepasst wird. 💡 Besonders geeignet für Dashboards oder Übersichtsseiten, bei denen Nutzer:innen zwischen mehreren Kennzahlen oder Darstellungen wechseln sollen – ohne dabei die Ansicht verlassen zu müssen. ## Anwendungscode Im folgenden Beispiel siehst du den Grundaufbau der `SwitchCard` mit zwei Umschaltoptionen – jede davon enthält eigene Logik (`active`) und Content zur Darstellung im unteren Bereich der Karte. ```javascript arcComponentSwitchCard({ uniqueId: "", title: "", subtitle: "", switchItems: [{ id: 1, title: "", recordId: "", fieldId: "", value: "", active:", content: "" }, { id: 2, title: "", recordId: "", fieldId: "", value: "", active:", content: "" },] }) ``` ## Anleitungsvideo - so baust du die switchCard einfach auf In diesem Tutorial zeigt dir Lisa, wie du die kostenlose SwitchCard-Component in Ninox einrichten kannst. ###   ### Hauptparameter Diese Parameter werden direkt im Aufruf der `arcComponentSwitchCard` übergeben:
    Parameter Typ Beschreibung Beispiel
    `uniqueId` Text Eindeutige ID für die Komponente (für Styling und Wiederverwendung wichtig) ```javascript uniqueId: "card element Lagerkapazitäten" + Nr, ```
    `title` Text Überschrift der Karte ```javascript title: "Lagebestände", ```
    `subtitle` Text Untertitel oder Zusatzinfo unter dem Titel ```javascript subtitle: "Lagebestände aller Lager", ```
    `switchItems` Liste Liste mit den Umschaltoptionen (siehe unten für Details) ```javascript switchItems: [ {…}, {…} ] ```
    💡 **Hinweis: **Die `title`- und `subtitle`-Werte werden optional angezeigt – wenn sie leer sind, wird der Platz automatisch freigegeben. Der `uniqueId` sollte immer vergeben werden, um Konflikte mit anderen Komponenten zu vermeiden. ### Eintrag in `switchItems` Jedes Element in `switchItems` repräsentiert eine **Umschaltoption** (z. B. "Bestände", "Kapazitäten", "Verteilung") mit eigenem Titel, Aktivierungslogik, Aktionslogik und angezeigtem Inhalt.
    Parameter Typ Beschreibung Beispiel
    `id` Zahl o. Text Eindeutige ID für das Item (kann z. B. für dynamisches Mapping genutzt werden) ```javascript id: 1, ```
    `title` Text Titel des Buttons im Switch (z. B. "Bestände") ```javascript title: "Bestände", ```
    `recordId` ID/Ref ID des aktuellen Datensatzes, auf den sich die Aktion bezieht ```javascript recordId: Nr, ```
    `fieldId` Funktion o. Text (ID) Referenz auf das Feld, das aktualisiert werden soll ```javascript fieldId: fieldId(Nr, "filter_lagerkapazitaet"), ```
    `value` beliebig Wert, der gesetzt werden soll, wenn der Button geklickt wird ```javascript value: 2, ```
    `active` bool./Expr. Ausdruck, wann das Item aktiv ist ```javascript active: filter_lagerkapazitaet = "2", ```
    `content` Text Der eigentliche Inhalt, der angezeigt wird, wenn dieses Item aktiv ist ```javascript content: 'arcProgressBarComponent _lager' ```
    **💡 Hinweis:** - `content` kann reiner Ninox-Text, ein Funktionsaufruf oder auch ein zusammengesetztes Widget sein. - Es wird immer der erste Eintrag mit `active: true` angezeigt. - Falls du nur ein Item hast, wird der Switch automatisch ausgeblendet. ###  Typische Inhalte für `content` Der `content`-Wert kann unterschiedlich aufgebaut sein – je nachdem, welches Widget oder Element du in der Karte anzeigen möchtest. Hier einige typische Beispiele:
    Inhaltstyp Beschreibung Beispiel
    Widget-Referenz Aufruf eines anderen Widgets (z. B. eine Fortschrittsanzeige, ein Donut-Chart, ein eigenes Layout, etc.) ```javascript content: helper_donuts_lager ```
    Textausgabe Einfacher Text (z. B. als Platzhalter oder zur schnellen Anzeige)
    ## Benötigte Widgets Die `SwitchCard` ist eine **kostenlose Component**, die du direkt verwenden kannst – vorausgesetzt, du hast die folgenden **Widgets in deinem Team lizenziert**:
    Widget Beschreibung
    `arcCustomCard` Grundstruktur der Karte (Layout, Padding, Hintergrund)
    `arcCustomLayout` Flexible Verschachtelung von Inhalten und Layouts
    `arcCustomText` (kostenfrei) Darstellung von Titel und Untertitel
    `arcCustomComponentCyberSwitch` (kostenfrei) Umschalter zur Auswahl der Inhalte innerhalb der Karte
    ## Fazit Die `SwitchCard`-Komponente ist ein leistungsstarkes UI-Element, mit dem du Inhalte dynamisch, elegant und übersichtlich darstellen kannst. Ob KPIs, Diagramme oder komplexe Widgets – die Darstellung passt sich automatisch an die Auswahl im Switch an. Dank des modularen Aufbaus mit `arcCustomCard`, `arcCustomLayout` und `arcCustomComponentCyberSwitch` bleibt die Komponente flexibel und lässt sich problemlos in bestehende Layouts und Designsysteme integrieren. 👉 Nutze sie überall dort, wo du zwischen mehreren Ansichten oder Datenbereichen innerhalb einer Oberfläche umschalten möchtest – zum Beispiel: - in Dashboards - bei Detailansichten mit mehreren Datenperspektiven - oder zur visuellen Filterung von Inhalten Let’s make data delightful. 🚀 --- # React-Dashboard als Vorlage Source: https://www.arc-rider.com/docs/templates/react-starter # React-Dashboard als Vorlage Diese Vorlage kombiniert Kennzahlen, Fortschritt und eine Auftragsliste. Installiere zuerst das [React-Paket](/docs/react/intro/setup-react) und verwende die folgende Komponente in deiner Browser-App. ## Vollständige Vorlage ```tsx import { ArcWidgetKpiBar, ArcWidgetTable, ArcWidgetProgressBar, } from '@arcrider/arcwidgets-react'; export default function Example() { return (

    Kennzahlen

    Fortschritt

    Aktuell

    ); } ``` ## Anpassen Ersetze die Beispieldaten durch deinen State oder Daten aus deiner API. Ergänze Laden, leere Ergebnisse und Fehleranzeige. Die Anordnung übernimmt CSS Grid und passt sich der verfügbaren Breite an. Weitere vollständige Quellen liegen im npm-Paket unter `examples/docs/`. Sie verwenden ausschließlich öffentliche Exporte und sind gegen die mitgelieferten Typen geprüft. Die `.arctemplate`-Dateien des Developer Kits sind für die andere Plattform bestimmt und werden in React nicht importiert. --- # State-Bag in einem Textfeld Source: https://www.arc-rider.com/docs/templates/tips_statefield Hey zusammen! 👋 Ich habe kürzlich immer mehr mit sogenannten **StateFields** gearbeitet und wollte das Konzept mal teilen – vielleicht hilft es euch auch weiter. **Das Problem:** Für jeden Filter, Tab oder ausgewählten Datensatz braucht man normalerweise ein eigenes Ninox-Feld. Bei komplexeren UIs hat man schnell 5–10 Helper-Felder im Schema, nur für UI-Zustände. **Die Lösung: Ein JSON-Feld als „State-Bag"** Ein einziges Textfeld speichert ein JSON-Objekt mit beliebig vielen Keys. Navigation, Filter, ausgewählte IDs – alles in einem Feld. --- **1. Feld und Rohdaten auslesen** ``` let stateField := fieldId(Nr, "helper_uiState"); let stateRaw := if current.helper_uiState then parseJSON(text(current.helper_uiState)) else {} end; ``` **2. Einzelne Werte extrahieren (mit Default-Werten)** ``` let activeTab := if item(stateRaw, "activeTab") then text(item(stateRaw, "activeTab")) else "uebersicht" end; let statusFilter := if item(stateRaw, "statusFilter") then text(item(stateRaw, "statusFilter")) else "alle" end; let selectedRecordId := if item(stateRaw, "selectedId") then text(item(stateRaw, "selectedId")) else "" end; ``` **3. Werte schreiben — empfohlen: `dataBag` auf der Action** Partielles Patchen ohne manuelles `setItem` in der Formel — der Action Performer übernimmt Read → Patch → Write: ``` { type: "update", recordId: Nr, fieldId: stateField, dataBag: { base: helper_uiState, patch: { activeTab: "details" } } } ``` Ausführlich: [`data-bag-actions`](../patterns/data-bag-actions.md) — gilt für **alle Widgets mit `actions`** (Button, Layout, Table, Drawing, …). **Alternative (manuell):** Topf lesen → `setItem` → `formatJSON` als `value`: ``` let uiData := parseJSON( if current.helper_uiState then text(current.helper_uiState) else "{}" end); setItem(uiData, "activeTab", "details"); ``` Dann als Action: ``` { type: "update", recordId: Nr, field: stateField, value: formatJSON(uiData) } ``` **4. Mehrere Keys gleichzeitig setzen (z.B. Zurück-Button)** Mit `dataBag` mehrere Keys in einem Patch: ``` dataBag: { base: helper_uiState, patch: { selectedId: "", statusFilter: "alle" } } ``` Oder manuell: ``` let uiDataReset := parseJSON(text(current.helper_uiState)); setItem(uiDataReset, "selectedId", ""); setItem(uiDataReset, "statusFilter", "alle"); --- value: formatJSON(uiDataReset) in der Action ---; ``` --- **Genutzte Ninox-Funktionen:** - `parseJSON()` / `formatJSON()` – JSON ↔ Objekt (Plain-JSON im Textfeld) - `setItem(obj, "key", value)` – Key setzen - `item(obj, "key")` – Wert auslesen **Legacy:** Ältere Projekte nutzen manchmal `urlEncode`/`urlDecode` um JSON in Actions zu schreiben. Für neue Apps: **`dataBag` + Plain-JSON** — siehe [`data-bag-actions`](../patterns/data-bag-actions.md#legacy-urlencoded). **Vorteile:** - **Ein Feld statt vieler** – Filter, Tabs, ausgewählte IDs in einem Feld - **Einfach erweiterbar** – neuer Key = kein neues Feld im Schema - **Übersichtlich** – alle Keys an einer Stelle dokumentieren - **Weniger Wartung** – weniger Felder im Formular **Tipp:** Keys am besten im Code dokumentieren: ``` --- State-Keys: activeTab (uebersicht|details), statusFilter (alle|offen|erledigt), selectedId ---; ``` Viel Spaß damit! 🚀 --- # Arc & Ninox Development Rules Source: https://www.arc-rider.com/docs/knowledge-base/development-rules # Arc & Ninox Development Rules Diese Datei dient als Wissensbasis für die Entwicklung von Arc Widgets in Ninox. Sie wird kontinuierlich erweitert. ## 1. Ninox Script Syntax (Strict) Ninox verwendet eine eigene Skriptsprache, die JavaScript ähnelt, aber wichtige Unterschiede aufweist. ### Kommentare & Block-Strings * ❌ **VERBOTEN**: `//` oder `/* ... */` (Führt zu Syntaxfehlern in bestimmten Kontexten). * ✅ **ERLAUBT**: `--- Mein Kommentar ---;` (WICHTIG: Mit Semikolon `;` abschließen, wenn es als Statement steht). * ⚠️ **ACHTUNG**: Innerhalb von `--- ---` dürfen keine geschweiften Klammern `{ }` verwendet werden, außer zur Variablen-Interpolation. * Falsch: `--- User: { name: string } ---` (Ninox versucht `name: string` als Code auszuführen). * Richtig: `--- User: ( name: string ) ---` oder `--- User: [Name: String] ---`. #### ⚠️ **KRITISCH: Keine Kommentare innerhalb von JSON-Objekten** Kommentare sind NUR als eigenständige Statements erlaubt, NIEMALS innerhalb von JSON-Objekt-Definitionen. ```javascript // ❌ FALSCH: Kommentar innerhalb von JSON-Objekt blocks: [{ --- Das ist mein Block ---; // FEHLER! width: "100%", value: "..." }] // ❌ FALSCH: comment Property (keine gültige Ninox-Property) blocks: [{ comment: "Das ist mein Block", // FEHLER! width: "100%", value: "..." }] // ✅ RICHTIG: Kommentar als eigenständiges Statement vor dem Objekt --- Das ist mein Block ---; let myBlock := { width: "100%", value: "..." }; ``` **Faustregel:** Kommentare mit `--- ... ---;` sind Statements und können nur dort erscheinen, wo Statements erlaubt sind - NICHT innerhalb von `{ }` Objekt-Literalen. ### Variablen & Deklaration * ❌ **VERBOTEN**: `const`, `let`, `var` innerhalb von Widget-Konfigurationsobjekten. * ✅ **ERLAUBT**: `let myVar := ...` (nur im äußeren Ninox-Skript-Scope, nicht im JSON-Objekt). * **Zuweisung**: Ninox nutzt `:=` für Zuweisungen im Skript-Teil. #### Variablen-Zuweisung für Widget-Snippets Bei der Zuweisung komplexer Widget-Konfigurationen zu Variablen, KEINE `do...end` Blöcke verwenden. Stattdessen interne `let` Statements vor die Variablen-Zuweisung setzen. ```javascript // ❌ FALSCH: Variable mit do...end Block let my_card := do let list := (select Items); arcCustomCard({ uniqueId: "card-1", value: list.[{ ... }] }) end; // ✅ RICHTIG: let Statements davor, Variable ohne do...end let list := (select Items); let my_card := arcCustomCard({ uniqueId: "card-1", value: list.[{ ... }] }); ``` **Für wiederverwendbare Funktionen in Snippet-Dateien:** ```javascript // Funktionen global definieren (auf Top-Level) function myHelper(data : any) do arcCustomLayout({ ... }) end; // Dann in Variablen-Definitionen verwenden let list := (select Items); let my_card := arcCustomCard({ uniqueId: "card-1", value: myHelper({ items: list }) }); ``` **Vorteile:** - Saubere Code-Struktur - Funktionen sind global verfügbar - Variablen sind direkt nutzbar ohne extra Scoping ### Strings & Interpolation * **HTML**: HTML-Strings müssen in Triple-Dashes `---` eingeschlossen werden. * **Interpolation**: Variablen in HTML-Strings werden mit `{ variableName }` eingefügt. * Beispiel: `value: ---
    Text
    ---` ### Funktions-Scope * ✅ **GLOBAL**: Funktionen sollten immer auf Top-Level definiert werden. * ❌ **LOKAL**: Verschachtelte Funktionen (Funktion innerhalb einer Funktion) vermeiden, da Ninox diese nicht im globalen Scope findet. #### Funktions-Reihenfolge (Top-to-Bottom) * **Funktionen müssen vor ihrer Verwendung definiert sein.** Ninox wertet das Script von oben nach unten aus. Ruft `navBar` `pillButton` auf, muss `pillButton` vor `navBar` stehen, sonst: "Die Funktion ist nicht definiert: pillButton(any)". * Neue Helper-Funktionen, die von bestehendem Code genutzt werden, immer über dem ersten Aufruf platzieren. ## 2. Widget Konfiguration ### Grundstruktur Die Konfiguration erfolgt immer als Funktionsaufruf, der ein JSON-Objekt zurückgibt. ```javascript arcCustomWidgetName({ uniqueId: "unique-id-" + Nr, property: "value", blocks: [ ... ] }) ``` ### Best Practices 1. **uniqueId**: MUSS vorhanden und einzigartig sein. Oft kombiniert mit der Record-ID (`Nr`). * ⚠️ **WICHTIG**: Niemals Sonderzeichen, Kommas, Punkte oder Leerzeichen in IDs verwenden * Diese IDs landen in HTML-IDs (`id="..."`) und können das Layout zerstören * ❌ Falsch: `uniqueId: data.id + "-time-" + timeItem.label` (wenn label "0,5 Std." enthält) * ✅ Richtig: `uniqueId: data.id + "-time-" + index` oder sanitized key ```javascript // ❌ Falsch: Label mit Sonderzeichen for timeItem in timeDetails do { value: arcCustomLayout({ uniqueId: "detail-" + timeItem.label // "0,5 Std." → HTML-Fehler! }) } end // ✅ Richtig: Index oder sanitized key verwenden for timeItem in timeDetails do let idx := index(timeDetails, timeItem); { value: arcCustomLayout({ uniqueId: "detail-" + idx // Saubere ID }) } end ``` 2. **embedded**: Meistens `true`, wenn es Teil eines Layouts ist. 3. **Kein JSDoc**: Ninox-Editor unterstützt kein JSDoc. Dokumentation erfolgt separat oder via `--- Text ---`. 4. **Widget Values**: Werte immer direkt übergeben. KEIN `if/else` für leere Werte – Widgets halten intern immer empty/null. Besser: `value: selectedTab.content` statt `value: if x then x else "" end`. #### ⚠️ **HOHE PRIORITÄT: Null- und Leer-Fallbacks – KRITISCH FÜR AI** **Arc Widgets fangen `null` automatisch ab.** Wenn eine Bedingung nichts zurückgibt (kein `else`-Zweig), erhält das Widget `null` und rendert korrekt. Daher: - **NIEMALS** `else "" end` für Widget-Werte – Leerstring bricht das Rendering. - **NIEMALS** `else null end` – redundant; den `else`-Zweig komplett weglassen. - **IMMER** `if Bedingung then widget end` – kein `else` nötig. Widgets fangen null ab. ```javascript // ❌ FALSCH: else "" end leftSideContent: if hasData then arcCustomButton({...}) else "" end // ❌ FALSCH: else null end (redundant) leftSideContent: if hasData then arcCustomButton({...}) else null end // ✅ RICHTIG: else weglassen – Widget erhält null automatisch leftSideContent: if hasData then arcCustomButton({...}) end ``` **In den meisten Fällen ist `else "" end` NICHT nötig** – unsere Widgets fangen `null` automatisch ab, wenn es als Wert übergeben wird. Diese Regel ist wichtig, weil sie: - Den Code verschlankt - Typfehler mit `any` und gemischten Typen vermeidet (z.B. „dann/sonst liefern unterschiedliche Typen“) - Redundanz reduziert **`else [] end` ist NIEMALS nötig** – `null` übergeben oder den `else`-Zweig weglassen; Widgets handhaben leere Arrays. ```javascript // ❌ VERBOTEN: else "" end leftSideContent: if hasData then arcCustomButton({...}) else "" end // ❌ REDUNDANT: else null end (niemals nötig) leftSideContent: if hasData then arcCustomButton({...}) else null end // ✅ RICHTIG: else weglassen – Widgets fangen null automatisch ab leftSideContent: if hasData then arcCustomButton({...}) end // ❌ Unnötig: else "" end für einfache Werte value: if data.name then data.name else "" end // ✅ Richtig: else weglassen – Widget erhält null, wenn Bedingung falsch value: if data.name then data.name end // ❌ Unnötig: else [] end participants: if mitarbeiterData then [{ value: mitarbeiterData.name }] else [] end // ✅ Richtig: else weglassen – Widget erhält null participants: if mitarbeiterData then [{ value: mitarbeiterData.name }] end ``` **Ausnahme**: `else` nur verwenden, wenn der Fallback semantische Bedeutung hat (z.B. ein Standard-Label, das von „leer“ abweicht). ## 3. Datenzugriff & Actions ### Queries Ninox-Abfragen werden direkt in die Widget-Struktur integriert, oft um Arrays für `blocks` zu generieren. * **Select**: `(select TableName)` * **Relation**: `record.RelationName` * **Mapping**: `(select Table).[{ value: ... }]` #### ⚠️ **WICHTIG: Performance bei Filterung** **Grundregel**: Bei `select`-Queries immer `where` verwenden, nicht die Bracket-Syntax `[...]`. ```javascript // ❌ Langsamer: Bracket-Syntax bei select let list := (select Tickets)[number(Status) = 1]; // ✅ Schneller: where-Syntax bei select let list := (select Tickets where number(Status) = 1); // ✅ Mit konditionaler Filterung let filterPrio := number(filter_prio); let list := (select Tickets where if filterPrio then number(Prio) = filterPrio else true end); ``` **Ausnahme**: Bei Untertabellen/Relations funktioniert nur die Bracket-Syntax: ```javascript // ✅ Richtig: Bracket-Syntax bei Relations (where nicht möglich) let items := record.Positionen[Aktiv = true]; // ❌ Falsch: where bei Relations (funktioniert nicht) let items := record.Positionen where Aktiv = true; ``` **Zusammenfassung**: - `(select Table where ...)` → performant bei Haupttabellen - `relation[...]` → nur bei Untertabellen/Relations (dort geht kein `where`) #### ⚠️ Filterwerte MÜSSEN außerhalb der Query definiert werden Filterwerte für `where`-Klauseln oder `[...]`-Filter MÜSSEN als Variable VOR der Query definiert werden. Ninox kann Feldverweise aus anderen Kontexten (Dashboard, Parent-Record etc.) innerhalb von Query-Ausdrücken nicht zuverlässig auflösen. ```javascript --- ❌ FALSCH: Inline-Feldzugriff in der Query ---; let list := (select Termine where Status = dashboard.filter_status); let items := record.Positionen[Typ = parent.SelectedType]; --- ✅ RICHTIG: Filterwert vorher extrahieren ---; let filterStatus := dashboard.filter_status; let list := (select Termine where Status = filterStatus); let selectedType := parent.SelectedType; let items := record.Positionen[Typ = selectedType]; ``` Dies gilt für ALLE Queries – `select ... where`, Untertabellen `[...]` und `any`/`all`-Ausdrücke. #### Mehrfachauswahl-Felder (Dynamic Choice Fields) Ninox Mehrfachauswahl-Felder speichern intern eine kommaseparierte Liste von IDs. Um diese dynamisch zu verarbeiten: **1. IDs extrahieren mit `numbers()`:** ```javascript // numbers() gibt ein Array der ausgewählten IDs zurück let selectedIds := numbers(Niederlassung.Typ); // z.B. [1, 3, 5] ``` **2. Record anhand ID holen mit `record()`:** ```javascript // record(Tabelle, ID) holt den vollständigen Datensatz let typRecord := record(Typ_Firma, number(typId)); ``` **3. Auf Felder des Records zugreifen:** ```javascript typRecord.Titel // Text-Feld typRecord.'Farbe Hintergrund' // Feld mit Leerzeichen → in Hochkommas ``` **Vollständiges Pattern für dynamische Badges:** ```javascript badges: for typId in numbers(Niederlassung.Typ) do let typRecord := record(Typ_Firma, number(typId)); { width: "auto", height: "auto", value: arcCustomBadge({ uniqueId: Nr + "-typ-badge-" + typId, value: text(typRecord.Titel), backgroundColor: if typRecord.'Farbe Hintergrund' then text(typRecord.'Farbe Hintergrund') else "#888888" end }) } end ``` **Wichtig:** - `numbers()` funktioniert nur bei Mehrfachauswahl-Feldern - Bei Einfachauswahl: direkt `number(Feld)` verwenden - `record()` benötigt den Tabellennamen als erstes Argument (nicht die Tabellen-ID) #### JSON-Lookup-Tabellen (for...do item end Pattern) Um eine JSON-Liste filterbar zu machen, muss sie mit `for item in [...] do item end` gewrapped werden. Nur so kann man später mit `first(list[condition])` auf die Elemente zugreifen. ```javascript // ❌ Falsch: Direktes Array ist nicht filterbar in Ninox let labels := [{ diff: 0, label: "Heute" }, { diff: 1, label: "Morgen" }]; first(labels[diff = 0]); // Funktioniert nicht! // ✅ Richtig: Mit for...do item end aktivieren let labels := for item in [ { diff: 0, label: "Heute" }, { diff: 1, label: "Morgen" }, { diff: 2, label: "Übermorgen" }, { diff: -1, label: "Gestern" }, { diff: -2, label: "Vorgestern" } ] do item end; // Jetzt filterbar! let matchedLabel := first(labels[number(myDiff) = number(item.diff)]); if matchedLabel then text(matchedLabel.label) else "Fallback" end ``` **Pattern für relative Datums-Texte:** ```javascript // Oben definieren let relativeDateLabels := for item in [ { diff: 0, label: "Heute" }, { diff: 1, label: "Morgen" }, { diff: 2, label: "Übermorgen" }, { diff: -1, label: "Gestern" }, { diff: -2, label: "Vorgestern" } ] do item end; // Unten verwenden let actDate := date(myRecord.Datum); let diff := round((actDate - today()) / 86400000); // Millisekunden → Tage let matchedLabel := first(relativeDateLabels[number(diff) = number(item.diff)]); title: if matchedLabel then text(matchedLabel.label) else if diff > 1 then "In " + diff + " Tagen" else if diff = 1 then "In 1 Tag" // Singular! else if diff = -1 then "Vor 1 Tag" // Singular! else "Vor " + abs(diff) + " Tagen" end end end end ``` **Wichtig - Datums-Differenz in Ninox:** - ❌ `days()` funktioniert nur mit der appointment-Funktion - ✅ `round((datum1 - datum2) / 86400000)` - rechnet Millisekunden in Tage um - 86400000 = 24h × 60min × 60s × 1000ms - `round()` rundet auf ganze Tage - ⚠️ **Grammatik beachten**: Singular bei 1 Tag ("In 1 Tag", nicht "In 1 Tagen") **Vorteile:** - Zentrale Definition, leicht erweiterbar - Sauberer Code in der Anwendung - Beliebig viele Spezialfälle (Heute, Morgen, Übermorgen...) ohne verschachtelte if/else - Korrekte Singular/Plural-Grammatik #### IMMER Anzeigenamen, NIEMALS interne IDs in NinoxScript In NinoxScript-Code (Formeln, .arc-Dateien) **IMMER** den **Anzeigenamen** (Caption) der Felder verwenden, **NIEMALS** die interne `_id`. Das YAML-Schema zeigt Felder so: ``` Aufgabe: ← ANZEIGENAME → im Code verwenden _id: A ← INTERNE ID → NIEMALS im NinoxScript ``` - `Aufgabe: { _id: A }` → im Code `Aufgabe` verwenden, NICHT `A` - `Fällig: { _id: B }` → im Code `'Fällig'` verwenden, NICHT `B` - Feldnamen mit **Umlauten** (ä, ö, ü, ß) oder **Sonderzeichen** MÜSSEN in **einfache Anführungszeichen** gesetzt werden: `'Fällig'`, `'Größe'`, `'Straße'`, `'E-Mail'` - Feldnamen ohne Sonderzeichen brauchen keine Anführungszeichen: `Aufgabe`, `Status`, `Datum` ```javascript --- ❌ FALSCH: interne _id verwenden ---; if A = "offen" then 10 else 0 end let x := B + 5 (select Termine where C = "aktiv") --- ✅ RICHTIG: Anzeigenamen verwenden ---; if Aufgabe = "offen" then 10 else 0 end let x := 'Fällig' + 5 (select Termine where Status = "aktiv") ``` **Ausnahme:** Seed-Daten JSON (`ninox:seed`) verwenden `_id` als Feld-Keys, da die Ninox REST API diese erwartet. #### fieldId immer verwenden Bei allen Widget-Actions und Select-Fields muss `fieldId()` verwendet werden, nicht nur der Feldname als String. Die `Nr` ist entscheidend für die korrekte Feld-ID. ```javascript // ❌ Falsch: Nur Feldname als String field: "Bearbeiter" // ✅ Richtig: fieldId mit korrekter Record-Nr field: fieldId(Nr, "Bearbeiter") // ✅ Bei JSON-Konfigurationen: fieldId oben definieren wo Nr verfügbar ist selectField: fieldId(Nr, "Bearbeiter") // Im JSON wo Ninox-Kontext verfügbar ist ``` **Wichtig:** In verschachtelten Strukturen oder `any`-Parametern hat Ninox keinen Zugriff mehr auf `Nr`. Daher `fieldId` immer dort aufrufen, wo der Record-Kontext noch vorhanden ist. #### arcCustomSelect / Verknüpfungsfelder – value muss number(Nr) sein Bei dynamischen Selects und Verknüpfungsfeldern muss der `value` in items `number(Nr)` sein, nicht rohes `Nr`. Ninox erwartet numerische Werte für Record-Referenzen. ```javascript // ❌ FALSCH: rohes Nr items: (select Artikel).[{ title: text('Beschreibung Excel'), value: Nr }] // ✅ RICHTIG: number(Nr) items: (select Artikel).[{ title: text('Beschreibung Excel'), value: number(Nr) }] ``` #### fieldId – Tabellenname erforderlich `fieldId()` für tabellenübergreifende Referenzen (z.B. in changeFieldValues) muss den **Anzeigenamen der Tabelle** verwenden, nicht die interne Tabellen-ID. `fieldId("XE", "FieldName")` funktioniert nicht. ```javascript // ✅ RICHTIG: Tabellen-Anzeigename fieldId: fieldId("Auftraege", "Bearbeiter") // ❌ FALSCH: Tabellen-ID funktioniert nicht fieldId: fieldId("A", "Bearbeiter") ``` #### if/else Typ-Konsistenz – KRITISCH **Ninox verlangt, dass beide Zweige `then` und `else` denselben Datentyp zurückgeben.** Sonst: „Die Ausdrücke für 'dann' und 'sonst' liefern unterschiedliche Datentypen zurück“. ```javascript // ❌ FALSCH: string vs nid – unterschiedliche Typen title: text(if 'Beschreibung Excel' then 'Beschreibung Excel' else Nr end) // ✅ RICHTIG: text() in beiden Zweigen – gleicher Typ title: if 'Beschreibung Excel' then text('Beschreibung Excel') else text(Nr) end ``` **Bei Mischung von Text und Widget** (z.B. Tabellenspalten-Wert: Text ODER arcCustomSelect) müssen beide Zweige denselben Typ liefern. Für den Textfall `arcCustomText` verwenden, damit beide Widgets zurückgeben: ```javascript // ❌ FALSCH: html(text(...)) vs widget – weiterhin string vs nid value: if Artikel then html(text(Artikel.'Beschreibung Excel')) else arcCustomSelect({...}) end // ✅ RICHTIG: arcCustomText vs arcCustomSelect – beide Widgets value: if Artikel then arcCustomText({ value: text(...), fontSize: "13px" }) else arcCustomSelect({...}) end ``` #### Dashboard-Filterfelder – `current.` Prefix verwenden Wenn Filterfelder (z.B. `filter_status_quotes`, `filter_category`) auf dem **Dashboard-Record** (dem Formular/Record, auf dem das Widget läuft) gespeichert sind, immer über `current.` in Queries und Datenlogik zugreifen. Der Dashboard-Record ist `this` / `current`; ohne Prefix kann Ninox das Feld im falschen Kontext auflösen (z.B. in einer Schleife über Tickets). ```javascript // ❌ Falsch: filter_status_quotes ohne current – falscher Kontext in Queries let list := list_tickets[number(Status) = number(filter_status_quotes)]; // ✅ Richtig: current.filter_status_quotes – expliziter Dashboard-Record let list := list_tickets[number(Status) = 4 and if current.filter_status_quotes then number(Status) = number(current.filter_status_quotes) else true end]; ``` **Faustregel:** Filterfelder auf dem Dashboard → in Queries und Filterlogik `current.filter_feldname` verwenden. #### Dynamische IDs in Schleifen (Counter-Pattern) Wenn du dynamische uniqueIds in Schleifen brauchst, muss die Counter-Variable einmal mit `let` definiert und dann ohne `let` überschrieben werden. ```javascript // ❌ Falsch: let in jeder Iteration → IDs bleiben gleich for item in items do let idx := idx + 1; // Falsch! Neue Variable in jeder Iteration { uniqueId: "item-" + idx } end // ✅ Richtig: let einmal vor der Schleife, dann ohne let überschreiben let idx := 0; for item in items do idx := idx + 1; // Richtig! Überschreibt die äußere Variable { uniqueId: "item-" + idx } end ``` **Problem bei falschem Pattern:** Alle Widgets bekommen dieselbe ID → Widgets werden nicht angezeigt (besonders bei Select/Input). #### setItem – Mutiert Objekt In-Place **`setItem` mutiert das übergebene Objekt in-place.** Es gibt kein neues Objekt mit der Änderung zurück. Alles, was nach dem `setItem`-Aufruf kommt, sieht das mutierte Objekt. Wenn der modifizierte State an eine Action übergeben wird (z.B. `type: "update"` mit `value`), die **gleiche Variable** verwenden, die an `setItem` übergeben wurde – sie enthält nun die Änderung. ```javascript // ❌ Falsch: rowActionDataTeil enthält ggf. nicht die Änderung (Rückgabewert von setItem) let rowActionDataTeil := setItem(parseJSON(...), "teiluntersuchung_recordId", text(Nr)); value: formatJSON(rowActionDataTeil) // Hat ggf. nicht die Änderung // ✅ Bevorzugt: dataBag patch dataBag: { base: helper_state, patch: { teiluntersuchung_recordId: text(Nr) } } // ✅ Manuell: Geparstes Objekt in Variable, setItem mutiert es, diese Variable verwenden let parsedStateTeil := parseJSON(text(current.helper_state)); setItem(parsedStateTeil, "teiluntersuchung_recordId", text(Nr)); value: formatJSON(parsedStateTeil) // parsedStateTeil hat die Änderung ``` **Faustregel:** In Variable parsen → `setItem` darauf aufrufen → diese Variable für `formatJSON` / `value` nutzen. #### Flexible Block-Breiten statt fixer Werte Statt feste Pixel-Breiten für Widgets zu verwenden, setze den Block auf `width: "fraction"` und das Widget auf `width: "100%"`. So passt sich die Breite dynamisch an. ```javascript // ❌ Unnötig komplex: Feste Breiten und extra Konfiguration { width: "auto", value: arcCustomSelect({ width: "140px" // Fix }) } // ✅ Besser: Block fraction, Widget 100% { width: "fraction", value: arcCustomSelect({ width: "100%" // Füllt Block aus }) } ``` ### Actions #### ⚠️ **WICHTIG: Action-Typ Hierarchie** **Grundregel**: Bei Feld-Updates IMMER widget-vorgefertigte Actions verwenden, NIEMALS customJS für Standard-Operationen. 1. **ERSTE WAHL: `update` Action** - Für alle Feld-Updates verwenden - Ninox Trigger werden automatisch ausgelöst - Sauber, wartbar, performant 2. **ZWEITE WAHL: Ninox Trigger + Helper-Feld** - Für komplexe Operationen, die Ninox-Funktionen benötigen - Z.B. `openURL()` für Mobile-Kompatibilität 3. **LETZTE WAHL: `customJS`** - NUR für spezielle UI-Anforderungen - Niemals für Feld-Updates - Niemals für Standard-Operationen ```javascript // ❌ FALSCH: customJS für Feld-Updates actions: [{ type: "customJS", value: "ninoxUpdate(" + Nr + ", '" + fieldId(Nr, "Status") + "', 'Aktiv'); " + "ninoxUpdate(" + Nr + ", '" + fieldId(Nr, "Tab") + "', 'day');" }] // ✅ RICHTIG: Separate update Actions actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Status"), value: "Aktiv" }, { type: "update", recordId: Nr, field: fieldId(Nr, "Tab"), value: "day" }] ``` **Vorteile von `update` Actions**: - ✅ Ninox Trigger werden automatisch ausgeführt - ✅ Validierungen greifen - ✅ Änderungen werden korrekt protokolliert - ✅ Bessere Performance - ✅ Wartbarer Code #### Update Action Details * ✅ **UPDATE**: Für Datenänderungen `type: "update"` verwenden (sauberer als customJS). * **recordId**: Innerhalb von Mappings/Schleifen (`select Table`) meist `Nr` (Referenz auf den aktuellen Datensatz). * **field/fieldId**: Parameter für das Feld. Achtung: Manche Widgets verlangen `field`, andere `fieldId`. * Wert ist oft die interne ID (z.B. `"A1"`) oder dynamisch ermittelt via `fieldId("TableName", "FieldName")` (besser, da robust gegen Umbenennung). ```javascript actions: [{ type: "update", recordId: Nr, // Aktueller Datensatz in der Schleife field: fieldId("MyTable", "Status"), // oder "A1" value: "newValue" }] ``` #### ⚠️ **KRITISCH: recordId bei Actions - NIEMALS number() verwenden** Bei `type: "popup"` oder `type: "update"` muss `recordId` immer die **rohe `Nr`** sein, NICHT `number(Nr)`. **Grund:** Ninox benötigt die versteckte `nxid` (Tabelle + Record-Nummer), die in `Nr` enthalten ist. `number()` entfernt diese Information! ```javascript // ❌ FALSCH: number() entfernt nxid → Record kann nicht geöffnet werden actions: [{ type: "popup", recordId: number(helper_nextOpenActivity.Nr) // Fehler! }] // ✅ RICHTIG: Nr direkt verwenden actions: [{ type: "popup", recordId: helper_nextOpenActivity.Nr // Behält nxid }] // ✅ RICHTIG: Bei update auch Nr direkt actions: [{ type: "update", recordId: Nr, // Nicht number(Nr) field: fieldId(Nr, "Status"), value: "active" }] ``` **Faustregel:** `recordId` in Actions = immer die rohe `Nr` ohne Konvertierung! #### Multiple Actions Pattern Widgets unterstützen Arrays von Actions, die sequenziell ausgeführt werden: ```javascript // Navigation + Datum setzen actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Datum von"), value: today() }, { type: "update", recordId: Nr, field: fieldId(Nr, "helper_mobileNav"), value: "day" }] ``` #### Bestätigungsdialog (confirm) Jede Action kann vor der Ausführung einen Bestätigungsdialog anzeigen, indem eine `confirm`-Property hinzugefügt wird. Der Dialog wird außerhalb des Ninox-Widget-DOM gerendert und überlebt daher Hintergrund-Neuladungen des Funktionsfelds (z.B. Datenaktualisierungen durch andere User). ```javascript // ✅ Destruktive Aktion mit Bestätigung actions: [{ type: "delete", recordId: Nr, confirm: { title: "Datensatz löschen", message: "Möchten Sie diesen Datensatz wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", confirmLabel: "Löschen", cancelLabel: "Abbrechen", destructive: true } }] // ✅ Nicht-destruktive Aktion mit Bestätigung actions: [{ type: "update", recordId: Nr, field: fieldId(Nr, "Status"), value: "Archiviert", confirm: { title: "Datensatz archivieren", message: "Dieser Datensatz wird ins Archiv verschoben.", confirmLabel: "Archivieren", destructive: false } }] ``` **Properties:** | Property | Type | Default | Beschreibung | |----------|------|---------|--------------| | `title` | string | — | Dialog-Titel | | `message` | string | — | Beschreibungstext | | `confirmLabel` | string | `"Bestätigen"` | Label des Bestätigen-Buttons (frei wählbar) | | `cancelLabel` | string | `"Abbrechen"` | Label des Abbrechen-Buttons (frei wählbar) | | `destructive` | boolean | `false` | Roter Confirm-Button + Warn-Icon; `false` = blaues Info-Icon | **Verhalten:** - Bei Bestätigung: Die Aktion wird normal ausgeführt - Bei Abbruch: Die Aktion wird übersprungen, keine Seiteneffekte - Funktioniert mit ALLEN Action-Typen (`update`, `delete`, `popup`, `create`, etc.) **Limitation:** Bei `actions`-Arrays mit mehreren Einträgen `confirm` auf eine einzelne Action setzen (typischerweise die erste oder einzige destruktive Action). Jede Action im Array wird unabhängig bestätigt. #### Verkettete Actions (`then`) Actions können mit der `then`-Property Folge-Aktionen nach Abschluss ausführen. Nützlich, wenn z.B. nach einem Löschen der UI-State zurückgesetzt werden soll (z.B. Detail-Ansicht schließen). ```javascript // Datensatz löschen, dann Auswahl zurücksetzen actions: [{ type: "delete", recordId: selectedItem.Nr, confirm: { title: "Datensatz löschen", message: "Wirklich löschen?", confirmLabel: "Löschen", cancelLabel: "Abbrechen", destructive: true }, then: [{ type: "update", recordId: Nr, field: mainContentStateField, value: formatJSON({ selectedId: "" }) }] }] ``` - `then` muss ein Array von Action-Objekten sein - Wird sequenziell nach Abschluss der Haupt-Aktion ausgeführt - Funktioniert mit `confirm` – `then` läuft nur nach Bestätigung und erfolgreicher Haupt-Aktion #### URLs und Telefonnummern öffnen (Mobile App Kompatibilität) ⚠️ **Problem**: `window.open()` via `customJS` funktioniert im Web, aber **nicht in Ninox Mobile Apps** ✅ **Lösung**: Ninox Helper-Feld mit `openUrl()` Trigger verwenden **Pattern**: 1. Erstelle ein Text-Feld (z.B. `trigger_openUrl`) mit folgendem Trigger: ```ninox openURL(trigger_openUrl); trigger_openUrl := null ``` 2. Nutze `update` Action statt `customJS`: ```javascript // ❌ Falsch: Funktioniert nur im Web actions: [{ type: "customJS", value: "window.open('tel:+49123456789')" }] // ✅ Richtig: Funktioniert Web + Mobile App actions: [{ type: "update", recordId: current.Nr, field: fieldId(current.Nr, "trigger_openUrl"), // Aktuell: field, später: fieldId value: "tel:+49123456789" // oder "https://..." für Links }] ``` **Beispiele**: ```javascript // Telefonnummer actions: [{ type: "update", recordId: current.Nr, field: fieldId(current.Nr, "trigger_openUrl"), value: "tel:" + phoneNumber }] // Google Maps actions: [{ type: "update", recordId: current.Nr, field: fieldId(current.Nr, "trigger_openUrl"), value: "https://www.google.com/maps/search/?api=1&query=" + urlEncode(address) }] // Externe Website actions: [{ type: "update", recordId: current.Nr, field: fieldId(current.Nr, "trigger_openUrl"), value: "https://example.com" }] ``` > ⚠️ **Hinweis**: Aktuell muss `field` statt `fieldId` verwendet werden. Dies wird in zukünftigen Widget-Versionen vereinheitlicht. **Vorteile**: - ✅ Funktioniert in Web **und** Mobile App - ✅ Zentralisierte URL-Logik (ein Trigger für alle URL-Typen) - ✅ Keine `customJS` Security-Einschränkungen * **Generic Slots**: Für flexible Komponenten `actionsLeft` / `actionsRight` statt nur `actions` verwenden, wenn das Layout es erfordert. * *Feature Request*: Vereinheitlichung von `field` und `fieldId` Parameter über alle Widgets hinweg. ### Formatierung (format-Funktion) Ninox bietet die `format()`-Funktion zum Formatieren von Zahlen, Datum und Zeit. Die Funktion verwendet spezielle Format-Token (ähnlich Moment.js). **Referenz**: [Ninox format() Dokumentation](https://forum.ninox.com/t/p8yzv31/format) #### Datumsformatierung ```javascript // ❌ Falsch: Ungültige Format-Tokens format(myDate, "EEEE, dd.MM.yyyy") // Produziert "2222, Di.12.2025" // ✅ Richtig: Korrekte Ninox Format-Tokens format(myDate, "dddd, DD.MM.YYYY") // "Dienstag, 01.12.2025" // ✅ Alternative: text() für Standard-Formatierung text(myDate) // Nutzt System-Locale // ✅ Wochentag extrahieren weekdayName(weekday(myDate)) // "Dienstag" substr(weekdayName(weekday(myDate)), 0, 2) // "Di" ``` **Format-Tokens für Datum:** - `dddd` - Vollständiger Wochentag (Monday, Dienstag) - `ddd` - Abgekürzter Wochentag (Mon, Di) - `DD` - Tag mit führender Null (01-31) - `MM` - Monat mit führender Null (01-12) - `YYYY` - Jahr 4-stellig (2025) - `Do` - Tag mit Ordnungszahl (1st, 2nd, 3rd) - `MMMM` - Vollständiger Monatsname (January, Dezember) **Format-Tokens für Zeit:** - `HH` - Stunden 24h-Format mit führender Null (00-23) - `mm` - Minuten mit führender Null (00-59) - `ss` - Sekunden mit führender Null (00-59) #### Zahlenformatierung ```javascript // Format: format(number, "prefix#thousands.decimals suffix#thousands_sep#decimal_sep") format(2385.97, "#,##0.00 €#,#0.0") // "2.385,97 €" (DE) format(2385.97, "$#,.#0,0") // "$2,385.97" (US) format(12345, "0000000000") // "0000012345" (Leading zeros) ``` ## 4. Design Patterns ### Nested Layouts Um komplexe Designs zu erreichen, werden `arcCustomLayout` Widgets tief verschachtelt. * Level 1: Container (Vertical) * Level 2: Row (Horizontal) * Level 3: Item (Content) ### Generic Props Wenn Funktionen erstellt werden, sollten die Parameter generisch benannt sein, um Wiederverwendbarkeit zu sichern. * **Inhalt**: Meistens `value` für den anzuzeigenden Text/Inhalt (z.B. bei `arcCustomText`, `arcCustomBadge`). * *Ausnahme*: `arcCustomButton` nutzt `title`. * **Benennung**: * `title` statt `sender` (für generische Karten-Header) * `subtitle` statt `company` * `meta` statt `timestamp` * `action` statt `clickFunction` ## 5. Layout & Dimensionen ### Fullscreen-Konfiguration Für Dashboard-Layouts, die ohne Ninox-UI-Elemente angezeigt werden sollen: * ✅ `embedded: false` (kein eingebettetes Widget) * ✅ `fullscreen: true` * ✅ `fullscreenMode: if isAdminMode() then "" else "full" end` (im Nicht-Admin-Modus wird die Ninox-UI ausgeblendet) * ✅ `height: "100%"` für den äußersten Container (nicht `auto`) ### Width & Height nach Orientierung Orientierung bestimmt, welche Dimension flexibel sein sollte: * **Vertical (`direction: "vertical"`)**: `width: "100%"` (Container nimmt volle Breite), `height: "auto"` (passt sich Inhalt an) oder `"100%"` für Fullscreen * **Horizontal (`direction: "horizontal"`)**: `height: "auto"` oder feste Höhe, `width` kann `"100%"`, `"auto"`, oder `"fraction"` sein ### Scrollbare Content-Bereiche Um Navigation/Header oben sichtbar zu halten, während nur der Content scrollt: * Erstelle einen Wrapper mit `height: "fraction"` (nimmt verbleibenden Platz) * Setze `scrollSettings: { scrollY: true, scrollX: false }` * Setze `alignY: "top"` für die Ausrichtung ```javascript // Navigation (height: auto) + Content Wrapper (height: fraction + scrollY: true) blocks: [ { /* Header/Nav */ height: "auto" }, { /* Scrollable Content */ height: "fraction", value: arcCustomLayout({ scrollSettings: { scrollY: true } }) } ] ``` ### CSS Positioning & Layering #### Position Sticky Für Sticky-Header, die beim Scrollen oben kleben bleiben: * ✅ **Styles im Block**: `styles: "position: sticky; top: 0px; z-index: 10; background-color: #F9FAFE;"` * ✅ **Overflow anpassen**: Parent-Container darf NICHT `overflow: hidden` haben * Verwende `overflow-x: hidden` statt `overflow: hidden` wenn nötig * ✅ **Hintergrund setzen**: Ohne `background-color` scheint der Content durch * ✅ **Z-Index**: Für Layering über anderen Elementen (z.B. `z-index: 10`) ```javascript // ❌ Blockiert Sticky: arcCustomLayout({ styles: "overflow: hidden;", blocks: [{ styles: "position: sticky; top: 0px;" }] }) // ✅ Funktioniert: arcCustomLayout({ styles: "overflow-x: hidden;", // Nur horizontal blockieren blocks: [{ styles: "position: sticky; top: 0px; z-index: 10; background-color: #F9FAFE;" }] }) ``` **Wichtig:** `position: sticky` funktioniert nur, wenn zwischen dem sticky Element und dem scrollbaren Container KEIN `overflow: hidden` ist. ### Widget-spezifische Props #### arcCustomSelect * **recordId** & **field**: Pflichtfelder für Daten-Binding * **autoFocus deaktivieren**: Verhindert automatisches Tastatur-Öffnen (wichtig auf Tablets) ```javascript focusAction: { autoFocus: false } ``` #### arcCustomBadge * **Text-Farbe**: `fontColor:` (nicht `color:` oder `textColor:`) * **Border**: Badge hat per default einen sichtbaren Border. Für randlose Badges `borderColor` auf `transparent` oder gleiche Farbe wie `backgroundColor` setzen. ```javascript arcCustomBadge({ value: "Text", fontColor: "#FFFFFF", // ✅ Korrekt backgroundColor: "#3388FF", borderColor: "#3388FF" // ✅ Gleiche Farbe wie Background für unsichtbaren Border }) ``` #### arcCustomButton * **Icon-Only Buttons**: Für Buttons ohne Text nur mit Icon * Verwende `title: ""` (leerer String) für reine Icon-Buttons * Setze `icon` Parameter mit `arcCustomIcon` * Besser als verschachtelte Layouts, da hover-Actions und button states bereits integriert sind ```javascript // ✅ Sauber: Button mit Icon, ohne Text arcCustomButton({ uniqueId: "close-btn", title: "", // Leer für Icon-Only width: "30px", height: "30px", backgroundColor: "#EBEDF5", borderRadius: "50%", icon: arcCustomIcon({ name: "x", color: "#555555", size: "16px" }), actions: [{ type: "update", recordId: Nr, field: "...", value: null }] }) // ❌ Unnötig komplex: Layout-Wrapper mit Icon arcCustomLayout({ width: "30px", height: "30px", clickAction: { ... }, blocks: [{ value: arcCustomIcon({ ... }) }] }) ``` * **Actions im Widget**: Verwende `actions` Array im Button statt `clickAction` im Block-Wrapper * Nutzt Button-Features (hover, active, disabled states) * Ermöglicht zukünftige `hoverActions` Konfiguration * **Badge-System**: Buttons unterstützen Badges für Notifications/Counter * ✅ **Separate Properties**: `showBadge`, `badgeTitle`, `badgeColor`, `badgeBackground`, `badgeBorderColor`, `badgePosition`, `badgeGap` * ❌ **NICHT** ein verschachteltes `badge: { ... }` Objekt verwenden * `showBadge` ist ein boolean und kann konditional gesetzt werden ```javascript // ✅ Richtig: Separate Badge-Properties arcCustomButton({ uniqueId: "btn-kommentare", title: "Kommentare", showBadge: if hasNewComments then true else false end, badgeTitle: "3", badgeColor: "#FFFFFF", badgeBackground: "#FF3B30", badgeBorderColor: "#FFFFFF", badgePosition: "", // Standard-Position badgeGap: "6px", // Abstand zwischen Inhalt und Badge actions: [...] }) // ❌ Falsch: Badge als Objekt (funktioniert nicht) arcCustomButton({ uniqueId: "btn-kommentare", title: "Kommentare", badge: { value: "3", backgroundColor: "#FF3B30", fontColor: "#FFFFFF" } }) ``` **Conditional Badge Display Pattern**: ```javascript // Pattern: Badge nur bei bestimmter Tab-ID und wenn Content vorhanden showBadge: if tab.id = "kommentare" and tab.hasComment then true else false end, badgeTitle: text(commentCount), badgeColor: "#FFFFFF", badgeBackground: "#FF3B30" ``` #### arcCustomIcon * **Eingebautes Container-Handling**: Das Icon-Widget verwaltet seinen Container selbst * Nutze `containerSize` statt externe Layout-Wrapper für Icon-Größe * `size` Parameter steuert Icon-Größe innerhalb des Containers * Kein zusätzliches `arcCustomLayout` um Icons nötig * **Container-Styling**: Alle Container-Styles können direkt am Icon gesetzt werden * `backgroundColor`: Hintergrundfarbe des Icon-Containers * `borderRadius`: Abrundung des Containers (z.B. `"50%"` für Kreis) * `borderColor`: Rahmenfarbe des Containers * `borderSize`: Rahmenstärke des Containers ```javascript // ✅ Icon mit vollständigem Container-Styling arcCustomIcon({ name: "user", color: "#818599", // Icon-Farbe size: "30px", // Icon-Größe containerSize: "52px", // Container-Größe backgroundColor: "#EBEDF5", // Container-Hintergrund borderRadius: "50%", // Runder Container borderColor: "#DDD", // Container-Rahmen borderSize: "1px" // Rahmen-Stärke }) // ❌ Unnötig: Zusätzliches Layout für Styling arcCustomLayout({ width: "52px", height: "52px", backgroundColor: "#EBEDF5", styles: "border-radius: 50%; border: 1px solid #DDD;", blocks: [{ value: arcCustomIcon({ name: "user", color: "#818599", size: "30px" }) }] }) ``` * **Vorteil**: Ein Widget statt verschachtelter Layouts → weniger Code, bessere Performance #### arcCustomTable – Leerzustand * **Leerzustand via emptyTable**: Nutze `emptyTable` settings (title, value) statt die Tabelle mit `if hasItems then table else arcCustomLayout({ ... empty state ... }) end` zu umschließen. * Das Table-Widget zeigt leere Listen intern an; kein extra Layout für „Keine Daten“ nötig. #### Ninox Zeitfelder – Anzeige * **`text()` statt `format()` für Zeit**: Ninox kann falsche Zeiten (z.B. 00:00) anzeigen bei `format(timeField, "HH:mm")` oder Zeit-Arithmetik. Nutze `text(timeField)` für zuverlässige Anzeige. * **Felder**: `'Uhrzeit von'` und `'Uhrzeit bis'` für Zeitbereiche. ## 6. Code-Qualität & Best Practices ### Leserliche Variablennamen Verwende aussagekräftige Namen für Loop-Variablen, besonders bei verschachtelten Schleifen: ```javascript // ❌ Schlecht: for t in types do for i in items do // Welches "i"? Welches "t"? end end // ✅ Gut: for typeItem in types do for iconItem in icons do // Klar und verständlich end end ``` ### Type Coercion bei `any` Parametern ⚠️ **Problem**: Wenn Werte als `any` in Funktionsparametern übergeben werden, verlieren sie ihre Typinformation in Ninox. Dies führt zu Fehlern bei Operationen oder Funktionsaufrufen, die spezifische Typen erwarten. ✅ **Lösung**: Explizite Typ-Konvertierung mit `number()`, `date()`, `text()` etc. #### Zahlen-Operationen ```javascript // ❌ Falsch: Type-Loss bei any Parameter function calculateNextDay(data : any) do data.currentDate + 1 // Fehler: "Ungültiger Operator: any - number" end // ✅ Richtig: Explizite date() Konvertierung function calculateNextDay(data : any) do date(data.currentDate) + 1 // Funktioniert end ``` #### Datum-Formatierung ```javascript // ❌ Falsch: format() erwartet Date-Typ format(footerData.currentDate, "DD.MM.YYYY") // Fehler bei any-Typ // ✅ Richtig: date() Konvertierung vor format() format(date(footerData.currentDate), "DD.MM.YYYY") // Funktioniert // ✅ Auch für month(), year(), day() month(date(footerData.currentDate)) year(date(footerData.currentDate)) ``` #### Nested Object Properties ```javascript // ❌ Falsch: Verschachtelte any-Properties ohne Konvertierung let weekEnd := date(data.dataDates.firstWeekDay) + 6 // firstWeekDay verliert Typ // ✅ Richtig: Beide Werte konvertieren let firstDay := date(data.dataDates.firstWeekDay); let weekEnd := date(firstDay) + 6; // Explizite Konvertierung ``` **Häufige Konvertierungen**: - `number()` - Für Zahlen und Berechnungen - `date()` - Für Datum-Operationen - `text()` - Für String-Operationen - `time()` - Für Zeit-Werte ### Text-Konvertierung in Schleifen Werte aus Ninox-Objekten sollten mit `text()` konvertiert werden, besonders in verschachtelten Strukturen: ```javascript // ✅ Konsistente Konvertierung: for typeItem in cardData.types do { value: arcCustomBadge({ value: text(typeItem.value), fontColor: if typeItem.color then text(typeItem.color) else "#A114A8" end, backgroundColor: if typeItem.backgroundColor then text(typeItem.backgroundColor) else "#FDDEFF" end }) } end ``` **Grund:** In tiefen Verschachtelungen können Ninox-Werte ihren Typ verlieren. `text()` stellt sicher, dass Strings korrekt übergeben werden. #### ⚠️ **KRITISCH: Konsistente Datentypen in if/else Zweigen** Beide Zweige eines if/else müssen denselben Datentyp zurückgeben. Bei Vergleichen oder Kombinationen von JSON-Werten mit String-Literalen, die JSON-Werte mit `text()` wrappen. ```javascript // ❌ FALSCH: Vergleich von any-Typ mit String backgroundColor: if emp.backgroundColor then emp.backgroundColor else "#888888" end // Fehler: "any" Typ verglichen mit String-Literal // ✅ RICHTIG: JSON-Wert mit text() wrappen backgroundColor: if emp.backgroundColor then text(emp.backgroundColor) else "#888888" end // ❌ FALSCH: Typ-Mischung bei Farb-Properties fontColor: if record.Color then record.Color else "#000000" end // ✅ RICHTIG: Konsistente text() Konvertierung fontColor: if record.Color then text(record.Color) else "#000000" end ``` **Wann `text()` verwenden:** - Wenn ein JSON-Objekt-Property in einem if/else mit einem String-Fallback verwendet wird - Wenn JSON-Werte mit String-Literalen verkettet werden - Wenn JSON-Properties an Widget-Properties übergeben werden, die Strings erwarten **Faustregel:** Wenn der else-Zweig ein String-Literal wie `"#888888"` ist, muss der if-Zweig auch einen String via `text()` zurückgeben. ## 7. Array-Handling & Dynamic Content ### Array-Funktion vs. Layout-Blocks Ninox bietet die `array()`-Funktion zum Zusammenführen von Arrays, aber in Widget-Kontexten ist die Verwendung von verschachtelten Layouts oft sauberer und flexibler. #### ⚠️ **WICHTIG: array() ist meist unnötig** Blöcke können direkt nebeneinander gesetzt werden - `array()` ist nur nötig wenn tatsächlich Arrays kombiniert werden müssen. ```javascript // ❌ Unnötig: array() mit if-else blocks: array( if condition then [{ value: widget1 }] else [] end, for item in items do { value: widget2 } end ) // ✅ Besser: Direkt nebeneinander, kein array(), kein else blocks: [ if condition then { value: widget1 } end, for item in items do { value: widget2 } end ] ``` **Merke:** Ninox filtert `null`-Werte automatisch aus Arrays. Daher: - Kein `else []` nötig bei bedingten Blöcken - Kein `array()` nötig um Blöcke zu kombinieren - Einfach Blöcke/Schleifen kommasepariert auflisten #### ❌ Vermeiden: Verschachtelte array() Aufrufe ```javascript // Problematisch: Schwer zu lesen, Verschachtelungstiefe begrenzt blocks: array(typeBadges, array(teilnehmerBadges, durationBadges)) ``` #### ✅ Bevorzugt: Wrapper-Layouts mit direkten Blocks ```javascript // Sauber: Jede Badge-Gruppe hat eigenes Layout blocks: [ { value: arcCustomLayout({ uniqueId: "types-wrapper", direction: "horizontal", gap: "6px", blocks: typeBadges // Direkt einsetzen }) }, { value: arcCustomLayout({ uniqueId: "teilnehmer-wrapper", direction: "horizontal", gap: "4px", blocks: teilnehmerBadges // Direkt einsetzen }) }, if durationValue then { value: arcCustomBadge({ ... }) } end ] ``` **Vorteile:** - Bessere Lesbarkeit und Wartbarkeit - Individuelle Styling-Kontrolle pro Gruppe (Gap, Ausrichtung) - Keine Limit-Probleme bei `array()` (Ninox erlaubt nur 2-3 verschachtelte Arrays) - Leichteres Debugging **Wann array() nutzen:** - Wenn nur 2 einfache Arrays kombiniert werden müssen - Bei flachen Strukturen ohne weitere Styling-Anforderungen - Beispiel: `actionsLeft: array(typeBadges, teilnehmerBadges)` #### Array-Merge (Referenzen) – 3 oder mehr Arrays Die Ninox-Funktion `array()` akzeptiert **nur 2 Argumente**. Um 3 oder mehr Arrays zusammenzuführen, muss die Funktion mehrfach sequentiell aufgerufen werden. ```javascript // ✅ RICHTIG: Sequentielles array() für 3+ Arrays let listCombined := array(list1, list2); listCombined := array(listCombined, list3); ``` **Offizielle Ninox-Dokumentation:** „If you need to merge more than 2 arrays, just execute the function as often as needed.“ ([array() Function](https://docs.ninox.com/en/script/functions-overview/functions/array)) **concat() und join() sind KEINE Alternative** – sie liefern Strings, keine Arrays. Ein Workaround wie `split(concat(ar1) + ", " + concat(ar2), ",")` wandelt alle Werte in Text um und führt zu Typverlust (Zahlen werden zu Strings; bei Werten mit Komma im Text entstehen falsche Splits). Für echte Array-Merges immer `array()` verwenden. **Referenzen:** - [Ninox array()](https://docs.ninox.com/en/script/functions-overview/functions/array) – offizielle Dokumentation - [Forum: How can I add two arrays?](https://forum.ninox.com/t/x2hr89p) – concat/join liefern Strings - [Forum: Some User Defined Array Functions](https://forum.ninox.com/t/h7hr822) – aMerge-Workaround (textbasiert, nur für String-Arrays geeignet) **Code-Referenz im Projekt:** `projects/desktop-calendar-planning/desktop-calendar-planning.arctemplate` Zeilen 2439–2440 #### ⚠️ **KRITISCH: Multiple for-Schleifen in blocks** Ninox erlaubt KEINE zwei for-Schleifen direkt nebeneinander in einem `blocks`-Array. Jede for-Schleife braucht ein eigenes Wrapper-Layout. ```javascript // ❌ FEHLER: Zwei for-Schleifen direkt nebeneinander blocks: [ for sel in data.selects do { value: arcCustomSelect({...}) } end, for btn in data.buttons do { value: arcCustomButton({...}) } end ] // ✅ RICHTIG: Jede for-Schleife in eigenem Wrapper-Layout blocks: [{ width: "fraction", value: arcCustomLayout({ uniqueId: "selects-wrapper", direction: "horizontal", blocks: for sel in data.selects do { value: arcCustomSelect({...}) } end }) }, { width: "auto", value: arcCustomLayout({ uniqueId: "buttons-wrapper", direction: "horizontal", blocks: for btn in data.buttons do { value: arcCustomButton({...}) } end }) }] ``` **Grund:** Ninox interpretiert zwei for-Schleifen nacheinander als Syntaxfehler. Das Wrapper-Layout kapselt jede Schleife sauber ab. #### ⚠️ **KRITISCH: Sections mit unterschiedlichen Daten – for-Schleife, kein statisches Array** Bei mehreren Sections (z.B. "Heute", "Morgen", "Nächste 7 Tage"), wo jede Section eigene Listen mit **section-spezifischen Daten** hat (z.B. Terminkarten mit unterschiedlichen Teilnehmern pro Karte), **muss** eine for-Schleife über die Sections verwendet werden. ```javascript // ❌ FALSCH: Statisches Array – Daten einer Section können in andere "leaken" blocks: [ dashboardSection({ sectionId: "heute", appointments: sectionHeuteList }), dashboardSection({ sectionId: "morgen", appointments: sectionMorgenList }), ... ] // ✅ RICHTIG: for-Schleife stellt pro Section eigenen Auswertungskontext sicher blocks: for section in sections[visible = true and cnt(list) > 0] do { width: "100%", value: dashboardSection({ sectionId: section.sectionId, appointments: for item in section.list do { value: listCard({ participants: item.participants, ... }) } end }) } end ``` **Grund:** Ohne for-Schleife kann Ninox Referenzen falsch auflösen – z.B. Teilnehmer-Badges von "Heute"-Karten erscheinen in "Morgen"-Karten. Die for-Schleife erzeugt pro Section einen sauberen Auswertungskontext, sodass jede Karte die richtigen Daten aus ihrer eigenen Section/Liste erhält. #### ⚠️ **KRITISCH: if/else mit einzelnem Block vs. for-Schleife** Wenn ein if-Zweig einen einzelnen Block und der else-Zweig eine for-Schleife enthält, muss der einzelne Block in `[{...}]` (eckige Klammern) gesetzt werden. ```javascript // ❌ FEHLER: Unterschiedliche Typen (Object vs. Array) if isEmpty then { value: "Leer" } // Object else for item in items do { value: item } end // Array end // ✅ RICHTIG: Beide als Array if isEmpty then [{ value: "Leer" }] // Array mit einem Element else for item in items do { value: item } end // Array end ``` **Grund:** Ninox erwartet bei if/else gleiche Typen. Die for-Schleife gibt ein Array zurück, also muss der einzelne Block auch als Array `[{...}]` definiert werden. #### ⚠️ **KRITISCH: if direkt in Arrays OHNE extra `{`** Bei bedingten Blöcken innerhalb von `blocks: [...]` darf das `if` NICHT in ein extra `{ }` gewrappt werden. ```javascript // ❌ FALSCH: Extra { vor if blocks: [{ width: "100%", value: "immer sichtbar" }, { // ← Dieses extra { ist FALSCH! if condition then { width: "100%", value: "bedingt" } end } // ← Und dieses schließende } ist auch FALSCH! ] // ✅ RICHTIG: if direkt im Array, kein extra Wrapper blocks: [{ width: "100%", value: "immer sichtbar" }, if condition then { width: "100%", value: "bedingt" } end ] ``` **Grund:** Das `if...then...end` selbst gibt das Block-Objekt zurück (oder null). Ein Wrapping in `{ }` erzeugt eine ungültige Objekt-Struktur, die Ninox nicht parsen kann. ### Bedingte Blöcke - else-Zweig oft unnötig Widget-`blocks`-Arrays ignorieren automatisch `null`/leere Werte. Daher sind leere `else`-Blöcke meist unnötig. #### ❌ Verbose: Leere else-Blöcke ```javascript blocks: [ if condition then { /* ... */ } else { width: "0px", height: "0px", value: "" } end ] ``` #### ✅ Clean: Ohne else ```javascript blocks: [ if condition then { /* ... */ } end ] ``` **Ninox filtert automatisch:** - `null` Werte - Leere Arrays `[]` - Undefined/falsy Ergebnisse ### Dynamisch vs. Statisch Vermeide hardcoded, starre Lösungen. Nutze immer Schleifen und Arrays, um flexibel auf Datenstrukturen zu reagieren. #### ❌ Statisch: Nur erste 2 Elemente ```javascript blocks: [ if count(teilnehmer) > 0 then { value: arcCustomBadge({ value: item(teilnehmer, 0).value }) } end, if count(teilnehmer) > 1 then { value: arcCustomBadge({ value: item(teilnehmer, 1).value }) } end ] ``` #### ✅ Dynamisch: Alle Elemente ```javascript // Vorbereitung (Top-Level oder in Funktion): let teilnehmerBadges := if teilnehmer then for t in teilnehmer do { width: "auto", height: "auto", value: arcCustomBadge({ uniqueId: "badge-" + t.value, value: t.value, fontColor: if t.color then text(t.color) else "#FFFFFF" end, backgroundColor: if t.backgroundColor then text(t.backgroundColor) else "#3388FF" end }) } end else [] end; // Verwendung im Layout: blocks: [ { value: arcCustomLayout({ uniqueId: "teilnehmer-wrapper", blocks: teilnehmerBadges // Dynamisch, alle Elemente }) } ] ``` **Vorteile der dynamischen Lösung:** - Funktioniert mit beliebig vielen Elementen (1, 5, 100...) - Keine Code-Duplizierung - Änderungen am Badge-Styling nur an einer Stelle - Wiederverwendbar (Variable kann mehrfach genutzt werden) ### Pattern: Wiederverwendbare Badge-Arrays Definiere Badge-Arrays als Variablen, um sie in mehreren Layouts zu verwenden (z.B. Card-Footer UND Popover-Header): ```javascript function myComponent(data : any) do // Einmal definieren let typeBadges := if data.types then for typeItem in data.types do { width: "auto", height: "auto", value: arcCustomBadge({ uniqueId: data.id + "-type-" + typeItem.value, value: text(typeItem.value), fontColor: if typeItem.color then text(typeItem.color) else "#A114A8" end, backgroundColor: if typeItem.backgroundColor then text(typeItem.backgroundColor) else "#FDDEFF" end }) } end else [] end; // Mehrfach verwenden arcCustomLayout({ blocks: [ { value: arcCustomLayout({ blocks: typeBadges }) }, // In Card { value: arcCustomLayout({ blocks: typeBadges }) } // In Popover ] }) end ``` ## 8. UI Patterns & Component Architecture ### Tab-basierte Navigation Für komplexe UI-Komponenten mit mehreren Content-Bereichen (z.B. Popovers, Modals): #### Tab-Konfiguration ```javascript tabs: [{ id: "termin", title: "Termin", hasComment: false, // Flag für Badge-Display contentBlock: arcCustomLayout({ uniqueId: "tab-termin-content", height: "100%", scrollSettings: { scrollY: true }, blocks: [/* Content */] }) }, { id: "kommentare", title: "Kommentare", hasComment: currentRecord.CommentField != null and text(currentRecord.CommentField) != "", contentBlock: arcCustomLayout({ uniqueId: "tab-kommentare-content", height: "100%", scrollSettings: { scrollY: true }, blocks: [/* Content */] }) }] ``` #### Tab-Navigation Rendering ```javascript // Tab-Buttons mit Badge-Support blocks: for tab in popoverData.tabs do { width: "auto", height: "auto", value: arcCustomButton({ uniqueId: popoverData.id + "-tab-" + tab.id, title: tab.title, backgroundColor: if popoverData.activeTab = tab.id then "#333333" else "#EBEDF5" end, fontColor: if popoverData.activeTab = tab.id then "#FFFFFF" else "#818599" end, showBadge: if tab.id = "kommentare" and tab.hasComment then true else false end, badgeTitle: "1", badgeColor: "#FFFFFF", badgeBackground: "#FF3B30", actions: [{ type: "update", recordId: Nr, field: popoverData.tabActionField, value: tab.id }] }) } end ``` #### Dynamic Tab Content Display ```javascript // Content-Bereich: Dynamisches Rendering basierend auf activeTab { width: "100%", height: "fraction", // Nimmt verfügbaren Platz value: let activeTabData := first(for tab in popoverData.tabs do if tab.id = popoverData.activeTab then tab end end); if activeTabData and activeTabData.contentBlock then html(activeTabData.contentBlock) // ⚠️ html() Wrapper erforderlich! else arcCustomLayout({ uniqueId: popoverData.id + "-empty-content", blocks: [{ value: arcCustomText({ value: "Kein Content verfügbar.", fontColor: "#999999" }) }] }) end } ``` **Wichtig**: `html()` Wrapper ist erforderlich, wenn `contentBlock` als `any` Type übergeben wird, um Type-Kompatibilität zwischen Widget-Return und erwartetem Layout zu gewährleisten. ### Footer-Struktur in Popovers/Modals Footer sollten **NICHT** Teil des scrollbaren Tab-Contents sein, sondern als separater Block am Ende positioniert werden. #### ❌ Problematisch: Footer im Content ```javascript contentBlock: arcCustomLayout({ blocks: [ { /* Content */ }, { /* Footer Buttons */ } // Scrollt mit → schlecht ] }) ``` #### ✅ Richtig: Footer als separates Property ```javascript // 1. Footer als separates Property im Data-Objekt { tabs: [...], footerBlock: arcCustomLayout({ uniqueId: "popover-footer", height: "auto", // Footer passt sich Inhalt an gap: "10px", paddingY: "10px", blocks: [ if phoneNumber then { value: arcCustomButton({ title: "Anrufen", ... }) } end, if mapsUrl then { value: arcCustomButton({ title: "In Google Maps öffnen", ... }) } end ] }) } ``` ```javascript // 2. Footer im Popover-Layout (nach Content) blocks: [ { /* Header */ }, { /* Tabs */ }, { width: "100%", height: "fraction", // Content scrollbar value: /* Tab Content */ }, if popoverData.footerBlock then { width: "100%", height: "auto", // Footer bleibt unten value: html(popoverData.footerBlock) } end ] ``` #### Layout-Struktur mit Footer ``` ┌─────────────────────────────┐ │ Popover Header (auto) │ ├─────────────────────────────┤ │ Tabs Navigation (auto) │ ├─────────────────────────────┤ │ Tab Content (fraction) │ ← scrollY: true │ - Scrollbarer Bereich │ │ - height: "fraction" │ ├─────────────────────────────┤ │ Footer (auto) │ ← Immer sichtbar │ [Action Button 1] │ │ [Action Button 2] │ └─────────────────────────────┘ ``` **Vorteile:** - Footer bleibt immer sichtbar (kein Scrollen nötig) - Klare Trennung zwischen Content und Actions - Bessere UX für Action-Buttons (z.B. "Anrufen", "In Maps öffnen") - Unabhängiges Styling für Footer-Bereich **Pattern-Zusammenfassung:** 1. Content-Bereich: `height: "fraction"` + `scrollY: true` 2. Footer-Bereich: `height: "auto"` + außerhalb des scrollbaren Content 3. Footer als `footerBlock` Property im Data-Objekt definieren 4. Footer mit `html(popoverData.footerBlock)` rendern ### Pagination für Listen/Kanban Bei großen Datenmengen (z.B. Inbox mit vielen Mails) kann das Widget crashen. Lösung: `arcCustomPagination` mit `slice()`. #### Pagination-Variablen oben definieren ```javascript let maxEntriesPerPage := 20; // Pro Liste/Swimlane let listAll := (select Items where ...); let totalCards := cnt(listAll); let currentPage := if page_field then number(page_field) else 1 end; let totalPages := ceil(totalCards / maxEntriesPerPage); let listPaginated := slice(listAll, maxEntriesPerPage * (currentPage - 1), maxEntriesPerPage * currentPage); ``` #### Pagination in Config-Objekt ```javascript let swimlaneConfig := [{ id: "inbox", title: "Inbox", showPagination: totalCards > maxEntriesPerPage, // Nur wenn nötig pagination: { currentPage: currentPage, totalPages: totalPages, totalCards: totalCards, fieldId: fieldId(Nr, "page_inbox") // Number-Feld für aktuelle Seite }, sections: [{ title: "Items", cards: listPaginated.[{...}] // Paginierte Liste verwenden }] }]; ``` #### Pagination Widget am Ende der Swimlane ```javascript if swimlane.showPagination then { width: "100%", height: "auto", alignX: "center", value: arcCustomPagination({ uniqueId: "pagination-" + text(swimlane.id), editable: true, recordId: Nr, fieldId: text(swimlane.pagination.fieldId), direction: "horizontal", title: swimlane.pagination.currentPage, value: swimlane.pagination.currentPage, total: swimlane.pagination.totalPages, totalPrefix: " von " }) } end ``` **Wichtig:** - `showPagination` als boolean für konditionale Anzeige - `slice(list, start, end)` für die paginierte Teilmenge - Separates Number-Feld pro Swimlane/Liste (z.B. `page_inbox`, `page_bearbeiten`) - `editable: true` erlaubt direkte Seiteneingabe #### Pagination bei Filterwechsel zurücksetzen ⚠️ **Problem:** Wenn ein Filter gewechselt wird (z.B. Postfach), kann die aktuelle Seite höher sein als die neue Gesamtseitenzahl → leere Anzeige. **Lösung:** Im Ninox Trigger "Nach Änderung" des Filterfelds die Seite zurücksetzen: ```ninox // Trigger: Nach Änderung von "selectedPostfach" 'Aktuelle Seite' := 1 ``` **Gilt für alle Filter die die Listengröße beeinflussen:** - Postfach/Mailbox Auswahl - Status Filter - Kategorie Filter - Bearbeiter Filter ### Empty States für Listen/Swimlanes Wenn eine Liste/Swimlane leer ist, sollte ein Fallback-Text angezeigt werden. #### Pattern: Bedingte Section-Anzeige + Empty State ⚠️ **WICHTIG:** Bei if/else mit unterschiedlichen Block-Typen (einzelner Block vs. for-Schleife) muss der einzelne Block in `[{...}]` (eckige Klammern) gesetzt werden, damit Ninox beide als Arrays erkennt. ```javascript // Config: emptyMessage pro Swimlane emptyMessage: "Keine Einträge im Posteingang", // Rendering: if/else Pattern (NICHT if...end, for...end nebeneinander!) let totalSectionCards := sum(for sec in swimlane.sections do count(sec.cards) end); if totalSectionCards = 0 then [{ // ← Eckige Klammern um einzelnen Block! width: "100%", height: "100%", value: arcCustomLayout({ uniqueId: "empty-" + swimlaneId, alignX: "center", alignY: "center", // ← Für vertikale Zentrierung braucht man Layout! width: "100%", height: "100%", blocks: [{ value: arcCustomText({ value: text(swimlane.emptyMessage), fontColor: "#999999" }) }] }) }] else for section in swimlane.sections do if count(section.cards) > 0 then { /* Section mit Title und Cards */ } end end end // ← Ein end für das gesamte if/else ``` **Warum `[{...}]`?** Ninox erwartet bei if/else gleiche Typen. Die for-Schleife gibt ein Array zurück, also muss der einzelne Block auch als Array `[{...}]` definiert werden. **Warum Layout für vertikale Zentrierung?** Blocks haben nur `alignX` für horizontale Ausrichtung. Für `alignY` (vertikale Zentrierung) muss ein `arcCustomLayout` Wrapper verwendet werden. ### Reset-Button in Select ausblenden Manche Selects haben "Alle" bereits als Record-Option und dürfen nicht auf null zurückgesetzt werden. ```javascript // Config: hideReset pro Filter filters: [{ placeholder: "Posteingang", hideReset: true, // Kein Reset-Button items: [...] }, { placeholder: "Status", hideReset: false, // Reset-Button "Alle" anzeigen items: [...] }] // Rendering: reset.hide aus Config lesen reset: { title: "Alle", value: "", hide: if filter.hideReset then true else false end } ``` ## 9. Responsive Design: isMobile Pattern mit ninoxApp() ### Plattform-Erkennung Verwende `ninoxApp()` um zwischen Mobile und Desktop/Tablet zu unterscheiden: ```javascript let isMobile := ninoxApp() = "iphone" or ninoxApp() = "android"; ``` Rückgabewerte von `ninoxApp()`: `"web"`, `"mac"`, `"iphone"`, `"ipad"`, `"android"`, `"tab"`, `"server"`. ### Desktop Layout Shell Auf Desktop/Tablet: Zweispaltiges Layout: - **Linke Sidebar** (fixe Breite, z.B. `370px`): Mobile-Ansichten als Sidebar adaptiert - **Rechter Content-Bereich** (`width: "fraction"`): Desktop-spezifischer Content mit Header (fixe Höhe), Content (fraction), Footer (auto) ### isMobile Parameter in Funktionen `isMobile` über das Data-Objekt an alle Funktionen weiterreichen, die plattformspezifische Anpassungen benötigen: ```javascript --- In Funktion: Styles bedingt setzen --- styles: if data.isMobile then "overflow-x: hidden; max-width: 400px; min-height: 700px;position:relative;" else "overflow-x: hidden; min-height: 700px;position:relative;" end, ``` ### Views-Architektur (Einheitliche Navigation) Zwei-Ebenen-Navigationssystem: 1. **`activeView`** (aus State abgeleitet): Bestimmt den Top-Level-Kontext (dashboard, ticket, customer) 2. **`activeTab`** (aus `helper_activeTab`): Wählt den Tab innerhalb der aktuellen View Alle Navigationsaktionen schreiben in ein einziges Feld `helper_activeTab` über `tabActionField`. ```javascript --- activeView wird aus globalem State abgeleitet, nicht in einem Feld gespeichert --- let activeView := if helper_currentCustomer then "customer" else if currentTicket then "ticket" else "dashboard" end end; --- Alle Tabs nutzen das gleiche Action-Feld --- let tabActionField := fieldId(Nr, "helper_activeTab"); let activeTabValue := if helper_activeTab then helper_activeTab else "" end; ``` ### View-Funktionen mit Lazy Content Loading **Kritisch für Performance**: Schweren Content in View-Funktionen mit `if data.active then ... end` Guards wrappen. Das verhindert, dass Ninox teure Widget-Berechnungen für inaktive Views auswertet (da `let`-Variablen in Ninox eager evaluiert werden). ```javascript function viewTicket(data : any) do if data.active then let activeTab := if data.activeTab then data.activeTab else "ticket" end; { desktop: { sidebar: { content: heavyWidget(), footer: null }, main: { content: mainWidget(), header: null, footer: footerWidget(), contentSidebar: null } }, mobile: { main: mobileWidget(), footer: null, header: null } } end end; ``` **Warum Funktionen statt Inline-JSON?**: Wenn man `let myWidget := expensiveWidget();` auf Top-Level setzt und es im JSON mit `if activeView = "ticket" then myWidget end` referenziert, evaluiert Ninox `myWidget` trotzdem. Durch das Wrappen in eine Funktion mit `data.active`-Check wird die teure Berechnung nur bei aktiver View ausgelöst. ### Views-Konfiguration Views als Array von Config-Objekten organisieren, jedes mit eigenen `navTabs` und lazy-geladenem `content`: ```javascript let views := [{ id: "dashboard", active: activeView = "dashboard", defaultTab: "cockpit", navTabs: [{ id: "cockpit", title: "Cockpit", visible: true, active: ..., actionField: tabActionField }, ...], content: viewDashboard({ active: activeView = "dashboard", activeTab: activeTabValue, ... }) }, { id: "ticket", active: activeView = "ticket", navTabs: [{ id: "ticket", title: "Ticket", visible: true, active: ..., actionField: tabActionField }, ...], content: viewTicket({ active: activeView = "ticket", activeTab: activeTabValue, ... }) }]; ``` ### Generisches Rendering aus Views-Config Aktuelle View auflösen und plattformspezifischen Content rendern: ```javascript let currentView := first(for v in views do if v.active then v end end); let currentNavTabs := if currentView then currentView.navTabs else [] end; let viewContent := if currentView then currentView.content else null end; if isMobile then if viewContent and viewContent.mobile and viewContent.mobile.main then html(viewContent.mobile.main) end else desktopLayout({ sidebarContent: viewContent.desktop.sidebar.content, rightHeader: desktopTicketHeader({ tabs: currentNavTabs, tabRecordId: Nr, ... }), rightContent: viewContent.desktop.main.content, rightFooter: viewContent.desktop.main.footer }) end ``` Jede View-Funktion gibt sowohl `desktop`- als auch `mobile`-Content-Strukturen zurück. Der generische Renderer wählt die richtige basierend auf `isMobile`. ## 10. Helper-Feld Namenskonvention Konsistentes Namensschema für Helper-Felder: | Pattern | Beispiel | Verwendung | |---------|----------|------------| | `helper_current{Entity}` | `helper_currentCustomer` | Aktuell ausgewählter/aktiver Datensatz (ID) | | `helper_active{Feature}` | `helper_activeView` | Aktiver Tab/View/Modus | | `helper_selected{Entity}` | `helper_selectedTicket` | Ausgewähltes Element für Detail/Popover | | `trigger_{action}` | `trigger_delete` | Boolean-Trigger-Felder für Ninox-Aktionen | Plattform-spezifische Präfixe wie `desktop_` oder `mobile_` sowie kontext-spezifische wie `popover_` oder `filter_` in Feldnamen vermeiden. Generische Namen verwenden, damit dasselbe Feld plattformübergreifend funktioniert. ### Refactoring-Learnings (Detail-/List-Views) **Skeleton-first-Architektur**: Layout-Shell (panelLayout, fullLayout) zuerst mit Platzhaltern bauen, dann reale Komponenten schrittweise integrieren. Vermeidet "alter Code läuft weiter" und ermöglicht sauberes Testen. **Shared Data Objects**: Ein einziges `ticketData` (oder `currentTicket`) Objekt wenn ein Ticket ausgewählt ist. Alle Sub-Views (Details, Notizen, Kommentare, Historie) konsumieren daraus. Einmal berechnen, überall wiederverwenden. **View-Level-Variablen-Scoping**: Variablen wie `ticketHeader`, `ticketSidebarContent`, `ticketFooter` innerhalb von `if activeView = "ticket" then ... end` werden nur bei aktiver View evaluiert. Verhindert eager evaluation schwerer Widgets für inaktive Views. **Generische Komponenten-Namen**: `ticketSidebarHeader` statt `mobileTicketHeader` oder `popoverHeader`. Dieselbe Komponente dient Desktop-Sidebar und Mobile-Panel; später für andere Entitäten wiederverwendbar. **Array-Projektion für Sections**: `list[visible = true and count(list) > 0].[{ block }]` direkt in blocks für effiziente Iteration. Query innerhalb `if hasSearchFilter then ... else [] end` für Performance, wenn eine "Später"-Section nur bei Suche relevant ist. **Select-Items-Syntax**: Für arcCustomSelect `(select Table).[{ title: ..., active: ..., value: ... }]` verwenden, nicht `for m in select ... do { id, title } end`. Das `active`-Flag prüft `contains(numbers(current.Field), number(Nr))` bei Multi-Select. **arcCustomSelect currentValue bei Verknüpfungsfeldern**: Wenn das Select-Feld eine Relation ist, muss `currentValue` das Anzeigefeld der verknüpften Tabelle nutzen, nicht `text(record.Relation)`. Beispiel: `currentValue: if record.Kategorie then text(record.Kategorie.Titel) else "" end`. **Conditional Tab `main` für Detail-Views**: Wenn ein Tab sowohl Listen- als auch Detail-Ansicht hat, die `main`-Property conditional machen: `main: if detailActive then { showNav: false, headerLeft: backBtn, headerRight: actionBtn, content: detailContent, contentRight: detailSidebar, footer: detailFooter } else { showNav: true, content: listContent, ... } end`. Nutzt die `desktopMainLayout`-Slots (headerLeft, headerRight, contentRight, footer) statt alles in eine einzige Content-Funktion zu packen. **Detail-View-Rendering auf Tab-Ebene, nicht in Content-Funktionen**: Nicht innerhalb von `untersuchungenContent()` zwischen Liste und Detail branchen. Content-Funktionen auf eine Aufgabe fokussieren (Liste ODER Detail) und die Tab-Definition entscheiden lassen, was gerendert wird. Ermöglicht korrekte Nutzung von Sidebar/Footer/Header-Slots. ## 11. Dokumentation für LLMs Wenn Code generiert wird, muss er: 1. Kompilierbar in Ninox sein (kein echtes JS). 2. Die Widget-Referenz strikt befolgen.