@unisim/sdk 0.168.0 → 0.169.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
@@ -627,7 +627,16 @@ invisible, keeps a private session, and nothing reports a problem. **Its
627
627
  apps** — the Java side keeps no second copy, it filters what the package manager
628
628
  admits to by the `.unisimsuiteauth` suffix. Adding an app to the suite means
629
629
  adding one line there. (The `<package>` list below it is a different list for a
630
- different job — see the switcher section.)
630
+ different job — see the switcher section. ⚠️ But a `<package>` entry makes the
631
+ WHOLE package visible, providers included, so an app on that list is reachable
632
+ for session sharing whether or not it is on the provider list — which is how
633
+ Ergo Assess's native app was reached before it was named there.)
634
+
635
+ **Ergo Assess is the one non-Capacitor member** (2026-09-29). Its Compose app
636
+ hosts a Kotlin port of `SuiteAuthProvider`/`SuiteAuthStore` under the same
637
+ permission and contract (`Ergo_Assess` `android/…/web/SuiteAuthStore.kt`) and
638
+ carries its own copy of the provider list in its manifest — so a new suite app
639
+ goes into both.
631
640
 
632
641
  ⚠️ **Same key, in practice, means the same BUILD TYPE.** Debug builds are signed
633
642
  with `~/.android/debug.keystore` and release builds with the upload key. Install
@@ -29,11 +29,16 @@
29
29
  Two Capacitor apps are deliberately absent, and neither is an oversight:
30
30
  Universal AI and Universal Family both run their own Supabase client on
31
31
  their own storage key by an explicit decision recorded in their source,
32
- so they have no shared session to take part in. Ergo Assess is absent for
33
- a different reason again: it is a native Compose app that ships no
34
- SuiteAuthProvider at all, so there is no authority to name. It carries
35
- the session in its own Keychain/KeyStore instead, which is why it appears
36
- in 2b below and not here.
32
+ so they have no shared session to take part in.
33
+
34
+ Ergo Assess IS here (2026-09-29), though it is not Capacitor: its native
35
+ Compose app hosts a Kotlin port of SuiteAuthProvider under the same
36
+ permission and contract (Ergo_Assess android/…/web/SuiteAuthStore.kt),
37
+ keeping the com.universalsimulation. prefix of its Gradle module. Before
38
+ this line it was already reachable by accident — its <package> entry in
39
+ 2b makes the whole package, providers included, visible — but sharing a
40
+ session must not hang on the switcher's list. Ergo's own manifest keeps
41
+ a copy of this list; add a new app there too.
37
42
 
38
43
  2b. The PACKAGE list: who the suite switcher can OPEN, via
39
44
  UnisimSuiteAuthPlugin.installedApps/openApp. Deliberately a separate
@@ -73,14 +78,14 @@
73
78
 
74
79
  <queries>
75
80
  <provider
76
- android:authorities="uk.co.unisim.coshhassess.unisimsuiteauth;uk.co.unisim.cyberassess.unisimsuiteauth;uk.co.unisim.fireassess.unisimsuiteauth;uk.co.unisim.riskassess.unisimsuiteauth;uk.co.unisim.wfhassess.unisimsuiteauth;uk.co.unisim.blackbook.unisimsuiteauth;uk.co.unisim.compress.unisimsuiteauth;uk.co.unisim.converter.unisimsuiteauth;uk.co.unisim.images.unisimsuiteauth;uk.co.unisim.jukebox.unisimsuiteauth;uk.co.unisim.pdf.unisimsuiteauth;uk.co.unisim.polling.unisimsuiteauth;uk.co.unisim.qr.unisimsuiteauth" />
81
+ android:authorities="uk.co.unisim.coshhassess.unisimsuiteauth;uk.co.unisim.cyberassess.unisimsuiteauth;uk.co.unisim.fireassess.unisimsuiteauth;uk.co.unisim.riskassess.unisimsuiteauth;uk.co.unisim.wfhassess.unisimsuiteauth;uk.co.unisim.blackbook.unisimsuiteauth;uk.co.unisim.compress.unisimsuiteauth;uk.co.unisim.converter.unisimsuiteauth;uk.co.unisim.images.unisimsuiteauth;uk.co.unisim.jukebox.unisimsuiteauth;uk.co.unisim.pdf.unisimsuiteauth;uk.co.unisim.polling.unisimsuiteauth;uk.co.unisim.qr.unisimsuiteauth;com.universalsimulation.ergoassesscapture.unisimsuiteauth" />
77
82
 
78
83
  <!-- Ergo Assess is the one non-Capacitor app here: a Compose app whose
79
84
  MainActivity already carries MAIN/LAUNCHER, which is all
80
85
  getLaunchIntentForPackage needs. It keeps the
81
- com.universalsimulation. prefix from its own Gradle module and is
82
- absent from the provider list above on purpose — it has no
83
- SuiteAuthProvider, so it shares no session. -->
86
+ com.universalsimulation. prefix from its own Gradle module. Since
87
+ 2026-09-29 it is in the provider list above as well: it hosts a
88
+ port of SuiteAuthProvider and shares the session. -->
84
89
  <package android:name="com.universalsimulation.ergoassesscapture" />
85
90
  <package android:name="uk.co.unisim.cyberassess" />
86
91
  <package android:name="uk.co.unisim.wfhassess" />
@@ -1,15 +1,37 @@
1
1
  import { type Language } from './i18n.js';
2
2
  import { type MenuTheme } from './menuTheme.js';
