clone

one developer profile for links and private tcp services, reached peer to peer over iroh with no open ports

main
devsite / docs / profile-template.md
profile-template.md 13.8K rendered
1# The profile template23A dev.site profile is one fixed piece of semantic HTML styled by [Pico CSS] and4nothing else. Personalisation happens through validated declarations, not rules:5Pico variables control appearance, while three dev.site variables control the6initial folder layout. That makes "is this profile declaration valid?" a question7the server can answer mechanically in `crates/devsite-server/src/theme.rs`.89[Pico CSS]: https://picocss.com1011## What the page is made of1213| Layer | File | Role |14| --- | --- | --- |15| Framework | `web/vendor/pico.min.css` | Pico 2.1.1, vendored. Styles semantic elements directly. |16| Site | `web/site.css` | The typeface, and the few layout rules Pico has no opinion about. No colours. |17| Theme | `<style id="profile-theme">` | One rule, written at render time, holding only `--pico-*` assignments. `--devsite-*` layout values never enter it. |1819They load in that order, and the theme is written as one rule:2021```css22:root[data-profile="alice"] { --pico-primary: #bc6c25; … }23```2425The `:root` is not decoration. Pico sets its palette on26`:root:not([data-theme=dark])`, which is specificity **(0,2,0)** — `:not()`27contributes the weight of its argument. A bare `[data-profile="alice"]` is28(0,1,0) and loses to that outright, however late it appears in the document.29Matching (0,2,0) and coming last is what makes a theme win.3031This was wrong in the first implementation, and it failed silently: themes32validated, stored, served in the profile response, and applied to a page that33went on looking exactly the same. Nothing short of reading a computed style in a34real browser would have caught it, which is now how it is checked.3536A theme still writes no selectors of its own — that one is supplied here, and the37user supplies only the declarations inside it.3839**Type is fixed.** Open Sans, vendored at `web/vendor/fonts/`, at 400 and 700 and40no other weight. The file is a variable font, so it is declared twice at those two41weights rather than as a range: a request for 600 — which Pico makes in a couple42of places — resolves to 700 instead of rendering an in-between instance. There is43no `--pico-font-family` in the whitelist.4445## The template4647The control plane renders this structure for `GET /ui/profile/{handle}/stream`.48SSEXi reads each HTML fragment, and Paxi morphs it into `<main>`. Profile declarations are49only meaningful against this structure. Change the structure and whitelist together.5051```html52<body>53 <header class="container">54 <nav>…wordmark…<ul id="session">…</ul></nav>55 </header>5657 <main class="container" id="main" data-profile-handle="alice"58 data-profile-scheme="auto">59 <article id="profile">60 <hgroup>61 <h1>@alice</h1>62 <p>3 sites</p>63 </hgroup>6465 <!-- loose sites first, in the order they were published -->66 <ul class="entries">67 <!-- reached at its own address, so it is an anchor and takes you away.68 The host is always present and folded into the arrow; see below. -->69 <li class="entry" id="site-res_…" data-kind="link" data-visibility="public">70 <a href="https://klot.ski" target="_blank" rel="noopener noreferrer">klot.ski</a>71 <small class="state"><span class="host"><span>klot.ski&nbsp;</span></span>↗</small>72 </li>73 <!-- reached through its owner's daemon, so it is a button that opens74 the browser's ticket-minting prompt here. -->75 <li class="entry" id="site-res_…" data-kind="service" data-visibility="private">76 <button class="outline" data-service-id="res_…"77 data-service-name="Hermes">Hermes</button>78 <small class="state">private</small>79 </li>80 </ul>8182 <!-- then a fold per folder, in the owner's declared order followed by83 unlisted folders in the order they first appear -->84 <details class="folder" data-folder="Games" open>85 <summary>Games <small>3</small></summary>86 <ul class="entries">…</ul>87 </details>8889 <!-- accepted shares use the same rows and folds, with "from @owner" in the name -->90 </article>91 </main>9293 <dialog id="service-ticket-dialog">…Get ticket, then devsite connect…</dialog>94</body>95```9697Everything on a profile is a site. `data-kind` says how you get to one — `link`98is reached at its own address, while `service` is a TCP byte stream reached with99`devsite connect`. The page does not sort itself into kinds, because which of the100two a thing is says nothing about what it is for.101102`data-visibility` is on the row: `public`, `private` or `shared`. It is written103out in `.state` only where it is not `public`. For a link, visibility controls104who can discover the URL on dev.site; it cannot stop someone who already knows105the destination from opening or forwarding it. Shared links require recipient106approval, and changing their destination requires fresh approval.107108There is deliberately no reachability state to style: the page does not know109whether a daemon is running, and says so by saying nothing.110111### The host112113Every link carries its host, including the ones named after it. At rest you see114the arrow; pointing at a row expands the host leftward out of it, so two repos115can both say `github.com` without two rows saying it at once.116117`.host` is an inline grid whose single column animates `0fr → 1fr`, which reaches118exactly the content's width — a `max-width` would need a guess, and a wrong guess119either clips a long host or spends the transition crossing empty space. The120trailing space sits inside the clipped inner span so it collapses with the text121rather than leaving a gap before the arrow. Where there is no hover to ask with,122`@media (hover: none)`, the host is simply always shown.123124## Folders125126A folder is a name on a site, not a thing of its own — `devsite link set --folder127Games` files one, and leaving `--folder` off takes it out again. There is nothing128to create and nothing to delete, so a profile can never be left holding an empty129one, and renaming a folder is retagging what is in it.130131That falls out of the rendering too. The set of folders is derived from the132entries the visitor is allowed to see, so a folder holding only private sites does133not appear to a stranger as an empty fold — it is never built at all.134135Folds are semantic `<details>` elements. They are open by default for profiles136without layout declarations. `--devsite-folders` changes that initial default,137and `--devsite-open-folders` names exceptions that start open. Once rendered,138the reader controls the ordinary open state. Pico draws the marker and handles139the interaction; `.folder > summary` is the label.140141`--devsite-folder-order` names folders that should appear first. A name not142visible to the current visitor is ignored; visible folders omitted from the list143retain their first-appearance order. Folder names use JSON-style quoted strings,144the same non-control Unicode accepted by resource folders, with the same14540-scalar limit. Quotes and backslashes use JSON escapes.146147`--pico-ins-color` and `--pico-del-color` are still in the whitelist, and are148what the page's success and failure messages are drawn with.149150## What profile presentation may set151152The list lives in `PROPERTIES` in `theme.rs` and is served at153`GET /api/theme/properties`, which is also what `devsite theme properties`154prints. Every `--pico-*` name in it is a variable the vendored Pico actually155defines. The control plane consumes the three folder-layout names as profile data. It156never emits them as CSS. An unrecognized name is rejected rather than accepted,157stored, and left to do nothing.158159| Group | Properties | Value |160| --- | --- | --- |161| Scheme | `--devsite-scheme` | `light`, `dark` or `auto` |162| Layout | `--devsite-folders` | `open` or `closed` |163| | `--devsite-open-folders`, `--devsite-folder-order` | one or more quoted folder names |164| Surfaces | `--pico-background-color`, `--pico-color`, `--pico-muted-color`, `--pico-muted-border-color`, `--pico-border-color`, `--pico-text-selection-color` | colour |165| Accents | `--pico-primary` and its `-background`, `-hover`, `-hover-background`, `-inverse`, `-underline`, `-focus`; `--pico-secondary` and its `-background`, `-hover`, `-inverse`; `--pico-contrast` and its `-background`, `-inverse` | colour |166| Headings | `--pico-h1-color` … `--pico-h6-color` | colour |167| Blocks | `--pico-card-background-color`, `--pico-card-border-color`, `--pico-card-sectioning-background-color`, `--pico-code-background-color`, `--pico-code-color`, `--pico-mark-background-color`, `--pico-mark-color`, `--pico-blockquote-border-color`, `--pico-accordion-active-summary-color`, `--pico-accordion-close-summary-color`, `--pico-accordion-open-summary-color`, `--pico-ins-color`, `--pico-del-color` | colour |168| Form elements | `--pico-form-element-background-color`, `--pico-form-element-border-color`, `--pico-form-element-color` | colour |169| Effects | `--pico-box-shadow` | `unset` |170| Metrics | `--pico-border-radius`, `--pico-border-width`, `--pico-outline-width`, `--pico-spacing`, `--pico-block-spacing-vertical`, `--pico-block-spacing-horizontal`, `--pico-typography-spacing-vertical`, `--pico-form-element-spacing-vertical`, `--pico-form-element-spacing-horizontal`, `--pico-nav-element-spacing-vertical`, `--pico-nav-element-spacing-horizontal`, `--pico-text-underline-offset`, `--pico-font-size` | length |171| Type | `--pico-line-height` | number |172| | `--pico-font-weight` | `400` or `700` |173| | `--pico-text-decoration` | `none` or `underline` |174175The `--devsite-*` keys are not Pico variables. `--devsite-scheme` sets176`data-theme` on the page, which is how Pico chooses between its own light and177dark palettes; the folder-layout keys are consumed as data while rendering the178semantic folds. `auto` leaves `data-theme` unset and follows the visitor's system179preference. Explicit `light` or `dark` forces that scheme.180181### Value grammars182183| Kind | Accepts |184| --- | --- |185| colour | `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`; `rgb()`, `rgba()`, `hsl()`, `hsla()`, `oklch()` over plain numbers; a CSS colour name; `transparent`; `currentcolor`; or `light-dark(<light-color>, <dark-color>)` containing exactly two of these colours |186| folder list | one or more comma-separated JSON-style strings containing non-empty, trimmed, non-control folder names of at most 40 Unicode scalar values |187| length | a non-negative number with `px`, `rem`, `em`, `ch`, `ex`, `vw`, `vh`, `vmin`, `vmax` or `%`, or bare `0` |188| number | a non-negative unitless number |189190`var()`, `calc()` and `url()` are refused from CSS values. Every accepted Pico191value is drawn from the alphabet `[0-9a-z#%.,/()+- ]`, so it cannot carry `<`,192`"` or `}`. Folder names instead use the bounded JSON-string grammar and never193enter the generated `<style>` rule.194195`light-dark()` uses its first colour in light mode and its second in dark mode.196Either side may use any supported colour form, including a functional colour:197198```css199--devsite-scheme: auto;200--pico-primary: light-dark(#7b3fe4, #a982ff);201--pico-background-color: light-dark(rgb(250 248 255), rgb(24 18 32));202```203204With `auto`, the pair follows the visitor's system preference. Setting205`--devsite-scheme: light` always selects the first colour; setting it to `dark`206always selects the second. The parser understands only this one level of207nesting: another `light-dark()`, `var()`, `calc()`, `url()`, or any other208function inside the pair is refused.209210## What profile presentation cannot do211212Refused outright, with a message naming the reason:213214- selectors, braces, at-rules, and `!important`215- ordinary CSS properties — `position`, `display`, `content` and the rest216- Pico variables outside the list, including `--pico-font-family` and the217 `--pico-icon-*` data URIs218219Pico declarations can recolour and re-space the template, and that is all.220Layout declarations may choose the initial open state and order of folders, but221they cannot remove a visible entry or prevent a reader from opening a fold.222Nothing can position, overlay, or load anything, so a profile you visit cannot223use its presentation to disguise itself as another part of the site.224225## Setting one226227From the CLI, and only from the CLI. The website reads profiles, manages account228controls, approves shares, and mints service tickets; links, hosted services,229sharing targets, themes, and folder layout are set with `devsite`. Claiming a230handle remains in the browser because that is where signing in finishes.231232```bash233devsite theme properties # what you may set234devsite theme show235devsite theme set my-theme.css # or `-` for stdin236devsite theme clear237```238239```css240/* my-theme.css */241--devsite-scheme: auto;242--devsite-folders: closed;243--devsite-open-folders: "Profiles";244--devsite-folder-order: "Profiles", "Services", "Games";245--pico-background-color: light-dark(#fefae0, #283618);246--pico-color: light-dark(#283618, #fefae0);247--pico-primary: light-dark(#bc6c25, #dda15e);248--pico-primary-hover: light-dark(#606c38, #fefae0);249--pico-accordion-active-summary-color: light-dark(#606c38, #dda15e);250--pico-accordion-close-summary-color: light-dark(#606c38, #dda15e);251--pico-accordion-open-summary-color: light-dark(#606c38, #dda15e);252--pico-box-shadow: unset;253--pico-border-radius: 0.5rem;254```255256Rejections are specific and name the offending declaration:257258```259--pico-primary: wine; → `--pico-primary: wine` — expected a colour, e.g. `#7b3fe4`, …260--pico-primary-color: → `--pico-primary-color` is not a profile property — did you mean `--pico-primary`?261body { color: red } → `{` is not allowed: a profile is a list of validated declarations262```263264## Storage265266Validated once, on write, by `PUT /api/theme`, and stored in267`profiles.custom_css` as canonical text — one `property: value;` per line. Every268later read treats the column as already-checked rather than re-deciding what is269safe. It is re-parsed on render anyway, so that a property retired from the270whitelist degrades a profile to the defaults instead of serving a rule the271current build no longer stands behind.