Deal items panel
Reference layout for the right-panel deal-items app. The layout adapts to deal type (service / subscription / support) and fills whatever width the host gives it — narrow right panel OR full-width expanded view. Composed entirely from recipes in @8maverik8/twenty-design + Twenty primitives via the /twenty-ui bridge.
Three deal types, one layout
The shape of the form is driven by opportunity.type. Use the demo's dashed Demo · deal type switcher to see each shape — in production this value is read-only on this surface (changed on the deal record itself).
Live demo
Fully interactive — switch deal type at the top, change deal-level Months / Start / End, add catalog or custom lines, change pricing / quantity, set a discount. Totals recompute live. The panel fills the container width — resize the docs page to verify both the narrow-panel and full-width compositions.
What composes it
Each visible piece maps 1:1 to a documented recipe — there's no bespoke styling buried in this demo file. The two trivial inline helpers (LabelledControl and LabelledValue) are kept local because they're 5-line wrappers and aren't reused elsewhere yet; promote them to recipes when a second consumer appears.
Imports for the real front-component
The demo above uses our root @8maverik8/twenty-designimports (Normal DOM). When the agent translates this layout into a real front-component, the bridge subpath provides Button (and the rest of the overlapping primitives) in a Remote-DOM-compatible form. Recipes don't change their import — they work in both.
For the agent rebuilding the real app
Translation checklist when porting this layout to oapps-deal-items/src/components/deal-items-table.front-component.tsx:
- Deal-type drives the shape. Read
opportunity.typeat the top. Forservice: skip the deal-level Months + Start/End row, drop the per-line Pricing toggle, compute line totals asunit × quantity. Forsubscription/support: render the duration row at top, expose Pricing on catalog lines, line totals =unit × quantity × months. - No per-line
months. Months lives on the deal (one source of truth); changing it recomputes every line in one place. Same forstartDate/endDate— they hang off the Opportunity, not the line. - Responsive — fill the host width. Do not hard-code a max-width on the panel root. Twenty's right panel can be narrow or expanded full-screen; the layout flexes via
display: flex; flex-wrap: wrapon the line body and the meta row. - Replace the local sample
PRODUCTS+itemsstate with a GraphQL query +updateDealItem/deleteDealItem/createDealItemmutations. Deal-level Months + dates areupdateOpportunity. - Keep all numeric handling in micros; only convert at render time inside
fmtMoney. Float arithmetic on the way in will drift line totals. - Currency comes from
opportunity.amount.currencyCode; pass it down to each line. - Don't trigger
setItemson every keystroke for the custom-line name — it will unmount the native<input>and lose focus. Use fire-and-forget mutation, local controlled state, and reconcile on tab-switch refetch. - Wrap the whole component in
<ThemeProvider colorScheme="light">from@8maverik8/twenty-design/twenty-uiat the top — Twenty's tokens won't apply otherwise. - Hover/active states: do not add
data-stateCSS rules. The recipes already useuseState-based hover viaonMouseEnter/onMouseLeave. Inheriting that pattern keeps you Remote-DOM-clean.