@krizaka/ui 3.18.2 → 3.20.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 (186) hide show
  1. package/dist/avatar/defaults.js +6 -1
  2. package/dist/avatar/defaults.js.map +1 -1
  3. package/dist/avatar.js +6 -1
  4. package/dist/avatar.js.map +1 -1
  5. package/dist/blocks.css +11 -0
  6. package/dist/card-grid.d.ts +14 -1
  7. package/dist/card-grid.js +29 -6
  8. package/dist/card-grid.js.map +1 -1
  9. package/dist/chat.d.ts +51 -3
  10. package/dist/chat.js +64 -32
  11. package/dist/chat.js.map +1 -1
  12. package/dist/{chunk-276PTQMQ.js → chunk-3BC4XQ6C.js} +2 -1
  13. package/dist/{chunk-YIBKJ36L.js → chunk-KB7OLYIJ.js} +7 -2
  14. package/dist/{chunk-YIBKJ36L.js.map → chunk-KB7OLYIJ.js.map} +1 -1
  15. package/dist/data-table.js +1 -1
  16. package/dist/file-upload.js +2 -2
  17. package/dist/file-upload.js.map +1 -1
  18. package/dist/match-dialog.js +2 -2
  19. package/dist/media-grid.d.ts +66 -3
  20. package/dist/media-grid.js +312 -65
  21. package/dist/media-grid.js.map +1 -1
  22. package/dist/native.js +6 -1
  23. package/dist/native.js.map +1 -1
  24. package/dist/progress.js +2 -2
  25. package/dist/progress.js.map +1 -1
  26. package/dist/vertical-video.d.ts +7 -1
  27. package/dist/vertical-video.js +7 -5
  28. package/dist/vertical-video.js.map +1 -1
  29. package/package.json +8 -8
  30. package/registry/achievement.json +16 -2
  31. package/registry/action-bar.json +16 -2
  32. package/registry/action-sheet.json +15 -0
  33. package/registry/activity-feed.json +13 -2
  34. package/registry/alert.json +15 -2
  35. package/registry/app-shell.json +15 -2
  36. package/registry/argument-card.json +16 -2
  37. package/registry/audio-player.json +15 -2
  38. package/registry/auth-layout.json +15 -2
  39. package/registry/avatar.json +14 -3
  40. package/registry/badge.json +13 -2
  41. package/registry/button.json +13 -2
  42. package/registry/card-grid.json +69 -4
  43. package/registry/card.json +13 -2
  44. package/registry/catalogue-page.json +15 -2
  45. package/registry/changelog-page.json +15 -2
  46. package/registry/chat.json +115 -10
  47. package/registry/checkbox.json +13 -2
  48. package/registry/chip-link.json +15 -2
  49. package/registry/chip.json +13 -2
  50. package/registry/choice-grid.json +16 -2
  51. package/registry/cn.json +12 -0
  52. package/registry/code-block.json +15 -2
  53. package/registry/combobox.json +13 -2
  54. package/registry/command.json +15 -2
  55. package/registry/component-page.json +15 -2
  56. package/registry/confirm-button.json +15 -2
  57. package/registry/contact-page.json +15 -2
  58. package/registry/continue-prompt.json +16 -2
  59. package/registry/countdown.json +13 -2
  60. package/registry/cta-panel.json +16 -2
  61. package/registry/dashboard-layout.json +15 -2
  62. package/registry/data-table.json +15 -2
  63. package/registry/date-picker.json +13 -2
  64. package/registry/decision-list.json +16 -2
  65. package/registry/description-list.json +13 -2
  66. package/registry/detail-layout.json +15 -2
  67. package/registry/dialog.json +13 -2
  68. package/registry/docs-article-page.json +15 -2
  69. package/registry/docs-hub-page.json +15 -2
  70. package/registry/docs-layout.json +15 -2
  71. package/registry/document-viewer.json +15 -2
  72. package/registry/dropdown-menu.json +15 -2
  73. package/registry/empty-state.json +13 -2
  74. package/registry/error-page.json +14 -2
  75. package/registry/examples/card-grid/foundation.d.ts +4 -0
  76. package/registry/examples/card-grid/foundation.js +38 -0
  77. package/registry/examples/card-grid/foundation.tsx +35 -0
  78. package/registry/examples/chat/conversation.js +2 -2
  79. package/registry/examples/chat/conversation.tsx +4 -3
  80. package/registry/examples/chat/pill-composer.d.ts +4 -0
  81. package/registry/examples/chat/pill-composer.js +34 -0
  82. package/registry/examples/chat/pill-composer.tsx +39 -0
  83. package/registry/examples/glow-card/plans.js +9 -12
  84. package/registry/examples/glow-card/plans.tsx +18 -14
  85. package/registry/examples/media-grid/loading.d.ts +4 -0
  86. package/registry/examples/media-grid/loading.js +28 -0
  87. package/registry/examples/media-grid/loading.tsx +25 -0
  88. package/registry/examples/vertical-video/rail.js +3 -2
  89. package/registry/examples/vertical-video/rail.tsx +4 -3
  90. package/registry/fab.json +15 -0
  91. package/registry/field.json +13 -2
  92. package/registry/file-upload.json +15 -2
  93. package/registry/filter-bar.json +15 -2
  94. package/registry/form.json +13 -2
  95. package/registry/gallery-page.json +15 -2
  96. package/registry/glow-card.json +18 -4
  97. package/registry/goal-gauge.json +13 -2
  98. package/registry/header.json +15 -0
  99. package/registry/how-it-works-page.json +15 -2
  100. package/registry/image-cropper.json +15 -2
  101. package/registry/image-viewer.json +15 -0
  102. package/registry/index.json +1773 -10
  103. package/registry/iso.json +15 -2
  104. package/registry/kanban.json +15 -2
  105. package/registry/kbd.json +14 -2
  106. package/registry/landing-page.json +15 -2
  107. package/registry/landscape.json +15 -2
  108. package/registry/legal-page.json +15 -2
  109. package/registry/link-card.json +16 -2
  110. package/registry/list.json +13 -2
  111. package/registry/loader.json +13 -2
  112. package/registry/longform-page.json +15 -2
  113. package/registry/map.json +14 -3
  114. package/registry/markdown.json +13 -2
  115. package/registry/marketing-layout.json +15 -2
  116. package/registry/marquee.json +15 -2
  117. package/registry/mascot-quote.json +16 -2
  118. package/registry/mascot-state.json +13 -2
  119. package/registry/mascots.json +13 -2
  120. package/registry/match-dialog.json +13 -2
  121. package/registry/media-capture.json +13 -2
  122. package/registry/media-grid.json +341 -13
  123. package/registry/media-picker.json +15 -0
  124. package/registry/meter.json +13 -2
  125. package/registry/nav-bar.json +17 -3
  126. package/registry/number-ticker.json +13 -2
  127. package/registry/onboarding.json +15 -0
  128. package/registry/otp-input.json +15 -0
  129. package/registry/page-header.json +15 -2
  130. package/registry/page-hero.json +16 -2
  131. package/registry/page.json +16 -2
  132. package/registry/pagination.json +13 -2
  133. package/registry/permission-prompt.json +15 -0
  134. package/registry/photo-grid.json +13 -2
  135. package/registry/pointer-light.json +15 -2
  136. package/registry/popover.json +15 -2
  137. package/registry/pricing-page.json +15 -2
  138. package/registry/product-page.json +15 -2
  139. package/registry/profile-card.json +13 -2
  140. package/registry/progress.json +14 -3
  141. package/registry/promise-card.json +16 -2
  142. package/registry/pull-to-refresh.json +15 -0
  143. package/registry/radio-group.json +13 -2
  144. package/registry/recipes.json +14 -2
  145. package/registry/screen.json +15 -0
  146. package/registry/scroll-stage.json +15 -2
  147. package/registry/search-field.json +13 -2
  148. package/registry/section-backdrop.json +15 -2
  149. package/registry/segmented.json +15 -0
  150. package/registry/separator.json +15 -2
  151. package/registry/settings-layout.json +15 -2
  152. package/registry/showcase.json +13 -2
  153. package/registry/site-footer.json +16 -3
  154. package/registry/sites.json +15 -2
  155. package/registry/skeleton.json +13 -2
  156. package/registry/skip-link.json +14 -2
  157. package/registry/slider.json +13 -2
  158. package/registry/slot.json +14 -2
  159. package/registry/spinner.json +13 -2
  160. package/registry/splash-handoff.json +15 -0
  161. package/registry/split-view.json +15 -2
  162. package/registry/stage-banner.json +15 -2
  163. package/registry/stat.json +15 -2
  164. package/registry/status-page.json +15 -2
  165. package/registry/step-progress.json +13 -2
  166. package/registry/stepper.json +13 -2
  167. package/registry/swipe-deck.json +13 -2
  168. package/registry/swipeable-row.json +15 -0
  169. package/registry/switch.json +13 -2
  170. package/registry/tab-bar.json +15 -0
  171. package/registry/tabs.json +13 -2
  172. package/registry/tag-input.json +15 -2
  173. package/registry/tag-selector.json +13 -2
  174. package/registry/terminal-replay.json +15 -2
  175. package/registry/theme.json +13 -2
  176. package/registry/toast.json +13 -2
  177. package/registry/toc.json +15 -2
  178. package/registry/tooltip.json +14 -2
  179. package/registry/tree-view.json +15 -2
  180. package/registry/txt.json +13 -0
  181. package/registry/verified-badge.json +13 -2
  182. package/registry/vertical-video.json +32 -6
  183. package/registry/video-player.json +13 -2
  184. package/registry/video-trimmer.json +15 -2
  185. package/registry/virtual.json +15 -2
  186. /package/dist/{chunk-276PTQMQ.js.map → chunk-3BC4XQ6C.js.map} +0 -0
