@asd20/ui-next 2.0.1 → 2.0.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.3](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.2...ui-next-v2.0.3) (2026-03-27)
4
+
5
+ ## [2.0.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.1...ui-next-v2.0.2) (2026-03-27)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * replace basicscroll scroll-track runtime ([c0fe361](https://github.com/academydistrict20/asd20-ui-next/commit/c0fe361c301bbf1d132eac7876c5e02cabe4a5b0))
11
+
3
12
  ## [2.0.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.0...ui-next-v2.0.1) (2026-03-27)
4
13
 
5
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -34,7 +34,6 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "axios": "^1.13.6",
37
- "basicscroll": "^3.0.2",
38
37
  "countup.js": "^2.0.0",
39
38
  "date-fns": "^2.30.0",
40
39
  "date-fns-tz": "^1.1.7",
@@ -1,32 +1,190 @@
1
- import * as basicScroll from 'basicscroll'
1
+ const DEFAULT_VARIABLE_NAME = 'scroll-progress'
2
2
 
3
- function mountScrollTrack(el, { modifiers = {}, expression } = {}) {
3
+ function clamp(value, min, max) {
4
+ return Math.min(Math.max(value, min), max)
5
+ }
6
+
7
+ function getScrollTop() {
8
+ const scrollingElement =
9
+ document.scrollingElement || document.documentElement || document.body
10
+
11
+ return scrollingElement ? scrollingElement.scrollTop : 0
12
+ }
13
+
14
+ function getViewportHeight() {
15
+ return (
16
+ window.innerHeight ||
17
+ window.outerHeight ||
18
+ document.documentElement?.clientHeight ||
19
+ 1
20
+ )
21
+ }
22
+
23
+ function parseAbsoluteValue(value) {
24
+ if (typeof value === 'number' && Number.isFinite(value)) {
25
+ return value
26
+ }
27
+
28
+ if (typeof value !== 'string') {
29
+ return null
30
+ }
31
+
32
+ const parsedValue = Number.parseFloat(value)
33
+
34
+ return Number.isFinite(parsedValue) ? parsedValue : null
35
+ }
36
+
37
+ function resolveKeywordValue(value, el, scrollTop, viewportHeight) {
38
+ if (!el || typeof value !== 'string') {
39
+ return null
40
+ }
41
+
42
+ const match = value.match(/^(top|middle|bottom)-(top|middle|bottom)$/)
43
+
44
+ if (!match) {
45
+ return null
46
+ }
47
+
48
+ const [, elementEdge, viewportEdge] = match
49
+ const rect = el.getBoundingClientRect()
50
+ let absoluteOffset = scrollTop + rect.top
51
+
52
+ if (elementEdge === 'middle') {
53
+ absoluteOffset += rect.height / 2
54
+ } else if (elementEdge === 'bottom') {
55
+ absoluteOffset += rect.height
56
+ }
57
+
58
+ if (viewportEdge === 'middle') {
59
+ absoluteOffset -= viewportHeight / 2
60
+ } else if (viewportEdge === 'bottom') {
61
+ absoluteOffset -= viewportHeight
62
+ }
63
+
64
+ return absoluteOffset
65
+ }
66
+
67
+ function resolveBoundary(value, el, scrollTop, viewportHeight) {
68
+ const absoluteValue = parseAbsoluteValue(value)
69
+
70
+ if (absoluteValue !== null) {
71
+ return absoluteValue
72
+ }
73
+
74
+ return resolveKeywordValue(value, el, scrollTop, viewportHeight)
75
+ }
76
+
77
+ function getBindingConfig(el, { modifiers = {}, expression } = {}) {
78
+ const viewportHeight = getViewportHeight()
79
+ const scrollTop = getScrollTop()
80
+ const from = modifiers.window
81
+ ? 0
82
+ : resolveBoundary('middle-top', el, scrollTop, viewportHeight)
83
+ const to = modifiers.window
84
+ ? window.outerHeight || viewportHeight
85
+ : resolveBoundary('middle-bottom', el, scrollTop, viewportHeight)
86
+
87
+ return {
88
+ cssVariable: `--${expression || DEFAULT_VARIABLE_NAME}`,
89
+ from,
90
+ to,
91
+ }
92
+ }
93
+
94
+ function setProgress(el, cssVariable, progress) {
95
+ const normalizedProgress = Number.isFinite(progress)
96
+ ? Math.round(progress * 10000) / 10000
97
+ : 0
98
+
99
+ el.style.setProperty(cssVariable, String(normalizedProgress))
100
+ }
101
+
102
+ function updateScrollTrack(el) {
103
+ if (typeof window === 'undefined' || !el.__scrollTrackState) {
104
+ return
105
+ }
106
+
107
+ const scrollTop = getScrollTop()
108
+ const { cssVariable, from, to } = getBindingConfig(
109
+ el,
110
+ el.__scrollTrackState.binding
111
+ )
112
+
113
+ if (from === null || to === null) {
114
+ setProgress(el, cssVariable, 0)
115
+ return
116
+ }
117
+
118
+ if (from === to) {
119
+ setProgress(el, cssVariable, scrollTop >= to ? 1 : 0)
120
+ return
121
+ }
122
+
123
+ const progress = clamp((scrollTop - from) / (to - from), 0, 1)
124
+
125
+ setProgress(el, cssVariable, progress)
126
+ }
127
+
128
+ function mountScrollTrack(el, binding = {}) {
4
129
  if (typeof window === 'undefined') return
5
130
 
6
- el.__scroll = basicScroll.create({
7
- elem: el,
8
- from: modifiers.window ? 0 : 'middle-top',
9
- to: modifiers.window ? window.outerHeight : 'middle-bottom',
10
- direct: true,
11
- props: {
12
- [`--${expression || 'scroll-progress'}`]: {
13
- from: '0',
14
- to: '1',
15
- },
131
+ unmountScrollTrack(el)
132
+
133
+ const state = {
134
+ binding,
135
+ frameId: null,
136
+ handleUpdate() {
137
+ updateScrollTrack(el)
138
+ },
139
+ scheduleUpdate() {
140
+ if (state.frameId !== null) {
141
+ return
142
+ }
143
+
144
+ state.frameId = window.requestAnimationFrame(() => {
145
+ state.frameId = null
146
+ state.handleUpdate()
147
+ })
16
148
  },
17
- })
18
- el.__scroll.start()
149
+ }
150
+
151
+ el.__scrollTrackState = state
152
+ updateScrollTrack(el)
153
+
154
+ window.addEventListener('scroll', state.scheduleUpdate, { passive: true })
155
+ window.addEventListener('resize', state.scheduleUpdate)
156
+ }
157
+
158
+ function updateBinding(el, binding = {}) {
159
+ if (!el.__scrollTrackState) {
160
+ mountScrollTrack(el, binding)
161
+ return
162
+ }
163
+
164
+ el.__scrollTrackState.binding = binding
165
+ updateScrollTrack(el)
19
166
  }
20
167
 
21
168
  function unmountScrollTrack(el) {
22
- if (!el.__scroll) return
23
- el.__scroll.stop()
24
- delete el.__scroll
169
+ const state = el.__scrollTrackState
170
+
171
+ if (!state || typeof window === 'undefined') return
172
+
173
+ window.removeEventListener('scroll', state.scheduleUpdate)
174
+ window.removeEventListener('resize', state.scheduleUpdate)
175
+
176
+ if (state.frameId !== null) {
177
+ window.cancelAnimationFrame(state.frameId)
178
+ }
179
+
180
+ delete el.__scrollTrackState
25
181
  }
26
182
 
27
183
  export default {
28
184
  inserted: mountScrollTrack,
29
185
  mounted: mountScrollTrack,
186
+ update: updateBinding,
187
+ updated: updateBinding,
30
188
  unbind: unmountScrollTrack,
31
189
  unmounted: unmountScrollTrack,
32
190
  }
@@ -50,20 +50,14 @@ export function shouldOpenInNewWindow(href, options) {
50
50
  return !isAsd20Hostname(url.hostname, options)
51
51
  }
52
52
 
53
- export function resolveCurrentHostname(vm, options) {
53
+ export function resolveCurrentHostname(_vm, options = {}) {
54
54
  if (typeof window !== 'undefined' && window.location) {
55
55
  return normalizeHostname(window.location.hostname, options)
56
56
  }
57
57
 
58
- const requestHost =
59
- vm &&
60
- vm.$ssrContext &&
61
- vm.$ssrContext.req &&
62
- vm.$ssrContext.req.headers &&
63
- (vm.$ssrContext.req.headers['x-forwarded-host'] ||
64
- vm.$ssrContext.req.headers.host)
65
-
66
- return normalizeHostname(requestHost, options)
58
+ // Server renders should stay deterministic and avoid reaching through the
59
+ // component proxy for SSR internals that are not part of the Vue 3 public API.
60
+ return normalizeHostname(options.currentHostname || '', options)
67
61
  }
68
62
 
69
63
  export function shouldShowExternalIcon(href, currentHostname, options) {
@@ -1,6 +1,12 @@
1
1
  import kebabCase from 'lodash/kebabCase'
2
+ import createComponentInstanceId from '../utils/createComponentInstanceId'
2
3
 
3
4
  const INPUT_MODEL_LISTENER_KEYS = ['onInput', 'onUpdate:modelValue']
5
+ const DEFAULT_INPUT_COMPONENT_NAME = 'asd20-input'
6
+
7
+ function resolveKebabComponentName(componentName = DEFAULT_INPUT_COMPONENT_NAME) {
8
+ return kebabCase(componentName).replace(/-([0-9])/g, '$1')
9
+ }
4
10
 
5
11
  function omitKeys(source, keys) {
6
12
  const target = { ...(source || {}) }
@@ -46,18 +52,23 @@ export default {
46
52
  reversed: { type: Boolean, default: false },
47
53
  taggable: { type: Boolean, default: true },
48
54
  },
49
- data: () => ({
50
- isDirty: false,
51
- isActive: false,
52
- validationErrors: [],
53
- }),
55
+ data() {
56
+ return {
57
+ isDirty: false,
58
+ isActive: false,
59
+ validationErrors: [],
60
+ generatedInputId: createComponentInstanceId(
61
+ resolveKebabComponentName(this.$options.name)
62
+ ),
63
+ }
64
+ },
54
65
  mounted() {
55
66
  this.validate(this.resolvedValue)
56
67
  },
57
68
 
58
69
  computed: {
59
70
  kebabComponentName() {
60
- return kebabCase(this.$options.name).replace(/-([0-9])/g, '$1')
71
+ return resolveKebabComponentName(this.$options.name)
61
72
  },
62
73
  classes() {
63
74
  let classes = {}
@@ -92,11 +103,7 @@ export default {
92
103
  },
93
104
 
94
105
  computedId() {
95
- return this.$attrs.id
96
- ? this.$attrs.id
97
- : `${this.kebabComponentName}_${Math.round(
98
- Math.random() * 10000000000000000
99
- )}`
106
+ return this.$attrs.id ? this.$attrs.id : this.generatedInputId
100
107
  },
101
108
 
102
109
  selectedItem() {
@@ -1,6 +1,9 @@
1
- let nextComponentInstanceId = 0
1
+ import { getCurrentInstance, useId } from 'vue'
2
2
 
3
3
  export default function createComponentInstanceId(prefix = 'asd20-component') {
4
- nextComponentInstanceId += 1
5
- return `${prefix}-${nextComponentInstanceId}`
4
+ if (getCurrentInstance()) {
5
+ return `${prefix}-${useId()}`
6
+ }
7
+
8
+ return `${prefix}-detached`
6
9
  }