@podoba/react 0.0.34 → 0.0.35

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 (67) hide show
  1. package/package.json +3 -3
  2. package/src/components/asset-masonry-grid.examples.tsx +9 -0
  3. package/src/components/asset-masonry-grid.tsx +62 -0
  4. package/src/components/asset-selection-surface.examples.tsx +32 -0
  5. package/src/components/asset-selection-surface.tsx +184 -0
  6. package/src/components/brand-page-header.tsx +60 -29
  7. package/src/components/button.tsx +27 -4
  8. package/src/components/combobox.tsx +1 -1
  9. package/src/components/compact-action-button.examples.tsx +7 -0
  10. package/src/components/compact-action-button.tsx +21 -0
  11. package/src/components/compact-settings-dialog.examples.tsx +15 -0
  12. package/src/components/compact-settings-dialog.tsx +48 -0
  13. package/src/components/context-action-glyph.examples.tsx +9 -0
  14. package/src/components/context-action-glyph.tsx +61 -0
  15. package/src/components/context-menu.tsx +191 -107
  16. package/src/components/context-search-panel.examples.tsx +9 -0
  17. package/src/components/context-search-panel.tsx +50 -0
  18. package/src/components/csv-binding-presentation.examples.tsx +14 -0
  19. package/src/components/csv-binding-presentation.tsx +40 -0
  20. package/src/components/dashboard-grid.tsx +1 -1
  21. package/src/components/date-picker.tsx +5 -3
  22. package/src/components/date-selection-calendar.examples.tsx +8 -0
  23. package/src/components/date-selection-calendar.tsx +47 -0
  24. package/src/components/delivery/send-to-print-modal.tsx +351 -84
  25. package/src/components/dialog-action-button.examples.tsx +7 -0
  26. package/src/components/dialog-action-button.tsx +22 -0
  27. package/src/components/dialog.tsx +32 -12
  28. package/src/components/document-upload-panel.examples.tsx +12 -0
  29. package/src/components/document-upload-panel.tsx +48 -0
  30. package/src/components/dropdown-menu.tsx +5 -3
  31. package/src/components/empty-panel-action.examples.tsx +7 -0
  32. package/src/components/empty-panel-action.tsx +8 -0
  33. package/src/components/field-appearance.ts +14 -0
  34. package/src/components/focus-field.tsx +3 -3
  35. package/src/components/icons.tsx +30 -0
  36. package/src/components/input.examples.tsx +8 -0
  37. package/src/components/input.tsx +13 -5
  38. package/src/components/media-asset-panel.examples.tsx +11 -0
  39. package/src/components/media-asset-panel.tsx +58 -0
  40. package/src/components/media-gallery.examples.tsx +12 -0
  41. package/src/components/media-gallery.tsx +53 -0
  42. package/src/components/media-settings-dialog.examples.tsx +21 -0
  43. package/src/components/media-settings-dialog.tsx +83 -0
  44. package/src/components/preview-info-card.examples.tsx +18 -0
  45. package/src/components/preview-info-card.tsx +27 -0
  46. package/src/components/reload-icon.examples.tsx +7 -0
  47. package/src/components/reload-icon.tsx +6 -0
  48. package/src/components/request-changes-modal.examples.tsx +26 -0
  49. package/src/components/request-changes-modal.tsx +36 -19
  50. package/src/components/rich-text-editor.tsx +1 -1
  51. package/src/components/select.examples.tsx +17 -0
  52. package/src/components/select.tsx +78 -22
  53. package/src/components/settings-dialog-surface.examples.tsx +44 -0
  54. package/src/components/settings-dialog-surface.tsx +240 -0
  55. package/src/components/subtle.tsx +64 -8
  56. package/src/components/table.examples.tsx +9 -0
  57. package/src/components/table.tsx +29 -12
  58. package/src/components/template-catalog-card.examples.tsx +25 -0
  59. package/src/components/template-catalog-card.tsx +178 -0
  60. package/src/components/text.tsx +14 -1
  61. package/src/components/textarea.examples.tsx +8 -0
  62. package/src/components/textarea.tsx +16 -6
  63. package/src/components/tile.tsx +16 -17
  64. package/src/editor/block-editor.tsx +56 -14
  65. package/src/index.ts +19 -0
  66. package/src/layout/app-shell.tsx +13 -9
  67. package/src/layout/topbar.tsx +9 -12
