@badliveware/pi-footer-framework 0.1.1 → 0.2.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.2.1
4
+
5
+ - Fixed footer rendering with cell-buffer composition so ANSI styling, OSC8 hyperlinks, grapheme clusters, wide characters, and overlays preserve terminal cell alignment.
6
+ - Improved diagnostics for rendered footer layout and right/center overlay behavior.
7
+ - Hardened footer line clearing so overwriting wide-character runs does not leave stale continuation cells.
8
+
9
+ ## 0.2.0
10
+
11
+ - Added TypeScript render config support.
12
+ - Simplified footer framework configuration and generalized framework-owned layout behavior.
13
+ - Added adapter templates and built-in footer data source adaptation.
14
+
15
+ ## 0.1.1
16
+
17
+ - Initial public package release.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # pi-footer-framework
2
2
 
3
- Configurable footer replacement for Pi. It gives you a stable two-line footer and lets you choose which status items appear where.
3
+ Configurable footer replacement for Pi. It owns footer layout, formatting, color, truncation, and placement while other extensions publish structured status/data for it to render.
4
4
 
5
- Use it when the default footer is too cramped, when you want context/model/branch/PR state in predictable places, or when other extensions need a shared place to publish compact status items.
5
+ Use it when you want to customize Pi's footer through a configurable framework that agents can inspect and reconfigure from natural-language prompts.
6
6
 
7
7
  ## Install
8
8
 
@@ -18,31 +18,228 @@ No external services or credentials are required.
18
18
  /footerfx on
19
19
  /footerfx item context line 1
20
20
  /footerfx item context after model
21
- /footerfx gap 1 10
21
+ /footerfx item pr line 3
22
+ /footerfx anchor all right
22
23
  /footerfx save user
23
24
  ```
24
25
 
25
- The extension replaces the default footer only while enabled. Disable it with:
26
+ The default layout uses two footer lines. If you place an item on another positive line number, the footer grows to include that line. Disable the replacement footer with:
26
27
 
27
28
  ```text
28
29
  /footerfx off
