import { createElement, Fragment } from 'react' import { Layer, Schema as S } from 'effect' import { Composition, Engine, Event, Reducer, State, Ui, each, mount, one, provide, scene, slot, type CapturedScene, type DerivedContract, type SlotService, ui, } from '@playfast/reform' import { type CompositionChildSummary, type EngineServices, type WiredUi } from '@playfast/reform/internal' const ListItem: S.Struct<{ id: typeof S.String; label: typeof S.String }> = S.Struct({ id: S.String, label: S.String, }) const ItemsBase: State.StateDefinition< 'items', ReadonlyArray<{ readonly id: string; readonly label: string }>, S.Array$ > = State.make('items', S.Array(ListItem)) class Items extends ItemsBase {} class Added extends Event.make('Added', ListItem) {} class Removed extends Event.make('Removed', S.Struct({ id: S.String })) {} class AddItem extends Reducer.make('AddItem', { states: [Items], events: [Added], }) {} class RemoveItem extends Reducer.make('RemoveItem', { states: [Items], events: [Removed], }) {} const BarUiBase: WiredUi< DerivedContract<{ props: S.Struct<{}> events: { add: typeof ListItem } }>, 'Bar' > = ui('Bar', { props: S.Struct({}), events: { add: ListItem }, }) class BarUi extends BarUiBase {} class Bar extends Composition.make('Bar', { title: 'Bar', ui: BarUi })() {} const ItemUiBase: WiredUi< DerivedContract<{ props: S.Struct<{ label: typeof S.String }> events: { remove: S.Struct<{}> } }>, 'Item' > = ui('Item', { props: S.Struct({ label: S.String }), events: { remove: S.Struct({}) }, }) class ItemUi extends ItemUiBase {} class ItemComp extends Composition.make('Item', { title: 'Item', ui: ItemUi, props: ListItem, })() {} class BarSlot extends slot('Bar')() {} class ItemSlot extends slot('Item')() {} const ListUiBase: Ui.UiClass< { props: { items: ReadonlyArray<{ id: string; label: string }> } slots: { Bar: BarSlot; Item: ItemSlot } }, 'List' > = ui('List')<{ props: { items: ReadonlyArray<{ id: string; label: string }> } slots: { Bar: BarSlot; Item: ItemSlot } }>() class ListUi extends ListUiBase {} class ListComp extends Composition.make('List', { title: 'List', slots: { Bar: BarSlot, Item: ItemSlot }, ui: ListUi, states: [Items], })() {} type ListScene = CapturedScene< typeof ListUi.Contract, readonly [typeof Items], | EngineServices | Ui.UiService<'Item'> | State.StateStore<'items', ReadonlyArray<{ readonly id: string; readonly label: string }>> | Ui.UiService<'Bar'> | Ui.UiService<'List'> | Composition.CompositionId<'List', ListComp, BarSlot | ItemSlot> | Composition.CompositionId<'Bar', Bar, never> | Composition.CompositionId<'Item', ItemComp, never> | SlotService<'Bar', BarSlot, CompositionChildSummary> | SlotService<'Item', ItemSlot, CompositionChildSummary>, unknown, 'List', ListComp > const makeListScene = ( initial: ReadonlyArray<{ id: string; label: string }> = [ { id: 'a', label: 'A' }, { id: 'b', label: 'B' }, ], ): ListScene => { const presentation = Layer.mergeAll( provide( ListUi, Ui.make(ListUi, (_props, slots) => createElement(Fragment, null, createElement(slots.Bar, {}), createElement(slots.Item, {})), ), ), provide( BarUi, Ui.make(BarUi, () => null), ), provide( ItemUi, Ui.make(ItemUi, ({ label }) => label), ), provide(BarSlot, Bar), provide(ItemSlot, ItemComp), State.live(Items, initial), ) const app = Layer.mergeAll( Composition.live(ListComp, function* () { const listItems = yield* Items return mount({ props: { items: listItems }, slots: { Bar: one({}), Item: each(listItems, { key: (entry) => entry.id, props: (entry) => ({ id: entry.id, label: entry.label }), }), }, }) }), Composition.live(Bar, function* () { const add = yield* Event.trigger(Added) return mount({ props: {}, slots: {}, events: { add } }) }), Composition.live(ItemComp, function* () { const itemProps = yield* ItemComp.props const removeTrigger = yield* Event.trigger(Removed) // The item binds its id so the wire event carries no payload. const remove = (): void => removeTrigger({ id: itemProps.id }) return mount({ props: { label: itemProps.label }, slots: {}, events: { remove }, }) }), Reducer.live(AddItem, (current, event) => [...current, { id: event.id, label: event.label }]), Reducer.live(RemoveItem, (current, event) => current.filter((entry) => entry.id !== event.id)), ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine)) return scene(ListComp, { provide: [app] }) } export const listScene: typeof makeListScene = makeListScene