@krizaka/ui 3.24.0 → 3.26.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 (236) hide show
  1. package/README.md +9 -1
  2. package/dist/account-menu.d.ts +163 -0
  3. package/dist/account-menu.js +211 -0
  4. package/dist/account-menu.js.map +1 -0
  5. package/dist/app-shell.js +4 -4
  6. package/dist/changelog-page.d.ts +13 -1
  7. package/dist/changelog-page.js +20 -0
  8. package/dist/changelog-page.js.map +1 -1
  9. package/dist/{chunk-CEJDH6SA.js → chunk-2PNLHZLT.js} +23 -3
  10. package/dist/chunk-2PNLHZLT.js.map +1 -0
  11. package/dist/chunk-3S4GHSPZ.js +109 -0
  12. package/dist/chunk-3S4GHSPZ.js.map +1 -0
  13. package/dist/{chunk-FTX66XZY.js → chunk-BCGJZ5PG.js} +3 -3
  14. package/dist/{chunk-FTX66XZY.js.map → chunk-BCGJZ5PG.js.map} +1 -1
  15. package/dist/chunk-C7LTF5CC.js +21 -0
  16. package/dist/chunk-C7LTF5CC.js.map +1 -0
  17. package/dist/{chunk-F7FVPPBB.js → chunk-LYOPPBMC.js} +2 -2
  18. package/dist/{chunk-AFY35UCB.js → chunk-P7GZR5CU.js} +3 -1
  19. package/dist/{chunk-AFY35UCB.js.map → chunk-P7GZR5CU.js.map} +1 -1
  20. package/dist/code-block.js +1 -1
  21. package/dist/cover-hero.d.ts +125 -0
  22. package/dist/cover-hero.js +82 -0
  23. package/dist/cover-hero.js.map +1 -0
  24. package/dist/data-table.js +2 -2
  25. package/dist/docs-nav.js +3 -16
  26. package/dist/docs-nav.js.map +1 -1
  27. package/dist/dropdown-menu.d.ts +12 -1
  28. package/dist/dropdown-menu.js +25 -2
  29. package/dist/dropdown-menu.js.map +1 -1
  30. package/dist/filter-bar.js +1 -1
  31. package/dist/genui.js +2 -2
  32. package/dist/language-switch.d.ts +120 -0
  33. package/dist/language-switch.js +32 -0
  34. package/dist/language-switch.js.map +1 -0
  35. package/dist/media-gallery.d.ts +73 -0
  36. package/dist/media-gallery.js +42 -0
  37. package/dist/media-gallery.js.map +1 -0
  38. package/dist/nav-bar.d.ts +40 -2
  39. package/dist/nav-bar.js +68 -12
  40. package/dist/nav-bar.js.map +1 -1
  41. package/dist/opening-hours.d.ts +124 -0
  42. package/dist/opening-hours.js +48 -0
  43. package/dist/opening-hours.js.map +1 -0
  44. package/dist/rail-nav.js +1 -1
  45. package/dist/service-card.d.ts +168 -0
  46. package/dist/service-card.js +112 -0
  47. package/dist/service-card.js.map +1 -0
  48. package/dist/site-footer.js +1 -1
  49. package/dist/sites.js +2 -2
  50. package/dist/sites.js.map +1 -1
  51. package/dist/slot-picker.d.ts +119 -0
  52. package/dist/slot-picker.js +46 -0
  53. package/dist/slot-picker.js.map +1 -0
  54. package/dist/sticky-action.d.ts +12 -0
  55. package/dist/sticky-action.js +14 -0
  56. package/dist/sticky-action.js.map +1 -0
  57. package/package.json +121 -7
  58. package/registry/account-menu.json +280 -0
  59. package/registry/achievement.json +1 -1
  60. package/registry/action-bar.json +1 -1
  61. package/registry/activity-feed.json +3 -3
  62. package/registry/alert.json +1 -1
  63. package/registry/app-header.json +1 -1
  64. package/registry/app-shell.json +1 -1
  65. package/registry/argument-card.json +1 -1
  66. package/registry/audio-player.json +1 -1
  67. package/registry/auth-layout.json +1 -1
  68. package/registry/avatar.json +1 -1
  69. package/registry/badge.json +1 -1
  70. package/registry/button.json +1 -1
  71. package/registry/captcha.json +1 -1
  72. package/registry/card-grid.json +1 -1
  73. package/registry/card.json +3 -3
  74. package/registry/catalogue-page.json +1 -1
  75. package/registry/changelog-page.json +3 -3
  76. package/registry/chat.json +1 -1
  77. package/registry/checkbox.json +1 -1
  78. package/registry/chip-link.json +1 -1
  79. package/registry/chip.json +1 -1
  80. package/registry/choice-grid.json +1 -1
  81. package/registry/code-block.json +1 -1
  82. package/registry/combobox.json +1 -1
  83. package/registry/command.json +1 -1
  84. package/registry/comparison-table.json +1 -1
  85. package/registry/component-page.json +1 -1
  86. package/registry/confirm-button.json +1 -1
  87. package/registry/contact-page.json +1 -1
  88. package/registry/continue-prompt.json +1 -1
  89. package/registry/countdown.json +1 -1
  90. package/registry/cover-hero.json +246 -0
  91. package/registry/cta-panel.json +1 -1
  92. package/registry/dashboard-layout.json +1 -1
  93. package/registry/data-table.json +1 -1
  94. package/registry/date-picker.json +1 -1
  95. package/registry/decision-list.json +1 -1
  96. package/registry/description-list.json +1 -1
  97. package/registry/detail-layout.json +1 -1
  98. package/registry/dialog.json +1 -1
  99. package/registry/docs-article-page.json +1 -1
  100. package/registry/docs-hub-page.json +1 -1
  101. package/registry/docs-layout.json +1 -1
  102. package/registry/docs-nav.json +1 -1
  103. package/registry/document-viewer.json +1 -1
  104. package/registry/dropdown-menu.json +33 -3
  105. package/registry/empty-state.json +1 -1
  106. package/registry/error-page.json +1 -1
  107. package/registry/examples/account-menu/default.d.ts +4 -0
  108. package/registry/examples/account-menu/default.js +28 -0
  109. package/registry/examples/account-menu/default.tsx +22 -0
  110. package/registry/examples/cover-hero/plain.d.ts +4 -0
  111. package/registry/examples/cover-hero/plain.js +12 -0
  112. package/registry/examples/cover-hero/plain.tsx +7 -0
  113. package/registry/examples/cover-hero/shop.d.ts +4 -0
  114. package/registry/examples/cover-hero/shop.js +27 -0
  115. package/registry/examples/cover-hero/shop.tsx +20 -0
  116. package/registry/examples/language-switch/inline.d.ts +4 -0
  117. package/registry/examples/language-switch/inline.js +11 -0
  118. package/registry/examples/language-switch/inline.tsx +6 -0
  119. package/registry/examples/language-switch/menu.d.ts +4 -0
  120. package/registry/examples/language-switch/menu.js +11 -0
  121. package/registry/examples/language-switch/menu.tsx +6 -0
  122. package/registry/examples/media-gallery/shop.d.ts +4 -0
  123. package/registry/examples/media-gallery/shop.js +22 -0
  124. package/registry/examples/media-gallery/shop.tsx +18 -0
  125. package/registry/examples/nav-bar/family-product.js +18 -10
  126. package/registry/examples/nav-bar/family-product.tsx +28 -27
  127. package/registry/examples/opening-hours/week.d.ts +4 -0
  128. package/registry/examples/opening-hours/week.js +28 -0
  129. package/registry/examples/opening-hours/week.tsx +22 -0
  130. package/registry/examples/service-card/services.d.ts +4 -0
  131. package/registry/examples/service-card/services.js +15 -0
  132. package/registry/examples/service-card/services.tsx +12 -0
  133. package/registry/examples/slot-picker/times.d.ts +4 -0
  134. package/registry/examples/slot-picker/times.js +27 -0
  135. package/registry/examples/slot-picker/times.tsx +23 -0
  136. package/registry/examples/sticky-action/phone.d.ts +4 -0
  137. package/registry/examples/sticky-action/phone.js +15 -0
  138. package/registry/examples/sticky-action/phone.tsx +14 -0
  139. package/registry/field.json +1 -1
  140. package/registry/file-upload.json +1 -1
  141. package/registry/filter-bar.json +1 -1
  142. package/registry/force-update-screen.json +1 -1
  143. package/registry/form.json +3 -3
  144. package/registry/gallery-page.json +1 -1
  145. package/registry/genui.json +1 -1
  146. package/registry/glow-card.json +1 -1
  147. package/registry/goal-gauge.json +1 -1
  148. package/registry/how-it-works-page.json +1 -1
  149. package/registry/image-cropper.json +1 -1
  150. package/registry/index.json +258 -5
  151. package/registry/iso.json +1 -1
  152. package/registry/kanban.json +1 -1
  153. package/registry/kbd.json +1 -1
  154. package/registry/landing-page.json +1 -1
  155. package/registry/landscape.json +1 -1
  156. package/registry/language-switch.json +246 -0
  157. package/registry/legal-page.json +1 -1
  158. package/registry/link-card.json +3 -3
  159. package/registry/list.json +1 -1
  160. package/registry/loader.json +1 -1
  161. package/registry/longform-page.json +1 -1
  162. package/registry/maintenance-screen.json +1 -1
  163. package/registry/map.json +2 -2
  164. package/registry/markdown.json +1 -1
  165. package/registry/marketing-layout.json +1 -1
  166. package/registry/marquee.json +1 -1
  167. package/registry/mascot-quote.json +1 -1
  168. package/registry/mascot-state.json +1 -1
  169. package/registry/mascots.json +1 -1
  170. package/registry/match-dialog.json +1 -1
  171. package/registry/media-capture.json +3 -3
  172. package/registry/media-gallery.json +144 -0
  173. package/registry/media-grid.json +3 -3
  174. package/registry/meta-row.json +1 -1
  175. package/registry/meter.json +1 -1
  176. package/registry/nav-bar.json +138 -8
  177. package/registry/number-ticker.json +1 -1
  178. package/registry/opening-hours.json +178 -0
  179. package/registry/page-header.json +1 -1
  180. package/registry/page-hero.json +3 -3
  181. package/registry/page.json +7 -7
  182. package/registry/pagination.json +1 -1
  183. package/registry/photo-grid.json +1 -1
  184. package/registry/pointer-light.json +1 -1
  185. package/registry/popover.json +1 -1
  186. package/registry/pricing-page.json +1 -1
  187. package/registry/product-page.json +1 -1
  188. package/registry/profile-card.json +1 -1
  189. package/registry/progress.json +1 -1
  190. package/registry/promise-card.json +1 -1
  191. package/registry/radio-group.json +1 -1
  192. package/registry/rail-nav.json +2 -2
  193. package/registry/recipes.json +1 -1
  194. package/registry/scroll-stage.json +1 -1
  195. package/registry/search-field.json +1 -1
  196. package/registry/section-backdrop.json +1 -1
  197. package/registry/separator.json +1 -1
  198. package/registry/service-card.json +358 -0
  199. package/registry/settings-layout.json +1 -1
  200. package/registry/showcase.json +1 -1
  201. package/registry/site-footer.json +2 -2
  202. package/registry/sites.json +2 -2
  203. package/registry/skeleton.json +3 -3
  204. package/registry/skip-link.json +1 -1
  205. package/registry/slider.json +1 -1
  206. package/registry/slot-picker.json +222 -0
  207. package/registry/slot.json +1 -1
  208. package/registry/spinner.json +1 -1
  209. package/registry/split-view.json +1 -1
  210. package/registry/stage-banner.json +1 -1
  211. package/registry/stat.json +1 -1
  212. package/registry/status-page.json +1 -1
  213. package/registry/step-progress.json +1 -1
  214. package/registry/step-showcase.json +1 -1
  215. package/registry/stepper.json +1 -1
  216. package/registry/sticky-action.json +108 -0
  217. package/registry/swipe-deck.json +1 -1
  218. package/registry/switch.json +1 -1
  219. package/registry/tabs.json +1 -1
  220. package/registry/tag-input.json +1 -1
  221. package/registry/tag-selector.json +1 -1
  222. package/registry/terminal-replay.json +1 -1
  223. package/registry/theme.json +1 -1
  224. package/registry/toast.json +1 -1
  225. package/registry/toc.json +1 -1
  226. package/registry/tooltip.json +1 -1
  227. package/registry/tree-view.json +1 -1
  228. package/registry/update-prompt.json +1 -1
  229. package/registry/verified-badge.json +1 -1
  230. package/registry/vertical-video.json +5 -5
  231. package/registry/video-player.json +3 -3
  232. package/registry/video-trimmer.json +1 -1
  233. package/registry/virtual.json +1 -1
  234. package/registry/waveform.json +1 -1
  235. package/dist/chunk-CEJDH6SA.js.map +0 -1
  236. /package/dist/{chunk-F7FVPPBB.js.map → chunk-LYOPPBMC.js.map} +0 -0
