portal-mojo 0.2.20 → 0.2.21
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/docs/README.md +7 -1
- package/docs/admin-deployments.md +201 -0
- package/docs/admin-dns-certificates.md +3 -3
- package/docs/admin-dns-domains.md +92 -0
- package/docs/admin-dns.md +4 -2
- package/docs/admin-edge.md +200 -196
- package/docs/admin-geolocation.md +100 -0
- package/docs/admin-maintenance.md +133 -0
- package/docs/admin-modals.md +17 -2
- package/docs/admin-operational-fleet.md +206 -0
- package/docs/admin-presentation.md +51 -0
- package/docs/admin-signin.md +12 -2
- package/docs/client.md +7 -0
- package/docs/combobox.md +18 -0
- package/docs/popover.md +5 -4
- package/package.json +1 -1
- package/src/admin/core/OperationsHelp.tsx +10 -0
- package/src/admin/core/index.ts +6 -4
- package/src/admin/core/route-context.ts +6 -0
- package/src/admin/deployments/ApiDeploymentDetail.tsx +58 -0
- package/src/admin/deployments/DeploymentsPage.tsx +73 -0
- package/src/admin/deployments/WebAppDeploySetup.tsx +49 -0
- package/src/admin/deployments/WebAppDeploymentDetail.tsx +59 -0
- package/src/admin/deployments/WebAppDetail.tsx +104 -0
- package/src/admin/deployments/WebAppEditDialog.tsx +70 -0
- package/src/admin/deployments/WebAppKeyDialog.tsx +25 -0
- package/src/admin/deployments/WebAppOnboardingDialog.tsx +217 -0
- package/src/admin/deployments/WebAppRollbackDialog.tsx +82 -0
- package/src/admin/deployments/WebAppUploadPanel.tsx +132 -0
- package/src/admin/deployments/api.ts +158 -0
- package/src/admin/deployments/authority-state.ts +9 -0
- package/src/admin/deployments/deployment-state.ts +13 -0
- package/src/admin/deployments/index.ts +4 -0
- package/src/admin/deployments/onboarding-state.ts +11 -0
- package/src/admin/deployments/permissions.ts +37 -0
- package/src/admin/deployments/receipts.ts +78 -0
- package/src/admin/deployments/sanitize.ts +73 -0
- package/src/admin/deployments/shared.tsx +39 -0
- package/src/admin/deployments/types.ts +238 -0
- package/src/admin/deployments/upload.ts +53 -0
- package/src/admin/deployments/useDeploymentObservation.ts +104 -0
- package/src/admin/dns/DomainDetail.tsx +10 -6
- package/src/admin/dns/DomainOnboardingContent.tsx +44 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +5 -24
- package/src/admin/dns/DomainPurchaseWizard.tsx +1 -1
- package/src/admin/dns/DomainRegisterExisting.tsx +63 -0
- package/src/admin/dns/DomainRegistrarSettings.tsx +52 -0
- package/src/admin/dns/DomainsPage.tsx +22 -13
- package/src/admin/dns/SharedValidationDialog.tsx +1 -1
- package/src/admin/dns/api.ts +4 -2
- package/src/admin/dns/domain-navigation.ts +16 -0
- package/src/admin/dns/registrar-api.ts +13 -0
- package/src/admin/domains/assistant.ts +5 -4
- package/src/admin/domains/communications.ts +10 -9
- package/src/admin/domains/identity.ts +8 -7
- package/src/admin/domains/infrastructure.ts +19 -13
- package/src/admin/domains/lazy-page.ts +6 -0
- package/src/admin/domains/observability.ts +6 -5
- package/src/admin/domains/operations.ts +22 -7
- package/src/admin/domains/security.ts +16 -15
- package/src/admin/edge/EdgeHelp.tsx +10 -0
- package/src/admin/edge/ServingReadiness.tsx +54 -0
- package/src/admin/edge/VhostDetail.tsx +145 -139
- package/src/admin/edge/VhostWizard.tsx +204 -115
- package/src/admin/edge/VhostsPage.tsx +27 -28
- package/src/admin/edge/api.ts +24 -10
- package/src/admin/edge/models.ts +4 -4
- package/src/admin/edge/serving-data.ts +78 -0
- package/src/admin/fleet/CapacityEditor.tsx +81 -0
- package/src/admin/fleet/CapacityInspector.tsx +19 -0
- package/src/admin/fleet/CapacityProgress.tsx +9 -0
- package/src/admin/fleet/FleetHelp.tsx +7 -0
- package/src/admin/fleet/FleetOverview.tsx +150 -0
- package/src/admin/fleet/FleetPage.tsx +11 -0
- package/src/admin/fleet/FleetWorkspace.tsx +82 -0
- package/src/admin/fleet/ScaleFleetDialog.tsx +169 -0
- package/src/admin/fleet/access.ts +22 -0
- package/src/admin/fleet/api.ts +77 -0
- package/src/admin/fleet/data.ts +148 -0
- package/src/admin/fleet/index.ts +8 -0
- package/src/admin/fleet/operations.ts +179 -0
- package/src/admin/fleet/standalone-data.ts +87 -0
- package/src/admin/fleet/types.ts +208 -0
- package/src/admin/geolocation/GeoLocationPage.tsx +171 -0
- package/src/admin/geolocation/data.ts +138 -0
- package/src/admin/geolocation/index.ts +3 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +2 -8
- package/src/admin/index.ts +8 -0
- package/src/admin/maintenance/MaintenanceDialogs.tsx +161 -0
- package/src/admin/maintenance/MaintenancePage.tsx +442 -0
- package/src/admin/maintenance/data.ts +486 -0
- package/src/admin/maintenance/index.ts +3 -0
- package/src/admin/maintenance/operations.ts +208 -0
- package/src/admin/public/infrastructure.ts +1 -0
- package/src/admin/public/operations.ts +3 -0
- package/src/admin/registry.ts +6 -2
- package/src/admin/settings/SettingDetail.tsx +7 -2
- package/src/admin/settings/SettingEditor.tsx +2 -1
- package/src/admin/settings/SettingsPage.tsx +3 -1
- package/src/admin/settings/model.ts +1 -0
- package/src/admin/signin/SigninPage.tsx +17 -3
- package/src/admin/signin/api.ts +2 -0
- package/src/charts/MetricsChartView.tsx +4 -11
- package/src/charts/MetricsMiniWidget.tsx +3 -15
- package/src/client/client.ts +6 -0
- package/src/client/deployments-mock.ts +375 -0
- package/src/client/domain-operations-mock.ts +37 -0
- package/src/client/geo-provider-mock.ts +82 -0
- package/src/client/maintenance-mock.ts +285 -0
- package/src/client/mock.ts +52 -10
- package/src/client/operational-fleet-mock.ts +279 -0
- package/src/client/operations-mock.ts +85 -0
- package/src/client/serving-mock.ts +22 -0
- package/src/styles/components/admin-deployments.css +35 -0
- package/src/styles/components/admin-domains.css +12 -0
- package/src/styles/components/admin-edge.css +125 -1
- package/src/styles/components/admin-geolocation.css +40 -0
- package/src/styles/components/admin-maintenance.css +127 -0
- package/src/styles/components/admin-operational-fleet.css +38 -0
- package/src/styles/components/admin-operations.css +9 -0
- package/src/styles/components/admin-signin.css +2 -0
- package/src/styles/components/combobox.css +12 -2
- package/src/styles/components/fleet-page.css +9 -0
- package/src/styles/index.css +7 -0
- package/src/ui/ComboBox.tsx +39 -8
- package/src/ui/FilterBar.tsx +3 -5
- package/src/ui/ModelTable.tsx +2 -8
- package/src/ui/Popover.tsx +15 -7
- package/src/ui/date/TimePicker.tsx +4 -14
- package/src/ui/image-editor/ImageEditor.tsx +4 -5
- package/src/ui/schema-form-core.tsx +3 -5
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.geo-admin { display: grid; max-width: 880px; margin-inline: auto; gap: 20px; min-width: 0; overflow-wrap: anywhere; container-type: inline-size; }
|
|
2
|
+
.geo-admin .page-header { display: flex; justify-content: space-between; margin: 0; align-items: start; flex-wrap: wrap; gap: 12px; }
|
|
3
|
+
.geo-admin h1 { font-size: 26px; font-weight: 750; line-height: 1.2; letter-spacing: -.025em; color: var(--ink-strong); margin: 4px 0 8px; }
|
|
4
|
+
.geo-admin h2 { font-size: 16px; font-weight: 700; line-height: 1.35; color: var(--ink-strong); margin: 0; }
|
|
5
|
+
.geo-admin-setup, .geo-admin-status { min-width: 0; }
|
|
6
|
+
.geo-admin-form { padding: 24px; }
|
|
7
|
+
.geo-admin-provider-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
|
|
8
|
+
.geo-admin-fields { border: 0; padding: 0; margin: 0; min-width: 0; display: grid; gap: 22px; }
|
|
9
|
+
.geo-admin .field { display: grid; gap: 8px; min-width: 0; }
|
|
10
|
+
.geo-admin .field-label, .geo-admin-key-heading, .geo-admin-sharing { display: flex; gap: 8px; align-items: center; }
|
|
11
|
+
.geo-admin .input { width: 100%; box-sizing: border-box; min-width: 0; }
|
|
12
|
+
.geo-admin [hidden] { display: none !important; }
|
|
13
|
+
.geo-admin-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
14
|
+
.geo-admin-actions .btn { white-space: normal; }
|
|
15
|
+
.geo-admin-key { display: grid; gap: 12px; padding: 16px; background: var(--surface2); border: 1px solid var(--line); border-radius: 8px; min-width: 0; }
|
|
16
|
+
.geo-admin-key p, .geo-admin-result p, .geo-admin-status p, .geo-admin-notice p { margin: 0; }
|
|
17
|
+
.geo-admin-provider-list { padding: 0; margin: 0; list-style: none; display: grid; gap: 8px; }
|
|
18
|
+
.geo-admin-provider-list li { display: flex; justify-content: space-between; align-items: center; gap: 8px; border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; }
|
|
19
|
+
.geo-admin-provider-list small { display: block; }
|
|
20
|
+
.geo-admin-provider-list .btn { padding: 5px 8px; }
|
|
21
|
+
.geo-admin-context, .geo-admin-result { margin: 0 24px 20px; }
|
|
22
|
+
.geo-admin-result { display: grid; gap: 8px; }
|
|
23
|
+
.geo-admin-footer { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding: 16px 24px; border-top: 1px solid var(--line); }
|
|
24
|
+
.geo-admin-status { padding: 18px 24px; display: grid; gap: 10px; }
|
|
25
|
+
.geo-admin-status-heading, .geo-admin-activation { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
|
|
26
|
+
.geo-admin-status-heading .btn { font-size: 12px; padding: 5px 8px; }
|
|
27
|
+
.geo-admin-notice { border: 1px solid var(--line); background: var(--accent-soft); border-radius: 8px; padding: 12px; margin: 0; display: grid; gap: 6px; }
|
|
28
|
+
.geo-admin-dialog dl { display: grid; gap: 14px; }
|
|
29
|
+
.geo-admin-dialog dt { color: var(--mute); font-size: 12px; margin-bottom: 4px; }
|
|
30
|
+
.geo-admin-dialog dd { margin: 0; overflow-wrap: anywhere; }
|
|
31
|
+
@container (max-width: 420px) {
|
|
32
|
+
.geo-admin-provider-fields { grid-template-columns: 1fr; }
|
|
33
|
+
.geo-admin-form, .geo-admin-status { padding: 18px; }
|
|
34
|
+
.geo-admin-key { padding: 12px; }
|
|
35
|
+
.geo-admin-provider-list li { flex-wrap: wrap; }
|
|
36
|
+
.geo-admin-actions { gap: 6px; }
|
|
37
|
+
.geo-admin-footer { padding: 16px 18px; }
|
|
38
|
+
.geo-admin-context, .geo-admin-result { margin-inline: 18px; }
|
|
39
|
+
.geo-admin-sharing { align-items: start; }
|
|
40
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
.admin-maintenance {
|
|
2
|
+
display: grid;
|
|
3
|
+
gap: 1.25rem;
|
|
4
|
+
max-width: 80rem;
|
|
5
|
+
margin: auto;
|
|
6
|
+
padding: 0;
|
|
7
|
+
overflow-wrap: anywhere;
|
|
8
|
+
}
|
|
9
|
+
.admin-maintenance header,
|
|
10
|
+
.maintenance-resource {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: flex-start;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
gap: 1rem;
|
|
15
|
+
}
|
|
16
|
+
.admin-maintenance section {
|
|
17
|
+
background: var(--surface);
|
|
18
|
+
border: 1px solid var(--line);
|
|
19
|
+
border-radius: var(--radius-lg, 0.75rem);
|
|
20
|
+
padding: 1.25rem;
|
|
21
|
+
min-width: 0;
|
|
22
|
+
}
|
|
23
|
+
.admin-maintenance h1,
|
|
24
|
+
.admin-maintenance h2,
|
|
25
|
+
.admin-maintenance h3 {
|
|
26
|
+
margin: 0 0 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
.maintenance-resource {
|
|
29
|
+
padding: 1rem 0;
|
|
30
|
+
border-bottom: 1px solid var(--line);
|
|
31
|
+
}
|
|
32
|
+
.maintenance-resource:last-child {
|
|
33
|
+
border-bottom: 0;
|
|
34
|
+
}
|
|
35
|
+
.admin-maintenance p {
|
|
36
|
+
margin: 0.5rem 0;
|
|
37
|
+
}
|
|
38
|
+
.admin-maintenance dl {
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: minmax(7rem, auto) 1fr;
|
|
41
|
+
gap: 0.5rem 1rem;
|
|
42
|
+
}
|
|
43
|
+
.admin-maintenance dd {
|
|
44
|
+
margin: 0;
|
|
45
|
+
}
|
|
46
|
+
.maintenance-warning {
|
|
47
|
+
color: var(--warn);
|
|
48
|
+
}
|
|
49
|
+
.maintenance-mode {
|
|
50
|
+
color: var(--mute);
|
|
51
|
+
}
|
|
52
|
+
.maintenance-dialog {
|
|
53
|
+
color: var(--ink);
|
|
54
|
+
background: var(--surface);
|
|
55
|
+
border: 1px solid var(--line);
|
|
56
|
+
border-radius: var(--radius-lg, 0.75rem);
|
|
57
|
+
max-width: min(38rem, calc(100vw - 2rem));
|
|
58
|
+
max-height: calc(100vh - 2rem);
|
|
59
|
+
overflow: auto;
|
|
60
|
+
}
|
|
61
|
+
.maintenance-dialog::backdrop {
|
|
62
|
+
background: rgb(0 0 0 / 0.6);
|
|
63
|
+
}
|
|
64
|
+
.maintenance-dialog-body {
|
|
65
|
+
padding: 1.25rem;
|
|
66
|
+
display: grid;
|
|
67
|
+
gap: 1rem;
|
|
68
|
+
}
|
|
69
|
+
.maintenance-dialog h2,
|
|
70
|
+
.maintenance-dialog p {
|
|
71
|
+
margin: 0;
|
|
72
|
+
}
|
|
73
|
+
.maintenance-dialog fieldset {
|
|
74
|
+
display: grid;
|
|
75
|
+
gap: 0.75rem;
|
|
76
|
+
}
|
|
77
|
+
.maintenance-confirm-label {
|
|
78
|
+
display: grid;
|
|
79
|
+
gap: 0.5rem;
|
|
80
|
+
}
|
|
81
|
+
.maintenance-confirm-label input {
|
|
82
|
+
width: 100%;
|
|
83
|
+
min-width: 0;
|
|
84
|
+
background: var(--field-bg, var(--surface));
|
|
85
|
+
color: var(--ink);
|
|
86
|
+
border: 1px solid var(--line);
|
|
87
|
+
padding: 0.65rem;
|
|
88
|
+
border-radius: 0.35rem;
|
|
89
|
+
}
|
|
90
|
+
.maintenance-actions {
|
|
91
|
+
display: flex;
|
|
92
|
+
justify-content: flex-end;
|
|
93
|
+
gap: 0.75rem;
|
|
94
|
+
}
|
|
95
|
+
@media (max-width: 600px) {
|
|
96
|
+
.admin-maintenance {
|
|
97
|
+
padding: 0;
|
|
98
|
+
}
|
|
99
|
+
.admin-maintenance header,
|
|
100
|
+
.maintenance-resource {
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
}
|
|
103
|
+
.admin-maintenance section {
|
|
104
|
+
padding: 0.8rem;
|
|
105
|
+
}
|
|
106
|
+
.maintenance-actions {
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.admin-maintenance h1{font-size:26px;font-weight:750;line-height:1.2;letter-spacing:-.025em;color:var(--ink-strong)}.admin-maintenance h2,.maintenance-dialog h2{font-size:18px;font-weight:700;line-height:1.35;color:var(--ink-strong)}.admin-maintenance h3{font-size:16px;font-weight:650;line-height:1.4;color:var(--ink-strong)}
|
|
112
|
+
|
|
113
|
+
.maintenance-resource>div{min-width:0}.maintenance-resource>button{flex-shrink:0}.maintenance-resource-status{color:var(--mute)}.maintenance-details{margin-block:.65rem}.maintenance-details>summary{font-size:13px;font-weight:600;cursor:pointer;color:var(--mute)}.maintenance-details[open]>summary{margin-bottom:.65rem}.admin-maintenance .maintenance-framework-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.maintenance-framework-summary>div{min-width:0}.maintenance-framework-summary dt{font-size:12px;color:var(--mute);margin-bottom:4px}.maintenance-framework-summary dd{font-size:16px;font-weight:600;overflow-wrap:anywhere}.admin-maintenance .maintenance-resource h3{margin-bottom:.25rem}.admin-maintenance .maintenance-resource p{margin:.25rem 0}.admin-maintenance .maintenance-details p{margin:.5rem 0}@media(max-width:600px){.admin-maintenance .maintenance-framework-summary{grid-template-columns:1fr}.maintenance-resource>button{align-self:flex-start}}
|
|
114
|
+
|
|
115
|
+
.admin-maintenance { gap: 24px; }
|
|
116
|
+
.admin-maintenance header > div > p:last-child { color: var(--mute); }
|
|
117
|
+
.admin-maintenance section.maintenance-scan { padding: 0; background: transparent; border: 0; }
|
|
118
|
+
.maintenance-scan .ops-details > summary { padding: 0; }
|
|
119
|
+
.maintenance-dialog { margin: auto; padding: 0; width: min(38rem, calc(100vw - 2rem)); }
|
|
120
|
+
.maintenance-dialog fieldset { border: 1px solid var(--line); border-radius: 8px; padding: 12px 16px; }
|
|
121
|
+
.maintenance-dialog legend { padding-inline: 4px; font-weight: 600; }
|
|
122
|
+
.maintenance-dialog-body { padding: 24px; gap: 20px; }
|
|
123
|
+
|
|
124
|
+
.maintenance-dialog input[type="radio"] { margin-right: 6px; }
|
|
125
|
+
|
|
126
|
+
/* Maintenance-only dialogs and compact progress. No shared Dashboard rules. */
|
|
127
|
+
.maintenance-dialog-body{max-height:calc(100dvh - 2rem);display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box}.maintenance-dialog-scroll{min-height:0;overflow:auto;display:grid;gap:16px;padding:2px}.maintenance-dialog .maintenance-actions{flex-shrink:0;padding-top:12px;align-items:center}.maintenance-dialog .maintenance-progress-facts{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px 16px;margin:0}.maintenance-progress-facts dt{color:var(--mute)}.maintenance-progress-facts dd{margin:0;overflow-wrap:anywhere}.maintenance-attempt-row{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:12px 0;border-bottom:1px solid var(--line)}.maintenance-attempt-row:last-of-type{border-bottom:0}.admin-maintenance .maintenance-actions{justify-content:flex-start;margin-top:16px;flex-wrap:wrap}.maintenance-dialog .maintenance-dialog-scroll>p{margin:0}.maintenance-dialog .maintenance-dialog-scroll fieldset{margin:0}.maintenance-dialog .maintenance-dialog-scroll .field{margin:0}.maintenance-attempt-row>div{min-width:0}@media(max-width:600px){.maintenance-dialog-body{padding:20px;gap:16px}.maintenance-attempt-row{align-items:flex-start;flex-direction:column}.maintenance-dialog .maintenance-actions{gap:8px}.maintenance-dialog .maintenance-actions .btn{max-width:100%;white-space:normal}}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.operational-fleet{display:grid;gap:16px;min-width:0;container-type:inline-size;overflow-wrap:anywhere}.fleet-heading,.fleet-buttons{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fleet-heading h2{margin:0}.fleet-heading p{margin:4px 0 0}.fleet-evidence-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.fleet-evidence{min-width:0;overflow:hidden}.fleet-evidence h3{padding:14px 16px;margin:0;border-bottom:1px solid var(--line)}.fleet-evidence-row{display:grid;gap:6px;padding:12px 16px;border-bottom:1px solid var(--line);min-width:0}.fleet-evidence-row:last-child{border-bottom:0}.fleet-evidence-row>div:first-child{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}.fleet-evidence-row p{margin:2px 0 5px}.fleet-source-status{font-size:11px;letter-spacing:.03em;text-transform:uppercase;color:var(--mute)}.fleet-status-healthy{color:var(--ok)}.fleet-status-unhealthy{color:var(--bad)}.fleet-status-degraded,.fleet-status-stale{color:var(--warn)}.fleet-availability{border-left:3px solid var(--mute)}.fleet-availability-ok{border-left-color:var(--ok)}.fleet-availability-down{border-left-color:var(--bad)}.fleet-capacity-facts summary{cursor:pointer;font-weight:600}.fleet-fact-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.fleet-member{display:block;margin:4px 0;color:var(--mute)}.fleet-scale{display:flex;flex-direction:column;max-height:calc(100dvh - 80px);min-height:0;overflow-wrap:anywhere}.fleet-scale-body{overflow:auto;min-height:0;padding-right:4px}.fleet-scale h3{margin:20px 0 8px}.fleet-scale-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.fleet-scale-fields .field{min-width:0}.fleet-node{display:flex;gap:12px;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid var(--line)}.fleet-node small{display:block}.fleet-plan-steps{padding-left:24px}.fleet-plan-steps li{margin-bottom:14px}.fleet-progress-step{padding:10px 0;border-bottom:1px solid var(--line)}.fleet-scale .modal-actions{flex-shrink:0}.fleet-scale fieldset{border:0;margin:0;padding:0;min-width:0}@container (max-width:850px){.fleet-evidence-grid{grid-template-columns:1fr}.fleet-fact-grid{grid-template-columns:1fr}}@media(max-width:560px){.fleet-scale-fields{grid-template-columns:1fr}.fleet-node{align-items:flex-start;flex-direction:column}.fleet-scale{max-height:calc(100dvh - 48px)}}
|
|
2
|
+
|
|
3
|
+
.fleet-page>.page-header{display:flex;justify-content:space-between;align-items:start;margin-bottom:18px}.fleet-page h1{font-size:26px;font-weight:750;line-height:1.2;letter-spacing:-.025em;color:var(--ink-strong);margin:0}.fleet-heading h2{font-size:20px;font-weight:700;line-height:1.3;color:var(--ink-strong)}.fleet-evidence h3{font-size:16px;font-weight:700;color:var(--ink-strong)}.fleet-availability>strong{font-size:18px;line-height:1.35;font-weight:700;color:var(--ink-strong)}.fleet-scale h3{font-size:17px;font-weight:700;color:var(--ink-strong)}.fleet-scale h4,.fleet-fact-grid h4{font-size:15px;font-weight:650;color:var(--ink-strong)}.fleet-scale .fleet-scale-disclosure{border-top:1px solid var(--line);padding:12px 0}.fleet-scale-disclosure>summary{font-size:15px;font-weight:650;cursor:pointer;color:var(--ink-strong)}.fleet-scale-disclosure[open]>summary{margin-bottom:14px}.fleet-scale fieldset>h3:first-child{margin-top:12px}.fleet-scale fieldset>p{margin-bottom:12px}.fleet-scale fieldset>.fleet-scale-fields{margin-bottom:14px}
|
|
4
|
+
|
|
5
|
+
.fleet-evidence-grid { grid-template-columns: minmax(0, 1fr); gap: 24px; }
|
|
6
|
+
.fleet-evidence { border: 0; border-radius: 0; box-shadow: none; background: transparent; }
|
|
7
|
+
.fleet-evidence h3 { padding: 0 0 10px; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--mute); }
|
|
8
|
+
.fleet-evidence-row { grid-template-columns: minmax(130px, .7fr) minmax(0, 2fr) auto 62px; align-items: center; gap: 16px; padding: 17px 0; }
|
|
9
|
+
.fleet-row-value { min-width: 0; color: var(--mute); }
|
|
10
|
+
.fleet-detail-button { border: 0; padding: 4px 0; background: transparent; color: var(--accent); cursor: pointer; text-align: right; }
|
|
11
|
+
.fleet-source-detail > p { margin: 14px 0; }
|
|
12
|
+
.operational-fleet { gap: 24px; }
|
|
13
|
+
@container (max-width: 600px) { .fleet-evidence-row { grid-template-columns: minmax(0, 1fr) auto; gap: 6px 12px; } .fleet-row-value { grid-row: 2; } .fleet-source-status { grid-column: 2; grid-row: 1; } .fleet-detail-button { grid-column: 2; grid-row: 2; } }
|
|
14
|
+
|
|
15
|
+
.fleet-capacity-tab { padding-top: 4px; }
|
|
16
|
+
.fleet-scale-body .fleet-buttons { margin-block: 10px 18px; }
|
|
17
|
+
.fleet-scale-body > fieldset { margin-block: 12px 20px; }
|
|
18
|
+
.fleet-scale-body > section { margin-block: 16px; }
|
|
19
|
+
.fleet-scale .modal-actions { padding-top: 16px; border-top: 1px solid var(--line); }
|
|
20
|
+
.fleet-scale .input { width: 100%; min-width: 0; }
|
|
21
|
+
|
|
22
|
+
.fleet-evidence.panel { padding: 0; border: 0; border-radius: 0; box-shadow: none; background: transparent; }
|
|
23
|
+
.fleet-evidence-row { padding-block: 14px; }
|
|
24
|
+
|
|
25
|
+
.fleet-heading h2 { font-size: 26px; letter-spacing: -.025em; }
|
|
26
|
+
|
|
27
|
+
.fleet-heading h1 { margin: 0; font-size: 26px; line-height: 1.3; font-weight: 700; letter-spacing: -.025em; color: var(--ink-strong); }
|
|
28
|
+
|
|
29
|
+
.fleet-capacity-tab .panel > p { margin: 8px 0 16px; }
|
|
30
|
+
.fleet-scale-fields .field { gap: 8px; }
|
|
31
|
+
|
|
32
|
+
.fleet-capacity-dialog { display: flex; flex-direction: column; max-height: calc(100dvh - 64px); min-height: 0; overflow-wrap: anywhere; }
|
|
33
|
+
.fleet-capacity-body { overflow: auto; min-height: 0; }
|
|
34
|
+
.fleet-capacity-body p { margin: 8px 0; line-height: 1.5; }
|
|
35
|
+
.fleet-capacity-body h4 { margin: 0 0 8px; }
|
|
36
|
+
.fleet-capacity-body > .dim { margin: 0 0 18px; }
|
|
37
|
+
.fleet-capacity-dialog .modal-actions { flex: none; padding-top: 16px; border-top: 1px solid var(--line); }
|
|
38
|
+
@media (max-width: 560px) { .fleet-capacity-dialog .fleet-fact-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
.ops-help { display: inline-flex; align-items: center; justify-content: center; padding: 3px; border: 0; background: transparent; color: var(--mute); cursor: pointer; vertical-align: middle; font-size: 14px; line-height: 1; border-radius: 4px; }
|
|
2
|
+
.ops-help:hover, .ops-help[aria-expanded="true"] { color: var(--accent); background: var(--accent-soft); }
|
|
3
|
+
.ops-help-content { width: min(340px, calc(100vw - 32px)); padding: 14px 16px; font-size: 13px; line-height: 1.6; color: var(--ink); overflow-wrap: anywhere; }
|
|
4
|
+
.ops-help-content p { margin: 0 0 8px; }
|
|
5
|
+
.ops-help-content p:last-child { margin-bottom: 0; }
|
|
6
|
+
.ops-details > summary { cursor: pointer; font-size: 13px; font-weight: 600; color: var(--mute); padding-block: 6px; }
|
|
7
|
+
.ops-details[open] > summary { margin-bottom: 10px; }
|
|
8
|
+
.ops-cell { display: grid; gap: 3px; min-width: 0; }
|
|
9
|
+
.ops-cell small { color: var(--mute); font-size: 12px; }
|
|
@@ -52,6 +52,8 @@ textarea.signin-code { resize: vertical; }
|
|
|
52
52
|
.signin-note i { flex: none; color: var(--accent); }
|
|
53
53
|
.signin-note.is-warning { border-color: color-mix(in srgb, var(--warn) 35%, var(--line)); background: var(--warn-soft); }
|
|
54
54
|
.signin-note.is-warning i { color: var(--warn); }
|
|
55
|
+
.signin-note.is-danger { border-color: color-mix(in srgb, var(--bad) 35%, var(--line)); background: var(--bad-soft); }
|
|
56
|
+
.signin-note.is-danger i { color: var(--bad); }
|
|
55
57
|
.signin-provider-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
56
58
|
|
|
57
59
|
@media (max-width: 1100px) { .signin-groups { grid-template-columns: 1fr; } }
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* ComboBox — styles for portal-mojo <ComboBox> (packages/portal-mojo/src/ui/ComboBox.tsx).
|
|
2
2
|
Tokens only; reads correctly under data-theme="light" AND "dark".
|
|
3
|
-
|
|
4
|
-
to theme.css at merge. */
|
|
3
|
+
*/
|
|
5
4
|
|
|
6
5
|
.combo { position: relative; width: 100%; }
|
|
7
6
|
.combo-input-wrap { position: relative; }
|
|
@@ -30,6 +29,8 @@
|
|
|
30
29
|
/* Keyboard highlight (.combo-hot) and hover share one look — there is only
|
|
31
30
|
one "pointed at" state. */
|
|
32
31
|
.combo-option:hover, .combo-option.combo-hot { background: var(--surface2); color: var(--ink-strong); }
|
|
32
|
+
.combo-option[aria-disabled="true"] { opacity: .5; cursor: not-allowed; background: transparent; }
|
|
33
|
+
.combo-input[aria-invalid="true"] { border-color: var(--bad); }
|
|
33
34
|
.combo-option-main { flex: 1; min-width: 0; }
|
|
34
35
|
.combo-option-label { font-weight: 550; }
|
|
35
36
|
.combo-option-desc { font-size: 12px; color: var(--mute); margin-top: 1px; }
|
|
@@ -40,3 +41,12 @@
|
|
|
40
41
|
/* The <mark> around the matched substring — override the UA's yellow/black
|
|
41
42
|
so it stays legible on both themes. */
|
|
42
43
|
.combo-mark { background: var(--warn-soft); color: inherit; border-radius: 3px; padding: 0 1px; }
|
|
44
|
+
|
|
45
|
+
.page-size .combo { width: 78px; }
|
|
46
|
+
.combo { min-width: 0; }
|
|
47
|
+
.combo-input { width: 100%; min-width: 0; }
|
|
48
|
+
.combo-pop { max-height: min(300px, calc(100dvh - 16px)); min-width: min(260px, calc(100vw - 16px)); }
|
|
49
|
+
|
|
50
|
+
.page-size .combo-input { min-height: 30px; padding: 5px 30px 5px 9px; font-size: 12px; }
|
|
51
|
+
.combo.gran-select { width: 110px; }
|
|
52
|
+
.gran-select .combo-input { min-height: 30px; padding: 5px 30px 5px 9px; font-size: 12px; }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* Standalone Fleet only. Dashboard FleetOverview retains its own presentation. */
|
|
2
|
+
.fleet-page .fp-workspace{display:flex;flex-direction:column;gap:20px;min-width:0}.fleet-page .fp-header{display:flex;align-items:center;justify-content:space-between;gap:24px}.fleet-page .fp-header h1{margin:3px 0 4px}.fleet-page .fp-header p{margin:0}.fleet-page .fp-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px}.fleet-page .fp-health{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:20px 24px;border-left:3px solid var(--ok)}.fleet-page .fp-health-down{border-left-color:var(--bad)}.fleet-page .fp-health-unknown{border-left-color:var(--warn)}.fleet-page .fp-health p{margin:6px 0 0}.fleet-page .fp-evidence-grid{display:grid;grid-template-columns:1fr 1fr;align-items:start;gap:0}.fleet-page .fp-section{overflow:hidden;padding:8px 18px}.fleet-page .fp-section .mojo-tab-panel{padding:4px 0}.fleet-page .fp-section .mojo-tab-list{margin:0 2px}.fleet-page .fp-section h2{font-size:14px;margin:0;padding:18px 20px;border-bottom:1px solid var(--line)}.fleet-page .fp-source{padding:14px 12px;border-bottom:1px solid var(--line);min-width:0}.fleet-page .fp-source:nth-last-child(-n+2){border-bottom:0}.fleet-page .fp-source:last-child{border-bottom:0}.fleet-page .fp-source p{margin:5px 0 3px}.fleet-page .fp-row,.fleet-standalone-dialog .fp-row{display:flex;align-items:center;justify-content:space-between;gap:12px}.fleet-page .fp-source-meta{font-size:11px;color:var(--mute);gap:8px;flex-wrap:wrap}.fleet-page .fp-state,.fleet-standalone-dialog .fp-state{font-size:12px;font-weight:600;text-transform:capitalize}.fleet-page .fp-capacity-strip{display:grid;grid-template-columns:repeat(3,1fr) 1.6fr;padding:18px 24px;gap:18px}.fleet-page .fp-capacity-strip>div{display:flex;flex-direction:column;gap:4px}.fleet-page .fp-capacity-strip strong{font-size:23px;line-height:1.3}.fleet-page .fp-capacity-context{justify-content:center;align-items:flex-end}.fleet-page .fp-operation{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:18px 24px}.fleet-page .fp-operation p{margin:6px 0 0}.fleet-page .fp-restricted{padding:16px 20px;margin:0}.fleet-page .fp-access-note{font-size:12px;margin:0}.fleet-standalone-dialog{display:flex;flex-direction:column;max-height:min(850px,calc(100dvh - 60px));min-width:0}.fleet-standalone-dialog .fleet-standalone-body{overflow:auto;min-height:0;overscroll-behavior:contain;padding:2px 2px 12px}.fleet-standalone-dialog .modal-message{flex-shrink:0}.fleet-standalone-dialog .modal-actions{flex-shrink:0}.fleet-standalone-dialog fieldset{min-width:0;border:0;padding:0;margin:0}.fleet-standalone-dialog .fp-stack{display:flex;flex-direction:column;gap:16px}.fleet-standalone-dialog .fp-stack>p,.fleet-standalone-dialog .fp-resource p,.fleet-standalone-dialog .fp-resource h3{margin:0}.fleet-standalone-dialog .mojo-tab-panel{padding-top:20px}.fleet-standalone-dialog .fp-resource{border:1px solid var(--line);border-radius:10px;padding:18px;overflow-wrap:anywhere}.fleet-standalone-dialog .fp-resource>p+small,.fleet-standalone-dialog .fp-resource>p+p{display:block;margin-top:8px}.fleet-standalone-dialog .fp-resource h3{font-size:14px}.fleet-standalone-dialog .fp-member{display:flex;flex-direction:column;gap:6px;padding:14px 0;border-bottom:1px solid var(--line)}.fleet-standalone-dialog .fp-member:last-child{border-bottom:0}.fleet-standalone-dialog .fp-fields{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}.fleet-standalone-dialog .fp-check{display:flex;align-items:flex-start;gap:9px;font-size:13px;line-height:1.6}.fleet-standalone-dialog .fp-check input{flex:none;margin-top:4px}.fleet-standalone-dialog .fp-toolbar{margin:0 0 18px;flex-wrap:wrap}.fleet-standalone-dialog .fp-empty{margin:20px 0 4px}.fleet-standalone-dialog .fp-plan{display:flex;flex-direction:column;gap:14px;padding-left:24px;margin:0}.fleet-standalone-dialog .fp-plan p{margin:6px 0 0}.fleet-standalone-dialog pre{white-space:pre-wrap;font-family:var(--font-mono,monospace);font-size:12px}.fleet-standalone-dialog .field-help{line-height:1.6}.fleet-standalone-dialog .input{width:100%;min-width:0}.fleet-standalone-dialog .modal-title{overflow-wrap:anywhere}.fleet-standalone-dialog .fp-member .fp-fields{margin-top:8px}
|
|
3
|
+
@media(max-width:700px){.fleet-page .fp-header{align-items:flex-start;flex-direction:column;gap:16px}.fleet-page .fp-actions{justify-content:flex-start}.fleet-page .fp-evidence-grid{grid-template-columns:1fr}.fleet-page .fp-capacity-strip{grid-template-columns:repeat(3,1fr);padding:16px;gap:16px}.fleet-page .fp-capacity-context{grid-column:1/-1;align-items:flex-start}.fleet-page .fp-health,.fleet-page .fp-operation{padding:18px;align-items:flex-start}.fleet-page .fp-operation{flex-direction:column}.fleet-standalone-dialog{max-height:calc(100dvh - 32px)}.fleet-standalone-dialog .fp-fields{grid-template-columns:1fr}.fleet-standalone-dialog .fp-resource>.fp-row{align-items:flex-start;flex-wrap:wrap}.fleet-standalone-dialog .fp-resource{padding:14px}.fleet-page .fp-source{padding:12px 4px}.fleet-page .fp-source:nth-last-child(2){border-bottom:1px solid var(--line)}.fleet-page .fp-section{padding:8px 14px}}
|
|
4
|
+
/* Keep every inspection tab visible within a narrow native dialog. */
|
|
5
|
+
@media(max-width:700px){.fleet-standalone-dialog .mojo-tab-list{flex-wrap:wrap;overflow:visible}}
|
|
6
|
+
|
|
7
|
+
/* Destination names are actions; keep evidence-only labels visually distinct. */
|
|
8
|
+
.fleet-page .fp-source strong a{color:var(--accent);text-decoration:underline;text-underline-offset:3px}
|
|
9
|
+
.fleet-page .fp-source strong a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:2px}
|
package/src/styles/index.css
CHANGED
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
@import "./components/admin-assistant.css" layer(portal-mojo);
|
|
25
25
|
@import "./components/admin-security.css" layer(portal-mojo);
|
|
26
26
|
@import "./components/admin-dashboard.css" layer(portal-mojo);
|
|
27
|
+
@import "./components/admin-operational-fleet.css" layer(portal-mojo);
|
|
28
|
+
@import "./components/fleet-page.css" layer(portal-mojo);
|
|
29
|
+
@import "./components/admin-maintenance.css" layer(portal-mojo);
|
|
30
|
+
@import "./components/admin-deployments.css" layer(portal-mojo);
|
|
31
|
+
@import "./components/admin-domains.css" layer(portal-mojo);
|
|
32
|
+
@import "./components/admin-operations.css" layer(portal-mojo);
|
|
33
|
+
@import "./components/admin-geolocation.css" layer(portal-mojo);
|
|
27
34
|
@import "./components/admin-cloudwatch.css" layer(portal-mojo);
|
|
28
35
|
@import "./components/admin-monitoring.css" layer(portal-mojo);
|
|
29
36
|
@import "./components/admin-bouncer.css" layer(portal-mojo);
|
package/src/ui/ComboBox.tsx
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// (web-mojo strict-equalled `data-value` strings against numbers).
|
|
19
19
|
// - Popover is the sole outside-click/Escape owner. ComboBox owns only the
|
|
20
20
|
// meaning of each dismissal (settle outside, revert on Escape).
|
|
21
|
-
import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
21
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState, type InputHTMLAttributes, type ReactNode, type Ref } from 'react';
|
|
22
22
|
import { Popover } from './Popover';
|
|
23
23
|
|
|
24
24
|
export type ComboValue = string | number;
|
|
@@ -28,11 +28,13 @@ export interface ComboOption {
|
|
|
28
28
|
label: string;
|
|
29
29
|
/** Muted second line in the dropdown (rendered while `showDescription`). */
|
|
30
30
|
description?: string;
|
|
31
|
+
/** Visible but unavailable; cannot be committed by pointer, typing or keyboard. */
|
|
32
|
+
disabled?: boolean;
|
|
31
33
|
/** Opaque payload handed back through onSelect — rule-builder hook. */
|
|
32
34
|
meta?: Record<string, unknown>;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
export interface ComboBoxProps {
|
|
37
|
+
export interface ComboBoxProps extends Pick<InputHTMLAttributes<HTMLInputElement>, 'aria-label' | 'aria-labelledby' | 'aria-describedby' | 'aria-invalid' | 'title' | 'autoFocus' | 'name'> {
|
|
36
38
|
/** Committed value. The input DISPLAYS the matching option's label. */
|
|
37
39
|
value?: ComboValue | null;
|
|
38
40
|
/** Bare strings normalize to { value: s, label: s }. */
|
|
@@ -54,6 +56,7 @@ export interface ComboBoxProps {
|
|
|
54
56
|
required?: boolean;
|
|
55
57
|
id?: string;
|
|
56
58
|
className?: string;
|
|
59
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function normalizeOptions(input: ReadonlyArray<ComboOption | string>): ComboOption[] {
|
|
@@ -66,6 +69,7 @@ function normalizeOptions(input: ReadonlyArray<ComboOption | string>): ComboOpti
|
|
|
66
69
|
const opt: ComboOption = { value: raw.value, label: raw.label || String(raw.value) };
|
|
67
70
|
if (raw.description) opt.description = raw.description;
|
|
68
71
|
if (raw.meta) opt.meta = raw.meta;
|
|
72
|
+
if (raw.disabled) opt.disabled = true;
|
|
69
73
|
out.push(opt);
|
|
70
74
|
}
|
|
71
75
|
return out;
|
|
@@ -115,6 +119,8 @@ export function ComboBox({
|
|
|
115
119
|
required = false,
|
|
116
120
|
id,
|
|
117
121
|
className,
|
|
122
|
+
inputRef: externalInputRef,
|
|
123
|
+
...inputAttributes
|
|
118
124
|
}: ComboBoxProps) {
|
|
119
125
|
// `draft` is the uncommitted typed text; null = not editing, display the
|
|
120
126
|
// committed value's label. This split IS the one-pipeline fix.
|
|
@@ -123,6 +129,11 @@ export function ComboBox({
|
|
|
123
129
|
const [highlight, setHighlight] = useState(-1);
|
|
124
130
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
125
131
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
132
|
+
const setInputRef = useCallback((node: HTMLInputElement | null) => {
|
|
133
|
+
inputRef.current = node;
|
|
134
|
+
if (typeof externalInputRef === 'function') return externalInputRef(node);
|
|
135
|
+
if (externalInputRef) externalInputRef.current = node;
|
|
136
|
+
}, [externalInputRef]);
|
|
126
137
|
// Mirror of `draft`, nulled synchronously on commit/revert so the
|
|
127
138
|
// mousedown-outside + blur pair can both call settle() without
|
|
128
139
|
// double-committing (blur fires before React re-renders).
|
|
@@ -152,6 +163,9 @@ export function ComboBox({
|
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
const canOpen = !disabled && !readOnly;
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!canOpen) { draftRef.current = null; setDraft(null); setOpen(false); setHighlight(-1); }
|
|
168
|
+
}, [canOpen]);
|
|
155
169
|
const displayValue = draft ?? (selected ? selected.label : value == null ? '' : String(value));
|
|
156
170
|
|
|
157
171
|
const setDraftBoth = (d: string | null) => { draftRef.current = d; setDraft(d); };
|
|
@@ -159,12 +173,15 @@ export function ComboBox({
|
|
|
159
173
|
|
|
160
174
|
// ── the ONE committed pipeline ───────────────────────────────────────
|
|
161
175
|
const commitOption = (opt: ComboOption) => {
|
|
176
|
+
if (!canOpen || inputRef.current?.matches(':disabled')) return;
|
|
162
177
|
setDraftBoth(null);
|
|
163
178
|
close();
|
|
179
|
+
if (opt.disabled) return;
|
|
164
180
|
if (opt.value !== value) onChange?.(opt.value, opt);
|
|
165
181
|
onSelect?.(opt);
|
|
166
182
|
};
|
|
167
183
|
const commitCustom = (text: string) => {
|
|
184
|
+
if (!canOpen || inputRef.current?.matches(':disabled')) return;
|
|
168
185
|
setDraftBoth(null);
|
|
169
186
|
close();
|
|
170
187
|
if (text !== (value == null ? '' : String(value))) onChange?.(text);
|
|
@@ -193,18 +210,27 @@ export function ComboBox({
|
|
|
193
210
|
document.getElementById(`combo-opt-${uid}-${highlight}`)?.scrollIntoView({ block: 'nearest' });
|
|
194
211
|
}, [highlight, uid]);
|
|
195
212
|
|
|
213
|
+
const moveHighlight = (direction: 1 | -1) => setHighlight((current) => {
|
|
214
|
+
let next = current < 0 && direction === -1 ? 0 : current;
|
|
215
|
+
for (let count = 0; count < visible.length; count++) {
|
|
216
|
+
next = (next + direction + visible.length) % visible.length;
|
|
217
|
+
if (!visible[next].disabled) return next;
|
|
218
|
+
}
|
|
219
|
+
return -1;
|
|
220
|
+
});
|
|
221
|
+
|
|
196
222
|
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
197
223
|
switch (e.key) {
|
|
198
224
|
case 'ArrowDown':
|
|
199
225
|
if (!canOpen) break;
|
|
200
226
|
e.preventDefault();
|
|
201
227
|
if (!open) setOpen(true);
|
|
202
|
-
else
|
|
228
|
+
else moveHighlight(1);
|
|
203
229
|
break;
|
|
204
230
|
case 'ArrowUp':
|
|
205
231
|
if (!open) break;
|
|
206
232
|
e.preventDefault();
|
|
207
|
-
|
|
233
|
+
moveHighlight(-1);
|
|
208
234
|
break;
|
|
209
235
|
case 'Enter':
|
|
210
236
|
if (open && highlight >= 0 && highlight < visible.length) {
|
|
@@ -251,7 +277,8 @@ export function ComboBox({
|
|
|
251
277
|
<div className={`combo${className ? ` ${className}` : ''}`} ref={wrapRef}>
|
|
252
278
|
<div className="combo-input-wrap">
|
|
253
279
|
<input
|
|
254
|
-
|
|
280
|
+
{...inputAttributes}
|
|
281
|
+
ref={setInputRef}
|
|
255
282
|
id={id}
|
|
256
283
|
type="text"
|
|
257
284
|
className="input combo-input"
|
|
@@ -267,6 +294,7 @@ export function ComboBox({
|
|
|
267
294
|
readOnly={readOnly}
|
|
268
295
|
required={required}
|
|
269
296
|
onFocus={() => { if (canOpen) setOpen(true); }}
|
|
297
|
+
onClick={() => { if (canOpen) setOpen(true); }}
|
|
270
298
|
onBlur={(e) => {
|
|
271
299
|
if (wrapRef.current && e.relatedTarget && wrapRef.current.contains(e.relatedTarget as Node)) return;
|
|
272
300
|
settle();
|
|
@@ -287,7 +315,8 @@ export function ComboBox({
|
|
|
287
315
|
type="button"
|
|
288
316
|
className="combo-toggle"
|
|
289
317
|
tabIndex={-1}
|
|
290
|
-
aria-
|
|
318
|
+
aria-hidden="true"
|
|
319
|
+
title="Show options"
|
|
291
320
|
onMouseDown={(e) => e.preventDefault() /* keep input focus */}
|
|
292
321
|
onClick={onToggle}
|
|
293
322
|
>
|
|
@@ -297,7 +326,7 @@ export function ComboBox({
|
|
|
297
326
|
</div>
|
|
298
327
|
<Popover
|
|
299
328
|
anchorRef={wrapRef}
|
|
300
|
-
open={open}
|
|
329
|
+
open={open && canOpen}
|
|
301
330
|
onClose={(reason) => reason === 'escape' ? revert() : settle()}
|
|
302
331
|
id={listId}
|
|
303
332
|
className="combo-pop"
|
|
@@ -316,9 +345,11 @@ export function ComboBox({
|
|
|
316
345
|
id={optionId(i)}
|
|
317
346
|
role="option"
|
|
318
347
|
aria-selected={isSelected}
|
|
348
|
+
data-value={opt.value}
|
|
349
|
+
aria-disabled={opt.disabled || undefined}
|
|
319
350
|
className={`combo-option${i === highlight ? ' combo-hot' : ''}${isSelected ? ' combo-selected' : ''}`}
|
|
320
351
|
onMouseDown={(e) => e.preventDefault() /* option click must not blur-settle first */}
|
|
321
|
-
onClick={() => commitOption(opt)}
|
|
352
|
+
onClick={() => { if (!opt.disabled) commitOption(opt); }}
|
|
322
353
|
>
|
|
323
354
|
<div className="combo-option-main">
|
|
324
355
|
<div className="combo-option-label">{highlightLabel(opt.label, (draft ?? '').trim())}</div>
|
package/src/ui/FilterBar.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ComboBox } from './ComboBox';
|
|
1
2
|
// FilterBar — web-mojo's filter UX: an "Add filter" menu listing every
|
|
2
3
|
// available filter with a per-type icon and a check on the active ones, each
|
|
3
4
|
// opening a type-appropriate dialog; applied filters become editable pills.
|
|
@@ -126,13 +127,10 @@ function FilterForm({ def, current, onDone }: {
|
|
|
126
127
|
))}
|
|
127
128
|
</div>
|
|
128
129
|
) : def.type === 'select' || def.type === 'boolean' ? (
|
|
129
|
-
<
|
|
130
|
-
<option value="">Select…</option>
|
|
131
|
-
{(def.type === 'boolean'
|
|
130
|
+
<ComboBox allowCustom={false} maxSuggestions={Number.MAX_SAFE_INTEGER} value={String(text)} onChange={(selectedValue) => setText(String(selectedValue))} autoFocus options={[({ value: "", label: "Select…" }), ...((def.type === 'boolean'
|
|
132
131
|
? [{ value: 'true', label: def.trueLabel ?? 'True' }, { value: 'false', label: def.falseLabel ?? 'False' }]
|
|
133
132
|
: def.options ?? []
|
|
134
|
-
).map((o) =>
|
|
135
|
-
</select>
|
|
133
|
+
).map((o) => ({ value: String(o.value), label: String(o.label) })) ?? [])]} />
|
|
136
134
|
) : (
|
|
137
135
|
<input
|
|
138
136
|
className="input"
|
package/src/ui/ModelTable.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ComboBox } from './ComboBox';
|
|
1
2
|
// ModelTable — schema-driven server table at web-mojo TableView fidelity
|
|
2
3
|
// (ListView.js / TableView.js / TablePage.js port). No table library: sort,
|
|
3
4
|
// filter, search and paging are all wire params django-mojo processes, so
|
|
@@ -986,14 +987,7 @@ function ModelTableView<T extends { id: RowId }>(props: ModelTableProps<T> & {
|
|
|
986
987
|
)}
|
|
987
988
|
<label className="page-size">
|
|
988
989
|
<span className="dim">Rows</span>
|
|
989
|
-
<
|
|
990
|
-
className="input input-compact"
|
|
991
|
-
value={p.size}
|
|
992
|
-
onChange={(e) => p.setSize(Number(e.target.value))}
|
|
993
|
-
aria-label="Rows per page"
|
|
994
|
-
>
|
|
995
|
-
{PAGE_SIZES.map((s) => <option key={s} value={s}>{s}</option>)}
|
|
996
|
-
</select>
|
|
990
|
+
<ComboBox allowCustom={false} maxSuggestions={Number.MAX_SAFE_INTEGER} value={String(p.size)} onChange={(selectedValue) => p.setSize(Number(String(selectedValue)))} aria-label="Rows per page" options={[...(PAGE_SIZES.map((s) => ({ value: String(s), label: String(s) })) ?? [])]} />
|
|
997
991
|
</label>
|
|
998
992
|
</div>
|
|
999
993
|
</div>
|
package/src/ui/Popover.tsx
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// flagged autofocus, and we don't. Typing in an anchored input keeps
|
|
27
27
|
// working; per-option focus retention (e.g. preventDefault on option
|
|
28
28
|
// mousedown) belongs to the consuming control.
|
|
29
|
-
import {
|
|
29
|
+
import { useLayoutEffect, useRef, type HTMLAttributes, type RefObject } from 'react';
|
|
30
30
|
import { createPortal } from 'react-dom';
|
|
31
31
|
|
|
32
32
|
export type PopoverPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
@@ -52,6 +52,7 @@ export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
52
52
|
|
|
53
53
|
const PLACEMENTS: readonly PopoverPlacement[] = ['bottom-start', 'bottom-end', 'top-start', 'top-end'];
|
|
54
54
|
const VIEWPORT_INSET = 8; // px breathing room at viewport edges (source parity)
|
|
55
|
+
const openPopovers: HTMLDivElement[] = [];
|
|
55
56
|
|
|
56
57
|
const SUPPORTS_POPOVER =
|
|
57
58
|
typeof HTMLElement !== 'undefined' && 'showPopover' in HTMLElement.prototype;
|
|
@@ -71,7 +72,7 @@ export function Popover({
|
|
|
71
72
|
// Latest-callback ref so an inline `onClose` doesn't churn the document
|
|
72
73
|
// listeners (and their deferred attach) on every parent render.
|
|
73
74
|
const onCloseRef = useRef(onClose);
|
|
74
|
-
|
|
75
|
+
useLayoutEffect(() => { onCloseRef.current = onClose; });
|
|
75
76
|
|
|
76
77
|
// ── Show + position ──────────────────────────────────────────────
|
|
77
78
|
// Layout effect: promote to the top layer and position BEFORE paint, so
|
|
@@ -146,8 +147,11 @@ export function Popover({
|
|
|
146
147
|
}, [open, placement, gap, anchorRef]);
|
|
147
148
|
|
|
148
149
|
// ── Outside-mousedown + Escape ───────────────────────────────────
|
|
149
|
-
|
|
150
|
+
useLayoutEffect(() => {
|
|
150
151
|
if (!open) return;
|
|
152
|
+
const popover = popRef.current;
|
|
153
|
+
if (!popover) return;
|
|
154
|
+
openPopovers.push(popover);
|
|
151
155
|
|
|
152
156
|
const onDocDown = (event: MouseEvent) => {
|
|
153
157
|
const target = event.target as Node;
|
|
@@ -159,7 +163,9 @@ export function Popover({
|
|
|
159
163
|
onCloseRef.current('outside');
|
|
160
164
|
};
|
|
161
165
|
const onKey = (event: KeyboardEvent) => {
|
|
162
|
-
if (event.key !== 'Escape') return;
|
|
166
|
+
if (event.key !== 'Escape' || event.defaultPrevented || openPopovers.at(-1) !== popover) return;
|
|
167
|
+
const targetDialog = event.target instanceof popover.ownerDocument.defaultView!.Element ? event.target.closest('dialog') : null;
|
|
168
|
+
if (targetDialog && targetDialog !== anchorRef.current?.closest('dialog')) return;
|
|
163
169
|
// Capture phase + preventDefault: when the anchor lives inside
|
|
164
170
|
// an open <dialog>, the first Escape must close the popover
|
|
165
171
|
// ONLY — preventing default stops the dialog's cancel/close
|
|
@@ -170,16 +176,18 @@ export function Popover({
|
|
|
170
176
|
onCloseRef.current('escape');
|
|
171
177
|
};
|
|
172
178
|
|
|
173
|
-
//
|
|
174
|
-
//
|
|
179
|
+
// Escape must be active in the opening frame, before native dialog
|
|
180
|
+
// cancellation. Only outside-click registration waits out the opener.
|
|
181
|
+
document.addEventListener('keydown', onKey, true);
|
|
175
182
|
const timer = window.setTimeout(() => {
|
|
176
183
|
document.addEventListener('mousedown', onDocDown, true);
|
|
177
|
-
document.addEventListener('keydown', onKey, true);
|
|
178
184
|
}, 0);
|
|
179
185
|
return () => {
|
|
180
186
|
window.clearTimeout(timer);
|
|
181
187
|
document.removeEventListener('mousedown', onDocDown, true);
|
|
182
188
|
document.removeEventListener('keydown', onKey, true);
|
|
189
|
+
const index = openPopovers.indexOf(popover);
|
|
190
|
+
if (index !== -1) openPopovers.splice(index, 1);
|
|
183
191
|
};
|
|
184
192
|
}, [open, anchorRef]);
|
|
185
193
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ComboBox } from '../ComboBox';
|
|
1
2
|
// TimePicker — HH:MM time input with the stepper panel and optional IANA
|
|
2
3
|
// timezone. Ported from web-mojo src/core/forms/inputs/TimePicker.js (648
|
|
3
4
|
// lines, read in full) + the `.mojo-time-*` block of calendar.css.
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
//
|
|
26
27
|
// Timezone-picker SEAM: this component owns every bit of the zone's value and
|
|
27
28
|
// serialization logic but renders the picker through `renderTimezoneSelect`.
|
|
28
|
-
// Without that prop it falls back to a
|
|
29
|
+
// Without that prop it falls back to a searchable ComboBox built from
|
|
29
30
|
// Intl.supportedValuesOf('timeZone'), so the feature is complete standalone.
|
|
30
31
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
31
32
|
import type { KeyboardEvent as ReactKeyboardEvent, ReactNode, RefObject } from 'react';
|
|
@@ -92,7 +93,7 @@ export interface TimePickerProps {
|
|
|
92
93
|
timezones?: string[] | null;
|
|
93
94
|
/** Default 'iso'. Unknown values warn + fall back. */
|
|
94
95
|
outputFormat?: TimeOutputFormat;
|
|
95
|
-
/** Zone-picker slot. Absent → the built-in
|
|
96
|
+
/** Zone-picker slot. Absent → the built-in ComboBox. */
|
|
96
97
|
renderTimezoneSelect?: (props: TimezoneSelectSlotProps) => ReactNode;
|
|
97
98
|
/** Fires on COMMIT only, and only when the serialized value changed. */
|
|
98
99
|
onChange?: (event: TimeChangeEvent) => void;
|
|
@@ -252,18 +253,7 @@ function FallbackTimezoneSelect({ value, onChange, timezones, disabled }: Timezo
|
|
|
252
253
|
}, [known, value]);
|
|
253
254
|
|
|
254
255
|
return (
|
|
255
|
-
<select
|
|
256
|
-
className="input mojo-time-tz-select"
|
|
257
|
-
aria-label="Timezone"
|
|
258
|
-
value={value ?? ''}
|
|
259
|
-
disabled={disabled}
|
|
260
|
-
onChange={(e) => onChange(e.currentTarget.value || null)}
|
|
261
|
-
>
|
|
262
|
-
<option value="">(no timezone)</option>
|
|
263
|
-
{/* A value the list doesn't carry still shows — never render nothing. */}
|
|
264
|
-
{!known && value ? <option value={value}>{value}</option> : null}
|
|
265
|
-
{zones.map((z) => <option key={z} value={z}>{z}</option>)}
|
|
266
|
-
</select>
|
|
256
|
+
<ComboBox allowCustom={false} maxSuggestions={Number.MAX_SAFE_INTEGER} className="mojo-time-tz-select" aria-label="Timezone" value={String(value ?? '')} disabled={disabled} onChange={(selectedValue) => onChange(String(selectedValue) || null)} options={[({ value: "", label: "(no timezone)" }), ...(!known && value ? [({ value: String(value), label: String(value) })] : []), ...(zones.map((z) => ({ value: String(z), label: String(z) })) ?? [])]} />
|
|
267
257
|
);
|
|
268
258
|
}
|
|
269
259
|
|