From 36cb34613f364b796cacb922b9e59d5896c5889a Mon Sep 17 00:00:00 2001 From: Coracle-Bot Date: Wed, 23 Sep 2026 00:03:45 +0000 Subject: [PATCH] Put component classes in a cascade layer so utilities can override them --- AGENTS.md | 1 + src/app/components/ContentLinkBlock.svelte | 4 +- .../components/ContentLinkBlockAudio.svelte | 2 +- .../components/ContentLinkBlockImage.svelte | 2 +- src/app/components/ContentLinkInline.svelte | 4 +- src/app/components/InfoZapperError.svelte | 2 +- src/app/components/ProfileLink.svelte | 4 +- src/app/components/RoomLink.svelte | 2 +- src/app/components/Zap.svelte | 2 +- src/lib/components/base.css | 49 +++++-------------- src/lib/components/link.css | 15 +++++- src/lib/components/page.css | 9 ++++ src/lib/components/room.css | 3 +- src/lib/components/theme.css | 2 +- src/lib/components/tooltip.css | 9 ++++ 15 files changed, 57 insertions(+), 53 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 129a3c78..1f6d5e58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,6 +129,7 @@ callbacks and hot paths. - Never hard-code the app's name. The brand is a build-time `VITE_PLATFORM_*` variable, so user-facing copy interpolates `PLATFORM_NAME` from `@app/env`, and `PLATFORM_URL`, `PLATFORM_LOGO`, `PLATFORM_ABOUT` for the rest of it. Each is set by the deployment, so don't write a `"Flotilla"` fallback behind one either. - Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components - TailwindCSS styling with css components customized by theme. See lib/components for examples. +- A component class goes in `@layer components`, in its own file under `lib/components`. An unlayered rule beats a layered one whatever the specificity, so a class outside the layer can never be overridden by a utility in markup. The third-party overrides in `base.css` stay unlayered. The library defaults they beat are unlayered too. - Comments, naming, conditionals and single-use indirection are covered by the Cleanup Pass above. - Do not use `any`. If there are type errors related to `unknown`, they are likely because the upstream definition of the data is incorrect. - When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax. diff --git a/src/app/components/ContentLinkBlock.svelte b/src/app/components/ContentLinkBlock.svelte index 4958a9c8..20ff39d1 100644 --- a/src/app/components/ContentLinkBlock.svelte +++ b/src/app/components/ContentLinkBlock.svelte @@ -77,7 +77,7 @@ {#if isRoomOrRelay} - + {:else if isAudio} {:else if isVideo} @@ -130,6 +130,6 @@ {:catch} - + {/await} {/if} diff --git a/src/app/components/ContentLinkBlockAudio.svelte b/src/app/components/ContentLinkBlockAudio.svelte index f84db095..7c8e7617 100644 --- a/src/app/components/ContentLinkBlockAudio.svelte +++ b/src/app/components/ContentLinkBlockAudio.svelte @@ -46,7 +46,7 @@ {#if failed} - {displayUrl(url)} + {displayUrl(url)} {:else if source} diff --git a/src/app/components/ContentLinkBlockImage.svelte b/src/app/components/ContentLinkBlockImage.svelte index 75124d86..271b2555 100644 --- a/src/app/components/ContentLinkBlockImage.svelte +++ b/src/app/components/ContentLinkBlockImage.svelte @@ -71,7 +71,7 @@ {#if hasError} - + {displayUrl(url)} diff --git a/src/app/components/ContentLinkInline.svelte b/src/app/components/ContentLinkInline.svelte index aeab3825..da76ddc8 100644 --- a/src/app/components/ContentLinkInline.svelte +++ b/src/app/components/ContentLinkInline.svelte @@ -21,11 +21,11 @@ {displayUrl(url)} {:else} - + {/if} diff --git a/src/app/components/InfoZapperError.svelte b/src/app/components/InfoZapperError.svelte index 68f3190a..2bb65824 100644 --- a/src/app/components/InfoZapperError.svelte +++ b/src/app/components/InfoZapperError.svelte @@ -30,7 +30,7 @@ Unable to Zap

- Zapping isn't possible right now because + Zapping isn't possible right now because {#if $zapper} their zap receiver isn't correctly set up. {:else} diff --git a/src/app/components/ProfileLink.svelte b/src/app/components/ProfileLink.svelte index 56aeee9d..b9cbd66d 100644 --- a/src/app/components/ProfileLink.svelte +++ b/src/app/components/ProfileLink.svelte @@ -18,8 +18,6 @@ const openProfile = () => pushModal(ProfileDetail, {pubkey, url}) - diff --git a/src/app/components/RoomLink.svelte b/src/app/components/RoomLink.svelte index e2d8034c..9eef418a 100644 --- a/src/app/components/RoomLink.svelte +++ b/src/app/components/RoomLink.svelte @@ -16,6 +16,6 @@ const path = makeSpacePath(url, h) - + # diff --git a/src/app/components/Zap.svelte b/src/app/components/Zap.svelte index e4229501..0873625c 100644 --- a/src/app/components/Zap.svelte +++ b/src/app/components/Zap.svelte @@ -146,7 +146,7 @@ Send a Zap - To + To {#if invoice} diff --git a/src/lib/components/base.css b/src/lib/components/base.css index 8cad7f9e..c8456715 100644 --- a/src/lib/components/base.css +++ b/src/lib/components/base.css @@ -189,7 +189,9 @@ } /* ---------------------------------------------------------------------------- - Keyboard-open adjustments + Keyboard-open adjustments — unlayered on purpose. Every element carrying + .hide-on-keyboard also carries display utilities (flex, md:hidden), and a + utility beats @layer components whatever the specificity. ---------------------------------------------------------------------------- */ body.keyboard-open { @@ -246,8 +248,14 @@ body.keyboard-open .room { } /* ---------------------------------------------------------------------------- - Integrations — rewired onto the shared tokens (unlayered so they win over - third-party defaults) + Integrations — third-party class names rewired onto the shared tokens. + + These stay unlayered. Every library here ships its own stylesheet outside any + cascade layer, and an unlayered rule beats a layered one whatever the + specificity, so a layered override would lose to the default it overrides. + @welshman/editor sets `.tiptap {min-height: 0}`, which beats a layered + `.note-editor .tiptap`. Our own classes go in @layer components, in the file + that owns them. ---------------------------------------------------------------------------- */ /* tiptap editor */ @@ -388,25 +396,6 @@ body.keyboard-open .room { box-shadow: none; } -/* link-content (tiptap pill) */ -.link-content { - @apply bg-surface-more text-content max-w-full overflow-hidden px-1 text-ellipsis whitespace-nowrap; - border-radius: 3px; -} - -.link-content--wrap { - @apply whitespace-normal; - overflow-wrap: anywhere; -} - -/* welshman/content */ -.welshman-content a { - @apply text-primary cursor-pointer underline underline-offset-2; -} -.welshman-content-error a { - @apply underline; -} - /* date picker. @svelte-plugins/datepicker renders its own markup, so these class names appear nowhere in src/ — don't delete them on a grep. */ .picker { @@ -431,12 +420,6 @@ body.keyboard-open .room { } /* tippy popover */ -.tippy-target { - @apply z-tooltip pointer-events-none fixed inset-0; -} -.tippy-target > * { - @apply pointer-events-auto; -} .tippy-box { @apply rounded-3xl; box-shadow: var(--shadow-lg); @@ -471,13 +454,3 @@ emoji-picker { overflow: visible; max-width: none; } - -/* content width for fixed elements (sidebar offsets) */ -.left-content { - left: var(--sail); -} -@media (min-width: 768px) { - .left-content { - left: calc(18.5rem + var(--sail)); - } -} diff --git a/src/lib/components/link.css b/src/lib/components/link.css index 2e9ab54e..dc6c147f 100644 --- a/src/lib/components/link.css +++ b/src/lib/components/link.css @@ -1,9 +1,22 @@ @layer components { - .link { + /* .welshman-content wraps rendered note content, whose links are raw anchors */ + .link, + .welshman-content a { @apply text-primary cursor-pointer underline underline-offset-2; &:hover { @apply text-primary-bold; } } + + /* an error toast colors its own text, so the link only needs the underline */ + .welshman-content-error a { + @apply underline; + } + + /* an inline pill for a link, mention or topic inside rendered content */ + .link-content { + @apply bg-surface-more text-content max-w-full overflow-hidden px-1 text-ellipsis whitespace-nowrap; + border-radius: 3px; + } } diff --git a/src/lib/components/page.css b/src/lib/components/page.css index 752faba3..e4917c18 100644 --- a/src/lib/components/page.css +++ b/src/lib/components/page.css @@ -10,4 +10,13 @@ @apply p-2 sm:p-4; } } + + /* left edge of the content column, for fixed elements the page cannot position */ + .left-content { + left: var(--sail); + + @media (min-width: 768px) { + left: calc(18.5rem + var(--sail)); + } + } } diff --git a/src/lib/components/room.css b/src/lib/components/room.css index 22a1541b..3eefad3a 100644 --- a/src/lib/components/room.css +++ b/src/lib/components/room.css @@ -69,7 +69,8 @@ .room-pins__message { @apply text-primary-content text-sm leading-relaxed; - :where(.link-content, a, p, span, code) { + /* a pill draws its own background, so it keeps its own color */ + :where(a, p, span, code):not(.link-content) { color: inherit; } } diff --git a/src/lib/components/theme.css b/src/lib/components/theme.css index 8f1ada91..7ea5c179 100644 --- a/src/lib/components/theme.css +++ b/src/lib/components/theme.css @@ -2,7 +2,7 @@ Design-system entry point (imported once, in src/routes/+layout.svelte). tailwindcss — framework + our @theme token registration (via base.css) - base.css — theme-independent structure (components, utilities, base) + base.css — theme-independent structure (base, utilities, integrations) clay.css — clay theme token values ([data-fl-theme="clay"] + dark) flat.css — flat theme token values ([data-fl-theme="flat"] + dark) navy.css — navy theme token values ([data-fl-theme="navy"] + dark) diff --git a/src/lib/components/tooltip.css b/src/lib/components/tooltip.css index d148aec6..15940f30 100644 --- a/src/lib/components/tooltip.css +++ b/src/lib/components/tooltip.css @@ -41,4 +41,13 @@ .mobile .tip[data-tip]::before { display: none !important; } + + /* the host tippy renders its popovers into; see getTippyTarget in lib/html.ts */ + .tippy-target { + @apply z-tooltip pointer-events-none fixed inset-0; + } + + .tippy-target > * { + @apply pointer-events-auto; + } }