From 757896796c5ccee1c7457f20282fa8362544ce87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 03:52:32 +0000 Subject: [PATCH 1/7] Initial plan From 82099a5509b0e8303485abfef8fd11f44757c7a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:02:09 +0000 Subject: [PATCH 2/7] feat(plugin-designer): add AppCreationWizard component Multi-step wizard for creating applications following the Airtable Interface Designer UX pattern with 4 steps: 1. Basic Info (name, title, description, icon, template, layout) 2. Object Selection (grid with search, toggle all/none) 3. Navigation Builder (auto-generated from objects, add/remove/reorder) 4. Branding (logo, primary color, favicon with preview) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../plugin-designer/src/AppCreationWizard.tsx | 931 ++++++++++++++++++ packages/plugin-designer/src/index.tsx | 18 + packages/types/src/app.ts | 124 +++ packages/types/src/index.ts | 10 +- 4 files changed, 1081 insertions(+), 2 deletions(-) create mode 100644 packages/plugin-designer/src/AppCreationWizard.tsx diff --git a/packages/plugin-designer/src/AppCreationWizard.tsx b/packages/plugin-designer/src/AppCreationWizard.tsx new file mode 100644 index 0000000000..15bbc5b796 --- /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}_${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 */} +
+ +