From 64d16441a390e889231f2f67333d5f305b7ab878 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 01:44:46 -0400 Subject: Set up a skeleton for swatches. 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, and are set up in a separate [group] from the rest of the UI. They do not require setup or login for simplicity's sake and because they don't _do_ anything that requires either of those things. [group]: https://svelte.dev/docs/kit/advanced-routing#Advanced-layouts-(group) Swatches are manually curated, for a couple of reasons: * We lack the technical infrastructure needed to do this based on static analysis; and * Manual curation lets us include affordances like "recommended values," that would be tricky to express as part of the type or schema for the component. The tradeoff, however, is that swatches may fall out of step with the components they depic, if not reviewed regularly. I hope that, by making them part of the development process, this risk will be mitigated through regular use. --- docs/developer/SUMMARY.md | 4 ++++ docs/developer/client/swatches.md | 16 ++++++++++++++++ src/routes.rs | 1 + src/ui/handlers/mod.rs | 2 ++ src/ui/handlers/swatch.rs | 8 ++++++++ ui/app.css | 1 + ui/routes/(swatch)/.swatch/+page.js | 1 + ui/routes/(swatch)/.swatch/+page.svelte | 10 ++++++++++ ui/styles/swatches.css | 24 ++++++++++++++++++++++++ 9 files changed, 67 insertions(+) create mode 100644 docs/developer/client/swatches.md create mode 100644 src/ui/handlers/swatch.rs create mode 100644 ui/routes/(swatch)/.swatch/+page.js create mode 100644 ui/routes/(swatch)/.swatch/+page.svelte create mode 100644 ui/styles/swatches.css diff --git a/docs/developer/SUMMARY.md b/docs/developer/SUMMARY.md index 6b0ce6a..71f85d1 100644 --- a/docs/developer/SUMMARY.md +++ b/docs/developer/SUMMARY.md @@ -14,6 +14,10 @@ - [Running Pilcrow locally](server/running.md) - [Debian packaging](server/debian-packaging.md) +# The client + +- [Swatches](client/swatches.md) + # Development tools - [Formatting](tools/formatting.md) diff --git a/docs/developer/client/swatches.md b/docs/developer/client/swatches.md new file mode 100644 index 0000000..df4f643 --- /dev/null +++ b/docs/developer/client/swatches.md @@ -0,0 +1,16 @@ +# Swatches + +To make it easier to experiment with the client's component framework, the client exposes "swatches" - pages demonstrating individual components in isolation. + +Swatches are available from a running client at the `/.swatch/` URL. This URL is not linked in the client; you Just Need To Know. + +## Writing Swatches + +Swatches are manually curated. When adding a component, add a swatch if you can. + +Things to consider: + +- For complex values, use a parser, so that the user reading your swatch can edit the structure as text. +- For freeform values whose meaning is significant, provide buttons to let the user rapidly enter and experiment with interesting values. +- Be thorough. Let the user experiment with values, even if you think those values may be nonsensical. +- Try to show the component _without_ supporting markup, as far as is possible. However, if a component generates markup that requires context - a table row component needs a table, for example, or a list element needs a list - then include that markup in the swatch. diff --git a/src/routes.rs b/src/routes.rs index 49d9fb6..6993070 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -10,6 +10,7 @@ pub fn routes(app: &App) -> Router { // UI routes that can be accessed before the administrator completes setup. let ui_bootstrap = Router::new() .route("/{*path}", get(ui::handlers::asset)) + .route("/.swatch/{*path}", get(ui::handlers::swatch)) .route("/setup", get(ui::handlers::setup)); // UI routes that require the administrator to complete setup first. diff --git a/src/ui/handlers/mod.rs b/src/ui/handlers/mod.rs index ed0c14e..bcc65a1 100644 --- a/src/ui/handlers/mod.rs +++ b/src/ui/handlers/mod.rs @@ -5,6 +5,7 @@ mod invite; mod login; mod me; mod setup; +mod swatch; pub use asset::handler as asset; pub use conversation::handler as conversation; @@ -13,3 +14,4 @@ pub use invite::handler as invite; pub use login::handler as login; pub use me::handler as me; pub use setup::handler as setup; +pub use swatch::handler as swatch; diff --git a/src/ui/handlers/swatch.rs b/src/ui/handlers/swatch.rs new file mode 100644 index 0000000..4562b04 --- /dev/null +++ b/src/ui/handlers/swatch.rs @@ -0,0 +1,8 @@ +use crate::{ + error::Internal, + ui::assets::{Asset, Assets}, +}; + +pub async fn handler() -> Result { + Assets::index() +} diff --git a/ui/app.css b/ui/app.css index 34a74b7..714c9f7 100644 --- a/ui/app.css +++ b/ui/app.css @@ -9,6 +9,7 @@ @import url('styles/messages.css'); @import url('styles/textarea.css'); @import url('styles/forms.css'); +@import url('styles/swatches.css'); @import url('styles/invites.css'); body { diff --git a/ui/routes/(swatch)/.swatch/+page.js b/ui/routes/(swatch)/.swatch/+page.js new file mode 100644 index 0000000..d3c3250 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/+page.js @@ -0,0 +1 @@ +export const trailingSlash = 'always'; diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte new file mode 100644 index 0000000..8d03c8d --- /dev/null +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -0,0 +1,10 @@ +

