react-os-shell 4.63.0 → 4.74.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 (64) hide show
  1. package/README.md +3 -0
  2. package/dist/{Browser-SPTNLEMR.js → Browser-ZF5X2FON.js} +7 -7
  3. package/dist/{Browser-SPTNLEMR.js.map → Browser-ZF5X2FON.js.map} +1 -1
  4. package/dist/{Calculator-XMOPPCK3.js → Calculator-SA62H5S3.js} +6 -6
  5. package/dist/{Calculator-XMOPPCK3.js.map → Calculator-SA62H5S3.js.map} +1 -1
  6. package/dist/ConfirmDialog-SREDNTIB.js +3 -0
  7. package/dist/{ConfirmDialog-EEH4CWSW.js.map → ConfirmDialog-SREDNTIB.js.map} +1 -1
  8. package/dist/{CurrencyConverter-24HNX6C6.js → CurrencyConverter-BMCRLZLY.js} +6 -6
  9. package/dist/{CurrencyConverter-24HNX6C6.js.map → CurrencyConverter-BMCRLZLY.js.map} +1 -1
  10. package/dist/{Documents-GMRVVCH7.js → Documents-2TGJPV2Y.js} +8 -8
  11. package/dist/{Documents-GMRVVCH7.js.map → Documents-2TGJPV2Y.js.map} +1 -1
  12. package/dist/{Files-ITH4SVML.js → Files-Z3ONKUA6.js} +9 -9
  13. package/dist/{Files-ITH4SVML.js.map → Files-Z3ONKUA6.js.map} +1 -1
  14. package/dist/{Notepad-GE5P55TL.js → Notepad-JWZS5YKJ.js} +8 -8
  15. package/dist/{Notepad-GE5P55TL.js.map → Notepad-JWZS5YKJ.js.map} +1 -1
  16. package/dist/{PomodoroTimer-ER5STVKM.js → PomodoroTimer-DRM5W25H.js} +8 -8
  17. package/dist/{PomodoroTimer-ER5STVKM.js.map → PomodoroTimer-DRM5W25H.js.map} +1 -1
  18. package/dist/{Preview-UXVDXCDJ.js → Preview-7O7W7OMQ.js} +9 -9
  19. package/dist/{Preview-UXVDXCDJ.js.map → Preview-7O7W7OMQ.js.map} +1 -1
  20. package/dist/{Spreadsheet-B32JQVX4.js → Spreadsheet-CLTZSW3T.js} +7 -7
  21. package/dist/{Spreadsheet-B32JQVX4.js.map → Spreadsheet-CLTZSW3T.js.map} +1 -1
  22. package/dist/{Stock-FSFPFDQT.js → Stock-CO5N3N2M.js} +6 -6
  23. package/dist/{Stock-FSFPFDQT.js.map → Stock-CO5N3N2M.js.map} +1 -1
  24. package/dist/{Weather-HXOERJJV.js → Weather-IEGJX7RI.js} +6 -6
  25. package/dist/{Weather-HXOERJJV.js.map → Weather-IEGJX7RI.js.map} +1 -1
  26. package/dist/{WorldClock-2FUGLIUZ.js → WorldClock-FLW3QZYG.js} +6 -6
  27. package/dist/{WorldClock-2FUGLIUZ.js.map → WorldClock-FLW3QZYG.js.map} +1 -1
  28. package/dist/apps/index.js +17 -17
  29. package/dist/{chunk-TK6HI2U4.js → chunk-2VM2FRZJ.js} +42 -11
  30. package/dist/chunk-2VM2FRZJ.js.map +1 -0
  31. package/dist/{chunk-ECE3B3FZ.js → chunk-AXWC3332.js} +3 -3
  32. package/dist/{chunk-ECE3B3FZ.js.map → chunk-AXWC3332.js.map} +1 -1
  33. package/dist/{chunk-ZCDFNZQS.js → chunk-BLLINYDD.js} +97 -5
  34. package/dist/chunk-BLLINYDD.js.map +1 -0
  35. package/dist/{chunk-OXLLRKBC.js → chunk-CDR2Q64Q.js} +38 -22
  36. package/dist/chunk-CDR2Q64Q.js.map +1 -0
  37. package/dist/{chunk-TO6ZZHJK.js → chunk-EG3WEQBH.js} +4 -4
  38. package/dist/{chunk-TO6ZZHJK.js.map → chunk-EG3WEQBH.js.map} +1 -1
  39. package/dist/{chunk-JMESET3G.js → chunk-G7IHPEJE.js} +138 -39
  40. package/dist/chunk-G7IHPEJE.js.map +1 -0
  41. package/dist/chunk-NJDH5PHA.js +7 -0
  42. package/dist/{chunk-WCGHIJZR.js.map → chunk-NJDH5PHA.js.map} +1 -1
  43. package/dist/{chunk-LYQVLRWQ.js → chunk-RUAZJ54W.js} +3 -3
  44. package/dist/{chunk-LYQVLRWQ.js.map → chunk-RUAZJ54W.js.map} +1 -1
  45. package/dist/{chunk-CWA27IGB.js → chunk-S6K37RLY.js} +781 -344
  46. package/dist/chunk-S6K37RLY.js.map +1 -0
  47. package/dist/{chunk-IOCOH2QQ.js → chunk-TT2WNTDS.js} +4 -4
  48. package/dist/{chunk-IOCOH2QQ.js.map → chunk-TT2WNTDS.js.map} +1 -1
  49. package/dist/{chunk-3OCL3DB4.js → chunk-TT56U4GY.js} +3 -3
  50. package/dist/{chunk-3OCL3DB4.js.map → chunk-TT56U4GY.js.map} +1 -1
  51. package/dist/{index-Ka32s3Ij.d.ts → index-DVekeidu.d.ts} +258 -27
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +121 -35
  54. package/dist/index.js.map +1 -1
  55. package/dist/ui/index.d.ts +1 -1
  56. package/dist/ui/index.js +5 -5
  57. package/package.json +2 -2
  58. package/dist/ConfirmDialog-EEH4CWSW.js +0 -3
  59. package/dist/chunk-CWA27IGB.js.map +0 -1
  60. package/dist/chunk-JMESET3G.js.map +0 -1
  61. package/dist/chunk-OXLLRKBC.js.map +0 -1
  62. package/dist/chunk-TK6HI2U4.js.map +0 -1
  63. package/dist/chunk-WCGHIJZR.js +0 -7
  64. package/dist/chunk-ZCDFNZQS.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { glassStyle, GLASS_DIVIDER } from './chunk-3CUSBZWG.js';
2
- import { forwardRef, createContext, useSyncExternalStore, useRef, useEffect, useState, useCallback, useContext } from 'react';
3
- import { createPortal } from 'react-dom';
2
+ import { createContext, forwardRef, useSyncExternalStore, useMemo, useContext, useRef, useEffect, useState, useCallback } from 'react';
4
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
+ import { createPortal } from 'react-dom';
5
5
 
6
6
  var QUERY = "(max-width: 767px), (pointer: coarse)";
7
7
  function getSnapshot() {
@@ -25,6 +25,98 @@ function subscribe(cb) {
25
25
  function useIsMobile() {
26
26
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
27
27
  }
28
+ var DEFAULT_SHELL_STRINGS = {
29
+ window: {
30
+ minimize: "Minimize",
31
+ maximize: "Maximize",
32
+ windowed: "Windowed",
33
+ pinOnTop: "Pin on top",
34
+ unpin: "Unpin from top",
35
+ close: "Close",
36
+ windowSuffix: "window",
37
+ prev: "Prev",
38
+ next: "Next",
39
+ snapLayouts: "Snap layouts",
40
+ snapLeft: "Snap left half",
41
+ snapRight: "Snap right half",
42
+ snapTopLeft: "Snap top-left quarter",
43
+ snapTopRight: "Snap top-right quarter",
44
+ snapBottomLeft: "Snap bottom-left quarter",
45
+ snapBottomRight: "Snap bottom-right quarter"
46
+ },
47
+ taskbar: {
48
+ closeWindow: "Close window",
49
+ offscreenHint: "This window is off screen \u2014 click to bring it back",
50
+ offscreenDot: "Off screen \u2014 click to bring this window back",
51
+ expose: "Expos\xE9 \u2014 show all open windows as thumbnails",
52
+ addToDesktop: "Add to desktop",
53
+ removeFromDesktop: "Remove from desktop"
54
+ },
55
+ logout: {
56
+ goodbye: "Goodbye",
57
+ seeYou: "See you next time"
58
+ },
59
+ about: {
60
+ aboutPrefix: "About",
61
+ thisApp: "this app",
62
+ openSourceLicenses: "Open Source Licenses",
63
+ whatsNew: "What's New",
64
+ noChangelog: "No changelog available."
65
+ },
66
+ table: {
67
+ empty: "Nothing to show",
68
+ loading: "Loading",
69
+ selectAll: "Select all rows",
70
+ selectRow: "Select row"
71
+ },
72
+ select: {
73
+ none: "\u2014 None \u2014",
74
+ noMatches: "No matches",
75
+ allSelected: "All options selected",
76
+ clear: "Clear selection",
77
+ remove: "Remove"
78
+ },
79
+ notifications: {
80
+ title: "Notifications",
81
+ markAllRead: "Mark all read",
82
+ dnd: "Do not disturb",
83
+ dndOn: "Do not disturb is on",
84
+ caughtUp: "All caught up",
85
+ empty: "No notifications yet",
86
+ viewAll: "View all notifications",
87
+ cardLabel: "Notification",
88
+ dismiss: "Dismiss notification"
89
+ },
90
+ form: {
91
+ errorSummaryTitle: "There is a problem"
92
+ },
93
+ help: {
94
+ title: "Help",
95
+ searchPlaceholder: "Search help\u2026",
96
+ empty: "No help articles yet.",
97
+ noResults: "No articles match your search.",
98
+ loading: "Loading\u2026",
99
+ pickArticle: "Pick a help article from the left.",
100
+ noBody: "This article has no body yet.",
101
+ draft: "Draft",
102
+ newArticle: "+ New",
103
+ edit: "Edit"
104
+ }
105
+ };
106
+ var ShellStringsContext = createContext(DEFAULT_SHELL_STRINGS);
107
+ function ShellStringsProvider({ value, children }) {
108
+ const merged = useMemo(() => {
109
+ const out = {};
110
+ for (const key of Object.keys(DEFAULT_SHELL_STRINGS)) {
111
+ out[key] = { ...DEFAULT_SHELL_STRINGS[key], ...value[key] ?? {} };
112
+ }
113
+ return out;
114
+ }, [value]);
115
+ return /* @__PURE__ */ jsx(ShellStringsContext.Provider, { value: merged, children });
116
+ }
117
+ function useShellStrings() {
118
+ return useContext(ShellStringsContext);
119
+ }
28
120
  function getDensity() {
29
121
  return getComputedStyle(document.documentElement).getPropertyValue("--menu-density")?.trim() || "normal";
30
122
  }
@@ -198,6 +290,6 @@ function useShellPrefs() {
198
290
  } };
199
291
  }
