@a-type/ui 1.1.19 → 1.1.20

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 (42) hide show
  1. package/dist/cjs/components/card/Card.d.ts +2 -2
  2. package/dist/cjs/components/errorBoundary/ErrorBoundary.d.ts +1 -1
  3. package/dist/cjs/components/input/Input.d.ts +3 -0
  4. package/dist/cjs/components/input/Input.js +13 -2
  5. package/dist/cjs/components/input/Input.js.map +1 -1
  6. package/dist/cjs/components/layouts/PageNav.js +2 -1
  7. package/dist/cjs/components/layouts/PageNav.js.map +1 -1
  8. package/dist/cjs/components/masonry/masonry.js +5 -1
  9. package/dist/cjs/components/masonry/masonry.js.map +1 -1
  10. package/dist/cjs/components/provider/Provider.js +4 -3
  11. package/dist/cjs/components/provider/Provider.js.map +1 -1
  12. package/dist/cjs/components/utility/HideWhileKeyboardOpen.d.ts +5 -0
  13. package/dist/cjs/components/utility/HideWhileKeyboardOpen.js +30 -0
  14. package/dist/cjs/components/utility/HideWhileKeyboardOpen.js.map +1 -0
  15. package/dist/cjs/hooks/useVisualViewportOffset.d.ts +1 -0
  16. package/dist/cjs/hooks/useVisualViewportOffset.js +56 -9
  17. package/dist/cjs/hooks/useVisualViewportOffset.js.map +1 -1
  18. package/dist/css/main.css +1 -1
  19. package/dist/esm/components/card/Card.d.ts +2 -2
  20. package/dist/esm/components/errorBoundary/ErrorBoundary.d.ts +1 -1
  21. package/dist/esm/components/input/Input.d.ts +3 -0
  22. package/dist/esm/components/input/Input.js +14 -3
  23. package/dist/esm/components/input/Input.js.map +1 -1
  24. package/dist/esm/components/layouts/PageNav.js +2 -1
  25. package/dist/esm/components/layouts/PageNav.js.map +1 -1
  26. package/dist/esm/components/masonry/masonry.js +2 -1
  27. package/dist/esm/components/masonry/masonry.js.map +1 -1
  28. package/dist/esm/components/provider/Provider.js +4 -3
  29. package/dist/esm/components/provider/Provider.js.map +1 -1
  30. package/dist/esm/components/utility/HideWhileKeyboardOpen.d.ts +5 -0
  31. package/dist/esm/components/utility/HideWhileKeyboardOpen.js +24 -0
  32. package/dist/esm/components/utility/HideWhileKeyboardOpen.js.map +1 -0
  33. package/dist/esm/hooks/useVisualViewportOffset.d.ts +1 -0
  34. package/dist/esm/hooks/useVisualViewportOffset.js +54 -8
  35. package/dist/esm/hooks/useVisualViewportOffset.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/input/Input.tsx +35 -2
  38. package/src/components/layouts/PageNav.tsx +4 -2
  39. package/src/components/masonry/masonry.tsx +6 -1
  40. package/src/components/provider/Provider.tsx +9 -3
  41. package/src/components/utility/HideWhileKeyboardOpen.tsx +26 -0
  42. package/src/hooks/useVisualViewportOffset.ts +83 -25
@@ -1,30 +1,76 @@
1
1
  // @unocss-include
2
- import { useEffect } from 'react';
2
+ import { useEffect, useState } from 'react';
3
+ import { useStableCallback } from './useStableCallback.js';
4
+ import { useConfig } from '../components/provider.js';
3
5
  /**
4
6
  * Applies bottom offset px as a CSS custom property to the document root.
5
7
  */
