ketekny-ui-kit 1.0.115 → 1.0.117

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,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.115",
4
+ "version": "1.0.117",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -58,9 +58,10 @@
58
58
  :label="mobileSidebarOpen ? 'Hide sidebar' : 'Show sidebar'"
59
59
  :icon="mobileSidebarOpen ? 'PanelLeftClose' : 'PanelLeftOpen'"
60
60
  variant="soft"
61
+ size="large"
61
62
  :icon-only="!mobileSidebarOpen"
62
63
  :aria-label="mobileSidebarOpen ? null : 'Show sidebar'"
63
- :class="mobileSidebarOpen ? '' : 'rounded-full shadow-lg shadow-slate-900/15'"
64
+ :class="mobileSidebarOpen ? '' : 'rounded-full text-slate-900 shadow-lg shadow-slate-900/15'"
64
65
  @click="toggleMobileSidebar"
65
66
  />
66
67
  </div>
@@ -43,8 +43,8 @@
43
43
  type="button"
44
44
  class="fixed bottom-0 left-0 z-50 inline-flex size-14 items-center justify-center rounded-tr-2xl transition-colors lg:hidden"
45
45
  :class="theme === 'light'
46
- ? 'border border-slate-200 bg-white text-slate-700 shadow-lg shadow-slate-900/10 hover:bg-slate-100'
47
- : 'bg-sidebar text-white shadow-lg shadow-black/25 hover:bg-sidebar-accent'"
46
+ ? 'border border-rose-200 bg-rose-100 text-rose-900 shadow-lg shadow-slate-900/10 hover:bg-rose-200'
47
+ : ' bg-[#e63933] text-white shadow-lg shadow-black/25'"
48
48
  aria-label="Open menu"
49
49
  @click="toggleMenu">
50
50
  <Menu class="size-6" :stroke-width="2.25" />
package/src/ui/kMenu.vue CHANGED
@@ -53,6 +53,13 @@
53
53
  @click="handleLinkClick(link, $event)">
54
54
  <kIcon v-if="link.icon" :name="link.icon" :size="16" class="shrink-0 self-center" :class="iconClass(link)" />
55
55
  <span class="leading-none">{{ link.label }}</span>
56
+ <kIcon
57
+ v-if="link.checked"
58
+ name="Check"
59
+ :size="16"
60
+ class="ml-auto shrink-0"
61
+ :class="checkedIconClass(link)"
62
+ />
56
63
  </component>
57
64
  </li>
58
65
  </ul>
@@ -135,6 +142,10 @@ export default {
135
142
  if (link?.tone === "danger") return "text-orange-500 dark:text-orange-300";
136
143
  return "text-primary/80";
137
144
  },
