@asd20/ui-next 2.0.0 → 2.0.2

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.1...ui-next-v2.0.2) (2026-03-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * replace basicscroll scroll-track runtime ([c0fe361](https://github.com/academydistrict20/asd20-ui-next/commit/c0fe361c301bbf1d132eac7876c5e02cabe4a5b0))
9
+
10
+ ## [2.0.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.0...ui-next-v2.0.1) (2026-03-27)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * harden vue 3 navigation focus refs ([ba0bf4f](https://github.com/academydistrict20/asd20-ui-next/commit/ba0bf4f3b67661b2a837a6257ded847d8a0aabe8))
16
+
3
17
  # [2.0.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v1.0.11...ui-next-v2.0.0) (2026-03-27)
4
18
 
5
19
 
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @asd20/ui-next
2
2
 
3
- Vue component library migration workspace for ASD20.
3
+ Vue 3 component library for ASD20 shared UI surfaces.
4
+
5
+ The current published baseline is `@asd20/ui-next@2.0.0`. This release is Vue 3-only and ships the model-only shared input contract.
4
6
 
5
7
  ## Public Entry Points
6
8
 
@@ -22,7 +24,7 @@ npm run storybook
22
24
  npm run build
23
25
  ```
24
26
 
25
- ## Package Artifact
27
+ ## Release Validation
26
28
 
27
29
  Build the staged publish artifact with:
28
30
 
@@ -30,28 +32,22 @@ Build the staged publish artifact with:
30
32
  npm run build:package
31
33
  ```
32
34
 
33
- This writes a curated source-based package to `dist/package` so release and pack validation do not publish Storybook files, snapshot tests, or the local app shell.
34
-
35
- The staged package also trims undocumented heavyweight `public/` media. Only explicit runtime public assets still referenced by package source are copied into `dist/package/public`.
36
-
37
- The staged package also trims `src/data` down to the small runtime subset still used by exported source. Story/demo payloads remain in the repo but are not staged for publish.
38
-
39
- Verify the publish path end to end with:
35
+ Validate the publish shape end to end with:
40
36
 
41
37
  ```bash
42
38
  npm run verify:publish
43
39
  ```
44
40
 
45
- That command rebuilds `dist/package`, verifies the root and staged export contract, and runs `npm pack --dry-run` against the staged package with a temporary writable npm cache.
46
-
47
- It also validates the checked public API contract for package subpaths and public entrypoint named exports, so export-surface drift fails before release.
41
+ That flow rebuilds `dist/package`, verifies the root and staged export contract, checks the public API surface, and runs `npm pack --dry-run` against the staged package.
48
42
 
49
- Verify the release path against a throwaway packed consumer app with:
43
+ Validate the packed consumer install path with:
50
44
 
51
45
  ```bash
52
46
  npm run verify:release
53
47
  ```
54
48
 
55
- That command runs the publish verifier, packs the staged package into a tarball, installs that tarball into a temporary Vue 3 smoke app, and confirms the consumer build succeeds against the installed package shape.
49
+ That flow packs the staged package into a tarball, installs it into a temporary Vue 3 smoke app, and confirms the consumer build succeeds against the installed package shape.
50
+
51
+ ## Migration Record
56
52
 
57
- The smoke app also compiles `v-model` usage against the exported input primitives, so Vue 3 two-way-binding regressions are part of the release check.
53
+ The package is now release-ready and published, but the migration record is still kept under `docs/migration/` for downstream audit history, release decisions, and future cleanup planning.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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",
@@ -108,11 +108,16 @@ export default {
108
108
  this.$emit('update:open', value)
109
109
  this.$emit('update:modelValue', value)
110
110
  },
111
+ focusRef(refName) {
112
+ const ref = this.$refs[refName]
113
+ const focusTarget = ref?.$el || ref
114
+ if (typeof focusTarget?.focus === 'function') {
115
+ focusTarget.focus()
116
+ }
117
+ },
111
118
  setFocus() {
112
119
  this.$nextTick(() => {
113
- if (this.$refs.closeButton && this.$refs.closeButton.$el) {
114
- this.$refs.closeButton.$el.focus()
115
- }
120
+ this.focusRef('closeButton')
116
121
  })
117
122
  },
118
123
  },
@@ -67,19 +67,26 @@ export default {
67
67
  menuActive: function(val) {
68
68
  if (!val) {
69
69
  this.$nextTick(() => {
70
- this.$refs.menuButton.$el.focus()
70
+ this.focusRef('menuButton')
71
71
  })
72
72
  }
73
73
  },
74
74
  searchActive: function(val) {
75
75
  if (!val) {
76
76
  this.$nextTick(() => {
77
- this.$refs.searchButton.$el.focus()
77
+ this.focusRef('searchButton')
78
78
  })
79
79
  }
80
80
  },
81
81
  },
82
82
  methods: {
83
+ focusRef(refName) {
84
+ const ref = this.$refs[refName]
85
+ const focusTarget = ref?.$el || ref
86
+ if (typeof focusTarget?.focus === 'function') {
87
+ focusTarget.focus()
88
+ }
89
+ },
83
90
  toggleMenu(event) {
84
91
  this.$emit('update:menuActive', !this.menuActive)
85
92
  this.$emit('keyboardTriggeredLastAction', event.type === 'keyup')
@@ -123,7 +123,7 @@ export default {
123
123
  menuOpen: function (val) {
124
124
  this.$nextTick(() => {
125
125
  if (val) {
126
- this.$refs.siteMenu.$el.focus()
126
+ this.focusSiteMenu()
127
127
  }
128
128
  })
129
129
  },
@@ -139,6 +139,16 @@ export default {
139
139
  window.removeEventListener('resize', this.handleResize)
140
140
  },
141
141
  methods: {
142
+ focusSiteMenu() {
143
+ const focusTarget =
144
+ this.$refs.siteMenu?.$refs?.menuContainer ||
145
+ this.$refs.siteMenu?.$el ||
146
+ this.$refs.siteMenu
147
+
148
+ if (typeof focusTarget?.focus === 'function') {
149
+ focusTarget.focus()
150
+ }
151
+ },
142
152
  onKeyboardToggleEvent(event) {
143
153
  this.keyboardToggleEvent = event
144
154
  },
@@ -1962,17 +1962,19 @@ export default {
1962
1962
  },
1963
1963
 
1964
1964
  focusQuery() {
1965
- if (this.$refs.query && this.$refs.query.$el) {
1966
- const input = this.$refs.query.$el.querySelector('input')
1967
- if (input) input.focus()
1968
- }
1965
+ const input = this.getQueryInput()
1966
+ if (input) input.focus()
1969
1967
  },
1970
1968
 
1971
1969
  blurQuery() {
1972
- if (this.$refs.query && this.$refs.query.$el) {
1973
- const input = this.$refs.query.$el.querySelector('input')
1974
- if (input) input.blur()
1975
- }
1970
+ const input = this.getQueryInput()
1971
+ if (input) input.blur()
1972
+ },
1973
+
1974
+ getQueryInput() {
1975
+ const queryRef = this.$refs.query?.$el || this.$refs.query
1976
+ if (typeof queryRef?.querySelector !== 'function') return null
1977
+ return queryRef.querySelector('input')
1976
1978
  },
1977
1979
 
1978
1980
  getResultsViewportRef(tabLabel = this.currentTab) {
@@ -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
  }