# [Slot] and contributions — a place on a page that other kits, and the app, add to

> A slot is a place on a page others add to. A kit marks one (`[Slot] public abstract component QuotePanel(Quote quote);`), and every component that writes `: QuotePanel` and takes exactly its parameters — in any kit, or the app — is drawn where a page draws `QuotePanel(quote)`, in one order, each only for a reader its `[Authorize]` admits. The app has the last word: `without` leaves one out, `[Replaces]` swaps one, and a component of the same name takes one's place.

<!-- id: ui-contributions · area: ui · stability: preview · html: https://osysharp.com/reference/ui/contributions/ -->

## Summary        {#summary}
A **slot** is a place on a page that other kits — and the app — add to. The kit that owns the page declares it once,
draws it where it belongs, and never learns who fills it:

```osy syntax
[Slot] public abstract component QuotePanel(Quote quote);       // the kit: "others may add to a quote's page here"

[Contribution("Approvals", Icon = Icons.Check, Order = 20)] [Authorize(ApprovesQuotes)]
component QuoteApprovals(Quote quote) : QuotePanel { render { … } }   // any kit, or the app

QuotePanel(quote);                                               // the page: every contribution, in order
```

It is a lowering, not a lookup: the compiler writes the slot out as its contributions, so a page that draws a slot costs
what the same components written by hand cost — no request of its own, nothing looked up when the page runs.

## Signature      {#signature}
```osy syntax
[Slot] public abstract component X(<params>);                    // the slot — no body; its parameters are the contract
[Slot] public abstract component RecordPanel<T>(T record) where T : entity;   // a slot for ANY record (Osysharp.Ui ships two)

[Contribution("Words", Icon = Icons.Y, Order = 20)]              // all three optional: words default to its name in words,
component C(<exactly X's params>) : X { render { … } }           //   Order to 100
[Order(20)]                                                      // the same place, said on its own
component Files<T>(T record) : RecordPanel<T> { … }              // for every record type
component QuoteOnly(Quote record) : RecordPanel<Quote> { … }     // for one

X(args);                                                         // draw every contribution, in order
X(args, only: c);                                                // draw one — `c` a SlotContribution
Contributions<X>()                       → List<SlotContribution>   // Key, Label, Icon, Order — what this reader is shown

use Acme.Kit@0 { without C; }                                    // app.osy: leave a package's contribution out
[Replaces(C)] component Mine(<X's params>) : X { … }             // the app's own, in C's place
```

## Description    {#description}

### In what order are they drawn?   {#order}
One order for the whole composition: `Order` (or `[Order]`) first, lower first, 100 when none is said; then the
**package** order — a kit before the kits built on it, the app last; then the full name. It never depends on file
order or on which kit compiled first, so two compiles of one composition draw the same page. Kits leave gaps (10, 20,
…) so an app can put its own between them.

### Who sees a contribution?   {#who}
A contribution marked `[Authorize(P)]` is drawn only for a reader `P` admits, and `Contributions<X>()` lists it only for
them; with no `[Authorize]` it is drawn for everyone who sees the page. Everything a contribution READS is read as its
reader, like any component's — so one contribution on a shared page shows each reader their own rows.

### How does a page lay them out as tabs?   {#tabs}
Draw the slot with `only:` and the list beside it. `Contributions<X>()` is the reader's list, in order — each with the
`Key` that `only:` matches, its `Label` and its `Icon`.

The list is written out where you call it — no request — so a `[Render(SSR)]` page that lists contributions is painted
by the server with the list already in it, as the browser then draws it. (A contribution with an `[Authorize]` makes the
list depend on the reader; such a page is painted in the browser instead, which already knows who is reading.)

### What can the app change?   {#app}
- **Leave one out** — `use Acme.Kit@0 { without QuoteHistory; }` in `app.osy`. A `without` that names nothing is refused
  with the package's whole list. A class closed for a subtype is named with it: `without WorkSource<PurchaseRequest>;`.
- **Swap one** — `[Replaces(QuoteHistory)]` on the app's own contribution to the same slot: drawn in its place, with its
  words unless the app's say others. Only the app replaces — a kit cannot change what another kit adds.
- **Take one over by name** — an app component with a contribution's NAME is drawn instead of the kit's, everywhere.
- **Restyle them** — contributions paint with theme tokens, so the app's theme repaints them ([theme tokens](https://osysharp.com/reference/ui/theming/)).

