EXAMPLE.md
<!--
This file is also the source for the docs site's EXAMPLE.md tab.
The comments are valid erza source comments and are ignored by the compiler.
-->
<Screen title="Koinonia">
<!-- Splash runs before the main screen and is useful for app identity. -->
<Splash duration-ms="1400">
<!-- SplashAnimation is an unboxed ASCII frame animation. -->
<SplashAnimation fps="7">
<Frame>
_ __ _ _
| |/ /___ (_)___ ____ ____ ____(_)_ _
| / __ \/ / __ \/ __ \/ __ \/ __/ / ' \
| / /_/ / / / / / /_/ / / / / /_/ / /| |
|_|\_\____/_/_/ /_/\____/_/ /_/\__/_/_/ |_|
terminal social commons .
</Frame>
<Frame>
_ __ _ _
| |/ /___ (_)___ ____ ____ ____(_)_ _
| / __ \/ / __ \/ __ \/ __ \/ __/ / ' \
| / /_/ / / / / / /_/ / / / / /_/ / /| |
|_|\_\____/_/_/ /_/\____/_/ /_/\__/_/_/ |_|
terminal social commons ..
</Frame>
<Frame>
_ __ _ _
| |/ /___ (_)___ ____ ____ ____(_)_ _
| / __ \/ / __ \/ __ \/ __ \/ __/ / ' \
| / /_/ / / / / / /_/ / / / / /_/ / /| |
|_|\_\____/_/_/ /_/\____/_/ /_/\__/_/_/ |_|
terminal social commons ...
</Frame>
</SplashAnimation>
</Splash>
<!-- backend(...) pulls read-side data into the template scope. -->
<? auth = backend("auth.viewer") ?>
<? status = backend("ui.status") ?>
<? overview = backend("network.overview") ?>
<? highlights = backend("mission.highlights") ?>
<!-- PHP-style template delimiters drive branching and loops. -->
<? if auth.logged_in ?>
<? profile = backend("profiles.current") ?>
<? timeline = backend("feed.timeline") ?>
<!-- Top-level Section blocks are app tabs. -->
<Section title="Feed" tab-order="1" default-tab="true">
<Header>Town Square</Header>
<Text><?= status ?></Text>
<!-- ButtonRow is the standard horizontal action strip. -->
<ButtonRow>
<!-- ui.open_modal is handled by the runtime, not the backend. -->
<Action on:press="ui.open_modal" modal:id="create-post">New post</Action>
</ButtonRow>
<Text><?= overview.posts ?> posts, <?= overview.replies ?> replies, and <?= overview.people ?> residents are live in the square.</Text>
<? if timeline ?>
<? for post in timeline ?>
<!-- Nested Section blocks become boxed content panels. -->
<Section title="@<?= post.handle ?>">
<Text><?= post.body ?></Text>
<Text><?= post.likes ?> likes | <?= post.reply_count ?> replies</Text>
<ButtonRow align="right">
<!-- Non-ui action names are backend commands. -->
<Action on:press="feed.like" post:id="<?= post.id ?>">Like</Action>
<Action on:press="ui.open_modal" modal:id="feed-thread-<?= post.id ?>">View Replies</Action>
<Action on:press="ui.open_modal" modal:id="feed-compose-post-<?= post.id ?>">Reply</Action>
</ButtonRow>
</Section>
<? endfor ?>
<? else ?>
<Text>No posts yet. Open the modal above and start the square.</Text>
<? endif ?>
</Section>
<Section title="Profile" tab-order="0">
<!-- AsciiArt preserves whitespace exactly, which is ideal for profile pictures. -->
<AsciiArt><?= profile.picture ?></AsciiArt>
<Header>@<?= profile.handle ?></Header>
<Text><?= profile.description or "No description set yet." ?></Text>
<ButtonRow>
<Action on:press="ui.open_modal" modal:id="create-post">New post</Action>
<Action on:press="ui.open_modal" modal:id="profile-edit">Edit profile</Action>
</ButtonRow>
<Text><?= status ?></Text>
<? if profile.posts ?>
<? for post in profile.posts ?>
<Section title="@<?= post.handle ?>">
<Text><?= post.body ?></Text>
<Text><?= post.likes ?> likes | <?= post.reply_count ?> replies</Text>
<ButtonRow align="right">
<Action on:press="feed.like" post:id="<?= post.id ?>">Like</Action>
<Action on:press="ui.open_modal" modal:id="profile-thread-<?= post.id ?>">View Replies</Action>
<Action on:press="ui.open_modal" modal:id="profile-compose-post-<?= post.id ?>">Reply</Action>
</ButtonRow>
</Section>
<? endfor ?>
<? else ?>
<Text>No posts yet. Open the modal above and start your thread history.</Text>
<? endif ?>
</Section>
<!-- Direct-action tabs are valid when the tab triggers behavior immediately. -->
<Section title="Logout" tab-order="2">
<Action on:press="auth.logout">Log out</Action>
</Section>
<? for post in timeline ?>
<!-- View modals may contain read-only content and buttons that open form-only modals. -->
<Modal id="feed-thread-<?= post.id ?>" title="Replies for @<?= post.handle ?>">
<Header>@<?= post.handle ?></Header>
<Text><?= post.body ?></Text>
<Text><?= post.likes ?> likes | <?= post.reply_count ?> replies</Text>
<? if post.replies ?>
<? for reply in post.replies ?>
<Section title="@<?= reply.handle ?>">
<Text><?= reply.body ?></Text>
<Text><?= reply.likes ?> likes | <?= reply.reply_count ?> replies</Text>
<? for nested in reply.replies ?>
<Section title="@<?= nested.handle ?>">
<Text><?= nested.body ?></Text>
<Text><?= nested.likes ?> likes</Text>
</Section>
<? endfor ?>
<ButtonRow align="right">
<Action on:press="ui.open_modal" modal:id="feed-compose-reply-<?= reply.id ?>">Reply on Thread</Action>
</ButtonRow>
</Section>
<? endfor ?>
<? else ?>
<Text>No replies yet.</Text>
<? endif ?>
</Modal>
<!-- Form modals contain exactly one Form. -->
<Modal id="feed-compose-post-<?= post.id ?>" title="Reply">
<Form action="/threads/reply">
<Header>@<?= post.handle ?></Header>
<Text><?= post.body ?></Text>
<Text><?= post.likes ?> likes | <?= post.reply_count ?> replies</Text>
<!-- Hidden inputs pass thread state without rendering visible fields. -->
<Input name="thread_slug" type="hidden" value="<?= post.slug ?>" />
<Input name="body" type="text" label="Reply" required="mandatory" />
<!-- Inside forms, ButtonRow contains Submit buttons. -->
<ButtonRow align="right">
<Submit>Post reply</Submit>
</ButtonRow>
</Form>
</Modal>
<? for reply in post.replies ?>
<!-- Reply-on-reply shows boxed context before the actual form fields. -->
<Modal id="feed-compose-reply-<?= reply.id ?>" title="Reply on Thread">
<Form action="/threads/reply">
<Section title="@<?= post.handle ?>">
<Text><?= post.body ?></Text>
<Text><?= post.likes ?> likes | <?= post.reply_count ?> replies</Text>
<Section title="@<?= reply.handle ?>">
<Text><?= reply.body ?></Text>
<Text><?= reply.likes ?> likes | <?= reply.reply_count ?> replies</Text>
</Section>
</Section>
<Input name="thread_slug" type="hidden" value="<?= post.slug ?>" />
<Input name="parent_reply_id" type="hidden" value="<?= reply.id ?>" />
<Input name="body" type="text" label="Reply" required="mandatory" />
<ButtonRow align="right">
<Submit>Post reply</Submit>
</ButtonRow>
</Form>
</Modal>
<? endfor ?>
<? endfor ?>
<!-- The Profile tab mirrors much of the Feed thread UI with profile-scoped modal ids. -->
<? for post in profile.posts ?>
<Modal id="profile-thread-<?= post.id ?>" title="Replies for @<?= post.handle ?>">
<Header>@<?= post.handle ?></Header>
<Text><?= post.body ?></Text>
<Text><?= post.likes ?> likes | <?= post.reply_count ?> replies</Text>
<? if post.replies ?>
<? for reply in post.replies ?>
<Section title="@<?= reply.handle ?>">
<Text><?= reply.body ?></Text>
<Text><?= reply.likes ?> likes | <?= reply.reply_count ?> replies</Text>
<? for nested in reply.replies ?>
<Section title="@<?= nested.handle ?>">
<Text><?= nested.body ?></Text>
<Text><?= nested.likes ?> likes</Text>
</Section>
<? endfor ?>
<ButtonRow align="right">
<Action on:press="ui.open_modal" modal:id="profile-compose-reply-<?= reply.id ?>">Reply on Thread</Action>
</ButtonRow>
</Section>
<? endfor ?>
<? else ?>
<Text>No replies yet.</Text>
<? endif ?>
</Modal>
<Modal id="profile-compose-post-<?= post.id ?>" title="Reply">
<Form action="/threads/reply">
<Header>@<?= post.handle ?></Header>
<Text><?= post.body ?></Text>
<Text><?= post.likes ?> likes | <?= post.reply_count ?> replies</Text>
<Input name="thread_slug" type="hidden" value="<?= post.slug ?>" />
<Input name="body" type="text" label="Reply" required="mandatory" />
<ButtonRow align="right">
<Submit>Post reply</Submit>
</ButtonRow>
</Form>
</Modal>
<? for reply in post.replies ?>
<Modal id="profile-compose-reply-<?= reply.id ?>" title="Reply on Thread">
<Form action="/threads/reply">
<Section title="@<?= post.handle ?>">
<Text><?= post.body ?></Text>
<Text><?= post.likes ?> likes | <?= post.reply_count ?> replies</Text>
<Section title="@<?= reply.handle ?>">
<Text><?= reply.body ?></Text>
<Text><?= reply.likes ?> likes | <?= reply.reply_count ?> replies</Text>
</Section>
</Section>
<Input name="thread_slug" type="hidden" value="<?= post.slug ?>" />
<Input name="parent_reply_id" type="hidden" value="<?= reply.id ?>" />
<Input name="body" type="text" label="Reply" required="mandatory" />
<ButtonRow align="right">
<Submit>Post reply</Submit>
</ButtonRow>
</Form>
</Modal>
<? endfor ?>
<? endfor ?>
<!-- Edit profile shows both a plain text input and an ascii-art input. -->
<Modal id="profile-edit" title="Edit Profile">
<Form action="/profile/edit">
<Input name="description" type="text" label="Description" value="<?= profile.description ?>" />
<Input name="profile_picture" type="ascii-art" label="Profile Picture" value="<?= profile.picture ?>" />
<ButtonRow align="right">
<Submit>Save profile</Submit>
</ButtonRow>
</Form>
</Modal>
<Modal id="create-post" title="New Post">
<Form action="/posts">
<Input name="body" type="text" label="Post" required="mandatory" />
<ButtonRow align="right">
<Submit>Post to Koinonia</Submit>
</ButtonRow>
</Form>
</Modal>
<? else ?>
<!-- Logged-out users get a different tab set, driven by the same file. -->
<Section title="Why Koinonia">
<Header>One terminal-native town square.</Header>
<Text><?= overview.posts ?> posts, <?= overview.replies ?> replies, and <?= overview.people ?> residents are already live.</Text>
<? for highlight in highlights ?>
<Text><?= highlight.title ?></Text>
<Text><?= highlight.body ?></Text>
<? endfor ?>
</Section>
<Section title="Login / Sign Up">
<Action on:press="ui.open_modal" modal:id="auth-access">Open account access</Action>
</Section>
<Modal id="auth-access" title="Login / Sign Up">
<Form action="/auth/access">
<Input name="username" type="text" label="Username" required="mandatory" />
<Input name="password" type="password" label="Password" required="mandatory" />
<ButtonRow align="right">
<Submit>Enter Koinonia</Submit>
</ButtonRow>
</Form>
</Modal>
<? endif ?>
</Screen>