package/README.md CHANGED
@@ -147,7 +147,8 @@ light or a product's — is a set of tokens.
147
147
  | `@krizaka/ui/skip-link` | `SkipLink` (`href`, default `#content`; the label as children): the first tab stop, hidden until focused, Enter moves focus to the content (WCAG 2.4.1) | client |
148
148
  | `@krizaka/ui/kbd` | `Kbd` (`size` sm·md), `kbdVariants` | server |
149
149
  | `@krizaka/ui/tabs` | `Tabs.Root/List/Trigger/Content` (`variant` underline·segmented·pills on the root, `orientation`, `value`/`defaultValue` + `onValueChange`; one light glides to the active tab), `tabs` (the slots) | client |
150
- | `@krizaka/ui/nav-bar` | `NavBar.Root` (`label`, `compactAfter`, `floating`) `/Brand` `/Family` (`current`, `labels`, `defaultOpen`: the family switcher on the brand, the mobile panel's last group inside `Mobile`) `/Badge` (`env`, `version`) `/Items/Link` (`active`) `/Menu` (`label`, `columns`) `/MenuLink` (`title`, `description`, `icon`) `/Actions` `/Repo` (`href`, `label`) `/Mobile` (`openLabel`, `closeLabel`) `/MobileLink`, `navBar` (the slots) | client |
150
+ | `@krizaka/ui/language-switch` | `LanguageSwitch` (`label`, `current`, `languages`, `variant`, `linkAs`), `languagesFor(path)`, `languageHref(path, to)` | client |
151
+ | `@krizaka/ui/nav-bar` | `NavBar.Root` (`label`, `compactAfter`, `floating`) `/Brand` `/Family` (`current`, `labels`, `defaultOpen`: the family switcher on the brand, the mobile panel's last group inside `Mobile`) `/Badge` (`env`, `version`) `/Items/Link` (`active`) `/Menu` (`label`, `columns`) `/MenuLink` (`title`, `description`, `icon`) `/Actions` `/Repo` (`href`, `label`) `/Language` (the language switch: a menu in the bar, a row in the mobile panel) `/Mobile` (`openLabel`, `closeLabel`) `/MobilePreferences` (`label`) `/MobileLink`, `navBar` (the slots) | client |
151
152
  | `@krizaka/ui/sites` | The Krizaka family as data: `sites` (id, name, url, host, kind, group, status, mark, repo), `siteGroups`, `siteById`, `sitesByGroup`, `familyLabels` (`en`, `fr`) | server |
152
153
  | `@krizaka/ui/site-footer` | `SiteFooter` (`current`, `labels`, `version`, `env`, `legal`; own columns as children) `.Column` (`title`) `.Link` (`active`, `asChild`): the footer of every Krizaka site — the family in columns, “A Krizaka company / product / open-source project”, the version, Montréal | client |
153
154
  | `@krizaka/ui/chip` | `Chip` (`selected`/`defaultSelected` + `onSelectedChange`, `size` sm·md; `removable` + `removeLabel` + `onRemove`), `Chip.Group` (`type` single·multiple, `label`, `required` for single), `chip` (the slots) | client |
@@ -212,8 +213,15 @@ light or a product's — is a set of tokens.
212
213
  | `@krizaka/ui/app-shell` | `AppShell.Root` (`defaultCollapsed`, `collapsed` + `onCollapsedChange`) `/Sidebar` (`label`, `brand`, `footer`, `collapseLabels`, `closeLabel`; a drawer on a phone) `/Group/Item` (`icon`, `label`, `current`, `badge`, `shortcut`, `asChild`) `/Body/Header` (`title`, `menuLabel`, `actions`) `/Search` (`label`, `onOpen`, `shortcut` ⌘K — opens the app's `CommandDialog`) `/Main`; `collapseShortcut` (⌘B), `useShortcuts` — see [Layouts and templates](#layouts-and-templates) | client |
213
214
  | `@krizaka/ui/rail-nav` | `RailNav.Shell/Root/Item/Body`: `Root` (`label`, `mark`, `accent`, `footer`, `expandable` + `expanded`/`onExpandedChange`, `expandLabels`) is a vertical rail of icons on a desktop — the mark on top, a tooltip per icon, an accent bar on the current item — and a bottom tab bar on a phone; `Item` (`icon`, `label`, `current`, `badge`, `badgeLabel`, `asChild`); arrow keys, Home and End move between items | client |
214
215
  | `@krizaka/ui/app-header` | `AppHeader` (`name`, `mark`, `href` or `homeLink`, `chips`, `actions`): the bar beside a `RailNav` — the name as text, the mark below `md` only (the rail carries it above), so one mark shows at every width | server |
216
+ | `@krizaka/ui/service-card` | `ServiceCard` (`name`, `price`, `priceFrom`, `duration`, `capacity`, `description`, `included`, `tag`, `icon`, `media`, `selected`, `actionLabel`, `href` + `linkAs` or `onSelect`): a service a shop sells as a card with one way in | server |
217
+ | `@krizaka/ui/slot-picker` | `SlotPicker` (`days` of `{ id, label, sub, slots }`, `label`, `emptyLabel`, `value`, `onValueChange`, `linkAs`): the next free times as chips by day — links (`slot.href`) or toggles | client |
218
+ | `@krizaka/ui/opening-hours` | `OpeningHours` (`days`, `today`, `status`, `label`): the weekly hours in one row (open now · closes at…) that unfolds into the week, today marked | server |
219
+ | `@krizaka/ui/cover-hero` | `CoverHero` (`cover` `{ src, alt, video }`, `eyebrow`, `title`, `lead`, `facts`, `actions`, `back`, `linkAs`): a shop's first screen over its photo or muted loop (a still under reduced motion), brand colours without a cover | client |
220
+ | `@krizaka/ui/sticky-action` | `StickyAction`: the page's one action held at the bottom edge of a phone (pure CSS, last child of `main`, not drawn from `md`) | server |
221
+ | `@krizaka/ui/media-gallery` | `MediaGallery` (`items` of `{ id, src, alt, video, caption, shape }`, `label`): a shop's photos and loops in tiles, the first featured, loops play while seen | client |
215
222
  | `@krizaka/ui/dashboard-layout` | `DashboardLayout` (`title`, `eyebrow`, `description`, `actions`, `toolbar`, `headingAs`), `Widget` (`title`, `description`, `action`, `span` 1·2·3·full, `level`) | server |
216
223
  | `@krizaka/ui/settings-layout` | `SettingsLayout` (`title`, `description`, `nav`, `navLabel`, `linkAs`), `SettingsSection` (`title`, `description`, `footer`) | server |
224
+ | `@krizaka/ui/account-menu` | `AccountMenu` (`label`, `name`, `email`, `avatar`, `links`, `language`, `signOut`) | client |
217
225
  | `@krizaka/ui/auth-layout` | `AuthLayout` (`brand`, `title`, `description`, `footer`, `legal`, `promise`, `aside`, `asideFooter`) | server |
218
226
  | `@krizaka/ui/detail-layout` | `DetailLayout` (`back`, `media`, `eyebrow`, `title`, `status`, `meta`, `actions`, `tabs`, `aside`) | server |
219
227
  | `@krizaka/ui/split-view` | `SplitView` (`listTitle`, `listActions`, `list`, `detailHead`, `detail`, `empty`, `showDetail`, `back`) | server |
@@ -0,0 +1,163 @@
1
+ import * as React from 'react';
2
+ import * as tailwind_variants from 'tailwind-variants';
3
+ import { VariantProps } from 'tailwind-variants';
4
+
5
+ type DefaultAvatarBrand = "krizaka" | "orazaka" | "orochia" | "yasakia" | "yororis" | "yomizaka" | "kachizaka" | "akinaza";
6
+ /** `person`: a neutral silhouette. `plain`: the brand's gradient alone, ready for initials. */
7
+ type DefaultAvatarKind = "person" | "plain";
8
+ interface DefaultAvatar {
9
+ /** The brand of the product showing the avatar: its gradient, its hues. */
10
+ brand: DefaultAvatarBrand;
11
+ /** Any stable text or number of the person (their id): the same seed gives the same tone everywhere. */
12
+ seed?: string | number;
13
+ /** Force a tone, 1 to 6, instead of deriving it from `seed`. */
14
+ tone?: number;
15
+ /**
16
+ * `person` (default): a silhouette. `plain`: the gradient alone.
17
+ * @default "person"
18
+ */
19
+ kind?: DefaultAvatarKind;
20
+ }
21
+
22
+ declare const avatar: tailwind_variants.TVReturnType<{
23
+ size: {
24
+ xs: {
25
+ root: string;
26
+ };
27
+ sm: {
28
+ root: string;
29
+ };
30
+ md: {
31
+ root: string;
32
+ };
33
+ lg: {
34
+ root: string;
35
+ };
36
+ xl: {
37
+ root: string;
38
+ };
39
+ };
40
+ }, {
41
+ root: string;
42
+ image: string;
43
+ fallback: string;
44
+ group: string;
45
+ }, undefined, {
46
+ size: {
47
+ xs: {
48
+ root: string;
49
+ };
50
+ sm: {
51
+ root: string;
52
+ };
53
+ md: {
54
+ root: string;
55
+ };
56
+ lg: {
57
+ root: string;
58
+ };
59
+ xl: {
60
+ root: string;
61
+ };
62
+ };
63
+ }, {
64
+ root: string;
65
+ image: string;
66
+ fallback: string;
67
+ group: string;
68
+ }, tailwind_variants.TVReturnTypeLike<{
69
+ size: {
70
+ xs: {
71
+ root: string;
72
+ };
73
+ sm: {
74
+ root: string;
75
+ };
76
+ md: {
77
+ root: string;
78
+ };
79
+ lg: {
80
+ root: string;
81
+ };
82
+ xl: {
83
+ root: string;
84
+ };
85
+ };
86
+ }, {
87
+ root: string;
88
+ image: string;
89
+ fallback: string;
90
+ group: string;
91
+ }>>;
92
+ type AvatarSize = NonNullable<VariantProps<typeof avatar>["size"]>;
93
+ type AvatarProps = Omit<React.ComponentProps<"span">, "children"> & {
94
+ /** The image's URL; `null` or nothing shows the `fallback`. */
95
+ src?: string | null;
96
+ /** The image's text alternative (the person's name); empty when a visible name sits next to the avatar. */
97
+ alt?: string;
98
+ /** Shown while the image loads, when it fails or when there is none: initials, an icon. */
99
+ fallback?: React.ReactNode;
100
+ /** The picture to show instead when `src` is missing or fails to load (an image URL). The `fallback` still covers its own loading. */
101
+ fallbackSrc?: string;
102
+ /**
103
+ * The Krizaka default picture when the person has no photo (or it fails): their brand's gradient and a silhouette,
104
+ * one of six tones from `seed` — `defaultAvatar={{ brand: "orochia", seed: user.id }}`. `fallbackSrc` wins over it.
105
+ */
106
+ defaultAvatar?: DefaultAvatar;
107
+ /**
108
+ * xs · sm · md · lg · xl.
109
+ * @default "md"
110
+ */
111
+ size?: AvatarSize;
112
+ };
113
+
114
+ interface AccountMenuLink {
115
+ id: string;
116
+ label: string;
117
+ href: string;
118
+ /** A signature icon. */
119
+ icon?: React.ReactNode;
120
+ }
121
+ interface AccountMenuLanguage {
122
+ /** The row's words (`Language`). */
123
+ label: string;
124
+ /** The current value (`en`). */
125
+ value: string;
126
+ options: readonly {
127
+ value: string;
128
+ label: string;
129
+ }[];
130
+ onChange: (value: string) => void;
131
+ }
132
+ interface AccountMenuProps {
133
+ /** The trigger's accessible name (`Account`). */
134
+ label: string;
135
+ /** Their name; falls back to the e-mail. */
136
+ name?: string;
137
+ /** Their e-mail, shown under the name. */
138
+ email?: string;
139
+ /** The picture: `src`, or `defaultAvatar` (`{ brand, seed }`) for the product's default person. */
140
+ avatar?: Omit<AvatarProps, "size">;
141
+ /** Show the name beside the avatar in the trigger from the `sm` breakpoint. */
142
+ showName?: boolean;
143
+ /** Where to go: profile, settings… */
144
+ links?: readonly AccountMenuLink[];
145
+ /** The app's link component (Next's `Link`); an `<a>` otherwise. */
146
+ linkAs?: React.ElementType;
147
+ /** The language, as a submenu that shows the current one. */
148
+ language?: AccountMenuLanguage;
149
+ /** Extra rows between the language and Sign out (a theme switch…). */
150
+ children?: React.ReactNode;
151
+ /** The last row: its words, what it does, an optional icon. */
152
+ signOut: {
153
+ label: string;
154
+ onSelect: () => void;
155
+ icon?: React.ReactNode;
156
+ };
157
+ /** Classes for the trigger. */
158
+ className?: string;
159
+ }
160
+ /** The account menu: avatar trigger → identity header, links, language submenu, Sign out. */
161
+ declare function AccountMenu({ label, name, email, avatar, showName, links, linkAs: Link, language, children, signOut, className }: AccountMenuProps): React.JSX.Element;
162
+
163
+ export { AccountMenu, type AccountMenuLanguage, type AccountMenuLink, type AccountMenuProps };
@@ -0,0 +1,211 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-KH3JOEPY.js";
4
+
5
+ // src/avatar/avatar.tsx
6
+ import { Avatar as AvatarPrimitive } from "radix-ui";
7
+ import * as React from "react";
8
+ import { tv } from "tailwind-variants";
9
+
10
+ // src/avatar/default-avatar.ts
11
+ var DEFAULT_AVATAR_TONES = 6;
12
+ var DEFAULT_AVATAR_BASE = "https://krizaka-assets.b-cdn.net/people/defaults/v1";
13
+ var clampTone = (tone) => Math.min(DEFAULT_AVATAR_TONES, Math.max(1, Math.round(tone)));
14
+ function defaultAvatarToneOf(seed) {
15
+ if (seed === void 0) return 1;
16
+ let h = 2166136261;
17
+ for (const ch of String(seed)) {
18
+ h ^= ch.codePointAt(0) ?? 0;
19
+ h = Math.imul(h, 16777619) >>> 0;
20
+ }
21
+ return h % DEFAULT_AVATAR_TONES + 1;
22
+ }
23
+ var resolveTone = (a) => a.tone === void 0 ? defaultAvatarToneOf(a.seed) : clampTone(a.tone);
24
+ function defaultAvatarFile(kind, tone, size) {
25
+ return size === "svg" ? `${kind}-${tone}.svg` : `${kind}-${tone}-${size}.webp`;
26
+ }
27
+ function trimTrailingSlashes(value) {
28
+ let end = value.length;
29
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
30
+ return value.slice(0, end);
31
+ }
32
+ function defaultAvatarUrl(avatar2, { size = 256, base = DEFAULT_AVATAR_BASE } = {}) {
33
+ return `${trimTrailingSlashes(base)}/${avatar2.brand}/${defaultAvatarFile(avatar2.kind ?? "person", resolveTone(avatar2), size)}`;
34
+ }
35
+
36
+ // src/avatar/avatar.tsx
37
+ import { jsx, jsxs } from "react/jsx-runtime";
38
+ var avatar = tv({
39
+ slots: {
40
+ root: "relative inline-flex shrink-0 overflow-hidden rounded-full bg-surface-3 align-middle",
41
+ image: "h-full w-full object-cover",
42
+ fallback: "flex h-full w-full items-center justify-center font-semibold uppercase text-fg-secondary",
43
+ group: "flex items-center -space-x-2 [&>*]:ring-2 [&>*]:ring-surface-0"
44
+ },
45
+ variants: {
46
+ size: {
47
+ xs: { root: "h-5 w-5 text-[9px]" },
48
+ sm: { root: "h-8 w-8 text-xs" },
49
+ md: { root: "h-10 w-10 text-sm" },
50
+ lg: { root: "h-14 w-14 text-base" },
51
+ xl: { root: "h-20 w-20 text-lg" }
52
+ }
53
+ },
54
+ defaultVariants: { size: "md" }
55
+ });
56
+ function AvatarRoot({ src, alt = "", fallback, fallbackSrc, defaultAvatar, size, className, ...props }) {
57
+ const s2 = avatar({ size });
58
+ const [failed, setFailed] = React.useState(null);
59
+ const backup = fallbackSrc ?? (defaultAvatar ? defaultAvatarUrl(defaultAvatar, { size: size === "xl" || size === "lg" ? 512 : 256 }) : void 0);
60
+ const shown = src && failed !== src ? src : backup;
61
+ return /* @__PURE__ */ jsxs(AvatarPrimitive.Root, { "data-size": size ?? "md", className: s2.root({ className }), ...props, children: [
62
+ shown && /* @__PURE__ */ jsx(
63
+ AvatarPrimitive.Image,
64
+ {
65
+ src: shown,
66
+ alt,
67
+ className: s2.image(),
68
+ onLoadingStatusChange: (status) => {
69
+ if (status === "error" && shown === src) setFailed(src);
70
+ }
71
+ }
72
+ ),
73
+ /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { delayMs: shown ? 300 : 0, className: s2.fallback(), children: fallback })
74
+ ] });
75
+ }
76
+ function AvatarGroup({ max, size, className, children, ...props }) {
77
+ const items = React.Children.toArray(children);
78
+ const shown = max === void 0 ? items : items.slice(0, max);
79
+ const rest = items.length - shown.length;
80
+ return /* @__PURE__ */ jsxs("div", { className: avatar().group({ className }), ...props, children: [
81
+ shown,
82
+ rest > 0 && /* @__PURE__ */ jsx(AvatarRoot, { size, fallback: `+${rest}` })
83
+ ] });
84
+ }
85
+ var Avatar = Object.assign(AvatarRoot, { Group: AvatarGroup });
86
+
87
+ // src/dropdown-menu/dropdown-menu.tsx
88
+ import { DropdownMenu as MenuPrimitive } from "radix-ui";
89
+ import { tv as tv2 } from "tailwind-variants";
90
+
91
+ // src/floating.ts
92
+ var floating = "kz-pop z-50 rounded-lg border border-border-default bg-surface-2 text-fg shadow-lg outline-hidden";
93
+
94
+ // src/dropdown-menu/dropdown-menu.tsx
95
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
96
+ var menu = tv2({
97
+ slots: {
98
+ content: cn(floating, "min-w-48 max-h-(--radix-dropdown-menu-content-available-height) overflow-y-auto overflow-x-hidden overscroll-contain p-1 origin-(--radix-dropdown-menu-content-transform-origin)"),
99
+ item: "relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm text-fg outline-hidden transition-colors data-highlighted:bg-surface-3 data-disabled:pointer-events-none data-disabled:opacity-40 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0 [&>svg]:text-fg-secondary",
100
+ indicator: "ml-auto flex h-4 w-4 items-center justify-center text-fg-accent",
101
+ label: "px-2.5 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary",
102
+ separator: "-mx-1 my-1 h-px bg-border-subtle"
103
+ },
104
+ variants: {
105
+ // The label reads in the danger text role (≥ 4.5:1 on the menu surface, both themes); the icon and the highlight
106
+ // carry the danger fill.
107
+ tone: { default: {}, danger: { item: "text-fg-danger data-highlighted:bg-danger/15 [&>svg]:text-danger" } }
108
+ },
109
+ defaultVariants: { tone: "default" }
110
+ });
111
+ var s = menu();
112
+ function DropdownMenuContent({ sideOffset = 6, align = "end", collisionPadding = 8, container, className, ...props }) {
113
+ return /* @__PURE__ */ jsx2(MenuPrimitive.Portal, { container, children: /* @__PURE__ */ jsx2(MenuPrimitive.Content, { sideOffset, align, collisionPadding, className: s.content({ className }), ...props }) });
114
+ }
115
+ function DropdownMenuItem({ tone, className, ...props }) {
116
+ return /* @__PURE__ */ jsx2(MenuPrimitive.Item, { "data-tone": tone ?? "default", className: menu({ tone }).item({ className }), ...props });
117
+ }
118
+ function DropdownMenuCheckboxItem({ className, children, ...props }) {
119
+ return /* @__PURE__ */ jsxs2(MenuPrimitive.CheckboxItem, { className: s.item({ className }), ...props, children: [
120
+ children,
121
+ /* @__PURE__ */ jsx2(MenuPrimitive.ItemIndicator, { className: s.indicator(), children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }) }) })
122
+ ] });
123
+ }
124
+ function DropdownMenuSubTrigger({ className, children, ...props }) {
125
+ return /* @__PURE__ */ jsxs2(MenuPrimitive.SubTrigger, { className: s.item({ className: cn("data-[state=open]:bg-surface-3", className) }), ...props, children: [
126
+ children,
127
+ /* @__PURE__ */ jsx2("svg", { className: "ml-auto !h-4 !w-4 text-fg-secondary", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "m9 18 6-6-6-6" }) })
128
+ ] });
129
+ }
130
+ function DropdownMenuSubContent({ sideOffset = 4, collisionPadding = 8, className, ...props }) {
131
+ return /* @__PURE__ */ jsx2(MenuPrimitive.Portal, { children: /* @__PURE__ */ jsx2(MenuPrimitive.SubContent, { sideOffset, collisionPadding, className: s.content({ className: cn("max-h-(--radix-dropdown-menu-content-available-height)", className) }), ...props }) });
132
+ }
133
+ function DropdownMenuRadioItem({ className, children, ...props }) {
134
+ return /* @__PURE__ */ jsxs2(MenuPrimitive.RadioItem, { className: s.item({ className }), ...props, children: [
135
+ children,
136
+ /* @__PURE__ */ jsx2(MenuPrimitive.ItemIndicator, { className: s.indicator(), children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }) }) })
137
+ ] });
138
+ }
139
+ function DropdownMenuLabel({ className, ...props }) {
140
+ return /* @__PURE__ */ jsx2(MenuPrimitive.Label, { className: s.label({ className }), ...props });
141
+ }
142
+ function DropdownMenuSeparator({ className, ...props }) {
143
+ return /* @__PURE__ */ jsx2(MenuPrimitive.Separator, { className: s.separator({ className }), ...props });
144
+ }
145
+ var DropdownMenu = {
146
+ Root: MenuPrimitive.Root,
147
+ Trigger: MenuPrimitive.Trigger,
148
+ Content: DropdownMenuContent,
149
+ Item: DropdownMenuItem,
150
+ CheckboxItem: DropdownMenuCheckboxItem,
151
+ Label: DropdownMenuLabel,
152
+ Separator: DropdownMenuSeparator,
153
+ Group: MenuPrimitive.Group,
154
+ Sub: MenuPrimitive.Sub,
155
+ SubTrigger: DropdownMenuSubTrigger,
156
+ SubContent: DropdownMenuSubContent,
157
+ RadioGroup: MenuPrimitive.RadioGroup,
158
+ RadioItem: DropdownMenuRadioItem
159
+ };
160
+
161
+ // src/account-menu/account-menu.tsx
162
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
163
+ function AccountMenu({ label, name, email, avatar: avatar2, showName = true, links = [], linkAs: Link = "a", language, children, signOut, className }) {
164
+ const title = name || email || "";
165
+ const current = language?.options.find((o) => o.value === language.value)?.label;
166
+ return /* @__PURE__ */ jsxs3(DropdownMenu.Root, { children: [
167
+ /* @__PURE__ */ jsxs3(
168
+ DropdownMenu.Trigger,
169
+ {
170
+ "aria-label": label,
171
+ className: cn("flex items-center gap-2.5 rounded-lg p-1 transition-colors duration-150 hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring", className),
172
+ children: [
173
+ /* @__PURE__ */ jsx3(Avatar, { size: "sm", fallback: title.charAt(0).toUpperCase() || "?", ...avatar2 }),
174
+ showName && title && /* @__PURE__ */ jsx3("span", { "aria-hidden": true, "data-name": title, className: "hidden max-w-40 truncate text-sm font-medium text-fg-secondary after:content-[attr(data-name)] sm:inline-block" })
175
+ ]
176
+ }
177
+ ),
178
+ /* @__PURE__ */ jsxs3(DropdownMenu.Content, { align: "end", className: "w-64", children: [
179
+ /* @__PURE__ */ jsxs3(DropdownMenu.Label, { className: "flex items-center gap-3 py-2 normal-case tracking-normal", children: [
180
+ /* @__PURE__ */ jsx3(Avatar, { size: "md", fallback: title.charAt(0).toUpperCase() || "?", ...avatar2 }),
181
+ /* @__PURE__ */ jsxs3("span", { className: "min-w-0", children: [
182
+ /* @__PURE__ */ jsx3("span", { className: "block truncate text-sm font-semibold text-fg", children: title }),
183
+ name && email && /* @__PURE__ */ jsx3("span", { className: "block truncate text-xs font-normal text-fg-secondary", children: email })
184
+ ] })
185
+ ] }),
186
+ links.length > 0 && /* @__PURE__ */ jsx3(DropdownMenu.Separator, {}),
187
+ links.map((link) => /* @__PURE__ */ jsx3(DropdownMenu.Item, { asChild: true, children: /* @__PURE__ */ jsxs3(Link, { href: link.href, children: [
188
+ link.icon,
189
+ link.label
190
+ ] }) }, link.id)),
191
+ (language || children) && /* @__PURE__ */ jsx3(DropdownMenu.Separator, {}),
192
+ language && /* @__PURE__ */ jsxs3(DropdownMenu.Sub, { children: [
193
+ /* @__PURE__ */ jsxs3(DropdownMenu.SubTrigger, { children: [
194
+ language.label,
195
+ current && /* @__PURE__ */ jsx3("span", { className: "ml-auto mr-1 text-xs text-fg-secondary", children: current })
196
+ ] }),
197
+ /* @__PURE__ */ jsx3(DropdownMenu.SubContent, { children: /* @__PURE__ */ jsx3(DropdownMenu.RadioGroup, { value: language.value, onValueChange: language.onChange, children: language.options.map((o) => /* @__PURE__ */ jsx3(DropdownMenu.RadioItem, { value: o.value, children: o.label }, o.value)) }) })
198
+ ] }),
199
+ children,
200
+ /* @__PURE__ */ jsx3(DropdownMenu.Separator, {}),
201
+ /* @__PURE__ */ jsxs3(DropdownMenu.Item, { onSelect: () => signOut.onSelect(), children: [
202
+ signOut.icon,
203
+ signOut.label
204
+ ] })
205
+ ] })
206
+ ] });
207
+ }
208
+ export {
209
+ AccountMenu
210
+ };
211
+ //# sourceMappingURL=account-menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/avatar/avatar.tsx","../src/avatar/default-avatar.ts","../src/dropdown-menu/dropdown-menu.tsx","../src/floating.ts","../src/account-menu/account-menu.tsx"],"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","// Client (a portal). Radix does the roving focus, the arrows, Home/End, typeahead, Escape and the focus return.\nimport { DropdownMenu as MenuPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { floating } from \"../floating\";\n\nexport const menu = tv({\n slots: {\n content: cn(floating, \"min-w-48 max-h-(--radix-dropdown-menu-content-available-height) overflow-y-auto overflow-x-hidden overscroll-contain p-1 origin-(--radix-dropdown-menu-content-transform-origin)\"),\n item:\n \"relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm text-fg outline-hidden transition-colors \" +\n \"data-highlighted:bg-surface-3 data-disabled:pointer-events-none data-disabled:opacity-40 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0 \" +\n \"[&>svg]:text-fg-secondary\",\n indicator: \"ml-auto flex h-4 w-4 items-center justify-center text-fg-accent\",\n label: \"px-2.5 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n separator: \"-mx-1 my-1 h-px bg-border-subtle\",\n },\n variants: {\n // The label reads in the danger text role (≥ 4.5:1 on the menu surface, both themes); the icon and the highlight\n // carry the danger fill.\n tone: { default: {}, danger: { item: \"text-fg-danger data-highlighted:bg-danger/15 [&>svg]:text-danger\" } },\n },\n defaultVariants: { tone: \"default\" },\n});\nconst s = menu();\n\nexport type DropdownMenuContentProps = React.ComponentProps<typeof MenuPrimitive.Content> & {\n /** The element the portal renders into (default: document.body). */\n container?: HTMLElement | null;\n};\n\nexport function DropdownMenuContent({ sideOffset = 6, align = \"end\", collisionPadding = 8, container, className, ...props }: DropdownMenuContentProps) {\n return (\n <MenuPrimitive.Portal container={container}>\n <MenuPrimitive.Content sideOffset={sideOffset} align={align} collisionPadding={collisionPadding} className={s.content({ className })} {...props} />\n </MenuPrimitive.Portal>\n );\n}\n\nexport type DropdownMenuItemProps = React.ComponentProps<typeof MenuPrimitive.Item> & {\n /** `danger` for a destructive action: the icon and the highlight carry the danger, the label stays legible. */\n tone?: \"default\" | \"danger\";\n};\n\n/** An action. `tone=\"danger\"` for a destructive one; `onSelect` runs it (the menu closes). */\nexport function DropdownMenuItem({ tone, className, ...props }: DropdownMenuItemProps) {\n return <MenuPrimitive.Item data-tone={tone ?? \"default\"} className={menu({ tone }).item({ className })} {...props} />;\n}\n\n/** A toggled option: `checked` + `onCheckedChange`. */\nexport function DropdownMenuCheckboxItem({ className, children, ...props }: React.ComponentProps<typeof MenuPrimitive.CheckboxItem>) {\n return (\n <MenuPrimitive.CheckboxItem className={s.item({ className })} {...props}>\n {children}\n <MenuPrimitive.ItemIndicator className={s.indicator()}>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n </MenuPrimitive.ItemIndicator>\n </MenuPrimitive.CheckboxItem>\n );\n}\n\n/** A submenu's trigger: an item that opens `DropdownMenu.SubContent`, with a chevron. */\nexport function DropdownMenuSubTrigger({ className, children, ...props }: React.ComponentProps<typeof MenuPrimitive.SubTrigger>) {\n return (\n <MenuPrimitive.SubTrigger className={s.item({ className: cn(\"data-[state=open]:bg-surface-3\", className) })} {...props}>\n {children}\n <svg className=\"ml-auto !h-4 !w-4 text-fg-secondary\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"m9 18 6-6-6-6\" />\n </svg>\n </MenuPrimitive.SubTrigger>\n );\n}\n\n/** The submenu's panel (same material as the menu). */\nexport function DropdownMenuSubContent({ sideOffset = 4, collisionPadding = 8, className, ...props }: React.ComponentProps<typeof MenuPrimitive.SubContent>) {\n return (\n <MenuPrimitive.Portal>\n <MenuPrimitive.SubContent sideOffset={sideOffset} collisionPadding={collisionPadding} className={s.content({ className: cn(\"max-h-(--radix-dropdown-menu-content-available-height)\", className) })} {...props} />\n </MenuPrimitive.Portal>\n );\n}\n\n/** One choice among several (`DropdownMenu.RadioGroup` holds the value): a language, a sort order. */\nexport function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof MenuPrimitive.RadioItem>) {\n return (\n <MenuPrimitive.RadioItem className={s.item({ className })} {...props}>\n {children}\n <MenuPrimitive.ItemIndicator className={s.indicator()}>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n </MenuPrimitive.ItemIndicator>\n </MenuPrimitive.RadioItem>\n );\n}\n\nexport function DropdownMenuLabel({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.Label>) {\n return <MenuPrimitive.Label className={s.label({ className })} {...props} />;\n}\n\nexport function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.Separator>) {\n return <MenuPrimitive.Separator className={s.separator({ className })} {...props} />;\n}\n\n/** `DropdownMenu.Root/Trigger/Content/Item/CheckboxItem/Label/Separator/Group`. `open`/`defaultOpen` + `onOpenChange`. */\nexport const DropdownMenu = {\n Root: MenuPrimitive.Root,\n Trigger: MenuPrimitive.Trigger,\n Content: DropdownMenuContent,\n Item: DropdownMenuItem,\n CheckboxItem: DropdownMenuCheckboxItem,\n Label: DropdownMenuLabel,\n Separator: DropdownMenuSeparator,\n Group: MenuPrimitive.Group,\n Sub: MenuPrimitive.Sub,\n SubTrigger: DropdownMenuSubTrigger,\n SubContent: DropdownMenuSubContent,\n RadioGroup: MenuPrimitive.RadioGroup,\n RadioItem: DropdownMenuRadioItem,\n};\n","// The one surface of everything that floats next to its trigger (popover, menu, tooltip): a raised surface, a border,\n// the large shadow and the `kz-pop` entrance from the side Radix measured (`--radix-*-transform-origin`).\nexport const floating =\n \"kz-pop z-50 rounded-lg border border-border-default bg-surface-2 text-fg shadow-lg outline-hidden\";\n","// Client (a portal). The signed-in person's menu under their avatar — who you are first (picture, name, e-mail), then\n// the places (profile, settings), the language as a compact submenu, the extras, and Sign out (a quiet item, not a\n// red alert). Built on DropdownMenu, so the keyboard, the focus return and the collisions are Radix's.\nimport * as React from \"react\";\n\nimport { Avatar, type AvatarProps } from \"../avatar/avatar\";\nimport { cn } from \"../cn\";\nimport { DropdownMenu } from \"../dropdown-menu/dropdown-menu\";\n\nexport interface AccountMenuLink {\n id: string;\n label: string;\n href: string;\n /** A signature icon. */\n icon?: React.ReactNode;\n}\n\nexport interface AccountMenuLanguage {\n /** The row's words (`Language`). */\n label: string;\n /** The current value (`en`). */\n value: string;\n options: readonly { value: string; label: string }[];\n onChange: (value: string) => void;\n}\n\nexport interface AccountMenuProps {\n /** The trigger's accessible name (`Account`). */\n label: string;\n /** Their name; falls back to the e-mail. */\n name?: string;\n /** Their e-mail, shown under the name. */\n email?: string;\n /** The picture: `src`, or `defaultAvatar` (`{ brand, seed }`) for the product's default person. */\n avatar?: Omit<AvatarProps, \"size\">;\n /** Show the name beside the avatar in the trigger from the `sm` breakpoint. */\n showName?: boolean;\n /** Where to go: profile, settings… */\n links?: readonly AccountMenuLink[];\n /** The app's link component (Next's `Link`); an `<a>` otherwise. */\n linkAs?: React.ElementType;\n /** The language, as a submenu that shows the current one. */\n language?: AccountMenuLanguage;\n /** Extra rows between the language and Sign out (a theme switch…). */\n children?: React.ReactNode;\n /** The last row: its words, what it does, an optional icon. */\n signOut: { label: string; onSelect: () => void; icon?: React.ReactNode };\n /** Classes for the trigger. */\n className?: string;\n}\n\n/** The account menu: avatar trigger → identity header, links, language submenu, Sign out. */\nexport function AccountMenu({ label, name, email, avatar, showName = true, links = [], linkAs: Link = \"a\", language, children, signOut, className }: AccountMenuProps) {\n const title = name || email || \"\";\n const current = language?.options.find((o) => o.value === language.value)?.label;\n return (\n <DropdownMenu.Root>\n <DropdownMenu.Trigger\n aria-label={label}\n className={cn(\"flex items-center gap-2.5 rounded-lg p-1 transition-colors duration-150 hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\", className)}\n >\n <Avatar size=\"sm\" fallback={title.charAt(0).toUpperCase() || \"?\"} {...avatar} />\n {/* Drawn by CSS: the name is not text of the button, so its accessible name stays `label` with no label-content-name-mismatch. */}\n {showName && title && <span aria-hidden data-name={title} className=\"hidden max-w-40 truncate text-sm font-medium text-fg-secondary after:content-[attr(data-name)] sm:inline-block\" />}\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"end\" className=\"w-64\">\n <DropdownMenu.Label className=\"flex items-center gap-3 py-2 normal-case tracking-normal\">\n <Avatar size=\"md\" fallback={title.charAt(0).toUpperCase() || \"?\"} {...avatar} />\n <span className=\"min-w-0\">\n <span className=\"block truncate text-sm font-semibold text-fg\">{title}</span>\n {name && email && <span className=\"block truncate text-xs font-normal text-fg-secondary\">{email}</span>}\n </span>\n </DropdownMenu.Label>\n {links.length > 0 && <DropdownMenu.Separator />}\n {links.map((link) => (\n <DropdownMenu.Item key={link.id} asChild>\n <Link href={link.href}>\n {link.icon}\n {link.label}\n </Link>\n </DropdownMenu.Item>\n ))}\n {(language || children) && <DropdownMenu.Separator />}\n {language && (\n <DropdownMenu.Sub>\n <DropdownMenu.SubTrigger>\n {language.label}\n {current && <span className=\"ml-auto mr-1 text-xs text-fg-secondary\">{current}</span>}\n </DropdownMenu.SubTrigger>\n <DropdownMenu.SubContent>\n <DropdownMenu.RadioGroup value={language.value} onValueChange={language.onChange}>\n {language.options.map((o) => (\n <DropdownMenu.RadioItem key={o.value} value={o.value}>\n {o.label}\n </DropdownMenu.RadioItem>\n ))}\n </DropdownMenu.RadioGroup>\n </DropdownMenu.SubContent>\n </DropdownMenu.Sub>\n )}\n {children}\n <DropdownMenu.Separator />\n <DropdownMenu.Item onSelect={() => signOut.onSelect()}>\n {signOut.icon}\n {signOut.label}\n </DropdownMenu.Item>\n </DropdownMenu.Content>\n </DropdownMenu.Root>\n );\n}\n"],"mappings":"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","names":["avatar","s","tv","jsx","jsxs","tv","jsx","jsxs","avatar"]}
package/dist/app-shell.js CHANGED
@@ -1,15 +1,15 @@
1
1
  "use client";
