@withpotter/components 0.13.0-next.82 → 0.13.0-next.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/dashboard/activity-feed.d.ts +2 -0
  2. package/dist/dashboard/activity-feed.js +3 -1
  3. package/dist/dashboard/activity-feed.js.map +1 -1
  4. package/dist/dashboard/activity-paid.d.ts +10 -0
  5. package/dist/dashboard/activity-paid.js +7 -0
  6. package/dist/dashboard/activity-paid.js.map +1 -0
  7. package/dist/dashboard/home/ask-controls.js +1 -0
  8. package/dist/dashboard/home/ask-controls.js.map +1 -1
  9. package/dist/dashboard/home/ask-mode-chip.js +4 -4
  10. package/dist/dashboard/home/ask-mode-chip.js.map +1 -1
  11. package/dist/dashboard/home/home-ask.js +3 -3
  12. package/dist/dashboard/home/home-ask.js.map +1 -1
  13. package/dist/dashboard/home/home-quick-actions.js +9 -1
  14. package/dist/dashboard/home/home-quick-actions.js.map +1 -1
  15. package/dist/dashboard/home/host-quick-actions.d.ts +19 -0
  16. package/dist/dashboard/home/host-quick-actions.js +12 -0
  17. package/dist/dashboard/home/host-quick-actions.js.map +1 -0
  18. package/dist/dashboard/home/widgets/activity-widget.js +56 -43
  19. package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
  20. package/dist/dashboard/home/widgets/home-widgets.js +3 -2
  21. package/dist/dashboard/home/widgets/home-widgets.js.map +1 -1
  22. package/dist/dashboard/home/widgets/host-widgets.d.ts +6 -0
  23. package/dist/dashboard/home/widgets/host-widgets.js +16 -0
  24. package/dist/dashboard/home/widgets/host-widgets.js.map +1 -0
  25. package/dist/dashboard/home/widgets/member-label.d.ts +11 -0
  26. package/dist/dashboard/home/widgets/member-label.js +10 -0
  27. package/dist/dashboard/home/widgets/member-label.js.map +1 -0
  28. package/dist/dashboard/home/widgets/middle-ellipsis.d.ts +5 -0
  29. package/dist/dashboard/home/widgets/middle-ellipsis.js +11 -0
  30. package/dist/dashboard/home/widgets/middle-ellipsis.js.map +1 -0
  31. package/dist/dashboard/home/widgets/money-widgets.js +9 -4
  32. package/dist/dashboard/home/widgets/money-widgets.js.map +1 -1
  33. package/dist/dashboard/home/widgets/registry.d.ts +6 -3
  34. package/dist/dashboard/home/widgets/registry.js +12 -2
  35. package/dist/dashboard/home/widgets/registry.js.map +1 -1
  36. package/dist/dashboard/home/widgets/revenue-series.d.ts +12 -0
  37. package/dist/dashboard/home/widgets/revenue-series.js +26 -0
  38. package/dist/dashboard/home/widgets/revenue-series.js.map +1 -0
  39. package/dist/dashboard/home/widgets/team-widget.js +2 -1
  40. package/dist/dashboard/home/widgets/team-widget.js.map +1 -1
  41. package/dist/dashboard/home/widgets/types.d.ts +13 -1
  42. package/dist/dashboard/home/widgets/types.js.map +1 -1
  43. package/dist/dashboard/home/widgets/use-home-layout.js +9 -4
  44. package/dist/dashboard/home/widgets/use-home-layout.js.map +1 -1
  45. package/dist/dashboard/home/widgets/website-widget.js +9 -1
  46. package/dist/dashboard/home/widgets/website-widget.js.map +1 -1
  47. package/dist/features/gift-card-artwork.js +1 -1
  48. package/dist/features/gift-card-artwork.js.map +1 -1
  49. package/dist/features/gift-cards/gift-card-logic.d.ts +41 -0
  50. package/dist/features/gift-cards/gift-card-logic.js +65 -0
  51. package/dist/features/gift-cards/gift-card-logic.js.map +1 -0
  52. package/dist/features/gift-cards/gift-cards-feature.js +656 -518
  53. package/dist/features/gift-cards/gift-cards-feature.js.map +1 -1
  54. package/dist/features/gift-cards/hint.d.ts +9 -0
  55. package/dist/features/gift-cards/hint.js +25 -0
  56. package/dist/features/gift-cards/hint.js.map +1 -0
  57. package/dist/features/product-form.js +1 -1
  58. package/dist/features/product-form.js.map +1 -1
  59. package/dist/sidebar/nav-renderers.js +2 -0
  60. package/dist/sidebar/nav-renderers.js.map +1 -1
  61. package/dist/sidebar/nav-section.js +5 -3
  62. package/dist/sidebar/nav-section.js.map +1 -1
  63. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/features/product-form.tsx"],"sourcesContent":["// @ts-nocheck — Legacy product form, no longer imported. Kept for reference only.\n'use client';\n\n/**\n * @deprecated Use ContentTypeStepperPanel + useProductFormState + useApiDispatch instead.\n *\n * This component is retained only for the product detail edit pages\n * (inventory/[productId]/page.tsx). New creation flows should use\n * ContentTypeStepperPanel with productMode props.\n */\n\nimport { useState, useEffect, useMemo, useCallback } from 'react';\nimport {\n PointerSensor,\n KeyboardSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core';\nimport { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { useCurrentTemplate } from '@withpotter/lib/template-context';\nimport { getCurrencySymbol } from '@withpotter/components/dynamic-form';\nimport { contentApi } from '@withpotter/lib/api/content';\nimport { inventoryApi } from '@withpotter/lib/api/inventory';\nimport { merchantSubscriptionsApi } from '@withpotter/lib/api/merchant-subscriptions';\nimport { resourceCategoriesApi } from '@withpotter/lib/api/resource-categories';\nimport type { MerchantPlan } from '@withpotter/lib/api/types';\nimport { ContentItem } from '@withpotter/lib/api/types';\nimport {\n ProductType,\n generateSlug,\n type CreateProductPayload,\n type Product,\n type ProductMedia,\n type ProductBehaviors,\n type BookableConfig,\n type ShippableConfig,\n type SubscribableConfig,\n type DownloadableConfig,\n type RentableConfig,\n type TicketedConfig,\n isBehaviorEnabled,\n} from '@withpotter/lib/product-types';\nimport type { ContentResourceRequirementItem } from '@withpotter/lib/resource-types';\nimport {\n FormStepperPanel,\n type FormStep,\n} from '@withpotter/ui-kit/form-stepper-panel';\nimport {\n type BehaviorKey,\n type BehaviorLabelOverrides,\n} from '@withpotter/components/behaviors/behavior-config-section';\nimport { createBasicsStep } from './product-form/basics-step';\nimport {\n createBehaviorConfigPanels,\n createBehaviorsStep,\n} from './product-form/behaviors-step';\nimport { createDetailsStep } from './product-form/details-step';\nimport { createOrganizeStep } from './product-form/organize-step';\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\ninterface ProductFormProps {\n /** Existing product to edit, or null for create mode */\n product?: Product | null;\n /** Called after successful save with the saved product */\n onSaved: () => void;\n /** Called when the form is closed/cancelled */\n onClose: () => void;\n}\n\n// Platform field names that are extracted from formValues and sent as top-level payload fields\n// Fields that are rendered manually in the platform section below.\n// All other template fields pass through to DynamicForm (e.g. category,\n// tags, gallery, shortDescription, allergens, dietary, etc.).\nconst PLATFORM_FIELDS = new Set([\n 'title',\n 'slug',\n 'description',\n 'featured',\n 'isActive',\n 'image',\n 'gallery',\n 'price',\n 'compareAtPrice',\n 'currency',\n 'trackInventory',\n 'quantity',\n 'lowStockThreshold',\n 'type',\n]);\n\nfunction normalizeMediaItem(input: unknown): ProductMedia | null {\n if (!input) return null;\n if (typeof input === 'string') {\n return { url: input, type: 'image' };\n }\n\n if (typeof input !== 'object') return null;\n const item = input as Record<string, unknown>;\n if (typeof item.url !== 'string' || item.url.trim().length === 0) return null;\n\n return {\n url: item.url,\n type: item.type === 'video' ? 'video' : 'image',\n alt: typeof item.alt === 'string' ? item.alt : undefined,\n width: typeof item.width === 'number' ? item.width : undefined,\n height: typeof item.height === 'number' ? item.height : undefined,\n thumbnail: typeof item.thumbnail === 'string' ? item.thumbnail : undefined,\n duration: typeof item.duration === 'number' ? item.duration : undefined,\n };\n}\n\nfunction normalizeMediaList(input: unknown): ProductMedia[] {\n if (!Array.isArray(input)) return [];\n return input\n .map((item) => normalizeMediaItem(item))\n .filter((item): item is ProductMedia => item !== null);\n}\n\ninterface ProductGalleryItem extends ProductMedia {\n id: string;\n}\n\nfunction createGalleryItemId(url: string): string {\n return `${url}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction toGalleryItems(media: ProductMedia[]): ProductGalleryItem[] {\n return media.map((item) => ({\n ...item,\n id: createGalleryItemId(item.url),\n }));\n}\n\nfunction stripGalleryItemIds(items: ProductGalleryItem[]): ProductMedia[] {\n return items.map(({ id, ...rest }) => rest);\n}\n\n// ============================================================================\n// COMPONENT\n// ============================================================================\n\nexport function ProductForm({ product, onSaved, onClose }: ProductFormProps) {\n const { tenant } = useAuth();\n const { template } = useCurrentTemplate(tenant?.templateId);\n const sellableContentType = null as any;\n\n // Form state\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);\n\n // Platform field values\n const [productType, setProductType] = useState<ProductType>(\n (product?.type as ProductType) || ProductType.PHYSICAL,\n );\n const [title, setTitle] = useState(product?.title || '');\n const [slug, setSlug] = useState(product?.slug || '');\n const [description, setDescription] = useState(product?.description || '');\n const [price, setPrice] = useState<number | undefined>(\n product?.price != null ? Number(product.price) : undefined,\n );\n const [compareAtPrice, setCompareAtPrice] = useState<number | undefined>(\n product?.compareAtPrice != null\n ? Number(product.compareAtPrice)\n : undefined,\n );\n const [trackInventory, setTrackInventory] = useState(\n product?.trackInventory ?? true,\n );\n const [quantity, setQuantity] = useState<number | ''>(product?.quantity ?? 0);\n const [lowStockThreshold, setLowStockThreshold] = useState<number | ''>(\n product?.lowStockThreshold ?? 5,\n );\n const [featured, setFeatured] = useState(product?.featured ?? false);\n const [isActive, setIsActive] = useState(product?.isActive ?? true);\n const [heroImage, setHeroImage] = useState<ProductMedia | null>(\n normalizeMediaItem(product?.image) || null,\n );\n const [galleryItems, setGalleryItems] = useState<ProductGalleryItem[]>(() =>\n toGalleryItems(normalizeMediaList(product?.gallery)),\n );\n const [showHeroImageModal, setShowHeroImageModal] = useState(false);\n const [showGalleryMediaModal, setShowGalleryMediaModal] = useState(false);\n const [editingGalleryIndex, setEditingGalleryIndex] = useState<number | null>(\n null,\n );\n\n const gallerySensors = useSensors(\n useSensor(PointerSensor, {\n activationConstraint: { distance: 4 },\n }),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n\n const handleGalleryDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const oldIndex = galleryItems.findIndex(\n (item) => item.id === String(active.id),\n );\n const newIndex = galleryItems.findIndex(\n (item) => item.id === String(over.id),\n );\n if (oldIndex < 0 || newIndex < 0) return;\n setGalleryItems((prev) => arrayMove(prev, oldIndex, newIndex));\n },\n [galleryItems],\n );\n\n // Collection assignment state\n const [allCollections, setAllCollections] = useState<ContentItem[]>([]);\n const [selectedCollectionIds, setSelectedCollectionIds] = useState<\n Set<string>\n >(new Set());\n const [collectionsLoading, setCollectionsLoading] = useState(false);\n\n // Resource categories & requirements state (for bookable behavior)\n const [resourceCategories, setResourceCategories] = useState<\n Array<{ id: string; label: string; labelPlural: string; kind: string }>\n >([]);\n const [resourceRequirements, setResourceRequirements] = useState<\n ContentResourceRequirementItem[]\n >([]);\n const [activePlans, setActivePlans] = useState<MerchantPlan[]>([]);\n\n // Template field values (stored in `data`)\n const [templateValues, setTemplateValues] = useState<Record<string, unknown>>(\n product?.data || {},\n );\n\n const isEditing = !!product;\n\n // Get tenant currency\n const tenantCurrency = useMemo(() => {\n const settings = tenant?.settings as Record<string, unknown> | undefined;\n return (settings?.currency as string) || 'USD';\n }, [tenant?.settings]);\n\n // Get tenant categories from settings, falling back to template defaults\n const tenantCategories = useMemo(() => {\n const saved =\n ((tenant?.settings as Record<string, unknown>)?.categories as Record<\n string,\n { value: string; label: string; sortOrder?: number }[]\n >) || {};\n\n // If tenant already has saved categories, use them\n if (Object.keys(saved).length > 0) return saved;\n\n // Fall back to defaults from template's categoryManager siteSettings\n if (!template?.siteSettings) return saved;\n const defaults: Record<\n string,\n { value: string; label: string; sortOrder?: number }[]\n > = {};\n for (const [, section] of Object.entries(template.siteSettings)) {\n const s = section as unknown as {\n type?: string;\n contentTypeKey?: string;\n defaults?: { value: string; label: string; sortOrder?: number }[];\n };\n if (s?.type === 'categoryManager' && s.contentTypeKey && s.defaults) {\n defaults[s.contentTypeKey] = [...s.defaults];\n }\n }\n return Object.keys(defaults).length > 0 ? defaults : saved;\n }, [tenant?.settings, template?.siteSettings]);\n\n // Currency symbol helper (moved up — needed by behavior config panels)\n const currencySymbol = useMemo(\n () => getCurrencySymbol(tenantCurrency),\n [tenantCurrency],\n );\n\n // Industry-defined fields from the content type\n const mergedFields = useMemo(() => {\n if (!sellableContentType) return [];\n return sellableContentType.fields;\n }, [sellableContentType]);\n\n // Filter out platform fields that we render manually — only show template-specific fields\n const templateOnlyFields = useMemo(() => {\n return mergedFields.filter((f) => !PLATFORM_FIELDS.has(f.name));\n }, [mergedFields]);\n\n // ── Suggested detail sections (opt-in template fields) ──\n // Build a list of toggleable sections from template field groups + ungrouped fields\n const detailSections = useMemo(() => {\n const groups = sellableContentType?.fieldGroups || [];\n const groupedFieldNames = new Set(groups.flatMap((g) => g.fields));\n const ungrouped = templateOnlyFields.filter(\n (f) => !groupedFieldNames.has(f.name),\n );\n\n const sections: Array<{\n key: string;\n label: string;\n description?: string;\n fieldNames: string[];\n }> = [];\n\n // Each field group becomes a section\n for (const g of groups) {\n const hasFields = g.fields.some((fn) =>\n templateOnlyFields.some((f) => f.name === fn),\n );\n if (hasFields) {\n sections.push({\n key: `group:${g.label}`,\n label: g.label,\n description: g.description,\n fieldNames: g.fields,\n });\n }\n }\n\n // Each ungrouped field becomes its own section\n for (const f of ungrouped) {\n sections.push({\n key: `field:${f.name}`,\n label: f.label || f.name,\n fieldNames: [f.name],\n });\n }\n\n return sections;\n }, [templateOnlyFields, sellableContentType?.fieldGroups]);\n\n // Track which sections the user has opted into\n const [enabledDetailSections, setEnabledDetailSections] = useState<\n Set<string>\n >(new Set());\n\n // On edit, auto-enable sections that already have data\n useEffect(() => {\n if (!isEditing || !product?.data || detailSections.length === 0) return;\n const data = product.data as Record<string, unknown>;\n const enabled = new Set<string>();\n for (const section of detailSections) {\n const hasData = section.fieldNames.some((fn) => {\n const val = data[fn];\n if (val === undefined || val === null || val === '') return false;\n if (Array.isArray(val) && val.length === 0) return false;\n return true;\n });\n if (hasData) enabled.add(section.key);\n }\n if (enabled.size > 0) setEnabledDetailSections(enabled);\n }, [isEditing, product?.data, detailSections]);\n\n const toggleDetailSection = useCallback((key: string) => {\n setEnabledDetailSections((prev) => {\n const next = new Set(prev);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n return next;\n });\n }, []);\n\n // Filter template fields + groups to only enabled ones for DynamicForm\n const activeTemplateFields = useMemo(() => {\n const enabledFieldNames = new Set<string>();\n for (const section of detailSections) {\n if (enabledDetailSections.has(section.key)) {\n for (const fn of section.fieldNames) enabledFieldNames.add(fn);\n }\n }\n return templateOnlyFields.filter((f) => enabledFieldNames.has(f.name));\n }, [templateOnlyFields, detailSections, enabledDetailSections]);\n\n const activeFieldGroups = useMemo(() => {\n if (!sellableContentType?.fieldGroups) return undefined;\n return sellableContentType.fieldGroups.filter((g) =>\n enabledDetailSections.has(`group:${g.label}`),\n );\n }, [sellableContentType?.fieldGroups, enabledDetailSections]);\n\n // Auto-generate slug from title (on create only)\n useEffect(() => {\n if (!isEditing && !slugManuallyEdited && title) {\n setSlug(generateSlug(title));\n }\n }, [title, isEditing, slugManuallyEdited]);\n\n // Pre-populate template values from product.data on edit\n useEffect(() => {\n setTemplateValues(product?.data || {});\n setHeroImage(normalizeMediaItem(product?.image) || null);\n setGalleryItems(toGalleryItems(normalizeMediaList(product?.gallery)));\n }, [product?.data, product?.image, product?.gallery]);\n\n // Fetch collections and pre-select those containing this product\n useEffect(() => {\n if (!tenant?.slug) return;\n let cancelled = false;\n const fetchCollections = async () => {\n setCollectionsLoading(true);\n try {\n const result = await contentApi.list(tenant.slug, 'collections', {\n limit: 200,\n });\n if (cancelled) return;\n const items = result.data || [];\n setAllCollections(items);\n\n // On edit, find which collections already contain this product\n if (product?.id) {\n const matched = new Set<string>();\n for (const c of items) {\n const ids = c.data?.productIds;\n if (Array.isArray(ids) && ids.includes(product.id)) {\n matched.add(c.id);\n }\n }\n setSelectedCollectionIds(matched);\n }\n } catch {\n // silently handle — collections picker just won't appear\n } finally {\n if (!cancelled) setCollectionsLoading(false);\n }\n };\n fetchCollections();\n return () => {\n cancelled = true;\n };\n }, [tenant?.slug, product?.id]);\n\n // Fetch resource categories (for bookable resource requirements)\n useEffect(() => {\n if (!tenant?.slug) return;\n let cancelled = false;\n const fetchCategories = async () => {\n try {\n const cats = await resourceCategoriesApi.list(tenant.slug);\n if (cancelled) return;\n setResourceCategories(\n (cats || []).map((c) => ({\n id: c.id,\n label: c.label || (c as any).name || '',\n labelPlural: c.labelPlural || '',\n kind: c.kind || 'person',\n })),\n );\n } catch {\n // silently handle — resource requirements section won't appear\n }\n };\n fetchCategories();\n return () => {\n cancelled = true;\n };\n }, [tenant?.slug]);\n\n // Fetch active MerchantPlans (for subscribable linked plan selector)\n useEffect(() => {\n if (!tenant?.slug) return;\n merchantSubscriptionsApi\n .getActivePlans(tenant.slug)\n .then((plans) => setActivePlans(plans || []))\n .catch(() => {\n // silently handle — plan selector will be hidden when plans is empty\n });\n }, [tenant?.slug]);\n\n // Fetch existing resource requirements when editing a product\n useEffect(() => {\n if (!tenant?.slug || !product?.id) return;\n let cancelled = false;\n const fetchRequirements = async () => {\n try {\n const reqs = await resourceCategoriesApi.getContentRequirements(\n tenant.slug,\n product.id,\n );\n if (cancelled) return;\n setResourceRequirements(\n (reqs || []).map((r) => ({\n categoryId: r.categoryId,\n isRequired: r.isRequired ?? true,\n assignmentStrategy: r.assignmentStrategy as\n ContentResourceRequirementItem['assignmentStrategy'] | undefined,\n requiredSkills: r.requiredSkills || [],\n eligibleResourceIds: r.eligibleResourceIds || null,\n })),\n );\n } catch {\n // silently handle — no existing requirements\n }\n };\n fetchRequirements();\n return () => {\n cancelled = true;\n };\n }, [tenant?.slug, product?.id]);\n\n // ── Behavior config state (Phase 4: config objects instead of booleans) ──\n const [behaviorConfigs, setBehaviorConfigs] = useState<ProductBehaviors>(\n () => {\n // On edit, load existing behaviors from the product\n if (product?.behaviors) {\n return product.behaviors as ProductBehaviors;\n }\n // On create, initialize from the template's content type behaviors\n if (sellableContentType?.behaviors) {\n const b: ProductBehaviors = {};\n for (const behavior of sellableContentType.behaviors) {\n (b as any)[behavior] = { enabled: true };\n }\n return b;\n }\n return { sellable: { enabled: true } };\n },\n );\n\n // Re-initialize behavior configs when the template changes (create mode)\n useEffect(() => {\n if (!isEditing && sellableContentType?.behaviors) {\n setBehaviorConfigs((prev) => {\n // Only set defaults for behaviors that aren't already configured\n const updated = { ...prev };\n for (const behavior of sellableContentType.behaviors) {\n if ((updated as any)[behavior] === undefined) {\n (updated as any)[behavior] = { enabled: true };\n }\n }\n return updated;\n });\n }\n }, [sellableContentType, isEditing]);\n\n // ── Content gating state ──\n const [gatedBy, setGatedBy] = useState<string[]>(() => {\n if (\n product?.behaviors?.gatedBy &&\n Array.isArray(product.behaviors.gatedBy)\n ) {\n return product.behaviors.gatedBy;\n }\n return [];\n });\n const [gatedByPlanIds, setGatedByPlanIds] = useState<string[]>(() => {\n if (\n product?.behaviors?.gatedByPlanIds &&\n Array.isArray(product.behaviors.gatedByPlanIds)\n ) {\n return product.behaviors.gatedByPlanIds;\n }\n return [];\n });\n\n // Derive which MerchantPlan is linked to the subscribable behavior\n const linkedPlanId = useMemo(() => {\n const config = getBehaviorConfig<SubscribableConfig>(\n behaviorConfigs.subscribable,\n );\n return config?.linkedPlanId;\n }, [behaviorConfigs.subscribable]);\n\n const linkedPlanName = useMemo(() => {\n if (!linkedPlanId || !activePlans) return undefined;\n return activePlans.find((p) => p.id === linkedPlanId)?.name;\n }, [linkedPlanId, activePlans]);\n\n // Derive which behaviors are available for the product form toggle list.\n // Priority: tenant's enabledBehaviors > template content type behaviors > fallback to all behaviors\n const availableBehaviors = useMemo<BehaviorKey[]>(() => {\n const tenantSettings = tenant?.settings as\n Record<string, unknown> | undefined;\n const tenantBehaviors = tenantSettings?.enabledBehaviors;\n\n // 1. Tenant has explicit preferences — use them\n if (Array.isArray(tenantBehaviors) && tenantBehaviors.length > 0) {\n return tenantBehaviors.filter(\n (b): b is BehaviorKey => b !== 'giftable' && b !== 'leadCapture',\n );\n }\n\n // 2. Template content type defines behaviors\n if (\n sellableContentType?.behaviors &&\n sellableContentType.behaviors.length > 0\n ) {\n const filtered = sellableContentType.behaviors.filter(\n (b): b is BehaviorKey => b !== 'giftable' && b !== 'leadCapture',\n );\n if (filtered.length > 0) return filtered;\n }\n\n // 3. Fallback — offer all behaviors by platform default\n return [\n 'sellable',\n 'bookable',\n 'shippable',\n 'subscribable',\n 'downloadable',\n 'rentable',\n 'ticketed',\n ] as BehaviorKey[];\n }, [tenant?.settings, sellableContentType]);\n\n // Template-provided behavior label overrides\n const behaviorLabelOverrides = useMemo<\n BehaviorLabelOverrides | undefined\n >(() => {\n // Templates can provide behaviorLabels in their schema\n const ct = sellableContentType as unknown as {\n behaviorLabels?: BehaviorLabelOverrides;\n };\n return ct?.behaviorLabels;\n }, [sellableContentType]);\n\n // Build flat behaviors for backward-compatible save (also used by behavior field merging)\n const behaviors = useMemo(() => {\n const b: Record<string, boolean> = {};\n for (const [key, value] of Object.entries(behaviorConfigs)) {\n if (key === 'gatedBy' || key === 'gatedByPlanIds' || key === 'reservable')\n continue;\n b[key] = isBehaviorEnabled(value);\n }\n return b;\n }, [behaviorConfigs]);\n\n // ── Behavior config update handlers ──\n const updateBookableConfig = useCallback((config: BookableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, bookable: config }));\n }, []);\n\n const updateShippableConfig = useCallback((config: ShippableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, shippable: config }));\n }, []);\n\n const updateSubscribableConfig = useCallback((config: SubscribableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, subscribable: config }));\n }, []);\n\n const updateDownloadableConfig = useCallback((config: DownloadableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, downloadable: config }));\n }, []);\n\n const updateRentableConfig = useCallback((config: RentableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, rentable: config }));\n }, []);\n\n const updateTicketedConfig = useCallback((config: TicketedConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, ticketed: config }));\n }, []);\n\n // Build config panel map for the BehaviorConfigSection\n const behaviorConfigPanels = useMemo(\n () =>\n createBehaviorConfigPanels({\n behaviorConfigs,\n updateBookableConfig,\n updateShippableConfig,\n updateSubscribableConfig,\n updateDownloadableConfig,\n updateRentableConfig,\n updateTicketedConfig,\n behaviorLabelOverrides,\n currencySymbol,\n resourceCategories,\n resourceRequirements,\n setResourceRequirements,\n activePlans,\n }),\n [\n behaviorConfigs,\n updateBookableConfig,\n updateShippableConfig,\n updateSubscribableConfig,\n updateDownloadableConfig,\n updateRentableConfig,\n updateTicketedConfig,\n behaviorLabelOverrides,\n currencySymbol,\n resourceCategories,\n resourceRequirements,\n ],\n );\n\n // Handle save\n const handleSave = useCallback(async () => {\n if (!tenant?.slug) return;\n\n // Basic validation\n if (!title.trim()) {\n setError('Title is required');\n return;\n }\n if (!slug.trim()) {\n setError('Slug is required');\n return;\n }\n\n setSaving(true);\n setError(null);\n\n try {\n const payload: CreateProductPayload = {\n type: productType,\n title: title.trim(),\n slug: slug.trim(),\n description: description || undefined,\n price: price !== undefined ? Number(price) || 0 : undefined,\n compareAtPrice:\n compareAtPrice !== undefined\n ? Number(compareAtPrice) || 0\n : undefined,\n currency: tenantCurrency,\n trackInventory,\n quantity: typeof quantity === 'number' ? quantity : 0,\n lowStockThreshold:\n typeof lowStockThreshold === 'number' ? lowStockThreshold : 5,\n featured,\n isActive,\n image: heroImage\n ? ({\n ...heroImage,\n type: 'image',\n } as ProductMedia)\n : undefined,\n // An empty list on an edit is sent as [], not left out: the API reads\n // an omitted gallery as \"unchanged\", so removing the last item would\n // save successfully and bring it straight back.\n gallery:\n galleryItems.length > 0\n ? stripGalleryItemIds(galleryItems)\n : isEditing\n ? []\n : undefined,\n behaviors: {\n ...behaviorConfigs,\n ...(gatedBy.length > 0 && { gatedBy }),\n ...(gatedByPlanIds.length > 0 && { gatedByPlanIds }),\n },\n data: templateValues,\n };\n\n let productId: string | undefined;\n\n if (isEditing && product) {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { type, ...updatePayload } = payload;\n await inventoryApi.update(\n tenant.slug,\n product.id,\n updatePayload as Record<string, unknown>,\n );\n productId = product.id;\n } else {\n const created = await inventoryApi.create(\n tenant.slug,\n payload as unknown as Record<string, unknown>,\n );\n productId = created.id;\n }\n\n // Sync collection assignments — add/remove this product from collections\n if (productId && allCollections.length > 0) {\n const updates: Promise<unknown>[] = [];\n for (const collection of allCollections) {\n const currentIds: string[] = Array.isArray(\n collection.data?.productIds,\n )\n ? (collection.data.productIds as string[])\n : [];\n const hasProduct = currentIds.includes(productId);\n const shouldHave = selectedCollectionIds.has(collection.id);\n\n if (shouldHave && !hasProduct) {\n // Add product to this collection\n updates.push(\n contentApi.update(tenant.slug, 'collections', collection.id, {\n data: {\n ...collection.data,\n productIds: [...currentIds, productId],\n },\n }),\n );\n } else if (!shouldHave && hasProduct) {\n // Remove product from this collection\n updates.push(\n contentApi.update(tenant.slug, 'collections', collection.id, {\n data: {\n ...collection.data,\n productIds: currentIds.filter((id) => id !== productId),\n },\n }),\n );\n }\n }\n if (updates.length > 0) {\n await Promise.all(updates);\n }\n }\n\n // Save resource requirements if bookable behavior is active\n if (\n productId &&\n isBehaviorEnabled(behaviorConfigs.bookable) &&\n resourceRequirements.length > 0\n ) {\n try {\n await resourceCategoriesApi.setContentRequirements(\n tenant.slug,\n productId,\n { requirements: resourceRequirements },\n );\n } catch {\n // Non-blocking — product was saved, requirements just didn't persist\n console.warn('Failed to save resource requirements');\n }\n }\n\n onSaved();\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : 'Failed to save product';\n setError(msg);\n } finally {\n setSaving(false);\n }\n }, [\n tenant?.slug,\n productType,\n title,\n slug,\n description,\n price,\n compareAtPrice,\n tenantCurrency,\n trackInventory,\n quantity,\n lowStockThreshold,\n featured,\n isActive,\n heroImage,\n galleryItems,\n behaviorConfigs,\n gatedBy,\n gatedByPlanIds,\n templateValues,\n isEditing,\n product,\n onSaved,\n allCollections,\n selectedCollectionIds,\n resourceRequirements,\n ]);\n\n // Currency formatting helpers\n const formatCurrency = (num: number | undefined): string => {\n if (num === undefined || num === null || isNaN(num)) return '';\n return new Intl.NumberFormat('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(num);\n };\n\n const parseCurrency = (str: string): number | undefined => {\n const cleaned = str.replace(/[^0-9.]/g, '');\n if (!cleaned) return undefined;\n const num = parseFloat(cleaned);\n return isNaN(num) ? undefined : num;\n };\n\n // Display state for currency fields\n const [priceDisplay, setPriceDisplay] = useState(() =>\n formatCurrency(product?.price != null ? Number(product.price) : undefined),\n );\n const [priceFocused, setPriceFocused] = useState(false);\n const [compareDisplay, setCompareDisplay] = useState(() =>\n formatCurrency(\n product?.compareAtPrice != null\n ? Number(product.compareAtPrice)\n : undefined,\n ),\n );\n const [compareFocused, setCompareFocused] = useState(false);\n\n // Build steps for the stepper\n const steps = useMemo<FormStep[]>(() => {\n const result: FormStep[] = [];\n\n // Step 1: Title, Image & Pricing\n result.push(\n createBasicsStep({\n title,\n setTitle,\n heroImage,\n setHeroImage,\n showHeroImageModal,\n setShowHeroImageModal,\n galleryItems,\n setGalleryItems,\n gallerySensors,\n handleGalleryDragEnd,\n showGalleryMediaModal,\n setShowGalleryMediaModal,\n editingGalleryIndex,\n setEditingGalleryIndex,\n description,\n setDescription,\n currencySymbol,\n priceFocused,\n setPriceFocused,\n priceDisplay,\n setPriceDisplay,\n price,\n setPrice,\n compareFocused,\n setCompareFocused,\n compareDisplay,\n setCompareDisplay,\n compareAtPrice,\n setCompareAtPrice,\n formatCurrency,\n parseCurrency,\n }),\n );\n\n // Step 2: Behaviors — What can customers do with this product?\n result.push(\n createBehaviorsStep({\n behaviorConfigs,\n setBehaviorConfigs,\n behaviorLabelOverrides,\n availableBehaviors,\n behaviorConfigPanels,\n gatedBy,\n setGatedBy,\n gatedByPlanIds,\n setGatedByPlanIds,\n activePlans,\n linkedPlanName,\n }),\n );\n\n // Step 3: Product Details & Inventory\n result.push(\n createDetailsStep({\n behaviorConfigs,\n detailSections,\n enabledDetailSections,\n toggleDetailSection,\n activeTemplateFields,\n activeFieldGroups,\n templateValues,\n setTemplateValues,\n tenantCurrency,\n tenantCategories,\n tenantSlug: tenant?.slug,\n trackInventory,\n setTrackInventory,\n quantity,\n setQuantity,\n lowStockThreshold,\n setLowStockThreshold,\n }),\n );\n\n // Step 4: Collections & Organization\n result.push(\n createOrganizeStep({\n collectionsLoading,\n allCollections,\n selectedCollectionIds,\n setSelectedCollectionIds,\n isActive,\n setIsActive,\n isEditing,\n productType,\n setProductType,\n slug,\n setSlug,\n setSlugManuallyEdited,\n product,\n featured,\n setFeatured,\n }),\n );\n\n return result;\n }, [\n title,\n heroImage,\n galleryItems,\n showHeroImageModal,\n showGalleryMediaModal,\n editingGalleryIndex,\n gallerySensors,\n handleGalleryDragEnd,\n description,\n currencySymbol,\n priceFocused,\n priceDisplay,\n price,\n compareFocused,\n compareDisplay,\n compareAtPrice,\n templateOnlyFields,\n sellableContentType,\n templateValues,\n tenantCurrency,\n tenantCategories,\n tenant?.slug,\n trackInventory,\n quantity,\n lowStockThreshold,\n collectionsLoading,\n allCollections,\n selectedCollectionIds,\n isEditing,\n productType,\n slug,\n product?.slug,\n isActive,\n featured,\n behaviorConfigs,\n behaviorLabelOverrides,\n availableBehaviors,\n behaviorConfigPanels,\n detailSections,\n enabledDetailSections,\n toggleDetailSection,\n activeTemplateFields,\n activeFieldGroups,\n gatedBy,\n gatedByPlanIds,\n linkedPlanId,\n linkedPlanName,\n ]);\n\n return (\n <FormStepperPanel\n title={isEditing ? 'Edit Product' : 'Add Product'}\n steps={steps}\n onSave={handleSave}\n onClose={onClose}\n saving={saving}\n saveLabel={isEditing ? 'Update Product' : 'Create Product'}\n error={error}\n onClearError={() => setError(null)}\n />\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/features/product-form.tsx"],"sourcesContent":["// @ts-nocheck — Legacy product form, no longer imported. Kept for reference only.\n'use client';\n\n/**\n * @deprecated Use ContentTypeStepperPanel + useProductFormState + useApiDispatch instead.\n *\n * This component is retained only for the product detail edit pages\n * (inventory/[productId]/page.tsx). New creation flows should use\n * ContentTypeStepperPanel with productMode props.\n */\n\nimport { useState, useEffect, useMemo, useCallback } from 'react';\nimport {\n PointerSensor,\n KeyboardSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core';\nimport { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { useCurrentTemplate } from '@withpotter/lib/template-context';\nimport { getCurrencySymbol } from '@withpotter/components/dynamic-form';\nimport { contentApi } from '@withpotter/lib/api/content';\nimport { inventoryApi } from '@withpotter/lib/api/inventory';\nimport { merchantSubscriptionsApi } from '@withpotter/lib/api/merchant-subscriptions';\nimport { resourceCategoriesApi } from '@withpotter/lib/api/resource-categories';\nimport type { MerchantPlan } from '@withpotter/lib/api/types';\nimport { ContentItem } from '@withpotter/lib/api/types';\nimport {\n ProductType,\n generateSlug,\n type CreateProductPayload,\n type Product,\n type ProductMedia,\n type ProductBehaviors,\n type BookableConfig,\n type ShippableConfig,\n type SubscribableConfig,\n type DownloadableConfig,\n type RentableConfig,\n type TicketedConfig,\n isBehaviorEnabled,\n} from '@withpotter/lib/product-types';\nimport type { ContentResourceRequirementItem } from '@withpotter/lib/resource-types';\nimport {\n FormStepperPanel,\n type FormStep,\n} from '@withpotter/ui-kit/form-stepper-panel';\nimport {\n type BehaviorKey,\n type BehaviorLabelOverrides,\n} from '@withpotter/components/behaviors/behavior-config-section';\nimport { createBasicsStep } from './product-form/basics-step';\nimport {\n createBehaviorConfigPanels,\n createBehaviorsStep,\n} from './product-form/behaviors-step';\nimport { createDetailsStep } from './product-form/details-step';\nimport { createOrganizeStep } from './product-form/organize-step';\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\ninterface ProductFormProps {\n /** Existing product to edit, or null for create mode */\n product?: Product | null;\n /** Called after successful save with the saved product */\n onSaved: () => void;\n /** Called when the form is closed/cancelled */\n onClose: () => void;\n}\n\n// Platform field names that are extracted from formValues and sent as top-level payload fields\n// Fields that are rendered manually in the platform section below.\n// All other template fields pass through to DynamicForm (e.g. category,\n// tags, gallery, shortDescription, allergens, dietary, etc.).\nconst PLATFORM_FIELDS = new Set([\n 'title',\n 'slug',\n 'description',\n 'featured',\n 'isActive',\n 'image',\n 'gallery',\n 'price',\n 'compareAtPrice',\n 'currency',\n 'trackInventory',\n 'quantity',\n 'lowStockThreshold',\n 'type',\n]);\n\nfunction normalizeMediaItem(input: unknown): ProductMedia | null {\n if (!input) return null;\n if (typeof input === 'string') {\n return { url: input, type: 'image' };\n }\n\n if (typeof input !== 'object') return null;\n const item = input as Record<string, unknown>;\n if (typeof item.url !== 'string' || item.url.trim().length === 0) return null;\n\n return {\n url: item.url,\n type: item.type === 'video' ? 'video' : 'image',\n alt: typeof item.alt === 'string' ? item.alt : undefined,\n width: typeof item.width === 'number' ? item.width : undefined,\n height: typeof item.height === 'number' ? item.height : undefined,\n thumbnail: typeof item.thumbnail === 'string' ? item.thumbnail : undefined,\n duration: typeof item.duration === 'number' ? item.duration : undefined,\n };\n}\n\nfunction normalizeMediaList(input: unknown): ProductMedia[] {\n if (!Array.isArray(input)) return [];\n return input\n .map((item) => normalizeMediaItem(item))\n .filter((item): item is ProductMedia => item !== null);\n}\n\ninterface ProductGalleryItem extends ProductMedia {\n id: string;\n}\n\nfunction createGalleryItemId(url: string): string {\n return `${url}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction toGalleryItems(media: ProductMedia[]): ProductGalleryItem[] {\n return media.map((item) => ({\n ...item,\n id: createGalleryItemId(item.url),\n }));\n}\n\nfunction stripGalleryItemIds(items: ProductGalleryItem[]): ProductMedia[] {\n return items.map(({ id, ...rest }) => rest);\n}\n\n// ============================================================================\n// COMPONENT\n// ============================================================================\n\nexport function ProductForm({ product, onSaved, onClose }: ProductFormProps) {\n const { tenant } = useAuth();\n const { template } = useCurrentTemplate(tenant?.templateId);\n const sellableContentType = null as any;\n\n // Form state\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);\n\n // Platform field values\n const [productType, setProductType] = useState<ProductType>(\n (product?.type as ProductType) || ProductType.PHYSICAL,\n );\n const [title, setTitle] = useState(product?.title || '');\n const [slug, setSlug] = useState(product?.slug || '');\n const [description, setDescription] = useState(product?.description || '');\n const [price, setPrice] = useState<number | undefined>(\n product?.price != null ? Number(product.price) : undefined,\n );\n const [compareAtPrice, setCompareAtPrice] = useState<number | undefined>(\n product?.compareAtPrice != null\n ? Number(product.compareAtPrice)\n : undefined,\n );\n const [trackInventory, setTrackInventory] = useState(\n product?.trackInventory ?? true,\n );\n const [quantity, setQuantity] = useState<number | ''>(product?.quantity ?? 0);\n const [lowStockThreshold, setLowStockThreshold] = useState<number | ''>(\n product?.lowStockThreshold ?? 5,\n );\n const [featured, setFeatured] = useState(product?.featured ?? false);\n const [isActive, setIsActive] = useState(product?.isActive ?? true);\n const [heroImage, setHeroImage] = useState<ProductMedia | null>(\n normalizeMediaItem(product?.image) || null,\n );\n const [galleryItems, setGalleryItems] = useState<ProductGalleryItem[]>(() =>\n toGalleryItems(normalizeMediaList(product?.gallery)),\n );\n const [showHeroImageModal, setShowHeroImageModal] = useState(false);\n const [showGalleryMediaModal, setShowGalleryMediaModal] = useState(false);\n const [editingGalleryIndex, setEditingGalleryIndex] = useState<number | null>(\n null,\n );\n\n const gallerySensors = useSensors(\n useSensor(PointerSensor, {\n activationConstraint: { distance: 4 },\n }),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n\n const handleGalleryDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const oldIndex = galleryItems.findIndex(\n (item) => item.id === String(active.id),\n );\n const newIndex = galleryItems.findIndex(\n (item) => item.id === String(over.id),\n );\n if (oldIndex < 0 || newIndex < 0) return;\n setGalleryItems((prev) => arrayMove(prev, oldIndex, newIndex));\n },\n [galleryItems],\n );\n\n // Collection assignment state\n const [allCollections, setAllCollections] = useState<ContentItem[]>([]);\n const [selectedCollectionIds, setSelectedCollectionIds] = useState<\n Set<string>\n >(new Set());\n const [collectionsLoading, setCollectionsLoading] = useState(false);\n\n // Resource categories & requirements state (for bookable behavior)\n const [resourceCategories, setResourceCategories] = useState<\n Array<{ id: string; label: string; labelPlural: string; kind: string }>\n >([]);\n const [resourceRequirements, setResourceRequirements] = useState<\n ContentResourceRequirementItem[]\n >([]);\n const [activePlans, setActivePlans] = useState<MerchantPlan[]>([]);\n\n // Template field values (stored in `data`)\n const [templateValues, setTemplateValues] = useState<Record<string, unknown>>(\n product?.data || {},\n );\n\n const isEditing = !!product;\n\n // Get tenant currency\n const tenantCurrency = useMemo(() => {\n const settings = tenant?.settings as Record<string, unknown> | undefined;\n return (settings?.currency as string) || 'NGN';\n }, [tenant?.settings]);\n\n // Get tenant categories from settings, falling back to template defaults\n const tenantCategories = useMemo(() => {\n const saved =\n ((tenant?.settings as Record<string, unknown>)?.categories as Record<\n string,\n { value: string; label: string; sortOrder?: number }[]\n >) || {};\n\n // If tenant already has saved categories, use them\n if (Object.keys(saved).length > 0) return saved;\n\n // Fall back to defaults from template's categoryManager siteSettings\n if (!template?.siteSettings) return saved;\n const defaults: Record<\n string,\n { value: string; label: string; sortOrder?: number }[]\n > = {};\n for (const [, section] of Object.entries(template.siteSettings)) {\n const s = section as unknown as {\n type?: string;\n contentTypeKey?: string;\n defaults?: { value: string; label: string; sortOrder?: number }[];\n };\n if (s?.type === 'categoryManager' && s.contentTypeKey && s.defaults) {\n defaults[s.contentTypeKey] = [...s.defaults];\n }\n }\n return Object.keys(defaults).length > 0 ? defaults : saved;\n }, [tenant?.settings, template?.siteSettings]);\n\n // Currency symbol helper (moved up — needed by behavior config panels)\n const currencySymbol = useMemo(\n () => getCurrencySymbol(tenantCurrency),\n [tenantCurrency],\n );\n\n // Industry-defined fields from the content type\n const mergedFields = useMemo(() => {\n if (!sellableContentType) return [];\n return sellableContentType.fields;\n }, [sellableContentType]);\n\n // Filter out platform fields that we render manually — only show template-specific fields\n const templateOnlyFields = useMemo(() => {\n return mergedFields.filter((f) => !PLATFORM_FIELDS.has(f.name));\n }, [mergedFields]);\n\n // ── Suggested detail sections (opt-in template fields) ──\n // Build a list of toggleable sections from template field groups + ungrouped fields\n const detailSections = useMemo(() => {\n const groups = sellableContentType?.fieldGroups || [];\n const groupedFieldNames = new Set(groups.flatMap((g) => g.fields));\n const ungrouped = templateOnlyFields.filter(\n (f) => !groupedFieldNames.has(f.name),\n );\n\n const sections: Array<{\n key: string;\n label: string;\n description?: string;\n fieldNames: string[];\n }> = [];\n\n // Each field group becomes a section\n for (const g of groups) {\n const hasFields = g.fields.some((fn) =>\n templateOnlyFields.some((f) => f.name === fn),\n );\n if (hasFields) {\n sections.push({\n key: `group:${g.label}`,\n label: g.label,\n description: g.description,\n fieldNames: g.fields,\n });\n }\n }\n\n // Each ungrouped field becomes its own section\n for (const f of ungrouped) {\n sections.push({\n key: `field:${f.name}`,\n label: f.label || f.name,\n fieldNames: [f.name],\n });\n }\n\n return sections;\n }, [templateOnlyFields, sellableContentType?.fieldGroups]);\n\n // Track which sections the user has opted into\n const [enabledDetailSections, setEnabledDetailSections] = useState<\n Set<string>\n >(new Set());\n\n // On edit, auto-enable sections that already have data\n useEffect(() => {\n if (!isEditing || !product?.data || detailSections.length === 0) return;\n const data = product.data as Record<string, unknown>;\n const enabled = new Set<string>();\n for (const section of detailSections) {\n const hasData = section.fieldNames.some((fn) => {\n const val = data[fn];\n if (val === undefined || val === null || val === '') return false;\n if (Array.isArray(val) && val.length === 0) return false;\n return true;\n });\n if (hasData) enabled.add(section.key);\n }\n if (enabled.size > 0) setEnabledDetailSections(enabled);\n }, [isEditing, product?.data, detailSections]);\n\n const toggleDetailSection = useCallback((key: string) => {\n setEnabledDetailSections((prev) => {\n const next = new Set(prev);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n return next;\n });\n }, []);\n\n // Filter template fields + groups to only enabled ones for DynamicForm\n const activeTemplateFields = useMemo(() => {\n const enabledFieldNames = new Set<string>();\n for (const section of detailSections) {\n if (enabledDetailSections.has(section.key)) {\n for (const fn of section.fieldNames) enabledFieldNames.add(fn);\n }\n }\n return templateOnlyFields.filter((f) => enabledFieldNames.has(f.name));\n }, [templateOnlyFields, detailSections, enabledDetailSections]);\n\n const activeFieldGroups = useMemo(() => {\n if (!sellableContentType?.fieldGroups) return undefined;\n return sellableContentType.fieldGroups.filter((g) =>\n enabledDetailSections.has(`group:${g.label}`),\n );\n }, [sellableContentType?.fieldGroups, enabledDetailSections]);\n\n // Auto-generate slug from title (on create only)\n useEffect(() => {\n if (!isEditing && !slugManuallyEdited && title) {\n setSlug(generateSlug(title));\n }\n }, [title, isEditing, slugManuallyEdited]);\n\n // Pre-populate template values from product.data on edit\n useEffect(() => {\n setTemplateValues(product?.data || {});\n setHeroImage(normalizeMediaItem(product?.image) || null);\n setGalleryItems(toGalleryItems(normalizeMediaList(product?.gallery)));\n }, [product?.data, product?.image, product?.gallery]);\n\n // Fetch collections and pre-select those containing this product\n useEffect(() => {\n if (!tenant?.slug) return;\n let cancelled = false;\n const fetchCollections = async () => {\n setCollectionsLoading(true);\n try {\n const result = await contentApi.list(tenant.slug, 'collections', {\n limit: 200,\n });\n if (cancelled) return;\n const items = result.data || [];\n setAllCollections(items);\n\n // On edit, find which collections already contain this product\n if (product?.id) {\n const matched = new Set<string>();\n for (const c of items) {\n const ids = c.data?.productIds;\n if (Array.isArray(ids) && ids.includes(product.id)) {\n matched.add(c.id);\n }\n }\n setSelectedCollectionIds(matched);\n }\n } catch {\n // silently handle — collections picker just won't appear\n } finally {\n if (!cancelled) setCollectionsLoading(false);\n }\n };\n fetchCollections();\n return () => {\n cancelled = true;\n };\n }, [tenant?.slug, product?.id]);\n\n // Fetch resource categories (for bookable resource requirements)\n useEffect(() => {\n if (!tenant?.slug) return;\n let cancelled = false;\n const fetchCategories = async () => {\n try {\n const cats = await resourceCategoriesApi.list(tenant.slug);\n if (cancelled) return;\n setResourceCategories(\n (cats || []).map((c) => ({\n id: c.id,\n label: c.label || (c as any).name || '',\n labelPlural: c.labelPlural || '',\n kind: c.kind || 'person',\n })),\n );\n } catch {\n // silently handle — resource requirements section won't appear\n }\n };\n fetchCategories();\n return () => {\n cancelled = true;\n };\n }, [tenant?.slug]);\n\n // Fetch active MerchantPlans (for subscribable linked plan selector)\n useEffect(() => {\n if (!tenant?.slug) return;\n merchantSubscriptionsApi\n .getActivePlans(tenant.slug)\n .then((plans) => setActivePlans(plans || []))\n .catch(() => {\n // silently handle — plan selector will be hidden when plans is empty\n });\n }, [tenant?.slug]);\n\n // Fetch existing resource requirements when editing a product\n useEffect(() => {\n if (!tenant?.slug || !product?.id) return;\n let cancelled = false;\n const fetchRequirements = async () => {\n try {\n const reqs = await resourceCategoriesApi.getContentRequirements(\n tenant.slug,\n product.id,\n );\n if (cancelled) return;\n setResourceRequirements(\n (reqs || []).map((r) => ({\n categoryId: r.categoryId,\n isRequired: r.isRequired ?? true,\n assignmentStrategy: r.assignmentStrategy as\n ContentResourceRequirementItem['assignmentStrategy'] | undefined,\n requiredSkills: r.requiredSkills || [],\n eligibleResourceIds: r.eligibleResourceIds || null,\n })),\n );\n } catch {\n // silently handle — no existing requirements\n }\n };\n fetchRequirements();\n return () => {\n cancelled = true;\n };\n }, [tenant?.slug, product?.id]);\n\n // ── Behavior config state (Phase 4: config objects instead of booleans) ──\n const [behaviorConfigs, setBehaviorConfigs] = useState<ProductBehaviors>(\n () => {\n // On edit, load existing behaviors from the product\n if (product?.behaviors) {\n return product.behaviors as ProductBehaviors;\n }\n // On create, initialize from the template's content type behaviors\n if (sellableContentType?.behaviors) {\n const b: ProductBehaviors = {};\n for (const behavior of sellableContentType.behaviors) {\n (b as any)[behavior] = { enabled: true };\n }\n return b;\n }\n return { sellable: { enabled: true } };\n },\n );\n\n // Re-initialize behavior configs when the template changes (create mode)\n useEffect(() => {\n if (!isEditing && sellableContentType?.behaviors) {\n setBehaviorConfigs((prev) => {\n // Only set defaults for behaviors that aren't already configured\n const updated = { ...prev };\n for (const behavior of sellableContentType.behaviors) {\n if ((updated as any)[behavior] === undefined) {\n (updated as any)[behavior] = { enabled: true };\n }\n }\n return updated;\n });\n }\n }, [sellableContentType, isEditing]);\n\n // ── Content gating state ──\n const [gatedBy, setGatedBy] = useState<string[]>(() => {\n if (\n product?.behaviors?.gatedBy &&\n Array.isArray(product.behaviors.gatedBy)\n ) {\n return product.behaviors.gatedBy;\n }\n return [];\n });\n const [gatedByPlanIds, setGatedByPlanIds] = useState<string[]>(() => {\n if (\n product?.behaviors?.gatedByPlanIds &&\n Array.isArray(product.behaviors.gatedByPlanIds)\n ) {\n return product.behaviors.gatedByPlanIds;\n }\n return [];\n });\n\n // Derive which MerchantPlan is linked to the subscribable behavior\n const linkedPlanId = useMemo(() => {\n const config = getBehaviorConfig<SubscribableConfig>(\n behaviorConfigs.subscribable,\n );\n return config?.linkedPlanId;\n }, [behaviorConfigs.subscribable]);\n\n const linkedPlanName = useMemo(() => {\n if (!linkedPlanId || !activePlans) return undefined;\n return activePlans.find((p) => p.id === linkedPlanId)?.name;\n }, [linkedPlanId, activePlans]);\n\n // Derive which behaviors are available for the product form toggle list.\n // Priority: tenant's enabledBehaviors > template content type behaviors > fallback to all behaviors\n const availableBehaviors = useMemo<BehaviorKey[]>(() => {\n const tenantSettings = tenant?.settings as\n Record<string, unknown> | undefined;\n const tenantBehaviors = tenantSettings?.enabledBehaviors;\n\n // 1. Tenant has explicit preferences — use them\n if (Array.isArray(tenantBehaviors) && tenantBehaviors.length > 0) {\n return tenantBehaviors.filter(\n (b): b is BehaviorKey => b !== 'giftable' && b !== 'leadCapture',\n );\n }\n\n // 2. Template content type defines behaviors\n if (\n sellableContentType?.behaviors &&\n sellableContentType.behaviors.length > 0\n ) {\n const filtered = sellableContentType.behaviors.filter(\n (b): b is BehaviorKey => b !== 'giftable' && b !== 'leadCapture',\n );\n if (filtered.length > 0) return filtered;\n }\n\n // 3. Fallback — offer all behaviors by platform default\n return [\n 'sellable',\n 'bookable',\n 'shippable',\n 'subscribable',\n 'downloadable',\n 'rentable',\n 'ticketed',\n ] as BehaviorKey[];\n }, [tenant?.settings, sellableContentType]);\n\n // Template-provided behavior label overrides\n const behaviorLabelOverrides = useMemo<\n BehaviorLabelOverrides | undefined\n >(() => {\n // Templates can provide behaviorLabels in their schema\n const ct = sellableContentType as unknown as {\n behaviorLabels?: BehaviorLabelOverrides;\n };\n return ct?.behaviorLabels;\n }, [sellableContentType]);\n\n // Build flat behaviors for backward-compatible save (also used by behavior field merging)\n const behaviors = useMemo(() => {\n const b: Record<string, boolean> = {};\n for (const [key, value] of Object.entries(behaviorConfigs)) {\n if (key === 'gatedBy' || key === 'gatedByPlanIds' || key === 'reservable')\n continue;\n b[key] = isBehaviorEnabled(value);\n }\n return b;\n }, [behaviorConfigs]);\n\n // ── Behavior config update handlers ──\n const updateBookableConfig = useCallback((config: BookableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, bookable: config }));\n }, []);\n\n const updateShippableConfig = useCallback((config: ShippableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, shippable: config }));\n }, []);\n\n const updateSubscribableConfig = useCallback((config: SubscribableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, subscribable: config }));\n }, []);\n\n const updateDownloadableConfig = useCallback((config: DownloadableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, downloadable: config }));\n }, []);\n\n const updateRentableConfig = useCallback((config: RentableConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, rentable: config }));\n }, []);\n\n const updateTicketedConfig = useCallback((config: TicketedConfig) => {\n setBehaviorConfigs((prev) => ({ ...prev, ticketed: config }));\n }, []);\n\n // Build config panel map for the BehaviorConfigSection\n const behaviorConfigPanels = useMemo(\n () =>\n createBehaviorConfigPanels({\n behaviorConfigs,\n updateBookableConfig,\n updateShippableConfig,\n updateSubscribableConfig,\n updateDownloadableConfig,\n updateRentableConfig,\n updateTicketedConfig,\n behaviorLabelOverrides,\n currencySymbol,\n resourceCategories,\n resourceRequirements,\n setResourceRequirements,\n activePlans,\n }),\n [\n behaviorConfigs,\n updateBookableConfig,\n updateShippableConfig,\n updateSubscribableConfig,\n updateDownloadableConfig,\n updateRentableConfig,\n updateTicketedConfig,\n behaviorLabelOverrides,\n currencySymbol,\n resourceCategories,\n resourceRequirements,\n ],\n );\n\n // Handle save\n const handleSave = useCallback(async () => {\n if (!tenant?.slug) return;\n\n // Basic validation\n if (!title.trim()) {\n setError('Title is required');\n return;\n }\n if (!slug.trim()) {\n setError('Slug is required');\n return;\n }\n\n setSaving(true);\n setError(null);\n\n try {\n const payload: CreateProductPayload = {\n type: productType,\n title: title.trim(),\n slug: slug.trim(),\n description: description || undefined,\n price: price !== undefined ? Number(price) || 0 : undefined,\n compareAtPrice:\n compareAtPrice !== undefined\n ? Number(compareAtPrice) || 0\n : undefined,\n currency: tenantCurrency,\n trackInventory,\n quantity: typeof quantity === 'number' ? quantity : 0,\n lowStockThreshold:\n typeof lowStockThreshold === 'number' ? lowStockThreshold : 5,\n featured,\n isActive,\n image: heroImage\n ? ({\n ...heroImage,\n type: 'image',\n } as ProductMedia)\n : undefined,\n // An empty list on an edit is sent as [], not left out: the API reads\n // an omitted gallery as \"unchanged\", so removing the last item would\n // save successfully and bring it straight back.\n gallery:\n galleryItems.length > 0\n ? stripGalleryItemIds(galleryItems)\n : isEditing\n ? []\n : undefined,\n behaviors: {\n ...behaviorConfigs,\n ...(gatedBy.length > 0 && { gatedBy }),\n ...(gatedByPlanIds.length > 0 && { gatedByPlanIds }),\n },\n data: templateValues,\n };\n\n let productId: string | undefined;\n\n if (isEditing && product) {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { type, ...updatePayload } = payload;\n await inventoryApi.update(\n tenant.slug,\n product.id,\n updatePayload as Record<string, unknown>,\n );\n productId = product.id;\n } else {\n const created = await inventoryApi.create(\n tenant.slug,\n payload as unknown as Record<string, unknown>,\n );\n productId = created.id;\n }\n\n // Sync collection assignments — add/remove this product from collections\n if (productId && allCollections.length > 0) {\n const updates: Promise<unknown>[] = [];\n for (const collection of allCollections) {\n const currentIds: string[] = Array.isArray(\n collection.data?.productIds,\n )\n ? (collection.data.productIds as string[])\n : [];\n const hasProduct = currentIds.includes(productId);\n const shouldHave = selectedCollectionIds.has(collection.id);\n\n if (shouldHave && !hasProduct) {\n // Add product to this collection\n updates.push(\n contentApi.update(tenant.slug, 'collections', collection.id, {\n data: {\n ...collection.data,\n productIds: [...currentIds, productId],\n },\n }),\n );\n } else if (!shouldHave && hasProduct) {\n // Remove product from this collection\n updates.push(\n contentApi.update(tenant.slug, 'collections', collection.id, {\n data: {\n ...collection.data,\n productIds: currentIds.filter((id) => id !== productId),\n },\n }),\n );\n }\n }\n if (updates.length > 0) {\n await Promise.all(updates);\n }\n }\n\n // Save resource requirements if bookable behavior is active\n if (\n productId &&\n isBehaviorEnabled(behaviorConfigs.bookable) &&\n resourceRequirements.length > 0\n ) {\n try {\n await resourceCategoriesApi.setContentRequirements(\n tenant.slug,\n productId,\n { requirements: resourceRequirements },\n );\n } catch {\n // Non-blocking — product was saved, requirements just didn't persist\n console.warn('Failed to save resource requirements');\n }\n }\n\n onSaved();\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : 'Failed to save product';\n setError(msg);\n } finally {\n setSaving(false);\n }\n }, [\n tenant?.slug,\n productType,\n title,\n slug,\n description,\n price,\n compareAtPrice,\n tenantCurrency,\n trackInventory,\n quantity,\n lowStockThreshold,\n featured,\n isActive,\n heroImage,\n galleryItems,\n behaviorConfigs,\n gatedBy,\n gatedByPlanIds,\n templateValues,\n isEditing,\n product,\n onSaved,\n allCollections,\n selectedCollectionIds,\n resourceRequirements,\n ]);\n\n // Currency formatting helpers\n const formatCurrency = (num: number | undefined): string => {\n if (num === undefined || num === null || isNaN(num)) return '';\n return new Intl.NumberFormat('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(num);\n };\n\n const parseCurrency = (str: string): number | undefined => {\n const cleaned = str.replace(/[^0-9.]/g, '');\n if (!cleaned) return undefined;\n const num = parseFloat(cleaned);\n return isNaN(num) ? undefined : num;\n };\n\n // Display state for currency fields\n const [priceDisplay, setPriceDisplay] = useState(() =>\n formatCurrency(product?.price != null ? Number(product.price) : undefined),\n );\n const [priceFocused, setPriceFocused] = useState(false);\n const [compareDisplay, setCompareDisplay] = useState(() =>\n formatCurrency(\n product?.compareAtPrice != null\n ? Number(product.compareAtPrice)\n : undefined,\n ),\n );\n const [compareFocused, setCompareFocused] = useState(false);\n\n // Build steps for the stepper\n const steps = useMemo<FormStep[]>(() => {\n const result: FormStep[] = [];\n\n // Step 1: Title, Image & Pricing\n result.push(\n createBasicsStep({\n title,\n setTitle,\n heroImage,\n setHeroImage,\n showHeroImageModal,\n setShowHeroImageModal,\n galleryItems,\n setGalleryItems,\n gallerySensors,\n handleGalleryDragEnd,\n showGalleryMediaModal,\n setShowGalleryMediaModal,\n editingGalleryIndex,\n setEditingGalleryIndex,\n description,\n setDescription,\n currencySymbol,\n priceFocused,\n setPriceFocused,\n priceDisplay,\n setPriceDisplay,\n price,\n setPrice,\n compareFocused,\n setCompareFocused,\n compareDisplay,\n setCompareDisplay,\n compareAtPrice,\n setCompareAtPrice,\n formatCurrency,\n parseCurrency,\n }),\n );\n\n // Step 2: Behaviors — What can customers do with this product?\n result.push(\n createBehaviorsStep({\n behaviorConfigs,\n setBehaviorConfigs,\n behaviorLabelOverrides,\n availableBehaviors,\n behaviorConfigPanels,\n gatedBy,\n setGatedBy,\n gatedByPlanIds,\n setGatedByPlanIds,\n activePlans,\n linkedPlanName,\n }),\n );\n\n // Step 3: Product Details & Inventory\n result.push(\n createDetailsStep({\n behaviorConfigs,\n detailSections,\n enabledDetailSections,\n toggleDetailSection,\n activeTemplateFields,\n activeFieldGroups,\n templateValues,\n setTemplateValues,\n tenantCurrency,\n tenantCategories,\n tenantSlug: tenant?.slug,\n trackInventory,\n setTrackInventory,\n quantity,\n setQuantity,\n lowStockThreshold,\n setLowStockThreshold,\n }),\n );\n\n // Step 4: Collections & Organization\n result.push(\n createOrganizeStep({\n collectionsLoading,\n allCollections,\n selectedCollectionIds,\n setSelectedCollectionIds,\n isActive,\n setIsActive,\n isEditing,\n productType,\n setProductType,\n slug,\n setSlug,\n setSlugManuallyEdited,\n product,\n featured,\n setFeatured,\n }),\n );\n\n return result;\n }, [\n title,\n heroImage,\n galleryItems,\n showHeroImageModal,\n showGalleryMediaModal,\n editingGalleryIndex,\n gallerySensors,\n handleGalleryDragEnd,\n description,\n currencySymbol,\n priceFocused,\n priceDisplay,\n price,\n compareFocused,\n compareDisplay,\n compareAtPrice,\n templateOnlyFields,\n sellableContentType,\n templateValues,\n tenantCurrency,\n tenantCategories,\n tenant?.slug,\n trackInventory,\n quantity,\n lowStockThreshold,\n collectionsLoading,\n allCollections,\n selectedCollectionIds,\n isEditing,\n productType,\n slug,\n product?.slug,\n isActive,\n featured,\n behaviorConfigs,\n behaviorLabelOverrides,\n availableBehaviors,\n behaviorConfigPanels,\n detailSections,\n enabledDetailSections,\n toggleDetailSection,\n activeTemplateFields,\n activeFieldGroups,\n gatedBy,\n gatedByPlanIds,\n linkedPlanId,\n linkedPlanName,\n ]);\n\n return (\n <FormStepperPanel\n title={isEditing ? 'Edit Product' : 'Add Product'}\n steps={steps}\n onSave={handleSave}\n onClose={onClose}\n saving={saving}\n saveLabel={isEditing ? 'Update Product' : 'Create Product'}\n error={error}\n onClearError={() => setError(null)}\n />\n );\n}\n"],"mappings":";AAqhCI;AA1gCJ,SAAS,UAAU,WAAW,SAAS,mBAAmB;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,WAAW,mCAAmC;AACvD,SAAS,eAAe;AACxB,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AACzC,SAAS,6BAA6B;AAGtC;AAAA,EACE;AAAA,EACA;AAAA,EAWA;AAAA,OACK;AAEP;AAAA,EACE;AAAA,OAEK;AAKP,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAClC,SAAS,0BAA0B;AAmBnC,MAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,mBAAmB,OAAqC;AAC/D,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,KAAK,OAAO,MAAM,QAAQ;AAAA,EACrC;AAEA,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,OAAO;AACb,MAAI,OAAO,KAAK,QAAQ,YAAY,KAAK,IAAI,KAAK,EAAE,WAAW,EAAG,QAAO;AAEzE,SAAO;AAAA,IACL,KAAK,KAAK;AAAA,IACV,MAAM,KAAK,SAAS,UAAU,UAAU;AAAA,IACxC,KAAK,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;AAAA,IAC/C,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,IACrD,QAAQ,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS;AAAA,IACxD,WAAW,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;AAAA,IACjE,UAAU,OAAO,KAAK,aAAa,WAAW,KAAK,WAAW;AAAA,EAChE;AACF;AAEA,SAAS,mBAAmB,OAAgC;AAC1D,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC;AACnC,SAAO,MACJ,IAAI,CAAC,SAAS,mBAAmB,IAAI,CAAC,EACtC,OAAO,CAAC,SAA+B,SAAS,IAAI;AACzD;AAMA,SAAS,oBAAoB,KAAqB;AAChD,SAAO,GAAG,GAAG,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC1D;AAEA,SAAS,eAAe,OAA6C;AACnE,SAAO,MAAM,IAAI,CAAC,UAAU;AAAA,IAC1B,GAAG;AAAA,IACH,IAAI,oBAAoB,KAAK,GAAG;AAAA,EAClC,EAAE;AACJ;AAEA,SAAS,oBAAoB,OAA6C;AACxE,SAAO,MAAM,IAAI,CAAC,EAAE,IAAI,GAAG,KAAK,MAAM,IAAI;AAC5C;AAMO,SAAS,YAAY,EAAE,SAAS,SAAS,QAAQ,GAAqB;AAC3E,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,EAAE,SAAS,IAAI,mBAAmB,QAAQ,UAAU;AAC1D,QAAM,sBAAsB;AAG5B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAS,KAAK;AAGlE,QAAM,CAAC,aAAa,cAAc,IAAI;AAAA,IACnC,SAAS,QAAwB,YAAY;AAAA,EAChD;AACA,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,SAAS,SAAS,EAAE;AACvD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,SAAS,QAAQ,EAAE;AACpD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,SAAS,eAAe,EAAE;AACzE,QAAM,CAAC,OAAO,QAAQ,IAAI;AAAA,IACxB,SAAS,SAAS,OAAO,OAAO,QAAQ,KAAK,IAAI;AAAA,EACnD;AACA,QAAM,CAAC,gBAAgB,iBAAiB,IAAI;AAAA,IAC1C,SAAS,kBAAkB,OACvB,OAAO,QAAQ,cAAc,IAC7B;AAAA,EACN;AACA,QAAM,CAAC,gBAAgB,iBAAiB,IAAI;AAAA,IAC1C,SAAS,kBAAkB;AAAA,EAC7B;AACA,QAAM,CAAC,UAAU,WAAW,IAAI,SAAsB,SAAS,YAAY,CAAC;AAC5E,QAAM,CAAC,mBAAmB,oBAAoB,IAAI;AAAA,IAChD,SAAS,qBAAqB;AAAA,EAChC;AACA,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,SAAS,YAAY,KAAK;AACnE,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,SAAS,YAAY,IAAI;AAClE,QAAM,CAAC,WAAW,YAAY,IAAI;AAAA,IAChC,mBAAmB,SAAS,KAAK,KAAK;AAAA,EACxC;AACA,QAAM,CAAC,cAAc,eAAe,IAAI;AAAA,IAA+B,MACrE,eAAe,mBAAmB,SAAS,OAAO,CAAC;AAAA,EACrD;AACA,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAS,KAAK;AAClE,QAAM,CAAC,uBAAuB,wBAAwB,IAAI,SAAS,KAAK;AACxE,QAAM,CAAC,qBAAqB,sBAAsB,IAAI;AAAA,IACpD;AAAA,EACF;AAEA,QAAM,iBAAiB;AAAA,IACrB,UAAU,eAAe;AAAA,MACvB,sBAAsB,EAAE,UAAU,EAAE;AAAA,IACtC,CAAC;AAAA,IACD,UAAU,gBAAgB;AAAA,MACxB,kBAAkB;AAAA,IACpB,CAAC;AAAA,EACH;AAEA,QAAM,uBAAuB;AAAA,IAC3B,CAAC,UAAwB;AACvB,YAAM,EAAE,QAAQ,KAAK,IAAI;AACzB,UAAI,CAAC,QAAQ,OAAO,OAAO,KAAK,GAAI;AACpC,YAAM,WAAW,aAAa;AAAA,QAC5B,CAAC,SAAS,KAAK,OAAO,OAAO,OAAO,EAAE;AAAA,MACxC;AACA,YAAM,WAAW,aAAa;AAAA,QAC5B,CAAC,SAAS,KAAK,OAAO,OAAO,KAAK,EAAE;AAAA,MACtC;AACA,UAAI,WAAW,KAAK,WAAW,EAAG;AAClC,sBAAgB,CAAC,SAAS,UAAU,MAAM,UAAU,QAAQ,CAAC;AAAA,IAC/D;AAAA,IACA,CAAC,YAAY;AAAA,EACf;AAGA,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAwB,CAAC,CAAC;AACtE,QAAM,CAAC,uBAAuB,wBAAwB,IAAI,SAExD,oBAAI,IAAI,CAAC;AACX,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAS,KAAK;AAGlE,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAElD,CAAC,CAAC;AACJ,QAAM,CAAC,sBAAsB,uBAAuB,IAAI,SAEtD,CAAC,CAAC;AACJ,QAAM,CAAC,aAAa,cAAc,IAAI,SAAyB,CAAC,CAAC;AAGjE,QAAM,CAAC,gBAAgB,iBAAiB,IAAI;AAAA,IAC1C,SAAS,QAAQ,CAAC;AAAA,EACpB;AAEA,QAAM,YAAY,CAAC,CAAC;AAGpB,QAAM,iBAAiB,QAAQ,MAAM;AACnC,UAAM,WAAW,QAAQ;AACzB,WAAQ,UAAU,YAAuB;AAAA,EAC3C,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAGrB,QAAM,mBAAmB,QAAQ,MAAM;AACrC,UAAM,QACF,QAAQ,UAAsC,cAG1C,CAAC;AAGT,QAAI,OAAO,KAAK,KAAK,EAAE,SAAS,EAAG,QAAO;AAG1C,QAAI,CAAC,UAAU,aAAc,QAAO;AACpC,UAAM,WAGF,CAAC;AACL,eAAW,CAAC,EAAE,OAAO,KAAK,OAAO,QAAQ,SAAS,YAAY,GAAG;AAC/D,YAAM,IAAI;AAKV,UAAI,GAAG,SAAS,qBAAqB,EAAE,kBAAkB,EAAE,UAAU;AACnE,iBAAS,EAAE,cAAc,IAAI,CAAC,GAAG,EAAE,QAAQ;AAAA,MAC7C;AAAA,IACF;AACA,WAAO,OAAO,KAAK,QAAQ,EAAE,SAAS,IAAI,WAAW;AAAA,EACvD,GAAG,CAAC,QAAQ,UAAU,UAAU,YAAY,CAAC;AAG7C,QAAM,iBAAiB;AAAA,IACrB,MAAM,kBAAkB,cAAc;AAAA,IACtC,CAAC,cAAc;AAAA,EACjB;AAGA,QAAM,eAAe,QAAQ,MAAM;AACjC,QAAI,CAAC,oBAAqB,QAAO,CAAC;AAClC,WAAO,oBAAoB;AAAA,EAC7B,GAAG,CAAC,mBAAmB,CAAC;AAGxB,QAAM,qBAAqB,QAAQ,MAAM;AACvC,WAAO,aAAa,OAAO,CAAC,MAAM,CAAC,gBAAgB,IAAI,EAAE,IAAI,CAAC;AAAA,EAChE,GAAG,CAAC,YAAY,CAAC;AAIjB,QAAM,iBAAiB,QAAQ,MAAM;AACnC,UAAM,SAAS,qBAAqB,eAAe,CAAC;AACpD,UAAM,oBAAoB,IAAI,IAAI,OAAO,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;AACjE,UAAM,YAAY,mBAAmB;AAAA,MACnC,CAAC,MAAM,CAAC,kBAAkB,IAAI,EAAE,IAAI;AAAA,IACtC;AAEA,UAAM,WAKD,CAAC;AAGN,eAAW,KAAK,QAAQ;AACtB,YAAM,YAAY,EAAE,OAAO;AAAA,QAAK,CAAC,OAC/B,mBAAmB,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE;AAAA,MAC9C;AACA,UAAI,WAAW;AACb,iBAAS,KAAK;AAAA,UACZ,KAAK,SAAS,EAAE,KAAK;AAAA,UACrB,OAAO,EAAE;AAAA,UACT,aAAa,EAAE;AAAA,UACf,YAAY,EAAE;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IACF;AAGA,eAAW,KAAK,WAAW;AACzB,eAAS,KAAK;AAAA,QACZ,KAAK,SAAS,EAAE,IAAI;AAAA,QACpB,OAAO,EAAE,SAAS,EAAE;AAAA,QACpB,YAAY,CAAC,EAAE,IAAI;AAAA,MACrB,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,oBAAoB,qBAAqB,WAAW,CAAC;AAGzD,QAAM,CAAC,uBAAuB,wBAAwB,IAAI,SAExD,oBAAI,IAAI,CAAC;AAGX,YAAU,MAAM;AACd,QAAI,CAAC,aAAa,CAAC,SAAS,QAAQ,eAAe,WAAW,EAAG;AACjE,UAAM,OAAO,QAAQ;AACrB,UAAM,UAAU,oBAAI,IAAY;AAChC,eAAW,WAAW,gBAAgB;AACpC,YAAM,UAAU,QAAQ,WAAW,KAAK,CAAC,OAAO;AAC9C,cAAM,MAAM,KAAK,EAAE;AACnB,YAAI,QAAQ,UAAa,QAAQ,QAAQ,QAAQ,GAAI,QAAO;AAC5D,YAAI,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW,EAAG,QAAO;AACnD,eAAO;AAAA,MACT,CAAC;AACD,UAAI,QAAS,SAAQ,IAAI,QAAQ,GAAG;AAAA,IACtC;AACA,QAAI,QAAQ,OAAO,EAAG,0BAAyB,OAAO;AAAA,EACxD,GAAG,CAAC,WAAW,SAAS,MAAM,cAAc,CAAC;AAE7C,QAAM,sBAAsB,YAAY,CAAC,QAAgB;AACvD,6BAAyB,CAAC,SAAS;AACjC,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,UAAI,KAAK,IAAI,GAAG,GAAG;AACjB,aAAK,OAAO,GAAG;AAAA,MACjB,OAAO;AACL,aAAK,IAAI,GAAG;AAAA,MACd;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAGL,QAAM,uBAAuB,QAAQ,MAAM;AACzC,UAAM,oBAAoB,oBAAI,IAAY;AAC1C,eAAW,WAAW,gBAAgB;AACpC,UAAI,sBAAsB,IAAI,QAAQ,GAAG,GAAG;AAC1C,mBAAW,MAAM,QAAQ,WAAY,mBAAkB,IAAI,EAAE;AAAA,MAC/D;AAAA,IACF;AACA,WAAO,mBAAmB,OAAO,CAAC,MAAM,kBAAkB,IAAI,EAAE,IAAI,CAAC;AAAA,EACvE,GAAG,CAAC,oBAAoB,gBAAgB,qBAAqB,CAAC;AAE9D,QAAM,oBAAoB,QAAQ,MAAM;AACtC,QAAI,CAAC,qBAAqB,YAAa,QAAO;AAC9C,WAAO,oBAAoB,YAAY;AAAA,MAAO,CAAC,MAC7C,sBAAsB,IAAI,SAAS,EAAE,KAAK,EAAE;AAAA,IAC9C;AAAA,EACF,GAAG,CAAC,qBAAqB,aAAa,qBAAqB,CAAC;AAG5D,YAAU,MAAM;AACd,QAAI,CAAC,aAAa,CAAC,sBAAsB,OAAO;AAC9C,cAAQ,aAAa,KAAK,CAAC;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,OAAO,WAAW,kBAAkB,CAAC;AAGzC,YAAU,MAAM;AACd,sBAAkB,SAAS,QAAQ,CAAC,CAAC;AACrC,iBAAa,mBAAmB,SAAS,KAAK,KAAK,IAAI;AACvD,oBAAgB,eAAe,mBAAmB,SAAS,OAAO,CAAC,CAAC;AAAA,EACtE,GAAG,CAAC,SAAS,MAAM,SAAS,OAAO,SAAS,OAAO,CAAC;AAGpD,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,KAAM;AACnB,QAAI,YAAY;AAChB,UAAM,mBAAmB,YAAY;AACnC,4BAAsB,IAAI;AAC1B,UAAI;AACF,cAAM,SAAS,MAAM,WAAW,KAAK,OAAO,MAAM,eAAe;AAAA,UAC/D,OAAO;AAAA,QACT,CAAC;AACD,YAAI,UAAW;AACf,cAAM,QAAQ,OAAO,QAAQ,CAAC;AAC9B,0BAAkB,KAAK;AAGvB,YAAI,SAAS,IAAI;AACf,gBAAM,UAAU,oBAAI,IAAY;AAChC,qBAAW,KAAK,OAAO;AACrB,kBAAM,MAAM,EAAE,MAAM;AACpB,gBAAI,MAAM,QAAQ,GAAG,KAAK,IAAI,SAAS,QAAQ,EAAE,GAAG;AAClD,sBAAQ,IAAI,EAAE,EAAE;AAAA,YAClB;AAAA,UACF;AACA,mCAAyB,OAAO;AAAA,QAClC;AAAA,MACF,QAAQ;AAAA,MAER,UAAE;AACA,YAAI,CAAC,UAAW,uBAAsB,KAAK;AAAA,MAC7C;AAAA,IACF;AACA,qBAAiB;AACjB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,SAAS,EAAE,CAAC;AAG9B,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,KAAM;AACnB,QAAI,YAAY;AAChB,UAAM,kBAAkB,YAAY;AAClC,UAAI;AACF,cAAM,OAAO,MAAM,sBAAsB,KAAK,OAAO,IAAI;AACzD,YAAI,UAAW;AACf;AAAA,WACG,QAAQ,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,YACvB,IAAI,EAAE;AAAA,YACN,OAAO,EAAE,SAAU,EAAU,QAAQ;AAAA,YACrC,aAAa,EAAE,eAAe;AAAA,YAC9B,MAAM,EAAE,QAAQ;AAAA,UAClB,EAAE;AAAA,QACJ;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AACA,oBAAgB;AAChB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,IAAI,CAAC;AAGjB,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,KAAM;AACnB,6BACG,eAAe,OAAO,IAAI,EAC1B,KAAK,CAAC,UAAU,eAAe,SAAS,CAAC,CAAC,CAAC,EAC3C,MAAM,MAAM;AAAA,IAEb,CAAC;AAAA,EACL,GAAG,CAAC,QAAQ,IAAI,CAAC;AAGjB,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,QAAQ,CAAC,SAAS,GAAI;AACnC,QAAI,YAAY;AAChB,UAAM,oBAAoB,YAAY;AACpC,UAAI;AACF,cAAM,OAAO,MAAM,sBAAsB;AAAA,UACvC,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AACA,YAAI,UAAW;AACf;AAAA,WACG,QAAQ,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,YACvB,YAAY,EAAE;AAAA,YACd,YAAY,EAAE,cAAc;AAAA,YAC5B,oBAAoB,EAAE;AAAA,YAEtB,gBAAgB,EAAE,kBAAkB,CAAC;AAAA,YACrC,qBAAqB,EAAE,uBAAuB;AAAA,UAChD,EAAE;AAAA,QACJ;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AACA,sBAAkB;AAClB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,SAAS,EAAE,CAAC;AAG9B,QAAM,CAAC,iBAAiB,kBAAkB,IAAI;AAAA,IAC5C,MAAM;AAEJ,UAAI,SAAS,WAAW;AACtB,eAAO,QAAQ;AAAA,MACjB;AAEA,UAAI,qBAAqB,WAAW;AAClC,cAAM,IAAsB,CAAC;AAC7B,mBAAW,YAAY,oBAAoB,WAAW;AACpD,UAAC,EAAU,QAAQ,IAAI,EAAE,SAAS,KAAK;AAAA,QACzC;AACA,eAAO;AAAA,MACT;AACA,aAAO,EAAE,UAAU,EAAE,SAAS,KAAK,EAAE;AAAA,IACvC;AAAA,EACF;AAGA,YAAU,MAAM;AACd,QAAI,CAAC,aAAa,qBAAqB,WAAW;AAChD,yBAAmB,CAAC,SAAS;AAE3B,cAAM,UAAU,EAAE,GAAG,KAAK;AAC1B,mBAAW,YAAY,oBAAoB,WAAW;AACpD,cAAK,QAAgB,QAAQ,MAAM,QAAW;AAC5C,YAAC,QAAgB,QAAQ,IAAI,EAAE,SAAS,KAAK;AAAA,UAC/C;AAAA,QACF;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,qBAAqB,SAAS,CAAC;AAGnC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAmB,MAAM;AACrD,QACE,SAAS,WAAW,WACpB,MAAM,QAAQ,QAAQ,UAAU,OAAO,GACvC;AACA,aAAO,QAAQ,UAAU;AAAA,IAC3B;AACA,WAAO,CAAC;AAAA,EACV,CAAC;AACD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAmB,MAAM;AACnE,QACE,SAAS,WAAW,kBACpB,MAAM,QAAQ,QAAQ,UAAU,cAAc,GAC9C;AACA,aAAO,QAAQ,UAAU;AAAA,IAC3B;AACA,WAAO,CAAC;AAAA,EACV,CAAC;AAGD,QAAM,eAAe,QAAQ,MAAM;AACjC,UAAM,SAAS;AAAA,MACb,gBAAgB;AAAA,IAClB;AACA,WAAO,QAAQ;AAAA,EACjB,GAAG,CAAC,gBAAgB,YAAY,CAAC;AAEjC,QAAM,iBAAiB,QAAQ,MAAM;AACnC,QAAI,CAAC,gBAAgB,CAAC,YAAa,QAAO;AAC1C,WAAO,YAAY,KAAK,CAAC,MAAM,EAAE,OAAO,YAAY,GAAG;AAAA,EACzD,GAAG,CAAC,cAAc,WAAW,CAAC;AAI9B,QAAM,qBAAqB,QAAuB,MAAM;AACtD,UAAM,iBAAiB,QAAQ;AAE/B,UAAM,kBAAkB,gBAAgB;AAGxC,QAAI,MAAM,QAAQ,eAAe,KAAK,gBAAgB,SAAS,GAAG;AAChE,aAAO,gBAAgB;AAAA,QACrB,CAAC,MAAwB,MAAM,cAAc,MAAM;AAAA,MACrD;AAAA,IACF;AAGA,QACE,qBAAqB,aACrB,oBAAoB,UAAU,SAAS,GACvC;AACA,YAAM,WAAW,oBAAoB,UAAU;AAAA,QAC7C,CAAC,MAAwB,MAAM,cAAc,MAAM;AAAA,MACrD;AACA,UAAI,SAAS,SAAS,EAAG,QAAO;AAAA,IAClC;AAGA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,UAAU,mBAAmB,CAAC;AAG1C,QAAM,yBAAyB,QAE7B,MAAM;AAEN,UAAM,KAAK;AAGX,WAAO,IAAI;AAAA,EACb,GAAG,CAAC,mBAAmB,CAAC;AAGxB,QAAM,YAAY,QAAQ,MAAM;AAC9B,UAAM,IAA6B,CAAC;AACpC,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,eAAe,GAAG;AAC1D,UAAI,QAAQ,aAAa,QAAQ,oBAAoB,QAAQ;AAC3D;AACF,QAAE,GAAG,IAAI,kBAAkB,KAAK;AAAA,IAClC;AACA,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,CAAC;AAGpB,QAAM,uBAAuB,YAAY,CAAC,WAA2B;AACnE,uBAAmB,CAAC,UAAU,EAAE,GAAG,MAAM,UAAU,OAAO,EAAE;AAAA,EAC9D,GAAG,CAAC,CAAC;AAEL,QAAM,wBAAwB,YAAY,CAAC,WAA4B;AACrE,uBAAmB,CAAC,UAAU,EAAE,GAAG,MAAM,WAAW,OAAO,EAAE;AAAA,EAC/D,GAAG,CAAC,CAAC;AAEL,QAAM,2BAA2B,YAAY,CAAC,WAA+B;AAC3E,uBAAmB,CAAC,UAAU,EAAE,GAAG,MAAM,cAAc,OAAO,EAAE;AAAA,EAClE,GAAG,CAAC,CAAC;AAEL,QAAM,2BAA2B,YAAY,CAAC,WAA+B;AAC3E,uBAAmB,CAAC,UAAU,EAAE,GAAG,MAAM,cAAc,OAAO,EAAE;AAAA,EAClE,GAAG,CAAC,CAAC;AAEL,QAAM,uBAAuB,YAAY,CAAC,WAA2B;AACnE,uBAAmB,CAAC,UAAU,EAAE,GAAG,MAAM,UAAU,OAAO,EAAE;AAAA,EAC9D,GAAG,CAAC,CAAC;AAEL,QAAM,uBAAuB,YAAY,CAAC,WAA2B;AACnE,uBAAmB,CAAC,UAAU,EAAE,GAAG,MAAM,UAAU,OAAO,EAAE;AAAA,EAC9D,GAAG,CAAC,CAAC;AAGL,QAAM,uBAAuB;AAAA,IAC3B,MACE,2BAA2B;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,IACH;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAGA,QAAM,aAAa,YAAY,YAAY;AACzC,QAAI,CAAC,QAAQ,KAAM;AAGnB,QAAI,CAAC,MAAM,KAAK,GAAG;AACjB,eAAS,mBAAmB;AAC5B;AAAA,IACF;AACA,QAAI,CAAC,KAAK,KAAK,GAAG;AAChB,eAAS,kBAAkB;AAC3B;AAAA,IACF;AAEA,cAAU,IAAI;AACd,aAAS,IAAI;AAEb,QAAI;AACF,YAAM,UAAgC;AAAA,QACpC,MAAM;AAAA,QACN,OAAO,MAAM,KAAK;AAAA,QAClB,MAAM,KAAK,KAAK;AAAA,QAChB,aAAa,eAAe;AAAA,QAC5B,OAAO,UAAU,SAAY,OAAO,KAAK,KAAK,IAAI;AAAA,QAClD,gBACE,mBAAmB,SACf,OAAO,cAAc,KAAK,IAC1B;AAAA,QACN,UAAU;AAAA,QACV;AAAA,QACA,UAAU,OAAO,aAAa,WAAW,WAAW;AAAA,QACpD,mBACE,OAAO,sBAAsB,WAAW,oBAAoB;AAAA,QAC9D;AAAA,QACA;AAAA,QACA,OAAO,YACF;AAAA,UACC,GAAG;AAAA,UACH,MAAM;AAAA,QACR,IACA;AAAA;AAAA;AAAA;AAAA,QAIJ,SACE,aAAa,SAAS,IAClB,oBAAoB,YAAY,IAChC,YACE,CAAC,IACD;AAAA,QACR,WAAW;AAAA,UACT,GAAG;AAAA,UACH,GAAI,QAAQ,SAAS,KAAK,EAAE,QAAQ;AAAA,UACpC,GAAI,eAAe,SAAS,KAAK,EAAE,eAAe;AAAA,QACpD;AAAA,QACA,MAAM;AAAA,MACR;AAEA,UAAI;AAEJ,UAAI,aAAa,SAAS;AAExB,cAAM,EAAE,MAAM,GAAG,cAAc,IAAI;AACnC,cAAM,aAAa;AAAA,UACjB,OAAO;AAAA,UACP,QAAQ;AAAA,UACR;AAAA,QACF;AACA,oBAAY,QAAQ;AAAA,MACtB,OAAO;AACL,cAAM,UAAU,MAAM,aAAa;AAAA,UACjC,OAAO;AAAA,UACP;AAAA,QACF;AACA,oBAAY,QAAQ;AAAA,MACtB;AAGA,UAAI,aAAa,eAAe,SAAS,GAAG;AAC1C,cAAM,UAA8B,CAAC;AACrC,mBAAW,cAAc,gBAAgB;AACvC,gBAAM,aAAuB,MAAM;AAAA,YACjC,WAAW,MAAM;AAAA,UACnB,IACK,WAAW,KAAK,aACjB,CAAC;AACL,gBAAM,aAAa,WAAW,SAAS,SAAS;AAChD,gBAAM,aAAa,sBAAsB,IAAI,WAAW,EAAE;AAE1D,cAAI,cAAc,CAAC,YAAY;AAE7B,oBAAQ;AAAA,cACN,WAAW,OAAO,OAAO,MAAM,eAAe,WAAW,IAAI;AAAA,gBAC3D,MAAM;AAAA,kBACJ,GAAG,WAAW;AAAA,kBACd,YAAY,CAAC,GAAG,YAAY,SAAS;AAAA,gBACvC;AAAA,cACF,CAAC;AAAA,YACH;AAAA,UACF,WAAW,CAAC,cAAc,YAAY;AAEpC,oBAAQ;AAAA,cACN,WAAW,OAAO,OAAO,MAAM,eAAe,WAAW,IAAI;AAAA,gBAC3D,MAAM;AAAA,kBACJ,GAAG,WAAW;AAAA,kBACd,YAAY,WAAW,OAAO,CAAC,OAAO,OAAO,SAAS;AAAA,gBACxD;AAAA,cACF,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF;AACA,YAAI,QAAQ,SAAS,GAAG;AACtB,gBAAM,QAAQ,IAAI,OAAO;AAAA,QAC3B;AAAA,MACF;AAGA,UACE,aACA,kBAAkB,gBAAgB,QAAQ,KAC1C,qBAAqB,SAAS,GAC9B;AACA,YAAI;AACF,gBAAM,sBAAsB;AAAA,YAC1B,OAAO;AAAA,YACP;AAAA,YACA,EAAE,cAAc,qBAAqB;AAAA,UACvC;AAAA,QACF,QAAQ;AAEN,kBAAQ,KAAK,sCAAsC;AAAA,QACrD;AAAA,MACF;AAEA,cAAQ;AAAA,IACV,SAAS,GAAY;AACnB,YAAM,MAAM,aAAa,QAAQ,EAAE,UAAU;AAC7C,eAAS,GAAG;AAAA,IACd,UAAE;AACA,gBAAU,KAAK;AAAA,IACjB;AAAA,EACF,GAAG;AAAA,IACD,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAGD,QAAM,iBAAiB,CAAC,QAAoC;AAC1D,QAAI,QAAQ,UAAa,QAAQ,QAAQ,MAAM,GAAG,EAAG,QAAO;AAC5D,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,uBAAuB;AAAA,MACvB,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,GAAG;AAAA,EACf;AAEA,QAAM,gBAAgB,CAAC,QAAoC;AACzD,UAAM,UAAU,IAAI,QAAQ,YAAY,EAAE;AAC1C,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,MAAM,WAAW,OAAO;AAC9B,WAAO,MAAM,GAAG,IAAI,SAAY;AAAA,EAClC;AAGA,QAAM,CAAC,cAAc,eAAe,IAAI;AAAA,IAAS,MAC/C,eAAe,SAAS,SAAS,OAAO,OAAO,QAAQ,KAAK,IAAI,MAAS;AAAA,EAC3E;AACA,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI;AAAA,IAAS,MACnD;AAAA,MACE,SAAS,kBAAkB,OACvB,OAAO,QAAQ,cAAc,IAC7B;AAAA,IACN;AAAA,EACF;AACA,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,KAAK;AAG1D,QAAM,QAAQ,QAAoB,MAAM;AACtC,UAAM,SAAqB,CAAC;AAG5B,WAAO;AAAA,MACL,iBAAiB;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAGA,WAAO;AAAA,MACL,oBAAoB;AAAA,QAClB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAGA,WAAO;AAAA,MACL,kBAAkB;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,YAAY,QAAQ;AAAA,QACpB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAGA,WAAO;AAAA,MACL,mBAAmB;AAAA,QACjB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,YAAY,iBAAiB;AAAA,MACpC;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,WAAW,YAAY,mBAAmB;AAAA,MAC1C;AAAA,MACA,cAAc,MAAM,SAAS,IAAI;AAAA;AAAA,EACnC;AAEJ;","names":[]}