package/dist/chat.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/chat/chat.tsx"],"sourcesContent":["// Client (the composer's draft). A conversation's parts — the bubble with its time and receipt, the typing dots, the\n// composer, a conversation in a list — the same API as React Native. Words are props.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Avatar } from \"../avatar\";\n\nexport const chat = tv({\n slots: {\n row: \"flex flex-col gap-0.5\",\n bubble: \"max-w-[80%] rounded-2xl px-3 py-2 text-sm leading-snug\",\n context: \"mb-0.5 block text-xs opacity-85\",\n time: \"mt-0.5 block text-right text-[11px] tabular-nums\",\n status: \"flex items-center gap-1 text-xs text-fg-secondary data-[status=failed]:text-fg-danger [&>svg]:h-3.5 [&>svg]:w-3.5\",\n typing: \"inline-flex items-center gap-1 self-start rounded-2xl bg-surface-2 px-3.5 py-3\",\n dot: \"h-[7px] w-[7px] rounded-full bg-fg-secondary motion-safe:animate-[kz-typing_1.2s_ease-in-out_infinite]\",\n composer: \"flex items-end gap-2 border-t border-border-subtle bg-surface-0 px-3 py-2\",\n input:\n \"max-h-32 min-h-10 flex-1 resize-none rounded-2xl border border-border-default bg-surface-1 px-3.5 py-2.5 text-sm text-fg placeholder:text-fg-muted \" +\n \"focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n send:\n \"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent hover:bg-accent-hover focus-visible:outline-hidden \" +\n \"focus-visible:ring-2 focus-visible:ring-ring disabled:bg-surface-3 disabled:text-fg-muted [&>svg]:h-[18px] [&>svg]:w-[18px]\",\n item: \"flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n count: \"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-xs font-extrabold text-on-accent\",\n },\n});\n\nexport type MessageStatus = \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n\nexport type MessageBubbleLabels = {\n /** The words of each status, shown under the last message of yours. */\n status?: Partial<Record<MessageStatus, string>>;\n /** A failed message's retry button (“Retry”). */\n retry?: string;\n};\n\nconst STATUS: Record<MessageStatus, string> = { sending: \"Sending\", sent: \"Sent\", delivered: \"Delivered\", read: \"Read\", failed: \"Not sent\" };\n\nexport type MessageBubbleProps = {\n /** The text. */\n text: string;\n /** Yours (right, in the accent) or theirs (left). */\n mine?: boolean;\n /** The time, written by the app (“14:02”). */\n time?: string;\n /** Where a message of yours stands; shown when `showStatus`. */\n status?: MessageStatus;\n /** Show the status under the bubble (the last message of yours). */\n showStatus?: boolean;\n /** A line above the text: what it answers. */\n context?: string;\n /** Sends a failed message again (a button under it). */\n onRetry?: () => void;\n /** Part of a run of the same sender: the corner towards the next one is tighter. */\n grouped?: \"first\" | \"middle\" | \"last\" | \"single\";\n /** The words, passed translated. */\n labels?: MessageBubbleLabels;\n /** Classes merged on the row. */\n className?: string;\n};\n\nconst CORNERS = {\n mine: { first: \"rounded-br-md\", middle: \"rounded-r-md\", last: \"rounded-tr-md\", single: \"\" },\n theirs: { first: \"rounded-bl-md\", middle: \"rounded-l-md\", last: \"rounded-tl-md\", single: \"\" },\n} as const;\n\n/** A message: yours in the accent on the right, theirs on the left, the time inside, the receipt under the last of yours. */\nexport function MessageBubble({ text, mine, time, status, showStatus, context, onRetry, grouped = \"single\", labels, className }: MessageBubbleProps) {\n const s = chat();\n const words = { ...STATUS, ...labels?.status };\n return (\n <div className={s.row({ className: [mine ? \"items-end\" : \"items-start\", className].filter(Boolean).join(\" \") })}>\n <p\n className={s.bubble({\n className: [mine ? \"bg-accent text-on-accent\" : \"bg-surface-2 text-fg\", CORNERS[mine ? \"mine\" : \"theirs\"][grouped], status === \"failed\" ? \"ring-2 ring-danger\" : \"\", status === \"sending\" ? \"opacity-70\" : \"\"].join(\" \"),\n })}\n >\n {context ? <span className={s.context()}>{context}</span> : null}\n {text}\n {time ? <span className={s.time()}>{time}</span> : null}\n </p>\n {showStatus && status ? (\n <p className={s.status()} data-status={status}>\n {status === \"sent\" || status === \"delivered\" || status === \"read\" ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={status === \"read\" ? \"stroke-accent\" : \"stroke-fg-secondary\"}>\n <path d={status === \"sent\" ? \"M5 12.5l4.5 4.5L19 7.5\" : \"M2 12.5l4.5 4.5L16 7.5M9 15l2 2 9.5-9.5\"} />\n </svg>\n ) : null}\n {words[status]}\n {status === \"failed\" && onRetry ? (\n <button type=\"button\" onClick={onRetry} className=\"ml-1 font-semibold text-fg underline underline-offset-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\">\n {labels?.retry ?? \"Retry\"}\n </button>\n ) : null}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport type TypingIndicatorProps = {\n /** What is read and shown beside the dots (“Elena is typing”) — passed translated. */\n label: string;\n /** Show the words beside the dots. */\n showLabel?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Three dots that breathe while the other person types, announced politely; still under reduced motion. */\nexport function TypingIndicator({ label, showLabel, className }: TypingIndicatorProps) {\n const s = chat();\n return (\n <div role=\"status\" className={[\"flex items-center gap-2\", className].filter(Boolean).join(\" \")}>\n <style href=\"krizaka-typing\" precedence=\"default\">{\"@keyframes kz-typing{0%,60%,100%{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-2px)}}\"}</style>\n <span className={s.typing()} aria-hidden>\n {[0, 1, 2].map((i) => (\n <span key={i} className={s.dot()} style={{ animationDelay: `${i * 0.18}s` }} />\n ))}\n </span>\n <span className={showLabel ? \"text-xs text-fg-secondary\" : \"sr-only\"}>{label}</span>\n </div>\n );\n}\n\nexport type MessageComposerProps = {\n /** The draft (controlled). */\n value: string;\n /** Called on every key. */\n onValueChange: (text: string) => void;\n /** Sends the draft (Enter, or the button); Shift+Enter adds a line. */\n onSend: () => void;\n /** The input's name and placeholder (“Message”). */\n placeholder: string;\n /** The send button's name (“Send”). */\n sendLabel: string;\n /** Before the input: an attach button. */\n leading?: React.ReactNode;\n /** Cannot write: the reason in its place. */\n disabledReason?: string;\n /** The longest message. Default 4000. */\n maxLength?: number;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. */\nexport function MessageComposer({ value, onValueChange, onSend, placeholder, sendLabel, leading, disabledReason, maxLength = 4000, className }: MessageComposerProps) {\n const s = chat();\n const ready = value.trim().length > 0;\n return (\n <form\n className={s.composer({ className })}\n onSubmit={(e) => {\n e.preventDefault();\n if (ready) onSend();\n }}\n >\n {disabledReason ? (\n <p className=\"m-0 flex-1 py-2 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <>\n {leading}\n <textarea\n aria-label={placeholder}\n placeholder={placeholder}\n rows={1}\n maxLength={maxLength}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n if (ready) onSend();\n }\n }}\n className={s.input()}\n style={{ fieldSizing: \"content\" } as React.CSSProperties}\n />\n <button type=\"submit\" aria-label={sendLabel} disabled={!ready} className={s.send()}>\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>\n <path d=\"M4 12l16-8-6 16-2.5-6.5L4 12z\" />\n </svg>\n </button>\n </>\n )}\n </form>\n );\n}\n\nexport type ConversationItemProps = {\n /** The other person's name. */\n name: string;\n /** Their avatar's URL. */\n avatar?: string | null;\n /** The last message, or “typing…”. */\n preview: string;\n /** The preview is the typing signal: in the accent. */\n typing?: boolean;\n /** When (“14:02”, “Mon”). */\n time?: string;\n /** Unread messages: a count, the name in bold. */\n unread?: number;\n /** The unread count in words (“3 unread”). */\n unreadLabel?: string;\n /** Before the name: a presence dot (online). */\n online?: boolean;\n /** The presence in words (“online”). */\n onlineLabel?: string;\n /** Opens the conversation (a link). */\n href?: string;\n /** Opens the conversation (a button). */\n onSelect?: () => void;\n /** Classes merged on the row. */\n className?: string;\n};\n\n/** A conversation in the list: avatar, name, the last message or typing, the time, the unread count. */\nexport function ConversationItem({ name, avatar, preview, typing, time, unread = 0, unreadLabel, online, onlineLabel, href, onSelect, className }: ConversationItemProps) {\n const s = chat();\n const body = (\n <>\n <span className=\"relative shrink-0\">\n <Avatar src={avatar ?? null} alt=\"\" fallback={name.charAt(0)} size=\"lg\" />\n {online ? <span aria-hidden className=\"absolute bottom-0 right-0 h-3 w-3 rounded-full bg-success ring-2 ring-surface-0\" /> : null}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-sm text-fg\", unread > 0 ? \"font-extrabold\" : \"font-semibold\"].join(\" \")}>\n {name}\n {online && onlineLabel ? <span className=\"sr-only\">{`, ${onlineLabel}`}</span> : null}\n </span>\n {time ? <span className={[\"shrink-0 text-xs tabular-nums\", unread > 0 ? \"text-fg-accent\" : \"text-fg-secondary\"].join(\" \")}>{time}</span> : null}\n </span>\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-xs\", typing ? \"text-fg-accent\" : unread > 0 ? \"text-fg\" : \"text-fg-secondary\"].join(\" \")}>{preview}</span>\n {unread > 0 ? (\n <span className={s.count()}>\n <span aria-hidden>{unread > 99 ? \"99+\" : unread}</span>\n <span className=\"sr-only\">{unreadLabel ?? String(unread)}</span>\n </span>\n ) : null}\n </span>\n </span>\n </>\n );\n return href ? (\n <a href={href} className={s.item({ className })}>\n {body}\n </a>\n ) : (\n <button type=\"button\" onClick={onSelect} className={s.item({ className })}>\n {body}\n </button>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/chat/chat.tsx"],"sourcesContent":["// Client (the composer's draft). A conversation's parts — the bubble with its time and receipt, the typing dots, the\n// composer, a conversation in a list — the same API as React Native. Words are props.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Avatar } from \"../avatar\";\n\nexport const chat = tv({\n slots: {\n row: \"flex flex-col gap-0.5\",\n bubble: \"max-w-[80%] rounded-2xl px-3 py-2 text-sm leading-snug\",\n context: \"mb-0.5 block text-xs opacity-85\",\n time: \"mt-0.5 block text-right text-[11px] tabular-nums\",\n status: \"flex items-center gap-1 text-xs text-fg-secondary data-[status=failed]:text-fg-danger [&>svg]:h-3.5 [&>svg]:w-3.5\",\n typing: \"inline-flex items-center gap-1 self-start rounded-2xl bg-surface-2 px-3.5 py-3\",\n dot: \"h-[7px] w-[7px] rounded-full bg-fg-secondary motion-safe:animate-[kz-typing_1.2s_ease-in-out_infinite]\",\n composer: \"flex items-end gap-2 border-t border-border-subtle bg-surface-0 px-3 py-2\",\n input:\n \"max-h-32 min-h-10 flex-1 resize-none rounded-2xl border border-border-default bg-surface-1 px-3.5 py-2.5 text-sm text-fg placeholder:text-fg-muted \" +\n \"focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n send:\n \"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent hover:bg-accent-hover focus-visible:outline-hidden \" +\n \"focus-visible:ring-2 focus-visible:ring-ring disabled:bg-surface-3 disabled:text-fg-muted [&>svg]:h-[18px] [&>svg]:w-[18px]\",\n pillRoot: \"flex w-full flex-col gap-2\",\n pill:\n \"flex items-end gap-1.5 rounded-[1.75rem] border border-border-default bg-surface-1 py-1.5 pl-2 pr-1.5 transition-colors \" +\n \"focus-within:border-accent focus-within:ring-2 focus-within:ring-ring\",\n pillLeading: \"flex min-h-9 shrink-0 items-center gap-1 self-end\",\n pillInput:\n \"max-h-32 min-h-9 min-w-0 flex-1 resize-none border-0 bg-transparent px-1.5 py-[7px] text-sm leading-snug text-fg placeholder:text-fg-muted \" +\n \"focus:outline-hidden focus-visible:outline-hidden\",\n pillSend:\n \"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent shadow-sm transition-colors hover:bg-accent-hover \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-1 \" +\n \"disabled:cursor-not-allowed disabled:bg-surface-3 disabled:text-fg-muted disabled:shadow-none [&>svg]:h-4 [&>svg]:w-4\",\n pillStatus: \"m-0 flex min-h-5 items-center gap-1.5 px-3 text-xs text-fg-secondary [&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:shrink-0\",\n item: \"flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n count: \"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-xs font-extrabold text-on-accent\",\n },\n});\n\nexport type MessageStatus = \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n\nexport type MessageBubbleLabels = {\n /** The words of each status, shown under the last message of yours. */\n status?: Partial<Record<MessageStatus, string>>;\n /** A failed message's retry button (“Retry”). */\n retry?: string;\n};\n\nconst STATUS: Record<MessageStatus, string> = { sending: \"Sending\", sent: \"Sent\", delivered: \"Delivered\", read: \"Read\", failed: \"Not sent\" };\n\nexport type MessageBubbleProps = {\n /** The text. */\n text: string;\n /** Yours (right, in the accent) or theirs (left). */\n mine?: boolean;\n /** The time, written by the app (“14:02”). */\n time?: string;\n /** Where a message of yours stands; shown when `showStatus`. */\n status?: MessageStatus;\n /** Show the status under the bubble (the last message of yours). */\n showStatus?: boolean;\n /** A line above the text: what it answers. */\n context?: string;\n /** Sends a failed message again (a button under it). */\n onRetry?: () => void;\n /** Part of a run of the same sender: the corner towards the next one is tighter. */\n grouped?: \"first\" | \"middle\" | \"last\" | \"single\";\n /** The words, passed translated. */\n labels?: MessageBubbleLabels;\n /** Classes merged on the row. */\n className?: string;\n};\n\nconst CORNERS = {\n mine: { first: \"rounded-br-md\", middle: \"rounded-r-md\", last: \"rounded-tr-md\", single: \"\" },\n theirs: { first: \"rounded-bl-md\", middle: \"rounded-l-md\", last: \"rounded-tl-md\", single: \"\" },\n} as const;\n\n/** A message: yours in the accent on the right, theirs on the left, the time inside, the receipt under the last of yours. */\nexport function MessageBubble({ text, mine, time, status, showStatus, context, onRetry, grouped = \"single\", labels, className }: MessageBubbleProps) {\n const s = chat();\n const words = { ...STATUS, ...labels?.status };\n return (\n <div className={s.row({ className: [mine ? \"items-end\" : \"items-start\", className].filter(Boolean).join(\" \") })}>\n <p\n className={s.bubble({\n className: [mine ? \"bg-accent text-on-accent\" : \"bg-surface-2 text-fg\", CORNERS[mine ? \"mine\" : \"theirs\"][grouped], status === \"failed\" ? \"ring-2 ring-danger\" : \"\", status === \"sending\" ? \"opacity-70\" : \"\"].join(\" \"),\n })}\n >\n {context ? <span className={s.context()}>{context}</span> : null}\n {text}\n {time ? <span className={s.time()}>{time}</span> : null}\n </p>\n {showStatus && status ? (\n <p className={s.status()} data-status={status}>\n {status === \"sent\" || status === \"delivered\" || status === \"read\" ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={status === \"read\" ? \"stroke-accent\" : \"stroke-fg-secondary\"}>\n <path d={status === \"sent\" ? \"M5 12.5l4.5 4.5L19 7.5\" : \"M2 12.5l4.5 4.5L16 7.5M9 15l2 2 9.5-9.5\"} />\n </svg>\n ) : null}\n {words[status]}\n {status === \"failed\" && onRetry ? (\n <button type=\"button\" onClick={onRetry} className=\"ml-1 font-semibold text-fg underline underline-offset-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\">\n {labels?.retry ?? \"Retry\"}\n </button>\n ) : null}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport type TypingIndicatorProps = {\n /** What is read and shown beside the dots (“Elena is typing”) — passed translated. */\n label: string;\n /** Show the words beside the dots. */\n showLabel?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Three dots that breathe while the other person types, announced politely; still under reduced motion. */\nexport function TypingIndicator({ label, showLabel, className }: TypingIndicatorProps) {\n const s = chat();\n return (\n <div role=\"status\" className={[\"flex items-center gap-2\", className].filter(Boolean).join(\" \")}>\n <style href=\"krizaka-typing\" precedence=\"default\">{\"@keyframes kz-typing{0%,60%,100%{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-2px)}}\"}</style>\n <span className={s.typing()} aria-hidden>\n {[0, 1, 2].map((i) => (\n <span key={i} className={s.dot()} style={{ animationDelay: `${i * 0.18}s` }} />\n ))}\n </span>\n <span className={showLabel ? \"text-xs text-fg-secondary\" : \"sr-only\"}>{label}</span>\n </div>\n );\n}\n\nexport type MessageComposerProps = {\n /** The draft (controlled). */\n value: string;\n /** Called on every key. */\n onValueChange: (text: string) => void;\n /** Sends the draft (Enter, or the button); Shift+Enter adds a line. */\n onSend: () => void;\n /** The input's name and placeholder (“Message”). */\n placeholder: string;\n /** The send button's name (“Send”). */\n sendLabel: string;\n /** Before the input: an attach button (`bar`), or the mode/model chip and attach, inside the field (`pill`). */\n leading?: React.ReactNode;\n /** Cannot write: the reason in its place. */\n disabledReason?: string;\n /** The longest message. Default 4000. */\n maxLength?: number;\n /** `bar`: a docked bar with a square-ish field. `pill`: one rounded field holding `leading`, the text and a round send button — no inner border. Default `bar`. */\n variant?: \"bar\" | \"pill\";\n /** `pill`: the quiet line under the field (privacy, cost estimate). Its row is always reserved, so a changing line never moves the field. Read politely. */\n status?: React.ReactNode;\n /** An answer is on its way: the send is held (and `aria-busy` is set) while the field stays where it is, size included. */\n busy?: boolean;\n /** Overrides “the draft has text” — e.g. an attachment alone can be sent. A busy composer never sends. */\n canSend?: boolean;\n /** Attributes (`data-testid`, `autoFocus`, `id`…) forwarded to the textarea. */\n inputProps?: Omit<React.ComponentPropsWithoutRef<\"textarea\">, \"value\" | \"onChange\" | \"placeholder\" | \"maxLength\" | \"className\" | \"rows\">;\n /** Attributes forwarded to the send button. */\n sendProps?: Omit<React.ComponentPropsWithoutRef<\"button\">, \"type\" | \"disabled\" | \"className\" | \"aria-label\">;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. The `pill` variant is the one-field look of the Krizaka chat card. */\nexport function MessageComposer({\n value,\n onValueChange,\n onSend,\n placeholder,\n sendLabel,\n leading,\n disabledReason,\n maxLength = 4000,\n variant = \"bar\",\n status,\n busy = false,\n canSend,\n inputProps,\n sendProps,\n className,\n}: MessageComposerProps) {\n const s = chat();\n const pill = variant === \"pill\";\n const ready = !busy && (canSend ?? value.trim().length > 0);\n const textarea = (\n <textarea\n aria-label={placeholder}\n placeholder={placeholder}\n rows={1}\n maxLength={maxLength}\n {...inputProps}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={(e) => {\n inputProps?.onKeyDown?.(e);\n if (e.defaultPrevented) return;\n if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n if (ready) onSend();\n }\n }}\n className={pill ? s.pillInput() : s.input()}\n style={{ fieldSizing: \"content\" } as React.CSSProperties}\n />\n );\n const send = (\n <button type=\"submit\" aria-label={sendLabel} {...sendProps} disabled={!ready} className={pill ? s.pillSend() : s.send()}>\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>\n <path d=\"M4 12l16-8-6 16-2.5-6.5L4 12z\" />\n </svg>\n </button>\n );\n const submit = (e: React.FormEvent) => {\n e.preventDefault();\n if (ready) onSend();\n };\n if (pill) {\n return (\n <form className={s.pillRoot({ className })} onSubmit={submit} aria-busy={busy || undefined}>\n {disabledReason ? (\n <p className=\"m-0 rounded-[1.75rem] border border-border-default bg-surface-1 px-4 py-2.5 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <div className={s.pill()}>\n {leading ? <div className={s.pillLeading()}>{leading}</div> : null}\n {textarea}\n {send}\n </div>\n )}\n <p role=\"status\" aria-live=\"polite\" className={s.pillStatus()}>\n {status}\n </p>\n </form>\n );\n }\n return (\n <form className={s.composer({ className })} onSubmit={submit} aria-busy={busy || undefined}>\n {disabledReason ? (\n <p className=\"m-0 flex-1 py-2 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <>\n {leading}\n {textarea}\n {send}\n </>\n )}\n </form>\n );\n}\n\nexport type ConversationItemProps = {\n /** The other person's name. */\n name: string;\n /** Their avatar's URL. */\n avatar?: string | null;\n /** The last message, or “typing…”. */\n preview: string;\n /** The preview is the typing signal: in the accent. */\n typing?: boolean;\n /** When (“14:02”, “Mon”). */\n time?: string;\n /** Unread messages: a count, the name in bold. */\n unread?: number;\n /** The unread count in words (“3 unread”). */\n unreadLabel?: string;\n /** Before the name: a presence dot (online). */\n online?: boolean;\n /** The presence in words (“online”). */\n onlineLabel?: string;\n /** Opens the conversation (a link). */\n href?: string;\n /** Opens the conversation (a button). */\n onSelect?: () => void;\n /** Classes merged on the row. */\n className?: string;\n};\n\n/** A conversation in the list: avatar, name, the last message or typing, the time, the unread count. */\nexport function ConversationItem({ name, avatar, preview, typing, time, unread = 0, unreadLabel, online, onlineLabel, href, onSelect, className }: ConversationItemProps) {\n const s = chat();\n const body = (\n <>\n <span className=\"relative shrink-0\">\n <Avatar src={avatar ?? null} alt=\"\" fallback={name.charAt(0)} size=\"lg\" />\n {online ? <span aria-hidden className=\"absolute bottom-0 right-0 h-3 w-3 rounded-full bg-success ring-2 ring-surface-0\" /> : null}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-sm text-fg\", unread > 0 ? \"font-extrabold\" : \"font-semibold\"].join(\" \")}>\n {name}\n {online && onlineLabel ? <span className=\"sr-only\">{`, ${onlineLabel}`}</span> : null}\n </span>\n {time ? <span className={[\"shrink-0 text-xs tabular-nums\", unread > 0 ? \"text-fg-accent\" : \"text-fg-secondary\"].join(\" \")}>{time}</span> : null}\n </span>\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-xs\", typing ? \"text-fg-accent\" : unread > 0 ? \"text-fg\" : \"text-fg-secondary\"].join(\" \")}>{preview}</span>\n {unread > 0 ? (\n <span className={s.count()}>\n <span aria-hidden>{unread > 99 ? \"99+\" : unread}</span>\n <span className=\"sr-only\">{unreadLabel ?? String(unread)}</span>\n </span>\n ) : null}\n </span>\n </span>\n </>\n );\n return href ? (\n <a href={href} className={s.item({ className })}>\n {body}\n </a>\n ) : (\n <button type=\"button\" onClick={onSelect} className={s.item({ className })}>\n {body}\n </button>\n );\n}\n"],"mappings":"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","names":[]}
@@ -128,7 +128,8 @@ function IconButton({ label, size = "icon", children, ...props }) {
128
128
  }