3
+ /** Where the suite's knowledge-base PDFs are served from. See note 4. */
4
+ export declare const KB_LIBRARY_URL = "https://opensource.unisim.co.uk/kb";
5
+ export type KnowledgeSourceKind = 'paper' | 'standard' | 'report' | 'guidance' | 'law';
6
+ /** A paper, standard or report an article draws on. */
7
+ export interface KnowledgeSource {
8
+ kind: KnowledgeSourceKind;
9
+ /** In the original language; titles of papers are not translated. */
10
+ title: string;
11
+ authors?: string;
12
+ /** Journal, conference or issuing body — "ACM CCS 2019", "NIST". */
13
+ publisher?: string;
14
+ year?: number;
15
+ /** The canonical free link: a DOI, the publisher's page or their own PDF. */
16
+ href: string;
17
+ /**
18
+ * Our hosted copy, relative to the library (`papers/fips-197-aes.pdf`) or
19
+ * absolute. Only when the licence permits redistribution (note 5).
20
+ */
21
+ pdf?: string;
22
+ /** The licence the hosted copy is shared under. Shown beside it. */
23
+ licence?: string;
24
+ }
3
25
  export interface KnowledgeArticle {
4
- /** Stable slug. Also what `initialArticle` matches. */
26
+ /** Stable slug. Also what `initialArticle` matches, and the guide PDF's name. */
5
27
  id: string;
6
28
  title: string;
7
- /** One line under the title in the list. */
29
+ /** One line under the title on its card. */
8
30
  summary?: string;
9
31
  /**
10
- * A heading the list groups articles under — "The basics", "Privacy and
32
+ * A heading the cards are grouped under — "The basics", "Privacy and
11
33
  * security". Articles keep their order; a group starts where its label
12
- * first appears. Leave unset on every article for a plain list.
34
+ * first appears. Leave unset on every article for one ungrouped grid.
13
35
  */
14
36
  group?: string;
15
37
  /**
@@ -17,22 +39,41 @@ export interface KnowledgeArticle {
17
39
  * `1. ` numbered steps and `**bold**`. Nothing else is interpreted.
18
40
  */
19
41
  body: string;
42
+ /** The research behind the article, shown beside it. Not translated. */
43
+ sources?: KnowledgeSource[];
20
44
  }
21
45
  export type KnowledgeArticles = KnowledgeArticle[] | ((language: Language) => KnowledgeArticle[] | Promise<KnowledgeArticle[]>);
22
46
  /** What an app hands the navbar (`knowledgeBase`) or <AdvancedMenu>. */
23
47
  export interface KnowledgeBaseConfig {
24
48
  articles: KnowledgeArticles;
49
+ /**
50
+ * The app's guide PDFs have been generated and published (see
51
+ * `scripts/kb-pdfs.mjs` in universal-platform). Turns on the "Download
52
+ * guide" buttons, which link to
53
+ * `<library>/<product>/<language>/<article id>.pdf`. Leave unset until
54
+ * they exist.
55
+ */
56
+ guides?: boolean;
57
+ /** Override the library's address. Tests and self-hosters only. */
58
+ library?: string;
25
59
  }
26
60
  export interface KnowledgeBaseDialogProps extends KnowledgeBaseConfig {
27
61
  open: boolean;
28
62
  onClose: () => void;
29
- /** Open straight onto this article rather than the list. */
63
+ /** Open straight onto this article rather than the cards. */
30
64
  initialArticle?: string;
31
65
  /** Heading beside the mark. Defaults to the product's name from the SDK. */
32
66
  name?: string;
33
67
  theme?: MenuTheme;
34
68
  }