2
+ import {
3
+ SkipLink
4
+ } from "./chunk-QDXV544C.js";
2
5
  import {
3
6
  ChevronGlyph,
4
7
  CloseGlyph,
5
8
  SearchGlyph
6
- } from "./chunk-AFY35UCB.js";
9
+ } from "./chunk-P7GZR5CU.js";
7
10
  import {
8
11
  cn
9
12
  } from "./chunk-WU4VLSEA.js";
10
- import {
11
- SkipLink
12
- } from "./chunk-QDXV544C.js";
13
13
 
14
14
  // src/app-shell/app-shell.tsx
15
15
  import { Dialog as DialogPrimitive, Slot } from "radix-ui";
@@ -100,5 +100,17 @@ declare function changelogRss(opts: {
100
100
  }): string;
101
101
  /** The template's kind for a tag: feature → added, fix → fixed, security and breaking stay themselves, the rest → changed. */
102
102
  declare const kindOfTag: (tag: ChangeTag) => ChangeKind;
103
+ interface MergedPull {
104
+ title: string;
105
+ html_url: string;
106
+ /** ISO timestamp, `null` for a pull request closed without merging. */
107
+ merged_at: string | null;
108
+ }
109
+ /**
110
+ * For a repository that publishes no releases (a product still in development): its merged pull requests, by week.
111
+ * Each week is one entry (`version` = the Monday, `date` = the last merge), its changes the conventional titles in plain
112
+ * language — housekeeping (chore, ci, test, docs…) is left out, weeks with nothing to say are dropped.
113
+ */
114
+ declare function releasesFromPulls(pulls: readonly MergedPull[], source: string, url?: string): Release[];
103
115
 
