@joanroucoux/cairn-ui 0.11.3 → 0.11.4
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.
package/CHANGELOG.md
CHANGED
|
@@ -264,7 +264,10 @@ var UiAsync = class UiAsync {
|
|
|
264
264
|
ngImport: i0,
|
|
265
265
|
template: `
|
|
266
266
|
@if (shown() === null || shown() === 'loading') {
|
|
267
|
-
<div
|
|
267
|
+
<div
|
|
268
|
+
[attr.aria-hidden]="shown() === null ? 'true' : null"
|
|
269
|
+
[class]="shown() === null ? 'invisible contents [&_*]:animate-none!' : 'contents'"
|
|
270
|
+
>
|
|
268
271
|
<ng-content select="[asyncLoading]" />
|
|
269
272
|
</div>
|
|
270
273
|
} @else {
|
|
@@ -324,7 +327,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
324
327
|
selector: "ui-async",
|
|
325
328
|
template: `
|
|
326
329
|
@if (shown() === null || shown() === 'loading') {
|
|
327
|
-
<div
|
|
330
|
+
<div
|
|
331
|
+
[attr.aria-hidden]="shown() === null ? 'true' : null"
|
|
332
|
+
[class]="shown() === null ? 'invisible contents [&_*]:animate-none!' : 'contents'"
|
|
333
|
+
>
|
|
328
334
|
<ng-content select="[asyncLoading]" />
|
|
329
335
|
</div>
|
|
330
336
|
} @else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"joanroucoux-cairn-ui-async.mjs","names":[],"sources":["../../../projects/ui/async/delayed-state.ts","../../../projects/ui/async/async.ts"],"sourcesContent":["import { DestroyRef, type Signal, effect, inject, signal, untracked } from '@angular/core';\n\nimport type { AsyncState } from './async';\n\nexport const SKELETON_DELAY_MS = 150;\nexport const SKELETON_MIN_MS = 400;\n\n/**\n * Turns the live state of a call into the state to draw: `loading` only appears once the call has\n * lasted 150 ms and, once shown, stays at least 400 ms. Until then the previous state stays on\n * screen; `null` means no state to draw yet (a first load under 150 ms), which `ui-async` renders as an invisible skeleton. Call it in an injection context.\n *\n * @example\n * protected readonly shown = delayedState(this.state);\n */\nexport function delayedState(source: Signal<AsyncState>): Signal<AsyncState | null> {\n const displayed = signal<AsyncState | null>(null);\n let timer: ReturnType<typeof setTimeout> | undefined;\n let shownAt = 0;\n\n const clear = (): void => {\n clearTimeout(timer);\n timer = undefined;\n };\n\n inject(DestroyRef).onDestroy(clear);\n\n effect(() => {\n const state = source();\n\n untracked(() => {\n if (state === 'loading') {\n if (displayed() === 'loading') {\n clear();\n } else {\n timer = setTimeout(() => {\n timer = undefined;\n shownAt = Date.now();\n displayed.set('loading');\n }, SKELETON_DELAY_MS);\n }\n\n return;\n }\n\n clear();\n\n const remaining = shownAt + SKELETON_MIN_MS - Date.now();\n\n if (displayed() === 'loading' && remaining > 0) {\n timer = setTimeout(() => {\n timer = undefined;\n displayed.set(untracked(source));\n }, remaining);\n } else {\n displayed.set(state);\n }\n });\n });\n\n return displayed.asReadonly();\n}\n","import { Component, booleanAttribute, computed, input, output } from '@angular/core';\n\nimport { holdTransitionsUntilRendered } from '@joanroucoux/cairn-ui/motion';\n\nimport { delayedState } from './delayed-state';\n\n/** Available async block states. */\nexport const ASYNC_STATES = ['loading', 'error', 'empty', 'ready'] as const;\nexport type AsyncState = (typeof ASYNC_STATES)[number];\n\n/** Looks of the error block. */\nexport const ASYNC_VARIANTS = ['elevated', 'plain', 'emphasis', 'inline'] as const;\nexport type AsyncVariant = (typeof ASYNC_VARIANTS)[number];\n\n/** Alignments of the error block; `auto` is centered from 64rem. */\nexport const ASYNC_ALIGNS = ['start', 'center', 'auto'] as const;\nexport type AsyncAlign = (typeof ASYNC_ALIGNS)[number];\n\nconst RETRY_BASE_CLASSES =\n 'relative inline-flex items-center justify-center rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[scale,background-color] [transition-duration:var(--duration-press),var(--duration-fast)] ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) text-label';\n\nconst RETRY_OUTLINE_CLASSES =\n 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';\n\nconst RETRY_PRIMARY_CLASSES = 'bg-(--primary) text-(--primary-foreground) hover:bg-(--primary-to)';\n\nconst RETRY_BLOCK_CLASSES = 'min-h-(--row-min) gap-2 pl-3 pr-4';\n\nconst RETRY_INLINE_CLASSES = 'h-11 pointer-fine:h-8 flex-none';\n\nconst RETRY_INLINE_ICON_CLASSES = 'gap-1.5 pl-2.5 pr-3';\n\nconst RETRY_INLINE_TEXT_CLASSES = 'px-3';\n\nconst ELEVATED_SURFACE_CLASSES = 'rounded-control bg-(--elevated)';\n\nconst CARD_SURFACE_CLASSES = 'rounded-container bg-(--card) shadow-[inset_0_0_0_1px_var(--border)]';\n\ntype BlockSpec = {\n surface: string;\n gap: string;\n padding: Record<AsyncAlign, string>;\n title: string;\n message: Record<AsyncAlign, string>;\n retryMargin: Record<AsyncAlign, string>;\n retry: string;\n};\n\nconst BLOCK_SPECS: Record<Exclude<AsyncVariant, 'inline'>, BlockSpec> = {\n elevated: {\n surface: ELEVATED_SURFACE_CLASSES,\n gap: 'gap-1',\n padding: { start: 'p-4', center: 'px-4 py-10', auto: 'p-4 lg:py-10' },\n title: 'text-body font-medium',\n message: { start: 'text-label', center: 'text-label', auto: 'text-label' },\n retryMargin: { start: 'mt-2', center: 'mt-2', auto: 'mt-2' },\n retry: RETRY_OUTLINE_CLASSES,\n },\n plain: {\n surface: '',\n gap: 'gap-1',\n padding: { start: 'px-4 py-5', center: 'px-4 py-12', auto: 'px-4 py-5 lg:py-12' },\n title: 'text-body leading-[normal] font-medium',\n message: {\n start: 'text-label leading-[normal]',\n center: 'text-label leading-[normal]',\n auto: 'text-label leading-[normal]',\n },\n retryMargin: { start: 'mt-2', center: 'mt-2', auto: 'mt-2' },\n retry: RETRY_OUTLINE_CLASSES,\n },\n emphasis: {\n surface: '',\n gap: 'gap-3',\n padding: { start: 'px-4 py-6', center: 'px-4 py-14', auto: 'px-4 py-6 lg:py-14' },\n title: 'text-title font-semibold',\n message: {\n start: 'text-label',\n center: 'text-body leading-[normal]',\n auto: 'text-label lg:text-body lg:leading-[normal]',\n },\n retryMargin: { start: 'mt-1', center: 'mt-2', auto: 'mt-1 lg:mt-2' },\n retry: RETRY_PRIMARY_CLASSES,\n },\n};\n\nconst ALIGN_CLASSES: Record<AsyncAlign, string> = {\n start: 'items-start',\n center: 'items-center text-center',\n auto: 'items-start lg:items-center lg:text-center',\n};\n\nconst INLINE_BOX_CLASSES = 'flex-row items-center justify-between gap-3 pt-1.5 pr-1.5 pb-2 pl-2.5';\n\n/**\n * The three states of a block that depends on a service call, next to the ready content. During the\n * first 150 ms of a load the `[asyncLoading]` content is laid out but invisible and hidden from\n * assistive technology, so the block already holds its skeleton's space.\n *\n * @example\n * <ui-async [state]=\"totalState()\" errorTitle=\"The total couldn’t be loaded\" errorMessage=\"The server didn’t respond.\" retryLabel=\"Try again\" (retry)=\"reload()\">\n * <span asyncLoading><ui-skeleton shape=\"figure\" /></span>\n * <span asyncEmpty>Add a holding to see your total.</span>\n * <span>{{ total() }}</span>\n * </ui-async>\n */\n@Component({\n selector: 'ui-async',\n template: `\n @if (shown() === null || shown() === 'loading') {\n <div class=\"contents\" [attr.aria-hidden]=\"shown() === null ? 'true' : null\" [class.invisible]=\"shown() === null\">\n <ng-content select=\"[asyncLoading]\" />\n </div>\n } @else {\n @switch (shown()) {\n @case ('error') {\n <div aria-live=\"polite\" role=\"alert\" [class]=\"boxClasses()\">\n @if (variant() === 'inline') {\n <span class=\"text-label text-(--muted-foreground)\">{{ errorMessage() }}</span>\n } @else {\n @if (variant() === 'emphasis') {\n <h2 [class]=\"titleClasses()\">{{ errorTitle() }}</h2>\n } @else {\n <span [class]=\"titleClasses()\">{{ errorTitle() }}</span>\n }\n <span class=\"text-(--muted-foreground)\" [class]=\"messageClasses()\">{{ errorMessage() }}</span>\n }\n <button type=\"button\" [class]=\"retryButtonClasses()\" (click)=\"retry.emit()\">\n @if (retryIcon() || variant() !== 'inline') {\n <svg\n aria-hidden=\"true\"\n class=\"block flex-none fill-none stroke-current\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n viewBox=\"0 0 24 24\"\n [attr.height]=\"iconSize()\"\n [attr.width]=\"iconSize()\"\n >\n <path d=\"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8\" />\n <path d=\"M21 3v5h-5\" />\n </svg>\n }\n {{ retryLabel() }}\n </button>\n </div>\n }\n @case ('empty') {\n <ng-content select=\"[asyncEmpty]\" />\n }\n @default {\n <ng-content />\n }\n }\n }\n `,\n host: {\n '[attr.aria-busy]': \"state() === 'loading' || null\",\n '[class.flex]': 'filling()',\n '[class.flex-col]': 'filling()',\n },\n})\nexport class UiAsync {\n readonly state = input.required<AsyncState>();\n readonly errorTitle = input<string>();\n readonly errorMessage = input<string>();\n readonly retryLabel = input<string>();\n /** Draws the rotate icon in the retry button. Only the `inline` variant honors `false`. */\n readonly retryIcon = input(true, { transform: booleanAttribute });\n readonly variant = input<AsyncVariant>('elevated');\n readonly align = input<AsyncAlign>('start');\n readonly card = input(false, { transform: booleanAttribute });\n readonly fill = input(false, { transform: booleanAttribute });\n readonly retry = output<void>();\n\n protected readonly shown = delayedState(this.state);\n\n protected readonly filling = computed(() => this.fill() && this.shown() === 'error');\n\n protected readonly iconSize = computed(() => (this.variant() === 'inline' ? 16 : 18));\n\n readonly #spec = computed(() => {\n const variant = this.variant();\n\n return variant === 'inline' ? null : BLOCK_SPECS[variant];\n });\n\n protected readonly boxClasses = computed(() => {\n const spec = this.#spec();\n const fill = this.fill() ? ' flex-1 justify-center' : '';\n\n if (spec === null) {\n return `flex ${INLINE_BOX_CLASSES}${this.card() ? ` ${CARD_SURFACE_CLASSES}` : ''}${this.fill() ? ' flex-1' : ''}`;\n }\n\n const surface = this.card() ? CARD_SURFACE_CLASSES : spec.surface;\n\n return `flex flex-col ${surface} ${spec.gap} ${ALIGN_CLASSES[this.align()]} ${spec.padding[this.align()]}${fill}`;\n });\n\n protected readonly titleClasses = computed(() => this.#spec()!.title);\n\n protected readonly messageClasses = computed(() => this.#spec()!.message[this.align()]);\n\n protected readonly retryButtonClasses = computed(() => {\n const spec = this.#spec();\n\n if (spec === null) {\n const spacing = this.retryIcon() ? RETRY_INLINE_ICON_CLASSES : RETRY_INLINE_TEXT_CLASSES;\n\n return `${RETRY_BASE_CLASSES} ${RETRY_OUTLINE_CLASSES} ${RETRY_INLINE_CLASSES} ${spacing}`;\n }\n\n return `${RETRY_BASE_CLASSES} ${spec.retry} ${RETRY_BLOCK_CLASSES} ${spec.retryMargin[this.align()]}`;\n });\n\n constructor() {\n holdTransitionsUntilRendered();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"joanroucoux-cairn-ui-async.mjs","names":[],"sources":["../../../projects/ui/async/delayed-state.ts","../../../projects/ui/async/async.ts"],"sourcesContent":["import { DestroyRef, type Signal, effect, inject, signal, untracked } from '@angular/core';\n\nimport type { AsyncState } from './async';\n\nexport const SKELETON_DELAY_MS = 150;\nexport const SKELETON_MIN_MS = 400;\n\n/**\n * Turns the live state of a call into the state to draw: `loading` only appears once the call has\n * lasted 150 ms and, once shown, stays at least 400 ms. Until then the previous state stays on\n * screen; `null` means no state to draw yet (a first load under 150 ms), which `ui-async` renders as an invisible skeleton. Call it in an injection context.\n *\n * @example\n * protected readonly shown = delayedState(this.state);\n */\nexport function delayedState(source: Signal<AsyncState>): Signal<AsyncState | null> {\n const displayed = signal<AsyncState | null>(null);\n let timer: ReturnType<typeof setTimeout> | undefined;\n let shownAt = 0;\n\n const clear = (): void => {\n clearTimeout(timer);\n timer = undefined;\n };\n\n inject(DestroyRef).onDestroy(clear);\n\n effect(() => {\n const state = source();\n\n untracked(() => {\n if (state === 'loading') {\n if (displayed() === 'loading') {\n clear();\n } else {\n timer = setTimeout(() => {\n timer = undefined;\n shownAt = Date.now();\n displayed.set('loading');\n }, SKELETON_DELAY_MS);\n }\n\n return;\n }\n\n clear();\n\n const remaining = shownAt + SKELETON_MIN_MS - Date.now();\n\n if (displayed() === 'loading' && remaining > 0) {\n timer = setTimeout(() => {\n timer = undefined;\n displayed.set(untracked(source));\n }, remaining);\n } else {\n displayed.set(state);\n }\n });\n });\n\n return displayed.asReadonly();\n}\n","import { Component, booleanAttribute, computed, input, output } from '@angular/core';\n\nimport { holdTransitionsUntilRendered } from '@joanroucoux/cairn-ui/motion';\n\nimport { delayedState } from './delayed-state';\n\n/** Available async block states. */\nexport const ASYNC_STATES = ['loading', 'error', 'empty', 'ready'] as const;\nexport type AsyncState = (typeof ASYNC_STATES)[number];\n\n/** Looks of the error block. */\nexport const ASYNC_VARIANTS = ['elevated', 'plain', 'emphasis', 'inline'] as const;\nexport type AsyncVariant = (typeof ASYNC_VARIANTS)[number];\n\n/** Alignments of the error block; `auto` is centered from 64rem. */\nexport const ASYNC_ALIGNS = ['start', 'center', 'auto'] as const;\nexport type AsyncAlign = (typeof ASYNC_ALIGNS)[number];\n\nconst RETRY_BASE_CLASSES =\n 'relative inline-flex items-center justify-center rounded-control font-medium whitespace-nowrap cursor-pointer select-none touch-manipulation transition-[scale,background-color] [transition-duration:var(--duration-press),var(--duration-fast)] ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--ring) text-label';\n\nconst RETRY_OUTLINE_CLASSES =\n 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] hover:bg-(--glow)';\n\nconst RETRY_PRIMARY_CLASSES = 'bg-(--primary) text-(--primary-foreground) hover:bg-(--primary-to)';\n\nconst RETRY_BLOCK_CLASSES = 'min-h-(--row-min) gap-2 pl-3 pr-4';\n\nconst RETRY_INLINE_CLASSES = 'h-11 pointer-fine:h-8 flex-none';\n\nconst RETRY_INLINE_ICON_CLASSES = 'gap-1.5 pl-2.5 pr-3';\n\nconst RETRY_INLINE_TEXT_CLASSES = 'px-3';\n\nconst ELEVATED_SURFACE_CLASSES = 'rounded-control bg-(--elevated)';\n\nconst CARD_SURFACE_CLASSES = 'rounded-container bg-(--card) shadow-[inset_0_0_0_1px_var(--border)]';\n\ntype BlockSpec = {\n surface: string;\n gap: string;\n padding: Record<AsyncAlign, string>;\n title: string;\n message: Record<AsyncAlign, string>;\n retryMargin: Record<AsyncAlign, string>;\n retry: string;\n};\n\nconst BLOCK_SPECS: Record<Exclude<AsyncVariant, 'inline'>, BlockSpec> = {\n elevated: {\n surface: ELEVATED_SURFACE_CLASSES,\n gap: 'gap-1',\n padding: { start: 'p-4', center: 'px-4 py-10', auto: 'p-4 lg:py-10' },\n title: 'text-body font-medium',\n message: { start: 'text-label', center: 'text-label', auto: 'text-label' },\n retryMargin: { start: 'mt-2', center: 'mt-2', auto: 'mt-2' },\n retry: RETRY_OUTLINE_CLASSES,\n },\n plain: {\n surface: '',\n gap: 'gap-1',\n padding: { start: 'px-4 py-5', center: 'px-4 py-12', auto: 'px-4 py-5 lg:py-12' },\n title: 'text-body leading-[normal] font-medium',\n message: {\n start: 'text-label leading-[normal]',\n center: 'text-label leading-[normal]',\n auto: 'text-label leading-[normal]',\n },\n retryMargin: { start: 'mt-2', center: 'mt-2', auto: 'mt-2' },\n retry: RETRY_OUTLINE_CLASSES,\n },\n emphasis: {\n surface: '',\n gap: 'gap-3',\n padding: { start: 'px-4 py-6', center: 'px-4 py-14', auto: 'px-4 py-6 lg:py-14' },\n title: 'text-title font-semibold',\n message: {\n start: 'text-label',\n center: 'text-body leading-[normal]',\n auto: 'text-label lg:text-body lg:leading-[normal]',\n },\n retryMargin: { start: 'mt-1', center: 'mt-2', auto: 'mt-1 lg:mt-2' },\n retry: RETRY_PRIMARY_CLASSES,\n },\n};\n\nconst ALIGN_CLASSES: Record<AsyncAlign, string> = {\n start: 'items-start',\n center: 'items-center text-center',\n auto: 'items-start lg:items-center lg:text-center',\n};\n\nconst INLINE_BOX_CLASSES = 'flex-row items-center justify-between gap-3 pt-1.5 pr-1.5 pb-2 pl-2.5';\n\n/**\n * The three states of a block that depends on a service call, next to the ready content. During the\n * first 150 ms of a load the `[asyncLoading]` content is laid out but invisible, unanimated and hidden from\n * assistive technology, so the block already holds its skeleton's space.\n *\n * @example\n * <ui-async [state]=\"totalState()\" errorTitle=\"The total couldn’t be loaded\" errorMessage=\"The server didn’t respond.\" retryLabel=\"Try again\" (retry)=\"reload()\">\n * <span asyncLoading><ui-skeleton shape=\"figure\" /></span>\n * <span asyncEmpty>Add a holding to see your total.</span>\n * <span>{{ total() }}</span>\n * </ui-async>\n */\n@Component({\n selector: 'ui-async',\n template: `\n @if (shown() === null || shown() === 'loading') {\n <div\n [attr.aria-hidden]=\"shown() === null ? 'true' : null\"\n [class]=\"shown() === null ? 'invisible contents [&_*]:animate-none!' : 'contents'\"\n >\n <ng-content select=\"[asyncLoading]\" />\n </div>\n } @else {\n @switch (shown()) {\n @case ('error') {\n <div aria-live=\"polite\" role=\"alert\" [class]=\"boxClasses()\">\n @if (variant() === 'inline') {\n <span class=\"text-label text-(--muted-foreground)\">{{ errorMessage() }}</span>\n } @else {\n @if (variant() === 'emphasis') {\n <h2 [class]=\"titleClasses()\">{{ errorTitle() }}</h2>\n } @else {\n <span [class]=\"titleClasses()\">{{ errorTitle() }}</span>\n }\n <span class=\"text-(--muted-foreground)\" [class]=\"messageClasses()\">{{ errorMessage() }}</span>\n }\n <button type=\"button\" [class]=\"retryButtonClasses()\" (click)=\"retry.emit()\">\n @if (retryIcon() || variant() !== 'inline') {\n <svg\n aria-hidden=\"true\"\n class=\"block flex-none fill-none stroke-current\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n viewBox=\"0 0 24 24\"\n [attr.height]=\"iconSize()\"\n [attr.width]=\"iconSize()\"\n >\n <path d=\"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8\" />\n <path d=\"M21 3v5h-5\" />\n </svg>\n }\n {{ retryLabel() }}\n </button>\n </div>\n }\n @case ('empty') {\n <ng-content select=\"[asyncEmpty]\" />\n }\n @default {\n <ng-content />\n }\n }\n }\n `,\n host: {\n '[attr.aria-busy]': \"state() === 'loading' || null\",\n '[class.flex]': 'filling()',\n '[class.flex-col]': 'filling()',\n },\n})\nexport class UiAsync {\n readonly state = input.required<AsyncState>();\n readonly errorTitle = input<string>();\n readonly errorMessage = input<string>();\n readonly retryLabel = input<string>();\n /** Draws the rotate icon in the retry button. Only the `inline` variant honors `false`. */\n readonly retryIcon = input(true, { transform: booleanAttribute });\n readonly variant = input<AsyncVariant>('elevated');\n readonly align = input<AsyncAlign>('start');\n readonly card = input(false, { transform: booleanAttribute });\n readonly fill = input(false, { transform: booleanAttribute });\n readonly retry = output<void>();\n\n protected readonly shown = delayedState(this.state);\n\n protected readonly filling = computed(() => this.fill() && this.shown() === 'error');\n\n protected readonly iconSize = computed(() => (this.variant() === 'inline' ? 16 : 18));\n\n readonly #spec = computed(() => {\n const variant = this.variant();\n\n return variant === 'inline' ? null : BLOCK_SPECS[variant];\n });\n\n protected readonly boxClasses = computed(() => {\n const spec = this.#spec();\n const fill = this.fill() ? ' flex-1 justify-center' : '';\n\n if (spec === null) {\n return `flex ${INLINE_BOX_CLASSES}${this.card() ? ` ${CARD_SURFACE_CLASSES}` : ''}${this.fill() ? ' flex-1' : ''}`;\n }\n\n const surface = this.card() ? CARD_SURFACE_CLASSES : spec.surface;\n\n return `flex flex-col ${surface} ${spec.gap} ${ALIGN_CLASSES[this.align()]} ${spec.padding[this.align()]}${fill}`;\n });\n\n protected readonly titleClasses = computed(() => this.#spec()!.title);\n\n protected readonly messageClasses = computed(() => this.#spec()!.message[this.align()]);\n\n protected readonly retryButtonClasses = computed(() => {\n const spec = this.#spec();\n\n if (spec === null) {\n const spacing = this.retryIcon() ? RETRY_INLINE_ICON_CLASSES : RETRY_INLINE_TEXT_CLASSES;\n\n return `${RETRY_BASE_CLASSES} ${RETRY_OUTLINE_CLASSES} ${RETRY_INLINE_CLASSES} ${spacing}`;\n }\n\n return `${RETRY_BASE_CLASSES} ${spec.retry} ${RETRY_BLOCK_CLASSES} ${spec.retryMargin[this.align()]}`;\n });\n\n constructor() {\n holdTransitionsUntilRendered();\n }\n}\n"],"mappings":"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"}
|