@codemem/server 0.28.0 → 0.29.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.
package/static/index.html CHANGED
@@ -469,6 +469,16 @@
469
469
  flex-direction: column;
470
470
  gap: var(--sp-5);
471
471
  }
472
+ /* Sync tab wraps its cards in #syncMainView / #syncDiagnosticsView
473
+ sub-view containers, which breaks the .tab-panel flex gap inheritance.
474
+ Restore the gap inside each sub-view so Sync cards match the Feed
475
+ tab's card rhythm. */
476
+ #syncMainView,
477
+ #syncDiagnosticsView {
478
+ display: flex;
479
+ flex-direction: column;
480
+ gap: var(--sp-5);
481
+ }
472
482
 
473
483
  /* ── Cards / sections ──────────────────────────────────── */
474
484
 
@@ -713,6 +723,84 @@
713
723
  margin-left: 2px;
714
724
  }
715
725
 
726
+ /* Scope-defaults drawer inside a group card. Reads top-down:
727
+ header · helper · auto-seed toggle (master) · include chips
728
+ · exclude chips · actions. Delimits itself as its own subsection
729
+ with a top border + breathing room so it doesn't run into the
730
+ peer-actions row above. Radix Collapsible.Content is the host,
731
+ so open/close is driven by data-state. */
732
+ .coordinator-admin-group-preferences {
733
+ display: grid;
734
+ gap: var(--sp-4);
735
+ padding-top: var(--sp-3);
736
+ border-top: 1px solid var(--border);
737
+ min-width: 0;
738
+ }
739
+ .coordinator-admin-group-preferences[data-state="closed"] { display: none; }
740
+ /* Rotate the trigger's chevron when the scope-defaults disclosure
741
+ is open. Matches the sync-tab device-row chevron affordance. */
742
+ .coordinator-admin-scope-trigger[data-state="open"] .device-row-chevron {
743
+ transform: rotate(90deg);
744
+ }
745
+ .coordinator-admin-scope-trigger .device-row-chevron {
746
+ display: inline-flex;
747
+ width: 14px;
748
+ height: 14px;
749
+ transition: transform 140ms ease;
750
+ }
751
+ .coordinator-admin-scope-trigger .device-row-chevron svg {
752
+ width: 100%;
753
+ height: 100%;
754
+ }
755
+
756
+ .coordinator-admin-group-preferences > .coordinator-admin-field,
757
+ .coordinator-admin-group-preferences > .coordinator-admin-inline-filter {
758
+ min-width: 0;
759
+ }
760
+
761
+ .coordinator-admin-drawer-title {
762
+ font-size: 13px;
763
+ font-weight: 600;
764
+ color: var(--text-primary);
765
+ margin: 0;
766
+ letter-spacing: 0.01em;
767
+ }
768
+
769
+ /* Tighten the helper text directly under the drawer title so they
770
+ read as one intro block, then let the sp-4 gap separate that
771
+ block from the toggle. */
772
+ .coordinator-admin-group-preferences > .coordinator-admin-drawer-title + .peer-submeta {
773
+ margin-top: calc(-1 * var(--sp-3));
774
+ }
775
+
776
+ /* Help text that lives INSIDE a chip field (e.g. "Exact names,
777
+ leave empty to allow all") — looser than the row gap, tighter
778
+ than the inter-section gap. */
779
+ .coordinator-admin-group-preferences .coordinator-admin-field > .peer-submeta {
780
+ margin-top: calc(-1 * var(--sp-1));
781
+ }
782
+
783
+ /* Dim the chip editor when auto-seed is off so it visually reads
784
+ as "configuration that isn't currently applied" rather than
785
+ "broken input". */
786
+ .coordinator-admin-group-preferences .peer-scope-input:disabled {
787
+ opacity: 0.55;
788
+ cursor: not-allowed;
789
+ }
790
+
791
+ /* Separate the action row (Save / Cancel) from the last field with
792
+ an explicit gap + top border so the terminating region reads as
793
+ distinct from the form controls above. */
794
+ .coordinator-admin-group-preferences > .peer-actions {
795
+ margin-top: var(--sp-1);
796
+ padding-top: var(--sp-3);
797
+ border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
798
+ }
799
+
800
+ .coordinator-admin-group-preferences > .coordinator-admin-inline-filter {
801
+ gap: var(--sp-3);
802
+ }
803
+
716
804
  .coordinator-admin-empty-state {
717
805
  padding: var(--sp-3) 0 var(--sp-1);
718
806
  }
