diff --git a/.claude/instructions.md b/.claude/instructions.md new file mode 100644 index 0000000..7e472a2 --- /dev/null +++ b/.claude/instructions.md @@ -0,0 +1,14 @@ +# RustyCMS – Hinweise für die AI + +Vollständiger Kontext steht in **CLAUDE.md** im Repo-Root (bzw. `.cursor/rules/project.mdc`). + +## Erklärungen beim Implementieren + +Beim Umsetzen von Aufgaben **kurz und nachvollziehbar erklären**: + +1. **Ziel und Vorgehen**: Am Anfang in 1–2 Sätzen: **was** wird umgesetzt und **wie** (welche Dateien, welches Muster). +2. **Schritte sichtbar machen**: Bei mehreren Änderungen kurz benennen („Als Nächstes: …“, „Dann: …“). +3. **Ungewöhnliche Entscheidungen begründen**: Abweichungen von Konventionen kurz **warum**. +4. **Am Ende zusammenfassen**: 2–4 Sätze: Was wurde geändert, wo es liegt, was der Nutzer prüfen kann. + +Erklärungen knapp halten, aber so, dass Gedankengang und Änderungen nachvollziehbar sind. diff --git a/CLAUDE.md b/CLAUDE.md index 0974454..e109126 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,19 @@ --- +## Arbeitsweise: Erklärungen beim Implementieren + +Beim Umsetzen von Aufgaben soll die AI **kurz und nachvollziehbar erklären, was sie tut**: + +1. **Ziel und Vorgehen**: Zu Beginn (oder im ersten Schritt) in 1–2 Sätzen sagen, **was** umgesetzt wird und **wie** (welche Dateien, welches Muster). +2. **Schritte sichtbar machen**: Bei mehreren Änderungen z. B. kurz benennen: „Als Nächstes: …“, „Dann: …“, damit der Ablauf klar ist. +3. **Ungewöhnliche Entscheidungen begründen**: Wenn etwas von Konventionen abweicht (andere Lib, anderer Ordner, Workaround), **warum** (z. B. „Damit Tailwind-Klassen zuverlässig greifen“). +4. **Am Ende zusammenfassen**: In 2–4 Sätzen: Was wurde geändert, wo es liegt, was der Nutzer prüfen kann. + +Die Erklärungen sollen **knapp** bleiben, aber so formuliert sein, dass jemand den Gedankengang und die Änderungen nachvollziehen kann, ohne alles selbst zu lesen. + +--- + ## Projektstruktur ``` diff --git a/README.md b/README.md index de4441b..308eb6f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A file-based headless CMS written in Rust. Content types are defined as JSON5 sc - **Optional SQLite**: Switch store via env – same API, different storage layer - **REST API**: Full CRUD endpoints, auto-generated per content type - **References & _resolve**: Reference fields as `{ _type, _slug }`; embed with `?_resolve=all` +- **Referrer index**: Reverse index of who references which entry; file-based in `content/_referrers.json`; full reindex when file is missing; `GET /api/content/:type/:slug/referrers` - **Reusable partials**: `reusable` schemas and `useFields` for shared field groups (e.g. layout) - **Optional API auth**: Protect write access (POST/PUT/DELETE) with an API key via env - **Admin UI**: Next.js web interface for browsing collections, creating and editing content; manage types (list, create, edit, delete) with schema files and JSON Schema export updated on save @@ -125,6 +126,10 @@ RUSTYCMS_STORE=sqlite cargo run RUSTYCMS_API_KEY=your-secret-token cargo run ``` +### Referrer index + +When not using `RUSTYCMS_ENVIRONMENTS`, the API maintains a **reverse index** of references: for each entry it knows which other entries reference it. This is stored as a single JSON file in the content directory: **`content/_referrers.json`**. On startup, if that file is missing, a full reindex is run over all collections and locales; on every create/update/delete the index is updated incrementally. Use **`GET /api/content/:type/:slug/referrers`** to list referrers (response: array of `{ collection, slug, field, locale? }`). Documented in Swagger UI and OpenAPI spec. + ## Project structure ``` @@ -259,6 +264,8 @@ Add a `.json5` file under `types/`: | `fields` | object | Nested fields for object type | | `collection`| string | Target collection for reference type | | `useFields` | string | For `type: "object"`: use fields from this schema (partial) | +| `widget` | string | For `string`: `"textarea"` = multi-line; `"code"` = code editor with syntax highlighting (requires `codeLanguage`) | +| `codeLanguage` | string | When `widget: "code"`: `"css"`, `"javascript"`, `"json"`, or `"html"` – used for highlighting and copy in admin UI | ### Field types @@ -427,6 +434,7 @@ curl -X POST http://localhost:3000/api/content/product \ | `DELETE` | `/api/schemas/:type` | Delete type definition (Admin UI: Delete type) | | `GET` | `/api/content/:type` | List all entries | | `GET` | `/api/content/:type/:slug` | Get single entry | +| `GET` | `/api/content/:type/:slug/referrers` | List entries that reference this entry | | `POST` | `/api/content/:type` | Create entry | | `PUT` | `/api/content/:type/:slug` | Update entry | | `DELETE` | `/api/content/:type/:slug` | Delete entry | diff --git a/admin-ui/messages/de.json b/admin-ui/messages/de.json index c5fd284..4b8356e 100644 --- a/admin-ui/messages/de.json +++ b/admin-ui/messages/de.json @@ -10,6 +10,7 @@ "dashboard": "Dashboard", "types": "Typen", "assets": "Assets", + "settings": "Einstellungen", "login": "Anmelden", "logout": "Abmelden", "searchPlaceholder": "Sammlungen suchen…", @@ -20,6 +21,7 @@ "noResults": "Keine Ergebnisse für \"{query}\"" }, "ContentForm": { + "copyCode": "Code kopieren", "slugRequired": "Slug ist erforderlich.", "slugInUse": "Slug bereits vergeben.", "slugMustStartWith": "Der Slug muss mit \"{prefix}\" beginnen.", @@ -36,6 +38,11 @@ "pleaseSelect": "— Bitte auswählen —", "removeEntry": "Entfernen", "addEntry": "+ Eintrag hinzufügen", + "arrayAddItem": "Eintrag hinzufügen", + "arrayRemoveItem": "Eintrag entfernen", + "arrayItemPlaceholder": "Wert", + "arrayItemPlaceholderNumber": "Zahl", + "arrayHint": "Liste von Werten. Einträge unten hinzufügen oder entfernen.", "keyPlaceholder": "Schlüssel", "valuePlaceholder": "Wert", "pickAsset": "Bild wählen", @@ -58,6 +65,7 @@ "typesLabel": "Typen: {collections}", "selectType": "\u2014 Typ w\u00e4hlen \u2014", "newEntry": "Neuer Eintrag", + "openEntry": "Eintrag \u00f6ffnen", "noCollection": "Keine Referenz-Collection im Schema. Setze {collectionCode} oder {collectionsCode} im Typ, oder starte die API und lade die Seite neu." }, "ReferenceArrayField": { @@ -72,9 +80,10 @@ "moveUp": "Nach oben", "moveDown": "Nach unten", "remove": "Entfernen", - "newComponent": "+ Neue {collection}-Komponente", + "newComponent": "Neue {collection}-Komponente", "createNewComponent": "+ Neue Komponente erstellen\u2026", "openInNewTab": "In neuem Tab \u00f6ffnen; dann Seite neu laden.", + "openEntry": "Eintrag \u00f6ffnen", "noCollection": "Keine Referenz-Collection im Schema. Setze {collectionCode} oder {collectionsCode} im Typ, oder starte die API und lade die Seite neu." }, "MarkdownEditor": { @@ -112,6 +121,9 @@ "editSchema": "Schema bearbeiten", "hidePreview": "Daten-Vorschau ausblenden", "showPreview": "Daten-Vorschau", + "sectionSchema": "Schema", + "sectionDataPreview": "Aktuelle Daten", + "copyCode": "Code kopieren", "loading": "Laden\u2026", "errorLoading": "Fehler beim Laden" }, @@ -127,6 +139,34 @@ "ContentLocaleSwitcher": { "label": "Inhaltssprache" }, + "Settings": { + "title": "Einstellungen", + "connection": "Verbindung", + "apiUrl": "API-URL", + "backendStatus": "Backend-Status", + "checking": "Prüfe…", + "apiReachable": "API erreichbar", + "apiUnreachable": "API nicht erreichbar", + "contentLocales": "Inhalts-Sprachen", + "default": "Standard", + "thisDevice": "Dieses Gerät", + "uiLanguage": "Oberflächen-Sprache", + "itemsPerPage": "Einträge pro Seite", + "itemsPerPageHint": "Gilt für Inhaltslisten.", + "refreshData": "Daten aktualisieren", + "refreshDataSuccess": "Daten aktualisiert.", + "clearSession": "Session bereinigen", + "clearSessionHint": "Entfernt API-Schlüssel und alle Admin-Voreinstellungen (z. B. auf geteilten Rechnern).", + "clearSessionConfirmTitle": "Session bereinigen?", + "clearSessionConfirmDescription": "Du wirst abgemeldet und alle gespeicherten Einstellungen (z. B. Einträge pro Seite) werden gelöscht. Anschließend kannst du dich wieder anmelden.", + "clearSessionConfirmAction": "Bereinigen", + "cancel": "Abbrechen", + "apiKeyStatus": "API-Schlüssel", + "apiKeyFromEnv": "Aus Umgebung (nur Lesen).", + "apiKeyManual": "Manuell gesetzt (diese Sitzung).", + "logout": "Abmelden", + "login": "Anmelden" + }, "Dashboard": { "title": "Dashboard", "subtitle": "W\u00e4hle eine Sammlung zur Inhaltsverwaltung.", @@ -141,6 +181,10 @@ "title": "Typen", "newType": "Neuer Typ", "description": "Inhaltstypen (Sammlungen). Schema bearbeiten oder Typ l\u00f6schen. Beim L\u00f6schen wird nur die Typdefinitionsdatei entfernt; vorhandene Inhaltseintr\u00e4ge bleiben erhalten.", + "searchPlaceholder": "Inhaltstypen suchen…", + "filterByTag": "Tag:", + "tagAll": "Alle", + "noResults": "Keine Typen entsprechen Ihrer Suche oder dem Filter.", "loading": "Laden\u2026", "errorLoading": "Fehler beim Laden der Typen: {error}", "noTypes": "Noch keine Typen vorhanden. Erstelle einen mit \"Neuer Typ\".", @@ -176,9 +220,44 @@ "fieldsLabel": "Felder", "addField": "Feld hinzuf\u00fcgen", "fieldNamePlaceholder": "Feldname", + "fieldTypeLabel": "Feldtyp", "required": "Pflichtfeld", "removeField": "Feld entfernen", "collectionPlaceholder": "Sammlung (z.\u00a0B. seite)", + "allowedSlugsPlaceholder": "Erlaubte Slugs (kommagetrennt, optional)", + "allowedCollectionsPlaceholder": "Erlaubte Inhaltstypen (kommagetrennt, optional)", + "arrayItemType": "Array-Elementtyp", + "itemTypePlaceholder": "z.\u00a0B. string, reference", + "arrayExplain": "Dieses Feld ist in JSON eine Liste [ ]. Jeder Eintrag hat denselben Typ—w\u00e4hle unten, was ein Element ist.", + "arrayEachEntry": "Ein Listeneintrag ist", + "itemKindString": "Einfacher Text (string)", + "itemKindNumber": "Zahl (number)", + "itemKindObject": "Objekt (feste Unterfelder pro Eintrag)", + "itemKindReference": "Referenz (Slug auf einen Eintrag einer Sammlung)", + "objectItemFieldsLabel": "Felder pro Listeneintrag", + "addObjectField": "Unterfeld hinzuf\u00fcgen", + "objectFieldNamePlaceholder": "Unterfeldname", + "arrayReferenceHelp": "Alle Slugs in der Liste m\u00fcssen in dieser Sammlung existieren (oder in der Whitelist stehen).", + "multiSelectOptions": "Optionen", + "multiSelectOptionsPlaceholder": "z.\u00a0B. option1, option2, option3", + "multiSelectOptionsHelp": "Komma- oder zeilengetrennte Liste erlaubter Werte. Es k\u00f6nnen mehrere ausgew\u00e4hlt werden.", + "stringWidgetLabel": "Eingabeart", + "stringWidgetSingleline": "Einzeilig", + "stringWidgetTextarea": "Mehrzeilig (Textbereich)", + "stringWidgetCode": "Code (Syntax-Hervorhebung)", + "codeLanguageLabel": "Code-Sprache", + "codeLanguageCss": "CSS", + "codeLanguageJavascript": "JavaScript", + "codeLanguageJson": "JSON", + "codeLanguageHtml": "HTML", + "defaultValueLabel": "Standardwert", + "defaultValuePlaceholder": "z.\u00a0B. \"text\", 0, true, [\"a\",\"b\"]", + "defaultValueHelp": "JSON-Wert; leer lassen = keiner. Wird bei neuen Eintr\u00e4gen verwendet.", + "defaultValueInvalid": "Ung\u00fcltiges JSON f\u00fcr Standardwert im Feld \"{field}\"", + "defaultValueBoolean": "Standard: angehakt", + "defaultValueEmpty": "Leer lassen = keiner", + "defaultValueMultiSelectSetOptions": "Zuerst Optionen oben eintragen, dann Standard ausw\u00e4hlen.", + "defaultValueArrayPlaceholder": "Kommagetrennte Werte", "fieldDescriptionPlaceholder": "Feldbeschreibung (optional)", "creating": "Erstellen\u2026", "createType": "Typ erstellen", @@ -204,9 +283,44 @@ "fieldsLabel": "Felder", "addField": "Feld hinzuf\u00fcgen", "fieldNamePlaceholder": "Feldname", + "fieldTypeLabel": "Feldtyp", "required": "Pflichtfeld", "removeField": "Feld entfernen", "collectionPlaceholder": "Sammlung (z.\u00a0B. seite)", + "allowedSlugsPlaceholder": "Erlaubte Slugs (kommagetrennt, optional)", + "allowedCollectionsPlaceholder": "Erlaubte Inhaltstypen (kommagetrennt, optional)", + "arrayItemType": "Array-Elementtyp", + "itemTypePlaceholder": "z.\u00a0B. string, reference", + "arrayExplain": "Dieses Feld ist in JSON eine Liste [ ]. Jeder Eintrag hat denselben Typ—w\u00e4hle unten, was ein Element ist.", + "arrayEachEntry": "Ein Listeneintrag ist", + "itemKindString": "Einfacher Text (string)", + "itemKindNumber": "Zahl (number)", + "itemKindObject": "Objekt (feste Unterfelder pro Eintrag)", + "itemKindReference": "Referenz (Slug auf einen Eintrag einer Sammlung)", + "objectItemFieldsLabel": "Felder pro Listeneintrag", + "addObjectField": "Unterfeld hinzuf\u00fcgen", + "objectFieldNamePlaceholder": "Unterfeldname", + "arrayReferenceHelp": "Alle Slugs in der Liste m\u00fcssen in dieser Sammlung existieren (oder in der Whitelist stehen).", + "multiSelectOptions": "Optionen", + "multiSelectOptionsPlaceholder": "z.\u00a0B. option1, option2, option3", + "multiSelectOptionsHelp": "Komma- oder zeilengetrennte Liste erlaubter Werte. Es k\u00f6nnen mehrere ausgew\u00e4hlt werden.", + "stringWidgetLabel": "Eingabeart", + "stringWidgetSingleline": "Einzeilig", + "stringWidgetTextarea": "Mehrzeilig (Textbereich)", + "stringWidgetCode": "Code (Syntax-Hervorhebung)", + "codeLanguageLabel": "Code-Sprache", + "codeLanguageCss": "CSS", + "codeLanguageJavascript": "JavaScript", + "codeLanguageJson": "JSON", + "codeLanguageHtml": "HTML", + "defaultValueLabel": "Standardwert", + "defaultValuePlaceholder": "z.\u00a0B. \"text\", 0, true, [\"a\",\"b\"]", + "defaultValueHelp": "JSON-Wert; leer lassen = keiner. Wird bei neuen Eintr\u00e4gen verwendet.", + "defaultValueInvalid": "Ung\u00fcltiges JSON f\u00fcr Standardwert im Feld \"{field}\"", + "defaultValueBoolean": "Standard: angehakt", + "defaultValueEmpty": "Leer lassen = keiner", + "defaultValueMultiSelectSetOptions": "Zuerst Optionen oben eintragen, dann Standard ausw\u00e4hlen.", + "defaultValueArrayPlaceholder": "Kommagetrennte Werte", "fieldDescriptionPlaceholder": "Feldbeschreibung (optional)", "saving": "Speichern\u2026", "save": "Speichern", @@ -227,20 +341,33 @@ "noEntries": "Keine Einträge.", "noEntriesCreate": "Noch keine Einträge. Erstellen Sie den ersten.", "edit": "Bearbeiten", + "delete": "Löschen", "draft": "Entwurf", "searchPlaceholder": "Suchen…", "loading": "Laden…", "sortBy": "Sortieren nach {field}", "sortAsc": "Aufsteigend", - "sortDesc": "Absteigend" + "sortDesc": "Absteigend", + "typeDependencies": "Typ-Abhängigkeiten", + "confirmDelete": "\"{slug}\" löschen?", + "confirmDeleteDescription": "Dies kann nicht rückgängig gemacht werden.", + "cancel": "Abbrechen", + "deleting": "Löschen…", + "yesDelete": "Ja, löschen", + "deleted": "Eintrag gelöscht.", + "errorDeleting": "Fehler beim Löschen." }, "ContentNewPage": { "breadcrumbNew": "Neu", - "title": "Neuen Eintrag anlegen" + "title": "Neuen Eintrag anlegen", + "sectionSchema": "Schema" }, "ContentEditPage": { "title": "Eintrag bearbeiten", - "apiLink": "API-Link (Daten-Vorschau):" + "apiLink": "API-Link (Daten-Vorschau):", + "referrersSection": "Referenziert von", + "noReferrers": "Kein anderer Eintrag verweist auf diesen.", + "openReferrer": "Eintrag öffnen" }, "AssetsPage": { "titleAll": "Alle Assets", diff --git a/admin-ui/messages/en.json b/admin-ui/messages/en.json index ec06b48..c53f6c1 100644 --- a/admin-ui/messages/en.json +++ b/admin-ui/messages/en.json @@ -10,6 +10,7 @@ "dashboard": "Dashboard", "types": "Types", "assets": "Assets", + "settings": "Settings", "login": "Login", "logout": "Logout", "searchPlaceholder": "Search collections…", @@ -20,6 +21,7 @@ "noResults": "No results for \"{query}\"" }, "ContentForm": { + "copyCode": "Copy code", "slugRequired": "Slug is required.", "slugInUse": "Slug already in use.", "slugMustStartWith": "Slug must start with \"{prefix}\".", @@ -36,6 +38,11 @@ "pleaseSelect": "— Please select —", "removeEntry": "Remove", "addEntry": "+ Add entry", + "arrayAddItem": "Add item", + "arrayRemoveItem": "Remove item", + "arrayItemPlaceholder": "Value", + "arrayItemPlaceholderNumber": "Number", + "arrayHint": "List of values. Add or remove items below.", "keyPlaceholder": "Key", "valuePlaceholder": "Value", "pickAsset": "Pick image", @@ -58,6 +65,7 @@ "typesLabel": "Types: {collections}", "selectType": "— Select type —", "newEntry": "New entry", + "openEntry": "Open entry", "noCollection": "No reference collection in schema. Set {collectionCode} or {collectionsCode} in the type, or start the API and reload the page." }, "ReferenceArrayField": { @@ -72,9 +80,10 @@ "moveUp": "Move up", "moveDown": "Move down", "remove": "Remove", - "newComponent": "+ New {collection} component", + "newComponent": "New {collection} component", "createNewComponent": "+ Create new component…", "openInNewTab": "Open in new tab; then reload this page.", + "openEntry": "Open entry", "noCollection": "No reference collection in schema. Set {collectionCode} or {collectionsCode} in the type, or start the API and reload the page." }, "MarkdownEditor": { @@ -112,6 +121,9 @@ "editSchema": "Edit schema", "hidePreview": "Hide data preview", "showPreview": "Data preview", + "sectionSchema": "Schema", + "sectionDataPreview": "Current data", + "copyCode": "Copy code", "loading": "Loading…", "errorLoading": "Error loading" }, @@ -127,6 +139,34 @@ "ContentLocaleSwitcher": { "label": "Content language" }, + "Settings": { + "title": "Settings", + "connection": "Connection", + "apiUrl": "API URL", + "backendStatus": "Backend status", + "checking": "Checking…", + "apiReachable": "API reachable", + "apiUnreachable": "API unreachable", + "contentLocales": "Content locales", + "default": "default", + "thisDevice": "This device", + "uiLanguage": "UI language", + "itemsPerPage": "Items per page", + "itemsPerPageHint": "Applies to content list pages.", + "refreshData": "Refresh data", + "refreshDataSuccess": "Data refreshed.", + "clearSession": "Clear session", + "clearSessionHint": "Removes API key and all admin preferences (e.g. on shared devices).", + "clearSessionConfirmTitle": "Clear session?", + "clearSessionConfirmDescription": "This will log you out and remove all stored preferences (e.g. items per page). You can log in again afterwards.", + "clearSessionConfirmAction": "Clear", + "cancel": "Cancel", + "apiKeyStatus": "API key", + "apiKeyFromEnv": "Set from environment (read-only).", + "apiKeyManual": "Set manually (this session).", + "logout": "Log out", + "login": "Log in" + }, "Dashboard": { "title": "Dashboard", "subtitle": "Choose a collection to manage content.", @@ -141,6 +181,10 @@ "title": "Types", "newType": "New type", "description": "Content types (collections). Edit the schema or delete a type. Deleting removes the type definition file; existing content entries are not removed.", + "searchPlaceholder": "Search content types…", + "filterByTag": "Tag:", + "tagAll": "All", + "noResults": "No content types match your search or filter.", "loading": "Loading…", "errorLoading": "Error loading types: {error}", "noTypes": "No types yet. Create one with \"New type\".", @@ -176,9 +220,44 @@ "fieldsLabel": "Fields", "addField": "Add field", "fieldNamePlaceholder": "Field name", + "fieldTypeLabel": "Field type", "required": "Required", "removeField": "Remove field", "collectionPlaceholder": "Collection (e.g. page)", + "allowedSlugsPlaceholder": "Allowed slugs (comma-separated, optional)", + "allowedCollectionsPlaceholder": "Allowed content types (comma-separated, optional)", + "arrayItemType": "Array item type", + "itemTypePlaceholder": "e.g. string, reference", + "arrayExplain": "This field is a list [ ] in JSON. Every position in the list has the same type—pick what one entry is below.", + "arrayEachEntry": "One list entry is", + "itemKindString": "Plain text (string)", + "itemKindNumber": "Number", + "itemKindObject": "Object (fixed sub-fields per entry)", + "itemKindReference": "Reference (slug to an entry in a collection)", + "objectItemFieldsLabel": "Fields on each list object", + "addObjectField": "Add sub-field", + "objectFieldNamePlaceholder": "Sub-field name", + "arrayReferenceHelp": "All slugs in the list must exist in this collection (unless you use allowed slugs).", + "multiSelectOptions": "Options", + "multiSelectOptionsPlaceholder": "e.g. option1, option2, option3", + "multiSelectOptionsHelp": "Comma- or newline-separated list of allowed values. User can select multiple.", + "stringWidgetLabel": "Input style", + "stringWidgetSingleline": "Single line", + "stringWidgetTextarea": "Multi-line (textarea)", + "stringWidgetCode": "Code (syntax highlighting)", + "codeLanguageLabel": "Code language", + "codeLanguageCss": "CSS", + "codeLanguageJavascript": "JavaScript", + "codeLanguageJson": "JSON", + "codeLanguageHtml": "HTML", + "defaultValueLabel": "Default value", + "defaultValuePlaceholder": "e.g. \"text\", 0, true, [\"a\",\"b\"]", + "defaultValueHelp": "JSON value; leave empty for none. Used when creating new entries.", + "defaultValueInvalid": "Invalid JSON for default value in field \"{field}\"", + "defaultValueBoolean": "Default: checked", + "defaultValueEmpty": "Leave empty for none", + "defaultValueMultiSelectSetOptions": "Set options above first, then choose defaults.", + "defaultValueArrayPlaceholder": "Comma-separated values", "fieldDescriptionPlaceholder": "Field description (optional)", "creating": "Creating…", "createType": "Create type", @@ -204,9 +283,44 @@ "fieldsLabel": "Fields", "addField": "Add field", "fieldNamePlaceholder": "Field name", + "fieldTypeLabel": "Field type", "required": "Required", "removeField": "Remove field", "collectionPlaceholder": "Collection (e.g. page)", + "allowedSlugsPlaceholder": "Allowed slugs (comma-separated, optional)", + "allowedCollectionsPlaceholder": "Allowed content types (comma-separated, optional)", + "arrayItemType": "Array item type", + "itemTypePlaceholder": "e.g. string, reference", + "arrayExplain": "This field is a list [ ] in JSON. Every position in the list has the same type—pick what one entry is below.", + "arrayEachEntry": "One list entry is", + "itemKindString": "Plain text (string)", + "itemKindNumber": "Number", + "itemKindObject": "Object (fixed sub-fields per entry)", + "itemKindReference": "Reference (slug to an entry in a collection)", + "objectItemFieldsLabel": "Fields on each list object", + "addObjectField": "Add sub-field", + "objectFieldNamePlaceholder": "Sub-field name", + "arrayReferenceHelp": "All slugs in the list must exist in this collection (unless you use allowed slugs).", + "multiSelectOptions": "Options", + "multiSelectOptionsPlaceholder": "e.g. option1, option2, option3", + "multiSelectOptionsHelp": "Comma- or newline-separated list of allowed values. User can select multiple.", + "stringWidgetLabel": "Input style", + "stringWidgetSingleline": "Single line", + "stringWidgetTextarea": "Multi-line (textarea)", + "stringWidgetCode": "Code (syntax highlighting)", + "codeLanguageLabel": "Code language", + "codeLanguageCss": "CSS", + "codeLanguageJavascript": "JavaScript", + "codeLanguageJson": "JSON", + "codeLanguageHtml": "HTML", + "defaultValueLabel": "Default value", + "defaultValuePlaceholder": "e.g. \"text\", 0, true, [\"a\",\"b\"]", + "defaultValueHelp": "JSON value; leave empty for none. Used when creating new entries.", + "defaultValueInvalid": "Invalid JSON for default value in field \"{field}\"", + "defaultValueBoolean": "Default: checked", + "defaultValueEmpty": "Leave empty for none", + "defaultValueMultiSelectSetOptions": "Set options above first, then choose defaults.", + "defaultValueArrayPlaceholder": "Comma-separated values", "fieldDescriptionPlaceholder": "Field description (optional)", "saving": "Saving…", "save": "Save", @@ -227,20 +341,33 @@ "noEntries": "No entries.", "noEntriesCreate": "No entries yet. Create the first one.", "edit": "Edit", + "delete": "Delete", "draft": "Draft", "searchPlaceholder": "Search…", "loading": "Loading…", "sortBy": "Sort by {field}", "sortAsc": "Ascending", - "sortDesc": "Descending" + "sortDesc": "Descending", + "typeDependencies": "Type dependencies", + "confirmDelete": "Delete \"{slug}\"?", + "confirmDeleteDescription": "This cannot be undone.", + "cancel": "Cancel", + "deleting": "Deleting…", + "yesDelete": "Yes, delete", + "deleted": "Entry deleted.", + "errorDeleting": "Error deleting entry." }, "ContentNewPage": { "breadcrumbNew": "New", - "title": "Create new entry" + "title": "Create new entry", + "sectionSchema": "Schema" }, "ContentEditPage": { "title": "Edit entry", - "apiLink": "API link (data preview):" + "apiLink": "API link (data preview):", + "referrersSection": "Referenced by", + "noReferrers": "No other entries reference this one.", + "openReferrer": "Open entry" }, "AssetsPage": { "titleAll": "All assets", diff --git a/admin-ui/package-lock.json b/admin-ui/package-lock.json index 9a58f6c..c0d0259 100644 --- a/admin-ui/package-lock.json +++ b/admin-ui/package-lock.json @@ -11,6 +11,7 @@ "@fontsource-variable/space-grotesk": "^5.2.10", "@iconify/react": "^6.0.2", "@tanstack/react-query": "^5.90.21", + "@xyflow/react": "^12.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -22,6 +23,7 @@ "react-dom": "19.2.3", "react-hook-form": "^7.71.1", "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^16.1.1", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7" @@ -31,6 +33,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", @@ -242,6 +245,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -4585,6 +4597,55 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4657,6 +4718,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -4676,6 +4743,16 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5226,6 +5303,38 @@ "win32" ] }, + "node_modules/@xyflow/react": { + "version": "12.10.1", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz", + "integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.75", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.75", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.75.tgz", + "integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -5767,6 +5876,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -5863,6 +5978,111 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6797,6 +7017,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6877,6 +7110,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7174,6 +7415,19 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -7214,6 +7468,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -7231,6 +7502,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -8271,6 +8557,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9519,6 +9819,15 @@ "node": ">= 0.8.0" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9812,6 +10121,26 @@ } } }, + "node_modules/react-syntax-highlighter": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -9835,6 +10164,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -11251,6 +11596,34 @@ "zod": "^3.25.0 || ^4.0.0" } }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/admin-ui/package.json b/admin-ui/package.json index bcb2b5e..921aac2 100644 --- a/admin-ui/package.json +++ b/admin-ui/package.json @@ -12,6 +12,7 @@ "@fontsource-variable/space-grotesk": "^5.2.10", "@iconify/react": "^6.0.2", "@tanstack/react-query": "^5.90.21", + "@xyflow/react": "^12.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -23,6 +24,7 @@ "react-dom": "19.2.3", "react-hook-form": "^7.71.1", "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^16.1.1", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7" @@ -32,6 +34,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", diff --git a/admin-ui/src/app/admin/DefaultValueField.tsx b/admin-ui/src/app/admin/DefaultValueField.tsx new file mode 100644 index 0000000..db55e6a --- /dev/null +++ b/admin-ui/src/app/admin/DefaultValueField.tsx @@ -0,0 +1,190 @@ +"use client"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Textarea } from "@/components/ui/textarea"; + +/** Minimal field shape for default value editing (same as FieldRow from type editor). */ +export type DefaultValueFieldRow = { + id: string; + type: string; + defaultValue: string; + enumOptions: string; + widget: string; + itemType: string; +}; + +/** Default value input: same UI as on content detail page (Input, Checkbox, Textarea). */ +export function DefaultValueField({ + field, + updateField, + t, +}: { + field: DefaultValueFieldRow; + updateField: (id: string, patch: Partial) => void; + t: (key: string) => string; +}) { + const id = field.id; + const raw = field.defaultValue.trim(); + let parsed: unknown = undefined; + if (raw) { + try { + parsed = JSON.parse(raw); + } catch { + // leave parsed undefined, show raw in fallback + } + } + + const setDefault = (value: unknown) => { + if (value === undefined || value === null || (typeof value === "string" && value === "")) { + updateField(id, { defaultValue: "" }); + return; + } + updateField(id, { defaultValue: JSON.stringify(value) }); + }; + + const type = field.type; + + if (type === "boolean") { + const checked = parsed === true; + return ( +
+ + +
+ ); + } + + if (type === "number" || type === "integer") { + const num = typeof parsed === "number" && Number.isFinite(parsed) ? parsed : ""; + return ( +
+ + { + const v = e.target.value; + if (v === "") setDefault(undefined); + else setDefault(type === "integer" ? parseInt(v, 10) : parseFloat(v)); + }} + placeholder={t("defaultValueEmpty")} + className="h-9 text-sm" + /> +
+ ); + } + + if (type === "multiSelect") { + const opts = field.enumOptions + ? field.enumOptions.split(/[\n,]+/).map((s) => s.trim()).filter(Boolean) + : []; + const selected: string[] = Array.isArray(parsed) ? (parsed as string[]) : []; + const toggle = (opt: string) => { + const next = selected.includes(opt) ? selected.filter((s) => s !== opt) : [...selected, opt]; + setDefault(next); + }; + return ( +
+ + {opts.length > 0 ? ( +
+ {opts.map((opt) => ( + + ))} +
+ ) : ( +

{t("defaultValueMultiSelectSetOptions")}

+ )} +
+ ); + } + + if ( + type === "string" || + type === "richtext" || + type === "html" || + type === "markdown" || + type === "textOrRef" || + type === "datetime" || + type === "reference" || + type === "referenceOrInline" + ) { + const str = typeof parsed === "string" ? parsed : raw ? String(parsed ?? raw) : ""; + const isTextarea = type === "string" && field.widget === "textarea"; + return ( +
+ + {isTextarea ? ( +