29
30
  ```
30
31
 
31
- ## What it shows
32
+ ## Showcase
32
33
 
33
- Built-in items include:
34
+ This is a real Pi terminal using TS render closures, theme-aware styles, right-anchored layout, responsive column placement, PR state, and an adapted extension status item:
34
35
 
35
- - `cwd`
36
- - `model` with thinking level
37
- - `branch`
38
- - `stats`
39
- - `context` usage, such as `ctx 52.2% 142K/272K`
40
- - `pr` state from compatible PR extensions
41
- - `ext` status text from other extensions
36
+ ![Footer framework showcase with cwd, PR, model, token stats, context, and watchdog status](assets/footer-framework-showcase.png)
42
37
 
43
- Items can be shown/hidden and positioned by line, left/right zone, relative order, or fixed column.
38
+ The screenshot demonstrates:
44
39
 
45
- ## Configuration
40
+ - combined render output: cwd plus branch/PR label in one item
41
+ - token-level styling: labels, model id, thinking level, token stats, context, and cost use different Pi theme colors/attributes
42
+ - built-in Pi data: `cwd`, `branch`, `model`, `stats`, `context`, and `pr`
43
+ - extension status adaptation: `compaction-continue` status becomes `watchdog:on`
44
+ - flexible placement: line 1 and line 2 are right-anchored, while line 3 uses responsive `column` placement for middle and center-right items
45
+
46
+ <details>
47
+ <summary>Config used for the screenshot</summary>
48
+
49
+ Save this as `~/.pi/agent/footer-framework.config.ts`. The same file ships as `examples/footer-framework.config.ts` in the npm package.
50
+
51
+ ```ts
52
+ import type { FooterFrameworkConfig } from "@badliveware/pi-footer-framework";
53
+
54
+ function shortPath(value: string, maxWidth = 48, tailSegments = 2): string {
55
+ const normalized = value.replace(/^\/home\/[^/]+/, "~");
56
+ const prefix = normalized.startsWith("~/") ? "~/" : normalized.startsWith("/") ? "/" : "";
57
+ const parts = normalized.slice(prefix.length).split("/").filter(Boolean);
58
+ const compact = parts.length > tailSegments ? `${prefix}…/${parts.slice(-tailSegments).join("/")}` : normalized;
59
+ return compact.length > maxWidth ? `…${compact.slice(-(maxWidth - 1))}` : compact;
60
+ }
61
+
62
+ const config = {
63
+ enabled: true,
64
+ lineAnchors: { 1: "right", 2: "right", 3: "left" },
65
+ minGap: 2,
66
+ maxGap: 24,
67
+ items: {
68
+ branch: { visible: false },
69
+ ext: { visible: false },
70
+ cwd: {
71
+ visible: true,
72
+ line: 1,
73
+ zone: "left",
74
+ order: 10,
75
+ render: ({ pi, span, fn }) => [
76
+ span("cwd", "muted"),
77
+ " ",
78
+ span(shortPath(pi.cwd.trim(), 48, 2), "dim"),
79
+ span(" · ", "muted"),
80
+ span(fn.truncate(pi.branch?.label ?? "", 22), "accent"),
81
+ ],
82
+ },
83
+ model: {
84
+ visible: true,
85
+ line: 1,
86
+ zone: "right",
87
+ order: 10,
88
+ render: ({ pi, span }) => [
89
+ span("model:", "muted"),
90
+ span(pi.model.id ?? "no-model", "accent"),
91
+ span("/", "muted"),
92
+ span(pi.model.thinking ?? "", "thinkingXhigh,bold"),
93
+ ],
94
+ },
95
+ stats: {
96
+ visible: true,
97
+ line: 2,
98
+ zone: "left",
99
+ order: 10,
100
+ render: ({ pi, span }) => [
101
+ span("↑", "dim"), span(pi.stats.inputText ?? "0", "dim"), " ",
102
+ span("↓", "dim"), span(pi.stats.outputText ?? "0", "dim"), " ",
103
+ span("$", "accent"), span(pi.stats.costText ?? "0.000", "success"),
104
+ ],
105
+ },
106
+ context: {
107
+ visible: true,
108
+ line: 3,
109
+ zone: "left",
110
+ order: 10,
111
+ column: "50%",
112
+ render: ({ pi, span }) => {
113
+ if (!pi.context) return undefined;
114
+ const tone = pi.context.tone ?? "muted";
115
+ return [span("ctx", tone), " ", span(pi.context.percentText ?? "?%", tone), " ", span(pi.context.tokenText ?? "?/?", tone)];
116
+ },
117
+ },
118
+ pr: {
119
+ visible: true,
120
+ line: 3,
121
+ zone: "left",
122
+ order: 20,
123
+ column: "66%",
124
+ render: ({ pi, span }) => {
125
+ if (!pi.pr) return undefined;
126
+ return [span("PR ", "muted"), span(pi.pr.checkGlyph ?? "•", pi.pr.checkTone ?? "muted"), span(pi.pr.commentsText ?? "", "muted")];
127
+ },
128
+ },
129
+ },
130
+ adapters: {
131
+ watchdog: {
132
+ source: "extensionStatus",
133
+ key: "compaction-continue",
134
+ itemId: "watchdog",
135
+ match: "(on|off)",
136
+ group: 1,
137
+ urlPath: "url",
138
+ placement: { visible: true, line: 2, zone: "right", order: 20 },
139
+ render: ({ value, span }) => [span("watchdog:", "muted"), span(value ?? "", "accent,bold")],
140
+ },
141
+ },
142
+ } satisfies FooterFrameworkConfig;
143
+
144
+ export default config;
145
+ ```
146
+
147
+ String `column` positions such as `"50%"`, `"66%"`, and `"center"` are resolved from the current terminal width, so they keep their relative position after resize. Numeric columns remain fixed absolute terminal columns.
148
+
149
+ </details>
150
+
151
+ ## How it works
152
+
153
+ Footer-framework renders normalized footer items from adapter mappings, direct TS/JS item renderers, and extension-published items. Adapter mappings can read three source types:
154
+
155
+ | Source | Use it for |
156
+ | --- | --- |
157
+ | `pi` | Built-in Pi/session/footer data such as `cwd`, `model`, `stats`, `context`, `branch`, `pr`, and `extensionStatuses`. |
158
+ | `extensionStatus` | Existing `ctx.ui.setStatus()` footer/status entries from other extensions. |
159
+ | `sessionEntry` | The latest custom session entry written by an extension with `pi.appendEntry()`. |
160
+
161
+ The built-in footer items (`cwd`, `model`, `branch`, `stats`, `context`, `pr`) are regular default adapters unless you replace them with `items.<id>.render` in TS/JS config. User/project config overrides built-in defaults and producer hints.
162
+
163
+ Agents can inspect concise footer-relevant data with `footer_framework_sources`, then add adapter rules with `footer_framework_adapter_config` or adjust layout with `footer_framework_config`. Runtime metadata such as tools, commands, skills, descriptions, and `sourceInfo` is opt-in via `includeTools`, `includeCommands`, `includeSkills`, and `includeDetails`.
164
+
165
+ ## Templates and styles
166
+
167
+ Adapters can render with a restricted Liquid-style interpolation subset:
168
+
169
+ ```liquid
170
+ {{ pi.stats.costText }}
171
+ {{ "EUR" }}
172
+ {{ "EUR" | style: "accent" }}{{ pi.stats.costText | style: "success,bold" }}
173
+ ```
174
+
175
+ Quoted strings are literals. Unquoted terms are variables, so missing variables are reported as template diagnostics instead of being guessed as text. Diagnostics appear in `/footerfx-debug`, `footer_framework_state`, and `footer_framework_sources`.
176
+
177
+ Useful template context:
178
+
179
+ | Path | Meaning |
180
+ | --- | --- |
181
+ | `value`, `label`, `status`, `data`, `url` | The current adapter source. |
182
+ | `pi.cwd` | Current working directory. |
183
+ | `pi.model.id`, `pi.model.provider`, `pi.model.thinking` | Current model information. |
184
+ | `pi.stats.inputText`, `pi.stats.outputText`, `pi.stats.costText` | Formatted session token/cost stats. Raw numbers are `input`, `output`, and `cost`. |
185
+ | `pi.context.percentText`, `pi.context.tokenText`, `pi.context.tone` | Context usage and recommended tone. |
186
+ | `pi.branch.name`, `pi.branch.label` | Git branch values. Use `truncate` in templates when you want a shorter display. |
187
+ | `pi.pr.number`, `pi.pr.url`, `pi.pr.checkGlyph`, `pi.pr.checkTone`, `pi.pr.commentsText` | Pull request state when available. |
188
+
189
+ Supported filters:
190
+
191
+ | Filter | Example |
192
+ | --- | --- |
193
+ | `style` / `color` | `{{ value | style: "accent,bold" }}` |
194
+ | `bg` / `background` | `{{ value | bg: "toolSuccessBg" }}` |
195
+ | `bold`, `italic`, `underline`, `inverse`, `strikethrough` | `{{ value | underline }}` |
196
+ | `link` | `{{ pi.pr.number | link: pi.pr.url }}` |
197
+ | `truncate` | `{{ pi.branch.label | truncate: 22 }}` limits any value to 22 cells with an ellipsis. |
198
+ | `compactPath` | `{{ pi.cwd | compactPath: 48, 2 }}` keeps the last 2 path segments when the path is wider than 48 cells. |
199
+ | `default` | `{{ data.state | default: "unknown" }}` |
200
+
201
+ Style strings use Pi theme tokens and text attributes. Foreground examples: `accent`, `muted`, `dim`, `success`, `warning`, `error`, `text`, `mdLink`, `toolDiffAdded`, and the other Pi theme foreground tokens. Backgrounds use `bg:<token>`, such as `bg:toolSuccessBg`. Attributes are `bold`, `italic`, `underline`, `inverse`, and `strikethrough`.
202
+
203
+ ## TypeScript render config
204
+
205
+ For personal formatting logic, use a normal TS/JS config file with render closures. Footer-framework still owns data collection, layout, clipping, diagnostics, and final rendering; the closure only returns text/spans for one item.
206
+
207
+ ```ts
208
+ import type { FooterFrameworkConfig } from "@badliveware/pi-footer-framework";
209
+
210
+ function shortPath(value: string, maxWidth = 48, tailSegments = 2) {
211
+ const normalized = value.replace(/^\/home\/[^/]+/, "~");
212
+ const prefix = normalized.startsWith("~/") ? "~/" : normalized.startsWith("/") ? "/" : "";
213
+ const parts = normalized.slice(prefix.length).split("/").filter(Boolean);
214
+ const compact = parts.length > tailSegments ? `${prefix}…/${parts.slice(-tailSegments).join("/")}` : normalized;
215
+ return compact.length > maxWidth ? `…${compact.slice(-(maxWidth - 1))}` : compact;
216
+ }
217
+
218
+ export default {
219
+ items: {
220
+ branch: { visible: false },
221
+ ext: { visible: false },
222
+ cwd: {
223
+ line: 1,
224
+ zone: "left",
225
+ order: 10,
226
+ render: ({ pi, span, fn }) => [
227
+ span("cwd", "muted"),
228
+ " ",
229
+ span(shortPath(pi.cwd.trim(), 48, 2), "dim"),
230
+ span(" · ", "muted"),
231
+ span(fn.truncate(pi.branch?.label ?? "", 22), "accent"),
232
+ ],
233
+ },
234
+ },
235
+ } satisfies FooterFrameworkConfig;
236
+ ```
237
+
238
+ Render functions are synchronous and may return strings, spans, arrays, `null`, or `undefined`. Use `span(text, style, { url })` for token-level style/link metadata and `fn.text`, `fn.width`, `fn.truncate`, or `fn.compactPath` for footer-safe helpers. Adapter render functions also receive `value`, `label`, `status`, `data`, `url`, and `source`.
239
+
240
+ `/footerfx-debug`, `footer_framework_state`, and `footer_framework_sources` show which TS/JS renderers loaded plus each rendered item's final tokens, width, placement, and diagnostics. If a render closure throws or returns a promise, the footer item is skipped and a diagnostic is recorded.
241
+
242
+ ## Configuration files
46
243
 
47
244
  User settings persist to:
48
245
 
@@ -50,13 +247,21 @@ User settings persist to:
50
247
  ~/.pi/agent/footer-framework.json
51
248
  ```
