panelui-native 0.25.0 → 0.30.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 (242) hide show
  1. package/README.md +18 -1
  2. package/lib/module/components/accordion/index.js +8 -8
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/alert/index.js +2 -2
  5. package/lib/module/components/alert/index.js.map +1 -1
  6. package/lib/module/components/area-chart/index.js +182 -50
  7. package/lib/module/components/area-chart/index.js.map +1 -1
  8. package/lib/module/components/attachment/index.js +2 -1
  9. package/lib/module/components/attachment/index.js.map +1 -1
  10. package/lib/module/components/avatar/index.js +3 -3
  11. package/lib/module/components/avatar/index.js.map +1 -1
  12. package/lib/module/components/badge/index.js +4 -4
  13. package/lib/module/components/badge/index.js.map +1 -1
  14. package/lib/module/components/bar-chart/index.js +197 -56
  15. package/lib/module/components/bar-chart/index.js.map +1 -1
  16. package/lib/module/components/bottom-sheet/index.js +15 -10
  17. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  18. package/lib/module/components/breadcrumb/index.js +6 -6
  19. package/lib/module/components/breadcrumb/index.js.map +1 -1
  20. package/lib/module/components/button/index.js +11 -6
  21. package/lib/module/components/button/index.js.map +1 -1
  22. package/lib/module/components/calendar/index.js +792 -0
  23. package/lib/module/components/calendar/index.js.map +1 -0
  24. package/lib/module/components/carousel/index.js +680 -0
  25. package/lib/module/components/carousel/index.js.map +1 -0
  26. package/lib/module/components/chip/index.js +4 -4
  27. package/lib/module/components/chip/index.js.map +1 -1
  28. package/lib/module/components/code-block/highlight.js +363 -0
  29. package/lib/module/components/code-block/highlight.js.map +1 -0
  30. package/lib/module/components/code-block/index.js +313 -0
  31. package/lib/module/components/code-block/index.js.map +1 -0
  32. package/lib/module/components/date-picker/index.js +165 -0
  33. package/lib/module/components/date-picker/index.js.map +1 -0
  34. package/lib/module/components/dialog/index.js +7 -2
  35. package/lib/module/components/dialog/index.js.map +1 -1
  36. package/lib/module/components/direction/index.js +2 -1
  37. package/lib/module/components/direction/index.js.map +1 -1
  38. package/lib/module/components/empty-state/index.js +2 -2
  39. package/lib/module/components/empty-state/index.js.map +1 -1
  40. package/lib/module/components/field/index.js +5 -5
  41. package/lib/module/components/field/index.js.map +1 -1
  42. package/lib/module/components/flow/index.js +3 -3
  43. package/lib/module/components/flow/index.js.map +1 -1
  44. package/lib/module/components/frame/index.js +5 -5
  45. package/lib/module/components/frame/index.js.map +1 -1
  46. package/lib/module/components/input-group/index.js +3 -2
  47. package/lib/module/components/input-group/index.js.map +1 -1
  48. package/lib/module/components/item/index.js +9 -9
  49. package/lib/module/components/item/index.js.map +1 -1
  50. package/lib/module/components/label/index.js +4 -4
  51. package/lib/module/components/label/index.js.map +1 -1
  52. package/lib/module/components/line-chart/index.js +167 -44
  53. package/lib/module/components/line-chart/index.js.map +1 -1
  54. package/lib/module/components/map/index.js +7 -7
  55. package/lib/module/components/map/index.js.map +1 -1
  56. package/lib/module/components/marker/index.js +3 -3
  57. package/lib/module/components/marker/index.js.map +1 -1
  58. package/lib/module/components/menu/index.js +714 -0
  59. package/lib/module/components/menu/index.js.map +1 -0
  60. package/lib/module/components/message/index.js +7 -7
  61. package/lib/module/components/message/index.js.map +1 -1
  62. package/lib/module/components/message-scroller/index.js +5 -4
  63. package/lib/module/components/message-scroller/index.js.map +1 -1
  64. package/lib/module/components/plan/index.js +296 -0
  65. package/lib/module/components/plan/index.js.map +1 -0
  66. package/lib/module/components/popover/index.js +73 -10
  67. package/lib/module/components/popover/index.js.map +1 -1
  68. package/lib/module/components/radio-group/index.js +15 -10
  69. package/lib/module/components/radio-group/index.js.map +1 -1
  70. package/lib/module/components/reasoning/index.js +244 -0
  71. package/lib/module/components/reasoning/index.js.map +1 -0
  72. package/lib/module/components/section-rail/index.js +4 -4
  73. package/lib/module/components/section-rail/index.js.map +1 -1
  74. package/lib/module/components/select/index.js +10 -4
  75. package/lib/module/components/select/index.js.map +1 -1
  76. package/lib/module/components/sources/index.js +221 -0
  77. package/lib/module/components/sources/index.js.map +1 -0
  78. package/lib/module/components/steps/index.js +4 -4
  79. package/lib/module/components/steps/index.js.map +1 -1
  80. package/lib/module/components/tabs/index.js +7 -7
  81. package/lib/module/components/tabs/index.js.map +1 -1
  82. package/lib/module/components/task/index.js +300 -0
  83. package/lib/module/components/task/index.js.map +1 -0
  84. package/lib/module/components/textarea/index.js +295 -0
  85. package/lib/module/components/textarea/index.js.map +1 -0
  86. package/lib/module/components/timeline/index.js +4 -4
  87. package/lib/module/components/timeline/index.js.map +1 -1
  88. package/lib/module/components/toast/index.js +2 -2
  89. package/lib/module/components/toast/index.js.map +1 -1
  90. package/lib/module/components/toggle-button/index.js +5 -5
  91. package/lib/module/components/toggle-button/index.js.map +1 -1
  92. package/lib/module/components/tooltip/index.js +648 -0
  93. package/lib/module/components/tooltip/index.js.map +1 -0
  94. package/lib/module/components/typography/index.js +2 -2
  95. package/lib/module/components/typography/index.js.map +1 -1
  96. package/lib/module/hooks/index.js +1 -0
  97. package/lib/module/hooks/index.js.map +1 -1
  98. package/lib/module/hooks/use-back-handler.js +37 -0
  99. package/lib/module/hooks/use-back-handler.js.map +1 -0
  100. package/lib/module/icons/index.js +202 -0
  101. package/lib/module/icons/index.js.map +1 -1
  102. package/lib/module/index.js +13 -1
  103. package/lib/module/index.js.map +1 -1
  104. package/lib/module/primitives/collapse.js +112 -0
  105. package/lib/module/primitives/collapse.js.map +1 -0
  106. package/lib/module/primitives/keyboard-avoider.js +2 -1
  107. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  108. package/lib/module/primitives/text.js +25 -1
  109. package/lib/module/primitives/text.js.map +1 -1
  110. package/lib/module/utils/date.js +386 -0
  111. package/lib/module/utils/date.js.map +1 -0
  112. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  114. package/lib/typescript/src/components/area-chart/index.d.ts +41 -1
  115. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/attachment/index.d.ts.map +1 -1
  117. package/lib/typescript/src/components/bar-chart/index.d.ts +44 -2
  118. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  119. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  120. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
  121. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  122. package/lib/typescript/src/components/calendar/index.d.ts +173 -0
  123. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -0
  124. package/lib/typescript/src/components/carousel/index.d.ts +179 -0
  125. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -0
  126. package/lib/typescript/src/components/chip/index.d.ts.map +1 -1
  127. package/lib/typescript/src/components/code-block/highlight.d.ts +39 -0
  128. package/lib/typescript/src/components/code-block/highlight.d.ts.map +1 -0
  129. package/lib/typescript/src/components/code-block/index.d.ts +113 -0
  130. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -0
  131. package/lib/typescript/src/components/date-picker/index.d.ts +97 -0
  132. package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -0
  133. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
  135. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  136. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  137. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  138. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  139. package/lib/typescript/src/components/item/index.d.ts.map +1 -1
  140. package/lib/typescript/src/components/label/index.d.ts.map +1 -1
  141. package/lib/typescript/src/components/line-chart/index.d.ts +41 -2
  142. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  143. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  144. package/lib/typescript/src/components/marker/index.d.ts.map +1 -1
  145. package/lib/typescript/src/components/menu/index.d.ts +261 -0
  146. package/lib/typescript/src/components/menu/index.d.ts.map +1 -0
  147. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  148. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/plan/index.d.ts +127 -0
  150. package/lib/typescript/src/components/plan/index.d.ts.map +1 -0
  151. package/lib/typescript/src/components/popover/index.d.ts +35 -1
  152. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  153. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  154. package/lib/typescript/src/components/reasoning/index.d.ts +97 -0
  155. package/lib/typescript/src/components/reasoning/index.d.ts.map +1 -0
  156. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  157. package/lib/typescript/src/components/sources/index.d.ts +85 -0
  158. package/lib/typescript/src/components/sources/index.d.ts.map +1 -0
  159. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  160. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  161. package/lib/typescript/src/components/task/index.d.ts +103 -0
  162. package/lib/typescript/src/components/task/index.d.ts.map +1 -0
  163. package/lib/typescript/src/components/textarea/index.d.ts +175 -0
  164. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -0
  165. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  166. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  167. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
  168. package/lib/typescript/src/components/tooltip/index.d.ts +180 -0
  169. package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -0
  170. package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
  171. package/lib/typescript/src/hooks/index.d.ts +1 -0
  172. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  173. package/lib/typescript/src/hooks/use-back-handler.d.ts +19 -0
  174. package/lib/typescript/src/hooks/use-back-handler.d.ts.map +1 -0
  175. package/lib/typescript/src/icons/index.d.ts +17 -0
  176. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  177. package/lib/typescript/src/index.d.ts +16 -4
  178. package/lib/typescript/src/index.d.ts.map +1 -1
  179. package/lib/typescript/src/primitives/collapse.d.ts +42 -0
  180. package/lib/typescript/src/primitives/collapse.d.ts.map +1 -0
  181. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  182. package/lib/typescript/src/primitives/text.d.ts +20 -0
  183. package/lib/typescript/src/primitives/text.d.ts.map +1 -1
  184. package/lib/typescript/src/utils/date.d.ts +116 -0
  185. package/lib/typescript/src/utils/date.d.ts.map +1 -0
  186. package/package.json +1 -1
  187. package/src/components/accordion/index.tsx +8 -12
  188. package/src/components/alert/index.tsx +2 -2
  189. package/src/components/area-chart/index.tsx +208 -55
  190. package/src/components/attachment/index.tsx +2 -2
  191. package/src/components/avatar/index.tsx +3 -3
  192. package/src/components/badge/index.tsx +6 -6
  193. package/src/components/bar-chart/index.tsx +214 -54
  194. package/src/components/bottom-sheet/index.tsx +17 -16
  195. package/src/components/breadcrumb/index.tsx +6 -8
  196. package/src/components/button/index.tsx +12 -8
  197. package/src/components/calendar/index.tsx +970 -0
  198. package/src/components/carousel/index.tsx +788 -0
  199. package/src/components/chip/index.tsx +4 -6
  200. package/src/components/code-block/highlight.ts +278 -0
  201. package/src/components/code-block/index.tsx +341 -0
  202. package/src/components/date-picker/index.tsx +255 -0
  203. package/src/components/dialog/index.tsx +7 -2
  204. package/src/components/direction/index.tsx +2 -1
  205. package/src/components/empty-state/index.tsx +2 -2
  206. package/src/components/field/index.tsx +3 -5
  207. package/src/components/flow/index.tsx +3 -3
  208. package/src/components/frame/index.tsx +5 -7
  209. package/src/components/input-group/index.tsx +3 -2
  210. package/src/components/item/index.tsx +9 -9
  211. package/src/components/label/index.tsx +2 -2
  212. package/src/components/line-chart/index.tsx +189 -50
  213. package/src/components/map/index.tsx +7 -9
  214. package/src/components/marker/index.tsx +3 -3
  215. package/src/components/menu/index.tsx +812 -0
  216. package/src/components/message/index.tsx +7 -7
  217. package/src/components/message-scroller/index.tsx +5 -4
  218. package/src/components/plan/index.tsx +325 -0
  219. package/src/components/popover/index.tsx +111 -8
  220. package/src/components/radio-group/index.tsx +13 -4
  221. package/src/components/reasoning/index.tsx +297 -0
  222. package/src/components/section-rail/index.tsx +4 -4
  223. package/src/components/select/index.tsx +10 -4
  224. package/src/components/sources/index.tsx +278 -0
  225. package/src/components/steps/index.tsx +4 -4
  226. package/src/components/tabs/index.tsx +7 -9
  227. package/src/components/task/index.tsx +329 -0
  228. package/src/components/textarea/index.tsx +381 -0
  229. package/src/components/timeline/index.tsx +4 -4
  230. package/src/components/toast/index.tsx +2 -2
  231. package/src/components/toggle-button/index.tsx +5 -7
  232. package/src/components/tooltip/index.tsx +793 -0
  233. package/src/components/typography/index.tsx +2 -2
  234. package/src/hooks/index.ts +1 -0
  235. package/src/hooks/use-back-handler.ts +37 -0
  236. package/src/icons/index.tsx +147 -0
  237. package/src/index.ts +116 -0
  238. package/src/primitives/collapse.tsx +128 -0
  239. package/src/primitives/keyboard-avoider.tsx +2 -1
  240. package/src/primitives/text.tsx +28 -1
  241. package/src/utils/date.ts +446 -0
  242. package/theme.css +112 -0
