@ng-native/components 0.13.0 → 0.15.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.
Files changed (41) hide show
  1. package/dist/keyboard-avoiding-view.js +3 -2
  2. package/dist/keyboard-avoiding-view.js.map +1 -1
  3. package/dist/markdown-blocks.d.ts +18 -1
  4. package/dist/markdown-blocks.js +112 -4
  5. package/dist/markdown-blocks.js.map +1 -1
  6. package/dist/markdown-columns.d.ts +24 -0
  7. package/dist/markdown-columns.js +58 -0
  8. package/dist/markdown-columns.js.map +1 -0
  9. package/dist/markdown-stream.d.ts +31 -0
  10. package/dist/markdown-stream.js +282 -0
  11. package/dist/markdown-stream.js.map +1 -0
  12. package/dist/markdown.d.ts +30 -3
  13. package/dist/markdown.js +131 -41
  14. package/dist/markdown.js.map +1 -1
  15. package/dist/modal.d.ts +8 -2
  16. package/dist/modal.js +8 -5
  17. package/dist/modal.js.map +1 -1
  18. package/dist/pressable.js +6 -6
  19. package/dist/pressable.js.map +1 -1
  20. package/dist/section-list.js +4 -4
  21. package/dist/section-list.js.map +1 -1
  22. package/dist/swipe-refresh-layout.js +1 -1
  23. package/dist/swipe-refresh-layout.js.map +1 -1
  24. package/dist/text-input.d.ts +6 -1
  25. package/dist/text-input.js +8 -2
  26. package/dist/text-input.js.map +1 -1
  27. package/dist/touchable-opacity.js +3 -2
  28. package/dist/touchable-opacity.js.map +1 -1
  29. package/dist/transforms.d.ts +6 -0
  30. package/dist/transforms.js +8 -0
  31. package/dist/transforms.js.map +1 -1
  32. package/dist/view-base.d.ts +1 -1
  33. package/dist/view-base.js +6 -4
  34. package/dist/view-base.js.map +1 -1
  35. package/dist/virtual-list.js +6 -5
  36. package/dist/virtual-list.js.map +1 -1
  37. package/dist/written-props.js +1 -0
  38. package/dist/written-props.js.map +1 -1
  39. package/dist/zero-size-warning.js +2 -2
  40. package/dist/zero-size-warning.js.map +1 -1
  41. package/package.json +3 -3
package/dist/markdown.js CHANGED
@@ -9,12 +9,14 @@
9
9
  * import { Markdown } from '@ng-native/components/markdown';
