@300codes/design-system 1.1.0 → 1.2.1

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.1",
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"
@@ -56,13 +56,13 @@ const nativeAttrs = computed((): Record<string, unknown> => {
56
56
  v-bind="nativeAttrs"
57
57
  :class="[
58
58
  'baseLabel',
59
- 'no-underline inline-flex items-center justify-center',
60
59
  `baseLabel--${props.variant}`,
61
60
  `baseLabel--${props.size}`,
62
61
  {
63
62
  'baseLabel--interactive': isInteractive,
64
63
  'baseLabel--disabled': props.disabled,
65
64
  },
65
+ 'no-underline inline-flex items-center justify-center',
66
66
  ]"
67
67
  >
68
68
  <slot />
@@ -79,10 +79,9 @@ const nativeAttrs = computed((): Record<string, unknown> => {
79
79
  border: var(--baseLabel-border-width, 1px) solid var(--baseLabel-border, transparent);
80
80
  border-radius: var(--baseLabel-radius, 3.5rem);
81
81
  padding: var(--baseLabel-py, 0.125rem) var(--baseLabel-px, 0.5rem);
82
- min-height: var(--baseLabel-min-h, 1.5rem);
82
+ height: var(--baseLabel-min-h, 1.5rem);
83
83
  font-size: var(--baseLabel-font-size, 0.75rem);
84
84
  font-weight: var(--baseLabel-font-weight, 600);
85
- line-height: 1.2;
86
85
  }
87
86
 
88
87
  .baseLabel--interactive {
@@ -106,7 +105,8 @@ const nativeAttrs = computed((): Record<string, unknown> => {
106
105
  /* ── sm ── */
107
106
 
108
107
  .baseLabel--sm {
109
- min-height: var(--baseLabel-sm-min-h, 1.25rem);
108
+ height: var(--baseLabel-sm-min-h, 1.25rem);
109
+ font-size: var(--baseLabel-sm-font-size, 0.625rem);
110
110
  }
111
111
 
112
112
  /* ── secondary ── */
@@ -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
@@ -39,12 +41,13 @@ const emit = defineEmits<{
39
41
  .flatIconButton {
40
42
  color: var(--flatIconButton-fg, #0e161b);
41
43
  background: var(--flatIconButton-bg, transparent);
42
- padding: var(--flatIconButton-p, 0.25rem);
44
+ padding: var(--flatIconButton-p, 0);
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,143 @@
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
+ min-width: var(--_input-w);
136
+ font-size: var(--_fs);
137
+ color: var(--quantitySelector-fg, #0e161b);
138
+ }
139
+
140
+ .quantitySelector__input:hover:not(:disabled) {
141
+ background-color: var(--quantitySelector-bg-hover, #f3f5f7);
142
+ }
143
+ </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
  />
@@ -93,7 +93,6 @@ const nativeAttrs = computed((): Record<string, unknown> => {
93
93
  min-height: var(--simpleButton-min-h, 3.5rem);
94
94
  font-size: var(--simpleButton-font-size, 1rem);
95
95
  font-weight: var(--simpleButton-font-weight, 600);
96
- line-height: 1.2;
97
96
  gap: var(--simpleButton-gap, 1rem);
98
97
  }
99
98
 
@@ -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';
@@ -126,6 +126,7 @@
126
126
 
127
127
  /* sm */
128
128
  --baseLabel-sm-min-h: theme(--spacing-5);
129
+ --baseLabel-sm-font-size: theme(--text-2xs);
129
130
 
130
131
  /* ────────────────────────────────────────────────
131
132
  * LabelInput
@@ -382,9 +383,31 @@
382
383
  --flatIconButton-fg-hover: theme(--color-input-icon-hover);
383
384
  --flatIconButton-bg-hover: theme(--color-input-icon-bg-hover);
384
385
  --flatIconButton-outline: theme(--color-outline);
385
- --flatIconButton-p: theme(--spacing-1);
386
+ --flatIconButton-p: theme(--spacing-0);
386
387
  --flatIconButton-radius: theme(--radius-full);
387
388
 
389
+ /* ────────────────────────────────────────────────
390
+ * QuantitySelector
391
+ * ──────────────────────────────────────────────── */
392
+
393
+ --quantitySelector-bg: theme(--color-input-bg);
394
+ --quantitySelector-fg: theme(--color-input-fg);
395
+ --quantitySelector-bg-hover: #f3f5f7;
396
+ --quantitySelector-fg-disabled: theme(--color-disabled-foreground);
397
+ --quantitySelector-border: theme(--color-input-border);
398
+ --quantitySelector-border-width: 0;
399
+ --quantitySelector-radius: 9999px;
400
+
401
+ /* md (default) */
402
+ --quantitySelector-h: theme(--spacing-14); /* 56px */
403
+ --quantitySelector-input-w: theme(--spacing-12); /* 48px */
404
+ --quantitySelector-font-size: theme(--text-base); /* 16px */
405
+
406
+ /* sm */
407
+ --quantitySelector-sm-h: theme(--spacing-10); /* 40px */
408
+ --quantitySelector-sm-input-w: theme(--spacing-10); /* 40px */
409
+ --quantitySelector-sm-font-size: theme(--text-sm); /* 14px */
410
+
388
411
  /* ────────────────────────────────────────────────
389
412
  * TabsList
390
413
  * ──────────────────────────────────────────────── */