@davidsouther/jiffies 2026.4.1 → 2026.24.0

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.
Files changed (121) hide show
  1. package/README.md +0 -3
  2. package/package.json +11 -6
  3. package/src/404.html +1 -1
  4. package/src/components/accordion.ts +25 -0
  5. package/src/components/alert.ts +47 -0
  6. package/src/components/card.ts +54 -0
  7. package/src/components/children.ts +11 -0
  8. package/src/components/form.ts +25 -0
  9. package/src/components/index.ts +22 -0
  10. package/src/components/link.ts +22 -0
  11. package/src/components/modal.ts +15 -0
  12. package/src/components/nav.ts +42 -0
  13. package/src/components/property.ts +32 -0
  14. package/src/components/tabs.ts +82 -0
  15. package/src/components/virtual_scroll.ts +1 -1
  16. package/src/dom/README.md +7 -2
  17. package/src/dom/SKILL.md +201 -0
  18. package/src/dom/dom.ts +185 -41
  19. package/src/dom/fc.ts +3 -2
  20. package/src/dom/form/form.app.ts +35 -41
  21. package/src/dom/form/form.ts +79 -10
  22. package/src/dom/form/index.html +2 -2
  23. package/src/dom/hydrate.ts +206 -0
  24. package/src/dom/navigation/index.ts +349 -0
  25. package/src/dom/render.ts +41 -0
  26. package/src/dom/svg.ts +6 -2
  27. package/src/fs_node.ts +2 -2
  28. package/src/log.ts +154 -2
  29. package/src/server/http/response.ts +6 -3
  30. package/src/server/http/sitemap.ts +10 -34
  31. package/src/server/http/static.ts +0 -2
  32. package/src/server/live-reload.ts +208 -0
  33. package/src/server/main.ts +14 -7
  34. package/src/server/ws/frame.ts +36 -0
  35. package/src/server/ws/handshake.ts +42 -0
  36. package/src/server/ws/index.ts +100 -0
  37. package/src/ssg/bundle.ts +85 -0
  38. package/src/ssg/copy-public.ts +44 -0
  39. package/src/ssg/discover.ts +143 -0
  40. package/src/ssg/main.ts +168 -0
  41. package/src/ssg/rewrite.ts +18 -0
  42. package/src/ssg/ssg.ts +134 -0
  43. package/src/components/test.ts +0 -5
  44. package/src/components/virtual_scroll.test.ts +0 -30
  45. package/src/context.test.ts +0 -58
  46. package/src/context.ts +0 -67
  47. package/src/diff.test.ts +0 -48
  48. package/src/dom/fc.test.ts +0 -43
  49. package/src/dom/form/form.test.ts +0 -0
  50. package/src/dom/html.test.ts +0 -74
  51. package/src/dom/observable.test.ts +0 -43
  52. package/src/dom/test.ts +0 -11
  53. package/src/equal.test.ts +0 -23
  54. package/src/flags.test.ts +0 -43
  55. package/src/flags.ts +0 -53
  56. package/src/fs.test.ts +0 -106
  57. package/src/fs_win.test.ts +0 -11
  58. package/src/generator.test.ts +0 -27
  59. package/src/index.html +0 -82
  60. package/src/is_browser.js +0 -1
  61. package/src/lock.test.ts +0 -17
  62. package/src/observable/observable.test.ts +0 -73
  63. package/src/pico/_variables.scss +0 -66
  64. package/src/pico/components/_accordion.scss +0 -112
  65. package/src/pico/components/_button-group.scss +0 -51
  66. package/src/pico/components/_card.scss +0 -47
  67. package/src/pico/components/_dropdown.scss +0 -203
  68. package/src/pico/components/_modal.scss +0 -181
  69. package/src/pico/components/_nav.scss +0 -79
  70. package/src/pico/components/_progress.scss +0 -70
  71. package/src/pico/components/_property.scss +0 -34
  72. package/src/pico/content/_button.scss +0 -152
  73. package/src/pico/content/_code.scss +0 -63
  74. package/src/pico/content/_embedded.scss +0 -0
  75. package/src/pico/content/_form-alt.scss +0 -276
  76. package/src/pico/content/_form.scss +0 -259
  77. package/src/pico/content/_misc.scss +0 -0
  78. package/src/pico/content/_table.scss +0 -28
  79. package/src/pico/content/_toggle.scss +0 -132
  80. package/src/pico/content/_typography.scss +0 -232
  81. package/src/pico/layout/_container.scss +0 -40
  82. package/src/pico/layout/_document.scss +0 -0
  83. package/src/pico/layout/_flex.scss +0 -46
  84. package/src/pico/layout/_grid.scss +0 -24
  85. package/src/pico/layout/_scroller.scss +0 -16
  86. package/src/pico/layout/_section.scss +0 -8
  87. package/src/pico/layout/_sectioning.scss +0 -55
  88. package/src/pico/pico.scss +0 -60
  89. package/src/pico/reset/_accessibility.scss +0 -34
  90. package/src/pico/reset/_button.scss +0 -17
  91. package/src/pico/reset/_code.scss +0 -15
  92. package/src/pico/reset/_document.scss +0 -48
  93. package/src/pico/reset/_embedded.scss +0 -39
  94. package/src/pico/reset/_form.scss +0 -97
  95. package/src/pico/reset/_misc.scss +0 -23
  96. package/src/pico/reset/_nav.scss +0 -5
  97. package/src/pico/reset/_progress.scss +0 -4
  98. package/src/pico/reset/_table.scss +0 -8
  99. package/src/pico/reset/_typography.scss +0 -25
  100. package/src/pico/themes/default/_colors.scss +0 -65
  101. package/src/pico/themes/default/_dark.scss +0 -148
  102. package/src/pico/themes/default/_light.scss +0 -149
  103. package/src/pico/themes/default/_styles.scss +0 -272
  104. package/src/pico/themes/default.scss +0 -34
  105. package/src/pico/utilities/_accessibility.scss +0 -3
  106. package/src/pico/utilities/_loading.scss +0 -52
  107. package/src/pico/utilities/_reduce-motion.scss +0 -27
  108. package/src/pico/utilities/_tooltip.scss +0 -101
  109. package/src/result.test.ts +0 -101
  110. package/src/scope/describe.ts +0 -81
  111. package/src/scope/display/console.ts +0 -26
  112. package/src/scope/display/dom.ts +0 -36
  113. package/src/scope/display/junit.ts +0 -64
  114. package/src/scope/execute.ts +0 -110
  115. package/src/scope/expect.ts +0 -169
  116. package/src/scope/fix.ts +0 -30
  117. package/src/scope/index.ts +0 -11
  118. package/src/scope/scope.ts +0 -21
  119. package/src/scope/state.ts +0 -13
  120. package/src/test.mjs +0 -33
  121. package/src/test_all.ts +0 -35
