@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.
- package/dist/avatar/defaults.js +6 -1
- package/dist/avatar/defaults.js.map +1 -1
- package/dist/avatar.js +6 -1
- package/dist/avatar.js.map +1 -1
- package/dist/blocks.css +11 -0
- package/dist/card-grid.d.ts +14 -1
- package/dist/card-grid.js +29 -6
- package/dist/card-grid.js.map +1 -1
- package/dist/chat.d.ts +51 -3
- package/dist/chat.js +64 -32
- package/dist/chat.js.map +1 -1
- package/dist/{chunk-276PTQMQ.js → chunk-3BC4XQ6C.js} +2 -1
- package/dist/{chunk-YIBKJ36L.js → chunk-KB7OLYIJ.js} +7 -2
- package/dist/{chunk-YIBKJ36L.js.map → chunk-KB7OLYIJ.js.map} +1 -1
- package/dist/data-table.js +1 -1
- package/dist/file-upload.js +2 -2
- package/dist/file-upload.js.map +1 -1
- package/dist/match-dialog.js +2 -2
- package/dist/media-grid.d.ts +66 -3
- package/dist/media-grid.js +312 -65
- package/dist/media-grid.js.map +1 -1
- package/dist/native.js +6 -1
- package/dist/native.js.map +1 -1
- package/dist/progress.js +2 -2
- package/dist/progress.js.map +1 -1
- package/dist/vertical-video.d.ts +7 -1
- package/dist/vertical-video.js +7 -5
- package/dist/vertical-video.js.map +1 -1
- package/package.json +8 -8
- package/registry/achievement.json +16 -2
- package/registry/action-bar.json +16 -2
- package/registry/action-sheet.json +15 -0
- package/registry/activity-feed.json +13 -2
- package/registry/alert.json +15 -2
- package/registry/app-shell.json +15 -2
- package/registry/argument-card.json +16 -2
- package/registry/audio-player.json +15 -2
- package/registry/auth-layout.json +15 -2
- package/registry/avatar.json +14 -3
- package/registry/badge.json +13 -2
- package/registry/button.json +13 -2
- package/registry/card-grid.json +69 -4
- package/registry/card.json +13 -2
- package/registry/catalogue-page.json +15 -2
- package/registry/changelog-page.json +15 -2
- package/registry/chat.json +115 -10
- package/registry/checkbox.json +13 -2
- package/registry/chip-link.json +15 -2
- package/registry/chip.json +13 -2
- package/registry/choice-grid.json +16 -2
- package/registry/cn.json +12 -0
- package/registry/code-block.json +15 -2
- package/registry/combobox.json +13 -2
- package/registry/command.json +15 -2
- package/registry/component-page.json +15 -2
- package/registry/confirm-button.json +15 -2
- package/registry/contact-page.json +15 -2
- package/registry/continue-prompt.json +16 -2
- package/registry/countdown.json +13 -2
- package/registry/cta-panel.json +16 -2
- package/registry/dashboard-layout.json +15 -2
- package/registry/data-table.json +15 -2
- package/registry/date-picker.json +13 -2
- package/registry/decision-list.json +16 -2
- package/registry/description-list.json +13 -2
- package/registry/detail-layout.json +15 -2
- package/registry/dialog.json +13 -2
- package/registry/docs-article-page.json +15 -2
- package/registry/docs-hub-page.json +15 -2
- package/registry/docs-layout.json +15 -2
- package/registry/document-viewer.json +15 -2
- package/registry/dropdown-menu.json +15 -2
- package/registry/empty-state.json +13 -2
- package/registry/error-page.json +14 -2
- package/registry/examples/card-grid/foundation.d.ts +4 -0
- package/registry/examples/card-grid/foundation.js +38 -0
- package/registry/examples/card-grid/foundation.tsx +35 -0
- package/registry/examples/chat/conversation.js +2 -2
- package/registry/examples/chat/conversation.tsx +4 -3
- package/registry/examples/chat/pill-composer.d.ts +4 -0
- package/registry/examples/chat/pill-composer.js +34 -0
- package/registry/examples/chat/pill-composer.tsx +39 -0
- package/registry/examples/glow-card/plans.js +9 -12
- package/registry/examples/glow-card/plans.tsx +18 -14
- package/registry/examples/media-grid/loading.d.ts +4 -0
- package/registry/examples/media-grid/loading.js +28 -0
- package/registry/examples/media-grid/loading.tsx +25 -0
- package/registry/examples/vertical-video/rail.js +3 -2
- package/registry/examples/vertical-video/rail.tsx +4 -3
- package/registry/fab.json +15 -0
- package/registry/field.json +13 -2
- package/registry/file-upload.json +15 -2
- package/registry/filter-bar.json +15 -2
- package/registry/form.json +13 -2
- package/registry/gallery-page.json +15 -2
- package/registry/glow-card.json +18 -4
- package/registry/goal-gauge.json +13 -2
- package/registry/header.json +15 -0
- package/registry/how-it-works-page.json +15 -2
- package/registry/image-cropper.json +15 -2
- package/registry/image-viewer.json +15 -0
- package/registry/index.json +1773 -10
- package/registry/iso.json +15 -2
- package/registry/kanban.json +15 -2
- package/registry/kbd.json +14 -2
- package/registry/landing-page.json +15 -2
- package/registry/landscape.json +15 -2
- package/registry/legal-page.json +15 -2
- package/registry/link-card.json +16 -2
- package/registry/list.json +13 -2
- package/registry/loader.json +13 -2
- package/registry/longform-page.json +15 -2
- package/registry/map.json +14 -3
- package/registry/markdown.json +13 -2
- package/registry/marketing-layout.json +15 -2
- package/registry/marquee.json +15 -2
- package/registry/mascot-quote.json +16 -2
- package/registry/mascot-state.json +13 -2
- package/registry/mascots.json +13 -2
- package/registry/match-dialog.json +13 -2
- package/registry/media-capture.json +13 -2
- package/registry/media-grid.json +341 -13
- package/registry/media-picker.json +15 -0
- package/registry/meter.json +13 -2
- package/registry/nav-bar.json +17 -3
- package/registry/number-ticker.json +13 -2
- package/registry/onboarding.json +15 -0
- package/registry/otp-input.json +15 -0
- package/registry/page-header.json +15 -2
- package/registry/page-hero.json +16 -2
- package/registry/page.json +16 -2
- package/registry/pagination.json +13 -2
- package/registry/permission-prompt.json +15 -0
- package/registry/photo-grid.json +13 -2
- package/registry/pointer-light.json +15 -2
- package/registry/popover.json +15 -2
- package/registry/pricing-page.json +15 -2
- package/registry/product-page.json +15 -2
- package/registry/profile-card.json +13 -2
- package/registry/progress.json +14 -3
- package/registry/promise-card.json +16 -2
- package/registry/pull-to-refresh.json +15 -0
- package/registry/radio-group.json +13 -2
- package/registry/recipes.json +14 -2
- package/registry/screen.json +15 -0
- package/registry/scroll-stage.json +15 -2
- package/registry/search-field.json +13 -2
- package/registry/section-backdrop.json +15 -2
- package/registry/segmented.json +15 -0
- package/registry/separator.json +15 -2
- package/registry/settings-layout.json +15 -2
- package/registry/showcase.json +13 -2
- package/registry/site-footer.json +16 -3
- package/registry/sites.json +15 -2
- package/registry/skeleton.json +13 -2
- package/registry/skip-link.json +14 -2
- package/registry/slider.json +13 -2
- package/registry/slot.json +14 -2
- package/registry/spinner.json +13 -2
- package/registry/splash-handoff.json +15 -0
- package/registry/split-view.json +15 -2
- package/registry/stage-banner.json +15 -2
- package/registry/stat.json +15 -2
- package/registry/status-page.json +15 -2
- package/registry/step-progress.json +13 -2
- package/registry/stepper.json +13 -2
- package/registry/swipe-deck.json +13 -2
- package/registry/swipeable-row.json +15 -0
- package/registry/switch.json +13 -2
- package/registry/tab-bar.json +15 -0
- package/registry/tabs.json +13 -2
- package/registry/tag-input.json +15 -2
- package/registry/tag-selector.json +13 -2
- package/registry/terminal-replay.json +15 -2
- package/registry/theme.json +13 -2
- package/registry/toast.json +13 -2
- package/registry/toc.json +15 -2
- package/registry/tooltip.json +14 -2
- package/registry/tree-view.json +15 -2
- package/registry/txt.json +13 -0
- package/registry/verified-badge.json +13 -2
- package/registry/vertical-video.json +32 -6
- package/registry/video-player.json +13 -2
- package/registry/video-trimmer.json +15 -2
- package/registry/virtual.json +15 -2
- /package/dist/{chunk-276PTQMQ.js.map → chunk-3BC4XQ6C.js.map} +0 -0
package/dist/avatar/defaults.js
CHANGED
|
@@ -53,8 +53,13 @@ var resolveTone = (a) => a.tone === void 0 ? defaultAvatarToneOf(a.seed) : clamp
|
|
|
53
53
|
function defaultAvatarFile(kind, tone, size) {
|
|
54
54
|
return size === "svg" ? `${kind}-${tone}.svg` : `${kind}-${tone}-${size}.webp`;
|
|
55
55
|
}
|
|
56
|
+
function trimTrailingSlashes(value) {
|
|
57
|
+
let end = value.length;
|
|
58
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
|
|
59
|
+
return value.slice(0, end);
|
|
60
|
+
}
|
|
56
61
|
function defaultAvatarUrl(avatar, { size = 256, base = DEFAULT_AVATAR_BASE } = {}) {
|
|
57
|
-
return `${base
|
|
62
|
+
return `${trimTrailingSlashes(base)}/${avatar.brand}/${defaultAvatarFile(avatar.kind ?? "person", resolveTone(avatar), size)}`;
|
|
58
63
|
}
|
|
59
64
|
function defaultAvatarSvg(avatar) {
|
|
60
65
|
const [from, to] = defaultAvatarTone(avatar.brand, resolveTone(avatar));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/avatar/default-avatar.ts"],"sourcesContent":["// 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":";AASO,IAAM,wBAAuD,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,YAAY,aAAa,SAAS;AACvJ,IAAM,uBAAqD,CAAC,UAAU,OAAO;AAC7E,IAAM,uBAAqD,CAAC,KAAK,GAAG;AAEpE,IAAM,uBAAuB;AAG7B,IAAM,sBAAsB;AAiBnC,IAAM,SAAuH;AAAA,EAC3H,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC9D,SAAS,EAAE,MAAM,CAAC,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,EAAE;AAAA,EAC/D,SAAS,EAAE,MAAM,CAAC,OAAO,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,MAAM,IAAI,GAAG,QAAQ,GAAG;AAAA,EAC1E,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC9D,SAAS,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC7D,UAAU,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC/D,WAAW,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC/D,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,GAAG;AAC/D;AAGA,IAAM,eAAe;AACrB,IAAM,UAAU,CAAC,GAAG,IAAI,GAAG,IAAI,GAAG,CAAC;AAEnC,SAAS,SAAS,CAAC,GAAG,GAAG,CAAC,GAAqC;AAC7D,QAAM,MAAM,IAAI;AAChB,QAAM,QAAQ,IAAI;AAClB,QAAM,IAAI,CAAC,OAAe,KAAO,IAAI,MAAO,OAAO,MAAO,MAAM;AAChE,QAAM,IAAI,MAAM,KAAK,IAAI,OAAO,IAAI,KAAK;AACzC,QAAM,UAAU,CAAC,MAAc,KAAK,MAAM,OAAO,QAAQ,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC,IAAI,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;AACrH,SAAO,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,QAAQ,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AACpF;AAEA,IAAM,mBAAmB,CAAC,MAAe,KAAK,UAAU,IAAI,UAAU,IAAI,SAAS,UAAU;AAC7F,IAAM,gBAAgB,CAAC,QAAgB;AACrC,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,iBAAiB,OAAO,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC;AACvG,SAAO,QAAQ,SAAS,IAAK,SAAS,IAAK,SAAS,IAAK;AAC3D;AAEA,SAAS,SAAS,CAAC,GAAG,GAAG,CAAC,GAAqC;AAC7D,MAAI,MAAM,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC;AAC5B,WAAS,QAAQ,GAAG,cAAc,GAAG,IAAI,gBAAgB,QAAQ,IAAI,SAAS,EAAG,OAAM,SAAS,CAAC,GAAG,GAAG,KAAK,CAAC;AAC7G,SAAO;AACT;AAGO,SAAS,kBAAkB,OAA2B,MAAyC;AACpG,QAAM,IAAI,OAAO,KAAK;AACtB,QAAM,IAAI,SAAS,UAAU,IAAI,IAAI,KAAK,QAAQ,MAAM,IAAI,EAAE;AAC9D,SAAO,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;AACpG;AAEA,IAAM,YAAY,CAAC,SAAiB,KAAK,IAAI,sBAAsB,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAGzF,SAAS,oBAAoB,MAA2C;AAC7E,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI,IAAI;AACR,aAAW,MAAM,OAAO,IAAI,GAAG;AAC7B,SAAK,GAAG,YAAY,CAAC,KAAK;AAC1B,QAAI,KAAK,KAAK,GAAG,QAAU,MAAM;AAAA,EACnC;AACA,SAAQ,IAAI,uBAAwB;AACtC;AAEA,IAAM,cAAc,CAAC,MAAsB,EAAE,SAAS,SAAY,oBAAoB,EAAE,IAAI,IAAI,UAAU,EAAE,IAAI;AAGzG,SAAS,kBAAkB,MAAyB,MAAc,MAAyC;AAChH,SAAO,SAAS,QAAQ,GAAG,IAAI,IAAI,IAAI,SAAS,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI;AACzE;AAOO,SAAS,iBAAiB,QAAuB,EAAE,OAAO,KAAK,OAAO,oBAAoB,IAAyD,CAAC,GAAW;AACpK,SAAO,GAAG,KAAK,QAAQ,QAAQ,EAAE,CAAC,IAAI,OAAO,KAAK,IAAI,kBAAkB,OAAO,QAAQ,UAAU,YAAY,MAAM,GAAG,IAAI,CAAC;AAC7H;AAGO,SAAS,iBAAiB,QAA+B;AAC9D,QAAM,CAAC,MAAM,EAAE,IAAI,kBAAkB,OAAO,OAAO,YAAY,MAAM,CAAC;AACtE,QAAM,UACH,OAAO,QAAQ,cAAc,WAC1B,6JACA;AACN,SAAO,gLAAgL,IAAI,mCAAmC,EAAE,0SAA0S,MAAM;AAClhB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/avatar/default-avatar.ts"],"sourcesContent":["// 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":";AASO,IAAM,wBAAuD,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,YAAY,aAAa,SAAS;AACvJ,IAAM,uBAAqD,CAAC,UAAU,OAAO;AAC7E,IAAM,uBAAqD,CAAC,KAAK,GAAG;AAEpE,IAAM,uBAAuB;AAG7B,IAAM,sBAAsB;AAiBnC,IAAM,SAAuH;AAAA,EAC3H,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC9D,SAAS,EAAE,MAAM,CAAC,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,EAAE;AAAA,EAC/D,SAAS,EAAE,MAAM,CAAC,OAAO,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,MAAM,IAAI,GAAG,QAAQ,GAAG;AAAA,EAC1E,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC9D,SAAS,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC7D,UAAU,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC/D,WAAW,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC/D,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,GAAG;AAC/D;AAGA,IAAM,eAAe;AACrB,IAAM,UAAU,CAAC,GAAG,IAAI,GAAG,IAAI,GAAG,CAAC;AAEnC,SAAS,SAAS,CAAC,GAAG,GAAG,CAAC,GAAqC;AAC7D,QAAM,MAAM,IAAI;AAChB,QAAM,QAAQ,IAAI;AAClB,QAAM,IAAI,CAAC,OAAe,KAAO,IAAI,MAAO,OAAO,MAAO,MAAM;AAChE,QAAM,IAAI,MAAM,KAAK,IAAI,OAAO,IAAI,KAAK;AACzC,QAAM,UAAU,CAAC,MAAc,KAAK,MAAM,OAAO,QAAQ,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC,IAAI,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;AACrH,SAAO,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,QAAQ,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AACpF;AAEA,IAAM,mBAAmB,CAAC,MAAe,KAAK,UAAU,IAAI,UAAU,IAAI,SAAS,UAAU;AAC7F,IAAM,gBAAgB,CAAC,QAAgB;AACrC,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,iBAAiB,OAAO,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC;AACvG,SAAO,QAAQ,SAAS,IAAK,SAAS,IAAK,SAAS,IAAK;AAC3D;AAEA,SAAS,SAAS,CAAC,GAAG,GAAG,CAAC,GAAqC;AAC7D,MAAI,MAAM,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC;AAC5B,WAAS,QAAQ,GAAG,cAAc,GAAG,IAAI,gBAAgB,QAAQ,IAAI,SAAS,EAAG,OAAM,SAAS,CAAC,GAAG,GAAG,KAAK,CAAC;AAC7G,SAAO;AACT;AAGO,SAAS,kBAAkB,OAA2B,MAAyC;AACpG,QAAM,IAAI,OAAO,KAAK;AACtB,QAAM,IAAI,SAAS,UAAU,IAAI,IAAI,KAAK,QAAQ,MAAM,IAAI,EAAE;AAC9D,SAAO,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;AACpG;AAEA,IAAM,YAAY,CAAC,SAAiB,KAAK,IAAI,sBAAsB,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAGzF,SAAS,oBAAoB,MAA2C;AAC7E,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI,IAAI;AACR,aAAW,MAAM,OAAO,IAAI,GAAG;AAC7B,SAAK,GAAG,YAAY,CAAC,KAAK;AAC1B,QAAI,KAAK,KAAK,GAAG,QAAU,MAAM;AAAA,EACnC;AACA,SAAQ,IAAI,uBAAwB;AACtC;AAEA,IAAM,cAAc,CAAC,MAAsB,EAAE,SAAS,SAAY,oBAAoB,EAAE,IAAI,IAAI,UAAU,EAAE,IAAI;AAGzG,SAAS,kBAAkB,MAAyB,MAAc,MAAyC;AAChH,SAAO,SAAS,QAAQ,GAAG,IAAI,IAAI,IAAI,SAAS,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI;AACzE;AAGA,SAAS,oBAAoB,OAAuB;AAClD,MAAI,MAAM,MAAM;AAChB,SAAO,MAAM,KAAK,MAAM,WAAW,MAAM,CAAC,MAAM,GAAI;AACpD,SAAO,MAAM,MAAM,GAAG,GAAG;AAC3B;AAOO,SAAS,iBAAiB,QAAuB,EAAE,OAAO,KAAK,OAAO,oBAAoB,IAAyD,CAAC,GAAW;AACpK,SAAO,GAAG,oBAAoB,IAAI,CAAC,IAAI,OAAO,KAAK,IAAI,kBAAkB,OAAO,QAAQ,UAAU,YAAY,MAAM,GAAG,IAAI,CAAC;AAC9H;AAGO,SAAS,iBAAiB,QAA+B;AAC9D,QAAM,CAAC,MAAM,EAAE,IAAI,kBAAkB,OAAO,OAAO,YAAY,MAAM,CAAC;AACtE,QAAM,UACH,OAAO,QAAQ,cAAc,WAC1B,6JACA;AACN,SAAO,gLAAgL,IAAI,mCAAmC,EAAE,0SAA0S,MAAM;AAClhB;","names":[]}
|
package/dist/avatar.js
CHANGED
|
@@ -60,8 +60,13 @@ var resolveTone = (a) => a.tone === void 0 ? defaultAvatarToneOf(a.seed) : clamp
|
|
|
60
60
|
function defaultAvatarFile(kind, tone, size) {
|
|
61
61
|
return size === "svg" ? `${kind}-${tone}.svg` : `${kind}-${tone}-${size}.webp`;
|
|
62
62
|
}
|
|
63
|
+
function trimTrailingSlashes(value) {
|
|
64
|
+
let end = value.length;
|
|
65
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
|
|
66
|
+
return value.slice(0, end);
|
|
67
|
+
}
|
|
63
68
|
function defaultAvatarUrl(avatar2, { size = 256, base = DEFAULT_AVATAR_BASE } = {}) {
|
|
64
|
-
return `${base
|
|
69
|
+
return `${trimTrailingSlashes(base)}/${avatar2.brand}/${defaultAvatarFile(avatar2.kind ?? "person", resolveTone(avatar2), size)}`;
|
|
65
70
|
}
|
|
66
71
|
function defaultAvatarSvg(avatar2) {
|
|
67
72
|
const [from, to] = defaultAvatarTone(avatar2.brand, resolveTone(avatar2));
|
package/dist/avatar.js.map
CHANGED
|
@@ -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":";;;AACA,SAAS,UAAU,uBAAuB;AAC1C,YAAY,WAAW;AACvB,SAAS,UAA6B;;;ACM/B,IAAM,wBAAuD,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,YAAY,aAAa,SAAS;AACvJ,IAAM,uBAAqD,CAAC,UAAU,OAAO;AAC7E,IAAM,uBAAqD,CAAC,KAAK,GAAG;AAEpE,IAAM,uBAAuB;AAG7B,IAAM,sBAAsB;AAiBnC,IAAM,SAAuH;AAAA,EAC3H,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC9D,SAAS,EAAE,MAAM,CAAC,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,EAAE;AAAA,EAC/D,SAAS,EAAE,MAAM,CAAC,OAAO,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,MAAM,IAAI,GAAG,QAAQ,GAAG;AAAA,EAC1E,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC9D,SAAS,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC7D,UAAU,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC/D,WAAW,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,GAAG;AAAA,EAC/D,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG,QAAQ,GAAG;AAC/D;AAGA,IAAM,eAAe;AACrB,IAAM,UAAU,CAAC,GAAG,IAAI,GAAG,IAAI,GAAG,CAAC;AAEnC,SAAS,SAAS,CAAC,GAAG,GAAG,CAAC,GAAqC;AAC7D,QAAM,MAAM,IAAI;AAChB,QAAM,QAAQ,IAAI;AAClB,QAAM,IAAI,CAAC,OAAe,KAAO,IAAI,MAAO,OAAO,MAAO,MAAM;AAChE,QAAM,IAAI,MAAM,KAAK,IAAI,OAAO,IAAI,KAAK;AACzC,QAAM,UAAU,CAAC,MAAc,KAAK,MAAM,OAAO,QAAQ,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC,IAAI,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;AACrH,SAAO,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,QAAQ,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AACpF;AAEA,IAAM,mBAAmB,CAAC,MAAe,KAAK,UAAU,IAAI,UAAU,IAAI,SAAS,UAAU;AAC7F,IAAM,gBAAgB,CAAC,QAAgB;AACrC,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,iBAAiB,OAAO,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC;AACvG,SAAO,QAAQ,SAAS,IAAK,SAAS,IAAK,SAAS,IAAK;AAC3D;AAEA,SAAS,SAAS,CAAC,GAAG,GAAG,CAAC,GAAqC;AAC7D,MAAI,MAAM,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC;AAC5B,WAAS,QAAQ,GAAG,cAAc,GAAG,IAAI,gBAAgB,QAAQ,IAAI,SAAS,EAAG,OAAM,SAAS,CAAC,GAAG,GAAG,KAAK,CAAC;AAC7G,SAAO;AACT;AAGO,SAAS,kBAAkB,OAA2B,MAAyC;AACpG,QAAM,IAAI,OAAO,KAAK;AACtB,QAAM,IAAI,SAAS,UAAU,IAAI,IAAI,KAAK,QAAQ,MAAM,IAAI,EAAE;AAC9D,SAAO,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;AACpG;AAEA,IAAM,YAAY,CAAC,SAAiB,KAAK,IAAI,sBAAsB,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAGzF,SAAS,oBAAoB,MAA2C;AAC7E,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI,IAAI;AACR,aAAW,MAAM,OAAO,IAAI,GAAG;AAC7B,SAAK,GAAG,YAAY,CAAC,KAAK;AAC1B,QAAI,KAAK,KAAK,GAAG,QAAU,MAAM;AAAA,EACnC;AACA,SAAQ,IAAI,uBAAwB;AACtC;AAEA,IAAM,cAAc,CAAC,MAAsB,EAAE,SAAS,SAAY,oBAAoB,EAAE,IAAI,IAAI,UAAU,EAAE,IAAI;AAGzG,SAAS,kBAAkB,MAAyB,MAAc,MAAyC;AAChH,SAAO,SAAS,QAAQ,GAAG,IAAI,IAAI,IAAI,SAAS,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI;AACzE;AAOO,SAAS,iBAAiBA,SAAuB,EAAE,OAAO,KAAK,OAAO,oBAAoB,IAAyD,CAAC,GAAW;AACpK,SAAO,GAAG,KAAK,QAAQ,QAAQ,EAAE,CAAC,IAAIA,QAAO,KAAK,IAAI,kBAAkBA,QAAO,QAAQ,UAAU,YAAYA,OAAM,GAAG,IAAI,CAAC;AAC7H;AAGO,SAAS,iBAAiBA,SAA+B;AAC9D,QAAM,CAAC,MAAM,EAAE,IAAI,kBAAkBA,QAAO,OAAO,YAAYA,OAAM,CAAC;AACtE,QAAM,UACHA,QAAO,QAAQ,cAAc,WAC1B,6JACA;AACN,SAAO,gLAAgL,IAAI,mCAAmC,EAAE,0SAA0S,MAAM;AAClhB;;;ADzDI,SAEI,KAFJ;AAjDJ,IAAM,SAAS,GAAG;AAAA,EAChB,OAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,IACV,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI,EAAE,MAAM,qBAAqB;AAAA,MACjC,IAAI,EAAE,MAAM,kBAAkB;AAAA,MAC9B,IAAI,EAAE,MAAM,oBAAoB;AAAA,MAChC,IAAI,EAAE,MAAM,sBAAsB;AAAA,MAClC,IAAI,EAAE,MAAM,oBAAoB;AAAA,IAClC;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CAAC;AAyBM,SAAS,WAAW,EAAE,KAAK,MAAM,IAAI,UAAU,aAAa,eAAe,MAAM,WAAW,GAAG,MAAM,GAAgB;AAC1H,QAAM,IAAI,OAAO,EAAE,KAAK,CAAC;AACzB,QAAM,CAAC,QAAQ,SAAS,IAAU,eAAwB,IAAI;AAE9D,QAAM,SAAS,gBAAgB,gBAAgB,iBAAiB,eAAe,EAAE,MAAM,SAAS,QAAQ,SAAS,OAAO,MAAM,IAAI,CAAC,IAAI;AACvI,QAAM,QAAQ,OAAO,WAAW,MAAM,MAAM;AAC5C,SACE,qBAAC,gBAAgB,MAAhB,EAAqB,aAAW,QAAQ,MAAM,WAAW,EAAE,KAAK,EAAE,UAAU,CAAC,GAAI,GAAG,OAClF;AAAA,aACC;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC,KAAK;AAAA,QACL;AAAA,QACA,WAAW,EAAE,MAAM;AAAA,QACnB,uBAAuB,CAAC,WAAW;AACjC,cAAI,WAAW,WAAW,UAAU,IAAK,WAAU,GAAG;AAAA,QACxD;AAAA;AAAA,IACF;AAAA,IAEF,oBAAC,gBAAgB,UAAhB,EAAyB,SAAS,QAAQ,MAAM,GAAG,WAAW,EAAE,SAAS,GACvE,oBACH;AAAA,KACF;AAEJ;AAUO,SAAS,YAAY,EAAE,KAAK,MAAM,WAAW,UAAU,GAAG,MAAM,GAAqB;AAC1F,QAAM,QAAc,eAAS,QAAQ,QAAQ;AAC7C,QAAM,QAAQ,QAAQ,SAAY,QAAQ,MAAM,MAAM,GAAG,GAAG;AAC5D,QAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SACE,qBAAC,SAAI,WAAW,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,GAAI,GAAG,OAChD;AAAA;AAAA,IACA,OAAO,KAAK,oBAAC,cAAW,MAAY,UAAU,IAAI,IAAI,IAAI;AAAA,KAC7D;AAEJ;AAGO,IAAM,SAAS,OAAO,OAAO,YAAY,EAAE,OAAO,YAAY,CAAC;","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"]}
|
package/dist/blocks.css
CHANGED
|
@@ -177,6 +177,17 @@ a.kz-fact:focus-visible { outline: 2px solid var(--kz-ring, var(--kz-accent)); o
|
|
|
177
177
|
.kz-cards[data-align="start"] { align-items: start; }
|
|
178
178
|
.kz-cards-cell { display: grid; min-width: 0; }
|
|
179
179
|
|
|
180
|
+
/* A foundation (CardGrid `foundation`): one wide card, a hairline stem, and a frame around the cards that stand on it. */
|
|
181
|
+
.kz-foundation { --kz-rail: color-mix(in srgb, var(--kz-hue, var(--kz-accent)) 50%, var(--kz-border-default)); display: grid; }
|
|
182
|
+
.kz-foundation-base { position: relative; display: grid; min-width: 0; }
|
|
183
|
+
.kz-foundation-base::after { content: ""; position: absolute; left: 50%; top: 100%; width: 1px; height: 28px; background: var(--kz-rail); }
|
|
184
|
+
.kz-foundation-group { position: relative; margin-top: 28px; padding: 22px 14px 14px; border: 1px solid var(--kz-rail); border-radius: 24px; }
|
|
185
|
+
.kz-foundation-label { position: absolute; top: -9px; left: 22px; padding: 0 8px; font: 500 11px/18px var(--kz-font-mono, ui-monospace, monospace); letter-spacing: 0.06em; text-transform: uppercase; color: var(--kz-hue-text, var(--kz-accent-text)); background: var(--kz-surface-0); }
|
|
186
|
+
.kz-foundation[data-position="end"] .kz-foundation-base { order: 2; }
|
|
187
|
+
.kz-foundation[data-position="end"] .kz-foundation-group { order: 1; margin-top: 0; margin-bottom: 28px; }
|
|
188
|
+
.kz-foundation[data-position="end"] .kz-foundation-base::after { top: auto; bottom: 100%; }
|
|
189
|
+
@media (max-width: 640px) { .kz-foundation-group { padding: 20px 8px 8px; border-radius: 20px; } }
|
|
190
|
+
|
|
180
191
|
/* The shared surface of the cards: a rail of the hue along the top, the light that follows the pointer (PointerLight
|
|
181
192
|
writes --kz-pointer-x/y), a lift and a lit border under the pointer or the focus. */
|
|
182
193
|
.kz-block-card {
|
package/dist/card-grid.d.ts
CHANGED
|
@@ -35,8 +35,21 @@ type CardGridProps = Omit<React.HTMLAttributes<HTMLElement>, "children"> & {
|
|
|
35
35
|
light?: boolean;
|
|
36
36
|
/** `ul` when the cards are a list (each cell an `li`). */
|
|
37
37
|
as?: "div" | "ul" | "ol";
|
|
38
|
+
/**
|
|
39
|
+
* A foundation: one wide card that the cards of the grid stand on (an engine, a platform, a shared service). It spans
|
|
40
|
+
* the full width beside the grid, and a hairline joins it to a frame around the cards — "everything inside runs on it".
|
|
41
|
+
* The frame takes the hue of `className` (a `data-hue` or a brand class on the grid sets it for the whole block).
|
|
42
|
+
*/
|
|
43
|
+
foundation?: React.ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Where the foundation sits: above the cards, or under them.
|
|
46
|
+
* @default "start"
|
|
47
|
+
*/
|
|
48
|
+
foundationPosition?: "start" | "end";
|
|
49
|
+
/** A short mono label on the frame's edge, naming the relation (`Powered by Orazaka`). Only with `foundation`. */
|
|
50
|
+
foundationLabel?: React.ReactNode;
|
|
38
51
|
};
|
|
39
52
|
/** A responsive grid of cards: equal rows, a staggered reveal, the pointer's light. */
|
|
40
|
-
declare function CardGrid({ children, min, fit, align, reveal, columns, light, as: Tag, className, style, ...props }: CardGridProps): React.JSX.Element;
|
|
53
|
+
declare function CardGrid({ children, min, fit, align, reveal, columns, light, as: Tag, foundation, foundationPosition, foundationLabel, className, style, ...props }: CardGridProps): React.JSX.Element;
|
|
41
54
|
|
|
42
55
|
export { CardGrid, type CardGridProps };
|
package/dist/card-grid.js
CHANGED
|
@@ -9,20 +9,35 @@ import {
|
|
|
9
9
|
|
|
10
10
|
// src/card-grid/card-grid.tsx
|
|
11
11
|
import * as React from "react";
|
|
12
|
-
import { jsx } from "react/jsx-runtime";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
var STEP_MS = 70;
|
|
14
|
-
function CardGrid({
|
|
14
|
+
function CardGrid({
|
|
15
|
+
children,
|
|
16
|
+
min = 300,
|
|
17
|
+
fit = "fill",
|
|
18
|
+
align = "stretch",
|
|
19
|
+
reveal = true,
|
|
20
|
+
columns = 3,
|
|
21
|
+
light = true,
|
|
22
|
+
as: Tag = "div",
|
|
23
|
+
foundation,
|
|
24
|
+
foundationPosition = "start",
|
|
25
|
+
foundationLabel,
|
|
26
|
+
className,
|
|
27
|
+
style,
|
|
28
|
+
...props
|
|
29
|
+
}) {
|
|
15
30
|
const ref = React.useRef(null);
|
|
16
31
|
usePointerLight(light ? ref : NO_REF, POINTER_LIGHT_TARGET);
|
|
17
32
|
const Cell = Tag === "div" ? "div" : "li";
|
|
18
|
-
|
|
33
|
+
const grid = /* @__PURE__ */ jsx(
|
|
19
34
|
Tag,
|
|
20
35
|
{
|
|
21
|
-
ref,
|
|
22
|
-
className: cn("kz-cards", className),
|
|
36
|
+
ref: foundation ? void 0 : ref,
|
|
37
|
+
className: cn("kz-cards", !foundation && className),
|
|
23
38
|
"data-fit": fit === "fit" ? "" : void 0,
|
|
24
39
|
"data-align": align === "start" ? "start" : void 0,
|
|
25
|
-
style: { "--kz-cards-min": `${min}px`, ...style },
|
|
40
|
+
style: { "--kz-cards-min": `${min}px`, ...foundation ? null : style },
|
|
26
41
|
...props,
|
|
27
42
|
children: React.Children.toArray(children).map((child, i) => {
|
|
28
43
|
const step = Math.min(i % Math.max(columns, 1), 5);
|
|
@@ -39,6 +54,14 @@ function CardGrid({ children, min = 300, fit = "fill", align = "stretch", reveal
|
|
|
39
54
|
})
|
|
40
55
|
}
|
|
41
56
|
);
|
|
57
|
+
if (!foundation) return grid;
|
|
58
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: cn("kz-foundation", className), "data-position": foundationPosition, style, children: [
|
|
59
|
+
/* @__PURE__ */ jsx("div", { className: "kz-foundation-base", "data-reveal": reveal ? "" : void 0, children: foundation }),
|
|
60
|
+
/* @__PURE__ */ jsxs("div", { className: "kz-foundation-group", children: [
|
|
61
|
+
foundationLabel && /* @__PURE__ */ jsx("span", { className: "kz-foundation-label", children: foundationLabel }),
|
|
62
|
+
grid
|
|
63
|
+
] })
|
|
64
|
+
] });
|
|
42
65
|
}
|
|
43
66
|
var NO_REF = { current: null };
|
|
44
67
|
export {
|
package/dist/card-grid.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/card-grid/card-grid.tsx"],"sourcesContent":["// Client (the pointer's light). A responsive grid of cards (blocks.css): one column on a phone, the cells of a row\n// share their height, each card rises once as it enters the viewport — staggered by column — and the pointer carries a\n// light over the card it is on.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { POINTER_LIGHT_TARGET, usePointerLight } from \"../pointer-light/pointer-light\";\n\nconst STEP_MS = 70;\n\nexport type CardGridProps = Omit<React.HTMLAttributes<HTMLElement>, \"children\"> & {\n /** The cards: one cell each. */\n children: React.ReactNode;\n /**\n * The narrowest a column may be, in pixels: the grid fits as many columns as the width allows.\n * @default 300\n */\n min?: number;\n /**\n * `fill` keeps empty tracks (a lone card keeps its width); `fit` stretches the cards of a short row across the width.\n * @default \"fill\"\n */\n fit?: \"fill\" | \"fit\";\n /**\n * `stretch` gives the cells of a row one height; `start` lets each card keep its own (cards that open a panel).\n * @default \"stretch\"\n */\n align?: \"stretch\" | \"start\";\n /**\n * The cards rise once into place (`[data-reveal]`, MotionObserver), staggered by column. `false` on the first screen.\n * @default true\n */\n reveal?: boolean;\n /**\n * The columns the stagger counts (the delay restarts on every row).\n * @default 3\n */\n columns?: number;\n /**\n * The light that follows the pointer over the cards (`[data-kz-light]`).\n * @default true\n */\n light?: boolean;\n /** `ul` when the cards are a list (each cell an `li`). */\n as?: \"div\" | \"ul\" | \"ol\";\n};\n\n/** A responsive grid of cards: equal rows, a staggered reveal, the pointer's light. */\nexport function CardGrid({
|
|
1
|
+
{"version":3,"sources":["../src/card-grid/card-grid.tsx"],"sourcesContent":["// Client (the pointer's light). A responsive grid of cards (blocks.css): one column on a phone, the cells of a row\n// share their height, each card rises once as it enters the viewport — staggered by column — and the pointer carries a\n// light over the card it is on.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { POINTER_LIGHT_TARGET, usePointerLight } from \"../pointer-light/pointer-light\";\n\nconst STEP_MS = 70;\n\nexport type CardGridProps = Omit<React.HTMLAttributes<HTMLElement>, \"children\"> & {\n /** The cards: one cell each. */\n children: React.ReactNode;\n /**\n * The narrowest a column may be, in pixels: the grid fits as many columns as the width allows.\n * @default 300\n */\n min?: number;\n /**\n * `fill` keeps empty tracks (a lone card keeps its width); `fit` stretches the cards of a short row across the width.\n * @default \"fill\"\n */\n fit?: \"fill\" | \"fit\";\n /**\n * `stretch` gives the cells of a row one height; `start` lets each card keep its own (cards that open a panel).\n * @default \"stretch\"\n */\n align?: \"stretch\" | \"start\";\n /**\n * The cards rise once into place (`[data-reveal]`, MotionObserver), staggered by column. `false` on the first screen.\n * @default true\n */\n reveal?: boolean;\n /**\n * The columns the stagger counts (the delay restarts on every row).\n * @default 3\n */\n columns?: number;\n /**\n * The light that follows the pointer over the cards (`[data-kz-light]`).\n * @default true\n */\n light?: boolean;\n /** `ul` when the cards are a list (each cell an `li`). */\n as?: \"div\" | \"ul\" | \"ol\";\n /**\n * A foundation: one wide card that the cards of the grid stand on (an engine, a platform, a shared service). It spans\n * the full width beside the grid, and a hairline joins it to a frame around the cards — \"everything inside runs on it\".\n * The frame takes the hue of `className` (a `data-hue` or a brand class on the grid sets it for the whole block).\n */\n foundation?: React.ReactNode;\n /**\n * Where the foundation sits: above the cards, or under them.\n * @default \"start\"\n */\n foundationPosition?: \"start\" | \"end\";\n /** A short mono label on the frame's edge, naming the relation (`Powered by Orazaka`). Only with `foundation`. */\n foundationLabel?: React.ReactNode;\n};\n\n/** A responsive grid of cards: equal rows, a staggered reveal, the pointer's light. */\nexport function CardGrid({\n children,\n min = 300,\n fit = \"fill\",\n align = \"stretch\",\n reveal = true,\n columns = 3,\n light = true,\n as: Tag = \"div\",\n foundation,\n foundationPosition = \"start\",\n foundationLabel,\n className,\n style,\n ...props\n}: CardGridProps) {\n const ref = React.useRef<HTMLElement>(null);\n usePointerLight(light ? ref : NO_REF, POINTER_LIGHT_TARGET);\n const Cell = Tag === \"div\" ? \"div\" : \"li\";\n const grid = (\n <Tag\n ref={foundation ? undefined : (ref as React.Ref<never>)}\n className={cn(\"kz-cards\", !foundation && className)}\n data-fit={fit === \"fit\" ? \"\" : undefined}\n data-align={align === \"start\" ? \"start\" : undefined}\n style={{ \"--kz-cards-min\": `${min}px`, ...(foundation ? null : style) } as React.CSSProperties}\n {...props}\n >\n {React.Children.toArray(children).map((child, i) => {\n const step = Math.min(i % Math.max(columns, 1), 5);\n return (\n <Cell\n key={React.isValidElement(child) && child.key != null ? child.key : i}\n className=\"kz-cards-cell\"\n data-reveal={reveal ? \"\" : undefined}\n style={reveal && step ? ({ \"--kz-delay\": `${step * STEP_MS}ms` } as React.CSSProperties) : undefined}\n >\n {child}\n </Cell>\n );\n })}\n </Tag>\n );\n if (!foundation) return grid;\n return (\n <div ref={ref as React.Ref<HTMLDivElement>} className={cn(\"kz-foundation\", className)} data-position={foundationPosition} style={style}>\n <div className=\"kz-foundation-base\" data-reveal={reveal ? \"\" : undefined}>\n {foundation}\n </div>\n <div className=\"kz-foundation-group\">\n {foundationLabel && <span className=\"kz-foundation-label\">{foundationLabel}</span>}\n {grid}\n </div>\n </div>\n );\n}\n\nconst NO_REF: React.RefObject<HTMLElement | null> = { current: null };\n"],"mappings":";;;;;;;;;;AAGA,YAAY,WAAW;AAyFb,cAkBJ,YAlBI;AApFV,IAAM,UAAU;AAqDT,SAAS,SAAS;AAAA,EACvB;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,IAAI,MAAM;AAAA,EACV;AAAA,EACA,qBAAqB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,MAAY,aAAoB,IAAI;AAC1C,kBAAgB,QAAQ,MAAM,QAAQ,oBAAoB;AAC1D,QAAM,OAAO,QAAQ,QAAQ,QAAQ;AACrC,QAAM,OACJ;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,aAAa,SAAa;AAAA,MAC/B,WAAW,GAAG,YAAY,CAAC,cAAc,SAAS;AAAA,MAClD,YAAU,QAAQ,QAAQ,KAAK;AAAA,MAC/B,cAAY,UAAU,UAAU,UAAU;AAAA,MAC1C,OAAO,EAAE,kBAAkB,GAAG,GAAG,MAAM,GAAI,aAAa,OAAO,MAAO;AAAA,MACrE,GAAG;AAAA,MAEH,UAAM,eAAS,QAAQ,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM;AAClD,cAAM,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,SAAS,CAAC,GAAG,CAAC;AACjD,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YACV,eAAa,SAAS,KAAK;AAAA,YAC3B,OAAO,UAAU,OAAQ,EAAE,cAAc,GAAG,OAAO,OAAO,KAAK,IAA4B;AAAA,YAE1F;AAAA;AAAA,UALU,qBAAe,KAAK,KAAK,MAAM,OAAO,OAAO,MAAM,MAAM;AAAA,QAMtE;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEF,MAAI,CAAC,WAAY,QAAO;AACxB,SACE,qBAAC,SAAI,KAAuC,WAAW,GAAG,iBAAiB,SAAS,GAAG,iBAAe,oBAAoB,OACxH;AAAA,wBAAC,SAAI,WAAU,sBAAqB,eAAa,SAAS,KAAK,QAC5D,sBACH;AAAA,IACA,qBAAC,SAAI,WAAU,uBACZ;AAAA,yBAAmB,oBAAC,UAAK,WAAU,uBAAuB,2BAAgB;AAAA,MAC1E;AAAA,OACH;AAAA,KACF;AAEJ;AAEA,IAAM,SAA8C,EAAE,SAAS,KAAK;","names":[]}
|
package/dist/chat.d.ts
CHANGED
|
@@ -14,8 +14,14 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
14
14
|
typing?: tailwind_variants.ClassValue;
|
|
15
15
|
dot?: tailwind_variants.ClassValue;
|
|
16
16
|
composer?: tailwind_variants.ClassValue;
|
|
17
|
+
pillRoot?: tailwind_variants.ClassValue;
|
|
18
|
+
pillLeading?: tailwind_variants.ClassValue;
|
|
19
|
+
pillStatus?: tailwind_variants.ClassValue;
|
|
17
20
|
item?: tailwind_variants.ClassValue;
|
|
18
21
|
count?: tailwind_variants.ClassValue;
|
|
22
|
+
pill?: tailwind_variants.ClassValue;
|
|
23
|
+
pillInput?: tailwind_variants.ClassValue;
|
|
24
|
+
pillSend?: tailwind_variants.ClassValue;
|
|
19
25
|
};
|
|
20
26
|
};
|
|
21
27
|
} | {
|
|
@@ -31,8 +37,14 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
31
37
|
typing?: tailwind_variants.ClassValue;
|
|
32
38
|
dot?: tailwind_variants.ClassValue;
|
|
33
39
|
composer?: tailwind_variants.ClassValue;
|
|
40
|
+
pillRoot?: tailwind_variants.ClassValue;
|
|
41
|
+
pillLeading?: tailwind_variants.ClassValue;
|
|
42
|
+
pillStatus?: tailwind_variants.ClassValue;
|
|
34
43
|
item?: tailwind_variants.ClassValue;
|
|
35
44
|
count?: tailwind_variants.ClassValue;
|
|
45
|
+
pill?: tailwind_variants.ClassValue;
|
|
46
|
+
pillInput?: tailwind_variants.ClassValue;
|
|
47
|
+
pillSend?: tailwind_variants.ClassValue;
|
|
36
48
|
};
|
|
37
49
|
};
|
|
38
50
|
} | {}, {
|
|
@@ -46,6 +58,12 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
46
58
|
composer: string;
|
|
47
59
|
input: string;
|
|
48
60
|
send: string;
|
|
61
|
+
pillRoot: string;
|
|
62
|
+
pill: string;
|
|
63
|
+
pillLeading: string;
|
|
64
|
+
pillInput: string;
|
|
65
|
+
pillSend: string;
|
|
66
|
+
pillStatus: string;
|
|
49
67
|
item: string;
|
|
50
68
|
count: string;
|
|
51
69
|
}, undefined, {
|
|
@@ -61,8 +79,14 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
61
79
|
typing?: tailwind_variants.ClassValue;
|
|
62
80
|
dot?: tailwind_variants.ClassValue;
|
|
63
81
|
composer?: tailwind_variants.ClassValue;
|
|
82
|
+
pillRoot?: tailwind_variants.ClassValue;
|
|
83
|
+
pillLeading?: tailwind_variants.ClassValue;
|
|
84
|
+
pillStatus?: tailwind_variants.ClassValue;
|
|
64
85
|
item?: tailwind_variants.ClassValue;
|
|
65
86
|
count?: tailwind_variants.ClassValue;
|
|
87
|
+
pill?: tailwind_variants.ClassValue;
|
|
88
|
+
pillInput?: tailwind_variants.ClassValue;
|
|
89
|
+
pillSend?: tailwind_variants.ClassValue;
|
|
66
90
|
};
|
|
67
91
|
};
|
|
68
92
|
} | {}, {
|
|
@@ -76,6 +100,12 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
76
100
|
composer: string;
|
|
77
101
|
input: string;
|
|
78
102
|
send: string;
|
|
103
|
+
pillRoot: string;
|
|
104
|
+
pill: string;
|
|
105
|
+
pillLeading: string;
|
|
106
|
+
pillInput: string;
|
|
107
|
+
pillSend: string;
|
|
108
|
+
pillStatus: string;
|
|
79
109
|
item: string;
|
|
80
110
|
count: string;
|
|
81
111
|
}, tailwind_variants.TVReturnTypeLike<unknown, {
|
|
@@ -89,6 +119,12 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
89
119
|
composer: string;
|
|
90
120
|
input: string;
|
|
91
121
|
send: string;
|
|
122
|
+
pillRoot: string;
|
|
123
|
+
pill: string;
|
|
124
|
+
pillLeading: string;
|
|
125
|
+
pillInput: string;
|
|
126
|
+
pillSend: string;
|
|
127
|
+
pillStatus: string;
|
|
92
128
|
item: string;
|
|
93
129
|
count: string;
|
|
94
130
|
}>>;
|
|
@@ -144,17 +180,29 @@ type MessageComposerProps = {
|
|
|
144
180
|
placeholder: string;
|
|
145
181
|
/** The send button's name (“Send”). */
|
|
146
182
|
sendLabel: string;
|
|
147
|
-
/** Before the input: an attach button. */
|
|
183
|
+
/** Before the input: an attach button (`bar`), or the mode/model chip and attach, inside the field (`pill`). */
|
|
148
184
|
leading?: React.ReactNode;
|
|
149
185
|
/** Cannot write: the reason in its place. */
|
|
150
186
|
disabledReason?: string;
|
|
151
187
|
/** The longest message. Default 4000. */
|
|
152
188
|
maxLength?: number;
|
|
189
|
+
/** `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`. */
|
|
190
|
+
variant?: "bar" | "pill";
|
|
191
|
+
/** `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. */
|
|
192
|
+
status?: React.ReactNode;
|
|
193
|
+
/** An answer is on its way: the send is held (and `aria-busy` is set) while the field stays where it is, size included. */
|
|
194
|
+
busy?: boolean;
|
|
195
|
+
/** Overrides “the draft has text” — e.g. an attachment alone can be sent. A busy composer never sends. */
|
|
196
|
+
canSend?: boolean;
|
|
197
|
+
/** Attributes (`data-testid`, `autoFocus`, `id`…) forwarded to the textarea. */
|
|
198
|
+
inputProps?: Omit<React.ComponentPropsWithoutRef<"textarea">, "value" | "onChange" | "placeholder" | "maxLength" | "className" | "rows">;
|
|
199
|
+
/** Attributes forwarded to the send button. */
|
|
200
|
+
sendProps?: Omit<React.ComponentPropsWithoutRef<"button">, "type" | "disabled" | "className" | "aria-label">;
|
|
153
201
|
/** Classes merged on the root. */
|
|
154
202
|
className?: string;
|
|
155
203
|
};
|
|
156
|
-
/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. */
|
|
157
|
-
declare function MessageComposer({ value, onValueChange, onSend, placeholder, sendLabel, leading, disabledReason, maxLength, className }: MessageComposerProps): React.JSX.Element;
|
|
204
|
+
/** 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. */
|
|
205
|
+
declare function MessageComposer({ value, onValueChange, onSend, placeholder, sendLabel, leading, disabledReason, maxLength, variant, status, busy, canSend, inputProps, sendProps, className, }: MessageComposerProps): React.JSX.Element;
|
|
158
206
|
type ConversationItemProps = {
|
|
159
207
|
/** The other person's name. */
|
|
160
208
|
name: string;
|
package/dist/chat.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "./chunk-NPF3OHE2.js";
|
|
3
3
|
import {
|
|
4
4
|
Avatar
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KB7OLYIJ.js";
|
|
6
6
|
|
|
7
7
|
// src/chat/chat.tsx
|
|
8
8
|
import { tv } from "tailwind-variants";
|
|
@@ -19,6 +19,12 @@ var chat = tv({
|
|
|
19
19
|
composer: "flex items-end gap-2 border-t border-border-subtle bg-surface-0 px-3 py-2",
|
|
20
20
|
input: "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 focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
|
|
21
21
|
send: "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 focus-visible:ring-2 focus-visible:ring-ring disabled:bg-surface-3 disabled:text-fg-muted [&>svg]:h-[18px] [&>svg]:w-[18px]",
|
|
22
|
+
pillRoot: "flex w-full flex-col gap-2",
|
|
23
|
+
pill: "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 focus-within:border-accent focus-within:ring-2 focus-within:ring-ring",
|
|
24
|
+
pillLeading: "flex min-h-9 shrink-0 items-center gap-1 self-end",
|
|
25
|
+
pillInput: "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 focus:outline-hidden focus-visible:outline-hidden",
|
|
26
|
+
pillSend: "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 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-1 disabled:cursor-not-allowed disabled:bg-surface-3 disabled:text-fg-muted disabled:shadow-none [&>svg]:h-4 [&>svg]:w-4",
|
|
27
|
+
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",
|
|
22
28
|
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",
|
|
23
29
|
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"
|
|
24
30
|
}
|
|
@@ -60,42 +66,68 @@ function TypingIndicator({ label, showLabel, className }) {
|
|
|
60
66
|
/* @__PURE__ */ jsx("span", { className: showLabel ? "text-xs text-fg-secondary" : "sr-only", children: label })
|
|
61
67
|
] });
|
|
62
68
|
}
|
|
63
|
-
function MessageComposer({
|
|
69
|
+
function MessageComposer({
|
|
70
|
+
value,
|
|
71
|
+
onValueChange,
|
|
72
|
+
onSend,
|
|
73
|
+
placeholder,
|
|
74
|
+
sendLabel,
|
|
75
|
+
leading,
|
|
76
|
+
disabledReason,
|
|
77
|
+
maxLength = 4e3,
|
|
78
|
+
variant = "bar",
|
|
79
|
+
status,
|
|
80
|
+
busy = false,
|
|
81
|
+
canSend,
|
|
82
|
+
inputProps,
|
|
83
|
+
sendProps,
|
|
84
|
+
className
|
|
85
|
+
}) {
|
|
64
86
|
const s = chat();
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
const pill = variant === "pill";
|
|
88
|
+
const ready = !busy && (canSend ?? value.trim().length > 0);
|
|
89
|
+
const textarea = /* @__PURE__ */ jsx(
|
|
90
|
+
"textarea",
|
|
68
91
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
"aria-label": placeholder,
|
|
93
|
+
placeholder,
|
|
94
|
+
rows: 1,
|
|
95
|
+
maxLength,
|
|
96
|
+
...inputProps,
|
|
97
|
+
value,
|
|
98
|
+
onChange: (e) => onValueChange(e.target.value),
|
|
99
|
+
onKeyDown: (e) => {
|
|
100
|
+
inputProps?.onKeyDown?.(e);
|
|
101
|
+
if (e.defaultPrevented) return;
|
|
102
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
if (ready) onSend();
|
|
105
|
+
}
|
|
73
106
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/* @__PURE__ */ jsx(
|
|
77
|
-
"textarea",
|
|
78
|
-
{
|
|
79
|
-
"aria-label": placeholder,
|
|
80
|
-
placeholder,
|
|
81
|
-
rows: 1,
|
|
82
|
-
maxLength,
|
|
83
|
-
value,
|
|
84
|
-
onChange: (e) => onValueChange(e.target.value),
|
|
85
|
-
onKeyDown: (e) => {
|
|
86
|
-
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
87
|
-
e.preventDefault();
|
|
88
|
-
if (ready) onSend();
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
className: s.input(),
|
|
92
|
-
style: { fieldSizing: "content" }
|
|
93
|
-
}
|
|
94
|
-
),
|
|
95
|
-
/* @__PURE__ */ jsx("button", { type: "submit", "aria-label": sendLabel, disabled: !ready, className: s.send(), children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M4 12l16-8-6 16-2.5-6.5L4 12z" }) }) })
|
|
96
|
-
] })
|
|
107
|
+
className: pill ? s.pillInput() : s.input(),
|
|
108
|
+
style: { fieldSizing: "content" }
|
|
97
109
|
}
|
|
98
110
|
);
|
|
111
|
+
const send = /* @__PURE__ */ jsx("button", { type: "submit", "aria-label": sendLabel, ...sendProps, disabled: !ready, className: pill ? s.pillSend() : s.send(), children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M4 12l16-8-6 16-2.5-6.5L4 12z" }) }) });
|
|
112
|
+
const submit = (e) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
if (ready) onSend();
|
|
115
|
+
};
|
|
116
|
+
if (pill) {
|
|
117
|
+
return /* @__PURE__ */ jsxs("form", { className: s.pillRoot({ className }), onSubmit: submit, "aria-busy": busy || void 0, children: [
|
|
118
|
+
disabledReason ? /* @__PURE__ */ jsx("p", { className: "m-0 rounded-[1.75rem] border border-border-default bg-surface-1 px-4 py-2.5 text-sm text-fg-secondary", children: disabledReason }) : /* @__PURE__ */ jsxs("div", { className: s.pill(), children: [
|
|
119
|
+
leading ? /* @__PURE__ */ jsx("div", { className: s.pillLeading(), children: leading }) : null,
|
|
120
|
+
textarea,
|
|
121
|
+
send
|
|
122
|
+
] }),
|
|
123
|
+
/* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", className: s.pillStatus(), children: status })
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
return /* @__PURE__ */ jsx("form", { className: s.composer({ className }), onSubmit: submit, "aria-busy": busy || void 0, children: disabledReason ? /* @__PURE__ */ jsx("p", { className: "m-0 flex-1 py-2 text-sm text-fg-secondary", children: disabledReason }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
127
|
+
leading,
|
|
128
|
+
textarea,
|
|
129
|
+
send
|
|
130
|
+
] }) });
|
|
99
131
|
}
|
|
100
132
|
function ConversationItem({ name, avatar, preview, typing, time, unread = 0, unreadLabel, online, onlineLabel, href, onSelect, className }) {
|
|
101
133
|
const s = chat();
|