@godxjp/ui 31.27.0 → 31.28.1
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 +8 -0
- package/agent/components/CommandPalette.json +5 -0
- package/agent/components/EmojiPicker.json +78 -0
- package/agent/components/Markdown.json +11 -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 +10 -0
- package/agent/components.json +194 -1
- package/agent/index.json +8 -8
- package/agent/llms.txt +7 -7
- package/agent/tokens.json +79 -1
- 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 +94 -0
- package/dist/styles/focus-ring.css +2 -0
- package/dist/styles/layers.json +17 -1
- package/dist/styles/layout.css +113 -0
- package/dist/tokens/components/control.css +10 -0
- package/dist/tokens/components/data-display.css +1 -0
- package/dist/tokens/semantic/layout.css +14 -0
- package/docs/FORMS.md +5 -0
- package/docs/FRAME-COVERAGE-REPORT.md +4 -2
- package/docs/data-entry/emoji-picker.tsx +35 -0
- package/docs/i18n/messages/en.json +16 -0
- package/docs/i18n/messages/ja.json +16 -0
- package/docs/i18n/messages/vi.json +16 -0
- package/docs/layout/page-cover.tsx +56 -0
- package/package.json +5 -6
package/agent/index.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"counts": {
|
|
3
3
|
"anti-ai-tells": 26,
|
|
4
|
-
"components":
|
|
4
|
+
"components": 184,
|
|
5
5
|
"patterns": 21,
|
|
6
6
|
"rules": 50,
|
|
7
|
-
"tokens":
|
|
7
|
+
"tokens": 2155,
|
|
8
8
|
"vocabulary": 14
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"file": "components-index.json",
|
|
13
|
-
"note": "
|
|
13
|
+
"note": "51 KB — name + group + tagline for all 184. FETCH THIS FIRST, then fetch only the components you chose.",
|
|
14
14
|
"url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"file": "components/<Name>.json",
|
|
18
|
-
"note": "One file per component (1 KB–36 KB, median 6 KB), each carrying its importPath. This is the selective route: read the index, then fetch only what you need instead of the 1.
|
|
18
|
+
"note": "One file per component (1 KB–36 KB, median 6 KB), each carrying its importPath. This is the selective route: read the index, then fetch only what you need instead of the 1.3 MB blob.",
|
|
19
19
|
"url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/<Name>.json"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
@@ -48,19 +48,19 @@
|
|
|
48
48
|
"note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
|
|
49
49
|
"read": {
|
|
50
50
|
"live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
|
|
51
|
-
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.
|
|
51
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.28.1/agent/index.json"
|
|
52
52
|
},
|
|
53
53
|
"source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
|
|
54
54
|
"start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
|
|
55
55
|
"tokenTiers": {
|
|
56
56
|
"component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
|
|
57
57
|
"counts": {
|
|
58
|
-
"component":
|
|
58
|
+
"component": 1832,
|
|
59
59
|
"foundation": 211,
|
|
60
|
-
"semantic":
|
|
60
|
+
"semantic": 112
|
|
61
61
|
},
|
|
62
62
|
"foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
|
|
63
63
|
"semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
|
|
64
64
|
},
|
|
65
|
-
"version": "31.
|
|
65
|
+
"version": "31.28.1"
|
|
66
66
|
}
|
package/agent/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
|
-
> A Japanese-enterprise React design system:
|
|
4
|
-
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.
|
|
3
|
+
> A Japanese-enterprise React design system: 184 components, 2155 design tokens,
|
|
4
|
+
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.28.1.
|
|
5
5
|
|
|
6
6
|
If your client can run a process, do not read these files — run the MCP server instead
|
|
7
|
-
(`npx @godxjp/ui-mcp@31.
|
|
7
|
+
(`npx @godxjp/ui-mcp@31.28.1`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -15,10 +15,10 @@ that can only fetch URLs.
|
|
|
15
15
|
## Catalog
|
|
16
16
|
|
|
17
17
|
- [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 21 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
|
|
18
|
-
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json):
|
|
18
|
+
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 51 KB — all 184 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
|
|
19
19
|
- [components/<Name>.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–36 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
|
|
20
|
-
- [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.
|
|
21
|
-
- [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand),
|
|
20
|
+
- [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.3 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
|
|
21
|
+
- [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 112 `semantic` roles, 1832 `component` knobs.
|
|
22
22
|
- [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
|
|
23
23
|
- [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 50 cardinal rules.
|
|
24
24
|
- [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 26 shapes that make generated UI look generated, each with its fix.
|
|
@@ -26,7 +26,7 @@ that can only fetch URLs.
|
|
|
26
26
|
## Pinning
|
|
27
27
|
|
|
28
28
|
Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
|
|
29
|
-
the tag: `.../godx-jp/godxjp-ui/v31.
|
|
29
|
+
the tag: `.../godx-jp/godxjp-ui/v31.28.1/agent/...`. A catalog that does not match the installed
|
|
30
30
|
package describes props that are absent, or hides props that are present, and says nothing either way.
|
|
31
31
|
|
|
32
32
|
Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
|
package/agent/tokens.json
CHANGED
|
@@ -1745,6 +1745,54 @@
|
|
|
1745
1745
|
"tier": "semantic",
|
|
1746
1746
|
"value": "var(--heading-h3, var(--font-size-base))"
|
|
1747
1747
|
},
|
|
1748
|
+
{
|
|
1749
|
+
"description": "PageContainer `headerScale=\"display\"` (gh#1160) — an ARTICLE's headline (a Notion / note.com page). The 4xl display step, which already scales with the viewport through `--font-size-display`'s clamp (≈25px on a phone → 42px wide), so one knob covers phone and desktop and the compact step below does not apply. CJK titles need more leading than Latin ones at this size: 1.4, not the tight 1.25 a short Latin headline gets.",
|
|
1750
|
+
"name": "--page-title-font-size-display",
|
|
1751
|
+
"tier": "semantic",
|
|
1752
|
+
"value": "var( --font-size-4xl, calc(var(--font-size-display) / var(--font-size-display-ratio)) )"
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
"description": "Layout primitive tokens: page, section, stack, inline.",
|
|
1756
|
+
"name": "--page-title-line-height-display",
|
|
1757
|
+
"tier": "semantic",
|
|
1758
|
+
"value": "1.4"
|
|
1759
|
+
},
|
|
1760
|
+
{
|
|
1761
|
+
"description": "The page icon above the title (gh#1160): emoji or glyph, and how far it rides up over a banner cover's bottom edge.",
|
|
1762
|
+
"name": "--page-icon-size",
|
|
1763
|
+
"tier": "semantic",
|
|
1764
|
+
"value": "var(--icon-size-4xl)"
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
"description": "Layout primitive tokens: page, section, stack, inline.",
|
|
1768
|
+
"name": "--page-icon-cover-overlap",
|
|
1769
|
+
"tier": "semantic",
|
|
1770
|
+
"value": "calc(var(--page-icon-size) * 0.6)"
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
"description": "PageCover (gh#1160). A banner's height scales with the viewport between a phone floor and a desktop ceiling — one knob per step, no media query; an eyecatch keeps note.com's ~1.91:1.",
|
|
1774
|
+
"name": "--page-cover-block-size-md",
|
|
1775
|
+
"tier": "semantic",
|
|
1776
|
+
"value": "clamp(8rem, 24vw, 15rem)"
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
"description": "Layout primitive tokens: page, section, stack, inline.",
|
|
1780
|
+
"name": "--page-cover-block-size-lg",
|
|
1781
|
+
"tier": "semantic",
|
|
1782
|
+
"value": "clamp(10rem, 32vw, 20rem)"
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
"description": "Layout primitive tokens: page, section, stack, inline.",
|
|
1786
|
+
"name": "--page-cover-eyecatch-aspect-ratio",
|
|
1787
|
+
"tier": "semantic",
|
|
1788
|
+
"value": "1.91"
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
"description": "default = var(--card-radius) at the call site (gh#888)",
|
|
1792
|
+
"name": "--page-cover-radius",
|
|
1793
|
+
"tier": "semantic",
|
|
1794
|
+
"value": "initial"
|
|
1795
|
+
},
|
|
1748
1796
|
{
|
|
1749
1797
|
"description": "The SUBTITLE step of the same fact. Measured against the reference chat screen, the chrome title and the chrome subtitle were rendering at the SAME step (both --font-size-base), which is not a hierarchy — a channel name and the channel's purpose line read as one undifferentiated block. The design puts the purpose line two steps down (`Text size=\"2xs\"`), and the size is what carries the LINE BOX: at 14px/1.7 the subtitle line is 23.8px tall, at 11px/1.7 it is 18.9px, so the whole header band loses ~5px it was spending on a caption. `--font-size-2xs` (ratio⁻², ≈11px) is that step, and it is the same golden scale the title step reads — never a literal. Quiet by default: a page that never passes `headerScale` never resolves it (#44).",
|
|
1750
1798
|
"name": "--page-subtitle-font-size-chrome",
|
|
@@ -4835,6 +4883,30 @@
|
|
|
4835
4883
|
"tier": "component",
|
|
4836
4884
|
"value": "initial"
|
|
4837
4885
|
},
|
|
4886
|
+
{
|
|
4887
|
+
"description": "EmojiPicker (gh#1164): the panel width, an emoji cell (a touch-sized square), the glyph inside it, and how tall the grid gets before it scrolls.",
|
|
4888
|
+
"name": "--emoji-picker-cell-size",
|
|
4889
|
+
"tier": "component",
|
|
4890
|
+
"value": "var(--control-height-default)"
|
|
4891
|
+
},
|
|
4892
|
+
{
|
|
4893
|
+
"description": "Eight cells plus the popover's inset — derived, so 44px touch cells (coarse pointer) still fit eight to a row (8 × 44 + insets = 376px, inside a 390px phone).",
|
|
4894
|
+
"name": "--emoji-picker-panel-inline-size",
|
|
4895
|
+
"tier": "component",
|
|
4896
|
+
"value": "calc( 8 * var(--emoji-picker-cell-size) + 2 * var(--popover-space-inset, var(--space-3)) )"
|
|
4897
|
+
},
|
|
4898
|
+
{
|
|
4899
|
+
"description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
|
|
4900
|
+
"name": "--emoji-picker-glyph-size",
|
|
4901
|
+
"tier": "component",
|
|
4902
|
+
"value": "var(--icon-size-lg)"
|
|
4903
|
+
},
|
|
4904
|
+
{
|
|
4905
|
+
"description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
|
|
4906
|
+
"name": "--emoji-picker-grid-max-block-size",
|
|
4907
|
+
"tier": "component",
|
|
4908
|
+
"value": "16rem"
|
|
4909
|
+
},
|
|
4838
4910
|
{
|
|
4839
4911
|
"description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
|
|
4840
4912
|
"name": "--control-height-compact",
|
|
@@ -5250,7 +5322,13 @@
|
|
|
5250
5322
|
"value": "var(--band-height-3xl)"
|
|
5251
5323
|
},
|
|
5252
5324
|
{
|
|
5253
|
-
"description": "
|
|
5325
|
+
"description": "`appearance=\"framed\"`: the inset between the frame and the text (the handle gutter is extra).",
|
|
5326
|
+
"name": "--block-editor-frame-padding",
|
|
5327
|
+
"tier": "component",
|
|
5328
|
+
"value": "var(--space-3)"
|
|
5329
|
+
},
|
|
5330
|
+
{
|
|
5331
|
+
"description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
|
|
5254
5332
|
"name": "--block-editor-menu-inline-size",
|
|
5255
5333
|
"tier": "component",
|
|
5256
5334
|
"value": "16rem"
|
|
@@ -63,6 +63,7 @@ function TreeRoot({
|
|
|
63
63
|
defaultExpandedValues,
|
|
64
64
|
onExpandedValuesChange,
|
|
65
65
|
defaultExpandAll = false,
|
|
66
|
+
expandAction = false,
|
|
66
67
|
loadData,
|
|
67
68
|
titleRender,
|
|
68
69
|
filterTreeNode,
|
|
@@ -491,6 +492,10 @@ function TreeRoot({
|
|
|
491
492
|
onClick: () => {
|
|
492
493
|
if (nodeDisabled) return;
|
|
493
494
|
select(node);
|
|
495
|
+
if (expandAction === "click" && expandable) toggleExpand(node);
|
|
496
|
+
},
|
|
497
|
+
onDoubleClick: () => {
|
|
498
|
+
if (!nodeDisabled && expandAction === "doubleClick" && expandable) toggleExpand(node);
|
|
494
499
|
},
|
|
495
500
|
"data-value": node.value,
|
|
496
501
|
"data-selected": isSelected ? "true" : void 0,
|
|
@@ -2,6 +2,11 @@ import * as React from "react";
|
|
|
2
2
|
export type CommandPaletteItem = {
|
|
3
3
|
id: string;
|
|
4
4
|
label: React.ReactNode;
|
|
5
|
+
/**
|
|
6
|
+
* Decorative glyph before the label — a folder, a file, an emoji (gh#1159). Sized like a menu
|
|
7
|
+
* row's icon (`--menu-icon-size`) and hidden from assistive tech: the label is the name.
|
|
8
|
+
*/
|
|
9
|
+
icon?: React.ReactNode;
|
|
5
10
|
searchValue?: string;
|
|
6
11
|
meta?: React.ReactNode;
|
|
7
12
|
disabled?: boolean;
|
|
@@ -189,6 +189,7 @@ function CommandPalette({
|
|
|
189
189
|
onSelect(item, modifiers);
|
|
190
190
|
},
|
|
191
191
|
children: [
|
|
192
|
+
item.icon != null ? /* @__PURE__ */ jsx("span", { className: "ui-command-palette-icon", "aria-hidden": "true", children: item.icon }) : null,
|
|
192
193
|
/* @__PURE__ */ jsx("span", { className: "ui-command-palette-label", children: item.label }),
|
|
193
194
|
item.meta != null ? /* @__PURE__ */ jsx("span", { className: "ui-command-palette-meta", children: item.meta }) : null
|
|
194
195
|
]
|
|
@@ -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";
|