52
249
 
250
+ Optional user TS/JS config files live next to it:
251
+
252
+ ```text
253
+ ~/.pi/agent/footer-framework.config.ts
254
+ ~/.pi/agent/footer-framework.config.js
255
+ ```
256
+
53
257
  Project settings can override them:
54
258
 
55
259
  ```text
56
260
  <project>/.pi/footer-framework.json
261
+ <project>/.pi/footer-framework.config.ts
57
262
  ```
58
263
 
59
- Use `/footerfx save project` only when you intentionally want a project-specific footer layout.
264
+ Load order is defaults, user TS/JS, user JSON, project TS/JS, then project JSON. `/footerfx` commands write JSON overrides; they do not rewrite TS/JS source. Use `/footerfx save project` only when you intentionally want a project-specific footer layout.
60
265
 
61
266
  ## Commands
62
267
 
@@ -64,42 +269,71 @@ Use `/footerfx save project` only when you intentionally want a project-specific
64
269
  | --- | --- |
65
270
  | `/footerfx` | Show current config and source. |
66
271
  | `/footerfx on` / `/footerfx off` | Enable or disable the replacement footer. |
272
+ | `/footerfx config` | Show loaded config source and config paths. |
67
273
  | `/footerfx load` | Reload user/project config files. |
68
274
  | `/footerfx save user` | Save current settings as the user default. |
