@300codes/design-system 1.0.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.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/components",
@@ -21,7 +21,9 @@
21
21
  "./radio-input": "./src/components/RadioInput/index.ts",
22
22
  "./tabs-list": "./src/components/TabsList/index.ts",
23
23
  "./search-input": "./src/components/SearchInput/index.ts",
24
- "./base-tooltip": "./src/components/BaseTooltip/index.ts"
24
+ "./base-tooltip": "./src/components/BaseTooltip/index.ts",
25
+ "./flat-icon-button": "./src/components/FlatIconButton/index.ts",
26
+ "./quantity-selector": "./src/components/QuantitySelector/index.ts"
25
27
  },
26
28
  "scripts": {
27
29
  "build": "vue-tsc --noEmit",
@@ -30,7 +32,8 @@
30
32
  "lint": "eslint .",
31
33
  "lint:fix": "eslint . --fix",
32
34
  "format": "prettier --write .",
33
- "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' --"
34
37
  },
35
38
  "peerDependencies": {
36
39
  "vue": "^3.5.0"
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import type { ConcreteComponent } from 'vue';
3
+ import type { FlatIconButtonProps } from './FlatIconButton.vue';
4
+ import FlatIconButton from './FlatIconButton.vue';
5
+
6
+ const meta: Meta<FlatIconButtonProps> = {
7
+ title: 'Components/FlatIconButton',
8
+ component: FlatIconButton as unknown as ConcreteComponent<FlatIconButtonProps>,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ iconName: { control: 'text' },
12
+ iconPath: { control: 'text' },
13
+ iconSize: {
14
+ control: 'select',
15
+ options: ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'auto'],
16
+ },
17
+ label: { control: 'text' },
18
+ },
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<FlatIconButtonProps>;
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ iconName: 'close',
27
+ iconSize: 'sm',
28
+ label: 'Close',
29
+ },
30
+ render: (args: FlatIconButtonProps) => ({
31
+ components: { FlatIconButton },
32
+ setup() {
33
+ return { args };
34
+ },
35
+ template: '<FlatIconButton v-bind="args" />',
36
+ }),
37
+ };
38
+
39
+ export const Search: Story = {
40
+ args: {
41
+ iconName: 'search',
42
+ iconSize: 'sm',
43
+ label: 'Search',
44
+ },
45
+ render: (args: FlatIconButtonProps) => ({
46
+ components: { FlatIconButton },
47
+ setup() {
48
+ return { args };
49
+ },
50
+ template: '<FlatIconButton v-bind="args" />',
51
+ }),
52
+ };
53
+
54
+ export const MediumSize: Story = {
55
+ args: {
56
+ iconName: 'close',
57
+ iconSize: 'md',
58
+ label: 'Close',
59
+ },
60
+ render: (args: FlatIconButtonProps) => ({
61
+ components: { FlatIconButton },
62
+ setup() {
63
+ return { args };
64
+ },
65
+ template: '<FlatIconButton v-bind="args" />',
66
+ }),
67
+ };
68
+
69
+ export const Sizes: Story = {
70
+ render: () => ({
71
+ components: { FlatIconButton },
72
+ template: `
73
+ <div class="flex items-center gap-4">
74
+ <FlatIconButton icon-name="close" icon-size="xs" aria-label="Close xs" />
75
+ <FlatIconButton icon-name="close" icon-size="sm" aria-label="Close sm" />
76
+ <FlatIconButton icon-name="close" icon-size="md" aria-label="Close md" />
77
+ <FlatIconButton icon-name="close" icon-size="lg" aria-label="Close lg" />
78
+ </div>
79
+ `,
80
+ }),
81
+ };
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import type { IconSize } from '../../types/icon';
3
+ import BaseIcon from '../BaseIcon/BaseIcon.vue';
4
+
5
+ export interface FlatIconButtonProps {
6
+ iconName: string;
7
+ iconPath?: string;
8
+ iconSize?: IconSize;
9
+ label: string;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ withDefaults(defineProps<FlatIconButtonProps>(), {
14
+ iconPath: '/icons',
15
+ iconSize: 'sm',
16
+ disabled: false,
17
+ });
18
+
19
+ const emit = defineEmits<{
20
+ click: [];
21
+ }>();
22
+ </script>
23
+
24
+ <template>
25
+ <button
26
+ type="button"
27
+ :class="['flatIconButton inline-flex items-center justify-center', disabled ? 'cursor-not-allowed' : 'cursor-pointer']"
28
+ :aria-label="label"
29
+ :disabled="disabled"
30
+ @click="emit('click')"
31
+ >
32
+ <BaseIcon
33
+ :name="iconName"
34
+ :icon-path="iconPath"
35
+ :size="iconSize"
36
+ />
37
+ </button>
38
+ </template>
39
+
40
+ <style scoped>
41
+ .flatIconButton {
42
+ color: var(--flatIconButton-fg, #0e161b);
43
+ background: var(--flatIconButton-bg, transparent);
44
+ padding: var(--flatIconButton-p, 0.25rem);
45
+ border-radius: var(--flatIconButton-radius, 9999px);
46
+ }
47
+
48
+ .flatIconButton:hover:not(:disabled) {
49
+ background-color: var(--flatIconButton-bg-hover, #f3f4f6);
50
+ color: var(--flatIconButton-fg-hover, #0e161b);
51
+ }
52
+
53
+ .flatIconButton:focus-visible {
54
+ outline: 2px solid var(--flatIconButton-outline, #0066cc);
55
+ outline-offset: 1px;
56
+ }
57
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as FlatIconButton } from './FlatIconButton.vue';
2
+ export type { FlatIconButtonProps } from './FlatIconButton.vue';
@@ -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';
@@ -2,7 +2,7 @@
2
2
  import { ref, computed, nextTick, watch } from 'vue';
3
3
  import { useMediaQuery, onClickOutside } from '@vueuse/core';
4
4
  import BaseIcon from '../BaseIcon/BaseIcon.vue';
5
- import InputIconButton from '../TextInput/components/InputIconButton.vue';
5
+ import FlatIconButton from '../FlatIconButton/FlatIconButton.vue';
6
6
 
7
7
  export type SelectOption = string | { value: string; label: string };
8
8
 
@@ -259,9 +259,9 @@ watch(isMobile, () => {
259
259
  >
260
260
  {{ mobileTitle || placeholder }}
261
261
 
262
- <InputIconButton
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
  />
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, type Ref } from 'vue';
3
3
  import type { IconSize } from '../../types/icon';
4
- import InputIconButton from './components/InputIconButton.vue';
4
+ import FlatIconButton from '../FlatIconButton/FlatIconButton.vue';
5
5
 
6
6
  export type InputType = 'text' | 'password' | 'email' | 'tel' | 'search';
7
7
  export type FilterType = 'number' | 'number-dash' | 'alpha' | 'alpha-space';
@@ -124,22 +124,22 @@ defineExpose<{ el: Ref<HTMLInputElement | undefined> }>({ el });
124
124
  @keyup.enter="emit('enter')"
125
125
  >
126
126
 
127
- <InputIconButton
127
+ <FlatIconButton
128
128
  v-if="iconLeft"
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
  />
136
136
 
137
- <InputIconButton
137
+ <FlatIconButton
138
138
  v-if="iconRight"
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
  />
@@ -12,4 +12,6 @@ export * from './RadioInput/index';
12
12
  export * from './TabsList/index';
13
13
  export * from './SearchInput/index';
14
14
  export * from './BaseTooltip/index';
15
+ export * from './FlatIconButton/index';
16
+ export * from './QuantitySelector/index';
15
17
  export type { IconSize } from '../types/icon';
@@ -182,15 +182,6 @@
182
182
  --input-border-color-disabled: theme(--color-input-border-disabled);
183
183
  --input-placeholder-fg-disabled: theme(--color-input-placeholder-disabled);
184
184
 
185
- /* icon button */
186
- --input-icon-bg: transparent;
187
- --input-icon-fg: theme(--color-input-icon);
188
- --input-icon-fg-hover: theme(--color-input-icon-hover);
189
- --input-icon-bg-hover: theme(--color-input-icon-bg-hover);
190
- --input-icon-outline: theme(--color-outline);
191
- --input-icon-p: theme(--spacing-1);
192
- --input-icon-radius: theme(--radius-full);
193
-
194
185
  /* ────────────────────────────────────────────────
195
186
  * TextInput — sizing
196
187
  * ──────────────────────────────────────────────── */
@@ -382,6 +373,40 @@
382
373
  --baseTooltip-lg-arrow-half-w: theme(--spacing-3); /* lg half-width 12px */
383
374
  --baseTooltip-lg-arrow-h: theme(--spacing-4); /* lg arrow height 16px */
384
375
 
376
+ /* ────────────────────────────────────────────────
377
+ * FlatIconButton
378
+ * ──────────────────────────────────────────────── */
379
+
380
+ --flatIconButton-bg: transparent;
381
+ --flatIconButton-fg: theme(--color-input-icon);
382
+ --flatIconButton-fg-hover: theme(--color-input-icon-hover);
383
+ --flatIconButton-bg-hover: theme(--color-input-icon-bg-hover);
384
+ --flatIconButton-outline: theme(--color-outline);
385
+ --flatIconButton-p: theme(--spacing-1);
386
+ --flatIconButton-radius: theme(--radius-full);
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
+
385
410
  /* ────────────────────────────────────────────────
386
411
  * TabsList
387
412
  * ──────────────────────────────────────────────── */
@@ -1,54 +0,0 @@
1
- <script setup lang="ts">
2
- import type { IconSize } from '../../../types/icon';
3
- import BaseIcon from '../../BaseIcon/BaseIcon.vue';
4
-
5
- export interface InputIconButtonProps {
6
- iconName: string;
7
- iconPath?: string;
8
- iconSize?: IconSize;
9
- ariaLabel?: string;
10
- }
11
-
12
- withDefaults(defineProps<InputIconButtonProps>(), {
13
- iconPath: '/icons',
14
- iconSize: 'sm',
15
- ariaLabel: undefined,
16
- });
17
-
18
- const emit = defineEmits<{
19
- click: [];
20
- }>();
21
- </script>
22
-
23
- <template>
24
- <button
25
- type="button"
26
- class="inputIconButton inline-flex items-center justify-center cursor-pointer"
27
- :aria-label="ariaLabel"
28
- @click="emit('click')"
29
- >
30
- <BaseIcon
31
- :name="iconName"
32
- :icon-path="iconPath"
33
- :size="iconSize"
34
- />
35
- </button>
36
- </template>
37
-
38
- <style scoped>
39
- .inputIconButton {
40
- color: var(--input-icon-fg, #0e161b);
41
- background: var(--input-icon-bg, transparent);
42
- padding: var(--input-icon-p, 0.25rem);
43
- border-radius: var(--input-icon-radius, 9999px);
44
- }
45
-
46
- .inputIconButton:hover {
47
- background-color: var(--input-icon-bg-hover, #f3f4f6);
48
- }
49
-
50
- .inputIconButton:focus-visible {
51
- outline: 2px solid var(--input-icon-outline, #0066cc);
52
- outline-offset: 1px;
53
- }
54
- </style>