@proj-airi/ui 0.8.1-beta.2 → 0.8.1-beta.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.
Files changed (62) hide show
  1. package/package.json +4 -3
  2. package/src/components/animations/index.ts +9 -0
  3. package/src/components/{Form/Checkbox/Checkbox.vue → form/checkbox/checkbox.vue} +1 -0
  4. package/src/components/form/checkbox/index.ts +1 -0
  5. package/src/components/form/combobox/index.ts +1 -0
  6. package/src/components/{Form/Field/FieldCheckbox.vue → form/field/field-checkbox.vue} +1 -1
  7. package/src/components/{Form/Field/FieldInput.vue → form/field/field-input.vue} +3 -3
  8. package/src/components/{Form/Field/FieldKeyValues.vue → form/field/field-key-values.vue} +2 -2
  9. package/src/components/{Form/Field/FieldRange.vue → form/field/field-range.vue} +1 -1
  10. package/src/components/{Form/Field/FieldSelect.vue → form/field/field-select.vue} +1 -1
  11. package/src/components/{Form/Field/FieldTextArea.vue → form/field/field-text-area.vue} +2 -2
  12. package/src/components/{Form/Field/FieldValues.vue → form/field/field-values.vue} +1 -1
  13. package/src/components/form/field/index.ts +7 -0
  14. package/src/components/form/index.ts +9 -0
  15. package/src/components/form/input/index.ts +4 -0
  16. package/src/components/{Form/Input/InputFile.vue → form/input/input-file.vue} +1 -1
  17. package/src/components/{Form/Input/InputKeyValue.vue → form/input/input-key-value.vue} +1 -1
  18. package/src/components/form/input/input.vue +71 -0
  19. package/src/components/form/radio/index.ts +1 -0
  20. package/src/components/form/range/index.ts +3 -0
  21. package/src/components/form/select/index.ts +2 -0
  22. package/src/components/{Form/Select/Select.vue → form/select/select.vue} +1 -1
  23. package/src/components/form/select-tab/index.ts +1 -0
  24. package/src/components/{Form/SelectTab/SelectTab.vue → form/select-tab/select-tab.vue} +1 -0
  25. package/src/components/form/textarea/index.ts +2 -0
  26. package/src/components/{Form/Textarea/Textarea.vue → form/textarea/textarea.vue} +1 -1
  27. package/src/components/layouts/collapsible.vue +39 -0
  28. package/src/components/layouts/index.ts +3 -0
  29. package/src/components/layouts/screen.vue +72 -0
  30. package/src/components/layouts/skeleton.vue +75 -0
  31. package/src/components/misc/button.vue +174 -0
  32. package/src/components/misc/callout.vue +77 -0
  33. package/src/components/{Misc/DoubleCheckButton.vue → misc/double-check-button.vue} +1 -1
  34. package/src/components/misc/index.ts +4 -0
  35. package/src/components/misc/progress.vue +44 -0
  36. package/src/fallback.css +4 -0
  37. package/src/index.ts +4 -3
  38. package/src/components/Animations/index.ts +0 -3
  39. package/src/components/Form/Checkbox/index.ts +0 -1
  40. package/src/components/Form/Combobox/index.ts +0 -1
  41. package/src/components/Form/Field/index.ts +0 -7
  42. package/src/components/Form/Input/Input.vue +0 -23
  43. package/src/components/Form/Input/index.ts +0 -4
  44. package/src/components/Form/Radio/index.ts +0 -1
  45. package/src/components/Form/Range/index.ts +0 -3
  46. package/src/components/Form/Select/index.ts +0 -2
  47. package/src/components/Form/SelectTab/index.ts +0 -1
  48. package/src/components/Form/Textarea/index.ts +0 -2
  49. package/src/components/Form/index.ts +0 -9
  50. package/src/components/Misc/Button.vue +0 -118
  51. package/src/components/Misc/index.ts +0 -2
  52. /package/src/components/{Animations/BidirectionalTransition.vue → animations/transition-bidirectional.vue} +0 -0
  53. /package/src/components/{Animations/TransitionHorizontal.vue → animations/transition-horizontal.vue} +0 -0
  54. /package/src/components/{Animations/TransitionVertical.vue → animations/transition-vertical.vue} +0 -0
  55. /package/src/components/{Form/Combobox/Combobox.vue → form/combobox/combobox.vue} +0 -0
  56. /package/src/components/{Form/Input/BasicInputFile.vue → form/input/basic-input-file.vue} +0 -0
  57. /package/src/components/{Form/Radio/Radio.vue → form/radio/radio.vue} +0 -0
  58. /package/src/components/{Form/Range/ColorHueRange.vue → form/range/color-hue-range.vue} +0 -0
  59. /package/src/components/{Form/Range/Range.vue → form/range/range.vue} +0 -0
  60. /package/src/components/{Form/Range/RoundRange.vue → form/range/round-range.vue} +0 -0
  61. /package/src/components/{Form/Select/Option.vue → form/select/option.vue} +0 -0
  62. /package/src/components/{Form/Textarea/Basic.vue → form/textarea/basic-text-area.vue} +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proj-airi/ui",
3
3
  "type": "module",
4
- "version": "0.8.1-beta.2",
4
+ "version": "0.8.1-beta.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",
@@ -16,8 +16,9 @@
16
16
  },
17
17
  "exports": {
18
18
  ".": "./src/index.ts",
19
- "./components/animations": "./src/components/Animations/index.ts",
20
- "./components/form": "./src/components/Form/index.ts",
19
+ "./components/animations": "./src/components/animations/index.ts",
20
+ "./components/form": "./src/components/form/index.ts",
21
+ "./components/layouts": "./src/components/layouts/index.ts",
21
22
  "./*": "./*"
22
23
  },
