feat(den): add shared select field (#1299)

* feat(den): add shared select field

* remove screenshots from repo

---------

Co-authored-by: Source Open <gh2@mcadam.io>
This commit is contained in:
terrikramer
2026-04-03 08:43:42 -07:00
committed by GitHub
parent b4309d4965
commit 941ebd2303
4 changed files with 67 additions and 17 deletions

View File

@@ -27,7 +27,7 @@ export type DenInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "disable
* Consistent text input for all dashboard pages, based on the
* Shared Workspaces compact search field.
*
* Defaults: rounded-lg · py-2.5 · px-4 · text-[14px]
* Defaults: rounded-lg · h-[42px] · px-4 · text-[14px]/leading-5
* Icon: auto-positions and adjusts left padding.
* No className needed at the call site — override only when necessary.
*/
@@ -56,7 +56,7 @@ export function DenInput({
className={[
// base visual style
"w-full rounded-lg border border-gray-200 bg-white",
"py-2.5 px-4 text-[14px] text-gray-900",
"h-[42px] px-4 text-[14px] leading-5 text-gray-900",
"outline-none transition-all placeholder:text-gray-400",
"focus:border-gray-300 focus:ring-2 focus:ring-gray-900/5",
// disabled state

View File

@@ -0,0 +1,57 @@
"use client";
import { ChevronDown } from "lucide-react";
import type { SelectHTMLAttributes } from "react";
export type DenSelectProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, "disabled"> & {
/**
* Disables the select and dims it to 60 % opacity.
* Forwarded as the native `disabled` attribute.
*/
disabled?: boolean;
};
/**
* DenSelect
*
* Consistent native select for all dashboard pages, matched to the
* Shared Workspaces compact field sizing used by DenInput.
*
* Defaults: rounded-lg · h-[42px] · px-4/pr-10 · text-[14px]/leading-5
* Chevron: custom Lucide chevron replaces browser-native control chrome.
* No className needed at the call site - override only when necessary.
*/
export function DenSelect({
disabled = false,
className,
children,
...rest
}: DenSelectProps) {
return (
<div className="relative">
<select
{...rest}
disabled={disabled}
className={[
"w-full appearance-none rounded-lg border border-gray-200 bg-white",
"h-[42px] px-4 pr-10 text-[14px] leading-5 text-gray-900",
"outline-none transition-all",
"focus:border-gray-300 focus:ring-2 focus:ring-gray-900/5",
disabled ? "cursor-not-allowed opacity-60" : "",
className ?? "",
]
.filter(Boolean)
.join(" ")}
>
{children}
</select>
<div className="pointer-events-none absolute inset-y-0 right-3 flex items-center">
<ChevronDown
size={16}
className={disabled ? "text-gray-300" : "text-gray-400"}
aria-hidden="true"
/>
</div>
</div>
);
}

View File

@@ -29,6 +29,7 @@ import { UnderlineTabs } from "../../../../_components/ui/tabs";
import { DashboardPageTemplate } from "../../../../_components/ui/dashboard-page-template";
import { DenButton } from "../../../../_components/ui/button";
import { DenInput } from "../../../../_components/ui/input";
import { DenSelect } from "../../../../_components/ui/select";
type MembersTab = "members" | "teams" | "roles" | "invitations";
@@ -304,17 +305,13 @@ export function ManageMembersScreen() {
</label>
<label className="grid gap-3">
<span className="text-[14px] font-medium text-gray-700">Role</span>
<select
value={inviteRole}
onChange={(event) => setInviteRole(event.target.value)}
className="h-14 rounded-[20px] border border-gray-200 bg-[#f8fafc] px-4 text-[15px] text-gray-900 outline-none transition focus:border-gray-300 focus:ring-4 focus:ring-gray-900/5"
>
<DenSelect value={inviteRole} onChange={(event) => setInviteRole(event.target.value)}>
{assignableRoles.map((role) => (
<option key={role.id} value={role.role}>
{formatRoleLabel(role.role)}
</option>
))}
</select>
</DenSelect>
</label>
<div className="flex gap-2 lg:justify-end">
<ActionButton onClick={resetInviteForm}>Cancel</ActionButton>
@@ -348,17 +345,13 @@ export function ManageMembersScreen() {
>
<label className="grid gap-3">
<span className="text-[14px] font-medium text-gray-700">Role</span>
<select
value={memberRoleDraft}
onChange={(event) => setMemberRoleDraft(event.target.value)}
className="h-14 rounded-[20px] border border-gray-200 bg-[#f8fafc] px-4 text-[15px] text-gray-900 outline-none transition focus:border-gray-300 focus:ring-4 focus:ring-gray-900/5"
>
<DenSelect value={memberRoleDraft} onChange={(event) => setMemberRoleDraft(event.target.value)}>
{assignableRoles.map((role) => (
<option key={role.id} value={role.role}>
{formatRoleLabel(role.role)}
</option>
))}
</select>
</DenSelect>
</label>
<div className="flex gap-2 lg:justify-end">
<ActionButton onClick={resetMemberEditor}>Cancel</ActionButton>

View File

@@ -6,6 +6,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { ArrowLeft, Upload } from "lucide-react";
import { DenButton } from "../../../../_components/ui/button";
import { DenInput } from "../../../../_components/ui/input";
import { DenSelect } from "../../../../_components/ui/select";
import { DenTextarea } from "../../../../_components/ui/textarea";
import { getErrorMessage, requestJson } from "../../../../_lib/den-flow";
import {
@@ -238,15 +239,14 @@ export function SkillEditorScreen({ skillId }: { skillId?: string }) {
<label className="grid gap-2">
<span className="text-[13px] font-medium text-gray-600">Visibility</span>
<select
<DenSelect
value={visibility}
onChange={(event) => setVisibility(event.target.value as SkillVisibility)}
className="w-full rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-[14px] text-gray-900 outline-none transition-all focus:border-gray-300 focus:ring-2 focus:ring-gray-900/5"
>
<option value="private">Private</option>
<option value="org">Org</option>
<option value="public">Public</option>
</select>
</DenSelect>
</label>
<label className="grid gap-2">