canopycms 0.2.0-int.121 → 0.3.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 (79) hide show
  1. package/CHANGELOG.md +241 -78
  2. package/dist/api/branch.d.ts +2 -2
  3. package/dist/cli/cli.js +0 -18
  4. package/dist/cli/generate-ai-content.js +0 -6
  5. package/dist/cli/init.js +0 -6
  6. package/dist/dev-content-watcher.js +8 -1
  7. package/dist/editor/BranchManager.d.ts +2 -0
  8. package/dist/editor/BranchManager.js +3 -3
  9. package/dist/editor/Editor.js +276 -130
  10. package/dist/editor/EntryNavigator.d.ts +44 -9
  11. package/dist/editor/EntryNavigator.js +278 -104
  12. package/dist/editor/branch-lock.d.ts +42 -0
  13. package/dist/editor/branch-lock.js +79 -0
  14. package/dist/editor/branch-status.d.ts +0 -6
  15. package/dist/editor/branch-status.js +0 -10
  16. package/dist/editor/components/BranchNotAvailableNotice.d.ts +11 -0
  17. package/dist/editor/components/BranchNotAvailableNotice.js +10 -0
  18. package/dist/editor/components/BranchPill.d.ts +3 -18
  19. package/dist/editor/components/BranchPill.js +7 -10
  20. package/dist/editor/components/BranchRowLock.d.ts +3 -1
  21. package/dist/editor/components/BranchRowLock.js +4 -2
  22. package/dist/editor/components/EditorHeader.d.ts +24 -52
  23. package/dist/editor/components/EditorHeader.js +151 -71
  24. package/dist/editor/components/EditorRail.d.ts +8 -8
  25. package/dist/editor/components/EditorRail.js +7 -5
  26. package/dist/editor/components/EntryBreadcrumb.d.ts +5 -1
  27. package/dist/editor/components/EntryBreadcrumb.js +3 -3
  28. package/dist/editor/components/EntryHeader.d.ts +60 -0
  29. package/dist/editor/components/EntryHeader.js +45 -0
  30. package/dist/editor/components/EntrySaveButton.d.ts +24 -0
  31. package/dist/editor/components/EntrySaveButton.js +101 -0
  32. package/dist/editor/components/NoEditPermissionNotice.d.ts +1 -1
  33. package/dist/editor/components/NoEditPermissionNotice.js +1 -1
  34. package/dist/editor/components/RetryToastMessage.d.ts +3 -1
  35. package/dist/editor/components/RetryToastMessage.js +8 -4
  36. package/dist/editor/components/SaveAllButton.d.ts +25 -0
  37. package/dist/editor/components/SaveAllButton.js +81 -0
  38. package/dist/editor/components/UnsavedMark.d.ts +1 -1
  39. package/dist/editor/components/UnsavedMark.js +1 -1
  40. package/dist/editor/components/index.d.ts +1 -1
  41. package/dist/editor/components/index.js +1 -1
  42. package/dist/editor/copy.d.ts +0 -1
  43. package/dist/editor/copy.js +0 -1
  44. package/dist/editor/editor-utils.d.ts +2 -2
  45. package/dist/editor/editor-utils.js +2 -2
  46. package/dist/editor/entry-search.d.ts +10 -16
  47. package/dist/editor/entry-search.js +4 -24
  48. package/dist/editor/hooks/editor-history.d.ts +7 -0
  49. package/dist/editor/hooks/editor-history.js +80 -0
  50. package/dist/editor/hooks/useBranchActions.js +3 -1
  51. package/dist/editor/hooks/useBranchManager.d.ts +7 -3
  52. package/dist/editor/hooks/useBranchManager.js +18 -11
  53. package/dist/editor/hooks/useEntryManager.d.ts +12 -2
  54. package/dist/editor/hooks/useEntryManager.js +141 -20
  55. package/dist/editor/shell/ActivityRail.d.ts +4 -0
  56. package/dist/editor/shell/ActivityRail.js +13 -4
  57. package/dist/editor/shell/EditorShell.d.ts +6 -7
  58. package/dist/editor/shell/EditorShell.js +9 -13
  59. package/dist/editor/shell/PaneLayout.d.ts +1 -0
  60. package/dist/editor/shell/PaneLayout.js +23 -5
  61. package/dist/editor/shell/SidePanelHost.d.ts +11 -4
  62. package/dist/editor/shell/SidePanelHost.js +95 -7
  63. package/dist/editor/shell/TopBar.d.ts +4 -4
  64. package/dist/editor/shell/TopBar.js +6 -3
  65. package/dist/editor/theme.js +22 -1
  66. package/dist/editor/utils/focus-return.d.ts +10 -2
  67. package/dist/editor/utils/focus-return.js +40 -6
  68. package/dist/operating-mode/client-safe-strategy.d.ts +1 -6
  69. package/dist/operating-mode/client-safe-strategy.js +1 -10
  70. package/dist/operating-mode/types.d.ts +0 -3
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/package.json +1 -1
  74. package/dist/editor/components/BranchLockBanner.d.ts +0 -23
  75. package/dist/editor/components/BranchLockBanner.js +0 -66
  76. package/dist/editor/components/QuickOpenPalette.d.ts +0 -28
  77. package/dist/editor/components/QuickOpenPalette.js +0 -71
  78. package/dist/editor/components/SaveSplitButton.d.ts +0 -31
  79. package/dist/editor/components/SaveSplitButton.js +0 -121
