# Migration: 0.2.0 to 1.0.0

1.0.0 is not compatible with 0.x. Do `@v-office/website-headless`'s 1.0.0 `MIGRATION.md` first
(dependencies, `SDK`, the site constants, `enhance()`), then these steps, in one change. A site that
copied kit code instead of importing 0.x follows the same steps: each switched surface's kit copy
is replaced by the component below and then deleted. The README (in this package) has every prop,
default, part and copy key.

## 1. Dependencies

```sh
pnpm add --save-exact @v-office/website-components@1.0.0 @v-office/website-headless@1.0.0 @v-office/website-sdk@2.28.1
pnpm add --save-exact maplibre-gl@<5.x>        # only where a page shows a map
pnpm add --save-exact @v-office/site-manifest@0.3.0   # so the build reports ContactForm
```

Pin `@v-office/website-sdk` 2.28.1 (the floor is 2.25.0; from 2.28.0 the guest selectors apply the v9
children and pets rules, so never pin below the version a site already has). With site-manifest
0.3.0 the build then
reports the SDK it constructed, and the platform keeps the website's public token and SDK options
current. Pass the one `SDK` setup to every surface. `sdkMode="all-rentals-at-once"` counts as a second SDK
on the same token: the token stays current, the stored SDK options are kept. A second setup with
another token corrects neither.

The peers are `@v-office/website-headless >=1.0.0 <2`, `@v-office/website-sdk >=2.25.0 <3`,
`react >=19`, `react-dom >=19` and the optional `maplibre-gl >=5 <6`. Run `pnpm install` so the
lockfile matches.

## 2. The stylesheet

The layer order is the stylesheet's first line, `properties` first:

```css
@layer properties, theme, base, voffice-tokens, voffice-components, components, utilities;
@import "tailwindcss";
@import "@v-office/website-components/css/components.css";
@import "@v-office/website-components/css/map.css"; /* pages with a map; needs maplibre-gl */
@import "@v-office/website-components/css/motion.css"; /* only where the site had the hover and press motions */
/* @import "@v-office/website-components/css/tokens.css";  only on a site without a shadcn theme */
```

- 0.1.x started the order at `theme`. Without `properties` first, Tailwind's `@property` fallbacks
  land last and beat the site's own utilities in older Safari and Firefox. Instead of the line, the
  first import may be `@v-office/website-components/css/layers.css`.
- The package reads the shadcn variables (`--background`, `--foreground`, `--primary`, `--muted`,
  `--border`, `--radius`, …) and never `--color-*`. Map the brand onto them, and onto the optional
  axes where the kit had its own variables: `--color-accent-text` → `--accent-text`,
  `--color-action-hover` → `--primary-hover`. `--primary` is the one conversion action; a site
  whose action colour must stay exclusive sets `--selection` for every other active state.
- Shapes and shadows are axes too: `--radius-card`, `--radius-pill` (what is pressed),
  `--radius-label` (what only says something: card pills, badges, price pins; falls back to
  `--radius-pill`), `--radius-control`, `--radius-badge`, `--shadow-card` (`none` removes every
  package shadow), `--shadow-overlay` (popovers and select lists; falls back to `--shadow-card`).
  "Buttons square, labels round" is `--radius-pill: 0.25rem; --radius-label: 9999px;`.
- Site rules that restyled package markup key on `data-part` and state attributes in the site's
  `components` layer; they win without a specificity fight. Classes are no contract.

## 3. Site constants

Beside headless's constants (`SDK`, `SEARCH_CONTRACT`, `GUESTS`, `DAY_PRICES`, `POLICY_CHOICE`,
`BOOKABLE`, `ADDRESS`, `FAVORITES_KEY`), write:

| constant  | type                | passed to                                                                     |
| --------- | ------------------- | ----------------------------------------------------------------------------- |
| `VOICE`   | `Voice`             | every component (`voice`): `"du"` or `"sie"` for a site that addresses guests |
| `CARD`    | `RentalCardOptions` | `Showcase`, `SearchResults`, `PrerenderedResults`, `SearchResultsMap`         |
| `PAYMENT` | `BookingPayment`    | `Booking` (`payment`; `methods` from the customer, never from the old page)   |

Pass the same constant everywhere it is taken: two surfaces with different `SDK` setups get two SDK
instances and two rate limits, and different `CARD` objects make cards and map pins disagree.

Write the site's copy overrides as constants through headless `defineCopy`
(`defineCopy<ShowcaseCopy>()({ moreCount: "Alle {count} {label}" })`, `defineCopy<SharedCopyOverrides>()`
for `shared`). A misspelt key or a token the component does not fill then fails `astro check`.

