@globalbrain/sefirot 3.0.0 → 3.2.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.
@@ -86,7 +86,7 @@ function emitBlur() {
86
86
  autocomplete="off"
87
87
  :value="inputValue"
88
88
  :disabled="disabled"
89
- v-on="disabled || inputEvents"
89
+ v-on="disabled ? {} : inputEvents"
90
90
  @blur="emitBlur"
91
91
  >
92
92
  </DatePicker>
@@ -0,0 +1,239 @@
1
+ <script setup lang="ts">
2
+ import { type IconifyIcon } from '@iconify/vue/dist/offline'
3
+ import IconImage from '@iconify-icons/ph/image-bold'
4
+ import { computed, ref } from 'vue'
5
+ import { useImageSrcFromFile } from '../composables/Image'
6
+ import { type Validatable } from '../composables/Validation'
7
+ import SButton from './SButton.vue'
8
+ import SIcon from './SIcon.vue'
9
+ import SInputBase from './SInputBase.vue'
10
+
11
+ export type Size = 'mini' | 'small' | 'medium'
12
+ export type CheckColor = 'neutral' | 'mute' | 'info' | 'success' | 'warning' | 'danger'
13
+ export type ImageType = 'rectangle' | 'circle'
14
+
15
+ const props = withDefaults(defineProps<{
16
+ size?: Size
17
+ label?: string
18
+ info?: string
19
+ note?: string
20
+ help?: string
21
+ checkIcon?: IconifyIcon
22
+ checkText?: string
23
+ checkColor?: CheckColor
24
+ imageType?: ImageType
25
+ imageWidth?: string
26
+ imageAspectRatio?: string
27
+ selectText?: string
28
+ removeText?: string
29
+ accept?: string
30
+ nullable?: boolean
31
+ disabled?: boolean
32
+ value?: File | string | null
33
+ modelValue?: File | string | null
34
+ hideError?: boolean
35
+ validation?: Validatable
36
+ }>(), {
37
+ imageType: 'rectangle',
38
+ imageWidth: '96px',
39
+ imageAspectRatio: '1 / 1',
40
+ selectText: 'Select image',
41
+ removeText: 'Remove image',
42
+ nullable: true
43
+ })
44
+
45
+ const emit = defineEmits<{
46
+ (e: 'update:model-value', value: File | null): void
47
+ (e: 'change', value: File | null): void
48
+ }>()
49
+
50
+ const fileInput = ref<HTMLInputElement | null>(null)
51
+
52
+ const _value = computed(() => {
53
+ return (props.modelValue !== undefined)
54
+ ? props.modelValue
55
+ : props.value !== undefined ? props.value : null
56
+ })
57
+
58
+ const { src: imageSrc } = useImageSrcFromFile(_value)
59
+
60
+ function openFileSelect() {
61
+ fileInput.value!.click()
62
+ }
63
+
64
+ function onFileSelect(e: Event) {
65
+ const file = ((e.target as HTMLInputElement).files ?? [])[0]
66
+
67
+ emit('update:model-value', file ?? null)
68
+ emit('change', file ?? null)
69
+
70
+ file && props.validation?.$touch()
71
+ }
72
+
73
+ function onFileDelete() {
74
+ fileInput.value!.value = ''
75
+
76
+ emit('update:model-value', null)
77
+ emit('change', null)
78
+ }
79
+ </script>
80
+
81
+ <template>
82
+ <SInputBase
83
+ class="SInputImage"
84
+ :class="[size]"
85
+ :label="label"
86
+ :note="note"
87
+ :info="info"
88
+ :check-icon="checkIcon"
89
+ :check-text="checkText"
90
+ :check-color="checkColor"
91
+ :hide-error="hideError"
92
+ :validation="validation"
93
+ >
94
+ <template #default>
95
+ <input
96
+ ref="fileInput"
97
+ class="file-input"
98
+ type="file"
99
+ :accept="accept"
100
+ @change="onFileSelect"
101
+ >
102
+
103
+ <div class="container">
104
+ <div class="image" :class="[imageType]">
105
+ <div v-if="imageSrc" class="image-fill">
106
+ <img class="image-fill-src" :src="imageSrc">
107
+ </div>
108
+ <div v-else class="image-empty">
109
+ <div class="image-empty-inner">
110
+ <SIcon class="image-empty-icon" :icon="IconImage" />
111
+ </div>
112
+ </div>
113
+ </div>
114
+ <div class="control">
115
+ <div class="actions">
116
+ <SButton
117
+ size="small"
118
+ :label="selectText"
119
+ :disabled="disabled"
120
+ @click="openFileSelect"
121
+ />
122
+ <SButton
123
+ v-if="nullable && imageSrc"
124
+ size="small"
125
+ mode="danger"
126
+ :label="removeText"
127
+ :disabled="disabled"
128
+ @click="onFileDelete"
129
+ />
130
+ </div>
131
+ <p v-if="help" class="help">
132
+ {{ help }}
133
+ </p>
134
+ </div>
135
+ </div>
136
+ </template>
137
+
138
+ <template v-if="$slots.info" #info>
139
+ <slot name="info" />
140
+ </template>
141
+ </SInputBase>
142
+ </template>
143
+
144
+ <style scoped lang="postcss">
145
+ .file-input {
146
+ display: none;
147
+ }
148
+
149
+ .container {
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 12px;
153
+ padding-top: 4px;
154
+
155
+ @media (min-width: 640px) {
156
+ flex-direction: row;
157
+ align-items: center;
158
+ gap: 24px;
159
+ }
160
+ }
161
+
162
+ .image {
163
+ display: flex;
164
+ flex-shrink: 0;
165
+ width: var(--input-image-width, v-bind(imageWidth));
166
+ aspect-ratio: v-bind(imageAspectRatio);
167
+ overflow: hidden;
168
+ }
169
+
170
+ .image-fill {
171
+ display: flex;
172
+ flex-shrink: 0;
173
+ width: 100%;
174
+ height: 100%;
175
+ overflow: hidden;
176
+ }
177
+
178
+ .image-fill-src {
179
+ width: 100%;
180
+ height: 100%;
181
+ object-fit: cover;
182
+ }
183
+
184
+ .image-empty {
185
+ display: flex;
186
+ border: 1px solid var(--c-border-mute-1);
187
+ padding: 8px;
188
+ width: 100%;
189
+ height: 100%;
190
+ }
191
+
192
+ .image-empty-inner {
193
+ display: flex;
194
+ justify-content: center;
195
+ align-items: center;
196
+ border: 1px dashed var(--c-border-mute-1);
197
+ width: 100%;
198
+ height: 100%;
199
+ }
200
+
201
+ .image-empty-icon {
202
+ width: 24px;
203
+ height: 24px;
204
+ color: var(--c-text-3);
205
+ }
206
+
207
+ .control {
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: 8px;
211
+ }
212
+
213
+ .actions {
214
+ display: flex;
215
+ gap: 8px;
216
+ }
217
+
218
+ .help {
219
+ margin: 0;
220
+ line-height: 20px;
221
+ font-size: 12px;
222
+ color: var(--c-text-2);
223
+ }
224
+
225
+ .image.rectangle .image-fill,
226
+ .image.rectangle .image-empty {
227
+ border-radius: 6px;
228
+ }
229
+
230
+ .image.rectangle .image-empty-inner {
231
+ border-radius: 3px;
232
+ }
233
+
234
+ .image.circle .image-fill,
235
+ .image.circle .image-empty,
236
+ .image.circle .image-empty-inner {
237
+ border-radius: 50%;
238
+ }
239
+ </style>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { useIntersectionObserver } from '@vueuse/core'
3
+ import { ref } from 'vue'
4
+
5
+ export interface Props {
6
+ as?: string
7
+ x?: string
8
+ y?: string
9
+ opacity?: string | number
10
+ duration?: string
11
+ delay?: string
12
+ once?: boolean
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ as: 'div',
17
+ x: '0',
18
+ y: '0',
19
+ opacity: 1,
20
+ duration: '0.75s',
21
+ delay: '0s',
22
+ once: true
23
+ })
24
+
25
+ const target = ref<HTMLElement | null>(null)
26
+ const on = ref(false)
27
+
28
+ const { stop } = useIntersectionObserver(target, ([{ isIntersecting }]) => {
29
+ on.value = isIntersecting
30
+ if (on.value && props.once) {
31
+ stop()
32
+ }
33
+ })
34
+ </script>
35
+
36
+ <template>
37
+ <component :is="as" class="SM" :class="{ on }" ref="target">
38
+ <slot :on="on" />
39
+ </component>
40
+ </template>
41
+
42
+ <style scoped lang="postcss">
43
+ .SM {
44
+ position: relative;
45
+ opacity: v-bind(opacity);
46
+ transform: translate(v-bind(x), v-bind(y));
47
+ transition: opacity, transform;
48
+ transition-duration: v-bind(duration);
49
+ transition-delay: v-bind(delay);
50
+ }
51
+
52
+ .SM.on {
53
+ opacity: 1;
54
+ transform: translate(0, 0);
55
+ }
56
+ </style>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ import SM, { type Props } from './SM.vue'
3
+
4
+ withDefaults(defineProps<Props>(), {
5
+ opacity: 0
6
+ })
7
+ </script>
8
+
9
+ <template>
10
+ <SM class="SMFade" v-bind="$props" v-slot="{ on }">
11
+ <slot :on="on" />
12
+ </SM>
13
+ </template>
@@ -0,0 +1,46 @@
1
+ import { type MaybeRefOrGetter, type Ref, ref, toValue, watchEffect } from 'vue'
2
+ import { isFile, isString } from '../support/Utils'
3
+
4
+ export interface ImageSrcFromFile {
5
+ src: Ref<string | null>
6
+ }
7
+
8
+ /**
9
+ * Get renderable image src from the given file. If a string is given, it will
10
+ * be returned as is. It is useful when you want to render an image from a
11
+ * remote URL.
12
+ */
13
+ export function useImageSrcFromFile(
14
+ file: MaybeRefOrGetter<File | string | null>
15
+ ): ImageSrcFromFile {
16
+ const reader = new FileReader()
17
+ const src = ref<string | null>(null)
18
+
19
+ reader.onload = function () {
20
+ src.value = this.result?.toString() ?? null
21
+ }
22
+
23
+ watchEffect(() => {
24
+ read()
25
+ })
26
+
27
+ function read(): void {
28
+ const f = toValue(file)
29
+
30
+ if (isFile(f)) {
31
+ reader.readAsDataURL(f)
32
+ return
33
+ }
34
+
35
+ if (isString(f)) {
36
+ src.value = f
37
+ return
38
+ }
39
+
40
+ src.value = null
41
+ }
42
+
43
+ return {
44
+ src
45
+ }
46
+ }
@@ -409,7 +409,7 @@
409
409
 
410
410
  :root {
411
411
  --button-mini-font-size: 12px;
412
- --button-small-font-size: 14px;
412
+ --button-small-font-size: 13px;
413
413
  --button-medium-font-size: 14px;
414
414
  --button-large-font-size: 14px;
415
415
  --button-jumbo-font-size: 16px;
@@ -17,3 +17,7 @@ export function isArray(value: unknown): value is unknown[] {
17
17
  export function isObject(value: unknown): value is Record<string, any> {
18
18
  return typeof value === 'object' && value !== null && !isArray(value)
19
19
  }
20
+
21
+ export function isFile(value: unknown): value is File {
22
+ return value instanceof File
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "packageManager": "pnpm@8.8.0",
5
5
  "description": "Vue Components for Global Brain Design System.",
6
6
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",