diff options
| author | ojacobson <ojacobson@noreply.codeberg.org> | 2025-07-10 03:35:32 +0200 |
|---|---|---|
| committer | ojacobson <ojacobson@noreply.codeberg.org> | 2025-07-10 03:35:32 +0200 |
| commit | f74b82450aa15b3e3e47617839c297cd1d60780e (patch) | |
| tree | b9091f1fa4f73de40fb7b530798ce2d3b94aee09 /ui/routes/(swatch)/.swatch/Message | |
| parent | 223b39a57ef6ca6b8288f5a8645183c41301f411 (diff) | |
| parent | 01ed82ac4f89810161fbc3aa1cb8e4691fb8938b (diff) | |
Create swatches for Svelte components.
A swatch is a live, and ideally editable, example of an element of the service. They serve as:
* Documentation: what is this element, how do you use it, what does it do?
* Demonstration: what does this element look like?
* Manual test scaffolding: when I change this element like _so_, what happens?
Swatches are collectively available under `/.swatch/` on a running instance. They do not require setup or login for simplicity's sake and because they don't _do_ anything that requires either of those things.
Swatches are manually curated. First, we lack the technical infrastructure needed to do this based on static analysis, and second, manual curation lets us include affordances like "interesting values," that would be tricky to express as part of the type or schema for the component. The tradeoff, however, is that they will fall out of step with the components if not reviewed regularly.
Swatches are _possible_ because we've gone to efforts to avoid global data access or direct side effects (including API requests) in our components, delegating that upwards to `+page`s and `+layout`s. However, the isolation is imperfect. For example, the swatch for `Conversation`, which renders the conversation sidebar entries, causes actual attempts to boot the app as browsers pre-fetch the links on mouseover, and clicking them will take the user to the "real" application because they really are links.
Merges swatch into main.
Diffstat (limited to 'ui/routes/(swatch)/.swatch/Message')
| -rw-r--r-- | ui/routes/(swatch)/.swatch/Message/+page.svelte | 91 |
1 files changed, 91 insertions, 0 deletions
diff --git a/ui/routes/(swatch)/.swatch/Message/+page.svelte b/ui/routes/(swatch)/.swatch/Message/+page.svelte new file mode 100644 index 0000000..6faf3bc --- /dev/null +++ b/ui/routes/(swatch)/.swatch/Message/+page.svelte @@ -0,0 +1,91 @@ +<script> + import { DateTime } from 'luxon'; + + import EventCapture from '$lib/swatch/event-capture.svelte.js'; + import { render } from '$lib/markdown.js'; + + import Message from '$lib/components/Message.svelte'; + import EventLog from '$lib/components/swatch/EventLog.svelte'; + + let id = $state('Mplayspelunky'); + let atInput = $state('2025-07-07T15:19:00Z'); + // Astonishingly, `DateTime.fromISO` does not throw on invalid inputs. It generates an "Invalid + // DateTime" sentinel value, instead. + let at = $derived(DateTime.fromISO(atInput)); + let renderedBodyInput = $state( + `Lorem ipsum \`dolor\` sit amet, consectetur adipiscing elit. Nunc quis ante ac leo tristique +iaculis vel in tortor. Praesent sed interdum ipsum. Pellentesque blandit, sapien at mattis +facilisis, leo mi gravida erat, in euismod mi lectus non dui. Praesent at justo vel mauris pulvinar +sodales ut sed nisl. Aliquam aliquet justo vel cursus imperdiet. Suspendisse potenti. Duis varius +tortor finibus, rutrum justo ac, tincidunt enim. + +Donec velit dui, bibendum a augue sit amet, tempus condimentum neque. Integer nibh tortor, imperdiet +at aliquet eu, rutrum eget ligula. Donec porttitor nisi lacus, eu bibendum augue maximus eget. Class +aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas in +est eget lectus dapibus tincidunt. Ut ut nisi egestas, posuere libero laoreet, venenatis erat. Nulla +maximus, nisl eget interdum ornare, enim turpis semper ligula, sed ultricies sem sem quis arcu. Ut a +dapibus augue. Pellentesque nec tincidunt sem. +`, + ); + /* + * Even though `Message` is notionally a generic container for markup, we restrict the swatch to + * message-flavoured Markdown. Swatches are available to all users, including + * technically-unsophisticated ones, and anything rendered in a swatch runs in the same origin + * context and the same cookie context as the rest of the client. + * + * This makes it possible that a user would be persuaded to enter something into a swatch that + * then runs _as them_, interacting with Pilcrow via its API or accessing client-stored data. + * + * As a proof of concept, `<img src="x" onerror="console.log('oh no')">` should not run the log + * statement. With generic HTML entry, it would do so. With our markdown processing, it does not + * (the `onerror` attribute is removed). Similarly, `script` elements are prohibited. + * + * Users who want to experiment with free HTML are encouraged to edit the swatch for themselves. + */ + let renderedBody = $derived(render(renderedBodyInput)); + let editable = $state(true); + let cssClass = $state(''); + + let capture = $state(new EventCapture()); + const deleteMessage = capture.on('deleteMessage'); +</script> + +<h1><code>Message</code></h1> + +<nav><p><a href=".">Back to swatches</a></p></nav> + +<h2>properties</h2> + +<div class="component-properties"> + <label>id <input type="text" bind:value={id} /></label> + <label>at (iso-8601)<input type="text" bind:value={atInput} /></label> + <div class="suggestion"> + interesting values: + <button onclick={() => (atInput = DateTime.now().toISO())}>Now</button> + </div> + + <label>css class <input type="text" bind:value={cssClass} /></label> + <div class="suggestion"> + interesting values: + <button onclick={() => (cssClass = '')}>(none)</button> + <button onclick={() => (cssClass = 'unsent')}>unsent</button> + <button onclick={() => (cssClass = 'deleted')}>deleted</button> + </div> + + <label>editable <input type="checkbox" bind:checked={editable} /></label> + + <label + ><p>rendered body (markdown)</p> + <textarea class="html" bind:value={renderedBodyInput}></textarea> + </label> +</div> + +<h2>rendered</h2> + +<div class="component-preview"> + <Message {id} {at} {renderedBody} {editable} class={cssClass} {deleteMessage} /> +</div> + +<h2>events</h2> + +<EventLog events={capture.events} clear={capture.clear.bind(capture)} /> |