package/README.md CHANGED
@@ -6,15 +6,12 @@ JEFRi Jiffies are a number of "common" utilities for JavaScript/TypeScript pulle
6
6
  - `context` - JavaScript implementation of the Python [`with`][pywith] statement.
7
7
  - `display` - TypeScript implementation of the Rust [Display][rustdisplay] trait.
8
8
  - `equal` - JavaScript deep equality checkers, including TS type checking.
9
- - `flags` - JavaScript flag, environment, and configuration loader.
10
9
  - `log` - JavaScript implementation of a [log4j][log4j]-alike logger.
11
10
  - `result` - JavaScript implementation of Rust's [Option][rustoption] and [Result][rustresult] types.
12
- - `loader.mjs` - Node 16.x typescript-transpiling module loader.
13
11
 
14
12
  Jiffies also includes several microframeworks.
15
13
 
16
14
  - `dom` - a tiny DOM functional library.
17
- - `pico` - a copy of [PicoCSS](pico.css).
18
15
  - `scope` - JavaScript testing microframework.
19
16
  - `server` - Node HTTP Server & middleware.
20
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidsouther/jiffies",
3
- "version": "2026.4.1",
3
+ "version": "2026.24.0",
4
4
  "private": false,
5
5
  "displayName": "JEFRi Jiffies",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "src",
12
+ "!src/**/*.test.ts",
12
13
  "LICENSE",
13
14
  "package.json",
14
15
  "README.md",
@@ -19,22 +20,26 @@
19
20
  },
