ketekny-ui-kit 1.0.137 → 1.0.139

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/README.md CHANGED
@@ -42,6 +42,7 @@ import "ketekny-ui-kit/styles.css";
42
42
  import {
43
43
  kButton,
44
44
  kInput,
45
+ kRange,
45
46
  kSelect,
46
47
  kDateSelector,
47
48
  kToggle,
@@ -58,6 +59,7 @@ const app = createApp(App);
58
59
 
59
60
  app.component("kButton", kButton);
60
61
  app.component("kInput", kInput);
62
+ app.component("kRange", kRange);
61
63
  app.component("kSelect", kSelect);
62
64
  app.component("kDateSelector", kDateSelector);
63
65
  app.component("kToggle", kToggle);
@@ -194,6 +196,49 @@ module.exports = {
194
196
 
195
197
  Note: exact option keys may vary by component/plugin implementation version.
196
198
 
199
+ ## Integer Range
200
+
201
+ `kRange` is a dual-handle slider that selects two integers. Its `v-model` is a
202
+ two-item array in `[from, to]` order.
203
+
204
+ ```vue
205
+ <template>
206
+ <kRange
207
+ v-model="range"
208
+ label="Allowed age"
209
+ :min="18"
210
+ :max="80"
211
+ :step="1"
212
+ />
213
+ </template>
214
+
215
+ <script setup>
216
+ import { ref } from "vue";
217
+ import { kRange } from "ketekny-ui-kit";
218
+
219
+ const range = ref([25, 65]);
220
+ </script>
221
+ ```
222
+
223
+ The configured minimum and maximum appear at the slider ends. Selected values
224
+ appear below their handles.
225
+
226
+ Props:
227
+
228
+ - `modelValue` (`Number[]`, default `[0, 100]`): selected `[from, to]` integers.
229
+ - `min` (`Number`, default `0`): minimum selectable value.
230
+ - `max` (`Number`, default `100`): maximum selectable value.
231
+ - `step` (`Number`, default `1`): positive integer increment.
232
+ - `label`, `info`, `error`, `id`: standard field metadata.
233
+ - `disabled`: disables the slider.
234
+ - `showStops`: shows every step along the slider.
235
+ - `showTooltip`: shows handle tooltips while dragging.
236
+
237
+ Events:
238
+
239
+ - `update:modelValue`: emitted while either handle moves.
240
+ - `change`: emitted when the selected range changes.
241
+
197
242
  ## Exports
198
243
 
199
244
  Components:
@@ -201,7 +246,7 @@ Components:
201
246
  - `kMessage`, `kCode`, `kToolbar`, `kTable`, `kTabs`, `kChip`
202
247
  - `kSpinner`, `kDatatable`, `kIcon`, `kMenu`, `kSkeleton`
203
248
  - `kProgressBar`, `kPagination`, `kTree`, `kButton`, `kSelect`, `kUploader`
204
- - `kToggle`, `kCheckbox`, `kInput`, `kDateSelector`, `kEditor`
249
+ - `kToggle`, `kCheckbox`, `kInput`, `kRange`, `kDateSelector`, `kEditor`
205
250
  - `kSelectButton`, `kTags`, `kSearch`, `kArrayList`, `kList`, `kTextArea`
206
251
  - `kDialog`, `kDrawer`
207
252
  - `kAppHeader`, `kAppFooter`, `kAppMain`, `kHero`
package/index.js CHANGED
@@ -9,6 +9,7 @@ import kCode from './src/ui/kCode.vue'
9
9
  import kDialog from './src/ui/kDialog.vue'
10
10
  import kDrawer from './src/ui/kDrawer.vue'
11
11
  import kInput from './src/ui/kInput.vue'
12
+ import kRange from './src/ui/kRange.vue'
12
13
  import kDateSelector from './src/ui/kDateSelector.vue'
13
14
  import kToolbar from './src/ui/kToolbar.vue'
14
15
  import kTooltip from './src/ui/kTooltip.vue'
@@ -63,7 +64,7 @@ export {
63
64
  kMessage, kCode, kToolbar, kTooltip, kTable, kTabs, kCard, kChip, kClipboard, kSpinner, kDatatable, kIcon, kMenu, kSkeleton, kProgressBar, kPagination, kTree,
64
65
 
65
66
  // Form Components
66
- kButton, kCheckbox, kSelect, kUploader, kToggle, kInput, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea, kForm, kOptionGroup,
67
+ kButton, kCheckbox, kSelect, kUploader, kToggle, kInput, kRange, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea, kForm, kOptionGroup,
67
68
 
68
69
  // Dialogs
69
70
  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.137",
4
+ "version": "1.0.139",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -0,0 +1,130 @@
1
+ <template>
2
+ <div class="w-full text-primary/90">
3
+ <label v-if="label != null" :for="rangeId" class="inputLabel" :class="labelClass">
4
+ {{ label }}
5
+ </label>
6
+
7
+ <div class="flex items-start gap-4">
8
+ <span class="min-w-8 pt-2 text-right text-sm tabular-nums">{{ min }}</span>
9
+ <div class="relative min-w-0 flex-1 pb-7">
10
+ <el-slider
11
+ :id="rangeId"
12
+ :model-value="rangeValue"
13
+ :min="min"
14
+ :max="max"
15
+ :step="step"
16
+ :disabled="disabled"
17
+ :show-stops="showStops"
18
+ :show-tooltip="showTooltip"
19
+ range
20
+ @update:model-value="updateValue"
21
+ @change="$emit('change', $event)"
22
+ />
23
+ <span
24
+ v-for="marker in selectedMarkers"
25
+ :key="marker.key"
26
+ class="absolute top-8 text-sm tabular-nums"
27
+ :class="marker.alignment"
28
+ :style="{ left: `${marker.position}%` }"
29
+ >
30
+ {{ marker.label }}
31
+ </span>
32
+ </div>
33
+ <span class="min-w-8 pt-2 text-sm tabular-nums">{{ max }}</span>
34
+ </div>
35
+
36
+ <div :id="errorId" :class="theme.errorText" v-if="hasError && error !== true && error !== ''">
37
+ {{ error }}
38
+ </div>
39
+ <div :id="infoId" :class="theme.infoText" v-else-if="info != null">
40
+ {{ info }}
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <script>
46
+ import { ElSlider } from "element-plus";
47
+ import { K_INPUT_THEME } from "./themes/kInput.theme";
48
+
49
+ export default {
50
+ name: "kRange",
51
+ components: { ElSlider },
52
+ props: {
53
+ modelValue: {
54
+ type: Array,
55
+ default: () => [0, 100],
56
+ validator: (value) => value.length === 2,
57
+ },
58
+ min: { type: Number, default: 0 },
59
+ max: { type: Number, default: 100 },
60
+ step: {
61
+ type: Number,
62
+ default: 1,
63
+ validator: (value) => Number.isInteger(value) && value > 0,
64
+ },
65
+ label: { type: String, default: null },
66
+ info: { type: String, default: null },
67
+ error: { type: [String, Boolean], default: null },
68
+ disabled: { type: Boolean, default: false },
69
+ showStops: { type: Boolean, default: false },
70
+ showTooltip: { type: Boolean, default: true },
71
+ id: { type: String, default: null },
72
+ },
73
+ emits: ["update:modelValue", "change"],
74
+ data() {
75
+ return {
76
+ theme: K_INPUT_THEME,
77
+ generatedId: `range-${Math.random().toString(36).slice(2, 11)}`,
78
+ };
79
+ },
80
+ computed: {
81
+ rangeId() {
82
+ return this.id || this.generatedId;
83
+ },
84
+ errorId() {
85
+ return `${this.rangeId}-error`;
86
+ },
87
+ infoId() {
88
+ return `${this.rangeId}-info`;
89
+ },
90
+ rangeValue() {
91
+ const values = this.modelValue?.length === 2 ? this.modelValue : [this.min, this.max];
92
+ return values.map((value, index) => {
93
+ const fallback = index === 0 ? this.min : this.max;
94
+ const integer = Number.isFinite(Number(value)) ? Math.round(Number(value)) : fallback;
95
+ return Math.min(this.max, Math.max(this.min, integer));
96
+ });
97
+ },
98
+ selectedMarkers() {
99
+ if (this.rangeValue[0] === this.rangeValue[1]) {
100
+ return [this.createMarker(this.rangeValue[0], `${this.rangeValue[0]} – ${this.rangeValue[1]}`, "range")];
101
+ }
102
+ return [
103
+ this.createMarker(this.rangeValue[0], this.rangeValue[0], "from"),
104
+ this.createMarker(this.rangeValue[1], this.rangeValue[1], "to"),
105
+ ];
106
+ },
107
+ hasError() {
108
+ return this.error != null && this.error !== false;
109
+ },
110
+ labelClass() {
111
+ if (this.hasError) return this.theme.labelError;
112
+ if (this.disabled) return this.theme.labelDisabled;
113
+ return this.theme.label;
114
+ },
115
+ },
116
+ methods: {
117
+ createMarker(value, label, key) {
118
+ const span = this.max - this.min;
119
+ const position = span === 0 ? 0 : ((value - this.min) / span) * 100;
120
+ let alignment = "-translate-x-1/2";
121
+ if (position <= 0) alignment = "translate-x-0";
122
+ if (position >= 100) alignment = "-translate-x-full";
123
+ return { key, label, position, alignment };
124
+ },
125
+ updateValue(value) {
126
+ this.$emit("update:modelValue", value.map((item) => Math.round(item)));
127
+ },
128
+ },
129
+ };
130
+ </script>
@@ -4,7 +4,7 @@
4
4
  {{ label }}
5
5
  </label>
6
6
 
7
- <SelectRoot v-model="internalValue" v-model:open="open" :disabled="disabled">
7
+ <SelectRoot v-model="internalValue" v-model:open="open" :disabled="disabled" :multiple="multiple">
8
8
  <SelectTrigger as-child>
9
9
  <div
10
10
  :id="computedId"
@@ -13,9 +13,32 @@
13
13
  :aria-label="label || 'Select option'"
14
14
  >
15
15
  <SelectValue
16
+ v-if="!multiple"
16
17
  :placeholder="placeholder"
17
18
  class="block pr-10 truncate text-gray-700 data-[placeholder]:text-gray-400 dark:text-slate-100 dark:data-[placeholder]:text-slate-500"
18
19
  />
20
+ <div v-else class="flex flex-wrap items-center gap-1 pr-10">
21
+ <span v-if="selectedOptions.length === 0" class="text-gray-400 dark:text-slate-500">
22
+ {{ placeholder }}
23
+ </span>
24
+ <kChip
25
+ v-for="option in selectedOptions"
26
+ :key="option[optionValue]"
27
+ size="small"
28
+ >
29
+ <span>{{ option[optionLabel] }}</span>
30
+ <button
31
+ v-if="!disabled"
32
+ type="button"
33
+ class="ml-1 text-primary/70 transition-colors hover:text-primary"
34
+ :aria-label="`Remove ${option[optionLabel]}`"
35
+ @pointerdown.stop.prevent
36
+ @click.stop.prevent="removeSelection(option[optionValue])"
37
+ >
38
+ <X class="w-3 h-3" />
39
+ </button>
40
+ </kChip>
41
+ </div>
19
42
 
20
43
  <button
21
44
  v-if="clearable && isSelectionSet && !disabled"
@@ -113,10 +136,12 @@ import {
113
136
  SelectViewport,
114
137
  } from "reka-ui";
115
138
  import { X, ChevronDown, Check, Search, SearchX } from "@lucide/vue";
139
+ import kChip from "./kChip.vue";
116
140
 
117
141
  const props = defineProps({
118
142
  options: { type: Array, required: true },
119
- modelValue: [String, Number],
143
+ modelValue: [String, Number, Array],
144
+ multiple: { type: Boolean, default: false },
120
145
  optionValue: { type: String, default: "value" },
121
146
  optionLabel: { type: String, default: "label" },
122
147
  label: String,
@@ -177,15 +202,25 @@ function normalizeDropdownHeight(value) {
177
202
 
178
203
  const computedId = computed(() => (props.id != null && props.id !== "" ? props.id : generatedId));
179
204
  const hasError = computed(() => props.error != null && props.error !== false);
180
- const isSelectionSet = computed(() => props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== "");
205
+ const isSelectionSet = computed(() => {
206
+ if (props.multiple) return Array.isArray(props.modelValue) && props.modelValue.length > 0;
207
+ return props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== "";
208
+ });
181
209
  const resolvedDropdownHeight = computed(() => normalizeDropdownHeight(props.dropdownHeight));
210
+ const selectedOptions = computed(() => {
211
+ if (!props.multiple || !Array.isArray(props.modelValue)) return [];
212
+ return props.modelValue
213
+ .map((value) => props.options.find((option) => option?.[props.optionValue] === value))
214
+ .filter(Boolean);
215
+ });
182
216
 
183
217
  const internalValue = computed({
184
218
  get() {
219
+ if (props.multiple) return Array.isArray(props.modelValue) ? props.modelValue : [];
185
220
  return props.modelValue === null ? undefined : props.modelValue;
186
221
  },
187
222
  set(val) {
188
- emit("update:modelValue", val ?? null);
223
+ emit("update:modelValue", props.multiple ? (Array.isArray(val) ? val : []) : (val ?? null));
189
224
  },
190
225
  });
191
226
 
@@ -219,10 +254,15 @@ watch(open, (val) => {
219
254
  });
220
255
 
221
256
  function clearSelection() {
222
- emit("update:modelValue", null);
257
+ emit("update:modelValue", props.multiple ? [] : null);
223
258
  open.value = false;
224
259
  }
225
260
 
261
+ function removeSelection(value) {
262
+ if (props.disabled || !Array.isArray(props.modelValue)) return;
263
+ emit("update:modelValue", props.modelValue.filter((selectedValue) => selectedValue !== value));
264
+ }
265
+
226
266
  function getOptionNodes() {
227
267
  const el = viewportRef.value?.$el ?? viewportRef.value;
228
268
  if (!el) return [];