@gjermundgaraba-designs/simple-rat 0.0.0-stage → 0.7.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 (179) hide show
  1. package/DESIGN.md +397 -0
  2. package/LICENSE +21 -0
  3. package/README.md +64 -2
  4. package/dist/highlight/highlight.d.ts +5 -0
  5. package/dist/highlight/highlight.d.ts.map +1 -0
  6. package/dist/highlight/highlight.js +120 -0
  7. package/dist/highlight/vite.d.ts +19 -0
  8. package/dist/highlight/vite.d.ts.map +1 -0
  9. package/dist/highlight/vite.js +38 -0
  10. package/dist/src/code/code.d.ts +20 -0
  11. package/dist/src/code/code.d.ts.map +1 -0
  12. package/dist/src/code/code.js +9 -0
  13. package/dist/src/code/index.d.ts +3 -0
  14. package/dist/src/code/index.d.ts.map +1 -0
  15. package/dist/src/code/index.js +1 -0
  16. package/dist/src/codeBlock/codeBlock.d.ts +32 -0
  17. package/dist/src/codeBlock/codeBlock.d.ts.map +1 -0
  18. package/dist/src/codeBlock/codeBlock.js +35 -0
  19. package/dist/src/codeBlock/index.d.ts +3 -0
  20. package/dist/src/codeBlock/index.d.ts.map +1 -0
  21. package/dist/src/codeBlock/index.js +1 -0
  22. package/dist/src/copy/copy.d.ts +83 -0
  23. package/dist/src/copy/copy.d.ts.map +1 -0
  24. package/dist/src/copy/copy.js +76 -0
  25. package/dist/src/copyTab/copyTab.d.ts +47 -0
  26. package/dist/src/copyTab/copyTab.d.ts.map +1 -0
  27. package/dist/src/copyTab/copyTab.js +31 -0
  28. package/dist/src/copyTab/index.d.ts +3 -0
  29. package/dist/src/copyTab/index.d.ts.map +1 -0
  30. package/dist/src/copyTab/index.js +1 -0
  31. package/dist/src/diagram/diagram.d.ts +18 -0
  32. package/dist/src/diagram/diagram.d.ts.map +1 -0
  33. package/dist/src/diagram/diagram.js +18 -0
  34. package/dist/src/diagram/index.d.ts +3 -0
  35. package/dist/src/diagram/index.d.ts.map +1 -0
  36. package/dist/src/diagram/index.js +1 -0
  37. package/dist/src/footer/footer.d.ts +9 -0
  38. package/dist/src/footer/footer.d.ts.map +1 -0
  39. package/dist/src/footer/footer.js +3 -0
  40. package/dist/src/footer/index.d.ts +3 -0
  41. package/dist/src/footer/index.d.ts.map +1 -0
  42. package/dist/src/footer/index.js +1 -0
  43. package/dist/src/footnotes/footnotes.d.ts +12 -0
  44. package/dist/src/footnotes/footnotes.d.ts.map +1 -0
  45. package/dist/src/footnotes/footnotes.js +16 -0
  46. package/dist/src/footnotes/index.d.ts +3 -0
  47. package/dist/src/footnotes/index.d.ts.map +1 -0
  48. package/dist/src/footnotes/index.js +1 -0
  49. package/dist/src/header/header.d.ts +11 -0
  50. package/dist/src/header/header.d.ts.map +1 -0
  51. package/dist/src/header/header.js +8 -0
  52. package/dist/src/header/index.d.ts +3 -0
  53. package/dist/src/header/index.d.ts.map +1 -0
  54. package/dist/src/header/index.js +1 -0
  55. package/dist/src/heading/heading.d.ts +12 -0
  56. package/dist/src/heading/heading.d.ts.map +1 -0
  57. package/dist/src/heading/heading.js +12 -0
  58. package/dist/src/heading/index.d.ts +3 -0
  59. package/dist/src/heading/index.d.ts.map +1 -0
  60. package/dist/src/heading/index.js +1 -0
  61. package/dist/src/index.d.ts +19 -0
  62. package/dist/src/index.d.ts.map +1 -0
  63. package/dist/src/index.js +20 -0
  64. package/dist/src/navLink/index.d.ts +3 -0
  65. package/dist/src/navLink/index.d.ts.map +1 -0
  66. package/dist/src/navLink/index.js +1 -0
  67. package/dist/src/navLink/navLink.d.ts +13 -0
  68. package/dist/src/navLink/navLink.d.ts.map +1 -0
  69. package/dist/src/navLink/navLink.js +7 -0
  70. package/dist/src/note/index.d.ts +3 -0
  71. package/dist/src/note/index.d.ts.map +1 -0
  72. package/dist/src/note/index.js +1 -0
  73. package/dist/src/note/note.d.ts +12 -0
  74. package/dist/src/note/note.d.ts.map +1 -0
  75. package/dist/src/note/note.js +7 -0
  76. package/dist/src/pager/index.d.ts +3 -0
  77. package/dist/src/pager/index.d.ts.map +1 -0
  78. package/dist/src/pager/index.js +1 -0
  79. package/dist/src/pager/pager.d.ts +17 -0
  80. package/dist/src/pager/pager.d.ts.map +1 -0
  81. package/dist/src/pager/pager.js +15 -0
  82. package/dist/src/postList/index.d.ts +3 -0
  83. package/dist/src/postList/index.d.ts.map +1 -0
  84. package/dist/src/postList/index.js +1 -0
  85. package/dist/src/postList/postList.d.ts +13 -0
  86. package/dist/src/postList/postList.d.ts.map +1 -0
  87. package/dist/src/postList/postList.js +3 -0
  88. package/dist/src/postMeta/index.d.ts +3 -0
  89. package/dist/src/postMeta/index.d.ts.map +1 -0
  90. package/dist/src/postMeta/index.js +1 -0
  91. package/dist/src/postMeta/postMeta.d.ts +11 -0
  92. package/dist/src/postMeta/postMeta.d.ts.map +1 -0
  93. package/dist/src/postMeta/postMeta.js +10 -0
  94. package/dist/src/scheme/index.d.ts +4 -0
  95. package/dist/src/scheme/index.d.ts.map +1 -0
  96. package/dist/src/scheme/index.js +3 -0
  97. package/dist/src/scheme/scheme.d.ts +10 -0
  98. package/dist/src/scheme/scheme.d.ts.map +1 -0
  99. package/dist/src/scheme/scheme.js +9 -0
  100. package/dist/src/scheme/schemeSwitch.d.ts +18 -0
  101. package/dist/src/scheme/schemeSwitch.d.ts.map +1 -0
  102. package/dist/src/scheme/schemeSwitch.js +25 -0
  103. package/dist/src/scheme/script.d.ts +9 -0
  104. package/dist/src/scheme/script.d.ts.map +1 -0
  105. package/dist/src/scheme/script.js +8 -0
  106. package/dist/src/scheme/switch.d.ts +64 -0
  107. package/dist/src/scheme/switch.d.ts.map +1 -0
  108. package/dist/src/scheme/switch.js +72 -0
  109. package/dist/src/scrolls/index.d.ts +2 -0
  110. package/dist/src/scrolls/index.d.ts.map +1 -0
  111. package/dist/src/scrolls/index.js +1 -0
  112. package/dist/src/scrolls/scrolls.d.ts +19 -0
  113. package/dist/src/scrolls/scrolls.d.ts.map +1 -0
  114. package/dist/src/scrolls/scrolls.js +49 -0
  115. package/dist/src/skipLink/index.d.ts +2 -0
  116. package/dist/src/skipLink/index.d.ts.map +1 -0
  117. package/dist/src/skipLink/index.js +1 -0
  118. package/dist/src/skipLink/skipLink.d.ts +7 -0
  119. package/dist/src/skipLink/skipLink.d.ts.map +1 -0
  120. package/dist/src/skipLink/skipLink.js +6 -0
  121. package/dist/src/swatch/index.d.ts +2 -0
  122. package/dist/src/swatch/index.d.ts.map +1 -0
  123. package/dist/src/swatch/index.js +1 -0
  124. package/dist/src/swatch/swatch.d.ts +4 -0
  125. package/dist/src/swatch/swatch.d.ts.map +1 -0
  126. package/dist/src/swatch/swatch.js +3 -0
  127. package/dist/src/table/index.d.ts +3 -0
  128. package/dist/src/table/index.d.ts.map +1 -0
  129. package/dist/src/table/index.js +1 -0
  130. package/dist/src/table/table.d.ts +15 -0
  131. package/dist/src/table/table.d.ts.map +1 -0
  132. package/dist/src/table/table.js +23 -0
  133. package/highlight/highlight.ts +128 -0
  134. package/highlight/vite.ts +42 -0
  135. package/package.json +75 -3
  136. package/src/code/code.ts +23 -0
  137. package/src/code/index.ts +2 -0
  138. package/src/code/lines.d.ts +39 -0
  139. package/src/codeBlock/codeBlock.ts +83 -0
  140. package/src/codeBlock/index.ts +2 -0
  141. package/src/copy/copy.ts +106 -0
  142. package/src/copyTab/copyTab.ts +61 -0
  143. package/src/copyTab/index.ts +2 -0
  144. package/src/diagram/diagram.ts +43 -0
  145. package/src/diagram/index.ts +2 -0
  146. package/src/footer/footer.ts +11 -0
  147. package/src/footer/index.ts +2 -0
  148. package/src/footnotes/footnotes.ts +38 -0
  149. package/src/footnotes/index.ts +2 -0
  150. package/src/header/header.ts +24 -0
  151. package/src/header/index.ts +2 -0
  152. package/src/heading/heading.ts +21 -0
  153. package/src/heading/index.ts +2 -0
  154. package/src/index.ts +20 -0
  155. package/src/navLink/index.ts +2 -0
  156. package/src/navLink/navLink.ts +18 -0
  157. package/src/note/index.ts +2 -0
  158. package/src/note/note.ts +19 -0
  159. package/src/pager/index.ts +2 -0
  160. package/src/pager/pager.ts +30 -0
  161. package/src/postList/index.ts +2 -0
  162. package/src/postList/postList.ts +19 -0
  163. package/src/postMeta/index.ts +2 -0
  164. package/src/postMeta/postMeta.ts +23 -0
  165. package/src/scheme/index.ts +3 -0
  166. package/src/scheme/scheme.ts +25 -0
  167. package/src/scheme/schemeSwitch.ts +43 -0
  168. package/src/scheme/script.ts +10 -0
  169. package/src/scheme/switch.ts +99 -0
  170. package/src/scrolls/index.ts +1 -0
  171. package/src/scrolls/scrolls.ts +56 -0
  172. package/src/simple-rat.css +601 -0
  173. package/src/skipLink/index.ts +1 -0
  174. package/src/skipLink/skipLink.ts +8 -0
  175. package/src/swatch/index.ts +1 -0
  176. package/src/swatch/swatch.ts +5 -0
  177. package/src/table/index.ts +2 -0
  178. package/src/table/table.ts +50 -0
  179. package/src/tokens.css +67 -0
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect";
2
+ import * as Mount from "foldkit/mount";
3
+ /** What the Mount reports: it is watching. Nothing in a Model follows it. */
4
+ export declare const Message: import("foldkit/message").MessageUnion<{
5
+ readonly CompletedWatchOverflow: {};
6
+ }>;
7
+ export type Message = typeof Message.Type;
8
+ /** Whether a block is wider than its box and scrolls sideways, checked when it mounts and
9
+ whenever its own box changes size, as when the window resizes. A font arriving that
10
+ widens the text but not the box is not noticed. A block that scrolls takes keyboard focus
11
+ (`tabindex="0"`) so it can be scrolled without a mouse, and with a name (`maybeName`)
12
+ becomes a `role="region"` called "Code: <name>, scrolls sideways"; one that fits stays out
13
+ of the tab order. The Mount owns those attributes and takes them away when it is released. */
14
+ export declare const WatchOverflow: Mount.MountDefinitionWithArgs<"WatchOverflow", {
15
+ maybeName: Schema.Option<Schema.String>;
16
+ }, {
17
+ readonly _tag: "CompletedWatchOverflow";
18
+ }>;
19
+ //# sourceMappingURL=scrolls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrolls.d.ts","sourceRoot":"","sources":["../../../src/scrolls/scrolls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,KAAK,KAAK,MAAM,eAAe,CAAC;AAIvC,6EAA6E;AAC7E,eAAO,MAAM,OAAO;;EAAqD,CAAC;AAC1E,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAC;AAI1C;;;;;iGAKiG;AACjG,eAAO,MAAM,aAAa;;;;EAqCxB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
2
+ import { Effect, Option, Schema } from "effect";
3
+ import { defineMessageUnion } from "foldkit/message";
4
+ import * as Mount from "foldkit/mount";
5
+ // MESSAGE
6
+ /** What the Mount reports: it is watching. Nothing in a Model follows it. */
7
+ export const Message = defineMessageUnion({ CompletedWatchOverflow: {} });
8
+ // MOUNT
9
+ /** Whether a block is wider than its box and scrolls sideways, checked when it mounts and
10
+ whenever its own box changes size, as when the window resizes. A font arriving that
11
+ widens the text but not the box is not noticed. A block that scrolls takes keyboard focus
12
+ (`tabindex="0"`) so it can be scrolled without a mouse, and with a name (`maybeName`)
13
+ becomes a `role="region"` called "Code: <name>, scrolls sideways"; one that fits stays out
14
+ of the tab order. The Mount owns those attributes and takes them away when it is released. */
15
+ export const WatchOverflow = Mount.define("WatchOverflow", {
16
+ args: { maybeName: Schema.Option(Schema.String) },
17
+ messages: [Message.CompletedWatchOverflow],
18
+ execute: ({ element, maybeName }) => __foldkitBrandViewResult((Effect.gen(function* () {
19
+ yield* Effect.acquireRelease(Effect.sync(() => {
20
+ const check = () => {
21
+ if (element.scrollWidth > element.clientWidth) {
22
+ element.setAttribute("tabindex", "0");
23
+ if (Option.isSome(maybeName)) {
24
+ element.setAttribute("role", "region");
25
+ element.setAttribute("aria-label", `Code: ${maybeName.value}, scrolls sideways`);
26
+ }
27
+ }
28
+ else {
29
+ clear();
30
+ }
31
+ };
32
+ const clear = () => {
33
+ element.removeAttribute("tabindex");
34
+ if (Option.isSome(maybeName)) {
35
+ element.removeAttribute("role");
36
+ element.removeAttribute("aria-label");
37
+ }
38
+ };
39
+ const resized = new ResizeObserver(check);
40
+ resized.observe(element);
41
+ check();
42
+ return __foldkitBrandViewResult((() => {
43
+ resized.disconnect();
44
+ clear();
45
+ }), "@gjermundgaraba-designs/simple-rat/src/scrolls/scrolls.js#anonymous~2");
46
+ }), (stop) => __foldkitBrandViewResult((Effect.sync(stop)), "@gjermundgaraba-designs/simple-rat/src/scrolls/scrolls.js#anonymous~4"));
47
+ return __foldkitBrandViewResult((Message.CompletedWatchOverflow()), "@gjermundgaraba-designs/simple-rat/src/scrolls/scrolls.js#anonymous");
48
+ })), "@gjermundgaraba-designs/simple-rat/src/scrolls/scrolls.js#execute"),
49
+ });
@@ -0,0 +1,2 @@
1
+ export { view } from "./skipLink.ts";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/skipLink/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1 @@
1
+ export { view } from "./skipLink.js";
@@ -0,0 +1,7 @@
1
+ import type { Html, HtmlBuilder } from "foldkit/html";
2
+ /** "Skip to content", first in the page and shown only when it has keyboard focus. It lands
3
+ on `<main id="main">`. A Foldkit application routes every link click through its own
4
+ update, so it must hand a fragment on the page being read back to the browser:
5
+ `load(href)` from `foldkit/navigation`. */
6
+ export declare const view: <Message>(h: HtmlBuilder<Message>) => Html;
7
+ //# sourceMappingURL=skipLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skipLink.d.ts","sourceRoot":"","sources":["../../../src/skipLink/skipLink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEtD;;;8CAG8C;AAC9C,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IACM,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
2
+ /** "Skip to content", first in the page and shown only when it has keyboard focus. It lands
3
+ on `<main id="main">`. A Foldkit application routes every link click through its own
4
+ update, so it must hand a fragment on the page being read back to the browser:
5
+ `load(href)` from `foldkit/navigation`. */
6
+ export const view = (h) => __foldkitBrandViewResult((h.a([h.Class("skip"), h.Href("#main")], ["Skip to content"])), "@gjermundgaraba-designs/simple-rat/src/skipLink/skipLink.js#view");
@@ -0,0 +1,2 @@
1
+ export { view } from "./swatch.ts";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/swatch/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1 @@
1
+ export { view } from "./swatch.js";
@@ -0,0 +1,4 @@
1
+ import type { Html, HtmlBuilder } from "foldkit/html";
2
+ /** A small square of colour before a colour value in a table cell. */
3
+ export declare const view: <Message>(color: string, h: HtmlBuilder<Message>) => Html;
4
+ //# sourceMappingURL=swatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swatch.d.ts","sourceRoot":"","sources":["../../../src/swatch/swatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEtD,sEAAsE;AACtE,eAAO,MAAM,IAAI,GAAI,OAAO,SAAS,MAAM,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IACV,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
2
+ /** A small square of colour before a colour value in a table cell. */
3
+ export const view = (color, h) => __foldkitBrandViewResult((h.span([h.Class("swatch"), h.Style({ background: color })])), "@gjermundgaraba-designs/simple-rat/src/swatch/swatch.js#view");
@@ -0,0 +1,3 @@
1
+ export { view } from "./table.ts";
2
+ export type { ViewConfig } from "./table.ts";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/table/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ export { view } from "./table.js";
@@ -0,0 +1,15 @@
1
+ import type { Html, HtmlBuilder } from "foldkit/html";
2
+ /** Configuration for rendering a table with `view`. */
3
+ export type ViewConfig = Readonly<{
4
+ columns: ReadonlyArray<string>;
5
+ rows: ReadonlyArray<ReadonlyArray<Html | string>>;
6
+ /** Under 40rem each row becomes a card, its cells labelled with their column. Without it
7
+ the table stays a table on a phone and scrolls sideways. */
8
+ isStacked?: boolean;
9
+ }>;
10
+ /** A ruled table in a `div.table-scroll`, which scrolls it sideways when it is wider than the
11
+ column. A stacked table also writes `data-label` on each cell and the ARIA table roles,
12
+ which keep it announced as a table in browsers that drop the table semantics once CSS
13
+ changes its display, as the stacked cards do. */
14
+ export declare const view: <Message>(config: ViewConfig, h: HtmlBuilder<Message>) => Html;
15
+ //# sourceMappingURL=table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../../src/table/table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEtD,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC;IAClD;mEAC+D;IAC/D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC,CAAC;AAEH;;;oDAGoD;AACpD,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAkC3E,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
2
+ /** A ruled table in a `div.table-scroll`, which scrolls it sideways when it is wider than the
3
+ column. A stacked table also writes `data-label` on each cell and the ARIA table roles,
4
+ which keep it announced as a table in browsers that drop the table semantics once CSS
5
+ changes its display, as the stacked cards do. */
6
+ export const view = (config, h) => {
7
+ const { columns, rows, isStacked = false } = config;
8
+ const role = (name) => (__foldkitBrandViewResult((isStacked ? [h.Role(name)] : []), "@gjermundgaraba-designs/simple-rat/src/table/table.js#role"));
9
+ return __foldkitBrandViewResult((h.div([h.Class("table-scroll")], [
10
+ h.table(role("table"), [
11
+ h.thead(role("rowgroup"), [
12
+ h.tr(role("row"), columns.map((column) => __foldkitBrandViewResult((h.keyed("th")(column, role("columnheader"), [column])), "@gjermundgaraba-designs/simple-rat/src/table/table.js#anonymous"))),
13
+ ]),
14
+ h.tbody(role("rowgroup"), rows.map((row) => __foldkitBrandViewResult((h.tr(role("row"), row.map((cell, index) => {
15
+ const label = columns[index];
16
+ return __foldkitBrandViewResult((h.td([
17
+ ...role("cell"),
18
+ ...(isStacked && label !== undefined ? [h.DataAttribute("label", label)] : []),
19
+ ], [cell])), "@gjermundgaraba-designs/simple-rat/src/table/table.js#anonymous~3");
20
+ }))), "@gjermundgaraba-designs/simple-rat/src/table/table.js#anonymous~2"))),
21
+ ]),
22
+ ])), "@gjermundgaraba-designs/simple-rat/src/table/table.js#view");
23
+ };
@@ -0,0 +1,128 @@
1
+ // Syntax colours for code blocks, by Shiki, for a build to run: the theme maps TextMate
2
+ // scopes to the --t-* tokens in tokens.css (Catppuccin: Latte in light, darkened for AA;
3
+ // Mocha in dark), so the colours switch with the page. It sits outside src/, which never
4
+ // imports it: a page is given the lines `highlight` made (vite.ts makes them of a file).
5
+ import { createHighlighterCoreSync, type ThemeRegistration } from "shiki/core";
6
+ import { createJavaScriptRawEngine } from "shiki/engine/javascript";
7
+ import css from "@shikijs/langs-precompiled/css";
8
+ import diff from "@shikijs/langs-precompiled/diff";
9
+ import html from "@shikijs/langs-precompiled/html";
10
+ import json from "@shikijs/langs-precompiled/json";
11
+ import shellscript from "@shikijs/langs-precompiled/shellscript";
12
+ import tsx from "@shikijs/langs-precompiled/tsx";
13
+
14
+ import type { Language, Lines } from "../src/code/code.ts";
15
+
16
+ const t = (token: string) => `var(--t-${token})`;
17
+
18
+ const codeTheme: ThemeRegistration = {
19
+ name: "simple-rat",
20
+ type: "light",
21
+ colors: { "editor.foreground": "var(--code-ink)", "editor.background": "var(--code-fill)" },
22
+ tokenColors: [
23
+ {
24
+ scope: ["comment", "punctuation.definition.comment", "meta.diff.header"],
25
+ settings: { foreground: t("com"), fontStyle: "italic" },
26
+ },
27
+ {
28
+ scope: [
29
+ "string",
30
+ "punctuation.definition.string",
31
+ "string.unquoted.argument",
32
+ "markup.inserted",
33
+ ],
34
+ settings: { foreground: t("str") },
35
+ },
36
+ {
37
+ scope: [
38
+ "constant.numeric",
39
+ "constant.language",
40
+ "constant.character",
41
+ "constant.other.color",
42
+ "keyword.other.unit",
43
+ "support.constant.property-value",
44
+ "variable.language.this",
45
+ ],
46
+ settings: { foreground: t("num") },
47
+ },
48
+ {
49
+ scope: [
50
+ "keyword",
51
+ "storage.type",
52
+ "storage.modifier",
53
+ "keyword.control.at-rule",
54
+ "support.function.misc.css",
55
+ "support.function.transform.css",
56
+ "meta.diff.range",
57
+ ],
58
+ settings: { foreground: t("kw") },
59
+ },
60
+ {
61
+ scope: [
62
+ "keyword.operator",
63
+ "punctuation.definition.tag",
64
+ "storage.type.function.arrow",
65
+ "punctuation.separator.pipe",
66
+ ],
67
+ settings: { foreground: t("op") },
68
+ },
69
+ {
70
+ scope: [
71
+ "entity.name.type",
72
+ "support.type.primitive",
73
+ "support.class",
74
+ "entity.other.inherited-class",
75
+ "entity.other.attribute-name",
76
+ "entity.name.tag.css",
77
+ "support.type.builtin",
78
+ ],
79
+ settings: { foreground: t("ty") },
80
+ },
81
+ {
82
+ scope: [
83
+ "entity.name.function",
84
+ "support.function",
85
+ "entity.name.tag",
86
+ "support.class.component",
87
+ "support.type.property-name",
88
+ "variable.css",
89
+ ],
90
+ settings: { foreground: t("fn") },
91
+ },
92
+ {
93
+ scope: ["entity.name.command", "support.function.builtin.shell"],
94
+ settings: { foreground: t("fn"), fontStyle: "italic" },
95
+ },
96
+ {
97
+ scope: [
98
+ "variable.other.property",
99
+ "variable.other.object.property",
100
+ "meta.object-literal.key",
101
+ "variable.argument.css",
102
+ ],
103
+ settings: { foreground: t("prop") },
104
+ },
105
+ { scope: "markup.deleted", settings: { foreground: t("del") } },
106
+ ],
107
+ };
108
+
109
+ // Built once, when a build first imports this file. The grammars come precompiled, so
110
+ // nothing translates their patterns. TypeScript is read with the TSX grammar.
111
+ const highlighter = createHighlighterCoreSync({
112
+ themes: [codeTheme],
113
+ langs: [css, diff, html, json, shellscript, tsx],
114
+ langAlias: { ts: "tsx" },
115
+ engine: createJavaScriptRawEngine(),
116
+ });
117
+
118
+ /** A block's runs, line by line, for `CodeBlock`'s `lines`. A run in the plain code ink,
119
+ upright, has no colour. */
120
+ export const highlight = (code: string, lang: Language): Lines =>
121
+ highlighter.codeToTokens(code, { lang, theme: "simple-rat" }).tokens.map((line) =>
122
+ line.map((token) => {
123
+ // Italic is bit 1 of Shiki's font style.
124
+ const isItalic = ((token.fontStyle ?? 0) & 1) === 1;
125
+ const isPlain = token.color === "var(--code-ink)" && !isItalic;
126
+ return { content: token.content, color: isPlain ? undefined : token.color, isItalic };
127
+ }),
128
+ );
@@ -0,0 +1,42 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { LANGUAGES, type Language } from "../src/code/code.ts";
4
+ import { highlight } from "./highlight.ts";
5
+
6
+ type Plugin = ReturnType<typeof import("@foldkit/vite-plugin").foldkitViewIdentity>;
7
+
8
+ // A file's path, then `lines=<lang>` anywhere in its query.
9
+ const LINES_IMPORT = /^([^?]+)\?(?:.*&)?lines=([^&]*)/;
10
+
11
+ const isLanguage = (lang: string): lang is Language => LANGUAGES.some((known) => known === lang);
12
+
13
+ /**
14
+ * Colours code while the site is built, so no page loads the highlighter. An import with the
15
+ * query `?lines=<lang>` (one of `LANGUAGES`) is the file's text and its coloured lines,
16
+ * which `CodeBlock` takes as `code` and `lines`:
17
+ *
18
+ * ```ts
19
+ * import retry from "./code/retry.txt?lines=ts";
20
+ *
21
+ * CodeBlock.view({ lang: "ts", ...retry, toParentMessage }, h);
22
+ * ```
23
+ *
24
+ * Add it to the build's plugins, beside `foldkit()`. The file is watched, so a change to it
25
+ * colours it again. A `.txt` holds code the toolchain should leave alone; a source file can
26
+ * be imported twice, to run and to show.
27
+ */
28
+ export const codeLines = (): Plugin => ({
29
+ name: "simple-rat:code-lines",
30
+ // Before Vite's own loaders, which would take a .txt for an asset.
31
+ enforce: "pre",
32
+ async load(id) {
33
+ const [, file, lang] = LINES_IMPORT.exec(id) ?? [];
34
+ if (file === undefined || lang === undefined) return null;
35
+ if (!isLanguage(lang)) {
36
+ return this.error(`simple-rat: ?lines=${lang} is none of ${LANGUAGES.join(", ")}`);
37
+ }
38
+ this.addWatchFile(file);
39
+ const code = await readFile(file, "utf8");
40
+ return `export default ${JSON.stringify({ code, lines: highlight(code, lang) })};`;
41
+ },
42
+ });
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@gjermundgaraba-designs/simple-rat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.7.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/gjermundgaraba/my-designs.git",
8
+ "directory": "designs/simple-rat"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "!src/**/*.test.ts",
13
+ "highlight",
14
+ "!highlight/**/*.test.ts",
15
+ "DESIGN.md",
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "sideEffects": [
20
+ "**/*.css"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/src/index.d.ts",
25
+ "default": "./dist/src/index.js"
26
+ },
27
+ "./scheme": {
28
+ "types": "./dist/src/scheme/script.d.ts",
29
+ "default": "./dist/src/scheme/script.js"
30
+ },
31
+ "./highlight": {
32
+ "types": "./dist/highlight/highlight.d.ts",
33
+ "default": "./dist/highlight/highlight.js"
34
+ },
35
+ "./highlight/vite": {
36
+ "types": "./dist/highlight/vite.d.ts",
37
+ "default": "./dist/highlight/vite.js"
38
+ },
39
+ "./tokens.css": "./src/tokens.css",
40
+ "./simple-rat.css": "./src/simple-rat.css"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@shikijs/langs-precompiled": "^4.4.3",
47
+ "shiki": "^4.4.3"
48
+ },
49
+ "devDependencies": {
50
+ "@effect/platform-browser": "4.0.2",
51
+ "@foldkit/ui": "0.168.0",
52
+ "@foldkit/vite-plugin": "0.27.1",
53
+ "@my-designs/specimen": "0.0.0",
54
+ "@types/node": "^26.6.2",
55
+ "effect": "4.0.2",
56
+ "foldkit": "0.168.0",
57
+ "happy-dom": "^20.14.5",
58
+ "typescript": "^7.0.2",
59
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
60
+ "vite-plus": "1.0.0"
61
+ },
62
+ "peerDependencies": {
63
+ "@effect/platform-browser": "^4.0.0",
64
+ "@foldkit/ui": "^0.168.0",
65
+ "@foldkit/vite-plugin": "^0.27.0",
66
+ "effect": "^4.0.0",
67
+ "foldkit": "^0.168.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "@foldkit/vite-plugin": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "scripts": {
75
+ "dev": "vp dev",
76
+ "preview": "vp preview"
77
+ }
6
78
  }
