abstract class UI::App

Overview

Abstract base class for the declarative app + route registry; subclasses use the screen macro.

Defined in:

asset_pipeline/native_app.cr

Class Method Summary

Macro Summary

Class Method Detail

def self.app_design_tokens : UI::DesignTokens::Tokens #

If the consumer did not declare design_tokens do ... end, this default method returns the framework default. Method body is compile-time emitted code, unaffected by the iOS class-init gap.


[View source]
def self.bootstrap! : Nil #

Default .bootstrap! on the abstract UI::App itself — no-op (no screens to register). Subclasses override via the macro inherited hook below, which uses macro finished to enumerate every _bootstrap_screen_* method that the screen macro emitted and emit explicit calls to each.


[View source]
def self.initial_route_id : Symbol #

The route_id chosen as the navigation root. Override via the initial_route macro. Default :_unset is a sentinel that the dispatcher's launch_* helper detects and raises against.

Implemented as a method (not a class_getter ... = :_unset) because class-var default initialisers are skipped under the iOS class-init gap; method bodies are compile-time code unaffected by the gap.


[View source]
def self.registration_for(route_id : Symbol) : ScreenRegistration #

Look up a screen registration by route_id. Raises UI::App::UnknownRouteError with the list of known routes if the id is not registered.


[View source]
def self.screens : Hash(Symbol, UI::App::ScreenRegistration) #

[View source]

Macro Detail

macro design_tokens(&block) #

Optional: per-app design-token override block. The block receives the default Tokens instance and must RETURN a (possibly transformed) Tokens. Macro hygiene means we can't expose a local variable named tokens for the block to reassign, so the contract is "block in, transformed Tokens out".

design_tokens do |tokens|
  tokens.copy_with(touch_target_minimum_px: 51.0)
end

design_tokens do |tokens|
  tokens.with_brand(AcmeBrand.new)
end

[View source]
macro initial_route(route_id) #

Declare the initial route on launch.

class SpikeApp < UI::App
  initial_route :sign_in
end

[View source]
macro screen(route_id, controller = nil, screen_class = nil, web_controller = nil, web_path = nil, web_actions = nil) #

Register a screen with its controller. Screen class can be omitted — convention is FooController -> FooScreen (under the same namespace).

Phase 8B form (still works in 8C):

screen :sign_in, SignInController
screen :detail,  DetailController, screen_class: CustomScreen

Phase 8C — web-only or dual-target screens. The positional controller arg is optional (defaults to nil); screen_class is the third positional (preserves Phase 8B's screen :foo, FooCtrl, CustomScreen form). The new web kwargs (web_controller:, web_path:, web_actions:) follow and are passed by name. The brief proposed a * kwarg-only separator before the web kwargs, but Crystal macros cannot combine *, with a default value on a positional arg before it — every call that omits the positional fails with "wrong number of arguments". Without the *, the web kwargs are kwarg-by-default at the call site (the kwarg name disambiguates from positional misuse) and the type system catches positional confusion downstream (Class refs for web_controller, String for web_path, Array(NamedTuple) for web_actions).

# Web-only — no native side yet.
screen :sign_in,
       web_controller: SignInController,
       web_path: "/",
       web_actions: [
         {verb: :get,  action: :index},
         {verb: :post, action: :submit, path: "/sign_in/submit"},
       ]

# Dual-target — same class is the native AND web controller.
# (In the spike, SignInController is `< Amber::Controller::Base`
# only — Phase 8A's parallel-controllers shape. A future spike
# may show a native UI::Controller and Amber::Controller::Base
# passed separately.)
screen :todos, TodosController,
       web_controller: TodosController,
       web_path: "/todos"

# Web-only shortcut — bare web_path defaults web_actions to
# `[{verb: :get, action: :index, path: web_path}]`.
screen :about, web_controller: AboutController, web_path: "/about"

Defaulting and validation (all enforced at macro-expansion time):

  • If controller, web_controller, web_path, and web_actions are all unset → {% raise %} (registration must declare at least one side).
  • If any web binding is set, web_controller MUST be set → {% raise %} otherwise (web_path or web_actions without a web_controller is meaningless).
  • If web_actions is non-empty, every entry must either carry its own path: OR web_path must be set as the default → {% raise %} otherwise.
  • If web_actions is empty AND web_path is set → default web_actions to [{verb: :get, action: :index, path: web_path}]. Note on signature: the brief proposed a * kwarg-only separator before the new web kwargs, but Crystal macros cannot combine a * separator with a default value on a positional arg before it ("wrong number of arguments" at every call site that omits the positional). To preserve Phase 8B's call shapes EXACTLY, the parameter order keeps screen_class as the third positional (immediately after controller) so old calls of the form screen :foo, FooController, CustomScreen continue to bind CustomScreen to screen_class. The new web kwargs follow. Codex iter-1 finding identified that re-ordering screen_class past the web kwargs silently re-bound the third positional — this reordering pins the legacy form.

[View source]