6
8
  export function useVisualViewportOffset(disable) {
9
+ useReactToViewportChanges((viewport) => {
10
+ document.documentElement.style.setProperty('--viewport-bottom-offset', `${window.innerHeight - viewport.height - viewport.offsetTop}px`);
11
+ document.documentElement.style.setProperty('--viewport-height', `${viewport.height}px`);
12
+ document.documentElement.style.setProperty('--viewport-width', `${viewport.width}px`);
13
+ document.documentElement.style.setProperty('--viewport-top-offset', `${viewport.offsetTop}px`);
14
+ document.documentElement.style.setProperty('--viewport-left-offset', `${viewport.offsetLeft}px`);
15
+ document.documentElement.style.setProperty('--keyboard-open', viewport.height < window.innerHeight ? '1' : '0');
16
+ }, disable);
17
+ }
18
+ export function useIsKeyboardOpen() {
19
+ const { virtualKeyboardBehavior } = useConfig();
20
+ const [isViewportConstrained, setIsViewportConstrained] = useState(false);
21
+ useReactToViewportChanges((viewport) => {
22
+ setIsViewportConstrained(viewport.height < window.innerHeight);
23
+ }, virtualKeyboardBehavior !== 'displace');
24
+ const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
25
+ useEffect(() => {
26
+ if (!('virtualKeyboard' in navigator)) {
27
+ // no support
28
+ console.warn(`virtual keyboard behavior set to 'overlay', but virtualKeyboard API is not supported in this browser.`);
29
+ return;
30
+ }
31
+ const virtualKeyboard = navigator.virtualKeyboard;
32
+ const update = () => {
33
+ setIsKeyboardOpen(virtualKeyboard.boundingRect.height > 0);
34
+ };
35
+ update();
36
+ virtualKeyboard.addEventListener('geometrychange', update);
37
+ return () => {
38
+ virtualKeyboard.removeEventListener('geometrychange', update);
39
+ };
40
+ }, []);
41
+ if (virtualKeyboardBehavior === 'displace') {
42
+ return isViewportConstrained;
43
+ }
44
+ return isKeyboardOpen;
45
+ }
46
+ function useReactToViewportChanges(cb, disable) {
47
+ const stableCb = useStableCallback(cb);
7
48
  useEffect(() => {
8
49
  if (disable)
9
50
  return;
10
- const viewport = typeof window === 'undefined' ? undefined : window.visualViewport;
51
+ const viewport = window.visualViewport;
11
52
  if (!viewport) {
12
53
  return;
13
54
  }
14
55
  const update = () => {
15
- document.documentElement.style.setProperty('--viewport-bottom-offset', `${window.innerHeight - viewport.height}px`);
16
- document.documentElement.style.setProperty('--viewport-height', `${viewport.height}px`);
17
- document.documentElement.style.setProperty('--viewport-width', `${viewport.width}px`);
18
- document.documentElement.style.setProperty('--viewport-top-offset', `${viewport.offsetTop}px`);
19
- document.documentElement.style.setProperty('--viewport-left-offset', `${viewport.offsetLeft}px`);
56
+ stableCb(viewport);
57
+ };
58
+ let prevTimeout;
59
+ const debouncedUpdate = () => {
60
+ if (prevTimeout) {
61
+ clearTimeout(prevTimeout);
62
+ }
63
+ prevTimeout = window.setTimeout(update, 50);
20
64
  };
21
65
  update();
22
66
  window.addEventListener('scroll', update, { passive: true });
23
67
  viewport.addEventListener('resize', update);
68
+ viewport.addEventListener('scroll', debouncedUpdate, { passive: true });
24
69
  return () => {
25
70
  viewport.removeEventListener('resize', update);
26
71
  window.removeEventListener('scroll', update);
72
+ viewport.removeEventListener('scroll', debouncedUpdate);
27
73
  };
28
- }, [disable]);
74
+ }, [stableCb, disable]);
29
75
  }
30
76
  //# sourceMappingURL=useVisualViewportOffset.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useVisualViewportOffset.js","sourceRoot":"","sources":["../../../src/hooks/useVisualViewportOffset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAiB;IACxD,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,OAAO;YAAE,OAAO;QAEpB,MAAM,QAAQ,GACb,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC;QAEnE,IAAI,CAAC,QAAQ,EAAE;YACd,OAAO;SACP;QAED,MAAM,MAAM,GAAG,GAAG,EAAE;YACnB,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CACzC,0BAA0B,EAC1B,GAAG,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAC,MAAM,IAAI,CAC3C,CAAC;YACF,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CACzC,mBAAmB,EACnB,GAAG,QAAQ,CAAC,MAAM,IAAI,CACtB,CAAC;YACF,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CACzC,kBAAkB,EAClB,GAAG,QAAQ,CAAC,KAAK,IAAI,CACrB,CAAC;YACF,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CACzC,uBAAuB,EACvB,GAAG,QAAQ,CAAC,SAAS,IAAI,CACzB,CAAC;YACF,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CACzC,wBAAwB,EACxB,GAAG,QAAQ,CAAC,UAAU,IAAI,CAC1B,CAAC;QACH,CAAC,CAAC;QAEF,MAAM,EAAE,CAAC;QAET,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAE5C,OAAO,GAAG,EAAE;YACX,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC/C,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC9C,CAAC,CAAC;IACH,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"useVisualViewportOffset.js","sourceRoot":"","sources":["../../../src/hooks/useVisualViewportOffset.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-type/ui",
