@mindlogic-ai/logician-ui 4.0.0-alpha.42 → 4.0.0-alpha.44
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/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/Button.js +14 -3
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Button/Button.mjs +14 -3
- package/dist/components/Button/Button.mjs.map +1 -1
- package/dist/components/Button/Button.styles.d.ts.map +1 -1
- package/dist/components/Button/Button.styles.js +6 -2
- package/dist/components/Button/Button.styles.js.map +1 -1
- package/dist/components/Button/Button.styles.mjs +6 -2
- package/dist/components/Button/Button.styles.mjs.map +1 -1
- package/dist/components/Checkbox/Checkbox.styles.js +1 -1
- package/dist/components/Checkbox/Checkbox.styles.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.styles.mjs +1 -1
- package/dist/components/Checkbox/Checkbox.styles.mjs.map +1 -1
- package/dist/components/ErrorFallback/ErrorFallback.d.ts.map +1 -1
- package/dist/components/ErrorFallback/ErrorFallback.js +4 -1
- package/dist/components/ErrorFallback/ErrorFallback.js.map +1 -1
- package/dist/components/ErrorFallback/ErrorFallback.mjs +4 -1
- package/dist/components/ErrorFallback/ErrorFallback.mjs.map +1 -1
- package/dist/components/IconButton/IconButton.d.ts.map +1 -1
- package/dist/components/IconButton/IconButton.js +6 -1
- package/dist/components/IconButton/IconButton.js.map +1 -1
- package/dist/components/IconButton/IconButton.mjs +6 -1
- package/dist/components/IconButton/IconButton.mjs.map +1 -1
- package/dist/components/InfoSprinkle/InfoSprinkle.d.ts.map +1 -1
- package/dist/components/InfoSprinkle/InfoSprinkle.js +1 -1
- package/dist/components/InfoSprinkle/InfoSprinkle.js.map +1 -1
- package/dist/components/InfoSprinkle/InfoSprinkle.mjs +1 -1
- package/dist/components/InfoSprinkle/InfoSprinkle.mjs.map +1 -1
- package/dist/components/Loaders/PageLoader.d.ts.map +1 -1
- package/dist/components/Loaders/PageLoader.js +4 -1
- package/dist/components/Loaders/PageLoader.js.map +1 -1
- package/dist/components/Loaders/PageLoader.mjs +4 -1
- package/dist/components/Loaders/PageLoader.mjs.map +1 -1
- package/dist/components/Loaders/SectionLoader.d.ts.map +1 -1
- package/dist/components/Loaders/SectionLoader.js +1 -1
- package/dist/components/Loaders/SectionLoader.js.map +1 -1
- package/dist/components/Loaders/SectionLoader.mjs +1 -1
- package/dist/components/Loaders/SectionLoader.mjs.map +1 -1
- package/dist/components/Menu/MenuItem.d.ts.map +1 -1
- package/dist/components/Menu/MenuItem.js +3 -1
- package/dist/components/Menu/MenuItem.js.map +1 -1
- package/dist/components/Menu/MenuItem.mjs +3 -1
- package/dist/components/Menu/MenuItem.mjs.map +1 -1
- package/dist/components/Menu/MenuList.js +2 -2
- package/dist/components/Menu/MenuList.js.map +1 -1
- package/dist/components/Menu/MenuList.mjs +2 -2
- package/dist/components/Menu/MenuList.mjs.map +1 -1
- package/dist/components/Popover/PopoverContent.js +2 -2
- package/dist/components/Popover/PopoverContent.js.map +1 -1
- package/dist/components/Popover/PopoverContent.mjs +2 -2
- package/dist/components/Popover/PopoverContent.mjs.map +1 -1
- package/dist/components/Select/Combobox.js +1 -1
- package/dist/components/Select/Combobox.js.map +1 -1
- package/dist/components/Select/Combobox.mjs +1 -1
- package/dist/components/Select/Combobox.mjs.map +1 -1
- package/dist/components/Select/Select.js +1 -1
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/Select.mjs +1 -1
- package/dist/components/Select/Select.mjs.map +1 -1
- package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/components/Tooltip/Tooltip.js +10 -1
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.mjs +10 -1
- package/dist/components/Tooltip/Tooltip.mjs.map +1 -1
- package/dist/components/Workflow/canvas/GraphErrorBanner/GraphErrorBanner.d.ts.map +1 -1
- package/dist/components/Workflow/canvas/GraphErrorBanner/GraphErrorBanner.js +1 -1
- package/dist/components/Workflow/canvas/GraphErrorBanner/GraphErrorBanner.js.map +1 -1
- package/dist/components/Workflow/canvas/GraphErrorBanner/GraphErrorBanner.mjs +1 -1
- package/dist/components/Workflow/canvas/GraphErrorBanner/GraphErrorBanner.mjs.map +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +2 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2 -0
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/motion.d.ts +74 -2
- package/dist/theme/motion.d.ts.map +1 -1
- package/dist/theme/motion.js +41 -8
- package/dist/theme/motion.js.map +1 -1
- package/dist/theme/motion.mjs +41 -9
- package/dist/theme/motion.mjs.map +1 -1
- package/dist/theme/recipes/accordion.d.ts +12 -0
- package/dist/theme/recipes/accordion.d.ts.map +1 -0
- package/dist/theme/recipes/accordion.js +25 -0
- package/dist/theme/recipes/accordion.js.map +1 -0
- package/dist/theme/recipes/accordion.mjs +23 -0
- package/dist/theme/recipes/accordion.mjs.map +1 -0
- package/dist/utils/focusRing.d.ts +7 -6
- package/dist/utils/focusRing.d.ts.map +1 -1
- package/dist/utils/focusRing.js +7 -4
- package/dist/utils/focusRing.js.map +1 -1
- package/dist/utils/focusRing.mjs +7 -4
- package/dist/utils/focusRing.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.presence.test.tsx +54 -0
- package/src/components/Button/Button.cursor.test.tsx +60 -0
- package/src/components/Button/Button.styles.ts +6 -2
- package/src/components/Button/Button.tsx +18 -3
- package/src/components/Checkbox/Checkbox.styles.ts +1 -1
- package/src/components/Collapsible/Collapsible.presence.test.tsx +2 -2
- package/src/components/ErrorFallback/ErrorFallback.tsx +3 -1
- package/src/components/IconButton/IconButton.tsx +7 -0
- package/src/components/InfoSprinkle/InfoSprinkle.tsx +0 -1
- package/src/components/Loaders/PageLoader.tsx +5 -2
- package/src/components/Loaders/SectionLoader.tsx +2 -1
- package/src/components/Menu/MenuItem.tsx +2 -0
- package/src/components/Menu/MenuList.tsx +2 -2
- package/src/components/Popover/PopoverContent.tsx +2 -2
- package/src/components/Select/Combobox.tsx +1 -1
- package/src/components/Select/Select.tsx +1 -1
- package/src/components/Tooltip/Tooltip.instant.test.tsx +43 -0
- package/src/components/Tooltip/Tooltip.tsx +11 -2
- package/src/components/Workflow/canvas/GraphErrorBanner/GraphErrorBanner.tsx +6 -3
- package/src/theme/Motion.stories.tsx +16 -10
- package/src/theme/animationStyles.test.ts +25 -9
- package/src/theme/claude.md +4 -2
- package/src/theme/index.ts +2 -0
- package/src/theme/motion.ts +43 -9
- package/src/theme/motionGuards.test.ts +2 -0
- package/src/theme/recipes/accordion.ts +20 -0
- package/src/utils/focusRing.ts +7 -4
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ChakraProvider } from '@chakra-ui/react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { system } from '../../theme';
|
|
6
|
+
import { Button } from './Button';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A disabled Button must not show the pointer.
|
|
10
|
+
*
|
|
11
|
+
* The Button sets `cursor="pointer"` as a style prop, which Chakra emits
|
|
12
|
+
* outside its cascade layers, while the recipe's disabled cursor sits inside
|
|
13
|
+
* `@layer recipes` — so the pointer won and a disabled button read as
|
|
14
|
+
* clickable (FactChat's 「현재 플랜」 button).
|
|
15
|
+
*/
|
|
16
|
+
const wrap = (ui: React.ReactNode) =>
|
|
17
|
+
render(<ChakraProvider value={system}>{ui}</ChakraProvider>);
|
|
18
|
+
|
|
19
|
+
describe('Button disabled cursor', () => {
|
|
20
|
+
it('enabled keeps the pointer', () => {
|
|
21
|
+
wrap(<Button>on</Button>);
|
|
22
|
+
expect(getComputedStyle(screen.getByRole('button')).cursor).toBe('pointer');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('disabled shows not-allowed', () => {
|
|
26
|
+
wrap(<Button disabled>off</Button>);
|
|
27
|
+
expect(getComputedStyle(screen.getByRole('button')).cursor).toBe(
|
|
28
|
+
'not-allowed'
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('a call-site _disabled does not drop it', () => {
|
|
33
|
+
wrap(
|
|
34
|
+
<Button disabled _disabled={{ opacity: 0.3 }}>
|
|
35
|
+
off
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
expect(getComputedStyle(screen.getByRole('button')).cursor).toBe(
|
|
39
|
+
'not-allowed'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('lift keeps it too', () => {
|
|
44
|
+
wrap(
|
|
45
|
+
<Button disabled lift>
|
|
46
|
+
off
|
|
47
|
+
</Button>
|
|
48
|
+
);
|
|
49
|
+
expect(getComputedStyle(screen.getByRole('button')).cursor).toBe(
|
|
50
|
+
'not-allowed'
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('loading shows progress, not not-allowed', () => {
|
|
55
|
+
wrap(<Button loading>busy</Button>);
|
|
56
|
+
const button = screen.getByRole('button');
|
|
57
|
+
expect(button).toBeDisabled();
|
|
58
|
+
expect(getComputedStyle(button).cursor).toBe('progress');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -31,14 +31,18 @@ type StyleProps = Partial<ChakraButtonProps>;
|
|
|
31
31
|
const PRESS = 'var(--chakra-durations-motion-press)';
|
|
32
32
|
const FEEDBACK = 'var(--chakra-durations-fast)';
|
|
33
33
|
const STANDARD = 'var(--chakra-easings-standard)';
|
|
34
|
+
// The press is contact, so it eases out: `standard` starts slowly and the
|
|
35
|
+
// first frames lag behind the finger.
|
|
36
|
+
const EMPHASIZED = 'var(--chakra-easings-emphasized)';
|
|
34
37
|
|
|
35
38
|
export const buttonTransition = [
|
|
36
|
-
`scale ${PRESS} ${
|
|
39
|
+
`scale ${PRESS} ${EMPHASIZED}`,
|
|
37
40
|
...[
|
|
38
41
|
'background-color',
|
|
39
42
|
'border-color',
|
|
40
43
|
'color',
|
|
41
|
-
|
|
44
|
+
// No `box-shadow`: no variant sets one, so the only box-shadow that ever
|
|
45
|
+
// changes here is the keyboard focus ring, and that should appear at once.
|
|
42
46
|
'opacity',
|
|
43
47
|
// Both moved by `lift`, listed unconditionally because a property nobody
|
|
44
48
|
// changes costs nothing, and leaving them out would make the opt-in jump.
|
|
@@ -2,6 +2,7 @@ import { forwardRef } from 'react';
|
|
|
2
2
|
import { Button as ChakraButton } from '@chakra-ui/react';
|
|
3
3
|
|
|
4
4
|
import { focusRing } from '@/utils/focusRing';
|
|
5
|
+
import { mergeCss } from '@/utils/mergeCss';
|
|
5
6
|
|
|
6
7
|
import { polymorphic } from '../../types/polymorphic';
|
|
7
8
|
import { buttonTransition, getButtonStyles } from './Button.styles';
|
|
@@ -35,6 +36,7 @@ const ButtonImpl = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
35
36
|
children,
|
|
36
37
|
_hover: hoverProp,
|
|
37
38
|
_active: activeProp,
|
|
39
|
+
_disabled: disabledProp,
|
|
38
40
|
...rest
|
|
39
41
|
},
|
|
40
42
|
ref
|
|
@@ -91,9 +93,22 @@ const ButtonImpl = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
91
93
|
// FactChat puts on its quiz buttons — used to silently drop the
|
|
92
94
|
// variant's pressed colour *and* the scale with it, and the button
|
|
93
95
|
// stopped reading as pressed at all. Its own keys still win; it just
|
|
94
|
-
// no longer erases the ones it did not mention.
|
|
95
|
-
_hover
|
|
96
|
-
|
|
96
|
+
// no longer erases the ones it did not mention. Deep, so a nested
|
|
97
|
+
// condition (lift's `_hover._dark`) survives a call site's own `_dark`.
|
|
98
|
+
_hover={mergeCss(styles._hover, hoverProp)}
|
|
99
|
+
_active={mergeCss(styles._active, activeProp)}
|
|
100
|
+
// `cursor` is a style prop, so it lands outside Chakra's
|
|
101
|
+
// `@layer recipes` and beat the recipe's disabled cursor — every
|
|
102
|
+
// disabled Button showed a pointer and read as clickable. The
|
|
103
|
+
// disabled cursor has to sit at this level, merged like the two above
|
|
104
|
+
// so a call site's `_disabled` cannot drop it.
|
|
105
|
+
_disabled={mergeCss(
|
|
106
|
+
'_disabled' in styles ? styles._disabled : undefined,
|
|
107
|
+
// Chakra disables a `loading` button too; it is working, not
|
|
108
|
+
// forbidden, so it gets `progress`.
|
|
109
|
+
{ cursor: rest.loading ? 'progress' : 'not-allowed' },
|
|
110
|
+
disabledProp
|
|
111
|
+
)}
|
|
97
112
|
{...focusRing}
|
|
98
113
|
cursor="pointer"
|
|
99
114
|
// Explicit identity so the press transitions from a definite value
|
|
@@ -18,7 +18,7 @@ import { CHECKMARK_DASH } from '@/theme/motion';
|
|
|
18
18
|
export const checkmarkDraw: SystemStyleObject = {
|
|
19
19
|
'& polyline, & path': {
|
|
20
20
|
strokeDasharray: CHECKMARK_DASH,
|
|
21
|
-
animation: `checkmark-draw var(--chakra-durations-
|
|
21
|
+
animation: `checkmark-draw var(--chakra-durations-moderate) var(--chakra-easings-emphasized) var(--chakra-durations-motion-beat) both`,
|
|
22
22
|
},
|
|
23
23
|
_motionReduce: {
|
|
24
24
|
'& polyline, & path': { animation: 'none', strokeDasharray: 'none' },
|
|
@@ -57,13 +57,13 @@ describe('Collapsible presence timing', () => {
|
|
|
57
57
|
);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it('leaves in half that, on the
|
|
60
|
+
it('leaves in half that, on the same ease-out', () => {
|
|
61
61
|
const { container } = setup();
|
|
62
62
|
const rules = rulesFor(container.querySelector('[data-part="content"]')!);
|
|
63
63
|
|
|
64
64
|
expect(rules).toContain('animation-duration:var(--chakra-durations-fast)');
|
|
65
65
|
expect(rules).toContain(
|
|
66
|
-
'animation-timing-function:var(--chakra-easings-
|
|
66
|
+
'animation-timing-function:var(--chakra-easings-emphasized)'
|
|
67
67
|
);
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -75,7 +75,9 @@ User Agent: ${errorInfo.userAgent}`;
|
|
|
75
75
|
<Flex
|
|
76
76
|
as="section"
|
|
77
77
|
aria-labelledby={titleId}
|
|
78
|
-
|
|
78
|
+
// `svh`: `100vh` is the largest mobile viewport and overflows under
|
|
79
|
+
// the URL bar.
|
|
80
|
+
minH="100svh"
|
|
79
81
|
align="center"
|
|
80
82
|
justify="center"
|
|
81
83
|
p={8}
|
|
@@ -4,6 +4,7 @@ import { IconButton as ChakraIconButton } from '@chakra-ui/react';
|
|
|
4
4
|
import { focusRing } from '@/utils/focusRing';
|
|
5
5
|
|
|
6
6
|
import { polymorphic } from '../../types/polymorphic';
|
|
7
|
+
import { buttonTransition } from '../Button/Button.styles';
|
|
7
8
|
import { getIconButtonStyles } from './IconButton.styles';
|
|
8
9
|
import { IconButtonOwnProps, IconButtonProps } from './IconButton.types';
|
|
9
10
|
|
|
@@ -39,6 +40,12 @@ const IconButtonImpl = forwardRef(
|
|
|
39
40
|
rounded="full"
|
|
40
41
|
{...styles}
|
|
41
42
|
{...focusRing}
|
|
43
|
+
// Same press as Button: it inherits the `_active` scale from the shared
|
|
44
|
+
// styles, but Chakra's `common` transition list has no `scale`, so the
|
|
45
|
+
// press used to snap. Same clock and reduced-motion guard as Button.
|
|
46
|
+
scale="1"
|
|
47
|
+
transition={buttonTransition}
|
|
48
|
+
animationStyle="composite"
|
|
42
49
|
{...rest}
|
|
43
50
|
>
|
|
44
51
|
{children}
|
|
@@ -10,13 +10,16 @@ export const PageLoader = ({
|
|
|
10
10
|
<Flex
|
|
11
11
|
position="fixed"
|
|
12
12
|
w="100vw"
|
|
13
|
-
|
|
13
|
+
// `dvh`, not `vh`: on mobile `100vh` is the largest viewport, so the
|
|
14
|
+
// overlay ran under the URL bar.
|
|
15
|
+
h="100dvh"
|
|
14
16
|
top={0}
|
|
15
17
|
left={0}
|
|
16
18
|
align="center"
|
|
17
19
|
justify="center"
|
|
18
20
|
opacity={isLoading ? 0.5 : 0}
|
|
19
|
-
|
|
21
|
+
animationStyle="feedback"
|
|
22
|
+
transitionProperty="opacity"
|
|
20
23
|
bg="bg.canvas"
|
|
21
24
|
zIndex={9999}
|
|
22
25
|
{...rest}
|
|
@@ -27,6 +27,8 @@ const MenuItemImpl = ({
|
|
|
27
27
|
fontWeight="semibold"
|
|
28
28
|
gap={3}
|
|
29
29
|
cursor="pointer"
|
|
30
|
+
// Same as `Button`: the style prop outranks the recipe's disabled cursor.
|
|
31
|
+
_disabled={{ cursor: 'not-allowed' }}
|
|
30
32
|
_hover={{
|
|
31
33
|
color: isDangerVariant ? 'danger.main' : 'fg.default',
|
|
32
34
|
backgroundColor: isDangerVariant ? 'danger.lightest' : 'bg.subtle',
|
|
@@ -59,9 +59,9 @@ export const MenuList = forwardRef<HTMLDivElement, MenuListProps>(
|
|
|
59
59
|
p="1.5"
|
|
60
60
|
// Chakra's recipe opens the menu in 150ms and closes it in 100 —
|
|
61
61
|
// nearly symmetric, and shorter than everything else that appears
|
|
62
|
-
// over the page. `presence` puts it on the house enter/exit ratio;
|
|
62
|
+
// over the page. `presence-anchored` puts it on the house enter/exit ratio;
|
|
63
63
|
// the slide-from-the-trigger movement is the recipe's and stays.
|
|
64
|
-
animationStyle="presence"
|
|
64
|
+
animationStyle="presence-anchored"
|
|
65
65
|
{...rest}
|
|
66
66
|
>
|
|
67
67
|
<ScaledContext fontSize={baseFontSize}>{rows}</ScaledContext>
|
|
@@ -18,8 +18,8 @@ export const PopoverContent = forwardRef<HTMLDivElement, PopoverContentProps>(
|
|
|
18
18
|
borderWidth="1px"
|
|
19
19
|
borderColor="border.subtle"
|
|
20
20
|
// Scales from its arrow's origin on the recipe's keyframes, on our
|
|
21
|
-
// clock — see `presence`.
|
|
22
|
-
animationStyle="presence"
|
|
21
|
+
// clock — see `presence-anchored`.
|
|
22
|
+
animationStyle="presence-anchored"
|
|
23
23
|
{...props}
|
|
24
24
|
>
|
|
25
25
|
<ScaledContext fontSize={baseFontSize}>{children}</ScaledContext>
|
|
@@ -27,7 +27,7 @@ const Content = forwardRef<HTMLDivElement, ChakraCombobox.ContentProps>(
|
|
|
27
27
|
ref={ref}
|
|
28
28
|
// Chakra gives this one `animationDuration: 0s` on close — the list
|
|
29
29
|
// vanishes mid-keystroke with no exit at all. Same policy as Select.
|
|
30
|
-
animationStyle="presence"
|
|
30
|
+
animationStyle="presence-anchored"
|
|
31
31
|
{...props}
|
|
32
32
|
css={mergeCss(contentStyles, css)}
|
|
33
33
|
/>
|
|
@@ -29,7 +29,7 @@ const Content = forwardRef<HTMLDivElement, ChakraSelect.ContentProps>(
|
|
|
29
29
|
ref={ref}
|
|
30
30
|
// Chakra closes this one over `fastest` (50ms), which is fast enough to
|
|
31
31
|
// read as the list being cut off rather than dismissed.
|
|
32
|
-
animationStyle="presence"
|
|
32
|
+
animationStyle="presence-anchored"
|
|
33
33
|
{...props}
|
|
34
34
|
css={mergeCss(contentStyles, css)}
|
|
35
35
|
/>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ChakraProvider } from '@chakra-ui/react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { system } from '../../theme';
|
|
6
|
+
import { Tooltip } from './Tooltip';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* zag marks a tooltip `data-instant` when the pointer moves to it from another
|
|
10
|
+
* open one. It skips the open delay there by itself; the animation is ours to
|
|
11
|
+
* skip. The rule has to outrank the presence clock's `:is([data-state=open])`,
|
|
12
|
+
* so this checks the emitted selector rather than trusting the cascade.
|
|
13
|
+
*/
|
|
14
|
+
describe('Tooltip instant mode', () => {
|
|
15
|
+
it('drops the enter/exit animation when zag marks the content instant', () => {
|
|
16
|
+
const { getByRole } = render(
|
|
17
|
+
<ChakraProvider value={system}>
|
|
18
|
+
<Tooltip content="hint" open>
|
|
19
|
+
<button type="button">trigger</button>
|
|
20
|
+
</Tooltip>
|
|
21
|
+
</ChakraProvider>
|
|
22
|
+
);
|
|
23
|
+
const content = getByRole('tooltip');
|
|
24
|
+
const css = Array.from(document.querySelectorAll('style'))
|
|
25
|
+
.map((s) => s.textContent ?? '')
|
|
26
|
+
.join('\n');
|
|
27
|
+
const classes = content.className
|
|
28
|
+
.split(' ')
|
|
29
|
+
.filter((c) => c.startsWith('css-'));
|
|
30
|
+
const rule = classes
|
|
31
|
+
.flatMap((c) => [
|
|
32
|
+
...css.matchAll(
|
|
33
|
+
new RegExp(
|
|
34
|
+
`\\.${c}\\[data-instant\\]\\[data-state\\]\\{([^}]*)\\}`,
|
|
35
|
+
'g'
|
|
36
|
+
)
|
|
37
|
+
),
|
|
38
|
+
])
|
|
39
|
+
.map((m) => m[1])
|
|
40
|
+
.join(';');
|
|
41
|
+
expect(rule).toContain('animation-duration:0ms');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -53,10 +53,19 @@ export const Tooltip = forwardRef(
|
|
|
53
53
|
// ways — which is why a tooltip used to feel like it was being
|
|
54
54
|
// peeled off the trigger. It has the shortest dwell of anything
|
|
55
55
|
// here, so the exit being half the enter matters most.
|
|
56
|
-
animationStyle="presence"
|
|
56
|
+
animationStyle="presence-anchored"
|
|
57
57
|
{...contentProps}
|
|
58
58
|
css={mergeCss(
|
|
59
|
-
{
|
|
59
|
+
{
|
|
60
|
+
'--tooltip-bg': 'var(--chakra-colors-gray-1200)',
|
|
61
|
+
// zag sets `data-instant` when the pointer moves from one
|
|
62
|
+
// open tooltip to the next. It already skips the delay
|
|
63
|
+
// there; skip the enter/exit too, so sweeping a toolbar
|
|
64
|
+
// doesn't replay the animation on every button. The extra
|
|
65
|
+
// `[data-state]` outranks the presence clock's
|
|
66
|
+
// `:is([data-state=open])`.
|
|
67
|
+
'&[data-instant][data-state]': { animationDuration: '0ms' },
|
|
68
|
+
},
|
|
60
69
|
contentProps?.css
|
|
61
70
|
)}
|
|
62
71
|
>
|
|
@@ -201,7 +201,8 @@ export function GraphErrorBanner({
|
|
|
201
201
|
border="none"
|
|
202
202
|
cursor="pointer"
|
|
203
203
|
opacity={showValidating ? 0.6 : 1}
|
|
204
|
-
|
|
204
|
+
animationStyle="feedback"
|
|
205
|
+
transitionProperty="opacity"
|
|
205
206
|
onClick={() => setOpen((o) => !o)}
|
|
206
207
|
>
|
|
207
208
|
<Text fontSize="sm" fontWeight="semibold" color="slate.1300">
|
|
@@ -211,7 +212,8 @@ export function GraphErrorBanner({
|
|
|
211
212
|
boxSize="xs"
|
|
212
213
|
color="slate.900"
|
|
213
214
|
transform={open ? 'rotate(180deg)' : 'none'}
|
|
214
|
-
|
|
215
|
+
animationStyle="feedback"
|
|
216
|
+
transitionProperty="transform"
|
|
215
217
|
/>
|
|
216
218
|
</Box>
|
|
217
219
|
{open ? (
|
|
@@ -221,7 +223,8 @@ export function GraphErrorBanner({
|
|
|
221
223
|
maxH="80"
|
|
222
224
|
overflowY="auto"
|
|
223
225
|
opacity={showValidating ? 0.6 : 1}
|
|
224
|
-
|
|
226
|
+
animationStyle="feedback"
|
|
227
|
+
transitionProperty="opacity"
|
|
225
228
|
borderTopWidth="1px"
|
|
226
229
|
borderColor="slate.200"
|
|
227
230
|
>
|
|
@@ -178,6 +178,11 @@ const VOCAB = [
|
|
|
178
178
|
name: 'presence',
|
|
179
179
|
line: 'animationStyle="presence"',
|
|
180
180
|
},
|
|
181
|
+
{
|
|
182
|
+
when: '트리거에 붙은 팝업이 열리고 닫힌다',
|
|
183
|
+
name: 'presence-anchored',
|
|
184
|
+
line: 'animationStyle="presence-anchored"',
|
|
185
|
+
},
|
|
181
186
|
{
|
|
182
187
|
when: '목록이 차례로 도착한다',
|
|
183
188
|
name: 'stagger',
|
|
@@ -733,7 +738,7 @@ export const Choose: Story = {
|
|
|
733
738
|
|
|
734
739
|
<Preset
|
|
735
740
|
name="press"
|
|
736
|
-
timing="120ms ·
|
|
741
|
+
timing="120ms · emphasized"
|
|
737
742
|
lead="포인터가 눌리는 순간의 접촉감. 유일하게 property를 안 적어도 되는 프리셋입니다 — 프레스는 언제나 scale이라 기본값이 들어 있습니다."
|
|
738
743
|
code={`<Box
|
|
739
744
|
scale="1"
|
|
@@ -805,7 +810,7 @@ export const Choose: Story = {
|
|
|
805
810
|
<Preset
|
|
806
811
|
name="presence"
|
|
807
812
|
timing="진입 300ms · emphasized / 퇴장 150ms · standard"
|
|
808
|
-
lead="열리고 닫히는 모든 것. 진입은 읽혀야 하고 퇴장은 이미 결정된 일이라 절반입니다.
|
|
813
|
+
lead="열리고 닫히는 모든 것. 진입은 읽혀야 하고 퇴장은 이미 결정된 일이라 절반입니다. Modal · BottomSheet · Collapsible 은 300 / 150, 트리거에 붙은 Menu · Select · Combobox · Popover · Tooltip 은 presence-anchored 로 200 / 100 입니다."
|
|
809
814
|
code={`<Menu.List animationStyle="presence" />
|
|
810
815
|
|
|
811
816
|
// 무엇이 움직이는지는 각 recipe의 것이고, presence는 시계만 바꿉니다 —
|
|
@@ -819,10 +824,10 @@ export const Choose: Story = {
|
|
|
819
824
|
<Stack gap={4}>
|
|
820
825
|
<Stack gap={1}>
|
|
821
826
|
{[
|
|
822
|
-
['Menu', '150 / 100 →
|
|
823
|
-
['Popover', '150 / 100 →
|
|
824
|
-
['Tooltip', '150 / 150 →
|
|
825
|
-
['Select', '150 / 50 →
|
|
827
|
+
['Menu', '150 / 100 → 200 / 100 (anchored)'],
|
|
828
|
+
['Popover', '150 / 100 → 200 / 100 (anchored)'],
|
|
829
|
+
['Tooltip', '150 / 150 → 200 / 100 (anchored)'],
|
|
830
|
+
['Select', '150 / 50 → 200 / 100 (anchored)'],
|
|
826
831
|
['Collapsible', '200 / 200 → 300 / 150'],
|
|
827
832
|
['Modal', '자체 keyframe + presence 시계'],
|
|
828
833
|
].map(([name, change]) => (
|
|
@@ -956,7 +961,7 @@ export const Choose: Story = {
|
|
|
956
961
|
lead="한 요소 안에서 속성마다 다른 속도가 필요할 때만. Button은 프레스가 120ms인데 색은 150ms라 duration 하나로 표현이 안 됩니다. composite가 주는 것은 동작 줄이기 가드 하나뿐이고, 값은 반드시 토큰 var()로 적으세요."
|
|
957
962
|
code={`// Button.styles.ts
|
|
958
963
|
export const buttonTransition = [
|
|
959
|
-
'scale var(--chakra-durations-motion-press) var(--chakra-easings-
|
|
964
|
+
'scale var(--chakra-durations-motion-press) var(--chakra-easings-emphasized)',
|
|
960
965
|
'background-color var(--chakra-durations-fast) var(--chakra-easings-standard)',
|
|
961
966
|
].join(', ');
|
|
962
967
|
|
|
@@ -1087,7 +1092,8 @@ const OURS = [
|
|
|
1087
1092
|
|
|
1088
1093
|
const CHAKRAS = [
|
|
1089
1094
|
{ name: 'fast', ms: 150, use: 'feedback 프리셋 · presence 퇴장' },
|
|
1090
|
-
{ name: '
|
|
1095
|
+
{ name: 'faster', ms: 100, use: 'presence-anchored 퇴장' },
|
|
1096
|
+
{ name: 'moderate', ms: 200, use: 'presence-anchored 진입' },
|
|
1091
1097
|
{ name: 'slow', ms: 300, use: '⚠️ 우리 motion.slow(500ms)와 다른 값' },
|
|
1092
1098
|
{ name: 'slower', ms: 400, use: '⚠️ 우리 motion.slower(700ms)와 다른 값' },
|
|
1093
1099
|
];
|
|
@@ -1097,13 +1103,13 @@ const CURVES = [
|
|
|
1097
1103
|
name: 'standard',
|
|
1098
1104
|
d: 'M0,100 C40,100 20,0 100,0',
|
|
1099
1105
|
half: '50%',
|
|
1100
|
-
use: '
|
|
1106
|
+
use: 'feedback',
|
|
1101
1107
|
},
|
|
1102
1108
|
{
|
|
1103
1109
|
name: 'emphasized',
|
|
1104
1110
|
d: 'M0,100 C22,0 36,0 100,0',
|
|
1105
1111
|
half: '96%',
|
|
1106
|
-
use: 'travel · presence
|
|
1112
|
+
use: 'press · travel · presence 진입·퇴장',
|
|
1107
1113
|
},
|
|
1108
1114
|
{
|
|
1109
1115
|
name: 'overshoot',
|
|
@@ -106,7 +106,7 @@ describe('motion vocabulary', () => {
|
|
|
106
106
|
const d = (n: string) => `var(--chakra-durations-${n})`;
|
|
107
107
|
const e = (n: string) => `var(--chakra-easings-${n})`;
|
|
108
108
|
|
|
109
|
-
expect(timing('press')).toEqual([d('motion-press'), e('
|
|
109
|
+
expect(timing('press')).toEqual([d('motion-press'), e('emphasized')]);
|
|
110
110
|
expect(timing('feedback')).toEqual([d('fast'), e('standard')]);
|
|
111
111
|
expect(timing('travel')).toEqual([d('motion-base'), e('emphasized')]);
|
|
112
112
|
expect(timing('spring')).toEqual([d('motion-base'), e('overshoot')]);
|
|
@@ -133,11 +133,12 @@ describe('the enter/exit policy', () => {
|
|
|
133
133
|
// A guard on the guard: if the condition Chakra emits for `_open` ever
|
|
134
134
|
// changes spelling, `withOpen` silently empties and every assertion below
|
|
135
135
|
// passes over nothing.
|
|
136
|
-
// `presence`
|
|
137
|
-
// carries its own two keyframe names and
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
136
|
+
// `presence` and its shorter anchored twin are the only global presets
|
|
137
|
+
// with an open state — the Modal carries its own two keyframe names and
|
|
138
|
+
// borrows the `presence` clock. The walk stays a walk rather than an
|
|
139
|
+
// assertion about these two alone, so the next preset with an `_open`
|
|
140
|
+
// branch is covered the day it is written.
|
|
141
|
+
expect(withOpen).toEqual(['presence', 'presence-anchored']);
|
|
141
142
|
});
|
|
142
143
|
|
|
143
144
|
it('never declares an enter without the matching exit', () => {
|
|
@@ -162,8 +163,23 @@ describe('the enter/exit policy', () => {
|
|
|
162
163
|
// The reason one preset can sit on a menu that slides, a popover that
|
|
163
164
|
// scales and a collapsible that interpolates a measured height. A name here
|
|
164
165
|
// would flatten all three into whatever it named.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
withOpen.forEach((name) => {
|
|
167
|
+
const css = applied(name) as Css;
|
|
168
|
+
expect(css[OPEN].animationName, name).toBeUndefined();
|
|
169
|
+
expect(css[CLOSED].animationName, name).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('opens anchored popups faster than dialogs, on the same curves', () => {
|
|
174
|
+
const full = applied('presence') as Css;
|
|
175
|
+
const anchored = applied('presence-anchored') as Css;
|
|
176
|
+
expect(DURATION_MS[anchored[OPEN].animationDuration]).toBe(200);
|
|
177
|
+
expect(DURATION_MS[anchored[CLOSED].animationDuration]).toBe(100);
|
|
178
|
+
expect(anchored[OPEN].animationTimingFunction).toBe(
|
|
179
|
+
full[OPEN].animationTimingFunction
|
|
180
|
+
);
|
|
181
|
+
expect(anchored[CLOSED].animationTimingFunction).toBe(
|
|
182
|
+
full[CLOSED].animationTimingFunction
|
|
183
|
+
);
|
|
168
184
|
});
|
|
169
185
|
});
|
package/src/theme/claude.md
CHANGED
|
@@ -54,10 +54,11 @@ it never knows *what* moves, so `transitionProperty` stays at the call site.
|
|
|
54
54
|
| `travel` | Something moving to a new position or size |
|
|
55
55
|
| `spring` | A physical flip, or two things crossing |
|
|
56
56
|
| `presence` | A part with **both** an open and a closed state |
|
|
57
|
+
| `presence-anchored` | The same, for a popup anchored to a trigger (Menu, Select, Combobox, Popover, Tooltip) |
|
|
57
58
|
| `stagger` | Siblings arriving in sequence (with `staggerProps(index)`) |
|
|
58
59
|
| `composite` | Escape hatch — one element needing two different clocks |
|
|
59
60
|
|
|
60
|
-
**
|
|
61
|
+
**Eight is the whole list and that is policy.** A motion with one caller belongs
|
|
61
62
|
next to that caller, not here (see `Spinner.styles.ts`, `Checkbox.styles.ts`).
|
|
62
63
|
Adding an eighth needs two real call sites that the existing seven cannot
|
|
63
64
|
express.
|
|
@@ -71,7 +72,8 @@ them:
|
|
|
71
72
|
default fails invisibly.
|
|
72
73
|
- **`prefers-reduced-motion`** is handled. Loops are the exception and slow down
|
|
73
74
|
rather than stopping — a frozen spinner reads as a dead request.
|
|
74
|
-
- **Enter is 300ms, exit is 150ms** for `presence
|
|
75
|
+
- **Enter is 300ms, exit is 150ms** for `presence` (Modal, BottomSheet,
|
|
76
|
+
Collapsible) and **200ms / 100ms** for `presence-anchored`, enforced by a test.
|
|
75
77
|
|
|
76
78
|
Three things `animationStyle` accepts and should never be given — all three
|
|
77
79
|
compile, and all three are caught by lint instead of by types, because Chakra
|
package/src/theme/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { colors, semanticTokens } from './colors';
|
|
10
10
|
import { globalCss } from './global';
|
|
11
11
|
import { animationStyles, durations, easings, keyframes } from './motion';
|
|
12
|
+
import { accordionSlotRecipe } from './recipes/accordion';
|
|
12
13
|
import { tableSlotRecipe } from './recipes/table';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -333,6 +334,7 @@ const config = defineConfig({
|
|
|
333
334
|
textarea: formControlLgOverride,
|
|
334
335
|
},
|
|
335
336
|
slotRecipes: {
|
|
337
|
+
accordion: accordionSlotRecipe,
|
|
336
338
|
table: tableSlotRecipe,
|
|
337
339
|
select: selectSlotRecipe,
|
|
338
340
|
},
|