Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
node_modules/
packages/*/node_modules/
apps/*/node_modules/
apps/labs/.cache/
apps/labs/runtime/.generated/
ee/apps/*/node_modules/
ee/packages/*/node_modules/
.next/
Expand Down
116 changes: 116 additions & 0 deletions apps/app/src/app/components/select-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
import { Check, ChevronDown } from "lucide-solid";

export type SelectMenuOption = {
value: string;
label: string;
};

type SelectMenuProps = {
options: SelectMenuOption[];
value: string;
onChange: (value: string) => void;
disabled?: boolean;
placeholder?: string;
id?: string;
/** For pairing with a visible `<label>` element */
ariaLabelledBy?: string;
/** When there is no visible label */
ariaLabel?: string;
};

const triggerClass =
"flex w-full items-center justify-between gap-2 rounded-xl border border-dls-border bg-dls-surface px-3.5 py-2.5 text-left text-[14px] text-dls-text shadow-none transition-[border-color,box-shadow] hover:border-dls-border focus:outline-none focus:ring-2 focus:ring-[rgba(var(--dls-accent-rgb),0.14)] disabled:cursor-not-allowed disabled:opacity-60";

const panelClass =
"absolute left-0 right-0 top-[calc(100%+6px)] z-[100] max-h-56 overflow-auto rounded-xl border border-dls-border bg-dls-surface py-1 shadow-[var(--dls-shell-shadow)]";

const optionRowClass =
"flex w-full items-center gap-2 px-3 py-2.5 text-left text-[13px] text-dls-text transition-colors hover:bg-dls-hover";

export default function SelectMenu(props: SelectMenuProps) {
const [open, setOpen] = createSignal(false);
let rootEl: HTMLDivElement | undefined;

const displayLabel = createMemo(() => {
const match = props.options.find((o) => o.value === props.value);
if (match) return match.label;
return props.placeholder?.trim() || "";
});

const close = () => setOpen(false);

createEffect(() => {
if (!open()) return;
const onPointerDown = (event: PointerEvent) => {
const target = event.target as Node | null;
if (rootEl && target && !rootEl.contains(target)) {
close();
}
};
window.addEventListener("pointerdown", onPointerDown, true);
onCleanup(() => window.removeEventListener("pointerdown", onPointerDown, true));
});

createEffect(() => {
if (!open()) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
close();
}
};
window.addEventListener("keydown", onKeyDown);
onCleanup(() => window.removeEventListener("keydown", onKeyDown));
});

return (
<div ref={(el) => (rootEl = el)} class="relative w-full">
<button
type="button"
id={props.id}
class={triggerClass}
disabled={props.disabled}
aria-expanded={open()}
aria-haspopup="listbox"
aria-labelledby={props.ariaLabelledBy}
aria-label={props.ariaLabel}
onClick={() => {
if (props.disabled) return;
setOpen((o) => !o);
}}
>
<span class="min-w-0 flex-1 truncate">{displayLabel()}</span>
<ChevronDown
size={18}
class={`shrink-0 text-dls-secondary transition-transform duration-200 ${open() ? "rotate-180" : ""}`}
aria-hidden
/>
</button>

<Show when={open() && !props.disabled}>
<div class={panelClass} role="listbox">
<For each={props.options}>
{(opt) => (
<button
type="button"
role="option"
aria-selected={opt.value === props.value}
class={`${optionRowClass} ${opt.value === props.value ? "bg-dls-hover/80" : ""}`}
onClick={() => {
props.onChange(opt.value);
close();
}}
>
<span class="min-w-0 flex-1 truncate">{opt.label}</span>
<Show when={opt.value === props.value}>
<Check size={16} class="shrink-0 text-[var(--dls-accent)]" aria-hidden />
</Show>
</button>
)}
</For>
</div>
</Show>
</div>
);
}
43 changes: 25 additions & 18 deletions apps/app/src/app/pages/skills.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { SkillBundleV1 } from "../bundles/types";
import { saveInstalledSkillToOpenWorkOrg } from "../bundles/skill-org-publish";

