diff --git a/ROADMAP.md b/ROADMAP.md index c264fd62f4..83a7ce801f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,7 +5,7 @@ > **Spec Version:** @objectstack/spec v3.0.9 > **Client Version:** @objectstack/client v3.0.9 > **Target UX Benchmark:** 🎯 Airtable parity -> **Current Priority:** AppShell Navigation · Designer Interaction · View Config Live Preview Sync · Dashboard Config Panel · Airtable UX Polish · **Flow Designer ✅** · **Schema-Driven View Config Panel ✅** +> **Current Priority:** AppShell Navigation · Designer Interaction · View Config Live Preview Sync · Dashboard Config Panel · Airtable UX Polish · **Flow Designer ✅** · **App Creation & Editing Flow ✅** --- @@ -13,7 +13,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind + Shadcn. It renders JSON metadata from the @objectstack/spec protocol into pixel-perfect, accessible, and interactive enterprise interfaces. -**Where We Are:** Foundation is **solid and shipping** — 35 packages, 99+ components, 5,618+ tests, 78 Storybook stories, 42/42 builds passing, ~88% protocol alignment. SpecBridge, Expression Engine, Action Engine, data binding, all view plugins (Grid/Kanban/Calendar/Gantt/Timeline/Map/Gallery), Record components, Report engine, Dashboard BI features, mobile UX, i18n (11 locales), WCAG AA accessibility, Designer Phase 1 (ViewDesigner drag-to-reorder ✅), Console through Phase 20 (L3), **AppShell Navigation Renderer** (P0.1), **Flow Designer** (P2.4), **Feed/Chatter UI** (P1.5), and **ListView Spec Protocol Gap Fixes** (P2.6 partial) — all ✅ complete. +**Where We Are:** Foundation is **solid and shipping** — 35 packages, 99+ components, 5,700+ tests, 78 Storybook stories, 42/42 builds passing, ~85% protocol alignment. SpecBridge, Expression Engine, Action Engine, data binding, all view plugins (Grid/Kanban/Calendar/Gantt/Timeline/Map/Gallery), Record components, Report engine, Dashboard BI features, mobile UX, i18n (11 locales), WCAG AA accessibility, Designer Phase 1 (ViewDesigner drag-to-reorder ✅), Console through Phase 20 (L3), **AppShell Navigation Renderer** (P0.1), **Flow Designer** (P2.4), **Feed/Chatter UI** (P1.5), and **App Creation & Editing Flow** (P1.11) — all ✅ complete. **What Remains:** The gap to **Airtable-level UX** is primarily in: 1. ~~**AppShell** — No dynamic navigation renderer from spec JSON (last P0 blocker)~~ ✅ Complete @@ -389,6 +389,78 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind - [ ] Mobile/tablet end-to-end testing for all view types - [ ] Dynamic width calculation for Gantt task list and Kanban columns based on container width +### P1.11 App Creation & Editing Flow (Airtable Interface Designer UX) + +> Full app creation & editing experience aligned with Airtable Interface Designer UX. +> Components live in `@object-ui/plugin-designer`, types in `@object-ui/types`. + +**Types & Interfaces:** +- [x] `AppWizardDraft` / `AppWizardStep` / `BrandingConfig` / `ObjectSelection` / `EditorMode` types +- [x] `isValidAppName()` snake_case validator function +- [x] `wizardDraftToAppSchema()` draft-to-schema conversion function + +**App Creation Wizard (4-step):** +- [x] Step 1: Basic Info — name (snake_case validated), title, description, icon, template, layout selector +- [x] Step 2: Object Selection — card grid with search, select all/none, toggle selection +- [x] Step 3: Navigation Builder — auto-generates NavigationItem[] from selected objects, add group/URL/separator, reorder up/down, remove +- [x] Step 4: Branding — logo URL, primary color, favicon, live preview card +- [x] Step indicator with connected progress dots +- [x] Step validation (step 1 requires valid snake_case name + title) +- [x] onComplete callback returns full AppWizardDraft + +**Navigation Designer:** +- [x] Recursive NavigationItem tree renderer (supports infinite nesting) +- [x] Quick add buttons for all 7 nav item types (object, dashboard, page, report, group, URL, separator) +- [x] Inline label editing (double-click to edit, Enter/Escape to commit/discard) +- [x] Add child to groups (nested navigation) +- [x] Reorder items (up/down buttons) +- [x] Deep tree operations (remove/reorder works at any depth) +- [x] Live preview sidebar showing navigation as rendered +- [x] Type badges with color coding + +**Page Canvas Editor:** +- [x] Component palette (Grid, Kanban, Calendar, Gallery, Dashboard, Form, Layout Grid) +- [x] Component list with drag handles, selection, reorder +- [x] Property panel for selected component (label, type, ID) +- [x] Add/remove/reorder components +- [x] Syncs PageSchema children on every change + +**Dashboard Editor:** +- [x] 6 widget types (KPI Metric, Bar Chart, Line Chart, Pie Chart, Table, Grid) +- [x] Widget card grid with selection, drag handles, reorder +- [x] Widget property panel (title, type, data source, value field, aggregate, color variant) +- [x] Add/remove/reorder widgets +- [x] Grid layout based on DashboardSchema columns + +**Object View Configurator:** +- [x] 7 view type switcher (Grid, Kanban, Calendar, Gallery, Timeline, Map, Gantt) +- [x] Column visibility toggle with visible count +- [x] Column reorder (up/down) +- [x] Toolbar toggles (showSearch, showFilters, showSort) +- [x] Appearance section (row height, striped, bordered) +- [x] Collapsible sections + +**Editor Mode Toggle:** +- [x] Three-way toggle (Edit / Preview / Code) +- [x] Radio group accessibility (role="radiogroup", aria-checked) +- [x] Disabled state support + +**i18n:** +- [x] `appDesigner` section with 54 keys added to all 10 locales (en, zh, ja, de, fr, es, ar, ru, pt, ko) + +**Testing:** +- [x] 9 type tests (isValidAppName, wizardDraftToAppSchema, type shapes) +- [x] 31 AppCreationWizard tests (rendering, steps 1-4, navigation, callbacks, read-only) +- [x] 18 NavigationDesigner tests (rendering, add, remove, groups, badges, read-only) +- [x] 7 EditorModeToggle tests (render, active mode, onChange, accessibility, disabled) +- [x] 14 DashboardEditor tests (rendering, add/remove widgets, property panel, read-only) +- [x] 10 PageCanvasEditor tests (rendering, add/remove components, property panel, read-only) +- [x] 14 ObjectViewConfigurator tests (rendering, view type switch, column visibility, toggles, read-only) +- [x] **Total: 103 new tests, all passing** + +**ComponentRegistry:** +- [x] Registered: `app-creation-wizard`, `navigation-designer`, `dashboard-editor`, `page-canvas-editor`, `object-view-configurator` + --- ## 🧩 P2 — Polish & Advanced Features diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index f73991bb7d..0e9a628112 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -138,6 +138,60 @@ const ar = { general: 'عام', advanced: 'متقدم', }, + appDesigner: { + createApp: 'Create Application', + editApp: 'Edit Application', + basicInfo: 'Basic Info', + objects: 'Objects', + navigation: 'Navigation', + branding: 'Branding', + appName: 'App Name', + appTitle: 'Title', + appDescription: 'Description', + appIcon: 'Icon', + template: 'Template', + layout: 'Layout', + layoutSidebar: 'Sidebar', + layoutHeader: 'Header', + layoutEmpty: 'Empty', + selectObjects: 'Select Objects', + searchObjects: 'Search objects…', + selectAll: 'Select All', + deselectAll: 'Deselect All', + navBuilder: 'Navigation Builder', + addGroup: 'Add Group', + addUrl: 'Add URL', + addSeparator: 'Add Separator', + noNavItems: 'No navigation items yet.', + logoUrl: 'Logo URL', + primaryColor: 'Primary Color', + faviconUrl: 'Favicon URL', + preview: 'Preview', + complete: 'Complete', + snakeCaseHint: 'Must be snake_case (e.g. my_app)', + modeEdit: 'Edit', + modePreview: 'Preview', + modeCode: 'Code', + addWidget: 'Add Widget', + widgetProperties: 'Widget Properties', + dataSource: 'Data Source', + valueField: 'Value Field', + aggregate: 'Aggregate', + colorVariant: 'Color Variant', + addComponent: 'Add Component', + componentProperties: 'Component Properties', + viewType: 'View Type', + fields: 'Fields', + toolbar: 'Toolbar', + showSearch: 'Show Search', + showFilters: 'Show Filters', + showSort: 'Show Sort', + appearance: 'Appearance', + rowHeight: 'Row Height', + stripedRows: 'Striped Rows', + bordered: 'Bordered', + livePreview: 'Live Preview', + }, console: { title: 'وحدة تحكم ObjectStack', initializing: 'جاري تهيئة التطبيق...', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index db66cc1f4b..0762055d39 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -137,6 +137,60 @@ const de = { general: 'Allgemein', advanced: 'Erweitert', }, + appDesigner: { + createApp: 'App erstellen', + editApp: 'App bearbeiten', + basicInfo: 'Grundinformationen', + objects: 'Objekte', + navigation: 'Navigation', + branding: 'Branding', + appName: 'App-Name', + appTitle: 'Titel', + appDescription: 'Beschreibung', + appIcon: 'Symbol', + template: 'Vorlage', + layout: 'Layout', + layoutSidebar: 'Seitenleiste', + layoutHeader: 'Kopfzeile', + layoutEmpty: 'Leer', + selectObjects: 'Objekte auswählen', + searchObjects: 'Objekte suchen…', + selectAll: 'Alle auswählen', + deselectAll: 'Alle abwählen', + navBuilder: 'Navigations-Builder', + addGroup: 'Gruppe hinzufügen', + addUrl: 'URL hinzufügen', + addSeparator: 'Trenner hinzufügen', + noNavItems: 'Noch keine Navigationselemente.', + logoUrl: 'Logo-URL', + primaryColor: 'Primärfarbe', + faviconUrl: 'Favicon-URL', + preview: 'Vorschau', + complete: 'Fertigstellen', + snakeCaseHint: 'Muss snake_case sein (z.B. my_app)', + modeEdit: 'Bearbeiten', + modePreview: 'Vorschau', + modeCode: 'Code', + addWidget: 'Widget hinzufügen', + widgetProperties: 'Widget-Eigenschaften', + dataSource: 'Datenquelle', + valueField: 'Wertfeld', + aggregate: 'Aggregat', + colorVariant: 'Farbvariante', + addComponent: 'Komponente hinzufügen', + componentProperties: 'Komponenteneigenschaften', + viewType: 'Ansichtstyp', + fields: 'Felder', + toolbar: 'Symbolleiste', + showSearch: 'Suche anzeigen', + showFilters: 'Filter anzeigen', + showSort: 'Sortierung anzeigen', + appearance: 'Erscheinung', + rowHeight: 'Zeilenhöhe', + stripedRows: 'Gestreifte Zeilen', + bordered: 'Umrandet', + livePreview: 'Live-Vorschau', + }, console: { title: 'ObjectStack Konsole', initializing: 'Anwendung wird initialisiert...', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 44df292ddc..571ebddd79 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -137,6 +137,60 @@ const en = { general: 'General', advanced: 'Advanced', }, + appDesigner: { + createApp: 'Create Application', + editApp: 'Edit Application', + basicInfo: 'Basic Info', + objects: 'Objects', + navigation: 'Navigation', + branding: 'Branding', + appName: 'App Name', + appTitle: 'Title', + appDescription: 'Description', + appIcon: 'Icon', + template: 'Template', + layout: 'Layout', + layoutSidebar: 'Sidebar', + layoutHeader: 'Header', + layoutEmpty: 'Empty', + selectObjects: 'Select Objects', + searchObjects: 'Search objects…', + selectAll: 'Select All', + deselectAll: 'Deselect All', + navBuilder: 'Navigation Builder', + addGroup: 'Add Group', + addUrl: 'Add URL', + addSeparator: 'Add Separator', + noNavItems: 'No navigation items yet.', + logoUrl: 'Logo URL', + primaryColor: 'Primary Color', + faviconUrl: 'Favicon URL', + preview: 'Preview', + complete: 'Complete', + snakeCaseHint: 'Must be snake_case (e.g. my_app)', + modeEdit: 'Edit', + modePreview: 'Preview', + modeCode: 'Code', + addWidget: 'Add Widget', + widgetProperties: 'Widget Properties', + dataSource: 'Data Source', + valueField: 'Value Field', + aggregate: 'Aggregate', + colorVariant: 'Color Variant', + addComponent: 'Add Component', + componentProperties: 'Component Properties', + viewType: 'View Type', + fields: 'Fields', + toolbar: 'Toolbar', + showSearch: 'Show Search', + showFilters: 'Show Filters', + showSort: 'Show Sort', + appearance: 'Appearance', + rowHeight: 'Row Height', + stripedRows: 'Striped Rows', + bordered: 'Bordered', + livePreview: 'Live Preview', + }, console: { title: 'ObjectStack Console', initializing: 'Initializing application...', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 594ee82555..f59acb2f53 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -137,6 +137,60 @@ const es = { general: 'General', advanced: 'Avanzado', }, + appDesigner: { + createApp: 'Crear aplicación', + editApp: 'Editar aplicación', + basicInfo: 'Información básica', + objects: 'Objetos', + navigation: 'Navegación', + branding: 'Marca', + appName: 'Nombre de la app', + appTitle: 'Título', + appDescription: 'Descripción', + appIcon: 'Ícono', + template: 'Plantilla', + layout: 'Diseño', + layoutSidebar: 'Barra lateral', + layoutHeader: 'Encabezado', + layoutEmpty: 'Vacío', + selectObjects: 'Seleccionar objetos', + searchObjects: 'Buscar objetos…', + selectAll: 'Seleccionar todo', + deselectAll: 'Deseleccionar todo', + navBuilder: 'Constructor de navegación', + addGroup: 'Agregar grupo', + addUrl: 'Agregar URL', + addSeparator: 'Agregar separador', + noNavItems: 'Sin elementos de navegación.', + logoUrl: 'URL del logo', + primaryColor: 'Color principal', + faviconUrl: 'URL del favicon', + preview: 'Vista previa', + complete: 'Completar', + snakeCaseHint: 'Debe ser snake_case (ej. my_app)', + modeEdit: 'Editar', + modePreview: 'Vista previa', + modeCode: 'Código', + addWidget: 'Agregar widget', + widgetProperties: 'Propiedades del widget', + dataSource: 'Fuente de datos', + valueField: 'Campo de valor', + aggregate: 'Agregado', + colorVariant: 'Variante de color', + addComponent: 'Agregar componente', + componentProperties: 'Propiedades del componente', + viewType: 'Tipo de vista', + fields: 'Campos', + toolbar: 'Barra de herramientas', + showSearch: 'Mostrar búsqueda', + showFilters: 'Mostrar filtros', + showSort: 'Mostrar orden', + appearance: 'Apariencia', + rowHeight: 'Altura de fila', + stripedRows: 'Filas rayadas', + bordered: 'Con borde', + livePreview: 'Vista previa en vivo', + }, console: { title: 'Consola ObjectStack', initializing: 'Inicializando aplicación...', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index e57ecd0f8f..db35642631 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -137,6 +137,60 @@ const fr = { general: 'Général', advanced: 'Avancé', }, + appDesigner: { + createApp: 'Créer une application', + editApp: "Modifier l'application", + basicInfo: 'Informations de base', + objects: 'Objets', + navigation: 'Navigation', + branding: 'Marque', + appName: "Nom de l'application", + appTitle: 'Titre', + appDescription: 'Description', + appIcon: 'Icône', + template: 'Modèle', + layout: 'Disposition', + layoutSidebar: 'Barre latérale', + layoutHeader: 'En-tête', + layoutEmpty: 'Vide', + selectObjects: 'Sélectionner les objets', + searchObjects: 'Rechercher des objets…', + selectAll: 'Tout sélectionner', + deselectAll: 'Tout désélectionner', + navBuilder: 'Constructeur de navigation', + addGroup: 'Ajouter un groupe', + addUrl: 'Ajouter un URL', + addSeparator: 'Ajouter un séparateur', + noNavItems: 'Aucun élément de navigation.', + logoUrl: 'URL du logo', + primaryColor: 'Couleur principale', + faviconUrl: 'URL du favicon', + preview: 'Aperçu', + complete: 'Terminer', + snakeCaseHint: 'Doit être en snake_case (ex. my_app)', + modeEdit: 'Éditer', + modePreview: 'Aperçu', + modeCode: 'Code', + addWidget: 'Ajouter un widget', + widgetProperties: 'Propriétés du widget', + dataSource: 'Source de données', + valueField: 'Champ de valeur', + aggregate: 'Agrégat', + colorVariant: 'Variante de couleur', + addComponent: 'Ajouter un composant', + componentProperties: 'Propriétés du composant', + viewType: 'Type de vue', + fields: 'Champs', + toolbar: "Barre d'outils", + showSearch: 'Afficher la recherche', + showFilters: 'Afficher les filtres', + showSort: 'Afficher le tri', + appearance: 'Apparence', + rowHeight: 'Hauteur de ligne', + stripedRows: 'Lignes rayées', + bordered: 'Bordé', + livePreview: 'Aperçu en direct', + }, console: { title: 'Console ObjectStack', initializing: "Initialisation de l'application...", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 66e47fa6b2..e194c50b2a 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -137,6 +137,60 @@ const ja = { general: '基本', advanced: '詳細', }, + appDesigner: { + createApp: 'アプリを作成', + editApp: 'アプリを編集', + basicInfo: '基本情報', + objects: 'オブジェクト', + navigation: 'ナビゲーション', + branding: 'ブランディング', + appName: 'アプリ名', + appTitle: 'タイトル', + appDescription: '説明', + appIcon: 'アイコン', + template: 'テンプレート', + layout: 'レイアウト', + layoutSidebar: 'サイドバー', + layoutHeader: 'ヘッダー', + layoutEmpty: '空白', + selectObjects: 'オブジェクトを選択', + searchObjects: 'オブジェクトを検索…', + selectAll: 'すべて選択', + deselectAll: 'すべて解除', + navBuilder: 'ナビビルダー', + addGroup: 'グループを追加', + addUrl: 'URLを追加', + addSeparator: '区切りを追加', + noNavItems: 'ナビゲーション項目がありません。', + logoUrl: 'ロゴURL', + primaryColor: 'プライマリカラー', + faviconUrl: 'ファビコンURL', + preview: 'プレビュー', + complete: '完了', + snakeCaseHint: 'snake_case形式が必要です(例: my_app)', + modeEdit: '編集', + modePreview: 'プレビュー', + modeCode: 'コード', + addWidget: 'ウィジェット追加', + widgetProperties: 'ウィジェット設定', + dataSource: 'データソース', + valueField: '値フィールド', + aggregate: '集計', + colorVariant: 'カラー', + addComponent: 'コンポーネント追加', + componentProperties: 'コンポーネント設定', + viewType: 'ビュータイプ', + fields: 'フィールド', + toolbar: 'ツールバー', + showSearch: '検索を表示', + showFilters: 'フィルタを表示', + showSort: 'ソートを表示', + appearance: '外観', + rowHeight: '行の高さ', + stripedRows: 'ストライプ', + bordered: 'ボーダー', + livePreview: 'ライブプレビュー', + }, console: { title: 'ObjectStack コンソール', initializing: 'アプリケーションを初期化中...', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 221852238f..888c7ec904 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -137,6 +137,60 @@ const ko = { general: '일반', advanced: '고급', }, + appDesigner: { + createApp: 'Create Application', + editApp: 'Edit Application', + basicInfo: 'Basic Info', + objects: 'Objects', + navigation: 'Navigation', + branding: 'Branding', + appName: 'App Name', + appTitle: 'Title', + appDescription: 'Description', + appIcon: 'Icon', + template: 'Template', + layout: 'Layout', + layoutSidebar: 'Sidebar', + layoutHeader: 'Header', + layoutEmpty: 'Empty', + selectObjects: 'Select Objects', + searchObjects: 'Search objects…', + selectAll: 'Select All', + deselectAll: 'Deselect All', + navBuilder: 'Navigation Builder', + addGroup: 'Add Group', + addUrl: 'Add URL', + addSeparator: 'Add Separator', + noNavItems: 'No navigation items yet.', + logoUrl: 'Logo URL', + primaryColor: 'Primary Color', + faviconUrl: 'Favicon URL', + preview: 'Preview', + complete: 'Complete', + snakeCaseHint: 'Must be snake_case (e.g. my_app)', + modeEdit: 'Edit', + modePreview: 'Preview', + modeCode: 'Code', + addWidget: 'Add Widget', + widgetProperties: 'Widget Properties', + dataSource: 'Data Source', + valueField: 'Value Field', + aggregate: 'Aggregate', + colorVariant: 'Color Variant', + addComponent: 'Add Component', + componentProperties: 'Component Properties', + viewType: 'View Type', + fields: 'Fields', + toolbar: 'Toolbar', + showSearch: 'Show Search', + showFilters: 'Show Filters', + showSort: 'Show Sort', + appearance: 'Appearance', + rowHeight: 'Row Height', + stripedRows: 'Striped Rows', + bordered: 'Bordered', + livePreview: 'Live Preview', + }, console: { title: 'ObjectStack 콘솔', initializing: '애플리케이션 초기화 중...', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 37e7fdd782..33a53a9e0a 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -137,6 +137,60 @@ const pt = { general: 'Geral', advanced: 'Avançado', }, + appDesigner: { + createApp: 'Create Application', + editApp: 'Edit Application', + basicInfo: 'Basic Info', + objects: 'Objects', + navigation: 'Navigation', + branding: 'Branding', + appName: 'App Name', + appTitle: 'Title', + appDescription: 'Description', + appIcon: 'Icon', + template: 'Template', + layout: 'Layout', + layoutSidebar: 'Sidebar', + layoutHeader: 'Header', + layoutEmpty: 'Empty', + selectObjects: 'Select Objects', + searchObjects: 'Search objects…', + selectAll: 'Select All', + deselectAll: 'Deselect All', + navBuilder: 'Navigation Builder', + addGroup: 'Add Group', + addUrl: 'Add URL', + addSeparator: 'Add Separator', + noNavItems: 'No navigation items yet.', + logoUrl: 'Logo URL', + primaryColor: 'Primary Color', + faviconUrl: 'Favicon URL', + preview: 'Preview', + complete: 'Complete', + snakeCaseHint: 'Must be snake_case (e.g. my_app)', + modeEdit: 'Edit', + modePreview: 'Preview', + modeCode: 'Code', + addWidget: 'Add Widget', + widgetProperties: 'Widget Properties', + dataSource: 'Data Source', + valueField: 'Value Field', + aggregate: 'Aggregate', + colorVariant: 'Color Variant', + addComponent: 'Add Component', + componentProperties: 'Component Properties', + viewType: 'View Type', + fields: 'Fields', + toolbar: 'Toolbar', + showSearch: 'Show Search', + showFilters: 'Show Filters', + showSort: 'Show Sort', + appearance: 'Appearance', + rowHeight: 'Row Height', + stripedRows: 'Striped Rows', + bordered: 'Bordered', + livePreview: 'Live Preview', + }, console: { title: 'Console ObjectStack', initializing: 'Inicializando aplicação...', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 9b87246b54..667b578052 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -137,6 +137,60 @@ const ru = { general: 'Основные', advanced: 'Дополнительно', }, + appDesigner: { + createApp: 'Create Application', + editApp: 'Edit Application', + basicInfo: 'Basic Info', + objects: 'Objects', + navigation: 'Navigation', + branding: 'Branding', + appName: 'App Name', + appTitle: 'Title', + appDescription: 'Description', + appIcon: 'Icon', + template: 'Template', + layout: 'Layout', + layoutSidebar: 'Sidebar', + layoutHeader: 'Header', + layoutEmpty: 'Empty', + selectObjects: 'Select Objects', + searchObjects: 'Search objects…', + selectAll: 'Select All', + deselectAll: 'Deselect All', + navBuilder: 'Navigation Builder', + addGroup: 'Add Group', + addUrl: 'Add URL', + addSeparator: 'Add Separator', + noNavItems: 'No navigation items yet.', + logoUrl: 'Logo URL', + primaryColor: 'Primary Color', + faviconUrl: 'Favicon URL', + preview: 'Preview', + complete: 'Complete', + snakeCaseHint: 'Must be snake_case (e.g. my_app)', + modeEdit: 'Edit', + modePreview: 'Preview', + modeCode: 'Code', + addWidget: 'Add Widget', + widgetProperties: 'Widget Properties', + dataSource: 'Data Source', + valueField: 'Value Field', + aggregate: 'Aggregate', + colorVariant: 'Color Variant', + addComponent: 'Add Component', + componentProperties: 'Component Properties', + viewType: 'View Type', + fields: 'Fields', + toolbar: 'Toolbar', + showSearch: 'Show Search', + showFilters: 'Show Filters', + showSort: 'Show Sort', + appearance: 'Appearance', + rowHeight: 'Row Height', + stripedRows: 'Striped Rows', + bordered: 'Bordered', + livePreview: 'Live Preview', + }, console: { title: 'Консоль ObjectStack', initializing: 'Инициализация приложения...', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 144231a18f..bcdccb3782 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -137,6 +137,60 @@ const zh = { general: '基本', advanced: '高级', }, + appDesigner: { + createApp: '创建应用', + editApp: '编辑应用', + basicInfo: '基本信息', + objects: '业务对象', + navigation: '导航', + branding: '品牌', + appName: '应用名称', + appTitle: '标题', + appDescription: '描述', + appIcon: '图标', + template: '模板', + layout: '布局', + layoutSidebar: '侧边栏', + layoutHeader: '顶部导航', + layoutEmpty: '空白', + selectObjects: '选择对象', + searchObjects: '搜索对象…', + selectAll: '全选', + deselectAll: '取消全选', + navBuilder: '导航构建器', + addGroup: '添加分组', + addUrl: '添加链接', + addSeparator: '添加分隔线', + noNavItems: '暂无导航项。', + logoUrl: 'Logo 链接', + primaryColor: '主色调', + faviconUrl: '网站图标链接', + preview: '预览', + complete: '完成', + snakeCaseHint: '必须使用 snake_case 格式(如 my_app)', + modeEdit: '编辑', + modePreview: '预览', + modeCode: '代码', + addWidget: '添加组件', + widgetProperties: '组件属性', + dataSource: '数据源', + valueField: '数值字段', + aggregate: '聚合', + colorVariant: '颜色', + addComponent: '添加组件', + componentProperties: '组件属性', + viewType: '视图类型', + fields: '字段', + toolbar: '工具栏', + showSearch: '显示搜索', + showFilters: '显示筛选', + showSort: '显示排序', + appearance: '外观', + rowHeight: '行高', + stripedRows: '斑马纹', + bordered: '边框', + livePreview: '实时预览', + }, console: { title: 'ObjectStack 控制台', initializing: '正在初始化应用程序...', diff --git a/packages/plugin-designer/src/AppCreationWizard.tsx b/packages/plugin-designer/src/AppCreationWizard.tsx new file mode 100644 index 0000000000..329f8b1c36 --- /dev/null +++ b/packages/plugin-designer/src/AppCreationWizard.tsx @@ -0,0 +1,931 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * AppCreationWizard Component + * + * Multi-step wizard for creating applications following the Airtable + * Interface Designer UX pattern. Steps: + * 1. Basic Info (name, title, description, icon, template, layout) + * 2. Object Selection (select business objects to include) + * 3. Navigation Builder (build navigation tree from selected objects) + * 4. Branding (logo, primary color, favicon) + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import type { + AppWizardDraft, + AppWizardStep, + BrandingConfig, + ObjectSelection, + NavigationItem, +} from '@object-ui/types'; +import { isValidAppName } from '@object-ui/types'; +import { + Check, + ChevronLeft, + ChevronRight, + ChevronUp, + ChevronDown, + Search, + Plus, + Trash2, + Layout, + PanelLeft, + LayoutTemplate, + FolderOpen, + Link, + Minus, + Image, + Palette, + Globe, +} from 'lucide-react'; +import { clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +function cn(...inputs: (string | undefined | false)[]) { + return twMerge(clsx(inputs)); +} + +// ============================================================================ +// Types +// ============================================================================ + +export interface AppCreationWizardProps { + /** Available business objects to select from */ + availableObjects?: ObjectSelection[]; + /** Available templates */ + templates?: Array<{ id: string; label: string; description?: string }>; + /** Initial draft state (for editing existing apps) */ + initialDraft?: Partial; + /** Callback when wizard completes */ + onComplete?: (draft: AppWizardDraft) => void; + /** Callback when wizard is cancelled */ + onCancel?: () => void; + /** Read-only mode */ + readOnly?: boolean; + /** CSS class */ + className?: string; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const WIZARD_STEPS: AppWizardStep[] = [ + { id: 'basic', label: 'Basic Info', description: 'Name, title, and layout', icon: 'Settings' }, + { id: 'objects', label: 'Objects', description: 'Select business objects', icon: 'Database' }, + { id: 'navigation', label: 'Navigation', description: 'Build navigation tree', icon: 'Menu' }, + { id: 'branding', label: 'Branding', description: 'Logo, colors, and favicon', icon: 'Palette' }, +]; + +const DEFAULT_DRAFT: AppWizardDraft = { + name: '', + title: '', + description: '', + icon: '', + template: '', + layout: 'sidebar', + objects: [], + navigation: [], + branding: { + logo: '', + primaryColor: '#3b82f6', + favicon: '', + }, +}; + +// ============================================================================ +// Helpers +// ============================================================================ + +function generateNavFromObjects(objects: ObjectSelection[]): NavigationItem[] { + return objects + .filter((o) => o.selected) + .map((o) => ({ + id: o.name, + type: 'object' as const, + label: o.label, + icon: o.icon, + objectName: o.name, + })); +} + +let navItemCounter = 0; + +function createNavId(prefix: string): string { + navItemCounter += 1; + return `${prefix}_${Date.now()}_${navItemCounter}`; +} + +// ============================================================================ +// Step Indicator +// ============================================================================ + +interface StepIndicatorProps { + steps: AppWizardStep[]; + currentIndex: number; + onStepClick: (index: number) => void; +} + +function StepIndicator({ steps, currentIndex, onStepClick }: StepIndicatorProps) { + return ( + + ); +} + +// ============================================================================ +// Step 1: Basic Info +// ============================================================================ + +interface BasicInfoStepProps { + draft: AppWizardDraft; + templates: Array<{ id: string; label: string; description?: string }>; + readOnly: boolean; + onChange: (partial: Partial) => void; +} + +function BasicInfoStep({ draft, templates, readOnly, onChange }: BasicInfoStepProps) { + const nameError = draft.name.length > 0 && !isValidAppName(draft.name); + + return ( +
+ {/* Name */} +
+ + onChange({ name: e.target.value })} + placeholder="my_app" + disabled={readOnly} + className={cn( + 'block w-full rounded-md border px-3 py-2 text-sm shadow-sm outline-none transition-colors', + 'focus:border-blue-500 focus:ring-1 focus:ring-blue-500', + 'disabled:cursor-not-allowed disabled:bg-gray-50', + nameError ? 'border-red-400' : 'border-gray-300' + )} + /> + {nameError && ( +

Must be snake_case (e.g. my_app)

+ )} +
+ + {/* Title */} +
+ + onChange({ title: e.target.value })} + placeholder="My Application" + disabled={readOnly} + className="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm outline-none transition-colors focus:border-blue-500 focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-50" + /> +
+ + {/* Description */} +
+ +