@simsustech/quasar-components 0.12.3 → 0.12.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @simsustech/quasar-components
2
2
 
3
+ ## 0.12.5
4
+
5
+ ### Patch Changes
6
+
7
+ - d6f5de7: fix(components): fix NavigationRailFabs
8
+ - 0ec347b: fix(CronScheduleInput): remove negative-margin hack, use items-center
9
+
10
+ Same pattern as DateInput: the old margin-top: -1.7em hack broke with
11
+ unocss-preset-quasar updates. Replaced with:
12
+
13
+ - items-center for proper vertical alignment
14
+ - CSS overrides for padding leaks from q-field--labeled
15
+ - Neutralized cascaded padding-top on inner control-containers
16
+ - Removed horizontal padding on inner controls
17
+
18
+ - fix(DateInput): prevent DD/MM/YYYY fields from overlapping QField stack label
19
+
20
+ Nesting QInput inside the outer QField's #control slot created a
21
+ QField-inside-QField structure that caused padding leaks from the
22
+ preset's q-field--labeled descendant selectors and broke with the
23
+ unocss-preset-quasar update. Refactored to use plain `<input>`
24
+ elements instead of nested QInput, removing the old negative-margin
25
+ hack (-1.7em) that caused the overlap.
26
+
27
+ ## 0.12.4
28
+
29
+ ### Patch Changes
30
+
31
+ - a21d944: fix(components): move ready check to QLayout in Md3Layout
32
+
3
33
  ## 0.12.3
4
34
 
5
35
  ### Patch Changes
package/dist/form.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as loadLang, r as useLang, t as LocaleSelect_default } from "./LocaleSelect-BtwfPrmu.js";
2
2
  import { QBtn, QDate, QEditor, QField, QIcon, QInput, QItem, QItemLabel, QItemSection, QPopupProxy, QSelect, QTooltip, useQuasar } from "quasar";
3
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, mergeProps, normalizeProps, openBlock, ref, renderList, renderSlot, resolveDirective, resolveDynamicComponent, toDisplayString, toRefs, unref, useAttrs, useSlots, watch, withAsyncContext, withCtx, withDirectives } from "vue";
3
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, mergeProps, normalizeClass, normalizeProps, normalizeStyle, openBlock, ref, renderList, renderSlot, resolveDirective, toDisplayString, toRefs, unref, useAttrs, useSlots, watch, withAsyncContext, withCtx, withDirectives } from "vue";
4
4
  //#endregion
5
5
  //#region src/ui/form/GenderSelect.vue
