paris 0.17.7 → 0.17.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # paris
2
2
 
3
+ ## 0.17.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 7d24348: fix(tilt): Replace react-parallax-tilt with a custom functional component to fix TypeError crash during React 19 concurrent rendering unmount
8
+
9
+ ## 0.17.8
10
+
11
+ ### Patch Changes
12
+
13
+ - 0ca30b7: fix(drawer): make bottom panel spacer padding conditional on bottomPanelPadding prop
14
+ fix(accordionselect): add padding to check icon for better tap target
15
+ feat(combobox): add customValueOption override for independent styling
16
+
3
17
  ## 0.17.7
4
18
 
5
19
  ### Patch Changes
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "paris",
3
3
  "author": "Sanil Chawla <sanil@slingshot.fm> (https://sanil.co)",
4
4
  "description": "Paris is Slingshot's React design system. It's a collection of reusable components, design tokens, and guidelines that help us build consistent, accessible, and performant user interfaces.",
5
- "version": "0.17.7",
5
+ "version": "0.17.9",
6
6
  "homepage": "https://paris.slingshot.fm",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -77,7 +77,6 @@
77
77
  "framer-motion": "^12.24.10",
78
78
  "pte": "^0.5.0",
79
79
  "react-hot-toast": "^2.4.1",
80
- "react-parallax-tilt": "^1.7.315",
81
80
  "react-tiny-popover": "^8.1.6",
82
81
  "ts-deepmerge": "^6.0.3"
83
82
  },
@@ -94,6 +94,10 @@
94
94
  background: transparent;
95
95
  }
96
96
  }
97
+
98
+ .check {
99
+ padding: 4px;
100
+ }
97
101
  }
98
102
 
