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 onCreate is 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" with aria-modal="true" and an accessible name from title. 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, an operationId, and an initially unaborted owned signal. 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: accepted requests onClose, rejected keeps the values, focuses the error summary, and permits a retry, and canceled returns 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 onClose immediately. While an operation is pending, user close first aborts the owned signal once and waits for that operation's matching canceled acknowledgement; that matching canceled acknowledgement requests onClose; a matching accepted acknowledgement requests onClose, while a matching rejection stays open as an error. An abort notification does not guarantee that application persistence rolled back.
      • A parent setting controlled open to false (or unmounting) aborts and invalidates pending work; late results are ignored. The next open resets the form. onClose is a controlled close request, so update open in response. returnFocusTo forwards its destination resolver to Dialog and uses Dialog's documented eligibility and fallback contract.
      • slugify lowercases, removes accents, collapses non-alphanumerics to hyphens and trims edge hyphens: Ação Global becomes acao-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.

      NomeTipoObrigatóriaDescrição
      openboolean—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.
      titlestring—Dialog heading. Default `"Create workspace"`.
      slugPrefixstring—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.

      tsx
      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.

      diff
      - <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çãoTipoObrigatóriaDescrição
      slugPrefixstring—Text before the editable slug. Default `lyra.dev/`.
      messagesPartial<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.

      html
      <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:

      html
      <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>