@astryxdesign/core 0.4.1 → 0.4.2-canary.01592f7
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 +55 -0
- package/dist/Avatar/Avatar.d.ts +4 -1
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +19 -23
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +9 -7
- package/dist/Chat/ChatMessage.d.ts +7 -0
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.d.ts +13 -1
- package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.js +19 -1
- package/dist/Chat/ChatTokenizedText.js +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +5 -0
- package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteFooter.js +5 -3
- package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +96 -35
- package/dist/ComplexSelector/index.d.ts +2 -2
- package/dist/ComplexSelector/index.d.ts.map +1 -1
- package/dist/ComplexSelector/index.js +1 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +2 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -15
- package/dist/HoverCard/HoverCard.d.ts +1 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +5 -13
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +6 -5
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +7 -2
- package/dist/Layer/layerHost.d.ts +24 -0
- package/dist/Layer/layerHost.d.ts.map +1 -0
- package/dist/Layer/layerHost.js +79 -0
- package/dist/Layer/useLayer.d.ts +14 -6
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +158 -30
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +55 -12
- package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
- package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
- package/dist/NavItem/navItemStyles.stylex.js +11 -5
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +5 -0
- package/dist/SideNav/SideNav.d.ts +7 -9
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +32 -5
- package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +39 -15
- package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
- package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseContext.js +16 -2
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +89 -32
- package/dist/SideNav/SideNavItem.d.ts +7 -2
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +119 -75
- package/dist/SideNav/SideNavSection.d.ts.map +1 -1
- package/dist/SideNav/SideNavSection.js +7 -16
- package/dist/SideNav/index.d.ts +1 -1
- package/dist/SideNav/index.d.ts.map +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +56 -15
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -0
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +5 -0
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +14 -6
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +25 -93
- package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +10 -5
- package/dist/astryx.css +40 -8
- package/dist/astryx.umd.js +50 -50
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/containerReveal.stylex.d.ts +71 -4
- package/dist/hooks/containerReveal.stylex.d.ts.map +1 -1
- package/dist/hooks/containerReveal.stylex.js +57 -5
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useContainerReveal.d.ts +60 -3
- package/dist/hooks/useContainerReveal.d.ts.map +1 -1
- package/dist/hooks/useContainerReveal.js +27 -5
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +16 -2
- package/dist/hooks/useMenuHover.d.ts +50 -5
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +171 -51
- package/dist/theme/defineTheme.d.ts +16 -5
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +44 -48
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +7 -0
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +1 -0
- package/dist/theme/expandMotionScale.d.ts +1 -0
- package/dist/theme/expandMotionScale.d.ts.map +1 -1
- package/dist/theme/expandMotionScale.js +1 -0
- package/dist/theme/expandRadiusScale.d.ts +1 -0
- package/dist/theme/expandRadiusScale.d.ts.map +1 -1
- package/dist/theme/expandRadiusScale.js +1 -0
- package/dist/theme/expandTypeScale.d.ts +1 -0
- package/dist/theme/expandTypeScale.d.ts.map +1 -1
- package/dist/theme/expandTypeScale.js +1 -0
- package/dist/theme/mergeComponents.d.ts +20 -0
- package/dist/theme/mergeComponents.d.ts.map +1 -0
- package/dist/theme/mergeComponents.js +56 -0
- package/dist/theme/onMediaTokens.d.ts +6 -1
- package/dist/theme/onMediaTokens.d.ts.map +1 -1
- package/dist/theme/onMediaTokens.js +11 -3
- package/dist/theme/tokens.stylex.d.ts +3 -1
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +3 -1
- package/locales/en.json +84 -0
- package/locales/pseudo.json +63 -0
- package/package.json +3 -3
- package/src/Avatar/Avatar.doc.mjs +5 -2
- package/src/Avatar/Avatar.test.tsx +51 -0
- package/src/Avatar/Avatar.tsx +30 -20
- package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +14 -0
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -2
- package/src/Button/Button.tsx +5 -3
- package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
- package/src/Card/Card.doc.mjs +2 -0
- package/src/Chat/Chat.doc.mjs +4 -1
- package/src/Chat/ChatMessage.doc.mjs +3 -3
- package/src/Chat/ChatMessage.tsx +7 -0
- package/src/Chat/ChatMessageBubble.doc.mjs +7 -0
- package/src/Chat/ChatMessageBubble.test.tsx +95 -6
- package/src/Chat/ChatMessageBubble.tsx +25 -0
- package/src/Chat/ChatTokenizedText.tsx +1 -1
- package/src/CheckboxInput/CheckboxInput.tsx +20 -0
- package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
- package/src/CommandPalette/CommandPaletteFooter.tsx +6 -3
- package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
- package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
- package/src/ComplexSelector/ComplexSelector.tsx +153 -32
- package/src/ComplexSelector/index.ts +3 -1
- package/src/DateTimeInput/DateTimeInput.tsx +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +72 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +24 -20
- package/src/HoverCard/HoverCard.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.test.tsx +283 -55
- package/src/HoverCard/HoverCard.tsx +5 -13
- package/src/HoverCard/useHoverCard.tsx +8 -11
- package/src/InputGroup/groupStyles.ts +7 -8
- package/src/Item/Item.doc.mjs +4 -0
- package/src/Layer/layerHost.test.ts +99 -0
- package/src/Layer/layerHost.ts +141 -0
- package/src/Layer/useLayer.doc.mjs +14 -4
- package/src/Layer/useLayer.test.tsx +332 -7
- package/src/Layer/useLayer.tsx +235 -36
- package/src/Markdown/parser.test.ts +53 -0
- package/src/Markdown/parser.ts +53 -12
- package/src/MobileNav/MobileNav.doc.mjs +7 -0
- package/src/NavItem/navItemStyles.stylex.ts +31 -5
- package/src/RadioList/RadioListItem.tsx +20 -0
- package/src/SideNav/SideNav.doc.mjs +13 -4
- package/src/SideNav/SideNav.test.tsx +858 -2
- package/src/SideNav/SideNav.tsx +37 -16
- package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
- package/src/SideNav/SideNavCollapseButton.tsx +67 -15
- package/src/SideNav/SideNavCollapseContext.ts +29 -9
- package/src/SideNav/SideNavHeading.tsx +53 -25
- package/src/SideNav/SideNavItem.doc.mjs +13 -0
- package/src/SideNav/SideNavItem.tsx +97 -84
- package/src/SideNav/SideNavSection.tsx +6 -20
- package/src/SideNav/index.ts +2 -0
- package/src/Slider/Slider.test.tsx +69 -33
- package/src/Slider/Slider.tsx +65 -13
- package/src/Switch/Switch.tsx +20 -0
- package/src/TabList/TabList.doc.mjs +3 -0
- package/src/Text/Text.doc.mjs +1 -1
- package/src/Thumbnail/Thumbnail.doc.mjs +3 -0
- package/src/Thumbnail/Thumbnail.tsx +10 -0
- package/src/Timestamp/Timestamp.test.tsx +40 -22
- package/src/Toolbar/Toolbar.test.tsx +14 -14
- package/src/TopNav/TopNav.test.tsx +86 -1
- package/src/TopNav/TopNavHeading.tsx +21 -14
- package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
- package/src/TopNav/TopNavMegaMenu.tsx +29 -105
- package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
- package/src/TopNav/TopNavMenu.test.tsx +41 -0
- package/src/TopNav/TopNavMenu.tsx +14 -4
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/hooks/containerReveal.stylex.ts +140 -9
- package/src/hooks/index.ts +6 -1
- package/src/hooks/useContainerReveal.doc.mjs +11 -5
- package/src/hooks/useContainerReveal.test.tsx +70 -3
- package/src/hooks/useContainerReveal.ts +86 -7
- package/src/hooks/useFocusTrap.test.tsx +16 -0
- package/src/hooks/useFocusTrap.ts +20 -2
- package/src/hooks/useMenuHover.test.tsx +364 -0
- package/src/hooks/useMenuHover.ts +243 -47
- package/src/theme/defineTheme.test.ts +127 -0
- package/src/theme/defineTheme.ts +57 -51
- package/src/theme/derivedVarRegistry.test.ts +78 -12
- package/src/theme/derivedVarRegistry.ts +4 -0
- package/src/theme/expandColorScale.ts +1 -0
- package/src/theme/expandMotionScale.ts +1 -0
- package/src/theme/expandRadiusScale.ts +1 -0
- package/src/theme/expandTypeScale.ts +1 -0
- package/src/theme/mergeComponents.ts +59 -0
- package/src/theme/onMediaTokens.ts +9 -2
- package/src/theme/themingTargets.test.ts +152 -26
- package/src/theme/tokens.stylex.ts +3 -1
- package/src/theme/tokens.test.ts +12 -0
- package/src/theme/useTheme.test.tsx +18 -0
package/src/Layer/useLayer.tsx
CHANGED
|
@@ -25,8 +25,10 @@ import React, {
|
|
|
25
25
|
} from 'react';
|
|
26
26
|
import * as stylex from '@stylexjs/stylex';
|
|
27
27
|
import type {StyleXStyles} from '@stylexjs/stylex';
|
|
28
|
+
import {createPortal} from 'react-dom';
|
|
28
29
|
import {addAnchorName, removeAnchorName} from './anchorName';
|
|
29
|
-
import {
|
|
30
|
+
import {resolveLayerPortalTarget} from './layerHost';
|
|
31
|
+
import {typeScaleVars, typographyVars} from '../theme/tokens.stylex';
|
|
30
32
|
|
|
31
33
|
const styles = stylex.create({
|
|
32
34
|
// Base reset for all layers
|
|
@@ -42,7 +44,12 @@ const styles = stylex.create({
|
|
|
42
44
|
borderWidth: 0,
|
|
43
45
|
borderStyle: 'none',
|
|
44
46
|
overflow: 'visible',
|
|
47
|
+
// A layer is hosted wherever its trigger happens to sit, so type that is
|
|
48
|
+
// inherited rather than declared makes the same component render at a
|
|
49
|
+
// different size in different callers.
|
|
45
50
|
fontFamily: typographyVars['--font-family-body'],
|
|
51
|
+
fontSize: typeScaleVars['--text-body-size'],
|
|
52
|
+
lineHeight: typeScaleVars['--text-body-leading'],
|
|
46
53
|
// Override browser default [popover] background (canvas color)
|
|
47
54
|
backgroundColor: 'transparent',
|
|
48
55
|
},
|
|
@@ -149,12 +156,11 @@ export interface ContextRenderProps {
|
|
|
149
156
|
/**
|
|
150
157
|
* HTML tag to render the popover container as.
|
|
151
158
|
*
|
|
152
|
-
* Defaults to `'div'`.
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* the same on either tag.
|
|
159
|
+
* Defaults to `'div'`. Context layers render an inert `<template>` marker at
|
|
160
|
+
* the JSX position. The marker's parent is checked before the requested
|
|
161
|
+
* container mounts there or portals outside ancestors that cannot safely
|
|
162
|
+
* contain it. The marker remains available to detect a new parent if the
|
|
163
|
+
* render call moves. With `lazyMount`, the first check waits until `show()`.
|
|
158
164
|
*
|
|
159
165
|
* @default 'div'
|
|
160
166
|
*/
|
|
@@ -222,6 +228,15 @@ interface BaseLayerOptions {
|
|
|
222
228
|
*/
|
|
223
229
|
export interface ContextLayerOptions extends BaseLayerOptions {
|
|
224
230
|
mode: 'context';
|
|
231
|
+
/**
|
|
232
|
+
* Defer mounting the final layer and resolving its inline/portal position
|
|
233
|
+
* until `show()` is requested. Hiding unmounts it while the inert marker
|
|
234
|
+
* remains at the JSX position. Use this when rich content must never enter
|
|
235
|
+
* an unsafe ancestor, even briefly, and does not need to exist while closed.
|
|
236
|
+
*
|
|
237
|
+
* @default false
|
|
238
|
+
*/
|
|
239
|
+
lazyMount?: boolean;
|
|
225
240
|
}
|
|
226
241
|
|
|
227
242
|
/**
|
|
@@ -314,16 +329,51 @@ function toCssLength(value: number | string): string {
|
|
|
314
329
|
return typeof value === 'number' ? `${value}px` : value;
|
|
315
330
|
}
|
|
316
331
|
|
|
332
|
+
interface ContextLayerMount {
|
|
333
|
+
/** Null means the marker's parent is safe and the layer stays inline. */
|
|
334
|
+
portalTarget: HTMLElement | null;
|
|
335
|
+
/** Logical writing context lost when moving outside an unsafe ancestor. */
|
|
336
|
+
portalStyle: React.CSSProperties;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function readPortalWritingContext(
|
|
340
|
+
element: HTMLElement,
|
|
341
|
+
portalTarget: HTMLElement,
|
|
342
|
+
): React.CSSProperties {
|
|
343
|
+
const view = element.ownerDocument.defaultView;
|
|
344
|
+
if (!view) {
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
const sourceStyle = view.getComputedStyle(element);
|
|
348
|
+
const targetStyle = view.getComputedStyle(portalTarget);
|
|
349
|
+
|
|
350
|
+
// Do not snapshot custom properties here. The portal target is the closest
|
|
351
|
+
// safe ancestor, so theme variables continue to inherit and update there.
|
|
352
|
+
// These two properties can be set on the unsafe chain itself and directly
|
|
353
|
+
// affect the logical anchor-positioning keywords used by the layer. Only
|
|
354
|
+
// override values the portal would actually lose; matching values should
|
|
355
|
+
// keep inheriting from the target so later direction changes remain live.
|
|
356
|
+
return {
|
|
357
|
+
...(sourceStyle.direction !== targetStyle.direction && {
|
|
358
|
+
direction: sourceStyle.direction as React.CSSProperties['direction'],
|
|
359
|
+
}),
|
|
360
|
+
...(sourceStyle.writingMode !== targetStyle.writingMode && {
|
|
361
|
+
writingMode:
|
|
362
|
+
sourceStyle.writingMode as React.CSSProperties['writingMode'],
|
|
363
|
+
}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
317
367
|
/**
|
|
318
368
|
* Map logical placement/alignment to a CSS position-area value.
|
|
319
369
|
*
|
|
320
370
|
* Uses the self-* logical keyword family: the inline axis resolves against
|
|
321
|
-
* the popover's own
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
371
|
+
* the popover's own direction (inherited inline or preserved when portaled),
|
|
372
|
+
* so it mirrors in RTL without placement-specific JS. The block axis is
|
|
373
|
+
* direction-neutral but must come from the same keyword family — mixing
|
|
374
|
+
* physical `top` with `self-inline-*` produces an invalid position-area
|
|
375
|
+
* (computes to `none`, which pins the popover to the viewport corner because
|
|
376
|
+
* styles.base zeroes the UA margins).
|
|
327
377
|
*
|
|
328
378
|
* Note the plain logical family (`inline-start`, no `self-`) is NOT a
|
|
329
379
|
* substitute: it resolves against the containing block — the page root for
|
|
@@ -406,39 +456,122 @@ export function useLayer(
|
|
|
406
456
|
options: ContextLayerOptions | FixedLayerOptions,
|
|
407
457
|
): ContextLayerReturn | FixedLayerReturn {
|
|
408
458
|
const {mode, onShow, onHide, lightDismiss = false} = options;
|
|
459
|
+
const lazyMount = mode === 'context' ? (options.lazyMount ?? false) : false;
|
|
409
460
|
const id = useId();
|
|
410
461
|
const anchorId = `--astryx-layer-${id.replace(/:/g, '')}`;
|
|
411
462
|
|
|
412
463
|
const [isOpen, setIsOpen] = useState(false);
|
|
413
464
|
const popoverRef = useRef<HTMLElement | null>(null);
|
|
465
|
+
// The DOM element on which the current logical open state was applied.
|
|
466
|
+
// A portal target change replaces the popover element; retaining the old
|
|
467
|
+
// reference lets the ref callback recognize and reopen its replacement.
|
|
468
|
+
const openedPopoverRef = useRef<HTMLElement | null>(null);
|
|
414
469
|
const triggerRef = useRef<HTMLElement | null>(null);
|
|
470
|
+
// Context layers place a persistent inert marker at their real JSX position.
|
|
471
|
+
// Its parent tells us whether the final layer can stay inline or needs a
|
|
472
|
+
// corrective portal, including after the render call moves.
|
|
473
|
+
const sentinelRef = useRef<HTMLTemplateElement | null>(null);
|
|
474
|
+
const contextMountRef = useRef<ContextLayerMount | null>(null);
|
|
475
|
+
const [contextMount, setContextMount] = useState<ContextLayerMount | null>(
|
|
476
|
+
null,
|
|
477
|
+
);
|
|
478
|
+
// A show() that arrives before the final layer mounts is replayed when its
|
|
479
|
+
// popover ref attaches.
|
|
480
|
+
const pendingShowRef = useRef(false);
|
|
415
481
|
|
|
416
482
|
// Ref mirrors isOpen for synchronous reads inside show/hide.
|
|
417
483
|
// State drives re-renders; the ref lets the imperative calls avoid
|
|
418
484
|
// stale-closure reads of the previous isOpen value.
|
|
419
485
|
const isOpenRef = useRef(false);
|
|
420
486
|
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
487
|
+
const showPopoverElement = useCallback((popover: HTMLElement) => {
|
|
488
|
+
// Finding infra-4: the Popover API is unsupported on Safari <17 and
|
|
489
|
+
// Firefox <125. On those browsers `showPopover` does not exist, so fall
|
|
490
|
+
// back to plain visibility instead of throwing.
|
|
491
|
+
if (typeof popover.showPopover === 'function') {
|
|
492
|
+
// The trigger is passed as the popover's invoker `source`: a layer
|
|
493
|
+
// hosted away from its trigger then still takes its sequential focus
|
|
494
|
+
// order (and its popover nesting) from the trigger rather than from its
|
|
495
|
+
// own DOM position. Browsers without the option ignore it.
|
|
496
|
+
popover.showPopover({source: triggerRef.current ?? undefined});
|
|
497
|
+
} else {
|
|
498
|
+
popover.style.display = 'block';
|
|
499
|
+
}
|
|
500
|
+
openedPopoverRef.current = popover;
|
|
501
|
+
}, []);
|
|
502
|
+
|
|
503
|
+
const isCurrentContextPopover = useCallback(
|
|
504
|
+
(popover: HTMLElement): boolean => {
|
|
505
|
+
if (mode !== 'context') {
|
|
506
|
+
return true;
|
|
434
507
|
}
|
|
508
|
+
const mount = contextMountRef.current;
|
|
509
|
+
if (mount === null) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
const expectedParent =
|
|
513
|
+
mount.portalTarget ?? sentinelRef.current?.parentElement ?? null;
|
|
514
|
+
return popover.parentElement === expectedParent;
|
|
515
|
+
},
|
|
516
|
+
[mode],
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const requestContextMount = useCallback(() => {
|
|
520
|
+
if (mode !== 'context') {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const sentinel = sentinelRef.current;
|
|
525
|
+
const inlineParent = sentinel?.parentElement ?? null;
|
|
526
|
+
if (!sentinel || !inlineParent) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const portalTarget = resolveLayerPortalTarget(inlineParent);
|
|
531
|
+
const mount: ContextLayerMount = {
|
|
532
|
+
portalTarget,
|
|
533
|
+
portalStyle: portalTarget
|
|
534
|
+
? readPortalWritingContext(sentinel, portalTarget)
|
|
535
|
+
: {},
|
|
536
|
+
};
|
|
537
|
+
contextMountRef.current = mount;
|
|
538
|
+
setContextMount(mount);
|
|
539
|
+
}, [mode]);
|
|
540
|
+
|
|
541
|
+
const clearContextMount = useCallback(() => {
|
|
542
|
+
if (mode !== 'context' || !lazyMount) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
contextMountRef.current = null;
|
|
546
|
+
setContextMount(null);
|
|
547
|
+
}, [mode, lazyMount]);
|
|
548
|
+
|
|
549
|
+
const show = useCallback(() => {
|
|
550
|
+
// A context popover left over until React commits a previous hide must not
|
|
551
|
+
// be reopened. The synchronous mount ref is the source of truth.
|
|
552
|
+
const candidate = popoverRef.current;
|
|
553
|
+
const popover =
|
|
554
|
+
candidate && isCurrentContextPopover(candidate) ? candidate : null;
|
|
555
|
+
if (!popover) {
|
|
556
|
+
pendingShowRef.current = true;
|
|
557
|
+
requestContextMount();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!isOpenRef.current) {
|
|
561
|
+
showPopoverElement(popover);
|
|
435
562
|
isOpenRef.current = true;
|
|
436
563
|
setIsOpen(true);
|
|
437
564
|
onShow?.();
|
|
438
565
|
}
|
|
439
|
-
}, [
|
|
566
|
+
}, [
|
|
567
|
+
onShow,
|
|
568
|
+
requestContextMount,
|
|
569
|
+
showPopoverElement,
|
|
570
|
+
isCurrentContextPopover,
|
|
571
|
+
]);
|
|
440
572
|
|
|
441
573
|
const hide = useCallback(() => {
|
|
574
|
+
pendingShowRef.current = false;
|
|
442
575
|
if (isOpenRef.current) {
|
|
443
576
|
const el = popoverRef.current;
|
|
444
577
|
// See finding infra-4 note in `show`: mirror the same guard on hide so
|
|
@@ -450,11 +583,13 @@ export function useLayer(
|
|
|
450
583
|
el.style.display = 'none';
|
|
451
584
|
}
|
|
452
585
|
}
|
|
586
|
+
openedPopoverRef.current = null;
|
|
453
587
|
isOpenRef.current = false;
|
|
454
588
|
setIsOpen(false);
|
|
455
589
|
onHide?.();
|
|
456
590
|
}
|
|
457
|
-
|
|
591
|
+
clearContextMount();
|
|
592
|
+
}, [onHide, clearContextMount]);
|
|
458
593
|
|
|
459
594
|
// Ref for trigger element (context mode only)
|
|
460
595
|
const ref: RefCallback<HTMLElement> | undefined =
|
|
@@ -488,12 +623,14 @@ export function useLayer(
|
|
|
488
623
|
(e: Event) => {
|
|
489
624
|
const toggleEvent = e as ToggleEvent;
|
|
490
625
|
if (toggleEvent.newState === 'closed' && isOpenRef.current) {
|
|
626
|
+
openedPopoverRef.current = null;
|
|
491
627
|
isOpenRef.current = false;
|
|
492
628
|
setIsOpen(false);
|
|
493
629
|
onHide?.();
|
|
630
|
+
clearContextMount();
|
|
494
631
|
}
|
|
495
632
|
},
|
|
496
|
-
[onHide],
|
|
633
|
+
[onHide, clearContextMount],
|
|
497
634
|
);
|
|
498
635
|
|
|
499
636
|
// Ref callback for popover element — sets up the `toggle` listener.
|
|
@@ -531,8 +668,40 @@ export function useLayer(
|
|
|
531
668
|
(el: HTMLElement | null) => {
|
|
532
669
|
popoverRef.current = el;
|
|
533
670
|
bindToggleListener(el, handleToggle);
|
|
671
|
+
if (el && pendingShowRef.current) {
|
|
672
|
+
pendingShowRef.current = false;
|
|
673
|
+
show();
|
|
674
|
+
} else if (
|
|
675
|
+
el &&
|
|
676
|
+
isOpenRef.current &&
|
|
677
|
+
openedPopoverRef.current !== el &&
|
|
678
|
+
isCurrentContextPopover(el)
|
|
679
|
+
) {
|
|
680
|
+
// Changing a portal target remounts the popover. Preserve the logical
|
|
681
|
+
// open state without firing onShow again for the replacement element.
|
|
682
|
+
showPopoverElement(el);
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
[
|
|
686
|
+
handleToggle,
|
|
687
|
+
bindToggleListener,
|
|
688
|
+
show,
|
|
689
|
+
showPopoverElement,
|
|
690
|
+
isCurrentContextPopover,
|
|
691
|
+
],
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const sentinelRefCallback = useCallback(
|
|
695
|
+
(el: HTMLTemplateElement | null) => {
|
|
696
|
+
sentinelRef.current = el;
|
|
697
|
+
if (el && (!lazyMount || pendingShowRef.current || isOpenRef.current)) {
|
|
698
|
+
// The render call may have moved while the hook stayed mounted.
|
|
699
|
+
// Resolve again from the newly attached marker rather than reusing a
|
|
700
|
+
// portal target from its previous JSX position.
|
|
701
|
+
requestContextMount();
|
|
702
|
+
}
|
|
534
703
|
},
|
|
535
|
-
[
|
|
704
|
+
[lazyMount, requestContextMount],
|
|
536
705
|
);
|
|
537
706
|
|
|
538
707
|
// Re-bind when the handler identity changes while the element stays mounted,
|
|
@@ -556,6 +725,15 @@ export function useLayer(
|
|
|
556
725
|
// Render function for context mode
|
|
557
726
|
const renderContext = useCallback(
|
|
558
727
|
(children: ReactNode, props?: ContextRenderProps) => {
|
|
728
|
+
// Keep the marker mounted after resolving the layer. Apart from giving
|
|
729
|
+
// us the real JSX parent on first show, this lets its ref report when a
|
|
730
|
+
// persistent hook's render call moves to a different host.
|
|
731
|
+
const sentinel = <template ref={sentinelRefCallback} />;
|
|
732
|
+
|
|
733
|
+
if (contextMount === null) {
|
|
734
|
+
return <>{sentinel}</>;
|
|
735
|
+
}
|
|
736
|
+
|
|
559
737
|
const {
|
|
560
738
|
placement = 'above',
|
|
561
739
|
alignment = 'center',
|
|
@@ -598,10 +776,10 @@ export function useLayer(
|
|
|
598
776
|
? `${extraClassName} ${stylexResult.className ?? ''}`
|
|
599
777
|
: stylexResult.className;
|
|
600
778
|
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
|
|
779
|
+
// The marker gives us the actual JSX parent without mounting arbitrary
|
|
780
|
+
// children there. Safe positions preserve the existing DOM order and
|
|
781
|
+
// cascade; unsafe positions use the nearest corrective portal target.
|
|
782
|
+
const layer = (
|
|
605
783
|
<Container
|
|
606
784
|
ref={popoverRefCallback}
|
|
607
785
|
id={id}
|
|
@@ -609,14 +787,35 @@ export function useLayer(
|
|
|
609
787
|
aria-label={ariaLabel}
|
|
610
788
|
popover={lightDismiss ? 'auto' : 'manual'}
|
|
611
789
|
className={combinedClassName}
|
|
612
|
-
style={{
|
|
790
|
+
style={{
|
|
791
|
+
...stylexResult.style,
|
|
792
|
+
...anchorStyle,
|
|
793
|
+
...contextMount.portalStyle,
|
|
794
|
+
...extraStyle,
|
|
795
|
+
}}
|
|
613
796
|
onMouseEnter={onMouseEnter}
|
|
614
797
|
onMouseLeave={onMouseLeave}>
|
|
615
798
|
{children}
|
|
616
799
|
</Container>
|
|
617
800
|
);
|
|
801
|
+
|
|
802
|
+
return (
|
|
803
|
+
<>
|
|
804
|
+
{sentinel}
|
|
805
|
+
{contextMount.portalTarget
|
|
806
|
+
? createPortal(layer, contextMount.portalTarget)
|
|
807
|
+
: layer}
|
|
808
|
+
</>
|
|
809
|
+
);
|
|
618
810
|
},
|
|
619
|
-
[
|
|
811
|
+
[
|
|
812
|
+
anchorId,
|
|
813
|
+
contextMount,
|
|
814
|
+
id,
|
|
815
|
+
lightDismiss,
|
|
816
|
+
popoverRefCallback,
|
|
817
|
+
sentinelRefCallback,
|
|
818
|
+
],
|
|
620
819
|
);
|
|
621
820
|
|
|
622
821
|
// Render function for fixed mode
|
|
@@ -51,6 +51,48 @@ describe('parseInline', () => {
|
|
|
51
51
|
expect(result).toEqual([{type: 'image', src: 'img.png', alt: 'alt'}]);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it('rejects javascript: links as plain text (XSS prevention)', () => {
|
|
55
|
+
const result = parseInline('[click](javascript:alert(1))');
|
|
56
|
+
// Should be emitted as plain text, NOT as a link node
|
|
57
|
+
expect(result).toEqual([
|
|
58
|
+
{type: 'text', content: '[click](javascript:alert(1))'},
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects javascript: with mixed case and whitespace (XSS prevention)', () => {
|
|
63
|
+
const result = parseInline('[click](JaVaScRiPt:alert(1))');
|
|
64
|
+
expect(result).toEqual([
|
|
65
|
+
{type: 'text', content: '[click](JaVaScRiPt:alert(1))'},
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('rejects vbscript: links (XSS prevention)', () => {
|
|
70
|
+
const result = parseInline('[click](vbscript:MsgBox(1))');
|
|
71
|
+
expect(result).toEqual([
|
|
72
|
+
{type: 'text', content: '[click](vbscript:MsgBox(1))'},
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects data:text/html image src (XSS prevention)', () => {
|
|
77
|
+
const result = parseInline(
|
|
78
|
+
'</script>)',
|
|
79
|
+
);
|
|
80
|
+
expect(result).toEqual([
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
content: '</script>)',
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('allows normal http/https links', () => {
|
|
89
|
+
const result = parseInline('[safe](https://example.com)');
|
|
90
|
+
expect(result[0].type).toBe('link');
|
|
91
|
+
if (result[0].type === 'link') {
|
|
92
|
+
expect(result[0].href).toBe('https://example.com');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
54
96
|
it('parses strikethrough', () => {
|
|
55
97
|
const result = parseInline('~~deleted~~');
|
|
56
98
|
expect(result[0].type).toBe('strikethrough');
|
|
@@ -727,6 +769,17 @@ describe('citation parsing', () => {
|
|
|
727
769
|
});
|
|
728
770
|
});
|
|
729
771
|
|
|
772
|
+
it('rejects <javascript:...> angle-bracket autolinks (XSS prevention)', () => {
|
|
773
|
+
const result = parseInline('see <javascript:alert(1)> ok', {
|
|
774
|
+
autolink: 'gfm',
|
|
775
|
+
});
|
|
776
|
+
// Should NOT produce a link node with javascript: href
|
|
777
|
+
const linkNodes = result.filter(
|
|
778
|
+
(n): n is Extract<typeof n, {type: 'link'}> => n.type === 'link',
|
|
779
|
+
);
|
|
780
|
+
expect(linkNodes).toHaveLength(0);
|
|
781
|
+
});
|
|
782
|
+
|
|
730
783
|
it('parses <email> angle-bracket autolinks', () => {
|
|
731
784
|
const result = parseInline('mail <user@example.com> please', {
|
|
732
785
|
autolink: 'gfm',
|
package/src/Markdown/parser.ts
CHANGED
|
@@ -275,7 +275,7 @@ function matchReferenceLink(
|
|
|
275
275
|
// matches nothing.
|
|
276
276
|
const label = rawLabel === '' ? linkText : rawLabel;
|
|
277
277
|
const href = linkDefs.get(normalizeLinkLabel(label));
|
|
278
|
-
if (href != null) {
|
|
278
|
+
if (href != null && isSafeUrl(href)) {
|
|
279
279
|
return {
|
|
280
280
|
node: {type: 'link', href, children: parseInlineImpl(linkText, opts)},
|
|
281
281
|
end: labelClose + 1,
|
|
@@ -290,7 +290,7 @@ function matchReferenceLink(
|
|
|
290
290
|
return null;
|
|
291
291
|
}
|
|
292
292
|
const href = linkDefs.get(normalizeLinkLabel(linkText));
|
|
293
|
-
if (href == null) {
|
|
293
|
+
if (href == null || !isSafeUrl(href)) {
|
|
294
294
|
return null;
|
|
295
295
|
}
|
|
296
296
|
return {
|
|
@@ -359,6 +359,31 @@ function isWordChar(ch: string | undefined): boolean {
|
|
|
359
359
|
return /\w/.test(ch);
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// URL scheme sanitization
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Reject URLs with dangerous schemes (javascript:, vbscript:, data:) that
|
|
368
|
+
* could execute arbitrary code when rendered as link hrefs or image srcs.
|
|
369
|
+
* Returns true if the URL is safe to use, false otherwise.
|
|
370
|
+
*/
|
|
371
|
+
function isSafeUrl(url: string): boolean {
|
|
372
|
+
// Trim and collapse whitespace/control chars that browsers tolerate but
|
|
373
|
+
// could bypass a naive prefix check (e.g. "java\nscript:alert(1)").
|
|
374
|
+
// eslint-disable-next-line no-control-regex -- control chars are the bypass
|
|
375
|
+
const normalized = url.replace(/[\x00-\x1f\x7f]/g, '').trim();
|
|
376
|
+
const lower = normalized.toLowerCase();
|
|
377
|
+
if (
|
|
378
|
+
lower.startsWith('javascript:') ||
|
|
379
|
+
lower.startsWith('vbscript:') ||
|
|
380
|
+
lower.startsWith('data:text/html')
|
|
381
|
+
) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
362
387
|
// ---------------------------------------------------------------------------
|
|
363
388
|
// Inline parser
|
|
364
389
|
// ---------------------------------------------------------------------------
|
|
@@ -478,11 +503,17 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
478
503
|
if (altClose !== -1 && text[altClose + 1] === '(') {
|
|
479
504
|
const srcClose = findClosingParen(text, altClose + 2);
|
|
480
505
|
if (srcClose !== -1) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
506
|
+
const src = text.slice(altClose + 2, srcClose);
|
|
507
|
+
if (!isSafeUrl(src)) {
|
|
508
|
+
// Dangerous scheme — emit as plain text.
|
|
509
|
+
nodes.push({type: 'text', content: text.slice(i, srcClose + 1)});
|
|
510
|
+
} else {
|
|
511
|
+
nodes.push({
|
|
512
|
+
type: 'image',
|
|
513
|
+
src,
|
|
514
|
+
alt: text.slice(i + 2, altClose),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
486
517
|
i = srcClose + 1;
|
|
487
518
|
continue;
|
|
488
519
|
}
|
|
@@ -515,11 +546,17 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
515
546
|
if (textClose !== -1 && text[textClose + 1] === '(') {
|
|
516
547
|
const urlClose = findClosingParen(text, textClose + 2);
|
|
517
548
|
if (urlClose !== -1) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
549
|
+
const href = text.slice(textClose + 2, urlClose);
|
|
550
|
+
if (!isSafeUrl(href)) {
|
|
551
|
+
// Dangerous scheme — emit as plain text instead of a link.
|
|
552
|
+
nodes.push({type: 'text', content: text.slice(i, urlClose + 1)});
|
|
553
|
+
} else {
|
|
554
|
+
nodes.push({
|
|
555
|
+
type: 'link',
|
|
556
|
+
href,
|
|
557
|
+
children: parseInlineImpl(text.slice(i + 1, textClose), opts),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
523
560
|
i = urlClose + 1;
|
|
524
561
|
continue;
|
|
525
562
|
}
|
|
@@ -780,6 +817,10 @@ function scanAutolinksInText(text: string): AutolinkMatch[] {
|
|
|
780
817
|
let m: RegExpExecArray | null;
|
|
781
818
|
while ((m = re.exec(text)) !== null) {
|
|
782
819
|
const url = m[1];
|
|
820
|
+
// Skip dangerous URL schemes (javascript:, vbscript:, data:text/html)
|
|
821
|
+
if (!isSafeUrl(url)) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
783
824
|
matches.push({
|
|
784
825
|
start: m.index,
|
|
785
826
|
end: m.index + m[0].length,
|
|
@@ -78,6 +78,13 @@ export const docs = {
|
|
|
78
78
|
name: 'MobileNavToggle',
|
|
79
79
|
displayName: 'Mobile Nav Toggle',
|
|
80
80
|
description: 'Hamburger button that opens/closes the mobile nav drawer. Reads open state from AppShell context automatically: does NOT accept isOpen or onOpenChange props. Renders nothing above the mobile breakpoint.',
|
|
81
|
+
// The toggle renders null unless AppShell mobile context reports an
|
|
82
|
+
// enabled mobile viewport — the default context outside AppShell never
|
|
83
|
+
// does, so the Properties preview was an empty stage. appShellMobile
|
|
84
|
+
// has the preview simulate that context instead (#4983).
|
|
85
|
+
playground: {
|
|
86
|
+
appShellMobile: true,
|
|
87
|
+
},
|
|
81
88
|
props: [
|
|
82
89
|
{
|
|
83
90
|
name: 'children',
|
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
* disabled state) so all nav-like components stay in sync — especially important
|
|
12
12
|
* when TopNav items render inside MobileNav drawers alongside SideNav items.
|
|
13
13
|
*
|
|
14
|
-
* Individual components layer their own overrides (e.g. collapsed mode,
|
|
15
|
-
*
|
|
14
|
+
* Individual components layer their own overrides (e.g. collapsed mode,
|
|
15
|
+
* indentation) via stylex.props composition.
|
|
16
|
+
*
|
|
17
|
+
* The focus ring is not defined here. Compose
|
|
18
|
+
* `focusOutlineProps.focusVisible` (utils/focusOutline.stylex.ts) at the call
|
|
19
|
+
* site, on whichever element actually takes focus — in a split-action row that
|
|
20
|
+
* is the link and the toggle, not the row that contains them.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
23
|
export type NavItemSize = 'sm' | 'md' | 'lg';
|
|
@@ -79,15 +84,36 @@ export const navItemStyles = stylex.create({
|
|
|
79
84
|
|
|
80
85
|
/** Selected/active page indicator — deemphasized background, medium weight */
|
|
81
86
|
selected: {
|
|
82
|
-
|
|
87
|
+
// Forced colors flatten `--color-neutral` away, leaving the current page
|
|
88
|
+
// unmarked; Highlight/HighlightText is the platform convention, as in
|
|
89
|
+
// ToggleButton and SegmentedControlItem. No `forced-color-adjust: none`
|
|
90
|
+
// here — a nav row is not a native control, so the keywords land without
|
|
91
|
+
// it, and it would inherit into `endContent` and pin a Badge's own fill.
|
|
92
|
+
backgroundColor: {
|
|
93
|
+
default: colorVars['--color-neutral'],
|
|
94
|
+
'@media (forced-colors: active)': 'Highlight',
|
|
95
|
+
},
|
|
96
|
+
color: {
|
|
97
|
+
default: null,
|
|
98
|
+
'@media (forced-colors: active)': 'HighlightText',
|
|
99
|
+
},
|
|
83
100
|
fontWeight: fontWeightVars['--font-weight-medium'],
|
|
84
101
|
':hover': {
|
|
85
102
|
'@media (hover: hover)': {
|
|
86
|
-
backgroundColor:
|
|
103
|
+
backgroundColor: {
|
|
104
|
+
default: colorVars['--color-neutral'],
|
|
105
|
+
// Nested, not a sibling `(hover: hover) and (forced-colors:
|
|
106
|
+
// active)` block: as siblings `item`'s hover overlay ties on
|
|
107
|
+
// specificity and wins on source order, erasing the fill.
|
|
108
|
+
'@media (forced-colors: active)': 'Highlight',
|
|
109
|
+
},
|
|
87
110
|
},
|
|
88
111
|
},
|
|
89
112
|
':active': {
|
|
90
|
-
backgroundColor:
|
|
113
|
+
backgroundColor: {
|
|
114
|
+
default: colorVars['--color-neutral'],
|
|
115
|
+
'@media (forced-colors: active)': 'Highlight',
|
|
116
|
+
},
|
|
91
117
|
},
|
|
92
118
|
},
|
|
93
119
|
|
|
@@ -51,6 +51,26 @@ const styles = stylex.create({
|
|
|
51
51
|
opacity: 0,
|
|
52
52
|
cursor: 'pointer',
|
|
53
53
|
zIndex: 1,
|
|
54
|
+
minInlineSize: {
|
|
55
|
+
default: null,
|
|
56
|
+
'@media (pointer: coarse)': '24px',
|
|
57
|
+
},
|
|
58
|
+
minBlockSize: {
|
|
59
|
+
default: null,
|
|
60
|
+
'@media (pointer: coarse)': '24px',
|
|
61
|
+
},
|
|
62
|
+
insetBlockStart: {
|
|
63
|
+
default: null,
|
|
64
|
+
'@media (pointer: coarse)': '50%',
|
|
65
|
+
},
|
|
66
|
+
insetInlineStart: {
|
|
67
|
+
default: null,
|
|
68
|
+
'@media (pointer: coarse)': '50%',
|
|
69
|
+
},
|
|
70
|
+
transform: {
|
|
71
|
+
default: null,
|
|
72
|
+
'@media (pointer: coarse)': 'translate(-50%, -50%)',
|
|
73
|
+
},
|
|
54
74
|
},
|
|
55
75
|
inputDisabled: {
|
|
56
76
|
cursor: 'not-allowed',
|