@@ -746,6 +834,32 @@
746
834
  align-items: center;
747
835
  }
748
836
 
837
+ /* Utility link that navigates between sibling sub-surfaces (e.g.
838
+ main Sync view ↔ diagnostics sub-view). One treatment used for
839
+ both directions so header/back links read as the same vocabulary
840
+ — quieter than any real button nearby, since it's navigation,
841
+ not action. */
842
+ .sync-subview-link {
843
+ display: inline-flex;
844
+ align-items: center;
845
+ gap: 4px;
846
+ font-size: 13px;
847
+ font-weight: 500;
848
+ color: var(--text-secondary);
849
+ text-decoration: none;
850
+ padding: 4px 6px;
851
+ border-radius: var(--radius-sm);
852
+ transition: color 140ms ease;
853
+ }
854
+ .sync-subview-link:hover {
855
+ color: var(--accent);
856
+ }
857
+ .sync-subview-link:focus-visible {
858
+ color: var(--accent);
859
+ outline: 2px solid var(--accent);
860
+ outline-offset: 2px;
861
+ }
862
+
749
863
  .section-meta {
750
864
  color: var(--text-tertiary);
751
865
  font-size: 12px;
@@ -860,6 +974,144 @@
860
974
  box-shadow: 0 0 0 4px var(--focus-ring);
861
975
  }
862
976
 
