@leavepulse/ui 0.27.1 → 0.28.1

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.
@@ -1,5 +1,5 @@
1
1
  import { defineComponent, computed, openBlock, createBlock, resolveDynamicComponent, normalizeClass, withCtx, renderSlot } from "vue";
2
- const revealClass = "relative after:absolute after:inset-x-0 after:-bottom-0.5 after:h-px after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-[var(--duration-medium)] hover:after:scale-x-100 focus-visible:after:scale-x-100";
2
+ const REVEAL_BASE = "relative after:absolute after:inset-x-0 after:-bottom-0.5 after:h-px after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-[var(--duration-medium)] after:ease-[var(--ease-emphasized)] motion-reduce:after:transition-none";
3
3
  const _sfc_main = /* @__PURE__ */ defineComponent({
4
4
  __name: "LpLink",
5
5
  props: {
@@ -8,7 +8,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
8
8
  muted: { type: Boolean },
9
9
  tone: {},
10
10
  as: { default: "a" },
11
- variant: { default: "default" }
11
+ variant: { default: "default" },
12
+ revealOn: { default: "self" }
12
13
  },
13
14
  setup(__props) {
14
15
  const props = __props;
@@ -28,6 +29,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
28
29
  return "text-brand hover:text-brand-hover";
29
30
  }
30
31
  });
