@katlux/preset-modern 0.1.0-beta.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.
@@ -0,0 +1,136 @@
1
+ <template lang="pug">
2
+ label.KCheckbox.modern-checkbox
3
+ input(type="checkbox" v-model="formattedModel" :value="checkboxValue" :disabled="isDisabled")
4
+ .checkbox-box
5
+ .checkbox-checkmark
6
+ KIcon.icon(iconname="check")
7
+ .checkbox-label
8
+ slot
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ // Modern template for KCheckbox
13
+ const props = defineProps<{
14
+ model: any
15
+ checkboxValue: any
16
+ isDisabled: boolean
17
+ }>()
18
+
19
+ const emit = defineEmits<{
20
+ (e: 'update:model', value: any): void
21
+ }>()
22
+
23
+ const formattedModel = computed({
24
+ get: () => props.model,
25
+ set: (value) => emit('update:model', value)
26
+ })
27
+ </script>
28
+
29
+ <style lang="scss" scoped>
30
+ .KCheckbox.modern-checkbox {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: var(--gap-sm);
34
+ cursor: pointer;
35
+ user-select: none;
36
+ transition: all var(--transition-fast);
37
+
38
+ &:hover {
39
+ .checkbox-box {
40
+ border-color: var(--primary-500);
41
+ box-shadow: var(--input-focus-shadow);
42
+ }
43
+ }
44
+
45
+ input {
46
+ display: none;
47
+ }
48
+
49
+ .checkbox-box {
50
+ position: relative;
51
+ width: 22px;
52
+ height: 22px;
53
+ border: 2px solid var(--neutral-400);
54
+ border-radius: var(--border-radius-sm);
55
+ background: var(--background-color);
56
+ transition: all var(--transition-bounce);
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ flex-shrink: 0;
61
+
62
+ &::before {
63
+ content: '';
64
+ position: absolute;
65
+ inset: 0;
66
+ border-radius: 4px;
67
+ background: var(--checkbox-checked-bg);
68
+ opacity: 0;
69
+ transform: scale(0.8);
70
+ transition: all var(--transition-bounce);
71
+ }
72
+ }
73
+
74
+ .checkbox-checkmark {
75
+ position: relative;
76
+ z-index: 1;
77
+
78
+ .icon {
79
+ display: none;
80
+ color: white;
81
+ font-size: 14px;
82
+ animation: checkmark-pop 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
83
+ }
84
+ }
85
+
86
+ .checkbox-label {
87
+ font-size: 15px;
88
+ color: var(--font-color-primary);
89
+ line-height: 1.5;
90
+ }
91
+
92
+ // Checked state
93
+ input:checked + .checkbox-box {
94
+ border-color: var(--primary-500);
95
+
96
+ &::before {
97
+ opacity: 1;
98
+ transform: scale(1);
99
+ }
100
+
101
+ .checkbox-checkmark .icon {
102
+ display: block;
103
+ }
104
+ }
105
+
106
+ // Disabled state
107
+ input:disabled + .checkbox-box {
108
+ background-color: var(--neutral-200);
109
+ border-color: var(--neutral-300);
110
+ cursor: not-allowed;
111
+
112
+ &::before {
113
+ background: var(--neutral-400);
114
+ }
115
+ }
116
+
117
+ input:disabled ~ .checkbox-label {
118
+ color: var(--neutral-400);
119
+ cursor: not-allowed;
120
+ }
121
+ }
122
+
123
+ @keyframes checkmark-pop {
124
+ 0% {
125
+ transform: scale(0);
126
+ opacity: 0;
127
+ }
128
+ 50% {
129
+ transform: scale(1.2);
130
+ }
131
+ 100% {
132
+ transform: scale(1);
133
+ opacity: 1;
134
+ }
135
+ }
136
+ </style>
@@ -0,0 +1,216 @@
1
+ <template lang="pug">
2
+ .k-datatable.modern-datatable(:style="{ '--page-size': dataProvider?.pageSize?.value || 10 }")
3
+ .header(v-if="bulkActions && bulkActions.length > 0 && selectedRows.length > 0")
4
+ .selected-count {{ selectedRows.length }} items selected
5
+ KBulkActions.bulk-actions(:bulkActions="bulkActions" :selectedRows="selectedRows" :selectAll="selectAll")
6
+
7
+ .k-datatable-wrapper
8
+ .loading-overlay(v-if="dataProvider.loading.value")
9
+ .spinner
10
+
11
+ table
12
+ thead
13
+ tr
14
+ th.check-cell(v-if="bulkActions && bulkActions.length > 0")
15
+ KCheckbox(v-model="selectAll")
16
+ th(v-for="field in visibleFields || dataProvider.fields" :key="field" :data-field="field" :style="getColumnStyle(field)")
17
+ component(v-if="getHeaderSlot(field)" :is="getHeaderSlot(field)" :field="field")
18
+ span(v-else) {{ columnHeaders?.[field] ?? field }}
19
+ th.actions-cell(v-if="rowActions && rowActions.length > 0") Actions
20
+
21
+ tbody
22
+ tr(v-for="(row, index) in dataProvider.pageData.value" :key="index" :class="{ 'selected': selectedRows.includes(row) }")
23
+ td.check-cell(v-if="bulkActions && bulkActions.length > 0")
24
+ KCheckbox(:value="row" v-model="selectedRows")
25
+ td(v-for="visibleField in visibleFields || dataProvider.fields" :key="visibleField" :data-field="visibleField" :style="getColumnStyle(visibleField)")
26
+ component(v-if="getCellSlot(visibleField)" :is="getCellSlot(visibleField)" :row="row" :field="visibleField" :value="row[visibleField]")
27
+ span(v-else) {{ row[visibleField] }}
28
+ td.actions-cell(v-if="rowActions && rowActions.length > 0")
29
+ .action-buttons
30
+ KButton.action-btn(
31
+ v-for="rowAction in rowActions"
32
+ :key="rowAction.label"
33
+ :type="rowAction.type || 'secondary'"
34
+ size="small"
35
+ @click="rowAction.action(row)"
36
+ ) {{ rowAction.label }}
37
+
38
+ .footer
39
+ KPagination.pagination(:dataProvider="dataProvider")
40
+ </template>
41
+
42
+ <script setup lang="ts">
43
+ import { ADataProvider } from '@katlux/providers'
44
+ import type { IKDatatableAction } from '@katlux/providers'
45
+
46
+ // Define models using Vue 3.5 defineModel
47
+ const selectedRows = defineModel<any[]>('selectedRows', { default: [] })
48
+ const selectAll = defineModel<boolean>('selectAll', { default: false })
49
+
50
+ const props = defineProps<{
51
+ dataProvider: ADataProvider
52
+ visibleFields: Array<string> | null
53
+ rowActions: Array<IKDatatableAction>
54
+ bulkActions: Array<IKDatatableAction>
55
+ cellSlots?: Record<string, any>
56
+ headerSlots?: Record<string, any>
57
+ columnHeaders?: Record<string, string>
58
+ columnWidths?: Record<string, string>
59
+ }>()
60
+
61
+ const getCellSlot = (fieldName: string) => {
62
+ const slotName = 'cell-' + fieldName
63
+ return props.cellSlots?.[slotName]
64
+ }
65
+
66
+ const getHeaderSlot = (fieldName: string) => {
67
+ const slotName = 'header-' + fieldName
68
+ return props.headerSlots?.[slotName] || props.cellSlots?.[slotName]
69
+ }
70
+
71
+ const getColumnStyle = (field: string) => {
72
+ const w = props.columnWidths?.[field]
73
+ if (!w) return undefined
74
+ return { width: w, whiteSpace: 'nowrap' as const }
75
+ }
76
+ </script>
77
+
78
+ <style lang="scss" scoped>
79
+ .modern-datatable {
80
+ --page-height: 50px;
81
+ width: 100%;
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: var(--gap-md);
85
+ font-family: var(--font-family);
86
+
87
+ .header {
88
+ display: flex;
89
+ justify-content: space-between;
90
+ align-items: center;
91
+ padding: var(--gap-sm) var(--gap-lg);
92
+ background: var(--table-header-bg);
93
+ border-radius: var(--button-border-radius);
94
+ border: 1px solid var(--table-border-color);
95
+
96
+ .selected-count {
97
+ font-weight: var(--font-weight-semibold);
98
+ color: var(--font-color-secondary);
99
+ }
100
+ }
101
+
102
+ .k-datatable-wrapper {
103
+ position: relative;
104
+ border-radius: var(--border-radius-lg);
105
+ box-shadow: var(--shadow-md);
106
+ background: var(--background-color);
107
+ overflow: hidden;
108
+ border: 1px solid var(--table-border-color);
109
+
110
+ .loading-overlay {
111
+ position: absolute;
112
+ top: 0;
113
+ left: 0;
114
+ right: 0;
115
+ bottom: 0;
116
+ background: rgba(255, 255, 255, 0.7);
117
+ backdrop-filter: blur(2px);
118
+ display: flex;
119
+ justify-content: center;
120
+ align-items: center;
121
+ z-index: 10;
122
+ }
123
+ }
124
+
125
+ table {
126
+ width: 100%;
127
+ border-collapse: separate;
128
+ border-spacing: 0;
129
+
130
+ thead {
131
+ background: var(--table-header-bg);
132
+
133
+ th {
134
+ padding: var(--gap-md);
135
+ font-weight: var(--font-weight-semibold);
136
+ text-align: left;
137
+ color: var(--table-header-color);
138
+ font-size: var(--font-size-sm);
139
+ letter-spacing: 0.05em;
140
+ text-transform: uppercase;
141
+ border-bottom: 1px solid var(--table-border-color);
142
+ white-space: nowrap;
143
+
144
+ &.check-cell {
145
+ width: 48px;
146
+ text-align: center;
147
+ }
148
+
149
+ &.actions-cell {
150
+ text-align: right;
151
+ }
152
+ }
153
+ }
154
+
155
+ tbody {
156
+ tr {
157
+ transition: all var(--transition-fast);
158
+
159
+ td {
160
+ padding: var(--gap-md);
161
+ border-bottom: 1px solid var(--neutral-100);
162
+ color: var(--font-color-primary);
163
+ font-size: var(--font-size-md);
164
+
165
+ &.check-cell {
166
+ text-align: center;
167
+ }
168
+
169
+ &.actions-cell {
170
+ text-align: right;
171
+ }
172
+ }
173
+
174
+ &:last-child td {
175
+ border-bottom: none;
176
+ }
177
+
178
+ &:hover {
179
+ background-color: var(--table-row-hover);
180
+ }
181
+
182
+ &.selected {
183
+ background-color: var(--table-row-selected);
184
+
185
+ td {
186
+ border-bottom-color: #dbeafe;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ .action-buttons {
194
+ display: flex;
195
+ gap: var(--gap-sm);
196
+ justify-content: flex-end;
197
+ }
198
+
199
+ .footer {
200
+ padding: 4px 0;
201
+ }
202
+ }
203
+
204
+ .spinner {
205
+ width: 40px;
206
+ height: 40px;
207
+ border: 3px solid var(--neutral-200);
208
+ border-radius: 50%;
209
+ border-top-color: var(--primary-500);
210
+ animation: spin 1s ease-in-out infinite;
211
+ }
212
+
213
+ @keyframes spin {
214
+ to { transform: rotate(360deg); }
215
+ }
216
+ </style>
@@ -0,0 +1,126 @@
1
+ <template lang="pug">
2
+ .KMaskTextbox
3
+ KTextbox(
4
+ :modelValue="maskedValue"
5
+ :placeholder="placeholder"
6
+ :showClear="showClear"
7
+ :maxlength="mask.length"
8
+ @update:modelValue="handleUpdate"
9
+ @beforeinput="handleBeforeInput"
10
+ )
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import { computed } from 'vue'
15
+ import { getMaskPatternDef } from '@katlux/toolkit/components/KMaskTextbox/KMaskTextbox.logic'
16
+
17
+ const props = defineProps<{
18
+ maskedValue: string
19
+ placeholder: string
20
+ showClear: boolean
21
+ mask: string
22
+ }>()
23
+
24
+ const emit = defineEmits<{
25
+ (e: 'update:maskedValue', value: string): void
26
+ (e: 'clear'): void
27
+ }>()
28
+
29
+ // Use beforeinput for better validation (fires before input is accepted)
30
+ const handleBeforeInput = (event: InputEvent) => {
31
+ if (!event.data || event.inputType !== 'insertText') {
32
+ return // Allow deletions, etc.
33
+ }
34
+
35
+ const char = event.data
36
+ if (!/[0-9]/.test(char)) {
37
+ return // Not a digit, let through for other patterns
38
+ }
39
+
40
+ const target = event.target as HTMLInputElement
41
+ const cursorPos = target.selectionStart || 0
42
+ const currentValue = target.value
43
+
44
+ // Find the mask pattern at cursor position
45
+ let maskIdx = 0
46
+ let valIdx = 0
47
+
48
+ // Walk through to find where we are in the mask
49
+ while (valIdx < cursorPos && maskIdx < props.mask.length) {
50
+ const def = getMaskPatternDef(props.mask, maskIdx)
51
+ if (def) {
52
+ // Pattern - check how many chars it uses in value
53
+ let consumed = 0
54
+ while (consumed < def.length && valIdx < currentValue.length) {
55
+ if (def.pattern.test(currentValue[valIdx])) {
56
+ consumed++
57
+ }
58
+ valIdx++
59
+ if (valIdx >= cursorPos) break
60
+ }
61
+ maskIdx += def.length
62
+ } else {
63
+ // Literal
64
+ valIdx++
65
+ maskIdx++
66
+ }
67
+ }
68
+
69
+ // Get pattern at current position
70
+ const patternType = props.mask.substring(maskIdx, maskIdx + 4)
71
+ const digit = parseInt(char, 10)
72
+
73
+ // Find how many digits already entered for this specific pattern instance
74
+ let digitsInPattern = 0
75
+ let scanIdx = cursorPos
76
+
77
+ // Count backwards to start of this pattern
78
+ while (scanIdx > 0 && digitsInPattern < 10) {
79
+ scanIdx--
80
+ const ch = currentValue[scanIdx]
81
+ if (/[0-9]/.test(ch)) {
82
+ digitsInPattern++
83
+ } else {
84
+ break // Hit a literal, stop
85
+ }
86
+ }
87
+
88
+ // Validate based on pattern
89
+ if (patternType.startsWith('DD')) {
90
+ if (digitsInPattern === 0) {
91
+ if (digit > 3) event.preventDefault()
92
+ } else if (digitsInPattern === 1) {
93
+ const firstDigit = parseInt(currentValue[cursorPos - 1], 10)
94
+ if (firstDigit === 0 && digit === 0) event.preventDefault()
95
+ if (firstDigit === 3 && digit > 1) event.preventDefault()
96
+ }
97
+ } else if (patternType.startsWith('MM')) {
98
+ if (digitsInPattern === 0) {
99
+ if (digit > 1) event.preventDefault()
100
+ } else if (digitsInPattern === 1) {
101
+ const firstDigit = parseInt(currentValue[cursorPos - 1], 10)
102
+ if (firstDigit === 0 && digit === 0) event.preventDefault()
103
+ if (firstDigit === 1 && digit > 2) event.preventDefault()
104
+ }
105
+ } else if (patternType.startsWith('HH')) {
106
+ if (digitsInPattern === 0) {
107
+ if (digit > 2) event.preventDefault()
108
+ } else if (digitsInPattern === 1) {
109
+ const firstDigit = parseInt(currentValue[cursorPos - 1], 10)
110
+ if (firstDigit === 2 && digit > 3) event.preventDefault()
111
+ }
112
+ } else if (patternType.startsWith('mm') || patternType.startsWith('ss')) {
113
+ if (digitsInPattern === 0) {
114
+ if (digit > 5) event.preventDefault()
115
+ }
116
+ }
117
+ }
118
+
119
+ const handleUpdate = (value: string) => {
120
+ emit('update:maskedValue', value)
121
+ }
122
+ </script>
123
+
124
+ <style lang="scss" scoped>
125
+ // No additional styles needed - KTextbox handles everything
126
+ </style>
@@ -0,0 +1,96 @@
1
+ <template lang="pug">
2
+ .k-pagination-modern(v-if="totalPages > 1")
3
+ ul.pagination-list
4
+ li
5
+ KButton.nav-btn(
6
+ :onClick="goToPrev"
7
+ :isDisabled="!showPrevButton"
8
+ :buttonClasses="['light', 'small']"
9
+ aria-label="Previous page"
10
+ ) ‹
11
+
12
+ li(v-if="showFirstEllipsis")
13
+ KButton.page-btn(
14
+ :onClick="goToFirst"
15
+ :buttonClasses="['light', 'small']"
16
+ :isLink="!!getPageHref(1)"
17
+ :href="getPageHref(1)"
18
+ ) 1
19
+
20
+ li(v-if="showFirstEllipsis")
21
+ span.ellipsis ...
22
+
23
+ li(v-for="item in visiblePages" :key="item")
24
+ KButton.page-btn(
25
+ :onClick="() => pageClicked(item)"
26
+ :buttonClasses="[item === dp.currentPage.value ? 'primary' : 'light', 'small']"
27
+ :isLink="!!getPageHref(item)"
28
+ :href="getPageHref(item)"
29
+ ) {{ item }}
30
+
31
+ li(v-if="showLastEllipsis")
32
+ span.ellipsis ...
33
+
34
+ li(v-if="showLastEllipsis")
35
+ KButton.page-btn(
36
+ :onClick="goToLast"
37
+ :buttonClasses="['light', 'small']"
38
+ :isLink="!!getPageHref(totalPages)"
39
+ :href="getPageHref(totalPages)"
40
+ ) {{ totalPages }}
41
+
42
+ li
43
+ KButton.nav-btn(
44
+ :onClick="goToNext"
45
+ :isDisabled="!showNextButton"
46
+ :buttonClasses="['light', 'small']"
47
+ aria-label="Next page"
48
+ ) ›
49
+ </template>
50
+
51
+ <script setup lang="ts">
52
+ // Modern Pagination Template
53
+ defineProps<{
54
+ dp: any
55
+ totalPages: number
56
+ getPageHref: (page: number) => string | undefined
57
+ visiblePages: number[]
58
+ showPrevButton: boolean
59
+ showNextButton: boolean
60
+ showFirstEllipsis: boolean
61
+ showLastEllipsis: boolean
62
+ pageClicked: (item: number) => void
63
+ goToPrev: () => void
64
+ goToNext: () => void
65
+ goToFirst: () => void
66
+ goToLast: () => void
67
+ }>()
68
+ </script>
69
+
70
+ <style lang="scss" scoped>
71
+ .k-pagination-modern {
72
+ display: flex;
73
+ justify-content: flex-end;
74
+ font-family: var(--font-family);
75
+
76
+ .pagination-list {
77
+ display: flex;
78
+ gap: 4px;
79
+ list-style: none;
80
+ padding: 0;
81
+ margin: 0;
82
+ align-items: center;
83
+
84
+ li {
85
+ display: flex;
86
+ align-items: center;
87
+ }
88
+ }
89
+
90
+ .ellipsis {
91
+ padding: 0 4px;
92
+ color: var(--neutral-400);
93
+ font-size: var(--font-size-sm);
94
+ }
95
+ }
96
+ </style>