panelui-native 0.30.0 → 0.34.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 (81) hide show
  1. package/README.md +4 -2
  2. package/lib/module/components/button/index.js +106 -11
  3. package/lib/module/components/button/index.js.map +1 -1
  4. package/lib/module/components/calendar/index.js +107 -24
  5. package/lib/module/components/calendar/index.js.map +1 -1
  6. package/lib/module/components/drawer/index.js +477 -0
  7. package/lib/module/components/drawer/index.js.map +1 -0
  8. package/lib/module/components/heatmap-chart/index.js +9 -8
  9. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  10. package/lib/module/components/map/index.js +50 -4
  11. package/lib/module/components/map/index.js.map +1 -1
  12. package/lib/module/components/panelside/index.js +1024 -0
  13. package/lib/module/components/panelside/index.js.map +1 -0
  14. package/lib/module/components/plan/index.js +307 -21
  15. package/lib/module/components/plan/index.js.map +1 -1
  16. package/lib/module/components/post/index.js +992 -0
  17. package/lib/module/components/post/index.js.map +1 -0
  18. package/lib/module/components/response/index.js +296 -0
  19. package/lib/module/components/response/index.js.map +1 -0
  20. package/lib/module/components/response/markdown.js +463 -0
  21. package/lib/module/components/response/markdown.js.map +1 -0
  22. package/lib/module/components/select/index.js +111 -18
  23. package/lib/module/components/select/index.js.map +1 -1
  24. package/lib/module/components/table/index.js +632 -0
  25. package/lib/module/components/table/index.js.map +1 -0
  26. package/lib/module/icons/index.js +220 -0
  27. package/lib/module/icons/index.js.map +1 -1
  28. package/lib/module/index.js +6 -1
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/native/index.js +30 -0
  31. package/lib/module/native/index.js.map +1 -1
  32. package/lib/module/utils/date.js +129 -22
  33. package/lib/module/utils/date.js.map +1 -1
  34. package/lib/typescript/src/components/button/index.d.ts +22 -1
  35. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/calendar/index.d.ts +12 -3
  37. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/drawer/index.d.ts +146 -0
  39. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -0
  40. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/map/index.d.ts +8 -1
  42. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  43. package/lib/typescript/src/components/panelside/index.d.ts +354 -0
  44. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -0
  45. package/lib/typescript/src/components/plan/index.d.ts +121 -10
  46. package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
  47. package/lib/typescript/src/components/post/index.d.ts +358 -0
  48. package/lib/typescript/src/components/post/index.d.ts.map +1 -0
  49. package/lib/typescript/src/components/response/index.d.ts +91 -0
  50. package/lib/typescript/src/components/response/index.d.ts.map +1 -0
  51. package/lib/typescript/src/components/response/markdown.d.ts +88 -0
  52. package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
  53. package/lib/typescript/src/components/select/index.d.ts +27 -2
  54. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  55. package/lib/typescript/src/components/table/index.d.ts +274 -0
  56. package/lib/typescript/src/components/table/index.d.ts.map +1 -0
  57. package/lib/typescript/src/icons/index.d.ts +41 -1
  58. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  59. package/lib/typescript/src/index.d.ts +7 -2
  60. package/lib/typescript/src/index.d.ts.map +1 -1
  61. package/lib/typescript/src/native/index.d.ts +20 -0
  62. package/lib/typescript/src/native/index.d.ts.map +1 -1
  63. package/lib/typescript/src/utils/date.d.ts +49 -5
  64. package/lib/typescript/src/utils/date.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/src/components/button/index.tsx +140 -8
  67. package/src/components/calendar/index.tsx +136 -23
  68. package/src/components/drawer/index.tsx +662 -0
  69. package/src/components/heatmap-chart/index.tsx +9 -9
  70. package/src/components/map/index.tsx +53 -4
  71. package/src/components/panelside/index.tsx +1358 -0
  72. package/src/components/plan/index.tsx +344 -21
  73. package/src/components/post/index.tsx +1116 -0
  74. package/src/components/response/index.tsx +390 -0
  75. package/src/components/response/markdown.ts +489 -0
  76. package/src/components/select/index.tsx +155 -15
  77. package/src/components/table/index.tsx +747 -0
  78. package/src/icons/index.tsx +176 -1
  79. package/src/index.ts +93 -0
  80. package/src/native/index.ts +51 -0
  81. package/src/utils/date.ts +146 -22
