ui changelog
Every ui release, newest first.
Versions
0.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.
0.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.
0.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
0.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.
0.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).
0.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:
0.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.
0.13.0
12 added, 2 changedAdded
SurfaceandsurfaceClasses: the list-item box (Card's b4 and 10px radius at p-3,mdp-4,lgp-5,asdiv, li, section, article, form or p), and its classes for other elements.LinkCardandCardLink: a card that is one link.CardLink's cover fills the card, its name stays its own text, and LinkCard lifts every other control above the cover.mediafor a full-bleed banner. No landmark.CardGrid: a list of cards, one column on phones,columns2 or 3 from sm/lg,gapsm or md, wrapping each child in its own flex<li>so cards in a row match height.StatList: label/value pairs as a<dl>,inline,tilesorgrid.EmptyState: the "nothing here yet" box,dashedorfilled,smormd, with an optional title and action.Progress: a native<progress>named by its label, described by an always-mounted<output>status line.LinkRow: a wrapping row of text links, plain or in a named nav,accentorquiet, with acurrentitem marked by weight and an underline.SectionHeading: the h2 under a page's h1, with an optionaldetail,actionsand a#anchor.PrevNext: previous/next links at the end of a page in a series, each named "Previous: " or "Next: <title>".CodeChip: inline code with a copy button, its nameCopy <code>unlesscopyLabel.CopyField(client): a read-only field in mono with focus-selects-all and a Copy row, described by the field's label.SegmentedControl(client): a two to four way view switch on native radios, drawn like a pill track.
Changed
Cardtakes its classes fromsurfaceClasses. No visual change.ContentIndexlays its cards out onCardGridandsurfaceClasses: the tiles get Card's 10px radius (from 6px) and a 10px gap (from 8px).
0.12.0
11 added, 8 changedAdded
- Motion tokens in
theme.css:duration-short(150ms),duration-medium(250ms),duration-long(400ms),ease-standard,ease-enterandease-exit. Everytransition-*now readsduration-shortandease-standard, the values it had. - One reduced-motion rule: under
prefers-reduced-motion: reduceevery animation and transition finishes in 0.01ms and smooth scrolling turns off, in the kit and in the app's own markup.data-motion="essential"keeps an element's motion. - High contrast: under
prefers-contrast: more,c2,c3,c4andh1get 8 points lighter andh24 points darker, through--contrast-lift(0%opts out). - The
coarse:variant (@media (pointer: coarse)), for a touchscreen as the main pointer. useMotionAllowed()(client): true when the visitor allows motion, false on the server, during hydration and under reduced motion, live. Lifted from haruhime.moe's homepage banner.TextandtextClasses: a line of text in six tones (defaultc2,mutedc3,subtlec4,error,warningamber-300,success) and three sizes, for the error, warning and muted text apps wrote by hand.downloadonButtonLinkandTextLinkrenders a plain<a download>(no prefetch, no client routing), for any value butfalse.hideLabelonTextInput,Select,Textarea,CheckboxandRadioGroup: the label (legend for RadioGroup) is hidden visually but still names the control, with no gap left above it.ModBadgecolor(ModBadgeColor): any of the six bucket colors, the ten @haruhimemoe/pool palette colors orneutral, overriding the color the mod's letters pick. A value outside the list falls back to the bucket.- 44px touch targets on a coarse pointer: chips and choice chips, checkbox and radio rows, Disclosure and HeaderMenu buttons and menu items, Tabs and LinkTabs, palette rows, FilterPanel's toggle, code copy buttons and ContentNav links.
- Under more contrast, cards, chips, neutral badges and the tabs track get a
c4edge, and code blocks, menus, the palette and its key hints ac4border. In forced colors, cards, chips, badges, mod badges and the tabs track keep a 1px border; an unavailable chip is GrayText.
Changed
c2,c3,c4,h1andh2are defined through lightness variables (--c2-l,--c3-land--c4-ljoin--h1-land--h2-l) plus the contrast lift. The default colors are unchanged.Notice, field errors,CharCounter,AsyncButton's failure text and the palette's input error use Text's tones, so they get one step lighter under more contrast.- Button no longer stretches in a flex column or a grid cell:
buttonClasses()addsw-fit(Button, ButtonLink, AsyncButton, CopyButton, Pagination). PassclassName="w-full"where a full-width button is wanted.self-starton buttons is now a no-op. - Buttons at
mdare 44px tall on touch screens (coarse:h-11). Secondary and ghost buttons get ac4edge under more contrast; every button keeps a 1px border in forced colors, and a disabled one uses the system's GrayText there. CopyMarkdownButtonfollows Button's classes again (a test now pins its copy).BrandPage's file links areTextLink download.- Fields are 44px tall with 16px text on touch screens (no iOS zoom), checkbox and radio rows 44px tall, and a field's border is
c4under more contrast. RangeSlider's value boxes also become 44px tall with 16px text on touch screens: they usefieldClasses, the same field look.
- Motion tokens in
0.11.2
1 fixedFixed
CopyMarkdownButtoncopies on Safari and iOS: whereClipboardItemexists, the clipboard write starts inside the click with the fetched Markdown as a promised Blob, instead of after an awaited fetch that loses the user activation. Other browsers keep the fetch-then-writeTextpath.
0.11.1
1 fixedFixed
- BrandPage asset previews use an empty alt, so axe no longer reports image-redundant-alt next to the download link.
0.11.0
4 addedAdded
ContentNavandContentLayout: a content section's side navigation and page grid, a port of bb's docs sidebar and layout.ContentNav(client) takesgroups(ContentNavGroup[], each an optionalheadingandContentNavItem[]) and an index link, marks the current pagearia-current="page", showsnavTitle ?? title(clamped to two lines, the full title on the link'stitleattribute) and an optionalbadge.ContentLayout(server) lays the nav in a 14rem column beside the page fromlgup, takingnavas an already-rendered slot so it stays section-agnostic (docs today, a blog section later). New types:ContentNavItem,ContentNavGroup,ContentSearchItem.searchContent,ContentSearchandContentIndex: a content section's search, a port of bb's docs search.searchContent(items, query)is pure: a blank query returns everything, otherwise every typed word must appear in an item'stitle,navTitle,description,badgeorkeywords, title-prefix matches first.ContentSearch(client) is aTextInputplus a polite live-region count ("12 pages.", "1 match.", "3 matches.", the unfiltered noun set bycountNoun, default["page", "pages"]) overContentIndex.ContentIndex(server) is the bare card grid, for a section with too few entries to search (/legal, a handful of guides).ContentPageandCopyMarkdownButton: a content section's page.ContentPage(server) isPageHeader(title,descriptionas itslead), a meta row withlastUpdated(a<time dateTime>) and, whenmarkdownHrefis set,CopyMarkdownButton, plusactions; optionaljsonLd(oneRecord<string, unknown>, passed toJsonLd); then the body inProse.CopyMarkdownButton(client) fetcheshrefand copies the response body to the clipboard, reporting the result likeCopyButton; any failure (the fetch, the response, or the clipboard) shows the same message and it never throws. Finished class strings (nocx), soContentPageships no tailwind-merge for it.BrandPageandBrandSwatch: a product's/brandpage,<BrandPage {...brandPageData("pools")} />with@haruhimemoe/brand's data (typed structurally; ui doesn't depend on brand). Sections: Name, Logo (each file previewed on a dark or light tile, with adownloadlink), Colors (BrandSwatch, click to copy the hex), Type (fonts, default Nunito), Do's and don'ts, osu!, Family (only withfamilyHref), Contact (mailto:), plusslots.afterLogo,slots.afterColorsandslots.end.BrandSwatch(client) has finished class strings, soBrandPageships no tailwind-merge. New types:BrandPageProps,BrandPageAsset,BrandPageFont,BrandSwatchProps.
0.10.0
1 addedAdded
PlayerCard: osu!-web's user card from plain props (cover, avatar, country and team flags, supporter heart, username linking to the profile, an optional status row). It never fetches; apps pass a snapshot.
0.9.0
4 added, 2 changedAdded
@haruhimemoe/ui/mdx:mdxComponents(thea/h2/h3/pre/table/blockquoteelement overrides for@next/mdxandreact-markdown),CodeBlock(a fenced code block, Shiki-highlighted when registered),Callout(a note/tip/warning aside, also reached through a GitHub-style> [!NOTE]blockquote),parseCodeMetaandslugify.@haruhimemoe/ui/remark:remarkHaruhime(default export, for Turbopack's module-name-onlyremarkPlugins) plus the namedremarkCodeMeta,remarkCalloutsandremarkHeadingIds, andcreateSlugger.@haruhimemoe/ui/shiki: an opt-in side-effect import (shikiis now an optional peer dependency) that registers Shiki's core highlighter forCodeBlock. An app that skips it, or doesn't installshiki, gets plain code blocks instead of a build failure.theme.css:--shiki-foreground,--shiki-backgroundand the--shiki-token-*variables Shiki's CSS-variables theme reads, derived from--huelike the rest of the palette.
Changed
Prosestyles apreat any depth, excludingCodeBlock's own (role="group"), so a plainprenested insideliorblockquotekeeps its fence look whileCodeBlock(asmdxComponents'preoverride renders it) keeps its own.Prosealso stylesblockquote.CodeBlockaccepts every native<div>prop (id,data-*,aria-*, …) exceptcode,lang,titleandhighlight, spread onto its wrapper.
0.8.0
4 added, 2 changedAdded
CommandPalette: a mod+k command palette in a native<dialog>. Fuzzy search over commands with group headings and marked matches, nested pages (Backspace or Escape go back), async providers that search as you type (debounced, aborted when superseded), argument prompts (text, number, choice) before a command runs, a Recent group from localStorage, and a calculator row (2*21→= 42, Enter copies). Command shortcuts (mod+shift+c, chords likeg p) work while the palette is closed.openCommandPalette(page?)opens it from anywhere;CommandPaletteButtonis the header button with the platform hint. Built as a combobox over a listbox, with anh1edge on the active row, a focus cue on the input row, status errors and a polite result count.siteCommands(options): the defaults every tool gets: Go to each nav page, Open each other haruhime tool, Copy page URL, Go back, Scroll to top, Reload, Open on GitHub, Sign in / My account / Sign out, Keyboard shortcuts, Report a bug.fuzzyScoreandevaluate/formatResultare public, so an app can rank its provider rows the same way and reuse the calculator.playground/: a Next.js app in the repo that renders the components fromsrc/(bun run play), andbun run play:axe, which builds it and runs axe-core in Chromium over the palette's states. Not published.
Changed
CheckboxandRadioGroupboxes are 24px (WCAG 2.2 target size; they were the browser's 13px), centered on the first line of their label.DisclosureandHeaderMenubuttons are at least 24px tall.- The consumer check runs axe-core in headless Chromium over the fixture page with color contrast and target-size checks on, at 1280 and 390 wide, after
next build. CI installs Chromium for it.
0.7.0
1 changed, 3 addedChanged
- Accessibility pass (WCAG 2.2 AA). Fields keep the theme's 2px focus outline instead of hiding it behind a 1px border change.
RangeSliderthumbs and chips are 24px, the minimum target size. The default--h1-lis76%(was70%), soh1text onb5clears 4.5:1 at every hue; pink gets a touch lighter at the default hue.StarRatingpicks its text color by contrast (white on the 6.5 to 7 star violet band, where gold was 3.9:1).Table's scrolling wrapper is a focusable<section>named by the caption orscrollLabel.SiteFooterholds its columns in one<nav>(navLabel, default "Footer"), each column a<section>with its title as a heading (headingLevel, default 2), instead of a<nav>per column.ReportDisclosurekeeps its status line mounted from the start and moves focus to it after a send. Field errors arerole="status", notrole="alert"; radios no longer carryaria-invalid(the group describes the error). Text-only nav entries droparia-disabled.BeatmapStatsreads full stat names to screen readers.
Added
CharCounter'sliveprop: announces only the over-limit text.Table'sscrollLabel,SiteFooter'snavLabelandheadingLevel.- README: an "Accessibility" section with the house rules every component follows.
- Accessibility pass (WCAG 2.2 AA). Fields keep the theme's 2px focus outline instead of hiding it behind a 1px border change.
0.6.0
1 addedAdded
SiteFooter'stoolsprop: a "haruhime tools" column linking the other live haruhime.moe tools (packs, pools, bb) and "All tools" on www, with the current tool left out.HARUHIME_TOOLSandharuhimeToolsColumnexport the same data for a custom footer.
0.5.1
1 securitySecurity
JsonLdalso escapes>,&, U+2028 and U+2029 as JSON unicode escapes, not only<. The data parses back unchanged.
0.5.0
4 addedAdded
Tabs: an ARIA tab list for panels on the same page. The chosen tab is the only one in the Tab order; Left and Right (wrapping), Home and End pick and focus a tab.tabIdandtabPanelIdgive the ids that tie a tab to its panel. Moved from bb.haruhime.moe.CharCounter: "1,234 / 60,000 characters", bold rose with how many to cut once over the limit. The caller counts. Moved from bb.haruhime.moe.VisibilitySelect: private, unlisted or public with a line each saying who sees it, as radios or a select, with the words overridable. AlsoVISIBILITIES,VISIBILITY_TEXTand theVisibilityandVisibilityTexttypes. It replaces the pools pool editor's radios and bb's template selects.ReportDisclosure: a report reason in a disclosure, sent throughonSubmit, which returns aReportResult; done replaces the form with a status line, an error stays on the field for a retry. Moved from bb.haruhime.moe (ReportForm).
0.4.0
17 added, 1 changed, 6 fixed, 2 securityAdded
SiteFootertakesdiscordLabel(default "Discord"), the Discord link's accessible name, asgithubLabeldoes for GitHub.HeadingLevel(2 | 3 | 4 | 5 | 6), the type ofheadingLevelonCardandFilterPanel.TextLinkandlinkClasses: a text link in two looks,accent(h1, underlined, for running text) andplain(boldc1, underlined on hover, for names in lists). LikeButtonLink, it isnext/linkinside the app and a plain<a>off-site.Badge: a small pill for a status or tag, inneutral,accent,warningormuted(an outlined "beta" tag).- Table primitives:
Table(a sideways-scrolling wrapper, and a caption that can be for screen readers only),THead,TBody,Th(scope="col"by default, bold forscope="row") andTd, withnumericfortabular-numscells. They carry the look the apps' tables share. cx, the class merger the components use (tailwind-merge), and itsClassValuetype.InlineConfirm: a two-step confirm in the page. Opening moves focus to cancel; cancel, Escape or a confirm that resolves puts it back on the trigger, so focus never falls to the page body. The open confirm is a group named by its question. A pending confirm keeps focus and ignores presses; a failed one stays open.AsyncButton: runs an async action and announces its result (or a failure, in rose) in an<output>, one run at a time, with an optional pending label.Disclosure: a button witharia-expandedandaria-controlsthat shows and hides a panel, which stays in the page while hidden. Uncontrolled, or controlled withopenandonOpenChange.RadioGroup: a native radio fieldset on theCheckboxlook, with a legend, per-option hints, and a group hint and error. Controlled or uncontrolled.TypeToConfirm: a form whose submit stays off until a name is typed exactly, for actions that can't be undone.ChoiceChips: single-select chips as native radios (arrow keys move and pick), withChip's look andChipGroup'slabelandhideLabel.ChipandChipOptiontakeunavailableReason: the chip is blocked but stays focusable (aria-disabled), with the reason as its description and title.LinkTabs: a named nav of pill links witharia-current="page"on the current one.HeaderMenu: the header's account disclosure (button, links, extra controls), closed by Escape (focus back on the button), a click outside, a link, or focus leaving it.- osu! display pieces:
StarRating(a pill on osu!'s star-rating spectrum that reads "5.23 stars"),BeatmapStats(CS, AR, OD, HP, BPM and length from plain numbers) andModBadge(a slot pill colored by its mod bucket). Paginationbutton mode:onPageChangeinstead ofhrefFor, for results fetched in place.pageCountcan benull(the status reads "Page X", andhasNextsays whether Next works). The ends stay in place witharia-disabled, and the status is a polite live region.
Changed
CardtakesheadingLevel5and6too, likeFilterPanel.
Fixed
- Hrefs that browsers read as off-site now count as external:
/\host,\\host, and a URL behind leading spaces or control characters. Before,ButtonLinksent them throughnext/linkwithout itsrel="noreferrer"default, and a nav hydrated its client list for them. PaginationnormalizespageandpageCount:NaNreads as page 1 (aNaNcount as one page), a page past either end is pulled back inside, and fractions are dropped. Before,page={NaN}showed "Page NaN of 5" with no links, and page 99 of 5 linked to page 98.CopyButtonreports only the latest press. A slow earlier copy that failed after a later one worked no longer replaces "Copied." with the failure.SiteHeaderandNavLinkskey entries by label and href, asSiteFooterdoes, so two entries with the same href no longer share a React key.Checkboxkeeps anaria-labelledbyyou pass, after its own label. Before, it was dropped.RangeSlidercounts astepof0, below0or not finite as1, and swapsminandmaxgiven the wrong way round. Before,step={0}sentNaNtoonChangeon every key press or drag.
Security
- The release workflow pins every GitHub Action to a full commit SHA and npm to an exact version, since that job holds the npm publish token. It also runs the coverage floor and the consumer check before publishing, as CI does. Dependabot (the
bunandgithub-actionsecosystems) keeps the pins and the exact dependency versions current. - Report vulnerabilities through GitHub's private vulnerability reporting first, or by email (SECURITY.md).
0.3.0
2 addedAdded
DiscordIcon: the Discord logo as an inline SVG in the current text color, sized and hidden from screen readers likeGitHubIcon. The path is Simple Icons'discord.svg(CC0 1.0). Discord's brand guidelines ask for the logo in color, black or white, so set one of those as the text color.SiteFootertakesdiscordHref. When set, a Discord icon link named "Discord" sits before the GitHub icon in the last row, at the same size. The logo stays white and dims on hover instead of changing color, as Discord's guidelines ask. WithoutdiscordHrefthe footer is unchanged.
0.2.0
1 added, 2 fixedAdded
CardtakesheadingLevel(2,3or4, default2), likeFilterPanel, for a card that sits under another heading.
Fixed
SiteHeaderandNavLinksno longer send tailwind-merge to the browser on every page.NavLinksis now a Server Component: it merges its classes on the server and hands finished class strings to a small client list that only reads the path foraria-current. In a Next.js 16 build, the nav's client chunk drops from about 12 KB to 4 KB gzipped. When no link can be the current page (all external or text-only), the list renders on the server alone and nothing in the nav hydrates.Proseno longer puts theh2orh3top margin above a heading that opens the block: its first child getsmt-0. The README shows the one-class pattern for content wrapped in<section>s.
0.1.0
8 addedAdded
@haruhimemoe/ui/theme.css: the osu!-web palette as Tailwind 4 colors (b1tob6,c1toc4,h1,h2) driven by one--hue(default 333),--h1-land--h2-lto moveh1andh2lightness at hues where the defaults fall under 4.5:1 contrast, Nunito asfont-sansthrough--font-nunito, a visible focus ring, and an@sourceline so an app's Tailwind generates the components' classes.- Basics:
Button,ButtonLink(next/link, or a plain<a>for external URLs),buttonClasses,Card,PageHeader,Notice(info, warning, error; optionally live) andProse. - Forms:
TextInput,Textarea,SelectandCheckbox, each with a label, hint and error wired througharia-describedbyandaria-invalid, plusfieldClassesfor bare controls. - Actions:
CopyButton(a client component that reports "Copied." or a failure in an<output>),PaginationandJsonLd. - Icons:
GitHubIcon,HaruhimeWordmarkandHaruhimeWordmarkLink. - Filters:
ChipandChipGroup(toggle pills witharia-pressed),RangeSlider(two thumbs with editable ends, keyboard support, an open "+" top end, comma decimals, and aninputModethat switches to the text keyboard with a customparse),FilterRowandFilterPanel(the osu! beatmap listing layout, collapsible on phones, with a live result count and "Clear filters"). classNameon every component, and the extras passed tobuttonClassesandfieldClasses, merge with tailwind-merge: a caller's class replaces a built-in one that sets the same property (fieldClasses("w-auto")dropsw-full).- Shell:
SiteHeader(brand slot, nav links as data witharia-current, actions slot),NavLinks,SiteFooter(link columns as data, fine print, the haruhime.moe wordmark and a GitHub link) andPageShell(skip link, header, main, footer).