977
+ /* Device-row compact header. A non-interactive container holding
978
+ the status pip + name + chips + meta, with a dedicated disclosure
979
+ button (.device-row-toggle) for assistive tech and keyboard users.
980
+ The whole row still responds to click via a background click
981
+ handler on .peer-card, but the accessible name of the toggle stays
982
+ clean ("Expand device work").
983
+ See docs/plans/2026-04-23-sync-tab-redesign.md (PR 2). */
984
+ .device-row-head {
985
+ display: flex;
986
+ align-items: center;
987
+ gap: var(--sp-3);
988
+ width: 100%;
989
+ min-height: 56px;
990
+ padding: 10px var(--sp-4);
991
+ }
992
+ /* Row head doubles as the Collapsible trigger so clicking anywhere
993
+ on the band toggles the drawer. Button defaults are reset; the
994
+ chevron at the right is a visual indicator only. */
995
+ button.device-row-head--trigger {
996
+ background: transparent;
997
+ border: none;
998
+ color: inherit;
999
+ text-align: left;
1000
+ font: inherit;
1001
+ cursor: pointer;
1002
+ }
1003
+ button.device-row-head--trigger:hover {
1004
+ background: color-mix(in srgb, var(--surface-2) 60%, transparent);
1005
+ }
1006
+ button.device-row-head--trigger:focus-visible {
1007
+ outline: 2px solid var(--accent);
1008
+ outline-offset: -2px;
1009
+ }
1010
+ .device-row-chevron {
1011
+ margin-left: auto;
1012
+ flex-shrink: 0;
1013
+ display: inline-flex;
1014
+ align-items: center;
1015
+ justify-content: center;
1016
+ width: 32px;
1017
+ height: 32px;
1018
+ color: var(--text-secondary);
1019
+ }
1020
+ .device-row-chevron svg {
1021
+ width: 18px;
1022
+ height: 18px;
1023
+ transition: transform 140ms ease;
1024
+ }
1025
+ button.device-row-head--trigger[data-state="open"] .device-row-chevron svg {
1026
+ transform: rotate(90deg);
1027
+ }
1028
+ .device-row-head .device-row-name {
1029
+ flex: 1 1 auto;
1030
+ min-width: 0;
1031
+ font-size: 14px;
1032
+ font-weight: 600;
1033
+ color: var(--text-primary);
1034
+ overflow: hidden;
1035
+ text-overflow: ellipsis;
1036
+ white-space: nowrap;
1037
+ }
1038
+ .device-row-head .device-row-chips {
1039
+ display: inline-flex;
1040
+ gap: 6px;
1041
+ align-items: center;
1042
+ flex-wrap: wrap;
1043
+ }
1044
+ .device-row-head .device-row-meta {
1045
+ font-size: 12px;
1046
+ color: var(--text-tertiary);
1047
+ font-feature-settings: 'tnum' 1;
1048
+ white-space: nowrap;
1049
+ }
1050
+
1051
+ /* Drawer content region revealed when the row is expanded.
1052
+ Keeps the existing .peer-actions / .peer-scope styling intact —
1053
+ this is just the outer wrapper + separator line. */
1054
+ .device-row-drawer {
1055
+ padding: var(--sp-3) var(--sp-4) var(--sp-4);
1056
+ border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
1057
+ display: grid;
1058
+ gap: var(--sp-3);
1059
+ }
1060
+
1061
+ /* Device-row skeleton (shown during the initial sync load). Shape
1062
+ matches the real row so layout doesn't shift when data arrives.
1063
+ See docs/plans/2026-04-23-sync-tab-redesign.md (loading-states). */
1064
+ .device-row-skeleton-list {
1065
+ display: grid;
1066
+ gap: var(--sp-2);
1067
+ }
1068
+ .device-row-skeleton {
1069
+ display: flex;
1070
+ align-items: center;
1071
+ gap: var(--sp-3);
1072
+ min-height: 56px;
1073
+ padding: 10px var(--sp-3);
1074
+ border-radius: var(--radius-lg);
1075
+ border: 1px solid var(--border);
1076
+ background: var(--surface-1);
1077
+ }
1078
+ .device-row-skeleton .skel-pip,
1079
+ .device-row-skeleton .skel-name,
1080
+ .device-row-skeleton .skel-chip {
1081
+ display: inline-block;
1082
+ border-radius: var(--radius-sm);
1083
+ background: linear-gradient(90deg,
1084
+ color-mix(in srgb, var(--surface-2) 82%, var(--surface-1)) 25%,
1085
+ color-mix(in srgb, var(--surface-2) 60%, var(--surface-1)) 50%,
1086
+ color-mix(in srgb, var(--surface-2) 82%, var(--surface-1)) 75%);
1087
+ background-size: 200% 100%;
1088
+ animation: device-skel-shimmer 1.5s ease-in-out infinite;
1089
+ }
1090
+ .device-row-skeleton .skel-pip {
1091
+ width: 8px;
1092
+ height: 8px;
1093
+ border-radius: 50%;
1094
+ }
1095
+ .device-row-skeleton .skel-name {
1096
+ flex: 1;
1097
+ max-width: 180px;
1098
+ height: 14px;
1099
+ }
1100
+ .device-row-skeleton .skel-chip {
1101
+ width: 80px;
1102
+ height: 18px;
1103
+ border-radius: var(--radius-pill);
1104
+ }
1105
+ @keyframes device-skel-shimmer {
1106
+ 0% { background-position: 200% 0; }
1107
+ 100% { background-position: -200% 0; }
1108
+ }
1109
+ @media (prefers-reduced-motion: reduce) {
1110
+ .device-row-skeleton .skel-pip,
1111
+ .device-row-skeleton .skel-name,
1112
+ .device-row-skeleton .skel-chip { animation: none; }
1113
+ }
1114
+
863
1115
  .sync-redact-control {
864
1116
  display: inline-flex;
865
1117
  align-items: center;
@@ -968,12 +1220,14 @@
968
1220
 
969
1221
  .feed-controls {
970
1222
  display: flex;
971
- align-items: center;
972
- justify-content: space-between;
973
- gap: var(--sp-3);
974
- flex-wrap: wrap;
1223
+ flex-direction: column;
1224
+ align-items: stretch;
1225
+ gap: var(--sp-2);
975
1226
  margin-bottom: var(--sp-4);
976
1227
  }
1228
+ .feed-controls > .section-meta {
1229
+ margin-bottom: 0;
1230
+ }
977
1231
 
978
1232
  .feed-controls-right {
979
1233
  display: flex;
@@ -1573,7 +1827,9 @@
1573
1827
  border-radius: var(--radius-sm);
1574
1828
  background: var(--surface-2);
1575
1829
  }
1576
- .sync-action + .sync-action { margin-top: var(--sp-2); }
1830
+ /* General sibling, not adjacent: the invite/join panels sit in empty
1831
+ portal <div>s between action rows and the `+` selector would miss. */
1832
+ .sync-action ~ .sync-action { margin-top: var(--sp-2); }
1577
1833
 
1578
1834
  .health-action-text, .sync-action-text { font-size: 13px; color: var(--text-primary); flex: 1; min-width: 0; }
1579
1835
  .sync-section-heading {
@@ -1632,11 +1888,26 @@
1632
1888
 
1633
1889
  /* ── Peers ─────────────────────────────────────────────── */
1634
1890
 
1635
- .peer-list { display: grid; grid-template-columns: 1fr; gap: var(--sp-3); }
1636
- @media (min-width: 900px) {
1637
- .peer-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1891
+ /* Device list: full-width rows so the row head reads as a single
1892
+ line and the expanded drawer uses the full card width. The old
1893
+ 2-column grid above 900px forced expansion into a narrow sidebar. */
1894
+ .peer-list { display: grid; grid-template-columns: 1fr; gap: var(--sp-2); }
1895
+ .peer-card {
1896
+ border: 1px solid var(--border);
1897
+ border-radius: var(--radius-lg);
1898
+ background: var(--surface-1);
1899
+ padding: 0;
1900
+ display: block;
1901
+ overflow: hidden;
1902
+ }
1903
+ /* Padded variant for surfaces that reuse .peer-card as a generic
1904
+ content card (e.g. coordinator-admin group cards). Device rows
1905
+ keep padding:0 because their row-head owns its own interior. */
1906
+ .peer-card.peer-card--padded {
1907
+ padding: var(--sp-3);
1908
+ display: grid;
1909
+ gap: var(--sp-2);
1638
1910
  }
1639
- .peer-card { border-radius: var(--radius-lg); padding: var(--sp-4); display: grid; gap: var(--sp-2); }
1640
1911
  .peer-title { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); flex-wrap: wrap; }
1641
1912
  .peer-title strong { font-size: 14px; font-weight: 600; }
1642
1913
  /* Direction glyph rendered when a peer is in mutual-trust state.
@@ -1668,9 +1939,189 @@
1668
1939
  .peer-actor-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
1669
1940
  .peer-scope-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
1670
1941
  .peer-scope-editor { display: grid; gap: 8px; }
1671
- .peer-scope-chips { display: flex; flex-wrap: wrap; gap: 6px; min-height: 32px; align-items: flex-start; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); }
1942
+ .peer-scope-chips { display: flex; flex-wrap: wrap; gap: 6px; min-height: 32px; align-items: flex-start; padding: 8px 10px; margin: 0; list-style: none; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); }
1672
1943
  .peer-scope-chip { padding: 4px 8px; background: rgba(43, 122, 176, 0.12); color: var(--accent); font-size: 12px; }
1673
1944
  .peer-scope-chip.empty { background: transparent; color: var(--text-tertiary); border-style: dashed; border-color: var(--border); }
1945
+
1946
+ /* ProjectScopePicker — selected chips on top, an "Add project"
1947
+ button below that opens a Radix Popover with a searchable list.
1948
+ Typing an unmatched name turns the last row into
1949
+ `+ Create "foo"` so free-text entry folds into the same
1950
+ surface. Used by coordinator-admin scope defaults and Sync
1951
+ peer-scope drawers. */
1952
+ .project-scope-picker {
1953
+ display: grid;
1954
+ gap: var(--sp-2);
1955
+ position: relative;
1956
+ }
1957
+ .project-scope-picker-selected {
1958
+ display: flex;
1959
+ flex-wrap: wrap;
1960
+ gap: 6px;
1961
+ padding: 8px 10px;
1962
+ margin: 0;
1963
+ min-height: 36px;
1964
+ list-style: none;
1965
+ border: 1px solid var(--border);
1966
+ border-radius: var(--radius-sm);
1967
+ background: var(--surface-2);
1968
+ align-items: center;
1969
+ }
1970
+ .project-scope-picker-selected-empty {
1971
+ font-size: 12px;
1972
+ color: var(--text-tertiary);
1973
+ }
1974
+ .project-scope-picker-selected-chip {
1975
+ display: inline-flex;
1976
+ align-items: center;
1977
+ gap: 6px;
1978
+ padding: 4px 4px 4px 10px;
1979
+ background: rgba(43, 122, 176, 0.16);
1980
+ border: 1px solid color-mix(in srgb, var(--accent) 48%, var(--border));
1981
+ border-radius: var(--radius-pill);
1982
+ color: var(--accent);
1983
+ font-size: 12px;
1984
+ }
1985
+ .project-scope-picker-selected-remove {
1986
+ appearance: none;
1987
+ border: none;
1988
+ background: transparent;
1989
+ color: inherit;
1990
+ cursor: pointer;
1991
+ padding: 0 4px;
1992
+ font: inherit;
1993
+ font-size: 14px;
1994
+ line-height: 1;
1995
+ opacity: 0.7;
1996
+ }
1997
+ .project-scope-picker-selected-remove:hover { opacity: 1; }
1998
+ .project-scope-picker-trigger {
1999
+ justify-self: start;
2000
+ display: inline-flex;
2001
+ align-items: center;
2002
+ gap: 6px;
2003
+ padding: 6px 12px;
2004
+ font-size: 12px;
2005
+ }
2006
+ .project-scope-picker-popover {
2007
+ width: min(360px, 100%);
2008
+ max-height: 280px;
2009
+ display: grid;
2010
+ grid-template-rows: auto 1fr;
2011
+ gap: 4px;
2012
+ padding: 6px;
2013
+ background: var(--surface-1);
2014
+ border: 1px solid var(--border);
2015
+ border-radius: var(--radius-md);
2016
+ box-shadow: var(--shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));
2017
+ z-index: 50;
2018
+ }
2019
+ /* Vanilla-DOM popover (peer-scope) anchors inline under its trigger;
2020
+ the Preact ToggleGroup version portals via Radix. Both share the
2021
+ rest of the styling. */
2022
+ .project-scope-picker-popover--inline {
2023
+ position: absolute;
2024
+ left: 0;
2025
+ right: auto;
2026
+ top: 100%;
2027
+ margin-top: 6px;
2028
+ }
2029
+ .project-scope-picker-search {
2030
+ appearance: none;
2031
+ width: 100%;
2032
+ border: 1px solid var(--border);
2033
+ border-radius: var(--radius-sm);
2034
+ background: var(--surface-2);
2035
+ color: var(--text-primary);
2036
+ padding: 6px 10px;
2037
+ font: inherit;
2038
+ font-size: 13px;
2039
+ }
2040
+ .project-scope-picker-search:focus {
2041
+ outline: 2px solid var(--accent);
2042
+ outline-offset: 1px;
2043
+ }
2044
+ .project-scope-picker-results {
2045
+ margin: 0;
2046
+ padding: 4px 0;
2047
+ list-style: none;
2048
+ max-height: 220px;
2049
+ overflow-y: auto;
2050
+ display: grid;
2051
+ gap: 2px;
2052
+ }
2053
+ .project-scope-picker-empty-row {
2054
+ padding: 8px 10px;
2055
+ font-size: 12px;
2056
+ color: var(--text-tertiary);
2057
+ }
2058
+ .project-scope-picker-row {
2059
+ appearance: none;
2060
+ border: none;
2061
+ background: transparent;
2062
+ width: 100%;
2063
+ text-align: left;
2064
+ font: inherit;
2065
+ display: grid;
2066
+ grid-template-columns: 16px 1fr;
2067
+ gap: 6px;
2068
+ align-items: center;
2069
+ padding: 6px 10px;
2070
+ border-radius: var(--radius-sm);
2071
+ cursor: pointer;
2072
+ color: var(--text-secondary);
2073
+ font-size: 13px;
2074
+ }
2075
+ .project-scope-picker-row:focus-visible {
2076
+ outline: 2px solid var(--accent);
2077
+ outline-offset: -2px;
2078
+ }
2079
+ .project-scope-picker-row--active {
2080
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
2081
+ color: var(--text-primary);
2082
+ }
2083
+ .project-scope-picker-row[aria-selected="true"] {
2084
+ color: var(--accent);
2085
+ }
2086
+ .project-scope-picker-row-check {
2087
+ display: inline-block;
2088
+ width: 16px;
2089
+ text-align: center;
2090
+ font-size: 12px;
2091
+ color: var(--accent);
2092
+ }
2093
+ .project-scope-picker-row--create .project-scope-picker-row-check {
2094
+ color: var(--text-tertiary);
2095
+ }
2096
+ /* PresencePip — atomic health indicator.
2097
+ See docs/plans/2026-04-23-sync-tab-redesign.md (loading-states). */
2098
+ .presence-pip {
2099
+ display: inline-block;
2100
+ width: var(--presence-pip-size, 8px);
2101
+ height: var(--presence-pip-size, 8px);
2102
+ border-radius: 50%;
2103
+ flex-shrink: 0;
2104
+ vertical-align: middle;
2105
+ }
2106
+ .presence-pip--online { background: var(--accent); }
2107
+ .presence-pip--offline { background: var(--accent-warm, var(--accent)); }
2108
+ .presence-pip--degraded { background: var(--accent-warm, var(--accent)); }
2109
+ .presence-pip--attention { background: var(--accent-warm-strong, var(--accent-warm, var(--accent))); animation: pip-attention 2.4s ease-in-out infinite; }
2110
+ .presence-pip--syncing { background: var(--accent); animation: pip-syncing 1.2s ease-in-out infinite; }
2111
+ .presence-pip--unknown { background: var(--text-tertiary); opacity: 0.6; }
2112
+
2113
+ @keyframes pip-attention {
2114
+ 0%, 100% { opacity: 0.6; transform: scale(1); }
2115
+ 50% { opacity: 1; transform: scale(1.15); }
2116
+ }
2117
+ @keyframes pip-syncing {
2118
+ 0%, 100% { opacity: 0.55; }
2119
+ 50% { opacity: 1; }
2120
+ }
2121
+ @media (prefers-reduced-motion: reduce) {
2122
+ .presence-pip--attention,
2123
+ .presence-pip--syncing { animation: none; opacity: 1; transform: none; }
2124
+ }
1674
2125
  .peer-scope-chip-remove { border: none; background: transparent; color: inherit; cursor: pointer; padding: 0; font: inherit; opacity: 0.75; }
1675
2126
  .peer-scope-chip-remove:hover { opacity: 1; }
1676
2127
  .peer-scope-input { width: 100%; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); color: var(--text-primary); padding: 8px 10px; font: inherit; }