69
275
  | `/footerfx save project` | Save current settings for the current project. |
70
276
  | `/footerfx reset` | Restore defaults and persist them to user config. |
277
+ | `/footerfx section <cwd|stats|context|model|branch|pr|ext> <on|off>` | Convenience alias for item visibility. |
71
278
  | `/footerfx item <id> <show|hide|reset>` | Control item visibility. |
72
- | `/footerfx item <id> line <1|2>` | Move an item to a footer line. |
279
+ | `/footerfx item <id> line <n>` / `row <n>` | Move an item to any positive footer line. |
73
280
  | `/footerfx item <id> zone <left|right>` | Move an item between left/right zones. |
74
- | `/footerfx item <id> before <other-id>` | Place an item before another item. |
75
- | `/footerfx item <id> after <other-id>` | Place an item after another item. |
76
- | `/footerfx item <id> column <n|off>` | Pin or unpin an item column. |
77
- | `/footerfx anchor <line1|line2|all> <gap|left|center|right|spread>` | Control line alignment. |
281
+ | `/footerfx item <id> before <other-id>` / `after <other-id>` | Place an item relative to another item. |
282
+ | `/footerfx item <id> column <n|center|middle|percent|off>` | Pin, center, percentage-place, or unpin an item. Percent examples: `50%`, `66%`. |
283
+ | `/footerfx anchor <line|all> <gap|left|center|right|spread>` | Control line alignment. `line3` and `3` both work. |
284
+ | `/footerfx adapter` | List configured adapters. |
285
+ | `/footerfx adapter <id> pi <source-key> [label]` | Adapt a built-in Pi source. |
286
+ | `/footerfx adapter <id> status <status-key> [label]` | Adapt an existing extension status key. |
287
+ | `/footerfx adapter <id> custom <custom-type> <path> [label]` | Adapt the latest matching custom session entry. |
288
+ | `/footerfx adapter <id> template <template>` | Set the adapter's render template. |
289
+ | `/footerfx adapter <id> empty-template <template>` | Set the template used for an empty adapter value. |
290
+ | `/footerfx adapter <id> style <style>` | Apply a default style to the rendered adapter text. |
291
+ | `/footerfx adapter <id> remove` | Remove an adapter. For built-ins, hide the item with `/footerfx item <id> hide`. |
78
292
  | `/footerfx gap <min> <max>` | Set spacing bounds. |
