Dialog

Share Dialog

A share dialog: the link in a read-only field, a copy button, and the list of people who already have access.

  • dialog
  • share
  • link
  • access

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/dialog-008?lang=en

Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "dialog-008" (Share Dialog) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/dialog-008.json

Registry item: https://vibeui.ru/r/dialog-008.json
Installs to: components/vibeui/dialog-008.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
A share dialog: the link in a read-only field, a copy button, and the list of people who already have access.

An access dialog: the link in a read-only field with a copy button, and beneath it the members with their initials and roles. The initials colour is derived from the name. Zero dependencies, one file, its own palette.

## 3. How to use it
import { Dialog008 } from "@/components/vibeui/dialog-008"

<Dialog008
  id="share"
  link="https://vibeui.ru/p/studio"
  people={[{ name: "Anna Kovalyova", role: "Editor" }]}
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-dialog-008-* palette — do not swap it for your theme tokens (bg-muted, bg-primary and the like)
- the link in a read-only field rather than as text: it gets selected and copied whole
- the monospaced link — hyphens and ones have to be distinguishable
- the list of existing members: before inviting someone new, people check who is already there
- deriving the initials colour from the name rather than at random
- styling the window by attribute and the palette through its own style — the window sits in the top layer

## 6. You may change
- link — the address itself
- the people array: name and role
- the title, accessLabel and button copy
- the copy handler: attach onClick to the button in your copy
- the accent through the accent prop — the copy button and focus ring

## 7. Rules
- The component does not write to the clipboard: navigator.clipboard needs client code and stays yours.
- Do not list more than five members here: move the rest to an access page.
- The role is what a person may do, not their job title: "Editor", not "Designer".
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/dialog-008.json
https://vibeui.ru/r/dialog-008.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-dialog-008-*. Ссылка лежит в readOnly-поле моноширинным: её видно целиком и можно выделить. Цвет инициалов в списке доступа выводится из имени детерминированной функцией — тот же приём, что в avatar-001, поэтому один участник всегда одного цвета. Компонент серверный: копирование вешает вызывающий код.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { CSSProperties } from "react"

export type Dialog008Person = {
  name: string
  role: string
}

export type Dialog008Props = {
  id?: string
  trigger?: string
  title?: string
  link?: string
  /** Кто уже имеет доступ: показывается списком инициалов. */
  people?: Dialog008Person[]
  accessLabel?: string
  copyLabel?: string
  doneLabel?: string
  accent?: string
  className?: string
  style?: CSSProperties
}