104
- export { CHANGE_TAGS, type ChangeItem, type ChangeKind, type ChangeTag, type ChangelogEntry, ChangelogPage, type ChangelogPageProps, type Release, anchorOf, changeFromTitle, changelogRss, draftFromTitles, kindOfTag, newestFirst, parseChangesets, parseReleaseBody, plain, prOf, tagOf };
116
+ export { CHANGE_TAGS, type ChangeItem, type ChangeKind, type ChangeTag, type ChangelogEntry, ChangelogPage, type ChangelogPageProps, type MergedPull, type Release, anchorOf, changeFromTitle, changelogRss, draftFromTitles, kindOfTag, newestFirst, parseChangesets, parseReleaseBody, plain, prOf, releasesFromPulls, tagOf };
@@ -101,6 +101,25 @@ function changelogRss(opts) {
101
101
  return `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>${esc(opts.title)}</title><link>${esc(opts.pageUrl)}</link><description>${esc(opts.description)}</description><language>${opts.language}</language><atom:link href="${esc(opts.feedUrl)}" rel="self" type="application/rss+xml"/>${items}</channel></rss>`;
102
102
  }
103
103
  var kindOfTag = (tag) => tag === "feature" ? "added" : tag === "fix" ? "fixed" : tag === "change" ? "changed" : tag;
