@hjmds/react 1.14.0 → 1.16.0

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 (91) hide show
  1. package/README.md +5 -0
  2. package/dist/asset.d.ts.map +1 -1
  3. package/dist/asset.js +4 -1
  4. package/dist/asset.js.map +1 -1
  5. package/dist/clipboard.d.ts.map +1 -1
  6. package/dist/clipboard.js +39 -5
  7. package/dist/clipboard.js.map +1 -1
  8. package/dist/code-block.d.ts.map +1 -1
  9. package/dist/code-block.js +3 -1
  10. package/dist/code-block.js.map +1 -1
  11. package/dist/collapsible.d.ts +4 -0
  12. package/dist/collapsible.d.ts.map +1 -1
  13. package/dist/collapsible.js +4 -4
  14. package/dist/collapsible.js.map +1 -1
  15. package/dist/collection-rail.d.ts +24 -0
  16. package/dist/collection-rail.d.ts.map +1 -0
  17. package/dist/collection-rail.js +109 -0
  18. package/dist/collection-rail.js.map +1 -0
  19. package/dist/command-palette.d.ts.map +1 -1
  20. package/dist/command-palette.js +3 -1
  21. package/dist/command-palette.js.map +1 -1
  22. package/dist/content-transition.d.ts +1 -1
  23. package/dist/content-transition.d.ts.map +1 -1
  24. package/dist/content-transition.js +3 -2
  25. package/dist/content-transition.js.map +1 -1
  26. package/dist/context-menu.d.ts.map +1 -1
  27. package/dist/context-menu.js +3 -1
  28. package/dist/context-menu.js.map +1 -1
  29. package/dist/design-profile.d.ts +18 -0
  30. package/dist/design-profile.d.ts.map +1 -0
  31. package/dist/design-profile.js +27 -0
  32. package/dist/design-profile.js.map +1 -0
  33. package/dist/effect-surface.d.ts.map +1 -1
  34. package/dist/effect-surface.js +4 -3
  35. package/dist/effect-surface.js.map +1 -1
  36. package/dist/evidence.d.ts +1 -1
  37. package/dist/evidence.js +1 -1
  38. package/dist/evidence.js.map +1 -1
  39. package/dist/floating-action-button.d.ts +1 -1
  40. package/dist/floating-action-button.d.ts.map +1 -1
  41. package/dist/floating-action-button.js +4 -1
  42. package/dist/floating-action-button.js.map +1 -1
  43. package/dist/heading.d.ts.map +1 -1
  44. package/dist/heading.js +3 -1
  45. package/dist/heading.js.map +1 -1
  46. package/dist/internal.d.ts +51 -0
  47. package/dist/internal.d.ts.map +1 -1
  48. package/dist/internal.js +7 -1
  49. package/dist/internal.js.map +1 -1
  50. package/dist/layout.d.ts +5 -1
  51. package/dist/layout.d.ts.map +1 -1
  52. package/dist/layout.js +11 -3
  53. package/dist/layout.js.map +1 -1
  54. package/dist/menubar.d.ts.map +1 -1
  55. package/dist/menubar.js +5 -2
  56. package/dist/menubar.js.map +1 -1
  57. package/dist/navigation.d.ts.map +1 -1
  58. package/dist/navigation.js +30 -9
  59. package/dist/navigation.js.map +1 -1
  60. package/dist/popover.d.ts.map +1 -1
  61. package/dist/popover.js +3 -1
  62. package/dist/popover.js.map +1 -1
  63. package/dist/provider.d.ts +4 -0
  64. package/dist/provider.d.ts.map +1 -1
  65. package/dist/provider.js +8 -2
  66. package/dist/provider.js.map +1 -1
  67. package/dist/screens.d.ts +7 -2
  68. package/dist/screens.d.ts.map +1 -1
  69. package/dist/screens.js +20 -7
  70. package/dist/screens.js.map +1 -1
  71. package/dist/selection.d.ts.map +1 -1
  72. package/dist/selection.js +4 -1
  73. package/dist/selection.js.map +1 -1
  74. package/dist/sidebar.d.ts.map +1 -1
  75. package/dist/sidebar.js +2 -1
  76. package/dist/sidebar.js.map +1 -1
  77. package/dist/skip-nav.d.ts.map +1 -1
  78. package/dist/skip-nav.js +2 -1
  79. package/dist/skip-nav.js.map +1 -1
  80. package/dist/styles.css +42 -8
  81. package/dist/styles.layered.css +42 -8
  82. package/dist/theme.d.ts.map +1 -1
  83. package/dist/theme.js +37 -8
  84. package/dist/theme.js.map +1 -1
  85. package/dist/toggle-group.d.ts.map +1 -1
  86. package/dist/toggle-group.js +3 -1
  87. package/dist/toggle-group.js.map +1 -1
  88. package/dist/top.d.ts.map +1 -1
  89. package/dist/top.js +9 -4
  90. package/dist/top.js.map +1 -1
  91. package/package.json +13 -3
package/README.md CHANGED
@@ -179,3 +179,8 @@ Stable `sortable`, `swipe-actions`, `content-transition`, `carousel-motion` and
179
179
  a brand/navigation/actions composition with a progressive glass surface.
180
180
  Existing [BottomNavigation](../design-contracts/docs/bottom-navigation.md) accepts
181
181
  `configuration={{ presentation: "capsule" }}` with an adjacent `primaryAction`.
