nexa-ui-kit 0.6.2 → 0.7.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.
@@ -0,0 +1,136 @@
1
+ <script setup>
2
+ import { signal, computed } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ onRefresh: { type: Function, default: null },
6
+ onEndReached: { type: Function, default: null },
7
+ endReachedThreshold: { type: Number, default: 80 },
8
+ horizontal: { type: Boolean, default: false }
9
+ })
10
+
11
+ const isRefreshing = signal(false)
12
+ const pullDelta = signal(0)
13
+ const refreshThreshold = 60
14
+ let touchStartY = 0
15
+ let startScrollTop = 0
16
+
17
+ const pullProgress = computed(() =>
18
+ Math.min(1, pullDelta.value / refreshThreshold)
19
+ )
20
+
21
+ const spinnerScale = computed(() =>
22
+ `scale(${pullProgress.value})`
23
+ )
24
+
25
+ const onTouchStart = (e) => {
26
+ const container = e.currentTarget
27
+ startScrollTop = container.scrollTop
28
+ touchStartY = e.touches[0].clientY
29
+ }
30
+
31
+ const onTouchMove = (e) => {
32
+ if (isRefreshing.value || !props.onRefresh) return
33
+ const container = e.currentTarget
34
+ const dy = e.touches[0].clientY - touchStartY
35
+ if (container.scrollTop === 0 && dy > 0 && startScrollTop === 0) {
36
+ pullDelta.value = Math.min(dy * 0.5, refreshThreshold * 1.5)
37
+ }
38
+ }
39
+
40
+ const onTouchEnd = async () => {
41
+ if (!props.onRefresh || pullDelta.value < refreshThreshold) {
42
+ pullDelta.value = 0
43
+ return
44
+ }
45
+ isRefreshing.value = true
46
+ pullDelta.value = 0
47
+ try {
48
+ await props.onRefresh()
49
+ } finally {
50
+ isRefreshing.value = false
51
+ }
52
+ }
53
+
54
+ const onScroll = (e) => {
55
+ if (!props.onEndReached) return
56
+ const el = e.target
57
+ const distanceFromBottom = props.horizontal
58
+ ? el.scrollWidth - el.scrollLeft - el.clientWidth
59
+ : el.scrollHeight - el.scrollTop - el.clientHeight
60
+ if (distanceFromBottom <= props.endReachedThreshold) {
61
+ props.onEndReached()
62
+ }
63
+ }
64
+ </script>
65
+
66
+ <template>
67
+ <div
68
+ class="n-scroll-view"
69
+ :class="{ 'n-scroll-view--horizontal': horizontal }"
70
+ @scroll="onScroll"
71
+ @touchstart="onTouchStart"
72
+ @touchmove="onTouchMove"
73
+ @touchend="onTouchEnd"
74
+ >
75
+ <div
76
+ v-if="onRefresh"
77
+ class="n-scroll-view-ptr"
78
+ :class="{ 'is-refreshing': isRefreshing.value }"
79
+ :style="{ height: pullDelta.value + 'px' }"
80
+ >
81
+ <div
82
+ class="n-scroll-view-spinner"
83
+ :style="{ transform: isRefreshing.value ? 'scale(1)' : spinnerScale.value }"
84
+ >
85
+
86
+ </div>
87
+ </div>
88
+ <slot />
89
+ </div>
90
+ </template>
91
+
92
+ <style scoped>
93
+ .n-scroll-view {
94
+ overflow-y: auto;
95
+ overflow-x: hidden;
96
+ width: 100%;
97
+ height: 100%;
98
+ -webkit-overflow-scrolling: touch;
99
+ overscroll-behavior: contain;
100
+ position: relative;
101
+ }
102
+
103
+ .n-scroll-view--horizontal {
104
+ overflow-y: hidden;
105
+ overflow-x: auto;
106
+ display: flex;
107
+ }
108
+
109
+ .n-scroll-view-ptr {
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ overflow: hidden;
114
+ transition: height 0.1s;
115
+ }
116
+
117
+ .n-scroll-view-ptr.is-refreshing {
118
+ height: 52px !important;
119
+ }
120
+
121
+ .n-scroll-view-spinner {
122
+ font-size: 24px;
123
+ color: var(--n-color-primary, #6366f1);
124
+ transition: transform 0.1s;
125
+ line-height: 1;
126
+ transform-origin: center;
127
+ }
128
+
129
+ .n-scroll-view-ptr.is-refreshing .n-scroll-view-spinner {
130
+ animation: n-spin 0.8s linear infinite;
131
+ }
132
+
133
+ @keyframes n-spin {
134
+ to { transform: rotate(360deg) scale(1); }
135
+ }
136
+ </style>
@@ -0,0 +1,94 @@
1
+ <script setup>
2
+ import { signal, computed, onMounted, onUnmounted, effect } from 'nexa-framework'
3
+
4
+ const props = defineProps({
5
+ items: { type: Array, default: () => [] },
6
+ itemHeight: { type: Number, default: 48 },
7
+ overscan: { type: Number, default: 3 },
8
+ renderItem: { type: Function, required: true },
9
+ keyFn: { type: Function, default: null }
10
+ })
11
+
12
+ const containerRef = signal(null)
13
+ const scrollTop = signal(0)
14
+ const containerHeight = signal(400)
15
+
16
+ const totalHeight = computed(() => props.items.length * props.itemHeight)
17
+
18
+ const visibleRange = computed(() => {
19
+ const start = Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.overscan)
20
+ const visibleCount = Math.ceil(containerHeight.value / props.itemHeight) + props.overscan * 2
21
+ const end = Math.min(props.items.length, start + visibleCount)
22
+ return { start, end }
23
+ })
24
+
25
+ const visibleItems = computed(() => {
26
+ const { start, end } = visibleRange.value
27
+ return props.items.slice(start, end).map((item, i) => ({
28
+ item,
29
+ index: start + i,
30
+ top: (start + i) * props.itemHeight
31
+ }))
32
+ })
33
+
34
+ let resizeObserver = null
35
+
36
+ onMounted(() => {
37
+ const el = containerRef.value
38
+ if (!el) return
39
+ containerHeight.value = el.clientHeight
40
+
41
+ resizeObserver = new ResizeObserver(entries => {
42
+ containerHeight.value = entries[0].contentRect.height
43
+ })
44
+ resizeObserver.observe(el)
45
+ })
46
+
47
+ onUnmounted(() => {
48
+ resizeObserver?.disconnect()
49
+ })
50
+
51
+ const onScroll = (e) => {
52
+ scrollTop.value = e.target.scrollTop
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <div
58
+ class="n-virtual-list"
59
+ @scroll="onScroll"
60
+ :ref="el => containerRef.value = el"
61
+ >
62
+ <div
63
+ class="n-virtual-list-spacer"
64
+ :style="{ height: totalHeight.value + 'px', position: 'relative' }"
65
+ >
66
+ <div
67
+ v-for="row in visibleItems.value"
68
+ :key="keyFn ? keyFn(row.item, row.index) : row.index"
69
+ class="n-virtual-list-row"
70
+ :style="{ position: 'absolute', top: row.top + 'px', width: '100%', height: itemHeight + 'px' }"
71
+ >
72
+ {{ renderItem(row.item, row.index) }}
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </template>
77
+
78
+ <style scoped>
79
+ .n-virtual-list {
80
+ overflow-y: auto;
81
+ width: 100%;
82
+ height: 100%;
83
+ -webkit-overflow-scrolling: touch;
84
+ overscroll-behavior: contain;
85
+ }
86
+
87
+ .n-virtual-list-spacer {
88
+ position: relative;
89
+ }
90
+
91
+ .n-virtual-list-row {
92
+ box-sizing: border-box;
93
+ }
94
+ </style>
package/src/index.ts CHANGED
@@ -30,6 +30,9 @@ export { default as NInputNumber } from './components/NInputNumber.nexa'
30
30
  export { default as NMultiSelect } from './components/NMultiSelect.nexa'
31
31
  export { default as NDataTable } from './components/NDataTable.nexa'
32
32
  export { default as NPaginator } from './components/NPaginator.nexa'
33
+ export { default as NVirtualList } from './components/NVirtualList.nexa'
34
+ export { default as NScrollView } from './components/NScrollView.nexa'
35
+ export { default as NImage } from './components/NImage.nexa'
33
36
  export * from './services/ToastService.js'
34
37
  export * from './services/FormValidation.js'
35
38
  export { installTheme } from './styles/theme.js'