@seed-design/react 2.4.0 → 2.4.2

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.
Files changed (107) hide show
  1. package/lib/components/ActionButton/ActionButton.cjs +2 -2
  2. package/lib/components/ActionButton/ActionButton.js +1 -1
  3. package/lib/components/AttachmentDisplay/AttachmentDisplayItem.cjs +5 -4
  4. package/lib/components/AttachmentDisplay/AttachmentDisplayItem.d.ts.map +1 -1
  5. package/lib/components/AttachmentDisplay/AttachmentDisplayItem.js +2 -1
  6. package/lib/components/AttachmentDisplay/AttachmentDisplayTrigger.cjs +2 -2
  7. package/lib/components/AttachmentDisplay/AttachmentDisplayTrigger.js +1 -1
  8. package/lib/components/AttachmentInput/AttachmentInputItem.cjs +5 -4
  9. package/lib/components/AttachmentInput/AttachmentInputItem.d.ts.map +1 -1
  10. package/lib/components/AttachmentInput/AttachmentInputItem.js +2 -1
  11. package/lib/components/AttachmentInput/AttachmentInputTrigger.cjs +2 -2
  12. package/lib/components/AttachmentInput/AttachmentInputTrigger.js +1 -1
  13. package/lib/components/BottomSheet/BottomSheet.cjs +2 -2
  14. package/lib/components/BottomSheet/BottomSheet.js +1 -1
  15. package/lib/components/Callout/Callout.cjs +3 -3
  16. package/lib/components/Callout/Callout.js +1 -1
  17. package/lib/components/Checkbox/Checkbox.cjs +2 -2
  18. package/lib/components/Checkbox/Checkbox.js +1 -1
  19. package/lib/components/Chip/Chip.cjs +2 -2
  20. package/lib/components/Chip/Chip.js +1 -1
  21. package/lib/components/ChipTabs/ChipTabs.cjs +2 -2
  22. package/lib/components/ChipTabs/ChipTabs.js +1 -1
  23. package/lib/components/ContentDialog/ContentDialog.cjs +2 -2
  24. package/lib/components/ContentDialog/ContentDialog.js +1 -1
  25. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.cjs +2 -2
  26. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.js +1 -1
  27. package/lib/components/FieldButton/FieldButton.cjs +2 -2
  28. package/lib/components/FieldButton/FieldButton.js +1 -1
  29. package/lib/components/FloatingActionButton/FloatingActionButton.cjs +2 -2
  30. package/lib/components/FloatingActionButton/FloatingActionButton.js +1 -1
  31. package/lib/components/HelpBubble/HelpBubble.cjs +2 -2
  32. package/lib/components/HelpBubble/HelpBubble.js +1 -1
  33. package/lib/components/ImageFrame/ImageFrame.cjs +2 -2
  34. package/lib/components/ImageFrame/ImageFrame.js +1 -1
  35. package/lib/components/MenuSheet/MenuSheet.cjs +2 -2
  36. package/lib/components/MenuSheet/MenuSheet.js +1 -1
  37. package/lib/components/PageBanner/PageBanner.cjs +3 -3
  38. package/lib/components/PageBanner/PageBanner.js +1 -1
  39. package/lib/components/QuantityPicker/QuantityPicker.cjs +3 -3
  40. package/lib/components/QuantityPicker/QuantityPicker.js +1 -1
  41. package/lib/components/RadioGroup/RadioGroup.cjs +2 -2
  42. package/lib/components/RadioGroup/RadioGroup.js +1 -1
  43. package/lib/components/ReactionButton/ReactionButton.cjs +2 -2
  44. package/lib/components/ReactionButton/ReactionButton.js +1 -1
  45. package/lib/components/SidePanel/SidePanel.cjs +2 -2
  46. package/lib/components/SidePanel/SidePanel.d.ts +13 -1
  47. package/lib/components/SidePanel/SidePanel.d.ts.map +1 -1
  48. package/lib/components/SidePanel/SidePanel.js +1 -1
  49. package/lib/components/Snackbar/Snackbar.cjs +2 -2
  50. package/lib/components/Snackbar/Snackbar.js +1 -1
  51. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.cjs +2 -2
  52. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.js +1 -1
  53. package/lib/components/Switch/Switch.cjs +2 -2
  54. package/lib/components/Switch/Switch.js +1 -1
  55. package/lib/components/Tabs/Tabs.cjs +2 -2
  56. package/lib/components/Tabs/Tabs.js +1 -1
  57. package/lib/components/ToggleButton/ToggleButton.cjs +2 -2
  58. package/lib/components/ToggleButton/ToggleButton.js +1 -1
  59. package/lib/index.cjs +13 -3
  60. package/lib/index.d.ts +2 -2
  61. package/lib/index.d.ts.map +1 -1
  62. package/lib/index.js +1 -1
  63. package/lib/utils/withScaleFeedback.cjs +25 -0
  64. package/lib/utils/withScaleFeedback.d.ts +8 -0
  65. package/lib/utils/withScaleFeedback.d.ts.map +1 -0
  66. package/lib/utils/withScaleFeedback.js +22 -0
  67. package/package.json +3 -2
  68. package/src/components/AGENTS.md +3 -3
  69. package/src/components/ActionButton/ActionButton.tsx +1 -1
  70. package/src/components/AttachmentDisplay/AttachmentDisplayItem.tsx +2 -1
  71. package/src/components/AttachmentDisplay/AttachmentDisplayTrigger.tsx +1 -1
  72. package/src/components/AttachmentInput/AttachmentInputItem.tsx +2 -1
  73. package/src/components/AttachmentInput/AttachmentInputTrigger.tsx +1 -1
  74. package/src/components/BottomSheet/BottomSheet.tsx +1 -1
  75. package/src/components/Callout/Callout.tsx +1 -1
  76. package/src/components/Checkbox/Checkbox.tsx +1 -1
  77. package/src/components/Chip/Chip.tsx +1 -1
  78. package/src/components/ChipTabs/ChipTabs.tsx +1 -1
  79. package/src/components/ContentDialog/ContentDialog.tsx +1 -1
  80. package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +1 -1
  81. package/src/components/FieldButton/FieldButton.tsx +1 -1
  82. package/src/components/FloatingActionButton/FloatingActionButton.tsx +1 -1
  83. package/src/components/HelpBubble/HelpBubble.tsx +1 -1
  84. package/src/components/ImageFrame/ImageFrame.tsx +1 -1
  85. package/src/components/MenuSheet/MenuSheet.tsx +1 -1
  86. package/src/components/PageBanner/PageBanner.tsx +1 -1
  87. package/src/components/QuantityPicker/QuantityPicker.tsx +1 -1
  88. package/src/components/RadioGroup/RadioGroup.tsx +1 -1
  89. package/src/components/ReactionButton/ReactionButton.tsx +1 -1
  90. package/src/components/SidePanel/SidePanel.tsx +18 -6
  91. package/src/components/Snackbar/Snackbar.tsx +1 -1
  92. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +1 -1
  93. package/src/components/Switch/Switch.tsx +1 -1
  94. package/src/components/Tabs/Tabs.tsx +1 -1
  95. package/src/components/ToggleButton/ToggleButton.tsx +1 -1
  96. package/src/index.ts +2 -2
  97. package/src/utils/withScaleFeedback.tsx +34 -0
  98. package/lib/utils/elementSizeVars.cjs +0 -76
  99. package/lib/utils/elementSizeVars.d.ts +0 -15
  100. package/lib/utils/elementSizeVars.d.ts.map +0 -1
  101. package/lib/utils/elementSizeVars.js +0 -74
  102. package/lib/utils/scaleFeedback.cjs +0 -99
  103. package/lib/utils/scaleFeedback.d.ts +0 -58
  104. package/lib/utils/scaleFeedback.d.ts.map +0 -1
  105. package/lib/utils/scaleFeedback.js +0 -94
  106. package/src/utils/elementSizeVars.tsx +0 -112
  107. package/src/utils/scaleFeedback.tsx +0 -128