@@ -0,0 +1,489 @@
1
+ /**
2
+ * A small Markdown reader, written for text that is still arriving.
3
+ *
4
+ * No parser library. What a chat answer actually contains is headings,
5
+ * paragraphs, lists, fenced code, blockquotes, tables, rules and a handful of
6
+ * inline marks — and every general-purpose parser worth using assumes it has
7
+ * been handed the whole document. That assumption is the one thing that is
8
+ * never true here.
9
+ *
10
+ * ## Half-arrived markdown
11
+ *
12
+ * A token stream shows you every prefix of the final text, so the reader sees
13
+ * `**bo`, then `**bol`, then `**bold**`. Treated naively that is three
14
+ * documents: two with literal asterisks in them and one with a bolded word. The
15
+ * text would flash between styles on nearly every frame, which is worse than no
16
+ * formatting at all — the eye tracks the flicker instead of the words.
17
+ *
18
+ * So an unterminated construct at the very end of the input is *completed
19
+ * speculatively* rather than escaped: an open fence becomes a code block that
20
+ * is still filling, an open `**` becomes bold text that is still being written.
21
+ * Anything unterminated anywhere else is literal, because a document that has
22
+ * stopped arriving means what it says.
23
+ *
24
+ * The two rules that follow from this:
25
+ *
26
+ * - **Only the tail is speculative.** An asterisk in the middle of a finished
27
+ * paragraph is an asterisk.
28
+ * - **A speculative construct never removes visible text.** The characters that
29
+ * opened it are hidden, and everything after them stays on screen. Otherwise
30
+ * the last word of the answer disappears and reappears as its delimiter
31
+ * arrives.
32
+ */
33
+
34
+ export type Align = 'left' | 'center' | 'right';
35
+
36
+ export interface InlineToken {
37
+ kind: 'text' | 'code' | 'link' | 'image';
38
+ value: string;
39
+ /** For links and images. */
40
+ href?: string;
41
+ bold?: boolean;
42
+ italic?: boolean;
43
+ strike?: boolean;
44
+ }
45
+
46
+ export type Block =
47
+ | { type: 'heading'; level: 1 | 2 | 3 | 4 | 5 | 6; inline: InlineToken[] }
48
+ | { type: 'paragraph'; inline: InlineToken[] }
49
+ | { type: 'code'; code: string; language?: string; open: boolean }
50
+ | { type: 'quote'; blocks: Block[] }
51
+ | { type: 'list'; ordered: boolean; start: number; items: Block[][] }
52
+ | { type: 'table'; head: InlineToken[][]; rows: InlineToken[][][]; align: (Align | null)[] }
53
+ | { type: 'rule' };
54
+
55
+ const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)/;
56
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*)$/;
57
+ const RULE = /^ {0,3}((\*\s*){3,}|(-\s*){3,}|(_\s*){3,})$/;
58
+ const QUOTE = /^ {0,3}> ?(.*)$/;
59
+ const BULLET = /^(\s*)([-*+])\s+(.*)$/;
60
+ const ORDERED = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
61
+ const TABLE_DIVIDER = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/;
62
+
63
+ /**
64
+ * Splits a document into blocks.
65
+ *
66
+ * `streaming` is what turns speculative completion on. With it off — a stored
67
+ * answer, a message that has finished — an unterminated fence is just a
68
+ * paragraph starting with three backticks, which is what it literally is.
69
+ */
70
+ export function parseMarkdown(source: string, streaming = false): Block[] {
71
+ const lines = source.replace(/\r\n?/g, '\n').split('\n');
72
+ return parseBlocks(lines, streaming);
73
+ }
74
+
75
+ function parseBlocks(lines: string[], streaming: boolean): Block[] {
76
+ const blocks: Block[] = [];
77
+ let index = 0;
78
+
79
+ while (index < lines.length) {
80
+ const line = lines[index]!;
81
+
82
+ if (!line.trim()) {
83
+ index += 1;
84
+ continue;
85
+ }
86
+
87
+ const fence = FENCE.exec(line);
88
+ if (fence) {
89
+ const marker = fence[1]!;
90
+ const language = fence[2] || undefined;
91
+ const body: string[] = [];
92
+ let closed = false;
93
+ index += 1;
94
+
95
+ while (index < lines.length) {
96
+ const current = lines[index]!;
97
+ // Closed by a run of the same character at least as long as the opener,
98
+ // and nothing else on the line.
99
+ if (current.trimStart().startsWith(marker[0]!.repeat(marker.length)) && !current.trim().slice(marker.length).trim()) {
100
+ closed = true;
101
+ index += 1;
102
+ break;
103
+ }
104
+ body.push(current);
105
+ index += 1;
106
+ }
107
+
108
+ // An unclosed fence in a finished document is not a code block: it is a
109
+ // paragraph that begins with three backticks, and pretending otherwise
110
+ // swallows the rest of the answer.
111
+ if (!closed && !streaming) {
112
+ blocks.push({ type: 'paragraph', inline: parseInline(line, false) });
113
+ for (const text of body) {
114
+ if (text.trim()) blocks.push({ type: 'paragraph', inline: parseInline(text, false) });
115
+ }
116
+ continue;
117
+ }
118
+
119
+ blocks.push({ type: 'code', code: body.join('\n'), language, open: !closed });
120
+ continue;
121
+ }
122
+
123
+ if (RULE.test(line)) {
124
+ blocks.push({ type: 'rule' });
125
+ index += 1;
126
+ continue;
127
+ }
128
+
129
+ const heading = HEADING.exec(line);
130
+ if (heading) {
131
+ const level = heading[1]!.length as 1 | 2 | 3 | 4 | 5 | 6;
132
+ blocks.push({
133
+ type: 'heading',
134
+ level,
135
+ // Trailing hashes are a closing marker, not content.
136
+ inline: parseInline(heading[2]!.replace(/\s+#+\s*$/, ''), streaming && index === lines.length - 1),
137
+ });
138
+ index += 1;
139
+ continue;
140
+ }
141
+
142
+ if (QUOTE.test(line)) {
143
+ const body: string[] = [];
144
+ while (index < lines.length) {
145
+ const match = QUOTE.exec(lines[index]!);
146
+ if (!match) break;
147
+ body.push(match[1]!);
148
+ index += 1;
149
+ }
150
+ blocks.push({ type: 'quote', blocks: parseBlocks(body, streaming) });
151
+ continue;
152
+ }
153
+
154
+ const table = parseTable(lines, index, streaming);
155
+ if (table) {
156
+ blocks.push(table.block);
157
+ index = table.next;
158
+ continue;
159
+ }
160
+
161
+ if (BULLET.test(line) || ORDERED.test(line)) {
162
+ const list = parseList(lines, index, streaming);
163
+ blocks.push(list.block);
164
+ index = list.next;
165
+ continue;
166
+ }
167
+
168
+ // A paragraph runs to the first blank line or the first line that starts
169
+ // something else.
170
+ const body: string[] = [];
171
+ while (index < lines.length) {
172
+ const current = lines[index]!;
173
+ if (!current.trim()) break;
174
+ if (
175
+ index > 0 &&
176
+ (FENCE.test(current) ||
177
+ HEADING.test(current) ||
178
+ RULE.test(current) ||
179
+ QUOTE.test(current) ||
180
+ BULLET.test(current) ||
181
+ ORDERED.test(current)) &&
182
+ body.length
183
+ ) {
184
+ break;
185
+ }
186
+ body.push(current.trim());
187
+ index += 1;
188
+ }
189
+
190
+ blocks.push({
191
+ type: 'paragraph',
192
+ inline: parseInline(body.join(' '), streaming && index >= lines.length),
193
+ });
194
+ }
195
+
196
+ return blocks;
197
+ }
198
+
199
+ /**
200
+ * A GFM table, or nothing.
201
+ *
202
+ * The divider row is what makes a table a table — a run of pipes on its own is
203
+ * just a paragraph with pipes in it — so a header line is only a header once
204
+ * the line under it has arrived. Mid-stream that means a table appears a beat
205
+ * after its first row, which is the right beat: it is not a table yet.
206
+ */
207
+ function parseTable(
208
+ lines: string[],
209
+ start: number,
210
+ streaming: boolean
211
+ ): { block: Block; next: number } | null {
212
+ const header = lines[start];
213
+ const divider = lines[start + 1];
214
+ if (!header || !divider) return null;
215
+ if (!header.includes('|')) return null;
216
+ if (!TABLE_DIVIDER.test(divider)) return null;
217
+
218
+ const head = splitRow(header).map((cell) => parseInline(cell, false));
219
+ const align = splitRow(divider).map((cell): Align | null => {
220
+ const left = cell.startsWith(':');
221
+ const right = cell.endsWith(':');
222
+ if (left && right) return 'center';
223
+ if (right) return 'right';
224
+ if (left) return 'left';
225
+ return null;
226
+ });
227
+
228
+ const rows: InlineToken[][][] = [];
229
+ let index = start + 2;
230
+ while (index < lines.length) {
231
+ const line = lines[index]!;
232
+ if (!line.trim() || !line.includes('|')) break;
233
+ // The last line of a stream is very likely half-typed. Holding it back for
234
+ // one token is better than a row that grows a cell at a time.
235
+ const partial = streaming && index === lines.length - 1;
236
+ if (partial) {
237
+ index += 1;
238
+ break;
239
+ }
240
+ rows.push(splitRow(line).map((cell) => parseInline(cell, false)));
241
+ index += 1;
242
+ }
243
+
244
+ return { block: { type: 'table', head, rows, align }, next: index };
245
+ }
246
+
247
+ function splitRow(row: string): string[] {
248
+ return row
249
+ .trim()
250
+ .replace(/^\|/, '')
251
+ .replace(/\|$/, '')
252
+ .split('|')
253
+ .map((cell) => cell.trim());
254
+ }
255
+
256
+ /**
257
+ * A list, and everything indented under it.
258
+ *
259
+ * Nesting is by indent, and a nested list becomes a block inside its parent
260
+ * item rather than a sibling — which is what lets an item hold a paragraph, a
261
+ * sub-list and a code block at once.
262
+ */
263
+ function parseList(
264
+ lines: string[],
265
+ start: number,
266
+ streaming: boolean
267
+ ): { block: Block; next: number } {
268
+ const first = BULLET.exec(lines[start]!) ?? ORDERED.exec(lines[start]!)!;
269
+ const ordered = !BULLET.test(lines[start]!);
270
+ const baseIndent = first[1]!.length;
271
+ const startNumber = ordered ? Number(first[2]) : 1;
272
+
273
+ const items: string[][] = [];
274
+ let current: string[] | null = null;
275
+ let index = start;
276
+
277
+ while (index < lines.length) {
278
+ const line = lines[index]!;
279
+
280
+ if (!line.trim()) {
281
+ // A blank line inside a list is only the end of it if what follows is not
282
+ // indented under an item.
283
+ const next = lines[index + 1];
284
+ if (next && next.trim() && leadingSpaces(next) <= baseIndent && !isItem(next)) break;
285
+ if (current) current.push('');
286
+ index += 1;
287
+ continue;
288
+ }
289
+
290
+ const item = BULLET.exec(line) ?? ORDERED.exec(line);
291
+ if (item && item[1]!.length <= baseIndent) {
292
+ if (ordered !== !BULLET.test(line)) break;
293
+ current = [item[3]!];
294
+ items.push(current);
295
+ index += 1;
296
+ continue;
297
+ }
298
+
299
+ if (leadingSpaces(line) > baseIndent && current) {
300
+ current.push(line.slice(baseIndent + 1));
301
+ index += 1;
302
+ continue;
303
+ }
304
+
305
+ break;
306
+ }
307
+
308
+ return {
309
+ block: {
310
+ type: 'list',
311
+ ordered,
312
+ start: Number.isFinite(startNumber) ? startNumber : 1,
313
+ items: items.map((body, position) =>
314
+ parseBlocks(body, streaming && position === items.length - 1)
315
+ ),
316
+ },
317
+ next: index,
318
+ };
319
+ }
320
+
321
+ function isItem(line: string): boolean {
322
+ return BULLET.test(line) || ORDERED.test(line);
323
+ }
324
+
325
+ function leadingSpaces(line: string): number {
326
+ return line.length - line.trimStart().length;
327
+ }
328
+
329
+ /* -------------------------------------------------------------------------- */
330
+ /* Inline */
331
+ /* -------------------------------------------------------------------------- */
332
+
333
+ interface Marks {
334
+ bold?: boolean;
335
+ italic?: boolean;
336
+ strike?: boolean;
337
+ }
338
+
339
+ /**
340
+ * Splits a run of text into styled spans.
341
+ *
342
+ * `tail` says this run is the end of a stream, which is what licenses closing
343
+ * an open delimiter speculatively. It is deliberately narrow: only the *last*
344
+ * unterminated opener on the line is completed, and only when nothing after it
345
+ * could still turn out to be the closer.
346
+ */
347
+ export function parseInline(source: string, tail: boolean): InlineToken[] {
348
+ const tokens: InlineToken[] = [];
349
+ let buffer = '';
350
+ let index = 0;
351
+ const marks: Marks = {};
352
+
353
+ const flush = () => {
354
+ if (!buffer) return;
355
+ tokens.push({ kind: 'text', value: buffer, ...marks });
356
+ buffer = '';
357
+ };
358
+
359
+ while (index < source.length) {
360
+ const rest = source.slice(index);
361
+
362
+ // Escapes come first, or `\*` would open emphasis.
363
+ if (rest[0] === '\\' && rest.length > 1) {
364
+ buffer += rest[1];
365
+ index += 2;
366
+ continue;
367
+ }
368
+
369
+ // Inline code wins over emphasis: asterisks inside backticks are literal.
370
+ if (rest[0] === '`') {
371
+ const run = /^(`+)/.exec(rest)![1]!;
372
+ const closer = source.indexOf(run, index + run.length);
373
+ if (closer !== -1) {
374
+ flush();
375
+ tokens.push({ kind: 'code', value: source.slice(index + run.length, closer), ...marks });
376
+ index = closer + run.length;
377
+ continue;
378
+ }
379
+ if (tail) {
380
+ // Still being typed. Everything after the backtick is code so far.
381
+ flush();
382
+ const value = source.slice(index + run.length);
383
+ if (value) tokens.push({ kind: 'code', value, ...marks });
384
+ index = source.length;
385
+ continue;
386
+ }
387
+ buffer += run;
388
+ index += run.length;
389
+ continue;
390
+ }
391
+
392
+ const image = /^!\[([^\]]*)\]\(([^)\s]*)[^)]*\)/.exec(rest);
393
+ if (image) {
394
+ flush();
395
+ tokens.push({ kind: 'image', value: image[1]!, href: image[2]! });
396
+ index += image[0].length;
397
+ continue;
398
+ }
399
+
400
+ const link = /^\[([^\]]*)\]\(([^)\s]*)[^)]*\)/.exec(rest);
401
+ if (link) {
402
+ flush();
403
+ tokens.push({ kind: 'link', value: link[1]!, href: link[2]!, ...marks });
404
+ index += link[0].length;
405
+ continue;
406
+ }
407
+
408
+ // A link whose closing paren has not arrived. Show the label and nothing
409
+ // else — the brackets and the half-typed URL are scaffolding, and dropping
410
+ // them now means the text does not jump when the rest lands.
411
+ if (tail) {
412
+ const partialLink = /^\[([^\]]*)\](\([^)]*)?$/.exec(rest);
413
+ if (partialLink) {
414
+ flush();
415
+ if (partialLink[1]) tokens.push({ kind: 'text', value: partialLink[1], ...marks });
416
+ index = source.length;
417
+ continue;
418
+ }
419
+ }
420
+
421
+ const emphasis = /^(\*\*\*|___|\*\*|__|~~|\*|_)/.exec(rest);
422
+ if (emphasis) {
423
+ const marker = emphasis[1]!;
424
+ const applied = markFor(marker);
425
+ const open = Object.keys(applied).every((key) => marks[key as keyof Marks]);
426
+
427
+ // Closing whichever of these is currently open.
428
+ if (open) {
429
+ flush();
430
+ for (const key of Object.keys(applied)) delete marks[key as keyof Marks];
431
+ index += marker.length;
432
+ continue;
433
+ }
434
+
435
+ // An opener has to lean on the word it opens. `2 * 3` is multiplication,
436
+ // and `a * b` is not the start of anything — without this the asterisk
437
+ // disappears and the reader is left with two spaces where an operator was.
438
+ const after = source[index + marker.length];
439
+ if (after === undefined || /\s/.test(after)) {
440
+ buffer += marker;
441
+ index += marker.length;
442
+ continue;
443
+ }
444
+
445
+ const closer = source.indexOf(marker, index + marker.length);
446
+ if (closer !== -1) {
447
+ flush();
448
+ Object.assign(marks, applied);
449
+ index += marker.length;
450
+ continue;
451
+ }
452
+
453
+ if (tail && index + marker.length < source.length) {
454
+ // Unterminated at the end of a stream: assume it will close, so the
455
+ // words that have arrived are styled now rather than restyled later.
456
+ flush();
457
+ Object.assign(marks, applied);
458
+ index += marker.length;
459
+ continue;
460
+ }
461
+
462
+ // Unterminated in finished text, or nothing after it yet. Literal.
463
+ buffer += marker;
464
+ index += marker.length;
465
+ continue;
466
+ }
467
+
468
+ buffer += rest[0];
469
+ index += 1;
470
+ }
471
+
472
+ flush();
473
+ return tokens;
474
+ }
475
+
476
+ function markFor(marker: string): Marks {
477
+ switch (marker) {
478
+ case '***':
479
+ case '___':
480
+ return { bold: true, italic: true };
481
+ case '**':
482
+ case '__':
483
+ return { bold: true };
484
+ case '~~':
485
+ return { strike: true };
486
+ default:
487
+ return { italic: true };
488
+ }
489
+ }