20
21
  "scripts": {
21
22
  "start": "node ./src/server/main.ts",
22
- "test": "node ./src/test.mjs",
23
- "ci": "npm run node ./src/test.mjs --mode=junit",
23
+ "test": "node --test --test-reporter=tap",
24
+ "ci": "node --test --test-reporter=junit",
24
25
  "format": "biome format --write",
25
26
  "fix": "biome check --write --unsafe",
26
27
  "check:format": "biome format",
27
28
  "check:lint": "biome check",
28
- "all": "npm run check:lint && npm run test"
29
+ "all": "npm run check:lint && npm run test",
30
+ "stamp": "node scripts/stamp.ts"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@biomejs/biome": "^2.3.11",
34
+ "@types/dom-navigation": "^1.0.7",
32
35
  "@types/jsdom": "^27.0.0",
33
- "@types/node": "^20.8.9"
36
+ "@types/node": "^22.19.19"
34
37
  },
35
38
  "dependencies": {
39
+ "@rollup/plugin-node-resolve": "^16.0.3",
36
40
  "jsdom": "^27.4.0",
41
+ "rollup": "^4.61.1",
37
42
  "ts-blank-space": "^0.7.0",
38
43
  "typescript": "^5.9.3"
39
44
  }
40
- }
45
+ }
package/src/404.html CHANGED
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en-US">
3
3
  <head>
4
4
  <title>Not Found</title>
5
5
  <style>
