@colixsystems/widget-sdk 0.166.0 → 0.168.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 +13 -2
- package/dist/contract.cjs +17 -5
- package/dist/contract.js +17 -5
- package/dist/host.js +5 -0
- package/dist/index.d.ts +27 -3
- package/dist/markdown.js +106 -11
- package/dist/navigation.cjs +59 -1
- package/dist/navigation.js +58 -0
- package/dist/richtext-view.js +58 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,18 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
73
73
|
|
|
74
74
|
## Status
|
|
75
75
|
|
|
76
|
-
`v0.
|
|
76
|
+
`v0.167.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
77
|
+
|
|
78
|
+
### What's new in 0.167.0 (contract 1.129.0)
|
|
79
|
+
|
|
80
|
+
**The markdown grammar stops eating characters, and `useAgent` says to render a reply with `<RichText>` (sc-8891).** A model answers in markdown by default, so a chat surface that renders `message.content` in a plain `<Text>` shows an app user the raw markers. Pointing model text at `<RichText>` surfaced two grammar bugs that were corrupting **author** content too:
|
|
81
|
+
|
|
82
|
+
- **Emphasis now follows CommonMark's flanking rules.** `_` no longer opens or closes inside a word and neither delimiter opens before whitespace, so `created_at` keeps its underscore and `2 * 4` keeps its asterisk. Previously both were silently deleted — in a newsfeed post as well as an AI reply.
|
|
83
|
+
- **`<RichText>` gains `stripLegacyHtml`** (default `true`, unchanged for stored rows). The legacy-HTML strip removes every `<…>` as soon as one tag-shaped token appears, which ate `Promise<void>` and `<name@example.com>`. Pass **`false`** for text that was never an HTML row.
|
|
84
|
+
- **Fenced ``` blocks render verbatim** in a monospace block, newlines and indentation intact. They used to collapse into one inline span with stray backticks.
|
|
85
|
+
- A line that cannot hold a link skips the link pass, so a bracket-heavy string no longer costs seconds of main-thread time.
|
|
86
|
+
|
|
87
|
+
Render an **assistant** message with `<RichText value={m.content} stripLegacyHtml={false} />`. Keep the **user's own** message a `<Text>` — someone who types `*maybe*` means asterisks.
|
|
77
88
|
|
|
78
89
|
### What's new in 0.166.0 (contract 1.128.0)
|
|
79
90
|
|
|
@@ -82,7 +93,7 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
82
93
|
A binding may now carry an opacity: `"theme:colors.primary/20"` is the primary brand colour at 20%.
|
|
83
94
|
|
|
84
95
|
- **`parseThemeColorTokenRef(value)` / `formatThemeColorTokenRef(path, percent)`** publish the grammar beside the existing `isThemeColorTokenRef` predicate. The BARE form inherits whatever opacity the token itself carries — a workspace may author a translucent brand colour — so `/100` is a real value, not a redundant spelling: it is how "opaque, whatever the brand does" is said. One opacity has one spelling otherwise (no leading zeros, nothing above 100).
|
|
85
|
-
- **`withHexAlpha(hex, percent)` / `hexAlphaPercent(hex)`** are the percent ↔ alpha-byte conversion the Studio's
|
|
96
|
+
- **`withHexAlpha(hex, percent)` / `hexAlphaPercent(hex)`** are the percent ↔ alpha-byte conversion, so the 20% a panel reads back is the 20% a render paints. The Studio's `frontend/src/utils/color.js` keeps its own copy of this maths rather than importing it — that module is reached by a `node --test` suite, and the SDK is a Vite alias with no node resolution — and the two are pinned equal by the SDK's suite.
|
|
86
97
|
- **An authored opacity replaces the token's.** With none authored the token is handed back verbatim, including a token that is itself translucent — reading the bare form as 100 would quietly paint a see-through brand colour solid.
|
|
87
98
|
- **A token resolving to a non-hex colour stays opaque** — alpha cannot be applied without rewriting the author's notation — and the Studio disables the opacity control for that case rather than lying about it.
|
|
88
99
|
- **Nothing changes for a widget author.** The host still hands your code an ordinary colour string; it is simply `#RRGGBBAA` when an opacity was authored.
|
package/dist/contract.cjs
CHANGED
|
@@ -1458,6 +1458,12 @@ const HOOKS = [
|
|
|
1458
1458
|
"no error at all. Call cancel() to stop waiting -- it ends the WAIT, " +
|
|
1459
1459
|
"not the turn. NEVER surface a " +
|
|
1460
1460
|
"raw error or the workspace's billing state to an app user. " +
|
|
1461
|
+
"RENDER AN ASSISTANT REPLY WITH <RichText value={msg.content} />, " +
|
|
1462
|
+
"not <Text>: a model answers in MARKDOWN by default — **bold**, " +
|
|
1463
|
+
"`code`, # headings, - bullets, 1. numbered steps, tables — and a " +
|
|
1464
|
+
"plain <Text> shows the raw markers to the app user, so a numbered " +
|
|
1465
|
+
"how-to arrives as one unreadable line. The USER's own message " +
|
|
1466
|
+
"stays <Text>: someone who types *maybe* means asterisks. " +
|
|
1461
1467
|
"Reads ctx.agents.",
|
|
1462
1468
|
returnShape: {
|
|
1463
1469
|
send: "(text) => Promise<{ id, role, content, created_at } | null>",
|
|
@@ -2265,7 +2271,7 @@ const PRIMITIVES = [
|
|
|
2265
2271
|
{
|
|
2266
2272
|
name: "RichText",
|
|
2267
2273
|
description:
|
|
2268
|
-
'Formatted-content renderer. `<RichText value={record.body} />`. THE way to display text an app user wrote with formatting — bold, italic, inline code, `#`/`##`/`###` headings, `-` bullets, `1.` numbered items, `[label](target)` links, `| a | b |` pipe tables (a header row, a `| --- | --- |` divider, then body rows; `:--` / `--:` / `:-:` in a divider cell sets that column\'s alignment), and one-line `` images. `value` is markdown text, NOT HTML: widgets render through React Native primitives, which have no `dangerouslySetInnerHTML` on either host, so HTML inside a content string shows up as literal tag soup — never assemble content out of `<strong>`/`<br>`/`<table>` tags. Stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. A link is pressed through the host\'s `navigation.openLink`, which is what decides page / external / refuse — so a link target is just a string here: an absolute URL leaves the app, a bare page id or slug navigates inside it, and anything unfollowable (`javascript:`, a control character) is refused by that one resolver. Never pre-filter a target yourself and never route one through `Linking.openURL`, which performs anything. Props: `value` (markdown string), `renderImage` (optional `({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving those needs the scopes your own widget holds; without it only absolute http(s) URLs render), `followLinks` (default true; pass false to render links styled but inert, which is what an editing preview wants so a tap cannot discard the draft), `style`, `testID`. Colour, type scale and leading all come from the workspace theme — never re-style them. Table cells are equal-width columns whose text wraps, so a wide table stays inside the widget on a phone instead of scrolling sideways.',
|
|
2274
|
+
'Formatted-content renderer. `<RichText value={record.body} />`. THE way to display text an app user wrote with formatting — bold, italic, inline code, `#`/`##`/`###` headings, `-` bullets, `1.` numbered items, `[label](target)` links, `| a | b |` pipe tables (a header row, a `| --- | --- |` divider, then body rows; `:--` / `--:` / `:-:` in a divider cell sets that column\'s alignment), and one-line `` images. `value` is markdown text, NOT HTML: widgets render through React Native primitives, which have no `dangerouslySetInnerHTML` on either host, so HTML inside a content string shows up as literal tag soup — never assemble content out of `<strong>`/`<br>`/`<table>` tags. Stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. A link is pressed through the host\'s `navigation.openLink`, which is what decides page / external / refuse — so a link target is just a string here: an absolute URL leaves the app, a bare page id or slug navigates inside it, and anything unfollowable (`javascript:`, a control character) is refused by that one resolver. Never pre-filter a target yourself and never route one through `Linking.openURL`, which performs anything. Fenced ```` ``` ```` blocks render verbatim in a monospace block, so a code sample keeps its newlines and indentation. Props: `value` (markdown string), `stripLegacyHtml` (default true — normalises a STORED row that still holds HTML; pass **false** for text that was never HTML, such as an AI reply, because the strip removes every `<…>` once any tag-shaped token appears and would eat `Promise<void>` or `<name@example.com>`), `renderImage` (optional `({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving those needs the scopes your own widget holds; without it only absolute http(s) URLs render), `followLinks` (default true; pass false to render links styled but inert, which is what an editing preview wants so a tap cannot discard the draft), `style`, `testID`. Colour, type scale and leading all come from the workspace theme — never re-style them. Table cells are equal-width columns whose text wraps, so a wide table stays inside the widget on a phone instead of scrolling sideways.',
|
|
2269
2275
|
rnComponent: null,
|
|
2270
2276
|
docsUrl: null,
|
|
2271
2277
|
},
|
|
@@ -4533,15 +4539,21 @@ const CONTRACT = deepFreeze({
|
|
|
4533
4539
|
// field bound, so "our brand colour at 20%" forced the author back to a
|
|
4534
4540
|
// frozen hex, which then stopped following a rebrand — the exact thing
|
|
4535
4541
|
// binding exists to prevent. `parseThemeColorTokenRef` /
|
|
4536
|
-
// `formatThemeColorTokenRef` publish the grammar and
|
|
4542
|
+
// `formatThemeColorTokenRef` publish the grammar, and
|
|
4537
4543
|
// `withHexAlpha` / `hexAlphaPercent` the percent-to-byte conversion the
|
|
4538
|
-
// Studio's control and both hosts' resolver
|
|
4539
|
-
// painted. The BARE form inherits the token's own opacity (a brand colour
|
|
4544
|
+
// Studio's control and both hosts' resolver answer alike, so 20% authored
|
|
4545
|
+
// is 20% painted. The BARE form inherits the token's own opacity (a brand colour
|
|
4540
4546
|
// may be authored translucent), which makes `/100` a real value rather
|
|
4541
4547
|
// than a redundant spelling: it is how "opaque, whatever the brand does"
|
|
4542
4548
|
// is said. Additive: an unsuffixed binding parses and resolves exactly as
|
|
4543
4549
|
// before, so every stored value is untouched.
|
|
4544
|
-
|
|
4550
|
+
// 1.129.0: additive (sc-8891) — `<RichText>` gains `stripLegacyHtml`
|
|
4551
|
+
// (default true) and renders fenced ``` blocks verbatim. The legacy-HTML
|
|
4552
|
+
// strip exists for stored author rows; applied to text that was never HTML
|
|
4553
|
+
// it removed every `<...>`, so an AI reply lost `Promise<void>`. The
|
|
4554
|
+
// emphasis grammar now follows CommonMark flanking rules, so `created_at`
|
|
4555
|
+
// keeps its underscore and `2 * 4` keeps its asterisk.
|
|
4556
|
+
version: "1.129.0",
|
|
4545
4557
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4546
4558
|
hooks: HOOKS,
|
|
4547
4559
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1458,6 +1458,12 @@ const HOOKS = [
|
|
|
1458
1458
|
"no error at all. Call cancel() to stop waiting -- it ends the WAIT, " +
|
|
1459
1459
|
"not the turn. NEVER surface a " +
|
|
1460
1460
|
"raw error or the workspace's billing state to an app user. " +
|
|
1461
|
+
"RENDER AN ASSISTANT REPLY WITH <RichText value={msg.content} />, " +
|
|
1462
|
+
"not <Text>: a model answers in MARKDOWN by default — **bold**, " +
|
|
1463
|
+
"`code`, # headings, - bullets, 1. numbered steps, tables — and a " +
|
|
1464
|
+
"plain <Text> shows the raw markers to the app user, so a numbered " +
|
|
1465
|
+
"how-to arrives as one unreadable line. The USER's own message " +
|
|
1466
|
+
"stays <Text>: someone who types *maybe* means asterisks. " +
|
|
1461
1467
|
"Reads ctx.agents.",
|
|
1462
1468
|
returnShape: {
|
|
1463
1469
|
send: "(text) => Promise<{ id, role, content, created_at } | null>",
|
|
@@ -2265,7 +2271,7 @@ const PRIMITIVES = [
|
|
|
2265
2271
|
{
|
|
2266
2272
|
name: "RichText",
|
|
2267
2273
|
description:
|
|
2268
|
-
'Formatted-content renderer. `<RichText value={record.body} />`. THE way to display text an app user wrote with formatting — bold, italic, inline code, `#`/`##`/`###` headings, `-` bullets, `1.` numbered items, `[label](target)` links, `| a | b |` pipe tables (a header row, a `| --- | --- |` divider, then body rows; `:--` / `--:` / `:-:` in a divider cell sets that column\'s alignment), and one-line `` images. `value` is markdown text, NOT HTML: widgets render through React Native primitives, which have no `dangerouslySetInnerHTML` on either host, so HTML inside a content string shows up as literal tag soup — never assemble content out of `<strong>`/`<br>`/`<table>` tags. Stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. A link is pressed through the host\'s `navigation.openLink`, which is what decides page / external / refuse — so a link target is just a string here: an absolute URL leaves the app, a bare page id or slug navigates inside it, and anything unfollowable (`javascript:`, a control character) is refused by that one resolver. Never pre-filter a target yourself and never route one through `Linking.openURL`, which performs anything. Props: `value` (markdown string), `renderImage` (optional `({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving those needs the scopes your own widget holds; without it only absolute http(s) URLs render), `followLinks` (default true; pass false to render links styled but inert, which is what an editing preview wants so a tap cannot discard the draft), `style`, `testID`. Colour, type scale and leading all come from the workspace theme — never re-style them. Table cells are equal-width columns whose text wraps, so a wide table stays inside the widget on a phone instead of scrolling sideways.',
|
|
2274
|
+
'Formatted-content renderer. `<RichText value={record.body} />`. THE way to display text an app user wrote with formatting — bold, italic, inline code, `#`/`##`/`###` headings, `-` bullets, `1.` numbered items, `[label](target)` links, `| a | b |` pipe tables (a header row, a `| --- | --- |` divider, then body rows; `:--` / `--:` / `:-:` in a divider cell sets that column\'s alignment), and one-line `` images. `value` is markdown text, NOT HTML: widgets render through React Native primitives, which have no `dangerouslySetInnerHTML` on either host, so HTML inside a content string shows up as literal tag soup — never assemble content out of `<strong>`/`<br>`/`<table>` tags. Stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. A link is pressed through the host\'s `navigation.openLink`, which is what decides page / external / refuse — so a link target is just a string here: an absolute URL leaves the app, a bare page id or slug navigates inside it, and anything unfollowable (`javascript:`, a control character) is refused by that one resolver. Never pre-filter a target yourself and never route one through `Linking.openURL`, which performs anything. Fenced ```` ``` ```` blocks render verbatim in a monospace block, so a code sample keeps its newlines and indentation. Props: `value` (markdown string), `stripLegacyHtml` (default true — normalises a STORED row that still holds HTML; pass **false** for text that was never HTML, such as an AI reply, because the strip removes every `<…>` once any tag-shaped token appears and would eat `Promise<void>` or `<name@example.com>`), `renderImage` (optional `({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving those needs the scopes your own widget holds; without it only absolute http(s) URLs render), `followLinks` (default true; pass false to render links styled but inert, which is what an editing preview wants so a tap cannot discard the draft), `style`, `testID`. Colour, type scale and leading all come from the workspace theme — never re-style them. Table cells are equal-width columns whose text wraps, so a wide table stays inside the widget on a phone instead of scrolling sideways.',
|
|
2269
2275
|
rnComponent: null,
|
|
2270
2276
|
docsUrl: null,
|
|
2271
2277
|
},
|
|
@@ -4529,15 +4535,21 @@ const CONTRACT = deepFreeze({
|
|
|
4529
4535
|
// field bound, so "our brand colour at 20%" forced the author back to a
|
|
4530
4536
|
// frozen hex, which then stopped following a rebrand — the exact thing
|
|
4531
4537
|
// binding exists to prevent. `parseThemeColorTokenRef` /
|
|
4532
|
-
// `formatThemeColorTokenRef` publish the grammar and
|
|
4538
|
+
// `formatThemeColorTokenRef` publish the grammar, and
|
|
4533
4539
|
// `withHexAlpha` / `hexAlphaPercent` the percent-to-byte conversion the
|
|
4534
|
-
// Studio's control and both hosts' resolver
|
|
4535
|
-
// painted. The BARE form inherits the token's own opacity (a brand colour
|
|
4540
|
+
// Studio's control and both hosts' resolver answer alike, so 20% authored
|
|
4541
|
+
// is 20% painted. The BARE form inherits the token's own opacity (a brand colour
|
|
4536
4542
|
// may be authored translucent), which makes `/100` a real value rather
|
|
4537
4543
|
// than a redundant spelling: it is how "opaque, whatever the brand does"
|
|
4538
4544
|
// is said. Additive: an unsuffixed binding parses and resolves exactly as
|
|
4539
4545
|
// before, so every stored value is untouched.
|
|
4540
|
-
|
|
4546
|
+
// 1.129.0: additive (sc-8891) — `<RichText>` gains `stripLegacyHtml`
|
|
4547
|
+
// (default true) and renders fenced ``` blocks verbatim. The legacy-HTML
|
|
4548
|
+
// strip exists for stored author rows; applied to text that was never HTML
|
|
4549
|
+
// it removed every `<...>`, so an AI reply lost `Promise<void>`. The
|
|
4550
|
+
// emphasis grammar now follows CommonMark flanking rules, so `created_at`
|
|
4551
|
+
// keeps its underscore and `2 * 4` keeps its asterisk.
|
|
4552
|
+
version: "1.129.0",
|
|
4541
4553
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4542
4554
|
hooks: HOOKS,
|
|
4543
4555
|
primitives: PRIMITIVES,
|
package/dist/host.js
CHANGED
|
@@ -74,6 +74,9 @@ export {
|
|
|
74
74
|
// SIDEBAR local menu hangs in the rail and the drawer. The compiler asks it
|
|
75
75
|
// once per screen at build time and the Player once per render, so the two
|
|
76
76
|
// chromes derive the same tree from the same code.
|
|
77
|
+
// REQ-NAV-LOCAL (sc-8901): `resolveLocalNavModel` answers WHICH model a page
|
|
78
|
+
// wears in one device view, so the same menu can be a bottom bar on a phone and
|
|
79
|
+
// unfold in the rail on a desktop.
|
|
77
80
|
export {
|
|
78
81
|
normaliseNavigation,
|
|
79
82
|
menuItemCap,
|
|
@@ -94,6 +97,8 @@ export {
|
|
|
94
97
|
DEFAULT_TOP_STRIPE_HEIGHT_PX,
|
|
95
98
|
DEFAULT_TOP_STRIPE_PADDING_X_PX,
|
|
96
99
|
resolveSidebarUnfold,
|
|
100
|
+
resolveLocalNavModel,
|
|
101
|
+
drawsLocalNavAnywhere,
|
|
97
102
|
} from "./navigation.js";
|
|
98
103
|
|
|
99
104
|
// sc-5717: the codec behind `useWidgetRoute()` — how a widget's own internal
|
package/dist/index.d.ts
CHANGED
|
@@ -1221,6 +1221,7 @@ export interface AgentMessage {
|
|
|
1221
1221
|
conversation_id: string;
|
|
1222
1222
|
/** The system prompt is NOT part of the transcript — it lives on the agent. */
|
|
1223
1223
|
role: "user" | "assistant";
|
|
1224
|
+
/** Markdown — an assistant answers in it. Render with `<RichText>`. */
|
|
1224
1225
|
content: string;
|
|
1225
1226
|
/**
|
|
1226
1227
|
* sc-8332 — what the agent DID on this turn, for the person to see. Present
|
|
@@ -2767,17 +2768,35 @@ export type MarkdownBlock =
|
|
|
2767
2768
|
| { type: "paragraph"; spans: MarkdownSpan[] }
|
|
2768
2769
|
| { type: "bullet"; marker: string; spans: MarkdownSpan[] }
|
|
2769
2770
|
| { type: "ordered"; marker: string; spans: MarkdownSpan[] }
|
|
2770
|
-
| { type: "image"; src: string; alt: string; size: MarkdownImageSize }
|
|
2771
|
+
| { type: "image"; src: string; alt: string; size: MarkdownImageSize }
|
|
2772
|
+
/** A fenced ``` block. `text` is verbatim, newlines and indentation intact. */
|
|
2773
|
+
| { type: "code"; text: string; lang: string };
|
|
2771
2774
|
|
|
2772
2775
|
export type MarkdownImageSize = "small" | "medium" | "large" | "full";
|
|
2773
2776
|
|
|
2774
2777
|
export const MARKDOWN_IMAGE_SIZES: readonly MarkdownImageSize[];
|
|
2775
2778
|
|
|
2776
2779
|
/** Parses the markdown subset into blocks. Non-string input yields `[]`. */
|
|
2777
|
-
export function parseMarkdown(
|
|
2780
|
+
export function parseMarkdown(
|
|
2781
|
+
text: unknown,
|
|
2782
|
+
options?: MarkdownParseOptions,
|
|
2783
|
+
): MarkdownBlock[];
|
|
2784
|
+
|
|
2785
|
+
export interface MarkdownParseOptions {
|
|
2786
|
+
/**
|
|
2787
|
+
* Normalise a STORED row that still holds HTML (default true). Pass false
|
|
2788
|
+
* for text that was never HTML — an AI reply, say — because the strip
|
|
2789
|
+
* removes every `<…>` once any tag-shaped token appears, which would eat
|
|
2790
|
+
* `Promise<void>` and `<name@example.com>`.
|
|
2791
|
+
*/
|
|
2792
|
+
stripLegacyHtml?: boolean;
|
|
2793
|
+
}
|
|
2778
2794
|
|
|
2779
2795
|
/** Marker-free projection of `text`, for search, previews and a11y labels. */
|
|
2780
|
-
export function markdownToPlainText(
|
|
2796
|
+
export function markdownToPlainText(
|
|
2797
|
+
text: unknown,
|
|
2798
|
+
options?: MarkdownParseOptions,
|
|
2799
|
+
): string;
|
|
2781
2800
|
|
|
2782
2801
|
/** Reads one line as an image block, or `null` when it is not one. */
|
|
2783
2802
|
export function parseMarkdownImage(
|
|
@@ -2824,6 +2843,11 @@ export function normaliseMarkdownImageSize(size: unknown): MarkdownImageSize;
|
|
|
2824
2843
|
export const RichText: (props: {
|
|
2825
2844
|
/** Markdown text. Legacy HTML is stripped to plain text, never rendered. */
|
|
2826
2845
|
value?: string;
|
|
2846
|
+
/**
|
|
2847
|
+
* See `MarkdownParseOptions.stripLegacyHtml` (default true). Pass false for
|
|
2848
|
+
* text that was never an HTML row, such as an AI agent's reply.
|
|
2849
|
+
*/
|
|
2850
|
+
stripLegacyHtml?: boolean;
|
|
2827
2851
|
/**
|
|
2828
2852
|
* Resolves an image block to a node. Supply it when images are filestore
|
|
2829
2853
|
* ids — without it only absolute http(s) URLs render.
|
package/dist/markdown.js
CHANGED
|
@@ -155,14 +155,53 @@ function linkSpans(rawLabel, href) {
|
|
|
155
155
|
return spans.some((span) => span.text) ? spans : [{ text: href, href }];
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// CommonMark's flanking rules. Without them `created_at` loses its underscore
|
|
159
|
+
// and `2 * 4` loses its asterisk -- silent character deletion in ordinary
|
|
160
|
+
// prose, which an agent's reply has no author around to notice and correct.
|
|
161
|
+
// ASCII punctuation rather than \p{P}: Hermes is the native engine and a
|
|
162
|
+
// word char here is anything that is neither space nor punctuation, so an
|
|
163
|
+
// accented letter still counts as intraword.
|
|
164
|
+
const ASCII_PUNCT_RE = /[!-/:-@[-`{-~]/;
|
|
165
|
+
|
|
166
|
+
function isWordChar(char) {
|
|
167
|
+
return char !== "" && !/\s/.test(char) && !ASCII_PUNCT_RE.test(char);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// `delim` may open only when the run is not followed by whitespace, and `_`
|
|
171
|
+
// additionally may not open or close inside a word.
|
|
172
|
+
function emphasisFlanks(delim, content, source, openAt, closeEnd) {
|
|
173
|
+
if (content === "") return false;
|
|
174
|
+
if (/\s/.test(content.charAt(0))) return false;
|
|
175
|
+
if (/\s/.test(content.charAt(content.length - 1))) return false;
|
|
176
|
+
if (delim !== "_") return true;
|
|
177
|
+
const before = openAt > 0 ? source.charAt(openAt - 1) : "";
|
|
178
|
+
const after = source.charAt(closeEnd);
|
|
179
|
+
return !isWordChar(before) && !isWordChar(after);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Alternation order matters: `**` must be tried before `*`, or the bold
|
|
183
|
+
// marker is consumed as two italics. Group numbers are shared by both so the
|
|
184
|
+
// branch handling below reads the same match indices either way.
|
|
185
|
+
const LINK_PATTERN =
|
|
186
|
+
/(!?)\[((?:[^\]\\]|\\.)*)\]\(\s*(\S+?)\s*\)|(\*\*)(.+?)\4|(`)([^`]+?)\6|([*_])(.+?)\8/g;
|
|
187
|
+
// Same groups so the branch handling reads identical match indices; the link
|
|
188
|
+
// alternative is `[^\s\S]`, which can never match, so it is skipped for free.
|
|
189
|
+
const PLAIN_PATTERN =
|
|
190
|
+
/(!?)([^\s\S])([^\s\S])|(\*\*)(.+?)\4|(`)([^`]+?)\6|([*_])(.+?)\8/g;
|
|
191
|
+
|
|
158
192
|
/** Splits one line into `{ text, bold?, italic?, code?, href? }` spans. */
|
|
159
193
|
function parseInline(line) {
|
|
160
194
|
const source = typeof line === "string" ? line : "";
|
|
161
195
|
const spans = [];
|
|
162
196
|
// One pass over the four delimiters. Alternation order matters: `**` must
|
|
163
197
|
// be tried before `*` or the bold marker is consumed as two italics.
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
// The link branch scans forward from EVERY `[`, so a line carrying many
|
|
199
|
+
// unclosed brackets costs O(n x brackets) -- 4s for 50k of them, a frozen
|
|
200
|
+
// JS thread. A link needs `](`, so a line without one skips that branch
|
|
201
|
+
// entirely and stays linear. Same spans either way.
|
|
202
|
+
const mayHoldLink = source.includes("](") && source.includes(")");
|
|
203
|
+
const pattern = mayHoldLink ? LINK_PATTERN : PLAIN_PATTERN;
|
|
204
|
+
pattern.lastIndex = 0;
|
|
166
205
|
let cursor = 0;
|
|
167
206
|
let match = pattern.exec(source);
|
|
168
207
|
while (match) {
|
|
@@ -174,14 +213,37 @@ function parseInline(line) {
|
|
|
174
213
|
// literal text rather than becoming a link to the image.
|
|
175
214
|
if (match[1]) spans.push({ text: match[0] });
|
|
176
215
|
else for (const span of linkSpans(match[2], match[3])) spans.push(span);
|
|
177
|
-
} else if (match[4]
|
|
178
|
-
|
|
179
|
-
|
|
216
|
+
} else if (match[4] || match[8]) {
|
|
217
|
+
const delim = match[4] || match[8];
|
|
218
|
+
const content = match[4] ? match[5] : match[9];
|
|
219
|
+
const closeEnd = match.index + match[0].length;
|
|
220
|
+
if (!emphasisFlanks(delim, content, source, match.index, closeEnd)) {
|
|
221
|
+
// Not emphasis after all: keep the delimiter as text and rescan from
|
|
222
|
+
// just after it, so a later legitimate pair can still match.
|
|
223
|
+
spans.push({ text: delim });
|
|
224
|
+
cursor = match.index + delim.length;
|
|
225
|
+
pattern.lastIndex = cursor;
|
|
226
|
+
match = pattern.exec(source);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (match[4]) spans.push({ text: content, bold: true });
|
|
230
|
+
else spans.push({ text: content, italic: true });
|
|
231
|
+
} else if (match[6]) spans.push({ text: match[7], code: true });
|
|
180
232
|
cursor = match.index + match[0].length;
|
|
181
233
|
match = pattern.exec(source);
|
|
182
234
|
}
|
|
183
235
|
if (cursor < source.length) spans.push({ text: source.slice(cursor) });
|
|
184
|
-
|
|
236
|
+
if (spans.length === 0) return [{ text: source }];
|
|
237
|
+
// A rejected delimiter leaves plain fragments either side of it; fold them
|
|
238
|
+
// back so a line of prose stays one span instead of three.
|
|
239
|
+
const merged = [];
|
|
240
|
+
for (const span of spans) {
|
|
241
|
+
const last = merged[merged.length - 1];
|
|
242
|
+
const plain = (s) => !s.bold && !s.italic && !s.code && !s.href;
|
|
243
|
+
if (last && plain(last) && plain(span)) last.text += span.text;
|
|
244
|
+
else merged.push({ ...span });
|
|
245
|
+
}
|
|
246
|
+
return merged;
|
|
185
247
|
}
|
|
186
248
|
|
|
187
249
|
// `\|` is how a literal pipe survives inside a cell, so the split has to walk
|
|
@@ -263,11 +325,22 @@ export function formatMarkdownTable(columns = 3, rows = 2) {
|
|
|
263
325
|
/**
|
|
264
326
|
* Parses markdown text into renderable blocks:
|
|
265
327
|
* `{ type: "heading"|"paragraph"|"bullet"|"ordered", level?, marker?, spans }`,
|
|
266
|
-
* `{ type: "image", src, alt, size }`,
|
|
328
|
+
* `{ type: "image", src, alt, size }`,
|
|
329
|
+
* `{ type: "code", text, lang }`, or
|
|
267
330
|
* `{ type: "table", rows: [{ header, cells: spans[] }], align }`.
|
|
331
|
+
*
|
|
332
|
+
* `stripLegacyHtml` (default true) normalises stored rows that still hold HTML.
|
|
333
|
+
* Pass false for text that was never HTML -- a model's reply, say -- because
|
|
334
|
+
* the strip removes EVERY `<...>` once any tag-shaped token appears, which
|
|
335
|
+
* silently eats `Promise<void>` and `<name@example.com>`.
|
|
268
336
|
*/
|
|
269
|
-
export function parseMarkdown(text) {
|
|
270
|
-
const
|
|
337
|
+
export function parseMarkdown(text, options) {
|
|
338
|
+
const stripLegacy = options?.stripLegacyHtml !== false;
|
|
339
|
+
const source = stripLegacy
|
|
340
|
+
? stripHtmlToMarkdown(text)
|
|
341
|
+
: typeof text === "string"
|
|
342
|
+
? text
|
|
343
|
+
: "";
|
|
271
344
|
if (!source.trim()) return [];
|
|
272
345
|
|
|
273
346
|
const blocks = [];
|
|
@@ -284,6 +357,27 @@ export function parseMarkdown(text) {
|
|
|
284
357
|
const lines = source.split(/\r?\n/);
|
|
285
358
|
for (let i = 0; i < lines.length; i += 1) {
|
|
286
359
|
const line = lines[i].trimEnd();
|
|
360
|
+
|
|
361
|
+
// A fenced block is verbatim: its newlines and indentation ARE the content,
|
|
362
|
+
// and joining them into a paragraph made a code sample unreadable.
|
|
363
|
+
const fence = /^\s*```+\s*([^`]*)$/.exec(line);
|
|
364
|
+
if (fence) {
|
|
365
|
+
flushParagraph();
|
|
366
|
+
const body = [];
|
|
367
|
+
let j = i + 1;
|
|
368
|
+
while (j < lines.length && !/^\s*```+\s*$/.test(lines[j])) {
|
|
369
|
+
body.push(lines[j]);
|
|
370
|
+
j += 1;
|
|
371
|
+
}
|
|
372
|
+
blocks.push({
|
|
373
|
+
type: "code",
|
|
374
|
+
text: body.join("\n"),
|
|
375
|
+
lang: fence[1].trim(),
|
|
376
|
+
});
|
|
377
|
+
i = j;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
287
381
|
if (!line.trim()) {
|
|
288
382
|
flushParagraph();
|
|
289
383
|
continue;
|
|
@@ -344,10 +438,11 @@ function spansToText(spans) {
|
|
|
344
438
|
}
|
|
345
439
|
|
|
346
440
|
/** Plain-text projection — used for search, previews, and a11y labels. */
|
|
347
|
-
export function markdownToPlainText(text) {
|
|
348
|
-
return parseMarkdown(text)
|
|
441
|
+
export function markdownToPlainText(text, options) {
|
|
442
|
+
return parseMarkdown(text, options)
|
|
349
443
|
.map((block) => {
|
|
350
444
|
if (block.type === "image") return block.alt;
|
|
445
|
+
if (block.type === "code") return block.text;
|
|
351
446
|
// A table has no `spans` of its own; one line per row keeps a search
|
|
352
447
|
// match and a screen reader reading the cells in their visual order.
|
|
353
448
|
if (block.type === "table") {
|
package/dist/navigation.cjs
CHANGED
|
@@ -675,6 +675,64 @@ function resolveSidebarUnfold(input) {
|
|
|
675
675
|
return { under: section.key, items: marked, standalone: true };
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
/**
|
|
679
|
+
* REQ-NAV-LOCAL (sc-8901) — which local-navigation model a page wears in ONE
|
|
680
|
+
* device view, so the same saved menu can be a bottom bar on a phone and unfold
|
|
681
|
+
* in the rail on a desktop.
|
|
682
|
+
*
|
|
683
|
+
* `local_nav` is the MOBILE value and the base of the cascade: a page authored
|
|
684
|
+
* before the wider views existed leaves both of them unset and therefore
|
|
685
|
+
* renders identically at every width. The cascade is REQ-LAY-07's — the one the
|
|
686
|
+
* layout grid and every responsive widget prop already use — because a page's
|
|
687
|
+
* chrome and its content must not disagree about what a tablet is.
|
|
688
|
+
*
|
|
689
|
+
* The vocabulary stays the API's: any non-empty string is a model and the
|
|
690
|
+
* host's own switch decides what it can draw, so a kind a newer backend allows
|
|
691
|
+
* cascades correctly here instead of being flattened to `NONE` on the way.
|
|
692
|
+
*
|
|
693
|
+
* @param {object|null|undefined} page — a page row carrying `local_nav` and,
|
|
694
|
+
* optionally, `local_nav_tablet` / `local_nav_desktop`.
|
|
695
|
+
* @param {"mobile"|"tablet"|"desktop"} view — the device view being drawn.
|
|
696
|
+
* @returns {string} the model for that view; `NONE` when the page sets none.
|
|
697
|
+
*/
|
|
698
|
+
function resolveLocalNavModel(page, view) {
|
|
699
|
+
const at = (key) => {
|
|
700
|
+
const value = isPlainObject(page) ? page[key] : null;
|
|
701
|
+
return typeof value === "string" && value ? value : null;
|
|
702
|
+
};
|
|
703
|
+
const mobile = at("local_nav") || "NONE";
|
|
704
|
+
if (view === "desktop") {
|
|
705
|
+
return at("local_nav_desktop") || at("local_nav_tablet") || mobile;
|
|
706
|
+
}
|
|
707
|
+
if (view === "tablet") return at("local_nav_tablet") || mobile;
|
|
708
|
+
return mobile;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// sc-8901: the three device views a local bar can differ across — REQ-LAY-07's
|
|
712
|
+
// own list, so a page's chrome and its content classify a width alike. Ordered
|
|
713
|
+
// narrowest first, which is also the cascade's direction.
|
|
714
|
+
const LOCAL_NAV_VIEWS = ["mobile", "tablet", "desktop"];
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* REQ-NAV-LOCAL (sc-8901) — does this page draw a local bar in ANY device view?
|
|
718
|
+
*
|
|
719
|
+
* The saved-menu reference is shared by all three views, so it is required — and
|
|
720
|
+
* resolved onto the wire — as soon as one of them draws. Asking
|
|
721
|
+
* `local_nav !== "NONE"` alone would miss a page that carries a bar only on the
|
|
722
|
+
* wider views.
|
|
723
|
+
*
|
|
724
|
+
* Beside the cascade it is derived from, so the API rule, the export's screen
|
|
725
|
+
* bake and Copy Workspace cannot disagree about which pages have a bar.
|
|
726
|
+
*
|
|
727
|
+
* @param {object|null|undefined} page — a page row.
|
|
728
|
+
* @returns {boolean}
|
|
729
|
+
*/
|
|
730
|
+
function drawsLocalNavAnywhere(page) {
|
|
731
|
+
return LOCAL_NAV_VIEWS.some(
|
|
732
|
+
(view) => resolveLocalNavModel(page, view) !== "NONE",
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
678
736
|
// REQ-THEME-LOOK: the divider's width in px, bounded HERE so both hosts inherit
|
|
679
737
|
// one range. The export clamped it on its way into the StyleSheet
|
|
680
738
|
// (`coerceBorderWidth(...) ?? 1`) where the Player passed any finite number
|
|
@@ -711,4 +769,4 @@ function borderWidthOr(...values) {
|
|
|
711
769
|
return BORDER_WIDTH_MIN;
|
|
712
770
|
}
|
|
713
771
|
|
|
714
|
-
module.exports = { normaliseNavigation, menuItemCap, resolveFooterTokens, resolveSidebarTokens, resolveTopStripeLayout, resolveTopBarTokens, topStripeShowKey, TOP_STRIPE_ALWAYS_SHOWN_SLOTS, TOP_STRIPE_SLOTS, MIN_TOP_STRIPE_HEIGHT_PX, MAX_TOP_STRIPE_HEIGHT_PX, MIN_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_OFFSET_Y_PX, DEFAULT_TOP_STRIPE_HEIGHT_PX, DEFAULT_TOP_STRIPE_PADDING_X_PX, resolveSidebarUnfold };
|
|
772
|
+
module.exports = { normaliseNavigation, menuItemCap, resolveFooterTokens, resolveSidebarTokens, resolveTopStripeLayout, resolveTopBarTokens, topStripeShowKey, TOP_STRIPE_ALWAYS_SHOWN_SLOTS, TOP_STRIPE_SLOTS, MIN_TOP_STRIPE_HEIGHT_PX, MAX_TOP_STRIPE_HEIGHT_PX, MIN_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_OFFSET_Y_PX, DEFAULT_TOP_STRIPE_HEIGHT_PX, DEFAULT_TOP_STRIPE_PADDING_X_PX, resolveSidebarUnfold, resolveLocalNavModel, drawsLocalNavAnywhere };
|
package/dist/navigation.js
CHANGED
|
@@ -666,6 +666,64 @@ export function resolveSidebarUnfold(input) {
|
|
|
666
666
|
return { under: section.key, items: marked, standalone: true };
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
/**
|
|
670
|
+
* REQ-NAV-LOCAL (sc-8901) — which local-navigation model a page wears in ONE
|
|
671
|
+
* device view, so the same saved menu can be a bottom bar on a phone and unfold
|
|
672
|
+
* in the rail on a desktop.
|
|
673
|
+
*
|
|
674
|
+
* `local_nav` is the MOBILE value and the base of the cascade: a page authored
|
|
675
|
+
* before the wider views existed leaves both of them unset and therefore
|
|
676
|
+
* renders identically at every width. The cascade is REQ-LAY-07's — the one the
|
|
677
|
+
* layout grid and every responsive widget prop already use — because a page's
|
|
678
|
+
* chrome and its content must not disagree about what a tablet is.
|
|
679
|
+
*
|
|
680
|
+
* The vocabulary stays the API's: any non-empty string is a model and the
|
|
681
|
+
* host's own switch decides what it can draw, so a kind a newer backend allows
|
|
682
|
+
* cascades correctly here instead of being flattened to `NONE` on the way.
|
|
683
|
+
*
|
|
684
|
+
* @param {object|null|undefined} page — a page row carrying `local_nav` and,
|
|
685
|
+
* optionally, `local_nav_tablet` / `local_nav_desktop`.
|
|
686
|
+
* @param {"mobile"|"tablet"|"desktop"} view — the device view being drawn.
|
|
687
|
+
* @returns {string} the model for that view; `NONE` when the page sets none.
|
|
688
|
+
*/
|
|
689
|
+
export function resolveLocalNavModel(page, view) {
|
|
690
|
+
const at = (key) => {
|
|
691
|
+
const value = isPlainObject(page) ? page[key] : null;
|
|
692
|
+
return typeof value === "string" && value ? value : null;
|
|
693
|
+
};
|
|
694
|
+
const mobile = at("local_nav") || "NONE";
|
|
695
|
+
if (view === "desktop") {
|
|
696
|
+
return at("local_nav_desktop") || at("local_nav_tablet") || mobile;
|
|
697
|
+
}
|
|
698
|
+
if (view === "tablet") return at("local_nav_tablet") || mobile;
|
|
699
|
+
return mobile;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// sc-8901: the three device views a local bar can differ across — REQ-LAY-07's
|
|
703
|
+
// own list, so a page's chrome and its content classify a width alike. Ordered
|
|
704
|
+
// narrowest first, which is also the cascade's direction.
|
|
705
|
+
const LOCAL_NAV_VIEWS = ["mobile", "tablet", "desktop"];
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* REQ-NAV-LOCAL (sc-8901) — does this page draw a local bar in ANY device view?
|
|
709
|
+
*
|
|
710
|
+
* The saved-menu reference is shared by all three views, so it is required — and
|
|
711
|
+
* resolved onto the wire — as soon as one of them draws. Asking
|
|
712
|
+
* `local_nav !== "NONE"` alone would miss a page that carries a bar only on the
|
|
713
|
+
* wider views.
|
|
714
|
+
*
|
|
715
|
+
* Beside the cascade it is derived from, so the API rule, the export's screen
|
|
716
|
+
* bake and Copy Workspace cannot disagree about which pages have a bar.
|
|
717
|
+
*
|
|
718
|
+
* @param {object|null|undefined} page — a page row.
|
|
719
|
+
* @returns {boolean}
|
|
720
|
+
*/
|
|
721
|
+
export function drawsLocalNavAnywhere(page) {
|
|
722
|
+
return LOCAL_NAV_VIEWS.some(
|
|
723
|
+
(view) => resolveLocalNavModel(page, view) !== "NONE",
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
669
727
|
// REQ-THEME-LOOK: the divider's width in px, bounded HERE so both hosts inherit
|
|
670
728
|
// one range. The export clamped it on its way into the StyleSheet
|
|
671
729
|
// (`coerceBorderWidth(...) ?? 1`) where the Player passed any finite number
|
package/dist/richtext-view.js
CHANGED
|
@@ -126,13 +126,23 @@ export function makeRichText(rn) {
|
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function RichText({
|
|
129
|
+
function RichText({
|
|
130
|
+
value,
|
|
131
|
+
renderImage,
|
|
132
|
+
followLinks = true,
|
|
133
|
+
stripLegacyHtml = true,
|
|
134
|
+
style,
|
|
135
|
+
testID,
|
|
136
|
+
}) {
|
|
130
137
|
const theme = useHostTheme();
|
|
131
138
|
// The non-throwing reader, so RichText stays renderable outside a widget
|
|
132
139
|
// host (a Studio preview, a test) the way DateTimePicker does.
|
|
133
140
|
const navigation = useHostNavigation();
|
|
134
141
|
const tokens = React.useMemo(() => resolveRichTextTokens(theme), [theme]);
|
|
135
|
-
const blocks = React.useMemo(
|
|
142
|
+
const blocks = React.useMemo(
|
|
143
|
+
() => parseMarkdown(value, { stripLegacyHtml }),
|
|
144
|
+
[value, stripLegacyHtml],
|
|
145
|
+
);
|
|
136
146
|
|
|
137
147
|
// sc-7349: `openLink` is the platform's ONE link resolver — it decides page
|
|
138
148
|
// / external / refuse, so a stored `javascript:` target can never be
|
|
@@ -157,6 +167,38 @@ export function makeRichText(rn) {
|
|
|
157
167
|
View,
|
|
158
168
|
{ style, testID },
|
|
159
169
|
blocks.map((block, i) => {
|
|
170
|
+
const gap = i === blocks.length - 1 ? 0 : tokens.blockGap;
|
|
171
|
+
|
|
172
|
+
if (block.type === "code") {
|
|
173
|
+
return React.createElement(
|
|
174
|
+
View,
|
|
175
|
+
{
|
|
176
|
+
key: i,
|
|
177
|
+
style: {
|
|
178
|
+
backgroundColor: tokens.surface,
|
|
179
|
+
borderColor: tokens.border,
|
|
180
|
+
borderWidth: 1,
|
|
181
|
+
borderRadius: 6,
|
|
182
|
+
padding: 8,
|
|
183
|
+
marginBottom: gap,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
React.createElement(
|
|
187
|
+
Text,
|
|
188
|
+
{
|
|
189
|
+
style: {
|
|
190
|
+
// The theme carries no mono token, as the inline span notes.
|
|
191
|
+
fontFamily: "monospace",
|
|
192
|
+
fontSize: tokens.codeSize,
|
|
193
|
+
lineHeight: Math.round(tokens.codeSize * 1.5),
|
|
194
|
+
color: tokens.color,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
block.text,
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
160
202
|
if (block.type === "table") {
|
|
161
203
|
return React.createElement(TableBlock, {
|
|
162
204
|
key: i,
|
|
@@ -185,6 +227,7 @@ export function makeRichText(rn) {
|
|
|
185
227
|
fontWeight: "700",
|
|
186
228
|
fontSize: tokens.headingSizes[block.level] || tokens.headingSizes[3],
|
|
187
229
|
lineHeight: undefined,
|
|
230
|
+
marginBottom: gap,
|
|
188
231
|
},
|
|
189
232
|
],
|
|
190
233
|
},
|
|
@@ -196,7 +239,7 @@ export function makeRichText(rn) {
|
|
|
196
239
|
if (typeof renderImage === "function") {
|
|
197
240
|
return React.createElement(
|
|
198
241
|
View,
|
|
199
|
-
{ key: i, style: { marginBottom:
|
|
242
|
+
{ key: i, style: { marginBottom: gap } },
|
|
200
243
|
renderImage(block),
|
|
201
244
|
);
|
|
202
245
|
}
|
|
@@ -207,7 +250,7 @@ export function makeRichText(rn) {
|
|
|
207
250
|
{
|
|
208
251
|
key: i,
|
|
209
252
|
style: {
|
|
210
|
-
marginBottom:
|
|
253
|
+
marginBottom: gap,
|
|
211
254
|
width: tokens.imageWidth[block.size] || tokens.imageWidth.full,
|
|
212
255
|
},
|
|
213
256
|
},
|
|
@@ -243,7 +286,7 @@ export function makeRichText(rn) {
|
|
|
243
286
|
if (block.type === "bullet" || block.type === "ordered") {
|
|
244
287
|
return React.createElement(
|
|
245
288
|
View,
|
|
246
|
-
{ key: i, style: { flexDirection: "row" } },
|
|
289
|
+
{ key: i, style: { flexDirection: "row", marginBottom: gap } },
|
|
247
290
|
React.createElement(
|
|
248
291
|
Text,
|
|
249
292
|
{
|
|
@@ -254,11 +297,19 @@ export function makeRichText(rn) {
|
|
|
254
297
|
},
|
|
255
298
|
block.marker,
|
|
256
299
|
),
|
|
257
|
-
React.createElement(
|
|
300
|
+
React.createElement(
|
|
301
|
+
Text,
|
|
302
|
+
{ style: [paragraph, { flex: 1, marginBottom: 0 }] },
|
|
303
|
+
spans,
|
|
304
|
+
),
|
|
258
305
|
);
|
|
259
306
|
}
|
|
260
307
|
|
|
261
|
-
return React.createElement(
|
|
308
|
+
return React.createElement(
|
|
309
|
+
Text,
|
|
310
|
+
{ key: i, style: [paragraph, { marginBottom: gap }] },
|
|
311
|
+
spans,
|
|
312
|
+
);
|
|
262
313
|
}),
|
|
263
314
|
);
|
|
264
315
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.168.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|