swatches

+ +

+ Swatches are "live, but disconnected" elements of the application, designed specifically to + support live editing and iteration. +

+ +

components

+ +
    diff --git a/ui/styles/swatches.css b/ui/styles/swatches.css new file mode 100644 index 0000000..f2a9f99 --- /dev/null +++ b/ui/styles/swatches.css @@ -0,0 +1,24 @@ +.component-preview { + border: 1px solid grey; + margin: 1rem 2rem; +} + +.component-properties { + margin: 1rem 2rem; +} + +.component-properties textarea { + width: 100%; + height: 10em; +} + +.component-properties textarea.html { + font-family: FiraCode, monospace; +} + +.component-properties .suggestion { + font-size: 80%; + margin-left: 1rem; + margin-right: 1rem; + margin-bottom: 0.5em; +} -- cgit v1.2.3 From aeb159c401ef446e6564e5a3643027560a3e22f4 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 01:49:07 -0400 Subject: Create "derivers," as an exception-free option for working with structured data in swatches. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is meant to be used alongside `$derive`, for inputs with complex structure. For example: ```js let jsonInput = $state('{}'); let json = $derived(deriver.json(jsonInput)); // … ``` This allows textual editing of the data, while preventing exceptions due to syntax or logical errors in partially-edited data from breaking Svelte's derive process (see comments). Note that these exceptions are not considered [unexpected errors] by SvelteKit, because they do not arise "while handling a request;" they are considered errors by Svelte, but Svelte doesn't appear to provide any affordances for handling errors in this context, so we have to bring our own. [unexpected errors]: https://svelte.dev/docs/kit/errors#Unexpected-errors --- ui/lib/swatch/derive.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 ui/lib/swatch/derive.js diff --git a/ui/lib/swatch/derive.js b/ui/lib/swatch/derive.js new file mode 100644 index 0000000..85547e8 --- /dev/null +++ b/ui/lib/swatch/derive.js @@ -0,0 +1,20 @@ +// A fun fact about $derive: if the deriving expression throws an error, then Svelte (at least as of +// 5.20.2) _permanently_ stops evaluating it, even if the inputs change. To prevent this, you need +// to ensure that derive expressions never raise an exception. +function tryDerive(args, func, fallback) { + try { + return func(...args); + } catch (e) { + console.debug('deriver threw exception', e, func, args); + return fallback; + } +} + +// A "deriver" is a function that never raises; if the underlying function would raise, the +// corresponding deriver instead returns a fallback value (or `undefined`). +export function makeDeriver(func, fallback) { + return (...args) => tryDerive(args, func, fallback); +} + +// Some widely-used derivers, for convenience. +export const json = makeDeriver(JSON.parse); -- cgit v1.2.3 From c631c8fc855b1854f14fc7fbf1d8afedaa37a0db Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 01:35:28 -0400 Subject: Event capture and display tools. This is meant to be used in swatches, to display the events and callbacks generated by a component as part of the swatch. The usage pattern is described in the comments (in both places). Naturally, this has its own swatch. --- ui/lib/components/swatch/EventLog.svelte | 37 +++++++++++++++++++++ ui/lib/swatch/event-capture.svelte.js | 22 +++++++++++++ ui/routes/(swatch)/.swatch/+page.svelte | 4 ++- .../(swatch)/.swatch/swatch/EventLog/+page.svelte | 38 ++++++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 ui/lib/components/swatch/EventLog.svelte create mode 100644 ui/lib/swatch/event-capture.svelte.js create mode 100644 ui/routes/(swatch)/.swatch/swatch/EventLog/+page.svelte diff --git a/ui/lib/components/swatch/EventLog.svelte b/ui/lib/components/swatch/EventLog.svelte new file mode 100644 index 0000000..2d8c5c0 --- /dev/null +++ b/ui/lib/components/swatch/EventLog.svelte @@ -0,0 +1,37 @@ + + + + + + + + + + + + {#each events as { event, args }} + + + + + {/each} + +
    eventarguments
    {event}{JSON.stringify(args)}
    diff --git a/ui/lib/swatch/event-capture.svelte.js b/ui/lib/swatch/event-capture.svelte.js new file mode 100644 index 0000000..32c0f39 --- /dev/null +++ b/ui/lib/swatch/event-capture.svelte.js @@ -0,0 +1,22 @@ +/* + * The interface exposed by this class is designed to closely match the interface expected by + * the `EventLog` component, so that you can do this: + * + * let capture = $state(new EventCapture()); + * const someEvent = capture.on('someEvent'); + * + * // … + * + * + */ +export default class EventCapture { + events = $state([]); + + on(event) { + return (...args) => this.events.push({ event, args }); + } + + clear() { + this.events = []; + } +} diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index 8d03c8d..abcb53f 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -7,4 +7,6 @@

    components

    -
      + diff --git a/ui/routes/(swatch)/.swatch/swatch/EventLog/+page.svelte b/ui/routes/(swatch)/.swatch/swatch/EventLog/+page.svelte new file mode 100644 index 0000000..a751ca3 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/swatch/EventLog/+page.svelte @@ -0,0 +1,38 @@ + + +

      swatch/EventLog

      + + + +

      properties

      + +
      + +
      + +

      rendered

      + +
      + +
      + +

      events

      + + -- cgit v1.2.3 From e7f942dd5b638a351cbff4ae4e4c35aef6aebcac Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 19:35:26 -0400 Subject: Add a swatch for the `ChangePassword` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + .../(swatch)/.swatch/ChangePassword/+page.svelte | 23 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/ChangePassword/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..4833bcb 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/ChangePassword/+page.svelte b/ui/routes/(swatch)/.swatch/ChangePassword/+page.svelte new file mode 100644 index 0000000..8ef6792 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/ChangePassword/+page.svelte @@ -0,0 +1,23 @@ + + +

      ChangePassword

      + + + +

      rendered

      + +
      + +
      + +

      events

      + + -- cgit v1.2.3 From a2a0ec035f74a454df5a76ed999355f536f5b277 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 19:45:49 -0400 Subject: Create a swatch for the `Conversation` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + .../(swatch)/.swatch/Conversation/+page.svelte | 31 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/Conversation/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..165bbad 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/Conversation/+page.svelte b/ui/routes/(swatch)/.swatch/Conversation/+page.svelte new file mode 100644 index 0000000..0b98204 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/Conversation/+page.svelte @@ -0,0 +1,31 @@ + + +

      Conversation

      + + + +

      properties

      + +
      + + + + +
      + +

      rendered

      + +
      + +
      -- cgit v1.2.3 From 3fd175165ee262f0180b78d2dd206a993c20200c Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 7 Jul 2025 16:11:59 -0400 Subject: Create a swatch for the `ConversationList` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + .../(swatch)/.swatch/ConversationList/+page.svelte | 54 ++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/ConversationList/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..c8f2133 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/ConversationList/+page.svelte b/ui/routes/(swatch)/.swatch/ConversationList/+page.svelte new file mode 100644 index 0000000..1a56966 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/ConversationList/+page.svelte @@ -0,0 +1,54 @@ + + +

      ConversationList

      + + + +

      properties

      + +
      + + + +
      + interesting values: + + + +
      +
      + +

      rendered

      + +
      + +
      -- cgit v1.2.3 From ca4a9dfbc2d2339e6f76330026d8fe15be36ee67 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 19:52:28 -0400 Subject: Create swatch for the `CreateConversationForm` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + .../.swatch/CreateConversationForm/+page.svelte | 23 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/CreateConversationForm/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..4ce1762 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/CreateConversationForm/+page.svelte b/ui/routes/(swatch)/.swatch/CreateConversationForm/+page.svelte new file mode 100644 index 0000000..f089969 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/CreateConversationForm/+page.svelte @@ -0,0 +1,23 @@ + + +

      CreateConversationForm

      + + + +

      rendered

      + +
      + +
      + +

      events

      + + -- cgit v1.2.3 From f7c89692e76112fd20275ce631c102588835c8b8 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 19:53:08 -0400 Subject: Create swatch for the `Invite` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + ui/routes/(swatch)/.swatch/Invite/+page.svelte | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/Invite/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..5242718 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/Invite/+page.svelte b/ui/routes/(swatch)/.swatch/Invite/+page.svelte new file mode 100644 index 0000000..0786194 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/Invite/+page.svelte @@ -0,0 +1,21 @@ + + +

      Invite

      + + + +

      properties

      + +
      + +
      + +

      rendered

      + +
      + +
      -- cgit v1.2.3 From 718ffe3dceffb4a0439d2b3192a266cb75c7fda7 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 19:55:34 -0400 Subject: Create swatch for the `Invites` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + ui/routes/(swatch)/.swatch/Invites/+page.svelte | 35 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/Invites/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..c28b46e 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/Invites/+page.svelte b/ui/routes/(swatch)/.swatch/Invites/+page.svelte new file mode 100644 index 0000000..8c24627 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/Invites/+page.svelte @@ -0,0 +1,35 @@ + + +

      Invites

      + + + +

      properties

      + +
      + +
      + +

      rendered

      + +
      + +
      + +

      events

      + + -- cgit v1.2.3 From dbccc44ad781bba04862017c9b041240821d4c08 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 19:56:11 -0400 Subject: Create swatch for the `LogIn` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + ui/routes/(swatch)/.swatch/LogIn/+page.svelte | 31 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/LogIn/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..9484cf9 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/LogIn/+page.svelte b/ui/routes/(swatch)/.swatch/LogIn/+page.svelte new file mode 100644 index 0000000..8501a6c --- /dev/null +++ b/ui/routes/(swatch)/.swatch/LogIn/+page.svelte @@ -0,0 +1,31 @@ + + +

      LogIn

      + + + +

      properties

      + +
      + +
      + +

      rendered

      + +
      + +
      + +

      events

      + + -- cgit v1.2.3 From 3b32644d2f30170bdf949dffaea79cde4d4ec647 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 19:57:50 -0400 Subject: Create swatch for the `MessageInput` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + .../(swatch)/.swatch/MessageInput/+page.svelte | 23 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/MessageInput/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..db02fc3 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/MessageInput/+page.svelte b/ui/routes/(swatch)/.swatch/MessageInput/+page.svelte new file mode 100644 index 0000000..b1d5864 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/MessageInput/+page.svelte @@ -0,0 +1,23 @@ + + +

      MessageInput

      + + + +

      rendered

      + +
      + +
      + +

      events

      + + -- cgit v1.2.3 From 44a462286ccaa8489642460a0199c951aad43418 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 19:58:14 -0400 Subject: Create swatch for the `Message` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + ui/routes/(swatch)/.swatch/Message/+page.svelte | 63 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/Message/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..d3b211a 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/Message/+page.svelte b/ui/routes/(swatch)/.swatch/Message/+page.svelte new file mode 100644 index 0000000..6fd9b6b --- /dev/null +++ b/ui/routes/(swatch)/.swatch/Message/+page.svelte @@ -0,0 +1,63 @@ + + +

      Message

      + + + +

      properties

      + +
      + + +
      + interesting values: + +
      + + +
      + interesting values: + + + +
      + + + + +
      + +

      rendered

      + +
      + +
      + +

      events

      + + -- cgit v1.2.3 From 0fc1908827cbae3652f1ba2ef746aad5ab1a73c8 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 20:03:24 -0400 Subject: Create swatch for the `MessageRun` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + ui/routes/(swatch)/.swatch/MessageRun/+page.svelte | 40 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/MessageRun/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..dbdf883 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/MessageRun/+page.svelte b/ui/routes/(swatch)/.swatch/MessageRun/+page.svelte new file mode 100644 index 0000000..a8c8853 --- /dev/null +++ b/ui/routes/(swatch)/.swatch/MessageRun/+page.svelte @@ -0,0 +1,40 @@ + + +

      MessageRun

      + + + +

      properties

      + +
      + + +
      + interesting values: + + +
      + + +
      + +

      rendered

      + +
      + + + {@html children} + +
      -- cgit v1.2.3 From 5bdebec905c3ad13220c8f55c136c34d31236d35 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 8 Jul 2025 20:08:12 -0400 Subject: Create swatch for the `LogOut` component. --- ui/routes/(swatch)/.swatch/+page.svelte | 1 + ui/routes/(swatch)/.swatch/LogOut/+page.svelte | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 ui/routes/(swatch)/.swatch/LogOut/+page.svelte diff --git a/ui/routes/(swatch)/.swatch/+page.svelte b/ui/routes/(swatch)/.swatch/+page.svelte index abcb53f..9a26017 100644 --- a/ui/routes/(swatch)/.swatch/+page.svelte +++ b/ui/routes/(swatch)/.swatch/+page.svelte @@ -8,5 +8,6 @@

      components

      diff --git a/ui/routes/(swatch)/.swatch/LogOut/+page.svelte b/ui/routes/(swatch)/.swatch/LogOut/+page.svelte new file mode 100644 index 0000000..5eebcde --- /dev/null +++ b/ui/routes/(swatch)/.swatch/LogOut/+page.svelte @@ -0,0 +1,23 @@ + + +

      LogOut

      + + + +

      rendered

      + +
      + +
      + +

      events

      + + -- cgit v1.2.3 From 01ed82ac4f89810161fbc3aa1cb8e4691fb8938b Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Wed, 9 Jul 2025 18:57:16 -0400 Subject: Do not support users entering bare HTML in swatches. You can inject Javascript into a swatch that uses `{@html }` fairly easily. `` doesn't appear to work, but `` does, for example. That code then runs with the same access to cookies, and the same access to local data, as the Pilcrow client. This change removes that capability, by replacing the two swatches that exposed it with more limited examples. I love the generality and flexibility of generic HTML entry here, and I think it might have been useful for swatching components that are generic DOM containers (which both `Message` and `MessageRun` are today), but swatches are a user interface and are exposed to _all_ users. A user who is unfamiliar with HTML and Javascript, but who is persuaded to open a swatch and enter some code into it (think about an attacker who tells their victim "hey check out this funny thing that happens," preying on curiousity, while providing a lightly-obfuscated payload) can then impersonate that user, exfiltrate anything saved locally, or potentially install persistent code using JS' various background-processing APIs. Gnarly stuff. We're not up to mitigating that in place. Anyone who knows JS can likely learn to build the client from source, and can experiment with arbitrary input that way, taking responsibility for the results in the process, while anyone who doesn't is unlikely to be persuaded to set up an entire Node toolchain just for an exploit. --- docs/developer/client/swatches.md | 10 +++++ ui/routes/(swatch)/.swatch/Message/+page.svelte | 38 ++++++++++++++--- ui/routes/(swatch)/.swatch/MessageRun/+page.svelte | 47 +++++++++++++++++----- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/docs/developer/client/swatches.md b/docs/developer/client/swatches.md index df4f643..ad11381 100644 --- a/docs/developer/client/swatches.md +++ b/docs/developer/client/swatches.md @@ -14,3 +14,13 @@ Things to consider: - For freeform values whose meaning is significant, provide buttons to let the user rapidly enter and experiment with interesting values. - Be thorough. Let the user experiment with values, even if you think those values may be nonsensical. - Try to show the component _without_ supporting markup, as far as is possible. However, if a component generates markup that requires context - a table row component needs a table, for example, or a list element needs a list - then include that markup in the swatch. + +## Swatches and XSS + +⚠ Swatches **should not** accept arbitrary HTML as user input and render it. Use something less expressive, instead. + +Swatches are an interface _intended for_ developers, but _available to_ everyone - including users who may not be familiar with the Web platform or the level of access a page might have to their credentials or data. Putting HTML inputs on a swatch invites the risk that a user may be mislead into running code in that swatch that then has access to the Pilcrow API, or to their locally-stored data. + +If a component takes HTML as an argument, then the best compromise we've found between allowing a swatch user to experiment with that HTML and protecting that user from these risks is to provide some kind of structured input. For example, a swatch for a message might instead allow test data to be entered using the same Markdown dialect real messages are entered with. + +For components where there isn't an easy way to do that, providing one or more predetermined test bodies is also a workable alternative. diff --git a/ui/routes/(swatch)/.swatch/Message/+page.svelte b/ui/routes/(swatch)/.swatch/Message/+page.svelte index 6fd9b6b..6faf3bc 100644 --- a/ui/routes/(swatch)/.swatch/Message/+page.svelte +++ b/ui/routes/(swatch)/.swatch/Message/+page.svelte @@ -2,6 +2,7 @@ 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'; @@ -11,10 +12,37 @@ // Astonishingly, `DateTime.fromISO` does not throw on invalid inputs. It generates an "Invalid // DateTime" sentinel value, instead. let at = $derived(DateTime.fromISO(atInput)); - let renderedBody = $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.

      \n' + - '

      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.

      ', + 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, `` 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(''); @@ -47,8 +75,8 @@ diff --git a/ui/routes/(swatch)/.swatch/MessageRun/+page.svelte b/ui/routes/(swatch)/.swatch/MessageRun/+page.svelte index a8c8853..34118ec 100644 --- a/ui/routes/(swatch)/.swatch/MessageRun/+page.svelte +++ b/ui/routes/(swatch)/.swatch/MessageRun/+page.svelte @@ -1,12 +1,41 @@

      MessageRun

      @@ -23,18 +52,14 @@ - -

      rendered

      - - {@html children} + {#each messages.entries() as [index, message]} + + {/each}
      -- cgit v1.2.3