CreateWorkspaceDialog
CreateWorkspaceDialog is an opinionated Dialog for naming a workspace and choosing its URL. Use it when that standard flow fits; compose Dialog, Input, Avatar and Button yourself when it does not.
Examples
Create a workspace
The slug follows the name until someone edits the slug. After that first edit it stays independent, so an intentional URL is never overwritten by a later name change.
A product-specific URL
title and slugPrefix change the product language around the same workflow. They do not change
the submitted request data: onCreate receives the trimmed name and URL-safe slug in
request.data.
When to use
Use CreateWorkspaceDialog when a product needs the supplied workspace name, slug and initials preview with the supplied interaction rules.
Reach for something else when:
- The creation flow needs different fields, validation or actions — compose Dialog, Input, Avatar and Button yourself instead of bending this opinionated component.
- The person needs to choose the current workspace — use WorkspaceSwitcher. Its
onCreateis the natural place to open this dialog in a product. - The decision blocks another short task but is not workspace creation — use Dialog directly.
Accessibility
- It renders a Dialog, so the panel is
role="dialog"witharia-modal="true"and an accessible name fromtitle. Focus enters and is trapped in the modal; page scroll is locked. - The workspace name and URL fields have visible labels. The avatar preview is derived from the name's initials and updates as the person types; it does not replace the written field labels.
- The name field receives the safe initial focus. Submitting a blank name or slug keeps the dialog
open, shows the field error, focuses the first invalid field, and does not call
onCreate. - A valid native form submission creates exactly one fresh request with trimmed, copied
data, anoperationId, and an initially unaborted ownedsignal. Invalid submission remains available for field validation; while submitting or canceling, duplicate submission is disabled and fields are read-only. - Return a matching terminal acknowledgement from
onCreate:acceptedrequestsonClose,rejectedkeeps the values, focuses the error summary, and permits a retry, andcanceledreturns to editing when no close is pending. A missing handler or result, a thrown error, or a rejected promise becomes an error rather than a successful create. - A normal user close requests
onCloseimmediately. While an operation is pending, user close first aborts the owned signal once and waits for that operation's matchingcanceledacknowledgement; that matchingcanceledacknowledgement requestsonClose; a matchingacceptedacknowledgement requestsonClose, while a matching rejection stays open as an error. An abort notification does not guarantee that application persistence rolled back. - A parent setting controlled
opentofalse(or unmounting) aborts and invalidates pending work; late results are ignored. The next open resets the form.onCloseis a controlled close request, so updateopenin response.returnFocusToforwards its destination resolver to Dialog and uses Dialog's documented eligibility and fallback contract. slugifylowercases, removes accents, collapses non-alphanumerics to hyphens and trims edge hyphens:Ação Globalbecomesacao-global.
API and code
Blade: O snapshot de API lançado do Blade v0.10.0 não tem componente para esta entrada do catálogo.
| Nome | Tipo | Obrigatória | Descrição |
|---|---|---|---|
open | boolean | — | Controls visibility. Default `false`. |
onClose | () => void | — | Called when the dialog is dismissed or a creation operation is accepted. |
onCreate | (request: CreateWorkspaceRequest) => CreateWorkspaceResult | Promise<CreateWorkspaceResult> | — | Receives one owned request and explicitly acknowledges its terminal outcome. |
title | string | — | Dialog heading. Default `"Create workspace"`. |
slugPrefix | string | — | Text rendered before the editable slug. Default `"lyra.dev/"`. |
returnFocusTo | () => HTMLElement | null | — | Resolves the focus destination after an accepted close. |
onCreate owns the application persistence decision. It must return an acknowledgement for the
same operationId; the dialog does not make a network request or invent an outcome.
import {
Button,
CreateWorkspaceDialog,
type CreateWorkspaceRequest,
type CreateWorkspaceResult,
} from '@lyra-ds/react';
import { useRef, useState } from 'react';
type Workspace = { id: string; name: string; slug: string };
interface WorkspacePersistence {
createWorkspace(
data: CreateWorkspaceRequest['data'],
options: { signal: AbortSignal },
): Promise<Workspace>;
}
interface WorkspaceCreatorProps {
persistence: WorkspacePersistence;
}
export function WorkspaceCreator({ persistence }: WorkspaceCreatorProps) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement | null>(null);
async function onCreate(request: CreateWorkspaceRequest): Promise<CreateWorkspaceResult> {
try {
await persistence.createWorkspace(request.data, { signal: request.signal });
return { operationId: request.operationId, status: 'accepted' };
} catch (error: unknown) {
if (request.signal.aborted) {
return { operationId: request.operationId, status: 'canceled' };
}
return {
operationId: request.operationId,
status: 'rejected',
error: error instanceof Error && error.message ? error.message : 'Could not create workspace.',
};
}
}
return (
<>
<Button ref={triggerRef} onClick={() => setOpen(true)}>
Create workspace
</Button>
<CreateWorkspaceDialog
open={open}
onClose={() => setOpen(false)}
onCreate={onCreate}
returnFocusTo={() => triggerRef.current}
/>
</>
);
}For migration, replace the historical void callback with the complete WorkspaceCreator consumer
above. It passes data as request.data, passes request.signal to persistence, and returns an
explicit result with the matching request.operationId.
- <CreateWorkspaceDialog onCreate={(data) => saveWorkspace(data)} />
+ // WorkspaceCreator above persists request.data with request.signal.
+ // It returns { operationId: request.operationId, status: 'accepted' | 'rejected' | 'canceled' }.
+ <CreateWorkspaceDialog onCreate={onCreate} />x-data="lyraCreateWorkspaceDialog({ … })"
| Opção | Tipo | Obrigatória | Descrição |
|---|---|---|---|
slugPrefix | string | — | Text before the editable slug. Default `lyra.dev/`. |
messages | Partial<LyraCreateWorkspaceMessages> | — | Validation and error copy. |
lyraCreateWorkspaceDialog compõe lyraDialog. Mantenha o gatilho e o modal no mesmo x-data.
O binding controla formulário, validação e foco; a aplicação controla a persistência. Um envio
válido emite lyra:create-workspace com { operationId, name, slug } (nome sem espaços nas
pontas). Responda com accept(operationId) ou
reject(operationId, { fieldErrors: { name?, slug? }, message? }) no mesmo componente Alpine.
Sem resposta, o diálogo permanece pendente. Envios duplicados são ignorados; rejeições focam o
resumo role="alert", e erros locais focam o primeiro campo inválido.
Fechar durante o envio emite lyra:create-workspace:cancel com { operationId } uma vez e entra
em canceling. Cancele a requisição e chame cancel(operationId) após ela terminar. accept
fecha; reject exibe o erro. Respostas antigas são ignoradas. Abortar não garante que os dados
já gravados tenham sido revertidos. O fechamento devolve o foco ao gatilho via lyraDialog.
Definir open = false externamente ou desmontar o componente também emite o evento de
cancelamento para uma requisição ativa; uma resposta posterior é ignorada.
<div
x-data="lyraCreateWorkspaceDialog({
messages: {
nameRequired: 'Informe o nome do workspace.',
slugRequired: 'Informe a URL do workspace.',
createFailed: 'Não foi possível criar o workspace. Tente novamente.',
},
})"
@lyra:create-workspace="
const { operationId, name, slug } = $event.detail;
criarWorkspace({ name, slug }).then(
() => accept(operationId),
(error) => reject(operationId, { message: error.message })
)
"
@lyra:create-workspace:cancel="cancelarRequisicao($event.detail.operationId)"
>
<button id="create-workspace-trigger" type="button" @click="open = true">Criar workspace</button>
<div class="lyra-dialog-overlay" x-bind="overlay" style="display: none">
<div class="lyra-dialog" x-bind="panel">
<div class="lyra-dialog__header">
<h2 class="lyra-dialog__title" x-bind="title">Criar workspace</h2>
<button class="lyra-dialog__close" x-bind="{ ...close, 'aria-label': 'Fechar' }">×</button>
</div>
<div class="lyra-dialog__body">
<form
id="workspace-create-form"
class="lyra-wscreate"
aria-label="Criar workspace"
x-bind="form"
>
<div class="lyra-wscreate__preview">
<span class="lyra-avatar lyra-avatar--lg lyra-avatar--square">
<span aria-hidden="true" x-bind="avatar"></span>
</span>
<span class="lyra-wscreate__preview-hint">O avatar usa as iniciais do nome.</span>
</div>
<div
class="lyra-hint lyra-hint--error"
data-lyra-wscreate-error
aria-label="Erro ao criar workspace"
x-bind="errorSummary"
></div>
<div class="lyra-field">
<label class="lyra-label" for="workspace-name">Nome do workspace</label>
<input
class="lyra-input"
id="workspace-name"
placeholder="Acme Inc"
data-lyra-wscreate-name
x-bind="nameInput"
:aria-describedby="fieldErrors.name ? 'workspace-name-error' : null"
/>
<span
id="workspace-name-error"
class="lyra-hint lyra-hint--error"
x-show="fieldErrors.name"
x-text="fieldErrors.name"
></span>
</div>
<div class="lyra-field">
<label class="lyra-label" for="workspace-slug">URL</label>
<span class="lyra-wscreate__slug">
<span class="lyra-wscreate__slug-prefix" x-bind="slugPrefixBinding"></span>
<input
class="lyra-wscreate__slug-input"
id="workspace-slug"
placeholder="acme-inc"
data-lyra-wscreate-slug
x-bind="slugInput"
:aria-describedby="fieldErrors.slug ? 'workspace-slug-error' : null"
/>
</span>
<span
id="workspace-slug-error"
class="lyra-hint lyra-hint--error"
x-show="fieldErrors.slug"
x-text="fieldErrors.slug"
></span>
<span class="lyra-hint" x-show="!fieldErrors.slug">
Letras minúsculas, números e hifens.
</span>
</div>
</form>
</div>
<div class="lyra-dialog__footer">
<button class="lyra-btn lyra-btn--ghost lyra-btn--md" x-bind="cancelButton">
Cancelar
</button>
<button
class="lyra-btn lyra-btn--primary lyra-btn--md"
form="workspace-create-form"
x-bind="createButton"
>
<span class="lyra-btn__spinner" aria-hidden="true" x-show="pending"></span>
Criar workspace
</button>
</div>
</div>
</div>
</div>Na aplicação, associe um AbortController a cada operationId. Em
cancelarRequisicao(id), aborte a requisição e chame cancel(id) quando ela terminar. Para erro
de validação do servidor, envie fieldErrors e uma message legível a reject. O binding não
inicia requisições.
Este componente compõe Dialog, Avatar, Input e Button. Sem React, as mesmas classes fornecem a aparência; a regra nome-para-slug, o reset, o foco e o comportamento de fechamento são seus:
<div class="lyra-dialog-overlay">
<div class="lyra-dialog" role="dialog" aria-modal="true" aria-labelledby="workspace-title">
<div class="lyra-dialog__header">
<h2 class="lyra-dialog__title" id="workspace-title">Criar workspace</h2>
<button class="lyra-dialog__close" type="button" aria-label="Close">×</button>
</div>
<div class="lyra-dialog__body">
<div class="lyra-wscreate">
<div class="lyra-wscreate__preview">
<span class="lyra-avatar lyra-avatar--lg lyra-avatar--square" title="Acme Inc">
<span aria-hidden="true">AI</span>
</span>
<span class="lyra-wscreate__preview-hint">O avatar usa as iniciais do nome.</span>
</div>
<div class="lyra-field">
<label class="lyra-label" for="workspace-name">Nome do workspace</label>
<input class="lyra-input" id="workspace-name" value="Acme Inc" />
</div>
<div class="lyra-field">
<label class="lyra-label" for="workspace-slug">URL</label>
<span class="lyra-wscreate__slug">
<span class="lyra-wscreate__slug-prefix">lyra.dev/</span>
<input class="lyra-wscreate__slug-input" id="workspace-slug" value="acme-inc" />
</span>
<span class="lyra-hint">Letras minúsculas, números e hífens.</span>
</div>
</div>
</div>
<div class="lyra-dialog__footer">
<button class="lyra-btn lyra-btn--ghost lyra-btn--md" type="button">Cancelar</button>
<button class="lyra-btn lyra-btn--primary lyra-btn--md" type="button">Criar workspace</button>
</div>
</div>
</div>