@@ -0,0 +1,42 @@
1
+ import type { BranchStatus } from '../types.js';
2
+ export interface BranchLockInput {
3
+ branchStatus: BranchStatus | undefined;
4
+ /** The protected base branch, read-only in prod. */
5
+ branchReadOnly?: boolean;
6
+ branchWriteBlocked?: boolean;
7
+ /** The branch list is still being fetched, so details aren't unavailable yet, only pending. */
8
+ branchPending?: boolean;
9
+ /** The branch list answered without this branch: no access to it, or it no longer exists. */
10
+ branchMissing?: boolean;
11
+ }
12
+ /**
13
+ * Which lock applies. `branchWriteBlocked` is the server's own answer (getBranchProtection), so
14
+ * nothing offers a write the API would reject; `branchReadOnly` only picks the message, and
15
+ * base-branch read-only takes precedence over a workflow status lock.
16
+ * @internal Exported for tests; the bar reads it through `branchBarStatus`.
17
+ */
18
+ export declare function branchLockState({ branchStatus, branchReadOnly, branchWriteBlocked, }: BranchLockInput): {
19
+ readOnly: boolean;
20
+ statusLocked: boolean;
21
+ branchDataUnavailable: boolean;
22
+ };
23
+ /** Why the branch takes no writes, and the way forward, as Save, row locks and the status say. */
24
+ export declare function describeBranchLock(lock: BranchLockInput): string | undefined;
25
+ export interface BranchBarStatusInput extends BranchLockInput {
26
+ /** The listed branch's `isProtected`, with no fallback here: no listed branch names no protection. */
27
+ isProtected: boolean | undefined;
28
+ }
29
+ export interface BranchBarStatus {
30
+ /** The status element's text; undefined for an editable branch, which shows none. */
31
+ label?: string;
32
+ value: string;
33
+ readOnly: boolean;
34
+ /** The way forward; `workflow` leaves it to the Withdraw control, never offered twice. */
35
+ action?: 'newBranch' | 'switchBranch' | 'refreshBranch' | 'workflow';
36
+ }
37
+ /**
38
+ * The branch bar's status and way forward, one row of the lock-state table, checked in order.
39
+ * Pending comes first because a fetch in flight is also `branchDataUnavailable`, and `readOnly`
40
+ * before any status lock, as in `describeBranchLock`.
41
+ */
42
+ export declare function branchBarStatus(input: BranchBarStatusInput): BranchBarStatus;
@@ -0,0 +1,79 @@
1
+ import { branchStatusPresentation } from './branch-status.js';
2
+ /**
3
+ * Which lock applies. `branchWriteBlocked` is the server's own answer (getBranchProtection), so
4
+ * nothing offers a write the API would reject; `branchReadOnly` only picks the message, and
5
+ * base-branch read-only takes precedence over a workflow status lock.
6
+ * @internal Exported for tests; the bar reads it through `branchBarStatus`.
7
+ */
8
+ export function branchLockState({ branchStatus, branchReadOnly = false, branchWriteBlocked = false, }) {
9
+ const statusLocked = branchWriteBlocked && !branchReadOnly;
10
+ // `branchStatus` can't describe the lock while branch data loads (undefined) or under version
11
+ // skew (an old server omits `branchWriteBlocked` but reports 'editing'), so both read as
12
+ // unavailable data rather than naming a status the lock contradicts.
13
+ const branchDataUnavailable = statusLocked && (branchStatus === undefined || branchStatus === 'editing');
14
+ return { readOnly: branchReadOnly, statusLocked, branchDataUnavailable };
15
+ }
16
+ /** Why the branch takes no writes, and the way forward, as Save, row locks and the status say. */
17
+ export function describeBranchLock(lock) {
18
+ const { readOnly, statusLocked, branchDataUnavailable } = branchLockState(lock);
19
+ if (readOnly)
20
+ return 'The base branch is read-only. Create a branch to make changes.';
21
+ if (branchDataUnavailable)
22
+ return lock.branchPending
23
+ ? 'Wait for the branch details to load.'
24
+ : lock.branchMissing
25
+ ? `You don't have access to this branch, or it no longer exists. Use "Switch branch…" to open another.`
26
+ : 'Branch details could not be loaded. Use "Refresh branch" to try again.';
27
+ if (!statusLocked)
28
+ return undefined;
29
+ if (lock.branchStatus === 'submitted')
30
+ return 'This branch is in review. Withdraw it to make changes.';
31
+ if (lock.branchStatus === 'approved')
32
+ return 'This branch is approved. Withdraw it to make changes.';
33
+ return `This branch is ${lock.branchStatus}. Its content is read-only.`;
34
+ }
35
+ const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
36
+ /**
37
+ * The branch bar's status and way forward, one row of the lock-state table, checked in order.
38
+ * Pending comes first because a fetch in flight is also `branchDataUnavailable`, and `readOnly`
39
+ * before any status lock, as in `describeBranchLock`.
40
+ */
41
+ export function branchBarStatus(input) {
42
+ const { readOnly, statusLocked, branchDataUnavailable } = branchLockState(input);
43
+ const status = input.branchStatus;
44
+ if (input.branchPending)
45
+ return { label: 'Loading branch…', value: 'loading', readOnly: false };
46
+ if (branchDataUnavailable)
47
+ return input.branchMissing
48
+ ? {
49
+ label: 'Not available · read-only',
50
+ value: 'not-available',
51
+ readOnly: true,
52
+ action: 'switchBranch',
53
+ }
54
+ : {
55
+ label: 'Details unavailable · read-only',
56
+ value: 'unavailable',
57
+ readOnly: true,
58
+ action: 'refreshBranch',
59
+ };
60
+ if (readOnly)
61
+ return {
62
+ label: 'Protected · read-only',
63
+ value: 'protected',
64
+ readOnly: true,
65
+ action: 'newBranch',
66
+ };
67
+ if (!statusLocked)
68
+ return input.isProtected === true
69
+ ? { label: 'Protected', value: 'protected-editable', readOnly: false }
70
+ : { value: 'editable', readOnly: false };
71
+ // A lock with no status is `branchDataUnavailable`, handled above.
72
+ const label = capitalize(branchStatusPresentation(status)?.label ?? String(status));
73
+ return {
74
+ label: `${label} · read-only`,
75
+ value: String(status),
76
+ readOnly: true,
77
+ action: status === 'submitted' || status === 'approved' ? 'workflow' : 'switchBranch',
78
+ };
79
+ }
@@ -15,9 +15,3 @@ export declare const BRANCH_STATUS_PRESENTATION: Record<BranchStatus, BranchStat
15
15
  * Returns undefined when there is nothing to show.
