@stemtrooper/learningcode 0.2.1 → 0.3.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.
package/README.md CHANGED
@@ -66,12 +66,23 @@ The LEARNINGCODE block banner replaces Pi's header at startup:
66
66
  The figlet "ANSI Shadow" face, 97 columns wide, kept verbatim because the
67
67
  double-line box characters only align if every row keeps its exact offset.
68
68
 
69
- Colour comes from the active theme, so it stays legible in light and dark.
69
+ It steps down through three tiers, because a phone will never have 97 columns and
70
+ clipping box characters looks like a rendering bug rather than a design:
71
+
72
+ | Terminal width | Shows |
73
+ |---|---|
74
+ | 99 or more | the figlet face above |
75
+ | 51 to 98 | a condensed 4 row face, 47 columns |
76
+ | under 51 | the wordmark |
70
77
 
71
- **It needs a 99 column terminal.** Below that it collapses to a wordmark,
72
- rather than drawing art that would be clipped into something that looks broken.
73
- An 80 column terminal will show the compact form, so widen the window or reduce
74
- the art.
78
+ The condensed tier still spells LEARNINGCODE:
79
+
80
+ \\n █ ███ █ ███ █ █ ███ █ █ ███ ███ ███ ███ ███
81
+ █ █ █ █ █ █ ███ █ ███ █ █ █ █ █ █ █
82
+ █ ███ ███ ███ █ █ █ █ █ █ █ █ █ █ █ █ ███
83
+ ███ ███ █ █ █ █ █ █ ███ █ █ ███ ███ ███ ███ ███
84
+ \\n
85
+ Colour comes from the active theme, so it stays legible in light and dark.
75
86
 
76
87
  It installs via `ctx.ui.setHeader`, the supported way to brand a fork.
77
88
 
@@ -5,22 +5,25 @@ import { foregroundAnsi, rgbColor } from "@earendil-works/pi-tui";
5
5
  * LEARNINGCODE startup banner.
6
6
  *
7
7
  * Pi lets an extension replace the whole header, which is the supported way to
8
- * brand a fork without touching Pi's internals. The built-in header is Pi's own
9
- * logo plus key hints; we trade that for the TLC banner and a line of slash
10
- * commands, so nothing is claimed about keybindings we cannot read.
8
+ * brand a fork without touching Pi's internals.
11
9
  *
12
- * The art is the figlet "ANSI Shadow" face, kept verbatim rather than rebuilt from
13
- * a glyph map: the double-line box characters only line up if every row keeps
14
- * its exact offset, and a one-space drift makes the whole thing look broken.
10
+ * Three tiers, because the full face does not fit everywhere. A phone will never
11
+ * have 97 columns, and clipping box characters looks like a rendering bug rather
12
+ * than a design, so the banner steps down instead:
15
13
  *
16
- * Rendering uses the active theme's colour tokens (`accent`, `border`, `dim`,
17
- * `muted`) so the banner stays legible in light and dark terminals instead of
18
- * hard-coding colours that break on one of them.
14
+ * >= 99 columns figlet "ANSI Shadow", 97 wide, 6 rows
15
+ * >= 51 columns condensed 4 row face, 47 wide
16
+ * below that the wordmark
17
+ *
18
+ * The full art is stored verbatim rather than rebuilt from a glyph map. That face
19
+ * only aligns because every row keeps its exact offset, the top row flush left
20
+ * and the rest flush too; a one-space drift makes the whole banner look broken.
21
+ * A test asserts those offsets.
19
22
  */
20
23
 
21
24
  /**
22
- * Only the parts of Pi's Theme this file touches, to keep the runtime import
23
- * down to the two colour helpers it actually needs.
25
+ * Only the parts of Pi's Theme this file touches, to keep the runtime import down
26
+ * to the two colour helpers actually needed.
24
27
  */
