@haruhimemoe/ui 0.2.0 → 0.4.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 +50 -1
- package/README.md +265 -29
- package/dist/components/actions/AsyncButton.d.ts +30 -0
- package/dist/components/actions/AsyncButton.js +48 -0
- package/dist/components/actions/CopyButton.d.ts +3 -3
- package/dist/components/actions/CopyButton.js +9 -12
- package/dist/components/actions/InlineConfirm.d.ts +45 -0
- package/dist/components/actions/InlineConfirm.js +80 -0
- package/dist/components/actions/Pagination.d.ts +40 -17
- package/dist/components/actions/Pagination.js +25 -18
- package/dist/components/actions/PaginationButton.d.ts +25 -0
- package/dist/components/actions/PaginationButton.js +23 -0
- package/dist/components/actions/PaginationStatus.d.ts +7 -4
- package/dist/components/actions/PaginationStatus.js +7 -5
- package/dist/components/actions/StatusOutput.d.ts +20 -0
- package/dist/components/actions/StatusOutput.js +9 -0
- package/dist/components/actions/pages.d.ts +21 -0
- package/dist/components/actions/pages.js +21 -0
- package/dist/components/actions/useLatestStatus.d.ts +26 -0
- package/dist/components/actions/useLatestStatus.js +32 -0
- package/dist/components/basics/AutoLink.d.ts +21 -0
- package/dist/components/basics/AutoLink.js +26 -0
- package/dist/components/basics/Badge.d.ts +22 -0
- package/dist/components/basics/Badge.js +18 -0
- package/dist/components/basics/ButtonLink.d.ts +5 -6
- package/dist/components/basics/ButtonLink.js +5 -11
- package/dist/components/basics/Card.d.ts +4 -3
- package/dist/components/basics/Card.js +3 -2
- package/dist/components/basics/Disclosure.d.ts +34 -0
- package/dist/components/basics/Disclosure.js +30 -0
- package/dist/components/basics/Notice.d.ts +2 -1
- package/dist/components/basics/TextLink.d.ts +21 -0
- package/dist/components/basics/TextLink.js +19 -0
- package/dist/components/basics/buttonStyles.d.ts +3 -1
- package/dist/components/basics/buttonStyles.js +1 -1
- package/dist/components/basics/cardStyles.d.ts +14 -0
- package/dist/components/basics/cardStyles.js +12 -0
- package/dist/components/basics/linkStyles.d.ts +23 -0
- package/dist/components/basics/linkStyles.js +23 -0
- package/dist/components/filters/Chip.d.ts +13 -5
- package/dist/components/filters/Chip.js +20 -11
- package/dist/components/filters/ChipGroup.d.ts +11 -12
- package/dist/components/filters/ChipGroup.js +5 -10
- package/dist/components/filters/ChoiceChips.d.ts +38 -0
- package/dist/components/filters/ChoiceChips.js +31 -0
- package/dist/components/filters/FilterPanel.d.ts +5 -4
- package/dist/components/filters/FilterPanel.js +5 -4
- package/dist/components/filters/FilterRow.d.ts +1 -1
- package/dist/components/filters/FilterRow.js +3 -2
- package/dist/components/filters/GroupFrame.d.ts +27 -0
- package/dist/components/filters/GroupFrame.js +27 -0
- package/dist/components/filters/RangeBox.d.ts +28 -0
- package/dist/components/filters/RangeBox.js +41 -0
- package/dist/components/filters/RangeSlider.d.ts +13 -14
- package/dist/components/filters/RangeSlider.js +40 -103
- package/dist/components/filters/chipStyles.d.ts +17 -0
- package/dist/components/filters/chipStyles.js +17 -0
- package/dist/components/filters/rangeMath.d.ts +106 -0
- package/dist/components/filters/rangeMath.js +146 -0
- package/dist/components/forms/Checkbox.d.ts +5 -4
- package/dist/components/forms/Checkbox.js +3 -3
- package/dist/components/forms/FieldFrame.d.ts +28 -2
- package/dist/components/forms/FieldFrame.js +14 -1
- package/dist/components/forms/RadioGroup.d.ts +46 -0
- package/dist/components/forms/RadioGroup.js +32 -0
- package/dist/components/forms/Select.d.ts +1 -1
- package/dist/components/forms/Select.js +2 -2
- package/dist/components/forms/TextInput.d.ts +1 -1
- package/dist/components/forms/TextInput.js +2 -2
- package/dist/components/forms/Textarea.d.ts +1 -1
- package/dist/components/forms/Textarea.js +2 -2
- package/dist/components/forms/TypeToConfirm.d.ts +40 -0
- package/dist/components/forms/TypeToConfirm.js +46 -0
- package/dist/components/forms/fieldStyles.d.ts +5 -2
- package/dist/components/forms/fieldStyles.js +5 -2
- package/dist/components/icons/DiscordIcon.d.ts +21 -0
- package/dist/components/icons/DiscordIcon.js +10 -0
- package/dist/components/{actions → meta}/JsonLd.d.ts +2 -2
- package/dist/components/osu/BeatmapStats.d.ts +36 -0
- package/dist/components/osu/BeatmapStats.js +44 -0
- package/dist/components/osu/ModBadge.d.ts +23 -0
- package/dist/components/osu/ModBadge.js +22 -0
- package/dist/components/osu/StarRating.d.ts +26 -0
- package/dist/components/osu/StarRating.js +17 -0
- package/dist/components/osu/starColors.d.ts +23 -0
- package/dist/components/osu/starColors.js +48 -0
- package/dist/components/shell/HeaderMenu.d.ts +39 -0
- package/dist/components/shell/HeaderMenu.js +58 -0
- package/dist/components/shell/LinkNote.d.ts +16 -0
- package/dist/components/shell/LinkNote.js +17 -0
- package/dist/components/shell/LinkTabs.d.ts +29 -0
- package/dist/components/shell/LinkTabs.js +15 -0
- package/dist/components/shell/NavItem.d.ts +1 -7
- package/dist/components/shell/NavItem.js +5 -10
- package/dist/components/shell/NavLinks.d.ts +1 -1
- package/dist/components/shell/NavLinks.js +3 -3
- package/dist/components/shell/NavListClient.d.ts +1 -1
- package/dist/components/shell/NavListClient.js +4 -4
- package/dist/components/shell/SiteFooter.d.ts +11 -6
- package/dist/components/shell/SiteFooter.js +18 -7
- package/dist/components/shell/links.d.ts +8 -1
- package/dist/components/shell/links.js +8 -1
- package/dist/components/tables/TBody.d.ts +16 -0
- package/dist/components/tables/TBody.js +10 -0
- package/dist/components/tables/THead.d.ts +16 -0
- package/dist/components/tables/THead.js +10 -0
- package/dist/components/tables/Table.d.ts +26 -0
- package/dist/components/tables/Table.js +12 -0
- package/dist/components/tables/Td.d.ts +19 -0
- package/dist/components/tables/Td.js +11 -0
- package/dist/components/tables/Th.d.ts +20 -0
- package/dist/components/tables/Th.js +11 -0
- package/dist/components/tables/tableStyles.d.ts +12 -0
- package/dist/components/tables/tableStyles.js +12 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.js +28 -2
- package/dist/utils/href.d.ts +7 -4
- package/dist/utils/href.js +23 -7
- package/package.json +3 -3
- package/dist/components/shell/AutoLink.d.ts +0 -19
- package/dist/components/shell/AutoLink.js +0 -23
- /package/dist/components/{actions → meta}/JsonLd.js +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/filters/rangeMath.ts
|
|
3
|
+
* @desc RangeSlider's math (internal), free of React so it tests on its own: guarded bounds and
|
|
4
|
+
* step, the incoming range normalized, snapping, the thumbs' keyboard moves, which end a
|
|
5
|
+
* drag moves, and what a typed box or a thumb move commits.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
const decimalsOf = (n) => {
|
|
11
|
+
const text = String(n);
|
|
12
|
+
const dot = text.indexOf(".");
|
|
13
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
14
|
+
};
|
|
15
|
+
const clamp = (n, low, high) => Math.min(Math.max(n, low), high);
|
|
16
|
+
/**
|
|
17
|
+
* @function defaultParse
|
|
18
|
+
* @param text {string} a typed value, without a trailing "+"
|
|
19
|
+
* @returns {number | null} the number, reading a comma as the decimal point (comma-locale keypads
|
|
20
|
+
* type "5,5"), or null when it isn't one
|
|
21
|
+
*/
|
|
22
|
+
export const defaultParse = (text) => {
|
|
23
|
+
const n = Number(text.replace(",", "."));
|
|
24
|
+
return Number.isFinite(n) ? n : null;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* @function rangeBounds
|
|
28
|
+
* @param min {number} the low bound as passed
|
|
29
|
+
* @param max {number} the high bound as passed
|
|
30
|
+
* @param step {number} the step as passed
|
|
31
|
+
* @returns {RangeBounds} the bounds in order (swapped when `max < min`) and the step, or 1 when
|
|
32
|
+
* the step is 0, negative or not finite, so snapping never divides by zero
|
|
33
|
+
*/
|
|
34
|
+
export const rangeBounds = (min, max, step) => {
|
|
35
|
+
const [low, high] = min <= max ? [min, max] : [max, min];
|
|
36
|
+
const safeStep = Number.isFinite(step) && step > 0 ? step : 1;
|
|
37
|
+
return {
|
|
38
|
+
min: low,
|
|
39
|
+
max: high,
|
|
40
|
+
step: safeStep,
|
|
41
|
+
decimals: Math.max(decimalsOf(safeStep), decimalsOf(low)),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* @function normalizeRange
|
|
46
|
+
* @param value {Readonly<RangeSliderValue>} the range as it came in (from a URL, say)
|
|
47
|
+
* @param bounds {RangeBounds} the guarded bounds
|
|
48
|
+
* @param openEnded {boolean} whether a top end at `max` means no upper limit
|
|
49
|
+
* @returns {RangeState} both ends inside the bounds with low <= high. A NaN or infinite end means
|
|
50
|
+
* no limit on that end, like an empty box.
|
|
51
|
+
*/
|
|
52
|
+
export const normalizeRange = (value, { min, max }, openEnded) => {
|
|
53
|
+
const low = Number.isFinite(value[0]) ? clamp(value[0], min, max) : min;
|
|
54
|
+
const top = value[1] !== null && Number.isFinite(value[1]) ? value[1] : null;
|
|
55
|
+
const highOpen = openEnded && (top === null || top >= max);
|
|
56
|
+
const high = top === null ? max : clamp(top, low, max);
|
|
57
|
+
return { low, high, highOpen, highOut: highOpen ? null : high };
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* @function snap
|
|
61
|
+
* @param n {number} a value
|
|
62
|
+
* @param bounds {RangeBounds} the guarded bounds
|
|
63
|
+
* @returns {number} the nearest step from `min`, rounded to the step's decimals (no float noise)
|
|
64
|
+
*/
|
|
65
|
+
export const snap = (n, { min, step, decimals }) => Number((min + Math.round((n - min) / step) * step).toFixed(decimals));
|
|
66
|
+
/**
|
|
67
|
+
* @function percent
|
|
68
|
+
* @param n {number} a value inside the bounds
|
|
69
|
+
* @param bounds {RangeBounds} the guarded bounds
|
|
70
|
+
* @returns {number} where it sits on the track, 0 to 100 (0 when `min` equals `max`)
|
|
71
|
+
*/
|
|
72
|
+
export const percent = (n, { min, max }) => max === min ? 0 : ((n - min) / (max - min)) * 100;
|
|
73
|
+
/**
|
|
74
|
+
* @function thumbTarget
|
|
75
|
+
* @param key {string} the key pressed on a thumb
|
|
76
|
+
* @param current {number} that thumb's value
|
|
77
|
+
* @param bounds {RangeBounds} the guarded bounds
|
|
78
|
+
* @returns {number | undefined} where the key sends the thumb (arrows one step, Page Up/Down ten
|
|
79
|
+
* steps, Home and End to the bounds), or undefined for any other key
|
|
80
|
+
*/
|
|
81
|
+
export const thumbTarget = (key, current, { min, max, step }) => {
|
|
82
|
+
const moves = {
|
|
83
|
+
ArrowLeft: current - step,
|
|
84
|
+
ArrowDown: current - step,
|
|
85
|
+
ArrowRight: current + step,
|
|
86
|
+
ArrowUp: current + step,
|
|
87
|
+
PageDown: current - step * 10,
|
|
88
|
+
PageUp: current + step * 10,
|
|
89
|
+
Home: min,
|
|
90
|
+
End: max,
|
|
91
|
+
};
|
|
92
|
+
return moves[key];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* @function dragEnd
|
|
96
|
+
* @param end {End} the thumb the drag started on
|
|
97
|
+
* @param n {number} the drag's first value
|
|
98
|
+
* @param state {RangeState} the range before the drag
|
|
99
|
+
* @returns {End} the end the drag moves. With both thumbs on one value, only the top thumb can be
|
|
100
|
+
* grabbed and it can only go one way, so a first move toward the other side moves the
|
|
101
|
+
* other end.
|
|
102
|
+
*/
|
|
103
|
+
export const dragEnd = (end, n, { low, high }) => {
|
|
104
|
+
if (low !== high)
|
|
105
|
+
return end;
|
|
106
|
+
if (end === "high" && n < low)
|
|
107
|
+
return "low";
|
|
108
|
+
if (end === "low" && n > high)
|
|
109
|
+
return "high";
|
|
110
|
+
return end;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* @function nextRange
|
|
114
|
+
* @param end {End} the end that moves
|
|
115
|
+
* @param n {number} where it moves to, before snapping
|
|
116
|
+
* @param state {RangeState} the range now
|
|
117
|
+
* @param bounds {RangeBounds} the guarded bounds
|
|
118
|
+
* @param openEnded {boolean} whether a top end at `max` reports null
|
|
119
|
+
* @returns {RangeSliderValue | null} the new range (snapped, the thumbs uncrossed, an open top
|
|
120
|
+
* end as null), or null when nothing changed
|
|
121
|
+
*/
|
|
122
|
+
export const nextRange = (end, n, state, bounds, openEnded) => {
|
|
123
|
+
if (end === "low") {
|
|
124
|
+
const next = clamp(snap(n, bounds), bounds.min, state.high);
|
|
125
|
+
return next === state.low ? null : [next, state.highOut];
|
|
126
|
+
}
|
|
127
|
+
const snapped = clamp(snap(n, bounds), state.low, bounds.max);
|
|
128
|
+
const next = openEnded && (n >= bounds.max || snapped >= bounds.max) ? null : snapped;
|
|
129
|
+
return next === state.highOut ? null : [state.low, next];
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* @function readDraft
|
|
133
|
+
* @param draft {string} what was typed in an end's box
|
|
134
|
+
* @param end {End} which box
|
|
135
|
+
* @param bounds {RangeBounds} the guarded bounds
|
|
136
|
+
* @param parse {(text: string) => number | null} the caller's reader
|
|
137
|
+
* @returns {number | null} the value to commit: an empty box is no limit on that end (`min` or
|
|
138
|
+
* `max`), a trailing "+" is ignored, and text `parse` can't read gives null
|
|
139
|
+
*/
|
|
140
|
+
export const readDraft = (draft, end, { min, max }, parse) => {
|
|
141
|
+
const trimmed = draft.trim().replace(/\+$/, "").trim();
|
|
142
|
+
if (trimmed === "")
|
|
143
|
+
return end === "low" ? min : max;
|
|
144
|
+
const n = parse(trimmed);
|
|
145
|
+
return n !== null && Number.isFinite(n) ? n : null;
|
|
146
|
+
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/components/forms/Checkbox.tsx
|
|
3
3
|
* @desc Checkbox with a bold label and an inline hint (the packs download-options look). The whole
|
|
4
|
-
* row toggles it; the label alone is its accessible name
|
|
5
|
-
* Server-safe: the required id names the label,
|
|
4
|
+
* row toggles it; the label alone is its accessible name (plus any aria-labelledby the
|
|
5
|
+
* caller adds) and the hint its description. Server-safe: the required id names the label,
|
|
6
|
+
* hint and error.
|
|
6
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
8
|
* @created Wed Sep 23, 2026
|
|
8
|
-
* @modified
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
9
10
|
*/
|
|
10
11
|
import type { ComponentProps } from "react";
|
|
11
12
|
import { type FieldProps } from "./FieldFrame.js";
|
|
@@ -17,4 +18,4 @@ export type CheckboxProps = Omit<ComponentProps<"input">, "id" | "type"> & Field
|
|
|
17
18
|
* plus id, label, hint, error and wrapperClassName; `className` goes on the `<input>`
|
|
18
19
|
* @returns {JSX.Element} the checkbox row, and the error under it when given
|
|
19
20
|
*/
|
|
20
|
-
export declare function Checkbox({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }: CheckboxProps): import("react").JSX.Element;
|
|
21
|
+
export declare function Checkbox({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-labelledby": labelledBy, ...props }: CheckboxProps): import("react").JSX.Element;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../../utils/cx.js";
|
|
3
|
-
import { FieldError,
|
|
3
|
+
import { FieldError, fieldControlProps, hintId } from "./FieldFrame.js";
|
|
4
4
|
/**
|
|
5
5
|
* @function Checkbox
|
|
6
6
|
* @param props {CheckboxProps} native input props (`checked`, `defaultChecked`, `onChange`...),
|
|
7
7
|
* plus id, label, hint, error and wrapperClassName; `className` goes on the `<input>`
|
|
8
8
|
* @returns {JSX.Element} the checkbox row, and the error under it when given
|
|
9
9
|
*/
|
|
10
|
-
export function Checkbox({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }) {
|
|
10
|
+
export function Checkbox({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-labelledby": labelledBy, ...props }) {
|
|
11
11
|
const labelId = `${id}-label`;
|
|
12
|
-
return (_jsxs("div", { className: cx("flex flex-col gap-1", wrapperClassName), children: [_jsxs("label", { htmlFor: id, className: "flex items-start gap-2 text-sm", children: [_jsx("input", { ...props, type: "checkbox",
|
|
12
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-1", wrapperClassName), children: [_jsxs("label", { htmlFor: id, className: "flex items-start gap-2 text-sm", children: [_jsx("input", { ...props, type: "checkbox", ...fieldControlProps({ id, hint, error, describedBy, invalid }), "aria-labelledby": labelledBy ? `${labelId} ${labelledBy}` : labelId, className: cx("mt-1 accent-h1", className) }), _jsxs("span", { children: [_jsx("span", { id: labelId, className: "font-bold text-c1", children: label }), hint ? (_jsxs("span", { className: "text-c3", children: [" · ", _jsx("span", { id: hintId(id), children: hint })] })) : null] })] }), _jsx(FieldError, { id: id, error: error })] }));
|
|
13
13
|
}
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* @desc Internal layout shared by the form fields: label on top, the control, then the hint and
|
|
4
4
|
* the error. Ids derive from the control's required id, so the fields need no useId and stay
|
|
5
5
|
* server components. The hint and error sit in `<div>`s, so they can hold a list.
|
|
6
|
+
* `fieldControlProps` is the one rule for the control's id, aria-describedby and
|
|
7
|
+
* aria-invalid.
|
|
6
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
9
|
* @created Wed Sep 23, 2026
|
|
8
|
-
* @modified
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
9
11
|
*/
|
|
10
|
-
import type { ReactNode } from "react";
|
|
12
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
11
13
|
/** The label, hint and error every form field takes, keyed on the control's required id. */
|
|
12
14
|
export type FieldProps = {
|
|
13
15
|
/** The control's id. The label points at it; the hint and error get `<id>-hint`, `<id>-error`. */
|
|
@@ -42,6 +44,30 @@ export declare const errorId: (id: string) => string;
|
|
|
42
44
|
* @returns {string | undefined} the aria-describedby value, or undefined when there is nothing
|
|
43
45
|
*/
|
|
44
46
|
export declare const fieldDescribedBy: (id: string, hint: ReactNode, error: ReactNode, extra: string | undefined) => string | undefined;
|
|
47
|
+
/** What a field knows when it wires up its control: ids, texts, and the caller's own ARIA. */
|
|
48
|
+
type FieldControlInput = {
|
|
49
|
+
id: string;
|
|
50
|
+
hint: ReactNode;
|
|
51
|
+
error: ReactNode;
|
|
52
|
+
/** The caller's own aria-describedby, kept after the hint and error ids. */
|
|
53
|
+
describedBy: string | undefined;
|
|
54
|
+
/** The caller's own aria-invalid, used when there is no error. */
|
|
55
|
+
invalid: ComponentProps<"input">["aria-invalid"];
|
|
56
|
+
};
|
|
57
|
+
/** The id and ARIA props a field puts on its control. */
|
|
58
|
+
type FieldControlProps = {
|
|
59
|
+
id: string;
|
|
60
|
+
"aria-describedby": string | undefined;
|
|
61
|
+
"aria-invalid": FieldControlInput["invalid"];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* @function fieldControlProps
|
|
65
|
+
* @param input {FieldControlInput} the control's id, its hint and error, and the caller's
|
|
66
|
+
* aria-describedby and aria-invalid
|
|
67
|
+
* @returns {FieldControlProps} the props every field puts on its control: an error sets
|
|
68
|
+
* aria-invalid, and the hint and error are linked before the caller's own descriptions
|
|
69
|
+
*/
|
|
70
|
+
export declare const fieldControlProps: ({ id, hint, error, describedBy, invalid, }: FieldControlInput) => FieldControlProps;
|
|
45
71
|
/**
|
|
46
72
|
* @function FieldError
|
|
47
73
|
* @param props {{ id: string; error?: ReactNode }} the control's id and the error, if any
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { FIELD_LABEL } from "./fieldStyles.js";
|
|
3
4
|
/**
|
|
4
5
|
* @function hintId
|
|
5
6
|
* @param id {string} the control's id
|
|
@@ -24,6 +25,18 @@ export const fieldDescribedBy = (id, hint, error, extra) =>
|
|
|
24
25
|
// A plain join, not cx: these are ids, and cx would treat an id like "text-hint" as a class.
|
|
25
26
|
[hint ? hintId(id) : null, error ? errorId(id) : null, extra].filter(Boolean).join(" ") ||
|
|
26
27
|
undefined;
|
|
28
|
+
/**
|
|
29
|
+
* @function fieldControlProps
|
|
30
|
+
* @param input {FieldControlInput} the control's id, its hint and error, and the caller's
|
|
31
|
+
* aria-describedby and aria-invalid
|
|
32
|
+
* @returns {FieldControlProps} the props every field puts on its control: an error sets
|
|
33
|
+
* aria-invalid, and the hint and error are linked before the caller's own descriptions
|
|
34
|
+
*/
|
|
35
|
+
export const fieldControlProps = ({ id, hint, error, describedBy, invalid, }) => ({
|
|
36
|
+
id,
|
|
37
|
+
"aria-describedby": fieldDescribedBy(id, hint, error, describedBy),
|
|
38
|
+
"aria-invalid": error ? true : invalid,
|
|
39
|
+
});
|
|
27
40
|
/**
|
|
28
41
|
* @function FieldError
|
|
29
42
|
* @param props {{ id: string; error?: ReactNode }} the control's id and the error, if any
|
|
@@ -40,5 +53,5 @@ export function FieldError({ id, error }) {
|
|
|
40
53
|
* @returns {JSX.Element} the label, control, hint and error stacked in a column
|
|
41
54
|
*/
|
|
42
55
|
export function FieldFrame({ id, label, hint, error, className, children }) {
|
|
43
|
-
return (_jsxs("div", { className: cx("flex flex-col gap-1", className), children: [_jsx("label", { htmlFor: id, className:
|
|
56
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-1", className), children: [_jsx("label", { htmlFor: id, className: FIELD_LABEL, children: label }), children, hint ? (_jsx("div", { id: hintId(id), className: "text-c4 text-xs", children: hint })) : null, _jsx(FieldError, { id: id, error: error })] }));
|
|
44
57
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/RadioGroup.tsx
|
|
3
|
+
* @desc A native radio group on the Checkbox look: a fieldset named by its legend, one radio per
|
|
4
|
+
* option with a bold label and an inline hint, then the group's hint and error. Each
|
|
5
|
+
* option's label is its accessible name and its hint the description; the group's hint and
|
|
6
|
+
* error describe the fieldset, and an error marks it invalid. Arrow keys move and pick, as
|
|
7
|
+
* native radios do. Controlled (`value`) or uncontrolled (`defaultValue`).
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Mon Sep 28, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
13
|
+
/** One radio: its value, its label, an optional inline hint, and whether it can be picked. */
|
|
14
|
+
export type RadioOption = {
|
|
15
|
+
value: string;
|
|
16
|
+
label: ReactNode;
|
|
17
|
+
hint?: ReactNode;
|
|
18
|
+
disabled?: boolean | undefined;
|
|
19
|
+
};
|
|
20
|
+
/** Every native `<fieldset>` prop except `onChange` and `children`, plus the group's parts. */
|
|
21
|
+
export type RadioGroupProps = Omit<ComponentProps<"fieldset">, "onChange" | "children" | "defaultValue"> & {
|
|
22
|
+
/** The legend, which names the group. */
|
|
23
|
+
label: ReactNode;
|
|
24
|
+
options: readonly RadioOption[];
|
|
25
|
+
/** The radios' name, for a form (default: a generated one). */
|
|
26
|
+
name?: string | undefined;
|
|
27
|
+
/** The picked value, when the caller holds it. */
|
|
28
|
+
value?: string | undefined;
|
|
29
|
+
/** The value picked at first, when the group holds it. */
|
|
30
|
+
defaultValue?: string | undefined;
|
|
31
|
+
/** Called with the picked option's value. */
|
|
32
|
+
onChange?: ((value: string) => void) | undefined;
|
|
33
|
+
/** Help text under the options, linked to the group with aria-describedby. */
|
|
34
|
+
hint?: ReactNode;
|
|
35
|
+
/** Error text under the options (a list is fine). Marks the group invalid and links it. */
|
|
36
|
+
error?: ReactNode;
|
|
37
|
+
/** Every radio gets `required`, so a form won't submit without a pick. */
|
|
38
|
+
required?: boolean | undefined;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* @function RadioGroup
|
|
42
|
+
* @param props {RadioGroupProps} the legend, options, value or defaultValue, a change handler,
|
|
43
|
+
* the group's hint and error, plus native fieldset props (`disabled` turns off every radio)
|
|
44
|
+
* @returns {JSX.Element} a `<fieldset>` with its `<legend>`, the radios, and the hint and error
|
|
45
|
+
*/
|
|
46
|
+
export declare function RadioGroup({ label, options, name, value, defaultValue, onChange, hint, error, required, className, "aria-describedby": describedBy, ...props }: RadioGroupProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/RadioGroup.tsx
|
|
3
|
+
* @desc A native radio group on the Checkbox look: a fieldset named by its legend, one radio per
|
|
4
|
+
* option with a bold label and an inline hint, then the group's hint and error. Each
|
|
5
|
+
* option's label is its accessible name and its hint the description; the group's hint and
|
|
6
|
+
* error describe the fieldset, and an error marks it invalid. Arrow keys move and pick, as
|
|
7
|
+
* native radios do. Controlled (`value`) or uncontrolled (`defaultValue`).
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Mon Sep 28, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
"use client";
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { useId } from "react";
|
|
15
|
+
import { cx } from "../../utils/cx.js";
|
|
16
|
+
import { FieldError, fieldDescribedBy, hintId } from "./FieldFrame.js";
|
|
17
|
+
import { FIELD_LABEL } from "./fieldStyles.js";
|
|
18
|
+
/**
|
|
19
|
+
* @function RadioGroup
|
|
20
|
+
* @param props {RadioGroupProps} the legend, options, value or defaultValue, a change handler,
|
|
21
|
+
* the group's hint and error, plus native fieldset props (`disabled` turns off every radio)
|
|
22
|
+
* @returns {JSX.Element} a `<fieldset>` with its `<legend>`, the radios, and the hint and error
|
|
23
|
+
*/
|
|
24
|
+
export function RadioGroup({ label, options, name, value, defaultValue, onChange, hint, error, required, className, "aria-describedby": describedBy, ...props }) {
|
|
25
|
+
const id = useId();
|
|
26
|
+
return (_jsxs("fieldset", { "aria-describedby": fieldDescribedBy(id, hint, error, describedBy), className: cx("flex flex-col gap-2", className), ...props, children: [_jsx("legend", { className: cx("mb-1", FIELD_LABEL), children: label }), options.map((option, i) => {
|
|
27
|
+
const optionId = `${id}-${i}`;
|
|
28
|
+
return (_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "radio", id: optionId, name: name ?? id, value: option.value, ...(value === undefined
|
|
29
|
+
? { defaultChecked: option.value === defaultValue }
|
|
30
|
+
: { checked: option.value === value }), disabled: option.disabled, required: required, "aria-invalid": error ? true : undefined, "aria-labelledby": `${optionId}-label`, "aria-describedby": option.hint ? hintId(optionId) : undefined, onChange: () => onChange?.(option.value), className: "mt-1 accent-h1" }), _jsxs("span", { children: [_jsx("span", { id: `${optionId}-label`, className: "font-bold text-c1", children: option.label }), option.hint ? (_jsxs("span", { className: "text-c3", children: [" · ", _jsx("span", { id: hintId(optionId), children: option.hint })] })) : null] })] }, option.value));
|
|
31
|
+
}), hint ? (_jsx("div", { id: hintId(id), className: "text-c4 text-xs", children: hint })) : null, _jsx(FieldError, { id: id, error: error })] }));
|
|
32
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* through aria-describedby and aria-invalid. Options come in as children. Server-safe.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Wed Sep 23, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
8
|
*/
|
|
9
9
|
import type { ComponentProps } from "react";
|
|
10
10
|
import { type FieldProps } from "./FieldFrame.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { FieldFrame,
|
|
2
|
+
import { FieldFrame, fieldControlProps } from "./FieldFrame.js";
|
|
3
3
|
import { fieldClasses } from "./fieldStyles.js";
|
|
4
4
|
/**
|
|
5
5
|
* @function Select
|
|
@@ -8,5 +8,5 @@ import { fieldClasses } from "./fieldStyles.js";
|
|
|
8
8
|
* @returns {JSX.Element} a label, the select, and the hint and error when given
|
|
9
9
|
*/
|
|
10
10
|
export function Select({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }) {
|
|
11
|
-
return (_jsx(FieldFrame, { id: id, label: label, hint: hint, error: error, className: wrapperClassName, children: _jsx("select", { ...props,
|
|
11
|
+
return (_jsx(FieldFrame, { id: id, label: label, hint: hint, error: error, className: wrapperClassName, children: _jsx("select", { ...props, ...fieldControlProps({ id, hint, error, describedBy, invalid }), className: fieldClasses(className) }) }));
|
|
12
12
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* aria-describedby and aria-invalid. Server-safe: the required id names the hint and error.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Wed Sep 23, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
8
|
*/
|
|
9
9
|
import type { ComponentProps } from "react";
|
|
10
10
|
import { type FieldProps } from "./FieldFrame.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { FieldFrame,
|
|
2
|
+
import { FieldFrame, fieldControlProps } from "./FieldFrame.js";
|
|
3
3
|
import { fieldClasses } from "./fieldStyles.js";
|
|
4
4
|
/**
|
|
5
5
|
* @function TextInput
|
|
@@ -8,5 +8,5 @@ import { fieldClasses } from "./fieldStyles.js";
|
|
|
8
8
|
* @returns {JSX.Element} a label, the input, and the hint and error when given
|
|
9
9
|
*/
|
|
10
10
|
export function TextInput({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }) {
|
|
11
|
-
return (_jsx(FieldFrame, { id: id, label: label, hint: hint, error: error, className: wrapperClassName, children: _jsx("input", { ...props,
|
|
11
|
+
return (_jsx(FieldFrame, { id: id, label: label, hint: hint, error: error, className: wrapperClassName, children: _jsx("input", { ...props, ...fieldControlProps({ id, hint, error, describedBy, invalid }), className: fieldClasses(className) }) }));
|
|
12
12
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* an optional hint and error wired through aria-describedby and aria-invalid. Server-safe.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Wed Sep 23, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
8
|
*/
|
|
9
9
|
import type { ComponentProps } from "react";
|
|
10
10
|
import { type FieldProps } from "./FieldFrame.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../../utils/cx.js";
|
|
3
|
-
import { FieldFrame,
|
|
3
|
+
import { FieldFrame, fieldControlProps } from "./FieldFrame.js";
|
|
4
4
|
import { fieldClasses } from "./fieldStyles.js";
|
|
5
5
|
/**
|
|
6
6
|
* @function Textarea
|
|
@@ -9,5 +9,5 @@ import { fieldClasses } from "./fieldStyles.js";
|
|
|
9
9
|
* @returns {JSX.Element} a label, the textarea, and the hint and error when given
|
|
10
10
|
*/
|
|
11
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,
|
|
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)) }) }));
|
|
13
13
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/TypeToConfirm.tsx
|
|
3
|
+
* @desc A confirm for something that can't be undone (delete a pool, hand it to someone else):
|
|
4
|
+
* a form whose submit button stays off until the expected name is typed exactly, with no
|
|
5
|
+
* autocomplete or spellcheck in the way. Enter submits once it matches. The pools
|
|
6
|
+
* delete and transfer forms, as one component.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
12
|
+
import type { ButtonVariant } from "../basics/buttonStyles.js";
|
|
13
|
+
/** Every native `<form>` prop except `onSubmit`, plus the name to type and the action. */
|
|
14
|
+
export type TypeToConfirmProps = Omit<ComponentProps<"form">, "onSubmit"> & {
|
|
15
|
+
/** The text field's id; its hint and error get `<id>-hint` and `<id>-error`. */
|
|
16
|
+
id: string;
|
|
17
|
+
/** What has to be typed, exactly (spaces around it don't count). */
|
|
18
|
+
expected: string;
|
|
19
|
+
/** The field's label. Default "Type <expected> to confirm". */
|
|
20
|
+
label?: ReactNode;
|
|
21
|
+
/** Help text under the field. */
|
|
22
|
+
hint?: ReactNode;
|
|
23
|
+
/** Error text under the field, e.g. when the action failed. */
|
|
24
|
+
error?: ReactNode;
|
|
25
|
+
/** The submit button's text, e.g. "Delete this pool". */
|
|
26
|
+
submitLabel: ReactNode;
|
|
27
|
+
/** The submit button's text while `onConfirm` runs. Defaults to `submitLabel`. */
|
|
28
|
+
pendingLabel?: ReactNode;
|
|
29
|
+
/** Runs on submit once the text matches. */
|
|
30
|
+
onConfirm: () => void | Promise<void>;
|
|
31
|
+
/** The submit button's look (default "secondary"). */
|
|
32
|
+
variant?: ButtonVariant | undefined;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* @function TypeToConfirm
|
|
36
|
+
* @param props {TypeToConfirmProps} the id, the expected text, the labels, the action, and native
|
|
37
|
+
* form props; children show above the field (what the action does)
|
|
38
|
+
* @returns {JSX.Element} a `<form>` with the children, the text field and the submit button
|
|
39
|
+
*/
|
|
40
|
+
export declare function TypeToConfirm({ id, expected, label, hint, error, submitLabel, pendingLabel, onConfirm, variant, className, children, ...props }: TypeToConfirmProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/TypeToConfirm.tsx
|
|
3
|
+
* @desc A confirm for something that can't be undone (delete a pool, hand it to someone else):
|
|
4
|
+
* a form whose submit button stays off until the expected name is typed exactly, with no
|
|
5
|
+
* autocomplete or spellcheck in the way. Enter submits once it matches. The pools
|
|
6
|
+
* delete and transfer forms, as one component.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useRef, useState } from "react";
|
|
14
|
+
import { cx } from "../../utils/cx.js";
|
|
15
|
+
import { Button } from "../basics/Button.js";
|
|
16
|
+
import { TextInput } from "./TextInput.js";
|
|
17
|
+
/**
|
|
18
|
+
* @function TypeToConfirm
|
|
19
|
+
* @param props {TypeToConfirmProps} the id, the expected text, the labels, the action, and native
|
|
20
|
+
* form props; children show above the field (what the action does)
|
|
21
|
+
* @returns {JSX.Element} a `<form>` with the children, the text field and the submit button
|
|
22
|
+
*/
|
|
23
|
+
export function TypeToConfirm({ id, expected, label = `Type ${expected} to confirm`, hint, error, submitLabel, pendingLabel, onConfirm, variant = "secondary", className, children, ...props }) {
|
|
24
|
+
const [typed, setTyped] = useState("");
|
|
25
|
+
const [pending, setPending] = useState(false);
|
|
26
|
+
const running = useRef(false);
|
|
27
|
+
const matches = typed.trim() === expected.trim();
|
|
28
|
+
const submit = async (event) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
if (!matches || running.current)
|
|
31
|
+
return;
|
|
32
|
+
running.current = true;
|
|
33
|
+
setPending(true);
|
|
34
|
+
try {
|
|
35
|
+
await onConfirm();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// The caller says what went wrong through `error`; the form stays as typed for a retry.
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
running.current = false;
|
|
42
|
+
setPending(false);
|
|
43
|
+
}
|
|
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, className: "self-start", disabled: !matches || pending, children: pending && pendingLabel !== undefined ? pendingLabel : submitLabel })] }));
|
|
46
|
+
}
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
* @desc Shared classes for inputs, selects, and textareas (osu!-web dark fields). Focus shows as
|
|
4
4
|
* an h1 border. On an invalid field it also gets an h1 ring, since rose to pink alone is
|
|
5
5
|
* hard to see. `outline-hidden` (not `outline-none`) leaves a transparent outline that
|
|
6
|
-
* forced-colors mode paints, so focus shows there too.
|
|
6
|
+
* forced-colors mode paints, so focus shows there too. Also the one label look that fields,
|
|
7
|
+
* groups and filter rows share.
|
|
7
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
9
|
* @created Wed Sep 23, 2026
|
|
9
|
-
* @modified
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
10
11
|
*/
|
|
12
|
+
/** A field or group label: bold c3 at text-sm (internal). */
|
|
13
|
+
export declare const FIELD_LABEL = "font-bold text-c3 text-sm";
|
|
11
14
|
/**
|
|
12
15
|
* @function fieldClasses
|
|
13
16
|
* @param className {string} extra classes, appended last. One that sets the same property as a
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
* @desc Shared classes for inputs, selects, and textareas (osu!-web dark fields). Focus shows as
|
|
4
4
|
* an h1 border. On an invalid field it also gets an h1 ring, since rose to pink alone is
|
|
5
5
|
* hard to see. `outline-hidden` (not `outline-none`) leaves a transparent outline that
|
|
6
|
-
* forced-colors mode paints, so focus shows there too.
|
|
6
|
+
* forced-colors mode paints, so focus shows there too. Also the one label look that fields,
|
|
7
|
+
* groups and filter rows share.
|
|
7
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
9
|
* @created Wed Sep 23, 2026
|
|
9
|
-
* @modified
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
10
11
|
*/
|
|
11
12
|
import { cx } from "../../utils/cx.js";
|
|
12
13
|
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";
|
|
14
|
+
/** A field or group label: bold c3 at text-sm (internal). */
|
|
15
|
+
export const FIELD_LABEL = "font-bold text-c3 text-sm";
|
|
13
16
|
/**
|
|
14
17
|
* @function fieldClasses
|
|
15
18
|
* @param className {string} extra classes, appended last. One that sets the same property as a
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/icons/DiscordIcon.tsx
|
|
3
|
+
* @desc The Discord logo as a static inline SVG (no icon library, no external request), in the
|
|
4
|
+
* current text color. Always decorative: label the link around it. The path is Simple
|
|
5
|
+
* Icons' discord.svg at tag 16.32.0 (https://github.com/simple-icons/simple-icons, CC0
|
|
6
|
+
* 1.0). Discord is a trademark of Discord Inc. Its brand guidelines ask for the logo in
|
|
7
|
+
* color, black or white and not recolored, so set one of those as the text color.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Fri Sep 25, 2026
|
|
10
|
+
* @modified Fri Sep 25, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ComponentProps } from "react";
|
|
13
|
+
/** Every native `<svg>` prop except children and viewBox. */
|
|
14
|
+
export type DiscordIconProps = Omit<ComponentProps<"svg">, "children" | "viewBox">;
|
|
15
|
+
/**
|
|
16
|
+
* @function DiscordIcon
|
|
17
|
+
* @param props {DiscordIconProps} native svg props; `className` replaces the default size
|
|
18
|
+
* ("size-5")
|
|
19
|
+
* @returns {JSX.Element} the Discord logo in the current text color, hidden from assistive tech
|
|
20
|
+
*/
|
|
21
|
+
export declare function DiscordIcon({ className, ...props }: DiscordIconProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @function DiscordIcon
|
|
4
|
+
* @param props {DiscordIconProps} native svg props; `className` replaces the default size
|
|
5
|
+
* ("size-5")
|
|
6
|
+
* @returns {JSX.Element} the Discord logo in the current text color, hidden from assistive tech
|
|
7
|
+
*/
|
|
8
|
+
export function DiscordIcon({ className = "size-5", ...props }) {
|
|
9
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", className: className, fill: "currentColor", ...props, children: _jsx("path", { d: "M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" }) }));
|
|
10
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file src/components/
|
|
2
|
+
* @file src/components/meta/JsonLd.tsx
|
|
3
3
|
* @desc schema.org structured data as a JSON-LD script tag. Every "<" is escaped, so no string
|
|
4
4
|
* in the data (a user-supplied name, say) can close the tag or open a comment.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Wed Sep 23, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
8
|
*/
|
|
9
9
|
import type { ComponentProps } from "react";
|
|
10
10
|
/** Native `<script>` props (like `id` or `nonce`), plus the schema.org data. */
|