129
129
 
130
130
  export {
131
+ LoaderGlyph,
131
132
  Button,
132
133
  IconButton
133
134
  };
134
- //# sourceMappingURL=chunk-276PTQMQ.js.map
135
+ //# sourceMappingURL=chunk-3BC4XQ6C.js.map
@@ -22,8 +22,13 @@ var resolveTone = (a) => a.tone === void 0 ? defaultAvatarToneOf(a.seed) : clamp
22
22
  function defaultAvatarFile(kind, tone, size) {
23
23
  return size === "svg" ? `${kind}-${tone}.svg` : `${kind}-${tone}-${size}.webp`;
24
24
  }
25
+ function trimTrailingSlashes(value) {
26
+ let end = value.length;
27
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
28
+ return value.slice(0, end);
29
+ }
25
30
  function defaultAvatarUrl(avatar2, { size = 256, base = DEFAULT_AVATAR_BASE } = {}) {
26
- return `${base.replace(/\/+$/, "")}/${avatar2.brand}/${defaultAvatarFile(avatar2.kind ?? "person", resolveTone(avatar2), size)}`;
31
+ return `${trimTrailingSlashes(base)}/${avatar2.brand}/${defaultAvatarFile(avatar2.kind ?? "person", resolveTone(avatar2), size)}`;
27
32
  }
28
33
 
29
34
  // src/avatar/avatar.tsx
@@ -80,4 +85,4 @@ var Avatar = Object.assign(AvatarRoot, { Group: AvatarGroup });
80
85
  export {
81
86
  Avatar
82
87
  };
