From d8b3aa9382e98eba9609cb1344846c4354d0d4b6 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 16:34:07 -0400 Subject: [PATCH] wip: zen --- .../[id]/members/member-section.module.css | 137 +++++++++++- .../workspace/[id]/members/member-section.tsx | 209 +++++++++++------- 2 files changed, 262 insertions(+), 84 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css index 4d142c48..d67a29eb 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css @@ -132,7 +132,6 @@ position: absolute; top: 100%; left: 0; - right: 0; z-index: 10; margin-top: var(--space-1); padding: var(--space-1); @@ -140,6 +139,8 @@ border: 1px solid var(--color-border); border-radius: var(--border-radius-sm); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-width: 280px; + width: max-content; [data-slot="item"] { display: block; @@ -199,6 +200,14 @@ font-weight: normal; color: var(--color-text-muted); text-transform: uppercase; + + &:nth-child(2) { + width: 180px; + } + + &:nth-child(3) { + width: 200px; + } } td { @@ -216,6 +225,94 @@ &[data-slot="member-role"] { font-family: var(--font-mono); + [data-slot="role-selector"] { + position: relative; + + [data-slot="trigger"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); + width: 100%; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + line-height: 1.5; + cursor: pointer; + transition: all 0.15s ease; + font-family: var(--font-sans); + + &:hover { + border-color: var(--color-accent); + } + + &:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); + } + + [data-slot="chevron"] { + opacity: 0.6; + transition: transform 0.15s ease; + } + } + + [data-slot="dropdown"] { + position: absolute; + top: 100%; + left: 0; + z-index: 10; + margin-top: var(--space-1); + padding: var(--space-1); + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-width: 280px; + width: max-content; + + [data-slot="item"] { + display: block; + width: 100%; + padding: var(--space-2) var(--space-3); + border: none; + background-color: transparent; + color: var(--color-text); + font-size: var(--font-size-sm); + text-align: left; + cursor: pointer; + border-radius: var(--border-radius-sm); + transition: background-color 0.15s ease; + + &:hover { + background-color: var(--color-bg-surface); + } + + &[data-selected="true"] { + background-color: var(--color-accent-alpha); + } + + div { + strong { + display: block; + color: var(--color-text); + margin-bottom: var(--space-1); + } + + p { + font-size: var(--font-size-xs); + color: var(--color-text-muted); + margin: 0; + } + } + } + } + } + button { display: flex; align-items: center; @@ -248,6 +345,30 @@ } } + &[data-slot="member-usage"] { + input { + width: 100%; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + line-height: 1.5; + font-family: var(--font-mono); + + &:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); + } + + &::placeholder { + color: var(--color-text-disabled); + } + } + } + &[data-slot="member-date"] { color: var(--color-text); } @@ -257,7 +378,17 @@ display: flex; gap: var(--space-2); - form button { + [data-slot="inline-edit-form"] { + display: flex; + gap: var(--space-2); + + button { + opacity: 1; + pointer-events: auto; + } + } + + form:not([data-slot="inline-edit-form"]) button { opacity: 0; pointer-events: none; transition: opacity 0.15s ease; @@ -267,7 +398,7 @@ tbody tr { &:hover { - [data-slot="member-actions"] form button { + [data-slot="member-actions"] form:not([data-slot="inline-edit-form"]) button { opacity: 1; pointer-events: auto; } diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index 8cbff503..99408d51 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx @@ -86,17 +86,50 @@ const updateMember = action(async (form: FormData) => { }, "member.update") function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) { - const [editing, setEditing] = createSignal(false) const submission = useSubmission(updateMember) const isCurrentUser = () => props.actorID === props.member.id const isAdmin = () => props.actorRole === "admin" + const [store, setStore] = createStore({ + editing: false, + selectedRole: props.member.role as (typeof UserRole)[number], + showRoleDropdown: false, + limit: "", + }) + + let roleDropdownRef: HTMLDivElement | undefined createEffect(() => { if (!submission.pending && submission.result && !submission.result.error) { - setEditing(false) + setStore("editing", false) } }) + createEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (roleDropdownRef && !roleDropdownRef.contains(event.target as Node)) { + setStore("showRoleDropdown", false) + } + } + + document.addEventListener("click", handleClickOutside) + onCleanup(() => document.removeEventListener("click", handleClickOutside)) + }) + + function show() { + while (true) { + submission.clear() + if (!submission.result) break + } + setStore("editing", true) + setStore("selectedRole", props.member.role) + setStore("limit", props.member.monthlyLimit?.toString() ?? "") + } + + function hide() { + setStore("editing", false) + setStore("showRoleDropdown", false) + } + function getUsageDisplay() { const currentUsage = (() => { const dateLastUsed = props.member.timeMonthlyUsageUpdated @@ -120,96 +153,110 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a return `$${currentUsage} / ${limit}` } - return ( - - {props.member.accountEmail ?? props.member.email} - {props.member.role} - {getUsageDisplay()} - {props.member.timeSeen ? "" : "invited"} - - - - -
- - - -
-
- -
- - } - > - - -
-
{props.member.accountEmail ?? props.member.email}
- - + const roleLabels = { + admin: { title: "Admin", description: "Can manage models, members, and billing" }, + member: { title: "Member", description: "Can only generate API keys for themselves" }, + } - -
Role: {props.member.role}
- - - } + return ( + + {props.member.accountEmail ?? props.member.email} + + {props.member.role}}> +
+ + +
+ +
- -
- -
- - - {(err) =>
{err()}
} -
- -
- -
+ + + + {getUsageDisplay()}}> + setStore("limit", e.currentTarget.value)} + placeholder="No limit" + min="0" + /> + + + {props.member.timeSeen ? "" : "invited"} + + + + + + + + + + + + + } + > +
+ + + + + -
- + + + + +
- -
+
+ ) } @@ -370,7 +417,7 @@ export function MemberSection() { Email Role - Usage + Limit