25
28
  type ThemeLike = {
26
29
  fg(token: string, text: string): string;
@@ -45,14 +48,20 @@ type HeaderComponent = { render(width: number): string[] };
45
48
  *
46
49
  * Note this must not go through `theme.fg()`. That resolves theme *tokens*, and
47
50
  * `Theme.tokenAnsi` throws `Unknown theme color: #29c8f2` for anything that is
48
- * not one, so a raw hex there crashes the header at render time. `foregroundAnsi`
49
- * takes a colour value directly, and unlike a hand-rolled escape it still
50
- * degrades to 256 colour on terminals without truecolour support.
51
+ * not one, so a raw hex there crashes the header at render time.
52
+ * `foregroundAnsi` takes a colour value directly, and unlike a hand-rolled escape
53
+ * it still degrades to 256 colour on terminals without truecolour support.
51
54
  */
52
55
  const CYAN_DARK_BG = rgbColor(0x29, 0xc8, 0xf2);
53
56
  const CYAN_LIGHT_BG = rgbColor(0x0d, 0xac, 0xd6);
54
57
  const RESET = "\x1b[0m";
55
58
 
59
+ const WORD = "LEARNINGCODE";
60
+ const PAD = " ";
61
+ const TAGLINE = "The Learning Curve · Sarawak";
62
+ const HINTS = "/help commands · /quota today's spend · /hotkeys keys";
63
+
64
+ /** figlet "ANSI Shadow", verbatim. 97 columns, 6 rows. */
56
65
  const ART: readonly string[] = [
57
66
  "██╗ ███████╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
58
67
  "██║ ██╔════╝██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
@@ -62,26 +71,47 @@ const ART: readonly string[] = [
62
71
  "╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
63
72
  ];
64
73
 
65
- const TAGLINE = "The Learning Curve · Sarawak";
66
- const HINTS = "/help commands · /quota today's spend · /hotkeys keys";
74
+ /**
75
+ * Condensed fallback for narrow terminals. Three columns per glyph, which is
76
+ * the most a twelve letter word can be compressed before the letterforms stop
77
+ * reading: an N needs four columns to show its diagonal, and dropping it to
78
+ * three turns the letter into a filled block.
79
+ */
80
+ const CONDENSED_GLYPHS: Record<string, readonly string[]> = {
81
+ L: ["█ ", "█ ", "█ ", "███"],
82
+ E: ["███", "█ ", "███", "███"],
83
+ A: [" █ ", "█ █", "███", "█ █"],
84
+ R: ["███", "█ █", "███", "█ █"],
85
+ N: ["█ █", "███", "█ █", "█ █"],
86
+ I: ["███", " █ ", " █ ", "███"],
87
+ G: ["███", "█ ", "█ █", "███"],
88
+ C: ["███", "█ ", "█ ", "███"],
89
+ O: ["███", "█ █", "█ █", "███"],
90
+ D: ["███", "█ █", "█ █", "███"],
91
+ };
67
92
 
68
- const PAD = " ";
93
+ const condensedArt = (): string[] => {
94
+ const rows = CONDENSED_GLYPHS[WORD[0]].length;
95
+ return Array.from({ length: rows }, (_, row) =>
96
+ [...WORD].map((letter) => CONDENSED_GLYPHS[letter][row]).join(" ").trimEnd(),
97
+ );
98
+ };
69
99
 
70
100
  /** A copy, so a caller mutating the result cannot corrupt later renders. */
71
101
  export const bannerArt = (): string[] => [...ART];
72
102
 
103
+ export const condensedArt_ = condensedArt;
104
+
73
105
  export const bannerWidth = (): number => Math.max(...ART.map((line) => line.length));
106
+ export const condensedWidth = (): number => Math.max(...condensedArt().map((line) => line.length));
74
107
 
75
- /**
76
- * The art is 97 columns, which does not fit the classic 80 column terminal.
77
- * Below the art plus its indent we show a wordmark instead, because clipped box
78
- * characters look like a rendering bug rather than a design.
79
- */
108
+ /** Art plus its indent. Derived, never guessed. */
80
109
  const MIN_FULL_WIDTH = bannerWidth() + PAD.length;
110
+ const MIN_CONDENSED_WIDTH = condensedWidth() + PAD.length;
81
111
 
82
112
  export function createBanner(theme: ThemeLike): HeaderComponent {
83
- const art = bannerArt();
84
- const artWidth = bannerWidth();
113
+ const full = bannerArt();
114
+ const small = condensedArt();
85
115
 
86
116
  // Two different things, easy to swap by accident:
87
117
  // appearance light or dark, decides which brand cyan is readable
@@ -95,16 +125,23 @@ export function createBanner(theme: ThemeLike): HeaderComponent {
95
125
 
96
126
  return {
97
127
  render(width: number): string[] {
98
- if (width < MIN_FULL_WIDTH) {
99
- return ["", ink(PAD + "learningcode"), theme.fg("muted", PAD + TAGLINE)];
128
+ if (width >= MIN_FULL_WIDTH) {
129
+ const lines = ["", ...full.map((line) => ink(PAD + line))];
130
+ lines.push(theme.fg("border", PAD + "═".repeat(bannerWidth())));
131
+ lines.push(theme.fg("muted", PAD + TAGLINE));
132
+ lines.push(theme.fg("dim", PAD + HINTS));
133
+ lines.push("");
134
+ return lines;
135
+ }
136
+
137
+ if (width >= MIN_CONDENSED_WIDTH) {
138
+ const lines = ["", ...small.map((line) => ink(PAD + line))];
139
+ lines.push(theme.fg("muted", PAD + TAGLINE));
140
+ lines.push("");
141
+ return lines;
100
142
  }
101
143
 
102
- const lines = ["", ...art.map((line) => ink(PAD + line))];
103
- lines.push(theme.fg("border", PAD + "═".repeat(artWidth)));
104
- lines.push(theme.fg("muted", PAD + TAGLINE));
105
- lines.push(theme.fg("dim", PAD + HINTS));
106
- lines.push("");
107
- return lines;
144
+ return ["", ink(PAD + "learningcode"), theme.fg("muted", PAD + TAGLINE)];
108
145
  },
109
146
  };
110
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stemtrooper/learningcode",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "TLC Spark coding agent for students: Pi wired to the Spark OpenAI-compatible endpoint with per-student tokens, quota and seat-queue awareness.",
5
5
  "type": "module",
6
6
  "license": "MIT",