sprintify-ui 0.6.33 → 0.6.34

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.
@@ -2,7 +2,7 @@
2
2
  <div ref="elementsRef">
3
3
  <slot
4
4
  v-for="(element, index) in modelValue"
5
- :key="element[itemKey]"
5
+ :key="getKey(element, index)"
6
6
  name="item"
7
7
  :element="element"
8
8
  :index="index"
@@ -25,6 +25,10 @@ const emit = defineEmits(['update:modelValue']);
25
25
 
26
26
  const elementsRef = ref<HTMLElement | null>(null);
27
27
 
28
+ function getKey(element: any, index: number) {
29
+ return element[props.itemKey] ?? index;
30
+ }
31
+
28
32
  let sortable = null as Sortable | null;
29
33
 
30
34
  onMounted(() => {
@@ -60,7 +60,7 @@ IconLeft.args = {
60
60
 
61
61
  export const IconRight = Template.bind({});
62
62
  IconRight.args = {
63
- iconRight: 'mdi:email-outline',
63
+ iconRight: 'mdi:email',
64
64
  placeholder: 'Enter your email',
65
65
  };
66
66
 
@@ -169,7 +169,7 @@ export const Field = createFieldStory({
169
169
  });
170
170
 
171
171
  const TemplateSizes = (args) => ({
172
- components: { BaseInput, ShowValue },
172
+ components: { BaseInput },
173
173
  setup() {
174
174
  const value = ref(null);
175
175
  const sizes = ['xs', 'sm', 'md'];
@@ -177,7 +177,7 @@ const TemplateSizes = (args) => ({
177
177
  },
178
178
  template: `
179
179
  <div v-for="size in sizes" :key="size" class="mb-4">
180
- <p class="text-xs text-slate-600 leading-tight mb-1">btn {{ size }}</p>
180
+ <p class="text-xs text-slate-600 leading-tight mb-1">{{ size }}</p>
181
181
  <BaseInput v-model="value" v-bind="args" :size="size" class="w-full"></BaseInput>
182
182
  </div>
183
183
  `,
@@ -1,18 +1,22 @@
1
1
  <template>
2
2
  <div :class="classes">
3
3
  <div :class="decorationWrapClasses">
4
- <div
4
+ <component
5
+ :is="hasLeftListener ? 'button' : 'div'"
5
6
  v-if="iconLeft"
6
- :class="decorationClasses"
7
+ :type="hasLeftListener ? 'button' : undefined"
8
+ :class="[decorationClasses, hasLeftListener ? 'hover:bg-slate-100' : '']"
9
+ @click="onIconLeftClickInternal"
7
10
  >
8
11
  <BaseIcon
9
12
  :icon="iconLeft"
10
13
  :class="iconClasses"
11
14
  />
12
- </div>
15
+ </component>
13
16
  <div
14
17
  v-if="prefix"
15
18
  :class="decorationClasses"
19
+ @click="focusAction"
16
20
  >
17
21
  {{ prefix }}
18
22
  </div>
@@ -47,18 +51,22 @@
47
51
  <div
48
52
  v-if="suffix"
49
53
  :class="decorationClasses"
54
+ @click="focusAction"
50
55
  >
51
56
  {{ suffix }}
52
57
  </div>
53
- <div
58
+ <component
59
+ :is="hasRightListener ? 'button' : 'div'"
54
60
  v-if="iconRight"
55
- :class="decorationClasses"
61
+ :type="hasRightListener ? 'button' : undefined"
62
+ :class="[decorationClasses, hasRightListener ? 'hover:bg-slate-100' : '']"
63
+ @click="onIconRightClickInternal"
56
64
  >
57
65
  <BaseIcon
58
66
  :icon="iconRight"
59
67
  :class="iconClasses"
60
68
  />
61
- </div>
69
+ </component>
62
70
  </div>
63
71
  </div>
64
72
  </template>
@@ -162,6 +170,14 @@ const props = defineProps({
162
170
  default: true,
163
171
  type: Boolean,
164
172
  },
173
+ onIconLeftClick: {
174
+ default: undefined,
175
+ type: Function as PropType<() => void>,
176
+ },
177
+ onIconRightClick: {
178
+ default: undefined,
179
+ type: Function as PropType<() => void>,
180
+ },
165
181
  });
166
182
 
167
183
  const input = ref<HTMLInputElement | null>(null);
@@ -175,6 +191,14 @@ const hasRightDecoration = computed(() => {
175
191
  return props.iconRight || props.suffix;
176
192
  });
177
193
 
194
+ const hasLeftListener = computed(() => {
195
+ return props.onIconLeftClick !== undefined;
196
+ });
197
+
198
+ const hasRightListener = computed(() => {
199
+ return props.onIconRightClick !== undefined;
200
+ });
201
+
178
202
  const maskOptions = computed(() => {
179
203
  if (props.mask) {
180
204
  return {
@@ -287,9 +311,9 @@ const baseClasses = computed(() => {
287
311
  const disabled = props.disabled ? 'cursor-not-allowed text-slate-300' : '';
288
312
 
289
313
  const paddingX = {
290
- xs: [hasLeftDecoration.value ? 'pl-1.5' : 'pl-2', hasRightDecoration.value ? 'pr-1.5' : 'pr-2'],
291
- sm: [hasLeftDecoration.value ? 'pl-2' : 'pl-2.5', hasRightDecoration.value ? 'pr-2' : 'pr-2.5'],
292
- md: [hasLeftDecoration.value ? 'pl-2.5' : 'pl-3', hasRightDecoration.value ? 'pr-2.5' : 'pr-3'],
314
+ xs: [hasLeftDecoration.value ? 'pl-0.5' : 'pl-2', hasRightDecoration.value ? 'pr-0' : 'pr-2'],
315
+ sm: [hasLeftDecoration.value ? 'pl-0.5' : 'pl-2.5', hasRightDecoration.value ? 'pr-0' : 'pr-2.5'],
316
+ md: [hasLeftDecoration.value ? 'pl-1' : 'pl-3', hasRightDecoration.value ? 'pr-1' : 'pr-3'],
293
317
  }[sizeInternal.value];
294
318
 
295
319
  return [
@@ -301,11 +325,11 @@ const baseClasses = computed(() => {
301
325
  });
302
326
 
303
327
  const decorationWrapClasses = computed(() => {
304
- const base = `flex items-center justify-center empty:hidden`;
328
+ const base = `flex justify-center empty:hidden`;
305
329
  const spacing = {
306
- xs: 'first:pl-2 last:pr-2 gap-1',
307
- sm: 'first:pl-2.5 last:pr-2.5 gap-2',
308
- md: 'first:pl-3 last:pr-3 gap-3',
330
+ xs: 'first:pl-0.5 last:pr-0.5 py-0.5',
331
+ sm: 'first:pl-1 last:pr-1 py-1',
332
+ md: 'first:pl-1 last:pr-1 py-1',
309
333
  }[sizeInternal.value];
310
334
 
311
335
  return [
@@ -315,10 +339,15 @@ const decorationWrapClasses = computed(() => {
315
339
  });
316
340
 
317
341
  const decorationClasses = computed(() => {
318
- const base = `flex items-center justify-center`;
342
+ const base = `flex items-center justify-center rounded-md`;
319
343
  const textColor = hasErrorInternal.value ? 'text-red-800' : 'text-slate-500';
344
+ const padding = {
345
+ xs: 'p-1',
346
+ sm: 'p-1.5',
347
+ md: 'p-2',
348
+ }[sizeInternal.value];
320
349
 
321
- return `${base} ${textColor}`;
350
+ return `${base} ${textColor} ${padding}`;
322
351
  });
323
352
 
324
353
  const iconClasses = computed(() => {
@@ -337,6 +366,22 @@ function blurAction() {
337
366
  input.value?.blur();
338
367
  }
339
368
 
369
+ function onIconLeftClickInternal() {
370
+ if (props.onIconLeftClick) {
371
+ props.onIconLeftClick();
372
+ } else {
373
+ focusAction();
374
+ }
375
+ }
376
+
377
+ function onIconRightClickInternal() {
378
+ if (props.onIconRightClick) {
379
+ props.onIconRightClick();
380
+ } else {
381
+ blurAction();
382
+ }
383
+ }
384
+
340
385
  defineExpose({
341
386
  focus: focusAction,
342
387
  blur: blurAction,
@@ -1,10 +1,18 @@
1
1
  import BasePassword from './BasePassword.vue';
2
2
  import { createFieldStory } from '@/../.storybook/utils';
3
3
 
4
+ const sizes = ['xs', 'sm', 'md'];
5
+
4
6
  export default {
5
7
  title: 'Form/BasePassword',
6
8
  component: BasePassword,
7
9
  args: {},
10
+ argTypes: {
11
+ size: {
12
+ control: { type: 'select' },
13
+ options: sizes,
14
+ },
15
+ },
8
16
  };
9
17
 
10
18
  const Template = (args) => ({
@@ -55,4 +63,21 @@ const FocusTemplate = (args) => ({
55
63
  `,
56
64
  });
57
65
 
66
+ const TemplateSizes = (args) => ({
67
+ components: { BasePassword },
68
+ setup() {
69
+ const value = ref(null);
70
+ const sizes = ['xs', 'sm', 'md'];
71
+ return { args, value, sizes };
72
+ },
73
+ template: `
74
+ <div v-for="size in sizes" :key="size" class="mb-4">
75
+ <p class="text-xs text-slate-600 leading-tight mb-1">{{ size }}</p>
76
+ <BasePassword v-model="value" v-bind="args" :size="size" class="w-full"></BasePassword>
77
+ </div>
78
+ `,
79
+ });
80
+
81
+ export const Sizes = TemplateSizes.bind({});
82
+
58
83
  export const Focus = FocusTemplate.bind({});
@@ -1,50 +1,25 @@
1
1
  <template>
2
- <div
3
- class="flex rounded border bg-white"
4
- :class="[
5
- disabled ? 'cursor-not-allowed text-slate-300' : '',
6
- hasErrorInternal ? 'border-red-500' : 'border-slate-300',
7
- ]"
8
- >
9
- <input
10
- ref="input"
11
- :value="modelValue"
12
- :type="showPassword ? 'text' : 'password'"
13
- :name="nameInternal"
14
- :disabled="disabled"
15
- :placeholder="placeholder"
16
- :required="requiredInternal"
17
- class="grow rounded-l rounded-r-none border-none focus:ring-2 focus:ring-primary-500 disabled:cursor-not-allowed"
18
- @input="onInput"
19
- >
20
- <div class="flex shrink-0 pl-3">
21
- <button
22
- tabindex="-1"
23
- type="button"
24
- class="pr-3 text-slate-500 disabled:cursor-not-allowed disabled:text-slate-300"
25
- :disabled="disabled"
26
- @click="showPassword = !showPassword"
27
- >
28
- <BaseIcon
29
- v-if="!showPassword"
30
- icon="heroicons:eye-slash-20-solid"
31
- class="h-5 w-5"
32
- />
33
- <BaseIcon
34
- v-else
35
- icon="heroicons:eye-20-solid"
36
- class="h-5 w-5"
37
- />
38
- </button>
39
- </div>
40
- </div>
2
+ <BaseInput
3
+ ref="input"
4
+ :model-value="modelValue"
5
+ :type="showPassword ? 'text' : 'password'"
6
+ :disabled="disabled"
7
+ :placeholder="placeholder"
8
+ :size="size"
9
+ :icon-right="showPassword ? 'heroicons:eye-20-solid' : 'heroicons:eye-slash-20-solid'"
10
+ @icon-right-click="onIconRightClick"
11
+ @update:model-value="onUpdateModelValue"
12
+ @focus="onFocus"
13
+ @blur="onBlur"
14
+ />
41
15
  </template>
42
16
 
43
17
  <script lang="ts" setup>
44
- import { trim } from 'lodash';
45
- import { Icon as BaseIcon } from '@iconify/vue';
46
18
  import { PropType } from 'vue';
47
- import { useField } from '@/composables/field';
19
+ import { Size } from '@/utils/sizes';
20
+ import BaseInput from './BaseInput.vue';
21
+
22
+ const emit = defineEmits(['update:modelValue', 'blur', 'focus']);
48
23
 
49
24
  const props = defineProps({
50
25
  modelValue: {
@@ -55,6 +30,10 @@ const props = defineProps({
55
30
  default: false,
56
31
  type: Boolean,
57
32
  },
33
+ size: {
34
+ default: undefined,
35
+ type: String as PropType<Size>,
36
+ },
58
37
  name: {
59
38
  default: undefined,
60
39
  type: String,
@@ -75,21 +54,10 @@ const props = defineProps({
75
54
 
76
55
  const input = ref<HTMLInputElement | null>(null);
77
56
 
78
- const emit = defineEmits(['update:modelValue']);
79
-
80
- const { nameInternal, requiredInternal, hasErrorInternal, emitUpdate } =
81
- useField({
82
- name: computed(() => props.name),
83
- required: computed(() => props.required),
84
- hasError: computed(() => props.hasError),
85
- emit: emit,
86
- });
87
-
88
57
  const showPassword = ref(false);
89
58
 
90
- function onInput(event: any) {
91
- const value = event.target.value + '';
92
- emitUpdate(trim(value));
59
+ function onIconRightClick() {
60
+ showPassword.value = !showPassword.value;
93
61
  }
94
62
 
95
63
  function focus() {
@@ -100,6 +68,18 @@ function blur() {
100
68
  input.value?.blur();
101
69
  }
102
70
 
71
+ function onUpdateModelValue(value: string) {
72
+ emit('update:modelValue', value);
73
+ }
74
+
75
+ function onBlur() {
76
+ emit('blur');
77
+ }
78
+
79
+ function onFocus() {
80
+ emit('focus');
81
+ }
82
+
103
83
  defineExpose({
104
84
  focus,
105
85
  blur,
@@ -1,12 +1,20 @@
1
1
  import { createFieldStory } from '../../.storybook/utils';
2
2
  import BaseTextarea from './BaseTextarea.vue';
3
3
 
4
+ const sizes = ['xs', 'sm', 'md'];
5
+
4
6
  export default {
5
7
  title: 'Form/BaseTextarea',
6
8
  component: BaseTextarea,
7
9
  args: {
8
10
  placeholder: 'Describe your complete life in 4 sentences...',
9
11
  },
12
+ argTypes: {
13
+ size: {
14
+ control: { type: 'select' },
15
+ options: sizes,
16
+ },
17
+ },
10
18
  };
11
19
 
12
20
  const Template = (args) => ({
@@ -42,6 +50,23 @@ export const Field = createFieldStory({
42
50
  label: 'Biography',
43
51
  });
44
52
 
53
+ const TemplateSizes = (args) => ({
54
+ components: { BaseTextarea },
55
+ setup() {
56
+ const value = ref(null);
57
+ const sizes = ['xs', 'sm', 'md'];
58
+ return { args, value, sizes };
59
+ },
60
+ template: `
61
+ <div v-for="size in sizes" :key="size" class="mb-4">
62
+ <p class="text-xs text-slate-600 leading-tight mb-1">{{ size }}</p>
63
+ <BaseTextarea v-model="value" v-bind="args" :size="size" class="w-full"></BaseTextarea>
64
+ </div>
65
+ `,
66
+ });
67
+
68
+ export const Sizes = TemplateSizes.bind({});
69
+
45
70
  const FocusTemplate = (args) => ({
46
71
  components: { BaseTextarea },
47
72
  setup() {
@@ -8,17 +8,23 @@
8
8
  :disabled="disabled"
9
9
  :required="requiredInternal"
10
10
  :rows="rows"
11
- :class="[hasErrorInternal ? 'border-red-500' : 'border-slate-300']"
12
- class="mb-0 block rounded disabled:cursor-not-allowed disabled:text-slate-300"
11
+ :autocomplete="autocomplete ? 'on' : 'off'"
12
+ :class="classes"
13
13
  @input="emitUpdate(transformInputValue($event))"
14
14
  />
15
15
  </template>
16
16
 
17
17
  <script lang="ts" setup>
18
18
  import { useField } from '@/composables/field';
19
+ import { Size, sizes } from '@/utils/sizes';
19
20
  import { get, isString } from 'lodash';
21
+ import { twMerge } from 'tailwind-merge';
20
22
  import { PropType } from 'vue';
21
23
 
24
+ defineOptions({
25
+ inheritAttrs: false,
26
+ });
27
+
22
28
  const props = defineProps({
23
29
  modelValue: {
24
30
  default: undefined,
@@ -28,6 +34,14 @@ const props = defineProps({
28
34
  type: String,
29
35
  default: 'text',
30
36
  },
37
+ size: {
38
+ default: undefined,
39
+ type: String as PropType<Size>,
40
+ },
41
+ class: {
42
+ default: '',
43
+ type: [String, Array] as PropType<string | string[]>,
44
+ },
31
45
  autocomplete: {
32
46
  default: true,
33
47
  type: Boolean,
@@ -64,10 +78,11 @@ const props = defineProps({
64
78
 
65
79
  const emit = defineEmits(['update:modelValue']);
66
80
 
67
- const { nameInternal, requiredInternal, hasErrorInternal, emitUpdate } =
81
+ const { nameInternal, requiredInternal, hasErrorInternal, emitUpdate, sizeInternal } =
68
82
  useField({
69
83
  name: computed(() => props.name),
70
84
  required: computed(() => props.required),
85
+ size: computed(() => props.size),
71
86
  hasError: computed(() => props.hasError),
72
87
  emit: emit,
73
88
  });
@@ -96,6 +111,22 @@ function blur() {
96
111
  textareaRef.value?.blur();
97
112
  }
98
113
 
114
+ const classes = computed(() => {
115
+ const base = 'mb-0 block input-rounded transition-colors duration-200';
116
+ const disabled = 'disabled:cursor-not-allowed disabled:text-slate-300';
117
+ const focus = 'focus:input-focus';
118
+ const error = hasErrorInternal.value ? 'border-red-500 focus:input-focus-error' : 'border-slate-300';
119
+ const sizeConfig = sizes[sizeInternal.value];
120
+
121
+ const padding = {
122
+ xs: 'p-2',
123
+ sm: 'p-2.5',
124
+ md: 'p-3',
125
+ }[sizeInternal.value];
126
+
127
+ return twMerge(base, disabled, error, focus, sizeConfig.fontSize, padding, props.class);
128
+ });
129
+
99
130
  defineExpose({
100
131
  focus,
101
132
  blur,
@@ -2,6 +2,8 @@ import BaseTextareaAutoresize from './BaseTextareaAutoresize.vue';
2
2
  import ShowValue from '@/../.storybook/components/ShowValue.vue';
3
3
  import { createFieldStory } from '../../.storybook/utils';
4
4
 
5
+ const sizes = ['xs', 'sm', 'md'];
6
+
5
7
  export default {
6
8
  title: 'Form/BaseTextareaAutoresize',
7
9
  component: BaseTextareaAutoresize,
@@ -9,6 +11,12 @@ export default {
9
11
  name: 'name',
10
12
  placeholder: 'Describe your complete life in 4 sentences...',
11
13
  },
14
+ argTypes: {
15
+ size: {
16
+ control: { type: 'select' },
17
+ options: sizes,
18
+ },
19
+ },
12
20
  };
13
21
 
14
22
  const Template = (args) => ({
@@ -68,8 +76,8 @@ const TemplateTWTextarea = (args) => ({
68
76
  <BaseTextareaAutoresize
69
77
  v-model="value"
70
78
  v-bind="args"
71
- class="w-full shadow rounded"
72
- tw-textarea="p-5 bg-slate-100 text-slate-700 focus:ring-blue-200 focus:ring-4 text-sm"
79
+ class="w-full"
80
+ tw-textarea="shadow-xl rounded-none p-5 duration-500 transition-all bg-slate-100 text-slate-900 ring-2 ring-purple-700 border-none ring-opacity-60 focus:ring-pink-500 focus:ring-4 focus:bg-pink-100"
73
81
  @submit="onSubmit"
74
82
  ></BaseTextareaAutoresize>
75
83
  </form>
@@ -86,6 +94,23 @@ export const Field = createFieldStory({
86
94
  label: 'Biography',
87
95
  });
88
96
 
97
+ const TemplateSizes = (args) => ({
98
+ components: { BaseTextareaAutoresize },
99
+ setup() {
100
+ const value = ref(null);
101
+ const sizes = ['xs', 'sm', 'md'];
102
+ return { args, value, sizes };
103
+ },
104
+ template: `
105
+ <div v-for="size in sizes" :key="size" class="mb-4">
106
+ <p class="text-xs text-slate-600 leading-tight mb-1">{{ size }}</p>
107
+ <BaseTextareaAutoresize v-model="value" v-bind="args" :size="size" class="w-full"></BaseTextareaAutoresize>
108
+ </div>
109
+ `,
110
+ });
111
+
112
+ export const Sizes = TemplateSizes.bind({});
113
+
89
114
  const FocusTemplate = (args) => ({
90
115
  components: { BaseTextareaAutoresize },
91
116
  setup() {
@@ -36,12 +36,10 @@
36
36
 
37
37
  <script lang="ts" setup>
38
38
  import { useField } from '@/composables/field';
39
+ import { sizes, Size } from '@/utils/sizes';
39
40
  import { twMerge } from 'tailwind-merge';
40
41
  import { PropType } from 'vue';
41
42
 
42
- const BASE_TEXTAREA_CLASSES =
43
- 'py-2 px-3 font-normal text-base disabled:cursor-not-allowed disabled:text-slate-300 font-sans rounded leading-normal tracking-normal border placeholder:text-slate-400 placeholder:font-light';
44
-
45
43
  const BASE_GRID_AREA = '1 / 1 / 2 / 2';
46
44
 
47
45
  /* Note the weird space! Needed to prevent jumpy behavior */
@@ -64,6 +62,10 @@ const props = defineProps({
64
62
  default: false,
65
63
  type: Boolean,
66
64
  },
65
+ size: {
66
+ default: undefined,
67
+ type: String as PropType<Size>,
68
+ },
67
69
  maxHeight: {
68
70
  default: 100,
69
71
  type: Number,
@@ -98,10 +100,11 @@ const emit = defineEmits(['update:modelValue', 'submit', 'focus', 'input']);
98
100
 
99
101
  const textareaRef = ref<null | HTMLTextAreaElement>(null);
100
102
 
101
- const { nameInternal, requiredInternal, hasErrorInternal } =
103
+ const { nameInternal, requiredInternal, hasErrorInternal, sizeInternal } =
102
104
  useField({
103
105
  name: computed(() => props.name),
104
106
  required: computed(() => props.required),
107
+ size: computed(() => props.size),
105
108
  hasError: computed(() => props.hasError),
106
109
  emit: emit,
107
110
  });
@@ -142,11 +145,27 @@ function onFocus(event: FocusEvent) {
142
145
  }
143
146
 
144
147
  const textareaClasses = computed(() => {
148
+ const base = 'py-2 px-3 input-rounded leading-normal tracking-normal border transition-colors duration-200';
149
+ const disabled = 'disabled:cursor-not-allowed disabled:text-slate-300';
150
+ const focus = 'focus:input-focus';
151
+ const error = hasErrorInternal.value ? 'border-red-500 focus:input-focus-error' : 'border-slate-300';
152
+ const placeholder = 'placeholder:text-slate-400 placeholder:font-light';
153
+ const sizeConfig = sizes[sizeInternal.value];
154
+
155
+ const padding = {
156
+ xs: 'px-2',
157
+ sm: 'px-2.5',
158
+ md: 'px-3',
159
+ }[sizeInternal.value];
160
+
145
161
  return twMerge(
146
- [
147
- BASE_TEXTAREA_CLASSES,
148
- hasErrorInternal.value ? 'border-red-500' : 'border-slate-300',
149
- ],
162
+ base,
163
+ disabled,
164
+ error,
165
+ placeholder,
166
+ focus,
167
+ sizeConfig.fontSize,
168
+ padding,
150
169
  props.twTextarea
151
170
  );
152
171
  });