summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-10-10 13:48:56 -0400
committerFrank <[email protected]>2025-10-10 13:48:56 -0400
commit94d0a3d888c3b5f5cfc4fcf5f275885128a968e6 (patch)
tree5752ec1a32886c430efbcafef3ba352de501bbb4
parentd83af721a6e6269481186df80d498aa1213a6e59 (diff)
downloadopencode-94d0a3d888c3b5f5cfc4fcf5f275885128a968e6.tar.gz
opencode-94d0a3d888c3b5f5cfc4fcf5f275885128a968e6.zip
wip: zen style members
-rw-r--r--packages/console/app/src/routes/workspace/[id]/members/member-section.module.css47
-rw-r--r--packages/console/app/src/routes/workspace/[id]/members/member-section.tsx144
2 files changed, 112 insertions, 79 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 16b6ff8d2..14246d585 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
@@ -1,4 +1,11 @@
.root {
+ [data-slot="title-row"] {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-4);
+ }
+
[data-component="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
@@ -64,6 +71,46 @@
margin-top: var(--space-1);
line-height: 1.4;
}
+
+ [data-slot="role-selector"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+
+ label {
+ display: flex;
+ gap: var(--space-3);
+ padding: var(--space-3);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ cursor: pointer;
+
+ &:hover {
+ background-color: var(--color-bg-surface);
+ }
+
+ input[type="radio"] {
+ margin-top: var(--space-1);
+ }
+
+ div {
+ flex: 1;
+
+ strong {
+ display: block;
+ color: var(--color-text);
+ font-family: var(--font-sans);
+ margin-bottom: var(--space-1);
+ }
+
+ p {
+ font-size: var(--font-size-xs);
+ color: var(--color-text-muted);
+ font-family: var(--font-sans);
+ }
+ }
+ }
+ }
}
[data-slot="members-table"] {
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 cfc8b5e71..e9f617248 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
@@ -81,83 +81,6 @@ const updateMember = action(async (form: FormData) => {
)
}, "member.update")
-export function MemberCreateForm() {
- const params = useParams()
- const submission = useSubmission(inviteMember)
- const [store, setStore] = createStore({ show: false })
-
- let input: HTMLInputElement
-
- createEffect(() => {
- if (!submission.pending && submission.result && !submission.result.error) {
- hide()
- }
- })
-
- function show() {
- // submission.clear() does not clear the result in some cases, ie.
- // 1. Create key with empty name => error shows
- // 2. Put in a key name and creates the key => form hides
- // 3. Click add key button again => form shows with the same error if
- // submission.clear() is called only once
- while (true) {
- submission.clear()
- if (!submission.result) break
- }
- setStore("show", true)
- input.focus()
- }
-
- function hide() {
- setStore("show", false)
- }
-
- return (
- <Show
- when={store.show}
- fallback={
- <button data-color="primary" onClick={() => show()}>
- Invite Member
- </button>
- }
- >
- <form action={inviteMember} method="post" data-slot="create-form">
- <div data-slot="input-container">
- <input ref={(r) => (input = r)} data-component="input" name="email" type="text" placeholder="Enter email" />
- <div data-slot="role-selector">
- <label>
- <input type="radio" name="role" value="admin" checked />
- <div>
- <strong>Admin</strong>
- <p>Can manage models, members, and billing</p>
- </div>
- </label>
- <label>
- <input type="radio" name="role" value="member" />
- <div>
- <strong>Member</strong>
- <p>Can only generate API keys for themselves</p>
- </div>
- </label>
- </div>
- <Show when={submission.result && submission.result.error}>
- {(err) => <div data-slot="form-error">{err()}</div>}
- </Show>
- </div>
- <input type="hidden" name="workspaceID" value={params.id} />
- <div data-slot="form-actions">
- <button type="reset" data-color="ghost" onClick={() => hide()}>
- Cancel
- </button>
- <button type="submit" data-color="primary" disabled={submission.pending}>
- {submission.pending ? "Inviting..." : "Invite"}
- </button>
- </div>
- </form>
- </Show>
- )
-}
-
function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) {
const [editing, setEditing] = createSignal(false)
const submission = useSubmission(updateMember)
@@ -289,14 +212,77 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a
export function MemberSection() {
const params = useParams()
const data = createAsync(() => listMembers(params.id))
+ const submission = useSubmission(inviteMember)
+ const [store, setStore] = createStore({ show: false })
+
+ let input: HTMLInputElement
+
+ createEffect(() => {
+ if (!submission.pending && submission.result && !submission.result.error) {
+ setStore("show", false)
+ }
+ })
+
+ function show() {
+ while (true) {
+ submission.clear()
+ if (!submission.result) break
+ }
+ setStore("show", true)
+ setTimeout(() => input?.focus(), 0)
+ }
+
+ function hide() {
+ setStore("show", false)
+ }
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>Members</h2>
+ <div data-slot="title-row">
+ <p>Manage workspace members and their permissions.</p>
+ <Show when={data()?.actorRole === "admin"}>
+ <button data-color="primary" onClick={() => show()}>
+ Invite Member
+ </button>
+ </Show>
+ </div>
</div>
- <Show when={data()?.actorRole === "admin"}>
- <MemberCreateForm />
+ <Show when={store.show}>
+ <form action={inviteMember} method="post" data-slot="create-form">
+ <div data-slot="input-container">
+ <input ref={(r) => (input = r)} data-component="input" name="email" type="text" placeholder="Enter email" />
+ <div data-slot="role-selector">
+ <label>
+ <input type="radio" name="role" value="admin" checked />
+ <div>
+ <strong>Admin</strong>
+ <p>Can manage models, members, and billing</p>
+ </div>
+ </label>
+ <label>
+ <input type="radio" name="role" value="member" />
+ <div>
+ <strong>Member</strong>
+ <p>Can only generate API keys for themselves</p>
+ </div>
+ </label>
+ </div>
+ <Show when={submission.result && submission.result.error}>
+ {(err) => <div data-slot="form-error">{err()}</div>}
+ </Show>
+ </div>
+ <input type="hidden" name="workspaceID" value={params.id} />
+ <div data-slot="form-actions">
+ <button type="reset" data-color="ghost" onClick={() => hide()}>
+ Cancel
+ </button>
+ <button type="submit" data-color="primary" disabled={submission.pending}>
+ {submission.pending ? "Inviting..." : "Invite"}
+ </button>
+ </div>
+ </form>
</Show>
<div data-slot="members-table">
<table data-slot="members-table-element">