diff --git a/src/components/EventDialog.vue b/src/components/EventDialog.vue
index 288d63d..0d67819 100644
--- a/src/components/EventDialog.vue
+++ b/src/components/EventDialog.vue
@@ -437,26 +437,25 @@ const recurrenceSummary = computed(() => {
- Every
diff --git a/src/components/EventOverlay.vue b/src/components/EventOverlay.vue
index cd363e6..d01f4d5 100644
--- a/src/components/EventOverlay.vue
+++ b/src/components/EventOverlay.vue
@@ -63,29 +63,30 @@ function generateRepeatOccurrencesForDate(targetDateStr) {
if (baseEvent.repeat === 'weeks') {
const repeatWeekdays = baseEvent.repeatWeekdays
if (targetDate < baseStartDate) continue
- const maxOccurrences = baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
+ const maxOccurrences =
+ baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
if (maxOccurrences === 0) continue
const interval = baseEvent.repeatInterval || 1
- const msPerDay = 24*60*60*1000
+ const msPerDay = 24 * 60 * 60 * 1000
// Determine if targetDate lies within some occurrence span. We look backwards up to spanDays to find a start day.
let occStart = null
- for (let back=0; back<=spanDays; back++) {
+ for (let back = 0; back <= spanDays; back++) {
const cand = new Date(targetDate)
cand.setDate(cand.getDate() - back)
if (cand < baseStartDate) break
- const daysDiff = Math.floor((cand - baseStartDate)/msPerDay)
+ const daysDiff = Math.floor((cand - baseStartDate) / msPerDay)
const weeksDiff = Math.floor(daysDiff / 7)
if (weeksDiff % interval !== 0) continue
- if (repeatWeekdays[cand.getDay()]) {
- // candidate start must produce span covering targetDate
- const candEnd = new Date(cand)
- candEnd.setDate(candEnd.getDate() + spanDays)
- if (targetDate <= candEnd) {
- occStart = cand
- break
- }
+ if (repeatWeekdays[cand.getDay()]) {
+ // candidate start must produce span covering targetDate
+ const candEnd = new Date(cand)
+ candEnd.setDate(candEnd.getDate() + spanDays)
+ if (targetDate <= candEnd) {
+ occStart = cand
+ break
}
+ }
}
if (!occStart) continue
// Skip base occurrence if this is within its span (base already physically stored)
@@ -94,10 +95,10 @@ function generateRepeatOccurrencesForDate(targetDateStr) {
let occIdx = 0
const cursor = new Date(baseStartDate)
while (cursor < occStart && occIdx < maxOccurrences) {
- const cDaysDiff = Math.floor((cursor - baseStartDate)/msPerDay)
+ const cDaysDiff = Math.floor((cursor - baseStartDate) / msPerDay)
const cWeeksDiff = Math.floor(cDaysDiff / 7)
if (cWeeksDiff % interval === 0 && repeatWeekdays[cursor.getDay()]) occIdx++
- cursor.setDate(cursor.getDate()+1)
+ cursor.setDate(cursor.getDate() + 1)
}
if (occIdx >= maxOccurrences) continue
const occEnd = new Date(occStart)
diff --git a/src/components/Numeric.vue b/src/components/Numeric.vue
index 3f8dc35..1beb9bf 100644
--- a/src/components/Numeric.vue
+++ b/src/components/Numeric.vue
@@ -7,7 +7,7 @@
role="spinbutton"
:aria-valuemin="minValue"
:aria-valuemax="maxValue"
- :aria-valuenow="current === infiniteValue ? undefined : current"
+ :aria-valuenow="isPrefix(current) ? undefined : current"
:aria-valuetext="display"
tabindex="0"
@pointerdown="onPointerDown"
@@ -26,8 +26,14 @@ const props = defineProps({
min: { type: Number, default: 0 },
max: { type: Number, default: 999 },
step: { type: Number, default: 1 },
- infiniteValue: { type: Number, default: 0 }, // model value representing infinity / special
- infiniteDisplay: { type: String, default: '∞' },
+ prefixValues: {
+ type: Array,
+ default: () => [],
+ validator: (arr) =>
+ arr.every((item) => typeof item === 'object' && 'value' in item && 'display' in item),
+ },
+ numberPrefix: { type: String, default: '' },
+ numberPostfix: { type: String, default: '' },
clamp: { type: Boolean, default: true },
pixelsPerStep: { type: Number, default: 16 },
// Movement now restricted to horizontal (x). Prop retained for compatibility but ignored.
@@ -39,12 +45,39 @@ const props = defineProps({
const minValue = computed(() => props.min)
const maxValue = computed(() => props.max)
+// Helper to check if a value is in the prefix values
+const isPrefix = (value) => {
+ return props.prefixValues.some((prefix) => prefix.value === value)
+}
+
+// Helper to get the display for a prefix value
+const getPrefixDisplay = (value) => {
+ const prefix = props.prefixValues.find((p) => p.value === value)
+ return prefix ? prefix.display : null
+}
+
+// Get all valid values in order: prefixValues, then min to max
+const allValidValues = computed(() => {
+ const prefixVals = props.prefixValues.map((p) => p.value)
+ const numericVals = []
+ for (let i = props.min; i <= props.max; i += props.step) {
+ numericVals.push(i)
+ }
+ return [...prefixVals, ...numericVals]
+})
+
const current = computed({
get() {
return model.value
},
set(v) {
if (props.clamp) {
+ // If it's a prefix value, allow it
+ if (isPrefix(v)) {
+ model.value = v
+ return
+ }
+ // Otherwise clamp to numeric range
if (v < props.min) v = props.min
if (v > props.max) v = props.max
}
@@ -52,9 +85,16 @@ const current = computed({
},
})
-const display = computed(() =>
- current.value === props.infiniteValue ? props.infiniteDisplay : String(current.value),
-)
+const display = computed(() => {
+ const prefixDisplay = getPrefixDisplay(current.value)
+ if (prefixDisplay !== null) {
+ // For prefix values, show only the display text without number prefix/postfix
+ return prefixDisplay
+ }
+ // For numeric values, include prefix and postfix
+ const numericValue = String(current.value)
+ return `${props.numberPrefix}${numericValue}${props.numberPostfix}`
+})
// Drag handling
const dragging = ref(false)
@@ -82,12 +122,22 @@ function onPointerMove(e) {
if (e.pointerType === 'touch') e.preventDefault()
const primary = e.clientX - startX // horizontal only
const steps = Math.trunc(primary / props.pixelsPerStep)
- let next = startVal + steps * props.step
+
+ // Find current value index in all valid values
+ const currentIndex = allValidValues.value.indexOf(startVal)
+ if (currentIndex === -1) return // shouldn't happen
+
+ const newIndex = currentIndex + steps
if (props.clamp) {
- if (next < props.min) next = props.min
- if (next > props.max) next = props.max
+ const clampedIndex = Math.max(0, Math.min(newIndex, allValidValues.value.length - 1))
+ const next = allValidValues.value[clampedIndex]
+ if (next !== current.value) current.value = next
+ } else {
+ if (newIndex >= 0 && newIndex < allValidValues.value.length) {
+ const next = allValidValues.value[newIndex]
+ if (next !== current.value) current.value = next
+ }
}
- if (next !== current.value) current.value = next
}
function endDragListeners() {
rootEl.value?.removeEventListener('pointermove', onPointerMove)
@@ -104,36 +154,64 @@ function onPointerCancel() {
function onKey(e) {
const key = e.key
let handled = false
- let delta = 0
+ let newValue = null
+
+ // Find current value index in all valid values
+ const currentIndex = allValidValues.value.indexOf(current.value)
+
switch (key) {
case 'ArrowRight':
case 'ArrowUp':
- delta = props.step
+ if (currentIndex !== -1 && currentIndex < allValidValues.value.length - 1) {
+ newValue = allValidValues.value[currentIndex + 1]
+ } else if (currentIndex === -1) {
+ // Current value not in list, try to increment normally
+ newValue = current.value + props.step
+ }
handled = true
break
case 'ArrowLeft':
case 'ArrowDown':
- delta = -props.step
+ if (currentIndex !== -1 && currentIndex > 0) {
+ newValue = allValidValues.value[currentIndex - 1]
+ } else if (currentIndex === -1) {
+ // Current value not in list, try to decrement normally
+ newValue = current.value - props.step
+ }
handled = true
break
case 'PageUp':
- delta = props.step * 10
+ if (currentIndex !== -1) {
+ const newIndex = Math.min(currentIndex + 10, allValidValues.value.length - 1)
+ newValue = allValidValues.value[newIndex]
+ } else {
+ newValue = current.value + props.step * 10
+ }
handled = true
break
case 'PageDown':
- delta = -props.step * 10
+ if (currentIndex !== -1) {
+ const newIndex = Math.max(currentIndex - 10, 0)
+ newValue = allValidValues.value[newIndex]
+ } else {
+ newValue = current.value - props.step * 10
+ }
handled = true
break
case 'Home':
- current.value = props.min
+ newValue = allValidValues.value[0] || props.min
handled = true
break
case 'End':
- current.value = props.max
+ newValue = allValidValues.value[allValidValues.value.length - 1] || props.max
handled = true
break
}
- if (delta !== 0) current.value = current.value + delta
+
+ if (newValue !== null) {
+ current.value = newValue
+ }
+
if (handled) {
e.preventDefault()
e.stopPropagation()