fuckin around with styling

Orual d3731e20 0fb1e75d

+949 -225
+74 -16
crates/weaver-app/assets/styling/entry-card.css
··· 1 1 /* Entry card styling */ 2 2 3 - .entries-list { 4 - max-width: 800px; 3 + /* Notebook layout - sidebar in left gutter on desktop, header on mobile */ 4 + .notebook-layout { 5 + display: grid; 6 + grid-template-columns: minmax(280px, 1fr) minmax(0, 90ch) minmax(280px, 1fr); 7 + gap: 2rem; 8 + max-width: calc(90ch + 560px + 4rem); /* content + gutters + gaps */ 5 9 margin: 0 auto; 6 - padding: 2rem 1rem; 10 + padding: 2.5rem 1.25rem 2.5rem 0; 11 + } 12 + 13 + .notebook-sidebar { 14 + grid-column: 1; 15 + position: sticky; 16 + top: 2rem; 17 + align-self: flex-start; 18 + max-height: calc(100vh - 4rem); 19 + overflow-y: auto; 20 + } 21 + 22 + .notebook-main { 23 + grid-column: 2; 24 + padding: 0 1rem; 25 + } 26 + 27 + /* Mobile layout - sidebar becomes header */ 28 + @media (max-width: 1400px) { 29 + .notebook-layout { 30 + grid-template-columns: 1fr !important; 31 + gap: 0 !important; 32 + max-width: 100vw !important; 33 + box-sizing: border-box !important; 34 + } 35 + 36 + .notebook-sidebar { 37 + grid-column: 1; 38 + position: static; 39 + max-height: none; 40 + min-width: 0; 41 + } 42 + 43 + .notebook-main { 44 + grid-column: 1; 45 + padding: 1.25rem; 46 + min-width: 0; 47 + } 48 + } 49 + 50 + /* Entries list - width constrained by grid column */ 51 + .entries-list { 7 52 } 8 53 9 54 .entry-card { ··· 13 58 .entry-card-link { 14 59 display: block; 15 60 background: var(--color-surface); 16 - border: 1px solid var(--color-border); 17 - border-radius: 6px; 61 + box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 6%, transparent); 62 + border-left: 2px solid transparent; 18 63 padding: 1.25rem; 19 64 text-decoration: none; 20 65 color: var(--color-text); 21 - transition: all 0.2s ease; 66 + transition: 67 + box-shadow 0.2s ease, 68 + border-color 0.2s ease; 22 69 } 23 70 24 71 .entry-card-link:hover { 25 - background: var(--color-overlay); 26 - border-color: var(--color-secondary); 27 - box-shadow: 0 2px 6px color-mix(in srgb, var(--color-secondary) 8%, transparent); 28 - transform: translateY(-1px); 72 + box-shadow: 0 2px 4px color-mix(in srgb, var(--color-text) 10%, transparent); 73 + border-left-color: var(--color-secondary); 74 + } 75 + 76 + .entry-card-link:hover .entry-card-title { 77 + color: var(--color-secondary); 29 78 } 30 79 31 80 .entry-card-header { ··· 38 87 color: var(--color-primary); 39 88 margin: 0; 40 89 font-family: var(--font-heading); 90 + transition: color 0.2s ease; 41 91 } 42 92 43 93 .entry-card-meta { ··· 73 123 } 74 124 75 125 .entry-card-tag { 76 - padding: 0.2rem 0.5rem; 77 - background: var(--color-base); 78 - border: 1px solid var(--color-border); 79 - border-radius: 3px; 126 + padding: 0.2rem 0.4rem 0.2rem 0; 80 127 font-size: 0.75rem; 81 128 color: var(--color-subtle); 82 - transition: all 0.15s ease; 129 + border-bottom: 1px solid var(--color-border); 130 + transition: 131 + color 0.15s ease, 132 + border-color 0.15s ease; 83 133 } 84 134 85 135 .entry-card-link:hover .entry-card-tag { 86 - background: var(--color-surface); 136 + color: var(--color-tertiary); 87 137 border-color: var(--color-tertiary); 88 138 } 89 139 ··· 128 178 .entry-card-preview code { 129 179 font-size: 0.8rem; 130 180 } 181 + 182 + /* Dark mode: replace shadows with borders */ 183 + @media (prefers-color-scheme: dark) { 184 + .entry-card-link { 185 + box-shadow: none; 186 + border: 1px solid var(--color-border); 187 + } 188 + }
+34 -34
crates/weaver-app/assets/styling/entry.css
··· 1 1 /* Entry page layout with gutter navigation */ 2 2 .entry-page-layout { 3 3 display: grid; 4 - grid-template-columns: minmax(0, 1fr) minmax(0, 90ch) minmax(0, 1fr); 5 - gap: 0; 4 + grid-template-columns: minmax(200px, 1fr) minmax(0, 90ch) minmax(200px, 1fr); 5 + gap: 2rem; 6 6 width: 100%; 7 7 min-height: 100vh; 8 8 background: var(--color-base); 9 + max-width: calc(90ch + 400px + 4rem); /* content + gutters + gaps */ 10 + margin: 0 auto; 11 + padding: 0 1rem 0 0; 9 12 } 10 13 11 14 /* Main content area */ ··· 18 21 .nav-gutter { 19 22 position: sticky; 20 23 top: auto; 21 - bottom: calc(2rem * var(--spacing-scale, 1.5)); 24 + bottom: 2rem; 22 25 height: fit-content; 23 26 align-self: end; 24 27 } 25 28 26 29 .nav-prev { 27 30 grid-column: 1; 28 - padding-left: calc(1rem * var(--spacing-scale, 1.5)); 29 31 } 30 32 31 33 .nav-next { 32 34 grid-column: 3; 33 - padding-right: calc(1rem * var(--spacing-scale, 1.5)); 34 35 } 35 36 36 37 /* Navigation buttons */ 37 38 .nav-button { 38 39 display: flex; 39 40 flex-direction: column; 40 - gap: calc(0.5rem * var(--spacing-scale, 1.5)); 41 - padding: calc(1rem * var(--spacing-scale, 1.5)); 41 + gap: 0.5rem; 42 + padding: 1rem; 42 43 background: var(--color-surface); 43 - border: 2px solid var(--color-border); 44 - border-radius: 4px; 44 + box-shadow: 0 1px 3px color-mix(in srgb, var(--color-text) 8%, transparent); 45 45 text-decoration: none; 46 46 color: var(--color-text); 47 - transition: all 0.2s ease; 47 + transition: 48 + box-shadow 0.2s ease, 49 + border-color 0.2s ease; 48 50 } 49 51 50 52 .nav-button:hover { 51 - background: var(--color-overlay); 52 - border-color: var(--color-primary); 53 - box-shadow: 0 2px 8px color-mix(in srgb, var(--color-primary) 20%, transparent); 53 + box-shadow: 0 2px 6px color-mix(in srgb, var(--color-text) 12%, transparent); 54 + } 55 + 56 + /* Dark mode: borders instead of shadows */ 57 + @media (prefers-color-scheme: dark) { 58 + .nav-button { 59 + box-shadow: none; 60 + border: 1px dashed var(--color-border); 61 + } 62 + 63 + .nav-button:hover { 64 + box-shadow: none; 65 + border-color: var(--color-primary); 66 + } 54 67 } 55 68 56 69 .nav-button-prev { ··· 74 87 color: var(--color-emphasis); 75 88 } 76 89 77 - .nav-label { 78 - font-size: 0.875rem; 79 - font-weight: 600; 80 - text-transform: uppercase; 81 - letter-spacing: 0.05em; 82 - color: var(--color-subtle); 83 - transition: color 0.2s ease; 84 - } 85 - 86 90 .nav-button:hover .nav-label { 87 91 color: var(--color-secondary); 88 92 } ··· 90 94 .nav-title { 91 95 font-size: 0.95rem; 92 96 font-weight: 500; 93 - max-width: 20ch; 94 - overflow: hidden; 95 - text-overflow: ellipsis; 96 - white-space: nowrap; 97 + line-height: 1.4; 97 98 } 98 99 99 100 /* Entry metadata header */ ··· 155 156 } 156 157 157 158 .entry-tag { 158 - padding: 0.25rem 0.75rem; 159 - background: var(--color-surface); 160 - border: 1px solid var(--color-border); 161 - border-radius: 3px; 159 + padding: 0.25rem 0.5rem 0.25rem 0; 162 160 font-size: 0.85rem; 163 161 color: var(--color-subtle); 162 + border-bottom: 1px solid var(--color-border); 164 163 text-decoration: none; 165 - transition: all 0.2s ease; 164 + transition: 165 + color 0.2s ease, 166 + border-color 0.2s ease; 166 167 } 167 168 168 169 .entry-tag:hover { 169 - background: var(--color-overlay); 170 + color: var(--color-tertiary); 170 171 border-color: var(--color-tertiary); 171 - color: var(--color-text); 172 172 } 173 173 174 174 /* Content styling */ ··· 204 204 } 205 205 206 206 /* Responsive layout */ 207 - @media (max-width: 1200px) { 207 + @media (max-width: 1400px) { 208 208 .entry-page-layout { 209 209 grid-template-columns: 1fr; 210 210 gap: 0;
+1 -35
crates/weaver-app/assets/styling/main.css
··· 5 5 margin: 20px; 6 6 } 7 7 8 - #hero { 9 - margin: 0; 10 - display: flex; 11 - flex-direction: column; 12 - justify-content: center; 13 - align-items: center; 14 - } 15 - 16 - #links { 17 - width: 400px; 18 - text-align: left; 19 - font-size: x-large; 20 - color: var(--color-text); 21 - display: flex; 22 - flex-direction: column; 23 - } 24 - 25 - #links a { 26 - color: var(--color-link); 27 - text-decoration: none; 28 - margin-top: 20px; 29 - margin: 10px 0px; 30 - border: 1px solid var(--color-border); 31 - border-radius: 5px; 32 - padding: 10px; 33 - transition: all 0.2s ease; 34 - } 35 - 36 - #links a:hover { 37 - background-color: var(--color-surface); 38 - border-color: var(--color-primary); 39 - cursor: pointer; 40 - } 41 - 42 8 #header { 43 9 max-width: 1200px; 44 - } 10 + }
+192 -38
crates/weaver-app/assets/styling/notebook-card.css
··· 1 1 /* Notebook card styling */ 2 2 3 - .notebooks-list { 4 - max-width: 800px; 3 + /* Repository layout - sidebar in left gutter on desktop, header on mobile */ 4 + 5 + .repository-layout { 6 + display: grid; 7 + grid-template-columns: minmax(240px, 1fr) minmax(0, 90ch) minmax(240px, 1fr); 8 + gap: 2rem; 9 + max-width: calc(90ch + 480px + 4rem); /* content + gutters + gaps */ 5 10 margin: 0 auto; 6 - padding: 2rem 1rem; 11 + padding: 2.25rem 1.25rem 2.25rem 0; 12 + } 13 + 14 + .repository-sidebar { 15 + grid-column: 1; 16 + position: sticky; 17 + top: 2rem; 18 + align-self: flex-start; 19 + overflow-y: auto; 20 + } 21 + 22 + .repository-main { 23 + grid-column: 2; 24 + padding: 0 1rem; 25 + } 26 + 27 + /* Mobile layout - sidebar becomes header */ 28 + @media (max-width: 1400px) { 29 + .repository-layout { 30 + grid-template-columns: 1fr !important; 31 + gap: 0 !important; 32 + max-width: 100vw !important; 33 + box-sizing: border-box !important; 34 + } 35 + 36 + .repository-sidebar { 37 + grid-column: 1; 38 + position: static; 39 + max-height: none; 40 + min-width: 0; 41 + margin-bottom: 2rem; 42 + } 43 + 44 + .repository-main { 45 + grid-column: 1; 46 + padding: 0; 47 + min-width: 0; 48 + } 49 + } 50 + 51 + /* Notebook list - width constrained by grid column */ 52 + .notebooks-list { 53 + margin-top: 0.25rem; 7 54 } 8 55 9 56 .notebook-card { 10 - margin-bottom: calc(1.5rem * var(--spacing-scale, 1.25)); 57 + margin-bottom: 2.5rem; /* 2 grid units */ 58 + } 59 + 60 + .notebook-card-container { 61 + background: var(--color-surface); 62 + box-shadow: 0 1px 3px color-mix(in srgb, var(--color-text) 8%, transparent); 63 + padding: 1.25rem; 11 64 } 12 65 13 - .notebook-card-link { 66 + .notebook-card-header-link { 14 67 display: block; 15 - background: var(--color-surface); 16 - border: 1px solid var(--color-border); 17 - border-radius: 8px; 18 - padding: 1.5rem; 19 68 text-decoration: none; 20 69 color: var(--color-text); 21 - transition: all 0.2s ease; 70 + border-left: 3px solid transparent; 71 + padding-left: 0.75rem; 72 + margin-left: -0.75rem; 73 + transition: border-color 0.2s ease; 74 + } 75 + 76 + .notebook-card-header-link:hover { 77 + border-left-color: var(--color-primary); 22 78 } 23 79 24 - .notebook-card-link:hover { 25 - background: var(--color-overlay); 26 - border-color: var(--color-primary); 80 + .notebook-card-header-link:hover .notebook-card-title { 81 + color: var(--color-secondary); 27 82 } 28 83 29 84 .notebook-card-header { 30 - margin-bottom: 1rem; 85 + margin-bottom: 1.25rem; 86 + padding-bottom: 1.25rem; 87 + border-bottom: 2px solid var(--color-border); 31 88 } 32 89 33 90 .notebook-card-title { ··· 36 93 color: var(--color-primary); 37 94 margin: 0 0 0.5rem 0; 38 95 font-family: var(--font-heading); 96 + transition: color 0.2s ease; 39 97 } 40 98 41 - .notebook-card-description { 99 + .notebook-card-date { 42 100 color: var(--color-muted); 43 - line-height: 1.5; 44 - margin: 0; 45 - display: -webkit-box; 46 - -webkit-line-clamp: 2; 47 - -webkit-box-orient: vertical; 48 - overflow: hidden; 101 + font-size: 0.85rem; 49 102 } 50 103 51 - .notebook-card-meta { 104 + .notebook-card-authors { 52 105 display: flex; 53 - align-items: center; 54 - gap: 1rem; 55 106 flex-wrap: wrap; 56 - margin-bottom: 0.75rem; 107 + gap: 0.5rem; 108 + margin-bottom: 1rem; 57 109 font-size: 0.9rem; 58 110 color: var(--color-subtle); 59 111 } 60 112 61 - .notebook-card-author { 113 + .author-separator { 114 + color: var(--color-muted); 115 + } 116 + 117 + /* Entry previews within notebook card */ 118 + .notebook-card-previews { 62 119 display: flex; 63 - align-items: center; 64 - gap: 0.5rem; 120 + flex-direction: column; 121 + margin-bottom: 0; /* Let card padding handle bottom spacing */ 65 122 } 66 123 67 - .notebook-card-author .author-name { 68 - font-weight: 500; 124 + .notebook-entry-preview-link { 125 + display: block; 126 + text-decoration: none; 69 127 color: var(--color-text); 128 + border-left: 2px solid transparent; 129 + border-top: 1px solid var(--color-border); 130 + padding-left: 0.625rem; /* 0.5 grid */ 131 + padding-top: 1.25rem; 132 + margin-left: -0.625rem; 133 + transition: border-left-color 0.2s ease; 70 134 } 71 135 72 - .notebook-card-date { 73 - margin-left: auto; 136 + .notebook-entry-preview-link:first-child { 137 + border-top: none; 138 + padding-top: 0; 139 + } 140 + 141 + .notebook-entry-preview-link:hover { 142 + border-left-color: var(--color-secondary); 143 + } 144 + 145 + .notebook-entry-preview-link:hover .entry-preview-title { 146 + color: var(--color-secondary); 147 + } 148 + 149 + .notebook-entry-preview { 150 + padding-bottom: 1.25rem; 151 + } 152 + 153 + .entry-preview-header { 154 + display: flex; 155 + justify-content: space-between; 156 + align-items: baseline; 157 + gap: 1rem; 158 + margin-bottom: 0.5rem; 159 + } 160 + 161 + .entry-preview-title { 162 + color: var(--color-text); 163 + font-weight: 600; 164 + font-size: 0.95rem; 165 + flex: 1; 166 + transition: color 0.2s ease; 167 + } 168 + 169 + .entry-preview-date { 170 + color: var(--color-muted); 171 + font-size: 0.8rem; 172 + white-space: nowrap; 173 + } 174 + 175 + .notebook-entry-interstitial { 176 + text-align: center; 74 177 color: var(--color-muted); 75 178 font-size: 0.85rem; 179 + padding: 1rem 0; 180 + font-style: italic; 181 + } 182 + 183 + .entry-preview-content { 184 + color: var(--color-subtle); 185 + font-size: 0.875rem; 186 + line-height: 1.5; 187 + display: -webkit-box; 188 + -webkit-line-clamp: 3; 189 + -webkit-box-orient: vertical; 190 + overflow: hidden; 191 + max-width: 100%; 192 + word-wrap: break-word; 193 + overflow-wrap: break-word; 194 + } 195 + 196 + .entry-preview-content p { 197 + margin: 0; 198 + display: inline; 199 + } 200 + 201 + .entry-preview-content h1, 202 + .entry-preview-content h2, 203 + .entry-preview-content h3, 204 + .entry-preview-content h4, 205 + .entry-preview-content h5, 206 + .entry-preview-content h6 { 207 + font-size: 0.875rem; 208 + font-weight: 600; 209 + margin: 0; 210 + } 211 + 212 + .entry-preview-content code { 213 + font-size: 0.8rem; 214 + white-space: pre-wrap; 215 + word-break: break-all; 216 + } 217 + 218 + .entry-preview-content pre { 219 + white-space: pre-wrap; 220 + word-break: break-all; 221 + max-width: 100%; 76 222 } 77 223 78 224 .notebook-card-tags { ··· 82 228 } 83 229 84 230 .notebook-card-tag { 85 - padding: 0.25rem 0.625rem; 86 - background: var(--color-base); 87 - border: 1px solid var(--color-border); 88 - border-radius: 3px; 231 + padding: 0.25rem 0.5rem 0.25rem 0; 89 232 font-size: 0.8rem; 90 233 color: var(--color-subtle); 91 - transition: all 0.15s ease; 234 + border-bottom: 1px solid var(--color-border); 235 + transition: 236 + color 0.15s ease, 237 + border-color 0.15s ease; 92 238 } 93 239 94 240 .notebook-card-link:hover .notebook-card-tag { 95 - background: var(--color-surface); 241 + color: var(--color-tertiary); 96 242 border-color: var(--color-tertiary); 97 243 } 98 244 ··· 106 252 -webkit-box-orient: vertical; 107 253 overflow: hidden; 108 254 } 255 + 256 + /* Dark mode: replace shadows with borders */ 257 + @media (prefers-color-scheme: dark) { 258 + .notebook-card-container { 259 + box-shadow: none; 260 + border: 1px dashed var(--color-border); 261 + } 262 + }
+110
crates/weaver-app/assets/styling/notebook-cover.css
··· 1 + /* Notebook cover - sidebar on desktop, header on mobile */ 2 + 3 + .notebook-cover { 4 + /* No background - same plane as page */ 5 + } 6 + 7 + /* Desktop: sidebar gets top and right borders */ 8 + @media (min-width: 1400px) { 9 + .notebook-cover { 10 + border-top: 1px solid var(--color-border); 11 + border-right: 1px solid var(--color-border); 12 + border-bottom: 1px solid var(--color-border); 13 + padding: 1.25rem; 14 + } 15 + } 16 + 17 + /* Mobile: header gets top and bottom borders */ 18 + @media (max-width: 1400px) { 19 + .notebook-cover { 20 + border-top: 1px solid var(--color-border); 21 + border-bottom: 1px solid var(--color-border); 22 + padding: 1.25rem 0; 23 + margin-bottom: 2rem; 24 + } 25 + } 26 + 27 + .notebook-cover-title { 28 + font-size: 1.5rem; 29 + font-weight: 700; 30 + color: var(--color-primary); 31 + margin: 0 0 1rem 0; 32 + font-family: var(--font-heading); 33 + } 34 + 35 + .notebook-cover-authors { 36 + margin-bottom: 1.25rem; 37 + } 38 + 39 + .notebook-authors-list { 40 + display: flex; 41 + flex-direction: column; 42 + gap: 1rem; 43 + } 44 + 45 + .author-separator { 46 + display: none; 47 + } 48 + 49 + .notebook-author { 50 + display: flex; 51 + align-items: center; 52 + gap: 0.75rem; 53 + } 54 + 55 + .notebook-author .avatar { 56 + width: 48px; 57 + height: 48px; 58 + flex-shrink: 0; 59 + } 60 + 61 + .notebook-author-info { 62 + display: flex; 63 + flex-direction: column; 64 + min-width: 0; 65 + } 66 + 67 + .notebook-author-name { 68 + font-weight: 600; 69 + color: var(--color-text); 70 + font-size: 1rem; 71 + } 72 + 73 + .notebook-author-handle { 74 + color: var(--color-subtle); 75 + font-size: 0.875rem; 76 + } 77 + 78 + .notebook-cover-description { 79 + color: var(--color-text); 80 + line-height: 1.6; 81 + margin-bottom: 1rem; 82 + white-space: pre-wrap; 83 + } 84 + 85 + .notebook-cover-meta { 86 + display: flex; 87 + gap: 1.5rem; 88 + align-items: center; 89 + flex-wrap: wrap; 90 + margin-bottom: 1rem; 91 + font-size: 0.9rem; 92 + color: var(--color-subtle); 93 + } 94 + 95 + .notebook-cover-stat { 96 + font-weight: 500; 97 + } 98 + 99 + .notebook-cover-tags { 100 + display: flex; 101 + gap: 0.5rem; 102 + flex-wrap: wrap; 103 + } 104 + 105 + .notebook-cover-tag { 106 + padding: 0.25rem 0.5rem 0.25rem 0; 107 + font-size: 0.85rem; 108 + color: var(--color-subtle); 109 + border-bottom: 1px solid var(--color-border); 110 + }
+35 -12
crates/weaver-app/assets/styling/profile.css
··· 1 1 /* Profile display - sidebar on desktop, header on mobile */ 2 2 3 3 .profile-display { 4 - background: var(--color-surface); 5 - border: 1px solid var(--color-border); 6 - border-radius: 4px; 4 + margin-top: 0.25rem; 5 + max-width: 100%; 6 + /* No background - same plane as page */ 7 + } 8 + 9 + /* Desktop: sidebar gets top and right borders */ 10 + @media (min-width: 1400px) { 11 + .profile-display { 12 + border-top: 1.5px dashed var(--color-border); 13 + border-right: 1.5px dashed var(--color-border); 14 + } 15 + } 16 + 17 + /* Mobile: header gets top and bottom borders */ 18 + @media (max-width: 1400px) { 19 + .profile-display { 20 + border-top: 1.5px dashed var(--color-border); 21 + border-bottom: 1.5px solid var(--color-border); 22 + } 7 23 } 8 24 9 25 .profile-banner { 10 - width: 100%; 26 + max-width: 100%; 11 27 height: 120px; 12 28 overflow: hidden; 13 - border-radius: 4px 4px 0 0; 14 29 } 15 30 16 31 .profile-banner img { ··· 20 35 } 21 36 22 37 .profile-content { 23 - padding: 1.5rem; 38 + padding: 1.25rem; 24 39 } 25 40 26 41 .profile-identity { 27 - margin-bottom: 1.5rem; 42 + margin-bottom: 1.25rem; /* Grid unit */ 28 43 } 29 44 30 45 .profile-identity .avatar { ··· 64 79 65 80 .profile-description { 66 81 color: var(--color-text); 82 + font-size: 0.875rem; 67 83 line-height: 1.5; 68 84 margin-top: 0.75rem; 69 85 white-space: pre-wrap; ··· 72 88 .profile-stats { 73 89 display: grid; 74 90 grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 75 - gap: 1rem; 76 - padding: 1rem 0; 77 - margin: 1rem 0; 78 - border-top: 1px solid var(--color-border); 79 - border-bottom: 1px solid var(--color-border); 91 + gap: 1.25rem; 92 + padding: 1.25rem 0; 93 + margin: 1.25rem 0; 94 + border-top: 1.5px dashed var(--color-border); 95 + border-bottom: 1.5px dashed var(--color-border); 80 96 } 81 97 82 98 .profile-stat { ··· 134 150 font-size: 1.25rem; 135 151 } 136 152 } 153 + 154 + @media (prefers-color-scheme: dark) { 155 + .profile-display { 156 + background-color: var(--color-surface); 157 + border: 1px dashed var(--color-border); 158 + } 159 + }
+4 -3
crates/weaver-app/assets/styling/theme-defaults.css
··· 20 20 --color-link: #d7827e; 21 21 --color-highlight: #cecacd; 22 22 23 - --font-body: IBM Plex, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 24 - --font-heading: IBM Plex Sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 25 - --font-mono: 'IBM Plex Mono', 'Berkeley Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace; 23 + --font-body: IBM Plex, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 24 + --font-heading: IBM Plex Sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 25 + --font-mono: "IBM Plex Mono", "Berkeley Mono", "Cascadia Code", "Roboto Mono", Consolas, monospace; 26 26 27 27 --spacing-base: 16px; 28 28 --spacing-line-height: 1.6; ··· 49 49 --color-link: #ebbcba; 50 50 --color-highlight: #524f67; 51 51 } 52 + 52 53 }
+13 -11
crates/weaver-app/src/components/entry.rs
··· 130 130 } 131 131 132 132 // Main content area 133 - div { class: "entry-content-main", 133 + div { class: "entry-content-main notebook-content", 134 134 // Metadata header 135 135 EntryMetadata { 136 136 entry_view: entry_view.clone(), ··· 161 161 } 162 162 163 163 #[component] 164 - pub fn EntryCard(entry: BookEntryView<'static>, book_title: SmolStr) -> Element { 164 + pub fn EntryCard( 165 + entry: BookEntryView<'static>, 166 + book_title: SmolStr, 167 + author_count: usize, 168 + ) -> Element { 165 169 use crate::Route; 166 170 use jacquard::{from_data, IntoStatic}; 167 171 use weaver_api::sh_weaver::notebook::entry::Entry; ··· 182 186 .format("%B %d, %Y") 183 187 .to_string(); 184 188 185 - // Get first author for display 186 - let first_author = entry_view.authors.first(); 189 + // Only show author if notebook has multiple authors 190 + let show_author = author_count > 1; 191 + let first_author = if show_author { 192 + entry_view.authors.first() 193 + } else { 194 + None 195 + }; 187 196 188 197 // Render preview from entry content 189 198 let preview_html = from_data::<Entry>(&entry_view.record).ok().map(|entry| { ··· 403 412 .as_ref() 404 413 .map(|t| t.as_ref()) 405 414 .unwrap_or("Untitled"); 406 - 407 - let label = if direction == "prev" { 408 - "← Previous" 409 - } else { 410 - "Next →" 411 - }; 412 415 let arrow = if direction == "prev" { "←" } else { "→" }; 413 416 414 417 rsx! { ··· 420 423 }, 421 424 class: "nav-button nav-button-{direction}", 422 425 div { class: "nav-arrow", "{arrow}" } 423 - div { class: "nav-label", "{label}" } 424 426 div { class: "nav-title", "{entry_title}" } 425 427 } 426 428 }
+238 -52
crates/weaver-app/src/components/identity.rs
··· 1 1 use crate::{fetch, Route}; 2 2 use dioxus::prelude::*; 3 - use jacquard::types::ident::AtIdentifier; 3 + use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier}; 4 + use weaver_api::com_atproto::repo::strong_ref::StrongRef; 4 5 use weaver_api::sh_weaver::notebook::NotebookView; 5 6 6 7 const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); ··· 17 18 18 19 #[component] 19 20 pub fn RepositoryIndex(ident: AtIdentifier<'static>) -> Element { 21 + use crate::components::ProfileDisplay; 22 + 20 23 let fetcher = use_context::<fetch::CachedFetcher>(); 21 24 22 25 // Fetch notebooks for this specific DID ··· 26 29 })); 27 30 28 31 rsx! { 29 - document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS } 32 + document::Stylesheet { href: NOTEBOOK_CARD_CSS } 33 + 34 + div { class: "repository-layout", 35 + // Profile sidebar (desktop) / header (mobile) 36 + aside { class: "repository-sidebar", 37 + ProfileDisplay { ident: ident.clone() } 38 + } 30 39 31 - div { class: "notebooks-list", 32 - match notebooks() { 33 - Some(Ok(notebook_list)) => rsx! { 34 - for notebook in notebook_list.iter() { 35 - { 36 - let view = &notebook.0; 37 - rsx! { 38 - div { 39 - key: "{view.cid}", 40 - NotebookCard { notebook: view.clone() } 40 + // Main content area 41 + main { class: "repository-main", 42 + div { class: "notebooks-list", 43 + match notebooks() { 44 + Some(Ok(notebook_list)) => rsx! { 45 + for notebook in notebook_list.iter() { 46 + { 47 + let view = &notebook.0; 48 + let entries = &notebook.1; 49 + rsx! { 50 + div { 51 + key: "{view.cid}", 52 + NotebookCard { 53 + notebook: view.clone(), 54 + entry_refs: entries.clone() 55 + } 56 + } 57 + } 41 58 } 42 59 } 60 + }, 61 + Some(Err(_)) => rsx! { 62 + div { "Error loading notebooks" } 63 + }, 64 + None => rsx! { 65 + div { "Loading notebooks..." } 43 66 } 44 67 } 45 - }, 46 - Some(Err(_)) => rsx! { 47 - div { "Error loading notebooks" } 48 - }, 49 - None => rsx! { 50 - div { "Loading notebooks..." } 51 68 } 52 69 } 53 70 } ··· 55 72 } 56 73 57 74 #[component] 58 - pub fn NotebookCard(notebook: NotebookView<'static>) -> Element { 59 - use crate::components::avatar::{Avatar, AvatarImage}; 75 + pub fn NotebookCard( 76 + notebook: NotebookView<'static>, 77 + entry_refs: Vec<StrongRef<'static>>, 78 + ) -> Element { 60 79 use jacquard::IntoStatic; 80 + 81 + let fetcher = use_context::<fetch::CachedFetcher>(); 61 82 62 83 let title = notebook 63 84 .title ··· 68 89 // Format date 69 90 let formatted_date = notebook.indexed_at.as_ref().format("%B %d, %Y").to_string(); 70 91 71 - // Get first author for display 72 - let first_author = notebook.authors.first(); 92 + // Show authors only if multiple 93 + let show_authors = notebook.authors.len() > 1; 73 94 74 95 let ident = notebook.uri.authority().clone().into_static(); 96 + let book_title: SmolStr = title.to_string().into(); 97 + 98 + // Fetch all entries to get first/last 99 + let ident_for_fetch = ident.clone(); 100 + let book_title_for_fetch = book_title.clone(); 101 + let entries = use_resource(use_reactive!(|(ident_for_fetch, book_title_for_fetch)| { 102 + let fetcher = fetcher.clone(); 103 + async move { 104 + fetcher 105 + .list_notebook_entries(ident_for_fetch, book_title_for_fetch) 106 + .await 107 + .ok() 108 + .flatten() 109 + } 110 + })); 75 111 rsx! { 76 112 div { class: "notebook-card", 77 - Link { 78 - to: Route::Entry { 79 - ident, 80 - book_title: title.to_string().into(), 81 - title: "".into() // Will redirect to first entry 82 - }, 83 - class: "notebook-card-link", 113 + div { class: "notebook-card-container", 84 114 85 - div { class: "notebook-card-header", 86 - h2 { class: "notebook-card-title", "{title}" } 115 + Link { 116 + to: Route::Entry { 117 + ident: ident.clone(), 118 + book_title: title.to_string().into(), 119 + title: "".into() // Will redirect to first entry 120 + }, 121 + class: "notebook-card-header-link", 122 + 123 + div { class: "notebook-card-header", 124 + h2 { class: "notebook-card-title", "{title}" } 125 + 126 + div { class: "notebook-card-date", 127 + time { datetime: "{notebook.indexed_at.as_str()}", "{formatted_date}" } 128 + } 129 + } 87 130 } 88 131 89 - div { class: "notebook-card-meta", 90 - if let Some(author) = first_author { 91 - div { class: "notebook-card-author", 132 + // Show authors only if multiple 133 + if show_authors { 134 + div { class: "notebook-card-authors", 135 + for (i, author) in notebook.authors.iter().enumerate() { 136 + if i > 0 { span { class: "author-separator", ", " } } 92 137 { 93 138 use weaver_api::sh_weaver::actor::ProfileDataViewInner; 94 139 95 140 match &author.record.inner { 96 141 ProfileDataViewInner::ProfileView(profile) => { 97 - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); 142 + let display_name = profile.display_name.as_ref() 143 + .map(|n| n.as_ref()) 144 + .unwrap_or("Unknown"); 98 145 rsx! { 99 - if let Some(ref avatar_url) = profile.avatar { 100 - Avatar { 101 - AvatarImage { src: avatar_url.as_ref() } 102 - } 103 - } 104 146 span { class: "author-name", "{display_name}" } 105 147 } 106 148 } 107 149 ProfileDataViewInner::ProfileViewDetailed(profile) => { 108 - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); 150 + let display_name = profile.display_name.as_ref() 151 + .map(|n| n.as_ref()) 152 + .unwrap_or("Unknown"); 109 153 rsx! { 110 - if let Some(ref avatar_url) = profile.avatar { 111 - Avatar { 112 - AvatarImage { src: avatar_url.as_ref() } 113 - } 114 - } 115 154 span { class: "author-name", "{display_name}" } 116 155 } 117 156 } ··· 120 159 span { class: "author-name", "@{profile.handle.as_ref()}" } 121 160 } 122 161 } 123 - _ => { 124 - rsx! { 125 - span { class: "author-name", "Unknown" } 126 - } 162 + _ => rsx! { 163 + span { class: "author-name", "Unknown" } 127 164 } 128 165 } 129 166 } 130 167 } 131 168 } 169 + } 132 170 133 - div { class: "notebook-card-date", 134 - time { datetime: "{notebook.indexed_at.as_str()}", "{formatted_date}" } 171 + // Entry previews section 172 + if let Some(Some(entry_list)) = entries() { 173 + div { class: "notebook-card-previews", 174 + { 175 + use jacquard::from_data; 176 + use weaver_api::sh_weaver::notebook::entry::Entry; 177 + 178 + if entry_list.len() <= 5 { 179 + // Show all entries if 5 or fewer 180 + rsx! { 181 + for (i, entry_view) in entry_list.iter().enumerate() { 182 + { 183 + let entry_title = entry_view.entry.title.as_ref() 184 + .map(|t| t.as_ref()) 185 + .unwrap_or("Untitled"); 186 + 187 + let preview_html = from_data::<Entry>(&entry_view.entry.record).ok().map(|entry| { 188 + let parser = markdown_weaver::Parser::new(&entry.content); 189 + let mut html_buf = String::new(); 190 + markdown_weaver::html::push_html(&mut html_buf, parser); 191 + html_buf 192 + }); 193 + 194 + let created_at = from_data::<Entry>(&entry_view.entry.record).ok() 195 + .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string()); 196 + 197 + rsx! { 198 + Link { 199 + to: Route::Entry { 200 + ident: ident.clone(), 201 + book_title: book_title.clone(), 202 + title: entry_title.to_string().into() 203 + }, 204 + class: "notebook-entry-preview-link", 205 + 206 + div { class: "notebook-entry-preview", 207 + div { class: "entry-preview-header", 208 + div { class: "entry-preview-title", "{entry_title}" } 209 + if let Some(ref date) = created_at { 210 + div { class: "entry-preview-date", "{date}" } 211 + } 212 + } 213 + if let Some(ref html) = preview_html { 214 + div { class: "entry-preview-content", dangerous_inner_html: "{html}" } 215 + } 216 + } 217 + } 218 + } 219 + } 220 + } 221 + } 222 + } else { 223 + // Show first, interstitial, and last 224 + rsx! { 225 + if let Some(first_entry) = entry_list.first() { 226 + { 227 + let entry_title = first_entry.entry.title.as_ref() 228 + .map(|t| t.as_ref()) 229 + .unwrap_or("Untitled"); 230 + 231 + let preview_html = from_data::<Entry>(&first_entry.entry.record).ok().map(|entry| { 232 + let parser = markdown_weaver::Parser::new(&entry.content); 233 + let mut html_buf = String::new(); 234 + markdown_weaver::html::push_html(&mut html_buf, parser); 235 + html_buf 236 + }); 237 + 238 + let created_at = from_data::<Entry>(&first_entry.entry.record).ok() 239 + .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string()); 240 + 241 + rsx! { 242 + Link { 243 + to: Route::Entry { 244 + ident: ident.clone(), 245 + book_title: book_title.clone(), 246 + title: entry_title.to_string().into() 247 + }, 248 + class: "notebook-entry-preview-link", 249 + 250 + div { class: "notebook-entry-preview notebook-entry-preview-first", 251 + div { class: "entry-preview-header", 252 + div { class: "entry-preview-title", "{entry_title}" } 253 + if let Some(ref date) = created_at { 254 + div { class: "entry-preview-date", "{date}" } 255 + } 256 + } 257 + if let Some(ref html) = preview_html { 258 + div { class: "entry-preview-content", dangerous_inner_html: "{html}" } 259 + } 260 + } 261 + } 262 + } 263 + } 264 + } 265 + 266 + // Interstitial showing count 267 + { 268 + let middle_count = entry_list.len().saturating_sub(2); 269 + rsx! { 270 + div { class: "notebook-entry-interstitial", 271 + "... {middle_count} more " 272 + if middle_count == 1 { "entry" } else { "entries" } 273 + " ..." 274 + } 275 + } 276 + } 277 + 278 + if let Some(last_entry) = entry_list.last() { 279 + { 280 + let entry_title = last_entry.entry.title.as_ref() 281 + .map(|t| t.as_ref()) 282 + .unwrap_or("Untitled"); 283 + 284 + let preview_html = from_data::<Entry>(&last_entry.entry.record).ok().map(|entry| { 285 + let parser = markdown_weaver::Parser::new(&entry.content); 286 + let mut html_buf = String::new(); 287 + markdown_weaver::html::push_html(&mut html_buf, parser); 288 + html_buf 289 + }); 290 + 291 + let created_at = from_data::<Entry>(&last_entry.entry.record).ok() 292 + .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string()); 293 + 294 + rsx! { 295 + Link { 296 + to: Route::Entry { 297 + ident: ident.clone(), 298 + book_title: book_title.clone(), 299 + title: entry_title.to_string().into() 300 + }, 301 + class: "notebook-entry-preview-link", 302 + 303 + div { class: "notebook-entry-preview notebook-entry-preview-last", 304 + div { class: "entry-preview-header", 305 + div { class: "entry-preview-title", "{entry_title}" } 306 + if let Some(ref date) = created_at { 307 + div { class: "entry-preview-date", "{date}" } 308 + } 309 + } 310 + if let Some(ref html) = preview_html { 311 + div { class: "entry-preview-content", dangerous_inner_html: "{html}" } 312 + } 313 + } 314 + } 315 + } 316 + } 317 + } 318 + } 319 + } 320 + } 135 321 } 136 322 } 137 323
+3
crates/weaver-app/src/components/mod.rs
··· 14 14 15 15 pub mod profile; 16 16 pub use profile::ProfileDisplay; 17 + 18 + pub mod notebook_cover; 19 + pub use notebook_cover::NotebookCover;
+155
crates/weaver-app/src/components/notebook_cover.rs
··· 1 + #![allow(non_snake_case)] 2 + 3 + use crate::components::avatar::{Avatar, AvatarImage}; 4 + use dioxus::prelude::*; 5 + use jacquard::types::ident::AtIdentifier; 6 + use weaver_api::sh_weaver::notebook::NotebookView; 7 + 8 + const NOTEBOOK_COVER_CSS: Asset = asset!("/assets/styling/notebook-cover.css"); 9 + 10 + #[component] 11 + pub fn NotebookCover(notebook: NotebookView<'static>, title: String) -> Element { 12 + use jacquard::from_data; 13 + use weaver_api::sh_weaver::notebook::book::Book; 14 + 15 + // Deserialize the book record from the view 16 + let book = match from_data::<Book>(&notebook.record) { 17 + Ok(book) => book, 18 + Err(_) => { 19 + return rsx! { 20 + document::Stylesheet { href: NOTEBOOK_COVER_CSS } 21 + div { class: "notebook-cover", 22 + h1 { class: "notebook-cover-title", "{title}" } 23 + div { "Error loading notebook details" } 24 + } 25 + } 26 + } 27 + }; 28 + 29 + rsx! { 30 + document::Stylesheet { href: NOTEBOOK_COVER_CSS } 31 + 32 + div { class: "notebook-cover", 33 + h1 { class: "notebook-cover-title", "{title}" } 34 + 35 + // Authors section 36 + if !notebook.authors.is_empty() { 37 + div { class: "notebook-cover-authors", 38 + NotebookAuthors { authors: notebook.authors.clone() } 39 + } 40 + } 41 + 42 + // Metadata 43 + div { class: "notebook-cover-meta", 44 + // Entry count 45 + span { class: "notebook-cover-stat", 46 + "{book.entry_list.len()} " 47 + if book.entry_list.len() == 1 { "entry" } else { "entries" } 48 + } 49 + 50 + // Created date 51 + if let Some(ref created_at) = book.created_at { 52 + { 53 + let formatted_date = created_at.as_ref().format("%B %d, %Y").to_string(); 54 + rsx! { 55 + span { class: "notebook-cover-date", 56 + "Created {formatted_date}" 57 + } 58 + } 59 + } 60 + } 61 + } 62 + 63 + // Tags if present 64 + if let Some(ref tags) = notebook.tags { 65 + if !tags.is_empty() { 66 + div { class: "notebook-cover-tags", 67 + for tag in tags.iter() { 68 + span { class: "notebook-cover-tag", "{tag}" } 69 + } 70 + } 71 + } 72 + } 73 + } 74 + } 75 + } 76 + 77 + #[component] 78 + fn NotebookAuthors( 79 + authors: Vec<weaver_api::sh_weaver::notebook::AuthorListView<'static>>, 80 + ) -> Element { 81 + rsx! { 82 + div { class: "notebook-authors-list", 83 + for (i, author) in authors.iter().enumerate() { 84 + if i > 0 { span { class: "author-separator", ", " } } 85 + NotebookAuthor { author: author.clone() } 86 + } 87 + } 88 + } 89 + } 90 + 91 + #[component] 92 + fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'static>) -> Element { 93 + use crate::data::use_handle; 94 + use weaver_api::sh_weaver::actor::ProfileDataViewInner; 95 + 96 + // Author already has profile data hydrated 97 + match &author.record.inner { 98 + ProfileDataViewInner::ProfileView(p) => { 99 + let display_name = p 100 + .display_name 101 + .as_ref() 102 + .map(|n| n.as_ref()) 103 + .unwrap_or("Unknown"); 104 + let handle = use_handle(p.did.clone().into())?; 105 + 106 + rsx! { 107 + div { class: "notebook-author", 108 + if let Some(ref avatar) = p.avatar { 109 + Avatar { 110 + AvatarImage { src: avatar.as_ref() } 111 + } 112 + } 113 + div { class: "notebook-author-info", 114 + div { class: "notebook-author-name", "{display_name}" } 115 + div { class: "notebook-author-handle", "@{handle()}" } 116 + } 117 + } 118 + } 119 + } 120 + ProfileDataViewInner::ProfileViewDetailed(p) => { 121 + let display_name = p 122 + .display_name 123 + .as_ref() 124 + .map(|n| n.as_ref()) 125 + .unwrap_or("Unknown"); 126 + let handle = use_handle(p.did.clone().into())?; 127 + 128 + rsx! { 129 + div { class: "notebook-author", 130 + if let Some(ref avatar) = p.avatar { 131 + Avatar { 132 + AvatarImage { src: avatar.as_ref() } 133 + } 134 + } 135 + div { class: "notebook-author-info", 136 + div { class: "notebook-author-name", "{display_name}" } 137 + div { class: "notebook-author-handle", "@{handle()}" } 138 + } 139 + } 140 + } 141 + } 142 + ProfileDataViewInner::TangledProfileView(p) => { 143 + rsx! { 144 + div { class: "notebook-author", 145 + div { class: "notebook-author-name", "@{p.handle.as_ref()}" } 146 + } 147 + } 148 + } 149 + _ => rsx! { 150 + div { class: "notebook-author", 151 + "Unknown author" 152 + } 153 + }, 154 + } 155 + }
+16 -5
crates/weaver-app/src/components/profile.rs
··· 2 2 3 3 use crate::{ 4 4 components::avatar::{Avatar, AvatarImage}, 5 + data::use_handle, 5 6 Route, 6 7 }; 7 8 use dioxus::prelude::*; ··· 17 18 18 19 match profile().as_ref() { 19 20 Some(profile_view) => rsx! { 20 - document::Link { rel: "stylesheet", href: PROFILE_CSS } 21 + document::Stylesheet { href: PROFILE_CSS } 21 22 22 23 div { class: "profile-display", 23 24 // Banner if present ··· 33 34 rsx! { } 34 35 } 35 36 } 37 + ProfileDataViewInner::ProfileViewDetailed(p) => { 38 + if let Some(ref banner) = p.banner { 39 + rsx! { 40 + div { class: "profile-banner", 41 + img { src: "{banner.as_ref()}", alt: "Profile banner" } 42 + } 43 + } 44 + } else { 45 + rsx! { } 46 + } 47 + } 36 48 _ => rsx! { } 37 49 }} 38 50 ··· 101 113 span { class: "profile-pronouns", " ({pronouns})" } 102 114 } 103 115 } 104 - div { class: "profile-handle", "@{ident}" } 116 + div { class: "profile-handle", "@{use_handle(ident.clone())?}" } 105 117 106 118 if let Some(ref location) = profile.location { 107 119 div { class: "profile-location", "{location}" } ··· 131 143 132 144 div { class: "profile-name-section", 133 145 h1 { class: "profile-display-name", "{display_name}" } 134 - div { class: "profile-handle", "@{ident}" } 146 + div { class: "profile-handle", "@{use_handle(ident.clone())?}" } 135 147 } 136 148 137 149 if let Some(ref description) = profile.description { ··· 176 188 rsx! { 177 189 div { class: "profile-stats", 178 190 div { class: "profile-stat", 179 - span { class: "profile-stat-label", "Notebooks" } 180 - span { class: "profile-stat-value", "{notebook_count}" } 191 + span { class: "profile-stat-label", "{notebook_count} notebooks" } 181 192 } 182 193 // TODO: Add entry count, subscriber counts when available 183 194 }
+5 -1
crates/weaver-app/src/views/home.rs
··· 23 23 for notebook in notebook_list.iter() { 24 24 { 25 25 let view = &notebook.0; 26 + let entries = &notebook.1; 26 27 rsx! { 27 28 div { 28 29 key: "{view.cid}", 29 - NotebookCard { notebook: view.clone() } 30 + NotebookCard { 31 + notebook: view.clone(), 32 + entry_refs: entries.clone() 33 + } 30 34 } 31 35 } 32 36 }
+4 -3
crates/weaver-app/src/views/navbar.rs
··· 1 + use crate::data::use_handle; 1 2 use crate::Route; 2 3 use dioxus::prelude::*; 3 4 ··· 30 31 match route { 31 32 Route::RepositoryIndex { ident } => rsx! { 32 33 span { class: "breadcrumb-separator", " > " } 33 - span { class: "breadcrumb breadcrumb-current", "@{ident}" } 34 + span { class: "breadcrumb breadcrumb-current", "@{use_handle(ident.clone())?}" } 34 35 }, 35 36 Route::NotebookIndex { ident, book_title } => rsx! { 36 37 span { class: "breadcrumb-separator", " > " } 37 38 Link { 38 39 to: Route::RepositoryIndex { ident: ident.clone() }, 39 40 class: "breadcrumb", 40 - "@{ident}" 41 + "@{use_handle(ident.clone())?}" 41 42 } 42 43 span { class: "breadcrumb-separator", " > " } 43 44 span { class: "breadcrumb breadcrumb-current", "{book_title}" } ··· 47 48 Link { 48 49 to: Route::RepositoryIndex { ident: ident.clone() }, 49 50 class: "breadcrumb", 50 - "@{ident}" 51 + "@{use_handle(ident.clone())?}" 51 52 } 52 53 span { class: "breadcrumb-separator", " > " } 53 54 Link {
+57 -13
crates/weaver-app/src/views/notebook.rs
··· 1 1 use crate::{ 2 - components::{EntryCard, NotebookCss}, 2 + components::{EntryCard, NotebookCover, NotebookCss}, 3 3 fetch, Route, 4 4 }; 5 5 use dioxus::prelude::*; ··· 27 27 let fetcher = use_context::<fetch::CachedFetcher>(); 28 28 let book_title_clone = book_title.clone(); 29 29 30 - let notebook_entries = use_resource(use_reactive!(|(ident, book_title)| { 31 - let fetcher = fetcher.clone(); 30 + // Fetch full notebook to get author count 31 + let ident_for_notebook = ident.clone(); 32 + let book_title_for_notebook = book_title.clone(); 33 + let data_fetcher = fetcher.clone(); 34 + let notebook_data = use_resource(use_reactive!(|( 35 + ident_for_notebook, 36 + book_title_for_notebook, 37 + )| { 38 + let fetcher = data_fetcher.clone(); 39 + async move { 40 + fetcher 41 + .get_notebook(ident_for_notebook, book_title_for_notebook) 42 + .await 43 + .ok() 44 + .flatten() 45 + } 46 + })); 47 + 48 + // Also fetch entries 49 + let entry_fetcher = fetcher.clone(); 50 + let entries_resource = use_resource(use_reactive!(|(ident, book_title)| { 51 + let fetcher = entry_fetcher.clone(); 32 52 async move { 33 - fetcher.list_notebook_entries(ident, book_title).await.ok().flatten() 53 + fetcher 54 + .list_notebook_entries(ident, book_title) 55 + .await 56 + .ok() 57 + .flatten() 34 58 } 35 59 })); 36 60 37 61 rsx! { 38 62 document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } 39 63 40 - div { class: "entries-list", 41 - match &*notebook_entries.read_unchecked() { 42 - Some(Some(entries)) => rsx! { 43 - for entry in entries { 44 - EntryCard { entry: entry.clone(), book_title: book_title_clone.clone() } 64 + match (&*notebook_data.read_unchecked(), &*entries_resource.read_unchecked()) { 65 + (Some(Some(data)), Some(Some(entries))) => { 66 + let (notebook_view, _) = data.as_ref(); 67 + let author_count = notebook_view.authors.len(); 68 + 69 + rsx! { 70 + div { class: "notebook-layout", 71 + aside { class: "notebook-sidebar", 72 + NotebookCover { 73 + notebook: notebook_view.clone(), 74 + title: book_title_clone.to_string() 75 + } 76 + } 77 + 78 + main { class: "notebook-main", 79 + div { class: "entries-list", 80 + for entry in entries { 81 + EntryCard { 82 + entry: entry.clone(), 83 + book_title: book_title_clone.clone(), 84 + author_count 85 + } 86 + } 87 + } 88 + } 45 89 } 46 - }, 47 - Some(None) => rsx! { div { class: "error", "Notebook not found" } }, 48 - None => rsx! { div { class: "loading", "Loading notebook..." } } 49 - } 90 + } 91 + }, 92 + (Some(None), _) | (_, Some(None)) => rsx! { div { class: "error", "Notebook or entries not found" } }, 93 + _ => rsx! { div { class: "loading", "Loading..." } } 50 94 } 51 95 } 52 96 }
+1 -1
crates/weaver-common/src/agent.rs
··· 548 548 .map(|blob| { 549 549 let cid = blob.blob().cid(); 550 550 jacquard::types::string::Uri::new_owned(format!( 551 - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}", 551 + "https://cdn.bsky.app/img/banner/plain/{}/{}", 552 552 did, cid 553 553 )) 554 554 })
+2 -1
crates/weaver-renderer/src/css.rs
··· 82 82 line-height: var(--spacing-line-height); 83 83 }} 84 84 85 - body {{ 85 + /* Scoped to notebook-content container */ 86 + .notebook-content {{ 86 87 font-family: var(--font-body); 87 88 color: var(--color-text); 88 89 background-color: var(--color-base);
+5
crates/weaver-renderer/src/static_site/document.rs
··· 155 155 156 156 writer.write_all(b"</head>\n").await.into_diagnostic()?; 157 157 writer.write_all(b"<body>\n").await.into_diagnostic()?; 158 + writer 159 + .write_all(b"<div class=\"notebook-content\">\n") 160 + .await 161 + .into_diagnostic()?; 158 162 159 163 Ok(()) 160 164 } ··· 164 168 ) -> miette::Result<()> { 165 169 use tokio::io::AsyncWriteExt; 166 170 171 + writer.write_all(b"</div>\n").await.into_diagnostic()?; 167 172 writer.write_all(b"</body>\n").await.into_diagnostic()?; 168 173 writer.write_all(b"</html>\n").await.into_diagnostic()?; 169 174