# `PhoenixKitProjects.Web.Widgets.Helpers`
[🔗](https://github.com/BeamLabEU/phoenix_kit_projects/blob/v0.21.2/lib/phoenix_kit_projects/web/widgets/helpers.ex#L1)

Shared helpers + frame for the dashboard widgets `phoenix_kit_projects`
contributes to `phoenix_kit_dashboards` via `phoenix_kit_widgets/0`.

Each widget is a `Phoenix.LiveComponent` the dashboards host renders with
`settings` / `view` / `size` / `scope` assigns. These helpers centralize the
enablement guard, the lenient project resolver (widgets pick a project by a
free-text `"project"` setting, since a dashboard widget's settings schema is
static), the shared card frame, and small formatters.

# `available?`

```elixir
@spec available?() :: boolean()
```

True when the projects module is loaded and enabled.

# `date`

```elixir
@spec date(DateTime.t() | nil) :: String.t()
```

Format a datetime as `YYYY-MM-DD`, or `—`.

# `effective_view`

```elixir
@spec effective_view(String.t() | nil, [String.t()]) :: String.t()
```

Pick the effective view: honor the selected `view` if it's one of `valid`,
else the first valid view. The user's choice is NEVER overridden by size —
content self-fits via container-query type scaling instead.

# `empty`

A centered, iconed empty-state body (widgets must never look broken-empty).
## Attributes

* `icon` (`:string`) - Defaults to `"hero-clipboard-document-list"`.
* `message` (`:string`) (required)

# `fit_text`

```elixir
@spec fit_text(number(), String.t(), number()) :: String.t()
```

A scale-aware self-fit font-size style: the type grows with its cq slot but
stays clamped to a consistent px range, so widgets look cohesive at any box
size (no comically large list rows, no unreadable KPI labels). The
`--pk-scale` var (set by the dashboards fit hook) keeps the clamp
proportional when the board renders scaled down.

# `frame`

A shared widget card frame: header (icon + title + optional link) + body slot.

## Attributes

* `title` (`:string`) (required)
* `icon` (`:string`) - Defaults to `"hero-clipboard-document-list"`.
* `href` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block` (required)
* `actions`

# `hours`

```elixir
@spec hours(number() | nil) :: String.t()
```

Format estimated hours compactly (e.g. `12h`, `1.5h`, `—`).

# `resolve_project`

```elixir
@spec resolve_project(term(), term()) :: PhoenixKitProjects.Schemas.Project.t() | nil
```

Resolve the `"project"` widget setting (a uuid, exact name, external id, or a
name substring) to a `%Project{}`. Falls back to the first running project (or
any project) when the setting is blank, so a freshly-added widget shows data.

# `safe_list_projects`

```elixir
@spec safe_list_projects(term()) :: [PhoenixKitProjects.Schemas.Project.t()]
```

`Projects.list_projects/0` with the widget no-crash guard: a transient DB
error (connection loss, mid-migration missing table) degrades to an empty
list — a widget must never crash the host dashboard.

# `safe_project_summary`

```elixir
@spec safe_project_summary(PhoenixKitProjects.Schemas.Project.t()) :: map() | nil
```

`Projects.project_summary/1` with the widget no-crash guard — `nil` on a DB
error, which every consuming widget already renders as its empty state.

# `scope_user_uuid`

```elixir
@spec scope_user_uuid(term()) :: String.t() | nil
```

The current user's uuid out of the host-provided scope assign, or nil.

# `unavailable`

The 'projects module is off' placeholder body.

# `viewer_for`

```elixir
@spec viewer_for(map() | nil) :: String.t() | nil
```

The `viewer:` a widget's queries should narrow by.

`nil` = don't narrow (a site admin with `projects.admin_all`). A
non-admin without a resolvable user gets a uuid matching nothing rather
than the whole site — the narrowing must fail CLOSED, since a widget
renders on a host dashboard where nothing else is checking.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
