@300codes/design-system 1.1.0 → 1.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.
- package/package.json +5 -3
- package/src/components/FlatIconButton/FlatIconButton.stories.ts +5 -5
- package/src/components/FlatIconButton/FlatIconButton.vue +8 -5
- package/src/components/QuantitySelector/QuantitySelector.stories.ts +84 -0
- package/src/components/QuantitySelector/QuantitySelector.vue +142 -0
- package/src/components/QuantitySelector/index.ts +2 -0
- package/src/components/SelectInput/SelectInput.vue +1 -1
- package/src/components/TextInput/TextInput.vue +2 -2
- package/src/components/index.ts +1 -0
- package/src/css/tokens.css +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@300codes/design-system",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/components",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"./tabs-list": "./src/components/TabsList/index.ts",
|
|
23
23
|
"./search-input": "./src/components/SearchInput/index.ts",
|
|
24
24
|
"./base-tooltip": "./src/components/BaseTooltip/index.ts",
|
|
25
|
-
"./flat-icon-button": "./src/components/FlatIconButton/index.ts"
|
|
25
|
+
"./flat-icon-button": "./src/components/FlatIconButton/index.ts",
|
|
26
|
+
"./quantity-selector": "./src/components/QuantitySelector/index.ts"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
29
|
"build": "vue-tsc --noEmit",
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
"lint": "eslint .",
|
|
32
33
|
"lint:fix": "eslint . --fix",
|
|
33
34
|
"format": "prettier --write .",
|
|
34
|
-
"type-check": "vue-tsc --noEmit"
|
|
35
|
+
"type-check": "vue-tsc --noEmit",
|
|
36
|
+
"release": "sh -c 'npm version $1 && git push && git push --tags && npm publish --access public' --"
|
|
35
37
|
},
|
|
36
38
|
"peerDependencies": {
|
|
37
39
|
"vue": "^3.5.0"
|
|
@@ -12,9 +12,9 @@ const meta: Meta<FlatIconButtonProps> = {
|
|
|
12
12
|
iconPath: { control: 'text' },
|
|
13
13
|
iconSize: {
|
|
14
14
|
control: 'select',
|
|
15
|
-
options: ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
15
|
+
options: ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'auto'],
|
|
16
16
|
},
|
|
17
|
-
|
|
17
|
+
label: { control: 'text' },
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -25,7 +25,7 @@ export const Default: Story = {
|
|
|
25
25
|
args: {
|
|
26
26
|
iconName: 'close',
|
|
27
27
|
iconSize: 'sm',
|
|
28
|
-
|
|
28
|
+
label: 'Close',
|
|
29
29
|
},
|
|
30
30
|
render: (args: FlatIconButtonProps) => ({
|
|
31
31
|
components: { FlatIconButton },
|
|
@@ -40,7 +40,7 @@ export const Search: Story = {
|
|
|
40
40
|
args: {
|
|
41
41
|
iconName: 'search',
|
|
42
42
|
iconSize: 'sm',
|
|
43
|
-
|
|
43
|
+
label: 'Search',
|
|
44
44
|
},
|
|
45
45
|
render: (args: FlatIconButtonProps) => ({
|
|
46
46
|
components: { FlatIconButton },
|
|
@@ -55,7 +55,7 @@ export const MediumSize: Story = {
|
|
|
55
55
|
args: {
|
|
56
56
|
iconName: 'close',
|
|
57
57
|
iconSize: 'md',
|
|
58
|
-
|
|
58
|
+
label: 'Close',
|
|
59
59
|
},
|
|
60
60
|
render: (args: FlatIconButtonProps) => ({
|
|
61
61
|
components: { FlatIconButton },
|
|
@@ -6,13 +6,14 @@ export interface FlatIconButtonProps {
|
|
|
6
6
|
iconName: string;
|
|
7
7
|
iconPath?: string;
|
|
8
8
|
iconSize?: IconSize;
|
|
9
|
-
|
|
9
|
+
label: string;
|
|
10
|
+
disabled?: boolean;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
withDefaults(defineProps<FlatIconButtonProps>(), {
|
|
13
14
|
iconPath: '/icons',
|
|
14
15
|
iconSize: 'sm',
|
|
15
|
-
|
|
16
|
+
disabled: false,
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
const emit = defineEmits<{
|
|
@@ -23,8 +24,9 @@ const emit = defineEmits<{
|
|
|
23
24
|
<template>
|
|
24
25
|
<button
|
|
25
26
|
type="button"
|
|
26
|
-
class="flatIconButton inline-flex items-center justify-center cursor-pointer"
|
|
27
|
-
:aria-label="
|
|
27
|
+
:class="['flatIconButton inline-flex items-center justify-center', disabled ? 'cursor-not-allowed' : 'cursor-pointer']"
|
|
28
|
+
:aria-label="label"
|
|
29
|
+
:disabled="disabled"
|
|
28
30
|
@click="emit('click')"
|
|
29
31
|
>
|
|
30
32
|
<BaseIcon
|
|
@@ -43,8 +45,9 @@ const emit = defineEmits<{
|
|
|
43
45
|
border-radius: var(--flatIconButton-radius, 9999px);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
.flatIconButton:hover {
|
|
48
|
+
.flatIconButton:hover:not(:disabled) {
|
|
47
49
|
background-color: var(--flatIconButton-bg-hover, #f3f4f6);
|
|
50
|
+
color: var(--flatIconButton-fg-hover, #0e161b);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
.flatIconButton:focus-visible {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import type { ConcreteComponent } from 'vue';
|
|
3
|
+
import { ref } from 'vue';
|
|
4
|
+
import type { QuantitySelectorProps } from './QuantitySelector.vue';
|
|
5
|
+
import QuantitySelector from './QuantitySelector.vue';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<QuantitySelectorProps> = {
|
|
8
|
+
title: 'Components/QuantitySelector',
|
|
9
|
+
component: QuantitySelector as unknown as ConcreteComponent<QuantitySelectorProps>,
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
argTypes: {
|
|
12
|
+
size: { control: 'select', options: ['sm', 'md'] },
|
|
13
|
+
max: { control: 'number' },
|
|
14
|
+
disabled: { control: 'boolean' },
|
|
15
|
+
name: { control: 'text' },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<QuantitySelectorProps>;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
name: 'quantity',
|
|
25
|
+
size: 'md',
|
|
26
|
+
},
|
|
27
|
+
render: (args: QuantitySelectorProps) => ({
|
|
28
|
+
components: { QuantitySelector },
|
|
29
|
+
setup() {
|
|
30
|
+
const value = ref(1);
|
|
31
|
+
return { args, value };
|
|
32
|
+
},
|
|
33
|
+
template: '<QuantitySelector v-bind="args" v-model="value" />',
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Sizes: Story = {
|
|
38
|
+
render: () => ({
|
|
39
|
+
components: { QuantitySelector },
|
|
40
|
+
setup() {
|
|
41
|
+
const sm = ref(1);
|
|
42
|
+
const md = ref(1);
|
|
43
|
+
return { sm, md };
|
|
44
|
+
},
|
|
45
|
+
template: `
|
|
46
|
+
<div class="flex items-center gap-6">
|
|
47
|
+
<QuantitySelector name="qty-sm" size="sm" v-model="sm" />
|
|
48
|
+
<QuantitySelector name="qty-md" size="md" v-model="md" />
|
|
49
|
+
</div>
|
|
50
|
+
`,
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const WithMax: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
name: 'quantity-max',
|
|
57
|
+
size: 'md',
|
|
58
|
+
max: 5,
|
|
59
|
+
},
|
|
60
|
+
render: (args: QuantitySelectorProps) => ({
|
|
61
|
+
components: { QuantitySelector },
|
|
62
|
+
setup() {
|
|
63
|
+
const value = ref(3);
|
|
64
|
+
return { args, value };
|
|
65
|
+
},
|
|
66
|
+
template: '<QuantitySelector v-bind="args" v-model="value" />',
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Disabled: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
name: 'quantity-disabled',
|
|
73
|
+
size: 'md',
|
|
74
|
+
disabled: true,
|
|
75
|
+
},
|
|
76
|
+
render: (args: QuantitySelectorProps) => ({
|
|
77
|
+
components: { QuantitySelector },
|
|
78
|
+
setup() {
|
|
79
|
+
const value = ref(1);
|
|
80
|
+
return { args, value };
|
|
81
|
+
},
|
|
82
|
+
template: '<QuantitySelector v-bind="args" v-model="value" />',
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
3
|
+
import type { IconSize } from '../../types/icon';
|
|
4
|
+
import FlatIconButton from '../FlatIconButton/FlatIconButton.vue';
|
|
5
|
+
|
|
6
|
+
export interface QuantitySelectorProps {
|
|
7
|
+
name: string;
|
|
8
|
+
size?: 'sm' | 'md';
|
|
9
|
+
max?: number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
inputLabel?: string;
|
|
12
|
+
decreaseLabel?: string;
|
|
13
|
+
increaseLabel?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<QuantitySelectorProps>(), {
|
|
17
|
+
size: 'md',
|
|
18
|
+
max: undefined,
|
|
19
|
+
disabled: false,
|
|
20
|
+
inputLabel: 'Quantity',
|
|
21
|
+
decreaseLabel: 'Decrease',
|
|
22
|
+
increaseLabel: 'Increase',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const model = defineModel<number>({ default: 1 });
|
|
26
|
+
|
|
27
|
+
const text = ref(String(model.value));
|
|
28
|
+
|
|
29
|
+
const isAtMin = computed(() => model.value <= 1);
|
|
30
|
+
const isAtMax = computed(() => props.max !== undefined && model.value >= props.max);
|
|
31
|
+
const iconSize = computed<IconSize>(() => (props.size === 'sm' ? 'md' : 'lg'));
|
|
32
|
+
|
|
33
|
+
const disabledBtnStyle = { '--flatIconButton-fg': 'var(--quantitySelector-fg-disabled, #89979f)' };
|
|
34
|
+
const disabledPropStyle = { '--flatIconButton-fg': 'transparent' };
|
|
35
|
+
|
|
36
|
+
function onDecrease() {
|
|
37
|
+
if (props.disabled || isAtMin.value) return;
|
|
38
|
+
model.value--;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function onIncrease() {
|
|
42
|
+
if (props.disabled || isAtMax.value) return;
|
|
43
|
+
model.value++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onInput(event: Event) {
|
|
47
|
+
const input = event.target as HTMLInputElement;
|
|
48
|
+
const filtered = input.value.replace(/\D/g, '');
|
|
49
|
+
input.value = filtered;
|
|
50
|
+
text.value = filtered;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function onBlur() {
|
|
54
|
+
const parsed = parseInt(text.value, 10);
|
|
55
|
+
let value = isNaN(parsed) || parsed < 1 ? 1 : parsed;
|
|
56
|
+
|
|
57
|
+
if (props.max !== undefined) value = Math.min(value, props.max);
|
|
58
|
+
|
|
59
|
+
model.value = value;
|
|
60
|
+
text.value = String(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
watch(model, (val) => {
|
|
64
|
+
text.value = String(val);
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<div
|
|
70
|
+
:class="[
|
|
71
|
+
'quantitySelector',
|
|
72
|
+
`quantitySelector--${size}`,
|
|
73
|
+
'inline-flex items-center overflow-hidden',
|
|
74
|
+
]"
|
|
75
|
+
>
|
|
76
|
+
<FlatIconButton
|
|
77
|
+
icon-name="minus"
|
|
78
|
+
:icon-size="iconSize"
|
|
79
|
+
:label="decreaseLabel"
|
|
80
|
+
:disabled="disabled || isAtMin"
|
|
81
|
+
class="h-full aspect-square"
|
|
82
|
+
:style="disabled ? disabledPropStyle : isAtMin ? disabledBtnStyle : undefined"
|
|
83
|
+
@click="onDecrease"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<input
|
|
87
|
+
:name="name"
|
|
88
|
+
:value="disabled ? '1' : text"
|
|
89
|
+
type="text"
|
|
90
|
+
inputmode="numeric"
|
|
91
|
+
pattern="[0-9]*"
|
|
92
|
+
autocomplete="off"
|
|
93
|
+
:aria-label="inputLabel"
|
|
94
|
+
:disabled="disabled"
|
|
95
|
+
:class="[
|
|
96
|
+
'quantitySelector__input',
|
|
97
|
+
'bg-transparent border-0 outline-none text-center leading-normal h-full shrink-0 disabled:cursor-not-allowed',
|
|
98
|
+
]"
|
|
99
|
+
@input="onInput"
|
|
100
|
+
@blur="onBlur"
|
|
101
|
+
>
|
|
102
|
+
|
|
103
|
+
<FlatIconButton
|
|
104
|
+
icon-name="plus"
|
|
105
|
+
:icon-size="iconSize"
|
|
106
|
+
:label="increaseLabel"
|
|
107
|
+
:disabled="disabled || isAtMax"
|
|
108
|
+
class="h-full aspect-square"
|
|
109
|
+
:style="disabled ? disabledPropStyle : isAtMax ? disabledBtnStyle : undefined"
|
|
110
|
+
@click="onIncrease"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<style scoped>
|
|
116
|
+
.quantitySelector {
|
|
117
|
+
--_h: var(--quantitySelector-h, 3.5rem);
|
|
118
|
+
--_input-w: var(--quantitySelector-input-w, 2.125rem);
|
|
119
|
+
--_fs: var(--quantitySelector-font-size, 1rem);
|
|
120
|
+
|
|
121
|
+
height: var(--_h);
|
|
122
|
+
background-color: var(--quantitySelector-bg, #f3f5f7);
|
|
123
|
+
border: var(--quantitySelector-border-width, 0) solid var(--quantitySelector-border, #d6dde1);
|
|
124
|
+
border-radius: var(--quantitySelector-radius, 9999px);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.quantitySelector--sm {
|
|
128
|
+
--_h: var(--quantitySelector-sm-h, 2.5rem);
|
|
129
|
+
--_input-w: var(--quantitySelector-sm-input-w, 2.875rem);
|
|
130
|
+
--_fs: var(--quantitySelector-sm-font-size, 0.875rem);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.quantitySelector__input {
|
|
134
|
+
width: var(--_input-w);
|
|
135
|
+
font-size: var(--_fs);
|
|
136
|
+
color: var(--quantitySelector-fg, #0e161b);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.quantitySelector__input:hover:not(:disabled) {
|
|
140
|
+
background-color: var(--quantitySelector-bg-hover, #f3f5f7);
|
|
141
|
+
}
|
|
142
|
+
</style>
|
|
@@ -129,7 +129,7 @@ defineExpose<{ el: Ref<HTMLInputElement | undefined> }>({ el });
|
|
|
129
129
|
:icon-name="iconLeft.name"
|
|
130
130
|
:icon-path="iconLeft.iconPath"
|
|
131
131
|
:icon-size="iconLeft.size"
|
|
132
|
-
:
|
|
132
|
+
:label="iconLeft.ariaLabel || iconLeft.name"
|
|
133
133
|
:class="['absolute top-1/2 -translate-y-1/2 left-3', iconLeft.class]"
|
|
134
134
|
@click="emit('clickIcon', 'left')"
|
|
135
135
|
/>
|
|
@@ -139,7 +139,7 @@ defineExpose<{ el: Ref<HTMLInputElement | undefined> }>({ el });
|
|
|
139
139
|
:icon-name="iconRight.name"
|
|
140
140
|
:icon-path="iconRight.iconPath"
|
|
141
141
|
:icon-size="iconRight.size"
|
|
142
|
-
:
|
|
142
|
+
:label="iconRight.ariaLabel || iconRight.name"
|
|
143
143
|
:class="['absolute top-1/2 -translate-y-1/2 right-3', iconRight.class]"
|
|
144
144
|
@click="emit('clickIcon', 'right')"
|
|
145
145
|
/>
|
package/src/components/index.ts
CHANGED
package/src/css/tokens.css
CHANGED
|
@@ -385,6 +385,28 @@
|
|
|
385
385
|
--flatIconButton-p: theme(--spacing-1);
|
|
386
386
|
--flatIconButton-radius: theme(--radius-full);
|
|
387
387
|
|
|
388
|
+
/* ────────────────────────────────────────────────
|
|
389
|
+
* QuantitySelector
|
|
390
|
+
* ──────────────────────────────────────────────── */
|
|
391
|
+
|
|
392
|
+
--quantitySelector-bg: theme(--color-input-bg);
|
|
393
|
+
--quantitySelector-fg: theme(--color-input-fg);
|
|
394
|
+
--quantitySelector-bg-hover: #f3f5f7;
|
|
395
|
+
--quantitySelector-fg-disabled: theme(--color-disabled-foreground);
|
|
396
|
+
--quantitySelector-border: theme(--color-input-border);
|
|
397
|
+
--quantitySelector-border-width: 0;
|
|
398
|
+
--quantitySelector-radius: 9999px;
|
|
399
|
+
|
|
400
|
+
/* md (default) */
|
|
401
|
+
--quantitySelector-h: theme(--spacing-14); /* 56px */
|
|
402
|
+
--quantitySelector-input-w: theme(--spacing-12); /* 48px */
|
|
403
|
+
--quantitySelector-font-size: theme(--text-base); /* 16px */
|
|
404
|
+
|
|
405
|
+
/* sm */
|
|
406
|
+
--quantitySelector-sm-h: theme(--spacing-10); /* 40px */
|
|
407
|
+
--quantitySelector-sm-input-w: theme(--spacing-10); /* 40px */
|
|
408
|
+
--quantitySelector-sm-font-size: theme(--text-sm); /* 14px */
|
|
409
|
+
|
|
388
410
|
/* ────────────────────────────────────────────────
|
|
389
411
|
* TabsList
|
|
390
412
|
* ──────────────────────────────────────────────── */
|