termcast 1.3.33 → 1.3.35

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 (88) hide show
  1. package/dist/apis/cache.d.ts +1 -2
  2. package/dist/apis/cache.d.ts.map +1 -1
  3. package/dist/apis/cache.js +134 -52
  4. package/dist/apis/cache.js.map +1 -1
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +25 -0
  7. package/dist/build.js.map +1 -1
  8. package/dist/cli.js +6 -8
  9. package/dist/cli.js.map +1 -1
  10. package/dist/components/dropdown.js +3 -3
  11. package/dist/components/dropdown.js.map +1 -1
  12. package/dist/components/footer.d.ts.map +1 -1
  13. package/dist/components/footer.js +1 -1
  14. package/dist/components/footer.js.map +1 -1
  15. package/dist/components/icon.d.ts.map +1 -1
  16. package/dist/components/icon.js +386 -23
  17. package/dist/components/icon.js.map +1 -1
  18. package/dist/components/list.d.ts.map +1 -1
  19. package/dist/components/list.js +90 -22
  20. package/dist/components/list.js.map +1 -1
  21. package/dist/examples/list-controlled-search.d.ts +2 -0
  22. package/dist/examples/list-controlled-search.d.ts.map +1 -0
  23. package/dist/examples/list-controlled-search.js +12 -0
  24. package/dist/examples/list-controlled-search.js.map +1 -0
  25. package/dist/extensions/home.js +1 -1
  26. package/dist/extensions/home.js.map +1 -1
  27. package/dist/extensions/react-refresh-init.d.ts.map +1 -1
  28. package/dist/extensions/react-refresh-init.js +4 -3
  29. package/dist/extensions/react-refresh-init.js.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/internal/dialog.d.ts.map +1 -1
  34. package/dist/internal/dialog.js +4 -5
  35. package/dist/internal/dialog.js.map +1 -1
  36. package/dist/internal/providers.d.ts.map +1 -1
  37. package/dist/internal/providers.js +18 -5
  38. package/dist/internal/providers.js.map +1 -1
  39. package/dist/state.d.ts +1 -0
  40. package/dist/state.d.ts.map +1 -1
  41. package/dist/state.js.map +1 -1
  42. package/dist/theme.d.ts.map +1 -1
  43. package/dist/theme.js +6 -2
  44. package/dist/theme.js.map +1 -1
  45. package/dist/utils/run-command.js +3 -3
  46. package/dist/utils/run-command.js.map +1 -1
  47. package/dist/utils.d.ts +16 -1
  48. package/dist/utils.d.ts.map +1 -1
  49. package/dist/utils.js +28 -1
  50. package/dist/utils.js.map +1 -1
  51. package/dist/watcher.d.ts.map +1 -1
  52. package/dist/watcher.js +24 -4
  53. package/dist/watcher.js.map +1 -1
  54. package/package.json +10 -9
  55. package/src/apis/cache.test.ts +35 -3
  56. package/src/apis/cache.tsx +180 -57
  57. package/src/build.tsx +28 -0
  58. package/src/cli.tsx +8 -10
  59. package/src/compile.vitest.tsx +42 -24
  60. package/src/components/dropdown.tsx +3 -3
  61. package/src/components/footer.tsx +4 -2
  62. package/src/components/icon.tsx +385 -23
  63. package/src/components/list.tsx +104 -28
  64. package/src/examples/github.vitest.tsx +37 -37
  65. package/src/examples/list-controlled-search.tsx +28 -0
  66. package/src/examples/list-controlled-search.vitest.tsx +49 -0
  67. package/src/examples/list-detail-metadata.vitest.tsx +1 -1
  68. package/src/examples/list-dropdown-default.vitest.tsx +9 -9
  69. package/src/examples/list-scrollbox.vitest.tsx +55 -41
  70. package/src/examples/list-with-detail.vitest.tsx +35 -36
  71. package/src/examples/list-with-dropdown.vitest.tsx +2 -2
  72. package/src/examples/list-with-sections.vitest.tsx +153 -118
  73. package/src/examples/simple-file-picker.vitest.tsx +1 -1
  74. package/src/examples/simple-grid.vitest.tsx +44 -44
  75. package/src/examples/simple-navigation.vitest.tsx +43 -12
  76. package/src/examples/store.vitest.tsx +1 -1
  77. package/src/examples/swift-extension.vitest.tsx +3 -3
  78. package/src/extensions/dev.vitest.tsx +69 -34
  79. package/src/extensions/home.tsx +1 -1
  80. package/src/extensions/react-refresh-init.tsx +4 -3
  81. package/src/index.tsx +1 -0
  82. package/src/internal/dialog.tsx +21 -23
  83. package/src/internal/providers.tsx +18 -5
  84. package/src/state.tsx +1 -0
  85. package/src/theme.tsx +6 -2
  86. package/src/utils/run-command.tsx +3 -3
  87. package/src/utils.tsx +41 -1
  88. package/src/watcher.tsx +26 -6