79
- | `/footerfx branch-width <n>` | Set max branch label width. |
80
- | `/footerfx-debug` | Show render snapshot, settings, and layout telemetry. |
293
+ | `/footerfx-debug` | Show render snapshot, settings, template/render diagnostics, loaded TS/JS renderers, and layout telemetry. |
81
294
 
82
295
  ## Agent tools
83
296
 
84
- The extension exposes two tools so agents can inspect and adjust the footer without asking you to run commands:
297
+ The extension exposes tools so agents can inspect and adjust the footer without asking you to run commands:
85
298
 
86
299
  - `footer_framework_state`
300
+ - `footer_framework_sources`
87
301
  - `footer_framework_config`
302
+ - `footer_framework_adapter_config`
88
303
 
89
- ## Extension item API
304
+ `footer_framework_sources` is concise by default. Pass `includeTools`, `includeCommands`, `includeSkills`, and `includeDetails` only when runtime metadata is directly useful.
90
305
 
91
- Other extensions can publish footer items through Pi's event bus:
306
+ ## Extension data API
307
+
308
+ Compatible extensions should publish data, not pre-rendered layout. The framework decides final text, color, position, and truncation. Producers may include hints, but user config wins.
92
309
 
93
310
  ```ts
94
311
  pi.events.emit("footer-framework:item", {
95
- id: "my-extension:status",
96
- text: "cache warm",
97
- placement: { line: 2, zone: "right", order: 50 }
312
+ id: "cache:status",
313
+ label: "cache",
314
+ value: "warm",
315
+ tone: "success",
316
+ data: { entries: 42 },
317
+ hint: {
318
+ icon: "◇",
319
+ format: "label-value",
320
+ placement: { line: 2, zone: "right", order: 50 }
321
+ }
98
322
  });
99
323
  ```
100
324
 
325
+ Legacy `text` and top-level `placement` fields still work for existing extensions, but new integrations should prefer `label`, `value`, `status`, `data`, and `hint`.
326
+
101
327
  Remove an item with:
102
328
 
103
329
  ```ts
104
- pi.events.emit("footer-framework:item", { id: "my-extension:status", remove: true });
330
+ pi.events.emit("footer-framework:item", { id: "cache:status", remove: true });
105
331
  ```
