termcast 1.3.32 → 1.3.34
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.
- package/dist/action-utils.d.ts.map +1 -1
- package/dist/action-utils.js +8 -0
- package/dist/action-utils.js.map +1 -1
- package/dist/apis/cache.d.ts +1 -2
- package/dist/apis/cache.d.ts.map +1 -1
- package/dist/apis/cache.js +138 -54
- package/dist/apis/cache.js.map +1 -1
- package/dist/apis/clipboard.d.ts.map +1 -1
- package/dist/apis/clipboard.js +4 -0
- package/dist/apis/clipboard.js.map +1 -1
- package/dist/apis/oauth.d.ts.map +1 -1
- package/dist/apis/oauth.js +31 -4
- package/dist/apis/oauth.js.map +1 -1
- package/dist/build.d.ts +0 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +30 -51
- package/dist/build.js.map +1 -1
- package/dist/cli.js +31 -14
- package/dist/cli.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -1
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.d.ts +14 -0
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +151 -59
- package/dist/components/actions.js.map +1 -1
- package/dist/components/alert.d.ts.map +1 -1
- package/dist/components/alert.js +6 -5
- package/dist/components/alert.js.map +1 -1
- package/dist/components/animation-tick.d.ts +1 -1
- package/dist/components/animation-tick.js +1 -1
- package/dist/components/animation-tick.js.map +1 -1
- package/dist/components/detail.d.ts +5 -31
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +36 -52
- package/dist/components/detail.js.map +1 -1
- package/dist/components/dropdown.d.ts +1 -1
- package/dist/components/dropdown.d.ts.map +1 -1
- package/dist/components/dropdown.js +50 -22
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +19 -18
- package/dist/components/footer.js.map +1 -1
- package/dist/components/form/checkbox.d.ts.map +1 -1
- package/dist/components/form/checkbox.js +12 -11
- package/dist/components/form/checkbox.js.map +1 -1
- package/dist/components/form/date-picker.d.ts.map +1 -1
- package/dist/components/form/date-picker.js +7 -22
- package/dist/components/form/date-picker.js.map +1 -1
- package/dist/components/form/description.d.ts +1 -1
- package/dist/components/form/description.d.ts.map +1 -1
- package/dist/components/form/description.js +6 -5
- package/dist/components/form/description.js.map +1 -1
- package/dist/components/form/dropdown.d.ts.map +1 -1
- package/dist/components/form/dropdown.js +53 -50
- package/dist/components/form/dropdown.js.map +1 -1
- package/dist/components/form/file-autocomplete.d.ts.map +1 -1
- package/dist/components/form/file-autocomplete.js +5 -4
- package/dist/components/form/file-autocomplete.js.map +1 -1
- package/dist/components/form/file-picker.d.ts.map +1 -1
- package/dist/components/form/file-picker.js +23 -22
- package/dist/components/form/file-picker.js.map +1 -1
- package/dist/components/form/form-end.d.ts.map +1 -1
- package/dist/components/form/form-end.js +6 -4
- package/dist/components/form/form-end.js.map +1 -1
- package/dist/components/form/form-field-wrapper.d.ts +15 -0
- package/dist/components/form/form-field-wrapper.d.ts.map +1 -0
- package/dist/components/form/form-field-wrapper.js +29 -0
- package/dist/components/form/form-field-wrapper.js.map +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +31 -30
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/password-field.d.ts.map +1 -1
- package/dist/components/form/password-field.js +7 -6
- package/dist/components/form/password-field.js.map +1 -1
- package/dist/components/form/separator.d.ts.map +1 -1
- package/dist/components/form/separator.js +3 -2
- package/dist/components/form/separator.js.map +1 -1
- package/dist/components/form/tagpicker.d.ts.map +1 -1
- package/dist/components/form/tagpicker.js +2 -1
- package/dist/components/form/tagpicker.js.map +1 -1
- package/dist/components/form/text-area.d.ts.map +1 -1
- package/dist/components/form/text-area.js +7 -6
- package/dist/components/form/text-area.js.map +1 -1
- package/dist/components/form/text-field.d.ts.map +1 -1
- package/dist/components/form/text-field.js +7 -6
- package/dist/components/form/text-field.js.map +1 -1
- package/dist/components/form/use-form-navigation.d.ts.map +1 -1
- package/dist/components/form/use-form-navigation.js +4 -4
- package/dist/components/form/use-form-navigation.js.map +1 -1
- package/dist/components/form/with-left-border.d.ts +15 -0
- package/dist/components/form/with-left-border.d.ts.map +1 -1
- package/dist/components/form/with-left-border.js +21 -9
- package/dist/components/form/with-left-border.js.map +1 -1
- package/dist/components/icon.d.ts +14 -0
- package/dist/components/icon.d.ts.map +1 -1
- package/dist/components/icon.js +60 -0
- package/dist/components/icon.js.map +1 -1
- package/dist/components/image.d.ts +47 -2
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +46 -7
- package/dist/components/image.js.map +1 -1
- package/dist/components/list.d.ts +5 -0
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +188 -132
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-bar.d.ts.map +1 -1
- package/dist/components/loading-bar.js +4 -3
- package/dist/components/loading-bar.js.map +1 -1
- package/dist/components/metadata.d.ts +70 -0
- package/dist/components/metadata.d.ts.map +1 -0
- package/dist/components/metadata.js +82 -0
- package/dist/components/metadata.js.map +1 -0
- package/dist/components/theme-picker.d.ts.map +1 -1
- package/dist/components/theme-picker.js +3 -2
- package/dist/components/theme-picker.js.map +1 -1
- package/dist/descendants-v2.d.ts +60 -0
- package/dist/descendants-v2.d.ts.map +1 -0
- package/dist/descendants-v2.js +144 -0
- package/dist/descendants-v2.js.map +1 -0
- package/dist/examples/actions-context.d.ts +2 -0
- package/dist/examples/actions-context.d.ts.map +1 -0
- package/dist/examples/actions-context.js +33 -0
- package/dist/examples/actions-context.js.map +1 -0
- package/dist/examples/form-basic.d.ts.map +1 -1
- package/dist/examples/form-basic.js +1 -1
- package/dist/examples/form-basic.js.map +1 -1
- package/dist/examples/form-dropdown.js +1 -1
- package/dist/examples/form-dropdown.js.map +1 -1
- package/dist/examples/internal/custom-action-renderables.d.ts +70 -0
- package/dist/examples/internal/custom-action-renderables.d.ts.map +1 -0
- package/dist/examples/internal/custom-action-renderables.js +163 -0
- package/dist/examples/internal/custom-action-renderables.js.map +1 -0
- package/dist/examples/internal/custom-dropdown.d.ts +99 -0
- package/dist/examples/internal/custom-dropdown.d.ts.map +1 -0
- package/dist/examples/internal/custom-dropdown.js +270 -0
- package/dist/examples/internal/custom-dropdown.js.map +1 -0
- package/dist/examples/internal/custom-renderable-form.d.ts +43 -0
- package/dist/examples/internal/custom-renderable-form.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-form.js +284 -0
- package/dist/examples/internal/custom-renderable-form.js.map +1 -0
- package/dist/examples/internal/custom-renderable-list-default-search.d.ts +2 -0
- package/dist/examples/internal/custom-renderable-list-default-search.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-list-default-search.js +16 -0
- package/dist/examples/internal/custom-renderable-list-default-search.js.map +1 -0
- package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts +2 -0
- package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-list-v2-default-search.js +24 -0
- package/dist/examples/internal/custom-renderable-list-v2-default-search.js.map +1 -0
- package/dist/examples/internal/custom-renderable-list-v2.d.ts +189 -0
- package/dist/examples/internal/custom-renderable-list-v2.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-list-v2.js +708 -0
- package/dist/examples/internal/custom-renderable-list-v2.js.map +1 -0
- package/dist/examples/internal/custom-renderable-list.d.ts +72 -0
- package/dist/examples/internal/custom-renderable-list.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-list.js +544 -0
- package/dist/examples/internal/custom-renderable-list.js.map +1 -0
- package/dist/examples/internal/rhf-custom-ref.js +5 -4
- package/dist/examples/internal/rhf-custom-ref.js.map +1 -1
- package/dist/examples/internal/scrollbox-with-descendants.js +4 -2
- package/dist/examples/internal/scrollbox-with-descendants.js.map +1 -1
- package/dist/examples/list-controlled-search.d.ts +2 -0
- package/dist/examples/list-controlled-search.d.ts.map +1 -0
- package/dist/examples/list-controlled-search.js +12 -0
- package/dist/examples/list-controlled-search.js.map +1 -0
- package/dist/examples/list-detail-metadata.js +1 -1
- package/dist/examples/list-detail-metadata.js.map +1 -1
- package/dist/examples/simple-image-mask.d.ts +8 -0
- package/dist/examples/simple-image-mask.d.ts.map +1 -0
- package/dist/examples/simple-image-mask.js +12 -0
- package/dist/examples/simple-image-mask.js.map +1 -0
- package/dist/examples/toast-variations.js +1 -1
- package/dist/examples/toast-variations.js.map +1 -1
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +3 -2
- package/dist/extensions/dev.js.map +1 -1
- package/dist/extensions/react-refresh-init.d.ts.map +1 -1
- package/dist/extensions/react-refresh-init.js +4 -3
- package/dist/extensions/react-refresh-init.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/date-picker-widget.d.ts.map +1 -1
- package/dist/internal/date-picker-widget.js +2 -1
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/dialog.d.ts +6 -0
- package/dist/internal/dialog.d.ts.map +1 -1
- package/dist/internal/dialog.js +59 -18
- package/dist/internal/dialog.js.map +1 -1
- package/dist/internal/navigation.d.ts.map +1 -1
- package/dist/internal/navigation.js +8 -1
- package/dist/internal/navigation.js.map +1 -1
- package/dist/internal/offscreen.d.ts +3 -0
- package/dist/internal/offscreen.d.ts.map +1 -1
- package/dist/internal/offscreen.js +5 -0
- package/dist/internal/offscreen.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +20 -3
- package/dist/internal/providers.js.map +1 -1
- package/dist/internal/scrollbox.d.ts.map +1 -1
- package/dist/internal/scrollbox.js +3 -2
- package/dist/internal/scrollbox.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/preload.js +5 -17
- package/dist/preload.js.map +1 -1
- package/dist/state.d.ts +4 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +4 -0
- package/dist/state.js.map +1 -1
- package/dist/test-border-overlay.d.ts +2 -0
- package/dist/test-border-overlay.d.ts.map +1 -0
- package/dist/test-border-overlay.js +7 -0
- package/dist/test-border-overlay.js.map +1 -0
- package/dist/test-layout-2.d.ts +2 -0
- package/dist/test-layout-2.d.ts.map +1 -0
- package/dist/test-layout-2.js +5 -0
- package/dist/test-layout-2.js.map +1 -0
- package/dist/test-layout.d.ts +2 -0
- package/dist/test-layout.d.ts.map +1 -0
- package/dist/test-layout.js +7 -0
- package/dist/test-layout.js.map +1 -0
- package/dist/theme.d.ts +1 -2
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +5 -9
- package/dist/theme.js.map +1 -1
- package/dist/utils/run-command.d.ts +1 -1
- package/dist/utils/run-command.d.ts.map +1 -1
- package/dist/utils/run-command.js +27 -7
- package/dist/utils/run-command.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +44 -23
- package/dist/utils.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +24 -4
- package/dist/watcher.js.map +1 -1
- package/package.json +14 -12
- package/src/action-utils.tsx +10 -0
- package/src/apis/cache.test.ts +35 -3
- package/src/apis/cache.tsx +184 -59
- package/src/apis/clipboard.tsx +5 -0
- package/src/apis/oauth.tsx +33 -4
- package/src/build.tsx +35 -58
- package/src/cli.tsx +156 -134
- package/src/compile.tsx +6 -3
- package/src/compile.vitest.tsx +33 -15
- package/src/components/actions.tsx +230 -99
- package/src/components/alert.tsx +11 -10
- package/src/components/animation-tick.tsx +1 -1
- package/src/components/detail.tsx +56 -151
- package/src/components/dropdown.tsx +70 -36
- package/src/components/footer.tsx +58 -33
- package/src/components/form/checkbox.tsx +30 -32
- package/src/components/form/date-picker.tsx +27 -47
- package/src/components/form/description.tsx +19 -18
- package/src/components/form/dropdown.tsx +95 -103
- package/src/components/form/file-autocomplete.tsx +9 -8
- package/src/components/form/file-picker.tsx +46 -46
- package/src/components/form/form-end.tsx +6 -4
- package/src/components/form/index.tsx +38 -48
- package/src/components/form/password-field.tsx +25 -27
- package/src/components/form/separator.tsx +3 -2
- package/src/components/form/tagpicker.tsx +2 -1
- package/src/components/form/text-area.tsx +25 -30
- package/src/components/form/text-field.tsx +25 -27
- package/src/components/form/use-form-navigation.tsx +4 -5
- package/src/components/form/with-left-border.tsx +48 -10
- package/src/components/icon.tsx +69 -0
- package/src/components/image.tsx +60 -7
- package/src/components/list.tsx +270 -202
- package/src/components/loading-bar.tsx +4 -3
- package/src/components/metadata.tsx +217 -0
- package/src/components/theme-picker.tsx +3 -2
- package/src/examples/actions-context.tsx +63 -0
- package/src/examples/actions-context.vitest.tsx +110 -0
- package/src/examples/actions-dialog-layout.vitest.tsx +2 -1
- package/src/examples/file-autocomplete.vitest.tsx +15 -15
- package/src/examples/form-basic.tsx +12 -0
- package/src/examples/form-basic.vitest.tsx +74 -74
- package/src/examples/form-dropdown.tsx +8 -0
- package/src/examples/form-dropdown.vitest.tsx +364 -421
- package/src/examples/form-tagpicker.vitest.tsx +56 -54
- package/src/examples/github.vitest.tsx +252 -0
- package/src/examples/internal/rhf-custom-ref.tsx +16 -15
- package/src/examples/internal/scrollbox-with-descendants.tsx +4 -2
- package/src/examples/internal/simple-dialog.tsx +1 -1
- package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -9
- package/src/examples/list-controlled-search.tsx +28 -0
- package/src/examples/list-controlled-search.vitest.tsx +49 -0
- package/src/examples/list-detail-metadata.tsx +8 -5
- package/src/examples/list-detail-metadata.vitest.tsx +22 -22
- package/src/examples/list-dropdown-default.vitest.tsx +12 -12
- package/src/examples/list-scrollbox.vitest.tsx +52 -38
- package/src/examples/list-with-detail.vitest.tsx +45 -41
- package/src/examples/list-with-dropdown.vitest.tsx +5 -5
- package/src/examples/list-with-sections.vitest.tsx +65 -12
- package/src/examples/list-with-toast.vitest.tsx +4 -4
- package/src/examples/simple-file-picker.vitest.tsx +12 -12
- package/src/examples/simple-grid.vitest.tsx +53 -53
- package/src/examples/simple-image-mask.tsx +58 -0
- package/src/examples/simple-navigation.vitest.tsx +19 -19
- package/src/examples/store.vitest.tsx +1 -1
- package/src/examples/swift-extension.vitest.tsx +4 -2
- package/src/examples/synonyms.vitest.tsx +31 -9
- package/src/examples/toast-action.vitest.tsx +8 -8
- package/src/examples/toast-variations.tsx +1 -1
- package/src/examples/toast-variations.vitest.tsx +69 -134
- package/src/extensions/dev.tsx +3 -2
- package/src/extensions/dev.vitest.tsx +65 -28
- package/src/extensions/react-refresh-init.tsx +4 -3
- package/src/index.tsx +3 -1
- package/src/internal/date-picker-widget.tsx +2 -1
- package/src/internal/dialog.tsx +100 -28
- package/src/internal/navigation.tsx +8 -1
- package/src/internal/offscreen.tsx +10 -0
- package/src/internal/providers.tsx +34 -8
- package/src/internal/scrollbox.tsx +4 -2
- package/src/logger.tsx +4 -0
- package/src/preload.tsx +5 -17
- package/src/state.tsx +12 -0
- package/src/theme.tsx +6 -9
- package/src/utils/run-command.tsx +32 -8
- package/src/utils.tsx +58 -23
- package/src/watcher.tsx +26 -6
|
@@ -3,7 +3,7 @@ import { TextareaRenderable } from '@opentui/core'
|
|
|
3
3
|
import { useForm } from 'react-hook-form'
|
|
4
4
|
import { useKeyboard } from '@opentui/react'
|
|
5
5
|
import { renderWithProviders } from '../../utils'
|
|
6
|
-
import {
|
|
6
|
+
import { useTheme } from 'termcast/src/theme'
|
|
7
7
|
import { logger } from 'termcast/src/logger'
|
|
8
8
|
import { createTextareaFormRef } from 'termcast/src/components/form/form-ref'
|
|
9
9
|
|
|
@@ -13,6 +13,7 @@ interface FormData {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function RHFCustomRefExample() {
|
|
16
|
+
const theme = useTheme()
|
|
16
17
|
const textareaRef1 = useRef<TextareaRenderable>(null)
|
|
17
18
|
const textareaRef2 = useRef<TextareaRenderable>(null)
|
|
18
19
|
const [focusedField, setFocusedField] = useState<'username' | 'message'>('username')
|
|
@@ -73,14 +74,14 @@ function RHFCustomRefExample() {
|
|
|
73
74
|
|
|
74
75
|
return (
|
|
75
76
|
<box flexDirection="column" gap={1}>
|
|
76
|
-
<text fg={
|
|
77
|
-
<text fg={
|
|
77
|
+
<text fg={theme.accent}>React Hook Form with Custom Ref Adapter</text>
|
|
78
|
+
<text fg={theme.textMuted}>
|
|
78
79
|
This example uses register() directly with opentui textarea
|
|
79
80
|
</text>
|
|
80
81
|
|
|
81
82
|
<box flexDirection="column">
|
|
82
83
|
<text
|
|
83
|
-
fg={
|
|
84
|
+
fg={theme.text}
|
|
84
85
|
onMouseDown={() => {
|
|
85
86
|
setFocusedField('username')
|
|
86
87
|
}}
|
|
@@ -99,13 +100,13 @@ function RHFCustomRefExample() {
|
|
|
99
100
|
focused={focusedField === 'username'}
|
|
100
101
|
/>
|
|
101
102
|
{formState.errors.username && (
|
|
102
|
-
<text fg={
|
|
103
|
+
<text fg={theme.error}>{formState.errors.username.message}</text>
|
|
103
104
|
)}
|
|
104
105
|
</box>
|
|
105
106
|
|
|
106
107
|
<box flexDirection="column">
|
|
107
108
|
<text
|
|
108
|
-
fg={
|
|
109
|
+
fg={theme.text}
|
|
109
110
|
onMouseDown={() => {
|
|
110
111
|
setFocusedField('message')
|
|
111
112
|
}}
|
|
@@ -124,28 +125,28 @@ function RHFCustomRefExample() {
|
|
|
124
125
|
focused={focusedField === 'message'}
|
|
125
126
|
/>
|
|
126
127
|
{formState.errors.message && (
|
|
127
|
-
<text fg={
|
|
128
|
+
<text fg={theme.error}>{formState.errors.message.message}</text>
|
|
128
129
|
)}
|
|
129
130
|
</box>
|
|
130
131
|
|
|
131
132
|
<box flexDirection="column" marginTop={1}>
|
|
132
|
-
<text fg={
|
|
133
|
-
<text fg={
|
|
134
|
-
<text fg={
|
|
135
|
-
<text fg={
|
|
136
|
-
<text fg={
|
|
133
|
+
<text fg={theme.textMuted}>Controls:</text>
|
|
134
|
+
<text fg={theme.textMuted}>• Ctrl+S: Submit form</text>
|
|
135
|
+
<text fg={theme.textMuted}>• Ctrl+V: Set values programmatically</text>
|
|
136
|
+
<text fg={theme.textMuted}>• Ctrl+G: Get current values</text>
|
|
137
|
+
<text fg={theme.textMuted}>• ESC: Reset form</text>
|
|
137
138
|
</box>
|
|
138
139
|
|
|
139
140
|
<box marginTop={1}>
|
|
140
|
-
<text fg={
|
|
141
|
+
<text fg={theme.textMuted}>
|
|
141
142
|
isDirty: {String(formState.isDirty)} | isValid:{' '}
|
|
142
143
|
{String(formState.isValid)} | submitCount: {formState.submitCount}
|
|
143
144
|
</text>
|
|
144
145
|
</box>
|
|
145
146
|
|
|
146
147
|
<box marginTop={1} flexDirection="column">
|
|
147
|
-
<text fg={
|
|
148
|
-
<text fg={
|
|
148
|
+
<text fg={theme.accent}>Form values:</text>
|
|
149
|
+
<text fg={theme.text}>{JSON.stringify(formValues, null, 2)}</text>
|
|
149
150
|
</box>
|
|
150
151
|
</box>
|
|
151
152
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import { useKeyboard } from '@opentui/react'
|
|
2
|
+
import { useKeyboard, flushSync } from '@opentui/react'
|
|
3
3
|
import { createDescendants } from 'termcast/src/descendants'
|
|
4
4
|
import { renderWithProviders } from 'termcast/src/utils'
|
|
5
5
|
|
|
@@ -45,7 +45,9 @@ function ScrollboxWithDescendants() {
|
|
|
45
45
|
|
|
46
46
|
const nextItem = items[nextIndex]
|
|
47
47
|
if (nextItem) {
|
|
48
|
-
|
|
48
|
+
flushSync(() => {
|
|
49
|
+
setSelectedIndex(nextIndex)
|
|
50
|
+
})
|
|
49
51
|
scrollToItem(nextItem)
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
useDialog,
|
|
6
6
|
type DialogPosition,
|
|
7
7
|
} from 'termcast/src/internal/dialog'
|
|
8
|
-
import {
|
|
8
|
+
import { useTheme } from 'termcast'
|
|
9
9
|
import { List } from 'termcast'
|
|
10
10
|
import { ActionPanel, Action } from 'termcast'
|
|
11
11
|
import { Dropdown } from 'termcast'
|
|
@@ -7,10 +7,15 @@ test('simple scrollbox navigation and scrolling', async () => {
|
|
|
7
7
|
args: ['src/examples/internal/simple-scrollbox.tsx'],
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
+
// Wait for initial render with Item 1 visible
|
|
10
11
|
await session.text({
|
|
11
|
-
waitFor: (text) =>
|
|
12
|
+
waitFor: (text) =>
|
|
13
|
+
text.includes('Simple ScrollBox Demo') && text.includes('Item 1'),
|
|
12
14
|
})
|
|
13
15
|
|
|
16
|
+
// Wait for render to stabilize
|
|
17
|
+
await session.waitIdle()
|
|
18
|
+
|
|
14
19
|
const initialText = await session.text()
|
|
15
20
|
expect(initialText).toMatchInlineSnapshot(`
|
|
16
21
|
"
|
|
@@ -40,13 +45,13 @@ test('simple scrollbox navigation and scrolling', async () => {
|
|
|
40
45
|
"
|
|
41
46
|
`)
|
|
42
47
|
|
|
43
|
-
// Scroll down to see more items
|
|
44
|
-
await session.scrollDown(
|
|
48
|
+
// Scroll down to see more items - use more scroll events for reliability
|
|
49
|
+
await session.scrollDown(5)
|
|
45
50
|
|
|
46
51
|
// Wait for Item 4 to appear (proves scroll happened)
|
|
47
52
|
const afterScrollDownSnapshot = await session.text({
|
|
48
53
|
waitFor: (text) => text.includes('Item 4'),
|
|
49
|
-
timeout:
|
|
54
|
+
timeout: 10000,
|
|
50
55
|
})
|
|
51
56
|
expect(afterScrollDownSnapshot).toMatchInlineSnapshot(`
|
|
52
57
|
"
|
|
@@ -76,13 +81,13 @@ test('simple scrollbox navigation and scrolling', async () => {
|
|
|
76
81
|
"
|
|
77
82
|
`)
|
|
78
83
|
|
|
79
|
-
// Scroll back up
|
|
80
|
-
await session.scrollUp(
|
|
84
|
+
// Scroll back up - use more scroll events for reliability
|
|
85
|
+
await session.scrollUp(5)
|
|
81
86
|
|
|
82
|
-
// Wait for
|
|
87
|
+
// Wait for scroll to take effect - Item 4 should no longer be visible
|
|
83
88
|
const afterScrollUpSnapshot = await session.text({
|
|
84
|
-
waitFor: (text) => text
|
|
85
|
-
timeout:
|
|
89
|
+
waitFor: (text) => !text.includes('Item 4'),
|
|
90
|
+
timeout: 10000,
|
|
86
91
|
})
|
|
87
92
|
expect(afterScrollUpSnapshot).toMatchInlineSnapshot(`
|
|
88
93
|
"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Example: List with controlled searchText (parent manages search state)
|
|
2
|
+
// Used to demonstrate that selection must reset to first visible item
|
|
3
|
+
// when search text changes, even in controlled mode.
|
|
4
|
+
import React, { useState } from 'react'
|
|
5
|
+
import { List, renderWithProviders } from 'termcast'
|
|
6
|
+
|
|
7
|
+
function ControlledSearchExample() {
|
|
8
|
+
const [searchText, setSearchText] = useState('')
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<List
|
|
12
|
+
navigationTitle='Controlled Search'
|
|
13
|
+
searchBarPlaceholder='Search items...'
|
|
14
|
+
filtering={true}
|
|
15
|
+
searchText={searchText}
|
|
16
|
+
onSearchTextChange={setSearchText}
|
|
17
|
+
>
|
|
18
|
+
<List.Item id='apple' title='Apple' subtitle='Red fruit' />
|
|
19
|
+
<List.Item id='banana' title='Banana' subtitle='Yellow fruit' />
|
|
20
|
+
<List.Item id='cherry' title='Cherry' subtitle='Small fruit' />
|
|
21
|
+
<List.Item id='grape' title='Grape' subtitle='Purple fruit' />
|
|
22
|
+
<List.Item id='lettuce' title='Lettuce' subtitle='Green veggie' />
|
|
23
|
+
<List.Item id='mango' title='Mango' subtitle='Tropical fruit' />
|
|
24
|
+
</List>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await renderWithProviders(<ControlledSearchExample />)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Repro: controlled searchText + filtering must reset selection to first visible item.
|
|
2
|
+
// Bug: when parent manages searchText, List never calls setSelectedIndex on search change,
|
|
3
|
+
// so the › marker stays on a stale (now-hidden) item or disappears entirely.
|
|
4
|
+
import { test, expect, afterEach, beforeEach } from 'vitest'
|
|
5
|
+
import { launchTerminal, Session } from 'tuistory/src'
|
|
6
|
+
|
|
7
|
+
let session: Session
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
session = await launchTerminal({
|
|
11
|
+
command: 'bun',
|
|
12
|
+
args: ['src/examples/list-controlled-search.tsx'],
|
|
13
|
+
cols: 60,
|
|
14
|
+
rows: 16,
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
session?.close()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('controlled search resets selection to first visible item', async () => {
|
|
23
|
+
await session.text({
|
|
24
|
+
waitFor: (text) => {
|
|
25
|
+
return /search items/i.test(text) && text.includes('Apple')
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Navigate down to select Grape (4th item)
|
|
30
|
+
await session.press('down')
|
|
31
|
+
await session.press('down')
|
|
32
|
+
await session.press('down')
|
|
33
|
+
|
|
34
|
+
const beforeSearch = await session.text()
|
|
35
|
+
expect(beforeSearch).toContain('›Grape')
|
|
36
|
+
|
|
37
|
+
// Type "let" — only Lettuce should match.
|
|
38
|
+
// Selection MUST move to Lettuce (first visible item).
|
|
39
|
+
await session.type('let')
|
|
40
|
+
|
|
41
|
+
const afterSearch = await session.text({
|
|
42
|
+
waitFor: (text) => {
|
|
43
|
+
return text.includes('let') && text.includes('Lettuce')
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// BUG REPRO: without the fix, › is missing or on a stale item
|
|
48
|
+
expect(afterSearch).toContain('›Lettuce')
|
|
49
|
+
}, 10000)
|
|
@@ -11,7 +11,7 @@ const ListDetailMetadataExample = () => {
|
|
|
11
11
|
>
|
|
12
12
|
<List.Item
|
|
13
13
|
id="item1"
|
|
14
|
-
title="
|
|
14
|
+
title="Short Values"
|
|
15
15
|
detail={
|
|
16
16
|
<List.Item.Detail
|
|
17
17
|
markdown="# Details"
|
|
@@ -29,14 +29,17 @@ const ListDetailMetadataExample = () => {
|
|
|
29
29
|
/>
|
|
30
30
|
<List.Item
|
|
31
31
|
id="item2"
|
|
32
|
-
title="
|
|
32
|
+
title="Long Values"
|
|
33
33
|
detail={
|
|
34
34
|
<List.Item.Detail
|
|
35
|
-
markdown="# Info"
|
|
35
|
+
markdown="# Info with Long Values"
|
|
36
36
|
metadata={
|
|
37
37
|
<List.Item.Detail.Metadata>
|
|
38
|
-
<List.Item.Detail.Metadata.Label title="
|
|
39
|
-
<List.Item.Detail.Metadata.Label title="
|
|
38
|
+
<List.Item.Detail.Metadata.Label title="Description" text="This is a very long description that would be truncated if shown inline" />
|
|
39
|
+
<List.Item.Detail.Metadata.Label title="Path" text="/Users/username/Documents/Projects/my-project/src/components" />
|
|
40
|
+
<List.Item.Detail.Metadata.Separator />
|
|
41
|
+
<List.Item.Detail.Metadata.Label title="Short" text="OK" />
|
|
42
|
+
<List.Item.Detail.Metadata.Link title="URL" target="https://example.com/very/long/path/that/exceeds/limit" text="example.com/very/long/path" />
|
|
40
43
|
</List.Item.Detail.Metadata>
|
|
41
44
|
}
|
|
42
45
|
/>
|
|
@@ -16,7 +16,7 @@ afterEach(() => {
|
|
|
16
16
|
session?.close()
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
test('list detail metadata label renders
|
|
19
|
+
test('list detail metadata label renders short values in row layout (key: value)', async () => {
|
|
20
20
|
const snapshot = await session.text({
|
|
21
21
|
waitFor: (text) => {
|
|
22
22
|
return (
|
|
@@ -36,24 +36,24 @@ test('list detail metadata label renders title and text in column layout', async
|
|
|
36
36
|
|
|
37
37
|
> Search...
|
|
38
38
|
|
|
39
|
-
›
|
|
40
|
-
|
|
41
|
-
│ ───────────────────────────────── █
|
|
42
|
-
│ ▀
|
|
43
|
-
│ Name:
|
|
44
|
-
│ John Doe
|
|
39
|
+
›Short Values
|
|
40
|
+
Long Values │
|
|
45
41
|
│
|
|
46
|
-
│
|
|
47
|
-
│
|
|
42
|
+
│
|
|
43
|
+
│ Name: John Doe
|
|
44
|
+
│
|
|
45
|
+
│ Email: john@example.com
|
|
48
46
|
│ ─────────────────
|
|
49
47
|
│
|
|
50
|
-
|
|
48
|
+
│ Status: Active
|
|
49
|
+
│ Website: example.com
|
|
50
|
+
↑↓ navigate ^k actions │
|
|
51
51
|
|
|
52
52
|
"
|
|
53
53
|
`)
|
|
54
54
|
}, 10000)
|
|
55
55
|
|
|
56
|
-
test('list detail metadata
|
|
56
|
+
test('list detail metadata renders long values in column layout (key on one line, value below)', async () => {
|
|
57
57
|
await session.text({
|
|
58
58
|
waitFor: (text) => text.includes('Metadata Test'),
|
|
59
59
|
})
|
|
@@ -62,7 +62,7 @@ test('list detail metadata navigation shows different metadata', async () => {
|
|
|
62
62
|
|
|
63
63
|
const snapshot = await session.text({
|
|
64
64
|
waitFor: (text) => {
|
|
65
|
-
return text.includes('
|
|
65
|
+
return text.includes('Description') && text.includes('very long')
|
|
66
66
|
},
|
|
67
67
|
})
|
|
68
68
|
|
|
@@ -74,18 +74,18 @@ test('list detail metadata navigation shows different metadata', async () => {
|
|
|
74
74
|
|
|
75
75
|
> Search...
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
›
|
|
79
|
-
│
|
|
77
|
+
Short Values
|
|
78
|
+
›Long Values │ Info with Long Values ▲
|
|
79
|
+
│ █
|
|
80
80
|
│
|
|
81
|
-
│
|
|
82
|
-
│
|
|
81
|
+
│ Description:
|
|
82
|
+
│ This is a very long description
|
|
83
|
+
│ that would be truncated if shown
|
|
84
|
+
│ inline
|
|
83
85
|
│
|
|
84
|
-
│
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
│ Path:
|
|
87
|
+
│ /Users/username/Documents/
|
|
88
|
+
↑↓ navigate ^k actions │ Projects/my-project/src/components▼
|
|
89
89
|
|
|
90
90
|
"
|
|
91
91
|
`)
|
|
@@ -43,9 +43,9 @@ test('dropdown defaults to first item when no value is provided', async () => {
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
↵ show selected fruit ↑↓ navigate ^k actions powered by termcast
|
|
47
|
-
|
|
48
46
|
|
|
47
|
+
↵ show selected ↑↓ navigate ^p dropdown ^k actionpowered by termcast
|
|
48
|
+
fruit
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
|
|
@@ -77,6 +77,7 @@ test('dropdown opens and shows items', async () => {
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
Dropdown Default Value Example ───────────────────────────────────────────
|
|
80
|
+
|
|
80
81
|
╭──────────────────────────────────────────────────────────────────────────╮
|
|
81
82
|
│ │
|
|
82
83
|
│ Filter by category esc │
|
|
@@ -88,7 +89,7 @@ test('dropdown opens and shows items', async () => {
|
|
|
88
89
|
│ Orange │
|
|
89
90
|
│ Grape │
|
|
90
91
|
│ │
|
|
91
|
-
│ │
|
|
92
|
+
│ │t
|
|
92
93
|
│ ↵ select ↑↓ navigate powered by termcast │
|
|
93
94
|
│ │
|
|
94
95
|
╰──────────────────────────────────────────────────────────────────────────╯
|
|
@@ -102,7 +103,6 @@ test('dropdown opens and shows items', async () => {
|
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
|
|
105
|
-
|
|
106
106
|
"
|
|
107
107
|
`)
|
|
108
108
|
|
|
@@ -114,6 +114,7 @@ test('dropdown opens and shows items', async () => {
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
Dropdown Default Value Example ───────────────────────────────────────────
|
|
117
|
+
|
|
117
118
|
╭──────────────────────────────────────────────────────────────────────────╮
|
|
118
119
|
│ │
|
|
119
120
|
│ Filter by category esc │
|
|
@@ -125,7 +126,7 @@ test('dropdown opens and shows items', async () => {
|
|
|
125
126
|
│ Orange │
|
|
126
127
|
│ Grape │
|
|
127
128
|
│ │
|
|
128
|
-
│ │
|
|
129
|
+
│ │t
|
|
129
130
|
│ ↵ select ↑↓ navigate powered by termcast │
|
|
130
131
|
│ │
|
|
131
132
|
╰──────────────────────────────────────────────────────────────────────────╯
|
|
@@ -139,7 +140,6 @@ test('dropdown opens and shows items', async () => {
|
|
|
139
140
|
|
|
140
141
|
|
|
141
142
|
|
|
142
|
-
|
|
143
143
|
"
|
|
144
144
|
`)
|
|
145
145
|
|
|
@@ -162,9 +162,9 @@ test('dropdown opens and shows items', async () => {
|
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
|
|
165
|
-
↵ show selected fruit ↑↓ navigate ^k actions powered by termcast
|
|
166
|
-
|
|
167
165
|
|
|
166
|
+
↵ show selected ↑↓ navigate ^p dropdown ^k actionpowered by termcast
|
|
167
|
+
fruit
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
|
|
@@ -196,6 +196,7 @@ test('clicking dropdown opens it', async () => {
|
|
|
196
196
|
|
|
197
197
|
|
|
198
198
|
Dropdown Default Value Example ───────────────────────────────────────────
|
|
199
|
+
|
|
199
200
|
╭──────────────────────────────────────────────────────────────────────────╮
|
|
200
201
|
│ │
|
|
201
202
|
│ Filter by category esc │
|
|
@@ -207,7 +208,7 @@ test('clicking dropdown opens it', async () => {
|
|
|
207
208
|
│ Orange │
|
|
208
209
|
│ Grape │
|
|
209
210
|
│ │
|
|
210
|
-
│ │
|
|
211
|
+
│ │t
|
|
211
212
|
│ ↵ select ↑↓ navigate powered by termcast │
|
|
212
213
|
│ │
|
|
213
214
|
╰──────────────────────────────────────────────────────────────────────────╯
|
|
@@ -221,7 +222,6 @@ test('clicking dropdown opens it', async () => {
|
|
|
221
222
|
|
|
222
223
|
|
|
223
224
|
|
|
224
|
-
|
|
225
225
|
"
|
|
226
226
|
`)
|
|
227
227
|
|
|
@@ -244,9 +244,9 @@ test('clicking dropdown opens it', async () => {
|
|
|
244
244
|
|
|
245
245
|
|
|
246
246
|
|
|
247
|
-
↵ show selected fruit ↑↓ navigate ^k actions powered by termcast
|
|
248
|
-
|
|
249
247
|
|
|
248
|
+
↵ show selected ↑↓ navigate ^p dropdown ^k actionpowered by termcast
|
|
249
|
+
fruit
|
|
250
250
|
|
|
251
251
|
|
|
252
252
|
|
|
@@ -2,8 +2,10 @@ import { test, expect, afterEach, beforeEach } from 'vitest'
|
|
|
2
2
|
import { launchTerminal, Session } from 'tuistory/src'
|
|
3
3
|
|
|
4
4
|
let session: Session
|
|
5
|
+
let sessionStartMs = 0
|
|
5
6
|
|
|
6
7
|
beforeEach(async () => {
|
|
8
|
+
sessionStartMs = Date.now()
|
|
7
9
|
session = await launchTerminal({
|
|
8
10
|
command: 'bun',
|
|
9
11
|
args: ['src/examples/list-scrollbox.tsx'],
|
|
@@ -16,6 +18,18 @@ afterEach(() => {
|
|
|
16
18
|
session?.close()
|
|
17
19
|
})
|
|
18
20
|
|
|
21
|
+
test('list scrollbox startup appears in under one second', async () => {
|
|
22
|
+
await session.text({
|
|
23
|
+
waitFor: (text) => {
|
|
24
|
+
return /Search items\.\.\./i.test(text)
|
|
25
|
+
},
|
|
26
|
+
timeout: 5000,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const startupMs = Date.now() - sessionStartMs
|
|
30
|
+
expect(startupMs).toBeLessThan(1000)
|
|
31
|
+
}, 10000)
|
|
32
|
+
|
|
19
33
|
test('list scrollbox auto-scrolls when navigating down', async () => {
|
|
20
34
|
await session.text({
|
|
21
35
|
waitFor: (text) => {
|
|
@@ -33,11 +47,11 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
|
|
|
33
47
|
> Search items...
|
|
34
48
|
|
|
35
49
|
›▲ Item 1 Description for item 1 ▲
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
■ Item 2 Description for item 2 ▀
|
|
51
|
+
▲ Item 3 Description for item 3
|
|
52
|
+
■ Item 4 Description for item 4
|
|
53
|
+
▼ Item 5 Description for item 5
|
|
54
|
+
● Item 6 Description for item 6 ▼"
|
|
41
55
|
`)
|
|
42
56
|
|
|
43
57
|
await session.press('down')
|
|
@@ -55,12 +69,12 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
|
|
|
55
69
|
|
|
56
70
|
> Search items...
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
▲ Item 3 Description for item 3 ▲
|
|
73
|
+
■ Item 4 Description for item 4 ▄
|
|
74
|
+
▼ Item 5 Description for item 5
|
|
75
|
+
›● Item 6 Description for item 6
|
|
76
|
+
■ Item 7 Description for item 7
|
|
77
|
+
■ Item 8 Description for item 8 ▼"
|
|
64
78
|
`)
|
|
65
79
|
|
|
66
80
|
await session.press('down')
|
|
@@ -76,12 +90,12 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
|
|
|
76
90
|
|
|
77
91
|
> Search items...
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
● Item 6 Description for item 6 ▲
|
|
94
|
+
■ Item 7 Description for item 7
|
|
95
|
+
■ Item 8 Description for item 8 ▄
|
|
96
|
+
›■ Item 9 Description for item 9
|
|
97
|
+
Item 10 Description for item 10
|
|
98
|
+
▲ Item 11 Description for item 11 ▼"
|
|
85
99
|
`)
|
|
86
100
|
|
|
87
101
|
await session.press('up')
|
|
@@ -101,12 +115,12 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
|
|
|
101
115
|
|
|
102
116
|
> Search items...
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
▲ Item 1 Description for item 1 ▲
|
|
119
|
+
›■ Item 2 Description for item 2 ▀
|
|
120
|
+
▲ Item 3 Description for item 3
|
|
121
|
+
■ Item 4 Description for item 4
|
|
122
|
+
▼ Item 5 Description for item 5
|
|
123
|
+
● Item 6 Description for item 6 ▼"
|
|
110
124
|
`)
|
|
111
125
|
}, 15000)
|
|
112
126
|
|
|
@@ -127,11 +141,11 @@ test('list scrollbox scrolls with mouse wheel', async () => {
|
|
|
127
141
|
> Search items...
|
|
128
142
|
|
|
129
143
|
›▲ Item 1 Description for item 1 ▲
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
■ Item 2 Description for item 2 ▀
|
|
145
|
+
▲ Item 3 Description for item 3
|
|
146
|
+
■ Item 4 Description for item 4
|
|
147
|
+
▼ Item 5 Description for item 5
|
|
148
|
+
● Item 6 Description for item 6 ▼"
|
|
135
149
|
`)
|
|
136
150
|
|
|
137
151
|
await session.scrollDown(3)
|
|
@@ -150,11 +164,11 @@ test('list scrollbox scrolls with mouse wheel', async () => {
|
|
|
150
164
|
> Search items...
|
|
151
165
|
|
|
152
166
|
■ Item 2 Description for item 2 ▲
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
▲ Item 3 Description for item 3 ▄
|
|
168
|
+
■ Item 4 Description for item 4
|
|
169
|
+
▼ Item 5 Description for item 5
|
|
170
|
+
● Item 6 Description for item 6
|
|
171
|
+
■ Item 7 Description for item 7 ▼"
|
|
158
172
|
`)
|
|
159
173
|
|
|
160
174
|
await session.scrollUp(2)
|
|
@@ -173,10 +187,10 @@ test('list scrollbox scrolls with mouse wheel', async () => {
|
|
|
173
187
|
> Search items...
|
|
174
188
|
|
|
175
189
|
›▲ Item 1 Description for item 1 ▲
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
■ Item 2 Description for item 2 ▀
|
|
191
|
+
▲ Item 3 Description for item 3
|
|
192
|
+
■ Item 4 Description for item 4
|
|
193
|
+
▼ Item 5 Description for item 5
|
|
194
|
+
● Item 6 Description for item 6 ▼"
|
|
181
195
|
`)
|
|
182
196
|
}, 15000)
|