use-wc 1.0.0-pre.67 → 1.0.0-pre.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -772,6 +772,71 @@
772
772
  }
773
773
  ]
774
774
  },
775
+ {
776
+ "kind": "javascript-module",
777
+ "path": "src/elements/use-caret/use-caret.ts",
778
+ "declarations": [
779
+ {
780
+ "kind": "class",
781
+ "description": "`use-caret` supplies the caret icon and label layout for a consumer who wants to own the\ninvoking button itself. It renders inside a plain `<button>`\nthat carries the native `popovertarget` attribute, pointing at a `use-menu`:\n\n```html\n<button id=\"thing\" popovertarget=\"thing-menu\">\n <use-caret>Menu</use-caret>\n</button>\n<use-menu id=\"thing-menu\" aria-label=\"Menu\">\n <button role=\"menuitem\">First thing</button>\n</use-menu>\n```\n\n`use-menu` finds this button on its own via `popovertarget` and anchors to it — no\n`anchortarget` needed here; that's only for a menu with more than one invoker. See `use-menu`'s\nown docs for that case.\n\nThe button is entirely the consumer's own element — their classes, their content, their event\nhandlers. `use-caret` contributes no interactivity of its own: no internal button, no focus\ntarget, no state. It only renders the label and caret glyph, so consumers get a consistent\nvisual trigger without giving up control of the button.\n\nThe caret direction is derived rather than configured: a trigger nested inside a `use-menu`\nis understood to open a submenu and renders pointing sideways; a top-level trigger renders\npointing down.\n\nSwapping in a different caret **per instance** is a slot: put your own element in the `icon`\nslot and it replaces `part(icon-default)` entirely for that trigger.\n\nSwapping the default caret **for every trigger in the app** doesn't need touching this\ncomponent's `render()` at all — style `::part(icon-default)` from an outside stylesheet, the\nsame way any shadow DOM part is themed:\n\n```css\nuse-caret::part(icon-default) {\n mask-image: url(\"/icons/my-caret.svg\");\n}\n```\n\n(When the design system's `theme.css` is loaded, this is already wired to a token —\n`--usewc-effect-dropdown-trigger-icon-mask-image` — so redefining that custom property has\nthe same effect without writing a `::part()` rule at all.)\n\n`::part()` only reaches CSS-expressible changes (a mask, a background, a transform). For\nanything else — different markup, an icon font, logic beyond a mask — set the static `icon`\nand `nestedIcon` hooks. Every `<use-caret>` in the page picks them up; consumers keep writing\nthe same tag, no new element to register or remember to use instead. `icon` supplies a\ntop-level trigger's glyph, `nestedIcon` a submenu trigger's — set whichever apply; an unset one\nkeeps the built-in ▼/▶ for that case. Set them once, as early as possible (an app's entry\npoint, before any triggers connect) — Lit calls `render()` fresh on every update, so an\nalready-connected instance only reflects the change once something causes it to re-render:\n\n```ts\n// A bare string or TemplateResult is used as-is, on every trigger of that kind:\nUseCaret.icon = \"▾\";\nUseCaret.nestedIcon = \"▸\";\n\n// A function is called on every render instead:\nUseCaret.nestedIcon = (html) => html`<my-icon name=\"chevron-right\"></my-icon>`;\n```\n\nThe function form is called with the component's own `html` tag as its (optional) first\nargument rather than requiring the caller to import one — a page can end up with more than one\ncopy of `lit-html` in its module graph, and a `TemplateResult` built from a *different* copy\nthan the one this component renders with can silently fail to render. Taking `html` in as a\nparameter sidesteps that: whatever's returned is guaranteed to come from the same `lit-html`\nthis component already uses. A bare string skips the concern entirely.\n\nOnly need to change the icon on **some** triggers, not every one in the app? Extend the\nclass, override `render()` normally, and register the subclass under a different tag name so\nit coexists with the built-in element instead of replacing it everywhere:\n\n```ts\nclass MyCaret extends UseCaret {\n override render() {\n return html`<slot></slot><slot name=\"icon\"><my-icon name=\"chevron\"></my-icon></slot>`;\n }\n}\ncustomElements.define(\"my-caret\", MyCaret);\n```",
782
+ "name": "UseCaret",
783
+ "slots": [
784
+ {
785
+ "description": "Trigger label content.",
786
+ "name": "default"
787
+ },
788
+ {
789
+ "description": "Replaces the default caret glyph.",
790
+ "name": "icon"
791
+ }
792
+ ],
793
+ "members": [
794
+ {
795
+ "kind": "field",
796
+ "name": "icon",
797
+ "type": {
798
+ "text": "((html: HtmlTag) => TemplateResult | string) | TemplateResult | string | undefined"
799
+ },
800
+ "static": true,
801
+ "description": "Global override for a top-level trigger's caret. When set, every non-nested `<use-caret>` in\nthe page renders this inside `part(icon-default)` instead of the built-in ▼ character. A\nplain string or `TemplateResult` is used as-is; the function form is called with the\ncomponent's own `html` tag, so a returned template is guaranteed to come from the same\n`lit-html` instance this component renders with. Unset (the default) keeps the built-in ▼."
802
+ },
803
+ {
804
+ "kind": "field",
805
+ "name": "nestedIcon",
806
+ "type": {
807
+ "text": "((html: HtmlTag) => TemplateResult | string) | TemplateResult | string | undefined"
808
+ },
809
+ "static": true,
810
+ "description": "Global override for a submenu trigger's caret — the counterpart to icon for a\n`<use-caret>` nested inside a `use-menu`. Unset (the default) keeps the built-in ▶."
811
+ }
812
+ ],
813
+ "superclass": {
814
+ "name": "LitElement",
815
+ "package": "lit"
816
+ },
817
+ "tagName": "use-caret",
818
+ "customElement": true
819
+ }
820
+ ],
821
+ "exports": [
822
+ {
823
+ "kind": "js",
824
+ "name": "UseCaret",
825
+ "declaration": {
826
+ "name": "UseCaret",
827
+ "module": "src/elements/use-caret/use-caret.ts"
828
+ }
829
+ },
830
+ {
831
+ "kind": "custom-element-definition",
832
+ "name": "use-caret",
833
+ "declaration": {
834
+ "name": "UseCaret",
835
+ "module": "src/elements/use-caret/use-caret.ts"
836
+ }
837
+ }
838
+ ]
839
+ },
775
840
  {
776
841
  "kind": "javascript-module",
777
842
  "path": "src/elements/use-date-input/use-date-input.ts",
@@ -898,71 +963,6 @@
898
963
  }
899
964
  ]
900
965
  },