83
- //# sourceMappingURL=chunk-YIBKJ36L.js.map
88
+ //# sourceMappingURL=chunk-KB7OLYIJ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/avatar/avatar.tsx","../src/avatar/default-avatar.ts"],"sourcesContent":["// Client: Radix tracks the image's loading state and shows the fallback until it has loaded (or when it fails).\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { type DefaultAvatar, defaultAvatarUrl } from \"./default-avatar\";\n\nconst avatar = tv({\n slots: {\n root: \"relative inline-flex shrink-0 overflow-hidden rounded-full bg-surface-3 align-middle\",\n image: \"h-full w-full object-cover\",\n fallback: \"flex h-full w-full items-center justify-center font-semibold uppercase text-fg-secondary\",\n group: \"flex items-center -space-x-2 [&>*]:ring-2 [&>*]:ring-surface-0\",\n },\n variants: {\n size: {\n xs: { root: \"h-5 w-5 text-[9px]\" },\n sm: { root: \"h-8 w-8 text-xs\" },\n md: { root: \"h-10 w-10 text-sm\" },\n lg: { root: \"h-14 w-14 text-base\" },\n xl: { root: \"h-20 w-20 text-lg\" },\n },\n },\n defaultVariants: { size: \"md\" },\n});\n\nexport type AvatarSize = NonNullable<VariantProps<typeof avatar>[\"size\"]>;\n\nexport type AvatarProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n /** The image's URL; `null` or nothing shows the `fallback`. */\n src?: string | null;\n /** The image's text alternative (the person's name); empty when a visible name sits next to the avatar. */\n alt?: string;\n /** Shown while the image loads, when it fails or when there is none: initials, an icon. */\n fallback?: React.ReactNode;\n /** The picture to show instead when `src` is missing or fails to load (an image URL). The `fallback` still covers its own loading. */\n fallbackSrc?: string;\n /**\n * The Krizaka default picture when the person has no photo (or it fails): their brand's gradient and a silhouette,\n * one of six tones from `seed` — `defaultAvatar={{ brand: \"orochia\", seed: user.id }}`. `fallbackSrc` wins over it.\n */\n defaultAvatar?: DefaultAvatar;\n /**\n * xs · sm · md · lg · xl.\n * @default \"md\"\n */\n size?: AvatarSize;\n};\n\nexport function AvatarRoot({ src, alt = \"\", fallback, fallbackSrc, defaultAvatar, size, className, ...props }: AvatarProps) {\n const s = avatar({ size });\n const [failed, setFailed] = React.useState<string | null>(null);\n // 512 px for the big sizes, 256 for the others: sharp on a 2x screen either way.\n const backup = fallbackSrc ?? (defaultAvatar ? defaultAvatarUrl(defaultAvatar, { size: size === \"xl\" || size === \"lg\" ? 512 : 256 }) : undefined);\n const shown = src && failed !== src ? src : backup;\n return (\n <AvatarPrimitive.Root data-size={size ?? \"md\"} className={s.root({ className })} {...props}>\n {shown && (\n <AvatarPrimitive.Image\n src={shown}\n alt={alt}\n className={s.image()}\n onLoadingStatusChange={(status) => {\n if (status === \"error\" && shown === src) setFailed(src);\n }}\n />\n )}\n <AvatarPrimitive.Fallback delayMs={shown ? 300 : 0} className={s.fallback()}>\n {fallback}\n </AvatarPrimitive.Fallback>\n </AvatarPrimitive.Root>\n );\n}\n\nexport type AvatarGroupProps = React.ComponentProps<\"div\"> & {\n /** How many avatars are shown; the rest becomes a `+n` avatar. */\n max?: number;\n /** The size of the `+n` avatar (match the avatars of the group). */\n size?: AvatarSize;\n};\n\n/** Overlapping avatars; past `max`, a `+n` avatar counts the rest. */\nexport function AvatarGroup({ max, size, className, children, ...props }: AvatarGroupProps) {\n const items = React.Children.toArray(children);\n const shown = max === undefined ? items : items.slice(0, max);\n const rest = items.length - shown.length;\n return (\n <div className={avatar().group({ className })} {...props}>\n {shown}\n {rest > 0 && <AvatarRoot size={size} fallback={`+${rest}`} />}\n </div>\n );\n}\n\n/** `Avatar` (an image with a fallback) and `Avatar.Group`. */\nexport const Avatar = Object.assign(AvatarRoot, { Group: AvatarGroup });\n","// The default profile pictures of the Krizaka family: what a product shows when a person has no photo.\n// They are generated from this file (tools/export-brand.mjs renders them to SVG and WebP and uploads them to Bunny,\n// `people/defaults/v1/<brand>/…`), so the pictures and the URLs that name them cannot drift apart.\n\nexport type DefaultAvatarBrand = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\";\n/** `person`: a neutral silhouette. `plain`: the brand's gradient alone, ready for initials. */\nexport type DefaultAvatarKind = \"person\" | \"plain\";\nexport type DefaultAvatarSize = 256 | 512;\n\nexport const DEFAULT_AVATAR_BRANDS: readonly DefaultAvatarBrand[] = [\"krizaka\", \"orazaka\", \"orochia\", \"yasakia\", \"yororis\", \"yomizaka\", \"kachizaka\", \"akinaza\"];\nexport const DEFAULT_AVATAR_KINDS: readonly DefaultAvatarKind[] = [\"person\", \"plain\"];\nexport const DEFAULT_AVATAR_SIZES: readonly DefaultAvatarSize[] = [256, 512];\n/** How many tones each brand has: a person always gets the same one from their seed, so a list is not one colour. */\nexport const DEFAULT_AVATAR_TONES = 6;\n\n/** Where the set lives. `v1` is the set's version: a changed picture is a new version folder, never an overwrite. */\nexport const DEFAULT_AVATAR_BASE = \"https://krizaka-assets.b-cdn.net/people/defaults/v1\";\n\nexport interface DefaultAvatar {\n /** The brand of the product showing the avatar: its gradient, its hues. */\n brand: DefaultAvatarBrand;\n /** Any stable text or number of the person (their id): the same seed gives the same tone everywhere. */\n seed?: string | number;\n /** Force a tone, 1 to 6, instead of deriving it from `seed`. */\n tone?: number;\n /**\n * `person` (default): a silhouette. `plain`: the gradient alone.\n * @default \"person\"\n */\n kind?: DefaultAvatarKind;\n}\n\n/** The mark gradient of each brand (BRAND.md, `$extensions[\"com.krizaka\"].mark`) as [hue, saturation %, lightness %] and the hue spread of its tones. */\nconst BRANDS: Record<DefaultAvatarBrand, { from: [number, number, number]; to: [number, number, number]; spread: number }> = {\n krizaka: { from: [217, 92, 53], to: [217, 92, 68], spread: 14 },\n orazaka: { from: [26, 90.5, 37.1], to: [26, 92, 45], spread: 7 },\n orochia: { from: [262.1, 83.3, 57.8], to: [328.6, 85.5, 70.2], spread: 12 },\n yasakia: { from: [156, 92, 32], to: [178, 90, 32], spread: 12 },\n yororis: { from: [34, 46, 44], to: [104, 28, 42], spread: 10 },\n yomizaka: { from: [232, 50, 55], to: [248, 50, 58], spread: 10 },\n kachizaka: { from: [226, 80, 56], to: [44, 92, 33], spread: 10 },\n akinaza: { from: [140, 48, 34], to: [22, 60, 46], spread: 10 },\n};\n\n/** White on top of a stop must stay visible: a stop is darkened until the contrast reaches this (WCAG 1.4.11, non-text). */\nconst MIN_CONTRAST = 3.2;\nconst OFFSETS = [0, -1, 1, -2, 2, 3] as const;\n\nfunction hslToHex([h, s, l]: [number, number, number]): string {\n const sat = s / 100;\n const light = l / 100;\n const k = (n: number) => (n + (((h % 360) + 360) % 360) / 30) % 12;\n const a = sat * Math.min(light, 1 - light);\n const channel = (n: number) => Math.round(255 * (light - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))));\n return `#${[0, 8, 4].map((n) => channel(n).toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nconst channelLuminance = (v: number) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);\nconst whiteContrast = (hex: string) => {\n const [r, g, b] = [1, 3, 5].map((i) => channelLuminance(Number.parseInt(hex.slice(i, i + 2), 16) / 255));\n return 1.05 / (0.2126 * r! + 0.7152 * g! + 0.0722 * b! + 0.05);\n};\n/** The stop, darkened one point of lightness at a time until white reads on it. */\nfunction readable([h, s, l]: [number, number, number]): string {\n let hex = hslToHex([h, s, l]);\n for (let light = l; whiteContrast(hex) < MIN_CONTRAST && light > 10; light -= 1) hex = hslToHex([h, s, light]);\n return hex;\n}\n\n/** The two stops (hex) of tone `tone` (1 to 6) of a brand. */\nexport function defaultAvatarTone(brand: DefaultAvatarBrand, tone: number): readonly [string, string] {\n const b = BRANDS[brand];\n const o = OFFSETS[(clampTone(tone) - 1) % OFFSETS.length] * b.spread;\n return [readable([b.from[0] + o, b.from[1], b.from[2]]), readable([b.to[0] + o, b.to[1], b.to[2]])];\n}\n\nconst clampTone = (tone: number) => Math.min(DEFAULT_AVATAR_TONES, Math.max(1, Math.round(tone)));\n\n/** 1 to 6 from a seed (FNV-1a): stable across renders, devices and products. */\nexport function defaultAvatarToneOf(seed: string | number | undefined): number {\n if (seed === undefined) return 1;\n let h = 0x811c9dc5;\n for (const ch of String(seed)) {\n h ^= ch.codePointAt(0) ?? 0;\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return (h % DEFAULT_AVATAR_TONES) + 1;\n}\n\nconst resolveTone = (a: DefaultAvatar) => (a.tone === undefined ? defaultAvatarToneOf(a.seed) : clampTone(a.tone));\n\n/** The file name of a picture in the set: `person-3` + 256 → `person-3-256.webp`; `svg` has no size. */\nexport function defaultAvatarFile(kind: DefaultAvatarKind, tone: number, size: DefaultAvatarSize | \"svg\"): string {\n return size === \"svg\" ? `${kind}-${tone}.svg` : `${kind}-${tone}-${size}.webp`;\n}\n\n/**\n * The URL of a default avatar: `defaultAvatarUrl({ brand: \"orochia\", seed: user.id })`.\n * @param size 256 for lists, 512 for a profile; `\"svg\"` for the vector.\n * @param base Another host or version of the set (a self-hosted copy); the default is the Krizaka CDN.\n */\nexport function defaultAvatarUrl(avatar: DefaultAvatar, { size = 256, base = DEFAULT_AVATAR_BASE }: { size?: DefaultAvatarSize | \"svg\"; base?: string } = {}): string {\n return `${base.replace(/\\/+$/, \"\")}/${avatar.brand}/${defaultAvatarFile(avatar.kind ?? \"person\", resolveTone(avatar), size)}`;\n}\n\n/** The SVG source of a default avatar, 512 × 512 and square (the Avatar clips it round). */\nexport function defaultAvatarSvg(avatar: DefaultAvatar): string {\n const [from, to] = defaultAvatarTone(avatar.brand, resolveTone(avatar));\n const person =\n (avatar.kind ?? \"person\") === \"person\"\n ? `<circle cx=\"256\" cy=\"200\" r=\"86\" fill=\"#fff\" fill-opacity=\".92\"/><path d=\"M84 512c0-108 78-182 172-182s172 74 172 182z\" fill=\"#fff\" fill-opacity=\".92\"/>`\n : \"\";\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\" width=\"512\" height=\"512\"><defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"${from}\"/><stop offset=\"1\" stop-color=\"${to}\"/></linearGradient><radialGradient id=\"l\" cx=\".28\" cy=\".18\" r=\".85\"><stop offset=\"0\" stop-color=\"#fff\" stop-opacity=\".24\"/><stop offset=\"1\" stop-color=\"#fff\" stop-opacity=\"0\"/></radialGradient></defs><rect width=\"512\" height=\"512\" fill=\"url(#g)\"/><rect width=\"512\" height=\"512\" fill=\"url(#l)\"/>${person}</svg>`;\n}\n"],"mappings":"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","names":["avatar"]}
1
+ {"version":3,"sources":["../src/avatar/avatar.tsx","../src/avatar/default-avatar.ts"],"sourcesContent":["// Client: Radix tracks the image's loading state and shows the fallback until it has loaded (or when it fails).\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { type DefaultAvatar, defaultAvatarUrl } from \"./default-avatar\";\n\nconst avatar = tv({\n slots: {\n root: \"relative inline-flex shrink-0 overflow-hidden rounded-full bg-surface-3 align-middle\",\n image: \"h-full w-full object-cover\",\n fallback: \"flex h-full w-full items-center justify-center font-semibold uppercase text-fg-secondary\",\n group: \"flex items-center -space-x-2 [&>*]:ring-2 [&>*]:ring-surface-0\",\n },\n variants: {\n size: {\n xs: { root: \"h-5 w-5 text-[9px]\" },\n sm: { root: \"h-8 w-8 text-xs\" },\n md: { root: \"h-10 w-10 text-sm\" },\n lg: { root: \"h-14 w-14 text-base\" },\n xl: { root: \"h-20 w-20 text-lg\" },\n },\n },\n defaultVariants: { size: \"md\" },\n});\n\nexport type AvatarSize = NonNullable<VariantProps<typeof avatar>[\"size\"]>;\n\nexport type AvatarProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n /** The image's URL; `null` or nothing shows the `fallback`. */\n src?: string | null;\n /** The image's text alternative (the person's name); empty when a visible name sits next to the avatar. */\n alt?: string;\n /** Shown while the image loads, when it fails or when there is none: initials, an icon. */\n fallback?: React.ReactNode;\n /** The picture to show instead when `src` is missing or fails to load (an image URL). The `fallback` still covers its own loading. */\n fallbackSrc?: string;\n /**\n * The Krizaka default picture when the person has no photo (or it fails): their brand's gradient and a silhouette,\n * one of six tones from `seed` — `defaultAvatar={{ brand: \"orochia\", seed: user.id }}`. `fallbackSrc` wins over it.\n */\n defaultAvatar?: DefaultAvatar;\n /**\n * xs · sm · md · lg · xl.\n * @default \"md\"\n */\n size?: AvatarSize;\n};\n\nexport function AvatarRoot({ src, alt = \"\", fallback, fallbackSrc, defaultAvatar, size, className, ...props }: AvatarProps) {\n const s = avatar({ size });\n const [failed, setFailed] = React.useState<string | null>(null);\n // 512 px for the big sizes, 256 for the others: sharp on a 2x screen either way.\n const backup = fallbackSrc ?? (defaultAvatar ? defaultAvatarUrl(defaultAvatar, { size: size === \"xl\" || size === \"lg\" ? 512 : 256 }) : undefined);\n const shown = src && failed !== src ? src : backup;\n return (\n <AvatarPrimitive.Root data-size={size ?? \"md\"} className={s.root({ className })} {...props}>\n {shown && (\n <AvatarPrimitive.Image\n src={shown}\n alt={alt}\n className={s.image()}\n onLoadingStatusChange={(status) => {\n if (status === \"error\" && shown === src) setFailed(src);\n }}\n />\n )}\n <AvatarPrimitive.Fallback delayMs={shown ? 300 : 0} className={s.fallback()}>\n {fallback}\n </AvatarPrimitive.Fallback>\n </AvatarPrimitive.Root>\n );\n}\n\nexport type AvatarGroupProps = React.ComponentProps<\"div\"> & {\n /** How many avatars are shown; the rest becomes a `+n` avatar. */\n max?: number;\n /** The size of the `+n` avatar (match the avatars of the group). */\n size?: AvatarSize;\n};\n\n/** Overlapping avatars; past `max`, a `+n` avatar counts the rest. */\nexport function AvatarGroup({ max, size, className, children, ...props }: AvatarGroupProps) {\n const items = React.Children.toArray(children);\n const shown = max === undefined ? items : items.slice(0, max);\n const rest = items.length - shown.length;\n return (\n <div className={avatar().group({ className })} {...props}>\n {shown}\n {rest > 0 && <AvatarRoot size={size} fallback={`+${rest}`} />}\n </div>\n );\n}\n\n/** `Avatar` (an image with a fallback) and `Avatar.Group`. */\nexport const Avatar = Object.assign(AvatarRoot, { Group: AvatarGroup });\n","// The default profile pictures of the Krizaka family: what a product shows when a person has no photo.\n// They are generated from this file (tools/export-brand.mjs renders them to SVG and WebP and uploads them to Bunny,\n// `people/defaults/v1/<brand>/…`), so the pictures and the URLs that name them cannot drift apart.\n\nexport type DefaultAvatarBrand = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\";\n/** `person`: a neutral silhouette. `plain`: the brand's gradient alone, ready for initials. */\nexport type DefaultAvatarKind = \"person\" | \"plain\";\nexport type DefaultAvatarSize = 256 | 512;\n\nexport const DEFAULT_AVATAR_BRANDS: readonly DefaultAvatarBrand[] = [\"krizaka\", \"orazaka\", \"orochia\", \"yasakia\", \"yororis\", \"yomizaka\", \"kachizaka\", \"akinaza\"];\nexport const DEFAULT_AVATAR_KINDS: readonly DefaultAvatarKind[] = [\"person\", \"plain\"];\nexport const DEFAULT_AVATAR_SIZES: readonly DefaultAvatarSize[] = [256, 512];\n/** How many tones each brand has: a person always gets the same one from their seed, so a list is not one colour. */\nexport const DEFAULT_AVATAR_TONES = 6;\n\n/** Where the set lives. `v1` is the set's version: a changed picture is a new version folder, never an overwrite. */\nexport const DEFAULT_AVATAR_BASE = \"https://krizaka-assets.b-cdn.net/people/defaults/v1\";\n\nexport interface DefaultAvatar {\n /** The brand of the product showing the avatar: its gradient, its hues. */\n brand: DefaultAvatarBrand;\n /** Any stable text or number of the person (their id): the same seed gives the same tone everywhere. */\n seed?: string | number;\n /** Force a tone, 1 to 6, instead of deriving it from `seed`. */\n tone?: number;\n /**\n * `person` (default): a silhouette. `plain`: the gradient alone.\n * @default \"person\"\n */\n kind?: DefaultAvatarKind;\n}\n\n/** The mark gradient of each brand (BRAND.md, `$extensions[\"com.krizaka\"].mark`) as [hue, saturation %, lightness %] and the hue spread of its tones. */\nconst BRANDS: Record<DefaultAvatarBrand, { from: [number, number, number]; to: [number, number, number]; spread: number }> = {\n krizaka: { from: [217, 92, 53], to: [217, 92, 68], spread: 14 },\n orazaka: { from: [26, 90.5, 37.1], to: [26, 92, 45], spread: 7 },\n orochia: { from: [262.1, 83.3, 57.8], to: [328.6, 85.5, 70.2], spread: 12 },\n yasakia: { from: [156, 92, 32], to: [178, 90, 32], spread: 12 },\n yororis: { from: [34, 46, 44], to: [104, 28, 42], spread: 10 },\n yomizaka: { from: [232, 50, 55], to: [248, 50, 58], spread: 10 },\n kachizaka: { from: [226, 80, 56], to: [44, 92, 33], spread: 10 },\n akinaza: { from: [140, 48, 34], to: [22, 60, 46], spread: 10 },\n};\n\n/** White on top of a stop must stay visible: a stop is darkened until the contrast reaches this (WCAG 1.4.11, non-text). */\nconst MIN_CONTRAST = 3.2;\nconst OFFSETS = [0, -1, 1, -2, 2, 3] as const;\n\nfunction hslToHex([h, s, l]: [number, number, number]): string {\n const sat = s / 100;\n const light = l / 100;\n const k = (n: number) => (n + (((h % 360) + 360) % 360) / 30) % 12;\n const a = sat * Math.min(light, 1 - light);\n const channel = (n: number) => Math.round(255 * (light - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))));\n return `#${[0, 8, 4].map((n) => channel(n).toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nconst channelLuminance = (v: number) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);\nconst whiteContrast = (hex: string) => {\n const [r, g, b] = [1, 3, 5].map((i) => channelLuminance(Number.parseInt(hex.slice(i, i + 2), 16) / 255));\n return 1.05 / (0.2126 * r! + 0.7152 * g! + 0.0722 * b! + 0.05);\n};\n/** The stop, darkened one point of lightness at a time until white reads on it. */\nfunction readable([h, s, l]: [number, number, number]): string {\n let hex = hslToHex([h, s, l]);\n for (let light = l; whiteContrast(hex) < MIN_CONTRAST && light > 10; light -= 1) hex = hslToHex([h, s, light]);\n return hex;\n}\n\n/** The two stops (hex) of tone `tone` (1 to 6) of a brand. */\nexport function defaultAvatarTone(brand: DefaultAvatarBrand, tone: number): readonly [string, string] {\n const b = BRANDS[brand];\n const o = OFFSETS[(clampTone(tone) - 1) % OFFSETS.length] * b.spread;\n return [readable([b.from[0] + o, b.from[1], b.from[2]]), readable([b.to[0] + o, b.to[1], b.to[2]])];\n}\n\nconst clampTone = (tone: number) => Math.min(DEFAULT_AVATAR_TONES, Math.max(1, Math.round(tone)));\n\n/** 1 to 6 from a seed (FNV-1a): stable across renders, devices and products. */\nexport function defaultAvatarToneOf(seed: string | number | undefined): number {\n if (seed === undefined) return 1;\n let h = 0x811c9dc5;\n for (const ch of String(seed)) {\n h ^= ch.codePointAt(0) ?? 0;\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return (h % DEFAULT_AVATAR_TONES) + 1;\n}\n\nconst resolveTone = (a: DefaultAvatar) => (a.tone === undefined ? defaultAvatarToneOf(a.seed) : clampTone(a.tone));\n\n/** The file name of a picture in the set: `person-3` + 256 → `person-3-256.webp`; `svg` has no size. */\nexport function defaultAvatarFile(kind: DefaultAvatarKind, tone: number, size: DefaultAvatarSize | \"svg\"): string {\n return size === \"svg\" ? `${kind}-${tone}.svg` : `${kind}-${tone}-${size}.webp`;\n}\n\n/** Linear on purpose: a `/\\/+$/` regex backtracks quadratically on a long run of slashes. */\nfunction trimTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47) end--;\n return value.slice(0, end);\n}\n\n/**\n * The URL of a default avatar: `defaultAvatarUrl({ brand: \"orochia\", seed: user.id })`.\n * @param size 256 for lists, 512 for a profile; `\"svg\"` for the vector.\n * @param base Another host or version of the set (a self-hosted copy); the default is the Krizaka CDN.\n */\nexport function defaultAvatarUrl(avatar: DefaultAvatar, { size = 256, base = DEFAULT_AVATAR_BASE }: { size?: DefaultAvatarSize | \"svg\"; base?: string } = {}): string {\n return `${trimTrailingSlashes(base)}/${avatar.brand}/${defaultAvatarFile(avatar.kind ?? \"person\", resolveTone(avatar), size)}`;\n}\n\n/** The SVG source of a default avatar, 512 × 512 and square (the Avatar clips it round). */\nexport function defaultAvatarSvg(avatar: DefaultAvatar): string {\n const [from, to] = defaultAvatarTone(avatar.brand, resolveTone(avatar));\n const person =\n (avatar.kind ?? \"person\") === \"person\"\n ? `<circle cx=\"256\" cy=\"200\" r=\"86\" fill=\"#fff\" fill-opacity=\".92\"/><path d=\"M84 512c0-108 78-182 172-182s172 74 172 182z\" fill=\"#fff\" fill-opacity=\".92\"/>`\n : \"\";\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\" width=\"512\" height=\"512\"><defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"${from}\"/><stop offset=\"1\" stop-color=\"${to}\"/></linearGradient><radialGradient id=\"l\" cx=\".28\" cy=\".18\" r=\".85\"><stop offset=\"0\" stop-color=\"#fff\" stop-opacity=\".24\"/><stop offset=\"1\" stop-color=\"#fff\" stop-opacity=\"0\"/></radialGradient></defs><rect width=\"512\" height=\"512\" fill=\"url(#g)\"/><rect width=\"512\" height=\"512\" fill=\"url(#l)\"/>${person}</svg>`;\n}\n"],"mappings":"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","names":["avatar"]}
@@ -2,7 +2,7 @@
2
2
  import "./chunk-JXLDUDTD.js";
3
3
  import {
4
4
  Button
5
- } from "./chunk-276PTQMQ.js";
5
+ } from "./chunk-3BC4XQ6C.js";
6
6
  import {
7
7
  Checkbox
8
8
  } from "./chunk-7Z4KHPFC.js";