200
292
 
201
- export { PopupMenu, PopupMenuDivider, PopupMenuItem, PopupMenuLabel, ShellPrefsProvider, useIsMobile, useLocalStoragePrefs, useShellPrefs };
202
- //# sourceMappingURL=chunk-ZCDFNZQS.js.map
203
- //# sourceMappingURL=chunk-ZCDFNZQS.js.map
293
+ export { DEFAULT_SHELL_STRINGS, PopupMenu, PopupMenuDivider, PopupMenuItem, PopupMenuLabel, ShellPrefsProvider, ShellStringsProvider, useIsMobile, useLocalStoragePrefs, useShellPrefs, useShellStrings };
294
+ //# sourceMappingURL=chunk-BLLINYDD.js.map
295
+ //# sourceMappingURL=chunk-BLLINYDD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/useIsMobile.ts","../src/shell/strings.tsx","../src/shell/PopupMenu.tsx","../src/shell/ShellPrefs.tsx"],"names":["jsx","PopupMenuItem","createContext","useContext"],"mappings":"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","file":"chunk-BLLINYDD.js","sourcesContent":["/**\n * Mobile breakpoint hook — single source of truth for \"is this a phone/tablet\n * portrait viewport?\" Drives whether <Layout> renders the desktop chrome\n * (windows / taskbar / start menu sidebar) or the <MobileAppLanding> screen\n * that points the user at the consumer-wired mobile app. A handful of\n * primitives (Modal, ResizableTable) also read it to adapt touch behaviour.\n *\n * Locked at 768 px so iPad portrait counts as mobile. Also OR's with\n * `pointer: coarse` so touch-only devices that happen to be wider (e.g. an\n * Android tablet in landscape) still get touch-appropriate UI.\n */\nimport { useSyncExternalStore } from 'react';\n\nconst QUERY = '(max-width: 767px), (pointer: coarse)';\n\nfunction getSnapshot(): boolean {\n if (typeof window === 'undefined' || !window.matchMedia) return false;\n return window.matchMedia(QUERY).matches;\n}\n\nfunction getServerSnapshot(): boolean {\n return false;\n}\n\nfunction subscribe(cb: () => void): () => void {\n if (typeof window === 'undefined' || !window.matchMedia) return () => {};\n const mql = window.matchMedia(QUERY);\n // Older Safari uses addListener; modern browsers use addEventListener.\n if (mql.addEventListener) {\n mql.addEventListener('change', cb);\n return () => mql.removeEventListener('change', cb);\n }\n mql.addListener(cb);\n return () => mql.removeListener(cb);\n}\n\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","/**\n * ShellStrings — the shell's user-facing strings, in one typed catalog.\n *\n * Every string here was hardcoded English at its call site, which made the\n * shell untranslatable no matter what the consuming portal did. The catalog\n * is the fix's whole shape: components read strings through `useShellStrings`\n * (which works with NO provider — the English defaults are the context\n * default), and a consumer that wants another language mounts\n * `ShellStringsProvider` once with a partial override. Overrides merge per\n * SECTION, so translating the window controls does not oblige anyone to\n * translate the help viewer.\n *\n * This is deliberately not an i18n library: no message IDs, no interpolation\n * DSL, no plural rules. The shell's strings are labels and short sentences;\n * a typed object keeps them discoverable and lets TypeScript flag a\n * translation that goes stale when a string is added.\n *\n * Prop-level text (a `emptyText` prop, a `placeholder`) always wins over the\n * catalog — the catalog replaces hardcoded DEFAULTS, never a caller's words.\n *\n * Kit-safe: no window-manager import, so `react-os-shell/ui` consumers get it\n * too.\n */\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\n\nexport interface ShellStrings {\n /** Window chrome: control tooltips and the title bar's accessible name. */\n window: {\n minimize: string;\n maximize: string;\n windowed: string;\n pinOnTop: string;\n unpin: string;\n close: string;\n /** Suffix of the title bar's aria-label: `\"{title} — {windowSuffix}\"`. */\n windowSuffix: string;\n prev: string;\n next: string;\n snapLayouts: string;\n snapLeft: string;\n snapRight: string;\n snapTopLeft: string;\n snapTopRight: string;\n snapBottomLeft: string;\n snapBottomRight: string;\n };\n /** Taskbar and exposé. */\n taskbar: {\n closeWindow: string;\n offscreenHint: string;\n offscreenDot: string;\n expose: string;\n addToDesktop: string;\n removeFromDesktop: string;\n };\n /** The logout cover. */\n logout: {\n goodbye: string;\n seeYou: string;\n };\n /** About + What's New dialogs. */\n about: {\n aboutPrefix: string;\n thisApp: string;\n openSourceLicenses: string;\n whatsNew: string;\n noChangelog: string;\n };\n /** Data-display defaults (DataTable, lists). */\n table: {\n empty: string;\n loading: string;\n selectAll: string;\n selectRow: string;\n };\n /** Picker defaults (SearchableSelect, TagInput). */\n select: {\n none: string;\n noMatches: string;\n allSelected: string;\n clear: string;\n /** aria-label of a TagInput chip's remove button: `\"{remove} {label}\"`. */\n remove: string;\n };\n /** Notification bell + popup. */\n notifications: {\n title: string;\n markAllRead: string;\n dnd: string;\n dndOn: string;\n caughtUp: string;\n empty: string;\n viewAll: string;\n cardLabel: string;\n dismiss: string;\n };\n /** Form-level messages. */\n form: {\n errorSummaryTitle: string;\n };\n /** HelpCenter chrome. */\n help: {\n title: string;\n searchPlaceholder: string;\n empty: string;\n noResults: string;\n loading: string;\n pickArticle: string;\n noBody: string;\n draft: string;\n newArticle: string;\n edit: string;\n };\n}\n\nexport const DEFAULT_SHELL_STRINGS: ShellStrings = {\n window: {\n minimize: 'Minimize',\n maximize: 'Maximize',\n windowed: 'Windowed',\n pinOnTop: 'Pin on top',\n unpin: 'Unpin from top',\n close: 'Close',\n windowSuffix: 'window',\n prev: 'Prev',\n next: 'Next',\n snapLayouts: 'Snap layouts',\n snapLeft: 'Snap left half',\n snapRight: 'Snap right half',\n snapTopLeft: 'Snap top-left quarter',\n snapTopRight: 'Snap top-right quarter',\n snapBottomLeft: 'Snap bottom-left quarter',\n snapBottomRight: 'Snap bottom-right quarter',\n },\n taskbar: {\n closeWindow: 'Close window',\n offscreenHint: 'This window is off screen — click to bring it back',\n offscreenDot: 'Off screen — click to bring this window back',\n expose: 'Exposé — show all open windows as thumbnails',\n addToDesktop: 'Add to desktop',\n removeFromDesktop: 'Remove from desktop',\n },\n logout: {\n goodbye: 'Goodbye',\n seeYou: 'See you next time',\n },\n about: {\n aboutPrefix: 'About',\n thisApp: 'this app',\n openSourceLicenses: 'Open Source Licenses',\n whatsNew: \"What's New\",\n noChangelog: 'No changelog available.',\n },\n table: {\n empty: 'Nothing to show',\n loading: 'Loading',\n selectAll: 'Select all rows',\n selectRow: 'Select row',\n },\n select: {\n none: '— None —',\n noMatches: 'No matches',\n allSelected: 'All options selected',\n clear: 'Clear selection',\n remove: 'Remove',\n },\n notifications: {\n title: 'Notifications',\n markAllRead: 'Mark all read',\n dnd: 'Do not disturb',\n dndOn: 'Do not disturb is on',\n caughtUp: 'All caught up',\n empty: 'No notifications yet',\n viewAll: 'View all notifications',\n cardLabel: 'Notification',\n dismiss: 'Dismiss notification',\n },\n form: {\n errorSummaryTitle: 'There is a problem',\n },\n help: {\n title: 'Help',\n searchPlaceholder: 'Search help…',\n empty: 'No help articles yet.',\n noResults: 'No articles match your search.',\n loading: 'Loading…',\n pickArticle: 'Pick a help article from the left.',\n noBody: 'This article has no body yet.',\n draft: 'Draft',\n newArticle: '+ New',\n edit: 'Edit',\n },\n};\n\n/** A per-section partial: override any subset of a section's strings. */\nexport type ShellStringsOverride = {\n [S in keyof ShellStrings]?: Partial<ShellStrings[S]>;\n};\n\nconst ShellStringsContext = createContext<ShellStrings>(DEFAULT_SHELL_STRINGS);\n\n/**\n * Mount once, near the top of the app. `value` is a partial — sections and\n * keys you do not translate keep their English defaults, so a catalog never\n * breaks by falling behind a release.\n */\nexport function ShellStringsProvider({ value, children }: { value: ShellStringsOverride; children: ReactNode }) {\n const merged = useMemo<ShellStrings>(() => {\n const out = {} as ShellStrings;\n for (const key of Object.keys(DEFAULT_SHELL_STRINGS) as (keyof ShellStrings)[]) {\n out[key] = { ...DEFAULT_SHELL_STRINGS[key], ...(value[key] ?? {}) } as never;\n }\n return out;\n }, [value]);\n return <ShellStringsContext.Provider value={merged}>{children}</ShellStringsContext.Provider>;\n}\n\n/** The active catalog — English defaults when no provider is mounted. */\nexport function useShellStrings(): ShellStrings {\n return useContext(ShellStringsContext);\n}\n","import { forwardRef, useEffect, useRef, type ReactNode, type CSSProperties } from 'react';\nimport { createPortal } from 'react-dom';\nimport { glassStyle, GLASS_DIVIDER } from '../utils/glass';\n\n/**\n * Unified popup menu component — used for all context menus, dropdowns, and flyouts.\n * Reads --menu-density CSS variable: 'tight', 'normal' (default), or 'large'.\n */\n\nexport interface PopupMenuProps {\n children: ReactNode;\n style?: CSSProperties;\n className?: string;\n onClose?: () => void;\n minWidth?: number;\n /** Render into document.body instead of in place. Required when the menu\n * is opened from INSIDE a window: the window panel is a transformed /\n * backdrop-filtered `overflow-hidden` container, which both re-anchors\n * `position: fixed` descendants to itself and clips them — a menu\n * positioned at viewport coordinates ends up offset or invisible.\n * Portaling restores true viewport positioning. */\n portal?: boolean;\n}\n\nfunction getDensity(): 'tight' | 'normal' | 'large' {\n return (getComputedStyle(document.documentElement).getPropertyValue('--menu-density')?.trim() as any) || 'normal';\n}\n\n/** Container for a popup menu — auto-clamps to stay within viewport */\nexport function PopupMenu({ children, style, className = '', onClose, minWidth = 180, portal = false }: PopupMenuProps) {\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!onClose) return;\n const handleClick = (e: PointerEvent | MouseEvent) => {\n if ((e.target as HTMLElement).closest('[data-menu-toggle]')) return;\n if (ref.current && !ref.current.contains(e.target as Node)) onClose();\n };\n const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };\n window.addEventListener('pointerdown', handleClick);\n window.addEventListener('keydown', handleKey);\n return () => { window.removeEventListener('pointerdown', handleClick); window.removeEventListener('keydown', handleKey); };\n }, [onClose]);\n\n // After render, clamp position to viewport boundaries\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n requestAnimationFrame(() => {\n const rect = el.getBoundingClientRect();\n const margin = 8;\n // Clamp right edge\n if (rect.right > window.innerWidth - margin) {\n const overflow = rect.right - window.innerWidth + margin;\n if (el.style.left) {\n el.style.left = `${parseFloat(el.style.left) - overflow}px`;\n } else if (!el.style.right) {\n el.style.left = `${rect.left - overflow}px`;\n }\n }\n // Clamp left edge\n if (rect.left < margin) {\n if (el.style.left) {\n el.style.left = `${margin}px`;\n } else if (el.style.right) {\n el.style.right = `${window.innerWidth - rect.width - margin}px`;\n }\n }\n // Clamp bottom edge\n if (rect.bottom > window.innerHeight - margin) {\n const overflow = rect.bottom - window.innerHeight + margin;\n if (el.style.top) {\n el.style.top = `${parseFloat(el.style.top) - overflow}px`;\n } else if (el.style.bottom) {\n // Already anchored to bottom, just clamp\n el.style.bottom = `${margin}px`;\n } else {\n el.style.top = `${rect.top - overflow}px`;\n }\n }\n // Clamp top edge\n if (rect.top < margin) {\n if (el.style.top) {\n el.style.top = `${margin}px`;\n } else if (el.style.bottom) {\n el.style.bottom = `${window.innerHeight - rect.height - margin}px`;\n }\n }\n });\n });\n\n const density = getDensity();\n\n const menu = (\n <div ref={ref}\n className={`fixed z-[400] rounded-2xl ${density === 'tight' ? 'py-1' : density === 'large' ? 'py-2' : 'py-1.5'} ${className}`}\n style={{ minWidth, animation: 'popup-in 0.12s ease-out', ...glassStyle(), ...style }}>\n {children}\n <style>{`@keyframes popup-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }`}</style>\n </div>\n );\n\n return portal ? createPortal(menu, document.body) : menu;\n}\n\n/** A clickable menu item */\nexport interface PopupMenuItemProps {\n onClick?: () => void;\n children: ReactNode;\n className?: string;\n danger?: boolean;\n disabled?: boolean;\n /**\n * The menu semantics, when this item is part of one.\n *\n * A context menu on the desktop is a list of buttons and needs none of this;\n * a dropdown attached to a trigger is a `menu` whose items are `menuitem`s\n * with one tab stop between them. Both draw the same item, so the styling\n * lives here rather than being copied into whichever one is newer.\n */\n role?: 'menuitem';\n tabIndex?: number;\n id?: string;\n onMouseEnter?: () => void;\n}\n\n/**\n * `forwardRef`, not a `ref` prop.\n *\n * Taking `ref` as an ordinary prop is React 19 only. This package's peer range\n * is `react: \">=18\"`, and React 18 strips `ref` from a function component's\n * props and warns — so on an 18 consumer the ref would silently never attach,\n * `DropdownMenu`'s `itemRefs` would all be null, and its arrow keys would move\n * `tabIndex` while DOM focus stayed put. A keyboard-dead menu with no error.\n */\nexport const PopupMenuItem = forwardRef<HTMLButtonElement, PopupMenuItemProps>(function PopupMenuItem({\n onClick, children, className = '', danger, disabled,\n role, tabIndex, id, onMouseEnter,\n}, ref) {\n const density = getDensity();\n // Vertical gap between items. `normal` sits a little tighter than the raw\n // size padding; `large` adds a bit more room. Floored at the tight value so\n // the reduced `normal` never drops below `tight` at the smallest menu size.\n const itemPadY = density === 'tight' ? '0.25rem'\n : density === 'large' ? '0.6rem'\n : 'max(0.25rem, calc(var(--menu-padding-y, 0.5rem) - 0.1rem))';\n return (\n <button\n ref={ref}\n id={id}\n role={role}\n tabIndex={tabIndex}\n onClick={onClick}\n onMouseEnter={onMouseEnter}\n disabled={disabled}\n className={`w-full flex items-center gap-2 text-left transition-colors rounded-lg mx-auto\n ${danger ? 'text-red-600 hover:bg-red-50' : 'text-gray-700 hover:bg-blue-50 hover:text-blue-700'}\n ${disabled ? 'opacity-40 cursor-not-allowed' : ''}\n ${className}`}\n style={{\n width: 'calc(100% - 8px)',\n marginLeft: 4,\n marginRight: 4,\n fontSize: 'var(--menu-font-size, 14px)',\n paddingLeft: 'var(--menu-padding-x, 1rem)',\n paddingRight: 'var(--menu-padding-x, 1rem)',\n paddingTop: itemPadY,\n paddingBottom: itemPadY,\n }}\n >\n {children}\n </button>\n );\n});\n\n/** A divider line between menu items */\nexport function PopupMenuDivider() {\n const density = getDensity();\n return <div className={`border-t ${GLASS_DIVIDER} ${density === 'tight' ? 'my-0.5' : density === 'large' ? 'my-1.5' : 'my-1'} mx-3`} />;\n}\n\n/** A section header label */\nexport function PopupMenuLabel({ children }: { children: ReactNode }) {\n const density = getDensity();\n const labelPadY = density === 'tight' ? '0.125rem' : density === 'large' ? '0.375rem' : '0.25rem';\n return (\n <div\n className=\"text-[10px] font-medium text-gray-400 uppercase tracking-wider\"\n style={{\n paddingLeft: 'var(--menu-padding-x, 1rem)',\n paddingRight: 'var(--menu-padding-x, 1rem)',\n paddingTop: labelPadY,\n paddingBottom: labelPadY,\n }}\n >\n {children}\n </div>\n );\n}\n\n/** An icon helper */\nexport function MenuIcon({ d, className = 'h-4 w-4 text-gray-400' }: { d: string; className?: string }) {\n return (\n <svg className={className} fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={1.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d={d} />\n </svg>\n );\n}\n","/**\n * Shell-side user-preferences surface — the package never owns where prefs\n * live. The consumer supplies a hook returning the current prefs object plus\n * a save callback; the shell reads keys it knows about (theme,\n * taskbar_position, taskbar_size, world_clocks, favorite_documents,\n * desktop_folders, desktop_snap, notepad_notes, …) and patches them.\n *\n * When no provider is mounted, the shell falls back to localStorage scoped\n * by `storageKey` — so the package works out of the box for a backend-less\n * consumer.\n */\nimport { createContext, useContext, useState, useCallback, type ReactNode } from 'react';\n\nexport interface ShellPrefsAdapter {\n /** The current prefs object. The shell reads its known keys directly off\n * this; consumer-private keys are ignored. */\n prefs: Record<string, any>;\n /** Patch a subset of prefs. The shell calls this with shallow diffs. */\n save: (patch: Record<string, any>) => Promise<void> | void;\n}\n\nconst ShellPrefsContext = createContext<ShellPrefsAdapter | null>(null);\n\nexport function ShellPrefsProvider({\n value,\n children,\n}: { value: ShellPrefsAdapter; children: ReactNode }) {\n return <ShellPrefsContext.Provider value={value}>{children}</ShellPrefsContext.Provider>;\n}\n\n/** Default localStorage-backed adapter — useful when the consumer doesn't\n * ship a backend. Pass the result into <ShellPrefsProvider value={…}>.\n *\n * `defaults` are merged behind whatever's already stored, so they only\n * apply for keys the user hasn't set yet. Useful for opting out of bundled\n * features (e.g. `{ show_desktop_version: false }`). */\nexport function useLocalStoragePrefs(\n storageKey = 'react-os-shell:prefs',\n defaults?: Record<string, any>,\n): ShellPrefsAdapter {\n const [prefs, setPrefs] = useState<Record<string, any>>(() => {\n try {\n const stored = JSON.parse(localStorage.getItem(storageKey) || '{}');\n return { ...(defaults ?? {}), ...stored };\n } catch { return { ...(defaults ?? {}) }; }\n });\n const save = useCallback((patch: Record<string, any>) => {\n setPrefs(prev => {\n const next = { ...prev, ...patch };\n try { localStorage.setItem(storageKey, JSON.stringify(next)); } catch {}\n return next;\n });\n }, [storageKey]);\n return { prefs, save };\n}\n\n/** Returns the active prefs adapter. When no provider is mounted, returns a\n * no-op adapter that reads as empty and silently drops saves — components\n * still render, but persistence is a no-op. */\nexport function useShellPrefs(): ShellPrefsAdapter {\n const ctx = useContext(ShellPrefsContext);\n if (ctx) return ctx;\n return { prefs: {}, save: () => {} };\n}\n"]}
@@ -122,10 +122,13 @@ function Dialog({
122
122
  blocking = false,
123
123
  size = "md",
124
124
  initialFocus,
125
+ "aria-label": ariaLabel,
125
126
  className = ""
126
127
  }) {
127
128
  const panelRef = useRef(null);
128
129
  const bodyId = useId();
130
+ const showClose = !blocking && !footer;
131
+ const titleId = useId();
129
132
  useFocusTrap(panelRef, open, initialFocus);
130
133
  useScrollLock(open);
131
134
  useEffect(() => {
@@ -138,30 +141,43 @@ function Dialog({
138
141
  }, [open, blocking, onClose]);
139
142
  if (!open) return null;
140
143
  return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-[9999]", role: "presentation", children: [
144
+ /* @__PURE__ */ jsx("div", { className: "fixed inset-0 bg-black/30", "aria-hidden": "true" }),
141
145
  /* @__PURE__ */ jsx(
142
146
  "div",
143
147
  {
144
- className: "fixed inset-0 bg-black/30",
145
- onClick: blocking ? void 0 : onClose,
146
- "aria-hidden": "true"
148
+ className: "fixed inset-0 flex items-center justify-center p-4",
149
+ onClick: blocking ? void 0 : ((e) => {
150
+ if (e.target === e.currentTarget) onClose();
151
+ }),
152
+ children: /* @__PURE__ */ jsxs(
153
+ "div",
154
+ {
155
+ ref: panelRef,
156
+ role: "dialog",
157
+ "aria-modal": "true",
158
+ "aria-labelledby": title ? titleId : void 0,
159
+ "aria-label": title ? void 0 : ariaLabel,
160
+ "aria-describedby": children ? bodyId : void 0,
161
+ className: `relative w-full ${SIZES[size]} rounded-lg bg-white p-6 shadow-xl ${className}`.trim(),
162
+ children: [
163
+ title && /* @__PURE__ */ jsx("h2", { id: titleId, className: `${showClose ? "pr-8 " : ""}text-base font-semibold text-gray-900`, children: title }),
164
+ children && /* @__PURE__ */ jsx("div", { id: bodyId, className: "mt-2 text-sm text-gray-600", children }),
165
+ footer && /* @__PURE__ */ jsx("div", { className: "mt-6 flex justify-end gap-3", children: footer }),
166
+ showClose && /* @__PURE__ */ jsx(
167
+ "button",
168
+ {
169
+ type: "button",
170
+ onClick: onClose,
171
+ "aria-label": "Close",
172
+ className: "absolute right-3 top-3 rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400",
173
+ children: /* @__PURE__ */ jsx("svg", { className: "h-5 w-5", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M6 6l8 8M14 6l-8 8" }) })
174
+ }
175
+ )
176
+ ]
177
+ }
178
+ )
147
179
  }
148
- ),
149
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 flex items-center justify-center p-4", children: /* @__PURE__ */ jsxs(
150
- "div",
151
- {
152
- ref: panelRef,
153
- role: "dialog",
154
- "aria-modal": "true",
155
- "aria-label": typeof title === "string" ? title : void 0,
156
- "aria-describedby": children ? bodyId : void 0,
157
- className: `relative w-full ${SIZES[size]} rounded-lg bg-white p-6 shadow-xl ${className}`.trim(),
158
- children: [
159
- title && /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-gray-900", children: title }),
160
- children && /* @__PURE__ */ jsx("div", { id: bodyId, className: "mt-2 text-sm text-gray-600", children }),
161
- footer && /* @__PURE__ */ jsx("div", { className: "mt-6 flex justify-end gap-3", children: footer })
162
- ]
163
- }
164
- ) })
180
+ )
165
181
  ] });