## 4. The surfaces

Every SDK-using component takes `sdk={SDK}` (an `SdkSetup`, was `SdkConfig`). Every component takes
`voice`. Routes have no defaults; unless the site's routes differ, they are the ones in the examples.

**`SearchBar`** (`client:load`)

- Add `action` (the search route, e.g. `"/suchen"`) and `contract={SEARCH_CONTRACT}` (replaces
  `backend`).
- `bookableFrom={date}` → `dates={{ bookable: BOOKABLE }}` (ISO days, with `noticeUntil` and
  `contact`); `flexiblePeriods={false}` → `dates={{ flexibleStays: [] }}`.
- `regions` are `{ value, label?, count?, group? }` objects.
- Remove `filterDefs`: the bar carries every URL pair it does not own.
- The mobile form is `mobile="steps"` (default), `"scroll"` or `"inline"`.

**`SearchResults`** (`client:load`)

- Add `contract={SEARCH_CONTRACT}` and `rentalHref` (e.g. `"/unterkuenfte/{slug}"`; replaces
  `rentalsPath`).
- `filterDefs` → `filters={resolveFilters(await sdk.static.filter.getFilters({ locale }), spec)}`
  at build; `rentalFacts` → `facts={rentalFactsMap(sources)}`; card settings → `card={CARD}`.
- A map, a count, sort, filters or chips elsewhere on the page are their own islands
  (`SearchResultsMap`, `SearchCount`, `SearchSort`, `SearchFilters`, `SearchFilterChips`) and read
  the same store by `storeKey`. Remove listeners for `voffice:search-results` events.
- A prerendered listing renders `PrerenderedResults` (never with a client directive) and inlines
  headless `resultsQueryFlagScript()` in `<head>`.

**`Showcase`**

- `groups` are headless `ShowcaseGroup`s: build them at build time with `buildShowcaseGroups`
  (or `selectRentals`) over `cardSource(rental, { href })`. `ShowcaseRental` with `badges`,
  `water` and `ownerPills` is gone; badges and pills are `CARD` keys.
- Use `layout`, `gridFrom`, `columns`, `arrows`, `rotate`, `moreTile` (with `moreLabels` per group
  id) as the page had them.

**`Quote`**

- `rentalId` and `petsAllowed` → `rental={{ id, maxGuests?, petsAllowed? }}` (leave out a key that is
  not known); `checkoutPath` → `checkoutHref` (required, e.g. `` `/buchen/${slug}` ``; `null` for an
  enquiry-only page).
- Pass `guests={GUESTS}`, `dayPrices={DAY_PRICES}`, `policyChoice={POLICY_CHOICE}` and
  `bookable={BOOKABLE}` where the site has them.
- The bottom bar is `variant="bar"` in no wrapper: the package fixes it to the viewport and hides it
  from `barBelow` (default `"lg"`); `barLayout="floating"` makes it a centred card from md. Delete the
  site's own bar positioning CSS and keep room below the footer.
- A not-bookable-yet sentence that leads into the contact link places it with `{contact}` in
  `bookableFromNotice` (and `deferredNotice` on the bar).

**`Booking`** (`client:load`, at the checkout route)

- `rentalId`, `rentalName`, `rentalLocation`, `rentalHref`, `images` and `petsAllowed` →
  `rental={{ id, name, href, location, images, facts, maxGuests?, petsAllowed? }}`.
- `legal={{ agb, privacy }}` HTML → `legal={{ terms: { href: "/agb", document? }, privacy: { href:
"/datenschutz", document? } }}`, the documents from headless `fetchLegalDocuments` at build.
- Add `returnPath` (the page that renders `BookingReturn`, e.g. `"/buchen/bestaetigung"`),
  `payment={PAYMENT}`, `countries={countryOptions({ locale, featured })}`, and `guests`,
  `dayPrices`, `policyChoice` as on `Quote`. Pass `policyCopy` to both with the same object.
- `CheckoutFlowCopy` overrides → `CheckoutCopy` keys; `submitLabel` and `optionSubmitLabel` must stay
  within headless `SUBMIT_WORDING` (§ 312j BGB).

**`ContactForm`** (`client:load`, new)

