ketekny-ui-kit 1.0.96 → 1.0.97

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/index.js CHANGED
@@ -14,6 +14,7 @@ import kTable from './src/ui/kTable.vue'
14
14
  import kTabs from './src/ui/kTabs.vue'
15
15
  import kToggle from './src/ui/kToggle.vue'
16
16
  import kForm from './src/ui/kForm.vue'
17
+ import kOptionGroup from './src/ui/kOptionGroup.vue'
17
18
  import kUploader from './src/ui/kUploader.vue'
18
19
  import kEditor from './src/ui/kEditor.vue'
19
20
  import kSpinner from './src/ui/kSpinner.vue'
@@ -57,7 +58,7 @@ export {
57
58
  kMessage, kCode, kToolbar, kTable, kTabs, kChip, kSpinner, kDatatable, kIcon, kMenu, kSkeleton, kProgressBar, kPagination, kTree,
58
59
 
59
60
  // Form Components
60
- kButton, kCheckbox, kSelect, kUploader, kToggle, kInput, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea, kForm,
61
+ kButton, kCheckbox, kSelect, kUploader, kToggle, kInput, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea, kForm, kOptionGroup,
61
62
 
62
63
  // Dialogs
63
64
  kDialog, kDrawer,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.96",
4
+ "version": "1.0.97",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
package/src/ui/kList.vue CHANGED
@@ -7,7 +7,13 @@
7
7
  <div
8
8
  v-for="(stat, index) in normalizedStats"
9
9
  :key="resolveKey(stat, index)"
10
- class="flex items-center gap-3 p-3 border border-gray-200 rounded-xl bg-white dark:bg-slate-800 dark:border-slate-700"
10
+ class="flex items-center gap-3 border border-gray-200 rounded-xl bg-white p-3 dark:border-slate-700 dark:bg-slate-800"
11
+ :class="entryClass(stat)"
12
+ :role="stat.action ? 'button' : undefined"
13
+ :tabindex="stat.action ? 0 : undefined"
14
+ @click="handleAction(stat, index)"
15
+ @keydown.enter.prevent="handleAction(stat, index)"
16
+ @keydown.space.prevent="handleAction(stat, index)"
11
17
  >
12
18
  <div
13
19
  v-if="stat.icon"
@@ -39,6 +45,12 @@
39
45
  v-for="(stat, index) in normalizedStats"
40
46
  :key="resolveKey(stat, index)"
41
47
  class="border-b border-gray-200 last:border-b-0 dark:border-slate-700"
48
+ :class="entryClass(stat)"
49
+ :role="stat.action ? 'button' : undefined"
50
+ :tabindex="stat.action ? 0 : undefined"
51
+ @click="handleAction(stat, index)"
52
+ @keydown.enter.prevent="handleAction(stat, index)"
53
+ @keydown.space.prevent="handleAction(stat, index)"
42
54
  >
43
55
  <td class="px-4 py-3 text-sm text-gray-600 dark:text-slate-400">
44
56
  <div class="flex items-center gap-2">
@@ -89,15 +101,18 @@ export default {
89
101
  validator: (value) => ['col', 'row'].includes(value),
90
102
  },
91
103
  },
104
+ emits: ['action'],
92
105
  computed: {
93
106
  isRowFormat() {
94
107
  return this.format === 'row'
95
108
  },
96
109
  normalizedStats() {
97
110
  return this.stats.map((stat) => ({
111
+ raw: stat,
98
112
  label: stat?.label ?? '',
99
113
  value: stat?.value ?? '-',
100
114
  icon: stat?.icon ?? null,
115
+ action: typeof stat?.action === 'function' ? stat.action : null,
101
116
  }))
102
117
  },
103
118
  },
