# canPress / canEdit / canSee

> `canPress`/`canEdit`/`canSee: <policy>` ties a control to a declared `policy`: `canPress` disables a button, `canEdit` makes a value control read-only, and `canSee` hides an element entirely — each unless the signed-in caller satisfies the policy. You declare the rule ONCE — on the entity or as a `policy` the server enforces — and the control simply REFLECTS it, live and reactively. It is a UX affordance, never the gate: the server still enforces the policy on every action, so a control that reflects the wrong answer can still never let a caller do something they may not.

<!-- id: ui-policy-controls · area: ui · stability: stable · html: https://osysharp.com/reference/ui/policy-controls/ -->

## Summary        {#summary}
`canPress: <policy>` ties a control's **pressable state** to a declared **`policy`**. The button is enabled when the
signed-in caller satisfies the policy and disabled when they don't — computed on the client, reactively, so it
reflects the current caller with no extra code.

```osy title="canPress mirrors the entity rule on the button" syntax
// Creating an organization is admins-only — declared once on the entity:
//   entity Organization { ... security { allow create when IsPlatformAdmin; } }
// The button that starts that create REFLECTS the same rule, so a non-admin sees it disabled:
Pressable(onClick: NewOrg, canPress: IsPlatformAdmin) {
  BtnPrimary("New organization") { Icon(Icons.Plus, size: 18); }
}
```