@@ -29,8 +29,8 @@ var progress = tv({
29
29
  center: "absolute inset-0 flex flex-col items-center justify-center text-center",
30
30
  buffer: "absolute inset-y-0 left-0 -z-10 rounded-full bg-accent/25 transition-[width] duration-500 motion-reduce:transition-none",
31
31
  frame: "flex w-full flex-col gap-1.5",
32
- header: "flex items-baseline gap-2 text-xs",
33
- caption: "min-w-0 flex-1 truncate font-medium text-fg",
32
+ header: "flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs",
33
+ caption: "max-w-full shrink-0 grow truncate font-medium text-fg",
34
34
  figure: "shrink-0 tabular-nums text-fg-secondary"
35
35
  },
36
36
  variants: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/file-upload/file-upload.tsx","../src/progress/progress.tsx"],"sourcesContent":["// Client. Three parts that work alone or together:\n// - `Dropzone`: drop files on it or pick them with its button (a real `<input type=\"file\">`, so the keyboard and the\n// phone's camera / files sheet work); it checks the type, the size and the count, and hands over the accepted files\n// and the refused ones with their reason;\n// - `UploadList`: each file with its preview, size, progress, speed, and the actions its state allows (pause, resume,\n// retry, cancel, remove); the progress is a `progressbar`, a failure is announced;\n// - `useUploadQueue`: runs the app's `upload(file, { signal, onProgress, resumed })` with a concurrency, pauses by\n// aborting the signal and resumes by calling it again with `resumed: true` — what a Tus client (tus-js-client's\n// `abort()` / `start()`) or a multipart upload needs, without depending on either. The line (concurrency, retries\n// with backoff, pause per file) is `createQueue` of @krizaka/pacer.\nimport { getNumberFormat } from \"@krizaka/intl\";\nimport { type QueueOptions } from \"@krizaka/pacer\";\nimport { useQueue, useQueueSnapshot } from \"@krizaka/pacer/react\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { CloseGlyph, FileGlyph, PauseGlyph, PlayGlyph, RetryGlyph, UploadGlyph } from \"../glyphs\";\nimport { useLatest } from \"../media\";\nimport { Progress } from \"../progress\";\n\nexport const fileUpload = tv({\n slots: {\n zone:\n \"relative flex w-full flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed border-border-default bg-surface-1 px-6 py-10 text-center \" +\n \"transition-colors data-[dragging]:border-accent data-[dragging]:bg-accent-soft data-[disabled]:opacity-50 has-[:focus-visible]:border-accent\",\n zoneIcon: \"flex h-12 w-12 items-center justify-center rounded-full bg-surface-3 text-fg-secondary\",\n zoneTitle: \"text-sm font-semibold text-fg\",\n zoneHint: \"text-xs text-fg-secondary\",\n browse:\n \"inline-flex h-9 items-center justify-center rounded-lg bg-accent px-4 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-hover \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:pointer-events-none\",\n refusals: \"w-full text-left text-xs font-medium text-fg-danger\",\n list: \"flex w-full flex-col gap-2\",\n item: \"flex items-center gap-3 rounded-xl border border-border-default bg-surface-1 p-3 data-[status=error]:border-danger/60\",\n preview: \"flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-surface-3 text-fg-secondary\",\n body: \"flex min-w-0 flex-1 flex-col gap-1.5\",\n name: \"truncate text-sm font-semibold text-fg\",\n meta: \"flex flex-wrap gap-x-2 text-xs tabular-nums text-fg-secondary\",\n error: \"text-xs font-medium text-fg-danger\",\n actions: \"flex shrink-0 items-center gap-1\",\n action:\n \"inline-flex h-8 w-8 items-center justify-center rounded-md text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n});\n\n/** Why a file was refused. */\nexport type FileRejection = { file: File; reason: \"type\" | \"size\" | \"count\" };\n\n/** Whether a file matches an `accept` list (`\"video/*,.mov,image/png\"`). */\nexport function acceptsFile(file: File, accept?: string): boolean {\n if (!accept) return true;\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return accept\n .split(\",\")\n .map((rule) => rule.trim().toLowerCase())\n .filter(Boolean)\n .some((rule) => (rule.startsWith(\".\") ? name.endsWith(rule) : rule.endsWith(\"/*\") ? type.startsWith(rule.slice(0, -1)) : type === rule));\n}\n\n/** A size in the locale's words (`\"1.4 GB\"`, `\"820 kB\"`), through `@krizaka/intl`. */\nexport function formatBytes(bytes: number, locale = \"en\"): string {\n const units = [\"byte\", \"kilobyte\", \"megabyte\", \"gigabyte\", \"terabyte\"] as const;\n let value = Math.max(0, bytes);\n let unit = 0;\n while (value >= 1000 && unit < units.length - 1) {\n value /= 1000;\n unit += 1;\n }\n return getNumberFormat(locale, { style: \"unit\", unit: units[unit], unitDisplay: \"short\", maximumFractionDigits: value < 10 && unit > 0 ? 1 : 0 }).format(value);\n}\n\nexport type DropzoneProps = {\n /** The title in the zone (\"Drop your video here\"), passed translated. */\n title: string;\n /** The button that opens the file picker (\"Choose a file\"), passed translated. */\n browseLabel: string;\n /** What is accepted, in words (\"MP4 or MOV, up to 4 GB\"). */\n hint?: string;\n /** The types accepted, as the `accept` attribute (`\"video/*\"`, `\"image/png,.pdf\"`). */\n accept?: string;\n /** The largest file, in bytes. */\n maxSize?: number;\n /** The most files at once (1 unless `multiple`). */\n maxFiles?: number;\n /** Several files at once. */\n multiple?: boolean;\n /** Not available (an upload is running). */\n disabled?: boolean;\n /** On a phone, opens the camera directly (`\"environment\"` the back one, `\"user\"` the front one). */\n capture?: \"user\" | \"environment\";\n /** Called with the files accepted (never empty). */\n onFiles: (files: File[]) => void;\n /** Called with the files refused and why (show `refusalLabel`'s words, or your own). */\n onReject?: (rejections: FileRejection[]) => void;\n /** The words of a refusal, shown in the zone: `(r) => r.file.name + \" is too large\"`. Without it, nothing is shown. */\n refusalLabel?: (rejection: FileRejection) => string;\n /** An icon instead of the upload glyph. */\n icon?: React.ReactNode;\n /** Classes merged on the zone. */\n className?: string;\n};\n\n/** A zone to drop files on, with a button that opens the picker: checks type, size and count. */\nexport function Dropzone({\n title,\n browseLabel,\n hint,\n accept,\n maxSize,\n maxFiles,\n multiple,\n disabled,\n capture,\n onFiles,\n onReject,\n refusalLabel,\n icon,\n className,\n}: DropzoneProps) {\n const s = fileUpload();\n const input = React.useRef<HTMLInputElement>(null);\n const [dragging, setDragging] = React.useState(false);\n const [refused, setRefused] = React.useState<FileRejection[]>([]);\n const depth = React.useRef(0);\n const id = React.useId();\n const limit = maxFiles ?? (multiple ? Infinity : 1);\n\n const take = (list: FileList | File[]) => {\n const files = [...list];\n const accepted: File[] = [];\n const rejections: FileRejection[] = [];\n for (const file of files) {\n if (!acceptsFile(file, accept)) rejections.push({ file, reason: \"type\" });\n else if (maxSize !== undefined && file.size > maxSize) rejections.push({ file, reason: \"size\" });\n else if (accepted.length >= limit) rejections.push({ file, reason: \"count\" });\n else accepted.push(file);\n }\n setRefused(rejections);\n if (accepted.length) onFiles(accepted);\n if (rejections.length) onReject?.(rejections);\n };\n\n return (\n <div\n className={s.zone({ className })}\n data-dragging={dragging ? \"\" : undefined}\n data-disabled={disabled ? \"\" : undefined}\n onDragEnter={(event) => {\n if (disabled) return;\n event.preventDefault();\n depth.current += 1;\n setDragging(true);\n }}\n onDragOver={(event) => {\n if (disabled) return;\n event.preventDefault();\n event.dataTransfer.dropEffect = \"copy\";\n }}\n onDragLeave={() => {\n depth.current = Math.max(0, depth.current - 1);\n if (depth.current === 0) setDragging(false);\n }}\n onDrop={(event) => {\n if (disabled) return;\n event.preventDefault();\n depth.current = 0;\n setDragging(false);\n take(event.dataTransfer.files);\n }}\n >\n <span className={s.zoneIcon()} aria-hidden>\n {icon ?? <UploadGlyph className=\"h-6 w-6\" />}\n </span>\n <p id={`${id}-title`} className={s.zoneTitle()}>\n {title}\n </p>\n {hint ? (\n <p id={`${id}-hint`} className={s.zoneHint()}>\n {hint}\n </p>\n ) : null}\n <button\n type=\"button\"\n className={s.browse()}\n disabled={disabled}\n aria-describedby={[`${id}-title`, hint ? `${id}-hint` : null].filter(Boolean).join(\" \")}\n onClick={() => input.current?.click()}\n >\n {browseLabel}\n </button>\n <input\n ref={input}\n type=\"file\"\n tabIndex={-1}\n aria-hidden\n className=\"sr-only\"\n accept={accept}\n multiple={multiple}\n capture={capture}\n disabled={disabled}\n onChange={(event) => {\n if (event.target.files?.length) take(event.target.files);\n event.target.value = \"\";\n }}\n />\n {refusalLabel && refused.length > 0 ? (\n <ul role=\"alert\" className={s.refusals()}>\n {refused.map((r) => (\n <li key={`${r.file.name}-${r.reason}`}>{refusalLabel(r)}</li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n\nexport type UploadStatus = \"queued\" | \"uploading\" | \"paused\" | \"done\" | \"error\";\n\nexport type UploadItem = {\n /** A stable id. */\n id: string;\n /** The file's name. */\n name: string;\n /** Its size, in bytes. */\n size: number;\n /** Its MIME type (`video/mp4`): picks the preview. */\n type?: string;\n /** Sent so far, between 0 and 1. */\n progress: number;\n /** Where it stands. */\n status: UploadStatus;\n /** Why it failed, passed translated. */\n error?: string;\n /** A preview (an object URL of the file, a thumbnail from the server). */\n previewUrl?: string;\n /** Bytes per second, for the speed and the time left. */\n speed?: number;\n};\n\nexport type UploadListLabels = {\n /** The list's accessible name (\"Uploads\"). */\n list: string;\n pause: (name: string) => string;\n resume: (name: string) => string;\n retry: (name: string) => string;\n cancel: (name: string) => string;\n remove: (name: string) => string;\n /** A state in words: `(status) => \"Paused\"`… */\n status: (status: UploadStatus) => string;\n /** The time left: `(seconds) => \"2 min left\"`. */\n remaining: (seconds: number) => string;\n};\n\nexport const uploadListLabels: UploadListLabels = {\n list: \"Uploads\",\n pause: (name) => `Pause ${name}`,\n resume: (name) => `Resume ${name}`,\n retry: (name) => `Retry ${name}`,\n cancel: (name) => `Cancel ${name}`,\n remove: (name) => `Remove ${name}`,\n status: (status) => ({ queued: \"Waiting\", uploading: \"Uploading\", paused: \"Paused\", done: \"Uploaded\", error: \"Failed\" })[status],\n remaining: (seconds) => (seconds < 60 ? `${Math.ceil(seconds)} s left` : `${Math.ceil(seconds / 60)} min left`),\n};\n\nexport type UploadListProps = {\n /** The uploads, as `useUploadQueue` gives them (or your own state). */\n items: readonly UploadItem[];\n /** Pauses an upload (shown while it uploads, when given). */\n onPause?: (id: string) => void;\n /** Resumes a paused one. */\n onResume?: (id: string) => void;\n /** Tries a failed one again. */\n onRetry?: (id: string) => void;\n /** Stops an upload under way and drops it. */\n onCancel?: (id: string) => void;\n /** Drops a finished or failed one from the list. */\n onRemove?: (id: string) => void;\n /** The locale of sizes and speeds. Default `\"en\"`. */\n locale?: string;\n /** The words, passed translated; English by default. */\n labels?: Partial<UploadListLabels>;\n /** Classes merged on the list. */\n className?: string;\n};\n\nfunction Preview({ item }: { item: UploadItem }) {\n if (item.previewUrl && item.type?.startsWith(\"image/\")) return <img src={item.previewUrl} alt=\"\" className=\"h-full w-full object-cover\" />;\n if (item.previewUrl && item.type?.startsWith(\"video/\")) return <video src={item.previewUrl} muted playsInline preload=\"metadata\" className=\"h-full w-full object-cover\" aria-hidden />;\n return <FileGlyph className=\"h-5 w-5\" />;\n}\n\n/** The files being sent: preview, size, progress, speed and time left, and the actions each state allows. */\nexport function UploadList({ items, onPause, onResume, onRetry, onCancel, onRemove, locale = \"en\", labels: given, className }: UploadListProps) {\n const labels = { ...uploadListLabels, ...given };\n const s = fileUpload();\n const action = (label: string, run: () => void, glyph: React.ReactNode) => (\n <button type=\"button\" className={s.action()} aria-label={label} title={label} onClick={run}>\n {glyph}\n </button>\n );\n return (\n <ul aria-label={labels.list} className={s.list({ className })}>\n {items.map((item) => {\n const left = item.status === \"uploading\" && item.speed ? ((1 - item.progress) * item.size) / item.speed : null;\n return (\n <li key={item.id} className={s.item()} data-status={item.status}>\n <span className={s.preview()}>\n <Preview item={item} />\n </span>\n <div className={s.body()}>\n <p className={s.name()}>{item.name}</p>\n {item.status !== \"done\" && item.status !== \"error\" ? (\n <Progress size=\"sm\" label={item.name} value={item.status === \"queued\" ? null : Math.round(item.progress * 100)} />\n ) : null}\n <p className={s.meta()}>\n <span>{labels.status(item.status)}</span>\n <span>{item.status === \"done\" ? formatBytes(item.size, locale) : `${formatBytes(item.progress * item.size, locale)} / ${formatBytes(item.size, locale)}`}</span>\n {item.status === \"uploading\" && item.speed ? <span>{formatBytes(item.speed, locale)}/s</span> : null}\n {left !== null ? <span>{labels.remaining(left)}</span> : null}\n </p>\n {item.status === \"error\" && item.error ? (\n <p role=\"alert\" className={s.error()}>\n {item.error}\n </p>\n ) : null}\n </div>\n <div className={s.actions()}>\n {item.status === \"uploading\" && onPause && action(labels.pause(item.name), () => onPause(item.id), <PauseGlyph className=\"h-4 w-4\" />)}\n {item.status === \"paused\" && onResume && action(labels.resume(item.name), () => onResume(item.id), <PlayGlyph className=\"h-4 w-4\" />)}\n {item.status === \"error\" && onRetry && action(labels.retry(item.name), () => onRetry(item.id), <RetryGlyph className=\"h-4 w-4\" />)}\n {(item.status === \"done\" || item.status === \"error\") && onRemove\n ? action(labels.remove(item.name), () => onRemove(item.id), <CloseGlyph className=\"h-4 w-4\" />)\n : item.status !== \"done\" && onCancel\n ? action(labels.cancel(item.name), () => onCancel(item.id), <CloseGlyph className=\"h-4 w-4\" />)\n : null}\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n\nexport type UploadContext = {\n /** Aborted on pause and cancel: stop sending (`tusUpload.abort()`, `xhr.abort()`). */\n signal: AbortSignal;\n /** Report the bytes sent so far. */\n onProgress: (sent: number) => void;\n /** `true` when this call resumes a paused or failed upload (a Tus client then continues from its offset). */\n resumed: boolean;\n};\n\nexport type UploadQueueOptions = {\n /** Sends one file; resolves when it is done, rejects on failure (its `message` becomes the item's error). */\n upload: (file: File, context: UploadContext) => Promise<void>;\n /** How many files go at once. Default 2. */\n concurrency?: number;\n /** How many times a failed upload is tried again by itself (with `backoff`) before it shows its error. Default 0. */\n retries?: number;\n /** The wait between retries: exponential from 1 s by default (`@krizaka/pacer`). */\n backoff?: QueueOptions[\"backoff\"];\n /** Whether an error deserves another try (a 4xx never does). Default: every error does, up to `retries`. */\n shouldRetry?: QueueOptions[\"shouldRetry\"];\n /** Makes an object URL for each file to preview (revoked when it leaves). Default `true`. */\n previews?: boolean;\n};\n\n/** What the screen reads of an upload besides its status: the file, how far it got, how fast. */\ntype UploadMeta = { file: File; progress: number; speed?: number; previewUrl?: string; last?: { at: number; sent: number } };\n\n/**\n * The state of an upload queue around the app's `upload` function: `add`, `pause`, `resume`, `retry`, `cancel`, `remove`.\n * The line itself — concurrency, retries with backoff, pause by aborting the signal — is `createQueue` of\n * `@krizaka/pacer`; this hook adds the files, their previews, their progress and their speed.\n */\nexport function useUploadQueue(options: UploadQueueOptions) {\n const { concurrency, retries, backoff, shouldRetry } = options;\n const latest = useLatest(options);\n const queue = useQueue<UploadMeta>({ concurrency: concurrency ?? 2, retries, backoff, shouldRetry });\n const jobs = useQueueSnapshot(queue);\n const metas = useLatest(new Map(jobs.map((job) => [job.id, job.meta])));\n\n const add = React.useCallback(\n (files: readonly File[]) => {\n const { previews = true } = latest.current;\n for (const file of files) {\n const id = `${file.name}-${file.size}-${Math.random().toString(36).slice(2, 8)}`;\n const previewUrl = previews && typeof URL.createObjectURL === \"function\" ? URL.createObjectURL(file) : undefined;\n queue.add(\n async ({ signal, resumed }) => {\n queue.update(id, (m) => ({ ...m, last: { at: Date.now(), sent: m.progress * file.size } }));\n try {\n await latest.current.upload(file, {\n signal,\n resumed,\n onProgress: (sent) => {\n if (signal.aborted) return;\n queue.update(id, (m) => {\n const now = Date.now();\n const last = m.last ?? { at: now, sent };\n const fresh = now - last.at > 400;\n return { ...m, progress: file.size ? Math.min(1, sent / file.size) : 0, speed: fresh ? ((sent - last.sent) / (now - last.at)) * 1000 : m.speed, last: fresh ? { at: now, sent } : last };\n });\n },\n });\n if (!signal.aborted) queue.update(id, (m) => ({ ...m, progress: 1, speed: undefined }));\n } catch (error) {\n if (!signal.aborted) queue.update(id, (m) => ({ ...m, speed: undefined }));\n throw error;\n }\n },\n { id, meta: { file, progress: 0, previewUrl } },\n );\n }\n },\n [latest, queue],\n );\n const forget = React.useCallback(\n (id: string) => {\n const url = metas.current.get(id)?.previewUrl;\n if (url) URL.revokeObjectURL(url);\n queue.cancel(id);\n },\n [metas, queue],\n );\n React.useEffect(\n () => () => {\n for (const meta of metas.current.values()) if (meta.previewUrl) URL.revokeObjectURL(meta.previewUrl);\n },\n [metas],\n );\n const pause = React.useCallback(\n (id: string) => {\n queue.pause(id);\n queue.update(id, (m) => ({ ...m, speed: undefined }));\n },\n [queue],\n );\n\n const items = React.useMemo<UploadItem[]>(\n () =>\n jobs.map(({ id, status, error, meta }) => ({\n id,\n name: meta.file.name,\n size: meta.file.size,\n type: meta.file.type,\n progress: meta.progress,\n status: status === \"running\" ? \"uploading\" : status,\n error: error?.message,\n previewUrl: meta.previewUrl,\n speed: status === \"running\" ? meta.speed : undefined,\n })),\n [jobs],\n );\n return { items, add, pause, resume: queue.resume, retry: queue.retry, cancel: forget, remove: forget };\n}\n","// Server-safe: no hook, no context. role=\"progressbar\" with aria-valuenow / aria-valuemax (none when indeterminate),\n// named by `label`. A bar or a ring; the ring can hold content in its centre (an amount, a percentage).\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const progress = tv({\n slots: {\n root: \"relative\",\n track: \"\",\n indicator: \"\",\n center: \"absolute inset-0 flex flex-col items-center justify-center text-center\",\n buffer: \"absolute inset-y-0 left-0 -z-10 rounded-full bg-accent/25 transition-[width] duration-500 motion-reduce:transition-none\",\n frame: \"flex w-full flex-col gap-1.5\",\n header: \"flex items-baseline gap-2 text-xs\",\n caption: \"min-w-0 flex-1 truncate font-medium text-fg\",\n figure: \"shrink-0 tabular-nums text-fg-secondary\",\n },\n variants: {\n variant: {\n bar: {\n root: \"relative isolate w-full overflow-hidden rounded-full bg-surface-3\",\n indicator:\n \"kz-progress h-full rounded-full bg-linear-to-r from-accent to-accent-2 transition-[width] duration-500 motion-reduce:transition-none \" +\n \"data-[state=indeterminate]:w-2/5\",\n },\n ring: {\n root: \"inline-flex shrink-0 items-center justify-center\",\n track: \"stroke-border-default\",\n indicator:\n \"stroke-accent transition-[stroke-dashoffset] duration-1000 motion-reduce:transition-none \" +\n \"data-[state=indeterminate]:origin-center data-[state=indeterminate]:animate-spin motion-reduce:data-[state=indeterminate]:animate-none\",\n },\n },\n size: { sm: {}, md: {}, lg: {} },\n status: {\n active: {},\n // Paused: the fill rests, striped — the task is waiting for someone, not broken.\n paused: { indicator: \"kz-progress-paused opacity-70\" },\n // Failed: the fill turns to the danger colour where it stopped.\n error: { indicator: \"from-danger to-danger stroke-danger\" },\n },\n },\n compoundVariants: [\n { variant: \"bar\", size: \"sm\", class: { root: \"h-1\" } },\n { variant: \"bar\", size: \"md\", class: { root: \"h-2\" } },\n { variant: \"bar\", size: \"lg\", class: { root: \"h-3\" } },\n { variant: \"ring\", size: \"sm\", class: { root: \"h-10 w-10\" } },\n { variant: \"ring\", size: \"md\", class: { root: \"h-20 w-20\" } },\n { variant: \"ring\", size: \"lg\", class: { root: \"h-36 w-36\" } },\n ],\n defaultVariants: { variant: \"bar\", size: \"md\", status: \"active\" },\n});\n\nexport type ProgressVariants = VariantProps<typeof progress>;\n\nexport type ProgressProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n ProgressVariants & {\n /** bar · ring (a ring holds its `children` in the centre). */\n variant?: ProgressVariants[\"variant\"];\n /** sm · md · lg: the bar's thickness, the ring's diameter. */\n size?: ProgressVariants[\"size\"];\n /** How far, between 0 and `max`. `null` or absent: indeterminate (a wait of unknown length). */\n value?: number | null;\n /** The value that means complete. */\n max?: number;\n /** The accessible name — passed translated (\"Upload\", \"Raised towards the goal\"). */\n label: string;\n /** The value in words for assistive technology (\"$420 of $1,000\") — default: the percentage. */\n valueText?: string;\n /** A ring's centre: an amount, a percentage, an icon. */\n children?: React.ReactNode;\n /** How far is already available beyond the value (a video buffered ahead), between 0 and `max` — bar only. */\n buffer?: number;\n /** Splits the bar or the ring into this many segments: the steps of a task, the parts of a goal. */\n segments?: number;\n /**\n * active · paused (the fill rests, striped: waiting for someone) · error (the fill turns to the danger colour where it stopped).\n * @default \"active\"\n */\n status?: ProgressVariants[\"status\"];\n /** Shows the label above the bar, with the percentage (or `valueText`) and `eta` — bar only. */\n showLabel?: boolean;\n /** The time left, in words (“About 2 min left”), shown with `showLabel` and read with the value — passed translated. */\n eta?: string;\n };\n\nconst STROKE = { sm: 12, md: 9, lg: 7 } as const;\n\n/** A progress bar or ring: `value` / `max`, indeterminate without a value. State: `data-state` (loading · complete · indeterminate). */\nexport function Progress({\n variant = \"bar\",\n size = \"md\",\n value,\n max = 100,\n label,\n valueText,\n className,\n children,\n buffer,\n segments,\n status = \"active\",\n showLabel,\n eta,\n ...props\n}: ProgressProps) {\n const s = progress({ variant, size, status });\n const maskId = `kz-seg-${React.useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n const known = typeof value === \"number\" && Number.isFinite(value);\n const ratio = known ? Math.min(1, Math.max(0, value / (max || 1))) : 0;\n const percent = Math.round(ratio * 100);\n const state = !known ? \"indeterminate\" : ratio >= 1 ? \"complete\" : \"loading\";\n const aria = {\n role: \"progressbar\" as const,\n \"aria-label\": label,\n \"aria-valuemin\": known ? 0 : undefined,\n \"aria-valuemax\": known ? max : undefined,\n \"aria-valuenow\": known ? Math.min(max, Math.max(0, value)) : undefined,\n \"aria-valuetext\": [known ? (valueText ?? `${percent}%`) : valueText, eta].filter(Boolean).join(\", \") || undefined,\n \"data-state\": state,\n \"data-status\": status === \"active\" ? undefined : status,\n \"data-value\": known ? value : undefined,\n \"data-variant\": variant,\n };\n if (variant === \"ring\") {\n const stroke = STROKE[size];\n const r = 50 - stroke / 2;\n return (\n <div {...aria} className={s.root({ className })} {...props}>\n <svg viewBox=\"0 0 100 100\" className=\"h-full w-full -rotate-90\" aria-hidden>\n {segments && segments > 1 && (\n <mask id={maskId}>\n <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" stroke=\"white\" strokeWidth={stroke + 2} pathLength={100} strokeDasharray={`${100 / segments - 2} 2`} />\n </mask>\n )}\n <g mask={segments && segments > 1 ? `url(#${maskId})` : undefined}>\n <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" strokeWidth={stroke} className={s.track()} />\n <circle\n cx=\"50\"\n cy=\"50\"\n r={r}\n fill=\"none\"\n strokeWidth={stroke}\n strokeLinecap=\"round\"\n pathLength={100}\n strokeDasharray={100}\n strokeDashoffset={known ? 100 - percent : 75}\n data-state={state}\n className={s.indicator()}\n />\n </g>\n </svg>\n {children !== undefined && <div className={s.center()}>{children}</div>}\n </div>\n );\n }\n const segmented = segments && segments > 1 ? ({ \"--kz-segments\": segments } as React.CSSProperties) : undefined;\n const bufferRatio = typeof buffer === \"number\" && Number.isFinite(buffer) ? Math.min(1, Math.max(0, buffer / (max || 1))) : null;\n const bar = (\n <div {...aria} className={s.root({ className: [segmented && \"kz-progress-segmented\", className].filter(Boolean).join(\" \") || undefined })} style={segmented} {...props}>\n <div data-state={state} className={s.indicator()} style={known ? { width: `${ratio * 100}%` } : undefined} />\n {bufferRatio !== null && <div aria-hidden className={s.buffer()} style={{ width: `${bufferRatio * 100}%` }} />}\n </div>\n );\n if (!showLabel) return bar;\n const figure = [known ? (valueText ?? `${percent} %`) : valueText, eta].filter(Boolean).join(\" · \");\n return (\n <div className={s.frame()}>\n <div aria-hidden className={s.header()}>\n <span className={s.caption()}>{label}</span>\n {figure && <span className={s.figure()}>{figure}</span>}\n </div>\n {bar}\n </div>\n );\n}\n"],"mappings":"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","names":["React","tv","jsx","jsxs","tv"]}
1
+ {"version":3,"sources":["../src/file-upload/file-upload.tsx","../src/progress/progress.tsx"],"sourcesContent":["// Client. Three parts that work alone or together:\n// - `Dropzone`: drop files on it or pick them with its button (a real `<input type=\"file\">`, so the keyboard and the\n// phone's camera / files sheet work); it checks the type, the size and the count, and hands over the accepted files\n// and the refused ones with their reason;\n// - `UploadList`: each file with its preview, size, progress, speed, and the actions its state allows (pause, resume,\n// retry, cancel, remove); the progress is a `progressbar`, a failure is announced;\n// - `useUploadQueue`: runs the app's `upload(file, { signal, onProgress, resumed })` with a concurrency, pauses by\n// aborting the signal and resumes by calling it again with `resumed: true` — what a Tus client (tus-js-client's\n// `abort()` / `start()`) or a multipart upload needs, without depending on either. The line (concurrency, retries\n// with backoff, pause per file) is `createQueue` of @krizaka/pacer.\nimport { getNumberFormat } from \"@krizaka/intl\";\nimport { type QueueOptions } from \"@krizaka/pacer\";\nimport { useQueue, useQueueSnapshot } from \"@krizaka/pacer/react\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { CloseGlyph, FileGlyph, PauseGlyph, PlayGlyph, RetryGlyph, UploadGlyph } from \"../glyphs\";\nimport { useLatest } from \"../media\";\nimport { Progress } from \"../progress\";\n\nexport const fileUpload = tv({\n slots: {\n zone:\n \"relative flex w-full flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed border-border-default bg-surface-1 px-6 py-10 text-center \" +\n \"transition-colors data-[dragging]:border-accent data-[dragging]:bg-accent-soft data-[disabled]:opacity-50 has-[:focus-visible]:border-accent\",\n zoneIcon: \"flex h-12 w-12 items-center justify-center rounded-full bg-surface-3 text-fg-secondary\",\n zoneTitle: \"text-sm font-semibold text-fg\",\n zoneHint: \"text-xs text-fg-secondary\",\n browse:\n \"inline-flex h-9 items-center justify-center rounded-lg bg-accent px-4 text-sm font-semibold text-on-accent transition-colors hover:bg-accent-hover \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:pointer-events-none\",\n refusals: \"w-full text-left text-xs font-medium text-fg-danger\",\n list: \"flex w-full flex-col gap-2\",\n item: \"flex items-center gap-3 rounded-xl border border-border-default bg-surface-1 p-3 data-[status=error]:border-danger/60\",\n preview: \"flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-surface-3 text-fg-secondary\",\n body: \"flex min-w-0 flex-1 flex-col gap-1.5\",\n name: \"truncate text-sm font-semibold text-fg\",\n meta: \"flex flex-wrap gap-x-2 text-xs tabular-nums text-fg-secondary\",\n error: \"text-xs font-medium text-fg-danger\",\n actions: \"flex shrink-0 items-center gap-1\",\n action:\n \"inline-flex h-8 w-8 items-center justify-center rounded-md text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n});\n\n/** Why a file was refused. */\nexport type FileRejection = { file: File; reason: \"type\" | \"size\" | \"count\" };\n\n/** Whether a file matches an `accept` list (`\"video/*,.mov,image/png\"`). */\nexport function acceptsFile(file: File, accept?: string): boolean {\n if (!accept) return true;\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return accept\n .split(\",\")\n .map((rule) => rule.trim().toLowerCase())\n .filter(Boolean)\n .some((rule) => (rule.startsWith(\".\") ? name.endsWith(rule) : rule.endsWith(\"/*\") ? type.startsWith(rule.slice(0, -1)) : type === rule));\n}\n\n/** A size in the locale's words (`\"1.4 GB\"`, `\"820 kB\"`), through `@krizaka/intl`. */\nexport function formatBytes(bytes: number, locale = \"en\"): string {\n const units = [\"byte\", \"kilobyte\", \"megabyte\", \"gigabyte\", \"terabyte\"] as const;\n let value = Math.max(0, bytes);\n let unit = 0;\n while (value >= 1000 && unit < units.length - 1) {\n value /= 1000;\n unit += 1;\n }\n return getNumberFormat(locale, { style: \"unit\", unit: units[unit], unitDisplay: \"short\", maximumFractionDigits: value < 10 && unit > 0 ? 1 : 0 }).format(value);\n}\n\nexport type DropzoneProps = {\n /** The title in the zone (\"Drop your video here\"), passed translated. */\n title: string;\n /** The button that opens the file picker (\"Choose a file\"), passed translated. */\n browseLabel: string;\n /** What is accepted, in words (\"MP4 or MOV, up to 4 GB\"). */\n hint?: string;\n /** The types accepted, as the `accept` attribute (`\"video/*\"`, `\"image/png,.pdf\"`). */\n accept?: string;\n /** The largest file, in bytes. */\n maxSize?: number;\n /** The most files at once (1 unless `multiple`). */\n maxFiles?: number;\n /** Several files at once. */\n multiple?: boolean;\n /** Not available (an upload is running). */\n disabled?: boolean;\n /** On a phone, opens the camera directly (`\"environment\"` the back one, `\"user\"` the front one). */\n capture?: \"user\" | \"environment\";\n /** Called with the files accepted (never empty). */\n onFiles: (files: File[]) => void;\n /** Called with the files refused and why (show `refusalLabel`'s words, or your own). */\n onReject?: (rejections: FileRejection[]) => void;\n /** The words of a refusal, shown in the zone: `(r) => r.file.name + \" is too large\"`. Without it, nothing is shown. */\n refusalLabel?: (rejection: FileRejection) => string;\n /** An icon instead of the upload glyph. */\n icon?: React.ReactNode;\n /** Classes merged on the zone. */\n className?: string;\n};\n\n/** A zone to drop files on, with a button that opens the picker: checks type, size and count. */\nexport function Dropzone({\n title,\n browseLabel,\n hint,\n accept,\n maxSize,\n maxFiles,\n multiple,\n disabled,\n capture,\n onFiles,\n onReject,\n refusalLabel,\n icon,\n className,\n}: DropzoneProps) {\n const s = fileUpload();\n const input = React.useRef<HTMLInputElement>(null);\n const [dragging, setDragging] = React.useState(false);\n const [refused, setRefused] = React.useState<FileRejection[]>([]);\n const depth = React.useRef(0);\n const id = React.useId();\n const limit = maxFiles ?? (multiple ? Infinity : 1);\n\n const take = (list: FileList | File[]) => {\n const files = [...list];\n const accepted: File[] = [];\n const rejections: FileRejection[] = [];\n for (const file of files) {\n if (!acceptsFile(file, accept)) rejections.push({ file, reason: \"type\" });\n else if (maxSize !== undefined && file.size > maxSize) rejections.push({ file, reason: \"size\" });\n else if (accepted.length >= limit) rejections.push({ file, reason: \"count\" });\n else accepted.push(file);\n }\n setRefused(rejections);\n if (accepted.length) onFiles(accepted);\n if (rejections.length) onReject?.(rejections);\n };\n\n return (\n <div\n className={s.zone({ className })}\n data-dragging={dragging ? \"\" : undefined}\n data-disabled={disabled ? \"\" : undefined}\n onDragEnter={(event) => {\n if (disabled) return;\n event.preventDefault();\n depth.current += 1;\n setDragging(true);\n }}\n onDragOver={(event) => {\n if (disabled) return;\n event.preventDefault();\n event.dataTransfer.dropEffect = \"copy\";\n }}\n onDragLeave={() => {\n depth.current = Math.max(0, depth.current - 1);\n if (depth.current === 0) setDragging(false);\n }}\n onDrop={(event) => {\n if (disabled) return;\n event.preventDefault();\n depth.current = 0;\n setDragging(false);\n take(event.dataTransfer.files);\n }}\n >\n <span className={s.zoneIcon()} aria-hidden>\n {icon ?? <UploadGlyph className=\"h-6 w-6\" />}\n </span>\n <p id={`${id}-title`} className={s.zoneTitle()}>\n {title}\n </p>\n {hint ? (\n <p id={`${id}-hint`} className={s.zoneHint()}>\n {hint}\n </p>\n ) : null}\n <button\n type=\"button\"\n className={s.browse()}\n disabled={disabled}\n aria-describedby={[`${id}-title`, hint ? `${id}-hint` : null].filter(Boolean).join(\" \")}\n onClick={() => input.current?.click()}\n >\n {browseLabel}\n </button>\n <input\n ref={input}\n type=\"file\"\n tabIndex={-1}\n aria-hidden\n className=\"sr-only\"\n accept={accept}\n multiple={multiple}\n capture={capture}\n disabled={disabled}\n onChange={(event) => {\n if (event.target.files?.length) take(event.target.files);\n event.target.value = \"\";\n }}\n />\n {refusalLabel && refused.length > 0 ? (\n <ul role=\"alert\" className={s.refusals()}>\n {refused.map((r) => (\n <li key={`${r.file.name}-${r.reason}`}>{refusalLabel(r)}</li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n\nexport type UploadStatus = \"queued\" | \"uploading\" | \"paused\" | \"done\" | \"error\";\n\nexport type UploadItem = {\n /** A stable id. */\n id: string;\n /** The file's name. */\n name: string;\n /** Its size, in bytes. */\n size: number;\n /** Its MIME type (`video/mp4`): picks the preview. */\n type?: string;\n /** Sent so far, between 0 and 1. */\n progress: number;\n /** Where it stands. */\n status: UploadStatus;\n /** Why it failed, passed translated. */\n error?: string;\n /** A preview (an object URL of the file, a thumbnail from the server). */\n previewUrl?: string;\n /** Bytes per second, for the speed and the time left. */\n speed?: number;\n};\n\nexport type UploadListLabels = {\n /** The list's accessible name (\"Uploads\"). */\n list: string;\n pause: (name: string) => string;\n resume: (name: string) => string;\n retry: (name: string) => string;\n cancel: (name: string) => string;\n remove: (name: string) => string;\n /** A state in words: `(status) => \"Paused\"`… */\n status: (status: UploadStatus) => string;\n /** The time left: `(seconds) => \"2 min left\"`. */\n remaining: (seconds: number) => string;\n};\n\nexport const uploadListLabels: UploadListLabels = {\n list: \"Uploads\",\n pause: (name) => `Pause ${name}`,\n resume: (name) => `Resume ${name}`,\n retry: (name) => `Retry ${name}`,\n cancel: (name) => `Cancel ${name}`,\n remove: (name) => `Remove ${name}`,\n status: (status) => ({ queued: \"Waiting\", uploading: \"Uploading\", paused: \"Paused\", done: \"Uploaded\", error: \"Failed\" })[status],\n remaining: (seconds) => (seconds < 60 ? `${Math.ceil(seconds)} s left` : `${Math.ceil(seconds / 60)} min left`),\n};\n\nexport type UploadListProps = {\n /** The uploads, as `useUploadQueue` gives them (or your own state). */\n items: readonly UploadItem[];\n /** Pauses an upload (shown while it uploads, when given). */\n onPause?: (id: string) => void;\n /** Resumes a paused one. */\n onResume?: (id: string) => void;\n /** Tries a failed one again. */\n onRetry?: (id: string) => void;\n /** Stops an upload under way and drops it. */\n onCancel?: (id: string) => void;\n /** Drops a finished or failed one from the list. */\n onRemove?: (id: string) => void;\n /** The locale of sizes and speeds. Default `\"en\"`. */\n locale?: string;\n /** The words, passed translated; English by default. */\n labels?: Partial<UploadListLabels>;\n /** Classes merged on the list. */\n className?: string;\n};\n\nfunction Preview({ item }: { item: UploadItem }) {\n if (item.previewUrl && item.type?.startsWith(\"image/\")) return <img src={item.previewUrl} alt=\"\" className=\"h-full w-full object-cover\" />;\n if (item.previewUrl && item.type?.startsWith(\"video/\")) return <video src={item.previewUrl} muted playsInline preload=\"metadata\" className=\"h-full w-full object-cover\" aria-hidden />;\n return <FileGlyph className=\"h-5 w-5\" />;\n}\n\n/** The files being sent: preview, size, progress, speed and time left, and the actions each state allows. */\nexport function UploadList({ items, onPause, onResume, onRetry, onCancel, onRemove, locale = \"en\", labels: given, className }: UploadListProps) {\n const labels = { ...uploadListLabels, ...given };\n const s = fileUpload();\n const action = (label: string, run: () => void, glyph: React.ReactNode) => (\n <button type=\"button\" className={s.action()} aria-label={label} title={label} onClick={run}>\n {glyph}\n </button>\n );\n return (\n <ul aria-label={labels.list} className={s.list({ className })}>\n {items.map((item) => {\n const left = item.status === \"uploading\" && item.speed ? ((1 - item.progress) * item.size) / item.speed : null;\n return (\n <li key={item.id} className={s.item()} data-status={item.status}>\n <span className={s.preview()}>\n <Preview item={item} />\n </span>\n <div className={s.body()}>\n <p className={s.name()}>{item.name}</p>\n {item.status !== \"done\" && item.status !== \"error\" ? (\n <Progress size=\"sm\" label={item.name} value={item.status === \"queued\" ? null : Math.round(item.progress * 100)} />\n ) : null}\n <p className={s.meta()}>\n <span>{labels.status(item.status)}</span>\n <span>{item.status === \"done\" ? formatBytes(item.size, locale) : `${formatBytes(item.progress * item.size, locale)} / ${formatBytes(item.size, locale)}`}</span>\n {item.status === \"uploading\" && item.speed ? <span>{formatBytes(item.speed, locale)}/s</span> : null}\n {left !== null ? <span>{labels.remaining(left)}</span> : null}\n </p>\n {item.status === \"error\" && item.error ? (\n <p role=\"alert\" className={s.error()}>\n {item.error}\n </p>\n ) : null}\n </div>\n <div className={s.actions()}>\n {item.status === \"uploading\" && onPause && action(labels.pause(item.name), () => onPause(item.id), <PauseGlyph className=\"h-4 w-4\" />)}\n {item.status === \"paused\" && onResume && action(labels.resume(item.name), () => onResume(item.id), <PlayGlyph className=\"h-4 w-4\" />)}\n {item.status === \"error\" && onRetry && action(labels.retry(item.name), () => onRetry(item.id), <RetryGlyph className=\"h-4 w-4\" />)}\n {(item.status === \"done\" || item.status === \"error\") && onRemove\n ? action(labels.remove(item.name), () => onRemove(item.id), <CloseGlyph className=\"h-4 w-4\" />)\n : item.status !== \"done\" && onCancel\n ? action(labels.cancel(item.name), () => onCancel(item.id), <CloseGlyph className=\"h-4 w-4\" />)\n : null}\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n\nexport type UploadContext = {\n /** Aborted on pause and cancel: stop sending (`tusUpload.abort()`, `xhr.abort()`). */\n signal: AbortSignal;\n /** Report the bytes sent so far. */\n onProgress: (sent: number) => void;\n /** `true` when this call resumes a paused or failed upload (a Tus client then continues from its offset). */\n resumed: boolean;\n};\n\nexport type UploadQueueOptions = {\n /** Sends one file; resolves when it is done, rejects on failure (its `message` becomes the item's error). */\n upload: (file: File, context: UploadContext) => Promise<void>;\n /** How many files go at once. Default 2. */\n concurrency?: number;\n /** How many times a failed upload is tried again by itself (with `backoff`) before it shows its error. Default 0. */\n retries?: number;\n /** The wait between retries: exponential from 1 s by default (`@krizaka/pacer`). */\n backoff?: QueueOptions[\"backoff\"];\n /** Whether an error deserves another try (a 4xx never does). Default: every error does, up to `retries`. */\n shouldRetry?: QueueOptions[\"shouldRetry\"];\n /** Makes an object URL for each file to preview (revoked when it leaves). Default `true`. */\n previews?: boolean;\n};\n\n/** What the screen reads of an upload besides its status: the file, how far it got, how fast. */\ntype UploadMeta = { file: File; progress: number; speed?: number; previewUrl?: string; last?: { at: number; sent: number } };\n\n/**\n * The state of an upload queue around the app's `upload` function: `add`, `pause`, `resume`, `retry`, `cancel`, `remove`.\n * The line itself — concurrency, retries with backoff, pause by aborting the signal — is `createQueue` of\n * `@krizaka/pacer`; this hook adds the files, their previews, their progress and their speed.\n */\nexport function useUploadQueue(options: UploadQueueOptions) {\n const { concurrency, retries, backoff, shouldRetry } = options;\n const latest = useLatest(options);\n const queue = useQueue<UploadMeta>({ concurrency: concurrency ?? 2, retries, backoff, shouldRetry });\n const jobs = useQueueSnapshot(queue);\n const metas = useLatest(new Map(jobs.map((job) => [job.id, job.meta])));\n\n const add = React.useCallback(\n (files: readonly File[]) => {\n const { previews = true } = latest.current;\n for (const file of files) {\n const id = `${file.name}-${file.size}-${Math.random().toString(36).slice(2, 8)}`;\n const previewUrl = previews && typeof URL.createObjectURL === \"function\" ? URL.createObjectURL(file) : undefined;\n queue.add(\n async ({ signal, resumed }) => {\n queue.update(id, (m) => ({ ...m, last: { at: Date.now(), sent: m.progress * file.size } }));\n try {\n await latest.current.upload(file, {\n signal,\n resumed,\n onProgress: (sent) => {\n if (signal.aborted) return;\n queue.update(id, (m) => {\n const now = Date.now();\n const last = m.last ?? { at: now, sent };\n const fresh = now - last.at > 400;\n return { ...m, progress: file.size ? Math.min(1, sent / file.size) : 0, speed: fresh ? ((sent - last.sent) / (now - last.at)) * 1000 : m.speed, last: fresh ? { at: now, sent } : last };\n });\n },\n });\n if (!signal.aborted) queue.update(id, (m) => ({ ...m, progress: 1, speed: undefined }));\n } catch (error) {\n if (!signal.aborted) queue.update(id, (m) => ({ ...m, speed: undefined }));\n throw error;\n }\n },\n { id, meta: { file, progress: 0, previewUrl } },\n );\n }\n },\n [latest, queue],\n );\n const forget = React.useCallback(\n (id: string) => {\n const url = metas.current.get(id)?.previewUrl;\n if (url) URL.revokeObjectURL(url);\n queue.cancel(id);\n },\n [metas, queue],\n );\n React.useEffect(\n () => () => {\n for (const meta of metas.current.values()) if (meta.previewUrl) URL.revokeObjectURL(meta.previewUrl);\n },\n [metas],\n );\n const pause = React.useCallback(\n (id: string) => {\n queue.pause(id);\n queue.update(id, (m) => ({ ...m, speed: undefined }));\n },\n [queue],\n );\n\n const items = React.useMemo<UploadItem[]>(\n () =>\n jobs.map(({ id, status, error, meta }) => ({\n id,\n name: meta.file.name,\n size: meta.file.size,\n type: meta.file.type,\n progress: meta.progress,\n status: status === \"running\" ? \"uploading\" : status,\n error: error?.message,\n previewUrl: meta.previewUrl,\n speed: status === \"running\" ? meta.speed : undefined,\n })),\n [jobs],\n );\n return { items, add, pause, resume: queue.resume, retry: queue.retry, cancel: forget, remove: forget };\n}\n","// Server-safe: no hook, no context. role=\"progressbar\" with aria-valuenow / aria-valuemax (none when indeterminate),\n// named by `label`. A bar or a ring; the ring can hold content in its centre (an amount, a percentage).\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const progress = tv({\n slots: {\n root: \"relative\",\n track: \"\",\n indicator: \"\",\n center: \"absolute inset-0 flex flex-col items-center justify-center text-center\",\n buffer: \"absolute inset-y-0 left-0 -z-10 rounded-full bg-accent/25 transition-[width] duration-500 motion-reduce:transition-none\",\n frame: \"flex w-full flex-col gap-1.5\",\n header: \"flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs\",\n caption: \"max-w-full shrink-0 grow truncate font-medium text-fg\",\n figure: \"shrink-0 tabular-nums text-fg-secondary\",\n },\n variants: {\n variant: {\n bar: {\n root: \"relative isolate w-full overflow-hidden rounded-full bg-surface-3\",\n indicator:\n \"kz-progress h-full rounded-full bg-linear-to-r from-accent to-accent-2 transition-[width] duration-500 motion-reduce:transition-none \" +\n \"data-[state=indeterminate]:w-2/5\",\n },\n ring: {\n root: \"inline-flex shrink-0 items-center justify-center\",\n track: \"stroke-border-default\",\n indicator:\n \"stroke-accent transition-[stroke-dashoffset] duration-1000 motion-reduce:transition-none \" +\n \"data-[state=indeterminate]:origin-center data-[state=indeterminate]:animate-spin motion-reduce:data-[state=indeterminate]:animate-none\",\n },\n },\n size: { sm: {}, md: {}, lg: {} },\n status: {\n active: {},\n // Paused: the fill rests, striped — the task is waiting for someone, not broken.\n paused: { indicator: \"kz-progress-paused opacity-70\" },\n // Failed: the fill turns to the danger colour where it stopped.\n error: { indicator: \"from-danger to-danger stroke-danger\" },\n },\n },\n compoundVariants: [\n { variant: \"bar\", size: \"sm\", class: { root: \"h-1\" } },\n { variant: \"bar\", size: \"md\", class: { root: \"h-2\" } },\n { variant: \"bar\", size: \"lg\", class: { root: \"h-3\" } },\n { variant: \"ring\", size: \"sm\", class: { root: \"h-10 w-10\" } },\n { variant: \"ring\", size: \"md\", class: { root: \"h-20 w-20\" } },\n { variant: \"ring\", size: \"lg\", class: { root: \"h-36 w-36\" } },\n ],\n defaultVariants: { variant: \"bar\", size: \"md\", status: \"active\" },\n});\n\nexport type ProgressVariants = VariantProps<typeof progress>;\n\nexport type ProgressProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n ProgressVariants & {\n /** bar · ring (a ring holds its `children` in the centre). */\n variant?: ProgressVariants[\"variant\"];\n /** sm · md · lg: the bar's thickness, the ring's diameter. */\n size?: ProgressVariants[\"size\"];\n /** How far, between 0 and `max`. `null` or absent: indeterminate (a wait of unknown length). */\n value?: number | null;\n /** The value that means complete. */\n max?: number;\n /** The accessible name — passed translated (\"Upload\", \"Raised towards the goal\"). */\n label: string;\n /** The value in words for assistive technology (\"$420 of $1,000\") — default: the percentage. */\n valueText?: string;\n /** A ring's centre: an amount, a percentage, an icon. */\n children?: React.ReactNode;\n /** How far is already available beyond the value (a video buffered ahead), between 0 and `max` — bar only. */\n buffer?: number;\n /** Splits the bar or the ring into this many segments: the steps of a task, the parts of a goal. */\n segments?: number;\n /**\n * active · paused (the fill rests, striped: waiting for someone) · error (the fill turns to the danger colour where it stopped).\n * @default \"active\"\n */\n status?: ProgressVariants[\"status\"];\n /** Shows the label above the bar, with the percentage (or `valueText`) and `eta` — bar only. */\n showLabel?: boolean;\n /** The time left, in words (“About 2 min left”), shown with `showLabel` and read with the value — passed translated. */\n eta?: string;\n };\n\nconst STROKE = { sm: 12, md: 9, lg: 7 } as const;\n\n/** A progress bar or ring: `value` / `max`, indeterminate without a value. State: `data-state` (loading · complete · indeterminate). */\nexport function Progress({\n variant = \"bar\",\n size = \"md\",\n value,\n max = 100,\n label,\n valueText,\n className,\n children,\n buffer,\n segments,\n status = \"active\",\n showLabel,\n eta,\n ...props\n}: ProgressProps) {\n const s = progress({ variant, size, status });\n const maskId = `kz-seg-${React.useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n const known = typeof value === \"number\" && Number.isFinite(value);\n const ratio = known ? Math.min(1, Math.max(0, value / (max || 1))) : 0;\n const percent = Math.round(ratio * 100);\n const state = !known ? \"indeterminate\" : ratio >= 1 ? \"complete\" : \"loading\";\n const aria = {\n role: \"progressbar\" as const,\n \"aria-label\": label,\n \"aria-valuemin\": known ? 0 : undefined,\n \"aria-valuemax\": known ? max : undefined,\n \"aria-valuenow\": known ? Math.min(max, Math.max(0, value)) : undefined,\n \"aria-valuetext\": [known ? (valueText ?? `${percent}%`) : valueText, eta].filter(Boolean).join(\", \") || undefined,\n \"data-state\": state,\n \"data-status\": status === \"active\" ? undefined : status,\n \"data-value\": known ? value : undefined,\n \"data-variant\": variant,\n };\n if (variant === \"ring\") {\n const stroke = STROKE[size];\n const r = 50 - stroke / 2;\n return (\n <div {...aria} className={s.root({ className })} {...props}>\n <svg viewBox=\"0 0 100 100\" className=\"h-full w-full -rotate-90\" aria-hidden>\n {segments && segments > 1 && (\n <mask id={maskId}>\n <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" stroke=\"white\" strokeWidth={stroke + 2} pathLength={100} strokeDasharray={`${100 / segments - 2} 2`} />\n </mask>\n )}\n <g mask={segments && segments > 1 ? `url(#${maskId})` : undefined}>\n <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" strokeWidth={stroke} className={s.track()} />\n <circle\n cx=\"50\"\n cy=\"50\"\n r={r}\n fill=\"none\"\n strokeWidth={stroke}\n strokeLinecap=\"round\"\n pathLength={100}\n strokeDasharray={100}\n strokeDashoffset={known ? 100 - percent : 75}\n data-state={state}\n className={s.indicator()}\n />\n </g>\n </svg>\n {children !== undefined && <div className={s.center()}>{children}</div>}\n </div>\n );\n }\n const segmented = segments && segments > 1 ? ({ \"--kz-segments\": segments } as React.CSSProperties) : undefined;\n const bufferRatio = typeof buffer === \"number\" && Number.isFinite(buffer) ? Math.min(1, Math.max(0, buffer / (max || 1))) : null;\n const bar = (\n <div {...aria} className={s.root({ className: [segmented && \"kz-progress-segmented\", className].filter(Boolean).join(\" \") || undefined })} style={segmented} {...props}>\n <div data-state={state} className={s.indicator()} style={known ? { width: `${ratio * 100}%` } : undefined} />\n {bufferRatio !== null && <div aria-hidden className={s.buffer()} style={{ width: `${bufferRatio * 100}%` }} />}\n </div>\n );\n if (!showLabel) return bar;\n const figure = [known ? (valueText ?? `${percent} %`) : valueText, eta].filter(Boolean).join(\" · \");\n return (\n <div className={s.frame()}>\n <div aria-hidden className={s.header()}>\n <span className={s.caption()}>{label}</span>\n {figure && <span className={s.figure()}>{figure}</span>}\n </div>\n {bar}\n </div>\n );\n}\n"],"mappings":"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","names":["React","tv","jsx","jsxs","tv"]}
@@ -2,12 +2,12 @@
2
2
  import "./chunk-NPF3OHE2.js";
3
3
  import {
4
4
  Avatar
5
- } from "./chunk-YIBKJ36L.js";
5
+ } from "./chunk-KB7OLYIJ.js";
6
6
  import "./chunk-JXLDUDTD.js";
