Contents
Changelog: packages
What changed in each @haruhimemoe package (ui, next-kit, osu, mirror, pool and the rest), release by release, newest first.
brand0.8.0
1 changedChanged
- packs gets its own hue, 30 (orange), instead of sharing haruhime's 333.
crowdfund0.1.0
12 addedAdded
fundingGoalSchema,goalTierSchema,donationSchema,refundSchema,settledSchema,currencyCodeSchema,minorUnitsSchema,positiveMinorUnitsSchema: zod schemas with no refinements, so they.extend().currencyExponent,toMinorUnits,netAmount,isRefunded,refundDonation: integer money in any currency, full and partial refunds.totalsByCurrency,totalsFor: gross, refunded and net totals, withsettledvalues and a host-suppliedconvert.goalProgress,unlockedTiers,nextTier: progress toward a goal and its stretch tiers.topDonors,publicDonation: donor leaderboards and public rows that never reveal an anonymous donor.donationKey,dedupeDonations,hasDonation: dedupe by(source, externalId).@haruhimemoe/crowdfund/kofi:kofiPayloadSchema,parseKofiBody,verifyKofiToken,kofiToDonation,extractKofiTransactionId,KOFI_TYPES.@haruhimemoe/crowdfund/stripe:verifyStripeSignature,parseStripeEvent,stripeEventSchema,stripeToDonation,stripeRefund.@haruhimemoe/crowdfund/paypal:paypalVerifyBody,parsePaypalEvent,paypalEventSchema,paypalToDonation,paypalRefund.@haruhimemoe/crowdfund/github-sponsors:verifyGithubSignature,parseGithubSponsorshipEvent,githubSponsorshipEventSchema,githubSponsorshipToDonation.@haruhimemoe/crowdfund/buymeacoffee:verifyBuyMeACoffeeSignature,parseBuyMeACoffeeEvent,buyMeACoffeeEventSchema,buyMeACoffeeToDonation,buyMeACoffeeRefund.CrowdfundErrorwith acode.
harumin-config0.1.0
1 addedAdded
- Guild settings (
guildSettingsSchema,defaultGuildSettings,readGuildSettings,guildSettingsPatchSchema,applyGuildSettingsPatch), auto-embed keys, defaults and labels,/trackentries, collection names, the bot's service routes and their bodies, andguildIconUrl.
- Guild settings (
invites0.1.0
6 addedAdded
- zod schemas for invites, drafts, terms, deadlines, reminders, history, reminder prefs and blocks, all
.extend()-able.INVITE_KINDS,INVITE_STATES,HISTORY_ACTIONS. createInvite,transition,allowedActions,isOpen,isExpired,effectiveState: the state machine over pending, countered, accepted, declined, withdrawn, cancelled and expired.checkTerms,diffTerms.checkBlocks,checkCooldown,checkRateLimit,canSend: send rules.canRemind: reminders for accepted invites per the agreed plan and the recipient's prefs.inbox: needs my answer, waiting on them, active, past.
- zod schemas for invites, drafts, terms, deadlines, reminders, history, reminder prefs and blocks, all
mirror0.1.0
7 addedAdded
createMirrorClientwithdownloadSet(failover across hinai, osu.direct, Mino and Sayobot, with zip checks, progress and abort),downloadLinks,health, and hinai'sgetBeatmapsandgetAvailability.@haruhimemoe/mirror/hinai: the whole@haruhimemoe/hinai0.3.1 client, moved here unchanged.@haruhimemoe/hinaiis deprecated in its favor; swap the import path.@haruhimemoe/mirror/testing:@haruhimemoe/hinai/testing, moved here unchanged (msw 2 as an optional peer).HinaiError,HinaiErrorCode,OSZ_MIME,parseRetryAfter,backoffDelayMsandMAX_RETRY_DELAY_MSfrom the root too, the same objects as/hinai's.- Per-mirror cooldowns: a network error, timeout or 429 at once, other failures after
MIRROR_FAILURE_LIMITin a row. MirrorErrorwithnot_found,unavailableanddownload_disabled, and every failed attempt.MIRRORS,DEFAULT_MIRRORSand customMirrorobjects, withmissingStatusesfor mirrors that don't answer 404.
next-kit0.15.0
3 added, 1 changedAdded
i18n(new subpath, no next-intl import):LocaleConfig,DEFAULT_LOCALES,hasLocaleandnegotiateLocale(saved user locale, then Accept-Language q-order with a region fallback, then the default). The header is capped at 1 KB and 20 entries before parsing.i18n/next-intl(new subpath):createRequestConfig(injected app and package catalog loaders, merged deeply with the app's last) andcreateI18nMiddleware(next-intl's middleware, "as-needed" prefix,NEXT_LOCALEcookie on an optional parent domain, then the app's own step). PlusmergeMessagesandresolveRequestConfig.next-intl^4.14.0 is an optional peer dependency; only this subpath needs it.seo:hreflangAlternates(config, path, baseUrl).
Changed
seo:pageMetadatatakesalternates: { languages }and writes it next to the canonical.
next-kit0.14.0
2 added, 1 changedAdded
account(new subpath): account export and delete across apps.createAccountHandlersgives an app POST handlers for/api/internal/account/{export,delete}behind its own bearer secret (ACCOUNT_FANOUT_SECRET, at least 32 bytes), with a zod-checked{ userId }body and no-store answers.fanOutlets the hub call every registered app with that app's secret: https only (http on localhost), redirects never followed, a 10 s timeout, three tries for delete, and unset secrets reported asnot_configured.exportBundlepacks the results into one JSON download. PlussecretFor,appUrl,usableSecretand theAccountAppregistry type.inbox(new subpath): invites and notifications in the identity database.createInboxStore(invites upserted by id and held by one app, notifications with an unread filter,markRead, a 90-day TTL,deleteForfor account delete),createInboxRoutesfor the hub's/api/internal/inbox(the matching secret picks the app, checked against every app with no early exit; another app's invite id is a 409),createInboxClientfor apps,matchAppandinboxIndexSpecs.
Changed
mongo:buildIdentityIndexesalso builds the inbox indexes.
next-kit0.13.0
3 addedAdded
auth: Discord link for the hub.discordLinkConfig(env, hubUrl)(null whenDISCORD_CLIENT_IDorDISCORD_CLIENT_SECRETis unset, which turns the feature off),createDiscordLinkRoutes(POSTstartbehindrefuseCrossSite, GETcallback, POSTunlink; a signed, 10-minute, host-only state cookie; writes onlydiscordIdanddiscordUsername, never a token or anaccountrow;?discord=linked|taken|error),findUserByDiscordId(refuses banned users),DISCORD_SCOPESandDISCORD_STATE_COOKIE.mongo:buildIdentityIndexesadds a partial unique index onuser.discordId(IDENTITY_INDEXES.userDiscordId). A duplicate-key error on link maps totaken.api-keys: scopes.createApiKeyStore({ scopes })declares the app's names,issue(userId, scopes?)defaults to["*"]and refuses undeclared names,ApiKeyInfoandApiKeyMatchcarryscopes(a stored key without the field reads as["*"], so nothing migrates), andwithApiKey(handler, { scope })answers 403insufficient_scope. NewhasScope,normalizeScopes,ALL_SCOPESandAPI_INSUFFICIENT_SCOPE.
next-kit0.12.2
1 fixed, 1 changedFixed
check'snext-kit migrate-identitynow skips users without a numericosuId(system accounts) entirely: they're never grouped with other users, never inserted intoidentity, and never get anidMapentry, so references pointing at them are left untouched instead of being rewritten to nowhere.MigrateReportgainsusersSkipped; the CLI's summary line reports it alongsideusersSeen/usersWritten.
Changed
auth's oldSessionReadertype (the better-auth instance shape used bygetOsuUser/getSessionUser/requireSession/requireAdmin) is renamed toOsuAuthInstance, soSessionReadercan mean one thing:createSessionReader's own instance type (session-reader.ts), which the barrel used to export only asSessionReaderInstanceto dodge this exact collision.session.tskeeps aSessionReaderalias ofOsuAuthInstancefor source compatibility, andsession-reader.tskeeps aSessionReaderInstancealias ofSessionReader(the barrel exports both —bb/packs/pools/haruhime.moeall importSessionReaderInstance, none import the oldSessionReader, so this is a non-breaking patch for every real consumer).
next-kit0.12.1
1 changedChanged
check'snext-kit migrate-identityis idempotent now that the hub is live andidentityholds real users who signed in there directly: a source group's osuId matching an existing identity user merges into it (that user's_idand fields win outright, only its own missing fields get filled from the source) instead of refusing on a non-empty identity. Accounts dedupe byproviderId+accountIdand API keys byhashagainst identity's own existing rows too, so a second--executerun inserts nothing new.MigrateReportgainsusersMerged.--drop-oldis unchanged: still its own run, still refusing whileidentityis empty.
next-kit0.12.0
1 added, 2 changedAdded
- Identity core (breaking for every app — see below):
mongo'screateMongogainsidentityDbName/getIdentityDb()so the hub reads anidentitydatabase on the same client,buildIdentityIndexesbuilds its four indexes,auth'screateOsuAuthgainscookieDomain/trustedOriginsfor the hub (every better-auth cookie, including OAuth state and PKCE, on the parent domain) and a 30-day session with a 1-dayupdateAge,createSessionReaderreads a satellite's session with zero database writes (raw cookie verification, nobetterAuth()instance) and pings the hub to refresh a session pastupdateAge,getSessionUser/requireSession/requireAdminread either source and refuse a banned user,server'ssafeAbsoluteNextis the hub's exact-hostname allowlist for a satellite's absolutenext(it returns the normalized URL),hubSignInUrlbuilds a satellite's link to the hub's sign-in page through the same allowlist, andcheck'snext-kit migrate-identitymergesbb/packs/poolsusers intoidentitybyosuId(dry run by default).
Changed
- Breaking.
mongo'sonConnectnow takes one{ db, identityDb?, client }argument instead of(db, client). - Breaking. The identity user gains
locale,notificationPrefs,bannedAt,banReason,limits,discordIdanddiscordUsername(auth'sIDENTITY_USER_FIELDS, merged into everycreateOsuAuthinstance'sadditionalFields, single-DB apps included).OsuSessionUserandOsuSessiongainbannedAt. packs'systemfield does not move with this release: it stays a plain field on packs' ownusercollection for now. Once packs cuts over to the sharedidentitydatabase (after the migration script runs),systemmust move to an app-side profile collection keyed byuserId, because the identity user row is shared withbbandpoolsand has no room for one app's own flag. Do that move in the same deploy that switches packs tocreateSessionReader, not before.
- Identity core (breaking for every app — see below):
next-kit0.11.1
1 fixedFixed
HARUHIME_ORG's email is haruhime@haruhime.moe and its Discord link is https://haruhime.moe/discord.
osu0.6.0
4 addedAdded
- Profiles and scores on the client:
getUserProfile(user, { beforeCall, ruleset })(anOsuUserProfilewith one ruleset's statistics),getUserScores(userId, "best" | "recent" | "firsts", { ruleset, limit, offset, includeFails }),getBeatmapUserScores(beatmapId, userId, { ruleset }),getBeatmapScores(beatmapId, { ruleset, limit }), andgetBeatmap(beatmapId)(metadata plusmaxComboandstatus). Score calls sendx-api-version: 20220705for the lazer score format; a row that fails the schema is left out. For harumin. - Score shapes in
/shapes:osuScoreSchema,toOsuScore,osuModSchema,SCORE_RANKS, and the typesOsuScore,OsuScoreRow,OsuScoreBeatmap,OsuScoreBeatmapset,OsuScoreUser,OsuMod,ScoreRank. Profile shapes:osuUserProfileSchema,toOsuUserProfile,OsuUserProfile,OsuUserStatistics. formatModsandformatAccuracyin/format.OSU_SCORES_API_VERSION,OSU_SCORES_LIMIT, and the typesUserScoreType,UserScoresOptions,BeatmapScoresOptions,BeatmapDetail. New public exports, so this release is a minor version.
- Profiles and scores on the client:
osu0.5.0
5 addedAdded
getMatch(match, { beforeCall, maxPages })on the client: a multiplayer match from/api/v2/matches/{id}by id or mp link, as{ match, complete }, or null on 404. It pages back through the events withbeforeuntil the first one (at mostmaxPages, 50 by default), merging events and users from every page. It rejects with code"budget"whenbeforeCallrefuses the first page; a refused later page ends the call withcomplete: false.- Match shapes in
/shapes:osuMatchResponseSchema,osuMatchEventSchema,osuMatchGameSchema,osuMatchScoreSchema,toOsuMatch,toMatchEvent,parseMatchId,MATCH_SCORING_TYPES,MATCH_TEAM_TYPES, and the typesOsuMatch,OsuMatchResponse,MatchEvent,MatchGame,MatchScore,MatchTeam. @haruhimemoe/osu/match, safe in browsers:matchGames,gameStatus,listGames(with warmups),isTeamGame,gameWinner(team vs or head-to-head, by the room's win condition or by score, accuracy or combo),mapWins, andmatchCostswithMATCH_COST_FORMULAS: Bathbot's, osu!plus's, Flashlight's (as osu!plus ships it) and Elitebotix's mixed formula, picked by name.@haruhimemoe/osu/tournament, safe in browsers:buildLazerBracketwrites the osu!lazer tournament client'sbracket.jsonfrom teams, rounds and matches, andLAZER_BRACKET_FILENAME.OSU_MATCH_EVENTS_LIMITandOSU_MATCH_PAGE_LIMIT, and the typesMatchOptionsandMatchLookup. The root entry point re-exports/matchand/tournament. These are new public exports, so this release is a minor version.
time0.1.0
6 addedAdded
@haruhimemoe/time/zone:isTimeZone,assertTimeZone,listTimeZones,offsetMinutes,formatOffset,parseOffset,zoneOffset,offsetZone,toWallTime,fromWallTime(Temporal's gap and overlap rules),weeklyWindowandWEEKDAYS.@haruhimemoe/time/availability: weekly grids in UTC slots withfromZone,toZone,localSlot,createAvailability,unionAvailability,intersectAvailability,countAvailability,resampleAvailability,isFreeAt,isFreeDuring,slotsPerWeek,weekSlotOfandavailabilitySchema. Grids made byfromZonefollow their owner's clock across DST.@haruhimemoe/time/slots:findSlots(ranked match starts with who is free, staff,need,filter,tiebreak, run merging),compareSlotsandMAX_STARTS.@haruhimemoe/time/region:Region,regionSchema,defineRegions,CONTINENTS,CONTINENT_OF_COUNTRY,regionOfCountryandcheckRegionLock.@haruhimemoe/time/ics:buildCalendar,escapeTextandfoldLine(RFC 5545).TimeErrorwith acodefor every refusal.
tourney0.2.0
6 added, 2 changedAdded
- Pick/ban phase order:
rules.phases(PickBanPhaseSchema) for split bans and any other order,pickBanPhases, andphasein the replay state. The 0.1protects/bansrules still work as shorthand and give the same results and refusals. rollFirst: two rolls and the roll winner's choice to the first turns; a tie is refused.- Third place match in single elimination (
thirdPlace: true, round3RD, beforeF). placements: standings from a bracket in both formats, filled in as rounds finish.@haruhimemoe/tourney/mp:fromOsuMatchreads an osu! multiplayer match (from@haruhimemoe/osu) into maps, score, winner and a problem list.MapResultSchematakes an optionalgameId.- Groups:
roundRobin,groupStandingswith points and tiebreaks (StandingsRulesSchema),swissPairingsandswissRounds,snakeGroups,seedsFromGroups,GroupSchema.
Changed
@haruhimemoe/osu(0.5 or newer) is an optional peer dependency, needed only for@haruhimemoe/tourney/mp.RoundSchema.sidecan be"third". Code that switches over every side needs a case for it.
- Pick/ban phase order:
tourney0.1.0
9 addedAdded
- Round ladders for single and double elimination (
buildLadder): RO/QF/SF/F, losers rounds, grand final with optional reset, qualifiers, best-of per round. - Brackets with progression (
createBracket,reportResult,reportForfeit,clearResult,champion): standard seeding, byes, losers bracket drops, grand final reset. - Qualifier ranking by total score or average rank (
rankQualifiers), and standard seed order (seedPositions). - Stored match records (
MatchSchema) with maps, per-player scores, lineups, staff, links and reschedules; score helpers. - Pick/ban replay against a
@haruhimemoe/poolpool (checkPickBans): protects, bans, picks, tiebreaker, whose turn is next. - Teams with rosters, subs and per-map lineups; solo sides.
- Captain drafts: snake or linear order, the clock, picks, deadlines.
- Tournaments with phases and a registration window; registrations with review, role conflicts and caps.
- Scheduling through
@haruhimemoe/time(suggestMatchTimes) and reschedule rules (checkReschedule).
- Round ladders for single and double elimination (
ui0.20.0
3 addedAdded
@haruhimemoe/ui/theme-light.css, a light scheme: import it aftertheme.css. Light surfaces, tinted ink,h1as the dark accent andh2as the pale one, dark code tokens, and a negative contrast lift. Every ink step keeps 4.5:1 on every surface at every hue; the README gives the--h1-lfor hues 30 to 200. For harumin.haruhime.moe's white site.- Lightness variables for every background and content step:
--b1-lto--b6-land--c1-l(--c2-lto--c4-lalready existed). Unset, the dark palette is unchanged. --wordmark-ink:HaruhimeWordmark's white parts read it (default#ffffff), so the light scheme can ink them.
ui0.19.0
3 fixed, 5 changedFixed
CodeBlock(and soMdxPre's fenced blocks) no longer shows a blank line between every line. Each line was adisplay:blockspan with a"\n"text node between them, and inside the<pre>that text node rendered as an extra empty line. The text node is gone; the copy button still copies the source text with its real newlines. The<code>element'stextContentno longer contains newlines.ContentPage's header has a bottom margin (mb-8), so the meta row and "Copy as Markdown" button no longer touch the body.SiteHeader's actions slot is a centered flex row (flex items-center gap-3), soCommandPaletteButtonand an account link like "Sign in" share a center line instead of sitting about 4px apart on their text baselines.
Changed
CopyMarkdownButtonis a compact 28px pill (h-7 px-3, still 44px on a coarse pointer) to sit level with the byline's small text. Buttonsize="sm"is a 28px icon square, too narrow for a label, so this is the secondary button with a smaller height and padding.SiteHeaderundersm, when it has actions: the nav moves to its own full-width row below, so the actions stay on the brand's row instead of wrapping under the nav. The DOM order (brand, nav, actions) and so the tab order is unchanged.CommandPaletteButtonhides itsCtrl K/⌘Khint undersm, leaving the magnifier (and anychildren).ContentPagewith atoc: the toc/body grid gap isgap-6,xl:gap-10(wasgap-8at every width), matchingContentLayout's nav/body gap (gap-6,lg:gap-10). On phones the "On this page" disclosure now sits the same 24px above the body as the "Contents" disclosure.- README and SECURITY.md: the Discord invite is now
https://haruhime.moe/discordand the security contact email isharuhime@haruhime.moe.
next-kit0.11.0
1 addedAdded
legalMarkdownTransform(site)inlegal: amdxToMarkdowntransformsentry that turns each self-closing legal block tag (<LegalContact />,<DataWeKeep />,<Processors />,<YourRights />,<DmcaNotice />,<NoWarranty />,<Changes />and<Changes date="YYYY-MM-DD" />) into Markdown carrying the same words as its React block. Without it, an app that drops the legal blocks straight into itscontent/legal/*.mdxloses that text from its.mdmirrors and llms-full.txt, sincemdxToMarkdownstrips unknown capitalized JSX. The seven blocks' sentences and list items now live in a newlegal/copy.ts, shared byblocks.tsxandlegalMarkdownTransformso the two outputs can't drift apart;blocks.tsx's rendered output is unchanged.
next-kit0.10.0
1 changedChanged
- Breaking.
next-kit check's legal standard requires all five pages of the legal convention:content/legal/{terms,privacy,your-privacy-rights,copyright,disclaimers}.mdx(was terms and privacy only).
- Breaking.
next-kit0.9.0
1 added, 1 changedAdded
legalentry point: the five-page legal convention every service ships (terms, privacy, your-privacy-rights, copyright, disclaimers). ALegalSiteconfig (site name, operator, contact email, effective date, data stores, processors, cookies), seven plain server-safe blocks an app drops into its own legal MDX (LegalContact,DataWeKeep,Processors,YourRights,DmcaNotice,NoWarranty,Changes; no hooks, no@haruhimemoe/ui), andlegalEntries(site, pages?)for the five standardContentEntryrecords so apps stop hand-writing the same titles and descriptions. next-kit's first.tsxentry point.LegalSite.hostingadds an optional line toDmcaNotice;Changestakes an optional per-pagedate.
Changed
- The
@haruhimemoe/uipeer range also allows^0.18.0.
next-kit0.8.0
3 changedChanged
- Breaking. The
@haruhimemoe/uipeer range is now^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0(0.15 to 0.17 not yet published):DeleteAccountFormneeds ui'sConfirmDialog. DeleteAccountFormis a "Delete my account" button that opens a dialog: what goes is its description, the username is typed there, and a refusal or no answer is said in the dialog, which stays open. Once the account is deleted, focus moves to the "Your account is deleted." line.- The account components use ui's text tones (
Text,textClasses) and Button's own width. No change in how they look.
- Breaking. The
next-kit0.7.0
1 addedAdded
vcsentry point:createRevisionStore, document history in MongoDB on top of@haruhimemoe/vcs(a new optional peer). Saves name their base revision and merge onto anything newer; conflicts write nothing and come back for the client to resolve. Also revert, diffs, author renames, history removal and autosave pruning.
pool0.3.0
1 addedAdded
reorderSlot(pool, mod, beatmapId, to): move a map within its own bucket (or the no-slot group) without changing its mod, renumbering the group 1..N. packs.haruhime.moe's map drag and drop uses this.
ui0.18.0
2 added, 1 changedAdded
- Button new
size="sm": a 28px icon-only square (still over the 24px WCAG 2.5.8 target);mdandlgunchanged. ChevronUpIconandChevronDownIcon: plain chevrons as static inline SVGs, matchingDiscordIcon/GitHubIcon's shape (defaultsize-5,aria-hidden, aclassNamereplaces the size).
Changed
SortableMoveButtonsdefault content is now the two chevron icons (aria-hidden; the accessible name stays on the button), sized"sm";upText/downTextstill swap in words and size back to"md"by default. Newsizeprop overrides either default. Neworientationprop ("horizontal"default,"vertical"stacks the buttons in a column). Each button'stitlenow mirrors itsaria-label. Accessible names ("Move {label} up"/"down") are unchanged.
- Button new
ui0.17.0
5 added, 6 changedAdded
@haruhimemoe/ui/remark:remarkFigures(a paragraph holding only an image becomes a<figure>, its title moving into a trailing<figcaption>),remarkEmbeds(a paragraph holding only a bare YouTube/Twitch URL becomes a click-to-load player),parseEmbedUrlandtype EmbedTarget(every YouTube URL form with start times, Twitch videos, clips and channels).articleData,type ArticleDataandtype TocItem: a flat table of contents (h2 to h4), a word count (Intl.Segmenter, Japanese-safe) and a reading time from a document's heading ids.remarkHaruhimeoptionsfigures,embeds(defaulttrue),wordsPerMinute(default 200),mdxExports(defaultfalse: addsexport const toc/readingMinutes/wordsto a compiled MDX module, an author's own export of the same name kept) andcollect(hands the same data to a function, for react-markdown, whose plugin options don't have to cross Turbopack's serializable boundary).haruhimeSanitizeSchemaandtype SanitizeSchema: extends anhast-util-sanitizeschema (usually rehype-sanitize'sdefaultSchema) with the tags and attributes this kit's plugins write;trusted: truedrops theuser-content-id prefix for an app's own READMEs and changelogs.rehypeLocalHrefsandtype HastLike: rewrites an in-page#xlink to#user-content-xwhen the sanitizer prefixed the id but not the href.mdxMarkdownTransforms(plusfigureMarkdown,embedMarkdown,linkCardMarkdown): turns<Figure>,<Embed>and<MdxLinkCard>into their Markdown line for an app's.mdmirror, skipping fenced code.remarkHeadingIdsnow idsh2throughh4(wash2/h3).@haruhimemoe/ui/mdxnew element overrides, throughmdxComponents:h4(same anchor treatment ash2/h3),img(a plain, lazy, async-decoded<img>),input(a GFM task-list checkbox becomes a named, disabled checkbox),details(a native<details>/<summary>pair styled likeDisclosure's toggle, works with no JS),kbd(Kbd's look) anddiv(aremarkEmbedsdata-embeddiv rendersEmbed). The GFM footnote labelh2now renders bare,sr-only, with no anchor.@haruhimemoe/ui/mdxnew article components:Figure(a sizedMdxImgwith an optional caption, credit and eager/priority loading),Steps(a numbered-rail wrapper for an ordered list),Embed(a click-to-load YouTube/Twitch player in a 16:9 box, with a poster image and a plain-link fallback for an unrecognized URL;poster={false}or a local poster replaces YouTube's own thumbnail),MdxLinkCard(one link named by its title, a description and a source line),Schedule(an ordered list ofwhen/label/note rows,whena<time dateTime>or plain text),Glossary/Term(a<dl>of entries anchored at#term-<slug>, with alias anchors, and a dotted-underline link to one),Kbd/kbdClasses(re-exported from the root), andtype MdxArticleModule(the structural shape of a compiled MDX module withmdxExports: true, forawait import(...)),type ArticleDataandtype TocItem(re-exported from./remark).- Root
@haruhimemoe/ui:Toc,type TocProps,type TocItem: an article's table of contents, a sticky column fromxlup plus a phone disclosure, both labelled "On this page", nesting a flat TOC by heading depth.Kbd,type KbdProps,kbdClasses: a keyboard key, shared with the command palette's own shortcut hints.ContentPagegrowsauthors(type ContentAuthor, through the newContentByline),published,lastUpdatednow formatted "Oct 4, 2026" (see Changed),readingMinutes,toc(rendered beside the body fromxlup) andfooter(rendered after the body, outsideProse), plusproseSizepassed straight toProse. Prosenewsizeprop ("base" | "sm", same asContentPage'sproseSize) and new element styles:h4,dl, figures, task lists, footnotes anddetails.
Changed
- h4 headings in existing Markdown and MDX now get an id and a hover-revealed
#anchor, throughremarkHeadingIds' new depth-4 support. - A paragraph holding only an image now renders as a
<figure>; one holding only a bare YouTube/Twitch URL now renders a click-to-load player. Turn either off per document with{ figures: false }/{ embeds: false }passed toremarkHaruhime. Prosenow stylesh4,dl, figures, task lists, footnotes anddetailswherever they appear in its content.mdxComponentshas six more keys (h4,img,input,details,kbd,div). An app's own key of the same name still wins.- The GFM footnotes label
h2loses its#anchor (it was a visible anchor beside invisible text). ContentPageprints ISO dates as "Oct 4, 2026" instead of "2026-10-04" on every docs, guides and legal page.ContentNav's classes move tocontentNavStyles.ts, shared withToc, with no visual change.
ui0.16.0
2 addedAdded
MapCard,MapSetCard,MapGroup,MapCover,MapPreviewButtonandMapCopyScope: map display from plain props.MapCarddraws one map as a row (packs' slot row) or a card (osu-web's beatmapset panel), with no background, the set's cover or a blurred cover behind a b5 overlay, comfortable or compact density, loading/missing/error states, an optional slot pill, status, stars and stats (overridable per key), an optional Copy ID, andleading,preview,badges,detailsandactionsslots. Itsmapprop takes@haruhimemoe/osu'sBeatmapMetaas-is.MapSetCardis a set with its difficulties,MapGroupa bucket with its heading, count, empty slots and one shared Copy ID scope.MapPreviewButtonplays a set's preview clip from b.ppy.sh (one clip per page),stopMapPreview()stops it. AlsomapCoverUrl,MAP_STATUS_LABELSand the typesMapCardProps,MapCardLabels,MapData,MapSetCardProps,MapSetDifficulty,MapGroupProps,MapCoverProps,MapCoverSize,MapPreviewButtonProps.CopyButtonstatusPosition("end"by default,"start"puts the status before the button) andreserveStatus(keeps the status's width so a press never moves the button).
ui0.15.0
1 addedAdded
- Sortable lists:
useSortable,SortableList,SortableHandle,SortableMoveButtons,SortableLayer,moveItem,SORTABLE_ITEMandSORTABLE_CONTAINER, with their types. Drag by mouse, touch or pen (pointer events only, no HTML5 drag and drop) or by keyboard (Space or Enter to pick up, the arrows, Home, End, PageUp and PageDown to move, Space or Enter to drop, Escape to cancel); a click withdetail0 lifts and drops for screen readers in browse mode. Up and Down buttons run on the same path. "between" and "onto" containers, nested containers,canDroprefusals with a reason, asynconMovewith a pending state, auto-scroll near edges, an assertive live region with overridable text, a pointer chip that shows the label and why a drop won't take, and focus that stays on the moved item's handle or button. The app owns the data. No new dependencies.
- Sortable lists:
ui0.14.0
4 added, 1 fixedAdded
Dialog(client): the modal base, a native<dialog>that follows a controlledopen. It focusesinitialFocus, locks the page's scroll (counted, restoring the page's own inline overflow), and hands focus back on close or on unmount to the opener, else toreturnFocus(). Escape and a backdrop press callonDismissinstead of closing; a press only counts as a backdrop press when it starts and ends there; a close the browser makes is reported as"browser". A dialog opened from inside one that closes at the same time returns focus to that one's opener. Fades in on the motion tokens (motion), closes at once. New types:DialogProps,DialogDismissReason.ConfirmDialog(client): a confirm in a modalalertdialogonDialog, opened from its own trigger or a controlledopen/onOpenChange. Focus starts on Cancel; Enter submits; one run at a time withpendingLabel, busy buttons that keep focus, and Escape and backdrop ignored while pending; a throw keeps it open withfailedMessagein an alert;tone="destructive"uses thedangerbutton;typeToConfirmadds a field (no autocomplete, autocapitalize or spellcheck) and keeps Confirmaria-disableduntil the text matches;returnFocusfor a confirm that removed its own row. New types:ConfirmDialogProps,ConfirmTone.dangerButton variant: white on rose-700, rose-800 on hover.InlineConfirm'sconfirmVariantandTypeToConfirm'svarianttake it too.- README: "Which confirm", the rule for no confirm,
InlineConfirm,ConfirmDialog,ConfirmDialogwithtypeToConfirm, andTypeToConfirm.
Fixed
CommandPaletteis built onDialog: closing it puts back the page's own inlineoverflowinstead of clearing it, a text selection dragged from the input onto the backdrop no longer closes it, and a command that opens another dialog sends focus back to whatever opened the palette once that dialog closes.
Older releases are on each repo's own changelog page.