@puredesktop/puredesktop-ui-bridge 2.1.8 → 2.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +35 -5
- package/src/agents/mcpServerPresets.test.ts +105 -0
- package/src/agents/mcpServerPresets.ts +380 -0
- package/src/agents/runtime/index.ts +7 -0
- package/src/agents/runtime/mapAgentSnapshot.ts +11 -8
- package/src/agents/runtime/pendingAsks.test.ts +117 -0
- package/src/agents/runtime/pendingAsks.ts +110 -0
- package/src/bridge/agentScopeDefaults.ts +65 -0
- package/src/bridge/agentTypes.ts +36 -2
- package/src/bridge/agents.d.mts +6 -0
- package/src/bridge/agents.mjs +10 -0
- package/src/bridge/agents.ts +4 -0
- package/src/bridge/credentials.d.mts +62 -0
- package/src/bridge/credentials.mjs +53 -0
- package/src/bridge/events.d.mts +1 -0
- package/src/bridge/events.mjs +1 -0
- package/src/bridge/googleAuth.ts +7 -152
- package/src/bridge/mcpClient.d.mts +74 -0
- package/src/bridge/mcpClient.mjs +79 -0
- package/src/bridge/methods.d.mts +22 -0
- package/src/bridge/methods.mjs +23 -0
- package/src/bridge/notifications.d.mts +12 -0
- package/src/bridge/notifications.mjs +36 -0
- package/src/bridge/operations.d.mts +44 -0
- package/src/bridge/operations.mjs +40 -0
- package/src/bridge/react/usePlatformAgentSessionDrive.test.tsx +29 -10
- package/src/bridge/react/usePlatformAgentSessionDrive.tsx +10 -81
- package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -105
- package/src/bridge/react/usePlatformAppSettings.tsx +104 -104
- package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -152
- package/src/bridge/react/usePlatformJsonStore.tsx +149 -149
- package/src/bridge/react/usePlatformOperations.tsx +75 -0
- package/src/bridge/storage.d.mts +17 -4
- package/src/bridge/storage.mjs +6 -3
- package/src/components/agents/AgentComposer.tsx +10 -6
- package/src/components/agents/AgentDrawerPanel.test.tsx +2 -205
- package/src/components/agents/AgentDrawerPanel.tsx +23 -64
- package/src/components/agents/AgentMessageBubble.tsx +59 -33
- package/src/components/agents/AgentMessageList.tsx +4 -9
- package/src/components/agents/AgentQuestionPromptList.tsx +34 -212
- package/src/components/agents/AgentToolApprovalActions.tsx +13 -11
- package/src/components/agents/AgentToolPendingList.tsx +6 -6
- package/src/components/agents/AgentToolPendingReview.tsx +6 -6
- package/src/components/agents/QuestionRequestList.tsx +278 -0
- package/src/components/agents/agentPanelStyles.ts +9 -33
- package/src/components/agents/agentTypes.ts +25 -43
- package/src/components/agents/index.ts +4 -0
- package/src/components/assets/asset-library/sidebar/AssetLibraryList.tsx +10 -4
- package/src/components/chrome/WorkspaceTabStrip.tsx +4 -3
- package/src/components/common/chat/ChatBox.tsx +9 -9
- package/src/components/common/chat/ChatModelMenu.tsx +34 -4
- package/src/components/common/chat/ChatThread.tsx +23 -7
- package/src/components/common/connections/ProviderConnection.test.tsx +208 -0
- package/src/components/common/connections/ProviderConnection.tsx +277 -0
- package/src/components/common/connections/index.ts +5 -0
- package/src/components/common/containers/AppFrame.tsx +26 -19
- package/src/components/common/containers/AppHeader.tsx +20 -20
- package/src/components/common/documents/DocumentSwitcher.tsx +63 -58
- package/src/components/common/dropdown/MenuDropdownItem.tsx +14 -2
- package/src/components/common/dropdown/MenuPortal.tsx +3 -1
- package/src/components/common/dropdown/menuViewportPosition.test.ts +20 -10
- package/src/components/common/dropdown/menuViewportPosition.ts +35 -27
- package/src/components/common/overlays/Modal.tsx +13 -2
- package/src/components/editor/EditorLinkPromptDialog.tsx +1 -1
- package/src/ics/generateIcs.test.ts +153 -0
- package/src/ics/generateIcs.ts +197 -0
- package/src/index.ts +3 -0
- package/src/net/httpRetry.test.ts +117 -0
- package/src/net/httpRetry.ts +111 -0
- package/src/theme/appAccents.ts +6 -0
- package/src/theme/appIdentityCss.mjs +11 -0
- package/src/theme/appIdentityCss.ts +11 -0
- package/src/theme/themes/dark.ts +3 -1
- package/src/theme/themes/light.ts +3 -1
- package/src/bridge/googleProviderConfig.ts +0 -73
|
@@ -613,7 +613,7 @@ const Scrim = styled.div`
|
|
|
613
613
|
|
|
614
614
|
const SheetHolder = styled.div`
|
|
615
615
|
position: absolute;
|
|
616
|
-
inset:
|
|
616
|
+
inset: 24px;
|
|
617
617
|
display: flex;
|
|
618
618
|
`
|
|
619
619
|
|
|
@@ -626,7 +626,7 @@ const Sheet = styled.div<{ $variant: 'modal' | 'landing' }>`
|
|
|
626
626
|
overflow: hidden;
|
|
627
627
|
color: ${ink};
|
|
628
628
|
background: ${elevated};
|
|
629
|
-
border-radius: ${({ $variant }) => ($variant === 'modal' ? '
|
|
629
|
+
border-radius: ${({ $variant }) => ($variant === 'modal' ? '6px' : '0')};
|
|
630
630
|
box-shadow: ${({ $variant }) =>
|
|
631
631
|
$variant === 'modal' ? '0 24px 80px rgba(14, 14, 17, 0.4)' : 'none'};
|
|
632
632
|
font-family: ${sansStack};
|
|
@@ -635,7 +635,7 @@ const Sheet = styled.div<{ $variant: 'modal' | 'landing' }>`
|
|
|
635
635
|
const Rail = styled.div`
|
|
636
636
|
display: flex;
|
|
637
637
|
flex-direction: column;
|
|
638
|
-
padding:
|
|
638
|
+
padding: 24px 18px;
|
|
639
639
|
background: ${surface};
|
|
640
640
|
border-right: 1px solid ${line};
|
|
641
641
|
`
|
|
@@ -670,14 +670,16 @@ const RailItem = styled.button<{ $active?: boolean }>`
|
|
|
670
670
|
gap: 11px;
|
|
671
671
|
width: 100%;
|
|
672
672
|
margin-bottom: 2px;
|
|
673
|
-
padding: 10px
|
|
673
|
+
padding: 8px 10px;
|
|
674
674
|
border: 0;
|
|
675
675
|
border-left: 3px solid
|
|
676
676
|
${({ $active }) => ($active ? 'var(--app-acc, #1f9d57)' : 'transparent')};
|
|
677
|
-
border-radius:
|
|
677
|
+
border-radius: ${({ $active }) => ($active ? '0 4px 4px 0' : '4px')};
|
|
678
678
|
background: ${({ $active }) =>
|
|
679
|
-
$active
|
|
680
|
-
|
|
679
|
+
$active
|
|
680
|
+
? 'color-mix(in srgb, var(--app-bg, #eef7f1) 42%, transparent)'
|
|
681
|
+
: 'transparent'};
|
|
682
|
+
color: ${({ $active }) => ($active ? ink : '#4a4a50')};
|
|
681
683
|
font: inherit;
|
|
682
684
|
font-size: 14px;
|
|
683
685
|
font-weight: ${({ $active }) => ($active ? 600 : 400)};
|
|
@@ -686,7 +688,9 @@ const RailItem = styled.button<{ $active?: boolean }>`
|
|
|
686
688
|
|
|
687
689
|
&:hover {
|
|
688
690
|
background: ${({ $active }) =>
|
|
689
|
-
$active
|
|
691
|
+
$active
|
|
692
|
+
? 'color-mix(in srgb, var(--app-bg, #eef7f1) 42%, transparent)'
|
|
693
|
+
: 'rgba(20, 18, 22, 0.05)'};
|
|
690
694
|
}
|
|
691
695
|
`
|
|
692
696
|
|
|
@@ -732,7 +736,7 @@ const MainHead = styled.div`
|
|
|
732
736
|
align-items: flex-start;
|
|
733
737
|
justify-content: space-between;
|
|
734
738
|
gap: 24px;
|
|
735
|
-
padding:
|
|
739
|
+
padding: 24px 32px 18px;
|
|
736
740
|
border-bottom: 1px solid ${line};
|
|
737
741
|
`
|
|
738
742
|
|
|
@@ -773,17 +777,17 @@ const FilterWrap = styled.div`
|
|
|
773
777
|
|
|
774
778
|
const FilterInput = styled.input`
|
|
775
779
|
width: 260px;
|
|
776
|
-
padding:
|
|
780
|
+
padding: 9px 12px 9px 38px;
|
|
777
781
|
border: 1px solid ${line};
|
|
778
|
-
border-radius:
|
|
779
|
-
background: ${
|
|
782
|
+
border-radius: 4px;
|
|
783
|
+
background: ${elevated};
|
|
780
784
|
color: ${ink};
|
|
781
785
|
font: inherit;
|
|
782
786
|
font-size: 14px;
|
|
783
787
|
outline: none;
|
|
784
788
|
|
|
785
789
|
&:focus {
|
|
786
|
-
border-color:
|
|
790
|
+
border-color: ${ink};
|
|
787
791
|
}
|
|
788
792
|
`
|
|
789
793
|
|
|
@@ -791,10 +795,10 @@ const CloseButton = styled.button`
|
|
|
791
795
|
display: flex;
|
|
792
796
|
align-items: center;
|
|
793
797
|
justify-content: center;
|
|
794
|
-
width:
|
|
795
|
-
height:
|
|
798
|
+
width: 34px;
|
|
799
|
+
height: 34px;
|
|
796
800
|
border: 1px solid ${line};
|
|
797
|
-
border-radius:
|
|
801
|
+
border-radius: 4px;
|
|
798
802
|
background: ${elevated};
|
|
799
803
|
color: #6a6a70;
|
|
800
804
|
cursor: pointer;
|
|
@@ -808,25 +812,25 @@ const Body = styled.div`
|
|
|
808
812
|
flex: 1;
|
|
809
813
|
min-height: 0;
|
|
810
814
|
overflow-y: auto;
|
|
811
|
-
padding:
|
|
815
|
+
padding: 22px 32px 32px;
|
|
812
816
|
`
|
|
813
817
|
|
|
814
818
|
const CardRow = styled.div`
|
|
815
819
|
display: grid;
|
|
816
820
|
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
|
|
817
|
-
gap:
|
|
818
|
-
margin-bottom:
|
|
821
|
+
gap: 12px;
|
|
822
|
+
margin-bottom: 28px;
|
|
819
823
|
`
|
|
820
824
|
|
|
821
825
|
const cardBase = `
|
|
822
|
-
min-height:
|
|
823
|
-
border-radius:
|
|
826
|
+
min-height: 172px;
|
|
827
|
+
border-radius: 6px;
|
|
824
828
|
cursor: pointer;
|
|
825
|
-
transition:
|
|
829
|
+
transition: box-shadow 0.12s ease, border-color 0.12s ease, background 0.12s ease;
|
|
826
830
|
|
|
827
831
|
&:hover {
|
|
828
|
-
|
|
829
|
-
box-shadow: 0
|
|
832
|
+
border-color: color-mix(in srgb, ${ink} 24%, ${line});
|
|
833
|
+
box-shadow: 0 8px 18px rgba(14, 14, 17, 0.1);
|
|
830
834
|
}
|
|
831
835
|
`
|
|
832
836
|
|
|
@@ -836,9 +840,9 @@ const NewCard = styled.button`
|
|
|
836
840
|
flex-direction: column;
|
|
837
841
|
align-items: center;
|
|
838
842
|
justify-content: center;
|
|
839
|
-
gap:
|
|
840
|
-
border:
|
|
841
|
-
background:
|
|
843
|
+
gap: 10px;
|
|
844
|
+
border: 1px solid ${line};
|
|
845
|
+
background: ${elevated};
|
|
842
846
|
font: inherit;
|
|
843
847
|
`
|
|
844
848
|
|
|
@@ -846,24 +850,23 @@ const NewIcon = styled.span`
|
|
|
846
850
|
display: flex;
|
|
847
851
|
align-items: center;
|
|
848
852
|
justify-content: center;
|
|
849
|
-
width:
|
|
850
|
-
height:
|
|
851
|
-
border-radius:
|
|
852
|
-
background:
|
|
853
|
-
color: #
|
|
853
|
+
width: 30px;
|
|
854
|
+
height: 30px;
|
|
855
|
+
border-radius: 4px;
|
|
856
|
+
background: ${elevated};
|
|
857
|
+
color: var(--app-acc, #1f9d57);
|
|
854
858
|
`
|
|
855
859
|
|
|
856
860
|
const NewLabel = styled.span`
|
|
857
861
|
font-size: 15px;
|
|
858
862
|
font-weight: 600;
|
|
859
|
-
color:
|
|
863
|
+
color: ${ink};
|
|
860
864
|
`
|
|
861
865
|
|
|
862
866
|
const NewHint = styled.span`
|
|
863
867
|
font-family: ${monoStack};
|
|
864
868
|
font-size: 10.5px;
|
|
865
|
-
color:
|
|
866
|
-
opacity: 0.7;
|
|
869
|
+
color: ${faint};
|
|
867
870
|
`
|
|
868
871
|
|
|
869
872
|
const PreviewCard = styled.button`
|
|
@@ -891,7 +894,7 @@ const PreviewMeta = styled.div`
|
|
|
891
894
|
align-items: center;
|
|
892
895
|
justify-content: space-between;
|
|
893
896
|
gap: 8px;
|
|
894
|
-
padding: 13px
|
|
897
|
+
padding: 11px 13px;
|
|
895
898
|
`
|
|
896
899
|
|
|
897
900
|
const PreviewName = styled.div<{ $serif?: boolean }>`
|
|
@@ -946,7 +949,7 @@ const SortButton = styled.button<{ $active?: boolean }>`
|
|
|
946
949
|
const ListBox = styled.div`
|
|
947
950
|
overflow: hidden;
|
|
948
951
|
border: 1px solid ${line};
|
|
949
|
-
border-radius:
|
|
952
|
+
border-radius: 6px;
|
|
950
953
|
`
|
|
951
954
|
|
|
952
955
|
const Row = styled.button<{ $selected?: boolean }>`
|
|
@@ -955,11 +958,10 @@ const Row = styled.button<{ $selected?: boolean }>`
|
|
|
955
958
|
align-items: center;
|
|
956
959
|
gap: 14px;
|
|
957
960
|
width: 100%;
|
|
958
|
-
padding:
|
|
961
|
+
padding: 11px 14px;
|
|
959
962
|
border: 0;
|
|
960
963
|
border-bottom: 1px solid ${line};
|
|
961
|
-
background: ${({ $selected }) =>
|
|
962
|
-
$selected ? 'var(--app-bg, #eef7f1)' : 'transparent'};
|
|
964
|
+
background: ${({ $selected }) => ($selected ? surface : 'transparent')};
|
|
963
965
|
font: inherit;
|
|
964
966
|
text-align: left;
|
|
965
967
|
cursor: pointer;
|
|
@@ -975,9 +977,9 @@ const RowIcon = styled.span`
|
|
|
975
977
|
justify-content: center;
|
|
976
978
|
width: 34px;
|
|
977
979
|
height: 34px;
|
|
978
|
-
border-radius:
|
|
979
|
-
background:
|
|
980
|
-
color:
|
|
980
|
+
border-radius: 4px;
|
|
981
|
+
background: ${surface};
|
|
982
|
+
color: ${faint};
|
|
981
983
|
`
|
|
982
984
|
|
|
983
985
|
const RowMain = styled.div`
|
|
@@ -1008,19 +1010,22 @@ const RowWhen = styled.div`
|
|
|
1008
1010
|
|
|
1009
1011
|
const RowBadge = styled.span<{ $draft?: boolean }>`
|
|
1010
1012
|
justify-self: end;
|
|
1011
|
-
padding:
|
|
1012
|
-
border
|
|
1013
|
+
padding: 2px 8px;
|
|
1014
|
+
border: 1px solid ${line};
|
|
1015
|
+
border-radius: 4px;
|
|
1013
1016
|
font-family: ${monoStack};
|
|
1014
1017
|
font-size: 10px;
|
|
1015
1018
|
letter-spacing: 0.06em;
|
|
1016
1019
|
text-transform: uppercase;
|
|
1017
|
-
color: ${
|
|
1018
|
-
background: ${
|
|
1020
|
+
color: ${faint};
|
|
1021
|
+
background: ${elevated};
|
|
1019
1022
|
`
|
|
1020
1023
|
|
|
1021
1024
|
const EmptyNote = styled.p`
|
|
1022
|
-
margin:
|
|
1023
|
-
padding:
|
|
1025
|
+
margin: 8px;
|
|
1026
|
+
padding: 24px 18px;
|
|
1027
|
+
border: 1px solid ${line};
|
|
1028
|
+
border-radius: 4px;
|
|
1024
1029
|
text-align: center;
|
|
1025
1030
|
color: ${faint};
|
|
1026
1031
|
font-size: 13.5px;
|
|
@@ -1030,7 +1035,7 @@ const FooterBar = styled.div`
|
|
|
1030
1035
|
display: flex;
|
|
1031
1036
|
align-items: center;
|
|
1032
1037
|
justify-content: space-between;
|
|
1033
|
-
padding:
|
|
1038
|
+
padding: 14px 32px;
|
|
1034
1039
|
border-top: 1px solid ${line};
|
|
1035
1040
|
background: ${surface};
|
|
1036
1041
|
`
|
|
@@ -1047,9 +1052,9 @@ const FooterActions = styled.div`
|
|
|
1047
1052
|
`
|
|
1048
1053
|
|
|
1049
1054
|
const SecondaryButton = styled.button`
|
|
1050
|
-
padding:
|
|
1055
|
+
padding: 8px 14px;
|
|
1051
1056
|
border: 1px solid ${line};
|
|
1052
|
-
border-radius:
|
|
1057
|
+
border-radius: 4px;
|
|
1053
1058
|
background: ${elevated};
|
|
1054
1059
|
color: #4a4a50;
|
|
1055
1060
|
font: inherit;
|
|
@@ -1063,9 +1068,9 @@ const SecondaryButton = styled.button`
|
|
|
1063
1068
|
`
|
|
1064
1069
|
|
|
1065
1070
|
const PrimaryButton = styled.button`
|
|
1066
|
-
padding:
|
|
1071
|
+
padding: 8px 16px;
|
|
1067
1072
|
border: 0;
|
|
1068
|
-
border-radius:
|
|
1073
|
+
border-radius: 4px;
|
|
1069
1074
|
background: var(--app-block, #235240);
|
|
1070
1075
|
color: #fff;
|
|
1071
1076
|
font: inherit;
|
|
@@ -1123,9 +1128,9 @@ const ThumbPills = styled.div`
|
|
|
1123
1128
|
|
|
1124
1129
|
const ThumbPill = styled.span<{ $accent?: boolean }>`
|
|
1125
1130
|
height: 5px;
|
|
1126
|
-
border-radius:
|
|
1131
|
+
border-radius: 2px;
|
|
1127
1132
|
background: ${({ $accent }) =>
|
|
1128
|
-
$accent ? '
|
|
1133
|
+
$accent ? 'rgba(20,18,22,0.24)' : 'rgba(20,18,22,0.15)'};
|
|
1129
1134
|
`
|
|
1130
1135
|
|
|
1131
1136
|
const ThumbGrid = styled.div`
|
|
@@ -1142,7 +1147,7 @@ const ThumbCell = styled.div<{ $accent?: boolean }>`
|
|
|
1142
1147
|
background: ${elevated};
|
|
1143
1148
|
border: 1px solid ${line};
|
|
1144
1149
|
border-left: ${({ $accent }) =>
|
|
1145
|
-
$accent ? '2px solid
|
|
1150
|
+
$accent ? '2px solid rgba(20,18,22,0.24)' : `1px solid ${line}`};
|
|
1146
1151
|
`
|
|
1147
1152
|
|
|
1148
1153
|
const ThumbCols = styled.div`
|
|
@@ -1170,7 +1175,7 @@ const ThumbBlock = styled.div<{ $accent?: boolean }>`
|
|
|
1170
1175
|
background: ${elevated};
|
|
1171
1176
|
border: 1px solid ${line};
|
|
1172
1177
|
border-left: ${({ $accent }) =>
|
|
1173
|
-
$accent ? '2px solid
|
|
1178
|
+
$accent ? '2px solid rgba(20,18,22,0.24)' : `1px solid ${line}`};
|
|
1174
1179
|
`
|
|
1175
1180
|
|
|
1176
1181
|
//#endregion
|
|
@@ -83,12 +83,24 @@ export function MenuDropdownItem({
|
|
|
83
83
|
$hasIcon={Boolean(item.icon)}
|
|
84
84
|
$tone={item.tone}
|
|
85
85
|
onPointerDown={event => {
|
|
86
|
-
|
|
86
|
+
// Select on pointerdown for pointing devices (fires before the menu
|
|
87
|
+
// closes) and preventDefault so the item doesn't steal focus.
|
|
88
|
+
if (
|
|
89
|
+
event.pointerType === 'mouse' ||
|
|
90
|
+
event.pointerType === 'touch' ||
|
|
91
|
+
event.pointerType === 'pen'
|
|
92
|
+
) {
|
|
87
93
|
event.preventDefault()
|
|
88
94
|
selectItem()
|
|
89
95
|
}
|
|
90
96
|
}}
|
|
91
|
-
onClick={
|
|
97
|
+
onClick={event => {
|
|
98
|
+
// A pointer interaction already selected on pointerdown above; the
|
|
99
|
+
// browser still fires a follow-up click, which would run the action a
|
|
100
|
+
// SECOND time (e.g. inserting two equations). Only handle genuine
|
|
101
|
+
// keyboard activation here — Enter/Space produce a click with detail 0.
|
|
102
|
+
if (event.detail === 0) selectItem()
|
|
103
|
+
}}
|
|
92
104
|
>
|
|
93
105
|
{item.icon ? (
|
|
94
106
|
<span className="menu-item-icon" aria-hidden="true">
|
|
@@ -137,9 +137,11 @@ export function MenuPortal({
|
|
|
137
137
|
|
|
138
138
|
const surfaceStyle: CSSProperties = layout
|
|
139
139
|
? {
|
|
140
|
-
top: layout.top,
|
|
141
140
|
left: layout.left,
|
|
142
141
|
maxHeight: layout.maxHeight,
|
|
142
|
+
// Above-opening menus are pinned by `bottom` so they grow upward.
|
|
143
|
+
...(layout.top !== undefined ? { top: layout.top } : {}),
|
|
144
|
+
...(layout.bottom !== undefined ? { bottom: layout.bottom } : {}),
|
|
143
145
|
...(layout.width !== undefined ? { width: layout.width } : {}),
|
|
144
146
|
...style,
|
|
145
147
|
}
|
|
@@ -71,7 +71,8 @@ describe('resolveMenuViewportLayout', () => {
|
|
|
71
71
|
expect(layout.width).toBeGreaterThanOrEqual(120)
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
it('
|
|
74
|
+
it('bottom-anchors a below-start menu that flips above at the viewport bottom', () => {
|
|
75
|
+
const viewportHeight = 760
|
|
75
76
|
const layout = resolveMenuViewportLayout({
|
|
76
77
|
anchorRect: {
|
|
77
78
|
top: 720,
|
|
@@ -83,16 +84,23 @@ describe('resolveMenuViewportLayout', () => {
|
|
|
83
84
|
preferredTop: 752,
|
|
84
85
|
preferredLeft: 120,
|
|
85
86
|
placement: 'below-start',
|
|
87
|
+
// Menu measured collapsed (pre-expansion) — must not drive the height.
|
|
86
88
|
menuWidth: 200,
|
|
87
|
-
menuHeight:
|
|
89
|
+
menuHeight: 8,
|
|
88
90
|
viewportWidth: 1000,
|
|
89
|
-
viewportHeight
|
|
91
|
+
viewportHeight,
|
|
90
92
|
})
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
// Pinned by bottom (grows upward), not top; its bottom edge sits above the
|
|
95
|
+
// anchor and it gets the real space above (not the ~0 measured height).
|
|
96
|
+
expect(layout.top).toBeUndefined()
|
|
97
|
+
expect(layout.bottom).toBeGreaterThan(0)
|
|
98
|
+
expect(viewportHeight - (layout.bottom ?? 0)).toBeLessThanOrEqual(720)
|
|
99
|
+
expect(layout.maxHeight).toBeGreaterThan(100)
|
|
93
100
|
})
|
|
94
101
|
|
|
95
|
-
it('
|
|
102
|
+
it('bottom-anchors above-start menus so they grow upward from the anchor', () => {
|
|
103
|
+
const viewportHeight = 760
|
|
96
104
|
const layout = resolveMenuViewportLayout({
|
|
97
105
|
anchorRect: {
|
|
98
106
|
top: 720,
|
|
@@ -105,13 +113,14 @@ describe('resolveMenuViewportLayout', () => {
|
|
|
105
113
|
preferredLeft: 120,
|
|
106
114
|
placement: 'above-start',
|
|
107
115
|
menuWidth: 200,
|
|
108
|
-
menuHeight:
|
|
116
|
+
menuHeight: 8,
|
|
109
117
|
viewportWidth: 1000,
|
|
110
|
-
viewportHeight
|
|
118
|
+
viewportHeight,
|
|
111
119
|
})
|
|
112
120
|
|
|
113
|
-
expect(layout.top).
|
|
114
|
-
expect(layout.
|
|
121
|
+
expect(layout.top).toBeUndefined()
|
|
122
|
+
expect(viewportHeight - (layout.bottom ?? 0)).toBeLessThanOrEqual(720)
|
|
123
|
+
expect(layout.maxHeight).toBeGreaterThan(100)
|
|
115
124
|
})
|
|
116
125
|
|
|
117
126
|
it('limits menu height to the remaining viewport space', () => {
|
|
@@ -134,6 +143,7 @@ describe('resolveMenuViewportLayout', () => {
|
|
|
134
143
|
})
|
|
135
144
|
|
|
136
145
|
expect(layout.maxHeight).toBeLessThanOrEqual(280)
|
|
137
|
-
expect(layout.top
|
|
146
|
+
expect(layout.top).toBeDefined()
|
|
147
|
+
expect((layout.top ?? 0) + layout.maxHeight).toBeLessThanOrEqual(300)
|
|
138
148
|
})
|
|
139
149
|
})
|
|
@@ -10,7 +10,14 @@ export const MENU_DEFAULT_MAX_HEIGHT_PX = 320
|
|
|
10
10
|
export type MenuWidthMode = 'intrinsic' | 'anchor'
|
|
11
11
|
|
|
12
12
|
export interface ResolvedMenuLayout {
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Exactly one of `top`/`bottom` is set. Above-opening menus pin by `bottom`
|
|
15
|
+
* so they grow upward — deriving `top`/height from the menu's own measurement
|
|
16
|
+
* is circular (it's measured collapsed, before the open transition expands
|
|
17
|
+
* it, which clipped bottom-anchored menus to ~0px).
|
|
18
|
+
*/
|
|
19
|
+
top?: number
|
|
20
|
+
bottom?: number
|
|
14
21
|
left: number
|
|
15
22
|
maxHeight: number
|
|
16
23
|
/** Set only for `anchor` width mode. */
|
|
@@ -78,45 +85,46 @@ export function resolveMenuViewportLayout({
|
|
|
78
85
|
Math.max(margin, viewportWidth - layoutWidth - margin),
|
|
79
86
|
)
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
const spaceAbove = Math.max(0, anchorRect.top - gap - margin)
|
|
82
89
|
|
|
90
|
+
let opensAbove: boolean
|
|
83
91
|
if (placement === 'above-start') {
|
|
84
|
-
top = anchorRect.top - gap - menuHeight
|
|
85
|
-
const spaceAbove = Math.max(0, anchorRect.top - gap - margin)
|
|
86
92
|
const spaceBelow = Math.max(
|
|
87
93
|
0,
|
|
88
94
|
viewportHeight - anchorRect.bottom - gap - margin,
|
|
89
95
|
)
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
placement === 'below-menubar-start' ||
|
|
96
|
-
placement === 'at-point'
|
|
97
|
-
) {
|
|
96
|
+
// Prefer above; flip below only if it can't fit above but fits better below.
|
|
97
|
+
opensAbove = !(menuHeight > spaceAbove && spaceBelow > spaceAbove)
|
|
98
|
+
} else {
|
|
99
|
+
// below-start / below-menubar-start / at-point: prefer below, flip above
|
|
100
|
+
// when there's no room below but more above.
|
|
98
101
|
const opensBelow = preferredTop >= anchorRect.bottom - 0.5
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (menuHeight > spaceBelow && spaceAbove > spaceBelow) {
|
|
103
|
-
top = anchorRect.top - gap - menuHeight
|
|
104
|
-
}
|
|
105
|
-
}
|
|
102
|
+
const spaceBelow = Math.max(0, viewportHeight - preferredTop - margin)
|
|
103
|
+
opensAbove =
|
|
104
|
+
opensBelow && menuHeight > spaceBelow && spaceAbove > spaceBelow
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
if (opensAbove) {
|
|
108
|
+
// Pin the panel's bottom just above the anchor so it grows UPWARD; its
|
|
109
|
+
// pre-expansion measured height can't misplace or clip it.
|
|
110
|
+
const bottom = Math.max(margin, viewportHeight - (anchorRect.top - gap))
|
|
111
|
+
return {
|
|
112
|
+
bottom,
|
|
113
|
+
left,
|
|
114
|
+
maxHeight: Math.min(maxHeight, spaceAbove),
|
|
115
|
+
...(widthMode === 'anchor' ? { width: layoutWidth } : {}),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
115
118
|
|
|
119
|
+
const top = clamp(
|
|
120
|
+
placement === 'above-start' ? anchorRect.bottom + gap : preferredTop,
|
|
121
|
+
margin,
|
|
122
|
+
Math.max(margin, viewportHeight - margin),
|
|
123
|
+
)
|
|
116
124
|
return {
|
|
117
125
|
top,
|
|
118
126
|
left,
|
|
119
|
-
maxHeight:
|
|
127
|
+
maxHeight: Math.min(maxHeight, Math.max(0, viewportHeight - top - margin)),
|
|
120
128
|
...(widthMode === 'anchor' ? { width: layoutWidth } : {}),
|
|
121
129
|
}
|
|
122
130
|
}
|
|
@@ -15,7 +15,7 @@ import { zi } from '../../../theme/themeVars.js'
|
|
|
15
15
|
import { PlatformIcon } from '../../chrome/PlatformIcon.js'
|
|
16
16
|
import { ModalChromeHeader } from './ModalChromeHeader.js'
|
|
17
17
|
|
|
18
|
-
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen'
|
|
18
|
+
export type ModalSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen'
|
|
19
19
|
|
|
20
20
|
export interface ModalProps {
|
|
21
21
|
open: boolean
|
|
@@ -42,6 +42,7 @@ const FOCUSABLE_SELECTOR = [
|
|
|
42
42
|
].join(',')
|
|
43
43
|
|
|
44
44
|
const sizeWidths = {
|
|
45
|
+
xs: 'min(420px, calc(100vw - var(--platform-spacing-xl)))',
|
|
45
46
|
sm: '42dvw',
|
|
46
47
|
md: '50dvw',
|
|
47
48
|
lg: '68dvw',
|
|
@@ -122,6 +123,9 @@ const StyledDialog = styled.div<{ $size: ModalSize; $chrome: boolean }>`
|
|
|
122
123
|
}};
|
|
123
124
|
min-height: ${({ $size, $chrome }) => {
|
|
124
125
|
if ($size === 'fullscreen') return '100%'
|
|
126
|
+
// Compact dialogs (confirmations, single-field prompts) hug their content —
|
|
127
|
+
// no forced aspect-ratio min-height that leaves a tall, empty box.
|
|
128
|
+
if ($size === 'xs') return 'auto'
|
|
125
129
|
if ($chrome && $size === 'xl') {
|
|
126
130
|
return 'min(460px, calc(100vh - var(--platform-spacing-xl)))'
|
|
127
131
|
}
|
|
@@ -314,7 +318,14 @@ export function Modal({
|
|
|
314
318
|
title={title}
|
|
315
319
|
titleId={titleId}
|
|
316
320
|
onClose={onClose}
|
|
317
|
-
|
|
321
|
+
// A modal that opens fullscreen has nothing to maximize — hide the
|
|
322
|
+
// no-op toggle. (Gate on the requested size, not the computed one,
|
|
323
|
+
// so a modal maximized via this button keeps its Restore control.)
|
|
324
|
+
onMaximize={
|
|
325
|
+
size === 'fullscreen'
|
|
326
|
+
? undefined
|
|
327
|
+
: () => setMaximized(current => !current)
|
|
328
|
+
}
|
|
318
329
|
maximized={maximized}
|
|
319
330
|
trailing={chromeTrailing}
|
|
320
331
|
/>
|