7
7
  import {
8
8
  Button,
9
9
  IconButton
10
- } from "./chunk-276PTQMQ.js";
10
+ } from "./chunk-3BC4XQ6C.js";
11
11
  import "./chunk-D3T54RSL.js";
12
12
 
13
13
  // src/dialog/dialog.tsx
@@ -1,5 +1,31 @@
1
1
  import * as React from 'react';
2
2
 
3
+ interface LoadMoreProps extends Omit<React.ComponentProps<"div">, "children" | "onError"> {
4
+ /** Asks for the next page. Not called while `loading`, nor once `hasMore` is false. */
5
+ onLoadMore: () => void;
6
+ /** There is a next page. */
7
+ hasMore: boolean;
8
+ /** A page is on its way. */
9
+ loading?: boolean;
10
+ /** The last page failed: the button reads `retryLabel`, and nothing loads by itself until it is pressed. */
11
+ failed?: boolean;
12
+ /** The button, passed translated ("Load more"). */
13
+ label: string;
14
+ /** The wait, passed translated ("Loading more videos"). */
15
+ loadingLabel: string;
16
+ /** The button after a failure, passed translated ("Try again"). Defaults to `label`. */
17
+ retryLabel?: string;
18
+ /** Shown instead of the button when the list is over, passed translated ("You have seen everything"). Nothing if absent. */
19
+ endLabel?: string;
20
+ /**
21
+ * How far below the viewport the page is already requested (an IntersectionObserver `rootMargin`).
22
+ * @default "900px 0px"
23
+ */
24
+ rootMargin?: string;
25
+ }
26
+ /** Infinite scroll with a visible "Load more" under it. */
27
+ declare function LoadMore({ onLoadMore, hasMore, loading, failed, label, loadingLabel, retryLabel, endLabel, rootMargin, className, ...props }: LoadMoreProps): React.JSX.Element;
28
+
3
29
  /** The app's link component (Next's `Link`, React Router's `Link`…): receives `href`, `className` and the children. */