182
+
183
+
184
+ ## React Server Component profile boundary
185
+
186
+ A server-authored guide exposed a missing client boundary in the layout entry when profile context was added. The current source declares `"use client"` in `/layout` and `/provider` so a Server Component can pass serializable product profiles and server-authored JSX children without a page-wide wrapper. This fix is not present in published1.15.0; keep the consumer's wrapper until a version containing the fix is installed. Hooks and callback props still belong in consumer Client Components, and this change does not promise direct server imports of the root barrel or other entries. The [integration fixture](test/fixtures/rsc-profile/README.md) preserves the reproduction; [QA](../../docs/qa/2026-10-07-rsc-profile-boundary.md) records the actual Next build and hydration scope.
@@ -1 +1 @@
1
- {"version":3,"file":"asset.d.ts","sourceRoot":"","sources":["../src/asset.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,UAAU,EAAE,eAAe,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,KAAK,SAAS,CAAC,CAAC;IAC7E,mEAAmE;IACnE,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEH,eAAO,MAAM,KAAK;gBAdJ,eAAe;IAC3B;;;;OAIG;cACO,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,KAAK,SAAS,CAAC;IAC5E,mEAAmE;gBACvD,SAAS;gBACT,MAAM;IAClB,gGAAgG;kBAClF,uBAAuB;mDAoCrC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEH,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,IAAgC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,eAAe,+BAYxH"}
1
+ {"version":3,"file":"asset.d.ts","sourceRoot":"","sources":["../src/asset.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,UAAU,EAAE,eAAe,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,KAAK,SAAS,CAAC,CAAC;IAC7E,mEAAmE;IACnE,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEH,eAAO,MAAM,KAAK;gBAdJ,eAAe;IAC3B;;;;OAIG;cACO,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,KAAK,SAAS,CAAC;IAC5E,mEAAmE;gBACvD,SAAS;gBACT,MAAM;IAClB,gGAAgG;kBAClF,uBAAuB;mDAuCrC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEH,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,IAAgC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,eAAe,+BAYxH"}
package/dist/asset.js CHANGED
@@ -7,7 +7,10 @@ export const Asset = forwardRef(function Asset({ descriptor, children, accessory
7
7
  validateAssetDescriptor(descriptor);
8
8
  const theme = useOptionalHjmTheme();
9
9
  const size = assetRecipe.sizes[descriptor.size ?? assetRecipe.defaults.size];
10
- const shape = assetRecipe.shapes[descriptor.shape ?? assetRecipe.defaults.shape];
10
+ const shapeKey = descriptor.shape ?? assetRecipe.defaults.shape;
11
+ // Rounded is a theme radius role; explicit square/circle are media geometry.
12
+ // Keeping the recipe's numeric radius would bypass nested product profiles.
13
+ const shape = shapeKey === "rounded" ? theme?.designProfile?.tokens.radius.md ?? assetRecipe.shapes.rounded : assetRecipe.shapes[shapeKey];
11
14
  const decorative = descriptor.decorative ?? false;
12
15
  // The frame reports whether motion is allowed so the product's player can
13
16
  // read one answer instead of re-deriving the preference per surface.
package/dist/asset.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"asset.js","sourceRoot":"","sources":["../src/asset.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,GAExB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,UAAU,EAAsC,MAAM,OAAO,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAkBpD,MAAM,CAAC,MAAM,KAAK,GAAG,UAAU,CAA6B,SAAS,KAAK,CACxE,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,EAC3D,YAAY;IAEZ,uBAAuB,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,mBAAmB,EAAE,CAAC;IACpC,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,IAAI,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACjF,MAAM,UAAU,GAAG,UAAU,CAAC,UAAU,IAAI,KAAK,CAAC;IAClD,0EAA0E;IAC1E,qEAAqE;IACrE,MAAM,OAAO,GAAG,kBAAkB,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,aAAa,IAAI,KAAK,CAAC,CAAC;IAC/F,OAAO,CACL,eACE,GAAG,EAAE,YAAY,EACjB,SAAS,EAAE,UAAU,CAAC,WAAW,EAAE,SAAS,CAAC,eAClC,UAAU,CAAC,IAAI,kBACZ,OAAO,IAAI,SAAS,EAClC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,iBAC5B,UAAU,IAAI,SAAS,gBACxB,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,kBAAkB,EAClE,KAAK,EAAE,EAAE,GAAG,WAAW;YACrB,kBAAkB,EAAE,GAAG,IAAI,IAAI;YAC/B,oBAAoB,EAAE,GAAG,KAAK,IAAI;YAClC,8BAA8B,EAAE,GAAG,WAAW,CAAC,SAAS,CAAC,MAAM,IAAI;SACnD,aAElB,cAAK,SAAS,EAAC,kBAAkB,YAC/B,cAAK,SAAS,EAAC,kBAAkB,YAAE,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAO,GACvG,EACL,SAAS,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,sBAAsB,YAAE,SAAS,GAAQ,CAAC,CAAC,CAAC,IAAI,IACzE,CACP,CAAC;AACJ,CAAC,CAAC,CAAC;AAYH,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAmB;IACvH,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,CACL,cACE,SAAS,EAAE,UAAU,CAAC,iBAAiB,EAAE,SAAS,CAAC,EACnD,IAAI,EAAC,OAAO,gBACA,KAAK,EACjB,KAAK,EAAE,EAAE,GAAG,WAAW,EAAE,qBAAqB,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,WAAW,CAAC,YAAY,CAAC,IAAI,EAAmB,YAEvH,QAAQ,GACL,CACP,CAAC;AACJ,CAAC","sourcesContent":["import {\n assetRecipe,\n shouldAnimateAsset,\n validateAssetDescriptor,\n type AssetDescriptor,\n} from \"@hjmds/design-contracts/components/asset\";\nimport { forwardRef, type CSSProperties, type ReactNode } from \"react\";\nimport { classNames } from \"./internal.js\";\nimport { useOptionalHjmTheme } from \"./provider.js\";\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\n\nexport type AssetProps = Readonly<{\n descriptor: AssetDescriptor;\n /**\n * The media itself. A player is the product's dependency, never this\n * package's — the frame is all the contract owns. `animate` is the\n * already-resolved answer to \"may this move right now\".\n */\n children: ReactNode | ((state: Readonly<{ animate: boolean }>) => ReactNode);\n /** A small mark on the frame's outer corner (play, status dot). */\n accessory?: ReactNode;\n className?: string;\n /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */\n layoutStyle?: HjmCompositionStyleProp;\n}>;\n\nexport const Asset = forwardRef<HTMLDivElement, AssetProps>(function Asset(\n { descriptor, children, accessory, className, layoutStyle },\n forwardedRef,\n) {\n validateAssetDescriptor(descriptor);\n const theme = useOptionalHjmTheme();\n const size = assetRecipe.sizes[descriptor.size ?? assetRecipe.defaults.size];\n const shape = assetRecipe.shapes[descriptor.shape ?? assetRecipe.defaults.shape];\n const decorative = descriptor.decorative ?? false;\n // The frame reports whether motion is allowed so the product's player can\n // read one answer instead of re-deriving the preference per surface.\n const animate = shouldAnimateAsset(descriptor.kind, theme?.environment.reducedMotion ?? false);\n return (\n <div\n ref={forwardedRef}\n className={classNames(\"hjm-asset\", className)}\n data-kind={descriptor.kind}\n data-animate={animate || undefined}\n role={decorative ? \"presentation\" : \"img\"}\n aria-hidden={decorative || undefined}\n aria-label={decorative ? undefined : descriptor.accessibilityLabel}\n style={{ ...layoutStyle,\n \"--hjm-asset-size\": `${size}px`,\n \"--hjm-asset-radius\": `${shape}px`,\n \"--hjm-asset-accessory-offset\": `${assetRecipe.accessory.offset}px`,\n } as CSSProperties}\n >\n <div className=\"hjm-asset__frame\">\n <div className=\"hjm-asset__media\">{typeof children === \"function\" ? children({ animate }) : children}</div>\n </div>\n {accessory ? <span className=\"hjm-asset__accessory\">{accessory}</span> : null}\n </div>\n );\n});\n\nexport type AssetGroupProps = Readonly<{\n /** Accessible name for the group; the overlap alone does not say what it is. */\n label: string;\n size?: AssetDescriptor[\"size\"];\n children: ReactNode;\n className?: string;\n /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */\n layoutStyle?: HjmCompositionStyleProp;\n}>;\n\n/** Overlaps assets with the same ratio Avatar uses — they share a row on purpose. */\nexport function AssetGroup({ label, size = assetRecipe.defaults.size, children, className, layoutStyle }: AssetGroupProps) {\n const pixels = assetRecipe.sizes[size];\n return (\n <div\n className={classNames(\"hjm-asset-group\", className)}\n role=\"group\"\n aria-label={label}\n style={{ ...layoutStyle, \"--hjm-asset-overlap\": `${-Math.round(pixels * assetRecipe.overlapRatio)}px` } as CSSProperties}\n >\n {children}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"asset.js","sourceRoot":"","sources":["../src/asset.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n assetRecipe,\n shouldAnimateAsset,\n validateAssetDescriptor,\n type AssetDescriptor,\n} from \"@hjmds/design-contracts/components/asset\";\nimport { forwardRef, type CSSProperties, type ReactNode } from \"react\";\nimport { classNames } from \"./internal.js\";\nimport { useOptionalHjmTheme } from \"./provider.js\";\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\n\nexport type AssetProps = Readonly<{\n descriptor: AssetDescriptor;\n /**\n * The media itself. A player is the product's dependency, never this\n * package's — the frame is all the contract owns. `animate` is the\n * already-resolved answer to \"may this move right now\".\n */\n children: ReactNode | ((state: Readonly<{ animate: boolean }>) => ReactNode);\n /** A small mark on the frame's outer corner (play, status dot). */\n accessory?: ReactNode;\n className?: string;\n /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */\n layoutStyle?: HjmCompositionStyleProp;\n}>;\n\nexport const Asset = forwardRef<HTMLDivElement, AssetProps>(function Asset(\n { descriptor, children, accessory, className, layoutStyle },\n forwardedRef,\n) {\n validateAssetDescriptor(descriptor);\n const theme = useOptionalHjmTheme();\n const size = assetRecipe.sizes[descriptor.size ?? assetRecipe.defaults.size];\n const shapeKey = descriptor.shape ?? assetRecipe.defaults.shape;\n // Rounded is a theme radius role; explicit square/circle are media geometry.\n // Keeping the recipe's numeric radius would bypass nested product profiles.\n const shape = shapeKey === \"rounded\" ? theme?.designProfile?.tokens.radius.md ?? assetRecipe.shapes.rounded : assetRecipe.shapes[shapeKey];\n const decorative = descriptor.decorative ?? false;\n // The frame reports whether motion is allowed so the product's player can\n // read one answer instead of re-deriving the preference per surface.\n const animate = shouldAnimateAsset(descriptor.kind, theme?.environment.reducedMotion ?? false);\n return (\n <div\n ref={forwardedRef}\n className={classNames(\"hjm-asset\", className)}\n data-kind={descriptor.kind}\n data-animate={animate || undefined}\n role={decorative ? \"presentation\" : \"img\"}\n aria-hidden={decorative || undefined}\n aria-label={decorative ? undefined : descriptor.accessibilityLabel}\n style={{ ...layoutStyle,\n \"--hjm-asset-size\": `${size}px`,\n \"--hjm-asset-radius\": `${shape}px`,\n \"--hjm-asset-accessory-offset\": `${assetRecipe.accessory.offset}px`,\n } as CSSProperties}\n >\n <div className=\"hjm-asset__frame\">\n <div className=\"hjm-asset__media\">{typeof children === \"function\" ? children({ animate }) : children}</div>\n </div>\n {accessory ? <span className=\"hjm-asset__accessory\">{accessory}</span> : null}\n </div>\n );\n});\n\nexport type AssetGroupProps = Readonly<{\n /** Accessible name for the group; the overlap alone does not say what it is. */\n label: string;\n size?: AssetDescriptor[\"size\"];\n children: ReactNode;\n className?: string;\n /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */\n layoutStyle?: HjmCompositionStyleProp;\n}>;\n\n/** Overlaps assets with the same ratio Avatar uses — they share a row on purpose. */\nexport function AssetGroup({ label, size = assetRecipe.defaults.size, children, className, layoutStyle }: AssetGroupProps) {\n const pixels = assetRecipe.sizes[size];\n return (\n <div\n className={classNames(\"hjm-asset-group\", className)}\n role=\"group\"\n aria-label={label}\n style={{ ...layoutStyle, \"--hjm-asset-overlap\": `${-Math.round(pixels * assetRecipe.overlapRatio)}px` } as CSSProperties}\n >\n {children}\n </div>\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../src/clipboard.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAwD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7F,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAKxD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC,GACrF,QAAQ,CAAC;IACP,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,MAAM,EAAE,QAAQ,CAAC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;IACzD,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,CAAC,CAAC;AAIL;;;;;GAKG;AACH,eAAO,MAAM,eAAe;WAjBjB,MAAM;IACb,oEAAoE;YAC5D,QAAQ,CAAC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,SAAS,CAAA;KAAE,CAAC;IACxD,8CAA8C;uBAC3B,MAAM;aAChB,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI;kBAClB,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI;sDAqDzC,CAAC"}
1
+ {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../src/clipboard.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAwD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7F,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAKxD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC,GACrF,QAAQ,CAAC;IACP,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,MAAM,EAAE,QAAQ,CAAC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;IACzD,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,CAAC,CAAC;AAIL;;;;;GAKG;AACH,eAAO,MAAM,eAAe;WAjBjB,MAAM;IACb,oEAAoE;YAC5D,QAAQ,CAAC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,SAAS,CAAA;KAAE,CAAC;IACxD,8CAA8C;uBAC3B,MAAM;aAChB,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI;kBAClB,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI;sDAiFzC,CAAC"}
package/dist/clipboard.js CHANGED
@@ -14,31 +14,65 @@ export const ClipboardButton = forwardRef(function ClipboardButton(
14
14
  // Pass `tone="primary"` explicitly when copying *is* the screen's main action.
15
15
  { value, labels, feedbackDuration = defaultFeedbackDuration, onCopy, onCopyError, tone = "secondary", ...props }, forwardedRef) {
16
16
  const [copied, setCopied] = useState(false);
17
+ const [copying, setCopying] = useState(false);
18
+ const pending = useRef(false);
19
+ const generation = useRef(0);
20
+ const mounted = useRef(false);
17
21
  const timer = useRef(undefined);
18
- useEffect(() => () => { if (timer.current !== undefined)
19
- clearTimeout(timer.current); }, []);
22
+ useEffect(() => {
23
+ mounted.current = true;
24
+ setCopying(pending.current);
25
+ return () => {
26
+ mounted.current = false;
27
+ generation.current += 1;
28
+ if (timer.current !== undefined)
29
+ clearTimeout(timer.current);
30
+ };
31
+ }, []);
20
32
  useEffect(() => {
21
33
  // A new value makes the previous "copied" claim false: it referred to the
22
34
  // old string, and leaving it up tells the user something untrue.
35
+ generation.current += 1;
23
36
  setCopied(false);
24
37
  if (timer.current !== undefined)
25
38
  clearTimeout(timer.current);
26
39
  }, [value]);
27
40
  const copy = useCallback(async () => {
41
+ // OS clipboard promises cannot be cancelled. A per-value generation ignores
42
+ // obsolete feedback/callbacks instead of announcing an old source as copied.
43
+ // A ref also closes the gap before React paints the unavailable busy button.
44
+ if (!mounted.current || pending.current)
45
+ return;
46
+ const request = ++generation.current;
47
+ pending.current = true;
48
+ setCopying(true);
49
+ setCopied(false);
50
+ if (timer.current !== undefined)
51
+ clearTimeout(timer.current);
28
52
  try {
29
53
  await navigator.clipboard.writeText(value);
54
+ if (!mounted.current || request !== generation.current)
55
+ return;
30
56
  setCopied(true);
31
- onCopy?.(value);
32
57
  if (timer.current !== undefined)
33
58
  clearTimeout(timer.current);
34
59
  timer.current = setTimeout(() => setCopied(false), feedbackDuration);
60
+ onCopy?.(value);
35
61
  }
36
62
  catch (error) {
37
63
  // Clipboard access can be denied; swallowing that would leave the user
38
64
  // believing the value was copied.
39
- onCopyError?.(error);
65
+ if (mounted.current && request === generation.current)
66
+ onCopyError?.(error);
67
+ }
68
+ finally {
69
+ // Keep the OS write single-flight across value changes as well: starting a
70
+ // second write early could let the first overwrite its clipboard contents.
71
+ pending.current = false;
72
+ if (mounted.current)
73
+ setCopying(false);
40
74
  }
41
75
  }, [value, feedbackDuration, onCopy, onCopyError]);
42
- return (_jsxs(_Fragment, { children: [_jsx(Button, { ...props, tone: tone, ref: forwardedRef, onClick: () => { void copy(); }, children: copied ? labels.copied : labels.idle }), _jsx("span", { role: "status", className: "hjm-visually-hidden", children: copied ? labels.copied : "" })] }));
76
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { ...props, tone: tone, loading: props.loading || copying, ref: forwardedRef, onClick: () => { void copy(); }, children: copied ? labels.copied : labels.idle }), _jsx("span", { role: "status", className: "hjm-visually-hidden", children: copied ? labels.copied : "" })] }));
43
77
  });
44
78
  //# sourceMappingURL=clipboard.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../src/clipboard.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAC7F,OAAO,EAAE,MAAM,EAAoB,MAAM,cAAc,CAAC;AAgBxD,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAErC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CACvC,SAAS,eAAe;AACtB,iGAAiG;AACjG,6FAA6F;AAC7F,+EAA+E;AAC/E,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,GAAG,uBAAuB,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,GAAG,WAAW,EAAE,GAAG,KAAK,EAAE,EAChH,YAAY;IAEZ,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,MAAM,CAA4C,SAAS,CAAC,CAAC;IAC3E,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC7F,SAAS,CAAC,GAAG,EAAE;QACb,0EAA0E;QAC1E,iEAAiE;QACjE,SAAS,CAAC,KAAK,CAAC,CAAC;QACjB,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;YAAE,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/D,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAClC,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YAC3C,SAAS,CAAC,IAAI,CAAC,CAAC;YAChB,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;YAChB,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;gBAAE,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7D,KAAK,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC,CAAC;QACvE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,uEAAuE;YACvE,kCAAkC;YAClC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAEnD,OAAO,CACL,8BACE,KAAC,MAAM,OAAK,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,YAC9E,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,GAC9B,EAET,eAAM,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,qBAAqB,YAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAQ,IACvF,CACJ,CAAC;AACJ,CAAC,CACF,CAAC","sourcesContent":["import { forwardRef, useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { Button, type ButtonProps } from \"./actions.js\";\n\n// `onCopy` also names the DOM clipboard event handler on ButtonProps; intersected, the\n// value callback had to accept a ClipboardEvent too and no product could pass it. The\n// renderer consumes the prop and never forwards the DOM event, so omitting is lossless.\nexport type ClipboardButtonProps = Omit<ButtonProps, \"children\" | \"onClick\" | \"onCopy\"> &\n Readonly<{\n value: string;\n /** Localized copy for both states; the renderer invents neither. */\n labels: Readonly<{ idle: ReactNode; copied: ReactNode }>;\n /** How long the copied state stays, in ms. */\n feedbackDuration?: number;\n onCopy?: (value: string) => void;\n onCopyError?: (error: unknown) => void;\n }>;\n\nconst defaultFeedbackDuration = 2000;\n\n/**\n * Copying is three things products kept re-deriving: the async clipboard call,\n * the temporary \"copied\" state, and announcing that state to a screen reader.\n * The last one is the part that was always missing — a label that only changes\n * visually tells a non-sighted user nothing happened.\n */\nexport const ClipboardButton = forwardRef<HTMLButtonElement, ClipboardButtonProps>(\n function ClipboardButton(\n // Default `secondary` (2026-10-06): copying is a helper beside the screen's main action, and the\n // inherited Button default `primary` added a second primary to screens that already had one.\n // Pass `tone=\"primary\"` explicitly when copying *is* the screen's main action.\n { value, labels, feedbackDuration = defaultFeedbackDuration, onCopy, onCopyError, tone = \"secondary\", ...props },\n forwardedRef,\n ) {\n const [copied, setCopied] = useState(false);\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => { if (timer.current !== undefined) clearTimeout(timer.current); }, []);\n useEffect(() => {\n // A new value makes the previous \"copied\" claim false: it referred to the\n // old string, and leaving it up tells the user something untrue.\n setCopied(false);\n if (timer.current !== undefined) clearTimeout(timer.current);\n }, [value]);\n\n const copy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(value);\n setCopied(true);\n onCopy?.(value);\n if (timer.current !== undefined) clearTimeout(timer.current);\n timer.current = setTimeout(() => setCopied(false), feedbackDuration);\n } catch (error) {\n // Clipboard access can be denied; swallowing that would leave the user\n // believing the value was copied.\n onCopyError?.(error);\n }\n }, [value, feedbackDuration, onCopy, onCopyError]);\n\n return (\n <>\n <Button {...props} tone={tone} ref={forwardedRef} onClick={() => { void copy(); }}>\n {copied ? labels.copied : labels.idle}\n </Button>\n {/* The state change is announced, not only painted. */}\n <span role=\"status\" className=\"hjm-visually-hidden\">{copied ? labels.copied : \"\"}</span>\n </>\n );\n },\n);\n"]}
1
+ {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../src/clipboard.tsx"],"names":[],"mappings":"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","sourcesContent":["import { forwardRef, useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { Button, type ButtonProps } from \"./actions.js\";\n\n// `onCopy` also names the DOM clipboard event handler on ButtonProps; intersected, the\n// value callback had to accept a ClipboardEvent too and no product could pass it. The\n// renderer consumes the prop and never forwards the DOM event, so omitting is lossless.\nexport type ClipboardButtonProps = Omit<ButtonProps, \"children\" | \"onClick\" | \"onCopy\"> &\n Readonly<{\n value: string;\n /** Localized copy for both states; the renderer invents neither. */\n labels: Readonly<{ idle: ReactNode; copied: ReactNode }>;\n /** How long the copied state stays, in ms. */\n feedbackDuration?: number;\n onCopy?: (value: string) => void;\n onCopyError?: (error: unknown) => void;\n }>;\n\nconst defaultFeedbackDuration = 2000;\n\n/**\n * Copying is three things products kept re-deriving: the async clipboard call,\n * the temporary \"copied\" state, and announcing that state to a screen reader.\n * The last one is the part that was always missing — a label that only changes\n * visually tells a non-sighted user nothing happened.\n */\nexport const ClipboardButton = forwardRef<HTMLButtonElement, ClipboardButtonProps>(\n function ClipboardButton(\n // Default `secondary` (2026-10-06): copying is a helper beside the screen's main action, and the\n // inherited Button default `primary` added a second primary to screens that already had one.\n // Pass `tone=\"primary\"` explicitly when copying *is* the screen's main action.\n { value, labels, feedbackDuration = defaultFeedbackDuration, onCopy, onCopyError, tone = \"secondary\", ...props },\n forwardedRef,\n ) {\n const [copied, setCopied] = useState(false);\n const [copying, setCopying] = useState(false);\n const pending = useRef(false);\n const generation = useRef(0);\n const mounted = useRef(false);\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => {\n mounted.current = true;\n setCopying(pending.current);\n return () => {\n mounted.current = false;\n generation.current += 1;\n if (timer.current !== undefined) clearTimeout(timer.current);\n };\n }, []);\n useEffect(() => {\n // A new value makes the previous \"copied\" claim false: it referred to the\n // old string, and leaving it up tells the user something untrue.\n generation.current += 1;\n setCopied(false);\n if (timer.current !== undefined) clearTimeout(timer.current);\n }, [value]);\n\n const copy = useCallback(async () => {\n // OS clipboard promises cannot be cancelled. A per-value generation ignores\n // obsolete feedback/callbacks instead of announcing an old source as copied.\n // A ref also closes the gap before React paints the unavailable busy button.\n if (!mounted.current || pending.current) return;\n const request = ++generation.current;\n pending.current = true;\n setCopying(true);\n setCopied(false);\n if (timer.current !== undefined) clearTimeout(timer.current);\n try {\n await navigator.clipboard.writeText(value);\n if (!mounted.current || request !== generation.current) return;\n setCopied(true);\n if (timer.current !== undefined) clearTimeout(timer.current);\n timer.current = setTimeout(() => setCopied(false), feedbackDuration);\n onCopy?.(value);\n } catch (error) {\n // Clipboard access can be denied; swallowing that would leave the user\n // believing the value was copied.\n if (mounted.current && request === generation.current) onCopyError?.(error);\n } finally {\n // Keep the OS write single-flight across value changes as well: starting a\n // second write early could let the first overwrite its clipboard contents.\n pending.current = false;\n if (mounted.current) setCopying(false);\n }\n }, [value, feedbackDuration, onCopy, onCopyError]);\n\n return (\n <>\n <Button {...props} tone={tone} loading={props.loading || copying} ref={forwardedRef} onClick={() => { void copy(); }}>\n {copied ? labels.copied : labels.idle}\n </Button>\n {/* The state change is announced, not only painted. */}\n <span role=\"status\" className=\"hjm-visually-hidden\">{copied ? labels.copied : \"\"}</span>\n </>\n );\n },\n);\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"code-block.d.ts","sourceRoot":"","sources":["../src/code-block.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AACrC,OAAO,EAAkB,KAAK,mBAAmB,EAAC,MAAM,oCAAoC,CAAC;AAC7F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACtE,MAAM,MAAM,cAAc,GAAC,mBAAmB,GAAG,QAAQ,CAAC;IACxD,UAAU,CAAC,EAAC,SAAS,CAAC;IACtB,gGAAgG;IAChG,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEH,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,EAAC,UAAU,EAAC,WAAW,EAAC,GAAG,UAAU,EAAC,EAAC,cAAc,+BAAuwB"}
1
+ {"version":3,"file":"code-block.d.ts","sourceRoot":"","sources":["../src/code-block.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AACrC,OAAO,EAAkB,KAAK,mBAAmB,EAAC,MAAM,oCAAoC,CAAC;AAC7F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACtE,MAAM,MAAM,cAAc,GAAC,mBAAmB,GAAG,QAAQ,CAAC;IACxD,UAAU,CAAC,EAAC,SAAS,CAAC;IACtB,gGAAgG;IAChG,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEH,mEAAmE;AAGnE,wBAAgB,SAAS,CAAC,EAAC,UAAU,EAAC,WAAW,EAAC,GAAG,UAAU,EAAC,EAAC,cAAc,+BAA6+B"}
@@ -2,5 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { resolveCodeBlock } from '@hjmds/design-contracts/code-block';
3
3
  const colors = { plain: 'var(--hjm-color-text)', keyword: 'var(--hjm-color-content-brand)', string: 'var(--hjm-color-text)', comment: 'var(--hjm-color-text-muted)', number: 'var(--hjm-color-content-brand)' };
4
4
  /** Token text is rendered as React text, never executable HTML. */
5
- export function CodeBlock({ copyAction, layoutStyle, ...descriptor }) { const spec = resolveCodeBlock(descriptor); return _jsxs("section", { "aria-label": spec.label, style: { minWidth: 0, borderRadius: 'var(--hjm-radius-lg)', background: 'var(--hjm-color-surface-alt)', color: 'var(--hjm-color-text)', overflow: 'hidden', ...layoutStyle }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--hjm-space-sm)', padding: 'var(--hjm-space-md)' }, children: [_jsx("span", { children: spec.language ?? spec.label }), copyAction] }), _jsx("pre", { tabIndex: 0, "aria-label": spec.label, style: { margin: 0, padding: 'var(--hjm-space-md)', overflowX: 'auto', whiteSpace: spec.wrap ? 'pre-wrap' : 'pre', overflowWrap: spec.wrap ? 'anywhere' : undefined }, children: _jsx("code", { children: spec.tokens.map((token, index) => _jsx("span", { style: { color: colors[token.tone ?? 'plain'] }, children: token.text }, index)) }) })] }); }
5
+ // Theme the complete selectable source once; nested syntax spans inherit its metrics.
6
+ // Code syntax stays LTR even in an RTL shell, which otherwise moves trailing punctuation ahead of the statement.
7
+ export function CodeBlock({ copyAction, layoutStyle, ...descriptor }) { const spec = resolveCodeBlock(descriptor); return _jsxs("section", { "aria-label": spec.label, style: { minWidth: 0, borderRadius: 'var(--hjm-radius-lg)', background: 'var(--hjm-color-surface-alt)', color: 'var(--hjm-color-text)', overflow: 'hidden', ...layoutStyle }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--hjm-space-sm)', padding: 'var(--hjm-space-md)' }, children: [_jsx("span", { children: spec.language ?? spec.label }), copyAction] }), _jsx("pre", { dir: "ltr", tabIndex: 0, "aria-label": spec.label, style: { margin: 0, padding: 'var(--hjm-space-md)', fontFamily: 'var(--hjm-font-family-code, ui-monospace, monospace)', fontSize: 'var(--hjm-type-body-size)', lineHeight: 'var(--hjm-type-body-line-height)', overflowX: 'auto', whiteSpace: spec.wrap ? 'pre-wrap' : 'pre', overflowWrap: spec.wrap ? 'anywhere' : undefined }, children: _jsx("code", { style: { fontFamily: 'inherit', fontSize: 'inherit', lineHeight: 'inherit' }, children: spec.tokens.map((token, index) => _jsx("span", { style: { color: colors[token.tone ?? 'plain'] }, children: token.text }, index)) }) })] }); }
6
8
  //# sourceMappingURL=code-block.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"code-block.js","sourceRoot":"","sources":["../src/code-block.tsx"],"names":[],"mappings":";AACA,OAAO,EAAC,gBAAgB,EAA0B,MAAM,oCAAoC,CAAC;AAO7F,MAAM,MAAM,GAAC,EAAC,KAAK,EAAC,uBAAuB,EAAC,OAAO,EAAC,gCAAgC,EAAC,MAAM,EAAC,uBAAuB,EAAC,OAAO,EAAC,6BAA6B,EAAC,MAAM,EAAC,gCAAgC,EAAC,CAAC;AACnM,mEAAmE;AACnE,MAAM,UAAU,SAAS,CAAC,EAAC,UAAU,EAAC,WAAW,EAAC,GAAG,UAAU,EAAgB,IAAE,MAAM,IAAI,GAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAA,OAAO,iCAAqB,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,EAAC,QAAQ,EAAC,CAAC,EAAC,YAAY,EAAC,sBAAsB,EAAC,UAAU,EAAC,8BAA8B,EAAC,KAAK,EAAC,uBAAuB,EAAC,QAAQ,EAAC,QAAQ,EAAC,GAAG,WAAW,EAAC,aAAE,eAAK,KAAK,EAAE,EAAC,OAAO,EAAC,MAAM,EAAC,UAAU,EAAC,QAAQ,EAAC,cAAc,EAAC,eAAe,EAAC,GAAG,EAAC,qBAAqB,EAAC,OAAO,EAAC,qBAAqB,EAAC,aAAE,yBAAO,IAAI,CAAC,QAAQ,IAAE,IAAI,CAAC,KAAK,GAAQ,EAAC,UAAU,IAAO,EAAA,cAAK,QAAQ,EAAE,CAAC,gBAAc,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,EAAC,MAAM,EAAC,CAAC,EAAC,OAAO,EAAC,qBAAqB,EAAC,SAAS,EAAC,MAAM,EAAC,UAAU,EAAC,IAAI,CAAC,IAAI,CAAA,CAAC,CAAA,UAAU,CAAA,CAAC,CAAA,KAAK,EAAC,YAAY,EAAC,IAAI,CAAC,IAAI,CAAA,CAAC,CAAA,UAAU,CAAA,CAAC,CAAA,SAAS,EAAC,YAAE,yBAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAC,KAAK,EAAC,EAAE,CAAA,eAAkB,KAAK,EAAE,EAAC,KAAK,EAAC,MAAM,CAAC,KAAK,CAAC,IAAI,IAAE,OAAO,CAAC,EAAC,YAAG,KAAK,CAAC,IAAI,IAA9D,KAAK,CAAiE,CAAC,GAAQ,GAAM,IAAU,CAAC,CAAA,CAAC","sourcesContent":["import type {ReactNode} from 'react';\nimport {resolveCodeBlock,type CodeBlockDescriptor} from '@hjmds/design-contracts/code-block';\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\nexport type CodeBlockProps=CodeBlockDescriptor & Readonly<{\n copyAction?:ReactNode;\n /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */\n layoutStyle?: HjmCompositionStyleProp;\n}>;\nconst colors={plain:'var(--hjm-color-text)',keyword:'var(--hjm-color-content-brand)',string:'var(--hjm-color-text)',comment:'var(--hjm-color-text-muted)',number:'var(--hjm-color-content-brand)'};\n/** Token text is rendered as React text, never executable HTML. */\nexport function CodeBlock({copyAction,layoutStyle,...descriptor}:CodeBlockProps){const spec=resolveCodeBlock(descriptor);return <section aria-label={spec.label} style={{minWidth:0,borderRadius:'var(--hjm-radius-lg)',background:'var(--hjm-color-surface-alt)',color:'var(--hjm-color-text)',overflow:'hidden',...layoutStyle}}><div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:'var(--hjm-space-sm)',padding:'var(--hjm-space-md)'}}><span>{spec.language??spec.label}</span>{copyAction}</div><pre tabIndex={0} aria-label={spec.label} style={{margin:0,padding:'var(--hjm-space-md)',overflowX:'auto',whiteSpace:spec.wrap?'pre-wrap':'pre',overflowWrap:spec.wrap?'anywhere':undefined}}><code>{spec.tokens.map((token,index)=><span key={index} style={{color:colors[token.tone??'plain']}}>{token.text}</span>)}</code></pre></section>;}\n"]}
1
+ {"version":3,"file":"code-block.js","sourceRoot":"","sources":["../src/code-block.tsx"],"names":[],"mappings":";AACA,OAAO,EAAC,gBAAgB,EAA0B,MAAM,oCAAoC,CAAC;AAO7F,MAAM,MAAM,GAAC,EAAC,KAAK,EAAC,uBAAuB,EAAC,OAAO,EAAC,gCAAgC,EAAC,MAAM,EAAC,uBAAuB,EAAC,OAAO,EAAC,6BAA6B,EAAC,MAAM,EAAC,gCAAgC,EAAC,CAAC;AACnM,mEAAmE;AACnE,sFAAsF;AACtF,iHAAiH;AACjH,MAAM,UAAU,SAAS,CAAC,EAAC,UAAU,EAAC,WAAW,EAAC,GAAG,UAAU,EAAgB,IAAE,MAAM,IAAI,GAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAA,OAAO,iCAAqB,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,EAAC,QAAQ,EAAC,CAAC,EAAC,YAAY,EAAC,sBAAsB,EAAC,UAAU,EAAC,8BAA8B,EAAC,KAAK,EAAC,uBAAuB,EAAC,QAAQ,EAAC,QAAQ,EAAC,GAAG,WAAW,EAAC,aAAE,eAAK,KAAK,EAAE,EAAC,OAAO,EAAC,MAAM,EAAC,UAAU,EAAC,QAAQ,EAAC,cAAc,EAAC,eAAe,EAAC,GAAG,EAAC,qBAAqB,EAAC,OAAO,EAAC,qBAAqB,EAAC,aAAE,yBAAO,IAAI,CAAC,QAAQ,IAAE,IAAI,CAAC,KAAK,GAAQ,EAAC,UAAU,IAAO,EAAA,cAAK,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAE,CAAC,gBAAc,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,EAAC,MAAM,EAAC,CAAC,EAAC,OAAO,EAAC,qBAAqB,EAAC,UAAU,EAAC,sDAAsD,EAAC,QAAQ,EAAC,2BAA2B,EAAC,UAAU,EAAC,kCAAkC,EAAC,SAAS,EAAC,MAAM,EAAC,UAAU,EAAC,IAAI,CAAC,IAAI,CAAA,CAAC,CAAA,UAAU,CAAA,CAAC,CAAA,KAAK,EAAC,YAAY,EAAC,IAAI,CAAC,IAAI,CAAA,CAAC,CAAA,UAAU,CAAA,CAAC,CAAA,SAAS,EAAC,YAAE,eAAM,KAAK,EAAE,EAAC,UAAU,EAAC,SAAS,EAAC,QAAQ,EAAC,SAAS,EAAC,UAAU,EAAC,SAAS,EAAC,YAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAC,KAAK,EAAC,EAAE,CAAA,eAAkB,KAAK,EAAE,EAAC,KAAK,EAAC,MAAM,CAAC,KAAK,CAAC,IAAI,IAAE,OAAO,CAAC,EAAC,YAAG,KAAK,CAAC,IAAI,IAA9D,KAAK,CAAiE,CAAC,GAAQ,GAAM,IAAU,CAAC,CAAA,CAAC","sourcesContent":["import type {ReactNode} from 'react';\nimport {resolveCodeBlock,type CodeBlockDescriptor} from '@hjmds/design-contracts/code-block';\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\nexport type CodeBlockProps=CodeBlockDescriptor & Readonly<{\n copyAction?:ReactNode;\n /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */\n layoutStyle?: HjmCompositionStyleProp;\n}>;\nconst colors={plain:'var(--hjm-color-text)',keyword:'var(--hjm-color-content-brand)',string:'var(--hjm-color-text)',comment:'var(--hjm-color-text-muted)',number:'var(--hjm-color-content-brand)'};\n/** Token text is rendered as React text, never executable HTML. */\n// Theme the complete selectable source once; nested syntax spans inherit its metrics.\n// Code syntax stays LTR even in an RTL shell, which otherwise moves trailing punctuation ahead of the statement.\nexport function CodeBlock({copyAction,layoutStyle,...descriptor}:CodeBlockProps){const spec=resolveCodeBlock(descriptor);return <section aria-label={spec.label} style={{minWidth:0,borderRadius:'var(--hjm-radius-lg)',background:'var(--hjm-color-surface-alt)',color:'var(--hjm-color-text)',overflow:'hidden',...layoutStyle}}><div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:'var(--hjm-space-sm)',padding:'var(--hjm-space-md)'}}><span>{spec.language??spec.label}</span>{copyAction}</div><pre dir=\"ltr\" tabIndex={0} aria-label={spec.label} style={{margin:0,padding:'var(--hjm-space-md)',fontFamily:'var(--hjm-font-family-code, ui-monospace, monospace)',fontSize:'var(--hjm-type-body-size)',lineHeight:'var(--hjm-type-body-line-height)',overflowX:'auto',whiteSpace:spec.wrap?'pre-wrap':'pre',overflowWrap:spec.wrap?'anywhere':undefined}}><code style={{fontFamily:'inherit',fontSize:'inherit',lineHeight:'inherit'}}>{spec.tokens.map((token,index)=><span key={index} style={{color:colors[token.tone??'plain']}}>{token.text}</span>)}</code></pre></section>;}\n"]}
@@ -6,6 +6,10 @@ export type CollapsibleProps = CollapsibleOpenState & Readonly<{
6
6
  trigger: ReactNode;
7
7
  children: ReactNode;
8
8
  disabled?: boolean;
9
+ /** Inline tools stay expanded without a disclosure trigger. */
10
+ presentation?: "disclosure" | "inline";
11
+ /** Preserve local input state while hidden; hidden content stays inaccessible. */
12
+ keepMounted?: boolean;
9
13
  className?: string;
10
14
  /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */
11
15
  layoutStyle?: HjmCompositionStyleProp;
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible.d.ts","sourceRoot":"","sources":["../src/collapsible.tsx"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAAmD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAExF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GACjD,QAAQ,CAAC;IACP,oEAAoE;IACpE,OAAO,EAAE,SAAS,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEL,eAAO,MAAM,WAAW,6GAyCtB,CAAC"}
1
+ {"version":3,"file":"collapsible.d.ts","sourceRoot":"","sources":["../src/collapsible.tsx"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAAmD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAExF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GACjD,QAAQ,CAAC;IACP,oEAAoE;IACpE,OAAO,EAAE,SAAS,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;IACvC,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEL,eAAO,MAAM,WAAW,6GAuCtB,CAAC"}
@@ -2,17 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { collapsibleRecipe, validateCollapsibleOpenState, } from "@hjmds/design-contracts/components/collapsible";
3
3
  import { forwardRef, useId, useState } from "react";
4
4
  import { classNames } from "./internal.js";
5
- export const Collapsible = forwardRef(function Collapsible({ trigger, children, disabled = false, className, layoutStyle, ...openState }, forwardedRef) {
5
+ export const Collapsible = forwardRef(function Collapsible({ trigger, children, disabled = false, presentation = "disclosure", keepMounted = false, className, layoutStyle, ...openState }, forwardedRef) {
6
6
  validateCollapsibleOpenState(openState);
7
7
  const controlled = openState.open !== undefined;
8
8
  const [internalOpen, setInternalOpen] = useState(openState.defaultOpen ?? false);
9
- const open = openState.open ?? internalOpen;
9
+ const open = presentation === "inline" || (openState.open ?? internalOpen);
10
10
  const id = `${useId().replaceAll(":", "")}-collapsible`;
11
- return (_jsxs("div", { ref: forwardedRef, className: classNames("hjm-collapsible", className), "data-state": open ? "open" : "closed", style: { ...layoutStyle, "--hjm-collapsible-gap": `${collapsibleRecipe.gap}px` }, children: [_jsxs("button", { type: "button", className: "hjm-collapsible__trigger", "aria-expanded": open, "aria-controls": `${id}-content`, disabled: disabled, onClick: () => {
11
+ return (_jsxs("div", { ref: forwardedRef, className: classNames("hjm-collapsible", className), "data-state": open ? "open" : "closed", style: { ...layoutStyle, "--hjm-collapsible-gap": `${collapsibleRecipe.gap}px` }, children: [_jsxs("button", { hidden: presentation === "inline", type: "button", className: "hjm-collapsible__trigger", "aria-expanded": open, "aria-controls": `${id}-content`, disabled: disabled, onClick: () => {
12
12
  const next = !open;
13
13
  if (!controlled)
14
14
  setInternalOpen(next);
15
15
  openState.onOpenChange?.(next);
16
- }, children: [trigger, _jsx("span", { "aria-hidden": "true", className: "hjm-collapsible__marker", children: open ? "▾" : "▸" })] }), open ? (_jsx("div", { id: `${id}-content`, role: "region", className: "hjm-collapsible__content", children: children })) : null] }));
16
+ }, children: [trigger, _jsx("span", { "aria-hidden": "true", className: "hjm-collapsible__marker", children: open ? "▾" : "▸" })] }), open || keepMounted ? (_jsx("div", { hidden: !open, id: `${id}-content`, role: "region", className: "hjm-collapsible__content", children: children })) : null] }));
17
17
  });
18
18
  //# sourceMappingURL=collapsible.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible.js","sourceRoot":"","sources":["../src/collapsible.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,iBAAiB,EACjB,4BAA4B,GAE7B,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAsC,MAAM,OAAO,CAAC;AACxF,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAc3C,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAmC,SAAS,WAAW,CAC1F,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,SAAS,EAAE,EAC7E,YAAY;IAEZ,4BAA4B,CAAC,SAAiC,CAAC,CAAC;IAChE,MAAM,UAAU,GAAG,SAAS,CAAC,IAAI,KAAK,SAAS,CAAC;IAChD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,WAAW,IAAI,KAAK,CAAC,CAAC;IACjF,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,IAAI,YAAY,CAAC;IAC5C,MAAM,EAAE,GAAG,GAAG,KAAK,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC;IACxD,OAAO,CACL,eACE,GAAG,EAAE,YAAY,EACjB,SAAS,EAAE,UAAU,CAAC,iBAAiB,EAAE,SAAS,CAAC,gBACvC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EACpC,KAAK,EAAE,EAAE,GAAG,WAAW,EAAE,uBAAuB,EAAE,GAAG,iBAAiB,CAAC,GAAG,IAAI,EAAmB,aAEjG,kBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,0BAA0B,mBACrB,IAAI,mBACJ,GAAG,EAAE,UAAU,EAC9B,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,GAAG,EAAE;oBACZ,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC;oBACnB,IAAI,CAAC,UAAU;wBAAE,eAAe,CAAC,IAAI,CAAC,CAAC;oBACvC,SAAS,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;gBACjC,CAAC,aAEA,OAAO,EACR,8BAAkB,MAAM,EAAC,SAAS,EAAC,yBAAyB,YAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAQ,IAC/E,EAMR,IAAI,CAAC,CAAC,CAAC,CACN,cAAK,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,0BAA0B,YAAE,QAAQ,GAAO,CAC9F,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import {\n collapsibleRecipe,\n validateCollapsibleOpenState,\n type CollapsibleOpenState,\n} from \"@hjmds/design-contracts/components/collapsible\";\nimport { forwardRef, useId, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { classNames } from \"./internal.js\";\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\n\nexport type CollapsibleProps = CollapsibleOpenState &\n Readonly<{\n /** The control's label; it also tells the user what will appear. */\n trigger: ReactNode;\n children: ReactNode;\n disabled?: boolean;\n className?: string;\n /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */\n layoutStyle?: HjmCompositionStyleProp;\n }>;\n\nexport const Collapsible = forwardRef<HTMLDivElement, CollapsibleProps>(function Collapsible(\n { trigger, children, disabled = false, className, layoutStyle, ...openState },\n forwardedRef,\n) {\n validateCollapsibleOpenState(openState as CollapsibleOpenState);\n const controlled = openState.open !== undefined;\n const [internalOpen, setInternalOpen] = useState(openState.defaultOpen ?? false);\n const open = openState.open ?? internalOpen;\n const id = `${useId().replaceAll(\":\", \"\")}-collapsible`;\n return (\n <div\n ref={forwardedRef}\n className={classNames(\"hjm-collapsible\", className)}\n data-state={open ? \"open\" : \"closed\"}\n style={{ ...layoutStyle, \"--hjm-collapsible-gap\": `${collapsibleRecipe.gap}px` } as CSSProperties}\n >\n <button\n type=\"button\"\n className=\"hjm-collapsible__trigger\"\n aria-expanded={open}\n aria-controls={`${id}-content`}\n disabled={disabled}\n onClick={() => {\n const next = !open;\n if (!controlled) setInternalOpen(next);\n openState.onOpenChange?.(next);\n }}\n >\n {trigger}\n <span aria-hidden=\"true\" className=\"hjm-collapsible__marker\">{open ? \"▾\" : \"▸\"}</span>\n </button>\n {/*\n Unmounted when closed rather than visually hidden: content left in the\n tree stays reachable by search and by a screen reader, which contradicts\n what the collapsed state says.\n */}\n {open ? (\n <div id={`${id}-content`} role=\"region\" className=\"hjm-collapsible__content\">{children}</div>\n ) : null}\n </div>\n );\n});\n"]}
1
+ {"version":3,"file":"collapsible.js","sourceRoot":"","sources":["../src/collapsible.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,iBAAiB,EACjB,4BAA4B,GAE7B,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAsC,MAAM,OAAO,CAAC;AACxF,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAkB3C,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAmC,SAAS,WAAW,CAC1F,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,KAAK,EAAE,YAAY,GAAG,YAAY,EAAE,WAAW,GAAG,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,SAAS,EAAE,EAC/H,YAAY;IAEZ,4BAA4B,CAAC,SAAiC,CAAC,CAAC;IAChE,MAAM,UAAU,GAAG,SAAS,CAAC,IAAI,KAAK,SAAS,CAAC;IAChD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,WAAW,IAAI,KAAK,CAAC,CAAC;IACjF,MAAM,IAAI,GAAG,YAAY,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,YAAY,CAAC,CAAC;IAC3E,MAAM,EAAE,GAAG,GAAG,KAAK,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC;IACxD,OAAO,CACL,eACE,GAAG,EAAE,YAAY,EACjB,SAAS,EAAE,UAAU,CAAC,iBAAiB,EAAE,SAAS,CAAC,gBACvC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EACpC,KAAK,EAAE,EAAE,GAAG,WAAW,EAAE,uBAAuB,EAAE,GAAG,iBAAiB,CAAC,GAAG,IAAI,EAAmB,aAEjG,kBACE,MAAM,EAAE,YAAY,KAAK,QAAQ,EACjC,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,0BAA0B,mBACrB,IAAI,mBACJ,GAAG,EAAE,UAAU,EAC9B,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,GAAG,EAAE;oBACZ,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC;oBACnB,IAAI,CAAC,UAAU;wBAAE,eAAe,CAAC,IAAI,CAAC,CAAC;oBACvC,SAAS,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;gBACjC,CAAC,aAEA,OAAO,EACR,8BAAkB,MAAM,EAAC,SAAS,EAAC,yBAAyB,YAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAQ,IAC/E,EAGR,IAAI,IAAI,WAAW,CAAC,CAAC,CAAC,CACrB,cAAK,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,0BAA0B,YAAE,QAAQ,GAAO,CAC7G,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import {\n collapsibleRecipe,\n validateCollapsibleOpenState,\n type CollapsibleOpenState,\n} from \"@hjmds/design-contracts/components/collapsible\";\nimport { forwardRef, useId, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { classNames } from \"./internal.js\";\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\n\nexport type CollapsibleProps = CollapsibleOpenState &\n Readonly<{\n /** The control's label; it also tells the user what will appear. */\n trigger: ReactNode;\n children: ReactNode;\n disabled?: boolean;\n /** Inline tools stay expanded without a disclosure trigger. */\n presentation?: \"disclosure\" | \"inline\";\n /** Preserve local input state while hidden; hidden content stays inaccessible. */\n keepMounted?: boolean;\n className?: string;\n /** Canonical layout-only placement on the root element. Controlled visual keys are excluded. */\n layoutStyle?: HjmCompositionStyleProp;\n }>;\n\nexport const Collapsible = forwardRef<HTMLDivElement, CollapsibleProps>(function Collapsible(\n { trigger, children, disabled = false, presentation = \"disclosure\", keepMounted = false, className, layoutStyle, ...openState },\n forwardedRef,\n) {\n validateCollapsibleOpenState(openState as CollapsibleOpenState);\n const controlled = openState.open !== undefined;\n const [internalOpen, setInternalOpen] = useState(openState.defaultOpen ?? false);\n const open = presentation === \"inline\" || (openState.open ?? internalOpen);\n const id = `${useId().replaceAll(\":\", \"\")}-collapsible`;\n return (\n <div\n ref={forwardedRef}\n className={classNames(\"hjm-collapsible\", className)}\n data-state={open ? \"open\" : \"closed\"}\n style={{ ...layoutStyle, \"--hjm-collapsible-gap\": `${collapsibleRecipe.gap}px` } as CSSProperties}\n >\n <button\n hidden={presentation === \"inline\"}\n type=\"button\"\n className=\"hjm-collapsible__trigger\"\n aria-expanded={open}\n aria-controls={`${id}-content`}\n disabled={disabled}\n onClick={() => {\n const next = !open;\n if (!controlled) setInternalOpen(next);\n openState.onOpenChange?.(next);\n }}\n >\n {trigger}\n <span aria-hidden=\"true\" className=\"hjm-collapsible__marker\">{open ? \"▾\" : \"▸\"}</span>\n </button>\n {/* Optional persistence keeps theme changes and closing tools from erasing local\n drafts. The HTML hidden boundary removes closed content from focus and speech. */}\n {open || keepMounted ? (\n <div hidden={!open} id={`${id}-content`} role=\"region\" className=\"hjm-collapsible__content\">{children}</div>\n ) : null}\n </div>\n );\n});\n"]}
@@ -0,0 +1,24 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ import { type CollectionRailItem, type CollectionRailDensity } from "@hjmds/design-contracts/collection-rail";
3
+ import type { HjmCompositionStyleProp } from "./composition-style.js";
4
+ export type CollectionRailLabels = Readonly<{
5
+ previous: string;
6
+ next: string;
7
+ navigation: string;
8
+ }>;
9
+ export type CollectionRailProps = Omit<HTMLAttributes<HTMLDivElement>, "children"> & Readonly<{
10
+ label: string;
11
+ items: readonly CollectionRailItem[];
12
+ renderItem: (item: CollectionRailItem) => ReactNode;
13
+ labels: CollectionRailLabels;
14
+ density?: CollectionRailDensity;
15
+ emptyContent?: ReactNode;
16
+ /** Initial scroll anchor, not selection. Later collection changes reconcile by stable ID. */
17
+ defaultStartKey?: string;
18
+ /** Observed leading item, independent of product selection/detail state. Empty lists report null. */
19
+ onStartKeyChange?: (key: string | null) => void;
20
+ layoutStyle?: HjmCompositionStyleProp;
21
+ }>;
22
+ /** Finite all-mounted list placement; Card, inputs and Dialog keep their own state. */
23
+ export declare function CollectionRail({ label, items, renderItem, labels, density, emptyContent, defaultStartKey, onStartKeyChange, layoutStyle, ...props }: CollectionRailProps): import("react").JSX.Element;
24
+ //# sourceMappingURL=collection-rail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-rail.d.ts","sourceRoot":"","sources":["../src/collection-rail.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/F,OAAO,EAG4B,KAAK,kBAAkB,EAAE,KAAK,qBAAqB,EACrF,MAAM,yCAAyC,CAAC;AAGjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACpG,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC;IAC5F,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAAC,UAAU,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,SAAS,CAAC;IACzG,MAAM,EAAE,oBAAoB,CAAC;IAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAAC,YAAY,CAAC,EAAE,SAAS,CAAC;IACxF,6FAA6F;IAC7F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qGAAqG;IACrG,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEH,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,OAA+C,EAChH,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAE,GAAG,KAAK,EAAE,EAAE,mBAAmB,+BAwG9F"}
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from "react";
3
+ import { collectionRailRecipe, validateCollectionRail, resolveCollectionRailLayout, resolveCollectionRailViewport, getCollectionRailTargetOffset, getCollectionRailItemOffset, getCollectionRailRevealOffset, getCollectionRailKeyboardIntent, } from "@hjmds/design-contracts/collection-rail";
4
+ import { Button } from "./actions.js";
5
+ import { useOptionalHjmTheme } from "./provider.js";
6
+ /** Finite all-mounted list placement; Card, inputs and Dialog keep their own state. */
7
+ export function CollectionRail({ label, items, renderItem, labels, density = collectionRailRecipe.defaults.density, emptyContent, defaultStartKey, onStartKeyChange, layoutStyle, ...props }) {
8
+ validateCollectionRail(items);
9
+ if (!label.trim() || Object.values(labels).some(value => !value.trim()))
10
+ throw new TypeError("CollectionRail labels must not be empty");
11
+ const theme = useOptionalHjmTheme(), direction = theme?.environment.direction ?? "ltr";
12
+ const gap = collectionRailRecipe.gap;
13
+ const viewportRef = useRef(null);
14
+ const anchor = useRef(defaultStartKey ?? items[0]?.id ?? null);
15
+ const initialized = useRef(false);
16
+ if (!initialized.current) {
17
+ validateCollectionRail(items, defaultStartKey);
18
+ initialized.current = true;
19
+ }
20
+ const callback = useRef(onStartKeyChange);
21
+ callback.current = onStartKeyChange;
22
+ const current = useRef({ layout: resolveCollectionRailLayout(items.length, 0, density, gap), offset: 0 });
23
+ const pendingOffset = useRef(null);
24
+ const [position, setPosition] = useState(current.current);
25
+ const identity = JSON.stringify(items.map(item => item.id));
26
+ const itemsRef = useRef(items);
27
+ itemsRef.current = items;
28
+ function publish(layout, offset) {
29
+ const resolved = resolveCollectionRailViewport(layout, offset);
30
+ const key = itemsRef.current[resolved.startIndex]?.id ?? null;
31
+ if (key !== anchor.current) {
32
+ anchor.current = key;
33
+ callback.current?.(key);
34
+ }
35
+ current.current = { layout, offset: resolved.offset };
36
+ setPosition(current.current);
37
+ }
38
+ function scrollToOffset(offset, animated) {
39
+ const viewport = viewportRef.current;
40
+ if (!viewport)
41
+ return;
42
+ const resolved = resolveCollectionRailViewport(current.current.layout, offset);
43
+ // CSS RTL scrollLeft is negative; clamp Safari rubber-banding through the
44
+ // shared logical resolver rather than reversing data or selection keys.
45
+ viewport.scrollTo({ left: direction === "rtl" ? -resolved.offset : resolved.offset,
46
+ behavior: animated && !theme?.environment.reducedMotion ? "smooth" : "instant" });
47
+ pendingOffset.current = animated && !theme?.environment.reducedMotion ? resolved.offset : null;
48
+ // Smooth scroll is a request, not an observed viewport. Publishing its
49
+ // destination now would disable the end action and report an unseen anchor.
50
+ publish(current.current.layout, direction === "rtl" ? -viewport.scrollLeft : viewport.scrollLeft);
51
+ }
52
+ useLayoutEffect(() => {
53
+ const viewport = viewportRef.current;
54
+ if (!viewport)
55
+ return;
56
+ let initialMeasurement = true;
57
+ function measure() {
58
+ const next = resolveCollectionRailLayout(itemsRef.current.length, viewport.clientWidth, density, gap);
59
+ // ResizeObserver also delivers an initial unchanged box. Re-aligning then
60
+ // would undo a focus reveal that happened after the layout effect.
61
+ if (!initialMeasurement && next.viewport === current.current.layout.viewport)
62
+ return;
63
+ initialMeasurement = false;
64
+ pendingOffset.current = null;
65
+ const index = Math.max(0, itemsRef.current.findIndex(item => item.id === anchor.current));
66
+ let offset = next.count ? getCollectionRailItemOffset(next, index) : 0;
67
+ const focused = document.activeElement?.closest("[data-collection-rail-item]");
68
+ const focusedIndex = focused && viewport.contains(focused)
69
+ ? itemsRef.current.findIndex(item => item.id === focused.getAttribute("data-collection-rail-item")) : -1;
70
+ // A narrower viewport cannot keep a distant focused action and the old
71
+ // leading item visible together; focus exposure takes priority over anchor alignment.
72
+ if (focusedIndex >= 0)
73
+ offset = getCollectionRailRevealOffset(next, offset, focusedIndex);
74
+ current.current = { layout: next, offset };
75
+ // Width/direction changes keep the leading item's identity, and never
76
+ // key/remount its input or detail owner (docs/collection-rail.md).
77
+ viewport.scrollTo({ left: direction === "rtl" ? -offset : offset, behavior: "instant" });
78
+ publish(next, offset);
79
+ }
80
+ measure();
81
+ const observer = new ResizeObserver(measure);
82
+ observer.observe(viewport);
83
+ return () => observer.disconnect();
84
+ }, [identity, density, gap, direction]);
85
+ function navigate(intent) {
86
+ scrollToOffset(getCollectionRailTargetOffset(current.current.layout, pendingOffset.current ?? current.current.offset, intent), true);
87
+ }
88
+ const resolved = resolveCollectionRailViewport(position.layout, position.offset);
89
+ return _jsxs("div", { ...props, style: { ...props.style, ...layoutStyle, minWidth: 0 }, "data-collection-rail": true, "data-start-key": items[resolved.startIndex]?.id, dir: direction, onKeyDown: event => {
90
+ props.onKeyDown?.(event);
91
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
92
+ return;
93
+ // Item widgets own arrows/Home/End. Rail navigation belongs only to its
94
+ // viewport and explicit controls, not an input nested in a visible card.
95
+ if (event.target !== viewportRef.current && !(event.target instanceof Element && event.target.closest("[data-collection-rail-controls]")))
96
+ return;
97
+ const intent = getCollectionRailKeyboardIntent(event.key, direction);
98
+ if (intent) {
99
+ event.preventDefault();
100
+ navigate(intent);
101
+ }
102
+ }, children: [_jsxs("div", { ref: viewportRef, role: "list", "aria-label": label, tabIndex: 0, "data-collection-rail-viewport": true, style: { overflowX: "auto", overscrollBehaviorX: "contain", minWidth: 0 }, onPointerDown: () => { pendingOffset.current = null; }, onWheel: () => { pendingOffset.current = null; }, onScroll: event => {
103
+ const offset = direction === "rtl" ? -event.currentTarget.scrollLeft : event.currentTarget.scrollLeft;
104
+ if (pendingOffset.current !== null && Math.abs(offset - pendingOffset.current) <= 0.5)
105
+ pendingOffset.current = null;
106
+ publish(current.current.layout, offset);
107
+ }, children: [_jsx("div", { style: { display: "flex", gap, width: "max-content", minWidth: "100%", paddingBlock: "var(--hjm-space-xs)" }, children: items.map((item, index) => _jsx("div", { role: "listitem", "data-collection-rail-item": item.id, style: { flex: "0 0 auto", width: position.layout.itemWidth || "auto", minWidth: 0 }, onFocusCapture: () => scrollToOffset(getCollectionRailRevealOffset(current.current.layout, current.current.offset, index), false), children: renderItem(item) }, item.id)) }), items.length === 0 ? emptyContent : null] }), _jsxs("div", { "data-collection-rail-controls": true, role: "group", "aria-label": labels.navigation, style: { display: "flex", flexWrap: "wrap", justifyContent: "flex-end", gap: "var(--hjm-space-sm)", marginBlockStart: "var(--hjm-space-sm)" }, children: [_jsx(Button, { tone: "ghost", "aria-disabled": resolved.atStart, onClick: () => navigate("previous"), children: labels.previous }), _jsx(Button, { tone: "ghost", "aria-disabled": resolved.atEnd, onClick: () => navigate("next"), children: labels.next })] })] });
108
+ }
109
+ //# sourceMappingURL=collection-rail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-rail.js","sourceRoot":"","sources":["../src/collection-rail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useLayoutEffect, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport {\n collectionRailRecipe, validateCollectionRail, resolveCollectionRailLayout, resolveCollectionRailViewport,\n getCollectionRailTargetOffset, getCollectionRailItemOffset, getCollectionRailRevealOffset,\n getCollectionRailKeyboardIntent, type CollectionRailItem, type CollectionRailDensity, type CollectionRailLayout,\n} from \"@hjmds/design-contracts/collection-rail\";\nimport { Button } from \"./actions.js\";\nimport { useOptionalHjmTheme } from \"./provider.js\";\nimport type { HjmCompositionStyleProp } from \"./composition-style.js\";\n\nexport type CollectionRailLabels = Readonly<{ previous: string; next: string; navigation: string }>;\nexport type CollectionRailProps = Omit<HTMLAttributes<HTMLDivElement>, \"children\"> & Readonly<{\n label: string; items: readonly CollectionRailItem[]; renderItem: (item: CollectionRailItem) => ReactNode;\n labels: CollectionRailLabels; density?: CollectionRailDensity; emptyContent?: ReactNode;\n /** Initial scroll anchor, not selection. Later collection changes reconcile by stable ID. */\n defaultStartKey?: string;\n /** Observed leading item, independent of product selection/detail state. Empty lists report null. */\n onStartKeyChange?: (key: string | null) => void;\n layoutStyle?: HjmCompositionStyleProp;\n}>;\n\n/** Finite all-mounted list placement; Card, inputs and Dialog keep their own state. */\nexport function CollectionRail({ label, items, renderItem, labels, density = collectionRailRecipe.defaults.density,\n emptyContent, defaultStartKey, onStartKeyChange, layoutStyle, ...props }: CollectionRailProps) {\n validateCollectionRail(items);\n if (!label.trim() || Object.values(labels).some(value => !value.trim())) throw new TypeError(\"CollectionRail labels must not be empty\");\n const theme = useOptionalHjmTheme(), direction = theme?.environment.direction ?? \"ltr\";\n const gap = collectionRailRecipe.gap;\n const viewportRef = useRef<HTMLDivElement>(null);\n const anchor = useRef<string | null>(defaultStartKey ?? items[0]?.id ?? null);\n const initialized = useRef(false);\n if (!initialized.current) { validateCollectionRail(items, defaultStartKey); initialized.current = true; }\n const callback = useRef(onStartKeyChange); callback.current = onStartKeyChange;\n const current = useRef({ layout: resolveCollectionRailLayout(items.length, 0, density, gap), offset: 0 });\n const pendingOffset = useRef<number | null>(null);\n const [position, setPosition] = useState(current.current);\n const identity = JSON.stringify(items.map(item => item.id));\n const itemsRef = useRef(items); itemsRef.current = items;\n\n function publish(layout: CollectionRailLayout, offset: number) {\n const resolved = resolveCollectionRailViewport(layout, offset);\n const key = itemsRef.current[resolved.startIndex]?.id ?? null;\n if (key !== anchor.current) { anchor.current = key; callback.current?.(key); }\n current.current = { layout, offset: resolved.offset };\n setPosition(current.current);\n }\n function scrollToOffset(offset: number, animated: boolean) {\n const viewport = viewportRef.current;\n if (!viewport) return;\n const resolved = resolveCollectionRailViewport(current.current.layout, offset);\n // CSS RTL scrollLeft is negative; clamp Safari rubber-banding through the\n // shared logical resolver rather than reversing data or selection keys.\n viewport.scrollTo({ left: direction === \"rtl\" ? -resolved.offset : resolved.offset,\n behavior: animated && !theme?.environment.reducedMotion ? \"smooth\" : \"instant\" });\n pendingOffset.current = animated && !theme?.environment.reducedMotion ? resolved.offset : null;\n // Smooth scroll is a request, not an observed viewport. Publishing its\n // destination now would disable the end action and report an unseen anchor.\n publish(current.current.layout, direction === \"rtl\" ? -viewport.scrollLeft : viewport.scrollLeft);\n }\n useLayoutEffect(() => {\n const viewport = viewportRef.current;\n if (!viewport) return;\n let initialMeasurement = true;\n function measure() {\n const next = resolveCollectionRailLayout(itemsRef.current.length, viewport!.clientWidth, density, gap);\n // ResizeObserver also delivers an initial unchanged box. Re-aligning then\n // would undo a focus reveal that happened after the layout effect.\n if (!initialMeasurement && next.viewport === current.current.layout.viewport) return;\n initialMeasurement = false;\n pendingOffset.current = null;\n const index = Math.max(0, itemsRef.current.findIndex(item => item.id === anchor.current));\n let offset = next.count ? getCollectionRailItemOffset(next, index) : 0;\n const focused = document.activeElement?.closest(\"[data-collection-rail-item]\");\n const focusedIndex = focused && viewport!.contains(focused)\n ? itemsRef.current.findIndex(item => item.id === focused.getAttribute(\"data-collection-rail-item\")) : -1;\n // A narrower viewport cannot keep a distant focused action and the old\n // leading item visible together; focus exposure takes priority over anchor alignment.\n if (focusedIndex >= 0) offset = getCollectionRailRevealOffset(next, offset, focusedIndex);\n current.current = { layout: next, offset };\n // Width/direction changes keep the leading item's identity, and never\n // key/remount its input or detail owner (docs/collection-rail.md).\n viewport!.scrollTo({ left: direction === \"rtl\" ? -offset : offset, behavior: \"instant\" });\n publish(next, offset);\n }\n measure();\n const observer = new ResizeObserver(measure); observer.observe(viewport);\n return () => observer.disconnect();\n }, [identity, density, gap, direction]);\n\n function navigate(intent: \"previous\" | \"next\" | \"first\" | \"last\") {\n scrollToOffset(getCollectionRailTargetOffset(current.current.layout, pendingOffset.current ?? current.current.offset, intent), true);\n }\n const resolved = resolveCollectionRailViewport(position.layout, position.offset);\n return <div {...props} style={{ ...props.style, ...layoutStyle, minWidth: 0 }}\n data-collection-rail data-start-key={items[resolved.startIndex]?.id} dir={direction}\n onKeyDown={event => {\n props.onKeyDown?.(event);\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n // Item widgets own arrows/Home/End. Rail navigation belongs only to its\n // viewport and explicit controls, not an input nested in a visible card.\n if (event.target !== viewportRef.current && !(event.target instanceof Element && event.target.closest(\"[data-collection-rail-controls]\"))) return;\n const intent = getCollectionRailKeyboardIntent(event.key, direction);\n if (intent) { event.preventDefault(); navigate(intent); }\n }}>\n <div ref={viewportRef} role=\"list\" aria-label={label} tabIndex={0} data-collection-rail-viewport\n style={{ overflowX: \"auto\", overscrollBehaviorX: \"contain\", minWidth: 0 }}\n onPointerDown={() => { pendingOffset.current = null; }} onWheel={() => { pendingOffset.current = null; }}\n onScroll={event => {\n const offset = direction === \"rtl\" ? -event.currentTarget.scrollLeft : event.currentTarget.scrollLeft;\n if (pendingOffset.current !== null && Math.abs(offset - pendingOffset.current) <= 0.5) pendingOffset.current = null;\n publish(current.current.layout, offset);\n }}>\n <div style={{ display: \"flex\", gap, width: \"max-content\", minWidth: \"100%\", paddingBlock: \"var(--hjm-space-xs)\" }}>\n {items.map((item, index) => <div key={item.id} role=\"listitem\" data-collection-rail-item={item.id}\n style={{ flex: \"0 0 auto\", width: position.layout.itemWidth || \"auto\", minWidth: 0 }}\n onFocusCapture={() => scrollToOffset(getCollectionRailRevealOffset(current.current.layout, current.current.offset, index), false)}>\n {renderItem(item)}\n </div>)}\n </div>\n {items.length === 0 ? emptyContent : null}\n </div>\n <div data-collection-rail-controls role=\"group\" aria-label={labels.navigation}\n style={{ display: \"flex\", flexWrap: \"wrap\", justifyContent: \"flex-end\", gap: \"var(--hjm-space-sm)\", marginBlockStart: \"var(--hjm-space-sm)\" }}>\n <Button tone=\"ghost\" aria-disabled={resolved.atStart} onClick={() => navigate(\"previous\")}>{labels.previous}</Button>\n <Button tone=\"ghost\" aria-disabled={resolved.atEnd} onClick={() => navigate(\"next\")}>{labels.next}</Button>\n </div>\n </div>;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../src/command-palette.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,6BAA6B,EAElC,KAAK,wBAAwB,EAC7B,KAAK,2BAA2B,EAEhC,KAAK,+BAA+B,EACpC,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EAC1B,MAAM,oDAAoD,CAAC;AAO5D,OAAO,EASL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAA0D,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AA4CzG,MAAM,MAAM,mBAAmB,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;IACtE,UAAU,EAAE,wBAAwB,CAAC;IACrC,MAAM,EAAE,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,UAAU,EAAE,6BAA6B,CAAC,GAAG,CAAC,CAAC;IAC/C,iFAAiF;IACjF,sBAAsB,CAAC,EAAE,6BAA6B,CAAC,GAAG,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,UAAU,CAAC,EAAE,wBAAwB,CAAC;IACtC,aAAa,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,+BAA+B,KAAK,IAAI,CAAC;IACjF,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,SAAS,CAAC;IAC3C,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH,eAAO,MAAM,cAAc,EAsMrB,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAChC,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,KAClE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../src/command-palette.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,6BAA6B,EAElC,KAAK,wBAAwB,EAC7B,KAAK,2BAA2B,EAEhC,KAAK,+BAA+B,EACpC,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EAC1B,MAAM,oDAAoD,CAAC;AAO5D,OAAO,EASL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAA0D,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AA4CzG,MAAM,MAAM,mBAAmB,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;IACtE,UAAU,EAAE,wBAAwB,CAAC;IACrC,MAAM,EAAE,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,UAAU,EAAE,6BAA6B,CAAC,GAAG,CAAC,CAAC;IAC/C,iFAAiF;IACjF,sBAAsB,CAAC,EAAE,6BAA6B,CAAC,GAAG,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,UAAU,CAAC,EAAE,wBAAwB,CAAC;IACtC,aAAa,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,+BAA+B,KAAK,IAAI,CAAC;IACjF,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,SAAS,CAAC;IAC3C,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH,eAAO,MAAM,cAAc,EAwMrB,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAChC,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,KAClE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
@@ -104,12 +104,14 @@ export const CommandPalette = forwardRef(function CommandPalette({ descriptor, s
104
104
  requestClose("outside"); }, children: _jsxs("div", { ref: composeRefs(contentRef, forwardedRef), id: id, role: "dialog", "aria-modal": "true", "aria-label": descriptor.accessibilityLabel, "data-hjm-modal-content": "", className: classNames("hjm-command-palette", className), style: { maxInlineSize: commandPaletteRecipe.content.maxWidth, maxBlockSize: commandPaletteRecipe.content.maxHeight }, children: [descriptor.closeLabel ? (_jsxs("div", { className: "hjm-command-palette__search-row", children: [searchInput, _jsx("button", { type: "button", className: "hjm-dialog__close hjm-command-palette__close", "aria-label": descriptor.closeLabel, onClick: () => requestClose("close-action"), children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] })) : searchInput, _jsxs("div", { id: `${id}-results`, role: "listbox", "aria-label": descriptor.accessibilityLabel, className: "hjm-command-palette__viewport", children: [results.status === null ? null : (
105
105
  // Once for the whole list, never per section.
106
106
  _jsx("p", { className: "hjm-command-palette__state", "data-kind": results.status.kind, role: results.status.kind === "error" ? "alert" : "status", children: results.status.message })), results.sections.map((section) => (_jsxs("div", { className: "hjm-command-palette__section", ...(section.name === undefined ? { role: "presentation" } : { role: "group", "aria-label": section.name }), children: [section.label ? _jsx("p", { className: "hjm-command-palette__section-label", "aria-hidden": "true", children: section.label }) : null, section.items.map((item) => {
107
+ // Positioning or filtering can enter a stationary cursor;
108
+ // only mouse movement should override keyboard navigation.
107
109
  const inert = item.disabled === true || !results.current;
108
110
  return (_jsxs("div", { id: `${id}-${item.id}`, role: "option", "aria-selected": item.id === active?.id, "aria-disabled": inert || undefined, className: "hjm-command-palette__item", "data-tone": item.tone, onMouseDown: (event) => {
109
111
  event.preventDefault();
110
112
  if (!inert)
111
113
  activate(item.id, "pointer");
112
- }, onMouseEnter: () => { if (!inert)
114
+ }, onMouseMove: () => { if (!inert)
113
115
  setActiveId(item.id); }, children: [renderLeading?.(item.id), _jsxs("span", { className: "hjm-command-palette__copy", children: [_jsx("span", { children: item.label }), item.description ? _jsx("span", { className: "hjm-command-palette__description", children: item.description }) : null] }), item.shortcut ? _jsx("kbd", { className: "hjm-command-palette__shortcut", children: item.shortcut }) : null] }, item.id));
114
116
  })] }, section.id)))] })] }) }) })] }));
115
117
  });