ServicesCard
The studio's menu in setup, read at a glance: each service a grey box of its own — its name, then minutes · price — with Edit and a light red Remove. Edit and Add service open the service's sheet (a bottom sheet on a phone, a dialog from sm): name, length in minutes, price, then Save or Add. A duration the source didn't give is Okami's guess, marked ◦ until the owner sets it. Removing a service asks first, then offers Undo; with nothing yet, the card can start from the craft's template.
Playground
Services
Manicure
45 min · €25
Gel nails — new set
◦ 90 min · €45
Pedicure
60 min · €30
◦ Okami's guess. Check these durations.
Edit a field: the patch it sends shows here.
import { ServicesCard } from "@/components/shared/services-card";
<ServicesCard
services={draft.services}
currency={draft.currency}
onChange={(patch) => saveDraft(patch)}
onUseTemplate={() => useTemplate(draft.vertical)}
/>Guessed durations
Price lists often skip durations, so an import fills them from the template and marks them. The ◦ sits inside the minutes field, and one line under the list says what it means — read with each marked field. Setting the minutes clears the mark; changing the name or price doesn't.
Services
Manicure
45 min · €25
Gel nails — new set
◦ 90 min · €45
Pedicure
60 min · €30
◦ Okami's guess. Check these durations.
import { ServicesCard } from "@/components/shared/services-card";
<ServicesCard services={draft.services} onChange={saveDraft} />Adding, removing, undo
Edit opens a service's sheet, filled in; Add service opens it empty, at 30 minutes. Save or Add checks the row whole — a name another service has, minutes outside 5 to 480, a price past the cap — and points at the field to fix; Cancel, the X or a drag down lets it go. Remove asks first, then offers Undo in a toast that stays until it's taken or dismissed; Undo puts the service back where it was.
Services
No services yet.
import { ServicesCard } from "@/components/shared/services-card";
<ServicesCard
services={draft.services}
onChange={saveDraft}
onUseTemplate={() => useTemplate(draft.vertical)}
/>Collapsed
import { ServicesCard } from "@/components/shared/services-card";
<ServicesCard services={draft.services} onChange={saveDraft} collapsed />API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| services | DraftService[] | — | The draft's services, with their ids, origin and estimate marks. |
| onChange | (patch: DraftPatch) => void | — | Called with the whole checked list after each saved change. |
| onUseTemplate | () => void | — | Fill the list from the craft's template. Offered only while the list is empty. |
| labels | Partial<ServicesCardLabels> | English | Every word, including remove(name), removed(name) and summary(count, lowest). |
| locale | string | "en" | How prices and the currency's sign are written: €25 in English, 25 € in Slovak. |
| currency | "EUR" | "CZK" | "EUR" | What the prices are in. Its sign heads the price column, it caps a price (10,000 € or 250,000 Kč), and the picker in the card's head switches it by sending { currency }. |
| collapsed | boolean | false | One line: how many services, from the lowest price. |
| hideTitle | boolean | false | The heading is read, not shown — in the conversation, where Okami's line introduces the card. |
Guidelines
- Keep the owner's words. Imported names stay as the source wrote them; only the template's are translated.
- Don't ask the owner to confirm each guess. The mark and its one line are enough; the Summary is the last look.
- Buffers aren't shown in setup. They come from the template and are tuned on the Services page.