- Replaces a copied contact kit: `sdk={SDK}`, `supportEmail` (required, `null` allowed),
  `countries`, `subject` per form, `fields` (`DEFAULT_CONTACT_FIELDS` or the site's specs),
  `privacyHref`.

**`SearchResultsMap`** (`./map`, `client:load`)

- It reads the page's search store; remove the event wiring. `style` → `styleUrl`,
  `DEFAULT_CENTER` → `emptyView`. Import `css/map.css`.

## 5. The card, the rental page and the maps

- Every card is `RentalCard` with the site's `CARD`; the kit card copy goes.
- Replace the copied rental-detail sections with the blocks (`RentalHeading`, `RentalActions`,
  `RentalGallery`, `RentalFactList`, `RatingBadge`, `RentalDescription`, `RentalAmenities`,
  `RentalRooms`, `RentalReviews`, `RentalProperty`, `RentalDistances`), server-rendered without a
  client directive, from headless `loadRentalCatalogue` (`RentalDetail`). `RentalHeading` and
  `RentalMap` require `address={ADDRESS}` and throw at render without it.
- `RentalFactList` takes site-derived `extraFacts` (strings or `{ label, glyph? }`); `"extra"` in
  `facts` places them.
- Legal and description texts render through `RichText`; a legal document that opens with its own
  title takes `firstHeading="title"` and `title={document.title}`.
- Map iframes and copied map code become `RentalMap` and `LocationMap` (`./map`). Tiles load on
  click; `loadOn="near"` only where the site's consent covers the tile vendor, `loadOn="consent"`
  with `consentCategory` where a consent host sets `data-consent-granted` on `<html>` and dispatches
  `voffice:consent`.
- `AvailabilityCalendar` replaces an occupancy calendar; `BookingReturn` renders the page at
  `returnPath`.

## 6. Own UI over headless

A site that keeps its own UI for a surface imports the binding (`useSearchForm`, `useSearchStore`,
`useQuoteFlow`, `useCheckoutFlow`, `useGuestForm`, `useContactForm`) or the headless store, and its
manifest reports that surface as `custom`. A custom checkout keeps headless `CHECKOUT_PARTS` and
`BOOKING_KIND_ATTRIBUTE` on its markup.

## 7. Defaults that change what the site shows

| 1.0.0 default                                            | to keep what the site showed                                                  |
| -------------------------------------------------------- | ----------------------------------------------------------------------------- |
| neutral German copy                                      | `voice="du"` or `"sie"` on every component                                    |
| calendar day prices `"rounded"` on `Quote` and `Booking` | `DAY_PRICES = "none"` passed to `Quote`, `AvailabilityCalendar` and `Booking` |
| no payment glyphs unless `payment.methods` lists them    | the methods the customer's account takes                                      |
| map tiles load on click                                  | `loadOn`, as in step 5                                                        |
| no hover zoom, lift or press dip                         | import `css/motion.css`                                                       |
| an eyebrow without a feed type prints the town only      | `copy={{ townOnly: "Ferienwohnung in {town}" }}` on `RentalHeading`           |
| floor plans apart from the photo strip                   | nothing to set; check the gallery                                             |

## 8. Events

- `onRateConfigurationFault` is gone: listen on `window` for `voffice:quote` and
  `voffice:checkout` (`detail.type === "rate-fault"`). `voffice:checkout` also reports `booked` and
  `payment-started`, `voffice:contact` reports `sent`. Details carry no personal data; the names
  and detail types are headless constants (`QUOTE_EVENT`, `CHECKOUT_EVENT`, `CONTACT_EVENT`).
- `SEARCH_RESULTS_EVENT` and `SEARCH_RESULTS_REQUEST_EVENT` are gone; nothing replaces them.

## 9. Delete

- The kit copies of every switched surface, pinned and unpinned (a dead copy keeps a kit check
  gating it), the copied card, rental-detail sections, carousel, gallery, favourites and map code.
- Site CSS that positioned the quote bar, re-declared Tailwind theme variables for the package, or
  restyled package markup by class.
- Imports of `getCountryGroups`, `RentalFacts`, `RentalWater`, `DISTANCE_ADJACENT`,
  `ShowcaseRental`, `ShowcaseImage`, `CheckoutImage` and `RenderedLegalDoc`.

## 10. Check

Run the site's tests, `astro check` (0 errors) and a build, then read the output: every
`dist/_astro/*.css` starts its layer order with `properties`, and `dist/site-manifest.json` reports
the switched surfaces as `imported`. Never press the final booking, payment or contact submit
against a real PMS.

## What stays the same

- Storage keys: `<namespace>-merkliste`, `voffice:checkout-context`, `voffice:booking-return`, so
  every guest's Merkliste survives the switch when `FAVORITES_KEY` is byte-identical.
- Every URL key and the German sort values (`empfohlen`, `preis-auf`, …): links, bookmarks and mails
  keep working.
- The gate markers on the same elements: `data-result-card`, `data-rental-stats`, `data-fav*`,
  `.card-images`, `data-img-*`, `data-skeleton-field`, the description and amenity markers.
- The `merkliste:changed` event.
