@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@300codes/design-system",
3
- "version": "1.1.0",
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
- ariaLabel: { control: 'text' },
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
- ariaLabel: 'Close',
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
- ariaLabel: 'Search',
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
- ariaLabel: 'Close',
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
- ariaLabel?: string;
9
+ label: string;
10
+ disabled?: boolean;
10
11
  }
11
12
 
12
13
  withDefaults(defineProps<FlatIconButtonProps>(), {
13
14
  iconPath: '/icons',
14
15
  iconSize: 'sm',
15
- ariaLabel: undefined,
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="ariaLabel"
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>
@@ -0,0 +1,2 @@
1
+ export { default as QuantitySelector } from './QuantitySelector.vue';
2
+ export type { QuantitySelectorProps } from './QuantitySelector.vue';
@@ -261,7 +261,7 @@ watch(isMobile, () => {
261
261
 
262
262
  <FlatIconButton
263
263
  icon-name="close"
264
- aria-label="Close"
264
+ label="Close"
265
265
  class="ml-auto"
266
266
  @click="closeDropdown(true)"
267
267
  />
@@ -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
- :aria-label="iconLeft.ariaLabel"
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
- :aria-label="iconRight.ariaLabel"
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
  />
@@ -13,4 +13,5 @@ export * from './TabsList/index';
13
13
  export * from './SearchInput/index';
14
14
  export * from './BaseTooltip/index';
15
15
  export * from './FlatIconButton/index';
16
+ export * from './QuantitySelector/index';
16
17
  export type { IconSize } from '../types/icon';
@@ -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
  * ──────────────────────────────────────────────── */