35
- export declare function KnowledgeBaseDialog({ open, onClose, articles, initialArticle, name, theme, }: KnowledgeBaseDialogProps): import("react").ReactPortal | null;
69
+ /**
70
+ * The folder a language's guides are filed under. The generator writes one
71
+ * folder per translation the app has; both Englishes share `en`.
72
+ */
73
+ export declare function guideLanguage(language: string): string;
74
+ /** Resolve a source's `pdf` (library-relative or absolute) to a URL. */
75
+ export declare function libraryUrl(path: string, library?: string): string;
76
+ export declare function KnowledgeBaseDialog({ open, onClose, articles, guides, library, initialArticle, name, theme, }: KnowledgeBaseDialogProps): import("react").ReactPortal | null;
36
77
  type Block = {
37
78
  kind: 'h';
38
79
  text: string;
@@ -46,7 +87,7 @@ type Block = {
46
87
  kind: 'ol';
47
88
  items: string[];
48
89
  };
49
- /** Exported for the tests — splits an article body into blocks. */
90
+ /** Exported for the tests and the PDF generator — splits a body into blocks. */
50
91
  export declare function parseArticleBody(body: string): Block[];
51
92
  /** The book glyph for the menu row. Exported so <AdvancedMenu> can use it. */
52
93
  export declare function IconBook(): import("react/jsx-runtime").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"KnowledgeBaseDialog.d.ts","sourceRoot":"","sources":["../src/KnowledgeBaseDialog.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAK,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAqCrD,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,EAAE,GAClB,CAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,gBAAgB,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAA;AAE9E,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,iBAAiB,CAAA;CAC5B;AAED,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,4DAA4D;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,cAAc,EACd,IAAI,EACJ,KAAe,GAChB,EAAE,wBAAwB,sCAkH1B;AAqDD,KAAK,KAAK,GACN;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEnC,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,EAAE,CAkCtD;AAwDD,8EAA8E;AAC9E,wBAAgB,QAAQ,4CAQvB"}
1
+ {"version":3,"file":"KnowledgeBaseDialog.d.ts","sourceRoot":"","sources":["../src/KnowledgeBaseDialog.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAK,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAgErD,yEAAyE;AACzE,eAAO,MAAM,cAAc,uCAAuC,CAAA;AAElE,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,KAAK,CAAA;AAEtF,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAA;IACzB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;CAC5B;AAED,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,EAAE,GAClB,CAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,gBAAgB,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAA;AAE9E,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,iBAAiB,CAAA;IAC3B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB;AAID;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAiB,GAAG,MAAM,CAEzE;AAED,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,MAAc,EACd,OAAwB,EACxB,cAAc,EACd,IAAI,EACJ,KAAe,GAChB,EAAE,wBAAwB,sCAkJ1B;AAgND,KAAK,KAAK,GACN;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEnC,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,EAAE,CAkCtD;AAqED,8EAA8E;AAC9E,wBAAgB,QAAQ,4CAQvB"}
@@ -7,17 +7,91 @@ import { t } from './i18n.js';
7
7
  import { MENU } from './menuTheme.js';
8
8
  import { useLanguage, useUniversal } from './provider.js';
9
9
  import { lookupProduct } from './AboutAppDialog.js';
10
- export function KnowledgeBaseDialog({ open, onClose, articles, initialArticle, name, theme = 'light', }) {
10
+ // ──────────────────────────────────────────────────────────────────────────────
11
+ // <KnowledgeBaseDialog /> — an app's own knowledge base, read in the app.
12
+ //
13
+ // Owner ask, 2026-09-27: "introduce the knowledge base as per Ergo Assess but
14
+ // in the universal apps it should be more discreet e.g. in advanced in the
15
+ // actions pill" — explainers like "What actually is a PDF?" beside how the app
16
+ // itself works ("level of encryption for send and sign").
17
+ //
18
+ // Owner ask, 2026-09-29: "it should be more like the knowledge base on Ergo
19
+ // Assess with a knowledge base page, cards to click through and downloads
20
+ // available in PDFs. Research papers are the best if we can find." So the
21
+ // entry stays discreet (Advanced ▸ Knowledge base) but what opens is a page:
22
+ // a card grid, and an article view with its guide PDF and its research
23
+ // beside it — Ergo's /knowledge-base and /[method] in one dialog. Ergo-style
24
+ // whole-guide and download-library banners were built and taken out the same
25
+ // day at the owner's ask ("remove the download whole knowledge base and
26
+ // download library buttons"): each download lives on the article it belongs to.
27
+ //
28
+ // Five decisions worth keeping:
29
+ //
30
+ // 1. **A dialog, not a route.** Ergo's knowledge base is a set of Next.js
31
+ // pages. The Universal Apps are single-page Vite apps that also ship as
32
+ // Capacitor and Electron builds with no server behind them, so a route
33
+ // would mean nineteen routers and a knowledge base that 404s offline. The
34
+ // articles are bundled into the app and read here, which works in Airplane
35
+ // mode and needs nothing from the host but the words.
36
+ //
37
+ // 2. **The body is a tiny, closed markdown — never HTML.** Blank-line
38
+ // paragraphs, `## ` subheadings, `- ` bullets, `1. ` steps, `**bold**`.
39
+ // That is enough for an explainer, easy for a translator to keep intact,
40
+ // and rendered as React elements, so nothing an article says can inject
41
+ // markup. Anything else in the text is shown literally.
42
+ //
43
+ // 3. **Articles are per language and may load lazily.** `articles` can be a
44
+ // function of the language returning a promise, so an app keeps seven
45
+ // translations out of its first paint and imports only the one it needs.
46
+ // A language the app has not translated falls back to English (the
47
+ // function decides; the dialog just shows what it is given).
48
+ //
49
+ // 4. **The PDFs live on the web, not in the app.** Guides and hosted papers
50
+ // are served from one library, `opensource.unisim.co.uk/kb/`, never from
51
+ // the app's own bundle. A relative link to a bundled PDF inside a Capacitor
52
+ // webview NAVIGATES the webview to the PDF, with no back button out; an
53
+ // https link with target=_blank goes to the system browser on iOS, Android
54
+ // and Electron alike. It also keeps ~10 MB of papers out of every app
55
+ // bundle, and lets two apps that cite FIPS 197 share one copy. The cost is
56
+ // that downloads need a connection; the articles themselves never do.
57
+ //
58
+ // 5. **A hosted copy only when its licence allows it.** `source.pdf` is set
59
+ // only for public-domain (US Government), Open Government Licence, CC BY,
60
+ // EUR-Lex and IETF documents; everything else — ACM, IEEE, ISO, Adobe —
61
+ // is a link to the publisher's own free copy. `licence` says which, and
62
+ // the dialog shows it beside the download. Same rule as Ergo's
63
+ // `research-papers.ts`.
64
+ //
65
+ // ⚠️ Inline styles, like every SDK dialog — this ships inside node_modules,
66
+ // where the apps' Tailwind builds do not scan. Layout switches on the card's
67
+ // measured width (ResizeObserver), since inline styles have no media queries.
68
+ // ──────────────────────────────────────────────────────────────────────────────
69
+ /** Where the suite's knowledge-base PDFs are served from. See note 4. */
70
+ export const KB_LIBRARY_URL = 'https://opensource.unisim.co.uk/kb';
71
+ /**
72
+ * The folder a language's guides are filed under. The generator writes one
73
+ * folder per translation the app has; both Englishes share `en`.
74
+ */
75
+ export function guideLanguage(language) {
76
+ return language === 'en-gb' || language === 'en' ? 'en' : language;
77
+ }
78
+ /** Resolve a source's `pdf` (library-relative or absolute) to a URL. */
79
+ export function libraryUrl(path, library = KB_LIBRARY_URL) {
80
+ return /^https?:\/\//.test(path) ? path : `${library.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
81
+ }
82
+ export function KnowledgeBaseDialog({ open, onClose, articles, guides = false, library = KB_LIBRARY_URL, initialArticle, name, theme = 'light', }) {
11
83
  const { language } = useLanguage();
12
84
  const { config } = useUniversal();
13
85
  const p = MENU[theme];
14
86
  const [list, setList] = useState(Array.isArray(articles) ? articles : null);
15
87
  const [failed, setFailed] = useState(false);
16
- const [current, setCurrent] = useState(initialArticle ?? null);
88
+ const [view, setView] = useState(initialArticle ? { kind: 'article', id: initialArticle } : { kind: 'index' });
89
+ const [width, setWidth] = useState(0);
17
90
  const cardRef = useRef(null);
18
91
  // The card scrolls, and a long article leaves it scrolled: moving between
19
- // the list and an article starts at the top, as turning a page would.
20
- useEffect(() => { cardRef.current?.scrollTo?.({ top: 0 }); }, [current]);
92
+ // views starts at the top, as turning a page would.
93
+ const viewKey = view.kind === 'article' ? view.id : '';
94
+ useEffect(() => { cardRef.current?.scrollTo?.({ top: 0 }); }, [viewKey]);
21
95
  // Resolve the articles for the reader's language each time the dialog opens
22
96
  // (or the language changes under it). A plain array needs no work.
23
97
  useEffect(() => {
@@ -37,10 +111,10 @@ export function KnowledgeBaseDialog({ open, onClose, articles, initialArticle, n
37
111
  setFailed(true); });
38
112
  return () => { live = false; };
39
113
  }, [open, language, articles]);
40
- // Every open starts where the caller asked — the list, unless told otherwise.
114
+ // Every open starts where the caller asked — the cards, unless told otherwise.
41
115
  useEffect(() => {
42
116
  if (open)
43
- setCurrent(initialArticle ?? null);
117
+ setView(initialArticle ? { kind: 'article', id: initialArticle } : { kind: 'index' });
44
118
  }, [open, initialArticle]);
45
119
  useEffect(() => {
46
120
  if (!open)
@@ -48,25 +122,82 @@ export function KnowledgeBaseDialog({ open, onClose, articles, initialArticle, n
48
122
  function onKey(e) {
49
123
  if (e.key !== 'Escape')
50
124
  return;
51
- // Escape steps back out of an article before it closes the dialog, the
52
- // same as the back arrow — reading three articles should not need the
53
- // mouse between each.
54
- if (current)
55
- setCurrent(null);
125
+ // Escape steps back out of an article before it closes
126
+ // the dialog, the same as the back arrow — reading three articles should
127
+ // not need the mouse between each.
128
+ if (view.kind !== 'index')
129
+ setView({ kind: 'index' });
56
130
  else
57
131
  onClose();
58
132
  }
59
133
  document.addEventListener('keydown', onKey);
60
134
  return () => document.removeEventListener('keydown', onKey);
61
- }, [open, onClose, current]);
62
- const article = useMemo(() => (current && list ? list.find((a) => a.id === current) ?? null : null), [current, list]);
135
+ }, [open, onClose, view.kind]);
136
+ // Measure the card so the grid and the sidebar can reflow (see the ⚠️ above).
137
+ useEffect(() => {
138
+ const el = cardRef.current;
139
+ if (!open || !el)
140
+ return;
141
+ setWidth(el.clientWidth);
142
+ if (typeof ResizeObserver === 'undefined')
143
+ return;
144
+ const ro = new ResizeObserver(() => setWidth(el.clientWidth));
145
+ ro.observe(el);
146
+ return () => ro.disconnect();
147
+ }, [open]);
148
+ const index = view.kind === 'article' && list ? list.findIndex((a) => a.id === view.id) : -1;
149
+ const article = index >= 0 && list ? list[index] ?? null : null;
63
150
  if (!open || typeof document === 'undefined')
64
151
  return null;
65
152
  const entry = lookupProduct(config.product);
66
153
  const title = name ?? entry?.name ?? t(language, 'menu.knowledge_base');
67
- return createPortal(_jsx("div", { style: overlayStyle, onClick: onClose, role: "presentation", children: _jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": `${t(language, 'menu.knowledge_base')} — ${title}`, ref: cardRef, style: cardStyle(p), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { type: "button", onClick: onClose, "aria-label": t(language, 'nav.menu_close'), style: closeButtonStyle(p), onMouseEnter: (e) => { e.currentTarget.style.color = p.text; }, onMouseLeave: (e) => { e.currentTarget.style.color = p.muted; }, children: _jsx(IconClose, {}) }), article ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: () => setCurrent(null), style: backButtonStyle(p), children: [_jsx("span", { "aria-hidden": true, style: { display: 'inline-flex' }, children: _jsx(IconBack, {}) }), t(language, 'kb.all_articles')] }), _jsx("h2", { style: { ...titleStyle(p), margin: '10px 34px 12px 0' }, children: article.title }), _jsx(ArticleBody, { body: article.body, palette: p })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: identityRowStyle, children: [entry?.glyph && _jsx("span", { style: markTileStyle(p), "aria-hidden": true, children: entry.glyph }), _jsxs("div", { style: { minWidth: 0 }, children: [_jsx("h2", { style: titleStyle(p), children: t(language, 'menu.knowledge_base') }), _jsx("p", { style: subtitleStyle(p), children: title })] })] }), failed ? (_jsx("p", { style: bodyStyle(p), children: t(language, 'kb.load_failed') })) : !list ? (_jsx("p", { style: { ...bodyStyle(p), color: p.faint }, children: t(language, 'kb.loading') })) : list.length === 0 ? (_jsx("p", { style: { ...bodyStyle(p), color: p.faint }, children: t(language, 'kb.no_articles') })) : (_jsx(ArticleList, { articles: list, onOpen: setCurrent, palette: p }))] }))] }) }), document.body);
154
+ const guideBase = guides && config.product
155
+ ? `${library.replace(/\/$/, '')}/${config.product}/${guideLanguage(language)}`
156
+ : null;
157
+ const wide = width >= 720;
158
+ let content;
159
+ if (failed)
160
+ content = _jsx("p", { style: bodyStyle(p), children: t(language, 'kb.load_failed') });
161
+ else if (!list)
162
+ content = _jsx("p", { style: { ...bodyStyle(p), color: p.faint }, children: t(language, 'kb.loading') });
163
+ else if (article) {
164
+ content = (_jsx(ArticleView, { article: article, prev: list[index - 1], next: list[index + 1], colour: groupColour(list, article, theme), guideHref: guideBase ? `${guideBase}/${article.id}.pdf` : null, library: library, wide: wide, language: language, palette: p, onOpen: (id) => setView({ kind: 'article', id }), onBack: () => setView({ kind: 'index' }) }));
165
+ }
166
+ else {
167
+ content = (_jsxs(_Fragment, { children: [_jsxs("div", { style: identityRowStyle, children: [entry?.glyph && _jsx("span", { style: markTileStyle(p), "aria-hidden": true, children: entry.glyph }), _jsxs("div", { style: { minWidth: 0 }, children: [_jsx("h2", { style: titleStyle(p), children: t(language, 'menu.knowledge_base') }), _jsx("p", { style: subtitleStyle(p), children: title })] })] }), list.length === 0 ? (_jsx("p", { style: { ...bodyStyle(p), color: p.faint }, children: t(language, 'kb.no_articles') })) : (_jsxs(_Fragment, { children: [_jsx("p", { style: { ...bodyStyle(p), margin: '0 0 16px', color: p.muted }, children: t(language, 'kb.intro').replace('{app}', title) }), _jsx(CardGrid, { articles: list, columns: width >= 760 ? 3 : width >= 480 ? 2 : 1, theme: theme, language: language, palette: p, onOpen: (id) => setView({ kind: 'article', id }) })] }))] }));
168
+ }
169
+ return createPortal(_jsx("div", { style: overlayStyle, onClick: onClose, role: "presentation", children: _jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": `${t(language, 'menu.knowledge_base')} — ${title}`, ref: cardRef, style: cardStyle(p), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { type: "button", onClick: onClose, "aria-label": t(language, 'nav.menu_close'), style: closeButtonStyle(p), onMouseEnter: (e) => { e.currentTarget.style.color = p.text; }, onMouseLeave: (e) => { e.currentTarget.style.color = p.muted; }, children: _jsx(IconClose, {}) }), content] }) }), document.body);
170
+ }
171
+ const HUES = {
172
+ light: [
173
+ { fg: '#1d4ed8', bg: '#eff6ff', bar: '#3b82f6' },
174
+ { fg: '#6d28d9', bg: '#f5f3ff', bar: '#8b5cf6' },
175
+ { fg: '#047857', bg: '#ecfdf5', bar: '#10b981' },
176
+ { fg: '#b45309', bg: '#fffbeb', bar: '#f59e0b' },
177
+ { fg: '#be123c', bg: '#fff1f2', bar: '#f43f5e' },
178
+ ],
179
+ dark: [
180
+ { fg: '#93c5fd', bg: 'rgba(59,130,246,0.16)', bar: '#3b82f6' },
181
+ { fg: '#c4b5fd', bg: 'rgba(139,92,246,0.16)', bar: '#8b5cf6' },
182
+ { fg: '#6ee7b7', bg: 'rgba(16,185,129,0.16)', bar: '#10b981' },
183
+ { fg: '#fcd34d', bg: 'rgba(245,158,11,0.16)', bar: '#f59e0b' },
184
+ { fg: '#fda4af', bg: 'rgba(244,63,94,0.16)', bar: '#f43f5e' },
185
+ ],
186
+ };
187
+ function groupOrder(articles) {
188
+ const seen = [];
189
+ for (const a of articles)
190
+ if (!seen.includes(a.group ?? ''))
191
+ seen.push(a.group ?? '');
192
+ return seen;
68
193
  }
69
- function ArticleList({ articles, onOpen, palette: p }) {
194
+ function groupColour(articles, a, theme) {
195
+ const hues = HUES[theme];
196
+ const i = Math.max(0, groupOrder(articles).indexOf(a.group ?? ''));
197
+ return hues[i % hues.length];
198
+ }
199
+ // ── The index: the card grid ────────────────────────────────────────────────
200
+ function CardGrid({ articles, columns, theme, language, palette: p, onOpen }) {
70
201
  // Consecutive runs of the same group, in the order the app gave them.
71
202
  const runs = [];
72
203
  for (const a of articles) {
@@ -76,9 +207,25 @@ function ArticleList({ articles, onOpen, palette: p }) {
76
207
  else
77
208
  runs.push({ group: a.group, items: [a] });
78
209
  }
79
- return (_jsx("div", { style: { display: 'grid', gap: 14 }, children: runs.map((run, i) => (_jsxs("div", { children: [run.group && _jsx("p", { style: groupLabelStyle(p), children: run.group }), _jsx("div", { style: { display: 'grid', gap: 2, margin: '0 -10px' }, children: run.items.map((a) => (_jsxs("button", { type: "button", onClick: () => onOpen(a.id), style: rowStyle, onMouseEnter: (e) => { e.currentTarget.style.background = p.rowHover; }, onMouseLeave: (e) => { e.currentTarget.style.background = 'transparent'; }, children: [_jsxs("span", { style: { minWidth: 0, flex: 1 }, children: [_jsx("span", { style: { display: 'block', fontSize: 13, fontWeight: 600, color: p.text }, children: a.title }), a.summary && (_jsx("span", { style: { display: 'block', marginTop: 2, fontSize: 11.5, lineHeight: 1.4, color: p.muted }, children: a.summary }))] }), _jsx("span", { "aria-hidden": true, style: { display: 'inline-flex', color: p.faint, flexShrink: 0 }, children: _jsx(IconChevron, {}) })] }, a.id))) })] }, `${run.group ?? ''}-${i}`))) }));
210
+ return (_jsx("div", { style: { display: 'grid', gap: 20 }, children: runs.map((run, i) => {
211
+ const hue = groupColour(articles, run.items[0], theme);
212
+ return (_jsxs("section", { children: [run.group && _jsx("p", { style: groupLabelStyle(p), children: run.group }), _jsx("div", { style: { display: 'grid', gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, gap: 10 }, children: run.items.map((a) => (_jsxs("button", { type: "button", onClick: () => onOpen(a.id), style: cardButtonStyle(p, hue), onMouseEnter: (e) => { e.currentTarget.style.borderColor = hue.bar; e.currentTarget.style.transform = 'translateY(-1px)'; }, onMouseLeave: (e) => { e.currentTarget.style.borderColor = p.divider; e.currentTarget.style.transform = 'none'; }, children: [_jsx("span", { style: { display: 'block', fontSize: 14, fontWeight: 700, lineHeight: 1.35, color: p.text }, children: a.title }), a.summary && (_jsx("span", { style: { display: 'block', marginTop: 6, fontSize: 12.5, lineHeight: 1.45, color: p.muted }, children: a.summary })), _jsxs("span", { style: cardFooterStyle(p), children: [_jsx("span", { children: a.sources?.length ? count(language, 'kb.sources_count', a.sources.length) : '' }), _jsxs("span", { style: { color: hue.fg, fontWeight: 650 }, children: [t(language, 'kb.read'), " \u2192"] })] })] }, a.id))) })] }, `${run.group ?? ''}-${i}`));
213
+ }) }));
214
+ }
215
+ // ── An article ──────────────────────────────────────────────────────────────
216
+ function ArticleView({ article, prev, next, colour, guideHref, library, wide, language, palette: p, onOpen, onBack }) {
217
+ const sources = article.sources ?? [];
218
+ const aside = (guideHref || sources.length > 0) && (_jsxs("aside", { style: { display: 'grid', gap: 14, alignContent: 'start' }, children: [guideHref && (_jsxs("a", { href: guideHref, target: "_blank", rel: "noopener noreferrer", style: guideButtonStyle(colour), children: [_jsx(IconDownload, {}), _jsx("span", { children: t(language, 'kb.download_guide') })] })), sources.length > 0 && (_jsxs("div", { style: asideBoxStyle(p), children: [_jsx("p", { style: { ...groupLabelStyle(p), margin: '0 0 10px' }, children: t(language, 'kb.sources') }), _jsx("div", { style: { display: 'grid', gap: 12 }, children: sources.map((s) => _jsx(SourceItem, { source: s, library: library, language: language, palette: p }, s.href)) })] }))] }));
219
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: onBack, style: backButtonStyle(p), children: [_jsx("span", { "aria-hidden": true, style: { display: 'inline-flex' }, children: _jsx(IconBack, {}) }), t(language, 'kb.all_articles')] }), _jsxs("div", { style: { ...heroStyle, background: colour.bg, borderColor: colour.bar }, children: [article.group && _jsx("p", { style: { ...groupLabelStyle(p), color: colour.fg, margin: '0 0 6px' }, children: article.group }), _jsx("h2", { style: { ...titleStyle(p), fontSize: 20, margin: '0 34px 0 0' }, children: article.title }), article.summary && _jsx("p", { style: { ...bodyStyle(p), margin: '6px 0 0', color: p.muted }, children: article.summary })] }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: wide && aside ? 'minmax(0, 1fr) 260px' : 'minmax(0, 1fr)', gap: 24 }, children: [_jsx("div", { style: { minWidth: 0, maxWidth: 640 }, children: _jsx(ArticleBody, { body: article.body, palette: p }) }), aside] }), (prev || next) && (_jsxs("nav", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginTop: 26, paddingTop: 16, borderTop: `1px solid ${p.divider}` }, children: [prev ? _jsx(PageLink, { label: t(language, 'kb.previous'), title: prev.title, onClick: () => onOpen(prev.id), palette: p }) : _jsx("span", {}), next ? _jsx(PageLink, { label: t(language, 'kb.next'), title: next.title, onClick: () => onOpen(next.id), palette: p, align: "right" }) : _jsx("span", {})] }))] }));
80
220
  }
81
- /** Exported for the tests — splits an article body into blocks. */
221
+ function PageLink({ label, title, onClick, palette: p, align = 'left' }) {
222
+ return (_jsxs("button", { type: "button", onClick: onClick, style: { ...rowStyle, display: 'block', textAlign: align, border: `1px solid ${p.divider}`, padding: '10px 12px' }, onMouseEnter: (e) => { e.currentTarget.style.background = p.rowHover; }, onMouseLeave: (e) => { e.currentTarget.style.background = 'transparent'; }, children: [_jsx("span", { style: { display: 'block', fontSize: 11, color: p.faint }, children: align === 'left' ? `← ${label}` : `${label} →` }), _jsx("span", { style: { display: 'block', marginTop: 2, fontSize: 13, fontWeight: 600, color: p.text }, children: title })] }));
223
+ }
224
+ function SourceItem({ source: s, library, language, palette: p }) {
225
+ const byline = [s.authors, s.publisher, s.year].filter(Boolean).join(' · ');
226
+ return (_jsxs("div", { children: [_jsx("span", { style: kindChipStyle(p), children: t(language, `kb.kind.${s.kind}`) }), _jsx("a", { href: s.href, target: "_blank", rel: "noopener noreferrer", style: sourceTitleStyle(p), children: s.title }), byline && _jsx("span", { style: { display: 'block', marginTop: 2, fontSize: 11.5, lineHeight: 1.4, color: p.muted }, children: byline }), _jsxs("span", { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 }, children: [s.pdf && (_jsxs("a", { href: libraryUrl(s.pdf, library), target: "_blank", rel: "noopener noreferrer", style: pillLinkStyle(p, true), title: s.licence, children: [_jsx(IconDownload, { size: 11 }), " PDF"] })), _jsxs("a", { href: s.href, target: "_blank", rel: "noopener noreferrer", style: pillLinkStyle(p, false), children: [t(language, s.pdf ? 'kb.publisher' : 'kb.read_source'), " \u2197"] })] }), s.pdf && s.licence && (_jsx("span", { style: { display: 'block', marginTop: 4, fontSize: 10.5, lineHeight: 1.35, color: p.faint }, children: s.licence }))] }));
227
+ }
228
+ /** Exported for the tests and the PDF generator — splits a body into blocks. */
82
229
  export function parseArticleBody(body) {
83
230
  const blocks = [];
84
231
  const lines = body.replace(/\r\n?/g, '\n').split('\n');
@@ -142,6 +289,10 @@ function ArticleBody({ body, palette: p }) {
142
289
  return (_jsx(List, { style: listStyle(p), children: b.items.map((item, j) => _jsx("li", { style: { marginTop: j ? 4 : 0 }, children: inline(item) }, j)) }, i));
143
290
  }) }));