32
+ const REVEAL_TRIGGER = {
33
+ self: "hover:after:scale-x-100 focus-visible:after:scale-x-100",
34
+ group: "group-hover:after:scale-x-100 group-focus-visible:after:scale-x-100 focus-visible:after:scale-x-100"
35
+ };
36
+ const revealClass = computed(
37
+ () => `${REVEAL_BASE} ${REVEAL_TRIGGER[props.revealOn]}`
38
+ );
31
39
  return (_ctx, _cache) => {
32
40
  return openBlock(), createBlock(resolveDynamicComponent(__props.as), {
33
41
  href: isAnchor.value ? __props.href : void 0,
@@ -35,7 +43,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
35
43
  rel: rel.value,
36
44
  class: normalizeClass(["inline-flex items-center gap-1 underline-offset-2 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring", [
37
45
  colorClass.value,
38
- __props.variant === "reveal" ? revealClass : __props.variant === "underline" ? "underline decoration-current/40 hover:decoration-current" : "hover:underline"
46
+ __props.variant === "reveal" ? revealClass.value : __props.variant === "underline" ? "underline decoration-current/40 hover:decoration-current" : "hover:underline"
39
47
  ]])
40
48
  }, {
41
49
  default: withCtx(() => [
@@ -1,6 +1,7 @@
1
- import { defineComponent, openBlock, createBlock, unref, withCtx, createVNode } from "vue";
1
+ import { defineComponent, useId, openBlock, createBlock, unref, withCtx, createVNode, createElementBlock, toDisplayString, createCommentVNode } from "vue";
2
2
  import { NumberFieldRoot, NumberFieldDecrement, NumberFieldInput, NumberFieldIncrement } from "reka-ui";
3
3
  import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
4
+ const _hoisted_1 = ["id"];
4
5
  const _sfc_main = /* @__PURE__ */ defineComponent({
5
6
  __name: "LpNumberField",
6
7
  props: {
@@ -8,10 +9,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
8
9
  min: {},
9
10
  max: {},
10
11
  step: {},
11
- disabled: { type: Boolean }
12
+ disabled: { type: Boolean },
13
+ unit: {}
12
14
  },
13
15
  emits: ["update:modelValue"],
14
16
  setup(__props) {
17
+ const unitId = useId();
15
18
  return (_ctx, _cache) => {
16
19
  return openBlock(), createBlock(unref(NumberFieldRoot), {
17
20
  "model-value": __props.modelValue ?? void 0,
@@ -34,7 +37,15 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
34
37
  ]),
35
38
  _: 1
36
39
  }),
37
- createVNode(unref(NumberFieldInput), { class: "min-w-0 flex-1 bg-transparent text-center text-sm text-ink outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" }),
40
+ createVNode(unref(NumberFieldInput), {
41
+ "aria-describedby": __props.unit ? unref(unitId) : void 0,
42
+ class: "min-w-0 flex-1 bg-transparent text-center text-sm text-ink outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
43
+ }, null, 8, ["aria-describedby"]),
44
+ __props.unit ? (openBlock(), createElementBlock("span", {
45
+ key: 0,
46
+ id: unref(unitId),
47
+ class: "shrink-0 pr-1 text-xs text-muted"
48
+ }, toDisplayString(__props.unit), 9, _hoisted_1)) : createCommentVNode("", true),
38
49
  createVNode(unref(NumberFieldIncrement), { class: "group grid h-full w-8 place-items-center text-muted transition-colors duration-[var(--duration-fast)] hover:text-ink disabled:opacity-40" }, {
39
50
  default: withCtx(() => [
40
51
  createVNode(_sfc_main$1, {
@@ -28,6 +28,16 @@ type __VLS_Props = {
28
28
  * good for inline prose links and nav items that want a bit of motion.
29
29
  */
30
30
  variant?: "default" | "underline" | "reveal";
31
+ /**
32
+ * Which hover draws the `reveal` bar.
33
+ *
34
+ * "self" — the link's own hover (default), for an inline prose link.
35
+ * "group" — the hover of an ancestor marked `group`, for a link inside a
36
+ * row or card that is itself the click target. The row lights up and the
37
+ * name sweeps with it, instead of the bar waiting for the pointer to
38
+ * cross the text exactly.
39
+ */
40
+ revealOn?: "self" | "group";
31
41
  };
32
42
  declare var __VLS_8: {};
33
43
  type __VLS_Slots = {} & {
@@ -36,6 +46,7 @@ type __VLS_Slots = {} & {
36
46
  declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
37
47
  as: string | Component;
38
48
  variant: "default" | "underline" | "reveal";
49
+ revealOn: "self" | "group";
39
50
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
40
51
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
41
52
  declare const _default: typeof __VLS_export;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpLink-Bu01SZzB.js";
1
+ import { _ as _sfc_main } from "../chunks/LpLink-BHZ1CLlF.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  max?: number;
5
5
  step?: number;
6
6
  disabled?: boolean;
7
+ unit?: string;
7
8
  };
8
9
  declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
9
10
  "update:modelValue": (value: number | null) => any;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpNumberField-B3JuQFol.js";
1
+ import { _ as _sfc_main } from "../chunks/LpNumberField-NPE8Bumx.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
package/dist/index.js CHANGED
@@ -32,11 +32,11 @@ import { _ as _27 } from "./chunks/LpInfraNode-qIL3l7IO.js";
32
32
  import { _ as _28 } from "./chunks/LpInput-jwqXlAy0.js";
33
33
  import { _ as _29 } from "./chunks/LpLightbox-Ci35Y_xP.js";
34
34
  import { fileNameOf } from "./components/lightbox.js";
35
- import { _ as _30 } from "./chunks/LpLink-Bu01SZzB.js";
35
+ import { _ as _30 } from "./chunks/LpLink-BHZ1CLlF.js";
36
36
  import { _ as _31 } from "./chunks/LpLogViewer-CcQ3s3Fp.js";
37
37
  import { _ as _32 } from "./chunks/LpModal-D_o-56To.js";
38
38
  import { _ as _33 } from "./chunks/LpNotificationBell-Dvu-b6Vn.js";
39
- import { _ as _34 } from "./chunks/LpNumberField-B3JuQFol.js";
39
+ import { _ as _34 } from "./chunks/LpNumberField-NPE8Bumx.js";
40
40
  import { _ as _35 } from "./chunks/LpNumberFlow-amWYaeuA.js";
41
41
  import { _ as _36 } from "./chunks/LpOtpInput-DqaT0Z75.js";
42
42
  import { _ as _37 } from "./chunks/LpPagination-CeGuNe_q.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.27.1",
3
+ "version": "0.28.1",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -31,8 +31,18 @@ const props = withDefaults(
31
31
  * good for inline prose links and nav items that want a bit of motion.
32
32
  */
33
33
  variant?: "default" | "underline" | "reveal"
34
+ /**
35
+ * Which hover draws the `reveal` bar.
36
+ *
37
+ * "self" — the link's own hover (default), for an inline prose link.
38
+ * "group" — the hover of an ancestor marked `group`, for a link inside a
39
+ * row or card that is itself the click target. The row lights up and the
40
+ * name sweeps with it, instead of the bar waiting for the pointer to
41
+ * cross the text exactly.
42
+ */
43
+ revealOn?: "self" | "group"
34
44
  }>(),
35
- { variant: "default", as: "a" },
45
+ { variant: "default", as: "a", revealOn: "self" },
36
46
  )
37
47
 
38
48
  // A router link component owns its destination through its own prop (`to`),
@@ -61,8 +71,29 @@ const colorClass = computed(() => {
61
71
  // but scaled to 0 on the x-axis from the left, growing to 1 on hover/focus. The
62
72
  // bar colour follows the link colour (brand, or ink when muted) so it re-skins
63
73
  // with the theme.
64
- const revealClass =
65
- "relative after:absolute after:inset-x-0 after:-bottom-0.5 after:h-px after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-[var(--duration-medium)] hover:after:scale-x-100 focus-visible:after:scale-x-100"
74
+ //
75
+ // motion-reduce drops the travel rather than the feedback: the bar still
76
+ // appears, it just stops sweeping. A link that gave no hover response at all
77
+ // would be worse for the people that setting is for, not better.
78
+ const REVEAL_BASE =
79
+ "relative after:absolute after:inset-x-0 after:-bottom-0.5 after:h-px after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-[var(--duration-medium)] after:ease-[var(--ease-emphasized)] motion-reduce:after:transition-none"
80
+
81
+ // Whose hover draws the bar. `self` is the ordinary inline link. `group` is for
82
+ // a link inside a row, card or tile that is ITSELF the click target: the whole
83
+ // row highlights, and the name in it should sweep with that rather than only
84
+ // when the pointer happens to cross the few characters of text. Tailwind needs
85
+ // the `group` class on that ancestor.
86
+ //
87
+ // Both spelled out as whole literals: the consuming app scans this file as
88
+ // TEXT, so a class assembled at runtime is one it never emits.
89
+ const REVEAL_TRIGGER = {
90
+ self: "hover:after:scale-x-100 focus-visible:after:scale-x-100",
91
+ group: "group-hover:after:scale-x-100 group-focus-visible:after:scale-x-100 focus-visible:after:scale-x-100",
92
+ } as const
93
+
94
+ const revealClass = computed(
95
+ () => `${REVEAL_BASE} ${REVEAL_TRIGGER[props.revealOn]}`,
96
+ )
66
97
  </script>
67
98
 
68
99
  <template>
@@ -6,6 +6,7 @@ import {
6
6
  NumberFieldInput,
7
7
  NumberFieldRoot,
8
8
  } from "reka-ui"
9
+ import { useId } from "vue"
9
10
  import LpIcon from "./LpIcon.vue"
10
11
 
11
12
  defineProps<{
@@ -16,9 +17,19 @@ defineProps<{
16
17
  max?: number
17
18
  step?: number
18
19
  disabled?: boolean
20
+ // The unit the number is in ("mm", "W", "mA"). It sits inside the field
21
+ // rather than in the label, because a grid of bare boxes labelled "Width",
22
+ // "Depth", "Weight" tells the reader nothing about which one wants grams.
23
+ // Not part of the value: it is never typed, selected, or submitted.
24
+ unit?: string
19
25
  }>()
20
26
 
21
27
  defineEmits<{ (e: "update:modelValue", value: number | null): void }>()
28
+
29
+ // The unit is described to the input rather than merely placed beside it: on
30
+ // screen the two read as one field, and without this a reader announces "380"
31
+ // and leaves out the milliamps that make it mean anything.
32
+ const unitId = useId()
22
33
  </script>
23
34
 
24
35
  <template>
@@ -42,8 +53,13 @@ defineEmits<{ (e: "update:modelValue", value: number | null): void }>()
42
53
  />
43
54
  </NumberFieldDecrement>
44
55
  <NumberFieldInput
56
+ :aria-describedby="unit ? unitId : undefined"
45
57
  class="min-w-0 flex-1 bg-transparent text-center text-sm text-ink outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
46
58
  />
59
+ <!-- The unit sits between the value and the increment button, so a long one
60
+ narrows the field rather than covering the digits. Not focusable and
61
+ not part of the value: it is read, never typed. -->
62
+ <span v-if="unit" :id="unitId" class="shrink-0 pr-1 text-xs text-muted">{{ unit }}</span>
47
63
  <NumberFieldIncrement
48
64
  class="group grid h-full w-8 place-items-center text-muted transition-colors duration-[var(--duration-fast)] hover:text-ink disabled:opacity-40"
49
65
  >