@aglyn/shared-ui-jsx 1.0.0-beta.230 → 1.0.0-beta.232

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 (52) hide show
  1. package/package.json +5 -5
  2. package/src/lib/components/copy-field.component.d.ts +25 -0
  3. package/src/lib/components/copy-field.component.js +77 -0
  4. package/src/lib/components/copy-field.component.js.map +1 -0
  5. package/src/lib/components/download-cost-badge.component.d.ts +37 -0
  6. package/src/lib/components/download-cost-badge.component.js +89 -0
  7. package/src/lib/components/download-cost-badge.component.js.map +1 -0
  8. package/src/lib/components/file-drop-zone.component.d.ts +48 -0
  9. package/src/lib/components/file-drop-zone.component.js +173 -0
  10. package/src/lib/components/file-drop-zone.component.js.map +1 -0
  11. package/src/lib/components/list-query-notices.component.d.ts +1 -18
  12. package/src/lib/components/list-query-notices.component.js +1 -11
  13. package/src/lib/components/list-query-notices.component.js.map +1 -1
  14. package/src/lib/components/list-table.component.d.ts +12 -0
  15. package/src/lib/components/list-table.component.js +30 -4
  16. package/src/lib/components/list-table.component.js.map +1 -1
  17. package/src/lib/components/option-card-grid.component.d.ts +46 -0
  18. package/src/lib/components/option-card-grid.component.js +139 -0
  19. package/src/lib/components/option-card-grid.component.js.map +1 -0
  20. package/src/lib/components/searchable-virtual-list.component.d.ts +72 -0
  21. package/src/lib/components/searchable-virtual-list.component.js +212 -0
  22. package/src/lib/components/searchable-virtual-list.component.js.map +1 -0
  23. package/src/lib/components/site-live-notice.component.d.ts +39 -0
  24. package/src/lib/components/site-live-notice.component.js +95 -0
  25. package/src/lib/components/site-live-notice.component.js.map +1 -0
  26. package/src/lib/components/status-chip.component.d.ts +26 -0
  27. package/src/lib/components/status-chip.component.js +48 -0
  28. package/src/lib/components/status-chip.component.js.map +1 -0
  29. package/src/lib/const/list-filter-sentence.d.ts +1 -24
  30. package/src/lib/const/list-filter-sentence.js +3 -105
  31. package/src/lib/const/list-filter-sentence.js.map +1 -1
  32. package/src/lib/const/list-filter.d.ts +1 -219
  33. package/src/lib/const/list-filter.js +4 -282
  34. package/src/lib/const/list-filter.js.map +1 -1
  35. package/src/lib/const/list-grid-filter.d.ts +4 -25
  36. package/src/lib/const/list-grid-filter.js +20 -44
  37. package/src/lib/const/list-grid-filter.js.map +1 -1
  38. package/src/lib/const/list-query-plan.d.ts +1 -154
  39. package/src/lib/const/list-query-plan.js +4 -610
  40. package/src/lib/const/list-query-plan.js.map +1 -1
  41. package/src/lib/hooks/use-client-pagination.d.ts +25 -0
  42. package/src/lib/hooks/use-client-pagination.js +65 -0
  43. package/src/lib/hooks/use-client-pagination.js.map +1 -0
  44. package/src/lib/hooks/use-in-view.d.ts +16 -0
  45. package/src/lib/hooks/use-in-view.js +66 -0
  46. package/src/lib/hooks/use-in-view.js.map +1 -0
  47. package/src/lib/hooks/use-list-column-sort.d.ts +57 -0
  48. package/src/lib/hooks/use-list-column-sort.js +156 -0
  49. package/src/lib/hooks/use-list-column-sort.js.map +1 -0
  50. package/src/lib/hooks/use-viewport-fill.d.ts +39 -0
  51. package/src/lib/hooks/use-viewport-fill.js +64 -0
  52. package/src/lib/hooks/use-viewport-fill.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-query-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type ListFilterField,\n type ListFilterRequest,\n listFilterDay,\n listFilterOperators,\n} from './list-filter'\n\n/*\n * EVERY CLAUSE ON THE QUERY (AGL-3321).\n *\n * The AGL-2501 contract turned ONE filter into a Firestore query — the list\n * put the first servable clause on its query and matched the rest over the\n * rows it had loaded, and search the same way. A clause matched over a loaded\n * page answers \"no match\" for a record on the next one, which is the one\n * answer a list must never give wrongly.\n *\n * This composes them all. A list declares its fields (the same\n * `ListFilterField` grammar), the sorts it offers and the token array its\n * quick search reads; `planListQuery` turns the clauses in force and the\n * search words into ONE query — every predicate, one order — and says which\n * it could not put there and why. Nothing is matched in memory afterwards:\n * a clause the plan refuses is not applied, and the list says so, rather\n * than being applied to some rows and not others.\n *\n * The plan is SDK-free and model-free: the text normalizers the written\n * search fields were stamped with come in as `ListQueryNormalizers` — the\n * platform's are `nameSearchNormalizers` (`@aglyn/aglyn/app-utils/name-search`)\n * — so the query asks for exactly the keys the writers stored. The web twin (`listQueryConstraints` in the instance\n * library) and the Admin twin (`applyListQuery` in the console) turn it into\n * their SDK's query, so the two cannot disagree about what a clause means.\n *\n * ## What one Firestore query can hold\n *\n * - any number of equalities (`==`, `in`), composed through INDEX MERGING:\n * one `(field, sort)` composite per equality field serves every\n * combination of them, measured against production (AGL-3321);\n * - ONE array clause — `array-contains` or `array-contains-any` — which a\n * search token, a `contains` filter and a scope clause all compete for;\n * - ONE inequality field (a range, `!=`, a prefix range), which must then\n * lead the order — so a range orders the list by the field it ranges over;\n * - at most thirty disjunctions, which an `in` and an\n * `array-contains-any` multiply.\n *\n * The declaration's `listQueryIndexes` enumerates the composites those\n * shapes need, and each list's spec holds the index file to it.\n */\n\n/**\n * How the written search fields were normalized, so a typed value becomes the\n * key the query can match: the lower-cased key (`equals`, `startsWith`), the\n * one word-prefix token (`contains`, search), and the reversed key\n * (`endsWith`). `maxPrefix` is the longest token a writer stores.\n */\nexport interface ListQueryNormalizers {\n key(value: string): string\n token(value: string): string\n reversed(value: string): string\n maxPrefix: number\n}\n\n/** A predicate on the query, SDK-free. `path` `__name__` is the document id. */\nexport type ListQueryOp =\n | '=='\n | '!='\n | '<'\n | '<='\n | '>'\n | '>='\n | 'in'\n | 'array-contains'\n | 'array-contains-any'\n\nexport type ListQueryValue = string | number | boolean | null | Date | readonly (string | number)[]\n\nexport interface ListQueryFilter {\n path: string\n op: ListQueryOp\n value: ListQueryValue\n}\n\nexport interface ListQuerySort {\n path: string\n direction: 'asc' | 'desc'\n /** The grid column this order sorts, when the list lets the grid sort by it. */\n column?: string\n}\n\n/** The document id, as a path. */\nexport const LIST_QUERY_ID_PATH = '__name__'\n\n/** Firestore's cap on disjunctions — `in` values times `array-contains-any` values. */\nexport const LIST_QUERY_DISJUNCTIONS = 30\n\n/** A very high private-use codepoint: `[p, p + HIGH]` spans every string starting `p`. */\nconst HIGH = '\\uf8ff'\n\nexport interface ListQueryDeclaration {\n /** Every field the Filters panel may name. */\n fields: readonly ListFilterField[]\n /**\n * The orders the list offers, its default FIRST. The query takes no other:\n * each is a composite the index file carries for every filterable field.\n */\n sorts: readonly ListQuerySort[]\n /**\n * The quick search: the token array every write stamps (see\n * `nameSearchTokens`), queried with `array-contains` on one typed word.\n * Absent, the list offers no search box.\n */\n search?: {\n tokensPath: string\n /**\n * Under a scope clause the search cannot be a SECOND array clause, so a\n * scoped list writes `<scopeToken><join><prefix>` tokens and the query\n * asks `array-contains-any` over the scope's tokens joined to the word —\n * one array clause that answers both.\n *\n * ⛔ Rules that grant a list from `visibleTo.hasAny(...)` DENY this\n * query, because it no longer constrains `visibleTo`. Declare it only for\n * readers whose rule needs no `visibleTo` term, or for an Admin-SDK read;\n * see `SCOPED_SEARCH_JOIN` in `@aglyn/aglyn/app-utils/name-search`.\n */\n scoped?: { tokensPath: string; join: string }\n }\n /**\n * Keys a presence-map field (`keysOf`) is filtered by, for the index\n * enumeration: `sources.form`, `sources.order`, …\n */\n keys?: Readonly<Record<string, readonly string[]>>\n}\n\n/** What the list asks this time. */\nexport interface ListQueryRequest {\n clauses: readonly ListFilterRequest[]\n /** The quick search's words. */\n search?: readonly string[]\n /** The order asked for; a sort the declaration does not offer is ignored. */\n sort?: ListQuerySort | null\n /**\n * Predicates the list always applies — its scope — which count against\n * the same limits. An `array-contains-any` scope is what a scoped search\n * folds into (`declaration.search.scoped`).\n */\n base?: readonly ListQueryFilter[]\n}\n\nexport interface ListQueryRefusal {\n /** The clause refused, or `search`. */\n clause: ListFilterRequest | 'search'\n reason: string\n}\n\nexport interface ListQueryPlan {\n /** Every predicate, base included, in the order the SDK should add them. */\n filters: ListQueryFilter[]\n /** The one order: the range field's when there is one, else the sort. */\n orderBy: ListQuerySort\n /** The clauses on the query. */\n served: ListFilterRequest[]\n /** The search word on the query, or null. */\n searched: string | null\n /** What was asked and could not be put on the query, with why. */\n refused: ListQueryRefusal[]\n /** Said to the reader about what WAS served — e.g. one search word of two. */\n notices: string[]\n}\n\n/** How one clause lands on a query, before it is composed with the others. */\ninterface ClauseShape {\n filters: ListQueryFilter[]\n /** The inequality field the clause needs to lead the order, if any. */\n inequality?: string\n /** The clause is the query's array clause. */\n array?: boolean\n /** `in` values, for the disjunction budget. */\n disjunctions?: number\n}\n\nconst csv = (raw: string): string[] =>\n raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n\nconst dayBounds = (raw: string): { start: Date; end: Date } | null => {\n const start = listFilterDay(raw)\n if (Number.isNaN(start.getTime())) return null\n const day = new Date(start)\n day.setHours(0, 0, 0, 0)\n const end = new Date(day)\n end.setDate(end.getDate() + 1)\n return { start: day, end }\n}\n\n/** The order a range over `path` puts the list in. */\nconst rangeOrder = (\n declaration: ListQueryDeclaration,\n field: ListFilterField | undefined,\n path: string,\n): ListQuerySort => {\n const offered = declaration.sorts.find((sort) => sort.path === path)\n if (offered) return offered\n return { path, direction: field?.kind === 'date' ? 'desc' : 'asc' }\n}\n\n/**\n * One clause as predicates, or the reason it has none. The same operator\n * meanings as the AGL-2501 twins (`applyListFilter`, `listFilterConstraints`),\n * which this replaces for a list that composes clauses.\n */\nfunction shapeClause(\n field: ListFilterField,\n input: ListFilterRequest,\n names: ListQueryNormalizers,\n): ClauseShape | string {\n if (field.windowOnly) return 'this field is not stored where a query can reach it'\n if (!listFilterOperators(field).includes(input.op)) {\n return `${input.op} is not something this list can ask of ${field.column}`\n }\n const raw = (input.value ?? '').trim()\n const op = input.op\n\n if (op === 'isEmpty') {\n if (field.presence !== 'nullable') return 'only a field stored as null can be asked for empty'\n return { filters: [{ path: field.path, op: '==', value: null }] }\n }\n if (op === 'isNotEmpty') {\n if (field.presence === 'always') return 'this field is never empty'\n return { filters: [{ path: field.path, op: '!=', value: null }], inequality: field.path }\n }\n if (field.kind === 'boolean') {\n if (raw !== 'true' && raw !== 'false') return 'pick true or false'\n return { filters: [{ path: field.path, op: '==', value: raw === 'true' }] }\n }\n if (!raw) return 'no value yet'\n\n if (field.keysOf) {\n if (op !== 'equals' || !/^[A-Za-z0-9_]+$/.test(raw)) return 'pick one of the choices'\n return { filters: [{ path: `${field.path}.${raw}`, op: '==', value: true }] }\n }\n\n if (field.kind === 'text') {\n if (op === 'contains' && field.tokensPath) {\n const token = field.verbatimTokens ? raw : names.token(raw)\n if (!token) return 'no value yet'\n return {\n filters: [{ path: field.tokensPath, op: 'array-contains', value: token }],\n array: true,\n }\n }\n if (op === 'equals' && field.lowerPath) {\n return { filters: [{ path: field.lowerPath, op: '==', value: names.key(raw) }] }\n }\n if (op === 'equals' && field.presence === 'always' && !field.lowerPath) {\n return { filters: [{ path: field.path, op: '==', value: raw }] }\n }\n if (op === 'startsWith' && field.lowerPath) {\n const key = names.key(raw)\n return {\n filters: [\n { path: field.lowerPath, op: '>=', value: key },\n { path: field.lowerPath, op: '<=', value: `${key}${HIGH}` },\n ],\n inequality: field.lowerPath,\n }\n }\n if (op === 'endsWith' && field.reversedPath) {\n const key = names.reversed(raw)\n return {\n filters: [\n { path: field.reversedPath, op: '>=', value: key },\n { path: field.reversedPath, op: '<=', value: `${key}${HIGH}` },\n ],\n inequality: field.reversedPath,\n }\n }\n if (op === 'isAnyOf' && field.lowerPath) {\n const values = csv(raw).map((value) => names.key(value))\n if (values.length > LIST_QUERY_DISJUNCTIONS) return `at most ${LIST_QUERY_DISJUNCTIONS} values`\n return {\n filters: [{ path: field.lowerPath, op: 'in', value: values }],\n disjunctions: values.length,\n }\n }\n return `${op} is not something this list can ask of ${field.column}`\n }\n\n if (field.kind === 'id') {\n if (op === 'equals') return { filters: [{ path: LIST_QUERY_ID_PATH, op: '==', value: raw }] }\n if (op === 'isAnyOf') {\n const values = csv(raw)\n if (values.length > LIST_QUERY_DISJUNCTIONS) return `at most ${LIST_QUERY_DISJUNCTIONS} values`\n return {\n filters: [{ path: LIST_QUERY_ID_PATH, op: 'in', value: values }],\n disjunctions: values.length,\n }\n }\n if (op === 'startsWith') {\n return {\n filters: [\n { path: LIST_QUERY_ID_PATH, op: '>=', value: raw },\n { path: LIST_QUERY_ID_PATH, op: '<=', value: `${raw}${HIGH}` },\n ],\n inequality: LIST_QUERY_ID_PATH,\n }\n }\n return `${op} is not something this list can ask of ${field.column}`\n }\n\n if (field.kind === 'exact') {\n if (op === 'equals') return { filters: [{ path: field.path, op: '==', value: raw }] }\n if (op === 'isAnyOf') {\n const values = csv(raw)\n if (values.length > LIST_QUERY_DISJUNCTIONS) return `at most ${LIST_QUERY_DISJUNCTIONS} values`\n if (field.tokensPath) {\n // An array field asked \"any of these\": one array clause.\n return {\n filters: [{ path: field.tokensPath, op: 'array-contains-any', value: values }],\n array: true,\n disjunctions: values.length,\n }\n }\n return {\n filters: [{ path: field.path, op: 'in', value: values }],\n disjunctions: values.length,\n }\n }\n if (op === 'contains' && field.tokensPath) {\n return {\n filters: [{ path: field.tokensPath, op: 'array-contains', value: raw }],\n array: true,\n }\n }\n return `${op} is not something this list can ask of ${field.column}`\n }\n\n if (field.kind === 'number') {\n const value = Number(raw)\n if (!Number.isFinite(value)) return 'type a number'\n if (op === '=') return { filters: [{ path: field.path, op: '==', value }] }\n const comparison: Record<string, ListQueryOp> = {\n '!=': '!=',\n '>': '>',\n '>=': '>=',\n '<': '<',\n '<=': '<=',\n }\n const found = comparison[op]\n if (!found) return `${op} is not something this list can ask of ${field.column}`\n return { filters: [{ path: field.path, op: found, value }], inequality: field.path }\n }\n\n if (field.kind === 'date') {\n const day = dayBounds(raw)\n if (!day) return 'pick a date'\n // A `…AtMs` field is compared as the number it is stored as.\n const at = (date: Date): Date | number =>\n field.storedAs === 'millis' ? date.getTime() : date\n if (op === 'is') {\n return {\n filters: [\n { path: field.path, op: '>=', value: at(day.start) },\n { path: field.path, op: '<', value: at(day.end) },\n ],\n inequality: field.path,\n }\n }\n const bound: Record<string, [ListQueryOp, Date]> = {\n after: ['>=', day.end],\n onOrAfter: ['>=', day.start],\n before: ['<', day.start],\n onOrBefore: ['<', day.end],\n }\n const found = bound[op]\n if (!found) return `${op} is not something this list can ask of ${field.column}`\n return { filters: [{ path: field.path, op: found[0], value: at(found[1]) }], inequality: field.path }\n }\n\n return `${op} is not something this list can ask of ${field.column}`\n}\n\nconst disjunctionsOf = (filter: ListQueryFilter): number =>\n filter.op === 'in' || filter.op === 'array-contains-any'\n ? Math.max(1, Array.isArray(filter.value) ? filter.value.length : 1)\n : 1\n\n/**\n * Every clause and the search, as ONE query — or as much of it as one query\n * can hold, with the rest refused by name.\n *\n * Order of claim: the base (the list's scope) first, then the search, then\n * the clauses in the order they were added. A clause that would be a second\n * array clause, a second inequality field or push the disjunctions past\n * thirty is refused, and the list shows the refusal beside its chip; the\n * clauses before it stand. The search is claimed before the clauses because\n * it is the thing a reader typed last and looks at.\n */\nexport function planListQuery(\n declaration: ListQueryDeclaration,\n request: ListQueryRequest,\n names: ListQueryNormalizers,\n): ListQueryPlan {\n const filters: ListQueryFilter[] = [...(request.base ?? [])]\n const served: ListFilterRequest[] = []\n const refused: ListQueryRefusal[] = []\n const notices: string[] = []\n let arrayTaken = filters.some(\n (filter) => filter.op === 'array-contains' || filter.op === 'array-contains-any',\n )\n let inequality: string | null =\n filters.find((filter) => ['!=', '<', '<=', '>', '>='].includes(filter.op))?.path ?? null\n let disjunctions = filters.reduce((product, filter) => product * disjunctionsOf(filter), 1)\n\n // The search.\n let searched: string | null = null\n const words = (request.search ?? []).map((word) => word.trim()).filter(Boolean)\n if (words.length) {\n const search = declaration.search\n const token = names.token(words.join(' '))\n if (!search) {\n refused.push({ clause: 'search', reason: 'this list has no search' })\n } else if (!token) {\n // Nothing searchable was typed.\n } else {\n const scopeAt = filters.findIndex(\n (filter) => filter.op === 'array-contains-any' || filter.op === 'array-contains',\n )\n if (scopeAt === -1) {\n filters.push({ path: search.tokensPath, op: 'array-contains', value: token })\n arrayTaken = true\n searched = token\n } else if (search.scoped) {\n // Fold the search into the scope: one array clause answers both.\n const scope = filters[scopeAt]\n const scopes = Array.isArray(scope.value) ? scope.value : [scope.value]\n filters.splice(scopeAt, 1, {\n path: search.scoped.tokensPath,\n op: 'array-contains-any',\n value: scopes.map((entry) => `${String(entry)}${search.scoped?.join}${token}`),\n })\n searched = token\n } else {\n refused.push({\n clause: 'search',\n reason: 'this list is already narrowed to what you can see, which search cannot combine with',\n })\n }\n if (searched && words.length > 1) {\n notices.push(\n `Search matches one word at a time: showing results for \"${token}\".`,\n )\n }\n if (searched && names.key(words[0]).length > names.maxPrefix) {\n notices.push(`Search reads the first ${names.maxPrefix} letters of a word.`)\n }\n }\n }\n\n // The clauses.\n for (const clause of request.clauses) {\n const field = declaration.fields.find((entry) => entry.column === clause.field)\n if (!field) {\n refused.push({ clause, reason: 'this list does not filter by that' })\n continue\n }\n const shape = shapeClause(field, clause, names)\n if (typeof shape === 'string') {\n refused.push({ clause, reason: shape })\n continue\n }\n if (shape.array && arrayTaken) {\n refused.push({\n clause,\n reason: searched\n ? 'cannot be combined with the search — clear the search to use it'\n : 'cannot be combined with another \"contains\" or \"any of\" filter on a list',\n })\n continue\n }\n if (shape.inequality && inequality && shape.inequality !== inequality) {\n refused.push({\n clause,\n reason: 'only one range (dates, numbers, starts with) can apply at a time',\n })\n continue\n }\n const product =\n disjunctions * shape.filters.reduce((total, filter) => total * disjunctionsOf(filter), 1)\n if (product > LIST_QUERY_DISJUNCTIONS) {\n refused.push({ clause, reason: `too many values at once (the limit is ${LIST_QUERY_DISJUNCTIONS})` })\n continue\n }\n filters.push(...shape.filters)\n served.push(clause)\n if (shape.array) arrayTaken = true\n if (shape.inequality) inequality = shape.inequality\n disjunctions = product\n }\n\n // The order.\n const asked = request.sort\n ? declaration.sorts.find(\n (sort) => sort.path === request.sort?.path && sort.direction === request.sort?.direction,\n )\n : undefined\n // A range leads the order with the field it ranges over — in the asked\n // direction when the asked order IS that field, since a declared order has\n // its composites either way.\n const orderBy = inequality\n ? asked?.path === inequality\n ? asked\n : rangeOrder(\n declaration,\n declaration.fields.find(\n (field) =>\n field.path === inequality ||\n field.lowerPath === inequality ||\n field.reversedPath === inequality,\n ),\n inequality,\n )\n : (asked ?? declaration.sorts[0] ?? { path: LIST_QUERY_ID_PATH, direction: 'asc' })\n\n return { filters, orderBy, served, searched, refused, notices }\n}\n\n/** A composite the plan's shapes need, in the index file's own terms. */\nexport interface ListQueryIndex {\n fields: Array<{ fieldPath: string; order?: 'ASCENDING' | 'DESCENDING'; arrayConfig?: 'CONTAINS' }>\n}\n\nconst ORDER = { asc: 'ASCENDING', desc: 'DESCENDING' } as const\n\n/**\n * Every composite a declaration's queries can need, under index merging:\n * one `(predicate field, order)` index per equality or array field and per\n * order the list can take — its sorts, and the order each range imposes.\n *\n * `base` names the scope predicates the list always applies (their paths\n * and whether they are array clauses), which need their own index per order\n * like any other equality. Pinned by each list's spec against\n * `cloud/firebase-firestore.indexes.json`.\n */\nexport function listQueryIndexes(\n declaration: ListQueryDeclaration,\n base: readonly { path: string; array?: boolean }[] = [],\n): ListQueryIndex[] {\n const equalities = new Map<string, boolean>() // path → is array clause\n const orders: ListQuerySort[] = [...declaration.sorts]\n const addOrder = (sort: ListQuerySort) => {\n if (!orders.some((entry) => entry.path === sort.path && entry.direction === sort.direction)) {\n orders.push(sort)\n }\n }\n for (const entry of base) equalities.set(entry.path, Boolean(entry.array))\n if (declaration.search) {\n equalities.set(declaration.search.tokensPath, true)\n if (declaration.search.scoped) equalities.set(declaration.search.scoped.tokensPath, true)\n }\n for (const field of declaration.fields) {\n if (field.windowOnly) continue\n const ops = listFilterOperators(field)\n const has = (...names: string[]) => names.some((name) => ops.includes(name))\n if (field.keysOf) {\n for (const key of declaration.keys?.[field.column] ?? []) {\n equalities.set(`${field.path}.${key}`, false)\n }\n continue\n }\n if (field.kind === 'text') {\n if (has('contains') && field.tokensPath) equalities.set(field.tokensPath, true)\n if (has('equals', 'isAnyOf') && field.lowerPath) equalities.set(field.lowerPath, false)\n if (has('equals') && !field.lowerPath && field.presence === 'always') equalities.set(field.path, false)\n if (has('startsWith') && field.lowerPath) addOrder(rangeOrder(declaration, field, field.lowerPath))\n if (has('endsWith') && field.reversedPath) addOrder(rangeOrder(declaration, field, field.reversedPath))\n } else if (field.kind === 'exact') {\n if (has('equals', 'isAnyOf')) equalities.set(field.path, false)\n if (has('contains', 'isAnyOf') && field.tokensPath) equalities.set(field.tokensPath, true)\n } else if (field.kind === 'boolean') {\n equalities.set(field.path, false)\n } else if (field.kind === 'number') {\n if (has('=')) equalities.set(field.path, false)\n if (has('!=', '>', '>=', '<', '<=')) addOrder(rangeOrder(declaration, field, field.path))\n } else if (field.kind === 'date') {\n if (has('is', 'after', 'onOrAfter', 'before', 'onOrBefore')) {\n addOrder(rangeOrder(declaration, field, field.path))\n }\n }\n if (has('isEmpty')) equalities.set(field.path, false)\n if (has('isNotEmpty')) addOrder(rangeOrder(declaration, field, field.path))\n }\n const indexes: ListQueryIndex[] = []\n const seen = new Set<string>()\n for (const [path, array] of equalities) {\n for (const order of orders) {\n if (order.path === path || order.path === LIST_QUERY_ID_PATH) continue\n const index: ListQueryIndex = {\n fields: [\n array ? { fieldPath: path, arrayConfig: 'CONTAINS' } : { fieldPath: path, order: 'ASCENDING' },\n { fieldPath: order.path, order: ORDER[order.direction] },\n ],\n }\n const key = JSON.stringify(index)\n if (!seen.has(key)) {\n seen.add(key)\n indexes.push(index)\n }\n }\n }\n return indexes\n}\n\n/** An index entry as `cloud/firebase-firestore.indexes.json` stores it. */\ninterface IndexFileEntry {\n collectionGroup: string\n queryScope: string\n fields: Array<{ fieldPath: string; order?: string; arrayConfig?: string }>\n}\n\n/**\n * The composites a declaration needs that an index file does not hold — what\n * a list's spec asserts is empty, so a declared filter cannot ship without\n * the index that serves it.\n */\nexport function missingListQueryIndexes(\n file: { indexes: readonly IndexFileEntry[] },\n collectionGroup: string,\n needed: readonly ListQueryIndex[],\n queryScope: 'COLLECTION' | 'COLLECTION_GROUP' = 'COLLECTION',\n): ListQueryIndex[] {\n const shape = (fields: IndexFileEntry['fields']) =>\n fields\n .filter((field) => field.fieldPath !== LIST_QUERY_ID_PATH)\n .map((field) => `${field.fieldPath}:${field.order ?? field.arrayConfig}`)\n .join(',')\n const present = new Set(\n file.indexes\n .filter((index) => index.collectionGroup === collectionGroup && index.queryScope === queryScope)\n .map((index) => shape(index.fields)),\n )\n return needed.filter((index) => !present.has(shape(index.fields)))\n}\n"],"names":["listFilterDay","listFilterOperators","LIST_QUERY_ID_PATH","LIST_QUERY_DISJUNCTIONS","HIGH","csv","raw","split","map","entry","trim","filter","Boolean","dayBounds","start","Number","isNaN","getTime","day","Date","setHours","end","setDate","getDate","rangeOrder","declaration","field","path","offered","sorts","find","sort","direction","kind","shapeClause","input","names","windowOnly","includes","op","column","value","presence","filters","inequality","keysOf","test","tokensPath","token","verbatimTokens","array","lowerPath","key","reversedPath","reversed","values","length","disjunctions","isFinite","comparison","found","at","date","storedAs","bound","after","onOrAfter","before","onOrBefore","disjunctionsOf","Math","max","Array","isArray","planListQuery","request","asked","base","served","refused","notices","arrayTaken","some","reduce","product","searched","words","search","word","join","push","clause","reason","scopeAt","findIndex","scoped","scope","scopes","splice","String","maxPrefix","clauses","fields","shape","total","undefined","orderBy","ORDER","asc","desc","listQueryIndexes","equalities","Map","orders","addOrder","set","ops","has","name","keys","indexes","seen","Set","order","index","fieldPath","arrayConfig","JSON","stringify","add","missingListQueryIndexes","file","collectionGroup","needed","queryScope","present"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-query-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * The list filter grammar lives in a pure module, so a surface with no web UI\n * (the native app) plans the same queries; this path stays its web entry.\n */\nexport * from '@aglyn/shared-util-tools/list-query/list-query-plan'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;CAGC,GACD,cAAc,sDAAqD"}
@@ -0,0 +1,25 @@
1
+ import type { ListPaginationProps } from '../components/list-pagination.component';
2
+ export interface ClientPagination<T> {
3
+ /** Zero-based. */
4
+ page: number;
5
+ pageSize: number;
6
+ /** The rows of the current page. */
7
+ pageItems: T[];
8
+ setPage: (page: number) => void;
9
+ /** Changes the page size and returns to the first page. */
10
+ setPageSize: (pageSize: number) => void;
11
+ /** Everything `ListPagination` needs, for a list held whole in memory. */
12
+ paginationProps: ListPaginationProps;
13
+ }
14
+ /**
15
+ * A list already held whole in memory, paged for `ListPagination` (AGL-3637).
16
+ *
17
+ * The pairing every plugin was writing by hand: a page and a page size in
18
+ * state, a `slice()`, and the footer's props. The page is pulled back inside
19
+ * the list when it shrinks under the reader — a filter that leaves two rows
20
+ * must not strand them on an empty page five. A list fetched page by page
21
+ * from Firestore pages with `usePagedCollection` instead.
22
+ */
23
+ export declare function useClientPagination<T>(items: readonly T[], options?: {
24
+ pageSize?: number;
25
+ }): ClientPagination<T>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { useCallback, useEffect, useMemo, useState } from "react";
18
+ import { TABLE_PAGE_SIZE_DEFAULT } from "../const/table-pagination.js";
19
+ /**
20
+ * A list already held whole in memory, paged for `ListPagination` (AGL-3637).
21
+ *
22
+ * The pairing every plugin was writing by hand: a page and a page size in
23
+ * state, a `slice()`, and the footer's props. The page is pulled back inside
24
+ * the list when it shrinks under the reader — a filter that leaves two rows
25
+ * must not strand them on an empty page five. A list fetched page by page
26
+ * from Firestore pages with `usePagedCollection` instead.
27
+ */ export function useClientPagination(items, options = {}) {
28
+ var _options_pageSize;
29
+ const [page, setPage] = useState(0);
30
+ const [pageSize, setPageSizeState] = useState((_options_pageSize = options.pageSize) != null ? _options_pageSize : TABLE_PAGE_SIZE_DEFAULT);
31
+ const lastPage = Math.max(0, Math.ceil(items.length / pageSize) - 1);
32
+ useEffect(()=>{
33
+ if (page > lastPage) setPage(lastPage);
34
+ }, [
35
+ page,
36
+ lastPage
37
+ ]);
38
+ const current = Math.min(page, lastPage);
39
+ const pageItems = useMemo(()=>items.slice(current * pageSize, (current + 1) * pageSize), [
40
+ items,
41
+ current,
42
+ pageSize
43
+ ]);
44
+ const setPageSize = useCallback((size)=>{
45
+ setPageSizeState(size);
46
+ setPage(0);
47
+ }, []);
48
+ return {
49
+ page: current,
50
+ pageSize,
51
+ pageItems,
52
+ setPage,
53
+ setPageSize,
54
+ paginationProps: {
55
+ page: current,
56
+ pageSize,
57
+ rowCount: pageItems.length,
58
+ count: items.length,
59
+ onPageChange: setPage,
60
+ onPageSizeChange: setPageSize
61
+ }
62
+ };
63
+ }
64
+
65
+ //# sourceMappingURL=use-client-pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/hooks/use-client-pagination.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '../const/table-pagination'\nimport type { ListPaginationProps } from '../components/list-pagination.component'\n\nexport interface ClientPagination<T> {\n /** Zero-based. */\n page: number\n pageSize: number\n /** The rows of the current page. */\n pageItems: T[]\n setPage: (page: number) => void\n /** Changes the page size and returns to the first page. */\n setPageSize: (pageSize: number) => void\n /** Everything `ListPagination` needs, for a list held whole in memory. */\n paginationProps: ListPaginationProps\n}\n\n/**\n * A list already held whole in memory, paged for `ListPagination` (AGL-3637).\n *\n * The pairing every plugin was writing by hand: a page and a page size in\n * state, a `slice()`, and the footer's props. The page is pulled back inside\n * the list when it shrinks under the reader — a filter that leaves two rows\n * must not strand them on an empty page five. A list fetched page by page\n * from Firestore pages with `usePagedCollection` instead.\n */\nexport function useClientPagination<T>(\n items: readonly T[],\n options: { pageSize?: number } = {},\n): ClientPagination<T> {\n const [page, setPage] = useState(0)\n const [pageSize, setPageSizeState] = useState(options.pageSize ?? TABLE_PAGE_SIZE_DEFAULT)\n const lastPage = Math.max(0, Math.ceil(items.length / pageSize) - 1)\n useEffect(() => {\n if (page > lastPage) setPage(lastPage)\n }, [page, lastPage])\n const current = Math.min(page, lastPage)\n const pageItems = useMemo(\n () => items.slice(current * pageSize, (current + 1) * pageSize),\n [items, current, pageSize],\n )\n const setPageSize = useCallback((size: number) => {\n setPageSizeState(size)\n setPage(0)\n }, [])\n return {\n page: current,\n pageSize,\n pageItems,\n setPage,\n setPageSize,\n paginationProps: {\n page: current,\n pageSize,\n rowCount: pageItems.length,\n count: items.length,\n onPageChange: setPage,\n onPageSizeChange: setPageSize,\n },\n }\n}\n"],"names":["useCallback","useEffect","useMemo","useState","TABLE_PAGE_SIZE_DEFAULT","useClientPagination","items","options","page","setPage","pageSize","setPageSizeState","lastPage","Math","max","ceil","length","current","min","pageItems","slice","setPageSize","size","paginationProps","rowCount","count","onPageChange","onPageSizeChange"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACjE,SAASC,uBAAuB,QAAQ,+BAA2B;AAgBnE;;;;;;;;CAQC,GACD,OAAO,SAASC,oBACdC,KAAmB,EACnBC,UAAiC,CAAC,CAAC;QAGWA;IAD9C,MAAM,CAACC,MAAMC,QAAQ,GAAGN,SAAS;IACjC,MAAM,CAACO,UAAUC,iBAAiB,GAAGR,UAASI,oBAAAA,QAAQG,QAAQ,YAAhBH,oBAAoBH;IAClE,MAAMQ,WAAWC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,IAAI,CAACT,MAAMU,MAAM,GAAGN,YAAY;IAClET,UAAU;QACR,IAAIO,OAAOI,UAAUH,QAAQG;IAC/B,GAAG;QAACJ;QAAMI;KAAS;IACnB,MAAMK,UAAUJ,KAAKK,GAAG,CAACV,MAAMI;IAC/B,MAAMO,YAAYjB,QAChB,IAAMI,MAAMc,KAAK,CAACH,UAAUP,UAAU,AAACO,CAAAA,UAAU,CAAA,IAAKP,WACtD;QAACJ;QAAOW;QAASP;KAAS;IAE5B,MAAMW,cAAcrB,YAAY,CAACsB;QAC/BX,iBAAiBW;QACjBb,QAAQ;IACV,GAAG,EAAE;IACL,OAAO;QACLD,MAAMS;QACNP;QACAS;QACAV;QACAY;QACAE,iBAAiB;YACff,MAAMS;YACNP;YACAc,UAAUL,UAAUH,MAAM;YAC1BS,OAAOnB,MAAMU,MAAM;YACnBU,cAAcjB;YACdkB,kBAAkBN;QACpB;IACF;AACF"}
@@ -0,0 +1,16 @@
1
+ export interface UseInViewOptions {
2
+ /** Stay true once the element has been seen (default), for work done once. */
3
+ once?: boolean;
4
+ /** How far outside the viewport counts as in view, as a CSS margin. */
5
+ rootMargin?: string;
6
+ }
7
+ /**
8
+ * Whether an element is on screen, through an `IntersectionObserver`: hand
9
+ * the returned ref to the element, and start what it needs — an image, a
10
+ * font, a read — once `inView` turns true.
11
+ *
12
+ * Where the browser has no observer (a test, an old engine) the element
13
+ * counts as in view at once, so the work runs rather than never.
14
+ */
15
+ export declare function useInView<T extends Element = HTMLElement>(options?: UseInViewOptions): [(node: T | null) => void, boolean];
16
+ export default useInView;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { useCallback, useEffect, useRef, useState } from "react";
18
+ /**
19
+ * Whether an element is on screen, through an `IntersectionObserver`: hand
20
+ * the returned ref to the element, and start what it needs — an image, a
21
+ * font, a read — once `inView` turns true.
22
+ *
23
+ * Where the browser has no observer (a test, an old engine) the element
24
+ * counts as in view at once, so the work runs rather than never.
25
+ */ export function useInView(options = {}) {
26
+ const { once = true, rootMargin = '0px' } = options;
27
+ const [inView, setInView] = useState(false);
28
+ const observer = useRef(null);
29
+ useEffect(()=>()=>{
30
+ var _observer_current;
31
+ return (_observer_current = observer.current) == null ? void 0 : _observer_current.disconnect();
32
+ }, []);
33
+ const ref = useCallback((node)=>{
34
+ var _observer_current;
35
+ (_observer_current = observer.current) == null ? void 0 : _observer_current.disconnect();
36
+ observer.current = null;
37
+ if (!node) return;
38
+ if (typeof IntersectionObserver === 'undefined') {
39
+ setInView(true);
40
+ return;
41
+ }
42
+ const watcher = new IntersectionObserver((entries)=>{
43
+ const visible = entries.some((entry)=>entry.isIntersecting);
44
+ if (visible && once) {
45
+ setInView(true);
46
+ watcher.disconnect();
47
+ } else if (!once) {
48
+ setInView(visible);
49
+ }
50
+ }, {
51
+ rootMargin
52
+ });
53
+ watcher.observe(node);
54
+ observer.current = watcher;
55
+ }, [
56
+ once,
57
+ rootMargin
58
+ ]);
59
+ return [
60
+ ref,
61
+ inView
62
+ ];
63
+ }
64
+ export default useInView;
65
+
66
+ //# sourceMappingURL=use-in-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/hooks/use-in-view.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nexport interface UseInViewOptions {\n /** Stay true once the element has been seen (default), for work done once. */\n once?: boolean\n /** How far outside the viewport counts as in view, as a CSS margin. */\n rootMargin?: string\n}\n\n/**\n * Whether an element is on screen, through an `IntersectionObserver`: hand\n * the returned ref to the element, and start what it needs — an image, a\n * font, a read — once `inView` turns true.\n *\n * Where the browser has no observer (a test, an old engine) the element\n * counts as in view at once, so the work runs rather than never.\n */\nexport function useInView<T extends Element = HTMLElement>(\n options: UseInViewOptions = {},\n): [(node: T | null) => void, boolean] {\n const { once = true, rootMargin = '0px' } = options\n const [inView, setInView] = useState(false)\n const observer = useRef<IntersectionObserver | null>(null)\n\n useEffect(() => () => observer.current?.disconnect(), [])\n\n const ref = useCallback(\n (node: T | null) => {\n observer.current?.disconnect()\n observer.current = null\n if (!node) return\n if (typeof IntersectionObserver === 'undefined') {\n setInView(true)\n return\n }\n const watcher = new IntersectionObserver(\n (entries) => {\n const visible = entries.some((entry) => entry.isIntersecting)\n if (visible && once) {\n setInView(true)\n watcher.disconnect()\n } else if (!once) {\n setInView(visible)\n }\n },\n { rootMargin },\n )\n watcher.observe(node)\n observer.current = watcher\n },\n [once, rootMargin],\n )\n\n return [ref, inView]\n}\n\nexport default useInView\n"],"names":["useCallback","useEffect","useRef","useState","useInView","options","once","rootMargin","inView","setInView","observer","current","disconnect","ref","node","IntersectionObserver","watcher","entries","visible","some","entry","isIntersecting","observe"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAShE;;;;;;;CAOC,GACD,OAAO,SAASC,UACdC,UAA4B,CAAC,CAAC;IAE9B,MAAM,EAAEC,OAAO,IAAI,EAAEC,aAAa,KAAK,EAAE,GAAGF;IAC5C,MAAM,CAACG,QAAQC,UAAU,GAAGN,SAAS;IACrC,MAAMO,WAAWR,OAAoC;IAErDD,UAAU,IAAM;gBAAMS;oBAAAA,oBAAAA,SAASC,OAAO,qBAAhBD,kBAAkBE,UAAU;WAAI,EAAE;IAExD,MAAMC,MAAMb,YACV,CAACc;YACCJ;SAAAA,oBAAAA,SAASC,OAAO,qBAAhBD,kBAAkBE,UAAU;QAC5BF,SAASC,OAAO,GAAG;QACnB,IAAI,CAACG,MAAM;QACX,IAAI,OAAOC,yBAAyB,aAAa;YAC/CN,UAAU;YACV;QACF;QACA,MAAMO,UAAU,IAAID,qBAClB,CAACE;YACC,MAAMC,UAAUD,QAAQE,IAAI,CAAC,CAACC,QAAUA,MAAMC,cAAc;YAC5D,IAAIH,WAAWZ,MAAM;gBACnBG,UAAU;gBACVO,QAAQJ,UAAU;YACpB,OAAO,IAAI,CAACN,MAAM;gBAChBG,UAAUS;YACZ;QACF,GACA;YAAEX;QAAW;QAEfS,QAAQM,OAAO,CAACR;QAChBJ,SAASC,OAAO,GAAGK;IACrB,GACA;QAACV;QAAMC;KAAW;IAGpB,OAAO;QAACM;QAAKL;KAAO;AACtB;AAEA,eAAeJ,UAAS"}
@@ -0,0 +1,57 @@
1
+ import type { GridSortDirection, GridSortModel } from '@mui/x-data-grid';
2
+ import { type ListPageSort } from '@aglyn/shared-util-tools/list-query/list-column-sort';
3
+ import type { ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan';
4
+ /** How a column's header sorts, as `ListTable` reads it. */
5
+ export interface ListColumnSortColumn {
6
+ mode: 'query' | 'page';
7
+ sortingOrder: GridSortDirection[];
8
+ }
9
+ export interface ListColumnSort<Row = any> {
10
+ /** The order to ask the query for, or null for the list's own order. */
11
+ sort: ListQuerySort | null;
12
+ sortModel: GridSortModel;
13
+ onSortModelChange: (model: GridSortModel) => void;
14
+ sortingMode: 'server';
15
+ /** The rows to draw: the page, sorted while a page header is active. */
16
+ rows: readonly Row[];
17
+ /** What the reader is told about the order on screen. */
18
+ notices: string[];
19
+ /** Every column that sorts, by field. A column not here does not. */
20
+ columns: Readonly<Record<string, ListColumnSortColumn>>;
21
+ /**
22
+ * A page sort by a comparator a column hands over itself — a plugin column
23
+ * that draws its own header. `null` clears it if it is still that column's.
24
+ */
25
+ sortPage: (id: string, compare: ((a: Row, b: Row) => number) | null) => void;
26
+ /** The comparator column sorting the page, or null. */
27
+ pageSortedBy: string | null;
28
+ }
29
+ export interface ListColumnSortOptions<Row> {
30
+ /**
31
+ * The orders the list's query serves. Each one with a `column` is a header
32
+ * sort; the rest (the default id order) are ignored here. Usually the
33
+ * declaration's `sorts`.
34
+ */
35
+ sorts: readonly ListQuerySort[];
36
+ /** The order asked while no header is: one of `sorts`, or null. */
37
+ defaultSort?: ListQuerySort | null;
38
+ /**
39
+ * The asked order, CONTROLLED — for a list whose query is read before the
40
+ * rows this hook sorts exist (the order feeds the read; the read feeds the
41
+ * rows). Omitted, the hook holds it.
42
+ */
43
+ sort?: ListQuerySort | null;
44
+ onSortChange?: (sort: ListQuerySort | null) => void;
45
+ /**
46
+ * The order the query WILL read in — the plan's `orderBy` — when that can
47
+ * differ from the one asked (an `alone` fallback, a range filter). The
48
+ * header shows that order, not the one clicked.
49
+ */
50
+ orderBy?: ListQuerySort | null;
51
+ rows: readonly Row[];
52
+ /** Columns sorted over the loaded page (strategy 3): field → value. */
53
+ pageSorts?: Readonly<Record<string, ListPageSort<Row>>>;
54
+ /** Headers, for the page-sort notice: field → label. */
55
+ headers?: Readonly<Record<string, string>>;
56
+ }
57
+ export declare function useListColumnSort<Row>(options: ListColumnSortOptions<Row>): ListColumnSort<Row>;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { useCallback, useMemo, useState } from "react";
18
+ import { listPageSortNotice, sortListRows } from "@aglyn/shared-util-tools/list-query/list-column-sort";
19
+ const same = (a, b)=>Boolean(a && b && a.path === b.path && a.direction === b.direction);
20
+ export function useListColumnSort(options) {
21
+ var _options_sort;
22
+ const { sorts, defaultSort = null, orderBy, rows, pageSorts, headers, onSortChange } = options;
23
+ const [heldSort, setHeldSort] = useState(defaultSort);
24
+ const controlled = options.sort !== undefined;
25
+ const querySort = controlled ? (_options_sort = options.sort) != null ? _options_sort : null : heldSort;
26
+ const setQuerySort = useCallback((next)=>{
27
+ if (!controlled) setHeldSort(next);
28
+ onSortChange == null ? void 0 : onSortChange(next);
29
+ }, [
30
+ controlled,
31
+ onSortChange
32
+ ]);
33
+ const [active, setActive] = useState(null);
34
+ const columns = useMemo(()=>{
35
+ const byField = {};
36
+ for (const sort of sorts){
37
+ var _byField, _sort_column, _;
38
+ if (!sort.column) continue;
39
+ const entry = (_ = (_byField = byField)[_sort_column = sort.column]) != null ? _ : _byField[_sort_column] = {
40
+ mode: 'query',
41
+ sortingOrder: []
42
+ };
43
+ if (!entry.sortingOrder.includes(sort.direction)) entry.sortingOrder.push(sort.direction);
44
+ }
45
+ for (const entry of Object.entries(byField)){
46
+ // A header that IS the default order has nowhere to clear to.
47
+ if (entry[0] !== (defaultSort == null ? void 0 : defaultSort.column)) entry[1].sortingOrder.push(null);
48
+ }
49
+ for (const field of Object.keys(pageSorts != null ? pageSorts : {})){
50
+ var _byField1, _field, _1;
51
+ (_1 = (_byField1 = byField)[_field = field]) != null ? _1 : _byField1[_field] = {
52
+ mode: 'page',
53
+ sortingOrder: [
54
+ 'asc',
55
+ 'desc',
56
+ null
57
+ ]
58
+ };
59
+ }
60
+ return byField;
61
+ }, [
62
+ sorts,
63
+ pageSorts,
64
+ defaultSort
65
+ ]);
66
+ const shown = orderBy != null ? orderBy : querySort;
67
+ const sortModel = useMemo(()=>{
68
+ if ((active == null ? void 0 : active.kind) === 'page') return [
69
+ {
70
+ field: active.field,
71
+ sort: active.direction
72
+ }
73
+ ];
74
+ if ((active == null ? void 0 : active.kind) === 'compare') return [];
75
+ const header = sorts.find((sort)=>sort.column && same(sort, shown));
76
+ return (header == null ? void 0 : header.column) ? [
77
+ {
78
+ field: header.column,
79
+ sort: header.direction
80
+ }
81
+ ] : [];
82
+ }, [
83
+ active,
84
+ sorts,
85
+ shown
86
+ ]);
87
+ const onSortModelChange = useCallback((model)=>{
88
+ var _columns_first_field;
89
+ const [first] = model;
90
+ const direction = first == null ? void 0 : first.sort;
91
+ if (first && direction && ((_columns_first_field = columns[first.field]) == null ? void 0 : _columns_first_field.mode) === 'page') {
92
+ // The query's order stands: a page sort re-reads nothing.
93
+ setActive({
94
+ kind: 'page',
95
+ field: first.field,
96
+ direction
97
+ });
98
+ return;
99
+ }
100
+ setActive(null);
101
+ setQuerySort(first && direction && sorts.find((sort)=>sort.column === first.field && sort.direction === direction) || defaultSort);
102
+ }, [
103
+ columns,
104
+ sorts,
105
+ defaultSort,
106
+ setQuerySort
107
+ ]);
108
+ const sortPage = useCallback((id, compare)=>{
109
+ setActive((current)=>{
110
+ if (compare) {
111
+ return (current == null ? void 0 : current.kind) === 'compare' && current.id === id && current.compare === compare ? current : {
112
+ kind: 'compare',
113
+ id,
114
+ compare
115
+ };
116
+ }
117
+ return (current == null ? void 0 : current.kind) === 'compare' && current.id === id ? null : current;
118
+ });
119
+ }, []);
120
+ const sorted = useMemo(()=>{
121
+ if ((active == null ? void 0 : active.kind) === 'compare') return [
122
+ ...rows
123
+ ].sort(active.compare);
124
+ if ((active == null ? void 0 : active.kind) === 'page') {
125
+ const value = pageSorts == null ? void 0 : pageSorts[active.field];
126
+ return value ? sortListRows(rows, value, active.direction) : rows;
127
+ }
128
+ return rows;
129
+ }, [
130
+ rows,
131
+ active,
132
+ pageSorts
133
+ ]);
134
+ const notices = useMemo(()=>{
135
+ var _ref;
136
+ return (active == null ? void 0 : active.kind) === 'page' ? [
137
+ listPageSortNotice((_ref = headers == null ? void 0 : headers[active.field]) != null ? _ref : active.field)
138
+ ] : [];
139
+ }, [
140
+ active,
141
+ headers
142
+ ]);
143
+ return {
144
+ sort: querySort,
145
+ sortModel,
146
+ onSortModelChange,
147
+ sortingMode: 'server',
148
+ rows: sorted,
149
+ notices,
150
+ columns,
151
+ sortPage,
152
+ pageSortedBy: (active == null ? void 0 : active.kind) === 'compare' ? active.id : null
153
+ };
154
+ }
155
+
156
+ //# sourceMappingURL=use-list-column-sort.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/hooks/use-list-column-sort.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { GridSortDirection, GridSortModel } from '@mui/x-data-grid'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n type ListPageSort,\n listPageSortNotice,\n sortListRows,\n} from '@aglyn/shared-util-tools/list-query/list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * A LIST'S HEADER SORTS, ONE HOOK (AGL-3680).\n *\n * Bridges MUI's `GridSortModel` and the list's `ListQuerySort`s, the way the\n * staff Organizations list did by hand: a header backed by a query order asks\n * the QUERY for it (`sort`, which the caller hands its query or route), and\n * a header backed only by a value on the loaded rows sorts the PAGE\n * (`pageSorts`) and says so. The grid itself never sorts — `sortingMode` is\n * always `server` — so nothing on screen claims an order the query did not\n * give. See `list-column-sort.ts` in `@aglyn/shared-util-tools` for which\n * strategy a column takes.\n *\n * const columnSort = useListColumnSort({ sorts, defaultSort, rows, pageSorts, headers })\n * // ask the query for `columnSort.sort`; draw\n * <ListTable columns={…} rows={columnSort.rows} columnSort={columnSort} … />\n */\n\n/** How a column's header sorts, as `ListTable` reads it. */\nexport interface ListColumnSortColumn {\n mode: 'query' | 'page'\n sortingOrder: GridSortDirection[]\n}\n\nexport interface ListColumnSort<Row = any> {\n /** The order to ask the query for, or null for the list's own order. */\n sort: ListQuerySort | null\n sortModel: GridSortModel\n onSortModelChange: (model: GridSortModel) => void\n sortingMode: 'server'\n /** The rows to draw: the page, sorted while a page header is active. */\n rows: readonly Row[]\n /** What the reader is told about the order on screen. */\n notices: string[]\n /** Every column that sorts, by field. A column not here does not. */\n columns: Readonly<Record<string, ListColumnSortColumn>>\n /**\n * A page sort by a comparator a column hands over itself — a plugin column\n * that draws its own header. `null` clears it if it is still that column's.\n */\n sortPage: (id: string, compare: ((a: Row, b: Row) => number) | null) => void\n /** The comparator column sorting the page, or null. */\n pageSortedBy: string | null\n}\n\nexport interface ListColumnSortOptions<Row> {\n /**\n * The orders the list's query serves. Each one with a `column` is a header\n * sort; the rest (the default id order) are ignored here. Usually the\n * declaration's `sorts`.\n */\n sorts: readonly ListQuerySort[]\n /** The order asked while no header is: one of `sorts`, or null. */\n defaultSort?: ListQuerySort | null\n /**\n * The asked order, CONTROLLED — for a list whose query is read before the\n * rows this hook sorts exist (the order feeds the read; the read feeds the\n * rows). Omitted, the hook holds it.\n */\n sort?: ListQuerySort | null\n onSortChange?: (sort: ListQuerySort | null) => void\n /**\n * The order the query WILL read in — the plan's `orderBy` — when that can\n * differ from the one asked (an `alone` fallback, a range filter). The\n * header shows that order, not the one clicked.\n */\n orderBy?: ListQuerySort | null\n rows: readonly Row[]\n /** Columns sorted over the loaded page (strategy 3): field → value. */\n pageSorts?: Readonly<Record<string, ListPageSort<Row>>>\n /** Headers, for the page-sort notice: field → label. */\n headers?: Readonly<Record<string, string>>\n}\n\ntype Active<Row> =\n | { kind: 'page'; field: string; direction: 'asc' | 'desc' }\n | { kind: 'compare'; id: string; compare: (a: Row, b: Row) => number }\n | null\n\nconst same = (a: ListQuerySort | null | undefined, b: ListQuerySort | null | undefined) =>\n Boolean(a && b && a.path === b.path && a.direction === b.direction)\n\nexport function useListColumnSort<Row>(options: ListColumnSortOptions<Row>): ListColumnSort<Row> {\n const { sorts, defaultSort = null, orderBy, rows, pageSorts, headers, onSortChange } = options\n const [heldSort, setHeldSort] = useState<ListQuerySort | null>(defaultSort)\n const controlled = options.sort !== undefined\n const querySort = controlled ? (options.sort ?? null) : heldSort\n const setQuerySort = useCallback(\n (next: ListQuerySort | null) => {\n if (!controlled) setHeldSort(next)\n onSortChange?.(next)\n },\n [controlled, onSortChange],\n )\n const [active, setActive] = useState<Active<Row>>(null)\n\n const columns = useMemo(() => {\n const byField: Record<string, ListColumnSortColumn> = {}\n for (const sort of sorts) {\n if (!sort.column) continue\n const entry = (byField[sort.column] ??= { mode: 'query', sortingOrder: [] })\n if (!entry.sortingOrder.includes(sort.direction)) entry.sortingOrder.push(sort.direction)\n }\n for (const entry of Object.entries(byField)) {\n // A header that IS the default order has nowhere to clear to.\n if (entry[0] !== defaultSort?.column) entry[1].sortingOrder.push(null)\n }\n for (const field of Object.keys(pageSorts ?? {})) {\n byField[field] ??= { mode: 'page', sortingOrder: ['asc', 'desc', null] }\n }\n return byField\n }, [sorts, pageSorts, defaultSort])\n\n const shown = orderBy ?? querySort\n const sortModel = useMemo<GridSortModel>(() => {\n if (active?.kind === 'page') return [{ field: active.field, sort: active.direction }]\n if (active?.kind === 'compare') return []\n const header = sorts.find((sort) => sort.column && same(sort, shown))\n return header?.column ? [{ field: header.column, sort: header.direction }] : []\n }, [active, sorts, shown])\n\n const onSortModelChange = useCallback(\n (model: GridSortModel) => {\n const [first] = model\n const direction = first?.sort\n if (first && direction && columns[first.field]?.mode === 'page') {\n // The query's order stands: a page sort re-reads nothing.\n setActive({ kind: 'page', field: first.field, direction })\n return\n }\n setActive(null)\n setQuerySort(\n (first &&\n direction &&\n sorts.find((sort) => sort.column === first.field && sort.direction === direction)) ||\n defaultSort,\n )\n },\n [columns, sorts, defaultSort, setQuerySort],\n )\n\n const sortPage = useCallback((id: string, compare: ((a: Row, b: Row) => number) | null) => {\n setActive((current) => {\n if (compare) {\n return current?.kind === 'compare' && current.id === id && current.compare === compare\n ? current\n : { kind: 'compare', id, compare }\n }\n return current?.kind === 'compare' && current.id === id ? null : current\n })\n }, [])\n\n const sorted = useMemo(() => {\n if (active?.kind === 'compare') return [...rows].sort(active.compare)\n if (active?.kind === 'page') {\n const value = pageSorts?.[active.field]\n return value ? sortListRows(rows, value, active.direction) : rows\n }\n return rows\n }, [rows, active, pageSorts])\n\n const notices = useMemo(\n () =>\n active?.kind === 'page'\n ? [listPageSortNotice(headers?.[active.field] ?? active.field)]\n : [],\n [active, headers],\n )\n\n return {\n sort: querySort,\n sortModel,\n onSortModelChange,\n sortingMode: 'server',\n rows: sorted,\n notices,\n columns,\n sortPage,\n pageSortedBy: active?.kind === 'compare' ? active.id : null,\n }\n}\n"],"names":["useCallback","useMemo","useState","listPageSortNotice","sortListRows","same","a","b","Boolean","path","direction","useListColumnSort","options","sorts","defaultSort","orderBy","rows","pageSorts","headers","onSortChange","heldSort","setHeldSort","controlled","sort","undefined","querySort","setQuerySort","next","active","setActive","columns","byField","column","entry","mode","sortingOrder","includes","push","Object","entries","field","keys","shown","sortModel","kind","header","find","onSortModelChange","model","first","sortPage","id","compare","current","sorted","value","notices","sortingMode","pageSortedBy"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAASA,WAAW,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACtD,SAEEC,kBAAkB,EAClBC,YAAY,QACP,uDAAsD;AAiF7D,MAAMC,OAAO,CAACC,GAAqCC,IACjDC,QAAQF,KAAKC,KAAKD,EAAEG,IAAI,KAAKF,EAAEE,IAAI,IAAIH,EAAEI,SAAS,KAAKH,EAAEG,SAAS;AAEpE,OAAO,SAASC,kBAAuBC,OAAmC;QAIxCA;IAHhC,MAAM,EAAEC,KAAK,EAAEC,cAAc,IAAI,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,OAAO,EAAEC,YAAY,EAAE,GAAGP;IACvF,MAAM,CAACQ,UAAUC,YAAY,GAAGnB,SAA+BY;IAC/D,MAAMQ,aAAaV,QAAQW,IAAI,KAAKC;IACpC,MAAMC,YAAYH,cAAcV,gBAAAA,QAAQW,IAAI,YAAZX,gBAAgB,OAAQQ;IACxD,MAAMM,eAAe1B,YACnB,CAAC2B;QACC,IAAI,CAACL,YAAYD,YAAYM;QAC7BR,gCAAAA,aAAeQ;IACjB,GACA;QAACL;QAAYH;KAAa;IAE5B,MAAM,CAACS,QAAQC,UAAU,GAAG3B,SAAsB;IAElD,MAAM4B,UAAU7B,QAAQ;QACtB,MAAM8B,UAAgD,CAAC;QACvD,KAAK,MAAMR,QAAQV,MAAO;gBAETkB,UAAQR,cAARQ;YADf,IAAI,CAACR,KAAKS,MAAM,EAAE;YAClB,MAAMC,SAASF,IAAAA,CAAAA,WAAAA,QAAO,CAACR,eAAAA,KAAKS,MAAM,CAAC,YAApBD,IAAAA,QAAO,CAACR,aAAY,GAAK;gBAAEW,MAAM;gBAASC,cAAc,EAAE;YAAC;YAC1E,IAAI,CAACF,MAAME,YAAY,CAACC,QAAQ,CAACb,KAAKb,SAAS,GAAGuB,MAAME,YAAY,CAACE,IAAI,CAACd,KAAKb,SAAS;QAC1F;QACA,KAAK,MAAMuB,SAASK,OAAOC,OAAO,CAACR,SAAU;YAC3C,8DAA8D;YAC9D,IAAIE,KAAK,CAAC,EAAE,MAAKnB,+BAAAA,YAAakB,MAAM,GAAEC,KAAK,CAAC,EAAE,CAACE,YAAY,CAACE,IAAI,CAAC;QACnE;QACA,KAAK,MAAMG,SAASF,OAAOG,IAAI,CAACxB,oBAAAA,YAAa,CAAC,GAAI;gBAChDc,WAAQS,QAART;aAAAA,KAAAA,CAAAA,YAAAA,QAAO,CAACS,SAAAA,MAAM,YAAdT,KAAAA,SAAO,CAACS,OAAM,GAAK;gBAAEN,MAAM;gBAAQC,cAAc;oBAAC;oBAAO;oBAAQ;iBAAK;YAAC;QACzE;QACA,OAAOJ;IACT,GAAG;QAAClB;QAAOI;QAAWH;KAAY;IAElC,MAAM4B,QAAQ3B,kBAAAA,UAAWU;IACzB,MAAMkB,YAAY1C,QAAuB;QACvC,IAAI2B,CAAAA,0BAAAA,OAAQgB,IAAI,MAAK,QAAQ,OAAO;YAAC;gBAAEJ,OAAOZ,OAAOY,KAAK;gBAAEjB,MAAMK,OAAOlB,SAAS;YAAC;SAAE;QACrF,IAAIkB,CAAAA,0BAAAA,OAAQgB,IAAI,MAAK,WAAW,OAAO,EAAE;QACzC,MAAMC,SAAShC,MAAMiC,IAAI,CAAC,CAACvB,OAASA,KAAKS,MAAM,IAAI3B,KAAKkB,MAAMmB;QAC9D,OAAOG,CAAAA,0BAAAA,OAAQb,MAAM,IAAG;YAAC;gBAAEQ,OAAOK,OAAOb,MAAM;gBAAET,MAAMsB,OAAOnC,SAAS;YAAC;SAAE,GAAG,EAAE;IACjF,GAAG;QAACkB;QAAQf;QAAO6B;KAAM;IAEzB,MAAMK,oBAAoB/C,YACxB,CAACgD;YAG2BlB;QAF1B,MAAM,CAACmB,MAAM,GAAGD;QAChB,MAAMtC,YAAYuC,yBAAAA,MAAO1B,IAAI;QAC7B,IAAI0B,SAASvC,aAAaoB,EAAAA,uBAAAA,OAAO,CAACmB,MAAMT,KAAK,CAAC,qBAApBV,qBAAsBI,IAAI,MAAK,QAAQ;YAC/D,0DAA0D;YAC1DL,UAAU;gBAAEe,MAAM;gBAAQJ,OAAOS,MAAMT,KAAK;gBAAE9B;YAAU;YACxD;QACF;QACAmB,UAAU;QACVH,aACE,AAACuB,SACCvC,aACAG,MAAMiC,IAAI,CAAC,CAACvB,OAASA,KAAKS,MAAM,KAAKiB,MAAMT,KAAK,IAAIjB,KAAKb,SAAS,KAAKA,cACvEI;IAEN,GACA;QAACgB;QAASjB;QAAOC;QAAaY;KAAa;IAG7C,MAAMwB,WAAWlD,YAAY,CAACmD,IAAYC;QACxCvB,UAAU,CAACwB;YACT,IAAID,SAAS;gBACX,OAAOC,CAAAA,2BAAAA,QAAST,IAAI,MAAK,aAAaS,QAAQF,EAAE,KAAKA,MAAME,QAAQD,OAAO,KAAKA,UAC3EC,UACA;oBAAET,MAAM;oBAAWO;oBAAIC;gBAAQ;YACrC;YACA,OAAOC,CAAAA,2BAAAA,QAAST,IAAI,MAAK,aAAaS,QAAQF,EAAE,KAAKA,KAAK,OAAOE;QACnE;IACF,GAAG,EAAE;IAEL,MAAMC,SAASrD,QAAQ;QACrB,IAAI2B,CAAAA,0BAAAA,OAAQgB,IAAI,MAAK,WAAW,OAAO;eAAI5B;SAAK,CAACO,IAAI,CAACK,OAAOwB,OAAO;QACpE,IAAIxB,CAAAA,0BAAAA,OAAQgB,IAAI,MAAK,QAAQ;YAC3B,MAAMW,QAAQtC,6BAAAA,SAAW,CAACW,OAAOY,KAAK,CAAC;YACvC,OAAOe,QAAQnD,aAAaY,MAAMuC,OAAO3B,OAAOlB,SAAS,IAAIM;QAC/D;QACA,OAAOA;IACT,GAAG;QAACA;QAAMY;QAAQX;KAAU;IAE5B,MAAMuC,UAAUvD,QACd;;eACE2B,CAAAA,0BAAAA,OAAQgB,IAAI,MAAK,SACb;YAACzC,2BAAmBe,2BAAAA,OAAS,CAACU,OAAOY,KAAK,CAAC,mBAAIZ,OAAOY,KAAK;SAAE,GAC7D,EAAE;OACR;QAACZ;QAAQV;KAAQ;IAGnB,OAAO;QACLK,MAAME;QACNkB;QACAI;QACAU,aAAa;QACbzC,MAAMsC;QACNE;QACA1B;QACAoB;QACAQ,cAAc9B,CAAAA,0BAAAA,OAAQgB,IAAI,MAAK,YAAYhB,OAAOuB,EAAE,GAAG;IACzD;AACF"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * The height that takes an element from where it starts on the page to the
19
+ * bottom of the viewport.
20
+ *
21
+ * `100dvh` is the whole window, so a full-screen tool placed under the
22
+ * console's header, site nav, notices and page title came out taller than
23
+ * the screen by exactly that chrome, and the page scrolled to reach its own
24
+ * bottom (the POS register). This measures the element's top in DOCUMENT
25
+ * coordinates, which its own height never changes, so resizing to the
26
+ * answer cannot feed back into the measurement. It re-measures when the
27
+ * window resizes and when anything above it changes size (a notice
28
+ * dismissed, the nav wrapping).
29
+ *
30
+ * Until the first measurement (and on the server) it answers `fallback`.
31
+ */
32
+ export declare function useViewportFill(options?: {
33
+ min?: number;
34
+ fallback?: string;
35
+ }): {
36
+ ref: (node: HTMLElement | null) => void;
37
+ height: string;
38
+ };
39
+ export default useViewportFill;