23
24
  "dependencies": {
@@ -0,0 +1,9 @@
1
+ export { default as TransitionBidirectional } from './transition-bidirectional.vue'
2
+ export {
3
+ /**
4
+ * @deprecated Use TransitionBidirectional instead
5
+ */
6
+ default as BidirectionalTransition,
7
+ } from './transition-bidirectional.vue'
8
+ export { default as TransitionHorizontal } from './transition-horizontal.vue'
9
+ export { default as TransitionVertical } from './transition-vertical.vue'
@@ -11,6 +11,7 @@ const modelValue = defineModel<boolean>({ required: true })
11
11
  'duration-250 ease-in-out',
12
12
  'focus-within:outline-none',
13
13
  'flex',
14
+ 'is-interacting',
14
15
  '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
16
  '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
17
  'relative h-7 w-12.5 rounded-full',
@@ -0,0 +1 @@
1
+ export { default as Checkbox } from './checkbox.vue'
@@ -0,0 +1 @@
1
+ export { default as Combobox } from './combobox.vue'
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Checkbox from '../Checkbox/Checkbox.vue'
2
+ import { Checkbox } from '../checkbox'
3
3
 
4
4
  const props = defineProps<{
5
5
  label?: string
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Input from '../Input/Input.vue'
2
+ import { Input } from '../input'
3
3
 
4
4
  const props = withDefaults(defineProps<{
5
5
  label?: string
@@ -24,9 +24,9 @@ const modelValue = defineModel<string>({ required: false })
24
24
  <slot name="label">
25
25
  {{ props.label }}
26
26
  </slot>
27
- <span v-if="props.required !== false" class="text-red-500">*</span>
27
+ <span v-if="props.required" class="text-red-500">*</span>
28
28
  </div>
29
- <div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
29
+ <div class="text-xs text-neutral-500 dark:text-neutral-400" text-wrap>
30
30
  <slot name="description">
31
31
  {{ props.description }}
32
32
  </slot>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch } from 'vue'
3
3
 
4
- import InputKeyValue from '../Input/InputKeyValue.vue'
4
+ import { InputKeyValue } from '../input'
5
5
 
6
6
  const props = defineProps<{
7
7
  label?: string
@@ -37,7 +37,7 @@ watch([inputKey, inputValue], () => {
37
37
  </slot>
38
38
  <span v-if="props.required !== false" class="text-red-500">*</span>
39
39
  </div>
40
- <div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
40
+ <div class="text-xs text-neutral-500 dark:text-neutral-400">
41
41
  <slot name="description">
42
42
  {{ props.description }}
43
43
  </slot>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Range from '../Range/Range.vue'
2
+ import { Range } from '../range'
3
3
 
4
4
  const props = withDefaults(defineProps<{
5
5
  min?: number
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { Select } from '@proj-airi/ui'
2
+ import { Select } from '../select'
3
3
 
4
4
  const props = withDefaults(defineProps<{
5
5
  label: string
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Textarea from '../Textarea/Textarea.vue'
2
+ import { Textarea } from '../textarea'
3
3
 
4
4
  const props = withDefaults(defineProps<{
5
5
  label?: string
@@ -25,7 +25,7 @@ const modelValue = defineModel<string>({ required: false })
25
25
  </slot>
26
26
  <span v-if="props.required !== false" class="text-red-500">*</span>
27
27
  </div>
28
- <div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
28
+ <div class="text-xs text-neutral-500 dark:text-neutral-400">
29
29
  <slot name="description">
30
30
  {{ props.description }}
31
31
  </slot>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Input from '../Input/Input.vue'
2
+ import { Input } from '../input'
3
3
 
4
4
  const props = defineProps<{
5
5
  label?: string
@@ -0,0 +1,7 @@
1
+ export { default as FieldCheckbox } from './field-checkbox.vue'
2
+ export { default as FieldInput } from './field-input.vue'
3
+ export { default as FieldKeyValues } from './field-key-values.vue'
4
+ export { default as FieldRange } from './field-range.vue'
5
+ export { default as FieldSelect } from './field-select.vue'
6
+ export { default as FieldTextArea } from './field-text-area.vue'
7
+ export { default as FieldValues } from './field-values.vue'
@@ -0,0 +1,9 @@
1
+ export * from './checkbox'
2
+ export * from './combobox'
3
+ export * from './field'
4
+ export * from './input'
5
+ export * from './radio'
6
+ export * from './range'
7
+ export * from './select'
8
+ export * from './select-tab'
9
+ export * from './textarea'
@@ -0,0 +1,4 @@
1
+ export { default as BasicInputFile } from './basic-input-file.vue'
2
+ export { default as InputFile } from './input-file.vue'
3
+ export { default as InputKeyValue } from './input-key-value.vue'
4
+ export { default as Input } from './input.vue'
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import BasicInputFile from './BasicInputFile.vue'
2
+ import BasicInputFile from './basic-input-file.vue'
3
3
 
4
4
  defineProps<{
5
5
  accept?: string
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import Input from './Input.vue'
2
+ import Input from './input.vue'
3
3
 
4
4
  const props = defineProps<{
5
5
  name?: string
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ // Define button variants for better type safety and maintainability
3
+ type InputVariant = 'primary' | 'secondary' | 'primary-dimmed'
4
+
5
+ type InputTheme = 'default'
6
+
7
+ // Define size options for better flexibility
8
+ type InputSize = 'sm' | 'md' | 'lg'
9
+
10
+ const props = withDefaults(defineProps<{
11
+ type?: string
12
+ variant?: InputVariant // Button style variant
13
+ size?: InputSize // Button size variant
14
+ theme?: InputTheme // Button theme
15
+ }>(), {
16
+ variant: 'primary',
17
+ size: 'md',
18
+ theme: 'default',
19
+ })
20
+
21
+ const modelValue = defineModel<string>({ required: false })
22
+
23
+ const variantClasses: Record<InputVariant, Record<InputTheme, {
24
+ default: string[]
25
+ }>> = {
26
+ 'primary': {
27
+ default: {
28
+ default: [
29
+ 'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
30
+ 'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
31
+ 'focus:border-primary-300 dark:focus:border-primary-400/50 border-2 border-solid border-neutral-100 dark:border-neutral-900',
32
+ 'text-disabled:neutral-400 dark:text-disabled:neutral-600',
33
+ 'shadow-sm',
34
+ ],
35
+ },
36
+ },
37
+ 'secondary': {
38
+ default: {
39
+ default: [
40
+ 'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
41
+ 'bg-neutral-50 dark:bg-neutral-950 focus:bg-neutral-50 dark:focus:bg-neutral-900',
42
+ 'focus:border-primary-300 dark:focus:border-primary-400/50 border-2 border-solid border-neutral-100 dark:border-neutral-900',
43
+ 'text-disabled:neutral-400 dark:text-disabled:neutral-600',
44
+ 'shadow-sm',
45
+ ],
46
+ },
47
+ },
48
+ 'primary-dimmed': {
49
+ default: {
50
+ default: [
51
+ 'w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none',
52
+ 'bg-neutral-100 dark:bg-neutral-800 focus:bg-neutral-50 dark:focus:bg-neutral-950',
53
+ 'focus:border-primary-500/30 dark:focus:border-primary-400/50 border-2 border-solid border-neutral-500/5 dark:border-neutral-700/40',
54
+ 'text-disabled:neutral-400 dark:text-disabled:neutral-600',
55
+ ],
56
+ },
57
+ },
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <input
63
+ v-model="modelValue"
64
+ :type="props.type || 'text'"
65
+ :class="[
66
+ 'transition-all duration-200 ease-in-out',
67
+ 'cursor-disabled:not-allowed',
68
+ ...variantClasses[props.variant][props.theme].default,
69
+ ]"
70
+ >
71
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Radio } from './radio.vue'
@@ -0,0 +1,3 @@
1
+ export { default as ColorHueRange } from './color-hue-range.vue'
2
+ export { default as Range } from './range.vue'
3
+ export { default as RoundRange } from './round-range.vue'
@@ -0,0 +1,2 @@
1
+ export { default as Option } from './option.vue'
2
+ export { default as Select } from './select.vue'
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { provide, ref } from 'vue'
3
3
 
4
- import Combobox from '../Combobox/Combobox.vue'
4
+ import { Combobox } from '../combobox'
5
5
 
6
6
  const props = defineProps<{
7
7
  options?: { label: string, value: string | number }[]
@@ -0,0 +1 @@
1
+ export { default as SelectTab } from './select-tab.vue'
@@ -48,6 +48,7 @@ const rootStyle = computed(() => ({
48
48
  :aria-readonly="props.readonly"
49
49
  :class="[
50
50
  'select-tab',
51
+ 'is-interacting',
51
52
  'relative', 'flex', 'w-full', 'items-stretch', 'rounded-lg',
52
53
  'overflow-hidden',
53
54
  'bg-white-400/6', 'dark:bg-neutral-950/70',
@@ -0,0 +1,2 @@
1
+ export { default as BasicTextarea } from './basic-text-area.vue'
2
+ export { default as Textarea } from './textarea.vue'
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import BasicTextarea from './Basic.vue'
2
+ import BasicTextarea from './basic-text-area.vue'
3
3
 
4
4
  const modelValue = defineModel<string>({ default: '' })
5
5
  </script>
@@ -0,0 +1,39 @@
1
+ <script lang="ts" setup>
2
+ import { watchEffect } from 'vue'
3
+
4
+ import { TransitionVertical } from '../animations'
5
+
6
+ const props = defineProps<{
7
+ default?: boolean
8
+ label?: string
9
+ }>()
10
+ const visible = defineModel<boolean>({ default: false })
11
+ watchEffect(() => {
12
+ if (props.default != null) {
13
+ visible.value = !!props.default
14
+ }
15
+ })
16
+
17
+ function setVisible(value: boolean) {
18
+ visible.value = value
19
+ return value
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <div>
25
+ <slot name="trigger" v-bind="{ visible, setVisible }">
26
+ <button
27
+ :class="['sticky top-0 z-10 flex items-center justify-between px2 py1 text-sm backdrop-blur-xl']"
28
+ @click="visible = !visible"
29
+ >
30
+ <span>
31
+ {{ props.label ?? 'Collapsable' }}
32
+ </span> <span op50>{{ visible ? '▲' : '▼' }}</span>
33
+ </button>
34
+ </slot>
35
+ <TransitionVertical>
36
+ <slot v-if="visible" v-bind="{ visible, setVisible }" />
37
+ </TransitionVertical>
38
+ </div>
39
+ </template>
@@ -0,0 +1,3 @@
1
+ export { default as Collapsible } from './collapsible.vue'
2
+ export { default as Screen } from './screen.vue'
3
+ export { default as Skeleton } from './skeleton.vue'
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import { breakpointsTailwind, useBreakpoints, useElementBounding, useWindowSize } from '@vueuse/core'
3
+ import { computed, onMounted, ref, watch } from 'vue'
4
+
5
+ const containerRef = ref<HTMLDivElement>()
6
+
7
+ const breakpoints = useBreakpoints(breakpointsTailwind)
8
+ const { width, height } = useWindowSize()
9
+ const containerElementBounding = useElementBounding(containerRef, { immediate: true, windowResize: true, reset: true })
10
+
11
+ const isMobile = computed(() => breakpoints.between('sm', 'md').value || breakpoints.smaller('sm').value)
12
+ const isTablet = computed(() => breakpoints.between('md', 'lg').value)
13
+ const isDesktop = computed(() => breakpoints.greaterOrEqual('lg').value)
14
+
15
+ const canvasWidth = computed(() => {
16
+ if (isDesktop.value)
17
+ return containerElementBounding.width.value
18
+ else if (isMobile.value)
19
+ return (width.value - 16) // padding
20
+ else if (isTablet.value)
21
+ return (width.value - 16) // padding
22
+ else
23
+ return containerElementBounding.width.value
24
+ })
25
+
26
+ const canvasHeight = ref(0)
27
+
28
+ watch([width, height, containerRef], () => {
29
+ const bounding = containerRef.value?.parentElement?.getBoundingClientRect()
30
+
31
+ if (isDesktop.value) {
32
+ canvasHeight.value = bounding?.height || 0
33
+ }
34
+ else if (isMobile.value) {
35
+ canvasHeight.value = bounding?.height || 0
36
+ }
37
+ else if (isTablet.value) {
38
+ canvasHeight.value = bounding?.height || 0
39
+ }
40
+ else {
41
+ canvasHeight.value = 600
42
+ }
43
+ })
44
+
45
+ watch([containerElementBounding.width, containerElementBounding.height], () => {
46
+ if (isDesktop.value) {
47
+ canvasHeight.value = containerElementBounding.height.value
48
+ }
49
+ else if (isMobile.value) {
50
+ canvasHeight.value = containerElementBounding.height.value
51
+ }
52
+ else if (isTablet.value) {
53
+ canvasHeight.value = containerElementBounding.height.value
54
+ }
55
+ else {
56
+ canvasHeight.value = 600
57
+ }
58
+ })
59
+
60
+ onMounted(async () => {
61
+ if (!containerRef.value)
62
+ return
63
+
64
+ containerElementBounding.update()
65
+ })
66
+ </script>
67
+
68
+ <template>
69
+ <div ref="containerRef" h-full w-full>
70
+ <slot :width="canvasWidth" :height="canvasHeight" />
71
+ </div>
72
+ </template>
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ animation?: 'pulse' | 'wave' | 'none'
4
+ }>(), {
5
+ animation: 'pulse',
6
+ })
7
+ </script>
8
+
9
+ <template>
10
+ <div
11
+ class="skeleton"
12
+ :class="props.animation !== 'none' ? `skeleton-${props.animation}` : ''"
13
+ bg="neutral-200 dark:neutral-800"
14
+ overflow="hidden"
15
+ >
16
+ <slot />
17
+ </div>
18
+ </template>
19
+
20
+ <style scoped>
21
+ .skeleton {
22
+ position: relative;
23
+ transition: all 0.2s ease-in-out;
24
+ }
25
+
26
+ /* Pulse animation */
27
+ .skeleton-pulse {
28
+ animation: skeleton-pulse 2s ease-in-out 0.5s infinite;
29
+ }
30
+
31
+ @keyframes skeleton-pulse {
32
+ 0% {
33
+ opacity: 1;
34
+ }
35
+ 50% {
36
+ opacity: 0.5;
37
+ }
38
+ 100% {
39
+ opacity: 1;
40
+ }
41
+ }
42
+
43
+ /* Wave animation */
44
+ .skeleton-wave::after {
45
+ content: '';
46
+ position: absolute;
47
+ top: 0;
48
+ right: 0;
49
+ bottom: 0;
50
+ left: 0;
51
+ transform: translateX(-100%);
52
+ background: linear-gradient(90deg, transparent, rgb(255, 255, 255), transparent);
53
+ animation: skeleton-wave 2s ease-in-out infinite;
54
+ border-radius: inherit;
55
+ }
56
+
57
+ .dark .skeleton-wave::after {
58
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
59
+ }
60
+
61
+ @keyframes skeleton-wave {
62
+ 0% {
63
+ transform: translateX(-100%);
64
+ opacity: 0;
65
+ }
66
+ 60% {
67
+ transform: translateX(100%);
68
+ opacity: 1;
69
+ }
70
+ 100% {
71
+ transform: translateX(100%);
72
+ opacity: 0;
73
+ }
74
+ }
75
+ </style>
@@ -0,0 +1,174 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ import { TransitionBidirectional } from '../animations'
5
+
6
+ // Define button variants for better type safety and maintainability
7
+ type ButtonVariant = 'primary' | 'secondary' | 'secondary-muted' | 'danger' | 'caution' | 'pure' | 'ghost'
8
+
9
+ type ButtonTheme = 'default'
10
+
11
+ // Define size options for better flexibility
12
+ type ButtonSize = 'sm' | 'md' | 'lg'
13
+
14
+ interface ButtonProps {
15
+ toggled?: boolean // Optional toggled state for toggle buttons
16
+ icon?: string // Icon class name
17
+ label?: string // Button text label
18
+ disabled?: boolean // Disabled state
19
+ loading?: boolean // Loading state
20
+ variant?: ButtonVariant // Button style variant
21
+ size?: ButtonSize // Button size variant
22
+ theme?: ButtonTheme // Button theme
23
+ block?: boolean // Full width button
24
+ }
25
+
26
+ const props = withDefaults(defineProps<ButtonProps>(), {
27
+ toggled: false,
28
+ variant: 'primary',
29
+ disabled: false,
30
+ loading: false,
31
+ size: 'md',
32
+ theme: 'default',
33
+ block: false,
34
+ })
35
+
36
+ const isDisabled = computed(() => props.disabled || props.loading)
37
+
38
+ // Extract variant styles for better organization
39
+ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
40
+ default: string[]
41
+ nonToggled?: string
42
+ toggled?: string
43
+ }>> = {
44
+ 'primary': {
45
+ default: {
46
+ default: [
47
+ 'rounded-lg',
48
+ 'backdrop-blur-md',
49
+ 'bg-primary-500/15 hover:bg-primary-500/20 active:bg-primary-500/30 dark:bg-primary-700/30 dark:hover:bg-primary-700/40 dark:active:bg-primary-700/30',
50
+ 'focus:ring-primary-300/60 dark:focus:ring-primary-600/30',
51
+ 'border-2 border-solid border-primary-500/5 dark:border-primary-900/40',
52
+ 'text-primary-950 dark:text-primary-100',
53
+ 'focus:ring-2',
54
+ ],
55
+ },
56
+ },
57
+ 'secondary': {
58
+ default: {
59
+ default: [
60
+ 'rounded-lg',
61
+ 'backdrop-blur-md',
62
+ 'bg-neutral-100/55 hover:bg-neutral-400/20 active:bg-neutral-400/30 dark:bg-neutral-700/60 dark:hover:bg-neutral-700/80 dark:active:bg-neutral-700/60',
63
+ 'focus:ring-neutral-300/30 dark:focus:ring-neutral-600/60 dark:focus:ring-neutral-600/30',
64
+ 'border-2 border-solid border-neutral-300/30 dark:border-neutral-700/30',
65
+ 'text-neutral-950 dark:text-neutral-100',
66
+ 'focus:ring-2',
67
+ ],
68
+ },
69
+ },
70
+ 'secondary-muted': {
71
+ default: {
72
+ default: [
73
+ 'rounded-lg',
74
+ 'backdrop-blur-md',
75
+ 'hover:bg-neutral-50/50 active:bg-neutral-50/90 hover:dark:bg-neutral-800/50 active:dark:bg-neutral-800/90',
76
+ 'border-2 border-solid border-neutral-100/60 dark:border-neutral-800/30',
77
+ 'focus:ring-2 focus:ring-neutral-300/30 dark:focus:ring-neutral-600/60 dark:focus:ring-neutral-600/30',
78
+ ],
79
+ nonToggled: 'bg-neutral-50/70 dark:bg-neutral-800/70 text-neutral-500 dark:text-neutral-400',
80
+ toggled: 'bg-white/90 dark:bg-neutral-500/70 ring-neutral-300/30 dark:ring-neutral-600/60 ring-2 dark:ring-neutral-600/30 text-primary-500 dark:text-primary-100',
81
+ },
82
+ },
83
+ 'danger': {
84
+ default: {
85
+ default: [
86
+ 'rounded-lg',
87
+ 'backdrop-blur-md',
88
+ 'bg-red-500/15 hover:bg-red-500/20 active:bg-red-500/30 dark:bg-red-700/30 dark:hover:bg-red-700/40 dark:active:bg-red-700/30',
89
+ 'focus:ring-2 focus:ring-red-300/30 dark:focus:ring-red-600/60 dark:focus:ring-red-600/30',
90
+ 'border-2 border-solid border-red-200/30 dark:border-red-900/30',
91
+ 'text-red-950 dark:text-red-100',
92
+ ],
93
+ },
94
+ },
95
+ 'caution': {
96
+ default: {
97
+ default: [
98
+ 'rounded-lg',
99
+ 'backdrop-blur-md',
100
+ 'bg-amber-400/20 hover:bg-amber-400/25 active:bg-amber-400/35 dark:bg-amber-500/20 dark:hover:bg-amber-500/30 dark:active:bg-amber-500/35',
101
+ 'focus:ring-2 focus:ring-amber-300/40 dark:focus:ring-amber-400/40',
102
+ 'border-2 border-solid border-amber-300/40 dark:border-amber-500/40',
103
+ 'text-amber-900 dark:text-amber-50',
104
+ ],
105
+ },
106
+ },
107
+ 'pure': {
108
+ default: {
109
+ default: [
110
+ 'bg-transparent',
111
+ 'text-neutral-900 dark:text-neutral-50',
112
+ '!px-0 !py-0',
113
+ ],
114
+ },
115
+ },
116
+ 'ghost': {
117
+ default: {
118
+ default: [
119
+ 'bg-transparent',
120
+ 'hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50',
121
+ 'text-neutral-500 dark:text-neutral-400',
122
+ 'focus:ring-2 focus:ring-neutral-300/30 dark:focus:ring-neutral-600/30',
123
+ ],
124
+ },
125
+ },
126
+ }
127
+
128
+ // Extract size styles for better organization
129
+ const sizeClasses: Record<ButtonSize, string> = {
130
+ sm: 'px-3 py-1.5 text-xs',
131
+ md: 'px-4 py-2 text-sm',
132
+ lg: 'px-6 py-3 text-base',
133
+ }
134
+
135
+ // Base classes that are always applied
136
+ const baseClasses = computed(() => {
137
+ const variant = variantClasses[props.variant] || variantClasses.primary
138
+ const theme = variant[props.theme] || variant.default
139
+
140
+ return [
141
+ 'rounded-lg font-medium outline-none',
142
+ 'transition-all duration-200 ease-in-out',
143
+ 'disabled:cursor-not-allowed disabled:opacity-50',
144
+ 'backdrop-blur-md',
145
+ props.block ? 'w-full' : '',
146
+ sizeClasses[props.size],
147
+ theme.default,
148
+ props.toggled ? theme.toggled || '' : theme.nonToggled || '',
149
+ { 'opacity-50 cursor-not-allowed': isDisabled.value },
150
+ 'focus:ring-2',
151
+ ]
152
+ })
153
+ </script>
154
+
155
+ <template>
156
+ <button
157
+ :disabled="isDisabled"
158
+ :class="baseClasses"
159
+ >
160
+ <div class="flex flex-row items-center justify-center gap-2">
161
+ <TransitionBidirectional
162
+ from-class="opacity-0 mr-0! w-0!"
163
+ active-class="transition-[width,margin] ease-in-out overflow-hidden transition-100"
164
+ >
165
+ <div v-if="loading || icon" class="w-4">
166
+ <div v-if="loading" class="i-svg-spinners:ring-resize h-4 w-4" />
167
+ <div v-else-if="icon" class="h-4 w-4" :class="icon" />
168
+ </div>
169
+ </TransitionBidirectional>
170
+ <span v-if="label">{{ label }}</span>
171
+ <slot v-else />
172
+ </div>
173
+ </button>
174
+ </template>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ type ThemeVariant = 'primary' | 'violet' | 'lime' | 'orange'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ theme?: ThemeVariant
6
+ label?: string
7
+ }>(), {
8
+ theme: 'primary',
9
+ })
10
+
11
+ const themeClasses: Record<ThemeVariant, {
12
+ container: string[]
13
+ label?: string[]
14
+ }> = {
15
+ primary: {
16
+ container: [
17
+ 'text-neutral-900/80 dark:text-neutral-100/80',
18
+ 'bg-primary-50/80 dark:bg-primary-900/50 backdrop-blur-md',
19
+ `before:bg-primary-500/30 before:content-[''] before:dark:bg-primary-200/20`,
20
+ ],
21
+ label: [
22
+ 'text-primary-500 dark:text-primary-200 font-semibold',
23
+ ],
24
+ },
25
+ lime: {
26
+ container: [
27
+ 'text-neutral-900/80 dark:text-neutral-100/80',
28
+ 'bg-lime-50/80 dark:bg-lime-900/50 backdrop-blur-md',
29
+ `before:bg-lime-500/30 before:content-[''] before:dark:bg-lime-200/20`,
30
+ ],
31
+ label: [
32
+ 'text-lime-500 dark:text-lime-200 font-semibold',
33
+ ],
34
+ },
35
+ violet: {
36
+ container: [
37
+ 'text-neutral-900/80 dark:text-neutral-100/80',
38
+ 'bg-violet-50/80 dark:bg-violet-900/50 backdrop-blur-md',
39
+ `before:bg-violet-500/30 before:content-[''] before:dark:bg-violet-200/20`,
40
+ ],
41
+ label: [
42
+ 'text-violet-500 dark:text-violet-200 font-semibold',
43
+ ],
44
+ },
45
+ orange: {
46
+ container: [
47
+ 'text-neutral-900/80 dark:text-neutral-100/80',
48
+ 'bg-orange-100/60 dark:bg-orange-900/50 backdrop-blur-md',
49
+ `before:bg-orange-500/30 before:content-[''] before:dark:bg-orange-200/20`,
50
+ ],
51
+ label: [
52
+ 'text-orange-500 dark:text-orange-200 font-semibold',
53
+ ],
54
+ },
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <div
60
+ relative
61
+ flex flex-col gap-1
62
+ rounded-lg
63
+ py="2.5" pl="5" pr-3
64
+ :class="[
65
+ ...themeClasses[props.theme || 'violet'].container,
66
+ // eslint-disable-next-line vue/prefer-separate-static-class
67
+ 'before-position-absolute before:left-2 before:right-0 before:h-[calc(100%-1rem)] before:top-50% before:translate-y--50% before:w-1 before:rounded-full',
68
+ ]"
69
+ >
70
+ <div text="font-semibold" :class="[...(themeClasses[props.theme || 'violet'].label || [])]">
71
+ <slot name="label">
72
+ {{ props.label || 'Callout' }}
73
+ </slot>
74
+ </div>
75
+ <slot />
76
+ </div>
77
+ </template>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, watch } from 'vue'
3
3
 
4
- import Button from './Button.vue'
4
+ import Button from './button.vue'
5
5
 
6
6
  type ButtonVariant = 'primary' | 'secondary' | 'secondary-muted' | 'danger' | 'caution'
7
7
  type ButtonSize = 'sm' | 'md' | 'lg'
@@ -0,0 +1,4 @@
1
+ export { default as Button } from './button.vue'
2
+ export { default as Callout } from './callout.vue'
3
+ export { default as DoubleCheckButton } from './double-check-button.vue'
4
+ export { default as Progress } from './progress.vue'
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ progress: number
4
+ barClass?: string
5
+ }>()
6
+ </script>
7
+
8
+ <template>
9
+ <div relative overflow-hidden rounded-md>
10
+ <div
11
+ :class="[barClass ? barClass : 'bg-primary-300 dark:bg-primary-300/50']"
12
+ absolute h-4 min-w-2 rounded-md will-change-width
13
+ :style="{ width: `${progress}%` }"
14
+ transition="width duration-500 ease-in-out"
15
+ >
16
+ <div
17
+ v-if="progress < 100"
18
+ absolute inset-0 origin-left rounded-md bg-white
19
+ class="progress-shine-animation"
20
+ />
21
+ </div>
22
+ <div
23
+ bg="neutral-100 dark:neutral-900" h-4 w-full rounded-md
24
+ />
25
+ </div>
26
+ </template>
27
+
28
+ <style scoped>
29
+ .progress-shine-animation {
30
+ animation: progress-shine 2s cubic-bezier(0.35, 0.08, 0.04, 0.99) infinite;
31
+ will-change: transform, opacity;
32
+ }
33
+
34
+ @keyframes progress-shine {
35
+ 0% {
36
+ opacity: 0.4;
37
+ transform: scale(0, 1);
38
+ }
39
+ 100% {
40
+ opacity: 0;
41
+ transform: scale(1, 1);
42
+ }
43
+ }
44
+ </style>
package/src/fallback.css CHANGED
@@ -13,3 +13,7 @@
13
13
  --chromatic-chroma-900: calc(var(--chromatic-chroma) * 0.7);
14
14
  --chromatic-chroma-950: calc(var(--chromatic-chroma) * 0.5);
15
15
  }
16
+
17
+ .is-interacting {
18
+ transition-property: none !important;
19
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import './fallback.css'
2
2
 
3
- export * from './components/Animations'
4
- export * from './components/Form'
5
- export * from './components/Misc'
3
+ export * from './components/animations'
4
+ export * from './components/form'
5
+ export * from './components/layouts'
6
+ export * from './components/misc'
6
7
  export * from './composables/use-deferred-mount'
7
8
  export * from './composables/use-theme'
@@ -1,3 +0,0 @@
1
- export { default as BidirectionalTransition } from './BidirectionalTransition.vue'
2
- export { default as TransitionHorizontal } from './TransitionHorizontal.vue'
3
- export { default as TransitionVertical } from './TransitionVertical.vue'
@@ -1 +0,0 @@
1
- export { default as Checkbox } from './Checkbox.vue'
@@ -1 +0,0 @@
1
- export { default as Combobox } from './Combobox.vue'
@@ -1,7 +0,0 @@
1
- export { default as FieldCheckbox } from './FieldCheckbox.vue'
2
- export { default as FieldInput } from './FieldInput.vue'
3
- export { default as FieldKeyValues } from './FieldKeyValues.vue'
4
- export { default as FieldRange } from './FieldRange.vue'
5
- export { default as FieldSelect } from './FieldSelect.vue'
6
- export { default as FieldTextArea } from './FieldTextArea.vue'
7
- export { default as FieldValues } from './FieldValues.vue'
@@ -1,23 +0,0 @@
1
- <script setup lang="ts">
2
- const props = defineProps<{
3
- type?: string
4
- }>()
5
-
6
- const modelValue = defineModel<string>({ required: false })
7
- </script>
8
-
9
- <template>
10
- <input
11
- v-model="modelValue"
12
- :type="props.type || 'text'"
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
- ]"
22
- >
23
- </template>
@@ -1,4 +0,0 @@
1
- export { default as BasicInputFile } from './BasicInputFile.vue'
2
- export { default as Input } from './Input.vue'
3
- export { default as InputFile } from './InputFile.vue'
4
- export { default as InputKeyValue } from './InputKeyValue.vue'
@@ -1 +0,0 @@
1
- export { default as Radio } from './Radio.vue'
@@ -1,3 +0,0 @@
1
- export { default as ColorHueRange } from './ColorHueRange.vue'
2
- export { default as Range } from './Range.vue'
3
- export { default as RoundRange } from './RoundRange.vue'
@@ -1,2 +0,0 @@
1
- export { default as Option } from './Option.vue'
2
- export { default as Select } from './Select.vue'
@@ -1 +0,0 @@
1
- export { default as SelectTab } from './SelectTab.vue'
@@ -1,2 +0,0 @@
1
- export { default as BasicTextarea } from './Basic.vue'
2
- export { default as Textarea } from './Textarea.vue'
@@ -1,9 +0,0 @@
1
- export * from './Checkbox'
2
- export * from './Combobox'
3
- export * from './Field'
4
- export * from './Input'
5
- export * from './Radio'
6
- export * from './Range'
7
- export * from './Select'
8
- export * from './SelectTab'
9
- export * from './Textarea'
@@ -1,118 +0,0 @@
1
- <script setup lang="ts">
2
- import { BidirectionalTransition } from '@proj-airi/ui'
3
- import { computed } from 'vue'
4
-
5
- // Define button variants for better type safety and maintainability
6
- type ButtonVariant = 'primary' | 'secondary' | 'secondary-muted' | 'danger' | 'caution' | 'pure'
7
-
8
- type ButtonTheme = 'default'
9
-
10
- // Define size options for better flexibility
11
- type ButtonSize = 'sm' | 'md' | 'lg'
12
-
13
- interface ButtonProps {
14
- toggled?: boolean // Optional toggled state for toggle buttons
15
- icon?: string // Icon class name
16
- label?: string // Button text label
17
- disabled?: boolean // Disabled state
18
- loading?: boolean // Loading state
19
- variant?: ButtonVariant // Button style variant
20
- size?: ButtonSize // Button size variant
21
- theme?: ButtonTheme // Button theme
22
- block?: boolean // Full width button
23
- }
24
-
25
- const props = withDefaults(defineProps<ButtonProps>(), {
26
- toggled: false,
27
- variant: 'primary',
28
- disabled: false,
29
- loading: false,
30
- size: 'md',
31
- theme: 'default',
32
- block: false,
33
- })
34
-
35
- const isDisabled = computed(() => props.disabled || props.loading)
36
-
37
- // Extract variant styles for better organization
38
- const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
39
- default: string
40
- nonToggled?: string
41
- toggled?: string
42
- }>> = {
43
- 'primary': {
44
- default: {
45
- default: 'bg-primary-500/15 hover:bg-primary-500/20 active:bg-primary-500/30 dark:bg-primary-700/30 dark:hover:bg-primary-700/40 dark:active:bg-primary-700/30 focus:ring-primary-300/60 dark:focus:ring-primary-600/30 border-2 border-solid border-primary-500/5 dark:border-primary-900/40 text-primary-950 dark:text-primary-100',
46
- },
47
- },
48
- 'secondary': {
49
- default: {
50
- default: 'bg-neutral-100/55 hover:bg-neutral-400/20 active:bg-neutral-400/30 dark:bg-neutral-700/60 dark:hover:bg-neutral-700/80 dark:active:bg-neutral-700/60 focus:ring-neutral-300/30 dark:focus:ring-neutral-600/60 dark:focus:ring-neutral-600/30 border-2 border-solid border-neutral-300/30 dark:border-neutral-700/30 text-neutral-950 dark:text-neutral-100',
51
- },
52
- },
53
- 'secondary-muted': {
54
- default: {
55
- default: 'hover:bg-neutral-50/50 active:bg-neutral-50/90 hover:dark:bg-neutral-800/50 active:dark:bg-neutral-800/90 border-2 border-solid border-neutral-100/60 dark:border-neutral-800/30 focus:ring-neutral-300/30 dark:focus:ring-neutral-600/60 dark:focus:ring-neutral-600/30',
56
- nonToggled: 'bg-neutral-50/70 dark:bg-neutral-800/70 text-neutral-500 dark:text-neutral-400',
57
- toggled: 'bg-white/90 dark:bg-neutral-500/70 ring-neutral-300/30 dark:ring-neutral-600/60 ring-2 dark:ring-neutral-600/30 text-primary-500 dark:text-primary-100',
58
- },
59
- },
60
- 'danger': {
61
- default: {
62
- default: 'bg-red-500/15 hover:bg-red-500/20 active:bg-red-500/30 dark:bg-red-700/30 dark:hover:bg-red-700/40 dark:active:bg-red-700/30 focus:ring-red-300/30 dark:focus:ring-red-600/60 dark:focus:ring-red-600/30 border-2 border-solid border-red-200/30 dark:border-red-900/30 text-red-950 dark:text-red-100',
63
- },
64
- },
65
- 'caution': {
66
- default: {
67
- default: 'bg-amber-400/20 hover:bg-amber-400/25 active:bg-amber-400/35 dark:bg-amber-500/20 dark:hover:bg-amber-500/30 dark:active:bg-amber-500/35 focus:ring-amber-300/40 dark:focus:ring-amber-400/40 border-2 border-solid border-amber-300/40 dark:border-amber-500/40 text-amber-900 dark:text-amber-50',
68
- },
69
- },
70
- 'pure': {
71
- default: {
72
- default: 'bg-white hover:bg-neutral-50 active:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:active:bg-neutral-700 border-2 border-solid border-neutral-100 dark:border-neutral-800 focus:ring-neutral-200/40 dark:focus:ring-neutral-600/40 text-neutral-900 dark:text-neutral-50',
73
- },
74
- },
75
- }
76
-
77
- // Extract size styles for better organization
78
- const sizeClasses: Record<ButtonSize, string> = {
79
- sm: 'px-3 py-1.5 text-xs',
80
- md: 'px-4 py-2 text-sm',
81
- lg: 'px-6 py-3 text-base',
82
- }
83
-
84
- // Base classes that are always applied
85
- const baseClasses = computed(() => [
86
- 'rounded-lg font-medium outline-none',
87
- 'transition-all duration-200 ease-in-out',
88
- 'disabled:cursor-not-allowed disabled:opacity-50',
89
- 'backdrop-blur-md',
90
- props.block ? 'w-full' : '',
91
- sizeClasses[props.size],
92
- variantClasses[props.variant][props.theme].default,
93
- props.toggled ? variantClasses[props.variant][props.theme].toggled || '' : variantClasses[props.variant][props.theme].nonToggled || '',
94
- { 'opacity-50 cursor-not-allowed': isDisabled.value },
95
- 'focus:ring-2',
96
- ])
97
- </script>
98
-
99
- <template>
100
- <button
101
- :disabled="isDisabled"
102
- :class="baseClasses"
103
- >
104
- <div class="flex flex-row items-center justify-center gap-2">
105
- <BidirectionalTransition
106
- from-class="opacity-0 mr-0! w-0!"
107
- active-class="transition-[width,margin] ease-in-out overflow-hidden transition-100"
108
- >
109
- <div v-if="loading || icon" class="w-4">
110
- <div v-if="loading" class="i-svg-spinners:ring-resize h-4 w-4" />
111
- <div v-else-if="icon" class="h-4 w-4" :class="icon" />
112
- </div>
113
- </BidirectionalTransition>
114
- <span v-if="label">{{ label }}</span>
115
- <slot v-else />
116
- </div>
117
- </button>
118
- </template>
@@ -1,2 +0,0 @@
1
- export { default as Button } from './Button.vue'
2
- export { default as DoubleCheckButton } from './DoubleCheckButton.vue'