@haruhimemoe/ui 0.1.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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +561 -0
- package/dist/components/actions/CopyButton.d.ts +33 -0
- package/dist/components/actions/CopyButton.js +40 -0
- package/dist/components/actions/JsonLd.d.ts +20 -0
- package/dist/components/actions/JsonLd.js +17 -0
- package/dist/components/actions/Pagination.d.ts +33 -0
- package/dist/components/actions/Pagination.js +27 -0
- package/dist/components/actions/PaginationStatus.d.ts +19 -0
- package/dist/components/actions/PaginationStatus.js +44 -0
- package/dist/components/basics/Button.d.ts +20 -0
- package/dist/components/basics/Button.js +10 -0
- package/dist/components/basics/ButtonLink.d.ts +23 -0
- package/dist/components/basics/ButtonLink.js +26 -0
- package/dist/components/basics/Card.d.ts +19 -0
- package/dist/components/basics/Card.js +20 -0
- package/dist/components/basics/Notice.d.ts +30 -0
- package/dist/components/basics/Notice.js +22 -0
- package/dist/components/basics/PageHeader.d.ts +25 -0
- package/dist/components/basics/PageHeader.js +10 -0
- package/dist/components/basics/Prose.d.ts +18 -0
- package/dist/components/basics/Prose.js +11 -0
- package/dist/components/basics/buttonStyles.d.ts +23 -0
- package/dist/components/basics/buttonStyles.js +28 -0
- package/dist/components/filters/Chip.d.ts +25 -0
- package/dist/components/filters/Chip.js +29 -0
- package/dist/components/filters/ChipGroup.d.ts +39 -0
- package/dist/components/filters/ChipGroup.js +44 -0
- package/dist/components/filters/FilterPanel.d.ts +36 -0
- package/dist/components/filters/FilterPanel.js +45 -0
- package/dist/components/filters/FilterRow.d.ts +22 -0
- package/dist/components/filters/FilterRow.js +22 -0
- package/dist/components/filters/RangeSlider.d.ts +62 -0
- package/dist/components/filters/RangeSlider.js +161 -0
- package/dist/components/forms/Checkbox.d.ts +20 -0
- package/dist/components/forms/Checkbox.js +13 -0
- package/dist/components/forms/FieldFrame.d.ts +66 -0
- package/dist/components/forms/FieldFrame.js +44 -0
- package/dist/components/forms/Select.d.ts +19 -0
- package/dist/components/forms/Select.js +12 -0
- package/dist/components/forms/TextInput.d.ts +19 -0
- package/dist/components/forms/TextInput.js +12 -0
- package/dist/components/forms/Textarea.d.ts +19 -0
- package/dist/components/forms/Textarea.js +13 -0
- package/dist/components/forms/fieldStyles.d.ts +17 -0
- package/dist/components/forms/fieldStyles.js +19 -0
- package/dist/components/icons/GitHubIcon.d.ts +18 -0
- package/dist/components/icons/GitHubIcon.js +10 -0
- package/dist/components/icons/HaruhimeWordmark.d.ts +23 -0
- package/dist/components/icons/HaruhimeWordmark.js +16 -0
- package/dist/components/icons/HaruhimeWordmarkLink.d.ts +21 -0
- package/dist/components/icons/HaruhimeWordmarkLink.js +12 -0
- package/dist/components/shell/AutoLink.d.ts +19 -0
- package/dist/components/shell/AutoLink.js +23 -0
- package/dist/components/shell/NavLinks.d.ts +24 -0
- package/dist/components/shell/NavLinks.js +40 -0
- package/dist/components/shell/PageShell.d.ts +29 -0
- package/dist/components/shell/PageShell.js +11 -0
- package/dist/components/shell/SiteFooter.d.ts +40 -0
- package/dist/components/shell/SiteFooter.js +21 -0
- package/dist/components/shell/SiteHeader.d.ts +32 -0
- package/dist/components/shell/SiteHeader.js +16 -0
- package/dist/components/shell/links.d.ts +25 -0
- package/dist/components/shell/links.js +32 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +41 -0
- package/dist/theme.css +54 -0
- package/dist/utils/cx.d.ts +18 -0
- package/dist/utils/cx.js +17 -0
- package/dist/utils/href.d.ts +15 -0
- package/dist/utils/href.js +17 -0
- package/package.json +90 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { FieldFrame, fieldDescribedBy } from "./FieldFrame.js";
|
|
4
|
+
import { fieldClasses } from "./fieldStyles.js";
|
|
5
|
+
/**
|
|
6
|
+
* @function Textarea
|
|
7
|
+
* @param props {TextareaProps} native textarea props, plus id, label, hint, error and
|
|
8
|
+
* wrapperClassName; `className` goes on the `<textarea>`
|
|
9
|
+
* @returns {JSX.Element} a label, the textarea, and the hint and error when given
|
|
10
|
+
*/
|
|
11
|
+
export function Textarea({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }) {
|
|
12
|
+
return (_jsx(FieldFrame, { id: id, label: label, hint: hint, error: error, className: wrapperClassName, children: _jsx("textarea", { ...props, id: id, "aria-describedby": fieldDescribedBy(id, hint, error, describedBy), "aria-invalid": error ? true : invalid, className: fieldClasses(cx("min-h-24 resize-y", className)) }) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/fieldStyles.ts
|
|
3
|
+
* @desc Shared classes for inputs, selects, and textareas (osu!-web dark fields). Focus shows as
|
|
4
|
+
* an h1 border. On an invalid field it also gets an h1 ring, since rose to pink alone is
|
|
5
|
+
* hard to see. `outline-hidden` (not `outline-none`) leaves a transparent outline that
|
|
6
|
+
* forced-colors mode paints, so focus shows there too.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Wed Sep 23, 2026
|
|
9
|
+
* @modified Wed Sep 23, 2026
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @function fieldClasses
|
|
13
|
+
* @param className {string} extra classes, appended last. One that sets the same property as a
|
|
14
|
+
* built-in class replaces it: `fieldClasses("w-auto")` drops `w-full`.
|
|
15
|
+
* @returns {string} class string for a form control
|
|
16
|
+
*/
|
|
17
|
+
export declare const fieldClasses: (className?: string | undefined) => string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/fieldStyles.ts
|
|
3
|
+
* @desc Shared classes for inputs, selects, and textareas (osu!-web dark fields). Focus shows as
|
|
4
|
+
* an h1 border. On an invalid field it also gets an h1 ring, since rose to pink alone is
|
|
5
|
+
* hard to see. `outline-hidden` (not `outline-none`) leaves a transparent outline that
|
|
6
|
+
* forced-colors mode paints, so focus shows there too.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Wed Sep 23, 2026
|
|
9
|
+
* @modified Wed Sep 23, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { cx } from "../../utils/cx.js";
|
|
12
|
+
const FIELD = "w-full rounded-md border border-b3 bg-b6 px-3 py-2 text-c1 text-sm placeholder:text-c4 focus-visible:border-h1 focus-visible:outline-hidden disabled:opacity-50 aria-invalid:border-rose-400 aria-invalid:focus-visible:border-h1 aria-invalid:focus-visible:ring-1 aria-invalid:focus-visible:ring-h1";
|
|
13
|
+
/**
|
|
14
|
+
* @function fieldClasses
|
|
15
|
+
* @param className {string} extra classes, appended last. One that sets the same property as a
|
|
16
|
+
* built-in class replaces it: `fieldClasses("w-auto")` drops `w-full`.
|
|
17
|
+
* @returns {string} class string for a form control
|
|
18
|
+
*/
|
|
19
|
+
export const fieldClasses = (className) => cx(FIELD, className);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/icons/GitHubIcon.tsx
|
|
3
|
+
* @desc The GitHub mark as a static inline SVG (no icon library, no external request). Always
|
|
4
|
+
* decorative: label the link or button around it.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ComponentProps } from "react";
|
|
10
|
+
/** Every native `<svg>` prop except children and viewBox. */
|
|
11
|
+
export type GitHubIconProps = Omit<ComponentProps<"svg">, "children" | "viewBox">;
|
|
12
|
+
/**
|
|
13
|
+
* @function GitHubIcon
|
|
14
|
+
* @param props {GitHubIconProps} native svg props; `className` replaces the default size
|
|
15
|
+
* ("size-5")
|
|
16
|
+
* @returns {JSX.Element} the GitHub mark in the current text color, hidden from assistive tech
|
|
17
|
+
*/
|
|
18
|
+
export declare function GitHubIcon({ className, ...props }: GitHubIconProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @function GitHubIcon
|
|
4
|
+
* @param props {GitHubIconProps} native svg props; `className` replaces the default size
|
|
5
|
+
* ("size-5")
|
|
6
|
+
* @returns {JSX.Element} the GitHub mark in the current text color, hidden from assistive tech
|
|
7
|
+
*/
|
|
8
|
+
export function GitHubIcon({ className = "size-5", ...props }) {
|
|
9
|
+
return (_jsx("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", className: className, fill: "currentColor", ...props, children: _jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z" }) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/icons/HaruhimeWordmark.tsx
|
|
3
|
+
* @desc The haruhime.moe parent brand wordmark as an inline SVG. The paths and colors are copied
|
|
4
|
+
* byte for byte from the brand asset (haruhime-wordmark.svg, the one for dark backgrounds).
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ComponentProps } from "react";
|
|
10
|
+
/** Every native `<svg>` prop except children and viewBox, plus a title and a decorative flag. */
|
|
11
|
+
export type HaruhimeWordmarkProps = Omit<ComponentProps<"svg">, "children" | "viewBox"> & {
|
|
12
|
+
/** Accessible name, rendered as the SVG `<title>`. Defaults to "haruhime.moe". */
|
|
13
|
+
title?: string | undefined;
|
|
14
|
+
/** Hide the wordmark from assistive tech (no role, no title), e.g. inside a labelled link. */
|
|
15
|
+
decorative?: boolean | undefined;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* @function HaruhimeWordmark
|
|
19
|
+
* @param props {HaruhimeWordmarkProps} native svg props; `className` replaces the default size
|
|
20
|
+
* ("h-6 w-auto"), `title` names the image, `decorative` hides it from assistive tech
|
|
21
|
+
* @returns {JSX.Element} the wordmark as an inline `<svg>`, sized by its viewBox aspect ratio
|
|
22
|
+
*/
|
|
23
|
+
export declare function HaruhimeWordmark({ title, decorative, className, ...props }: HaruhimeWordmarkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The brand asset's paths and colors, unchanged. The colors are the brand's own, so they
|
|
3
|
+
// stay fixed when an app changes --hue.
|
|
4
|
+
const PATHS = (_jsxs(_Fragment, { children: [_jsx("path", { fill: "#ffffff", d: "M134 1009Q97 1009 77.5 989Q58 969 58 932L58 363Q58 325 77.5 305.5Q97 286 134 286Q170 286 189.5 305.5Q209 325 209 363L209 601L195 601Q218 552 263.5 525.5Q309 499 367 499Q426 499 464 521.5Q502 544 521 589.5Q540 635 540 705L540 932Q540 969 520.5 989Q501 1009 464 1009Q428 1009 408.5 989Q389 969 389 932L389 712Q389 661 370.5 638.5Q352 616 314 616Q266 616 237.5 646Q209 676 209 726L209 932Q209 1009 134 1009ZM819 1011Q765 1011 722 990Q679 969 655 933Q631 897 631 852Q631 798 659 766.5Q687 735 750 721.5Q813 708 917 708L970 708L970 786L918 786Q867 786 835.5 791.5Q804 797 790.5 810Q777 823 777 846Q777 874 796.5 892Q816 910 853 910Q882 910 904.5 896.5Q927 883 940 859.5Q953 836 953 806L953 691Q953 647 933 628.5Q913 610 865 610Q838 610 806.5 616.5Q775 623 737 638Q715 648 698 642.5Q681 637 672 622.5Q663 608 663 590.5Q663 573 673 556.5Q683 540 706 532Q753 513 794.5 506Q836 499 871 499Q948 499 997.5 521.5Q1047 544 1072 590.5Q1097 637 1097 710L1097 932Q1097 969 1079 989Q1061 1009 1027 1009Q993 1009 974.5 989Q956 969 956 932L956 895L963 901Q957 935 937.5 959.5Q918 984 888 997.5Q858 1011 819 1011ZM1288 1009Q1250 1009 1230 989Q1210 969 1210 932L1210 577Q1210 540 1229.5 520.5Q1249 501 1284 501Q1320 501 1339 520.5Q1358 540 1358 577L1358 629L1348 629Q1360 569 1401 536Q1442 503 1507 499Q1532 497 1545.5 511.5Q1559 526 1560 559Q1562 590 1547.5 608Q1533 626 1499 630L1476 632Q1419 637 1391.5 665Q1364 693 1364 746L1364 932Q1364 969 1344.5 989Q1325 1009 1288 1009ZM1803 1011Q1741 1011 1700 988Q1659 965 1639.5 918.5Q1620 872 1620 803L1620 577Q1620 539 1639.5 520Q1659 501 1695 501Q1731 501 1751 520Q1771 539 1771 577L1771 807Q1771 851 1789 872.5Q1807 894 1846 894Q1889 894 1916.5 863.5Q1944 833 1944 783L1944 577Q1944 539 1963.5 520Q1983 501 2019 501Q2056 501 2075.5 520Q2095 539 2095 577L2095 932Q2095 1009 2022 1009Q1986 1009 1967 989Q1948 969 1948 932L1948 865L1963 904Q1941 955 1900 983Q1859 1011 1803 1011ZM2288 1009Q2251 1009 2231.5 989Q2212 969 2212 932L2212 363Q2212 325 2231.5 305.5Q2251 286 2288 286Q2324 286 2343.5 305.5Q2363 325 2363 363L2363 601L2349 601Q2372 552 2417.5 525.5Q2463 499 2521 499Q2580 499 2618 521.5Q2656 544 2675 589.5Q2694 635 2694 705L2694 932Q2694 969 2674.5 989Q2655 1009 2618 1009Q2582 1009 2562.5 989Q2543 969 2543 932L2543 712Q2543 661 2524.5 638.5Q2506 616 2468 616Q2420 616 2391.5 646Q2363 676 2363 726L2363 932Q2363 1009 2288 1009ZM2883 1008Q2846 1008 2826.5 986.5Q2807 965 2807 926L2807 584Q2807 544 2826.5 522.5Q2846 501 2883 501Q2919 501 2938.5 522.5Q2958 544 2958 584L2958 926Q2958 965 2939 986.5Q2920 1008 2883 1008ZM2883 419Q2841 419 2818.5 399.5Q2796 380 2796 344Q2796 307 2818.5 287.5Q2841 268 2883 268Q2925 268 2947 287.5Q2969 307 2969 344Q2969 380 2947 399.5Q2925 419 2883 419ZM3153 1009Q3116 1009 3096.5 989Q3077 969 3077 932L3077 577Q3077 540 3096.5 520.5Q3116 501 3151 501Q3187 501 3206 520.5Q3225 540 3225 577L3225 641L3214 601Q3234 555 3276 527Q3318 499 3375 499Q3432 499 3470 526Q3508 553 3524 609L3510 609Q3532 558 3577.5 528.5Q3623 499 3682 499Q3739 499 3775.5 521.5Q3812 544 3830 589.5Q3848 635 3848 705L3848 932Q3848 969 3828.5 989Q3809 1009 3773 1009Q3736 1009 3716.5 989Q3697 969 3697 932L3697 710Q3697 661 3681.5 638.5Q3666 616 3629 616Q3586 616 3562 646Q3538 676 3538 731L3538 932Q3538 969 3518.5 989Q3499 1009 3463 1009Q3427 1009 3407 989Q3387 969 3387 932L3387 710Q3387 661 3371.5 638.5Q3356 616 3319 616Q3276 616 3252 646Q3228 676 3228 731L3228 932Q3228 1009 3153 1009ZM4219 1011Q4132 1011 4069.5 979.5Q4007 948 3973.5 890.5Q3940 833 3940 755Q3940 679 3972 621.5Q4004 564 4061.5 531.5Q4119 499 4192 499Q4245 499 4288 516.5Q4331 534 4362 566.5Q4393 599 4409 645.5Q4425 692 4425 749Q4425 768 4413 777.5Q4401 787 4378 787L4064 787L4064 709L4316 709L4300 723Q4300 682 4288 654.5Q4276 627 4253.5 613Q4231 599 4198 599Q4161 599 4135 616Q4109 633 4095 665Q4081 697 4081 742L4081 750Q4081 826 4116.5 862Q4152 898 4222 898Q4246 898 4277 892Q4308 886 4335 873Q4358 862 4376 865.5Q4394 869 4404 882Q4414 895 4415.5 912Q4417 929 4408 945.5Q4399 962 4378 973Q4344 992 4301.5 1001.5Q4259 1011 4219 1011Z" }), _jsx("path", { fill: "#ff66ab", d: "M3362 1364Q3342 1364 3330.25 1352Q3318.5 1340 3318.5 1321Q3318.5 1302.5 3330.25 1290.75Q3342 1279 3362 1279Q3382 1279 3393.25 1290.75Q3404.5 1302.5 3404.5 1321Q3404.5 1340 3393.25 1352Q3382 1364 3362 1364Z" }), _jsx("path", { fill: "#ffffff", d: "M3494.5 1366Q3476 1366 3466.25 1356Q3456.5 1346 3456.5 1327.5L3456.5 1150Q3456.5 1131.5 3466.25 1121.75Q3476 1112 3493.5 1112Q3511.5 1112 3521 1121.75Q3530.5 1131.5 3530.5 1150L3530.5 1182L3525 1162Q3535 1139 3556 1125Q3577 1111 3605.5 1111Q3634 1111 3653 1124.5Q3672 1138 3680 1166L3673 1166Q3684 1140.5 3706.75 1125.75Q3729.5 1111 3759 1111Q3787.5 1111 3805.75 1122.25Q3824 1133.5 3833 1156.25Q3842 1179 3842 1214L3842 1327.5Q3842 1346 3832.25 1356Q3822.5 1366 3804.5 1366Q3786 1366 3776.25 1356Q3766.5 1346 3766.5 1327.5L3766.5 1216.5Q3766.5 1192 3758.75 1180.75Q3751 1169.5 3732.5 1169.5Q3711 1169.5 3699 1184.5Q3687 1199.5 3687 1227L3687 1327.5Q3687 1346 3677.25 1356Q3667.5 1366 3649.5 1366Q3631.5 1366 3621.5 1356Q3611.5 1346 3611.5 1327.5L3611.5 1216.5Q3611.5 1192 3603.75 1180.75Q3596 1169.5 3577.5 1169.5Q3556 1169.5 3544 1184.5Q3532 1199.5 3532 1227L3532 1327.5Q3532 1366 3494.5 1366ZM4018 1367Q3978.5 1367 3949.25 1351.5Q3920 1336 3904 1307Q3888 1278 3888 1238.5Q3888 1209 3897 1185.5Q3906 1162 3923.25 1145.25Q3940.5 1128.5 3964.5 1119.75Q3988.5 1111 4018 1111Q4057.5 1111 4086.75 1126.5Q4116 1142 4132.25 1170.5Q4148.5 1199 4148.5 1238.5Q4148.5 1268.5 4139.25 1292Q4130 1315.5 4113 1332.5Q4096 1349.5 4071.75 1358.25Q4047.5 1367 4018 1367ZM4018 1310.5Q4034.5 1310.5 4046.5 1302.5Q4058.5 1294.5 4065.5 1278.75Q4072.5 1263 4072.5 1238.5Q4072.5 1202 4057.5 1184.75Q4042.5 1167.5 4018 1167.5Q4002 1167.5 3989.75 1175Q3977.5 1182.5 3970.75 1198.25Q3964 1214 3964 1238.5Q3964 1275 3979 1292.75Q3994 1310.5 4018 1310.5ZM4322 1367Q4278.5 1367 4247.25 1351.25Q4216 1335.5 4199.25 1306.75Q4182.5 1278 4182.5 1239Q4182.5 1201 4198.5 1172.25Q4214.5 1143.5 4243.25 1127.25Q4272 1111 4308.5 1111Q4335 1111 4356.5 1119.75Q4378 1128.5 4393.5 1144.75Q4409 1161 4417 1184.25Q4425 1207.5 4425 1236Q4425 1245.5 4419 1250.25Q4413 1255 4401.5 1255L4244.5 1255L4244.5 1216L4370.5 1216L4362.5 1223Q4362.5 1202.5 4356.5 1188.75Q4350.5 1175 4339.25 1168Q4328 1161 4311.5 1161Q4293 1161 4280 1169.5Q4267 1178 4260 1194Q4253 1210 4253 1232.5L4253 1236.5Q4253 1274.5 4270.75 1292.5Q4288.5 1310.5 4323.5 1310.5Q4335.5 1310.5 4351 1307.5Q4366.5 1304.5 4380 1298Q4391.5 1292.5 4400.5 1294.25Q4409.5 1296 4414.5 1302.5Q4419.5 1309 4420.25 1317.5Q4421 1326 4416.5 1334.25Q4412 1342.5 4401.5 1348Q4384.5 1357.5 4363.25 1362.25Q4342 1367 4322 1367Z" })] }));
|
|
5
|
+
/**
|
|
6
|
+
* @function HaruhimeWordmark
|
|
7
|
+
* @param props {HaruhimeWordmarkProps} native svg props; `className` replaces the default size
|
|
8
|
+
* ("h-6 w-auto"), `title` names the image, `decorative` hides it from assistive tech
|
|
9
|
+
* @returns {JSX.Element} the wordmark as an inline `<svg>`, sized by its viewBox aspect ratio
|
|
10
|
+
*/
|
|
11
|
+
export function HaruhimeWordmark({ title = "haruhime.moe", decorative = false, className = "h-6 w-auto", ...props }) {
|
|
12
|
+
if (decorative) {
|
|
13
|
+
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "18 228 4447 1179", "aria-hidden": "true", className: className, ...props, children: PATHS }));
|
|
14
|
+
}
|
|
15
|
+
return (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "18 228 4447 1179", role: "img", className: className, ...props, children: [_jsx("title", { children: title }), PATHS] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/icons/HaruhimeWordmarkLink.tsx
|
|
3
|
+
* @desc The parent brand wordmark as a plain external link to haruhime.moe (a real `<a>`, not
|
|
4
|
+
* next/link, since it leaves the app), dimmed until hovered like the packs footer.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ComponentProps } from "react";
|
|
10
|
+
/** Every native `<a>` prop except children, plus classes for the wordmark inside. */
|
|
11
|
+
export type HaruhimeWordmarkLinkProps = Omit<ComponentProps<"a">, "children"> & {
|
|
12
|
+
/** Replaces the wordmark's default size ("h-6 w-auto"). */
|
|
13
|
+
wordmarkClassName?: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* @function HaruhimeWordmarkLink
|
|
17
|
+
* @param props {HaruhimeWordmarkLinkProps} native anchor props (`href` defaults to
|
|
18
|
+
* https://www.haruhime.moe, `aria-label` to "haruhime.moe"), plus the wordmark's classes
|
|
19
|
+
* @returns {JSX.Element} an `<a>` wrapping the decorative wordmark
|
|
20
|
+
*/
|
|
21
|
+
export declare function HaruhimeWordmarkLink({ href, className, wordmarkClassName, ...props }: HaruhimeWordmarkLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { HaruhimeWordmark } from "./HaruhimeWordmark.js";
|
|
4
|
+
/**
|
|
5
|
+
* @function HaruhimeWordmarkLink
|
|
6
|
+
* @param props {HaruhimeWordmarkLinkProps} native anchor props (`href` defaults to
|
|
7
|
+
* https://www.haruhime.moe, `aria-label` to "haruhime.moe"), plus the wordmark's classes
|
|
8
|
+
* @returns {JSX.Element} an `<a>` wrapping the decorative wordmark
|
|
9
|
+
*/
|
|
10
|
+
export function HaruhimeWordmarkLink({ href = "https://www.haruhime.moe", className, wordmarkClassName, ...props }) {
|
|
11
|
+
return (_jsx("a", { href: href, "aria-label": "haruhime.moe", className: cx("opacity-80 transition-opacity hover:opacity-100", className), ...props, children: _jsx(HaruhimeWordmark, { decorative: true, className: wordmarkClassName }) }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/shell/AutoLink.tsx
|
|
3
|
+
* @desc A link that picks its element from the href: `next/link` for paths inside the app, a
|
|
4
|
+
* plain `<a>` for anything with a scheme (https:, mailto:) or a protocol-relative URL.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ComponentProps } from "react";
|
|
10
|
+
/** Every native `<a>` prop (including `ref`), with `href` required and a plain string. */
|
|
11
|
+
export type AutoLinkProps = Omit<ComponentProps<"a">, "href"> & {
|
|
12
|
+
href: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* @function AutoLink
|
|
16
|
+
* @param props {AutoLinkProps} native anchor props with a string href
|
|
17
|
+
* @returns {JSX.Element} a `next/link` for internal paths, a plain `<a>` for external URLs
|
|
18
|
+
*/
|
|
19
|
+
export declare function AutoLink({ href, ...props }: AutoLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @file src/components/shell/AutoLink.tsx
|
|
4
|
+
* @desc A link that picks its element from the href: `next/link` for paths inside the app, a
|
|
5
|
+
* plain `<a>` for anything with a scheme (https:, mailto:) or a protocol-relative URL.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Wed Sep 23, 2026
|
|
8
|
+
* @modified Wed Sep 23, 2026
|
|
9
|
+
*/
|
|
10
|
+
import Link from "next/link.js";
|
|
11
|
+
import { isExternalHref } from "./links.js";
|
|
12
|
+
/**
|
|
13
|
+
* @function AutoLink
|
|
14
|
+
* @param props {AutoLinkProps} native anchor props with a string href
|
|
15
|
+
* @returns {JSX.Element} a `next/link` for internal paths, a plain `<a>` for external URLs
|
|
16
|
+
*/
|
|
17
|
+
export function AutoLink({ href, ...props }) {
|
|
18
|
+
if (isExternalHref(href))
|
|
19
|
+
return _jsx("a", { href: href, ...props });
|
|
20
|
+
// Link takes every anchor prop, but types a few handlers (onClick, onMouseEnter) without
|
|
21
|
+
// `| undefined`, which exactOptionalPropertyTypes rejects for a spread of optional props.
|
|
22
|
+
return _jsx(Link, { href: href, ...props });
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/shell/NavLinks.tsx
|
|
3
|
+
* @desc The header's nav list. A client component only so it can read the current path and set
|
|
4
|
+
* aria-current on the matching link; SiteHeader around it stays a server component.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ComponentProps } from "react";
|
|
10
|
+
import { type SiteLinkItem } from "./links.js";
|
|
11
|
+
/** Where the nav sits in the header: after the brand ("start") or centered in the free space. */
|
|
12
|
+
export type SiteNavAlign = "start" | "center";
|
|
13
|
+
/** Every native `<ul>` prop (including `ref`), plus the links and their alignment. */
|
|
14
|
+
export type NavLinksProps = Omit<ComponentProps<"ul">, "children"> & {
|
|
15
|
+
links: readonly SiteLinkItem[];
|
|
16
|
+
align?: SiteNavAlign | undefined;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* @function NavLinks
|
|
20
|
+
* @param props {NavLinksProps} the links (items without `href` show as dimmed text with their
|
|
21
|
+
* `note`), the alignment (default "start") and native list props
|
|
22
|
+
* @returns {JSX.Element} a `<ul>` of links, the one for the current path marked aria-current
|
|
23
|
+
*/
|
|
24
|
+
export declare function NavLinks({ links, align, className, ...props }: NavLinksProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/shell/NavLinks.tsx
|
|
3
|
+
* @desc The header's nav list. A client component only so it can read the current path and set
|
|
4
|
+
* aria-current on the matching link; SiteHeader around it stays a server component.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
"use client";
|
|
10
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { usePathname } from "next/navigation.js";
|
|
12
|
+
import { cx } from "../../utils/cx.js";
|
|
13
|
+
import { AutoLink } from "./AutoLink.js";
|
|
14
|
+
import { ariaCurrentFor } from "./links.js";
|
|
15
|
+
const LISTS = {
|
|
16
|
+
start: "flex flex-wrap gap-x-5 gap-y-1 font-bold text-sm",
|
|
17
|
+
center: "flex flex-wrap items-center justify-center gap-x-6 gap-y-1 font-bold",
|
|
18
|
+
};
|
|
19
|
+
// One text color per state: the current link is lit, the rest light up on hover.
|
|
20
|
+
const LINKS = {
|
|
21
|
+
start: "text-c3 transition-colors hover:text-c1",
|
|
22
|
+
center: "text-c2 transition-colors hover:text-c1",
|
|
23
|
+
};
|
|
24
|
+
const CURRENT = "text-c1 transition-colors";
|
|
25
|
+
/**
|
|
26
|
+
* @function NavLinks
|
|
27
|
+
* @param props {NavLinksProps} the links (items without `href` show as dimmed text with their
|
|
28
|
+
* `note`), the alignment (default "start") and native list props
|
|
29
|
+
* @returns {JSX.Element} a `<ul>` of links, the one for the current path marked aria-current
|
|
30
|
+
*/
|
|
31
|
+
export function NavLinks({ links, align = "start", className, ...props }) {
|
|
32
|
+
const pathname = usePathname();
|
|
33
|
+
return (_jsx("ul", { className: cx(LISTS[align], className), ...props, children: links.map((item) => {
|
|
34
|
+
if (!item.href) {
|
|
35
|
+
return (_jsx("li", { children: _jsxs("span", { "aria-disabled": "true", className: "text-c4", children: [item.label, item.note ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { className: "text-xs uppercase tracking-wide", children: item.note })] })) : null] }) }, item.label));
|
|
36
|
+
}
|
|
37
|
+
const current = ariaCurrentFor(pathname, item.href);
|
|
38
|
+
return (_jsx("li", { children: _jsx(AutoLink, { href: item.href, "aria-current": current, className: current ? CURRENT : LINKS[align], children: item.label }) }, item.href));
|
|
39
|
+
}) }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/shell/PageShell.tsx
|
|
3
|
+
* @desc Page frame: a skip link, the header, the main landmark and the footer, with the footer
|
|
4
|
+
* held to the bottom on short pages.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
10
|
+
/** Every native `<div>` prop (including `ref`), plus the header and footer slots. */
|
|
11
|
+
export type PageShellProps = ComponentProps<"div"> & {
|
|
12
|
+
/** Rendered above main, usually a `<SiteHeader>`. */
|
|
13
|
+
header?: ReactNode;
|
|
14
|
+
/** Rendered below main, usually a `<SiteFooter>`. */
|
|
15
|
+
footer?: ReactNode;
|
|
16
|
+
/** The skip link's text. Default "Skip to content". */
|
|
17
|
+
skipLabel?: string | undefined;
|
|
18
|
+
/** The main landmark's id, which the skip link targets. Default "main". */
|
|
19
|
+
mainId?: string | undefined;
|
|
20
|
+
/** Extra classes for the main landmark, appended last (they win on conflict). */
|
|
21
|
+
mainClassName?: string | undefined;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* @function PageShell
|
|
25
|
+
* @param props {PageShellProps} the page as children, header and footer slots, skip link text,
|
|
26
|
+
* main id and classes, and native div props
|
|
27
|
+
* @returns {JSX.Element} the page between the header and footer, behind a skip link to main
|
|
28
|
+
*/
|
|
29
|
+
export declare function PageShell({ header, footer, skipLabel, mainId, mainClassName, className, children, ...props }: PageShellProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
/**
|
|
4
|
+
* @function PageShell
|
|
5
|
+
* @param props {PageShellProps} the page as children, header and footer slots, skip link text,
|
|
6
|
+
* main id and classes, and native div props
|
|
7
|
+
* @returns {JSX.Element} the page between the header and footer, behind a skip link to main
|
|
8
|
+
*/
|
|
9
|
+
export function PageShell({ header, footer, skipLabel = "Skip to content", mainId = "main", mainClassName, className, children, ...props }) {
|
|
10
|
+
return (_jsxs("div", { className: cx("flex min-h-dvh flex-col", className), ...props, children: [_jsx("a", { href: `#${mainId}`, className: "sr-only rounded-full bg-h2 px-4 py-2 font-bold text-c1 focus:not-sr-only focus:absolute focus:top-2 focus:left-2", children: skipLabel }), header, _jsx("main", { id: mainId, className: cx("mx-auto w-full max-w-5xl flex-1 px-4 py-10", mainClassName), children: children }), footer] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/shell/SiteFooter.tsx
|
|
3
|
+
* @desc Site footer: labelled link columns from data (entries without an href show as text with
|
|
4
|
+
* a small note, like "soon"), an optional extra slot, one line of fine print, the
|
|
5
|
+
* haruhime.moe wordmark linking the parent site, and a GitHub icon link.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Wed Sep 23, 2026
|
|
8
|
+
* @modified Wed Sep 23, 2026
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
11
|
+
import type { SiteLinkItem } from "./links.js";
|
|
12
|
+
/** One footer column: a title (also the nav landmark's name) and its entries. */
|
|
13
|
+
export type SiteFooterColumn = {
|
|
14
|
+
title: string;
|
|
15
|
+
items: readonly SiteLinkItem[];
|
|
16
|
+
};
|
|
17
|
+
/** Every native `<footer>` prop (including `ref`), plus the columns and the bottom row. */
|
|
18
|
+
export type SiteFooterProps = Omit<ComponentProps<"footer">, "children"> & {
|
|
19
|
+
/** Link columns, left to right. Internal hrefs use `next/link`, external ones a plain `<a>`. */
|
|
20
|
+
columns?: readonly SiteFooterColumn[] | undefined;
|
|
21
|
+
/** Rendered above the fine print, e.g. a "clear local data" control. */
|
|
22
|
+
extra?: ReactNode;
|
|
23
|
+
/** One line of small print, e.g. a trademark notice. */
|
|
24
|
+
finePrint?: ReactNode;
|
|
25
|
+
/** Show the haruhime.moe wordmark linking the parent site. Default true. */
|
|
26
|
+
parentLink?: boolean | undefined;
|
|
27
|
+
/** Where the wordmark links. Default "https://www.haruhime.moe". */
|
|
28
|
+
parentHref?: string | undefined;
|
|
29
|
+
/** Where the GitHub icon links, or false to leave it out. Default the haruhimemoe org. */
|
|
30
|
+
githubHref?: string | false | undefined;
|
|
31
|
+
/** The GitHub link's accessible name. Default "haruhimemoe on GitHub". */
|
|
32
|
+
githubLabel?: string | undefined;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* @function SiteFooter
|
|
36
|
+
* @param props {SiteFooterProps} columns, extra slot, fine print, parent and GitHub links, and
|
|
37
|
+
* native footer props
|
|
38
|
+
* @returns {JSX.Element} the footer: columns on top, then extra, fine print and the brand row
|
|
39
|
+
*/
|
|
40
|
+
export declare function SiteFooter({ columns, extra, finePrint, parentLink, parentHref, githubHref, githubLabel, className, ...props }: SiteFooterProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { GitHubIcon } from "../icons/GitHubIcon.js";
|
|
4
|
+
import { HaruhimeWordmarkLink } from "../icons/HaruhimeWordmarkLink.js";
|
|
5
|
+
import { AutoLink } from "./AutoLink.js";
|
|
6
|
+
// Static strings so Tailwind sees every class. Four or more columns share the four-column grid.
|
|
7
|
+
const GRID_COLUMNS = ["", "", "sm:grid-cols-2", "sm:grid-cols-3", "sm:grid-cols-4"];
|
|
8
|
+
/**
|
|
9
|
+
* @function SiteFooter
|
|
10
|
+
* @param props {SiteFooterProps} columns, extra slot, fine print, parent and GitHub links, and
|
|
11
|
+
* native footer props
|
|
12
|
+
* @returns {JSX.Element} the footer: columns on top, then extra, fine print and the brand row
|
|
13
|
+
*/
|
|
14
|
+
export function SiteFooter({ columns = [], extra, finePrint, parentLink = true, parentHref = "https://www.haruhime.moe", githubHref = "https://github.com/haruhimemoe", githubLabel = "haruhimemoe on GitHub", className, ...props }) {
|
|
15
|
+
const fine = finePrint ? _jsx("p", { className: "text-c4 text-xs", children: finePrint }) : null;
|
|
16
|
+
const github = githubHref ? (_jsx("a", { href: githubHref, "aria-label": githubLabel, className: "shrink-0 text-c3 transition-colors hover:text-c1", children: _jsx(GitHubIcon, {}) })) : null;
|
|
17
|
+
// With the wordmark, the fine print gets its own line and the row holds wordmark + icon.
|
|
18
|
+
// Without it, the fine print shares the row with the icon.
|
|
19
|
+
const rowStart = parentLink ? _jsx(HaruhimeWordmarkLink, { href: parentHref }) : fine;
|
|
20
|
+
return (_jsx("footer", { className: cx("border-b4 border-t bg-b6 text-c3 text-sm", className), ...props, children: _jsxs("div", { className: "mx-auto flex max-w-5xl flex-col gap-8 px-4 py-10", children: [columns.length > 0 ? (_jsx("div", { className: cx("grid gap-8", GRID_COLUMNS[Math.min(columns.length, 4)]), children: columns.map((column) => (_jsxs("nav", { "aria-label": column.title, children: [_jsx("p", { className: "mb-3 font-bold text-c4 text-xs uppercase tracking-wide", children: column.title }), _jsx("ul", { className: "flex flex-col gap-2", children: column.items.map((item) => (_jsxs("li", { children: [item.href ? (_jsx(AutoLink, { href: item.href, className: "wrap-anywhere transition-colors hover:text-c1", children: item.label })) : (_jsx("span", { children: item.label })), item.note ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { className: "text-c4 text-xs uppercase tracking-wide", children: item.note })] })) : null] }, `${item.label} ${item.href ?? ""}`))) })] }, column.title))) })) : null, extra || fine || rowStart || github ? (_jsxs("div", { className: "flex flex-col gap-3 border-b4 border-t pt-6", children: [extra, parentLink ? fine : null, rowStart || github ? (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [rowStart, github] })) : null] })) : null] }) }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/shell/SiteHeader.tsx
|
|
3
|
+
* @desc Site header, the osu!-web dark bar: a brand slot on the left, nav links from data, and an
|
|
4
|
+
* actions slot on the right (an account menu, say). A server component; only the nav list
|
|
5
|
+
* inside is a client component, to mark the current page.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Wed Sep 23, 2026
|
|
8
|
+
* @modified Wed Sep 23, 2026
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
11
|
+
import type { SiteLinkItem } from "./links.js";
|
|
12
|
+
import { type SiteNavAlign } from "./NavLinks.js";
|
|
13
|
+
/** Every native `<header>` prop (including `ref`), plus the brand, links and actions. */
|
|
14
|
+
export type SiteHeaderProps = Omit<ComponentProps<"header">, "children"> & {
|
|
15
|
+
/** The left side, usually a home link with the site's wordmark. */
|
|
16
|
+
brand: ReactNode;
|
|
17
|
+
/** Nav entries. Internal hrefs use `next/link`, external ones a plain `<a>`. */
|
|
18
|
+
links?: readonly SiteLinkItem[] | undefined;
|
|
19
|
+
/** The nav landmark's accessible name. Default "Main". */
|
|
20
|
+
navLabel?: string | undefined;
|
|
21
|
+
/** "start" puts the nav right after the brand, "center" centers it. Default "start". */
|
|
22
|
+
navAlign?: SiteNavAlign | undefined;
|
|
23
|
+
/** The right side, pushed to the far end. */
|
|
24
|
+
actions?: ReactNode;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* @function SiteHeader
|
|
28
|
+
* @param props {SiteHeaderProps} brand, nav links, nav label and alignment, actions, and native
|
|
29
|
+
* header props
|
|
30
|
+
* @returns {JSX.Element} the top bar: brand, nav, actions, wrapping onto a second line on phones
|
|
31
|
+
*/
|
|
32
|
+
export declare function SiteHeader({ brand, links, navLabel, navAlign, actions, className, ...props }: SiteHeaderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { NavLinks } from "./NavLinks.js";
|
|
4
|
+
const ROWS = {
|
|
5
|
+
start: "gap-x-8 gap-y-2",
|
|
6
|
+
center: "gap-4",
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* @function SiteHeader
|
|
10
|
+
* @param props {SiteHeaderProps} brand, nav links, nav label and alignment, actions, and native
|
|
11
|
+
* header props
|
|
12
|
+
* @returns {JSX.Element} the top bar: brand, nav, actions, wrapping onto a second line on phones
|
|
13
|
+
*/
|
|
14
|
+
export function SiteHeader({ brand, links = [], navLabel = "Main", navAlign = "start", actions, className, ...props }) {
|
|
15
|
+
return (_jsx("header", { className: cx("border-b4 border-b bg-b6", className), ...props, children: _jsxs("div", { className: cx("mx-auto flex max-w-5xl flex-wrap items-center px-4 py-3", ROWS[navAlign]), children: [brand, links.length > 0 ? (_jsx("nav", { "aria-label": navLabel, className: navAlign === "center" ? "flex-1" : undefined, children: _jsx(NavLinks, { links: links, align: navAlign }) })) : null, actions ? _jsx("div", { className: "ml-auto", children: actions }) : null] }) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/shell/links.ts
|
|
3
|
+
* @desc Link data shared by SiteHeader and SiteFooter, plus the two rules they apply to it: which
|
|
4
|
+
* hrefs leave the app (plain `<a>`) and which link marks the current page.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
import { isExternalHref } from "../../utils/href.js";
|
|
10
|
+
export { isExternalHref };
|
|
11
|
+
/** A nav or footer entry. Without `href` it renders as plain text, with `note` beside it. */
|
|
12
|
+
export type SiteLinkItem = {
|
|
13
|
+
label: string;
|
|
14
|
+
href?: string | undefined;
|
|
15
|
+
note?: string | undefined;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* @function ariaCurrentFor
|
|
19
|
+
* @param pathname {string | null} the current pathname (`usePathname()`), null outside the router
|
|
20
|
+
* @param href {string} a nav link target
|
|
21
|
+
* @returns {"page" | "true" | undefined} "page" when the link is the current page, "true" when
|
|
22
|
+
* the current page sits under it (`/packs` while on `/packs/123`), undefined otherwise.
|
|
23
|
+
* External and relative hrefs never match, and "/" only matches itself.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ariaCurrentFor(pathname: string | null, href: string): "page" | "true" | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/shell/links.ts
|
|
3
|
+
* @desc Link data shared by SiteHeader and SiteFooter, plus the two rules they apply to it: which
|
|
4
|
+
* hrefs leave the app (plain `<a>`) and which link marks the current page.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Wed Sep 23, 2026
|
|
7
|
+
* @modified Wed Sep 23, 2026
|
|
8
|
+
*/
|
|
9
|
+
import { isExternalHref } from "../../utils/href.js";
|
|
10
|
+
export { isExternalHref };
|
|
11
|
+
const trimSlash = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
12
|
+
/**
|
|
13
|
+
* @function ariaCurrentFor
|
|
14
|
+
* @param pathname {string | null} the current pathname (`usePathname()`), null outside the router
|
|
15
|
+
* @param href {string} a nav link target
|
|
16
|
+
* @returns {"page" | "true" | undefined} "page" when the link is the current page, "true" when
|
|
17
|
+
* the current page sits under it (`/packs` while on `/packs/123`), undefined otherwise.
|
|
18
|
+
* External and relative hrefs never match, and "/" only matches itself.
|
|
19
|
+
*/
|
|
20
|
+
export function ariaCurrentFor(pathname, href) {
|
|
21
|
+
if (!pathname || isExternalHref(href))
|
|
22
|
+
return undefined;
|
|
23
|
+
const target = trimSlash(href.replace(/[?#].*$/s, ""));
|
|
24
|
+
if (!target.startsWith("/"))
|
|
25
|
+
return undefined;
|
|
26
|
+
const here = trimSlash(pathname);
|
|
27
|
+
if (here === target)
|
|
28
|
+
return "page";
|
|
29
|
+
if (target !== "/" && here.startsWith(`${target}/`))
|
|
30
|
+
return "true";
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/index.ts
|
|
3
|
+
* @desc @haruhimemoe/ui: React components for the haruhime.moe osu! tools on Next.js. Pair with
|
|
4
|
+
* the theme: `@import "@haruhimemoe/ui/theme.css";` after Tailwind. Client components carry
|
|
5
|
+
* their own "use client" directive, so this barrel is safe to import from Server Components.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Wed Sep 23, 2026
|
|
8
|
+
* @modified Wed Sep 23, 2026
|
|
9
|
+
*/
|
|
10
|
+
export { CopyButton, type CopyButtonProps } from "./components/actions/CopyButton.js";
|
|
11
|
+
export { JsonLd, type JsonLdProps } from "./components/actions/JsonLd.js";
|
|
12
|
+
export { Pagination, type PaginationProps } from "./components/actions/Pagination.js";
|
|
13
|
+
export { Button, type ButtonProps } from "./components/basics/Button.js";
|
|
14
|
+
export { ButtonLink, type ButtonLinkProps } from "./components/basics/ButtonLink.js";
|
|
15
|
+
export { type ButtonClassOptions, type ButtonSize, type ButtonVariant, buttonClasses, } from "./components/basics/buttonStyles.js";
|
|
16
|
+
export { Card, type CardProps } from "./components/basics/Card.js";
|
|
17
|
+
export { Notice, type NoticeProps, type NoticeTone } from "./components/basics/Notice.js";
|
|
18
|
+
export { PageHeader, type PageHeaderProps } from "./components/basics/PageHeader.js";
|
|
19
|
+
export { Prose, type ProseProps } from "./components/basics/Prose.js";
|
|
20
|
+
export { Chip, type ChipProps } from "./components/filters/Chip.js";
|
|
21
|
+
export { ChipGroup, type ChipGroupProps, type ChipOption } from "./components/filters/ChipGroup.js";
|
|
22
|
+
export { FilterPanel, type FilterPanelProps } from "./components/filters/FilterPanel.js";
|
|
23
|
+
export { FilterRow, type FilterRowProps } from "./components/filters/FilterRow.js";
|
|
24
|
+
export { RangeSlider, type RangeSliderProps, type RangeSliderValue, } from "./components/filters/RangeSlider.js";
|
|
25
|
+
export { Checkbox, type CheckboxProps } from "./components/forms/Checkbox.js";
|
|
26
|
+
export type { FieldProps } from "./components/forms/FieldFrame.js";
|
|
27
|
+
export { fieldClasses } from "./components/forms/fieldStyles.js";
|
|
28
|
+
export { Select, type SelectProps } from "./components/forms/Select.js";
|
|
29
|
+
export { Textarea, type TextareaProps } from "./components/forms/Textarea.js";
|
|
30
|
+
export { TextInput, type TextInputProps } from "./components/forms/TextInput.js";
|
|
31
|
+
export { GitHubIcon, type GitHubIconProps } from "./components/icons/GitHubIcon.js";
|
|
32
|
+
export { HaruhimeWordmark, type HaruhimeWordmarkProps, } from "./components/icons/HaruhimeWordmark.js";
|
|
33
|
+
export { HaruhimeWordmarkLink, type HaruhimeWordmarkLinkProps, } from "./components/icons/HaruhimeWordmarkLink.js";
|
|
34
|
+
export type { SiteLinkItem } from "./components/shell/links.js";
|
|
35
|
+
export { NavLinks, type NavLinksProps, type SiteNavAlign } from "./components/shell/NavLinks.js";
|
|
36
|
+
export { PageShell, type PageShellProps } from "./components/shell/PageShell.js";
|
|
37
|
+
export { SiteFooter, type SiteFooterColumn, type SiteFooterProps, } from "./components/shell/SiteFooter.js";
|
|
38
|
+
export { SiteHeader, type SiteHeaderProps } from "./components/shell/SiteHeader.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/index.ts
|
|
3
|
+
* @desc @haruhimemoe/ui: React components for the haruhime.moe osu! tools on Next.js. Pair with
|
|
4
|
+
* the theme: `@import "@haruhimemoe/ui/theme.css";` after Tailwind. Client components carry
|
|
5
|
+
* their own "use client" directive, so this barrel is safe to import from Server Components.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Wed Sep 23, 2026
|
|
8
|
+
* @modified Wed Sep 23, 2026
|
|
9
|
+
*/
|
|
10
|
+
// Actions
|
|
11
|
+
export { CopyButton } from "./components/actions/CopyButton.js";
|
|
12
|
+
export { JsonLd } from "./components/actions/JsonLd.js";
|
|
13
|
+
export { Pagination } from "./components/actions/Pagination.js";
|
|
14
|
+
// Basics
|
|
15
|
+
export { Button } from "./components/basics/Button.js";
|
|
16
|
+
export { ButtonLink } from "./components/basics/ButtonLink.js";
|
|
17
|
+
export { buttonClasses, } from "./components/basics/buttonStyles.js";
|
|
18
|
+
export { Card } from "./components/basics/Card.js";
|
|
19
|
+
export { Notice } from "./components/basics/Notice.js";
|
|
20
|
+
export { PageHeader } from "./components/basics/PageHeader.js";
|
|
21
|
+
export { Prose } from "./components/basics/Prose.js";
|
|
22
|
+
// Filters
|
|
23
|
+
export { Chip } from "./components/filters/Chip.js";
|
|
24
|
+
export { ChipGroup } from "./components/filters/ChipGroup.js";
|
|
25
|
+
export { FilterPanel } from "./components/filters/FilterPanel.js";
|
|
26
|
+
export { FilterRow } from "./components/filters/FilterRow.js";
|
|
27
|
+
export { RangeSlider, } from "./components/filters/RangeSlider.js";
|
|
28
|
+
// Forms
|
|
29
|
+
export { Checkbox } from "./components/forms/Checkbox.js";
|
|
30
|
+
export { fieldClasses } from "./components/forms/fieldStyles.js";
|
|
31
|
+
export { Select } from "./components/forms/Select.js";
|
|
32
|
+
export { Textarea } from "./components/forms/Textarea.js";
|
|
33
|
+
export { TextInput } from "./components/forms/TextInput.js";
|
|
34
|
+
// Icons
|
|
35
|
+
export { GitHubIcon } from "./components/icons/GitHubIcon.js";
|
|
36
|
+
export { HaruhimeWordmark, } from "./components/icons/HaruhimeWordmark.js";
|
|
37
|
+
export { HaruhimeWordmarkLink, } from "./components/icons/HaruhimeWordmarkLink.js";
|
|
38
|
+
export { NavLinks } from "./components/shell/NavLinks.js";
|
|
39
|
+
export { PageShell } from "./components/shell/PageShell.js";
|
|
40
|
+
export { SiteFooter, } from "./components/shell/SiteFooter.js";
|
|
41
|
+
export { SiteHeader } from "./components/shell/SiteHeader.js";
|