This is a **reflection, not a gate**. The server enforces the policy on the action itself; `canPress` only spares the
caller a click that would be refused. Because the rule lives in one place — the `policy` (see [page authorization (policies)](https://osysharp.com/reference/ui/authorize/)) — the
button can never drift out of sync with what the server actually allows.

A value control uses **`canEdit`** the same way — the field stays visible but becomes read-only unless the caller
satisfies the policy:

```osy title="the three policy props, on a real page" test app=ui-policy-controls
[Principal] entity User { [Required] string Email; }
[Role] enum AppRole { PlatformAdmin, Member }

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role;
  security { allow read when IsAuthenticated; }
}

policy IsPlatformAdmin => RoleGrant.Any(r => r.User == user && r.Role == AppRole.PlatformAdmin);

entity Application {
  [Required] string Slug;
  [Required] string Name;
  security { allow create, read, update when IsAuthenticated; }
}

[Page("/settings/{id}")] [Render(CSR)]
component OrgSettings(Guid id) {
  var org = Application.Single(a => a.Id == id);
  action Seed() { }
  action NewOrg() { }
  render {
    // The field shows the value to everyone, editable to admins:
    Input(value: org.Slug, canEdit: IsPlatformAdmin);

    // An admin-only affordance — a member never sees it (nothing is rendered, not just hidden):
    Stack(canSee: IsPlatformAdmin) {
      Button("Seed sample data", onPress: Seed);
    }

    // Disabled rather than hidden, and it says WHY.
    Pressable(onClick: NewOrg,
              canPress: IsPlatformAdmin,
              whenDenied: "Only platform admins can create organizations.") {
      Text("New organization");
    }
  }
}
```

To hide an element (and its whole subtree) unless the policy holds, use **`canSee`** on any element — a container is the
usual choice, so a whole section appears only for callers who satisfy the policy:

```osy title="canSee removes the subtree instead of disabling it" syntax
// An admin-only affordance — a member never sees it (nothing is rendered, not just hidden):
Stack(canSee: IsPlatformAdmin) {
  Button("Seed sample data", onPress: Seed);
}
```

## Signature      {#signature}
```osy syntax
<PressableControl>(canPress: <PolicyName>)   // → disabled unless the policy holds
<ValueControl>(canEdit: <PolicyName>)        // → read-only unless the policy holds
<AnyElement>(canSee: <PolicyName>)           // → not rendered at all unless the policy holds
```
- **`<PolicyName>`** — a declared `policy`, the same vocabulary [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) uses. A name that isn't a declared
  policy is a compile error (with a did-you-mean).
- **`canPress`** applies to **pressable** controls (a `Pressable` / button); **`canEdit`** applies to **value**
  controls (an `Input`) — used on a control that doesn't take the matching state, it is a compile error. **`canSee`**
  applies to **any element**; when the policy is false the element and its children render nothing at all.

## Description    {#description}
A `policy` names a condition about the caller — for example "is a platform admin", or "is an owner or admin of an
organization". You already write policies to gate pages ([page authorization (policies)](https://osysharp.com/reference/ui/authorize/)) and entity access. `canPress` lets the same
policy drive a control's enabled state, so the UI and the enforced rule are one declaration.

The control is **disabled until the policy is known to be true**. This is deliberate and fail-safe: while the data a
policy depends on is still loading, the control stays locked, then unlocks the moment the policy resolves true — it
never flashes enabled first. If the policy is false, the control stays disabled.

A policy usable on a control must be **self-scoped** — a check about the caller's own membership or roles, such as:

```osy syntax
policy IsPlatformAdmin => PlatformRoleGrant.Any(g => g.User == user && g.Role == PlatformRole.Admin);
policy IsOrgAdmin      => OrganizationMember.Any(m => m.User == user && m.Role != OrgRole.Member);
```

A policy that compares to data the control doesn't hold (a specific page's row, or an unrelated entity) can't be
reflected on a control today and is a compile error — keeping the clean syntax honest about what it can and can't show.

Only the caller's **own** rows are ever loaded to evaluate the reflection; nothing about other users is read to decide
whether your button is enabled.

## Saying WHY a control is locked — `whenDenied`   {#denial-hint}
Pair a policy prop with **`whenDenied`** to explain *why* a control is unavailable — the sentence shows as a tooltip
while the control is locked (disabled or read-only), and is silent once the caller satisfies the policy:

```osy syntax
Pressable(onClick: NewOrg,
          canPress: IsPlatformAdmin,
          whenDenied: "Only platform admins can create organizations.") {
  BtnPrimary("New organization") { Icon(Icons.Plus, size: 18); }
}
```

The message lives at the **control**, not the policy — the same policy can deny different actions for different
reasons. It renders to the native `title` attribute only while the control is locked, so a normal reader hovers to see
why an action is off, and assistive technology reads it either way.

## Policy as a value {#policy-value}
Sometimes you don't want to reflect a policy onto one control — you want to **branch the whole layout** on it, or reuse
the answer in several places. A policy can be used directly **as a `bool` value**: in an `if`, or held in a `live var`.

```osy title="branching the whole layout on a policy" test app=ui-policy-controls
policy CanManageApp(Application a) => RoleGrant.Any(r => r.User == user && r.Role == AppRole.PlatformAdmin);

[Composable] component AppRosterEditor(Application app) { render { Text(app.Name); } }
[Composable] component AppRosterReadonly(Application app) { render { Text(app.Name); } }

// A page that lays out differently for someone who can manage the app vs. a plain viewer:
component AppDetail(string slug) {
  var app = Application.Single(a => a.Slug == slug);
  render {
    if (CanManageApp(app)) {
      // the manager's view — an editable roster, an invite button …
      AppRosterEditor(app: app);
    } else {
      // the read-only view
      AppRosterReadonly(app: app);
    }
  }
}
```

Hold the answer once and reuse it with a **`live var`** — it tracks reactively, exactly like a control prop:

```osy title="holding the answer in a live var" test app=ui-policy-controls
[Composable] component NavLinks() { render { Text("nav"); } }

[Page("/toolbar")] [Render(CSR)]
component Toolbar() {
  live var canManage = IsPlatformAdmin;   // a bool, reactive to the caller's own rows
  action OpenSettings() { }
  render {
    if (canManage) { Button("Settings", onPress: OpenSettings); }
    NavLinks();
  }
}
```

Both forms lower to the **same** self-list reflection a `canPress:` prop uses — client-computed, reactive, and reading
only the caller's own rows. And they carry the same guarantee: this is **render-only**. Branching the layout hides an
affordance a caller can't use; it does not protect anything. The server still enforces the policy on every read and
every action, so a page that showed the manager's view to the wrong caller would still refuse every mutation behind it.

The rules match a control prop: the value must be a **declared, self-scoped** `policy` (a typo is an unknown-identifier
error, not a silently-false branch), and a parameterized policy is applied to a value the page already holds
(`CanManageApp(app)`). A policy is **not** callable in a server position — you can't put one in an entity query's
`where`; it is the server's own enforcement, never a query term.

## See also   {#see-also}
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — gate a whole page or route on a policy (the enforced side).
- [component](https://osysharp.com/reference/ui/component/) — where controls and their handlers live.
- [style props](https://osysharp.com/reference/ui/styling/) — the visual vocabulary a control's variants assign.