901
- {
902
- "kind": "javascript-module",
903
- "path": "src/elements/use-caret/use-caret.ts",
904
- "declarations": [
905
- {
906
- "kind": "class",
907
- "description": "`use-caret` supplies the caret icon and label layout for a consumer who wants to own the\ninvoking button itself. It renders inside a plain `<button>`\nthat carries the native `popovertarget` attribute, pointing at a `use-menu`:\n\n```html\n<button id=\"thing\" popovertarget=\"thing-menu\">\n <use-caret>Menu</use-caret>\n</button>\n<use-menu id=\"thing-menu\" aria-label=\"Menu\">\n <button role=\"menuitem\">First thing</button>\n</use-menu>\n```\n\n`use-menu` finds this button on its own via `popovertarget` and anchors to it — no\n`anchortarget` needed here; that's only for a menu with more than one invoker. See `use-menu`'s\nown docs for that case.\n\nThe button is entirely the consumer's own element — their classes, their content, their event\nhandlers. `use-caret` contributes no interactivity of its own: no internal button, no focus\ntarget, no state. It only renders the label and caret glyph, so consumers get a consistent\nvisual trigger without giving up control of the button.\n\nThe caret direction is derived rather than configured: a trigger nested inside a `use-menu`\nis understood to open a submenu and renders pointing sideways; a top-level trigger renders\npointing down.\n\nSwapping in a different caret **per instance** is a slot: put your own element in the `icon`\nslot and it replaces `part(icon-default)` entirely for that trigger.\n\nSwapping the default caret **for every trigger in the app** doesn't need touching this\ncomponent's `render()` at all — style `::part(icon-default)` from an outside stylesheet, the\nsame way any shadow DOM part is themed:\n\n```css\nuse-caret::part(icon-default) {\n mask-image: url(\"/icons/my-caret.svg\");\n}\n```\n\n(When the design system's `theme.css` is loaded, this is already wired to a token —\n`--usewc-effect-dropdown-trigger-icon-mask-image` — so redefining that custom property has\nthe same effect without writing a `::part()` rule at all.)\n\n`::part()` only reaches CSS-expressible changes (a mask, a background, a transform). For\nanything else — different markup, an icon font, logic beyond a mask — set the static `icon`\nand `nestedIcon` hooks. Every `<use-caret>` in the page picks them up; consumers keep writing\nthe same tag, no new element to register or remember to use instead. `icon` supplies a\ntop-level trigger's glyph, `nestedIcon` a submenu trigger's — set whichever apply; an unset one\nkeeps the built-in ▼/▶ for that case. Set them once, as early as possible (an app's entry\npoint, before any triggers connect) — Lit calls `render()` fresh on every update, so an\nalready-connected instance only reflects the change once something causes it to re-render:\n\n```ts\n// A bare string or TemplateResult is used as-is, on every trigger of that kind:\nUseCaret.icon = \"▾\";\nUseCaret.nestedIcon = \"▸\";\n\n// A function is called on every render instead:\nUseCaret.nestedIcon = (html) => html`<my-icon name=\"chevron-right\"></my-icon>`;\n```\n\nThe function form is called with the component's own `html` tag as its (optional) first\nargument rather than requiring the caller to import one — a page can end up with more than one\ncopy of `lit-html` in its module graph, and a `TemplateResult` built from a *different* copy\nthan the one this component renders with can silently fail to render. Taking `html` in as a\nparameter sidesteps that: whatever's returned is guaranteed to come from the same `lit-html`\nthis component already uses. A bare string skips the concern entirely.\n\nOnly need to change the icon on **some** triggers, not every one in the app? Extend the\nclass, override `render()` normally, and register the subclass under a different tag name so\nit coexists with the built-in element instead of replacing it everywhere:\n\n```ts\nclass MyCaret extends UseCaret {\n override render() {\n return html`<slot></slot><slot name=\"icon\"><my-icon name=\"chevron\"></my-icon></slot>`;\n }\n}\ncustomElements.define(\"my-caret\", MyCaret);\n```",
908
- "name": "UseCaret",
909
- "slots": [
910
- {
911
- "description": "Trigger label content.",
912
- "name": "default"
913
- },
914
- {
915
- "description": "Replaces the default caret glyph.",
916
- "name": "icon"
917
- }
918
- ],
919
- "members": [
920
- {
921
- "kind": "field",
922
- "name": "icon",
923
- "type": {
924
- "text": "((html: HtmlTag) => TemplateResult | string) | TemplateResult | string | undefined"
925
- },
926
- "static": true,
927
- "description": "Global override for a top-level trigger's caret. When set, every non-nested `<use-caret>` in\nthe page renders this inside `part(icon-default)` instead of the built-in ▼ character. A\nplain string or `TemplateResult` is used as-is; the function form is called with the\ncomponent's own `html` tag, so a returned template is guaranteed to come from the same\n`lit-html` instance this component renders with. Unset (the default) keeps the built-in ▼."
928
- },
929
- {
930
- "kind": "field",
931
- "name": "nestedIcon",
932
- "type": {
933
- "text": "((html: HtmlTag) => TemplateResult | string) | TemplateResult | string | undefined"
934
- },
935
- "static": true,
936
- "description": "Global override for a submenu trigger's caret — the counterpart to icon for a\n`<use-caret>` nested inside a `use-menu`. Unset (the default) keeps the built-in ▶."
937
- }
938
- ],
939
- "superclass": {
940
- "name": "LitElement",
941
- "package": "lit"
942
- },
943
- "tagName": "use-caret",
944
- "customElement": true
945
- }
946
- ],
947
- "exports": [
948
- {
949
- "kind": "js",
950
- "name": "UseCaret",
951
- "declaration": {
952
- "name": "UseCaret",
953
- "module": "src/elements/use-caret/use-caret.ts"
954
- }
955
- },
956
- {
957
- "kind": "custom-element-definition",
958
- "name": "use-caret",
959
- "declaration": {
960
- "name": "UseCaret",
961
- "module": "src/elements/use-caret/use-caret.ts"
962
- }
963
- }
964
- ]
965
- },
966
966
  {
967
967
  "kind": "javascript-module",
968
968
  "path": "src/elements/use-date-picker/use-date-picker.ts",
@@ -1996,8 +1996,8 @@
1996
1996
  `<main>` / `<footer>` regions are arranged to match:
1997
1997
 
1998
1998
  use-layout.page an app frame — ruled topbar and footer, roomy
1999
- use-layout.prose a document — stacked title/byline header, measure-capped, loose
2000
- use-layout.message a chat or comment — an avatar rail with the regions beside it, compact
1999
+ use-layout.entry a row in a list — a leading avatar rail with the regions beside it
2000
+ use-layout.message a chat or comment — `.entry`'s layout at chat density
2001
2001
  use-layout.card a panel — avatar + hgroup + actions, end-aligned ruled
2002
2002
  footer, compact, plus the chrome at end of file
2003
2003
 
@@ -2008,10 +2008,9 @@
2008
2008
 
2009
2009
  Rather than four arrangements, a variant class sets *only* a density scale on itself
2010
2010
  (`--usewc-layout-region-padding`, `-gap`, `-avatar-size`, `--usewc-layout-header-rule`,
2011
- `-footer-rule`, `--usewc-layout-footer-justify`, `--usewc-layout-measure`) and one generic set
2012
- of region rules reads them. Just three arrangements can't be said in a property — prose's
2013
- column header, message's rail, message's inline hgroup — and those follow as structural
2014
- overrides. The mechanic isn't repeated per variant either: a `use-layout.page` is a
2011
+ `-footer-rule`, `--usewc-layout-footer-justify`, `-row-gap`) and one generic set of region
2012
+ rules reads them. Only a couple of arrangements can't be said in a property — the entry and
2013
+ message rail, and message's inline hgroup — and those follow as structural overrides. The mechanic isn't repeated per variant either: a `use-layout.page` is a
2015
2014
  `use-layout`, so the base pin/grow rules above already cover it.
2016
2015
 
2017
2016
  Every selector stays compound (`use-layout.page`, never a bare `.page`) so none of these
@@ -2213,8 +2212,8 @@
2213
2212
  --usewc-layout-region-padding-block: var(--usewc-layout-page-padding-block);
2214
2213
  --usewc-layout-region-gap: var(--usewc-layout-page-gap);
2215
2214
  --usewc-layout-region-avatar-size: var(--usewc-layout-page-avatar-size);
2216
- --usewc-layout-header-rule: var(--usewc-color-region-border);
2217
- --usewc-layout-footer-rule: var(--usewc-color-region-border);
2215
+ --usewc-layout-header-rule: var(--usewc-layout-region-rule);
2216
+ --usewc-layout-footer-rule: var(--usewc-layout-region-rule);
2218
2217
  --usewc-layout-footer-justify: space-between;
2219
2218
 
2220
2219
  use-theme-escape & {
@@ -2222,12 +2221,11 @@
2222
2221
  }
2223
2222
  }
2224
2223
 
