ketekny-ui-kit 1.0.79 → 1.0.81

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.79",
4
+ "version": "1.0.81",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -32,18 +32,23 @@ function createNodes() {
32
32
 
33
33
  const bubble = document.createElement('div')
34
34
  bubble.className = `
35
- px-3 py-2 rounded-lg text-sm font-medium leading-5
35
+ px-3 py-2 rounded-lg text-base font-medium leading-5
36
36
  text-slate-50 bg-slate-900 shadow-xl ring-1 ring-black/20
37
37
  opacity-0 scale-95 transition duration-150 ease-out
38
38
  `.replace(/\s+/g, ' ').trim()
39
+ bubble.style.color = '#f8fafc'
40
+ bubble.style.backgroundColor = '#0f172a'
41
+ bubble.style.borderColor = 'rgba(0, 0, 0, 0.2)'
39
42
  bubble.style.whiteSpace = 'normal' // allow wrap when needed
40
43
  bubble.style.maxWidth = 'min(36rem, calc(100vw - 1.5rem))' // clamp width
41
44
  bubble.style.wordBreak = 'break-word'
42
45
  bubble.style.letterSpacing = '0.01em'
43
46
  bubble.style.position = 'relative'
47
+ bubble.setAttribute('role', 'tooltip')
48
+ bubble.setAttribute('aria-hidden', 'true')
44
49
 
45
50
  const arrow = document.createElement('div')
46
- arrow.className = 'absolute w-2 h-2 rotate-45 bg-slate-900'
51
+ arrow.className = 'absolute w-2 h-2 rotate-45 bg-slate-900 opacity-0 transition-opacity duration-150 ease-out'
47
52
  arrow.setAttribute('aria-hidden', 'true')
48
53
 
49
54
  wrapper.appendChild(bubble)
@@ -184,25 +189,35 @@ export default {
184
189
  (content.mode === 'component' && !content.component)
185
190
  ) return
186
191
 
187
- const { wrapper, bubble, arrow } = createNodes()
188
- document.body.appendChild(wrapper)
189
-
190
- // Render content
191
- let app
192
- if (content.mode === 'text') bubble.textContent = content.text
193
- if (content.mode === 'html') bubble.innerHTML = content.html
194
- if (content.mode === 'component') {
195
- app = createApp({ render: () => h(content.component, content.props || {}) })
196
- app.mount(bubble)
197
- }
198
-
199
192
  const state = {
200
193
  placement: placementPref,
201
194
  pad: 8, // viewport padding
202
- gap: 8 // distance from trigger
195
+ gap: 8, // distance from trigger
196
+ visible: false,
197
+ nodes: null,
198
+ app: null,
199
+ }
200
+
201
+ function ensureNodes() {
202
+ if (state.nodes) return state.nodes
203
+
204
+ const { wrapper, bubble, arrow } = createNodes()
205
+ document.body.appendChild(wrapper)
206
+
207
+ if (content.mode === 'text') bubble.textContent = content.text
208
+ if (content.mode === 'html') bubble.innerHTML = content.html
209
+ if (content.mode === 'component') {
210
+ state.app = createApp({ render: () => h(content.component, content.props || {}) })
211
+ state.app.mount(bubble)
212
+ }
213
+
214
+ state.nodes = { wrapper, bubble, arrow }
215
+ return state.nodes
203
216
  }
204
217
 
205
218
  function positionAndShow() {
219
+ const nodes = ensureNodes()
220
+ const { wrapper, bubble, arrow } = nodes
206
221
  const elRect = el.getBoundingClientRect()
207
222
  const bSize = measureBubble(bubble)
208
223
  const pos = computePosition({ elRect, bSize, placement: state.placement, gap: state.gap, pad: state.pad })
@@ -215,18 +230,42 @@ export default {
215
230
  placeArrow({ arrow, bubbleRect: { width: bSize.width, height: bSize.height }, elRect, pos })
216
231
 
217
232
  // Show
233
+ bubble.setAttribute('aria-hidden', 'false')
218
234
  bubble.classList.remove('opacity-0', 'scale-95')
219
235
  bubble.classList.add('opacity-100', 'scale-100')
220
236
  arrow.style.display = 'block'
237
+ arrow.classList.remove('opacity-100')
238
+ arrow.classList.add('opacity-0')
239
+ window.setTimeout(() => {
240
+ if (!state.visible || !state.nodes) return
241
+ arrow.classList.remove('opacity-0')
242
+ arrow.classList.add('opacity-100')
243
+ }, 150)
221
244
 
222
245
  // Save current placement (may be flipped)
223
246
  state.placement = pos.placement
247
+ state.visible = true
224
248
  }
225
249
 
226
250
  function hide() {
251
+ if (!state.nodes) return
252
+
253
+ const { bubble, wrapper } = state.nodes
254
+ bubble.setAttribute('aria-hidden', 'true')
227
255
  bubble.classList.remove('opacity-100', 'scale-100')
228
256
  bubble.classList.add('opacity-0', 'scale-95')
229
- arrow.style.display = 'none'
257
+ state.visible = false
258
+ const { arrow } = state.nodes
259
+ arrow.classList.remove('opacity-100')
260
+ arrow.classList.add('opacity-0')
261
+
262
+ window.setTimeout(() => {
263
+ if (state.visible || !state.nodes) return
264
+ state.app?.unmount?.()
265
+ wrapper.remove()
266
+ state.nodes = null
267
+ state.app = null
268
+ }, 160)
230
269
  }
231
270
 
232
271
  // Events
@@ -244,7 +283,7 @@ export default {
244
283
  window.addEventListener('scroll', onScrollOrResize, true)
245
284
  window.addEventListener('resize', onScrollOrResize)
246
285
 
247
- el._tooltip = { wrapper, bubble, arrow, onEnter, onLeave, onScrollOrResize, app }
286
+ el._tooltip = { onEnter, onLeave, onScrollOrResize, state }
248
287
  },
249
288
 
250
289
  updated(el, binding) {
@@ -255,27 +294,30 @@ export default {
255
294
  if (nextPlacement !== undefined) {
256
295
  // Update preferred placement; flip may still occur on show
257
296
  // We store it on a small state on the element:
258
- t.prefPlacement = nextPlacement
297
+ t.state.placement = nextPlacement
259
298
  }
260
299
 
261
300
  const content = normalizeValue(binding)
262
- if (content.mode === 'text') t.bubble.textContent = content.text || ''
263
- else if (content.mode === 'html') t.bubble.innerHTML = content.html || ''
301
+ if (!t.state.nodes) return
302
+
303
+ const { bubble } = t.state.nodes
304
+ if (content.mode === 'text') bubble.textContent = content.text || ''
305
+ else if (content.mode === 'html') bubble.innerHTML = content.html || ''
264
306
  else if (content.mode === 'component') {
265
- if (t.app) t.app.unmount()
307
+ t.state.app?.unmount?.()
266
308
  const app = createApp({ render: () => h(content.component, content.props || {}) })
267
- app.mount(t.bubble)
268
- t.app = app
309
+ app.mount(bubble)
310
+ t.state.app = app
269
311
  }
270
312
  },
271
313
 
272
314
  unmounted(el) {
273
315
  const t = el._tooltip
274
316
  if (!t) return
275
- if (t.app) t.app.unmount()
276
- t.bubble.remove()
277
- t.arrow.remove()
278
- t.wrapper.remove()
317
+ if (t.state.app) t.state.app.unmount()
318
+ if (t.state.nodes) {
319
+ t.state.nodes.wrapper.remove()
320
+ }
279
321
  el.removeEventListener('mouseenter', t.onEnter)
280
322
  el.removeEventListener('mouseleave', t.onLeave)
281
323
  el.removeEventListener('focus', t.onEnter)
@@ -43,6 +43,7 @@
43
43
  :show-contact-info="showContactInfo"
44
44
  :account="account"
45
45
  :contact-email="contactEmail"
46
+ :help-url="helpUrl"
46
47
  :collapsed="isSidebarCollapsed"
47
48
  @logo-click="handleLogoClick"
48
49
  @signin="handleSignin"
@@ -95,6 +96,7 @@
95
96
  :show-contact-info="showContactInfo"
96
97
  :account="account"
97
98
  :contact-email="contactEmail"
99
+ :help-url="helpUrl"
98
100
  :collapsed="false"
99
101
  @logo-click="handleLogoClick"
100
102
  @item-click="closeMobileMenu"
@@ -155,6 +157,7 @@ export default {
155
157
  showContactInfo: { type: Boolean, default: false },
156
158
  account: { type: Object, default: null },
157
159
  contactEmail: { type: String, default: "grammateia@ketekny.gr" },
160
+ helpUrl: { type: String, default: "" },
158
161
  },
159
162
  data() {
160
163
  return {
@@ -23,10 +23,22 @@
23
23
  {{ title }}
24
24
  </div>
25
25
 
26
+ <div v-if="helpUrl && !collapsed" class="px-4 pb-2">
27
+ <a
28
+ :href="helpUrl"
29
+ target="_blank"
30
+ rel="noreferrer"
31
+ class="inline-flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-semibold text-white/85 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
32
+ >
33
+ <kIcon name="HelpCircle" size="16" color="#ffffff" />
34
+ <span>Χρειάζεστε βοήθεια;</span>
35
+ </a>
36
+ </div>
37
+
26
38
  <slot name="sidebar-header" />
27
39
  </div>
28
40
 
29
- <div class="flex-1 pt-4">
41
+ <div class="flex-1 pt-1">
30
42
  <div v-if="collapsed" class="flex flex-col items-center gap-2 mx-1 mt-3">
31
43
  <component
32
44
  :is="menuItemComponent(item)"
@@ -74,7 +86,7 @@
74
86
  <span
75
87
  v-if="item.description"
76
88
  class="block mt-1 text-sm"
77
- :class="item.active ? 'text-slate-700' : 'text-slate-300'"
89
+ :class="item.active ? 'text-slate-700' : 'text-slate-200'"
78
90
  >
79
91
  {{ item.description }}
80
92
  </span>
@@ -85,9 +97,22 @@
85
97
 
86
98
  <div
87
99
  v-if="account"
88
- class="px-1 pb-4 border-b shrink-0 border-white/10 bg-slate-900/80 text-white/80"
100
+ class="px-1 pb-4 border-b shrink-0 border-white/10 bg-slate-900/80 text-white/90"
89
101
  >
90
102
  <div v-if="collapsed" class="flex flex-col gap-3">
103
+ <div v-if="helpUrl" class="flex flex-col items-center gap-2">
104
+ <a
105
+ :href="helpUrl"
106
+ target="_blank"
107
+ rel="noreferrer"
108
+ aria-label="Χρειάζεστε βοήθεια;"
109
+ title="Χρειάζεστε βοήθεια;"
110
+ class="relative flex items-center justify-center text-white transition border group h-14 w-14 rounded-xl border-white/15 bg-white/5 hover:border-cyan-400/40 hover:bg-white/10"
111
+ v-tooltip.right="'Χρειάζεστε βοήθεια;'"
112
+ >
113
+ <kIcon name="HelpCircle" size="20" color="#ffffff" />
114
+ </a>
115
+ </div>
91
116
  <div v-if="account.loggedIn" class="flex flex-col items-center gap-2">
92
117
  <button
93
118
  type="button"
@@ -179,10 +204,10 @@
179
204
 
180
205
  <div
181
206
  v-if="!collapsed"
182
- class="border-t shrink-0 border-white/10 text-white/50"
207
+ class="border-t shrink-0 border-white/10 text-white/70"
183
208
  >
184
209
  <div v-if="showContactInfo" class="flex flex-col gap-2 px-4">
185
- <p class="text-sm font-semibold uppercase tracking-[0.25em] text-white/60">
210
+ <p class="text-sm font-semibold uppercase tracking-[0.25em] text-white/80">
186
211
  KETEKNY AE
187
212
  </p>
188
213
  <div class="flex flex-wrap items-center text-sm gap-x-3">
@@ -240,9 +265,10 @@ const LABELS = {
240
265
  signOut: "Αποσύνδεση",
241
266
  signIn: "Σύνδεση",
242
267
  signUp: "Εγγραφή",
243
- contactAddress: "Βερανζέου 13, 10677, Αθήνα",
268
+ contactAddress: "Βερανζέρου 13, 10677, Αθήνα",
244
269
  };
245
270
 
271
+
246
272
  export default {
247
273
  name: "TwoColSidebarPanel",
248
274
  components: {
@@ -271,6 +297,7 @@ export default {
271
297
  showContactInfo: { type: Boolean, default: false },
272
298
  account: { type: Object, default: null },
273
299
  contactEmail: { type: String, default: "grammateia@ketekny.gr" },
300
+ helpUrl: { type: String, default: "" },
274
301
  collapsed: { type: Boolean, default: false },
275
302
  },
276
303
  computed: {
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="overflow-hidden border rounded-lg shadow-sm component-card cp-card cp-border">
3
3
  <div class="flex items-center justify-between px-4 py-3 border-b cp-border cp-muted-50">
4
- <h3 class="text-lg font-semibold cp-text">{{ componentName }} Description</h3>
4
+ <h2 class="text-lg font-semibold cp-text">{{ componentName }} Description</h2>
5
5
 
6
6
  <div class="flex items-center gap-1">
7
7
  <button
@@ -34,7 +34,7 @@
34
34
  </td>
35
35
  <td class="px-4 py-3"><span class="font-mono text-sm text-blue-600">{{ prop.type }}</span></td>
36
36
  <td class="px-4 py-3">
37
- <code v-if="prop.default !== undefined" class="font-mono text-sm text-emerald-600">{{ prop.default }}</code>
37
+ <code v-if="prop.default !== undefined" class="font-mono text-sm text-slate-700">{{ prop.default }}</code>
38
38
  <span v-else class="cp-text-muted">-</span>
39
39
  </td>
40
40
  <td class="px-4 py-3 cp-text-muted">{{ prop.description }}</td>
@@ -65,7 +65,7 @@
65
65
  <code class="text-sm font-mono px-1.5 py-0.5 rounded cp-chip cp-chip-text">{{ slot.name }}</code>
66
66
  </td>
67
67
  <td class="px-4 py-3">
68
- <span :class="['inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium', slot.scoped ? 'bg-emerald-100 text-emerald-700' : 'cp-badge-muted']">
68
+ <span :class="['inline-flex items-center px-2 py-0.5 rounded-full text-sm font-medium', slot.scoped ? 'bg-emerald-100 text-emerald-700' : 'cp-badge-muted']">
69
69
  {{ slot.scoped ? "Yes" : "No" }}
70
70
  </span>
71
71
  </td>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="overflow-hidden border rounded-lg shadow-sm component-card cp-card cp-border">
3
3
  <div class="flex items-center justify-between px-4 py-3 border-b cp-border cp-muted-50">
4
- <h3 class="text-lg font-semibold cp-text">{{ componentName }} Preview</h3>
4
+ <h2 class="text-lg font-semibold cp-text">{{ componentName }} Preview</h2>
5
5
 
6
6
  <div class="flex items-center gap-1">
7
7
  <button
@@ -22,7 +22,7 @@
22
22
  v-for="vp in previewViewports"
23
23
  :key="vp.id"
24
24
  @click="activeViewport = vp.id"
25
- :class="['px-3 py-1.5 text-xs font-semibold rounded-md transition-colors', activeViewport === vp.id ? 'cp-tab-active' : 'cp-tab']">
25
+ :class="['px-3 py-1.5 text-sm font-semibold rounded-md transition-colors', activeViewport === vp.id ? 'cp-tab-active' : 'cp-tab']">
26
26
  {{ vp.label }}
27
27
  </button>
28
28
  </div>
@@ -1,6 +1,18 @@
1
1
  <template>
2
2
  <div class="relative w-full">
3
- <input type="text" v-model="localValue" @input="onInput" :class="[defaultStyle, disabled ? disabledStyle : '']" :placeholder="placeholder" :disabled="disabled" />
3
+ <label v-if="label" :for="inputId" class="mb-1 block text-sm font-medium text-gray-700 dark:text-slate-300">
4
+ {{ label }}
5
+ </label>
6
+ <input
7
+ :id="inputId"
8
+ type="text"
9
+ v-model="localValue"
10
+ @input="onInput"
11
+ :class="[defaultStyle, disabled ? disabledStyle : '']"
12
+ :placeholder="placeholder"
13
+ :aria-label="!label ? placeholder : null"
14
+ :disabled="disabled"
15
+ />
4
16
  <Search class="absolute w-4 h-4 text-primary/70 -translate-y-1/2 pointer-events-none right-3 top-1/2" />
5
17
  </div>
6
18
  </template>
@@ -16,10 +28,12 @@ export default {
16
28
  modelValue: { type: String, default: "" },
17
29
  debounce: { type: [String, Number], default: 300 },
18
30
  placeholder: { type: String, default: "Αναζήτηση..." },
31
+ label: { type: String, default: "" },
19
32
  disabled: { type: Boolean, default: false },
20
33
  },
21
34
  data() {
22
35
  return {
36
+ inputId: `ksearch-${Math.random().toString(36).slice(2, 10)}`,
23
37
  defaultStyle: "w-full px-3 py-2 border rounded-lg transition shadow-sm focus:outline-none text-gray-700 focus:ring-2 focus:ring-primary/20 focus:border-primary bg-white placeholder-gray-400 dark:bg-slate-800 dark:text-slate-100 dark:border-slate-600 dark:placeholder-slate-500",
24
38
  disabledStyle: "bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-slate-900 dark:text-slate-500",
25
39
 
package/src/ui/kTags.vue CHANGED
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div class="w-full">
3
- <div v-if="label != null" class="inputLabel" :class="hasError ? 'text-rose-800 dark:text-rose-400' : 'text-primary/90'">
3
+ <label v-if="label != null" :for="inputId" class="inputLabel" :class="hasError ? 'text-rose-800 dark:text-rose-400' : 'text-primary/90'">
4
4
  {{ label }}
5
- </div>
5
+ </label>
6
6
  <div
7
7
  :class="[
8
8
  'flex flex-wrap items-center gap-2 p-2 border rounded-xl shadow-sm min-h-[3rem] transition',
@@ -19,11 +19,13 @@
19
19
  </button>
20
20
  </span>
21
21
  <input
22
+ :id="inputId"
22
23
  v-model="newTag"
23
24
  @keydown.enter.prevent="addTag"
24
25
  @keydown.backspace="handleBackspace"
25
26
  @blur="addTag"
26
27
  :placeholder="placeholder"
28
+ :aria-label="!label ? placeholder || 'Add tag' : null"
27
29
  :disabled="disabled"
28
30
  :class="['flex-1 p-1 bg-transparent border-none focus:outline-none dark:text-slate-100 dark:placeholder-slate-500', disabled ? 'text-gray-400 cursor-not-allowed dark:text-slate-500' : '']"
29
31
  />
@@ -56,6 +58,7 @@ export default {
56
58
  data() {
57
59
  return {
58
60
  newTag: "",
61
+ inputId: `ktags-${Math.random().toString(36).slice(2, 10)}`,
59
62
  };
60
63
  },
61
64
  computed: {