@godxjp/ui 31.26.2 → 31.28.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/agent/START-HERE.md +7 -7
- package/agent/components/BlockEditor.json +91 -0
- package/agent/components/CommandPalette.json +5 -0
- package/agent/components/EmojiPicker.json +78 -0
- package/agent/components/Markdown.json +16 -0
- package/agent/components/PageContainer.json +11 -1
- package/agent/components/PageCover.json +75 -0
- package/agent/components/Tree.json +6 -0
- package/agent/components-index.json +15 -0
- package/agent/components.json +282 -1
- package/agent/index.json +8 -8
- package/agent/llms.txt +7 -7
- package/agent/tokens.json +108 -0
- package/dist/components/data-display/tree.js +5 -0
- package/dist/components/data-entry/command-palette.d.ts +5 -0
- package/dist/components/data-entry/command-palette.js +1 -0
- package/dist/components/data-entry/emoji-data.d.ts +43 -0
- package/dist/components/data-entry/emoji-data.js +54 -0
- package/dist/components/data-entry/emoji-picker.d.ts +13 -0
- package/dist/components/data-entry/emoji-picker.js +272 -0
- package/dist/components/data-entry/index.d.ts +2 -0
- package/dist/components/data-entry/index.js +2 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +8 -1
- package/dist/components/layout/page-cover.d.ts +14 -0
- package/dist/components/layout/page-cover.js +127 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +25 -0
- package/dist/i18n/messages/ja.json +25 -0
- package/dist/i18n/messages/vi.json +25 -0
- package/dist/props/components/data-display.prop.d.ts +6 -0
- package/dist/props/components/data-entry.prop.d.ts +26 -0
- package/dist/props/components/layout.prop.d.ts +49 -2
- package/dist/props/registry.d.ts +85 -1
- package/dist/props/registry.js +95 -1
- package/dist/styles/control.css +81 -0
- package/dist/styles/data-display-layout.css +315 -2
- package/dist/styles/focus-ring.css +4 -0
- package/dist/styles/layers.json +26 -1
- package/dist/styles/layout.css +113 -0
- package/dist/tokens/components/control.css +10 -0
- package/dist/tokens/components/data-display.css +9 -0
- package/dist/tokens/semantic/layout.css +14 -0
- package/docs/FRAME-COVERAGE-REPORT.md +5 -2
- package/docs/data-entry/block-editor.tsx +105 -0
- package/docs/data-entry/emoji-picker.tsx +35 -0
- package/docs/i18n/messages/en.json +40 -0
- package/docs/i18n/messages/ja.json +40 -0
- package/docs/i18n/messages/vi.json +40 -0
- package/docs/layout/page-cover.tsx +56 -0
- package/package.json +28 -4
- package/scripts/prune-css.mjs +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmojiPicker's data (gh#1164): `emojibase-data` (MIT, CLDR annotations), ONE locale's compact file,
|
|
3
|
+
* loaded the first time a picker opens — never in the kit's main bundle. Each file is ~570 KB raw
|
|
4
|
+
* (~100 KB gzip). Unsupported locales fall back to English. Skin-tone variants are ignored.
|
|
5
|
+
*/
|
|
6
|
+
export type EmojiEntry = {
|
|
7
|
+
unicode: string;
|
|
8
|
+
label: string;
|
|
9
|
+
tags: readonly string[];
|
|
10
|
+
group: number;
|
|
11
|
+
order: number;
|
|
12
|
+
};
|
|
13
|
+
/** Emojibase groups, in display order — 2 (components: bare skin-tone swatches) is not shown. */
|
|
14
|
+
export declare const EMOJI_GROUPS: readonly [{
|
|
15
|
+
readonly group: 0;
|
|
16
|
+
readonly key: "smileys";
|
|
17
|
+
}, {
|
|
18
|
+
readonly group: 1;
|
|
19
|
+
readonly key: "people";
|
|
20
|
+
}, {
|
|
21
|
+
readonly group: 3;
|
|
22
|
+
readonly key: "animals";
|
|
23
|
+
}, {
|
|
24
|
+
readonly group: 4;
|
|
25
|
+
readonly key: "food";
|
|
26
|
+
}, {
|
|
27
|
+
readonly group: 5;
|
|
28
|
+
readonly key: "travel";
|
|
29
|
+
}, {
|
|
30
|
+
readonly group: 6;
|
|
31
|
+
readonly key: "activities";
|
|
32
|
+
}, {
|
|
33
|
+
readonly group: 7;
|
|
34
|
+
readonly key: "objects";
|
|
35
|
+
}, {
|
|
36
|
+
readonly group: 8;
|
|
37
|
+
readonly key: "symbols";
|
|
38
|
+
}, {
|
|
39
|
+
readonly group: 9;
|
|
40
|
+
readonly key: "flags";
|
|
41
|
+
}];
|
|
42
|
+
export declare function loadEmoji(locale: string): Promise<EmojiEntry[]>;
|
|
43
|
+
export declare function searchEmoji(entries: readonly EmojiEntry[], query: string, limit?: number): EmojiEntry[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const LOADERS = {
|
|
2
|
+
ja: () => import("emojibase-data/ja/compact.json"),
|
|
3
|
+
en: () => import("emojibase-data/en/compact.json"),
|
|
4
|
+
vi: () => import("emojibase-data/vi/compact.json")
|
|
5
|
+
};
|
|
6
|
+
const EMOJI_GROUPS = [
|
|
7
|
+
{ group: 0, key: "smileys" },
|
|
8
|
+
{ group: 1, key: "people" },
|
|
9
|
+
{ group: 3, key: "animals" },
|
|
10
|
+
{ group: 4, key: "food" },
|
|
11
|
+
{ group: 5, key: "travel" },
|
|
12
|
+
{ group: 6, key: "activities" },
|
|
13
|
+
{ group: 7, key: "objects" },
|
|
14
|
+
{ group: 8, key: "symbols" },
|
|
15
|
+
{ group: 9, key: "flags" }
|
|
16
|
+
];
|
|
17
|
+
const cache = /* @__PURE__ */ new Map();
|
|
18
|
+
function loadEmoji(locale) {
|
|
19
|
+
const lang = locale.toLowerCase().split("-")[0];
|
|
20
|
+
const key = lang in LOADERS ? lang : "en";
|
|
21
|
+
let pending = cache.get(key);
|
|
22
|
+
if (!pending) {
|
|
23
|
+
pending = LOADERS[key]().then(
|
|
24
|
+
({ default: raw }) => raw.filter((e) => typeof e.group === "number" && e.group !== 2).map((e) => ({
|
|
25
|
+
unicode: e.unicode,
|
|
26
|
+
label: e.label,
|
|
27
|
+
tags: e.tags ?? [],
|
|
28
|
+
group: e.group,
|
|
29
|
+
order: e.order ?? 0
|
|
30
|
+
})).sort((a, b) => a.order - b.order)
|
|
31
|
+
);
|
|
32
|
+
cache.set(key, pending);
|
|
33
|
+
}
|
|
34
|
+
return pending;
|
|
35
|
+
}
|
|
36
|
+
const fold = (value) => value.normalize("NFKC").toLowerCase();
|
|
37
|
+
function searchEmoji(entries, query, limit = 120) {
|
|
38
|
+
const q = fold(query.trim());
|
|
39
|
+
if (!q) return [];
|
|
40
|
+
const starts = [];
|
|
41
|
+
const contains = [];
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const terms = [entry.label, ...entry.tags].map(fold);
|
|
44
|
+
if (terms.some((t) => t.startsWith(q))) starts.push(entry);
|
|
45
|
+
else if (terms.some((t) => t.includes(q))) contains.push(entry);
|
|
46
|
+
if (starts.length >= limit) break;
|
|
47
|
+
}
|
|
48
|
+
return [...starts, ...contains].slice(0, limit);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
EMOJI_GROUPS,
|
|
52
|
+
loadEmoji,
|
|
53
|
+
searchEmoji
|
|
54
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { EmojiPickerProp } from "../../props/components/data-entry.prop.js";
|
|
3
|
+
export type { EmojiPickerProp, EmojiPickerProp as EmojiPickerProps, } from "../../props/components/data-entry.prop.js";
|
|
4
|
+
/**
|
|
5
|
+
* EmojiPicker (gh#1164) — a page icon / reaction picker in a Popover: search by ja / en / vi
|
|
6
|
+
* keywords (emojibase-data, the CLDR annotations, loaded for the active locale only when it first
|
|
7
|
+
* opens), categories, per-user recents, and "remove icon". No skin tones.
|
|
8
|
+
*
|
|
9
|
+
* The emoji are a WAI-ARIA GRID: one tab stop, arrows move by cell / row, Home / End to the row
|
|
10
|
+
* ends, Ctrl+Home / End to the first / last emoji, Enter or Space picks. ArrowDown from the search
|
|
11
|
+
* field enters the grid.
|
|
12
|
+
*/
|
|
13
|
+
export declare function EmojiPicker({ value: valueProp, defaultValue, onValueChange, open: openProp, defaultOpen, onOpenChange, removable, recentsKey, size, disabled, trigger, "aria-label": ariaLabel, id, className, }: EmojiPickerProp): React.JSX.Element;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Clock,
|
|
6
|
+
Flag,
|
|
7
|
+
Lightbulb,
|
|
8
|
+
PawPrint,
|
|
9
|
+
Plane,
|
|
10
|
+
Shapes,
|
|
11
|
+
Smile,
|
|
12
|
+
SmilePlus,
|
|
13
|
+
Trophy,
|
|
14
|
+
User,
|
|
15
|
+
UtensilsCrossed
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
18
|
+
import { cn } from "../../lib/utils.js";
|
|
19
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
|
|
20
|
+
import { Button } from "../general/button.js";
|
|
21
|
+
import { Text } from "../general/typography.js";
|
|
22
|
+
import { EMOJI_GROUPS, loadEmoji, searchEmoji } from "./emoji-data.js";
|
|
23
|
+
import { SearchInput } from "./search-input.js";
|
|
24
|
+
const COLUMNS = 8;
|
|
25
|
+
const ICON_SIZE = { xs: "icon-xs", sm: "icon-sm", md: "icon", lg: "icon-lg" };
|
|
26
|
+
const RECENTS_LIMIT = 24;
|
|
27
|
+
const GROUP_ICON = {
|
|
28
|
+
smileys: Smile,
|
|
29
|
+
people: User,
|
|
30
|
+
animals: PawPrint,
|
|
31
|
+
food: UtensilsCrossed,
|
|
32
|
+
travel: Plane,
|
|
33
|
+
activities: Trophy,
|
|
34
|
+
objects: Lightbulb,
|
|
35
|
+
symbols: Shapes,
|
|
36
|
+
flags: Flag
|
|
37
|
+
};
|
|
38
|
+
function readRecents(key) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
41
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
42
|
+
return Array.isArray(list) ? list.filter((e) => typeof e === "string") : [];
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function writeRecents(key, list) {
|
|
48
|
+
try {
|
|
49
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(list));
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function EmojiPicker({
|
|
54
|
+
value: valueProp,
|
|
55
|
+
defaultValue = null,
|
|
56
|
+
onValueChange,
|
|
57
|
+
open: openProp,
|
|
58
|
+
defaultOpen = false,
|
|
59
|
+
onOpenChange,
|
|
60
|
+
removable = true,
|
|
61
|
+
recentsKey = "godxjp-ui:emoji-recents",
|
|
62
|
+
size = "md",
|
|
63
|
+
disabled = false,
|
|
64
|
+
trigger,
|
|
65
|
+
"aria-label": ariaLabel,
|
|
66
|
+
id,
|
|
67
|
+
className
|
|
68
|
+
}) {
|
|
69
|
+
const { t, locale } = useTranslation();
|
|
70
|
+
const [inner, setInner] = React.useState(defaultValue);
|
|
71
|
+
const value = valueProp !== void 0 ? valueProp : inner;
|
|
72
|
+
const [innerOpen, setInnerOpen] = React.useState(defaultOpen);
|
|
73
|
+
const open = openProp ?? innerOpen;
|
|
74
|
+
const setOpen = (next) => {
|
|
75
|
+
if (openProp === void 0) setInnerOpen(next);
|
|
76
|
+
onOpenChange?.(next);
|
|
77
|
+
};
|
|
78
|
+
const [entries, setEntries] = React.useState(null);
|
|
79
|
+
const [query, setQuery] = React.useState("");
|
|
80
|
+
const [recents, setRecents] = React.useState([]);
|
|
81
|
+
const [tab, setTab] = React.useState("smileys");
|
|
82
|
+
const [active, setActive] = React.useState(0);
|
|
83
|
+
const gridRef = React.useRef(null);
|
|
84
|
+
const gridId = React.useId();
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
if (!open) return;
|
|
87
|
+
setRecents(readRecents(recentsKey));
|
|
88
|
+
setQuery("");
|
|
89
|
+
let live = true;
|
|
90
|
+
void loadEmoji(locale).then((loaded) => {
|
|
91
|
+
if (live) setEntries(loaded);
|
|
92
|
+
});
|
|
93
|
+
return () => {
|
|
94
|
+
live = false;
|
|
95
|
+
};
|
|
96
|
+
}, [open, locale, recentsKey]);
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
if (open && recents.length > 0) setTab("recent");
|
|
99
|
+
}, [open, recents.length]);
|
|
100
|
+
const byUnicode = React.useMemo(
|
|
101
|
+
() => new Map((entries ?? []).map((e) => [e.unicode, e])),
|
|
102
|
+
[entries]
|
|
103
|
+
);
|
|
104
|
+
const shown = React.useMemo(() => {
|
|
105
|
+
if (!entries) return [];
|
|
106
|
+
if (query.trim()) return searchEmoji(entries, query);
|
|
107
|
+
if (tab === "recent") {
|
|
108
|
+
return recents.map(
|
|
109
|
+
(u) => byUnicode.get(u) ?? { unicode: u, label: u, tags: [], group: -1, order: 0 }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const group = EMOJI_GROUPS.find((g) => g.key === tab)?.group;
|
|
113
|
+
return entries.filter((e) => e.group === group);
|
|
114
|
+
}, [entries, query, tab, recents, byUnicode]);
|
|
115
|
+
React.useEffect(() => setActive(0), [tab, query]);
|
|
116
|
+
const pick = (unicode) => {
|
|
117
|
+
if (valueProp === void 0) setInner(unicode);
|
|
118
|
+
onValueChange?.(unicode);
|
|
119
|
+
if (unicode) {
|
|
120
|
+
const next = [unicode, ...readRecents(recentsKey).filter((u) => u !== unicode)].slice(
|
|
121
|
+
0,
|
|
122
|
+
RECENTS_LIMIT
|
|
123
|
+
);
|
|
124
|
+
writeRecents(recentsKey, next);
|
|
125
|
+
}
|
|
126
|
+
setOpen(false);
|
|
127
|
+
};
|
|
128
|
+
const focusCell = (index) => {
|
|
129
|
+
const clamped = Math.max(0, Math.min(shown.length - 1, index));
|
|
130
|
+
setActive(clamped);
|
|
131
|
+
const cell = () => gridRef.current?.querySelector(`[data-index="${clamped}"]`);
|
|
132
|
+
const now = cell();
|
|
133
|
+
if (now) now.focus();
|
|
134
|
+
else requestAnimationFrame(() => cell()?.focus());
|
|
135
|
+
};
|
|
136
|
+
const onGridKeyDown = (event) => {
|
|
137
|
+
const row = Math.floor(active / COLUMNS);
|
|
138
|
+
const moves = {
|
|
139
|
+
ArrowRight: active + 1,
|
|
140
|
+
ArrowLeft: active - 1,
|
|
141
|
+
ArrowDown: active + COLUMNS,
|
|
142
|
+
ArrowUp: active - COLUMNS,
|
|
143
|
+
Home: event.ctrlKey ? 0 : row * COLUMNS,
|
|
144
|
+
End: event.ctrlKey ? shown.length - 1 : row * COLUMNS + COLUMNS - 1
|
|
145
|
+
};
|
|
146
|
+
if (event.key in moves) {
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
focusCell(moves[event.key]);
|
|
149
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
const entry = shown[active];
|
|
152
|
+
if (entry) pick(entry.unicode);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const rows = [];
|
|
156
|
+
for (let i = 0; i < shown.length; i += COLUMNS) rows.push(shown.slice(i, i + COLUMNS));
|
|
157
|
+
const tabs = [
|
|
158
|
+
...recents.length ? [{ key: "recent", label: t("dataEntry.emojiPicker.recent"), Icon: Clock }] : [],
|
|
159
|
+
...EMOJI_GROUPS.map((g) => ({
|
|
160
|
+
key: g.key,
|
|
161
|
+
label: t(`dataEntry.emojiPicker.categories.${g.key}`),
|
|
162
|
+
Icon: GROUP_ICON[g.key]
|
|
163
|
+
}))
|
|
164
|
+
];
|
|
165
|
+
const gridLabel = query.trim() ? t("dataEntry.emojiPicker.search") : tabs.find((x) => x.key === tab)?.label ?? t("dataEntry.emojiPicker.grid");
|
|
166
|
+
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
167
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: trigger ?? /* @__PURE__ */ jsx(
|
|
168
|
+
Button,
|
|
169
|
+
{
|
|
170
|
+
id,
|
|
171
|
+
variant: "ghost",
|
|
172
|
+
size: value ? size : ICON_SIZE[size],
|
|
173
|
+
disabled,
|
|
174
|
+
"aria-label": ariaLabel ?? t("dataEntry.emojiPicker.trigger"),
|
|
175
|
+
className: cn("ui-emoji-picker-trigger", className),
|
|
176
|
+
"data-has-value": value ? "" : void 0,
|
|
177
|
+
children: value ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: value }) : /* @__PURE__ */ jsx(SmilePlus, { "aria-hidden": "true" })
|
|
178
|
+
}
|
|
179
|
+
) }),
|
|
180
|
+
/* @__PURE__ */ jsxs(
|
|
181
|
+
PopoverContent,
|
|
182
|
+
{
|
|
183
|
+
align: "start",
|
|
184
|
+
className: "ui-emoji-picker",
|
|
185
|
+
"aria-label": ariaLabel ?? t("dataEntry.emojiPicker.trigger"),
|
|
186
|
+
children: [
|
|
187
|
+
/* @__PURE__ */ jsxs(
|
|
188
|
+
"div",
|
|
189
|
+
{
|
|
190
|
+
className: "ui-emoji-picker-head",
|
|
191
|
+
onKeyDown: (event) => {
|
|
192
|
+
if (event.key === "ArrowDown" && shown.length && event.target.tagName === "INPUT") {
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
focusCell(0);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
children: [
|
|
198
|
+
/* @__PURE__ */ jsx(
|
|
199
|
+
SearchInput,
|
|
200
|
+
{
|
|
201
|
+
"aria-label": t("dataEntry.emojiPicker.search"),
|
|
202
|
+
placeholder: t("dataEntry.emojiPicker.search"),
|
|
203
|
+
value: query,
|
|
204
|
+
onValueChange: setQuery
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
removable && value ? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => pick(null), children: t("dataEntry.emojiPicker.remove") }) : null
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
!query.trim() ? /* @__PURE__ */ jsx(
|
|
212
|
+
"div",
|
|
213
|
+
{
|
|
214
|
+
role: "tablist",
|
|
215
|
+
"aria-label": t("dataEntry.emojiPicker.grid"),
|
|
216
|
+
className: "ui-emoji-picker-tabs",
|
|
217
|
+
children: tabs.map((x) => /* @__PURE__ */ jsx(
|
|
218
|
+
Button,
|
|
219
|
+
{
|
|
220
|
+
role: "tab",
|
|
221
|
+
size: "icon-sm",
|
|
222
|
+
variant: "ghost",
|
|
223
|
+
"aria-selected": tab === x.key,
|
|
224
|
+
"aria-controls": gridId,
|
|
225
|
+
"aria-label": x.label,
|
|
226
|
+
title: x.label,
|
|
227
|
+
"data-state": tab === x.key ? "active" : void 0,
|
|
228
|
+
onClick: () => setTab(x.key),
|
|
229
|
+
children: /* @__PURE__ */ jsx(x.Icon, { "aria-hidden": "true" })
|
|
230
|
+
},
|
|
231
|
+
x.key
|
|
232
|
+
))
|
|
233
|
+
}
|
|
234
|
+
) : null,
|
|
235
|
+
entries === null ? /* @__PURE__ */ jsx(Text, { size: "sm", tone: "muted", role: "status", className: "ui-emoji-picker-state", children: t("dataEntry.emojiPicker.loading") }) : shown.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", tone: "muted", role: "status", className: "ui-emoji-picker-state", children: t("dataEntry.emojiPicker.empty") }) : /* @__PURE__ */ jsx(
|
|
236
|
+
"div",
|
|
237
|
+
{
|
|
238
|
+
ref: gridRef,
|
|
239
|
+
id: gridId,
|
|
240
|
+
role: "grid",
|
|
241
|
+
"aria-label": gridLabel,
|
|
242
|
+
className: "ui-emoji-picker-grid",
|
|
243
|
+
onKeyDown: onGridKeyDown,
|
|
244
|
+
children: rows.map((row, r) => /* @__PURE__ */ jsx("div", { role: "row", className: "ui-emoji-picker-row", children: row.map((entry, c) => {
|
|
245
|
+
const index = r * COLUMNS + c;
|
|
246
|
+
return /* @__PURE__ */ jsx(
|
|
247
|
+
"div",
|
|
248
|
+
{
|
|
249
|
+
role: "gridcell",
|
|
250
|
+
"data-index": index,
|
|
251
|
+
tabIndex: index === active ? 0 : -1,
|
|
252
|
+
"aria-label": entry.label,
|
|
253
|
+
"aria-selected": entry.unicode === value,
|
|
254
|
+
title: entry.label,
|
|
255
|
+
className: "ui-emoji-picker-cell ui-focus-ring",
|
|
256
|
+
onClick: () => pick(entry.unicode),
|
|
257
|
+
onFocus: () => setActive(index),
|
|
258
|
+
children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: entry.unicode })
|
|
259
|
+
},
|
|
260
|
+
entry.unicode
|
|
261
|
+
);
|
|
262
|
+
}) }, r))
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
] });
|
|
269
|
+
}
|
|
270
|
+
export {
|
|
271
|
+
EmojiPicker
|
|
272
|
+
};
|
|
@@ -54,6 +54,8 @@ export type { TransferProps, TransferItemProp } from "./transfer.js";
|
|
|
54
54
|
export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command.js";
|
|
55
55
|
export type { CommandProps } from "./command.js";
|
|
56
56
|
export { CommandPalette } from "./command-palette.js";
|
|
57
|
+
export { EmojiPicker } from "./emoji-picker.js";
|
|
58
|
+
export type { EmojiPickerProp, EmojiPickerProps } from "./emoji-picker.js";
|
|
57
59
|
export type { CommandPaletteGroup, CommandPaletteItem, CommandPaletteLabels, CommandPaletteProps, CommandPaletteSelectModifiers, } from "./command-palette.js";
|
|
58
60
|
export { PasswordInput } from "./password-input.js";
|
|
59
61
|
export type { PasswordInputProps, PasswordVisibilityToggleProp } from "./password-input.js";
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
CommandList
|
|
54
54
|
} from "./command.js";
|
|
55
55
|
import { CommandPalette } from "./command-palette.js";
|
|
56
|
+
import { EmojiPicker } from "./emoji-picker.js";
|
|
56
57
|
import { PasswordInput } from "./password-input.js";
|
|
57
58
|
import { PasswordStrength, usePasswordStrength } from "./password-strength.js";
|
|
58
59
|
import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "./input-otp.js";
|
|
@@ -82,6 +83,7 @@ export {
|
|
|
82
83
|
CommandPalette,
|
|
83
84
|
DatePicker,
|
|
84
85
|
DayButton,
|
|
86
|
+
EmojiPicker,
|
|
85
87
|
Field,
|
|
86
88
|
Form,
|
|
87
89
|
FormErrors,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { PageContainer } from "./page-container.js";
|
|
2
2
|
export type { PageContainerProp, PageContainerProps, BreadcrumbItem, BreadcrumbItemProp, } from "./page-container.js";
|
|
3
|
+
export { PageCover } from "./page-cover.js";
|
|
4
|
+
export type { PageCoverProp, PageCoverProps } from "./page-cover.js";
|
|
3
5
|
export { Flex } from "./flex.js";
|
|
4
6
|
export type { FlexAlignProp, FlexDirectionProp, FlexJustifyProp, FlexProp, FlexProps, } from "./flex.js";
|
|
5
7
|
export { SpaceCompact } from "./space-compact.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PageContainer } from "./page-container.js";
|
|
2
|
+
import { PageCover } from "./page-cover.js";
|
|
2
3
|
import { Flex } from "./flex.js";
|
|
3
4
|
import { SpaceCompact } from "./space-compact.js";
|
|
4
5
|
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
|
|
@@ -55,6 +56,7 @@ export {
|
|
|
55
56
|
NavList,
|
|
56
57
|
OrgSwitcher,
|
|
57
58
|
PageContainer,
|
|
59
|
+
PageCover,
|
|
58
60
|
ResizableHandle,
|
|
59
61
|
ResizablePanel,
|
|
60
62
|
ResizablePanelGroup,
|
|
@@ -2,7 +2,7 @@ import type { PageContainerProp, PageInsetProp } from "../../props/components/la
|
|
|
2
2
|
export type { PageContainerProp, PageContainerProp as PageContainerProps, PageContainerExtraProp, } from "../../props/components/layout.prop.js";
|
|
3
3
|
export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop.js";
|
|
4
4
|
export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react").JSX.Element;
|
|
5
|
-
declare function PageContainerRoot({ title, subtitle, status, extra, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, titleRef, density, variant, preset, headerLayout, headerScale, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
|
|
5
|
+
declare function PageContainerRoot({ title, subtitle, status, extra, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, titleRef, density, variant, preset, headerLayout, headerScale, icon, cover, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
|
|
6
6
|
export declare const PageContainer: typeof PageContainerRoot & {
|
|
7
7
|
Inset: typeof PageContainerInset;
|
|
8
8
|
};
|
|
@@ -51,6 +51,8 @@ function PageContainerRoot({
|
|
|
51
51
|
preset = "default",
|
|
52
52
|
headerLayout = "stack",
|
|
53
53
|
headerScale = "document",
|
|
54
|
+
icon,
|
|
55
|
+
cover,
|
|
54
56
|
measure = "default",
|
|
55
57
|
stickyFooter = false,
|
|
56
58
|
footerReveal = "always",
|
|
@@ -63,12 +65,14 @@ function PageContainerRoot({
|
|
|
63
65
|
const { t } = useTranslation();
|
|
64
66
|
const { start: extraStart, end: extraEnd } = resolvePageExtra(extra);
|
|
65
67
|
const titleFocus = titleRef === void 0 ? void 0 : { ref: titleRef, tabIndex: -1 };
|
|
68
|
+
const coverVariant = cover == null || cover === false ? void 0 : isValidElement(cover) && cover.props.variant === "eyecatch" ? "eyecatch" : "banner";
|
|
66
69
|
return /* @__PURE__ */ jsxs(
|
|
67
70
|
"div",
|
|
68
71
|
{
|
|
69
72
|
"data-preset": preset,
|
|
70
73
|
"data-measure": measure,
|
|
71
|
-
"data-header-scale": headerScale === "
|
|
74
|
+
"data-header-scale": headerScale === "document" ? void 0 : headerScale,
|
|
75
|
+
"data-cover": coverVariant,
|
|
72
76
|
"data-revealed": revealed ? "true" : void 0,
|
|
73
77
|
className: cn(
|
|
74
78
|
"ui-page-container",
|
|
@@ -82,6 +86,7 @@ function PageContainerRoot({
|
|
|
82
86
|
className
|
|
83
87
|
),
|
|
84
88
|
children: [
|
|
89
|
+
coverVariant === "banner" ? /* @__PURE__ */ jsx("div", { className: "ui-page-cover-slot", children: cover }) : null,
|
|
85
90
|
/* @__PURE__ */ jsxs(
|
|
86
91
|
"header",
|
|
87
92
|
{
|
|
@@ -134,8 +139,10 @@ function PageContainerRoot({
|
|
|
134
139
|
}) })
|
|
135
140
|
}
|
|
136
141
|
),
|
|
142
|
+
coverVariant === "eyecatch" ? /* @__PURE__ */ jsx("div", { className: "ui-page-cover-slot", children: cover }) : null,
|
|
137
143
|
/* @__PURE__ */ jsxs("div", { className: "ui-page-header-row", children: [
|
|
138
144
|
/* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading", children: [
|
|
145
|
+
icon != null && icon !== false ? /* @__PURE__ */ jsx("div", { className: "ui-page-header-icon", children: icon }) : null,
|
|
139
146
|
headerLoading ? /* @__PURE__ */ jsx(
|
|
140
147
|
"h1",
|
|
141
148
|
{
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { PageCoverProp } from "../../props/components/layout.prop.js";
|
|
3
|
+
export type { PageCoverProp, PageCoverProp as PageCoverProps, } from "../../props/components/layout.prop.js";
|
|
4
|
+
/**
|
|
5
|
+
* PageCover (gh#1160) — a page's cover image: Notion's banner across the top, or note.com's
|
|
6
|
+
* eyecatch in the reading column (`variant="eyecatch"`, ~1.91:1, rounded). Put it in
|
|
7
|
+
* `PageContainer cover`, which places it and lets the page `icon` overlap its bottom edge.
|
|
8
|
+
*
|
|
9
|
+
* The focal point is `positionY` (0 top – 100 bottom), applied as `object-position: 50% y%`.
|
|
10
|
+
* While `repositioning`, the image IS the control: a vertical WAI-ARIA slider that follows a drag
|
|
11
|
+
* (pixels map to percent through the image's real overflow, so the picture tracks the pointer)
|
|
12
|
+
* and the slider keys (arrows ±1, PageUp / PageDown ±10, Home / End); Enter or Escape ends it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PageCover: React.ForwardRefExoticComponent<PageCoverProp & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
const clamp = (value) => Math.min(100, Math.max(0, Math.round(value)));
|
|
7
|
+
const PageCover = React.forwardRef(function PageCover2({
|
|
8
|
+
src,
|
|
9
|
+
alt,
|
|
10
|
+
positionY = 50,
|
|
11
|
+
onPositionChange,
|
|
12
|
+
repositioning = false,
|
|
13
|
+
onRepositioningChange,
|
|
14
|
+
variant = "banner",
|
|
15
|
+
height = "md",
|
|
16
|
+
actions,
|
|
17
|
+
id,
|
|
18
|
+
className
|
|
19
|
+
}, ref) {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
const frameRef = React.useRef(null);
|
|
22
|
+
const imageRef = React.useRef(null);
|
|
23
|
+
const drag = React.useRef(null);
|
|
24
|
+
const y = clamp(positionY);
|
|
25
|
+
const overflow = () => {
|
|
26
|
+
const frame = frameRef.current;
|
|
27
|
+
const image = imageRef.current;
|
|
28
|
+
if (!frame || !image || !image.naturalWidth) return 0;
|
|
29
|
+
const rendered = frame.clientWidth / image.naturalWidth * image.naturalHeight;
|
|
30
|
+
return Math.max(0, rendered - frame.clientHeight);
|
|
31
|
+
};
|
|
32
|
+
const onKeyDown = (event) => {
|
|
33
|
+
if (!repositioning) return;
|
|
34
|
+
const step = {
|
|
35
|
+
ArrowUp: 1,
|
|
36
|
+
ArrowRight: 1,
|
|
37
|
+
ArrowDown: -1,
|
|
38
|
+
ArrowLeft: -1,
|
|
39
|
+
PageUp: 10,
|
|
40
|
+
PageDown: -10
|
|
41
|
+
}[event.key];
|
|
42
|
+
let next = null;
|
|
43
|
+
if (step !== void 0) next = clamp(y + step);
|
|
44
|
+
else if (event.key === "Home") next = 0;
|
|
45
|
+
else if (event.key === "End") next = 100;
|
|
46
|
+
else if (event.key === "Enter" || event.key === "Escape") {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
onRepositioningChange?.(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (next === null) return;
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
if (next !== y) onPositionChange?.(next);
|
|
54
|
+
};
|
|
55
|
+
const onPointerDown = (event) => {
|
|
56
|
+
if (!repositioning || event.button !== 0) return;
|
|
57
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
58
|
+
drag.current = { pointer: event.pointerId, startY: event.clientY, startValue: y };
|
|
59
|
+
};
|
|
60
|
+
const onPointerMove = (event) => {
|
|
61
|
+
const active = drag.current;
|
|
62
|
+
if (!active || active.pointer !== event.pointerId) return;
|
|
63
|
+
const travel = overflow();
|
|
64
|
+
if (travel <= 0) return;
|
|
65
|
+
const next = clamp(active.startValue - (event.clientY - active.startY) / travel * 100);
|
|
66
|
+
if (next !== y) onPositionChange?.(next);
|
|
67
|
+
};
|
|
68
|
+
const endDrag = (event) => {
|
|
69
|
+
if (drag.current?.pointer === event.pointerId) drag.current = null;
|
|
70
|
+
};
|
|
71
|
+
const slider = repositioning ? {
|
|
72
|
+
role: "slider",
|
|
73
|
+
tabIndex: 0,
|
|
74
|
+
"aria-orientation": "vertical",
|
|
75
|
+
"aria-valuemin": 0,
|
|
76
|
+
"aria-valuemax": 100,
|
|
77
|
+
"aria-valuenow": y,
|
|
78
|
+
"aria-valuetext": t("layout.pageCover.valueText", { value: y }),
|
|
79
|
+
"aria-label": t("layout.pageCover.position")
|
|
80
|
+
} : {};
|
|
81
|
+
return /* @__PURE__ */ jsxs(
|
|
82
|
+
"div",
|
|
83
|
+
{
|
|
84
|
+
ref,
|
|
85
|
+
id,
|
|
86
|
+
"data-slot": "page-cover",
|
|
87
|
+
"data-variant": variant,
|
|
88
|
+
"data-height": variant === "banner" ? height : void 0,
|
|
89
|
+
"data-repositioning": repositioning ? "" : void 0,
|
|
90
|
+
className: cn("ui-page-cover", className),
|
|
91
|
+
children: [
|
|
92
|
+
/* @__PURE__ */ jsxs(
|
|
93
|
+
"div",
|
|
94
|
+
{
|
|
95
|
+
ref: frameRef,
|
|
96
|
+
className: "ui-page-cover-frame ui-focus-ring",
|
|
97
|
+
onKeyDown,
|
|
98
|
+
onPointerDown,
|
|
99
|
+
onPointerMove,
|
|
100
|
+
onPointerUp: endDrag,
|
|
101
|
+
onPointerCancel: endDrag,
|
|
102
|
+
...slider,
|
|
103
|
+
children: [
|
|
104
|
+
/* @__PURE__ */ jsx(
|
|
105
|
+
"img",
|
|
106
|
+
{
|
|
107
|
+
ref: imageRef,
|
|
108
|
+
className: "ui-page-cover-image",
|
|
109
|
+
src,
|
|
110
|
+
alt,
|
|
111
|
+
draggable: false,
|
|
112
|
+
style: { objectPosition: `50% ${y}%` }
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
repositioning ? /* @__PURE__ */ jsx("span", { className: "ui-page-cover-hint", "aria-hidden": "true", children: t("layout.pageCover.hint") }) : null
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
),
|
|
119
|
+
actions != null ? /* @__PURE__ */ jsx("div", { className: "ui-page-cover-actions", children: actions }) : null
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
PageCover.displayName = "PageCover";
|
|
125
|
+
export {
|
|
126
|
+
PageCover
|
|
127
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "31.
|
|
3
|
+
"version": "31.28.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -212,6 +212,26 @@
|
|
|
212
212
|
"openDialog": "Choose from list",
|
|
213
213
|
"openSearch": "Search",
|
|
214
214
|
"suggestions": "Suggestions"
|
|
215
|
+
},
|
|
216
|
+
"emojiPicker": {
|
|
217
|
+
"trigger": "Choose an icon",
|
|
218
|
+
"search": "Search emoji",
|
|
219
|
+
"recent": "Recently used",
|
|
220
|
+
"remove": "Remove icon",
|
|
221
|
+
"empty": "No emoji found",
|
|
222
|
+
"loading": "Loading emoji…",
|
|
223
|
+
"grid": "Emoji",
|
|
224
|
+
"categories": {
|
|
225
|
+
"smileys": "Smileys & emotion",
|
|
226
|
+
"people": "People & body",
|
|
227
|
+
"animals": "Animals & nature",
|
|
228
|
+
"food": "Food & drink",
|
|
229
|
+
"travel": "Travel & places",
|
|
230
|
+
"activities": "Activities",
|
|
231
|
+
"objects": "Objects",
|
|
232
|
+
"symbols": "Symbols",
|
|
233
|
+
"flags": "Flags"
|
|
234
|
+
}
|
|
215
235
|
}
|
|
216
236
|
},
|
|
217
237
|
"feedback": {
|
|
@@ -301,6 +321,11 @@
|
|
|
301
321
|
},
|
|
302
322
|
"draggablePanel": {
|
|
303
323
|
"moveLabel": "Move panel. Use the arrow keys to move it, or hold Shift for larger steps."
|
|
324
|
+
},
|
|
325
|
+
"pageCover": {
|
|
326
|
+
"position": "Cover position",
|
|
327
|
+
"hint": "Drag the image, or use the arrow keys, to reposition it",
|
|
328
|
+
"valueText": "{value}% from the top"
|
|
304
329
|
}
|
|
305
330
|
},
|
|
306
331
|
"dataDisplay": {
|