@fastkit/vui 0.7.46 → 0.7.47

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.
@@ -0,0 +1,278 @@
1
+ import './VBusyImage.scss';
2
+
3
+ import {
4
+ defineComponent,
5
+ HTMLAttributes,
6
+ ImgHTMLAttributes,
7
+ PropType,
8
+ computed,
9
+ ref,
10
+ watch,
11
+ onMounted,
12
+ onBeforeUnmount,
13
+ CSSProperties,
14
+ } from 'vue';
15
+ import { IN_WINDOW, loadImage } from '@fastkit/helpers';
16
+ import { defineSlotsProps } from '@fastkit/vue-utils';
17
+
18
+ export declare type ImgHTMLAttributesPropOptions = {
19
+ [K in keyof ImgHTMLAttributes]-?: PropType<ImgHTMLAttributes[K]>;
20
+ };
21
+
22
+ const IMAGE_ATTRS = [
23
+ 'alt',
24
+ 'crossorigin',
25
+ 'decoding',
26
+ 'sizes',
27
+ 'src',
28
+ 'srcset',
29
+ 'usemap',
30
+ ] as const;
31
+
32
+ export function splitAttrs(attrs: ImgHTMLAttributes): {
33
+ el: HTMLAttributes;
34
+ img: ImgHTMLAttributes;
35
+ } {
36
+ const el: HTMLAttributes = { ...attrs };
37
+ const img: ImgHTMLAttributes = {};
38
+
39
+ for (const ATTR of IMAGE_ATTRS) {
40
+ const value = attrs[ATTR];
41
+ if (value != null) {
42
+ img[ATTR] = value as any;
43
+ }
44
+ }
45
+
46
+ return {
47
+ el,
48
+ img,
49
+ };
50
+ }
51
+
52
+ export type BusyImageLoadState = 'pending' | 'loading' | 'loaded' | 'error';
53
+
54
+ export type BusyImageSizeValue = number | string;
55
+
56
+ const NUMBERISH_PROP = [Number, String] as PropType<BusyImageSizeValue>;
57
+
58
+ const DEFAULT_HEIGHT = 150;
59
+
60
+ const resolveBusyImageSizeValue = (
61
+ source?: BusyImageSizeValue,
62
+ ): string | undefined => {
63
+ if (source == null) return;
64
+ return isNaN(source as number) ? String(source) : `${source}px`;
65
+ };
66
+
67
+ export interface BusyImageRect {
68
+ width?: string;
69
+ height?: string;
70
+ }
71
+
72
+ export interface BusyImageRects {
73
+ placeholder: BusyImageRect;
74
+ main: BusyImageRect;
75
+ }
76
+
77
+ /**
78
+ * 画像表示用コンポーネント
79
+ * <img /> タグのように振る舞うが、ローディング表示や、表示サイズの調整など、ちょっといい感じにやるためのコンポーネント
80
+ */
81
+ export const VBusyImage = defineComponent({
82
+ name: 'VBusyImage',
83
+ props: {
84
+ ...(undefined as unknown as ImgHTMLAttributesPropOptions),
85
+ aspectRatio: String,
86
+ width: NUMBERISH_PROP,
87
+ height: NUMBERISH_PROP,
88
+ cover: Boolean,
89
+ ...defineSlotsProps<{
90
+ default: void;
91
+ }>(),
92
+ },
93
+ emits: {
94
+ load: (ev: Event) => true,
95
+ error: (ev: Event) => true,
96
+ },
97
+ setup(props, ctx) {
98
+ const loadStateRef = ref<BusyImageLoadState>(
99
+ props.cover && isAvairableSrc(ctx.attrs.src as any)
100
+ ? 'loading'
101
+ : 'pending',
102
+ );
103
+ const attrsRef = computed(() => splitAttrs(ctx.attrs));
104
+ const isPendingRef = computed(() => loadStateRef.value === 'pending');
105
+ const isLoadingRef = computed(() => loadStateRef.value === 'loading');
106
+ const isLoadedRef = computed(() => loadStateRef.value === 'loaded');
107
+ const isErrorRef = computed(() => loadStateRef.value === 'error');
108
+ const classesRef = computed(() => [
109
+ `v-busy-image--${loadStateRef.value}`,
110
+ {
111
+ 'v-busy-image--cover': props.cover,
112
+ 'v-busy-image--clickable': typeof ctx.attrs.onClick === 'function',
113
+ },
114
+ ]);
115
+ const nodeRef = ref<HTMLImageElement>();
116
+ const coverImageRef = ref<HTMLImageElement>();
117
+ const rectsRef = computed<BusyImageRects>(() => {
118
+ const placeholder: BusyImageRect = {};
119
+ const main: BusyImageRect = {};
120
+ const { cover } = props;
121
+ let { width, height } = props;
122
+
123
+ if (height == null && cover) {
124
+ height = DEFAULT_HEIGHT;
125
+ }
126
+
127
+ if (width == null && cover) {
128
+ width = height;
129
+ }
130
+
131
+ width = resolveBusyImageSizeValue(width);
132
+ height = resolveBusyImageSizeValue(height);
133
+
134
+ main.width = width;
135
+ main.height = height;
136
+
137
+ placeholder.width = width;
138
+ placeholder.height = height;
139
+
140
+ if (placeholder.height == null) {
141
+ placeholder.height = resolveBusyImageSizeValue(DEFAULT_HEIGHT);
142
+ }
143
+
144
+ if (placeholder.width == null) {
145
+ placeholder.width = placeholder.height;
146
+ }
147
+
148
+ return {
149
+ placeholder,
150
+ main,
151
+ };
152
+ });
153
+
154
+ const currentRectRef = computed<BusyImageRect>(() =>
155
+ isLoadedRef.value ? rectsRef.value.main : rectsRef.value.placeholder,
156
+ );
157
+
158
+ const stylesRef = computed<CSSProperties>(() => {
159
+ const { value: coverImage } = coverImageRef;
160
+ const coverSrc = coverImage && coverImage.src;
161
+ const styles: CSSProperties = {
162
+ ...currentRectRef.value,
163
+ };
164
+ if (coverSrc) {
165
+ styles.backgroundImage = `url(${coverSrc})`;
166
+ }
167
+ return styles;
168
+ });
169
+
170
+ let booted = false;
171
+
172
+ watch(
173
+ () => ctx.attrs.src as string | null | undefined,
174
+ (value) => {
175
+ coverImageRef.value = undefined;
176
+ if (isAvairableSrc(value)) {
177
+ loadStateRef.value = 'loading';
178
+ if (props.cover) {
179
+ loadImage(value)
180
+ .then((image) => {
181
+ coverImageRef.value = image;
182
+ setManualState('load');
183
+ })
184
+ .catch((err) => {
185
+ setManualState('error');
186
+ });
187
+ }
188
+ } else {
189
+ loadStateRef.value = 'pending';
190
+ }
191
+ },
192
+ { immediate: IN_WINDOW },
193
+ );
194
+
195
+ const setManualState = (loadType: 'load' | 'error') => {
196
+ loadStateRef.value = loadType === 'load' ? 'loaded' : 'error';
197
+
198
+ const ev = new Event(loadType, {
199
+ bubbles: false,
200
+ cancelable: false,
201
+ composed: false,
202
+ });
203
+
204
+ ctx.emit(loadType as any, ev);
205
+ };
206
+
207
+ onMounted(() => {
208
+ if (booted || props.cover) return;
209
+
210
+ const image = nodeRef.value;
211
+ if (!image) return;
212
+
213
+ const loadType = imageIsReady(image);
214
+
215
+ if (loadType) {
216
+ setManualState(loadType);
217
+ }
218
+ });
219
+
220
+ onBeforeUnmount(() => {
221
+ coverImageRef.value = undefined;
222
+ });
223
+
224
+ const handleLoad = (ev: Event) => {
225
+ booted = true;
226
+ loadStateRef.value = 'loaded';
227
+ ctx.emit('load', ev);
228
+ };
229
+
230
+ const handleError = (ev: Event) => {
231
+ booted = true;
232
+ loadStateRef.value = 'error';
233
+ ctx.emit('error', ev);
234
+ };
235
+
236
+ ctx.expose({
237
+ state: loadStateRef,
238
+ isPending: isPendingRef,
239
+ isLoading: isLoadingRef,
240
+ isLoaded: isLoadedRef,
241
+ isError: isErrorRef,
242
+ });
243
+
244
+ return () => {
245
+ const { el: elAttrs, img: imgAttrs } = attrsRef.value;
246
+ const styles = stylesRef.value;
247
+ const children = ctx.slots.default && ctx.slots.default();
248
+
249
+ return (
250
+ <span
251
+ class={['v-busy-image', classesRef.value]}
252
+ {...elAttrs}
253
+ style={styles}>
254
+ {!props.cover && (
255
+ <img
256
+ key="img"
257
+ ref={nodeRef}
258
+ class="v-busy-image__node"
259
+ {...imgAttrs}
260
+ onLoad={handleLoad}
261
+ onError={handleError}
262
+ />
263
+ )}
264
+ {!!children && <span class="v-busy-image__slot">{children}</span>}
265
+ </span>
266
+ );
267
+ };
268
+ },
269
+ });
270
+
271
+ function imageIsReady(image: HTMLImageElement): 'load' | 'error' | false {
272
+ if (!image.complete) return false;
273
+ return image.naturalWidth + image.naturalHeight > 0 ? 'load' : 'error';
274
+ }
275
+
276
+ function isAvairableSrc(src: string | null | undefined): src is string {
277
+ return typeof src === 'string' && src !== '';
278
+ }
@@ -0,0 +1 @@
1
+ export * from './VBusyImage';
Binary file
@@ -0,0 +1,78 @@
1
+ @use '../../styles/core.scss';
2
+
3
+ .v-chip {
4
+ @include core.button-reset();
5
+
6
+ $chip-sizes: xs, sm, md, lg, xl;
7
+
8
+ --chip-radius: calc(var(--chip-height) / 2);
9
+
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ height: var(--chip-height);
14
+ padding: 0 var(--chip-padding);
15
+ overflow: hidden;
16
+ font-size: var(--chip-font-size);
17
+ font-weight: var(--chip-font-weight);
18
+ line-height: 1;
19
+ text-align: center;
20
+ white-space: nowrap;
21
+ vertical-align: bottom;
22
+ cursor: default;
23
+ border-style: solid;
24
+ border-width: 1px;
25
+ border-radius: var(--chip-radius);
26
+
27
+ --shadow-color: rgba(0, 0, 0, 0) !important; // @TODO layer supports
28
+ --focusShadow-color: rgba(0, 0, 0, 0) !important; // @TODO layer supports
29
+
30
+ &--clickable {
31
+ cursor: pointer;
32
+ }
33
+
34
+ @each $size in $chip-sizes {
35
+ &--#{$size} {
36
+ --chip-height: var(--chip-height-#{$size});
37
+ --chip-font-size: var(--chip-font-size-#{$size});
38
+ }
39
+ }
40
+
41
+ &--label {
42
+ --chip-radius: 4px;
43
+ }
44
+
45
+ // &[disabled] {
46
+ // pointer-events: none;
47
+
48
+ // &::before {
49
+ // content: none;
50
+ // }
51
+ // }
52
+ // // noop
53
+
54
+ // img,
55
+ // &__image {
56
+ // display: inline-flex;
57
+ // width: inherit !important;
58
+ // height: inherit !important;
59
+ // border-radius: inherit;
60
+ // object-fit: cover;
61
+ // }
62
+
63
+ &__icon {
64
+ &--start {
65
+ margin-right: calc(var(--chip-padding) * 0.5);
66
+ margin-left: calc(var(--chip-padding) * -0.5);
67
+ }
68
+
69
+ &--end {
70
+ margin-right: calc(var(--chip-padding) * -0.5);
71
+ margin-left: calc(var(--chip-padding) * 0.5);
72
+ }
73
+ }
74
+
75
+ .v-avatar {
76
+ --avatar-size: 14px;
77
+ }
78
+ }
@@ -0,0 +1,102 @@
1
+ import './VChip.scss';
2
+ import { defineComponent, computed, PropType, VNodeChild } from 'vue';
3
+ import {
4
+ navigationableInheritProps,
5
+ useNavigationable,
6
+ renderSlotOrEmpty,
7
+ } from '@fastkit/vue-utils';
8
+ import { colorSchemeProps, useColorClasses } from '@fastkit/vue-color-scheme';
9
+ import { useVui } from '../../injections';
10
+ import type { IconName } from '../VIcon';
11
+ import { VIcon } from '../VIcon';
12
+
13
+ export const CHIP_SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
14
+
15
+ export type ChipSize = typeof CHIP_SIZES[number];
16
+
17
+ export type VChipIcon = () => VNodeChild;
18
+
19
+ export type RawVChipIcon = IconName | VChipIcon;
20
+
21
+ export function createChipProps() {
22
+ return {
23
+ ...colorSchemeProps(),
24
+ ...navigationableInheritProps,
25
+ size: {
26
+ type: String as PropType<ChipSize>,
27
+ default: 'md',
28
+ },
29
+ startIcon: [String, Function] as PropType<RawVChipIcon>,
30
+ endIcon: [String, Function] as PropType<RawVChipIcon>,
31
+ label: Boolean,
32
+ disabled: Boolean,
33
+ };
34
+ }
35
+
36
+ function resolveRawVChipIcon(
37
+ raw: RawVChipIcon | undefined,
38
+ type: 'start' | 'end',
39
+ ): VChipIcon | undefined {
40
+ if (!raw) return;
41
+ if (typeof raw === 'function') {
42
+ return () => {
43
+ const child = raw();
44
+ if (child == null) return;
45
+ return (
46
+ <span class={['v-chip__icon', `v-chip__icon--${type}`]}>{child}</span>
47
+ );
48
+ };
49
+ }
50
+ return () => (
51
+ <VIcon class={['v-chip__icon', `v-chip__icon--${type}`]} name={raw} />
52
+ );
53
+ }
54
+
55
+ export const VChip = defineComponent({
56
+ name: 'VChip',
57
+ inheritAttrs: false,
58
+ props: createChipProps(),
59
+ setup(props, ctx) {
60
+ const vui = useVui();
61
+ const defaults = vui.setting('buttonDefault');
62
+ const color = useColorClasses({
63
+ color: () => props.color || defaults.color,
64
+ variant: () => props.variant || defaults.variant,
65
+ });
66
+ const startIcon = computed(() => {
67
+ const _icon = resolveRawVChipIcon(props.startIcon, 'start');
68
+ return _icon && _icon();
69
+ });
70
+ const endIcon = computed(() => {
71
+ const _icon = resolveRawVChipIcon(props.endIcon, 'end');
72
+ return _icon && _icon();
73
+ });
74
+ const navigationable = useNavigationable(ctx, {
75
+ clickableClassName: () => 'v-chip--clickable',
76
+ linkFallbackTag: 'div',
77
+ });
78
+
79
+ const classes = computed(() => {
80
+ const { size } = props;
81
+ return [
82
+ {
83
+ 'v-chip--label': props.label,
84
+ },
85
+ typeof size === 'string' && `v-chip--${size}`,
86
+ ];
87
+ });
88
+
89
+ return () => {
90
+ const { Tag, attrs } = navigationable.value;
91
+ return (
92
+ <Tag
93
+ {...attrs}
94
+ class={['v-chip', classes.value, color.colorClasses.value]}>
95
+ {startIcon.value}
96
+ <span class="v-chip__content">{renderSlotOrEmpty(ctx.slots)}</span>
97
+ {endIcon.value}
98
+ </Tag>
99
+ );
100
+ };
101
+ },
102
+ });
@@ -0,0 +1 @@
1
+ export * from './VChip';
@@ -0,0 +1,31 @@
1
+ .v-skelton-loader-bone {
2
+ position: relative;
3
+ overflow: hidden;
4
+ cursor: progress;
5
+ background: rgba(0, 0, 0, 0.12);
6
+ border-radius: inherit;
7
+
8
+ &::after {
9
+ position: absolute;
10
+ top: 0;
11
+ right: 0;
12
+ left: 0;
13
+ z-index: 1;
14
+ height: 100%;
15
+ content: '';
16
+ background: linear-gradient(
17
+ 90deg,
18
+ hsla(0deg, 0%, 100%, 0),
19
+ hsla(0deg, 0%, 100%, 0.3),
20
+ hsla(0deg, 0%, 100%, 0)
21
+ );
22
+ transform: translateX(-100%);
23
+ animation: v-skelton-loader-bone-loading 1.5s infinite;
24
+ }
25
+ }
26
+
27
+ @keyframes v-skelton-loader-bone-loading {
28
+ 100% {
29
+ transform: translateX(100%);
30
+ }
31
+ }
@@ -0,0 +1,25 @@
1
+ import './VSkeltonLoaderBone.scss';
2
+
3
+ import { defineComponent } from 'vue';
4
+ import { renderSlotOrEmpty } from '@fastkit/vue-utils';
5
+
6
+ export const VSkeltonLoaderBone = defineComponent({
7
+ name: 'VSkeltonLoaderBone',
8
+ inheritAttrs: false,
9
+ props: {
10
+ tag: {
11
+ type: String,
12
+ default: 'div',
13
+ },
14
+ },
15
+ setup(props, ctx) {
16
+ return () => {
17
+ const TagName = props.tag as 'div';
18
+ return (
19
+ <TagName {...ctx.attrs} class="v-skelton-loader-bone">
20
+ {renderSlotOrEmpty(ctx.slots)}
21
+ </TagName>
22
+ );
23
+ };
24
+ },
25
+ });
@@ -0,0 +1 @@
1
+ export * from './VSkeltonLoaderBone';
@@ -1,7 +1,11 @@
1
1
  export * from './loading';
2
2
  export * from './kits';
3
+ export * from './VSkeltonLoader';
4
+ export * from './VBusyImage';
3
5
  export * from './VGrid';
4
6
  export * from './VPaper';
7
+ export * from './VAvatar';
8
+ export * from './VChip';
5
9
  export * from './VCard';
6
10
  export * from './VIcon';
7
11
  export * from './VButton';
@@ -30,6 +30,9 @@
30
30
  --typo-base-height: 1.5;
31
31
  --typo-base-spacing: 0em;
32
32
 
33
+ // medium
34
+ --typo-medium-weight: 500;
35
+
33
36
  // h1
34
37
  --typo-h1-size: 2.5rem;
35
38
  --typo-h1-weight: 500;
@@ -123,6 +126,22 @@
123
126
  // --typo-overline-height: 2.66;
124
127
  // --typo-overline-spacing: 0.08333em;
125
128
 
129
+ // =============================================
130
+ // chips
131
+ // =============================================
132
+ --chip-padding: 0.875em;
133
+ --chip-font-weight: var(--typo-base-weight);
134
+ --chip-height-xs: 16px;
135
+ --chip-font-size-xs: 10px;
136
+ --chip-height-sm: 24px;
137
+ --chip-font-size-sm: 12px;
138
+ --chip-height-md: 32px;
139
+ --chip-font-size-md: 14px;
140
+ --chip-height-lg: 54px;
141
+ --chip-font-size-lg: 16px;
142
+ --chip-height-xl: 66px;
143
+ --chip-font-size-xl: 18px;
144
+
126
145
  // =============================================
127
146
  // controls
128
147
  // =============================================