@@ -2581,7 +3032,8 @@
2581
3032
  align-items: center;
2582
3033
  }
2583
3034
  .sync-dialog-input {
2584
- width: 100%;
3035
+ width: calc(100% - 6px);
3036
+ margin: 3px;
2585
3037
  padding: var(--sp-2) var(--sp-3);
2586
3038
  border-radius: var(--radius-sm);
2587
3039
  border: 1px solid var(--border);
@@ -2589,11 +3041,44 @@
2589
3041
  color: var(--text-primary);
2590
3042
  font-family: var(--font-sans);
2591
3043
  font-size: 13px;
3044
+ box-sizing: border-box;
2592
3045
  }
2593
3046
  .sync-dialog-input:focus {
2594
- outline: none;
2595
3047
  border-color: var(--accent);
2596
- box-shadow: 0 0 0 3px var(--focus-ring);
3048
+ outline: 2px solid var(--focus-ring);
3049
+ outline-offset: 1px;
3050
+ }
3051
+ .autocomplete-popover {
3052
+ background: var(--surface-1, var(--bg-elevated));
3053
+ border: 1px solid var(--border);
3054
+ border-radius: var(--radius-sm);
3055
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
3056
+ max-height: 260px;
3057
+ overflow-y: auto;
3058
+ padding: 4px;
3059
+ z-index: 120;
3060
+ }
3061
+ .autocomplete-listbox {
3062
+ display: flex;
3063
+ flex-direction: column;
3064
+ list-style: none;
3065
+ margin: 0;
3066
+ padding: 0;
3067
+ }
3068
+ .autocomplete-option {
3069
+ padding: 6px 10px;
3070
+ border-radius: var(--radius-sm);
3071
+ color: var(--text-primary);
3072
+ font-family: var(--font-sans);
3073
+ font-size: 13px;
3074
+ cursor: pointer;
3075
+ user-select: none;
3076
+ }
3077
+ .autocomplete-option:hover,
3078
+ .autocomplete-option.active,
3079
+ .autocomplete-option[aria-selected="true"] {
3080
+ background: var(--surface-2, rgba(255, 255, 255, 0.06));
3081
+ color: var(--accent);
2597
3082
  }