10
10
  * ```
11
11
  */
12
- import { Component, Injector, computed, inject, input, linkedSignal, output } from '@angular/core';
12
+ import { Component, Injector, booleanAttribute, computed, inject, input, linkedSignal, output, } from '@angular/core';
13
13
  import { DeepLinks } from '@ng-native/device';
14
14
  import { nativePlatform } from '@ng-native/fabric';
15
15
  import { lexer } from 'marked';
16
16
  import { Image } from "./image.js";
17
- import { markdownBlocks, opensOnPress, } from "./markdown-blocks.js";
17
+ import { blockKey, markdownBlocks, opensOnPress, shareBlocks, } from "./markdown-blocks.js";
18
+ import { MarkdownColumns } from "./markdown-columns.js";
19
+ import { MarkdownStream } from "./markdown-stream.js";
18
20
  import { ScrollView } from "./scroll-view.js";
19
21
  import { TemplateSlot } from "./template-slot.js";
20
22
  import { Text } from "./text.js";
@@ -56,6 +58,15 @@ function imageSources(blocks, into = new Set()) {
56
58
  * dark. `classes` replaces one by its element's name, so a global stylesheet or Tailwind styles it
57
59
  * instead: `{ h1: 'text-3xl font-bold' }`. Put the default back beside your own to keep both.
58
60
  *
61
+ * For a source that grows as it streams in, an answer arriving a few words at a time, set
62
+ * `streaming` until it is complete. The blocks before the last one are then kept as they are and
63
+ * only the last is lexed and converted again, unless a chunk defines or changes a link reference,
64
+ * which can change a block already kept, and the whole source is lexed again. Syntax still open at its end, a code span, an emphasis, a link,
65
+ * an image or a table row, is held back until it closes. Of a code fence only an unfinished
66
+ * opening line is held back, and its code then streams as it arrives. Clearing `streaming` draws
67
+ * the whole source as written, and `tokens` are always drawn as given. A block that did not
68
+ * change keeps its views, streaming or not.
69
+ *
59
70
  * A pressed link is `(linkPress)`. An absolute target is then opened with `DeepLinks.open` unless
60
71
  * a handler calls `preventDefault()`; a relative one is only reported, for the app to route.
61
72
  */
@@ -69,13 +80,36 @@ export class Markdown {
69
80
  /** Tokens `marked.lexer` made already, drawn in place of `source`. */
70
81
  tokens = input(/* @ts-ignore */
71
82
  ...(ngDevMode ? [undefined, { debugName: "tokens" }] : /* istanbul ignore next */ []));
83
+ /**
84
+ * Whether `source` is still arriving, a chunk at a time. While it is, only its last block is
85
+ * lexed again, unless a link reference changes, and syntax still open at its end is held back.
86
+ * See the class note.
87
+ */
88
+ streaming = input(false, { ...(ngDevMode ? { debugName: "streaming" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
72
89
  /** A class for an element, in place of its default. See the class note. */
73
90
  classes = input({}, /* @ts-ignore */
74
91
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
75
92
  /** A link was pressed. See the class note. */
76
93
  linkPress = output();
77
- blocks = computed(() => markdownBlocks(this.tokens() ?? lexer(this.source() ?? '')), /* @ts-ignore */
78
- ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
94
+ stream = new MarkdownStream();
95
+ blockKey = blockKey;
96
+ /**
97
+ * The blocks each token the stream kept was drawn as. A kept token is the same object from one
98
+ * chunk to the next, so its blocks are not converted or compared again; a source lexed whole
99
+ * makes new tokens, and so new blocks.
100
+ */
101
+ streamed = new WeakMap();
102
+ /** The document's blocks, with each one that did not change kept, so it is not drawn again. */
103
+ blocks = linkedSignal({ ...(ngDevMode ? { debugName: "blocks" } : /* istanbul ignore next */ {}), source: () => {
104
+ const tokens = this.tokens();
105
+ if (tokens)
106
+ return { tokens, streamed: false };
107
+ const streamed = this.streaming();
108
+ return { tokens: this.lex(this.source() ?? '', streamed), streamed };
109
+ },
110
+ computation: ({ tokens, streamed }, previous) => streamed
111
+ ? this.streamedBlocks(tokens, previous?.value)
112
+ : shareBlocks(previous?.value, markdownBlocks(tokens)) });
79
113
  classOf = computed(() => ({ ...DEFAULT_CLASSES, ...this.classes() }), /* @ts-ignore */
80
114
  ...(ngDevMode ? [{ debugName: "classOf" }] : /* istanbul ignore next */ []));
81
115
  /**
@@ -93,6 +127,32 @@ export class Markdown {
93
127
  kept.set(src, ratio);
94
128
  return kept;
95
129
  } });
130
+ lex(source, streaming) {
131
+ return streaming ? this.stream.lex(source) : lexer(source);
132
+ }
133
+ /** `tokens`' blocks, converting and sharing only those of tokens the stream had not kept. */
134
+ streamedBlocks(tokens, previous) {
135
+ const kept = [];
136
+ let at = 0;
137
+ for (; at < tokens.length; at++) {
138
+ const blocks = this.streamed.get(tokens[at]);
139
+ if (!blocks)
140
+ break;
141
+ kept.push(...blocks);
142
+ }
143
+ const rest = tokens.slice(at);
144
+ const converted = rest.map((token) => markdownBlocks([token]));
145
+ // The blocks drawn after the kept ones, when those are still the ones drawn first.
146
+ const drawn = !kept.length || previous?.[kept.length - 1] === kept.at(-1);
147
+ const was = drawn ? previous?.slice(kept.length) : undefined;
148
+ const shared = shareBlocks(was, converted.flat());
149
+ let from = 0;
150
+ rest.forEach((token, i) => {
151
+ const count = converted[i].length;
152
+ this.streamed.set(token, shared.slice(from, (from += count)));
153
+ });
154
+ return [...kept, ...shared];
155
+ }
96
156
  classFor(element) {
97
157
  return this.classOf()[element];
98
158
  }
@@ -118,9 +178,9 @@ export class Markdown {
118
178
  this.injector.get(DeepLinks).open(link.href);
119
179
  }
120
180
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Markdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
121
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: Markdown, isStandalone: true, selector: "markdown", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: false, transformFunction: null }, classes: { classPropertyName: "classes", publicName: "classes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { linkPress: "linkPress" }, host: { properties: { "class.md-ios": "platform === 'ios'", "class.md-android": "platform === 'android'" } }, ngImport: i0, template: `
181
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: Markdown, isStandalone: true, selector: "markdown", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: false, transformFunction: null }, streaming: { classPropertyName: "streaming", publicName: "streaming", isSignal: true, isRequired: false, transformFunction: null }, classes: { classPropertyName: "classes", publicName: "classes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { linkPress: "linkPress" }, host: { properties: { "class.md-ios": "platform === 'ios'", "class.md-android": "platform === 'android'" } }, ngImport: i0, template: `
122
182
  <ng-template #blockTemplate let-list>
123
- @for (block of list; track $index) {
183
+ @for (block of list; track blockKey(block)) {
124
184
  @switch (block.kind) {
125
185
  @case ('heading') {
126
186
  <text [class]="classFor(block.element)" accessibilityRole="header"
@@ -171,24 +231,39 @@ export class Markdown {
171
231
  <view [class]="classFor('hr')"></view>
172
232
  }
173
233
  @case ('table') {
174
- <view [class]="classFor('table')">
175
- <view [class]="classFor('tr')">
176
- @for (cell of block.header; track $index) {
177
- <text [class]="classFor('th')"
178
- ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
179
- /></text>
180
- }
181
- </view>
182
- @for (row of block.rows; track $index) {
234
+ <scroll-view horizontal>
235
+ <view
236
+ [class]="classFor('table')"
237
+ [markdownColumns]="block"
238
+ #columns="markdownColumns"
239
+ >
183
240
  <view [class]="classFor('tr')">
184
- @for (cell of row; track $index) {
185
- <text [class]="classFor('td')"
186
- ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
187
- /></text>
241
+ @for (cell of block.header; track $index; let column = $index) {
242
+ <view class="md-cell" [style.minWidth]="columns.width(column)">
243
+ <text
244
+ [class]="classFor('th')"
245
+ (layout)="columns.measure(0, column, $any($event))"
246
+ ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
247
+ /></text>
248
+ </view>
188
249
  }
189
250
  </view>
190
- }
191
- </view>
251
+ @for (row of block.rows; track $index; let r = $index) {
252
+ <view [class]="classFor('tr')">
253
+ @for (cell of row; track $index; let column = $index) {
254
+ <view class="md-cell md-cell-body" [style.minWidth]="columns.width(column)">
255
+ <text
256
+ [class]="classFor('td')"
257
+ (layout)="columns.measure(r + 1, column, $any($event))"
258
+ ><ng-container
259
+ *templateSlot="inlineTemplate; context: { $implicit: cell }"
260
+ /></text>
261
+ </view>
262
+ }
263
+ </view>
264
+ }
265
+ </view>
266
+ </scroll-view>
192
267
  }
193
268
  }
194
269
  }
@@ -222,13 +297,13 @@ export class Markdown {
222
297
  </ng-template>
223
298
 
224
299
  <ng-container *templateSlot="blockTemplate; context: { $implicit: blocks() }" />
225
- `, isInline: true, styles: ["/* What a document looks like with no `classes` of the app's, in light and in dark. */\n\n:host {\n gap: 12px;\n color: #1c1c1e;\n}\n\n.md-h1,\n.md-h2,\n.md-h3,\n.md-h4,\n.md-h5,\n.md-h6 {\n font-weight: 700;\n}\n\n.md-h1 {\n font-size: 28px;\n line-height: 34px;\n}\n\n.md-h2 {\n font-size: 22px;\n line-height: 28px;\n}\n\n.md-h3 {\n font-size: 20px;\n line-height: 25px;\n}\n\n.md-h4 {\n font-size: 17px;\n line-height: 22px;\n}\n\n.md-h5 {\n font-size: 15px;\n line-height: 20px;\n}\n\n.md-h6 {\n font-size: 13px;\n line-height: 18px;\n color: #6c6c70;\n}\n\n.md-p,\n.md-marker,\n.md-th,\n.md-td {\n font-size: 16px;\n line-height: 23px;\n}\n\n.md-strong {\n font-weight: 700;\n}\n\n.md-em {\n font-style: italic;\n}\n\n.md-del {\n text-decoration-line: line-through;\n}\n\n.md-a {\n color: #0a66d8;\n text-decoration-line: underline;\n}\n\n.md-code {\n background-color: rgba(120, 120, 128, 0.16);\n}\n\n.md-code,\n.md-pre {\n font-family: Courier New;\n}\n\n:host(.md-ios) .md-code,\n:host(.md-ios) .md-pre {\n font-family: Menlo;\n}\n\n:host(.md-android) .md-code,\n:host(.md-android) .md-pre {\n font-family: monospace;\n}\n\n.md-blockquote {\n gap: 8px;\n padding-left: 12px;\n border-left-width: 3px;\n border-left-color: #d1d1d6;\n color: #6c6c70;\n}\n\n.md-ul,\n.md-ol {\n gap: 4px;\n}\n\n.md-li {\n flex-direction: row;\n gap: 8px;\n}\n\n.md-marker {\n min-width: 16px;\n}\n\n.md-li-body {\n flex: 1;\n gap: 4px;\n}\n\n.md-pre {\n flex-grow: 0;\n border-radius: 8px;\n background-color: #f2f2f7;\n}\n\n.md-pre-text {\n padding: 12px;\n font-size: 14px;\n line-height: 20px;\n}\n\n.md-hr {\n height: 1px;\n background-color: #d1d1d6;\n}\n\n.md-img {\n width: 100%;\n height: 200px;\n border-radius: 8px;\n}\n\n.md-table {\n border-width: 1px;\n border-color: #d1d1d6;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.md-tr {\n flex-direction: row;\n}\n\n.md-th,\n.md-td {\n flex: 1;\n padding: 6px 8px;\n}\n\n.md-td {\n border-top-width: 1px;\n border-top-color: #d1d1d6;\n}\n\n.md-th {\n font-weight: 600;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n color: #f2f2f7;\n }\n\n .md-h6,\n .md-blockquote {\n color: #98989f;\n }\n\n .md-a {\n color: #4c9bff;\n }\n\n .md-code {\n background-color: rgba(120, 120, 128, 0.32);\n }\n\n .md-pre {\n background-color: #1c1c1e;\n }\n\n .md-blockquote,\n .md-table,\n .md-td {\n border-color: #38383a;\n }\n\n .md-hr {\n background-color: #38383a;\n }\n}\n"], dependencies: [{ kind: "directive", type: Image, selector: "image", inputs: ["source", "src", "srcSet", "defaultSource", "loadingIndicatorSource", "resizeMode", "resizeMethod", "resizeMultiplier", "blurRadius", "capInsets", "tintColor", "fadeDuration", "progressiveRenderingEnabled", "alt", "crossOrigin", "referrerPolicy"] }, { kind: "component", type: ScrollView, selector: "scroll-view", inputs: ["contentContainerStyle", "contentContainerClass", "horizontal", "contentOffset", "maintainVisibleContentPosition", "keyboardShouldPersistTaps", "nestedScrollEnabled", "stickyHeaderIndices"], outputs: ["contentSizeChange"], exportAs: ["scrollView"] }, { kind: "directive", type: TemplateSlot, selector: "[templateSlot]", inputs: ["templateSlot", "templateSlotContext"] }, { kind: "directive", type: Text, selector: "text", inputs: ["numberOfLines", "ellipsizeMode", "selectable", "selectionColor", "allowFontScaling", "maxFontSizeMultiplier", "dynamicTypeRamp", "adjustsFontSizeToFit", "minimumFontScale", "lineBreakStrategyIOS", "textBreakStrategy", "dataDetectorType", "android_hyphenationFrequency", "pressable", "suppressHighlighting"], exportAs: ["text"] }, { kind: "directive", type: View, selector: "view" }] });
300
+ `, isInline: true, styles: ["/* What a document looks like with no `classes` of the app's, in light and in dark. */\n\n:host {\n gap: 12px;\n color: #1c1c1e;\n}\n\n.md-h1,\n.md-h2,\n.md-h3,\n.md-h4,\n.md-h5,\n.md-h6 {\n font-weight: 700;\n}\n\n.md-h1 {\n font-size: 28px;\n line-height: 34px;\n}\n\n.md-h2 {\n font-size: 22px;\n line-height: 28px;\n}\n\n.md-h3 {\n font-size: 20px;\n line-height: 25px;\n}\n\n.md-h4 {\n font-size: 17px;\n line-height: 22px;\n}\n\n.md-h5 {\n font-size: 15px;\n line-height: 20px;\n}\n\n.md-h6 {\n font-size: 13px;\n line-height: 18px;\n color: #6c6c70;\n}\n\n.md-p,\n.md-marker,\n.md-th,\n.md-td {\n font-size: 16px;\n line-height: 23px;\n}\n\n.md-strong {\n font-weight: 700;\n}\n\n.md-em {\n font-style: italic;\n}\n\n.md-del {\n text-decoration-line: line-through;\n}\n\n.md-a {\n color: #0a66d8;\n text-decoration-line: underline;\n}\n\n.md-code {\n background-color: rgba(120, 120, 128, 0.16);\n}\n\n.md-code,\n.md-pre {\n font-family: Courier New;\n}\n\n:host(.md-ios) .md-code,\n:host(.md-ios) .md-pre {\n font-family: Menlo;\n}\n\n:host(.md-android) .md-code,\n:host(.md-android) .md-pre {\n font-family: monospace;\n}\n\n.md-blockquote {\n gap: 8px;\n padding-left: 12px;\n border-left-width: 3px;\n border-left-color: #d1d1d6;\n color: #6c6c70;\n}\n\n.md-ul,\n.md-ol {\n gap: 4px;\n}\n\n.md-li {\n flex-direction: row;\n gap: 8px;\n}\n\n.md-marker {\n min-width: 16px;\n}\n\n.md-li-body {\n flex: 1;\n gap: 4px;\n}\n\n.md-pre {\n flex-grow: 0;\n border-radius: 8px;\n background-color: #f2f2f7;\n}\n\n.md-pre-text {\n padding: 12px;\n font-size: 14px;\n line-height: 20px;\n}\n\n.md-hr {\n height: 1px;\n background-color: #d1d1d6;\n}\n\n.md-img {\n width: 100%;\n height: 200px;\n border-radius: 8px;\n}\n\n.md-table {\n border-width: 1px;\n border-color: #d1d1d6;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.md-tr {\n flex-direction: row;\n}\n\n/* The text hugs its content inside the column's least width, so it can report a narrower one. */\n.md-cell {\n align-items: flex-start;\n}\n\n.md-th,\n.md-td {\n max-width: 280px;\n padding: 6px 8px;\n}\n\n.md-cell-body {\n border-top-width: 1px;\n border-top-color: #d1d1d6;\n}\n\n.md-th {\n font-weight: 600;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n color: #f2f2f7;\n }\n\n .md-h6,\n .md-blockquote {\n color: #98989f;\n }\n\n .md-a {\n color: #4c9bff;\n }\n\n .md-code {\n background-color: rgba(120, 120, 128, 0.32);\n }\n\n .md-pre {\n background-color: #1c1c1e;\n }\n\n .md-blockquote,\n .md-table,\n .md-cell-body {\n border-color: #38383a;\n }\n\n .md-hr {\n background-color: #38383a;\n }\n}\n"], dependencies: [{ kind: "directive", type: Image, selector: "image", inputs: ["source", "src", "srcSet", "defaultSource", "loadingIndicatorSource", "resizeMode", "resizeMethod", "resizeMultiplier", "blurRadius", "capInsets", "tintColor", "fadeDuration", "progressiveRenderingEnabled", "alt", "crossOrigin", "referrerPolicy"] }, { kind: "directive", type: MarkdownColumns, selector: "[markdownColumns]", inputs: ["markdownColumns"], exportAs: ["markdownColumns"] }, { kind: "component", type: ScrollView, selector: "scroll-view", inputs: ["contentContainerStyle", "contentContainerClass", "horizontal", "contentOffset", "maintainVisibleContentPosition", "keyboardShouldPersistTaps", "nestedScrollEnabled", "stickyHeaderIndices"], outputs: ["contentSizeChange"], exportAs: ["scrollView"] }, { kind: "directive", type: TemplateSlot, selector: "[templateSlot]", inputs: ["templateSlot", "templateSlotContext"] }, { kind: "directive", type: Text, selector: "text", inputs: ["numberOfLines", "ellipsizeMode", "selectable", "selectionColor", "allowFontScaling", "maxFontSizeMultiplier", "dynamicTypeRamp", "adjustsFontSizeToFit", "minimumFontScale", "lineBreakStrategyIOS", "textBreakStrategy", "dataDetectorType", "android_hyphenationFrequency", "pressable", "suppressHighlighting"], exportAs: ["text"] }, { kind: "directive", type: View, selector: "view" }] });
226
301
  }
227
302
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Markdown, decorators: [{
228
303
  type: Component,
229
- args: [{ selector: 'markdown', imports: [Image, ScrollView, TemplateSlot, Text, View], template: `
304
+ args: [{ selector: 'markdown', imports: [Image, MarkdownColumns, ScrollView, TemplateSlot, Text, View], template: `
230
305
  <ng-template #blockTemplate let-list>
231
- @for (block of list; track $index) {
306
+ @for (block of list; track blockKey(block)) {
232
307
  @switch (block.kind) {
233
308
  @case ('heading') {
234
309
  <text [class]="classFor(block.element)" accessibilityRole="header"
@@ -279,24 +354,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
279
354
  <view [class]="classFor('hr')"></view>
280
355
  }
281
356
  @case ('table') {
282
- <view [class]="classFor('table')">
283
- <view [class]="classFor('tr')">
284
- @for (cell of block.header; track $index) {
285
- <text [class]="classFor('th')"
286
- ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
287
- /></text>
288
- }
289
- </view>
290
- @for (row of block.rows; track $index) {
357
+ <scroll-view horizontal>
358
+ <view
359
+ [class]="classFor('table')"
360
+ [markdownColumns]="block"
361
+ #columns="markdownColumns"
362
+ >
291
363
  <view [class]="classFor('tr')">
292
- @for (cell of row; track $index) {
293
- <text [class]="classFor('td')"
294
- ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
295
- /></text>
364
+ @for (cell of block.header; track $index; let column = $index) {
365
+ <view class="md-cell" [style.minWidth]="columns.width(column)">
366
+ <text
367
+ [class]="classFor('th')"
368
+ (layout)="columns.measure(0, column, $any($event))"
369
+ ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
370
+ /></text>
371
+ </view>
296
372
  }
297
373
  </view>
298
- }
299
- </view>
374
+ @for (row of block.rows; track $index; let r = $index) {
375
+ <view [class]="classFor('tr')">
376
+ @for (cell of row; track $index; let column = $index) {
377
+ <view class="md-cell md-cell-body" [style.minWidth]="columns.width(column)">
378
+ <text
379
+ [class]="classFor('td')"
380
+ (layout)="columns.measure(r + 1, column, $any($event))"
381
+ ><ng-container
382
+ *templateSlot="inlineTemplate; context: { $implicit: cell }"
383
+ /></text>
384
+ </view>
385
+ }
386
+ </view>
387
+ }
388
+ </view>
389
+ </scroll-view>
300
390
  }
301
391
  }
302
392
  }
@@ -333,6 +423,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
333
423
  `, host: {
334
424
  '[class.md-ios]': "platform === 'ios'",
335
425
  '[class.md-android]': "platform === 'android'",
336
- }, styles: ["/* What a document looks like with no `classes` of the app's, in light and in dark. */\n\n:host {\n gap: 12px;\n color: #1c1c1e;\n}\n\n.md-h1,\n.md-h2,\n.md-h3,\n.md-h4,\n.md-h5,\n.md-h6 {\n font-weight: 700;\n}\n\n.md-h1 {\n font-size: 28px;\n line-height: 34px;\n}\n\n.md-h2 {\n font-size: 22px;\n line-height: 28px;\n}\n\n.md-h3 {\n font-size: 20px;\n line-height: 25px;\n}\n\n.md-h4 {\n font-size: 17px;\n line-height: 22px;\n}\n\n.md-h5 {\n font-size: 15px;\n line-height: 20px;\n}\n\n.md-h6 {\n font-size: 13px;\n line-height: 18px;\n color: #6c6c70;\n}\n\n.md-p,\n.md-marker,\n.md-th,\n.md-td {\n font-size: 16px;\n line-height: 23px;\n}\n\n.md-strong {\n font-weight: 700;\n}\n\n.md-em {\n font-style: italic;\n}\n\n.md-del {\n text-decoration-line: line-through;\n}\n\n.md-a {\n color: #0a66d8;\n text-decoration-line: underline;\n}\n\n.md-code {\n background-color: rgba(120, 120, 128, 0.16);\n}\n\n.md-code,\n.md-pre {\n font-family: Courier New;\n}\n\n:host(.md-ios) .md-code,\n:host(.md-ios) .md-pre {\n font-family: Menlo;\n}\n\n:host(.md-android) .md-code,\n:host(.md-android) .md-pre {\n font-family: monospace;\n}\n\n.md-blockquote {\n gap: 8px;\n padding-left: 12px;\n border-left-width: 3px;\n border-left-color: #d1d1d6;\n color: #6c6c70;\n}\n\n.md-ul,\n.md-ol {\n gap: 4px;\n}\n\n.md-li {\n flex-direction: row;\n gap: 8px;\n}\n\n.md-marker {\n min-width: 16px;\n}\n\n.md-li-body {\n flex: 1;\n gap: 4px;\n}\n\n.md-pre {\n flex-grow: 0;\n border-radius: 8px;\n background-color: #f2f2f7;\n}\n\n.md-pre-text {\n padding: 12px;\n font-size: 14px;\n line-height: 20px;\n}\n\n.md-hr {\n height: 1px;\n background-color: #d1d1d6;\n}\n\n.md-img {\n width: 100%;\n height: 200px;\n border-radius: 8px;\n}\n\n.md-table {\n border-width: 1px;\n border-color: #d1d1d6;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.md-tr {\n flex-direction: row;\n}\n\n.md-th,\n.md-td {\n flex: 1;\n padding: 6px 8px;\n}\n\n.md-td {\n border-top-width: 1px;\n border-top-color: #d1d1d6;\n}\n\n.md-th {\n font-weight: 600;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n color: #f2f2f7;\n }\n\n .md-h6,\n .md-blockquote {\n color: #98989f;\n }\n\n .md-a {\n color: #4c9bff;\n }\n\n .md-code {\n background-color: rgba(120, 120, 128, 0.32);\n }\n\n .md-pre {\n background-color: #1c1c1e;\n }\n\n .md-blockquote,\n .md-table,\n .md-td {\n border-color: #38383a;\n }\n\n .md-hr {\n background-color: #38383a;\n }\n}\n"] }]
337
- }], propDecorators: { source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], tokens: [{ type: i0.Input, args: [{ isSignal: true, alias: "tokens", required: false }] }], classes: [{ type: i0.Input, args: [{ isSignal: true, alias: "classes", required: false }] }], linkPress: [{ type: i0.Output, args: ["linkPress"] }] } });
426
+ }, styles: ["/* What a document looks like with no `classes` of the app's, in light and in dark. */\n\n:host {\n gap: 12px;\n color: #1c1c1e;\n}\n\n.md-h1,\n.md-h2,\n.md-h3,\n.md-h4,\n.md-h5,\n.md-h6 {\n font-weight: 700;\n}\n\n.md-h1 {\n font-size: 28px;\n line-height: 34px;\n}\n\n.md-h2 {\n font-size: 22px;\n line-height: 28px;\n}\n\n.md-h3 {\n font-size: 20px;\n line-height: 25px;\n}\n\n.md-h4 {\n font-size: 17px;\n line-height: 22px;\n}\n\n.md-h5 {\n font-size: 15px;\n line-height: 20px;\n}\n\n.md-h6 {\n font-size: 13px;\n line-height: 18px;\n color: #6c6c70;\n}\n\n.md-p,\n.md-marker,\n.md-th,\n.md-td {\n font-size: 16px;\n line-height: 23px;\n}\n\n.md-strong {\n font-weight: 700;\n}\n\n.md-em {\n font-style: italic;\n}\n\n.md-del {\n text-decoration-line: line-through;\n}\n\n.md-a {\n color: #0a66d8;\n text-decoration-line: underline;\n}\n\n.md-code {\n background-color: rgba(120, 120, 128, 0.16);\n}\n\n.md-code,\n.md-pre {\n font-family: Courier New;\n}\n\n:host(.md-ios) .md-code,\n:host(.md-ios) .md-pre {\n font-family: Menlo;\n}\n\n:host(.md-android) .md-code,\n:host(.md-android) .md-pre {\n font-family: monospace;\n}\n\n.md-blockquote {\n gap: 8px;\n padding-left: 12px;\n border-left-width: 3px;\n border-left-color: #d1d1d6;\n color: #6c6c70;\n}\n\n.md-ul,\n.md-ol {\n gap: 4px;\n}\n\n.md-li {\n flex-direction: row;\n gap: 8px;\n}\n\n.md-marker {\n min-width: 16px;\n}\n\n.md-li-body {\n flex: 1;\n gap: 4px;\n}\n\n.md-pre {\n flex-grow: 0;\n border-radius: 8px;\n background-color: #f2f2f7;\n}\n\n.md-pre-text {\n padding: 12px;\n font-size: 14px;\n line-height: 20px;\n}\n\n.md-hr {\n height: 1px;\n background-color: #d1d1d6;\n}\n\n.md-img {\n width: 100%;\n height: 200px;\n border-radius: 8px;\n}\n\n.md-table {\n border-width: 1px;\n border-color: #d1d1d6;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.md-tr {\n flex-direction: row;\n}\n\n/* The text hugs its content inside the column's least width, so it can report a narrower one. */\n.md-cell {\n align-items: flex-start;\n}\n\n.md-th,\n.md-td {\n max-width: 280px;\n padding: 6px 8px;\n}\n\n.md-cell-body {\n border-top-width: 1px;\n border-top-color: #d1d1d6;\n}\n\n.md-th {\n font-weight: 600;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n color: #f2f2f7;\n }\n\n .md-h6,\n .md-blockquote {\n color: #98989f;\n }\n\n .md-a {\n color: #4c9bff;\n }\n\n .md-code {\n background-color: rgba(120, 120, 128, 0.32);\n }\n\n .md-pre {\n background-color: #1c1c1e;\n }\n\n .md-blockquote,\n .md-table,\n .md-cell-body {\n border-color: #38383a;\n }\n\n .md-hr {\n background-color: #38383a;\n }\n}\n"] }]
427
+ }], propDecorators: { source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], tokens: [{ type: i0.Input, args: [{ isSignal: true, alias: "tokens", required: false }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], classes: [{ type: i0.Input, args: [{ isSignal: true, alias: "classes", required: false }] }], linkPress: [{ type: i0.Output, args: ["linkPress"] }] } });
338
428
  //# sourceMappingURL=markdown.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACnG,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAc,MAAM,QAAQ,CAAC;AAE3C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EACL,cAAc,EACd,YAAY,GAIb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;;AAgBjC,MAAM,QAAQ,GAA+B;IAC3C,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC;IACtF,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;CAClE,CAAC;AAEvB,MAAM,eAAe,GAAG,MAAM,CAAC,WAAW,CACxC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,OAAO,EAAE,CAAC,CAAC,CACT,CAAC;AAI/C,yEAAyE;AACzE,SAAS,YAAY,CAAC,MAAgC,EAAE,OAAO,IAAI,GAAG,EAAU;IAC9E,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;YAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;aAC3C,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,YAAY,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;aACpE,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK;gBAAE,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AAmHH,MAAM,OAAO,QAAQ;IACF,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7C,uEAAuE;IACpD,QAAQ,GAAG,cAAc,EAAE,CAAC;IAE/C,4BAA4B;IACnB,MAAM,GAAG,KAAK;0FAAU,CAAC;IAClC,sEAAsE;IAC7D,MAAM,GAAG,KAAK;0FAAoB,CAAC;IAC5C,2EAA2E;IAClE,OAAO,GAAG,KAAK,CAAkB,EAAE;gFAAC,CAAC;IAC9C,8CAA8C;IACrC,SAAS,GAAG,MAAM,EAAqB,CAAC;IAE9B,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,CACxC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;+EAC5D,CAAC;IAEe,OAAO,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,eAAe,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;gFAAC,CAAC;IAEvF;;;OAGG;IACgB,MAAM,GAAG,YAAY,6EACtC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,WAAW,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;YACvC,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC3B,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;YACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK;gBAAE,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACtF,OAAO,IAAI,CAAC;QACd,CAAC,GACD,CAAC;IAEO,QAAQ,CAAC,OAAwB;QACzC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC;IAES,SAAS,CAAC,GAAW,EAAE,KAA8B;QAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC;QACzC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;QACjC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;YAAE,OAAO;QACvC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC;IAC3E,CAAC;IAES,UAAU,CAAC,IAAkB;QACrC,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAClB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,cAAc,EAAE,GAAG,EAAE;gBACnB,SAAS,GAAG,IAAI,CAAC;YACnB,CAAC;SACF,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1F,CAAC;uGAzDU,QAAQ;2FAAR,QAAQ,ulBA/GT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwGT,6hFAzGS,KAAK,mTAAE,UAAU,0TAAE,YAAY,4GAAE,IAAI,mYAAE,IAAI;;2FAgH1C,QAAQ;kBAlHpB,SAAS;+BACE,UAAU,WACX,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,YAC5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwGT,QAEK;wBACJ,gBAAgB,EAAE,oBAAoB;wBACtC,oBAAoB,EAAE,wBAAwB;qBAC/C","sourcesContent":["/**\n * Markdown, drawn as native views and text.\n *\n * Its own entry point because it parses with `marked`, an optional peer dependency an app installs\n * only when it renders Markdown: the barrel never imports it, so an app that does not is not asked\n * for it.\n *\n * ```ts\n * import { Markdown } from '@ng-native/components/markdown';\n * ```\n */\nimport { Component, Injector, computed, inject, input, linkedSignal, output } from '@angular/core';\nimport { DeepLinks } from '@ng-native/device';\nimport { nativePlatform } from '@ng-native/fabric';\nimport { lexer, type Token } from 'marked';\nimport type { ImageLoadEvent } from './events.ts';\nimport { Image } from './image.ts';\nimport {\n markdownBlocks,\n opensOnPress,\n type MarkdownBlock,\n type MarkdownElement,\n type MarkdownInline,\n} from './markdown-blocks.ts';\nimport { ScrollView } from './scroll-view.ts';\nimport { TemplateSlot } from './template-slot.ts';\nimport { Text } from './text.ts';\nimport { View } from './view.ts';\n\nexport type { MarkdownElement } from './markdown-blocks.ts';\n\n/** Classes for the elements a document is drawn with, each one in place of the default. */\nexport type MarkdownClasses = Partial<Readonly<Record<MarkdownElement, string>>>;\n\n/** `(linkPress)`: a link was pressed. */\nexport interface MarkdownLinkPress {\n /** The target as written, after its entities are decoded and its scheme was allowed. */\n readonly href: string;\n readonly title: string | null;\n /** Keep the link from being opened, to handle it in the app instead. */\n preventDefault(): void;\n}\n\nconst ELEMENTS: readonly MarkdownElement[] = [\n ...['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'ul', 'ol', 'li', 'marker'],\n ...['pre', 'code', 'strong', 'em', 'del', 'a', 'hr', 'img', 'table', 'tr', 'th', 'td'],\n] as MarkdownElement[];\n\nconst DEFAULT_CLASSES = Object.fromEntries(\n ELEMENTS.map((element) => [element, `md-${element}`]),\n) as Readonly<Record<MarkdownElement, string>>;\n\ntype MarkdownLink = Extract<MarkdownInline, { kind: 'link' }>;\n\n/** Every image address in `blocks`, however deep in quotes and lists. */\nfunction imageSources(blocks: readonly MarkdownBlock[], into = new Set<string>()): Set<string> {\n for (const block of blocks) {\n if (block.kind === 'image') into.add(block.src);\n else if (block.kind === 'blockquote') imageSources(block.children, into);\n else if (block.kind === 'list') {\n for (const item of block.items) imageSources(item.children, into);\n }\n }\n return into;\n}\n\n/**\n * A Markdown document as `<text>`, `<view>`, `<image>` and `<scroll-view>`: headings, paragraphs,\n * emphasis, inline code, links, quotes, ordered, unordered and task lists, code blocks, rules,\n * tables and images.\n *\n * `source` is parsed with `marked.lexer`; `tokens` takes tokens lexed already, and wins when both\n * are set. A paragraph is one `<text>` with a nested `<text>` for each emphasis, code span and\n * link, so it wraps as one run; an image is lifted out to a block of its own.\n *\n * Nothing in the document is interpreted. Raw HTML is drawn as the text it is written as, a link\n * is followed only to `http`, `https`, `mailto`, `tel` or a relative target, and an image is\n * loaded only over `http` or `https`; anything else is drawn as its text.\n *\n * Each element carries a default class, `md-h1`, `md-p`, `md-a` and so on, styled in light and\n * dark. `classes` replaces one by its element's name, so a global stylesheet or Tailwind styles it\n * instead: `{ h1: 'text-3xl font-bold' }`. Put the default back beside your own to keep both.\n *\n * A pressed link is `(linkPress)`. An absolute target is then opened with `DeepLinks.open` unless\n * a handler calls `preventDefault()`; a relative one is only reported, for the app to route.\n */\n@Component({\n selector: 'markdown',\n imports: [Image, ScrollView, TemplateSlot, Text, View],\n template: `\n <ng-template #blockTemplate let-list>\n @for (block of list; track $index) {\n @switch (block.kind) {\n @case ('heading') {\n <text [class]=\"classFor(block.element)\" accessibilityRole=\"header\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: block.children }\"\n /></text>\n }\n @case ('paragraph') {\n <text [class]=\"classFor('p')\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: block.children }\"\n /></text>\n }\n @case ('image') {\n <image\n [src]=\"block.src\"\n [alt]=\"block.alt || undefined\"\n resizeMode=\"contain\"\n [class]=\"classFor('img')\"\n [style.aspectRatio]=\"ratios().get(block.src) ?? null\"\n [style.height]=\"ratios().has(block.src) ? 'auto' : null\"\n (load)=\"sizeImage(block.src, $any($event))\"\n />\n }\n @case ('blockquote') {\n <view [class]=\"classFor('blockquote')\">\n <ng-container *templateSlot=\"blockTemplate; context: { $implicit: block.children }\" />\n </view>\n }\n @case ('list') {\n <view [class]=\"classFor(block.element)\">\n @for (item of block.items; track $index) {\n <view [class]=\"classFor('li')\">\n <text [class]=\"classFor('marker')\">{{ item.marker }}</text>\n <view class=\"md-li-body\">\n <ng-container\n *templateSlot=\"blockTemplate; context: { $implicit: item.children }\"\n />\n </view>\n </view>\n }\n </view>\n }\n @case ('code') {\n <scroll-view horizontal [class]=\"classFor('pre')\">\n <text selectable class=\"md-pre-text\">{{ block.text }}</text>\n </scroll-view>\n }\n @case ('hr') {\n <view [class]=\"classFor('hr')\"></view>\n }\n @case ('table') {\n <view [class]=\"classFor('table')\">\n <view [class]=\"classFor('tr')\">\n @for (cell of block.header; track $index) {\n <text [class]=\"classFor('th')\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: cell }\"\n /></text>\n }\n </view>\n @for (row of block.rows; track $index) {\n <view [class]=\"classFor('tr')\">\n @for (cell of row; track $index) {\n <text [class]=\"classFor('td')\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: cell }\"\n /></text>\n }\n </view>\n }\n </view>\n }\n }\n }\n </ng-template>\n\n <ng-template #inlineTemplate let-runs>\n @for (run of runs; track $index) {\n @switch (run.kind) {\n @case ('text') {\n <ng-container>{{ run.text }}</ng-container>\n }\n @case ('code') {\n <text [class]=\"classFor('code')\">{{ run.text }}</text>\n }\n @case ('link') {\n <text\n [class]=\"classFor('a')\"\n pressable\n [accessibilityHint]=\"run.title ?? undefined\"\n (press)=\"followLink(run)\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: run.children }\"\n /></text>\n }\n @default {\n <text [class]=\"classFor(run.kind)\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: run.children }\"\n /></text>\n }\n }\n }\n </ng-template>\n\n <ng-container *templateSlot=\"blockTemplate; context: { $implicit: blocks() }\" />\n `,\n styleUrl: './markdown.css',\n host: {\n '[class.md-ios]': \"platform === 'ios'\",\n '[class.md-android]': \"platform === 'android'\",\n },\n})\nexport class Markdown {\n private readonly injector = inject(Injector);\n /** Which monospace font code is drawn in, as the platform names it. */\n protected readonly platform = nativePlatform();\n\n /** The Markdown to draw. */\n readonly source = input<string>();\n /** Tokens `marked.lexer` made already, drawn in place of `source`. */\n readonly tokens = input<readonly Token[]>();\n /** A class for an element, in place of its default. See the class note. */\n readonly classes = input<MarkdownClasses>({});\n /** A link was pressed. See the class note. */\n readonly linkPress = output<MarkdownLinkPress>();\n\n protected readonly blocks = computed(() =>\n markdownBlocks(this.tokens() ?? lexer(this.source() ?? '')),\n );\n\n private readonly classOf = computed(() => ({ ...DEFAULT_CLASSES, ...this.classes() }));\n\n /**\n * Each image's width over its height, once it has loaded, so it is drawn at the page's width and\n * its own shape. An image the document no longer has is forgotten when the document changes.\n */\n protected readonly ratios = linkedSignal<readonly MarkdownBlock[], ReadonlyMap<string, number>>({\n source: this.blocks,\n computation: (blocks, previous) => {\n const kept = new Map<string, number>();\n if (!previous) return kept;\n const present = imageSources(blocks);\n for (const [src, ratio] of previous.value) if (present.has(src)) kept.set(src, ratio);\n return kept;\n },\n });\n\n protected classFor(element: MarkdownElement): string {\n return this.classOf()[element];\n }\n\n protected sizeImage(src: string, event: Partial<ImageLoadEvent>): void {\n const source = event.nativeEvent?.source;\n if (!source) return;\n const { width, height } = source;\n if (!(width > 0 && height > 0)) return;\n this.ratios.update((ratios) => new Map(ratios).set(src, width / height));\n }\n\n protected followLink(link: MarkdownLink): void {\n let prevented = false;\n this.linkPress.emit({\n href: link.href,\n title: link.title,\n preventDefault: () => {\n prevented = true;\n },\n });\n if (!prevented && opensOnPress(link.href)) this.injector.get(DeepLinks).open(link.href);\n }\n}\n"]}
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Markdown, drawn as native views and text.\n *\n * Its own entry point because it parses with `marked`, an optional peer dependency an app installs\n * only when it renders Markdown: the barrel never imports it, so an app that does not is not asked\n * for it.\n *\n * ```ts\n * import { Markdown } from '@ng-native/components/markdown';\n * ```\n */\nimport {\n Component,\n Injector,\n booleanAttribute,\n computed,\n inject,\n input,\n linkedSignal,\n output,\n} from '@angular/core';\nimport { DeepLinks } from '@ng-native/device';\nimport { nativePlatform } from '@ng-native/fabric';\nimport { lexer, type Token } from 'marked';\nimport type { ImageLoadEvent } from './events.ts';\nimport { Image } from './image.ts';\nimport {\n blockKey,\n markdownBlocks,\n opensOnPress,\n shareBlocks,\n type MarkdownBlock,\n type MarkdownElement,\n type MarkdownInline,\n} from './markdown-blocks.ts';\nimport { MarkdownColumns } from './markdown-columns.ts';\nimport { MarkdownStream } from './markdown-stream.ts';\nimport { ScrollView } from './scroll-view.ts';\nimport { TemplateSlot } from './template-slot.ts';\nimport { Text } from './text.ts';\nimport { View } from './view.ts';\n\nexport type { MarkdownElement } from './markdown-blocks.ts';\n\n/** Classes for the elements a document is drawn with, each one in place of the default. */\nexport type MarkdownClasses = Partial<Readonly<Record<MarkdownElement, string>>>;\n\n/** `(linkPress)`: a link was pressed. */\nexport interface MarkdownLinkPress {\n /** The target as written, after its entities are decoded and its scheme was allowed. */\n readonly href: string;\n readonly title: string | null;\n /** Keep the link from being opened, to handle it in the app instead. */\n preventDefault(): void;\n}\n\nconst ELEMENTS: readonly MarkdownElement[] = [\n ...['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'ul', 'ol', 'li', 'marker'],\n ...['pre', 'code', 'strong', 'em', 'del', 'a', 'hr', 'img', 'table', 'tr', 'th', 'td'],\n] as MarkdownElement[];\n\nconst DEFAULT_CLASSES = Object.fromEntries(\n ELEMENTS.map((element) => [element, `md-${element}`]),\n) as Readonly<Record<MarkdownElement, string>>;\n\ntype MarkdownLink = Extract<MarkdownInline, { kind: 'link' }>;\n\n/** Every image address in `blocks`, however deep in quotes and lists. */\nfunction imageSources(blocks: readonly MarkdownBlock[], into = new Set<string>()): Set<string> {\n for (const block of blocks) {\n if (block.kind === 'image') into.add(block.src);\n else if (block.kind === 'blockquote') imageSources(block.children, into);\n else if (block.kind === 'list') {\n for (const item of block.items) imageSources(item.children, into);\n }\n }\n return into;\n}\n\n/**\n * A Markdown document as `<text>`, `<view>`, `<image>` and `<scroll-view>`: headings, paragraphs,\n * emphasis, inline code, links, quotes, ordered, unordered and task lists, code blocks, rules,\n * tables and images.\n *\n * `source` is parsed with `marked.lexer`; `tokens` takes tokens lexed already, and wins when both\n * are set. A paragraph is one `<text>` with a nested `<text>` for each emphasis, code span and\n * link, so it wraps as one run; an image is lifted out to a block of its own.\n *\n * Nothing in the document is interpreted. Raw HTML is drawn as the text it is written as, a link\n * is followed only to `http`, `https`, `mailto`, `tel` or a relative target, and an image is\n * loaded only over `http` or `https`; anything else is drawn as its text.\n *\n * Each element carries a default class, `md-h1`, `md-p`, `md-a` and so on, styled in light and\n * dark. `classes` replaces one by its element's name, so a global stylesheet or Tailwind styles it\n * instead: `{ h1: 'text-3xl font-bold' }`. Put the default back beside your own to keep both.\n *\n * For a source that grows as it streams in, an answer arriving a few words at a time, set\n * `streaming` until it is complete. The blocks before the last one are then kept as they are and\n * only the last is lexed and converted again, unless a chunk defines or changes a link reference,\n * which can change a block already kept, and the whole source is lexed again. Syntax still open at its end, a code span, an emphasis, a link,\n * an image or a table row, is held back until it closes. Of a code fence only an unfinished\n * opening line is held back, and its code then streams as it arrives. Clearing `streaming` draws\n * the whole source as written, and `tokens` are always drawn as given. A block that did not\n * change keeps its views, streaming or not.\n *\n * A pressed link is `(linkPress)`. An absolute target is then opened with `DeepLinks.open` unless\n * a handler calls `preventDefault()`; a relative one is only reported, for the app to route.\n */\n@Component({\n selector: 'markdown',\n imports: [Image, MarkdownColumns, ScrollView, TemplateSlot, Text, View],\n template: `\n <ng-template #blockTemplate let-list>\n @for (block of list; track blockKey(block)) {\n @switch (block.kind) {\n @case ('heading') {\n <text [class]=\"classFor(block.element)\" accessibilityRole=\"header\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: block.children }\"\n /></text>\n }\n @case ('paragraph') {\n <text [class]=\"classFor('p')\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: block.children }\"\n /></text>\n }\n @case ('image') {\n <image\n [src]=\"block.src\"\n [alt]=\"block.alt || undefined\"\n resizeMode=\"contain\"\n [class]=\"classFor('img')\"\n [style.aspectRatio]=\"ratios().get(block.src) ?? null\"\n [style.height]=\"ratios().has(block.src) ? 'auto' : null\"\n (load)=\"sizeImage(block.src, $any($event))\"\n />\n }\n @case ('blockquote') {\n <view [class]=\"classFor('blockquote')\">\n <ng-container *templateSlot=\"blockTemplate; context: { $implicit: block.children }\" />\n </view>\n }\n @case ('list') {\n <view [class]=\"classFor(block.element)\">\n @for (item of block.items; track $index) {\n <view [class]=\"classFor('li')\">\n <text [class]=\"classFor('marker')\">{{ item.marker }}</text>\n <view class=\"md-li-body\">\n <ng-container\n *templateSlot=\"blockTemplate; context: { $implicit: item.children }\"\n />\n </view>\n </view>\n }\n </view>\n }\n @case ('code') {\n <scroll-view horizontal [class]=\"classFor('pre')\">\n <text selectable class=\"md-pre-text\">{{ block.text }}</text>\n </scroll-view>\n }\n @case ('hr') {\n <view [class]=\"classFor('hr')\"></view>\n }\n @case ('table') {\n <scroll-view horizontal>\n <view\n [class]=\"classFor('table')\"\n [markdownColumns]=\"block\"\n #columns=\"markdownColumns\"\n >\n <view [class]=\"classFor('tr')\">\n @for (cell of block.header; track $index; let column = $index) {\n <view class=\"md-cell\" [style.minWidth]=\"columns.width(column)\">\n <text\n [class]=\"classFor('th')\"\n (layout)=\"columns.measure(0, column, $any($event))\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: cell }\"\n /></text>\n </view>\n }\n </view>\n @for (row of block.rows; track $index; let r = $index) {\n <view [class]=\"classFor('tr')\">\n @for (cell of row; track $index; let column = $index) {\n <view class=\"md-cell md-cell-body\" [style.minWidth]=\"columns.width(column)\">\n <text\n [class]=\"classFor('td')\"\n (layout)=\"columns.measure(r + 1, column, $any($event))\"\n ><ng-container\n *templateSlot=\"inlineTemplate; context: { $implicit: cell }\"\n /></text>\n </view>\n }\n </view>\n }\n </view>\n </scroll-view>\n }\n }\n }\n </ng-template>\n\n <ng-template #inlineTemplate let-runs>\n @for (run of runs; track $index) {\n @switch (run.kind) {\n @case ('text') {\n <ng-container>{{ run.text }}</ng-container>\n }\n @case ('code') {\n <text [class]=\"classFor('code')\">{{ run.text }}</text>\n }\n @case ('link') {\n <text\n [class]=\"classFor('a')\"\n pressable\n [accessibilityHint]=\"run.title ?? undefined\"\n (press)=\"followLink(run)\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: run.children }\"\n /></text>\n }\n @default {\n <text [class]=\"classFor(run.kind)\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: run.children }\"\n /></text>\n }\n }\n }\n </ng-template>\n\n <ng-container *templateSlot=\"blockTemplate; context: { $implicit: blocks() }\" />\n `,\n styleUrl: './markdown.css',\n host: {\n '[class.md-ios]': \"platform === 'ios'\",\n '[class.md-android]': \"platform === 'android'\",\n },\n})\nexport class Markdown {\n private readonly injector = inject(Injector);\n /** Which monospace font code is drawn in, as the platform names it. */\n protected readonly platform = nativePlatform();\n\n /** The Markdown to draw. */\n readonly source = input<string>();\n /** Tokens `marked.lexer` made already, drawn in place of `source`. */\n readonly tokens = input<readonly Token[]>();\n /**\n * Whether `source` is still arriving, a chunk at a time. While it is, only its last block is\n * lexed again, unless a link reference changes, and syntax still open at its end is held back.\n * See the class note.\n */\n readonly streaming = input(false, { transform: booleanAttribute });\n /** A class for an element, in place of its default. See the class note. */\n readonly classes = input<MarkdownClasses>({});\n /** A link was pressed. See the class note. */\n readonly linkPress = output<MarkdownLinkPress>();\n\n private readonly stream = new MarkdownStream();\n protected readonly blockKey = blockKey;\n\n /**\n * The blocks each token the stream kept was drawn as. A kept token is the same object from one\n * chunk to the next, so its blocks are not converted or compared again; a source lexed whole\n * makes new tokens, and so new blocks.\n */\n private readonly streamed = new WeakMap<Token, readonly MarkdownBlock[]>();\n\n /** The document's blocks, with each one that did not change kept, so it is not drawn again. */\n protected readonly blocks = linkedSignal({\n source: () => {\n const tokens = this.tokens();\n if (tokens) return { tokens, streamed: false };\n const streamed = this.streaming();\n return { tokens: this.lex(this.source() ?? '', streamed), streamed };\n },\n computation: ({ tokens, streamed }, previous?: { value: readonly MarkdownBlock[] }) =>\n streamed\n ? this.streamedBlocks(tokens, previous?.value)\n : shareBlocks(previous?.value, markdownBlocks(tokens)),\n });\n\n private readonly classOf = computed(() => ({ ...DEFAULT_CLASSES, ...this.classes() }));\n\n /**\n * Each image's width over its height, once it has loaded, so it is drawn at the page's width and\n * its own shape. An image the document no longer has is forgotten when the document changes.\n */\n protected readonly ratios = linkedSignal<readonly MarkdownBlock[], ReadonlyMap<string, number>>({\n source: this.blocks,\n computation: (blocks, previous) => {\n const kept = new Map<string, number>();\n if (!previous) return kept;\n const present = imageSources(blocks);\n for (const [src, ratio] of previous.value) if (present.has(src)) kept.set(src, ratio);\n return kept;\n },\n });\n\n private lex(source: string, streaming: boolean): readonly Token[] {\n return streaming ? this.stream.lex(source) : lexer(source);\n }\n\n /** `tokens`' blocks, converting and sharing only those of tokens the stream had not kept. */\n private streamedBlocks(\n tokens: readonly Token[],\n previous: readonly MarkdownBlock[] | undefined,\n ): readonly MarkdownBlock[] {\n const kept: MarkdownBlock[] = [];\n let at = 0;\n for (; at < tokens.length; at++) {\n const blocks = this.streamed.get(tokens[at]!);\n if (!blocks) break;\n kept.push(...blocks);\n }\n const rest = tokens.slice(at);\n const converted = rest.map((token) => markdownBlocks([token]));\n // The blocks drawn after the kept ones, when those are still the ones drawn first.\n const drawn = !kept.length || previous?.[kept.length - 1] === kept.at(-1);\n const was = drawn ? previous?.slice(kept.length) : undefined;\n const shared = shareBlocks(was, converted.flat());\n let from = 0;\n rest.forEach((token, i) => {\n const count = converted[i]!.length;\n this.streamed.set(token, shared.slice(from, (from += count)));\n });\n return [...kept, ...shared];\n }\n\n protected classFor(element: MarkdownElement): string {\n return this.classOf()[element];\n }\n\n protected sizeImage(src: string, event: Partial<ImageLoadEvent>): void {\n const source = event.nativeEvent?.source;\n if (!source) return;\n const { width, height } = source;\n if (!(width > 0 && height > 0)) return;\n this.ratios.update((ratios) => new Map(ratios).set(src, width / height));\n }\n\n protected followLink(link: MarkdownLink): void {\n let prevented = false;\n this.linkPress.emit({\n href: link.href,\n title: link.title,\n preventDefault: () => {\n prevented = true;\n },\n });\n if (!prevented && opensOnPress(link.href)) this.injector.get(DeepLinks).open(link.href);\n }\n}\n"]}
package/dist/modal.d.ts CHANGED
@@ -36,9 +36,15 @@ export declare class Modal extends ViewBase {
36
36
  readonly supportedOrientations: import("@angular/core").InputSignal<readonly ModalOrientation[] | undefined>;
37
37
  /** iOS: let a downward swipe dismiss the modal, reported through `(requestClose)`. */
38
38
  readonly allowSwipeDismissal: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
39
- /** Android: draw under the status bar. */
39
+ /**
40
+ * Android: draw under the status bar. Always on under Android edge-to-edge (Expo SDK 57), where
41
+ * React Native ignores `false`.
42
+ */
40
43
  readonly statusBarTranslucent: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
41
- /** Android: draw under the navigation bar. Needs `statusBarTranslucent` too. */
44
+ /**
45
+ * Android: draw under the navigation bar. Always on under Android edge-to-edge (Expo SDK 57),
46
+ * where React Native ignores `false`. In an app that opts out, it needs `statusBarTranslucent`.
47
+ */
42
48
  readonly navigationBarTranslucent: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
43
49
  /** Android: give the modal's window a hardware-accelerated surface. */
44
50
  readonly hardwareAccelerated: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
package/dist/modal.js CHANGED
@@ -43,9 +43,15 @@ export class Modal extends ViewBase {
43
43
  ...(ngDevMode ? [undefined, { debugName: "supportedOrientations" }] : /* istanbul ignore next */ []));
44
44
  /** iOS: let a downward swipe dismiss the modal, reported through `(requestClose)`. */
45
45
  allowSwipeDismissal = input(undefined, { ...(ngDevMode ? { debugName: "allowSwipeDismissal" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
46
- /** Android: draw under the status bar. */
46
+ /**
47
+ * Android: draw under the status bar. Always on under Android edge-to-edge (Expo SDK 57), where
48
+ * React Native ignores `false`.
49
+ */
47
50
  statusBarTranslucent = input(undefined, { ...(ngDevMode ? { debugName: "statusBarTranslucent" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
48
- /** Android: draw under the navigation bar. Needs `statusBarTranslucent` too. */
51
+ /**
52
+ * Android: draw under the navigation bar. Always on under Android edge-to-edge (Expo SDK 57),
53
+ * where React Native ignores `false`. In an app that opts out, it needs `statusBarTranslucent`.
54
+ */
49
55
  navigationBarTranslucent = input(undefined, { ...(ngDevMode ? { debugName: "navigationBarTranslucent" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
50
56
  /** Android: give the modal's window a hardware-accelerated surface. */
51
57
  hardwareAccelerated = input(undefined, { ...(ngDevMode ? { debugName: "hardwareAccelerated" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
@@ -72,9 +78,6 @@ export class Modal extends ViewBase {
72
78
  if (this.transparent() && style && style !== 'overFullScreen') {
73
79
  console.warn(`[angular-native] <modal> is transparent, which needs presentationStyle="overFullScreen" on iOS; '${style}' will not be see-through.`);
74
80
  }
75
- if (this.navigationBarTranslucent() && !this.statusBarTranslucent()) {
76
- console.warn('[angular-native] <modal> navigationBarTranslucent has no effect without statusBarTranslucent.');
77
- }
78
81
  });
79
82
  }
80
83
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Modal, deps: [], target: i0.ɵɵFactoryTarget.Component });
package/dist/modal.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"modal.js","sourceRoot":"","sources":["../src/modal.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Component,\n DestroyRef,\n booleanAttribute,\n computed,\n effect,\n inject,\n input,\n} from '@angular/core';\nimport { Direction } from '@ng-native/device';\nimport { HostEngine } from '@ng-native/fabric';\nimport { optionalBoolean } from './transforms.ts';\nimport { View } from './view.ts';\nimport { ViewBase } from './view-base.ts';\n\nexport type ModalPresentationStyle = 'fullScreen' | 'pageSheet' | 'formSheet' | 'overFullScreen';\nexport type ModalOrientation =\n 'portrait' | 'portrait-upside-down' | 'landscape' | 'landscape-left' | 'landscape-right';\n\n/**\n * Content presented over everything else. Commits as `ModalHostView`.\n *\n * RN's `Modal.js` gives the native host `{position: 'absolute'}` (the engine's default for it)\n * and puts the children inside a full-bleed view carrying the backdrop colour. Without the\n * container the content has no backdrop and no layout box, so a modal renders as nothing.\n *\n * Events are element events: `(requestClose)` for the Android back button and an iOS swipe,\n * `(show)`, `(dismiss)`, and `(orientationChange)`.\n *\n * The container is pinned to the leading edge with `[side]: 0`, the same as RN's own Modal.js -\n * `left` in a left-to-right layout, `right` in a right-to-left one - so the backdrop starts from\n * the edge content actually begins at rather than always the physical left.\n */\n@Component({\n selector: 'modal',\n imports: [View],\n template: `<view [style]=\"containerStyle()\" collapsable=\"false\"><ng-content /></view>`,\n host: {\n '[visible]': 'visible()',\n '[transparent]': 'transparent()',\n '[animationType]': 'animationType()',\n '[presentationStyle]': 'resolvedPresentationStyle()',\n '[supportedOrientations]': 'supportedOrientations()',\n '[allowSwipeDismissal]': 'allowSwipeDismissal()',\n '[statusBarTranslucent]': 'statusBarTranslucent()',\n '[navigationBarTranslucent]': 'navigationBarTranslucent()',\n '[hardwareAccelerated]': 'hardwareAccelerated()',\n },\n})\nexport class Modal extends ViewBase {\n /**\n * Whether the modal is showing. Defaults to true, as in RN. While false the engine leaves the\n * native host out of the commit, as RN's Modal returns null when hidden, because the host\n * covers the screen and takes every touch whether or not it presents; on iOS a modal that was\n * showing stays until native reports its dismissal.\n */\n readonly visible = input(true, { transform: booleanAttribute });\n /** See through to the screen behind; the container gets no backdrop. */\n readonly transparent = input(false, { transform: booleanAttribute });\n /** The container's background when not transparent. Defaults to white. */\n readonly backdropColor = input<string>();\n /** How the modal appears and leaves. Defaults to `none`. */\n readonly animationType = input<'none' | 'slide' | 'fade'>();\n /** iOS: full screen, a sheet, or over the current content. `overFullScreen` if transparent. */\n readonly presentationStyle = input<ModalPresentationStyle>();\n /** iOS: which orientations the modal may rotate to. Defaults to portrait. */\n readonly supportedOrientations = input<readonly ModalOrientation[]>();\n /** iOS: let a downward swipe dismiss the modal, reported through `(requestClose)`. */\n readonly allowSwipeDismissal = input(undefined, { transform: optionalBoolean });\n /** Android: draw under the status bar. */\n readonly statusBarTranslucent = input(undefined, { transform: optionalBoolean });\n /** Android: draw under the navigation bar. Needs `statusBarTranslucent` too. */\n readonly navigationBarTranslucent = input(undefined, { transform: optionalBoolean });\n /** Android: give the modal's window a hardware-accelerated surface. */\n readonly hardwareAccelerated = input(undefined, { transform: optionalBoolean });\n\n private readonly direction = inject(Direction);\n\n protected readonly resolvedPresentationStyle = computed(\n () => this.presentationStyle() ?? (this.transparent() ? 'overFullScreen' : undefined),\n );\n\n protected readonly containerStyle = computed(() => ({\n [this.direction.rtl() ? 'right' : 'left']: 0,\n top: 0,\n flex: 1,\n backgroundColor: this.transparent() ? 'transparent' : (this.backdropColor() ?? 'white'),\n }));\n\n constructor() {\n super();\n // RN's host claims any touch that reaches it, so nothing inside the modal can hand a\n // gesture to the screen underneath.\n const stop = inject(HostEngine).setResponder(this.node, {\n onStartShouldSetResponder: () => true,\n });\n inject(DestroyRef).onDestroy(stop);\n\n effect(() => {\n const style = this.presentationStyle();\n if (this.transparent() && style && style !== 'overFullScreen') {\n console.warn(\n `[angular-native] <modal> is transparent, which needs presentationStyle=\"overFullScreen\" on iOS; '${style}' will not be see-through.`,\n );\n }\n if (this.navigationBarTranslucent() && !this.statusBarTranslucent()) {\n console.warn(\n '[angular-native] <modal> navigationBarTranslucent has no effect without statusBarTranslucent.',\n );\n }\n });\n }\n}\n"]}
1
+ {"version":3,"file":"modal.js","sourceRoot":"","sources":["../src/modal.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Component,\n DestroyRef,\n booleanAttribute,\n computed,\n effect,\n inject,\n input,\n} from '@angular/core';\nimport { Direction } from '@ng-native/device';\nimport { HostEngine } from '@ng-native/fabric';\nimport { optionalBoolean } from './transforms.ts';\nimport { View } from './view.ts';\nimport { ViewBase } from './view-base.ts';\n\nexport type ModalPresentationStyle = 'fullScreen' | 'pageSheet' | 'formSheet' | 'overFullScreen';\nexport type ModalOrientation =\n 'portrait' | 'portrait-upside-down' | 'landscape' | 'landscape-left' | 'landscape-right';\n\n/**\n * Content presented over everything else. Commits as `ModalHostView`.\n *\n * RN's `Modal.js` gives the native host `{position: 'absolute'}` (the engine's default for it)\n * and puts the children inside a full-bleed view carrying the backdrop colour. Without the\n * container the content has no backdrop and no layout box, so a modal renders as nothing.\n *\n * Events are element events: `(requestClose)` for the Android back button and an iOS swipe,\n * `(show)`, `(dismiss)`, and `(orientationChange)`.\n *\n * The container is pinned to the leading edge with `[side]: 0`, the same as RN's own Modal.js -\n * `left` in a left-to-right layout, `right` in a right-to-left one - so the backdrop starts from\n * the edge content actually begins at rather than always the physical left.\n */\n@Component({\n selector: 'modal',\n imports: [View],\n template: `<view [style]=\"containerStyle()\" collapsable=\"false\"><ng-content /></view>`,\n host: {\n '[visible]': 'visible()',\n '[transparent]': 'transparent()',\n '[animationType]': 'animationType()',\n '[presentationStyle]': 'resolvedPresentationStyle()',\n '[supportedOrientations]': 'supportedOrientations()',\n '[allowSwipeDismissal]': 'allowSwipeDismissal()',\n '[statusBarTranslucent]': 'statusBarTranslucent()',\n '[navigationBarTranslucent]': 'navigationBarTranslucent()',\n '[hardwareAccelerated]': 'hardwareAccelerated()',\n },\n})\nexport class Modal extends ViewBase {\n /**\n * Whether the modal is showing. Defaults to true, as in RN. While false the engine leaves the\n * native host out of the commit, as RN's Modal returns null when hidden, because the host\n * covers the screen and takes every touch whether or not it presents; on iOS a modal that was\n * showing stays until native reports its dismissal.\n */\n readonly visible = input(true, { transform: booleanAttribute });\n /** See through to the screen behind; the container gets no backdrop. */\n readonly transparent = input(false, { transform: booleanAttribute });\n /** The container's background when not transparent. Defaults to white. */\n readonly backdropColor = input<string>();\n /** How the modal appears and leaves. Defaults to `none`. */\n readonly animationType = input<'none' | 'slide' | 'fade'>();\n /** iOS: full screen, a sheet, or over the current content. `overFullScreen` if transparent. */\n readonly presentationStyle = input<ModalPresentationStyle>();\n /** iOS: which orientations the modal may rotate to. Defaults to portrait. */\n readonly supportedOrientations = input<readonly ModalOrientation[]>();\n /** iOS: let a downward swipe dismiss the modal, reported through `(requestClose)`. */\n readonly allowSwipeDismissal = input(undefined, { transform: optionalBoolean });\n /**\n * Android: draw under the status bar. Always on under Android edge-to-edge (Expo SDK 57), where\n * React Native ignores `false`.\n */\n readonly statusBarTranslucent = input(undefined, { transform: optionalBoolean });\n /**\n * Android: draw under the navigation bar. Always on under Android edge-to-edge (Expo SDK 57),\n * where React Native ignores `false`. In an app that opts out, it needs `statusBarTranslucent`.\n */\n readonly navigationBarTranslucent = input(undefined, { transform: optionalBoolean });\n /** Android: give the modal's window a hardware-accelerated surface. */\n readonly hardwareAccelerated = input(undefined, { transform: optionalBoolean });\n\n private readonly direction = inject(Direction);\n\n protected readonly resolvedPresentationStyle = computed(\n () => this.presentationStyle() ?? (this.transparent() ? 'overFullScreen' : undefined),\n );\n\n protected readonly containerStyle = computed(() => ({\n [this.direction.rtl() ? 'right' : 'left']: 0,\n top: 0,\n flex: 1,\n backgroundColor: this.transparent() ? 'transparent' : (this.backdropColor() ?? 'white'),\n }));\n\n constructor() {\n super();\n // RN's host claims any touch that reaches it, so nothing inside the modal can hand a\n // gesture to the screen underneath.\n const stop = inject(HostEngine).setResponder(this.node, {\n onStartShouldSetResponder: () => true,\n });\n inject(DestroyRef).onDestroy(stop);\n\n effect(() => {\n const style = this.presentationStyle();\n if (this.transparent() && style && style !== 'overFullScreen') {\n console.warn(\n `[angular-native] <modal> is transparent, which needs presentationStyle=\"overFullScreen\" on iOS; '${style}' will not be see-through.`,\n );\n }\n });\n }\n}\n"]}
package/dist/pressable.js CHANGED
@@ -5,9 +5,9 @@
5
5
  * press rectangle, delays and a long-press timer around them. This is that machine, on the same
6
6
  * responder negotiation, so a button and an enclosing scroll view agree on who owns a drag.
7
7
  */
8
- import { Directive, booleanAttribute, computed, forwardRef, input, numberAttribute, output, signal, } from '@angular/core';
8
+ import { Directive, computed, forwardRef, input, output, signal, booleanAttribute, } from '@angular/core';
9
9
  import { nativePlatform } from '@ng-native/fabric';
10
- import { optionalBoolean } from "./transforms.js";
10
+ import { numberOr, optionalBoolean } from "./transforms.js";
11
11
  import { ViewBase } from "./view-base.js";
12
12
  import * as i0 from "@angular/core";
13
13
  /**
@@ -67,13 +67,13 @@ export class TouchableBase extends ViewBase {
67
67
  pressRetentionOffset = input(/* @ts-ignore */
68
68
  ...(ngDevMode ? [undefined, { debugName: "pressRetentionOffset" }] : /* istanbul ignore next */ []));
69
69
  /** Milliseconds a touch must be held before `(longPress)` fires. */
70
- delayLongPress = input(DEFAULT_LONG_PRESS_DELAY, { ...(ngDevMode ? { debugName: "delayLongPress" } : /* istanbul ignore next */ {}), transform: numberAttribute });
70
+ delayLongPress = input(DEFAULT_LONG_PRESS_DELAY, { ...(ngDevMode ? { debugName: "delayLongPress" } : /* istanbul ignore next */ {}), transform: numberOr(DEFAULT_LONG_PRESS_DELAY) });
71
71
  /** Milliseconds between the touch landing and `(pressIn)`, so a scroll can start first. */
72
- delayPressIn = input(0, { ...(ngDevMode ? { debugName: "delayPressIn" } : /* istanbul ignore next */ {}), transform: numberAttribute });
72
+ delayPressIn = input(0, { ...(ngDevMode ? { debugName: "delayPressIn" } : /* istanbul ignore next */ {}), transform: numberOr(0) });
73
73
  /** Milliseconds between the touch lifting and `(pressOut)`. */
74
- delayPressOut = input(0, { ...(ngDevMode ? { debugName: "delayPressOut" } : /* istanbul ignore next */ {}), transform: numberAttribute });
74
+ delayPressOut = input(0, { ...(ngDevMode ? { debugName: "delayPressOut" } : /* istanbul ignore next */ {}), transform: numberOr(0) });
75
75
  /** The pressed state lasts at least this long, so a quick tap still shows it. */
76
- minPressDuration = input(DEFAULT_MIN_PRESS_DURATION, { ...(ngDevMode ? { debugName: "minPressDuration" } : /* istanbul ignore next */ {}), transform: numberAttribute });
76
+ minPressDuration = input(DEFAULT_MIN_PRESS_DURATION, { ...(ngDevMode ? { debugName: "minPressDuration" } : /* istanbul ignore next */ {}), transform: numberOr(DEFAULT_MIN_PRESS_DURATION) });
77
77
  /** Whether a scrolling ancestor may take the gesture away mid-press. */
78
78
  cancelable = input(true, { ...(ngDevMode ? { debugName: "cancelable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
79
79
  /** Android: a native ripple on press. */