@@ -0,0 +1,25 @@
1
+ // Accordion emits the jiffies-css disclosure widget: details > summary + body.
2
+ // Why: jiffies-css targets details > summary; the summary must be the first child
3
+ // and the disclosed body follows it.
4
+
5
+ import type { Attrs, DenormChildren } from "../dom/dom.ts";
6
+ import { details, summary } from "../dom/html.ts";
7
+ import { toChildren } from "./children.ts";
8
+
9
+ // Accordion props: the summary slot plus any DOM attrs (class, lang, ...) to apply
10
+ // to the outermost <details>.
11
+ export type AccordionProps = {
12
+ summary: DenormChildren | DenormChildren[];
13
+ } & Attrs<HTMLDetailsElement>;
14
+
15
+ // Invariant: <summary> is always the first child; remaining children form the body.
16
+ export function Accordion(
17
+ { summary: summaryContent, ...attrs }: AccordionProps,
18
+ ...bodyChildren: DenormChildren[]
19
+ ): HTMLDetailsElement {
20
+ return details(
21
+ attrs,
22
+ summary(...toChildren(summaryContent)),
23
+ ...bodyChildren,
24
+ );
25
+ }
@@ -0,0 +1,47 @@
1
+ import type { Attrs, DenormChildren } from "../dom/dom.ts";
2
+ import { aside, small } from "../dom/html.ts";
3
+
4
+ export type AlertVariant = "warning" | "error" | "info" | "success" | "neutral";
5
+
6
+ // Alert/Chip props: the variant plus any DOM attrs (class, lang, style, ...) to
7
+ // apply to the outermost element. The variant is consumed; the rest fall through.
8
+ export type AlertProps = { variant: AlertVariant } & Attrs<HTMLElement>;
9
+
10
+ // The variant vocabulary is this module's single source of truth; this map both
11
+ // drives Alert's role derivation and stays exhaustive (a new variant without a
12
+ // role entry is a type error).
13
+ const ALERT_ROLE: Record<AlertVariant, "alert" | "status"> = {
14
+ warning: "alert",
15
+ error: "alert",
16
+ info: "status",
17
+ success: "status",
18
+ neutral: "status",
19
+ };
20
+
21
+ // Alert emits aside[role][data-variant] for banner-level messaging.
22
+ // Why: jiffies-css styles alerts by role + data-variant, not by class. role and
23
+ // data-variant are not in the typed attrs surface (role is constrained, data-* is
24
+ // not a property), so they are set with setAttribute.
25
+ // Invariant: warning|error => role="alert"; info|success|neutral => role="status";
26
+ // data-variant always equals the variant; emits no class of its own, but
27
+ // forwards a caller-supplied class (and other attrs) to the <aside>.
28
+ export function Alert(
29
+ { variant, ...attrs }: AlertProps,
30
+ ...children: DenormChildren[]
31
+ ): HTMLElement {
32
+ const el = aside(attrs, ...children);
33
+ el.setAttribute("role", ALERT_ROLE[variant]);
34
+ el.setAttribute("data-variant", variant);
35
+ return el;
36
+ }
37
+
38
+ // Chip emits small[data-variant] for inline status pills. Same variant vocabulary
39
+ // as Alert, no role. Not exercised by the feature test.
40
+ export function Chip(
41
+ { variant, ...attrs }: AlertProps,
42
+ ...children: DenormChildren[]
43
+ ): HTMLElement {
44
+ const el = small(attrs, ...children);
45
+ el.setAttribute("data-variant", variant);
46
+ return el;
47
+ }
@@ -0,0 +1,54 @@
1
+ import type { Attrs, DenormChildren } from "../dom/dom.ts";
2
+ import { article, footer, header, main, section } from "../dom/html.ts";
3
+ import { toChildren } from "./children.ts";
4
+
5
+ export interface CardParts {
6
+ header?: DenormChildren | DenormChildren[];
7
+ footer?: DenormChildren | DenormChildren[];
8
+ }
9
+
10
+ // Card/Panel props: the structured header/footer slots plus any DOM attrs
11
+ // (class, lang, style, ...) to apply to the outermost wrapper element.
12
+ export type CardProps = CardParts & Attrs<HTMLElement>;
13
+
14
+ // Build the shared header? / main / footer? sequence. `root` is the wrapper
15
+ // element builder (article for Card, section for Panel); the only difference
16
+ // between the two components is which wrapper they use.
17
+ function cardLike(
18
+ root: typeof article,
19
+ { header: headerPart, footer: footerPart, ...attrs }: CardProps,
20
+ children: DenormChildren[],
21
+ ): HTMLElement {
22
+ const sections: DenormChildren[] = [];
23
+ if (headerPart !== undefined) {
24
+ sections.push(header(...toChildren(headerPart)));
25
+ }
26
+ sections.push(main(...children));
27
+ if (footerPart !== undefined) {
28
+ sections.push(footer(...toChildren(footerPart)));
29
+ }
30
+ return root(attrs, ...sections);
31
+ }
32
+
33
+ // Card emits the jiffies-css elevated-card structure: article > header? / main / footer?.
34
+ // Why: jiffies-css targets `article > main` for card body padding, so body content
35
+ // must always be wrapped in <main>, never placed as a bare article child.
36
+ // Invariants: <main> is always emitted (even with no parts); <header> only when
37
+ // parts.header is set; <footer> only when parts.footer is set; child order is
38
+ // always header, main, footer; emits no class of its own, but forwards
39
+ // caller-supplied attrs (class, lang, ...) to the wrapper element.
40
+ export function Card(
41
+ parts: CardProps,
42
+ ...children: DenormChildren[]
43
+ ): HTMLElement {
44
+ return cardLike(article, parts, children);
45
+ }
46
+
47
+ // Panel is the flat variant: section > header? / main / footer?. Same contract as
48
+ // Card with `section` in place of `article`. Not exercised by the feature test.
49
+ export function Panel(
50
+ parts: CardProps,
51
+ ...children: DenormChildren[]
52
+ ): HTMLElement {
53
+ return cardLike(section, parts, children);
54
+ }
@@ -0,0 +1,11 @@
1
+ import type { DenormChildren } from "../dom/dom";
2
+
3
+ // Normalize a "one child or many" part-slot to a flat children array. Several
4
+ // components accept either a single child or an array in the same position
5
+ // (CardParts.header/footer, Accordion summary, PropertyEntry.value); this is the
6
+ // one place that distinction is collapsed.
7
+ export function toChildren(
8
+ part: DenormChildren | DenormChildren[],
9
+ ): DenormChildren[] {
10
+ return Array.isArray(part) ? part : [part];
11
+ }
@@ -0,0 +1,25 @@
1
+ import type { Attrs, DenormChildren } from "../dom/dom.ts";
2
+ import { fieldset, legend } from "../dom/html.ts";
3
+
4
+ // FormGroup props: the legend label plus any DOM attrs (class, lang, ...) to apply
5
+ // to the outermost <fieldset>.
6
+ export type FormGroupProps = {
7
+ legend: DenormChildren;
8
+ } & Attrs<HTMLFieldSetElement>;
9
+
10
+ // FormGroup emits fieldset[role=group] > legend + children — the jiffies-css
11
+ // grouped-controls pattern. This is the structural form group; the richer form
12
+ // controls (Input, Select, Radios, ...) live in src/dom/form/form.ts.
13
+ // Why: jiffies-css targets fieldset[role=group] to lay grouped controls out as a
14
+ // row; role is set with setAttribute since "group" is outside the typed role surface.
15
+
16
+ // Invariant: <legend> is the first child; role="group"; emits no class of its own,
17
+ // but forwards caller-supplied attrs (class, lang, ...) to the <fieldset>.
18
+ export function FormGroup(
19
+ { legend: legendLabel, ...attrs }: FormGroupProps,
20
+ ...children: DenormChildren[]
21
+ ): HTMLFieldSetElement {
22
+ const group = fieldset(attrs, legend(legendLabel), ...children);
23
+ group.setAttribute("role", "group");
24
+ return group;
25
+ }
@@ -0,0 +1,22 @@
1
+ // Re-exports the public component surface. The feature test imports from here.
2
+
3
+ export { Accordion, type AccordionProps } from "./accordion.ts";
4
+ export { Alert, type AlertProps, type AlertVariant, Chip } from "./alert.ts";
5
+ export { Card, type CardParts, type CardProps, Panel } from "./card.ts";
6
+ export { FormGroup, type FormGroupProps } from "./form.ts";
7
+ export { type JiffiesCssLinkProps, jiffiesCssLink } from "./link.ts";
8
+ export { Modal } from "./modal.ts";
9
+ export { Breadcrumb, Nav, type NavItem, type NavProps } from "./nav.ts";
10
+ export {
11
+ type PropertyEntry,
12
+ PropertySheet,
13
+ type PropertySheetProps,
14
+ } from "./property.ts";
15
+ export {
16
+ type StaticTabItem,
17
+ StaticTabList,
18
+ type StaticTabListProps,
19
+ type TabItem,
20
+ TabList,
21
+ type TabListProps,
22
+ } from "./tabs.ts";
@@ -0,0 +1,22 @@
1
+ import type { Attrs } from "../dom/dom.ts";
2
+ import { link } from "../dom/html.ts";
3
+
4
+ const JIFFIES_CSS_CDN =
5
+ "https://unpkg.com/@davidsouther/jiffies-css/dist/index.css";
6
+
7
+ // jiffiesCssLink props: an optional href override plus any DOM attrs (lang,
8
+ // media, ...) to apply to the <link>.
9
+ export type JiffiesCssLinkProps = { href?: string } & Attrs<HTMLLinkElement>;
10
+
11
+ // jiffiesCssLink builds the <link> a page puts in <head> to load jiffies-css.
12
+ // Why: callers must never hand-write the CDN URL or accidentally point at Pico;
13
+ // this is the single sanctioned source of the stylesheet href.
14
+ // Invariant: returns a <link rel="stylesheet"> whose href contains "jiffies-css"
15
+ // and never "pico". Default href is the unpkg CDN; callers bundling locally pass
16
+ // their own href.
17
+ export function jiffiesCssLink({
18
+ href = JIFFIES_CSS_CDN,
19
+ ...attrs
20
+ }: JiffiesCssLinkProps = {}): HTMLLinkElement {
21
+ return link({ ...attrs, rel: "stylesheet", href });
22
+ }
@@ -0,0 +1,15 @@
1
+ import type { DenormAttrs, DenormChildren } from "../dom/dom.ts";
2
+ import { dialog } from "../dom/html.ts";
3
+
4
+ // Modal emits a <dialog>. It has no domain props, so its leading argument is the
5
+ // denormalized attrs of any html builder: a plain object is attrs (class, lang,
6
+ // ...) applied to the <dialog>, anything else is the first child. The element
7
+ // already carries .update() (attached by up()), so callers toggle visibility
8
+ // later with modal.update({ open: true }) / modal.update({ open: false }).
9
+ // Invariant: returns a <dialog> whose children are the supplied content.
10
+ export function Modal(
11
+ attrs?: DenormAttrs<HTMLDialogElement>,
12
+ ...children: DenormChildren[]
13
+ ): HTMLDialogElement {
14
+ return dialog(attrs, ...children);
15
+ }
@@ -0,0 +1,42 @@
1
+ import type { Attrs } from "../dom/dom.ts";
2
+ import { a, li, nav, ol, span } from "../dom/html.ts";
3
+
4
+ export interface NavItem {
5
+ label: string;
6
+ href?: string;
7
+ current?: boolean;
8
+ }
9
+
10
+ // Nav/Breadcrumb props: the item list plus any DOM attrs (class, lang, ...) to
11
+ // apply to the outermost element.
12
+ export type NavProps = { items: NavItem[] } & Attrs<HTMLElement>;
13
+
14
+ // One <li><a> per item. aria-current is set with setAttribute (aria-* is not in
15
+ // the typed attrs surface); the anchor carries href only when the item supplies one.
16
+ function navItem(item: NavItem): HTMLElement {
17
+ const anchor = a(item.href ? { href: item.href } : {}, item.label);
18
+ if (item.current) {
19
+ anchor.setAttribute("aria-current", "page");
20
+ }
21
+ return li(anchor);
22
+ }
23
+
24
+ function navList(items: NavItem[]): HTMLElement {
25
+ return ol(...items.map(navItem));
26
+ }
27
+
28
+ // Nav emits nav > ol > li > a, one <li> per item.
29
+ // Why: jiffies-css targets the nav > ol > li > a chain; a bare ul > li > a is unstyled.
30
+ // Invariants: every item is an <a> inside an <li> inside the single <ol>; an item
31
+ // with current:true gets aria-current="page" on its <a>; emits no class of its own,
32
+ // but forwards caller-supplied attrs (class, lang, ...) to the <nav>.
33
+ export function Nav({ items, ...attrs }: NavProps): HTMLElement {
34
+ return nav(attrs, navList(items));
35
+ }
36
+
37
+ // Breadcrumb wraps the same nav > ol > li chain in a <span> (span > nav > ol > li),
38
+ // the jiffies-css breadcrumb selector. Same file, same pattern. Not exercised by
39
+ // the feature test.
40
+ export function Breadcrumb({ items, ...attrs }: NavProps): HTMLElement {
41
+ return span(attrs, nav(navList(items)));
42
+ }
@@ -0,0 +1,32 @@
1
+ import type { Attrs, DenormChildren } from "../dom/dom.ts";
2
+ import { dd, dl, dt } from "../dom/html.ts";
3
+ import { toChildren } from "./children.ts";
4
+
5
+ export interface PropertyEntry {
6
+ label: string;
7
+ value: DenormChildren | DenormChildren[];
8
+ }
9
+
10
+ // PropertySheet props: the entry list plus any DOM attrs (class, lang, ...) to
11
+ // apply to the outermost <dl>.
12
+ export type PropertySheetProps = {
13
+ entries: PropertyEntry[];
14
+ } & Attrs<HTMLDListElement>;
15
+
16
+ // PropertySheet emits dl > (dt + dd)* — the jiffies-css property-sheet pattern,
17
+ // one dt/dd pair per entry.
18
+ // Why: jiffies-css targets dl > dt + dd for aligned label/value rows; a table or
19
+ // div grid is unstyled.
20
+ // Invariant: exactly one <dt> (the label) and one <dd> (the value) per entry, in
21
+ // entry order; emits no class of its own, but forwards caller-supplied attrs
22
+ // (class, lang, ...) to the <dl>.
23
+ export function PropertySheet({
24
+ entries,
25
+ ...attrs
26
+ }: PropertySheetProps): HTMLDListElement {
27
+ const rows: DenormChildren[] = [];
28
+ for (const entry of entries) {
29
+ rows.push(dt(entry.label), dd(...toChildren(entry.value)));
30
+ }
31
+ return dl(attrs, ...rows);
32
+ }
@@ -0,0 +1,82 @@
1
+ import type { Attrs, DenormChildren } from "../dom/dom.ts";
2
+ import { button, div, input, label } from "../dom/html.ts";
3
+
4
+ // Shared tablist container: div[role=tablist] holding the supplied tab controls.
5
+ // Both tab variants emit the same container; this is the one place the role string
6
+ // lives (matching the cardLike/navList helpers elsewhere in the module).
7
+ function tablist(
8
+ attrs: Attrs<HTMLDivElement>,
9
+ ...children: DenormChildren[]
10
+ ): HTMLElement {
11
+ const list = div(attrs, ...children);
12
+ list.setAttribute("role", "tablist");
13
+ return list;
14
+ }
15
+
16
+ export interface TabItem {
17
+ label: string;
18
+ selected?: boolean;
19
+ onSelect?: (e: Event) => void; // JS variant only
20
+ }
21
+
22
+ export interface StaticTabItem {
23
+ id: string;
24
+ label: string;
25
+ selected?: boolean;
26
+ }
27
+
28
+ // TabList props: the tab list plus any DOM attrs (class, lang, ...) to apply to
29
+ // the outermost div[role=tablist].
30
+ export type TabListProps = { tabs: TabItem[] } & Attrs<HTMLDivElement>;
31
+
32
+ // StaticTabList props: the shared radio-group name and tab list, plus any DOM
33
+ // attrs to apply to the outermost div[role=tablist].
34
+ export type StaticTabListProps = {
35
+ name: string;
36
+ tabs: StaticTabItem[];
37
+ } & Attrs<HTMLDivElement>;
38
+
39
+ // TabList emits the JS-driven tab strip: div[role=tablist] > button[role=tab].
40
+ // Why: jiffies-css targets [role=tablist] > button[role=tab][aria-selected]; the
41
+ // caller owns which tab is active and re-renders via .update() on the element.
42
+ // Invariant: role="tablist" on the container; every tab is a button[role=tab];
43
+ // selected:true sets aria-selected="true"; onSelect is wired as a click handler.
44
+ export function TabList({ tabs, ...attrs }: TabListProps): HTMLElement {
45
+ const buttons = tabs.map((tab) => {
46
+ const btn = button(
47
+ tab.onSelect
48
+ ? { type: "button", events: { click: tab.onSelect } }
49
+ : { type: "button" },
50
+ tab.label,
51
+ );
52
+ btn.setAttribute("role", "tab");
53
+ if (tab.selected) {
54
+ btn.setAttribute("aria-selected", "true");
55
+ }
56
+ return btn;
57
+ });
58
+ return tablist(attrs, ...buttons);
59
+ }
60
+
61
+ // StaticTabList emits the CSS-only tab strip: div[role=tablist] >
62
+ // (input[type=radio][name][id] + label[role=tab][for])*. The shared name groups
63
+ // the radios; :checked drives the active panel with zero JavaScript.
64
+ // Invariant: role="tablist" on the container; one radio + one label[role=tab] per
65
+ // tab; id/for pair come from StaticTabItem.id; selected:true sets defaultChecked.
66
+ export function StaticTabList({
67
+ name,
68
+ tabs,
69
+ ...attrs
70
+ }: StaticTabListProps): HTMLElement {
71
+ const children = tabs.flatMap((tab) => {
72
+ const radio = input({ type: "radio", name, id: tab.id });
73
+ if (tab.selected) {
74
+ radio.defaultChecked = true;
75
+ }
76
+ const lbl = label(tab.label);
77
+ lbl.setAttribute("for", tab.id);
78
+ lbl.setAttribute("role", "tab");
79
+ return [radio, lbl];
80
+ });
81
+ return tablist(attrs, ...children);
82
+ }
@@ -160,7 +160,7 @@ export const VirtualScroll = FC<
160
160
  },
161
161
  });
162
162
  setTimeout(() => {
163
- viewportElement.scroll({ top: state.scrollTop });
163
+ viewportElement.scroll?.({ top: state.scrollTop });
164
164
  });
165
165
 
166
166
  const setState = (newState: VirtualScrollState<unknown>) => {
package/src/dom/README.md CHANGED
@@ -97,6 +97,11 @@ const MyPage = () => [
97
97
  ];
98
98
  ```
99
99
 
100
- ## Pico CSS
100
+ ## jiffies-css
101
101
 
102
- Including [Pico.css](https://picocss.com/) provides a high-quality semantic HTML base to begin styling from.
102
+ Including [jiffies-css](https://www.npmjs.com/package/@davidsouther/jiffies-css)
103
+ provides a semantic HTML base that styles by element type and ARIA role rather
104
+ than class names. Load it with `jiffiesCssLink()` and build pages from the typed
105
+ component functions in [`dom/components`](./components/index.ts) (`Card`, `Alert`,
106
+ `Nav`, ...), which emit the exact structures jiffies-css targets — no manual class
107
+ annotation.