166
182
  }
167
183
  var ConfirmContext = createContext(() => Promise.resolve(false));
@@ -410,5 +426,5 @@ function ConfirmProvider({ children }) {
410
426
  }
411
427
 
412
428
  export { ConfirmProvider, Dialog, confirm, confirmDestructive, prompt, registerModalEscapeInterceptor, runEscapeInterceptors, useConfirm, useFocusTrap, useScrollLock };
413
- //# sourceMappingURL=chunk-OXLLRKBC.js.map
414
- //# sourceMappingURL=chunk-OXLLRKBC.js.map
429
+ //# sourceMappingURL=chunk-CDR2Q64Q.js.map
430
+ //# sourceMappingURL=chunk-CDR2Q64Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/focusTrap.ts","../src/shell/escapeInterceptors.ts","../src/shell/Dialog.tsx","../src/shell/ConfirmDialog.tsx"],"names":["useEffect","jsx","useRef","jsxs"],"mappings":"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","file":"chunk-CDR2Q64Q.js","sourcesContent":["/**\n * Focus containment and scroll locking for a modal surface — the two things\n * Headless UI was doing for `ConfirmDialog` before it was rewritten to import\n * nothing.\n *\n * Both are small, and both are the kind of small that is wrong in a way nobody\n * notices until someone navigates by keyboard: focus tabbing out of a dialog\n * onto the page behind it reads as the dialog having closed, and a page that\n * scrolls under a fixed overlay loses the user's place when it reopens.\n */\nimport { useEffect, type RefObject } from 'react';\n\n/**\n * Focusable candidates, in DOM order. Deliberately excludes anything with\n * `tabindex=\"-1\"` — that attribute means \"focusable by script, skipped by Tab\",\n * and treating it as a stop is the usual bug in a hand-rolled trap.\n */\nconst FOCUSABLE = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE))\n // A hidden element still matches the selector. offsetParent is null for\n // anything display:none or inside it — cheap, and right for our markup.\n .filter(el => el.offsetParent !== null || el === document.activeElement);\n}\n\n/**\n * While `active`, keep Tab inside `ref`, move focus in on open, and restore it\n * to wherever it came from on close.\n *\n * Restoring matters more than trapping: a dialog opened from a button in a\n * toolbar should hand focus back to that button, or a keyboard user is dumped\n * at the top of the document and has to find their place again.\n */\nexport function useFocusTrap(\n ref: RefObject<HTMLElement | null>,\n active: boolean,\n initialFocus?: RefObject<HTMLElement | null>,\n): void {\n useEffect(() => {\n if (!active) return;\n const root = ref.current;\n if (!root) return;\n\n const previous = document.activeElement as HTMLElement | null;\n\n // Focus the requested element, else the first natural stop, else the\n // container itself so the trap has somewhere to hold focus at all.\n const first = initialFocus?.current ?? focusable(root)[0] ?? root;\n if (first === root && !root.hasAttribute('tabindex')) root.setAttribute('tabindex', '-1');\n first.focus();\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key !== 'Tab') return;\n const stops = focusable(root);\n if (stops.length === 0) { e.preventDefault(); return; }\n const firstStop = stops[0];\n const lastStop = stops[stops.length - 1];\n const current = document.activeElement;\n\n // Wrap at both ends. Also catches focus having escaped the dialog\n // entirely (current is outside root), which happens if something inside\n // was removed while focused.\n if (e.shiftKey && (current === firstStop || !root.contains(current))) {\n e.preventDefault();\n lastStop.focus();\n } else if (!e.shiftKey && (current === lastStop || !root.contains(current))) {\n e.preventDefault();\n firstStop.focus();\n }\n };\n\n document.addEventListener('keydown', onKeyDown, true);\n return () => {\n document.removeEventListener('keydown', onKeyDown, true);\n // Only take focus back if it is still inside the dialog. If something\n // else claimed it in the meantime, stealing it back is the rude option.\n if (!root.contains(document.activeElement) && document.activeElement !== document.body) return;\n previous?.focus?.();\n };\n }, [ref, active, initialFocus]);\n}\n\n/**\n * While `active`, stop the document behind the overlay from scrolling.\n *\n * Compensates for the scrollbar's width so the page does not visibly jump\n * sideways as it locks — the artefact that makes a dialog feel cheap. Nested\n * locks are counted, so two stacked dialogs unlock exactly once.\n */\nlet lockCount = 0;\nlet restore: { overflow: string; paddingRight: string } | null = null;\n\nexport function useScrollLock(active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const body = document.body;\n if (lockCount === 0) {\n restore = { overflow: body.style.overflow, paddingRight: body.style.paddingRight };\n const gap = window.innerWidth - document.documentElement.clientWidth;\n body.style.overflow = 'hidden';\n if (gap > 0) body.style.paddingRight = `${gap}px`;\n }\n lockCount += 1;\n return () => {\n lockCount -= 1;\n if (lockCount === 0 && restore) {\n body.style.overflow = restore.overflow;\n body.style.paddingRight = restore.paddingRight;\n restore = null;\n }\n };\n }, [active]);\n}\n","/**\n * The Escape-interceptor seam — deliberately a leaf that imports NOTHING.\n *\n * This lived inside `shell/Modal.tsx` until 4.16.0, and that one detail decided\n * what the whole package cost to import. `forms/Select.tsx` registers an\n * interceptor so Escape closes its portaled listbox instead of the window\n * around it — a two-line need — and reaching it through Modal pulled the entire\n * window manager into any bundle containing a dropdown: react-router-dom,\n * @tanstack/react-query, axios, @headlessui/react and @heroicons/react, plus\n * Modal's module-scope `localStorage` reads, which run at import time. An app\n * that wanted a text field and a select paid for a desktop it never rendered.\n *\n * Splitting the Set out is the whole fix: Modal and Select now both import this\n * file, and neither imports the other. `registerModalEscapeInterceptor` is\n * re-exported from Modal (and so from the package root) with the same binding\n * identity, so nothing downstream changes.\n *\n * NOTHING may be imported here — not React, not a type, not a sibling. The\n * moment it has an edge, every consumer of `react-os-shell/ui` inherits that\n * edge's transitive graph, which is exactly the failure this module exists to\n * prevent. `tests/uiEntryIsPeerFree.test.ts` enforces it.\n *\n * ── Escape interceptors ────────────────────────────────────────────────────\n * Window content can claim an Escape press before the topmost-modal handler\n * closes the window — e.g. the DXF Preview's measure tool exits AutoCAD-style\n * (clear the command input, then the tool, and only a further Esc closes the\n * window). Interceptors run in REVERSE registration order — most recently\n * registered first — and the first to return true consumes the event (the\n * modal neither closes nor sees it). An interceptor is global, so it must\n * check it belongs to the *active* modal itself (via `getActiveModalId()`)\n * before consuming.\n *\n * Reverse, because registration order tracks stacking order and Escape belongs\n * to whatever is on top. Two stacked `Dialog`s each register one; oldest-first\n * would hand Escape to the dialog UNDERNEATH and dismiss the wrong surface,\n * leaving the one the user is looking at on screen. This was invisible while\n * `ConfirmDialog` was the only multi-dialog registrant, because it registered\n * a single interceptor and hand-ordered its three dialogs inside it.\n *\n * Registrants that check `getActiveModalId()` are unaffected: at most one of\n * them can consume, so the order they are offered the event in cannot change\n * which one takes it.\n *\n * ── Draining the Set with no shell present ────────────────────────────────\n * This used to say that nothing draining the Set was fine, because a component\n * would handle Escape itself. That holds for `Select`, which has its own\n * `onKeyDown`. It did NOT hold for `Dialog`, which registers here and has no\n * other handler — so on a till or a routed portal, the two places `Dialog`\n * exists to serve, Escape did nothing at all.\n *\n * So the Set drains itself. The first registration attaches one document-level\n * capture listener; the last removal takes it away.\n *\n * It cannot double-fire when a shell IS present. `Modal` listens on `window`\n * in the capture phase, and capture runs window before document: Modal's\n * handler goes first, calls `runEscapeInterceptors`, and stops propagation when\n * one consumes — so this listener never sees the event. When Modal declines\n * (it is not the topmost window), this runs and the dialog floating above it\n * still gets its Escape, which is the right outcome anyway.\n *\n * Ordering across stacked dialogs is unchanged, because it is still the same\n * reverse walk deciding who consumes — not the listener that started it.\n */\nconst escapeInterceptors = new Set<(e: KeyboardEvent) => boolean>();\n\n/**\n * The self-drain listener, attached while at least one interceptor is\n * registered. Kept in module scope rather than per-registration so N dialogs\n * share one listener and the reverse walk stays the only thing deciding who\n * consumes.\n */\nlet detachDrain: (() => void) | null = null;\n\nfunction attachDrain(): void {\n // `document` is absent when this module is imported on a server; there is no\n // Escape to handle there, and registration still works for when it hydrates.\n if (detachDrain || typeof document === 'undefined') return;\n const onKeyDown = (e: KeyboardEvent): void => {\n if (e.key !== 'Escape') return;\n if (runEscapeInterceptors(e)) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n document.addEventListener('keydown', onKeyDown, true);\n detachDrain = () => document.removeEventListener('keydown', onKeyDown, true);\n}\n\n/** Register an Escape interceptor; returns an unregister function. */\nexport function registerModalEscapeInterceptor(fn: (e: KeyboardEvent) => boolean): () => void {\n escapeInterceptors.add(fn);\n attachDrain();\n return () => {\n escapeInterceptors.delete(fn);\n if (escapeInterceptors.size === 0 && detachDrain) {\n detachDrain();\n detachDrain = null;\n }\n };\n}\n\n/** Run the registered interceptors, most recent first; true if one consumed\n * the event. Internal to the shell — the window key handler calls this,\n * consumers never do. */\nexport function runEscapeInterceptors(e: KeyboardEvent): boolean {\n // A Set iterates in insertion order and has no reverse iterator, so snapshot\n // it. The copy also makes the walk safe against an interceptor that\n // unregisters itself while handling the event — which is exactly what a\n // dialog closing on Escape does.\n const ordered = Array.from(escapeInterceptors);\n for (let i = ordered.length - 1; i >= 0; i--) {\n try {\n if (ordered[i](e)) return true;\n } catch { /* a broken interceptor must not block closing */ }\n }\n return false;\n}\n","/**\n * Dialog — a modal sheet that is NOT a shell window.\n *\n * The distinction matters. `Modal` is a window: it has a title bar, it can be\n * minimised, dragged, stacked and restored, and it lives in the window manager's\n * activation order. This is the other thing — an overlay that interrupts, is\n * answered, and goes away. A till has no desktop to put a window on, and a\n * routed portal page has no window manager either, so both need this one.\n *\n * It imports nothing but React, which is the point: it is what lets\n * `ConfirmDialog` — and every consumer of `react-os-shell/ui` — stop depending\n * on Headless UI and Heroicons. Focus containment and scroll locking come from\n * `./focusTrap` rather than a library.\n */\nimport { useEffect, useId, useRef, type ReactNode, type RefObject } from 'react';\nimport { useFocusTrap, useScrollLock } from './focusTrap';\nimport { registerModalEscapeInterceptor } from './escapeInterceptors';\n\nexport type DialogSize = 'sm' | 'md' | 'lg';\n\nexport interface DialogProps {\n open: boolean;\n /** Called for Escape, a backdrop click, and the close control. */\n onClose: () => void;\n title?: ReactNode;\n children?: ReactNode;\n /** Action row, right-aligned below the body. */\n footer?: ReactNode;\n /**\n * Escape and backdrop clicks do nothing — the only way out is an action in\n * the footer. For a state the user must actually resolve rather than dismiss,\n * such as a sale whose outcome is unknown. Use it sparingly: a dialog that\n * cannot be dismissed is a dead end if its actions ever fail to render.\n */\n blocking?: boolean;\n size?: DialogSize;\n /**\n * Element to focus on open. Defaults to the first focusable thing in the\n * dialog — which for a destructive confirm should be Cancel, not the button\n * that does the irreversible thing.\n */\n initialFocus?: RefObject<HTMLElement | null>;\n /**\n * Names the dialog when it carries no visible `title`. Without a name it is\n * announced as just \"dialog\", which tells a screen reader user nothing about\n * what opened. Ignored when `title` is set — a visible label always wins over\n * a parallel invisible one, or the two drift.\n */\n 'aria-label'?: string;\n className?: string;\n}\n\nconst SIZES: Record<DialogSize, string> = {\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n};\n\nexport default function Dialog({\n open, onClose, title, children, footer, blocking = false, size = 'md', initialFocus,\n 'aria-label': ariaLabel, className = '',\n}: DialogProps) {\n const panelRef = useRef<HTMLDivElement>(null);\n const bodyId = useId();\n // One source of truth for the close control: the button and the title's\n // gutter are the same decision, and computing it twice is how they drift.\n const showClose = !blocking && !footer;\n const titleId = useId();\n\n useFocusTrap(panelRef, open, initialFocus);\n useScrollLock(open);\n\n // Escape goes through the shell's interceptor rather than a local listener.\n // A dialog floats above every shell window but is absent from the window\n // activation order, so an unclaimed Escape reaches the frontmost Modal and\n // closes the WINDOW BEHIND the dialog. Claiming it here means the dialog\n // closes and the window does not. With no shell present nothing else is\n // listening and this is simply where Escape is handled.\n useEffect(() => {\n if (!open) return;\n return registerModalEscapeInterceptor(() => {\n if (blocking) return true; // claimed, but deliberately does nothing\n onClose();\n return true;\n });\n }, [open, blocking, onClose]);\n\n if (!open) return null;\n\n return (\n <div className=\"fixed inset-0 z-[9999]\" role=\"presentation\">\n {/* No handler here. The dismissal layer below is also `fixed inset-0`\n and sits on top of this, so a click in this region never reaches the\n scrim — an onClick here would be unreachable in every case, and two\n dismissal paths where only one can fire is how the next reader loses\n an afternoon. */}\n <div className=\"fixed inset-0 bg-black/30\" aria-hidden=\"true\" />\n {/* The scrim above covers the viewport, and so does this — which sits on\n top of it. A click \"outside the dialog\" therefore landed HERE and\n never reached the scrim's handler, so clicking away never dismissed\n anything and a dialog with no buttons could only be left with Escape.\n The dismissal lives on this layer as well, gated on the target being\n the layer itself so a click inside the panel does not bubble into it. */}\n <div\n className=\"fixed inset-0 flex items-center justify-center p-4\"\n onClick={blocking ? undefined : (e => { if (e.target === e.currentTarget) onClose(); })}\n >\n <div\n ref={panelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n // Points at the rendered heading rather than re-deriving the name\n // from the prop: `title` is a ReactNode, so an element title — an\n // icon beside a word, a count in a badge — used to fall through the\n // string check and leave the dialog with no name at all.\n aria-labelledby={title ? titleId : undefined}\n aria-label={title ? undefined : ariaLabel}\n // The body is the dialog's description, and saying so is what makes a\n // screen reader read the question along with the title when focus\n // lands here. Without it the user hears \"Cancel order, dialog\" and\n // has to go looking for what cancelling actually does.\n aria-describedby={children ? bodyId : undefined}\n className={`relative w-full ${SIZES[size]} rounded-lg bg-white p-6 shadow-xl ${className}`.trim()}\n >\n {/* `id` for the accessible name, and a right gutter ONLY when the\n close button is actually there to need it — a dialog with a\n footer draws no cross, and reserving 2rem beside its title just\n indents the heading away from the edge for nothing. */}\n {title && (\n <h2 id={titleId} className={`${showClose ? 'pr-8 ' : ''}text-base font-semibold text-gray-900`}>\n {title}\n </h2>\n )}\n {children && <div id={bodyId} className=\"mt-2 text-sm text-gray-600\">{children}</div>}\n {footer && <div className=\"mt-6 flex justify-end gap-3\">{footer}</div>}\n {/* A way out that is visible, for a dialog that offers none of its\n own. Escape and the backdrop both work, but neither is on screen,\n so a dialog whose body is just an image could be left only by a\n keyboard shortcut nothing announces.\n\n NOT when there is a footer. A confirm already offers two labelled\n choices, and an unlabelled cross beside \"Discard\" and \"Keep\n Editing\" is a third exit that says nothing about which one it\n means — worse on the decision where it matters most. It also put\n a stop in the middle of that dialog's Tab cycle, which is the\n thing the browser test guards.\n\n LAST in the DOM though drawn top-right: first, it became the\n dialog's first tab stop and the first button a focus trap finds. */}\n {showClose && (\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n className=\"absolute right-3 top-3 rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n >\n <svg className=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden=\"true\">\n <path d=\"M6 6l8 8M14 6l-8 8\" />\n </svg>\n </button>\n )}\n </div>\n </div>\n </div>\n );\n}\n","/**\n * confirm / confirmDestructive / prompt — the imperative dialogs, callable from\n * any click handler without a hook.\n *\n * Rebuilt on `./Dialog` in 4.18.0. It previously rendered Headless UI, which\n * meant every consumer of `react-os-shell/ui` inherited `@headlessui/react` and\n * `@heroicons/react` to ask someone a yes/no question. Focus containment and\n * scroll locking now come from `./focusTrap`; the icons are inline SVG. The\n * three dialogs' behaviour is otherwise unchanged, with two deliberate\n * exceptions noted below.\n *\n * FAIL-CLOSED. Each global starts as a function resolving to false/null, so a\n * `confirm()` with no provider mounted answers \"no\" rather than hanging or\n * throwing. A dialog nobody can see must never resolve true — that is the\n * difference between a no-op and a deletion.\n *\n * CONFIRMS QUEUE, they do not drop. A second `confirm()` while one is open\n * waits its turn and shows afterwards. Dropping it would silently resolve a\n * question the user was never asked, and the caller cannot tell that from a\n * genuine \"no\".\n */\nimport { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';\nimport Dialog from './Dialog';\n\ninterface ConfirmOptions {\n title?: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n variant?: 'danger' | 'warning' | 'info';\n}\n\ntype ConfirmFn = (options: ConfirmOptions | string) => Promise<boolean>;\ninterface PendingConfirm {\n id: number;\n options: ConfirmOptions;\n resolve: (value: boolean) => void;\n}\n\nconst ConfirmContext = createContext<ConfirmFn>(() => Promise.resolve(false));\n\nexport const useConfirm = () => useContext(ConfirmContext);\n\n// Global callable — works without a hook, usable from any click handler\nlet globalConfirmFn: ConfirmFn = () => Promise.resolve(false);\nexport const confirm = (opts: ConfirmOptions | string) => globalConfirmFn(opts);\n\ninterface DestructiveConfirmOptions {\n title?: string;\n message: string;\n /**\n * Require typing this word (case-sensitive) before the action is enabled —\n * for something genuinely irreversible at scale, like dropping a tenant.\n *\n * OPTIONAL since 4.18.0. Omit it for a plain two-button destructive confirm.\n * Type-to-confirm assumes a keyboard, and a touch device that has none (a\n * till, a warehouse scanner) cannot satisfy it at all — the dialog becomes an\n * unanswerable question. Ask for a word when the cost of a mis-tap is high\n * enough to justify making the user work; not by default.\n */\n confirmWord?: string;\n /** Label for the action button when there is no `confirmWord`. */\n confirmLabel?: string;\n cancelLabel?: string;\n variant?: 'danger' | 'warning';\n}\ntype DestructiveConfirmFn = (options: DestructiveConfirmOptions) => Promise<boolean>;\nlet globalDestructiveConfirmFn: DestructiveConfirmFn = () => Promise.resolve(false);\nexport const confirmDestructive = (opts: DestructiveConfirmOptions) => globalDestructiveConfirmFn(opts);\n\n// Prompt — windowed replacement for native window.prompt(). Resolves to the\n// trimmed string, or null if the user cancelled. Empty input counts as\n// cancel by default; pass `allowEmpty: true` to opt in.\ninterface PromptOptions {\n title?: string;\n message?: string;\n defaultValue?: string;\n placeholder?: string;\n confirmLabel?: string;\n cancelLabel?: string;\n allowEmpty?: boolean;\n}\ntype PromptFn = (options: PromptOptions | string) => Promise<string | null>;\nlet globalPromptFn: PromptFn = () => Promise.resolve(null);\nexport const prompt = (opts: PromptOptions | string) => globalPromptFn(opts);\n\nconst TONE: Record<'danger' | 'warning' | 'info', { icon: string; button: string }> = {\n danger: { icon: 'text-red-600 bg-red-100', button: 'bg-red-600 hover:bg-red-700 text-white' },\n warning: { icon: 'text-yellow-600 bg-yellow-100', button: 'bg-yellow-600 hover:bg-yellow-700 text-white' },\n info: { icon: 'text-blue-600 bg-blue-100', button: 'bg-blue-600 hover:bg-blue-700 text-white' },\n};\n\nconst CANCEL_BTN =\n 'bg-white text-gray-700 border border-gray-300 px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-50';\n\n/** Inline replacements for the two Heroicons this file used to import. */\nfunction WarningIcon() {\n return (\n <svg className=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={1.5} aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\" />\n </svg>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg className=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={1.5} aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n );\n}\n\nexport function ConfirmProvider({ children }: { children: React.ReactNode }) {\n const [open, setOpen] = useState(false);\n const [options, setOptions] = useState<ConfirmOptions>({ message: '' });\n const [confirmId, setConfirmId] = useState(0);\n const resolveRef = useRef<(value: boolean) => void>();\n const activeConfirmIdRef = useRef<number | null>(null);\n const nextConfirmIdRef = useRef(0);\n const confirmQueueRef = useRef<PendingConfirm[]>([]);\n\n // Cancel is the default focus target in every dialog here. The destructive\n // action sits on the RIGHT and is never what Enter or a stray keypress hits.\n const cancelRef = useRef<HTMLButtonElement>(null);\n const dCancelRef = useRef<HTMLButtonElement>(null);\n\n const showConfirm = useCallback((request: PendingConfirm) => {\n activeConfirmIdRef.current = request.id;\n resolveRef.current = request.resolve;\n setConfirmId(request.id);\n setOptions(request.options);\n setOpen(true);\n }, []);\n\n const confirmFn: ConfirmFn = useCallback((opts) => {\n const normalized = typeof opts === 'string' ? { message: opts } : opts;\n return new Promise<boolean>((resolve) => {\n const request = { id: ++nextConfirmIdRef.current, options: normalized, resolve };\n if (activeConfirmIdRef.current === null) showConfirm(request);\n else confirmQueueRef.current.push(request);\n });\n }, [showConfirm]);\n\n // Destructive confirm state\n const [dOpen, setDOpen] = useState(false);\n const [dOptions, setDOptions] = useState<DestructiveConfirmOptions>({ message: '' });\n const [dInput, setDInput] = useState('');\n const dResolveRef = useRef<(value: boolean) => void>();\n\n const destructiveConfirmFn: DestructiveConfirmFn = useCallback((opts) => {\n setDOptions(opts);\n setDInput('');\n setDOpen(true);\n return new Promise<boolean>((resolve) => {\n dResolveRef.current = resolve;\n });\n }, []);\n\n // Prompt state\n const [pOpen, setPOpen] = useState(false);\n const [pOptions, setPOptions] = useState<PromptOptions>({});\n const [pInput, setPInput] = useState('');\n const pResolveRef = useRef<(value: string | null) => void>();\n\n const promptFn: PromptFn = useCallback((opts) => {\n const normalized = typeof opts === 'string' ? { message: opts } : opts;\n setPOptions(normalized);\n setPInput(normalized.defaultValue ?? '');\n setPOpen(true);\n return new Promise<string | null>((resolve) => {\n pResolveRef.current = resolve;\n });\n }, []);\n\n useEffect(() => {\n globalConfirmFn = confirmFn;\n globalDestructiveConfirmFn = destructiveConfirmFn;\n globalPromptFn = promptFn;\n }, [confirmFn, destructiveConfirmFn, promptFn]);\n\n const handleClose = (id: number, result: boolean) => {\n // A callback captured by the previous dialog must never settle the next\n // queued request after the active request advances.\n if (activeConfirmIdRef.current !== id) return;\n const resolve = resolveRef.current;\n const next = confirmQueueRef.current.shift();\n if (next) showConfirm(next);\n else {\n activeConfirmIdRef.current = null;\n resolveRef.current = undefined;\n setOpen(false);\n }\n resolve?.(result);\n };\n\n const handleDClose = (result: boolean) => {\n setDOpen(false);\n setDInput('');\n dResolveRef.current?.(result);\n };\n\n const handlePClose = (commit: boolean) => {\n if (commit) {\n const trimmed = pInput.trim();\n if (!trimmed && !pOptions.allowEmpty) {\n // Empty + not opted-in to allow empty: treat Save as Cancel.\n setPOpen(false);\n pResolveRef.current?.(null);\n return;\n }\n setPOpen(false);\n pResolveRef.current?.(trimmed);\n } else {\n setPOpen(false);\n pResolveRef.current?.(null);\n }\n };\n\n // Escape is claimed by each Dialog itself now, and interceptors run most\n // recent first, so a stack of these dismisses top-down without the provider\n // hand-ordering them the way it used to.\n\n const variant = options.variant || (options.confirmLabel?.toLowerCase().includes('delete') || options.message.toLowerCase().includes('delete') ? 'danger' : 'info');\n const tone = TONE[variant];\n const dTone = TONE[dOptions.variant === 'warning' ? 'warning' : 'danger'];\n const dWord = dOptions.confirmWord;\n const dSatisfied = dWord == null || dInput === dWord;\n\n return (\n <ConfirmContext.Provider value={confirmFn}>\n {children}\n\n <Dialog\n open={open}\n onClose={() => handleClose(confirmId, false)}\n initialFocus={cancelRef}\n footer={\n <>\n <button ref={cancelRef} type=\"button\" onClick={() => handleClose(confirmId, false)} className={CANCEL_BTN}>\n {options.cancelLabel || 'Cancel'}\n </button>\n <button type=\"button\" onClick={() => handleClose(confirmId, true)}\n className={`px-4 py-2 text-sm font-medium rounded-lg ${tone.button}`}>\n {options.confirmLabel || 'OK'}\n </button>\n </>\n }\n >\n <div className=\"flex gap-4\">\n <div className={`shrink-0 w-10 h-10 rounded-full flex items-center justify-center ${tone.icon}`}>\n <WarningIcon />\n </div>\n <div className=\"flex-1\">\n <h2 className=\"text-base font-semibold text-gray-900\">{options.title || 'Confirm'}</h2>\n <p className=\"mt-2 text-sm text-gray-600\">{options.message}</p>\n </div>\n </div>\n </Dialog>\n\n {/* Destructive confirm — optionally gated on typing a word */}\n <Dialog\n open={dOpen}\n onClose={() => handleDClose(false)}\n // With a word to type, focus the input: that is the task. Without one,\n // focus Cancel, so the irreversible button is never one Enter away.\n initialFocus={dWord != null ? undefined : dCancelRef}\n footer={\n <>\n <button ref={dCancelRef} type=\"button\" onClick={() => handleDClose(false)} className={CANCEL_BTN}>\n {dOptions.cancelLabel || 'Dismiss'}\n </button>\n <button type=\"button\" onClick={() => handleDClose(true)} disabled={!dSatisfied}\n className={`px-4 py-2 text-sm font-medium rounded-lg disabled:opacity-40 ${dTone.button}`}>\n {dWord ?? dOptions.confirmLabel ?? 'Delete'}\n </button>\n </>\n }\n >\n <button\n type=\"button\"\n onClick={() => handleDClose(false)}\n aria-label=\"Close\"\n className=\"absolute right-4 top-4 rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600\"\n >\n <CloseIcon />\n </button>\n <div className=\"flex gap-4\">\n <div className={`shrink-0 w-10 h-10 rounded-full flex items-center justify-center ${dTone.icon}`}>\n <WarningIcon />\n </div>\n <div className=\"flex-1\">\n <h2 className=\"text-base font-semibold text-gray-900 pr-6\">{dOptions.title || 'Confirm Action'}</h2>\n <p className=\"mt-2 text-sm text-gray-600\">{dOptions.message}</p>\n {dWord != null && (\n <>\n <p className=\"mt-3 text-sm text-gray-700\">\n Type <kbd className=\"rounded border border-gray-300 bg-gray-50 px-1.5 py-0.5 text-xs font-bold text-red-600 font-mono\">{dWord}</kbd> to confirm:\n </p>\n <input\n autoFocus\n type=\"text\"\n value={dInput}\n onChange={e => setDInput(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter' && dInput === dWord) handleDClose(true); }}\n placeholder={dWord}\n className=\"mt-2 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-red-500 focus:ring-red-500\"\n />\n </>\n )}\n </div>\n </div>\n </Dialog>\n\n {/* Prompt — windowed replacement for window.prompt() */}\n <Dialog\n open={pOpen}\n onClose={() => handlePClose(false)}\n title={pOptions.title || 'Enter a value'}\n footer={\n <>\n <button type=\"button\" onClick={() => handlePClose(false)} className={CANCEL_BTN}>\n {pOptions.cancelLabel || 'Cancel'}\n </button>\n <button type=\"button\" onClick={() => handlePClose(true)}\n disabled={!pOptions.allowEmpty && !pInput.trim()}\n className=\"bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 text-sm font-medium rounded-lg disabled:opacity-40\">\n {pOptions.confirmLabel || 'OK'}\n </button>\n </>\n }\n >\n {pOptions.message && <p className=\"text-sm text-gray-600\">{pOptions.message}</p>}\n <input\n autoFocus\n type=\"text\"\n value={pInput}\n onChange={(e) => setPInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') handlePClose(true);\n else if (e.key === 'Escape') handlePClose(false);\n }}\n onFocus={(e) => e.target.select()}\n placeholder={pOptions.placeholder}\n className=\"mt-3 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none\"\n />\n </Dialog>\n </ConfirmContext.Provider>\n );\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { VERSION } from './chunk-WCGHIJZR.js';
2
- import { useWindowMenuItem, Modal } from './chunk-JMESET3G.js';
1
+ import { VERSION } from './chunk-NJDH5PHA.js';
2
+ import { useWindowMenuItem, Modal } from './chunk-G7IHPEJE.js';
3
3
  import { navIcons } from './chunk-WNLPTLT6.js';