104
+ var mondayOf = (iso) => {
105
+ const d = /* @__PURE__ */ new Date(`${iso.slice(0, 10)}T12:00:00Z`);
106
+ d.setUTCDate(d.getUTCDate() - (d.getUTCDay() + 6) % 7);
107
+ return d.toISOString().slice(0, 10);
108
+ };
109
+ function releasesFromPulls(pulls, source, url) {
110
+ const weeks = /* @__PURE__ */ new Map();
111
+ for (const p of pulls) {
112
+ if (!p.merged_at) continue;
113
+ const item = changeFromTitle(p.title, p.html_url);
114
+ if (!item) continue;
115
+ const key = mondayOf(p.merged_at);
116
+ const week = weeks.get(key) ?? { date: p.merged_at.slice(0, 10), changes: [] };
117
+ if (p.merged_at.slice(0, 10) > week.date) week.date = p.merged_at.slice(0, 10);
118
+ week.changes.push(item);
119
+ weeks.set(key, week);
120
+ }
121
+ return [...weeks].map(([version, w]) => ({ source, version, date: w.date, url, changes: w.changes })).sort(newestFirst);
122
+ }
104
123
 
105
124
  // src/changelog-page/changelog-page.tsx
106
125
  import { Page } from "@krizaka/ui/page";
@@ -138,6 +157,7 @@ export {
138
157
  parseReleaseBody,
139
158
  plain,
140
159
  prOf,
160
+ releasesFromPulls,
141
161
  tagOf
142
162
  };
143
163
  //# sourceMappingURL=changelog-page.js.map