### What if a replacement drops a slot?   {#kept}
A component that takes another's place — an app component of a kit component's name, a `[Replaces]`, or the app's design
of a kit's look contract — keeps the slots the one it replaces drew. Dropping a slot that has contributions is refused,
naming each contribution that would have nowhere to land:

> `'MyFrame' takes the place of the package's design Acme.Kit.KitBareFrame, which draws the slot Greeting — and
> 'MyFrame' does not, so its 2 contributions would have nowhere to land: `KitA` (Acme.Kit) and `AppE` (this app).
> Draw `Greeting(…)` in 'MyFrame', or leave them out with `without` on the package's `use` line.`

### What does every record's page offer?   {#record}
`Osysharp.Ui` declares `RecordPanel<T>` and `RecordBadge<T>`. A kit that works on any record (its files, its activity)
contributes once with an open contribution; one for a single type writes the type. A page draws `RecordPanel(record)`.

### What is refused?   {#errors}
A contribution that does not take exactly its slot's parameters (names, types, order) — named with the shape it must
take; `[Contribution]` on a component whose base is not a slot — listing the slots in sight; a slot with a body; a
`[Replaces]` in a package, or naming nothing; `Contributions<X>()` for something that is not a slot.

## Examples       {#examples}

```osy title="a slot, three contributions — one only for approvers — and a page drawing them as tabs" test app=ui-contributions
[Principal] entity User { [Required, MaxLength(200)] string Email; security { allow read when IsAuthenticated; } }
[Role] enum AppRole { Member, Approver }
[RoleGrant] entity RoleGrant { [Required] User Grantee; [Required] AppRole Level; security { allow read when IsAuthenticated; } }
policy Approves => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Approver);

entity Quote {
  [Required, MaxLength(100)] string Title;
  security { allow read, create when IsAuthenticated; }
}

/// A section on a quote's page — others add to it.
[Slot] abstract component QuotePanel(Quote quote);

[Contribution("Lines", Order = 10)]
component QuoteLines(Quote quote) : QuotePanel { render { Text("Lines of " + quote.Title); } }

[Contribution("Approvals", Icon = Icons.Check, Order = 20)] [Authorize(Approves)]
component QuoteApprovals(Quote quote) : QuotePanel { render { Text("Approve " + quote.Title); } }

[Contribution("History")]
component QuoteHistory(Quote quote) : QuotePanel { render { Text("History of " + quote.Title); } }

[Route("/q/{id}")]
[Render(CSR)]
component QuotePage(Guid id) {
  live var q = Quote.FirstOrDefault(x => x.Id == id);
  SlotContribution? shown = null;
  action Show(SlotContribution? c) { shown = c; }
  render {
    if (q != null) {
      Stack(gap: 3) {
        Tabs {
          Tab("All", selected: shown == null, onPress: () => Show(null));
          foreach (var c in Contributions<QuotePanel>()) {
            Tab(c.Label, selected: shown != null && shown.Key == c.Key, onPress: () => Show(c));
          }
        }
        QuotePanel(q, only: shown);
      }
    }
  }
}
```

```osy title="a panel on every record's page, and one only on a quote's" test app=ui-contributions
[Contribution("Files", Order = 50)]
component AnyRecordFiles<T>(T record) : RecordPanel<T> { render { Text("Files"); } }

[Contribution("Pricing")]
component QuotePricing(Quote record) : RecordPanel<Quote> { render { Text("Pricing for " + record.Title); } }

[Route("/record/{id}")]
[Render(CSR)]
component QuoteRecordPage(Guid id) {
  live var q = Quote.FirstOrDefault(x => x.Id == id);
  render { if (q != null) { RecordPanel(q); } }
}
```

```osy title="the app swaps one contribution for its own" test app=ui-contributions
[Replaces(QuoteHistory)]
component QuoteTrail(Quote quote) : QuotePanel { render { Text("Every change to " + quote.Title); } }
```

## See also       {#see-also}
- [WorkspaceShell, WorkspaceFrame and [Nav] — a kit's pages in your app's shell and navigation](https://osysharp.com/reference/ui/workspace-frame/) — a kit's pages in the app's own shell, and `[Nav]`
- [Looks — a site's whole design, picked per version](https://osysharp.com/reference/ui/looks/) — one design of a contract, where a slot draws every contribution
- [App.Implementations<T>()](https://osysharp.com/reference/class/app-implementations/) — the same order, `without` and `[Replaces]` for classes a kit discovers
- [[Action] — what an app offers to be done, by name](https://osysharp.com/reference/function/action/) — `Actions.For(record)`, every action on a record the reader may run