// Идея компонента: окно «поделиться». Ссылка стоит первой в поле только для
// чтения — её видно целиком и можно выделить, а рядом кнопка копирования; под
// ней список тех, у кого доступ уже есть: прежде чем звать нового, человек
// проверяет, кто там. Цвет инициалов выводится из имени, как в аватаре,
// поэтому один участник всегда одного цвета. Компонент остаётся серверным:
// копирование вешает вызывающий код.
const STYLES = `
:where([data-vibeui-block="dialog-008"]){
--vibeui-dialog-008-fg:oklch(0.22 0.016 265);
--vibeui-dialog-008-muted:oklch(0.5 0.014 265);
--vibeui-dialog-008-bg:oklch(1 0 0);
--vibeui-dialog-008-panel:oklch(0.975 0.003 265);
--vibeui-dialog-008-border:oklch(0.89 0.006 265);
--vibeui-dialog-008-accent:oklch(0.55 0.2 262);
--vibeui-dialog-008-radius:1rem;
--vibeui-dialog-008-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
--vibeui-dialog-008-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="dialog-008"]{display:inline-flex;font-family:var(--vibeui-dialog-008-font)}
[data-vibeui-block="dialog-008"] [data-part="trigger"]{
appearance:none;cursor:pointer;font:inherit;font-size:0.875rem;font-weight:500;
display:inline-flex;align-items:center;height:2.25rem;padding:0 0.9375rem;
border:1px solid var(--vibeui-dialog-008-border);border-radius:0.5rem;
background:var(--vibeui-dialog-008-bg);color:var(--vibeui-dialog-008-fg);
}
[data-vibeui-block="dialog-008"] [data-part="trigger"]:hover{background:color-mix(in oklab,var(--vibeui-dialog-008-border) 30%,transparent)}
[data-vibeui-block="dialog-008"] [data-part="trigger"]:focus-visible{outline:2px solid var(--vibeui-dialog-008-accent);outline-offset:2px}
[data-vibeui-dialog-008-window]{
position:fixed;inset:0;margin:auto;height:fit-content;
width:min(27rem,calc(100vw - 2rem));box-sizing:border-box;padding:1.375rem;
border:1px solid var(--vibeui-dialog-008-border,oklch(0.89 0.006 265));
border-radius:var(--vibeui-dialog-008-radius,1rem);
background:var(--vibeui-dialog-008-bg,oklch(1 0 0));
color:var(--vibeui-dialog-008-fg,oklch(0.22 0.016 265));
font-family:var(--vibeui-dialog-008-font,ui-sans-serif,system-ui,sans-serif);
box-shadow:0 24px 60px -24px oklch(0.2 0.03 265 / 45%);
opacity:0;transform:scale(0.97);
transition:opacity .18s ease,transform .18s ease,display .18s allow-discrete,overlay .18s allow-discrete;
}
[data-vibeui-dialog-008-window]:popover-open{opacity:1;transform:none}
@starting-style{[data-vibeui-dialog-008-window]:popover-open{opacity:0;transform:scale(0.97)}}
[data-vibeui-dialog-008-window]::backdrop{background:oklch(0.18 0.02 265 / 45%)}
[data-vibeui-dialog-008-window] [data-part="title"]{margin:0 0 0.875rem;font-size:1rem;font-weight:620;line-height:1.35}
[data-vibeui-dialog-008-window] [data-part="row"]{display:flex;gap:0.5rem}
[data-vibeui-dialog-008-window] input{
flex:1 1 auto;min-width:0;box-sizing:border-box;margin:0;height:2.375rem;padding:0 0.75rem;
border:1px solid var(--vibeui-dialog-008-border,oklch(0.89 0.006 265));
border-radius:0.5rem;background:var(--vibeui-dialog-008-panel,oklch(0.975 0.003 265));
color:inherit;font-family:var(--vibeui-dialog-008-mono,ui-monospace,monospace);font-size:0.8125rem;
}
[data-vibeui-dialog-008-window] input:focus{
outline:none;border-color:var(--vibeui-dialog-008-accent,oklch(0.55 0.2 262));
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-dialog-008-accent,oklch(0.55 0.2 262)) 20%,transparent);
}
[data-vibeui-dialog-008-window] [data-part="copy"]{
appearance:none;cursor:pointer;font:inherit;font-size:0.8125rem;font-weight:600;flex:none;
display:inline-flex;align-items:center;height:2.375rem;padding:0 0.875rem;
border:0;border-radius:0.5rem;
background:var(--vibeui-dialog-008-accent,oklch(0.55 0.2 262));color:oklch(1 0 0);
}
[data-vibeui-dialog-008-window] [data-part="copy"]:hover{filter:brightness(0.94)}
[data-vibeui-dialog-008-window] [data-part="access"]{
margin:1.125rem 0 0.5rem;font-size:0.75rem;font-weight:650;letter-spacing:0.05em;text-transform:uppercase;
color:var(--vibeui-dialog-008-muted,oklch(0.5 0.014 265));
}
[data-vibeui-dialog-008-window] ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:0.5rem}
[data-vibeui-dialog-008-window] li{display:flex;align-items:center;gap:0.625rem;font-size:0.875rem}
[data-vibeui-dialog-008-window] [data-part="avatar"]{
display:flex;align-items:center;justify-content:center;flex:none;
width:1.75rem;height:1.75rem;border-radius:9999px;
background:oklch(0.92 0.05 var(--vibeui-dialog-008-hue,250));
color:oklch(0.38 0.09 var(--vibeui-dialog-008-hue,250));
font-size:0.6875rem;font-weight:650;
}
[data-vibeui-dialog-008-window] [data-part="role"]{margin-left:auto;font-size:0.75rem;color:var(--vibeui-dialog-008-muted,oklch(0.5 0.014 265))}
[data-vibeui-dialog-008-window] [data-part="actions"]{display:flex;justify-content:flex-end;margin-top:1.25rem}
[data-vibeui-dialog-008-window] [data-part="done"]{
appearance:none;cursor:pointer;font:inherit;font-size:0.875rem;font-weight:600;
display:inline-flex;align-items:center;height:2.25rem;padding:0 1rem;
border-radius:0.5rem;background:transparent;color:inherit;
border:1px solid var(--vibeui-dialog-008-border,oklch(0.89 0.006 265));
}
[data-vibeui-dialog-008-window] :focus-visible{outline:2px solid var(--vibeui-dialog-008-accent,oklch(0.55 0.2 262));outline-offset:2px}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="dialog-008"] *{animation:none!important;transition:none!important}
[data-vibeui-dialog-008-window]{transition:none!important;opacity:1;transform:none}
}
`

