termcast 1.3.34 → 1.3.36

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 (57) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +25 -0
  3. package/dist/build.js.map +1 -1
  4. package/dist/components/footer.d.ts.map +1 -1
  5. package/dist/components/footer.js +1 -1
  6. package/dist/components/footer.js.map +1 -1
  7. package/dist/components/icon.d.ts.map +1 -1
  8. package/dist/components/icon.js +386 -23
  9. package/dist/components/icon.js.map +1 -1
  10. package/dist/components/list.d.ts.map +1 -1
  11. package/dist/components/list.js +70 -7
  12. package/dist/components/list.js.map +1 -1
  13. package/dist/extensions/home.js +1 -1
  14. package/dist/extensions/home.js.map +1 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/internal/dialog.d.ts.map +1 -1
  19. package/dist/internal/dialog.js +4 -5
  20. package/dist/internal/dialog.js.map +1 -1
  21. package/dist/internal/providers.d.ts.map +1 -1
  22. package/dist/internal/providers.js +18 -5
  23. package/dist/internal/providers.js.map +1 -1
  24. package/dist/state.d.ts +1 -0
  25. package/dist/state.d.ts.map +1 -1
  26. package/dist/state.js.map +1 -1
  27. package/dist/theme.d.ts.map +1 -1
  28. package/dist/theme.js +6 -2
  29. package/dist/theme.js.map +1 -1
  30. package/dist/utils.d.ts +16 -1
  31. package/dist/utils.d.ts.map +1 -1
  32. package/dist/utils.js +28 -1
  33. package/dist/utils.js.map +1 -1
  34. package/package.json +8 -4
  35. package/src/build.tsx +28 -0
  36. package/src/compile.vitest.tsx +18 -18
  37. package/src/components/footer.tsx +4 -2
  38. package/src/components/icon.tsx +385 -23
  39. package/src/components/list.tsx +84 -13
  40. package/src/examples/github.vitest.tsx +36 -36
  41. package/src/examples/list-detail-metadata.vitest.tsx +1 -1
  42. package/src/examples/list-dropdown-default.vitest.tsx +9 -9
  43. package/src/examples/list-scrollbox.vitest.tsx +41 -41
  44. package/src/examples/list-with-detail.vitest.tsx +35 -36
  45. package/src/examples/list-with-sections.vitest.tsx +117 -117
  46. package/src/examples/simple-grid.vitest.tsx +44 -44
  47. package/src/examples/simple-navigation.vitest.tsx +43 -12
  48. package/src/examples/store.vitest.tsx +1 -1
  49. package/src/examples/swift-extension.vitest.tsx +3 -3
  50. package/src/extensions/dev.vitest.tsx +21 -21
  51. package/src/extensions/home.tsx +1 -1
  52. package/src/index.tsx +1 -0
  53. package/src/internal/dialog.tsx +21 -23
  54. package/src/internal/providers.tsx +18 -5
  55. package/src/state.tsx +1 -0
  56. package/src/theme.tsx +6 -2
  57. package/src/utils.tsx +40 -1
@@ -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 currentStackSelectedListIndex = useStore((state) => {
778
+ const stack = state.navigationStack
779
+ const currentItem = stack[stack.length - 1]
780
+ return currentItem?.selectedListIndex
781
+ })
777
782
  const [internalSearchText, setInternalSearchText] = useState('')
778
- const [selectedIndex, setSelectedIndex] = useState(0)
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,6 +852,36 @@ export const List: ListType = (props) => {
845
852
  setIsDropdownOpen(true)
846
853
  }
847
854
 
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
+ }
877
+ })
878
+ }
879
+
880
+ const setSelectedIndexWithPersistence = (index: number) => {
881
+ setSelectedIndex(index)
882
+ persistSelectedIndexInCurrentNavigationItem(index)
883
+ }
884
+
848
885
  // Sync selection to the first visible item whenever searchText changes.
849
886
  // Runs after children's useLayoutEffects (descendants registered) but before paint,
850
887
  // so there is no intermediate frame with stale selection.
@@ -861,7 +898,7 @@ export const List: ListType = (props) => {
861
898
  .sort((a, b) => a.index - b.index)
862
899
 
863
900
  if (items.length > 0 && items[0]) {
864
- setSelectedIndex(items[0].index)
901
+ setSelectedIndexWithPersistence(items[0].index)
865
902
  }
866
903
  })
867
904
 
@@ -871,7 +908,7 @@ export const List: ListType = (props) => {
871
908
  setIsDropdownOpen,
872
909
  openDropdown,
873
910
  selectedIndex,
874
- setSelectedIndex,
911
+ setSelectedIndex: setSelectedIndexWithPersistence,
875
912
  searchText,
876
913
  isFiltering: isFilteringEnabled,
877
914
  setCurrentDetail,
@@ -899,7 +936,7 @@ export const List: ListType = (props) => {
899
936
 
900
937
  const foundItem = items.find((item) => item.props?.id === selectedItemId)
901
938
  if (foundItem) {
902
- setSelectedIndex(foundItem.index)
939
+ setSelectedIndexWithPersistence(foundItem.index)
903
940
  }
904
941
  }
905
942
  }, [selectedItemId])
@@ -935,6 +972,21 @@ export const List: ListType = (props) => {
935
972
  scrollBox.scrollTo(Math.max(0, targetScrollTop))
936
973
  }
937
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
+
938
990
  const move = (direction: -1 | 1) => {
939
991
  // Get all visible items
940
992
  const items = Object.values(descendantsContext.map.current)
@@ -943,6 +995,12 @@ export const List: ListType = (props) => {
943
995
 
944
996
  if (items.length === 0) return
945
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
+
946
1004
  // Find currently selected item's position in visible items
947
1005
  let currentVisibleIndex = items.findIndex(
948
1006
  (item) => item.index === selectedIndex,
@@ -950,13 +1008,21 @@ export const List: ListType = (props) => {
950
1008
  if (currentVisibleIndex === -1) {
951
1009
  // If current selection is not visible, select first visible item
952
1010
  if (items[0]) {
953
- setSelectedIndex(items[0].index)
1011
+ setSelectedIndexWithPersistence(items[0].index)
954
1012
  }
955
1013
  return
956
1014
  }
957
1015
 
958
1016
  // Calculate next visible index
959
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
+
960
1026
  if (nextVisibleIndex < 0) nextVisibleIndex = items.length - 1
961
1027
  if (nextVisibleIndex >= items.length) nextVisibleIndex = 0
962
1028
 
@@ -965,7 +1031,11 @@ export const List: ListType = (props) => {
965
1031
  flushSync(() => {
966
1032
  setSelectedIndex(nextItem.index)
967
1033
  })
1034
+ persistSelectedIndexInCurrentNavigationItem(nextItem.index)
968
1035
  scrollToItem(nextItem)
1036
+
1037
+ // Check if we're approaching the end and should trigger pagination
1038
+ triggerPaginationIfNeeded(nextVisibleIndex, items.length)
969
1039
  }
970
1040
  }
971
1041
 
@@ -1107,10 +1177,11 @@ export const List: ListType = (props) => {
1107
1177
  rootOptions: {
1108
1178
  backgroundColor: undefined,
1109
1179
  },
1180
+ viewportOptions: {
1181
+ paddingRight: 0,
1182
+ },
1110
1183
  scrollbarOptions: {
1111
-
1112
- showArrows: true,
1113
-
1184
+ visible: false,
1114
1185
  },
1115
1186
  }}
1116
1187
  >