layout · in-panel app

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).

ServiceOne-off engagement. No duration.Hides the deal-level Months + Start/End row entirely. Inside each line: no Pricing toggle (annual/monthly is meaningless for one-offs), no per-line multiplier. Line total = unit × quantity.
SubscriptionRecurring product subscription.Deal-level Months + Start/End row appears at the top. Catalog lines expose the Pricing toggle (Annual / Monthly) since unit price differs. Line total = unit × quantity × deal months.
SupportRecurring support agreement.Same shape as Subscription — Months + Start/End + per-line Pricing toggle. Logically distinct on the deal record (different revenue stream) but identical UI on this surface.

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.

Demo · deal type
Service
Subscription
Support
Months
Start
End
Subscriptions & Services
Unit Price
Quantity
Line Total€3,768.00
×
Creomate · Bundle (WebAPI + Webhook) · 32 SC
Pricing
Annual
Monthly
Quantity
Unit Price€91.50/mo
Line Total€1,098.00
×
OAPPS 3CX for Zendesk · Professional
Pricing
Annual
Monthly
Quantity
Unit Price€99.00/mo
Line Total€1,188.00
×
Subtotal
€6,054.00
Discount
%
Total
€6,054.00

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.

SegmentedControlTop: deal-type switch (Service / Subscription / Support) drives the rest of the layout. Inside catalog lines: Annual / Monthly pricing toggle (only when duration applies).
NumberInputDeal-level Months (subscription/support only) + per-line Quantity.
InputShellBordered "input box" chrome — wraps the product chip OR the custom-name input OR the deal-level date inputs.
PanelHeaderTitle row "Subscriptions & Services" + two add-buttons.
LineItemCardOne row per deal item. Header / body / footer slots; outside delete button at matching height.
EntityChipInside the catalog-line header InputShell — shows the selected product.
RecordPickerInline below the chip when the user clicks it. Search + alphabet + list.
MoneyInputUnit Price input for custom lines — currency prefix + micros-precision.
TotalsBlockFooter: subtotal, discount controls (—/%/€), total.
DiscountControllerComposed inside TotalsBlock — mode toggle + preset chips + manual input.

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.

tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
// Overlapping primitives — Twenty's own, via the bridge:
import { Button, ThemeProvider } from '@8maverik8/twenty-design/twenty-ui';
// In-panel recipes — our own, work in Remote DOM out of the box:
import {
PanelHeader,
LineItemCard,
TotalsBlock,
SegmentedControl,
EntityChip,
RecordPicker,
InputShell,
MoneyInput,
NumberInput,
} from '@8maverik8/twenty-design';
import { IconPlus } from '@8maverik8/twenty-icons';
type DealType = 'service' | 'subscription' | 'support';
const needsDuration = (t: DealType) => t !== 'service';

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.type at the top. For service: skip the deal-level Months + Start/End row, drop the per-line Pricing toggle, compute line totals as unit × quantity. For subscription / 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 for startDate / 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 viadisplay: flex; flex-wrap: wrap on the line body and the meta row.
  • Replace the local sample PRODUCTS + items state with a GraphQL query + updateDealItem / deleteDealItem / createDealItem mutations. Deal-level Months + dates are updateOpportunity.
  • 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 setItems on 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-ui at the top — Twenty's tokens won't apply otherwise.
  • Hover/active states: do not add data-state CSS rules. The recipes already use useState-based hover via onMouseEnter/onMouseLeave. Inheriting that pattern keeps you Remote-DOM-clean.