@proj-airi/ui 0.10.1 → 0.11.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proj-airi/ui",
3
3
  "type": "module",
4
- "version": "0.10.1",
4
+ "version": "0.11.0",
5
5
  "description": "A collection of UI components that used by Project AIRI",
6
6
  "author": {
7
7
  "name": "Moeru AI Project AIRI Team",
@@ -30,7 +30,7 @@
30
30
  "vue": "^3.5.32"
31
31
  },
32
32
  "devDependencies": {
33
- "@vue-macros/volar": "3.0.0-beta.8",
33
+ "@vue-macros/volar": "^3.1.2",
34
34
  "vue-tsc": "^3.2.6"
35
35
  },
36
36
  "scripts": {
@@ -35,10 +35,12 @@ const props = withDefaults(defineProps<{
35
35
  options: ComboboxOptionItem<T>[] | ComboboxOptionGroupItem<T>[]
36
36
  placeholder?: string
37
37
  disabled?: boolean
38
+ openOnClick?: boolean
38
39
  contentMinWidth?: string | number
39
40
  contentWidth?: string | number
40
41
  }>(), {
41
42
  disabled: false,
43
+ openOnClick: true,
42
44
  })
43
45
 
44
46
  const modelValue = defineModel<T>({ required: false })
@@ -83,6 +85,7 @@ function toCssSize(value?: string | number): string | undefined {
83
85
  <ComboboxRoot
84
86
  v-model="modelValue"
85
87
  :disabled="props.disabled"
88
+ :open-on-click="props.openOnClick"
86
89
  :class="['relative', 'w-full', 'h-fit']"
87
90
  >
88
91
  <ComboboxAnchor
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { Combobox } from '../combobox'
3
3
 
4
- const props = defineProps<{
4
+ const props = withDefaults(defineProps<{
5
5
  options?: {
6
6
  label: string
7
7
  value: string | number
@@ -11,11 +11,15 @@ const props = defineProps<{
11
11
  }[]
12
12
  placeholder?: string
13
13
  disabled?: boolean
14
+ openOnClick?: boolean
14
15
  title?: string
15
16
  layout?: 'horizontal' | 'vertical'
16
17
  contentMinWidth?: string | number
17
18
  contentWidth?: string | number
18
- }>()
19
+ }>(), {
20
+ disabled: false,
21
+ openOnClick: true,
22
+ })
19
23
 
20
24
  const modelValue = defineModel<string | number>({ required: false })
21
25
  </script>
@@ -25,6 +29,7 @@ const modelValue = defineModel<string | number>({ required: false })
25
29
  v-model="modelValue"
26
30
  :options="[{ groupLabel: '', children: props.options }]"
27
31
  :disabled="props.disabled"
32
+ :open-on-click="props.openOnClick"
28
33
  :content-min-width="props.contentMinWidth"
29
34
  :content-width="props.contentWidth"
30
35
  :placeholder="props.placeholder"
@@ -13,12 +13,15 @@ const props = withDefaults(defineProps<{
13
13
  }[]
14
14
  placeholder?: string
15
15
  disabled?: boolean
16
+ openOnClick?: boolean
16
17
  layout?: 'horizontal' | 'vertical'
17
18
  selectClass?: string | string[]
18
19
  contentMinWidth?: string | number
19
20
  contentWidth?: string | number
20
21
  }>(), {
21
22
  layout: 'horizontal',
23
+ disabled: false,
24
+ openOnClick: true,
22
25
  })
23
26
 
24
27
  const modelValue = defineModel<string>({ required: false })
@@ -55,6 +58,7 @@ const modelValue = defineModel<string>({ required: false })
55
58
  :options="props.options?.filter(option => option.label && option.value) || []"
56
59
  :placeholder="props.placeholder"
57
60
  :disabled="props.disabled"
61
+ :open-on-click="props.openOnClick"
58
62
  :content-min-width="props.contentMinWidth"
59
63
  :content-width="props.contentWidth"
60
64
  :title="label"
@@ -27,6 +27,7 @@ const props = withDefaults(defineProps<{
27
27
  */
28
28
  hideRequiredMark?: boolean
29
29
  type?: InputType
30
+ autocomplete?: string
30
31
  inputClass?: string
31
32
  singleLine?: boolean
32
33
  }>(), {
@@ -57,6 +58,7 @@ const modelValue = defineModel<T>({ required: false })
57
58
  v-model.number="modelValue"
58
59
  :type="props.type"
59
60
  :placeholder="props.placeholder"
61
+ :autocomplete="props.autocomplete"
60
62
  :required="props.required"
61
63
  :class="props.inputClass"
62
64
  />
@@ -65,6 +67,7 @@ const modelValue = defineModel<T>({ required: false })
65
67
  v-model="modelValue"
66
68
  :type="props.type"
67
69
  :placeholder="props.placeholder"
70
+ :autocomplete="props.autocomplete"
68
71
  :required="props.required"
69
72
  :class="props.inputClass"
70
73
  />
@@ -73,6 +76,7 @@ const modelValue = defineModel<T>({ required: false })
73
76
  v-model="modelValue as string | undefined"
74
77
  :type="props.type"
75
78
  :placeholder="props.placeholder"
79
+ :autocomplete="props.autocomplete"
76
80
  :required="props.required"
77
81
  :class="[
78
82
  props.inputClass,
@@ -81,6 +85,7 @@ const modelValue = defineModel<T>({ required: false })
81
85
  'text-disabled:neutral-400 dark:text-disabled:neutral-600',
82
86
  'cursor-disabled:not-allowed',
83
87
  'w-full rounded-lg px-2 py-1 text-sm outline-none',
88
+ 'text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500',
84
89
  'shadow-sm',
85
90
  'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
86
91
  ]"
@@ -9,11 +9,22 @@ const props = withDefaults(defineProps<{
9
9
  description?: string
10
10
  formatValue?: (value: number) => string
11
11
  as?: 'label' | 'div'
12
+ /**
13
+ * When provided, renders a reset control next to the label that restores the
14
+ * value to this default. Prefer `as="div"` when using this, since the reset
15
+ * button should not live inside a `<label>` element.
16
+ */
17
+ defaultValue?: number
12
18
  }>(), {
13
19
  as: 'label',
14
20
  })
15
21
 
16
22
  const modelValue = defineModel<number>({ required: true })
23
+
24
+ function resetToDefault() {
25
+ if (props.defaultValue !== undefined)
26
+ modelValue.value = props.defaultValue
27
+ }
17
28
  </script>
18
29
 
19
30
  <template>
@@ -24,6 +35,15 @@ const modelValue = defineModel<number>({ required: true })
24
35
  <slot name="label">
25
36
  {{ label }}
26
37
  </slot>
38
+ <button
39
+ v-if="defaultValue !== undefined"
40
+ type="button"
41
+ title="Reset to default"
42
+ :class="['px-2', 'text-xs', 'outline-none']"
43
+ @click.prevent="resetToDefault"
44
+ >
45
+ <div :class="['i-solar:forward-linear', 'transform-scale-x--100', 'text-neutral-500', 'dark:text-neutral-400']" />
46
+ </button>
27
47
  </div>
28
48
  <div :class="['text-xs', 'text-neutral-500', 'dark:text-neutral-400']">
29
49
  <slot name="description">
@@ -40,6 +40,7 @@ const variantClasses: Record<InputVariant, Record<InputTheme, {
40
40
  default: [
41
41
  'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
42
42
  'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
43
+ 'text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500',
43
44
  'focus:border-primary-300 dark:focus:border-primary-400/50 border-2 border-solid border-neutral-100 dark:border-neutral-900',
44
45
  'text-disabled:neutral-400 dark:text-disabled:neutral-600',
45
46
  'shadow-sm',
@@ -51,6 +52,7 @@ const variantClasses: Record<InputVariant, Record<InputTheme, {
51
52
  default: [
52
53
  'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
53
54
  'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
55
+ 'text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500',
54
56
  'focus:border-primary-300 dark:focus:border-primary-400/50 border-2 border-solid border-neutral-100 dark:border-neutral-900',
55
57
  'text-disabled:neutral-400 dark:text-disabled:neutral-600',
56
58
  'shadow-sm',
@@ -62,6 +64,7 @@ const variantClasses: Record<InputVariant, Record<InputTheme, {
62
64
  default: [
63
65
  'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
64
66
  'bg-neutral-100 dark:bg-neutral-800 focus:bg-neutral-50 dark:focus:bg-neutral-950',
67
+ 'text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500',
65
68
  'focus:border-primary-500/30 dark:focus:border-primary-400/50 border-2 border-solid border-neutral-500/5 dark:border-neutral-700/40',
66
69
  'text-disabled:neutral-400 dark:text-disabled:neutral-600',
67
70
  ],
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { useElementHover, useKeyModifier } from '@vueuse/core'
2
3
  import { computed, onMounted, ref, watch } from 'vue'
3
4
 
4
5
  const props = withDefaults(defineProps<{
@@ -6,6 +7,7 @@ const props = withDefaults(defineProps<{
6
7
  max?: number
7
8
  step?: number
8
9
  disabled?: boolean
10
+ handleWheel?: boolean
9
11
  }>(), {
10
12
  min: 0,
11
13
  max: 100,
@@ -13,17 +15,22 @@ const props = withDefaults(defineProps<{
13
15
  disabled: false,
14
16
  })
15
17
 
16
- const modelValue = defineModel<number>({ required: true })
18
+ const smoothingFactor = 10000
17
19
 
18
- const scaledMin = computed(() => props.min * 10000)
19
- const scaledMax = computed(() => props.max * 10000)
20
- const scaledStep = computed(() => props.step * 10000)
20
+ const modelValue = defineModel<number>({ required: true })
21
21
 
22
22
  const sliderRef = ref<HTMLInputElement>()
23
+
24
+ const scaledMin = computed(() => props.min * smoothingFactor)
25
+ const scaledMax = computed(() => props.max * smoothingFactor)
26
+ const scaledStep = computed(() => props.step * smoothingFactor)
27
+ const shiftPressed = useKeyModifier('Shift')
28
+ const isHovered = useElementHover(sliderRef)
29
+
23
30
  const sliderValue = computed({
24
- get: () => modelValue.value * 10000,
31
+ get: () => modelValue.value * smoothingFactor,
25
32
  set: (value: number) => {
26
- modelValue.value = value / 10000
33
+ modelValue.value = value / smoothingFactor
27
34
  updateTrackColor()
28
35
  },
29
36
  })
@@ -31,6 +38,15 @@ const sliderValue = computed({
31
38
  onMounted(() => updateTrackColor())
32
39
  watch(sliderValue, () => updateTrackColor(), { immediate: true })
33
40
  watch([scaledMin, scaledMax, scaledStep], () => updateTrackColor(), { immediate: true })
41
+ watch(isHovered, (hovered: boolean) => {
42
+ if (!props.handleWheel)
43
+ return
44
+ if (hovered) {
45
+ sliderRef.value?.addEventListener('wheel', onWheelInput)
46
+ return
47
+ }
48
+ sliderRef.value?.removeEventListener('wheel', onWheelInput)
49
+ })
34
50
 
35
51
  function updateTrackColor() {
36
52
  if (!sliderRef.value) {
@@ -46,6 +62,17 @@ function handleInput(e: Event) {
46
62
  const target = e.target as HTMLInputElement
47
63
  target.style.setProperty('--value', target.value)
48
64
  }
65
+
66
+ function onWheelInput(ev: WheelEvent) {
67
+ if (ev.deltaY === 0)
68
+ return
69
+ let valueAfter = modelValue.value
70
+ if (ev.deltaY < 0)
71
+ valueAfter += props.step * (shiftPressed.value ? 50 : 1)
72
+ if (ev.deltaY > 0)
73
+ valueAfter -= props.step * (shiftPressed.value ? 50 : 1)
74
+ modelValue.value = Math.min(Math.max(valueAfter, props.min), props.max)
75
+ }
49
76
  </script>
50
77
 
51
78
  <template>
@@ -66,21 +93,23 @@ function handleInput(e: Event) {
66
93
  /*generated with Input range slider CSS style generator (version 20211225)
67
94
  https://toughengineer.github.io/demo/slider-styler*/
68
95
  .form_input-round-range {
69
- --height: 2em;
96
+ --height: 100%;
97
+ --width: 2rem;
70
98
 
71
- min-height: var(--height);
99
+ min-height: var(--width);
72
100
  appearance: none;
73
101
  background: transparent;
74
102
  transition: background-color 0.2s ease;
75
103
 
76
- --thumb-width: var(--height);
77
- --thumb-height: var(--height);
104
+ --thumb-width: var(--width);
105
+ --thumb-height: var(--width);
78
106
  --thumb-box-shadow: none;
79
107
  --thumb-border: none;
80
108
  --thumb-border-radius: 0px;
81
109
  --thumb-background: transparent;
82
110
 
83
111
  --track-height: calc(var(--height) - var(--track-value-padding) * 2);
112
+ --track-width: var(--width);
84
113
  --track-box-shadow: 0 0 12px -2px rgb(0 0 0 / 22%);
85
114
  --track-border: none;
86
115
  --track-border-radius: 16px;
@@ -95,8 +124,6 @@ https://toughengineer.github.io/demo/slider-styler*/
95
124
  }
96
125
 
97
126
  .dark .form_input-round-range {
98
- --thumb-background: rgb(238, 238, 238);
99
-
100
127
  --track-border: none;
101
128
  --track-background: rgba(64, 64, 64, 0.7);
102
129
  --track-box-shadow: 0 0 12px -2px rgb(0 0 0 / 22%);
@@ -13,6 +13,7 @@ const modelValue = defineModel<string>({ default: '' })
13
13
  'text-disabled:neutral-400 dark:text-disabled:neutral-600',
14
14
  'cursor-disabled:not-allowed',
15
15
  'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
16
+ 'text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500',
16
17
  'shadow-sm',
17
18
  'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
18
19
  ]"
@@ -63,6 +63,10 @@ onMounted(async () => {
63
63
 
64
64
  containerElementBounding.update()
65
65
  })
66
+
67
+ defineExpose({
68
+ containerRef,
69
+ })
66
70
  </script>
67
71
 
68
72
  <template>
@@ -154,7 +154,7 @@ const baseClasses = computed(() => {
154
154
  const theme = variant[props.theme] || variant.default
155
155
 
156
156
  return [
157
- 'font-medium outline-none',
157
+ 'font-medium outline-none whitespace-nowrap',
158
158
  'transition-all duration-200 ease-in-out',
159
159
  'disabled:cursor-not-allowed disabled:opacity-50',
160
160
  'backdrop-blur-md',