4
4
  import { useState, useCallback, isValidElement, cloneElement } from 'react';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -75,5 +75,5 @@ function AboutApp({ app }) {
75
75
  }
76
76
 
77
77
  export { AboutApp, BUILTIN_APP_INFO };
78
- //# sourceMappingURL=chunk-TO6ZZHJK.js.map
79
- //# sourceMappingURL=chunk-TO6ZZHJK.js.map
78
+ //# sourceMappingURL=chunk-EG3WEQBH.js.map
79
+ //# sourceMappingURL=chunk-EG3WEQBH.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/apps/_about.tsx"],"names":[],"mappings":";;;;;;AA4BO,IAAM,gBAAA,GAAmB;AAAA,EAC9B,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,cAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,KAAA,EAAO,cAAA;AAAA,IACP,WAAA,EAAa;AAAA,GACf;AAAA,EACA,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,SAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,KAAA,EAAO,UAAA;AAAA,IACP,WAAA,EAAa;AAAA,GACf;AAAA,EACA,SAAA,EAAW;AAAA,IACT,IAAA,EAAM,WAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,KAAA,EAAO,YAAA;AAAA,IACP,WAAA,EAAa;AAAA,GACf;AAAA,EACA,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,SAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,KAAA,EAAO,UAAA;AAAA,IACP,WAAA,EAAa;AAAA,GACf;AAAA,EACA,KAAA,EAAO;AAAA,IACL,IAAA,EAAM,OAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,KAAA,EAAO,QAAA;AAAA,IACP,WAAA,EAAa;AAAA,GACf;AAAA,EACA,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,SAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,KAAA,EAAO,UAAA;AAAA,IACP,WAAA,EAAa;AAAA;AAEjB;AAIA,IAAM,eAAA,uBACH,KAAA,EAAA,EAAI,SAAA,EAAU,yBAAwB,IAAA,EAAK,MAAA,EAAO,SAAQ,WAAA,EAAY,MAAA,EAAO,gBAAe,WAAA,EAAa,GAAA,EACxG,8BAAC,MAAA,EAAA,EAAK,aAAA,EAAc,SAAQ,cAAA,EAAe,OAAA,EAAQ,CAAA,EAAE,oJAAA,EAAqJ,CAAA,EAC5M,CAAA;AAKF,IAAM,iBAAA,uBACH,KAAA,EAAA,EAAI,SAAA,EAAU,WAAU,IAAA,EAAK,MAAA,EAAO,SAAQ,WAAA,EAAY,MAAA,EAAO,gBAAe,WAAA,EAAa,GAAA,EAC1F,8BAAC,MAAA,EAAA,EAAK,aAAA,EAAc,SAAQ,cAAA,EAAe,OAAA,EAAQ,CAAA,EAAE,oIAAA,EAAqI,CAAA,EAC5L,CAAA;AAKa,SAAR,QAAA,CAA0B,EAAE,GAAA,EAAI,EAA0B;AAC/D,EAAA,MAAM,IAAA,GAAO,iBAAiB,GAAG,CAAA;AACjC,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAS,KAAK,CAAA;AACtC,EAAA,MAAM,aAAa,WAAA,CAAY,MAAM,QAAQ,IAAI,CAAA,EAAG,EAAE,CAAA;AACtD,EAAA,iBAAA,CAAkB,CAAA,MAAA,EAAS,IAAA,CAAK,IAAI,CAAA,CAAA,EAAI,YAAY,eAAe,CAAA;AAEnE,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAElB,EAAA,MAAM,UAAA,GAAa,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA;AACtC,EAAA,MAAM,OAAA,GAAU,cAAA,CAAe,UAAU,CAAA,GACrC,YAAA,CAAa,YAA4B,EAAE,SAAA,EAAW,SAAA,EAAkB,CAAA,GACxE,iBAAA;AAEJ,EAAA,uBACE,GAAA,CAAC,KAAA,EAAA,EAAM,IAAA,EAAI,IAAA,EAAC,OAAA,EAAS,MAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,KAAA,EAAO,CAAA,MAAA,EAAS,IAAA,CAAK,IAAI,CAAA,CAAA,EAAI,IAAA,EAAK,IAAA,EAAK,OAAA,EAAO,IAAA,EAAC,UAAA,EAAY,KAAA,EAAO,UAAA,EAAU,IAAA,EAAC,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG,CAAA,EAC3I,QAAA,kBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,uDAAA,EACb,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,2FAAA,EACZ,QAAA,EAAA,OAAA,EACH,CAAA;AAAA,oBACA,GAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,sCAAA,EAAwC,eAAK,IAAA,EAAK,CAAA;AAAA,oBAChE,IAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,qCAAA,EAAsC,QAAA,EAAA;AAAA,MAAA,UAAA;AAAA,MAAS,IAAA,CAAK;AAAA,KAAA,EAAQ,CAAA;AAAA,oBACzE,GAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,4CAAA,EAA8C,eAAK,WAAA,EAAY,CAAA;AAAA,oBAC5E,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,2CAAA,EACb,QAAA,EAAA;AAAA,sBAAA,IAAA,CAAC,GAAA,EAAA,EAAE,WAAU,2BAAA,EAA4B,QAAA,EAAA;AAAA,QAAA,cAAA;AAAA,wBAC3B,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,2BAAA,EAA4B,QAAA,EAAA,gBAAA,EAAc,CAAA;AAAA,QAAO;AAAA,OAAA,EAC/E,CAAA;AAAA,MACC,OAAA,oBAAW,IAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,4CAAA,EAA6C,QAAA,EAAA;AAAA,QAAA,SAAA;AAAA,QAAQ;AAAA,OAAA,EAAQ;AAAA,KAAA,EACxF;AAAA,GAAA,EACF,CAAA,EACF,CAAA;AAEJ","file":"chunk-TO6ZZHJK.js","sourcesContent":["/**\n * Shared \"About\" dialog for the bundled apps.\n *\n * Each bundled app carries its own version (independent of the package\n * version) so app-level changes can be tracked without bumping every app at\n * once. `<AboutApp app=\"…\" />` registers an \"About <Name>\" item in the\n * window title menu and renders the dialog: app icon, name, app version and\n * the react-os-shell attribution + shell version.\n *\n * Bump an app's version in `BUILTIN_APP_INFO` whenever that app's behaviour\n * changes, alongside the package-level CHANGELOG entry.\n */\nimport { useCallback, useState, isValidElement, cloneElement, type ReactElement } from 'react';\nimport Modal, { useWindowMenuItem } from '../shell/Modal';\nimport { navIcons } from '../shell-config/nav';\nimport { VERSION } from '../version';\n\nexport interface BuiltinAppInfo {\n /** Display name — matches the window-registry label. */\n name: string;\n /** App version, independent of the package version. */\n version: string;\n /** One-line summary shown in the About dialog. */\n description: string;\n /** Registry route — used to look up the consumer-registered nav icon. */\n route: string;\n}\n\nexport const BUILTIN_APP_INFO = {\n spreadsheet: {\n name: 'Spreadsheets',\n version: '1.1.1',\n route: '/spreadsheet',\n description: 'Multi-sheet spreadsheet editor with CSV / TSV import and export.',\n },\n notepad: {\n name: 'Notepad',\n version: '1.0.0',\n route: '/notepad',\n description: 'Color-coded notes with checklists, desktop stickies and entity autolinking.',\n },\n documents: {\n name: 'Documents',\n version: '1.1.0',\n route: '/documents',\n description: 'Letter-page editor for Word documents and plain-text files — images, lists, alignment.',\n },\n preview: {\n name: 'Preview',\n version: '1.2.1',\n route: '/preview',\n description: 'Viewer for PDF documents, images, DXF drawings and 3D models — CAD-style measuring with object snap and an AutoCAD-like command bar.',\n },\n files: {\n name: 'Files',\n version: '1.1.0',\n route: '/files',\n description: 'Personal file manager with folders, uploads, quota, trash and desktop shortcut folders.',\n },\n browser: {\n name: 'Browser',\n version: '1.0.1',\n route: '/browser',\n description: 'Minimal web browser with bookmarks, history and navigation bar.',\n },\n} satisfies Record<string, BuiltinAppInfo>;\n\nexport type BuiltinAppId = keyof typeof BUILTIN_APP_INFO;\n\nconst ABOUT_MENU_ICON = (\n <svg className=\"h-4 w-4 text-gray-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={1.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z\" />\n </svg>\n);\n\n// Generic window glyph shown when the consumer hasn't registered a nav icon\n// for the app's route (same fallback shape as the Modal title bar).\nconst FALLBACK_APP_ICON = (\n <svg className=\"h-9 w-9\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={1.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M3.75 6.75A2.25 2.25 0 016 4.5h12a2.25 2.25 0 012.25 2.25v10.5A2.25 2.25 0 0118 19.5H6a2.25 2.25 0 01-2.25-2.25V6.75z M3.75 9h16.5\" />\n </svg>\n);\n\n/** Registers \"About <Name>\" in the window title menu and renders the dialog.\n * Render anywhere inside the app's window. */\nexport default function AboutApp({ app }: { app: BuiltinAppId }) {\n const info = BUILTIN_APP_INFO[app];\n const [open, setOpen] = useState(false);\n const openDialog = useCallback(() => setOpen(true), []);\n useWindowMenuItem(`About ${info.name}`, openDialog, ABOUT_MENU_ICON);\n\n if (!open) return null;\n\n const registered = navIcons[info.route];\n const appIcon = isValidElement(registered)\n ? cloneElement(registered as ReactElement, { className: 'h-9 w-9' } as any)\n : FALLBACK_APP_ICON;\n\n return (\n <Modal open onClose={() => setOpen(false)} title={`About ${info.name}`} size=\"sm\" compact bodyScroll={false} autoHeight dimensions={[320, 320]}>\n <div className=\"flex flex-col items-center text-center px-5 pt-5 pb-4\">\n <div className=\"flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-100 text-gray-600 shrink-0\">\n {appIcon}\n </div>\n <h2 className=\"mt-3 text-lg font-bold text-gray-900\">{info.name}</h2>\n <p className=\"text-[11px] font-mono text-gray-400\">Version {info.version}</p>\n <p className=\"mt-2 text-xs text-gray-500 leading-relaxed\">{info.description}</p>\n <div className=\"mt-4 pt-3 border-t border-gray-200 w-full\">\n <p className=\"text-[11px] text-gray-500\">\n Part of the <span className=\"font-medium text-gray-700\">react-os-shell</span> desktop environment\n </p>\n {VERSION && <p className=\"mt-0.5 text-[10px] font-mono text-gray-400\">shell v{VERSION}</p>}\n </div>\n </div>\n </Modal>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/apps/_about.tsx"],"names":[],"mappings":"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","file":"chunk-EG3WEQBH.js","sourcesContent":["/**\n * Shared \"About\" dialog for the bundled apps.\n *\n * Each bundled app carries its own version (independent of the package\n * version) so app-level changes can be tracked without bumping every app at\n * once. `<AboutApp app=\"…\" />` registers an \"About <Name>\" item in the\n * window title menu and renders the dialog: app icon, name, app version and\n * the react-os-shell attribution + shell version.\n *\n * Bump an app's version in `BUILTIN_APP_INFO` whenever that app's behaviour\n * changes, alongside the package-level CHANGELOG entry.\n */\nimport { useCallback, useState, isValidElement, cloneElement, type ReactElement } from 'react';\nimport Modal, { useWindowMenuItem } from '../shell/Modal';\nimport { navIcons } from '../shell-config/nav';\nimport { VERSION } from '../version';\n\nexport interface BuiltinAppInfo {\n /** Display name — matches the window-registry label. */\n name: string;\n /** App version, independent of the package version. */\n version: string;\n /** One-line summary shown in the About dialog. */\n description: string;\n /** Registry route — used to look up the consumer-registered nav icon. */\n route: string;\n}\n\nexport const BUILTIN_APP_INFO = {\n spreadsheet: {\n name: 'Spreadsheets',\n version: '1.1.1',\n route: '/spreadsheet',\n description: 'Multi-sheet spreadsheet editor with CSV / TSV import and export.',\n },\n notepad: {\n name: 'Notepad',\n version: '1.0.0',\n route: '/notepad',\n description: 'Color-coded notes with checklists, desktop stickies and entity autolinking.',\n },\n documents: {\n name: 'Documents',\n version: '1.1.0',\n route: '/documents',\n description: 'Letter-page editor for Word documents and plain-text files — images, lists, alignment.',\n },\n preview: {\n name: 'Preview',\n version: '1.2.1',\n route: '/preview',\n description: 'Viewer for PDF documents, images, DXF drawings and 3D models — CAD-style measuring with object snap and an AutoCAD-like command bar.',\n },\n files: {\n name: 'Files',\n version: '1.1.0',\n route: '/files',\n description: 'Personal file manager with folders, uploads, quota, trash and desktop shortcut folders.',\n },\n browser: {\n name: 'Browser',\n version: '1.0.1',\n route: '/browser',\n description: 'Minimal web browser with bookmarks, history and navigation bar.',\n },\n} satisfies Record<string, BuiltinAppInfo>;\n\nexport type BuiltinAppId = keyof typeof BUILTIN_APP_INFO;\n\nconst ABOUT_MENU_ICON = (\n <svg className=\"h-4 w-4 text-gray-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={1.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z\" />\n </svg>\n);\n\n// Generic window glyph shown when the consumer hasn't registered a nav icon\n// for the app's route (same fallback shape as the Modal title bar).\nconst FALLBACK_APP_ICON = (\n <svg className=\"h-9 w-9\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={1.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M3.75 6.75A2.25 2.25 0 016 4.5h12a2.25 2.25 0 012.25 2.25v10.5A2.25 2.25 0 0118 19.5H6a2.25 2.25 0 01-2.25-2.25V6.75z M3.75 9h16.5\" />\n </svg>\n);\n\n/** Registers \"About <Name>\" in the window title menu and renders the dialog.\n * Render anywhere inside the app's window. */\nexport default function AboutApp({ app }: { app: BuiltinAppId }) {\n const info = BUILTIN_APP_INFO[app];\n const [open, setOpen] = useState(false);\n const openDialog = useCallback(() => setOpen(true), []);\n useWindowMenuItem(`About ${info.name}`, openDialog, ABOUT_MENU_ICON);\n\n if (!open) return null;\n\n const registered = navIcons[info.route];\n const appIcon = isValidElement(registered)\n ? cloneElement(registered as ReactElement, { className: 'h-9 w-9' } as any)\n : FALLBACK_APP_ICON;\n\n return (\n <Modal open onClose={() => setOpen(false)} title={`About ${info.name}`} size=\"sm\" compact bodyScroll={false} autoHeight dimensions={[320, 320]}>\n <div className=\"flex flex-col items-center text-center px-5 pt-5 pb-4\">\n <div className=\"flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-100 text-gray-600 shrink-0\">\n {appIcon}\n </div>\n <h2 className=\"mt-3 text-lg font-bold text-gray-900\">{info.name}</h2>\n <p className=\"text-[11px] font-mono text-gray-400\">Version {info.version}</p>\n <p className=\"mt-2 text-xs text-gray-500 leading-relaxed\">{info.description}</p>\n <div className=\"mt-4 pt-3 border-t border-gray-200 w-full\">\n <p className=\"text-[11px] text-gray-500\">\n Part of the <span className=\"font-medium text-gray-700\">react-os-shell</span> desktop environment\n </p>\n {VERSION && <p className=\"mt-0.5 text-[10px] font-mono text-gray-400\">shell v{VERSION}</p>}\n </div>\n </div>\n </Modal>\n );\n}\n"]}