@haruhimemoe/ui 0.11.2 → 0.12.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 +29 -1
- package/README.md +64 -6
- package/dist/components/actions/AsyncButton.d.ts +1 -1
- package/dist/components/actions/AsyncButton.js +3 -2
- package/dist/components/basics/AutoLink.d.ts +8 -4
- package/dist/components/basics/AutoLink.js +12 -5
- package/dist/components/basics/Badge.d.ts +1 -1
- package/dist/components/basics/Badge.js +3 -3
- package/dist/components/basics/Disclosure.d.ts +1 -1
- package/dist/components/basics/Disclosure.js +2 -2
- package/dist/components/basics/Notice.d.ts +1 -1
- package/dist/components/basics/Notice.js +6 -6
- package/dist/components/basics/Tabs.d.ts +1 -1
- package/dist/components/basics/Tabs.js +3 -3
- package/dist/components/basics/Text.d.ts +26 -0
- package/dist/components/basics/Text.js +17 -0
- package/dist/components/basics/buttonStyles.d.ts +2 -2
- package/dist/components/basics/buttonStyles.js +9 -6
- package/dist/components/basics/cardStyles.d.ts +4 -3
- package/dist/components/basics/cardStyles.js +4 -3
- package/dist/components/basics/textStyles.d.ts +28 -0
- package/dist/components/basics/textStyles.js +18 -0
- package/dist/components/basics/textTones.d.ts +17 -0
- package/dist/components/basics/textTones.js +24 -0
- package/dist/components/basics/useMotionAllowed.d.ts +16 -0
- package/dist/components/basics/useMotionAllowed.js +41 -0
- package/dist/components/brand/BrandPage.js +2 -1
- package/dist/components/content/ContentNav.js +2 -2
- package/dist/components/content/CopyMarkdownButton.js +3 -2
- package/dist/components/filters/FilterPanel.d.ts +1 -1
- package/dist/components/filters/FilterPanel.js +2 -2
- package/dist/components/filters/GroupFrame.d.ts +4 -2
- package/dist/components/filters/GroupFrame.js +1 -1
- package/dist/components/filters/chipStyles.d.ts +7 -6
- package/dist/components/filters/chipStyles.js +8 -6
- package/dist/components/forms/CharCounter.d.ts +1 -1
- package/dist/components/forms/CharCounter.js +6 -1
- package/dist/components/forms/Checkbox.d.ts +4 -3
- package/dist/components/forms/Checkbox.js +5 -4
- package/dist/components/forms/FieldFrame.d.ts +9 -4
- package/dist/components/forms/FieldFrame.js +6 -5
- package/dist/components/forms/RadioGroup.d.ts +4 -2
- package/dist/components/forms/RadioGroup.js +4 -4
- package/dist/components/forms/ReportDisclosure.d.ts +1 -1
- package/dist/components/forms/ReportDisclosure.js +2 -2
- package/dist/components/forms/Select.d.ts +4 -4
- package/dist/components/forms/Select.js +4 -4
- package/dist/components/forms/TextInput.d.ts +3 -3
- package/dist/components/forms/TextInput.js +3 -3
- package/dist/components/forms/Textarea.d.ts +6 -4
- package/dist/components/forms/Textarea.js +3 -3
- package/dist/components/forms/TypeToConfirm.d.ts +1 -1
- package/dist/components/forms/TypeToConfirm.js +2 -2
- package/dist/components/forms/fieldStyles.d.ts +5 -3
- package/dist/components/forms/fieldStyles.js +6 -4
- package/dist/components/mdx/CodeBlock.d.ts +1 -1
- package/dist/components/mdx/CodeBlock.js +1 -1
- package/dist/components/mdx/CodeCopyButton.d.ts +1 -1
- package/dist/components/mdx/CodeCopyButton.js +2 -2
- package/dist/components/osu/ModBadge.d.ts +14 -9
- package/dist/components/osu/ModBadge.js +42 -14
- package/dist/components/palette/CommandPalette.d.ts +1 -1
- package/dist/components/palette/CommandPalette.js +2 -2
- package/dist/components/palette/PaletteInput.d.ts +1 -1
- package/dist/components/palette/PaletteInput.js +3 -2
- package/dist/components/palette/PaletteRow.d.ts +1 -1
- package/dist/components/palette/PaletteRow.js +3 -3
- package/dist/components/shell/HeaderMenu.d.ts +1 -1
- package/dist/components/shell/HeaderMenu.js +3 -3
- package/dist/components/shell/LinkTabs.d.ts +1 -1
- package/dist/components/shell/LinkTabs.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/theme.css +56 -6
- package/package.json +1 -1
|
@@ -4,10 +4,10 @@ import { FieldFrame, fieldControlProps } from "./FieldFrame.js";
|
|
|
4
4
|
import { fieldClasses } from "./fieldStyles.js";
|
|
5
5
|
/**
|
|
6
6
|
* @function Textarea
|
|
7
|
-
* @param props {TextareaProps} native textarea props, plus id, label, hint, error and
|
|
7
|
+
* @param props {TextareaProps} native textarea props, plus id, label, hideLabel, hint, error and
|
|
8
8
|
* wrapperClassName; `className` goes on the `<textarea>`
|
|
9
9
|
* @returns {JSX.Element} a label, the textarea, and the hint and error when given
|
|
10
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, ...fieldControlProps({ id, hint, error, describedBy, invalid }), className: fieldClasses(cx("min-h-24 resize-y", className)) }) }));
|
|
11
|
+
export function Textarea({ id, label, hideLabel, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }) {
|
|
12
|
+
return (_jsx(FieldFrame, { id: id, label: label, hideLabel: hideLabel, hint: hint, error: error, className: wrapperClassName, children: _jsx("textarea", { ...props, ...fieldControlProps({ id, hint, error, describedBy, invalid }), className: fieldClasses(cx("coarse:min-h-24 min-h-24 resize-y", className)) }) }));
|
|
13
13
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* delete and transfer forms, as one component.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Mon Sep 28, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import { type ComponentProps, type ReactNode } from "react";
|
|
12
12
|
import type { ButtonVariant } from "../basics/buttonStyles.js";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* delete and transfer forms, as one component.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Mon Sep 28, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
"use client";
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -42,5 +42,5 @@ export function TypeToConfirm({ id, expected, label = `Type ${expected} to confi
|
|
|
42
42
|
setPending(false);
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
-
return (_jsxs("form", { onSubmit: submit, className: cx("flex flex-col gap-3", className), ...props, children: [children, _jsx(TextInput, { id: id, label: label, hint: hint, error: error, value: typed, autoComplete: "off", autoCapitalize: "off", spellCheck: false, onChange: (event) => setTyped(event.currentTarget.value) }), _jsx(Button, { type: "submit", variant: variant,
|
|
45
|
+
return (_jsxs("form", { onSubmit: submit, className: cx("flex flex-col gap-3", className), ...props, children: [children, _jsx(TextInput, { id: id, label: label, hint: hint, error: error, value: typed, autoComplete: "off", autoCapitalize: "off", spellCheck: false, onChange: (event) => setTyped(event.currentTarget.value) }), _jsx(Button, { type: "submit", variant: variant, disabled: !matches || pending, children: pending && pendingLabel !== undefined ? pendingLabel : submitLabel })] }));
|
|
46
46
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/components/forms/fieldStyles.ts
|
|
3
|
-
*
|
|
3
|
+
* @desc Shared field classes for TextInput, Select, Textarea and RangeBox (the packs look), and
|
|
4
4
|
* the label class the fieldsets share. Focus is the theme's 2px h1 outline, the same ring
|
|
5
5
|
* every control gets, plus an h1 border; an invalid field keeps its rose border until it
|
|
6
|
-
* takes focus, when the h1 border and ring take over so focus stays visible.
|
|
6
|
+
* takes focus, when the h1 border and ring take over so focus stays visible. On a coarse
|
|
7
|
+
* pointer a field is 44px tall with 16px text (iOS Safari zooms into smaller text); under
|
|
8
|
+
* more contrast its border is c4.
|
|
7
9
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
10
|
* @created Wed Sep 23, 2026
|
|
9
|
-
* @modified
|
|
11
|
+
* @modified Sun Oct 4, 2026
|
|
10
12
|
*/
|
|
11
13
|
/** A field or group label: bold c3 at text-sm (internal). */
|
|
12
14
|
export declare const FIELD_LABEL = "font-bold text-c3 text-sm";
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/components/forms/fieldStyles.ts
|
|
3
|
-
*
|
|
3
|
+
* @desc Shared field classes for TextInput, Select, Textarea and RangeBox (the packs look), and
|
|
4
4
|
* the label class the fieldsets share. Focus is the theme's 2px h1 outline, the same ring
|
|
5
5
|
* every control gets, plus an h1 border; an invalid field keeps its rose border until it
|
|
6
|
-
* takes focus, when the h1 border and ring take over so focus stays visible.
|
|
6
|
+
* takes focus, when the h1 border and ring take over so focus stays visible. On a coarse
|
|
7
|
+
* pointer a field is 44px tall with 16px text (iOS Safari zooms into smaller text); under
|
|
8
|
+
* more contrast its border is c4.
|
|
7
9
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
10
|
* @created Wed Sep 23, 2026
|
|
9
|
-
* @modified
|
|
11
|
+
* @modified Sun Oct 4, 2026
|
|
10
12
|
*/
|
|
11
13
|
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 disabled:opacity-50 aria-invalid:border-rose-400 aria-invalid:focus-visible:border-h1";
|
|
14
|
+
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 disabled:opacity-50 aria-invalid:border-rose-400 aria-invalid:focus-visible:border-h1 coarse:min-h-11 coarse:text-base contrast-more:border-c4";
|
|
13
15
|
/** A field or group label: bold c3 at text-sm (internal). */
|
|
14
16
|
export const FIELD_LABEL = "font-bold text-c3 text-sm";
|
|
15
17
|
/**
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Server-safe: the only client piece is the copy button (CodeCopyButton).
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Sat Oct 3, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import type { ComponentProps } from "react";
|
|
12
12
|
/** The code (any line endings, trailing newline optional), language, title, highlighted lines
|
|
@@ -37,7 +37,7 @@ export async function CodeBlock({ code, lang, title, highlight = [], className,
|
|
|
37
37
|
const lines = tokens ?? text.split("\n").map((content) => [{ content, offset: 0 }]);
|
|
38
38
|
const marked = new Set(highlight);
|
|
39
39
|
const name = title ?? lang;
|
|
40
|
-
return (_jsxs("div", { className: cx("mt-3 overflow-hidden rounded-md border border-b3 bg-b6", className), ...rest, children: [_jsxs("div", { className: "flex min-h-9 items-center justify-between gap-3 border-b3 border-b px-3 py-1 text-c3 text-sm", children: [_jsx("span", { className: "truncate", children: name ?? "Code" }), _jsx(CodeCopyButton, { code: text, label: title ? `Copy ${title}` : "Copy code" })] }), _jsx("pre", { role: "group",
|
|
40
|
+
return (_jsxs("div", { className: cx("mt-3 overflow-hidden rounded-md border border-b3 bg-b6 contrast-more:border-c4", className), ...rest, children: [_jsxs("div", { className: "flex min-h-9 items-center justify-between gap-3 border-b3 border-b px-3 py-1 text-c3 text-sm", children: [_jsx("span", { className: "truncate", children: name ?? "Code" }), _jsx(CodeCopyButton, { code: text, label: title ? `Copy ${title}` : "Copy code" })] }), _jsx("pre", { role: "group",
|
|
41
41
|
// biome-ignore lint/a11y/noNoninteractiveTabindex: a scrollable region needs keyboard focus
|
|
42
42
|
tabIndex: 0, "aria-label": name ? `Code: ${name}` : "Code", className: "overflow-x-auto py-3 text-c2 text-sm leading-6", children: _jsx("code", { children: lines.flatMap((line, index) => {
|
|
43
43
|
const span = (_jsx("span", { className: marked.has(index + 1) ? MARKED : LINE, "data-highlighted": marked.has(index + 1) ? "" : undefined, children: line.map((token, i) => tokens ? (_jsx("span", { style: tokenStyle(token), children: token.content }, i)) : (token.content)) }, index));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* string and never imports cx, keeping tailwind-merge out of the client bundle.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Sat Oct 3, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
/** The code to copy, and the button's accessible name (e.g. "Copy x.ts" or "Copy code"). */
|
|
12
12
|
export type CodeCopyButtonProps = {
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* string and never imports cx, keeping tailwind-merge out of the client bundle.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Sat Oct 3, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
"use client";
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { StatusOutput } from "../actions/StatusOutput.js";
|
|
14
14
|
import { useLatestStatus } from "../actions/useLatestStatus.js";
|
|
15
|
-
const BUTTON = "inline-flex min-h-6 min-w-6 items-center justify-center rounded px-2 py-0.5 text-c2 text-sm hover:bg-b4 hover:text-c1";
|
|
15
|
+
const BUTTON = "inline-flex min-h-6 min-w-6 items-center justify-center rounded px-2 py-0.5 text-c2 text-sm hover:bg-b4 hover:text-c1 coarse:min-h-11 coarse:min-w-11";
|
|
16
16
|
/**
|
|
17
17
|
* @function CodeCopyButton
|
|
18
18
|
* @param props {CodeCopyButtonProps} the code to copy and the button's accessible name
|
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
* @file src/components/osu/ModBadge.tsx
|
|
3
3
|
* @desc A mod pool slot's pill (NM1, HD2, TB), colored by its mod bucket the way tournament
|
|
4
4
|
* sheets and packs.haruhime.moe color them: NM sky, HD amber, HR rose, DT and NC violet,
|
|
5
|
-
* FM emerald, TB orange. Anything else is a quiet b3 pill; `
|
|
6
|
-
* custom bucket
|
|
5
|
+
* FM emerald, TB orange. Anything else is a quiet b3 pill; `color` picks any of 17 colors
|
|
6
|
+
* (the buckets', the pool palette's and neutral) for a custom bucket. Server-safe.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Mon Sep 28, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import type { ComponentProps } from "react";
|
|
12
|
-
/**
|
|
13
|
-
export type
|
|
12
|
+
/** A badge color: the six bucket colors, @haruhimemoe/pool's PALETTE names, or neutral. */
|
|
13
|
+
export type ModBadgeColor = "sky" | "amber" | "rose" | "violet" | "emerald" | "orange" | "green" | "teal" | "pink" | "lime" | "cyan" | "fuchsia" | "yellow" | "red" | "indigo" | "stone" | "neutral";
|
|
14
|
+
/** Every native `<span>` prop (except the legacy `color`), plus the slot and an optional color. */
|
|
15
|
+
export type ModBadgeProps = Omit<ComponentProps<"span">, "color"> & {
|
|
14
16
|
/** A mod or slot label ("HD", "NM1", "TB"). Its first two letters pick the color. */
|
|
15
17
|
mod: string;
|
|
18
|
+
/** Overrides the color picked from the mod's first two letters. */
|
|
19
|
+
color?: ModBadgeColor | undefined;
|
|
16
20
|
};
|
|
17
21
|
/**
|
|
18
22
|
* @function ModBadge
|
|
19
|
-
* @param props {ModBadgeProps} the mod or slot label, and native span props
|
|
20
|
-
* the shown text; a `title` can name the bucket)
|
|
21
|
-
* @returns {JSX.Element} a rounded pill in the bucket's
|
|
23
|
+
* @param props {ModBadgeProps} the mod or slot label, an optional color, and native span props
|
|
24
|
+
* (children replace the shown text; a `title` can name the bucket)
|
|
25
|
+
* @returns {JSX.Element} a rounded pill in the color (or the bucket's), dark text on color, c2 on
|
|
26
|
+
* neutral
|
|
22
27
|
*/
|
|
23
|
-
export declare function ModBadge({ mod, className, children, ...props }: ModBadgeProps): import("react").JSX.Element;
|
|
28
|
+
export declare function ModBadge({ mod, color, className, children, ...props }: ModBadgeProps): import("react").JSX.Element;
|
|
@@ -1,22 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../../utils/cx.js";
|
|
3
|
+
// Literal classes, so the app's Tailwind finds them through theme.css's @source. Every background
|
|
4
|
+
// is light enough for text-b6 at 4.5:1 or more.
|
|
5
|
+
const COLORS = {
|
|
6
|
+
sky: "bg-sky-400 text-b6",
|
|
7
|
+
amber: "bg-amber-300 text-b6",
|
|
8
|
+
rose: "bg-rose-400 text-b6",
|
|
9
|
+
violet: "bg-violet-400 text-b6",
|
|
10
|
+
emerald: "bg-emerald-400 text-b6",
|
|
11
|
+
orange: "bg-orange-400 text-b6",
|
|
12
|
+
green: "bg-green-400 text-b6",
|
|
13
|
+
teal: "bg-teal-300 text-b6",
|
|
14
|
+
pink: "bg-pink-400 text-b6",
|
|
15
|
+
lime: "bg-lime-300 text-b6",
|
|
16
|
+
cyan: "bg-cyan-300 text-b6",
|
|
17
|
+
fuchsia: "bg-fuchsia-400 text-b6",
|
|
18
|
+
yellow: "bg-yellow-300 text-b6",
|
|
19
|
+
red: "bg-red-400 text-b6",
|
|
20
|
+
indigo: "bg-indigo-300 text-b6",
|
|
21
|
+
stone: "bg-stone-300 text-b6",
|
|
22
|
+
neutral: "bg-b3 text-c2",
|
|
23
|
+
};
|
|
3
24
|
const BUCKETS = {
|
|
4
|
-
NM: "
|
|
5
|
-
HD: "
|
|
6
|
-
HR: "
|
|
7
|
-
DT: "
|
|
8
|
-
NC: "
|
|
9
|
-
FM: "
|
|
10
|
-
TB: "
|
|
25
|
+
NM: "sky",
|
|
26
|
+
HD: "amber",
|
|
27
|
+
HR: "rose",
|
|
28
|
+
DT: "violet",
|
|
29
|
+
NC: "violet",
|
|
30
|
+
FM: "emerald",
|
|
31
|
+
TB: "orange",
|
|
32
|
+
};
|
|
33
|
+
const BASE = "inline-flex min-w-11 shrink-0 items-center justify-center whitespace-nowrap rounded-full px-2 py-0.5 font-extrabold text-xs forced-colors:border";
|
|
34
|
+
/** The color's classes: `color` when it is one, else the mod's bucket, else neutral. */
|
|
35
|
+
const colorClasses = (color, mod) => {
|
|
36
|
+
if (color !== undefined && Object.hasOwn(COLORS, color))
|
|
37
|
+
return COLORS[color];
|
|
38
|
+
const bucket = BUCKETS[mod.slice(0, 2).toUpperCase()];
|
|
39
|
+
return COLORS[bucket ?? "neutral"];
|
|
11
40
|
};
|
|
12
|
-
const BASE = "inline-flex min-w-11 shrink-0 items-center justify-center whitespace-nowrap rounded-full px-2 py-0.5 font-extrabold text-xs";
|
|
13
41
|
/**
|
|
14
42
|
* @function ModBadge
|
|
15
|
-
* @param props {ModBadgeProps} the mod or slot label, and native span props
|
|
16
|
-
* the shown text; a `title` can name the bucket)
|
|
17
|
-
* @returns {JSX.Element} a rounded pill in the bucket's
|
|
43
|
+
* @param props {ModBadgeProps} the mod or slot label, an optional color, and native span props
|
|
44
|
+
* (children replace the shown text; a `title` can name the bucket)
|
|
45
|
+
* @returns {JSX.Element} a rounded pill in the color (or the bucket's), dark text on color, c2 on
|
|
46
|
+
* neutral
|
|
18
47
|
*/
|
|
19
|
-
export function ModBadge({ mod, className, children, ...props }) {
|
|
20
|
-
|
|
21
|
-
return (_jsx("span", { className: cx(BASE, color ? `${color} text-b6` : "bg-b3 text-c2", className), ...props, children: children ?? mod }));
|
|
48
|
+
export function ModBadge({ mod, color, className, children, ...props }) {
|
|
49
|
+
return (_jsx("span", { className: cx(BASE, colorClasses(color, mod), className), ...props, children: children ?? mod }));
|
|
22
50
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Mount once, inside a client component, since commands carry functions.
|
|
9
9
|
* @author David @dvhsh (https://dvh.sh)
|
|
10
10
|
* @created Sat Oct 3, 2026
|
|
11
|
-
* @modified
|
|
11
|
+
* @modified Sun Oct 4, 2026
|
|
12
12
|
*/
|
|
13
13
|
import type { CommandPaletteProps } from "./types.js";
|
|
14
14
|
export type { CommandPaletteProps } from "./types.js";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Mount once, inside a client component, since commands carry functions.
|
|
9
9
|
* @author David @dvhsh (https://dvh.sh)
|
|
10
10
|
* @created Sat Oct 3, 2026
|
|
11
|
-
* @modified
|
|
11
|
+
* @modified Sun Oct 4, 2026
|
|
12
12
|
*/
|
|
13
13
|
"use client";
|
|
14
14
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -29,7 +29,7 @@ import { useProviderSearch } from "./useProviderSearch.js";
|
|
|
29
29
|
const NONE = [];
|
|
30
30
|
const CHORD_MS = 800;
|
|
31
31
|
const DIALOG = "m-0 h-dvh max-h-none w-full max-w-none bg-transparent p-0 text-c1 backdrop:bg-b6/70 backdrop:backdrop-blur-sm sm:h-auto sm:pt-[12vh]";
|
|
32
|
-
const PANEL = "mx-auto flex h-full max-h-dvh w-full flex-col overflow-hidden border-b3 bg-b6 shadow-2xl sm:h-auto sm:max-h-[70vh] sm:max-w-xl sm:rounded-xl sm:border";
|
|
32
|
+
const PANEL = "mx-auto flex h-full max-h-dvh w-full flex-col overflow-hidden border-b3 bg-b6 shadow-2xl sm:h-auto sm:max-h-[70vh] sm:max-w-xl sm:rounded-xl sm:border contrast-more:border-c4";
|
|
33
33
|
/**
|
|
34
34
|
* @function CommandPalette
|
|
35
35
|
* @param props {CommandPaletteProps} commands, root providers, storage key, hotkey, placeholder,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* always mounted, so a message that appears is announced.
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Sat Oct 3, 2026
|
|
10
|
-
* @modified
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
11
|
*/
|
|
12
12
|
import { type Ref } from "react";
|
|
13
13
|
/** The input's state and ids. */
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* always mounted, so a message that appears is announced.
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Sat Oct 3, 2026
|
|
10
|
-
* @modified
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
11
|
*/
|
|
12
12
|
"use client";
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
14
|
import { useId } from "react";
|
|
15
|
+
import { TEXT_TONES } from "../basics/textTones.js";
|
|
15
16
|
/**
|
|
16
17
|
* @function PaletteInput
|
|
17
18
|
* @param props {PaletteInputProps} value, placeholder, name, crumbs, list and active ids, error
|
|
@@ -19,5 +20,5 @@ import { useId } from "react";
|
|
|
19
20
|
*/
|
|
20
21
|
export function PaletteInput({ ref, value, placeholder, label, crumbs, listId, activeId, error, onChange, }) {
|
|
21
22
|
const errorId = `${useId()}-error`;
|
|
22
|
-
return (_jsxs("div", { className: "border-b4 border-b has-[input:focus-visible]:border-h1", children: [_jsxs("div", { className: "flex items-center gap-2 px-4 py-3", children: [crumbs.map((crumb, index) => (_jsx("span", { className: "shrink-0 rounded bg-b4 px-2 py-0.5 text-c3 text-xs", children: crumb }, index))), _jsx("input", { ref: ref, type: "text", role: "combobox", "aria-expanded": "true", "aria-controls": listId, "aria-activedescendant": activeId, "aria-autocomplete": "list", "aria-label": label, "aria-invalid": error ? true : undefined, "aria-describedby": errorId, autoComplete: "off", spellCheck: false, value: value, placeholder: placeholder, onChange: (event) => onChange(event.target.value), className: "min-w-0 flex-1 bg-transparent text-base text-c1 outline-hidden placeholder:text-c4 sm:text-lg" })] }), _jsx("p", { id: errorId, role: "status", className: error ?
|
|
23
|
+
return (_jsxs("div", { className: "border-b4 border-b has-[input:focus-visible]:border-h1", children: [_jsxs("div", { className: "flex items-center gap-2 px-4 py-3", children: [crumbs.map((crumb, index) => (_jsx("span", { className: "shrink-0 rounded bg-b4 px-2 py-0.5 text-c3 text-xs", children: crumb }, index))), _jsx("input", { ref: ref, type: "text", role: "combobox", "aria-expanded": "true", "aria-controls": listId, "aria-activedescendant": activeId, "aria-autocomplete": "list", "aria-label": label, "aria-invalid": error ? true : undefined, "aria-describedby": errorId, autoComplete: "off", spellCheck: false, value: value, placeholder: placeholder, onChange: (event) => onChange(event.target.value), className: "min-w-0 flex-1 bg-transparent text-base text-c1 outline-hidden placeholder:text-c4 sm:text-lg" })] }), _jsx("p", { id: errorId, role: "status", className: error ? `px-4 pb-2 text-xs ${TEXT_TONES.error}` : "sr-only", children: error })] }));
|
|
23
24
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* on the dialog, so this isn't focusable (aria-activedescendant names it).
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Sat Oct 3, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import type { FuzzyMatch } from "./fuzzy.js";
|
|
12
12
|
import type { Command } from "./types.js";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { displayShortcut, parseShortcut } from "./hotkeys.js";
|
|
3
3
|
// The active row shows its state with an h1 edge, not background alone: bg-b4 on b6 is only
|
|
4
|
-
// 1.5:1. min-h-9 keeps every row a 36px target.
|
|
5
|
-
const ROW = "flex min-h-9 cursor-default items-center gap-3 rounded-lg border-l-2 border-transparent px-3 py-2";
|
|
4
|
+
// 1.5:1. min-h-9 keeps every row a 36px target, 44px on a coarse pointer.
|
|
5
|
+
const ROW = "flex min-h-9 cursor-default items-center gap-3 rounded-lg border-l-2 border-transparent px-3 py-2 coarse:min-h-11";
|
|
6
6
|
const ACTIVE = `${ROW} border-h1 bg-b4 forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]`;
|
|
7
|
-
const KBD = "rounded border border-b3 bg-b5 px-1.5 font-sans text-c3 text-xs";
|
|
7
|
+
const KBD = "rounded border border-b3 bg-b5 px-1.5 font-sans text-c3 text-xs contrast-more:border-c4";
|
|
8
8
|
const highlighted = (title, ranges) => {
|
|
9
9
|
const parts = [];
|
|
10
10
|
let at = 0;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* a click on a link, or focus leaving it closes it too.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Mon Sep 28, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import { type ComponentProps, type ReactNode } from "react";
|
|
12
12
|
/** One link in the panel. */
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* a click on a link, or focus leaving it closes it too.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Mon Sep 28, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
"use client";
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { useEffect, useId, useRef, useState } from "react";
|
|
14
14
|
import { cx } from "../../utils/cx.js";
|
|
15
15
|
import { AutoLink } from "../basics/AutoLink.js";
|
|
16
|
-
const ITEM = "block rounded px-3 py-2 font-bold text-c2 text-sm hover:bg-b4 hover:text-c1";
|
|
16
|
+
const ITEM = "block rounded px-3 py-2 font-bold text-c2 text-sm hover:bg-b4 hover:text-c1 coarse:py-3";
|
|
17
17
|
/**
|
|
18
18
|
* @function HeaderMenu
|
|
19
19
|
* @param props {HeaderMenuProps} the button's content, the links, extra controls as children, the
|
|
@@ -54,5 +54,5 @@ export function HeaderMenu({ label, items = [], children, align = "end", buttonL
|
|
|
54
54
|
box?.removeEventListener("focusout", onFocusOut);
|
|
55
55
|
};
|
|
56
56
|
}, [open]);
|
|
57
|
-
return (_jsxs("div", { ref: wrapper, className: cx("relative", className), ...props, children: [_jsx("button", { ref: button, type: "button", "aria-expanded": open, "aria-controls": panelId, "aria-label": buttonLabel, onClick: () => setOpen(!open), className: cx("flex min-h-6 items-center gap-2 font-bold text-c1 text-sm transition-colors hover:text-h1", buttonClassName), children: label }), _jsxs("div", { id: panelId, hidden: !open, className: cx("absolute z-10 mt-2 w-44 flex-col gap-1 rounded-lg border border-b3 bg-b6 p-2", open && "flex", align === "end" ? "right-0" : "left-0"), children: [items.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1", children: items.map((item) => (_jsx("li", { children: _jsx(AutoLink, { href: item.href, className: ITEM, onClick: () => setOpen(false), children: item.label }) }, item.href))) })) : null, children] })] }));
|
|
57
|
+
return (_jsxs("div", { ref: wrapper, className: cx("relative", className), ...props, children: [_jsx("button", { ref: button, type: "button", "aria-expanded": open, "aria-controls": panelId, "aria-label": buttonLabel, onClick: () => setOpen(!open), className: cx("flex coarse:min-h-11 min-h-6 items-center gap-2 font-bold text-c1 text-sm transition-colors hover:text-h1", buttonClassName), children: label }), _jsxs("div", { id: panelId, hidden: !open, className: cx("absolute z-10 mt-2 w-44 flex-col gap-1 rounded-lg border border-b3 bg-b6 p-2 contrast-more:border-c4", open && "flex", align === "end" ? "right-0" : "left-0"), children: [items.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1", children: items.map((item) => (_jsx("li", { children: _jsx(AutoLink, { href: item.href, className: ITEM, onClick: () => setOpen(false), children: item.label }) }, item.href))) })) : null, children] })] }));
|
|
58
58
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* loads its own URL. Server-safe: the caller says which link is current.
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Mon Sep 28, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
10
|
import type { ComponentProps, ReactNode } from "react";
|
|
11
11
|
/** One tab: where it links, its text, and whether it is the page being shown. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../../utils/cx.js";
|
|
3
3
|
import { AutoLink } from "../basics/AutoLink.js";
|
|
4
|
-
const TAB = "rounded-full px-3 py-1 font-bold text-sm transition-colors";
|
|
4
|
+
const TAB = "inline-flex items-center rounded-full px-3 py-1 font-bold text-sm transition-colors coarse:min-h-11";
|
|
5
5
|
// Forced-colors mode drops the pill's background, so the current tab is underlined there.
|
|
6
6
|
const CURRENT = "bg-b3 text-c1 forced-colors:underline";
|
|
7
7
|
const OTHER = "text-c3 hover:text-c1";
|
package/dist/index.d.ts
CHANGED
|
@@ -23,8 +23,11 @@ export { Notice, type NoticeProps, type NoticeTone } from "./components/basics/N
|
|
|
23
23
|
export { PageHeader, type PageHeaderProps } from "./components/basics/PageHeader.js";
|
|
24
24
|
export { Prose, type ProseProps } from "./components/basics/Prose.js";
|
|
25
25
|
export { type TabItem, Tabs, type TabsProps } from "./components/basics/Tabs.js";
|
|
26
|
+
export { Text, type TextProps } from "./components/basics/Text.js";
|
|
26
27
|
export { TextLink, type TextLinkProps } from "./components/basics/TextLink.js";
|
|
27
28
|
export { tabId, tabPanelId } from "./components/basics/tabIds.js";
|
|
29
|
+
export { type TextClassOptions, type TextSize, type TextTone, textClasses, } from "./components/basics/textStyles.js";
|
|
30
|
+
export { useMotionAllowed } from "./components/basics/useMotionAllowed.js";
|
|
28
31
|
export { BrandPage, type BrandPageAsset, type BrandPageFont, type BrandPageProps, } from "./components/brand/BrandPage.js";
|
|
29
32
|
export { BrandSwatch, type BrandSwatchProps } from "./components/brand/BrandSwatch.js";
|
|
30
33
|
export { ContentIndex, type ContentIndexProps } from "./components/content/ContentIndex.js";
|
|
@@ -58,7 +61,7 @@ export { HaruhimeWordmark, type HaruhimeWordmarkProps, } from "./components/icon
|
|
|
58
61
|
export { HaruhimeWordmarkLink, type HaruhimeWordmarkLinkProps, } from "./components/icons/HaruhimeWordmarkLink.js";
|
|
59
62
|
export { JsonLd, type JsonLdProps } from "./components/meta/JsonLd.js";
|
|
60
63
|
export { type BeatmapStatKey, BeatmapStats, type BeatmapStatsProps, } from "./components/osu/BeatmapStats.js";
|
|
61
|
-
export { ModBadge, type ModBadgeProps } from "./components/osu/ModBadge.js";
|
|
64
|
+
export { ModBadge, type ModBadgeColor, type ModBadgeProps, } from "./components/osu/ModBadge.js";
|
|
62
65
|
export { PlayerCard, type PlayerCardProps, type PlayerTeam, } from "./components/osu/PlayerCard.js";
|
|
63
66
|
export { StarRating, type StarRatingProps } from "./components/osu/StarRating.js";
|
|
64
67
|
export { CommandPalette } from "./components/palette/CommandPalette.js";
|
package/dist/index.js
CHANGED
|
@@ -24,8 +24,11 @@ export { Notice } from "./components/basics/Notice.js";
|
|
|
24
24
|
export { PageHeader } from "./components/basics/PageHeader.js";
|
|
25
25
|
export { Prose } from "./components/basics/Prose.js";
|
|
26
26
|
export { Tabs } from "./components/basics/Tabs.js";
|
|
27
|
+
export { Text } from "./components/basics/Text.js";
|
|
27
28
|
export { TextLink } from "./components/basics/TextLink.js";
|
|
28
29
|
export { tabId, tabPanelId } from "./components/basics/tabIds.js";
|
|
30
|
+
export { textClasses, } from "./components/basics/textStyles.js";
|
|
31
|
+
export { useMotionAllowed } from "./components/basics/useMotionAllowed.js";
|
|
29
32
|
// Brand
|
|
30
33
|
export { BrandPage, } from "./components/brand/BrandPage.js";
|
|
31
34
|
export { BrandSwatch } from "./components/brand/BrandSwatch.js";
|
|
@@ -64,7 +67,7 @@ export { HaruhimeWordmarkLink, } from "./components/icons/HaruhimeWordmarkLink.j
|
|
|
64
67
|
export { JsonLd } from "./components/meta/JsonLd.js";
|
|
65
68
|
// osu!
|
|
66
69
|
export { BeatmapStats, } from "./components/osu/BeatmapStats.js";
|
|
67
|
-
export { ModBadge } from "./components/osu/ModBadge.js";
|
|
70
|
+
export { ModBadge, } from "./components/osu/ModBadge.js";
|
|
68
71
|
export { PlayerCard, } from "./components/osu/PlayerCard.js";
|
|
69
72
|
export { StarRating } from "./components/osu/StarRating.js";
|
|
70
73
|
// Palette
|
package/dist/theme.css
CHANGED
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
* (h2 lightness, default 45%) or --h1-l (h1 lightness, default 76%, which clears 4.5:1 on
|
|
14
14
|
* b5 at every hue) moved to keep text at 4.5:1; the README lists values. Load Nunito with next/font as the --font-nunito variable;
|
|
15
15
|
* without it the sans stack falls back to the system font.
|
|
16
|
+
*
|
|
17
|
+
* It also follows three browser settings: prefers-reduced-motion (one global rule,
|
|
18
|
+
* data-motion="essential" opts out), prefers-contrast: more (--contrast-lift) and a coarse
|
|
19
|
+
* pointer (the coarse: variant).
|
|
16
20
|
* @author David @dvhsh (https://dvh.sh)
|
|
17
21
|
* @created Wed Sep 23, 2026
|
|
18
|
-
* @modified
|
|
22
|
+
* @modified Sun Oct 4, 2026
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
25
|
/* This file ships as dist/theme.css, so './' is the package's compiled components. */
|
|
@@ -23,9 +27,21 @@
|
|
|
23
27
|
|
|
24
28
|
:root {
|
|
25
29
|
--hue: 333;
|
|
30
|
+
--contrast-lift: 0%;
|
|
26
31
|
color-scheme: dark;
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
/* More contrast: c2, c3, c4 and h1 get lighter and h2 darker by the lift. An app opts out with
|
|
35
|
+
--contrast-lift: 0%; the lift adds to its own --h1-l and --h2-l. */
|
|
36
|
+
@media (prefers-contrast: more) {
|
|
37
|
+
:root {
|
|
38
|
+
--contrast-lift: 8%;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* A touchscreen as the main pointer. The same query as Tailwind's pointer-coarse:, shorter. */
|
|
43
|
+
@custom-variant coarse (@media (pointer: coarse));
|
|
44
|
+
|
|
29
45
|
@theme inline {
|
|
30
46
|
--color-b1: hsl(var(--hue) 10% 40%);
|
|
31
47
|
--color-b2: hsl(var(--hue) 10% 30%);
|
|
@@ -35,16 +51,30 @@
|
|
|
35
51
|
--color-b6: hsl(var(--hue) 10% 10%);
|
|
36
52
|
|
|
37
53
|
--color-c1: hsl(var(--hue) 40% 100%);
|
|
38
|
-
--color-c2: hsl(var(--hue) 40% 90%);
|
|
39
|
-
--color-c3: hsl(var(--hue) 40% 80%);
|
|
40
|
-
--color-c4: hsl(var(--hue) 40% 70%);
|
|
54
|
+
--color-c2: hsl(var(--hue) 40% calc(var(--c2-l, 90%) + var(--contrast-lift)));
|
|
55
|
+
--color-c3: hsl(var(--hue) 40% calc(var(--c3-l, 80%) + var(--contrast-lift)));
|
|
56
|
+
--color-c4: hsl(var(--hue) 40% calc(var(--c4-l, 70%) + var(--contrast-lift)));
|
|
41
57
|
|
|
42
|
-
--color-h1: hsl(var(--hue) 100% var(--h1-l, 76%));
|
|
43
|
-
--color-h2: hsl(var(--hue) 50% var(--h2-l, 45%));
|
|
58
|
+
--color-h1: hsl(var(--hue) 100% calc(var(--h1-l, 76%) + var(--contrast-lift)));
|
|
59
|
+
--color-h2: hsl(var(--hue) 50% calc(var(--h2-l, 45%) - var(--contrast-lift) / 2));
|
|
44
60
|
|
|
45
61
|
--font-sans: var(--font-nunito), ui-sans-serif, system-ui, sans-serif;
|
|
46
62
|
}
|
|
47
63
|
|
|
64
|
+
/* Motion tokens. A plain @theme, not inline, so utilities read the variables and one change
|
|
65
|
+
reaches every user: duration-short (hovers, presses, color changes), duration-medium (panels,
|
|
66
|
+
dialogs), duration-long (large surfaces); ease-standard, ease-enter, ease-exit. */
|
|
67
|
+
@theme {
|
|
68
|
+
--transition-duration-short: 150ms;
|
|
69
|
+
--transition-duration-medium: 250ms;
|
|
70
|
+
--transition-duration-long: 400ms;
|
|
71
|
+
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
|
72
|
+
--ease-enter: cubic-bezier(0, 0, 0.2, 1);
|
|
73
|
+
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
|
74
|
+
--default-transition-duration: var(--transition-duration-short);
|
|
75
|
+
--default-transition-timing-function: var(--ease-standard);
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
/* CodeBlock reads these as Shiki's CSS-variables theme. Every token keeps 4.5:1 against both b6
|
|
49
79
|
(the block's background) and b4 (the highlighted-line tint) at every integer --hue. The keyword
|
|
50
80
|
and link tokens get their own hue-derived value instead of var(--color-h1): that variable's
|
|
@@ -69,4 +99,24 @@
|
|
|
69
99
|
outline-offset: 2px;
|
|
70
100
|
border-radius: 4px;
|
|
71
101
|
}
|
|
102
|
+
|
|
103
|
+
/* The one reduced-motion rule for the family: the browser setting, no in-app toggle. 0.01ms, not
|
|
104
|
+
0, so transitionend and animationend still fire. !important in the base layer beats every
|
|
105
|
+
normal declaration in later layers, so an app's own duration-* or animate-* can't escape it.
|
|
106
|
+
data-motion="essential" keeps motion that carries meaning. Pseudo-elements can't sit inside
|
|
107
|
+
:where() or :not(), so each gets its own selector. */
|
|
108
|
+
@media (prefers-reduced-motion: reduce) {
|
|
109
|
+
:where(*:not([data-motion="essential"], [data-motion="essential"] *)),
|
|
110
|
+
:where(*:not([data-motion="essential"], [data-motion="essential"] *))::before,
|
|
111
|
+
:where(*:not([data-motion="essential"], [data-motion="essential"] *))::after {
|
|
112
|
+
/* biome-ignore lint/complexity/noImportantStyles: reduced motion must beat utility and inline transitions */
|
|
113
|
+
animation-duration: 0.01ms !important;
|
|
114
|
+
/* biome-ignore lint/complexity/noImportantStyles: reduced motion must beat utility and inline transitions */
|
|
115
|
+
animation-iteration-count: 1 !important;
|
|
116
|
+
/* biome-ignore lint/complexity/noImportantStyles: reduced motion must beat utility and inline transitions */
|
|
117
|
+
transition-duration: 0.01ms !important;
|
|
118
|
+
/* biome-ignore lint/complexity/noImportantStyles: reduced motion must beat utility and inline transitions */
|
|
119
|
+
scroll-behavior: auto !important;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
72
122
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haruhimemoe/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "React components for the haruhime.moe osu! tools on Next.js: the osu!-web-style palette as a Tailwind theme, buttons, cards, form fields and confirms, filter controls, tables, osu! beatmap display pieces and player cards, a command palette, content section tooling, brand pages and the site header and footer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|