99
103
  .optionContent {
@@ -136,7 +136,7 @@ export const AccordionSelect: FC<AccordionSelectProps> = ({
136
136
 
137
137
  useEffect(() => {
138
138
  if (!closeOnClickOutside || !open) {
139
- return () => {};
139
+ return () => { };
140
140
  }
141
141
 
142
142
  const handleClickOutside = (e: MouseEvent) => {
@@ -253,7 +253,7 @@ export const AccordionSelect: FC<AccordionSelectProps> = ({
253
253
  )}
254
254
  </div>
255
255
  {isOptionSelected && (
256
- <Icon icon={Check} size={13} />
256
+ <Icon icon={Check} size={13} className={styles.check} />
257
257
  )}
258
258
  </button>
259
259
  );
@@ -164,6 +164,47 @@ export const AllowCustomValue: Story = {
164
164
  },
165
165
  };
166
166
 
167
+ export const CustomValueWithDivider: Story = {
168
+ args: {
169
+ ...ComboboxArgs,
170
+ allowCustomValue: true,
171
+ customValueString: 'Add "%v"',
172
+ },
173
+ render: function Render(args) {
174
+ const [selected, setSelected] = useState<Option | null>(null);
175
+ const [inputValue, setInputValue] = useState<string>('');
176
+ return createElement(
177
+ 'div',
178
+ {
179
+ style: { minHeight: '200px' },
180
+ },
181
+ createElement(Combobox<{ name: string }>, {
182
+ ...args,
183
+ value:
184
+ selected?.id === null
185
+ ? {
186
+ id: null,
187
+ node: inputValue,
188
+ metadata: {
189
+ name: inputValue,
190
+ },
191
+ }
192
+ : (selected as Option<{ name: string }> | null),
193
+ options: (args.options as Option<{ name: string }>[]).filter((o) => ((o.metadata?.name as string) || '')
194
+ .toLowerCase()
195
+ .includes(inputValue.toLowerCase())),
196
+ onChange: (e) => setSelected(e),
197
+ onInputChange: (e) => setInputValue(e),
198
+ overrides: {
199
+ customValueOption: {
200
+ style: { borderBottom: '5px solid var(--pte-new-colors-borderMedium)' },
201
+ },
202
+ },
203
+ }),
204
+ );
205
+ },
206
+ };
207
+
167
208
  export const HideOptionsInitially: Story = {
168
209
  args: ComboboxArgs,
169
210
  render: function Render(args) {
@@ -113,6 +113,7 @@ export type ComboboxProps<T extends Record<string, any>> = {
113
113
  input?: ComponentPropsWithoutRef<'input'>;
114
114
  optionsContainer?: ComponentPropsWithoutRef<'ul'>;
115
115
  option?: ComponentPropsWithoutRef<'li'>;
116
+ customValueOption?: ComponentPropsWithoutRef<'li'>;
116
117
  label?: TextProps<'label'>;
117
118
  description?: TextProps<'p'>;
118
119
  startEnhancerContainer?: ComponentPropsWithoutRef<'div'>;
@@ -249,11 +250,6 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
249
250
  });
250
251
  }
251
252
  }}
252
- onBlur={(e) => {
253
- setQuery('');
254
- if (onInputChange) onInputChange('');
255
- if (overrides?.input?.onBlur) overrides.input.onBlur(e);
256
- }}
257
253
  aria-disabled={disabled}
258
254
  data-status={disabled ? 'disabled' : (status || 'default')}
259
255
  className={clsx(
@@ -324,10 +320,10 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
324
320
  value={query}
325
321
  data-selected={false}
326
322
  className={clsx(
327
- overrides?.option?.className,
323
+ overrides?.customValueOption?.className,
328
324
  styles.option,
329
325
  )}
330
- {...overrides?.option}
326
+ {...overrides?.customValueOption}
331
327
  >
332
328
  <Text as="span" kind="paragraphSmall">
333
329
  {customValueString.replace('%v', query)}
@@ -423,6 +423,11 @@ $panelAnimationDelay: var(--pte-animations-duration-fast);
423
423
 
424
424
  .bottomPanelSpacer {
425
425
  padding: 20px;
426
+
427
+ &.noPadding {
428
+ padding: 0;
429
+ }
430
+
426
431
  opacity: 0;
427
432
  pointer-events: none;
428
433
  }
@@ -150,7 +150,7 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
150
150
  */
151
151
  export const Drawer = <T extends string[] | readonly string[] = string[]>({
152
152
  isOpen = false,
153
- onClose = () => {},
153
+ onClose = () => { },
154
154
  title,
155
155
  hideTitle = false,
156
156
  hideCloseButton = false,
@@ -383,7 +383,7 @@ export const Drawer = <T extends string[] | readonly string[] = string[]>({
383
383
  </div>
384
384
  {bottomPanel && (
385
385
  <>
386
- <div tabIndex={-1} aria-hidden="true" className={clsx(styles.bottomPanelSpacer, overrides?.bottomPanelSpacer?.className)}>
386
+ <div tabIndex={-1} aria-hidden="true" className={clsx(styles.bottomPanelSpacer, { [styles.noPadding]: !bottomPanelPadding }, overrides?.bottomPanelSpacer?.className)}>
387
387
  {bottomPanel}
388
388
  </div>
389
389
  <div className={clsx(styles.bottomPanel, overrides?.bottomPanel?.className)}>
@@ -1 +1,22 @@
1
- .container {}
1
+ .container {
2
+ position: relative;
3
+ }
4
+
5
+ .glareWrapper {
6
+ position: absolute;
7
+ top: 0;
8
+ left: 0;
9
+ width: 100%;
10
+ height: 100%;
11
+ overflow: hidden;
12
+ -webkit-mask-image: -webkit-radial-gradient(white, black);
13
+ pointer-events: none;
14
+ }
15
+
16
+ .glare {
17
+ position: absolute;
18
+ top: 50%;
19
+ left: 50%;
20
+ transform-origin: 0% 0%;
21
+ pointer-events: none;
22
+ }
@@ -1,26 +1,116 @@
1
1
  'use client';
2
2
 
3
- import type { FC, ReactNode } from 'react';
4
- import RPTilt from 'react-parallax-tilt';
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import type {
10
+ CSSProperties,
11
+ FC,
12
+ MouseEvent as ReactMouseEvent,
13
+ ReactNode,
14
+ TouchEvent as ReactTouchEvent,
15
+ } from 'react';
16
+ import { clsx } from 'clsx';
5
17
  import styles from './Tilt.module.scss';
6
18
 
19
+ type GlarePosition = 'top' | 'right' | 'bottom' | 'left' | 'all';
20
+
21
+ export type OnMoveParams = {
22
+ tiltAngleX: number;
23
+ tiltAngleY: number;
24
+ tiltAngleXPercentage: number;
25
+ tiltAngleYPercentage: number;
26
+ glareAngle: number;
27
+ glareOpacity: number;
28
+ };
29
+
7
30
  export type TiltProps = {
8
- /**
9
- * Disables the tilt effect.
10
- * @default false
11
- */
31
+ /** Disables the tilt effect. @default false */
12
32
  disableTilt?: boolean;
13
- /** The contents of the Tilt. */
14
33
  children?: ReactNode | ReactNode[];
15
- /** The class names to apply to this element. */
16
34
  className?: string;
17
- } & RPTilt['props'];
35
+ style?: CSSProperties;
36
+ /** Scale on hover (1.05 = 105%). @default 1.05 */
37
+ scale?: number;
38
+ /** Max tilt on X axis in degrees. @default 12.5 */
39
+ tiltMaxAngleX?: number;
40
+ /** Max tilt on Y axis in degrees. @default 12.5 */
41
+ tiltMaxAngleY?: number;
42
+ /** Manual tilt on X axis in degrees. Overrides mouse input when non-null. */
43
+ tiltAngleXManual?: number | null;
44
+ /** Manual tilt on Y axis in degrees. Overrides mouse input when non-null. */
45
+ tiltAngleYManual?: number | null;
46
+ /** CSS perspective distance in px. @default 1000 */
47
+ perspective?: number;
48
+ /** Transition speed in ms for enter/leave. @default 400 */
49
+ transitionSpeed?: number;
50
+ /** Transition easing function. @default 'cubic-bezier(.03,.98,.52,.99)' */
51
+ transitionEasing?: string;
52
+ /** Reset tilt on mouse leave. @default true */
53
+ reset?: boolean;
54
+ /** Enable glare overlay. @default true */
55
+ glareEnable?: boolean;
56
+ /** Max glare opacity (0-1). @default 0.5 */
57
+ glareMaxOpacity?: number;
58
+ /** Glare gradient color. @default '#ffffff' */
59
+ glareColor?: string;
60
+ /** Glare gradient origin position. @default 'bottom' */
61
+ glarePosition?: GlarePosition;
62
+ /** Reverse glare direction. @default false */
63
+ glareReverse?: boolean;
64
+ /** CSS border-radius for glare wrapper. Derived from style.borderRadius by default. */
65
+ glareBorderRadius?: string;
66
+ onEnter?: (params: { event: ReactMouseEvent | ReactTouchEvent }) => void;
67
+ onLeave?: (params: { event: ReactMouseEvent | ReactTouchEvent }) => void;
68
+ onMove?: (params: OnMoveParams) => void;
69
+ };
70
+
71
+ type TiltState = {
72
+ tiltAngleX: number;
73
+ tiltAngleY: number;
74
+ currentScale: number;
75
+ glareAngle: number;
76
+ glareOpacity: number;
77
+ transitioning: boolean;
78
+ };
79
+
80
+ const INITIAL_STATE: TiltState = {
81
+ tiltAngleX: 0,
82
+ tiltAngleY: 0,
83
+ currentScale: 1,
84
+ glareAngle: 0,
85
+ glareOpacity: 0,
86
+ transitioning: false,
87
+ };
88
+
89
+ const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
90
+
91
+ function computeGlareOpacity(
92
+ xPct: number,
93
+ yPct: number,
94
+ position: GlarePosition,
95
+ reverse: boolean,
96
+ maxOpacity: number,
97
+ ): number {
98
+ const g = reverse ? -1 : 1;
99
+ let raw = 0;
100
+ switch (position) {
101
+ case 'top': raw = -xPct * g; break;
102
+ case 'right': raw = yPct * g; break;
103
+ case 'left': raw = -yPct * g; break;
104
+ case 'all': raw = Math.hypot(xPct, yPct); break;
105
+ case 'bottom':
106
+ default: raw = xPct * g; break;
107
+ }
108
+ return clamp(raw, 0, 100) * maxOpacity / 100;
109
+ }
18
110
 
19
111
  /**
20
112
  * Tilt components allow you to add a parallax tilt effect to any component.
21
113
  *
22
- * Based on [react-parallax-tilt](https://github.com/mkosir/react-parallax-tilt), customized with our preferred defaults.
23
- *
24
114
  * > Card components include a Tilt component by default, by setting the `tilt` prop to `true`. You can override any of the underlying Tilt props by passing them to the Card component's `overrides.tilt` prop.
25
115
  *
26
116
  * <hr />
@@ -37,29 +127,215 @@ export const Tilt: FC<TiltProps> = ({
37
127
  scale = 1.05,
38
128
  tiltMaxAngleX = 12.5,
39
129
  tiltMaxAngleY = 12.5,
40
- glareEnable = true,
41
- glareMaxOpacity = 0.5,
42
- style = {},
43
130
  tiltAngleXManual = null,
44
131
  tiltAngleYManual = null,
45
132
  perspective = 1000,
133
+ transitionSpeed = 400,
134
+ transitionEasing = 'cubic-bezier(.03,.98,.52,.99)',
135
+ reset = true,
136
+ glareEnable = true,
137
+ glareMaxOpacity = 0.5,
138
+ glareColor = '#ffffff',
139
+ glarePosition = 'bottom',
140
+ glareReverse = false,
141
+ glareBorderRadius,
142
+ style = {},
143
+ className,
46
144
  children,
47
- ...props
48
- }) => (
49
- <RPTilt
50
- scale={disableTilt ? 1 : scale}
51
- tiltMaxAngleX={disableTilt ? 0 : tiltMaxAngleX}
52
- tiltMaxAngleY={disableTilt ? 0 : tiltMaxAngleY}
53
- glareEnable={!disableTilt && glareEnable}
54
- glareMaxOpacity={disableTilt ? 0 : glareMaxOpacity}
55
- style={style}
56
- glareBorderRadius={`calc(${style?.borderRadius || '1px'} - 1px)`}
57
- tiltAngleXManual={disableTilt ? 0 : tiltAngleXManual}
58
- tiltAngleYManual={disableTilt ? 0 : tiltAngleYManual}
59
- perspective={perspective}
60
- {...props}
61
- className={`${styles.container} ${props.className}`}
62
- >
63
- {children}
64
- </RPTilt>
65
- );
145
+ onEnter,
146
+ onLeave,
147
+ onMove,
148
+ }) => {
149
+ const containerRef = useRef<HTMLDivElement>(null);
150
+ const rafID = useRef<number | null>(null);
151
+ const rectRef = useRef<DOMRect | null>(null);
152
+ const pendingRef = useRef<TiltState | null>(null);
153
+
154
+ const [tilt, setTilt] = useState<TiltState>(INITIAL_STATE);
155
+
156
+ const effectiveScale = disableTilt ? 1 : scale;
157
+ const effectiveMaxX = disableTilt ? 0 : tiltMaxAngleX;
158
+ const effectiveMaxY = disableTilt ? 0 : tiltMaxAngleY;
159
+ const effectiveGlare = !disableTilt && glareEnable;
160
+ const effectiveGlareMax = disableTilt ? 0 : glareMaxOpacity;
161
+ const effectiveManualX = disableTilt ? 0 : tiltAngleXManual;
162
+ const effectiveManualY = disableTilt ? 0 : tiltAngleYManual;
163
+
164
+ const computedGlareBorderRadius = glareBorderRadius ?? `calc(${style?.borderRadius || '1px'} - 1px)`;
165
+ const isManual = effectiveManualX !== null || effectiveManualY !== null;
166
+
167
+ // Cancel any pending rAF on unmount
168
+ useEffect(() => () => {
169
+ if (rafID.current !== null) cancelAnimationFrame(rafID.current);
170
+ }, []);
171
+
172
+ const flushToState = useCallback(() => {
173
+ if (!pendingRef.current) return;
174
+ const next = pendingRef.current;
175
+ pendingRef.current = null;
176
+ setTilt(next);
177
+ }, []);
178
+
179
+ const scheduleUpdate = useCallback((next: TiltState) => {
180
+ pendingRef.current = next;
181
+ if (rafID.current !== null) cancelAnimationFrame(rafID.current);
182
+ rafID.current = requestAnimationFrame(flushToState);
183
+ }, [flushToState]);
184
+
185
+ const computeTilt = useCallback((pageX: number, pageY: number): TiltState => {
186
+ const rect = rectRef.current;
187
+ if (!rect) return { ...INITIAL_STATE, currentScale: effectiveScale };
188
+
189
+ const xPct = clamp(((pageX - rect.left) / rect.width) * 200 - 100, -100, 100);
190
+ const yPct = clamp(((pageY - rect.top) / rect.height) * 200 - 100, -100, 100);
191
+
192
+ const angleX = (effectiveManualX !== null) ? effectiveManualX : (yPct * effectiveMaxX / 100);
193
+ const angleY = (effectiveManualY !== null) ? effectiveManualY : (xPct * effectiveMaxY / 100 * -1);
194
+
195
+ const glareAngle = xPct ? Math.atan2(yPct, -xPct) * (180 / Math.PI) : 0;
196
+ const glareOpacity = effectiveGlare
197
+ ? computeGlareOpacity(xPct, yPct, glarePosition, glareReverse, effectiveGlareMax)
198
+ : 0;
199
+
200
+ return {
201
+ tiltAngleX: clamp(angleX, -90, 90),
202
+ tiltAngleY: clamp(angleY, -90, 90),
203
+ currentScale: effectiveScale,
204
+ glareAngle,
205
+ glareOpacity,
206
+ transitioning: false,
207
+ };
208
+ }, [effectiveScale, effectiveMaxX, effectiveMaxY, effectiveManualX, effectiveManualY, effectiveGlare, effectiveGlareMax, glarePosition, glareReverse]);
209
+
210
+ const handleMouseEnter = useCallback((e: ReactMouseEvent<HTMLDivElement>) => {
211
+ if (!containerRef.current) return;
212
+ rectRef.current = containerRef.current.getBoundingClientRect();
213
+
214
+ const next = computeTilt(e.pageX, e.pageY);
215
+ setTilt({ ...next, transitioning: true });
216
+ onEnter?.({ event: e });
217
+ }, [computeTilt, onEnter]);
218
+
219
+ const handleMouseMove = useCallback((e: ReactMouseEvent<HTMLDivElement>) => {
220
+ if (!containerRef.current || !rectRef.current) return;
221
+ if (effectiveManualX !== null || effectiveManualY !== null) return;
222
+
223
+ const next = computeTilt(e.pageX, e.pageY);
224
+ scheduleUpdate(next);
225
+
226
+ if (onMove) {
227
+ onMove({
228
+ tiltAngleX: next.tiltAngleX,
229
+ tiltAngleY: next.tiltAngleY,
230
+ tiltAngleXPercentage: effectiveMaxX ? (next.tiltAngleX / effectiveMaxX) * 100 : 0,
231
+ tiltAngleYPercentage: effectiveMaxY ? (next.tiltAngleY / effectiveMaxY) * 100 : 0,
232
+ glareAngle: next.glareAngle,
233
+ glareOpacity: next.glareOpacity,
234
+ });
235
+ }
236
+ }, [computeTilt, scheduleUpdate, onMove, effectiveManualX, effectiveManualY, effectiveMaxX, effectiveMaxY]);
237
+
238
+ const handleMouseLeave = useCallback((e: ReactMouseEvent<HTMLDivElement>) => {
239
+ if (!containerRef.current) return;
240
+ rectRef.current = null;
241
+
242
+ if (reset) {
243
+ setTilt({ ...INITIAL_STATE, transitioning: true });
244
+ }
245
+ onLeave?.({ event: e });
246
+ }, [reset, onLeave]);
247
+
248
+ const handleTouchStart = useCallback((e: ReactTouchEvent<HTMLDivElement>) => {
249
+ if (!containerRef.current || !e.touches[0]) return;
250
+ rectRef.current = containerRef.current.getBoundingClientRect();
251
+
252
+ const touch = e.touches[0];
253
+ const next = computeTilt(touch.pageX, touch.pageY);
254
+ setTilt({ ...next, transitioning: true });
255
+ onEnter?.({ event: e });
256
+ }, [computeTilt, onEnter]);
257
+
258
+ const handleTouchMove = useCallback((e: ReactTouchEvent<HTMLDivElement>) => {
259
+ if (!containerRef.current || !rectRef.current || !e.touches[0]) return;
260
+ if (effectiveManualX !== null || effectiveManualY !== null) return;
261
+
262
+ const touch = e.touches[0];
263
+ const next = computeTilt(touch.pageX, touch.pageY);
264
+ scheduleUpdate(next);
265
+ }, [computeTilt, scheduleUpdate, effectiveManualX, effectiveManualY]);
266
+
267
+ const handleTouchEnd = useCallback((e: ReactTouchEvent<HTMLDivElement>) => {
268
+ if (!containerRef.current) return;
269
+ rectRef.current = null;
270
+
271
+ if (reset) {
272
+ setTilt({ ...INITIAL_STATE, transitioning: true });
273
+ }
274
+ onLeave?.({ event: e });
275
+ }, [reset, onLeave]);
276
+
277
+ // Compute glare element size (diagonal of container)
278
+ const [glareSize, setGlareSize] = useState(0);
279
+ useEffect(() => {
280
+ if (!effectiveGlare || !containerRef.current) {
281
+ return undefined;
282
+ }
283
+ const el = containerRef.current;
284
+ const observer = new ResizeObserver(([entry]) => {
285
+ if (!entry) return;
286
+ const { width, height } = entry.contentRect;
287
+ setGlareSize(Math.sqrt(width ** 2 + height ** 2));
288
+ });
289
+ observer.observe(el);
290
+ return () => observer.disconnect();
291
+ }, [effectiveGlare]);
292
+
293
+ // Apply manual angle overrides at render time
294
+ const finalAngleX = isManual ? (effectiveManualX ?? 0) : tilt.tiltAngleX;
295
+ const finalAngleY = isManual ? (effectiveManualY ?? 0) : tilt.tiltAngleY;
296
+ const finalScale = tilt.currentScale;
297
+
298
+ const transform = `perspective(${perspective}px) rotateX(${finalAngleX}deg) rotateY(${finalAngleY}deg) scale3d(${finalScale},${finalScale},${finalScale})`;
299
+
300
+ const transition = tilt.transitioning || isManual
301
+ ? `transform ${transitionSpeed}ms ${transitionEasing}`
302
+ : undefined;
303
+
304
+ return (
305
+ <div
306
+ ref={containerRef}
307
+ onMouseEnter={handleMouseEnter}
308
+ onMouseMove={handleMouseMove}
309
+ onMouseLeave={handleMouseLeave}
310
+ onTouchStart={handleTouchStart}
311
+ onTouchMove={handleTouchMove}
312
+ onTouchEnd={handleTouchEnd}
313
+ className={clsx(styles.container, className)}
314
+ style={{
315
+ ...style,
316
+ transform,
317
+ transition,
318
+ willChange: tilt.transitioning || finalScale !== 1 || finalAngleX !== 0 || finalAngleY !== 0 ? 'transform' : undefined,
319
+ }}
320
+ >
321
+ {children}
322
+ {effectiveGlare && (
323
+ <div
324
+ className={styles.glareWrapper}
325
+ style={{ borderRadius: computedGlareBorderRadius }}
326
+ >
327
+ <div
328
+ className={styles.glare}
329
+ style={{
330
+ width: glareSize,
331
+ height: glareSize,
332
+ transform: `rotate(${tilt.glareAngle}deg) translate(-50%, -50%)`,
333
+ opacity: tilt.glareOpacity,
334
+ background: `linear-gradient(0deg, rgba(255,255,255,0) 0%, ${glareColor} 100%)`,
335
+ }}
336
+ />
337
+ </div>
338
+ )}
339
+ </div>
340
+ );
341
+ };