@@ -146,6 +146,7 @@ function createNavRenderers({
146
146
  onMouseEnter: () => rows.forEach((c) => warm(c.href)),
147
147
  onFocus: () => rows.forEach((c) => warm(c.href)),
148
148
  "aria-expanded": open,
149
+ "aria-controls": open ? `navsub-${group.id}` : void 0,
149
150
  children: [
150
151
  /* @__PURE__ */ jsx("span", { className: "ic", children: renderIcon(group.id, group.icon) }),
151
152
  /* @__PURE__ */ jsx("span", { className: "lbl", children: group.label }),
@@ -157,6 +158,7 @@ function createNavRenderers({
157
158
  /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: open && /* @__PURE__ */ jsxs(
158
159
  motion.ul,
159
160
  {
161
+ id: `navsub-${group.id}`,
160
162
  className: "navsub",
161
163
  initial: { height: 0, opacity: 0, marginBottom: 0 },
162
164
  animate: { height: "auto", opacity: 1, marginBottom: SUB_GAP },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/nav-renderers.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { ChevronDown } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { eases } from '@withpotter/lib/motion';\nimport { prefersReducedMotion } from './nav-highlights';\nimport { navIcon } from '../shell/icons';\nimport { RailLabel } from './rail-label';\nimport { type NavItem, type NavGroup } from './config';\nimport type { NavRendererDeps } from './nav-renderer-deps';\nimport { NavHighlight, NavPending, SubHighlight } from './nav-highlights';\n\n/**\n * The air under an open hub, before the next top-level row.\n *\n * The rail's gaps are what say where one thing stops and the next begins, and\n * an open hub had nothing to say it: its last child sat 2px above the next\n * top-level row — the same 2px that separates two children — so the row after\n * an open hub read as the last item IN it.\n *\n * 6px, not 30. With the group's own 2px that is 8px, four times a sibling gap\n * and unmistakable, and still well under the 20px that opens a new SECTION. A\n * gap bigger than the section break would rank an open hub above a heading,\n * which is backwards: the hub is one row inside the section it sits in.\n */\nconst SUB_GAP = 6;\n\nexport type { NavRendererDeps } from './nav-renderer-deps';\n\n/**\n * One highlight per open hub, MOVED between its children rather than\n * replaced on each of them.\n *\n * The obvious version — a `layoutId` on a pill that unmounts from one row and\n * mounts on the next — is what this replaces. Framer took ownership of the\n * element (it carried the projection styles) but resolved no delta and put it\n * down on the new row with the frames in between missing, and chasing that\n * through framer's projection internals is not worth what it buys. A pill\n * that never unmounts cannot fail to match itself: `y` is an ordinary\n * animated value on one persistent element, so there is nothing to hand off.\n *\n * `offsetTop` off the row rather than index × pitch, so the row height, the\n * gap and the group's padding stay facts of the stylesheet. Read in a layout\n * effect, before paint, so the pill is never seen at the wrong row.\n *\n * A spring, not a curve: this is the one thing in the nav that genuinely\n * moves, and clicking a third row mid-flight has to reverse from wherever it\n * got to, carrying its velocity.\n */\n/**\n * Read at render rather than through `useReducedMotion`, because\n * `createNavRenderers` is a plain factory and not a hook — it cannot hold one.\n * Non-reactive is correct here anyway: the transition config is consulted when\n * an animation starts, so a preference changed mid-session applies to the next\n * open rather than to one already in flight.\n */\n/**\n * The sidebar's row, hub and section renderers.\n *\n * A factory rather than components: each one reads seven values that belong to\n * the sidebar's render pass (what is active, what the merchant has toggled\n * open), and threading those through props one component at a time was what\n * kept them inside a 1,510-line file in the first place.\n *\n * Anything with STATE must stay outside it. This runs on every render of the\n * sidebar, so a component declared in here is a new function identity each\n * time, and React unmounts and remounts it on every pass — which silently\n * destroys any hook state it was holding. That is what defeated the first two\n * attempts at a sliding highlight: `SubHighlight` was resetting its motion\n * values before it could ever animate them, and it looked exactly like a\n * framer bug. `SubHighlight` and `NavPending` live above for that reason.\n */\nexport function createNavRenderers({\n collapsed,\n warm,\n activeHref,\n isActive,\n hubChoice,\n setHubOpen,\n getGroupItems,\n badges,\n isNew,\n}: NavRendererDeps) {\n const renderIcon = (\n id: string | undefined,\n Fallback: LucideIcon,\n size = 18,\n ) => {\n const Console = id ? navIcon(id) : null;\n if (Console) return <Console width={size} height={size} />;\n return <Fallback width={size} height={size} />;\n };\n\n // The highlight on a top-level row.\n //\n // It still does not TRAVEL: there is no \"between\" for a row whose neighbours\n // are unrelated destinations, only a jump the length of the whole sidebar,\n // which is what got the old shared-layout pill torn out. But it no longer\n // simply appears either. Coming back up out of a hub, the pill rises the\n // same eight pixels the sub-pill drops, so the two halves of that gesture\n // are the same gesture seen from either end.\n const renderHighlight = (active: boolean) =>\n active ? <NavHighlight /> : null;\n\n /**\n * A nav row. Sub-rows carry no icon.\n *\n * A glyph beside a child was doing no work the layout wasn't already doing.\n * A child is identified by the parent it sits under and the indent it sits\n * at, and a second column of icons only made the tree read as two lists of\n * peers stacked on each other. Dropping it also lands the child's label\n * directly under the parent's, so the labels form one column rather than two\n * ragged ones. Icons stay where they still distinguish: the top level.\n */\n /**\n * The count a row wears, or nothing.\n *\n * Hidden when the rail is collapsed: at that width the label is gone too,\n * and a bare digit beside an icon names nothing.\n */\n const renderBadge = (n: number | undefined) =>\n !collapsed && !!n ? (\n <span className=\"navbadge squircle\" aria-label={`${n} need attention`}>\n {/* Capped, because the badge is a fixed square: `.squircle` is a mask\n stretched to the box, so a third digit would widen it and distort the\n corners away from every other squircle in the app. Past 99 the exact\n number has stopped being the point. */}\n {n > 99 ? '99+' : n}\n </span>\n ) : null;\n\n /**\n * A CLOSED HUB CARRIES WHAT IS INSIDE IT.\n *\n * Notifications land on the row their link names, and most of those rows are\n * a hub's CHILD — `/dashboard/inventory` is inside Inventory, which is shut\n * on most screens. Counting only the hub's own id meant the badge existed\n * and was invisible on every business that had one, which is the exact bug\n * this whole feature was replacing the bell to avoid.\n *\n * Only while closed. Open, the children are on screen wearing their own\n * numbers, and a total above them would be the same work counted twice.\n */\n const hubTotal = (group: NavGroup) =>\n (badges?.[group.id] ?? 0) +\n (group.items ?? []).reduce((sum, c) => sum + (badges?.[c.id] ?? 0), 0);\n\n const renderRow = (item: NavItem, sub = false, count?: number) => {\n const active = item.href === activeHref;\n return (\n // Keyed by href, not id. A business running several archetypes gets one\n // `catalog` row per archetype — Services, Properties, Menu — all sharing\n // the id and differing only by their intent-scoped href, which React saw\n // as duplicate keys and warned it may drop rows for.\n <li className=\"navitem\" key={item.href}>\n <RailLabel label={item.name} collapsed={collapsed}>\n <Link\n href={item.href}\n prefetch={false}\n onMouseEnter={() => warm(item.href)}\n onFocus={() => warm(item.href)}\n // Touch has no hover, so a tap used to hit a stone-cold route.\n // pointerdown lands before click and buys the prefetch a head start.\n onPointerDown={() => warm(item.href)}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? item.name : undefined}\n >\n {/* A sub-row's highlight belongs to its GROUP, which owns one and\n slides it. Rendering a second one here would put a static pill\n under the moving one on the row it just left. */}\n {!sub && renderHighlight(active)}\n <NavPending />\n {!sub && (\n <span className=\"ic\">{renderIcon(item.id, item.icon)}</span>\n )}\n <span className=\"lbl\">{item.name}</span>\n {/* Work waiting, not how many rows the page has. Rendered only\n when there is something to do, so the row is unmarked in the\n ordinary case and a number means something every time it\n appears. */}\n {/* NEW, on a row that has only just appeared. John, 2026-09-03:\n *\"it should show New on the new things added to the sidebar\"*.\n A merchant who switches on bookings gets three rows they have\n never seen among nine they know by shape, and without this the\n thing they just chose is the hardest thing on the rail to find.\n\n Before the count, and never both: a row cannot be simultaneously\n brand new and carrying work, and if it somehow is, the count is\n the one that needs doing. */}\n {isNew?.(item.id) && !badges?.[item.id] ? (\n <span className=\"navnew\">New</span>\n ) : (\n renderBadge(count ?? badges?.[item.id])\n )}\n </Link>\n </RailLabel>\n </li>\n );\n };\n\n // A hub row. With ≤1 child it's a plain link; with real sub-pages it gets a\n // hover-revealed chevron that expands the children inline (label still\n // navigates to the hub landing — only the chevron toggles).\n /**\n * A nav section, headed only when the heading is doing work. One item under a\n * heading is a label restating the row beneath it; two or more is a group.\n * Falsy children are dropped first so the count reflects what actually\n * renders, not how many slots were passed.\n */\n /**\n * Does the current route live inside these items?\n *\n * A section that owns the route is forced open, so the page you are on always\n * has a highlighted row. Hubs are checked through their children too — the\n * route is usually a child, not the hub's own landing page.\n */\n const ownsRoute = (items: (NavItem | NavGroup | undefined)[]) =>\n items.some((item) => {\n if (!item) return false;\n if ('href' in item && item.href && isActive(item.href)) return true;\n const children = 'id' in item ? getGroupItems(item as NavGroup) : [];\n return children.some((c) => c.href && isActive(c.href));\n });\n\n const renderHub = (group: NavGroup) => {\n const children = getGroupItems(group);\n if (children.length === 0 || (children.length === 1 && !group.alwaysHub)) {\n const active = group.href === activeHref;\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed={collapsed}>\n <Link\n href={group.href ?? '/dashboard'}\n prefetch={false}\n onMouseEnter={() => warm(group.href ?? '/dashboard')}\n onFocus={() => warm(group.href ?? '/dashboard')}\n onPointerDown={() => warm(group.href ?? '/dashboard')}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? group.label : undefined}\n >\n {renderHighlight(active)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n /**\n * The hub's own landing page, sitting among its children instead of on the\n * hub's row.\n *\n * Every hub here had a real screen behind it, and putting that screen on\n * the container's row is what made the nav ambiguous: one click both\n * navigated and disclosed, and the highlight settled on the parent while\n * the list underneath it — which had appeared on that same click — read as\n * a menu still awaiting a choice. As a child it is an ordinary\n * destination, reachable by name, and the highlight lands where you\n * actually are.\n */\n const landing: NavItem[] = group.href\n ? [\n {\n id: `${group.id}-landing`,\n // `group.label` has already been through renameGroup, so a hub the\n // merchant renamed carries that name into its own landing row.\n //\n // NOTHING RELIES ON THE FALLBACK, and a new hub should not start.\n // Every group in the spine names its own landing child, because\n // \"Overview\" distinguishes nothing — every hub could carry one, so\n // the word tells the merchant nothing about which hub they are in.\n // A hub with an href owes its landing page a name that says what is\n // on it. The fallback stays only so a group under construction\n // renders rather than throwing.\n name: (group.landingLabel ?? 'Overview').replace(\n '{label}',\n group.label.toLowerCase(),\n ),\n href: group.href,\n icon: group.icon,\n },\n ]\n : [];\n const at = Math.min(group.landingAt ?? 0, children.length);\n const rows = [...children.slice(0, at), ...landing, ...children.slice(at)];\n // Which row the group's highlight sits on. -1 means none of them, so the\n // group renders no pill at all rather than one parked on the first row.\n const activeIndex = rows.findIndex((c) => c.href === activeHref);\n const childActive = activeIndex >= 0;\n // Auto-open when this hub owns the current route, unless the merchant has\n // toggled one open themselves (their choice wins, and it is exclusive —\n // opening this hub is what closed the last one).\n const open = hubChoice === undefined ? childActive : hubChoice === group.id;\n /**\n * Collapsed, the row falls back to being a link.\n *\n * Not a fudge: the rail hides `.navsub` entirely, so there is no list for\n * the row to open and therefore no ambiguity for it to create. The\n * ambiguity this whole change removes only exists when the children are on\n * screen. With them hidden, a container with nowhere to go is just a dead\n * target, so the row goes to its landing page as it always did.\n */\n if (collapsed) {\n const href = group.href ?? '/dashboard';\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed>\n <Link\n href={href}\n prefetch={false}\n className={'navlink' + (href === activeHref ? ' active' : '')}\n aria-label={group.label}\n onMouseEnter={() => warm(href)}\n onPointerDown={() => warm(href)}\n >\n {renderHighlight(href === activeHref)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n return (\n <li className=\"navitem\" key={group.id}>\n {/* A button, not a link. The whole row toggles — reaching for a 20px\n chevron to see what is inside a nav item is a target you have to aim\n at, when the row already reads as \"there is more here\". It carries no\n highlight and no pending state, because it goes nowhere. */}\n {/* Warms the whole group, not the row.\n The hub itself goes nowhere, so there is nothing to prefetch FOR it —\n but a hub is only ever pressed on the way to a child, and the pointer\n reaching this row is the earliest honest signal of that. It buys the\n children the length of one pointer travel plus one click, which is the\n difference between a child that renders and a child that waits: every\n route here is `prefetch={false}`, and a cold one was measured at 1.7s.\n Both handlers, because touch has no hover. */}\n <button\n type=\"button\"\n className={'navlink navhub' + (open ? ' open' : '')}\n onClick={() => {\n rows.forEach((c) => warm(c.href));\n setHubOpen(group.id, !open);\n }}\n onMouseEnter={() => rows.forEach((c) => warm(c.href))}\n onFocus={() => rows.forEach((c) => warm(c.href))}\n aria-expanded={open}\n >\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {!open && renderBadge(hubTotal(group))}\n <ChevronDown className=\"chev\" width={14} height={14} />\n </button>\n {/* No id-collision guard here any more. It existed because a business\n running several archetypes gets one `catalog` row per archetype, all\n sharing an id, so all three resolved to the same console glyph — and\n the row's own icon had to win. Children have no glyph to collide\n over now; their labels already differ. */}\n {/* THE CHILDREN GROW AND COLLAPSE, they do not blink.\n This was `{open && <ul>}` — a bare mount and unmount, so a hub's\n children appeared and vanished between one frame and the next while\n the chevron above them rotated over 200ms. One control, two\n different speeds, and the faster one was a cut.\n\n Height, not opacity alone: the rows below have to MOVE, and a fade\n over a jump still jumps. `height: auto` is the only honest target\n here because a hub's child count varies, and Motion measures it\n rather than us guessing a row pitch that breaks the moment a label\n wraps.\n\n Opacity finishes early on the way in and starts late on the way out,\n so the children are solid for most of the travel — content that\n fades over the full distance reads as sluggish even when the\n distance is right.\n\n The bottom margin travels WITH the height rather than sitting in\n the stylesheet. As CSS it would hold its full value all the way\n through the close and then vanish on the frame the list unmounts —\n the one snap in an otherwise continuous move, at the bottom edge\n where the rows below are already travelling. */}\n <AnimatePresence initial={false}>\n {open && (\n <motion.ul\n className=\"navsub\"\n initial={{ height: 0, opacity: 0, marginBottom: 0 }}\n animate={{ height: 'auto', opacity: 1, marginBottom: SUB_GAP }}\n exit={{ height: 0, opacity: 0, marginBottom: 0 }}\n transition={\n prefersReducedMotion()\n ? { duration: 0 }\n : {\n height: { duration: 0.24, ease: eases.settle },\n marginBottom: { duration: 0.24, ease: eases.settle },\n opacity: { duration: 0.16, ease: eases.out },\n }\n }\n // Clipped, or the rows spill past the collapsing box on the way\n // out and the whole thing reads as a glitch rather than a close.\n style={{ overflow: 'hidden' }}\n >\n {activeIndex >= 0 && (\n <SubHighlight key={group.id} index={activeIndex} />\n )}\n {/* THE HUB'S OWN COUNT FOLLOWS IT DOWN.\n John, 2026-09-03: *\"when I click on the parent nav, where\n does the notification thingy go, what sub nav is the\n notification from, why don't I see it\"*.\n\n Nowhere, was the answer. A count keyed on the HUB's id showed\n on the closed row and then vanished on open, because the\n landing child carries `${group.id}-landing` and nothing was\n looking there. So a merchant pressed the row to find out what\n the number meant and the number disappeared — which is worse\n than never showing it.\n\n Most counts belong to a real child and land on it by id;\n this is for the ones that belong to the hub itself, which is\n to say to its landing page. Low stock is the live example:\n it is about the catalogue, and the catalogue IS the hub's\n landing row. */}\n {rows.map((c, i) =>\n renderRow(\n c,\n true,\n landing.length && i === at ? badges?.[group.id] : undefined,\n ),\n )}\n </motion.ul>\n )}\n </AnimatePresence>\n </li>\n );\n };\n\n /**\n * A work-surface row, which is a hub when it carries children.\n *\n * The fixed spine already had this shape (Customers opens onto Inquiries and\n * Reviews); Operations rows could not, so anything attached to one had to\n * become its own top-level row or be reached from inside the page. Neither is\n * right for a queue like Waitlist: as a peer row it claims to be a second\n * equal destination, and buried in the page header it competes with the\n * page's own actions and pushed the Appointments title down to \"App…\".\n */\n const renderWorkRow = (item: NavItem) =>\n item.children?.length\n ? renderHub({\n id: item.id,\n label: item.name,\n icon: item.icon,\n href: item.href,\n items: item.children,\n alwaysHub: true,\n landingLabel: item.landingLabel,\n })\n : renderRow(item);\n\n // Settings sub-nav that drills into the sidebar (Back returns you out).\n return {\n renderIcon,\n renderHighlight,\n renderRow,\n renderHub,\n renderWorkRow,\n ownsRoute,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/nav-renderers.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { ChevronDown } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { eases } from '@withpotter/lib/motion';\nimport { prefersReducedMotion } from './nav-highlights';\nimport { navIcon } from '../shell/icons';\nimport { RailLabel } from './rail-label';\nimport { type NavItem, type NavGroup } from './config';\nimport type { NavRendererDeps } from './nav-renderer-deps';\nimport { NavHighlight, NavPending, SubHighlight } from './nav-highlights';\n\n/**\n * The air under an open hub, before the next top-level row.\n *\n * The rail's gaps are what say where one thing stops and the next begins, and\n * an open hub had nothing to say it: its last child sat 2px above the next\n * top-level row — the same 2px that separates two children — so the row after\n * an open hub read as the last item IN it.\n *\n * 6px, not 30. With the group's own 2px that is 8px, four times a sibling gap\n * and unmistakable, and still well under the 20px that opens a new SECTION. A\n * gap bigger than the section break would rank an open hub above a heading,\n * which is backwards: the hub is one row inside the section it sits in.\n */\nconst SUB_GAP = 6;\n\nexport type { NavRendererDeps } from './nav-renderer-deps';\n\n/**\n * One highlight per open hub, MOVED between its children rather than\n * replaced on each of them.\n *\n * The obvious version — a `layoutId` on a pill that unmounts from one row and\n * mounts on the next — is what this replaces. Framer took ownership of the\n * element (it carried the projection styles) but resolved no delta and put it\n * down on the new row with the frames in between missing, and chasing that\n * through framer's projection internals is not worth what it buys. A pill\n * that never unmounts cannot fail to match itself: `y` is an ordinary\n * animated value on one persistent element, so there is nothing to hand off.\n *\n * `offsetTop` off the row rather than index × pitch, so the row height, the\n * gap and the group's padding stay facts of the stylesheet. Read in a layout\n * effect, before paint, so the pill is never seen at the wrong row.\n *\n * A spring, not a curve: this is the one thing in the nav that genuinely\n * moves, and clicking a third row mid-flight has to reverse from wherever it\n * got to, carrying its velocity.\n */\n/**\n * Read at render rather than through `useReducedMotion`, because\n * `createNavRenderers` is a plain factory and not a hook — it cannot hold one.\n * Non-reactive is correct here anyway: the transition config is consulted when\n * an animation starts, so a preference changed mid-session applies to the next\n * open rather than to one already in flight.\n */\n/**\n * The sidebar's row, hub and section renderers.\n *\n * A factory rather than components: each one reads seven values that belong to\n * the sidebar's render pass (what is active, what the merchant has toggled\n * open), and threading those through props one component at a time was what\n * kept them inside a 1,510-line file in the first place.\n *\n * Anything with STATE must stay outside it. This runs on every render of the\n * sidebar, so a component declared in here is a new function identity each\n * time, and React unmounts and remounts it on every pass — which silently\n * destroys any hook state it was holding. That is what defeated the first two\n * attempts at a sliding highlight: `SubHighlight` was resetting its motion\n * values before it could ever animate them, and it looked exactly like a\n * framer bug. `SubHighlight` and `NavPending` live above for that reason.\n */\nexport function createNavRenderers({\n collapsed,\n warm,\n activeHref,\n isActive,\n hubChoice,\n setHubOpen,\n getGroupItems,\n badges,\n isNew,\n}: NavRendererDeps) {\n const renderIcon = (\n id: string | undefined,\n Fallback: LucideIcon,\n size = 18,\n ) => {\n const Console = id ? navIcon(id) : null;\n if (Console) return <Console width={size} height={size} />;\n return <Fallback width={size} height={size} />;\n };\n\n // The highlight on a top-level row.\n //\n // It still does not TRAVEL: there is no \"between\" for a row whose neighbours\n // are unrelated destinations, only a jump the length of the whole sidebar,\n // which is what got the old shared-layout pill torn out. But it no longer\n // simply appears either. Coming back up out of a hub, the pill rises the\n // same eight pixels the sub-pill drops, so the two halves of that gesture\n // are the same gesture seen from either end.\n const renderHighlight = (active: boolean) =>\n active ? <NavHighlight /> : null;\n\n /**\n * A nav row. Sub-rows carry no icon.\n *\n * A glyph beside a child was doing no work the layout wasn't already doing.\n * A child is identified by the parent it sits under and the indent it sits\n * at, and a second column of icons only made the tree read as two lists of\n * peers stacked on each other. Dropping it also lands the child's label\n * directly under the parent's, so the labels form one column rather than two\n * ragged ones. Icons stay where they still distinguish: the top level.\n */\n /**\n * The count a row wears, or nothing.\n *\n * Hidden when the rail is collapsed: at that width the label is gone too,\n * and a bare digit beside an icon names nothing.\n */\n const renderBadge = (n: number | undefined) =>\n !collapsed && !!n ? (\n <span className=\"navbadge squircle\" aria-label={`${n} need attention`}>\n {/* Capped, because the badge is a fixed square: `.squircle` is a mask\n stretched to the box, so a third digit would widen it and distort the\n corners away from every other squircle in the app. Past 99 the exact\n number has stopped being the point. */}\n {n > 99 ? '99+' : n}\n </span>\n ) : null;\n\n /**\n * A CLOSED HUB CARRIES WHAT IS INSIDE IT.\n *\n * Notifications land on the row their link names, and most of those rows are\n * a hub's CHILD — `/dashboard/inventory` is inside Inventory, which is shut\n * on most screens. Counting only the hub's own id meant the badge existed\n * and was invisible on every business that had one, which is the exact bug\n * this whole feature was replacing the bell to avoid.\n *\n * Only while closed. Open, the children are on screen wearing their own\n * numbers, and a total above them would be the same work counted twice.\n */\n const hubTotal = (group: NavGroup) =>\n (badges?.[group.id] ?? 0) +\n (group.items ?? []).reduce((sum, c) => sum + (badges?.[c.id] ?? 0), 0);\n\n const renderRow = (item: NavItem, sub = false, count?: number) => {\n const active = item.href === activeHref;\n return (\n // Keyed by href, not id. A business running several archetypes gets one\n // `catalog` row per archetype — Services, Properties, Menu — all sharing\n // the id and differing only by their intent-scoped href, which React saw\n // as duplicate keys and warned it may drop rows for.\n <li className=\"navitem\" key={item.href}>\n <RailLabel label={item.name} collapsed={collapsed}>\n <Link\n href={item.href}\n prefetch={false}\n onMouseEnter={() => warm(item.href)}\n onFocus={() => warm(item.href)}\n // Touch has no hover, so a tap used to hit a stone-cold route.\n // pointerdown lands before click and buys the prefetch a head start.\n onPointerDown={() => warm(item.href)}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? item.name : undefined}\n >\n {/* A sub-row's highlight belongs to its GROUP, which owns one and\n slides it. Rendering a second one here would put a static pill\n under the moving one on the row it just left. */}\n {!sub && renderHighlight(active)}\n <NavPending />\n {!sub && (\n <span className=\"ic\">{renderIcon(item.id, item.icon)}</span>\n )}\n <span className=\"lbl\">{item.name}</span>\n {/* Work waiting, not how many rows the page has. Rendered only\n when there is something to do, so the row is unmarked in the\n ordinary case and a number means something every time it\n appears. */}\n {/* NEW, on a row that has only just appeared. John, 2026-09-03:\n *\"it should show New on the new things added to the sidebar\"*.\n A merchant who switches on bookings gets three rows they have\n never seen among nine they know by shape, and without this the\n thing they just chose is the hardest thing on the rail to find.\n\n Before the count, and never both: a row cannot be simultaneously\n brand new and carrying work, and if it somehow is, the count is\n the one that needs doing. */}\n {isNew?.(item.id) && !badges?.[item.id] ? (\n <span className=\"navnew\">New</span>\n ) : (\n renderBadge(count ?? badges?.[item.id])\n )}\n </Link>\n </RailLabel>\n </li>\n );\n };\n\n // A hub row. With ≤1 child it's a plain link; with real sub-pages it gets a\n // hover-revealed chevron that expands the children inline (label still\n // navigates to the hub landing — only the chevron toggles).\n /**\n * A nav section, headed only when the heading is doing work. One item under a\n * heading is a label restating the row beneath it; two or more is a group.\n * Falsy children are dropped first so the count reflects what actually\n * renders, not how many slots were passed.\n */\n /**\n * Does the current route live inside these items?\n *\n * A section that owns the route is forced open, so the page you are on always\n * has a highlighted row. Hubs are checked through their children too — the\n * route is usually a child, not the hub's own landing page.\n */\n const ownsRoute = (items: (NavItem | NavGroup | undefined)[]) =>\n items.some((item) => {\n if (!item) return false;\n if ('href' in item && item.href && isActive(item.href)) return true;\n const children = 'id' in item ? getGroupItems(item as NavGroup) : [];\n return children.some((c) => c.href && isActive(c.href));\n });\n\n const renderHub = (group: NavGroup) => {\n const children = getGroupItems(group);\n if (children.length === 0 || (children.length === 1 && !group.alwaysHub)) {\n const active = group.href === activeHref;\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed={collapsed}>\n <Link\n href={group.href ?? '/dashboard'}\n prefetch={false}\n onMouseEnter={() => warm(group.href ?? '/dashboard')}\n onFocus={() => warm(group.href ?? '/dashboard')}\n onPointerDown={() => warm(group.href ?? '/dashboard')}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? group.label : undefined}\n >\n {renderHighlight(active)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n /**\n * The hub's own landing page, sitting among its children instead of on the\n * hub's row.\n *\n * Every hub here had a real screen behind it, and putting that screen on\n * the container's row is what made the nav ambiguous: one click both\n * navigated and disclosed, and the highlight settled on the parent while\n * the list underneath it — which had appeared on that same click — read as\n * a menu still awaiting a choice. As a child it is an ordinary\n * destination, reachable by name, and the highlight lands where you\n * actually are.\n */\n const landing: NavItem[] = group.href\n ? [\n {\n id: `${group.id}-landing`,\n // `group.label` has already been through renameGroup, so a hub the\n // merchant renamed carries that name into its own landing row.\n //\n // NOTHING RELIES ON THE FALLBACK, and a new hub should not start.\n // Every group in the spine names its own landing child, because\n // \"Overview\" distinguishes nothing — every hub could carry one, so\n // the word tells the merchant nothing about which hub they are in.\n // A hub with an href owes its landing page a name that says what is\n // on it. The fallback stays only so a group under construction\n // renders rather than throwing.\n name: (group.landingLabel ?? 'Overview').replace(\n '{label}',\n group.label.toLowerCase(),\n ),\n href: group.href,\n icon: group.icon,\n },\n ]\n : [];\n const at = Math.min(group.landingAt ?? 0, children.length);\n const rows = [...children.slice(0, at), ...landing, ...children.slice(at)];\n // Which row the group's highlight sits on. -1 means none of them, so the\n // group renders no pill at all rather than one parked on the first row.\n const activeIndex = rows.findIndex((c) => c.href === activeHref);\n const childActive = activeIndex >= 0;\n // Auto-open when this hub owns the current route, unless the merchant has\n // toggled one open themselves (their choice wins, and it is exclusive —\n // opening this hub is what closed the last one).\n const open = hubChoice === undefined ? childActive : hubChoice === group.id;\n /**\n * Collapsed, the row falls back to being a link.\n *\n * Not a fudge: the rail hides `.navsub` entirely, so there is no list for\n * the row to open and therefore no ambiguity for it to create. The\n * ambiguity this whole change removes only exists when the children are on\n * screen. With them hidden, a container with nowhere to go is just a dead\n * target, so the row goes to its landing page as it always did.\n */\n if (collapsed) {\n const href = group.href ?? '/dashboard';\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed>\n <Link\n href={href}\n prefetch={false}\n className={'navlink' + (href === activeHref ? ' active' : '')}\n aria-label={group.label}\n onMouseEnter={() => warm(href)}\n onPointerDown={() => warm(href)}\n >\n {renderHighlight(href === activeHref)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n return (\n <li className=\"navitem\" key={group.id}>\n {/* A button, not a link. The whole row toggles — reaching for a 20px\n chevron to see what is inside a nav item is a target you have to aim\n at, when the row already reads as \"there is more here\". It carries no\n highlight and no pending state, because it goes nowhere. */}\n {/* Warms the whole group, not the row.\n The hub itself goes nowhere, so there is nothing to prefetch FOR it —\n but a hub is only ever pressed on the way to a child, and the pointer\n reaching this row is the earliest honest signal of that. It buys the\n children the length of one pointer travel plus one click, which is the\n difference between a child that renders and a child that waits: every\n route here is `prefetch={false}`, and a cold one was measured at 1.7s.\n Both handlers, because touch has no hover. */}\n <button\n type=\"button\"\n className={'navlink navhub' + (open ? ' open' : '')}\n onClick={() => {\n rows.forEach((c) => warm(c.href));\n setHubOpen(group.id, !open);\n }}\n onMouseEnter={() => rows.forEach((c) => warm(c.href))}\n onFocus={() => rows.forEach((c) => warm(c.href))}\n aria-expanded={open}\n aria-controls={open ? `navsub-${group.id}` : undefined}\n >\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {!open && renderBadge(hubTotal(group))}\n <ChevronDown className=\"chev\" width={14} height={14} />\n </button>\n {/* No id-collision guard here any more. It existed because a business\n running several archetypes gets one `catalog` row per archetype, all\n sharing an id, so all three resolved to the same console glyph — and\n the row's own icon had to win. Children have no glyph to collide\n over now; their labels already differ. */}\n {/* THE CHILDREN GROW AND COLLAPSE, they do not blink.\n This was `{open && <ul>}` — a bare mount and unmount, so a hub's\n children appeared and vanished between one frame and the next while\n the chevron above them rotated over 200ms. One control, two\n different speeds, and the faster one was a cut.\n\n Height, not opacity alone: the rows below have to MOVE, and a fade\n over a jump still jumps. `height: auto` is the only honest target\n here because a hub's child count varies, and Motion measures it\n rather than us guessing a row pitch that breaks the moment a label\n wraps.\n\n Opacity finishes early on the way in and starts late on the way out,\n so the children are solid for most of the travel — content that\n fades over the full distance reads as sluggish even when the\n distance is right.\n\n The bottom margin travels WITH the height rather than sitting in\n the stylesheet. As CSS it would hold its full value all the way\n through the close and then vanish on the frame the list unmounts —\n the one snap in an otherwise continuous move, at the bottom edge\n where the rows below are already travelling. */}\n <AnimatePresence initial={false}>\n {open && (\n <motion.ul\n id={`navsub-${group.id}`}\n className=\"navsub\"\n initial={{ height: 0, opacity: 0, marginBottom: 0 }}\n animate={{ height: 'auto', opacity: 1, marginBottom: SUB_GAP }}\n exit={{ height: 0, opacity: 0, marginBottom: 0 }}\n transition={\n prefersReducedMotion()\n ? { duration: 0 }\n : {\n height: { duration: 0.24, ease: eases.settle },\n marginBottom: { duration: 0.24, ease: eases.settle },\n opacity: { duration: 0.16, ease: eases.out },\n }\n }\n // Clipped, or the rows spill past the collapsing box on the way\n // out and the whole thing reads as a glitch rather than a close.\n style={{ overflow: 'hidden' }}\n >\n {activeIndex >= 0 && (\n <SubHighlight key={group.id} index={activeIndex} />\n )}\n {/* THE HUB'S OWN COUNT FOLLOWS IT DOWN.\n John, 2026-09-03: *\"when I click on the parent nav, where\n does the notification thingy go, what sub nav is the\n notification from, why don't I see it\"*.\n\n Nowhere, was the answer. A count keyed on the HUB's id showed\n on the closed row and then vanished on open, because the\n landing child carries `${group.id}-landing` and nothing was\n looking there. So a merchant pressed the row to find out what\n the number meant and the number disappeared — which is worse\n than never showing it.\n\n Most counts belong to a real child and land on it by id;\n this is for the ones that belong to the hub itself, which is\n to say to its landing page. Low stock is the live example:\n it is about the catalogue, and the catalogue IS the hub's\n landing row. */}\n {rows.map((c, i) =>\n renderRow(\n c,\n true,\n landing.length && i === at ? badges?.[group.id] : undefined,\n ),\n )}\n </motion.ul>\n )}\n </AnimatePresence>\n </li>\n );\n };\n\n /**\n * A work-surface row, which is a hub when it carries children.\n *\n * The fixed spine already had this shape (Customers opens onto Inquiries and\n * Reviews); Operations rows could not, so anything attached to one had to\n * become its own top-level row or be reached from inside the page. Neither is\n * right for a queue like Waitlist: as a peer row it claims to be a second\n * equal destination, and buried in the page header it competes with the\n * page's own actions and pushed the Appointments title down to \"App…\".\n */\n const renderWorkRow = (item: NavItem) =>\n item.children?.length\n ? renderHub({\n id: item.id,\n label: item.name,\n icon: item.icon,\n href: item.href,\n items: item.children,\n alwaysHub: true,\n landingLabel: item.landingLabel,\n })\n : renderRow(item);\n\n // Settings sub-nav that drills into the sidebar (Back returns you out).\n return {\n renderIcon,\n renderHighlight,\n renderRow,\n renderHub,\n renderWorkRow,\n ownsRoute,\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useSyncExternalStore } from "react";
3
+ import { useCallback, useId, useSyncExternalStore } from "react";
4
4
  import { Check, ChevronDown, Settings2 } from "lucide-react";
5
5
  import { EditableName } from "./rail-editable";
6
6
  const STORAGE_KEY = "pb_nav_sections_closed";
@@ -47,6 +47,7 @@ function NavSection({
47
47
  onDone
48
48
  }) {
49
49
  const key = foldKey ?? label;
50
+ const listId = useId();
50
51
  const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);
51
52
  const items = children.filter(Boolean);
52
53
  const isClosed = !containsActive && closedMap[key] === true;
@@ -91,7 +92,7 @@ function NavSection({
91
92
  type: "button",
92
93
  onClick: onConfigure,
93
94
  className: "navheading-gear",
94
- "aria-label": "Customize sidebar",
95
+ "aria-label": `Customize sidebar, ${label} section`,
95
96
  title: "Customize sidebar",
96
97
  children: /* @__PURE__ */ jsx(Settings2, { "aria-hidden": "true" })
97
98
  }
@@ -103,6 +104,7 @@ function NavSection({
103
104
  onClick: toggle,
104
105
  className: "navheading navheading-toggle",
105
106
  "aria-expanded": !isClosed,
107
+ "aria-controls": isClosed ? void 0 : listId,
106
108
  children: [
107
109
  /* @__PURE__ */ jsx("span", { children: label }),
108
110
  /* @__PURE__ */ jsx(
@@ -115,7 +117,7 @@ function NavSection({
115
117
  ]
116
118
  }
117
119
  ),
118
- !isClosed && /* @__PURE__ */ jsx("ul", { className: "navgroup", children: items })
120
+ !isClosed && /* @__PURE__ */ jsx("ul", { id: listId, className: "navgroup", children: items })
119
121
  ] });
120
122
  }
121
123
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/nav-section.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useSyncExternalStore } from 'react';\nimport { Check, ChevronDown, Settings2 } from 'lucide-react';\nimport { EditableName } from './rail-editable';\n\nconst STORAGE_KEY = 'pb_nav_sections_closed';\n\n/**\n * Which sections the merchant has folded away, as an external store.\n *\n * Not `useState` + `useEffect`: localStorage does not exist while the server\n * renders, so reading it into initial state mismatches the HTML, and reading it\n * in an effect is a synchronous setState in an effect — a cascading render the\n * lint rule rejects. `useSyncExternalStore` is built for exactly this, and it\n * is the pattern the home money snapshot already uses here.\n */\nlet closedCache: Record<string, boolean> = {};\nlet closedRaw: string | null = null;\nconst listeners = new Set<() => void>();\n\nfunction readClosed(): Record<string, boolean> {\n if (typeof window === 'undefined') return closedCache;\n const raw = localStorage.getItem(STORAGE_KEY);\n // Cached by raw string so the snapshot is referentially stable between\n // renders — a fresh object every call makes useSyncExternalStore loop.\n if (raw !== closedRaw) {\n closedRaw = raw;\n try {\n closedCache = raw ? JSON.parse(raw) : {};\n } catch {\n closedCache = {};\n }\n }\n return closedCache;\n}\n\n/** The server has no localStorage, so nothing is folded on first paint. */\nconst serverSnapshot = () => closedCache;\n\nfunction subscribe(fn: () => void) {\n listeners.add(fn);\n return () => listeners.delete(fn);\n}\n\nfunction setSectionClosed(label: string, closed: boolean) {\n const next = { ...readClosed(), [label]: closed };\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n } catch {\n /* private mode — the choice just won't survive the visit */\n }\n closedRaw = JSON.stringify(next);\n closedCache = next;\n listeners.forEach((fn) => fn());\n}\n\n/**\n * A nav section — \"Operations\", \"Grow\", \"Build\" — that can be folded away.\n *\n * A heading earns its row by grouping. Over one item it costs a line of height\n * and a label restating what the item already says, so below two children the\n * section renders as a plain run of rows with no heading and nothing to toggle:\n * there is no point offering to hide a single link behind a chevron.\n *\n * Folding is the merchant's choice and it sticks, per section, across visits.\n * But a section is FORCED OPEN while it owns the current route — otherwise the\n * page you are on has no highlighted row anywhere in the nav, which reads as\n * the sidebar having lost your place rather than as a section being shut.\n *\n * On the collapsed icon-only rail there are no headings, so there is nothing to\n * fold and the rows render bare.\n */\nexport function NavSection({\n label,\n children,\n collapsed,\n containsActive,\n keepLabel = false,\n onConfigure,\n foldKey,\n onRenameLabel,\n onDone,\n}: {\n label: string;\n children: React.ReactNode[];\n /** The icon-only rail: no headings, so no toggle. */\n collapsed: boolean;\n /** The current route lives in this section — it cannot be folded shut. */\n containsActive: boolean;\n /**\n * Keep the heading over a single row.\n *\n * The default below drops it, and that rule is right almost everywhere: a\n * heading over one item usually restates the item (\"Build · Website\"), which\n * is the case this file was written to stop.\n *\n * The exception is a heading that answers a DIFFERENT question from its row.\n * \"Your business · Add more\" is the one in the app: the row names the act\n * and the heading names the scope, and the scope is the part a merchant was\n * missing — the label read \"Add to Box\" until somebody pointed out that a\n * shop is not adding to Box. Dropping the heading there would take the\n * answer away again.\n *\n * Opt-in rather than a judgement the component makes, because it cannot tell\n * the two apart and the caller can.\n */\n keepLabel?: boolean;\n /**\n * Opens the sidebar editor (reorder, hide, rename). Shown as a gear beside\n * the chevron when the pointer or focus is on the heading, so a resting\n * sidebar stays quiet. A sibling of the toggle, not inside it: a button\n * cannot hold a button.\n */\n onConfigure?: () => void;\n /**\n * What the fold state is remembered under. Defaults to `label`, which is\n * right until the merchant renames the section: then the label changes and\n * the remembered fold would be lost, so the caller pins it to the default.\n */\n foldKey?: string;\n /**\n * Present only while the rail is being edited: the heading becomes a field\n * and the section stays open (there is nothing to fold while you edit).\n */\n onRenameLabel?: (next: string) => void;\n /** While editing: leaves edit mode. Drawn on the heading being edited. */\n onDone?: () => void;\n}) {\n const key = foldKey ?? label;\n const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);\n const items = children.filter(Boolean);\n const isClosed = !containsActive && closedMap[key] === true;\n const toggle = useCallback(\n () => setSectionClosed(key, !isClosed),\n [key, isClosed],\n );\n\n if (!items.length) return null;\n\n // One item: a heading would only restate the row beneath it — unless the\n // caller says the heading carries something the row does not. See `keepLabel`.\n if ((items.length === 1 && !keepLabel) || collapsed) {\n return (\n <div className=\"navsection-plain\">\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n if (onRenameLabel) {\n return (\n <div className=\"navsection\">\n <div className=\"navheading nav-edit-heading\">\n <EditableName\n value={label}\n label={`Rename section ${label}`}\n onCommit={onRenameLabel}\n />\n {onDone && (\n <button\n type=\"button\"\n onClick={onDone}\n className=\"nav-edit-done\"\n aria-label=\"Done customizing\"\n title=\"Done\"\n >\n <Check aria-hidden=\"true\" />\n </button>\n )}\n </div>\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n return (\n <div className=\"navsection\">\n {onConfigure && (\n <button\n type=\"button\"\n onClick={onConfigure}\n className=\"navheading-gear\"\n aria-label=\"Customize sidebar\"\n title=\"Customize sidebar\"\n >\n <Settings2 aria-hidden=\"true\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={toggle}\n className=\"navheading navheading-toggle\"\n aria-expanded={!isClosed}\n >\n <span>{label}</span>\n <ChevronDown\n className={'navheading-chevron' + (isClosed ? ' closed' : '')}\n aria-hidden=\"true\"\n />\n </button>\n {!isClosed && <ul className=\"navgroup\">{items}</ul>}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/nav-section.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useId, useSyncExternalStore } from 'react';\nimport { Check, ChevronDown, Settings2 } from 'lucide-react';\nimport { EditableName } from './rail-editable';\n\nconst STORAGE_KEY = 'pb_nav_sections_closed';\n\n/**\n * Which sections the merchant has folded away, as an external store.\n *\n * Not `useState` + `useEffect`: localStorage does not exist while the server\n * renders, so reading it into initial state mismatches the HTML, and reading it\n * in an effect is a synchronous setState in an effect — a cascading render the\n * lint rule rejects. `useSyncExternalStore` is built for exactly this, and it\n * is the pattern the home money snapshot already uses here.\n */\nlet closedCache: Record<string, boolean> = {};\nlet closedRaw: string | null = null;\nconst listeners = new Set<() => void>();\n\nfunction readClosed(): Record<string, boolean> {\n if (typeof window === 'undefined') return closedCache;\n const raw = localStorage.getItem(STORAGE_KEY);\n // Cached by raw string so the snapshot is referentially stable between\n // renders — a fresh object every call makes useSyncExternalStore loop.\n if (raw !== closedRaw) {\n closedRaw = raw;\n try {\n closedCache = raw ? JSON.parse(raw) : {};\n } catch {\n closedCache = {};\n }\n }\n return closedCache;\n}\n\n/** The server has no localStorage, so nothing is folded on first paint. */\nconst serverSnapshot = () => closedCache;\n\nfunction subscribe(fn: () => void) {\n listeners.add(fn);\n return () => listeners.delete(fn);\n}\n\nfunction setSectionClosed(label: string, closed: boolean) {\n const next = { ...readClosed(), [label]: closed };\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n } catch {\n /* private mode — the choice just won't survive the visit */\n }\n closedRaw = JSON.stringify(next);\n closedCache = next;\n listeners.forEach((fn) => fn());\n}\n\n/**\n * A nav section — \"Operations\", \"Grow\", \"Build\" — that can be folded away.\n *\n * A heading earns its row by grouping. Over one item it costs a line of height\n * and a label restating what the item already says, so below two children the\n * section renders as a plain run of rows with no heading and nothing to toggle:\n * there is no point offering to hide a single link behind a chevron.\n *\n * Folding is the merchant's choice and it sticks, per section, across visits.\n * But a section is FORCED OPEN while it owns the current route — otherwise the\n * page you are on has no highlighted row anywhere in the nav, which reads as\n * the sidebar having lost your place rather than as a section being shut.\n *\n * On the collapsed icon-only rail there are no headings, so there is nothing to\n * fold and the rows render bare.\n */\nexport function NavSection({\n label,\n children,\n collapsed,\n containsActive,\n keepLabel = false,\n onConfigure,\n foldKey,\n onRenameLabel,\n onDone,\n}: {\n label: string;\n children: React.ReactNode[];\n /** The icon-only rail: no headings, so no toggle. */\n collapsed: boolean;\n /** The current route lives in this section — it cannot be folded shut. */\n containsActive: boolean;\n /**\n * Keep the heading over a single row.\n *\n * The default below drops it, and that rule is right almost everywhere: a\n * heading over one item usually restates the item (\"Build · Website\"), which\n * is the case this file was written to stop.\n *\n * The exception is a heading that answers a DIFFERENT question from its row.\n * \"Your business · Add more\" is the one in the app: the row names the act\n * and the heading names the scope, and the scope is the part a merchant was\n * missing — the label read \"Add to Box\" until somebody pointed out that a\n * shop is not adding to Box. Dropping the heading there would take the\n * answer away again.\n *\n * Opt-in rather than a judgement the component makes, because it cannot tell\n * the two apart and the caller can.\n */\n keepLabel?: boolean;\n /**\n * Opens the sidebar editor (reorder, hide, rename). Shown as a gear beside\n * the chevron when the pointer or focus is on the heading, so a resting\n * sidebar stays quiet. A sibling of the toggle, not inside it: a button\n * cannot hold a button.\n */\n onConfigure?: () => void;\n /**\n * What the fold state is remembered under. Defaults to `label`, which is\n * right until the merchant renames the section: then the label changes and\n * the remembered fold would be lost, so the caller pins it to the default.\n */\n foldKey?: string;\n /**\n * Present only while the rail is being edited: the heading becomes a field\n * and the section stays open (there is nothing to fold while you edit).\n */\n onRenameLabel?: (next: string) => void;\n /** While editing: leaves edit mode. Drawn on the heading being edited. */\n onDone?: () => void;\n}) {\n const key = foldKey ?? label;\n const listId = useId();\n const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);\n const items = children.filter(Boolean);\n const isClosed = !containsActive && closedMap[key] === true;\n const toggle = useCallback(\n () => setSectionClosed(key, !isClosed),\n [key, isClosed],\n );\n\n if (!items.length) return null;\n\n // One item: a heading would only restate the row beneath it — unless the\n // caller says the heading carries something the row does not. See `keepLabel`.\n if ((items.length === 1 && !keepLabel) || collapsed) {\n return (\n <div className=\"navsection-plain\">\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n if (onRenameLabel) {\n return (\n <div className=\"navsection\">\n <div className=\"navheading nav-edit-heading\">\n <EditableName\n value={label}\n label={`Rename section ${label}`}\n onCommit={onRenameLabel}\n />\n {onDone && (\n <button\n type=\"button\"\n onClick={onDone}\n className=\"nav-edit-done\"\n aria-label=\"Done customizing\"\n title=\"Done\"\n >\n <Check aria-hidden=\"true\" />\n </button>\n )}\n </div>\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n return (\n <div className=\"navsection\">\n {onConfigure && (\n <button\n type=\"button\"\n onClick={onConfigure}\n className=\"navheading-gear\"\n aria-label={`Customize sidebar, ${label} section`}\n title=\"Customize sidebar\"\n >\n <Settings2 aria-hidden=\"true\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={toggle}\n className=\"navheading navheading-toggle\"\n aria-expanded={!isClosed}\n aria-controls={isClosed ? undefined : listId}\n >\n <span>{label}</span>\n <ChevronDown\n className={'navheading-chevron' + (isClosed ? ' closed' : '')}\n aria-hidden=\"true\"\n />\n </button>\n {!isClosed && (\n <ul id={listId} className=\"navgroup\">\n {items}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/components",
3
- "version": "0.13.0-next.82",
3
+ "version": "0.13.0-next.85",
4
4
  "description": "Box dashboard feature components: editors, mobile screens, pickers, and composite UI built on @withpotter/ui-kit.",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": [
@@ -50,9 +50,9 @@
50
50
  "@tiptap/react": "^3.22.5",
51
51
  "@tiptap/starter-kit": "^3.22.5",
52
52
  "@tiptap/suggestion": "^3.22.5",
53
- "@withpotter/hooks": "^0.13.0-next.82",
54
- "@withpotter/lib": "^0.13.0-next.82",
55
- "@withpotter/ui-kit": "^0.13.0-next.82",
53
+ "@withpotter/hooks": "^0.13.0-next.85",
54
+ "@withpotter/lib": "^0.13.0-next.85",
55
+ "@withpotter/ui-kit": "^0.13.0-next.85",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",