import Button from "../components/button";
import SelectMenu, { type SelectMenuOption } from "../components/select-menu";
import {
ArrowLeft,
Copy,
Expand All @@ -30,17 +31,17 @@ import { buildDenAuthUrl, createDenClient, readDenSettings, type DenOrgSkillHubS
import { useStatusToasts, type AppStatusToastTone } from "../shell/status-toasts";
import WorkspaceOptionCard from "../workspace/option-card";
import {
errorBannerClass,
inputClass,
modalHeaderButtonClass,
modalHeaderClass,
modalNoticeErrorClass,
modalNoticeSuccessClass,
modalOverlayClass,
modalShellClass,
modalSubtitleClass,
modalTitleClass,
pillPrimaryClass as sharePillPrimaryClass,
pillSecondaryClass as sharePillSecondaryClass,
successBannerClass,
surfaceCardClass,
tagClass as shareTagClass,
} from "../workspace/modal-styles";
Expand Down Expand Up @@ -162,6 +163,13 @@ export default function SkillsView(props: SkillsViewProps) {
}
});

const shareHubSelectOptions = createMemo(
(): SelectMenuOption[] => [
{ value: "", label: translate("skills.share_team_hub_none") },
...shareManageableHubs().map((h) => ({ value: h.id, label: h.name })),
],
);

createEffect(() => {
if (!shareOpen()) return;
const onKeyDown = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -1067,7 +1075,7 @@ export default function SkillsView(props: SkillsViewProps) {
</div>

<Show when={shareError()}>
<div class={`mb-3 ${errorBannerClass}`}>{shareError()}</div>
<div class={`mb-3 ${modalNoticeErrorClass}`}>{shareError()}</div>
</Show>

<Show
Expand Down Expand Up @@ -1119,38 +1127,37 @@ export default function SkillsView(props: SkillsViewProps) {
</div>

<Show when={shareTeamError()?.trim()}>
<div class={`mt-4 ${errorBannerClass}`}>{shareTeamError()}</div>
<div class={`mt-4 ${modalNoticeErrorClass}`}>{shareTeamError()}</div>
</Show>

<Show when={shareTeamSuccess()?.trim()}>
<div class={`mt-4 ${successBannerClass}`}>{shareTeamSuccess()}</div>
<div class={`mt-4 ${modalNoticeSuccessClass}`}>{shareTeamSuccess()}</div>
</Show>

<Show when={shareHubsError()?.trim()}>
<div class={`mt-4 ${errorBannerClass}`}>{shareHubsError()}</div>
<div class={`mt-4 ${modalNoticeErrorClass}`}>{shareHubsError()}</div>
</Show>

<Show when={shareCloudSignedIn() && shareTeamDisabledReason()?.trim()}>
<div class="mt-4 text-[12px] text-dls-secondary">{shareTeamDisabledReason()}</div>
</Show>

<Show when={shareCloudSignedIn() && shareManageableHubs().length > 0}>
<label class="mt-4 block">
<span class="mb-1.5 block text-[13px] font-medium text-dls-text">
<div class="mt-4">
<span
id="skills-share-hub-label"
class="mb-1.5 block text-[13px] font-medium text-dls-text"
>
{translate("skills.share_team_hub_label")}
</span>
<select
class={inputClass}
<SelectMenu
aria-labelledby="skills-share-hub-label"
options={shareHubSelectOptions()}
value={shareHubChoice()}
onChange={(e) => setShareHubChoice(e.currentTarget.value)}
onChange={setShareHubChoice}
disabled={shareTeamBusy() || Boolean(shareTeamSuccess()?.trim())}
>
<option value="">{translate("skills.share_team_hub_none")}</option>
<For each={shareManageableHubs()}>
{(hub) => <option value={hub.id}>{hub.name}</option>}
</For>
</select>
</label>
/>
</div>
</Show>

<Show when={shareCloudSignedIn() && shareHubsLoading()}>
Expand Down
10 changes: 10 additions & 0 deletions apps/app/src/app/workspace/modal-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,13 @@ export const errorBannerClass =

export const successBannerClass =
"rounded-[20px] border border-emerald-7/20 bg-emerald-3/30 px-4 py-3 text-[13px] text-emerald-11";

/** Softer inline notices inside modals (avoids heavy outlines on light surfaces) */
export const modalNoticeNeutralClass =
"rounded-xl border border-dls-border bg-dls-hover px-3 py-2.5 text-[13px] leading-relaxed text-dls-text";

export const modalNoticeSuccessClass =
"rounded-xl border border-dls-border bg-emerald-2/25 px-3 py-2.5 text-[13px] leading-relaxed text-dls-text";

export const modalNoticeErrorClass =
"rounded-xl border border-dls-border bg-red-2/20 px-3 py-2.5 text-[13px] leading-relaxed text-dls-text";
Loading
Loading