chore: harden reliability checks #2

Merged
nalalangan merged 67 commits from main-new into main 2026-05-25 14:28:32 -09:00
2 changed files with 464 additions and 210 deletions
Showing only changes of commit 043460ac21 - Show all commits

View File

@ -28,6 +28,20 @@ const JOIN_POLICY_OPTIONS = [
{ label: "Manual", value: "APPROVAL_REQUIRED" }, { label: "Manual", value: "APPROVAL_REQUIRED" },
]; ];
const ROLE_METADATA = {
owner: { icon: "👑", label: "Owner" },
admin: { icon: "🛠️", label: "Admin" },
member: { icon: "🙂", label: "Member" },
viewer: { icon: "👀", label: "Viewer" },
};
const STATUS_METADATA = {
Active: { tone: "active", icon: "🟢" },
Used: { tone: "used", icon: "⚪" },
Revoked: { tone: "revoked", icon: "🔴" },
Expired: { tone: "expired", icon: "🟠" },
};
export default function ManageHousehold() { export default function ManageHousehold() {
const { userId } = useContext(AuthContext); const { userId } = useContext(AuthContext);
const { activeHousehold, refreshHouseholds } = useContext(HouseholdContext); const { activeHousehold, refreshHouseholds } = useContext(HouseholdContext);
@ -309,10 +323,21 @@ export default function ManageHousehold() {
); );
}; };
const managerCount = members.filter((member) => ["owner", "admin"].includes(member.role)).length;
const memberCount = members.filter((member) => member.role === "member").length;
return ( return (
<div className="manage-household"> <div className="manage-household">
<section key="household-name" className="manage-section"> <section key="household-name" className="manage-section">
<h2>Household Name</h2> <div className="manage-section-header">
<div>
<p className="manage-section-eyebrow">Household</p>
<h2>Identity</h2>
<p className="section-description">
Keep the household name crisp and easy to recognize across invites and shared lists.
</p>
</div>
</div>
{editingName ? ( {editingName ? (
<div className="edit-name-form"> <div className="edit-name-form">
<input <input
@ -327,7 +352,14 @@ export default function ManageHousehold() {
</div> </div>
) : ( ) : (
<div className="name-display"> <div className="name-display">
<h3>{activeHousehold.name}</h3> <div className="name-display-copy">
<h3>{activeHousehold.name}</h3>
<div className="household-summary-chips">
<span className="household-summary-chip">🏠 {members.length} people</span>
<span className="household-summary-chip">🛡 {managerCount} managers</span>
<span className="household-summary-chip">🛒 {memberCount} shoppers</span>
</div>
</div>
{isManager && ( {isManager && (
<button <button
onClick={() => { onClick={() => {
@ -345,30 +377,48 @@ export default function ManageHousehold() {
{isManager && ( {isManager && (
<section key="invite-code" className="manage-section"> <section key="invite-code" className="manage-section">
<h2>Legacy Invite Code</h2> <div className="manage-section-header">
<p className="section-description"> <div>
Share this code for legacy join-by-code flows. <p className="manage-section-eyebrow">Legacy Access</p>
</p> <h2>Invite Code</h2>
<p className="section-description">
Keep a simple code handy for older join flows while newer invite links stay fully managed below.
</p>
</div>
</div>
<div className="invite-actions"> <div className="invite-actions">
<button onClick={() => setShowInviteCode(!showInviteCode)} className="btn-secondary"> <div className="invite-code-panel">
{showInviteCode ? "Hide Code" : "Show Code"} <div className="invite-code-copy">
</button> <span className="invite-code-label">Current code</span>
{showInviteCode && ( <code className="invite-code">{showInviteCode ? activeHousehold.invite_code : "••••••••"}</code>
<React.Fragment key="invite-code-display"> </div>
<code className="invite-code">{activeHousehold.invite_code}</code> <div className="invite-action-group">
<button onClick={copyInviteCode} className="btn-secondary">Copy</button> <button onClick={() => setShowInviteCode(!showInviteCode)} className="btn-secondary">
</React.Fragment> {showInviteCode ? "Hide Code" : "Show Code"}
)} </button>
<button onClick={handleRefreshInvite} className="btn-secondary"> <button onClick={copyInviteCode} className="btn-secondary" disabled={!showInviteCode}>
Generate New Code Copy
</button> </button>
<button onClick={handleRefreshInvite} className="btn-secondary">
Generate New Code
</button>
</div>
</div>
</div> </div>
</section> </section>
)} )}
{isManager && ( {isManager && (
<section key="join-and-invites" className="manage-section"> <section key="join-and-invites" className="manage-section">
<h2>Join and Invites</h2> <div className="manage-section-header">
<div>
<p className="manage-section-eyebrow">Entry Rules</p>
<h2>Join and Invites</h2>
<p className="section-description">
Decide how new people can enter, then generate compact links that match your current policy.
</p>
</div>
</div>
{inviteError && <p className="section-error">{inviteError}</p>} {inviteError && <p className="section-error">{inviteError}</p>}
<ToggleButtonGroup <ToggleButtonGroup
@ -384,7 +434,7 @@ export default function ManageHousehold() {
<div className="invite-controls"> <div className="invite-controls">
<label> <label>
TTL <span className="invite-control-label">TTL</span>
<select value={ttlDays} onChange={(e) => setTtlDays(Number(e.target.value))}> <select value={ttlDays} onChange={(e) => setTtlDays(Number(e.target.value))}>
{[1, 2, 3, 4, 5, 6, 7].map((day) => ( {[1, 2, 3, 4, 5, 6, 7].map((day) => (
<option key={day} value={day}>{day} day{day > 1 ? "s" : ""}</option> <option key={day} value={day}>{day} day{day > 1 ? "s" : ""}</option>
@ -392,7 +442,7 @@ export default function ManageHousehold() {
</select> </select>
</label> </label>
<label> <label>
Usage <span className="invite-control-label">Usage</span>
<select value={singleUseMode} onChange={(e) => setSingleUseMode(e.target.value)}> <select value={singleUseMode} onChange={(e) => setSingleUseMode(e.target.value)}>
<option value="UNLIMITED">Unlimited</option> <option value="UNLIMITED">Unlimited</option>
<option value="ONE_TIME">1 use</option> <option value="ONE_TIME">1 use</option>
@ -412,12 +462,18 @@ export default function ManageHousehold() {
{inviteLinks.map((link) => { {inviteLinks.map((link) => {
const status = getLinkStatus(link); const status = getLinkStatus(link);
const isActive = status === "Active"; const isActive = status === "Active";
const statusMeta = STATUS_METADATA[status] || STATUS_METADATA.Active;
return ( return (
<div key={link.id} className="invite-link-card"> <div key={link.id} className="invite-link-card">
<div> <div className="invite-link-main">
<p className="invite-link-token">Token ending in {String(link.token).slice(-4)}</p> <div className="invite-link-topline">
<p className="invite-link-token">Invite ending in {String(link.token).slice(-4)}</p>
<span className={`invite-status-badge is-${statusMeta.tone}`}>
{statusMeta.icon} {status}
</span>
</div>
<p className="invite-link-meta"> <p className="invite-link-meta">
Status: <strong>{status}</strong> | Policy: {link.policy} | TTL: until {new Date(link.expires_at).toLocaleString()} Policy: {link.policy} Expires {new Date(link.expires_at).toLocaleString()}
</p> </p>
</div> </div>
<div className="invite-link-actions"> <div className="invite-link-actions">
@ -446,58 +502,81 @@ export default function ManageHousehold() {
)} )}
<section key="members" className="manage-section"> <section key="members" className="manage-section">
<h2>Members ({members.length})</h2> <div className="manage-section-header">
<div>
<p className="manage-section-eyebrow">People</p>
<h2>Members ({members.length})</h2>
<p className="section-description">
Role badges and compact actions make it easier to see who runs the household and who just shops.
</p>
</div>
</div>
{loading ? ( {loading ? (
<p>Loading members...</p> <p>Loading members...</p>
) : ( ) : (
<div className="members-list"> <div className="members-list">
{members.map((member) => ( {members.map((member) => {
<div key={member.id} className="member-card"> const roleMeta = ROLE_METADATA[member.role] || { icon: "👤", label: member.role };
<div className="member-info"> const isSelf = member.id === parseInt(userId, 10);
<span className="member-role">{member.role}</span>
<span className="member-name"> return (
{member.username} [{member.id}] {member.id === parseInt(userId, 10) ? "(You)" : ""} <div key={member.id} className="member-card">
</span> <div className="member-avatar" aria-hidden="true">{roleMeta.icon}</div>
</div> <div className="member-info">
{isManager && member.id !== parseInt(userId, 10) && member.role !== "owner" && ( <div className="member-topline">
<div className="member-actions"> <span className={`member-role member-role-${member.role}`}>
<button {roleMeta.icon} {roleMeta.label}
onClick={() => handleUpdateRole(member.id, member.role, member.username)} </span>
className="btn-secondary btn-small" {isSelf && <span className="member-self-pill"> You</span>}
> </div>
{member.role === "admin" ? "Make Member" : "Make Admin"} <span className="member-name">{member.username}</span>
</button> <span className="member-meta">ID #{member.id}</span>
<button
onClick={() => handleRemoveMember(member.id, member.username)}
className="btn-danger btn-small"
>
Remove
</button>
</div> </div>
)} {isManager && !isSelf && member.role !== "owner" && (
</div> <div className="member-actions">
))} <button
onClick={() => handleUpdateRole(member.id, member.role, member.username)}
className="btn-secondary btn-small"
>
{member.role === "admin" ? "Make Member" : "Make Admin"}
</button>
<button
onClick={() => handleRemoveMember(member.id, member.username)}
className="btn-danger btn-small"
>
Remove
</button>
</div>
)}
</div>
);
})}
</div> </div>
)} )}
</section> </section>
{(isManager || isMemberOnly) && ( {(isManager || isMemberOnly) && (
<section key="danger-zone" className="manage-section danger-zone"> <section key="danger-zone" className="manage-section danger-zone">
<h2>Danger Zone</h2> <div className="manage-section-header">
<p className="section-description"> <div>
{isMemberOnly <p className="manage-section-eyebrow">Final Actions</p>
? "Leaving removes your access to this household." <h2>Danger Zone</h2>
: "Deleting a household is permanent and will delete all lists, items, and history."} <p className="section-description">
</p> {isMemberOnly
{isMemberOnly ? ( ? "Leaving removes your access to this household."
<button onClick={() => setIsLeaveModalOpen(true)} className="btn-danger"> : "Deleting a household is permanent and will delete all lists, items, and history."}
Leave Household </p>
</button> </div>
) : ( {isMemberOnly ? (
<button onClick={handleDeleteHousehold} className="btn-danger"> <button onClick={() => setIsLeaveModalOpen(true)} className="btn-danger">
Delete Household Leave Household
</button> </button>
)} ) : (
<button onClick={handleDeleteHousehold} className="btn-danger">
Delete Household
</button>
)}
</div>
</section> </section>
)} )}

View File

@ -2,65 +2,117 @@
.manage-household { .manage-household {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1rem;
max-width: 800px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
} }
/* Section Styling */
.manage-section { .manage-section {
background: var(--card-bg); display: flex;
border: 1px solid var(--border); flex-direction: column;
border-radius: 8px; gap: 1rem;
padding: 2rem;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 1.2rem 1.25rem;
border: 1px solid var(--color-border-light);
border-radius: var(--border-radius-lg);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.72)),
var(--card-bg);
box-shadow: var(--shadow-sm);
} }
.manage-section h2 { .manage-section-header {
font-size: 1.3rem; display: flex;
font-weight: 600; align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.manage-section-header h2 {
margin: 0.15rem 0 0;
font-size: 1.2rem;
color: var(--text-primary); color: var(--text-primary);
margin-bottom: 1rem; }
.manage-section-eyebrow {
margin: 0;
color: var(--primary);
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
} }
.section-description { .section-description {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 0.9rem; font-size: 0.92rem;
margin-bottom: 1rem; line-height: 1.55;
margin: 0.45rem 0 0;
}
.section-error {
color: var(--danger);
margin: 0;
padding: 0.8rem 0.95rem;
border-radius: var(--border-radius-md);
border: 1px solid color-mix(in srgb, var(--danger) 28%, transparent);
background: color-mix(in srgb, var(--danger-light) 78%, white);
} }
/* Household Name Section */ /* Household Name Section */
.name-display { .name-display {
display: flex; display: flex;
flex-direction: column; align-items: flex-start;
align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
} }
.name-display-copy {
display: flex;
flex-direction: column;
gap: 0.7rem;
}
.name-display h3 { .name-display h3 {
font-size: 1.5rem; font-size: 1.55rem;
color: var(--text-primary); color: var(--text-primary);
margin: 0; margin: 0;
} }
.edit-name-form { .household-summary-chips {
display: flex; display: flex;
flex-wrap: wrap;
gap: 0.55rem;
}
.household-summary-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.75rem;
border-radius: var(--border-radius-full);
background: var(--primary-light);
color: var(--primary-dark);
font-size: 0.82rem;
font-weight: 600;
}
.edit-name-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 0.75rem; gap: 0.75rem;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
.edit-name-form input { .edit-name-form input {
flex: 1; min-width: 0;
min-width: 200px; padding: 0.85rem 1rem;
padding: 0.75rem;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: var(--border-radius-md);
font-size: 1rem; font-size: 0.98rem;
background: var(--background); background: rgba(255, 255, 255, 0.82);
color: var(--text-primary); color: var(--text-primary);
} }
@ -68,73 +120,121 @@
.invite-actions { .invite-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; }
.invite-code-panel {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.9rem;
align-items: center; align-items: center;
flex-wrap: wrap; padding: 0.95rem 1rem;
border: 1px solid var(--color-border-light);
border-radius: var(--border-radius-lg);
background: rgba(255, 255, 255, 0.58);
}
.invite-code-copy {
display: flex;
flex-direction: column;
gap: 0.3rem;
min-width: 0;
}
.invite-code-label {
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.06em;
color: var(--text-secondary);
text-transform: uppercase;
} }
.invite-code { .invite-code {
display: inline-flex;
width: fit-content;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
background: var(--background); background: var(--background);
padding: 0.75rem 1rem; padding: 0.65rem 0.8rem;
border-radius: 6px; border-radius: var(--border-radius-md);
font-family: 'Courier New', monospace; font-family: var(--font-family-mono);
font-size: 1rem; font-size: 0.95rem;
color: var(--primary); color: var(--primary);
border: 2px solid var(--border); border: 1px solid var(--border);
font-weight: 600; font-weight: 700;
letter-spacing: 0.5px; letter-spacing: 0.04em;
} }
.section-error { .invite-action-group {
color: var(--danger); display: flex;
margin: 0 0 0.75rem 0; flex-wrap: wrap;
gap: 0.55rem;
justify-content: flex-end;
} }
.manage-household-join-policy-toggle { .manage-household-join-policy-toggle {
margin-bottom: 1rem; margin-bottom: 0.2rem;
} }
.invite-controls { .invite-controls {
display: flex; display: grid;
grid-template-columns: repeat(2, minmax(140px, 180px)) auto;
gap: 0.8rem; gap: 0.8rem;
align-items: end; align-items: end;
flex-wrap: wrap;
margin-bottom: 1rem;
} }
.invite-controls label { .invite-controls label {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.3rem; gap: 0.35rem;
color: var(--text-primary); color: var(--text-primary);
font-size: 0.9rem; font-size: 0.9rem;
} }
.invite-control-label {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--text-secondary);
text-transform: uppercase;
}
.invite-controls select { .invite-controls select {
min-width: 120px; min-width: 120px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: var(--border-radius-md);
padding: 0.45rem 0.6rem; padding: 0.7rem 0.75rem;
background: var(--background); background: rgba(255, 255, 255, 0.82);
color: var(--text-primary); color: var(--text-primary);
} }
.invite-links-list { .invite-links-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.8rem; gap: 0.75rem;
} }
.invite-link-card { .invite-link-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.85rem;
align-items: center;
padding: 0.9rem 1rem;
border: 1px solid var(--border); border: 1px solid var(--border);
background: var(--background); border-radius: var(--border-radius-lg);
border-radius: 8px; background: rgba(255, 255, 255, 0.62);
padding: 0.9rem; }
.invite-link-main {
min-width: 0;
}
.invite-link-topline {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 0.6rem;
gap: 0.75rem;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 0.35rem;
} }
.invite-link-token, .invite-link-token,
@ -143,118 +243,193 @@
} }
.invite-link-token { .invite-link-token {
font-weight: 600; font-weight: 700;
color: var(--text-primary);
} }
.invite-link-meta { .invite-link-meta {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 0.85rem; font-size: 0.84rem;
line-height: 1.5;
}
.invite-status-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.28rem 0.6rem;
border-radius: var(--border-radius-full);
font-size: 0.78rem;
font-weight: 700;
}
.invite-status-badge.is-active {
background: var(--success-light);
color: var(--success);
}
.invite-status-badge.is-used {
background: color-mix(in srgb, var(--color-gray-200) 74%, white);
color: var(--color-gray-700);
}
.invite-status-badge.is-revoked {
background: var(--danger-light);
color: var(--danger);
}
.invite-status-badge.is-expired {
background: var(--color-warning-light);
color: var(--color-warning);
} }
.invite-link-actions { .invite-link-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end;
} }
/* Members Section */ /* Members Section */
.members-list { .members-list {
display: flex; display: grid;
flex-direction: column; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
gap: 1rem; gap: 0.85rem;
} }
.member-card { .member-card {
display: flex; display: grid;
flex-direction: row; grid-template-columns: auto minmax(0, 1fr);
justify-content: space-between; gap: 0.85rem;
align-items: center; align-items: flex-start;
padding: 1.25rem; padding: 0.95rem 1rem;
background: var(--background); background: rgba(255, 255, 255, 0.62);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: var(--border-radius-lg);
transition: all 0.2s; transition: all 0.2s;
gap: 1rem;
} }
.member-card:hover { .member-card:hover {
background: var(--card-hover); background: var(--card-hover);
border-color: var(--primary); border-color: var(--primary);
transform: translateY(-1px);
}
.member-avatar {
width: 2.6rem;
height: 2.6rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: var(--primary-light);
font-size: 1.15rem;
} }
.member-info { .member-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: 0.35rem;
min-width: 0;
}
.member-topline {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
} }
.member-name { .member-name {
font-weight: 500; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
font-size: 1rem; font-size: 1rem;
} }
.member-meta {
color: var(--text-secondary);
font-size: 0.82rem;
}
.member-role { .member-role {
font-size: 0.85rem; display: inline-flex;
padding: 0.2rem 0.5rem; align-items: center;
border-radius: 4px; gap: 0.35rem;
font-size: 0.78rem;
padding: 0.24rem 0.55rem;
border-radius: var(--border-radius-full);
width: fit-content; width: fit-content;
text-transform: capitalize; text-transform: capitalize;
background: var(--primary-light, rgba(0, 122, 255, 0.1)); font-weight: 700;
color: var(--primary); }
.member-role-owner {
background: rgba(245, 158, 11, 0.18);
color: #b45309;
}
.member-role-admin {
background: rgba(30, 144, 255, 0.16);
color: var(--primary-dark);
}
.member-role-member,
.member-role-viewer {
background: rgba(139, 92, 246, 0.12);
color: #6d28d9;
}
.member-self-pill {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.24rem 0.5rem;
border-radius: var(--border-radius-full);
background: rgba(245, 158, 11, 0.16);
color: #a16207;
font-size: 0.75rem;
font-weight: 700;
} }
.member-actions { .member-actions {
display: flex; display: flex;
gap: 0.75rem; gap: 0.55rem;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 0.4rem;
} }
/* Danger Zone */ /* Danger Zone */
.danger-zone { .danger-zone {
border-color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, transparent);
background:
linear-gradient(180deg, rgba(254, 242, 242, 0.95), rgba(255, 255, 255, 0.78)),
var(--card-bg);
} }
.danger-zone h2 { .danger-zone h2,
.danger-zone .manage-section-eyebrow {
color: var(--danger); color: var(--danger);
} }
.danger-zone .manage-section-header {
align-items: center;
}
/* Buttons */ /* Buttons */
.btn-primary, .btn-primary,
.btn-secondary, .btn-secondary,
.btn-danger { .btn-danger {
padding: 0.5rem 1rem; min-height: 40px;
border: none; padding: 0.58rem 0.95rem;
border-radius: 6px; border-radius: var(--border-radius-full);
font-size: 0.9rem; font-size: 0.88rem;
font-weight: 500; font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary,
.btn-secondary {
background: var(--primary);
color: white;
}
.btn-primary:hover,
.btn-secondary:hover {
background: var(--primary-dark, #0056b3);
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-danger:hover {
background: var(--danger-dark, #c82333);
} }
.btn-small { .btn-small {
padding: 0.4rem 0.75rem; min-height: 34px;
font-size: 0.85rem; padding: 0.38rem 0.72rem;
font-size: 0.8rem;
} }
.btn-danger:disabled { .btn-danger:disabled {
@ -264,62 +439,54 @@
} }
/* Responsive */ /* Responsive */
@media (max-width: 900px) {
.invite-controls {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.invite-controls .btn-primary {
grid-column: 1 / -1;
}
.invite-link-card {
grid-template-columns: 1fr;
}
.invite-link-actions {
justify-content: flex-start;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.manage-section { .manage-section {
padding: 1.25rem; padding: 1rem;
} }
.name-display { .manage-section-header,
.name-display,
.danger-zone .manage-section-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: stretch;
} }
.edit-name-form { .edit-name-form {
flex-direction: column; grid-template-columns: 1fr;
width: 100%;
align-items: stretch;
} }
.edit-name-form input { .invite-code-panel {
width: 100%; grid-template-columns: 1fr;
min-width: unset;
}
.edit-name-form button {
width: 100%;
} }
.member-card { .invite-action-group {
flex-direction: column; justify-content: stretch;
align-items: flex-start;
gap: 1rem;
} }
.member-actions { .invite-action-group button {
width: 100%; flex: 1 1 100%;
}
.member-actions button {
flex: 1;
}
.invite-actions {
flex-direction: column;
align-items: stretch;
}
.invite-actions button {
width: 100%;
}
.invite-code {
text-align: center;
width: 100%;
} }
.invite-controls { .invite-controls {
flex-direction: column; grid-template-columns: 1fr;
align-items: stretch;
} }
.invite-controls label, .invite-controls label,
@ -328,11 +495,19 @@
width: 100%; width: 100%;
} }
.invite-link-actions { .members-list {
grid-template-columns: 1fr;
}
.member-card {
grid-template-columns: auto 1fr;
}
.member-actions {
width: 100%; width: 100%;
} }
.invite-link-actions button { .member-actions button {
flex: 1; flex: 1 1 100%;
} }
} }