2598
3083
  .sync-dialog-confirm.danger {
2599
3084
  background: rgba(182, 75, 47, 0.12);
@@ -3024,6 +3509,10 @@
3024
3509
  <!-- ── Tab: Sync ───────────────────────────────────────── -->
3025
3510
  <div class="tab-panel" id="tab-sync" hidden>
3026
3511
 
3512
+ <!-- Sub-view: main Sync surface. Toggled by syncViewController
3513
+ based on #sync vs #sync/diagnostics in the URL hash. -->
3514
+ <div id="syncMainView">
3515
+
3027
3516
  <!-- 1. Needs attention + team setup/status -->
3028
3517
  <div class="card" id="syncTeamCard">
3029
3518
  <div class="section-header">
@@ -3033,6 +3522,7 @@
3033
3522
  </div>
3034
3523
  <div class="section-actions">
3035
3524
  <button class="settings-button" id="syncNowButton">Sync now</button>
3525
+ <a class="sync-subview-link" href="#sync/diagnostics">View diagnostics &rarr;</a>
3036
3526
  </div>
3037
3527
  </div>
3038
3528
  <div class="section-meta" id="syncTeamMeta">Start with the next step below, then review team status and device details.</div>
@@ -3047,22 +3537,20 @@
3047
3537
  <h3 class="settings-group-title">Join a team</h3>
3048
3538
  <div class="section-meta">Have an invite? Paste it here.</div>
3049
3539
  <div class="actor-create-row" id="syncJoinPanel">
3050
- <label for="syncJoinInvite" class="sr-only">Team invite</label>
3051
- <textarea class="feed-search" id="syncJoinInvite" placeholder="Paste team invite or codemem:// link"></textarea>
3052
- <button class="settings-button" id="syncJoinButton">Join team</button>
3540
+ <label for="syncJoinInvite" class="sr-only">Team invite or pairing payload</label>
3541
+ <textarea class="feed-search" id="syncJoinInvite" placeholder="Paste a team invite or device pairing payload"></textarea>
3542
+ <button class="settings-button" id="syncJoinButton">Accept</button>
3053
3543
  </div>
3054
3544
  </div>
3055
3545
  <div id="syncAdminSection">
3056
3546
  <div id="syncAdminDisclosureMount" style="display: contents"></div>
3057
3547
  </div>
3058
3548
  </div>
3059
- <h3 class="settings-group-title" style="margin-top: 20px">Team status</h3>
3060
- <div class="actor-list" id="syncTeamStatus" aria-live="polite"></div>
3061
3549
  <div class="actor-list" id="syncJoinRequests"></div>
3062
3550
  <div id="syncCoordinatorDiscovered" hidden>
3063
- <h3 class="settings-group-title" style="margin-top: 20px">Devices seen on team</h3>
3551
+ <h3 class="settings-group-title" style="margin-top: 20px">Team devices to pair</h3>
3064
3552
  <div class="section-meta" id="syncCoordinatorDiscoveredMeta"></div>
3065
- <div class="actor-list" id="syncCoordinatorDiscoveredList"></div>
3553
+ <div class="peer-list" id="syncCoordinatorDiscoveredList"></div>
3066
3554
  </div>
3067
3555
  </div>
3068
3556
 
@@ -3087,11 +3575,19 @@
3087
3575
 
3088
3576
  <h3 class="settings-group-title" style="margin-top: 20px">Devices</h3>
3089
3577
  <div class="peer-list" id="syncPeers"></div>
3090
- <div class="sync-skeleton" id="syncPeersSkeleton" role="status" aria-label="Loading devices">
3091
- <div class="sync-skeleton-block"></div>
3092
- <div class="sync-skeleton-block"></div>
3578
+ <div class="device-row-skeleton-list" id="syncPeersSkeleton" role="status" aria-label="Loading devices" aria-busy="true">
3579
+ <div class="device-row-skeleton"><span class="skel-pip"></span><span class="skel-name"></span><span class="skel-chip"></span></div>
3580
+ <div class="device-row-skeleton"><span class="skel-pip"></span><span class="skel-name"></span><span class="skel-chip"></span></div>
3581
+ <div class="device-row-skeleton"><span class="skel-pip"></span><span class="skel-name"></span><span class="skel-chip"></span></div>
3093
3582
  </div>
3094
3583
 
3584
+ <h3 class="settings-group-title" style="margin-top: 20px">Connect another device</h3>
3585
+ <div class="section-meta">Share this command on the new device to join it to the team.</div>
3586
+ <div class="actor-create-row">
3587
+ <div id="syncPairingDisclosureMount" style="display: contents"></div>
3588
+ </div>
3589
+ <div id="syncPairingPanelMount" style="display: contents"></div>
3590
+
3095
3591
  <div id="syncSharingReview" hidden>
3096
3592
  <h3 class="settings-group-title" style="margin-top: 20px">What teammates will receive</h3>
3097
3593
  <div class="section-meta" id="syncSharingReviewMeta"></div>
@@ -3109,19 +3605,25 @@
3109
3605
  </div>
3110
3606
  </div>
3111
3607
 
3112
- <!-- 3. Advanced diagnostics — status, pairing, attempts -->
3608
+ </div><!-- /#syncMainView -->
3609
+
3610
+ <!-- Sub-view: Advanced diagnostics. Reached via #sync/diagnostics;
3611
+ shown by syncViewController when that hash is active. -->
3612
+ <div id="syncDiagnosticsView" hidden>
3113
3613
  <div class="card" style="animation-delay: 0.06s">
3114
3614
  <div class="section-header">
3115
- <h2>Advanced diagnostics</h2>
3615
+ <div class="section-header-title">
3616
+ <a class="sync-subview-link" href="#sync">&larr; Back to sync</a>
3617
+ <h2>Sync diagnostics</h2>
3618
+ </div>
3116
3619
  <div class="section-actions">
3117
- <div id="syncPairingDisclosureMount" style="display: contents"></div>
3118
3620
  <label class="small sync-redact-control">
3119
3621
  <span id="syncRedactMount"></span>
3120
3622
  <span id="syncRedactLabel">Redact</span>
3121
3623
  </label>
3122
3624
  </div>
3123
3625
  </div>
3124
- <div class="section-meta" id="syncMeta">Use this section for sync debugging, pairing, and recent attempt details.</div>
3626
+ <div class="section-meta" id="syncMeta">Use this surface for sync debugging and recent attempt details.</div>
3125
3627
  <div class="sync-skeleton" id="syncDiagSkeleton" role="status" aria-label="Loading diagnostics">
3126
3628
  <div class="sync-skeleton-line long"></div>
3127
3629
  <div class="sync-skeleton-line medium"></div>
@@ -3130,11 +3632,10 @@
3130
3632
  <div class="sync-actions" id="syncActions"></div>
3131
3633
  <div class="grid-2" id="syncStatusGrid"></div>
3132
3634
 
3133
- <div id="syncPairingPanelMount" style="display: contents"></div>
3134
-
3135
- <h3 class="settings-group-title" style="margin-top: 20px">Recent sync attempts (advanced)</h3>
3635
+ <h3 class="settings-group-title" style="margin-top: 20px">Recent sync attempts</h3>
3136
3636
  <div class="attempts-list" id="syncAttempts"></div>
3137
3637
  </div>
3638
+ </div><!-- /#syncDiagnosticsView -->
3138
3639
  </div>
3139
3640
 
3140
3641
  <div class="tab-panel" id="tab-coordinator-admin" hidden>