145
+ checkedIconClass(link) {
146
+ if (link?.tone === "danger") return "text-orange-500 dark:text-orange-300";
147
+ return "text-primary dark:text-primary";
148
+ },
138
149
  handleLinkClick(link, event) {
139
150
  if (link?.disabled) {
140
151
  event.preventDefault();
@@ -37,24 +37,48 @@
37
37
  </button>
38
38
 
39
39
  <div v-if="actions.length && !isCompact" class="flex flex-wrap items-center justify-end gap-2 ml-auto shrink-0">
40
- <kButton
41
- v-for="(action, index) in actions"
42
- :key="`${action.label || 'action'}-${index}`"
43
- :label="action.label"
44
- :icon="action.icon"
45
- :loading="!!action.loading"
46
- :disabled="disabled || !!action.disabled"
47
- :aria-label="action.ariaLabel || action.label"
48
- :tooltip="action.tooltip"
49
- :size="resolveActionButtonSize(action)"
50
- :small="action.small === true"
51
- :icon-only="!!action.iconOnly"
52
- :variant="action.variant || 'outlined'"
53
- :secondary="!!action.secondary"
54
- :success="!!action.success"
55
- :warning="!!action.warning"
56
- :danger="!!action.danger"
57
- @click="handleActionClick(action)" />
40
+ <template v-for="(action, index) in actions" :key="`${action.label || 'action'}-${index}`">
41
+ <kMenu
42
+ v-if="isActionGroup(action)"
43
+ :links="resolveActionGroupLinks(action)"
44
+ :aria-label="action.ariaLabel || action.label || 'Action group'"
45
+ >
46
+ <template #trigger>
47
+ <button
48
+ type="button"
49
+ :disabled="disabled || !!action.disabled"
50
+ :aria-label="action.ariaLabel || action.label"
51
+ :class="resolveActionGroupTriggerClass(action)"
52
+ >
53
+ <kIcon
54
+ v-if="action.icon"
55
+ :name="action.icon"
56
+ :size="resolveActionGroupIconSize(action)"
57
+ class="shrink-0"
58
+ />
59
+ <span v-if="!action.iconOnly" class="truncate">{{ action.label }}</span>
60
+ <kIcon v-if="!action.iconOnly" name="ChevronDown" :size="16" class="shrink-0 opacity-80" />
61
+ </button>
62
+ </template>
63
+ </kMenu>
64
+ <kButton
65
+ v-else
66
+ :label="action.label"
67
+ :icon="action.icon"
68
+ :loading="!!action.loading"
69
+ :disabled="disabled || !!action.disabled"
70
+ :aria-label="action.ariaLabel || action.label"
71
+ :tooltip="action.tooltip"
72
+ :size="resolveActionButtonSize(action)"
73
+ :small="action.small === true"
74
+ :icon-only="!!action.iconOnly"
75
+ :variant="action.variant || 'outlined'"
76
+ :secondary="!!action.secondary"
77
+ :success="!!action.success"
78
+ :warning="!!action.warning"
79
+ :danger="!!action.danger"
80
+ @click="handleActionClick(action)" />
81
+ </template>
58
82
  </div>
59
83
  </div>
60
84
 
@@ -79,20 +103,42 @@
79
103
  </div>
80
104
 
81
105
  <div class="flex flex-col overflow-y-auto max-h-[60vh]">
82
- <button
83
- v-for="(action, index) in actions"
84
- :key="`mobile-${action.label || 'action'}-${index}`"
85
- type="button"
86
- :disabled="disabled || !!action.disabled || !!action.loading"
87
- :class="[
88
- 'flex items-center w-full gap-3 px-2 py-3 text-left rounded-lg min-h-11',
89
- disabled || action.disabled || action.loading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary/10 active:bg-primary/15',
90
- ]"
91
- @click="handleMobileAction(action)">
92
- <kIcon v-if="action.icon" :name="action.icon" :size="18" class="shrink-0 text-slate-600 dark:text-slate-400" />
93
- <span class="text-sm font-medium text-slate-900 dark:text-slate-200">{{ action.label }}</span>
94
- <span v-if="action.loading" class="w-4 h-4 ml-auto border-2 rounded-full border-slate-300 border-t-transparent animate-spin dark:border-slate-600"></span>
95
- </button>
106
+ <template v-for="(action, index) in actions" :key="`mobile-${action.label || 'action'}-${index}`">
107
+ <kMenu
108
+ v-if="isActionGroup(action)"
109
+ class="w-full"
110
+ :links="resolveActionGroupLinks(action)"
111
+ :aria-label="action.ariaLabel || action.label || 'Action group'"
112
+ >
113
+ <template #trigger>
114
+ <button
115
+ type="button"
116
+ :disabled="disabled || !!action.disabled"
117
+ :class="[
118
+ 'flex items-center w-full gap-3 px-2 py-3 text-left rounded-lg min-h-11',
119
+ disabled || action.disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary/10 active:bg-primary/15',
120
+ ]"
121
+ >
122
+ <kIcon v-if="action.icon" :name="action.icon" :size="18" class="shrink-0 text-slate-600 dark:text-slate-400" />
123
+ <span class="text-sm font-medium text-slate-900 dark:text-slate-200">{{ action.label }}</span>
124
+ <kIcon name="ChevronDown" :size="16" class="ml-auto shrink-0 text-slate-500 dark:text-slate-400" />
125
+ </button>
126
+ </template>
127
+ </kMenu>
128
+ <button
129
+ v-else
130
+ type="button"
131
+ :disabled="disabled || !!action.disabled || !!action.loading"
132
+ :class="[
133
+ 'flex items-center w-full gap-3 px-2 py-3 text-left rounded-lg min-h-11',
134
+ disabled || action.disabled || action.loading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary/10 active:bg-primary/15',
135
+ ]"
136
+ @click="handleMobileAction(action)">
137
+ <kIcon v-if="action.icon" :name="action.icon" :size="18" class="shrink-0 text-slate-600 dark:text-slate-400" />
138
+ <span class="text-sm font-medium text-slate-900 dark:text-slate-200">{{ action.label }}</span>
139
+ <span v-if="action.loading" class="w-4 h-4 ml-auto border-2 rounded-full border-slate-300 border-t-transparent animate-spin dark:border-slate-600"></span>
140
+ </button>
141
+ </template>
96
142
  </div>