@@ -1,32 +1,394 @@
1
1
  import { pascalCase } from 'change-case'
2
2
 
3
- const ICON_SHAPES = [
4
- '■', // filled square
5
- '●', // filled circle
6
- '◆', // filled diamond
7
- '▲', // filled triangle up
8
- '▼', // filled triangle down
9
- // '', // filled star
10
- // '', // diamond suit
11
- // '', // heart suit
12
- // '', // spade suit
13
- // '', // club suit
14
- ]
3
+ // Semantic unicode mapping for Raycast icon IDs.
4
+ // Uses ONLY characters from emoji-safe unicode ranges for consistent terminal text rendering.
5
+ // Safe ranges used: Arrows (U+2190), Math Operators (U+2200), Geometric Shapes (U+25A0,
6
+ // excluding U+25B6/U+25C0), Block Elements (U+2580), Misc Technical non-emoji subset,
7
+ // Box Drawing (U+2500), Letterlike Symbols, General Punctuation, Latin/ASCII.
8
+ const ICON_MAP: Record<string, string> = {
9
+ 'add-person-16': '',
10
+ 'airplane-16': '',
11
+ 'airplane-filled-16': '',
12
+ 'airplane-landing-16': '',
13
+ 'airplane-takeoff-16': '',
14
+ 'airpods-16': '⊚',
15
+ 'alarm-16': '⌀',
16
+ 'alarm-ringing-16': '⌀',
17
+ 'align-centre-16': '≡',
18
+ 'align-left-16': '≡',
19
+ 'align-right-16': '≡',
20
+ 'american-football-16': '◇',
21
+ 'anchor-16': '⊥',
22
+ 'app-window-16': '⊞',
23
+ 'app-window-grid-2x2-16': '⊞',
24
+ 'app-window-grid-3x3-16': '⊞',
25
+ 'app-window-list-16': '≡',
26
+ 'app-window-sidebar-left-16': '◧',
27
+ 'app-window-sidebar-right-16': '◨',
28
+ 'arrow-clockwise-16': '↻',
29
+ 'arrow-counter-clockwise-16': '↺',
30
+ 'arrow-down-16': '↓',
31
+ 'arrow-down-circle-16': '⇩',
32
+ 'arrow-down-circle-filled-16': '⇩',
33
+ 'arrow-left-16': '←',
34
+ 'arrow-left-circle-16': '⇦',
35
+ 'arrow-left-circle-filled-16': '⇦',
36
+ 'arrow-ne-16': '↗',
37
+ 'arrow-right-16': '→',
38
+ 'arrow-right-circle-16': '⇨',
39
+ 'arrow-right-circle-filled-16': '⇨',
40
+ 'arrow-up-16': '↑',
41
+ 'arrow-up-circle-16': '⇧',
42
+ 'arrow-up-circle-filled-16': '⇧',
43
+ 'arrows-contract-16': '⇤',
44
+ 'arrows-expand-16': '⇥',
45
+ 'at-symbol-16': '@',
46
+ 'band-aid-16': '╋',
47
+ 'bank-note-16': '¤',
48
+ 'bar-chart-16': '▊',
49
+ 'bar-code-16': '▐',
50
+ 'bath-tub-16': '▬',
51
+ 'battery-16': '▮',
52
+ 'battery-charging-16': '▮',
53
+ 'battery-disabled-16': '▯',
54
+ 'bell-16': '⍾',
55
+ 'bell-disabled-16': '⍻',
56
+ 'bike-16': '⎌',
57
+ 'binoculars-16': '⌕',
58
+ 'bird-16': '∿',
59
+ 'blank-document-16': '▯',
60
+ 'bluetooth-16': 'ᛒ',
61
+ 'boat-16': '≈',
62
+ 'bold-16': '𝐁',
63
+ 'bolt-16': '↯',
64
+ 'bolt-disabled-16': '↯',
65
+ 'book-16': '⊞',
66
+ 'bookmark-16': '▷',
67
+ 'box-16': '□',
68
+ 'brush-16': '⊘',
69
+ 'bug-16': '⊗',
70
+ 'building-16': '⌂',
71
+ 'bullet-points-16': '•',
72
+ 'bulls-eye-16': '◎',
73
+ 'bulls-eye-missed-16': '◌',
74
+ 'buoy-16': '◉',
75
+ 'calculator-16': '⊞',
76
+ 'calendar-16': '▦',
77
+ 'camera-16': '⌬',
78
+ 'car-16': '⊳',
79
+ 'cart-16': '⊲',
80
+ 'cd-16': '◎',
81
+ 'center-16': '⊡',
82
+ 'check-16': '✓',
83
+ 'check-circle-16': '✓',
84
+ 'check-list-16': '✓',
85
+ 'check-rosette-16': '✓',
86
+ 'checkmark-16': '✓',
87
+ 'chess-piece-16': '⊠',
88
+ 'chevron-down-16': '⌄',
89
+ 'chevron-down-small-16': '⌄',
90
+ 'chevron-left-16': '‹',
91
+ 'chevron-left-small-16': '‹',
92
+ 'chevron-right-16': '›',
93
+ 'chevron-right-small-16': '›',
94
+ 'chevron-up-16': '⌃',
95
+ 'chevron-up-down-16': '⇕',
96
+ 'chevron-up-small-16': '⌃',
97
+ 'circle-16': '○',
98
+ 'circle-disabled-16': '⊘',
99
+ 'circle-ellipsis-16': '◯',
100
+ 'circle-filled-16': '●',
101
+ 'circle-progress-16': '◔',
102
+ 'circle-progress-100-16': '●',
103
+ 'circle-progress-25-16': '◔',
104
+ 'circle-progress-50-16': '◑',
105
+ 'circle-progress-75-16': '◕',
106
+ 'clear-formatting-16': '⌧',
107
+ 'clipboard-16': '⎘',
108
+ 'clock-16': '◴',
109
+ 'cloud-16': '◠',
110
+ 'cloud-lightning-16': '◠',
111
+ 'cloud-rain-16': '◠',
112
+ 'cloud-snow-16': '◠',
113
+ 'cloud-sun-16': '◠',
114
+ 'code-16': '⟨⟩',
115
+ 'code-block-16': '▤',
116
+ 'cog-16': '⊛',
117
+ 'coin-16': '¤',
118
+ 'coins-16': '¤',
119
+ 'command-symbol-16': '⌘',
120
+ 'compass-16': '◎',
121
+ 'computer-chip-16': '⎔',
122
+ 'contrast-16': '◑',
123
+ 'copy-clipboard-16': '⎘',
124
+ 'credit-card-16': '▬',
125
+ 'cricket-ball-16': '◉',
126
+ 'crop-16': '⌌',
127
+ 'crown-16': '△',
128
+ 'crypto-16': '₿',
129
+ 'delete-document-16': '⌧',
130
+ 'desktop-16': '⎚',
131
+ 'devices-16': '⎚',
132
+ 'dna-16': '⧖',
133
+ 'document-16': '▯',
134
+ 'dot-16': '·',
135
+ 'download-16': '⤓',
136
+ 'droplets-16': '≋',
137
+ 'duplicate-16': '⧉',
138
+ 'edit-shape-16': '⌑',
139
+ 'eject-16': '△',
140
+ 'ellipsis-16': '…',
141
+ 'ellipsis-vertical-16': '⋮',
142
+ 'emoji-16': '◠',
143
+ 'emoji-sad-16': '◡',
144
+ 'envelope-16': '▷',
145
+ 'eraser-16': '⌫',
146
+ 'exclamationmark-16': '!',
147
+ 'exclamationmark-2-16': '‼',
148
+ 'exclamationmark-3-16': '‼',
149
+ 'eye-16': '◉',
150
+ 'eye-disabled-16': '⊘',
151
+ 'eye-dropper-16': '⊙',
152
+ 'female-16': '⊕',
153
+ 'film-strip-16': '▥',
154
+ 'filter-16': '⧩',
155
+ 'finder-16': '⌕',
156
+ 'fingerprint-16': '◉',
157
+ 'flag-16': '⊳',
158
+ 'folder-16': '▤',
159
+ 'footprints-16': '⊹',
160
+ 'forward-16': '▸',
161
+ 'forward-filled-16': '▸',
162
+ 'fountain-tip-16': '⌑',
163
+ 'full-signal-16': '▊',
164
+ 'game-controller-16': '⊞',
165
+ 'gauge-16': '◔',
166
+ 'geopin-16': '⊙',
167
+ 'germ-16': '※',
168
+ 'gift-16': '⊠',
169
+ 'glasses-16': '∞',
170
+ 'globe-01-16': '⊕',
171
+ 'goal-16': '⊡',
172
+ 'hammer-16': '⊤',
173
+ 'hard-drive-16': '▬',
174
+ 'hashtag-16': '#',
175
+ 'heading-16': '𝐇',
176
+ 'headphones-16': '⊚',
177
+ 'heart-16': '◆',
178
+ 'heart-disabled-16': '◇',
179
+ 'heartbeat-16': '◆',
180
+ 'highlight-16': '▮',
181
+ 'hourglass-16': '⧖',
182
+ 'house-16': '⌂',
183
+ 'humidity-16': '≋',
184
+ 'image-16': '⊞',
185
+ 'important-01-16': '‼',
186
+ 'info-01-16': 'ℹ',
187
+ 'italics-16': '𝐼',
188
+ 'key-16': '⊢',
189
+ 'keyboard-16': '⌗',
190
+ 'layers-16': '◫',
191
+ 'leaderboard-16': '▊',
192
+ 'leaf-16': '∿',
193
+ 'light-bulb-16': '※',
194
+ 'light-bulb-off-16': '※',
195
+ 'line-chart-16': '⌒',
196
+ 'link-16': '∞',
197
+ 'livestream-01-16': '◉',
198
+ 'livestream-disabled-01-16': '◌',
199
+ 'lock-16': '⊟',
200
+ 'lock-disabled-16': '⊠',
201
+ 'lock-unlocked-16': '⊠',
202
+ 'logout-16': '⎋',
203
+ 'lorry-16': '⊳',
204
+ 'lowercase-16': 'aᵃ',
205
+ 'magnifying-glass-16': '⌕',
206
+ 'male-16': '⊕',
207
+ 'map-16': '▦',
208
+ 'mask-16': '◑',
209
+ 'maximize-16': '⤢',
210
+ 'medical-support-16': '╋',
211
+ 'megaphone-16': '⊳',
212
+ 'memory-stick-16': '▬',
213
+ 'microphone-16': '⊙',
214
+ 'microphone-disabled-16': '⊘',
215
+ 'minimize-16': '⤡',
216
+ 'minus-16': '−',
217
+ 'minus-circle-16': '⊖',
218
+ 'minus-circle-filled-16': '⊖',
219
+ 'mobile-16': '▯',
220
+ 'monitor-16': '⎚',
221
+ 'moon-16': '◐',
222
+ 'moon-down-16': '◐',
223
+ 'moon-up-16': '◐',
224
+ 'moonrise-16': '◐',
225
+ 'mountain-16': '△',
226
+ 'mouse-16': '⊙',
227
+ 'move-16': '⊹',
228
+ 'mug-16': '⊔',
229
+ 'mug-steam-16': '⊔',
230
+ 'multiply-16': '×',
231
+ 'music-16': '∿',
232
+ 'network-16': '⊕',
233
+ 'new-document-16': '▯',
234
+ 'new-folder-16': '▤',
235
+ 'number-list-16': '⑴',
236
+ 'paperclip-16': '⊶',
237
+ 'paragraph-16': '¶',
238
+ 'patch-16': '╋',
239
+ 'pause-16': '‖',
240
+ 'pause-filled-16': '‖',
241
+ 'pencil-16': '⌑',
242
+ 'person-16': '⊙',
243
+ 'person-circle-16': '⊙',
244
+ 'person-lines-16': '⊙',
245
+ 'phone-16': '⌕',
246
+ 'phone-ringing-16': '⌕',
247
+ 'pie-chart-16': '◔',
248
+ 'pill-16': '⊝',
249
+ 'pin-16': '⊙',
250
+ 'pin-disabled-16': '⊘',
251
+ 'play-16': '▸',
252
+ 'play-filled-16': '▸',
253
+ 'plug-16': '⊢',
254
+ 'plus-16': '+',
255
+ 'plus-circle-16': '⊕',
256
+ 'plus-circle-filled-16': '⊕',
257
+ 'plus-minus-divide-multiply-16': '±',
258
+ 'plus-square-16': '⊞',
259
+ 'plus-top-right-square-16': '⊞',
260
+ 'power-16': '⊙',
261
+ 'print-16': '⎙',
262
+ 'question-mark-circle-16': '?',
263
+ 'quicklink-16': '∞',
264
+ 'quotation-marks-16': '❝',
265
+ 'quote-block-16': '❝',
266
+ 'racket-16': '⊙',
267
+ 'raindrop-16': '≋',
268
+ 'raycast-logo-neg-16': '◆',
269
+ 'raycast-logo-pos-16': '◆',
270
+ 'receipt-16': '▯',
271
+ 'redo-16': '↻',
272
+ 'remove-person-16': '⊖',
273
+ 'repeat-16': '↻',
274
+ 'replace-16': '⇄',
275
+ 'replace-one-16': '⇄',
276
+ 'reply-16': '↩',
277
+ 'rewind-16': '◂',
278
+ 'rewind-filled-16': '◂',
279
+ 'rocket-16': '⁕',
280
+ 'rosette-16': '✦',
281
+ 'rotate-anti-clockwise-16': '↺',
282
+ 'rotate-clockwise-16': '↻',
283
+ 'rss-16': '◉',
284
+ 'ruler-16': '⊢',
285
+ 'save-document-16': '⊞',
286
+ 'shield-01-16': '⊛',
287
+ 'short-paragraph-16': '¶',
288
+ 'shuffle-16': '⇝',
289
+ 'signal-0-16': '▁',
290
+ 'signal-1-16': '▂',
291
+ 'signal-2-16': '▄',
292
+ 'signal-3-16': '█',
293
+ 'snippets-16': '⊠',
294
+ 'snowflake-16': '※',
295
+ 'soccer-ball-16': '◎',
296
+ 'speaker-16': '◁',
297
+ 'speaker-down-16': '◁',
298
+ 'speaker-high-16': '◂',
299
+ 'speaker-low-16': '◁',
300
+ 'speaker-off-16': '◃',
301
+ 'speaker-on-16': '◂',
302
+ 'speaker-up-16': '◂',
303
+ 'speech-bubble-16': '⊐',
304
+ 'speech-bubble-active-16': '⊐',
305
+ 'speech-bubble-important-16': '⊐',
306
+ 'square-ellipsis-16': '□',
307
+ 'stacked-bars-1-16': '▁',
308
+ 'stacked-bars-2-16': '▃',
309
+ 'stacked-bars-3-16': '▅',
310
+ 'stacked-bars-4-16': '▇',
311
+ 'star-16': '★',
312
+ 'star-circle-16': '★',
313
+ 'star-disabled-16': '☆',
314
+ 'stars-16': '✦',
315
+ 'stop-16': '■',
316
+ 'stop-filled-16': '■',
317
+ 'stopwatch-16': '◴',
318
+ 'store-16': '⊞',
319
+ 'strike-through-16': 'S̶',
320
+ 'sun-16': '※',
321
+ 'sunrise-16': '※',
322
+ 'swatch-16': '◧',
323
+ 'switch-16': '⇋',
324
+ 'syringe-16': '⊸',
325
+ 'tack-16': '⊙',
326
+ 'tack-disabled-16': '⊘',
327
+ 'tag-16': '⊢',
328
+ 'temperature-16': '⊥',
329
+ 'tennis-ball-16': '◎',
330
+ 'terminal-16': '>_',
331
+ 'text-16': '𝐓',
332
+ 'text-cursor-16': '⌶',
333
+ 'text-input-16': '⌗',
334
+ 'text-selection-16': '▮',
335
+ 'thumbs-down-16': '⊘',
336
+ 'thumbs-down-filled-16': '⊘',
337
+ 'thumbs-up-16': '✓',
338
+ 'thumbs-up-filled-16': '✓',
339
+ 'ticket-16': '▬',
340
+ 'torch-16': '※',
341
+ 'train-16': '⊳',
342
+ 'trash-16': '⌧',
343
+ 'tray-16': '⊔',
344
+ 'tree-16': '⊥',
345
+ 'trophy-16': '⊛',
346
+ 'two-people-16': '⊙',
347
+ 'umbrella-16': '△',
348
+ 'underline-16': 'U̲',
349
+ 'undo-16': '↺',
350
+ 'upload-16': '⤒',
351
+ 'uppercase-16': 'Aᴬ',
352
+ 'video-16': '▸',
353
+ 'video-disabled-16': '⊘',
354
+ 'wallet-16': '▬',
355
+ 'wand-16': '✧',
356
+ 'warning-16': '△',
357
+ 'waveform-16': '∿',
358
+ 'weights-16': '⊥',
359
+ 'wifi-16': '⊛',
360
+ 'wifi-disabled-16': '⊘',
361
+ 'wind-16': '≋',
362
+ 'windsock-16': '⊳',
363
+ 'wrench-screwdriver-16': '⊤',
364
+ 'wrist-watch-16': '◴',
365
+ 'x-mark-circle-16': '×',
366
+ 'x-mark-circle-filled-16': '×',
367
+ 'x-mark-circle-half-dash-16': '×',
368
+ 'x-mark-top-right-square-16': '×',
369
+ 'xmark-16': '×',
370
+ }
15
371
 
