Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

ui: consistency

Hugo 154f4583 bd008822

+111 -87
+2 -1
packages/app/src/pages/sphere-permissions.tsx
··· 19 19 20 20 const moduleLabels: Record<string, string> = { 21 21 sphere: "Sphere", 22 - "feature-requests": "Infuse", 22 + "feature-requests": "Infuse - Feature Requests", 23 + kanban: "Flux - Project Board", 23 24 }; 24 25 25 26 export function SpherePermissionsPage() {
+31 -48
packages/app/src/pages/sphere.css.ts
··· 99 99 export const kpis = style({ 100 100 display: "grid", 101 101 gap: "1px", 102 - gridTemplateColumns: "repeat(4, 1fr)", 102 + gridTemplateColumns: "repeat(3, 1fr)", 103 103 backgroundColor: vars.color.border, 104 104 border: `1px solid ${vars.color.border}`, 105 105 borderRadius: vars.radius.md, ··· 194 194 export const fluxColumns = style({ 195 195 display: "grid", 196 196 gap: "1px", 197 - gridTemplateColumns: "repeat(4, 1fr)", 197 + gridTemplateColumns: "repeat(3, 1fr)", 198 198 backgroundColor: vars.color.border, 199 199 border: `1px solid ${vars.color.border}`, 200 200 borderRadius: vars.radius.md, 201 201 overflow: "hidden", 202 - "@media": { 203 - ["screen and (max-width: 480px)"]: { 204 - gridTemplateColumns: "repeat(2, 1fr)", 205 - }, 206 - }, 207 202 }); 208 203 209 204 export const fluxCol = style({ ··· 291 286 whiteSpace: "nowrap", 292 287 }); 293 288 294 - // ---- Status pill ---- 289 + // ---- Status indicator (dot + label) ---- 295 290 296 - const statusPillBase = { 297 - display: "inline-block" as const, 298 - paddingBlock: "2px", 299 - paddingInline: "7px", 300 - borderRadius: "4px", 301 - fontSize: "0.6875rem", 302 - fontWeight: 600, 303 - letterSpacing: "0.02em", 304 - lineHeight: 1, 305 - textTransform: "uppercase" as const, 306 - }; 291 + export const statusIndicator = style({ 292 + display: "inline-flex", 293 + alignItems: "center", 294 + gap: "6px", 295 + fontSize: "0.8125rem", 296 + fontWeight: 500, 297 + whiteSpace: "nowrap", 298 + flexShrink: 0, 299 + color: vars.color.textMuted, 300 + }); 307 301 308 - export const statusPillRequested = style({ 309 - ...statusPillBase, 310 - backgroundColor: vars.color.primaryLight, 311 - color: vars.color.primary, 302 + export const statusDot = style({ 303 + display: "inline-block", 304 + inlineSize: "6px", 305 + blockSize: "6px", 306 + borderRadius: "50%", 307 + backgroundColor: vars.color.textMuted, 312 308 }); 313 - export const statusPillApproved = style({ 314 - ...statusPillBase, 315 - backgroundColor: vars.color.primaryLight, 316 - color: vars.color.primary, 317 - }); 318 - export const statusPillInProgress = style({ 319 - ...statusPillBase, 320 - backgroundColor: vars.color.warningLight, 321 - color: vars.color.warning, 322 - }); 323 - export const statusPillDone = style({ 324 - ...statusPillBase, 325 - backgroundColor: vars.color.successLight, 326 - color: vars.color.success, 327 - }); 328 - export const statusPillNotPlanned = style({ 329 - ...statusPillBase, 330 - backgroundColor: vars.color.dangerLight, 331 - color: vars.color.danger, 332 - }); 333 - export const statusPillNeutral = style({ 334 - ...statusPillBase, 335 - backgroundColor: vars.color.surfaceHover, 336 - color: vars.color.textMuted, 337 - }); 309 + 310 + globalStyle(`${statusDot}[data-status="approved"]`, { backgroundColor: vars.color.primary }); 311 + globalStyle(`${statusIndicator}[data-status="approved"]`, { color: vars.color.primary }); 312 + 313 + globalStyle(`${statusDot}[data-status="in-progress"]`, { backgroundColor: vars.color.warning }); 314 + globalStyle(`${statusIndicator}[data-status="in-progress"]`, { color: vars.color.warning }); 315 + 316 + globalStyle(`${statusDot}[data-status="done"]`, { backgroundColor: vars.color.success }); 317 + globalStyle(`${statusIndicator}[data-status="done"]`, { color: vars.color.success }); 318 + 319 + globalStyle(`${statusDot}[data-status="not-planned"]`, { backgroundColor: vars.color.danger }); 320 + globalStyle(`${statusIndicator}[data-status="not-planned"]`, { color: vars.color.danger }); 338 321 339 322 // ---- Vote chip ---- 340 323
+34 -35
packages/app/src/pages/sphere.tsx
··· 2 2 import { useSignal } from "@preact/signals"; 3 3 import { sphereState, sphereHandle } from "@exosphere/client/sphere"; 4 4 import { useQuery } from "@exosphere/client/hooks"; 5 + import { canDo } from "@exosphere/client/permissions"; 5 6 import { spherePath } from "@exosphere/client/router"; 6 7 import { Link } from "@exosphere/client/link"; 7 8 import { ssrPageData } from "@exosphere/client/ssr-data"; ··· 58 59 { key: "not-planned", label: "Not planned" }, 59 60 ]; 60 61 61 - const FR_PILL_BY_STATUS: Record<FrStatus, string> = { 62 - requested: s.statusPillRequested, 63 - approved: s.statusPillApproved, 64 - "in-progress": s.statusPillInProgress, 65 - done: s.statusPillDone, 66 - "not-planned": s.statusPillNotPlanned, 67 - duplicate: s.statusPillNeutral, 68 - }; 69 - 70 62 const FR_STATUS_LABEL: Record<FrStatus, string> = { 71 63 requested: "Requested", 72 64 approved: "Approved", ··· 76 68 duplicate: "Duplicate", 77 69 }; 78 70 79 - // StatusType → dashboard column label (matches the design wording). 80 71 const FLUX_COLUMNS: { type: KanbanStatusType; label: string }[] = [ 81 72 { type: "backlog", label: "Backlog" }, 82 73 { type: "planned", label: "To do" }, 83 74 { type: "started", label: "Started" }, 84 - { type: "completed", label: "Done" }, 85 75 ]; 86 76 87 - // StatusType → pill variant used for latest tasks. 88 - const FLUX_STATUS_PILL: Record<KanbanStatusType, string> = { 89 - backlog: s.statusPillNeutral, 90 - planned: s.statusPillApproved, 91 - started: s.statusPillInProgress, 92 - completed: s.statusPillDone, 93 - canceled: s.statusPillNotPlanned, 77 + // Map kanban status types onto the FR status indicator palette so a single set of 78 + // data-status color rules drives both widgets. 79 + const FLUX_INDICATOR_STATUS: Record<KanbanStatusType, string> = { 80 + backlog: "neutral", 81 + planned: "approved", 82 + started: "in-progress", 83 + completed: "done", 84 + canceled: "not-planned", 94 85 }; 95 86 96 87 function InfuseWidget({ handle }: { handle: string }) { ··· 118 109 <div class={s.widgetSub}>Feature requests</div> 119 110 </div> 120 111 <div class={s.widgetActions}> 121 - <Link href={spherePath("/infuse?new=1")} class={s.widgetNewBtn}> 122 - + New 123 - </Link> 112 + {canDo("feature-requests", "create") && ( 113 + <Link href={spherePath("/infuse?new=1")} class={s.widgetNewBtn}> 114 + + New 115 + </Link> 116 + )} 124 117 <Link href={spherePath("/infuse")} class={s.widgetLink}> 125 118 Open &rarr; 126 119 </Link> ··· 131 124 <> 132 125 <div class={s.kpis}> 133 126 <div class={s.kpi}> 134 - <span class={s.kpiLabel}>Total</span> 135 - <span class={s.kpiNum.default}>{data.total}</span> 136 - </div> 137 - <div class={s.kpi}> 138 127 <span class={s.kpiLabel}>Requested</span> 139 128 <span class={s.kpiNum.primary}>{data.statusCounts.requested}</span> 140 129 </div> 141 130 <div class={s.kpi}> 142 - <span class={s.kpiLabel}>In progress</span> 143 - <span class={s.kpiNum.warning}>{data.statusCounts["in-progress"]}</span> 131 + <span class={s.kpiLabel}>Approved</span> 132 + <span class={s.kpiNum.default}>{data.statusCounts.approved}</span> 144 133 </div> 145 134 <div class={s.kpi}> 146 - <span class={s.kpiLabel}>Done</span> 147 - <span class={s.kpiNum.success}>{data.statusCounts.done}</span> 135 + <span class={s.kpiLabel}>In progress</span> 136 + <span class={s.kpiNum.warning}>{data.statusCounts["in-progress"]}</span> 148 137 </div> 149 138 </div> 150 139 ··· 170 159 )} 171 160 </div> 172 161 <div class={s.listRightMeta}> 173 - <span class={FR_PILL_BY_STATUS[req.status] ?? s.statusPillNeutral}> 162 + <span class={s.statusIndicator} data-status={req.status}> 163 + <span class={s.statusDot} data-status={req.status} /> 174 164 {FR_STATUS_LABEL[req.status] ?? req.status} 175 165 </span> 176 166 </div> ··· 216 206 return ( 217 207 <> 218 208 <div class={s.kpis}> 219 - {[0, 1, 2, 3].map((i) => ( 209 + {[0, 1, 2].map((i) => ( 220 210 <div key={i} class={s.kpi}> 221 211 <span class={s.kpiLabel}>&nbsp;</span> 222 212 <span class={s.kpiNum.default}>&nbsp;</span> ··· 264 254 <div class={s.widgetSub}>Project board</div> 265 255 </div> 266 256 <div class={s.widgetActions}> 267 - <Link href={spherePath("/flux?new=1")} class={s.widgetNewBtn}> 268 - + New 269 - </Link> 257 + {canDo("kanban", "create") && ( 258 + <Link href={spherePath("/flux?new=1")} class={s.widgetNewBtn}> 259 + + New 260 + </Link> 261 + )} 270 262 <Link href={spherePath("/flux")} class={s.widgetLink}> 271 263 Open &rarr; 272 264 </Link> ··· 303 295 )} 304 296 </div> 305 297 <div class={s.listRightMeta}> 306 - <span class={FLUX_STATUS_PILL[task.statusType] ?? s.statusPillNeutral}> 298 + <span 299 + class={s.statusIndicator} 300 + data-status={FLUX_INDICATOR_STATUS[task.statusType]} 301 + > 302 + <span 303 + class={s.statusDot} 304 + data-status={FLUX_INDICATOR_STATUS[task.statusType]} 305 + /> 307 306 {task.statusLabel} 308 307 </span> 309 308 </div>
+11 -3
packages/kanban/src/ui/pages/task.tsx
··· 36 36 return cols.find((c) => c.slug === value)?.label ?? value; 37 37 } 38 38 39 + function StatusIndicator({ status, cols }: { status: string; cols: KanbanColumnDef[] }) { 40 + const col = cols.find((c) => c.slug === status); 41 + return ( 42 + <span class={kbUi.statusIndicator} data-status={col?.statusType}> 43 + <span class={kbUi.statusDot} data-status={col?.statusType} /> 44 + {col?.label ?? status} 45 + </span> 46 + ); 47 + } 48 + 39 49 // ---- Comment components ---- 40 50 41 51 function CommentForm({ ··· 596 606 ))} 597 607 </select> 598 608 ) : ( 599 - <span class={ui.badge}> 600 - {statusLabel(localStatus.value ?? task.status, cols)} 601 - </span> 609 + <StatusIndicator status={localStatus.value ?? task.status} cols={cols} /> 602 610 )} 603 611 </div> 604 612 </div>
+33
packages/kanban/src/ui/ui.css.ts
··· 189 189 }, 190 190 }); 191 191 192 + // ---- Status indicator (dot + label) ---- 193 + 194 + export const statusIndicator = style({ 195 + display: "inline-flex", 196 + alignItems: "center", 197 + gap: "6px", 198 + fontSize: "0.8125rem", 199 + fontWeight: 500, 200 + whiteSpace: "nowrap", 201 + flexShrink: 0, 202 + color: vars.color.textMuted, 203 + }); 204 + 205 + export const statusDot = style({ 206 + display: "inline-block", 207 + inlineSize: "6px", 208 + blockSize: "6px", 209 + borderRadius: "50%", 210 + backgroundColor: vars.color.textMuted, 211 + }); 212 + 213 + globalStyle(`${statusDot}[data-status="planned"]`, { backgroundColor: vars.color.primary }); 214 + globalStyle(`${statusIndicator}[data-status="planned"]`, { color: vars.color.primary }); 215 + 216 + globalStyle(`${statusDot}[data-status="started"]`, { backgroundColor: vars.color.warning }); 217 + globalStyle(`${statusIndicator}[data-status="started"]`, { color: vars.color.warning }); 218 + 219 + globalStyle(`${statusDot}[data-status="completed"]`, { backgroundColor: vars.color.success }); 220 + globalStyle(`${statusIndicator}[data-status="completed"]`, { color: vars.color.success }); 221 + 222 + globalStyle(`${statusDot}[data-status="canceled"]`, { backgroundColor: vars.color.danger }); 223 + globalStyle(`${statusIndicator}[data-status="canceled"]`, { color: vars.color.danger }); 224 + 192 225 // ---- Status select (compact) ---- 193 226 194 227 export const statusSelect = style({