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: The released Blade v0.10.0 API snapshot has no component for this catalog entry.

      NameTypeRequiredDescription
      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({ … })"

      OptionTypeRequiredDescription
      slugPrefixstring—Text before the editable slug. Default `lyra.dev/`.
      messagesPartial<LyraCreateWorkspaceMessages>—Validation and error copy.

      lyraCreateWorkspaceDialog composes lyraDialog; keep the trigger and modal in one x-data root. The binding owns the form state and focus, while your application owns persistence. A valid submit emits lyra:create-workspace with { operationId, name, slug } (trimmed name). Respond with accept(operationId) or reject(operationId, { fieldErrors: { name?, slug? }, message? }) on the same Alpine data object. Missing acknowledgement leaves the dialog pending. The button and fields are locked during submit. reject focuses the role="alert" summary; local validation focuses the first invalid field. A repeated submit creates no second request.

      Closing during submit emits lyra:create-workspace:cancel with { operationId } once and enters canceling. Abort your request, then call cancel(operationId) to close. A matching accept closes; a matching reject shows the error. A stale acknowledgement is ignored. An abort signal is a request to stop work, not proof that persistence rolled back. Closing outside a pending request returns focus to the trigger through lyraDialog. Setting open = false externally or removing the component also emits the cancellation event for an active request; its later acknowledgement is ignored.

      html
      <div
        x-data="lyraCreateWorkspaceDialog()"
        @lyra:create-workspace="
          const { operationId, name, slug } = $event.detail;
          createWorkspace({ name, slug }).then(
            () => accept(operationId),
            (error) => reject(operationId, { message: error.message })
          )
        "
        @lyra:create-workspace:cancel="abortWorkspace($event.detail.operationId)"
      >
        <button id="create-workspace-trigger" type="button" @click="open = true">Create 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">Create workspace</h2>
              <button class="lyra-dialog__close" x-bind="close" aria-label="Close">×</button>
            </div>
            <div class="lyra-dialog__body">
              <form
                id="workspace-create-form"
                class="lyra-wscreate"
                aria-label="Create 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">The avatar uses the name initials.</span>
                </div>
                <div
                  class="lyra-hint lyra-hint--error"
                  data-lyra-wscreate-error
                  aria-label="Workspace creation error"
                  x-bind="errorSummary"
                ></div>
                <div class="lyra-field">
                  <label class="lyra-label" for="workspace-name">Workspace name</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">
                    Lowercase letters, numbers, and hyphens.
                  </span>
                </div>
              </form>
            </div>
            <div class="lyra-dialog__footer">
              <button class="lyra-btn lyra-btn--ghost lyra-btn--md" x-bind="cancelButton">Cancel</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>
                Create workspace
              </button>
            </div>
          </div>
        </div>
      </div>

      Use an operation-ID keyed AbortController map in your application. In abortWorkspace(id), abort that controller and call cancel(id) when its request has settled. For a server validation failure, pass both fieldErrors and a readable message to reject. The binding never starts a request.

      This component composes Dialog, Avatar, Input and Button. Without React, the same classes supply the appearance; the name-to-slug rule, reset behavior, focus and closing behavior are yours to implement:

      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">Create 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">The avatar uses the name initials.</span>
              </div>
              <div class="lyra-field">
                <label class="lyra-label" for="workspace-name">Workspace name</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">Lowercase letters, numbers, and hyphens.</span>
              </div>
            </div>
          </div>
          <div class="lyra-dialog__footer">
            <button class="lyra-btn lyra-btn--ghost lyra-btn--md" type="button">Cancel</button>
            <button class="lyra-btn lyra-btn--primary lyra-btn--md" type="button">
              Create workspace
            </button>
          </div>
        </div>
      </div>