soften app share modal surfaces

This commit is contained in:
Benjamin Shafii
2026-03-26 11:54:47 -07:00
parent 3e2cd73523
commit c1340824f9
3 changed files with 404 additions and 160 deletions

View File

@@ -101,6 +101,10 @@ Use for landing sections that need grouping but should still feel calm.
- subtle edge definition - subtle edge definition
- **no box shadow by default** - **no box shadow by default**
This is no longer landing-only in spirit. For app surfaces like modals, package builders,
and share flows, the same shell language is often the right starting point when the surface
represents a workflow object instead of generic settings chrome.
#### Elevated showcase shell #### Elevated showcase shell
Use only when a hero/demo needs one extra level of emphasis. Use only when a hero/demo needs one extra level of emphasis.
@@ -151,6 +155,10 @@ Borders are one of the main structure tools in OpenWork.
- low-alpha white borders for translucent landing shells - low-alpha white borders for translucent landing shells
- soft shell borders like `#eceef1` for app sidebars and large rounded utility panels - soft shell borders like `#eceef1` for app sidebars and large rounded utility panels
Do not use a dark or high-contrast outline as the main styling for a small icon tile,
badge shell, or compact decorative container. If the element is just carrying an icon,
prefer a soft filled tile over an outlined chip.
Selection should usually feel like: Selection should usually feel like:
- soft neutral fill - soft neutral fill
@@ -563,6 +571,36 @@ If the user is just choosing between three conceptual options, dont force eve
- text-only list - text-only list
- opacity-driven stacked copy - opacity-driven stacked copy
### Product object cards
Use when the UI is presenting a reusable worker, template, integration, or packaged setup.
Pattern:
- soft shell or near-white card
- generous padding
- title first
- one short supporting sentence
- compact status pill in the top-right if needed
- actions inline underneath or within the card
These should feel like curated product objects, not admin rows.
### Icon tiles inside cards
When a card uses an icon block:
- use a soft filled tile (`bg-slate-50` / similar)
- prefer no visible border by default
- let size, radius, and fill define the tile
- if a muted version is needed, use a quieter fill rather than an outline
Do not:
- put a dark stroke around the icon tile
- make the icon tile look like a separate outlined button unless it actually is one
- introduce standalone black/ink borders for decorative icon wrappers
### Section composition ### Section composition
Most sections should follow one of these layouts: Most sections should follow one of these layouts:
@@ -604,6 +642,16 @@ Meaning:
- keep labels readable - keep labels readable
- emphasize utility over visual flourish - emphasize utility over visual flourish
### Packaged workflow surfaces
When showing a workflow like share/package/export:
- prefer a soft shell over default modal chrome
- make the core object the hero (template, worker, integration, package)
- reduce the number of nested bordered panels
- use one or two strong cards, then flatter supporting sections
- present actions as intentional product actions, not generic form controls
--- ---
## 13. Selection States ## 13. Selection States
@@ -635,6 +683,33 @@ Avoid making the selected state look like a separate floating card unless the in
--- ---
## 13.5 Modal Surfaces
Not every modal should look like a system dialog.
For workflow modals (share, package, connect, publish, save to team):
- use a large soft shell with a near-white background
- keep the header airy and typographic
- avoid harsh header separators unless they add real structure
- prefer one scrollable content region inside the shell
- use soft cards for major choices
- reduce mini-panels and stacked utility boxes
Good modal direction:
- feels like a product surface
- can contain object cards and actions
- uses soft hierarchy and breathing room
Bad modal direction:
- dense settings sheet
- too many small bordered sub-panels
- generic dialog chrome with no product feel
---
## 14. Motion ## 14. Motion
Motion should be tight and purposeful. Motion should be tight and purposeful.
@@ -696,6 +771,7 @@ Do not introduce these:
- giant gradients behind readable text - giant gradients behind readable text
- decorative badges/counters with no functional meaning - decorative badges/counters with no functional meaning
- hiding anchor labels just to show hover actions - hiding anchor labels just to show hover actions
- outlined icon chips that read darker than the card they sit inside
If something looks “designed” before it looks “useful,” it is probably wrong. If something looks “designed” before it looks “useful,” it is probably wrong.
@@ -744,6 +820,15 @@ If something looks “designed” before it looks “useful,” it is probably w
- selected row uses soft gray fill - selected row uses soft gray fill
- floating footer action can be white if it needs separation from the shell - floating footer action can be white if it needs separation from the shell
### Share/package modal
- soft shell modal
- object cards for reusable templates or integrations
- compact status pills
- strong dark primary CTA
- white secondary CTA with tiny ring/shadow
- avoid form-heavy utility styling unless the step is truly form-driven
### Landing shell ### Landing shell
- reserved for hero/showcase moments - reserved for hero/showcase moments
@@ -780,6 +865,7 @@ Use these as implementation references:
- Landing hero and selector patterns: `_repos/openwork/ee/apps/landing/components/landing-home.tsx` - Landing hero and selector patterns: `_repos/openwork/ee/apps/landing/components/landing-home.tsx`
- Landing demo list rhythm: `_repos/openwork/ee/apps/landing/components/landing-app-demo-panel.tsx` - Landing demo list rhythm: `_repos/openwork/ee/apps/landing/components/landing-app-demo-panel.tsx`
- Cloud dashboard sidebar shell + selected state: `_repos/openwork/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/org-dashboard-shell.tsx` - Cloud dashboard sidebar shell + selected state: `_repos/openwork/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/org-dashboard-shell.tsx`
- Share/package modal direction: `_repos/openwork/apps/app/src/app/components/share-workspace-modal.tsx`
- App workspace/session list rhythm: `_repos/openwork/apps/app/src/app/components/session/workspace-session-list.tsx` - App workspace/session list rhythm: `_repos/openwork/apps/app/src/app/components/session/workspace-session-list.tsx`
When in doubt, prefer the calmer version. When in doubt, prefer the calmer version.