16
- function hashString(str: string): number {
17
- let hash = 0
18
- for (let i = 0; i < str.length; i++) {
19
- const char = str.charCodeAt(i)
20
- hash = (hash << 5) - hash + char
21
- hash = hash & hash
372
+ // Number icons (number-00-16 through number-99-16) use circled numbers where available,
373
+ // falling back to the plain digit string for values without unicode circled forms.
374
+ for (let i = 0; i <= 99; i++) {
375
+ const id = `number-${String(i).padStart(2, '0')}-16`
376
+ if (i >= 0 && i <= 20) {
377
+ // Unicode circled numbers: ⓪①②…⑳
378
+ ICON_MAP[id] = String.fromCodePoint(i === 0 ? 0x24EA : 0x2460 + i - 1)
379
+ } else if (i <= 50) {
380
+ // Unicode circled numbers 21-50: ㉑㉒…㊿
381
+ ICON_MAP[id] = String.fromCodePoint(0x3251 + i - 21)
382
+ } else {
383
+ // No standard circled unicode above 50, use plain digits
384
+ ICON_MAP[id] = String(i)
22
385
  }
23
- return Math.abs(hash)
24
386
  }
25
387
 
388
+ const FALLBACK_ICON = '●'
389
+
26
390
  export function getIconShape(iconId: string): string {
27
- const hash = hashString(iconId)
28
- const index = hash % ICON_SHAPES.length
29
- return ICON_SHAPES[index]
391
+ return ICON_MAP[iconId] || FALLBACK_ICON
30
392
  }
31
393
 
32
394
  export const iconIds = [
@@ -591,7 +953,7 @@ export function getIconValue(icon: unknown): string {
591
953
 
592
954
  // Handle { fileIcon: string }
593
955
  if ('fileIcon' in obj && typeof obj.fileIcon === 'string') {
594
- return '📁'
956
+ return ''
595
957
  }
596
958
 
597
959
  // Unknown format - return empty string instead of [object Object]
@@ -81,30 +81,30 @@ function ListFooter(): any {
81
81
  const hasDropdown = listContext?.hasDropdown ?? false
82
82
 
83
83
  const content = hasToast ? null : (
84
- <box style={{ flexDirection: 'row', gap: 3 }}>
84
+ <box style={{ flexDirection: 'row', gap: 3, flexShrink: 0 }}>
85
85
  {firstActionTitle && (
86
- <box style={{ flexDirection: 'row', gap: 1 }}>
86
+ <box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
87
87
  <text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
88
88
 
89
89
  </text>
90
90
  <text flexShrink={0} fg={theme.textMuted}>{firstActionTitle.toLowerCase()}</text>
91
91
  </box>
92
92
  )}
93
- <box style={{ flexDirection: 'row', gap: 1 }}>
93
+ <box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
94
94
  <text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
95
95
  ↑↓
96
96
  </text>
97
97
  <text flexShrink={0} fg={theme.textMuted}>navigate</text>
98
98
  </box>
99
99
  {hasDropdown && (
100
- <box style={{ flexDirection: 'row', gap: 1 }}>
100
+ <box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
101
101
  <text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
102
102
  ^p
103
103
  </text>
104
104
  <text flexShrink={0} fg={theme.textMuted}>dropdown</text>
105
105
  </box>
106
106
  )}
107
- <box style={{ flexDirection: 'row', gap: 1 }}>
107
+ <box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
108
108
  <text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
109
109
  ^k
110
110
  </text>
@@ -774,8 +774,15 @@ export const List: ListType = (props) => {
774
774
  } = props
775
775
 
776
776
  const theme = useTheme()
777
- const [internalSearchText, setInternalSearchTextRaw] = useState('')
778
- const [selectedIndex, setSelectedIndex] = useState(0)
777
+ const currentStackSelectedListIndex = useStore((state) => {
778
+ const stack = state.navigationStack
779
+ const currentItem = stack[stack.length - 1]
780
+ return currentItem?.selectedListIndex
781
+ })
782
+ const [internalSearchText, setInternalSearchText] = useState('')
783
+ const [selectedIndex, setSelectedIndex] = useState<number>(() => {
784
+ return currentStackSelectedListIndex ?? 0
785
+ })
779
786
  const [isDropdownOpen, setIsDropdownOpen] = useState(false)
780
787
  const [currentDetail, setCurrentDetail] = useState<ReactNode>(null)
781
788
 
@@ -845,20 +852,55 @@ export const List: ListType = (props) => {
845
852
  setIsDropdownOpen(true)
846
853
  }
847
854
 
848
- // Wrapper function that updates search text
849
- const setInternalSearchText = (value: string) => {
850
- // Using flushSync to force descendants to update visibility before querying
851
- flushSync(() => {
852
- setInternalSearchTextRaw(value)
855
+ const persistSelectedIndexInCurrentNavigationItem = (index: number) => {
856
+ useStore.setState((state) => {
857
+ const stack = state.navigationStack
858
+ const currentIndex = stack.length - 1
859
+ const currentItem = stack[currentIndex]
860
+ if (!currentItem) {
861
+ return {}
862
+ }
863
+
864
+ if (currentItem.selectedListIndex === index) {
865
+ return {}
866
+ }
867
+
868
+ const nextStack = [...stack]
869
+ nextStack[currentIndex] = {
870
+ ...currentItem,
871
+ selectedListIndex: index,
872
+ }
873
+
874
+ return {
875
+ navigationStack: nextStack,
876
+ }
853
877
  })
878
+ }
879
+
880
+ const setSelectedIndexWithPersistence = (index: number) => {
881
+ setSelectedIndex(index)
882
+ persistSelectedIndexInCurrentNavigationItem(index)
883
+ }
884
+
885
+ // Sync selection to the first visible item whenever searchText changes.
886
+ // Runs after children's useLayoutEffects (descendants registered) but before paint,
887
+ // so there is no intermediate frame with stale selection.
888
+ // Works for both controlled and uncontrolled searchText.
889
+ const prevSearchTextRef = useRef(searchText)
890
+ useLayoutEffect(() => {
891
+ if (prevSearchTextRef.current === searchText) return
892
+ prevSearchTextRef.current = searchText
893
+
894
+ if (!isFilteringEnabled) return
895
+
854
896
  const items = Object.values(descendantsContext.map.current)
855
897
  .filter((item) => item.index !== -1 && item.props?.visible !== false)
856
898
  .sort((a, b) => a.index - b.index)
857
899
 
858
900
  if (items.length > 0 && items[0]) {
859
- setSelectedIndex(items[0].index)
901
+ setSelectedIndexWithPersistence(items[0].index)
860
902
  }
861
- }
903
+ })
862
904
 
863
905
  const listContextValue = useMemo<ListContextValue>(
864
906
  () => ({
@@ -866,7 +908,7 @@ export const List: ListType = (props) => {
866
908
  setIsDropdownOpen,
867
909
  openDropdown,
868
910
  selectedIndex,
869
- setSelectedIndex,
911
+ setSelectedIndex: setSelectedIndexWithPersistence,
870
912
  searchText,
871
913
  isFiltering: isFilteringEnabled,
872
914
  setCurrentDetail,
@@ -878,15 +920,15 @@ export const List: ListType = (props) => {
878
920
  [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail, isLoading, searchBarAccessory],
879
921
  )
880
922
 
881
- // Clear detail when detail view is hidden
882
- useEffect(() => {
923
+ // Clear detail when detail view is hidden (before paint to avoid flash)
924
+ useLayoutEffect(() => {
883
925
  if (!isShowingDetail) {
884
926
  setCurrentDetail(null)
885
927
  }
886
928
  }, [isShowingDetail])
887
929
 
888
- // Handle selectedItemId prop changes
889
- useEffect(() => {
930
+ // Handle selectedItemId prop changes (before paint to avoid flash)
931
+ useLayoutEffect(() => {
890
932
  // Only update selection if selectedItemId is explicitly provided
891
933
  if (selectedItemId !== undefined) {
892
934
  const items = Object.values(descendantsContext.map.current)
@@ -894,7 +936,7 @@ export const List: ListType = (props) => {
894
936
 
895
937
  const foundItem = items.find((item) => item.props?.id === selectedItemId)
896
938
  if (foundItem) {
897
- setSelectedIndex(foundItem.index)
939
+ setSelectedIndexWithPersistence(foundItem.index)
898
940
  }
899
941
  }
900
942
  }, [selectedItemId])
@@ -930,6 +972,21 @@ export const List: ListType = (props) => {
930
972
  scrollBox.scrollTo(Math.max(0, targetScrollTop))
931
973
  }
932
974
 
975
+ // Track whether onLoadMore has been called and we're waiting for new items.
976
+ // Reset when item count changes (new items arrived) so we can trigger again.
977
+ const paginationCalledRef = useRef(false)
978
+ const prevItemCountRef = useRef(0)
979
+
980
+ const triggerPaginationIfNeeded = (currentVisibleIndex: number, totalItems: number) => {
981
+ if (!props.pagination?.hasMore || paginationCalledRef.current) return
982
+ // Trigger when within 5 items of the end, matching Raycast's behavior
983
+ const threshold = Math.min(5, Math.max(1, Math.floor(totalItems * 0.2)))
984
+ if (totalItems - currentVisibleIndex <= threshold) {
985
+ paginationCalledRef.current = true
986
+ props.pagination.onLoadMore()
987
+ }
988
+ }
989
+
933
990
  const move = (direction: -1 | 1) => {
934
991
  // Get all visible items
935
992
  const items = Object.values(descendantsContext.map.current)
@@ -938,6 +995,12 @@ export const List: ListType = (props) => {
938
995
 
939
996
  if (items.length === 0) return
940
997
 
998
+ // Reset pagination lock when new items arrive
999
+ if (items.length !== prevItemCountRef.current) {
1000
+ prevItemCountRef.current = items.length
1001
+ paginationCalledRef.current = false
1002
+ }
1003
+
941
1004
  // Find currently selected item's position in visible items
942
1005
  let currentVisibleIndex = items.findIndex(
943
1006
  (item) => item.index === selectedIndex,
@@ -945,13 +1008,21 @@ export const List: ListType = (props) => {
945
1008
  if (currentVisibleIndex === -1) {
946
1009
  // If current selection is not visible, select first visible item
947
1010
  if (items[0]) {
948
- setSelectedIndex(items[0].index)
1011
+ setSelectedIndexWithPersistence(items[0].index)
949
1012
  }
950
1013
  return
951
1014
  }
952
1015
 
953
1016
  // Calculate next visible index
954
1017
  let nextVisibleIndex = currentVisibleIndex + direction
1018
+
1019
+ // When navigating past the end and pagination has more, don't wrap
1020
+ if (direction === 1 && nextVisibleIndex >= items.length && props.pagination?.hasMore) {
1021
+ triggerPaginationIfNeeded(currentVisibleIndex, items.length)
1022
+ // Stay on the last item instead of wrapping
1023
+ return
1024
+ }
1025
+
955
1026
  if (nextVisibleIndex < 0) nextVisibleIndex = items.length - 1
956
1027
  if (nextVisibleIndex >= items.length) nextVisibleIndex = 0
957
1028
 
@@ -960,7 +1031,11 @@ export const List: ListType = (props) => {
960
1031
  flushSync(() => {
961
1032
  setSelectedIndex(nextItem.index)
962
1033
  })
1034
+ persistSelectedIndexInCurrentNavigationItem(nextItem.index)
963
1035
  scrollToItem(nextItem)
1036
+
1037
+ // Check if we're approaching the end and should trigger pagination
1038
+ triggerPaginationIfNeeded(nextVisibleIndex, items.length)
964
1039
  }
965
1040
  }
966
1041
 
@@ -1102,10 +1177,11 @@ export const List: ListType = (props) => {
1102
1177
  rootOptions: {
1103
1178
  backgroundColor: undefined,
1104
1179
  },
1180
+ viewportOptions: {
1181
+ paddingRight: 0,
1182
+ },
1105
1183
  scrollbarOptions: {
1106
-
1107
- showArrows: true,
1108
-
1184
+ visible: false,
1109
1185
  },
1110
1186
  }}
1111
1187
  >
@@ -1269,8 +1345,8 @@ const ListItem: ListItemType = (props) => {
1269
1345
  const selectedIndex = listContext?.selectedIndex ?? 0
1270
1346
  const isActive = index === selectedIndex
1271
1347
 
1272
- // Update detail when this item becomes active or detail prop changes
1273
- useEffect(() => {
1348
+ // Update detail when this item becomes active or detail prop changes (before paint)
1349
+ useLayoutEffect(() => {
1274
1350
  if (isActive && listContext?.isShowingDetail && listContext?.setCurrentDetail) {
1275
1351
  listContext.setCurrentDetail(props.detail || null)
1276
1352
  }
@@ -1486,8 +1562,8 @@ const ListDropdown: ListDropdownType = (props) => {
1486
1562
  [],
1487
1563
  )
1488
1564
 
1489
- // Open dropdown dialog when triggered
1490
- useEffect(() => {
1565
+ // Open dropdown dialog when triggered (before paint to avoid flash)
1566
+ useLayoutEffect(() => {
1491
1567
  if (isDropdownOpen && !dialog.stack.length) {
1492
1568
  // Pass the children to the dialog to render them there
1493
1569
  dialog.push({