@mintplayer/ng-spark 22.23.0 → 22.25.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 (71) hide show
  1. package/README.md +196 -0
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +100 -47
  3. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +310 -329
  5. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +175 -135
  7. package/fesm2022/mintplayer-ng-spark-column-filter.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-contributions.mjs +721 -0
  9. package/fesm2022/mintplayer-ng-spark-contributions.mjs.map +1 -0
  10. package/fesm2022/mintplayer-ng-spark-grid.mjs +2385 -1118
  11. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  12. package/fesm2022/mintplayer-ng-spark-history.mjs +598 -0
  13. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -0
  14. package/fesm2022/mintplayer-ng-spark-icon.mjs +64 -27
  15. package/fesm2022/mintplayer-ng-spark-icon.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-models.mjs +405 -762
  17. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  18. package/fesm2022/mintplayer-ng-spark-moderation.mjs +1212 -0
  19. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -0
  20. package/fesm2022/mintplayer-ng-spark-panels.mjs +60 -0
  21. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -0
  22. package/fesm2022/mintplayer-ng-spark-pipes.mjs +1158 -765
  23. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-create.mjs +273 -169
  25. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +795 -410
  27. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +1196 -280
  29. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-po-form.mjs +2244 -1224
  31. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-query-list.mjs +571 -303
  33. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-renderers.mjs +44 -55
  35. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +216 -177
  37. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-routes.mjs +24 -28
  39. package/fesm2022/mintplayer-ng-spark-routes.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-services.mjs +625 -640
  41. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-shell.mjs +1031 -397
  43. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  44. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +294 -0
  45. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -0
  46. package/fesm2022/mintplayer-ng-spark.mjs +14 -48
  47. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  48. package/package.json +23 -2
  49. package/types/mintplayer-ng-spark-attribute-description.d.ts +25 -18
  50. package/types/mintplayer-ng-spark-client-operations.d.ts +139 -160
  51. package/types/mintplayer-ng-spark-column-filter.d.ts +73 -55
  52. package/types/mintplayer-ng-spark-contributions.d.ts +231 -0
  53. package/types/mintplayer-ng-spark-grid.d.ts +917 -448
  54. package/types/mintplayer-ng-spark-history.d.ts +216 -0
  55. package/types/mintplayer-ng-spark-icon.d.ts +15 -13
  56. package/types/mintplayer-ng-spark-models.d.ts +681 -573
  57. package/types/mintplayer-ng-spark-moderation.d.ts +402 -0
  58. package/types/mintplayer-ng-spark-panels.d.ts +143 -0
  59. package/types/mintplayer-ng-spark-pipes.d.ts +131 -149
  60. package/types/mintplayer-ng-spark-po-create.d.ts +46 -37
  61. package/types/mintplayer-ng-spark-po-detail.d.ts +180 -117
  62. package/types/mintplayer-ng-spark-po-edit.d.ts +367 -64
  63. package/types/mintplayer-ng-spark-po-form.d.ts +558 -373
  64. package/types/mintplayer-ng-spark-query-list.d.ts +143 -82
  65. package/types/mintplayer-ng-spark-renderers.d.ts +121 -85
  66. package/types/mintplayer-ng-spark-retry-action-modal.d.ts +31 -34
  67. package/types/mintplayer-ng-spark-routes.d.ts +7 -11
  68. package/types/mintplayer-ng-spark-services.d.ts +377 -261
  69. package/types/mintplayer-ng-spark-shell.d.ts +228 -132
  70. package/types/mintplayer-ng-spark-soft-delete.d.ts +90 -0
  71. package/types/mintplayer-ng-spark.d.ts +19 -12
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mintplayer-ng-spark-moderation.mjs","names":[],"sources":["../../moderation/src/spark-moderation.service.ts","../../moderation/src/spark-moderation.tokens.ts","../../moderation/src/spark-vote.component.ts","../../moderation/src/spark-flag-button.component.ts","../../moderation/src/spark-moderator-panel.component.ts","../../moderation/src/spark-reputation-badge.component.ts","../../moderation/src/spark-review-queue.component.ts","../../moderation/src/spark-review-queue-link.component.ts","../../moderation/src/provide-spark-moderation.ts"],"sourcesContent":["import { computed, inject, Injectable, signal } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router } from '@angular/router';\nimport { EMPTY, filter } from 'rxjs';\nimport { SparkService } from '@mintplayer/ng-spark/services';\nimport {\n ModerationAuditEntry,\n ModerationCaseDetail,\n ModerationCaseSummary,\n ModerationReputation,\n ModerationReputationLine,\n ModerationStatus,\n ModerationVoteState,\n} from './spark-moderation.models';\n\n/**\n * Client for `MintPlayer.Spark.Moderation`'s endpoints (`/spark/moderation/*`, #460). Every call is\n * a POST with the ids in the body — the route table is literal and RavenDB ids contain slashes.\n *\n * A target the caller cannot see (hidden, missing, not moderatable) is the standard refusal: 404\n * (or 401 when signing in would help). A rule (own post, locked, suspended) is a 400 with `errors`,\n * a quota (votes per day) a 429 whose `result.retryAfterSeconds` says when to try again.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkModerationService {\n private readonly spark = inject(SparkService);\n\n // Vote states are fetched in batches: every widget that asks within one macrotask shares a request.\n private readonly pending = new Map<string, Map<string, ((state: ModerationVoteState | null) => void)[]>>();\n private flushScheduled = false;\n\n private readonly router = inject(Router, { optional: true });\n\n readonly #votesCast = signal(0);\n\n /** Counts the caller's accepted votes, so the own reputation badge re-reads (a downvote costs reputation). */\n readonly votesCast = this.#votesCast.asReadonly();\n\n private readonly navigated = toSignal(\n this.router?.events.pipe(filter(e => e instanceof NavigationEnd)) ?? EMPTY,\n { initialValue: null });\n\n /**\n * Changes whenever the caller's own reputation may have changed: after every navigation (a\n * sign-in, a sign-out, another page) and after the caller's own votes. The own badge and the\n * review-queue link read it to re-fetch {@link ownReputation}; a component in the app shell lives\n * for the whole session and would otherwise show the first answer until a full reload.\n */\n // A fresh object each time: returning the vote count alone would compare equal after a navigation\n // and notify nobody.\n readonly ownReputationChanged = computed(() => ({ navigation: this.navigated(), votes: this.#votesCast() }));\n\n #ownInFlight: Promise<ModerationReputation> | null = null;\n\n /**\n * The caller's own reputation, shared by every caller in flight at once: the badge and the\n * review-queue link both re-read on the same navigation and cost one request.\n */\n ownReputation(): Promise<ModerationReputation> {\n return this.#ownInFlight ??= this.reputation().finally(() => this.#ownInFlight = null);\n }\n\n /** Casts (+1 / −1) or withdraws (0) the caller's vote; resolves to the target's new state. */\n async vote(type: string, id: string, direction: -1 | 0 | 1): Promise<ModerationVoteState> {\n const state = await this.spark.postEnvelope<ModerationVoteState>('/moderation/vote', { objectTypeId: type, id, direction });\n this.#votesCast.update(n => n + 1);\n return state;\n }\n\n /**\n * The score and the caller's vote for one target, batched with every other call made in the same\n * macrotask for the same type (one `POST /spark/moderation/votes`, ≤ 100 ids). Resolves to `null`\n * for an id the server left out (not visible to the caller).\n */\n voteState(type: string, id: string): Promise<ModerationVoteState | null> {\n return new Promise(resolve => {\n let byId = this.pending.get(type);\n if (!byId) this.pending.set(type, (byId = new Map()));\n const waiting = byId.get(id) ?? [];\n waiting.push(resolve);\n byId.set(id, waiting);\n if (!this.flushScheduled) {\n this.flushScheduled = true;\n setTimeout(() => this.flush(), 0);\n }\n });\n }\n\n /** `POST /spark/moderation/votes` for up to 100 ids of one type. */\n votes(type: string, ids: string[]): Promise<ModerationVoteState[]> {\n return this.spark.postEnvelope<ModerationVoteState[]>('/moderation/votes', { objectTypeId: type, ids });\n }\n\n flag(type: string, id: string, reason: string): Promise<void> {\n return this.spark.postEnvelope<void>('/moderation/flag', { objectTypeId: type, id, reason });\n }\n\n lock(type: string, id: string, reason?: string): Promise<void> {\n return this.spark.postEnvelope<void>('/moderation/lock', { objectTypeId: type, id, reason });\n }\n\n unlock(type: string, id: string): Promise<void> {\n return this.spark.postEnvelope<void>('/moderation/unlock', { objectTypeId: type, id });\n }\n\n /** What the moderator panel shows about a target, including which tools the caller holds. */\n status(type: string, id: string): Promise<ModerationStatus> {\n return this.spark.postEnvelope<ModerationStatus>('/moderation/status', { objectTypeId: type, id });\n }\n\n /** A user's reputation; the caller's own when `userId` is omitted (with privileges and suspension). */\n reputation(userId?: string): Promise<ModerationReputation> {\n return this.spark.postEnvelope<ModerationReputation>('/moderation/reputation', userId ? { userId } : {});\n }\n\n /** The caller's own ledger, newest first. Voters are never named; a reversal reads \"Voting corrected (−N)\". */\n history(take = 100): Promise<ModerationReputationLine[]> {\n return this.spark.postEnvelope<ModerationReputationLine[]>('/moderation/reputation/history', { take });\n }\n\n cases(status: string = 'open', skip = 0, take = 50): Promise<ModerationCaseSummary[]> {\n return this.spark.postEnvelope<ModerationCaseSummary[]>('/moderation/cases', { status, skip, take });\n }\n\n case(caseId: string): Promise<ModerationCaseDetail> {\n return this.spark.postEnvelope<ModerationCaseDetail>('/moderation/case', { caseId });\n }\n\n /** `uphold` / `decline` (flag cases), `dismiss` / `reverse` / `merge` / `suspend` (fraud cases; the last two name `accountId`). */\n decide(caseId: string, decision: string, accountId?: string, reason?: string): Promise<void> {\n return this.spark.postEnvelope<void>('/moderation/case/decide', { caseId, decision, accountId, reason });\n }\n\n suspend(userId: string, days?: number | null, reason?: string): Promise<void> {\n return this.spark.postEnvelope<void>('/moderation/suspend', { userId, days: days ?? null, reason });\n }\n\n unsuspend(userId: string): Promise<void> {\n return this.spark.postEnvelope<void>('/moderation/unsuspend', { userId });\n }\n\n audit(skip = 0, take = 50): Promise<ModerationAuditEntry[]> {\n return this.spark.postEnvelope<ModerationAuditEntry[]>('/moderation/audit', { skip, take });\n }\n\n private flush(): void {\n this.flushScheduled = false;\n const batches = [...this.pending.entries()];\n this.pending.clear();\n for (const [type, byId] of batches) {\n const ids = [...byId.keys()];\n for (let i = 0; i < ids.length; i += 100) {\n const chunk = ids.slice(i, i + 100);\n this.votes(type, chunk).then(\n states => {\n const found = new Map(states.map(s => [s.id, s] as const));\n for (const id of chunk) byId.get(id)?.forEach(resolve => resolve(found.get(id) ?? null));\n },\n () => {\n for (const id of chunk) byId.get(id)?.forEach(resolve => resolve(null));\n });\n }\n }\n }\n}\n","import { InjectionToken } from '@angular/core';\n\n/** Where the review queue is routed; the moderator panel links there. Set by {@link provideSparkModeration}. */\nexport const SPARK_MODERATION_REVIEW_PATH = new InjectionToken<string>('SparkModerationReviewPath', {\n factory: () => '/moderation/review',\n});\n","import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal } from '@angular/core';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { EntityAttributeDefinition, PersistentObject, QueryResultItem, SparkCellColumn } from '@mintplayer/ng-spark/models';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkModerationService } from './spark-moderation.service';\nimport { ModerationVoteState } from './spark-moderation.models';\n\n/**\n * The vote widget (#460): up, score, down. An attribute renderer — register it with\n * {@link sparkModerationRenderers} and give an attribute of an `IModeratable` type\n * `\"renderer\": \"spark-vote\"`. The attribute's own value is ignored: the score comes from\n * `/spark/moderation/votes` (batched per page), never from the document, so a vote never moves the\n * row's etag.\n *\n * An arrow the caller holds no right for (`canUpvote` / `canDownvote` false on the state) is\n * disabled, so a user without the privilege sees the score but is not offered a click that the server\n * refuses — except the active arrow, since withdrawing a vote needs no right.\n *\n * On the detail page the entity type is the object's. In a query grid a row carries no type, so the\n * column needs `\"rendererOptions\": { \"type\": \"<entity type id or alias>\" }`.\n */\n@Component({\n selector: 'spark-vote',\n imports: [TranslateKeyPipe],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (state(); as s) {\n <span class=\"spark-vote d-inline-flex align-items-center gap-1\" [class.spark-vote-locked]=\"s.locked\">\n <button type=\"button\" class=\"btn btn-sm spark-vote-up\" [class.btn-primary]=\"s.myVote === 1\" [class.btn-outline-secondary]=\"s.myVote !== 1\"\n [disabled]=\"busy() || s.locked || (s.canUpvote === false && s.myVote !== 1)\" [attr.aria-pressed]=\"s.myVote === 1\" [attr.aria-label]=\"'moderation.upvote' | t\" (click)=\"cast(1)\">▲</button>\n <span class=\"spark-vote-score fw-bold\" [attr.aria-label]=\"'moderation.score' | t\">{{ s.score }}</span>\n <button type=\"button\" class=\"btn btn-sm spark-vote-down\" [class.btn-danger]=\"s.myVote === -1\" [class.btn-outline-secondary]=\"s.myVote !== -1\"\n [disabled]=\"busy() || s.locked || (s.canDownvote === false && s.myVote !== -1)\" [attr.aria-pressed]=\"s.myVote === -1\" [attr.aria-label]=\"'moderation.downvote' | t\" (click)=\"cast(-1)\">▼</button>\n @if (error(); as message) {\n <span class=\"text-danger small spark-vote-error\" role=\"alert\">{{ message }}</span>\n }\n </span>\n }\n `,\n})\nexport class SparkVoteComponent {\n private readonly moderation = inject(SparkModerationService);\n\n // Renderer inputs (all optional: the hosts pass only what a renderer declares).\n value = input<unknown>();\n attribute = input<EntityAttributeDefinition | undefined>();\n column = input<SparkCellColumn | undefined>();\n options = input<Record<string, any> | undefined>();\n item = input<PersistentObject | QueryResultItem | Record<string, any> | undefined>();\n\n protected readonly state = signal<ModerationVoteState | null>(null);\n protected readonly busy = signal(false);\n protected readonly error = signal<string | null>(null);\n\n protected readonly target = computed(() => {\n const item = this.item() as Record<string, any> | undefined;\n const id = item?.['id'] as string | undefined;\n const type = (item?.['objectTypeId'] as string | undefined) ?? (this.options()?.['type'] as string | undefined);\n return id && type ? { id, type } : null;\n });\n\n constructor() {\n effect(() => {\n const target = this.target();\n if (!target) return;\n this.moderation.voteState(target.type, target.id).then(s => this.state.set(s));\n });\n }\n\n async cast(direction: 1 | -1): Promise<void> {\n const target = this.target();\n const current = this.state();\n if (!target || !current || this.busy()) return;\n // Clicking the active arrow withdraws the vote.\n const next = current.myVote === direction ? 0 : direction;\n this.busy.set(true);\n this.error.set(null);\n try {\n this.state.set(await this.moderation.vote(target.type, target.id, next));\n } catch (e) {\n this.error.set(describeModerationError(e));\n } finally {\n this.busy.set(false);\n }\n }\n}\n\n/** A readable message for a refused moderation call (400 errors, 429 quota, 404 refusal). */\nexport function describeModerationError(e: unknown): string {\n const err = e as HttpErrorResponse;\n const body = err?.error as { result?: { errors?: { errorMessage?: unknown }[]; error?: string } } | undefined;\n const first = body?.result?.errors?.[0]?.errorMessage;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object') {\n const values = Object.values(first as Record<string, string>);\n if (values.length) return values[0];\n }\n return body?.result?.error ?? err?.message ?? 'Failed';\n}\n","import { ChangeDetectionStrategy, Component, inject, input, signal } from '@angular/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport { SparkDetailContext } from '@mintplayer/ng-spark/panels';\nimport { SparkModerationService } from './spark-moderation.service';\nimport { describeModerationError } from './spark-vote.component';\n\n/**\n * **Flag** on the detail page of every object (#460). Registered by {@link provideSparkModeration}\n * as a `SPARK_DETAIL_ACTIONS` entry. The button is offered to every signed-in viewer of a live row;\n * the server decides (`Flag/T`, a visible moderatable row) and answers a refusal inline — the\n * permissions endpoint does not report moderation rights, and a type that is not moderatable answers\n * like a missing row, so the button cannot know in advance.\n */\n@Component({\n selector: 'spark-flag-button',\n imports: [TranslateKeyPipe],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (context().deleted !== 'only') {\n <span class=\"d-inline-flex gap-2 align-items-center\">\n <button type=\"button\" class=\"btn btn-outline-warning spark-flag\" [disabled]=\"busy() || done()\" (click)=\"flag()\">\n {{ 'moderation.flag' | t }}\n </button>\n @if (done()) {\n <span class=\"small text-success spark-flag-done\">{{ 'moderation.flagged' | t }}</span>\n }\n @if (error(); as message) {\n <span class=\"small text-danger spark-flag-error\" role=\"alert\">{{ message }}</span>\n }\n </span>\n }\n `,\n})\nexport class SparkFlagButtonComponent {\n private readonly moderation = inject(SparkModerationService);\n private readonly lang = inject(SparkLanguageService);\n\n context = input.required<SparkDetailContext>();\n\n protected readonly busy = signal(false);\n protected readonly done = signal(false);\n protected readonly error = signal<string | null>(null);\n\n async flag(): Promise<void> {\n const reason = prompt(this.lang.t('moderation.flagReason'));\n if (!reason?.trim()) return;\n const ctx = this.context();\n this.busy.set(true);\n this.error.set(null);\n try {\n await this.moderation.flag(ctx.type, ctx.id, reason.trim());\n this.done.set(true);\n } catch (e) {\n this.error.set(describeModerationError(e));\n } finally {\n this.busy.set(false);\n }\n }\n}\n","import { ChangeDetectionStrategy, Component, effect, inject, input, signal } from '@angular/core';\nimport { RouterLink } from '@angular/router';\nimport { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport { SparkDetailContext } from '@mintplayer/ng-spark/panels';\nimport { SparkModerationService } from './spark-moderation.service';\nimport { ModerationStatus } from './spark-moderation.models';\nimport { describeModerationError } from './spark-vote.component';\nimport { SPARK_MODERATION_REVIEW_PATH } from './spark-moderation.tokens';\n\n/**\n * The moderator panel under a moderatable object's detail page (#460): lock state and Lock/Unlock\n * (`Lock/T`), open flags with a link to the review queue (`Review/Moderation`), and Suspend author\n * (`Suspend/Moderation`). Restore/Purge and Revert are the SoftDelete and History entry points'.\n * Renders nothing for a caller with none of these rights, or for a type that is not moderatable.\n */\n@Component({\n selector: 'spark-moderator-panel',\n imports: [BsCardComponent, BsCardHeaderComponent, TranslateKeyPipe, RouterLink],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (status(); as s) {\n @if (s.canLock || s.canReview || s.canSuspend) {\n <bs-card class=\"d-block spark-moderator-panel\">\n <bs-card-header>{{ 'moderation.panel' | t }}</bs-card-header>\n <div class=\"p-3 d-flex flex-wrap gap-2 align-items-center\">\n @if (s.locked) {\n <span class=\"badge bg-secondary spark-moderation-locked\">{{ 'moderation.locked' | t }}</span>\n @if (s.lockReason) { <span class=\"small text-muted\">{{ s.lockReason }}</span> }\n }\n @if (s.canLock) {\n @if (s.locked) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-unlock\" [disabled]=\"busy()\" (click)=\"unlock()\">{{ 'moderation.unlock' | t }}</button>\n } @else {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-lock\" [disabled]=\"busy()\" (click)=\"lock()\">{{ 'moderation.lock' | t }}</button>\n }\n }\n @if (s.canReview && s.openFlags > 0) {\n <a class=\"btn btn-sm btn-outline-warning spark-open-flags\" [routerLink]=\"reviewPath\" [queryParams]=\"{ case: s.openCaseId }\">\n {{ 'moderation.openFlags' | t }}: {{ s.openFlags }}\n </a>\n }\n @if (s.canSuspend && s.authorId) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger spark-suspend-author\" [disabled]=\"busy()\" (click)=\"suspendAuthor(s.authorId)\">{{ 'moderation.suspendAuthor' | t }}</button>\n }\n @if (error(); as message) {\n <span class=\"small text-danger\" role=\"alert\">{{ message }}</span>\n }\n </div>\n </bs-card>\n }\n }\n `,\n})\nexport class SparkModeratorPanelComponent {\n private readonly moderation = inject(SparkModerationService);\n private readonly lang = inject(SparkLanguageService);\n protected readonly reviewPath = inject(SPARK_MODERATION_REVIEW_PATH);\n\n context = input.required<SparkDetailContext>();\n\n protected readonly status = signal<ModerationStatus | null>(null);\n protected readonly busy = signal(false);\n protected readonly error = signal<string | null>(null);\n\n constructor() {\n effect(() => {\n const ctx = this.context();\n if (ctx.deleted === 'only') {\n this.status.set(null);\n return;\n }\n // A refusal (not moderatable, not visible) simply hides the panel.\n this.moderation.status(ctx.type, ctx.id).then(s => this.status.set(s), () => this.status.set(null));\n });\n }\n\n async lock(): Promise<void> {\n const reason = prompt(this.lang.t('moderation.lockReason')) ?? undefined;\n await this.run(() => this.moderation.lock(this.context().type, this.context().id, reason));\n }\n\n async unlock(): Promise<void> {\n await this.run(() => this.moderation.unlock(this.context().type, this.context().id));\n }\n\n async suspendAuthor(userId: string): Promise<void> {\n const answer = prompt(this.lang.t('moderation.suspendDays'));\n if (answer === null) return;\n const days = answer.trim() === '' ? null : Number(answer);\n if (days !== null && !(days > 0)) return;\n await this.run(() => this.moderation.suspend(userId, days));\n }\n\n private async run(work: () => Promise<void>): Promise<void> {\n this.busy.set(true);\n this.error.set(null);\n try {\n await work();\n const ctx = this.context();\n this.status.set(await this.moderation.status(ctx.type, ctx.id));\n await ctx.reload();\n } catch (e) {\n this.error.set(describeModerationError(e));\n } finally {\n this.busy.set(false);\n }\n }\n}\n","import { ChangeDetectionStrategy, Component, effect, inject, input, signal } from '@angular/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkModerationService } from './spark-moderation.service';\nimport { ModerationReputation } from './spark-moderation.models';\n\n/**\n * A user's reputation (#460): `<spark-reputation-badge [userId]=\"post.authorId\" />`, or without\n * `userId` the signed-in user's own, with the not-yet-credited part (delayed crediting) beside it.\n *\n * The own badge usually sits in the app shell, which lives for the whole session: it re-reads the\n * reputation after every navigation (a sign-in, a sign-out, opening another page) and after the\n * caller's own votes, or it would show the first value it read until a full page reload.\n */\n@Component({\n selector: 'spark-reputation-badge',\n imports: [TranslateKeyPipe],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (reputation(); as r) {\n <span class=\"badge bg-body-tertiary text-body border spark-reputation-badge\" [attr.title]=\"'moderation.reputation' | t\">\n {{ r.total }}\n @if (!userId() && r.pending) {\n <!-- A received downvote makes the pending part negative: \"(-2 pending)\", never \"(+-2 pending)\". -->\n <span class=\"text-muted spark-reputation-pending\">({{ r.pending > 0 ? '+' : '' }}{{ r.pending }} {{ 'moderation.pending' | t }})</span>\n }\n </span>\n }\n `,\n})\nexport class SparkReputationBadgeComponent {\n private readonly moderation = inject(SparkModerationService);\n\n userId = input<string | null | undefined>();\n\n protected readonly reputation = signal<ModerationReputation | null>(null);\n\n constructor() {\n effect(() => {\n const userId = this.userId() ?? undefined;\n // The own badge only: someone else's total does not move because the viewer navigated.\n const read = userId\n ? this.moderation.reputation(userId)\n : (this.moderation.ownReputationChanged(), this.moderation.ownReputation());\n read.then(r => this.reputation.set(r), () => this.reputation.set(null));\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';\nimport { DatePipe } from '@angular/common';\nimport { ActivatedRoute, Router, RouterLink } from '@angular/router';\nimport { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';\nimport { EntityType } from '@mintplayer/ng-spark/models';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkService } from '@mintplayer/ng-spark/services';\nimport { SparkModerationService } from './spark-moderation.service';\nimport { ModerationCaseDetail, ModerationCaseSummary } from './spark-moderation.models';\nimport { describeModerationError } from './spark-vote.component';\n\n/**\n * The review queue (#460): open cases on the left, the selected case's review surface on the right —\n * the voter→target matrix, the timeline against the posts' creation, the accounts side by side (age,\n * registration method, how many accounts share a network with it, reputation now and after reversing)\n * and the rule that fired — with the decisions the case allows. Needs `Review/Moderation`; merge and\n * suspend also `Suspend/Moderation` (the server refuses otherwise).\n *\n * Routed by {@link sparkModerationRoutes} at `moderation/review`; `?case=` selects a case.\n */\n@Component({\n selector: 'spark-review-queue',\n imports: [BsCardComponent, BsCardHeaderComponent, RouterLink, TranslateKeyPipe, DatePipe],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div class=\"container-fluid spark-review-queue\">\n <h1 class=\"h3\">{{ 'moderation.reviewQueue' | t }}</h1>\n @if (error(); as message) { <div class=\"alert alert-danger\" role=\"alert\">{{ message }}</div> }\n <div class=\"row g-3\">\n <div class=\"col-md-4\">\n @for (c of cases(); track c.id) {\n <button type=\"button\" class=\"list-group-item list-group-item-action w-100 text-start p-2 border mb-1 spark-case\"\n [class.active]=\"selected()?.case?.id === c.id\" (click)=\"open(c.id)\">\n <strong>{{ c.kind }}</strong>\n @if (c.flagCount) { <span class=\"badge text-bg-warning ms-1\">{{ c.flagCount }}</span> }\n @if (targets()[c.id]?.title; as title) { <div class=\"small fw-semibold text-truncate spark-case-target\">{{ title }}</div> }\n <div class=\"small\">{{ c.summary }}</div>\n <div class=\"small text-muted\">{{ c.openedAtUtc | date: 'short' }}</div>\n </button>\n } @empty {\n <p class=\"text-muted spark-no-cases\">{{ 'moderation.noCases' | t }}</p>\n }\n </div>\n <div class=\"col-md-8\">\n @if (selected(); as d) {\n <bs-card class=\"d-block spark-case-detail\">\n <bs-card-header>{{ 'moderation.rule' | t }}: {{ d.case.kind }} — {{ d.case.summary }}</bs-card-header>\n <div class=\"p-3\">\n @if (targets()[d.case.id]; as target) {\n <p class=\"spark-case-target-link\">\n @if (target.link) { <a [routerLink]=\"target.link\">{{ target.title ?? d.case.targetId }}</a> }\n @else { {{ target.title ?? d.case.targetId }} }\n </p>\n }\n @if (d.accounts.length) {\n <h2 class=\"h6\">{{ 'moderation.accounts' | t }}</h2>\n <table class=\"table table-sm spark-case-accounts\">\n <thead><tr><th></th><th>{{ 'moderation.ageDays' | t }}</th><th></th><th>{{ 'moderation.sharesNetwork' | t }}</th><th>{{ 'moderation.repBefore' | t }}</th><th>{{ 'moderation.repAfter' | t }}</th></tr></thead>\n <tbody>\n @for (a of d.accounts; track a.id) {\n <tr><td>{{ a.id }}</td><td>{{ a.ageDays }}</td><td>{{ a.registrationMethod }}</td><td>{{ a.sharesNetworkWith }}</td><td>{{ a.reputationBefore }}</td><td>{{ a.reputationAfter }}</td></tr>\n }\n </tbody>\n </table>\n }\n @if (d.matrix.length) {\n <h2 class=\"h6\">{{ 'moderation.matrix' | t }}</h2>\n <ul class=\"small spark-case-matrix\">\n @for (m of d.matrix; track m.voterId + m.authorId) { <li>{{ m.voterId }} → {{ m.authorId }}: {{ m.votes }}</li> }\n </ul>\n }\n @if (d.timeline.length) {\n <h2 class=\"h6\">{{ 'moderation.timeline' | t }}</h2>\n <table class=\"table table-sm small spark-case-timeline\">\n <tbody>\n @for (t of d.timeline; track t.voteId) {\n <tr><td>{{ t.castAtUtc | date: 'short' }}</td><td>{{ t.voterId }} → {{ t.authorId }}</td><td>{{ t.direction > 0 ? '▲' : t.direction < 0 ? '▼' : '·' }}</td><td>{{ t.secondsAfterPost ?? '' }} s</td></tr>\n }\n </tbody>\n </table>\n }\n @if (d.flags.length) {\n <ul class=\"small spark-case-flags\">\n @for (f of d.flags; track f.flaggerId) { <li>{{ f.reason }} <span class=\"text-muted\">({{ f.raisedAtUtc | date: 'short' }})</span></li> }\n </ul>\n }\n @if (d.case.status === 'open') {\n <div class=\"d-flex flex-wrap gap-2\">\n @if (d.case.kind === 'flag') {\n <button type=\"button\" class=\"btn btn-sm btn-success spark-decide-uphold\" [disabled]=\"busy()\" (click)=\"decide('uphold')\">{{ 'moderation.uphold' | t }}</button>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-decide-decline\" [disabled]=\"busy()\" (click)=\"decide('decline')\">{{ 'moderation.decline' | t }}</button>\n } @else {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-decide-dismiss\" [disabled]=\"busy()\" (click)=\"decide('dismiss')\">{{ 'moderation.dismiss' | t }}</button>\n <button type=\"button\" class=\"btn btn-sm btn-warning spark-decide-reverse\" [disabled]=\"busy()\" (click)=\"decide('reverse')\">{{ 'moderation.reverse' | t }}</button>\n @for (a of d.case.accountIds; track a) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger spark-decide-merge\" [disabled]=\"busy()\" (click)=\"decide('merge', a)\">{{ 'moderation.merge' | t }}: {{ a }}</button>\n <button type=\"button\" class=\"btn btn-sm btn-danger spark-decide-suspend\" [disabled]=\"busy()\" (click)=\"decide('suspend', a)\">{{ 'moderation.suspend' | t }}: {{ a }}</button>\n }\n }\n </div>\n }\n </div>\n </bs-card>\n }\n </div>\n </div>\n </div>\n `,\n})\nexport class SparkReviewQueueComponent implements OnInit {\n private readonly moderation = inject(SparkModerationService);\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n\n private readonly spark = inject(SparkService);\n\n protected readonly cases = signal<ModerationCaseSummary[]>([]);\n /** Per case id: the target post's title and route, once resolved. */\n protected readonly targets = signal<Record<string, { title: string | null; link: string[] | null }>>({});\n protected readonly selected = signal<ModerationCaseDetail | null>(null);\n protected readonly busy = signal(false);\n protected readonly error = signal<string | null>(null);\n\n async ngOnInit(): Promise<void> {\n await this.load();\n const requested = this.route.snapshot.queryParamMap.get('case');\n if (requested) await this.open(requested);\n }\n\n async open(caseId: string): Promise<void> {\n await this.run(async () => {\n this.selected.set(await this.moderation.case(caseId));\n await this.router.navigate([], { relativeTo: this.route, queryParams: { case: caseId }, replaceUrl: true });\n });\n }\n\n async decide(decision: string, accountId?: string): Promise<void> {\n const current = this.selected();\n if (!current) return;\n await this.run(async () => {\n await this.moderation.decide(current.case.id, decision, accountId);\n this.selected.set(await this.moderation.case(current.case.id));\n await this.load();\n });\n }\n\n private async load(): Promise<void> {\n await this.run(async () => this.cases.set(await this.moderation.cases('open')));\n void this.resolveTargets(this.cases());\n }\n\n /**\n * Names each case's post: its title (the object's breadcrumb) and a link to it. A case carries only\n * the target's id and type name; without this the queue listed \"flag · Spam\" with no way to tell\n * which post it was about. A target the caller cannot open (purged, hidden) keeps just its id.\n */\n private async resolveTargets(cases: ModerationCaseSummary[]): Promise<void> {\n const pending = cases.filter(c => c.targetId && c.targetType && !(c.id in this.targets()));\n if (!pending.length) return;\n let types: EntityType[] = [];\n try {\n types = await this.spark.getEntityTypes();\n } catch {\n return;\n }\n await Promise.all(pending.map(async c => {\n const type = types.find(t => t.name === c.targetType || t.id === c.targetType || t.alias === c.targetType);\n const route = type ? type.alias || type.id : null;\n let title: string | null = null;\n if (route) {\n try {\n const po = await this.spark.get(route, c.targetId!);\n title = po.breadcrumb || null;\n } catch {\n // Not visible to the caller (or gone): the id stays the label, without a link.\n }\n }\n const link = route && title ? ['/po', route, c.targetId!] : null;\n this.targets.update(t => ({ ...t, [c.id]: { title, link } }));\n }));\n }\n\n private async run(work: () => Promise<void>): Promise<void> {\n this.busy.set(true);\n this.error.set(null);\n try {\n await work();\n } catch (e) {\n this.error.set(describeModerationError(e));\n } finally {\n this.busy.set(false);\n }\n }\n}\n","import { ChangeDetectionStrategy, Component, effect, inject, input, signal } from '@angular/core';\nimport { RouterLink } from '@angular/router';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkModerationService } from './spark-moderation.service';\n\n/**\n * A link to the review queue (#460) that renders only for a caller who may review — earned, or\n * granted by a group such as moderators (`canReview` on the caller's own reputation). Anyone else\n * would open a page whose every request answers 404. Re-checked after each navigation, so it appears\n * on sign-in and disappears on sign-out.\n *\n * `<spark-review-queue-link linkClass=\"btn btn-sm btn-link\" />` — `linkClass` styles the anchor.\n */\n@Component({\n selector: 'spark-review-queue-link',\n imports: [RouterLink, TranslateKeyPipe],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[style.display]': 'canReview() ? null : \"none\"' },\n template: `\n @if (canReview()) {\n <a class=\"spark-review-queue-link\" [class]=\"linkClass()\" routerLink=\"/moderation/review\">{{ 'moderation.reviewQueue' | t }}</a>\n }\n `,\n})\nexport class SparkReviewQueueLinkComponent {\n private readonly moderation = inject(SparkModerationService);\n\n /** Classes for the anchor itself (the host element is only a wrapper). */\n linkClass = input<string>('');\n\n protected readonly canReview = signal(false);\n\n constructor() {\n effect(() => {\n this.moderation.ownReputationChanged();\n this.moderation.ownReputation().then(r => this.canReview.set(r.canReview === true), () => this.canReview.set(false));\n });\n }\n}\n","import { Provider } from '@angular/core';\nimport { Routes } from '@angular/router';\nimport { SparkAttributeRendererRegistration } from '@mintplayer/ng-spark/renderers';\nimport { provideSparkDetailActions, provideSparkDetailPanels } from '@mintplayer/ng-spark/panels';\nimport { SparkFlagButtonComponent } from './spark-flag-button.component';\nimport { SparkModeratorPanelComponent } from './spark-moderator-panel.component';\nimport { SparkReviewQueueComponent } from './spark-review-queue.component';\nimport { SparkVoteComponent } from './spark-vote.component';\nimport { SPARK_MODERATION_REVIEW_PATH } from './spark-moderation.tokens';\n\n/**\n * Adds the Moderation UI (#460) to the pages `sparkRoutes()` routes to: a **Flag** action on every\n * detail page and the **moderator panel** under it. Needs the server package\n * `MintPlayer.Spark.Moderation`.\n *\n * ```ts\n * providers: [\n * provideSpark(...),\n * provideSparkModeration(),\n * provideSparkAttributeRenderers([...sparkModerationRenderers, ...yourRenderers]),\n * ]\n * // routes: [...sparkModerationRoutes(), ...sparkRoutes()]\n * ```\n *\n * The vote widget is an attribute renderer (`\"renderer\": \"spark-vote\"` on an attribute of an\n * `IModeratable` type), so it is registered through {@link sparkModerationRenderers} with the app's\n * own renderers — `provideSparkAttributeRenderers` takes one list.\n */\nexport function provideSparkModeration(options?: { reviewPath?: string; panelOrder?: number }): Provider[] {\n return [\n ...provideSparkDetailActions({ id: 'spark-moderation-flag', component: SparkFlagButtonComponent, priority: 90 }),\n ...provideSparkDetailPanels({ id: 'spark-moderation-panel', component: SparkModeratorPanelComponent, order: options?.panelOrder ?? 90 }),\n { provide: SPARK_MODERATION_REVIEW_PATH, useValue: options?.reviewPath ?? '/moderation/review' },\n ];\n}\n\n/** The vote widget as an attribute renderer named `spark-vote` (detail page and grid column). */\nexport const sparkModerationRenderers: SparkAttributeRendererRegistration[] = [\n { name: 'spark-vote', detailComponent: SparkVoteComponent, columnComponent: SparkVoteComponent },\n];\n\n/**\n * The review-queue page at `moderation/review`. Put it **before** `sparkRoutes()` — a literal first\n * segment is fine anywhere, but keeping add-on routes first avoids surprises with parameterised ones.\n */\nexport function sparkModerationRoutes(path = 'moderation/review'): Routes {\n return [{ path, component: SparkReviewQueueComponent }];\n}\n"],"mappings":";;;;;;;;;;AAwBA,IAAa,yBAAb,MAAa,uBAAsB;CAChB,QAAQ,OAAO,YAAY;CAG3B,0BAAU,IAAI,IAAG;CAC1B,iBAAiB;CAER,SAAS,OAAO,QAAQ,EAAE,UAAU,KAAI,CAAE;CAElD,aAAa,OAAO,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGtB,YAAY,KAAK,WAAW,WAAU;CAE9B,YAAY,SAC3B,KAAK,QAAQ,OAAO,KAAK,QAAO,MAAK,aAAa,aAAa,CAAC,KAAK,OACrE,EAAE,cAAc,KAAI,CAAE;CAUf,uBAAuB,gBAAgB;EAAE,YAAY,KAAK,UAAS;EAAI,OAAO,KAAK,WAAU;CAAE,IAAI,GAAA,YAAA,CAAA,EAAA,WAAA,uBAAA,CAAA,+BAAA,CAAA,CAAA;CAE5G,eAAqD;CAMrD,gBAAa;EACX,OAAO,KAAK,iBAAiB,KAAK,WAAU,CAAE,CAAC,cAAc,KAAK,eAAe,IAAI;CACvF;CAGA,MAAM,KAAK,MAAc,IAAY,WAAqB;EACxD,MAAM,QAAQ,MAAM,KAAK,MAAM,aAAkC,oBAAoB;GAAE,cAAc;GAAM;GAAI;EAAS,CAAE;EAC1H,KAAK,WAAW,QAAO,MAAK,IAAI,CAAC;EACjC,OAAO;CACT;CAOA,UAAU,MAAc,IAAU;EAChC,OAAO,IAAI,SAAQ,YAAU;GAC3B,IAAI,OAAO,KAAK,QAAQ,IAAI,IAAI;GAChC,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAO,uBAAO,IAAI,IAAG,CAAG;GACpD,MAAM,UAAU,KAAK,IAAI,EAAE,KAAK,CAAA;GAChC,QAAQ,KAAK,OAAO;GACpB,KAAK,IAAI,IAAI,OAAO;GACpB,IAAI,CAAC,KAAK,gBAAgB;IACxB,KAAK,iBAAiB;IACtB,iBAAiB,KAAK,MAAK,GAAI,CAAC;GAClC;EACF,CAAC;CACH;CAGA,MAAM,MAAc,KAAa;EAC/B,OAAO,KAAK,MAAM,aAAoC,qBAAqB;GAAE,cAAc;GAAM;EAAG,CAAE;CACxG;CAEA,KAAK,MAAc,IAAY,QAAc;EAC3C,OAAO,KAAK,MAAM,aAAmB,oBAAoB;GAAE,cAAc;GAAM;GAAI;EAAM,CAAE;CAC7F;CAEA,KAAK,MAAc,IAAY,QAAe;EAC5C,OAAO,KAAK,MAAM,aAAmB,oBAAoB;GAAE,cAAc;GAAM;GAAI;EAAM,CAAE;CAC7F;CAEA,OAAO,MAAc,IAAU;EAC7B,OAAO,KAAK,MAAM,aAAmB,sBAAsB;GAAE,cAAc;GAAM;EAAE,CAAE;CACvF;CAGA,OAAO,MAAc,IAAU;EAC7B,OAAO,KAAK,MAAM,aAA+B,sBAAsB;GAAE,cAAc;GAAM;EAAE,CAAE;CACnG;CAGA,WAAW,QAAe;EACxB,OAAO,KAAK,MAAM,aAAmC,0BAA0B,SAAS,EAAE,OAAM,IAAK,CAAA,CAAE;CACzG;CAGA,QAAQ,OAAO,KAAG;EAChB,OAAO,KAAK,MAAM,aAAyC,kCAAkC,EAAE,KAAI,CAAE;CACvG;CAEA,MAAM,SAAiB,QAAQ,OAAO,GAAG,OAAO,IAAE;EAChD,OAAO,KAAK,MAAM,aAAsC,qBAAqB;GAAE;GAAQ;GAAM;EAAI,CAAE;CACrG;CAEA,KAAK,QAAc;EACjB,OAAO,KAAK,MAAM,aAAmC,oBAAoB,EAAE,OAAM,CAAE;CACrF;CAGA,OAAO,QAAgB,UAAkB,WAAoB,QAAe;EAC1E,OAAO,KAAK,MAAM,aAAmB,2BAA2B;GAAE;GAAQ;GAAU;GAAW;EAAM,CAAE;CACzG;CAEA,QAAQ,QAAgB,MAAsB,QAAe;EAC3D,OAAO,KAAK,MAAM,aAAmB,uBAAuB;GAAE;GAAQ,MAAM,QAAQ;GAAM;EAAM,CAAE;CACpG;CAEA,UAAU,QAAc;EACtB,OAAO,KAAK,MAAM,aAAmB,yBAAyB,EAAE,OAAM,CAAE;CAC1E;CAEA,MAAM,OAAO,GAAG,OAAO,IAAE;EACvB,OAAO,KAAK,MAAM,aAAqC,qBAAqB;GAAE;GAAM;EAAI,CAAE;CAC5F;CAEQ,QAAK;EACX,KAAK,iBAAiB;EACtB,MAAM,UAAU,CAAC,GAAG,KAAK,QAAQ,QAAO,CAAE;EAC1C,KAAK,QAAQ,MAAK;EAClB,KAAK,MAAM,CAAC,MAAM,SAAS,SAAS;GAClC,MAAM,MAAM,CAAC,GAAG,KAAK,KAAI,CAAE;GAC3B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,KAAK;IACxC,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,GAAG;IAClC,KAAK,MAAM,MAAM,KAAK,CAAC,CAAC,MACtB,WAAS;KACP,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAI,MAAK,CAAC,EAAE,IAAI,CAAC,CAAU,CAAC;KACzD,KAAK,MAAM,MAAM,OAAO,KAAK,IAAI,EAAE,CAAC,EAAE,SAAQ,YAAW,QAAQ,MAAM,IAAI,EAAE,KAAK,IAAI,CAAC;IACzF,SACK;KACH,KAAK,MAAM,MAAM,OAAO,KAAK,IAAI,EAAE,CAAC,EAAE,SAAQ,YAAW,QAAQ,IAAI,CAAC;IACxE,CAAC;GACL;EACF;CACF;CA3IW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAsB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAtB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAsB,YADT;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAsB,YAAA,CAAA;EADlC,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACpBlC,MAAa,+BAA+B,IAAI,eAAuB,6BAA6B,EAClG,eAAe,qBAChB,CAAA;ACmCD,IAAa,qBAAb,MAAa,mBAAkB;CACZ,aAAa,OAAO,sBAAsB;CAG3D,QAAQ,MAAgB,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACxB,YAAY,MAA8C,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAC1D,SAAS,MAAoC,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAC7C,UAAU,MAAwC,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAClD,OAAO,MAA6E,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAEjE,QAAQ,OAAmC,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAChD,OAAO,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACpB,QAAQ,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAEnC,SAAS,eAAc;EACxC,MAAM,OAAO,KAAK,KAAI;EACtB,MAAM,KAAK,OAAO;EAClB,MAAM,OAAQ,OAAO,mBAA2C,KAAK,QAAO,CAAE,GAAG;EACjF,OAAO,MAAM,OAAO;GAAE;GAAI;EAAI,IAAK;CACrC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAEF,cAAA;EACE,aAAY;GACV,MAAM,SAAS,KAAK,OAAM;GAC1B,IAAI,CAAC,QAAQ;GACb,KAAK,WAAW,UAAU,OAAO,MAAM,OAAO,EAAE,CAAC,CAAC,MAAK,MAAK,KAAK,MAAM,IAAI,CAAC,CAAC;EAC/E,CAAC;CACH;CAEA,MAAM,KAAK,WAAiB;EAC1B,MAAM,SAAS,KAAK,OAAM;EAC1B,MAAM,UAAU,KAAK,MAAK;EAC1B,IAAI,CAAC,UAAU,CAAC,WAAW,KAAK,KAAI,GAAI;EAExC,MAAM,OAAO,QAAQ,WAAW,YAAY,IAAI;EAChD,KAAK,KAAK,IAAI,IAAI;EAClB,KAAK,MAAM,IAAI,IAAI;EACnB,IAAI;GACF,KAAK,MAAM,IAAI,MAAM,KAAK,WAAW,KAAK,OAAO,MAAM,OAAO,IAAI,IAAI,CAAC;EACzE,SAAS,GAAG;GACV,KAAK,MAAM,IAAI,wBAAwB,CAAC,CAAC;EAC3C,UAAU;GACR,KAAK,KAAK,IAAI,KAAK;EACrB;CACF;CA5CW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAkB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAlB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAkB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,QAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UAfnB;;;;;;;;;;;;;;EAaT,UAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MAfS;GAAgB,MAAA;EAAA,CAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAiBf,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAkB,YAAA,CAAA;EAnB9B,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS,CAAC,gBAAgB;GAC1B,iBAAiB,wBAAwB;GACzC,UAAU;;;;;;;;;;;;;;EAcX,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDD,SAAgB,wBAAwB,GAAU;CAChD,MAAM,MAAM;CACZ,MAAM,OAAO,KAAK;CAClB,MAAM,QAAQ,MAAM,QAAQ,SAAS,EAAE,EAAE;CACzC,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,SAAS,OAAO,UAAU,UAAU;EACtC,MAAM,SAAS,OAAO,OAAO,KAA+B;EAC5D,IAAI,OAAO,QAAQ,OAAO,OAAO;CACnC;CACA,OAAO,MAAM,QAAQ,SAAS,KAAK,WAAW;AAChD;AChEA,IAAa,2BAAb,MAAa,yBAAwB;CAClB,aAAa,OAAO,sBAAsB;CAC1C,OAAO,OAAO,oBAAoB;CAEnD,UAAU,MAAM,SAA8B,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAE3B,OAAO,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACpB,OAAO,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACpB,QAAQ,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAEtD,MAAM,OAAI;EACR,MAAM,SAAS,OAAO,KAAK,KAAK,EAAE,uBAAuB,CAAC;EAC1D,IAAI,CAAC,QAAQ,KAAI,GAAI;EACrB,MAAM,MAAM,KAAK,QAAO;EACxB,KAAK,KAAK,IAAI,IAAI;EAClB,KAAK,MAAM,IAAI,IAAI;EACnB,IAAI;GACF,MAAM,KAAK,WAAW,KAAK,IAAI,MAAM,IAAI,IAAI,OAAO,KAAI,CAAE;GAC1D,KAAK,KAAK,IAAI,IAAI;EACpB,SAAS,GAAG;GACV,KAAK,MAAM,IAAI,wBAAwB,CAAC,CAAC;EAC3C,UAAU;GACR,KAAK,KAAK,IAAI,KAAK;EACrB;CACF;CAxBW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAwB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAxB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAwB,cAAA;EAAA,UAAA;EAAA,QAAA,EAAA,SAAA;GAAA,mBAAA;GAAA,YAAA;GAAA,UAAA;GAAA,YAAA;GAAA,mBAAA;EAAA,EAAA;EAAA,UAAA;EAAA,UAhBzB;;;;;;;;;;;;;;;EAcT,UAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MAhBS;GAAgB,MAAA;EAAA,CAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAkBf,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAwB,YAAA,CAAA;EApBpC,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS,CAAC,gBAAgB;GAC1B,iBAAiB,wBAAwB;GACzC,UAAU;;;;;;;;;;;;;;;EAeX,CAAA;;;;;;;;;;;ACsBD,IAAa,+BAAb,MAAa,6BAA4B;CACtB,aAAa,OAAO,sBAAsB;CAC1C,OAAO,OAAO,oBAAoB;CAChC,aAAa,OAAO,4BAA4B;CAEnE,UAAU,MAAM,SAA8B,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAE3B,SAAS,OAAgC,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAC9C,OAAO,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACpB,QAAQ,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAEtD,cAAA;EACE,aAAY;GACV,MAAM,MAAM,KAAK,QAAO;GACxB,IAAI,IAAI,YAAY,QAAQ;IAC1B,KAAK,OAAO,IAAI,IAAI;IACpB;GACF;GAEA,KAAK,WAAW,OAAO,IAAI,MAAM,IAAI,EAAE,CAAC,CAAC,MAAK,MAAK,KAAK,OAAO,IAAI,CAAC,SAAS,KAAK,OAAO,IAAI,IAAI,CAAC;EACpG,CAAC;CACH;CAEA,MAAM,OAAI;EACR,MAAM,SAAS,OAAO,KAAK,KAAK,EAAE,uBAAuB,CAAC,KAAK;EAC/D,MAAM,KAAK,UAAU,KAAK,WAAW,KAAK,KAAK,QAAO,CAAE,CAAC,MAAM,KAAK,QAAO,CAAE,CAAC,IAAI,MAAM,CAAC;CAC3F;CAEA,MAAM,SAAM;EACV,MAAM,KAAK,UAAU,KAAK,WAAW,OAAO,KAAK,QAAO,CAAE,CAAC,MAAM,KAAK,QAAO,CAAE,CAAC,EAAE,CAAC;CACrF;CAEA,MAAM,cAAc,QAAc;EAChC,MAAM,SAAS,OAAO,KAAK,KAAK,EAAE,wBAAwB,CAAC;EAC3D,IAAI,WAAW,MAAM;EACrB,MAAM,OAAO,OAAO,KAAI,MAAO,KAAK,OAAO,OAAO,MAAM;EACxD,IAAI,SAAS,QAAQ,EAAE,OAAO,IAAI;EAClC,MAAM,KAAK,UAAU,KAAK,WAAW,QAAQ,QAAQ,IAAI,CAAC;CAC5D;CAEQ,MAAM,IAAI,MAAyB;EACzC,KAAK,KAAK,IAAI,IAAI;EAClB,KAAK,MAAM,IAAI,IAAI;EACnB,IAAI;GACF,MAAM,KAAI;GACV,MAAM,MAAM,KAAK,QAAO;GACxB,KAAK,OAAO,IAAI,MAAM,KAAK,WAAW,OAAO,IAAI,MAAM,IAAI,EAAE,CAAC;GAC9D,MAAM,IAAI,OAAM;EAClB,SAAS,GAAG;GACV,KAAK,MAAM,IAAI,wBAAwB,CAAC,CAAC;EAC3C,UAAU;GACR,KAAK,KAAK,IAAI,KAAK;EACrB;CACF;CArDW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAA4B,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAA5B,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAA4B,cAAA;EAAA,UAAA;EAAA,QAAA,EAAA,SAAA;GAAA,mBAAA;GAAA,YAAA;GAAA,UAAA;GAAA,YAAA;GAAA,mBAAA;EAAA,EAAA;EAAA,UAAA;EAAA,UAlC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgCT,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MAlCS;IAAe,UAAA;IAAA,QAAA,CAAA,SAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAqB,UAAA;IAAA,QAAA,CAAA,SAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAoB;IAAU,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAA5B;IAAgB,MAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAoCvD,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAA4B,YAAA,CAAA;EAtCxC,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS;IAAC;IAAiB;IAAuB;IAAkB;GAAU;GAC9E,iBAAiB,wBAAwB;GACzC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCX,CAAA;;;;;;;;;;;;ACzBD,IAAa,gCAAb,MAAa,8BAA6B;CACvB,aAAa,OAAO,sBAAsB;CAE3D,SAAS,MAAkC,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAExB,aAAa,OAAoC,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEzE,cAAA;EACE,aAAY;GACV,MAAM,SAAS,KAAK,OAAM,KAAM;GAKhC,CAHa,SACT,KAAK,WAAW,WAAW,MAAM,KAChC,KAAK,WAAW,qBAAoB,GAAI,KAAK,WAAW,cAAa,GAC1E,CAAK,MAAK,MAAK,KAAK,WAAW,IAAI,CAAC,SAAS,KAAK,WAAW,IAAI,IAAI,CAAC;EACxE,CAAC;CACH;CAhBW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAA6B,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAA7B,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAA6B,cAAA;EAAA,UAAA;EAAA,QAAA,EAAA,QAAA;GAAA,mBAAA;GAAA,YAAA;GAAA,UAAA;GAAA,YAAA;GAAA,mBAAA;EAAA,EAAA;EAAA,UAAA;EAAA,UAZ9B;;;;;;;;;;;EAUT,UAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MAZS;GAAgB,MAAA;EAAA,CAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAcf,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAA6B,YAAA,CAAA;EAhBzC,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS,CAAC,gBAAgB;GAC1B,iBAAiB,wBAAwB;GACzC,UAAU;;;;;;;;;;;EAWX,CAAA;;;;;;;;;;;;ACiFD,IAAa,4BAAb,MAAa,0BAAyB;CACnB,aAAa,OAAO,sBAAsB;CAC1C,QAAQ,OAAO,cAAc;CAC7B,SAAS,OAAO,MAAM;CAEtB,QAAQ,OAAO,YAAY;CAEzB,QAAQ,OAAgC,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAE3C,UAAU,OAAwE,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CACrF,WAAW,OAAoC,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CACpD,OAAO,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACpB,QAAQ,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAEtD,MAAM,WAAQ;EACZ,MAAM,KAAK,KAAI;EACf,MAAM,YAAY,KAAK,MAAM,SAAS,cAAc,IAAI,MAAM;EAC9D,IAAI,WAAW,MAAM,KAAK,KAAK,SAAS;CAC1C;CAEA,MAAM,KAAK,QAAc;EACvB,MAAM,KAAK,IAAI,YAAW;GACxB,KAAK,SAAS,IAAI,MAAM,KAAK,WAAW,KAAK,MAAM,CAAC;GACpD,MAAM,KAAK,OAAO,SAAS,CAAA,GAAI;IAAE,YAAY,KAAK;IAAO,aAAa,EAAE,MAAM,OAAM;IAAI,YAAY;GAAI,CAAE;EAC5G,CAAC;CACH;CAEA,MAAM,OAAO,UAAkB,WAAkB;EAC/C,MAAM,UAAU,KAAK,SAAQ;EAC7B,IAAI,CAAC,SAAS;EACd,MAAM,KAAK,IAAI,YAAW;GACxB,MAAM,KAAK,WAAW,OAAO,QAAQ,KAAK,IAAI,UAAU,SAAS;GACjE,KAAK,SAAS,IAAI,MAAM,KAAK,WAAW,KAAK,QAAQ,KAAK,EAAE,CAAC;GAC7D,MAAM,KAAK,KAAI;EACjB,CAAC;CACH;CAEQ,MAAM,OAAI;EAChB,MAAM,KAAK,IAAI,YAAY,KAAK,MAAM,IAAI,MAAM,KAAK,WAAW,MAAM,MAAM,CAAC,CAAC;EAC9E,AAAK,KAAK,eAAe,KAAK,MAAK,CAAE;CACvC;CAOQ,MAAM,eAAe,OAA8B;EACzD,MAAM,UAAU,MAAM,QAAO,MAAK,EAAE,YAAY,EAAE,cAAc,EAAE,EAAE,MAAM,KAAK,QAAO,EAAG;EACzF,IAAI,CAAC,QAAQ,QAAQ;EACrB,IAAI,QAAsB,CAAA;EAC1B,IAAI;GACF,QAAQ,MAAM,KAAK,MAAM,eAAc;EACzC,QAAQ;GACN;EACF;EACA,MAAM,QAAQ,IAAI,QAAQ,IAAI,OAAM,MAAI;GACtC,MAAM,OAAO,MAAM,MAAK,MAAK,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU;GACzG,MAAM,QAAQ,OAAO,KAAK,SAAS,KAAK,KAAK;GAC7C,IAAI,QAAuB;GAC3B,IAAI,OACF,IAAI;IAEF,SAAQ,MADS,KAAK,MAAM,IAAI,OAAO,EAAE,QAAS,EAC1C,CAAG,cAAc;GAC3B,QAAQ,CAER;GAEF,MAAM,OAAO,SAAS,QAAQ;IAAC;IAAO;IAAO,EAAE;GAAS,IAAI;GAC5D,KAAK,QAAQ,QAAO,OAAM;IAAE,GAAG;KAAI,EAAE,KAAK;KAAE;KAAO;IAAI;GAAE,EAAG;EAC9D,CAAC,CAAC;CACJ;CAEQ,MAAM,IAAI,MAAyB;EACzC,KAAK,KAAK,IAAI,IAAI;EAClB,KAAK,MAAM,IAAI,IAAI;EACnB,IAAI;GACF,MAAM,KAAI;EACZ,SAAS,GAAG;GACV,KAAK,MAAM,IAAI,wBAAwB,CAAC,CAAC;EAC3C,UAAU;GACR,KAAK,KAAK,IAAI,KAAK;EACrB;CACF;CAnFW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAyB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAzB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAyB,cAAA;EAAA,UAAA;EAAA,UAAA;EAAA,UArF1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmFT,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MArFS;IAAe,UAAA;IAAA,QAAA,CAAA,SAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAqB,UAAA;IAAA,QAAA,CAAA,SAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAU,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAgB,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAQ,MAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAuF7E,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAyB,YAAA,CAAA;EAzFrC,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS;IAAC;IAAiB;IAAuB;IAAY;IAAkB;GAAQ;GACxF,iBAAiB,wBAAwB;GACzC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoFX,CAAA;;;ACpFD,IAAa,gCAAb,MAAa,8BAA6B;CACvB,aAAa,OAAO,sBAAsB;CAG3D,YAAY,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEV,YAAY,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAE5C,cAAA;EACE,aAAY;GACV,KAAK,WAAW,qBAAoB;GACpC,KAAK,WAAW,cAAa,CAAE,CAAC,MAAK,MAAK,KAAK,UAAU,IAAI,EAAE,cAAc,IAAI,SAAS,KAAK,UAAU,IAAI,KAAK,CAAC;EACrH,CAAC;CACH;CAbW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAA6B,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAA7B,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAA6B,cAAA;EAAA,UAAA;EAAA,QAAA,EAAA,WAAA;GAAA,mBAAA;GAAA,YAAA;GAAA,UAAA;GAAA,YAAA;GAAA,mBAAA;EAAA,EAAA;EAAA,MAAA,EAAA,YAAA,EAAA,iBAAA,gCAAA,EAAA;EAAA,UAAA;EAAA,UAN9B;;;;;EAIT,UAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MAPS;GAAU,UAAA;GAAA,QAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;GAAA;EAAA,GAAA;GAAA,MAAA;GAAA,MAAE;GAAgB,MAAA;EAAA,CAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAS3B,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAA6B,YAAA,CAAA;EAXzC,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS,CAAC,YAAY,gBAAgB;GACtC,iBAAiB,wBAAwB;GACzC,MAAM,EAAE,mBAAmB,gCAA6B;GACxD,UAAU;;;;;EAKX,CAAA;;;;;;;;;;;;ACKD,SAAgB,uBAAuB,SAAsD;CAC3F,OAAO;EACL,GAAG,0BAA0B;GAAE,IAAI;GAAyB,WAAW;GAA0B,UAAU;EAAE,CAAE;EAC/G,GAAG,yBAAyB;GAAE,IAAI;GAA0B,WAAW;GAA8B,OAAO,SAAS,cAAc;EAAE,CAAE;EACvI;GAAE,SAAS;GAA8B,UAAU,SAAS,cAAc;EAAoB;CAC/F;AACH;AAGA,MAAa,2BAAiE,CAC5E;CAAE,MAAM;CAAc,iBAAiB;CAAoB,iBAAiB;AAAkB,CAC/F;AAMD,SAAgB,sBAAsB,OAAO,qBAAmB;CAC9D,OAAO,CAAC;EAAE;EAAM,WAAW;CAAyB,CAAE;AACxD"}
@@ -0,0 +1,60 @@
1
+ import { InjectionToken } from "@angular/core";
2
+ const SPARK_QUERY_ROW_ACTIONS = new InjectionToken("SparkQueryRowActions");
3
+ function provideSparkQueryRowActions(...actions) {
4
+ return actions.map((action) => ({
5
+ provide: SPARK_QUERY_ROW_ACTIONS,
6
+ useValue: action,
7
+ multi: true
8
+ }));
9
+ }
10
+ const SPARK_DETAIL_PANELS = new InjectionToken("SparkDetailPanels");
11
+ const SPARK_DETAIL_ACTIONS = new InjectionToken("SparkDetailActions");
12
+ const SPARK_QUERY_LIST_ACTIONS = new InjectionToken("SparkQueryListActions");
13
+ function provideSparkDetailPanels(...panels) {
14
+ return panels.map((panel) => ({
15
+ provide: SPARK_DETAIL_PANELS,
16
+ useValue: panel,
17
+ multi: true
18
+ }));
19
+ }
20
+ function provideSparkDetailActions(...actions) {
21
+ return actions.map((action) => ({
22
+ provide: SPARK_DETAIL_ACTIONS,
23
+ useValue: action,
24
+ multi: true
25
+ }));
26
+ }
27
+ function provideSparkQueryListActions(...actions) {
28
+ return actions.map((action) => ({
29
+ provide: SPARK_QUERY_LIST_ACTIONS,
30
+ useValue: action,
31
+ multi: true
32
+ }));
33
+ }
34
+ function orderSparkExtensions(entries, key) {
35
+ if (!entries?.length) return [];
36
+ const byId = /* @__PURE__ */ new Map();
37
+ entries.forEach((entry, index) => {
38
+ const previous = byId.get(entry.id);
39
+ byId.set(entry.id, {
40
+ entry,
41
+ index: previous?.index ?? index
42
+ });
43
+ });
44
+ return [...byId.values()].sort((a, b) => {
45
+ const ka = key(a.entry) ?? Number.POSITIVE_INFINITY;
46
+ const kb = key(b.entry) ?? Number.POSITIVE_INFINITY;
47
+ return ka === kb ? a.index - b.index : ka - kb;
48
+ }).map((x) => x.entry);
49
+ }
50
+ function parseSparkDeletedParam(value) {
51
+ switch (value?.toLowerCase()) {
52
+ case "include": return "include";
53
+ case "only": return "only";
54
+ case "exclude": return "exclude";
55
+ default: return null;
56
+ }
57
+ }
58
+ export { SPARK_DETAIL_ACTIONS, SPARK_DETAIL_PANELS, SPARK_QUERY_LIST_ACTIONS, SPARK_QUERY_ROW_ACTIONS, orderSparkExtensions, parseSparkDeletedParam, provideSparkDetailActions, provideSparkDetailPanels, provideSparkQueryListActions, provideSparkQueryRowActions };
59
+
60
+ //# sourceMappingURL=mintplayer-ng-spark-panels.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mintplayer-ng-spark-panels.mjs","names":[],"sources":["../../panels/src/spark-panels.ts"],"sourcesContent":["import { InjectionToken, Provider, Type } from '@angular/core';\nimport {\n EntityPermissions,\n EntityType,\n PersistentObject,\n SparkDeletedFilter,\n QueryResultItem,\n SparkQuery,\n} from '@mintplayer/ng-spark/models';\n\n/**\n * What a detail-page extension is told about the object on screen (#460).\n *\n * Handed to every {@link SPARK_DETAIL_PANELS} and {@link SPARK_DETAIL_ACTIONS} component through its\n * `context` input. A new object is passed whenever the page's state changes (a reload, a patch), so a\n * component reads it as a plain value and never needs to subscribe to anything.\n */\nexport interface SparkDetailContext {\n /** The entity type id or alias from the route (`/po/:type/:id`). */\n type: string;\n /** The object id from the route. */\n id: string;\n item: PersistentObject;\n entityType: EntityType;\n /**\n * Type-level rights (`/spark/permissions/{type}`). `null` while they load — treat as \"not allowed\".\n * The optional add-on flags (`canRestore`, `canViewHistory`, …) are absent on an older server.\n */\n permissions: EntityPermissions | null;\n /**\n * The soft-deletion mode the object was loaded with — the route's `?deleted=` query parameter\n * (`include` / `only`), or `null` for a normal load. `only` means the object was opened from the\n * recycle bin and is a deleted row.\n */\n deleted: SparkDeletedFilter | null;\n /** Re-fetches the object (and its permissions) with the same mode. */\n reload(): Promise<void>;\n}\n\n/**\n * What a query-page extension is told about the list on screen (#460).\n *\n * Handed to every {@link SPARK_QUERY_LIST_ACTIONS} component through its `context` input.\n */\nexport interface SparkQueryListContext {\n query: SparkQuery;\n /** Null for a query whose rows are not an entity type (a composed query). */\n entityType: EntityType | null;\n /** Type-level rights of `entityType`; null while they load or when there is no entity type. */\n permissions: EntityPermissions | null;\n /** The soft-deletion mode the list currently runs with (`exclude` unless the route says otherwise). */\n deleted: SparkDeletedFilter;\n /**\n * Switches the list's soft-deletion mode. Written to the route as `?deleted=`, so the recycle bin\n * survives a reload and the back button; `exclude` removes the parameter.\n */\n setDeleted(mode: SparkDeletedFilter): void;\n /** Re-runs the query, keeping page and sort. */\n reload(): void;\n}\n\n/**\n * A component shown under the attributes of every routed detail page (`sparkRoutes()`'s\n * `po/:type/:id`). It must declare `context = input.required<SparkDetailContext>()`.\n *\n * `sparkRoutes()` passes no templates to the pages it routes to, so a multi-provider is the only way\n * an add-on (History, Moderation) can put something on them without the app replacing the page.\n */\nexport interface SparkDetailPanel {\n /** Unique name; a second registration with the same id replaces the first. */\n id: string;\n component: Type<unknown>;\n /** Ascending; panels without an order come last, in registration order. */\n order?: number;\n}\n\n/**\n * A component rendered in the action bar of every routed detail page, after the built-in and custom\n * actions. It must declare `context = input.required<SparkDetailContext>()` and render nothing when\n * it does not apply (it is responsible for its own permission checks).\n */\nexport interface SparkDetailAction {\n id: string;\n component: Type<unknown>;\n /** Priority in the action bar's overflow ordering; defaults to 60. Lower stays visible longer. */\n priority?: number;\n}\n\n/**\n * A component rendered in the action bar of every routed query page (`query/:queryId`, `po/:type`).\n * It must declare `context = input.required<SparkQueryListContext>()`.\n */\nexport interface SparkQueryListAction {\n id: string;\n component: Type<unknown>;\n /** Priority in the action bar's overflow ordering; defaults to 60. */\n priority?: number;\n}\n\n/** What a grid row action (`SPARK_QUERY_ROW_ACTIONS`) is asked whether it is offered. */\nexport interface SparkQueryRowActionScope {\n query: SparkQuery;\n entityType: EntityType;\n permissions: EntityPermissions | null;\n /** The grid's soft-deletion mode (null when the grid sends none). */\n deleted: SparkDeletedFilter | null;\n}\n\n/** What a grid row action runs with: the scope, the one row, and the grid's refresh. */\nexport interface SparkQueryRowActionContext extends SparkQueryRowActionScope {\n row: QueryResultItem;\n /** Re-runs the grid's query (page, sort and filters kept). */\n reload(): void;\n}\n\n/**\n * An add-on entry of a Spark grid's per-row `⋮` menu (the query page and the sub-query card): an\n * action an add-on package serves on its own route, so it is not in the type's custom-action\n * catalogue (e.g. Contributions' \"Revert to this version\").\n *\n * `run` executes in the grid's injection context, so it may `inject()` synchronously before its first\n * `await`. A rejection is shown in the grid's alert (the server's message when it sent one).\n */\nexport interface SparkQueryRowAction {\n id: string;\n /** Translation key of the menu label. */\n labelKey: string;\n /** Order among the row menu's entries (custom actions are 10+); defaults to 50. */\n priority?: number;\n isOffered(scope: SparkQueryRowActionScope): boolean;\n run(context: SparkQueryRowActionContext): Promise<void>;\n}\n\n/** Multi-provider: add-on entries of every Spark grid's row menu. */\nexport const SPARK_QUERY_ROW_ACTIONS = new InjectionToken<SparkQueryRowAction[]>('SparkQueryRowActions');\n\n/** Registers grid row actions (multi — every call adds). */\nexport function provideSparkQueryRowActions(...actions: SparkQueryRowAction[]): Provider[] {\n return actions.map(action => ({ provide: SPARK_QUERY_ROW_ACTIONS, useValue: action, multi: true }));\n}\n\n/** Multi-provider: panels under the attributes of the routed detail page. */\nexport const SPARK_DETAIL_PANELS = new InjectionToken<SparkDetailPanel[]>('SparkDetailPanels');\n\n/** Multi-provider: extra buttons in the routed detail page's action bar. */\nexport const SPARK_DETAIL_ACTIONS = new InjectionToken<SparkDetailAction[]>('SparkDetailActions');\n\n/** Multi-provider: extra buttons in the routed query page's action bar. */\nexport const SPARK_QUERY_LIST_ACTIONS = new InjectionToken<SparkQueryListAction[]>('SparkQueryListActions');\n\n/** Registers detail panels (multi — every call adds). */\nexport function provideSparkDetailPanels(...panels: SparkDetailPanel[]): Provider[] {\n return panels.map(panel => ({ provide: SPARK_DETAIL_PANELS, useValue: panel, multi: true }));\n}\n\n/** Registers detail-page actions (multi — every call adds). */\nexport function provideSparkDetailActions(...actions: SparkDetailAction[]): Provider[] {\n return actions.map(action => ({ provide: SPARK_DETAIL_ACTIONS, useValue: action, multi: true }));\n}\n\n/** Registers query-page actions (multi — every call adds). */\nexport function provideSparkQueryListActions(...actions: SparkQueryListAction[]): Provider[] {\n return actions.map(action => ({ provide: SPARK_QUERY_LIST_ACTIONS, useValue: action, multi: true }));\n}\n\n/**\n * Normalises a multi-provided list: the last registration of an id wins (so an app can replace a\n * shipped panel by registering its own under the same id), then sorted by `key` ascending with\n * unordered entries last, keeping registration order among equals.\n */\nexport function orderSparkExtensions<T extends { id: string }>(\n entries: readonly T[] | null | undefined,\n key: (entry: T) => number | undefined,\n): T[] {\n if (!entries?.length) return [];\n\n const byId = new Map<string, { entry: T; index: number }>();\n entries.forEach((entry, index) => {\n const previous = byId.get(entry.id);\n // Replacement keeps the ORIGINAL slot, so overriding a shipped panel does not move it.\n byId.set(entry.id, { entry, index: previous?.index ?? index });\n });\n\n return [...byId.values()]\n .sort((a, b) => {\n const ka = key(a.entry) ?? Number.POSITIVE_INFINITY;\n const kb = key(b.entry) ?? Number.POSITIVE_INFINITY;\n return ka === kb ? a.index - b.index : ka - kb;\n })\n .map(x => x.entry);\n}\n\n/** Reads `?deleted=` into a filter, ignoring anything that is not one of the three modes. */\nexport function parseSparkDeletedParam(value: string | null | undefined): SparkDeletedFilter | null {\n switch (value?.toLowerCase()) {\n case 'include': return 'include';\n case 'only': return 'only';\n case 'exclude': return 'exclude';\n default: return null;\n }\n}\n"],"mappings":";AAsIA,MAAa,0BAA0B,IAAI,eAAsC,sBAAsB;AAGvG,SAAgB,4BAA4B,GAAG,SAA8B;CAC3E,OAAO,QAAQ,KAAI,YAAW;EAAE,SAAS;EAAyB,UAAU;EAAQ,OAAO;CAAI,EAAG;AACpG;AAGA,MAAa,sBAAsB,IAAI,eAAmC,mBAAmB;AAG7F,MAAa,uBAAuB,IAAI,eAAoC,oBAAoB;AAGhG,MAAa,2BAA2B,IAAI,eAAuC,uBAAuB;AAG1G,SAAgB,yBAAyB,GAAG,QAA0B;CACpE,OAAO,OAAO,KAAI,WAAU;EAAE,SAAS;EAAqB,UAAU;EAAO,OAAO;CAAI,EAAG;AAC7F;AAGA,SAAgB,0BAA0B,GAAG,SAA4B;CACvE,OAAO,QAAQ,KAAI,YAAW;EAAE,SAAS;EAAsB,UAAU;EAAQ,OAAO;CAAI,EAAG;AACjG;AAGA,SAAgB,6BAA6B,GAAG,SAA+B;CAC7E,OAAO,QAAQ,KAAI,YAAW;EAAE,SAAS;EAA0B,UAAU;EAAQ,OAAO;CAAI,EAAG;AACrG;AAOA,SAAgB,qBACd,SACA,KAAqC;CAErC,IAAI,CAAC,SAAS,QAAQ,OAAO,CAAA;CAE7B,MAAM,uBAAO,IAAI,IAAG;CACpB,QAAQ,SAAS,OAAO,UAAS;EAC/B,MAAM,WAAW,KAAK,IAAI,MAAM,EAAE;EAElC,KAAK,IAAI,MAAM,IAAI;GAAE;GAAO,OAAO,UAAU,SAAS;EAAK,CAAE;CAC/D,CAAC;CAED,OAAO,CAAC,GAAG,KAAK,OAAM,CAAE,CAAC,CACtB,MAAM,GAAG,MAAK;EACb,MAAM,KAAK,IAAI,EAAE,KAAK,KAAK,OAAO;EAClC,MAAM,KAAK,IAAI,EAAE,KAAK,KAAK,OAAO;EAClC,OAAO,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,KAAK;CAC9C,CAAC,CAAC,CACD,KAAI,MAAK,EAAE,KAAK;AACrB;AAGA,SAAgB,uBAAuB,OAAgC;CACrE,QAAQ,OAAO,YAAW,GAA1B;EACE,KAAK,WAAW,OAAO;EACvB,KAAK,QAAQ,OAAO;EACpB,KAAK,WAAW,OAAO;EACvB,SAAS,OAAO;CAClB;AACF"}