@panaversity/ksor 0.0.31 → 0.0.32

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 CHANGED
@@ -1,5 +1,39 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.32
4
+
5
+ ### Patch Changes
6
+
7
+ - 658c496: One instruction per tool, as tabs.
8
+
9
+ A document that has to say the same thing two ways — one command for one agent,
10
+ another for another — can now put each in its own fenced block and give the
11
+ fence a `tab`:
12
+
13
+ ````markdown
14
+ ```bash tab="Claude Code" tab-group="agent"
15
+ curl -fsSL https://claude.ai/install.sh | bash
16
+ ```
17
+
18
+ ```bash tab="OpenCode" tab-group="agent"
19
+ curl -fsSL https://opencode.ai/install | bash
20
+ ```
21
+ ````
22
+
23
+ Consecutive blocks declaring a `tab` become one tab group. **This is still
24
+ CommonMark** — a fence's info string is free text, so any other markdown reader
25
+ shows both blocks one after another, correct and readable, just without the
26
+ picker. Nothing framework-shaped enters `knowledge/`.
27
+
28
+ `tab-group` is the part worth knowing: blocks sharing a group name switch
29
+ together across the whole page, and the choice is remembered for the reader's
30
+ next visit. A document with ten tabbed sections is one decision rather than ten.
31
+
32
+ A tool the site recognises takes its own colour and mark on its tab — Claude
33
+ Code and OpenCode ship known. Anything else renders in the site's own accent,
34
+ which is the right default for tabs that are `npm`/`pnpm` or `US`/`EU`. The list
35
+ lives in `system/site/app/global.css` and is yours to extend or delete.
36
+
3
37
  ## 0.0.31
4
38
 