6
6
  var GenderSelect_default = /* @__PURE__ */ defineComponent({
@@ -249,15 +249,28 @@ var TelephoneNumberInput_default = /* @__PURE__ */ defineComponent({
249
249
  });
250
250
  //#endregion
251
251
  //#region src/ui/form/DateInput.vue?vue&type=script&setup=true&lang.ts
252
- var _hoisted_1$2 = { class: "row" };
253
- var _hoisted_2$1 = {
252
+ var _hoisted_1$2 = {
253
+ class: "row items-center date-input-row no-wrap",
254
+ style: { "height": "100%" }
255
+ };
256
+ var _hoisted_2$1 = [
257
+ "value",
258
+ "placeholder",
259
+ "maxlength",
260
+ "onInput",
261
+ "onKeydown"
262
+ ];
263
+ var _hoisted_3 = {
254
264
  key: 0,
265
+ class: "q-field__marginal",
255
266
  style: {
256
- "margin-top": "1em",
257
- "width": "1ch"
267
+ "flex": "initial",
268
+ "width": "1ch",
269
+ "padding": "0",
270
+ "margin": "0"
258
271
  }
259
272
  };
260
- var _hoisted_3 = { class: "row items-center justify-end" };
273
+ var _hoisted_4 = { class: "row items-center justify-end" };
261
274
  //#endregion
262
275
  //#region src/ui/form/DateInput.vue
263
276
  var DateInput_default = /* @__PURE__ */ defineComponent({
@@ -281,46 +294,84 @@ var DateInput_default = /* @__PURE__ */ defineComponent({
281
294
  const emit = __emit;
282
295
  const lang = useLang();
283
296
  const { modelValue, format, locale } = toRefs(props);
284
- const year = ref();
285
- const month = ref();
286
- const day = ref();
287
- const setYear = (val) => {
288
- const nr = Number(val);
289
- if (nr && nr > 1e3 && nr < 1e4) year.value = nr;
290
- else year.value = void 0;
297
+ const year = ref("");
298
+ const month = ref("");
299
+ const day = ref("");
300
+ const parts = computed(() => {
301
+ const keys = format.value.split("-");
302
+ const map = {
303
+ YYYY: {
304
+ value: year.value,
305
+ placeholder: lang.value.datePicker.YYYY,
306
+ style: { "max-width": format.value === "YYYY-MM-DD" ? "8ch" : "7ch" },
307
+ inputClass: "text-center",
308
+ maxLength: 10
309
+ },
310
+ MM: {
311
+ value: month.value,
312
+ placeholder: lang.value.datePicker.MM,
313
+ style: { "max-width": "7ch" },
314
+ inputClass: "text-center",
315
+ maxLength: 10
316
+ },
317
+ DD: {
318
+ value: day.value,
319
+ placeholder: lang.value.datePicker.DD,
320
+ style: { "max-width": format.value === "DD-MM-YYYY" ? "7ch" : "4ch" },
321
+ inputClass: "text-center",
322
+ maxLength: 10
323
+ }
324
+ };
325
+ return keys.map((k) => ({
326
+ key: k,
327
+ ...map[k]
328
+ }));
329
+ });
330
+ const onInput = (e, key) => {
331
+ const input = e.target;
332
+ const raw = input.value;
333
+ let clamped;
334
+ if (key === "YYYY") clamped = clamp(raw, 4);
335
+ else if (key === "MM") clamped = clamp(raw, 2, 12);
336
+ else if (key === "DD") clamped = clamp(raw, 2, 31);
337
+ else clamped = raw;
338
+ if (input.value !== clamped) input.value = clamped;
339
+ if (key === "YYYY") year.value = clamped;
340
+ else if (key === "MM") month.value = clamped;
341
+ else if (key === "DD") day.value = clamped;
291
342
  };
292
- const setMonth = (val) => {
293
- const nr = Number(val);
294
- if (nr && nr > 0 && nr < 13) month.value = nr;
295
- else month.value = void 0;
343
+ const onKeydown = (e, _index) => {
344
+ if (["Minus", "Slash"].includes(e.code)) {
345
+ e.preventDefault();
346
+ const all = e.currentTarget.closest(".date-input-row")?.querySelectorAll(".q-field__native");
347
+ const currentIdx = Array.from(all || []).indexOf(e.currentTarget);
348
+ (all?.[currentIdx + 1])?.focus();
349
+ }
296
350
  };
297
- const setDay = (val) => {
298
- const nr = Number(val);
299
- if (nr && nr > 0 && nr < 32) day.value = nr;
300
- else day.value = void 0;
351
+ const clamp = (val, maxLen, maxVal) => {
352
+ const digits = val.replace(/\D/g, "").slice(0, maxLen);
353
+ if (maxVal !== void 0 && digits.length === maxLen) {
354
+ if (parseInt(digits, 10) > maxVal) return String(maxVal);
355
+ }
356
+ return digits;
301
357
  };
302
- const setInternalDate = (dateString, separator = "-") => {
303
- if (dateString) {
304
- const [yearPart, monthPart, dayPart] = dateString.split(separator);
305
- if (yearPart && monthPart && dayPart) {
306
- year.value = Number(yearPart);
307
- month.value = Number(monthPart);
308
- day.value = Number(dayPart);
358
+ watch(year, () => emitDate());
359
+ watch(month, () => emitDate());
360
+ watch(day, () => emitDate());
361
+ function emitDate() {
362
+ const y = year.value;
363
+ const m = month.value;
364
+ const d = day.value;
365
+ if (y.length === 4 && m.length === 2 && d.length === 2) {
366
+ const date = `${y}-${m}-${d}`;
367
+ if (!isNaN(Date.parse(date))) {
368
+ emit("update:modelValue", date);
369
+ return;
309
370
  }
310
371
  }
311
- };
312
- const setDate = (value) => {
313
- setInternalDate(value, "/");
314
- };
315
- watch([
316
- year,
317
- month,
318
- day
319
- ], () => {
320
- const date = `${year.value}-${String(month.value).padStart(2, "0")}-${String(day.value).padStart(2, "0")}`;
321
- if (year.value && month.value && day.value && !isNaN(Date.parse(date))) emit("update:modelValue", date);
322
- else if (modelValue.value !== null) emit("update:modelValue", null);
323
- });
372
+ const partial = `${y.padEnd(4, "_")}-${m.padEnd(2, "_")}-${d.padEnd(2, "_")}`;
373
+ if (partial !== modelValue.value) emit("update:modelValue", partial);
374
+ }
324
375
  const formattedDate = computed(() => {
325
376
  if (modelValue.value) return new Date(Date.parse(modelValue.value)).toLocaleDateString(locale.value, {
326
377
  weekday: "long",
@@ -330,69 +381,28 @@ var DateInput_default = /* @__PURE__ */ defineComponent({
330
381
  });
331
382
  return "";
332
383
  });
384
+ function setInternalDate(dateString, separator = "-") {
385
+ if (dateString) {
386
+ const [yearPart, monthPart, dayPart] = dateString.split(separator);
387
+ if (yearPart && monthPart && dayPart) {
388
+ year.value = String(yearPart);
389
+ month.value = String(monthPart).padStart(2, "0");
390
+ day.value = String(dayPart).padStart(2, "0");
391
+ }
392
+ }
393
+ }
394
+ const setDate = (value) => {
395
+ setInternalDate(value, "/");
396
+ };
333
397
  watch(modelValue, (newVal) => {
334
- if (newVal) setInternalDate(newVal);
398
+ if (newVal && !newVal.includes("_")) setInternalDate(newVal);
335
399
  else if (newVal === null) {
336
- year.value = void 0;
337
- month.value = void 0;
338
- day.value = void 0;
400
+ year.value = "";
401
+ month.value = "";
402
+ day.value = "";
339
403
  }
340
404
  });
341
405
  setInternalDate(modelValue.value);
342
- const goToNextElement = (e) => {
343
- if (["Minus", "Slash"].includes(e.code)) {
344
- e.preventDefault();
345
- const next = e.currentTarget.parentElement?.parentElement?.parentElement?.parentElement?.nextElementSibling;
346
- if (next) next.focus();
347
- }
348
- };
349
- const dateProps = computed(() => ({
350
- YYYY: {
351
- modelValue: year.value,
352
- placeholder: lang.value.datePicker.YYYY,
353
- style: {
354
- "max-width": format.value === "YYYY-MM-DD" ? "8ch" : "7ch",
355
- "margin-top": "-1.7em",
356
- "margin-bottom": "-0.5em",
357
- background: "transparent",
358
- border: 0
359
- },
360
- class: format.value !== "YYYY-MM-DD" ? "q-mb-none q-ml-none" : void 0,
361
- inputClass: "text-center",
362
- "onUpdate:modelValue": setYear,
363
- onKeydown: goToNextElement
364
- },
365
- MM: {
366
- modelValue: month.value ? String(month.value).padStart(2, "0") : "",
367
- placeholder: lang.value.datePicker.MM,
368
- style: {
369
- "max-width": "7ch",
370
- "margin-top": "-1.7em",
371
- "margin-bottom": "-0.5em",
372
- background: "transparent",
373
- border: 0
374
- },
375
- class: "q-ml-none",
376
- inputClass: "text-center",
377
- "onUpdate:modelValue": setMonth,
378
- onKeydown: goToNextElement
379
- },
380
- DD: {
381
- modelValue: day.value ? String(day.value).padStart(2, "0") : "",
382
- placeholder: lang.value.datePicker.DD,
383
- style: {
384
- "max-width": format.value === "DD-MM-YYYY" ? "7ch" : "4ch",
385
- "margin-top": "-1.7em",
386
- "margin-bottom": "-0.5em",
387
- background: "transparent",
388
- border: 0
389
- },
390
- class: format.value === "YYYY-MM-DD" ? "q-ml-none" : void 0,
391
- inputClass: "text-center",
392
- "onUpdate:modelValue": setDay,
393
- onKeydown: goToNextElement
394
- }
395
- }));
396
406
  const validations = ref([(v) => {
397
407
  if (v !== null) return /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/.test(v);
398
408
  return true;
@@ -411,24 +421,24 @@ var DateInput_default = /* @__PURE__ */ defineComponent({
411
421
  rules: validations.value,
412
422
  label: `${__props.label}${__props.required ? "*" : ""}`,
413
423
  "stack-label": "",
414
- "lazy-rules": ""
424
+ "lazy-rules": "",
425
+ class: "date-input-field"
415
426
  }, createSlots({
416
- control: withCtx(() => [createElementVNode("div", _hoisted_1$2, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(format).split("-"), (part, index) => {
417
- return openBlock(), createBlock(resolveDynamicComponent(unref(QInput)), mergeProps({
418
- key: part,
419
- class: "col-auto",
420
- borderless: "",
421
- filled: false,
422
- outlined: false,
423
- standout: false,
424
- rounded: false
425
- }, { ref_for: true }, dateProps.value[part], {
427
+ control: withCtx(() => [createElementVNode("div", _hoisted_1$2, [(openBlock(true), createElementBlock(Fragment, null, renderList(parts.value, (part, index) => {
428
+ return openBlock(), createElementBlock("div", {
429
+ key: part.key,
430
+ class: "row no-wrap items-center",
431
+ style: { "gap": "0" }
432
+ }, [createElementVNode("input", {
433
+ value: part.value,
434
+ placeholder: part.placeholder,
435
+ class: normalizeClass(["q-field__native text-center", part.inputClass]),
436
+ style: normalizeStyle(part.style),
426
437
  inputmode: "numeric",
427
- dense: ""
428
- }), {
429
- after: withCtx(() => [index < 2 ? (openBlock(), createElementBlock("a", _hoisted_2$1, "-")) : createCommentVNode("", true)]),
430
- _: 2
431
- }, 1040);
438
+ maxlength: part.maxLength,
439
+ onInput: ($event) => onInput($event, part.key),
440
+ onKeydown: ($event) => onKeydown($event, index)
441
+ }, null, 46, _hoisted_2$1), index < parts.value.length - 1 ? (openBlock(), createElementBlock("span", _hoisted_3, "-")) : createCommentVNode("", true)]);
432
442
  }), 128))])]),
433
443
  append: withCtx(() => [__props.clearable ? (openBlock(), createBlock(_component_q_icon, {
434
444
  key: 0,
@@ -451,7 +461,7 @@ var DateInput_default = /* @__PURE__ */ defineComponent({
451
461
  "model-value": unref(modelValue)?.replaceAll("-", "/"),
452
462
  "onUpdate:modelValue": setDate
453
463
  }), {
454
- default: withCtx(() => [createElementVNode("div", _hoisted_3, [withDirectives(createVNode(_component_q_btn, {
464
+ default: withCtx(() => [createElementVNode("div", _hoisted_4, [withDirectives(createVNode(_component_q_btn, {
455
465
  label: unref(lang).buttons.close,
456
466
  color: "primary",
457
467
  flat: ""
@@ -805,10 +815,10 @@ var FilteredModelSelect_default = /* @__PURE__ */ defineComponent({
805
815
  });
806
816
  //#endregion
807
817
  //#region src/ui/form/CronScheduleInput.vue?vue&type=script&setup=true&lang.ts
808
- var _hoisted_1$1 = { class: "row" };
818
+ var _hoisted_1$1 = { class: "row cron-input-row items-center" };
809
819
  var _hoisted_2 = {
810
820
  key: 0,
811
- class: "row"
821
+ class: "row cron-input-row items-center"
812
822
  };
813
823
  //#endregion
814
824
  //#region src/ui/form/CronScheduleInput.vue
@@ -869,13 +879,10 @@ var CronScheduleInput_default = /* @__PURE__ */ defineComponent({
869
879
  }
870
880
  }
871
881
  const smallStyle = {
872
- "margin-top": "-1.7em",
873
- "margin-bottom": "-0.5em",
874
882
  background: "transparent",
875
883
  border: 0
876
884
  };
877
885
  const dayOfWeekStyle = {
878
- "margin-bottom": "-0.5em",
879
886
  background: "transparent",
880
887
  border: 0
881
888
  };
@@ -892,7 +899,8 @@ var CronScheduleInput_default = /* @__PURE__ */ defineComponent({
892
899
  rules: validations.value,
893
900
  label: `${unref(label)}${__props.required ? "*" : ""}`,
894
901
  "stack-label": "",
895
- borderless: ""
902
+ borderless: "",
903
+ class: "cron-input-field"
896
904
  }, {
897
905
  control: withCtx(() => [createElementVNode("div", _hoisted_1$1, [
898
906
  __props.showHour ? (openBlock(), createBlock(_component_q_select, {
package/dist/md3.js CHANGED
@@ -32,7 +32,11 @@ var NavigationRailFabs_default = /* @__PURE__ */ defineComponent({
32
32
  "gt-sm": true,
33
33
  animated: !!__props.seekAttention,
34
34
  "animated-tada": !!__props.seekAttention,
35
- "animated-infinite": !!__props.seekAttention
35
+ "animated-infinite": !!__props.seekAttention,
36
+ "!bg-$light-primary-container": true,
37
+ "dark:!bg-$dark-primary-container": true,
38
+ "text-$on-light-primary-container": true,
39
+ "dark:text-$on-dark-primary-container": true
36
40
  }),
37
41
  fab: "",
38
42
  icon: unref(addIcon),
@@ -49,7 +53,11 @@ var NavigationRailFabs_default = /* @__PURE__ */ defineComponent({
49
53
  "gt-sm": true,
50
54
  animated: !!__props.seekAttention,
51
55
  "animated-tada": !!__props.seekAttention,
52
- "animated-infinite": !!__props.seekAttention
56
+ "animated-infinite": !!__props.seekAttention,
57
+ "!bg-$light-primary-container": true,
58
+ "dark:!bg-$dark-primary-container": true,
59
+ "text-$on-light-primary-container": true,
60
+ "dark:text-$on-dark-primary-container": true
53
61
  }),
54
62
  fab: "",
55
63
  icon: unref(editIcon),
@@ -264,8 +272,7 @@ function onClickOutside(target, handler, options = {}) {
264
272
  Number.POSITIVE_INFINITY;
265
273
  //#endregion
266
274
  //#region src/ui/md3/Md3Layout.vue?vue&type=script&setup=true&lang.ts
267
- var _hoisted_1 = { key: 0 };
268
- var _hoisted_2 = {
275
+ var _hoisted_1 = {
269
276
  id: "fabs",
270
277
  class: "q-mb-md min-h-56px"
271
278
  };
@@ -325,8 +332,11 @@ var Md3Layout_default = /* @__PURE__ */ defineComponent({
325
332
  const _component_router_view = resolveComponent("router-view");
326
333
  const _component_q_page_container = QPageContainer;
327
334
  const _component_q_layout = QLayout;
328
- return openBlock(), createBlock(_component_q_layout, { view: "lHh Lpr lFf" }, {
329
- default: withCtx(() => [__props.ready ? (openBlock(), createElementBlock("div", _hoisted_1, [
335
+ return __props.ready ? (openBlock(), createBlock(_component_q_layout, {
336
+ key: 0,
337
+ view: "lHh Lpr lFf"
338
+ }, {
339
+ default: withCtx(() => [
330
340
  createVNode(_component_q_header, null, {
331
341
  default: withCtx(() => [createVNode(_component_q_toolbar, null, {
332
342
  default: withCtx(() => [!miniState.value ? (openBlock(), createBlock(_component_q_btn, {
@@ -369,7 +379,7 @@ var Md3Layout_default = /* @__PURE__ */ defineComponent({
369
379
  class: "q-mb-md",
370
380
  onClick: _cache[1] || (_cache[1] = ($event) => toggleLeftDrawer())
371
381
  }),
372
- createElementVNode("div", _hoisted_2, [renderSlot(_ctx.$slots, "fabs", { showSticky: false })]),
382
+ createElementVNode("div", _hoisted_1, [renderSlot(_ctx.$slots, "fabs", { showSticky: false })]),
373
383
  renderSlot(_ctx.$slots, "drawer-mini-navigation")
374
384
  ], 2)]),
375
385
  default: withCtx(() => [renderSlot(_ctx.$slots, "drawer")]),
@@ -387,9 +397,9 @@ var Md3Layout_default = /* @__PURE__ */ defineComponent({
387
397
  default: withCtx(() => [createVNode(_component_router_view), renderSlot(_ctx.$slots, "fabs", { showSticky: true })]),
388
398
  _: 3
389
399
  })
390
- ])) : createCommentVNode("", true)]),
400
+ ]),
391
401
  _: 3
392
- });
402
+ })) : createCommentVNode("", true);
393
403
  };
394
404
  }
395
405
  });
@@ -2,6 +2,44 @@
2
2
  .break-all {
3
3
  word-break: break-all;
4
4
  }
5
+
6
+ /* The outer QField gets q-field--labeled which leaks padding into
7
+ descendant .q-field__native elements. The auto-height override
8
+ fixes padding-top but not padding-bottom. */
9
+ .date-input-field.q-field--auto-height.q-field--labeled .q-field__native {
10
+ padding-bottom: 0 !important;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ /* Remove the standard underline (creates whitespace beneath) */
15
+ .date-input-field.q-field--standard
16
+ > .q-field__inner
17
+ > .q-field__control::before {
18
+ border-bottom: none !important;
19
+ }
20
+ .date-input-field.q-field--standard
21
+ > .q-field__inner
22
+ > .q-field__control::after {
23
+ display: none !important;
24
+ }
25
+
26
+ /* Stop padding leak from outer q-field--labeled into inner QSelects */
27
+ .cron-input-field.q-field--labeled .cron-input-row .q-field__native,
28
+ .cron-input-field.q-field--labeled .cron-input-row .q-field__input {
29
+ padding-bottom: 0 !important;
30
+ }
31
+
32
+ /* Inner control-container — neutralize cascaded padding-top */
33
+ .cron-input-field.q-field--auto-height.q-field--labeled
34
+ .cron-input-row
35
+ .q-field__control-container {
36
+ padding-top: 0 !important;
37
+ }
38
+
39
+ /* Inner controls — remove horizontal padding */
40
+ .cron-input-field .cron-input-row .q-field__control {
41
+ padding-inline: 0 !important;
42
+ }
5
43
  /* https://github.com/quasarframework/quasar/blob/dev/docs/src/layouts/doc-layout/DocPageMenu.sass */
6
44
  .q-drawer-list .q-item__section--avatar {
7
45
  color: var(--q-primary);
@@ -12,9 +12,9 @@ export interface Props {
12
12
  clear: string;
13
13
  };
14
14
  }
15
- declare var __VLS_57: string, __VLS_58: any;
15
+ declare var __VLS_50: string, __VLS_51: any;
16
16
  type __VLS_Slots = {} & {
17
- [K in NonNullable<typeof __VLS_57>]?: (props: typeof __VLS_58) => any;
17
+ [K in NonNullable<typeof __VLS_50>]?: (props: typeof __VLS_51) => any;
18
18
  };
19
19
  declare const __VLS_base: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
20
20
  "update:modelValue": (val: string | null) => any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simsustech/quasar-components",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "description": "High level components for Quasar Framework",
5
5
  "bugs": "https://github.com/simsusech/quasar-components/issues",
6
6
  "license": "MIT",
@@ -5,9 +5,10 @@
5
5
  :label="`${label}${required ? '*' : ''}`"
6
6
  stack-label
7
7
  borderless
8
+ class="cron-input-field"
8
9
  >
9
10
  <template #control>
10
- <div class="row">
11
+ <div class="row cron-input-row items-center">
11
12
  <q-select
12
13
  v-if="showHour"
13
14
  v-model="hour"
@@ -102,7 +103,7 @@
102
103
  </template>
103
104
  </q-select>
104
105
  </div>
105
- <div v-if="showDayOfWeek" class="row">
106
+ <div v-if="showDayOfWeek" class="row cron-input-row items-center">
106
107
  <q-select
107
108
  v-if="showDayOfWeek"
108
109
  v-model="dayOfWeek"
@@ -214,14 +215,11 @@ function setInternalCron(cronString: string) {
214
215
  }
215
216
 
216
217
  const smallStyle = {
217
- 'margin-top': '-1.7em',
218
- 'margin-bottom': '-0.5em',
219
218
  background: 'transparent',
220
219
  border: 0
221
220
  }
222
221
 
223
222
  const dayOfWeekStyle = {
224
- 'margin-bottom': '-0.5em',
225
223
  background: 'transparent',
226
224
  border: 0
227
225
  }
@@ -234,3 +232,23 @@ watch(modelValue, (newVal) => {
234
232
  })
235
233
  setInternalCron(modelValue.value)
236
234
  </script>
235
+
236
+ <style>
237
+ /* Stop padding leak from outer q-field--labeled into inner QSelects */
238
+ .cron-input-field.q-field--labeled .cron-input-row .q-field__native,
239
+ .cron-input-field.q-field--labeled .cron-input-row .q-field__input {
240
+ padding-bottom: 0 !important;
241
+ }
242
+
243
+ /* Inner control-container — neutralize cascaded padding-top */
244
+ .cron-input-field.q-field--auto-height.q-field--labeled
245
+ .cron-input-row
246
+ .q-field__control-container {
247
+ padding-top: 0 !important;
248
+ }
249
+
250
+ /* Inner controls — remove horizontal padding */
251
+ .cron-input-field .cron-input-row .q-field__control {
252
+ padding-inline: 0 !important;
253
+ }
254
+ </style>
@@ -6,27 +6,34 @@
6
6
  :label="`${label}${required ? '*' : ''}`"
7
7
  stack-label
8
8
  lazy-rules
9
+ class="date-input-field"
9
10
  >
10
11
  <template #control>
11
- <div class="row">
12
- <component
13
- :is="QInput"
14
- v-for="(part, index) in format.split('-')"
15
- :key="part"
16
- class="col-auto"
17
- borderless
18
- :filled="false"
19
- :outlined="false"
20
- :standout="false"
21
- :rounded="false"
22
- v-bind="dateProps[part]"
23
- inputmode="numeric"
24
- dense
12
+ <div class="row items-center date-input-row no-wrap" style="height: 100%">
13
+ <div
14
+ v-for="(part, index) in parts"
15
+ :key="part.key"
16
+ class="row no-wrap items-center"
17
+ style="gap: 0"
25
18
  >
26
- <template #after>
27
- <a v-if="index < 2" style="margin-top: 1em; width: 1ch">-</a>
28
- </template>
29
- </component>
19
+ <input
20
+ :value="part.value"
21
+ :placeholder="part.placeholder"
22
+ class="q-field__native text-center"
23
+ :class="part.inputClass"
24
+ :style="part.style"
25
+ inputmode="numeric"
26
+ :maxlength="part.maxLength"
27
+ @input="onInput($event, part.key)"
28
+ @keydown="onKeydown($event, index)"
29
+ />
30
+ <span
31
+ v-if="index < parts.length - 1"
32
+ class="q-field__marginal"
33
+ style="flex: initial; width: 1ch; padding: 0; margin: 0"
34
+ >-</span
35
+ >
36
+ </div>
30
37
  </div>
31
38
  </template>
32
39
 
@@ -68,13 +75,7 @@
68
75
 
69
76
  <script setup lang="ts">
70
77
  import { ref, watch, toRefs, computed } from 'vue'
71
- import {
72
- QDate,
73
- QDateProps,
74
- QInput,
75
- QInputProps,
76
- QuasarLanguageCodes
77
- } from 'quasar'
78
+ import { QDate, QDateProps, QuasarLanguageCodes } from 'quasar'
78
79
  import { useLang } from './lang'
79
80
 
80
81
  export interface Props {
@@ -103,68 +104,112 @@ const props = withDefaults(defineProps<Props>(), {
103
104
  const emit = defineEmits<{
104
105
  (e: 'update:modelValue', val: string | null): void
105
106
  }>()
106
- // const attrs = useAttrs()
107
107
 
108
108
  const lang = useLang()
109
109
 
110
110
  const { modelValue, format, locale } = toRefs(props)
111
111
 
112
- const year = ref<number>()
113
- const month = ref<number>()
114
- const day = ref<number>()
112
+ const year = ref('')
113
+ const month = ref('')
114
+ const day = ref('')
115
115
 
116
- const setYear: InstanceType<typeof QInput>['$props']['onUpdate:modelValue'] = (
117
- val
118
- ) => {
119
- const nr = Number(val)
120
- if (nr && nr > 1e3 && nr < 1e4) year.value = nr
121
- else year.value = undefined
116
+ interface PartInfo {
117
+ value: string
118
+ placeholder: string
119
+ style: Record<string, string>
120
+ inputClass: string
121
+ maxLength: number
122
122
  }
123
123
 
124
- const setMonth: InstanceType<typeof QInput>['$props']['onUpdate:modelValue'] = (
125
- val
126
- ) => {
127
- const nr = Number(val)
128
- if (nr && nr > 0 && nr < 13) month.value = nr
129
- else month.value = undefined
130
- }
124
+ const parts = computed(() => {
125
+ const keys = format.value.split('-')
126
+ const map: Record<string, PartInfo> = {
127
+ YYYY: {
128
+ value: year.value,
129
+ placeholder: lang.value.datePicker.YYYY,
130
+ style: { 'max-width': format.value === 'YYYY-MM-DD' ? '8ch' : '7ch' },
131
+ inputClass: 'text-center',
132
+ maxLength: 10
133
+ },
134
+ MM: {
135
+ value: month.value,
136
+ placeholder: lang.value.datePicker.MM,
137
+ style: { 'max-width': '7ch' },
138
+ inputClass: 'text-center',
139
+ maxLength: 10
140
+ },
141
+ DD: {
142
+ value: day.value,
143
+ placeholder: lang.value.datePicker.DD,
144
+ style: { 'max-width': format.value === 'DD-MM-YYYY' ? '7ch' : '4ch' },
145
+ inputClass: 'text-center',
146
+ maxLength: 10
147
+ }
148
+ }
149
+ return keys.map((k) => ({ key: k, ...map[k] }))
150
+ })
131
151
 
132
- const setDay: InstanceType<typeof QInput>['$props']['onUpdate:modelValue'] = (
133
- val
134
- ) => {
135
- const nr = Number(val)
136
- if (nr && nr > 0 && nr < 32) day.value = nr
137
- else day.value = undefined
152
+ const onInput = (e: Event, key: string) => {
153
+ const input = e.target as HTMLInputElement
154
+ const raw = input.value
155
+ let clamped: string
156
+ if (key === 'YYYY') clamped = clamp(raw, 4)
157
+ else if (key === 'MM') clamped = clamp(raw, 2, 12)
158
+ else if (key === 'DD') clamped = clamp(raw, 2, 31)
159
+ else clamped = raw
160
+ // Directly set DOM value so it's visible immediately — Vue's :value
161
+ // binding is async and `pressSequentially` types faster than it resolves.
162
+ // Without this, the browser shows the raw non-digit-mixed value between renders.
163
+ if (input.value !== clamped) input.value = clamped
164
+ if (key === 'YYYY') year.value = clamped
165
+ else if (key === 'MM') month.value = clamped
166
+ else if (key === 'DD') day.value = clamped
138
167
  }
139
168
 
140
- const setInternalDate = (
141
- dateString?: string | null,
142
- separator: '-' | '/' = '-'
143
- ) => {
144
- if (dateString) {
145
- const [yearPart, monthPart, dayPart] = dateString.split(separator)
146
- if (yearPart && monthPart && dayPart) {
147
- year.value = Number(yearPart)
148
- month.value = Number(monthPart)
149
- day.value = Number(dayPart)
150
- }
169
+ const onKeydown = (e: KeyboardEvent, _index: number) => {
170
+ if (['Minus', 'Slash'].includes(e.code)) {
171
+ e.preventDefault()
172
+ const row = (e.currentTarget as HTMLElement).closest('.date-input-row')
173
+ const all = row?.querySelectorAll('.q-field__native')
174
+ const currentIdx = Array.from(all || []).indexOf(
175
+ e.currentTarget as HTMLElement
176
+ )
177
+ const next = all?.[currentIdx + 1] as HTMLElement | undefined
178
+ next?.focus()
151
179
  }
152
180
  }
153
181
 
154
- const setDate: InstanceType<typeof QDate>['$props']['onUpdate:modelValue'] = (
155
- value
156
- ) => {
157
- setInternalDate(value, '/')
182
+ const clamp = (val: string, maxLen: number, maxVal?: number): string => {
183
+ const digits = val.replace(/\D/g, '').slice(0, maxLen)
184
+ if (maxVal !== undefined && digits.length === maxLen) {
185
+ const n = parseInt(digits, 10)
186
+ if (n > maxVal) return String(maxVal)
187
+ }
188
+ return digits
158
189
  }
159
190
 
160
- watch([year, month, day], () => {
161
- const date = `${year.value}-${String(month.value).padStart(2, '0')}-${String(day.value).padStart(2, '0')}`
162
- if (year.value && month.value && day.value && !isNaN(Date.parse(date))) {
163
- emit('update:modelValue', date)
164
- } else if (modelValue.value !== null) {
165
- emit('update:modelValue', null)
191
+ watch(year, () => emitDate())
192
+ watch(month, () => emitDate())
193
+ watch(day, () => emitDate())
194
+
195
+ function emitDate() {
196
+ const y = year.value
197
+ const m = month.value
198
+ const d = day.value
199
+ if (y.length === 4 && m.length === 2 && d.length === 2) {
200
+ const date = `${y}-${m}-${d}`
201
+ if (!isNaN(Date.parse(date))) {
202
+ emit('update:modelValue', date)
203
+ return
204
+ }
166
205
  }
167
- })
206
+ // Emit partial date so validation rules still catch invalid input,
207
+ // but watch(modelValue) doesn't clear the refs (it only clears on null).
208
+ const partial = `${y.padEnd(4, '_')}-${m.padEnd(2, '_')}-${d.padEnd(2, '_')}`
209
+ if (partial !== modelValue.value) {
210
+ emit('update:modelValue', partial)
211
+ }
212
+ }
168
213
 
169
214
  const formattedDate = computed(() => {
170
215
  if (modelValue.value)
@@ -180,82 +225,42 @@ const formattedDate = computed(() => {
180
225
  return ''
181
226
  })
182
227
 
183
- watch(modelValue, (newVal) => {
184
- if (newVal) setInternalDate(newVal)
185
- else if (newVal === null) {
186
- year.value = undefined
187
- month.value = undefined
188
- day.value = undefined
189
- }
190
- })
191
- setInternalDate(modelValue.value)
192
-
193
- const goToNextElement = (e: KeyboardEvent) => {
194
- if (['Minus', 'Slash'].includes(e.code)) {
195
- e.preventDefault()
196
- const next = (e.currentTarget as HTMLElement).parentElement?.parentElement
197
- ?.parentElement?.parentElement?.nextElementSibling
198
- if (next) {
199
- ;(next as HTMLElement).focus()
228
+ function setInternalDate(
229
+ dateString?: string | null,
230
+ separator: '-' | '/' = '-'
231
+ ) {
232
+ if (dateString) {
233
+ const [yearPart, monthPart, dayPart] = dateString.split(separator)
234
+ if (yearPart && monthPart && dayPart) {
235
+ year.value = String(yearPart)
236
+ month.value = String(monthPart).padStart(2, '0')
237
+ day.value = String(dayPart).padStart(2, '0')
200
238
  }
201
239
  }
202
240
  }
203
241
 
204
- const dateProps = computed<Record<string, QInputProps>>(() => ({
205
- YYYY: {
206
- modelValue: year.value,
207
- placeholder: lang.value.datePicker.YYYY,
208
- style: {
209
- 'max-width': format.value === 'YYYY-MM-DD' ? '8ch' : '7ch',
210
- 'margin-top': '-1.7em',
211
- 'margin-bottom': '-0.5em',
212
- background: 'transparent',
213
- border: 0
214
- },
215
- // suffix: format.value === 'YYYY-MM-DD' ? '-' : undefined,
216
- class: format.value !== 'YYYY-MM-DD' ? 'q-mb-none q-ml-none' : undefined,
217
- inputClass: 'text-center',
218
- 'onUpdate:modelValue': setYear,
219
- onKeydown: goToNextElement
220
- },
221
- MM: {
222
- modelValue: month.value ? String(month.value).padStart(2, '0') : '',
223
- placeholder: lang.value.datePicker.MM,
224
- style: {
225
- 'max-width': '7ch',
226
- 'margin-top': '-1.7em',
227
- 'margin-bottom': '-0.5em',
228
- background: 'transparent',
229
- border: 0
230
- },
231
- // suffix: '-',
232
- class: 'q-ml-none',
233
- inputClass: 'text-center',
234
- 'onUpdate:modelValue': setMonth,
235
- onKeydown: goToNextElement
236
- },
237
- DD: {
238
- modelValue: day.value ? String(day.value).padStart(2, '0') : '',
239
- placeholder: lang.value.datePicker.DD,
240
- style: {
241
- 'max-width': format.value === 'DD-MM-YYYY' ? '7ch' : '4ch',
242
- 'margin-top': '-1.7em',
243
- 'margin-bottom': '-0.5em',
244
- background: 'transparent',
245
- border: 0
246
- },
247
- // suffix: format.value === 'DD-MM-YYYY' ? '-' : undefined,
248
- class: format.value === 'YYYY-MM-DD' ? 'q-ml-none' : undefined,
249
- inputClass: 'text-center',
250
- 'onUpdate:modelValue': setDay,
251
- onKeydown: goToNextElement
242
+ const setDate: InstanceType<typeof QDate>['$props']['onUpdate:modelValue'] = (
243
+ value
244
+ ) => {
245
+ setInternalDate(value, '/')
246
+ }
247
+
248
+ watch(modelValue, (newVal) => {
249
+ if (newVal && !newVal.includes('_')) {
250
+ // Proper date string (no _ padding from partial emits)
251
+ setInternalDate(newVal)
252
+ } else if (newVal === null) {
253
+ year.value = ''
254
+ month.value = ''
255
+ day.value = ''
252
256
  }
253
- }))
257
+ // Partial dates contain _ — refs are already correct from onInput
258
+ })
259
+ setInternalDate(modelValue.value)
254
260
 
255
261
  const validations = ref<((val: string) => boolean | string)[]>([
256
262
  (v) => {
257
263
  if (v !== null)
258
- // return /^\d{4}\/(0?[1-9]|1[012])\/(0?[1-9]|[12][0-9]|3[01])$/.test(v)
259
264
  return /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/.test(v)
260
265
  return true
261
266
  }
@@ -267,4 +272,24 @@ if (props.required)
267
272
  )
268
273
  </script>
269
274
 
270
- <style></style>
275
+ <style>
276
+ /* The outer QField gets q-field--labeled which leaks padding into
277
+ descendant .q-field__native elements. The auto-height override
278
+ fixes padding-top but not padding-bottom. */
279
+ .date-input-field.q-field--auto-height.q-field--labeled .q-field__native {
280
+ padding-bottom: 0 !important;
281
+ box-sizing: border-box;
282
+ }
283
+
284
+ /* Remove the standard underline (creates whitespace beneath) */
285
+ .date-input-field.q-field--standard
286
+ > .q-field__inner
287
+ > .q-field__control::before {
288
+ border-bottom: none !important;
289
+ }
290
+ .date-input-field.q-field--standard
291
+ > .q-field__inner
292
+ > .q-field__control::after {
293
+ display: none !important;
294
+ }
295
+ </style>
@@ -1,71 +1,69 @@
1
1
  <template>
2
- <q-layout view="lHh Lpr lFf">
3
- <div v-if="ready">
4
- <q-header>
5
- <q-toolbar>
2
+ <q-layout v-if="ready" view="lHh Lpr lFf">
3
+ <q-header>
4
+ <q-toolbar>
5
+ <q-btn
6
+ v-if="!miniState"
7
+ flat
8
+ dense
9
+ round
10
+ aria-label="Menu"
11
+ icon="i-mdi-menu"
12
+ @click="toggleLeftDrawer()"
13
+ >
14
+ </q-btn>
15
+ <slot name="header-toolbar" />
16
+ </q-toolbar>
17
+ </q-header>
18
+
19
+ <q-drawer
20
+ ref="drawerRef"
21
+ :model-value="leftDrawerOpen"
22
+ :width="drawerWidth"
23
+ :mini-width="80"
24
+ :mini="miniState"
25
+ show-if-above
26
+ bordered
27
+ @hide="onDrawerHide"
28
+ @update:model-value="toggleLeftDrawer"
29
+ @mouseleave="debouncedToggleMiniState(true)"
30
+ >
31
+ <template #mini>
32
+ <div
33
+ :class="{
34
+ column: true,
35
+ 'items-center': miniState,
36
+ 'pr-0': true
37
+ }"
38
+ >
6
39
  <q-btn
7
- v-if="!miniState"
8
40
  flat
9
41
  dense
10
42
  round
11
43
  aria-label="Menu"
12
44
  icon="i-mdi-menu"
45
+ class="q-mb-md"
13
46
  @click="toggleLeftDrawer()"
14
47
  >
15
48
  </q-btn>
16
- <slot name="header-toolbar" />
17
- </q-toolbar>
18
- </q-header>
19
-
20
- <q-drawer
21
- ref="drawerRef"
22
- :model-value="leftDrawerOpen"
23
- :width="drawerWidth"
24
- :mini-width="80"
25
- :mini="miniState"
26
- show-if-above
27
- bordered
28
- @hide="onDrawerHide"
29
- @update:model-value="toggleLeftDrawer"
30
- @mouseleave="debouncedToggleMiniState(true)"
31
- >
32
- <template #mini>
33
- <div
34
- :class="{
35
- column: true,
36
- 'items-center': miniState,
37
- 'pr-0': true
38
- }"
39
- >
40
- <q-btn
41
- flat
42
- dense
43
- round
44
- aria-label="Menu"
45
- icon="i-mdi-menu"
46
- class="q-mb-md"
47
- @click="toggleLeftDrawer()"
48
- >
49
- </q-btn>
50
- <div id="fabs" class="q-mb-md min-h-56px">
51
- <slot name="fabs" :show-sticky="false" />
52
- </div>
53
-
54
- <slot name="drawer-mini-navigation" />
49
+ <div id="fabs" class="q-mb-md min-h-56px">
50
+ <slot name="fabs" :show-sticky="false" />
55
51
  </div>
56
- </template>
57
- <slot name="drawer" />
58
- </q-drawer>
59
-
60
- <q-footer class="h-80px lt-md">
61
- <slot name="footer" />
62
- </q-footer>
63
-
64
- <q-page-container>
65
- <router-view />
66
- <slot name="fabs" :show-sticky="true" />
67
- </q-page-container>
68
- </div>
52
+
53
+ <slot name="drawer-mini-navigation" />
54
+ </div>
55
+ </template>
56
+ <slot name="drawer" />
57
+ </q-drawer>
58
+
59
+ <q-footer class="h-80px lt-md">
60
+ <slot name="footer" />
61
+ </q-footer>
62
+
63
+ <q-page-container>
64
+ <router-view />
65
+ <slot name="fabs" :show-sticky="true" />
66
+ </q-page-container>
69
67
  </q-layout>
70
68
  </template>
71
69
 
@@ -7,7 +7,11 @@
7
7
  'gt-sm': true,
8
8
  animated: !!seekAttention,
9
9
  'animated-tada': !!seekAttention,
10
- 'animated-infinite': !!seekAttention
10
+ 'animated-infinite': !!seekAttention,
11
+ '!bg-$light-primary-container': true,
12
+ 'dark:!bg-$dark-primary-container': true,
13
+ 'text-$on-light-primary-container': true,
14
+ 'dark:text-$on-dark-primary-container': true
11
15
  }"
12
16
  fab
13
17
  :icon="addIcon"
@@ -21,7 +25,11 @@
21
25
  'gt-sm': true,
22
26
  animated: !!seekAttention,
23
27
  'animated-tada': !!seekAttention,
24
- 'animated-infinite': !!seekAttention
28
+ 'animated-infinite': !!seekAttention,
29
+ '!bg-$light-primary-container': true,
30
+ 'dark:!bg-$dark-primary-container': true,
31
+ 'text-$on-light-primary-container': true,
32
+ 'dark:text-$on-dark-primary-container': true
25
33
  }"
26
34
  fab
27
35
  :icon="editIcon"