97
143
  </section>
98
144
  </div>
@@ -103,6 +149,13 @@
103
149
  import { ref, watch, nextTick, onMounted, onBeforeUnmount } from "vue";
104
150
  import kButton from "./kButton.vue";
105
151
  import kIcon from "./kIcon.vue";
152
+ import kMenu from "./kMenu.vue";
153
+ import {
154
+ K_BUTTON_BASE,
155
+ K_BUTTON_FOCUS_RING,
156
+ K_BUTTON_SIZE_STYLES,
157
+ K_BUTTON_VARIANT_STYLES,
158
+ } from "./themes/kButton.theme";
106
159
 
107
160
  const props = defineProps({
108
161
  title: {
@@ -140,6 +193,18 @@ const isCompact = ref(false);
140
193
  let resizeObserver = null;
141
194
 
142
195
  const VALID_BUTTON_SIZES = ["small", "normal", "large"];
196
+ const ACTION_GROUP_KEYS = ["items", "children", "links"];
197
+
198
+ const isActionGroup = (action) =>
199
+ ACTION_GROUP_KEYS.some((key) => Array.isArray(action?.[key]) && action[key].length > 0);
200
+
201
+ const resolveActionGroupLinks = (action) => {
202
+ const items = ACTION_GROUP_KEYS.map((key) => action?.[key]).find((value) => Array.isArray(value)) || [];
203
+ return items.map((item) => ({
204
+ ...item,
205
+ action: item?.action || item?.onClick,
206
+ }));
207
+ };
143
208
 
144
209
  const handleActionClick = (action) => {
145
210
  if (typeof action?.onClick === "function") action.onClick();
@@ -157,6 +222,52 @@ const resolveActionButtonSize = (action) => {
157
222
  return "normal";
158
223
  };
159
224
 
225
+ const resolveActionGroupIntent = (action) => {
226
+ if (action?.danger) return "danger";
227
+ if (action?.success) return "success";
228
+ if (action?.warning) return "warning";
229
+ if (action?.secondary) return "secondary";
230
+ return "primary";
231
+ };
232
+
233
+ const resolveActionGroupVariant = (action) => {
234
+ if (action?.outlined) return "outlined";
235
+ if (action?.secondary && !action?.variant) return "soft";
236
+ return action?.variant || "outlined";
237
+ };
238
+
239
+ const resolveActionGroupSizeKey = (action) => {
240
+ const size = resolveActionButtonSize(action);
241
+ return K_BUTTON_SIZE_STYLES[size] ? size : "normal";
242
+ };
243
+
244
+ const resolveActionGroupTriggerClass = (action) => {
245
+ const intent = resolveActionGroupIntent(action);
246
+ const variant = resolveActionGroupVariant(action);
247
+ const sizeSet = K_BUTTON_SIZE_STYLES[resolveActionGroupSizeKey(action)];
248
+ const intentSet = K_BUTTON_VARIANT_STYLES[intent] || K_BUTTON_VARIANT_STYLES.primary;
249
+ const styleSet = intentSet[variant] || intentSet.outlined;
250
+ const variantClass = (action?.disabled || props.disabled) ? styleSet.disabled : styleSet.enabled;
251
+ const sizeClass = action?.iconOnly ? sizeSet.iconOnly : sizeSet.regular;
252
+
253
+ return [
254
+ K_BUTTON_BASE,
255
+ K_BUTTON_FOCUS_RING,
256
+ variantClass,
257
+ sizeClass,
258
+ "gap-2",
259
+ action?.fullWidth ? "w-full justify-center" : action?.iconOnly ? "" : "w-max shrink-0",
260
+ ];
261
+ };
262
+
263
+ const resolveActionGroupIconSize = (action) => {
264
+ const sizeSet = K_BUTTON_SIZE_STYLES[resolveActionGroupSizeKey(action)];
265
+ const iconClass = sizeSet?.icon || "h-4 w-4";
266
+ const match = /(?:^|\s)(?:size-|h-)(\d+(?:\.\d+)?)/.exec(iconClass);
267
+ if (!match) return 16;
268
+ return Number(match[1]) * 4;
269
+ };
270
+
160
271
  const updateCompactMode = () => {
161
272
  if (!toolbarRef.value) return;
162
273
  isCompact.value = toolbarRef.value.clientWidth < 768;