5
39
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "description": "Knowledge System of Record — compile governed markdown into a static site for people and an MCP server for AI agents, with citations and measured abstention.",
5
5
  "keywords": [
6
6
  "abstention",
@@ -633,6 +633,35 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
633
633
  Declaring both `deck:` and `slides.url:` is refused — two presentations with
634
634
  nothing to say which one governs.
635
635
 
636
+ - **One instruction per tool, as tabs.** When a document has to say the same
637
+ thing two ways — one command for one agent, another for another — put each in
638
+ its own fenced block and give the fence a `tab`:
639
+
640
+ ````markdown
641
+ ```bash tab="Claude Code" tab-group="agent"
642
+ curl -fsSL https://claude.ai/install.sh | bash
643
+ ```
644
+
645
+ ```bash tab="OpenCode" tab-group="agent"
646
+ curl -fsSL https://opencode.ai/install | bash
647
+ ```
648
+ ````
649
+
650
+ Consecutive blocks that declare a `tab` become one tab group. This is still
651
+ CommonMark: a fence's info string is free text, so any other markdown reader
652
+ shows both blocks one after another — correct, just without the picker.
653
+
654
+ **`tab-group` is what makes the choice stick.** Blocks sharing a group name
655
+ switch together across the whole page and the choice is remembered for the
656
+ reader's next visit, so a document with ten tabbed sections is one decision
657
+ rather than ten. Leave it off and each group is independent, which is almost
658
+ never what you want. Use the same group name for the same axis throughout a
659
+ record.
660
+
661
+ A tool the site knows gets its own colour and mark on its tab; anything else
662
+ renders in the site's accent. The list lives in `system/site/app/global.css`
663
+ and is yours — adding a tool is one colour and one mark.
664
+
636
665
  - Copy load-bearing values (numbers, thresholds, dates) exactly from their
637
666
  source, and name the source in `provenance`.
638
667
 
@@ -274,6 +274,97 @@ html > body[data-scroll-locked] {
274
274
  --ksor-caution: #e0906e;
275
275
  }
276
276
 
277
+ /* A tool's own colour, across the whole block.
278
+
279
+ When a document gives one instruction per tool, the tab is the moment a
280
+ reader picks theirs, so the choice is worth being unmistakable. The colour
281
+ is not confined to the tab: the strip and the panel take a wash of it too,
282
+ in two different strengths, so the block says which tool you are reading at
283
+ any scroll position and the strip still reads as a distinct band.
284
+
285
+ Each tool declares ONE value and shared rules do the rest, so adding a tool
286
+ is a line rather than a set. `:has()` lifts the active tab's colour onto the
287
+ container, which is what lets the panel follow the tab.
288
+
289
+ Keyed on `data-tab-value`, which `components/mdx.tsx` puts on the trigger.
290
+ Radix also encodes the value in its generated id, but that is a private
291
+ format and keying on it would drop the branding silently if it changed.
292
+
293
+ THIS BLOCK IS YOURS. These are the tools this scaffold happened to know
294
+ about; a tab value matching nothing renders in the site's own accent, which
295
+ is the right default for a record whose tabs are npm/pnpm or US/EU. Add
296
+ your own, or delete the block — nothing else depends on it. Values are the
297
+ vendors' own. */
298
+ .prose [data-orientation]:has([data-tab-value="Claude Code"][data-state="active"]) {
299
+ --ksor-tool: #da7756;
300
+ }
301
+
302
+ .prose [data-orientation]:has([data-tab-value="OpenCode"][data-state="active"]) {
303
+ --ksor-tool: #6366f1;
304
+ }
305
+
306
+ /* A decorative mark per tool. Purely visual — the label beside it is the
307
+ accessible name — so it is CSS content rather than markup an author would
308
+ have to write. */
309
+ .prose [role="tab"][data-tab-value="Claude Code"]::before {
310
+ content: "\2726";
311
+ }
312
+
313
+ .prose [role="tab"][data-tab-value="OpenCode"]::before {
314
+ content: "</>";
315
+ font-size: 0.85em;
316
+ letter-spacing: -0.05em;
317
+ }
318
+
319
+ /* Everything below is shared, and applies only where a tool was recognised. */
320
+ .prose [data-orientation]:has([data-tab-value][data-state="active"]) {
321
+ background-color: color-mix(in oklab, var(--ksor-tool) 7%, transparent);
322
+ border-color: color-mix(in oklab, var(--ksor-tool) 28%, transparent);
323
+ overflow: hidden;
324
+ }
325
+
326
+ /* The strip is a BAND: stronger than the panel, full width, and the tabs sit
327
+ inside it rather than floating above it. */
328
+ .prose [data-orientation]:has([data-tab-value][data-state="active"]) > [role="tablist"] {
329
+ background-color: color-mix(in oklab, var(--ksor-tool) 20%, transparent);
330
+ border-bottom: 1px solid color-mix(in oklab, var(--ksor-tool) 26%, transparent);
331
+ padding: 0.5rem;
332
+ gap: 0.25rem;
333
+ }
334
+
335
+ .prose [role="tab"][data-tab-value] {
336
+ display: inline-flex;
337
+ align-items: center;
338
+ gap: 0.5rem;
339
+ /* Big enough to be a target and to read as a block rather than a word. */
340
+ padding: 0.7rem 1.5rem;
341
+ font-size: 0.95rem;
342
+ font-weight: 600;
343
+ border-radius: 0.25rem;
344
+ }
345
+
346
+ .prose [role="tab"][data-tab-value][data-state="active"] {
347
+ background-color: var(--ksor-tool);
348
+ color: #fff;
349
+ }
350
+
351
+ .prose [role="tab"][data-tab-value][data-state="inactive"]:hover {
352
+ background-color: color-mix(in oklab, var(--ksor-tool) 14%, transparent);
353
+ }
354
+
355
+ /* The code sits INSET on the wash, with its own surface, so the panel reads as
356
+ a frame around it. A tinted block on a tinted panel is one colour with words
357
+ in it, and the code is the part somebody actually has to read. */
358
+ .prose [data-orientation]:has([data-tab-value][data-state="active"]) figure {
359
+ margin: 1.25rem;
360
+ background-color: var(--card);
361
+ border-color: color-mix(in oklab, var(--ksor-tool) 18%, transparent);
362
+ }
363
+
364
+ .dark .prose [data-orientation]:has([data-tab-value][data-state="active"]) figure {
365
+ background-color: color-mix(in oklab, var(--muted) 88%, #000);
366
+ }
367
+
277
368
  /* The slide STAGE.
278
369
 
279
370
  A slide is a projected surface, and reading as one at a glance is most of
@@ -1,9 +1,37 @@
1
1
  import defaultMdxComponents from "fumadocs-ui/mdx";
2
+ import { CodeBlockTabsTrigger } from "fumadocs-ui/components/codeblock";
3
+ import { Tab, Tabs } from "fumadocs-ui/components/tabs";
2
4
  import type { MDXComponents } from "mdx/types";
5
+ import type * as React from "react";
6
+
7
+ /**
8
+ * A tab trigger that says WHICH tab it is, in an attribute we own.
9
+ *
10
+ * Radix already encodes the value in its generated `id`
11
+ * (`radix-…-trigger-Claude Code`), and styling could key on that — but that is
12
+ * a private format, so a change upstream would drop the branding silently and
13
+ * nothing would go red. One attribute of our own costs a few lines and cannot
14
+ * be taken away.
15
+ *
16
+ * `app/global.css` uses it to give a known tool its own colour; a tab value it
17
+ * does not recognise simply renders in the site's own accent.
18
+ */
19
+ function BrandedTabsTrigger({
20
+ value,
21
+ ...props
22
+ }: React.ComponentProps<typeof CodeBlockTabsTrigger>): React.ReactElement {
23
+ return <CodeBlockTabsTrigger data-tab-value={value} value={value} {...props} />;
24
+ }
3
25
 
4
26
  export function getMDXComponents(components?: MDXComponents) {
5
27
  return {
6
28
  ...defaultMdxComponents,
29
+ // `remarkCodeTab` (source.config.ts) rewrites consecutive fenced blocks
30
+ // that declare `tab="…"` into these, so they have to be in the map or the
31
+ // build fails on an unknown component rather than at authoring time.
32
+ Tabs,
33
+ Tab,
34
+ CodeBlockTabsTrigger: BrandedTabsTrigger,
7
35
  ...components,
8
36
  } satisfies MDXComponents;
9
37
  }
@@ -1,4 +1,5 @@
1
1
  import { defineCollections, defineConfig, defineDocs } from "fumadocs-mdx/config";
2
+ import { remarkCodeTab } from "fumadocs-core/mdx-plugins/remark-code-tab";
2
3
  import { metaSchema, pageSchema } from "fumadocs-core/source/schema";
3
4
  import { z } from "zod";
4
5
  import { DeckSchema } from "./lib/deck";
@@ -114,6 +115,28 @@ export const slides = defineCollections({
114
115
 
115
116
  export default defineConfig({
116
117
  mdxOptions: {
117
- // MDX options
118
+ /**
119
+ * Alternative versions of the same instruction, as TABS.
120
+ *
121
+ * A record often has to say the same thing twice — one way for one tool,
122
+ * one for another — and stacking both is how a reader follows the wrong
123
+ * one. `remarkCodeTab` turns consecutive fenced blocks that declare a
124
+ * `tab="…"` into a tab group.
125
+ *
126
+ * The reason this works HERE, where a JSX `<Tabs>` cannot: a fence's info
127
+ * string is free text in CommonMark. `\`\`\`bash tab="Claude Code"` is a
128
+ * perfectly ordinary bash block to every other markdown reader, which sees
129
+ * both blocks one after another and is not misled — it just does not get
130
+ * to pick. So the record stays framework-free (critical rule 2) and the
131
+ * site still renders the affordance.
132
+ *
133
+ * `CodeBlockTabs` rather than `Tabs`, and the difference is not cosmetic:
134
+ * only that branch honours `tab-group`, which is what makes ONE choice
135
+ * apply to every group on the page and persist to the next visit. The
136
+ * `Tabs` branch drops the attribute silently, so a reader with a
137
+ * ten-section document would pick their tool ten times (verified against
138
+ * fumadocs-core 16.14.5, remark-code-tab.js).
139
+ */
140
+ remarkPlugins: [[remarkCodeTab, { Tabs: "CodeBlockTabs" }]],
118
141
  },
119
142
  });