@proj-airi/ui 0.7.2-beta.2 → 0.8.0-alpha.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/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # @proj-airi/ui
2
2
 
3
- A stylized UI component library built with [Reka UI](https://reka-ui.com/).
3
+ A stylized UI component library built with [Reka UI](https://reka-ui.com/) and [UnoCSS](https://unocss.dev/).
4
4
 
5
5
  To preview the components, refer to the [`stage-ui`](../stage-ui) package for instructions for running the Histoire UI storyboard.
6
6
 
7
- ## Usage
7
+ ## Get started
8
+
9
+ Install the library:
8
10
 
9
11
  ```shell
10
12
  ni @proj-airi/ui -D # from @antfu/ni, can be installed via `npm i -g @antfu/ni`
@@ -13,6 +15,33 @@ yarn i @proj-airi/ui -D
13
15
  npm i @proj-airi/ui -D
14
16
  ```
15
17
 
18
+ This library requires `unocss` with Attributify Mode and a style reset.
19
+
20
+ First, install `unocss` if you haven't already:
21
+
22
+ ```shell
23
+ pnpm i -D unocss
24
+ ```
25
+
26
+ Next, in your `uno.config.ts`, add `presetAttributify()` to your presets array:
27
+ ```ts
28
+ import { defineConfig, presetAttributify } from 'unocss'
29
+
30
+ export default defineConfig({
31
+ presets: [
32
+ presetAttributify(),
33
+ // ...your other presets
34
+ ],
35
+ })
36
+ ```
37
+
38
+ Finally, import the reset styles in your `main.ts`:
39
+ ```ts
40
+ import '@unocss/reset/tailwind.css'
41
+ ```
42
+
43
+ ## Usage
44
+
16
45
  ```vue
17
46
  <script setup lang="ts">
18
47
  import { Button } from '@proj-airi/ui'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proj-airi/ui",
3
3
  "type": "module",
4
- "version": "0.7.2-beta.2",
4
+ "version": "0.8.0-alpha.3",
5
5
  "description": "A collection of UI components that used by Project AIRI",
6
6
  "author": {
7
7
  "name": "Moeru AI Project AIRI Team",
@@ -21,14 +21,14 @@
21
21
  "./*": "./*"
22
22
  },
23
23
  "dependencies": {
24
- "@vueuse/core": "^13.6.0",
24
+ "@vueuse/core": "^14.0.0",
25
25
  "floating-vue": "^5.2.2",
26
- "reka-ui": "^2.4.1",
27
- "vue": "^3.5.18"
26
+ "reka-ui": "^2.6.0",
27
+ "vue": "^3.5.24"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@vue-macros/volar": "3.0.0-beta.8",
31
- "vue-tsc": "^3.0.5"
31
+ "vue-tsc": "^3.1.4"
32
32
  },
33
33
  "scripts": {
34
34
  "typecheck": "vue-tsc --noEmit"
@@ -7,21 +7,25 @@ const modelValue = defineModel<boolean>({ required: true })
7
7
  <template>
8
8
  <SwitchRoot
9
9
  v-model="modelValue"
10
- transition="background duration-250 ease-in-out"
11
- outline="focus-within:none"
12
- flex="~"
13
- border="neutral-300 dark:neutral-700 data-[state=checked]:primary-200 data-[state=unchecked]:neutral-300 focus-within:neutral-800"
14
- bg="data-[state=checked]:primary-400 data-[state=unchecked]:neutral-300 data-[state=checked]:dark:primary-400/80 dark:data-[state=unchecked]:neutral-800"
15
- relative h-7 w="12.5" rounded-full
16
- shadow="sm focus-within:shadow-neutral-800 focus-within:[0_0_0_1px] "
10
+ :class="[
11
+ 'duration-250 ease-in-out',
12
+ 'focus-within:outline-none',
13
+ 'flex',
14
+ 'border-neutral-300 dark:border-neutral-700 data-[state=checked]:border-primary-200 data-[state=unchecked]:border-neutral-300 focus-within:border-neutral-800',
15
+ 'data-[state=checked]:bg-primary-400 data-[state=unchecked]:bg-neutral-300 data-[state=checked]:dark:bg-primary-400/80 dark:data-[state=unchecked]:bg-neutral-800',
16
+ 'relative h-7 w-12.5 rounded-full',
17
+ 'shadow-sm focus-within:shadow-none',
18
+ ]"
17
19
  >
18
20
  <SwitchThumb
19
- my-auto size-6
20
- flex items-center justify-center
21
- translate-x="0.5 data-[state=checked]:full"
22
- rounded-full bg-white text-xs shadow-xl
23
- transition="transform duration-250 ease-in-out"
24
- will-change-transform
21
+ :class="[
22
+ 'my-auto size-6',
23
+ 'flex items-center justify-center',
24
+ 'translate-x-0.5 data-[state=checked]:translate-x-full',
25
+ 'rounded-full bg-white text-xs',
26
+ 'transition-transform duration-250 ease-in-out',
27
+ 'will-change-transform',
28
+ ]"
25
29
  />
26
30
  </SwitchRoot>
27
31
  </template>
@@ -30,7 +30,7 @@ function toDisplayValue(value: T): string {
30
30
  </script>
31
31
 
32
32
  <template>
33
- <ComboboxRoot v-model="modelValue" class="relative w-full">
33
+ <ComboboxRoot v-model="modelValue" :class="['relative', 'w-full']">
34
34
  <ComboboxAnchor
35
35
  :class="[
36
36
  'w-full inline-flex items-center justify-between rounded-xl border px-3 leading-none h-10 gap-[5px] outline-none',
@@ -84,10 +84,10 @@ function toDisplayValue(value: T): string {
84
84
  v-for="(group, index) in options"
85
85
  :key="group.name"
86
86
  >
87
- <ComboboxGroup class="overflow-x-hidden">
87
+ <ComboboxGroup :class="['overflow-x-hidden']">
88
88
  <ComboboxSeparator
89
89
  v-if="index !== 0"
90
- class="m-[5px] h-[1px] bg-neutral-400"
90
+ :class="['m-[5px]', 'h-[1px]', 'bg-neutral-400']"
91
91
  />
92
92
 
93
93
  <ComboboxLabel
@@ -114,11 +114,11 @@ function toDisplayValue(value: T): string {
114
114
  ]"
115
115
  >
116
116
  <ComboboxItemIndicator
117
- class="absolute left-0 w-[25px] inline-flex items-center justify-center opacity-30"
117
+ :class="['absolute', 'left-0', 'w-[25px]', 'inline-flex', 'items-center', 'justify-center', 'opacity-30']"
118
118
  >
119
119
  <div i-solar:alt-arrow-right-outline />
120
120
  </ComboboxItemIndicator>
121
- <span class="line-clamp-1 overflow-hidden text-ellipsis whitespace-nowrap">
121
+ <span :class="['line-clamp-1', 'overflow-hidden', 'text-ellipsis', 'whitespace-nowrap']">
122
122
  {{ option.label }}
123
123
  </span>
124
124
  </ComboboxItem>
@@ -10,9 +10,9 @@ const modelValue = defineModel<boolean>({ required: true })
10
10
  </script>
11
11
 
12
12
  <template>
13
- <label flex="~ col gap-4">
14
- <div flex="~ row" items-center gap-2>
15
- <div flex="1">
13
+ <label class="flex flex-col gap-4">
14
+ <div class="flex flex-row items-center gap-2">
15
+ <div class="flex-1">
16
16
  <div class="flex items-center gap-1 text-sm font-medium">
17
17
  <slot name="label">
18
18
  {{ props.label }}
@@ -17,8 +17,8 @@ const modelValue = defineModel<string>({ required: false })
17
17
  </script>
18
18
 
19
19
  <template>
20
- <div max-w-full>
21
- <label flex="~ col gap-4">
20
+ <div class="max-w-full">
21
+ <label class="flex flex-col gap-4">
22
22
  <div>
23
23
  <div class="flex items-center gap-1 text-sm font-medium">
24
24
  <slot name="label">
@@ -44,14 +44,16 @@ const modelValue = defineModel<string>({ required: false })
44
44
  v-model="modelValue"
45
45
  :type="props.type"
46
46
  :placeholder="props.placeholder"
47
- :class="props.inputClass"
48
- border="focus:primary-300 dark:focus:primary-400/50 2 solid neutral-100 dark:neutral-900"
49
- transition="all duration-200 ease-in-out"
50
- text="disabled:neutral-400 dark:disabled:neutral-600"
51
- cursor="disabled:not-allowed"
52
- w-full rounded-lg px-2 py-1 text-sm outline-none
53
- shadow="sm"
54
- bg="neutral-50 dark:neutral-950 focus:neutral-50 dark:focus:neutral-900"
47
+ :class="[
48
+ props.inputClass,
49
+ 'focus:primary-300 dark:focus:primary-400/50 border-2 border-solid border-neutral-100 dark:border-neutral-900',
50
+ 'transition-all duration-200 ease-in-out',
51
+ 'text-disabled:neutral-400 dark:text-disabled:neutral-600',
52
+ 'cursor-disabled:not-allowed',
53
+ 'w-full rounded-lg px-2 py-1 text-sm outline-none',
54
+ 'shadow-sm',
55
+ 'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
56
+ ]"
55
57
  />
56
58
  </label>
57
59
  </div>
@@ -28,8 +28,8 @@ watch([inputKey, inputValue], () => {
28
28
  </script>
29
29
 
30
30
  <template>
31
- <div max-w-full>
32
- <label flex="~ col gap-2">
31
+ <div class="max-w-full">
32
+ <label class="flex flex-col gap-2">
33
33
  <div>
34
34
  <div class="flex items-center gap-1 text-sm font-medium">
35
35
  <slot name="label">
@@ -43,18 +43,18 @@ watch([inputKey, inputValue], () => {
43
43
  </slot>
44
44
  </div>
45
45
  </div>
46
- <div v-auto-animate flex="~ col gap-2">
46
+ <div v-auto-animate class="flex flex-col gap-2">
47
47
  <div
48
48
  v-for="(keyValue, index) in keyValues"
49
49
  :key="index"
50
- w-full flex items-center gap-2
50
+ class="w-full flex items-center gap-2"
51
51
  >
52
52
  <InputKeyValue
53
53
  v-model:property-key="keyValue.key"
54
54
  v-model:property-value="keyValue.value"
55
55
  :key-placeholder="props.keyPlaceholder"
56
56
  :value-placeholder="props.valuePlaceholder"
57
- w-full
57
+ class="w-full"
58
58
  />
59
59
  <button @click="emit('remove', index)">
60
60
  <div i-solar:minus-circle-line-duotone size="6" />
@@ -17,29 +17,29 @@ const modelValue = defineModel<number>({ required: true })
17
17
  </script>
18
18
 
19
19
  <template>
20
- <props.as flex="~ col gap-4">
21
- <div flex="~ row" items-center gap-2>
22
- <div flex="1">
23
- <div class="flex items-center gap-1 text-sm font-medium">
20
+ <props.as :class="['flex flex-col gap-4']">
21
+ <div :class="['flex', 'flex-row', 'items-center', 'gap-2']">
22
+ <div :class="['flex-1']">
23
+ <div :class="['flex', 'items-center', 'gap-1', 'text-sm', 'font-medium']">
24
24
  <slot name="label">
25
25
  {{ label }}
26
26
  </slot>
27
27
  </div>
28
- <div class="text-xs text-neutral-500 dark:text-neutral-400">
28
+ <div :class="['text-xs', 'text-neutral-500', 'dark:text-neutral-400']">
29
29
  <slot name="description">
30
30
  {{ description }}
31
31
  </slot>
32
32
  </div>
33
33
  </div>
34
- <span font-mono>{{ props.formatValue?.(modelValue) || modelValue }}</span>
34
+ <span :class="['font-mono']">{{ props.formatValue?.(modelValue) || modelValue }}</span>
35
35
  </div>
36
- <div flex="~ row" items-center gap-2>
36
+ <div :class="['flex', 'flex-row', 'items-center', 'gap-2']">
37
37
  <Range
38
38
  v-model="modelValue"
39
39
  :min="min || 0"
40
40
  :max="max || 1"
41
41
  :step="step || 0.01"
42
- w-full
42
+ :class="['w-full']"
43
43
  />
44
44
  </div>
45
45
  </props.as>
@@ -17,25 +17,26 @@ const modelValue = defineModel<string>({ required: false })
17
17
  </script>
18
18
 
19
19
  <template>
20
- <label flex="~ col gap-4">
20
+ <label :class="['flex', 'flex-col', 'gap-4']">
21
21
  <div
22
- class="items-center justify-center"
23
22
  :class="[
23
+ 'items-center',
24
+ 'justify-center',
24
25
  props.layout === 'horizontal' ? 'grid grid-cols-3 gap-2' : 'grid grid-cols-2 gap-2',
25
26
  ]"
26
27
  >
27
28
  <div
28
- class="w-full"
29
29
  :class="[
30
+ 'w-full',
30
31
  props.layout === 'horizontal' ? 'col-span-2' : 'row-span-1',
31
32
  ]"
32
33
  >
33
- <div class="flex items-center gap-1 break-words text-sm font-medium">
34
+ <div :class="['flex', 'items-center', 'gap-1', 'break-words', 'text-sm', 'font-medium']">
34
35
  <slot name="label">
35
36
  {{ props.label }}
36
37
  </slot>
37
38
  </div>
38
- <div class="break-words text-xs text-neutral-500 dark:text-neutral-400">
39
+ <div :class="['break-words', 'text-xs', 'text-neutral-500', 'dark:text-neutral-400']">
39
40
  <slot name="description">
40
41
  {{ props.description }}
41
42
  </slot>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import Textarea from '../Textarea/Textarea.vue'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ label?: string
6
+ description?: string
7
+ placeholder?: string
8
+ required?: boolean
9
+ textareaClass?: string
10
+ rows?: number
11
+ }>(), {
12
+ rows: 6,
13
+ })
14
+
15
+ const modelValue = defineModel<string>({ required: false })
16
+ </script>
17
+
18
+ <template>
19
+ <div class="max-w-full">
20
+ <label class="flex flex-col gap-4">
21
+ <div>
22
+ <div class="flex items-center gap-1 text-sm font-medium">
23
+ <slot name="label">
24
+ {{ props.label }}
25
+ </slot>
26
+ <span v-if="props.required !== false" class="text-red-500">*</span>
27
+ </div>
28
+ <div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
29
+ <slot name="description">
30
+ {{ props.description }}
31
+ </slot>
32
+ </div>
33
+ </div>
34
+ <Textarea
35
+ v-model="modelValue"
36
+ :rows="props.rows"
37
+ :placeholder="props.placeholder"
38
+ :class="props.textareaClass"
39
+ />
40
+ </label>
41
+ </div>
42
+ </template>
@@ -29,16 +29,16 @@ function removeItem(index: number) {
29
29
  </script>
30
30
 
31
31
  <template>
32
- <div class="max-w-full">
33
- <label class="flex flex-col gap-2">
32
+ <div :class="['max-w-full']">
33
+ <label :class="['flex', 'flex-col', 'gap-2']">
34
34
  <div>
35
- <div class="flex items-center gap-1 text-sm font-medium">
35
+ <div :class="['flex', 'items-center', 'gap-1', 'text-sm', 'font-medium']">
36
36
  <slot name="label">
37
37
  {{ props.label }}
38
38
  </slot>
39
- <span v-if="props.required !== false" class="text-red-500">*</span>
39
+ <span v-if="props.required !== false" :class="['text-red-500']">*</span>
40
40
  </div>
41
- <div class="text-nowrap text-xs text-neutral-500 dark:text-neutral-400">
41
+ <div :class="['text-nowrap', 'text-xs', 'text-neutral-500', 'dark:text-neutral-400']">
42
42
  <slot name="description">
43
43
  {{ props.description }}
44
44
  </slot>
@@ -49,17 +49,17 @@ function removeItem(index: number) {
49
49
  <div
50
50
  v-for="(_, index) in items"
51
51
  :key="index"
52
- class="w-full flex items-center gap-2"
52
+ :class="['w-full', 'flex', 'items-center', 'gap-2']"
53
53
  >
54
54
  <Input
55
55
  v-model="items[index]"
56
56
  :placeholder="props.valuePlaceholder"
57
- class="w-90%"
57
+ :class="['w-90%']"
58
58
  />
59
- <button i-solar:minus-circle-line-duotone size="6" class="min-w-20px w-10% flex text-red-500" @click="removeItem(index)" />
59
+ <button i-solar:minus-circle-line-duotone size="6" :class="['min-w-20px', 'w-10%', 'flex', 'text-red-500']" @click="removeItem(index)" />
60
60
  </div>
61
61
 
62
- <div i-solar:add-circle-line-duotone size="6" class="mt-2 w-4/5 text-blue-500" @click="addItem" />
62
+ <div i-solar:add-circle-line-duotone size="6" :class="['mt-2', 'w-4/5', 'text-blue-500']" @click="addItem" />
63
63
  </div>
64
64
  </label>
65
65
  </div>
@@ -3,4 +3,5 @@ export { default as FieldInput } from './FieldInput.vue'
3
3
  export { default as FieldKeyValues } from './FieldKeyValues.vue'
4
4
  export { default as FieldRange } from './FieldRange.vue'
5
5
  export { default as FieldSelect } from './FieldSelect.vue'
6
+ export { default as FieldTextArea } from './FieldTextArea.vue'
6
7
  export { default as FieldValues } from './FieldValues.vue'
@@ -37,8 +37,9 @@ function handleFileChange(e: Event) {
37
37
 
38
38
  <template>
39
39
  <label
40
- relative cursor-pointer
41
40
  :class="[
41
+ 'relative',
42
+ 'cursor-pointer',
42
43
  props.class,
43
44
  isDragging
44
45
  ? [...Array.isArray(isDraggingClasses) ? isDraggingClasses : [isDraggingClasses]]
@@ -10,12 +10,14 @@ const modelValue = defineModel<string>({ required: false })
10
10
  <input
11
11
  v-model="modelValue"
12
12
  :type="props.type || 'text'"
13
- border="focus:primary-300 dark:focus:primary-400/50 2 solid neutral-100 dark:neutral-900"
14
- transition="all duration-200 ease-in-out"
15
- text="disabled:neutral-400 dark:disabled:neutral-600"
16
- cursor="disabled:not-allowed"
17
- w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
18
- shadow="sm"
19
- bg="neutral-50 dark:neutral-950 focus:neutral-50 dark:focus:neutral-900"
13
+ :class="[
14
+ 'focus:border-primary-300 dark:focus:border-primary-400/50 border-2 border-solid border-neutral-100 dark:border-neutral-900',
15
+ 'transition-all duration-200 ease-in-out',
16
+ 'text-disabled:neutral-400 dark:text-disabled:neutral-600',
17
+ 'cursor-disabled:not-allowed',
18
+ 'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
19
+ 'shadow-sm',
20
+ 'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
21
+ ]"
20
22
  >
21
23
  </template>
@@ -9,7 +9,13 @@ defineProps<{
9
9
 
10
10
  <template>
11
11
  <BasicInputFile
12
- class="min-h-[120px] flex flex-col cursor-pointer items-center justify-center rounded-xl p-6"
12
+ :class="[
13
+ 'min-h-[120px] flex flex-col cursor-pointer items-center justify-center rounded-xl p-6',
14
+ 'border-dashed border-2',
15
+ 'transition-all duration-300',
16
+ 'opacity-95',
17
+ 'hover:scale-100 hover:opacity-100 hover:shadow-md hover:dark:shadow-lg',
18
+ ]"
13
19
  :is-not-dragging-classes="[
14
20
  'border-neutral-200 dark:border-neutral-700 hover:border-primary-300 dark:hover:border-primary-700',
15
21
  'bg-white/60 dark:bg-black/30 hover:bg-white/80 dark:hover:bg-black/40',
@@ -18,11 +24,6 @@ defineProps<{
18
24
  'border-primary-400 dark:border-primary-600 hover:border-primary-300 dark:hover:border-primary-700',
19
25
  'bg-primary-50/5 dark:bg-primary-900/5',
20
26
  ]"
21
- border="dashed 2"
22
- transition="all duration-300"
23
- opacity-95
24
- hover="scale-100 opacity-100 shadow-md dark:shadow-lg"
25
-
26
27
  :accept="accept"
27
28
  :multiple="multiple"
28
29
  >
@@ -12,8 +12,8 @@ const value = defineModel<string>('propertyValue', { required: true })
12
12
  </script>
13
13
 
14
14
  <template>
15
- <div flex="~ gap-2">
16
- <Input v-model="key" :placeholder="props.keyPlaceholder" class="w-1/2" />
17
- <Input v-model="value" :placeholder="props.valuePlaceholder" class="w-1/2" />
15
+ <div :class="['flex', 'gap-2']">
16
+ <Input v-model="key" :placeholder="props.keyPlaceholder" :class="['w-1/2']" />
17
+ <Input v-model="value" :placeholder="props.valuePlaceholder" :class="['w-1/2']" />
18
18
  </div>
19
19
  </template>
@@ -15,10 +15,10 @@ const modelValue = defineModel<string>({ required: true })
15
15
  <template>
16
16
  <label
17
17
  :key="id"
18
- class="form_radio relative flex cursor-pointer items-start rounded-xl p-3 pr-[20px]"
19
- transition="all duration-200 ease-in-out"
20
- border="2 solid"
21
18
  :class="[
19
+ 'form_radio relative flex cursor-pointer items-start rounded-xl p-3 pr-[20px]',
20
+ 'transition-all duration-200 ease-in-out',
21
+ 'border-2 border-solid',
22
22
  modelValue === value
23
23
  ? 'bg-primary-50 dark:bg-primary-900/20 border-primary-100 dark:border-primary-900 hover:border-primary-500/30 dark:hover:border-primary-400/30'
24
24
  : 'bg-white dark:bg-neutral-900/20 border-neutral-100 dark:border-neutral-900 hover:border-primary-500/30 dark:hover:border-primary-400/30',
@@ -34,20 +34,20 @@ const modelValue = defineModel<string>({ required: true })
34
34
  type="radio"
35
35
  :name="name"
36
36
  :value="value"
37
- class="absolute opacity-0"
37
+ :class="['absolute opacity-0']"
38
38
  >
39
- <div class="relative mr-3 mt-0.5 flex-shrink-0">
39
+ <div :class="['relative mr-3 mt-0.5 flex-shrink-0']">
40
40
  <div
41
- class="size-5 border-2 rounded-full transition-colors duration-200"
42
41
  :class="[
42
+ 'size-5 border-2 rounded-full transition-colors duration-200',
43
43
  modelValue === value
44
44
  ? 'border-primary-500 dark:border-primary-400'
45
45
  : 'border-neutral-300 dark:border-neutral-600',
46
46
  ]"
47
47
  >
48
48
  <div
49
- class="absolute left-1/2 top-1/2 size-3 rounded-full transition-opacity duration-200 -translate-x-1/2 -translate-y-1/2"
50
49
  :class="[
50
+ 'absolute left-1/2 top-1/2 size-3 rounded-full transition-opacity duration-200 -translate-x-1/2 -translate-y-1/2',
51
51
  modelValue === value
52
52
  ? 'opacity-100 bg-primary-500 dark:bg-primary-400'
53
53
  : 'opacity-0',
@@ -55,11 +55,11 @@ const modelValue = defineModel<string>({ required: true })
55
55
  />
56
56
  </div>
57
57
  </div>
58
- <div class="w-full flex flex-col gap-2">
59
- <div class="flex items-center">
58
+ <div :class="['w-full flex flex-col gap-2']">
59
+ <div :class="['flex items-center']">
60
60
  <span
61
- class="line-clamp-1 font-medium"
62
61
  :class="[
62
+ 'line-clamp-1 font-medium',
63
63
  modelValue === value
64
64
  ? 'text-neutral-700 dark:text-neutral-300'
65
65
  : 'text-neutral-700 dark:text-neutral-400',
@@ -1,23 +1,32 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
2
4
  const props = defineProps<{
3
5
  disabled?: boolean
4
6
  class?: string
5
7
  }>()
6
8
 
7
- const colorValue = defineModel<string>('colorValue', {
8
- type: String,
9
- default: '',
9
+ const modelValue = defineModel<number>({ required: true })
10
+
11
+ const sliderValue = computed({
12
+ get: () => modelValue.value,
13
+ set: (value: number) => {
14
+ if (Number.isNaN(value))
15
+ return
16
+
17
+ modelValue.value = value
18
+ },
10
19
  })
11
20
  </script>
12
21
 
13
22
  <template>
14
23
  <input
15
- v-model="colorValue"
24
+ v-model.number="sliderValue"
16
25
  type="range" min="0" max="360" step="0.01"
17
- class="color-hue-range"
18
- transition="all ease-in-out duration-250"
19
26
  :disabled="props.disabled"
20
27
  :class="[
28
+ 'color-hue-range',
29
+ 'transition-all ease-in-out duration-250',
21
30
  props.disabled ? 'opacity-25 cursor-not-allowed' : 'cursor-pointer',
22
31
  props.class || '',
23
32
  ]"
@@ -7,6 +7,7 @@ const props = defineProps<{
7
7
 
8
8
  const events = defineEmits<{
9
9
  (event: 'submit', message: string): void
10
+ (event: 'pasteFile', files: File[]): void
10
11
  }>()
11
12
 
12
13
  const input = defineModel<string>({
@@ -23,6 +24,17 @@ function onKeyDown(e: KeyboardEvent) {
23
24
  }
24
25
  }
25
26
 
27
+ function onPaste(e: ClipboardEvent) {
28
+ if (!e.clipboardData)
29
+ return
30
+
31
+ const { files } = e.clipboardData
32
+ if (files.length > 0) {
33
+ e.preventDefault()
34
+ events('pasteFile', Array.from(files))
35
+ }
36
+ }
37
+
26
38
  // javascript - Creating a textarea with auto-resize - Stack Overflow
27
39
  // https://stackoverflow.com/questions/454202/creating-a-textarea-with-auto-resize
28
40
  watch(input, () => {
@@ -46,5 +58,6 @@ watch(input, () => {
46
58
  v-model="input"
47
59
  :style="{ height: textareaHeight }"
48
60
  @keydown="onKeyDown"
61
+ @paste="onPaste"
49
62
  />
50
63
  </template>
@@ -7,12 +7,14 @@ const modelValue = defineModel<string>({ default: '' })
7
7
  <template>
8
8
  <BasicTextarea
9
9
  v-model="modelValue"
10
- border="focus:primary-300 dark:focus:primary-400/50 2 solid neutral-100 dark:neutral-900"
11
- transition="all duration-200 ease-in-out"
12
- text="disabled:neutral-400 dark:disabled:neutral-600"
13
- cursor="disabled:not-allowed"
14
- w-full rounded-lg px-2 py-1 text-sm outline-none
15
- shadow="sm"
16
- bg="neutral-50 dark:neutral-950 focus:neutral-50 dark:focus:neutral-900"
10
+ :class="[
11
+ 'focus:border-primary-300 dark:focus:border-primary-400/50 border-2 border-solid border-neutral-100 dark:border-neutral-900',
12
+ 'transition-all duration-200 ease-in-out',
13
+ 'text-disabled:neutral-400 dark:text-disabled:neutral-600',
14
+ 'cursor-disabled:not-allowed',
15
+ 'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
16
+ 'shadow-sm',
17
+ 'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
18
+ ]"
17
19
  />
18
20
  </template>
@@ -0,0 +1,20 @@
1
+ import { onMounted, ref } from 'vue'
2
+
3
+ export function useDeferredMount() {
4
+ const isReady = ref(false)
5
+
6
+ onMounted(() => {
7
+ // NOTICE: yield the main thread to the browser to paint.
8
+ // Other approaches work too, e.g. double RAF like here, or a setTimeout 0, or Suspense with async component, etc.
9
+ // Virtualization somewhat helps, however, virtual list may break Ctrl-F and Ctrl-A.
10
+ requestAnimationFrame(() => {
11
+ requestAnimationFrame(() => {
12
+ isReady.value = true
13
+ })
14
+ })
15
+ })
16
+
17
+ return {
18
+ isReady,
19
+ }
20
+ }
@@ -0,0 +1,14 @@
1
+ import { useDark, useToggle } from '@vueuse/core'
2
+
3
+ const isDark = useDark({
4
+ disableTransition: true,
5
+ })
6
+
7
+ const toggleDark = useToggle(isDark)
8
+
9
+ export function useTheme() {
10
+ return {
11
+ isDark,
12
+ toggleDark,
13
+ }
14
+ }
@@ -0,0 +1,15 @@
1
+ :root {
2
+ --chromatic-hue: 220.44;
3
+ --chromatic-chroma: calc(0.18 + (cos(var(--chromatic-hue) * 3.14159265 / 180) * 0.04));
4
+ --chromatic-chroma-50: calc(var(--chromatic-chroma) * 0.3);
5
+ --chromatic-chroma-100: calc(var(--chromatic-chroma) * 0.5);
6
+ --chromatic-chroma-200: calc(var(--chromatic-chroma) * 0.6);
7
+ --chromatic-chroma-300: calc(var(--chromatic-chroma) * 0.75);
8
+ --chromatic-chroma-400: calc(var(--chromatic-chroma) * 0.85);
9
+ --chromatic-chroma-500: var(--chromatic-chroma);
10
+ --chromatic-chroma-600: calc(var(--chromatic-chroma) * 1.15);
11
+ --chromatic-chroma-700: calc(var(--chromatic-chroma) * 1.1);
12
+ --chromatic-chroma-800: calc(var(--chromatic-chroma) * 0.85);
13
+ --chromatic-chroma-900: calc(var(--chromatic-chroma) * 0.7);
14
+ --chromatic-chroma-950: calc(var(--chromatic-chroma) * 0.5);
15
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,6 @@
1
+ import './fallback.css'
2
+
1
3
  export * from './components/Animations'
2
4
  export * from './components/Form'
5
+ export * from './composables/use-deferred-mount'
6
+ export * from './composables/use-theme'
package/tsconfig.json CHANGED
@@ -32,7 +32,6 @@
32
32
  "include": [
33
33
  "src/**/*.ts",
34
34
  "src/**/*.d.ts",
35
- "src/**/*.mts",
36
35
  "src/**/*.vue"
37
36
  ],
38
37
  "exclude": [