16
16
  */
17
17
  export declare function branchStatusPresentation(status: string | undefined, isProtected?: boolean): BranchStatusPresentation | undefined;
18
- /**
19
- * The status the top bar shows: only one that tells the user something, so not 'editing', the
20
- * state every branch starts in. The Branches drawer shows every status through
21
- * `branchStatusPresentation`.
22
- */
23
- export declare function branchBarStatusPresentation(status: string | undefined, isProtected?: boolean): BranchStatusPresentation | undefined;
@@ -27,13 +27,3 @@ export function branchStatusPresentation(status, isProtected = false) {
27
27
  return BRANCH_STATUS_PRESENTATION[status];
28
28
  return { label: status, color: 'gray', variant: 'light' };
29
29
  }
30
- /**
31
- * The status the top bar shows: only one that tells the user something, so not 'editing', the
32
- * state every branch starts in. The Branches drawer shows every status through
33
- * `branchStatusPresentation`.
34
- */
35
- export function branchBarStatusPresentation(status, isProtected = false) {
36
- if (!isProtected && status === 'editing')
37
- return undefined;
38
- return branchStatusPresentation(status, isProtected);
39
- }
@@ -0,0 +1,11 @@
1
+ export interface BranchNotAvailableNoticeProps {
2
+ /** Opens the Branches drawer. Without it the notice is text alone. */
3
+ onSwitchBranch?: () => void;
4
+ align?: 'flex-start' | 'center';
5
+ 'data-testid'?: string;
6
+ }
7
+ /**
8
+ * The empty state of a branch on the bar's "Not available" row (`branchBarStatus`), where the
9
+ * branch list answered without it, so nothing of the branch can show.
10
+ */
11
+ export declare function BranchNotAvailableNotice({ onSwitchBranch, align, 'data-testid': testId, }: BranchNotAvailableNoticeProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Stack, Text } from '@mantine/core';
3
+ import { EDITOR_ACTIONS } from '../copy.js';
4
+ /**
5
+ * The empty state of a branch on the bar's "Not available" row (`branchBarStatus`), where the
6
+ * branch list answered without it, so nothing of the branch can show.
7
+ */
8
+ export function BranchNotAvailableNotice({ onSwitchBranch, align = 'flex-start', 'data-testid': testId, }) {
9
+ return (_jsxs(Stack, { gap: 4, py: "sm", align: align, "data-testid": testId, children: [_jsx(Text, { size: "sm", c: "dimmed", children: "You don't have access to this branch, or it no longer exists." }), onSwitchBranch && (_jsx(Button, { size: "compact-sm", variant: "subtle", onClick: onSwitchBranch, children: `${EDITOR_ACTIONS.switchBranch}…` }))] }));
10
+ }
@@ -1,33 +1,18 @@
1
1
  import { type Ref } from 'react';