4
30
  type LinkComponent = React.ElementType<{
5
31
  href: string;
@@ -81,8 +107,22 @@ interface VideoCardProps extends Omit<React.ComponentProps<"article">, "title">
81
107
  layout?: "stack" | "row";
82
108
  /** The picture. */
83
109
  poster?: string | null;
84
- /** An animated picture (WebP) shown while the card is hovered or focused. */
110
+ /**
111
+ * An animated picture (WebP, a few hundred kB) that fades in over the poster once the pointer has rested on the card
112
+ * (or it is focused) for `previewDelay`, and is removed again on leave. It is never requested before. On a phone (no
113
+ * hover) only the card most inside the viewport plays it, unless the person asked for less data or less motion.
114
+ */
85
115
  previewSrc?: string | null;
116
+ /**
117
+ * How long the pointer must rest before the preview is requested, in ms: a pointer crossing the grid loads nothing.
118
+ * @default 200
119
+ */
120
+ previewDelay?: number;
121
+ /**
122
+ * `fetchPriority` of the poster: `high` for the first row (the largest contentful paint).
123
+ * @default "auto"
124
+ */
125
+ posterPriority?: "high" | "low" | "auto";
86
126
  /**
87
127
  * `cover` fills the frame; `contain` draws the whole picture over a blurred copy of itself (a source of the other
88
128
  * orientation: no cropped face, no black bar).
@@ -109,7 +149,7 @@ interface VideoCardProps extends Omit<React.ComponentProps<"article">, "title">
109
149
  loading?: "lazy" | "eager";
110
150
  }
111
151
  /** A video card whose frame, body and footer have one height per aspect, so a grid row or a shelf never staggers. */
112
- declare function VideoCard({ title, href, linkAs, aspect, layout, poster, previewSrc, fit, duration, badges, creator, meta, action, fallback, blurred, overlay, loading, className, onMouseEnter, onMouseLeave, onFocus, onBlur, ...props }: VideoCardProps): React.JSX.Element;
152
+ declare function VideoCard({ title, href, linkAs, aspect, layout, poster, previewSrc, previewDelay, posterPriority, fit, duration, badges, creator, meta, action, fallback, blurred, overlay, loading, className, onMouseEnter, onMouseLeave, onFocus, onBlur, ref, ...props }: VideoCardProps): React.JSX.Element;
113
153
 
114
154
  interface MediaGridState {
115
155
  /** The frame the card is drawn in (`aspect` of `VideoCard`). */
@@ -143,4 +183,27 @@ interface MediaGridProps<T> extends Omit<React.ComponentProps<"div">, "children"
143
183
  /** A media grid that never mixes heights badly: a shelf for the 9:16, a uniform grid for the rest. */
144
184
  declare function MediaGrid<T>({ items, getId, orientationOf, renderItem, mode, frame, layout, shelf, className, ...props }: MediaGridProps<T>): React.JSX.Element;
145
185
 
146
- export { MediaGrid, type MediaGridProps, type MediaGridState, ShortsShelf, type ShortsShelfProps, VideoCard, type VideoCardAspect, type VideoCardCreator, type VideoCardProps };
186
+ interface VideoCardSkeletonProps extends React.ComponentProps<"div"> {
187
+ /** `landscape` (16:9) or `portrait` (9:16), as the card it stands for. @default "landscape" */
188
+ aspect?: VideoCardAspect;
189
+ /** `stack` or `row`, as the card it stands for. @default "stack" */
190
+ layout?: "stack" | "row";
191
+ }
192
+ /** The placeholder of a `VideoCard` (decorative: announce the wait on the list, with `MediaGridSkeleton`). */
193
+ declare function VideoCardSkeleton({ aspect, layout, className, ...props }: VideoCardSkeletonProps): React.JSX.Element;
194
+ interface MediaGridSkeletonProps extends Omit<React.ComponentProps<"div">, "children"> {
195
+ /** What is loading, passed translated ("Loading videos"): announced once, the cards are hidden from assistive technology. */
196
+ label: string;
197
+ /** Cards in the grid. @default 8 */
198
+ count?: number;
199
+ /** Cards on the Shorts shelf above the grid (`split` mode); 0 for none. @default 0 */
200
+ shelfCount?: number;
201
+ /** `grid` or `list`. @default "grid" */
202
+ layout?: "grid" | "list";
203
+ /** The frame of the grid. @default "landscape" */
204
+ frame?: VideoCardAspect;
205
+ }
206
+ /** The placeholder of a `MediaGrid`: the same shelf and grid, skeleton cards, one polite announcement. */
207
+ declare function MediaGridSkeleton({ label, count, shelfCount, layout, frame, className, ...props }: MediaGridSkeletonProps): React.JSX.Element;
208
+
209
+ export { LoadMore, type LoadMoreProps, MediaGrid, type MediaGridProps, MediaGridSkeleton, type MediaGridSkeletonProps, type MediaGridState, ShortsShelf, type ShortsShelfProps, VideoCard, type VideoCardAspect, type VideoCardCreator, type VideoCardProps, VideoCardSkeleton, type VideoCardSkeletonProps };