@@ -1,99 +0,0 @@
1
- "use client";
2
- const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
- const require_elementSizeVars = require("./elementSizeVars.cjs");
4
- let clsx = require("clsx");
5
- clsx = require_runtime.__toESM(clsx);
6
- let react = require("react");
7
- react = require_runtime.__toESM(react);
8
- let react_jsx_runtime = require("react/jsx-runtime");
9
- let _radix_ui_react_compose_refs = require("@radix-ui/react-compose-refs");
10
- let _seed_design_css_scale_feedback = require("@seed-design/css/scale-feedback");
11
- let _radix_ui_react_slot = require("@radix-ui/react-slot");
12
- //#region src/utils/scaleFeedback.tsx
13
- /**
14
- * Opts an element into scale feedback: put `scaleFeedbackRef` and
15
- * `scaleFeedbackClassName` on the same element, and it publishes
16
- * `--seed-feedback-scale`, a ratio that shrinks it by a fixed 2px when consumed.
17
- * `ScaleFeedback` pairs them up for an element you render; reach for the hook when
18
- * you can't wrap one, such as inside a component already assembling its own
19
- * `ref` and `className`.
20
- *
21
- * The two halves belong together — the size vars `useElementSizeVars` publishes,
22
- * and the class that derives the ratio from them — so the hook hands them back
23
- * as a pair. If one goes missing the derivation stays guaranteed-invalid and
24
- * `--seed-feedback-scale` resolves to 1: a mistake costs the effect rather than
25
- * producing a wrong scale.
26
- *
27
- * Applying the ratio is yours, because the selector that scales the element is
28
- * usually the same one that changes its background, and splitting them across
29
- * two rules lets them fire under different conditions:
30
- *
31
- * .my-button {
32
- * transition: background-color 0.2s, var(--seed-feedback-scale-transition);
33
- * }
34
- * .my-button:active {
35
- * background-color: ...;
36
- * scale: var(--seed-feedback-scale);
37
- * }
38
- *
39
- * The resting `scale: 1` comes with the class rather than with the rule above:
40
- * it keeps the element a stacking context throughout, instead of becoming one
41
- * mid-press and dragging `position: fixed` descendants with it.
42
- */
43
- function useScaleFeedback() {
44
- const { sizeVarsRef } = require_elementSizeVars.useElementSizeVars();
45
- return {
46
- scaleFeedbackRef: sizeVarsRef,
47
- scaleFeedbackClassName: _seed_design_css_scale_feedback.SCALE_FEEDBACK_CLASS_NAME
48
- };
49
- }
50
- var ScaleFeedbackSlot = (0, _radix_ui_react_slot.createSlot)("ScaleFeedback");
51
- /**
52
- * Opts its child into scale feedback, so the ref and the class can no longer
53
- * land on different elements or go missing one at a time. The child keeps its own
54
- * `ref` and `className`, and applying the ratio is still yours — see
55
- * `useScaleFeedback` for the rule this expects you to write.
56
- *
57
- * There is no `asChild` prop because rendering an element of our own is never the
58
- * right answer: that element would be the one measured, and a block wrapper around
59
- * an inline-block button reports the container's width, so the ratio would come
60
- * out wrong rather than absent. The resting `scale: 1` would settle on the wrapper
61
- * too, leaving the child to become a stacking context mid-press.
62
- */
63
- var ScaleFeedback = react.forwardRef(({ children }, ref) => {
64
- const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
65
- const childRef = react.useRef(null);
66
- const composedRef = (0, _radix_ui_react_compose_refs.useComposedRefs)(scaleFeedbackRef, childRef, ref);
67
- react.useEffect(() => {
68
- if (process.env.NODE_ENV === "production") return;
69
- const node = childRef.current;
70
- if (!node) {
71
- console.warn("ScaleFeedback: the child never forwarded `ref` to a DOM element, so its size is never measured. A Fragment cannot receive one.");
72
- return;
73
- }
74
- if (!node.classList.contains(_seed_design_css_scale_feedback.SCALE_FEEDBACK_CLASS_NAME)) console.warn("ScaleFeedback: the child dropped the `className` it was handed, so no ratio is derived. Forward `className` to the element that takes `ref`.");
75
- }, []);
76
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScaleFeedbackSlot, {
77
- ref: composedRef,
78
- className: scaleFeedbackClassName,
79
- children
80
- });
81
- });
82
- ScaleFeedback.displayName = "ScaleFeedback";
83
- function withScaleFeedback(Component) {
84
- const Pressable = react.forwardRef((props, ref) => {
85
- const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
86
- const composedRef = (0, _radix_ui_react_compose_refs.useComposedRefs)(scaleFeedbackRef, ref);
87
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
88
- ...props,
89
- ref: composedRef,
90
- className: (0, clsx.default)(scaleFeedbackClassName, props.className)
91
- });
92
- });
93
- Pressable.displayName = Component.displayName;
94
- return Pressable;
95
- }
96
- //#endregion
97
- exports.ScaleFeedback = ScaleFeedback;
98
- exports.useScaleFeedback = useScaleFeedback;
99
- exports.withScaleFeedback = withScaleFeedback;
@@ -1,58 +0,0 @@
1
- import * as React from "react";
2
- /**
3
- * Opts an element into scale feedback: put `scaleFeedbackRef` and
4
- * `scaleFeedbackClassName` on the same element, and it publishes
5
- * `--seed-feedback-scale`, a ratio that shrinks it by a fixed 2px when consumed.
6
- * `ScaleFeedback` pairs them up for an element you render; reach for the hook when
7
- * you can't wrap one, such as inside a component already assembling its own
8
- * `ref` and `className`.
9
- *
10
- * The two halves belong together — the size vars `useElementSizeVars` publishes,
11
- * and the class that derives the ratio from them — so the hook hands them back
12
- * as a pair. If one goes missing the derivation stays guaranteed-invalid and
13
- * `--seed-feedback-scale` resolves to 1: a mistake costs the effect rather than
14
- * producing a wrong scale.
15
- *
16
- * Applying the ratio is yours, because the selector that scales the element is
17
- * usually the same one that changes its background, and splitting them across
18
- * two rules lets them fire under different conditions:
19
- *
20
- * .my-button {
21
- * transition: background-color 0.2s, var(--seed-feedback-scale-transition);
22
- * }
23
- * .my-button:active {
24
- * background-color: ...;
25
- * scale: var(--seed-feedback-scale);
26
- * }
27
- *
28
- * The resting `scale: 1` comes with the class rather than with the rule above:
29
- * it keeps the element a stacking context throughout, instead of becoming one
30
- * mid-press and dragging `position: fixed` descendants with it.
31
- */
32
- export declare function useScaleFeedback(): {
33
- scaleFeedbackRef: (node: HTMLElement | null) => void;
34
- scaleFeedbackClassName: "seed-scale-feedback";
35
- };
36
- export interface ScaleFeedbackProps {
37
- children: React.ReactElement;
38
- }
39
- /**
40
- * Opts its child into scale feedback, so the ref and the class can no longer
41
- * land on different elements or go missing one at a time. The child keeps its own
42
- * `ref` and `className`, and applying the ratio is still yours — see
43
- * `useScaleFeedback` for the rule this expects you to write.
44
- *
45
- * There is no `asChild` prop because rendering an element of our own is never the
46
- * right answer: that element would be the one measured, and a block wrapper around
47
- * an inline-block button reports the container's width, so the ratio would come
48
- * out wrong rather than absent. The resting `scale: 1` would settle on the wrapper
49
- * too, leaving the child to become a stacking context mid-press.
50
- */
51
- export declare const ScaleFeedback: React.ForwardRefExoticComponent<ScaleFeedbackProps & React.RefAttributes<HTMLElement>>;
52
- /**
53
- * Wraps a component so its element opts into scale feedback — for one-liner
54
- * factory components (`withContext`/`withProvider` results). Hand-written
55
- * forwardRef components call `useScaleFeedback` directly instead.
56
- */
57
- export declare function withScaleFeedback<P>(Component: React.ForwardRefExoticComponent<P>): React.ForwardRefExoticComponent<P>;
58
- //# sourceMappingURL=scaleFeedback.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"scaleFeedback.d.ts","sourceRoot":"","sources":["../../src/utils/scaleFeedback.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,gBAAgB;;;EAI/B;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC;CAC9B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,wFAkCzB,CAAC;AAIF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,SAAS,EAAE,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC,GAC5C,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC"}
@@ -1,94 +0,0 @@
1
- "use client";
2
- import { useElementSizeVars } from "./elementSizeVars.js";
3
- import clsx$1 from "clsx";
4
- import * as React$1 from "react";
5
- import { jsx } from "react/jsx-runtime";
6
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
7
- import { SCALE_FEEDBACK_CLASS_NAME } from "@seed-design/css/scale-feedback";
8
- import { createSlot } from "@radix-ui/react-slot";
9
- //#region src/utils/scaleFeedback.tsx
10
- /**
11
- * Opts an element into scale feedback: put `scaleFeedbackRef` and
12
- * `scaleFeedbackClassName` on the same element, and it publishes
13
- * `--seed-feedback-scale`, a ratio that shrinks it by a fixed 2px when consumed.
14
- * `ScaleFeedback` pairs them up for an element you render; reach for the hook when
15
- * you can't wrap one, such as inside a component already assembling its own
16
- * `ref` and `className`.
17
- *
18
- * The two halves belong together — the size vars `useElementSizeVars` publishes,
19
- * and the class that derives the ratio from them — so the hook hands them back
20
- * as a pair. If one goes missing the derivation stays guaranteed-invalid and
21
- * `--seed-feedback-scale` resolves to 1: a mistake costs the effect rather than
22
- * producing a wrong scale.
23
- *
24
- * Applying the ratio is yours, because the selector that scales the element is
25
- * usually the same one that changes its background, and splitting them across
26
- * two rules lets them fire under different conditions:
27
- *
28
- * .my-button {
29
- * transition: background-color 0.2s, var(--seed-feedback-scale-transition);
30
- * }
31
- * .my-button:active {
32
- * background-color: ...;
33
- * scale: var(--seed-feedback-scale);
34
- * }
35
- *
36
- * The resting `scale: 1` comes with the class rather than with the rule above:
37
- * it keeps the element a stacking context throughout, instead of becoming one
38
- * mid-press and dragging `position: fixed` descendants with it.
39
- */
40
- function useScaleFeedback() {
41
- const { sizeVarsRef } = useElementSizeVars();
42
- return {
43
- scaleFeedbackRef: sizeVarsRef,
44
- scaleFeedbackClassName: SCALE_FEEDBACK_CLASS_NAME
45
- };
46
- }
47
- var ScaleFeedbackSlot = createSlot("ScaleFeedback");
48
- /**
49
- * Opts its child into scale feedback, so the ref and the class can no longer
50
- * land on different elements or go missing one at a time. The child keeps its own
51
- * `ref` and `className`, and applying the ratio is still yours — see
52
- * `useScaleFeedback` for the rule this expects you to write.
53
- *
54
- * There is no `asChild` prop because rendering an element of our own is never the
55
- * right answer: that element would be the one measured, and a block wrapper around
56
- * an inline-block button reports the container's width, so the ratio would come
57
- * out wrong rather than absent. The resting `scale: 1` would settle on the wrapper
58
- * too, leaving the child to become a stacking context mid-press.
59
- */
60
- var ScaleFeedback = React$1.forwardRef(({ children }, ref) => {
61
- const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
62
- const childRef = React$1.useRef(null);
63
- const composedRef = useComposedRefs(scaleFeedbackRef, childRef, ref);
64
- React$1.useEffect(() => {
65
- if (process.env.NODE_ENV === "production") return;
66
- const node = childRef.current;
67
- if (!node) {
68
- console.warn("ScaleFeedback: the child never forwarded `ref` to a DOM element, so its size is never measured. A Fragment cannot receive one.");
69
- return;
70
- }
71
- if (!node.classList.contains(SCALE_FEEDBACK_CLASS_NAME)) console.warn("ScaleFeedback: the child dropped the `className` it was handed, so no ratio is derived. Forward `className` to the element that takes `ref`.");
72
- }, []);
73
- return /* @__PURE__ */ jsx(ScaleFeedbackSlot, {
74
- ref: composedRef,
75
- className: scaleFeedbackClassName,
76
- children
77
- });
78
- });
79
- ScaleFeedback.displayName = "ScaleFeedback";
80
- function withScaleFeedback(Component) {
81
- const Pressable = React$1.forwardRef((props, ref) => {
82
- const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
83
- const composedRef = useComposedRefs(scaleFeedbackRef, ref);
84
- return /* @__PURE__ */ jsx(Component, {
85
- ...props,
86
- ref: composedRef,
87
- className: clsx$1(scaleFeedbackClassName, props.className)
88
- });
89
- });
90
- Pressable.displayName = Component.displayName;
91
- return Pressable;
92
- }
93
- //#endregion
94
- export { ScaleFeedback, useScaleFeedback, withScaleFeedback };
@@ -1,112 +0,0 @@
1
- import * as React from "react";
2
-
3
- /**
4
- * Publishes an element's rendered size as CSS custom properties.
5
- *
6
- * The size is a plain fact about the element rather than a feature, so this
7
- * util keeps unitless `--seed-element-width` / `--seed-element-height` in sync
8
- * on the element and stops there. Whatever needs the size derives its own
9
- * values from those vars in CSS.
10
- *
11
- * The names must stay in sync with `@seed-design/qvism-preset`
12
- * (utils/scale-feedback.ts), which owns the formula that consumes them. Press
13
- * detection stays in CSS (`:active` gates), so nothing here tracks interaction;
14
- * without JS the vars are simply never set and consumers fall back to scale 1.
15
- */
16
-
17
- // One observer for every measured element; created lazily so the module stays
18
- // importable during SSR.
19
- let sharedObserver: ResizeObserver | null = null;
20
-
21
- function getSharedObserver() {
22
- sharedObserver ??= new ResizeObserver((entries) => {
23
- // `borderBoxSize` is absent before Safari 15.4, inside the range SEED
24
- // supports, so the `offsetWidth` / `offsetHeight` fallback is a live path
25
- // rather than a formality. Reading every entry before writing any property
26
- // is what keeps it free: layout is already clean when observations are
27
- // broadcast, so the reads force nothing, while a write interleaved between
28
- // them would dirty layout and make every later read reflow.
29
- const measurements = entries.map((entry) => {
30
- const element = entry.target as HTMLElement;
31
- const size = entry.borderBoxSize?.[0];
32
-
33
- return {
34
- element,
35
- width: size ? size.inlineSize : element.offsetWidth,
36
- height: size ? size.blockSize : element.offsetHeight,
37
- };
38
- });
39
-
40
- for (const { element, width, height } of measurements) {
41
- element.style.setProperty("--seed-element-width", String(width));
42
- element.style.setProperty("--seed-element-height", String(height));
43
- }
44
- });
45
-
46
- return sharedObserver;
47
- }
48
-
49
- /**
50
- * Returns a callback ref that keeps the size vars in sync on the element it is
51
- * attached to. Compose it with the element's other refs however you like: being
52
- * handed an element it already watches is a no-op, so the observation survives a
53
- * ref whose identity changes every render and the vars are republished on resize
54
- * rather than on render.
55
- *
56
- * Teardown still runs when the ref genuinely leaves an element — it is unobserved
57
- * and its vars removed — so an element that keeps the class but loses the ref
58
- * falls back to a ratio of 1 instead of holding a stale one.
59
- */
60
- export function useElementSizeVars() {
61
- // What React last handed the ref, against what the observer is watching. The
62
- // two disagree for exactly one commit whenever a ref identity changes, because
63
- // React detaches with `null` before re-attaching the very same element.
64
- const attachedRef = React.useRef<HTMLElement | null>(null);
65
- const observedRef = React.useRef<HTMLElement | null>(null);
66
-
67
- const release = React.useCallback(() => {
68
- const observed = observedRef.current;
69
- if (!observed) return;
70
-
71
- observedRef.current = null;
72
- getSharedObserver().unobserve(observed);
73
- observed.style.removeProperty("--seed-element-width");
74
- observed.style.removeProperty("--seed-element-height");
75
- }, []);
76
-
77
- const sizeVarsRef = React.useCallback(
78
- (node: HTMLElement | null) => {
79
- attachedRef.current = node;
80
-
81
- if (!node) {
82
- // A commit runs to completion synchronously, so this cannot land before
83
- // the re-attach that an identity change performs within the same commit.
84
- // By the time it runs, the two refs tell a detached element apart from
85
- // one that never actually left.
86
- queueMicrotask(() => {
87
- if (attachedRef.current === observedRef.current) return;
88
-
89
- release();
90
- });
91
- return;
92
- }
93
-
94
- if (observedRef.current === node) return;
95
-
96
- release();
97
- observedRef.current = node;
98
- // `box` selects which box change marks the element active — it does not
99
- // decide which sizes the entry carries (all of them are always present).
100
- // Observing the content box would miss a border-box change that leaves the
101
- // content box untouched, e.g. a variant that only alters padding.
102
- //
103
- // Before Safari 15.4 `observe` declares no options at all, so this argument
104
- // is dropped and that miss is exactly what happens there. Bounded: the ratio
105
- // stays valid, only derived from the last size whose content box moved.
106
- getSharedObserver().observe(node, { box: "border-box" });
107
- },
108
- [release],
109
- );
110
-
111
- return { sizeVarsRef };
112
- }
@@ -1,128 +0,0 @@
1
- import { SCALE_FEEDBACK_CLASS_NAME } from "@seed-design/css/scale-feedback";
2
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
3
- import { createSlot } from "@radix-ui/react-slot";
4
- import clsx from "clsx";
5
- import * as React from "react";
6
- import { useElementSizeVars } from "./elementSizeVars";
7
-
8
- /**
9
- * Opts an element into scale feedback: put `scaleFeedbackRef` and
10
- * `scaleFeedbackClassName` on the same element, and it publishes
11
- * `--seed-feedback-scale`, a ratio that shrinks it by a fixed 2px when consumed.
12
- * `ScaleFeedback` pairs them up for an element you render; reach for the hook when
13
- * you can't wrap one, such as inside a component already assembling its own
14
- * `ref` and `className`.
15
- *
16
- * The two halves belong together — the size vars `useElementSizeVars` publishes,
17
- * and the class that derives the ratio from them — so the hook hands them back
18
- * as a pair. If one goes missing the derivation stays guaranteed-invalid and
19
- * `--seed-feedback-scale` resolves to 1: a mistake costs the effect rather than
20
- * producing a wrong scale.
21
- *
22
- * Applying the ratio is yours, because the selector that scales the element is
23
- * usually the same one that changes its background, and splitting them across
24
- * two rules lets them fire under different conditions:
25
- *
26
- * .my-button {
27
- * transition: background-color 0.2s, var(--seed-feedback-scale-transition);
28
- * }
29
- * .my-button:active {
30
- * background-color: ...;
31
- * scale: var(--seed-feedback-scale);
32
- * }
33
- *
34
- * The resting `scale: 1` comes with the class rather than with the rule above:
35
- * it keeps the element a stacking context throughout, instead of becoming one
36
- * mid-press and dragging `position: fixed` descendants with it.
37
- */
38
- export function useScaleFeedback() {
39
- const { sizeVarsRef } = useElementSizeVars();
40
-
41
- return { scaleFeedbackRef: sizeVarsRef, scaleFeedbackClassName: SCALE_FEEDBACK_CLASS_NAME };
42
- }
43
-
44
- const ScaleFeedbackSlot = createSlot("ScaleFeedback");
45
-
46
- export interface ScaleFeedbackProps {
47
- children: React.ReactElement;
48
- }
49
-
50
- /**
51
- * Opts its child into scale feedback, so the ref and the class can no longer
52
- * land on different elements or go missing one at a time. The child keeps its own
53
- * `ref` and `className`, and applying the ratio is still yours — see
54
- * `useScaleFeedback` for the rule this expects you to write.
55
- *
56
- * There is no `asChild` prop because rendering an element of our own is never the
57
- * right answer: that element would be the one measured, and a block wrapper around
58
- * an inline-block button reports the container's width, so the ratio would come
59
- * out wrong rather than absent. The resting `scale: 1` would settle on the wrapper
60
- * too, leaving the child to become a stacking context mid-press.
61
- */
62
- export const ScaleFeedback = React.forwardRef<HTMLElement, ScaleFeedbackProps>(
63
- ({ children }, ref) => {
64
- const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
65
-
66
- const childRef = React.useRef<HTMLElement | null>(null);
67
- const composedRef = useComposedRefs(scaleFeedbackRef, childRef, ref);
68
-
69
- React.useEffect(() => {
70
- if (process.env.NODE_ENV === "production") return;
71
-
72
- // Slot throws on children it can't slot onto, but a Fragment child is a valid
73
- // element it silently declines to hand a ref to, and a component child is free
74
- // to drop what it is handed. Both leave a ratio of 1 and no other trace.
75
- const node = childRef.current;
76
- if (!node) {
77
- console.warn(
78
- "ScaleFeedback: the child never forwarded `ref` to a DOM element, so its size is never measured. A Fragment cannot receive one.",
79
- );
80
- return;
81
- }
82
-
83
- if (!node.classList.contains(SCALE_FEEDBACK_CLASS_NAME)) {
84
- console.warn(
85
- "ScaleFeedback: the child dropped the `className` it was handed, so no ratio is derived. Forward `className` to the element that takes `ref`.",
86
- );
87
- }
88
- }, []);
89
-
90
- return (
91
- <ScaleFeedbackSlot ref={composedRef} className={scaleFeedbackClassName}>
92
- {children}
93
- </ScaleFeedbackSlot>
94
- );
95
- },
96
- );
97
-
98
- ScaleFeedback.displayName = "ScaleFeedback";
99
-
100
- /**
101
- * Wraps a component so its element opts into scale feedback — for one-liner
102
- * factory components (`withContext`/`withProvider` results). Hand-written
103
- * forwardRef components call `useScaleFeedback` directly instead.
104
- */
105
- export function withScaleFeedback<P>(
106
- Component: React.ForwardRefExoticComponent<P>,
107
- ): React.ForwardRefExoticComponent<P>;
108
- export function withScaleFeedback(
109
- Component: React.ForwardRefExoticComponent<
110
- React.RefAttributes<HTMLElement> & { className?: string }
111
- >,
112
- ): React.ForwardRefExoticComponent<React.RefAttributes<HTMLElement> & { className?: string }> {
113
- const Pressable = React.forwardRef<HTMLElement, { className?: string }>((props, ref) => {
114
- const { scaleFeedbackRef, scaleFeedbackClassName } = useScaleFeedback();
115
- const composedRef = useComposedRefs(scaleFeedbackRef, ref);
116
-
117
- return (
118
- <Component
119
- {...props}
120
- ref={composedRef}
121
- className={clsx(scaleFeedbackClassName, props.className)}
122
- />
123
- );
124
- });
125
-
126
- Pressable.displayName = Component.displayName;
127
- return Pressable;
128
- }