2
- import type { LogicalPath } from '../../paths/types.js';
3
2
  export interface BranchPillProps {
4
3
  branchName: string;
5
4
  /** The editor is busy; with no branch name yet, that is the branch list loading. */
6
5
  loading: boolean;
7
- /** The status label ("In review", "Protected"), set only for a status worth a place in the bar. */
8
- status?: {
9
- label: string;
10
- testId: string;
11
- value: string;
12
- };
13
6
  /** How many entries on the branch have unsaved changes, counting any the list can't name yet. */
14
7
  unsavedCount: number;
15
- /** The listed entries with unsaved changes. */
16
- unsavedEntries: Array<{
17
- path: LogicalPath;
18
- label: string;
19
- }>;
20
8
  onSwitchBranch: () => void;
21
9
  onRefreshBranch: () => void;
22
10
  onDiscardAll: () => void;
23
- onEntrySelect: (path: LogicalPath) => void;
24
11
  /** The pill, which stands in for a menu item that opened something. */
25
12
  pillRef?: Ref<HTMLButtonElement>;
26
13
  }
27
14
  /**
28
- * The branch's name, status and unsaved count: the unsaved mark and a number. Its menu holds the
29
- * branch actions and lists the unsaved entries, each opening that entry. Below `sm` it is the
30
- * branch icon and the count, its name then in its Tooltip, accessible name and menu label: the
31
- * row's actions must not clip.
15
+ * The branch's name. Its menu, headed by the full name, holds the branch actions. Below `sm` the
16
+ * name truncates shorter, so the actions don't clip.
32
17
  */
33
- export declare function BranchPill({ branchName, loading, status, unsavedCount: unsaved, unsavedEntries, onSwitchBranch, onRefreshBranch, onDiscardAll, onEntrySelect, pillRef, }: BranchPillProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function BranchPill({ branchName, loading, unsavedCount: unsaved, onSwitchBranch, onRefreshBranch, onDiscardAll, pillRef, }: BranchPillProps): import("react/jsx-runtime").JSX.Element;
@@ -1,31 +1,28 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef } from 'react';
4
- import { Box, Button, Menu, Text, Tooltip, VisuallyHidden } from '@mantine/core';
4
+ import { Button, Menu, Text, Tooltip } from '@mantine/core';
5
5
  import { useMediaQuery, useMergedRef } from '@mantine/hooks';
6
6
  import { IconChevronDown, IconGitBranch } from '@tabler/icons-react';