144
291
  }
292
+ /** `{n}` into the singular or plural key (`<key>_one` / `<key>`). */
293
+ function count(language, key, n) {
294
+ return t(language, n === 1 ? `${key}_one` : key).replace('{n}', String(n));
295
+ }
145
296
  // ── Icons ───────────────────────────────────────────────────────────────────
146
297
  function IconClose() {
147
298
  return (_jsx("svg", { viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }));
@@ -152,6 +303,9 @@ function IconBack() {
152
303
  function IconChevron() {
153
304
  return (_jsx("svg", { viewBox: "0 0 12 12", width: "11", height: "11", "aria-hidden": "true", children: _jsx("path", { d: "M4 2 L8 6 L4 10", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
154
305
  }
306
+ function IconDownload({ size = 16 }) {
307
+ return (_jsx("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M12 3v12M7 10l5 5 5-5M5 21h14" }) }));
308
+ }
155
309
  /** The book glyph for the menu row. Exported so <AdvancedMenu> can use it. */
156
310
  export function IconBook() {
157
311
  return (_jsxs("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M4 5.5A2.5 2.5 0 0 1 6.5 3H20v15H6.5A2.5 2.5 0 0 0 4 20.5z" }), _jsx("path", { d: "M4 20.5A2.5 2.5 0 0 0 6.5 23H20v-5" }), _jsx("path", { d: "M8 7h8M8 11h6" })] }));
@@ -168,20 +322,22 @@ const overlayStyle = {
168
322
  zIndex: NAV_OVERLAY_Z_INDEX + 100,
169
323
  fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
170
324
  };
171
- // Wider than About (460): this is for reading, and 60-odd characters a line
172
- // is where prose stops being tiring.
325
+ // A page, not a panel: wide enough for three columns of cards, or an article
326
+ // beside its sources. The body column itself stops at 640px, since 60-odd
327
+ // characters a line is where prose stops being tiring.
173
328
  const cardStyle = (p) => ({
174
329
  position: 'relative',
175
330
  width: '100%',
176
- maxWidth: 560,
177
- maxHeight: 'calc(100vh - 32px)',
331
+ maxWidth: 980,
332
+ height: 'calc(100vh - 32px)',
333
+ maxHeight: 900,
178
334
  overflowY: 'auto',
179
335
  background: p.surface,
180
336
  color: p.text,
181
337
  borderRadius: 16,
182
338
  border: `1px solid ${p.border}`,
183
339
  boxShadow: p.shadow,
184
- padding: '22px 24px 24px',
340
+ padding: '22px 24px 28px',
185
341
  boxSizing: 'border-box',
186
342
  });
187
343
  const closeButtonStyle = (p) => ({
@@ -198,13 +354,14 @@ const closeButtonStyle = (p) => ({
198
354
  background: 'transparent',
199
355
  color: p.muted,
200
356
  cursor: 'pointer',
357
+ zIndex: 1,
201
358
  });
202
359
  const backButtonStyle = (p) => ({
203
360
  display: 'inline-flex',
204
361
  alignItems: 'center',
205
362
  gap: 4,
206
363
  padding: '4px 6px 4px 2px',
207
- margin: '-4px 0 0 -2px',
364
+ margin: '-4px 0 10px -2px',
208
365
  border: 0,
209
366
  borderRadius: 6,
210
367
  background: 'transparent',
@@ -218,7 +375,7 @@ const identityRowStyle = {
218
375
  display: 'flex',
219
376
  alignItems: 'center',
220
377
  gap: 12,
221
- margin: '0 34px 18px 0',
378
+ margin: '0 34px 14px 0',
222
379
  };
223
380
  const markTileStyle = (p) => ({
224
381
  display: 'inline-flex',
@@ -232,7 +389,7 @@ const markTileStyle = (p) => ({
232
389
  });
233
390
  const titleStyle = (p) => ({
234
391
  margin: 0,
235
- fontSize: 17,
392
+ fontSize: 18,
236
393
  fontWeight: 700,
237
394
  letterSpacing: '-0.01em',
238
395
  lineHeight: 1.3,
@@ -244,13 +401,95 @@ const subtitleStyle = (p) => ({
244
401
  color: p.muted,
245
402
  });
246
403
  const groupLabelStyle = (p) => ({
247
- margin: '0 0 4px',
404
+ margin: '0 0 8px',
248
405
  fontSize: 10.5,
249
406
  fontWeight: 700,
250
407
  letterSpacing: '0.07em',
251
408
  textTransform: 'uppercase',
252
409
  color: p.faint,
253
410
  });
411
+ const cardButtonStyle = (p, hue) => ({
412
+ display: 'flex',
413
+ flexDirection: 'column',
414
+ alignItems: 'stretch',
415
+ minHeight: 132,
416
+ padding: '14px 14px 12px 16px',
417
+ borderRadius: 12,
418
+ border: `1px solid ${p.divider}`,
419
+ borderLeft: `4px solid ${hue.bar}`,
420
+ background: 'transparent',
421
+ textAlign: 'left',
422
+ fontFamily: 'inherit',
423
+ cursor: 'pointer',
424
+ transition: 'border-color 120ms, transform 120ms',
425
+ });
426
+ const cardFooterStyle = (p) => ({
427
+ display: 'flex',
428
+ justifyContent: 'space-between',
429
+ gap: 8,
430
+ marginTop: 'auto',
431
+ paddingTop: 12,
432
+ fontSize: 11.5,
433
+ color: p.faint,
434
+ });
435
+ const heroStyle = {
436
+ padding: '16px 18px',
437
+ margin: '0 0 20px',
438
+ borderRadius: 12,
439
+ borderLeft: '4px solid',
440
+ };
441
+ const guideButtonStyle = (hue) => ({
442
+ display: 'flex',
443
+ alignItems: 'center',
444
+ justifyContent: 'center',
445
+ gap: 8,
446
+ padding: '10px 12px',
447
+ borderRadius: 10,
448
+ background: hue.bar,
449
+ color: '#ffffff',
450
+ fontSize: 13,
451
+ fontWeight: 650,
452
+ textDecoration: 'none',
453
+ });
454
+ const asideBoxStyle = (p) => ({
455
+ padding: '14px 14px 16px',
456
+ borderRadius: 12,
457
+ border: `1px solid ${p.divider}`,
458
+ });
459
+ const kindChipStyle = (p) => ({
460
+ display: 'inline-block',
461
+ marginBottom: 4,
462
+ padding: '1px 7px',
463
+ borderRadius: 999,
464
+ background: p.chipBg,
465
+ color: p.chipText,
466
+ fontSize: 10,
467
+ fontWeight: 650,
468
+ letterSpacing: '0.02em',
469
+ });
470
+ const sourceTitleStyle = (p) => ({
471
+ display: 'block',
472
+ fontSize: 12.5,
473
+ fontWeight: 650,
474
+ lineHeight: 1.4,
475
+ color: p.text,
476
+ textDecoration: 'none',
477
+ });
478
+ const pillLinkStyle = (p, primary) => ({
479
+ display: 'inline-flex',
480
+ alignItems: 'center',
481
+ gap: 4,
482
+ flexShrink: 0,
483
+ padding: '3px 9px',
484
+ borderRadius: 999,
485
+ border: `1px solid ${primary ? p.accentBorder : p.divider}`,
486
+ background: primary ? p.accentBg : 'transparent',
487
+ color: primary ? p.accentText : p.muted,
488
+ fontSize: 11,
489
+ fontWeight: 650,
490
+ textDecoration: 'none',
491
+ whiteSpace: 'nowrap',
492
+ });
254
493
  const rowStyle = {
255
494
  display: 'flex',
256
495
  alignItems: 'center',
@@ -266,22 +505,22 @@ const rowStyle = {
266
505
  transition: 'background 120ms',
267
506
  };
268
507
  const subheadingStyle = (p) => ({
269
- margin: '6px 0 0',
270
- fontSize: 13.5,
508
+ margin: '8px 0 0',
509
+ fontSize: 14.5,
271
510
  fontWeight: 700,
272
511
  color: p.text,
273
512
  });
274
513
  const bodyStyle = (p) => ({
275
514
  margin: 0,
276
- fontSize: 13.5,
277
- lineHeight: 1.6,
515
+ fontSize: 14,
516
+ lineHeight: 1.65,
278
517
  color: p.body,
279
518
  });
280
519
  const listStyle = (p) => ({
281
520
  margin: 0,
282
521
  paddingLeft: 20,
283
- fontSize: 13.5,
284
- lineHeight: 1.55,
522
+ fontSize: 14,
523
+ lineHeight: 1.6,
285
524
  color: p.body,
286
525
  });
287
526
  //# sourceMappingURL=KnowledgeBaseDialog.js.map