const DEFAULT_PEOPLE: Dialog008Person[] = [
  { name: "Анна Ковалёва", role: "Редактор" },
  { name: "Дмитрий Орлов", role: "Читатель" },
  { name: "Сергей Мохов", role: "Владелец" },
]

function hueOf(name: string): number {
  let sum = 0

  for (let index = 0; index < name.length; index += 1) {
    sum = (sum + name.charCodeAt(index) * (index + 1)) % 360
  }

  return sum
}

function initialsOf(name: string): string {
  return name
    .trim()
    .split(/\s+/)
    .slice(0, 2)
    .map((word) => word.charAt(0).toUpperCase())
    .join("")
}

/**
 * Окно «поделиться»: ссылка, копирование и список тех, у кого есть доступ.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Dialog008({
  id = "vibeui-dialog-008",
  trigger = "Поделиться",
  title = "Доступ к проекту",
  link = "https://vibeui.ru/p/studio-polet",
  people = DEFAULT_PEOPLE,
  accessLabel = "Уже есть доступ",
  copyLabel = "Копировать",
  doneLabel = "Готово",
  accent,
  className,
  style,
}: Dialog008Props) {
  const palette = {
    ...(accent ? { "--vibeui-dialog-008-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-dialog-008" precedence="medium">
        {STYLES}
      </style>
      <div data-vibeui-block="dialog-008" className={className} style={palette}>
        <button data-part="trigger" type="button" popoverTarget={id}>
          {trigger}
        </button>
        <div
          id={id}
          popover="auto"
          data-vibeui-dialog-008-window=""
          role="dialog"
          aria-labelledby={`${id}-title`}
          style={palette}
        >
          <h2 data-part="title" id={`${id}-title`}>
            {title}
          </h2>
          <div data-part="row">
            <input
              type="text"
              value={link}
              readOnly
              aria-label="Ссылка на проект"
            />
            <button data-part="copy" type="button">
              {copyLabel}
            </button>
          </div>
          <p data-part="access">{accessLabel}</p>
          <ul>
            {people.map((person) => (
              <li key={person.name}>
                <span
                  data-part="avatar"
                  aria-hidden="true"
                  style={
                    {
                      "--vibeui-dialog-008-hue": hueOf(person.name),
                    } as CSSProperties
                  }
                >
                  {initialsOf(person.name)}
                </span>
                {person.name}
                <span data-part="role">{person.role}</span>
              </li>
            ))}
          </ul>
          <div data-part="actions">
            <button data-part="done" type="button" popoverTarget={id}>
              {doneLabel}
            </button>
          </div>
        </div>
      </div>
    </>
  )
}