@@ -0,0 +1,23 @@
1
+ // The `?lines=<lang>` imports are declared for every program that reaches this file, which
2
+ // is every one that imports the package: an ambient declaration has no import to travel by.
3
+ // oxlint-disable-next-line typescript/triple-slash-reference
4
+ /// <reference path="./lines.d.ts" />
5
+
6
+ // A code block's colours as data, with no highlighter: what `CodeBlock` renders and what a
7
+ // build makes with `highlight` (highlight/highlight.ts), so a page carries the colours and
8
+ // not Shiki.
9
+
10
+ /** The languages a block can be coloured in. */
11
+ export const LANGUAGES = ["ts", "tsx", "sh", "json", "css", "html", "diff"] as const;
12
+ export type Language = (typeof LANGUAGES)[number];
13
+
14
+ /** One coloured run of a line: its text, the `--t-*` colour Shiki gave it, and whether it
15
+ is italic. */
16
+ type Run = Readonly<{ content: string; color: string | undefined; isItalic: boolean }>;
17
+
18
+ /** A block's runs, line by line. A run in the plain code ink, upright, has no colour. */
19
+ export type Lines = ReadonlyArray<ReadonlyArray<Run>>;
20
+
21
+ /** A file's text and its coloured lines: what an import with `?lines=<lang>` gives (see
22
+ lines.d.ts). */
23
+ export type Highlighted = Readonly<{ code: string; lines: Lines }>;
@@ -0,0 +1,2 @@
1
+ export { LANGUAGES } from "./code.ts";
2
+ export type { Highlighted, Language, Lines } from "./code.ts";
@@ -0,0 +1,39 @@
1
+ // A file imported with `?lines=<lang>`, one of `LANGUAGES`: its text, and its lines coloured
2
+ // at build by `codeLines()` (highlight/vite.ts, the package's `./highlight/vite`). code.ts
3
+ // refers to this file, so every program that imports the package has these declarations. A
4
+ // module pattern takes one wildcard, so there is a declaration for each language.
5
+
6
+ declare module "*?lines=ts" {
7
+ const highlighted: import("./code.ts").Highlighted;
8
+ export default highlighted;
9
+ }
10
+
11
+ declare module "*?lines=tsx" {
12
+ const highlighted: import("./code.ts").Highlighted;
13
+ export default highlighted;
14
+ }
15
+
16
+ declare module "*?lines=sh" {
17
+ const highlighted: import("./code.ts").Highlighted;
18
+ export default highlighted;
19
+ }
20
+
21
+ declare module "*?lines=json" {
22
+ const highlighted: import("./code.ts").Highlighted;
23
+ export default highlighted;
24
+ }
25
+
26
+ declare module "*?lines=css" {
27
+ const highlighted: import("./code.ts").Highlighted;
28
+ export default highlighted;
29
+ }
30
+
31
+ declare module "*?lines=html" {
32
+ const highlighted: import("./code.ts").Highlighted;
33
+ export default highlighted;
34
+ }
35
+
36
+ declare module "*?lines=diff" {
37
+ const highlighted: import("./code.ts").Highlighted;
38
+ export default highlighted;
39
+ }
@@ -0,0 +1,83 @@
1
+ import { Option } from "effect";
2
+ import type { Html, HtmlBuilder } from "foldkit/html";
3
+ import * as Mount from "foldkit/mount";
4
+
5
+ import type { Language, Lines } from "../code/code.ts";
6
+ import type { ViewInputs as CopyInputs } from "../copyTab/copyTab.ts";
7
+ import * as Scrolls from "../scrolls/scrolls.ts";
8
+
9
+ /** Configuration for rendering a code block with `view`. */
10
+ export type ViewConfig<ParentMessage> = Readonly<{
11
+ code: string;
12
+ /** The code's language, named on the `code` element. A block without one, with a copy
13
+ tab, is a prose block that wraps at text size, like a prompt. */
14
+ lang?: Language;
15
+ /** The code's coloured lines, made at build: an import with `?lines=<lang>` gives them
16
+ with the `code` (the package's `./highlight/vite`), or `highlight` makes them (its
17
+ `./highlight`). Without them the block is plain. */
18
+ lines?: Lines;
19
+ /** A file name, shown in a tab on the block's top-left edge. */
20
+ file?: string;
21
+ /** A `copy` tab on the block's top-right edge: the page's `CopyTab`, rendered with
22
+ `copyInputs`. The tab wraps the block, so it is given the block's view to render as its
23
+ `toBlockView`. */
24
+ copy?: ((toBlockView: () => Html) => Html) | undefined;
25
+ /** The overflow Mount's report, lifted into the page's Messages, which may ignore it. */
26
+ toParentMessage: (message: Scrolls.Message) => ParentMessage;
27
+ }>;
28
+
29
+ /** The block's name for assistive technology: its file, or its first line. */
30
+ export const nameOf = (code: string, file?: string): string => {
31
+ const firstLine = code.trim().split("\n")[0] ?? "";
32
+ return file ?? (firstLine.length > 40 ? `${firstLine.slice(0, 40)}…` : firstLine);
33
+ };
34
+
35
+ /** What a block's `CopyTab` is given: the block's name and its code, without a trailing
36
+ newline. */
37
+ export const copyInputs = (code: string, file?: string): Omit<CopyInputs, "toBlockView"> => ({
38
+ name: nameOf(code, file),
39
+ text: code.trimEnd(),
40
+ });
41
+
42
+ const linesView = <Message>(lines: Lines, h: HtmlBuilder<Message>): ReadonlyArray<Html | string> =>
43
+ lines.flatMap((line, index) => [
44
+ ...(index > 0 ? ["\n"] : []),
45
+ ...line.map((run) =>
46
+ run.color === undefined
47
+ ? run.content
48
+ : h.span(
49
+ [h.Style({ color: run.color, ...(run.isItalic ? { fontStyle: "italic" } : {}) })],
50
+ [run.content],
51
+ ),
52
+ ),
53
+ ]);
54
+
55
+ /** A code block: coloured where it is given its lines, optionally with a file tab and a
56
+ copy tab. Only a block that overflows takes focus, through `Scrolls.WatchOverflow`. */
57
+ export const view = <ParentMessage>(
58
+ config: ViewConfig<ParentMessage>,
59
+ h: HtmlBuilder<ParentMessage>,
60
+ ): Html => {
61
+ const { code, lang, lines, file, copy, toParentMessage } = config;
62
+ const name = nameOf(code, file);
63
+ const toBlockView = (): Html =>
64
+ h.pre(
65
+ [
66
+ ...(copy === undefined ? [] : [h.DataAttribute("copy", "")]),
67
+ h.OnMount(
68
+ Mount.mapMessage(
69
+ Scrolls.WatchOverflow({ maybeName: Option.some(name) }),
70
+ toParentMessage,
71
+ ),
72
+ ),
73
+ ],
74
+ [
75
+ h.code(
76
+ lang === undefined ? [] : [h.Class(`language-${lang}`)],
77
+ lines === undefined ? [code] : linesView(lines, h),
78
+ ),
79
+ ],
80
+ );
81
+ const body = copy === undefined ? toBlockView() : copy(toBlockView);
82
+ return file === undefined ? body : h.figure([h.Class("file")], [h.figcaption([], [file]), body]);
83
+ };
@@ -0,0 +1,2 @@
1
+ export { copyInputs, view } from "./codeBlock.ts";
2
+ export type { ViewConfig } from "./codeBlock.ts";