7
7
  import { EDITOR_ACTIONS } from '../copy.js';
8
8
  import { DestructiveMenuItem } from './DestructiveMenuItem.js';
9
9
  import { ReasonedMenuItem } from './ReasonedMenuItem.js';
10
- import { UnsavedMark } from './UnsavedMark.js';
11
10
  import { useMenuItemFocusReturn } from '../utils/focus-return.js';
12
11
  const plural = (count, one, many) => `${count} ${count === 1 ? one : many}`;
13
12
  /**
14
- * The branch's name, status and unsaved count: the unsaved mark and a number. Its menu holds the
15
- * branch actions and lists the unsaved entries, each opening that entry. Below `sm` it is the
16
- * branch icon and the count, its name then in its Tooltip, accessible name and menu label: the
17
- * row's actions must not clip.
13
+ * The branch's name. Its menu, headed by the full name, holds the branch actions. Below `sm` the
14
+ * name truncates shorter, so the actions don't clip.
18
15
  */
19
- export function BranchPill({ branchName, loading, status, unsavedCount: unsaved, unsavedEntries, onSwitchBranch, onRefreshBranch, onDiscardAll, onEntrySelect, pillRef, }) {
16
+ export function BranchPill({ branchName, loading, unsavedCount: unsaved, onSwitchBranch, onRefreshBranch, onDiscardAll, pillRef, }) {
20
17
  const trigger = useRef(null);
21
18
  const triggerRef = useMergedRef(trigger, pillRef);
22
19
  const menuFocus = useMenuItemFocusReturn(trigger);
23
20
  const noBranch = branchName ? undefined : 'Open a branch first.';
24
21
  const wide = useMediaQuery('(min-width: 48em)');
25
22
  const name = branchName || (loading ? 'Loading branches…' : 'No branch open');
26
- return (_jsxs(Menu, { withinPortal: true, shadow: "sm", position: "bottom-end", ...menuFocus.menu, children: [_jsx(Tooltip, { label: status ? `${name} · ${status.label}` : name, disabled: wide, events: { hover: true, focus: true, touch: false }, children: _jsx(Menu.Target, { children: _jsxs(Button, { ref: triggerRef, variant: "default", size: "sm", miw: 0, px: { base: 8, sm: 14 },
27
- // The count is the bar's one branch-wide unsaved signal: the row wraps before it clips.
28
- style: { flexShrink: 0 }, leftSection: _jsx(IconGitBranch, { size: 16, "aria-hidden": "true" }), rightSection: wide ? _jsx(IconChevronDown, { size: 14, "aria-hidden": "true" }) : undefined, loading: !branchName && loading, "data-testid": "branch-dropdown-button", styles: { label: { minWidth: 0, gap: 6 } }, children: [_jsx(Text, { span: true, inherit: true, truncate: "end", maw: 200, visibleFrom: "sm", children: name }), _jsx(VisuallyHidden, { component: "span", hiddenFrom: "sm", children: name }), branchName && status && (_jsxs(Text, { span: true, size: "xs", c: "dimmed", visibleFrom: "md", style: { flexShrink: 0 }, children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7 " }), _jsx("span", { "data-testid": status.testId, "data-status": status.value, children: status.label })] })), unsaved > 0 && (_jsxs(Box, { component: "span", display: "inline-flex", style: { alignItems: 'center', gap: 4, flexShrink: 0 }, "data-testid": "branch-unsaved-count", children: [_jsx(UnsavedMark, { "aria-hidden": "true" }), _jsx(Text, { span: true, size: "xs", fw: 600, "aria-hidden": "true", children: unsaved }), _jsx(VisuallyHidden, { component: "span", children: `, ${plural(unsaved, 'entry', 'entries')} with unsaved changes` })] }))] }) }) }), _jsxs(Menu.Dropdown, { "data-testid": "branch-menu", ...menuFocus.dropdown, children: [branchName && status && (_jsx(Menu.Label, { hiddenFrom: "md", children: `${branchName} · ${status.label}` })), _jsx(Menu.Item, { onClick: onSwitchBranch, "data-testid": "manage-branches-menu-item", children: `${EDITOR_ACTIONS.switchBranch}…` }), _jsx(ReasonedMenuItem, { onClick: onRefreshBranch, blockedReason: noBranch, "data-testid": "refresh-branch-menu-item", children: "Refresh branch" }), _jsx(Menu.Divider, {}), _jsx(Menu.Label, { children: "Unsaved changes" }), unsavedEntries.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", px: "sm", py: 6, "data-testid": "edited-entries-empty", children: unsaved === 0 ? 'No unsaved changes on this branch.' : 'Loading their names…' })) : (unsavedEntries.map((entry) => (_jsx(Menu.Item, { onClick: () => onEntrySelect(entry.path), children: entry.label }, entry.path)))), _jsx(Menu.Divider, {}), _jsx(DestructiveMenuItem, { "data-testid": "discard-all-drafts-menu-item", onClick: onDiscardAll, blockedReason: noBranch ?? (unsaved === 0 ? 'Nothing to discard.' : undefined), children: unsaved > 0
23
+ return (_jsxs(Menu, { withinPortal: true, shadow: "sm", position: "bottom-start", ...menuFocus.menu, children: [_jsx(Tooltip, { label: name, disabled: wide, events: { hover: true, focus: true, touch: true }, children: _jsx(Menu.Target, { children: _jsx(Button, { ref: triggerRef, variant: "default", size: "sm", miw: 0, px: { base: 8, sm: 14 },
24
+ // The bar's row wraps before the pill's name clips.
25
+ style: { flexShrink: 0 }, leftSection: _jsx(IconGitBranch, { size: 16, "aria-hidden": "true" }), rightSection: wide ? _jsx(IconChevronDown, { size: 14, "aria-hidden": "true" }) : undefined, loading: !branchName && loading, "data-testid": "branch-dropdown-button", styles: { label: { minWidth: 0, gap: 6 } }, children: _jsx(Text, { span: true, inherit: true, truncate: "end", maw: { base: 120, sm: 200 }, children: name }) }) }) }), _jsxs(Menu.Dropdown, { "data-testid": "branch-menu", ...menuFocus.dropdown, children: [branchName && _jsx(Menu.Label, { children: branchName }), _jsx(Menu.Item, { onClick: onSwitchBranch, "data-testid": "manage-branches-menu-item", children: `${EDITOR_ACTIONS.switchBranch}…` }), _jsx(ReasonedMenuItem, { onClick: onRefreshBranch, blockedReason: noBranch, "data-testid": "refresh-branch-menu-item", children: "Refresh branch" }), _jsx(Menu.Divider, {}), _jsx(DestructiveMenuItem, { "data-testid": "discard-all-drafts-menu-item", onClick: onDiscardAll, blockedReason: noBranch ?? (unsaved === 0 ? 'Nothing to discard.' : undefined), children: unsaved > 0
29
26
  ? `Discard all changes (${plural(unsaved, 'entry', 'entries')})`
30
27
  : 'Discard all changes' })] })] }));
31
28
  }
@@ -4,7 +4,9 @@ import React from 'react';
4
4
  * writes. It matches the menu button's footprint so rows do not shift between branches, and takes
5
5
  * the menu's name; the reason is its Tooltip and its description. It stays `aria-disabled` rather
6
6
  * than `disabled`, so the reason opens on keyboard focus and touch too. Space is stopped here: the
7
- * tree would read it as expand/collapse.
7
+ * tree would read it as expand/collapse. `data-row-lock` marks it for the theme stylesheet, which
8
+ * draws it only on the hovered, focused or selected row (opacity, never `display`, so
9
+ * focusing it reveals it); a touch user sees it on the row they tapped.
8
10
  */
9
11
  export declare const BranchRowLock: React.FC<{
10
12
  label: string;
@@ -8,11 +8,13 @@ import { IconLock } from '@tabler/icons-react';
8
8
  * writes. It matches the menu button's footprint so rows do not shift between branches, and takes
9
9
  * the menu's name; the reason is its Tooltip and its description. It stays `aria-disabled` rather
10
10
  * than `disabled`, so the reason opens on keyboard focus and touch too. Space is stopped here: the
11
- * tree would read it as expand/collapse.
11
+ * tree would read it as expand/collapse. `data-row-lock` marks it for the theme stylesheet, which
12
+ * draws it only on the hovered, focused or selected row (opacity, never `display`, so
13
+ * focusing it reveals it); a touch user sees it on the row they tapped.
12
14
  */
13
15
  export const BranchRowLock = ({ label, reason }) => {
14
16
  const reasonId = useId();
15
- return (_jsx(Tooltip, { label: reason, withinPortal: true, position: "bottom-end", multiline: true, w: 240, events: { hover: true, focus: true, touch: true }, children: _jsxs(ActionIcon, { size: "xs", variant: "transparent", c: "dimmed", "aria-disabled": true, "aria-label": label, "aria-describedby": reasonId, "data-testid": "branch-lock", style: { cursor: 'default' }, onClick: (event) => event.stopPropagation(), onKeyDown: (event) => {
17
+ return (_jsx(Tooltip, { label: reason, withinPortal: true, position: "bottom-end", multiline: true, w: 240, events: { hover: true, focus: true, touch: true }, children: _jsxs(ActionIcon, { size: "xs", variant: "transparent", c: "dimmed", "aria-disabled": true, "aria-label": label, "aria-describedby": reasonId, "data-testid": "branch-lock", "data-row-lock": true, style: { cursor: 'default' }, onClick: (event) => event.stopPropagation(), onKeyDown: (event) => {
16
18
  if (event.nativeEvent.code === 'Space')
17
19
  event.stopPropagation();
18
20
  }, children: [_jsx(IconLock, { size: 14, "aria-hidden": true }), _jsx(VisuallyHidden, { component: "span", id: reasonId, children: reason })] }) }));
@@ -1,66 +1,34 @@
1
1
  import { type Ref } from 'react';
2
- import type { OperatingMode } from '../../operating-mode/index.js';
3
2
  import type { BranchStatus } from '../../types.js';
4
- import type { EditorEntry } from '../Editor.js';
5
- import type { LogicalPath } from '../../paths/types.js';
6
- import type { BreadcrumbSegment } from '../editor-utils.js';
7
3
  import type { SaveAllProgress } from '../hooks/save-all.js';
8
4
  export interface EditorHeaderProps {
9
- currentEntry: EditorEntry | undefined;
10
5
  branchName: string;
11
- operatingMode: OperatingMode;
12
6
  /** Whether operations are currently in progress. */
13
7
  busy: boolean;
14
- /** The crumbs before the open entry's own. */
15
- breadcrumbSegments: BreadcrumbSegment[];
16
- /** The listed entries with unsaved changes on the branch. */
17
- editedFiles: Array<{
18
- path: LogicalPath;
19
- label: string;
20
- }>;
21
8
  /** Entries with unsaved changes on the branch. */
22
9
  modifiedCount: number;
23
- /** Opens the content panel on a collection. */
24
- onCollectionOpen: (collectionPath: string) => void;
25
- /** The entry menu's trigger. */
26
- navigatorToggleRef?: Ref<HTMLButtonElement>;
10
+ /** Lists the unsaved entries: the content panel, filtered to Changed. */
11
+ onShowUnsaved: () => void;
27
12
  /** The branch pill. */
28
13
  branchToggleRef?: Ref<HTMLButtonElement>;
29
- /** Replaces the open entry with the server's copy. */
30
- onFileReload: () => void;
31
- onFileDiscardDraft: () => void;
32
- /** Opens the open entry's "Change URL" dialog. */
33
- onEntryRename: () => void;
34
- /** Asks to delete the open entry. */
35
- onEntryDelete: () => void;
36
- onEntrySelect: (path: LogicalPath) => void;
37
14
  /** Re-reads the branch and its entry list, keeping drafts. */
38
15
  onBranchReloadData: () => void;
39
16
  onBranchDiscardDrafts: () => void;
40
17
  onBranchManagerOpen: () => void;
41
- onSave: () => void;
18
+ /** Opens Branches with its new-branch form; Branches alone without it. */
19
+ onNewBranch?: () => void;
42
20
  onSaveAll: () => void;
43
21
  saveAllProgress: SaveAllProgress | null;
44
- /** A Save of the open entry is in flight. */
45
- saving?: boolean;
46
- /** Rises each time a single Save lands, whichever entry it saved. */
47
- savedSignal?: number;
48
- /** The last Save that landed saved the open entry. */
49
- savedHere?: boolean;
22
+ /** A single Save is in flight, on any entry. */
23
+ singleSaving?: boolean;
50
24
  /** The branch's entry list failed to load and none is held. */
51
25
  entryListFailed?: boolean;
52
- onSubmit: () => void;
53
- onWithdraw: () => void;
54
- /** The open entry has unsaved changes it could save. */
55
- hasUnsavedChanges: boolean;
56
- /** The open entry has a draft, savable or not (a read-only branch's still discards). */
57
- entryHasDraft?: boolean;
26
+ /** Opens the submit dialog, which returns focus to `returnFocusTo()` when given. */
27
+ onSubmit: (returnFocusTo?: () => HTMLElement | null) => void;
28
+ /** Opens the withdraw dialog, which returns focus to `returnFocusTo()` when given. */
29
+ onWithdraw: (returnFocusTo?: () => HTMLElement | null) => void;
58
30
  /** Writing drafts to localStorage failed, so they live only in this tab. */
59
31
  draftStorageFailed?: boolean;
60
- /** The open entry can't be edited by this user (a path rule). */
61
- entryReadOnly?: boolean;
62
- /** The open entry's type has no schema in the running code. */
63
- entryUnavailable?: boolean;
64
32
  /** Current branch status (undefined if unknown). */
65
33
  branchStatus: BranchStatus | undefined;
66
34
  /** Current user context for permission checks. */
@@ -76,22 +44,26 @@ export interface EditorHeaderProps {
76
44
  };
77
45
  /**
78
46
  * Whether the current branch is the protected base branch (see
79
- * authorization/protected-branch.ts). Hides the Submit button (Withdraw
80
- * stays, as the recovery path). Default false.
47
+ * authorization/protected-branch.ts). Hides Submit, as a button or in the caret
48
+ * (Withdraw stays, as the recovery path). Default false.
81
49
  */
82
50
  branchIsProtected?: boolean;
83
- /** Whether the current branch is read-only (protected base branch in prod); disables Save. */
51
+ /** `currentBranch.isProtected` for the status, unlike `branchIsProtected` not failed closed here. */
52
+ branchServerProtected?: boolean;
53
+ /** Whether the current branch is read-only (protected base branch in prod); disables Save all. */
84
54
  branchReadOnly?: boolean;
85
55
  /** Server-computed: content writes are rejected on this branch; see `branchLockState`. */
86
56
  branchWriteBlocked?: boolean;
87
- /**
88
- * No branch details yet (loading, or the list failed and the banner says so): the fail-closed
89
- * lock defaults are guesses, so neither the chip nor the status names them.
90
- */
57
+ /** No branch details yet: the fail-closed lock defaults are guesses the status doesn't name. */
91
58
  branchLoading?: boolean;
59
+ /** The branch list is still being fetched, as opposed to having failed or lacking this branch. */
60
+ branchPending?: boolean;
61
+ /** The branch list answered without this branch. */
62
+ branchMissing?: boolean;
92
63
  }
93
64
  /**
94
- * The top bar's contents: the breadcrumb with the entry menu and "Read-only" chip, then the branch
95
- * pill, Submit or Withdraw, and Save with Save all.
65
+ * The branch bar: the pill, the branch's status and its unsaved count, then the draft-storage
66
+ * warning, Save all, the way forward and Submit or Withdraw. The open entry's own controls are in
67
+ * `EntryHeader`.
96
68
  */
97
- export declare function EditorHeader({ currentEntry, branchName, operatingMode, busy, breadcrumbSegments, editedFiles, modifiedCount, onCollectionOpen, navigatorToggleRef, branchToggleRef, onFileReload, onFileDiscardDraft, onEntryRename, onEntryDelete, onEntrySelect, onBranchReloadData, onBranchDiscardDrafts, onBranchManagerOpen, onSave, onSaveAll, saveAllProgress, saving, savedSignal, savedHere, entryListFailed, onSubmit, onWithdraw, hasUnsavedChanges, entryHasDraft, draftStorageFailed, entryReadOnly, entryUnavailable, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected, branchReadOnly, branchWriteBlocked, branchLoading, }: EditorHeaderProps): import("react/jsx-runtime").JSX.Element;
69
+ export declare function EditorHeader({ branchName, busy, modifiedCount, onShowUnsaved, branchToggleRef, onBranchReloadData, onBranchDiscardDrafts, onBranchManagerOpen, onNewBranch, onSaveAll, saveAllProgress, singleSaving, entryListFailed, onSubmit, onWithdraw, draftStorageFailed, branchStatus, userContext, branchCreatedBy, branchAccess, branchIsProtected, branchServerProtected, branchReadOnly, branchWriteBlocked, branchLoading, branchPending, branchMissing, }: EditorHeaderProps): import("react/jsx-runtime").JSX.Element;