3
- "version": "1.1.19",
3
+ "version": "1.1.20",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "/dist",
@@ -1,10 +1,11 @@
1
1
  import classNames from 'clsx';
2
2
  import {
3
3
  ComponentProps,
4
- ComponentPropsWithRef,
5
4
  FocusEvent,
6
5
  forwardRef,
7
6
  useCallback,
7
+ useEffect,
8
+ useState,
8
9
  } from 'react';
9
10
  import { Slot } from '@radix-ui/react-slot';
10
11
 
@@ -20,10 +21,23 @@ export interface InputProps extends ComponentProps<'input'> {
20
21
  variant?: 'default' | 'primary';
21
22
  autoSelect?: boolean;
22
23
  asChild?: boolean;
24
+ /** Shuffle between random placeholders */
25
+ placeholders?: string[];
26
+ placeholdersIntervalMs?: number;
23
27
  }
24
28
 
25
29
  export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
26
- { className, autoSelect, onFocus, variant: _, asChild, ...props },
30
+ {
31
+ className,
32
+ autoSelect,
33
+ onFocus,
34
+ variant: _,
35
+ asChild,
36
+ placeholders,
37
+ placeholder,
38
+ placeholdersIntervalMs = 5000,
39
+ ...props
40
+ },
27
41
  ref,
28
42
  ) {
29
43
  const handleFocus = useCallback(
@@ -36,6 +50,24 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
36
50
  [onFocus, autoSelect],
37
51
  );
38
52
 
53
+ const [randomPlaceholder, setRandomPlaceholder] = useState<
54
+ string | undefined
55
+ >(
56
+ placeholders
57
+ ? placeholders[Math.floor(Math.random() * placeholders.length)]
58
+ : undefined,
59
+ );
60
+ useEffect(() => {
61
+ if (placeholders) {
62
+ const interval = setInterval(() => {
63
+ setRandomPlaceholder(
64
+ placeholders[Math.floor(Math.random() * placeholders.length)],
65
+ );
66
+ }, placeholdersIntervalMs);
67
+ return () => clearInterval(interval);
68
+ }
69
+ }, [placeholders, placeholdersIntervalMs]);
70
+
39
71
  const Component = asChild ? Slot : 'input';
40
72
 
41
73
  return (
@@ -44,6 +76,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
44
76
  onFocus={handleFocus}
45
77
  className={classNames(inputClassName, className)}
46
78
  ref={ref}
79
+ placeholder={placeholder ?? randomPlaceholder}
47
80
  />
48
81
  );
49
82
  });
@@ -3,6 +3,7 @@
3
3
  import classNames from 'clsx';
4
4
  import { HTMLAttributes, useRef } from 'react';
5
5
  import { useBoundsCssVars } from '../../hooks.js';
6
+ import { HideWhileKeyboardOpen } from '../utility/HideWhileKeyboardOpen.js';
6
7
 