@@ -0,0 +1,9 @@
1
+ import { ContextActionGlyph, CONTEXT_ACTION_GLYPHS } from './context-action-glyph'
2
+ import { Button } from './button'
3
+ export const examples = {
4
+ default: () => <ContextActionGlyph kind="edit" />,
5
+ variants: () => <>{CONTEXT_ACTION_GLYPHS.map(kind => <ContextActionGlyph key={kind} kind={kind} />)}</>,
6
+ sizes: () => <><ContextActionGlyph kind="settings" /><ContextActionGlyph kind="settings" className="size-5" /></>,
7
+ states: () => <><Button><ContextActionGlyph kind="open" />Open</Button><Button isDisabled><ContextActionGlyph kind="archive" />Archive</Button></>,
8
+ }
9
+ export const meta = { category: 'Primitives', description: 'Decorative compact action glyphs from the original Radix icon set; inherit the parent action color and accessible name.' }
@@ -0,0 +1,61 @@
1
+ /* Glyph paths from @radix-ui/react-icons 1.3.2 (same artwork as original Manager).
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2022 WorkOS
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ import type { SVGProps } from 'react'
25
+ import { uic } from '../utils/uic'
26
+
27
+ export const CONTEXT_ACTION_GLYPHS = ['rocket', 'edit', 'settings', 'copy', 'approve', 'check', 'request-changes', 'send', 'open', 'pin', 'share', 'favorite', 'archive', 'add', 'image', 'file-add', 'download', 'link', 'search', 'reload', 'enter'] as const
28
+ export type ContextActionGlyphKind = (typeof CONTEXT_ACTION_GLYPHS)[number]
29
+ const paths: Record<ContextActionGlyphKind, string> = {
30
+ rocket: "M6.85357 3.85355L7.65355 3.05353C8.2981 2.40901 9.42858 1.96172 10.552 1.80125C11.1056 1.72217 11.6291 1.71725 12.0564 1.78124C12.4987 1.84748 12.7698 1.97696 12.8965 2.10357C13.0231 2.23018 13.1526 2.50125 13.2188 2.94357C13.2828 3.37086 13.2779 3.89439 13.1988 4.44801C13.0383 5.57139 12.591 6.70188 11.9464 7.34645L7.49999 11.7929L6.35354 10.6465C6.15827 10.4512 5.84169 10.4512 5.64643 10.6465C5.45117 10.8417 5.45117 11.1583 5.64643 11.3536L7.14644 12.8536C7.34171 13.0488 7.65829 13.0488 7.85355 12.8536L8.40073 12.3064L9.57124 14.2572C9.65046 14.3893 9.78608 14.4774 9.9389 14.4963C10.0917 14.5151 10.2447 14.4624 10.3535 14.3536L12.3535 12.3536C12.4648 12.2423 12.5172 12.0851 12.495 11.9293L12.0303 8.67679L12.6536 8.05355C13.509 7.19808 14.0117 5.82855 14.1887 4.58943C14.2784 3.9618 14.2891 3.33847 14.2078 2.79546C14.1287 2.26748 13.9519 1.74482 13.6035 1.39645C13.2552 1.04809 12.7325 0.871332 12.2045 0.792264C11.6615 0.710945 11.0382 0.721644 10.4105 0.8113C9.17143 0.988306 7.80189 1.491 6.94644 2.34642L6.32322 2.96968L3.07071 2.50504C2.91492 2.48278 2.75773 2.53517 2.64645 2.64646L0.646451 4.64645C0.537579 4.75533 0.484938 4.90829 0.50375 5.0611C0.522563 5.21391 0.61073 5.34954 0.742757 5.42876L2.69364 6.59928L2.14646 7.14645C2.0527 7.24022 2.00002 7.3674 2.00002 7.50001C2.00002 7.63261 2.0527 7.75979 2.14646 7.85356L3.64647 9.35356C3.84173 9.54883 4.15831 9.54883 4.35357 9.35356C4.54884 9.1583 4.54884 8.84172 4.35357 8.64646L3.20712 7.50001L3.85357 6.85356L6.85357 3.85355ZM10.0993 13.1936L9.12959 11.5775L11.1464 9.56067L11.4697 11.8232L10.0993 13.1936ZM3.42251 5.87041L5.43935 3.85356L3.17678 3.53034L1.80638 4.90074L3.42251 5.87041ZM2.35356 10.3535C2.54882 10.1583 2.54882 9.8417 2.35356 9.64644C2.1583 9.45118 1.84171 9.45118 1.64645 9.64644L0.646451 10.6464C0.451188 10.8417 0.451188 11.1583 0.646451 11.3535C0.841713 11.5488 1.1583 11.5488 1.35356 11.3535L2.35356 10.3535ZM3.85358 11.8536C4.04884 11.6583 4.04885 11.3417 3.85359 11.1465C3.65833 10.9512 3.34175 10.9512 3.14648 11.1465L1.14645 13.1464C0.95119 13.3417 0.951187 13.6583 1.14645 13.8535C1.34171 14.0488 1.65829 14.0488 1.85355 13.8536L3.85358 11.8536ZM5.35356 13.3535C5.54882 13.1583 5.54882 12.8417 5.35356 12.6464C5.1583 12.4512 4.84171 12.4512 4.64645 12.6464L3.64645 13.6464C3.45119 13.8417 3.45119 14.1583 3.64645 14.3535C3.84171 14.5488 4.1583 14.5488 4.35356 14.3535L5.35356 13.3535ZM9.49997 6.74881C10.1897 6.74881 10.7488 6.1897 10.7488 5.5C10.7488 4.8103 10.1897 4.25118 9.49997 4.25118C8.81026 4.25118 8.25115 4.8103 8.25115 5.5C8.25115 6.1897 8.81026 6.74881 9.49997 6.74881Z",
31
+ 'file-add': "M3.5 2C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V4.70711L9.29289 2H3.5ZM2 2.5C2 1.67157 2.67157 1 3.5 1H9.5C9.63261 1 9.75979 1.05268 9.85355 1.14645L12.7803 4.07322C12.921 4.21388 13 4.40464 13 4.60355V12.5C13 13.3284 12.3284 14 11.5 14H3.5C2.67157 14 2 13.3284 2 12.5V2.5ZM4.75 7.5C4.75 7.22386 4.97386 7 5.25 7H7V5.25C7 4.97386 7.22386 4.75 7.5 4.75C7.77614 4.75 8 4.97386 8 5.25V7H9.75C10.0261 7 10.25 7.22386 10.25 7.5C10.25 7.77614 10.0261 8 9.75 8H8V9.75C8 10.0261 7.77614 10.25 7.5 10.25C7.22386 10.25 7 10.0261 7 9.75V8H5.25C4.97386 8 4.75 7.77614 4.75 7.5Z",
32
+ 'download': "M7.50005 1.04999C7.74858 1.04999 7.95005 1.25146 7.95005 1.49999V8.41359L10.1819 6.18179C10.3576 6.00605 10.6425 6.00605 10.8182 6.18179C10.994 6.35753 10.994 6.64245 10.8182 6.81819L7.81825 9.81819C7.64251 9.99392 7.35759 9.99392 7.18185 9.81819L4.18185 6.81819C4.00611 6.64245 4.00611 6.35753 4.18185 6.18179C4.35759 6.00605 4.64251 6.00605 4.81825 6.18179L7.05005 8.41359V1.49999C7.05005 1.25146 7.25152 1.04999 7.50005 1.04999ZM2.5 10C2.77614 10 3 10.2239 3 10.5V12C3 12.5539 3.44565 13 3.99635 13H11.0012C11.5529 13 12 12.5528 12 12V10.5C12 10.2239 12.2239 10 12.5 10C12.7761 10 13 10.2239 13 10.5V12C13 13.1041 12.1062 14 11.0012 14H3.99635C2.89019 14 2 13.103 2 12V10.5C2 10.2239 2.22386 10 2.5 10Z",
33
+ 'link': "M8.51194 3.00541C9.18829 2.54594 10.0435 2.53694 10.6788 2.95419C10.8231 3.04893 10.9771 3.1993 11.389 3.61119C11.8009 4.02307 11.9513 4.17714 12.046 4.32141C12.4633 4.95675 12.4543 5.81192 11.9948 6.48827C11.8899 6.64264 11.7276 6.80811 11.3006 7.23511L10.6819 7.85383C10.4867 8.04909 10.4867 8.36567 10.6819 8.56093C10.8772 8.7562 11.1938 8.7562 11.389 8.56093L12.0077 7.94221L12.0507 7.89929C12.4203 7.52976 12.6568 7.2933 12.822 7.0502C13.4972 6.05623 13.5321 4.76252 12.8819 3.77248C12.7233 3.53102 12.4922 3.30001 12.1408 2.94871L12.0961 2.90408L12.0515 2.85942C11.7002 2.508 11.4692 2.27689 11.2277 2.11832C10.2377 1.46813 8.94398 1.50299 7.95001 2.17822C7.70691 2.34336 7.47044 2.57991 7.1009 2.94955L7.058 2.99247L6.43928 3.61119C6.24401 3.80645 6.24401 4.12303 6.43928 4.31829C6.63454 4.51355 6.95112 4.51355 7.14638 4.31829L7.7651 3.69957C8.1921 3.27257 8.35757 3.11027 8.51194 3.00541ZM4.31796 7.14672C4.51322 6.95146 4.51322 6.63487 4.31796 6.43961C4.12269 6.24435 3.80611 6.24435 3.61085 6.43961L2.99213 7.05833L2.94922 7.10124C2.57957 7.47077 2.34303 7.70724 2.17788 7.95035C1.50265 8.94432 1.4678 10.238 2.11799 11.2281C2.27656 11.4695 2.50766 11.7005 2.8591 12.0518L2.90374 12.0965L2.94837 12.1411C3.29967 12.4925 3.53068 12.7237 3.77214 12.8822C4.76219 13.5324 6.05589 13.4976 7.04986 12.8223C7.29296 12.6572 7.52943 12.4206 7.89896 12.051L7.89897 12.051L7.94188 12.0081L8.5606 11.3894C8.75586 11.1941 8.75586 10.8775 8.5606 10.6823C8.36533 10.487 8.04875 10.487 7.85349 10.6823L7.23477 11.301C6.80777 11.728 6.6423 11.8903 6.48794 11.9951C5.81158 12.4546 4.95642 12.4636 4.32107 12.0464C4.17681 11.9516 4.02274 11.8012 3.61085 11.3894C3.19896 10.9775 3.0486 10.8234 2.95385 10.6791C2.53661 10.0438 2.54561 9.18863 3.00507 8.51227C3.10993 8.35791 3.27224 8.19244 3.69924 7.76544L4.31796 7.14672ZM9.62172 6.08558C9.81698 5.89032 9.81698 5.57373 9.62172 5.37847C9.42646 5.18321 9.10988 5.18321 8.91461 5.37847L5.37908 8.91401C5.18382 9.10927 5.18382 9.42585 5.37908 9.62111C5.57434 9.81637 5.89092 9.81637 6.08619 9.62111L9.62172 6.08558Z",
34
+ search: "M10 6.5C10 8.433 8.433 10 6.5 10C4.567 10 3 8.433 3 6.5C3 4.567 4.567 3 6.5 3C8.433 3 10 4.567 10 6.5ZM9.30884 10.0159C8.53901 10.6318 7.56251 11 6.5 11C4.01472 11 2 8.98528 2 6.5C2 4.01472 4.01472 2 6.5 2C8.98528 2 11 4.01472 11 6.5C11 7.56251 10.6318 8.53901 10.0159 9.30884L12.8536 12.1464C13.0488 12.3417 13.0488 12.6583 12.8536 12.8536C12.6583 13.0488 12.3417 13.0488 12.1464 12.8536L9.30884 10.0159Z",
35
+ reload: "M1.84998 7.49998C1.84998 4.66458 4.05979 1.84998 7.49998 1.84998C10.2783 1.84998 11.6515 3.9064 12.2367 5H10.5C10.2239 5 10 5.22386 10 5.5C10 5.77614 10.2239 6 10.5 6H13.5C13.7761 6 14 5.77614 14 5.5V2.5C14 2.22386 13.7761 2 13.5 2C13.2239 2 13 2.22386 13 2.5V4.31318C12.2955 3.07126 10.6659 0.849976 7.49998 0.849976C3.43716 0.849976 0.849976 4.18537 0.849976 7.49998C0.849976 10.8146 3.43716 14.15 7.49998 14.15C9.44382 14.15 11.0622 13.3808 12.2145 12.2084C12.8315 11.5806 13.3133 10.839 13.6418 10.0407C13.7469 9.78536 13.6251 9.49315 13.3698 9.38806C13.1144 9.28296 12.8222 9.40478 12.7171 9.66014C12.4363 10.3425 12.0251 10.9745 11.5013 11.5074C10.5295 12.4963 9.16504 13.15 7.49998 13.15C4.05979 13.15 1.84998 10.3354 1.84998 7.49998Z",
36
+ enter: "M4.5 1C4.22386 1 4 1.22386 4 1.5C4 1.77614 4.22386 2 4.5 2H12V13H4.5C4.22386 13 4 13.2239 4 13.5C4 13.7761 4.22386 14 4.5 14H12C12.5523 14 13 13.5523 13 13V2C13 1.44772 12.5523 1 12 1H4.5ZM6.60355 4.89645C6.40829 4.70118 6.09171 4.70118 5.89645 4.89645C5.70118 5.09171 5.70118 5.40829 5.89645 5.60355L7.29289 7H0.5C0.223858 7 0 7.22386 0 7.5C0 7.77614 0.223858 8 0.5 8H7.29289L5.89645 9.39645C5.70118 9.59171 5.70118 9.90829 5.89645 10.1036C6.09171 10.2988 6.40829 10.2988 6.60355 10.1036L8.85355 7.85355C9.04882 7.65829 9.04882 7.34171 8.85355 7.14645L6.60355 4.89645Z",
37
+ add: "M8 2.75C8 2.47386 7.77614 2.25 7.5 2.25C7.22386 2.25 7 2.47386 7 2.75V7H2.75C2.47386 7 2.25 7.22386 2.25 7.5C2.25 7.77614 2.47386 8 2.75 8H7V12.25C7 12.5261 7.22386 12.75 7.5 12.75C7.77614 12.75 8 12.5261 8 12.25V8H12.25C12.5261 8 12.75 7.77614 12.75 7.5C12.75 7.22386 12.5261 7 12.25 7H8V2.75Z",
38
+ image: "M2.5 1H12.5C13.3284 1 14 1.67157 14 2.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V2.5C1 1.67157 1.67157 1 2.5 1ZM2.5 2C2.22386 2 2 2.22386 2 2.5V8.3636L3.6818 6.6818C3.76809 6.59551 3.88572 6.54797 4.00774 6.55007C4.12975 6.55216 4.24568 6.60372 4.32895 6.69293L7.87355 10.4901L10.6818 7.6818C10.8575 7.50607 11.1425 7.50607 11.3182 7.6818L13 9.3636V2.5C13 2.22386 12.7761 2 12.5 2H2.5ZM2 12.5V9.6364L3.98887 7.64753L7.5311 11.4421L8.94113 13H2.5C2.22386 13 2 12.7761 2 12.5ZM12.5 13H10.155L8.48336 11.153L11 8.6364L13 10.6364V12.5C13 12.7761 12.7761 13 12.5 13ZM6.64922 5.5C6.64922 5.03013 7.03013 4.64922 7.5 4.64922C7.96987 4.64922 8.35078 5.03013 8.35078 5.5C8.35078 5.96987 7.96987 6.35078 7.5 6.35078C7.03013 6.35078 6.64922 5.96987 6.64922 5.5ZM7.5 3.74922C6.53307 3.74922 5.74922 4.53307 5.74922 5.5C5.74922 6.46693 6.53307 7.25078 7.5 7.25078C8.46693 7.25078 9.25078 6.46693 9.25078 5.5C9.25078 4.53307 8.46693 3.74922 7.5 3.74922Z",
39
+ open: "M3.64645 11.3536C3.45118 11.1583 3.45118 10.8417 3.64645 10.6465L10.2929 4L6 4C5.72386 4 5.5 3.77614 5.5 3.5C5.5 3.22386 5.72386 3 6 3L11.5 3C11.6326 3 11.7598 3.05268 11.8536 3.14645C11.9473 3.24022 12 3.36739 12 3.5L12 9.00001C12 9.27615 11.7761 9.50001 11.5 9.50001C11.2239 9.50001 11 9.27615 11 9.00001V4.70711L4.35355 11.3536C4.15829 11.5488 3.84171 11.5488 3.64645 11.3536Z",
40
+ pin: "M10.3285 1.13607C10.1332 0.940809 9.81662 0.940808 9.62136 1.13607C9.42609 1.33133 9.42609 1.64792 9.62136 1.84318L10.2744 2.49619L5.42563 6.13274L4.31805 5.02516C4.12279 4.8299 3.80621 4.8299 3.61095 5.02516C3.41569 5.22042 3.41569 5.537 3.61095 5.73226L5.02516 7.14648L6.08582 8.20714L2.81545 11.4775C2.62019 11.6728 2.62019 11.9894 2.81545 12.1846C3.01072 12.3799 3.3273 12.3799 3.52256 12.1846L6.79293 8.91425L7.85359 9.97491L9.2678 11.3891C9.46306 11.5844 9.77965 11.5844 9.97491 11.3891C10.1702 11.1939 10.1702 10.8773 9.97491 10.682L8.86733 9.57443L12.5039 4.7257L13.1569 5.37871C13.3522 5.57397 13.6687 5.57397 13.864 5.37871C14.0593 5.18345 14.0593 4.86687 13.864 4.6716L12.8033 3.61094L11.3891 2.19673L10.3285 1.13607ZM6.13992 6.84702L10.9887 3.21047L11.7896 4.01142L8.15305 8.86015L6.13992 6.84702Z",
41
+ share: "M5 7.50003C5 8.32845 4.32843 9.00003 3.5 9.00003C2.67157 9.00003 2 8.32845 2 7.50003C2 6.6716 2.67157 6.00003 3.5 6.00003C4.32843 6.00003 5 6.6716 5 7.50003ZM5.71313 8.66388C5.29445 9.45838 4.46048 10 3.5 10C2.11929 10 1 8.88074 1 7.50003C1 6.11931 2.11929 5.00003 3.5 5.00003C4.46048 5.00003 5.29445 5.54167 5.71313 6.33616L9.10424 4.21671C9.03643 3.98968 9 3.74911 9 3.50003C9 2.11932 10.1193 1.00003 11.5 1.00003C12.8807 1.00003 14 2.11932 14 3.50003C14 4.88074 12.8807 6.00003 11.5 6.00003C10.6915 6.00003 9.97264 5.61624 9.51566 5.0209L5.9853 7.22738C5.99502 7.31692 6 7.40789 6 7.50003C6 7.59216 5.99502 7.68312 5.9853 7.77267L9.51567 9.97915C9.97265 9.38382 10.6915 9.00003 11.5 9.00003C12.8807 9.00003 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5C9 11.2509 9.03643 11.0104 9.10425 10.7833L5.71313 8.66388ZM11.5 5.00003C12.3284 5.00003 13 4.32846 13 3.50003C13 2.6716 12.3284 2.00003 11.5 2.00003C10.6716 2.00003 10 2.6716 10 3.50003C10 4.32846 10.6716 5.00003 11.5 5.00003ZM13 11.5C13 12.3285 12.3284 13 11.5 13C10.6716 13 10 12.3285 10 11.5C10 10.6716 10.6716 10 11.5 10C12.3284 10 13 10.6716 13 11.5Z",
42
+ favorite: "M6.97942 1.25171L6.9585 1.30199L5.58662 4.60039C5.54342 4.70426 5.44573 4.77523 5.3336 4.78422L1.7727 5.0697L1.71841 5.07405L1.38687 5.10063L1.08608 5.12475C0.820085 5.14607 0.712228 5.47802 0.914889 5.65162L1.14406 5.84793L1.39666 6.06431L1.43802 6.09974L4.15105 8.42374C4.23648 8.49692 4.2738 8.61176 4.24769 8.72118L3.41882 12.196L3.40618 12.249L3.32901 12.5725L3.25899 12.866C3.19708 13.1256 3.47945 13.3308 3.70718 13.1917L3.9647 13.0344L4.24854 12.861L4.29502 12.8326L7.34365 10.9705C7.43965 10.9119 7.5604 10.9119 7.6564 10.9705L10.705 12.8326L10.7515 12.861L11.0354 13.0344L11.2929 13.1917C11.5206 13.3308 11.803 13.1256 11.7411 12.866L11.671 12.5725L11.5939 12.249L11.5812 12.196L10.7524 8.72118C10.7263 8.61176 10.7636 8.49692 10.849 8.42374L13.562 6.09974L13.6034 6.06431L13.856 5.84793L14.0852 5.65162C14.2878 5.47802 14.18 5.14607 13.914 5.12475L13.6132 5.10063L13.2816 5.07405L13.2274 5.0697L9.66645 4.78422C9.55432 4.77523 9.45663 4.70426 9.41343 4.60039L8.04155 1.30199L8.02064 1.25171L7.89291 0.944609L7.77702 0.665992C7.67454 0.419604 7.32551 0.419604 7.22303 0.665992L7.10715 0.944609L6.97942 1.25171ZM7.50003 2.60397L6.50994 4.98442C6.32273 5.43453 5.89944 5.74207 5.41351 5.78103L2.84361 5.98705L4.8016 7.66428C5.17183 7.98142 5.33351 8.47903 5.2204 8.95321L4.62221 11.461L6.8224 10.1171C7.23842 9.86302 7.76164 9.86302 8.17766 10.1171L10.3778 11.461L9.77965 8.95321C9.66654 8.47903 9.82822 7.98142 10.1984 7.66428L12.1564 5.98705L9.58654 5.78103C9.10061 5.74207 8.67732 5.43453 8.49011 4.98442L7.50003 2.60397Z",
43
+ archive: "M3.30902 1C2.93025 1 2.58398 1.214 2.41459 1.55279L1.05279 4.27639C1.01807 4.34582 1 4.42238 1 4.5V13C1 13.5523 1.44772 14 2 14H13C13.5523 14 14 13.5523 14 13V4.5C14 4.42238 13.9819 4.34582 13.9472 4.27639L12.5854 1.55281C12.416 1.21403 12.0698 1.00003 11.691 1.00003L7.5 1.00001L3.30902 1ZM3.30902 2L7 2.00001V4H2.30902L3.30902 2ZM8 4V2.00002L11.691 2.00003L12.691 4H8ZM7.5 5H13V13H2V5H7.5ZM5.5 7C5.22386 7 5 7.22386 5 7.5C5 7.77614 5.22386 8 5.5 8H9.5C9.77614 8 10 7.77614 10 7.5C10 7.22386 9.77614 7 9.5 7H5.5Z",
44
+ approve: "M7.49991 0.877045C3.84222 0.877045 0.877075 3.84219 0.877075 7.49988C0.877075 11.1575 3.84222 14.1227 7.49991 14.1227C11.1576 14.1227 14.1227 11.1575 14.1227 7.49988C14.1227 3.84219 11.1576 0.877045 7.49991 0.877045ZM1.82708 7.49988C1.82708 4.36686 4.36689 1.82704 7.49991 1.82704C10.6329 1.82704 13.1727 4.36686 13.1727 7.49988C13.1727 10.6329 10.6329 13.1727 7.49991 13.1727C4.36689 13.1727 1.82708 10.6329 1.82708 7.49988ZM10.1589 5.53774C10.3178 5.31191 10.2636 5.00001 10.0378 4.84109C9.81194 4.68217 9.50004 4.73642 9.34112 4.96225L6.51977 8.97154L5.35681 7.78706C5.16334 7.59002 4.84677 7.58711 4.64973 7.78058C4.45268 7.97404 4.44978 8.29061 4.64325 8.48765L6.22658 10.1003C6.33054 10.2062 6.47617 10.2604 6.62407 10.2483C6.77197 10.2363 6.90686 10.1591 6.99226 10.0377L10.1589 5.53774Z",
45
+ check: "M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48004 3.43039 8.08475 3.66276 7.82934C3.89513 7.57393 4.29042 7.55527 4.54583 7.78764L6.75292 9.79409L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z",
46
+ 'request-changes': "M0.877075 7.49988C0.877075 3.84219 3.84222 0.877045 7.49991 0.877045C11.1576 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1575 0.877075 7.49988ZM7.49991 1.82704C4.36689 1.82704 1.82708 4.36686 1.82708 7.49988C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 4.36686 10.6329 1.82704 7.49991 1.82704ZM9.85358 5.14644C10.0488 5.3417 10.0488 5.65829 9.85358 5.85355L8.20713 7.49999L9.85358 9.14644C10.0488 9.3417 10.0488 9.65829 9.85358 9.85355C9.65832 10.0488 9.34173 10.0488 9.14647 9.85355L7.50002 8.2071L5.85358 9.85355C5.65832 10.0488 5.34173 10.0488 5.14647 9.85355C4.95121 9.65829 4.95121 9.3417 5.14647 9.14644L6.79292 7.49999L5.14647 5.85355C4.95121 5.65829 4.95121 5.3417 5.14647 5.14644C5.34173 4.95118 5.65832 4.95118 5.85358 5.14644L7.50002 6.79289L9.14647 5.14644C9.34173 4.95118 9.65832 4.95118 9.85358 5.14644Z",
47
+ send: "M1.20308 1.04312C1.00481 0.954998 0.772341 1.0048 0.627577 1.16641C0.482813 1.32802 0.458794 1.56455 0.568117 1.75196L3.92115 7.50002L0.568117 13.2481C0.458794 13.4355 0.482813 13.672 0.627577 13.8336C0.772341 13.9952 1.00481 14.045 1.20308 13.9569L14.7031 7.95693C14.8836 7.87668 15 7.69762 15 7.50002C15 7.30243 14.8836 7.12337 14.7031 7.04312L1.20308 1.04312ZM4.84553 7.10002L2.21234 2.586L13.2689 7.50002L2.21234 12.414L4.84552 7.90002H9C9.22092 7.90002 9.4 7.72094 9.4 7.50002C9.4 7.27911 9.22092 7.10002 9 7.10002H4.84553Z",
48
+ edit: "M11.8536 1.14645C11.6583 0.951184 11.3417 0.951184 11.1465 1.14645L3.71455 8.57836C3.62459 8.66832 3.55263 8.77461 3.50251 8.89155L2.04044 12.303C1.9599 12.491 2.00189 12.709 2.14646 12.8536C2.29103 12.9981 2.50905 13.0401 2.69697 12.9596L6.10847 11.4975C6.2254 11.4474 6.3317 11.3754 6.42166 11.2855L13.8536 3.85355C14.0488 3.65829 14.0488 3.34171 13.8536 3.14645L11.8536 1.14645ZM4.42166 9.28547L11.5 2.20711L12.7929 3.5L5.71455 10.5784L4.21924 11.2192L3.78081 10.7808L4.42166 9.28547Z",
49
+ settings: "M7.07095 0.650238C6.67391 0.650238 6.32977 0.925096 6.24198 1.31231L6.0039 2.36247C5.6249 2.47269 5.26335 2.62363 4.92436 2.81013L4.01335 2.23585C3.67748 2.02413 3.23978 2.07312 2.95903 2.35386L2.35294 2.95996C2.0722 3.2407 2.0232 3.6784 2.23493 4.01427L2.80942 4.92561C2.62307 5.2645 2.47227 5.62594 2.36216 6.00481L1.31209 6.24287C0.924883 6.33065 0.650024 6.6748 0.650024 7.07183V7.92897C0.650024 8.32601 0.924883 8.67015 1.31209 8.75794L2.36228 8.99603C2.47246 9.375 2.62335 9.73652 2.80979 10.0755L2.2354 10.9867C2.02367 11.3225 2.07267 11.7602 2.35341 12.041L2.95951 12.6471C3.24025 12.9278 3.67795 12.9768 4.01382 12.7651L4.92506 12.1907C5.26384 12.377 5.62516 12.5278 6.0039 12.6379L6.24198 13.6881C6.32977 14.0753 6.67391 14.3502 7.07095 14.3502H7.92809C8.32512 14.3502 8.66927 14.0753 8.75705 13.6881L8.99505 12.6383C9.37411 12.5282 9.73573 12.3773 10.0748 12.1909L10.986 12.7653C11.3218 12.977 11.7595 12.928 12.0403 12.6473L12.6464 12.0412C12.9271 11.7604 12.9761 11.3227 12.7644 10.9869L12.1902 10.076C12.3768 9.73688 12.5278 9.37515 12.638 8.99596L13.6879 8.75794C14.0751 8.67015 14.35 8.32601 14.35 7.92897V7.07183C14.35 6.6748 14.0751 6.33065 13.6879 6.24287L12.6381 6.00488C12.528 5.62578 12.3771 5.26414 12.1906 4.92507L12.7648 4.01407C12.9766 3.6782 12.9276 3.2405 12.6468 2.95975L12.0407 2.35366C11.76 2.07292 11.3223 2.02392 10.9864 2.23565L10.0755 2.80989C9.73622 2.62328 9.37437 2.47229 8.99505 2.36209L8.75705 1.31231C8.66927 0.925096 8.32512 0.650238 7.92809 0.650238H7.07095ZM4.92053 3.81251C5.44724 3.44339 6.05665 3.18424 6.71543 3.06839L7.07095 1.50024H7.92809L8.28355 3.06816C8.94267 3.18387 9.5524 3.44302 10.0794 3.81224L11.4397 2.9547L12.0458 3.56079L11.1882 4.92117C11.5573 5.44798 11.8164 6.0575 11.9321 6.71638L13.5 7.07183V7.92897L11.932 8.28444C11.8162 8.94342 11.557 9.55301 11.1878 10.0798L12.0453 11.4402L11.4392 12.0462L10.0787 11.1886C9.55192 11.5576 8.94241 11.8166 8.28355 11.9323L7.92809 13.5002H7.07095L6.71543 11.932C6.0569 11.8162 5.44772 11.5572 4.92116 11.1883L3.56055 12.046L2.95445 11.4399L3.81213 10.0794C3.4431 9.55266 3.18403 8.94326 3.06825 8.2845L1.50002 7.92897V7.07183L3.06818 6.71632C3.18388 6.05765 3.44283 5.44833 3.81171 4.92165L2.95398 3.561L3.56008 2.95491L4.92053 3.81251ZM9.02496 7.50008C9.02496 8.34226 8.34223 9.02499 7.50005 9.02499C6.65786 9.02499 5.97513 8.34226 5.97513 7.50008C5.97513 6.65789 6.65786 5.97516 7.50005 5.97516C8.34223 5.97516 9.02496 6.65789 9.02496 7.50008ZM9.92496 7.50008C9.92496 8.83932 8.83929 9.92499 7.50005 9.92499C6.1608 9.92499 5.07513 8.83932 5.07513 7.50008C5.07513 6.16084 6.1608 5.07516 7.50005 5.07516C8.83929 5.07516 9.92496 6.16084 9.92496 7.50008Z",
50
+ copy: "M1 9.50006C1 10.3285 1.67157 11.0001 2.5 11.0001H4L4 10.0001H2.5C2.22386 10.0001 2 9.7762 2 9.50006L2 2.50006C2 2.22392 2.22386 2.00006 2.5 2.00006L9.5 2.00006C9.77614 2.00006 10 2.22392 10 2.50006V4.00002H5.5C4.67158 4.00002 4 4.67159 4 5.50002V12.5C4 13.3284 4.67158 14 5.5 14H12.5C13.3284 14 14 13.3284 14 12.5V5.50002C14 4.67159 13.3284 4.00002 12.5 4.00002H11V2.50006C11 1.67163 10.3284 1.00006 9.5 1.00006H2.5C1.67157 1.00006 1 1.67163 1 2.50006V9.50006ZM5 5.50002C5 5.22388 5.22386 5.00002 5.5 5.00002H12.5C12.7761 5.00002 13 5.22388 13 5.50002V12.5C13 12.7762 12.7761 13 12.5 13H5.5C5.22386 13 5 12.7762 5 12.5V5.50002Z",
51
+ }
52
+ const GlyphSurface = uic('svg', {
53
+ displayName: 'ContextActionGlyph',
54
+ baseClass: 'size-3.5 shrink-0',
55
+ })
56
+ /** Decorative, source-exact 15-unit icons for compact action menus. */
57
+ export function ContextActionGlyph({ kind, ...props }: SVGProps<SVGSVGElement> & { kind: ContextActionGlyphKind }) {
58
+ return <GlyphSurface viewBox="0 0 15 15" fill="none" aria-hidden="true" {...props}>
59
+ <path d={paths[kind]} fill="currentColor" fillRule="evenodd" clipRule="evenodd" />
60
+ </GlyphSurface>
61
+ }
@@ -1,4 +1,4 @@
1
- import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
1
+ import { type CSSProperties, type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
2
2
  import {
3
3
  Header,
4
4
  Menu as RACMenu,
@@ -15,10 +15,9 @@ import { uic } from '../utils/uic'
15
15
  * hand-rolling outside-click / Escape / viewport-clamping (its `EDGE_GAP = 8`). We
16
16
  * re-implement on a React Aria standalone `Popover` + `Menu`, which provides the
17
17
  * WAI-ARIA menu pattern for free: roving focus, arrow / Home / End navigation,
18
- * type-ahead, Escape-to-close, outside-press dismissal, focus restoration and —
19
- * crucially viewport-aware collision handling (the popover flips / shifts to stay
20
- * on-screen, replacing the manual edge clamp). The popover is anchored to a
21
- * zero-size element placed at the click coordinates.
18
+ * type-ahead, Escape-to-close, outside-press dismissal and focus restoration.
19
+ * The source cursor clamp is retained instead of RAC's anchor flip: close to the
20
+ * lower edge, the panel slides up only as far as needed to keep an 8px gutter.
22
21
  *
23
22
  * Two ways to drive it, sharing one renderer:
24
23
  * - **Controlled** (the documented API): pass `isOpen` + `position` + `onClose`
@@ -41,11 +40,13 @@ export interface ContextMenuItem {
41
40
  label: ReactNode
42
41
  icon?: ReactNode
43
42
  /**
44
- * Trailing pill (e.g. a count or status). When omitted, a disabled item falls
45
- * back to the `soonLabel` pill so unbuilt actions read as upcoming.
43
+ * Explicit trailing pill (e.g. a count or status). A disabled item only falls
44
+ * back to a pill when its caller supplies `soonLabel`; disabled is not "unbuilt".
46
45
  */
47
46
  badge?: ReactNode
48
47
  disabled?: boolean
48
+ /** Native hover hint for disabled/unavailable actions. */
49
+ title?: string
49
50
  /** Styled with the danger token (red text). */
50
51
  destructive?: boolean
51
52
  /** Filtered out before render. */
@@ -81,7 +82,7 @@ export interface ContextMenuProps {
81
82
  onClose: () => void
82
83
  /** Names the menu for assistive tech. Defaults to "Actions". */
83
84
  'aria-label'?: string
84
- /** Label shown on the fallback pill of disabled items. Defaults to "Soon". */
85
+ /** Optional fallback pill for disabled items. No implicit badge. */
85
86
  soonLabel?: ReactNode
86
87
  className?: string
87
88
  }
@@ -89,40 +90,14 @@ export interface ContextMenuProps {
89
90
  /** Wrapper API — wrap a target; the component owns the open state. */
90
91
  interface ContextMenuWrapperProps {
91
92
  groups: ContextMenuGroup[] | ContextMenuGroupsResolver
93
+ /** Leave native text/editing menus untouched for excluded targets. */
94
+ shouldOpen?: (target: HTMLElement) => boolean
92
95
  children: ReactNode
93
96
  soonLabel?: ReactNode
94
97
  'aria-label'?: string
95
98
  className?: string
96
99
  }
97
100
 
98
- /**
99
- * Lock page scroll while the menu is open — an open action menu should pin the view
100
- * (gs `ContextActionPanel` behaviour), not let the content scroll out from under the
101
- * cursor-anchored panel. Wheel + touch scrolling is prevented everywhere EXCEPT inside
102
- * `allowRef` (the panel itself, so a long/overflowing menu still scrolls). No dep on
103
- * react-aria's `usePreventScroll` (not a direct dependency here).
104
- */
105
- function useScrollLock(active: boolean, allowRef: React.RefObject<HTMLElement | null>): void {
106
- useEffect(() => {
107
- if (!active) {
108
- return
109
- }
110
- const prevent = (e: Event) => {
111
- const target = e.target as Node | null
112
- if (target && allowRef.current?.contains(target)) {
113
- return
114
- }
115
- e.preventDefault()
116
- }
117
- document.addEventListener('wheel', prevent, { passive: false })
118
- document.addEventListener('touchmove', prevent, { passive: false })
119
- return () => {
120
- document.removeEventListener('wheel', prevent)
121
- document.removeEventListener('touchmove', prevent)
122
- }
123
- }, [active, allowRef])
124
- }
125
-
126
101
  const itemId = (item: ContextMenuItem, fallback: number): string => item.id ?? item.key ?? String(fallback)
127
102
  const groupId = (group: ContextMenuGroup, fallback: number): string => group.id ?? group.key ?? String(fallback)
128
103
 
@@ -134,33 +109,144 @@ const visibleGroupsOf = (groups: ContextMenuGroup[]): ContextMenuGroup[] =>
134
109
 
135
110
  const MenuItem = uic(RACMenuItem, {
136
111
  displayName: 'ContextMenuItem',
137
- // Dark panel (gs `ContextActionPanel`): light text on the inverted surface, a
138
- // solid #2f2f2f wash + weight bump on hover / focus (gs `.itemButton:hover`).
112
+ // Source `.itemButton`: normal ink weight, with a weight bump on hover/focus.
113
+ // The panel is ALWAYS dark it does not follow the theme — so its ink and hover
114
+ // wash are literal, not semantic tokens: `text-fg` / `bg-surface-muted` flip with
115
+ // `[data-theme]` and would go dark-on-dark in a light shell. Raw values (rather
116
+ // than new `--color-context-menu-*` custom properties) because every token in
117
+ // this design system is generated into @podoba/tokens' `variables.css` from the
118
+ // upstream API — a hand-written custom property has nowhere to be defined and
119
+ // silently resolves to nothing.
139
120
  baseClass:
140
- 'flex min-h-[34px] cursor-pointer select-none items-center gap-2.5 rounded-[10px] px-3 py-1.5 ' +
141
- 'text-compact text-white outline-none ' +
121
+ 'flex min-h-8.5 cursor-pointer select-none items-center gap-2.5 rounded-[10px] px-3 py-1.5 ' +
122
+ 'text-compact leading-4 tracking-normal font-normal text-white outline-none ' +
142
123
  'data-[focused]:bg-[#2f2f2f] data-[focused]:font-medium data-[hovered]:bg-[#2f2f2f] data-[hovered]:font-medium ' +
143
124
  'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-45',
144
125
  variants: {
145
- // gs uses `--color-error-light` (#ffb5b5) a light red tuned for the dark
146
- // panel; our `text-danger` (#dc2626) is a light-surface red that fails
147
- // contrast here, and there is no danger-light token.
126
+ // gs `--color-error-light` (#ffb5b5), a light red tuned for this dark panel.
127
+ // `--color-danger-light` DOES exist, but it flips to a ~15% translucent red
128
+ // under `[data-theme="dark"]` and would all but vanish here; `text-danger`
129
+ // (#dc2626) is a light-surface red that fails contrast on #242424.
148
130
  destructive: { true: 'text-[#ffb5b5] data-[focused]:text-[#ffb5b5]' },
149
131
  },
150
132
  }) as (
151
133
  props: React.ComponentProps<typeof RACMenuItem> & { destructive?: boolean },
152
134
  ) => ReturnType<typeof RACMenuItem>
153
135
 
154
- // gs `.panel`: 258px, radius 12px, 8px/6px padding, 180deg #242424→#1f1f1f
155
- // gradient, 0 12px 28px rgba(0,0,0,.22) shadow. No token covers the gradient/shadow.
136
+ // gs `.panel`: 258px, radius 12px, 8px/6px padding, 180deg #242424→#1f1f1f gradient,
137
+ // 0 12px 28px rgba(0,0,0,.22) shadow. No token covers the gradient or the shadow, and
138
+ // see the note on MenuItem for why these stay literal rather than becoming tokens.
139
+ // Constrain/scroll the entire panel, INCLUDING its 8px top/bottom padding —
140
+ // constraining the inner Menu instead makes a long panel exceed the viewport gutter.
156
141
  const panelClass =
157
- 'w-[258px] max-w-[calc(100vw-16px)] rounded-lg bg-gradient-to-b from-[#242424] to-[#1f1f1f] ' +
142
+ 'w-[258px] max-w-[calc(100vw-16px)] max-h-[calc(100vh-16px)] box-border overflow-y-auto rounded-xl ' +
143
+ 'bg-gradient-to-b from-[#242424] to-[#1f1f1f] ' +
158
144
  'px-1.5 py-2 shadow-[0_12px_28px_rgba(0,0,0,0.22)] outline-none'
159
- const menuClass = 'grid max-h-[calc(100vh-16px)] gap-0.5 overflow-y-auto outline-none'
145
+ const menuClass = 'grid gap-0.5 outline-none'
146
+
147
+ const CursorPopoverSurface = uic(RACPopover, {
148
+ displayName: 'ContextMenuPopoverSurface',
149
+ baseClass: panelClass,
150
+ })
151
+
152
+ /** RAC owns interaction; source-style fixed cursor geometry owns placement. */
153
+ function CursorMenuPopover({
154
+ position,
155
+ panelRef,
156
+ ...props
157
+ }: Omit<React.ComponentProps<typeof RACPopover>, 'ref' | 'key' | 'children' | 'className'> & {
158
+ position: { x: number; y: number }
159
+ panelRef: React.RefObject<HTMLElement | null>
160
+ /**
161
+ * Plain children only. `uic` intersects RAC's `ChildrenOrFunction` with a bare
162
+ * `ReactNode`, which is an impossible type (a render function AND a string), so
163
+ * the render-prop form cannot be forwarded through this wrapper — and this menu
164
+ * never uses it.
165
+ */
166
+ children?: ReactNode
167
+ /** Plain class string only, for the same reason as `children` above. */
168
+ className?: string
169
+ }) {
170
+ // This menu is anchored to a viewport coordinate, not a scrollable DOM trigger.
171
+ // With automatic positioning disabled, a null trigger also avoids RAC dismissing
172
+ // it when a trigger ancestor scrolls. Focus restoration belongs to RAC's FocusScope.
173
+ const cursorAnchorRef = useRef<HTMLElement | null>(null)
174
+ const [panel, setPanel] = useState<HTMLElement | null>(null)
175
+ const [measured, setMeasured] = useState<{
176
+ panel: HTMLElement; sourceX: number; sourceY: number; x: number; y: number
177
+ } | null>(null)
178
+ const ref = useCallback((element: HTMLElement | null) => {
179
+ panelRef.current = element
180
+ setPanel(element)
181
+ }, [panelRef])
182
+ const { x: sourceX, y: sourceY } = position
183
+ useLayoutEffect(() => {
184
+ if (!panel || !props.isOpen) return
185
+ let active = true
186
+ const measure = () => {
187
+ if (!active) return
188
+ // offsetHeight includes the padding and respects the panel's viewport cap.
189
+ const x = Math.max(8, Math.min(sourceX, window.innerWidth - panel.offsetWidth - 8))
190
+ const y = Math.max(8, Math.min(sourceY, window.innerHeight - panel.offsetHeight - 8))
191
+ setMeasured(previous => previous?.panel === panel && previous.sourceX === sourceX && previous.sourceY === sourceY && previous.x === x && previous.y === y
192
+ ? previous : { panel, sourceX, sourceY, x, y })
193
+ }
194
+ measure()
195
+ window.addEventListener('resize', measure)
196
+ // Dynamic permissions, translated labels or group counts can resize an open menu.
197
+ const observer = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(measure)
198
+ observer?.observe(panel)
199
+ return () => {
200
+ active = false
201
+ window.removeEventListener('resize', measure)
202
+ observer?.disconnect()
203
+ }
204
+ }, [panel, props.isOpen, sourceX, sourceY])
205
+ const onOpenChange = props.onOpenChange
206
+ useEffect(() => {
207
+ if (!panel || !props.isOpen) return
208
+ // Source uses outside mousedown, without consuming the background's action.
209
+ // Non-modal RAC keeps its blur/Escape/focus handling but does not supply this listener.
210
+ const dismissOutside = (event: Event) => {
211
+ if (event.target instanceof Node && !panel.contains(event.target)) onOpenChange?.(false)
212
+ }
213
+ const dismissTouch = (event: PointerEvent) => {
214
+ if (event.pointerType !== 'mouse') dismissOutside(event)
215
+ }
216
+ document.addEventListener('mousedown', dismissOutside)
217
+ document.addEventListener('pointerdown', dismissTouch)
218
+ return () => {
219
+ document.removeEventListener('mousedown', dismissOutside)
220
+ document.removeEventListener('pointerdown', dismissTouch)
221
+ }
222
+ }, [panel, props.isOpen, onOpenChange])
223
+ const resolved = measured && measured.panel === panel && measured.sourceX === sourceX && measured.sourceY === sourceY ? measured : null
224
+ // Annotated rather than inlined: RAC types `style` as a union with a render-prop
225
+ // function, so an inline literal is not contextually narrowed and `position`
226
+ // widens to `string`.
227
+ const panelStyle: CSSProperties = {
228
+ position: 'fixed',
229
+ left: resolved?.x ?? sourceX,
230
+ top: resolved?.y ?? sourceY,
231
+ // Override RAC's unmeasured inline 100vh, which would otherwise beat the CSS cap.
232
+ maxHeight: 'calc(100vh - 16px)',
233
+ opacity: resolved ? 1 : 0, // Unlike visibility:hidden, keeps RAC's initial autofocus possible.
234
+ }
235
+ return <CursorPopoverSurface
236
+ {...props}
237
+ ref={ref}
238
+ triggerRef={cursorAnchorRef}
239
+ data-context-menu-panel=""
240
+ shouldUpdatePosition={false}
241
+ shouldFlip={false}
242
+ isNonModal
243
+ style={panelStyle}
244
+ />
245
+ }
160
246
 
161
247
  /**
162
248
  * The popover body shared by both modes — the RAC `Menu` of grouped items. `onItem`
163
- * fires after an item's own handler so the controlled mode can close the menu.
249
+ * fires after an item's own handler so either mode can close the menu.
164
250
  */
165
251
  function ContextMenuBody({
166
252
  groups,
@@ -169,23 +255,24 @@ function ContextMenuBody({
169
255
  onItem,
170
256
  }: {
171
257
  groups: ContextMenuGroup[]
172
- soonLabel: ReactNode
258
+ soonLabel?: ReactNode
173
259
  ariaLabel: string
174
260
  onItem?: () => void
175
261
  }) {
176
262
  return (
177
- <RACMenu aria-label={ariaLabel} className={menuClass}>
263
+ <RACMenu aria-label={ariaLabel} className={menuClass} autoFocus onClose={onItem}>
178
264
  {groups.map((group, gi) => (
179
265
  // gs separates groups with an 18px top margin (`.group + .group`), not a
180
266
  // rule line — so the first group sits flush, the rest gain the gap.
181
- <RACMenuSection key={groupId(group, gi)} className={gi > 0 ? 'mt-[18px] grid gap-0.5' : 'grid gap-0.5'}>
267
+ <RACMenuSection key={groupId(group, gi)} className={gi > 0 ? 'mt-4.5 grid gap-0.5' : 'grid gap-0.5'}>
182
268
  {group.label ? (
183
- <Header className="mx-2 mt-1.5 mb-1 text-compact font-medium text-white">{group.label}</Header>
269
+ // The grid contributes 2px: 2px margin + 2px gap = source 4px below label.
270
+ <Header className="mx-2 mt-1.5 mb-0.5 text-compact leading-4 tracking-normal font-medium text-white">{group.label}</Header>
184
271
  ) : null}
185
272
  {group.items.map((item, ii) => {
186
273
  const id = itemId(item, ii)
187
274
  const select = item.onSelect ?? item.onAction
188
- const badge = item.badge ?? (item.disabled ? soonLabel : null)
275
+ const badge = item.badge ?? (item.disabled && !select ? soonLabel : null)
189
276
  return (
190
277
  <MenuItem
191
278
  key={id}
@@ -199,13 +286,12 @@ function ContextMenuBody({
199
286
  }}
200
287
  >
201
288
  {item.icon ? (
202
- <span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
289
+ <span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center [&>svg]:size-full">
203
290
  {item.icon}
204
291
  </span>
205
292
  ) : null}
206
- <span className="min-w-0 flex-1 truncate">{item.label}</span>
293
+ <span className="min-w-0 flex-1 truncate" title={item.title}>{item.label}</span>
207
294
  {badge ? (
208
- // gs `.badge`: 24px pill, #333437 bg / #c8c8ca text, 10px (text-micro).
209
295
  <span className="ml-auto inline-flex h-6 items-center justify-center rounded-full bg-[#333437] px-3 py-0.5 text-micro font-medium leading-5 tracking-tight text-[#c8c8ca]">
210
296
  {badge}
211
297
  </span>
@@ -225,58 +311,47 @@ function ControlledContextMenu({
225
311
  isOpen,
226
312
  position,
227
313
  onClose,
228
- soonLabel = 'Soon',
314
+ soonLabel,
229
315
  'aria-label': ariaLabel = 'Actions',
230
316
  className,
231
317
  }: ContextMenuProps): React.JSX.Element | null {
232
- const anchorRef = useRef<HTMLSpanElement>(null)
233
318
  const popoverRef = useRef<HTMLElement>(null)
234
319
  const visibleGroups = visibleGroupsOf(groups)
235
- // Lock scroll while open (hook runs unconditionally — before the empty-groups return).
236
- useScrollLock(isOpen && visibleGroups.length > 0, popoverRef)
237
320
  if (visibleGroups.length === 0) {
238
321
  return null
239
322
  }
240
323
  const pos = position ?? { x: 0, y: 0 }
241
324
  return (
242
- <>
243
- {/* Zero-size anchor at the cursor; the Popover attaches here. Keyed by
244
- position so a re-open at a new spot re-measures (RAC only measures the
245
- anchor when the popover opens). */}
246
- <span
247
- ref={anchorRef}
248
- aria-hidden="true"
249
- style={{ position: 'fixed', left: pos.x, top: pos.y, width: 0, height: 0 }}
250
- />
251
- <RACPopover
325
+ <CursorMenuPopover
252
326
  key={`${pos.x}:${pos.y}`}
253
- ref={popoverRef}
327
+ panelRef={popoverRef}
328
+ position={pos}
254
329
  isOpen={isOpen}
255
330
  onOpenChange={(open) => {
256
331
  if (!open) {
257
332
  onClose()
258
333
  }
259
334
  }}
260
- triggerRef={anchorRef}
261
335
  placement="bottom start"
262
- className={className ? `${panelClass} ${className}` : panelClass}
336
+ offset={0}
337
+ containerPadding={8}
338
+ className={className}
263
339
  >
264
340
  <ContextMenuBody groups={visibleGroups} soonLabel={soonLabel} ariaLabel={ariaLabel} onItem={onClose} />
265
- </RACPopover>
266
- </>
341
+ </CursorMenuPopover>
267
342
  )
268
343
  }
269
344
 
270
345
  /** Wrapper context menu — wraps a target and owns its own open state. */
271
346
  function WrapperContextMenu({
272
347
  groups,
348
+ shouldOpen,
273
349
  children,
274
- soonLabel = 'Soon',
350
+ soonLabel,
275
351
  'aria-label': ariaLabel = 'Actions',
276
352
  className,
277
353
  }: ContextMenuWrapperProps): React.JSX.Element {
278
354
  const [isOpen, setOpen] = useState(false)
279
- const anchorRef = useRef<HTMLSpanElement>(null)
280
355
  const wrapperRef = useRef<HTMLDivElement>(null)
281
356
  const popoverRef = useRef<HTMLElement>(null)
282
357
  const [position, setPosition] = useState({ x: 0, y: 0 })
@@ -288,6 +363,8 @@ function WrapperContextMenu({
288
363
  // every parent render (resolvers are usually inline functions).
289
364
  const groupsRef = useRef(groups)
290
365
  groupsRef.current = groups
366
+ const shouldOpenRef = useRef(shouldOpen)
367
+ shouldOpenRef.current = shouldOpen
291
368
 
292
369
  const openAt = (x: number, y: number, target: HTMLElement) => {
293
370
  if (typeof groupsRef.current === 'function') {
@@ -298,15 +375,30 @@ function WrapperContextMenu({
298
375
  }
299
376
 
300
377
  const onContextMenu = (e: React.MouseEvent) => {
378
+ if (e.defaultPrevented) return
379
+ const target = e.target instanceof HTMLElement ? e.target : e.currentTarget as HTMLElement
380
+ if (shouldOpenRef.current?.(target) === false) {
381
+ setOpen(false)
382
+ return
383
+ }
384
+ e.preventDefault()
385
+ openAt(e.clientX, e.clientY, target)
386
+ }
387
+
388
+ const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
389
+ if (e.defaultPrevented || (e.key !== 'ContextMenu' && !(e.key === 'F10' && e.shiftKey))) return
390
+ const target = e.target instanceof HTMLElement ? e.target : e.currentTarget
391
+ if (shouldOpenRef.current?.(target) === false) return
301
392
  e.preventDefault()
302
- openAt(e.clientX, e.clientY, e.target as HTMLElement)
393
+ e.stopPropagation()
394
+ const rect = target.getBoundingClientRect()
395
+ openAt(rect.left, rect.bottom, target)
303
396
  }
304
397
 
305
398
  // While the panel is open, a second right-click would otherwise be swallowed by
306
399
  // the overlay's outside-press dismissal and surface the BROWSER's native menu.
307
- // Intercept it ourselves: keep the OS menu suppressed and re-anchor our panel to
308
- // the new cursor. Re-anchoring needs a close + reopen across a frame so the
309
- // popover recomputes its position (it only measures the anchor on open). A
400
+ // Intercept it ourselves: keep the OS menu suppressed and move our panel to
401
+ // the new cursor in-place (no queued close/reopen frame). A
310
402
  // right-click on our own panel just suppresses the OS menu; one outside our
311
403
  // region closes the panel and behaves normally.
312
404
  useEffect(() => {
@@ -326,20 +418,19 @@ function WrapperContextMenu({
326
418
  setOpen(false)
327
419
  return
328
420
  }
329
- // In our region with the panel open: take over fully. stopImmediatePropagation
330
- // keeps the wrapper's bubble onContextMenu from re-opening synchronously —
331
- // otherwise isOpen never commits `false` and the popover never re-anchors.
421
+ if (shouldOpenRef.current?.(target) === false) {
422
+ setOpen(false)
423
+ return
424
+ }
425
+ // In our region with the panel open: update target and pointer atomically.
426
+ // The cursor clamp reacts to position changes without dismissing the menu.
332
427
  e.preventDefault()
333
428
  e.stopImmediatePropagation()
334
- setOpen(false)
335
429
  const { clientX, clientY } = e
336
- requestAnimationFrame(() => {
337
- if (typeof groupsRef.current === 'function') {
338
- setResolved(groupsRef.current({ target }))
339
- }
340
- setPosition({ x: clientX, y: clientY })
341
- setOpen(true)
342
- })
430
+ if (typeof groupsRef.current === 'function') {
431
+ setResolved(groupsRef.current({ target }))
432
+ }
433
+ setPosition({ x: clientX, y: clientY })
343
434
  }
344
435
  document.addEventListener('contextmenu', handle, true)
345
436
  return () => document.removeEventListener('contextmenu', handle, true)
@@ -347,29 +438,22 @@ function WrapperContextMenu({
347
438
 
348
439
  const sourceGroups = typeof groups === 'function' ? resolved : groups
349
440
  const visibleGroups = visibleGroupsOf(sourceGroups)
350
- // Lock scroll while the panel is open (still scrollable inside a long menu).
351
- useScrollLock(isOpen && visibleGroups.length > 0, popoverRef)
352
441
 
353
442
  return (
354
- <div ref={wrapperRef} className={className} onContextMenu={onContextMenu}>
443
+ <div ref={wrapperRef} className={className} onContextMenu={onContextMenu} onKeyDown={onKeyDown}>
355
444
  {children}
356
- {/* Zero-size anchor positioned at the cursor; the Popover attaches here. */}
357
- <span
358
- ref={anchorRef}
359
- aria-hidden="true"
360
- style={{ position: 'fixed', left: position.x, top: position.y, width: 0, height: 0 }}
361
- />
362
445
  {visibleGroups.length > 0 ? (
363
- <RACPopover
364
- ref={popoverRef}
446
+ <CursorMenuPopover
447
+ panelRef={popoverRef}
448
+ position={position}
365
449
  isOpen={isOpen}
366
450
  onOpenChange={setOpen}
367
- triggerRef={anchorRef}
368
451
  placement="bottom start"
369
- className={panelClass}
452
+ offset={0}
453
+ containerPadding={8}
370
454
  >
371
- <ContextMenuBody groups={visibleGroups} soonLabel={soonLabel} ariaLabel={ariaLabel} />
372
- </RACPopover>
455
+ <ContextMenuBody groups={visibleGroups} soonLabel={soonLabel} ariaLabel={ariaLabel} onItem={() => setOpen(false)} />
456
+ </CursorMenuPopover>
373
457
  ) : null}
374
458
  </div>
375
459
  )
@@ -0,0 +1,9 @@
1
+ import { useState } from 'react'
2
+ import { ContextSearchPanel } from './context-search-panel'
3
+ function SearchExample() {
4
+ const [value, setValue] = useState('')
5
+ const [isOpen, setIsOpen] = useState(true)
6
+ return <><button type="button" onClick={() => setIsOpen(true)}>Search</button><ContextSearchPanel isOpen={isOpen} value={value} onChange={setValue} onClose={() => setIsOpen(false)} label="Search" placeholder="Search tasks" clearLabel="Clear" closeLabel="Close" /></>
7
+ }
8
+ export const examples = { default: SearchExample }
9
+ export const meta = { category: 'Patterns', description: 'Context search panel with retained query, clear, outside-click and Escape dismissal.' }
@@ -0,0 +1,50 @@
1
+ import { useEffect, useId, useRef } from 'react'
2
+ import { uic } from '../utils/uic'
3
+
4
+ export interface ContextSearchPanelProps {
5
+ isOpen: boolean
6
+ value: string
7
+ onChange: (value: string) => void
8
+ onClose: () => void
9
+ label: string
10
+ placeholder: string
11
+ clearLabel: string
12
+ closeLabel: string
13
+ }
14
+ const SearchInput = uic('input', { displayName: 'ContextSearchPanelInput', baseClass: 'min-h-[38px] min-w-0 w-full rounded-lg border border-border bg-surface px-3 text-body text-fg outline-none focus-visible:ring-2 focus-visible:ring-ring' })
15
+ const ActionButton = uic('button', { displayName: 'ContextSearchPanelButton', baseClass: 'min-h-[38px] rounded-lg border border-border bg-surface-card px-3 text-small font-medium text-fg cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring' })
16
+
17
+ export function ContextSearchPanel({ isOpen, value, onChange, onClose, label, placeholder, clearLabel, closeLabel }: ContextSearchPanelProps) {
18
+ const panelRef = useRef<HTMLDivElement>(null)
19
+ const inputRef = useRef<HTMLInputElement>(null)
20
+ const closeRef = useRef(onClose)
21
+ closeRef.current = onClose
22
+ const inputId = useId()
23
+ useEffect(() => {
24
+ if (!isOpen) return
25
+ inputRef.current?.focus()
26
+ const key = (event: KeyboardEvent) => { if (event.key === 'Escape') closeRef.current() }
27
+ const mouse = (event: MouseEvent) => {
28
+ if (event.target instanceof Node && !panelRef.current?.contains(event.target)) closeRef.current()
29
+ }
30
+ document.addEventListener('keydown', key)
31
+ document.addEventListener('mousedown', mouse)
32
+ return () => {
33
+ document.removeEventListener('keydown', key)
34
+ document.removeEventListener('mousedown', mouse)
35
+ }
36
+ }, [isOpen])
37
+ if (!isOpen) return null
38
+ return (
39
+ <div className="relative shrink-0" role="search" aria-label={label}>
40
+ <div ref={panelRef} className="absolute right-0 top-[calc(100%+0.5rem)] z-[5] w-[min(40vw,32rem)] min-w-96 rounded-lg border border-border-muted bg-surface p-2 shadow-[0_12px_30px_color-mix(in_srgb,var(--color-fg)_14%,transparent)] max-[1023px]:left-0 max-[1023px]:right-auto max-[1023px]:w-[min(90vw,32rem)] max-[1023px]:min-w-[min(90vw,24rem)]">
41
+ <label className="sr-only" htmlFor={inputId}>{label}</label>
42
+ <div className="flex items-center gap-2">
43
+ <SearchInput ref={inputRef} id={inputId} value={value} onChange={event => onChange(event.currentTarget.value)} placeholder={placeholder} />
44
+ {value ? <ActionButton type="button" onClick={() => onChange('')}>{clearLabel}</ActionButton> : null}
45
+ <ActionButton type="button" onClick={onClose}>{closeLabel}</ActionButton>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ )
50
+ }