@codemem/server 0.28.1 → 0.29.1

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;
@@ -1575,7 +1827,9 @@
1575
1827
  border-radius: var(--radius-sm);
1576
1828
  background: var(--surface-2);
1577
1829
  }
1578
- .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); }
1579
1833
 
1580
1834
  .health-action-text, .sync-action-text { font-size: 13px; color: var(--text-primary); flex: 1; min-width: 0; }
1581
1835
  .sync-section-heading {
@@ -1634,11 +1888,26 @@
1634
1888
 
1635
1889
  /* ── Peers ─────────────────────────────────────────────── */
1636
1890
 
1637
- .peer-list { display: grid; grid-template-columns: 1fr; gap: var(--sp-3); }
1638
- @media (min-width: 900px) {
1639
- .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);
1640
1910
  }
1641
- .peer-card { border-radius: var(--radius-lg); padding: var(--sp-4); display: grid; gap: var(--sp-2); }
1642
1911
  .peer-title { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); flex-wrap: wrap; }
1643
1912
  .peer-title strong { font-size: 14px; font-weight: 600; }
1644
1913
  /* Direction glyph rendered when a peer is in mutual-trust state.
@@ -1670,9 +1939,189 @@
1670
1939
  .peer-actor-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
1671
1940
  .peer-scope-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
1672
1941
  .peer-scope-editor { display: grid; gap: 8px; }
1673
- .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); }
1674
1943
  .peer-scope-chip { padding: 4px 8px; background: rgba(43, 122, 176, 0.12); color: var(--accent); font-size: 12px; }
1675
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
+ }
1676
2125
  .peer-scope-chip-remove { border: none; background: transparent; color: inherit; cursor: pointer; padding: 0; font: inherit; opacity: 0.75; }
1677
2126
  .peer-scope-chip-remove:hover { opacity: 1; }
1678
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; }
@@ -2583,7 +3032,8 @@
2583
3032
  align-items: center;
2584
3033
  }
2585
3034
  .sync-dialog-input {
2586
- width: 100%;
3035
+ width: calc(100% - 6px);
3036
+ margin: 3px;
2587
3037
  padding: var(--sp-2) var(--sp-3);
2588
3038
  border-radius: var(--radius-sm);
2589
3039
  border: 1px solid var(--border);
@@ -2591,11 +3041,44 @@
2591
3041
  color: var(--text-primary);
2592
3042
  font-family: var(--font-sans);
2593
3043
  font-size: 13px;
3044
+ box-sizing: border-box;
2594
3045
  }
2595
3046
  .sync-dialog-input:focus {
2596
- outline: none;
2597
3047
  border-color: var(--accent);
2598
- 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);
2599
3082
  }
2600
3083
  .sync-dialog-confirm.danger {
2601
3084
  background: rgba(182, 75, 47, 0.12);
@@ -3026,6 +3509,10 @@
3026
3509
  <!-- ── Tab: Sync ───────────────────────────────────────── -->
3027
3510
  <div class="tab-panel" id="tab-sync" hidden>
3028
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
+
3029
3516
  <!-- 1. Needs attention + team setup/status -->
3030
3517
  <div class="card" id="syncTeamCard">
3031
3518
  <div class="section-header">
@@ -3035,6 +3522,7 @@
3035
3522
  </div>
3036
3523
  <div class="section-actions">
3037
3524
  <button class="settings-button" id="syncNowButton">Sync now</button>
3525
+ <a class="sync-subview-link" href="#sync/diagnostics">View diagnostics &rarr;</a>
3038
3526
  </div>
3039
3527
  </div>
3040
3528
  <div class="section-meta" id="syncTeamMeta">Start with the next step below, then review team status and device details.</div>
@@ -3049,22 +3537,20 @@
3049
3537
  <h3 class="settings-group-title">Join a team</h3>
3050
3538
  <div class="section-meta">Have an invite? Paste it here.</div>
3051
3539
  <div class="actor-create-row" id="syncJoinPanel">
3052
- <label for="syncJoinInvite" class="sr-only">Team invite</label>
3053
- <textarea class="feed-search" id="syncJoinInvite" placeholder="Paste team invite or codemem:// link"></textarea>
3054
- <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>
3055
3543
  </div>
3056
3544
  </div>
3057
3545
  <div id="syncAdminSection">
3058
3546
  <div id="syncAdminDisclosureMount" style="display: contents"></div>
3059
3547
  </div>
3060
3548
  </div>
3061
- <h3 class="settings-group-title" style="margin-top: 20px">Team status</h3>
3062
- <div class="actor-list" id="syncTeamStatus" aria-live="polite"></div>
3063
3549
  <div class="actor-list" id="syncJoinRequests"></div>
3064
3550
  <div id="syncCoordinatorDiscovered" hidden>
3065
- <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>
3066
3552
  <div class="section-meta" id="syncCoordinatorDiscoveredMeta"></div>
3067
- <div class="actor-list" id="syncCoordinatorDiscoveredList"></div>
3553
+ <div class="peer-list" id="syncCoordinatorDiscoveredList"></div>
3068
3554
  </div>
3069
3555
  </div>
3070
3556
 
@@ -3089,9 +3575,10 @@
3089
3575
 
3090
3576
  <h3 class="settings-group-title" style="margin-top: 20px">Devices</h3>
3091
3577
  <div class="peer-list" id="syncPeers"></div>
3092
- <div class="sync-skeleton" id="syncPeersSkeleton" role="status" aria-label="Loading devices">
3093
- <div class="sync-skeleton-block"></div>
3094
- <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>
3095
3582
  </div>
3096
3583
 
3097
3584
  <h3 class="settings-group-title" style="margin-top: 20px">Connect another device</h3>
@@ -3118,10 +3605,17 @@
3118
3605
  </div>
3119
3606
  </div>
3120
3607
 
3121
- <!-- 3. Advanced diagnostics — status, 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>
3122
3613
  <div class="card" style="animation-delay: 0.06s">
3123
3614
  <div class="section-header">
3124
- <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>
3125
3619
  <div class="section-actions">
3126
3620
  <label class="small sync-redact-control">
3127
3621
  <span id="syncRedactMount"></span>
@@ -3129,7 +3623,7 @@
3129
3623
  </label>
3130
3624
  </div>
3131
3625
  </div>
3132
- <div class="section-meta" id="syncMeta">Use this section for sync debugging and recent attempt details.</div>
3626
+ <div class="section-meta" id="syncMeta">Use this surface for sync debugging and recent attempt details.</div>
3133
3627
  <div class="sync-skeleton" id="syncDiagSkeleton" role="status" aria-label="Loading diagnostics">
3134
3628
  <div class="sync-skeleton-line long"></div>
3135
3629
  <div class="sync-skeleton-line medium"></div>
@@ -3138,9 +3632,10 @@
3138
3632
  <div class="sync-actions" id="syncActions"></div>
3139
3633
  <div class="grid-2" id="syncStatusGrid"></div>
3140
3634
 
3141
- <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>
3142
3636
  <div class="attempts-list" id="syncAttempts"></div>
3143
3637
  </div>
3638
+ </div><!-- /#syncDiagnosticsView -->
3144
3639
  </div>
3145
3640
 
3146
3641
  <div class="tab-panel" id="tab-coordinator-admin" hidden>