7
8
  export function PageNav({
8
9
  className,
@@ -17,8 +18,9 @@ export function PageNav({
17
18
  height: '--nav-height',
18
19
  ready: '--nav-ready',
19
20
  });
21
+
20
22
  return (
21
- <div
23
+ <HideWhileKeyboardOpen
22
24
  {...props}
23
25
  className={classNames(
24
26
  'layer-components:([grid-area:nav] relative z-nav pb-[calc(0.25rem+env(safe-area-inset-bottom,0px))])',
@@ -28,6 +30,6 @@ export function PageNav({
28
30
  ref={ref}
29
31
  >
30
32
  {children}
31
- </div>
33
+ </HideWhileKeyboardOpen>
32
34
  );
33
35
  }
@@ -1,4 +1,5 @@
1
1
  import { debounce } from '@a-type/utils';
2
+ import clsx from 'clsx';
2
3
  import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
3
4
 
4
5
  interface Layout {
@@ -240,7 +241,11 @@ export function Masonry({
240
241
  }, [layout, ref]);
241
242
 
242
243
  return (
243
- <div ref={ref} style={initialStyle} className={className}>
244
+ <div
245
+ ref={ref}
246
+ style={initialStyle}
247
+ className={clsx('layer-components:z-1', className)}
248
+ >
244
249
  {children}
245
250
  </div>
246
251
  );
@@ -27,7 +27,9 @@ export function Provider({
27
27
  virtualKeyboardBehavior = 'displace',
28
28
  }: ProviderProps) {
29
29
  useVisualViewportOffset(disableViewportOffset);
30
- useVirtualKeyboardBehavior(virtualKeyboardBehavior);
30
+ const supportedVirtualKeyboardBehavior =
31
+ 'virtualKeyboard' in navigator ? virtualKeyboardBehavior : 'displace';
32
+ useVirtualKeyboardBehavior(supportedVirtualKeyboardBehavior);
31
33
 
32
34
  const otherStuff = (
33
35
  <>
@@ -41,7 +43,9 @@ export function Provider({
41
43
 
42
44
  if (disableParticles)
43
45
  return (
44
- <ConfigContext.Provider value={{ virtualKeyboardBehavior }}>
46
+ <ConfigContext.Provider
47
+ value={{ virtualKeyboardBehavior: supportedVirtualKeyboardBehavior }}
48
+ >
45
49
  <TooltipProvider>
46
50
  {children}
47
51
  {otherStuff}
@@ -50,7 +54,9 @@ export function Provider({
50
54
  );
51
55
 
52
56
  return (
53
- <ConfigContext.Provider value={{ virtualKeyboardBehavior }}>
57
+ <ConfigContext.Provider
58
+ value={{ virtualKeyboardBehavior: supportedVirtualKeyboardBehavior }}
59
+ >
54
60
  <TooltipProvider>
55
61
  <ParticleLayer>
56
62
  {children}
@@ -0,0 +1,26 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { useIsKeyboardOpen } from '../../hooks.js';
3
+ import clsx from 'clsx';
4
+ import { forwardRef, HTMLAttributes } from 'react';
5
+
6
+ export interface HideWhileKeyboardOpenProps
7
+ extends HTMLAttributes<HTMLDivElement> {
8
+ asChild?: boolean;
9
+ }
10
+
11
+ export const HideWhileKeyboardOpen = forwardRef<
12
+ HTMLDivElement,
13
+ HideWhileKeyboardOpenProps
14
+ >(function HideWhileKeyboardOpen({ asChild, className, ...rest }, ref) {
15
+ const isKeyboardOpen = useIsKeyboardOpen();
16
+
17
+ const Comp = asChild ? Slot : 'div';
18
+
19
+ return (
20
+ <Comp
21
+ ref={ref}
22
+ className={clsx(isKeyboardOpen && 'layer-responsive:hidden', className)}
23
+ {...rest}
24
+ />
25
+ );
26
+ });
@@ -1,50 +1,108 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useState } from 'react';
2
+ import { useStableCallback } from './useStableCallback.js';
3
+ import { useConfig } from '../components/provider.js';
2
4
 
3
5
  /**
4
6
  * Applies bottom offset px as a CSS custom property to the document root.
5
7
  */
6
8
  export function useVisualViewportOffset(disable?: boolean) {
9
+ useReactToViewportChanges((viewport) => {
10
+ document.documentElement.style.setProperty(
11
+ '--viewport-bottom-offset',
12
+ `${window.innerHeight - viewport.height - viewport.offsetTop}px`,
13
+ );
14
+ document.documentElement.style.setProperty(
15
+ '--viewport-height',
16
+ `${viewport.height}px`,
17
+ );
18
+ document.documentElement.style.setProperty(
19
+ '--viewport-width',
20
+ `${viewport.width}px`,
21
+ );
22
+ document.documentElement.style.setProperty(
23
+ '--viewport-top-offset',
24
+ `${viewport.offsetTop}px`,
25
+ );
26
+ document.documentElement.style.setProperty(
27
+ '--viewport-left-offset',
28
+ `${viewport.offsetLeft}px`,
29
+ );
30
+ document.documentElement.style.setProperty(
31
+ '--keyboard-open',
32
+ viewport.height < window.innerHeight ? '1' : '0',
33
+ );
34
+ }, disable);
35
+ }
36
+
37
+ export function useIsKeyboardOpen() {
38
+ const { virtualKeyboardBehavior } = useConfig();
39
+
40
+ const [isViewportConstrained, setIsViewportConstrained] = useState(false);
41
+ useReactToViewportChanges((viewport) => {
42
+ setIsViewportConstrained(viewport.height < window.innerHeight);
43
+ }, virtualKeyboardBehavior !== 'displace');
44
+
45
+ const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
7
46
  useEffect(() => {
8
- if (disable) return;
47
+ if (!('virtualKeyboard' in navigator)) {
48
+ // no support
49
+ console.warn(
50
+ `virtual keyboard behavior set to 'overlay', but virtualKeyboard API is not supported in this browser.`,
51
+ );
52
+ return;
53
+ }
54
+ const virtualKeyboard = navigator.virtualKeyboard as any;
55
+ const update = () => {
56
+ setIsKeyboardOpen(virtualKeyboard.boundingRect.height > 0);
57
+ };
58
+ update();
59
+ virtualKeyboard.addEventListener('geometrychange', update);
60
+ return () => {
61
+ virtualKeyboard.removeEventListener('geometrychange', update);
62
+ };
63
+ }, []);
64
+
65
+ if (virtualKeyboardBehavior === 'displace') {
66
+ return isViewportConstrained;
67
+ }
68
+
69
+ return isKeyboardOpen;
70
+ }
9
71
 
10
- const viewport =
11
- typeof window === 'undefined' ? undefined : window.visualViewport;
72
+ function useReactToViewportChanges(
73
+ cb: (viewport: VisualViewport) => void,
74
+ disable?: boolean,
75
+ ) {
76
+ const stableCb = useStableCallback(cb);
77
+ useEffect(() => {
78
+ if (disable) return;
12
79
 
80
+ const viewport = window.visualViewport;
13
81
  if (!viewport) {
14
82
  return;
15
83
  }
16
84
 
17
85
  const update = () => {
18
- document.documentElement.style.setProperty(
19
- '--viewport-bottom-offset',
20
- `${window.innerHeight - viewport.height}px`,
21
- );
22
- document.documentElement.style.setProperty(
23
- '--viewport-height',
24
- `${viewport.height}px`,
25
- );
26
- document.documentElement.style.setProperty(
27
- '--viewport-width',
28
- `${viewport.width}px`,
29
- );
30
- document.documentElement.style.setProperty(
31
- '--viewport-top-offset',
32
- `${viewport.offsetTop}px`,
33
- );
34
- document.documentElement.style.setProperty(
35
- '--viewport-left-offset',
36
- `${viewport.offsetLeft}px`,
37
- );
86
+ stableCb(viewport);
87
+ };
88
+ let prevTimeout: number | undefined;
89
+ const debouncedUpdate = () => {
90
+ if (prevTimeout) {
91
+ clearTimeout(prevTimeout);
92
+ }
93
+ prevTimeout = window.setTimeout(update, 50);
38
94
  };
39
95
 
40
96
  update();
41
97
 
42
98
  window.addEventListener('scroll', update, { passive: true });
43
99
  viewport.addEventListener('resize', update);
100
+ viewport.addEventListener('scroll', debouncedUpdate, { passive: true });
44
101
 
45
102
  return () => {
46
103
  viewport.removeEventListener('resize', update);
47
104
  window.removeEventListener('scroll', update);
105
+ viewport.removeEventListener('scroll', debouncedUpdate);
48
106
  };
49
- }, [disable]);
107
+ }, [stableCb, disable]);
50
108
  }