2225
- use-layout.prose {
2226
- --usewc-layout-region-padding: var(--usewc-layout-prose-padding);
2227
- --usewc-layout-region-gap: var(--usewc-layout-prose-gap);
2228
- --usewc-layout-region-avatar-size: var(--usewc-layout-prose-avatar-size);
2229
- --usewc-layout-measure: var(--usewc-layout-prose-measure);
2230
- --usewc-layout-footer-rule: var(--usewc-color-region-border);
2224
+ use-layout.entry {
2225
+ --usewc-layout-region-padding: var(--usewc-layout-entry-padding);
2226
+ --usewc-layout-region-gap: var(--usewc-layout-entry-gap);
2227
+ --usewc-layout-region-avatar-size: var(--usewc-layout-entry-avatar-size);
2228
+ --usewc-layout-region-row-gap: var(--usewc-layout-entry-gap);
2231
2229
  --usewc-layout-footer-justify: flex-start;
2232
2230
 
2233
2231
  use-theme-escape & {
@@ -2235,10 +2233,12 @@
2235
2233
  }
2236
2234
  }
2237
2235
 
2236
+ /* Chat density on top of `.entry`'s layout — tighter, since a message repeats down a feed. */
2238
2237
  use-layout.message {
2239
2238
  --usewc-layout-region-padding: var(--usewc-layout-message-padding);
2240
2239
  --usewc-layout-region-gap: var(--usewc-layout-message-gap);
2241
2240
  --usewc-layout-region-avatar-size: var(--usewc-layout-message-rail);
2241
+ --usewc-layout-region-row-gap: var(--usewc-layout-region-title-gap);
2242
2242
  --usewc-layout-footer-justify: flex-start;
2243
2243
 
2244
2244
  use-theme-escape & {
@@ -2250,7 +2250,7 @@
2250
2250
  --usewc-layout-region-padding: var(--usewc-layout-card-padding);
2251
2251
  --usewc-layout-region-gap: var(--usewc-layout-card-gap);
2252
2252
  --usewc-layout-region-avatar-size: var(--usewc-layout-card-avatar-size);
2253
- --usewc-layout-footer-rule: var(--usewc-color-region-border);
2253
+ --usewc-layout-footer-rule: var(--usewc-layout-region-rule);
2254
2254
  --usewc-layout-footer-justify: flex-end;
2255
2255
 
2256
2256
  use-theme-escape & {
@@ -2258,11 +2258,15 @@
2258
2258
  }
2259
2259
  }
2260
2260
 
2261
+ /* A rule token carries the whole border value, not just its colour, so a variant that never
2262
+ names one gets `0 solid transparent` — no width. Declaring only a colour and leaving the
2263
+ width at 1px would give every ruleless header and footer an invisible pixel of border, which
2264
+ is enough to push a 36px control into a 37px region. */
2261
2265
  /* ── Generic regions ────────────────────────────────────────────────────────────────────────
2262
- One ruleset for all four variants, reading the properties above. A rule colour left unset
2266
+ One ruleset for every variant, reading the properties above. A rule colour left unset
2263
2267
  falls back to `transparent`, so a variant opts into a hairline by naming one rather than by
2264
2268
  having a rule of its own. */
2265
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) {
2269
+ :where(use-layout:is(.page, .entry, .message, .card)) {
2266
2270
  min-block-size: 0;
2267
2271
 
2268
2272
  use-theme-escape & {
@@ -2270,7 +2274,7 @@
2270
2274
  }
2271
2275
  }
2272
2276
 
2273
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > use-layout[fill] {
2277
+ :where(use-layout:is(.page, .entry, .message, .card)) > use-layout[fill] {
2274
2278
  overflow: auto;
2275
2279
  padding: var(--usewc-layout-region-padding);
2276
2280
 
@@ -2279,9 +2283,8 @@
2279
2283
  }
2280
2284
  }
2281
2285
 
2282
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > main {
2286
+ :where(use-layout:is(.page, .entry, .message, .card)) > main {
2283
2287
  padding: var(--usewc-layout-region-padding);
2284
- max-inline-size: var(--usewc-layout-measure, none);
2285
2288
 
2286
2289
  use-theme-escape & {
2287
2290
  all: revert-layer;
@@ -2292,9 +2295,7 @@
2292
2295
  they'd otherwise stack on top of the padding — a leading `<p>`, `<ul>`, `<ol>` or heading
2293
2296
  would otherwise push itself away from an edge that's already spaced. Matched by position
2294
2297
  rather than by tag so it holds for whatever flow content lands there. */
2295
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card)
2296
- > :is(main, use-layout[fill])
2297
- > :first-child {
2298
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(main, use-layout[fill]) > :first-child {
2298
2299
  margin-block-start: 0;
2299
2300
 
2300
2301
  use-theme-escape & {
@@ -2302,9 +2303,7 @@
2302
2303
  }
2303
2304
  }
2304
2305
 
2305
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card)
2306
- > :is(main, use-layout[fill])
2307
- > :last-child {
2306
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(main, use-layout[fill]) > :last-child {
2308
2307
  margin-block-end: 0;
2309
2308
 
2310
2309
  use-theme-escape & {
@@ -2314,9 +2313,7 @@
2314
2313
 
2315
2314
  /* A header already supplies the block-start padding, so the body region drops its own and the
2316
2315
  two read as one padded column rather than a double gap under the header. */
2317
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card)
2318
- > header
2319
- + :is(main, use-layout[fill]) {
2316
+ :where(use-layout:is(.page, .entry, .message, .card)) > header + :is(main, use-layout[fill]) {
2320
2317
  padding-block-start: 0;
2321
2318
 
2322
2319
  use-theme-escape & {
@@ -2324,7 +2321,7 @@
2324
2321
  }
2325
2322
  }
2326
2323
 
2327
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > figure {
2324
+ :where(use-layout:is(.page, .entry, .message, .card)) > figure {
2328
2325
  margin: 0;
2329
2326
 
2330
2327
  use-theme-escape & {
@@ -2332,7 +2329,7 @@
2332
2329
  }
2333
2330
  }
2334
2331
 
2335
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > figure > img {
2332
+ :where(use-layout:is(.page, .entry, .message, .card)) > figure > img {
2336
2333
  display: block;
2337
2334
  inline-size: 100%;
2338
2335
  block-size: auto;
@@ -2343,28 +2340,28 @@
2343
2340
  }
2344
2341
  }
2345
2342
 
2346
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > header {
2343
+ :where(use-layout:is(.page, .entry, .message, .card)) > header {
2347
2344
  display: flex;
2348
2345
  align-items: center;
2349
2346
  justify-content: flex-start;
2350
2347
  gap: var(--usewc-layout-region-gap);
2351
2348
  padding-block: var(--usewc-layout-region-padding-block, var(--usewc-layout-region-padding));
2352
2349
  padding-inline: var(--usewc-layout-region-padding);
2353
- border-block-end: var(--usewc-layout-surface-border) solid var(--usewc-layout-header-rule, transparent);
2350
+ border-block-end: var(--usewc-layout-header-rule, 0 solid transparent);
2354
2351
 
2355
2352
  use-theme-escape & {
2356
2353
  all: revert-layer;
2357
2354
  }
2358
2355
  }
2359
2356
 
2360
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > footer {
2357
+ :where(use-layout:is(.page, .entry, .message, .card)) > footer {
2361
2358
  display: flex;
2362
2359
  align-items: center;
2363
2360
  justify-content: var(--usewc-layout-footer-justify, flex-end);
2364
2361
  gap: var(--usewc-layout-region-gap);
2365
2362
  padding-block: var(--usewc-layout-region-padding-block, var(--usewc-layout-region-padding));
2366
2363
  padding-inline: var(--usewc-layout-region-padding);
2367
- border-block-start: var(--usewc-layout-surface-border) solid var(--usewc-layout-footer-rule, transparent);
2364
+ border-block-start: var(--usewc-layout-footer-rule, 0 solid transparent);
2368
2365
 
2369
2366
  use-theme-escape & {
2370
2367
  all: revert-layer;
@@ -2375,7 +2372,7 @@
2375
2372
  since a bare `<img>` in a header is as likely to be a logo as a face, and a class would have
2376
2373
  to be remembered where the element already says it. Both are squared to the variant's avatar
2377
2374
  size; a `<figure>` centres whatever it holds, the same way the message rail does. */
2378
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > header > :is(use-avatar, figure) {
2375
+ :where(use-layout:is(.page, .entry, .message, .card)) > header > :is(use-avatar, figure) {
2379
2376
  flex: none;
2380
2377
  inline-size: var(--usewc-layout-region-avatar-size);
2381
2378
  block-size: var(--usewc-layout-region-avatar-size);
@@ -2387,7 +2384,7 @@
2387
2384
  }
2388
2385
  }
2389
2386
 
2390
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > header > figure {
2387
+ :where(use-layout:is(.page, .entry, .message, .card)) > header > figure {
2391
2388
  display: flex;
2392
2389
  align-items: center;
2393
2390
  justify-content: center;
@@ -2400,7 +2397,7 @@
2400
2397
  }
2401
2398
  }
2402
2399
 
2403
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > header > figure > img {
2400
+ :where(use-layout:is(.page, .entry, .message, .card)) > header > figure > img {
2404
2401
  inline-size: 100%;
2405
2402
  block-size: 100%;
2406
2403
  object-fit: cover;
@@ -2414,7 +2411,7 @@
2414
2411
  `<section>` in a topbar — so it's matched by descent rather than as a direct child. Only the
2415
2412
  one sitting directly in a header grows, since that's what pushes trailing actions to the
2416
2413
  far end. */
2417
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > :is(header, footer) hgroup {
2414
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(header, footer) hgroup {
2418
2415
  display: flex;
2419
2416
  flex-direction: column;
2420
2417
  gap: var(--usewc-layout-region-title-gap);
@@ -2425,7 +2422,7 @@
2425
2422
  }
2426
2423
  }
2427
2424
 
2428
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > header > hgroup {
2425
+ :where(use-layout:is(.page, .entry, .message, .card)) > header > hgroup {
2429
2426
  flex: 1;
2430
2427
 
2431
2428
  use-theme-escape & {
@@ -2433,16 +2430,35 @@
2433
2430
  }
2434
2431
  }
2435
2432
 
2433
+ /* An `hgroup`'s lines get explicit line boxes so a stacked title and subtitle add up to a
2434
+ predictable height beside a fixed-size avatar, and everything after the first steps down a
2435
+ size — a timestamp, a byline, a location is secondary to the title it hangs off. */
2436
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(header, footer) hgroup > * {
2437
+ line-height: var(--usewc-layout-region-title-line-height);
2438
+
2439
+ use-theme-escape & {
2440
+ all: revert-layer;
2441
+ }
2442
+ }
2443
+
2444
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(header, footer) hgroup > * + * {
2445
+ font-size: var(--usewc-font-region-subtitle-size);
2446
+ line-height: var(--usewc-layout-region-subtitle-line-height);
2447
+ color: var(--usewc-color-document-text-muted);
2448
+
2449
+ use-theme-escape & {
2450
+ all: revert-layer;
2451
+ }
2452
+ }
2453
+
2436
2454
  /* A header and footer are flex containers and an `hgroup` is a flex column, so `gap` is the
2437
2455
  spacing — a default block margin stacks on top of it and blows the region apart. Headings are
2438
2456
  matched at any depth, since a topbar routinely nests them a `<section>` and an `<hgroup>`
2439
2457
  down; `<p>` and friends only need it where they sit directly in the flex flow. Body copy in
2440
2458
  `<main>` keeps its margins, which is the whole point of a paragraph there. */
2441
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > :is(header, footer) > *,
2442
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > :is(header, footer) hgroup > *,
2443
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card)
2444
- > :is(header, footer)
2445
- :is(h1, h2, h3, h4, h5, h6) {
2459
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(header, footer) > *,
2460
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(header, footer) hgroup > *,
2461
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(header, footer) :is(h1, h2, h3, h4, h5, h6) {
2446
2462
  margin-block: 0;
2447
2463
 
2448
2464
  use-theme-escape & {
@@ -2453,7 +2469,7 @@
2453
2469
  /* A `<section>` inside a header or footer is one cluster of related controls. Two or more of
2454
2470
  them means the region has distinct ends, so it splits them apart regardless of the variant's
2455
2471
  default justification. */
2456
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > :is(header, footer) > section {
2472
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(header, footer) > section {
2457
2473
  display: flex;
2458
2474
  align-items: center;
2459
2475
  gap: var(--usewc-layout-region-gap);
@@ -2463,8 +2479,7 @@
2463
2479
  }
2464
2480
  }
2465
2481
 
2466
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card)
2467
- > :is(header, footer):has(> section ~ section) {
2482
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(header, footer):has(> section ~ section) {
2468
2483
  justify-content: space-between;
2469
2484
 
2470
2485
  use-theme-escape & {
@@ -2473,40 +2488,47 @@
2473
2488
  }
2474
2489
 
2475
2490
  /* ── Structural overrides ───────────────────────────────────────────────────────────────────
2476
- The three arrangements a density property can't express. Same `:where()` weight as the
2477
- generic rules, winning on source order. */
2478
-
2479
- /* Prose stacks its header instead of spreading it across a row: an eyebrow, then a title free
2480
- to wrap, then a byline. It shares `main`'s measure so the two align down the left edge,
2481
- while a `<figure>` above stays full-bleed to the container. A growing `hgroup` would stretch
2482
- vertically in a column, so it sizes to content here. */
2483
- :where(use-layout.prose) > header {
2484
- flex-direction: column;
2485
- align-items: start;
2486
- max-inline-size: var(--usewc-layout-measure);
2491
+ The arrangements a density property can't express. Same `:where()` weight as the generic
2492
+ rules, winning on source order. */
2493
+
2494
+ /* `.entry` is the one variant that isn't a column: a leading avatar rail with every other
2495
+ region stacked beside it — a comment, a notification, a search result, a row in a people
2496
+ list. `.message` is the same layout at chat density, so both are matched here.
2497
+
2498
+ `align-self: start` on the rail is what keeps it honest. Left to stretch, a rail spanning
2499
+ every row forces the rows to add up to at least its own height, which pads out a short
2500
+ header and drops an awkward gap between the name and the body. Held at the start it sizes
2501
+ to itself, the rows keep their content heights, and spanning still guarantees the entry is
2502
+ never shorter than the rail.
2503
+
2504
+ Each variant sets its own row gap. `.entry` spaces its regions evenly, since a title, a body
2505
+ and a row of actions are three separate things. `.message` keeps them tight — a name and the
2506
+ message under it read as one unit — and buys the footer's separation back with a margin, so
2507
+ reactions still sit apart from the text. The rail's column is reserved by `grid-template-columns` whether or not
2508
+ a rail is present, so a grouped entry that omits it still lines its body up with the rest. */
2509
+ use-layout:is(.entry, .message) {
2510
+ display: grid;
2511
+ grid-template-columns: var(--usewc-layout-region-avatar-size) 1fr;
2512
+ column-gap: var(--usewc-layout-region-gap);
2513
+ row-gap: var(--usewc-layout-region-row-gap);
2514
+ padding: var(--usewc-layout-region-padding);
2487
2515
 
2488
2516
  use-theme-escape & {
2489
2517
  all: revert-layer;
2490
2518
  }
2491
2519
  }
2492
2520
 
2493
- :where(use-layout.prose) > header > hgroup {
2494
- flex: 0 1 auto;
2521
+ :where(use-layout:is(.entry, .message)) > :is(header, main, footer) {
2522
+ grid-column: 2;
2523
+ padding: 0;
2495
2524
 
2496
2525
  use-theme-escape & {
2497
2526
  all: revert-layer;
2498
2527
  }
2499
2528
  }
2500
2529
 
2501
- /* A message is the one variant that isn't a column: a fixed avatar rail, then header, body and
2502
- reactions stacked beside it. It's a grid rather than nested flex so the rail keeps its width
2503
- when a grouped message omits its header and leaves the rail empty — bodies stay aligned down
2504
- the feed either way. The container owns the padding, so the regions drop theirs. */
2505
- use-layout.message {
2506
- display: grid;
2507
- grid-template-columns: var(--usewc-layout-message-rail) 1fr;
2508
- gap: var(--usewc-layout-region-gap);
2509
- padding: var(--usewc-layout-region-padding);
2530
+ :where(use-layout.message) > footer {
2531
+ margin-block-start: var(--usewc-layout-region-gap);
2510
2532
 
2511
2533
  use-theme-escape & {
2512
2534
  all: revert-layer;
@@ -2517,11 +2539,12 @@
2517
2539
  brings its own initials fallback and shape — and a bare `<figure>` works too, for a date
2518
2540
  badge or an icon that isn't a person. A `<figure>` centres whatever it's given rather than
2519
2541
  assuming an `<img>`; a photo inside one fills the square and crops. */
2520
- :where(use-layout.message) > :is(use-avatar, figure) {
2542
+ :where(use-layout:is(.entry, .message)) > :is(use-avatar, figure) {
2521
2543
  grid-column: 1;
2522
- grid-row: 1 / -1;
2523
- inline-size: var(--usewc-layout-message-rail);
2524
- block-size: var(--usewc-layout-message-rail);
2544
+ grid-row: 1;
2545
+ align-self: start;
2546
+ inline-size: var(--usewc-layout-region-avatar-size);
2547
+ block-size: var(--usewc-layout-region-avatar-size);
2525
2548
  overflow: hidden;
2526
2549
  border-radius: var(--usewc-effect-surface-avatar-border-radius);
2527
2550
 
@@ -2530,11 +2553,26 @@
2530
2553
  }
2531
2554
  }
2532
2555
 
2533
- :where(use-layout.message) > figure {
2556
+ /* The rail shares a centre with the title block rather than being pinned to the top of it, so
2557
+ a stacked title over a subtitle and a single line each sit level with the avatar without
2558
+ either being measured against the other. The rail's own row grows to hold it, so it can
2559
+ never overhang, and everything after the header falls below both.
2560
+
2561
+ With no header the rail has no title to centre against — the body starts at the top of the
2562
+ column and the rail starts with it. */
2563
+ :where(use-layout:is(.entry, .message):has(> header)) > :is(use-avatar, figure) {
2564
+ align-self: center;
2565
+
2566
+ use-theme-escape & {
2567
+ all: revert-layer;
2568
+ }
2569
+ }
2570
+
2571
+ :where(use-layout:is(.entry, .message)) > figure {
2534
2572
  display: flex;
2535
2573
  align-items: center;
2536
2574
  justify-content: center;
2537
- font-size: calc(var(--usewc-layout-message-rail) * 0.4);
2575
+ font-size: calc(var(--usewc-layout-region-avatar-size) * 0.4);
2538
2576
  line-height: 1;
2539
2577
 
2540
2578
  use-theme-escape & {
@@ -2542,7 +2580,7 @@
2542
2580
  }
2543
2581
  }
2544
2582
 
2545
- :where(use-layout.message) > figure > img {
2583
+ :where(use-layout:is(.entry, .message)) > figure > img {
2546
2584
  inline-size: 100%;
2547
2585
  block-size: 100%;
2548
2586
  object-fit: cover;
@@ -2554,7 +2592,7 @@
2554
2592
 
2555
2593
  /* A `<figure>` inside the body is an attachment, not the rail — it keeps the flow but sheds the
2556
2594
  UA margin so the region's own spacing is the only spacing. */
2557
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card) > :is(main, use-layout[fill]) figure {
2595
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(main, use-layout[fill]) figure {
2558
2596
  margin-inline: 0;
2559
2597
 
2560
2598
  use-theme-escape & {
@@ -2562,10 +2600,7 @@
2562
2600
  }
2563
2601
  }
2564
2602
 
2565
- :where(use-layout.page, use-layout.prose, use-layout.message, use-layout.card)
2566
- > :is(main, use-layout[fill])
2567
- figure
2568
- > img {
2603
+ :where(use-layout:is(.page, .entry, .message, .card)) > :is(main, use-layout[fill]) figure > img {
2569
2604
  display: block;
2570
2605
  inline-size: 100%;
2571
2606
  block-size: auto;
@@ -2575,16 +2610,6 @@
2575
2610
  }
2576
2611
  }
2577
2612
 
2578
- :where(use-layout.message) > :is(header, main, footer) {
2579
- grid-column: 2;
2580
- padding: 0;
2581
- max-inline-size: none;
2582
-
2583
- use-theme-escape & {
2584
- all: revert-layer;
2585
- }
2586
- }
2587
-
2588
2613
  /* Name and timestamp sit on one line here rather than stacking, and the line never wraps —
2589
2614
  trailing actions stay put as the name grows. */
2590
2615
  :where(use-layout.message) > header > hgroup {
@@ -2630,3 +2655,236 @@
2630
2655
  }
2631
2656
  }
2632
2657
 
2658
+ /* Prose — `use-prose` is a theme-only tag (no custom element registration, styled by tag name
2659
+ like `use-layout`/`use-field`), giving a block of authored text a typographic scale: a heading
2660
+ ramp, the rhythm between blocks, list indentation, quotes, code blocks, rules and tables.
2661
+
2662
+ This is the only place the system sets any of that. Outside a `use-prose`, `h1`-`h6`, `p` and
2663
+ `ul` keep the browser's own defaults, so adopting the design system never silently restyles an
2664
+ app's existing copy — you opt a region of text in.
2665
+
2666
+ <use-prose>
2667
+ <article>
2668
+ <h1>On keeping a field notebook</h1>
2669
+ <p>The point of a field notebook is not to be complete…</p>
2670
+ </article>
2671
+ </use-prose>
2672
+
2673
+ Everything is matched by descent, not as direct children, so `<article>`, `<section>`,
2674
+ `<div>`, `<li>` and `<blockquote>` are transparent at any depth — markdown output nests freely
2675
+ and still reads as prose.
2676
+
2677
+ The rhythm is the space *between* blocks (`+` on siblings) rather than a margin on each, so a
2678
+ heading's spacing above it is stated once instead of emerging from two margins colliding. That
2679
+ also means the first block in any container has no leading margin, and prose drops into a
2680
+ layout region without fighting its padding.
2681
+
2682
+ Prose reaches every descendant, which includes the slotted content of a nested component — a
2683
+ `<p>` inside a `use-layout.card` picks up prose's margins today. That is deliberate for a first
2684
+ pass: the alternative was a hand-maintained list of every `use-` tag to revert inside, which is
2685
+ a lot of upkeep for a problem nobody has hit yet. Wrap the exception in `use-theme-escape` if
2686
+ it bites, and revisit once there's a real case to design against.
2687
+
2688
+ Every rule here is held at the weight of `use-prose` plus one type selector, with `:where()`
2689
+ absorbing the rest, so an author's own selector out-ranks prose without a fight — and a
2690
+ boundary rule can be reintroduced later at equal specificity if it's wanted. */
2691
+ @layer use-theme.prose {
2692
+ use-prose {
2693
+ display: block;
2694
+ max-inline-size: var(--usewc-layout-prose-measure);
2695
+ line-height: var(--usewc-layout-prose-line-height);
2696
+
2697
+ use-theme-escape & {
2698
+ all: revert-layer;
2699
+ }
2700
+ }
2701
+
2702
+ /* The blocks prose owns. Their own margins are cleared so the rhythm below is the only source
2703
+ of spacing between them. */
2704
+ use-prose :is(h1, h2, h3, h4, h5, h6, p, ul, ol, dl, blockquote, pre, table, figure, hr) {
2705
+ margin-block: 0;
2706
+
2707
+ use-theme-escape & {
2708
+ all: revert-layer;
2709
+ }
2710
+ }
2711
+
2712
+ use-prose
2713
+ :where(h1, h2, h3, h4, h5, h6, p, ul, ol, dl, blockquote, pre, table, figure, hr)
2714
+ + :is(h1, h2, h3, h4, h5, h6, p, ul, ol, dl, blockquote, pre, table, figure, hr) {
2715
+ margin-block-start: var(--usewc-layout-prose-flow-gap);
2716
+
2717
+ use-theme-escape & {
2718
+ all: revert-layer;
2719
+ }
2720
+ }
2721
+
2722
+ /* A heading opens a new section, so it takes more room above it than a paragraph does — but
2723
+ only when something precedes it. */
2724
+ use-prose
2725
+ :where(h1, h2, h3, h4, h5, h6, p, ul, ol, dl, blockquote, pre, table, figure, hr)
2726
+ + :is(h1, h2, h3, h4, h5, h6) {
2727
+ margin-block-start: var(--usewc-layout-prose-heading-gap);
2728
+
2729
+ use-theme-escape & {
2730
+ all: revert-layer;
2731
+ }
2732
+ }
2733
+
2734
+ use-prose :is(h1, h2, h3, h4, h5, h6) {
2735
+ font-weight: var(--usewc-font-prose-heading-weight);
2736
+
2737
+ use-theme-escape & {
2738
+ all: revert-layer;
2739
+ }
2740
+ }
2741
+
2742
+ use-prose h1 {
2743
+ font-size: var(--usewc-font-prose-h1-size);
2744
+ line-height: var(--usewc-layout-prose-h1-line-height);
2745
+
2746
+ use-theme-escape & {
2747
+ all: revert-layer;
2748
+ }
2749
+ }
2750
+
2751
+ use-prose h2 {
2752
+ font-size: var(--usewc-font-prose-h2-size);
2753
+ line-height: var(--usewc-layout-prose-h2-line-height);
2754
+
2755
+ use-theme-escape & {
2756
+ all: revert-layer;
2757
+ }
2758
+ }
2759
+
2760
+ use-prose h3 {
2761
+ font-size: var(--usewc-font-prose-h3-size);
2762
+ line-height: var(--usewc-layout-prose-h3-line-height);
2763
+
2764
+ use-theme-escape & {
2765
+ all: revert-layer;
2766
+ }
2767
+ }
2768
+
2769
+ use-prose h4 {
2770
+ font-size: var(--usewc-font-prose-h4-size);
2771
+ line-height: var(--usewc-layout-prose-h4-line-height);
2772
+
2773
+ use-theme-escape & {
2774
+ all: revert-layer;
2775
+ }
2776
+ }
2777
+
2778
+ use-prose h5 {
2779
+ font-size: var(--usewc-font-prose-h5-size);
2780
+ line-height: var(--usewc-layout-prose-h5-line-height);
2781
+
2782
+ use-theme-escape & {
2783
+ all: revert-layer;
2784
+ }
2785
+ }
2786
+
2787
+ use-prose h6 {
2788
+ font-size: var(--usewc-font-prose-h6-size);
2789
+ line-height: var(--usewc-layout-prose-h6-line-height);
2790
+
2791
+ use-theme-escape & {
2792
+ all: revert-layer;
2793
+ }
2794
+ }
2795
+
2796
+ use-prose :is(ul, ol) {
2797
+ padding-inline-start: var(--usewc-layout-prose-list-indent);
2798
+
2799
+ use-theme-escape & {
2800
+ all: revert-layer;
2801
+ }
2802
+ }
2803
+
2804
+ use-prose blockquote {
2805
+ border-inline-start: var(--usewc-layout-prose-quote-border) solid var(--usewc-color-prose-quote-border);
2806
+ padding-inline-start: var(--usewc-layout-prose-quote-gap);
2807
+ color: var(--usewc-color-prose-quote-text);
2808
+
2809
+ use-theme-escape & {
2810
+ all: revert-layer;
2811
+ }
2812
+ }
2813
+
2814
+ use-prose pre {
2815
+ padding: var(--usewc-layout-prose-block-padding);
2816
+ overflow-x: auto;
2817
+ background: var(--usewc-color-prose-code-background);
2818
+ border: var(--usewc-layout-prose-rule-size) solid var(--usewc-color-prose-code-border);
2819
+ border-radius: var(--usewc-effect-prose-code-border-radius);
2820
+
2821
+ use-theme-escape & {
2822
+ all: revert-layer;
2823
+ }
2824
+ }
2825
+
2826
+ /* `code.css` paints an inline pill — a tinted, padded, rounded inline-block. Inside a `<pre>`
2827
+ that reads as a badge floating in a slab, so the pill is undone and the code fills the block
2828
+ instead. The monospace family and size stay; only the inline chrome goes. */
2829
+ use-prose :where(pre) > code {
2830
+ display: block;
2831
+ padding: 0;
2832
+ border: none;
2833
+ border-radius: 0;
2834
+ background: none;
2835
+ color: inherit;
2836
+
2837
+ use-theme-escape & {
2838
+ all: revert-layer;
2839
+ }
2840
+ }
2841
+
2842
+ use-prose hr {
2843
+ border: none;
2844
+ border-block-start: var(--usewc-layout-prose-rule-size) solid var(--usewc-color-prose-rule);
2845
+
2846
+ use-theme-escape & {
2847
+ all: revert-layer;
2848
+ }
2849
+ }
2850
+
2851
+ use-prose table {
2852
+ inline-size: 100%;
2853
+ border-collapse: collapse;
2854
+
2855
+ use-theme-escape & {
2856
+ all: revert-layer;
2857
+ }
2858
+ }
2859
+
2860
+ use-prose :is(th, td) {
2861
+ padding-block: var(--usewc-layout-prose-cell-padding-block);
2862
+ padding-inline: var(--usewc-layout-prose-cell-padding-inline);
2863
+ border-block-end: var(--usewc-layout-prose-rule-size) solid var(--usewc-color-prose-cell-border);
2864
+ text-align: start;
2865
+
2866
+ use-theme-escape & {
2867
+ all: revert-layer;
2868
+ }
2869
+ }
2870
+
2871
+ use-prose figcaption {
2872
+ font-size: var(--usewc-font-prose-caption-size);
2873
+ color: var(--usewc-color-prose-caption-text);
2874
+
2875
+ use-theme-escape & {
2876
+ all: revert-layer;
2877
+ }
2878
+ }
2879
+
2880
+ use-prose :where(figure) > :is(img, picture, video) {
2881
+ display: block;
2882
+ inline-size: 100%;
2883
+ block-size: auto;
2884
+
2885
+ use-theme-escape & {
2886
+ all: revert-layer;
2887
+ }
2888
+ }
2889
+ }
2890
+
package/dist/tokens.css CHANGED
@@ -52,6 +52,7 @@
52
52
  --usewc-font-family-monospace: monospace;
53
53
 
54
54
  /** Line height */
55
+ --usewc-line-height-14: 0.875rem;
55
56
  --usewc-line-height-18: 1.125rem;
56
57
  --usewc-line-height-20: 1.25rem;
57
58
  --usewc-line-height-24: 1.5rem;
@@ -657,34 +658,100 @@
657
658
  --usewc-layout-flex-gap-xlarge: var(--usewc-space-24);
658
659
  --usewc-layout-flex-gap-super: var(--usewc-space-32);
659
660
 
661
+ /* Prose — the typographic scale for a block of authored text (see `theme/prose.css`). It is
662
+ the only place this system sets heading sizes, paragraph rhythm or list indentation: outside
663
+ a `use-prose`, flow content keeps the browser's own defaults, so adopting the design system
664
+ never silently restyles an app's existing copy.
665
+
666
+ The rhythm is expressed as the space *between* blocks rather than a margin on each, so the
667
+ scale reads as one ramp instead of a set of colliding margins. */
668
+ --usewc-layout-prose-measure: 68ch;
669
+ --usewc-layout-prose-flow-gap: var(--usewc-space-16);
670
+ --usewc-layout-prose-heading-gap: var(--usewc-space-32);
671
+ --usewc-layout-prose-list-indent: var(--usewc-space-24);
672
+ --usewc-layout-prose-quote-border: var(--usewc-space-2);
673
+ --usewc-layout-prose-quote-gap: var(--usewc-space-16);
674
+ --usewc-layout-prose-rule-size: var(--usewc-space-1);
675
+ --usewc-layout-prose-block-padding: var(--usewc-space-16);
676
+ --usewc-layout-prose-cell-padding-block: var(--usewc-space-8);
677
+ --usewc-layout-prose-cell-padding-inline: var(--usewc-space-12);
678
+
679
+ --usewc-font-prose-h1-size: var(--usewc-font-size-24);
680
+ --usewc-font-prose-h2-size: var(--usewc-font-size-20);
681
+ --usewc-font-prose-h3-size: var(--usewc-font-size-18);
682
+ --usewc-font-prose-h4-size: var(--usewc-font-size-16);
683
+ --usewc-font-prose-h5-size: var(--usewc-font-size-14);
684
+ --usewc-font-prose-h6-size: var(--usewc-font-size-14);
685
+ --usewc-font-prose-heading-weight: 600;
686
+ --usewc-font-prose-caption-size: var(--usewc-font-size-14);
687
+
688
+ --usewc-layout-prose-h1-line-height: var(--usewc-line-height-32);
689
+ --usewc-layout-prose-h2-line-height: var(--usewc-line-height-28);
690
+ --usewc-layout-prose-h3-line-height: var(--usewc-line-height-24);
691
+ --usewc-layout-prose-h4-line-height: var(--usewc-line-height-24);
692
+ --usewc-layout-prose-h5-line-height: var(--usewc-line-height-20);
693
+ --usewc-layout-prose-h6-line-height: var(--usewc-line-height-20);
694
+ /* The body line box every flow block inherits — paragraphs, list items, table cells, quotes.
695
+ Headings declare their own below, so this is the one value that sets reading rhythm. */
696
+ --usewc-layout-prose-line-height: var(--usewc-line-height-24);
697
+
698
+ --usewc-color-prose-quote-border: var(--usewc-color-surface-border);
699
+ --usewc-color-prose-quote-text: var(--usewc-color-document-text-muted);
700
+ --usewc-color-prose-caption-text: var(--usewc-color-document-text-muted);
701
+ --usewc-color-prose-rule: var(--usewc-color-surface-border);
702
+ --usewc-color-prose-cell-border: var(--usewc-color-surface-border);
703
+ /* A code block is a slab, not the inline pill `code.css` paints — it gets a neutral ground of
704
+ its own so the page's own background can sit behind it. */
705
+ --usewc-color-prose-code-background: var(--usewc-color-gray-50);
706
+ --usewc-color-prose-code-border: var(--usewc-color-surface-border);
707
+ --usewc-effect-prose-code-border-radius: var(--usewc-effect-border-radius-6);
708
+
660
709
  /* Layout regions — the density scale each `use-layout` variant sets on itself, which the one
661
710
  generic set of header / main / footer rules in `theme/layout.css` then reads. The four
662
711
  variants are deliberately far apart: a document and a card in a feed can't share one
663
712
  padding step and still read as themselves, so each gets its own rather than overriding a
664
713
  shared base. The hairline colour is shared, since a rule is a rule wherever it lands. */
665
714
  --usewc-color-region-border: var(--usewc-color-surface-border);
715
+ --usewc-layout-region-rule: var(--usewc-layout-surface-border) solid var(--usewc-color-region-border);
666
716
  --usewc-layout-region-title-gap: var(--usewc-space-2);
667
-
668
- --usewc-layout-prose-padding: var(--usewc-space-32);
669
- --usewc-layout-prose-gap: var(--usewc-space-16);
670
- --usewc-layout-prose-measure: 68ch;
671
- --usewc-layout-prose-avatar-size: var(--usewc-space-24);
717
+ /* A title group sits beside a fixed-size avatar, so its lines are given explicit line boxes
718
+ rather than the document's ratio — a stacked title and subtitle then add up to a known
719
+ height instead of drifting with the inherited leading. The subtitle steps down a size, since
720
+ a timestamp or a byline is secondary to the thing it belongs to. */
721
+ --usewc-layout-region-title-line-height: var(--usewc-line-height-20);
722
+ --usewc-font-region-subtitle-size: var(--usewc-font-size-14);
723
+ --usewc-layout-region-subtitle-line-height: var(--usewc-line-height-14);
724
+ /* A stacked title and subtitle add up to this, and it's what an avatar beside them is sized
725
+ to — so the two always agree, and both land on a base button's 36px height rather than
726
+ being pinned there by a magic number. */
727
+ --usewc-layout-region-title-block-size: calc(
728
+ var(--usewc-layout-region-title-line-height) + var(--usewc-layout-region-title-gap) +
729
+ var(--usewc-layout-region-subtitle-line-height)
730
+ );
672
731
 
673
732
  --usewc-layout-page-padding: var(--usewc-space-20);
674
733
  --usewc-layout-page-padding-block: var(--usewc-space-12);
675
734
  --usewc-layout-page-gap: var(--usewc-space-12);
676
- --usewc-layout-page-avatar-size: var(--usewc-space-32);
735
+ --usewc-layout-page-avatar-size: var(--usewc-layout-region-title-block-size);
677
736
 
678
737
  --usewc-layout-card-padding: var(--usewc-space-12);
679
738
  --usewc-layout-card-gap: var(--usewc-space-8);
680
739
  --usewc-layout-card-avatar-size: var(--usewc-space-24);
681
740
 
682
- /* The message rail width and its avatar are one value on purpose: a grouped message omits its
683
- header and leaves the rail empty, and the bodies only stay aligned down a feed if the empty
684
- rail is exactly as wide as the avatar it replaces. */
741
+ /* Avatar sizes match a base button's 36px height, so a likeness and the controls beside it in
742
+ a header line up on both edges rather than one floating inside the other.
743
+
744
+ An entry's avatar size doubles as the width of the rail its regions sit beside — a grouped
745
+ entry omits the avatar entirely, and the bodies only stay aligned down a list if the
746
+ reserved strip is exactly as wide as the avatar it replaces. `.message` is the same layout
747
+ at chat density, since it repeats far more often in a feed. */
748
+ --usewc-layout-entry-padding: var(--usewc-space-16);
749
+ --usewc-layout-entry-gap: var(--usewc-space-12);
750
+ --usewc-layout-entry-avatar-size: var(--usewc-layout-region-title-block-size);
751
+
685
752
  --usewc-layout-message-padding: var(--usewc-space-12);
686
753
  --usewc-layout-message-gap: var(--usewc-space-12);
687
- --usewc-layout-message-rail: var(--usewc-space-32);
754
+ --usewc-layout-message-rail: var(--usewc-layout-region-title-block-size);
688
755
 
689
756
  /* Surface — the visible chrome for a surface host (background/border/radius/shadow), the
690
757
  backdrop behind a modal `<dialog>` (non-modal `[popover]` keeps its transparent UA
@@ -712,6 +779,7 @@
712
779
  /* The primary/danger fill stays the same shade in both themes — only the
713
780
  focus ring lightens in the dark. */
714
781
  --usewc-color-outline: var(--usewc-color-blue-400);
782
+ --usewc-color-prose-code-background: var(--usewc-color-gray-900);
715
783
  --usewc-color-input-background-static: oklch(from var(--usewc-color-gray-100) l c h / 0.025);
716
784
  --usewc-color-input-border-static: var(--usewc-color-gray-600);
717
785
  --usewc-color-input-text-static: var(--usewc-color-gray-200);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-wc",
3
- "version": "1.0.0-pre.67",
3
+ "version": "1.0.0-pre.68",
4
4
  "private": false,
5
5
  "homepage": "https://usewc.com",
6
6
  "bugs": "https://github.com/dang-rabbits/use-wc/issues",
@@ -69,6 +69,12 @@
69
69
  "attributes": [{ "name": "date", "values": [] }],
70
70
  "references": []
71
71
  },
72
+ {
73
+ "name": "use-caret",
74
+ "description": "`use-caret` supplies the caret icon and label layout for a consumer who wants to own the\ninvoking button itself. It renders inside a plain `<button>`\nthat carries the native `popovertarget` attribute, pointing at a `use-menu`:\n\n```html\n<button id=\"thing\" popovertarget=\"thing-menu\">\n <use-caret>Menu</use-caret>\n</button>\n<use-menu id=\"thing-menu\" aria-label=\"Menu\">\n <button role=\"menuitem\">First thing</button>\n</use-menu>\n```\n\n`use-menu` finds this button on its own via `popovertarget` and anchors to it — no\n`anchortarget` needed here; that's only for a menu with more than one invoker. See `use-menu`'s\nown docs for that case.\n\nThe button is entirely the consumer's own element — their classes, their content, their event\nhandlers. `use-caret` contributes no interactivity of its own: no internal button, no focus\ntarget, no state. It only renders the label and caret glyph, so consumers get a consistent\nvisual trigger without giving up control of the button.\n\nThe caret direction is derived rather than configured: a trigger nested inside a `use-menu`\nis understood to open a submenu and renders pointing sideways; a top-level trigger renders\npointing down.\n\nSwapping in a different caret **per instance** is a slot: put your own element in the `icon`\nslot and it replaces `part(icon-default)` entirely for that trigger.\n\nSwapping the default caret **for every trigger in the app** doesn't need touching this\ncomponent's `render()` at all — style `::part(icon-default)` from an outside stylesheet, the\nsame way any shadow DOM part is themed:\n\n```css\nuse-caret::part(icon-default) {\n mask-image: url(\"/icons/my-caret.svg\");\n}\n```\n\n(When the design system's `theme.css` is loaded, this is already wired to a token —\n`--usewc-effect-dropdown-trigger-icon-mask-image` — so redefining that custom property has\nthe same effect without writing a `::part()` rule at all.)\n\n`::part()` only reaches CSS-expressible changes (a mask, a background, a transform). For\nanything else — different markup, an icon font, logic beyond a mask — set the static `icon`\nand `nestedIcon` hooks. Every `<use-caret>` in the page picks them up; consumers keep writing\nthe same tag, no new element to register or remember to use instead. `icon` supplies a\ntop-level trigger's glyph, `nestedIcon` a submenu trigger's — set whichever apply; an unset one\nkeeps the built-in ▼/▶ for that case. Set them once, as early as possible (an app's entry\npoint, before any triggers connect) — Lit calls `render()` fresh on every update, so an\nalready-connected instance only reflects the change once something causes it to re-render:\n\n```ts\n// A bare string or TemplateResult is used as-is, on every trigger of that kind:\nUseCaret.icon = \"▾\";\nUseCaret.nestedIcon = \"▸\";\n\n// A function is called on every render instead:\nUseCaret.nestedIcon = (html) => html`<my-icon name=\"chevron-right\"></my-icon>`;\n```\n\nThe function form is called with the component's own `html` tag as its (optional) first\nargument rather than requiring the caller to import one — a page can end up with more than one\ncopy of `lit-html` in its module graph, and a `TemplateResult` built from a *different* copy\nthan the one this component renders with can silently fail to render. Taking `html` in as a\nparameter sidesteps that: whatever's returned is guaranteed to come from the same `lit-html`\nthis component already uses. A bare string skips the concern entirely.\n\nOnly need to change the icon on **some** triggers, not every one in the app? Extend the\nclass, override `render()` normally, and register the subclass under a different tag name so\nit coexists with the built-in element instead of replacing it everywhere:\n\n```ts\nclass MyCaret extends UseCaret {\n override render() {\n return html`<slot></slot><slot name=\"icon\"><my-icon name=\"chevron\"></my-icon></slot>`;\n }\n}\ncustomElements.define(\"my-caret\", MyCaret);\n```\n---\n\n\n### **Slots:**\n - **default** - Trigger label content.\n- **icon** - Replaces the default caret glyph.",
75
+ "attributes": [],
76
+ "references": []
77
+ },
72
78
  {
73
79
  "name": "use-date-input",
74
80
  "description": "Displays a date picker using segmented inputs for year, month, and day.\n\nUses browser locale for formatting and ARIA labels.\n---\n",
@@ -80,12 +86,6 @@
80
86
  ],
81
87
  "references": []
82
88
  },
83
- {
84
- "name": "use-caret",
85
- "description": "`use-caret` supplies the caret icon and label layout for a consumer who wants to own the\ninvoking button itself. It renders inside a plain `<button>`\nthat carries the native `popovertarget` attribute, pointing at a `use-menu`:\n\n```html\n<button id=\"thing\" popovertarget=\"thing-menu\">\n <use-caret>Menu</use-caret>\n</button>\n<use-menu id=\"thing-menu\" aria-label=\"Menu\">\n <button role=\"menuitem\">First thing</button>\n</use-menu>\n```\n\n`use-menu` finds this button on its own via `popovertarget` and anchors to it — no\n`anchortarget` needed here; that's only for a menu with more than one invoker. See `use-menu`'s\nown docs for that case.\n\nThe button is entirely the consumer's own element — their classes, their content, their event\nhandlers. `use-caret` contributes no interactivity of its own: no internal button, no focus\ntarget, no state. It only renders the label and caret glyph, so consumers get a consistent\nvisual trigger without giving up control of the button.\n\nThe caret direction is derived rather than configured: a trigger nested inside a `use-menu`\nis understood to open a submenu and renders pointing sideways; a top-level trigger renders\npointing down.\n\nSwapping in a different caret **per instance** is a slot: put your own element in the `icon`\nslot and it replaces `part(icon-default)` entirely for that trigger.\n\nSwapping the default caret **for every trigger in the app** doesn't need touching this\ncomponent's `render()` at all — style `::part(icon-default)` from an outside stylesheet, the\nsame way any shadow DOM part is themed:\n\n```css\nuse-caret::part(icon-default) {\n mask-image: url(\"/icons/my-caret.svg\");\n}\n```\n\n(When the design system's `theme.css` is loaded, this is already wired to a token —\n`--usewc-effect-dropdown-trigger-icon-mask-image` — so redefining that custom property has\nthe same effect without writing a `::part()` rule at all.)\n\n`::part()` only reaches CSS-expressible changes (a mask, a background, a transform). For\nanything else — different markup, an icon font, logic beyond a mask — set the static `icon`\nand `nestedIcon` hooks. Every `<use-caret>` in the page picks them up; consumers keep writing\nthe same tag, no new element to register or remember to use instead. `icon` supplies a\ntop-level trigger's glyph, `nestedIcon` a submenu trigger's — set whichever apply; an unset one\nkeeps the built-in ▼/▶ for that case. Set them once, as early as possible (an app's entry\npoint, before any triggers connect) — Lit calls `render()` fresh on every update, so an\nalready-connected instance only reflects the change once something causes it to re-render:\n\n```ts\n// A bare string or TemplateResult is used as-is, on every trigger of that kind:\nUseCaret.icon = \"▾\";\nUseCaret.nestedIcon = \"▸\";\n\n// A function is called on every render instead:\nUseCaret.nestedIcon = (html) => html`<my-icon name=\"chevron-right\"></my-icon>`;\n```\n\nThe function form is called with the component's own `html` tag as its (optional) first\nargument rather than requiring the caller to import one — a page can end up with more than one\ncopy of `lit-html` in its module graph, and a `TemplateResult` built from a *different* copy\nthan the one this component renders with can silently fail to render. Taking `html` in as a\nparameter sidesteps that: whatever's returned is guaranteed to come from the same `lit-html`\nthis component already uses. A bare string skips the concern entirely.\n\nOnly need to change the icon on **some** triggers, not every one in the app? Extend the\nclass, override `render()` normally, and register the subclass under a different tag name so\nit coexists with the built-in element instead of replacing it everywhere:\n\n```ts\nclass MyCaret extends UseCaret {\n override render() {\n return html`<slot></slot><slot name=\"icon\"><my-icon name=\"chevron\"></my-icon></slot>`;\n }\n}\ncustomElements.define(\"my-caret\", MyCaret);\n```\n---\n\n\n### **Slots:**\n - **default** - Trigger label content.\n- **icon** - Replaces the default caret glyph.",
86
- "attributes": [],
87
- "references": []
88
- },
89
89
  {
90
90
  "name": "use-date-picker",
91
91
  "description": "A day-granularity date picker. Value format: YYYY-MM-DD.\n\nCustomize day rendering on an instance:\n```ts\ndatePicker.renderDay = ({ day, date }, html) =>\n date === \"2026-03-17\" ? html`<strong>${day}</strong>` : String(day);\n```\n\nOr override on a subclass to apply it to every instance, falling back to\n`super.renderDay(...)` for dates you don't want to customize:\n```ts\nclass MyDatePicker extends UseDatePicker {\n renderDay(data: { day: number; date: string }, html: LitHtml) {\n if (data.date === \"2026-03-17\") {\n return html`<strong>${data.day}</strong>`;\n }\n return super.renderDay(data, html);\n }\n}\n```\n---\n\n\n### **Events:**\n - **use-change** - Fired on selection with detail `{ value: string }`\n\n### **Methods:**\n - **renderDay(data: _{ day: number; date: string }_, _html: _LitHtml_): _TemplateResult | string_** - See UseCalendarRenderDay. Assigning a new function on an\ninstance (e.g. via `.renderDay=${fn}` in a Lit template) after first\nrender does not schedule a re-render on its own — call `requestUpdate()`\nif needed.\n\nDeclared as a regular method rather than a class field so subclass\noverrides can call `super.renderDay(data, html)` to fall back to the\ndefault (or a parent override's) rendering.\n\n### **Slots:**\n - **header-start** - Content before the title in the header\n- **title** - Title showing the month and year\n- **controls** - Navigation controls (previous, today, next buttons)\n- **header-end** - Content after the controls in the header\n- **date-{YYYY-MM-DD}** - Per-date content that replaces the default day number\n\n### **CSS Parts:**\n - **header** - The header container\n- **title** - The title text\n- **title-month** - The month part of the title\n- **title-literal** - Literal text in the title\n- **title-year** - The year part of the title\n- **controls** - The controls slot container\n- **control** - Individual control button\n- **control-previous** - Previous month button\n- **control-today** - Today button\n- **control-next** - Next month button\n- **grid** - The main calendar grid\n- **grid-header** - The header row with day names\n- **grid-header-cell** - Individual day name header cell\n- **grid-body** - The container for calendar weeks\n- **row** - A row of days\n- **day** - A day cell\n- **day-empty** - An empty day cell (from previous/next month)\n- **day-selected** - A selected day cell\n- **day-today** - The current date cell\n- **day-disabled** - A disabled date cell\n- **title-button** - The clickable button wrapping the month/year title\n- **picker** - The scrollable month/year picker panel (replaces grid when open)\n- **picker-year-section** - Container for one year's label and month grid\n- **picker-year-label** - The year heading inside a year section\n- **picker-months** - The 4-column month grid inside a year section\n- **picker-month** - An individual month button\n- **picker-month-current** - Applied to the currently selected month button",