mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-06-07 10:46:45 +00:00
Compare commits
377 Commits
add-parall
...
xrpl-brand
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c8025fa7d | ||
|
|
a565ceb59d | ||
|
|
f10dbc8c29 | ||
|
|
03dae02593 | ||
|
|
b82df3bb5d | ||
|
|
9e1bc798ba | ||
|
|
c8cb28fd80 | ||
|
|
b09fe6c7b3 | ||
|
|
28af114305 | ||
|
|
d846367857 | ||
|
|
8481e05d6e | ||
|
|
b0428e30d4 | ||
|
|
4b65061304 | ||
|
|
6d99930c9a | ||
|
|
84f78d83dd | ||
|
|
5311ffb3b4 | ||
|
|
4f991e14a5 | ||
|
|
da17bb427f | ||
|
|
c10456f103 | ||
|
|
a31124c94b | ||
|
|
7f2588c514 | ||
|
|
bba796d818 | ||
|
|
7d3145b0a1 | ||
|
|
a874b034d7 | ||
|
|
0defd68316 | ||
|
|
a439eef72b | ||
|
|
e94068b0db | ||
|
|
71e5ff4bdb | ||
|
|
ccaaa55ca3 | ||
|
|
e2da8d58a0 | ||
|
|
e0cc0849ad | ||
|
|
19b376be8e | ||
|
|
59a119db66 | ||
|
|
2041b55e4b | ||
|
|
d51da2ff5c | ||
|
|
19aad7809b | ||
|
|
508a39908c | ||
|
|
aca16f3609 | ||
|
|
187542fef5 | ||
|
|
0972abb0b6 | ||
|
|
6256e1d7c8 | ||
|
|
9f7c675517 | ||
|
|
202a16288c | ||
|
|
8c68d4a7ad | ||
|
|
5e63775a97 | ||
|
|
3e96de6323 | ||
|
|
4e7d0aadc9 | ||
|
|
bbe80b34c9 | ||
|
|
3152430e47 | ||
|
|
81279b4761 | ||
|
|
aaa4668392 | ||
|
|
89de054d4d | ||
|
|
583b169680 | ||
|
|
1241a33a5d | ||
|
|
d0f6d04715 | ||
|
|
f8d7ca470d | ||
|
|
d0187414a5 | ||
|
|
aed88784d9 | ||
|
|
dc13312be6 | ||
|
|
a063951f9e | ||
|
|
6ac6893f4a | ||
|
|
25bfaca2c0 | ||
|
|
5728345a42 | ||
|
|
f60393e9fa | ||
|
|
028e523b6d | ||
|
|
d945d6a5d6 | ||
|
|
fed058fe51 | ||
|
|
c3e898c047 | ||
|
|
98db42f996 | ||
|
|
3f551f68e3 | ||
|
|
4c5f65ff54 | ||
|
|
8d65b8e751 | ||
|
|
339bd84540 | ||
|
|
25760d1ef8 | ||
|
|
c714657c62 | ||
|
|
9b72e6c6ff | ||
|
|
5e19f0e01c | ||
|
|
3c86f7ac29 | ||
|
|
7d2ea6110e | ||
|
|
5e500d58ca | ||
|
|
cb48d4f789 | ||
|
|
b7db009786 | ||
|
|
39f5b9ab66 | ||
|
|
97bb0f13c9 | ||
|
|
74e6d20ec5 | ||
|
|
3d3ac6adb3 | ||
|
|
06de729eba | ||
|
|
3f1d5a148e | ||
|
|
8498fef70e | ||
|
|
f9d84f916d | ||
|
|
3ff8607cc5 | ||
|
|
539cef2510 | ||
|
|
0da70afdff | ||
|
|
4e3a6f322b | ||
|
|
2a3746eb07 | ||
|
|
513c86dff3 | ||
|
|
35571903e0 | ||
|
|
a89fcbb275 | ||
|
|
b0e99161bb | ||
|
|
a441171000 | ||
|
|
892714550e | ||
|
|
b6388ccb13 | ||
|
|
6ab5de13bb | ||
|
|
38000f19d6 | ||
|
|
ad9e5e14fa | ||
|
|
663cd6df6a | ||
|
|
6bee1983eb | ||
|
|
9df53455e9 | ||
|
|
13dddb8b22 | ||
|
|
b47c96d91a | ||
|
|
93abc4dc09 | ||
|
|
ae266aba7f | ||
|
|
ab9eec63f5 | ||
|
|
8b8ed4c6ea | ||
|
|
6aaca7f568 | ||
|
|
0dcab2ccd1 | ||
|
|
10b3b7f081 | ||
|
|
79e7cee695 | ||
|
|
8eb5173ebc | ||
|
|
2acc7c4213 | ||
|
|
e7904a0f0f | ||
|
|
a045e9e40c | ||
|
|
68f10b41ad | ||
|
|
ad9327c4c0 | ||
|
|
53983bf8e2 | ||
|
|
aa3b5e173c | ||
|
|
4bceb09b1b | ||
|
|
0da3a1e13c | ||
|
|
3e6dd8687d | ||
|
|
5d2e8f5f98 | ||
|
|
84109910b2 | ||
|
|
8f0db49832 | ||
|
|
32e89c1299 | ||
|
|
a97a009d93 | ||
|
|
d4726e0816 | ||
|
|
df074e625b | ||
|
|
a4b1925b31 | ||
|
|
26d1cf102c | ||
|
|
a41a9e31cc | ||
|
|
159ac52acc | ||
|
|
c7e01d322a | ||
|
|
17582d543d | ||
|
|
986ca23ff7 | ||
|
|
acb2476d7d | ||
|
|
f5c38ffe77 | ||
|
|
ee6a32d159 | ||
|
|
e1d18bd621 | ||
|
|
a85dc47781 | ||
|
|
eecd14d763 | ||
|
|
42282a2012 | ||
|
|
6442318205 | ||
|
|
1ee76bfbea | ||
|
|
d558b7474d | ||
|
|
da49b0a154 | ||
|
|
01931bd177 | ||
|
|
c2ef761b01 | ||
|
|
d6ce246420 | ||
|
|
33c6315510 | ||
|
|
af0b8cd40a | ||
|
|
95c4ffaa1b | ||
|
|
08c5572f16 | ||
|
|
b49bc02dd2 | ||
|
|
65a61c5e47 | ||
|
|
237ddc3c74 | ||
|
|
dd6cfd34fe | ||
|
|
607f8bdf07 | ||
|
|
7b601da3a0 | ||
|
|
dc85b8c241 | ||
|
|
0a25b1b9c0 | ||
|
|
6021b458e6 | ||
|
|
e5f3bf75e3 | ||
|
|
7dd32d63da | ||
|
|
7376dce9ef | ||
|
|
ce9012f26f | ||
|
|
9042a60b28 | ||
|
|
9ff586b172 | ||
|
|
a082d9030c | ||
|
|
9814a24dcf | ||
|
|
05c36beae2 | ||
|
|
41e01b51bd | ||
|
|
4c2b2d487c | ||
|
|
dec76b1f71 | ||
|
|
b26b185c04 | ||
|
|
fb7707c6b6 | ||
|
|
ee80283265 | ||
|
|
a3119f9fc0 | ||
|
|
5c87e7e1cb | ||
|
|
9084d37db3 | ||
|
|
bf7bd6fbd7 | ||
|
|
50df631f8a | ||
|
|
0d779b4d47 | ||
|
|
071e940e6b | ||
|
|
9fe2a7377f | ||
|
|
e40ea50259 | ||
|
|
41e3e82984 | ||
|
|
1ba6c9753d | ||
|
|
0fe57025c6 | ||
|
|
e43ce195a4 | ||
|
|
c192ccec70 | ||
|
|
8a2ff6e69f | ||
|
|
6bce7efae0 | ||
|
|
df1ab88ef7 | ||
|
|
8f931a2a4c | ||
|
|
be46c362cf | ||
|
|
99d3442bef | ||
|
|
e66a877868 | ||
|
|
b9410305ef | ||
|
|
d5e7fceb21 | ||
|
|
7be7ad4806 | ||
|
|
7498f9820c | ||
|
|
9d4ed9a477 | ||
|
|
7a6aab6493 | ||
|
|
a992f0ddf3 | ||
|
|
87c3c6ef19 | ||
|
|
8c5f6f79c1 | ||
|
|
a6fde81c36 | ||
|
|
b085502a4d | ||
|
|
4da20f1ac1 | ||
|
|
ef200ee737 | ||
|
|
9b05da7131 | ||
|
|
f7c80a5c04 | ||
|
|
1e61c71c94 | ||
|
|
bf88924d3d | ||
|
|
daa8b7d292 | ||
|
|
3873ae0085 | ||
|
|
f630796da0 | ||
|
|
eac1859507 | ||
|
|
2d75a0a727 | ||
|
|
f62c99b387 | ||
|
|
7383bf8044 | ||
|
|
e12b1bf8dc | ||
|
|
0b52e5f747 | ||
|
|
161e4305e6 | ||
|
|
8e7d7ecba1 | ||
|
|
32f6a1de2d | ||
|
|
e3459b336e | ||
|
|
1a1e1b30a6 | ||
|
|
5eb98cacac | ||
|
|
9a7f9479d4 | ||
|
|
c21785855f | ||
|
|
887e1f38f5 | ||
|
|
79294acb05 | ||
|
|
5cb06eaf86 | ||
|
|
fe0057aa9f | ||
|
|
1e3ca30ace | ||
|
|
66356984b4 | ||
|
|
1fcc294ffb | ||
|
|
0d0fc38344 | ||
|
|
cd1759332d | ||
|
|
62d23ce36b | ||
|
|
678e168029 | ||
|
|
31a9cac20b | ||
|
|
79f40fb2c6 | ||
|
|
9738402921 | ||
|
|
e064ce02d0 | ||
|
|
862a5c42d8 | ||
|
|
d29a5083d1 | ||
|
|
60fc8eb22e | ||
|
|
ec4ef6e9fc | ||
|
|
42552e4d24 | ||
|
|
e46e4006d5 | ||
|
|
5cf22174dc | ||
|
|
7fd39abb2b | ||
|
|
d7e042bdb6 | ||
|
|
9006dc3812 | ||
|
|
f3bef3784f | ||
|
|
b8286bf6b4 | ||
|
|
3feb69a1da | ||
|
|
e94be3ca20 | ||
|
|
4776c45c33 | ||
|
|
1278b1aca9 | ||
|
|
da529fd71e | ||
|
|
e467e27448 | ||
|
|
de84fa25a8 | ||
|
|
b4ddfa7955 | ||
|
|
ce75b4388c | ||
|
|
46add22436 | ||
|
|
93ca38ed76 | ||
|
|
e0430b9899 | ||
|
|
5df5f38e83 | ||
|
|
2b15495835 | ||
|
|
ab366c79ef | ||
|
|
1074670da7 | ||
|
|
1a2cb105f3 | ||
|
|
3badef78c1 | ||
|
|
a1f4c82e3a | ||
|
|
478f5784ee | ||
|
|
471bf7f193 | ||
|
|
f346a80ce0 | ||
|
|
e849cc95b9 | ||
|
|
0bff1aab4c | ||
|
|
e6aa704841 | ||
|
|
9415ae085a | ||
|
|
4cb232a068 | ||
|
|
35958ebede | ||
|
|
688ac5dc91 | ||
|
|
e298a45902 | ||
|
|
176e187c6a | ||
|
|
15046f431e | ||
|
|
ecd4a1bb66 | ||
|
|
3fbed79209 | ||
|
|
fc472a4f77 | ||
|
|
bebc019daa | ||
|
|
b3f235ded6 | ||
|
|
7b223aafc2 | ||
|
|
ffe0eff61a | ||
|
|
be0e324d0b | ||
|
|
fadfde1775 | ||
|
|
32899e9c41 | ||
|
|
15f48991c3 | ||
|
|
642c0dd2ce | ||
|
|
a08b24ed5d | ||
|
|
74e8be5a13 | ||
|
|
7895d6dee9 | ||
|
|
8e6d8f7c30 | ||
|
|
36785bc0f1 | ||
|
|
977c37ef83 | ||
|
|
52d895b1d0 | ||
|
|
230ddcbe21 | ||
|
|
1ee5828747 | ||
|
|
314980a667 | ||
|
|
2783d90cf6 | ||
|
|
5fbdbb8d42 | ||
|
|
b7ba976fb2 | ||
|
|
a6eb9e63e5 | ||
|
|
7d694c76a5 | ||
|
|
cbc56937e6 | ||
|
|
1a9fa9b970 | ||
|
|
cd82ea5484 | ||
|
|
57898ab010 | ||
|
|
2dbb111943 | ||
|
|
cb6323d153 | ||
|
|
08941588aa | ||
|
|
e183369ef6 | ||
|
|
702e180de6 | ||
|
|
a265f82980 | ||
|
|
021899906d | ||
|
|
f022c48f6c | ||
|
|
3e07b8400d | ||
|
|
5433894f20 | ||
|
|
a6d84de417 | ||
|
|
6f76d4ece5 | ||
|
|
17778ad84b | ||
|
|
518585227d | ||
|
|
ad0631f701 | ||
|
|
01c19628a9 | ||
|
|
44614dba9d | ||
|
|
621db81c7d | ||
|
|
c01749eba2 | ||
|
|
2ff14e4224 | ||
|
|
7685c2eb1e | ||
|
|
2de2bac211 | ||
|
|
bdc69f047a | ||
|
|
f20177b5f9 | ||
|
|
73b2127f87 | ||
|
|
32b309c878 | ||
|
|
9cf1b07954 | ||
|
|
5b73ccb8be | ||
|
|
c4188c47d6 | ||
|
|
2429574182 | ||
|
|
42ec50df27 | ||
|
|
37e96a9dae | ||
|
|
f3ae760c40 | ||
|
|
97c302822a | ||
|
|
e92929e148 | ||
|
|
9d3d11800a | ||
|
|
a956d5ae78 | ||
|
|
52e070dcf6 | ||
|
|
605eb70aed | ||
|
|
0c2a1bc249 | ||
|
|
51e763b967 | ||
|
|
86998c82d6 | ||
|
|
c2287a7fe6 | ||
|
|
f09ab44280 | ||
|
|
08807db2e9 | ||
|
|
201479ced6 | ||
|
|
ce49c8b6ba |
19
.claude/CLAUDE.md
Normal file
19
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# XRPL Dev Portal — Claude Code Instructions
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Framework:** Redocly Realm
|
||||
- **Production branch:** `master`
|
||||
- **Local preview:** `npm start`
|
||||
|
||||
## Localization
|
||||
|
||||
- Default: `en-US`
|
||||
- Japanese: `ja`
|
||||
- Translations mirror `docs/` structure under `@l10n/<language-code>/`
|
||||
|
||||
## Navigation
|
||||
|
||||
- Update `sidebars.yaml` when adding new doc pages
|
||||
- Blog posts have a separate `blog/sidebars.yaml`
|
||||
- Redirects go in `redirects.yaml`
|
||||
268
.claude/commands/figma-build-page.md
Normal file
268
.claude/commands/figma-build-page.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# Figma Build Page
|
||||
|
||||
Build a `.page.tsx` file from a Figma design URL by mapping each Figma section to an existing shared section component, and export any new images/icons from Figma into the project's asset folders.
|
||||
|
||||
**Usage:** `/figma-build-page <figma-url> <target-page-file>`
|
||||
|
||||
**Example:** `/figma-build-page https://www.figma.com/design/7vhTxPgH1in2AjIETuX49Z/Documentation?node-id=0-1 docs/index.page.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Parse the input
|
||||
|
||||
Extract from `$ARGUMENTS`:
|
||||
- **Figma URL**: extract `fileKey` and `nodeId` (convert `-` to `:` in nodeId)
|
||||
- **Target file**: the `.page.tsx` file path to rebuild
|
||||
- **Page slug**: derive from the target file path (e.g. `docs/index.page.tsx` → `docs`, `resources/about.page.tsx` → `resources-about`). This is used to prefix shared media filenames.
|
||||
|
||||
### Step 2 — Understand the design structure
|
||||
|
||||
Run in parallel:
|
||||
1. Read the current target page file to understand what already exists
|
||||
2. Run `find shared/sections -name "*.tsx"` and `find shared/components -name "*.tsx"` to inventory available components
|
||||
3. Run `find static/img/icons/2026 static/img/logos/black static/img/bds-2026 -type f` to inventory existing assets so the build can reuse them instead of re-exporting duplicates
|
||||
4. Call `mcp__claude_ai_Figma__get_metadata` on the **page root** (the parent of the `nodeId` you were given) — pass `nodeId="0:1"` if you don't know the page id, otherwise walk up one level from the given node. The goal is to see ALL sibling frames on the canvas, not just the LG/MD/SM page mockups. Section frames alone are not enough — designers also park reference data in sibling frames.
|
||||
|
||||
#### Step 2A — Locate the Assets frame (REQUIRED, do this before anything else design-side)
|
||||
|
||||
Designers park clean, isolated versions of the page's icons, logos, photos, and **canonical card/list content** in a sibling frame on the same canvas — typically named `Assets`, `Assets Library`, `Exports`, `Resources`, `Content`, or similar. It lives outside the main `LG`/`MD`/`SM` page frames, usually far to the right or below them.
|
||||
|
||||
From the page-root metadata, find any frame whose name matches `/asset|export|library|resource|content/i` OR whose x/y position places it clearly outside the LG/MD/SM stack. Record:
|
||||
- Its `nodeId` and name
|
||||
- Every child node's `nodeId`, layer name, and visual role (icon vs photo vs logo vs card content)
|
||||
|
||||
**Then immediately call `mcp__claude_ai_Figma__get_design_context` on the Assets frame** to extract its contents — child layer names, image asset URLs, and any text labels. Do this BEFORE fetching the section contexts in Step 3, because the Assets frame is the source of truth for:
|
||||
- **Real partner/customer logos** — the LG frame typically shows placeholder logos (e.g. the same logo repeated 8×); the Assets frame holds the real brand SVGs.
|
||||
- **Full carousel card sets** — the LG frame may only show 3-4 visible slides (with duplicates as overflow placeholders); the Assets frame holds the full N-card list with no duplicates.
|
||||
- **Canonical icon artwork** — the icon inside a section is often a low-fidelity instance of the master icon in the Assets frame.
|
||||
- **Per-card hrefs, hidden subtitles, alt text, video URLs** — content the section mockup doesn't have room to show.
|
||||
|
||||
Build an **Assets Index**: `{ layerName, nodeId, role, imageAssetUrl?, associatedSectionHint? }` for every child. Use the layer name to associate each asset with a section (e.g. layer `RWA Partner — Archax` belongs in the logo grid; layer `Carousel Card 04 — Onchain Trading` belongs in the carousel).
|
||||
|
||||
**If the page also has annotations pointing at the Assets frame** (e.g. `data-annotations="Logo - see right side asset frame"`, `"Full list in Assets"`, `"Cards continue in Assets frame"`), treat that as a hard contract: the Assets frame is the authoritative content for that section, and the LG mockup is just a visual stub.
|
||||
|
||||
**Fail-loud if no Assets frame is found:** if you can't find a sibling frame and the page has more than one media/logo/icon, surface this to the user before continuing — e.g. "I didn't find an Assets/Exports frame on this canvas. The LG mockup shows N placeholder logos and a 3-card carousel — proceed treating those as the final content, or point me at the Assets frame?" Don't silently fall back to the LG mockup when the design clearly references external content.
|
||||
|
||||
### Step 3 — Fetch design context for each section (cross-reference with Assets frame)
|
||||
|
||||
For every top-level section frame found in the metadata, call `mcp__claude_ai_Figma__get_design_context` in parallel batches (max 3 at a time). For each section, gather:
|
||||
- The heading/title text
|
||||
- Any body/description text
|
||||
- All link labels and their annotation `href` values (from `data-annotations`)
|
||||
- Color variants visible (background color tells you which variant to use)
|
||||
- The arrangement (left/right, content vs media position)
|
||||
- Every distinct image/icon node — capture its Figma `nodeId`, layer name, fill color, and visual role (icon vs photo, monochrome vs colored)
|
||||
|
||||
**Then cross-reference against the Assets Index from Step 2A:**
|
||||
- For carousels/lists: if the LG mockup shows fewer cards than the Assets frame, USE THE ASSETS-FRAME COUNT and content. If cards in the mockup look like exact duplicates (same icon + same text), treat them as overflow placeholders and drop them in favor of the Assets-frame set.
|
||||
- For logo grids: if the mockup repeats the same logo, IGNORE that and use the distinct logos from the Assets frame.
|
||||
- For media: if the section's media node and an Assets-frame node share a layer name or visible image, prefer the Assets-frame node — it's higher fidelity and isolated from section chrome.
|
||||
- For icons: if a section icon has a matching layer name in the Assets frame, export from the Assets frame, not the section instance.
|
||||
|
||||
**Important:** Note any Figma annotations that say content should be removed or changed, OR that redirect to the Assets frame ("see right side asset frame", "real logos in assets", etc.) — annotations override what the mockup shows.
|
||||
|
||||
### Step 4 — Map sections to existing components
|
||||
|
||||
For each Figma section, find the best matching component from `shared/sections/`. Common mappings:
|
||||
|
||||
| Figma Section Name | Component | Import |
|
||||
|---|---|---|
|
||||
| `headerHeroPrimaryMedia` | `HeaderHeroPrimaryMedia` (default export) | `shared/sections/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia` |
|
||||
| `CarouselCardListOffGrid` | `CarouselCardList` | `shared/sections/CarouselCardList/CarouselCardList` |
|
||||
| `TextGridCard` | `CardsTextGrid` | `shared/sections/CardsTextGrid/CardsTextGrid` |
|
||||
| `VideoFeatured` | `FeaturedVideoHero` (default export) | `shared/sections/FeaturedVideoHero/FeaturedVideoHero` |
|
||||
| `LinkSmallGrid` | `LinkSmallGrid` | `shared/sections/LinkSmallGrid/LinkSmallGrid` |
|
||||
| `LinkTextDirectory` | `LinkTextDirectory` | `shared/sections/LinkTextDirectory/LinkTextDirectory` |
|
||||
| `FeatureTwoColumn` | `FeatureTwoColumn` | `shared/sections/FeatureTwoColumn/FeatureTwoColumn` |
|
||||
| `LinkSmallTiles` | `SmallTilesSection` | `shared/sections/SmallTilesSection/SmallTilesSection` |
|
||||
| `IconTextGridCard` | `CardsIconGrid` | `shared/sections/CardsIconGrid/CardsIconGrid` |
|
||||
| `StandardCard` group | `StandardCardGroupSection` | `shared/sections/StandardCardGroupSection/StandardCardGroupSection` |
|
||||
|
||||
Read the prop interfaces of each matched component before writing (`head -60` of the component file is usually enough).
|
||||
|
||||
**Key prop notes:**
|
||||
- `HeaderHeroPrimaryMedia` / `FeaturedVideoHero` — **default exports**; all others are named exports
|
||||
- `FeaturedVideoHero` video source: `{ type: 'embed', embedUrl: 'https://www.youtube.com/embed/VIDEO_ID' }`
|
||||
- `FeatureTwoColumn` `description` prop is required — pass `""` if the Figma shows no body text
|
||||
- `LinkTextDirectory` buttons use `ButtonConfig`: `{ label: string; href?: string }`
|
||||
- `StandardCardGroupSection` callsToAction uses `DesignConstrainedButtonProps`: `{ children: ReactNode; href?: string }`
|
||||
- `CardsTextGrid` / `CardsIconGrid` `description` field accepts `React.ReactNode` — use this for inline links
|
||||
- `SmallTilesSection` uses existing logo assets: `require('../static/img/logos/black/javascript.svg')` etc.
|
||||
|
||||
### Step 5 — Determine color variants from Figma
|
||||
|
||||
Match Figma background colors to component variant props:
|
||||
- White / `#ffffff` → `"neutral"` or `"gray"` (default)
|
||||
- Green `#70ee97` / `#21e46b` → `"green"`
|
||||
- Lilac/purple `#d9caff` → `"lilac"`
|
||||
- Yellow → `"yellow"`
|
||||
|
||||
### Step 6 — Plan asset exports
|
||||
|
||||
For every image/icon node identified in Step 3, decide:
|
||||
|
||||
**A) Source node — always prefer the Assets frame:** For each asset slot in the page, look up the matching node in the Assets Index from Step 2A by layer name or visual match. **Export from the Assets-frame node, not the section instance.** Section instances are often scaled-down thumbnails or shared placeholders; the Assets frame holds the master artwork at its intended export size. Only fall back to the section instance if the Assets frame genuinely doesn't have a counterpart.
|
||||
|
||||
**B) Reuse vs export:** If a visually-equivalent asset already exists in the inventory from Step 2 (same role, same color), reuse its path and skip export. Otherwise schedule a new export.
|
||||
|
||||
**C) Target folder** — route by section type and asset role:
|
||||
|
||||
| Section / role | Folder | Format |
|
||||
|---|---|---|
|
||||
| `CarouselCardList` card icons (monochrome glyphs) | `static/img/icons/2026/black/` | `.svg` |
|
||||
| `CardsIconGrid` card icons (colored glyphs) | `static/img/icons/2026/color/<color>/` | `.svg` |
|
||||
| `SmallTilesSection` card icons (SDK / language logos) | `static/img/logos/black/` | `.svg` |
|
||||
| `LogoRectangleGrid` partner logos (rendered from inner `Logo` frame, 1.5× scale) | `static/img/logos/black/` | `.png` |
|
||||
| `HeaderHeroPrimaryMedia` hero media (photo) | `static/img/bds-2026/` | `.jpg` (composite raw asset onto Figma bg color at native size, see Step 7) |
|
||||
| `HeaderHeroSplitMedia` hero media (photo) | `static/img/bds-2026/` | `.jpg` (composite raw asset onto Figma bg color at native size, see Step 7) |
|
||||
| `FeatureTwoColumn` media (photo) | `static/img/bds-2026/` | `.jpg` (composite raw asset onto Figma bg color at native size, see Step 7) |
|
||||
| `FeatureSingleTopic` media (photo) | `static/img/bds-2026/` | `.jpg` (composite raw asset onto Figma bg color at native size, see Step 7) |
|
||||
| `CardsFeatured` / `logoRectangleGrid`-as-image-cards card media | `static/img/bds-2026/` | `.jpg` (screenshot card-image frame at 1.5×, see Step 7) |
|
||||
| `FeaturedVideoHero` poster (if used) | `static/img/bds-2026/` | `.jpg` (composite raw asset onto Figma bg color at native size, see Step 7) |
|
||||
| `CarouselFeatured` slide HeroMedia (photo) | `static/img/bds-2026/` | `.jpg` (composite raw asset onto Figma bg color at native size, see Step 7) |
|
||||
| Any other colored decorative icon | `static/img/icons/2026/color/<color>/` | `.svg` |
|
||||
| Any other monochrome icon | `static/img/icons/2026/black/` | `.svg` |
|
||||
|
||||
For the colored-icon folders, pick `<color>` from the Figma fill on the icon (or, if the icon inherits color from a parent component, the parent section's variant prop). Existing folders: `lilac`. Create a new sibling folder (`green`, `yellow`, etc.) if the design uses a new color — never dump differently-colored icons into the wrong folder.
|
||||
|
||||
**D) Filename — always kebab-case:**
|
||||
|
||||
1. Start from the Figma layer name.
|
||||
2. Lowercase, replace spaces / underscores / camelCase boundaries with `-`, strip non-`[a-z0-9-]`.
|
||||
3. For shared photographic media in `bds-2026/`, **prefix with the page slug** to avoid cross-page collisions:
|
||||
- hero media → `<page-slug>-hero-media.jpg`
|
||||
- feature media (multi) → `<page-slug>-feature-media-1.jpg`, `<page-slug>-feature-media-2.jpg`, …
|
||||
4. Icons and SDK logos are not page-prefixed — they're meant to be reusable across pages.
|
||||
|
||||
Examples:
|
||||
- Figma layer `Ready to Use Code Samples` (CarouselCardList icon) → `static/img/icons/2026/black/ready-to-use-code-samples.svg`
|
||||
- Figma layer `XRPL Server` (CardsIconGrid icon, lilac fill) → `static/img/icons/2026/color/lilac/xrpl-server.svg`
|
||||
- Figma layer `Hero Image` on `docs/index.page.tsx` → `static/img/bds-2026/docs-hero-media.jpg`
|
||||
- Figma layer `Feature 02` on `resources/about.page.tsx` → `static/img/bds-2026/resources-about-feature-media-2.jpg`
|
||||
- Figma frame `Logo_Circle` → `Logo` (inner, LogoRectangleGrid partner) → `static/img/logos/black/circle.png` (1.5× PNG, see Step 7)
|
||||
- Figma frame `Logo_Ondo` → `Logo` (inner, LogoRectangleGrid partner) → `static/img/logos/black/ondo.png`
|
||||
|
||||
### Step 7 — Export the assets from Figma
|
||||
|
||||
For each scheduled export from Step 6 (sourced from the Assets frame per Step 6A whenever possible):
|
||||
|
||||
1. Ensure the target folder exists (`mkdir -p` it if not).
|
||||
2. **Get the raw Figma asset URL.** Calling `mcp__claude_ai_Figma__get_design_context` on a frame returns inline TS that declares `const imgXxx = "https://www.figma.com/api/mcp/asset/<uuid>"` constants — one per leaf image/icon node. These URLs serve the **raw underlying asset** (SVG for vector icons, PNG for photos/rasters), NOT a re-rendered screenshot of the surrounding chrome. Pull the URL for the specific Assets-frame child you mapped to this slot.
|
||||
3. **Download with `curl -sL`** for raw vector assets that render as-is with no framing chrome — typically SVG icons and SDK/language logos:
|
||||
- SVG icons / vector logos → `curl -sL <url> -o <target>.svg` — Figma serves the actual SVG markup for vector nodes, so no conversion is needed.
|
||||
- Verify the format with `file <path>` after downloading the first one of each type, in case Figma serves something unexpected.
|
||||
|
||||
3a. **Photographic media with backgrounds — composite the raw image onto the design's background color, save as JPG at native size.** Most media nodes in the design system (hero media, FeatureTwoColumn media, FeatureSingleTopic media, FeaturedVideoHero poster, CardsFeatured card image, CarouselFeatured slide hero) live inside a wrapper frame named `HeroMedia` / `FeatureMedia` / similar. In Figma, that wrapper frame may have a background fill (e.g. `bg-neutral/100` = `#f0f3f7`, `bg-neutral/200` = `#e6eaf0`) BEHIND a foreground illustration. The raw `imgXxx` URL gives you the foreground image only — strips the background, leaves transparent edges. When the JPG converter then flattens to white, the image looks "floating on white" instead of "sitting on the gray tile" the designer intended.
|
||||
|
||||
To capture the composited image + background fill as a flat JPG **at the source image's native size** (no upscale):
|
||||
|
||||
1. **Pull the asset URL from the Assets frame.** This is the priority. Calling `get_design_context` on the Assets frame returns the master `imgXxx = "https://www.figma.com/api/mcp/asset/<uuid>"` constants — clean, isolated, full-size. Only fall back to the section instance's asset URL when the Assets frame has no counterpart.
|
||||
|
||||
2. **Inspect the design context to find the background color** the image sits on. Look at the wrapper frame around the `<img>` for a sibling/child div with `bg-[var(--neutral\/100,#f0f3f7)]`, `bg-[var(--neutral\/200,#e6eaf0)]`, or similar. If there's no bg div, use `#ffffff`.
|
||||
|
||||
3. **Download the raw asset:** `curl -sL <imgXxx-url> -o /tmp/<name>.png`. Most photo nodes return PNG with alpha (illustrations / partial photos); some return JPEG (full-bleed photos with no alpha).
|
||||
|
||||
4. **Composite onto the background color and save as JPG using ImageMagick** (install once with `brew install imagemagick`):
|
||||
```
|
||||
magick /tmp/<name>.png -background "<hex>" -alpha remove -alpha off -quality 85 <target>.jpg
|
||||
```
|
||||
**No `-resize` flag** — preserve the source's native dimensions. (For very large source PNGs above ~2000px, you may add `-resize "1500x1500>"` to cap size for web, but otherwise leave the native size alone — the designer chose it.) The `-alpha remove -alpha off` flattens alpha onto the `-background` color BEFORE the JPEG encoder strips alpha to white, giving the correct background instead of white.
|
||||
|
||||
**Size budget — ≤250 KB per non-hero photo, ≤500 KB for the hero.** After step 4, check the JPG size. If a non-hero photo lands above ~250 KB (a hero may go to ~500 KB), re-encode at lower quality:
|
||||
```
|
||||
magick <target>.jpg -resize "1500x1500>" -quality 78 /tmp/recomp.jpg && mv /tmp/recomp.jpg <target>.jpg
|
||||
```
|
||||
Why: cumulative image weight across a single page's `require()` calls (often 5–10 images at ~250 KB) plus the rest of the bundle pushes Redocly's cloud builder close to its memory cap. Three outlier 600–670 KB photos on one branch contributed to an OOM build failure; consistent ≤250 KB compression keeps headroom.
|
||||
|
||||
5. Save with the `<page-slug>-feature-media-N.jpg` (or `-hero-media.jpg`, `-video-poster.jpg`, etc.) naming convention from Step 6.
|
||||
|
||||
**DO NOT use `get_screenshot` on the media frame** — instance children inside design-system components have IDs like `I<sectionId>;<...>` which are not screenshotable, and the MCP rejects them. The composite-from-raw-asset approach above works for all cases.
|
||||
|
||||
**Sanity-check the first one.** After exporting the first media of each kind, eyeball the JPG: the foreground illustration should sit on a uniform colored tile (`#f0f3f7` light gray, `#e6eaf0` slightly darker gray, etc.) — NOT on white when the design called for gray. If it's still white-flat, you used the wrong `-background` hex; re-read the Figma frame's `bg-` value.
|
||||
4. **LogoRectangleGrid partner logos — special procedure.** Multi-layer brand logos (Circle, Zeconomy, DB Schenker, etc.) decompose into many vector pieces under `get_design_context`, so the raw-asset-URL technique above gives you fragments, not a composite. Use this instead:
|
||||
1. Target the **inner `Logo` frame** (the 170×96 child of `Logo_<Name>`), not the gray-tile parent (`Logo_<Name>`) and not the bottom-most leaf layer (e.g. `Ondo finance`, `DB Schenker_Logo_0 1`).
|
||||
2. Call `mcp__claude_ai_Figma__get_screenshot` on that inner Logo frame with `contentsOnly: true` and `maxDimension: 256` to request the longer-edge at ~1.5× the native 170 px. Note that Figma's MCP does not supersample beyond a frame's native render size, so the returned PNG is typically still 170×96 — that's expected, not an error.
|
||||
3. Download the PNG: `curl -sL <image_url> -o /tmp/<name>.png`.
|
||||
4. Upscale to the 1.5× target dimensions (255×144) using `sips`: `sips -z 144 255 /tmp/<name>.png --out static/img/logos/black/<name>.png`. This gives a crisper-on-retina raster at the design's intended display proportion, even if the resampled pixels don't add new detail.
|
||||
5. Save as `.png` (not `.svg` — partner logos in the grid are raster). Filename is kebab-case of the layer name with the `Logo_` prefix stripped (e.g. `Logo_DBS` → `db-schenker.png` — use the visually-recognized brand name, not the literal Figma slug if it's an abbreviation).
|
||||
6. **Surface logo-files annotations.** Designers often annotate one of the logo frames (e.g. `Logo_Circle`) with `data-assets-annotations` text like `"Need logo files"` plus a Google Drive link. When present, add a `MISSING_ASSETS` entry: `{ note: "Partner logos are placeholder reproductions — final brand-approved files pending from <Drive URL>", affects: [list of logo paths] }`. The exports are still useful as stand-ins until the user swaps in real assets.
|
||||
5. **Do NOT use `get_screenshot` on the section frame** to capture media — that re-renders the whole section (chrome, text, padding) and the resulting image cannot be used as a clean media src. The right target for media is the **named media frame inside the section** (e.g. `HeroMedia`, `FeatureMedia`, the `CardImage` media child) per Step 3a above, not the section itself and not the leaf `<img>` (which would strip the background fill).
|
||||
6. **Fallback when export fails or returns nothing usable:** create a zero-byte placeholder file at the target path AND record `{ figmaNodeId, layerName, targetPath, reason }` in a `MISSING_ASSETS` list. Do not silently drop the asset.
|
||||
|
||||
After all attempted exports, run `ls -la` on each unique target folder so the user can see what landed.
|
||||
|
||||
### Step 8 — Write the implementation (Plan Mode)
|
||||
|
||||
Present a plan first showing:
|
||||
- All sections in order with their mapped component
|
||||
- The full content (text, links, variants) for each section
|
||||
- Any content removed per Figma annotations
|
||||
- **For each list/grid/carousel section, the canonical item count and source** — e.g. "Logo grid: 8 distinct partner logos from Assets frame (LG mockup showed 8× placeholder, ignored)" or "Carousel: 5 cards from Assets frame (LG mockup showed 3 + 2 duplicates, kept 5)". Make it explicit that the Assets frame won, so the user can spot if something was missed.
|
||||
- **The asset manifest**: for every image/icon, show `figma layer → source (assets-frame node / section-instance fallback) → target path → status (exported | reused | missing)`
|
||||
|
||||
After approval, write the full `.page.tsx`:
|
||||
1. Keep the existing `frontmatter` export (SEO metadata)
|
||||
2. Import all section components at the top
|
||||
3. **Import and use the translate hook inside the default-exported component:**
|
||||
```tsx
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
||||
// ...
|
||||
export default function MyPage() {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
return (/* ... */);
|
||||
}
|
||||
```
|
||||
4. **Wrap every user-facing string in `translate(...)`** — this includes every prop value that renders as visible text. No bare string literals in JSX text or text-bearing props. Apply to:
|
||||
- `headline`, `subtitle`, `title`, `description`, `heading`, `label`, `iconAlt`, `alt` props
|
||||
- Every entry's `title`, `description`, `label`, `heading` inside `cards`, `links`, `buttons` arrays
|
||||
- `children` strings (including `callsToAction[].children` on `StandardCardGroupSection`)
|
||||
- Inline-link anchor text inside `React.ReactNode` descriptions (e.g. `<a href="...">{translate('Read More')}</a>`)
|
||||
- **Do not** translate URLs (`href`, `src`), variant strings (`"green"`, `"left"`), or `embedUrl` values
|
||||
- Example pattern (mirrors the project's house style):
|
||||
```tsx
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline={translate("XRP Ledger (XRPL) Documentation")}
|
||||
subtitle={translate("Explore XRPL documentation with our essential guide for developers and admins who want to start building and integrating with the XRP Ledger.")}
|
||||
media={{ type: 'image', src: require('../static/img/bds-2026/docs-hero-media.jpg'), alt: translate('XRPL Documentation') }}
|
||||
/>
|
||||
```
|
||||
5. Render sections in Figma order inside a wrapper `<div className="landing">`
|
||||
6. For every image/icon, reference it via `require('../static/img/...')` using the exact path from the manifest — even if the export failed (the placeholder file holds the path)
|
||||
7. Apply all link `href` values from Figma `data-annotations`
|
||||
8. Use `React.ReactNode` descriptions to embed inline links where headings link to pages
|
||||
|
||||
### Step 9 — Fix TypeScript errors
|
||||
|
||||
After writing, check IDE diagnostics and fix:
|
||||
- Default vs named export mismatches
|
||||
- Wrong video source type (`type: 'embed'` not `type: 'embed_url'`)
|
||||
- Missing required props
|
||||
- `callsToAction` tuple shape `[primary, secondary?]`
|
||||
|
||||
### Step 10 — Report
|
||||
|
||||
End the run by printing:
|
||||
- The list of exported assets and their final paths
|
||||
- The `MISSING_ASSETS` list (if any) so the user knows exactly which Figma nodes need manual export
|
||||
- The full target file path
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- **Never install Tailwind** — the project uses SCSS + Bootstrap
|
||||
- **Never create new components** — only use what exists in `shared/sections/` and `shared/components/`
|
||||
- **The Assets frame is the source of truth.** Before fetching any section context, find and read the sibling Assets/Exports/Library/Resources frame on the page canvas. Use it for: real partner logos (not LG mockup placeholders), full carousel/list item sets (not the truncated visible cards), master icon artwork, and per-item hrefs the mockup hides. If you can't find an Assets frame and the design references one (via annotations or repeated placeholders), surface that to the user before guessing.
|
||||
- **Carousel and logo-grid item counts come from the Assets frame.** If the LG mockup shows repeated identical cards/logos, treat them as overflow placeholders and use the Assets-frame count instead.
|
||||
- **Export from Assets-frame nodes, not section instances.** Section instances are usually thumbnails of the master artwork in the Assets frame.
|
||||
- **Always apply links** from Figma annotations to make lists navigable
|
||||
- **Respect Figma notes** — if a section is annotated for removal, skip it; if it's annotated to redirect content to the Assets frame ("see right side asset frame", "logos in assets", etc.), follow that pointer rather than using the visible mockup content
|
||||
- **Light mode only** — follow the light mode variant of any design that shows both
|
||||
- **Always use `require('../static/img/...')` for images** — every image/icon prop must point at a real path under `static/img/` following the folder routing in Step 6. Never pass `src=""` and never inline a remote URL.
|
||||
- **Kebab-case all new asset filenames.** Shared photographic media in `bds-2026/` must be prefixed with the page slug; icons and logos are not page-prefixed.
|
||||
- **Reuse existing assets** when the inventory in Step 2 already has a matching icon/logo — do not re-export duplicates with new names.
|
||||
- **Never delete or overwrite an existing asset** without explicit user confirmation. If an export would collide with an existing file, rename the new export with a `-2` suffix and flag it in the plan.
|
||||
- **Always use `translate(...)` for visible text.** Import `useThemeHooks` from `@redocly/theme/core/hooks`, call `const { useTranslate } = useThemeHooks(); const { translate } = useTranslate();` at the top of the component, and wrap every user-facing string in `translate(...)`. Never leave a bare string literal in a text-bearing prop or as JSX text. URLs, variant enums, and embed URLs are not text — leave those untranslated.
|
||||
13
.claude/settings.json
Normal file
13
.claude/settings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__claude_ai_Figma__get_design_context",
|
||||
"mcp__claude_ai_Figma__get_metadata",
|
||||
"mcp__claude_ai_Figma__get_screenshot",
|
||||
"mcp__claude_ai_Figma__whoami"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(git push *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
117
.claude/skills/generate-release-notes/SKILL.md
Normal file
117
.claude/skills/generate-release-notes/SKILL.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: generate-release-notes
|
||||
description: Generate and sort rippled release notes from GitHub commit history
|
||||
argument-hint: --from <ref> --to <ref> [--date YYYY-MM-DD] [--output <path>]
|
||||
allowed-tools: Bash, Read, Edit, Write, Grep, Glob
|
||||
effort: max
|
||||
---
|
||||
|
||||
# Generate rippled Release Notes
|
||||
|
||||
This skill generates a draft release notes blog post for a new rippled version, then sorts the entries into the correct subsections.
|
||||
|
||||
## Execution constraints
|
||||
|
||||
- **Do NOT write scripts** to sort or process the file. Prefer the Edit tool for targeted changes. Use Write only when replacing large sections that are impractical to edit incrementally.
|
||||
- **Output progress**: Before each major step (generating raw release notes, reviewing file, processing amendments, sorting entries, reformatting, cleanup), output a brief status message so the user can see progress.
|
||||
|
||||
## Step 1: Generate the raw release notes
|
||||
|
||||
Run the Python script from the repo root. Pass through all arguments from `$ARGUMENTS`:
|
||||
|
||||
```bash
|
||||
python3 tools/generate-release-notes.py $ARGUMENTS
|
||||
```
|
||||
|
||||
If the user didn't provide `--from` or `--to`, ask them for the base and target refs (tags or branches).
|
||||
|
||||
The script will:
|
||||
- Fetch the version string from `BuildInfo.cpp`
|
||||
- Fetch all commits between the two refs
|
||||
- Fetch PR details (title, link, labels, files, description) via GraphQL
|
||||
- Compare `features.macro` between refs to identify amendment changes
|
||||
- Auto-sort amendment entries into the Amendments section
|
||||
- Output all other entries as unsorted with full context
|
||||
|
||||
## Step 2: Review the generated file
|
||||
|
||||
Read the output file (path shown in script output). Note the **Full Changelog** structure:
|
||||
- **Amendments section**: Contains auto-sorted entries and an HTML comment listing which amendments to include or remove
|
||||
- **Empty subsections**: Features, Breaking Changes, Bug Fixes, Refactors, Documentation, Testing, CI/Build
|
||||
- **Unsorted entries**: After the **Bug Bounties and Responsible Disclosures** section is an unsorted list of entries with title, link, labels, files, and description for context
|
||||
|
||||
## Step 3: Process amendments
|
||||
|
||||
Handle Amendments first, before sorting other entries.
|
||||
|
||||
**3a. Process the auto-sorted Amendments subsection:**
|
||||
The HTML comment contains three lists — follow them exactly:
|
||||
- **Include**: Keep these entries.
|
||||
- **Exclude**: Remove these entries.
|
||||
- Entries on **neither** list: Remove these entries.
|
||||
|
||||
**3b. Scan unsorted entries for unreleased amendment work:**
|
||||
Search through ALL unsorted entries for titles, labels, descriptions, or files that reference amendments on the "Exclude" or "Other amendments not part of this release" lists. Remove entries that directly implement, enable, fix, or refactor these amendments. Keep entries that are general changes that merely reference the amendment as motivation — if the code change is useful on its own regardless of whether the amendment ships, keep it.
|
||||
|
||||
**3c. If you disagree with any amendment decisions, make a note to the user but do NOT deviate from the rules.**
|
||||
|
||||
## Step 4: Sort remaining unsorted entries into subsections
|
||||
|
||||
Move each remaining unsorted entry into the appropriate subsection.
|
||||
|
||||
Use these signals to categorize:
|
||||
|
||||
**Files changed** (strongest signal):
|
||||
- Only `.github/`, `CMakeLists.txt`, `conan*`, CI config files → **CI/Build**
|
||||
- Only `src/test/`, `*_test.cpp` files → **Testing**
|
||||
- Only `*.md`, `docs/` files → **Documentation**
|
||||
|
||||
**Labels** (strong signal):
|
||||
- `Bug` label → **Bug Fixes**
|
||||
|
||||
**Title prefixes** (medium signal):
|
||||
- `fix:` → **Bug Fixes**
|
||||
- `feat:` → **Features**
|
||||
- `refactor:` → **Refactors**
|
||||
- `docs:` → **Documentation**
|
||||
- `test:` → **Testing**
|
||||
- `ci:`, `build:`, `chore:` → **CI/Build**
|
||||
|
||||
**Description content** (when other signals are ambiguous):
|
||||
- Read the PR description to understand the change's purpose
|
||||
- PRs that change API behavior, remove features, or have "Breaking change" checked in their description → **Breaking Changes**
|
||||
|
||||
Additional sorting guidance:
|
||||
- Watch for revert pairs: If a PR was committed and then reverted (or vice versa), check that the net effect is accounted for — don't include both.
|
||||
|
||||
## Step 5: Reformat sorted entries
|
||||
|
||||
After sorting, reformat each entry to match the release notes style.
|
||||
|
||||
**Amendment entries** should follow this format:
|
||||
```markdown
|
||||
- **amendmentName**: Description of what the amendment does. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Use more detail for amendment descriptions since they are the most important. Use present tense.
|
||||
- If there are multiple entries for the same amendment, merge into one, prioritizing the entry that describes the actual amendment.
|
||||
|
||||
**Feature and Breaking Change entries** should follow this format:
|
||||
```markdown
|
||||
- Description of the change. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Keep the description concise. Use past tense.
|
||||
|
||||
**All other entries** should follow this format:
|
||||
```markdown
|
||||
- The PR title of the entry. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Copy the PR title as-is. Only fix capitalization, remove conventional commit prefixes (fix:, feat:, ci:, refactor:, docs:, test:, chore:, build:), and adjust to past tense if needed. Do NOT rewrite, paraphrase, or summarize.
|
||||
|
||||
## Step 6: Clean up
|
||||
|
||||
- Add a short and generic description of changes to the existing `seo.description` frontmatter, e.g., "This version introduces new amendments and bug fixes." Do not create long lists of detailed changes.
|
||||
- Add a more detailed summary of the release to the existing "Introducing XRP Ledger Version X.Y.Z" section. Include amendment names (organized in a list if more than 2), featuress, and breaking changes. Limit this to 1 paragraph.
|
||||
- Do NOT delete the **Credits** or **Bug Bounties and Responsible Disclosures** sections
|
||||
- Remove empty subsections that have no entries
|
||||
- Remove all HTML comments (sorting instructions)
|
||||
- Do a final review of the release notes. If you see anything strange, or were forced to take unintuitive actions by these instructions, notify the user, but don't make changes.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,8 +8,11 @@ yarn-error.log
|
||||
*.iml
|
||||
.venv/
|
||||
_code-samples/*/js/package-lock.json
|
||||
*.css.map
|
||||
_code-samples/*/go/go.sum
|
||||
_code-samples/*/java/target/
|
||||
_code-samples/*/*/*[Ss]etup.json
|
||||
|
||||
# PHP
|
||||
composer.lock
|
||||
.cursor/
|
||||
@@ -2,6 +2,7 @@ navbar.about: Acerca de
|
||||
navbar.docs: Docs
|
||||
navbar.resources: Recursos
|
||||
navbar.community: Comunidad
|
||||
navbar.showcase: Escaparate
|
||||
footer.about: Acerca de
|
||||
footer.docs: Docs
|
||||
footer.resources: Recursos
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
seo:
|
||||
description: アカウントの削除
|
||||
labels:
|
||||
- アカウント
|
||||
- Accounts
|
||||
requiredAmendment: DeletableAccounts
|
||||
txIcon: cancel
|
||||
---
|
||||
# AccountDelete
|
||||
|
||||
@@ -10,7 +12,7 @@ labels:
|
||||
|
||||
AccountDeleteトランザクションは、XRP Ledgerで[アカウント](../../ledger-data/ledger-entry-types/accountroot.md)と、アカウントが所有するオブジェクトを削除し、可能であれば、アカウントの残りのXRPを指定された送金先アカウントに送信します。アカウントを削除する要件については、[アカウントの削除](../../../../concepts/accounts/deleting-accounts.md)をご覧ください。
|
||||
|
||||
_[DeletableAccounts Amendment][]が必要です。_
|
||||
{% amendment-disclaimer name="DeletableAccounts" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
seo:
|
||||
description: XRP Ledgerのアカウントのプロパティーを修正します。
|
||||
labels:
|
||||
- アカウント
|
||||
- Accounts
|
||||
txIcon: modify
|
||||
---
|
||||
# AccountSet
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
html: ammbid.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 自動マーケットメーカーのオークションスロットに入札することで、手数料の割引を受けることができます。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: modify
|
||||
---
|
||||
# AMMBid
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMBid.cpp "Source")
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
seo:
|
||||
description: 自動マーケットメーカープールに発行済みトークンを預け入れた保有者から、トークンを回収する。
|
||||
labels:
|
||||
- AMM
|
||||
- Tokens
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMMClawback
|
||||
txIcon: cancel
|
||||
---
|
||||
# AMMClawback
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ parent: transaction-types.html
|
||||
seo:
|
||||
description: 指定された資産ペアを取引するための新しい自動マーケットメーカーを作成します。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: create
|
||||
---
|
||||
# AMMCreate
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMCreate.cpp "Source")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
html: ammdelete.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 空のプールを持つ自動マーケットメーカーのインスタンスを削除します。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: cancel
|
||||
---
|
||||
# AMMDelete
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMDelete.cpp "Source")
|
||||
|
||||
@@ -4,7 +4,10 @@ parent: transaction-types.html
|
||||
seo:
|
||||
description: 自動マーケットメーカーに資金を預け、LPTokenを受け取ります。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: send
|
||||
---
|
||||
# AMMDeposit
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMDeposit.cpp "Source")
|
||||
|
||||
@@ -4,7 +4,10 @@ parent: transaction-types.html
|
||||
seo:
|
||||
description: 自動マーケットメーカーインスタンスの取引手数料へ投票する。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: modify
|
||||
---
|
||||
# AMMVote
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMVote.cpp "Source")
|
||||
|
||||
@@ -4,7 +4,10 @@ parent: transaction-types.html
|
||||
seo:
|
||||
description: LPトークを自動マーケットメーカーに返却し、プールが保有する資産の一部と引き換えマス。
|
||||
labels:
|
||||
- AMM
|
||||
- AMM
|
||||
- DEX
|
||||
requiredAmendment: AMM
|
||||
txIcon: send
|
||||
---
|
||||
# AMMWithdraw
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/AMMWithdraw.cpp "Source")
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
seo:
|
||||
description: 最大8件のトランザクションをまとめて作成・送信し、それらがすべて成功するか、すべて失敗するようにアトミックに処理されるようにします。
|
||||
labels:
|
||||
- Batch
|
||||
- Transaction Sending
|
||||
- Transaction Sending
|
||||
- Other Transactions
|
||||
requiredAmendment: Batch
|
||||
status: not_enabled
|
||||
txIcon: other
|
||||
---
|
||||
# Batch
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Batch.cpp "Source")
|
||||
|
||||
`Batch`トランザクションは、最大8つのトランザクションを単一のバッチで送信します。各トランザクションは、4つのモード(全て成功または全て失敗(All or Nothing)、一つのみ成功(Only One)、失敗まで継続(Until Failure)、および独立実行(Independent))のいずれかでアトミックに実行されます。
|
||||
|
||||
{% amendment-disclaimer name="Batch" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
### 単一アカウントの場合
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
---
|
||||
html: checkcancel.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 未清算のCheckを取り消し、送金を行わずにレジャーから削除します。
|
||||
labels:
|
||||
- Checks
|
||||
- Checks
|
||||
- Payments
|
||||
requiredAmendment: Checks
|
||||
txIcon: cancel
|
||||
---
|
||||
# CheckCancel
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CancelCheck.cpp "Source")
|
||||
|
||||
未清算のCheckを取り消し、送金を行わずにレジャーから削除します。Checkの送金元または送金先は、いつでもこのトランザクションタイプを使用してCheckを取り消すことができます。有効期限切れのCheckはすべてのアドレスが取り消すことができます。
|
||||
|
||||
_([Checks Amendment][]が必要です)_
|
||||
{% amendment-disclaimer name="Checks" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
html: checkcash.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: レジャーでCheckオブジェクトの清算を試みます。
|
||||
labels:
|
||||
- Checks
|
||||
- Checks
|
||||
- Payments
|
||||
requiredAmendment: Checks
|
||||
txIcon: finish
|
||||
---
|
||||
# CheckCash
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CashCheck.cpp "Source")
|
||||
@@ -13,7 +14,7 @@ labels:
|
||||
|
||||
Checkに相当する資金があるとは保証されないため、送金元に十分な残高がないか、または資金を送金できるだけの十分な流動性がないことが原因で、Checkの清算が失敗することがあります。このような状況が発生した場合、Checkはレジャーに残り、送金先は後でこのCheckの換金を再試行するか、または異なる額で換金を試みることができます。
|
||||
|
||||
_([Checks Amendment][]が必要です)_
|
||||
{% amendment-disclaimer name="Checks" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
---
|
||||
html: checkcreate.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: レジャーにCheckオブジェクトを作成します
|
||||
labels:
|
||||
- Checks
|
||||
- Checks
|
||||
- Payments
|
||||
requiredAmendment: Checks
|
||||
txIcon: create
|
||||
---
|
||||
# CheckCreate
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CreateCheck.cpp "Source")
|
||||
|
||||
レジャーにCheckオブジェクトを作成します。これにより指定の送金先は後日換金することができます。このトランザクションの送信者はCheckの送金元です。
|
||||
|
||||
_([Checks Amendment][]が必要です)_
|
||||
{% amendment-disclaimer name="Checks" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
seo:
|
||||
description: 発行したトークンを取り戻します。
|
||||
labels:
|
||||
- トークン
|
||||
- Tokens
|
||||
requiredAmendment: Clawback
|
||||
txIcon: cancel
|
||||
---
|
||||
# Clawback
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
---
|
||||
seo:
|
||||
description: アカウントに仮発行された資格情報を承認します。
|
||||
status: not_enabled
|
||||
labels:
|
||||
- Decentralized Storage
|
||||
- Credentials
|
||||
requiredAmendment: Credentials
|
||||
txIcon: finish
|
||||
---
|
||||
# CredentialAccept
|
||||
|
||||
CredentialAcceptトランザクションは資格情報を承認し、その資格情報を有効にします。資格情報の対象者のみがこの操作を実行できます。
|
||||
|
||||
{% amendment-disclaimer name="Credentials" /%}
|
||||
|
||||
## CredentialAccept JSONの例
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
---
|
||||
seo:
|
||||
description: アカウントに対して暫定的に資格情報を発行します。
|
||||
status: not_enabled
|
||||
labels:
|
||||
- Decentralized Storage
|
||||
- Credentials
|
||||
requiredAmendment: Credentials
|
||||
txIcon: create
|
||||
---
|
||||
|
||||
# CredentialCreate
|
||||
|
||||
CredentialCreateトランザクションは、レジャーにCredentialを作成します。Credential(資格情報)の発行者はこのトランザクションを使用して、暫定的に資格情報を発行します。Credentialは、その対象アカウントが[CredentialAcceptトランザクション][]で承認するまで有効になりません。
|
||||
|
||||
{% amendment-disclaimer name="Credentials" /%}
|
||||
|
||||
## CredentialCreate JSONの例
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
seo:
|
||||
description: レジャーから認証情報を削除し、事実上失効させます。
|
||||
status: not_enabled
|
||||
labels:
|
||||
- Decentralized Storage
|
||||
- Credentials
|
||||
requiredAmendment: Credentials
|
||||
txIcon: cancel
|
||||
---
|
||||
# CredentialDelete
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: DepositPreauthトランザクションは別のアカウントに対し、このトランザクションの送信者に支払いを送金することを事前承認します。
|
||||
labels:
|
||||
- セキュリティ
|
||||
- Accounts
|
||||
- Security
|
||||
requiredAmendment: DepositPreauth
|
||||
txIcon: modify
|
||||
---
|
||||
# DepositPreauth
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/DepositPreauth.cpp "Source")
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
---
|
||||
html: diddelete.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: DIDを削除する。
|
||||
labels:
|
||||
- DID
|
||||
- DID
|
||||
- Decentralized Storage
|
||||
requiredAmendment: DID
|
||||
txIcon: cancel
|
||||
---
|
||||
# DIDDelete
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/DID.cpp "ソース")
|
||||
|
||||
_([DID Amendment][])_
|
||||
|
||||
指定した`Account`フィールドに関連付けられている[DIDレジャーエントリ](../../ledger-data/ledger-entry-types/did.md)を削除します。
|
||||
|
||||
{% admonition type="info" name="注記" %}このトランザクションは[共通フィールド][]のみ利用します。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="DID" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
---
|
||||
html: didset.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: DIDを作成または更新します。
|
||||
labels:
|
||||
- DID
|
||||
- DID
|
||||
- Decentralized Storage
|
||||
requiredAmendment: DID
|
||||
txIcon: create
|
||||
---
|
||||
# DIDSet
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/DID.cpp "ソース")
|
||||
|
||||
_([DID Amendment][])_
|
||||
|
||||
新しい[DIDレジャーエントリ](../../ledger-data/ledger-entry-types/did.md)を作成したり、既存の項目を更新したりします。
|
||||
|
||||
{% amendment-disclaimer name="DID" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ seo:
|
||||
description: Escrowに留保されているXRPを送金元に返金します。
|
||||
labels:
|
||||
- Escrow
|
||||
- Payments
|
||||
requiredAmendment: Escrow
|
||||
txIcon: cancel
|
||||
---
|
||||
# EscrowCancel
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp "Source")
|
||||
|
||||
@@ -3,6 +3,9 @@ seo:
|
||||
description: Escrowプロセスが終了または取り消されるまでXRPを隔離します。
|
||||
labels:
|
||||
- Escrow
|
||||
- Payments
|
||||
requiredAmendment: Escrow
|
||||
txIcon: create
|
||||
---
|
||||
# EscrowCreate
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp "Source")
|
||||
|
||||
@@ -3,6 +3,9 @@ seo:
|
||||
description: エスクローされたXRPを受取人へ送金します。
|
||||
labels:
|
||||
- Escrow
|
||||
- Payments
|
||||
requiredAmendment: Escrow
|
||||
txIcon: finish
|
||||
---
|
||||
# EscrowFinish
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp "Source")
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
---
|
||||
html: transaction-types.html
|
||||
parent: transaction-formats.html
|
||||
seo:
|
||||
description: トランザクションのタイプは、どういったタイプの操作を実行することが想定されているのかを示します。
|
||||
metadata:
|
||||
indexPage: true
|
||||
indexPage: true
|
||||
labels:
|
||||
- ブロックチェーン
|
||||
- ブロックチェーン
|
||||
---
|
||||
# トランザクションのタイプ
|
||||
|
||||
トランザクションのタイプ(`TransactionType`フィールド)は、トランザクションに関する最も基本的な情報です。トランザクションで、どういったタイプの操作を実行することが想定されているのかを示します。
|
||||
トランザクションのタイプ(`TransactionType`フィールド)は、どういったタイプの操作を実行することが想定されているのかを示します。すべてのトランザクションに、[共通フィールド](../common-fields.md)が含まれています。
|
||||
|
||||
すべてのトランザクションに、特定の共通フィールドが含まれています。
|
||||
## アカウント
|
||||
|
||||
* [共通フィールド](../common-fields.md)
|
||||
{% tx-category name="Accounts" /%}
|
||||
|
||||
トランザクションのタイプごとに、実行される操作のタイプに関連した追加のフィールドが含まれています。
|
||||
## 支払い
|
||||
|
||||
{% tx-category name="Payments" /%}
|
||||
|
||||
## トークン
|
||||
|
||||
{% tx-category name="Tokens" /%}
|
||||
|
||||
## 分散型取引所(DEX)
|
||||
|
||||
{% tx-category name="DEX" /%}
|
||||
|
||||
## 分散型ストレージ
|
||||
|
||||
{% tx-category name="Decentralized Storage" /%}
|
||||
|
||||
## XRPLサイドチェーン
|
||||
|
||||
{% tx-category name="Interoperability" /%}
|
||||
|
||||
## その他
|
||||
|
||||
{% tx-category name="Other Transactions" /%}
|
||||
|
||||
{% child-pages /%}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
seo:
|
||||
description: アカウントが特定のMPTの残高を保持することを許可します。
|
||||
labels:
|
||||
- Multi-Purpose Token, MPT
|
||||
- Multi-purpose Tokens, MPTs
|
||||
- Tokens
|
||||
requiredAmendment: MPTokensV1
|
||||
txIcon: modify
|
||||
---
|
||||
|
||||
# MPTokenAuthorize
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp "ソース")
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: 新しいMulti-Purpose Tokenを発行します。
|
||||
labels:
|
||||
- Multi-Purpose Token, MPT
|
||||
- Multi-purpose Tokens, MPTs
|
||||
- Tokens
|
||||
requiredAmendment: MPTokensV1
|
||||
txIcon: create
|
||||
---
|
||||
|
||||
# MPTokenIssuanceCreate
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: Multi-Purpose Tokenを削除します。
|
||||
labels:
|
||||
- Multi-Purpose Token, MPT
|
||||
- Multi-purpose Tokens, MPTs
|
||||
- Tokens
|
||||
requiredAmendment: MPTokensV1
|
||||
txIcon: cancel
|
||||
---
|
||||
# MPTokenIssuanceDestroy
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: MPTの変更可能なプロパティを設定します。
|
||||
labels:
|
||||
- Multi-Purpose Token, MPT
|
||||
- Multi-purpose Tokens, MPTs
|
||||
- Tokens
|
||||
requiredAmendment: MPTokensV1
|
||||
txIcon: modify
|
||||
---
|
||||
# MPTokenIssuanceSet
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
seo:
|
||||
description: NFTokenの購入または売却のオファーを受け入れる。
|
||||
labels:
|
||||
- NFT, 非代替性トークン
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
- DEX
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: finish
|
||||
---
|
||||
# NFTokenAcceptOffer
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp "ソース")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
html: nftokenburn.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: TokenBurnを使用して、NFTを永久に破棄します。
|
||||
labels:
|
||||
- 非代替性トークン, NFT
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: cancel
|
||||
---
|
||||
# NFTokenBurn
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
html: nftokencanceloffer.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: NFTokenの売買のための既存のトークンへのオファーをキャンセルする。
|
||||
labels:
|
||||
- NFT, 非代替性トークン
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
- DEX
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: cancel
|
||||
---
|
||||
# NFTokenCancelOffer
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp "ソース")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
html: nftokencreateoffer.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: NFTの売買のオファーを作成する。
|
||||
labels:
|
||||
- 非代替性トークン, NFT
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
- DEX
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: create
|
||||
---
|
||||
# NFTokenCreateOffer
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: NFTokenMintを使用して新規NFTを発行する。
|
||||
labels:
|
||||
- 非代替性トークン, NFT
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
requiredAmendment: NonFungibleTokensV1_1
|
||||
txIcon: create
|
||||
---
|
||||
# NFTokenMint
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenMint.cpp "Source")
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
seo:
|
||||
description: ダイナミックNFTを変更します。
|
||||
labels:
|
||||
- 非代替性トークン, トークン, NFT
|
||||
title:
|
||||
- NFTokenModify
|
||||
- Non-fungible Tokens, NFTs
|
||||
- Tokens
|
||||
requiredAmendment: DynamicNFT
|
||||
txIcon: modify
|
||||
---
|
||||
# NFTokenModify
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/NFTokenModify.cpp "ソース")
|
||||
|
||||
`NFTokenModify`は、NFTの`URI`フィールドを別のURIに変更し、NFTのサポートデータを更新するために使用されます。NFTは、`tfMutable`フラグが設定された状態でミントされている必要があります。[ダイナミックNFT](../../../../concepts/tokens/nfts/dynamic-nfts.md)をご覧ください。
|
||||
|
||||
{% amendment-disclaimer name="DynamicNFT" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %} JSONの例
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
---
|
||||
html: offercancel.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: XRP LedgerからOfferオブジェクトを削除します。
|
||||
description: 分散型取引所からオファーを削除します。
|
||||
labels:
|
||||
- 分散型取引所
|
||||
- DEX
|
||||
txIcon: cancel
|
||||
---
|
||||
# OfferCancel
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CancelOffer.cpp "Source")
|
||||
|
||||
OfferCancelトランザクションは、XRP LedgerからOfferオブジェクトを削除します。
|
||||
[分散型取引所](../../../../concepts/tokens/decentralized-exchange/index.md)からオファーを削除します。
|
||||
|
||||
## {% $frontmatter.seo.title %}のJSONの例
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
seo:
|
||||
description: 通貨交換の注文を作成します。
|
||||
labels:
|
||||
- 分散型取引所
|
||||
- DEX
|
||||
txIcon: create
|
||||
---
|
||||
# OfferCreate
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CreateOffer.cpp "ソース")
|
||||
|
||||
OfferCreateトランザクションは[分散型取引所](../../../../concepts/tokens/decentralized-exchange/index.md)で[注文](../../../../concepts/tokens/decentralized-exchange/offers.md)を作成します。
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
seo:
|
||||
description: 既存の価格オラクルを削除します。
|
||||
labels:
|
||||
- オラクル
|
||||
- Oracle
|
||||
- Decentralized Storage
|
||||
requiredAmendment: PriceOracle
|
||||
txIcon: cancel
|
||||
---
|
||||
# OracleDelete
|
||||
_([PriceOracle Amendment][])_
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/DeleteOracle.cpp "ソース")
|
||||
|
||||
既存の`Oracle`レジャーエントリを削除します。
|
||||
|
||||
{% amendment-disclaimer name="PriceOracle" /%}
|
||||
|
||||
## OracleDeleteのJSONの例
|
||||
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
seo:
|
||||
description: 価格オラクルを作成または更新します。
|
||||
labels:
|
||||
- オラクル
|
||||
- Oracle
|
||||
- Decentralized Storage
|
||||
requiredAmendment: PriceOracle
|
||||
txIcon: create
|
||||
---
|
||||
# OracleSet
|
||||
_([PriceOracle Amendment][])_
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/SetOracle.cpp "ソース")
|
||||
|
||||
Oracle Document ID を使用して、新しい`Oracle`レジャーエントリを作成するか、既存のフィールドを更新します。
|
||||
|
||||
{% amendment-disclaimer name="PriceOracle" /%}
|
||||
|
||||
## OracleSetのJSONの例
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
seo:
|
||||
description: アカウント間での価値の移動します。
|
||||
description: アカウント間での価値の移動します、またはアカウントを作成する。
|
||||
labels:
|
||||
- 支払い
|
||||
- XRP
|
||||
- クロスカレンシー
|
||||
- トークン
|
||||
- Accounts
|
||||
- Payments
|
||||
- XRP
|
||||
- Cross-Currency
|
||||
txIcon: send
|
||||
---
|
||||
# Payment
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Payment.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: Payment Channelに対しXRPを請求します。
|
||||
labels:
|
||||
- Payment Channel
|
||||
- Payment Channels
|
||||
- Payments
|
||||
requiredAmendment: PayChan
|
||||
txIcon: finish
|
||||
---
|
||||
# PaymentChannelClaim
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PayChan.cpp "Source")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: 新しいペイメントチャネルを作成します。
|
||||
labels:
|
||||
- Payment Channel
|
||||
- Payment Channels
|
||||
- Payments
|
||||
requiredAmendment: PayChan
|
||||
txIcon: create
|
||||
---
|
||||
# PaymentChannelCreate
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PayChan.cpp "ソース")
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
seo:
|
||||
description: Payment ChannelにXRPを追加します。
|
||||
labels:
|
||||
- Payment Channel
|
||||
- Payment Channels
|
||||
- Payments
|
||||
requiredAmendment: PayChan
|
||||
txIcon: modify
|
||||
---
|
||||
# PaymentChannelFund
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PayChan.cpp "Source")
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
seo:
|
||||
description: 許可型ドメインのレジャーエントリを削除する
|
||||
labels:
|
||||
- コンプライアンス
|
||||
- 許可型ドメイン
|
||||
- Compliance
|
||||
- Permissioned Domains
|
||||
- Decentralized Storage
|
||||
requiredAmendment: PermissionedDomains
|
||||
txIcon: cancel
|
||||
---
|
||||
# PermissionedDomainDelete
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp "ソース")
|
||||
|
||||
所有する[許可型ドメイン][]を削除します。
|
||||
|
||||
_([PermissionedDomains amendment][]が必要です {% not-enabled /%})_
|
||||
{% amendment-disclaimer name="PermissionedDomains" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %}のJSONの例
|
||||
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
seo:
|
||||
description: 許可型ドメインを作成または更新する
|
||||
labels:
|
||||
- コンプライアンス
|
||||
- 許可型ドメイン
|
||||
- Compliance
|
||||
- Permissioned Domains
|
||||
- Decentralized Storage
|
||||
requiredAmendment: PermissionedDomains
|
||||
txIcon: create
|
||||
---
|
||||
# PermissionedDomainSet
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp "ソース")
|
||||
|
||||
[許可型ドメイン][]を作成するか、所有するドメインを変更します。
|
||||
|
||||
_([PermissionedDomains amendment][]が必要です {% not-enabled /%})_
|
||||
{% amendment-disclaimer name="PermissionedDomains" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %}のJSONの例
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
html: setregularkey.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: アカウントに関連付けられているレギュラーキーペアの割り当て、変更、削除を行います。
|
||||
labels:
|
||||
- セキュリティ
|
||||
- Security
|
||||
- Accounts
|
||||
txIcon: modify
|
||||
---
|
||||
# SetRegularKey
|
||||
|
||||
@@ -29,7 +29,6 @@ labels:
|
||||
{% tx-example txid="6AA6F6EAAAB56E65F7F738A9A2A8A7525439D65BA990E9BA08F6F4B1C2D349B4" /%}
|
||||
|
||||
{% raw-partial file="/@l10n/ja/docs/_snippets/tx-fields-intro.md" /%}
|
||||
<!--{# fix md highlighting_ #}-->
|
||||
|
||||
| フィールド | JSONの型 | [内部の型][] | 説明 |
|
||||
|:-------------|:----------|:------------------|:------------------------------|
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
---
|
||||
html: signerlistset.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: トランザクションのマルチシグに使用できる署名者のリストを作成、置換、削除します。
|
||||
labels:
|
||||
- セキュリティ
|
||||
- Security
|
||||
- Accounts
|
||||
requiredAmendment: MultiSign
|
||||
txIcon: modify
|
||||
---
|
||||
# SignerListSet
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/SetSignerList.cpp "ソース")
|
||||
|
||||
SignerListSetトランザクションは、トランザクションの[マルチシグ](../../../../concepts/accounts/multi-signing.md)に使用できる署名者のリストを作成、置換、削除します。このトランザクションタイプは[MultiSign Amendment][]により導入されました。
|
||||
|
||||
{% amendment-disclaimer name="MultiSign" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %}のJSONの例
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
html: ticketcreate.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: チケットとして1つ以上のシーケンス番号を確保する。
|
||||
labels:
|
||||
- Transaction Sending
|
||||
- Transaction Sending
|
||||
- Accounts
|
||||
requiredAmendment: TicketBatch
|
||||
txIcon: create
|
||||
---
|
||||
# TicketCreate
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/CreateTicket.cpp "Source")
|
||||
|
||||
_([TicketBatch amendment][]が必要です)_
|
||||
|
||||
TicketCreateトランザクションは、1つまたは複数の[シーケンス番号](../../data-types/basic-data-types.md#アカウントシーケンス)を[Tickets](../../ledger-data/ledger-entry-types/ticket.md)として確保します。
|
||||
|
||||
{% amendment-disclaimer name="TicketBatch" /%}
|
||||
|
||||
## {% $frontmatter.seo.title %}JSONの例
|
||||
|
||||
```json
|
||||
@@ -28,9 +28,6 @@ TicketCreateトランザクションは、1つまたは複数の[シーケンス
|
||||
|
||||
{% tx-example txid="738AEF36B48CA4A2D85C2B74910DC34DDBBCA4C83643F2DB84A58785ED5AD3E3" /%}
|
||||
|
||||
{% raw-partial file="/@l10n/ja/docs/_snippets/tx-fields-intro.md" /%}
|
||||
<!--{# fix md highlighting_ #}-->
|
||||
|
||||
| フィールド | JSONの型 | [内部の型][] | 説明 |
|
||||
|:-----------------|:-----------------|:------------------|:-------------------|
|
||||
| `TicketCount` | 数値 | UInt32 | 作成するチケットの枚数。これは正の数でなければならず、このトランザクションの実行の結果、アカウントが250枚以上のチケットを所有することはできません。 |
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
---
|
||||
html: trustset.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: トラストラインを作成または変更します。
|
||||
labels:
|
||||
- トークン
|
||||
- Tokens
|
||||
txIcon: modify
|
||||
---
|
||||
# TrustSet
|
||||
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/SetTrust.cpp "Source")
|
||||
|
||||
2つのアカウントをリンクする[トラストライン](../../../../concepts/tokens/fungible-tokens/index.md)を作成または変更します。
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
---
|
||||
html: xchainaccountcreatecommit.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: ブリッジが接続するチェーンの一つでアカウントを作成します。このアカウントがそのチェーンのブリッジの入り口となります。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: create
|
||||
---
|
||||
# XChainAccountCreateCommit
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L466-L474 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
このトランザクションはXRP-XRPブリッジにのみ使用できます。
|
||||
|
||||
`XChainAccountCreateCommit`トランザクションは、発行チェーンにトランザクションを送信するために、Witnessサーバ用の新しいアカウントを作成します。
|
||||
発行チェーンにトランザクションを送信するために、Witnessサーバ用の新しいアカウントを作成します。このトランザクションはXRP-XRPブリッジにのみ使用できます。
|
||||
|
||||
{% admonition type="warning" name="注意" %}このトランザクションは、Witnessの証明書が送信先チェーンに確実に送信される場合にのみ実行されるべきです。署名が送信されない場合、証明書が受信されるまでアカウント作成はブロックされます。XRP-XRPブリッジでこのトランザクションを無効にするには、ブリッジの`MinAccountCreateAmount`フィールドを省略します。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainAccountCreateCommit JSONの例
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
---
|
||||
html: xchainaddaccountcreateattestation.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: XChainAddAccountCreateAttestationトランザクションは他のチェーンでXChainAccountCreateCommitトランザクションが発生した証明をWitnessサーバから提示します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: create
|
||||
---
|
||||
# XChainAddAccountCreateAttestation
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L447-L464 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainAddAccountCreateAttestation`トランザクションは、`XChainAccountCreateCommit`トランザクションがもう一方のチェーンで発生したというWitnessサーバからの証明を提示します。
|
||||
`XChainAccountCreateCommit`トランザクションがもう一方のチェーンで発生したというWitnessサーバからの証明を提示します。
|
||||
|
||||
この署名は署名が提供された時点のドアの署名者リストにある鍵の一つでなければなりません。署名が提出されてから定足数に達するまでの間に署名者リストが変更された場合、新しい署名セットが使用され、現在収集されている署名の一部が削除される可能性があります。
|
||||
|
||||
@@ -20,6 +18,7 @@ _([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
{% admonition type="info" name="注記" %}報酬は現在のリストにある鍵を持っているアカウントにのみ送られます。署名者の定足数は`SignatureReward`に一致する必要があります。より大きな報酬を得ようとして、一つのWitnessサーバがこの値に不正な値を指定することはできません。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainAddAccountCreateAttestation JSONの例
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
---
|
||||
html: xchainaddclaimattestation.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 送信元チェーンで発生したイベントを、送信先チェーンに証明(アテスト)します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: modify
|
||||
---
|
||||
# XChainAddClaimAttestation
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L429-L445 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainAddClaimAttestation`トランザクションは`XChainCommit`トランザクションを証明するWitnessサーバの署名を提供します。
|
||||
`XChainCommit`トランザクションを証明するWitnessサーバの署名を提供します。
|
||||
|
||||
この署名は、署名が提出された時点のドアの署名者リストにある鍵の一つでなければなりません。ただし、署名が提出されてから定足数に達するまでの間に署名者リストが変更された場合は、新しい署名セットが使用され、現在収集されている署名の一部が削除されることがあります。
|
||||
|
||||
@@ -20,6 +18,7 @@ _([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
{% admonition type="info" name="注記" %}報酬は現在のリストにある鍵を持っているアカウントにのみ送られます。署名者の定足数は`SignatureReward`に一致する必要があります。より大きな報酬を得ようとして、一つのWitnessサーバがこの値に不正な値を指定することはできません。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainAddClaimAttestation JSONの例
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
---
|
||||
html: xchainclaim.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 送信先チェーンで金額を請求することで、クロスチェーンでの価値移転を完了させます。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: finish
|
||||
---
|
||||
# XChainClaim
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L418-L427 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainClaim`トランザクションはクロスチェーンでの価値の移転を完了させます。`XChainClaim`トランザクションにより、ユーザは送信元チェーンでロックされた価値と同等の価値を送信先チェーンで請求することができます。ユーザは、送金元チェーンでロックされた価値に関連付けられたクロスチェーン請求ID(`Account`フィールド)を所有している場合にのみ、その価値を請求することができます。ユーザは誰にでも資金を送ることができます(`Destination`フィールド)。このトランザクションが必要になるのは`XChainCommit`トランザクションで`OtherChainDestination`が指定されていない場合、または自動送金で何か問題が発生した場合のみです。
|
||||
|
||||
トランザクションによって送金に成功すると、対象の`XChainOwnedClaimID`レジャーオブジェクトは削除されます。これはトランザクションのリプレイを防ぎます。トランザクションが失敗した場合、`XChainOwnedClaimID`は削除されず、異なるパラメータでトランザクションを再実行できます。
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainClaim JSONの例
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
---
|
||||
html: xchaincommit.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: クロスチェーンでの価値移転を開始します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: send
|
||||
---
|
||||
# XChainCommit
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L408-L416 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainCommit`はクロスチェーン送金の2番目のステップです。`XChainCommit`は発行チェーンでラップできるようにロックチェーンで資産を保管したり、ロックチェーンで返却できるように発行チェーンでラップされた資産をバーンしたりします。
|
||||
クロスチェーン送金の2番目のステップです。`XChainCommit`は発行チェーンでラップできるようにロックチェーンで資産を保管したり、ロックチェーンで返却できるように発行チェーンでラップされた資産をバーンしたりします。
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainCommit JSONの例
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
---
|
||||
html: xchaincreatebridge.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: 2つのチェーン間にブリッジを作成します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: create
|
||||
---
|
||||
# XChainCreateBridge
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L381-L388 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainCreateBridge`トランザクションは新しい`Bridge`レジャーオブジェクトを作成し、トランザクショ ンが送信されたチェーン上に新しいクロスチェーンブリッジの入り口を定義します。これにはブリッジのドアアカウントと資産に関する情報が含まれます。
|
||||
新しい`Bridge`レジャーオブジェクトを作成し、トランザクショ ンが送信されたチェーン上に新しいクロスチェーンブリッジの入り口を定義します。これにはブリッジのドアアカウントと資産に関する情報が含まれます。
|
||||
|
||||
このトランザクションは、ロックチェーンのドアアカウントが最初に送信する必要があります。有効なブリッジをセットアップするには、Witnessサーバのセットアップに加えて、両チェーンのドアアカウントがこのトランザクションを送信しなければなりません。
|
||||
|
||||
@@ -20,6 +18,7 @@ _([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
{% admonition type="info" name="注記" %}各ドアアカウントは1つのブリッジしか持つことができません。これにより、同じ資産に対して複数のブリッジが作成され、いずれかのチェーンで資産が不一致となるのを防ぐことができます。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainCreateBridge JSONの例
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
---
|
||||
html: xchaincreateclaimid.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: クロスチェーン送金に使用するクロスチェーン請求IDを作成します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: create
|
||||
---
|
||||
# XChainCreateClaimID
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp#L399-L406 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainCreateClaimID`トランザクションはクロスチェーン送金に使われる新しいクロスチェーン請求IDを作成します。クロスチェーン請求IDは*1つの*クロスチェーン送金を表します。
|
||||
|
||||
このトランザクションはクロスチェーン送金の最初のステップであり、送金元チェーンではなく、送金先チェーンで送信されます。
|
||||
|
||||
また、送金元チェーン上の資金をロックまたはバーンする送金元チェーン上のアカウントも含まれます。
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainCreateClaimID JSONの例
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
---
|
||||
html: xchainmodifybridge.html
|
||||
parent: transaction-types.html
|
||||
seo:
|
||||
description: ブリッジの設定を変更します。
|
||||
labels:
|
||||
- 相互運用性
|
||||
- Interoperability
|
||||
requiredAmendment: XChainBridge
|
||||
status: not_enabled
|
||||
txIcon: modify
|
||||
---
|
||||
# XChainModifyBridge
|
||||
[[ソース]](https://github.com/XRPLF/rippled/blob/develop/src/ripple/protocol/impl/TxFormats.cpp#L390-L397 "ソース")
|
||||
|
||||
_([XChainBridge Amendment][] {% not-enabled /%} が必要です)_
|
||||
|
||||
`XChainModifyBridge`トランザクションでは、ブリッジ管理者がブリッジの設定を変更することができます。変更できるのは`SignatureReward`と`MinAccountCreateAmount`だけです。
|
||||
|
||||
このトランザクションはドアアカウントから送信される必要があり、Witnessサーバを管理するエンティティがこのトランザクションのために協調し、署名を提供する必要があります。この調整はレジャーの外部で行われます。
|
||||
|
||||
{% admonition type="info" name="注記" %}このトランザクションでブリッジの署名者リストを変更することはできません。署名者リストはドアアカウント自体にあり、署名者リストがアカウント上で変更されるのと同じ方法で変更されます(`SignerListSet`トランザクションを利用)。{% /admonition %}
|
||||
|
||||
{% amendment-disclaimer name="XChainBridge" /%}
|
||||
|
||||
## XChainModifyBridge JSONの例
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ navbar.about: 概要
|
||||
navbar.docs: ドキュメント
|
||||
navbar.resources: リソース
|
||||
navbar.community: コミュニティ
|
||||
navbar.showcase: ショーケース
|
||||
footer.about: 概要
|
||||
footer.docs: ドキュメント
|
||||
footer.resources: リソース
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Components related to XRPL Amendment previews and statuses
|
||||
|
||||
import * as React from 'react'
|
||||
import { Link } from '@redocly/theme/components/Link/Link'
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks'
|
||||
|
||||
26
@theme/components/ChildPages.tsx
Normal file
26
@theme/components/ChildPages.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
// Component for {% child-pages /%} markdoc tag.
|
||||
// Return a list of children of the current page.
|
||||
|
||||
import { useThemeHooks } from '@redocly/theme/core/hooks'
|
||||
import { Link } from '@redocly/theme/components/Link/Link'
|
||||
import NotEnabled from './NotEnabled'
|
||||
|
||||
export default function ChildPages() {
|
||||
const { usePageSharedData } = useThemeHooks()
|
||||
const data = usePageSharedData('index-page-items') as any[]
|
||||
return (
|
||||
<div className="children-display">
|
||||
<ul>
|
||||
{data?.map((item: any) => (
|
||||
<li className="level-1" key={item.slug}>
|
||||
<Link to={item.slug}>{item.title}</Link>
|
||||
{
|
||||
item.status === "not_enabled" ? (<NotEnabled />) : ""
|
||||
}
|
||||
<p className="blurb child-blurb">{item.seo?.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
@theme/components/CodePageName.tsx
Normal file
11
@theme/components/CodePageName.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
// Component for {% code-page-name /%} Markdoc tag.
|
||||
// Returns the current page title in monospace (code) font.
|
||||
// Useful in includes / templates that may be reused across pages.
|
||||
|
||||
export default function CodePageName(props: {
|
||||
name: string
|
||||
}) {
|
||||
return (
|
||||
<code>{props.name}</code>
|
||||
)
|
||||
}
|
||||
28
@theme/components/CopyableUrl.tsx
Normal file
28
@theme/components/CopyableUrl.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState } from "react"
|
||||
|
||||
// Copyable URL component with click-to-copy functionality
|
||||
export default function CopyableUrl({ url, translate }: { url: string; translate: (text: string) => string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`quick-ref-value-btn ${copied ? "copied" : ""}`}
|
||||
onClick={handleCopy}
|
||||
title={copied ? translate("Copied!") : translate("Click to copy")}
|
||||
>
|
||||
<code className="quick-ref-value">{url}</code>
|
||||
<span className="copy-icon">{copied ? "✓" : ""}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
45
@theme/components/InteractiveBlock.tsx
Normal file
45
@theme/components/InteractiveBlock.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// Component for {% interactive-block %} markdoc tag. Used in legacy interactive
|
||||
// tutorials; not recommended for new tutorials.
|
||||
|
||||
import * as React from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import dynamicReact from '@markdoc/markdoc/dist/react'
|
||||
import { idify } from '../helpers'
|
||||
|
||||
export default function InteractiveBlock(props: {
|
||||
children: React.ReactNode
|
||||
label: string
|
||||
steps: string[]
|
||||
}) {
|
||||
const stepId = idify(props.label)
|
||||
const { pathname } = useLocation()
|
||||
|
||||
return (
|
||||
// add key={pathname} to ensure old step state gets rerendered on page navigation
|
||||
<div className="interactive-block" id={'interactive-' + stepId} key={pathname}>
|
||||
<div className="interactive-block-inner">
|
||||
<div className="breadcrumbs-wrap">
|
||||
<ul
|
||||
className="breadcrumb tutorial-step-crumbs"
|
||||
id={'bc-ul-' + stepId}
|
||||
data-steplabel={props.label}
|
||||
data-stepid={stepId}
|
||||
>
|
||||
{props.steps?.map((step, idx) => {
|
||||
const iterStepId = idify(step).toLowerCase()
|
||||
let className = `breadcrumb-item bc-${iterStepId}`
|
||||
if (idx > 0) className += ' disabled'
|
||||
if (iterStepId === stepId) className += ' current'
|
||||
return (
|
||||
<li className={className} key={iterStepId}>
|
||||
<a href={`#interactive-${iterStepId}`}>{step}</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="interactive-block-ui">{dynamicReact(props.children, React, {})}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
// Replaces Redocly's built-in language picker with our custom language picker component.
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { useSearchDialog } from "@redocly/theme/core/hooks";
|
||||
import { SearchDialog } from "@redocly/theme/components/Search/SearchDialog";
|
||||
// Replaces the top navbar with our custom XRPL.org top navbar
|
||||
|
||||
import { useThemeConfig, useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { LanguagePicker } from "@redocly/theme/components/LanguagePicker/LanguagePicker";
|
||||
import { slugify } from "../../helpers";
|
||||
@@ -8,442 +12,151 @@ import { Search } from "@redocly/theme/components/Search/Search";
|
||||
import arrowUpRight from "../../../static/img/icons/arrow-up-right-custom.svg";
|
||||
import moment from "moment-timezone";
|
||||
|
||||
// @ts-ignore
|
||||
// Import from modular components
|
||||
import { AlertBanner } from "./components/AlertBanner";
|
||||
import { NavLogo } from "./components/NavLogo";
|
||||
import { NavItems } from "./components/NavItems";
|
||||
import { NavControls, HamburgerButton } from "./controls";
|
||||
import { DevelopSubmenu, UseCasesSubmenu, CommunitySubmenu, NetworkSubmenu } from "./submenus";
|
||||
import { MobileMenu } from "./mobile-menu";
|
||||
import { alertBanner } from "./constants/navigation";
|
||||
|
||||
const alertBanner = {
|
||||
show: false,
|
||||
message: "APEX 2025",
|
||||
button: "REGISTER",
|
||||
link: "https://www.xrpledgerapex.com/?utm_source=xrplwebsite&utm_medium=direct&utm_campaign=xrpl-event-ho-xrplapex-glb-2025-q1_xrplwebsite_ari_arp_bf_rsvp&utm_content=cta_btn_english_pencilbanner"
|
||||
};
|
||||
// Re-export AlertBanner for backwards compatibility
|
||||
export { AlertBanner } from "./components/AlertBanner";
|
||||
|
||||
export function AlertBanner({ message, button, link, show }) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
const bannerRef = React.useRef(null);
|
||||
const [displayDate, setDisplayDate] = React.useState("JUNE 10-12");
|
||||
|
||||
React.useEffect(() => {
|
||||
const calculateCountdown = () => {
|
||||
// Calculate days until June 11, 2025 8AM Singapore time
|
||||
// This will automatically adjust for the user's timezone
|
||||
const target = moment.tz('2025-06-11 08:00:00', 'Asia/Singapore');
|
||||
const now = moment();
|
||||
const daysUntil = target.diff(now, 'days');
|
||||
|
||||
// Show countdown if event is in the future, otherwise show the provided date
|
||||
let newDisplayDate = "JUNE 10-12";
|
||||
if (daysUntil > 0) {
|
||||
newDisplayDate = daysUntil === 1 ? 'IN 1 DAY' : `IN ${daysUntil} DAYS`;
|
||||
} else if (daysUntil === 0) {
|
||||
// Check if it's today
|
||||
const hoursUntil = target.diff(now, 'hours');
|
||||
newDisplayDate = hoursUntil > 0 ? 'TODAY' : "JUNE 10-12";
|
||||
}
|
||||
|
||||
setDisplayDate(newDisplayDate);
|
||||
};
|
||||
|
||||
// Calculate immediately
|
||||
calculateCountdown();
|
||||
|
||||
// Update every hour
|
||||
const interval = setInterval(calculateCountdown, 60 * 60 * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const banner = bannerRef.current;
|
||||
if (!banner) return;
|
||||
const handleMouseEnter = () => {
|
||||
banner.classList.add("has-hover");
|
||||
};
|
||||
// Attach the event listener
|
||||
banner.addEventListener("mouseenter", handleMouseEnter);
|
||||
// Clean up the event listener on unmount
|
||||
return () => {
|
||||
banner.removeEventListener("mouseenter", handleMouseEnter);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (show) {
|
||||
return (
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
ref={bannerRef}
|
||||
className="top-banner fixed-top web-banner"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Get Tickets for the APEX 2025 Event"
|
||||
>
|
||||
<div className="banner-event-details">
|
||||
<div className="event-info">{translate(message)}</div>
|
||||
<div className="event-date">{displayDate}</div>
|
||||
</div>
|
||||
<div className="banner-button">
|
||||
<div className="button-text">{translate(button)}</div>
|
||||
<img
|
||||
className="button-icon"
|
||||
src={arrowUpRight}
|
||||
alt="Get Tickets Icon"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
// Props interface for Navbar (extensible for future use)
|
||||
interface NavbarProps {
|
||||
className?: string;
|
||||
}
|
||||
export function Navbar(props) {
|
||||
// const [isOpen, setIsOpen] = useMobileMenu(false);
|
||||
const themeConfig = useThemeConfig();
|
||||
const { useL10n } = useThemeHooks();
|
||||
const { changeLanguage } = useL10n();
|
||||
const menu = themeConfig.navbar?.items;
|
||||
const logo = themeConfig.logo;
|
||||
|
||||
const { href, altText, items } = props;
|
||||
const pathPrefix = "";
|
||||
/**
|
||||
* Main Navbar Component.
|
||||
* Renders the complete navigation bar including:
|
||||
* - Alert banner (when enabled)
|
||||
* - Logo
|
||||
* - Navigation items with desktop submenus
|
||||
* - Control buttons (search, theme toggle, language)
|
||||
* - Mobile menu
|
||||
*/
|
||||
export function Navbar(_props: NavbarProps = {}) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
||||
const [activeSubmenu, setActiveSubmenu] = React.useState<string | null>(null);
|
||||
const [closingSubmenu, setClosingSubmenu] = React.useState<string | null>(null);
|
||||
const submenuTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
const closingTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const navItems = menu.map((item, index) => {
|
||||
if (item.type === "group") {
|
||||
return (
|
||||
<NavDropdown
|
||||
key={index}
|
||||
label={item.label}
|
||||
labelTranslationKey={item.labelTranslationKey}
|
||||
items={item.items}
|
||||
pathPrefix={pathPrefix}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NavItem key={index}>
|
||||
<Link to={item.link} className="nav-link">
|
||||
{item.label}
|
||||
</Link>
|
||||
</NavItem>
|
||||
);
|
||||
// Use Redocly's search dialog hook - shared across navbar and mobile menu
|
||||
const { isOpen: isSearchOpen, onOpen: onSearchOpen, onClose: onSearchClose } = useSearchDialog();
|
||||
|
||||
const handleHamburgerClick = () => {
|
||||
setMobileMenuOpen(true);
|
||||
};
|
||||
|
||||
const handleMobileMenuClose = () => {
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmenuMouseEnter = (itemLabel: string) => {
|
||||
// Clear any pending close/closing timeouts
|
||||
if (submenuTimeoutRef.current) {
|
||||
clearTimeout(submenuTimeoutRef.current);
|
||||
submenuTimeoutRef.current = null;
|
||||
}
|
||||
});
|
||||
if (closingTimeoutRef.current) {
|
||||
clearTimeout(closingTimeoutRef.current);
|
||||
closingTimeoutRef.current = null;
|
||||
}
|
||||
// Cancel closing state and activate the new submenu
|
||||
setClosingSubmenu(null);
|
||||
setActiveSubmenu(itemLabel);
|
||||
};
|
||||
|
||||
const handleSubmenuMouseLeave = () => {
|
||||
submenuTimeoutRef.current = setTimeout(() => {
|
||||
// Start closing animation
|
||||
const currentSubmenu = activeSubmenu;
|
||||
if (currentSubmenu) {
|
||||
setClosingSubmenu(currentSubmenu);
|
||||
setActiveSubmenu(null);
|
||||
|
||||
// After animation completes (300ms), clear closing state
|
||||
closingTimeoutRef.current = setTimeout(() => {
|
||||
setClosingSubmenu(null);
|
||||
}, 350); // Slightly longer than animation to ensure completion
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const handleSubmenuClose = () => {
|
||||
// Close submenu immediately (for keyboard navigation)
|
||||
if (activeSubmenu) {
|
||||
setClosingSubmenu(activeSubmenu);
|
||||
setActiveSubmenu(null);
|
||||
|
||||
// After animation completes, clear closing state
|
||||
closingTimeoutRef.current = setTimeout(() => {
|
||||
setClosingSubmenu(null);
|
||||
}, 350);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle scroll lock when submenu is open or closing
|
||||
React.useEffect(() => {
|
||||
if (activeSubmenu || closingSubmenu) {
|
||||
document.body.classList.add('bds-submenu-open');
|
||||
} else {
|
||||
document.body.classList.remove('bds-submenu-open');
|
||||
}
|
||||
return () => {
|
||||
document.body.classList.remove('bds-submenu-open');
|
||||
};
|
||||
}, [activeSubmenu, closingSubmenu]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Turns out jQuery is necessary for firing events on Bootstrap v4
|
||||
// dropdowns. These events set classes so that the search bar and other
|
||||
// submenus collapse on mobile when you expand one submenu.
|
||||
const dds = $("#topnav-pages .dropdown");
|
||||
const top_main_nav = document.querySelector("#top-main-nav");
|
||||
dds.on("show.bs.dropdown", (evt) => {
|
||||
top_main_nav.classList.add("submenu-expanded");
|
||||
});
|
||||
dds.on("hidden.bs.dropdown", (evt) => {
|
||||
top_main_nav.classList.remove("submenu-expanded");
|
||||
});
|
||||
// Close navbar on .dropdown-item click
|
||||
const toggleNavbar = () => {
|
||||
const navbarToggler = document.querySelector(".navbar-toggler");
|
||||
const isNavbarCollapsed =
|
||||
navbarToggler.getAttribute("aria-expanded") === "true";
|
||||
if (isNavbarCollapsed) {
|
||||
navbarToggler?.click(); // Simulate click to toggle navbar
|
||||
return () => {
|
||||
if (submenuTimeoutRef.current) {
|
||||
clearTimeout(submenuTimeoutRef.current);
|
||||
}
|
||||
if (closingTimeoutRef.current) {
|
||||
clearTimeout(closingTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const dropdownItems = document.querySelectorAll(".dropdown-item");
|
||||
dropdownItems.forEach((item) => {
|
||||
item.addEventListener("click", toggleNavbar);
|
||||
});
|
||||
|
||||
// Cleanup function to remove event listeners
|
||||
return () => {
|
||||
dropdownItems.forEach((item) => {
|
||||
item.removeEventListener("click", toggleNavbar);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const navbarClasses = [
|
||||
"bds-navbar",
|
||||
alertBanner.show ? "bds-navbar--with-banner" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertBanner {...alertBanner} />
|
||||
<NavWrapper belowAlertBanner={alertBanner.show}>
|
||||
<LogoBlock to={href} img={logo} alt={altText} />
|
||||
<NavControls>
|
||||
<MobileMenuIcon />
|
||||
</NavControls>
|
||||
<TopNavCollapsible>
|
||||
<NavItems>
|
||||
{navItems}
|
||||
<div id="topnav-search" className="nav-item search">
|
||||
<Search className="topnav-search" />
|
||||
</div>
|
||||
<div id="topnav-language" className="nav-item">
|
||||
<LanguagePicker
|
||||
onChangeLanguage={changeLanguage}
|
||||
onlyIcon
|
||||
alignment="end"
|
||||
/>
|
||||
</div>
|
||||
<div id="topnav-theme" className="nav-item">
|
||||
<ColorModeSwitcher />
|
||||
</div>
|
||||
</NavItems>
|
||||
</TopNavCollapsible>
|
||||
</NavWrapper>
|
||||
{/* Backdrop blur overlay when submenu is open or closing */}
|
||||
<div
|
||||
className={`bds-submenu-backdrop ${activeSubmenu || closingSubmenu ? 'bds-submenu-backdrop--active' : ''}`}
|
||||
onClick={() => setActiveSubmenu(null)}
|
||||
/>
|
||||
<header
|
||||
className={navbarClasses}
|
||||
onMouseLeave={handleSubmenuMouseLeave}
|
||||
>
|
||||
<div className="bds-navbar__content">
|
||||
<NavLogo />
|
||||
<NavItems activeSubmenu={activeSubmenu} onSubmenuEnter={handleSubmenuMouseEnter} onSubmenuClose={handleSubmenuClose} />
|
||||
<NavControls onSearch={onSearchOpen} />
|
||||
<HamburgerButton onClick={handleHamburgerClick} />
|
||||
</div>
|
||||
{/* Submenus positioned relative to navbar */}
|
||||
<div onMouseEnter={() => activeSubmenu && handleSubmenuMouseEnter(activeSubmenu)}>
|
||||
<DevelopSubmenu isActive={activeSubmenu === 'Develop'} isClosing={closingSubmenu === 'Develop'} onClose={handleSubmenuClose} />
|
||||
<UseCasesSubmenu isActive={activeSubmenu === 'Use Cases'} isClosing={closingSubmenu === 'Use Cases'} onClose={handleSubmenuClose} />
|
||||
<CommunitySubmenu isActive={activeSubmenu === 'Community'} isClosing={closingSubmenu === 'Community'} onClose={handleSubmenuClose} />
|
||||
<NetworkSubmenu isActive={activeSubmenu === 'Network'} isClosing={closingSubmenu === 'Network'} onClose={handleSubmenuClose} />
|
||||
</div>
|
||||
</header>
|
||||
<MobileMenu isOpen={mobileMenuOpen} onClose={handleMobileMenuClose} onSearch={onSearchOpen} />
|
||||
{/* Render SearchDialog when open - this is the actual search modal */}
|
||||
{isSearchOpen && <SearchDialog onClose={onSearchClose} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopNavCollapsible({ children }) {
|
||||
return (
|
||||
<div
|
||||
className="collapse navbar-collapse justify-content-between"
|
||||
id="top-main-nav"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavDropdown(props) {
|
||||
const { label, items, pathPrefix, labelTranslationKey } = props;
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
|
||||
const dropdownGroups = items.map((item, index) => {
|
||||
if (item.items) {
|
||||
const groupLinks = item.items.map((item2, index2) => {
|
||||
const cls2 = item2.external
|
||||
? "dropdown-item external-link"
|
||||
: "dropdown-item";
|
||||
let item2_href = item2.link;
|
||||
if (item2_href && !item2_href.match(/^https?:/)) {
|
||||
item2_href = pathPrefix + item2_href;
|
||||
}
|
||||
//conditional specific for brand kit
|
||||
if (item2.link === "/XRPL_Brand_Kit.zip") {
|
||||
return (
|
||||
<a target="_blank" key={index2} href="/XRPL_Brand_Kit.zip" className={cls2}>
|
||||
{translate(item2.labelTranslationKey, item2.label)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link key={index2} className={cls2} to={item2_href}>
|
||||
{translate(item2.labelTranslationKey, item2.label)}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
const clnm = "navcol col-for-" + slugify(item.label);
|
||||
|
||||
return (
|
||||
<div key={index} className={clnm}>
|
||||
<h5 className="dropdown-item">
|
||||
{translate(item.labelTranslationKey, item.label)}
|
||||
</h5>
|
||||
{groupLinks}
|
||||
</div>
|
||||
);
|
||||
} else if (item.icon) {
|
||||
const hero_id = "dropdown-hero-for-" + slugify(label);
|
||||
const img_alt = item.label + " icon";
|
||||
|
||||
let hero_href = item.link;
|
||||
if (hero_href && !hero_href.match(/^https?:/)) {
|
||||
hero_href = pathPrefix + hero_href;
|
||||
}
|
||||
const splitlabel = item.label.split(" || ");
|
||||
let splittranslationkey = ["", ""];
|
||||
if (item.labelTranslationKey) {
|
||||
splittranslationkey = item.labelTranslationKey.split(" || ");
|
||||
}
|
||||
const newlabel = translate(splittranslationkey[0], splitlabel[0]);
|
||||
const description = translate(splittranslationkey[1], splitlabel[1]); // splitlabel[1] might be undefined, that's ok
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
className="dropdown-item dropdown-hero"
|
||||
id={hero_id}
|
||||
to={hero_href}
|
||||
>
|
||||
<img id={item.hero} alt={img_alt} src={item.icon} />
|
||||
<div className="dropdown-hero-text">
|
||||
<h4>{newlabel}</h4>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
const cls = item.external
|
||||
? "dropdown-item ungrouped external-link"
|
||||
: "dropdown-item ungrouped";
|
||||
let item_href = item.link;
|
||||
if (item_href && !item_href.match(/^https?:/)) {
|
||||
item_href = pathPrefix + item_href;
|
||||
}
|
||||
return (
|
||||
<Link key={index} className={cls} to={item_href}>
|
||||
{translate(item.labelTranslationKey, item.label)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const toggler_id = "topnav_" + slugify(label);
|
||||
const dd_id = "topnav_dd_" + slugify(label);
|
||||
|
||||
return (
|
||||
<li className="nav-item dropdown">
|
||||
<a
|
||||
className="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
id={toggler_id}
|
||||
role="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span>{translate(labelTranslationKey, label)}</span>
|
||||
</a>
|
||||
<div className="dropdown-menu" aria-labelledby={toggler_id} id={dd_id}>
|
||||
{dropdownGroups}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavWrapper(props) {
|
||||
return (
|
||||
<nav
|
||||
className="top-nav navbar navbar-expand-lg navbar-dark fixed-top"
|
||||
style={props.belowAlertBanner ? { marginTop: "52px" } : {}}
|
||||
>
|
||||
{props.children}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavControls(props) {
|
||||
return (
|
||||
<button
|
||||
className="navbar-toggler collapsed"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#top-main-nav"
|
||||
aria-controls="navbarHolder"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileMenuIcon() {
|
||||
return (
|
||||
<span className="navbar-toggler-icon">
|
||||
<div></div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function GetStartedButton() {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="btn btn-primary"
|
||||
to={"/docs/tutorials"}
|
||||
style={{ height: "38px", paddingTop: "11px" }}
|
||||
>
|
||||
{translate("Get Started")}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavItems(props) {
|
||||
return (
|
||||
<ul className="nav navbar-nav" id="topnav-pages">
|
||||
{props.children}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavItem(props) {
|
||||
return <li className="nav-item">{props.children}</li>;
|
||||
}
|
||||
|
||||
export function LogoBlock(props) {
|
||||
const { to, img, altText } = props;
|
||||
return (
|
||||
<Link className="navbar-brand" to="/">
|
||||
<img className="logo" alt={"XRP LEDGER"} height="40" src="data:," />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export class ThemeToggle extends React.Component {
|
||||
auto_update_theme() {
|
||||
const upc = window.localStorage.getItem("user-prefers-color");
|
||||
let theme = "dark"; // Default to dark theme
|
||||
if (!upc) {
|
||||
// User hasn't saved a preference specifically for this site; check
|
||||
// the browser-level preferences.
|
||||
if (
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: light)").matches
|
||||
) {
|
||||
theme = "light";
|
||||
}
|
||||
} else {
|
||||
// Follow user's saved setting.
|
||||
theme = upc == "light" ? "light" : "dark";
|
||||
}
|
||||
const disable_theme = theme == "dark" ? "light" : "dark";
|
||||
document.documentElement.classList.add(theme);
|
||||
document.documentElement.classList.remove(disable_theme);
|
||||
}
|
||||
|
||||
user_choose_theme() {
|
||||
const new_theme = document.documentElement.classList.contains("dark")
|
||||
? "light"
|
||||
: "dark";
|
||||
window.localStorage.setItem("user-prefers-color", new_theme);
|
||||
document.body.style.transition = "background-color .2s ease";
|
||||
const disable_theme = new_theme == "dark" ? "light" : "dark";
|
||||
document.documentElement.classList.add(new_theme);
|
||||
document.documentElement.classList.remove(disable_theme);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="nav-item" id="topnav-theme">
|
||||
<form className="form-inline">
|
||||
<div
|
||||
className="custom-control custom-theme-toggle form-inline-item"
|
||||
title=""
|
||||
data-toggle="tooltip"
|
||||
data-placement="left"
|
||||
data-original-title="Toggle Dark Mode"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="custom-control-input"
|
||||
id="css-toggle-btn"
|
||||
onClick={this.user_choose_theme}
|
||||
/>
|
||||
<label className="custom-control-label" htmlFor="css-toggle-btn">
|
||||
<span className="d-lg-none">Light/Dark Theme</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.auto_update_theme();
|
||||
}
|
||||
}
|
||||
|
||||
82
@theme/components/Navbar/components/AlertBanner.tsx
Normal file
82
@theme/components/Navbar/components/AlertBanner.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as React from "react";
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import moment from "moment-timezone";
|
||||
import { arrowUpRight } from "../constants/icons";
|
||||
|
||||
interface AlertBannerProps {
|
||||
message: string;
|
||||
button: string;
|
||||
link: string;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert Banner Component.
|
||||
* Displays a promotional banner at the top of the page.
|
||||
*/
|
||||
export function AlertBanner({ message, button, link, show }: AlertBannerProps) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
const bannerRef = React.useRef<HTMLAnchorElement>(null);
|
||||
// Use null initial state to avoid hydration mismatch - server and client both render null initially
|
||||
const [displayDate, setDisplayDate] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const calculateCountdown = () => {
|
||||
const target = moment.tz('2025-06-11 08:00:00', 'Asia/Singapore');
|
||||
const now = moment();
|
||||
const daysUntil = target.diff(now, 'days');
|
||||
|
||||
let newDisplayDate = "JUNE 10-12";
|
||||
if (daysUntil > 0) {
|
||||
newDisplayDate = daysUntil === 1 ? 'IN 1 DAY' : `IN ${daysUntil} DAYS`;
|
||||
} else if (daysUntil === 0) {
|
||||
const hoursUntil = target.diff(now, 'hours');
|
||||
newDisplayDate = hoursUntil > 0 ? 'TODAY' : "JUNE 10-12";
|
||||
}
|
||||
|
||||
setDisplayDate(newDisplayDate);
|
||||
};
|
||||
|
||||
calculateCountdown();
|
||||
const interval = setInterval(calculateCountdown, 60 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const banner = bannerRef.current;
|
||||
if (!banner) return;
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
banner.classList.add("has-hover");
|
||||
};
|
||||
|
||||
banner.addEventListener("mouseenter", handleMouseEnter);
|
||||
return () => {
|
||||
banner.removeEventListener("mouseenter", handleMouseEnter);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
ref={bannerRef}
|
||||
className="top-banner fixed-top web-banner"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={translate("Get Tickets for the APEX 2025 Event")}
|
||||
>
|
||||
<div className="banner-event-details">
|
||||
<div className="event-info">{translate(message)}</div>
|
||||
<div className="event-date">{displayDate ?? translate("JUNE 10-12")}</div>
|
||||
</div>
|
||||
<div className="banner-button">
|
||||
<div className="button-text">{translate(button)}</div>
|
||||
<img className="button-icon" src={arrowUpRight} alt="" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
114
@theme/components/Navbar/components/NavItems.tsx
Normal file
114
@theme/components/Navbar/components/NavItems.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as React from "react";
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { BdsLink } from "../../../../shared/components/Link/Link";
|
||||
import { navItems } from "../constants/navigation";
|
||||
|
||||
interface NavItemsProps {
|
||||
activeSubmenu: string | null;
|
||||
onSubmenuEnter: (itemLabel: string) => void;
|
||||
onSubmenuClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nav Items Component.
|
||||
* Centered navigation links with submenu support.
|
||||
* ARIA compliant with full keyboard navigation support.
|
||||
*/
|
||||
export function NavItems({ activeSubmenu, onSubmenuEnter, onSubmenuClose }: NavItemsProps) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
const [activeItem, setActiveItem] = React.useState<string | null>(null);
|
||||
|
||||
const handleMouseEnter = (itemLabel: string, hasSubmenu: boolean) => {
|
||||
setActiveItem(itemLabel);
|
||||
if (hasSubmenu) {
|
||||
onSubmenuEnter(itemLabel);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = (hasSubmenu: boolean) => {
|
||||
if (!hasSubmenu) {
|
||||
setActiveItem(null);
|
||||
}
|
||||
// Don't close submenu on leave - let the parent Navbar handle that
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent, itemLabel: string) => {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
event.preventDefault();
|
||||
// Toggle submenu on Enter/Space
|
||||
if (activeSubmenu === itemLabel) {
|
||||
onSubmenuClose?.();
|
||||
} else {
|
||||
onSubmenuEnter(itemLabel);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
onSubmenuClose?.();
|
||||
break;
|
||||
case 'Tab':
|
||||
// If submenu is open and Tab is pressed (without Shift), move focus into submenu
|
||||
if (activeSubmenu === itemLabel && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
// Focus first focusable element in submenu
|
||||
const submenu = document.querySelector('.bds-submenu--active');
|
||||
const firstFocusable = submenu?.querySelector<HTMLElement>('a, button');
|
||||
firstFocusable?.focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
// If submenu is open, move focus into submenu
|
||||
if (activeSubmenu === itemLabel) {
|
||||
event.preventDefault();
|
||||
// Focus first focusable element in submenu
|
||||
const submenu = document.querySelector('.bds-submenu--active');
|
||||
const firstFocusable = submenu?.querySelector<HTMLElement>('a, button');
|
||||
firstFocusable?.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Sync activeItem with activeSubmenu state
|
||||
React.useEffect(() => {
|
||||
if (!activeSubmenu) {
|
||||
setActiveItem(null);
|
||||
}
|
||||
}, [activeSubmenu]);
|
||||
|
||||
return (
|
||||
<nav className="bds-navbar__items" aria-label={translate("Main navigation")}>
|
||||
{navItems.map((item) => (
|
||||
item.hasSubmenu ? (
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
className={`bds-navbar__item ${activeItem === item.label || activeSubmenu === item.label ? 'bds-navbar__item--active' : ''}`}
|
||||
onMouseEnter={() => handleMouseEnter(item.label, true)}
|
||||
onMouseLeave={() => handleMouseLeave(true)}
|
||||
onKeyDown={(e) => handleKeyDown(e, item.label)}
|
||||
aria-expanded={activeSubmenu === item.label}
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
{translate(item.labelTranslationKey, item.label)}
|
||||
</button>
|
||||
) : (
|
||||
<BdsLink
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className={`bds-navbar__item ${activeItem === item.label ? 'bds-navbar__item--active' : ''}`}
|
||||
onMouseEnter={() => handleMouseEnter(item.label, false)}
|
||||
onMouseLeave={() => handleMouseLeave(false)}
|
||||
variant="inline"
|
||||
>
|
||||
{translate(item.labelTranslationKey, item.label)}
|
||||
</BdsLink>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
34
@theme/components/Navbar/components/NavLogo.tsx
Normal file
34
@theme/components/Navbar/components/NavLogo.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { BdsLink } from "../../../../shared/components/Link/Link";
|
||||
import { xrpSymbolBlack, xrpLogotypeBlack, xrpLedgerNav } from "../constants/icons";
|
||||
|
||||
/**
|
||||
* Nav Logo Component.
|
||||
* Shows symbol on desktop/mobile, full logotype on tablet.
|
||||
* On desktop hover, the "XRP LEDGER" text animates out to the right.
|
||||
*/
|
||||
export function NavLogo() {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
|
||||
return (
|
||||
<BdsLink href="/" className="bds-navbar__logo" aria-label={translate("XRP Ledger Home")} variant="inline">
|
||||
<img
|
||||
src={xrpSymbolBlack}
|
||||
alt={translate("XRP Ledger")}
|
||||
className="bds-navbar__logo-symbol"
|
||||
/>
|
||||
<img
|
||||
src={xrpLedgerNav}
|
||||
alt=""
|
||||
className="bds-navbar__logo-text"
|
||||
/>
|
||||
<img
|
||||
src={xrpLogotypeBlack}
|
||||
alt={translate("XRP Ledger")}
|
||||
className="bds-navbar__logo-full"
|
||||
/>
|
||||
</BdsLink>
|
||||
);
|
||||
}
|
||||
|
||||
5
@theme/components/Navbar/components/index.ts
Normal file
5
@theme/components/Navbar/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Re-export navbar components
|
||||
export { AlertBanner } from './AlertBanner';
|
||||
export { NavLogo } from './NavLogo';
|
||||
export { NavItems } from './NavItems';
|
||||
|
||||
71
@theme/components/Navbar/constants/icons.ts
Normal file
71
@theme/components/Navbar/constants/icons.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// Navbar icon imports
|
||||
|
||||
// Main navbar icons
|
||||
export { default as xrpSymbolBlack } from "../../../../static/img/navbar/xrp-symbol-black.svg";
|
||||
export { default as xrpLogotypeBlack } from "../../../../static/img/navbar/xrp-logotype-black.svg";
|
||||
export { default as xrpLedgerNav } from "../../../../static/img/navbar/xrp-ledger-nav.svg";
|
||||
export { default as searchIcon } from "../../../../static/img/navbar/search-icon.svg";
|
||||
export { default as modeToggleIcon } from "../../../../static/img/navbar/mode-toggle.svg";
|
||||
export { default as globeIcon } from "../../../../static/img/navbar/globe-icon.svg";
|
||||
export { default as chevronDown } from "../../../../static/img/navbar/chevron-down.svg";
|
||||
export { default as hamburgerIcon } from "../../../../static/img/navbar/hamburger-icon.svg";
|
||||
export { default as arrowUpRight } from "../../../../static/img/icons/arrow-up-right-custom.svg";
|
||||
|
||||
// Network submenu pattern images (used for both light and dark mode)
|
||||
export { default as resourcesIconPattern } from "../../../../static/img/navbar/resources-icon.svg";
|
||||
export { default as insightsIconPattern } from "../../../../static/img/navbar/insights-icon.svg";
|
||||
|
||||
// Submenu icons - imported once, exported individually and used in navIcons mapping
|
||||
import devHomeIcon from "../../../../static/img/navbar/dev_home.svg";
|
||||
import learnIcon from "../../../../static/img/navbar/learn.svg";
|
||||
import codeSamplesIcon from "../../../../static/img/navbar/code_samples.svg";
|
||||
import docsIcon from "../../../../static/img/navbar/docs.svg";
|
||||
import clientLibIcon from "../../../../static/img/navbar/client_lib.svg";
|
||||
import paymentsIcon from "../../../../static/img/navbar/payments.svg";
|
||||
import tokenizationIcon from "../../../../static/img/navbar/tokenization.svg";
|
||||
import creditIcon from "../../../../static/img/navbar/credit.svg";
|
||||
import tradingIcon from "../../../../static/img/navbar/trading.svg";
|
||||
import communityIcon from "../../../../static/img/navbar/community.svg";
|
||||
import fundingIcon from "../../../../static/img/navbar/funding.svg";
|
||||
import contributeIcon from "../../../../static/img/navbar/contribute.svg";
|
||||
import ecosystemIcon from "../../../../static/img/navbar/ecosystem.svg";
|
||||
import insightsIcon from "../../../../static/img/navbar/insights.svg";
|
||||
import resourcesIcon from "../../../../static/img/navbar/resources.svg";
|
||||
|
||||
// Re-export submenu icons for direct imports
|
||||
export default {
|
||||
devHomeIcon,
|
||||
learnIcon,
|
||||
codeSamplesIcon,
|
||||
docsIcon,
|
||||
clientLibIcon,
|
||||
paymentsIcon,
|
||||
tokenizationIcon,
|
||||
creditIcon,
|
||||
tradingIcon,
|
||||
communityIcon,
|
||||
fundingIcon,
|
||||
contributeIcon,
|
||||
ecosystemIcon,
|
||||
insightsIcon,
|
||||
resourcesIcon,
|
||||
};
|
||||
|
||||
// Dynamic icon lookup for navbar submenus
|
||||
export const navIcons: Record<string, string> = {
|
||||
dev_home: devHomeIcon,
|
||||
learn: learnIcon,
|
||||
code_samples: codeSamplesIcon,
|
||||
docs: docsIcon,
|
||||
client_lib: clientLibIcon,
|
||||
payments: paymentsIcon,
|
||||
tokenization: tokenizationIcon,
|
||||
credit: creditIcon,
|
||||
trading: tradingIcon,
|
||||
community: communityIcon,
|
||||
contribute: contributeIcon,
|
||||
insights: insightsIcon,
|
||||
resources: resourcesIcon,
|
||||
ecosystem: ecosystemIcon,
|
||||
funding: fundingIcon,
|
||||
};
|
||||
4
@theme/components/Navbar/constants/index.ts
Normal file
4
@theme/components/Navbar/constants/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Re-export all constants
|
||||
export * from './icons';
|
||||
export * from './navigation';
|
||||
|
||||
161
@theme/components/Navbar/constants/navigation.ts
Normal file
161
@theme/components/Navbar/constants/navigation.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { NavItem, SubmenuItemBase, SubmenuItemWithChildren, SubmenuItem, NetworkSubmenuSection } from '../types';
|
||||
|
||||
// Alert Banner Configuration
|
||||
export const alertBanner = {
|
||||
show: false,
|
||||
message: "APEX 2025",
|
||||
button: "REGISTER",
|
||||
link: "https://www.xrpledgerapex.com/?utm_source=xrplwebsite&utm_medium=direct&utm_campaign=xrpl-event-ho-xrplapex-glb-2025-q1_xrplwebsite_ari_arp_bf_rsvp&utm_content=cta_btn_english_pencilbanner"
|
||||
};
|
||||
|
||||
// Main navigation items
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: "Develop", labelTranslationKey: "navbar.develop", href: "/develop", hasSubmenu: true },
|
||||
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/use-cases", hasSubmenu: true },
|
||||
{ label: "Community", labelTranslationKey: "navbar.community", href: "/community", hasSubmenu: true },
|
||||
{ label: "Network", labelTranslationKey: "navbar.network", href: "/resources", hasSubmenu: true },
|
||||
{ label: "Showcase (Temporary)", labelTranslationKey: "navbar.showcase", href: "/showcase", hasSubmenu: false },
|
||||
];
|
||||
|
||||
// Develop submenu data structure
|
||||
export const developSubmenuData: {
|
||||
left: SubmenuItemBase[];
|
||||
right: SubmenuItemWithChildren[];
|
||||
} = {
|
||||
left: [
|
||||
{ label: "Developer's Home", href: "/develop", icon: "dev_home" },
|
||||
{ label: "Learn", href: "https://learn.xrpl.org", icon: "learn" },
|
||||
{ label: "Code Samples", href: "/resources/code-samples", icon: "code_samples" },
|
||||
],
|
||||
right: [
|
||||
{
|
||||
label: "Docs",
|
||||
href: "/docs",
|
||||
icon: "docs",
|
||||
children: [
|
||||
{ label: "API Reference", href: "/docs/references" },
|
||||
{ label: "Tutorials", href: "/docs/tutorials" },
|
||||
{ label: "Concepts", href: "/docs/concepts" },
|
||||
{ label: "Infrastructure", href: "/docs/infrastructure" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Client Libraries",
|
||||
href: "/docs/tutorials",
|
||||
icon: "client_lib",
|
||||
children: [
|
||||
{ label: "JavaScript", href: "/docs/tutorials/get-started/get-started-javascript" },
|
||||
{ label: "Python", href: "/docs/tutorials/get-started/get-started-python" },
|
||||
{ label: "PHP", href: "/docs/tutorials/get-started/get-started-php" },
|
||||
{ label: "Go", href: "/docs/tutorials/get-started/get-started-go" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Use Cases submenu data structure
|
||||
export const useCasesSubmenuData: {
|
||||
left: SubmenuItemWithChildren[];
|
||||
right: SubmenuItemWithChildren[];
|
||||
} = {
|
||||
left: [
|
||||
{
|
||||
label: "Payments",
|
||||
href: "/docs/use-cases/payments",
|
||||
icon: "payments",
|
||||
children: [
|
||||
{ label: "Direct XRP Payments", href: "/docs/use-cases/payments/direct-xrp-payments" },
|
||||
{ label: "Cross-currency Payments", href: "/docs/use-cases/payments/cross-currency-payments" },
|
||||
{ label: "Escrow", href: "/docs/use-cases/payments/escrow" },
|
||||
{ label: "Checks", href: "/docs/use-cases/payments/checks" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Tokenization",
|
||||
href: "/docs/use-cases/tokenization",
|
||||
icon: "tokenization",
|
||||
children: [
|
||||
{ label: "Stablecoin", href: "/docs/use-cases/tokenization/stablecoin" },
|
||||
{ label: "NFT", href: "/docs/use-cases/tokenization/nft" },
|
||||
],
|
||||
},
|
||||
],
|
||||
right: [
|
||||
{
|
||||
label: "Credit",
|
||||
href: "/docs/use-cases/credit",
|
||||
icon: "credit",
|
||||
children: [
|
||||
{ label: "Lending", href: "/docs/use-cases/credit/lending" },
|
||||
{ label: "Collateralization", href: "/docs/use-cases/credit/collateralization" },
|
||||
{ label: "Sustainability", href: "/docs/use-cases/credit/sustainability" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Trading",
|
||||
href: "/docs/use-cases/trading",
|
||||
icon: "trading",
|
||||
children: [
|
||||
{ label: "DEX", href: "/docs/use-cases/trading/dex" },
|
||||
{ label: "Permissioned Trading", href: "/docs/use-cases/trading/permissioned-trading" },
|
||||
{ label: "AMM", href: "/docs/use-cases/trading/amm" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Community submenu data structure
|
||||
export const communitySubmenuData: {
|
||||
left: SubmenuItem[];
|
||||
right: SubmenuItem[];
|
||||
} = {
|
||||
left: [
|
||||
{
|
||||
label: "Community",
|
||||
href: "/community",
|
||||
icon: "community",
|
||||
children: [
|
||||
{ label: "Events", href: "/community/events" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
],
|
||||
},
|
||||
{ label: "Funding", href: "/community/developer-funding", icon: "funding" },
|
||||
],
|
||||
right: [
|
||||
{
|
||||
label: "Contribute",
|
||||
href: "/community/contribute",
|
||||
icon: "contribute",
|
||||
children: [
|
||||
{ label: "Bug Bounty", href: "/blog/2020/rippled-1.5.0#bug-bounties-and-responsible-disclosures" },
|
||||
{ label: "Research", href: "https://xls.xrpl.org/" },
|
||||
],
|
||||
},
|
||||
{ label: "Ecosystem Map", href: "/about/uses", icon: "ecosystem" },
|
||||
],
|
||||
};
|
||||
|
||||
// Network submenu data
|
||||
export const networkSubmenuData: NetworkSubmenuSection[] = [
|
||||
{
|
||||
label: "Resources",
|
||||
href: "/resources",
|
||||
icon: "resources",
|
||||
children: [
|
||||
{ label: "About", href: "/about/history" },
|
||||
{ label: "XRPL Brand Kit", href: "/community/brand-kit" },
|
||||
],
|
||||
patternColor: 'lilac',
|
||||
},
|
||||
{
|
||||
label: "Insights",
|
||||
href: "/insights",
|
||||
icon: "insights",
|
||||
children: [
|
||||
{ label: "Explorer", href: "https://livenet.xrpl.org" },
|
||||
{ label: "Amendment Voting Status", href: "https://xrpl.org/resources/known-amendments" },
|
||||
],
|
||||
patternColor: 'green',
|
||||
},
|
||||
];
|
||||
|
||||
19
@theme/components/Navbar/controls/HamburgerButton.tsx
Normal file
19
@theme/components/Navbar/controls/HamburgerButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { hamburgerIcon } from "../constants/icons";
|
||||
|
||||
interface HamburgerButtonProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hamburger Menu Button Component.
|
||||
* Mobile-only button that opens the mobile menu.
|
||||
*/
|
||||
export function HamburgerButton({ onClick }: HamburgerButtonProps) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
|
||||
return <IconButton icon={hamburgerIcon} ariaLabel={translate("Open menu")} className="bds-navbar__hamburger" onClick={onClick} />;
|
||||
}
|
||||
|
||||
28
@theme/components/Navbar/controls/IconButton.tsx
Normal file
28
@theme/components/Navbar/controls/IconButton.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
interface IconButtonProps {
|
||||
/** The icon image source */
|
||||
icon: string;
|
||||
/** Accessible label for the button */
|
||||
ariaLabel: string;
|
||||
/** Optional click handler */
|
||||
onClick?: () => void;
|
||||
/** CSS class name for styling variants */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified Icon Button component.
|
||||
* Used for search, mode toggle, hamburger menu, and other icon-only buttons.
|
||||
*/
|
||||
export function IconButton({ icon, ariaLabel, onClick, className = "bds-navbar__icon" }: IconButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
aria-label={ariaLabel}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img src={icon} alt="" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
85
@theme/components/Navbar/controls/LanguageDropdown.tsx
Normal file
85
@theme/components/Navbar/controls/LanguageDropdown.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as React from "react";
|
||||
import { useLanguagePicker, useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
|
||||
interface LanguageDropdownProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Language Dropdown Component.
|
||||
* Displays available language options in a dropdown menu.
|
||||
* Based on Figma design: dark background with rounded corners.
|
||||
*/
|
||||
export function LanguageDropdown({ isOpen, onClose }: LanguageDropdownProps) {
|
||||
const { currentLocale, locales, setLocale } = useLanguagePicker();
|
||||
const { useL10n, useTranslate } = useThemeHooks();
|
||||
const { changeLanguage } = useL10n();
|
||||
const { translate } = useTranslate();
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle clicking outside to close
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
// Check if click was on the language pill (parent trigger)
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.bds-navbar__lang-pill')) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || locales.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleLanguageSelect = (localeCode: string) => {
|
||||
setLocale(localeCode);
|
||||
changeLanguage(localeCode);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="bds-lang-dropdown"
|
||||
role="menu"
|
||||
aria-label={translate("Language selection")}
|
||||
>
|
||||
{locales.map((locale) => {
|
||||
const isActive = locale.code === currentLocale?.code;
|
||||
return (
|
||||
<button
|
||||
key={locale.code}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={`bds-lang-dropdown__item ${isActive ? 'bds-lang-dropdown__item--active' : ''}`}
|
||||
onClick={() => handleLanguageSelect(locale.code)}
|
||||
aria-current={isActive ? 'true' : undefined}
|
||||
>
|
||||
{locale.name || locale.code}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
53
@theme/components/Navbar/controls/LanguagePill.tsx
Normal file
53
@theme/components/Navbar/controls/LanguagePill.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useLanguagePicker, useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { globeIcon, chevronDown } from "../constants/icons";
|
||||
|
||||
interface LanguagePillProps {
|
||||
onClick?: () => void;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get short display name for a locale code.
|
||||
* e.g., "en-US" -> "En", "ja" -> "日本語"
|
||||
*/
|
||||
function getLocaleShortName(code: string | undefined): string {
|
||||
if (!code) return "En";
|
||||
|
||||
// Map locale codes to short display names
|
||||
const shortNames: Record<string, string> = {
|
||||
"en-US": "En",
|
||||
"en": "En",
|
||||
"ja": "日本語",
|
||||
};
|
||||
|
||||
return shortNames[code] || code.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Language Pill Button Component.
|
||||
* Shows current language and opens language selector.
|
||||
*/
|
||||
export function LanguagePill({ onClick, isOpen }: LanguagePillProps) {
|
||||
const { currentLocale } = useLanguagePicker();
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
const displayName = getLocaleShortName(currentLocale?.code);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`bds-navbar__lang-pill ${isOpen ? 'bds-navbar__lang-pill--open' : ''}`}
|
||||
aria-label={translate("Select language")}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="menu"
|
||||
onClick={onClick}
|
||||
>
|
||||
<img src={globeIcon} alt="" className="bds-navbar__lang-pill-icon" />
|
||||
<span className="bds-navbar__lang-pill-text">
|
||||
<span>{displayName}</span>
|
||||
<img src={chevronDown} alt="" className="bds-navbar__lang-pill-chevron" />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
19
@theme/components/Navbar/controls/ModeToggleButton.tsx
Normal file
19
@theme/components/Navbar/controls/ModeToggleButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { modeToggleIcon } from "../constants/icons";
|
||||
|
||||
interface ModeToggleButtonProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode Toggle Button Component.
|
||||
* Icon button that toggles between light and dark mode.
|
||||
*/
|
||||
export function ModeToggleButton({ onClick }: ModeToggleButtonProps) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
|
||||
return <IconButton icon={modeToggleIcon} ariaLabel={translate("Toggle color mode")} onClick={onClick} />;
|
||||
}
|
||||
|
||||
46
@theme/components/Navbar/controls/NavControls.tsx
Normal file
46
@theme/components/Navbar/controls/NavControls.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import { SearchButton } from "./SearchButton";
|
||||
import { ModeToggleButton } from "./ModeToggleButton";
|
||||
import { LanguagePill } from "./LanguagePill";
|
||||
import { LanguageDropdown } from "./LanguageDropdown";
|
||||
|
||||
interface NavControlsProps {
|
||||
onSearch?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nav Controls Component.
|
||||
* Right side of the navbar containing search, mode toggle, and language selector.
|
||||
*/
|
||||
export function NavControls({ onSearch }: NavControlsProps) {
|
||||
const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = React.useState(false);
|
||||
|
||||
const handleModeToggle = () => {
|
||||
// Toggle between light and dark theme
|
||||
const newTheme = document.documentElement.classList.contains("dark") ? "light" : "dark";
|
||||
window.localStorage.setItem("user-prefers-color", newTheme);
|
||||
document.body.style.transition = "background-color .2s ease";
|
||||
document.documentElement.classList.remove("dark", "light");
|
||||
document.documentElement.classList.add(newTheme);
|
||||
};
|
||||
|
||||
const handleLanguageClick = () => {
|
||||
setIsLanguageDropdownOpen(!isLanguageDropdownOpen);
|
||||
};
|
||||
|
||||
const handleLanguageDropdownClose = () => {
|
||||
setIsLanguageDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bds-navbar__controls">
|
||||
<SearchButton onClick={onSearch} />
|
||||
<ModeToggleButton onClick={handleModeToggle} />
|
||||
<div className="bds-navbar__lang-wrapper">
|
||||
<LanguagePill onClick={handleLanguageClick} isOpen={isLanguageDropdownOpen} />
|
||||
<LanguageDropdown isOpen={isLanguageDropdownOpen} onClose={handleLanguageDropdownClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
19
@theme/components/Navbar/controls/SearchButton.tsx
Normal file
19
@theme/components/Navbar/controls/SearchButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { searchIcon } from "../constants/icons";
|
||||
|
||||
interface SearchButtonProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Button Component.
|
||||
* Icon button that triggers the search modal.
|
||||
*/
|
||||
export function SearchButton({ onClick }: SearchButtonProps) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
|
||||
return <IconButton icon={searchIcon} ariaLabel={translate("Search")} onClick={onClick} />;
|
||||
}
|
||||
|
||||
11
@theme/components/Navbar/controls/index.ts
Normal file
11
@theme/components/Navbar/controls/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Unified base component
|
||||
export { IconButton } from './IconButton';
|
||||
|
||||
// Specific button implementations (use IconButton internally)
|
||||
export { NavControls } from './NavControls';
|
||||
export { SearchButton } from './SearchButton';
|
||||
export { ModeToggleButton } from './ModeToggleButton';
|
||||
export { LanguagePill } from './LanguagePill';
|
||||
export { LanguageDropdown } from './LanguageDropdown';
|
||||
export { HamburgerButton } from './HamburgerButton';
|
||||
|
||||
30
@theme/components/Navbar/icons/ChevronIcon.tsx
Normal file
30
@theme/components/Navbar/icons/ChevronIcon.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from "react";
|
||||
|
||||
interface ChevronIconProps {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chevron Icon Component for Mobile Accordion
|
||||
*/
|
||||
export function ChevronIcon({ expanded }: ChevronIconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={`bds-mobile-menu__chevron ${expanded ? 'bds-mobile-menu__chevron--expanded' : ''}`}
|
||||
width="13"
|
||||
height="8"
|
||||
viewBox="0 0 13 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1 1L6.5 6.5L12 1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
14
@theme/components/Navbar/icons/CloseIcon.tsx
Normal file
14
@theme/components/Navbar/icons/CloseIcon.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* Close Icon Component for Mobile Menu
|
||||
*/
|
||||
export function CloseIcon() {
|
||||
return (
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="7" y1="7" x2="21" y2="21" stroke="#141414" strokeWidth="2" strokeLinecap="round" />
|
||||
<line x1="21" y1="7" x2="7" y2="21" stroke="#141414" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
56
@theme/components/Navbar/icons/SubmenuArrow.tsx
Normal file
56
@theme/components/Navbar/icons/SubmenuArrow.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
interface ArrowIconProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
/**
|
||||
* When true, the horizontal line has a class for CSS animation (parent links).
|
||||
* When false, the full arrow is shown without animation class (child links).
|
||||
*/
|
||||
animated?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified Arrow Icon component.
|
||||
* - For parent links (animated=true): horizontal line animates away on hover
|
||||
* - For child links (animated=false): full static arrow
|
||||
*/
|
||||
export function ArrowIcon({ className, color = "currentColor", animated = true }: ArrowIconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="15"
|
||||
height="14"
|
||||
viewBox="0 0 26 22"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Chevron part */}
|
||||
<path
|
||||
d="M14.0019 1.00191L24.0015 11.0015L14.0019 21.001"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{/* Horizontal line */}
|
||||
<path
|
||||
d="M23.999 10.999H0"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
className={animated ? "arrow-horizontal" : undefined}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Backwards-compatible aliases
|
||||
export const SubmenuArrow = (props: Omit<ArrowIconProps, 'animated'>) => (
|
||||
<ArrowIcon {...props} animated={true} />
|
||||
);
|
||||
|
||||
export const SubmenuChildArrow = (props: Omit<ArrowIconProps, 'animated'>) => (
|
||||
<ArrowIcon {...props} animated={false} />
|
||||
);
|
||||
|
||||
6
@theme/components/Navbar/icons/index.ts
Normal file
6
@theme/components/Navbar/icons/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Re-export all icon components
|
||||
// Unified arrow icon with backwards-compatible aliases
|
||||
export { ArrowIcon, SubmenuArrow, SubmenuChildArrow } from './SubmenuArrow';
|
||||
export { CloseIcon } from './CloseIcon';
|
||||
export { ChevronIcon } from './ChevronIcon';
|
||||
|
||||
13
@theme/components/Navbar/index.ts
Normal file
13
@theme/components/Navbar/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// Main Navbar component
|
||||
export { Navbar } from './Navbar';
|
||||
|
||||
// Re-export types
|
||||
export * from './types';
|
||||
|
||||
// Re-export components for advanced usage
|
||||
export * from './components';
|
||||
export * from './controls';
|
||||
export * from './submenus';
|
||||
export * from './mobile-menu';
|
||||
export * from './icons';
|
||||
|
||||
215
@theme/components/Navbar/mobile-menu/MobileMenu.tsx
Normal file
215
@theme/components/Navbar/mobile-menu/MobileMenu.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import * as React from "react";
|
||||
import { useThemeHooks, useLanguagePicker } from "@redocly/theme/core/hooks";
|
||||
import { BdsLink } from "../../../../shared/components/Link/Link";
|
||||
import { CloseIcon, ChevronIcon } from "../icons";
|
||||
import { xrpSymbolBlack, globeIcon, chevronDown, modeToggleIcon, searchIcon } from "../constants/icons";
|
||||
import { navItems } from "../constants/navigation";
|
||||
import { MobileMenuContent, type MobileMenuKey } from "./MobileMenuContent";
|
||||
|
||||
interface MobileMenuProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSearch?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile Menu Component.
|
||||
* Full-screen slide-out menu for mobile devices.
|
||||
*/
|
||||
export function MobileMenu({ isOpen, onClose, onSearch }: MobileMenuProps) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
const [expandedItem, setExpandedItem] = React.useState<string | null>("Develop");
|
||||
|
||||
// Handle body scroll lock
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.classList.add('bds-mobile-menu-open');
|
||||
} else {
|
||||
document.body.classList.remove('bds-mobile-menu-open');
|
||||
}
|
||||
return () => {
|
||||
document.body.classList.remove('bds-mobile-menu-open');
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const toggleAccordion = (item: string) => {
|
||||
setExpandedItem(expandedItem === item ? null : item);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
if (onSearch) {
|
||||
onSearch();
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleModeToggle = () => {
|
||||
const newTheme = document.documentElement.classList.contains("dark") ? "light" : "dark";
|
||||
window.localStorage.setItem("user-prefers-color", newTheme);
|
||||
document.body.style.transition = "background-color .2s ease";
|
||||
document.documentElement.classList.remove("dark", "light");
|
||||
document.documentElement.classList.add(newTheme);
|
||||
};
|
||||
|
||||
const renderAccordionContent = (label: string) => {
|
||||
// All nav items with submenus use the unified MobileMenuContent
|
||||
const validKeys: MobileMenuKey[] = ['Develop', 'Use Cases', 'Community', 'Network'];
|
||||
if (validKeys.includes(label as MobileMenuKey)) {
|
||||
return <MobileMenuContent menuKey={label as MobileMenuKey} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`bds-mobile-menu ${isOpen ? 'bds-mobile-menu--open' : ''}`}>
|
||||
{/* Header */}
|
||||
<div className="bds-mobile-menu__header">
|
||||
<BdsLink href="/" className="bds-navbar__logo" aria-label={translate("XRP Ledger Home")} onClick={onClose} variant="inline">
|
||||
<img src={xrpSymbolBlack} alt={translate("XRP Ledger")} className="bds-navbar__logo-symbol" style={{ width: 33, height: 28 }} />
|
||||
</BdsLink>
|
||||
<button
|
||||
type="button"
|
||||
className="bds-mobile-menu__close"
|
||||
aria-label={translate("Close menu")}
|
||||
onClick={onClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bds-mobile-menu__content">
|
||||
<div className="bds-mobile-menu__accordion">
|
||||
{navItems.map((item) => (
|
||||
<React.Fragment key={item.label}>
|
||||
<button
|
||||
type="button"
|
||||
className="bds-mobile-menu__accordion-header"
|
||||
onClick={() => item.hasSubmenu ? toggleAccordion(item.label) : null}
|
||||
aria-expanded={expandedItem === item.label}
|
||||
>
|
||||
{item.hasSubmenu ? (
|
||||
<>
|
||||
<span>{translate(item.labelTranslationKey, item.label)}</span>
|
||||
<ChevronIcon expanded={expandedItem === item.label} />
|
||||
</>
|
||||
) : (
|
||||
<BdsLink
|
||||
href={item.href}
|
||||
onClick={onClose}
|
||||
variant="inline"
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
color: 'inherit',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<span>{translate(item.labelTranslationKey, item.label)}</span>
|
||||
<ChevronIcon expanded={false} />
|
||||
</BdsLink>
|
||||
)}
|
||||
</button>
|
||||
{item.hasSubmenu && (
|
||||
<div
|
||||
className={`bds-mobile-menu__accordion-content ${
|
||||
expandedItem === item.label ? 'bds-mobile-menu__accordion-content--expanded' : ''
|
||||
}`}
|
||||
>
|
||||
{renderAccordionContent(item.label)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<MobileMenuFooter
|
||||
onModeToggle={handleModeToggle}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface MobileMenuFooterProps {
|
||||
onModeToggle: () => void;
|
||||
onSearch: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get short display name for a locale code.
|
||||
*/
|
||||
function getLocaleShortName(code: string | undefined): string {
|
||||
if (!code) return "En";
|
||||
const shortNames: Record<string, string> = {
|
||||
"en-US": "En",
|
||||
"en": "En",
|
||||
"ja": "日本語",
|
||||
};
|
||||
return shortNames[code] || code.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function MobileMenuFooter({ onModeToggle, onSearch }: MobileMenuFooterProps) {
|
||||
const { currentLocale, locales, setLocale } = useLanguagePicker();
|
||||
const { useL10n, useTranslate } = useThemeHooks();
|
||||
const { changeLanguage } = useL10n();
|
||||
const { translate } = useTranslate();
|
||||
const [isLangOpen, setIsLangOpen] = React.useState(false);
|
||||
const displayName = getLocaleShortName(currentLocale?.code);
|
||||
|
||||
const handleLanguageSelect = (localeCode: string) => {
|
||||
setLocale(localeCode);
|
||||
changeLanguage(localeCode);
|
||||
setIsLangOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bds-mobile-menu__footer">
|
||||
<div className="bds-mobile-menu__lang-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
className={`bds-mobile-menu__lang-pill ${isLangOpen ? 'bds-mobile-menu__lang-pill--open' : ''}`}
|
||||
aria-label={translate("Select language")}
|
||||
aria-expanded={isLangOpen}
|
||||
onClick={() => setIsLangOpen(!isLangOpen)}
|
||||
>
|
||||
<img src={globeIcon} alt="" className="bds-mobile-menu__lang-pill-icon" />
|
||||
<span className="bds-mobile-menu__lang-pill-text">
|
||||
<span>{displayName}</span>
|
||||
<img src={chevronDown} alt="" className="bds-mobile-menu__lang-pill-chevron" />
|
||||
</span>
|
||||
</button>
|
||||
{isLangOpen && locales.length >= 2 && (
|
||||
<div className="bds-lang-dropdown bds-lang-dropdown--mobile" role="menu">
|
||||
{locales.map((locale) => {
|
||||
const isActive = locale.code === currentLocale?.code;
|
||||
return (
|
||||
<button
|
||||
key={locale.code}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={`bds-lang-dropdown__item ${isActive ? 'bds-lang-dropdown__item--active' : ''}`}
|
||||
onClick={() => handleLanguageSelect(locale.code)}
|
||||
>
|
||||
{locale.name || locale.code}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="bds-mobile-menu__footer-icon" aria-label={translate("Toggle color mode")} onClick={onModeToggle}>
|
||||
<img src={modeToggleIcon} alt="" />
|
||||
</button>
|
||||
<button type="button" className="bds-mobile-menu__footer-icon" aria-label={translate("Search")} onClick={onSearch}>
|
||||
<img src={searchIcon} alt="" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
47
@theme/components/Navbar/mobile-menu/MobileMenuContent.tsx
Normal file
47
@theme/components/Navbar/mobile-menu/MobileMenuContent.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { MobileMenuSection } from "./MobileMenuSection";
|
||||
import { developSubmenuData, useCasesSubmenuData, communitySubmenuData, networkSubmenuData } from "../constants/navigation";
|
||||
import type { SubmenuItem, SubmenuItemWithChildren, NetworkSubmenuSection } from "../types";
|
||||
|
||||
export type MobileMenuKey = 'Develop' | 'Use Cases' | 'Community' | 'Network';
|
||||
|
||||
interface MobileMenuContentProps {
|
||||
/** Which menu section to render */
|
||||
menuKey: MobileMenuKey;
|
||||
}
|
||||
|
||||
/** Get flattened menu items based on menu key */
|
||||
function getMenuItems(menuKey: MobileMenuKey): (SubmenuItem | SubmenuItemWithChildren | NetworkSubmenuSection)[] {
|
||||
switch (menuKey) {
|
||||
case 'Develop':
|
||||
return [...developSubmenuData.left, ...developSubmenuData.right];
|
||||
case 'Use Cases':
|
||||
return [...useCasesSubmenuData.left, ...useCasesSubmenuData.right];
|
||||
case 'Community':
|
||||
return [...communitySubmenuData.left, ...communitySubmenuData.right];
|
||||
case 'Network':
|
||||
return networkSubmenuData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified Mobile Menu Content component.
|
||||
* Renders accordion content for any menu section.
|
||||
*/
|
||||
export function MobileMenuContent({ menuKey }: MobileMenuContentProps) {
|
||||
const items = getMenuItems(menuKey);
|
||||
|
||||
return (
|
||||
<div className="bds-mobile-menu__tier-list">
|
||||
{items.map((item) => (
|
||||
<MobileMenuSection key={item.label} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Backwards-compatible named exports
|
||||
export const MobileMenuDevelopContent = () => <MobileMenuContent menuKey="Develop" />;
|
||||
export const MobileMenuUseCasesContent = () => <MobileMenuContent menuKey="Use Cases" />;
|
||||
export const MobileMenuCommunityContent = () => <MobileMenuContent menuKey="Community" />;
|
||||
export const MobileMenuNetworkContent = () => <MobileMenuContent menuKey="Network" />;
|
||||
|
||||
54
@theme/components/Navbar/mobile-menu/MobileMenuSection.tsx
Normal file
54
@theme/components/Navbar/mobile-menu/MobileMenuSection.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as React from "react";
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { SubmenuArrow, SubmenuChildArrow } from "../icons";
|
||||
import { navIcons } from "../constants/icons";
|
||||
import { hasChildren, type SubmenuItem } from "../types";
|
||||
|
||||
interface MobileMenuSectionProps {
|
||||
item: SubmenuItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable mobile menu section component.
|
||||
* Renders a parent link with icon, and optionally child links.
|
||||
*/
|
||||
export function MobileMenuSection({ item }: MobileMenuSectionProps) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
const itemHasChildren = hasChildren(item);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<a href={item.href} className="bds-mobile-menu__tier1 bds-mobile-menu__parent-link">
|
||||
<span className="bds-mobile-menu__icon">
|
||||
<img src={navIcons[item.icon]} alt={translate(item.label)} />
|
||||
</span>
|
||||
<span className="bds-mobile-menu__link bds-mobile-menu__link--bold">
|
||||
{translate(item.label)}
|
||||
<span className="bds-mobile-menu__arrow">
|
||||
<SubmenuArrow />
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
{itemHasChildren && (
|
||||
<div className="bds-mobile-menu__tier2">
|
||||
{item.children.map((child) => (
|
||||
<a
|
||||
key={child.label}
|
||||
href={child.href}
|
||||
className="bds-mobile-menu__sublink"
|
||||
target={child.href.startsWith('http') ? '_blank' : undefined}
|
||||
rel={child.href.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
{translate(child.label)}
|
||||
<span className="bds-mobile-menu__sublink-arrow">
|
||||
<SubmenuChildArrow />
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
16
@theme/components/Navbar/mobile-menu/index.ts
Normal file
16
@theme/components/Navbar/mobile-menu/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Main mobile menu component
|
||||
export { MobileMenu } from './MobileMenu';
|
||||
|
||||
// Unified content component with backwards-compatible aliases
|
||||
export {
|
||||
MobileMenuContent,
|
||||
MobileMenuDevelopContent,
|
||||
MobileMenuUseCasesContent,
|
||||
MobileMenuCommunityContent,
|
||||
MobileMenuNetworkContent,
|
||||
type MobileMenuKey
|
||||
} from './MobileMenuContent';
|
||||
|
||||
// Reusable section component
|
||||
export { MobileMenuSection } from './MobileMenuSection';
|
||||
|
||||
16
@theme/components/Navbar/submenus/CommunitySubmenu.tsx
Normal file
16
@theme/components/Navbar/submenus/CommunitySubmenu.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Submenu } from "./Submenu";
|
||||
|
||||
interface CommunitySubmenuProps {
|
||||
isActive: boolean;
|
||||
isClosing: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop Community Submenu Component.
|
||||
* Wrapper for unified Submenu component with 'community' variant.
|
||||
*/
|
||||
export function CommunitySubmenu({ isActive, isClosing, onClose }: CommunitySubmenuProps) {
|
||||
return <Submenu variant="community" isActive={isActive} isClosing={isClosing} onClose={onClose} />;
|
||||
}
|
||||
|
||||
16
@theme/components/Navbar/submenus/DevelopSubmenu.tsx
Normal file
16
@theme/components/Navbar/submenus/DevelopSubmenu.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Submenu } from "./Submenu";
|
||||
|
||||
interface DevelopSubmenuProps {
|
||||
isActive: boolean;
|
||||
isClosing: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop Develop Submenu Component.
|
||||
* Wrapper for unified Submenu component with 'develop' variant.
|
||||
*/
|
||||
export function DevelopSubmenu({ isActive, isClosing, onClose }: DevelopSubmenuProps) {
|
||||
return <Submenu variant="develop" isActive={isActive} isClosing={isClosing} onClose={onClose} />;
|
||||
}
|
||||
|
||||
16
@theme/components/Navbar/submenus/NetworkSubmenu.tsx
Normal file
16
@theme/components/Navbar/submenus/NetworkSubmenu.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Submenu } from "./Submenu";
|
||||
|
||||
interface NetworkSubmenuProps {
|
||||
isActive: boolean;
|
||||
isClosing: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop Network Submenu Component.
|
||||
* Wrapper for unified Submenu component with 'network' variant.
|
||||
*/
|
||||
export function NetworkSubmenu({ isActive, isClosing, onClose }: NetworkSubmenuProps) {
|
||||
return <Submenu variant="network" isActive={isActive} isClosing={isClosing} onClose={onClose} />;
|
||||
}
|
||||
|
||||
277
@theme/components/Navbar/submenus/Submenu.tsx
Normal file
277
@theme/components/Navbar/submenus/Submenu.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import * as React from "react";
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { SubmenuSection } from "./SubmenuSection";
|
||||
import { ArrowIcon } from "../icons";
|
||||
import { navIcons, resourcesIconPattern, insightsIconPattern } from "../constants/icons";
|
||||
import { developSubmenuData, useCasesSubmenuData, communitySubmenuData, networkSubmenuData } from "../constants/navigation";
|
||||
import type { SubmenuItem, SubmenuItemWithChildren, NetworkSubmenuSection } from "../types";
|
||||
|
||||
export type SubmenuVariant = 'develop' | 'use-cases' | 'community' | 'network';
|
||||
|
||||
interface SubmenuProps {
|
||||
/** Which submenu variant to render */
|
||||
variant: SubmenuVariant;
|
||||
/** Whether this submenu is currently active (visible) */
|
||||
isActive: boolean;
|
||||
/** Whether this submenu is in closing animation */
|
||||
isClosing: boolean;
|
||||
/** Callback when submenu should close (e.g., Escape key) */
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/** Get submenu data based on variant */
|
||||
function getSubmenuData(variant: SubmenuVariant) {
|
||||
switch (variant) {
|
||||
case 'develop': return developSubmenuData;
|
||||
case 'use-cases': return useCasesSubmenuData;
|
||||
case 'community': return communitySubmenuData;
|
||||
case 'network': return networkSubmenuData;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get CSS modifier class for variant */
|
||||
function getVariantClass(variant: SubmenuVariant): string {
|
||||
if (variant === 'develop') return '';
|
||||
return `bds-submenu--${variant}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all focusable elements within a container
|
||||
*/
|
||||
function getFocusableElements(container: HTMLElement | null): HTMLElement[] {
|
||||
if (!container) return [];
|
||||
return Array.from(
|
||||
container.querySelectorAll<HTMLElement>('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next nav item button after the current expanded one
|
||||
*/
|
||||
function getNextNavItem(): HTMLElement | null {
|
||||
const navItems = document.querySelectorAll<HTMLElement>('.bds-navbar__item');
|
||||
const currentIndex = Array.from(navItems).findIndex(item =>
|
||||
item.getAttribute('aria-expanded') === 'true'
|
||||
);
|
||||
if (currentIndex >= 0 && currentIndex < navItems.length - 1) {
|
||||
return navItems[currentIndex + 1];
|
||||
}
|
||||
// If at the last nav item, go to the first control button (search, etc.)
|
||||
const controls = document.querySelector<HTMLElement>('.bds-navbar__controls button, .bds-navbar__controls a');
|
||||
return controls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified Submenu component.
|
||||
* Handles all submenu variants (develop, use-cases, community, network).
|
||||
* ARIA compliant with full keyboard navigation support.
|
||||
*/
|
||||
export function Submenu({ variant, isActive, isClosing, onClose }: SubmenuProps) {
|
||||
const submenuRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle keyboard events for accessibility
|
||||
const handleKeyDown = React.useCallback((event: KeyboardEvent) => {
|
||||
if (!isActive) return;
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onClose?.();
|
||||
// Return focus to the trigger button
|
||||
const triggerButton = document.querySelector<HTMLButtonElement>(
|
||||
`.bds-navbar__item[aria-expanded="true"]`
|
||||
);
|
||||
triggerButton?.focus();
|
||||
}
|
||||
|
||||
// Handle Tab at end of submenu - move to next nav item
|
||||
if (event.key === 'Tab' && !event.shiftKey) {
|
||||
const activeSubmenu = document.querySelector<HTMLElement>('.bds-submenu--active');
|
||||
const focusableElements = getFocusableElements(activeSubmenu);
|
||||
const lastFocusable = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (document.activeElement === lastFocusable) {
|
||||
event.preventDefault();
|
||||
onClose?.();
|
||||
const nextItem = getNextNavItem();
|
||||
nextItem?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Shift+Tab at start of submenu - move back to trigger button
|
||||
if (event.key === 'Tab' && event.shiftKey) {
|
||||
const activeSubmenu = document.querySelector<HTMLElement>('.bds-submenu--active');
|
||||
const focusableElements = getFocusableElements(activeSubmenu);
|
||||
const firstFocusable = focusableElements[0];
|
||||
|
||||
if (document.activeElement === firstFocusable) {
|
||||
event.preventDefault();
|
||||
onClose?.();
|
||||
// Return focus to the trigger button
|
||||
const triggerButton = document.querySelector<HTMLButtonElement>(
|
||||
`.bds-navbar__item[aria-expanded="true"]`
|
||||
);
|
||||
triggerButton?.focus();
|
||||
}
|
||||
}
|
||||
}, [isActive, onClose]);
|
||||
|
||||
// Add keyboard event listener when submenu is active
|
||||
React.useEffect(() => {
|
||||
if (isActive) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
}, [isActive, handleKeyDown]);
|
||||
|
||||
// Network submenu needs special handling for theme-aware patterns
|
||||
if (variant === 'network') {
|
||||
return <NetworkSubmenuContent isActive={isActive} isClosing={isClosing} onClose={onClose} />;
|
||||
}
|
||||
|
||||
const data = getSubmenuData(variant);
|
||||
const classNames = [
|
||||
'bds-submenu',
|
||||
getVariantClass(variant),
|
||||
isActive ? 'bds-submenu--active' : '',
|
||||
isClosing ? 'bds-submenu--closing' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
// Standard two-column layout
|
||||
const leftItems = 'left' in data ? data.left : [];
|
||||
const rightItems = 'right' in data ? data.right : [];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={submenuRef}
|
||||
className={classNames}
|
||||
role="menu"
|
||||
aria-hidden={!isActive}
|
||||
>
|
||||
<div className="bds-submenu__left">
|
||||
{leftItems.map((item: SubmenuItem | SubmenuItemWithChildren) => (
|
||||
<SubmenuSection key={item.label} item={item} />
|
||||
))}
|
||||
</div>
|
||||
<div className="bds-submenu__right">
|
||||
{rightItems.map((item: SubmenuItem | SubmenuItemWithChildren) => (
|
||||
<SubmenuSection key={item.label} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Network submenu with pattern images (same for light and dark mode) */
|
||||
function NetworkSubmenuContent({ isActive, isClosing, onClose }: { isActive: boolean; isClosing: boolean; onClose?: () => void }) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
|
||||
// Handle keyboard events for accessibility
|
||||
const handleKeyDown = React.useCallback((event: KeyboardEvent) => {
|
||||
if (!isActive) return;
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onClose?.();
|
||||
// Return focus to the trigger button
|
||||
const triggerButton = document.querySelector<HTMLButtonElement>(
|
||||
`.bds-navbar__item[aria-expanded="true"]`
|
||||
);
|
||||
triggerButton?.focus();
|
||||
}
|
||||
|
||||
// Handle Tab at end of submenu - move to next nav item
|
||||
if (event.key === 'Tab' && !event.shiftKey) {
|
||||
const activeSubmenu = document.querySelector<HTMLElement>('.bds-submenu--active');
|
||||
const focusableElements = getFocusableElements(activeSubmenu);
|
||||
const lastFocusable = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (document.activeElement === lastFocusable) {
|
||||
event.preventDefault();
|
||||
onClose?.();
|
||||
const nextItem = getNextNavItem();
|
||||
nextItem?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Shift+Tab at start of submenu - move back to trigger button
|
||||
if (event.key === 'Tab' && event.shiftKey) {
|
||||
const activeSubmenu = document.querySelector<HTMLElement>('.bds-submenu--active');
|
||||
const focusableElements = getFocusableElements(activeSubmenu);
|
||||
const firstFocusable = focusableElements[0];
|
||||
|
||||
if (document.activeElement === firstFocusable) {
|
||||
event.preventDefault();
|
||||
onClose?.();
|
||||
// Return focus to the trigger button
|
||||
const triggerButton = document.querySelector<HTMLButtonElement>(
|
||||
`.bds-navbar__item[aria-expanded="true"]`
|
||||
);
|
||||
triggerButton?.focus();
|
||||
}
|
||||
}
|
||||
}, [isActive, onClose]);
|
||||
|
||||
// Add keyboard event listener when submenu is active
|
||||
React.useEffect(() => {
|
||||
if (isActive) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
}, [isActive, handleKeyDown]);
|
||||
|
||||
// Use same pattern images for both light and dark mode
|
||||
const patternImages = {
|
||||
lilac: resourcesIconPattern,
|
||||
green: insightsIconPattern,
|
||||
};
|
||||
|
||||
const classNames = [
|
||||
'bds-submenu',
|
||||
'bds-submenu--network',
|
||||
isActive ? 'bds-submenu--active' : '',
|
||||
isClosing ? 'bds-submenu--closing' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div className={classNames} role="menu" aria-hidden={!isActive}>
|
||||
{networkSubmenuData.map((section: NetworkSubmenuSection) => (
|
||||
<div key={section.label} className="bds-submenu__section">
|
||||
<a href={section.href} className="bds-submenu__tier1 bds-submenu__parent-link">
|
||||
<span className="bds-submenu__icon">
|
||||
<img src={navIcons[section.icon]} alt={translate(section.label)} />
|
||||
</span>
|
||||
<span className="bds-submenu__link bds-submenu__link--bold">
|
||||
{translate(section.label)}
|
||||
<span className="bds-submenu__arrow">
|
||||
<ArrowIcon animated />
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<div className="bds-submenu__network-content">
|
||||
<div className="bds-submenu__tier2">
|
||||
{section.children.map((child) => (
|
||||
<a
|
||||
key={child.label}
|
||||
href={child.href}
|
||||
className="bds-submenu__sublink"
|
||||
target={child.href.startsWith('http') ? '_blank' : undefined}
|
||||
rel={child.href.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
{translate(child.label)}
|
||||
<span className="bds-submenu__sublink-arrow">
|
||||
<ArrowIcon animated={false} />
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="bds-submenu__pattern-container">
|
||||
<img src={patternImages[section.patternColor]} alt="" className="bds-submenu__pattern" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
70
@theme/components/Navbar/submenus/SubmenuSection.tsx
Normal file
70
@theme/components/Navbar/submenus/SubmenuSection.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { ArrowIcon } from "../icons";
|
||||
import { navIcons } from "../constants/icons";
|
||||
import { hasChildren, type SubmenuItem, type SubmenuItemWithChildren, type SubmenuItemBase } from "../types";
|
||||
|
||||
interface SubmenuSectionProps {
|
||||
/** The menu item data */
|
||||
item: SubmenuItem | SubmenuItemWithChildren | SubmenuItemBase;
|
||||
/** Whether to render children links (default: true) */
|
||||
showChildren?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified submenu section component.
|
||||
* Renders a parent link with icon, and optionally child links if the item has them.
|
||||
*
|
||||
* Usage:
|
||||
* - For items that may or may not have children: <SubmenuSection item={item} />
|
||||
* - For parent-only rendering: <SubmenuSection item={item} showChildren={false} />
|
||||
*/
|
||||
export function SubmenuSection({ item, showChildren = true }: SubmenuSectionProps) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
const itemHasChildren = hasChildren(item as SubmenuItem);
|
||||
const shouldShowChildren = showChildren && itemHasChildren;
|
||||
|
||||
return (
|
||||
<div className="bds-submenu__section">
|
||||
<a href={item.href} className="bds-submenu__tier1 bds-submenu__parent-link">
|
||||
<span className="bds-submenu__icon">
|
||||
<img src={navIcons[item.icon]} alt={translate(item.label)} />
|
||||
</span>
|
||||
<span className="bds-submenu__link bds-submenu__link--bold">
|
||||
{translate(item.label)}
|
||||
<span className="bds-submenu__arrow">
|
||||
<ArrowIcon animated />
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
{shouldShowChildren && (
|
||||
<div className="bds-submenu__tier2">
|
||||
{(item as SubmenuItemWithChildren).children.map((child) => (
|
||||
<a
|
||||
key={child.label}
|
||||
href={child.href}
|
||||
className="bds-submenu__sublink"
|
||||
target={child.href.startsWith('http') ? '_blank' : undefined}
|
||||
rel={child.href.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
{translate(child.label)}
|
||||
<span className="bds-submenu__sublink-arrow">
|
||||
<ArrowIcon animated={false} />
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Backwards-compatible aliases (all use the unified SubmenuSection)
|
||||
export const SubmenuParentOnly = ({ item }: { item: SubmenuItemBase }) => (
|
||||
<SubmenuSection item={item} showChildren={false} />
|
||||
);
|
||||
|
||||
export const SubmenuWithChildren = ({ item }: { item: SubmenuItemWithChildren }) => (
|
||||
<SubmenuSection item={item} showChildren={true} />
|
||||
);
|
||||
|
||||
16
@theme/components/Navbar/submenus/UseCasesSubmenu.tsx
Normal file
16
@theme/components/Navbar/submenus/UseCasesSubmenu.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Submenu } from "./Submenu";
|
||||
|
||||
interface UseCasesSubmenuProps {
|
||||
isActive: boolean;
|
||||
isClosing: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop Use Cases Submenu Component.
|
||||
* Wrapper for unified Submenu component with 'use-cases' variant.
|
||||
*/
|
||||
export function UseCasesSubmenu({ isActive, isClosing, onClose }: UseCasesSubmenuProps) {
|
||||
return <Submenu variant="use-cases" isActive={isActive} isClosing={isClosing} onClose={onClose} />;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user