foldkit-primitives 0.3.0 → 0.5.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 +144 -723
  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-BSZsUrrk.mjs +805 -0
  16. package/dist/follow-tab-stop-BSZsUrrk.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 +54 -12
  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 +20 -9
  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":[],"sources":["../../src/state/history.ts","../../src/state/locale.ts","../../src/state/pagination.ts","../../src/state/range.ts","../../src/state/selection.ts","../../src/state/virtual.ts"],"sourcesContent":["/**\n * Undo/redo as a bundle factory: the past, present, and future of one value\n * live in the Model, so every step replays. Pushing records, even an unchanged\n * value; skipping duplicates is the application's policy, not the stack's.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const HistoryModel = <Value>(value: Schema.Codec<Value, unknown>) =>\n Schema.Struct({\n past: Schema.Array(value),\n present: value,\n future: Schema.Array(value),\n })\nexport type HistoryModel<Value> = {\n readonly past: ReadonlyArray<Value>\n readonly present: Value\n readonly future: ReadonlyArray<Value>\n}\n\nexport const history = <const Name extends string, Value>(config: {\n readonly name: Name\n readonly value: Schema.Codec<Value, unknown>\n readonly capacity?: number | undefined\n}) => {\n const capacity = config.capacity ?? 100\n // A negative or fractional capacity would silently keep a wrong-sized past;\n // fail at the factory, naming the caller, instead.\n if (!Number.isInteger(capacity) || capacity < 0) {\n throw new Error(`History \"${config.name}\": capacity must be a non-negative integer`)\n }\n const Model = HistoryModel(config.value)\n const Message = defineMessageUnion({\n Push: { value: config.value },\n Undo: {},\n Redo: {},\n Clear: {},\n })\n return Object.assign(\n Bundle.make(config.name, {\n Model,\n Message,\n args: Schema.Struct({ initial: config.value }),\n init: args => ({ model: { past: [], present: args.initial, future: [] } }),\n update: (model, message) =>\n Message.match(message, {\n Push: ({ value }) => ({\n model: {\n // slice(-0) is slice(0): a zero capacity keeps nothing, explicitly.\n past: capacity === 0 ? [] : [...model.past, model.present].slice(-capacity),\n present: value,\n future: [],\n },\n }),\n Undo: () => {\n const previous = model.past[model.past.length - 1]\n if (previous === undefined) return { model }\n return {\n model: {\n past: model.past.slice(0, -1),\n present: previous,\n future: [model.present, ...model.future],\n },\n }\n },\n Redo: () => {\n const next = model.future[0]\n if (next === undefined) return { model }\n return {\n model: {\n past: [...model.past, model.present],\n present: next,\n future: model.future.slice(1),\n },\n }\n },\n Clear: () => ({ model: { past: [], present: model.present, future: [] } }),\n }),\n }),\n // The union object is both the Message codec and its constructors, so a\n // placement dispatches `EditHistory.Message.Push(...)` like any bundle's\n // exported Message.\n { Message },\n )\n}\n\n/** Whether Undo would move: the past is non-empty. */\nexport const canUndo = <Value>(model: HistoryModel<Value>): boolean => model.past.length > 0\n\n/** Whether Redo would move: the future is non-empty. */\nexport const canRedo = <Value>(model: HistoryModel<Value>): boolean => model.future.length > 0\n","/**\n * Locale as a bundle: the current locale string in the Model. Starts from\n * `navigator.language` where available, falling back to the configured\n * default; changes arrive as Messages. No subscriptions: language changes\n * take effect on reload.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const LocaleModel = Schema.Struct({ locale: Schema.String })\nexport type LocaleModel = typeof LocaleModel.Type\n\nexport const LocaleMessage = defineMessageUnion({ SetLocale: { locale: Schema.String } })\nexport type LocaleMessage = typeof LocaleMessage.Type\n\nexport const Locale = Bundle.make('Locale', {\n Model: LocaleModel,\n Message: LocaleMessage,\n args: Schema.Struct({ default: Schema.String }),\n init: args => ({\n model: {\n locale:\n typeof navigator !== 'undefined' && typeof navigator.language === 'string'\n ? navigator.language\n : args.default,\n },\n }),\n update: (model, message) =>\n LocaleMessage.match(message, {\n SetLocale: ({ locale }) => ({ model: { ...model, locale } }),\n }),\n})\n","/**\n * Pagination as a bundle: the page, page size, and total live in the Model,\n * so page changes replay and every transition clamps into range. Pure logic,\n * no streams: data loading stays the application's job.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const PaginationModel = Schema.Struct({\n page: Schema.Number,\n perPage: Schema.Number,\n total: Schema.NullOr(Schema.Number),\n})\nexport type PaginationModel = typeof PaginationModel.Type\n\nexport const PaginationMessage = defineMessageUnion({\n GoToPage: { page: Schema.Number },\n NextPage: {},\n PrevPage: {},\n SetPerPage: { perPage: Schema.Number },\n SetTotal: { total: Schema.NullOr(Schema.Number) },\n})\nexport type PaginationMessage = typeof PaginationMessage.Type\n\nconst clampPage = (page: number, perPage: number, total: number | null): number => {\n if (!Number.isInteger(page)) return 1\n if (page < 1) return 1\n if (total === null) return page\n return Math.min(page, Math.max(1, Math.ceil(total / perPage)))\n}\n\n/** The number of pages, or null while the total is unknown. */\nexport const pageCount = (model: PaginationModel): number | null =>\n model.total === null ? null : Math.max(1, Math.ceil(model.total / model.perPage))\n\n/** The offset of the first item on the page, for a slice or a query. */\nexport const offset = (model: PaginationModel): number => (model.page - 1) * model.perPage\n\nexport const Pagination = Bundle.make('Pagination', {\n Model: PaginationModel,\n Message: PaginationMessage,\n args: Schema.Struct({\n perPage: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n total: Schema.optional(Schema.NullOr(Schema.Number.pipe(Schema.check(Schema.isFinite())))),\n }),\n init: args => ({\n model: { page: 1, perPage: args.perPage, total: args.total ?? null },\n }),\n update: (model, message) =>\n PaginationMessage.match(message, {\n GoToPage: ({ page }) => ({\n model: { ...model, page: clampPage(page, model.perPage, model.total) },\n }),\n NextPage: () => ({\n model: { ...model, page: clampPage(model.page + 1, model.perPage, model.total) },\n }),\n PrevPage: () => ({\n model: { ...model, page: clampPage(model.page - 1, model.perPage, model.total) },\n }),\n SetPerPage: ({ perPage }) =>\n perPage > 0\n ? { model: { ...model, perPage, page: clampPage(model.page, perPage, model.total) } }\n : { model },\n SetTotal: ({ total }) =>\n total === null || total >= 0\n ? {\n model: { ...model, total, page: clampPage(model.page, model.perPage, total) },\n }\n : { model },\n }),\n})\n","/**\n * A numeric range as an array: `range(1, 4)` is `[1, 2, 3]`, inclusive of\n * the start and exclusive of the end, like `Array.from` length math. The\n * pagination companion: `range(1, (pageCount(model) ?? 0) + 1)` is the page\n * list. A zero or non-finite step would loop forever, so it throws, naming\n * the caller — like History's capacity check.\n */\nexport const range = (start: number, end: number, step = 1): ReadonlyArray<number> => {\n if (!Number.isFinite(step) || step === 0) {\n throw new Error(`range: step must be a non-zero finite number, got ${step}`)\n }\n const values: Array<number> = []\n if (step > 0) {\n for (let value = start; value < end; value += step) {\n values.push(value)\n }\n } else {\n for (let value = start; value > end; value += step) {\n values.push(value)\n }\n }\n return values\n}\n","/**\n * A selection set as a bundle: the selected ids in the Model, changed one at\n * a time or replaced whole. Order follows first selection; toggling moves a\n * reselected id to the end. Pure logic, no streams.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const SelectionSetModel = Schema.Struct({ selected: Schema.Array(Schema.String) })\nexport type SelectionSetModel = typeof SelectionSetModel.Type\n\nexport const SelectionSetMessage = defineMessageUnion({\n Select: { id: Schema.String },\n Deselect: { id: Schema.String },\n Toggle: { id: Schema.String },\n ReplaceAll: { ids: Schema.Array(Schema.String) },\n Clear: {},\n})\nexport type SelectionSetMessage = typeof SelectionSetMessage.Type\n\nconst without = (selected: ReadonlyArray<string>, id: string): ReadonlyArray<string> =>\n selected.filter(other => other !== id)\n\n/** Whether the id is currently selected. */\nexport const isSelected = (model: SelectionSetModel, id: string): boolean =>\n model.selected.includes(id)\n\nexport const SelectionSet = Bundle.make('SelectionSet', {\n Model: SelectionSetModel,\n Message: SelectionSetMessage,\n init: () => ({ model: { selected: [] } }),\n update: (model, message) =>\n SelectionSetMessage.match(message, {\n Select: ({ id }) =>\n isSelected(model, id) ? { model } : { model: { selected: [...model.selected, id] } },\n Deselect: ({ id }) => ({ model: { selected: without(model.selected, id) } }),\n Toggle: ({ id }) =>\n isSelected(model, id)\n ? { model: { selected: without(model.selected, id) } }\n : { model: { selected: [...model.selected, id] } },\n ReplaceAll: ({ ids }) => ({ model: { selected: [...new Set(ids)] } }),\n Clear: () => ({ model: { selected: [] } }),\n }),\n})\n","/**\n * Windowing math for virtualized lists as pure functions: which rows to\n * render, how tall the scrollable area is, and where each row starts. Row\n * heights come from measured values with a per-row estimate fallback, so\n * unmeasured rows still occupy space and measuring later only corrects.\n * Keys are strings; heights are non-negative, and non-finite measurements\n * fall back to the estimate instead of poisoning the sums.\n *\n * The `Virtual` bundle below owns the scroll position and the measured\n * heights; `Viewport` reports container scrolls and `MeasureRow` reports\n * row heights, both as Mounts on the elements they observe. The application\n * renders `windowFor(...)` rows with its own views (keyed, so `each`\n * placements keep identity) inside a spacer of `totalHeight(...)`.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Mount from 'foldkit/mount'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport interface VirtualWindow {\n /** First visible row index, inclusive. */\n readonly start: number\n /** End row index, exclusive. */\n readonly end: number\n}\n\n/** Layout constants: per-row estimate, inter-row gap, and end padding. */\nexport interface VirtualLayout {\n readonly estimatedHeight: number\n readonly gap: number\n readonly paddingStart: number\n readonly paddingEnd: number\n}\n\nconst saneHeight = (height: number, estimatedHeight: number): number =>\n Number.isFinite(height) && height >= 0 ? height : estimatedHeight\n\nconst heightAt = (\n index: number,\n heights: Readonly<Record<string, number>>,\n keys: ReadonlyArray<string>,\n layout: VirtualLayout,\n): number => {\n const key = keys[index]\n if (key === undefined) return layout.estimatedHeight\n return saneHeight(heights[key] ?? layout.estimatedHeight, layout.estimatedHeight)\n}\n\n/** Total scrollable height: padding, measured rows, estimates, and gaps. */\nexport const totalHeight = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n): number => {\n let total = layout.paddingStart + layout.paddingEnd\n for (let index = 0; index < keys.length; index++) {\n total += heightAt(index, heights, keys, layout)\n if (index < keys.length - 1) total += layout.gap\n }\n return total\n}\n\n/** Pixel offset of a row's top edge: padding plus the prefix before it. */\nexport const offsetFor = (\n index: number,\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n): number => {\n const clamped = Math.max(0, Math.min(index, keys.length))\n let offset = layout.paddingStart\n for (let i = 0; i < clamped; i++) {\n offset += heightAt(i, heights, keys, layout) + layout.gap\n }\n return offset\n}\n\n/**\n * Which rows to render for a scroll position: the rows intersecting\n * `[scrollTop, scrollTop + viewportHeight)`, widened by `overscan` on both\n * sides and clamped to `[0, count]`. Gaps and padding are dead space — no\n * row — so boxes are tested, not cumulative coverage. An overscan of zero\n * renders exactly the visible rows.\n */\nexport const visibleRange = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n scrollTop: number,\n viewportHeight: number,\n overscan: number,\n): VirtualWindow => {\n const count = keys.length\n if (count === 0 || viewportHeight <= 0) return { start: 0, end: 0 }\n const top = Math.max(0, scrollTop)\n const bottom = top + viewportHeight\n let start = 0\n let offset = layout.paddingStart\n for (let index = 0; index < count; index++) {\n const height = heightAt(index, heights, keys, layout)\n if (offset + height <= top) {\n start = index + 1\n offset += height + layout.gap\n } else {\n break\n }\n }\n let end = start\n let cursor = offset\n while (end < count && cursor < bottom) {\n cursor += heightAt(end, heights, keys, layout) + layout.gap\n end += 1\n }\n return {\n start: Math.max(0, start - Math.max(0, overscan)),\n end: Math.min(count, end + Math.max(0, overscan)),\n }\n}\n\nexport const VirtualModel = Schema.Struct({\n scrollTop: Schema.Number,\n heights: Schema.Record(Schema.String, Schema.Number),\n scrolling: Schema.Boolean,\n generation: Schema.Number,\n estimatedHeight: Schema.Number,\n overscan: Schema.Number,\n gap: Schema.Number,\n paddingStart: Schema.Number,\n paddingEnd: Schema.Number,\n})\nexport type VirtualModel = typeof VirtualModel.Type\n\nexport const VirtualMessage = defineMessageUnion({\n Scrolled: { top: Schema.Number },\n Measured: { key: Schema.String, height: Schema.Number },\n /** Drops heights for keys no longer listed: the bundle never sees the key\n order, so the application tells it what left. */\n Prune: { keys: Schema.Array(Schema.String) },\n /** Fires when scrolling has been silent for `settleMs`: only the latest\n generation counts, so a fling's intermediate timers emit nothing. */\n Settled: { generation: Schema.Number },\n})\nexport type VirtualMessage = typeof VirtualMessage.Type\n\nconst layoutOf = (model: VirtualModel): VirtualLayout => ({\n estimatedHeight: model.estimatedHeight,\n gap: model.gap,\n paddingStart: model.paddingStart,\n paddingEnd: model.paddingEnd,\n})\n\n/**\n * The rows to render plus the spacer height, from the Model, the key order,\n * and the viewport height. Layout and overscan come from the Model, so the\n * call cannot disagree with the placement. Keys stay the application's:\n * render each row keyed, and per-row placements keep identity.\n */\nexport const windowFor = (\n model: VirtualModel,\n keys: ReadonlyArray<string>,\n viewportHeight: number,\n): VirtualWindow & { readonly totalHeight: number } => {\n const layout = layoutOf(model)\n const window = visibleRange(\n keys,\n model.heights,\n layout,\n model.scrollTop,\n viewportHeight,\n model.overscan,\n )\n return { ...window, totalHeight: totalHeight(keys, model.heights, layout) }\n}\n\n/**\n * Pixels from the viewport's bottom edge to the content's end: negative\n * past it, zero exactly there. Prefetch thresholds want this number (\"load\n * when within 500px\"), not just the boolean below.\n */\nexport const distanceToEnd = (\n model: VirtualModel,\n keys: ReadonlyArray<string>,\n viewportHeight: number,\n): number => {\n const total = totalHeight(keys, model.heights, layoutOf(model))\n return total - (model.scrollTop + viewportHeight)\n}\n\n/**\n * Whether the viewport rests at (or past) the end, within `threshold`\n * pixels: the infinite-scroll check. An empty list counts as ended — there\n * is nothing to scroll, so more should load. Thresholds below zero clamp\n * to zero.\n */\nexport const isAtEnd = (\n model: VirtualModel,\n keys: ReadonlyArray<string>,\n viewportHeight: number,\n threshold = 0,\n): boolean => {\n const total = totalHeight(keys, model.heights, layoutOf(model))\n if (total <= 0) return true\n return distanceToEnd(model, keys, viewportHeight) <= Math.max(0, threshold)\n}\n\nconst saneTop = (top: number): number | null => (Number.isFinite(top) ? Math.max(0, top) : null)\n\nconst saneMeasured = (height: number): number | null =>\n Number.isFinite(height) && height >= 0 ? height : null\n\nexport const Virtual = Bundle.make<\n 'Virtual',\n VirtualModel,\n VirtualMessage,\n {\n readonly estimatedHeight: number\n readonly overscan: number\n readonly gap: number\n readonly paddingStart: number\n readonly paddingEnd: number\n readonly settleMs?: number | undefined\n readonly initialScrollTop?: number | undefined\n readonly initialHeights?: Readonly<Record<string, number>> | undefined\n }\n>('Virtual', {\n Model: VirtualModel,\n Message: VirtualMessage,\n args: Schema.Struct({\n estimatedHeight: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n overscan: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),\n gap: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n paddingStart: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n paddingEnd: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n settleMs: Schema.optional(\n Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())),\n ),\n initialScrollTop: Schema.optional(\n Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n ),\n initialHeights: Schema.optional(Schema.Record(Schema.String, Schema.Number)),\n }),\n init: args => {\n // Restored measurements pass through the same sanitation as live ones,\n // so a poisoned cache cannot wedge the sums.\n const heights: Record<string, number> = {}\n for (const [key, height] of Object.entries(args.initialHeights ?? {})) {\n const sane = saneMeasured(height)\n if (sane !== null) heights[key] = sane\n }\n return {\n model: {\n scrollTop: args.initialScrollTop ?? 0,\n heights,\n scrolling: false,\n generation: 0,\n estimatedHeight: args.estimatedHeight,\n overscan: args.overscan,\n gap: args.gap,\n paddingStart: args.paddingStart,\n paddingEnd: args.paddingEnd,\n },\n }\n },\n update: (model, message, args) =>\n VirtualMessage.match<Update.ReturnWithOutMessage<VirtualModel, VirtualMessage, never>>(\n message,\n {\n Scrolled: ({ top }) => {\n const sane = saneTop(top)\n if (sane === null) return { model }\n const generation = model.generation + 1\n return {\n model: { ...model, scrollTop: sane, scrolling: true, generation },\n commands: [\n {\n name: 'Virtual.settle',\n args: { generation },\n effect: Effect.as(\n Effect.sleep(args.settleMs ?? 150),\n VirtualMessage.Settled({ generation }),\n ),\n },\n ],\n }\n },\n Measured: ({ key, height }) => {\n const sane = saneMeasured(height)\n return sane === null\n ? { model }\n : { model: { ...model, heights: { ...model.heights, [key]: sane } } }\n },\n Prune: ({ keys }) => {\n const kept = new Set(keys)\n const heights: Record<string, number> = {}\n for (const [key, height] of Object.entries(model.heights)) {\n if (kept.has(key)) heights[key] = height\n }\n return { model: { ...model, heights } }\n },\n // A superseded silence timer carries an old generation: ignore it.\n Settled: ({ generation }) =>\n generation === model.generation ? { model: { ...model, scrolling: false } } : { model },\n },\n ),\n})\n\nexport const ViewportScrolled = Schema.TaggedStruct('ViewportScrolled', { top: Schema.Number })\nexport type ViewportScrolled = typeof ViewportScrolled.Type\n\n/**\n * The scroll container's position, reported on its own scrolls starting\n * with the current one. Attach to the scrolling element — viewport scrolls\n * arrive through the window-level scroll entry instead.\n */\nexport const Viewport = Mount.defineStream('Viewport', {\n messages: [ViewportScrolled],\n execute: ({ element }) => {\n // The mount needs only a scroll position, not a full Element.\n const read = (): ViewportScrolled => {\n const scroller = element as unknown as { readonly scrollTop: number }\n return ViewportScrolled.make({ top: scroller.scrollTop })\n }\n return Stream.concat(\n Stream.make(read()),\n Stream.fromEventListener(element, 'scroll').pipe(Stream.map(read)),\n )\n },\n})\n\nexport const RowMeasured = Schema.TaggedStruct('RowMeasured', {\n key: Schema.String,\n height: Schema.Number,\n})\nexport type RowMeasured = typeof RowMeasured.Type\n\ntype RowObserverCtor = new (callback: ResizeObserverCallback) => ResizeObserver\n\n/**\n * One row's height, reported on every resize starting with the current\n * one. The key is the application's row identity, echoed back so the\n * parent files the height without tracking which Mount sent what.\n */\nexport const MeasureRow = Mount.defineStream('MeasureRow', {\n messages: [RowMeasured],\n args: { key: Schema.String },\n execute: ({ element, key }) =>\n Stream.callback<typeof RowMeasured.Type>(queue =>\n Effect.gen(function* () {\n const Observed = (globalThis as { ResizeObserver?: RowObserverCtor }).ResizeObserver\n if (Observed === undefined) return\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const observer = new Observed(entries => {\n const rect = entries[0]?.contentRect\n if (rect !== undefined) {\n Queue.offerUnsafe(queue, RowMeasured.make({ key, height: rect.height }))\n }\n })\n observer.observe(element)\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n )\n }),\n ),\n})\n\n/**\n * Which section header sticks for a window: the last section starting at\n * or before the first rendered row, or null above the first section. Order\n * independent — the scan takes the maximum, so unsorted input still\n * answers. CSS `position: sticky` does the sticking; this answers *what*\n * sticks, for styling and callbacks.\n */\nexport const stickyHeader = (\n sections: ReadonlyArray<{ readonly key: string; readonly index: number }>,\n startIndex: number,\n): string | null => {\n let current: string | null = null\n let currentIndex = -1\n for (const section of sections) {\n if (section.index <= startIndex && section.index > currentIndex) {\n current = section.key\n currentIndex = section.index\n }\n }\n return current\n}\n\nexport interface MasonryPlacement {\n readonly key: string\n readonly column: number\n readonly x: number\n readonly y: number\n readonly width: number\n readonly height: number\n}\n\n/**\n * Masonry layout as a pure function: fixed column count and width, each\n * item packed into the currently shortest column. Unmeasured items use the\n * estimate; poisoned heights fall back the same way. This is layout only,\n * not windowing — every placed item renders, so it fits hundreds of images,\n * not hundred-thousands. Needing a windowed grid is a different algorithm.\n */\nexport const masonry = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n options: {\n readonly columns: number\n readonly columnWidth: number\n readonly gap: number\n readonly estimatedHeight: number\n },\n): { readonly placements: ReadonlyArray<MasonryPlacement>; readonly totalHeight: number } => {\n if (!Number.isInteger(options.columns) || options.columns < 1) {\n throw new Error(`masonry: columns must be a positive integer, got ${options.columns}`)\n }\n if (!(options.columnWidth > 0) || !Number.isFinite(options.columnWidth)) {\n throw new Error(\n `masonry: columnWidth must be a positive finite number, got ${options.columnWidth}`,\n )\n }\n if (options.gap < 0 || !Number.isFinite(options.gap)) {\n throw new Error(`masonry: gap must be a non-negative finite number, got ${options.gap}`)\n }\n if (options.estimatedHeight < 0 || !Number.isFinite(options.estimatedHeight)) {\n throw new Error(\n `masonry: estimatedHeight must be a non-negative finite number, got ${options.estimatedHeight}`,\n )\n }\n const columnHeights: Array<number> = new Array(options.columns).fill(0)\n const placements: Array<MasonryPlacement> = []\n for (const key of keys) {\n const raw = heights[key] ?? options.estimatedHeight\n const height = Number.isFinite(raw) && raw >= 0 ? raw : options.estimatedHeight\n let column = 0\n for (let c = 1; c < columnHeights.length; c++) {\n if (columnHeights[c]! < columnHeights[column]!) column = c\n }\n placements.push({\n key,\n column,\n x: column * (options.columnWidth + options.gap),\n y: columnHeights[column]!,\n width: options.columnWidth,\n height,\n })\n columnHeights[column] = columnHeights[column]! + height + options.gap\n }\n return {\n placements,\n totalHeight: placements.length === 0 ? 0 : Math.max(...columnHeights) - options.gap,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/state/history.ts","../../src/state/locale.ts","../../src/state/pagination.ts","../../src/state/range.ts","../../src/state/selection.ts","../../src/state/virtual.ts"],"sourcesContent":["/**\n * Undo/redo as a bundle factory: the past, present, and future of one value\n * live in the Model, so every step replays. Pushing records, even an unchanged\n * value; skipping duplicates is the application's policy, not the stack's.\n *\n * A push may name a `group`. Consecutive pushes of the same group are one step:\n * the present moves and nothing new is recorded, so typing a word undoes as a\n * whole. Grouping needs no clock; the application names what belongs together.\n *\n * The steps are also pure functions (`History.push`, `undo`, `redo`, `goTo`, `clear`),\n * for a parent that records an edit in the same transition that makes it\n * rather than dispatching a Message to a placed bundle.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const HistoryModel = <Value>(value: Schema.Codec<Value, unknown>) =>\n Schema.Struct({\n past: Schema.Array(value),\n present: value,\n future: Schema.Array(value),\n /** The group of the last push, so the next push of the same group joins it; `null` for none. */\n group: Schema.NullOr(Schema.String),\n })\nexport type HistoryModel<Value> = {\n readonly past: ReadonlyArray<Value>\n readonly present: Value\n readonly future: ReadonlyArray<Value>\n readonly group: string | null\n}\n\nconst start = <Value>(present: Value): HistoryModel<Value> => ({\n past: [],\n present,\n future: [],\n group: null,\n})\n\n/**\n * No redo steps, keeping an already empty future: a view that reads the\n * future compares it by identity, so a push per keystroke or stroke cell\n * must not hand it a new empty array each time.\n */\nconst noFuture = <Value>(model: HistoryModel<Value>): ReadonlyArray<Value> =>\n model.future.length === 0 ? model.future : []\n\nconst push = <Value>(\n model: HistoryModel<Value>,\n value: Value,\n options: { readonly capacity: number; readonly group?: string | null | undefined },\n): HistoryModel<Value> => {\n const group = options.group ?? null\n // The same group again joins the last step: the present moves, the past stays.\n if (group !== null && group === model.group && model.past.length > 0)\n return { ...model, present: value, future: noFuture(model) }\n return {\n // slice(-0) is slice(0): a zero capacity keeps nothing, explicitly.\n past: options.capacity === 0 ? [] : [...model.past, model.present].slice(-options.capacity),\n present: value,\n future: noFuture(model),\n group,\n }\n}\n\nconst undo = <Value>(model: HistoryModel<Value>): HistoryModel<Value> => {\n const previous = model.past[model.past.length - 1]\n if (previous === undefined) return model\n return {\n past: model.past.slice(0, -1),\n present: previous,\n future: [model.present, ...model.future],\n group: null,\n }\n}\n\nconst redo = <Value>(model: HistoryModel<Value>): HistoryModel<Value> => {\n const next = model.future[0]\n if (next === undefined) return model\n return {\n past: [...model.past, model.present],\n present: next,\n future: model.future.slice(1),\n group: null,\n }\n}\n\nconst goTo = <Value>(model: HistoryModel<Value>, step: number): HistoryModel<Value> => {\n const steps = [...model.past, model.present, ...model.future]\n if (!Number.isInteger(step) || step < 0 || step >= steps.length || step === model.past.length)\n return model\n return {\n past: steps.slice(0, step),\n present: steps[step]!,\n future: steps.slice(step + 1),\n group: null,\n }\n}\n\nconst clear = <Value>(model: HistoryModel<Value>): HistoryModel<Value> =>\n model.past.length === 0 && model.future.length === 0 && model.group === null\n ? model\n : start(model.present)\n\nconst close = <Value>(model: HistoryModel<Value>): HistoryModel<Value> =>\n model.group === null ? model : { ...model, group: null }\n\nconst revert = <Value>(model: HistoryModel<Value>, group: string): HistoryModel<Value> =>\n // The group is ended even where there is nothing to take back (capacity 0).\n model.group === group ? { ...undo(model), future: [], group: null } : model\n\nconst checkCapacity = (name: string, capacity: number): number => {\n // A negative or fractional capacity would silently keep a wrong-sized past;\n // fail where it is given, naming the caller, instead.\n if (!Number.isInteger(capacity) || capacity < 0)\n throw new Error(`History \"${name}\": capacity must be a non-negative integer`)\n return capacity\n}\n\n/** Whether Undo would move: the past is non-empty. */\nexport const canUndo = <Value>(model: HistoryModel<Value>): boolean => model.past.length > 0\n\n/** Whether Redo would move: the future is non-empty. */\nexport const canRedo = <Value>(model: HistoryModel<Value>): boolean => model.future.length > 0\n\n/** The steps of a history as pure functions of its Model. */\nexport const History = {\n /** A history whose present is `value`, with nothing to undo. */\n start,\n /** Records `value` as the present; the same `group` as the last push joins its step. */\n push,\n /** One step back, or the Model as it was when there is none. */\n undo,\n /** One step forward, or the Model as it was when there is none. */\n redo,\n /**\n * The value at `step` made the present, counting every kept value from the\n * oldest (0), so the present is at `past.length`: what a click on a step's\n * thumbnail does. The Model as it was for the present or a step not kept.\n */\n goTo,\n /** Nothing to undo or redo, the present kept. */\n clear,\n /** The next push starts a step of its own, whatever its group: a group ends here. */\n close,\n /**\n * The step `group` is making taken back, and not redoable: what a cancelled\n * edit, or one that ended where it began, leaves. The Model as it was when\n * the last push was not of `group`. What the group's first push dropped\n * stays dropped: the redo future it cleared, and an entry capacity evicted;\n * with a capacity of 0 nothing was kept, so the present stays and only the\n * group ends.\n */\n revert,\n canUndo,\n canRedo,\n}\n\nexport const history = <const Name extends string, Value>(config: {\n readonly name: Name\n readonly value: Schema.Codec<Value, unknown>\n readonly capacity?: number | undefined\n}) => {\n const capacity = checkCapacity(config.name, config.capacity ?? 100)\n const Model = HistoryModel(config.value)\n const Message = defineMessageUnion({\n Push: { value: config.value, group: Schema.optional(Schema.String) },\n Undo: {},\n Redo: {},\n GoTo: { step: Schema.Number },\n Clear: {},\n })\n return Object.assign(\n Bundle.make(config.name, {\n Model,\n Message,\n args: Schema.Struct({ initial: config.value }),\n init: args => ({ model: start(args.initial) }),\n update: (model, message) =>\n Message.match(message, {\n Push: ({ value, group }) => ({ model: push(model, value, { capacity, group }) }),\n Undo: () => ({ model: undo(model) }),\n Redo: () => ({ model: redo(model) }),\n GoTo: ({ step }) => ({ model: goTo(model, step) }),\n Clear: () => ({ model: clear(model) }),\n }),\n }),\n // The union object is both the Message codec and its constructors, so a\n // placement dispatches `EditHistory.Message.Push(...)` like any bundle's\n // exported Message.\n { Message },\n )\n}\n","/**\n * Locale as a bundle: the current locale string in the Model. Starts from\n * `navigator.language` where available, falling back to the configured\n * default; changes arrive as Messages. No subscriptions: language changes\n * take effect on reload.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const LocaleModel = Schema.Struct({ locale: Schema.String })\nexport type LocaleModel = typeof LocaleModel.Type\n\nexport const LocaleMessage = defineMessageUnion({ SetLocale: { locale: Schema.String } })\nexport type LocaleMessage = typeof LocaleMessage.Type\n\nexport const Locale = Bundle.make('Locale', {\n Model: LocaleModel,\n Message: LocaleMessage,\n args: Schema.Struct({ default: Schema.String }),\n init: args => ({\n model: {\n locale:\n typeof navigator !== 'undefined' && typeof navigator.language === 'string'\n ? navigator.language\n : args.default,\n },\n }),\n update: (model, message) =>\n LocaleMessage.match(message, {\n SetLocale: ({ locale }) =>\n locale === model.locale ? { model } : { model: { ...model, locale } },\n }),\n})\n","/**\n * Pagination as a bundle: the page, page size, and total live in the Model,\n * so page changes replay and every transition clamps into range. Pure logic,\n * no streams: data loading stays the application's job.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\nimport { unlessSame } from '../internal.js'\n\nexport const PaginationModel = Schema.Struct({\n page: Schema.Number,\n perPage: Schema.Number,\n total: Schema.NullOr(Schema.Number),\n})\nexport type PaginationModel = typeof PaginationModel.Type\n\nexport const PaginationMessage = defineMessageUnion({\n GoToPage: { page: Schema.Number },\n NextPage: {},\n PrevPage: {},\n SetPerPage: { perPage: Schema.Number },\n SetTotal: { total: Schema.NullOr(Schema.Number) },\n})\nexport type PaginationMessage = typeof PaginationMessage.Type\n\nconst clampPage = (page: number, perPage: number, total: number | null): number => {\n if (!Number.isInteger(page)) return 1\n if (page < 1) return 1\n if (total === null) return page\n return Math.min(page, Math.max(1, Math.ceil(total / perPage)))\n}\n\n/** The number of pages, or null while the total is unknown. */\nexport const pageCount = (model: PaginationModel): number | null =>\n model.total === null ? null : Math.max(1, Math.ceil(model.total / model.perPage))\n\n/** The offset of the first item on the page, for a slice or a query. */\nexport const offset = (model: PaginationModel): number => (model.page - 1) * model.perPage\n\nexport const Pagination = Bundle.make('Pagination', {\n Model: PaginationModel,\n Message: PaginationMessage,\n args: Schema.Struct({\n perPage: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n total: Schema.optional(Schema.NullOr(Schema.Number.pipe(Schema.check(Schema.isFinite())))),\n }),\n init: args => ({\n model: { page: 1, perPage: args.perPage, total: args.total ?? null },\n }),\n update: (model, message) =>\n PaginationMessage.match(message, {\n GoToPage: ({ page }) => ({\n model: unlessSame(model, { ...model, page: clampPage(page, model.perPage, model.total) }),\n }),\n NextPage: () => ({\n model: unlessSame(model, {\n ...model,\n page: clampPage(model.page + 1, model.perPage, model.total),\n }),\n }),\n PrevPage: () => ({\n model: unlessSame(model, {\n ...model,\n page: clampPage(model.page - 1, model.perPage, model.total),\n }),\n }),\n SetPerPage: ({ perPage }) =>\n perPage > 0\n ? {\n model: unlessSame(model, {\n ...model,\n perPage,\n page: clampPage(model.page, perPage, model.total),\n }),\n }\n : { model },\n // Sent again on every refetch, usually with the total already held.\n SetTotal: ({ total }) =>\n total === null || total >= 0\n ? {\n model: unlessSame(model, {\n ...model,\n total,\n page: clampPage(model.page, model.perPage, total),\n }),\n }\n : { model },\n }),\n})\n","/**\n * A numeric range as an array: `range(1, 4)` is `[1, 2, 3]`, inclusive of\n * the start and exclusive of the end, like `Array.from` length math. The\n * pagination companion: `range(1, (pageCount(model) ?? 0) + 1)` is the page\n * list. A zero or non-finite step would loop forever, so it throws, naming\n * the caller — like History's capacity check.\n */\nexport const range = (start: number, end: number, step = 1): ReadonlyArray<number> => {\n if (!Number.isFinite(step) || step === 0) {\n throw new Error(`range: step must be a non-zero finite number, got ${step}`)\n }\n const values: Array<number> = []\n if (step > 0) {\n for (let value = start; value < end; value += step) {\n values.push(value)\n }\n } else {\n for (let value = start; value > end; value += step) {\n values.push(value)\n }\n }\n return values\n}\n","/**\n * A selection set as a bundle: the selected ids in the Model, changed one at\n * a time or replaced whole. Order follows first selection; toggling moves a\n * reselected id to the end. Pure logic, no streams.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\nimport { sameIds } from '../internal.js'\n\nexport const SelectionSetModel = Schema.Struct({ selected: Schema.Array(Schema.String) })\nexport type SelectionSetModel = typeof SelectionSetModel.Type\n\nexport const SelectionSetMessage = defineMessageUnion({\n Select: { id: Schema.String },\n Deselect: { id: Schema.String },\n Toggle: { id: Schema.String },\n ReplaceAll: { ids: Schema.Array(Schema.String) },\n Clear: {},\n})\nexport type SelectionSetMessage = typeof SelectionSetMessage.Type\n\nconst without = (selected: ReadonlyArray<string>, id: string): ReadonlyArray<string> =>\n selected.filter(other => other !== id)\n\n/** Whether the id is currently selected. */\nexport const isSelected = (model: SelectionSetModel, id: string): boolean =>\n model.selected.includes(id)\n\nexport const SelectionSet = Bundle.make('SelectionSet', {\n Model: SelectionSetModel,\n Message: SelectionSetMessage,\n init: () => ({ model: { selected: [] } }),\n update: (model, message) =>\n SelectionSetMessage.match(message, {\n Select: ({ id }) =>\n isSelected(model, id) ? { model } : { model: { selected: [...model.selected, id] } },\n Deselect: ({ id }) =>\n isSelected(model, id) ? { model: { selected: without(model.selected, id) } } : { model },\n Toggle: ({ id }) =>\n isSelected(model, id)\n ? { model: { selected: without(model.selected, id) } }\n : { model: { selected: [...model.selected, id] } },\n ReplaceAll: ({ ids }) => {\n const selected = [...new Set(ids)]\n return sameIds(selected, model.selected) ? { model } : { model: { selected } }\n },\n Clear: () => (model.selected.length === 0 ? { model } : { model: { selected: [] } }),\n }),\n})\n","/**\n * Windowing math for virtualized lists as pure functions: which rows to\n * render, how tall the scrollable area is, and where each row starts. Row\n * heights come from measured values with a per-row estimate fallback, so\n * unmeasured rows still occupy space and measuring later only corrects.\n * Keys are strings; heights are non-negative, and non-finite measurements\n * fall back to the estimate instead of poisoning the sums.\n *\n * The `Virtual` bundle below owns the scroll position and the measured\n * heights; `Viewport` reports container scrolls and `MeasureRow` reports\n * row heights, both as Mounts on the elements they observe. The application\n * renders `windowFor(...)` rows with its own views (keyed, so `each`\n * placements keep identity) inside a spacer of `totalHeight(...)`.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Mount from 'foldkit/mount'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport interface VirtualWindow {\n /** First visible row index, inclusive. */\n readonly start: number\n /** End row index, exclusive. */\n readonly end: number\n}\n\n/** Layout constants: per-row estimate, inter-row gap, and end padding. */\nexport interface VirtualLayout {\n readonly estimatedHeight: number\n readonly gap: number\n readonly paddingStart: number\n readonly paddingEnd: number\n}\n\nconst saneHeight = (height: number, estimatedHeight: number): number =>\n Number.isFinite(height) && height >= 0 ? height : estimatedHeight\n\nconst heightAt = (\n index: number,\n heights: Readonly<Record<string, number>>,\n keys: ReadonlyArray<string>,\n layout: VirtualLayout,\n): number => {\n const key = keys[index]\n if (key === undefined) return layout.estimatedHeight\n return saneHeight(heights[key] ?? layout.estimatedHeight, layout.estimatedHeight)\n}\n\ninterface Offsets {\n readonly heights: Readonly<Record<string, number>>\n readonly layout: VirtualLayout\n /** `tops[i]` is row i's top edge; `tops[count]` is past the last row's gap. */\n readonly tops: Float64Array\n}\n\n// One entry per keys array, replaced when the heights or the layout change: a\n// view that keeps its keys array between renders pays O(n) once per change of\n// heights, not once per call. Keys and heights are compared by identity, so an\n// array or record changed in place reads stale offsets; the Model never is.\nconst offsetsCache = new WeakMap<ReadonlyArray<string>, Offsets>()\n\nconst sameLayout = (a: VirtualLayout, b: VirtualLayout): boolean =>\n a.estimatedHeight === b.estimatedHeight &&\n a.gap === b.gap &&\n a.paddingStart === b.paddingStart &&\n a.paddingEnd === b.paddingEnd\n\nconst topsFor = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n): Float64Array => {\n const cached = offsetsCache.get(keys)\n if (cached !== undefined && cached.heights === heights && sameLayout(cached.layout, layout)) {\n return cached.tops\n }\n const tops = new Float64Array(keys.length + 1)\n tops[0] = layout.paddingStart\n for (let index = 0; index < keys.length; index++) {\n tops[index + 1] = tops[index]! + heightAt(index, heights, keys, layout) + layout.gap\n }\n offsetsCache.set(keys, { heights, layout: { ...layout }, tops })\n return tops\n}\n\n/** The first index in `[from, to)` whose value passes `test`, or `to`; `test` must be monotonic. */\nconst firstIndex = (from: number, to: number, test: (index: number) => boolean): number => {\n let low = from\n let high = to\n while (low < high) {\n const middle = (low + high) >>> 1\n if (test(middle)) high = middle\n else low = middle + 1\n }\n return low\n}\n\n/** Total scrollable height: padding, measured rows, estimates, and gaps. */\nexport const totalHeight = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n): number => {\n const last = keys.length - 1\n if (last === -1) return layout.paddingStart + layout.paddingEnd\n // The last row's bottom edge, not `tops[count]` less a gap: subtracting a\n // fractional gap back out need not land where the row ends.\n return (\n topsFor(keys, heights, layout)[last]! +\n heightAt(last, heights, keys, layout) +\n layout.paddingEnd\n )\n}\n\n/** Pixel offset of a row's top edge: padding plus the prefix before it. */\nexport const offsetFor = (\n index: number,\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n): number => topsFor(keys, heights, layout)[Math.max(0, Math.min(index, keys.length))]!\n\n/**\n * Which rows to render for a scroll position: the rows intersecting\n * `[scrollTop, scrollTop + viewportHeight)`, widened by `overscan` on both\n * sides and clamped to `[0, count]`. Gaps and padding are dead space — no\n * row — so boxes are tested, not cumulative coverage. An overscan of zero\n * renders exactly the visible rows.\n */\nexport const visibleRange = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n scrollTop: number,\n viewportHeight: number,\n overscan: number,\n): VirtualWindow => {\n const count = keys.length\n if (count === 0 || viewportHeight <= 0) return { start: 0, end: 0 }\n const tops = topsFor(keys, heights, layout)\n const top = Math.max(0, scrollTop)\n const bottom = top + viewportHeight\n // Row bottoms and tops only grow with the index, so both edges are searches.\n const start = firstIndex(\n 0,\n count,\n index => tops[index]! + heightAt(index, heights, keys, layout) > top,\n )\n const end = firstIndex(start, count, index => tops[index]! >= bottom)\n return {\n start: Math.max(0, start - Math.max(0, overscan)),\n end: Math.min(count, end + Math.max(0, overscan)),\n }\n}\n\nexport const VirtualModel = Schema.Struct({\n scrollTop: Schema.Number,\n heights: Schema.Record(Schema.String, Schema.Number),\n scrolling: Schema.Boolean,\n generation: Schema.Number,\n estimatedHeight: Schema.Number,\n overscan: Schema.Number,\n gap: Schema.Number,\n paddingStart: Schema.Number,\n paddingEnd: Schema.Number,\n})\nexport type VirtualModel = typeof VirtualModel.Type\n\nexport const VirtualMessage = defineMessageUnion({\n Scrolled: { top: Schema.Number },\n Measured: { key: Schema.String, height: Schema.Number },\n /** Drops heights for keys no longer listed: the bundle never sees the key\n order, so the application tells it what left. */\n Prune: { keys: Schema.Array(Schema.String) },\n /** Fires when scrolling has been silent for `settleMs`: only the latest\n generation counts, so a fling's intermediate timers emit nothing. */\n Settled: { generation: Schema.Number },\n})\nexport type VirtualMessage = typeof VirtualMessage.Type\n\nconst layoutOf = (model: VirtualModel): VirtualLayout => ({\n estimatedHeight: model.estimatedHeight,\n gap: model.gap,\n paddingStart: model.paddingStart,\n paddingEnd: model.paddingEnd,\n})\n\n/**\n * The rows to render plus the spacer height, from the Model, the key order,\n * and the viewport height. Layout and overscan come from the Model, so the\n * call cannot disagree with the placement. Keys stay the application's:\n * render each row keyed, and per-row placements keep identity.\n */\nexport const windowFor = (\n model: VirtualModel,\n keys: ReadonlyArray<string>,\n viewportHeight: number,\n): VirtualWindow & { readonly totalHeight: number } => {\n const layout = layoutOf(model)\n const window = visibleRange(\n keys,\n model.heights,\n layout,\n model.scrollTop,\n viewportHeight,\n model.overscan,\n )\n return { ...window, totalHeight: totalHeight(keys, model.heights, layout) }\n}\n\n/**\n * Pixels from the viewport's bottom edge to the content's end: negative\n * past it, zero exactly there. Prefetch thresholds want this number (\"load\n * when within 500px\"), not just the boolean below.\n */\nexport const distanceToEnd = (\n model: VirtualModel,\n keys: ReadonlyArray<string>,\n viewportHeight: number,\n): number => {\n const total = totalHeight(keys, model.heights, layoutOf(model))\n return total - (model.scrollTop + viewportHeight)\n}\n\n/**\n * Whether the viewport rests at (or past) the end, within `threshold`\n * pixels: the infinite-scroll check. An empty list counts as ended — there\n * is nothing to scroll, so more should load. Thresholds below zero clamp\n * to zero.\n */\nexport const isAtEnd = (\n model: VirtualModel,\n keys: ReadonlyArray<string>,\n viewportHeight: number,\n threshold = 0,\n): boolean => {\n const total = totalHeight(keys, model.heights, layoutOf(model))\n if (total <= 0) return true\n return total - (model.scrollTop + viewportHeight) <= Math.max(0, threshold)\n}\n\nconst saneTop = (top: number): number | null => (Number.isFinite(top) ? Math.max(0, top) : null)\n\nconst saneMeasured = (height: number): number | null =>\n Number.isFinite(height) && height >= 0 ? height : null\n\nexport const Virtual = Bundle.make<\n 'Virtual',\n VirtualModel,\n VirtualMessage,\n {\n readonly estimatedHeight: number\n readonly overscan: number\n readonly gap: number\n readonly paddingStart: number\n readonly paddingEnd: number\n readonly settleMs?: number | undefined\n readonly initialScrollTop?: number | undefined\n readonly initialHeights?: Readonly<Record<string, number>> | undefined\n }\n>('Virtual', {\n Model: VirtualModel,\n Message: VirtualMessage,\n args: Schema.Struct({\n estimatedHeight: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n overscan: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),\n gap: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n paddingStart: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n paddingEnd: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n settleMs: Schema.optional(\n Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())),\n ),\n initialScrollTop: Schema.optional(\n Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n ),\n initialHeights: Schema.optional(Schema.Record(Schema.String, Schema.Number)),\n }),\n init: args => {\n // Restored measurements pass through the same sanitation as live ones,\n // so a poisoned cache cannot wedge the sums.\n const heights: Record<string, number> = {}\n for (const [key, height] of Object.entries(args.initialHeights ?? {})) {\n const sane = saneMeasured(height)\n if (sane !== null) heights[key] = sane\n }\n return {\n model: {\n scrollTop: args.initialScrollTop ?? 0,\n heights,\n scrolling: false,\n generation: 0,\n estimatedHeight: args.estimatedHeight,\n overscan: args.overscan,\n gap: args.gap,\n paddingStart: args.paddingStart,\n paddingEnd: args.paddingEnd,\n },\n }\n },\n update: (model, message, args) =>\n VirtualMessage.match<Update.ReturnWithOutMessage<VirtualModel, VirtualMessage, never>>(\n message,\n {\n Scrolled: ({ top }) => {\n const sane = saneTop(top)\n // The Viewport reports the position it mounts at, and a horizontal\n // scroll reports an unchanged top. The raw top is compared, so an\n // overscroll past zero still counts as scrolling.\n if (sane === null || top === model.scrollTop) return { model }\n const generation = model.generation + 1\n return {\n model: { ...model, scrollTop: sane, scrolling: true, generation },\n commands: [\n {\n name: 'Virtual.settle',\n args: { generation },\n effect: Effect.as(\n Effect.sleep(args.settleMs ?? 150),\n VirtualMessage.Settled({ generation }),\n ),\n },\n ],\n }\n },\n Measured: ({ key, height }) => {\n const sane = saneMeasured(height)\n // A remounted row, or a width-only resize, reports the height held.\n return sane === null || (Object.hasOwn(model.heights, key) && model.heights[key] === sane)\n ? { model }\n : { model: { ...model, heights: { ...model.heights, [key]: sane } } }\n },\n Prune: ({ keys }) => {\n const kept = new Set(keys)\n const heights: Record<string, number> = {}\n let dropped = false\n for (const [key, height] of Object.entries(model.heights)) {\n if (kept.has(key)) heights[key] = height\n else dropped = true\n }\n return dropped ? { model: { ...model, heights } } : { model }\n },\n // A superseded silence timer carries an old generation: ignore it.\n Settled: ({ generation }) =>\n generation === model.generation ? { model: { ...model, scrolling: false } } : { model },\n },\n ),\n})\n\nexport const ViewportScrolled = Schema.TaggedStruct('ViewportScrolled', { top: Schema.Number })\nexport type ViewportScrolled = typeof ViewportScrolled.Type\n\n/**\n * The scroll container's position, reported on its own scrolls starting\n * with the current one. Attach to the scrolling element — viewport scrolls\n * arrive through the window-level scroll entry instead.\n */\nexport const Viewport = Mount.defineStream('Viewport', {\n messages: [ViewportScrolled],\n execute: ({ element }) => {\n // The mount needs only a scroll position, not a full Element.\n const read = (): ViewportScrolled => {\n const scroller = element as unknown as { readonly scrollTop: number }\n return ViewportScrolled.make({ top: scroller.scrollTop })\n }\n return Stream.concat(\n Stream.make(read()),\n Stream.fromEventListener(element, 'scroll').pipe(Stream.map(read)),\n )\n },\n})\n\nexport const RowMeasured = Schema.TaggedStruct('RowMeasured', {\n key: Schema.String,\n height: Schema.Number,\n})\nexport type RowMeasured = typeof RowMeasured.Type\n\ntype RowObserverCtor = new (callback: ResizeObserverCallback) => ResizeObserver\n\n/**\n * One row's height, reported on every resize starting with the current\n * one. The key is the application's row identity, echoed back so the\n * parent files the height without tracking which Mount sent what.\n */\nexport const MeasureRow = Mount.defineStream('MeasureRow', {\n messages: [RowMeasured],\n args: { key: Schema.String },\n execute: ({ element, key }) =>\n Stream.callback<typeof RowMeasured.Type>(queue =>\n Effect.gen(function* () {\n const Observed = (globalThis as { ResizeObserver?: RowObserverCtor }).ResizeObserver\n if (Observed === undefined) return\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const observer = new Observed(entries => {\n const rect = entries[0]?.contentRect\n if (rect !== undefined) {\n Queue.offerUnsafe(queue, RowMeasured.make({ key, height: rect.height }))\n }\n })\n observer.observe(element)\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n )\n }),\n ),\n})\n\n/**\n * Which section header sticks for a window: the last section starting at\n * or before the first rendered row, or null above the first section. Order\n * independent — the scan takes the maximum, so unsorted input still\n * answers. CSS `position: sticky` does the sticking; this answers *what*\n * sticks, for styling and callbacks.\n */\nexport const stickyHeader = (\n sections: ReadonlyArray<{ readonly key: string; readonly index: number }>,\n startIndex: number,\n): string | null => {\n let current: string | null = null\n let currentIndex = -1\n for (const section of sections) {\n if (section.index <= startIndex && section.index > currentIndex) {\n current = section.key\n currentIndex = section.index\n }\n }\n return current\n}\n\nexport interface MasonryPlacement {\n readonly key: string\n readonly column: number\n readonly x: number\n readonly y: number\n readonly width: number\n readonly height: number\n}\n\n/**\n * Masonry layout as a pure function: fixed column count and width, each\n * item packed into the currently shortest column. Unmeasured items use the\n * estimate; poisoned heights fall back the same way. This is layout only,\n * not windowing — every placed item renders, so it fits hundreds of images,\n * not hundred-thousands. Needing a windowed grid is a different algorithm.\n */\nexport const masonry = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n options: {\n readonly columns: number\n readonly columnWidth: number\n readonly gap: number\n readonly estimatedHeight: number\n },\n): { readonly placements: ReadonlyArray<MasonryPlacement>; readonly totalHeight: number } => {\n if (!Number.isInteger(options.columns) || options.columns < 1) {\n throw new Error(`masonry: columns must be a positive integer, got ${options.columns}`)\n }\n if (!(options.columnWidth > 0) || !Number.isFinite(options.columnWidth)) {\n throw new Error(\n `masonry: columnWidth must be a positive finite number, got ${options.columnWidth}`,\n )\n }\n if (options.gap < 0 || !Number.isFinite(options.gap)) {\n throw new Error(`masonry: gap must be a non-negative finite number, got ${options.gap}`)\n }\n if (options.estimatedHeight < 0 || !Number.isFinite(options.estimatedHeight)) {\n throw new Error(\n `masonry: estimatedHeight must be a non-negative finite number, got ${options.estimatedHeight}`,\n )\n }\n const columnHeights: Array<number> = new Array(options.columns).fill(0)\n const placements: Array<MasonryPlacement> = []\n for (const key of keys) {\n const raw = heights[key] ?? options.estimatedHeight\n const height = Number.isFinite(raw) && raw >= 0 ? raw : options.estimatedHeight\n let column = 0\n for (let c = 1; c < columnHeights.length; c++) {\n if (columnHeights[c]! < columnHeights[column]!) column = c\n }\n placements.push({\n key,\n column,\n x: column * (options.columnWidth + options.gap),\n y: columnHeights[column]!,\n width: options.columnWidth,\n height,\n })\n columnHeights[column] = columnHeights[column]! + height + options.gap\n }\n return {\n placements,\n totalHeight: placements.length === 0 ? 0 : Math.max(...columnHeights) - options.gap,\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { Schema } from "effect";
1
+ import { Option, Schema } from "effect";
2
+ import * as Subscription from "foldkit/subscription";
2
3
  import { Bundle } from "foldkit-bundle";
3
4
  //#region src/time/debounce.d.ts
4
5
  export declare const debounce: <const Name extends string, Value>(config: {
@@ -101,6 +102,25 @@ export declare const Throttle: Bundle.Bundle<"Throttle", {
101
102
  readonly at: number;
102
103
  }, never, never, void, {}, {}>;
103
104
  //#endregion
105
+ //#region src/time/ticks.d.ts
106
+ declare const TickDependencies: {
107
+ intervalMs: Schema.Option<Schema.Number>;
108
+ };
109
+ type TickDependencies = {
110
+ readonly intervalMs: Option.Option<number>;
111
+ };
112
+ /**
113
+ * Ticks while `intervalMs` answers `Some`, each tick one interval after the
114
+ * last and the first one interval after starting. A changed interval applies
115
+ * from the next tick, with no restart; other Model changes do nothing.
116
+ */
117
+ export declare const ticks: <Model, Message>(config: {
118
+ /** The time between ticks for this Model, in milliseconds; `None` stops the clock. */
119
+ readonly intervalMs: (model: Model) => Option.Option<number>;
120
+ /** The Message a tick sends, given its time on Effect's clock. */
121
+ readonly onTick: (at: number) => Message;
122
+ }) => Subscription.Subscription<Model, Message, TickDependencies>;
123
+ //#endregion
104
124
  //#region src/time/timer.d.ts
105
125
  export declare const TimerModel: Schema.Struct<{
106
126
  readonly count: Schema.Number;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/time/debounce.ts","../../src/time/interval.ts","../../src/time/relative.ts","../../src/time/throttle.ts","../../src/time/timer.ts"],"mappings":";;;qBAaa,iBAAkB,qBAAqB,OAAO;WAChD,MAAM;WACN,OAAO,OAAO,MAAM;MAC9B,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCJY,eAAa,OAAA;;;;YAId,uBAAuB,cAAc;qBAEpC,2CAAe;;;;iBAAf,OAAA;;;YAKD,yBAAyB,gBAAgB;qBAExC,UAAQ,OAAA;;;;;;;;;;;;;;;;;;;;;qBCnBR,qBAAsB,MAAM,MAAM,IAAI,MAAM;;;qBCM5C,eAAa,OAAA;;;YACd,uBAAuB,cAAc;qBAEpC,2CAAe;;;kBAAf,OAAA;;;YAID,yBAAyB,gBAAgB;qBAExC,WAAS,OAAA;;;YACV,mBAAmB,UAAU;qBAE5B,UAAQ,OAAA;;;;;;;;;;;;;;;qBCdR,YAAU,OAAA;;;;YACX,oBAAoB,WAAW;qBAE9B,wCAAY;;;;;YAKb,sBAAsB,aAAa;qBAElC,OAAK,OAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/time/debounce.ts","../../src/time/interval.ts","../../src/time/relative.ts","../../src/time/throttle.ts","../../src/time/ticks.ts","../../src/time/timer.ts"],"mappings":";;;;qBAaa,iBAAkB,qBAAqB,OAAO;WAChD,MAAM;WACN,OAAO,OAAO,MAAM;MAC9B,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCFY,eAAa,OAAA;;;;YAId,uBAAuB,cAAc;qBAEpC,2CAAe;;;;iBAAf,OAAA;;;YAKD,yBAAyB,gBAAgB;qBAExC,UAAQ,OAAA;;;;;;;;;;;;;;;;;;;;;qBCrBR,qBAAsB,MAAM,MAAM,IAAI,MAAM;;;qBCM5C,eAAa,OAAA;;;YACd,uBAAuB,cAAc;qBAEpC,2CAAe;;;kBAAf,OAAA;;;YAID,yBAAyB,gBAAgB;qBAExC,WAAS,OAAA;;;YACV,mBAAmB,UAAU;qBAE5B,UAAQ,OAAA;;;;;;;;;;;;;;;cCff;;;KACD;WAA8B,YAAY,OAAO;;;;;;;qBAezC,QAAS,OAAO,SAAS;;WAE3B,aAAa,OAAO,UAAU,OAAO;;WAErC,SAAS,eAAe;MAC/B,aAAa,aAAa,OAAO,SAAS;;;qBCnBjC,YAAU,OAAA;;;;YACX,oBAAoB,WAAW;qBAE9B,wCAAY;;;;;YAKb,sBAAsB,aAAa;qBAElC,OAAK,OAAA"}
@@ -1,4 +1,4 @@
1
- import { Clock, Effect, Schema, Stream } from "effect";
1
+ import { Clock, Effect, Option, Schema, Stream } from "effect";
2
2
  import { defineMessageUnion } from "foldkit/message";
3
3
  import * as Subscription from "foldkit/subscription";
4
4
  import { Bundle } from "foldkit-bundle";
@@ -62,12 +62,40 @@ const debounce = (config) => {
62
62
  });
63
63
  };
64
64
  //#endregion
65
+ //#region src/time/ticks.ts
66
+ /**
67
+ * A clock the parent owns, as an entry: whether it ticks and how fast are
68
+ * functions of the parent's Model, so nothing keeps a second running flag or
69
+ * interval beside the state they derive from (a game's phase and score).
70
+ * Timer and Interval are built on it, for a clock that owns its own flag.
71
+ */
72
+ const TickDependencies = { intervalMs: Schema.Option(Schema.Number) };
73
+ const checked = (intervalMs) => {
74
+ if (Option.isSome(intervalMs) && !(intervalMs.value > 0 && Number.isFinite(intervalMs.value))) throw new Error(`ticks: intervalMs must be positive and finite, got ${intervalMs.value}`);
75
+ return intervalMs;
76
+ };
77
+ /**
78
+ * Ticks while `intervalMs` answers `Some`, each tick one interval after the
79
+ * last and the first one interval after starting. A changed interval applies
80
+ * from the next tick, with no restart; other Model changes do nothing.
81
+ */
82
+ const ticks = (config) => Subscription.make()((entry) => ({ ticks: entry(TickDependencies, {
83
+ modelToDependencies: (model) => ({ intervalMs: checked(config.intervalMs(model)) }),
84
+ keepAliveEquivalence: (a, b) => Option.isSome(a.intervalMs) === Option.isSome(b.intervalMs),
85
+ dependenciesToStream: ({ intervalMs }, readDependencies) => Option.match(intervalMs, {
86
+ onNone: () => Stream.empty,
87
+ onSome: (started) => Stream.fromEffectRepeat(Effect.suspend(() => Effect.sleep(Option.getOrElse(readDependencies().intervalMs, () => started))).pipe(Effect.andThen(Clock.currentTimeMillis), Effect.map(config.onTick)))
88
+ })
89
+ }) })).ticks;
90
+ //#endregion
65
91
  //#region src/time/interval.ts
66
92
  /**
67
93
  * A wall-clock interval as a bundle: the last tick time in the Model,
68
94
  * advanced by a tick stream while running. Sibling of Timer (which counts
69
95
  * ticks); this one records *when* they happened, so views can render clocks
70
96
  * and elapsed times. The clock is Effect's, so tests drive it with TestClock.
97
+ * For a clock whose running or interval the parent's Model decides, use the
98
+ * `ticks` entry instead.
71
99
  */
72
100
  const IntervalModel = Schema.Struct({
73
101
  running: Schema.Boolean,
@@ -87,23 +115,23 @@ const Interval = Bundle.make("Interval", {
87
115
  lastAt: null
88
116
  } }),
89
117
  update: (model, message) => IntervalMessage.match(message, {
90
- Started: () => ({ model: {
118
+ Started: () => model.running ? { model } : { model: {
91
119
  ...model,
92
120
  running: true
93
- } }),
94
- Stopped: () => ({ model: {
121
+ } },
122
+ Stopped: () => model.running ? { model: {
95
123
  ...model,
96
124
  running: false
97
- } }),
125
+ } } : { model },
98
126
  Ticked: ({ at }) => ({ model: {
99
127
  ...model,
100
128
  lastAt: at
101
129
  } })
102
130
  }),
103
- subscriptions: ({ intervalMs }) => Subscription.make()((entry) => ({ ticks: entry({ running: Schema.Boolean }, {
104
- modelToDependencies: (model) => ({ running: model.running }),
105
- dependenciesToStream: ({ running }) => running ? Stream.mapEffect(Stream.tick(intervalMs), () => Clock.currentTimeMillis).pipe(Stream.map((at) => IntervalMessage.Ticked({ at }))) : Stream.empty
106
- }) }))
131
+ subscriptions: ({ intervalMs }) => ({ ticks: ticks({
132
+ intervalMs: (model) => model.running ? Option.some(intervalMs) : Option.none(),
133
+ onTick: (at) => IntervalMessage.Ticked({ at })
134
+ }) })
107
135
  });
108
136
  //#endregion
109
137
  //#region src/time/relative.ts
@@ -169,7 +197,8 @@ const Throttle = Bundle.make("Throttle", {
169
197
  /**
170
198
  * A timer as a bundle: a count in the Model, advanced by a tick stream while
171
199
  * running. The clock is Effect's, so tests drive it with TestClock instead of
172
- * waiting.
200
+ * waiting. For a clock whose running or interval the parent's Model decides,
201
+ * use the `ticks` entry instead.
173
202
  */
174
203
  const TimerModel = Schema.Struct({
175
204
  count: Schema.Number,
@@ -189,25 +218,25 @@ const Timer = Bundle.make("Timer", {
189
218
  running: false
190
219
  } }),
191
220
  update: (model, message) => TimerMessage.match(message, {
192
- Started: () => ({ model: {
221
+ Started: () => model.running ? { model } : { model: {
193
222
  ...model,
194
223
  running: true
195
- } }),
196
- Stopped: () => ({ model: {
224
+ } },
225
+ Stopped: () => model.running ? { model: {
197
226
  ...model,
198
227
  running: false
199
- } }),
228
+ } } : { model },
200
229
  Ticked: () => ({ model: {
201
230
  ...model,
202
231
  count: model.count + 1
203
232
  } })
204
233
  }),
205
- subscriptions: ({ intervalMs }) => Subscription.make()((entry) => ({ ticks: entry({ running: Schema.Boolean }, {
206
- modelToDependencies: (model) => ({ running: model.running }),
207
- dependenciesToStream: ({ running }) => running ? Stream.map(Stream.tick(intervalMs), () => TimerMessage.Ticked()) : Stream.empty
208
- }) }))
234
+ subscriptions: ({ intervalMs }) => ({ ticks: ticks({
235
+ intervalMs: (model) => model.running ? Option.some(intervalMs) : Option.none(),
236
+ onTick: () => TimerMessage.Ticked()
237
+ }) })
209
238
  });
210
239
  //#endregion
211
- export { Interval, IntervalMessage, IntervalModel, Throttle, ThrottleMessage, ThrottleModel, Throttled, Timer, TimerMessage, TimerModel, debounce, formatRelativeTime };
240
+ export { Interval, IntervalMessage, IntervalModel, Throttle, ThrottleMessage, ThrottleModel, Throttled, Timer, TimerMessage, TimerModel, debounce, formatRelativeTime, ticks };
212
241
 
213
242
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/time/debounce.ts","../../src/time/interval.ts","../../src/time/relative.ts","../../src/time/throttle.ts","../../src/time/timer.ts"],"sourcesContent":["/**\n * Debounced input as a bundle factory: `Changed` restarts a timer, and the\n * latest value settles as an OutMessage the placement must handle with\n * `onOut`. Each `Changed` bumps a generation carried by the `Settled` fact\n * it schedules, so a superseded timer's fact emits nothing — the same guard\n * as Presence's exit timeout. Generic over the value Schema, like History.\n * The clock is Effect's, so tests drive it with TestClock.\n */\nimport { Effect, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const debounce = <const Name extends string, Value>(config: {\n readonly name: Name\n readonly value: Schema.Codec<Value, unknown>\n}) => {\n const Model = Schema.Struct({\n latest: Schema.NullOr(config.value),\n generation: Schema.Number,\n })\n type Model = typeof Model.Type\n\n const Message = defineMessageUnion({\n Changed: { value: config.value },\n Settled: { value: config.value, generation: Schema.Number },\n })\n type Message = typeof Message.Type\n\n const Debounced = Schema.TaggedStruct('Debounced', { value: config.value })\n type Debounced = typeof Debounced.Type\n\n const bundle = Bundle.make(config.name, {\n Model,\n Message,\n args: Schema.Struct({\n delayMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { latest: null, generation: 0 } }),\n update: (model, message, args) =>\n Message.match<Update.ReturnWithOutMessage<Model, Message, Debounced>>(message, {\n Changed: ({ value }) => {\n const generation = model.generation + 1\n return {\n model: { latest: value, generation },\n commands: [\n {\n name: `${config.name}.settle`,\n args: { generation },\n effect: Effect.as(\n Effect.sleep(args.delayMs),\n Message.Settled({ value, generation }),\n ),\n },\n ],\n }\n },\n // A superseded timer's fact arrives with an old generation: no\n // OutMessage, so the placement never sees it.\n Settled: ({ generation, value }) =>\n generation === model.generation\n ? { model, outMessage: Debounced.make({ value }) }\n : { model },\n }),\n })\n\n return Object.assign(bundle, { Message, Debounced })\n}\n","/**\n * A wall-clock interval as a bundle: the last tick time in the Model,\n * advanced by a tick stream while running. Sibling of Timer (which counts\n * ticks); this one records *when* they happened, so views can render clocks\n * and elapsed times. The clock is Effect's, so tests drive it with TestClock.\n */\nimport { Clock, 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 IntervalModel = Schema.Struct({\n running: Schema.Boolean,\n lastAt: Schema.NullOr(Schema.Number),\n})\nexport type IntervalModel = typeof IntervalModel.Type\n\nexport const IntervalMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Ticked: { at: Schema.Number },\n})\nexport type IntervalMessage = typeof IntervalMessage.Type\n\nexport const Interval = Bundle.make('Interval', {\n Model: IntervalModel,\n Message: IntervalMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { running: false, lastAt: null } }),\n update: (model, message) =>\n IntervalMessage.match<Update.ReturnWithOutMessage<IntervalModel, IntervalMessage, never>>(\n message,\n {\n Started: () => ({ model: { ...model, running: true } }),\n Stopped: () => ({ model: { ...model, running: false } }),\n Ticked: ({ at }) => ({ model: { ...model, lastAt: at } }),\n },\n ),\n subscriptions: ({ intervalMs }): Subscription.Subscriptions<IntervalModel, IntervalMessage> =>\n Subscription.make<IntervalModel, IntervalMessage>()(entry => ({\n ticks: entry(\n { running: Schema.Boolean },\n {\n modelToDependencies: model => ({ running: model.running }),\n dependenciesToStream: ({ running }) =>\n running\n ? Stream.mapEffect(Stream.tick(intervalMs), () => Clock.currentTimeMillis).pipe(\n Stream.map(at => IntervalMessage.Ticked({ at })),\n )\n : Stream.empty,\n },\n ),\n })),\n})\n","/**\n * Relative time as a pure function: picks the unit (seconds through years)\n * and delegates the wording to `Intl.RelativeTimeFormat`, so locales come\n * from the platform, not a phrase table. `from` is the earlier moment;\n * swap the arguments for future phrasing (\"in 3 days\").\n */\nexport const formatRelativeTime = (from: Date, to: Date, locale = 'en'): string => {\n const format = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n const seconds = Math.round((to.getTime() - from.getTime()) / 1000)\n const abs = Math.abs(seconds)\n if (abs < 60) return format.format(-seconds, 'second')\n const minutes = Math.round(seconds / 60)\n if (abs < 60 * 60) return format.format(-minutes, 'minute')\n const hours = Math.round(seconds / (60 * 60))\n if (abs < 24 * 60 * 60) return format.format(-hours, 'hour')\n const days = Math.round(seconds / (24 * 60 * 60))\n if (abs < 7 * 24 * 60 * 60) return format.format(-days, 'day')\n const weeks = Math.round(seconds / (7 * 24 * 60 * 60))\n if (abs < 30 * 24 * 60 * 60) return format.format(-weeks, 'week')\n const months = Math.round(seconds / (30 * 24 * 60 * 60))\n if (abs < 365 * 24 * 60 * 60) return format.format(-months, 'month')\n return format.format(-Math.round(seconds / (365 * 24 * 60 * 60)), 'year')\n}\n","/**\n * Leading-edge throttle as a bundle: the first `Attempted` in an interval\n * surfaces a `Throttled` OutMessage, the rest are dropped. `Attempted`\n * reads the clock through a Command, so the check runs against Effect time\n * and tests drive it with TestClock. Trailing-edge needs are Debounce's:\n * pair the two rather than adding a second timer here.\n */\nimport { Clock, Effect, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const ThrottleModel = Schema.Struct({ lastAt: Schema.NullOr(Schema.Number) })\nexport type ThrottleModel = typeof ThrottleModel.Type\n\nexport const ThrottleMessage = defineMessageUnion({\n Attempted: {},\n Evaluated: { now: Schema.Number },\n})\nexport type ThrottleMessage = typeof ThrottleMessage.Type\n\nexport const Throttled = Schema.TaggedStruct('Throttled', { at: Schema.Number })\nexport type Throttled = typeof Throttled.Type\n\nexport const Throttle = Bundle.make('Throttle', {\n Model: ThrottleModel,\n Message: ThrottleMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { lastAt: null } }),\n update: (model, message, args) =>\n ThrottleMessage.match<Update.ReturnWithOutMessage<ThrottleModel, ThrottleMessage, Throttled>>(\n message,\n {\n Attempted: () => ({\n model,\n commands: [\n {\n name: 'Throttle.evaluate',\n effect: Effect.map(Clock.currentTimeMillis, now =>\n ThrottleMessage.Evaluated({ now }),\n ),\n },\n ],\n }),\n // First attempt ever, or the interval has passed: fire and stamp.\n Evaluated: ({ now }) =>\n model.lastAt === null || now - model.lastAt >= args.intervalMs\n ? { model: { lastAt: now }, outMessage: Throttled.make({ at: now }) }\n : { model },\n },\n ),\n})\n","/**\n * A timer as a bundle: a count in the Model, advanced by a tick stream while\n * running. The clock is Effect's, so tests drive it with TestClock instead of\n * waiting.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const TimerModel = Schema.Struct({ count: Schema.Number, running: Schema.Boolean })\nexport type TimerModel = typeof TimerModel.Type\n\nexport const TimerMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Ticked: {},\n})\nexport type TimerMessage = typeof TimerMessage.Type\n\nexport const Timer = Bundle.make('Timer', {\n Model: TimerModel,\n Message: TimerMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { count: 0, running: false } }),\n update: (model, message) =>\n TimerMessage.match(message, {\n Started: () => ({ model: { ...model, running: true } }),\n Stopped: () => ({ model: { ...model, running: false } }),\n Ticked: () => ({ model: { ...model, count: model.count + 1 } }),\n }),\n subscriptions: ({ intervalMs }): Subscription.Subscriptions<TimerModel, TimerMessage> =>\n Subscription.make<TimerModel, TimerMessage>()(entry => ({\n ticks: entry(\n { running: Schema.Boolean },\n {\n modelToDependencies: model => ({ running: model.running }),\n dependenciesToStream: ({ running }) =>\n running\n ? Stream.map(Stream.tick(intervalMs), () => TimerMessage.Ticked())\n : Stream.empty,\n },\n ),\n })),\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/time/debounce.ts","../../src/time/ticks.ts","../../src/time/interval.ts","../../src/time/relative.ts","../../src/time/throttle.ts","../../src/time/timer.ts"],"sourcesContent":["/**\n * Debounced input as a bundle factory: `Changed` restarts a timer, and the\n * latest value settles as an OutMessage the placement must handle with\n * `onOut`. Each `Changed` bumps a generation carried by the `Settled` fact\n * it schedules, so a superseded timer's fact emits nothing — the same guard\n * as Presence's exit timeout. Generic over the value Schema, like History.\n * The clock is Effect's, so tests drive it with TestClock.\n */\nimport { Effect, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const debounce = <const Name extends string, Value>(config: {\n readonly name: Name\n readonly value: Schema.Codec<Value, unknown>\n}) => {\n const Model = Schema.Struct({\n latest: Schema.NullOr(config.value),\n generation: Schema.Number,\n })\n type Model = typeof Model.Type\n\n const Message = defineMessageUnion({\n Changed: { value: config.value },\n Settled: { value: config.value, generation: Schema.Number },\n })\n type Message = typeof Message.Type\n\n const Debounced = Schema.TaggedStruct('Debounced', { value: config.value })\n type Debounced = typeof Debounced.Type\n\n const bundle = Bundle.make(config.name, {\n Model,\n Message,\n args: Schema.Struct({\n delayMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { latest: null, generation: 0 } }),\n update: (model, message, args) =>\n Message.match<Update.ReturnWithOutMessage<Model, Message, Debounced>>(message, {\n Changed: ({ value }) => {\n const generation = model.generation + 1\n return {\n model: { latest: value, generation },\n commands: [\n {\n name: `${config.name}.settle`,\n args: { generation },\n effect: Effect.as(\n Effect.sleep(args.delayMs),\n Message.Settled({ value, generation }),\n ),\n },\n ],\n }\n },\n // A superseded timer's fact arrives with an old generation: no\n // OutMessage, so the placement never sees it.\n Settled: ({ generation, value }) =>\n generation === model.generation\n ? { model, outMessage: Debounced.make({ value }) }\n : { model },\n }),\n })\n\n return Object.assign(bundle, { Message, Debounced })\n}\n","/**\n * A clock the parent owns, as an entry: whether it ticks and how fast are\n * functions of the parent's Model, so nothing keeps a second running flag or\n * interval beside the state they derive from (a game's phase and score).\n * Timer and Interval are built on it, for a clock that owns its own flag.\n */\nimport { Clock, Effect, Option, Schema, Stream } from 'effect'\nimport * as Subscription from 'foldkit/subscription'\n\nconst TickDependencies = { intervalMs: Schema.Option(Schema.Number) }\ntype TickDependencies = { readonly intervalMs: Option.Option<number> }\n\nconst checked = (intervalMs: Option.Option<number>): Option.Option<number> => {\n // A zero, negative or non-finite interval would spin or never tick; the\n // runtime crashes on a defect here, which names the mistake where it is made.\n if (Option.isSome(intervalMs) && !(intervalMs.value > 0 && Number.isFinite(intervalMs.value)))\n throw new Error(`ticks: intervalMs must be positive and finite, got ${intervalMs.value}`)\n return intervalMs\n}\n\n/**\n * Ticks while `intervalMs` answers `Some`, each tick one interval after the\n * last and the first one interval after starting. A changed interval applies\n * from the next tick, with no restart; other Model changes do nothing.\n */\nexport const ticks = <Model, Message>(config: {\n /** The time between ticks for this Model, in milliseconds; `None` stops the clock. */\n readonly intervalMs: (model: Model) => Option.Option<number>\n /** The Message a tick sends, given its time on Effect's clock. */\n readonly onTick: (at: number) => Message\n}): Subscription.Subscription<Model, Message, TickDependencies> =>\n Subscription.make<Model, Message>()(entry => ({\n ticks: entry(TickDependencies, {\n modelToDependencies: model => ({ intervalMs: checked(config.intervalMs(model)) }),\n // Stopping and starting restart the stream; a new interval does not, so\n // a clock that speeds up keeps its phase instead of ticking at once.\n keepAliveEquivalence: (a, b) => Option.isSome(a.intervalMs) === Option.isSome(b.intervalMs),\n dependenciesToStream: ({ intervalMs }, readDependencies) =>\n Option.match(intervalMs, {\n onNone: () => Stream.empty,\n onSome: started =>\n Stream.fromEffectRepeat(\n Effect.suspend(() =>\n Effect.sleep(Option.getOrElse(readDependencies().intervalMs, () => started)),\n ).pipe(Effect.andThen(Clock.currentTimeMillis), Effect.map(config.onTick)),\n ),\n }),\n }),\n })).ticks\n","/**\n * A wall-clock interval as a bundle: the last tick time in the Model,\n * advanced by a tick stream while running. Sibling of Timer (which counts\n * ticks); this one records *when* they happened, so views can render clocks\n * and elapsed times. The clock is Effect's, so tests drive it with TestClock.\n * For a clock whose running or interval the parent's Model decides, use the\n * `ticks` entry instead.\n */\nimport { Option, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\nimport { ticks } from './ticks.js'\n\nexport const IntervalModel = Schema.Struct({\n running: Schema.Boolean,\n lastAt: Schema.NullOr(Schema.Number),\n})\nexport type IntervalModel = typeof IntervalModel.Type\n\nexport const IntervalMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Ticked: { at: Schema.Number },\n})\nexport type IntervalMessage = typeof IntervalMessage.Type\n\nexport const Interval = Bundle.make('Interval', {\n Model: IntervalModel,\n Message: IntervalMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { running: false, lastAt: null } }),\n update: (model, message) =>\n IntervalMessage.match<Update.ReturnWithOutMessage<IntervalModel, IntervalMessage, never>>(\n message,\n {\n Started: () => (model.running ? { model } : { model: { ...model, running: true } }),\n Stopped: () => (model.running ? { model: { ...model, running: false } } : { model }),\n Ticked: ({ at }) => ({ model: { ...model, lastAt: at } }),\n },\n ),\n subscriptions: ({ intervalMs }) => ({\n ticks: ticks<IntervalModel, IntervalMessage>({\n intervalMs: model => (model.running ? Option.some(intervalMs) : Option.none()),\n onTick: at => IntervalMessage.Ticked({ at }),\n }),\n }),\n})\n","/**\n * Relative time as a pure function: picks the unit (seconds through years)\n * and delegates the wording to `Intl.RelativeTimeFormat`, so locales come\n * from the platform, not a phrase table. `from` is the earlier moment;\n * swap the arguments for future phrasing (\"in 3 days\").\n */\nexport const formatRelativeTime = (from: Date, to: Date, locale = 'en'): string => {\n const format = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n const seconds = Math.round((to.getTime() - from.getTime()) / 1000)\n const abs = Math.abs(seconds)\n if (abs < 60) return format.format(-seconds, 'second')\n const minutes = Math.round(seconds / 60)\n if (abs < 60 * 60) return format.format(-minutes, 'minute')\n const hours = Math.round(seconds / (60 * 60))\n if (abs < 24 * 60 * 60) return format.format(-hours, 'hour')\n const days = Math.round(seconds / (24 * 60 * 60))\n if (abs < 7 * 24 * 60 * 60) return format.format(-days, 'day')\n const weeks = Math.round(seconds / (7 * 24 * 60 * 60))\n if (abs < 30 * 24 * 60 * 60) return format.format(-weeks, 'week')\n const months = Math.round(seconds / (30 * 24 * 60 * 60))\n if (abs < 365 * 24 * 60 * 60) return format.format(-months, 'month')\n return format.format(-Math.round(seconds / (365 * 24 * 60 * 60)), 'year')\n}\n","/**\n * Leading-edge throttle as a bundle: the first `Attempted` in an interval\n * surfaces a `Throttled` OutMessage, the rest are dropped. `Attempted`\n * reads the clock through a Command, so the check runs against Effect time\n * and tests drive it with TestClock. Trailing-edge needs are Debounce's:\n * pair the two rather than adding a second timer here.\n */\nimport { Clock, Effect, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const ThrottleModel = Schema.Struct({ lastAt: Schema.NullOr(Schema.Number) })\nexport type ThrottleModel = typeof ThrottleModel.Type\n\nexport const ThrottleMessage = defineMessageUnion({\n Attempted: {},\n Evaluated: { now: Schema.Number },\n})\nexport type ThrottleMessage = typeof ThrottleMessage.Type\n\nexport const Throttled = Schema.TaggedStruct('Throttled', { at: Schema.Number })\nexport type Throttled = typeof Throttled.Type\n\nexport const Throttle = Bundle.make('Throttle', {\n Model: ThrottleModel,\n Message: ThrottleMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { lastAt: null } }),\n update: (model, message, args) =>\n ThrottleMessage.match<Update.ReturnWithOutMessage<ThrottleModel, ThrottleMessage, Throttled>>(\n message,\n {\n Attempted: () => ({\n model,\n commands: [\n {\n name: 'Throttle.evaluate',\n effect: Effect.map(Clock.currentTimeMillis, now =>\n ThrottleMessage.Evaluated({ now }),\n ),\n },\n ],\n }),\n // First attempt ever, or the interval has passed: fire and stamp.\n Evaluated: ({ now }) =>\n model.lastAt === null || now - model.lastAt >= args.intervalMs\n ? { model: { lastAt: now }, outMessage: Throttled.make({ at: now }) }\n : { model },\n },\n ),\n})\n","/**\n * A timer as a bundle: a count in the Model, advanced by a tick stream while\n * running. The clock is Effect's, so tests drive it with TestClock instead of\n * waiting. For a clock whose running or interval the parent's Model decides,\n * use the `ticks` entry instead.\n */\nimport { Option, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\nimport { ticks } from './ticks.js'\n\nexport const TimerModel = Schema.Struct({ count: Schema.Number, running: Schema.Boolean })\nexport type TimerModel = typeof TimerModel.Type\n\nexport const TimerMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Ticked: {},\n})\nexport type TimerMessage = typeof TimerMessage.Type\n\nexport const Timer = Bundle.make('Timer', {\n Model: TimerModel,\n Message: TimerMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { count: 0, running: false } }),\n update: (model, message) =>\n TimerMessage.match(message, {\n Started: () => (model.running ? { model } : { model: { ...model, running: true } }),\n Stopped: () => (model.running ? { model: { ...model, running: false } } : { model }),\n Ticked: () => ({ model: { ...model, count: model.count + 1 } }),\n }),\n subscriptions: ({ intervalMs }) => ({\n ticks: ticks<TimerModel, TimerMessage>({\n intervalMs: model => (model.running ? Option.some(intervalMs) : Option.none()),\n onTick: () => TimerMessage.Ticked(),\n }),\n }),\n})\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit-primitives",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Ready-made primitives for the Elm architecture, packaged with foldkit-bundle: media queries, presence, timers, pagination, history, sockets, observers, clipboard, and interaction Bundles with their Behaviors.",
5
5
  "keywords": [
6
6
  "foldkit",
@@ -27,46 +27,57 @@
27
27
  "sideEffects": false,
28
28
  "exports": {
29
29
  ".": {
30
+ "foldkit-plus:source": "./src/index.ts",
30
31
  "types": "./dist/index.d.mts",
31
32
  "import": "./dist/index.mjs"
32
33
  },
33
34
  "./dom": {
35
+ "foldkit-plus:source": "./src/dom/index.ts",
34
36
  "types": "./dist/dom/index.d.mts",
35
37
  "import": "./dist/dom/index.mjs"
36
38
  },
37
39
  "./device": {
40
+ "foldkit-plus:source": "./src/device/index.ts",
38
41
  "types": "./dist/device/index.d.mts",
39
42
  "import": "./dist/device/index.mjs"
40
43
  },
41
44
  "./events": {
45
+ "foldkit-plus:source": "./src/events/index.ts",
42
46
  "types": "./dist/events/index.d.mts",
43
47
  "import": "./dist/events/index.mjs"
44
48
  },
45
49
  "./interaction": {
50
+ "foldkit-plus:source": "./src/interaction/index.ts",
46
51
  "types": "./dist/interaction/index.d.mts",
47
52
  "import": "./dist/interaction/index.mjs"
48
53
  },
49
54
  "./media": {
55
+ "foldkit-plus:source": "./src/media/index.ts",
50
56
  "types": "./dist/media/index.d.mts",
51
57
  "import": "./dist/media/index.mjs"
52
58
  },
53
59
  "./motion": {
60
+ "foldkit-plus:source": "./src/motion/index.ts",
54
61
  "types": "./dist/motion/index.d.mts",
55
62
  "import": "./dist/motion/index.mjs"
56
63
  },
57
64
  "./net": {
65
+ "foldkit-plus:source": "./src/net/index.ts",
58
66
  "types": "./dist/net/index.d.mts",
59
67
  "import": "./dist/net/index.mjs"
60
68
  },
61
69
  "./observers": {
70
+ "foldkit-plus:source": "./src/observers/index.ts",
62
71
  "types": "./dist/observers/index.d.mts",
63
72
  "import": "./dist/observers/index.mjs"
64
73
  },
65
74
  "./state": {
75
+ "foldkit-plus:source": "./src/state/index.ts",
66
76
  "types": "./dist/state/index.d.mts",
67
77
  "import": "./dist/state/index.mjs"
68
78
  },
69
79
  "./time": {
80
+ "foldkit-plus:source": "./src/time/index.ts",
70
81
  "types": "./dist/time/index.d.mts",
71
82
  "import": "./dist/time/index.mjs"
72
83
  },
@@ -78,23 +89,23 @@
78
89
  "LICENSE"
79
90
  ],
80
91
  "dependencies": {
81
- "foldkit-bundle": "0.3.0"
92
+ "foldkit-bundle": "0.5.0"
82
93
  },
83
94
  "peerDependencies": {
84
- "effect": "^4.0.0-rc.116",
85
- "foldkit": "^0.163.0",
86
- "foldkit-mixins": "^0.4.0"
95
+ "effect": "^4.0.0",
96
+ "foldkit": "^0.165.0",
97
+ "foldkit-mixins": "^0.6.0"
87
98
  },
88
99
  "devDependencies": {
89
- "@effect/platform-browser": "4.0.0-rc.116",
100
+ "@effect/platform-browser": "4.0.0",
90
101
  "@types/ws": "^8.5.12",
91
- "effect": "4.0.0-rc.116",
92
- "foldkit": "0.163.0",
102
+ "effect": "4.0.0",
103
+ "foldkit": "0.165.0",
93
104
  "jsdom": "26.1.0",
94
105
  "tsdown": "^0.23.0",
95
106
  "typescript": "5.9.3",
96
107
  "ws": "^8.18.0",
97
- "foldkit-mixins": "0.4.0"
108
+ "foldkit-mixins": "0.6.0"
98
109
  },
99
110
  "peerDependenciesMeta": {
100
111
  "foldkit-mixins": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"motion-LfJYAMnP.mjs","names":[],"sources":["../src/motion/motion.ts","../src/motion/presence.ts","../src/motion/spring.ts","../src/motion/tween.ts"],"sourcesContent":["/**\n * Whether motion should be reduced, as a service read when a transition\n * starts, not sniffed once. Absent, motion is full, so a placement that\n * provides nothing behaves as before; a page provides `live` (the user's\n * `prefers-reduced-motion`) and a test provides `reduced` or `full`. Under\n * reduced motion a presence exits at once and a tween or spring jumps to its\n * end, in the same Messages, so the Model sees the same transitions.\n */\nimport { Context, Effect, Layer, Option } from 'effect'\n\nexport interface MotionShape {\n readonly reduced: Effect.Effect<boolean>\n}\n\nexport class Motion extends Context.Service<Motion, MotionShape>()('foldkit-primitives/Motion') {}\n\n/** Reduced or full motion, fixed: for a test or a setting the application owns. */\nexport const layer = (reduced: boolean): Layer.Layer<Motion> =>\n Layer.succeed(Motion, { reduced: Effect.succeed(reduced) })\n\nexport const reduced: Layer.Layer<Motion> = layer(true)\nexport const full: Layer.Layer<Motion> = layer(false)\n\n/** The user's `prefers-reduced-motion`, read each time a transition starts. */\nexport const live: Layer.Layer<Motion> = Layer.succeed(Motion, {\n reduced: Effect.sync(\n () =>\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches,\n ),\n})\n\n/** Whether to reduce motion now: the service's answer, or `false` without one. */\nexport const reducedMotion: Effect.Effect<boolean> = Effect.flatMap(\n Effect.serviceOption(Motion),\n Option.match({ onNone: () => Effect.succeed(false), onSome: motion => motion.reduced }),\n)\n","/**\n * Presence as a bundle: mount-transition state for exit animations. `Hide`\n * starts a timed `hiding` phase; the `Hidden` fact it yields carries the\n * generation it was scheduled under, so a `Show` in between wins and the\n * late fact is ignored. The clock is Effect's, so tests drive it.\n */\nimport { Effect, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\nimport { reducedMotion } from './motion.js'\n\nexport const PresenceModel = Schema.Struct({\n phase: Schema.Literals(['shown', 'hiding', 'hidden']),\n generation: Schema.Number,\n})\nexport type PresenceModel = typeof PresenceModel.Type\n\nexport const PresenceMessage = defineMessageUnion({\n Show: {},\n Hide: {},\n Hidden: { generation: Schema.Number },\n})\nexport type PresenceMessage = typeof PresenceMessage.Type\n\n/** Whether the content renders: shown or mid-exit. */\nexport const isVisible = (model: PresenceModel): boolean => model.phase !== 'hidden'\n\nexport const Presence = Bundle.make<\n 'Presence',\n PresenceModel,\n PresenceMessage,\n { readonly durationMs: number },\n never,\n never,\n never,\n void,\n {},\n {}\n>('Presence', {\n Model: PresenceModel,\n Message: PresenceMessage,\n args: Schema.Struct({\n durationMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { phase: 'hidden', generation: 0 } }),\n update: (model, message, args) =>\n PresenceMessage.match<Update.ReturnWithOutMessage<PresenceModel, PresenceMessage, never>>(\n message,\n {\n // Every new transition invalidates pending timeouts.\n Show: () => ({\n model: { ...model, phase: 'shown' as const, generation: model.generation + 1 },\n }),\n Hide: () => {\n const generation = model.generation + 1\n return {\n model: { ...model, phase: 'hiding' as const, generation },\n commands: [\n {\n name: 'Presence.hide',\n args: { generation },\n effect: Effect.flatMap(reducedMotion, reduced =>\n reduced\n ? Effect.succeed(PresenceMessage.Hidden({ generation }))\n : Effect.as(\n Effect.sleep(args.durationMs),\n PresenceMessage.Hidden({ generation }),\n ),\n ),\n },\n ],\n }\n },\n Hidden: ({ generation }) =>\n generation === model.generation\n ? { model: { ...model, phase: 'hidden' as const } }\n : { model },\n },\n ),\n})\n","/**\n * A spring as a bundle: a number pulled toward a target with stiffness and\n * damping, in the Model. Semi-implicit Euler at a fixed 16ms step, so the\n * trajectory is deterministic under TestClock; the stream ends with\n * Finished, and the value rests exactly at `to`. Underdamped springs\n * overshoot — that is the point — but the rest value is still exact.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\nimport { reducedMotion } from './motion.js'\n\nexport const SpringModel = Schema.Struct({\n value: Schema.Number,\n velocity: Schema.Number,\n running: Schema.Boolean,\n})\nexport type SpringModel = typeof SpringModel.Type\n\nexport const SpringMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Ticked: { value: Schema.Number, velocity: Schema.Number },\n /** Carries the exact end value: the integrator stops near it, not on it. */\n Finished: { value: Schema.Number },\n})\nexport type SpringMessage = typeof SpringMessage.Type\n\nconst step = 16\nconst epsilon = 0.001\n\ninterface Point {\n readonly x: number\n readonly v: number\n}\n\nconst settled = (point: Point, to: number): boolean =>\n Math.abs(to - point.x) < epsilon && Math.abs(point.v) < epsilon\n\nexport const Spring = Bundle.make('Spring', {\n Model: SpringModel,\n Message: SpringMessage,\n args: Schema.Struct({\n from: Schema.Number.pipe(Schema.check(Schema.isFinite())),\n to: Schema.Number.pipe(Schema.check(Schema.isFinite())),\n stiffness: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n damping: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: args => ({ model: { value: args.from, velocity: 0, running: false } }),\n update: (model, message) =>\n SpringMessage.match(message, {\n Started: () => ({ model: { ...model, running: true } }),\n Stopped: () => ({ model: { ...model, running: false } }),\n Ticked: ({ value, velocity }) => ({ model: { ...model, value, velocity } }),\n Finished: ({ value }) => ({ model: { ...model, value, velocity: 0, running: false } }),\n }),\n subscriptions: ({\n from,\n to,\n stiffness,\n damping,\n }): Subscription.Subscriptions<SpringModel, SpringMessage> =>\n Subscription.make<SpringModel, SpringMessage>()(entry => ({\n ticks: entry(\n { running: Schema.Boolean },\n {\n modelToDependencies: model => ({ running: model.running }),\n dependenciesToStream: ({ running }) => {\n if (!running) return Stream.empty\n // One integration step per tick, not per elapsed wall time: the\n // trajectory is identical on the live clock and TestClock, and a\n // stalled event loop slows the animation instead of catching up\n // all at once.\n const dt = step / 1000\n const points = Stream.tick(step).pipe(\n Stream.scan(\n (): Point => ({ x: from, v: 0 }),\n point => {\n const force = -stiffness * (point.x - to) - damping * point.v\n const v = point.v + force * dt\n return { x: point.x + v * dt, v }\n },\n ),\n // scan emits the seed (no movement yet) before the first step.\n Stream.drop(1),\n )\n return Stream.unwrap(\n Effect.map(reducedMotion, reduced =>\n reduced\n ? Stream.make(SpringMessage.Finished({ value: to }))\n : Stream.concat(\n points.pipe(\n Stream.takeUntil(point => settled(point, to)),\n Stream.map(point =>\n SpringMessage.Ticked({ value: point.x, velocity: point.v }),\n ),\n ),\n Stream.make(SpringMessage.Finished({ value: to })),\n ),\n ),\n )\n },\n },\n ),\n })),\n})\n","/**\n * A tween as a bundle: a number animated from one value to another over a\n * duration, in the Model. Progress comes from Effect's clock, so tests drive\n * it with TestClock; the stream ends with Finished, and the value rests at\n * `to`. Linear interpolation only: easing curves stay the application's job.\n */\nimport { Clock, Effect, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\nimport { reducedMotion } from './motion.js'\n\nexport const TweenModel = Schema.Struct({ value: Schema.Number, running: Schema.Boolean })\nexport type TweenModel = typeof TweenModel.Type\n\nexport const TweenMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Ticked: { value: Schema.Number },\n /** Carries the exact end value: the stream may stop on either side of it. */\n Finished: { value: Schema.Number },\n})\nexport type TweenMessage = typeof TweenMessage.Type\n\nexport const Tween = Bundle.make('Tween', {\n Model: TweenModel,\n Message: TweenMessage,\n args: Schema.Struct({\n from: Schema.Number.pipe(Schema.check(Schema.isFinite())),\n to: Schema.Number.pipe(Schema.check(Schema.isFinite())),\n ms: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())),\n }),\n init: args => ({ model: { value: args.from, running: false } }),\n update: (model, message) =>\n TweenMessage.match(message, {\n Started: () => ({ model: { ...model, running: true } }),\n Stopped: () => ({ model: { ...model, running: false } }),\n Ticked: ({ value }) => ({ model: { ...model, value } }),\n Finished: ({ value }) => ({ model: { ...model, value, running: false } }),\n }),\n subscriptions: ({ from, to, ms }): Subscription.Subscriptions<TweenModel, TweenMessage> =>\n Subscription.make<TweenModel, TweenMessage>()(entry => ({\n ticks: entry(\n { running: Schema.Boolean },\n {\n modelToDependencies: model => ({ running: model.running }),\n dependenciesToStream: ({ running }) =>\n running\n ? Stream.unwrap(\n Effect.flatMap(reducedMotion, reduced =>\n reduced\n ? Effect.succeed(Stream.make(TweenMessage.Finished({ value: to })))\n : Clock.currentTimeMillis.pipe(\n Effect.map(startedAt => {\n const progress = Stream.tick(16).pipe(\n Stream.mapEffect(() => Clock.currentTimeMillis),\n Stream.map(now => Math.min(1, (now - startedAt) / ms)),\n )\n return Stream.concat(\n progress.pipe(\n Stream.takeUntil(done => done >= 1),\n Stream.map(done =>\n TweenMessage.Ticked({ value: from + (to - from) * done }),\n ),\n ),\n Stream.make(TweenMessage.Finished({ value: to })),\n )\n }),\n ),\n ),\n )\n : Stream.empty,\n },\n ),\n })),\n})\n"],"mappings":"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"}