View File

@@ -81,6 +81,11 @@ export default function ShareWorkspaceModal(props: {
const [teamTemplateName, setTeamTemplateName] = createSignal(""); const [teamTemplateName, setTeamTemplateName] = createSignal("");
const title = createMemo(() => props.title ?? "Share workspace"); const title = createMemo(() => props.title ?? "Share workspace");
const workspaceBadge = createMemo(() => {
const raw = props.workspaceName?.trim() || "Workspace";
const parts = raw.split(/[\\/]/).filter(Boolean);
return parts[parts.length - 1] || raw;
});
const note = createMemo(() => props.note?.trim() ?? ""); const note = createMemo(() => props.note?.trim() ?? "");
const teamShareNeedsSignIn = createMemo( const teamShareNeedsSignIn = createMemo(
() => props.shareWorkspaceProfileToTeamNeedsSignIn === true, () => props.shareWorkspaceProfileToTeamNeedsSignIn === true,
@@ -91,6 +96,11 @@ export default function ShareWorkspaceModal(props: {
accessFields().filter((field) => !isCollaboratorField(field.label)), accessFields().filter((field) => !isCollaboratorField(field.label)),
); );
const primaryButtonClass = "ow-button-primary px-5 py-3";
const secondaryButtonClass = "ow-button-secondary px-5 py-3";
const softCardClass = "ow-soft-card rounded-[1.5rem] p-5";
const quietCardClass = "ow-soft-card-quiet rounded-[1.5rem] p-4";
createEffect( createEffect(
on( on(
() => props.open, () => props.open,
@@ -160,13 +170,56 @@ export default function ShareWorkspaceModal(props: {
} }
}; };
const renderOptionCard = (
titleText: string,
body: string,
icon: typeof Rocket,
onClick: () => void,
tone: "primary" | "secondary" = "primary",
) => {
const Icon = icon;
const isSecondary = tone === "secondary";
return (
<button
type="button"
onClick={onClick}
class={
isSecondary
? "ow-soft-card-quiet group w-full rounded-[1.5rem] p-5 text-left transition-colors hover:bg-[#f1f5f9]"
: "ow-soft-card group w-full rounded-[1.5rem] p-5 text-left transition-colors hover:bg-gray-50/50"
}
>
<div class="flex items-start justify-between gap-4">
<div class="flex gap-4">
<div
class={
isSecondary
? "ow-icon-tile-muted mt-0.5 h-10 w-10 shrink-0"
: "ow-icon-tile mt-0.5 h-10 w-10 shrink-0"
}
>
<Icon size={18} />
</div>
<div class="flex-1">
<h3 class={isSecondary ? "text-[15px] font-medium tracking-tight text-gray-800" : "text-[15px] font-medium tracking-tight text-[#011627]"}>
{titleText}
</h3>
<p class="mt-1 max-w-[38ch] text-[13px] leading-relaxed text-gray-500">{body}</p>
</div>
</div>
</div>
</button>
);
};
const renderCredentialField = (field: ShareField, index: () => number, keyPrefix: string) => { const renderCredentialField = (field: ShareField, index: () => number, keyPrefix: string) => {
const key = () => `${keyPrefix}:${field.label}:${index()}`; const key = () => `${keyPrefix}:${field.label}:${index()}`;
const isSecret = () => Boolean(field.secret); const isSecret = () => Boolean(field.secret);
const revealed = () => Boolean(revealedByIndex()[index()]); const revealed = () => Boolean(revealedByIndex()[index()]);
return ( return (
<div class="group"> <div>
<label class="text-[11px] uppercase tracking-wider font-medium text-gray-10 mb-1.5 block"> <label class="mb-1.5 block text-[11px] font-medium uppercase tracking-wider text-gray-500">
{displayFieldLabel(field)} {displayFieldLabel(field)}
</label> </label>
<div class="relative flex items-center"> <div class="relative flex items-center">
@@ -174,7 +227,7 @@ export default function ShareWorkspaceModal(props: {
type={isSecret() && !revealed() ? "password" : "text"} type={isSecret() && !revealed() ? "password" : "text"}
readonly readonly
value={field.value || field.placeholder || ""} value={field.value || field.placeholder || ""}
class="w-full bg-transparent border border-dls-border rounded-md py-2 pl-3 pr-20 text-[12px] font-mono text-dls-text transition-colors outline-none focus:border-[rgba(var(--dls-accent-rgb),0.45)] focus:ring-1 focus:ring-[rgba(var(--dls-accent-rgb),0.18)]" class="w-full rounded-xl border border-[#e5e7eb] bg-[#fbfbfc] py-3 pl-3 pr-20 text-[12px] font-mono text-dls-text outline-none transition-colors focus:border-[rgba(var(--dls-accent-rgb),0.45)] focus:ring-1 focus:ring-[rgba(var(--dls-accent-rgb),0.18)]"
/> />
<div class="absolute right-1 flex items-center gap-0.5"> <div class="absolute right-1 flex items-center gap-0.5">
<Show when={isSecret()}> <Show when={isSecret()}>
@@ -186,7 +239,7 @@ export default function ShareWorkspaceModal(props: {
})) }))
} }
disabled={!field.value} disabled={!field.value}
class="p-1.5 text-gray-10 hover:text-gray-12 hover:bg-gray-3 rounded-md transition-colors disabled:opacity-50" class="rounded-full p-1.5 text-gray-500 transition-colors hover:bg-white hover:text-gray-700 disabled:opacity-50"
title={revealed() ? "Hide password" : "Reveal password"} title={revealed() ? "Hide password" : "Reveal password"}
> >
<Show when={revealed()} fallback={<Eye size={14} />}> <Show when={revealed()} fallback={<Eye size={14} />}>
@@ -197,17 +250,17 @@ export default function ShareWorkspaceModal(props: {
<button <button
onClick={() => handleCopy(field.value, key())} onClick={() => handleCopy(field.value, key())}
disabled={!field.value} disabled={!field.value}
class="p-1.5 text-gray-10 hover:text-gray-12 hover:bg-gray-3 rounded-md transition-colors disabled:opacity-50" class="rounded-full p-1.5 text-gray-500 transition-colors hover:bg-white hover:text-gray-700 disabled:opacity-50"
title="Copy" title="Copy"
> >
<Show when={copiedKey() === key()} fallback={<Copy size={14} />}> <Show when={copiedKey() === key()} fallback={<Copy size={14} />}>
<Check size={14} class="text-emerald-10" /> <Check size={14} class="text-emerald-600" />
</Show> </Show>
</button> </button>
</div> </div>
</div> </div>
<Show when={field.hint && field.hint.trim()}> <Show when={field.hint && field.hint.trim()}>
<p class="text-[11px] text-gray-9 mt-1.5">{field.hint}</p> <p class="mt-1.5 text-[11px] text-gray-500">{field.hint}</p>
</Show> </Show>
</div> </div>
); );
@@ -229,51 +282,53 @@ export default function ShareWorkspaceModal(props: {
<button <button
onClick={() => createAction?.()} onClick={() => createAction?.()}
disabled={Boolean(disabledReason) || !createAction || busy} disabled={Boolean(disabledReason) || !createAction || busy}
class="mt-3 w-full rounded-full bg-dls-text px-5 py-3 text-[13px] font-medium text-dls-surface shadow-sm transition-colors hover:bg-gray-12 active:scale-[0.99] disabled:opacity-50" class={`${primaryButtonClass} mt-4 w-full`}
> >
{busy ? "Publishing..." : createLabel} {busy ? "Publishing..." : createLabel}
</button> </button>
} }
> >
<div class="flex items-center gap-2 animate-in fade-in zoom-in-95 duration-200"> <div class="animate-in fade-in zoom-in-95 duration-200">
<div class="flex items-center gap-2">
<input <input
type="text" type="text"
readonly readonly
value={value!} value={value!}
class="flex-1 bg-transparent border border-dls-border rounded-md py-1.5 px-2.5 text-[12px] font-mono text-gray-11 outline-none focus:border-[rgba(var(--dls-accent-rgb),0.45)] focus:ring-1 focus:ring-[rgba(var(--dls-accent-rgb),0.18)]" class="flex-1 rounded-xl border border-[#e5e7eb] bg-[#fbfbfc] px-3 py-3 text-[12px] font-mono text-gray-700 outline-none focus:border-[rgba(var(--dls-accent-rgb),0.45)] focus:ring-1 focus:ring-[rgba(var(--dls-accent-rgb),0.18)]"
/> />
<button <button
onClick={() => handleCopy(value ?? "", copyKey)} onClick={() => handleCopy(value ?? "", copyKey)}
class="p-1.5 hover:bg-gray-3 text-gray-11 hover:text-gray-12 rounded-md transition-colors" class="rounded-full p-2 text-gray-500 transition-colors hover:bg-white hover:text-gray-700"
title="Copy link" title="Copy link"
> >
<Show when={copiedKey() === copyKey} fallback={<Copy size={14} />}> <Show when={copiedKey() === copyKey} fallback={<Copy size={14} />}>
<Check size={14} class="text-emerald-10" /> <Check size={14} class="text-emerald-600" />
</Show> </Show>
</button> </button>
</div> </div>
<button <button
onClick={() => regenerate?.()} onClick={() => regenerate?.()}
disabled={busy} disabled={busy}
class="mt-3 w-full rounded-full bg-gray-2 px-4 py-2 text-[12px] font-medium text-gray-11 transition-colors hover:bg-gray-3 hover:text-gray-12" class={`${secondaryButtonClass} mt-3 w-full`}
> >
{busy ? "Publishing..." : regenerateLabel} {busy ? "Publishing..." : regenerateLabel}
</button> </button>
</div>
</Show> </Show>
); );
return ( return (
<Show when={props.open}> <Show when={props.open}>
<div class="fixed inset-0 z-50 flex items-start justify-center bg-black/45 px-3 pt-[12vh] md:px-6 font-sans animate-in fade-in duration-200"> <div class="fixed inset-0 z-50 flex items-start justify-center bg-black/35 px-3 pt-[10vh] font-sans animate-in fade-in duration-200 md:px-6">
<div <div
class="w-full max-w-[580px] rounded-2xl border border-dls-border bg-dls-surface shadow-[0_20px_70px_rgba(0,0,0,0.45)] overflow-hidden animate-in fade-in zoom-in-95 duration-300 relative flex flex-col max-h-[75vh]" class="ow-soft-shell relative flex max-h-[78vh] w-full max-w-[640px] flex-col overflow-hidden rounded-[2rem] shadow-[0_24px_72px_-30px_rgba(15,23,42,0.28)] animate-in fade-in zoom-in-95 duration-300"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
> >
<div class="border-b border-dls-border px-4 py-3 relative shrink-0"> <div class="relative shrink-0 px-6 pb-4 pt-5 md:px-7 md:pt-6">
<button <button
onClick={props.onClose} onClick={props.onClose}
class="absolute top-3 right-3 p-1 text-gray-9 hover:text-gray-12 hover:bg-gray-3 rounded-md transition-colors" class="absolute right-5 top-5 rounded-full p-2 text-gray-500 transition-colors hover:bg-white hover:text-gray-700"
aria-label="Close" aria-label="Close"
title="Close" title="Close"
> >
@@ -283,7 +338,7 @@ export default function ShareWorkspaceModal(props: {
<Show when={activeView() !== "chooser"}> <Show when={activeView() !== "chooser"}>
<button <button
onClick={goBack} onClick={goBack}
class="absolute top-3 left-3 p-1 text-gray-9 hover:text-gray-12 hover:bg-gray-3 rounded-md transition-colors" class="absolute left-5 top-5 rounded-full p-2 text-gray-500 transition-colors hover:bg-white hover:text-gray-700"
aria-label="Back" aria-label="Back"
title="Back to share options" title="Back to share options"
> >
@@ -291,121 +346,96 @@ export default function ShareWorkspaceModal(props: {
</button> </button>
</Show> </Show>
<div class="flex items-center gap-2" classList={{ "ml-6": activeView() !== "chooser" }}> <div class="flex items-center gap-2" classList={{ "ml-8": activeView() !== "chooser" }}>
<div class="min-w-0"> <div class="min-w-0">
<h2 class="text-[14px] font-medium text-dls-text tracking-tight truncate"> <div class="flex flex-wrap items-center gap-2">
<h2 class="truncate text-[20px] font-semibold tracking-tight text-[#011627]">
<Show when={activeView() === "chooser"}>{title()}</Show> <Show when={activeView() === "chooser"}>{title()}</Show>
<Show when={activeView() === "template"}>Share a template</Show> <Show when={activeView() === "template"}>Share a template</Show>
<Show when={activeView() === "template-public"}>Public template</Show> <Show when={activeView() === "template-public"}>Public template</Show>
<Show when={activeView() === "template-team"}>Share with team</Show> <Show when={activeView() === "template-team"}>Share with team</Show>
<Show when={activeView() === "access"}>Access workspace remotely</Show> <Show when={activeView() === "access"}>Access workspace remotely</Show>
</h2> </h2>
<div class="mt-0.5 text-[12px] text-gray-10 truncate">{props.workspaceName}</div>
</div>
</div>
</div>
<div class="px-4 pb-6 flex-1 overflow-y-auto scrollbar-hide">
<Show when={activeView() === "chooser"}> <Show when={activeView() === "chooser"}>
<div class="space-y-2 pt-4 animate-in fade-in slide-in-from-bottom-3 duration-300"> <span class="rounded-full bg-gray-100 px-3 py-1 text-[11px] font-medium text-gray-600">
<button {workspaceBadge()}
type="button" </span>
onClick={() => setActiveView("template")} </Show>
class="w-full text-left rounded-xl p-3 hover:bg-dls-hover transition-colors group flex items-start gap-3" </div>
> </div>
<div class="mt-0.5 text-gray-10 group-hover:text-gray-12 transition-colors shrink-0">
<Rocket size={18} />
</div> </div>
<div class="flex-1">
<h3 class="text-[13px] font-medium text-dls-text">Share a template</h3>
<p class="text-[12px] text-gray-10 leading-snug mt-0.5 pr-4">
Share your setup and defaults so someone else can start from the same environment.
</p>
</div> </div>
</button>
<button <div class="flex-1 overflow-y-auto px-6 pb-7 scrollbar-hide md:px-7">
type="button" <Show when={activeView() === "chooser"}>
onClick={() => setActiveView("access")} <div class="space-y-4 pt-2 animate-in fade-in slide-in-from-bottom-3 duration-300">
class="w-full text-left rounded-xl p-3 hover:bg-dls-hover transition-colors group flex items-start gap-3" {renderOptionCard(
> "Share a template",
<div class="mt-0.5 text-gray-10 group-hover:text-gray-12 transition-colors shrink-0"> "Package this setup so someone else can start from the same environment.",
<MonitorUp size={18} /> Rocket,
</div> () => setActiveView("template"),
<div class="flex-1"> )}
<h3 class="text-[13px] font-medium text-dls-text">Access workspace remotely</h3>
<p class="text-[12px] text-gray-10 leading-snug mt-0.5 pr-4"> {renderOptionCard(
Copy the connection details needed to reach this live workspace from another machine or messaging surface. "Access workspace remotely",
</p> "Reveal the live connection details needed to reach this running workspace from another machine.",
</div> MonitorUp,
</button> () => setActiveView("access"),
)}
</div> </div>
</Show> </Show>
<Show when={activeView() === "template"}> <Show when={activeView() === "template"}>
<div class="space-y-6 pt-4 animate-in fade-in slide-in-from-right-4 duration-300"> <div class="space-y-4 pt-2 animate-in fade-in slide-in-from-right-4 duration-300">
<div class="text-[12px] text-gray-10"> <div class="text-[14px] leading-relaxed text-gray-500">
Share a reusable setup without granting live access to this running workspace. Share a reusable setup without granting live access to this running workspace.
</div> </div>
<div class="space-y-2"> <div class="space-y-4">
<button {renderOptionCard(
type="button" "Share with team",
onClick={() => setActiveView("template-public")} "Save this workspace template to your active OpenWork Cloud organization.",
class="w-full text-left rounded-xl p-3 hover:bg-dls-hover transition-colors group flex items-start gap-3" Users,
> () => setActiveView("template-team"),
<div class="mt-0.5 text-gray-10 group-hover:text-gray-12 transition-colors shrink-0"> )}
<Rocket size={18} />
</div>
<div class="flex-1">
<h3 class="text-[13px] font-medium text-dls-text">Public</h3>
<p class="text-[12px] text-gray-10 leading-snug mt-0.5 pr-4">
Create a public share link anyone can use to start from this template.
</p>
</div>
</button>
<button {renderOptionCard(
type="button" "Public template",
onClick={() => setActiveView("template-team")} "Create a share link anyone can use to start from this template.",
class="w-full text-left rounded-xl p-3 hover:bg-dls-hover transition-colors group flex items-start gap-3" Rocket,
> () => setActiveView("template-public"),
<div class="mt-0.5 text-gray-10 group-hover:text-gray-12 transition-colors shrink-0"> "secondary",
<Users size={18} /> )}
</div>
<div class="flex-1">
<h3 class="text-[13px] font-medium text-dls-text">Share with team</h3>
<p class="text-[12px] text-gray-10 leading-snug mt-0.5 pr-4">
Save this workspace template to your active OpenWork Cloud organization.
</p>
</div>
</button>
</div> </div>
</div> </div>
</Show> </Show>
<Show when={activeView() === "template-public"}> <Show when={activeView() === "template-public"}>
<div class="space-y-6 pt-4 animate-in fade-in slide-in-from-right-4 duration-300"> <div class="space-y-5 pt-2 animate-in fade-in slide-in-from-right-4 duration-300">
<div class="text-[12px] text-gray-10"> <div class="text-[14px] leading-relaxed text-gray-500">
Share this workspace as a public template link. Share this workspace as a public template link.
</div> </div>
<div class="space-y-3"> <div class={softCardClass}>
<div class="flex items-center gap-2 mb-1"> <div class="mb-4 flex items-start justify-between gap-3">
<FolderCode size={16} class="text-gray-9 shrink-0" /> <div class="flex items-center gap-3">
<div class="ow-icon-tile h-10 w-10 shrink-0 rounded-full">
<FolderCode size={18} />
</div>
<div class="flex-1"> <div class="flex-1">
<h3 class="text-[13px] font-medium text-dls-text">Workspace template</h3> <h3 class="text-[18px] font-medium tracking-tight text-[#011627]">Workspace template</h3>
<p class="text-[12px] text-gray-10 leading-tight mt-0.5">Share the core setup and workspace defaults.</p> <p class="mt-1 text-[14px] leading-relaxed text-gray-500">Share the core setup and workspace defaults.</p>
</div>
</div> </div>
</div> </div>
<Show when={props.shareWorkspaceProfileError?.trim()}> <Show when={props.shareWorkspaceProfileError?.trim()}>
<div class="rounded-md border border-red-6/40 bg-red-3/30 px-3 py-2 mb-2 text-[12px] text-red-11"> <div class="mb-3 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-[12px] text-red-700">
{props.shareWorkspaceProfileError} {props.shareWorkspaceProfileError}
</div> </div>
</Show> </Show>
<Show when={props.shareWorkspaceProfileDisabledReason?.trim()}> <Show when={props.shareWorkspaceProfileDisabledReason?.trim()}>
<div class="text-[12px] text-gray-9 mb-2">{props.shareWorkspaceProfileDisabledReason}</div> <div class="mb-3 text-[12px] text-gray-500">{props.shareWorkspaceProfileDisabledReason}</div>
</Show> </Show>
{renderGeneratedLink( {renderGeneratedLink(
@@ -413,8 +443,8 @@ export default function ShareWorkspaceModal(props: {
"share-workspace-profile", "share-workspace-profile",
props.onShareWorkspaceProfile, props.onShareWorkspaceProfile,
props.shareWorkspaceProfileBusy, props.shareWorkspaceProfileBusy,
"Create Template Link", "Create template link",
"Regenerate Link", "Regenerate link",
props.onShareWorkspaceProfile, props.onShareWorkspaceProfile,
props.shareWorkspaceProfileDisabledReason, props.shareWorkspaceProfileDisabledReason,
)} )}
@@ -423,45 +453,45 @@ export default function ShareWorkspaceModal(props: {
</Show> </Show>
<Show when={activeView() === "template-team"}> <Show when={activeView() === "template-team"}>
<div class="space-y-6 pt-4 animate-in fade-in slide-in-from-right-4 duration-300"> <div class="space-y-5 pt-2 animate-in fade-in slide-in-from-right-4 duration-300">
<div class="text-[12px] text-gray-10"> <div class="text-[14px] leading-relaxed text-gray-500">
Save this template to your active OpenWork Cloud organization so teammates can open it later from Cloud settings. Save this template to your active OpenWork Cloud organization so teammates can open it later from Cloud settings.
</div> </div>
<div class="space-y-4 rounded-[20px] border border-dls-border bg-gray-2/30 px-4 py-4"> <div class={softCardClass}>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<span class="rounded-full border border-gray-6/60 bg-gray-1/40 px-2.5 py-1 text-[11px] font-medium text-gray-11"> <span class="rounded-full border border-[#e5e7eb] bg-[#f8fafc] px-3 py-1 text-[11px] font-medium text-gray-600">
{props.shareWorkspaceProfileToTeamOrgName?.trim() || "Active Cloud org"} {props.shareWorkspaceProfileToTeamOrgName?.trim() || "Active Cloud org"}
</span> </span>
</div> </div>
<div> <div class="mt-4">
<label class="text-[11px] uppercase tracking-wider font-medium text-gray-10 mb-1.5 block"> <label class="mb-1.5 block text-[11px] font-medium uppercase tracking-wider text-gray-500">
Template name Template name
</label> </label>
<input <input
type="text" type="text"
value={teamTemplateName()} value={teamTemplateName()}
onInput={(event) => setTeamTemplateName(event.currentTarget.value)} onInput={(event) => setTeamTemplateName(event.currentTarget.value)}
class="w-full bg-transparent border border-dls-border rounded-md py-2 px-3 text-[13px] text-dls-text transition-colors outline-none focus:border-[rgba(var(--dls-accent-rgb),0.45)] focus:ring-1 focus:ring-[rgba(var(--dls-accent-rgb),0.18)]" class="w-full rounded-xl border border-[#e5e7eb] bg-[#fbfbfc] px-3 py-3 text-[14px] text-dls-text outline-none transition-colors focus:border-[rgba(var(--dls-accent-rgb),0.45)] focus:ring-1 focus:ring-[rgba(var(--dls-accent-rgb),0.18)]"
placeholder={`${props.workspaceName.trim() || "Workspace"} template`} placeholder={`${props.workspaceName.trim() || "Workspace"} template`}
/> />
</div> </div>
<Show when={props.shareWorkspaceProfileToTeamError?.trim()}> <Show when={props.shareWorkspaceProfileToTeamError?.trim()}>
<div class="rounded-md border border-red-6/40 bg-red-3/30 px-3 py-2 text-[12px] text-red-11"> <div class="mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-[12px] text-red-700">
{props.shareWorkspaceProfileToTeamError} {props.shareWorkspaceProfileToTeamError}
</div> </div>
</Show> </Show>
<Show when={props.shareWorkspaceProfileToTeamSuccess?.trim()}> <Show when={props.shareWorkspaceProfileToTeamSuccess?.trim()}>
<div class="rounded-md border border-emerald-6/40 bg-emerald-3/30 px-3 py-2 text-[12px] text-emerald-11"> <div class="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-700">
{props.shareWorkspaceProfileToTeamSuccess} {props.shareWorkspaceProfileToTeamSuccess}
</div> </div>
</Show> </Show>
<Show when={props.shareWorkspaceProfileToTeamDisabledReason?.trim() && !teamShareNeedsSignIn()}> <Show when={props.shareWorkspaceProfileToTeamDisabledReason?.trim() && !teamShareNeedsSignIn()}>
<div class="text-[12px] text-gray-9">{props.shareWorkspaceProfileToTeamDisabledReason}</div> <div class="mt-4 text-[12px] text-gray-500">{props.shareWorkspaceProfileToTeamDisabledReason}</div>
</Show> </Show>
<button <button
@@ -480,7 +510,7 @@ export default function ShareWorkspaceModal(props: {
props.shareWorkspaceProfileToTeamBusy || props.shareWorkspaceProfileToTeamBusy ||
!teamTemplateName().trim() !teamTemplateName().trim()
} }
class="w-full rounded-full bg-dls-text px-5 py-3 text-[13px] font-medium text-dls-surface shadow-sm transition-colors hover:bg-gray-12 active:scale-[0.99] disabled:opacity-50" class={`${primaryButtonClass} mt-4 w-full`}
> >
{teamShareNeedsSignIn() {teamShareNeedsSignIn()
? "Sign in to share with team" ? "Sign in to share with team"
@@ -490,7 +520,7 @@ export default function ShareWorkspaceModal(props: {
</button> </button>
<Show when={teamShareNeedsSignIn()}> <Show when={teamShareNeedsSignIn()}>
<div class="text-[11px] text-gray-9"> <div class="mt-3 text-[11px] text-gray-500">
OpenWork Cloud opens in your browser and returns here after sign-in. OpenWork Cloud opens in your browser and returns here after sign-in.
</div> </div>
</Show> </Show>
@@ -499,8 +529,8 @@ export default function ShareWorkspaceModal(props: {
</Show> </Show>
<Show when={activeView() === "access"}> <Show when={activeView() === "access"}>
<div class="space-y-6 pt-4 animate-in fade-in slide-in-from-right-4 duration-300"> <div class="space-y-5 pt-2 animate-in fade-in slide-in-from-right-4 duration-300">
<div class="rounded-md border border-amber-6/40 bg-amber-3/30 px-3 py-2 text-[12px] text-amber-11 flex items-start gap-2"> <div class="flex items-start gap-2 rounded-[1.25rem] border border-amber-200 bg-amber-50 px-4 py-3 text-[13px] text-amber-900">
<span class="mt-0.5"></span> <span class="mt-0.5"></span>
<span class="leading-relaxed"> <span class="leading-relaxed">
<Show <Show
@@ -518,30 +548,31 @@ export default function ShareWorkspaceModal(props: {
{(remoteAccess) => { {(remoteAccess) => {
const hasPendingChange = () => const hasPendingChange = () =>
remoteAccessEnabled() !== remoteAccess().enabled; remoteAccessEnabled() !== remoteAccess().enabled;
return ( return (
<div class="rounded-[20px] border border-dls-border bg-gray-2/30 px-4 py-4 space-y-4"> <div class={softCardClass}>
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div> <div>
<h3 class="text-[13px] font-medium text-dls-text">Remote access</h3> <h3 class="text-[18px] font-medium tracking-tight text-[#011627]">Remote access</h3>
<p class="text-[12px] text-gray-10 mt-0.5 leading-relaxed"> <p class="mt-1 text-[14px] leading-relaxed text-gray-500">
Off by default. Turn this on only when you want this worker reachable from another machine. Off by default. Turn this on only when you want this worker reachable from another machine.
</p> </p>
</div> </div>
<label class="relative inline-flex items-center cursor-pointer shrink-0"> <label class="relative inline-flex shrink-0 cursor-pointer items-center">
<input <input
type="checkbox" type="checkbox"
class="sr-only peer" class="peer sr-only"
checked={remoteAccessEnabled()} checked={remoteAccessEnabled()}
onInput={(event) => onInput={(event) =>
setRemoteAccessEnabled(event.currentTarget.checked)} setRemoteAccessEnabled(event.currentTarget.checked)}
disabled={remoteAccess().busy} disabled={remoteAccess().busy}
/> />
<div class="w-11 h-6 rounded-full bg-gray-6 transition-colors peer-checked:bg-amber-8 peer-disabled:opacity-50 after:absolute after:top-[2px] after:left-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white after:transition-transform peer-checked:after:translate-x-5" /> <div class="h-6 w-11 rounded-full bg-gray-300 transition-colors peer-checked:bg-amber-500 peer-disabled:opacity-50 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white after:transition-transform peer-checked:after:translate-x-5" />
</label> </label>
</div> </div>
<div class="flex items-center justify-between gap-3"> <div class="mt-4 flex items-center justify-between gap-3">
<div class="text-[12px] text-gray-10"> <div class="text-[13px] text-gray-500">
{remoteAccess().enabled {remoteAccess().enabled
? "Remote access is currently enabled." ? "Remote access is currently enabled."
: "Remote access is currently disabled."} : "Remote access is currently disabled."}
@@ -550,14 +581,14 @@ export default function ShareWorkspaceModal(props: {
type="button" type="button"
onClick={() => remoteAccess().onSave(remoteAccessEnabled())} onClick={() => remoteAccess().onSave(remoteAccessEnabled())}
disabled={remoteAccess().busy || !hasPendingChange()} disabled={remoteAccess().busy || !hasPendingChange()}
class="px-3 py-1.5 bg-gray-2 hover:bg-gray-3 rounded-md text-[12px] font-medium text-dls-text transition-colors disabled:opacity-50" class={secondaryButtonClass}
> >
{remoteAccess().busy ? "Saving..." : "Save"} {remoteAccess().busy ? "Saving..." : "Save"}
</button> </button>
</div> </div>
<Show when={remoteAccess().error?.trim()}> <Show when={remoteAccess().error?.trim()}>
<div class="rounded-md border border-red-6/40 bg-red-3/30 px-3 py-2 text-[12px] text-red-11"> <div class="mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-[12px] text-red-700">
{remoteAccess().error} {remoteAccess().error}
</div> </div>
</Show> </Show>
@@ -566,32 +597,44 @@ export default function ShareWorkspaceModal(props: {
}} }}
</Show> </Show>
<div class="flex items-center justify-between gap-3 rounded-[20px] border border-dls-border bg-gray-2/30 px-3 py-3"> <div class={softCardClass}>
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<MessageSquare size={16} class="text-gray-9 shrink-0" /> <div class="ow-icon-tile-muted h-9 w-9 shrink-0 rounded-full">
<MessageSquare size={16} />
</div>
<div class="min-w-0"> <div class="min-w-0">
<h4 class="text-[13px] font-medium text-dls-text">Connect messaging</h4> <h4 class="text-[18px] font-medium tracking-tight text-[#011627]">Connect messaging</h4>
<p class="text-[12px] text-gray-10 mt-0.5 truncate">Use this workspace from Slack, Telegram, and others.</p> <p class="mt-1 truncate text-[14px] text-gray-500">Use this workspace from Slack, Telegram, and others.</p>
</div> </div>
</div> </div>
<button <button
onClick={() => props.onOpenBots?.()} onClick={() => props.onOpenBots?.()}
disabled={!props.onOpenBots} disabled={!props.onOpenBots}
class="px-3 py-1.5 bg-gray-2 hover:bg-gray-3 rounded-md text-[12px] font-medium text-dls-text transition-colors disabled:opacity-50" class={`${secondaryButtonClass} mt-5 w-full sm:w-auto`}
> >
Setup Setup
</button> </button>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<Show when={primaryAccessFields().length > 0} fallback={ <Show
<div class="rounded-[20px] border border-dls-border bg-gray-2/20 px-4 py-4 text-[12px] text-gray-10 leading-relaxed"> when={primaryAccessFields().length > 0}
fallback={
<div class={quietCardClass}>
<div class="text-[13px] leading-relaxed text-gray-500">
Enable remote access and click Save to restart the worker and reveal the live connection details for this workspace. Enable remote access and click Save to restart the worker and reveal the live connection details for this workspace.
</div> </div>
}> </div>
}
>
<div class={softCardClass}>
<div class="mb-4 text-[11px] font-bold uppercase tracking-[0.18em] text-gray-500">Connection details</div>
<div class="space-y-4">
<For each={primaryAccessFields()}> <For each={primaryAccessFields()}>
{(field, index) => renderCredentialField(field, index, "primary")} {(field, index) => renderCredentialField(field, index, "primary")}
</For> </For>
</div>
</div>
</Show> </Show>
</div> </div>
@@ -600,7 +643,7 @@ export default function ShareWorkspaceModal(props: {
<div class="pt-1"> <div class="pt-1">
<button <button
type="button" type="button"
class="inline-flex items-center gap-2 rounded-full border border-dls-border/70 bg-gray-2/30 px-3 py-1.5 text-[11px] font-medium text-gray-10 transition-colors hover:border-dls-border hover:bg-gray-2/60 hover:text-gray-11" class="inline-flex items-center gap-2 rounded-full border border-[#e5e7eb] bg-white px-4 py-2 text-[11px] font-medium text-gray-500 shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_1px_2px_0_rgba(0,0,0,0.04)] transition-colors hover:bg-gray-50 hover:text-gray-700"
onClick={() => setCollaboratorExpanded((value) => !value)} onClick={() => setCollaboratorExpanded((value) => !value)}
aria-expanded={collaboratorExpanded()} aria-expanded={collaboratorExpanded()}
> >
@@ -611,8 +654,8 @@ export default function ShareWorkspaceModal(props: {
/> />
</button> </button>
<Show when={collaboratorExpanded()}> <Show when={collaboratorExpanded()}>
<div class="mt-3 rounded-[20px] border border-dls-border bg-gray-2/30 px-3 py-3"> <div class={`${quietCardClass} mt-3`}>
<div class="mb-2 text-[11px] text-gray-9">Routine access without permission approvals.</div> <div class="mb-3 text-[12px] text-gray-500">Routine access without permission approvals.</div>
{renderCredentialField(field(), () => 0, "collaborator")} {renderCredentialField(field(), () => 0, "collaborator")}
</div> </div>
</Show> </Show>
@@ -621,9 +664,8 @@ export default function ShareWorkspaceModal(props: {
</Show> </Show>
<Show when={note()}> <Show when={note()}>
<div class="px-1 text-[11px] text-gray-9">{note()}</div> <div class="px-1 text-[11px] text-gray-500">{note()}</div>
</Show> </Show>
</div> </div>
</Show> </Show>
</div> </div>

View File

@@ -74,6 +74,122 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.ow-soft-shell {
border: 1px solid #eceef1;
background: #fbfbfc;
border-radius: 2rem;
}
.ow-soft-card {
border: 1px solid #eceef1;
background: #ffffff;
border-radius: 1.5rem;
box-shadow: 0 10px 24px -22px rgba(15, 23, 42, 0.14);
}
.ow-soft-card-quiet {
border: 1px solid #eceef1;
background: #f8fafc;
border-radius: 1.5rem;
}
.ow-button-primary {
display: inline-flex;
min-height: 48px;
align-items: center;
justify-content: center;
border-radius: 9999px;
background: #011627;
color: #ffffff;
box-shadow: 0 8px 20px -16px rgba(1, 22, 39, 0.45);
}
.ow-button-primary:hover:not(:disabled) {
background: #000000;
}
.ow-button-secondary {
display: inline-flex;
min-height: 48px;
align-items: center;
justify-content: center;
border-radius: 9999px;
border: 1px solid #e5e7eb;
background: #ffffff;
color: #011627;
box-shadow: 0 0 0 1px rgba(0,0,0,0.04), 0 1px 2px 0 rgba(0,0,0,0.04);
}
.ow-button-secondary:hover:not(:disabled) {
background: #f9fafb;
}
.ow-button-primary,
.ow-button-secondary {
padding: 0.75rem 1.25rem;
font-size: 13px;
font-weight: 500;
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease, opacity 150ms ease;
}
.ow-button-primary:active:not(:disabled),
.ow-button-secondary:active:not(:disabled) {
transform: scale(0.99);
}
.ow-button-primary:disabled,
.ow-button-secondary:disabled {
opacity: 0.5;
}
.ow-status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
padding: 0.25rem 0.625rem;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.ow-status-pill-positive {
border: 1px solid rgba(16, 185, 129, 0.16);
background: #ecfdf5;
color: #047857;
}
.ow-status-pill-warning {
border: 1px solid rgba(245, 158, 11, 0.16);
background: #fffbeb;
color: #b45309;
}
.ow-status-pill-neutral {
border: 1px solid #e5e7eb;
background: #f9fafb;
color: #6b7280;
}
.ow-icon-tile {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.85rem;
background: #f4f6f8;
color: #011627;
}
.ow-icon-tile-muted {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.85rem;
background: #f1f5f9;
color: #6b7280;
}
/* Global clickable elements pointer */ /* Global clickable elements pointer */
button, button,
[role="button"], [role="button"],