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: unify feature request card and description into single block

The description now sits inside the same card as the header,
with a tinted background zone, giving it clear visual hierarchy
over comments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Hugo 0025ecd3 8a092256

+105 -60
+1 -1
packages/feature-requests/src/ui/components/request-card.tsx
··· 45 45 const voteClasses = `${frUi.voteButton}${hasVoted ? ` ${frUi.voteButtonActive}` : ""}`; 46 46 47 47 return ( 48 - <div class={`${ui.card} ${frUi.cardWithVote}`}> 48 + <div class={isDetail ? frUi.cardWithVote : `${ui.card} ${frUi.cardWithVote}`}> 49 49 {isAuthenticated ? ( 50 50 <button 51 51 class={voteClasses}
+61 -57
packages/feature-requests/src/ui/pages/feature-request.tsx
··· 676 676 <p class={ui.muted}>Feature request not found.</p> 677 677 ) : ( 678 678 <div class={frUi.detailContent}> 679 - <div class={ui.stackLg}> 680 - <RequestCard 681 - fr={fr} 682 - hasVoted={votedIds.value.has(fr.id)} 683 - isAuthenticated={isAuthenticated} 684 - isDetail 685 - onVote={toggleVote} 686 - onUnvote={toggleVote} 687 - statusSlot={ 688 - canChangeStatus.value || currentDid === fr.authorDid || canModerate.value ? ( 689 - <div class={ui.cluster}> 690 - {currentDid === fr.authorDid && ( 691 - <button class={ui.buttonDangerInline} onClick={handleDelete}> 692 - Delete 693 - </button> 694 - )} 695 - {canModerate.value && currentDid !== fr.authorDid && ( 696 - <button class={ui.buttonDangerInline} onClick={handleHide}> 697 - Hide 698 - </button> 699 - )} 700 - {canChangeStatus.value ? ( 701 - <select 702 - class={frUi.sortSelect} 703 - value={fr.status} 704 - onChange={(e) => handleStatusChange((e.target as HTMLSelectElement).value)} 705 - > 706 - {fr.status === "duplicate" && ( 707 - <option value="duplicate">{statusLabels.duplicate}</option> 708 - )} 709 - {settableStatuses.map((s) => ( 710 - <option key={s} value={s}> 711 - {statusLabels[s]} 712 - </option> 713 - ))} 714 - </select> 715 - ) : ( 716 - <span class={frUi.statusIndicator} data-status={fr.status}> 717 - <span class={frUi.statusDot} data-status={fr.status} /> 718 - {statusLabels[fr.status as Status] ?? fr.status} 719 - </span> 720 - )} 721 - </div> 722 - ) : undefined 723 - } 724 - > 725 - {canChangeStatus.value && availableLabels.value.length > 0 ? ( 726 - <InlineLabelEditor 727 - labels={availableLabels.value} 728 - selectedIds={localLabelIds.value ?? fr.labels.map((l) => l.id)} 729 - onToggle={handleLabelToggle} 730 - /> 731 - ) : null} 732 - </RequestCard> 679 + <div class={frUi.detailCardWrapper}> 680 + <div class={frUi.detailCardHeader}> 681 + <RequestCard 682 + fr={fr} 683 + hasVoted={votedIds.value.has(fr.id)} 684 + isAuthenticated={isAuthenticated} 685 + isDetail 686 + onVote={toggleVote} 687 + onUnvote={toggleVote} 688 + statusSlot={ 689 + canChangeStatus.value || currentDid === fr.authorDid || canModerate.value ? ( 690 + <div class={ui.cluster}> 691 + {currentDid === fr.authorDid && ( 692 + <button class={ui.buttonDangerInline} onClick={handleDelete}> 693 + Delete 694 + </button> 695 + )} 696 + {canModerate.value && currentDid !== fr.authorDid && ( 697 + <button class={ui.buttonDangerInline} onClick={handleHide}> 698 + Hide 699 + </button> 700 + )} 701 + {canChangeStatus.value ? ( 702 + <select 703 + class={frUi.sortSelect} 704 + value={fr.status} 705 + onChange={(e) => handleStatusChange((e.target as HTMLSelectElement).value)} 706 + > 707 + {fr.status === "duplicate" && ( 708 + <option value="duplicate">{statusLabels.duplicate}</option> 709 + )} 710 + {settableStatuses.map((s) => ( 711 + <option key={s} value={s}> 712 + {statusLabels[s]} 713 + </option> 714 + ))} 715 + </select> 716 + ) : ( 717 + <span class={frUi.statusIndicator} data-status={fr.status}> 718 + <span class={frUi.statusDot} data-status={fr.status} /> 719 + {statusLabels[fr.status as Status] ?? fr.status} 720 + </span> 721 + )} 722 + </div> 723 + ) : undefined 724 + } 725 + > 726 + {canChangeStatus.value && availableLabels.value.length > 0 ? ( 727 + <InlineLabelEditor 728 + labels={availableLabels.value} 729 + selectedIds={localLabelIds.value ?? fr.labels.map((l) => l.id)} 730 + onToggle={handleLabelToggle} 731 + /> 732 + ) : null} 733 + </RequestCard> 734 + </div> 735 + 736 + {fr.description && ( 737 + <div class={frUi.descriptionBlock}>{fr.description}</div> 738 + )} 733 739 734 740 {data.duplicateOf && ( 735 - <p class={ui.muted}> 741 + <p class={`${ui.muted} ${frUi.duplicateNotice}`}> 736 742 Duplicate of{" "} 737 743 <a href={spherePath(`/infuse/${data.duplicateOf.number}`)}> 738 744 #{data.duplicateOf.number} &mdash; {data.duplicateOf.title} ··· 740 746 </p> 741 747 )} 742 748 </div> 743 - 744 - <div class={frUi.descriptionBlock}>{fr.description}</div> 745 749 746 750 <CommentsSection 747 751 requestId={fr.id}
+43 -2
packages/feature-requests/src/ui/ui.css.ts
··· 1 1 import { globalStyle, style } from "@vanilla-extract/css"; 2 2 import { vars } from "@exosphere/client/theme.css"; 3 3 4 + const bp = { 5 + sm: "screen and (min-width: 480px)", 6 + }; 7 + 4 8 // ---- Vote buttons ---- 5 9 6 10 const voteBase = { ··· 63 67 gap: vars.space.xl, 64 68 }); 65 69 70 + export const detailCardWrapper = style({ 71 + border: `1px solid ${vars.color.border}`, 72 + borderRadius: vars.radius.lg, 73 + boxShadow: `0 1px 3px ${vars.color.shadow}, 0 1px 2px ${vars.color.shadow}`, 74 + overflow: "hidden", 75 + }); 76 + 77 + export const detailCardHeader = style({ 78 + backgroundColor: vars.color.surface, 79 + paddingBlock: vars.space.md, 80 + paddingInline: vars.space.md, 81 + "@media": { 82 + [bp.sm]: { 83 + paddingInline: vars.space.lg, 84 + }, 85 + }, 86 + }); 87 + 66 88 export const descriptionBlock = style({ 67 89 fontSize: "0.9375rem", 68 90 lineHeight: 1.7, 69 91 whiteSpace: "pre-wrap", 70 - paddingBlockEnd: vars.space.xl, 71 - borderBlockEnd: `1px solid ${vars.color.border}`, 92 + paddingBlock: vars.space.md, 93 + paddingInline: vars.space.md, 94 + backgroundColor: vars.color.bg, 95 + borderBlockStart: `1px solid ${vars.color.border}`, 96 + "@media": { 97 + [bp.sm]: { 98 + paddingInline: vars.space.lg, 99 + }, 100 + }, 101 + }); 102 + 103 + export const duplicateNotice = style({ 104 + paddingBlock: vars.space.sm, 105 + paddingInline: vars.space.md, 106 + backgroundColor: vars.color.bg, 107 + borderBlockStart: `1px solid ${vars.color.border}`, 108 + "@media": { 109 + [bp.sm]: { 110 + paddingInline: vars.space.lg, 111 + }, 112 + }, 72 113 }); 73 114 74 115 // ---- Title row ----