@@ -105,6 +120,16 @@ export default {
105
120
  resolveKey(stat, index) {
106
121
  return `${String(stat.label)}-${index}`
107
122
  },
123
+ entryClass(stat) {
124
+ return stat.action
125
+ ? 'cursor-pointer transition hover:border-primary/40 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-primary/20 dark:hover:bg-slate-700/70'
126
+ : ''
127
+ },
128
+ handleAction(stat, index) {
129
+ if (!stat.action) return
130
+ stat.action(stat.raw, index)
131
+ this.$emit('action', stat.raw, index)
132
+ },
108
133
  },
109
134
  }
110
135
  </script>
@@ -0,0 +1,209 @@
1
+ <template>
2
+ <div class="w-full">
3
+ <div
4
+ v-if="title"
5
+ :class="titleClass"
6
+ >
7
+ {{ title }}
8
+ </div>
9
+ <div :class="containerClass">
10
+ <button
11
+ v-for="(option, index) in normalizedOptions"
12
+ :key="resolveKey(option, index)"
13
+ type="button"
14
+ class="flex w-full text-left transition"
15
+ :class="buttonClass(option)"
16
+ @click="toggleOption(option)"
17
+ >
18
+ <div
19
+ v-if="option.icon"
20
+ class="shrink-0"
21
+ :class="iconWrapperClass(option)"
22
+ >
23
+ <kIcon
24
+ v-if="typeof option.icon === 'string'"
25
+ :name="option.icon"
26
+ :size="iconSize"
27
+ />
28
+ <component
29
+ :is="option.icon"
30
+ v-else
31
+ :class="iconComponentClass"
32
+ />
33
+ </div>
34
+
35
+ <div class="min-w-0 flex-1">
36
+ <div :class="contentRowClass">
37
+ <div class="min-w-0">
38
+ <div class="truncate text-sm font-semibold">
39
+ {{ option.label }}
40
+ </div>
41
+ <div
42
+ v-if="option.description"
43
+ :class="descriptionClass"
44
+ >
45
+ {{ option.description }}
46
+ </div>
47
+ </div>
48
+ <span
49
+ :class="checkClass(option)"
50
+ >
51
+
52
+ </span>
53
+ </div>
54
+ </div>
55
+ </button>
56
+ </div>
57
+ </div>
58
+ </template>
59
+
60
+ <script>
61
+ import kIcon from "./kIcon.vue";
62
+
63
+ export default {
64
+ name: "kOptionGroup",
65
+ components: {
66
+ kIcon,
67
+ },
68
+ props: {
69
+ modelValue: {
70
+ type: Array,
71
+ default: () => [],
72
+ },
73
+ options: {
74
+ type: Array,
75
+ default: () => [],
76
+ },
77
+ selection: {
78
+ type: String,
79
+ default: "multiple",
80
+ validator: (value) => ["single", "multiple"].includes(value),
81
+ },
82
+ variant: {
83
+ type: String,
84
+ default: "table",
85
+ validator: (value) => ["table", "card"].includes(value),
86
+ },
87
+ title: {
88
+ type: String,
89
+ default: "",
90
+ },
91
+ valueKey: {
92
+ type: String,
93
+ default: "value",
94
+ },
95
+ labelKey: {
96
+ type: String,
97
+ default: "label",
98
+ },
99
+ descriptionKey: {
100
+ type: String,
101
+ default: "description",
102
+ },
103
+ iconKey: {
104
+ type: String,
105
+ default: "icon",
106
+ },
107
+ },
108
+ emits: ["update:modelValue", "change"],
109
+ computed: {
110
+ isMultiple() {
111
+ return this.selection === "multiple";
112
+ },
113
+ isTableVariant() {
114
+ return this.variant === "table";
115
+ },
116
+ normalizedSelection() {
117
+ return Array.isArray(this.modelValue) ? this.modelValue : [];
118
+ },
119
+ normalizedOptions() {
120
+ return this.options.map((option) => ({
121
+ raw: option,
122
+ value: option?.[this.valueKey],
123
+ label: option?.[this.labelKey] ?? "",
124
+ description: option?.[this.descriptionKey] ?? "",
125
+ icon: option?.[this.iconKey] ?? null,
126
+ }));
127
+ },
128
+ containerClass() {
129
+ return this.isTableVariant
130
+ ? "overflow-hidden rounded-xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"
131
+ : "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3";
132
+ },
133
+ titleClass() {
134
+ return this.isTableVariant
135
+ ? "mb-2 text-xs font-semibold uppercase tracking-[0.12em] text-slate-500 dark:text-slate-400"
136
+ : "mb-3 text-sm font-semibold text-slate-800 dark:text-slate-100";
137
+ },
138
+ contentRowClass() {
139
+ return this.isTableVariant ? "flex items-center justify-between gap-3" : "flex items-start justify-between gap-3";
140
+ },
141
+ descriptionClass() {
142
+ return this.isTableVariant
143
+ ? "mt-0.5 text-xs text-gray-500 dark:text-slate-400"
144
+ : "mt-1 text-xs text-gray-500 dark:text-slate-400";
145
+ },
146
+ iconSize() {
147
+ return this.isTableVariant ? 16 : 18;
148
+ },
149
+ iconComponentClass() {
150
+ return this.isTableVariant ? "h-4 w-4" : "h-[18px] w-[18px]";
151
+ },
152
+ },
153
+ methods: {
154
+ isSelected(option) {
155
+ return this.normalizedSelection.includes(option.value);
156
+ },
157
+ toggleOption(option) {
158
+ let nextSelection = [];
159
+
160
+ if (this.isMultiple) {
161
+ nextSelection = [...this.normalizedSelection];
162
+ const selectedIndex = nextSelection.indexOf(option.value);
163
+
164
+ if (selectedIndex === -1) nextSelection.push(option.value);
165
+ else nextSelection.splice(selectedIndex, 1);
166
+ } else {
167
+ nextSelection = this.isSelected(option) ? [] : [option.value];
168
+ }
169
+
170
+ this.$emit("update:modelValue", nextSelection);
171
+ this.$emit("change", nextSelection);
172
+ },
173
+ resolveKey(option, index) {
174
+ return `${String(option.value ?? option.label)}-${index}`;
175
+ },
176
+ buttonClass(option) {
177
+ if (this.isTableVariant) {
178
+ return this.isSelected(option)
179
+ ? "items-center gap-3 border-b border-slate-200 bg-primary/10 px-3 py-3 text-primary last:border-b-0 dark:border-slate-700 dark:bg-primary/15 dark:text-slate-100"
180
+ : "items-center gap-3 border-b border-slate-200 bg-white px-3 py-3 text-gray-700 hover:bg-slate-50 last:border-b-0 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800";
181
+ }
182
+
183
+ return this.isSelected(option)
184
+ ? "items-start gap-3 rounded-xl border border-primary bg-primary/10 p-3 text-primary dark:border-primary dark:bg-primary/15 dark:text-slate-100"
185
+ : "items-start gap-3 rounded-xl border border-gray-200 bg-white p-3 text-gray-700 hover:border-primary/40 hover:bg-primary/5 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:border-primary/50 dark:hover:bg-primary/10";
186
+ },
187
+ iconWrapperClass(option) {
188
+ if (this.isTableVariant) {
189
+ return this.isSelected(option)
190
+ ? "flex h-8 w-8 items-center justify-center rounded-md bg-primary/15 text-primary"
191
+ : "flex h-8 w-8 items-center justify-center rounded-md bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-300";
192
+ }
193
+
194
+ return "flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary";
195
+ },
196
+ checkClass(option) {
197
+ if (this.isTableVariant) {
198
+ return this.isSelected(option)
199
+ ? "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary text-[11px] font-semibold text-white"
200
+ : "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-slate-300 text-[11px] font-semibold text-transparent dark:border-slate-600";
201
+ }
202
+
203
+ return this.isSelected(option)
204
+ ? "mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-primary bg-primary text-[11px] font-semibold text-white"
205
+ : "mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-gray-300 text-[11px] font-semibold text-transparent dark:border-slate-600";
206
+ },
207
+ },
208
+ };
209
+ </script>