foldkit-primitives 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +202 -13
  2. package/dist/device/index.d.mts.map +1 -1
  3. package/dist/device/index.mjs +95 -87
  4. package/dist/device/index.mjs.map +1 -1
  5. package/dist/dom/index.d.mts +71 -3
  6. package/dist/dom/index.d.mts.map +1 -1
  7. package/dist/dom/index.mjs +303 -6
  8. package/dist/dom/index.mjs.map +1 -1
  9. package/dist/editable-text-CKkS_mWt.d.mts +235 -0
  10. package/dist/editable-text-CKkS_mWt.d.mts.map +1 -0
  11. package/dist/events/index.d.mts +8 -1
  12. package/dist/events/index.d.mts.map +1 -1
  13. package/dist/events/index.mjs +32 -17
  14. package/dist/events/index.mjs.map +1 -1
  15. package/dist/follow-tab-stop-DpxwLXvU.mjs +800 -0
  16. package/dist/follow-tab-stop-DpxwLXvU.mjs.map +1 -0
  17. package/dist/index.d.mts +6 -6
  18. package/dist/index.mjs +7 -7
  19. package/dist/interaction/index.d.mts +247 -92
  20. package/dist/interaction/index.d.mts.map +1 -1
  21. package/dist/interaction/index.mjs +507 -224
  22. package/dist/interaction/index.mjs.map +1 -1
  23. package/dist/internal-DP-xLKbT.mjs +37 -0
  24. package/dist/internal-DP-xLKbT.mjs.map +1 -0
  25. package/dist/media/index.d.mts.map +1 -1
  26. package/dist/media/index.mjs +3 -3
  27. package/dist/media/index.mjs.map +1 -1
  28. package/dist/motion/index.mjs +1 -1
  29. package/dist/{motion-LfJYAMnP.mjs → motion-DGGBZ0wt.mjs} +14 -13
  30. package/dist/motion-DGGBZ0wt.mjs.map +1 -0
  31. package/dist/net/index.d.mts +69 -5
  32. package/dist/net/index.d.mts.map +1 -1
  33. package/dist/net/index.mjs +139 -56
  34. package/dist/net/index.mjs.map +1 -1
  35. package/dist/observers/index.d.mts +1 -1
  36. package/dist/observers/index.mjs +3 -0
  37. package/dist/observers/index.mjs.map +1 -1
  38. package/dist/state/index.d.mts +57 -15
  39. package/dist/state/index.d.mts.map +1 -1
  40. package/dist/state/index.mjs +198 -90
  41. package/dist/state/index.mjs.map +1 -1
  42. package/dist/time/index.d.mts +21 -1
  43. package/dist/time/index.d.mts.map +1 -1
  44. package/dist/time/index.mjs +48 -19
  45. package/dist/time/index.mjs.map +1 -1
  46. package/package.json +16 -5
  47. package/dist/motion-LfJYAMnP.mjs.map +0 -1
  48. package/dist/move-CZwHyTGk.mjs +0 -215
  49. package/dist/move-CZwHyTGk.mjs.map +0 -1
  50. package/dist/move-DNiMPcuf.d.mts +0 -58
  51. package/dist/move-DNiMPcuf.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["failMessage","failMessage","isDenied","failMessage"],"sources":["../../src/device/fullscreen.ts","../../src/device/geolocation.ts","../../src/device/media-devices.ts","../../src/device/media-stream.ts","../../src/device/permissions.ts"],"sourcesContent":["/**\n * Fullscreen as Commands plus an entry — not a bundle, because the document\n * owns the state: `document.fullscreenElement` is the fact, and the entry\n * reports it as `Changed`. Commands yield `Entered`/`Exited` on success and\n * `Failed` otherwise (rejected request, missing API), so `update` handles\n * both outcomes as Messages.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const FullscreenMessage = defineMessageUnion({\n Changed: { active: Schema.Boolean },\n Entered: {},\n Exited: {},\n Failed: { message: Schema.String },\n})\nexport type FullscreenMessage = typeof FullscreenMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Requests fullscreen for an element. The capability is feature-checked on\n * the element itself (SVG and HTML elements both qualify), falling back to\n * the legacy `webkit` prefix; a platform with neither yields `Failed`\n * instead of throwing.\n */\nexport const enterFullscreen = (element: Element): Command<FullscreenMessage, never, never> => ({\n name: 'Fullscreen.enter',\n args: {},\n effect: Effect.matchEffect(\n Effect.tryPromise({\n try: () => {\n const requestable = element as unknown as {\n readonly requestFullscreen?: unknown\n readonly webkitRequestFullscreen?: unknown\n }\n const request = requestable.requestFullscreen ?? requestable.webkitRequestFullscreen\n if (typeof request !== 'function') {\n throw new Error('fullscreen is unavailable')\n }\n return (request as () => Promise<void>).call(element)\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error => Effect.succeed(FullscreenMessage.Failed({ message: failMessage(error) })),\n onSuccess: () => Effect.succeed(FullscreenMessage.Entered()),\n },\n ),\n})\n\n/** Leaves fullscreen, with the same legacy fallback. Without a document API it yields `Failed`, not a throw. */\nexport const exitFullscreen = (): Command<FullscreenMessage, never, never> => ({\n name: 'Fullscreen.exit',\n args: {},\n effect: Effect.matchEffect(\n Effect.tryPromise({\n try: () => {\n if (typeof document === 'undefined') {\n throw new Error('fullscreen is unavailable')\n }\n const api = document as unknown as {\n readonly exitFullscreen?: unknown\n readonly webkitExitFullscreen?: unknown\n }\n const exit = api.exitFullscreen ?? api.webkitExitFullscreen\n if (typeof exit !== 'function') {\n throw new Error('fullscreen is unavailable')\n }\n return (exit as () => Promise<void>).call(document)\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error => Effect.succeed(FullscreenMessage.Failed({ message: failMessage(error) })),\n onSuccess: () => Effect.succeed(FullscreenMessage.Exited()),\n },\n ),\n})\n\n/**\n * `Changed` whenever fullscreen state flips, starting with the current\n * answer. Without a document the stream is empty instead of throwing.\n */\nexport const fullscreenChanges = (): Stream.Stream<FullscreenMessage> => {\n if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') {\n return Stream.empty\n }\n // Incomplete DOMs leave fullscreenElement undefined instead of null.\n const isActive = (): boolean =>\n document.fullscreenElement !== null && document.fullscreenElement !== undefined\n return Stream.concat(\n Stream.make(FullscreenMessage.Changed({ active: isActive() })),\n Stream.fromEventListener(document, 'fullscreenchange').pipe(\n Stream.map(() => FullscreenMessage.Changed({ active: isActive() })),\n ),\n )\n}\n","/**\n * Geolocation as a bundle: the device position in the Model, watched while\n * placed. Permission denial is its own status (actionable UI); transient\n * failures keep the last fix and note the error. No geolocation API yields\n * an empty stream instead of throwing.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const Coordinates = Schema.Struct({\n latitude: Schema.Number,\n longitude: Schema.Number,\n accuracy: Schema.Number,\n})\nexport type Coordinates = typeof Coordinates.Type\n\nexport const GeolocationModel = Schema.Struct({\n status: Schema.Literals(['unknown', 'ready', 'denied']),\n coords: Schema.NullOr(Coordinates),\n lastError: Schema.NullOr(Schema.String),\n})\nexport type GeolocationModel = typeof GeolocationModel.Type\n\nexport const GeolocationMessage = defineMessageUnion({\n Located: {\n latitude: Schema.Number,\n longitude: Schema.Number,\n accuracy: Schema.Number,\n },\n Denied: {},\n Failed: { message: Schema.String },\n})\nexport type GeolocationMessage = typeof GeolocationMessage.Type\n\ntype PositionCallback = (position: {\n readonly coords: {\n readonly latitude: number\n readonly longitude: number\n readonly accuracy: number\n }\n}) => void\ntype ErrorCallback = (error: { readonly code: number; readonly message: string }) => void\n\nconst watchStream = (): Stream.Stream<GeolocationMessage> => {\n if (\n typeof navigator === 'undefined' ||\n typeof navigator.geolocation?.watchPosition !== 'function'\n ) {\n return Stream.empty\n }\n const geolocation = navigator.geolocation\n return Stream.unwrap(\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<GeolocationMessage>()\n const onLocated: PositionCallback = position => {\n Effect.runFork(\n Queue.offer(\n queue,\n GeolocationMessage.Located({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n accuracy: position.coords.accuracy,\n }),\n ),\n )\n }\n const onError: ErrorCallback = error => {\n Effect.runFork(\n Queue.offer(\n queue,\n error.code === 1\n ? GeolocationMessage.Denied()\n : GeolocationMessage.Failed({ message: error.message }),\n ),\n )\n }\n const watchId = geolocation.watchPosition(onLocated, onError)\n yield* Effect.addFinalizer(() => Effect.sync(() => geolocation.clearWatch(watchId)))\n return Stream.fromQueue(queue)\n }),\n )\n}\n\nexport const Geolocation = Bundle.make<\n 'Geolocation',\n GeolocationModel,\n GeolocationMessage,\n void,\n never,\n never,\n never,\n void,\n {},\n {}\n>('Geolocation', {\n Model: GeolocationModel,\n Message: GeolocationMessage,\n init: () => ({ model: { status: 'unknown', coords: null, lastError: null } }),\n update: (model, message) =>\n GeolocationMessage.match<\n Update.ReturnWithOutMessage<GeolocationModel, GeolocationMessage, never>\n >(message, {\n Located: ({ latitude, longitude, accuracy }) => ({\n model: { ...model, status: 'ready' as const, coords: { latitude, longitude, accuracy } },\n }),\n Denied: () => ({ model: { ...model, status: 'denied' as const } }),\n Failed: ({ message }) => ({ model: { ...model, lastError: message } }),\n }),\n subscriptions: (): Subscription.Subscriptions<GeolocationModel, GeolocationMessage> =>\n Subscription.make<GeolocationModel, GeolocationMessage>()(() => ({\n watch: Subscription.persistent(watchStream()),\n })),\n})\n","/**\n * Media devices as a bundle factory: the enumerated device list in the\n * Model, scanned on placement and re-scanned on every `devicechange`.\n * Denial lands as `denied` while keeping the last list (actionable UI over\n * stale data, like Geolocation's last fix); any other failure keeps the\n * list and notes the error. No media-devices API yields an empty stream\n * and a `Failed` scan instead of throwing.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const MediaDevice = Schema.Struct({\n deviceId: Schema.String,\n groupId: Schema.String,\n kind: Schema.Literals(['audioinput', 'audiooutput', 'videoinput']),\n label: Schema.String,\n})\nexport type MediaDevice = typeof MediaDevice.Type\n\nexport const MediaDevicesModel = Schema.Struct({\n status: Schema.Literals(['unknown', 'ready', 'denied']),\n devices: Schema.Array(MediaDevice),\n lastError: Schema.NullOr(Schema.String),\n})\nexport type MediaDevicesModel = typeof MediaDevicesModel.Type\n\nexport const MediaDevicesMessage = defineMessageUnion({\n Scan: {},\n DevicesChanged: {},\n Refreshed: { devices: Schema.Array(MediaDevice) },\n Denied: {},\n Failed: { message: Schema.String },\n})\nexport type MediaDevicesMessage = typeof MediaDevicesMessage.Type\n\n/** The slice of MediaDevices the bundle needs; satisfied by the platform object. */\nexport interface MediaDevicesHandle {\n enumerateDevices(): Promise<\n ReadonlyArray<{\n readonly deviceId: string\n readonly groupId: string\n readonly kind: string\n readonly label: string\n }>\n >\n addEventListener(type: string, listener: () => void): void\n removeEventListener(type: string, listener: () => void): void\n}\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst isDenied = (error: unknown): boolean =>\n typeof (error as { readonly name?: unknown } | null)?.name === 'string' &&\n ['NotAllowedError', 'SecurityError'].includes((error as { readonly name: string }).name)\n\nexport const mediaDevices = <const Name extends string>(config: {\n readonly name: Name\n /** Read lazily so tests can substitute a double, like the SSE bundle. */\n readonly create?: ((navigator: Navigator) => MediaDevicesHandle | null) | undefined\n}) => {\n const handle = (): MediaDevicesHandle | null => {\n if (typeof navigator === 'undefined') return null\n if (config.create !== undefined) return config.create(navigator)\n const devices = navigator.mediaDevices as unknown as MediaDevicesHandle | undefined\n return devices ?? null\n }\n\n const scanEffect = Effect.matchEffect(\n Effect.tryPromise({\n try: async () => {\n const devices = handle()\n if (devices === null) throw new Error('media devices are unavailable')\n const listed = await devices.enumerateDevices()\n // The platform constrains kind to three values; anything else is a\n // boundary violation, so it fails the scan instead of entering the Model.\n return listed.map(item => {\n if (\n item.kind !== 'audioinput' &&\n item.kind !== 'audiooutput' &&\n item.kind !== 'videoinput'\n ) {\n throw new Error(`unknown device kind: ${item.kind}`)\n }\n // Guarded by the check above: only the three platform kinds reach here.\n return {\n deviceId: item.deviceId,\n groupId: item.groupId,\n kind: item.kind as MediaDevice['kind'],\n label: item.label,\n }\n })\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error =>\n Effect.succeed(\n isDenied(error)\n ? MediaDevicesMessage.Denied()\n : MediaDevicesMessage.Failed({ message: failMessage(error) }),\n ),\n onSuccess: devices =>\n Effect.succeed(MediaDevicesMessage.Refreshed({ devices: [...devices] })),\n },\n )\n\n const scan = () => ({ name: `${config.name}.scan`, effect: scanEffect })\n\n const changedStream = (): Stream.Stream<MediaDevicesMessage> => {\n const devices = handle()\n if (devices === null) return Stream.empty\n // The handle carries only the slice the bundle needs, not the full\n // EventTarget surface, so the listener attaches through a cast.\n return Stream.fromEventListener(devices as unknown as EventTarget, 'devicechange').pipe(\n Stream.map(() => MediaDevicesMessage.DevicesChanged()),\n )\n }\n\n return Bundle.make<Name, MediaDevicesModel, MediaDevicesMessage>(config.name, {\n Model: MediaDevicesModel,\n Message: MediaDevicesMessage,\n init: () => ({\n model: { status: 'unknown', devices: [], lastError: null },\n commands: [scan()],\n }),\n update: (model, message) =>\n MediaDevicesMessage.match<\n Update.ReturnWithOutMessage<MediaDevicesModel, MediaDevicesMessage, never>\n >(message, {\n Scan: () => ({ model, commands: [scan()] }),\n DevicesChanged: () => ({ model, commands: [scan()] }),\n Refreshed: ({ devices }) => ({\n model: { ...model, status: 'ready' as const, devices: [...devices] },\n }),\n Denied: () => ({ model: { ...model, status: 'denied' as const } }),\n Failed: ({ message }) => ({ model: { ...model, lastError: message } }),\n }),\n subscriptions: (): Subscription.Subscriptions<MediaDevicesModel, MediaDevicesMessage> =>\n Subscription.make<MediaDevicesModel, MediaDevicesMessage>()(() => ({\n changes: Subscription.persistent(changedStream()),\n })),\n })\n}\n","/**\n * A camera/mic stream as a bundle factory: one live MediaStream held in a\n * Managed Resource while the Model asks for it. `Started` requests,\n * `Stopped` releases; denial parks at `denied` (actionable UI), any other\n * acquire failure parks at `idle` with the error noted — both clear\n * requirements, so a failing device never spins an acquire loop. Release\n * stops every track. No media-devices API fails the acquire instead of\n * throwing.\n */\nimport { Effect, Option, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as ManagedResource from 'foldkit/managedResource'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const MediaStreamModel = Schema.Struct({\n status: Schema.Literals(['idle', 'requesting', 'live', 'denied']),\n lastError: Schema.NullOr(Schema.String),\n})\nexport type MediaStreamModel = typeof MediaStreamModel.Type\n\nexport const MediaStreamMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Live: {},\n Ended: {},\n Denied: {},\n Failed: { message: Schema.String },\n})\nexport type MediaStreamMessage = typeof MediaStreamMessage.Type\n\n/** A single media track: stopped on release. */\nexport interface MediaTrackHandle {\n stop(): void\n}\n\n/** The live stream: its tracks stop on release. */\nexport interface MediaStreamHandle {\n getTracks(): ReadonlyArray<MediaTrackHandle>\n}\n\nexport interface MediaStreamConstraints {\n readonly audio: boolean\n readonly video: boolean\n}\n\n/** The live stream, for commands that attach it to an element. One assembly holds one. */\nexport const LiveStream = ManagedResource.tag<MediaStreamHandle>()('media-stream')\n\n/** The stream service a placed camera requires. */\nexport type LiveStreamService = ManagedResource.ServiceOf<typeof LiveStream>\n\ntype StreamResources = Readonly<{\n stream: ManagedResource.Entry<\n MediaStreamModel,\n MediaStreamMessage,\n Option.Option<MediaStreamConstraints>,\n MediaStreamHandle,\n LiveStreamService,\n () => MediaStreamMessage\n >\n}>\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst isDenied = (error: unknown): boolean =>\n typeof (error as { readonly name?: unknown } | null)?.name === 'string' &&\n ['NotAllowedError', 'SecurityError'].includes((error as { readonly name: string }).name)\n\nexport const mediaStream = <const Name extends string>(config: {\n readonly name: Name\n /** Read lazily so tests can substitute a double, like the SSE bundle. */\n readonly request?:\n ((constraints: MediaStreamConstraints) => Promise<MediaStreamHandle>) | undefined\n}) => {\n const request = (constraints: MediaStreamConstraints): Promise<MediaStreamHandle> => {\n if (config.request !== undefined) return config.request(constraints)\n if (\n typeof navigator === 'undefined' ||\n typeof navigator.mediaDevices?.getUserMedia !== 'function'\n ) {\n return Promise.reject(new Error('media devices are unavailable'))\n }\n return navigator.mediaDevices.getUserMedia(constraints)\n }\n\n return Bundle.make(config.name, {\n Model: MediaStreamModel,\n Message: MediaStreamMessage,\n args: Schema.Struct({ audio: Schema.Boolean, video: Schema.Boolean }),\n init: () => ({ model: { status: 'idle' as const, lastError: null } }),\n update: (model, message) =>\n MediaStreamMessage.match<\n Update.ReturnWithOutMessage<MediaStreamModel, MediaStreamMessage, never>\n >(message, {\n Started: () => ({ model: { ...model, status: 'requesting' as const } }),\n Stopped: () => ({ model: { ...model, status: 'idle' as const } }),\n Live: () => ({ model: { ...model, status: 'live' as const, lastError: null } }),\n Ended: () => ({ model: { ...model, status: 'idle' as const } }),\n Denied: () => ({ model: { ...model, status: 'denied' as const } }),\n Failed: ({ message }) => ({\n model: { ...model, status: 'idle' as const, lastError: message },\n }),\n }),\n resources: args =>\n ManagedResource.make<MediaStreamModel, MediaStreamMessage>()(entry => ({\n stream: entry(\n Schema.Option(Schema.Struct({ audio: Schema.Boolean, video: Schema.Boolean })),\n {\n resource: LiveStream,\n modelToMaybeRequirements: model =>\n model.status === 'requesting' || model.status === 'live'\n ? Option.some({ audio: args.audio, video: args.video })\n : Option.none(),\n acquire: (constraints: MediaStreamConstraints) =>\n Effect.tryPromise({\n try: () => request(constraints),\n catch: (error: unknown) => error,\n }),\n onAcquired: () => MediaStreamMessage.Live(),\n onReleased: () => MediaStreamMessage.Ended(),\n onAcquireError: error =>\n isDenied(error)\n ? MediaStreamMessage.Denied()\n : MediaStreamMessage.Failed({ message: failMessage(error) }),\n release: stream =>\n Effect.sync(() => {\n for (const track of stream.getTracks()) track.stop()\n }),\n },\n ),\n })),\n })\n}\n","/**\n * Permission states as a bundle factory: queried on placement, kept current\n * by each status object's `onchange`. A resource holds the status objects\n * (they cannot live in the Model); its queue feeds the subscription, and\n * release detaches every handler. An unknown permission name fails the\n * acquire; an unknown state string fails it too — both become `Failed`,\n * never Model facts.\n */\nimport { Effect, Option, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as ManagedResource from 'foldkit/managedResource'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const PermissionState = Schema.Literals(['granted', 'denied', 'prompt'])\nexport type PermissionState = typeof PermissionState.Type\n\nexport const PermissionsModel = Schema.Struct({\n states: Schema.Record(Schema.String, PermissionState),\n lastError: Schema.NullOr(Schema.String),\n})\nexport type PermissionsModel = typeof PermissionsModel.Type\n\nexport const PermissionsMessage = defineMessageUnion({\n Snapshot: { states: Schema.Record(Schema.String, PermissionState) },\n Changed: { name: Schema.String, state: PermissionState },\n Cleared: {},\n Failed: { message: Schema.String },\n})\nexport type PermissionsMessage = typeof PermissionsMessage.Type\n\n/** A queried permission status; `onchange` fires while watched. */\nexport interface PermissionStatusHandle {\n readonly state: string\n onchange: (() => void) | null\n}\n\n/** The slice of the Permissions API the bundle needs. */\nexport interface PermissionsHandle {\n query(descriptor: { readonly name: string }): Promise<PermissionStatusHandle>\n}\n\ninterface Acquired {\n readonly statuses: ReadonlyMap<string, PermissionStatusHandle>\n readonly snapshot: Readonly<Record<string, PermissionState>>\n readonly events: Queue.Queue<PermissionsMessage>\n}\n\n/** The watched statuses, for the changes subscription. One assembly holds one. */\nexport const Watch = ManagedResource.tag<Acquired>()('permissions')\n\n/** The watch service a placed Permissions bundle requires. */\nexport type WatchService = ManagedResource.ServiceOf<typeof Watch>\n\ntype WatchResources = Readonly<{\n watch: ManagedResource.Entry<\n PermissionsModel,\n PermissionsMessage,\n Option.Option<ReadonlyArray<string>>,\n Acquired,\n WatchService,\n (acquired: Acquired) => PermissionsMessage\n >\n}>\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst readState = (state: string, name: string): PermissionState => {\n if (state !== 'granted' && state !== 'denied' && state !== 'prompt') {\n throw new Error(`unknown permission state for ${name}: ${state}`)\n }\n return state\n}\n\nexport const permissions = <const Name extends string>(config: {\n readonly name: Name\n /** Read lazily so tests can substitute a double, like the SSE bundle. */\n readonly create?: ((navigator: Navigator) => PermissionsHandle | null) | undefined\n}) => {\n const handle = (): PermissionsHandle | null => {\n if (typeof navigator === 'undefined') return null\n if (config.create !== undefined) return config.create(navigator)\n const api = navigator.permissions as unknown as PermissionsHandle | undefined\n return api ?? null\n }\n\n return Bundle.make<\n Name,\n PermissionsModel,\n PermissionsMessage,\n { readonly names: ReadonlyArray<string> },\n never,\n never,\n WatchService,\n void,\n WatchResources,\n {}\n >(config.name, {\n Model: PermissionsModel,\n Message: PermissionsMessage,\n args: Schema.Struct({ names: Schema.Array(Schema.String) }),\n init: () => ({ model: { states: {}, lastError: null } }),\n update: (model, message) =>\n PermissionsMessage.match<\n Update.ReturnWithOutMessage<PermissionsModel, PermissionsMessage, never>\n >(message, {\n Snapshot: ({ states }) => ({ model: { ...model, states: { ...states } } }),\n Changed: ({ name, state }) => ({\n model: { ...model, states: { ...model.states, [name]: state } },\n }),\n Cleared: () => ({ model: { ...model, states: {} } }),\n Failed: ({ message }) => ({ model: { ...model, lastError: message } }),\n }),\n resources: args =>\n ManagedResource.make<PermissionsModel, PermissionsMessage>()(entry => ({\n watch: entry(Schema.Option(Schema.Array(Schema.String)), {\n resource: Watch,\n modelToMaybeRequirements: () => Option.some([...new Set(args.names)]),\n acquire: (names: ReadonlyArray<string>) =>\n Effect.gen(function* () {\n const api = handle()\n if (api === null) throw new Error('permissions are unavailable')\n const events = yield* Queue.unbounded<PermissionsMessage>()\n const statuses = new Map<string, PermissionStatusHandle>()\n // Offers run in arrival order; see the WebSocket bundle.\n let tail: Promise<void> = Promise.resolve()\n const offer = (message: PermissionsMessage) => {\n const run = () =>\n Effect.runPromise(Queue.offer(events, message)).then(\n () => undefined,\n () => undefined,\n )\n tail = tail.then(run, run)\n }\n // Two phases: query and validate everything first, so a later\n // name failing cannot abandon handlers attached to earlier ones.\n const queried: Array<readonly [string, PermissionStatusHandle, PermissionState]> = []\n for (const name of names) {\n const status = yield* Effect.tryPromise({\n try: () => api.query({ name }),\n catch: (error: unknown) => error,\n })\n queried.push([name, status, readState(status.state, name)] as const)\n }\n const snapshot: Record<string, PermissionState> = {}\n for (const [name, status, state] of queried) {\n status.onchange = () => {\n try {\n offer(\n PermissionsMessage.Changed({ name, state: readState(status.state, name) }),\n )\n } catch {\n // readState evaluates before offer runs, so a state the\n // schema rejects lands here instead of crashing the handler.\n offer(\n PermissionsMessage.Failed({\n message: `unknown permission state for ${name}`,\n }),\n )\n }\n }\n statuses.set(name, status)\n snapshot[name] = state\n }\n return { statuses, snapshot, events }\n }),\n // The snapshot is captured during acquire, so onAcquired cannot\n // throw re-reading a state that changed mid-acquire.\n onAcquired: ({ snapshot }) => PermissionsMessage.Snapshot({ states: { ...snapshot } }),\n onReleased: () => PermissionsMessage.Cleared(),\n onAcquireError: error => PermissionsMessage.Failed({ message: failMessage(error) }),\n release: ({ statuses, events }) =>\n Effect.asVoid(\n Effect.andThen(\n Effect.sync(() => {\n for (const status of statuses.values()) status.onchange = null\n }),\n Queue.shutdown(events),\n ),\n ),\n }),\n })),\n subscriptions: (): Subscription.Subscriptions<\n PermissionsModel,\n PermissionsMessage,\n WatchService\n > =>\n Subscription.make<PermissionsModel, PermissionsMessage, WatchService>()(() => ({\n // No model gate: the queue outlives every state change, and release\n // ends the stream through teardown, not through dependencies.\n changes: Subscription.persistent(\n Stream.unwrap(\n Effect.matchEffect(Watch.get, {\n // Released between placement and attach: end quietly.\n onFailure: () => Effect.succeed(Stream.empty),\n onSuccess: ({ events }) => Effect.succeed(Stream.fromQueue(events)),\n }),\n ),\n ),\n })),\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["failMessage","failMessage","isDenied","failMessage"],"sources":["../../src/device/fullscreen.ts","../../src/device/geolocation.ts","../../src/device/media-devices.ts","../../src/device/media-stream.ts","../../src/device/permissions.ts"],"sourcesContent":["/**\n * Fullscreen as Commands plus an entry — not a bundle, because the document\n * owns the state: `document.fullscreenElement` is the fact, and the entry\n * reports it as `Changed`. Commands yield `Entered`/`Exited` on success and\n * `Failed` otherwise (rejected request, missing API), so `update` handles\n * both outcomes as Messages.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const FullscreenMessage = defineMessageUnion({\n Changed: { active: Schema.Boolean },\n Entered: {},\n Exited: {},\n Failed: { message: Schema.String },\n})\nexport type FullscreenMessage = typeof FullscreenMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Requests fullscreen for an element. The capability is feature-checked on\n * the element itself (SVG and HTML elements both qualify), falling back to\n * the legacy `webkit` prefix; a platform with neither yields `Failed`\n * instead of throwing.\n */\nexport const enterFullscreen = (element: Element): Command<FullscreenMessage, never, never> => ({\n name: 'Fullscreen.enter',\n args: {},\n effect: Effect.matchEffect(\n Effect.tryPromise({\n try: () => {\n const requestable = element as unknown as {\n readonly requestFullscreen?: unknown\n readonly webkitRequestFullscreen?: unknown\n }\n const request = requestable.requestFullscreen ?? requestable.webkitRequestFullscreen\n if (typeof request !== 'function') {\n throw new Error('fullscreen is unavailable')\n }\n return (request as () => Promise<void>).call(element)\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error => Effect.succeed(FullscreenMessage.Failed({ message: failMessage(error) })),\n onSuccess: () => Effect.succeed(FullscreenMessage.Entered()),\n },\n ),\n})\n\n/** Leaves fullscreen, with the same legacy fallback. Without a document API it yields `Failed`, not a throw. */\nexport const exitFullscreen = (): Command<FullscreenMessage, never, never> => ({\n name: 'Fullscreen.exit',\n args: {},\n effect: Effect.matchEffect(\n Effect.tryPromise({\n try: () => {\n if (typeof document === 'undefined') {\n throw new Error('fullscreen is unavailable')\n }\n const api = document as unknown as {\n readonly exitFullscreen?: unknown\n readonly webkitExitFullscreen?: unknown\n }\n const exit = api.exitFullscreen ?? api.webkitExitFullscreen\n if (typeof exit !== 'function') {\n throw new Error('fullscreen is unavailable')\n }\n return (exit as () => Promise<void>).call(document)\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error => Effect.succeed(FullscreenMessage.Failed({ message: failMessage(error) })),\n onSuccess: () => Effect.succeed(FullscreenMessage.Exited()),\n },\n ),\n})\n\n/**\n * `Changed` whenever fullscreen state flips, starting with the current\n * answer. Without a document the stream is empty instead of throwing.\n */\nexport const fullscreenChanges = (): Stream.Stream<FullscreenMessage> => {\n if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') {\n return Stream.empty\n }\n // Incomplete DOMs leave fullscreenElement undefined instead of null.\n const isActive = (): boolean =>\n document.fullscreenElement !== null && document.fullscreenElement !== undefined\n return Stream.concat(\n Stream.make(FullscreenMessage.Changed({ active: isActive() })),\n Stream.fromEventListener(document, 'fullscreenchange').pipe(\n Stream.map(() => FullscreenMessage.Changed({ active: isActive() })),\n ),\n )\n}\n","/**\n * Geolocation as a bundle: the device position in the Model, watched while\n * placed. Permission denial is its own status (actionable UI); transient\n * failures keep the last fix and note the error. No geolocation API yields\n * an empty stream instead of throwing.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\nimport { unlessSame } from '../internal.js'\n\nexport const Coordinates = Schema.Struct({\n latitude: Schema.Number,\n longitude: Schema.Number,\n accuracy: Schema.Number,\n})\nexport type Coordinates = typeof Coordinates.Type\n\nexport const GeolocationModel = Schema.Struct({\n status: Schema.Literals(['unknown', 'ready', 'denied']),\n coords: Schema.NullOr(Coordinates),\n lastError: Schema.NullOr(Schema.String),\n})\nexport type GeolocationModel = typeof GeolocationModel.Type\n\nexport const GeolocationMessage = defineMessageUnion({\n Located: {\n latitude: Schema.Number,\n longitude: Schema.Number,\n accuracy: Schema.Number,\n },\n Denied: {},\n Failed: { message: Schema.String },\n})\nexport type GeolocationMessage = typeof GeolocationMessage.Type\n\ntype PositionCallback = (position: {\n readonly coords: {\n readonly latitude: number\n readonly longitude: number\n readonly accuracy: number\n }\n}) => void\ntype ErrorCallback = (error: { readonly code: number; readonly message: string }) => void\n\nconst watchStream = (): Stream.Stream<GeolocationMessage> => {\n if (\n typeof navigator === 'undefined' ||\n typeof navigator.geolocation?.watchPosition !== 'function'\n ) {\n return Stream.empty\n }\n const geolocation = navigator.geolocation\n return Stream.unwrap(\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<GeolocationMessage>()\n const onLocated: PositionCallback = position => {\n Effect.runFork(\n Queue.offer(\n queue,\n GeolocationMessage.Located({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n accuracy: position.coords.accuracy,\n }),\n ),\n )\n }\n const onError: ErrorCallback = error => {\n Effect.runFork(\n Queue.offer(\n queue,\n error.code === 1\n ? GeolocationMessage.Denied()\n : GeolocationMessage.Failed({ message: error.message }),\n ),\n )\n }\n const watchId = geolocation.watchPosition(onLocated, onError)\n yield* Effect.addFinalizer(() => Effect.sync(() => geolocation.clearWatch(watchId)))\n return Stream.fromQueue(queue)\n }),\n )\n}\n\nexport const Geolocation = Bundle.make<\n 'Geolocation',\n GeolocationModel,\n GeolocationMessage,\n void,\n never,\n never,\n never,\n void,\n {},\n {}\n>('Geolocation', {\n Model: GeolocationModel,\n Message: GeolocationMessage,\n init: () => ({ model: { status: 'unknown', coords: null, lastError: null } }),\n update: (model, message) =>\n GeolocationMessage.match<\n Update.ReturnWithOutMessage<GeolocationModel, GeolocationMessage, never>\n >(message, {\n // A watch repeats a fix that has not moved.\n Located: ({ latitude, longitude, accuracy }) =>\n model.status === 'ready' &&\n model.coords !== null &&\n model.coords.latitude === latitude &&\n model.coords.longitude === longitude &&\n model.coords.accuracy === accuracy\n ? { model }\n : {\n model: {\n ...model,\n status: 'ready' as const,\n coords: { latitude, longitude, accuracy },\n },\n },\n Denied: () => ({ model: unlessSame(model, { ...model, status: 'denied' }) }),\n Failed: ({ message }) => ({ model: unlessSame(model, { ...model, lastError: message }) }),\n }),\n subscriptions: (): Subscription.Subscriptions<GeolocationModel, GeolocationMessage> =>\n Subscription.make<GeolocationModel, GeolocationMessage>()(() => ({\n watch: Subscription.persistent(watchStream()),\n })),\n})\n","/**\n * Media devices as a bundle factory: the enumerated device list in the\n * Model, scanned on placement and re-scanned on every `devicechange`.\n * Denial lands as `denied` while keeping the last list (actionable UI over\n * stale data, like Geolocation's last fix); any other failure keeps the\n * list and notes the error. No media-devices API yields an empty stream\n * and a `Failed` scan instead of throwing.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\nimport { unlessSame } from '../internal.js'\n\nexport const MediaDevice = Schema.Struct({\n deviceId: Schema.String,\n groupId: Schema.String,\n kind: Schema.Literals(['audioinput', 'audiooutput', 'videoinput']),\n label: Schema.String,\n})\nexport type MediaDevice = typeof MediaDevice.Type\n\nexport const MediaDevicesModel = Schema.Struct({\n status: Schema.Literals(['unknown', 'ready', 'denied']),\n devices: Schema.Array(MediaDevice),\n lastError: Schema.NullOr(Schema.String),\n})\nexport type MediaDevicesModel = typeof MediaDevicesModel.Type\n\nexport const MediaDevicesMessage = defineMessageUnion({\n Scan: {},\n DevicesChanged: {},\n Refreshed: { devices: Schema.Array(MediaDevice) },\n Denied: {},\n Failed: { message: Schema.String },\n})\nexport type MediaDevicesMessage = typeof MediaDevicesMessage.Type\n\nconst sameDevices = (a: ReadonlyArray<MediaDevice>, b: ReadonlyArray<MediaDevice>): boolean =>\n a.length === b.length &&\n a.every(\n (device, index) =>\n device.deviceId === b[index]!.deviceId &&\n device.groupId === b[index]!.groupId &&\n device.kind === b[index]!.kind &&\n device.label === b[index]!.label,\n )\n\n/** The slice of MediaDevices the bundle needs; satisfied by the platform object. */\nexport interface MediaDevicesHandle {\n enumerateDevices(): Promise<\n ReadonlyArray<{\n readonly deviceId: string\n readonly groupId: string\n readonly kind: string\n readonly label: string\n }>\n >\n addEventListener(type: string, listener: () => void): void\n removeEventListener(type: string, listener: () => void): void\n}\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst isDenied = (error: unknown): boolean =>\n typeof (error as { readonly name?: unknown } | null)?.name === 'string' &&\n ['NotAllowedError', 'SecurityError'].includes((error as { readonly name: string }).name)\n\nexport const mediaDevices = <const Name extends string>(config: {\n readonly name: Name\n /** Read lazily so tests can substitute a double, like the SSE bundle. */\n readonly create?: ((navigator: Navigator) => MediaDevicesHandle | null) | undefined\n}) => {\n const handle = (): MediaDevicesHandle | null => {\n if (typeof navigator === 'undefined') return null\n if (config.create !== undefined) return config.create(navigator)\n const devices = navigator.mediaDevices as unknown as MediaDevicesHandle | undefined\n return devices ?? null\n }\n\n const scanEffect = Effect.matchEffect(\n Effect.tryPromise({\n try: async () => {\n const devices = handle()\n if (devices === null) throw new Error('media devices are unavailable')\n const listed = await devices.enumerateDevices()\n // The platform constrains kind to three values; anything else is a\n // boundary violation, so it fails the scan instead of entering the Model.\n return listed.map(item => {\n if (\n item.kind !== 'audioinput' &&\n item.kind !== 'audiooutput' &&\n item.kind !== 'videoinput'\n ) {\n throw new Error(`unknown device kind: ${item.kind}`)\n }\n // Guarded by the check above: only the three platform kinds reach here.\n return {\n deviceId: item.deviceId,\n groupId: item.groupId,\n kind: item.kind as MediaDevice['kind'],\n label: item.label,\n }\n })\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error =>\n Effect.succeed(\n isDenied(error)\n ? MediaDevicesMessage.Denied()\n : MediaDevicesMessage.Failed({ message: failMessage(error) }),\n ),\n onSuccess: devices =>\n Effect.succeed(MediaDevicesMessage.Refreshed({ devices: [...devices] })),\n },\n )\n\n const scan = () => ({ name: `${config.name}.scan`, effect: scanEffect })\n\n const changedStream = (): Stream.Stream<MediaDevicesMessage> => {\n const devices = handle()\n if (devices === null) return Stream.empty\n // The handle carries only the slice the bundle needs, not the full\n // EventTarget surface, so the listener attaches through a cast.\n return Stream.fromEventListener(devices as unknown as EventTarget, 'devicechange').pipe(\n Stream.map(() => MediaDevicesMessage.DevicesChanged()),\n )\n }\n\n return Bundle.make<Name, MediaDevicesModel, MediaDevicesMessage>(config.name, {\n Model: MediaDevicesModel,\n Message: MediaDevicesMessage,\n init: () => ({\n model: { status: 'unknown', devices: [], lastError: null },\n commands: [scan()],\n }),\n update: (model, message) =>\n MediaDevicesMessage.match<\n Update.ReturnWithOutMessage<MediaDevicesModel, MediaDevicesMessage, never>\n >(message, {\n Scan: () => ({ model, commands: [scan()] }),\n DevicesChanged: () => ({ model, commands: [scan()] }),\n // One plug-in fires several `devicechange`s, and each rescan finds the same list.\n Refreshed: ({ devices }) =>\n model.status === 'ready' && sameDevices(model.devices, devices)\n ? { model }\n : { model: { ...model, status: 'ready' as const, devices } },\n Denied: () => ({ model: unlessSame(model, { ...model, status: 'denied' }) }),\n Failed: ({ message }) => ({ model: unlessSame(model, { ...model, lastError: message }) }),\n }),\n subscriptions: (): Subscription.Subscriptions<MediaDevicesModel, MediaDevicesMessage> =>\n Subscription.make<MediaDevicesModel, MediaDevicesMessage>()(() => ({\n changes: Subscription.persistent(changedStream()),\n })),\n })\n}\n","/**\n * A camera/mic stream as a bundle factory: one live MediaStream held in a\n * Managed Resource while the Model asks for it. `Started` requests,\n * `Stopped` releases; denial parks at `denied` (actionable UI), any other\n * acquire failure parks at `idle` with the error noted — both clear\n * requirements, so a failing device never spins an acquire loop. Release\n * stops every track. No media-devices API fails the acquire instead of\n * throwing.\n */\nimport { Effect, Option, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as ManagedResource from 'foldkit/managedResource'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\nimport { unlessSame } from '../internal.js'\n\nexport const MediaStreamModel = Schema.Struct({\n status: Schema.Literals(['idle', 'requesting', 'live', 'denied']),\n lastError: Schema.NullOr(Schema.String),\n})\nexport type MediaStreamModel = typeof MediaStreamModel.Type\n\nexport const MediaStreamMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Live: {},\n Ended: {},\n Denied: {},\n Failed: { message: Schema.String },\n})\nexport type MediaStreamMessage = typeof MediaStreamMessage.Type\n\n/** A single media track: stopped on release. */\nexport interface MediaTrackHandle {\n stop(): void\n}\n\n/** The live stream: its tracks stop on release. */\nexport interface MediaStreamHandle {\n getTracks(): ReadonlyArray<MediaTrackHandle>\n}\n\nexport interface MediaStreamConstraints {\n readonly audio: boolean\n readonly video: boolean\n}\n\n/** The live stream, for commands that attach it to an element. One assembly holds one. */\nexport const LiveStream = ManagedResource.tag<MediaStreamHandle>()('media-stream')\n\n/** The stream service a placed camera requires. */\nexport type LiveStreamService = ManagedResource.ServiceOf<typeof LiveStream>\n\ntype StreamResources = Readonly<{\n stream: ManagedResource.Entry<\n MediaStreamModel,\n MediaStreamMessage,\n Option.Option<MediaStreamConstraints>,\n MediaStreamHandle,\n LiveStreamService,\n () => MediaStreamMessage\n >\n}>\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst isDenied = (error: unknown): boolean =>\n typeof (error as { readonly name?: unknown } | null)?.name === 'string' &&\n ['NotAllowedError', 'SecurityError'].includes((error as { readonly name: string }).name)\n\nexport const mediaStream = <const Name extends string>(config: {\n readonly name: Name\n /** Read lazily so tests can substitute a double, like the SSE bundle. */\n readonly request?:\n ((constraints: MediaStreamConstraints) => Promise<MediaStreamHandle>) | undefined\n}) => {\n const request = (constraints: MediaStreamConstraints): Promise<MediaStreamHandle> => {\n if (config.request !== undefined) return config.request(constraints)\n if (\n typeof navigator === 'undefined' ||\n typeof navigator.mediaDevices?.getUserMedia !== 'function'\n ) {\n return Promise.reject(new Error('media devices are unavailable'))\n }\n return navigator.mediaDevices.getUserMedia(constraints)\n }\n\n return Bundle.make(config.name, {\n Model: MediaStreamModel,\n Message: MediaStreamMessage,\n args: Schema.Struct({ audio: Schema.Boolean, video: Schema.Boolean }),\n init: () => ({ model: { status: 'idle' as const, lastError: null } }),\n update: (model, message) =>\n MediaStreamMessage.match<\n Update.ReturnWithOutMessage<MediaStreamModel, MediaStreamMessage, never>\n >(message, {\n // Starting a live stream would wait at `requesting` for an acquire\n // that never comes: the requirements are the same.\n Started: () =>\n model.status === 'requesting' || model.status === 'live'\n ? { model }\n : { model: { ...model, status: 'requesting' as const } },\n Stopped: () => ({ model: unlessSame(model, { ...model, status: 'idle' }) }),\n Live: () => ({ model: unlessSame(model, { ...model, status: 'live', lastError: null }) }),\n // Stopping releases the stream, whose release reports it ended.\n Ended: () => ({ model: unlessSame(model, { ...model, status: 'idle' }) }),\n Denied: () => ({ model: unlessSame(model, { ...model, status: 'denied' }) }),\n Failed: ({ message }) => ({\n model: unlessSame(model, { ...model, status: 'idle', lastError: message }),\n }),\n }),\n resources: args =>\n ManagedResource.make<MediaStreamModel, MediaStreamMessage>()(entry => ({\n stream: entry(\n Schema.Option(Schema.Struct({ audio: Schema.Boolean, video: Schema.Boolean })),\n {\n resource: LiveStream,\n modelToMaybeRequirements: model =>\n model.status === 'requesting' || model.status === 'live'\n ? Option.some({ audio: args.audio, video: args.video })\n : Option.none(),\n acquire: (constraints: MediaStreamConstraints) =>\n Effect.tryPromise({\n try: () => request(constraints),\n catch: (error: unknown) => error,\n }),\n onAcquired: () => MediaStreamMessage.Live(),\n onReleased: () => MediaStreamMessage.Ended(),\n onAcquireError: error =>\n isDenied(error)\n ? MediaStreamMessage.Denied()\n : MediaStreamMessage.Failed({ message: failMessage(error) }),\n release: stream =>\n Effect.sync(() => {\n for (const track of stream.getTracks()) track.stop()\n }),\n },\n ),\n })),\n })\n}\n","/**\n * Permission states as a bundle factory: queried on placement, kept current\n * by each status object's `onchange`. A resource holds the status objects\n * (they cannot live in the Model); its queue feeds the subscription, and\n * release detaches every handler. An unknown permission name fails the\n * acquire; an unknown state string fails it too — both become `Failed`,\n * never Model facts.\n */\nimport { Effect, Option, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as ManagedResource from 'foldkit/managedResource'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\nimport { unlessSame } from '../internal.js'\n\nexport const PermissionState = Schema.Literals(['granted', 'denied', 'prompt'])\nexport type PermissionState = typeof PermissionState.Type\n\nexport const PermissionsModel = Schema.Struct({\n states: Schema.Record(Schema.String, PermissionState),\n lastError: Schema.NullOr(Schema.String),\n})\nexport type PermissionsModel = typeof PermissionsModel.Type\n\nexport const PermissionsMessage = defineMessageUnion({\n Snapshot: { states: Schema.Record(Schema.String, PermissionState) },\n Changed: { name: Schema.String, state: PermissionState },\n Cleared: {},\n Failed: { message: Schema.String },\n})\nexport type PermissionsMessage = typeof PermissionsMessage.Type\n\n/** A queried permission status; `onchange` fires while watched. */\nexport interface PermissionStatusHandle {\n readonly state: string\n onchange: (() => void) | null\n}\n\n/** The slice of the Permissions API the bundle needs. */\nexport interface PermissionsHandle {\n query(descriptor: { readonly name: string }): Promise<PermissionStatusHandle>\n}\n\ninterface Acquired {\n readonly statuses: ReadonlyMap<string, PermissionStatusHandle>\n readonly snapshot: Readonly<Record<string, PermissionState>>\n readonly events: Queue.Queue<PermissionsMessage>\n}\n\n/** The watched statuses, for the changes subscription. One assembly holds one. */\nexport const Watch = ManagedResource.tag<Acquired>()('permissions')\n\n/** The watch service a placed Permissions bundle requires. */\nexport type WatchService = ManagedResource.ServiceOf<typeof Watch>\n\ntype WatchResources = Readonly<{\n watch: ManagedResource.Entry<\n PermissionsModel,\n PermissionsMessage,\n Option.Option<ReadonlyArray<string>>,\n Acquired,\n WatchService,\n (acquired: Acquired) => PermissionsMessage\n >\n}>\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst readState = (state: string, name: string): PermissionState => {\n if (state !== 'granted' && state !== 'denied' && state !== 'prompt') {\n throw new Error(`unknown permission state for ${name}: ${state}`)\n }\n return state\n}\n\nexport const permissions = <const Name extends string>(config: {\n readonly name: Name\n /** Read lazily so tests can substitute a double, like the SSE bundle. */\n readonly create?: ((navigator: Navigator) => PermissionsHandle | null) | undefined\n}) => {\n const handle = (): PermissionsHandle | null => {\n if (typeof navigator === 'undefined') return null\n if (config.create !== undefined) return config.create(navigator)\n const api = navigator.permissions as unknown as PermissionsHandle | undefined\n return api ?? null\n }\n\n return Bundle.make<\n Name,\n PermissionsModel,\n PermissionsMessage,\n { readonly names: ReadonlyArray<string> },\n never,\n never,\n WatchService,\n void,\n WatchResources,\n {}\n >(config.name, {\n Model: PermissionsModel,\n Message: PermissionsMessage,\n args: Schema.Struct({ names: Schema.Array(Schema.String) }),\n init: () => ({ model: { states: {}, lastError: null } }),\n update: (model, message) =>\n PermissionsMessage.match<\n Update.ReturnWithOutMessage<PermissionsModel, PermissionsMessage, never>\n >(message, {\n Snapshot: ({ states }) => {\n const next = unlessSame(model.states, { ...states })\n return next === model.states ? { model } : { model: { ...model, states: next } }\n },\n Changed: ({ name, state }) =>\n Object.hasOwn(model.states, name) && model.states[name] === state\n ? { model }\n : { model: { ...model, states: { ...model.states, [name]: state } } },\n Cleared: () =>\n Object.keys(model.states).length === 0 ? { model } : { model: { ...model, states: {} } },\n Failed: ({ message }) => ({ model: unlessSame(model, { ...model, lastError: message }) }),\n }),\n resources: args => {\n const names = Option.some([...new Set(args.names)])\n return ManagedResource.make<PermissionsModel, PermissionsMessage>()(entry => ({\n watch: entry(Schema.Option(Schema.Array(Schema.String)), {\n resource: Watch,\n modelToMaybeRequirements: () => names,\n acquire: (names: ReadonlyArray<string>) =>\n Effect.gen(function* () {\n const api = handle()\n if (api === null) throw new Error('permissions are unavailable')\n const events = yield* Queue.unbounded<PermissionsMessage>()\n const statuses = new Map<string, PermissionStatusHandle>()\n // Offers run in arrival order; see the WebSocket bundle.\n let tail: Promise<void> = Promise.resolve()\n const offer = (message: PermissionsMessage) => {\n const run = () =>\n Effect.runPromise(Queue.offer(events, message)).then(\n () => undefined,\n () => undefined,\n )\n tail = tail.then(run, run)\n }\n // Two phases: query and validate everything first, so a later\n // name failing cannot abandon handlers attached to earlier ones.\n const queried: Array<readonly [string, PermissionStatusHandle, PermissionState]> = []\n for (const name of names) {\n const status = yield* Effect.tryPromise({\n try: () => api.query({ name }),\n catch: (error: unknown) => error,\n })\n queried.push([name, status, readState(status.state, name)] as const)\n }\n const snapshot: Record<string, PermissionState> = {}\n for (const [name, status, state] of queried) {\n status.onchange = () => {\n try {\n offer(\n PermissionsMessage.Changed({ name, state: readState(status.state, name) }),\n )\n } catch {\n // readState evaluates before offer runs, so a state the\n // schema rejects lands here instead of crashing the handler.\n offer(\n PermissionsMessage.Failed({\n message: `unknown permission state for ${name}`,\n }),\n )\n }\n }\n statuses.set(name, status)\n snapshot[name] = state\n }\n return { statuses, snapshot, events }\n }),\n // The snapshot is captured during acquire, so onAcquired cannot\n // throw re-reading a state that changed mid-acquire.\n onAcquired: ({ snapshot }) => PermissionsMessage.Snapshot({ states: { ...snapshot } }),\n onReleased: () => PermissionsMessage.Cleared(),\n onAcquireError: error => PermissionsMessage.Failed({ message: failMessage(error) }),\n release: ({ statuses, events }) =>\n Effect.asVoid(\n Effect.andThen(\n Effect.sync(() => {\n for (const status of statuses.values()) status.onchange = null\n }),\n Queue.shutdown(events),\n ),\n ),\n }),\n }))\n },\n subscriptions: (): Subscription.Subscriptions<\n PermissionsModel,\n PermissionsMessage,\n WatchService\n > =>\n Subscription.make<PermissionsModel, PermissionsMessage, WatchService>()(() => ({\n // No model gate: the queue outlives every state change, and release\n // ends the stream through teardown, not through dependencies.\n changes: Subscription.persistent(\n Stream.unwrap(\n Effect.matchEffect(Watch.get, {\n // Released between placement and attach: end quietly.\n onFailure: () => Effect.succeed(Stream.empty),\n onSuccess: ({ events }) => Effect.succeed(Stream.fromQueue(events)),\n }),\n ),\n ),\n })),\n })\n}\n"],"mappings":";;;;;;;;;;;;;;AAWA,MAAa,oBAAoB,mBAAmB;CAClD,SAAS,EAAE,QAAQ,OAAO,QAAQ;CAClC,SAAS,CAAC;CACV,QAAQ,CAAC;CACT,QAAQ,EAAE,SAAS,OAAO,OAAO;AACnC,CAAC;AAGD,MAAMA,iBAAe,UACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;;;;;;;AAQvD,MAAa,mBAAmB,aAAgE;CAC9F,MAAM;CACN,MAAM,CAAC;CACP,QAAQ,OAAO,YACb,OAAO,WAAW;EAChB,WAAW;GACT,MAAM,cAAc;GAIpB,MAAM,UAAU,YAAY,qBAAqB,YAAY;GAC7D,IAAI,OAAO,YAAY,YACrB,MAAM,IAAI,MAAM,2BAA2B;GAE7C,OAAQ,QAAgC,KAAK,OAAO;EACtD;EACA,QAAQ,UAAmB;CAC7B,CAAC,GACD;EACE,YAAW,UAAS,OAAO,QAAQ,kBAAkB,OAAO,EAAE,SAASA,cAAY,KAAK,EAAE,CAAC,CAAC;EAC5F,iBAAiB,OAAO,QAAQ,kBAAkB,QAAQ,CAAC;CAC7D,CACF;AACF;;AAGA,MAAa,wBAAkE;CAC7E,MAAM;CACN,MAAM,CAAC;CACP,QAAQ,OAAO,YACb,OAAO,WAAW;EAChB,WAAW;GACT,IAAI,OAAO,aAAa,aACtB,MAAM,IAAI,MAAM,2BAA2B;GAE7C,MAAM,MAAM;GAIZ,MAAM,OAAO,IAAI,kBAAkB,IAAI;GACvC,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,MAAM,2BAA2B;GAE7C,OAAQ,KAA6B,KAAK,QAAQ;EACpD;EACA,QAAQ,UAAmB;CAC7B,CAAC,GACD;EACE,YAAW,UAAS,OAAO,QAAQ,kBAAkB,OAAO,EAAE,SAASA,cAAY,KAAK,EAAE,CAAC,CAAC;EAC5F,iBAAiB,OAAO,QAAQ,kBAAkB,OAAO,CAAC;CAC5D,CACF;AACF;;;;;AAMA,MAAa,0BAA4D;CACvE,IAAI,OAAO,aAAa,eAAe,OAAO,SAAS,qBAAqB,YAC1E,OAAO,OAAO;CAGhB,MAAM,iBACJ,SAAS,sBAAsB,QAAQ,SAAS,sBAAsB,KAAA;CACxE,OAAO,OAAO,OACZ,OAAO,KAAK,kBAAkB,QAAQ,EAAE,QAAQ,SAAS,EAAE,CAAC,CAAC,GAC7D,OAAO,kBAAkB,UAAU,kBAAkB,CAAC,CAAC,KACrD,OAAO,UAAU,kBAAkB,QAAQ,EAAE,QAAQ,SAAS,EAAE,CAAC,CAAC,CACpE,CACF;AACF;;;;;;;;;ACtFA,MAAa,cAAc,OAAO,OAAO;CACvC,UAAU,OAAO;CACjB,WAAW,OAAO;CAClB,UAAU,OAAO;AACnB,CAAC;AAGD,MAAa,mBAAmB,OAAO,OAAO;CAC5C,QAAQ,OAAO,SAAS;EAAC;EAAW;EAAS;CAAQ,CAAC;CACtD,QAAQ,OAAO,OAAO,WAAW;CACjC,WAAW,OAAO,OAAO,OAAO,MAAM;AACxC,CAAC;AAGD,MAAa,qBAAqB,mBAAmB;CACnD,SAAS;EACP,UAAU,OAAO;EACjB,WAAW,OAAO;EAClB,UAAU,OAAO;CACnB;CACA,QAAQ,CAAC;CACT,QAAQ,EAAE,SAAS,OAAO,OAAO;AACnC,CAAC;AAYD,MAAM,oBAAuD;CAC3D,IACE,OAAO,cAAc,eACrB,OAAO,UAAU,aAAa,kBAAkB,YAEhD,OAAO,OAAO;CAEhB,MAAM,cAAc,UAAU;CAC9B,OAAO,OAAO,OACZ,OAAO,IAAI,aAAa;EACtB,MAAM,QAAQ,OAAO,MAAM,UAA8B;EACzD,MAAM,aAA8B,aAAY;GAC9C,OAAO,QACL,MAAM,MACJ,OACA,mBAAmB,QAAQ;IACzB,UAAU,SAAS,OAAO;IAC1B,WAAW,SAAS,OAAO;IAC3B,UAAU,SAAS,OAAO;GAC5B,CAAC,CACH,CACF;EACF;EACA,MAAM,WAAyB,UAAS;GACtC,OAAO,QACL,MAAM,MACJ,OACA,MAAM,SAAS,IACX,mBAAmB,OAAO,IAC1B,mBAAmB,OAAO,EAAE,SAAS,MAAM,QAAQ,CAAC,CAC1D,CACF;EACF;EACA,MAAM,UAAU,YAAY,cAAc,WAAW,OAAO;EAC5D,OAAO,OAAO,mBAAmB,OAAO,WAAW,YAAY,WAAW,OAAO,CAAC,CAAC;EACnF,OAAO,OAAO,UAAU,KAAK;CAC/B,CAAC,CACH;AACF;AAEA,MAAa,cAAc,OAAO,KAWhC,eAAe;CACf,OAAO;CACP,SAAS;CACT,aAAa,EAAE,OAAO;EAAE,QAAQ;EAAW,QAAQ;EAAM,WAAW;CAAK,EAAE;CAC3E,SAAS,OAAO,YACd,mBAAmB,MAEjB,SAAS;EAET,UAAU,EAAE,UAAU,WAAW,eAC/B,MAAM,WAAW,WACjB,MAAM,WAAW,QACjB,MAAM,OAAO,aAAa,YAC1B,MAAM,OAAO,cAAc,aAC3B,MAAM,OAAO,aAAa,WACtB,EAAE,MAAM,IACR,EACE,OAAO;GACL,GAAG;GACH,QAAQ;GACR,QAAQ;IAAE;IAAU;IAAW;GAAS;EAC1C,EACF;EACN,eAAe,EAAE,OAAO,WAAW,OAAO;GAAE,GAAG;GAAO,QAAQ;EAAS,CAAC,EAAE;EAC1E,SAAS,EAAE,eAAe,EAAE,OAAO,WAAW,OAAO;GAAE,GAAG;GAAO,WAAW;EAAQ,CAAC,EAAE;CACzF,CAAC;CACH,qBACE,aAAa,KAA2C,CAAC,QAAQ,EAC/D,OAAO,aAAa,WAAW,YAAY,CAAC,EAC9C,EAAE;AACN,CAAC;;;;;;;;;;;ACjHD,MAAa,cAAc,OAAO,OAAO;CACvC,UAAU,OAAO;CACjB,SAAS,OAAO;CAChB,MAAM,OAAO,SAAS;EAAC;EAAc;EAAe;CAAY,CAAC;CACjE,OAAO,OAAO;AAChB,CAAC;AAGD,MAAa,oBAAoB,OAAO,OAAO;CAC7C,QAAQ,OAAO,SAAS;EAAC;EAAW;EAAS;CAAQ,CAAC;CACtD,SAAS,OAAO,MAAM,WAAW;CACjC,WAAW,OAAO,OAAO,OAAO,MAAM;AACxC,CAAC;AAGD,MAAa,sBAAsB,mBAAmB;CACpD,MAAM,CAAC;CACP,gBAAgB,CAAC;CACjB,WAAW,EAAE,SAAS,OAAO,MAAM,WAAW,EAAE;CAChD,QAAQ,CAAC;CACT,QAAQ,EAAE,SAAS,OAAO,OAAO;AACnC,CAAC;AAGD,MAAM,eAAe,GAA+B,MAClD,EAAE,WAAW,EAAE,UACf,EAAE,OACC,QAAQ,UACP,OAAO,aAAa,EAAE,MAAM,CAAE,YAC9B,OAAO,YAAY,EAAE,MAAM,CAAE,WAC7B,OAAO,SAAS,EAAE,MAAM,CAAE,QAC1B,OAAO,UAAU,EAAE,MAAM,CAAE,KAC/B;AAgBF,MAAMC,iBAAe,UACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAEvD,MAAMC,cAAY,UAChB,OAAQ,OAA8C,SAAS,YAC/D,CAAC,mBAAmB,eAAe,CAAC,CAAC,SAAU,MAAoC,IAAI;AAEzF,MAAa,gBAA2C,WAIlD;CACJ,MAAM,eAA0C;EAC9C,IAAI,OAAO,cAAc,aAAa,OAAO;EAC7C,IAAI,OAAO,WAAW,KAAA,GAAW,OAAO,OAAO,OAAO,SAAS;EAE/D,OADgB,UAAU,gBACR;CACpB;CAEA,MAAM,aAAa,OAAO,YACxB,OAAO,WAAW;EAChB,KAAK,YAAY;GACf,MAAM,UAAU,OAAO;GACvB,IAAI,YAAY,MAAM,MAAM,IAAI,MAAM,+BAA+B;GAIrE,QAAO,MAHc,QAAQ,iBAAiB,EAAA,CAGhC,KAAI,SAAQ;IACxB,IACE,KAAK,SAAS,gBACd,KAAK,SAAS,iBACd,KAAK,SAAS,cAEd,MAAM,IAAI,MAAM,wBAAwB,KAAK,MAAM;IAGrD,OAAO;KACL,UAAU,KAAK;KACf,SAAS,KAAK;KACd,MAAM,KAAK;KACX,OAAO,KAAK;IACd;GACF,CAAC;EACH;EACA,QAAQ,UAAmB;CAC7B,CAAC,GACD;EACE,YAAW,UACT,OAAO,QACLA,WAAS,KAAK,IACV,oBAAoB,OAAO,IAC3B,oBAAoB,OAAO,EAAE,SAASD,cAAY,KAAK,EAAE,CAAC,CAChE;EACF,YAAW,YACT,OAAO,QAAQ,oBAAoB,UAAU,EAAE,SAAS,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;CAC3E,CACF;CAEA,MAAM,cAAc;EAAE,MAAM,GAAG,OAAO,KAAK;EAAQ,QAAQ;CAAW;CAEtE,MAAM,sBAA0D;EAC9D,MAAM,UAAU,OAAO;EACvB,IAAI,YAAY,MAAM,OAAO,OAAO;EAGpC,OAAO,OAAO,kBAAkB,SAAmC,cAAc,CAAC,CAAC,KACjF,OAAO,UAAU,oBAAoB,eAAe,CAAC,CACvD;CACF;CAEA,OAAO,OAAO,KAAmD,OAAO,MAAM;EAC5E,OAAO;EACP,SAAS;EACT,aAAa;GACX,OAAO;IAAE,QAAQ;IAAW,SAAS,CAAC;IAAG,WAAW;GAAK;GACzD,UAAU,CAAC,KAAK,CAAC;EACnB;EACA,SAAS,OAAO,YACd,oBAAoB,MAElB,SAAS;GACT,aAAa;IAAE;IAAO,UAAU,CAAC,KAAK,CAAC;GAAE;GACzC,uBAAuB;IAAE;IAAO,UAAU,CAAC,KAAK,CAAC;GAAE;GAEnD,YAAY,EAAE,cACZ,MAAM,WAAW,WAAW,YAAY,MAAM,SAAS,OAAO,IAC1D,EAAE,MAAM,IACR,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;IAAkB;GAAQ,EAAE;GAC/D,eAAe,EAAE,OAAO,WAAW,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAS,CAAC,EAAE;GAC1E,SAAS,EAAE,eAAe,EAAE,OAAO,WAAW,OAAO;IAAE,GAAG;IAAO,WAAW;GAAQ,CAAC,EAAE;EACzF,CAAC;EACH,qBACE,aAAa,KAA6C,CAAC,QAAQ,EACjE,SAAS,aAAa,WAAW,cAAc,CAAC,EAClD,EAAE;CACN,CAAC;AACH;;;;;;;;;;;;AC/IA,MAAa,mBAAmB,OAAO,OAAO;CAC5C,QAAQ,OAAO,SAAS;EAAC;EAAQ;EAAc;EAAQ;CAAQ,CAAC;CAChE,WAAW,OAAO,OAAO,OAAO,MAAM;AACxC,CAAC;AAGD,MAAa,qBAAqB,mBAAmB;CACnD,SAAS,CAAC;CACV,SAAS,CAAC;CACV,MAAM,CAAC;CACP,OAAO,CAAC;CACR,QAAQ,CAAC;CACT,QAAQ,EAAE,SAAS,OAAO,OAAO;AACnC,CAAC;;AAmBD,MAAa,aAAa,gBAAgB,IAAuB,CAAC,CAAC,cAAc;AAgBjF,MAAME,iBAAe,UACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAEvD,MAAM,YAAY,UAChB,OAAQ,OAA8C,SAAS,YAC/D,CAAC,mBAAmB,eAAe,CAAC,CAAC,SAAU,MAAoC,IAAI;AAEzF,MAAa,eAA0C,WAKjD;CACJ,MAAM,WAAW,gBAAoE;EACnF,IAAI,OAAO,YAAY,KAAA,GAAW,OAAO,OAAO,QAAQ,WAAW;EACnE,IACE,OAAO,cAAc,eACrB,OAAO,UAAU,cAAc,iBAAiB,YAEhD,OAAO,QAAQ,uBAAO,IAAI,MAAM,+BAA+B,CAAC;EAElE,OAAO,UAAU,aAAa,aAAa,WAAW;CACxD;CAEA,OAAO,OAAO,KAAK,OAAO,MAAM;EAC9B,OAAO;EACP,SAAS;EACT,MAAM,OAAO,OAAO;GAAE,OAAO,OAAO;GAAS,OAAO,OAAO;EAAQ,CAAC;EACpE,aAAa,EAAE,OAAO;GAAE,QAAQ;GAAiB,WAAW;EAAK,EAAE;EACnE,SAAS,OAAO,YACd,mBAAmB,MAEjB,SAAS;GAGT,eACE,MAAM,WAAW,gBAAgB,MAAM,WAAW,SAC9C,EAAE,MAAM,IACR,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAsB,EAAE;GAC3D,gBAAgB,EAAE,OAAO,WAAW,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAO,CAAC,EAAE;GACzE,aAAa,EAAE,OAAO,WAAW,OAAO;IAAE,GAAG;IAAO,QAAQ;IAAQ,WAAW;GAAK,CAAC,EAAE;GAEvF,cAAc,EAAE,OAAO,WAAW,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAO,CAAC,EAAE;GACvE,eAAe,EAAE,OAAO,WAAW,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAS,CAAC,EAAE;GAC1E,SAAS,EAAE,eAAe,EACxB,OAAO,WAAW,OAAO;IAAE,GAAG;IAAO,QAAQ;IAAQ,WAAW;GAAQ,CAAC,EAC3E;EACF,CAAC;EACH,YAAW,SACT,gBAAgB,KAA2C,CAAC,EAAC,WAAU,EACrE,QAAQ,MACN,OAAO,OAAO,OAAO,OAAO;GAAE,OAAO,OAAO;GAAS,OAAO,OAAO;EAAQ,CAAC,CAAC,GAC7E;GACE,UAAU;GACV,2BAA0B,UACxB,MAAM,WAAW,gBAAgB,MAAM,WAAW,SAC9C,OAAO,KAAK;IAAE,OAAO,KAAK;IAAO,OAAO,KAAK;GAAM,CAAC,IACpD,OAAO,KAAK;GAClB,UAAU,gBACR,OAAO,WAAW;IAChB,WAAW,QAAQ,WAAW;IAC9B,QAAQ,UAAmB;GAC7B,CAAC;GACH,kBAAkB,mBAAmB,KAAK;GAC1C,kBAAkB,mBAAmB,MAAM;GAC3C,iBAAgB,UACd,SAAS,KAAK,IACV,mBAAmB,OAAO,IAC1B,mBAAmB,OAAO,EAAE,SAASA,cAAY,KAAK,EAAE,CAAC;GAC/D,UAAS,WACP,OAAO,WAAW;IAChB,KAAK,MAAM,SAAS,OAAO,UAAU,GAAG,MAAM,KAAK;GACrD,CAAC;EACL,CACF,EACF,EAAE;CACN,CAAC;AACH;;;;;;;;;;;AC7HA,MAAa,kBAAkB,OAAO,SAAS;CAAC;CAAW;CAAU;AAAQ,CAAC;AAG9E,MAAa,mBAAmB,OAAO,OAAO;CAC5C,QAAQ,OAAO,OAAO,OAAO,QAAQ,eAAe;CACpD,WAAW,OAAO,OAAO,OAAO,MAAM;AACxC,CAAC;AAGD,MAAa,qBAAqB,mBAAmB;CACnD,UAAU,EAAE,QAAQ,OAAO,OAAO,OAAO,QAAQ,eAAe,EAAE;CAClE,SAAS;EAAE,MAAM,OAAO;EAAQ,OAAO;CAAgB;CACvD,SAAS,CAAC;CACV,QAAQ,EAAE,SAAS,OAAO,OAAO;AACnC,CAAC;;AAqBD,MAAa,QAAQ,gBAAgB,IAAc,CAAC,CAAC,aAAa;AAgBlE,MAAM,eAAe,UACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAEvD,MAAM,aAAa,OAAe,SAAkC;CAClE,IAAI,UAAU,aAAa,UAAU,YAAY,UAAU,UACzD,MAAM,IAAI,MAAM,gCAAgC,KAAK,IAAI,OAAO;CAElE,OAAO;AACT;AAEA,MAAa,eAA0C,WAIjD;CACJ,MAAM,eAAyC;EAC7C,IAAI,OAAO,cAAc,aAAa,OAAO;EAC7C,IAAI,OAAO,WAAW,KAAA,GAAW,OAAO,OAAO,OAAO,SAAS;EAE/D,OADY,UAAU,eACR;CAChB;CAEA,OAAO,OAAO,KAWZ,OAAO,MAAM;EACb,OAAO;EACP,SAAS;EACT,MAAM,OAAO,OAAO,EAAE,OAAO,OAAO,MAAM,OAAO,MAAM,EAAE,CAAC;EAC1D,aAAa,EAAE,OAAO;GAAE,QAAQ,CAAC;GAAG,WAAW;EAAK,EAAE;EACtD,SAAS,OAAO,YACd,mBAAmB,MAEjB,SAAS;GACT,WAAW,EAAE,aAAa;IACxB,MAAM,OAAO,WAAW,MAAM,QAAQ,EAAE,GAAG,OAAO,CAAC;IACnD,OAAO,SAAS,MAAM,SAAS,EAAE,MAAM,IAAI,EAAE,OAAO;KAAE,GAAG;KAAO,QAAQ;IAAK,EAAE;GACjF;GACA,UAAU,EAAE,MAAM,YAChB,OAAO,OAAO,MAAM,QAAQ,IAAI,KAAK,MAAM,OAAO,UAAU,QACxD,EAAE,MAAM,IACR,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;KAAE,GAAG,MAAM;MAAS,OAAO;IAAM;GAAE,EAAE;GACxE,eACE,OAAO,KAAK,MAAM,MAAM,CAAC,CAAC,WAAW,IAAI,EAAE,MAAM,IAAI,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ,CAAC;GAAE,EAAE;GACzF,SAAS,EAAE,eAAe,EAAE,OAAO,WAAW,OAAO;IAAE,GAAG;IAAO,WAAW;GAAQ,CAAC,EAAE;EACzF,CAAC;EACH,YAAW,SAAQ;GACjB,MAAM,QAAQ,OAAO,KAAK,CAAC,GAAG,IAAI,IAAI,KAAK,KAAK,CAAC,CAAC;GAClD,OAAO,gBAAgB,KAA2C,CAAC,EAAC,WAAU,EAC5E,OAAO,MAAM,OAAO,OAAO,OAAO,MAAM,OAAO,MAAM,CAAC,GAAG;IACvD,UAAU;IACV,gCAAgC;IAChC,UAAU,UACR,OAAO,IAAI,aAAa;KACtB,MAAM,MAAM,OAAO;KACnB,IAAI,QAAQ,MAAM,MAAM,IAAI,MAAM,6BAA6B;KAC/D,MAAM,SAAS,OAAO,MAAM,UAA8B;KAC1D,MAAM,2BAAW,IAAI,IAAoC;KAEzD,IAAI,OAAsB,QAAQ,QAAQ;KAC1C,MAAM,SAAS,YAAgC;MAC7C,MAAM,YACJ,OAAO,WAAW,MAAM,MAAM,QAAQ,OAAO,CAAC,CAAC,CAAC,WACxC,KAAA,SACA,KAAA,CACR;MACF,OAAO,KAAK,KAAK,KAAK,GAAG;KAC3B;KAGA,MAAM,UAA6E,CAAC;KACpF,KAAK,MAAM,QAAQ,OAAO;MACxB,MAAM,SAAS,OAAO,OAAO,WAAW;OACtC,WAAW,IAAI,MAAM,EAAE,KAAK,CAAC;OAC7B,QAAQ,UAAmB;MAC7B,CAAC;MACD,QAAQ,KAAK;OAAC;OAAM;OAAQ,UAAU,OAAO,OAAO,IAAI;MAAC,CAAU;KACrE;KACA,MAAM,WAA4C,CAAC;KACnD,KAAK,MAAM,CAAC,MAAM,QAAQ,UAAU,SAAS;MAC3C,OAAO,iBAAiB;OACtB,IAAI;QACF,MACE,mBAAmB,QAAQ;SAAE;SAAM,OAAO,UAAU,OAAO,OAAO,IAAI;QAAE,CAAC,CAC3E;OACF,QAAQ;QAGN,MACE,mBAAmB,OAAO,EACxB,SAAS,gCAAgC,OAC3C,CAAC,CACH;OACF;MACF;MACA,SAAS,IAAI,MAAM,MAAM;MACzB,SAAS,QAAQ;KACnB;KACA,OAAO;MAAE;MAAU;MAAU;KAAO;IACtC,CAAC;IAGH,aAAa,EAAE,eAAe,mBAAmB,SAAS,EAAE,QAAQ,EAAE,GAAG,SAAS,EAAE,CAAC;IACrF,kBAAkB,mBAAmB,QAAQ;IAC7C,iBAAgB,UAAS,mBAAmB,OAAO,EAAE,SAAS,YAAY,KAAK,EAAE,CAAC;IAClF,UAAU,EAAE,UAAU,aACpB,OAAO,OACL,OAAO,QACL,OAAO,WAAW;KAChB,KAAK,MAAM,UAAU,SAAS,OAAO,GAAG,OAAO,WAAW;IAC5D,CAAC,GACD,MAAM,SAAS,MAAM,CACvB,CACF;GACJ,CAAC,EACH,EAAE;EACJ;EACA,qBAKE,aAAa,KAAyD,CAAC,QAAQ,EAG7E,SAAS,aAAa,WACpB,OAAO,OACL,OAAO,YAAY,MAAM,KAAK;GAE5B,iBAAiB,OAAO,QAAQ,OAAO,KAAK;GAC5C,YAAY,EAAE,aAAa,OAAO,QAAQ,OAAO,UAAU,MAAM,CAAC;EACpE,CAAC,CACH,CACF,EACF,EAAE;CACN,CAAC;AACH"}
@@ -1,5 +1,5 @@
1
- import { a as Moved, c as FocusScope, i as MoveStarted, l as tabbableWithin, n as Move, o as HideOutside, r as MoveEnded, s as ScrollLock, t as Fact } from "../move-DNiMPcuf.mjs";
2
- import { Schema } from "effect";
1
+ import { A as ScrollLock, C as targetOf, D as MoveStarted, E as MoveEnded, M as tabbableWithin, O as Moved, S as Targets, T as Move, _ as boxOf, a as TextEdited, b as TargetHovered, c as DRAG_THRESHOLD, d as DragFact, f as DragPlace, g as PointerDrag, h as DraggedOver, i as TextCommitted, j as FocusScope, k as HideOutside, l as DragCancelled, m as DragZone, n as EditableText, o as TextFact, p as DragStarted, r as TextCancelled, s as textOf, t as EditAsked, u as DragDropped, v as zoneOf, w as Fact, x as TargetPressed, y as TargetFact } from "../editable-text-CKkS_mWt.mjs";
2
+ import { Schema, Stream } from "effect";
3
3
  import * as Mount from "foldkit/mount";
4
4
  import { Command } from "foldkit/command";
5
5
  //#region src/dom/autofocus.d.ts
@@ -17,12 +17,26 @@ export declare const ClipboardMessage: import("foldkit/message").MessageUnion<{
17
17
  };
18
18
  }>;
19
19
  export type ClipboardMessage = typeof ClipboardMessage.Type;
20
+ export declare const ClipboardReadMessage: import("foldkit/message").MessageUnion<{
21
+ readonly Read: {
22
+ readonly text: Schema.String;
23
+ };
24
+ readonly ReadFailed: {
25
+ readonly message: Schema.String;
26
+ };
27
+ }>;
28
+ export type ClipboardReadMessage = typeof ClipboardReadMessage.Type;
20
29
  /**
21
30
  * Copies text to the clipboard. Without a clipboard API (SSR, insecure
22
31
  * context, denied permission) the command yields `CopyFailed` instead of
23
32
  * throwing, so `update` handles both outcomes as Messages.
24
33
  */
25
34
  export declare const copyText: (text: string) => Command<ClipboardMessage, never, never>;
35
+ /**
36
+ * Reads the clipboard's text. The browser may ask the user first; a refusal,
37
+ * an insecure context or no clipboard API yields `ReadFailed`.
38
+ */
39
+ export declare const readText: () => Command<ClipboardReadMessage, never, never>;
26
40
  //#endregion
27
41
  //#region src/dom/input-mask.d.ts
28
42
  export declare const Input: Schema.TaggedStruct<"Input", {
@@ -42,6 +56,12 @@ export declare const InputMask: Mount.MountDefinitionWithArgs<"InputMask", {
42
56
  readonly raw: string;
43
57
  }>;
44
58
  //#endregion
59
+ //#region src/dom/keep-in-view.d.ts
60
+ export declare const KeepInView: Mount.MountDefinitionWithArgs<"KeepInView", {
61
+ /** What is marked, as a selector within the element: `[aria-selected="true"]`. */
62
+ selector: Schema.String;
63
+ }, never>;
64
+ //#endregion
45
65
  //#region src/dom/script-loader.d.ts
46
66
  export declare const ScriptMessage: import("foldkit/message").MessageUnion<{
47
67
  readonly Loaded: {};
@@ -71,5 +91,53 @@ export type ShareMessage = typeof ShareMessage.Type;
71
91
  /** Shares data through the platform sheet. */
72
92
  export declare const share: (data: ShareData) => Command<ShareMessage, never, never>;
73
93
  //#endregion
74
- export { Fact, FocusScope, HideOutside, Move, MoveEnded, MoveStarted, Moved, ScrollLock, tabbableWithin };
94
+ //#region src/dom/measure.d.ts
95
+ /** The custom properties `Measure` writes for a target named `name`. */
96
+ export declare const measured: (name: string) => {
97
+ readonly x: `--fk-${string}-x`;
98
+ readonly y: `--fk-${string}-y`;
99
+ readonly w: `--fk-${string}-w`;
100
+ readonly h: `--fk-${string}-h`;
101
+ readonly display: `--fk-${string}-display`;
102
+ };
103
+ export declare const Measure: Mount.MountDefinitionWithArgs<"Measure", {
104
+ /** Each target by name, as a selector within the element: `{ selected: '[aria-selected="true"]' }`. */
105
+ targets: Schema.$Record<Schema.String, Schema.String>;
106
+ }, never>;
107
+ //#endregion
108
+ //#region src/dom/follow-tab-stop.d.ts
109
+ export declare const FollowTabStop: Mount.MountDefinitionNoArgs<"FollowTabStop", never>;
110
+ //#endregion
111
+ //#region src/dom/scroll.d.ts
112
+ /** The part of the Navigation API used here: TypeScript's DOM library does not declare it yet. */
113
+ interface Navigation extends EventTarget {
114
+ readonly currentEntry: {
115
+ readonly key: string;
116
+ } | null;
117
+ }
118
+ declare global {
119
+ interface Window {
120
+ readonly navigation?: Navigation;
121
+ }
122
+ }
123
+ export interface KeepScrollOptions {
124
+ /** The `sessionStorage` key holding entry offsets. Default `'foldkit:scroll'`. */
125
+ readonly storageKey?: string | undefined;
126
+ /** Entries remembered at most: the oldest are forgotten first. Default 50. */
127
+ readonly kept?: number | undefined;
128
+ /**
129
+ * How long a restore holds its place while the next screen settles, in
130
+ * milliseconds. Default 2000.
131
+ */
132
+ readonly patience?: number | undefined;
133
+ }
134
+ /**
135
+ * Keeps the window's scroll across same-document navigations: a pushed entry
136
+ * starts at the top, and Back, Forward or a reload return where that entry
137
+ * was. Without a window the stream is empty instead of throwing. Lift with
138
+ * `Subscription.persistent`; the stream sends no Messages.
139
+ */
140
+ export declare const keepScroll: (options?: KeepScrollOptions) => Stream.Stream<never>;
141
+ //#endregion
142
+ export { DRAG_THRESHOLD, DragCancelled, DragDropped, DragFact, DragPlace, DragStarted, DragZone, DraggedOver, EditAsked, EditableText, Fact, FocusScope, HideOutside, Move, MoveEnded, MoveStarted, Moved, PointerDrag, ScrollLock, TargetFact, TargetHovered, TargetPressed, Targets, TextCancelled, TextCommitted, TextEdited, TextFact, boxOf, tabbableWithin, targetOf, textOf, zoneOf };
75
143
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts"],"mappings":";;;;;qBAUa,SAAO,OAAA;YACR,iBAAiB,QAAQ;qBAExB,WAAS,MAAA;;;;;qBCJT,4CAAgB;;;sBAAhB,OAAA;;;YAID,0BAA0B,iBAAiB;;;;;;qBAU1C,WAAY,iBAAe,QAAQ;;;qBCXnC,OAAK,OAAA;;;;YAIN,eAAe,MAAM;qBA2BpB,YACX,eACA;WACY;WAAyB;;qBAoC1B,WAAS,MAAA;;;;;;;;;qBCvET,yCAAa;;;sBAAb,OAAA;;;YAID,uBAAuB,cAAc;;qBASpC,aAAc,gBAAc,QAAQ;;;qBCbpC,WAAS,OAAA;;;;;YAKV,mBAAmB,UAAU;qBAE5B,wCAAY;;;;sBAAZ,OAAA;;;YAKD,sBAAsB,aAAa;;qBAUlC,QAAS,MAAM,cAAY,QAAQ"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/keep-in-view.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts","../../src/dom/measure.ts","../../src/dom/follow-tab-stop.ts","../../src/dom/scroll.ts"],"mappings":";;;;;qBAUa,SAAO,OAAA;YACR,iBAAiB,QAAQ;qBAExB,WAAS,MAAA;;;;;qBCJT,4CAAgB;;;sBAAhB,OAAA;;;YAID,0BAA0B,iBAAiB;qBAE1C,gDAAoB;;;;;;;;YAIrB,8BAA8B,qBAAqB;;;;;;qBAUlD,WAAY,iBAAe,QAAQ;;;;;qBAsBnC,gBAAe,QAAQ;;;qBCvCvB,OAAK,OAAA;;;;YAIN,eAAe,MAAM;qBA2BpB,YACX,eACA;WACY;WAAyB;;qBAoC1B,WAAS,MAAA;;;;;;;;;qBCtET,YAAU,MAAA;;;;;;qBCDV,yCAAa;;;sBAAb,OAAA;;;YAID,uBAAuB,cAAc;;qBASpC,aAAc,gBAAc,QAAQ;;;qBCbpC,WAAS,OAAA;;;;;YAKV,mBAAmB,UAAU;qBAE5B,wCAAY;;;;sBAAZ,OAAA;;;YAKD,sBAAsB,aAAa;;qBAUlC,QAAS,MAAM,cAAY,QAAQ;;;;qBCVnC,WAAY;;;;;;;qBASZ,SAAO,MAAA;;;;;;qBCrBP,eAAa,MAAA;;;;UCuBhB,mBAAmB;WAClB;aAAyB;;;QAE5B;YACI;aACC,aAAa;;;iBAIT;;WAEN;;WAEA;;;;;WAKA;;;;;;;;qBAqHE,aAAc,UAAS,sBAAyB,OAAO"}
@@ -1,5 +1,5 @@
1
- import { a as HideOutside, c as tabbableWithin, i as Moved, n as MoveEnded, o as ScrollLock, r as MoveStarted, s as FocusScope, t as Move } from "../move-CZwHyTGk.mjs";
2
- import { Effect, Queue, Schema, Stream } from "effect";
1
+ import { C as MoveEnded, D as ScrollLock, E as HideOutside, O as FocusScope, S as Move, T as Moved, _ as zoneOf, a as TextCommitted, b as Targets, c as DRAG_THRESHOLD, d as DragPlace, f as DragStarted, g as boxOf, h as PointerDrag, i as TextCancelled, k as tabbableWithin, l as DragCancelled, m as DraggedOver, n as EditAsked, o as TextEdited, p as DragZone, r as EditableText, s as textOf, t as FollowTabStop, u as DragDropped, v as TargetHovered, w as MoveStarted, x as targetOf, y as TargetPressed } from "../follow-tab-stop-DpxwLXvU.mjs";
2
+ import { Effect, Option, Queue, Schema, Stream } from "effect";
3
3
  import * as Mount from "foldkit/mount";
4
4
  import { defineMessageUnion } from "foldkit/message";
5
5
  //#region src/dom/autofocus.ts
@@ -22,14 +22,18 @@ const Autofocus = Mount.define("Autofocus", {
22
22
  //#endregion
23
23
  //#region src/dom/clipboard.ts
24
24
  /**
25
- * Clipboard copy as a Command: runs in `update` beside any bundle, yields
26
- * `Copied` on success and `CopyFailed` otherwise. No Model involved: the
27
- * clipboard is not application state.
25
+ * The clipboard as Commands: run in `update` beside any bundle. A copy yields
26
+ * `Copied` or `CopyFailed`, a read `Read` or `ReadFailed`. No Model involved:
27
+ * the clipboard is not application state.
28
28
  */
29
29
  const ClipboardMessage = defineMessageUnion({
30
30
  Copied: {},
31
31
  CopyFailed: { message: Schema.String }
32
32
  });
33
+ const ClipboardReadMessage = defineMessageUnion({
34
+ Read: { text: Schema.String },
35
+ ReadFailed: { message: Schema.String }
36
+ });
33
37
  const failMessage$2 = (error) => error instanceof Error ? error.message : String(error);
34
38
  /**
35
39
  * Copies text to the clipboard. Without a clipboard API (SSR, insecure
@@ -47,6 +51,20 @@ const copyText = (text) => ({
47
51
  onSuccess: () => Effect.succeed(ClipboardMessage.Copied())
48
52
  }) : Effect.succeed(ClipboardMessage.CopyFailed({ message: "clipboard is unavailable" }))
49
53
  });
54
+ /**
55
+ * Reads the clipboard's text. The browser may ask the user first; a refusal,
56
+ * an insecure context or no clipboard API yields `ReadFailed`.
57
+ */
58
+ const readText = () => ({
59
+ name: "Clipboard.read",
60
+ effect: typeof navigator !== "undefined" && typeof navigator.clipboard?.readText === "function" ? Effect.matchEffect(Effect.tryPromise({
61
+ try: () => navigator.clipboard.readText(),
62
+ catch: failMessage$2
63
+ }), {
64
+ onFailure: (message) => Effect.succeed(ClipboardReadMessage.ReadFailed({ message })),
65
+ onSuccess: (text) => Effect.succeed(ClipboardReadMessage.Read({ text }))
66
+ }) : Effect.succeed(ClipboardReadMessage.ReadFailed({ message: "clipboard is unavailable" }))
67
+ });
50
68
  //#endregion
51
69
  //#region src/dom/input-mask.ts
52
70
  /**
@@ -127,6 +145,40 @@ const InputMask = Mount.defineStream("InputMask", {
127
145
  }))
128
146
  });
129
147
  //#endregion
148
+ //#region src/dom/keep-in-view.ts
149
+ /**
150
+ * Keeps what is marked in view as a Mount: whenever an element in the mounted
151
+ * one's subtree newly matches `selector`, such as the row just selected in a
152
+ * list, it is scrolled into view the least amount that shows it (`nearest`).
153
+ * The element keeps its identity and its focus: nothing is redrawn to move it.
154
+ *
155
+ * It watches with a `MutationObserver` for as long as it is mounted, which is as
156
+ * long as the list or canvas that can change what is marked.
157
+ */
158
+ const KeepInView = Mount.defineStream("KeepInView", {
159
+ messages: [Schema.Never],
160
+ args: {
161
+ /** What is marked, as a selector within the element: `[aria-selected="true"]`. */
162
+ selector: Schema.String },
163
+ execute: ({ element, selector }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
164
+ let marked = element.querySelector(selector);
165
+ const observer = new MutationObserver(() => {
166
+ const now = element.querySelector(selector);
167
+ if (now !== null && now !== marked && typeof now.scrollIntoView === "function") now.scrollIntoView({
168
+ block: "nearest",
169
+ inline: "nearest"
170
+ });
171
+ marked = now;
172
+ });
173
+ observer.observe(element, {
174
+ subtree: true,
175
+ childList: true,
176
+ attributes: true
177
+ });
178
+ return observer;
179
+ }), (observer) => Effect.sync(() => observer.disconnect())))
180
+ });
181
+ //#endregion
130
182
  //#region src/dom/script-loader.ts
131
183
  /**
132
184
  * Script loading as a Command: appends a `src` script to the head unless one
@@ -214,6 +266,251 @@ const share = (data) => ({
214
266
  }) : Effect.succeed(ShareMessage.ShareFailed({ message: "share is unavailable" }))
215
267
  });
216
268
  //#endregion
217
- export { Autofocus, ClipboardMessage, FocusScope, Focused, HideOutside, Input, InputMask, Move, MoveEnded, MoveStarted, Moved, ScriptMessage, ScrollLock, ShareData, ShareMessage, applyMask, copyText, loadScript, share, tabbableWithin };
269
+ //#region src/dom/measure.ts
270
+ /**
271
+ * Measures marked elements as a Mount, for a view to draw something over them,
272
+ * such as an editor's selection outline, with no Message and no Model: where
273
+ * an element is, is presentation, known to the DOM and to no transition.
274
+ *
275
+ * For each named target, the first element in the mounted one's subtree that
276
+ * matches its selector is measured relative to the mounted element's scroll
277
+ * box, which is what an absolutely placed child of it is laid out in. The
278
+ * mounted element gets `--fk-<name>-x`, `-y`, `-w` and `-h` in pixels, and
279
+ * `--fk-<name>-display`: `block` while a target is found, `none` while none is.
280
+ *
281
+ * It measures again when the subtree changes (a selection moves), in the
282
+ * frame the change is drawn in; and, at most once a frame, when the mounted
283
+ * element scrolls, the window resizes, the mounted element or a measured one
284
+ * changes size, an image or a font inside loads, and on every frame of a
285
+ * transition or an animation inside. What moves a target by any other way,
286
+ * such as a stylesheet added that shifts it without resizing it, is seen at
287
+ * the next of these.
288
+ */
289
+ /** The custom properties `Measure` writes for a target named `name`. */
290
+ const measured = (name) => ({
291
+ x: `--fk-${name}-x`,
292
+ y: `--fk-${name}-y`,
293
+ w: `--fk-${name}-w`,
294
+ h: `--fk-${name}-h`,
295
+ display: `--fk-${name}-display`
296
+ });
297
+ const Measure = Mount.defineStream("Measure", {
298
+ messages: [Schema.Never],
299
+ args: {
300
+ /** Each target by name, as a selector within the element: `{ selected: '[aria-selected="true"]' }`. */
301
+ targets: Schema.Record(Schema.String, Schema.String) },
302
+ execute: ({ element, targets }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
303
+ const container = element;
304
+ const names = Object.keys(targets);
305
+ let watched = [];
306
+ let frame;
307
+ let animating = false;
308
+ const measure = () => {
309
+ if (frame !== void 0) cancelAnimationFrame(frame);
310
+ frame = void 0;
311
+ const box = container.getBoundingClientRect();
312
+ const found = [];
313
+ for (const name of names) {
314
+ const property = measured(name);
315
+ const target = container.querySelector(targets[name]);
316
+ if (target === null) {
317
+ container.style.setProperty(property.display, "none");
318
+ continue;
319
+ }
320
+ found.push(target);
321
+ const rect = target.getBoundingClientRect();
322
+ const x = rect.left - box.left - container.clientLeft + container.scrollLeft;
323
+ const y = rect.top - box.top - container.clientTop + container.scrollTop;
324
+ container.style.setProperty(property.x, `${x}px`);
325
+ container.style.setProperty(property.y, `${y}px`);
326
+ container.style.setProperty(property.w, `${rect.width}px`);
327
+ container.style.setProperty(property.h, `${rect.height}px`);
328
+ container.style.setProperty(property.display, "block");
329
+ }
330
+ if (found.length !== watched.length || found.some((each, at) => each !== watched[at])) {
331
+ for (const each of watched) sizes.unobserve(each);
332
+ for (const each of found) sizes.observe(each);
333
+ watched = found;
334
+ }
335
+ if (animating) {
336
+ animating = container.getAnimations({ subtree: true }).some((animation) => animation.playState === "running");
337
+ if (animating) soon();
338
+ }
339
+ };
340
+ const soon = () => {
341
+ if (frame === void 0) frame = requestAnimationFrame(measure);
342
+ };
343
+ const moving = () => {
344
+ animating = true;
345
+ soon();
346
+ };
347
+ const changes = new MutationObserver(measure);
348
+ const sizes = new ResizeObserver(soon);
349
+ document.fonts.addEventListener("loadingdone", soon);
350
+ changes.observe(container, {
351
+ subtree: true,
352
+ childList: true,
353
+ attributes: true
354
+ });
355
+ sizes.observe(container);
356
+ container.addEventListener("scroll", soon, { passive: true });
357
+ window.addEventListener("resize", soon);
358
+ container.addEventListener("load", soon, { capture: true });
359
+ container.addEventListener("transitionrun", moving);
360
+ container.addEventListener("animationstart", moving);
361
+ measure();
362
+ return () => {
363
+ if (frame !== void 0) cancelAnimationFrame(frame);
364
+ changes.disconnect();
365
+ sizes.disconnect();
366
+ container.removeEventListener("scroll", soon);
367
+ window.removeEventListener("resize", soon);
368
+ container.removeEventListener("load", soon, { capture: true });
369
+ document.fonts.removeEventListener("loadingdone", soon);
370
+ container.removeEventListener("transitionrun", moving);
371
+ container.removeEventListener("animationstart", moving);
372
+ };
373
+ }), (stop) => Effect.sync(stop)))
374
+ });
375
+ //#endregion
376
+ //#region src/dom/scroll.ts
377
+ /**
378
+ * Where the window is scrolled, across an application's own navigations, as
379
+ * a stream. Left to itself, a browser keeps the scroll of the entry it
380
+ * leaves when a link pushes a new one, and on Back restores the old offset
381
+ * before the application has drawn the entry it returns to, so the offset is
382
+ * clamped to a page still loading. Here a pushed entry starts at the top,
383
+ * and Back, Forward or a reload return where that entry was, once the page
384
+ * is tall enough to hold it.
385
+ *
386
+ * Three rules, each found the slow way:
387
+ *
388
+ * 1. The offset is taken when the reader acts. The next screen is drawn
389
+ * before a Navigate Command pushes the address, so by the time the URL
390
+ * changes the window is already clamped to the new, shorter page. The
391
+ * scroll is recorded at the reader's click or key (capture phase), and at
392
+ * the start of Back and Forward, which begin in the browser before
393
+ * anything is drawn.
394
+ * 2. A restore holds its place while the next screen settles. A restore made
395
+ * when the entry changes is undone by the loading state drawn right after
396
+ * it, so the target is held on each frame for a moment, until the reader
397
+ * scrolls, touches or types, or another navigation starts.
398
+ * 3. Entries are keyed by the Navigation API's entry key, which a reload and
399
+ * a full navigation keep; offsets rest in `sessionStorage`, so Back into
400
+ * another document restores too. Where the browser has no Navigation API
401
+ * the stream attaches nothing and leaves the browser's own behaviour
402
+ * alone.
403
+ *
404
+ * Lift with `Subscription.persistent`; the stream sends no Messages. Only
405
+ * the window scrolls: a nested container is the route graph's to know, when
406
+ * `Site.transition` arrives.
407
+ */
408
+ const navigationTypeOf = (event) => "navigationType" in event ? Option.some(event.navigationType) : Option.none();
409
+ const readSaved = (storageKey) => {
410
+ try {
411
+ const stored = JSON.parse(sessionStorage.getItem(storageKey) ?? "[]");
412
+ return new Map(Array.isArray(stored) ? stored.filter((pair) => Array.isArray(pair) && typeof pair[0] === "string" && typeof pair[1] === "number") : []);
413
+ } catch {
414
+ return /* @__PURE__ */ new Map();
415
+ }
416
+ };
417
+ /** Starts keeping the scroll; the release undoes everything it installed. */
418
+ const attach = (storageKey, kept, patience) => {
419
+ const navigation = window.navigation;
420
+ if (navigation === void 0) return () => {};
421
+ const restoration = window.history.scrollRestoration;
422
+ window.history.scrollRestoration = "manual";
423
+ const saved = readSaved(storageKey);
424
+ const detaches = [];
425
+ const on = (target, type, listener, capture) => {
426
+ target.addEventListener(type, listener, capture);
427
+ detaches.push(() => target.removeEventListener(type, listener, capture));
428
+ };
429
+ const keyOf = () => Option.fromNullOr(navigation.currentEntry).pipe(Option.map((entry) => entry.key));
430
+ /** Where the current entry was left, if it has been. */
431
+ const savedHere = () => Option.flatMap(keyOf(), (key) => Option.fromUndefinedOr(saved.get(key)));
432
+ const remember = () => {
433
+ const key = keyOf();
434
+ if (Option.isNone(key)) return;
435
+ saved.delete(key.value);
436
+ saved.set(key.value, window.scrollY);
437
+ for (const old of saved.keys()) {
438
+ if (saved.size <= kept) break;
439
+ saved.delete(old);
440
+ }
441
+ };
442
+ let settling = new AbortController();
443
+ /**
444
+ * Holds the window at `top` while the next screen settles, whenever the page
445
+ * can hold it, until the reader scrolls, touches or types, or another
446
+ * navigation starts.
447
+ */
448
+ const settle = (top) => {
449
+ settling.abort();
450
+ settling = new AbortController();
451
+ const { signal } = settling;
452
+ for (const input of [
453
+ "wheel",
454
+ "touchstart",
455
+ "keydown",
456
+ "pointerdown"
457
+ ]) window.addEventListener(input, () => settling.abort(), {
458
+ passive: true,
459
+ signal
460
+ });
461
+ const until = performance.now() + patience;
462
+ const step = () => {
463
+ if (signal.aborted) return;
464
+ if (document.documentElement.scrollHeight - window.innerHeight >= top && Math.round(window.scrollY) !== top) window.scrollTo({
465
+ top,
466
+ behavior: "instant"
467
+ });
468
+ if (performance.now() < until) requestAnimationFrame(step);
469
+ else settling.abort();
470
+ };
471
+ step();
472
+ };
473
+ const acted = () => remember();
474
+ for (const input of ["click", "keydown"]) on(window, input, acted, true);
475
+ const traversing = (event) => {
476
+ if (Option.contains(navigationTypeOf(event), "traverse")) remember();
477
+ };
478
+ on(navigation, "navigate", traversing);
479
+ const changed = (event) => {
480
+ const type = navigationTypeOf(event);
481
+ if (Option.contains(type, "push")) settle(0);
482
+ else if (Option.contains(type, "traverse")) settle(Option.getOrElse(savedHere(), () => 0));
483
+ };
484
+ on(navigation, "currententrychange", changed);
485
+ const hidden = () => {
486
+ remember();
487
+ try {
488
+ sessionStorage.setItem(storageKey, JSON.stringify([...saved]));
489
+ } catch {}
490
+ };
491
+ on(window, "pagehide", hidden);
492
+ const here = savedHere();
493
+ if (Option.isSome(here)) settle(here.value);
494
+ return () => {
495
+ settling.abort();
496
+ for (const detach of detaches) detach();
497
+ window.history.scrollRestoration = restoration;
498
+ };
499
+ };
500
+ /**
501
+ * Keeps the window's scroll across same-document navigations: a pushed entry
502
+ * starts at the top, and Back, Forward or a reload return where that entry
503
+ * was. Without a window the stream is empty instead of throwing. Lift with
504
+ * `Subscription.persistent`; the stream sends no Messages.
505
+ */
506
+ const keepScroll = (options = {}) => {
507
+ if (typeof window === "undefined") return Stream.empty;
508
+ const storageKey = options.storageKey ?? "foldkit:scroll";
509
+ const kept = options.kept ?? 50;
510
+ const patience = options.patience ?? 2e3;
511
+ return Stream.callback(() => Effect.acquireRelease(Effect.sync(() => attach(storageKey, kept, patience)), (detach) => Effect.sync(detach)));
512
+ };
513
+ //#endregion
514
+ export { Autofocus, ClipboardMessage, ClipboardReadMessage, DRAG_THRESHOLD, DragCancelled, DragDropped, DragPlace, DragStarted, DragZone, DraggedOver, EditAsked, EditableText, FocusScope, Focused, FollowTabStop, HideOutside, Input, InputMask, KeepInView, Measure, Move, MoveEnded, MoveStarted, Moved, PointerDrag, ScriptMessage, ScrollLock, ShareData, ShareMessage, TargetHovered, TargetPressed, Targets, TextCancelled, TextCommitted, TextEdited, applyMask, boxOf, copyText, keepScroll, loadScript, measured, readText, share, tabbableWithin, targetOf, textOf, zoneOf };
218
515
 
219
516
  //# sourceMappingURL=index.mjs.map