chore: snapshot workflow migration progress
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { ArrowRight, Plus, Search } from 'lucide-react'
|
||||
|
||||
import type { WorkflowNodeDefinition } from '../../api/workflows'
|
||||
import type { StepCategory, WorkflowNodeDefinition } from '../../api/workflows'
|
||||
import {
|
||||
AUTHORING_STAGE_DESCRIPTIONS,
|
||||
AUTHORING_STAGE_LABELS,
|
||||
AUTHORING_STAGE_STYLES,
|
||||
CATEGORY_COLORS,
|
||||
CATEGORY_LABELS,
|
||||
FAMILY_FILTER_DESCRIPTIONS,
|
||||
FAMILY_FILTER_LABELS,
|
||||
FAMILY_FILTER_STYLES,
|
||||
NODE_KIND_FILTER_LABELS,
|
||||
NODE_LIBRARY_GROUP_DESCRIPTIONS,
|
||||
NODE_LIBRARY_GROUP_LABELS,
|
||||
NODE_LIBRARY_GROUP_STYLES,
|
||||
getDefinitionBadges,
|
||||
getDefinitionFamily,
|
||||
getDefinitionModuleLabel,
|
||||
getDefinitionModuleNamespace,
|
||||
type WorkflowGraphFamily,
|
||||
type WorkflowNodeFamilyFilter,
|
||||
@@ -20,10 +24,11 @@ import {
|
||||
type WorkflowNodeLibraryGroup,
|
||||
} from './workflowNodeLibrary'
|
||||
import {
|
||||
buildWorkflowNodeCatalog,
|
||||
buildWorkflowNodeCatalogModel,
|
||||
filterWorkflowNodeDefinitions,
|
||||
getAvailableFamilyFilters,
|
||||
} from './workflowNodeCatalog'
|
||||
import { STARTER_NODE_STEP_ORDER, STARTER_PATH_TITLES } from './workflowAuthoringGuidance'
|
||||
|
||||
type WorkflowNodeCatalogBrowserProps = {
|
||||
definitions: WorkflowNodeDefinition[]
|
||||
@@ -37,10 +42,21 @@ type WorkflowNodeCatalogBrowserProps = {
|
||||
autoFocusSearch?: boolean
|
||||
}
|
||||
|
||||
type WorkflowNodeCatalogModuleFilter = {
|
||||
namespace: string
|
||||
type WorkflowNodeCategoryFilter = 'all' | StepCategory
|
||||
|
||||
const CATEGORY_FILTERS: WorkflowNodeCategoryFilter[] = ['all', 'input', 'processing', 'rendering', 'output']
|
||||
|
||||
const CATEGORY_FILTER_LABELS: Record<WorkflowNodeCategoryFilter, string> = {
|
||||
all: 'All Categories',
|
||||
input: 'Input',
|
||||
processing: 'Processing',
|
||||
rendering: 'Rendering',
|
||||
output: 'Output',
|
||||
}
|
||||
|
||||
type FilterPillOption<T extends string> = {
|
||||
value: T
|
||||
label: string
|
||||
count: number
|
||||
}
|
||||
|
||||
function readContractList(contract: Record<string, unknown>, key: string) {
|
||||
@@ -60,6 +76,42 @@ function formatContractLabel(value: string) {
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function FilterPillGroup<T extends string>({
|
||||
title,
|
||||
options,
|
||||
activeValue,
|
||||
onChange,
|
||||
}: {
|
||||
title: string
|
||||
options: FilterPillOption<T>[]
|
||||
activeValue: T
|
||||
onChange: (value: T) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-content-secondary">
|
||||
{title}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{options.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => onChange(option.value)}
|
||||
className={`rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors ${
|
||||
activeValue === option.value
|
||||
? 'bg-accent text-white'
|
||||
: 'border border-border-default bg-surface text-content-secondary hover:bg-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkflowNodeCatalogBrowser({
|
||||
definitions,
|
||||
graphFamily,
|
||||
@@ -76,11 +128,15 @@ export function WorkflowNodeCatalogBrowser({
|
||||
graphFamily === 'mixed' ? 'all' : graphFamily,
|
||||
)
|
||||
const [kindFilter, setKindFilter] = useState<WorkflowNodeKindFilter>('all')
|
||||
const [categoryFilter, setCategoryFilter] = useState<WorkflowNodeCategoryFilter>('all')
|
||||
const [moduleFilter, setModuleFilter] = useState<string>('all')
|
||||
const [moduleQuery, setModuleQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setFamilyFilter(graphFamily === 'mixed' ? 'all' : graphFamily)
|
||||
setCategoryFilter('all')
|
||||
setModuleFilter('all')
|
||||
setModuleQuery('')
|
||||
}, [graphFamily])
|
||||
|
||||
const availableFamilyFilters = useMemo(
|
||||
@@ -97,25 +153,32 @@ export function WorkflowNodeCatalogBrowser({
|
||||
})
|
||||
}, [definitions, familyFilter, graphFamily, kindFilter, query])
|
||||
|
||||
const moduleFilters = useMemo<WorkflowNodeCatalogModuleFilter[]>(() => {
|
||||
const modules = new Map<string, WorkflowNodeCatalogModuleFilter>()
|
||||
const normalizedModuleQuery = moduleQuery.trim().toLowerCase()
|
||||
|
||||
for (const definition of filteredDefinitions) {
|
||||
const namespace = getDefinitionModuleNamespace(definition)
|
||||
const current = modules.get(namespace)
|
||||
if (current) {
|
||||
current.count += 1
|
||||
continue
|
||||
}
|
||||
modules.set(namespace, {
|
||||
namespace,
|
||||
label: definition.module_key,
|
||||
count: 1,
|
||||
})
|
||||
}
|
||||
const visibleDefinitions = useMemo(() => {
|
||||
const scopedByModule =
|
||||
moduleFilter === 'all'
|
||||
? filteredDefinitions
|
||||
: filteredDefinitions.filter(definition => getDefinitionModuleNamespace(definition) === moduleFilter)
|
||||
|
||||
return Array.from(modules.values()).sort((a, b) => a.label.localeCompare(b.label))
|
||||
}, [filteredDefinitions])
|
||||
const scopedByModuleQuery = !normalizedModuleQuery
|
||||
? scopedByModule
|
||||
: scopedByModule.filter(definition => {
|
||||
const namespace = getDefinitionModuleNamespace(definition).toLowerCase()
|
||||
return (
|
||||
namespace.includes(normalizedModuleQuery) ||
|
||||
getDefinitionModuleLabel(definition).toLowerCase().includes(normalizedModuleQuery) ||
|
||||
definition.module_key.toLowerCase().includes(normalizedModuleQuery) ||
|
||||
definition.label.toLowerCase().includes(normalizedModuleQuery)
|
||||
)
|
||||
})
|
||||
|
||||
if (categoryFilter === 'all') return scopedByModuleQuery
|
||||
return scopedByModuleQuery.filter(definition => definition.category === categoryFilter)
|
||||
}, [categoryFilter, filteredDefinitions, moduleFilter, normalizedModuleQuery])
|
||||
|
||||
const catalogModel = useMemo(() => buildWorkflowNodeCatalogModel(visibleDefinitions), [visibleDefinitions])
|
||||
const moduleFilters = catalogModel.moduleFilters
|
||||
|
||||
useEffect(() => {
|
||||
if (moduleFilter !== 'all' && !moduleFilters.some(module => module.namespace === moduleFilter)) {
|
||||
@@ -123,14 +186,36 @@ export function WorkflowNodeCatalogBrowser({
|
||||
}
|
||||
}, [moduleFilter, moduleFilters])
|
||||
|
||||
const visibleDefinitions = useMemo(() => {
|
||||
if (moduleFilter === 'all') return filteredDefinitions
|
||||
return filteredDefinitions.filter(definition => getDefinitionModuleNamespace(definition) === moduleFilter)
|
||||
}, [filteredDefinitions, moduleFilter])
|
||||
|
||||
const catalogSections = useMemo(() => buildWorkflowNodeCatalog(visibleDefinitions), [visibleDefinitions])
|
||||
const familySections = catalogModel.familySections
|
||||
const firstVisibleDefinition = visibleDefinitions[0]
|
||||
const totalModuleCount = moduleFilters.length
|
||||
const quickInsertDefinitions = useMemo(() => {
|
||||
const prioritizedSteps = STARTER_NODE_STEP_ORDER[graphFamily]
|
||||
if (prioritizedSteps.length === 0) return visibleDefinitions.slice(0, 4)
|
||||
|
||||
const byStep = new Map(visibleDefinitions.map(definition => [definition.step, definition]))
|
||||
return prioritizedSteps
|
||||
.map(step => byStep.get(step))
|
||||
.filter((definition): definition is WorkflowNodeDefinition => Boolean(definition))
|
||||
.slice(0, variant === 'menu' ? 5 : 6)
|
||||
}, [graphFamily, variant, visibleDefinitions])
|
||||
const quickInsertTitle = graphFamily === 'mixed' ? 'Suggested nodes' : STARTER_PATH_TITLES[graphFamily]
|
||||
const runtimeFilterOptions: FilterPillOption<WorkflowNodeKindFilter>[] = [
|
||||
{ value: 'all', label: NODE_KIND_FILTER_LABELS.all },
|
||||
{ value: 'legacy', label: NODE_KIND_FILTER_LABELS.legacy },
|
||||
{ value: 'bridge', label: NODE_KIND_FILTER_LABELS.bridge },
|
||||
{ value: 'graph', label: NODE_KIND_FILTER_LABELS.graph },
|
||||
]
|
||||
const familyFilterOptions = availableFamilyFilters.map(filter => ({
|
||||
value: filter,
|
||||
label: FAMILY_FILTER_LABELS[filter],
|
||||
}))
|
||||
const categoryFilterOptions: FilterPillOption<WorkflowNodeCategoryFilter>[] = CATEGORY_FILTERS.map(
|
||||
filter => ({
|
||||
value: filter,
|
||||
label: CATEGORY_FILTER_LABELS[filter],
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -149,10 +234,13 @@ export function WorkflowNodeCatalogBrowser({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{moduleFilter !== 'all' && (
|
||||
{(moduleFilter !== 'all' || moduleQuery) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setModuleFilter('all')}
|
||||
onClick={() => {
|
||||
setModuleFilter('all')
|
||||
setModuleQuery('')
|
||||
}}
|
||||
className="text-[11px] font-medium text-accent hover:text-accent-hover"
|
||||
>
|
||||
Show all modules
|
||||
@@ -178,39 +266,26 @@ export function WorkflowNodeCatalogBrowser({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['all', 'legacy', 'bridge', 'graph'] as WorkflowNodeKindFilter[]).map(filter => (
|
||||
<button
|
||||
key={filter}
|
||||
type="button"
|
||||
onClick={() => setKindFilter(filter)}
|
||||
className={`rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors ${
|
||||
kindFilter === filter
|
||||
? 'bg-accent text-white'
|
||||
: 'border border-border-default bg-surface text-content-secondary hover:bg-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{NODE_KIND_FILTER_LABELS[filter]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<FilterPillGroup
|
||||
title="Runtime"
|
||||
options={runtimeFilterOptions}
|
||||
activeValue={kindFilter}
|
||||
onChange={setKindFilter}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableFamilyFilters.map(filter => (
|
||||
<button
|
||||
key={filter}
|
||||
type="button"
|
||||
onClick={() => setFamilyFilter(filter)}
|
||||
className={`rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors ${
|
||||
familyFilter === filter
|
||||
? 'bg-accent text-white'
|
||||
: 'border border-border-default bg-surface text-content-secondary hover:bg-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{FAMILY_FILTER_LABELS[filter]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<FilterPillGroup
|
||||
title="Family"
|
||||
options={familyFilterOptions}
|
||||
activeValue={familyFilter}
|
||||
onChange={setFamilyFilter}
|
||||
/>
|
||||
|
||||
<FilterPillGroup
|
||||
title="Category"
|
||||
options={categoryFilterOptions}
|
||||
activeValue={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
/>
|
||||
|
||||
{moduleFilters.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
@@ -222,6 +297,15 @@ export function WorkflowNodeCatalogBrowser({
|
||||
family + runtime scoped
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search size={12} className="absolute left-3 top-1/2 -translate-y-1/2 text-content-muted" />
|
||||
<input
|
||||
value={moduleQuery}
|
||||
onChange={event => setModuleQuery(event.target.value)}
|
||||
placeholder="Search modules"
|
||||
className="w-full rounded-xl border border-border-default bg-surface px-8 py-2 text-xs text-content focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -244,9 +328,9 @@ export function WorkflowNodeCatalogBrowser({
|
||||
? 'bg-accent text-white'
|
||||
: 'border border-border-default bg-surface text-content-secondary hover:bg-surface-hover'
|
||||
}`}
|
||||
title={module.label}
|
||||
title={`${module.label} · ${module.stages.map(stage => AUTHORING_STAGE_LABELS[stage]).join(' / ')}`}
|
||||
>
|
||||
{module.namespace}
|
||||
{module.label}
|
||||
<span className="ml-1 opacity-70">{module.count}</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -258,21 +342,51 @@ export function WorkflowNodeCatalogBrowser({
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['legacy', 'bridge', 'graph'] as WorkflowNodeLibraryGroup[]).map(group => {
|
||||
const count = catalogSections.find(section => section.group === group)?.definitions.length ?? 0
|
||||
const count = catalogModel.runtimeCounts[group] ?? 0
|
||||
if (count === 0) return null
|
||||
return (
|
||||
<span
|
||||
key={group}
|
||||
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium ${NODE_LIBRARY_GROUP_STYLES[group]}`}
|
||||
title={NODE_LIBRARY_GROUP_DESCRIPTIONS[group]}
|
||||
title={NODE_LIBRARY_GROUP_LABELS[group]}
|
||||
>
|
||||
<span>{NODE_KIND_FILTER_LABELS[group]}</span>
|
||||
<span>{NODE_LIBRARY_GROUP_LABELS[group]}</span>
|
||||
<span>{count}</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{quickInsertDefinitions.length > 0 && (
|
||||
<div className="rounded-xl border border-border-default bg-surface-hover/35 p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-content-secondary">
|
||||
Quick Insert
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-content-muted">{quickInsertTitle}</p>
|
||||
</div>
|
||||
<span className="rounded-full border border-border-default bg-surface px-2 py-0.5 text-[11px] text-content-muted">
|
||||
{quickInsertDefinitions.length} picks
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{quickInsertDefinitions.map(definition => (
|
||||
<button
|
||||
key={`quick-${definition.step}`}
|
||||
type="button"
|
||||
onClick={() => onSelectStep?.(definition.step)}
|
||||
disabled={!onSelectStep}
|
||||
className="rounded-full border border-border-default bg-surface px-3 py-1.5 text-xs font-medium text-content transition-colors hover:bg-surface-hover disabled:cursor-default disabled:opacity-60"
|
||||
title={definition.description}
|
||||
>
|
||||
{definition.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visibleDefinitions.length === 0 && (
|
||||
<div className="rounded-2xl border border-dashed border-border-default bg-surface-hover/40 px-4 py-8 text-center">
|
||||
<p className="text-sm font-medium text-content">No matching nodes</p>
|
||||
@@ -292,172 +406,258 @@ export function WorkflowNodeCatalogBrowser({
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{catalogSections.map(section => {
|
||||
const group = section.group as WorkflowNodeLibraryGroup
|
||||
{familySections.map(familySection => {
|
||||
const familyLabel = FAMILY_FILTER_LABELS[familySection.family]
|
||||
const familyDescription = FAMILY_FILTER_DESCRIPTIONS[familySection.family]
|
||||
const familyStyle = FAMILY_FILTER_STYLES[familySection.family]
|
||||
|
||||
return (
|
||||
<div key={group} className="rounded-lg border border-border-default bg-surface-hover/40 p-2">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<span className={`rounded-full px-2 py-0.5 text-[11px] font-medium ${NODE_LIBRARY_GROUP_STYLES[group]}`}>
|
||||
{NODE_LIBRARY_GROUP_LABELS[group]}
|
||||
</span>
|
||||
<span className="text-xs text-content-muted">{section.definitions.length}</span>
|
||||
<div
|
||||
key={familySection.family}
|
||||
className="rounded-xl border border-border-default bg-surface-hover/35 p-3"
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className={`rounded-full px-2 py-0.5 text-[11px] font-medium ${familyStyle}`}>
|
||||
{familyLabel}
|
||||
</span>
|
||||
<span className="rounded-full border border-border-default bg-surface px-2 py-0.5 text-[11px] text-content-muted">
|
||||
{familySection.modules.length} modules
|
||||
</span>
|
||||
<span className="rounded-full border border-border-default bg-surface px-2 py-0.5 text-[11px] text-content-muted">
|
||||
{familySection.definitions.length} nodes
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-content-muted">{familyDescription}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['legacy', 'bridge', 'graph'] as WorkflowNodeLibraryGroup[]).map(group => {
|
||||
const count = familySection.runtimeCounts[group]
|
||||
if (count === 0) return null
|
||||
return (
|
||||
<span
|
||||
key={`${familySection.family}-${group}`}
|
||||
className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${NODE_LIBRARY_GROUP_STYLES[group]}`}
|
||||
>
|
||||
{NODE_KIND_FILTER_LABELS[group]} {count}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-2 text-xs text-content-muted">{NODE_LIBRARY_GROUP_DESCRIPTIONS[group]}</p>
|
||||
<div className="space-y-2">
|
||||
{section.modules.map(moduleGroup => (
|
||||
|
||||
<div className="mt-3 space-y-3">
|
||||
{familySection.modules.map(moduleGroup => (
|
||||
<div
|
||||
key={`${group}:${moduleGroup.namespace}`}
|
||||
className="rounded-md border border-border-default bg-surface/70 px-2 py-2"
|
||||
key={`${familySection.family}:${moduleGroup.namespace}`}
|
||||
className="rounded-lg border border-border-default bg-surface/80 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 py-1 text-xs text-content-secondary">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
||||
<span className="rounded-full border border-border-default bg-surface px-1.5 py-0.5 font-medium">
|
||||
{moduleGroup.label}
|
||||
</span>
|
||||
<span className="truncate rounded-full border border-border-default bg-surface px-1.5 py-0.5 font-mono text-[10px]">
|
||||
{moduleGroup.namespace}
|
||||
</span>
|
||||
{moduleGroup.familyCounts.cad_file > 0 && (
|
||||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${FAMILY_FILTER_STYLES.cad_file}`}>
|
||||
{FAMILY_FILTER_LABELS.cad_file}
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
||||
<span className="rounded-full border border-border-default bg-surface px-2 py-0.5 text-xs font-medium text-content">
|
||||
{moduleGroup.label}
|
||||
</span>
|
||||
)}
|
||||
{moduleGroup.familyCounts.order_line > 0 && (
|
||||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${FAMILY_FILTER_STYLES.order_line}`}>
|
||||
{FAMILY_FILTER_LABELS.order_line}
|
||||
<span className="truncate rounded-full border border-border-default bg-surface px-2 py-0.5 font-mono text-[10px] text-content-muted">
|
||||
{moduleGroup.namespace}
|
||||
</span>
|
||||
)}
|
||||
{moduleGroup.stages.map(stage => (
|
||||
<span
|
||||
key={`${moduleGroup.namespace}-${stage}`}
|
||||
className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${AUTHORING_STAGE_STYLES[stage]}`}
|
||||
title={AUTHORING_STAGE_DESCRIPTIONS[stage]}
|
||||
>
|
||||
{AUTHORING_STAGE_LABELS[stage]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(['legacy', 'bridge', 'graph'] as WorkflowNodeLibraryGroup[]).map(group => {
|
||||
const count = moduleGroup.runtimeCounts[group]
|
||||
if (count === 0) return null
|
||||
return (
|
||||
<span
|
||||
key={`${moduleGroup.namespace}-${group}`}
|
||||
className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${NODE_LIBRARY_GROUP_STYLES[group]}`}
|
||||
>
|
||||
{NODE_KIND_FILTER_LABELS[group]} {count}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-content-muted">{moduleGroup.definitions.length}</span>
|
||||
<span className="text-xs text-content-muted">{moduleGroup.definitions.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 space-y-2">
|
||||
{moduleGroup.categories.map(categorySection => {
|
||||
const { category, definitions: categoryDefinitions } = categorySection
|
||||
<div className="mt-3 space-y-3">
|
||||
{moduleGroup.stageSections.map(stageSection => {
|
||||
const stageLabel = AUTHORING_STAGE_LABELS[stageSection.stage]
|
||||
const stageDescription = AUTHORING_STAGE_DESCRIPTIONS[stageSection.stage]
|
||||
const stageStyle = AUTHORING_STAGE_STYLES[stageSection.stage]
|
||||
|
||||
return (
|
||||
<div key={`${group}:${moduleGroup.namespace}:${category}`}>
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${CATEGORY_COLORS[category]}`}>
|
||||
{CATEGORY_LABELS[category]}
|
||||
<div
|
||||
key={`${familySection.family}:${moduleGroup.namespace}:${stageSection.stage}`}
|
||||
className="rounded-md border border-border-default bg-surface-hover/35 p-2"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${stageStyle}`}>
|
||||
{stageLabel}
|
||||
</span>
|
||||
<p className="text-xs text-content-muted">{stageDescription}</p>
|
||||
</div>
|
||||
<span className="text-[10px] text-content-muted">
|
||||
{stageSection.definitions.length}
|
||||
</span>
|
||||
<span className="text-[10px] text-content-muted">{categoryDefinitions.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{categoryDefinitions.map(definition => {
|
||||
const family = getDefinitionFamily(definition)
|
||||
const requiredInputs = readContractList(definition.input_contract, 'requires')
|
||||
const providedOutputs = readContractList(definition.output_contract, 'provides')
|
||||
const inputContext = readContractContext(definition.input_contract)
|
||||
const outputContext = readContractContext(definition.output_contract)
|
||||
const isActionable = Boolean(onSelectStep)
|
||||
|
||||
<div className="space-y-2">
|
||||
{stageSection.categories.map(categorySection => {
|
||||
const { category, definitions: categoryDefinitions } = categorySection
|
||||
return (
|
||||
<div
|
||||
key={definition.step}
|
||||
className={`rounded-lg border border-border-default bg-surface px-3 py-2 ${
|
||||
isActionable ? 'transition-colors hover:bg-surface-hover' : ''
|
||||
}`}
|
||||
title={definition.description}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
{renderIcon && (
|
||||
<span className="mt-0.5 text-content-secondary">
|
||||
{renderIcon(definition.icon, variant === 'menu' ? 14 : 13)}
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<p className="truncate text-sm font-medium text-content">{definition.label}</p>
|
||||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${FAMILY_FILTER_STYLES[family]}`}>
|
||||
{FAMILY_FILTER_LABELS[family]}
|
||||
</span>
|
||||
{getDefinitionBadges(definition).map(badge => (
|
||||
<span
|
||||
key={`${definition.step}-${badge.label}`}
|
||||
className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${badge.className}`}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-0.5 truncate font-mono text-[11px] text-content-muted">{definition.step}</p>
|
||||
</div>
|
||||
<div key={`${moduleGroup.namespace}:${stageSection.stage}:${category}`}>
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${CATEGORY_COLORS[category]}`}>
|
||||
{CATEGORY_LABELS[category]}
|
||||
</span>
|
||||
<span className="text-[10px] text-content-muted">{categoryDefinitions.length}</span>
|
||||
</div>
|
||||
|
||||
{isActionable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectStep?.(definition.step)}
|
||||
className={`shrink-0 rounded-lg px-2 py-1 text-xs font-medium ${
|
||||
variant === 'panel'
|
||||
? 'border border-border-default text-content hover:bg-surface-hover'
|
||||
: 'bg-accent text-white hover:bg-accent-hover'
|
||||
}`}
|
||||
>
|
||||
{variant === 'panel' ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Plus size={12} />
|
||||
Insert
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
Use
|
||||
<ArrowRight size={12} />
|
||||
<div className="space-y-1">
|
||||
{categoryDefinitions.map(definition => {
|
||||
const family = getDefinitionFamily(definition)
|
||||
const requiredInputs = readContractList(definition.input_contract, 'requires')
|
||||
const providedOutputs = readContractList(definition.output_contract, 'provides')
|
||||
const inputContext = readContractContext(definition.input_contract)
|
||||
const outputContext = readContractContext(definition.output_contract)
|
||||
const isActionable = Boolean(onSelectStep)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={definition.step}
|
||||
className={`rounded-lg border border-border-default bg-surface px-3 py-2 ${
|
||||
isActionable ? 'transition-colors hover:bg-surface-hover' : ''
|
||||
}`}
|
||||
title={definition.description}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
{renderIcon && (
|
||||
<span className="mt-0.5 text-content-secondary">
|
||||
{renderIcon(definition.icon, variant === 'menu' ? 14 : 13)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<p className="truncate text-sm font-medium text-content">
|
||||
{definition.label}
|
||||
</p>
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${FAMILY_FILTER_STYLES[family]}`}
|
||||
>
|
||||
{FAMILY_FILTER_LABELS[family]}
|
||||
</span>
|
||||
{getDefinitionBadges(definition).map(badge => (
|
||||
<span
|
||||
key={`${definition.step}-${badge.label}`}
|
||||
className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${badge.className}`}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-0.5 truncate font-mono text-[11px] text-content-muted">
|
||||
{definition.step}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="mt-1 line-clamp-2 text-xs text-content-muted">{definition.description}</p>
|
||||
{isActionable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectStep?.(definition.step)}
|
||||
aria-label={
|
||||
variant === 'panel'
|
||||
? `Insert ${definition.label}`
|
||||
: `Use ${definition.label}`
|
||||
}
|
||||
className={`shrink-0 rounded-lg px-2 py-1 text-xs font-medium ${
|
||||
variant === 'panel'
|
||||
? 'border border-border-default text-content hover:bg-surface-hover'
|
||||
: 'bg-accent text-white hover:bg-accent-hover'
|
||||
}`}
|
||||
>
|
||||
{variant === 'panel' ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Plus size={12} />
|
||||
Insert
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
Use
|
||||
<ArrowRight size={12} />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex flex-wrap gap-1.5 text-[10px]">
|
||||
{inputContext && (
|
||||
<span className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary">
|
||||
In {formatContractLabel(inputContext)}
|
||||
</span>
|
||||
)}
|
||||
{outputContext && (
|
||||
<span className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary">
|
||||
Out {formatContractLabel(outputContext)}
|
||||
</span>
|
||||
)}
|
||||
{requiredInputs.slice(0, 2).map(input => (
|
||||
<span
|
||||
key={`${definition.step}-requires-${input}`}
|
||||
className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary"
|
||||
>
|
||||
Requires {formatContractLabel(input)}
|
||||
</span>
|
||||
))}
|
||||
{providedOutputs.slice(0, 2).map(output => (
|
||||
<span
|
||||
key={`${definition.step}-provides-${output}`}
|
||||
className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary"
|
||||
>
|
||||
Provides {formatContractLabel(output)}
|
||||
</span>
|
||||
))}
|
||||
{definition.artifact_roles_consumed.slice(0, 1).map(artifact => (
|
||||
<span
|
||||
key={`${definition.step}-consumes-${artifact}`}
|
||||
className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary"
|
||||
>
|
||||
Consumes {formatContractLabel(artifact)}
|
||||
</span>
|
||||
))}
|
||||
{definition.artifact_roles_produced.slice(0, 1).map(artifact => (
|
||||
<span
|
||||
key={`${definition.step}-produces-${artifact}`}
|
||||
className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary"
|
||||
>
|
||||
Produces {formatContractLabel(artifact)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-xs text-content-muted">
|
||||
{definition.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-2 flex flex-wrap gap-1.5 text-[10px]">
|
||||
{inputContext && (
|
||||
<span className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary">
|
||||
In {formatContractLabel(inputContext)}
|
||||
</span>
|
||||
)}
|
||||
{outputContext && (
|
||||
<span className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary">
|
||||
Out {formatContractLabel(outputContext)}
|
||||
</span>
|
||||
)}
|
||||
{requiredInputs.slice(0, 2).map(input => (
|
||||
<span
|
||||
key={`${definition.step}-requires-${input}`}
|
||||
className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary"
|
||||
>
|
||||
Requires {formatContractLabel(input)}
|
||||
</span>
|
||||
))}
|
||||
{providedOutputs.slice(0, 2).map(output => (
|
||||
<span
|
||||
key={`${definition.step}-provides-${output}`}
|
||||
className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary"
|
||||
>
|
||||
Provides {formatContractLabel(output)}
|
||||
</span>
|
||||
))}
|
||||
{definition.artifact_roles_consumed.slice(0, 1).map(artifact => (
|
||||
<span
|
||||
key={`${definition.step}-consumes-${artifact}`}
|
||||
className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary"
|
||||
>
|
||||
Consumes {formatContractLabel(artifact)}
|
||||
</span>
|
||||
))}
|
||||
{definition.artifact_roles_produced.slice(0, 1).map(artifact => (
|
||||
<span
|
||||
key={`${definition.step}-produces-${artifact}`}
|
||||
className="rounded-full border border-border-default bg-surface-hover/80 px-1.5 py-0.5 text-content-secondary"
|
||||
>
|
||||
Produces {formatContractLabel(artifact)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user