feat(places): M5.a — places adopt the unified visibility system

Sixth consumer of @mana/shared-privacy. Places now carry a VisibilityLevel
flipped via <VisibilityPicker> in the Places DetailView; the new
places.places embed powers "my favourite cafes" / "rehearsal rooms" /
"gyms I train at" sections on the owner's website.

Changes:
- places/types: visibility + unlistedToken + visibilityChangedAt +
  visibilityChangedBy on LocalPlace; Place (UI type) requires visibility
- places/queries: toPlace forwards visibility with 'space' fallback for
  legacy rows
- places/stores/places: createPlace stamps
  defaultVisibilityFor(activeSpace.type); new setVisibility(id, level)
  mints/clears the unlisted token on the transition boundary and emits
  cross-module VisibilityChanged
- places/views/DetailView: <VisibilityPicker> as the first field-row,
  above Kategorie

website embed:
- website-blocks/moduleEmbed/schema: 'places.places' added to
  EmbedSourceSchema; filter docstring describes the places-specific
  reuse of existing kind/isFavorite/tagIds filter fields
- website/embeds: resolvePlaces gates hard on canEmbedOnWebsite,
  applies optional kind (→ PlaceCategory) / isFavorite / tagIds
  filters, sorts favourites-first then alphabetical.

Privacy: Whitelist (title + address only). Latitude/longitude are
explicitly NOT inlined — 10m precision of a home or workplace can
identify someone, and silently publishing coords on a visibility flip
would be the classic leak the design was built to prevent (plan §2).

Verified:
- pnpm check (web): 7450 files, 0 errors

Next: M5.b — Events (socialEvents), Recipes, Wardrobe-Outfits, Habits,
Quiz, Invoices-Clients. Same pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-24 13:59:15 +02:00
parent 0cebb2411e
commit 2af2a4d5c0
6 changed files with 109 additions and 0 deletions

View file

@ -32,6 +32,7 @@ export const EmbedSourceSchema = z.enum([
'calendar.events',
'todo.tasks',
'goals.goals',
'places.places',
]);
export type EmbedSource = z.infer<typeof EmbedSourceSchema>;
@ -56,6 +57,9 @@ export const ModuleEmbedSchema = z.object({
* goals.goals: { status? } 'active' | 'completed' filter;
* useful for "currently working on" vs "things
* I've hit" progress sections
* places.places: { kind? (mapped to PlaceCategory),
* isFavorite?, tagIds? } "my favourite cafes",
* "rehearsal rooms I use", etc.
*/
filter: z
.object({