332
+
333
+ ## Troubleshooting
334
+
335
+ ### Blank space below the footer
336
+
337
+ If blank rows sometimes appear below the footer and disappear after you send a prompt, check `/footerfx-debug` or `footer_framework_state`. When `lastFooterSnapshot.lines` contains only the expected footer lines, the framework is not rendering extra rows. This is usually Pi's TUI viewport/differential rendering leaving unused terminal space below the last rendered component.
338
+
339
+ A Pi-side workaround is `terminal.clearOnShrink: true` in `~/.pi/agent/settings.json`, but that can add redraw flicker. Footer-framework does not change this setting.
@@ -0,0 +1,115 @@
1
+ import type { FooterFrameworkConfig } from "@badliveware/pi-footer-framework";
2
+
3
+ function shortPath(value: string, maxWidth = 48, tailSegments = 2): string {
4
+ const normalized = value.replace(/^\/home\/[^/]+/, "~");
5
+ const prefix = normalized.startsWith("~/") ? "~/" : normalized.startsWith("/") ? "/" : "";
6
+ const parts = normalized.slice(prefix.length).split("/").filter(Boolean);
7
+ const compact = parts.length > tailSegments ? `${prefix}…/${parts.slice(-tailSegments).join("/")}` : normalized;
8
+ return compact.length > maxWidth ? `…${compact.slice(-(maxWidth - 1))}` : compact;
9
+ }
10
+
11
+ const config = {
12
+ enabled: true,
13
+ lineAnchors: {
14
+ 1: "right",
15
+ 2: "right",
16
+ 3: "left",
17
+ },
18
+ minGap: 2,
19
+ maxGap: 24,
20
+ items: {
21
+ branch: { visible: false },
22
+ ext: { visible: false },
23
+ cwd: {
24
+ visible: true,
25
+ line: 1,
26
+ zone: "left",
27
+ order: 10,
28
+ render: ({ pi, span, fn }) => [
29
+ span("cwd", "muted"),
30
+ " ",
31
+ span(shortPath(pi.cwd.trim(), 48, 2), "dim"),
32
+ span(" · ", "muted"),
33
+ span(fn.truncate(pi.branch?.label ?? "", 22), "accent"),
34
+ ],
35
+ },
36
+ model: {
37
+ visible: true,
38
+ line: 1,
39
+ zone: "right",
40
+ order: 10,
41
+ render: ({ pi, span }) => [
42
+ span("model:", "muted"),
43
+ span(pi.model.id ?? "no-model", "accent"),
44
+ span("/", "muted"),
45
+ span(pi.model.thinking ?? "", "thinkingXhigh,bold"),
46
+ ],
47
+ },
48
+ stats: {
49
+ visible: true,
50
+ line: 2,
51
+ zone: "left",
52
+ order: 10,
53
+ render: ({ pi, span }) => [
54
+ span("↑", "dim"),
55
+ span(pi.stats.inputText ?? "0", "dim"),
56
+ " ",
57
+ span("↓", "dim"),
58
+ span(pi.stats.outputText ?? "0", "dim"),
59
+ " ",
60
+ span("$", "accent"),
61
+ span(pi.stats.costText ?? "0.000", "success"),
62
+ ],
63
+ },
64
+ context: {
65
+ visible: true,
66
+ line: 3,
67
+ zone: "left",
68
+ order: 10,
69
+ column: "50%",
70
+ render: ({ pi, span }) => {
71
+ if (!pi.context) return undefined;
72
+ const tone = pi.context.tone ?? "muted";
73
+ return [
74
+ span("ctx", tone),
75
+ " ",
76
+ span(pi.context.percentText ?? "?%", tone),
77
+ " ",
78
+ span(pi.context.tokenText ?? "?/?", tone),
79
+ ];
80
+ },
81
+ },
82
+ pr: {
83
+ visible: true,
84
+ line: 3,
85
+ zone: "left",
86
+ order: 20,
87
+ column: "66%",
88
+ render: ({ pi, span }) => {
89
+ if (!pi.pr) return undefined;
90
+ return [
91
+ span("PR ", "muted"),
92
+ span(pi.pr.checkGlyph ?? "•", pi.pr.checkTone ?? "muted"),
93
+ span(pi.pr.commentsText ?? "", "muted"),
94
+ ];
95
+ },
96
+ },
97
+ },
98
+ adapters: {
99
+ watchdog: {
100
+ source: "extensionStatus",
101
+ key: "compaction-continue",
102
+ itemId: "watchdog",
103
+ match: "(on|off)",
104
+ group: 1,
105
+ urlPath: "url",
106
+ placement: { visible: true, line: 2, zone: "right", order: 20 },
107
+ render: ({ value, span }) => [
108
+ span("watchdog:", "muted"),
109
+ span(value ?? "", "accent,bold"),
110
+ ],
111
+ },
112
+ },
113
+ } satisfies FooterFrameworkConfig;
114
+
115
+ export default config;