@@ -38,7 +38,7 @@ import {
38
38
  AnimatedPressable,
39
39
  type AnimatedPressableProps,
40
40
  } from '../../primitives/animated-pressable';
41
- import { Text } from '../../primitives/text';
41
+ import { Text, textChildren } from '../../primitives/text';
42
42
  import { cn } from '../../utils/cn';
43
43
  import { selectionTick } from '../../utils/haptics';
44
44
 
@@ -246,13 +246,11 @@ const ChipRoot = forwardRef<View, ChipProps>(
246
246
  const body = (
247
247
  <>
248
248
  {start}
249
- {typeof children === 'string' ? (
249
+ {textChildren(children, (text) => (
250
250
  <Text className={slots.label({ className: labelClassName })}>
251
- {children}
251
+ {text}
252
252
  </Text>
253
- ) : (
254
- children
255
- )}
253
+ ))}
256
254
  {closable ? (
257
255
  <Pressable
258
256
  accessibilityRole="button"
@@ -0,0 +1,278 @@
1
+ /**
2
+ * A small syntax highlighter, sized for a phone.
3
+ *
4
+ * The web answer to this problem is a real tokenizer with a grammar per
5
+ * language, which is megabytes of WASM and a worker. That is the wrong shape
6
+ * here twice over: a chat renders short fragments rather than files, and the
7
+ * fragments arrive a token at a time, so whatever runs has to run again on
8
+ * every frame of a stream. This trades exactness for a single pass of regex
9
+ * that costs nothing and never blocks.
10
+ *
11
+ * What it therefore does *not* do: nested languages, template-literal
12
+ * interpolation, or anything requiring a parser to know it. What it does do is
13
+ * make a keyword look like a keyword, which is the whole of the value in a
14
+ * twelve-line snippet.
15
+ *
16
+ * Anything it does not recognise is returned as one plain token per line, so
17
+ * an unknown language renders as clean monospace rather than as a guess.
18
+ */
19
+
20
+ /** The kinds a token can be. Each maps to one theme colour. */
21
+ export type TokenKind =
22
+ | 'plain'
23
+ | 'keyword'
24
+ | 'string'
25
+ | 'number'
26
+ | 'comment'
27
+ | 'function'
28
+ | 'property'
29
+ | 'punctuation'
30
+ | 'inserted'
31
+ | 'deleted';
32
+
33
+ export interface Token {
34
+ text: string;
35
+ kind: TokenKind;
36
+ }
37
+
38
+ /** The languages that get more than plain monospace. */
39
+ export type CodeLanguage =
40
+ | 'ts'
41
+ | 'tsx'
42
+ | 'js'
43
+ | 'jsx'
44
+ | 'json'
45
+ | 'bash'
46
+ | 'python'
47
+ | 'css'
48
+ | 'html'
49
+ | 'sql'
50
+ | 'markdown'
51
+ | 'diff'
52
+ | 'text';
53
+
54
+ /** Spellings a caller is likely to pass, mapped onto the ones handled here. */
55
+ const ALIASES: Record<string, CodeLanguage> = {
56
+ typescript: 'ts',
57
+ ts: 'ts',
58
+ tsx: 'tsx',
59
+ javascript: 'js',
60
+ js: 'js',
61
+ jsx: 'jsx',
62
+ mjs: 'js',
63
+ cjs: 'js',
64
+ json: 'json',
65
+ json5: 'json',
66
+ bash: 'bash',
67
+ sh: 'bash',
68
+ shell: 'bash',
69
+ zsh: 'bash',
70
+ console: 'bash',
71
+ python: 'python',
72
+ py: 'python',
73
+ css: 'css',
74
+ scss: 'css',
75
+ html: 'html',
76
+ xml: 'html',
77
+ svg: 'html',
78
+ sql: 'sql',
79
+ markdown: 'markdown',
80
+ md: 'markdown',
81
+ mdx: 'markdown',
82
+ diff: 'diff',
83
+ patch: 'diff',
84
+ };
85
+
86
+ /** Resolves a caller's language string. Unknown spellings become `text`. */
87
+ export function resolveLanguage(language: string | undefined): CodeLanguage {
88
+ if (!language) return 'text';
89
+ return ALIASES[language.toLowerCase()] ?? 'text';
90
+ }
91
+
92
+ const JS_KEYWORDS =
93
+ 'as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|is|keyof|let|new|of|private|protected|public|readonly|return|satisfies|set|static|super|switch|this|throw|try|type|typeof|var|void|while|yield';
94
+ const JS_LITERALS = 'true|false|null|undefined|NaN|Infinity';
95
+
96
+ const PYTHON_KEYWORDS =
97
+ 'and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield';
98
+ const PYTHON_LITERALS = 'True|False|None|self';
99
+
100
+ const SQL_KEYWORDS =
101
+ 'ADD|ALL|ALTER|AND|AS|ASC|BETWEEN|BY|CASE|COLUMN|CREATE|CROSS|DELETE|DESC|DISTINCT|DROP|ELSE|END|EXISTS|FROM|FULL|GROUP|HAVING|IN|INDEX|INNER|INSERT|INTO|IS|JOIN|LEFT|LIKE|LIMIT|NOT|NULL|OFFSET|ON|OR|ORDER|OUTER|PRIMARY|RIGHT|SELECT|SET|TABLE|THEN|UNION|UPDATE|VALUES|WHEN|WHERE|WITH';
102
+
103
+ const BASH_KEYWORDS =
104
+ 'case|do|done|elif|else|esac|export|fi|for|function|if|in|local|return|source|then|until|while';
105
+
106
+ /**
107
+ * One rule: a pattern, and the kind it produces.
108
+ *
109
+ * Order is the whole design. Comments and strings come first so that a keyword
110
+ * inside either is left alone — the single most visible failure a highlighter
111
+ * this size can have.
112
+ */
113
+ interface Rule {
114
+ pattern: RegExp;
115
+ kind: TokenKind;
116
+ }
117
+
118
+ function wordsOf(list: string): RegExp {
119
+ return new RegExp(`^(?:${list})\\b`);
120
+ }
121
+
122
+ const RULES: Record<CodeLanguage, Rule[]> = {
123
+ ts: jsRules(),
124
+ tsx: jsRules(),
125
+ js: jsRules(),
126
+ jsx: jsRules(),
127
+ json: [
128
+ { pattern: /^"(?:[^"\\]|\\.)*"\s*(?=:)/, kind: 'property' },
129
+ { pattern: /^"(?:[^"\\]|\\.)*"/, kind: 'string' },
130
+ { pattern: /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?/i, kind: 'number' },
131
+ { pattern: /^(?:true|false|null)\b/, kind: 'keyword' },
132
+ { pattern: /^[{}[\],:]/, kind: 'punctuation' },
133
+ ],
134
+ bash: [
135
+ { pattern: /^#.*/, kind: 'comment' },
136
+ { pattern: /^"(?:[^"\\]|\\.)*"/, kind: 'string' },
137
+ { pattern: /^'(?:[^'\\]|\\.)*'/, kind: 'string' },
138
+ { pattern: /^\$\{?[A-Za-z_][\w]*\}?/, kind: 'property' },
139
+ { pattern: wordsOf(BASH_KEYWORDS), kind: 'keyword' },
140
+ { pattern: /^-{1,2}[A-Za-z][\w-]*/, kind: 'property' },
141
+ { pattern: /^[|&;<>()$]/, kind: 'punctuation' },
142
+ ],
143
+ python: [
144
+ { pattern: /^#.*/, kind: 'comment' },
145
+ { pattern: /^[rbf]?"""[\s\S]*?(?:"""|$)/, kind: 'string' },
146
+ { pattern: /^[rbf]?"(?:[^"\\]|\\.)*"/, kind: 'string' },
147
+ { pattern: /^[rbf]?'(?:[^'\\]|\\.)*'/, kind: 'string' },
148
+ { pattern: wordsOf(PYTHON_KEYWORDS), kind: 'keyword' },
149
+ { pattern: wordsOf(PYTHON_LITERALS), kind: 'keyword' },
150
+ { pattern: /^[A-Za-z_]\w*(?=\s*\()/, kind: 'function' },
151
+ { pattern: /^@[A-Za-z_]\w*/, kind: 'function' },
152
+ { pattern: /^\d[\d_]*(?:\.\d+)?/, kind: 'number' },
153
+ { pattern: /^[{}[\]().,:;]/, kind: 'punctuation' },
154
+ ],
155
+ css: [
156
+ { pattern: /^\/\*[\s\S]*?(?:\*\/|$)/, kind: 'comment' },
157
+ { pattern: /^"(?:[^"\\]|\\.)*"/, kind: 'string' },
158
+ { pattern: /^'(?:[^'\\]|\\.)*'/, kind: 'string' },
159
+ { pattern: /^--[\w-]+/, kind: 'property' },
160
+ { pattern: /^@[\w-]+/, kind: 'keyword' },
161
+ { pattern: /^[.#][\w-]+/, kind: 'function' },
162
+ { pattern: /^[a-z-]+(?=\s*:)/i, kind: 'property' },
163
+ { pattern: /^-?\d*\.?\d+(?:px|rem|em|%|vh|vw|s|ms|deg)?/, kind: 'number' },
164
+ { pattern: /^[{}();:,]/, kind: 'punctuation' },
165
+ ],
166
+ html: [
167
+ { pattern: /^<!--[\s\S]*?(?:-->|$)/, kind: 'comment' },
168
+ { pattern: /^"(?:[^"\\]|\\.)*"/, kind: 'string' },
169
+ { pattern: /^'(?:[^'\\]|\\.)*'/, kind: 'string' },
170
+ { pattern: /^<\/?[\w-]+/, kind: 'keyword' },
171
+ { pattern: /^[\w-]+(?==)/, kind: 'property' },
172
+ { pattern: /^\/?>/, kind: 'keyword' },
173
+ { pattern: /^[=]/, kind: 'punctuation' },
174
+ ],
175
+ sql: [
176
+ { pattern: /^--.*/, kind: 'comment' },
177
+ { pattern: /^'(?:[^'\\]|\\.)*'/, kind: 'string' },
178
+ { pattern: new RegExp(`^(?:${SQL_KEYWORDS})\\b`, 'i'), kind: 'keyword' },
179
+ { pattern: /^\d+(?:\.\d+)?/, kind: 'number' },
180
+ { pattern: /^[(),;*.]/, kind: 'punctuation' },
181
+ ],
182
+ markdown: [
183
+ { pattern: /^#{1,6}\s.*/, kind: 'keyword' },
184
+ { pattern: /^`[^`]*`/, kind: 'string' },
185
+ { pattern: /^\*\*[^*]+\*\*/, kind: 'function' },
186
+ { pattern: /^\[[^\]]*\]\([^)]*\)/, kind: 'property' },
187
+ { pattern: /^[>-]\s/, kind: 'punctuation' },
188
+ ],
189
+ diff: [],
190
+ text: [],
191
+ };
192
+
193
+ function jsRules(): Rule[] {
194
+ return [
195
+ { pattern: /^\/\/.*/, kind: 'comment' },
196
+ { pattern: /^\/\*[\s\S]*?(?:\*\/|$)/, kind: 'comment' },
197
+ { pattern: /^`(?:[^`\\]|\\.)*`/, kind: 'string' },
198
+ { pattern: /^"(?:[^"\\]|\\.)*"/, kind: 'string' },
199
+ { pattern: /^'(?:[^'\\]|\\.)*'/, kind: 'string' },
200
+ { pattern: wordsOf(JS_KEYWORDS), kind: 'keyword' },
201
+ { pattern: wordsOf(JS_LITERALS), kind: 'keyword' },
202
+ // A name followed by a paren reads as a call whether or not it is one,
203
+ // which is exactly the reading a highlighter is meant to support.
204
+ { pattern: /^[A-Za-z_$][\w$]*(?=\s*\()/, kind: 'function' },
205
+ { pattern: /^0[xb][\da-f]+|^\d[\d_]*(?:\.\d+)?(?:e[+-]?\d+)?/i, kind: 'number' },
206
+ { pattern: /^[{}[\]().,;:?!<>=+\-*/%&|^~]/, kind: 'punctuation' },
207
+ ];
208
+ }
209
+
210
+ /**
211
+ * A diff is coloured by line, not by token — the leading `+` or `-` says
212
+ * everything, and highlighting the code inside as well fights with it.
213
+ */
214
+ function tokenizeDiffLine(line: string): Token[] {
215
+ if (line.startsWith('+')) return [{ text: line, kind: 'inserted' }];
216
+ if (line.startsWith('-')) return [{ text: line, kind: 'deleted' }];
217
+ if (line.startsWith('@@') || line.startsWith('diff ')) {
218
+ return [{ text: line, kind: 'comment' }];
219
+ }
220
+ return [{ text: line, kind: 'plain' }];
221
+ }
222
+
223
+ /** Runs the rules over one line, longest-first from wherever it has got to. */
224
+ function tokenizeLine(line: string, rules: Rule[]): Token[] {
225
+ if (rules.length === 0) return line ? [{ text: line, kind: 'plain' }] : [];
226
+
227
+ const tokens: Token[] = [];
228
+ let rest = line;
229
+ let plain = '';
230
+
231
+ while (rest.length > 0) {
232
+ let matched: { text: string; kind: TokenKind } | null = null;
233
+
234
+ for (const rule of rules) {
235
+ const found = rule.pattern.exec(rest);
236
+ if (found && found[0].length > 0) {
237
+ matched = { text: found[0], kind: rule.kind };
238
+ break;
239
+ }
240
+ }
241
+
242
+ if (matched) {
243
+ // Whatever was skipped over to get here is one plain run, not one plain
244
+ // token per character — a token per character is a view per character.
245
+ if (plain) {
246
+ tokens.push({ text: plain, kind: 'plain' });
247
+ plain = '';
248
+ }
249
+ tokens.push(matched);
250
+ rest = rest.slice(matched.text.length);
251
+ } else {
252
+ plain += rest[0];
253
+ rest = rest.slice(1);
254
+ }
255
+ }
256
+
257
+ if (plain) tokens.push({ text: plain, kind: 'plain' });
258
+ return tokens;
259
+ }
260
+
261
+ /**
262
+ * Splits code into lines of tokens.
263
+ *
264
+ * Line by line, so a stream that has just gained a character only makes the
265
+ * work grow with the code and never with time. The cost of that choice is that
266
+ * a block comment spanning several lines is only coloured on its first — an
267
+ * acceptable trade for a snippet, and the reason this is not sold as a
268
+ * highlighter for a file.
269
+ */
270
+ export function highlight(code: string, language: string | undefined): Token[][] {
271
+ const resolved = resolveLanguage(language);
272
+ const lines = code.split('\n');
273
+
274
+ if (resolved === 'diff') return lines.map(tokenizeDiffLine);
275
+
276
+ const rules = RULES[resolved];
277
+ return lines.map((line) => tokenizeLine(line, rules));
278
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * CodeBlock — a fenced snippet, syntax-coloured and scrolled sideways.
3
+ *
4
+ * Code does not wrap. Wrapping a line of code puts the continuation under the
5
+ * indentation and the two stop meaning different things, so the body scrolls
6
+ * horizontally instead — which on a phone is the only honest way to show a
7
+ * ninety-column line.
8
+ *
9
+ * ```tsx
10
+ * <CodeBlock code={code} language="tsx">
11
+ * <CodeBlock.Header>
12
+ * <CodeBlock.Filename>calendar.tsx</CodeBlock.Filename>
13
+ * <CodeBlock.CopyButton />
14
+ * </CodeBlock.Header>
15
+ * </CodeBlock>
16
+ * ```
17
+ *
18
+ * ## Colouring
19
+ *
20
+ * The highlighter is a single pass of regex, shipped in the component, over the
21
+ * languages a chat actually streams. There is no grammar and no worker, because
22
+ * the alternative costs megabytes and has to run again on every frame of a
23
+ * stream. It gets keywords, strings, comments and numbers right in a twelve-line
24
+ * snippet and does not pretend to more; an unrecognised language renders as
25
+ * clean monospace rather than as a guess. Colours come from the theme's
26
+ * `--color-code-*` tokens, so a snippet follows light and dark like everything
27
+ * else.
28
+ *
29
+ * ## Where the props come from
30
+ *
31
+ * With the AI SDK, `code` and `language` are what you pull out of a fenced
32
+ * block in a text part, or the fields of a tool call that wrote a file.
33
+ */
34
+ import { createContext, useContext, useMemo, type ReactNode } from 'react';
35
+ import { ScrollView, View, type ViewProps } from 'react-native';
36
+ import { tv } from 'tailwind-variants';
37
+ import { useCSSVariable } from 'uniwind';
38
+ import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard';
39
+ import { CheckIcon, CopyIcon } from '../../icons';
40
+ import { Text } from '../../primitives/text';
41
+ import { cn } from '../../utils/cn';
42
+ import { AnimatedPressable } from '../../primitives/animated-pressable';
43
+ import { highlight, resolveLanguage, type Token, type TokenKind } from './highlight';
44
+
45
+ const codeBlockVariants = tv({
46
+ slots: {
47
+ root: 'w-full overflow-hidden rounded-2xl border border-border bg-muted/40',
48
+ header: 'flex-row items-center justify-between gap-2 border-b border-border px-3 py-2',
49
+ filename: 'font-mono text-xs text-muted-foreground',
50
+ language: 'text-xs uppercase tracking-wide text-muted-foreground',
51
+ actions: 'flex-row items-center gap-1',
52
+ action: 'h-7 w-7 items-center justify-center rounded-lg active:bg-accent',
53
+ body: 'p-3',
54
+ line: 'flex-row',
55
+ /*
56
+ * `leading-*` on the row rather than on each token: the tokens of a line
57
+ * are separate Text nodes, and a line height set on some of them and not
58
+ * others makes the baseline jog wherever a keyword ends.
59
+ */
60
+ gutter: 'w-8 pe-3 text-end font-mono text-xs text-muted-foreground/50',
61
+ code: 'font-mono text-xs leading-5',
62
+ },
63
+ });
64
+
65
+ interface CodeBlockContextValue {
66
+ code: string;
67
+ }
68
+
69
+ const CodeBlockContext = createContext<CodeBlockContextValue | null>(null);
70
+
71
+ function useCodeBlock(component: string): CodeBlockContextValue {
72
+ const context = useContext(CodeBlockContext);
73
+ if (!context) {
74
+ throw new Error(`${component} must be used within a <CodeBlock>`);
75
+ }
76
+ return context;
77
+ }
78
+
79
+ /** Which theme token paints each kind of token. */
80
+ const TOKEN_COLORS: Record<TokenKind, string> = {
81
+ plain: '--color-foreground',
82
+ keyword: '--color-code-keyword',
83
+ string: '--color-code-string',
84
+ number: '--color-code-number',
85
+ comment: '--color-code-comment',
86
+ function: '--color-code-function',
87
+ property: '--color-code-property',
88
+ punctuation: '--color-code-punctuation',
89
+ inserted: '--color-code-inserted',
90
+ deleted: '--color-code-deleted',
91
+ };
92
+
93
+ /**
94
+ * The palette, resolved once for the whole block.
95
+ *
96
+ * `useCSSVariable` is a hook, so it cannot be called per token — and a snippet
97
+ * has hundreds. Ten lookups at the top and a lookup table underneath is the
98
+ * same answer at a fraction of the cost.
99
+ */
100
+ function useTokenColors(): Record<TokenKind, string | undefined> {
101
+ const plain = useCSSVariable(TOKEN_COLORS.plain);
102
+ const keyword = useCSSVariable(TOKEN_COLORS.keyword);
103
+ const string = useCSSVariable(TOKEN_COLORS.string);
104
+ const number = useCSSVariable(TOKEN_COLORS.number);
105
+ const comment = useCSSVariable(TOKEN_COLORS.comment);
106
+ const fn = useCSSVariable(TOKEN_COLORS.function);
107
+ const property = useCSSVariable(TOKEN_COLORS.property);
108
+ const punctuation = useCSSVariable(TOKEN_COLORS.punctuation);
109
+ const inserted = useCSSVariable(TOKEN_COLORS.inserted);
110
+ const deleted = useCSSVariable(TOKEN_COLORS.deleted);
111
+
112
+ const only = (value: unknown) => (typeof value === 'string' ? value : undefined);
113
+
114
+ return useMemo(
115
+ () => ({
116
+ plain: only(plain),
117
+ keyword: only(keyword),
118
+ string: only(string),
119
+ number: only(number),
120
+ comment: only(comment),
121
+ function: only(fn),
122
+ property: only(property),
123
+ punctuation: only(punctuation),
124
+ inserted: only(inserted),
125
+ deleted: only(deleted),
126
+ }),
127
+ [plain, keyword, string, number, comment, fn, property, punctuation, inserted, deleted]
128
+ );
129
+ }
130
+
131
+ export interface CodeBlockProps extends Omit<ViewProps, 'children'> {
132
+ className?: string;
133
+ /** The snippet. */
134
+ code: string;
135
+ /**
136
+ * What it is written in. `ts`, `tsx`, `js`, `jsx`, `json`, `bash`, `python`,
137
+ * `css`, `html`, `sql`, `markdown` and `diff` are coloured, along with the
138
+ * usual spellings of each; anything else renders as plain monospace.
139
+ */
140
+ language?: string;
141
+ /** Number the lines in a gutter down the leading edge. */
142
+ showLineNumbers?: boolean;
143
+ /** A header, and anything else that goes above the code. */
144
+ children?: ReactNode;
145
+ }
146
+
147
+ function CodeBlockRoot({
148
+ className,
149
+ code,
150
+ language,
151
+ showLineNumbers = false,
152
+ children,
153
+ ...props
154
+ }: CodeBlockProps) {
155
+ const slots = codeBlockVariants();
156
+ const colors = useTokenColors();
157
+ const lines = useMemo(() => highlight(code, language), [code, language]);
158
+ const context = useMemo(() => ({ code }), [code]);
159
+ const gutterWidth = String(lines.length).length;
160
+
161
+ return (
162
+ <CodeBlockContext.Provider value={context}>
163
+ <View {...props} className={cn(slots.root(), className)}>
164
+ {children}
165
+ {/*
166
+ Horizontal only. The rows are laid out at their natural width inside
167
+ it, so the longest line sets the scrollable width and every row shares
168
+ it — otherwise each row would scroll to its own end and the gutter
169
+ would drift away from its code.
170
+ */}
171
+ <ScrollView
172
+ horizontal
173
+ bounces={false}
174
+ showsHorizontalScrollIndicator={false}
175
+ contentContainerClassName={slots.body()}
176
+ >
177
+ <View>
178
+ {lines.map((tokens, index) => (
179
+ <View key={index} className={slots.line()}>
180
+ {showLineNumbers ? (
181
+ <Text
182
+ className={slots.gutter()}
183
+ style={{ width: 16 + gutterWidth * 7 }}
184
+ >
185
+ {index + 1}
186
+ </Text>
187
+ ) : null}
188
+ <CodeLine tokens={tokens} colors={colors} className={slots.code()} />
189
+ </View>
190
+ ))}
191
+ </View>
192
+ </ScrollView>
193
+ </View>
194
+ </CodeBlockContext.Provider>
195
+ );
196
+ }
197
+ CodeBlockRoot.displayName = 'CodeBlock';
198
+
199
+ /**
200
+ * One line, as a single Text with a run of coloured children.
201
+ *
202
+ * Nested Text rather than a row of Views: only inside one Text do the runs
203
+ * share a baseline and keep their spaces. A row of Views gives every token its
204
+ * own box and the line comes apart at the seams.
205
+ */
206
+ function CodeLine({
207
+ tokens,
208
+ colors,
209
+ className,
210
+ }: {
211
+ tokens: Token[];
212
+ colors: Record<TokenKind, string | undefined>;
213
+ className: string;
214
+ }) {
215
+ return (
216
+ <Text className={className}>
217
+ {/* An empty line still needs a glyph's worth of height. */}
218
+ {tokens.length === 0 ? ' ' : null}
219
+ {tokens.map((token, index) => (
220
+ <Text key={index} className={className} style={{ color: colors[token.kind] }}>
221
+ {token.text}
222
+ </Text>
223
+ ))}
224
+ </Text>
225
+ );
226
+ }
227
+
228
+ export interface CodeBlockHeaderProps extends ViewProps {
229
+ className?: string;
230
+ children?: ReactNode;
231
+ }
232
+
233
+ /** The bar above the code: what the file is, and what can be done with it. */
234
+ function CodeBlockHeader({ className, children, ...props }: CodeBlockHeaderProps) {
235
+ const { header } = codeBlockVariants();
236
+ return (
237
+ <View {...props} className={cn(header(), className)}>
238
+ {children}
239
+ </View>
240
+ );
241
+ }
242
+
243
+ export interface CodeBlockFilenameProps extends ViewProps {
244
+ className?: string;
245
+ children?: ReactNode;
246
+ }
247
+
248
+ function CodeBlockFilename({ className, children, ...props }: CodeBlockFilenameProps) {
249
+ const { filename } = codeBlockVariants();
250
+ return (
251
+ <Text numberOfLines={1} className={cn(filename(), className)} {...props}>
252
+ {children}
253
+ </Text>
254
+ );
255
+ }
256
+
257
+ export interface CodeBlockLanguageProps extends ViewProps {
258
+ className?: string;
259
+ children?: ReactNode;
260
+ }
261
+
262
+ /** The language, for a block with no filename to name it by. */
263
+ function CodeBlockLanguage({ className, children, ...props }: CodeBlockLanguageProps) {
264
+ const { language } = codeBlockVariants();
265
+ return (
266
+ <Text className={cn(language(), className)} {...props}>
267
+ {children}
268
+ </Text>
269
+ );
270
+ }
271
+
272
+ export interface CodeBlockActionsProps extends ViewProps {
273
+ className?: string;
274
+ children?: ReactNode;
275
+ }
276
+
277
+ function CodeBlockActions({ className, children, ...props }: CodeBlockActionsProps) {
278
+ const { actions } = codeBlockVariants();
279
+ return (
280
+ <View {...props} className={cn(actions(), className)}>
281
+ {children}
282
+ </View>
283
+ );
284
+ }
285
+
286
+ export interface CodeBlockCopyButtonProps {
287
+ className?: string;
288
+ /** How long the tick stays up before turning back into the copy glyph. */
289
+ timeout?: number;
290
+ onCopy?: () => void;
291
+ }
292
+
293
+ /**
294
+ * Copies the block's code.
295
+ *
296
+ * The tick is the whole feedback: there is no toast, because a snippet in a
297
+ * transcript is copied often and a notification for each one is noise.
298
+ */
299
+ function CodeBlockCopyButton({ className, timeout = 2000, onCopy }: CodeBlockCopyButtonProps) {
300
+ const { code } = useCodeBlock('CodeBlock.CopyButton');
301
+ const { action } = codeBlockVariants();
302
+ const { copy, copied } = useCopyToClipboard({ timeout });
303
+ // Both glyphs are given a colour: the check's own fallback is white, which is
304
+ // invisible on this header in a light theme.
305
+ const rawTick = useCSSVariable('--color-success');
306
+ const rawIdle = useCSSVariable('--color-muted-foreground');
307
+ const tick = typeof rawTick === 'string' ? rawTick : undefined;
308
+ const idle = typeof rawIdle === 'string' ? rawIdle : undefined;
309
+
310
+ return (
311
+ <AnimatedPressable
312
+ accessibilityRole="button"
313
+ accessibilityLabel={copied ? 'Copied' : 'Copy code'}
314
+ accessibilityState={{ selected: copied }}
315
+ hitSlop={6}
316
+ onPress={() => {
317
+ void copy(code);
318
+ onCopy?.();
319
+ }}
320
+ className={cn(action(), className)}
321
+ >
322
+ {copied ? <CheckIcon size={14} color={tick} /> : <CopyIcon size={14} color={idle} />}
323
+ </AnimatedPressable>
324
+ );
325
+ }
326
+
327
+ CodeBlockHeader.displayName = 'CodeBlock.Header';
328
+ CodeBlockFilename.displayName = 'CodeBlock.Filename';
329
+ CodeBlockLanguage.displayName = 'CodeBlock.Language';
330
+ CodeBlockActions.displayName = 'CodeBlock.Actions';
331
+ CodeBlockCopyButton.displayName = 'CodeBlock.CopyButton';
332
+
333
+ export { resolveLanguage, type CodeLanguage, type Token, type TokenKind } from './highlight';
334
+
335
+ export const CodeBlock = Object.assign(CodeBlockRoot, {
336
+ Header: CodeBlockHeader,
337
+ Filename: CodeBlockFilename,
338
+ Language: CodeBlockLanguage,
339
+ Actions: CodeBlockActions,
340
+ CopyButton: CodeBlockCopyButton,
341
+ });